qa-core-flaky 0.1.0 → 0.1.1
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/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/dist/heal/run.js +4 -3
- package/dist/rerun/plan.d.ts +9 -1
- package/dist/rerun/plan.js +39 -3
- package/dist/rerun/run.d.ts +38 -1
- package/dist/rerun/run.js +107 -27
- package/package.json +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 - 2026-10-30
|
|
4
|
+
|
|
5
|
+
A Windows fix for `rerun`, plus package details and docs.
|
|
6
|
+
|
|
7
|
+
- **Fixed:** `rerun` now works on Windows. It used to stop with `spawn ... node_modules/.bin/playwright ENOENT`, because Windows cannot start that file. The tool now starts Playwright's own `cli.js` with Node on every OS. On Windows the child processes are no longer detached, so no console windows open during a rerun or a heal call, and cleanup stops the whole process tree with `taskkill /T`. The file filter on Windows now matches a backslash or a forward slash in the path, and the printed command shows that filter correctly. Reported by Abdul Rafay.
|
|
8
|
+
- **Changed:** The package author is now SprintSynergy (https://sprintsynergy.com), and Muhammad Usman is listed as a contributor.
|
|
9
|
+
- **Changed:** The printed rerun command now starts with `npx playwright test` when Playwright is installed in the folder where you run it.
|
|
10
|
+
- **Docs:** The README now shows version 0.1.1 and ends with a line saying who builds and maintains the project. The rerun docs explain how Playwright is found and started.
|
|
11
|
+
- **Tests:** New tests cover Windows file filters, the Playwright lookup, and how the rerun starts Playwright. The HTML example now shows version 0.1.1.
|
|
12
|
+
|
|
3
13
|
## 0.1.0 - 2026-10-29
|
|
4
14
|
|
|
5
15
|
First public release.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ qa-core-flaky triages flaky Playwright tests. It separates a likely real bug fro
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
Version 0.1.
|
|
7
|
+
Version 0.1.1. The commands are `parse`, `causes`, `triage`, and `rerun`.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -103,3 +103,5 @@ Tests and CI use the committed report. They do not install browsers and they do
|
|
|
103
103
|
## License
|
|
104
104
|
|
|
105
105
|
MIT. See [LICENSE](LICENSE).
|
|
106
|
+
|
|
107
|
+
Built by [SprintSynergy](https://sprintsynergy.com) and maintained by [Muhammad Usman](https://linkedin.com/in/sdet-muhammadusman).
|
package/dist/heal/run.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { rerunChildEnv, stopChildTree } from '../rerun/run.js';
|
|
5
|
+
import { DETACH_CHILDREN, rerunChildEnv, stopChildTree } from '../rerun/run.js';
|
|
6
6
|
/** Flags this tool must never put on a heal command line. */
|
|
7
7
|
export const FORBIDDEN_HEAL_ARGS = ['--apply', '--yes', '--no-verify'];
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
@@ -126,8 +126,9 @@ export function runHealCommand(args, timeoutMs = healTimeoutMs()) {
|
|
|
126
126
|
shell: false,
|
|
127
127
|
windowsHide: true,
|
|
128
128
|
env: rerunChildEnv(tempRoot),
|
|
129
|
-
// A new process group so the timeout can kill grandchildren.
|
|
130
|
-
|
|
129
|
+
// A new process group so the timeout can kill grandchildren. Not on
|
|
130
|
+
// Windows, where it opens a console window and taskkill /T is used.
|
|
131
|
+
detached: DETACH_CHILDREN,
|
|
131
132
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
132
133
|
});
|
|
133
134
|
bindHealChild(child);
|
package/dist/rerun/plan.d.ts
CHANGED
|
@@ -115,7 +115,15 @@ export declare function anchoredGrep(project: string, file: string, titlePath: r
|
|
|
115
115
|
* `old/tests/login.spec.ts`.
|
|
116
116
|
*/
|
|
117
117
|
export declare function fileFilterArg(file: string, line?: number): string;
|
|
118
|
-
/**
|
|
118
|
+
/** One path separator in a Windows file filter: a backslash or a forward slash. */
|
|
119
|
+
export declare const WINDOWS_SEPARATOR = "[\\\\/]";
|
|
120
|
+
/**
|
|
121
|
+
* Exact path, anchored, with an optional `:line`. Slashes are escaped inside `/.../`.
|
|
122
|
+
* A Windows path matches each separator with `[\\/]`. Playwright 1.60.0 tests
|
|
123
|
+
* the file filter against the native path, which uses backslashes on Windows,
|
|
124
|
+
* and then against its `file:///` URL. `[\\/]` matches the native path with
|
|
125
|
+
* either separator.
|
|
126
|
+
*/
|
|
119
127
|
export declare function exactFileArg(filePath: string, line?: number): string;
|
|
120
128
|
/**
|
|
121
129
|
* The command a person can copy. Every argument is redacted. The file
|
package/dist/rerun/plan.js
CHANGED
|
@@ -143,9 +143,22 @@ export function anchoredGrep(project, file, titlePath) {
|
|
|
143
143
|
export function fileFilterArg(file, line) {
|
|
144
144
|
return exactFileArg(file.replaceAll('\\', '/'), line);
|
|
145
145
|
}
|
|
146
|
-
/**
|
|
146
|
+
/** One path separator in a Windows file filter: a backslash or a forward slash. */
|
|
147
|
+
export const WINDOWS_SEPARATOR = '[\\\\/]';
|
|
148
|
+
/** A drive path (`C:\` or `C:/`), or a UNC or extended path (`\\server`, `\\?\`). */
|
|
149
|
+
const WINDOWS_ABSOLUTE = /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/])/;
|
|
150
|
+
/**
|
|
151
|
+
* Exact path, anchored, with an optional `:line`. Slashes are escaped inside `/.../`.
|
|
152
|
+
* A Windows path matches each separator with `[\\/]`. Playwright 1.60.0 tests
|
|
153
|
+
* the file filter against the native path, which uses backslashes on Windows,
|
|
154
|
+
* and then against its `file:///` URL. `[\\/]` matches the native path with
|
|
155
|
+
* either separator.
|
|
156
|
+
*/
|
|
147
157
|
export function exactFileArg(filePath, line) {
|
|
148
|
-
const
|
|
158
|
+
const body = WINDOWS_ABSOLUTE.test(filePath)
|
|
159
|
+
? filePath.split(/[\\/]/).map(escapeSlash).join(WINDOWS_SEPARATOR)
|
|
160
|
+
: escapeSlash(filePath);
|
|
161
|
+
const wrapped = `/^${body}$/`;
|
|
149
162
|
if (line === undefined)
|
|
150
163
|
return wrapped;
|
|
151
164
|
return `${wrapped}:${line}`;
|
|
@@ -167,7 +180,11 @@ export function formatPrintedCommand(args, record, prefix = []) {
|
|
|
167
180
|
const visibleTitle = title?.replaceAll('\\/', '/');
|
|
168
181
|
const titleChanged = visibleTitle !== undefined && redact(visibleTitle) !== visibleTitle;
|
|
169
182
|
const kept = titleChanged ? args.filter((part, index) => part !== '-g' && args[index - 1] !== '-g') : args;
|
|
170
|
-
const
|
|
183
|
+
const fileAt = kept.lastIndexOf('--') + 1;
|
|
184
|
+
const shown = [...prefix, ...kept]
|
|
185
|
+
.map((part, index) => index === prefix.length + fileAt && fileAt > 0 ? redactFileFilter(part, redact) : redact(part))
|
|
186
|
+
.map(shellQuote)
|
|
187
|
+
.join(' ');
|
|
171
188
|
if (!titleChanged)
|
|
172
189
|
return shown;
|
|
173
190
|
return `${shown} (the title was redacted, so -g is omitted here)`;
|
|
@@ -212,6 +229,25 @@ export function splitRerunSentence(nextStep) {
|
|
|
212
229
|
return { lead: nextStep, command: nextStep };
|
|
213
230
|
return { lead: nextStep.slice(0, at + marker.length), command };
|
|
214
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Redact a Windows file filter for printing. The native path is rebuilt from
|
|
234
|
+
* the filter, redacted the same way as any Windows path, and escaped again
|
|
235
|
+
* with `[\\/]` separators. The printed filter keeps the escapes and the
|
|
236
|
+
* separators the process received. Only the home becomes `<home>`.
|
|
237
|
+
* Any other argument is redacted as written.
|
|
238
|
+
*/
|
|
239
|
+
function redactFileFilter(part, redact) {
|
|
240
|
+
const match = /^\/\^(.*)\$\/(:\d+)?$/s.exec(part);
|
|
241
|
+
const body = match?.[1];
|
|
242
|
+
if (body === undefined || !part.includes(WINDOWS_SEPARATOR))
|
|
243
|
+
return redact(part);
|
|
244
|
+
const native = body
|
|
245
|
+
.split(WINDOWS_SEPARATOR)
|
|
246
|
+
.map((segment) => segment.replace(/\\(n|.)/gs, (_all, char) => (char === 'n' ? '\n' : char)))
|
|
247
|
+
.join('\\');
|
|
248
|
+
const shown = redact(native).split(/[\\/]/).map(escapeSlash).join(WINDOWS_SEPARATOR);
|
|
249
|
+
return `/^${shown}$/${match?.[2] ?? ''}`;
|
|
250
|
+
}
|
|
215
251
|
function escapeSlash(value) {
|
|
216
252
|
return escapeRegex(value)
|
|
217
253
|
.replaceAll('/', '\\/')
|
package/dist/rerun/run.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type ChildProcess } from 'node:child_process';
|
|
2
2
|
import type { TestRecord } from '../model.js';
|
|
3
3
|
import { type PlannedRun, type RerunPlan } from './plan.js';
|
|
4
|
+
/**
|
|
5
|
+
* Children get their own process group on macOS and Linux only. On Windows,
|
|
6
|
+
* a detached child opens a new console window.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DETACH_CHILDREN: boolean;
|
|
4
9
|
/** Signals that stop a rerun. The child process group is killed before the temp directory is removed. */
|
|
5
10
|
export declare const STOP_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
6
11
|
/** How long to wait after SIGTERM before SIGKILL. */
|
|
@@ -49,8 +54,12 @@ export declare function releaseRun(state: ReleaseState): Promise<void>;
|
|
|
49
54
|
export declare function onStopSignal(signal: NodeJS.Signals, release: () => void | Promise<void>, reraise: (signal: NodeJS.Signals) => void): Promise<void>;
|
|
50
55
|
export declare function killProcessGroup(pid: number, child?: ChildProcess): Promise<void>;
|
|
51
56
|
/**
|
|
52
|
-
* Spawn a
|
|
57
|
+
* Spawn a child with temp env inside `dir`.
|
|
53
58
|
* The caller owns stdout piping and cleanup.
|
|
59
|
+
* On macOS and Linux the child gets its own process group, so cleanup can stop
|
|
60
|
+
* its descendants. On Windows a detached child gets a new console window, and
|
|
61
|
+
* `windowsHide` does not stop the windows its own children open, so the child
|
|
62
|
+
* stays attached and cleanup uses `taskkill /T` instead.
|
|
54
63
|
*/
|
|
55
64
|
export declare function launchRerunChild(input: {
|
|
56
65
|
command: string;
|
|
@@ -79,6 +88,34 @@ export declare function resolveProjectFile(file: string, cwd: string, config?: s
|
|
|
79
88
|
* A missing id is an error. Extra tests are an error. Nothing is merged on failure.
|
|
80
89
|
*/
|
|
81
90
|
export declare function selectRecords(run: PlannedRun, records: readonly TestRecord[], times: number): TestRecord[];
|
|
91
|
+
/**
|
|
92
|
+
* Playwright's own JavaScript entry points, in lookup order. The rerun runs
|
|
93
|
+
* them with the Node that runs this tool. It does not start the
|
|
94
|
+
* `node_modules/.bin/playwright` shim, because that file is a shell script
|
|
95
|
+
* on Windows, and Node refuses to start `playwright.cmd` without a shell.
|
|
96
|
+
* A shell would read `^`, `$`, and `|` in the filters.
|
|
97
|
+
*/
|
|
98
|
+
export declare const PLAYWRIGHT_CLI_PATHS: readonly (readonly string[])[];
|
|
99
|
+
/**
|
|
100
|
+
* Playwright's `cli.js` in `startDir`, then each parent, the same way Node
|
|
101
|
+
* resolves a package. `playwright/cli.js` is checked before
|
|
102
|
+
* `@playwright/test/cli.js` in each folder. Stops at the filesystem root.
|
|
103
|
+
*/
|
|
104
|
+
export declare function findPlaywrightCli(startDir: string): string | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* With `--config`, start at the config folder and walk parents, the same way
|
|
107
|
+
* Node resolves a package. Without `--config`, look only in the working directory.
|
|
108
|
+
*/
|
|
109
|
+
export declare function playwrightCli(cwd: string, config?: string): string;
|
|
110
|
+
/**
|
|
111
|
+
* The launcher a person reads in front of each printed command.
|
|
112
|
+
* `npx playwright test` when Playwright is installed in the working
|
|
113
|
+
* directory, because `npx` finds it there on every OS. Otherwise `node` and
|
|
114
|
+
* the path to Playwright's `cli.js`, relative to the working directory, so a
|
|
115
|
+
* kit that keeps `node_modules` in a subfolder still has a command that runs.
|
|
116
|
+
* The process itself is started with the same Node and `cli.js` either way.
|
|
117
|
+
*/
|
|
118
|
+
export declare function printedLauncher(cwd: string, config?: string): string;
|
|
82
119
|
/**
|
|
83
120
|
* Signal one spawned child, then every descendant.
|
|
84
121
|
* Descendants are collected before the signal. Each one is signaled, and so
|
package/dist/rerun/run.js
CHANGED
|
@@ -6,6 +6,12 @@ import { parsePlaywrightJson } from '../parsers/playwright-json.js';
|
|
|
6
6
|
import { redactHomeText } from '../report-path.js';
|
|
7
7
|
import { commitFor, mergeRerunRecords, tagRerunAttempts } from './merge.js';
|
|
8
8
|
import { exactFileArg, formatPrintedCommand } from './plan.js';
|
|
9
|
+
import { shellQuote } from './quote.js';
|
|
10
|
+
/**
|
|
11
|
+
* Children get their own process group on macOS and Linux only. On Windows,
|
|
12
|
+
* a detached child opens a new console window.
|
|
13
|
+
*/
|
|
14
|
+
export const DETACH_CHILDREN = process.platform !== 'win32';
|
|
9
15
|
/** Signals that stop a rerun. The child process group is killed before the temp directory is removed. */
|
|
10
16
|
export const STOP_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
11
17
|
/** How long to wait after SIGTERM before SIGKILL. */
|
|
@@ -87,6 +93,10 @@ export async function onStopSignal(signal, release, reraise) {
|
|
|
87
93
|
reraise(signal);
|
|
88
94
|
}
|
|
89
95
|
export async function killProcessGroup(pid, child) {
|
|
96
|
+
if (process.platform === 'win32') {
|
|
97
|
+
await taskkillTree(pid);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
90
100
|
const tracked = signalTree(pid, 'SIGTERM', child);
|
|
91
101
|
const gone = await waitUntilGone(pid, SIGNAL_WAIT_MS, child, tracked);
|
|
92
102
|
if (!gone) {
|
|
@@ -95,14 +105,19 @@ export async function killProcessGroup(pid, child) {
|
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
/**
|
|
98
|
-
* Spawn a
|
|
108
|
+
* Spawn a child with temp env inside `dir`.
|
|
99
109
|
* The caller owns stdout piping and cleanup.
|
|
110
|
+
* On macOS and Linux the child gets its own process group, so cleanup can stop
|
|
111
|
+
* its descendants. On Windows a detached child gets a new console window, and
|
|
112
|
+
* `windowsHide` does not stop the windows its own children open, so the child
|
|
113
|
+
* stays attached and cleanup uses `taskkill /T` instead.
|
|
100
114
|
*/
|
|
101
115
|
export function launchRerunChild(input) {
|
|
102
116
|
return spawn(input.command, input.args, {
|
|
103
117
|
cwd: input.cwd,
|
|
104
118
|
env: rerunChildEnv(input.dir),
|
|
105
|
-
detached:
|
|
119
|
+
detached: DETACH_CHILDREN,
|
|
120
|
+
windowsHide: true,
|
|
106
121
|
stdio: ['ignore', input.stdout ?? 'pipe', 'pipe'],
|
|
107
122
|
});
|
|
108
123
|
}
|
|
@@ -150,7 +165,8 @@ export async function executePlan(input) {
|
|
|
150
165
|
break;
|
|
151
166
|
const record = input.records.find((item) => item.id === run.testId) ??
|
|
152
167
|
input.records.find((item) => item.file === run.file && item.project === run.project);
|
|
153
|
-
|
|
168
|
+
const shown = formatPrintedCommand(args, record);
|
|
169
|
+
safePrint(input.print, input.runner ? shown : `${printedLauncher(input.cwd, flagValue(args, '--config'))} ${shown}`);
|
|
154
170
|
if (state.stopping)
|
|
155
171
|
break;
|
|
156
172
|
let result;
|
|
@@ -270,7 +286,7 @@ async function prepareArgs(run, args, cwd, requireFile, rerunDir) {
|
|
|
270
286
|
// A spec under testDir is not in the working directory. Check the local
|
|
271
287
|
// binary before that lookup, so a missing install is reported as itself.
|
|
272
288
|
if (requireFile && !config)
|
|
273
|
-
|
|
289
|
+
playwrightCli(cwd);
|
|
274
290
|
const absolute = await resolveProjectFile(run.file, cwd, config, path.join(rerunDir, 'tmp'));
|
|
275
291
|
if (!absolute) {
|
|
276
292
|
if (requireFile) {
|
|
@@ -302,7 +318,7 @@ export async function resolveProjectFile(file, cwd, config, cacheRoot) {
|
|
|
302
318
|
// The spec is under testDir, which is not the folder where triage ran.
|
|
303
319
|
// Playwright has to load before that root is known. A missing install
|
|
304
320
|
// is not a missing test file.
|
|
305
|
-
if (!
|
|
321
|
+
if (!findPlaywrightCli(configDir))
|
|
306
322
|
throw missingPlaywrightError(configDir);
|
|
307
323
|
const loaded = await loadRootDir(resolvedConfig, cacheRoot);
|
|
308
324
|
if (loaded)
|
|
@@ -359,7 +375,8 @@ function readRootDirFromChild(configFile, cacheDir) {
|
|
|
359
375
|
// parents. That is the config folder, not the triage folder, so a kit
|
|
360
376
|
// that holds node_modules still loads when triage ran in the parent.
|
|
361
377
|
cwd: path.dirname(configFile),
|
|
362
|
-
detached:
|
|
378
|
+
detached: DETACH_CHILDREN,
|
|
379
|
+
windowsHide: true,
|
|
363
380
|
env: {
|
|
364
381
|
...process.env,
|
|
365
382
|
QA_CORE_FLAKY_CONFIG: configFile,
|
|
@@ -478,19 +495,41 @@ function withOutput(args, outputDir) {
|
|
|
478
495
|
copy.splice(at, 0, '--output', outputDir);
|
|
479
496
|
return copy;
|
|
480
497
|
}
|
|
481
|
-
|
|
482
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Playwright's own JavaScript entry points, in lookup order. The rerun runs
|
|
500
|
+
* them with the Node that runs this tool. It does not start the
|
|
501
|
+
* `node_modules/.bin/playwright` shim, because that file is a shell script
|
|
502
|
+
* on Windows, and Node refuses to start `playwright.cmd` without a shell.
|
|
503
|
+
* A shell would read `^`, `$`, and `|` in the filters.
|
|
504
|
+
*/
|
|
505
|
+
export const PLAYWRIGHT_CLI_PATHS = [
|
|
506
|
+
['playwright', 'cli.js'],
|
|
507
|
+
['@playwright/test', 'cli.js'],
|
|
508
|
+
];
|
|
509
|
+
function cliInFolder(dir) {
|
|
510
|
+
for (const parts of PLAYWRIGHT_CLI_PATHS) {
|
|
511
|
+
const file = path.join(dir, 'node_modules', ...parts);
|
|
512
|
+
try {
|
|
513
|
+
if (fs.statSync(file).isFile())
|
|
514
|
+
return file;
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// Not installed in this folder.
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return undefined;
|
|
483
521
|
}
|
|
484
522
|
/**
|
|
485
|
-
* `
|
|
486
|
-
*
|
|
523
|
+
* Playwright's `cli.js` in `startDir`, then each parent, the same way Node
|
|
524
|
+
* resolves a package. `playwright/cli.js` is checked before
|
|
525
|
+
* `@playwright/test/cli.js` in each folder. Stops at the filesystem root.
|
|
487
526
|
*/
|
|
488
|
-
function
|
|
527
|
+
export function findPlaywrightCli(startDir) {
|
|
489
528
|
let dir = path.resolve(startDir);
|
|
490
529
|
for (;;) {
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
493
|
-
return
|
|
530
|
+
const cli = cliInFolder(dir);
|
|
531
|
+
if (cli)
|
|
532
|
+
return cli;
|
|
494
533
|
const parent = path.dirname(dir);
|
|
495
534
|
if (parent === dir)
|
|
496
535
|
return undefined;
|
|
@@ -498,38 +537,61 @@ function findPlaywrightBin(startDir) {
|
|
|
498
537
|
}
|
|
499
538
|
}
|
|
500
539
|
function missingPlaywrightError(start) {
|
|
501
|
-
const
|
|
502
|
-
return new Error(redactHomeText(`Local Playwright was not found
|
|
540
|
+
const modules = path.join(start, 'node_modules');
|
|
541
|
+
return new Error(redactHomeText(`Local Playwright was not found in ${modules}. The rerun looks for playwright/cli.js and @playwright/test/cli.js. Install @playwright/test in this project before rerun.`));
|
|
503
542
|
}
|
|
504
543
|
/**
|
|
505
544
|
* With `--config`, start at the config folder and walk parents, the same way
|
|
506
545
|
* Node resolves a package. Without `--config`, look only in the working directory.
|
|
507
546
|
*/
|
|
508
|
-
function
|
|
547
|
+
export function playwrightCli(cwd, config) {
|
|
509
548
|
if (!config) {
|
|
510
549
|
const start = path.resolve(cwd);
|
|
511
|
-
const
|
|
512
|
-
if (!
|
|
550
|
+
const cli = cliInFolder(start);
|
|
551
|
+
if (!cli)
|
|
513
552
|
throw missingPlaywrightError(start);
|
|
514
|
-
return
|
|
553
|
+
return cli;
|
|
515
554
|
}
|
|
516
555
|
const start = path.dirname(path.resolve(cwd, config));
|
|
517
|
-
const
|
|
518
|
-
if (!
|
|
556
|
+
const cli = findPlaywrightCli(start);
|
|
557
|
+
if (!cli)
|
|
519
558
|
throw missingPlaywrightError(start);
|
|
520
|
-
return
|
|
559
|
+
return cli;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The launcher a person reads in front of each printed command.
|
|
563
|
+
* `npx playwright test` when Playwright is installed in the working
|
|
564
|
+
* directory, because `npx` finds it there on every OS. Otherwise `node` and
|
|
565
|
+
* the path to Playwright's `cli.js`, relative to the working directory, so a
|
|
566
|
+
* kit that keeps `node_modules` in a subfolder still has a command that runs.
|
|
567
|
+
* The process itself is started with the same Node and `cli.js` either way.
|
|
568
|
+
*/
|
|
569
|
+
export function printedLauncher(cwd, config) {
|
|
570
|
+
let cli;
|
|
571
|
+
try {
|
|
572
|
+
cli = playwrightCli(cwd, config);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return 'npx playwright test';
|
|
576
|
+
}
|
|
577
|
+
const local = path.join(path.resolve(cwd), 'node_modules') + path.sep;
|
|
578
|
+
if (cli.startsWith(local))
|
|
579
|
+
return 'npx playwright test';
|
|
580
|
+
const relative = path.relative(path.resolve(cwd), cli).split(path.sep).join('/');
|
|
581
|
+
return `node ${shellQuote(redactHomeText(relative))} test`;
|
|
521
582
|
}
|
|
522
583
|
function defaultRunner(args, cwd, stdoutFile, setPid, dir, state) {
|
|
523
584
|
return new Promise((resolve, reject) => {
|
|
524
|
-
let
|
|
585
|
+
let cli;
|
|
525
586
|
try {
|
|
526
|
-
|
|
587
|
+
cli = playwrightCli(cwd, flagValue(args, '--config'));
|
|
527
588
|
}
|
|
528
589
|
catch (error) {
|
|
529
590
|
reject(error instanceof Error ? error : new Error('Local Playwright was not found.'));
|
|
530
591
|
return;
|
|
531
592
|
}
|
|
532
|
-
|
|
593
|
+
// Node runs Playwright's cli.js directly on every OS. No shell and no shim.
|
|
594
|
+
const child = launchRerunChild({ command: process.execPath, args: [cli, 'test', ...args], cwd, dir });
|
|
533
595
|
state.child = child;
|
|
534
596
|
if (child.pid !== undefined)
|
|
535
597
|
setPid(child.pid);
|
|
@@ -586,7 +648,7 @@ export async function stopChildTree(child) {
|
|
|
586
648
|
if (pid === undefined || pid <= 1)
|
|
587
649
|
return;
|
|
588
650
|
if (process.platform === 'win32') {
|
|
589
|
-
|
|
651
|
+
await taskkillTree(pid);
|
|
590
652
|
return;
|
|
591
653
|
}
|
|
592
654
|
let tracked = signalTree(pid, 'SIGKILL', child);
|
|
@@ -596,6 +658,24 @@ export async function stopChildTree(child) {
|
|
|
596
658
|
await waitUntilGone(pid, SIGNAL_WAIT_MS, child, tracked);
|
|
597
659
|
}
|
|
598
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Stop a process and every descendant on Windows, and wait for taskkill to
|
|
663
|
+
* finish so the temp folder is no longer in use when it is removed.
|
|
664
|
+
*/
|
|
665
|
+
function taskkillTree(pid) {
|
|
666
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
667
|
+
return Promise.resolve();
|
|
668
|
+
return new Promise((resolve) => {
|
|
669
|
+
try {
|
|
670
|
+
const killer = spawn('taskkill', ['/T', '/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
|
|
671
|
+
killer.once('error', () => resolve());
|
|
672
|
+
killer.once('close', () => resolve());
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
resolve();
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
599
679
|
function signalPid(pid, signal) {
|
|
600
680
|
if (!Number.isInteger(pid) || pid <= 1)
|
|
601
681
|
return;
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qa-core-flaky",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Triage flaky Playwright tests: real bug or flaky, likely cause, and a suggested fix to review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"author":
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "SprintSynergy",
|
|
9
|
+
"url": "https://sprintsynergy.com"
|
|
10
|
+
},
|
|
11
|
+
"contributors": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Muhammad Usman",
|
|
14
|
+
"url": "https://linkedin.com/in/sdet-muhammadusman"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
8
17
|
"repository": {
|
|
9
18
|
"type": "git",
|
|
10
19
|
"url": "git+https://github.com/sprintsynergyhq/qa-core-flaky.git"
|