From 24693df2189d8c97722163bfdd63eec3aeb85648 Mon Sep 17 00:00:00 2001 From: Sarah Young Date: Thu, 3 Sep 2026 11:13:56 -0400 Subject: [PATCH 1/4] Apply default taxonomy to locationless details --- src/helpers/report-builder.cjs | 19 +++++++++++++++++++ src/helpers/report-configuration.cjs | 19 ++++++++++++++----- src/helpers/report-configuration.d.cts | 17 +++++++++++++++++ test/unit/report-builder.test.js | 11 +++++++++++ 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 src/helpers/report-configuration.d.cts diff --git a/src/helpers/report-builder.cjs b/src/helpers/report-builder.cjs index f74624b..1ffaa09 100644 --- a/src/helpers/report-builder.cjs +++ b/src/helpers/report-builder.cjs @@ -309,6 +309,7 @@ class ReportDetailBuilder extends ReportBuilderBase { return this; } + } class ReportBuilder extends ReportBuilderBase { @@ -409,6 +410,8 @@ class ReportBuilder extends ReportBuilderBase { const missingConfigByFile = new Map(); for (const [, { data: detail }] of this._data.details) { + this.#applyDefaultTaxonomy(detail); + const { status, retries } = detail; if (status === 'passed') { @@ -449,6 +452,22 @@ class ReportBuilder extends ReportBuilderBase { return this; } + #applyDefaultTaxonomy(detail) { + if (detail.location?.file != null) { + return; + } + + const { type, tool } = this.#reportConfiguration.getDefaultTaxonomy(); + + if (type == null && tool == null) { + return; + } + + detail.taxonomy ??= {}; + detail.taxonomy.type ??= type; + detail.taxonomy.tool ??= tool; + } + #logMissingConfigWarnings(missingConfigByFile) { if (missingConfigByFile.size === 0) { return; diff --git a/src/helpers/report-configuration.cjs b/src/helpers/report-configuration.cjs index 057ce74..9300897 100644 --- a/src/helpers/report-configuration.cjs +++ b/src/helpers/report-configuration.cjs @@ -103,6 +103,18 @@ class ReportConfiguration { return this.#reportConfigurationPath; } + getDefaultTaxonomy() { + const { + type, + tool + } = this.#reportConfiguration; + + return { + type: type?.toLowerCase(), + tool + }; + } + getTaxonomy(filePath) { filePath = makeRelativeFilePath(filePath); @@ -124,12 +136,9 @@ class ReportConfiguration { } } - const { - type: defaultType, - tool: defaultTool - } = this.#reportConfiguration; + const { type: defaultType, tool: defaultTool } = this.getDefaultTaxonomy(); - metadata.type = metadata.type ?? defaultType?.toLowerCase(); + metadata.type = metadata.type ?? defaultType; metadata.tool = metadata.tool ?? defaultTool; return metadata; diff --git a/src/helpers/report-configuration.d.cts b/src/helpers/report-configuration.d.cts new file mode 100644 index 0000000..a74bcd6 --- /dev/null +++ b/src/helpers/report-configuration.d.cts @@ -0,0 +1,17 @@ +import type { Logger } from './report-builder.js'; + +export interface Taxonomy { + type?: string; + tool?: string; +} + +export declare class ReportConfiguration { + constructor(path?: string, logger?: Pick); + + getPath(): string | undefined; + getDefaultTaxonomy(): Taxonomy; + getTaxonomy(filePath: string): Taxonomy; + hasTaxonomy(filePath: string): boolean; + ignoreFilePath(filePath: string): boolean; + toJSON(): Record; +} diff --git a/test/unit/report-builder.test.js b/test/unit/report-builder.test.js index 79fcf96..dd1ba86 100644 --- a/test/unit/report-builder.test.js +++ b/test/unit/report-builder.test.js @@ -638,6 +638,17 @@ describe('report builder', () => { }); }); + describe('taxonomy', () => { + it('applies defaults to a detail without a location', () => { + const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ type: 'unit', tool: 'Test Reporting' }); + }); + }); + describe('ignore', () => { it('false without config', () => { const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); From 438a7c71f451af02e61e8294feb9863fd461546b Mon Sep 17 00:00:00 2001 From: Sarah Young Date: Thu, 3 Sep 2026 11:40:48 -0400 Subject: [PATCH 2/4] lint --- src/helpers/report-builder.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/report-builder.cjs b/src/helpers/report-builder.cjs index 1ffaa09..7c2af48 100644 --- a/src/helpers/report-builder.cjs +++ b/src/helpers/report-builder.cjs @@ -309,7 +309,6 @@ class ReportDetailBuilder extends ReportBuilderBase { return this; } - } class ReportBuilder extends ReportBuilderBase { From e067a956575fa74f29cf46c139505c8a2a95cd90 Mon Sep 17 00:00:00 2001 From: Sarah Young Date: Thu, 3 Sep 2026 14:38:40 -0400 Subject: [PATCH 3/4] some tests --- src/helpers/report-builder.cjs | 10 +++- src/helpers/report-configuration.cjs | 9 +-- test/integration/report-validation.test.js | 23 ++++++++ test/unit/report-builder.test.js | 67 ++++++++++++++++++++++ test/unit/report-configuration.test.js | 38 ++++++++++++ 5 files changed, 138 insertions(+), 9 deletions(-) diff --git a/src/helpers/report-builder.cjs b/src/helpers/report-builder.cjs index 7c2af48..3e27936 100644 --- a/src/helpers/report-builder.cjs +++ b/src/helpers/report-builder.cjs @@ -463,8 +463,14 @@ class ReportBuilder extends ReportBuilderBase { } detail.taxonomy ??= {}; - detail.taxonomy.type ??= type; - detail.taxonomy.tool ??= tool; + + if (type != null) { + detail.taxonomy.type ??= type; + } + + if (tool != null) { + detail.taxonomy.tool ??= tool; + } } #logMissingConfigWarnings(missingConfigByFile) { diff --git a/src/helpers/report-configuration.cjs b/src/helpers/report-configuration.cjs index 9300897..2a57b1f 100644 --- a/src/helpers/report-configuration.cjs +++ b/src/helpers/report-configuration.cjs @@ -104,14 +104,9 @@ class ReportConfiguration { } getDefaultTaxonomy() { - const { - type, - tool - } = this.#reportConfiguration; - return { - type: type?.toLowerCase(), - tool + type: this.#reportConfiguration.type?.toLowerCase(), + tool: this.#reportConfiguration.tool }; } diff --git a/test/integration/report-validation.test.js b/test/integration/report-validation.test.js index 7357cd4..5ff4dcc 100644 --- a/test/integration/report-validation.test.js +++ b/test/integration/report-validation.test.js @@ -6,6 +6,7 @@ import { getOperatingSystemType } from '../../src/helpers/system.cjs'; import { hasContext } from '../../src/helpers/github.cjs'; import { latestReportVersion } 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'; import { testReportLatestPartial as testReportLatestPartialMocha } from './data/validation/test-report-mocha.js'; import { testReportLatestPartial as testReportLatestPartialNodeTest } from './data/validation/test-report-node.js'; @@ -61,6 +62,28 @@ const reportTests = [{ }]; describe('report validation', () => { + it('applies available defaults to details without locations', () => { + const warnings = []; + const logger = { + error: () => {}, + info: () => {}, + location: () => {}, + warning: message => warnings.push(message) + }; + const builder = new ReportBuilder('node', logger, { reportWriter: () => {} }); + const detail = builder.getDetail('locationless').setPassed(); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ tool: 'Test Tooling' }); + expect(warnings).to.deep.equal([ + '1 test missing taxonomy fields: type (1).', + 'Affected files: 1:', + '- unknown location (1 test)', + 'Check d2l-test-reporting.config.json to configure missing taxonomy fields.' + ]); + }); + for (const reportTest of reportTests) { describe(reportTest.name, () => { it('exists', () => { diff --git a/test/unit/report-builder.test.js b/test/unit/report-builder.test.js index dd1ba86..757439e 100644 --- a/test/unit/report-builder.test.js +++ b/test/unit/report-builder.test.js @@ -647,6 +647,73 @@ describe('report builder', () => { expect(detail.data.taxonomy).to.deep.equal({ type: 'unit', tool: 'Test Reporting' }); }); + + it('does not override file taxonomy with defaults', () => { + const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); + const detail = builder.getDetail('test'); + + detail.setLocationFile('test/example.test.js'); + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ type: 'unit', tool: 'Test Reporting' }); + }); + + it('applies available default taxonomy fields', () => { + mock.method(fs, 'readFileSync', () => JSON.stringify({ type: 'unit' })); + + const builder = new ReportBuilder('mocha', noopLogger, { + reportConfigurationPath: './d2l-test-reporting.config.json', + reportWriter: () => { } + }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ type: 'unit' }); + }); + + it('applies a tool-only default', () => { + mock.method(fs, 'readFileSync', () => JSON.stringify({ tool: 'Test Reporting' })); + + const builder = new ReportBuilder('mocha', noopLogger, { + reportConfigurationPath: './d2l-test-reporting.config.json', + reportWriter: () => { } + }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ tool: 'Test Reporting' }); + }); + + it('preserves existing taxonomy fields', () => { + const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); + const detail = builder.getDetail('test'); + + detail.data.taxonomy = { type: 'custom', tool: 'custom' }; + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ + type: 'custom', + tool: 'custom' + }); + }); + + it('does not create taxonomy without defaults', () => { + mock.method(fs, 'readFileSync', () => JSON.stringify({ + overrides: [{ pattern: '**', type: 'unit', tool: 'Test Reporting' }] + })); + + const builder = new ReportBuilder('mocha', noopLogger, { + reportConfigurationPath: './d2l-test-reporting.config.json', + reportWriter: () => { } + }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data).to.not.have.property('taxonomy'); + }); }); describe('ignore', () => { diff --git a/test/unit/report-configuration.test.js b/test/unit/report-configuration.test.js index d8b8c82..5c19d7f 100644 --- a/test/unit/report-configuration.test.js +++ b/test/unit/report-configuration.test.js @@ -172,6 +172,28 @@ describe('report configuration', () => { }); describe('taxonomy', () => { + describe('defaults', () => { + it('lowercases type and preserves tool', () => { + const config = loadConfig({ type: 'UI', tool: 'My Tool' }); + + expect(config.getDefaultTaxonomy()).to.deep.equal({ + type: 'ui', + tool: 'My Tool' + }); + }); + + it('omits absent values', () => { + const config = loadConfig({ + overrides: [{ pattern: '**', type: 'unit', tool: 'Test Reporting' }] + }); + + expect(config.getDefaultTaxonomy()).to.deep.equal({ + type: undefined, + tool: undefined + }); + }); + }); + it('lowercases type', () => { const config = loadConfig({ type: 'UI', tool: 'My Tool' }); @@ -198,6 +220,22 @@ describe('report configuration', () => { }); }); + it('inherits missing fields from defaults', () => { + const config = loadConfig({ + type: 'integration', + tool: 'Default Tool', + overrides: [{ + pattern: '**/special.test.js', + type: 'UI' + }] + }); + + expect(config.getTaxonomy('test/special.test.js')).to.deep.equal({ + type: 'ui', + tool: 'Default Tool' + }); + }); + it('normalizes leading ./', () => { const config = loadConfig({ type: 'integration', From 43412c5610f5c5b91fb6680b3370b898c716184f Mon Sep 17 00:00:00 2001 From: Sarah Young Date: Thu, 3 Sep 2026 14:58:44 -0400 Subject: [PATCH 4/4] few more tests --- test/unit/report-configuration.test.js | 12 ++++++++++++ test/unit/report.test.js | 27 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/test/unit/report-configuration.test.js b/test/unit/report-configuration.test.js index 5c19d7f..c629472 100644 --- a/test/unit/report-configuration.test.js +++ b/test/unit/report-configuration.test.js @@ -141,6 +141,12 @@ describe('report configuration', () => { expect(() => new ReportConfiguration(configPath, logger)).to.throw('Unable to read/parse'); }); + + it('when the default configuration is unparseable', () => { + mock.method(fs, 'readFileSync', () => 'not json'); + + expect(() => new ReportConfiguration(undefined, logger)).to.throw('Unable to read/parse'); + }); }); it('empty without config file', () => { @@ -152,6 +158,12 @@ describe('report configuration', () => { }); }); + it('reports the resolved configuration path', () => { + const config = loadConfig({ type: 'integration', tool: 'Test Reporting' }); + + expect(config.getPath()).to.equal('d2l-test-reporting.config.json'); + }); + describe('default logger', () => { const legacyConfig = { type: 'integration', diff --git a/test/unit/report.test.js b/test/unit/report.test.js index d36954d..92bd98f 100644 --- a/test/unit/report.test.js +++ b/test/unit/report.test.js @@ -407,6 +407,33 @@ const testReportOldV3ConfigOnly = { describe('report', () => { afterEach(() => mock.reset()); + describe('LMS information', () => { + const lmsInfo = { + buildNumber: '20.26.9.12345', + instanceUrl: 'https://example.brightspace.com' + }; + const reports = [{ + name: 'v1', + report: testReportV1Full + }, { + name: 'v2', + report: testReportV2Full + }, { + name: 'v3', + report: testReportLatestFull + }]; + + for (const { name, report: reportData } of reports) { + it(`adds LMS information to ${name} reports`, () => { + mock.method(fs, 'readFileSync', () => JSON.stringify(reportData)); + + const report = new Report(testReportPath, { lmsInfo }); + + expect(report.toJSON().summary.lms).to.deep.equal(lmsInfo); + }); + } + }); + describe(`legacy (v1, upgrades to v${latestReportVersion})`, () => { const testReportCurrentVersion = 1;