cabloy 5.1.216 → 5.1.217

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/playwright-e2e.yml +163 -13
  3. package/.github/workflows/vona-test-sqlite3.yml +77 -7
  4. package/CHANGELOG.md +12 -0
  5. package/CONTRIBUTING.md +4 -2
  6. package/package.json +1 -1
  7. package/repo-docs/frontend/scripts.md +2 -0
  8. package/repo-docs/reference/repo-scripts.md +27 -1
  9. package/repo-e2e/scripts/e2e.test.ts +1 -1
  10. package/repo-e2e/scripts/managedTest.test.ts +115 -3
  11. package/repo-e2e/scripts/managedTest.ts +69 -20
  12. package/repo-e2e/scripts/managedTestImports.test.ts +55 -5
  13. package/repo-e2e/scripts/runE2e.test.ts +109 -0
  14. package/repo-e2e/scripts/runE2e.ts +58 -39
  15. package/repo-e2e/scripts/runE2eArgs.test.ts +16 -1
  16. package/repo-e2e/scripts/runE2eArgs.ts +5 -0
  17. package/repo-e2e/specs/a-commerce.spec.ts +10 -2
  18. package/repo-e2e/specs/home-user-account.spec.ts +4 -0
  19. package/repo-e2e/specs/markdown.spec.ts +14 -3
  20. package/scripts/generateOpenapiIsolated.test.ts +5 -1
  21. package/scripts/runIsolatedTest.test.ts +403 -36
  22. package/scripts/runIsolatedTest.ts +139 -12
  23. package/scripts/testBrowserGuardianWindows.test.ts +327 -0
  24. package/scripts/testClaimCleanup.test.ts +25 -9
  25. package/scripts/testClaimCleanup.ts +14 -45
  26. package/scripts/testClaimProcesses.test.ts +81 -2
  27. package/scripts/testClaimProcesses.ts +46 -10
  28. package/scripts/testProcessGuardianWindows.test.ts +602 -0
  29. package/scripts/testProcessGuardianWindows.ts +155 -0
  30. package/vona/packages-cli/cli/package.json +1 -1
  31. package/vona/packages-cli/cli-set-api/package.json +1 -1
  32. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +4 -1
  33. package/vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts +159 -0
  34. package/vona/packages-vona/vona/package.json +1 -1
  35. package/vona/packages-vona/vona-core/package.json +1 -1
  36. package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +298 -17
  37. package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +205 -97
  38. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +247 -0
  39. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts +224 -0
  40. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +229 -0
  41. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
  42. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +200 -0
  43. package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.cs +336 -0
  44. package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ps1 +20 -0
  45. package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts +531 -0
  46. package/vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts +61 -0
  47. package/vona/packages-vona/vona-core/src/lib/framework/index.ts +1 -0
  48. package/vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts +59 -0
  49. package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +8 -5
  50. package/vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts +84 -0
  51. package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +2 -2
  52. package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -1
  53. package/vona/packages-vona/vona-mock/package.json +1 -1
  54. package/vona/pnpm-lock.yaml +22 -22
  55. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  56. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  57. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  58. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  59. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +2 -1
  60. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  61. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +2 -2
  62. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +16 -0
  63. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +6 -3
  64. package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +80 -0
  65. package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
  66. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +73 -2
  67. package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +309 -0
  68. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionAsyncLocalStorage_.ts +3 -3
  70. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  71. package/vona/src/suite-vendor/a-vona/modules/a-static/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-static/src/bean/middlewareSystem.static.ts +2 -2
  73. package/vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts +101 -0
  74. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  75. package/zova/packages-cli/cli/package.json +2 -2
  76. package/zova/packages-cli/cli-set-front/package.json +2 -2
  77. package/zova/packages-utils/zova-vite/package.json +1 -1
  78. package/zova/packages-utils/zova-vite/src/configUtils.ts +1 -1
  79. package/zova/packages-utils/zova-vite/test/configUtils.test.ts +55 -0
  80. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +8 -4
  81. package/zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts +71 -0
@@ -24,6 +24,7 @@ import {
24
24
  } from '../../scripts/testClaimProcesses.ts';
25
25
  import {
26
26
  assertTestClaimSafePath,
27
+ assertTestClaimWindowsJob,
27
28
  getTestClaimPaths,
28
29
  hasTestClaimTransport,
29
30
  sendTestClaimEvent,
@@ -49,6 +50,11 @@ interface ManagedBrowserDependencies {
49
50
  sendEvent: (env: NodeJS.ProcessEnv, event: TestClaimEvent) => Promise<void>;
50
51
  inspectProcess: (pid: number) => Promise<TestProcessSnapshot>;
51
52
  waitForGroupExit: (pgid: number) => Promise<void>;
53
+ platform?: NodeJS.Platform;
54
+ assertWindowsJob?: (
55
+ env: NodeJS.ProcessEnv,
56
+ identity: { pid: number; startTime: string; host: string },
57
+ ) => Promise<void>;
52
58
  }
53
59
 
54
60
  function assertWithin(root: string, target: string) {
@@ -94,6 +100,8 @@ export async function useManagedBrowser(
94
100
  },
95
101
  ) {
96
102
  const { env } = dependencies;
103
+ const windows = (dependencies.platform ?? process.platform) === 'win32';
104
+ const assertWindowsJob = dependencies.assertWindowsJob ?? assertTestClaimWindowsJob;
97
105
  const runId = env.VONA_TEST_RUN_ID;
98
106
  if (!runId || !hasTestClaimTransport(env)) {
99
107
  throw new Error('Managed browser requires an isolated test claim transport');
@@ -103,8 +111,16 @@ export async function useManagedBrowser(
103
111
  const { dataRoot } = getTestClaimPaths(dependencies.projectPath, runId);
104
112
  const temporaryRoot = resolve(dataRoot, 'tmp');
105
113
  assertTestRunOwner(dataRoot, runId);
106
- if (env.TMPDIR !== temporaryRoot || !lstatSync(temporaryRoot).isDirectory()) {
107
- throw new Error('Managed browser requires the inherited run-owned TMPDIR');
114
+ const tempKeys = windows
115
+ ? Object.keys(env).filter(key => ['TEMP', 'TMP', 'TMPDIR'].includes(key.toUpperCase()))
116
+ : ['TMPDIR'];
117
+ if (
118
+ (windows &&
119
+ !['TEMP', 'TMP', 'TMPDIR'].every(name => tempKeys.some(key => key.toUpperCase() === name))) ||
120
+ tempKeys.some(key => env[key] !== temporaryRoot) ||
121
+ !lstatSync(temporaryRoot).isDirectory()
122
+ ) {
123
+ throw new Error('Managed browser requires inherited run-owned temporary paths');
108
124
  }
109
125
  assertWithin(dataRoot, temporaryRoot);
110
126
 
@@ -120,16 +136,37 @@ export async function useManagedBrowser(
120
136
  }
121
137
  // Browser profile creation happens in the Playwright worker's inherited
122
138
  // TMPDIR. A custom browser environment must not redirect child temp writes.
123
- if (launchOptions.env) launchOptions.env = { ...launchOptions.env, TMPDIR: temporaryRoot };
139
+ if (launchOptions.env) {
140
+ const browserEnv = { ...launchOptions.env };
141
+ if (windows) {
142
+ for (const key of Object.keys(browserEnv)) {
143
+ if (['TEMP', 'TMP', 'TMPDIR'].includes(key.toUpperCase())) delete browserEnv[key];
144
+ }
145
+ browserEnv.TEMP = temporaryRoot;
146
+ browserEnv.TMP = temporaryRoot;
147
+ }
148
+ launchOptions.env = { ...browserEnv, TMPDIR: temporaryRoot };
149
+ }
150
+ if (windows) {
151
+ const identity = await dependencies.inspectProcess(process.pid);
152
+ if (identity.pid !== process.pid || !identity.startTime || identity.state.startsWith('Z')) {
153
+ throw new Error('Managed browser worker has no live process identity');
154
+ }
155
+ await assertWindowsJob(env, {
156
+ pid: identity.pid,
157
+ startTime: identity.startTime,
158
+ host: identity.host,
159
+ });
160
+ }
124
161
  const processRecord: TestClaimProcess = {
125
162
  id: randomUUID(),
126
163
  role: 'browser',
127
164
  stage: 'pending',
128
165
  profilePath: temporaryRoot,
129
166
  };
130
- // Acknowledgement precedes spawning. A rejected launch leaves this intent
131
- // pending: launchServer may have spawned before it rejects or the worker dies.
132
- await dependencies.sendEvent(env, { type: 'process', process: processRecord });
167
+ // POSIX browsers own independent groups; Windows browsers inherit the verified
168
+ // guardian Job. Only that guardian can acknowledge whole-Job drainage.
169
+ if (!windows) await dependencies.sendEvent(env, { type: 'process', process: processRecord });
133
170
  const server = await options.browserType.launchServer({
134
171
  handleSIGINT: false,
135
172
  ...launchOptions,
@@ -147,25 +184,37 @@ export async function useManagedBrowser(
147
184
  child.exitCode !== null ||
148
185
  child.signalCode !== null ||
149
186
  identity.pid !== child.pid ||
150
- identity.pgid !== child.pid ||
151
187
  !identity.startTime ||
152
- identity.state.startsWith('Z')
188
+ identity.state.startsWith('Z') ||
189
+ (!windows && identity.pgid !== child.pid)
153
190
  ) {
154
- throw new Error('Managed browser has no independent live process group');
191
+ throw new Error(
192
+ windows
193
+ ? 'Managed browser has no live process identity'
194
+ : 'Managed browser has no independent live process group',
195
+ );
155
196
  }
156
197
  const profilePath = discoverBrowserProfile(child.spawnargs) ?? temporaryRoot;
157
198
  assertWithin(temporaryRoot, profilePath);
158
- const record: TestClaimProcess = {
159
- ...processRecord,
160
- stage: 'registered',
161
- pid: identity.pid,
162
- pgid: identity.pgid,
163
- startTime: identity.startTime,
164
- host: identity.host,
165
- profilePath,
166
- };
167
- await dependencies.sendEvent(env, { type: 'process', process: record });
168
- registration = record;
199
+ if (windows) {
200
+ await assertWindowsJob(env, {
201
+ pid: identity.pid,
202
+ startTime: identity.startTime,
203
+ host: identity.host,
204
+ });
205
+ } else {
206
+ const record: TestClaimProcess = {
207
+ ...processRecord,
208
+ stage: 'registered',
209
+ pid: identity.pid,
210
+ pgid: identity.pgid,
211
+ startTime: identity.startTime,
212
+ host: identity.host,
213
+ profilePath,
214
+ };
215
+ await dependencies.sendEvent(env, { type: 'process', process: record });
216
+ registration = record;
217
+ }
169
218
  const browser = await options.browserType.connect(server.wsEndpoint(), { slowMo });
170
219
  await use(browser);
171
220
  } finally {
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
- import { relative, resolve } from 'node:path';
4
+ import { dirname, relative, resolve } from 'node:path';
5
5
  import test from 'node:test';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
@@ -51,8 +51,60 @@ test('fixture import gate allows type-only imports and catches runtime bypass fo
51
51
  );
52
52
  });
53
53
 
54
- test('production gate rejects empty trees, bypasses in nested specs, and symlinks', async () => {
55
- const root = await mkdtemp(resolve(tmpdir(), 'cabloy-managed-imports-'));
54
+ test('fixture import gate rejects absolute imports even when they point to managedTest', () => {
55
+ const spec = resolve(specRoot, 'example.spec.ts');
56
+ assert.deepEqual(
57
+ violations(`import { test } from ${JSON.stringify(fixturePath.replaceAll('\\', '/'))};`, spec),
58
+ ['application spec must import test from managedTest'],
59
+ );
60
+ });
61
+
62
+ test('production gate rejects directory links, including Windows junctions', async () => {
63
+ const root = await mkdtemp(resolve(tmpdir(), 'cabloy-managed-directory-link-'));
64
+ try {
65
+ const nested = resolve(root, 'nested');
66
+ await mkdir(nested);
67
+ await symlink(
68
+ nested,
69
+ resolve(root, 'linked'),
70
+ process.platform === 'win32' ? 'junction' : 'dir',
71
+ );
72
+ await assert.rejects(assertManagedTestImports(root), /cannot contain symlinks/);
73
+ } finally {
74
+ await rm(root, { recursive: true, force: true });
75
+ }
76
+ });
77
+
78
+ test('production gate rejects file symlinks when supported', async t => {
79
+ // Windows OS temp and the checkout may be on different drives, preventing relative imports.
80
+ const root = await mkdtemp(resolve(dirname(fixturePath), '.cabloy-managed-file-link-'));
81
+ try {
82
+ const spec = resolve(root, 'application.spec.ts');
83
+ const managedPath = relative(root, fixturePath).replaceAll('\\', '/');
84
+ await writeFile(spec, `import { test } from ${JSON.stringify(managedPath)};`);
85
+ await assertManagedTestImports(root);
86
+ try {
87
+ await symlink(spec, resolve(root, 'linked.spec.ts'), 'file');
88
+ } catch (error) {
89
+ if (
90
+ process.platform === 'win32' &&
91
+ ['EPERM', 'EACCES', 'ENOTSUP'].includes((error as NodeJS.ErrnoException).code ?? '')
92
+ ) {
93
+ t.skip(
94
+ `Windows file symlink creation unavailable (${(error as NodeJS.ErrnoException).code})`,
95
+ );
96
+ return;
97
+ }
98
+ throw error;
99
+ }
100
+ await assert.rejects(assertManagedTestImports(root), /cannot contain symlinks/);
101
+ } finally {
102
+ await rm(root, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test('production gate rejects empty trees and bypasses in nested specs', async () => {
107
+ const root = await mkdtemp(resolve(dirname(fixturePath), '.cabloy-managed-imports-'));
56
108
  try {
57
109
  await assert.rejects(assertManagedTestImports(root), /no specs/);
58
110
  const nested = resolve(root, 'nested');
@@ -66,8 +118,6 @@ test('production gate rejects empty trees, bypasses in nested specs, and symlink
66
118
  const managedPath = relative(nested, fixturePath).replaceAll('\\', '/');
67
119
  await writeFile(spec, `import { test } from ${JSON.stringify(managedPath)};`);
68
120
  await assertManagedTestImports(root);
69
- await symlink(spec, resolve(root, 'linked.spec.ts'));
70
- await assert.rejects(assertManagedTestImports(root), /cannot contain symlinks/);
71
121
  } finally {
72
122
  await rm(root, { recursive: true, force: true });
73
123
  }
@@ -0,0 +1,109 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { resolve } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { runPlaywright } from './runE2e.ts';
8
+ import { buildSpecFilter, parseE2eArgs } from './runE2eArgs.ts';
9
+
10
+ function withCli(
11
+ callback: (fixture: {
12
+ rootDir: string;
13
+ configFile: string;
14
+ specsDir: string;
15
+ outputFile: string;
16
+ }) => void,
17
+ exitCode = 0,
18
+ ): void {
19
+ const rootDir = mkdtempSync(resolve(tmpdir(), 'cabloy e2e café [x]+(y).$-'));
20
+ try {
21
+ const packageDir = resolve(rootDir, 'node_modules', '@playwright', 'test');
22
+ const specsDir = resolve(rootDir, 'specs 中文 [selected]');
23
+ const configFile = resolve(rootDir, 'config café [ui].ts');
24
+ const outputFile = resolve(rootDir, 'observed argv.json');
25
+ mkdirSync(packageDir, { recursive: true });
26
+ mkdirSync(specsDir);
27
+ writeFileSync(resolve(rootDir, 'package.json'), JSON.stringify({ type: 'module' }));
28
+ writeFileSync(
29
+ resolve(packageDir, 'package.json'),
30
+ JSON.stringify({ name: '@playwright/test', exports: { './cli': './cli.cjs' } }),
31
+ );
32
+ writeFileSync(
33
+ resolve(packageDir, 'cli.cjs'),
34
+ `require('node:fs').writeFileSync(${JSON.stringify(outputFile)}, JSON.stringify({
35
+ args: process.argv.slice(2), cwd: process.cwd(), execPath: process.execPath,
36
+ })); process.exit(${exitCode});`,
37
+ );
38
+ for (const name of ['cabloy-basic', 'home-user-account']) {
39
+ writeFileSync(resolve(specsDir, `${name}.spec.ts`), '');
40
+ }
41
+ callback({ rootDir, configFile, specsDir, outputFile });
42
+ } finally {
43
+ rmSync(rootDir, { recursive: true, force: true });
44
+ }
45
+ }
46
+
47
+ test('launches the resolved JavaScript CLI through Node without a shell in Unicode/space paths', () => {
48
+ withCli(fixture => {
49
+ const playwrightArgs = [
50
+ '--grep',
51
+ 'café 中文 "quoted" & $HOME; $(not-a-command) | > ignored',
52
+ '--trace=retain-on-failure',
53
+ '--output',
54
+ 'artifacts with spaces 中文',
55
+ ];
56
+ runPlaywright({
57
+ ...fixture,
58
+ parsed: parseE2eArgs(
59
+ ['cabloy-basic', 'home-user-account', ...playwrightArgs],
60
+ fixture.specsDir,
61
+ ),
62
+ });
63
+ const observed = JSON.parse(readFileSync(fixture.outputFile, 'utf8'));
64
+ assert.equal(observed.cwd, realpathSync(fixture.rootDir));
65
+ assert.equal(observed.execPath, process.execPath);
66
+ assert.deepEqual(observed.args, [
67
+ 'test',
68
+ '--config',
69
+ fixture.configFile,
70
+ buildSpecFilter(resolve(fixture.specsDir, 'cabloy-basic.spec.ts')),
71
+ buildSpecFilter(resolve(fixture.specsDir, 'home-user-account.spec.ts')),
72
+ ...playwrightArgs,
73
+ ]);
74
+ for (let index = 0; index < 2; index++) {
75
+ const name = ['cabloy-basic', 'home-user-account'][index];
76
+ const selectedPath = resolve(fixture.specsDir, `${name}.spec.ts`).replaceAll('\\', '/');
77
+ const filter = new RegExp(observed.args[3 + index]);
78
+ assert.match(selectedPath, filter);
79
+ assert.doesNotMatch(`${selectedPath}.unselected`, filter);
80
+ assert.doesNotMatch(selectedPath.replace('.spec.ts', 'XspecXts'), filter);
81
+ }
82
+ });
83
+ });
84
+
85
+ test('keeps all-spec discovery and combines tags without changing additional arguments', () => {
86
+ withCli(fixture => {
87
+ runPlaywright({
88
+ ...fixture,
89
+ parsed: parseE2eArgs(['--tag', '@web', '--grep=@flow', '--list'], fixture.specsDir),
90
+ });
91
+ const observed = JSON.parse(readFileSync(fixture.outputFile, 'utf8'));
92
+ assert.deepEqual(observed.args, [
93
+ 'test',
94
+ '--config',
95
+ fixture.configFile,
96
+ '--grep=(?=.*(?:@flow))(?=.*@web)',
97
+ '--list',
98
+ ]);
99
+ });
100
+ });
101
+
102
+ test('propagates the Playwright CLI failure rather than reporting a successful launch', () => {
103
+ withCli(fixture => {
104
+ assert.throws(
105
+ () => runPlaywright({ ...fixture, parsed: parseE2eArgs([], fixture.specsDir) }),
106
+ (error: Error & { status?: number }) => error.status === 23,
107
+ );
108
+ }, 23);
109
+ });
@@ -1,31 +1,46 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
2
3
  import { createServer } from 'node:net';
3
4
  import { resolve } from 'node:path';
4
5
 
5
- import {
6
- E2E_CONFIG_FILE,
7
- E2E_LOCAL_BASE_URL,
8
- E2E_PORT,
9
- E2E_ROOT_DIR,
10
- E2E_SPECS_DIR,
11
- } from './e2e.ts';
12
- import { assertManagedTestImports } from './managedTestImports.ts';
13
- import { combineGreps, parseE2eArgs } from './runE2eArgs.ts';
14
-
15
- function run(command: string, args: string[]): void {
6
+ import type { ParsedE2eArgs } from './runE2eArgs.ts';
7
+
8
+ import { buildSpecFilter, combineGreps, parseE2eArgs } from './runE2eArgs.ts';
9
+
10
+ export function runPlaywright(options: {
11
+ rootDir: string;
12
+ configFile: string;
13
+ specsDir: string;
14
+ parsed: ParsedE2eArgs;
15
+ }): void {
16
+ const { rootDir, configFile, specsDir, parsed } = options;
17
+ const require = createRequire(resolve(rootDir, 'package.json'));
18
+ const cli = require.resolve('@playwright/test/cli');
19
+ const specFilters = parsed.specNames.map(name =>
20
+ buildSpecFilter(resolve(specsDir, `${name}.spec.ts`)),
21
+ );
22
+ const args = [
23
+ cli,
24
+ 'test',
25
+ '--config',
26
+ configFile,
27
+ ...specFilters,
28
+ ...combineGreps(parsed.playwrightArgs, parsed.tags),
29
+ ];
16
30
  // eslint-disable-next-line no-console
17
- console.log(`\n===> ${[command, ...args].join(' ')}`);
18
- execFileSync(command, args, {
19
- cwd: E2E_ROOT_DIR,
31
+ console.log(`\n===> ${[process.execPath, ...args].join(' ')}`);
32
+ execFileSync(process.execPath, args, {
33
+ cwd: rootDir,
20
34
  stdio: 'inherit',
35
+ shell: false,
21
36
  });
22
37
  }
23
38
 
24
- async function assertPortAvailable(): Promise<void> {
39
+ async function assertPortAvailable(port: number): Promise<void> {
25
40
  const server = createServer();
26
41
  await new Promise<void>((resolvePromise, reject) => {
27
42
  server.once('error', reject);
28
- server.listen(E2E_PORT, '127.0.0.1', () => {
43
+ server.listen(port, '127.0.0.1', () => {
29
44
  server.close(error => {
30
45
  if (error) reject(error);
31
46
  else resolvePromise();
@@ -34,28 +49,32 @@ async function assertPortAvailable(): Promise<void> {
34
49
  });
35
50
  }
36
51
 
37
- const parsed = parseE2eArgs(process.argv.slice(2), E2E_SPECS_DIR);
38
- if (
39
- process.env.VONA_TEST_RUN_ID &&
40
- process.env.APP_NAME !== `cabtest${process.env.VONA_TEST_RUN_ID}`
41
- ) {
42
- throw new Error('Managed E2E test identity does not match its run ID');
43
- }
44
- if (process.env.VONA_TEST_RUN_ID) {
45
- await assertManagedTestImports();
46
- } else {
47
- await assertPortAvailable();
52
+ async function main(): Promise<void> {
53
+ const { E2E_CONFIG_FILE, E2E_LOCAL_BASE_URL, E2E_PORT, E2E_ROOT_DIR, E2E_SPECS_DIR } =
54
+ await import('./e2e.ts');
55
+ const parsed = parseE2eArgs(process.argv.slice(2), E2E_SPECS_DIR);
56
+ if (
57
+ process.env.VONA_TEST_RUN_ID &&
58
+ process.env.APP_NAME !== `cabtest${process.env.VONA_TEST_RUN_ID}`
59
+ ) {
60
+ throw new Error('Managed E2E test identity does not match its run ID');
61
+ }
62
+ if (process.env.VONA_TEST_RUN_ID) {
63
+ const { assertManagedTestImports } = await import('./managedTestImports.ts');
64
+ await assertManagedTestImports();
65
+ } else {
66
+ await assertPortAvailable(E2E_PORT);
67
+ }
68
+
69
+ runPlaywright({
70
+ rootDir: E2E_ROOT_DIR,
71
+ configFile: E2E_CONFIG_FILE,
72
+ specsDir: E2E_SPECS_DIR,
73
+ parsed,
74
+ });
75
+
76
+ // eslint-disable-next-line no-console
77
+ console.log(`E2E target: ${E2E_LOCAL_BASE_URL}`);
48
78
  }
49
79
 
50
- const playwrightCommand = resolve(
51
- E2E_ROOT_DIR,
52
- 'node_modules',
53
- '.bin',
54
- process.platform === 'win32' ? 'playwright.cmd' : 'playwright',
55
- );
56
- const specPaths = parsed.specNames.map(name => resolve(E2E_SPECS_DIR, `${name}.spec.ts`));
57
- const playwrightArgs = combineGreps(parsed.playwrightArgs, parsed.tags);
58
- run(playwrightCommand, ['test', '--config', E2E_CONFIG_FILE, ...specPaths, ...playwrightArgs]);
59
-
60
- // eslint-disable-next-line no-console
61
- console.log(`E2E target: ${E2E_LOCAL_BASE_URL}`);
80
+ if (import.meta.main) await main();
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import { resolve } from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
- import { buildTagGrep, combineGreps, parseE2eArgs } from './runE2eArgs.ts';
7
+ import { buildSpecFilter, buildTagGrep, combineGreps, parseE2eArgs } from './runE2eArgs.ts';
8
8
 
9
9
  function withSpecs(callback: (specsDir: string) => void): void {
10
10
  const tempDir = mkdtempSync(resolve(tmpdir(), 'cabloy-e2e-args-'));
@@ -70,6 +70,21 @@ test('rejects removed clean and fast options', () => {
70
70
  });
71
71
  });
72
72
 
73
+ test('spec filters normalize Windows paths and escape regex syntax literally', () => {
74
+ for (const specPath of [
75
+ '/synthetic/café 中文 [work]+(tree).$/specs/cabloy-basic.spec.ts',
76
+ 'C:\\synthetic\\café 中文 [work]+(tree).$\\specs\\cabloy-basic.spec.ts',
77
+ '\\\\server\\share with spaces\\café [work]\\specs\\a-commerce.spec.ts',
78
+ ]) {
79
+ const normalized = specPath.replaceAll('\\', '/');
80
+ const filter = new RegExp(buildSpecFilter(specPath));
81
+ assert.match(normalized, filter);
82
+ assert.doesNotMatch(`${normalized}.unselected`, filter);
83
+ assert.doesNotMatch(`prefix${normalized}`, filter);
84
+ assert.doesNotMatch(normalized.replace('.spec.ts', 'XspecXts'), filter);
85
+ }
86
+ });
87
+
73
88
  test('combines tags with an existing grep expression', () => {
74
89
  assert.equal(buildTagGrep(['@web', '@smoke']), '(?=.*@web)(?=.*@smoke)');
75
90
  assert.deepEqual(combineGreps(['--grep', '@flow'], ['@web', '@smoke']), [
@@ -36,6 +36,11 @@ function escapeRegExp(value: string): string {
36
36
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
37
  }
38
38
 
39
+ /** Playwright interprets positional file filters as regexes, including on Windows. */
40
+ export function buildSpecFilter(specPath: string): string {
41
+ return `^${escapeRegExp(specPath.replaceAll('\\', '/'))}$`;
42
+ }
43
+
39
44
  export function buildTagGrep(tags: string[]): string | undefined {
40
45
  if (tags.length === 0) return undefined;
41
46
  return tags.map(tag => `(?=.*${escapeRegExp(tag)})`).join('');
@@ -801,7 +801,7 @@ test(
801
801
 
802
802
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
803
803
  await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
804
- await addResponse;
804
+ expect((await addResponse).ok()).toBeTruthy();
805
805
 
806
806
  await page.getByRole('link', { name: /^Cart/ }).click();
807
807
  await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
@@ -1343,7 +1343,7 @@ test(
1343
1343
  await expect(page.getByText('$45.99')).toBeVisible();
1344
1344
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
1345
1345
  await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
1346
- await addResponse;
1346
+ expect((await addResponse).ok()).toBeTruthy();
1347
1347
  await page.getByRole('link', { name: /^Cart/ }).click();
1348
1348
  await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
1349
1349
  await page.getByRole('link', { name: 'Checkout', exact: true }).click();
@@ -1662,6 +1662,10 @@ test(
1662
1662
  /\/commerce-admin\/rest\/resource\/commerce-promotion(?:%3A|:|%253A)couponTemplate\/create(?:[/?#]|$)/,
1663
1663
  );
1664
1664
 
1665
+ await expect(adminPage.locator('html')).toHaveAttribute(
1666
+ 'data-zova-hydrated',
1667
+ 'commerceAdmin',
1668
+ );
1665
1669
  for (const groupName of [
1666
1670
  'Basic Information',
1667
1671
  'Discount Policy',
@@ -1765,6 +1769,10 @@ test(
1765
1769
  expectTableIdentity(parentId);
1766
1770
 
1767
1771
  await adminPage.goto(categoryCreatePath, { waitUntil: 'load' });
1772
+ await expect(adminPage.locator('html')).toHaveAttribute(
1773
+ 'data-zova-hydrated',
1774
+ 'commerceAdmin',
1775
+ );
1768
1776
  const parentPublication = adminPage
1769
1777
  .getByRole('group', { name: 'Published' })
1770
1778
  .getByRole('combobox');
@@ -536,6 +536,10 @@ test(
536
536
  expect(profile.ok()).toBeTruthy();
537
537
  const profileBody = (await profile.json()).data;
538
538
  expect(profileBody.avatar).toEqual(expect.any(String));
539
+ const avatarResponse = await request.get(profileBody.avatar);
540
+ expect(avatarResponse.ok()).toBeTruthy();
541
+ expect(avatarResponse.headers()['content-type']).toMatch(/^image\//);
542
+ expect((await avatarResponse.body()).length).toBeGreaterThan(0);
539
543
  expect(pageErrors).toEqual([]);
540
544
  expect(consoleErrors).toEqual([]);
541
545
  } finally {
@@ -590,15 +590,26 @@ test(
590
590
  await linkInput.fill('');
591
591
  await linkInput.press('Enter');
592
592
  await expect(editor.locator('h1 a')).toHaveCount(0);
593
+ const persistedText = `Link removal saved ${suffix}`;
594
+ await editor.press('ControlOrMeta+End');
595
+ await editor.press('Enter');
596
+ await editor.pressSequentially(persistedText);
597
+ const productUpdate = waitForApiResponse(
598
+ adminPage,
599
+ 'PATCH',
600
+ `${productActionPath}/${productId}`,
601
+ );
593
602
  await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
603
+ expect((await productUpdate).ok()).toBeTruthy();
594
604
  const productResponseAfterLinkRemoval = await adminPage.request.get(
595
605
  `${productActionPath}/${productId}`,
596
606
  { headers },
597
607
  );
598
608
  expect(productResponseAfterLinkRemoval.ok()).toBeTruthy();
599
- expect(
600
- (await productResponseAfterLinkRemoval.json()).productContentForm.descriptionMarkdown,
601
- ).not.toContain('https://example.com/updated');
609
+ const savedMarkdown = (await productResponseAfterLinkRemoval.json()).productContentForm
610
+ .descriptionMarkdown;
611
+ expect(savedMarkdown).toContain(persistedText);
612
+ expect(savedMarkdown).not.toContain('https://example.com/updated');
602
613
  expect(adminPageErrors).toEqual([]);
603
614
  expect(adminConsoleErrors).toEqual([]);
604
615
  } finally {
@@ -1,6 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { once } from 'node:events';
3
3
  import { createServer } from 'node:http';
4
+ import { resolve } from 'node:path';
4
5
  import { test } from 'node:test';
5
6
 
6
7
  import type { GuardianResult, OwnedProcess } from './runIsolatedTest.ts';
@@ -46,7 +47,7 @@ test('launches only a loopback single-worker test-mode Vona service', () => {
46
47
  '--flavor=normal',
47
48
  '--mode=test',
48
49
  ]);
49
- assert.equal(command.cwd, `${ROOT_DIR}/vona`);
50
+ assert.equal(command.cwd, resolve(ROOT_DIR, 'vona'));
50
51
  assert.deepEqual(command.env, {
51
52
  CUSTOM: 'keep',
52
53
  VONA_TEST_READY_KEY: 'test-key',
@@ -81,6 +82,9 @@ function lifecycleFixture(options: { readyFailure?: boolean; stopFailure?: boole
81
82
  finish,
82
83
  result,
83
84
  dependencies: {
85
+ // These legacy fixtures assert POSIX signal/stop semantics; Windows cooperative
86
+ // shutdown is exercised separately with explicit platform injection.
87
+ platform: 'darwin' as const,
84
88
  start: async () => {
85
89
  events.push('start');
86
90
  return server;