diff --git a/docs/report-builder.md b/docs/report-builder.md index aec720f..7c32933 100644 --- a/docs/report-builder.md +++ b/docs/report-builder.md @@ -28,6 +28,8 @@ const report = new ReportBuilder('your-framework-name', logger); * `reportConfigurationPath` (default: `./d2l-test-reporting.config.json`): Path to the D2L test reporting configuration file for taxonomy mapping and ignore patterns. See [Report Configuration Format] for the schema. +* `reportVersion` (default: `3`): Report schema version to generate. Version 4 + is an opt-in schema that supports an optional detail `testId`. * `verbose` (default: `false`): Enable verbose logging for debugging purposes. ## How It Works @@ -137,6 +139,16 @@ const testId = `${test.file}[${test.fullName}]`; // Example: "test/unit/component.test.js[MyComponent > should render]" ``` +For version 4 reports, set a stable logical identity on the detail. This lets +otherwise identical details remain distinct in the report. `setTestId()` is +only available when `reportVersion` is `4`. + +```js +if (report.getVersion() === 4) { + report.getDetail(testId).setTestId(testId); +} +``` + ### Checking Ignored Files Always check before processing a test: diff --git a/schemas/report/v4.json b/schemas/report/v4.json new file mode 100644 index 0000000..d7f00e7 --- /dev/null +++ b/schemas/report/v4.json @@ -0,0 +1,338 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/test-reporting/schemas/report/v4.json", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer", + "const": 4 + }, + "summary": { + "$ref": "#/$defs/context", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "framework": { + "$ref": "#/$defs/nonEmptyUnpaddedString" + }, + "lms": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "buildNumber": { + "type": "string", + "pattern": "^([0-9]{2}\\.){2}[0-9]{1,2}\\.[0-9]{5}$", + "errorMessage": { + "pattern": "should be a valid LMS build number (XX.XX.XX.XXXXX)" + } + }, + "instanceUrl": { + "type": "string", + "format": "uri" + } + } + }, + "operatingSystem": { + "type": "string", + "enum": [ + "windows", + "linux", + "mac" + ] + }, + "started": { + "type": "string", + "format": "date-time" + }, + "duration": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "total": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "total" + ] + }, + "status": { + "type": "string", + "enum": [ + "passed", + "failed" + ] + }, + "count": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "passed": { + "type": "integer", + "minimum": 0 + }, + "failed": { + "type": "integer", + "minimum": 0 + }, + "skipped": { + "type": "integer", + "minimum": 0 + }, + "flaky": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "passed", + "failed", + "skipped", + "flaky" + ] + } + }, + "required": [ + "operatingSystem", + "framework", + "started", + "duration", + "status", + "count" + ] + }, + "details": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "testId": { + "$ref": "#/$defs/nonEmptyUnpaddedString" + }, + "name": { + "$ref": "#/$defs/nonEmptyUnpaddedString" + }, + "location": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/nonEmptyUnpaddedString" + }, + "line": { + "type": "integer", + "minimum": 0 + }, + "column": { + "type": "integer", + "minimum": 0 + } + } + }, + "taxonomy": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "tool": { + "$ref": "#/$defs/nonEmptyUnpaddedString" + }, + "type": { + "$ref": "#/$defs/nonEmptyUnpaddedString" + } + } + }, + "started": { + "type": "string", + "format": "date-time" + }, + "configuration": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "timeout": { + "type": "integer", + "minimum": 0 + } + } + }, + "duration": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "final": { + "type": "integer", + "minimum": 0 + }, + "total": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "final", + "total" + ] + }, + "status": { + "type": "string", + "enum": [ + "passed", + "failed", + "skipped" + ] + }, + "browser": { + "type": "string", + "enum": [ + "chromium", + "chrome", + "firefox", + "webkit", + "safari", + "edge" + ] + }, + "retries": { + "type": "integer", + "minimum": 0 + }, + "github": { + "type": "object", + "unevaluatedProperties": false, + "properties": { + "codeowners": { + "type": "array", + "items": { + "$ref": "#/$defs/githubCodeownerString" + }, + "minItems": 1 + } + } + } + }, + "required": [ + "name", + "started", + "duration", + "status", + "retries" + ] + } + } + }, + "required": [ + "id", + "version", + "summary", + "details" + ], + "$defs": { + "nonEmptyUnpaddedString": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/test-reporting/schemas/report/v4/non-empty-unpadded-string.json", + "type": "string", + "pattern": "^(?!\\s).+(? void; verbose?: boolean; } @@ -30,6 +31,7 @@ export interface ReportSummaryBuilder { export interface ReportDetailBuilder { readonly data: Record; setName(name: string, options?: SetOptions): ReportDetailBuilder; + setTestId(testId: string, options?: SetOptions): ReportDetailBuilder; setStarted(started: string, options?: SetOptions): ReportDetailBuilder; setLocationFile( filePath: string, @@ -67,6 +69,7 @@ export declare class ReportBuilder { finalize(): ReportBuilder; getDetail(id: string): ReportDetailBuilder; getSummary(): ReportSummaryBuilder; + getVersion(): 3 | 4; ignoreFilePath(filePath: string): boolean; save(): void; } diff --git a/src/helpers/report.cjs b/src/helpers/report.cjs index 9cc1ef4..fe07197 100644 --- a/src/helpers/report.cjs +++ b/src/helpers/report.cjs @@ -9,10 +9,13 @@ const { validateReportV1Ajv, validateReportV2Ajv, validateReportV3Ajv, + validateReportV4Ajv, validateReportV1ContextAjv, validateReportV2ContextAjv, validateReportV3ContextAjv, - latestReportVersion + validateReportV4ContextAjv, + latestReportVersion, + latestSupportedReportVersion } = schema; const getReportVersion = (report) => { @@ -50,6 +53,12 @@ const validateReport = (report, dataVar = 'report') => { errors = validateReportV3Ajv.errors; } + break; + case 4: + if (!validateReportV4Ajv(report)) { + errors = validateReportV4Ajv.errors; + } + break; default: throw new Error(`Unknown report version '${reportVersion}'`); @@ -94,14 +103,19 @@ const injectReportV2Context = (report, context, override) => { return report; }; -const injectReportV3Context = (report, context, override) => { +const injectReportV3Context = ( + report, + context, + override, + contextValidator = validateReportV3ContextAjv +) => { const { summary } = report; if (!summary) { throw new Error('Report is missing needed property \'summary\''); } - if (override || !validateReportV3ContextAjv(summary)) { + if (override || !contextValidator(summary)) { report.summary = { ...summary, ...context @@ -111,6 +125,10 @@ const injectReportV3Context = (report, context, override) => { return report; }; +const injectReportV4Context = (report, context, override) => { + return injectReportV3Context(report, context, override, validateReportV4ContextAjv); +}; + const injectReportContext = (report, context, override) => { const reportVersion = getReportVersion(report); @@ -121,6 +139,8 @@ const injectReportContext = (report, context, override) => { return injectReportV2Context(report, context, override); case 3: return injectReportV3Context(report, context, override); + case 4: + return injectReportV4Context(report, context, override); default: throw new Error(`Unknown report version '${reportVersion}'`); } @@ -220,6 +240,10 @@ const injectReportV3LmsInfo = (report, lmsInfo) => { return report; }; +const injectReportV4LmsInfo = (report, lmsInfo) => { + return injectReportV3LmsInfo(report, lmsInfo); +}; + const injectReportLmsInfo = (report, lmsInfo) => { const reportVersion = getReportVersion(report); @@ -230,6 +254,8 @@ const injectReportLmsInfo = (report, lmsInfo) => { return injectReportV2LmsInfo(report, lmsInfo); case 3: return injectReportV3LmsInfo(report, lmsInfo); + case 4: + return injectReportV4LmsInfo(report, lmsInfo); default: throw new Error(`Unknown report version '${reportVersion}'`); } @@ -332,7 +358,7 @@ const upgradeReportV1ToV2 = (report) => { const upgradeReportV2ToV3 = (report) => { return { ...report, - version: latestReportVersion, + version: 3, details: report.details.map((detail) => { const { tool, experience, type, timeout, ...rest } = detail; const upgraded = { ...rest }; @@ -394,6 +420,7 @@ const upgradeReportToV2 = (report) => { return upgradeReportV1ToV2(report); case 2: case 3: + case 4: return report; default: throw new Error(`Unknown report version: ${reportVersion}`); @@ -410,6 +437,8 @@ const upgradeReport = (report) => { return upgradeReportV2ToV3(report); case 3: return cleanReportV3(report); + case 4: + return report; default: throw new Error(`Unknown report version: ${reportVersion}`); } @@ -461,7 +490,7 @@ class Report { report = upgradeReport(report); validateReport(report, `report (v${getReportVersion(report)})`); - } else if (getReportVersion(report) > latestReportVersion) { + } else if (getReportVersion(report) > latestSupportedReportVersion) { throw new Error(`Unsupported report version specified: ${reportVersionOriginal}`); } diff --git a/src/helpers/schema.cjs b/src/helpers/schema.cjs index afdf13a..82545e6 100644 --- a/src/helpers/schema.cjs +++ b/src/helpers/schema.cjs @@ -3,6 +3,7 @@ const addFormats = require('ajv-formats'); const addErrors = require('ajv-errors'); const latestReportSchema = require('../../schemas/report/v3.json'); +const reportV4Schema = require('../../schemas/report/v4.json'); const ajv = new Ajv({ verbose: true, strict: true, @@ -16,7 +17,8 @@ ajv.addSchema(require('../../schemas/report-configuration/v1.json')); ajv.addSchema(require('../../schemas/report-configuration/v2.json')); ajv.addSchema(require('../../schemas/report/v1.json')); ajv.addSchema(require('../../schemas/report/v2.json')); -ajv.addSchema(latestReportSchema); +ajv.addSchema(require('../../schemas/report/v3.json')); +ajv.addSchema(reportV4Schema); ajv.addSchema({ $schema: 'https://json-schema.org/draft/2019-09/schema', $id: '/test-reporting/schemas/report/v1/context/loose.json', @@ -38,15 +40,24 @@ ajv.addSchema({ type: 'object', unevaluatedProperties: true }); +ajv.addSchema({ + $schema: 'https://json-schema.org/draft/2019-09/schema', + $id: '/test-reporting/schemas/report/v4/context/loose.json', + $ref: '/test-reporting/schemas/report/v4/context.json', + type: 'object', + unevaluatedProperties: true +}); const validateReportConfigurationV1Ajv = ajv.getSchema('/test-reporting/schemas/report-configuration/v1.json'); const validateReportConfigurationV2Ajv = ajv.getSchema('/test-reporting/schemas/report-configuration/v2.json'); const validateReportV1ContextAjv = ajv.getSchema('/test-reporting/schemas/report/v1/context/loose.json'); const validateReportV2ContextAjv = ajv.getSchema('/test-reporting/schemas/report/v2/context/loose.json'); const validateReportV3ContextAjv = ajv.getSchema('/test-reporting/schemas/report/v3/context/loose.json'); +const validateReportV4ContextAjv = ajv.getSchema('/test-reporting/schemas/report/v4/context/loose.json'); const validateReportV1Ajv = ajv.getSchema('/test-reporting/schemas/report/v1.json'); const validateReportV2Ajv = ajv.getSchema('/test-reporting/schemas/report/v2.json'); const validateReportV3Ajv = ajv.getSchema('/test-reporting/schemas/report/v3.json'); +const validateReportV4Ajv = ajv.getSchema('/test-reporting/schemas/report/v4.json'); const decodeInstancePathAjv = (instancePath = '') => { if (!instancePath) { @@ -72,6 +83,8 @@ const formatErrorAjv = (errors, options = {}) => { const { properties: latestReportSchemaProperties } = latestReportSchema; const { version: { const: latestReportVersion } } = latestReportSchemaProperties; +const { properties: reportV4SchemaProperties } = reportV4Schema; +const { version: { const: latestSupportedReportVersion } } = reportV4SchemaProperties; const { details: { items: { properties: { browser: { enum: latestSupportedBrowsers } } } } } = latestReportSchemaProperties; module.exports = { @@ -81,9 +94,12 @@ module.exports = { validateReportV1ContextAjv, validateReportV2ContextAjv, validateReportV3ContextAjv, + validateReportV4ContextAjv, validateReportV1Ajv, validateReportV2Ajv, validateReportV3Ajv, + validateReportV4Ajv, latestReportVersion, + latestSupportedReportVersion, latestSupportedBrowsers }; diff --git a/src/reporters/jest.cjs b/src/reporters/jest.cjs index 01d7db4..fe50545 100644 --- a/src/reporters/jest.cjs +++ b/src/reporters/jest.cjs @@ -154,6 +154,10 @@ class JestReporter { .setLocationFile(file) .setStarted(started); + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } + this.#applyLocation(detail, testCaseResult.location); this.#applyTimeout(detail); @@ -217,6 +221,10 @@ class JestReporter { .setDurationFinal(0) .setDurationTotal(0); + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } + this.#applyLocation(detail, testCaseResult.location); this.#applyTimeout(detail); @@ -240,6 +248,10 @@ class JestReporter { .addDuration(0) .setFailed(); + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } + this.#applyTimeout(detail); } diff --git a/src/reporters/mocha.cjs b/src/reporters/mocha.cjs index 30cdc5d..8979f75 100644 --- a/src/reporters/mocha.cjs +++ b/src/reporters/mocha.cjs @@ -98,12 +98,16 @@ class TestReportingMochaReporter extends Spec { const name = makeDetailName(test); const id = makeDetailId(file, name); - this.#report + const detail = this.#report .getDetail(id) .setName(name) .setLocationFile(file) .setStarted(getNowISOString()) .setTimeout(_timeout); // using internal property, not ideal + + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } } #onTestRetry(test) { diff --git a/src/reporters/node.js b/src/reporters/node.js index a5eb725..8d96542 100644 --- a/src/reporters/node.js +++ b/src/reporters/node.js @@ -54,6 +54,7 @@ class NodeReporter extends Transform { const resolvedOptions = { reportConfigurationPath: options.reportConfigurationPath, + reportVersion: options.reportVersion, verbose: options.verbose, reportWriter: (reportData) => { this.push(reportData); @@ -141,6 +142,10 @@ class NodeReporter extends Transform { .setLocationFile(file) .setStarted(started); + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } + if (typeof line === 'number') { detail.setLocationLine(line); } diff --git a/src/reporters/playwright.js b/src/reporters/playwright.js index aa3290b..462861f 100644 --- a/src/reporters/playwright.js +++ b/src/reporters/playwright.js @@ -122,6 +122,10 @@ export default class Reporter { .setStarted(started) .setTimeout(Math.round(timeout)) .addDuration(Math.round(duration)); + + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } const isRetry = retry !== 0; if (isRetry) { diff --git a/src/reporters/web-test-runner.js b/src/reporters/web-test-runner.js index 3248656..0dd7b37 100644 --- a/src/reporters/web-test-runner.js +++ b/src/reporters/web-test-runner.js @@ -38,6 +38,10 @@ const makeDetailId = (sessionId, file, name) => { return `${sessionId}/${file}/${name}`; }; +const makeTestId = (browserName, file, name) => { + return `${browserName}/${file}/${name}`; +}; + const getBrowser = (browserName, logger) => { const browser = browserName?.trim().toLowerCase(); @@ -92,6 +96,7 @@ export function reporter(options = {}) { const { skipped, passed, duration, name } = test; const testName = makeDetailName(prefix, name); const id = makeDetailId(sessionId, testFile, testName); + const testId = makeTestId(browserName, testFile, testName); const detail = report .getDetail(id) .setName(testName) @@ -99,6 +104,10 @@ export function reporter(options = {}) { .setStarted(started) .setTimeout(testsFinishTimeout); + if (report.getVersion() === 4) { + detail.setTestId(testId); + } + if (browser) { detail.setBrowser(browser); } diff --git a/src/reporters/webdriverio.cjs b/src/reporters/webdriverio.cjs index 9fadf8c..a423bbf 100644 --- a/src/reporters/webdriverio.cjs +++ b/src/reporters/webdriverio.cjs @@ -50,6 +50,7 @@ class WebdriverIO extends WDIOReporter { this.#options = { reportPath: options.reportPath ?? './d2l-test-report.json', reportConfigurationPath: options.reportConfigurationPath ?? './d2l-test-reporting.config.json', + reportVersion: options.reportVersion, verbose: options.verbose ?? false }; this.#report = null; @@ -111,6 +112,10 @@ class WebdriverIO extends WDIOReporter { .setLocationFile(suite.file) .setStarted(getNowISOString()); + if (this.#report.getVersion() === 4) { + detail.setTestId(id); + } + if (browser) { detail.setBrowser(browser); } @@ -134,7 +139,7 @@ class WebdriverIO extends WDIOReporter { onRunnerStart(runner) { const { cid, start } = runner; - const { reportPath, reportConfigurationPath, verbose } = this.#options; + const { reportPath, reportConfigurationPath, reportVersion, verbose } = this.#options; const dir = dirname(reportPath); const ext = extname(reportPath); const base = basename(reportPath, ext); @@ -144,6 +149,7 @@ class WebdriverIO extends WDIOReporter { this.#report = new ReportBuilder('webdriverio', this.#logger, { reportPath: workerReportPath, reportConfigurationPath, + reportVersion, verbose }); } catch ({ message }) { @@ -205,6 +211,10 @@ class WebdriverIO extends WDIOReporter { .addDuration(0) .setFailed(); + if (this.#report.getVersion() === 4) { + detail.setTestId(makeDetailId(frame.file, fullTitle)); + } + if (browser) { detail.setBrowser(browser); } diff --git a/test/integration/data/configs/node.js b/test/integration/data/configs/node.js index 316d26a..83062da 100644 --- a/test/integration/data/configs/node.js +++ b/test/integration/data/configs/node.js @@ -10,6 +10,7 @@ const files = readdirSync(testDirectory) .map(name => join(testDirectory, name)); const reporterOptions = { reportConfigurationPath: './test/integration/data/d2l-test-reporting.config.json', + reportVersion: 4, verbose: true }; const testStream = run({ files, concurrency: true }); diff --git a/test/integration/data/validation/test-report-node.js b/test/integration/data/validation/test-report-node.js index f9638e9..c38e52d 100644 --- a/test/integration/data/validation/test-report-node.js +++ b/test/integration/data/validation/test-report-node.js @@ -1,7 +1,7 @@ -import { latestReportVersion } from '../../../../src/helpers/schema.cjs'; +import { latestSupportedReportVersion } from '../../../../src/helpers/schema.cjs'; export const testReportLatestPartial = { - version: latestReportVersion, + version: latestSupportedReportVersion, summary: { status: 'failed', framework: 'node', diff --git a/test/integration/report-validation.test.js b/test/integration/report-validation.test.js index 5ff4dcc..de66eaa 100644 --- a/test/integration/report-validation.test.js +++ b/test/integration/report-validation.test.js @@ -4,7 +4,7 @@ import { expect, use } from 'chai'; import chaiSubset from 'chai-subset'; import { getOperatingSystemType } from '../../src/helpers/system.cjs'; import { hasContext } from '../../src/helpers/github.cjs'; -import { latestReportVersion } from '../../src/helpers/schema.cjs'; +import { latestReportVersion, latestSupportedReportVersion } from '../../src/helpers/schema.cjs'; import { Report } from '../../src/helpers/report.cjs'; import { ReportBuilder } from '../../src/helpers/report-builder.cjs'; import { testReportLatestPartial as testReportLatestPartialJest } from './data/validation/test-report-jest.js'; @@ -41,9 +41,10 @@ const reportTests = [{ expected: testReportLatestPartialMocha }, { name: 'node', - version: latestReportVersion, + version: latestSupportedReportVersion, path: './d2l-test-report-node.json', - expected: testReportLatestPartialNodeTest + expected: testReportLatestPartialNodeTest, + hasTestIds: true }, { name: 'playwright', version: latestReportVersion, @@ -121,6 +122,16 @@ describe('report validation', () => { const { summary, details } = report; + if (reportTest.hasTestIds) { + for (const detail of details) { + expect(detail.testId).to.be.a('string').and.not.be.empty; + } + } else { + for (const detail of details) { + expect(detail).to.not.have.property('testId'); + } + } + expect(summary.operatingSystem).to.eq(getOperatingSystemType()); expect(summary.duration.total).to.be.above(0); diff --git a/test/unit/jest.test.js b/test/unit/jest.test.js new file mode 100644 index 0000000..230acf9 --- /dev/null +++ b/test/unit/jest.test.js @@ -0,0 +1,30 @@ +import { describe, it } from 'node:test'; +import { expect } from 'chai'; +import JestReporter from '../../src/reporters/jest.cjs'; + +describe('Jest reporter', () => { + it('sets a v4 test ID for an execution error without test results', () => { + let reportData; + const reporter = new JestReporter({}, { + reportVersion: 4, + reportWriter: (data) => { + reportData = data; + } + }); + const path = 'test/example.test.cjs'; + + reporter.onRunStart({ startTime: Date.now() }); + reporter.onTestResult({ path }, { + testResults: [], + testExecError: { message: 'test execution error' } + }); + reporter.onRunComplete(null, { startTime: Date.now(), success: false }); + + const { details } = JSON.parse(reportData); + const [detail] = details; + + expect(detail.testId).to.eq(`${path}[test execution error]`); + expect(detail.name).to.eq('test execution error'); + expect(detail.status).to.eq('failed'); + }); +}); diff --git a/test/unit/report-builder.test.js b/test/unit/report-builder.test.js index 757439e..84b43d2 100644 --- a/test/unit/report-builder.test.js +++ b/test/unit/report-builder.test.js @@ -110,6 +110,23 @@ describe('report builder', () => { expect(builder.data.version).to.eq(latestReportVersion); }); + it('sets requested v4 version', () => { + const builder = new ReportBuilder('mocha', noopLogger, { + reportVersion: 4, + reportWriter: () => { } + }); + + expect(builder.data.version).to.eq(4); + expect(builder.getVersion()).to.eq(4); + }); + + it('rejects unsupported version', () => { + expect(() => new ReportBuilder('mocha', noopLogger, { + reportVersion: 2, + reportWriter: () => { } + })).to.throw("Unsupported report version '2'"); + }); + it('produces loadable report', () => { const builder = buildValidReport(); @@ -120,6 +137,21 @@ describe('report builder', () => { expect(report.getVersion()).to.eq(latestReportVersion); }); + it('validates otherwise identical v4 details with distinct test IDs', () => { + const builder = buildValidReport({ reportVersion: 4 }); + const detail = builder.getDetail('test-1'); + const duplicate = builder.getDetail('test-2'); + + detail.setTestId('test-id-1'); + Object.assign(duplicate.data, detail.data, { testId: 'test-id-2' }); + mock.method(fs, 'readFileSync', () => JSON.stringify(builder)); + + const report = new Report('./test-report.json', { context: testContext }); + + expect(report.getVersion()).to.eq(4); + expect(report.toJSON().details).to.have.length(2); + }); + it('sets codeowners on details', () => { const builder = buildValidReport(); const json = builder.toJSON(); @@ -272,6 +304,22 @@ describe('report builder', () => { expect(detail.data.name).to.eq('my test'); }); + it('rejects test ID for v3', () => { + expect(() => detail.setTestId('test-id')).to.throw('testId requires report version 4'); + }); + + it('sets test ID for v4', () => { + const builder = new ReportBuilder('mocha', noopLogger, { + reportVersion: 4, + reportWriter: () => { } + }); + const v4Detail = builder.getDetail('test-1'); + + v4Detail.setTestId('test-id'); + + expect(v4Detail.data.testId).to.eq('test-id'); + }); + it('sets started', () => { const started = new Date().toISOString();