diff --git a/src/classes/Test.js b/src/classes/Test.js index 0da162b..0ab3bcc 100644 --- a/src/classes/Test.js +++ b/src/classes/Test.js @@ -23,6 +23,14 @@ const INHERITED_PROPS = [ * Represents a single test or a group of tests */ export default class Test { + /** + * Source file of each test definition, e.g. `{ label: "check.js", path: "file:///…/check.js" }`. + * Populated by the environment, which cannot store it on the test object itself: + * test definitions come from modules that may hold unrelated data. + * @type {WeakMap} + */ + static files = new WeakMap(); + data = {}; constructor (test, parent) { @@ -63,6 +71,12 @@ export default class Test { } Object.defineProperties(this, descriptors); + // Only set it if tagged: an own `file: undefined` would block inheriting the parent's below + let file = Test.files.get(test); + if (file) { + this.file = file; + } + if (typeof this.data === "function") { this.getData = this.data; this.data = {}; diff --git a/src/env/node.js b/src/env/node.js index 9791f49..e985d62 100644 --- a/src/env/node.js +++ b/src/env/node.js @@ -208,8 +208,8 @@ async function rerun (options, urls) { let test = module.default ?? Object.values(module); - if (Object.isExtensible(test)) { - test.file = old.test.file; + if (old.test.file && typeof test === "object") { + Test.files.set(test, old.test.file); } test = new Test(test, currentRoot.test); @@ -342,16 +342,16 @@ export default { [...loadedFiles].map(async url => { let module = await import(url); let test = module.default ?? module; - if (test && typeof test === "object" && Object.isExtensible(test) && !test.file) { + if (test && typeof test === "object") { let fileUrl = new URL(url); fileUrl.search = ""; - test.file = { + Test.files.set(test, { label: path.relative( isDirectory ? location : path.dirname(location), fileURLToPath(url), ), path: fileUrl.href, - }; + }); } }), ); diff --git a/tests/fixtures/plain-module.js b/tests/fixtures/plain-module.js new file mode 100644 index 0000000..e470a21 --- /dev/null +++ b/tests/fixtures/plain-module.js @@ -0,0 +1,3 @@ +// A plain data module, not a test definition. Imported by ../module-tagging.js +// to verify hTest doesn't tag it with source file metadata. +export default { a: 1 }; diff --git a/tests/module-tagging.js b/tests/module-tagging.js new file mode 100644 index 0000000..70be5bb --- /dev/null +++ b/tests/module-tagging.js @@ -0,0 +1,27 @@ +import Test from "../src/classes/Test.js"; +import data from "./fixtures/plain-module.js"; + +export default { + name: "Module tagging", + tests: [ + { + name: "Non-test modules are left untouched", + description: + "The Node env tags every loaded module with its source file. Data modules must not be mutated: a `file` key would shadow a real one and leak into Object.keys().", + run: () => Object.keys(data), + expect: ["a"], + }, + { + name: "Tests get their file from the registry", + description: "Source file metadata reaches the Test instance without touching the spec.", + run () { + let spec = { name: "Tagged" }; + Test.files.set(spec, { label: "tagged.js", path: "file:///tagged.js" }); + let test = new Test(spec); + + return [test.file?.label, "file" in spec]; + }, + expect: ["tagged.js", false], + }, + ], +};