-
Notifications
You must be signed in to change notification settings - Fork 781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ESM build #1551
Comments
@matthewp There shouldn't be any need to bring the QUnit object in as a module import. It is meant to bootstrap the HTML page for you and takes care of itself. The test suites should reference Could you explain what problem or advantage you are currently addressing by using QUnit in this way? I'd prefer not to maintain multiple build outputs, but I could consider it or come up with simpler solutions, if I understood the use case better. Thanks! |
Maybe I'm not following but I need to reference the QUnit object to do: QUnit.test('my test').... The advantage to it being available as a module is the same as for any module. |
@matthewp The |
Same problem here. Test modules that require QUnit al import QUnit, and I need to build them before the can be used. |
@Krinkle Yeah maybe I wasn't being clear about my reasoning here. Yes, QUnit works fine as a global via a classic script tag. There's nothing that doesn't work about it. It's just that I would rather my modules explicitly import their dependencies and not rely on external dependency management (having to remember to always include a script tag in the right order on the page). So the reason to have QUnit be a module is the same reason to have any dependency to be a module. |
Same for me: Having explicit imports makes code easier to reason about for me: When I read the tests, I want to know where objects come from. Currently I need to know that QUnit comes as a global when I run my tests in a browser window. |
This will also resolve #1724 once done. |
== Background == In QUnit 2.x, we always set the QUnit global in browser contexts, but outside browsers, we only set the global if CJS/AMD wasn't detected. Except in in the QUnit CLI we still set the global anyway, because that's how most QUnit tests are written, incliuding for Node.js. So really the only case where the QUnit global is missing is: * AMD context, where QUnit would be a local argument name in the define/require() callback in each file. * Custom test runners that 1) run in Node.js, and 2) require qunit directly and 3) don't also export it as a global. I'm aware a growing proportion of users import 'qunit' directly in each test file, to aid TypeScript for example. That's a great way to avoid needing to rely on globals. But, it's not a requirement, and is not always an option, especially in a browser context with a simple no-build-step project. == Why == 1. Improve portability between test runners. Make it QUnit's responsiblity to define this reliably, as indeed it almost always already does, instead of requiring specific test runners to patch this up on their end. Esp in light of Karma deprecation, and emergence of more general purpose TAP runners, I'd like to remove this as factor that might make/break QUnit support in those. 2. Prepare for native ESM build. We don't natively support ESM exports yet, but once we do this will become a problem. To prevent split-brain problems around test registry and other state, standardise internally on whichever globalThis.QUnit was defined first, and then reliably export that to any importers, regardless of import method. Ref #1551.
Quoting here to ease discovery. From #1729 (comment):
|
=== What === Turn html.js into an HtmlReporter class where the HtmlReporter.init static method contains (most) of the browser runner logic that we would run even if the HtmlReporter were turned off. This patch primarily deals with making html.js itself free of side-effects by delaying any browser setup logic to the init() function. In a future patch we can take this further and move the logic to a "browser runner" function of sorts, such that what is left in the reporter can be made conditional on the presence of `div[id=qunit]` and/or `QUnit.config.reporters.html`. This is tracked in qunitjs#1118 and qunitjs#1711. Ref qunitjs#1486. === Why === We need html.js to be side-effect free in order to add native ESM support, tracked at qunitjs#1551. To support native ESM exports, we need to introduce a separate distribution file since ESM export syntax isn't allowed in scripts. I expect numerous projects to involve mixed ESM imports and CJS require usage, especially when integration layers and plugins are involved. To avoid a split-brain problem, this means we need to somehow have both export the same one. The way I intend to do this is by detecting if QUnit was already defined, and if so, discard our local definition and use that one instead. This means we need to only init the browser runner only if we're the first one (e.g. window.onerror, QUnit.begin for fixture, QUnit.testDone for HTML reporting, etc). To accomplish that, start by making html.js side-effect free, deferring its init code to a (for now) unconditional call at the end of qunit.js.
=== What === Turn html.js into an HtmlReporter class where the HtmlReporter.init static method contains (most) of the browser runner logic that we would run even if the HtmlReporter were turned off. This patch primarily deals with making html.js itself free of side-effects by delaying any browser setup logic to the init() function. In a future patch we can take this further and move the logic to a "browser runner" function of sorts, such that what is left in the reporter can be made conditional on the presence of `div[id=qunit]` and/or `QUnit.config.reporters.html`. This is tracked in qunitjs#1118 and qunitjs#1711. Ref qunitjs#1486. === Why === We need html.js to be side-effect free in order to add native ESM support, tracked at qunitjs#1551. To support native ESM exports, we need to introduce a separate distribution file since ESM export syntax isn't allowed in scripts. I expect numerous projects to involve mixed ESM imports and CJS require usage, especially when integration layers and plugins are involved. To avoid a split-brain problem, this means we need to somehow have both export the same one. The way I intend to do this is by detecting if QUnit was already defined, and if so, discard our local definition and use that one instead. This means we need to only init the browser runner only if we're the first one (e.g. window.onerror, QUnit.begin for fixture, QUnit.testDone for HTML reporting, etc). To accomplish that, start by making html.js side-effect free, deferring its init code to a (for now) unconditional call at the end of qunit.js.
=== What === Turn html.js into an HtmlReporter class where the HtmlReporter.init static method contains (most) of the browser runner logic that we would run even if the HtmlReporter were turned off. This patch primarily deals with making html.js itself free of side-effects by delaying any browser setup logic to the init() function. In a future patch we can take this further and move the logic to a "browser runner" function of sorts, such that what is left in the reporter can be made conditional on the presence of `div[id=qunit]` and/or `QUnit.config.reporters.html`. This is tracked in qunitjs#1118 and qunitjs#1711. Ref qunitjs#1486. === Why === We need html.js to be side-effect free in order to add native ESM support, tracked at qunitjs#1551. To support native ESM exports, we need to introduce a separate distribution file since ESM export syntax isn't allowed in scripts. I expect numerous projects to involve mixed ESM imports and CJS require usage, especially when integration layers and plugins are involved. To avoid a split-brain problem, this means we need to somehow have both export the same one. The way I intend to do this is by detecting if QUnit was already defined, and if so, discard our local definition and use that one instead. This means we need to only init the browser runner only if we're the first one (e.g. window.onerror, QUnit.begin for fixture, QUnit.testDone for HTML reporting, etc). To accomplish that, start by making html.js side-effect free, deferring its init code to a (for now) unconditional call at the end of qunit.js.
=== What === Turn html.js into an HtmlReporter class where the HtmlReporter.init static method contains (most) of the browser runner logic that we would run even if the HtmlReporter were turned off. This patch primarily deals with making html.js itself free of side-effects by delaying any browser setup logic to the init() function. In a future patch we can take this further and move the logic to a "browser runner" function of sorts, such that what is left in the reporter can be made conditional on the presence of `div[id=qunit]` and/or `QUnit.config.reporters.html`. This is tracked in #1118 and #1711. Ref #1486. Intentional changes: * Use of previousFailure is no longer conditional on QUnit.config.reorder in order to make it more stateless. The message already did not state that it relates to config.reorder, and stating that it previously failed is accurate either way. * Calling ev.preventDefault() is no longer conditional. This has been supported since IE9. QUnit 2 already required IE9+, and QUnit 3.0 will require IE11. jQuery removed similar check in jQuery 2.2.0, with commit jquery/jquery@a873558436. === Why === We need html.js to be side-effect free in order to add native ESM support, tracked at #1551. To support native ESM exports, we need to introduce a separate distribution file since ESM export syntax isn't allowed in scripts. I expect numerous projects to involve mixed ESM imports and CJS require usage, especially when integration layers and plugins are involved. To avoid a split-brain problem, this means we need to somehow have both export the same one. The way I intend to do this is by detecting if QUnit was already defined, and if so, discard our local definition and use that one instead. This means we need to only init the browser runner only if we're the first one (e.g. window.onerror, QUnit.begin for fixture, QUnit.testDone for HTML reporting, etc). To accomplish that, start by making html.js side-effect free, deferring its init code to a (for now) unconditional call at the end of qunit.js.
This refactors the initFixture and initUrlconfig code to be an exported callable so that it is safe to define without side-effects and can then be called conditionally. The previous code was already conditionally with an inline check for window/document. This is now centralised in prep for making it further conditional on whether or not QUnit was already exported, thus making it safe to load QUnit twice (e.g. ESM and CJS without split-brain conflict). Tracked at #1551. Follows-up e1e03e6. Closes #1118.
This refactors the initFixture and initUrlconfig code to be an exported callable so that it is safe to define without side-effects and can then be called conditionally. The previous code was already conditionally with an inline check for window/document. This is now centralised in prep for making it further conditional on whether or not QUnit was already exported, thus making it safe to load QUnit twice (e.g. ESM and CJS without split-brain conflict). Tracked at #1551. Follows-up e1e03e6. Closes #1118.
== Background == In QUnit 2.x, we always set the QUnit global in browser contexts, but outside browsers, we only set the global if CJS/AMD wasn't detected. Except in the QUnit CLI, where we do set the global anyway, because that's how most QUnit tests are written, incliuding for Node.js. So really the only case where the QUnit global is missing is custom test runners that: * run in Node.js, * and require/import qunit.js directly, * and don't export it as a global. I'm aware a growing proportion of developers import 'qunit' directly in each test file for improved type support. That's a great way to avoid needing to rely on globals. But, it's not a requirement, and is not always an option, especially for simple no-build-step and browser-facing projects. == Why == 1. Improve portability between test runners. Remove the last edge case where the QUnit global can be undefined. Make it QUnit's responsiblity to define this reliably, as indeed it almost always already does. Remove this as undocumented requirement for specific test runners to patch up on their end. In light of Karma deprecation, and emergence of more general purpose TAP runners, I'd like to remove this as factor that might make/break QUnit support in one of those. 2. Prepare for native ESM build. We don't natively support ESM exports yet, but once we do this will become a problem. To prevent split-brain problems with mixed use (e.g. in test registry and other state) standardise internally on which ever globalThis.QUnit was defined first, and then reliably export that to any importers. Ref qunitjs#1551.
== Background == In QUnit 2.x, we always set the QUnit global in browser contexts, but outside browsers, we only set the global if CJS/AMD wasn't detected. Except in the QUnit CLI, where we do set the global anyway, because that's how most QUnit tests are written, incliuding for Node.js. So really the only case where the QUnit global is missing is custom test runners that: * run in Node.js, * and require/import qunit.js directly, * and don't export it as a global. I'm aware a growing proportion of developers import 'qunit' directly in each test file for improved type support. That's a great way to avoid needing to rely on globals. But, it's not a requirement, and is not always an option, especially for simple no-build-step and browser-facing projects. == Why == 1. Improve portability between test runners. Remove the last edge case where the QUnit global can be undefined. Make it QUnit's responsiblity to define this reliably, as indeed it almost always already does. Remove this as undocumented requirement for specific test runners to patch up on their end. In light of Karma deprecation, and emergence of more general purpose TAP runners, I'd like to remove this as factor that might make/break QUnit support in one of those. 2. Prepare for native ESM build. We don't natively support ESM exports yet, but once we do this will become a problem. To prevent split-brain problems with mixed use (e.g. in test registry and other state) standardise internally on which ever globalThis.QUnit was defined first, and then reliably export that to any importers. Ref qunitjs#1551.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for qunitjs#1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
== Background == In QUnit 2.x, we always set the QUnit global in browser contexts, but outside browsers, we only set the global if CJS/AMD wasn't detected. Except in the QUnit CLI, where we do set the global anyway, because that's how most QUnit tests are written, incliuding for Node.js. So really the only case where the QUnit global is missing is custom test runners that: * run in Node.js, * and require/import qunit.js directly, * and don't export it as a global. I'm aware a growing proportion of developers import 'qunit' directly in each test file for improved type support. That's a great way to avoid needing to rely on globals. But, it's not a requirement, and is not always an option, especially for simple no-build-step and browser-facing projects. == Why == 1. Improve portability between test runners. Remove the last edge case where the QUnit global can be undefined. Make it QUnit's responsiblity to define this reliably, as indeed it almost always already does. Remove this as undocumented requirement for specific test runners to patch up on their end. In light of Karma deprecation, and emergence of more general purpose TAP runners, I'd like to remove this as factor that might make/break QUnit support in one of those. 2. Prepare for native ESM build. We don't natively support ESM exports yet, but once we do this will become a problem. To prevent split-brain problems with mixed use (e.g. in test registry and other state) standardise internally on which ever globalThis.QUnit was defined first, and then reliably export that to any importers. Ref qunitjs#1551.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for qunitjs#1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
== Background == In QUnit 2.x, we always set the QUnit global in browser contexts, but outside browsers, we only set the global if CJS/AMD wasn't detected. Except in the QUnit CLI, where we do set the global anyway, because that's how most QUnit tests are written, incliuding for Node.js. So really the only case where the QUnit global is missing is custom test runners that: * run in Node.js, * and require/import qunit.js directly, * and don't export it as a global. I'm aware a growing proportion of developers import 'qunit' directly in each test file for improved type support. That's a great way to avoid needing to rely on globals. But, it's not a requirement, and is not always an option, especially for simple no-build-step and browser-facing projects. == Why == 1. Improve portability between test runners. Remove the last edge case where the QUnit global can be undefined. Make it QUnit's responsiblity to define this reliably, as indeed it almost always already does. Remove this as undocumented requirement for specific test runners to patch up on their end. In light of Karma deprecation, and emergence of more general purpose TAP runners, I'd like to remove this as factor that might make/break QUnit support in one of those. 2. Prepare for native ESM build. We don't natively support ESM exports yet, but once we do this will become a problem. To prevent split-brain problems with mixed use (e.g. in test registry and other state) standardise internally on which ever globalThis.QUnit was defined first, and then reliably export that to any importers. Ref qunitjs#1551.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for #1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for #1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for #1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for #1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
In particular, events.js was the only test we had in which a QUnit test was loading another QUnit instance. This fails as-is after the "Always define globalThis.QUnit" patch for #1551, because it would return the instance that's already running instead of a separate one. It will still be possible to do this, by adding `delete global.QUnit` before the import, but that's not the purpose of the events test. Instead, I've added a separate test that proves inception works.
== Background == In QUnit 2.x, we always set the QUnit global in browser contexts, but outside browsers, we only set the global if CJS/AMD wasn't detected. Except in the QUnit CLI, where we do set the global anyway, because that's how most QUnit tests are written, incliuding for Node.js. So really the only case where the QUnit global is missing is custom test runners that: * run in Node.js, * and require/import qunit.js directly, * and don't export it as a global. I'm aware a growing proportion of developers import 'qunit' directly in each test file for improved type support. That's a great way to avoid needing to rely on globals. But, it's not a requirement, and is not always an option, especially for simple no-build-step and browser-facing projects. == Why == 1. Improve portability between test runners. Remove the last edge case where the QUnit global can be undefined. Make it QUnit's responsiblity to define this reliably, as indeed it almost always already does. Remove this as undocumented requirement for specific test runners to patch up on their end. In light of Karma deprecation, and emergence of more general purpose TAP runners, I'd like to remove this as factor that might make/break QUnit support in one of those. 2. Prepare for native ESM build. We don't natively support ESM exports yet, but once we do this will become a problem. To prevent split-brain problems with mixed use (e.g. in test registry and other state) standardise internally on which ever globalThis.QUnit was defined first, and then reliably export that to any importers. Ref qunitjs#1551.
* Add explicit file extensions as per ESM standard, instead of relying on Node.js-specific require() resolution, or Rollup-specific ESM resolution for Node.js compat. * As minor prep for external ESM support, move UMD export to the /src/qunit.js entrypoint. I considered adding an `export` statement to the entrypoint, but this interfers with generating the CJS distribution with Rollup the way we do today. - Adding `export default` to /src/qunit.js, creates a Rollup error about `output: none` for input files that perform exports. - Setting `output: defaults` in Rollup config, creates a warning about having an export while using `format: iife` but having set no name for it. - Adding a name would change the output in a meaningful way, namely it would create an (implied global) variable in the form of `var QUnit = (function () { … return QUnit; }());` - Creating such variable using `var` instead of as `window.QUnit` means that it cannot be unset via `delete window.QUnit` which breaks certain test, and is out of the scope for this pathc. * Fix fragile code in stracktrace.js that previously worked only because of Babel transformations masking a violation of the Temporal Dead Zone between `const fileName` and the functions it uses to compute that value. ``` $ node --experimental-detect-module Welcome to Node.js v21.1.0. Type ".help" for more information. > await import("./src/qunit.js"); Uncaught ReferenceError: Cannot access 'fileName' before initialization at extractStacktrace (file:///Users/krinkle/Development/qunit/src/core/stacktrace.js:51:5) at sourceFromStacktrace (file:///Users/krinkle/Development/qunit/src/core/stacktrace.js:81:10) at file:///Users/krinkle/Development/qunit/src/core/stacktrace.js:35:19 at … at async REPL ``` After this: ``` > await import("./src/qunit.js"); //> null > (await import("./src/core.js")).default; //> { version: …, module: …, test: … } ``` Ref #1551.
* Add explicit file extensions as per ESM standard, instead of relying on Node.js-specific require() resolution, or Rollup-specific ESM resolution for Node.js compat. * As minor prep for external ESM support, move UMD export to the /src/qunit.js entrypoint. I considered adding an `export` statement to the entrypoint, but this interfers with generating the CJS distribution with Rollup the way we do today. - Adding `export default` to /src/qunit.js, creates a Rollup error about `output: none` for input files that perform exports. - Setting `output: defaults` in Rollup config, creates a warning about having an export while using `format: iife` but having set no name for it. - Adding a name would change the output in a meaningful way, namely it would create an (implied global) variable in the form of `var QUnit = (function () { … return QUnit; }());` - Creating such variable using `var` instead of as `window.QUnit` means that it cannot be unset via `delete window.QUnit` which breaks certain test, and is out of the scope for this pathc. * Fix fragile code in stracktrace.js that previously worked only because of Babel transformations masking a violation of the Temporal Dead Zone between `const fileName` and the functions it uses to compute that value. ``` $ node --experimental-detect-module Welcome to Node.js v21.1.0. Type ".help" for more information. > await import("./src/qunit.js"); Uncaught ReferenceError: Cannot access 'fileName' before initialization at extractStacktrace (file:///Users/krinkle/Development/qunit/src/core/stacktrace.js:51:5) at sourceFromStacktrace (file:///Users/krinkle/Development/qunit/src/core/stacktrace.js:81:10) at file:///Users/krinkle/Development/qunit/src/core/stacktrace.js:35:19 at … at async REPL ``` After this: ``` > await import("./src/qunit.js"); //> null > (await import("./src/core.js")).default; //> { version: …, module: …, test: … } ``` Ref #1551.
Summary of recent changes, mostly around getting
|
In 05e15ba, I converted QUnit.start() to be generated by createStartFunction(QUnit) to avoid a circular dependency. This doesn't work in practice with ESM. See inline comment added for details. Ref qunitjs#1551.
Follows-up 05e15ba, which made this into a factory function, but that has the downside of making the QUnit object not defined in one object literal, which makes a few other things reasier to reason about. In a way, it's more honest to say that start is the product of a factory function, but I'd prefer to maintain the simplicity of an uncoupled literal declaration the entire API, in particular in prep for native ESM export (ref #1551). I'll accept in return the internal responsiblity to not call start() "incorrectly" (i.e. before it is ready). This responsibility does not leak into, complicate, break, or otherwise change the public API, and is mitigated by a runtime detection, for the benefit of future contributors and maintainers to QUnit.
Follows-up 05e15ba, which made this into a factory function, but that has the downside of making the QUnit object not defined in one object literal, which makes a few other things reasier to reason about. In a way, it's more honest to say that start is the product of a factory function, but I'd prefer to maintain the simplicity of an uncoupled literal declaration the entire API, in particular in prep for native ESM export (ref #1551). I'll accept in return the internal responsiblity to not call start() "incorrectly" (i.e. before it is ready). This responsibility does not leak into, complicate, break, or otherwise change the public API, and is mitigated by a runtime detection, for the benefit of future contributors and maintainers to QUnit.
First released with QUnit 3.0.0-alpha.4. Ref qunitjs/qunit#1798. Ref qunitjs/qunit#1551.
First released with QUnit 3.0.0-alpha.4. Ref qunitjs/qunit#1798. Ref qunitjs/qunit#1551.
An ESM build would be nice given that browser support for modules is mature these days.
Currently to use modules when testing with QUnit I do something like this:
local_qunit.js
test.js
Given that qunit is using rollup for the build it should be relatively straight-foward to add a second build output for use with
<script type="module">
so you would be able to do:The text was updated successfully, but these errors were encountered: