Skip to content

Commit aac2fbb

Browse files
implemented html streaming (#50)
* implemented html streaming
1 parent 7f25042 commit aac2fbb

File tree

7 files changed

+1210
-1181
lines changed

7 files changed

+1210
-1181
lines changed

CHANGELOG.MD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 0.17.0
2+
- implemented runtime streaming
3+
14
# 0.16.0
25
- added dark theme
36

formatter/formatter.js

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,99 @@
1-
const JsonFormatter = require('./json_formatter');
2-
const fs = require('fs');
3-
const path = require('path');
4-
class HTMLFormatter extends JsonFormatter {
1+
const { Formatter } = require('@cucumber/cucumber');
2+
const { write, readFileSync } = require('node:fs');
3+
const path = require('node:path');
4+
5+
class HTMLStream {
6+
7+
constructor(fd, ender) {
8+
this.fd = fd;
9+
this.position = 0;
10+
this.ender = Buffer.from(ender);
11+
}
12+
13+
write(data) {
14+
const dataBuffer = Buffer.from(data);
15+
const buffer = Buffer.concat([dataBuffer, this.ender]);
16+
write(this.fd, buffer, 0, buffer.length, this.position, () => {});
17+
this.position += dataBuffer.length;
18+
}
19+
20+
}
21+
22+
class HTMLFormatter extends Formatter {
23+
24+
hooks = {};
525

626
constructor(options) {
727
super(options);
8-
this.metadata = options.parsedArgvOptions.htmlConfig?.metadata ?? {};
9-
const log = this.log.bind(this);
10-
this.log = function(json) {
11-
const htmlTemplate = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8');
12-
log(htmlTemplate
13-
.replace('METADATA', () => JSON.stringify(this.metadata))
14-
.replace('SOURCE_DATA', () => JSON.stringify(JSON.parse(json))));
28+
const metadata = JSON.stringify(options.parsedArgvOptions.htmlConfig?.metadata ?? {});
29+
const htmlTemplate = readFileSync(path.resolve(__dirname, './index.html'), 'utf-8')
30+
.replace('METADATA', metadata);
31+
const [left, right] = htmlTemplate.split('SOURCE_DATA');
32+
this.htmlStream = new HTMLStream(this.stream.fd, right);
33+
this.htmlStream.write(left);
34+
options.eventBroadcaster.on('envelope', this.processEnvelope.bind(this));
35+
}
36+
37+
processEnvelope(envelope) {
38+
if (envelope.testCaseFinished) {
39+
return this.finishTest(envelope);
40+
}
41+
if (envelope.hook) {
42+
return this.hooks[envelope.hook.id] = envelope.hook;
43+
}
44+
}
45+
46+
finishTest(envelope) {
47+
if (envelope.testCaseFinished.willBeRetried) return;
48+
const testCase = this.eventDataCollector.getTestCaseAttempt(envelope.testCaseFinished.testCaseStartedId);
49+
const feature = {
50+
description: testCase.gherkinDocument.feature.description,
51+
id: 'feature' + testCase.pickle.id,
52+
line: testCase.gherkinDocument.feature.location.line,
53+
keyword: testCase.gherkinDocument.feature.keyword,
54+
name: testCase.gherkinDocument.feature.name,
55+
uri: testCase.gherkinDocument.uri,
56+
tags: this.formatTags(testCase.gherkinDocument.feature.tags)
57+
};
58+
59+
const steps = testCase.testCase.testSteps;
60+
for (const step of steps) {
61+
const pickle = testCase.pickle.steps.find(pickle => step.pickleStepId === pickle.id);
62+
step.name = pickle ? pickle.text : this.hookText(steps, step);
63+
step.arguments = pickle ? [{ ...(pickle.argument?.dataTable ?? pickle.argument?.docString) }] : undefined;
64+
const result = testCase.stepResults[step.id];
65+
step.result = {
66+
status: result.status.toLowerCase(),
67+
duration: result.duration.seconds * 1_000_000_000 + result.duration.nanos,
68+
error_message: result.message
69+
};
70+
step.embeddings = testCase.stepAttachments[step.id]?.map(attachment => ({
71+
...attachment,
72+
data: attachment.body,
73+
mime_type: attachment.mediaType
74+
}));
1575
}
76+
const scenario = {
77+
feature,
78+
steps,
79+
name: testCase.pickle.name,
80+
id: testCase.pickle.id,
81+
keyword: 'Scenario',
82+
tags: this.formatTags(testCase.pickle.tags),
83+
type: 'scenario'
84+
};
85+
this.htmlStream.write(JSON.stringify(scenario) + ',');
86+
}
87+
88+
formatTags(tags) {
89+
return tags.map(tag => ({ name: tag.name, line: tag.location?.line }));
90+
}
91+
92+
hookText(steps, step) {
93+
const hook = this.hooks[step.hookId];
94+
if (hook?.name) return hook.name;
95+
const stepsBefore = steps.slice(0, steps.findIndex((element) => element === step));
96+
return stepsBefore.every(element => element.pickleStepId === undefined) ? 'Before' : 'After';
1697
}
1798

1899
}

formatter/json_formatter.js

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { Formatter } = require('@cucumber/cucumber');
2+
23
class JsonFormatter extends Formatter {
34

45
constructor(options) {
@@ -22,26 +23,22 @@ class JsonFormatter extends Formatter {
2223
finishTest(envelope) {
2324
if (envelope.testCaseFinished.willBeRetried) return;
2425
const testCase = this.eventDataCollector.getTestCaseAttempt(envelope.testCaseFinished.testCaseStartedId);
25-
let feature = this.json.find(feature => feature.name === testCase.gherkinDocument.feature.name);
26-
if (!feature) {
27-
feature = {
28-
description: testCase.gherkinDocument.feature.description,
29-
id: 'feature' + testCase.pickle.id,
30-
line: testCase.gherkinDocument.feature.location.line,
31-
keyword: testCase.gherkinDocument.feature.keyword,
32-
name: testCase.gherkinDocument.feature.name,
33-
uri: testCase.gherkinDocument.uri,
34-
elements: [],
35-
tags: this.formatTags(testCase.gherkinDocument.feature.tags)
36-
};
37-
this.json.push(feature);
38-
}
26+
const feature = {
27+
description: testCase.gherkinDocument.feature.description,
28+
id: 'feature' + testCase.pickle.id,
29+
line: testCase.gherkinDocument.feature.location.line,
30+
keyword: testCase.gherkinDocument.feature.keyword,
31+
name: testCase.gherkinDocument.feature.name,
32+
uri: testCase.gherkinDocument.uri,
33+
elements: [],
34+
tags: this.formatTags(testCase.gherkinDocument.feature.tags)
35+
};
3936
const steps = testCase.testCase.testSteps;
4037
for (const step of steps) {
4138
const pickle = testCase.pickle.steps.find(pickle => step.pickleStepId === pickle.id);
4239
step.name = pickle ? pickle.text : this.hookText(steps, step);
4340
step.arguments = pickle ? [{ ...(pickle.argument?.dataTable ?? pickle.argument?.docString) }] : undefined;
44-
const result= testCase.stepResults[step.id];
41+
const result = testCase.stepResults[step.id];
4542
step.result = {
4643
status: result.status.toLowerCase(),
4744
duration: result.duration.seconds * 1_000_000_000 + result.duration.nanos,
@@ -55,28 +52,29 @@ class JsonFormatter extends Formatter {
5552
}
5653
const scenario = {
5754
steps,
55+
feature,
5856
name: testCase.pickle.name,
5957
id: testCase.pickle.id,
6058
keyword: 'Scenario',
6159
tags: this.formatTags(testCase.pickle.tags),
6260
type: 'scenario'
63-
}
64-
feature.elements.push(scenario);
61+
};
62+
this.json.push(scenario);
6563
}
6664

6765
finishLaunch() {
68-
this.log(JSON.stringify(this.json, null, 2))
66+
this.log(JSON.stringify(this.json, null, 2));
6967
}
7068

7169
formatTags(tags) {
72-
return tags.map(tag => ({ name: tag.name, line: tag.location?.line }))
70+
return tags.map(tag => ({ name: tag.name, line: tag.location?.line }));
7371
}
7472

7573
hookText(steps, step) {
7674
const hook = this.hooks[step.hookId];
7775
if (hook?.name) return hook.name;
7876
const stepsBefore = steps.slice(0, steps.findIndex((element) => element === step));
79-
return stepsBefore.every(element => element.pickleStepId === undefined) ? 'Before' : 'After'
77+
return stepsBefore.every(element => element.pickleStepId === undefined) ? 'Before' : 'After';
8078
}
8179
}
8280

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<div id="root"></div>
1111
<script>
1212
window.metaSourceData = METADATA;
13-
window.sourceData = SOURCE_DATA;
13+
window.sourceData = [SOURCE_DATA];
1414
</script>
1515
<script type="module" src="/src/index.tsx"></script>
1616
</body>

0 commit comments

Comments
 (0)