Skip to content

Commit 8bbb682

Browse files
authored
Isolate from other tests (#80)
1 parent 2950835 commit 8bbb682

File tree

8 files changed

+109
-41
lines changed

8 files changed

+109
-41
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Support Node.js 24 ([#67](https://github.com/cucumber/cucumber-node/pull/67))
1111

12+
### Fixed
13+
- Fully isolate Cucumber tests from other tests for accurate reporting ([#80](https://github.com/cucumber/cucumber-node/pull/80))
14+
1215
### Removed
1316
- Drop support for Node.js 23 ([#67](https://github.com/cucumber/cucumber-node/pull/67))
1417

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,11 @@ Some Cucumber formatters are included as Node.js test reporters:
149149
- JUnit `--test-reporter=@cucumber/node/reporters/junit --test-reporter-destination=./TEST-cucumber.xml`
150150
- Message `--test-reporter=@cucumber/node/reporters/message --test-reporter-destination=./messages.ndjson`
151151

152-
There are some caveats that apply when using these reporters (but not otherwise):
152+
For now, avoid using the `spec` reporter at the same time as one of the above reporters - because we're abusing the `diagnostic` channel to send messages to the reporter, it makes the `spec` output very noisy - we recommend the `dot` reporter instead.
153153

154-
- *Don't mix Cucumber tests with other tests* - if you have non-Cucumber tests to run with `node --test`, you should do that in a separate run.
155-
- *Don't use the `spec` reporter at the same time* - because we're abusing the `diagnostic` channel to send messages to the reporter, it makes the `spec` output very noisy - we recommend the `dot` reporter instead.
154+
## Mixing tests
155+
156+
You can execute Cucumber tests and normal JavaScript tests in the same test run - cucumber-node won't interfere with the other tests. But the reporters mentioned above will only report on the Cucumber tests in your run.
156157

157158
## Limitations
158159

package-lock.json

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"devDependencies": {
4343
"@cucumber/compatibility-kit": "^18.0.3",
44+
"@cucumber/query": "^13.5.0",
4445
"@eslint/compat": "^1.2.5",
4546
"@eslint/eslintrc": "^3.2.0",
4647
"@eslint/js": "^9.18.0",

src/reporters/generateEnvelopes.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import path from 'node:path'
12
import { TestEvent } from 'node:test/reporters'
23

3-
import { Envelope, TestRunStarted, TestStepFinished } from '@cucumber/messages'
4+
import {
5+
Envelope,
6+
TestRunStarted,
7+
TestStepFinished,
8+
TestStepResult,
9+
TestStepResultStatus,
10+
} from '@cucumber/messages'
411

512
import { makeId } from '../makeId.js'
613
import { makeTimestamp } from '../makeTimestamp.js'
@@ -13,11 +20,11 @@ export async function* generateEnvelopes(
1320
): AsyncGenerator<Envelope> {
1421
yield { meta }
1522

23+
const nodeFailOrPassEvents: Array<TestFail | TestPass> = []
24+
const testStepFinishedMessages: Array<TestStepFinished> = []
25+
const nonSuccessTestCaseStartedIds: Array<string> = []
1626
const testRunEnvelopes: Array<Envelope> = []
17-
const testStepFinishedOrder: Array<TestStepFinished> = []
18-
const testStepFinishEvents: Array<TestFail | TestPass> = []
1927

20-
let success: boolean = false
2128
const testRunStarted: TestRunStarted = {
2229
id: makeId(),
2330
timestamp: makeTimestamp(),
@@ -27,15 +34,12 @@ export async function* generateEnvelopes(
2734
switch (event.type) {
2835
case 'test:fail':
2936
case 'test:pass':
30-
if (event.data.nesting === 2) {
31-
testStepFinishEvents.push(event.data)
37+
if (isFromHere(event.data) && event.data.nesting === 2) {
38+
nodeFailOrPassEvents.push(event.data)
3239
}
3340
break
34-
case 'test:summary':
35-
success = event.data.success
36-
break
3741
case 'test:diagnostic':
38-
if (isEnvelope(event.data.message)) {
42+
if (isFromHere(event.data) && isEnvelope(event.data.message)) {
3943
const envelope = fromPrefixed(event.data.message)
4044
for (const key of Object.keys(envelope) as ReadonlyArray<keyof Envelope>) {
4145
switch (key) {
@@ -52,10 +56,13 @@ export async function* generateEnvelopes(
5256
case 'testStepFinished':
5357
{
5458
const testStepFinished = envelope.testStepFinished!
55-
testStepFinishedOrder.push(testStepFinished)
59+
testStepFinishedMessages.push(testStepFinished)
5660
testStepFinished.testStepResult = mapTestStepResult(
57-
testStepFinishEvents.at(testStepFinishedOrder.indexOf(testStepFinished))
61+
nodeFailOrPassEvents.at(testStepFinishedMessages.indexOf(testStepFinished))
5862
)
63+
if (isNonSuccess(testStepFinished.testStepResult)) {
64+
nonSuccessTestCaseStartedIds.push(testStepFinished.testCaseStartedId)
65+
}
5966
testRunEnvelopes.push(envelope)
6067
}
6168
break
@@ -74,7 +81,7 @@ export async function* generateEnvelopes(
7481
const testRunFinished = {
7582
testRunStartedId: testRunStarted.id,
7683
timestamp: makeTimestamp(),
77-
success,
84+
success: nonSuccessTestCaseStartedIds.length === 0,
7885
}
7986

8087
yield { testRunStarted }
@@ -84,10 +91,22 @@ export async function* generateEnvelopes(
8491
yield { testRunFinished }
8592
}
8693

94+
function isFromHere(testLocationInfo: TestLocationInfo) {
95+
return testLocationInfo.file?.startsWith(path.join(import.meta.dirname, '..'))
96+
}
97+
8798
function isEnvelope(data: string) {
8899
return data.startsWith(PROTOCOL_PREFIX)
89100
}
90101

91102
function fromPrefixed(data: string): Envelope {
92103
return JSON.parse(data.substring(PROTOCOL_PREFIX.length))
93104
}
105+
106+
function isNonSuccess(testStepResult: TestStepResult) {
107+
return ![
108+
TestStepResultStatus.UNKNOWN,
109+
TestStepResultStatus.PASSED,
110+
TestStepResultStatus.SKIPPED,
111+
].includes(testStepResult.status)
112+
}

test/cck/cck.spec.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,6 @@ const UNSUPPORTED = [
4242
'retry', // node:test doesnt support retries yet
4343
]
4444

45-
const OVERRIDES: Record<string, (actual: ReadonlyArray<Envelope>) => ReadonlyArray<Envelope>> = {
46-
pending: (actual) => {
47-
// node:test treats pending (aka todo) as a pass
48-
return actual.map((envelope) => {
49-
if (envelope.testRunFinished) {
50-
return {
51-
testRunFinished: {
52-
...envelope.testRunFinished,
53-
success: false,
54-
},
55-
}
56-
}
57-
return envelope
58-
})
59-
},
60-
}
61-
6245
describe('Cucumber Compatibility Kit', () => {
6346
const ndjsonPaths = globSync('node_modules/@cucumber/compatibility-kit/features/**/*.ndjson')
6447
for (const ndjsonPath of ndjsonPaths) {
@@ -82,10 +65,7 @@ describe('Cucumber Compatibility Kit', () => {
8265
const [actualOutput] = await harness.run('@cucumber/node/reporters/message')
8366
const expectedOutput = await readFile(ndjsonPath, { encoding: 'utf-8' })
8467

85-
let actualEnvelopes = parseEnvelopes(actualOutput)
86-
if (OVERRIDES[name]) {
87-
actualEnvelopes = OVERRIDES[name](actualEnvelopes)
88-
}
68+
const actualEnvelopes = parseEnvelopes(actualOutput)
8969
const expectedEnvelopes = parseEnvelopes(expectedOutput)
9070

9171
// first assert on the order and type of messages

test/integration/reporters.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { stripVTControlCharacters } from 'node:util'
22
import { expect } from 'chai'
33
import { makeTestHarness } from '../utils.js'
44
import path from 'node:path'
5+
import { TestStepResultStatus } from '@cucumber/messages'
56

67
describe('Reporters', () => {
78
describe('spec', () => {
@@ -61,6 +62,47 @@ Given('a step', () => {})`
6162
})
6263
})
6364

65+
describe('message', () => {
66+
it('only factors cucumber tests into results', async () => {
67+
const harness = await makeTestHarness()
68+
await harness.writeFile(
69+
'features/first.feature',
70+
`Feature:
71+
Scenario:
72+
Given a step
73+
`
74+
)
75+
await harness.writeFile(
76+
'features/steps.js',
77+
`import { Given } from '@cucumber/node'
78+
Given('a step', () => {})
79+
`
80+
)
81+
await harness.writeFile(
82+
'example.test.mjs',
83+
`import test from 'node:test'
84+
test('top level', (t) => {
85+
test('next level', (t1) => {
86+
t1.test('failing test', (t2) => {
87+
t2.assert.strictEqual(1, 2)
88+
})
89+
})
90+
})
91+
`
92+
)
93+
94+
const query = await harness.collectMessages()
95+
96+
expect(query.findAllTestCaseStarted().length).to.eq(1)
97+
expect(
98+
query
99+
.findAllTestCaseStarted()
100+
.map((testCaseStarted) => query.findMostSevereTestStepResultBy(testCaseStarted)?.status)
101+
).to.deep.eq([TestStepResultStatus.PASSED])
102+
expect(query.findTestRunFinished()?.success).to.be.true
103+
})
104+
})
105+
64106
describe('junit', () => {
65107
it('outputs a junit xml report', async () => {
66108
const harness = await makeTestHarness()

test/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { copyFile, cp, mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promis
22
import path from 'node:path'
33
import { tmpdir } from 'node:os'
44
import { exec } from 'node:child_process'
5+
import { Query } from '@cucumber/query'
6+
import { Envelope } from '@cucumber/messages'
57

68
class TestHarness {
79
constructor(private readonly tempDir: string) {}
@@ -34,6 +36,7 @@ class TestHarness {
3436
`--test-reporter-destination=stdout`,
3537
...extraArgs,
3638
`--test`,
39+
`"*.test.mjs"`,
3740
`"features/**/*.feature"`,
3841
`"features/**/*.feature.md"`,
3942
].join(' '),
@@ -46,6 +49,17 @@ class TestHarness {
4649
)
4750
})
4851
}
52+
53+
async collectMessages(): Promise<Query> {
54+
const query = new Query()
55+
const [output] = await this.run('@cucumber/node/reporters/message')
56+
output
57+
.trim()
58+
.split('\n')
59+
.map((line) => JSON.parse(line) as Envelope)
60+
.forEach((envelope) => query.update(envelope))
61+
return query
62+
}
4963
}
5064

5165
export async function makeTestHarness() {

0 commit comments

Comments
 (0)