@gate-forge/pack-playwright 0.7.0 → 0.8.0
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/README.md +31 -0
- package/dist/constants.d.ts +13 -140
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +13 -148
- package/dist/constants.js.map +1 -1
- package/dist/cypress/plugin.d.ts +53 -0
- package/dist/cypress/plugin.d.ts.map +1 -0
- package/dist/cypress/plugin.js +369 -0
- package/dist/cypress/plugin.js.map +1 -0
- package/dist/cypress/spec-scan.d.ts +39 -0
- package/dist/cypress/spec-scan.d.ts.map +1 -0
- package/dist/cypress/spec-scan.js +301 -0
- package/dist/cypress/spec-scan.js.map +1 -0
- package/dist/diagnosis.d.ts +39 -0
- package/dist/diagnosis.d.ts.map +1 -0
- package/dist/diagnosis.js +63 -0
- package/dist/diagnosis.js.map +1 -0
- package/dist/discovery/adapter-projection.d.ts +13 -0
- package/dist/discovery/adapter-projection.d.ts.map +1 -0
- package/dist/discovery/adapter-projection.js +84 -0
- package/dist/discovery/adapter-projection.js.map +1 -0
- package/dist/discovery/config-locations.d.ts +5 -0
- package/dist/discovery/config-locations.d.ts.map +1 -0
- package/dist/discovery/config-locations.js +19 -0
- package/dist/discovery/config-locations.js.map +1 -0
- package/dist/discovery/cypress-runner-adapter.d.ts +112 -0
- package/dist/discovery/cypress-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/cypress-runner-adapter.js +501 -0
- package/dist/discovery/cypress-runner-adapter.js.map +1 -0
- package/dist/discovery/discover.d.ts +53 -1
- package/dist/discovery/discover.d.ts.map +1 -1
- package/dist/discovery/discover.js +100 -3
- package/dist/discovery/discover.js.map +1 -1
- package/dist/discovery/git-ignore.d.ts +13 -0
- package/dist/discovery/git-ignore.d.ts.map +1 -0
- package/dist/discovery/git-ignore.js +62 -0
- package/dist/discovery/git-ignore.js.map +1 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/playwright-runner-adapter.d.ts +108 -0
- package/dist/discovery/playwright-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/playwright-runner-adapter.js +185 -0
- package/dist/discovery/playwright-runner-adapter.js.map +1 -0
- package/dist/discovery/prepare-barrier.d.ts +522 -0
- package/dist/discovery/prepare-barrier.d.ts.map +1 -0
- package/dist/discovery/prepare-barrier.js +739 -0
- package/dist/discovery/prepare-barrier.js.map +1 -0
- package/dist/discovery/pytest-adapter.d.ts.map +1 -1
- package/dist/discovery/pytest-adapter.js +6 -2
- package/dist/discovery/pytest-adapter.js.map +1 -1
- package/dist/discovery/pytest-runner-adapter.d.ts +124 -0
- package/dist/discovery/pytest-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/pytest-runner-adapter.js +454 -0
- package/dist/discovery/pytest-runner-adapter.js.map +1 -0
- package/dist/discovery/reconcile.d.ts +80 -11
- package/dist/discovery/reconcile.d.ts.map +1 -1
- package/dist/discovery/reconcile.js +395 -78
- package/dist/discovery/reconcile.js.map +1 -1
- package/dist/discovery/runner-env.d.ts +88 -2
- package/dist/discovery/runner-env.d.ts.map +1 -1
- package/dist/discovery/runner-env.js +142 -4
- package/dist/discovery/runner-env.js.map +1 -1
- package/dist/discovery/static-discovery.d.ts +38 -3
- package/dist/discovery/static-discovery.d.ts.map +1 -1
- package/dist/discovery/static-discovery.js +454 -15
- package/dist/discovery/static-discovery.js.map +1 -1
- package/dist/discovery/supervised-run.d.ts +199 -10
- package/dist/discovery/supervised-run.d.ts.map +1 -1
- package/dist/discovery/supervised-run.js +350 -39
- package/dist/discovery/supervised-run.js.map +1 -1
- package/dist/discovery/trusted-config.d.ts +180 -6
- package/dist/discovery/trusted-config.d.ts.map +1 -1
- package/dist/discovery/trusted-config.js +279 -10
- package/dist/discovery/trusted-config.js.map +1 -1
- package/dist/discovery/vitest-runner-adapter.d.ts +172 -0
- package/dist/discovery/vitest-runner-adapter.d.ts.map +1 -0
- package/dist/discovery/vitest-runner-adapter.js +607 -0
- package/dist/discovery/vitest-runner-adapter.js.map +1 -0
- package/dist/fixture/consumer-runner.d.ts +4 -0
- package/dist/fixture/consumer-runner.d.ts.map +1 -0
- package/dist/fixture/consumer-runner.js +38 -0
- package/dist/fixture/consumer-runner.js.map +1 -0
- package/dist/fixture/evidence.d.ts +27 -0
- package/dist/fixture/evidence.d.ts.map +1 -1
- package/dist/fixture/evidence.js +12 -0
- package/dist/fixture/evidence.js.map +1 -1
- package/dist/fixture/fixture.d.ts +2 -1
- package/dist/fixture/fixture.d.ts.map +1 -1
- package/dist/fixture/fixture.js +1 -17
- package/dist/fixture/fixture.js.map +1 -1
- package/dist/fixture/witness-client.d.ts +6 -174
- package/dist/fixture/witness-client.d.ts.map +1 -1
- package/dist/fixture/witness-client.js +6 -267
- package/dist/fixture/witness-client.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +6 -12
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +6 -17
- package/dist/json.js.map +1 -1
- package/dist/reporter/project-graph-reporter.d.ts +92 -0
- package/dist/reporter/project-graph-reporter.d.ts.map +1 -0
- package/dist/reporter/project-graph-reporter.js +92 -0
- package/dist/reporter/project-graph-reporter.js.map +1 -0
- package/dist/reporter/reporter.cjs.map +1 -1
- package/dist/reporter/reporter.d.ts +93 -4
- package/dist/reporter/reporter.d.ts.map +1 -1
- package/dist/reporter/reporter.js +174 -18
- package/dist/reporter/reporter.js.map +1 -1
- package/dist/runner-claims.d.ts +13 -0
- package/dist/runner-claims.d.ts.map +1 -0
- package/dist/runner-claims.js +46 -0
- package/dist/runner-claims.js.map +1 -0
- package/dist/runner-resolution.d.ts +3 -0
- package/dist/runner-resolution.d.ts.map +1 -0
- package/dist/runner-resolution.js +14 -0
- package/dist/runner-resolution.js.map +1 -0
- package/dist/supervisor/client.d.ts +23 -1
- package/dist/supervisor/client.d.ts.map +1 -1
- package/dist/supervisor/client.js +49 -0
- package/dist/supervisor/client.js.map +1 -1
- package/dist/supervisor/drain.d.ts +65 -0
- package/dist/supervisor/drain.d.ts.map +1 -1
- package/dist/supervisor/drain.js +353 -9
- package/dist/supervisor/drain.js.map +1 -1
- package/dist/supervisor/index.d.ts +7 -3
- package/dist/supervisor/index.d.ts.map +1 -1
- package/dist/supervisor/index.js +7 -3
- package/dist/supervisor/index.js.map +1 -1
- package/dist/supervisor/spool.d.ts +65 -2
- package/dist/supervisor/spool.d.ts.map +1 -1
- package/dist/supervisor/spool.js +47 -0
- package/dist/supervisor/spool.js.map +1 -1
- package/dist/surface.d.ts +6 -207
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +6 -264
- package/dist/surface.js.map +1 -1
- package/dist/vitest/index.d.ts +23 -0
- package/dist/vitest/index.d.ts.map +1 -0
- package/dist/vitest/index.js +356 -0
- package/dist/vitest/index.js.map +1 -0
- package/dist/vitest/reporter.d.ts +83 -0
- package/dist/vitest/reporter.d.ts.map +1 -0
- package/dist/vitest/reporter.js +211 -0
- package/dist/vitest/reporter.js.map +1 -0
- package/dist/vitest/worker-slot.d.ts +36 -0
- package/dist/vitest/worker-slot.d.ts.map +1 -0
- package/dist/vitest/worker-slot.js +51 -0
- package/dist/vitest/worker-slot.js.map +1 -0
- package/dist/witness/adapter-registry.d.ts +6 -50
- package/dist/witness/adapter-registry.d.ts.map +1 -1
- package/dist/witness/adapter-registry.js +6 -257
- package/dist/witness/adapter-registry.js.map +1 -1
- package/dist/witness/behavior-request.d.ts +6 -76
- package/dist/witness/behavior-request.d.ts.map +1 -1
- package/dist/witness/behavior-request.js +6 -273
- package/dist/witness/behavior-request.js.map +1 -1
- package/dist/witness/behavior.d.ts +6 -48
- package/dist/witness/behavior.d.ts.map +1 -1
- package/dist/witness/behavior.js +6 -114
- package/dist/witness/behavior.js.map +1 -1
- package/dist/witness/bin.d.ts +6 -17
- package/dist/witness/bin.d.ts.map +1 -1
- package/dist/witness/bin.js +6 -168
- package/dist/witness/bin.js.map +1 -1
- package/dist/witness/browser.d.ts +14 -206
- package/dist/witness/browser.d.ts.map +1 -1
- package/dist/witness/browser.js +15 -654
- package/dist/witness/browser.js.map +1 -1
- package/dist/witness/classifications.d.ts +6 -38
- package/dist/witness/classifications.d.ts.map +1 -1
- package/dist/witness/classifications.js +6 -83
- package/dist/witness/classifications.js.map +1 -1
- package/dist/witness/env-attestation.d.ts +6 -94
- package/dist/witness/env-attestation.d.ts.map +1 -1
- package/dist/witness/env-attestation.js +6 -198
- package/dist/witness/env-attestation.js.map +1 -1
- package/dist/witness/fixture-provider.d.ts +6 -82
- package/dist/witness/fixture-provider.d.ts.map +1 -1
- package/dist/witness/fixture-provider.js +6 -105
- package/dist/witness/fixture-provider.js.map +1 -1
- package/dist/witness/loopback-pins.d.ts +6 -75
- package/dist/witness/loopback-pins.d.ts.map +1 -1
- package/dist/witness/loopback-pins.js +6 -240
- package/dist/witness/loopback-pins.js.map +1 -1
- package/dist/witness/server.d.ts +8 -26
- package/dist/witness/server.d.ts.map +1 -1
- package/dist/witness/server.js +24 -3761
- package/dist/witness/server.js.map +1 -1
- package/dist/witness/types.d.ts +6 -882
- package/dist/witness/types.d.ts.map +1 -1
- package/dist/witness/types.js +9 -1
- package/dist/witness/types.js.map +1 -1
- package/examples/overlay-proof.spec.js +75 -0
- package/package.json +8 -2
- package/python/gateforge_pytest_plugin.py +462 -0
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* TRUST BOUNDARY (plan §3.3, phase 2 item 4): `--list` loads the
|
|
8
8
|
* consumer's playwright config and test modules as UNTRUSTED code —
|
|
9
9
|
* they execute in a child process. This module therefore:
|
|
10
|
-
* - strips every `GATEFORGE_*` variable
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* - strips every `GATEFORGE_*` variable from the scrubbed child
|
|
11
|
+
* environment, then sets only an isolated, secret-free temporary
|
|
12
|
+
* `GATEFORGE_STATE_DIR`;
|
|
13
|
+
* - gives wired comparison listings only the safe run-variable allowlist
|
|
14
|
+
* actually exposed to runner children;
|
|
15
|
+
* - removes temporary state after scrubbed enumeration;
|
|
15
16
|
* - enforces a finite timeout (the child is killed; a timeout is a
|
|
16
17
|
* typed failure, never a hang or an empty inventory);
|
|
17
18
|
* - treats a failed invocation as a typed error (CLI exit 2), while
|
|
@@ -37,27 +38,12 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync }
|
|
|
37
38
|
import { createRequire } from 'node:module';
|
|
38
39
|
import { tmpdir } from 'node:os';
|
|
39
40
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
'playwright.config.cts',
|
|
47
|
-
'playwright.config.js',
|
|
48
|
-
'playwright.config.mjs',
|
|
49
|
-
'playwright.config.cjs',
|
|
50
|
-
];
|
|
51
|
-
/** Directory names never searched for a nested playwright config
|
|
52
|
-
* (dependency trees, build output, VCS state, runner artifacts). */
|
|
53
|
-
const CONFIG_SEARCH_PRUNED_DIRS = new Set([
|
|
54
|
-
'node_modules',
|
|
55
|
-
'dist',
|
|
56
|
-
'.git',
|
|
57
|
-
'test-results',
|
|
58
|
-
'coverage',
|
|
59
|
-
'build',
|
|
60
|
-
]);
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
import { buildRunnerChildEnv } from './runner-env.js';
|
|
43
|
+
import { CLAIM_ANNOTATION_TYPE, ENV_PLAYWRIGHT_CONFIG_DIR } from '../constants.js';
|
|
44
|
+
import { localPlaywrightCliCandidates } from '../runner-resolution.js';
|
|
45
|
+
import { CONFIG_SEARCH_PRUNED_DIRS, PLAYWRIGHT_CONFIG_NAMES } from './config-locations.js';
|
|
46
|
+
import { PROJECT_GRAPH_PATH_ENV } from '../reporter/project-graph-reporter.js';
|
|
61
47
|
/** Default wall-clock bound for one `--list` invocation. */
|
|
62
48
|
export const DEFAULT_LIST_TIMEOUT_MS = 60_000;
|
|
63
49
|
/** Typed discovery failure: a playwright invocation that could not run
|
|
@@ -68,17 +54,43 @@ export class TestDiscoveryError extends Error {
|
|
|
68
54
|
this.name = 'TestDiscoveryError';
|
|
69
55
|
}
|
|
70
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves the engine-owned project-graph reporter entry.
|
|
59
|
+
*
|
|
60
|
+
* Absolute and pack-relative for the same reason the trusted reporter
|
|
61
|
+
* entry is: the enumeration child must load an ENGINE file by absolute
|
|
62
|
+
* path, never a candidate-relative one. Resolution works from both the
|
|
63
|
+
* `src/` and `dist/` layouts.
|
|
64
|
+
*
|
|
65
|
+
* @param fromModule: module URL to resolve the pack from (default: this file).
|
|
66
|
+
*
|
|
67
|
+
* @returns
|
|
68
|
+
* string: absolute `<pack>/dist/reporter/project-graph-reporter.js`.
|
|
69
|
+
*/
|
|
70
|
+
export function projectGraphReporterEntry(fromModule = import.meta.url) {
|
|
71
|
+
const pkgPath = fileURLToPath(new URL('../../package.json', fromModule));
|
|
72
|
+
return join(pkgPath.slice(0, -'package.json'.length), 'dist', 'reporter', 'project-graph-reporter.js');
|
|
73
|
+
}
|
|
71
74
|
/**
|
|
72
75
|
* The playwright CLI of the repo being scanned, else the pack's own.
|
|
73
|
-
* Resolution order (consumer-first,
|
|
74
|
-
* DIRECTORY's own install
|
|
75
|
-
*
|
|
76
|
-
* against it dies with
|
|
77
|
-
*
|
|
76
|
+
* Resolution order (consumer-first, nearest-first): the CONFIG
|
|
77
|
+
* DIRECTORY's own install and then every directory above it (a
|
|
78
|
+
* subdirectory project pins the playwright version its config and
|
|
79
|
+
* specs load through — running any other version against it dies with
|
|
80
|
+
* the two-versions-of-@playwright/test conflict), and within each
|
|
81
|
+
* directory the `@playwright/test` CLI before the bare `playwright`
|
|
82
|
+
* one (see {@link localPlaywrightCliCandidates}).
|
|
78
83
|
*
|
|
79
84
|
* Args:
|
|
80
85
|
* cwd: absolute repo root.
|
|
81
86
|
* configDir: config directory repo-relative (`'.'` for root-level).
|
|
87
|
+
*
|
|
88
|
+
* Returns:
|
|
89
|
+
* string: absolute path of the CLI to invoke.
|
|
90
|
+
*
|
|
91
|
+
* Throws:
|
|
92
|
+
* TestDiscoveryError: when neither the repo's own nor the pack's CLI
|
|
93
|
+
* exists (a broken pack dependency, never a silent fallback).
|
|
82
94
|
*/
|
|
83
95
|
function playwrightCliPath(cwd, configDir) {
|
|
84
96
|
// CONSUMER-FIRST resolution: a consumer repo pins its own
|
|
@@ -86,15 +98,12 @@ function playwrightCliPath(cwd, configDir) {
|
|
|
86
98
|
// through it). Running the pack's CLI against a consumer whose local
|
|
87
99
|
// version differs dies with the two-versions-of-@playwright/test
|
|
88
100
|
// conflict — so the scanned repo's own CLI wins when present
|
|
89
|
-
// (consumer migration, E22). The pack's CLI
|
|
90
|
-
// (fixture repos symlink the monorepo
|
|
91
|
-
// to the same bytes either way).
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (existsSync(candidate))
|
|
96
|
-
return candidate;
|
|
97
|
-
}
|
|
101
|
+
// (consumer migration, E22; install rehearsal F7). The pack's CLI
|
|
102
|
+
// remains the fallback (fixture repos symlink the monorepo
|
|
103
|
+
// node_modules, so they resolve to the same bytes either way).
|
|
104
|
+
for (const candidate of localPlaywrightCliCandidates(join(cwd, configDir))) {
|
|
105
|
+
if (existsSync(candidate))
|
|
106
|
+
return candidate;
|
|
98
107
|
}
|
|
99
108
|
const require = createRequire(import.meta.url);
|
|
100
109
|
const pkgJson = require.resolve('playwright/package.json');
|
|
@@ -104,13 +113,6 @@ function playwrightCliPath(cwd, configDir) {
|
|
|
104
113
|
}
|
|
105
114
|
return cli;
|
|
106
115
|
}
|
|
107
|
-
/** The scanned repo's local playwright CLI locations, in preference order. */
|
|
108
|
-
export function localPlaywrightCliCandidates(cwd) {
|
|
109
|
-
return [
|
|
110
|
-
join(cwd, 'node_modules', 'playwright', 'cli.js'),
|
|
111
|
-
join(cwd, 'node_modules', '@playwright', 'test', 'cli.js'),
|
|
112
|
-
];
|
|
113
|
-
}
|
|
114
116
|
/**
|
|
115
117
|
* Strips every `GATEFORGE_*` variable from the environment for UNTRUSTED
|
|
116
118
|
* child runs, then adds only a caller-owned temporary state directory.
|
|
@@ -135,39 +137,38 @@ export function untrustedEnv(env, discoveryStateDir) {
|
|
|
135
137
|
return child;
|
|
136
138
|
}
|
|
137
139
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* subdirectory project), or null when none exists.
|
|
140
|
+
* Enumerates EVERY playwright config the search space contains, in the
|
|
141
|
+
* order the choice is made: repo-root configs first (in
|
|
142
|
+
* {@link PLAYWRIGHT_CONFIG_NAMES} order), then ONE directory level
|
|
143
|
+
* deep (immediate subdirectories, dependency/build/VCS/runner
|
|
144
|
+
* directories pruned, alphabetically).
|
|
144
145
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
146
|
+
* Enumeration runs exactly one config, so a repo with several configs
|
|
147
|
+
* is inventoried as a subset. Listing them all is what lets that
|
|
148
|
+
* narrowing be reported instead of silent — see
|
|
149
|
+
* {@link findPlaywrightConfig}.
|
|
149
150
|
*
|
|
150
151
|
* Args:
|
|
151
152
|
* cwd: absolute repo root.
|
|
152
153
|
*
|
|
153
154
|
* Returns:
|
|
154
|
-
* string
|
|
155
|
+
* string[]: repo-relative posix config paths, in choice order.
|
|
155
156
|
*/
|
|
156
|
-
export function
|
|
157
|
+
export function findPlaywrightConfigs(cwd) {
|
|
158
|
+
const found = [];
|
|
157
159
|
for (const name of PLAYWRIGHT_CONFIG_NAMES) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return name;
|
|
160
|
+
if (existsSync(join(cwd, name)))
|
|
161
|
+
found.push(name);
|
|
161
162
|
}
|
|
162
163
|
let names;
|
|
163
164
|
try {
|
|
164
165
|
names = readdirSync(cwd);
|
|
165
166
|
}
|
|
166
167
|
catch {
|
|
167
|
-
return
|
|
168
|
+
return found; // unreadable root: the root-level search already came up empty
|
|
168
169
|
}
|
|
169
170
|
const subdirs = names
|
|
170
|
-
.filter((name) =>
|
|
171
|
+
.filter((name) => CONFIG_SEARCH_PRUNED_DIRS[name] !== true)
|
|
171
172
|
.filter((name) => {
|
|
172
173
|
try {
|
|
173
174
|
return statSync(join(cwd, name)).isDirectory();
|
|
@@ -180,16 +181,134 @@ export function findPlaywrightConfig(cwd) {
|
|
|
180
181
|
for (const dir of subdirs) {
|
|
181
182
|
for (const name of PLAYWRIGHT_CONFIG_NAMES) {
|
|
182
183
|
if (existsSync(join(cwd, dir, name)))
|
|
183
|
-
|
|
184
|
+
found.push(`${dir}/${name}`);
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
|
-
return
|
|
187
|
+
return found;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Finds the ONE consumer playwright config enumeration runs: at the
|
|
191
|
+
* repo root first, then — only when no root-level config exists — ONE
|
|
192
|
+
* directory level deep (see {@link findPlaywrightConfigs}). Returns
|
|
193
|
+
* the repo-relative posix path (`'playwright.config.ts'`, or
|
|
194
|
+
* `'e2e/playwright.config.ts'` for a subdirectory project), or null
|
|
195
|
+
* when none exists.
|
|
196
|
+
*
|
|
197
|
+
* Root-level configs always win: an existing root project must keep its
|
|
198
|
+
* exact historical invocation. The nested search only extends discovery
|
|
199
|
+
* to the self-contained subdirectory layout (the config's OWN directory
|
|
200
|
+
* pins its playwright install — see {@link playwrightCliPath}).
|
|
201
|
+
*
|
|
202
|
+
* The choice is never silent: when more than one config exists,
|
|
203
|
+
* {@link listNativePlaywrightTests} names every discovered config, the
|
|
204
|
+
* one used and why, and the ones NOT inventoried. There is deliberately
|
|
205
|
+
* no configuration key for the choice: any such key would change WHICH
|
|
206
|
+
* consumer code runs, not just what is reported, and it would have to
|
|
207
|
+
* be plumbed through the discovery callers' option sets.
|
|
208
|
+
*
|
|
209
|
+
* Args:
|
|
210
|
+
* cwd: absolute repo root.
|
|
211
|
+
*
|
|
212
|
+
* Returns:
|
|
213
|
+
* string | null: repo-relative posix config path, or null.
|
|
214
|
+
*/
|
|
215
|
+
export function findPlaywrightConfig(cwd) {
|
|
216
|
+
return findPlaywrightConfigs(cwd)[0] ?? null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Builds the one-line disclosure appended to enumeration's detail when
|
|
220
|
+
* the repo holds more than one playwright config. Without it a repo
|
|
221
|
+
* with several suites is silently graded as a subset.
|
|
222
|
+
*
|
|
223
|
+
* Args:
|
|
224
|
+
* configs: every discovered config, in choice order (first = used).
|
|
225
|
+
* used: the config enumeration ran.
|
|
226
|
+
*
|
|
227
|
+
* Returns:
|
|
228
|
+
* string: the disclosure, or '' when there is nothing to disclose.
|
|
229
|
+
*/
|
|
230
|
+
function configChoiceNote(configs, used) {
|
|
231
|
+
if (configs.length < 2)
|
|
232
|
+
return '';
|
|
233
|
+
const reason = used.includes('/')
|
|
234
|
+
? 'no repo-root config exists, so the alphabetically first subdirectory config is inventoried'
|
|
235
|
+
: 'a repo-root config always wins, so a root project keeps its exact invocation';
|
|
236
|
+
const listed = configs
|
|
237
|
+
.map((path) => path === used
|
|
238
|
+
? `${path} (inventoried: ${reason})`
|
|
239
|
+
: `${path} (not inventoried: its test cases are missing from this catalog)`)
|
|
240
|
+
.join(', ');
|
|
241
|
+
return (` — note: ${String(configs.length)} playwright configs are present, and only 1 is inventoried: ${listed}`);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* True when a parsed graph field is a plain object (never an array, never
|
|
245
|
+
* null): the container both project-graph maps are.
|
|
246
|
+
*/
|
|
247
|
+
function isPlainRecord(value) {
|
|
248
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
249
|
+
}
|
|
250
|
+
/** True when a parsed field is an object whose values are all strings. */
|
|
251
|
+
function isStringRecord(value) {
|
|
252
|
+
return isPlainRecord(value) && Object.values(value).every((entry) => typeof entry === 'string');
|
|
253
|
+
}
|
|
254
|
+
/** True when a parsed dependency list is an array of strings. */
|
|
255
|
+
function isStringArray(value) {
|
|
256
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
187
257
|
}
|
|
188
258
|
/** Converts an absolute path to repo-root-relative posix form. */
|
|
189
259
|
function toRepoRelative(cwd, path) {
|
|
190
260
|
const rel = relative(cwd, resolve(cwd, path));
|
|
191
261
|
return rel.split('\\').join('/');
|
|
192
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Reads the `version` of an installed package by its directory (the
|
|
265
|
+
* `<pkg>/package.json` beside a CLI), or null when it is unreadable.
|
|
266
|
+
*
|
|
267
|
+
* Args:
|
|
268
|
+
* packageDir: absolute directory of the installed package.
|
|
269
|
+
*
|
|
270
|
+
* Returns:
|
|
271
|
+
* string | null: the declared version, or null when unreadable.
|
|
272
|
+
*/
|
|
273
|
+
function installedVersion(packageDir) {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
276
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Expands a runner load error that is really a playwright VERSION
|
|
284
|
+
* conflict into one sentence naming both versions and the real cause.
|
|
285
|
+
* A missing dependency is the wrong diagnosis there: nothing is
|
|
286
|
+
* missing, the two-versions-of-@playwright/test conflict is (install
|
|
287
|
+
* rehearsal F7), and the pack's pinned copy must not be what runs the
|
|
288
|
+
* consumer's project. Every other message is passed through
|
|
289
|
+
* unchanged.
|
|
290
|
+
*
|
|
291
|
+
* Args:
|
|
292
|
+
* message: one reporter load error.
|
|
293
|
+
* cli: absolute path of the CLI that produced it.
|
|
294
|
+
*
|
|
295
|
+
* Returns:
|
|
296
|
+
* string: the message, with the conflict named when it is one.
|
|
297
|
+
*/
|
|
298
|
+
function diagnoseRunnerLoadError(message, cli) {
|
|
299
|
+
if (!/did not expect test\(\) to be called here/.test(message))
|
|
300
|
+
return message;
|
|
301
|
+
const require = createRequire(import.meta.url);
|
|
302
|
+
const packDir = dirname(require.resolve('playwright/package.json'));
|
|
303
|
+
const runnerDir = dirname(cli);
|
|
304
|
+
const runnerName = basename(runnerDir) === 'test' ? `@playwright/${basename(runnerDir)}` : basename(runnerDir);
|
|
305
|
+
const runner = `${runnerName}@${installedVersion(runnerDir) ?? 'unknown version'}`;
|
|
306
|
+
const pack = `playwright@${installedVersion(packDir) ?? 'unknown version'} (the pack's pin)`;
|
|
307
|
+
return (`${message} — this is the two-versions-of-@playwright/test conflict, not a missing ` +
|
|
308
|
+
`dependency: this project was enumerated with ${runner} while the pack pins ${pack}. ` +
|
|
309
|
+
"Make the project's own @playwright/test the one Gateforge runs (remove the other copy from " +
|
|
310
|
+
'node_modules), or align both to one version.');
|
|
311
|
+
}
|
|
193
312
|
/**
|
|
194
313
|
* Enumerates the consumer's playwright tests via official list mode.
|
|
195
314
|
* See the module doc for the trust boundary. The CLI is the consumer's
|
|
@@ -205,8 +324,8 @@ function toRepoRelative(cwd, path) {
|
|
|
205
324
|
* invocation (repo-root cwd, auto-discovered config, no `--config`).
|
|
206
325
|
*
|
|
207
326
|
* Args:
|
|
208
|
-
* options: `cwd` (absolute repo root)
|
|
209
|
-
*
|
|
327
|
+
* options: `cwd` (absolute repo root), optional `timeoutMs`, and
|
|
328
|
+
* optional allowlisted wired-runner variables for registration comparison.
|
|
210
329
|
*
|
|
211
330
|
* Returns:
|
|
212
331
|
* Promise<NativeListResult>: enumerated instances, reporter errors,
|
|
@@ -214,10 +333,12 @@ function toRepoRelative(cwd, path) {
|
|
|
214
333
|
*
|
|
215
334
|
* Throws:
|
|
216
335
|
* TestDiscoveryError: when the child cannot spawn, exceeds the
|
|
217
|
-
* timeout, or
|
|
336
|
+
* timeout, or produced no readable reporter JSON (the report is read
|
|
337
|
+
* from the reporter's own output file, never from stdout).
|
|
218
338
|
*/
|
|
219
339
|
export async function listNativePlaywrightTests(options) {
|
|
220
|
-
const
|
|
340
|
+
const configs = findPlaywrightConfigs(options.cwd);
|
|
341
|
+
const configPath = configs[0] ?? null;
|
|
221
342
|
if (configPath === null) {
|
|
222
343
|
return {
|
|
223
344
|
status: 'unavailable',
|
|
@@ -231,15 +352,53 @@ export async function listNativePlaywrightTests(options) {
|
|
|
231
352
|
const childCwd = join(options.cwd, configDir);
|
|
232
353
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LIST_TIMEOUT_MS;
|
|
233
354
|
const cli = playwrightCliPath(options.cwd, configDir);
|
|
234
|
-
|
|
355
|
+
// The JSON report is read from a FILE the runner writes, never from
|
|
356
|
+
// stdout: a consumer's playwright config routinely prints at load
|
|
357
|
+
// time (a dotenv/dotenvx banner, a stray `console.log`) and stdout is
|
|
358
|
+
// the runner's own channel, not a document channel (install
|
|
359
|
+
// rehearsal F6). The path is absolute, so the reporter's
|
|
360
|
+
// cwd-relative resolution cannot move it, and the JSON reporter's
|
|
361
|
+
// `printsToStdio()` turns false — no part of the report can
|
|
362
|
+
// interleave with the config's logging. The same directory holds the
|
|
363
|
+
// project-graph document (below) and is removed with the child.
|
|
364
|
+
const reportDir = mkdtempSync(join(tmpdir(), 'gateforge-playwright-report-'));
|
|
365
|
+
const reportPath = join(reportDir, 'reporter.json');
|
|
366
|
+
// The project graph rides along with the json report through an
|
|
367
|
+
// ENGINE-OWNED reporter (see {@link projectGraphReporterEntry}): the
|
|
368
|
+
// json reporter's `config.projects[]` does not carry `dependencies` in
|
|
369
|
+
// any released playwright (verified against 1.58.2 and 1.62.1), and
|
|
370
|
+
// reading them out of the consumer config would mean trusting candidate
|
|
371
|
+
// code. Without the built entry the enumeration runs exactly as before
|
|
372
|
+
// and reports no graph — the field is optional precisely so a missing
|
|
373
|
+
// graph is honest absence, never a guessed empty one.
|
|
374
|
+
const graphReporterEntry = projectGraphReporterEntry();
|
|
375
|
+
const args = [
|
|
376
|
+
cli,
|
|
377
|
+
'test',
|
|
378
|
+
'--list',
|
|
379
|
+
`--reporter=json${existsSync(graphReporterEntry) ? `,${graphReporterEntry}` : ''}`,
|
|
380
|
+
];
|
|
235
381
|
if (configDir !== '.')
|
|
236
382
|
args.push('--config', basename(configPath));
|
|
237
|
-
const discoveryStateDir = mkdtempSync(join(tmpdir(), 'gateforge-discovery-state-'));
|
|
383
|
+
const discoveryStateDir = options.wiredEnv === undefined ? mkdtempSync(join(tmpdir(), 'gateforge-discovery-state-')) : undefined;
|
|
238
384
|
let outcome;
|
|
385
|
+
let reportText = null;
|
|
386
|
+
let graphText = null;
|
|
239
387
|
try {
|
|
388
|
+
const childEnv = options.wiredEnv === undefined
|
|
389
|
+
? untrustedEnv(process.env, discoveryStateDir)
|
|
390
|
+
: buildRunnerChildEnv(options.wiredEnv, process.env);
|
|
391
|
+
// The evidence fixture binds to the CONSUMER's runner, resolved from
|
|
392
|
+
// the config directory this enumeration just discovered: that is what
|
|
393
|
+
// makes a non-root config with its own `node_modules` work, not only a
|
|
394
|
+
// hoisted repository-root install. Set on BOTH the scrubbed and the
|
|
395
|
+
// wired child so the two registrations can never differ.
|
|
396
|
+
childEnv[ENV_PLAYWRIGHT_CONFIG_DIR] = childCwd;
|
|
397
|
+
childEnv['PLAYWRIGHT_JSON_OUTPUT_FILE'] = reportPath;
|
|
398
|
+
childEnv[PROJECT_GRAPH_PATH_ENV] = join(reportDir, 'project-graph.json');
|
|
240
399
|
const child = spawn(process.execPath, args, {
|
|
241
400
|
cwd: childCwd,
|
|
242
|
-
env:
|
|
401
|
+
env: childEnv,
|
|
243
402
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
244
403
|
});
|
|
245
404
|
outcome = await new Promise((settle) => {
|
|
@@ -273,9 +432,27 @@ export async function listNativePlaywrightTests(options) {
|
|
|
273
432
|
finish({ code, stdout, stderr, timedOut, error: null });
|
|
274
433
|
});
|
|
275
434
|
});
|
|
435
|
+
try {
|
|
436
|
+
reportText = readFileSync(reportPath, 'utf8');
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// A runner that never reached the reporter (or one whose version
|
|
440
|
+
// predates its output-file support) leaves no file. The whole
|
|
441
|
+
// captured stream is then the only candidate, and it is read as
|
|
442
|
+
// one document — never scanned for a plausible-looking substring.
|
|
443
|
+
reportText = outcome.stdout;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
graphText = readFileSync(join(reportDir, 'project-graph.json'), 'utf8');
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
graphText = null;
|
|
450
|
+
}
|
|
276
451
|
}
|
|
277
452
|
finally {
|
|
278
|
-
|
|
453
|
+
if (discoveryStateDir !== undefined)
|
|
454
|
+
rmSync(discoveryStateDir, { recursive: true, force: true });
|
|
455
|
+
rmSync(reportDir, { recursive: true, force: true });
|
|
279
456
|
}
|
|
280
457
|
if (outcome.error !== null) {
|
|
281
458
|
throw new TestDiscoveryError(`playwright --list failed to run: ${outcome.error.message}`);
|
|
@@ -285,10 +462,12 @@ export async function listNativePlaywrightTests(options) {
|
|
|
285
462
|
}
|
|
286
463
|
let document;
|
|
287
464
|
try {
|
|
288
|
-
document = JSON.parse(
|
|
465
|
+
document = JSON.parse(reportText ?? '');
|
|
289
466
|
}
|
|
290
467
|
catch {
|
|
291
|
-
throw new TestDiscoveryError(`playwright --list produced
|
|
468
|
+
throw new TestDiscoveryError(`playwright --list produced no readable reporter JSON (exit ${String(outcome.code)}). The report ` +
|
|
469
|
+
"is read from the reporter's own output file, so a config that logs to stdout no longer corrupts " +
|
|
470
|
+
'it — this means the run never reached the reporter. Runner output: ' +
|
|
292
471
|
`${(outcome.stderr || outcome.stdout).slice(0, 400)}`);
|
|
293
472
|
}
|
|
294
473
|
// The runner reports files relative to ITS rootDir — the config
|
|
@@ -340,13 +519,151 @@ export async function listNativePlaywrightTests(options) {
|
|
|
340
519
|
}
|
|
341
520
|
};
|
|
342
521
|
walkSuites(document.suites ?? [], [], null);
|
|
343
|
-
const errors = (document.errors ?? []).map((error) => error.message ?? String(error));
|
|
522
|
+
const errors = (document.errors ?? []).map((error) => diagnoseRunnerLoadError(error.message ?? String(error), cli));
|
|
523
|
+
// The project graph the runner resolved, plus the storage states its
|
|
524
|
+
// projects declare. A document that does not parse — or that carries a
|
|
525
|
+
// field this reader does not understand — is absence, never a partial
|
|
526
|
+
// graph: a downstream run that emitted a `dependencies` edge from half
|
|
527
|
+
// a graph would order projects wrongly, and one that honored half the
|
|
528
|
+
// declared states would authenticate the wrong projects.
|
|
529
|
+
let projectDependencies;
|
|
530
|
+
let projectStorageStates;
|
|
531
|
+
if (graphText !== null) {
|
|
532
|
+
try {
|
|
533
|
+
const parsed = JSON.parse(graphText);
|
|
534
|
+
// Untrusted input: everything below is checked at runtime, and one
|
|
535
|
+
// bad field discards the WHOLE document rather than half of it.
|
|
536
|
+
const graph = parsed.projectDependencies;
|
|
537
|
+
const states = parsed.projectStorageStates;
|
|
538
|
+
if (parsed.schemaVersion === 2 && isPlainRecord(graph)) {
|
|
539
|
+
// Null-prototype: a project NAME is candidate data and `__proto__`
|
|
540
|
+
// is a legal one, so the maps keyed by it must not inherit.
|
|
541
|
+
const dependencies = Object.create(null);
|
|
542
|
+
const storageStates = Object.create(null);
|
|
543
|
+
let wellFormed = true;
|
|
544
|
+
for (const [name, edges] of Object.entries(graph)) {
|
|
545
|
+
if (!isStringArray(edges)) {
|
|
546
|
+
wellFormed = false;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
dependencies[name] = [...new Set(edges.filter((edge) => edge.length > 0))].sort();
|
|
550
|
+
}
|
|
551
|
+
if (wellFormed && states !== undefined) {
|
|
552
|
+
if (!isStringRecord(states)) {
|
|
553
|
+
wellFormed = false;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
for (const [name, value] of Object.entries(states)) {
|
|
557
|
+
// An empty string is carried, not filtered: whether it may
|
|
558
|
+
// be read is the planner's refusal, never this reader's
|
|
559
|
+
// silent omission.
|
|
560
|
+
storageStates[name] = value;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (wellFormed) {
|
|
565
|
+
projectDependencies = dependencies;
|
|
566
|
+
// Omitted when nothing declared a state — an empty map would
|
|
567
|
+
// read as "every project runs with an empty state".
|
|
568
|
+
if (Object.keys(storageStates).length > 0)
|
|
569
|
+
projectStorageStates = storageStates;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
projectDependencies = undefined;
|
|
575
|
+
projectStorageStates = undefined;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
344
578
|
const configDetail = configDir !== '.' ? ` (cwd '${configDir}')` : '';
|
|
579
|
+
const envDetail = options.wiredEnv === undefined
|
|
580
|
+
? 'isolated temporary GATEFORGE_STATE_DIR'
|
|
581
|
+
: 'allowlisted wired runner variables';
|
|
345
582
|
return {
|
|
346
583
|
status: 'discovered',
|
|
347
|
-
detail: `native playwright --list over '${configPath}'${configDetail} enumerated ${String(instances.length)}
|
|
584
|
+
detail: `native playwright --list over '${configPath}'${configDetail} enumerated ${String(instances.length)} ` +
|
|
585
|
+
`instance(s) as untrusted code (${envDetail})` + configChoiceNote(configs, configPath),
|
|
348
586
|
instances,
|
|
349
587
|
errors,
|
|
588
|
+
...(projectDependencies !== undefined ? { projectDependencies } : {}),
|
|
589
|
+
...(projectStorageStates !== undefined ? { projectStorageStates } : {}),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Finds registration instances present in only one environment's native
|
|
594
|
+
* Playwright listing, treating project and duplicate instances as identity.
|
|
595
|
+
*
|
|
596
|
+
* Args:
|
|
597
|
+
* scrubbed: native instances enumerated without run wiring.
|
|
598
|
+
* wired: native instances enumerated with the runner's safe run variables.
|
|
599
|
+
*
|
|
600
|
+
* Returns:
|
|
601
|
+
* An object containing project-qualified instances unique to each listing.
|
|
602
|
+
*/
|
|
603
|
+
export function diffNativePlaywrightTests(scrubbed, wired) {
|
|
604
|
+
/**
|
|
605
|
+
* Builds the project-qualified matching identity for one instance.
|
|
606
|
+
*
|
|
607
|
+
* Args:
|
|
608
|
+
* instance: native test instance.
|
|
609
|
+
*
|
|
610
|
+
* Returns:
|
|
611
|
+
* string: serialized file, title path, and project tuple.
|
|
612
|
+
*/
|
|
613
|
+
const keyOf = (instance) => JSON.stringify([instance.file, instance.titlePath, instance.project]);
|
|
614
|
+
const wiredCounts = new Map();
|
|
615
|
+
for (const instance of wired) {
|
|
616
|
+
const key = keyOf(instance);
|
|
617
|
+
wiredCounts.set(key, (wiredCounts.get(key) ?? 0) + 1);
|
|
618
|
+
}
|
|
619
|
+
const matchedCounts = new Map();
|
|
620
|
+
const scrubbedOnly = [];
|
|
621
|
+
for (const instance of scrubbed) {
|
|
622
|
+
const key = keyOf(instance);
|
|
623
|
+
const matched = matchedCounts.get(key) ?? 0;
|
|
624
|
+
if (matched < (wiredCounts.get(key) ?? 0)) {
|
|
625
|
+
matchedCounts.set(key, matched + 1);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
scrubbedOnly.push(instance);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const wiredOnly = [];
|
|
632
|
+
const emittedCounts = new Map();
|
|
633
|
+
for (const instance of wired) {
|
|
634
|
+
const key = keyOf(instance);
|
|
635
|
+
const emitted = emittedCounts.get(key) ?? 0;
|
|
636
|
+
if (emitted < (matchedCounts.get(key) ?? 0)) {
|
|
637
|
+
emittedCounts.set(key, emitted + 1);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
wiredOnly.push(instance);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Sorts instances deterministically by file, project, and title path.
|
|
645
|
+
*
|
|
646
|
+
* Args:
|
|
647
|
+
* left: first native test instance.
|
|
648
|
+
* right: second native test instance.
|
|
649
|
+
*
|
|
650
|
+
* Returns:
|
|
651
|
+
* number: standard array comparator result.
|
|
652
|
+
*/
|
|
653
|
+
const compare = (left, right) => {
|
|
654
|
+
const byFile = left.file < right.file ? -1 : left.file > right.file ? 1 : 0;
|
|
655
|
+
if (byFile !== 0)
|
|
656
|
+
return byFile;
|
|
657
|
+
const byProject = left.project < right.project ? -1 : left.project > right.project ? 1 : 0;
|
|
658
|
+
if (byProject !== 0)
|
|
659
|
+
return byProject;
|
|
660
|
+
const leftTitlePath = left.titlePath.join('>');
|
|
661
|
+
const rightTitlePath = right.titlePath.join('>');
|
|
662
|
+
return leftTitlePath < rightTitlePath ? -1 : leftTitlePath > rightTitlePath ? 1 : 0;
|
|
663
|
+
};
|
|
664
|
+
return {
|
|
665
|
+
scrubbedOnly: scrubbedOnly.sort(compare),
|
|
666
|
+
wiredOnly: wiredOnly.sort(compare),
|
|
350
667
|
};
|
|
351
668
|
}
|
|
352
669
|
/** Matching key for reconciliation: file + full title path (no project). */
|