cabloy 5.1.215 → 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.
- package/.cabloy-version +1 -1
- package/.github/workflows/playwright-e2e.yml +163 -13
- package/.github/workflows/vona-test-sqlite3.yml +77 -7
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +4 -2
- package/package.json +1 -1
- package/repo-docs/frontend/scripts.md +2 -0
- package/repo-docs/reference/repo-scripts.md +27 -1
- package/repo-e2e/scripts/e2e.test.ts +1 -1
- package/repo-e2e/scripts/managedTest.test.ts +115 -3
- package/repo-e2e/scripts/managedTest.ts +69 -20
- package/repo-e2e/scripts/managedTestImports.test.ts +55 -5
- package/repo-e2e/scripts/runE2e.test.ts +109 -0
- package/repo-e2e/scripts/runE2e.ts +58 -39
- package/repo-e2e/scripts/runE2eArgs.test.ts +16 -1
- package/repo-e2e/scripts/runE2eArgs.ts +5 -0
- package/repo-e2e/specs/a-commerce.spec.ts +10 -2
- package/repo-e2e/specs/home-user-account.spec.ts +4 -0
- package/repo-e2e/specs/markdown.spec.ts +14 -3
- package/scripts/generateOpenapiIsolated.test.ts +5 -1
- package/scripts/runIsolatedTest.test.ts +403 -36
- package/scripts/runIsolatedTest.ts +139 -12
- package/scripts/testBrowserGuardianWindows.test.ts +327 -0
- package/scripts/testClaimCleanup.test.ts +25 -9
- package/scripts/testClaimCleanup.ts +14 -45
- package/scripts/testClaimProcesses.test.ts +81 -2
- package/scripts/testClaimProcesses.ts +46 -10
- package/scripts/testProcessGuardianWindows.test.ts +602 -0
- package/scripts/testProcessGuardianWindows.ts +155 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +4 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +4 -4
- package/vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts +159 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +298 -17
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +205 -97
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +247 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts +224 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +229 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +200 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.cs +336 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts +531 -0
- package/vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts +61 -0
- package/vona/packages-vona/vona-core/src/lib/framework/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts +59 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +8 -5
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts +84 -0
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +2 -2
- package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +22 -22
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +16 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +6 -3
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +80 -0
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +73 -2
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +309 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionAsyncLocalStorage_.ts +3 -3
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/src/bean/middlewareSystem.static.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts +101 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +1 -1
- package/zova/packages-utils/zova-vite/src/configUtils.ts +1 -1
- package/zova/packages-utils/zova-vite/test/configUtils.test.ts +55 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +8 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts +71 -0
- /package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/{<%=argv.detailDtoBaseName%>.tsx_ → {{detailDtoBaseName}}.tsx_} +0 -0
- /package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/{<%=argv.detailDtoMutateName%>.tsx_ → {{detailDtoMutateName}}.tsx_} +0 -0
- /package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/{<%=argv.detailDtoResItemName%>.tsx_ → {{detailDtoResItemName}}.tsx_} +0 -0
- /package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/{<%=argv.detailDtoViewName%>.tsx_ → {{detailDtoViewName}}.tsx_} +0 -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
|
-
|
|
107
|
-
|
|
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)
|
|
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
|
-
//
|
|
131
|
-
//
|
|
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(
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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('
|
|
55
|
-
const
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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===> ${[
|
|
18
|
-
execFileSync(
|
|
19
|
-
cwd:
|
|
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(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
).
|
|
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,
|
|
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;
|