mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(test): discover UI unit tests with browser in their names (#163493)
* fix(test): discover UI unit tests with browser in their names Use the native full-basename selector while preserving the dependency-free planning matcher. Compare registered root and package inventories so browser-import unit tests keep their Node owner. * fix(test): align project inventory with native discovery
This commit is contained in:
parent
f1d9422fbd
commit
aa9d51a24f
8 changed files with 203 additions and 48 deletions
21
test/fixtures/vitest-ui-runtime-partition.mjs
vendored
21
test/fixtures/vitest-ui-runtime-partition.mjs
vendored
|
|
@ -20,9 +20,26 @@ const ctx = await createVitest({
|
|||
try {
|
||||
const emptyDiscoveryAllowed = Boolean(ctx.config.passWithNoTests);
|
||||
const specifications = await ctx.globTestSpecifications();
|
||||
process.env.OPENCLAW_VITEST_POST_SHARD_INCLUDE_FILE = includeFile;
|
||||
const paths = (files) =>
|
||||
files.map((file) => path.relative(process.cwd(), file.moduleId).replaceAll("\\", "/")).sort();
|
||||
const packageNodeFiles = paths(
|
||||
specifications.filter((file) => ["unit", "unit-node"].includes(file.project.name)),
|
||||
);
|
||||
const rootCtx = await createVitest({
|
||||
config: path.resolve("test/vitest/vitest.ui.config.ts"),
|
||||
watch: false,
|
||||
reporters: [],
|
||||
configLoader: "runner",
|
||||
api: false,
|
||||
cache: false,
|
||||
});
|
||||
let rootNodeFiles;
|
||||
try {
|
||||
rootNodeFiles = paths(await rootCtx.globTestSpecifications());
|
||||
} finally {
|
||||
await rootCtx.close();
|
||||
}
|
||||
process.env.OPENCLAW_VITEST_POST_SHARD_INCLUDE_FILE = includeFile;
|
||||
const rows = [];
|
||||
for (const index of [undefined, 1, 2, 3]) {
|
||||
ctx.config.shard = index ? { index, count: 3 } : undefined;
|
||||
|
|
@ -215,6 +232,8 @@ try {
|
|||
output,
|
||||
JSON.stringify({
|
||||
discovered: paths(specifications),
|
||||
packageNodeFiles,
|
||||
rootNodeFiles,
|
||||
rows,
|
||||
scheduling,
|
||||
empty: { modules: empty.testModules.length, errors: empty.unhandledErrors.length },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { globSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { globSync as nativeGlobSync } from "tinyglobby";
|
||||
import { fullSuiteVitestShards } from "./vitest/vitest.test-shards.mjs";
|
||||
|
||||
type VitestTestConfig = {
|
||||
|
|
@ -81,9 +82,12 @@ async function listConfigTestFiles(config: VitestConfig, includeFile?: string):
|
|||
const exclude = (testConfig.exclude ?? []).map((pattern) =>
|
||||
path.isAbsolute(pattern) ? toRepoPath(path.relative(dir, pattern)) : toRepoPath(pattern),
|
||||
);
|
||||
return globSync(testConfig.include ?? [], { cwd: dir, exclude }).map((file) =>
|
||||
toRepoPath(path.relative(process.cwd(), path.resolve(dir, file))),
|
||||
);
|
||||
return nativeGlobSync(testConfig.include ?? [], {
|
||||
cwd: dir,
|
||||
ignore: exclude,
|
||||
dot: true,
|
||||
expandDirectories: false,
|
||||
}).map((file) => toRepoPath(path.relative(process.cwd(), path.resolve(dir, file))));
|
||||
}
|
||||
|
||||
export async function listVitestConfigTestFiles(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import os from "node:os";
|
|||
import path from "node:path";
|
||||
import { minimatch } from "minimatch";
|
||||
import { BUNDLED_PLUGIN_TEST_GLOB } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { globSync } from "tinyglobby";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resolveConfig } from "vitest/node";
|
||||
import { useAutoCleanupTempDirTracker } from "./helpers/temp-dir.js";
|
||||
import { normalizeConfigPath } from "./helpers/vitest-config-paths.js";
|
||||
import { createAgentsCoreIsolatedVitestConfig } from "./vitest/vitest.agents-core-isolated.config.ts";
|
||||
import { createAgentsCoreVitestConfig } from "./vitest/vitest.agents-core.config.ts";
|
||||
|
|
@ -228,6 +230,7 @@ describe("createScopedVitestConfig", () => {
|
|||
});
|
||||
|
||||
describe("scoped vitest configs", () => {
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const defaultCliProcessConfig = createCliProcessVitestConfig({});
|
||||
const defaultCliConfig = createCliVitestConfig({});
|
||||
const defaultExtensionTelegramConfig = createExtensionTelegramVitestConfig({});
|
||||
|
|
@ -511,17 +514,28 @@ describe("scoped vitest configs", () => {
|
|||
it("normalizes ui include patterns relative to the scoped dir", () => {
|
||||
const testConfig = requireTestConfig(defaultUiConfig);
|
||||
expect(testConfig.dir).toBe(process.cwd());
|
||||
for (const [file, included] of [
|
||||
const files = [
|
||||
["ui/src/pages/chat/chat-view.test.ts", true],
|
||||
["ui/src/components/form-controls.browser.test.ts", true],
|
||||
["ui/src/components/markdown-mermaid.runtime.browser.test.ts", false],
|
||||
["extensions/workboard/browser/catalog.test.ts", true],
|
||||
["extensions/workboard/browser/native.browser.test.ts", false],
|
||||
] as const) {
|
||||
expect(
|
||||
testConfig.include?.some((pattern) => minimatch(file, pattern)),
|
||||
file,
|
||||
).toBe(included);
|
||||
] as const;
|
||||
const tempDir = tempDirs.make("openclaw-ui-scoped-");
|
||||
for (const [file] of files) {
|
||||
const target = path.join(tempDir, file);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, "");
|
||||
}
|
||||
const discovered = new Set(
|
||||
globSync(testConfig.include ?? [], {
|
||||
cwd: tempDir,
|
||||
dot: true,
|
||||
expandDirectories: false,
|
||||
}),
|
||||
);
|
||||
for (const [file, included] of files) {
|
||||
expect(discovered.has(file), file).toBe(included);
|
||||
}
|
||||
expect(testConfig.exclude).toContain("ui/src/**/*.e2e.test.ts");
|
||||
expect(testConfig.exclude).toContain("extensions/*/browser/**/*.e2e.test.ts");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Vitest UI package config tests validate UI package test project settings.
|
||||
import { globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { globSync as nativeGlobSync } from "tinyglobby";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BrowserProviderOption } from "vitest/node";
|
||||
import { resolveCiTestRuntimeSelections } from "../scripts/lib/ci-test-runtime.mts";
|
||||
|
|
@ -15,7 +16,7 @@ import { runVitestShutdownCommand } from "./helpers/vitest-shutdown-command.js";
|
|||
import { loadVitestPerformanceConfig } from "./vitest/vitest.performance-config.ts";
|
||||
import { DEFAULT_VITEST_TEST_TIMEOUT_MS } from "./vitest/vitest.timeouts.ts";
|
||||
import { createUiIsolatedVitestConfig } from "./vitest/vitest.ui-isolated.config.ts";
|
||||
import { uiTimingTestFiles } from "./vitest/vitest.ui-paths.mjs";
|
||||
import { uiNodeDrivenBrowserTestFiles, uiTimingTestFiles } from "./vitest/vitest.ui-paths.mjs";
|
||||
import { createUiTimingVitestConfig } from "./vitest/vitest.ui-timing.config.ts";
|
||||
import { createUiVitestConfig } from "./vitest/vitest.ui.config.ts";
|
||||
|
||||
|
|
@ -34,6 +35,19 @@ type ExpectedTestConfig = ReturnType<typeof loadVitestPerformanceConfig> & {
|
|||
sequence?: { groupOrder?: number };
|
||||
};
|
||||
|
||||
function globTestFiles(
|
||||
patterns: string | string[],
|
||||
options: { cwd?: string; exclude?: string[] } = {},
|
||||
): string[] {
|
||||
// Use the same discovery engine and options as Vitest, including for scoped owners.
|
||||
return nativeGlobSync(patterns, {
|
||||
cwd: options.cwd,
|
||||
ignore: options.exclude,
|
||||
dot: true,
|
||||
expandDirectories: false,
|
||||
});
|
||||
}
|
||||
|
||||
function requireTestConfig(config: unknown): ExpectedTestConfig {
|
||||
if (!config || typeof config !== "object" || !("test" in config) || !config.test) {
|
||||
throw new Error("expected ui package vitest test config");
|
||||
|
|
@ -117,6 +131,8 @@ describe("ui package vitest config", () => {
|
|||
expect(result.stdout).toContain("[ui-runtime] file shuffle seed: 37");
|
||||
const report = JSON.parse(readFileSync(output, "utf8")) as {
|
||||
discovered: string[];
|
||||
packageNodeFiles: string[];
|
||||
rootNodeFiles: string[];
|
||||
rows: Array<{
|
||||
original: string[];
|
||||
selected: Record<string, Array<{ runtime: string; files: string[] }>>;
|
||||
|
|
@ -150,6 +166,12 @@ describe("ui package vitest config", () => {
|
|||
};
|
||||
};
|
||||
expect(report.discovered.length).toBeGreaterThan(1000);
|
||||
// Package isolated/timing/Chromium projects retain their separate owners.
|
||||
expect(report.rootNodeFiles).toEqual(report.packageNodeFiles);
|
||||
expect(report.rootNodeFiles).toContain(
|
||||
"ui/src/pages/chat/talk/shared.browser-import.test.ts",
|
||||
);
|
||||
expect(report.rootNodeFiles).toEqual(expect.arrayContaining(uiNodeDrivenBrowserTestFiles));
|
||||
expect(report.rows).toHaveLength(4);
|
||||
expect(report.empty).toEqual({ modules: 0, errors: 0 });
|
||||
expect(report.emptyDiscoveryAllowed).toBe(false);
|
||||
|
|
@ -242,18 +264,18 @@ describe("ui package vitest config", () => {
|
|||
const rootIsolated = requireTestConfig(createUiIsolatedVitestConfig({}));
|
||||
expect(packageIsolated?.isolate).toBe(true);
|
||||
expect(rootIsolated.isolate).toBe(true);
|
||||
const packageFiles = globSync(packageIsolated?.include ?? [], {
|
||||
const packageFiles = globTestFiles(packageIsolated?.include ?? [], {
|
||||
cwd: path.join(process.cwd(), "ui"),
|
||||
exclude: packageIsolated?.exclude,
|
||||
}).map((file) => path.posix.normalize(`ui/${file.replaceAll("\\", "/")}`));
|
||||
expect(packageFiles.length).toBeGreaterThan(0);
|
||||
const rootFiles = globSync(rootIsolated.include ?? [], { exclude: rootIsolated.exclude }).map(
|
||||
(file) => file.replaceAll("\\", "/"),
|
||||
);
|
||||
const rootFiles = globTestFiles(rootIsolated.include ?? [], {
|
||||
exclude: rootIsolated.exclude,
|
||||
}).map((file) => file.replaceAll("\\", "/"));
|
||||
expect(rootFiles.toSorted()).toEqual(packageFiles.toSorted());
|
||||
const rootShared = requireTestConfig(createUiVitestConfig({}));
|
||||
expect(
|
||||
globSync(rootShared.include ?? [], { exclude: rootShared.exclude }).filter((file) =>
|
||||
globTestFiles(rootShared.include ?? [], { exclude: rootShared.exclude }).filter((file) =>
|
||||
packageFiles.includes(file.replaceAll("\\", "/")),
|
||||
),
|
||||
).toEqual([]);
|
||||
|
|
@ -341,18 +363,18 @@ describe("ui package vitest config", () => {
|
|||
expect(timing?.sequence?.groupOrder).toBeGreaterThan(project.sequence?.groupOrder ?? 0);
|
||||
}
|
||||
const selected = projects.flatMap((project) =>
|
||||
globSync(project.include ?? [], {
|
||||
globTestFiles(project.include ?? [], {
|
||||
cwd: path.join(process.cwd(), "ui"),
|
||||
exclude: project.exclude,
|
||||
}).map((file) => `ui/${file.replaceAll("\\", "/")}`),
|
||||
);
|
||||
const rootTiming = requireTestConfig(createUiTimingVitestConfig({}));
|
||||
const rootShared = requireTestConfig(createUiVitestConfig({}));
|
||||
const rootFiles = globSync(rootShared.include ?? [], { exclude: rootShared.exclude });
|
||||
const rootFiles = globTestFiles(rootShared.include ?? [], { exclude: rootShared.exclude });
|
||||
for (const project of [rootShared, requireTestConfig(createUiIsolatedVitestConfig({}))]) {
|
||||
expect(rootTiming.sequence?.groupOrder).toBeGreaterThan(project.sequence?.groupOrder ?? 0);
|
||||
}
|
||||
expect(globSync(rootTiming.include ?? [], { exclude: rootTiming.exclude })).toEqual(
|
||||
expect(globTestFiles(rootTiming.include ?? [], { exclude: rootTiming.exclude })).toEqual(
|
||||
uiTimingTestFiles,
|
||||
);
|
||||
for (const file of uiTimingTestFiles) {
|
||||
|
|
@ -391,23 +413,23 @@ describe("ui package vitest config", () => {
|
|||
const browser = projects.find((project) => project.browser?.enabled);
|
||||
const node = projects.find((project) => project.name === "unit-node");
|
||||
const root = requireTestConfig(createUiVitestConfig());
|
||||
const nativeFiles = globSync(browser?.include ?? [], {
|
||||
const nativeFiles = globTestFiles(browser?.include ?? [], {
|
||||
cwd: uiRoot,
|
||||
exclude: browser?.exclude,
|
||||
}).map((file) => `ui/${file}`);
|
||||
const nodeFiles = globSync(node?.include ?? [], {
|
||||
const nodeFiles = globTestFiles(node?.include ?? [], {
|
||||
cwd: uiRoot,
|
||||
exclude: node?.exclude,
|
||||
})
|
||||
.filter((file) => file.endsWith(".browser.test.ts"))
|
||||
.map((file) => `ui/${file}`);
|
||||
const rootFiles = globSync(root.include ?? [], { exclude: root.exclude });
|
||||
const rootFiles = globTestFiles(root.include ?? [], { exclude: root.exclude });
|
||||
expect(nativeFiles).toContain("ui/src/components/markdown-mermaid.runtime.browser.test.ts");
|
||||
expect(nodeFiles).toContain("ui/src/components/form-controls.browser.test.ts");
|
||||
expect(rootFiles.filter((file) => nativeFiles.includes(file))).toEqual([]);
|
||||
expect(rootFiles).toEqual(expect.arrayContaining(nodeFiles));
|
||||
expect([...nativeFiles, ...nodeFiles].toSorted()).toEqual(
|
||||
globSync("ui/src/**/*.browser.test.ts").toSorted(),
|
||||
globTestFiles("ui/src/**/*.browser.test.ts").toSorted(),
|
||||
);
|
||||
const unpartitionedBrowser = requireTestConfig(createUiBrowserVitestConfig({}));
|
||||
expect(unpartitionedBrowser.browser?.provider?.prewarm).toEqual(expect.any(Function));
|
||||
|
|
@ -451,9 +473,9 @@ describe("ui package vitest config", () => {
|
|||
const scopedRoot = requireTestConfig(
|
||||
createUiVitestConfig({ OPENCLAW_VITEST_INCLUDE_FILE: includeFile }),
|
||||
);
|
||||
expect(globSync(scopedRoot.include ?? [], { exclude: scopedRoot.exclude }).toSorted()).toEqual(
|
||||
nodeFiles.toSorted(),
|
||||
);
|
||||
expect(
|
||||
globTestFiles(scopedRoot.include ?? [], { exclude: scopedRoot.exclude }).toSorted(),
|
||||
).toEqual(nodeFiles.toSorted());
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -491,7 +513,7 @@ describe("ui package vitest config", () => {
|
|||
expect(config.root).toBe(uiRoot);
|
||||
const selected = (requireTestConfig(config).projects ?? []).flatMap((project) => {
|
||||
const test = requireTestConfig(project);
|
||||
return globSync(test.include ?? [], { cwd: uiRoot, exclude: test.exclude }).map((file) =>
|
||||
return globTestFiles(test.include ?? [], { cwd: uiRoot, exclude: test.exclude }).map((file) =>
|
||||
path.posix.normalize(`ui/${file.replaceAll("\\", "/")}`),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@ import path from "node:path";
|
|||
|
||||
type GlobMatcher = (value: string, pattern: string) => boolean;
|
||||
|
||||
export const nonBrowserTestBasenamePattern = "!(*.browser.test.ts|!(*.test.ts))";
|
||||
|
||||
export function resolveNonBrowserTestPattern(pattern: string): string | null {
|
||||
if (!pattern.endsWith(nonBrowserTestBasenamePattern)) {
|
||||
return null;
|
||||
}
|
||||
const prefix = pattern.slice(0, -nonBrowserTestBasenamePattern.length);
|
||||
if (prefix && !prefix.endsWith("/") && !prefix.endsWith("\\")) {
|
||||
return null;
|
||||
}
|
||||
return prefix + "!(*.browser).test.ts";
|
||||
}
|
||||
|
||||
export function filterFilesByPatterns(
|
||||
files: readonly string[],
|
||||
include: readonly string[],
|
||||
|
|
@ -113,10 +126,11 @@ function isAtOrUnder(value: string, root: string): boolean {
|
|||
|
||||
function patternIsFullyUnderDirectory(pattern: string, root: string): boolean {
|
||||
const normalized = pattern.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
if (!normalized.endsWith(".test.ts")) {
|
||||
const testPattern = resolveNonBrowserTestPattern(normalized) ?? normalized;
|
||||
if (!testPattern.endsWith(".test.ts")) {
|
||||
return false;
|
||||
}
|
||||
const literalPrefix = literalPrefixForGlobPattern(normalized).replace(/\/+$/u, "");
|
||||
const literalPrefix = literalPrefixForGlobPattern(testPattern).replace(/\/+$/u, "");
|
||||
return isAtOrUnder(literalPrefix, root);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@ import { spawnSync } from "node:child_process";
|
|||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { filterFilesByPatterns, intersectIncludePatterns } from "./vitest.include-patterns.ts";
|
||||
import { globSync } from "tinyglobby";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
import {
|
||||
filterFilesByPatterns,
|
||||
intersectIncludePatterns,
|
||||
nonBrowserTestBasenamePattern,
|
||||
} from "./vitest.include-patterns.ts";
|
||||
import {
|
||||
collectVitestExcludePatterns,
|
||||
matchesVitestCliSelection,
|
||||
|
|
@ -31,16 +37,18 @@ describe("native CLI selection", () => {
|
|||
"--input-type=module",
|
||||
"--eval",
|
||||
`import { matchesVitestGlob } from './test/vitest/vitest.pattern-file.ts';
|
||||
import { filterFilesByPatterns } from './test/vitest/vitest.include-patterns.ts';
|
||||
import { filterFilesByPatterns, nonBrowserTestBasenamePattern } from './test/vitest/vitest.include-patterns.ts';
|
||||
console.log(JSON.stringify(filterFilesByPatterns(
|
||||
['ui/src/example.test.ts', 'ui/src/example.browser.test.ts'],
|
||||
['ui/src/**/!(*.browser).test.ts'], [], matchesVitestGlob
|
||||
['ui/src/example.test.ts', 'ui/src/example.browser-import.test.ts', 'ui/src/example.browser.test.ts'],
|
||||
['ui/src/**/' + nonBrowserTestBasenamePattern], [], matchesVitestGlob
|
||||
)));`,
|
||||
],
|
||||
{ cwd: root, encoding: "utf8", env: { ...process.env, NODE_OPTIONS: "", NODE_PATH: "" } },
|
||||
);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout.trim()).toBe('["ui/src/example.test.ts"]');
|
||||
expect(result.stdout.trim()).toBe(
|
||||
'["ui/src/example.test.ts","ui/src/example.browser-import.test.ts"]',
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -48,13 +56,33 @@ describe("native CLI selection", () => {
|
|||
|
||||
it.each([
|
||||
{ file: "ui/src/pages/devices/capability-chips.test.ts", selected: true },
|
||||
{ file: "ui/src/pages/devices/capability.browser-import.test.ts", selected: true },
|
||||
{ file: "ui/src/pages/devices.browser/capability.test.ts", selected: true },
|
||||
{ file: "ui/src/pages/devices/capability.browser-helper.browser.test.ts", selected: false },
|
||||
{ file: "ui/src/pages/devices/capability-chips.browser.test.ts", selected: false },
|
||||
])("preserves UI extglob ownership for $file", ({ file, selected }) => {
|
||||
const include = ["ui/src/**/!(*.browser).test.ts"];
|
||||
const include = ["ui/src/**/" + nonBrowserTestBasenamePattern];
|
||||
const expected = selected ? [file] : [];
|
||||
expect(narrowIncludePatternsForCli(include, ["node", "vitest", "run", file])).toEqual(expected);
|
||||
expect(intersectIncludePatterns(include, [file], matchesVitestGlob)).toEqual(expected);
|
||||
expect(matchesVitestCliSelection(file, include, ["run", file], "", {})).toBe(selected);
|
||||
expect(
|
||||
narrowIncludePatternsForCli(include, ["node", "vitest", "run", path.resolve(file)]),
|
||||
).toEqual(selected ? [path.resolve(file).replaceAll("\\", "/")] : []);
|
||||
});
|
||||
|
||||
it("keeps non-test paths outside UI discovery when CLI narrowing is unavailable", () => {
|
||||
const file = "ui/src/pages/devices/capability.ts";
|
||||
const absoluteFile = path.resolve(file);
|
||||
const include = ["ui/src/**/" + nonBrowserTestBasenamePattern];
|
||||
expect(narrowIncludePatternsForCli(include, ["node", "vitest", "run", file])).toEqual([]);
|
||||
expect(
|
||||
narrowIncludePatternsForCli(include, ["node", "vitest", "run", absoluteFile]),
|
||||
).toBeNull();
|
||||
expect(intersectIncludePatterns(include, [file], matchesVitestGlob)).toEqual([]);
|
||||
for (const target of [file, absoluteFile]) {
|
||||
expect(matchesVitestCliSelection(file, include, ["run", target], "", {})).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
const infraFile = "src/infra/sqlite-worker-operation-attachment.test.ts";
|
||||
|
|
@ -198,6 +226,52 @@ describe("batch file selection", () => {
|
|||
});
|
||||
|
||||
describe("intersectIncludePatterns", () => {
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
it("preserves native non-browser test discovery through directory selection", () => {
|
||||
const root = tempDirs.make("vitest-ui-selector-");
|
||||
const uiFiles = [
|
||||
"ui/src/ordinary.test.ts",
|
||||
"ui/src/shared.browser-import.test.ts",
|
||||
"ui/src/folder.browser/ordinary.test.ts",
|
||||
"ui/src/shared.browser.contract.test.ts",
|
||||
];
|
||||
const pluginFiles = ["extensions/example/browser/shared.browser-import.test.ts"];
|
||||
const files = [
|
||||
...uiFiles,
|
||||
...pluginFiles,
|
||||
"ui/src/only.browser.test.ts",
|
||||
"ui/src/helper.browser-helper.browser.test.ts",
|
||||
"ui/src/readme.ts",
|
||||
"extensions/example/browser/only.browser.test.ts",
|
||||
"other/example.test.ts",
|
||||
];
|
||||
for (const file of files) {
|
||||
const target = path.join(root, file);
|
||||
mkdirSync(path.dirname(target), { recursive: true });
|
||||
writeFileSync(target, "");
|
||||
}
|
||||
const owner = [
|
||||
"ui/src/**/" + nonBrowserTestBasenamePattern,
|
||||
"extensions/*/browser/**/" + nonBrowserTestBasenamePattern,
|
||||
];
|
||||
for (const { requested, expected } of [
|
||||
{ requested: ["ui/src/**/*.test.ts"], expected: uiFiles },
|
||||
{ requested: ["extensions/**/*.test.ts"], expected: pluginFiles },
|
||||
{ requested: ["**/*.test.ts"], expected: [...uiFiles, ...pluginFiles] },
|
||||
]) {
|
||||
const selected = intersectIncludePatterns(owner, requested, matchesVitestGlob);
|
||||
expect(filterFilesByPatterns(files, selected!, [], matchesVitestGlob).toSorted()).toEqual(
|
||||
expected.toSorted(),
|
||||
);
|
||||
expect(
|
||||
globSync(selected!, { cwd: root, dot: true, expandDirectories: false })
|
||||
.map((file) => file.replaceAll("\\", "/"))
|
||||
.toSorted(),
|
||||
).toEqual(expected.toSorted());
|
||||
}
|
||||
});
|
||||
|
||||
it("projects arbitrary candidate globs onto a finite literal owner", () => {
|
||||
const owner = [
|
||||
"ui/src/e2e/chat.e2e.test.ts",
|
||||
|
|
@ -214,13 +288,13 @@ describe("intersectIncludePatterns", () => {
|
|||
).toEqual(["ui/src/e2e/chat.e2e.test.ts", "ui/src/e2e/chat.capture.e2e.test.ts"]);
|
||||
});
|
||||
|
||||
it("retains the ambiguity guard for glob-owned inventories", () => {
|
||||
expect(() =>
|
||||
intersectIncludePatterns(
|
||||
["ui/src/**/*.e2e.test.ts"],
|
||||
["ui/src/e2e/*.e2e.test.ts"],
|
||||
matchesVitestGlob,
|
||||
),
|
||||
).toThrow("cannot safely intersect non-literal include path");
|
||||
it.each([
|
||||
["ui/src/**/*.e2e.test.ts", "ui/src/e2e/*.e2e.test.ts"],
|
||||
["ui/src/**/!(*.browser.test.ts)", "ui/src/**/*.test.ts"],
|
||||
["ui/src/**/prefix" + nonBrowserTestBasenamePattern, "ui/src/**/*.test.ts"],
|
||||
])("retains the ambiguity guard for glob-owned inventory %s", (owner, requested) => {
|
||||
expect(() => intersectIncludePatterns([owner], [requested], matchesVitestGlob)).toThrow(
|
||||
"cannot safely intersect non-literal include path",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { createRequire } from "node:module";
|
|||
import path from "node:path";
|
||||
import type { Minimatch } from "minimatch";
|
||||
import { collectVitestFileFilters } from "../../scripts/lib/vitest-cli-mode.mts";
|
||||
import { narrowIncludePatterns } from "./vitest.include-patterns.ts";
|
||||
import { narrowIncludePatterns, resolveNonBrowserTestPattern } from "./vitest.include-patterns.ts";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const require = createRequire(import.meta.url);
|
||||
|
|
@ -32,6 +32,11 @@ export function isSharedVitestExcludedPath(file: string, scopedDir = ""): boolea
|
|||
}
|
||||
|
||||
export function matchesVitestGlob(value: string, pattern: string): boolean {
|
||||
// Preserve the original Node/Bun spelling while native discovery uses the full basename.
|
||||
const nonBrowser = resolveNonBrowserTestPattern(pattern);
|
||||
if (nonBrowser) {
|
||||
return matchesVitestGlob(value, nonBrowser);
|
||||
}
|
||||
// CI plans tests before installing dependencies; keep Node's matcher dependency-free.
|
||||
if (!process.versions.bun) {
|
||||
return path.matchesGlob(value, pattern);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Vitest ui config wires the ui test shard.
|
||||
import type { ViteUserConfig } from "vitest/config";
|
||||
import { controlUiLocaleModulesPlugin } from "../../ui/config/control-ui-locales.ts";
|
||||
import { nonBrowserTestBasenamePattern } from "./vitest.include-patterns.ts";
|
||||
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
|
||||
import { jsdomOptimizedDeps } from "./vitest.shared.config.ts";
|
||||
import { uiIsolatedTestFiles } from "./vitest.ui-isolated-paths.mjs";
|
||||
|
|
@ -14,7 +15,9 @@ import {
|
|||
// Explicit nameable return type: inference reaches vite-internal names (TS4058/TS4082).
|
||||
export function createUiVitestConfig(env?: Record<string, string | undefined>): ViteUserConfig {
|
||||
const includePatterns = [
|
||||
...controlUiTestGlobs.map((pattern) => pattern.replace("*.test.ts", "!(*.browser).test.ts")),
|
||||
...controlUiTestGlobs.map((pattern) =>
|
||||
pattern.replace("*.test.ts", nonBrowserTestBasenamePattern),
|
||||
),
|
||||
...uiNodeDrivenBrowserTestFiles,
|
||||
];
|
||||
// Isolated files must never enter the shared module graph, including scoped runs.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue