Skip to content

test: modernise the test packs (Node + Jasmine runner, Jasmine for comparison tests, Babel 7) - #1718

Merged
johnnyreilly merged 11 commits into
nextfrom
execution-tests-refine
Nov 9, 2026
Merged

johnnyreilly merged 11 commits into
nextfrom
execution-tests-refine

Conversation

@johnnyreilly

@johnnyreilly johnnyreilly commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Modernises the test packs:

  1. Dependencies and conventions: drop the TypeScript version prefixes from the test folder names, install shared test dependencies once at the root, and move Babel 6 → 7 (comparison tests included).
  2. Replace Karma and headless Chrome with a Node-based runner that uses Jasmine 7.
  3. Run the comparison tests with Jasmine too, and remove mocha, so the repo has one test framework.
  4. Upgrade the React execution tests to React 18, merge overlapping Babel tests, add a test for the inert loader options, and share one set of test.json skip rules between both packs.

Webpack 4 is still tested throughout.

1. Dependencies and conventions

Folder names: drop the TypeScript version prefixes

  • The 18 prefixed folders are renamed by feature, e.g. 3.0.1_projectReferences → projectReferences, 4.7.0_5.0.0_node16-file-suffix → node16-file-suffix. ts-loader now needs TypeScript 7.1+, and every prefix was ≤ 5.6.2, so the TypeScript check always passed.
  • The webpack part of the prefix still mattered for two tests (node16-file-suffix and optionsCaching). They now declare it in a test.json ({ "minWebpack": 5 }), which run-tests.js reads, comparing webpack's major version. The TypeScript check and the isLowerVersion helper are removed.
  • src/index.ts has a comment pointing at resolveModuleName, and it's updated for the new name.

Shared test dependencies installed once

  • jasmine and @types/jasmine live in the root package.json. The per-test jasmine-core ^2.3.4 and @types/jasmine ^2.x are removed, and so is the root jasmine-core ^6, which nothing loaded (Karma used karma-jasmine's own 4.6.1).
  • 15 tests no longer need a package.json or lockfile of their own, so the runner skips npm install for them.

Babel 6 → Babel 7 (execution and comparison tests)

  • The root babel-core, babel-loader@7, babel-polyfill and babel-preset-es2015/es2016/react are replaced by @babel/core, @babel/preset-env and @babel/preset-react (^7.29.0) with babel-loader@^8.4.1, shared by both test packs. Babel 8 is out, but babel-loader@8 (the last version that supports webpack 4) requires @babel/core 7.
  • The per-test Babel installs are removed. Five Babel execution tests now have no package.json at all.
  • Configs use @babel/preset-env (with modules: false, as before) in place of es2015+es2016, and @babel/preset-react in place of react. The syntax-dynamic-import plugin is gone: Babel 7 parses import() by default.
  • babel-polyfill is removed, including from importCodeSplitting/main.js, a non-Babel test that only worked because the root happened to install it. Babel 7.18+ inlines the regenerator runtime that the async tests need.
  • The three Babel comparison tests (babel-issue81, babel-issue92, babel-es6resolveParent) have regenerated snapshots. The diffs are Babel 7's helpers and formatting, with the same compiled code.
  • pnpm/pnpm-lock.yaml is regenerated to remove the jasmine entries. CI's pnpm uses a frozen lockfile.

2. Run the tests in Node with Jasmine instead of Karma

No test needed a browser: the two React specs use the shallow renderer, and the only uses of document are in app entry files that the specs never load.

How a test now runs

New test/execution-tests/run-specs.js, run from each test's directory by run-tests.js:

  • It builds the test's main.js (which loads its *.tests.ts specs with require.context) with target: 'node' and devtool: 'inline-source-map', into a temporary folder that's deleted afterwards. The mode, module, resolve and resolveLoader settings come from the test's webpack.config.js, the same parts test/karmaConfig.js passed to Karma.
  • It then runs the bundle with Jasmine 7, as a library in the same process (exitOnCompletion = false). Source maps are enabled, so failure stacks point at the TypeScript source, e.g. webpack:///test/app.tests.ts:9:52.
  • A test fails if the build has errors, if any spec fails, or if no specs ran. Jasmine reports the last case as incomplete, as it does when a focused fit/fdescribe is left in.
  • --watch (with --single-test) uses webpack's watch mode and runs the specs in a fresh child process after each successful build, so Jasmine's globals and the module cache start clean. Ctrl+C deletes the temporary folder.
  • --debug still runs node --inspect-brk=5858, so the VS Code "Run open execution test" config works unchanged. The build and the specs now run in one process, so breakpoints work in both src/ and the specs.
  • Tests without a main.js (just optionsCaching) are still built by run-webpack.js.
  • allowJs-entryFileIsJs was the one test whose karma.conf.js listed spec files with a glob instead of main.js. It now has a main.js like the rest.

Build errors now fail the test

Karma ran the specs even when webpack reported errors, which hid two broken tests:

  • basic: lib/externalLib.d.ts used declare module externalLib { ... }, which TypeScript 7 rejects (TS1540). It's now declare namespace externalLib.
  • resolveModuleName: the resolveModuleName loader option is accepted but does nothing on the TypeScript 7 API (as Support TypeScript 7.1 API #1704 notes), so foo/bar/baz don't resolve for type-checking (4 × TS2307). The specs passed only because webpack's own alias resolved the modules at runtime. It's now skipped, with the same reason as loaderOptions (which covers the also-inert getCustomTransformers). Both tests now have "skip": "<reason>" in their test.json, so each can be turned back on by deleting that line if the API adds these hooks.

Dependencies

  • Removed: karma, karma-chrome-launcher, karma-jasmine, karma-mocha-reporter, karma-sourcemap-loader, karma-webpack (181 packages out of node_modules), the 26 per-test karma.conf.js files and test/karmaConfig.js.
  • Added: jasmine@^7.0.0 and @types/jasmine@^7.0.0. All the existing specs type-check and run unchanged on Jasmine 7.
  • CI's webpack 4 step now installs only webpack@^4.47.0, without karma-webpack@^4.0.2.

Notes

  • Building with target: 'node' instead of Karma's web changes how webpack resolves packages: it ignores the browser field, and code-split chunks load with require instead of script tags. Every test passes either way. If a future test needs the browser build of a package, it can set resolve.aliasFields: ['browser'] in its webpack config, and run-specs.js passes that through.
  • Jasmine runs specs in random order by default, as karma-jasmine did. The default reporter prints dots plus failure details instead of each spec's name.

3. Comparison tests: mocha → Jasmine

The comparison harness used mocha lightly (one describe, two its, a timeout and done callbacks), while the execution tests now use Jasmine. Moving the harness is a small change in two files; moving the execution specs to mocha would have meant rewriting about 70 expect calls. Startup time is about the same (Jasmine is ~18ms faster per process, about 1s across the 168s suite), so this is about having one framework, not speed.

  • create-and-execute-test.js: the 90s timeout moves from mocha's this.timeout(90000) to it()'s third argument. done callbacks work unchanged. Its own arguments now come after Jasmine's -- marker (-- --test-to-run=<name> [--save-output]), so they're parsed with util.parseArgs in strict mode instead of being mixed with the test runner's.
  • run-tests.js runs node node_modules/jasmine/bin/jasmine.js --random=false --color create-and-execute-test.js -- ... with execFileSync (no shell). --random=false keeps the transpileOnly run after the normal one. picocolors also reads the --color flag, so webpack's output keeps its colours and the snapshots are unchanged. --debug still pauses on port 5858.
  • run-tests.js now skips testLib (the library the npmLink test links to) and folders with an _IGNORE_ file before starting them. Mocha passed them with zero specs, but Jasmine reports that as incomplete. So the suite count drops from 57 to 56; no real test is lost.
  • globals.d.ts (hand-written mocha types) is replaced by @types/jasmine in the comparison tests' tsconfig.json.
  • Removed mocha (16 packages). knip now sees that jasmine and @types/jasmine are used, so they're off its ignore list along with mocha. .claude/settings.json allows the Jasmine commands instead of mocha's.

4. React 18, test merges, shared skip rules

React 15/16 → 18

  • react, babel-react and pnpm move to React 18.3.1, the newest React that webpack 4 can still bundle (verified on webpack 4.47).
  • React 18 drops the shallow renderer and react-addons-test-utils, so the specs render with react-dom/server's renderToStaticMarkup and compare the HTML. App components are plain functions (React.SFC is gone from the types), and the app entry files, which the specs don't load, use createRoot.
  • react compiles JSX with "jsx": "react-jsx". babel-react keeps "preserve" and uses @babel/preset-react's automatic runtime.
  • pnpm pins React 18.3.1 exactly, because its spec checks the version resolved through pnpm's layout. Its lockfile is regenerated.

Overlapping Babel tests merged (8 → 6)

  • babel-es2015 merges into babel-es2016. Both put ts-loader's output through @babel/preset-env, so one TypeScript target covers them. All 7 specs are kept, in es2015.tests.ts and es2016.tests.ts.
  • babel-importCodeSplitting becomes babel-codeSplitting and takes the old test's require.ensure spec, so one test covers both ways webpack splits code. The old test's "a and b" spec duplicated one already in app.tests.ts.

New inertLoaderOptions test

A build that sets resolveModuleName, resolveTypeReferenceDirective and getCustomTransformers (accepted but unused on the TypeScript 7 API) still succeeds and runs. Each option is set to something that would work if it were used. This keeps the backwards-compatibility promise covered while the resolveModuleName and loaderOptions tests are skipped. Adding an option ts-loader doesn't accept makes it fail with "unexpected loader option".

One set of skip rules for both packs

  • New test/testConfig.js reads a test's test.json ("skip": "<reason>" and "minWebpack"), used by both runners.
  • The comparison runner's hard-coded skip list (projectReferences*, customTransformer*, nolib, tsconfigInvalidFile) becomes a test.json with the reason in each of those 23 tests, and the reason is printed when skipping. The unused, undocumented _IGNORE_ marker is replaced by "skip".
  • --single-test now applies these skips too. Before, --single-test projectReferences ran the test despite the skip list; now it says why it's skipped. To try a skipped test, remove its "skip" line.

Other cleanup

  • Removed the unused isNotBabelTest filter from run-tests.js. It was commented out, and its message about webpack support was outdated.

  • Rewrote test/execution-tests/README.md for the new runner, test.json, and the root dependencies. AGENTS.md is updated to match.

  • knip.json: ignoreDependencies now lists jasmine, @types/jasmine and the Babel 7 packages instead of Karma, Babel 6 and jasmine-core.

  • test.json holds every execution test skip rule: "minWebpack" and "skip": "<reason>". One isRunnableTest in run-tests.js reads and validates it.

  • --single-test now applies the same skips as a full run: test.json (execution), and testLib / _IGNORE_ folders (comparison, through a shared isRunnableTest). --watch, --debug and the VS Code launch configs all go through --single-test.

  • An invalid test.json (bad JSON, a minWebpack that isn't a positive whole number, or an empty skip) fails that test instead of stopping the whole run.

  • run-specs.js deletes its temporary folder from an exit handler, so the folder is also removed when webpack.config.js or webpack() throws, and on SIGINT/SIGTERM/SIGHUP in watch mode.

  • --watch stops the previous spec run as soon as webpack sees a change, so its output can't be mistaken for the new edit's (even if the rebuild fails), and stops it before the temporary folder is deleted on exit.

  • The README says that with --watch the specs run in a fresh process, so breakpoints in the specs aren't hit under --debug (ones in src/ still are).

  • --match-test is applied before test.json is read, so only the selected tests' test.json can skip or fail them.

  • run-specs.js exits once the specs finish (keeping the exit code), so a spec that leaves a timer or socket open can't hang the run, as Karma's single run couldn't. The temp folder cleanup on exit no longer throws if the folder can't be removed (e.g. on Windows, when a stopped spec process still has the bundle open).

  • Jasmine treats spec paths as glob patterns, so a path containing [ or { would match nothing. run-specs.js passes the bundle with addSpecFile (taken as-is), and the comparison runner passes create-and-execute-test.js relative to the working directory, so neither the temp directory nor the checkout path can hide the specs.

  • The comparison runner also applies --match-test before its skip checks, so skip messages are only logged for the selected tests.

Testing (local, macOS, Node 26, TypeScript 7.1.0-beta)

  • Execution tests, webpack 5: 25/25 suites pass (loaderOptions and resolveModuleName skipped). Before the Babel merges and the new test, every suite ran the same number of specs as under Karma on next. 16s, down from about 50s on next.
  • Execution tests, webpack 4.47: 23/23 pass (node16-file-suffix and optionsCaching also skipped, via test.json), including React 18.
  • Checked that failures fail: a failing expect, a type error in a spec, and a main.js that matches no specs each exit with 1.
  • --watch: re-ran after a spec was edited, and cleaned up on Ctrl+C. --debug: pauses with the debugger listening on 5858.
  • Comparison tests on Jasmine: 56/56 pass, with snapshots unchanged; editing a snapshot makes the test fail as expected.
  • Plus lint, knip, format:check and the three check: scripts.

🤖 Generated with Claude Code

- Rename execution test folders by feature, dropping the TypeScript
  version prefixes (ts-loader needs TypeScript 7.1+, so they always
  passed). Tests that need webpack 5 declare it in a test.json
  ({ "minWebpack": 5 }), which run-tests.js reads.
- Install @types/jasmine once at the root (v4, matching the
  jasmine-core 4.6 that karma-jasmine loads). Remove the unused
  per-test jasmine-core 2.x and root jasmine-core 6. 15 tests no
  longer need their own package.json or lockfile.
- Move the execution and comparison tests from Babel 6 to Babel 7
  with babel-loader 8 (the last version that supports webpack 4),
  installed once at the root. Drop babel-polyfill: Babel 7.18+ inlines
  the regenerator runtime. Regenerate the three Babel comparison
  snapshots.
- Update README, AGENTS.md, knip.json and stale comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Benchmark (macOS)

Scenario transpileOnly PR branch median (ms) base branch median (ms) Δ vs base branch
Cold build false 1265.5 1270.9 -0.4%
Incremental rebuild (leaf touch) false 117.3 119.7 -2.0%
Incremental rebuild (hub touch) false 644.8 637.9 +1.1%
Cold build true 512.1 512.5 -0.1%
Incremental rebuild (leaf touch) true 64.7 69.2 -6.6%
Incremental rebuild (hub touch) true 72.3 70.7 +2.3%
Cold build (all orphan files) false 1279.9 1294.5 -1.1%
Incremental rebuild (hub touch, all orphan files) false 680.3 708.4 -4.0%

PR branch = /Users/runner/work/ts-loader/ts-loader, base branch = /Users/runner/work/ts-loader/ts-loader-main. 2 warmup + 10 measured iterations per scenario, median reported. Report-only - no threshold fails this check.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Benchmark (Ubuntu)

Scenario transpileOnly PR branch median (ms) base branch median (ms) Δ vs base branch
Cold build false 1222.2 1222.9 -0.1%
Incremental rebuild (leaf touch) false 55.4 55.9 -0.9%
Incremental rebuild (hub touch) false 485.1 483.7 +0.3%
Cold build true 392.0 393.3 -0.3%
Incremental rebuild (leaf touch) true 33.8 32.8 +3.2%
Incremental rebuild (hub touch) true 34.6 35.6 -2.7%
Cold build (all orphan files) false 1302.3 1298.3 +0.3%
Incremental rebuild (hub touch, all orphan files) false 520.2 519.1 +0.2%

PR branch = /home/runner/work/ts-loader/ts-loader, base branch = /home/runner/work/ts-loader/ts-loader-main. 2 warmup + 10 measured iterations per scenario, median reported. Report-only - no threshold fails this check.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Benchmark (Windows)

Scenario transpileOnly PR branch median (ms) base branch median (ms) Δ vs base branch
Cold build false 924.5 928.4 -0.4%
Incremental rebuild (leaf touch) false 42.8 42.6 +0.4%
Incremental rebuild (hub touch) false 377.7 377.2 +0.1%
Cold build true 289.6 293.3 -1.3%
Incremental rebuild (leaf touch) true 29.9 30.8 -2.9%
Incremental rebuild (hub touch) true 29.3 28.5 +2.6%
Cold build (all orphan files) false 1031.1 1040.1 -0.9%
Incremental rebuild (hub touch, all orphan files) false 413.1 415.3 -0.5%

PR branch = C:\source\ts-loader, base branch = C:\source\ts-loader-main. 2 warmup + 10 measured iterations per scenario, median reported. Report-only - no threshold fails this check.

Each test's main.js is now built for Node (target: 'node', inline source
map) using the module/resolve/resolveLoader settings from its
webpack.config.js, the same parts Karma used, and the bundle is run with
Jasmine 7 in-process. A test fails on build errors, failing specs, or no
specs running. --watch rebuilds and re-runs specs in a fresh process;
--debug still pauses on port 5858, now covering ts-loader and the specs.

- Remove karma, karma-chrome-launcher, karma-jasmine,
  karma-mocha-reporter, karma-sourcemap-loader, karma-webpack, the
  per-test karma.conf.js files and test/karmaConfig.js. Add jasmine 7
  and @types/jasmine 7.
- Karma ran tests even when the build had errors, which hid two:
  - basic: fix `declare module externalLib` (TS1540 on TypeScript 7)
    to `declare namespace`.
  - resolveModuleName: the option is inert on the TypeScript 7 API, so
    skip it alongside loaderOptions until the API supports it.
- allowJs-entryFileIsJs gets a main.js like the other tests.
- CI no longer installs karma-webpack@4 for the webpack 4 runs.
- Update README, AGENTS.md and knip.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnnyreilly johnnyreilly changed the title test: modernise execution test pack deps; move Babel tests to Babel 7 test: modernise the execution test pack (Node + Jasmine runner, Babel 7, shared deps) Nov 8, 2026
The comparison harness used mocha lightly (one describe, two its, a
timeout and done callbacks), while the execution tests already use
Jasmine. Standardise on Jasmine.

- create-and-execute-test.js: the 90s timeout moves from this.timeout
  to it()'s third argument. Its arguments now follow Jasmine's `--`
  marker and are parsed strictly.
- run-tests.js starts Jasmine with --random=false (keeps the
  transpileOnly run after the normal one) and --color, which picocolors
  also reads, so the colored snapshots are unchanged.
- run-tests.js skips testLib (the library the npmLink test links to)
  and _IGNORE_ folders itself. Mocha passed them with zero specs;
  Jasmine reports that as incomplete.
- Replace the hand-written mocha globals with @types/jasmine, remove
  mocha (16 packages), and update knip.json and .claude/settings.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnnyreilly johnnyreilly changed the title test: modernise the execution test pack (Node + Jasmine runner, Babel 7, shared deps) test: modernise the test packs (Node + Jasmine runner, Jasmine for comparison tests, Babel 7) Nov 8, 2026
johnnyreilly and others added 8 commits November 8, 2026 18:46
- --single-test now applies the same skips as a full run: test.json's
  minWebpack and the TypeScript 7 unsupported tests (execution), and
  testLib / _IGNORE_ folders (comparison, via a shared isRunnableTest).
  --watch, --debug and the VS Code launch config all use --single-test.
- An invalid test.json (bad JSON, or minWebpack not a positive whole
  number) now fails that test instead of stopping the whole run, or
  silently never skipping on a string like "5.0.0".
- run-specs.js deletes its temp folder from an 'exit' handler, so it is
  also removed when webpack.config.js or webpack() throws, and on
  SIGINT/SIGTERM/SIGHUP in watch mode.
- README: with --watch, specs run in a fresh process, so breakpoints in
  the specs aren't hit under --debug (ones in src still are).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The TypeScript 7 skips for loaderOptions and resolveModuleName move
  from a hard-coded map in run-tests.js into those tests' test.json, as
  `"skip": "<reason>"`. One isRunnableTest reads and validates test.json
  (minWebpack and skip) for both full and --single-test runs.
- run-specs.js --watch stops the previous spec run as soon as webpack
  sees a change (so a stale run's output can't be mistaken for the new
  edit's, even if the rebuild fails), and before the temp folder is
  deleted on exit.
- runTests no longer normalises the test name a second time; both
  callers pass a folder name.
- README and AGENTS.md describe the test.json "skip" field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- run-tests.js applies --match-test before reading test.json, so only
  the selected tests' test.json can skip or fail them.
- run-specs.js exits once the specs finish (keeping the exit code), so
  a spec that leaves a timer or socket open can't hang the run, as
  Karma's single run couldn't.
- The temp folder cleanup on exit no longer throws if the folder can't
  be removed (e.g. on Windows, when a stopped spec process still has
  the bundle open).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jasmine treats spec paths as glob patterns, so a path containing `[` or
`{` matched nothing and every test failed with "No specs found".

- run-specs.js passes the bundle with addSpecFile (taken as-is) instead
  of execute([path]), for temp directories with such characters.
- The comparison runner passes create-and-execute-test.js relative to
  the working directory, for checkouts with such characters (mocha took
  the path literally).
- The comparison runner applies --match-test first, like the execution
  runner, so skip messages are only logged for the selected tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
react, babel-react and pnpm move from React 15/16 to 18.3.1, the
newest React that webpack 4 can still bundle.

- React 18 drops the shallow renderer and react-addons-test-utils, so
  the specs render with react-dom/server's renderToStaticMarkup and
  compare the HTML, which also runs in plain Node.
- App components are plain functions (React.SFC is gone from the
  types); the unused app entry files use createRoot.
- react compiles JSX with "jsx": "react-jsx"; babel-react keeps
  "preserve" and uses @babel/preset-react's automatic runtime.
- pnpm pins React 18.3.1 exactly, as its spec checks the resolved
  version, and its lockfile is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- babel-es2015 merges into babel-es2016: both put ts-loader's output
  through @babel/preset-env, so one TypeScript target covers them. All 7
  specs are kept, in es2015.tests.ts and es2016.tests.ts (Jasmine 7
  rejects duplicate top-level suite names, so each is named for what
  it checks).
- babel-importCodeSplitting becomes babel-codeSplitting and takes the
  old babel-codeSplitting's require.ensure spec, so one test covers both
  ways webpack splits code. The old test's "a and b" spec duplicated
  one in app.tests.ts, so it isn't carried over.
- New inertLoaderOptions test: a build that sets resolveModuleName,
  resolveTypeReferenceDirective and getCustomTransformers (accepted but
  unused on the TypeScript 7 API) still succeeds and runs. This keeps
  the backwards-compatibility promise covered while the resolveModuleName
  and loaderOptions tests are skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New test/testConfig.js reads a test's test.json ("skip" reason and
  "minWebpack"), used by both runners.
- The comparison runner's hard-coded skip list (projectReferences*,
  customTransformer*, nolib, tsconfigInvalidFile) becomes a test.json
  with the reason in each of those 23 tests, and the reason is printed
  when skipping. The unused, undocumented _IGNORE_ marker is replaced
  by "skip". --single-test now applies these skips too, and an invalid
  test.json fails that test.
- Document the skip rules in the comparison README and AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnnyreilly
johnnyreilly merged commit ed921aa into next Nov 9, 2026
45 checks passed
@johnnyreilly
johnnyreilly deleted the execution-tests-refine branch November 9, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant