Skip to content

fix: bug fixes for 0.6.1 - #69

Merged
Huldoser merged 21 commits into
mainfrom
fixes-0.6.1
Oct 30, 2026
Merged

Huldoser merged 21 commits into
mainfrom
fixes-0.6.1

Conversation

@Huldoser

Copy link
Copy Markdown
Owner

Bug fixes for 0.6.1, found in a round of runner comparisons (Playwright 1.64.1, Vitest 5.0.5, 4.1.12, 4.0.18 and 3.2.x), a review of the 0.5.0 and 0.6.0 code, and the upstream release notes. One commit and one changeset per fix; each fix has a test that failed first.

Test states (Vitest)

  • A test's own skip, only, todo or fails option replaces its tag's, also when set to false.
  • A retry object before Vitest 4.1 gets an unresolved-option warning and retries: null: Vitest 4.0 and 3.2 never run such a test.
  • Vitest 5.0.5 rules: the last of .skip, skipIf() and runIf() decides, a skipIf() or runIf() replaces the skip of a tag or of a describe's options, and an option set to undefined changes nothing.

dependencyHash, dependencies and --related

  • Files that only re-export, as a barrel index.ts does, are in dependencies, so --related keeps their tests.
  • An imported file that isn't UTF-8 is hashed by its bytes, with line endings read as \n unless git takes it for binary.
  • A tsconfig paths pattern with no matching file falls back to the usual lookup, as TypeScript does.
  • A .jsx import finds its .tsx file.
  • A component <script> block whose type isn't JavaScript, such as JSON-LD, isn't read as code.
  • An @/ or ~/ import that no tsconfig paths define marks the test unfollowed, with a warning.

Configs

  • Vitest projects without test.name are named as Vitest names them; Vitest 5 names a listed config's projects inside its name, as web (unit). An unknown test.name gets a warning.
  • Playwright's defineConfig(base, config) merges projects by name and use, expect and build by key.
  • Playwright 1.64 projects with default: false are left out unless a running project pulls them in; unknown dependencies leave that open, with a warning.
  • Playwright's config-level tag is read.

Parsing

Checks: npm run coverage passes, 1,033 tests and 100% for every file in src/; typecheck, lint and format are clean. Golden changes: dependencyHash in trading-web (its export { expect } from '@playwright/test'), a warning in the Vitest 3.0 and 4.0 syntax tours, and one expectCount in chart-browser.

Vitest merges a test's options over its tags' options (`assignDefined` in
5.0.5, a spread in 4.1.12), so `skip: false`, `todo: false`, `fails: false`
or `only: false` on a test undoes what its tag sets, and a conditional
`skip` replaces the tag's unconditional one. The scan kept the tag's state.
Checked with Vitest 5.0.5: all four tests run.

Closes TI-21.
…e test

Before 4.1 Vitest compares the attempt number with the retry option, so an
object makes the comparison false and the test body never runs (checked
with Vitest 4.0.18 and 3.2.4: zero attempts, reported as pending). The scan
ignored the object and filled in the config's count, which hid that.

A retry object in a test's or describe's options below 4.1 now gives
`retries: null` from where it is written, with an `unresolved-option`
warning. One in the config keeps `null` with source `config`: config
objects carry no position for a warning, and Vitest 4.0's types reject it.

Closes TI-15.
Vitest 5.0.5 (vitest-dev/vitest#11591) made `skipIf()` and `runIf()` set a
test's `skip` in its chain context, and merges options with `assignDefined`,
which leaves out `undefined`. So the last of `.skip`, `skipIf()` and
`runIf()` decides, `test.skip.skipIf(false)` runs, a `skipIf()` or `runIf()`
replaces a tag's skip, and `{ skip: undefined }` no longer undoes
`test.skip`. Each case was run with Vitest 5.0.5 and 4.1.12; 4.1 keeps the
old behaviour, which the scan still gives for it.

The scan keys rules on major and minor only, so it applies these to all
of Vitest 5.

Chose: apply the 5.0.5 rules to all of 5.0 over adding patch versions to
the version checks (about 20 call sites), as the scan already takes each
minor's newest rules.
Imports were read as UTF-8 text and line endings normalised, so every
byte that isn't UTF-8 became U+FFFD and three different PNGs gave the
same dependencyHash. A file that isn't valid UTF-8 now keeps its bytes,
which the hash takes as they are, as it already does for screenshots.
Its decoded text stays as before, so a code file with a stray Latin-1
byte still parses.
TypeScript tries the files of a matching `paths` pattern and, when none
exists, goes on to baseUrl and node_modules. The scan stopped at the
alias, so a catch-all such as `"*": ["./src/types/*"]` made 'vitest',
'decimal.js' and every other package unreadable, and every test got
hasUnfollowedDependencies, which keeps it in every --related run.

When nothing is found after the fallback either, a matched alias still
gives "could not be read", so a broken alias keeps its warning.
TypeScript maps a .jsx extension in an import to the .tsx source, as it maps .js to .ts. The resolver only stripped .js, .mjs and .cjs, so './OrderHeader.jsx' with OrderHeader.tsx on disk was unreadable and the test was marked unfollowed.
A Svelte component with JSON-LD in <svelte:head> had that block joined to its script, so the component failed to parse, its imports were dropped from dependencies and its tests were marked unfollowed. Only blocks with no type, type="module" or a JavaScript MIME type are code, as in HTML.
A barrel that only re-exports added no code to the hash, so it was in no
test's dependencies, and --related left out every test that imports
through it. Pointing `export * from './fees'` at './fees-eu' changed the
hash but kept no test, while `vitest related` runs the test.

Each re-export a name passes through now adds a unit for the statement,
as an import already does, since which module a name comes from is code
too. The old test said files that only re-export had nothing to list;
its expectation now includes them. The trading-web golden changes only
in dependencyHash, from its `export { expect } from '@playwright/test'`.

Chose: list re-exporting files over documenting the gap, since the docs
promise --related keeps every test a change to a file can affect.
An import such as `@/pricing`, set up only in Vite's resolve.alias, was
taken for an installed package: the aliased file was missing from
dependencies, hasUnfollowedDependencies stayed false and no warning
said so, so --related left the test out when that file changed.

npm allows no package name that starts with `@/` or `~/`, so such an
import is a path alias for sure. When no tsconfig paths define it, the
test is now marked unfollowed with a dependency-not-followed warning,
and the unresolved-test-import message names it as an alias. Three
tests that import `test` from '@/fixtures' without a usable tsconfig
expected the old wording.

Chose: mark these aliases now and leave reading resolve.alias from the
config to a follow-up, as that needs the config reader to pass aliases
to the resolver; other aliases, such as @app/pricing, still read as
packages, which the limits page now says.
…l alias

With "paths": { "*": [...] }, an import with no file under the alias returned 'could not be read' unless it was installed or a workspace package, so 'node:fs', 'path' and packages before npm ci still marked tests unfollowed. TypeScript goes on to its usual lookup for any name, so only an @/ or ~/ import, which can't be a package, keeps that reason now.
Vitest 5.0.5 merges a test's options over those its describe passes down, and skipIf()/runIf() now set the test's own skip, so test.runIf(true) inside describe.only('...', { skip: true }) runs (checked with 5.0.5; 4.1.12 skips it). The scan only let a skip in the options object replace the describe's.
Hashing a Latin-1 CSV by its raw bytes made its dependencyHash differ between a Windows and a Linux checkout, as git rewrites the line endings of any file it takes for text. Bytes of a file git takes for text, with no NUL byte in its first 8,000, now have their line endings read as \n, as UTF-8 text already does; a binary file such as a PNG stays byte for byte.
oxc-parser recurses on the native stack. A file with brackets nested about
5,000 deep overflows the main thread's 8 MB stack (2,400 in a worker
thread's 4 MB) and the process dies with SIGSEGV, which no try/catch can
stop, so one generated fixture killed the whole scan and no inventory was
written. oxc has no depth guard (oxc-project/oxc#24375, #21953) and 0.156.0
is the newest release.

parseFile now counts bracket nesting in one pass over the text before
parsing, skipping strings, comments, regular expressions and template text,
and returns a parse error for a file nested more than 500 deep. The check
takes about 6% of parseSync's time on typescript.js (31 ms against 499 ms
for 9.1 M characters) and refuses none of the 10,091 files in node_modules,
the deepest of which nests 34.

Chose: a bracket-depth pre-check over parsing in a worker with a larger
stack (a worker only raises the crash depth, a SIGSEGV in it still kills
the process, and the AST would have to be copied back), and over counting
operator chains too (ternaries, arrows, else-if and unary chains crash only
past 10,000 levels on the main thread and are not nesting written by hand).
Chose: a limit of 500, five times below the crash depth in a 4 MB worker
stack and fifteen times above the deepest real file found.
In Vitest browser mode, `await expect.element(locator).toHaveTextContent()`
is the usual assertion, and it is `expect.poll` with a locator. The scan
counted only `expect()`, `expect.soft()` and `expect.poll()`, so such tests
had `expectCount: 0` and looked like tests without assertions; the
chart-browser golden showed it for "shows the prices of the candle under
the pointer", whose only assertion is an `expect.element()`.

`expect.element` is in @vitest/browser from 2.0.0 (not in 1.6.0), so every
Vitest version the scan supports, 3.0 and newer, has it.

Chose: count it in Playwright files too, as expectCount counts the calls
written and the extractor reads `expect` the same way for both frameworks;
Playwright has no `expect.element`, so it only appears there by mistake.
A Vitest project without a test.name got the name "", so a monorepo with
projects: ['packages/*'] listed every test under "". Vitest (3.2, 4.1 and 5)
names such a project after the name in the package.json of its folder, else
the folder, and an inline project after its index among the inline entries.
Vitest 5 also drops test.name from what an inline project inherits, and
names the projects of a config another config lists inside that config's
name, as "web (unit)"; Vitest 5.0.0 already does both, so the rules apply
to all of Vitest 5.

Two existing expectations were wrong for the runner: a folder project
without a package.json is named after the folder ("quotes", as Vitest
3.2.7, 4.1.12 and 5.0.5 name packages/quotes in a real run), and an
unnamed inline browser project at index 3 has its instance named
"3 (firefox)" (resolveTestConfig names instances after the resolved
project name; not run, as browser mode needs a provider).

Chose: keep "" for an unnamed root config's own project over the
package.json name, as Vitest names only the projects a config lists; and
keep the names a nested config has when started there for its configs
entry, over the names the listing config gives, as that entry describes
the config run on its own.
defineConfig(base, config) was read as a shallow spread, so a config that
overrides one project of its base lost the others: with base projects
setup and chromium (dependencies: ['setup']) and an override of chromium
only, the scan listed chromium alone, without its dependency, where
Playwright 1.64.1 lists [setup] and [chromium]. An override's
expect: { timeout } also dropped the base's toHaveScreenshot.pathTemplate,
so tests comparing screenshots lost hasUnfollowedDependencies.

Playwright's defineConfig (lib/common/index.js in 1.64.1) merges expect,
use and build key by key and projects by name, the project's use key by
key, adds the other projects after the base's, keeping the last of each
name, and takes the first config as it is. The scan now does the same.

Chose: leave webServer as replaced, over joining its lists, as the scan
doesn't read it; and take the projects as unknown when a project's name
can't be worked out, over guessing which ones merge.
Playwright 1.64 added testProject.default: without --project, a project
with default: false doesn't run, unless a project that runs lists it in
dependencies or teardown (filterProjects and buildProjectsClosure in
1.64.1). The scan ignored it and listed such a project in the projects of
its tests: with a slow project set to default: false, npx playwright test
--list lists no test of it, and the scan gave ["slow"].

The scan now leaves such a project out from 1.64, and keeps the ones that
running projects pull in through dependencies or teardown, transitively.

Chose: take a project that runs by default as pulling in its dependencies
even when none of its tests are scanned, over working out whether it has
tests, which Playwright checks but a per-file scan can't; and give
projects: null with a config-value-unknown warning for a default the scan
can't work out, over taking the project as running.
Playwright (1.57 and later) gives every test the config's top-level tag:
loadTestFile starts each file suite with config.tags, so the tag is first
in test.tags and in the text grep and grepInvert match, right after the
file path. The scan ignored it: with tag: '@nightly', npx playwright test
--list in 1.64.1 gives "returns a quote for AAPL" the tags ["@nightly"],
where the scan gave [], and a project with grepInvert: /@nightly/ runs no
test, where the scan listed it.

The tag now comes first in each test's tags and in the grep text. When it
can't be worked out, the tests of a project with a grep get projects: null
and the config a config-value-unknown warning where it writes tag.

src/records.ts takes the tags from FileConfig.configTags, as the config's
placement is what knows them.

Chose: leave out a tag the scan can't work out, over keeping its known
items, as part of a list is no truer than none; and report it only where
the config writes tag, over also where unknown code may set it, which the
config's projects already report.
The nesting check read the slash of '</ul>' and of '} />' as the start of a regular expression, which then ran over the closing brackets after it, so each component left braces open and a file of 500 small components got a parse-error that oxc doesn't give. A slash after '<', or before '>' right after a '}', now ends a tag. Over the 173 .tsx and .jsx files at hand the deepest nesting is 8.
When a running Playwright project's dependencies or teardown can't be worked out, as in dependencies: process.env.CI ? ['setup'] : [], a project with default: false was taken as not running, so its tests had no projects and a wrong file-not-run warning; Playwright 1.64.1 runs it when the list names it. Such a list now leaves the other projects unknown, with a config-value-unknown warning at the setting.
A test.name such as process.env.DESK fell back to the project's folder or index without a sign, a plausible name that Vitest doesn't give (4.1.12 and 5.0.0 name it after the variable's value). It now gets a config-value-unknown warning that says what the scan names it instead.
@Huldoser
Huldoser merged commit 41ce74d into main Oct 30, 2026
31 checks passed
@Huldoser
Huldoser deleted the fixes-0.6.1 branch October 30, 2026 14:12
@Huldoser Huldoser mentioned this pull request Oct 30, 2026
2 of 4 tasks
Huldoser added a commit that referenced this pull request Oct 30, 2026
## What changed

Release 0.6.1, made with `npm run release`.

### Patch Changes

- [#69](#69)
[`41ce74d`](41ce74d)
- An import that matches a tsconfig `paths` pattern but none of its
files, as an installed package does with
`"paths": { "*": ["./src/types/*"] }`, now goes on to the usual lookup,
as TypeScript does. Such imports were reported
as unreadable, so every test that used a package or a built-in module
such as `node:fs` got
`hasUnfollowedDependencies`. An `@/` or `~/` alias whose files are
missing is still reported.

- [#69](#69)
[`41ce74d`](41ce74d)
- A file that only passes names on, as a barrel `index.ts` with `export
* from './fees'` or
`export { OrderTicket } from './order-ticket'` does, is now in the
`dependencies` of the tests that import through it,
so `--related` keeps them when the file changes. Which module a
re-export points at is now part of `dependencyHash`, as
an import's already was, so the hash of a test that imports through a
re-export changes once.

- [#69](#69)
[`41ce74d`](41ce74d)
- An imported file that isn't text, such as `import logo from
'./logo.png'`, is now taken by its bytes in
`dependencyHash`. It was read as text, where every byte that isn't UTF-8
reads the same, so two different images could
  give the same hash.

- [#69](#69)
[`41ce74d`](41ce74d)
- A `<script>` block in a Vue or Svelte component whose `type` isn't
JavaScript, such as
`<script type="application/ld+json">` in `<svelte:head>`, is no longer
read as the component's code. It made the
component unreadable, so the modules its real script imports were
missing from `dependencies`.

- [#68](#68)
[`fa9b97a`](fa9b97a)
- `dependencyHash` now follows what a Vue or Svelte single-file
component imports, also the components and helpers only
its template uses, so a change to a child component changes the hash of
the tests that mount its parent. A component
whose script doesn't parse is still taken by its text, and its tests get
`hasUnfollowedDependencies`.

- [#69](#69)
[`41ce74d`](41ce74d)
- A file with brackets nested more than 500 deep, such as a generated
fixture, now gets a `parse-error` instead of
crashing the scan. oxc-parser overflows the native stack on such a file,
about 5,000 deep, and the process dies with a
segmentation fault that no error handling can catch, so no inventory was
written.

- [#69](#69)
[`41ce74d`](41ce74d)
- `expectCount` now counts `expect.element()`, the assertion of Vitest
browser mode, so a browser test that checks the
page only with `await expect.element(locator).toBeVisible()` no longer
looks like a test without assertions.

- [#69](#69)
[`41ce74d`](41ce74d)
- An import written with a `.jsx` extension now finds the `.tsx` file it
refers to, as `.js` already found `.ts`, so
`import { OrderHeader } from './OrderHeader.jsx'` is followed instead of
marking the test with
  `hasUnfollowedDependencies`.

- [#69](#69)
[`41ce74d`](41ce74d)
- From Playwright 1.57, the config's `tag` is now read: every test gets
it first in its `tags`, and a project's `grep` or
`grepInvert` matches it, so a project with `grepInvert: /@nightly/` in a
config with `tag: '@nightly'` no longer runs
the tests. When the scan can't work out the `tag`, as for
`process.env.CI_ENVIRONMENT_NAME`, it gets a
`config-value-unknown` warning, and the tests of a project with a `grep`
get `projects: null`.

- [#69](#69)
[`41ce74d`](41ce74d)
- From Playwright 1.64, a project with `default: false` is no longer
listed in a test's `projects`, as Playwright runs it
only with `--project`, unless a project that runs lists it in
`dependencies` or `teardown`. A `default` the scan can't
work out gets a `config-value-unknown` warning, and the tests it may run
get `projects: null`.

- [#69](#69)
[`41ce74d`](41ce74d)
- Playwright's `defineConfig(base, config)` is now merged as Playwright
merges it: a project of `config` merges into the
project of `base` with the same name instead of replacing all of
`base`'s projects, and `expect`, `use` and `build`
merge key by key. A setup project from a base config is no longer lost,
and an `expect.toHaveScreenshot.pathTemplate`
from the base still marks the tests that compare screenshots with
`hasUnfollowedDependencies`.

- [#69](#69)
[`41ce74d`](41ce74d)
- A Vitest `retry` object in a test's or describe's options before
Vitest 4.1 now gets an `unresolved-option` warning
and `retries: null`. Vitest 4.0 and 3.2 never run such a test; the scan
used to ignore the object and give the
  config's retry count instead.

- [#69](#69)
[`41ce74d`](41ce74d)
- A Vitest test's own `skip`, `only`, `todo` or `fails` option now
replaces the one its tags set, as Vitest does, so
`test('title', { tags: ['flaky'], skip: false }, fn)` is active instead
of skipped, and a conditional `skip` replaces
  the tag's unconditional one.

- [#69](#69)
[`41ce74d`](41ce74d)
- An import that starts with `@/` or `~/`, as a Vite `resolve.alias`
allows, now gives the test
`hasUnfollowedDependencies` and a `dependency-not-followed` warning when
no tsconfig `paths` define it. It was taken for
an installed package, so the aliased file was missing from
`dependencies` without a sign, and `--related` left the test
  out when that file changed.

- [#69](#69)
[`41ce74d`](41ce74d)
- Vitest 5 tests follow the option rules of Vitest 5.0.5: the last of
`.skip`, `skipIf()` and `runIf()` on a test
decides its skip, so `test.skip.skipIf(false)` is active, and a
`skipIf()` or `runIf()` replaces the skip of a tag or of
the options a describe passes down. An option set to `undefined`, as in
`test.skip('title', { skip: undefined }, fn)`,
  no longer undoes the chain or a tag.

- [#69](#69)
[`41ce74d`](41ce74d)
- In Vitest 5, the projects of a config that another config lists in
`projects` are named inside that config's name, as
Vitest names them: the `unit` project of `apps/web/vitest.config.ts`,
named `web`, is `web (unit)`.

- [#69](#69)
[`41ce74d`](41ce74d)
- A Vitest project without a `test.name` now has the name Vitest gives
it, in `projects` and in `configs`: the `name` in
the package.json of its folder, else the folder's name, so `projects:
['packages/*']` names its projects
`@trading/pricing` or `ledger` instead of `""`. An inline project
without a name is named after its place among the
inline projects, as `0`, and in Vitest 5 no longer takes the name of the
config it extends.

## Checklist

- [ ] Tests cover the change, and `npm run coverage` reports 100%.
- [ ] The docs describe the change: the guide or reference pages, the
README, or the TSDoc of exported functions.
- [x] No docs change is needed.
- [x] A changeset describes the change for users (`npm run changeset`),
or the change does not affect them.
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