Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,19 @@ jobs:

# `@argos-ci/vitest` supports Vitest 4 and 5. The workspace is pinned to 5, so
# these two jobs put 4 back and re-run the same suites against it. They upload
# under their own build names — a build name owns one baseline, and the two
# majors are separate baselines even when the pixels agree.
# under their own build names — a build name owns one baseline, and each
# Vitest version tested is a separate baseline even when the pixels agree.
e2e-vitest-compat:
timeout-minutes: 5
strategy:
fail-fast: false
matrix:
node-version: [lts/*]
os: [ubuntu-latest]
# 4.0.0 is the oldest supported release: it predates `TestRunner` and
# the task `fullName`, so it runs the SDK's fallbacks. `4` is the latest
# 4.x.
vitest-version: ["4.0.0", "4"]

runs-on: ${{ matrix.os }}

Expand All @@ -321,17 +325,21 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Downgrade to Vitest 4
- name: Downgrade to Vitest ${{ matrix.vitest-version }}
run: >-
pnpm --filter=@argos-ci/vitest up "vitest@^4" "@vitest/browser@^4"
"@vitest/browser-playwright@^4" "@vitest/pretty-format@^4"
pnpm --filter=@argos-ci/vitest up
"vitest@${{ matrix.vitest-version }}"
"@vitest/browser@${{ matrix.vitest-version }}"
"@vitest/browser-playwright@${{ matrix.vitest-version }}"
"@vitest/pretty-format@${{ matrix.vitest-version }}"

- name: Run integration tests
run: pnpm exec -- turbo run e2e-compat --filter=@argos-ci/vitest
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
NODE_VERSION: ${{ matrix.node-version }}
OS: ${{ matrix.os }}
VITEST_VERSION: ${{ matrix.vitest-version }}

e2e-storybook-vitest-compat:
timeout-minutes: 10
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"install-playwright": "playwright install chromium --with-deps",
"build-e2e": "pnpm run install-playwright",
"e2e": "cross-env BUILD_NAME=\"argos-vitest-e2e-node-$NODE_VERSION-$OS\" UPLOAD_TO_ARGOS=true pnpm run test-e2e",
"e2e-compat": "cross-env BUILD_NAME=\"argos-vitest4-e2e-node-$NODE_VERSION-$OS\" UPLOAD_TO_ARGOS=true pnpm run test-e2e",
"e2e-compat": "cross-env BUILD_NAME=\"argos-vitest$VITEST_VERSION-e2e-node-$NODE_VERSION-$OS\" UPLOAD_TO_ARGOS=true pnpm run test-e2e",
"check-types": "tsc",
"check-format": "prettier --check --ignore-unknown --ignore-path=./.gitignore --ignore-path=../../.gitignore --ignore-path=../../.prettierignore .",
"lint": "eslint ."
Expand Down
23 changes: 22 additions & 1 deletion packages/vitest/src/auto-name.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { TestRunner } from "vitest";
import { resolveAutoName } from "./auto-name";
import { getCurrentTest } from "./test-context";

// The real implementation, unless a test swaps in a task of its own.
vi.mock(import("./test-context"), async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, getCurrentTest: vi.fn(actual.getCurrentTest) };
});

it("returns an explicit name verbatim", async () => {
await expect(resolveAutoName("button")).resolves.toBe("button");
Expand All @@ -22,6 +29,20 @@ it("restarts the counter for each test", async () => {
await expect(resolveAutoName()).resolves.toBe(`${testName} 1`);
});

describe("on Vitest < 4.0.14", () => {
it("rebuilds the full name, which tasks do not have yet", async () => {
const test = TestRunner.getCurrentTest()!;
vi.mocked(getCurrentTest).mockResolvedValueOnce({
id: test.id,
name: test.name,
file: test.file,
suite: test.suite,
});
// Same name as from the real `fullName`, so upgrading Vitest keeps it.
await expect(resolveAutoName()).resolves.toBe(`${test.fullName} 1`);
});
});

it("includes the test file path so names are unique across files", async () => {
const file = TestRunner.getCurrentTest()?.file?.name;
expect(file).toBe("src/auto-name.test.ts");
Expand Down
10 changes: 7 additions & 3 deletions packages/vitest/src/auto-name.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getTitlePath } from "./metadata";
import { getCurrentTest } from "./test-context";

/**
Expand Down Expand Up @@ -73,11 +74,14 @@ export async function resolveAutoName(
const count = (counters.get(test) ?? 0) + 1;
counters.set(test, count);

// Vitest sets `fullName` from 4.0.14 on; before that, its own `getFullName()`
// joined the title path.
const testFullName = test.fullName ?? getTitlePath(test).join(" > ");
const file = test.file?.name;
const fullName =
file && !test.fullName.startsWith(file)
? `${file} > ${test.fullName}`
: test.fullName;
file && !testFullName.startsWith(file)
? `${file} > ${testFullName}`
: testFullName;

// Reserve room for the trailing counter and the caller's suffix, then keep
// the whole name within the filesystem limit. The counter always survives
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type TestMetadata = ScreenshotMetadata["test"];
* Build the title path of a task (`[file, ...describes, title]`), replicating
* Vitest's own `getNames` helper so it matches the framework's conventions.
*/
function getTitlePath(task: CurrentTask): string[] {
export function getTitlePath(task: CurrentTask): string[] {
const names = [task.name];
let current: CurrentSuite = task;
while (current.suite) {
Expand Down
45 changes: 28 additions & 17 deletions packages/vitest/src/test-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export interface CurrentSuite {
export interface CurrentTask extends CurrentSuite {
id: string;
name: string;
fullName: string;
/** Path of the file, describe blocks and title (Vitest >= 4.0.14). */
fullName?: string | undefined;
file: { name: string; filepath: string };
/** Tags declared on the test (Vitest >= 4). */
tags?: string[] | undefined;
Expand All @@ -36,25 +37,38 @@ export interface CurrentTask extends CurrentSuite {
}

/**
* Entry point exposing `getCurrentTest` before Vitest 4.1, and removed
* altogether in Vitest 5.
* Global where Vitest keeps the state of the worker running the tests, in Node
* and in the browser alike. Vitest internal: only read on 4.0, a release line
* that no longer changes.
*/
const WORKER_STATE_GLOBAL = "__vitest_worker__";

/**
* Get the running test from Vitest's worker state, for Vitest 4.0.
*
* Held in a variable rather than written inline at the import: Vite's
* dependency pre-bundler resolves a literal specifier eagerly, and on Vitest 5
* that fails the whole optimize step over an export that no longer exists —
* even though the branch importing it cannot run there.
* Vitest 4.0 exports `getCurrentTest()` only from `vitest/suite`, which this
* package cannot import: written inline, the specifier fails Vite's dependency
* optimizer on Vitest 5, where that export is gone; hidden from Vite, it
* reaches the browser unresolved. The test runner also keeps the running task
* on the worker state, and Vitest 4.0's own browser commands
* (`page.screenshot()`) read it from there. The task is the test while it runs,
* hooks included, and its suite or file otherwise.
*/
const LEGACY_SUITE_ENTRY = "vitest/suite";
function getCurrentTestFromWorkerState(): CurrentTask | undefined {
const state = (globalThis as Record<string, unknown>)[WORKER_STATE_GLOBAL] as
{ current?: CurrentTask & { type?: string } } | undefined;
const task = state?.current;
return task?.type === "test" ? task : undefined;
}

/**
* Get the current Vitest test task, or `undefined` when not inside a test.
*
* Vitest >= 4.1 exposes `TestRunner.getCurrentTest()` from the `vitest` entry
* point; the `vitest/suite` export is deprecated there and gone in Vitest 5. We
* prefer the new API and fall back to `vitest/suite` for older 4.x. Both are
* imported dynamically so importing `@argos-ci/vitest` in a non-Vitest
* environment does not pull Vitest in — only call this once you know Vitest is
* available.
* point; on Vitest 4.0, we read the worker state instead (see
* {@link getCurrentTestFromWorkerState}). `vitest` is imported dynamically so
* importing `@argos-ci/vitest` in a non-Vitest environment does not pull Vitest
* in — only call this once you know Vitest is available.
*/
export async function getCurrentTest(): Promise<CurrentTask | undefined> {
const vitest = (await import("vitest")) as {
Expand All @@ -64,8 +78,5 @@ export async function getCurrentTest(): Promise<CurrentTask | undefined> {
if (runner?.getCurrentTest) {
return runner.getCurrentTest();
}
const suite = (await import(/* @vite-ignore */ LEGACY_SUITE_ENTRY)) as {
getCurrentTest: () => CurrentTask | undefined;
};
return suite.getCurrentTest();
return getCurrentTestFromWorkerState();
}
1 change: 1 addition & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"ARGOS_BUILD_NUMBER",
"NODE_VERSION",
"OS",
"VITEST_VERSION",
"GITHUB_*"
],
"dependsOn": ["build-e2e"],
Expand Down
Loading