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 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.0. The commands are `parse`, `causes`, `triage`, and `rerun`.
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
- detached: true,
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);
@@ -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
- /** Exact path, anchored, with an optional `:line`. Slashes are escaped inside `/.../`. */
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
@@ -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
- /** Exact path, anchored, with an optional `:line`. Slashes are escaped inside `/.../`. */
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 wrapped = `/^${escapeSlash(filePath)}$/`;
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 shown = [...prefix, ...kept].map((part) => shellQuote(redact(part))).join(' ');
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('/', '\\/')
@@ -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 detached child with temp env inside `dir`.
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 detached child with temp env inside `dir`.
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: true,
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
- safePrint(input.print, formatPrintedCommand(args, record, input.runner ? [] : launcherPrefix(input.cwd, flagValue(args, '--config'))));
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
- playwrightBin(cwd);
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 (!findPlaywrightBin(configDir))
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: true,
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
- function launcherPrefix(cwd, config) {
482
- return [playwrightBin(cwd, config), 'test'];
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
- * `node_modules/.bin/playwright` in `startDir`, then each parent, the same
486
- * way Node resolves a package. Stops at the filesystem root.
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 findPlaywrightBin(startDir) {
527
+ export function findPlaywrightCli(startDir) {
489
528
  let dir = path.resolve(startDir);
490
529
  for (;;) {
491
- const bin = path.join(dir, 'node_modules', '.bin', 'playwright');
492
- if (fs.existsSync(bin))
493
- return bin;
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 bin = path.join(start, 'node_modules', '.bin', 'playwright');
502
- return new Error(redactHomeText(`Local Playwright was not found at ${bin}. Install playwright in this project before rerun.`));
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 playwrightBin(cwd, config) {
547
+ export function playwrightCli(cwd, config) {
509
548
  if (!config) {
510
549
  const start = path.resolve(cwd);
511
- const bin = path.join(start, 'node_modules', '.bin', 'playwright');
512
- if (!fs.existsSync(bin))
550
+ const cli = cliInFolder(start);
551
+ if (!cli)
513
552
  throw missingPlaywrightError(start);
514
- return bin;
553
+ return cli;
515
554
  }
516
555
  const start = path.dirname(path.resolve(cwd, config));
517
- const bin = findPlaywrightBin(start);
518
- if (!bin)
556
+ const cli = findPlaywrightCli(start);
557
+ if (!cli)
519
558
  throw missingPlaywrightError(start);
520
- return bin;
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 bin;
585
+ let cli;
525
586
  try {
526
- bin = playwrightBin(cwd, flagValue(args, '--config'));
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
- const child = launchRerunChild({ command: bin, args: ['test', ...args], cwd, dir });
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
- spawn('taskkill', ['/T', '/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
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.0",
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": "SprintSynergy",
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"