diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49178bf..ae675ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,18 @@ jobs: path: d2l-test-report-mocha.json archive: false if-no-files-found: ignore + - name: Upload test report (node) + if: > + !cancelled() && ( + steps.tests.conclusion == 'failure' || + steps.tests.outcome == 'success' + ) + id: upload-node + uses: Brightspace/third-party-actions@actions/upload-artifact + with: + path: d2l-test-report-node.json + archive: false + if-no-files-found: ignore - name: Upload test report (playwright) if: > !cancelled() && ( @@ -226,6 +238,7 @@ jobs: REPORT_ARTIFACT_URLS: > { "mocha": "${{steps.upload-mocha.outputs.artifact-url}}", + "node": "${{steps.upload-node.outputs.artifact-url}}", "playwright": "${{steps.upload-playwright.outputs.artifact-url}}", "@web/test-runner": "${{steps.upload-web-test-runner.outputs.artifact-url}}", "webdriverio": "${{steps.upload-webdriverio.outputs.artifact-url}}" diff --git a/README.md b/README.md index 4f52ded..01516e4 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Each reporter wraps your test framework and emits a D2L test report JSON file when the test run completes. The following frameworks are supported. * [Mocha] +* [Node.js Test Runner] * [Playwright] * [Web Test Runner] * [WebdriverIO] @@ -131,6 +132,7 @@ refer to the [semantic-release GitHub Action] documentation. [#test-reporting]: https://d2l.slack.com/archives/C05MMC7H7EK [semantic-release GitHub Action]: https://github.com/BrightspaceUI/actions/tree/main/semantic-release [Mocha]: ./docs/reporters/mocha.md +[Node.js Test Runner]: ./docs/reporters/node.md [Playwright]: ./docs/reporters/playwright.md [Web Test Runner]: ./docs/reporters/web-test-runner.md [WebdriverIO]: ./docs/reporters/webdriverio.md diff --git a/docs/report-builder.md b/docs/report-builder.md index 57e60d6..843de09 100644 --- a/docs/report-builder.md +++ b/docs/report-builder.md @@ -3,8 +3,8 @@ ## When to Use Use the `ReportBuilder` class when your test framework isn't supported by one of -the existing reporters ([Mocha Reporter], [Playwright Reporter], [Web Test -Runner Reporter], [WebdriverIO Reporter]). +the existing reporters ([Mocha Reporter], [Node.js Test Runner Reporter], +[Playwright Reporter], [Web Test Runner Reporter], [WebdriverIO Reporter]). ## Quick Start @@ -239,6 +239,7 @@ report.finalize().save(); [Mocha Reporter]: ./reporters/mocha.md +[Node.js Test Runner Reporter]: ./reporters/node.md [Playwright Reporter]: ./reporters/playwright.md [Web Test Runner Reporter]: ./reporters/web-test-runner.md [WebdriverIO Reporter]: ./reporters/webdriverio.md diff --git a/docs/reporters/node.md b/docs/reporters/node.md new file mode 100644 index 0000000..2e5a2b8 --- /dev/null +++ b/docs/reporters/node.md @@ -0,0 +1,104 @@ +# Node.js Test Runner Reporter + +Please consult the [official documentation for the Node.js test runner] to see +how to use [custom reporters]. + +> [!NOTE] +> This is a simplified example. Update values and options to match your +> specific project setup. + +## CLI Usage + +The reporter has a default export that works directly with the +`--test-reporter` flag. Pair it with another reporter (such as `spec`) to keep +human readable output on the console. + +```console +node --test \ + --test-reporter=spec --test-reporter-destination=stdout \ + --test-reporter=d2l-test-reporting/reporters/node.js --test-reporter-destination=stdout +``` + +> [!NOTE] +> Node.js requires a `--test-reporter-destination` for every `--test-reporter` +> when more than one reporter is used. The reporter writes the report file +> itself (see `reportPath` under [Inputs]), so its destination receives no +> output and `stdout` acts as a harmless placeholder. + +Options are read from environment variables since the Node.js test runner does +not forward options to custom reporters. See [Inputs] for the available +variables. + +## Programmatic Usage + +For finer control, use the named `reporter` factory with the [`run()`] API and +compose it onto the test stream. + +```js +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { reporter } from 'd2l-test-reporting/reporters/node.js'; +import { run } from 'node:test'; + +const testDirectory = 'test'; +const files = readdirSync(testDirectory) + .filter(name => name.endsWith('.test.js')) + .map(name => join(testDirectory, name)); + +const stream = run({ files }).compose(reporter()); + +stream.on('data', () => {}); +stream.on('error', () => {}); +``` + +## Inputs + +The `reporter` factory accepts the following options. When running through the +CLI, the corresponding environment variable is used instead. + +* `reportPath` / `D2L_TEST_REPORTING_REPORT_PATH` (default: + `./d2l-test-report.json`): Path to output the report to, relative to current + working directory. +* `reportConfigurationPath` / `D2L_TEST_REPORTING_REPORT_CONFIGURATION_PATH` + (default: `./d2l-test-reporting.config.json`): Path to the D2L test reporting + configuration file for mapping test type and tool to test code. +* `verbose` / `D2L_TEST_REPORTING_VERBOSE` (default: `false`): Enable verbose + logging for debugging purposes. + +## Full Example + +The defaults for all optional inputs work well for most setups. The example +below shows every available option explicitly set, and is intended as a +reference if any values need to be customized. + +```js +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { reporter } from 'd2l-test-reporting/reporters/node.js'; +import { run } from 'node:test'; + +const testDirectory = 'test'; +const files = readdirSync(testDirectory) + .filter(name => name.endsWith('.test.js')) + .map(name => join(testDirectory, name)); +const stream = run({ files }).compose(reporter({ + reportPath: './d2l-test-report.json', + reportConfigurationPath: './d2l-test-reporting.config.json', + verbose: true +})); + +stream.on('data', () => {}); +stream.on('error', () => {}); +``` + +> [!NOTE] +> The Node.js test runner does not expose the configured timeout for a test, so +> each test detail omits the `timeout` value under `configuration`. It also has +> no built-in retry mechanism, so each test detail always reports `retries` as +> `0` and no test is ever counted as `flaky`. + + +[official documentation for the Node.js test runner]: https://nodejs.org/api/test.html +[custom reporters]: https://nodejs.org/api/test.html#custom-reporters +[Inputs]: #inputs +[`run()`]: https://nodejs.org/api/test.html#runoptions diff --git a/package.json b/package.json index c5f0f80..900b318 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "./helpers/report.js": "./src/helpers/report.cjs", "./helpers/report-configuration.js": "./src/helpers/report-configuration.cjs", "./reporters/mocha.js": "./src/reporters/mocha.cjs", + "./reporters/node.js": "./src/reporters/node.js", "./reporters/playwright.js": "./src/reporters/playwright.js", "./reporters/web-test-runner.js": "./src/reporters/web-test-runner.js", "./reporters/webdriverio.js": "./src/reporters/webdriverio.cjs" @@ -37,6 +38,7 @@ "test:all": "c8 run-s test:unit test:integration", "test:integration": "run-s test:integration:* test:integration:validate-reports", "test:integration:mocha": "mocha --config test/integration/data/configs/mocha.cjs || exit 0", + "test:integration:node": "node test/integration/data/configs/node.js || exit 0", "test:integration:playwright": "playwright test --config test/integration/data/configs/playwright.js || exit 0", "test:integration:web-test-runner": "wtr --config test/integration/data/configs/web-test-runner.js || exit 0", "test:integration:webdriverio": "wdio run test/integration/data/configs/webdriverio.cjs || exit 0", diff --git a/src/reporters/node.js b/src/reporters/node.js new file mode 100644 index 0000000..3789aac --- /dev/null +++ b/src/reporters/node.js @@ -0,0 +1,227 @@ +import { createRequire } from 'node:module'; +import { Transform } from 'node:stream'; + +const require = createRequire(import.meta.url); + +const { ReportBuilder } = require('../helpers/report-builder.cjs'); +const { escapeSpecialCharacters } = require('../helpers/strings.cjs'); +const { getNow, getNowISOString } = require('../helpers/system.cjs'); + +class NodeTestLogger { + info(message) { + const lines = `${message}`.split(/\r?\n/u); + + for (const line of lines) { + console.log(`[D2L Reporter] ${line}`); + } + } + + warning(message) { + console.warn(`[D2L Reporter] ${message}`); + } + + error(message) { + console.error(`[D2L Reporter] ${message}`); + } + + location(message, location) { + this.info(`${message}: ${location}`); + } +} + +const sanitizeName = (name) => { + return escapeSpecialCharacters(`${name}`).trim(); +}; + +const makeDetailId = (file, name) => { + return `${file}[${name}]`; +}; + +const getOptionsFromEnvironment = () => { + const { + D2L_TEST_REPORTING_REPORT_PATH, + D2L_TEST_REPORTING_REPORT_CONFIGURATION_PATH, + D2L_TEST_REPORTING_VERBOSE + } = process.env; + const options = {}; + + if (D2L_TEST_REPORTING_REPORT_PATH) { + options.reportPath = D2L_TEST_REPORTING_REPORT_PATH; + } + + if (D2L_TEST_REPORTING_REPORT_CONFIGURATION_PATH) { + options.reportConfigurationPath = D2L_TEST_REPORTING_REPORT_CONFIGURATION_PATH; + } + + if (D2L_TEST_REPORTING_VERBOSE) { + options.verbose = ['1', 'true', 'yes'].includes(D2L_TEST_REPORTING_VERBOSE.trim().toLowerCase()); + } + + return options; +}; + +class NodeTestReporter extends Transform { + #anyFailure; + #logger; + #nameStacks; + #report; + #runStarted; + #startTimes; + #summary; + + constructor(options = {}) { + super({ objectMode: true }); + + this.#logger = new NodeTestLogger(); + this.#nameStacks = new Map(); + this.#startTimes = new Map(); + this.#anyFailure = false; + this.#report = null; + + try { + this.#report = new ReportBuilder('node', this.#logger, options); + } catch ({ message }) { + this.#logger.error('Failed to initialize D2L test report builder, report will not be generated'); + this.#logger.error(message); + + return; + } + + this.#runStarted = getNow(); + this.#summary = this.#report + .getSummary() + .addContext() + .setStarted(this.#runStarted.toISOString()); + } + + #getNameStack(file) { + if (!this.#nameStacks.has(file)) { + this.#nameStacks.set(file, []); + } + + return this.#nameStacks.get(file); + } + + #buildFullName(file, name, nesting) { + const nameStack = this.#getNameStack(file); + + nameStack[nesting] = sanitizeName(name); + nameStack.length = nesting + 1; + + return nameStack.join(' > '); + } + + #handleStart(data) { + const { name, nesting, file } = data; + + if (!file || this.#report.ignoreFilePath(file)) { + return; + } + + const fullName = this.#buildFullName(file, name, nesting); + + this.#startTimes.set(makeDetailId(file, fullName), getNowISOString()); + } + + #handleResult(type, data) { + const { name, nesting, file, line, column, details = {}, skip, todo } = data; + + if (type === 'test:fail') { + this.#anyFailure = true; + } + + if (details.type === 'suite') { + return; + } + + if (!file || this.#report.ignoreFilePath(file)) { + return; + } + + const fullName = this.#buildFullName(file, name, nesting); + const id = makeDetailId(file, fullName); + const started = this.#startTimes.get(id) ?? getNowISOString(); + const detail = this.#report + .getDetail(id) + .setName(fullName) + .setLocationFile(file) + .setStarted(started); + + if (typeof line === 'number') { + detail.setLocationLine(line); + } + + if (typeof column === 'number') { + detail.setLocationColumn(column); + } + + const isSkipped = (skip !== undefined && skip !== false) || (todo !== undefined && todo !== false); + + if (isSkipped) { + detail + .setSkipped() + .setDurationFinal(0) + .setDurationTotal(0); + + return; + } + + const duration = Math.max(0, Math.round(details.duration_ms ?? 0)); + + detail.addDuration(duration); + + if (type === 'test:pass') { + detail.setPassed(); + } else { + detail.setFailed(); + } + } + + _transform({ type, data }, encoding, callback) { + if (!this.#report) { + callback(); + + return; + } + + if (type === 'test:start') { + this.#handleStart(data); + } else if (type === 'test:pass' || type === 'test:fail') { + this.#handleResult(type, data); + } + + callback(); + } + + _flush(callback) { + if (!this.#report) { + this.#logger.error('D2L test report was not generated due to initialization failure'); + + callback(); + + return; + } + + const durationTotal = Math.max(1, getNow() - this.#runStarted); + + this.#summary.setDurationTotal(durationTotal); + + if (this.#anyFailure) { + this.#summary.setFailed(); + } else { + this.#summary.setPassed(); + } + + this.#report + .finalize() + .save(); + + callback(); + } +} + +export const reporter = (options = {}) => { + return new NodeTestReporter(options); +}; + +export default new NodeTestReporter(getOptionsFromEnvironment()); diff --git a/test/integration/data/configs/node.js b/test/integration/data/configs/node.js new file mode 100644 index 0000000..aafcaee --- /dev/null +++ b/test/integration/data/configs/node.js @@ -0,0 +1,21 @@ +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { reporter } from '../../../../src/reporters/node.js'; +import { run } from 'node:test'; + +const testDirectory = 'test/integration/data/tests/node'; +const files = readdirSync(testDirectory) + .filter(name => name.endsWith('.test.js')) + .map(name => join(testDirectory, name)); + +const stream = run({ + files, + concurrency: true +}).compose(reporter({ + reportPath: './d2l-test-report-node.json', + reportConfigurationPath: './test/integration/data/d2l-test-reporting.config.json', + verbose: true +})); + +stream.on('data', () => {}); +stream.on('error', () => {}); diff --git a/test/integration/data/d2l-test-reporting.config.json b/test/integration/data/d2l-test-reporting.config.json index dd9b7cc..11688e5 100644 --- a/test/integration/data/d2l-test-reporting.config.json +++ b/test/integration/data/d2l-test-reporting.config.json @@ -7,7 +7,8 @@ "**/tests/playwright/setup.js", "**/tests/playwright/teardown.js", "**/tests/web-test-runner/ignored.test.js", - "**/tests/webdriverio/ignored.test.js" + "**/tests/webdriverio/ignored.test.js", + "**/tests/node/ignored.test.js" ], "overrides": [ { @@ -49,6 +50,16 @@ "pattern": "**/tests/webdriverio/hook-failures.test.js", "type": "ui", "tool": "WebdriverIO Hook Failures Test Reporting" + }, + { + "pattern": "**/tests/node/statuses.test.js", + "type": "ui", + "tool": "Node 1 Test Reporting" + }, + { + "pattern": "**/tests/node/hook-failures.test.js", + "type": "ui", + "tool": "Node Hook Failures Test Reporting" } ] } diff --git a/test/integration/data/tests/node/custom-timeout.test.js b/test/integration/data/tests/node/custom-timeout.test.js new file mode 100644 index 0000000..3e4b1fa --- /dev/null +++ b/test/integration/data/tests/node/custom-timeout.test.js @@ -0,0 +1,11 @@ +import { describe, it } from 'node:test'; + +const delay = (ms = 50) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +describe('custom timeout', { timeout: 5000 }, () => { + it('suite level timeout', async() => { await delay(); }); + + it('test level timeout', { timeout: 10000 }, async() => { await delay(); }); +}); diff --git a/test/integration/data/tests/node/fake-timers.test.js b/test/integration/data/tests/node/fake-timers.test.js new file mode 100644 index 0000000..613486f --- /dev/null +++ b/test/integration/data/tests/node/fake-timers.test.js @@ -0,0 +1,24 @@ +import { after, before, describe, it } from 'node:test'; +import { createSandbox } from 'sinon'; + +const delay = (ms = 50) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +const fakeNow = 1234567890; + +describe('fake timers', () => { + let sandbox; + + before(() => { + sandbox = createSandbox(); + + sandbox.useFakeTimers({ now: fakeNow, toFake: ['Date'] }); + }); + + after(() => sandbox.restore()); + + it('passed', async() => { await delay(); }); + + it('failed', () => { throw new Error('fail'); }); +}); diff --git a/test/integration/data/tests/node/hook-failures.test.js b/test/integration/data/tests/node/hook-failures.test.js new file mode 100644 index 0000000..0612059 --- /dev/null +++ b/test/integration/data/tests/node/hook-failures.test.js @@ -0,0 +1,87 @@ +import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; + +const delay = (ms = 50) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +describe('hook failures', () => { + describe('before all failure', () => { + before(() => { throw new Error('before all hook failure'); }); + + it('test with before all failure', async() => { await delay(); }); + }); + + describe('before each failure', () => { + beforeEach(() => { throw new Error('before each hook failure'); }); + + it('test with before each failure', async() => { await delay(); }); + }); + + describe('after each failure', () => { + afterEach(() => { throw new Error('after each hook failure'); }); + + it('test with after each failure', async() => { await delay(); }); + }); + + describe('after all failure', () => { + after(() => { throw new Error('after all hook failure'); }); + + it('test with after all failure', async() => { await delay(); }); + }); + + describe('flaky before all', () => { + let beforeAllCount = 0; + + before(() => { + if (beforeAllCount < 2) { + beforeAllCount++; + + throw new Error('flaky before all'); + } + }); + + it('test with flaky before all', async() => { await delay(); }); + }); + + describe('flaky before each', () => { + let beforeEachCount = 0; + + beforeEach(() => { + if (beforeEachCount < 2) { + beforeEachCount++; + + throw new Error('flaky before each'); + } + }); + + it('test with flaky before each', async() => { await delay(); }); + }); + + describe('flaky after each', () => { + let afterEachCount = 0; + + afterEach(() => { + if (afterEachCount < 2) { + afterEachCount++; + + throw new Error('flaky after each'); + } + }); + + it('test with flaky after each', async() => { await delay(); }); + }); + + describe('flaky after all', () => { + let afterAllCount = 0; + + after(() => { + if (afterAllCount < 2) { + afterAllCount++; + + throw new Error('flaky after all'); + } + }); + + it('test with flaky after all', async() => { await delay(); }); + }); +}); diff --git a/test/integration/data/tests/node/ignored.test.js b/test/integration/data/tests/node/ignored.test.js new file mode 100644 index 0000000..30505f9 --- /dev/null +++ b/test/integration/data/tests/node/ignored.test.js @@ -0,0 +1,5 @@ +import { describe, it } from 'node:test'; + +describe('ignored', () => { + it('should not appear in report', () => {}); +}); diff --git a/test/integration/data/tests/node/special-characters.test.js b/test/integration/data/tests/node/special-characters.test.js new file mode 100644 index 0000000..771477f --- /dev/null +++ b/test/integration/data/tests/node/special-characters.test.js @@ -0,0 +1,9 @@ +import { describe, it } from 'node:test'; + +const delay = (ms = 50) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +describe('special characters', () => { + it(' special/characters "(\n\r\t\b\f)" ', async() => { await delay(); }); +}); diff --git a/test/integration/data/tests/node/statuses.test.js b/test/integration/data/tests/node/statuses.test.js new file mode 100644 index 0000000..6562fef --- /dev/null +++ b/test/integration/data/tests/node/statuses.test.js @@ -0,0 +1,17 @@ +import { describe, it } from 'node:test'; + +const delay = (ms = 50) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +describe('statuses', () => { + it('passed', async() => { await delay(); }); + + it('skipped', { skip: true }, () => {}); + + it('failed', async() => { + await delay(); + + throw new Error('fail'); + }); +}); diff --git a/test/integration/data/tests/node/taxonomy-overrides.test.js b/test/integration/data/tests/node/taxonomy-overrides.test.js new file mode 100644 index 0000000..fbff422 --- /dev/null +++ b/test/integration/data/tests/node/taxonomy-overrides.test.js @@ -0,0 +1,17 @@ +import { describe, it } from 'node:test'; + +const delay = (ms = 50) => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +describe('taxonomy overrides', () => { + it('passed', async() => { await delay(); }); + + it('skipped', { skip: true }, () => {}); + + it('failed', async() => { + await delay(); + + throw new Error('fail'); + }); +}); diff --git a/test/integration/data/validation/test-report-node.js b/test/integration/data/validation/test-report-node.js new file mode 100644 index 0000000..3f30980 --- /dev/null +++ b/test/integration/data/validation/test-report-node.js @@ -0,0 +1,125 @@ +import { latestReportVersion } from '../../../../src/helpers/schema.cjs'; + +export const testReportLatestPartial = { + version: latestReportVersion, + summary: { + status: 'failed', + framework: 'node', + count: { passed: 8, failed: 9, skipped: 2, flaky: 0 } + }, + details: [{ + name: 'custom timeout > suite level timeout', + status: 'passed', + location: { file: 'test/integration/data/tests/node/custom-timeout.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'custom timeout > test level timeout', + status: 'passed', + location: { file: 'test/integration/data/tests/node/custom-timeout.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'fake timers > passed', + status: 'passed', + location: { file: 'test/integration/data/tests/node/fake-timers.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'fake timers > failed', + status: 'failed', + location: { file: 'test/integration/data/tests/node/fake-timers.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'hook failures > before all failure > test with before all failure', + status: 'failed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > before each failure > test with before each failure', + status: 'failed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > after each failure > test with after each failure', + status: 'failed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > after all failure > test with after all failure', + status: 'passed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > flaky before all > test with flaky before all', + status: 'failed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > flaky before each > test with flaky before each', + status: 'failed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > flaky after each > test with flaky after each', + status: 'failed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'hook failures > flaky after all > test with flaky after all', + status: 'passed', + location: { file: 'test/integration/data/tests/node/hook-failures.test.js' }, + taxonomy: { tool: 'Node Hook Failures Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'special characters > special/characters "(\\n\\r\\t\\b\\f)"', + status: 'passed', + location: { file: 'test/integration/data/tests/node/special-characters.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'statuses > passed', + status: 'passed', + location: { file: 'test/integration/data/tests/node/statuses.test.js' }, + taxonomy: { tool: 'Node 1 Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'statuses > skipped', + status: 'skipped', + location: { file: 'test/integration/data/tests/node/statuses.test.js' }, + taxonomy: { tool: 'Node 1 Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'statuses > failed', + status: 'failed', + location: { file: 'test/integration/data/tests/node/statuses.test.js' }, + taxonomy: { tool: 'Node 1 Test Reporting', type: 'ui' }, + retries: 0 + }, { + name: 'taxonomy overrides > passed', + status: 'passed', + location: { file: 'test/integration/data/tests/node/taxonomy-overrides.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'taxonomy overrides > skipped', + status: 'skipped', + location: { file: 'test/integration/data/tests/node/taxonomy-overrides.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }, { + name: 'taxonomy overrides > failed', + status: 'failed', + location: { file: 'test/integration/data/tests/node/taxonomy-overrides.test.js' }, + taxonomy: { tool: 'Test Reporting', type: 'integration' }, + retries: 0 + }] +}; diff --git a/test/integration/report-validation.test.js b/test/integration/report-validation.test.js index 6e03a15..ff4f14f 100644 --- a/test/integration/report-validation.test.js +++ b/test/integration/report-validation.test.js @@ -7,6 +7,7 @@ import { hasContext } from '../../src/helpers/github.cjs'; import { latestReportVersion } from '../../src/helpers/schema.cjs'; import { Report } from '../../src/helpers/report.cjs'; import { testReportLatestPartial as testReportLatestPartialMocha } from './data/validation/test-report-mocha.js'; +import { testReportLatestPartial as testReportLatestPartialNodeTest } from './data/validation/test-report-node.js'; import { testReportLatestPartial as testReportLatestPartialPlaywright } from './data/validation/test-report-playwright.js'; import { testReportLatestPartial as testReportLatestPartialWebTestRunner } from './data/validation/test-report-web-test-runner.js'; import { testReportLatestPartial as testReportLatestPartialWebdriverIO } from './data/validation/test-report-webdriverio.js'; @@ -34,6 +35,11 @@ const reportTests = [{ version: latestReportVersion, path: './d2l-test-report-mocha.json', expected: testReportLatestPartialMocha +}, { + name: 'node', + version: latestReportVersion, + path: './d2l-test-report-node.json', + expected: testReportLatestPartialNodeTest }, { name: 'playwright', version: latestReportVersion,