@piwitests/reporter 0.45.0 → 0.46.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/dist/cli/index.js +530 -249
- package/dist/global-setup-module.js +132 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +693 -104
- package/dist/internal/capture/capture-fixtures.js +187 -77
- package/dist/internal/capture/locator-healing.js +9 -7
- package/dist/internal/capture/pick-on-failure.js +130 -37
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -0
- package/templates/skills/investigate-failure/SKILL.md +5 -1
- package/templates/skills/write-the-missing-test/SKILL.md +9 -2
package/dist/cli/index.js
CHANGED
|
@@ -261,7 +261,7 @@ function checkAiTree(root, opts = {}) {
|
|
|
261
261
|
severity: "error",
|
|
262
262
|
kind: "non-canonical",
|
|
263
263
|
file: rel(found.file),
|
|
264
|
-
message: "file is not in canonical form \u2014
|
|
264
|
+
message: "file is not in canonical form \u2014 re-author it with `piwi ai resolve --update-ai`"
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
if (!sourceCache.has(found.specFile)) sourceCache.set(found.specFile, readTextOr(found.specFile, null));
|
|
@@ -428,7 +428,9 @@ async function runAi(argv, env = process.env) {
|
|
|
428
428
|
case "resolve":
|
|
429
429
|
return runResolve(rest, env);
|
|
430
430
|
case "prune":
|
|
431
|
-
console.error(
|
|
431
|
+
console.error(
|
|
432
|
+
"piwi ai prune: not available in this build yet. `piwi ai check` lists orphaned entries: delete their files by hand, and re-author a non-canonical one with `piwi ai resolve --update-ai`."
|
|
433
|
+
);
|
|
432
434
|
return EXIT_ERROR;
|
|
433
435
|
default:
|
|
434
436
|
console.error(`piwi ai: unknown command "${sub}"
|
|
@@ -439,10 +441,164 @@ async function runAi(argv, env = process.env) {
|
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
// src/cli/bug.ts
|
|
442
|
-
var
|
|
443
|
-
var
|
|
444
|
+
var fs4 = __toESM(require("fs"));
|
|
445
|
+
var path7 = __toESM(require("path"));
|
|
444
446
|
var import_node_child_process2 = require("child_process");
|
|
445
447
|
var import_node_module = require("module");
|
|
448
|
+
|
|
449
|
+
// src/internal/config/desktop.ts
|
|
450
|
+
var fs3 = __toESM(require("fs"));
|
|
451
|
+
var os = __toESM(require("os"));
|
|
452
|
+
var path6 = __toESM(require("path"));
|
|
453
|
+
function defaultDesktopConfigPath() {
|
|
454
|
+
return path6.join(os.homedir(), ".piwi", "desktop.json");
|
|
455
|
+
}
|
|
456
|
+
function readDesktopConfig(filePath) {
|
|
457
|
+
try {
|
|
458
|
+
const parsed = JSON.parse(fs3.readFileSync(filePath, "utf8"));
|
|
459
|
+
const url = parsed?.url;
|
|
460
|
+
const token = parsed?.token;
|
|
461
|
+
if (typeof url !== "string" || !url) return null;
|
|
462
|
+
if (typeof token !== "string" || !token) return null;
|
|
463
|
+
return { url, token };
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/internal/config/env.ts
|
|
470
|
+
var DEFAULTS = {
|
|
471
|
+
projectName: "default-project",
|
|
472
|
+
uploadTraces: true,
|
|
473
|
+
uploadReport: true,
|
|
474
|
+
liveFileUploads: true,
|
|
475
|
+
collectScmInfo: true,
|
|
476
|
+
collectCiInfo: true,
|
|
477
|
+
collectPerformanceMetrics: true,
|
|
478
|
+
captureLocators: true,
|
|
479
|
+
capturePageState: true,
|
|
480
|
+
// Off by default: the page inventory reads control and link names from every
|
|
481
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
482
|
+
capturePageInventory: false,
|
|
483
|
+
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
484
|
+
captureCodeReach: false,
|
|
485
|
+
captureServerTraces: true,
|
|
486
|
+
captureResources: true,
|
|
487
|
+
leakCheck: "report",
|
|
488
|
+
sampleAriaOnPass: true,
|
|
489
|
+
uploadManifest: true,
|
|
490
|
+
defaultCapture: true,
|
|
491
|
+
checkBaseUrl: false,
|
|
492
|
+
streaming: true,
|
|
493
|
+
streamingBatchSize: 5,
|
|
494
|
+
streamingBatchDelay: 2e3,
|
|
495
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
496
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
497
|
+
failOnFlakyTests: false,
|
|
498
|
+
username: null,
|
|
499
|
+
password: null,
|
|
500
|
+
apiKey: null,
|
|
501
|
+
verbose: false
|
|
502
|
+
};
|
|
503
|
+
var PIWI_ENV_KEYS = {
|
|
504
|
+
serverUrl: "PIWI_DASHBOARD_URL",
|
|
505
|
+
projectName: "PIWI_PROJECT_NAME",
|
|
506
|
+
verbose: "PIWI_VERBOSE",
|
|
507
|
+
apiKey: "PIWI_API_KEY",
|
|
508
|
+
username: "PIWI_USERNAME",
|
|
509
|
+
password: "PIWI_PASSWORD",
|
|
510
|
+
environment: "PIWI_ENVIRONMENT",
|
|
511
|
+
label: "PIWI_LABEL",
|
|
512
|
+
runLabel: "PIWI_RUN_LABEL",
|
|
513
|
+
keep: "PIWI_KEEP",
|
|
514
|
+
streaming: "PIWI_STREAMING",
|
|
515
|
+
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
516
|
+
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
517
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
518
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
519
|
+
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
520
|
+
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
521
|
+
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
522
|
+
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
523
|
+
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
524
|
+
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
525
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
526
|
+
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
527
|
+
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
528
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
529
|
+
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
530
|
+
leakCheck: "PIWI_LEAK_CHECK",
|
|
531
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
532
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
533
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
534
|
+
checkBaseUrl: "PIWI_CHECK_BASE_URL",
|
|
535
|
+
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
536
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
537
|
+
outputFile: "PIWI_OUTPUT_FILE",
|
|
538
|
+
aiMode: "PIWI_AI",
|
|
539
|
+
aiDir: "PIWI_AI_DIR",
|
|
540
|
+
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
541
|
+
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
542
|
+
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
543
|
+
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
544
|
+
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
545
|
+
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
546
|
+
};
|
|
547
|
+
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
548
|
+
var PIWI_ORIGIN_ENV = {
|
|
549
|
+
kind: "PIWI_ORIGIN",
|
|
550
|
+
ref: "PIWI_ORIGIN_REF"
|
|
551
|
+
};
|
|
552
|
+
function originEnv(kind, ref) {
|
|
553
|
+
return ref === void 0 ? { [PIWI_ORIGIN_ENV.kind]: kind } : { [PIWI_ORIGIN_ENV.kind]: kind, [PIWI_ORIGIN_ENV.ref]: String(ref) };
|
|
554
|
+
}
|
|
555
|
+
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
556
|
+
var PIWI_PROBE_ENV = {
|
|
557
|
+
flag: "PIWI_PROBE",
|
|
558
|
+
plan: "PIWI_PROBE_PLAN",
|
|
559
|
+
results: "PIWI_PROBE_RESULTS",
|
|
560
|
+
secret: "PIWI_PROBE_SECRET"
|
|
561
|
+
};
|
|
562
|
+
var PIWI_FLAKE_ENV = {
|
|
563
|
+
plan: "PIWI_FLAKE_PLAN",
|
|
564
|
+
results: "PIWI_FLAKE_RESULTS"
|
|
565
|
+
};
|
|
566
|
+
var ENV_FALLBACK_SPECS = [
|
|
567
|
+
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
568
|
+
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
569
|
+
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
570
|
+
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
571
|
+
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
572
|
+
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
573
|
+
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
574
|
+
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
575
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
576
|
+
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
577
|
+
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
578
|
+
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
579
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
580
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
581
|
+
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
582
|
+
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
583
|
+
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
584
|
+
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
585
|
+
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
586
|
+
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
587
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
588
|
+
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
589
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
590
|
+
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
591
|
+
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
592
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
593
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
594
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
595
|
+
{ option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
|
|
596
|
+
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
597
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
598
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
// src/cli/bug.ts
|
|
446
602
|
var EXIT_OK2 = 0;
|
|
447
603
|
var EXIT_RUN_FAILED = 1;
|
|
448
604
|
var EXIT_ERROR2 = 2;
|
|
@@ -535,7 +691,7 @@ function repositoryRoot(cwd) {
|
|
|
535
691
|
encoding: "utf-8",
|
|
536
692
|
stdio: ["ignore", "pipe", "ignore"]
|
|
537
693
|
}).trim();
|
|
538
|
-
return out ?
|
|
694
|
+
return out ? path7.resolve(out) : null;
|
|
539
695
|
} catch {
|
|
540
696
|
return null;
|
|
541
697
|
}
|
|
@@ -543,23 +699,23 @@ function repositoryRoot(cwd) {
|
|
|
543
699
|
function realFolder(dir) {
|
|
544
700
|
const rest = [];
|
|
545
701
|
let current = dir;
|
|
546
|
-
while (!
|
|
547
|
-
rest.unshift(
|
|
548
|
-
current =
|
|
702
|
+
while (!fs4.existsSync(current) && path7.dirname(current) !== current) {
|
|
703
|
+
rest.unshift(path7.basename(current));
|
|
704
|
+
current = path7.dirname(current);
|
|
549
705
|
}
|
|
550
|
-
return
|
|
706
|
+
return path7.join(fs4.realpathSync(current), ...rest);
|
|
551
707
|
}
|
|
552
708
|
function folderInRepository(root, dir) {
|
|
553
|
-
const relative6 =
|
|
554
|
-
if (relative6.startsWith("..") ||
|
|
555
|
-
return relative6.split(
|
|
709
|
+
const relative6 = path7.relative(realFolder(root), realFolder(dir));
|
|
710
|
+
if (relative6.startsWith("..") || path7.isAbsolute(relative6)) return void 0;
|
|
711
|
+
return relative6.split(path7.sep).join("/");
|
|
556
712
|
}
|
|
557
713
|
function fileFilter(file) {
|
|
558
714
|
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
559
|
-
return `${escape(
|
|
715
|
+
return `${escape(path7.basename(path7.dirname(file)))}[\\\\/]${escape(path7.basename(file))}$`;
|
|
560
716
|
}
|
|
561
717
|
function resolvePlaywrightCli(cwd) {
|
|
562
|
-
const require2 = (0, import_node_module.createRequire)(
|
|
718
|
+
const require2 = (0, import_node_module.createRequire)(path7.join(cwd, "noop.js"));
|
|
563
719
|
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
564
720
|
try {
|
|
565
721
|
return require2.resolve(id);
|
|
@@ -568,12 +724,14 @@ function resolvePlaywrightCli(cwd) {
|
|
|
568
724
|
}
|
|
569
725
|
return null;
|
|
570
726
|
}
|
|
571
|
-
function runPlaywright(file, cwd) {
|
|
727
|
+
function runPlaywright(file, cwd, id, env) {
|
|
572
728
|
const cli = resolvePlaywrightCli(cwd);
|
|
573
729
|
const filter = fileFilter(file);
|
|
574
|
-
const
|
|
730
|
+
const childEnv = { ...env, ...originEnv("bug", id) };
|
|
731
|
+
const child = cli ? (0, import_node_child_process2.spawn)(process.execPath, [cli, "test", filter], { stdio: "inherit", cwd, env: childEnv }) : (0, import_node_child_process2.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", filter], {
|
|
575
732
|
stdio: "inherit",
|
|
576
|
-
cwd
|
|
733
|
+
cwd,
|
|
734
|
+
env: childEnv
|
|
577
735
|
});
|
|
578
736
|
return new Promise((resolve8) => {
|
|
579
737
|
child.on("error", (err) => {
|
|
@@ -598,10 +756,10 @@ ${USAGE2}`);
|
|
|
598
756
|
return EXIT_ERROR2;
|
|
599
757
|
}
|
|
600
758
|
const root = args.write ? repositoryRoot(cwd) : null;
|
|
601
|
-
const outFile = args.out !== null ?
|
|
759
|
+
const outFile = args.out !== null ? path7.resolve(cwd, args.out) : null;
|
|
602
760
|
let spec;
|
|
603
761
|
try {
|
|
604
|
-
spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root,
|
|
762
|
+
spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path7.dirname(outFile)) : void 0);
|
|
605
763
|
} catch (e) {
|
|
606
764
|
console.error(`piwi bug: ${e.message}`);
|
|
607
765
|
return EXIT_ERROR2;
|
|
@@ -616,19 +774,19 @@ ${USAGE2}`);
|
|
|
616
774
|
if (!outFile && !root) {
|
|
617
775
|
console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
|
|
618
776
|
}
|
|
619
|
-
const file = outFile ??
|
|
777
|
+
const file = outFile ?? path7.resolve(root ?? cwd, spec.path);
|
|
620
778
|
try {
|
|
621
|
-
|
|
622
|
-
|
|
779
|
+
fs4.mkdirSync(path7.dirname(file), { recursive: true });
|
|
780
|
+
fs4.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
|
|
623
781
|
} catch (e) {
|
|
624
782
|
const exists = e.code === "EEXIST";
|
|
625
783
|
console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
|
|
626
784
|
return EXIT_ERROR2;
|
|
627
785
|
}
|
|
628
|
-
const shown =
|
|
786
|
+
const shown = path7.relative(cwd, file) || file;
|
|
629
787
|
console.error(`piwi bug: wrote ${shown}`);
|
|
630
788
|
if (!args.run) return EXIT_OK2;
|
|
631
|
-
const code = await runPlaywright(file, cwd);
|
|
789
|
+
const code = await runPlaywright(file, cwd, args.id, env);
|
|
632
790
|
if (code === EXIT_ERROR2 && !resolvePlaywrightCli(cwd)) return EXIT_ERROR2;
|
|
633
791
|
if (code === 0) {
|
|
634
792
|
console.error(
|
|
@@ -643,8 +801,8 @@ ${USAGE2}`);
|
|
|
643
801
|
}
|
|
644
802
|
|
|
645
803
|
// src/cli/codegen.ts
|
|
646
|
-
var
|
|
647
|
-
var
|
|
804
|
+
var fs5 = __toESM(require("fs"));
|
|
805
|
+
var path8 = __toESM(require("path"));
|
|
648
806
|
|
|
649
807
|
// ../core/src/recording.ts
|
|
650
808
|
var ASSERTION_MATCHERS = [
|
|
@@ -717,13 +875,20 @@ function extractArgs(entry, window, pairs) {
|
|
|
717
875
|
}
|
|
718
876
|
return args;
|
|
719
877
|
}
|
|
878
|
+
function sameElement(pattern, recorded) {
|
|
879
|
+
if (pattern.testId || !pattern.name) return true;
|
|
880
|
+
const haystack = `${recorded?.accessibleName ?? ""} ${recorded?.text ?? ""}`.toLowerCase().trim();
|
|
881
|
+
const needle = pattern.name.toLowerCase();
|
|
882
|
+
return haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
883
|
+
}
|
|
720
884
|
function contiguousAlignment(steps, startIndex, pattern) {
|
|
721
885
|
if (startIndex + pattern.length > steps.length) return null;
|
|
722
886
|
const pairs = [];
|
|
723
887
|
let total = 0;
|
|
724
888
|
for (let j = 0; j < pattern.length; j++) {
|
|
725
|
-
const
|
|
726
|
-
|
|
889
|
+
const recorded = steps[startIndex + j];
|
|
890
|
+
const score = stepPairScore(pattern[j], recorded);
|
|
891
|
+
if (score <= 0 || !sameElement(pattern[j].target, recorded.target)) return null;
|
|
727
892
|
total += score;
|
|
728
893
|
pairs.push([j, j]);
|
|
729
894
|
}
|
|
@@ -1549,6 +1714,41 @@ function pageUrlPattern(url) {
|
|
|
1549
1714
|
function commentText(s) {
|
|
1550
1715
|
return s.replace(/[\n\r\u2028\u2029]+/g, " ").trim();
|
|
1551
1716
|
}
|
|
1717
|
+
var ENV_NAME_MAX = 40;
|
|
1718
|
+
function envPrefixOf(options) {
|
|
1719
|
+
const prefix = options.envPrefix;
|
|
1720
|
+
return typeof prefix === "string" && ENV_PREFIX.test(prefix) ? prefix : "E2E_";
|
|
1721
|
+
}
|
|
1722
|
+
var ENV_PREFIX = /^[A-Z][A-Z0-9_]*$/;
|
|
1723
|
+
function envNameOf(target, prefix) {
|
|
1724
|
+
for (const source of [target?.accessibleName, target?.testId, target?.text]) {
|
|
1725
|
+
const name = (source ?? "").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "").toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, ENV_NAME_MAX).replace(/_+$/, "");
|
|
1726
|
+
if (name) return `${prefix}${name}`;
|
|
1727
|
+
}
|
|
1728
|
+
return `${prefix}VALUE`;
|
|
1729
|
+
}
|
|
1730
|
+
function uniqueEnvName(name, taken) {
|
|
1731
|
+
if (!taken.includes(name)) return name;
|
|
1732
|
+
let n = 2;
|
|
1733
|
+
while (taken.includes(`${name}_${n}`)) n++;
|
|
1734
|
+
return `${name}_${n}`;
|
|
1735
|
+
}
|
|
1736
|
+
function envVarFor(step, index, ctx) {
|
|
1737
|
+
const envVar = uniqueEnvName(envNameOf(step.target, envPrefixOf(ctx.options)), ctx.envNames);
|
|
1738
|
+
ctx.envNames.push(envVar);
|
|
1739
|
+
if (step.redacted) {
|
|
1740
|
+
ctx.warnings.push({
|
|
1741
|
+
step: index,
|
|
1742
|
+
code: "redacted-value",
|
|
1743
|
+
detail: envVar,
|
|
1744
|
+
message: `A password was typed here; the spec reads it from ${envVar}.`
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return envVar;
|
|
1748
|
+
}
|
|
1749
|
+
function readsEnv(step, ctx) {
|
|
1750
|
+
return step.action === "fill" && (step.redacted || ctx.options.values === "env");
|
|
1751
|
+
}
|
|
1552
1752
|
function urlForCode(url, ctx) {
|
|
1553
1753
|
const isPath = url.startsWith("/");
|
|
1554
1754
|
if (ctx.options.urls === "relative") {
|
|
@@ -1566,7 +1766,7 @@ function locatorForStep(step, index, ctx) {
|
|
|
1566
1766
|
const chosen = chooseLocator(step.target, ctx.options);
|
|
1567
1767
|
if (!chosen) {
|
|
1568
1768
|
ctx.warnings.push({ step: index, code: "no-locator", message: "No locator was captured for this element." });
|
|
1569
|
-
return
|
|
1769
|
+
return `${ctx.page}.locator(${quote2("/* no locator captured */")})`;
|
|
1570
1770
|
}
|
|
1571
1771
|
if (chosen.level === "brittle") {
|
|
1572
1772
|
ctx.warnings.push({
|
|
@@ -1576,7 +1776,7 @@ function locatorForStep(step, index, ctx) {
|
|
|
1576
1776
|
message: `The best locator captured for this element is brittle: ${chosen.text}`
|
|
1577
1777
|
});
|
|
1578
1778
|
}
|
|
1579
|
-
return
|
|
1779
|
+
return `${ctx.page}.${chosen.text}`;
|
|
1580
1780
|
}
|
|
1581
1781
|
function renderAssertStep(step, index, ctx) {
|
|
1582
1782
|
const assertion = step.action === "assertVisible" ? null : step.assertion;
|
|
@@ -1601,7 +1801,7 @@ function renderAssertStep(step, index, ctx) {
|
|
|
1601
1801
|
}
|
|
1602
1802
|
arg = quote2(matcher === "toHaveURL" ? urlForCode(assertion.expected, ctx) : assertion.expected);
|
|
1603
1803
|
}
|
|
1604
|
-
const subject = matcher === "toHaveURL" ?
|
|
1804
|
+
const subject = matcher === "toHaveURL" ? ctx.page : locatorForStep(step, index, ctx);
|
|
1605
1805
|
let line = ` await expect(${subject})${not}.${matcher}(${arg});`;
|
|
1606
1806
|
if (assertion?.actual != null && assertion.actual !== assertion.expected) {
|
|
1607
1807
|
line += ` // recorded: ${quote2(assertion.actual)}`;
|
|
@@ -1613,9 +1813,12 @@ function fileNames(value) {
|
|
|
1613
1813
|
return (value ?? "").split("\n").map((name) => name.trim().replace(/^.*[\\/]/, "")).filter(Boolean);
|
|
1614
1814
|
}
|
|
1615
1815
|
function renderRawStep(step, index, ctx) {
|
|
1616
|
-
|
|
1816
|
+
const { page } = ctx;
|
|
1817
|
+
if (step.action === "goto") return [` await ${page}.goto(${quote2(urlForCode(step.value ?? step.pageUrl, ctx))});`];
|
|
1617
1818
|
if (step.action === "assert" || step.action === "assertVisible") return renderAssertStep(step, index, ctx);
|
|
1618
|
-
if (step.action === "press" && !step.target)
|
|
1819
|
+
if (step.action === "press" && !step.target) {
|
|
1820
|
+
return [` await ${page}.keyboard.press(${quote2(step.value ?? "Enter")});`];
|
|
1821
|
+
}
|
|
1619
1822
|
const loc = locatorForStep(step, index, ctx);
|
|
1620
1823
|
switch (step.action) {
|
|
1621
1824
|
case "click":
|
|
@@ -1641,19 +1844,7 @@ function renderRawStep(step, index, ctx) {
|
|
|
1641
1844
|
case "hover":
|
|
1642
1845
|
return [` await ${loc}.hover();`];
|
|
1643
1846
|
case "fill": {
|
|
1644
|
-
if (step
|
|
1645
|
-
const envVar = `PIWI_TEST_VALUE_${index}`;
|
|
1646
|
-
ctx.envNames.push(envVar);
|
|
1647
|
-
if (step.redacted) {
|
|
1648
|
-
ctx.warnings.push({
|
|
1649
|
-
step: index,
|
|
1650
|
-
code: "redacted-value",
|
|
1651
|
-
detail: envVar,
|
|
1652
|
-
message: `A password was typed here; the spec reads it from ${envVar}.`
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
return [` await ${loc}.fill(process.env.${envVar} ?? '');`];
|
|
1656
|
-
}
|
|
1847
|
+
if (readsEnv(step, ctx)) return [` await ${loc}.fill(process.env.${envVarFor(step, index, ctx)} ?? '');`];
|
|
1657
1848
|
return [` await ${loc}.fill(${quote2(step.value ?? "")});`];
|
|
1658
1849
|
}
|
|
1659
1850
|
case "check":
|
|
@@ -1674,16 +1865,102 @@ function callIdentifiersAreSafe(entry) {
|
|
|
1674
1865
|
if (entry.importName != null && !IDENTIFIER_RE.test(entry.importName)) return false;
|
|
1675
1866
|
return true;
|
|
1676
1867
|
}
|
|
1677
|
-
|
|
1868
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
1869
|
+
"await",
|
|
1870
|
+
"break",
|
|
1871
|
+
"case",
|
|
1872
|
+
"catch",
|
|
1873
|
+
"class",
|
|
1874
|
+
"const",
|
|
1875
|
+
"continue",
|
|
1876
|
+
"debugger",
|
|
1877
|
+
"default",
|
|
1878
|
+
"delete",
|
|
1879
|
+
"do",
|
|
1880
|
+
"else",
|
|
1881
|
+
"enum",
|
|
1882
|
+
"export",
|
|
1883
|
+
"extends",
|
|
1884
|
+
"false",
|
|
1885
|
+
"finally",
|
|
1886
|
+
"for",
|
|
1887
|
+
"function",
|
|
1888
|
+
"if",
|
|
1889
|
+
"implements",
|
|
1890
|
+
"import",
|
|
1891
|
+
"in",
|
|
1892
|
+
"instanceof",
|
|
1893
|
+
"interface",
|
|
1894
|
+
"let",
|
|
1895
|
+
"new",
|
|
1896
|
+
"null",
|
|
1897
|
+
"package",
|
|
1898
|
+
"private",
|
|
1899
|
+
"protected",
|
|
1900
|
+
"public",
|
|
1901
|
+
"return",
|
|
1902
|
+
"static",
|
|
1903
|
+
"super",
|
|
1904
|
+
"switch",
|
|
1905
|
+
"throw",
|
|
1906
|
+
"true",
|
|
1907
|
+
"try",
|
|
1908
|
+
"typeof",
|
|
1909
|
+
"var",
|
|
1910
|
+
"void",
|
|
1911
|
+
"while",
|
|
1912
|
+
"with",
|
|
1913
|
+
"yield"
|
|
1914
|
+
]);
|
|
1915
|
+
function isPageExpression(text) {
|
|
1916
|
+
if (typeof text !== "string") return false;
|
|
1917
|
+
const segments = text.split(".");
|
|
1918
|
+
return segments.every((segment) => IDENTIFIER_RE.test(segment)) && (segments[0] === "this" || !RESERVED_WORDS.has(segments[0]));
|
|
1919
|
+
}
|
|
1920
|
+
function pageExpression(options) {
|
|
1921
|
+
const page = options.page ?? "page";
|
|
1922
|
+
if (!isPageExpression(page)) {
|
|
1923
|
+
throw new RangeError(
|
|
1924
|
+
`Not a page expression (an identifier or a member chain such as app.page): ${JSON.stringify(page)}`
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
if (options.format !== "body" && page.split(".")[0] === "this") {
|
|
1928
|
+
throw new RangeError(
|
|
1929
|
+
`The ${options.format ?? "file"} format takes the test's parameter from the page expression, which starts with this: ${JSON.stringify(page)}`
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
return page;
|
|
1933
|
+
}
|
|
1934
|
+
function objectArgExpr(param, match, envArgs) {
|
|
1678
1935
|
const entries = (param.fields ?? []).map((field) => {
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1936
|
+
const key = `${param.name}.${field}`;
|
|
1937
|
+
const env = envArgs.get(key);
|
|
1938
|
+
const raw = match.args[key];
|
|
1939
|
+
if (env == null && raw == null) return null;
|
|
1940
|
+
return `${IDENTIFIER_RE.test(field) ? field : quote2(field)}: ${env ?? quote2(raw)}`;
|
|
1681
1941
|
}).filter((pair) => pair != null);
|
|
1682
1942
|
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
1683
1943
|
}
|
|
1684
|
-
function
|
|
1944
|
+
function envArgsOf(match, steps, ctx) {
|
|
1945
|
+
const envArgs = /* @__PURE__ */ new Map();
|
|
1946
|
+
const params = new Map(match.entry.params.map((p) => [p.name, p]));
|
|
1947
|
+
const sources = match.entry.paramSources.filter((source) => source.from === "value").sort((a, b) => a.stepIndex - b.stepIndex);
|
|
1948
|
+
for (const source of sources) {
|
|
1949
|
+
const type = params.get(source.param)?.type;
|
|
1950
|
+
if (source.path ? type !== "object" : type !== "string") continue;
|
|
1951
|
+
const index = match.matchedIndices[source.stepIndex];
|
|
1952
|
+
const step = index == null ? void 0 : steps[index];
|
|
1953
|
+
if (index == null || !step || !readsEnv(step, ctx)) continue;
|
|
1954
|
+
const key = source.path ? `${source.param}.${source.path}` : source.param;
|
|
1955
|
+
if (!envArgs.has(key)) envArgs.set(key, `process.env.${envVarFor(step, index, ctx)} ?? ''`);
|
|
1956
|
+
}
|
|
1957
|
+
return envArgs;
|
|
1958
|
+
}
|
|
1959
|
+
function argExpr(entry, match, envArgs) {
|
|
1685
1960
|
return entry.params.map((p) => {
|
|
1686
|
-
if (p.type === "object") return objectArgExpr(p, match);
|
|
1961
|
+
if (p.type === "object") return objectArgExpr(p, match, envArgs);
|
|
1962
|
+
const env = envArgs.get(p.name);
|
|
1963
|
+
if (env != null) return env;
|
|
1687
1964
|
const raw = match.args[p.name];
|
|
1688
1965
|
if (raw == null) return p.type === "number" ? "0" : p.type === "boolean" ? "false" : "''";
|
|
1689
1966
|
if (p.type === "number") return String(Number(raw) || 0);
|
|
@@ -1691,19 +1968,20 @@ function argExpr(entry, match) {
|
|
|
1691
1968
|
return quote2(raw);
|
|
1692
1969
|
}).join(", ");
|
|
1693
1970
|
}
|
|
1694
|
-
function renderFunctionCall(match) {
|
|
1971
|
+
function renderFunctionCall(match, page, envArgs) {
|
|
1695
1972
|
const { entry } = match;
|
|
1696
|
-
const args = argExpr(entry, match);
|
|
1973
|
+
const args = argExpr(entry, match, envArgs);
|
|
1697
1974
|
if (entry.kind === "page-object-method" && entry.receiver) {
|
|
1698
1975
|
return ` await ${entry.receiver}.${entry.name}(${args});`;
|
|
1699
1976
|
}
|
|
1700
|
-
return ` await ${entry.name}(page${args ? `, ${args}` : ""});`;
|
|
1977
|
+
return ` await ${entry.name}(${page}${args ? `, ${args}` : ""});`;
|
|
1701
1978
|
}
|
|
1702
|
-
function renderImports(usedEntries) {
|
|
1979
|
+
function renderImports(usedEntries, given) {
|
|
1703
1980
|
const lines = [];
|
|
1704
1981
|
const seenModules = /* @__PURE__ */ new Set();
|
|
1705
1982
|
for (const entry of usedEntries) {
|
|
1706
1983
|
if (entry.kind === "page-object-method" && entry.receiver && entry.importName) {
|
|
1984
|
+
if (given.has(entry.receiver)) continue;
|
|
1707
1985
|
const moduleKey = `${entry.module}#${entry.importName}`;
|
|
1708
1986
|
if (!seenModules.has(moduleKey)) {
|
|
1709
1987
|
seenModules.add(moduleKey);
|
|
@@ -1716,14 +1994,14 @@ function renderImports(usedEntries) {
|
|
|
1716
1994
|
}
|
|
1717
1995
|
return lines;
|
|
1718
1996
|
}
|
|
1719
|
-
function renderInstantiations(usedEntries) {
|
|
1720
|
-
const seen =
|
|
1997
|
+
function renderInstantiations(usedEntries, page, declared) {
|
|
1998
|
+
const seen = new Set(declared);
|
|
1721
1999
|
const lines = [];
|
|
1722
2000
|
for (const entry of usedEntries) {
|
|
1723
2001
|
if (entry.kind !== "page-object-method" || !entry.receiver || !entry.importName) continue;
|
|
1724
2002
|
if (seen.has(entry.receiver)) continue;
|
|
1725
2003
|
seen.add(entry.receiver);
|
|
1726
|
-
lines.push(` const ${entry.receiver} = new ${entry.importName}(page);`);
|
|
2004
|
+
lines.push(` const ${entry.receiver} = new ${entry.importName}(${page});`);
|
|
1727
2005
|
}
|
|
1728
2006
|
return lines;
|
|
1729
2007
|
}
|
|
@@ -1752,10 +2030,35 @@ function samePage(a, b) {
|
|
|
1752
2030
|
const kb = pageKey(b);
|
|
1753
2031
|
return ka === null || kb === null || ka === kb;
|
|
1754
2032
|
}
|
|
1755
|
-
function viewportLine(viewport) {
|
|
1756
|
-
return ` await page.setViewportSize({ width: ${Math.round(viewport.width)}, height: ${Math.round(viewport.height)} });`;
|
|
2033
|
+
function viewportLine(viewport, page) {
|
|
2034
|
+
return ` await ${page}.setViewportSize({ width: ${Math.round(viewport.width)}, height: ${Math.round(viewport.height)} });`;
|
|
2035
|
+
}
|
|
2036
|
+
function pageStepTitle(url, origin) {
|
|
2037
|
+
if (url.startsWith("/")) return url.split(/[?#]/)[0] || "/";
|
|
2038
|
+
try {
|
|
2039
|
+
const parsed = new URL(url);
|
|
2040
|
+
return parsed.origin === origin ? parsed.pathname : `${parsed.host}${parsed.pathname}`;
|
|
2041
|
+
} catch {
|
|
2042
|
+
return url;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function wrapInPageSteps(lines, starts, origin) {
|
|
2046
|
+
if (starts.length === 0) return { lines, at: (index) => index };
|
|
2047
|
+
const out = lines.slice(0, starts[0].line);
|
|
2048
|
+
starts.forEach((start, n) => {
|
|
2049
|
+
const end = starts[n + 1]?.line ?? lines.length;
|
|
2050
|
+
out.push(` await test.step(${quote2(pageStepTitle(start.url, origin))}, async () => {`);
|
|
2051
|
+
out.push(...lines.slice(start.line, end).map((line) => ` ${line}`));
|
|
2052
|
+
out.push(" });");
|
|
2053
|
+
});
|
|
2054
|
+
const at = (index) => {
|
|
2055
|
+
const opened = starts.filter((start) => start.line <= index).length;
|
|
2056
|
+
return opened === 0 ? index : index + 2 * opened - 1;
|
|
2057
|
+
};
|
|
2058
|
+
return { lines: out, at };
|
|
1757
2059
|
}
|
|
1758
2060
|
function renderSpec(session, options = {}) {
|
|
2061
|
+
const page = pageExpression(options);
|
|
1759
2062
|
const { steps } = session;
|
|
1760
2063
|
const viewportAt = new Map((session.viewports ?? []).map((v) => [v.step, v]));
|
|
1761
2064
|
const title = options.title ?? "recorded flow";
|
|
@@ -1763,6 +2066,7 @@ function renderSpec(session, options = {}) {
|
|
|
1763
2066
|
const ctx = {
|
|
1764
2067
|
options,
|
|
1765
2068
|
origin: originOf(session.startUrl) ?? originOf(steps[0]?.pageUrl ?? ""),
|
|
2069
|
+
page,
|
|
1766
2070
|
warnings: [],
|
|
1767
2071
|
envNames: []
|
|
1768
2072
|
};
|
|
@@ -1770,23 +2074,41 @@ function renderSpec(session, options = {}) {
|
|
|
1770
2074
|
const matchedSpans = [];
|
|
1771
2075
|
const usedEntries = [];
|
|
1772
2076
|
const stepStarts = [];
|
|
2077
|
+
const pageStarts = [];
|
|
2078
|
+
const enterPageOf = (index) => {
|
|
2079
|
+
const url = steps[index]?.pageUrl;
|
|
2080
|
+
if (options.testSteps !== "page" || url == null) return;
|
|
2081
|
+
const last = pageStarts[pageStarts.length - 1];
|
|
2082
|
+
if (!last || !samePage(last.url, url)) pageStarts.push({ line: bodyLines.length, url });
|
|
2083
|
+
};
|
|
1773
2084
|
const checkNextPage = (last) => {
|
|
1774
2085
|
if (!options.urlChecks) return;
|
|
1775
2086
|
const next = steps[last + 1];
|
|
1776
2087
|
const current = steps[last];
|
|
1777
2088
|
if (!next || !current || next.action === "goto" || samePage(current.pageUrl, next.pageUrl)) return;
|
|
1778
2089
|
const pattern = pageUrlPattern(next.pageUrl);
|
|
1779
|
-
|
|
2090
|
+
enterPageOf(last + 1);
|
|
2091
|
+
if (pattern) bodyLines.push(` await expect(${page}).toHaveURL(${pattern});`);
|
|
1780
2092
|
const acts = next.action !== "assert" && next.action !== "assertVisible";
|
|
1781
2093
|
const chosen = acts && next.target ? chooseLocator(next.target, options) : null;
|
|
1782
|
-
if (chosen) bodyLines.push(` await expect(page.${chosen.text}).toHaveCount(1);`);
|
|
2094
|
+
if (chosen) bodyLines.push(` await expect(${page}.${chosen.text}).toHaveCount(1);`);
|
|
2095
|
+
};
|
|
2096
|
+
const calls = /* @__PURE__ */ new Map();
|
|
2097
|
+
const callAt = (at) => {
|
|
2098
|
+
if (calls.has(at)) return calls.get(at);
|
|
2099
|
+
const step = steps[at];
|
|
2100
|
+
const found = step && step.action !== "goto" && catalog.length > 0 ? matchFunctionAt(steps, at, catalog) : null;
|
|
2101
|
+
const match = found && ![...viewportAt.keys()].some((v) => v > at && v <= Math.max(...found.matchedIndices)) ? found : null;
|
|
2102
|
+
calls.set(at, match);
|
|
2103
|
+
return match;
|
|
1783
2104
|
};
|
|
1784
2105
|
let pos = 0;
|
|
1785
2106
|
let sawGoto = false;
|
|
1786
2107
|
while (pos < steps.length) {
|
|
1787
2108
|
const step = steps[pos];
|
|
2109
|
+
enterPageOf(pos);
|
|
1788
2110
|
const viewport = pos > 0 ? viewportAt.get(pos) : void 0;
|
|
1789
|
-
if (viewport) bodyLines.push(viewportLine(viewport));
|
|
2111
|
+
if (viewport) bodyLines.push(viewportLine(viewport, page));
|
|
1790
2112
|
stepStarts[pos] = bodyLines.length;
|
|
1791
2113
|
if (step.action === "goto") {
|
|
1792
2114
|
bodyLines.push(...renderRawStep(step, pos, ctx));
|
|
@@ -1794,47 +2116,65 @@ function renderSpec(session, options = {}) {
|
|
|
1794
2116
|
pos++;
|
|
1795
2117
|
continue;
|
|
1796
2118
|
}
|
|
1797
|
-
const
|
|
1798
|
-
const match = found && ![...viewportAt.keys()].some((at) => at > pos && at <= Math.max(...found.matchedIndices)) ? found : null;
|
|
2119
|
+
const match = callAt(pos);
|
|
1799
2120
|
if (match) {
|
|
1800
|
-
bodyLines.push(renderFunctionCall(match));
|
|
2121
|
+
bodyLines.push(renderFunctionCall(match, page, envArgsOf(match, steps, ctx)));
|
|
1801
2122
|
usedEntries.push(match.entry);
|
|
1802
2123
|
const last = Math.max(...match.matchedIndices);
|
|
1803
2124
|
for (let i = pos + 1; i <= last; i++) stepStarts[i] = stepStarts[pos];
|
|
1804
2125
|
matchedSpans.push({ startStep: pos, endStep: last, functionName: match.entry.name });
|
|
1805
|
-
checkNextPage(last);
|
|
1806
2126
|
pos = last + 1;
|
|
1807
2127
|
continue;
|
|
1808
2128
|
}
|
|
1809
2129
|
bodyLines.push(...renderRawStep(step, pos, ctx));
|
|
1810
|
-
checkNextPage(pos);
|
|
2130
|
+
if (!callAt(pos + 1)) checkNextPage(pos);
|
|
1811
2131
|
pos++;
|
|
1812
2132
|
}
|
|
1813
2133
|
const start = viewportAt.get(0);
|
|
1814
2134
|
const opening = [
|
|
1815
|
-
...start ? [viewportLine(start)] : [],
|
|
1816
|
-
...!sawGoto && session.startUrl ? [` await page.goto(${quote2(urlForCode(session.startUrl, ctx))});`] : []
|
|
2135
|
+
...start ? [viewportLine(start, page)] : [],
|
|
2136
|
+
...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote2(urlForCode(session.startUrl, ctx))});`] : []
|
|
1817
2137
|
];
|
|
1818
2138
|
bodyLines.unshift(...opening);
|
|
1819
2139
|
const bodyOffset = opening.length;
|
|
1820
|
-
const
|
|
1821
|
-
|
|
2140
|
+
const wrapped = wrapInPageSteps(
|
|
2141
|
+
bodyLines,
|
|
2142
|
+
pageStarts.map((start2, n) => ({ line: n === 0 ? 0 : start2.line + bodyOffset, url: start2.url })),
|
|
2143
|
+
ctx.origin
|
|
2144
|
+
);
|
|
2145
|
+
const fixtureReceivers = options.format === "body" ? [] : [
|
|
2146
|
+
...new Set(
|
|
2147
|
+
usedEntries.flatMap(
|
|
2148
|
+
(e) => e.kind === "page-object-method" && e.receiver && options.fixtures?.has(e.receiver) ? [e.receiver] : []
|
|
2149
|
+
)
|
|
2150
|
+
)
|
|
2151
|
+
];
|
|
2152
|
+
const given = /* @__PURE__ */ new Set([...options.declaredNames ?? [], ...fixtureReceivers]);
|
|
2153
|
+
const importLines = renderImports(usedEntries, given);
|
|
2154
|
+
const instantiationLines = renderInstantiations(usedEntries, page, given);
|
|
1822
2155
|
const failLine = expectFailLine(options);
|
|
1823
2156
|
const envLines = options.values === "env" && ctx.envNames.length > 0 ? [` // Typed values come from ${[...new Set(ctx.envNames)].join(", ")}.`] : [];
|
|
1824
|
-
const testBody = [...failLine ? [failLine] : [], ...envLines, ...instantiationLines, ...
|
|
1825
|
-
const bodyStart = testBody.length -
|
|
1826
|
-
const
|
|
2157
|
+
const testBody = [...failLine ? [failLine] : [], ...envLines, ...instantiationLines, ...wrapped.lines];
|
|
2158
|
+
const bodyStart = testBody.length - wrapped.lines.length;
|
|
2159
|
+
const result = (lines2, header) => ({
|
|
2160
|
+
code: lines2.join("\n"),
|
|
2161
|
+
imports: importLines,
|
|
2162
|
+
matchedSpans,
|
|
2163
|
+
warnings: ctx.warnings,
|
|
2164
|
+
stepLines: steps.map((_, i) => header + bodyStart + wrapped.at(bodyOffset + (stepStarts[i] ?? 0)) + 1)
|
|
2165
|
+
});
|
|
2166
|
+
const needsLines = (indent) => options.bodyImports === "none" ? [] : importLines.map((line) => `${indent}// Needs: ${line}`);
|
|
1827
2167
|
if (options.format === "body") {
|
|
1828
|
-
const needs =
|
|
1829
|
-
return
|
|
1830
|
-
code: [...needs, ...testBody, ""].join("\n"),
|
|
1831
|
-
matchedSpans,
|
|
1832
|
-
warnings: ctx.warnings,
|
|
1833
|
-
stepLines: linesFrom(needs.length)
|
|
1834
|
-
};
|
|
2168
|
+
const needs = needsLines(" ");
|
|
2169
|
+
return result([...needs, ...testBody, ""], needs.length);
|
|
1835
2170
|
}
|
|
1836
2171
|
const details = renderDetails(options);
|
|
1837
|
-
const
|
|
2172
|
+
const fixture = [.../* @__PURE__ */ new Set([page.split(".")[0], ...fixtureReceivers])].join(", ");
|
|
2173
|
+
const testOpening = details.length > 0 ? [`test(${quote2(title)}, {`, ...details, `}, async ({ ${fixture} }) => {`] : [`test(${quote2(title)}, async ({ ${fixture} }) => {`];
|
|
2174
|
+
if (options.format === "test") {
|
|
2175
|
+
const needs = needsLines("");
|
|
2176
|
+
return result([...needs, ...testOpening, ...testBody, `});`, ``], needs.length + testOpening.length);
|
|
2177
|
+
}
|
|
1838
2178
|
const lines = [
|
|
1839
2179
|
// Package name interpolated rather than adjoining the preceding keyword
|
|
1840
2180
|
// directly, so this generated-code line does not itself look like a real
|
|
@@ -1847,8 +2187,7 @@ function renderSpec(session, options = {}) {
|
|
|
1847
2187
|
`});`,
|
|
1848
2188
|
``
|
|
1849
2189
|
];
|
|
1850
|
-
|
|
1851
|
-
return { code: lines.join("\n"), matchedSpans, warnings: ctx.warnings, stepLines: linesFrom(header) };
|
|
2190
|
+
return result(lines, 1 + importLines.length + 1 + testOpening.length);
|
|
1852
2191
|
}
|
|
1853
2192
|
|
|
1854
2193
|
// ../core/src/steps.ts
|
|
@@ -2231,7 +2570,7 @@ Code:
|
|
|
2231
2570
|
--test-import <mod> Module test and expect come from (default @playwright/test)
|
|
2232
2571
|
--absolute-urls Keep recorded URLs instead of paths for your baseURL
|
|
2233
2572
|
--no-url-checks Do not wait for each new page's URL
|
|
2234
|
-
--env-values Read typed values from
|
|
2573
|
+
--env-values Read typed values from variables named after their fields (E2E_EMAIL)
|
|
2235
2574
|
--fail Mark the test as expected to fail (test.fail())
|
|
2236
2575
|
--fail-reason <text> The reason written beside test.fail()
|
|
2237
2576
|
--tag <tag> Add a tag; repeat for more
|
|
@@ -2391,8 +2730,8 @@ ${USAGE3}`);
|
|
|
2391
2730
|
}
|
|
2392
2731
|
} else {
|
|
2393
2732
|
try {
|
|
2394
|
-
if (
|
|
2395
|
-
text =
|
|
2733
|
+
if (fs5.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
|
|
2734
|
+
text = fs5.readFileSync(args.file, "utf-8");
|
|
2396
2735
|
} catch (e) {
|
|
2397
2736
|
const reason = e.code === "ENOENT" ? "does not exist" : e.message;
|
|
2398
2737
|
console.error(`piwi codegen: ${args.file} ${reason}`);
|
|
@@ -2424,8 +2763,8 @@ ${USAGE3}`);
|
|
|
2424
2763
|
return EXIT_OK3;
|
|
2425
2764
|
}
|
|
2426
2765
|
try {
|
|
2427
|
-
|
|
2428
|
-
|
|
2766
|
+
fs5.mkdirSync(path8.dirname(args.out), { recursive: true });
|
|
2767
|
+
fs5.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
|
|
2429
2768
|
} catch (e) {
|
|
2430
2769
|
const exists = e.code === "EEXIST";
|
|
2431
2770
|
console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
|
|
@@ -2477,148 +2816,6 @@ function estimateFlakeSessionMs(arms, medianDurationMs) {
|
|
|
2477
2816
|
return total;
|
|
2478
2817
|
}
|
|
2479
2818
|
|
|
2480
|
-
// src/internal/config/desktop.ts
|
|
2481
|
-
var fs5 = __toESM(require("fs"));
|
|
2482
|
-
var os = __toESM(require("os"));
|
|
2483
|
-
var path8 = __toESM(require("path"));
|
|
2484
|
-
function defaultDesktopConfigPath() {
|
|
2485
|
-
return path8.join(os.homedir(), ".piwi", "desktop.json");
|
|
2486
|
-
}
|
|
2487
|
-
function readDesktopConfig(filePath) {
|
|
2488
|
-
try {
|
|
2489
|
-
const parsed = JSON.parse(fs5.readFileSync(filePath, "utf8"));
|
|
2490
|
-
const url = parsed?.url;
|
|
2491
|
-
const token = parsed?.token;
|
|
2492
|
-
if (typeof url !== "string" || !url) return null;
|
|
2493
|
-
if (typeof token !== "string" || !token) return null;
|
|
2494
|
-
return { url, token };
|
|
2495
|
-
} catch {
|
|
2496
|
-
return null;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
// src/internal/config/env.ts
|
|
2501
|
-
var DEFAULTS = {
|
|
2502
|
-
projectName: "default-project",
|
|
2503
|
-
uploadTraces: true,
|
|
2504
|
-
uploadReport: true,
|
|
2505
|
-
liveFileUploads: true,
|
|
2506
|
-
collectScmInfo: true,
|
|
2507
|
-
collectCiInfo: true,
|
|
2508
|
-
collectPerformanceMetrics: true,
|
|
2509
|
-
captureLocators: true,
|
|
2510
|
-
capturePageState: true,
|
|
2511
|
-
// Off by default: the page inventory reads control and link names from every
|
|
2512
|
-
// visited page, so it stays opt-in until a project turns it on.
|
|
2513
|
-
capturePageInventory: false,
|
|
2514
|
-
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
2515
|
-
captureCodeReach: false,
|
|
2516
|
-
captureServerTraces: true,
|
|
2517
|
-
captureResources: true,
|
|
2518
|
-
leakCheck: "report",
|
|
2519
|
-
sampleAriaOnPass: true,
|
|
2520
|
-
uploadManifest: true,
|
|
2521
|
-
defaultCapture: true,
|
|
2522
|
-
streaming: true,
|
|
2523
|
-
streamingBatchSize: 5,
|
|
2524
|
-
streamingBatchDelay: 2e3,
|
|
2525
|
-
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
2526
|
-
submitTimeout: 15 * 60 * 1e3,
|
|
2527
|
-
failOnFlakyTests: false,
|
|
2528
|
-
username: null,
|
|
2529
|
-
password: null,
|
|
2530
|
-
apiKey: null,
|
|
2531
|
-
verbose: false
|
|
2532
|
-
};
|
|
2533
|
-
var PIWI_ENV_KEYS = {
|
|
2534
|
-
serverUrl: "PIWI_DASHBOARD_URL",
|
|
2535
|
-
projectName: "PIWI_PROJECT_NAME",
|
|
2536
|
-
verbose: "PIWI_VERBOSE",
|
|
2537
|
-
apiKey: "PIWI_API_KEY",
|
|
2538
|
-
username: "PIWI_USERNAME",
|
|
2539
|
-
password: "PIWI_PASSWORD",
|
|
2540
|
-
environment: "PIWI_ENVIRONMENT",
|
|
2541
|
-
label: "PIWI_LABEL",
|
|
2542
|
-
runLabel: "PIWI_RUN_LABEL",
|
|
2543
|
-
keep: "PIWI_KEEP",
|
|
2544
|
-
streaming: "PIWI_STREAMING",
|
|
2545
|
-
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
2546
|
-
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
2547
|
-
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
2548
|
-
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
2549
|
-
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
2550
|
-
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
2551
|
-
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
2552
|
-
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
2553
|
-
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
2554
|
-
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
2555
|
-
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
2556
|
-
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
2557
|
-
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2558
|
-
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2559
|
-
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
2560
|
-
leakCheck: "PIWI_LEAK_CHECK",
|
|
2561
|
-
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
2562
|
-
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
2563
|
-
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
2564
|
-
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
2565
|
-
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
2566
|
-
outputFile: "PIWI_OUTPUT_FILE",
|
|
2567
|
-
aiMode: "PIWI_AI",
|
|
2568
|
-
aiDir: "PIWI_AI_DIR",
|
|
2569
|
-
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
2570
|
-
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
2571
|
-
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
2572
|
-
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
2573
|
-
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
2574
|
-
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
2575
|
-
};
|
|
2576
|
-
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
2577
|
-
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
2578
|
-
var PIWI_PROBE_ENV = {
|
|
2579
|
-
flag: "PIWI_PROBE",
|
|
2580
|
-
plan: "PIWI_PROBE_PLAN",
|
|
2581
|
-
results: "PIWI_PROBE_RESULTS",
|
|
2582
|
-
secret: "PIWI_PROBE_SECRET"
|
|
2583
|
-
};
|
|
2584
|
-
var PIWI_FLAKE_ENV = {
|
|
2585
|
-
plan: "PIWI_FLAKE_PLAN",
|
|
2586
|
-
results: "PIWI_FLAKE_RESULTS"
|
|
2587
|
-
};
|
|
2588
|
-
var ENV_FALLBACK_SPECS = [
|
|
2589
|
-
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
2590
|
-
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
2591
|
-
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
2592
|
-
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
2593
|
-
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
2594
|
-
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
2595
|
-
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
2596
|
-
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
2597
|
-
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
2598
|
-
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
2599
|
-
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
2600
|
-
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
2601
|
-
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
2602
|
-
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
2603
|
-
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
2604
|
-
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
2605
|
-
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
2606
|
-
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
2607
|
-
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
2608
|
-
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
2609
|
-
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
2610
|
-
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
2611
|
-
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
2612
|
-
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
2613
|
-
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
2614
|
-
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
2615
|
-
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
2616
|
-
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
2617
|
-
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
2618
|
-
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
2619
|
-
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
2620
|
-
];
|
|
2621
|
-
|
|
2622
2819
|
// ../core/src/flake-verdict.ts
|
|
2623
2820
|
var FLAKE_SIGNIFICANCE = 0.05;
|
|
2624
2821
|
var FLAKE_REPRODUCED_RATE = 0.5;
|
|
@@ -3462,7 +3659,7 @@ var fs9 = __toESM(require("fs"));
|
|
|
3462
3659
|
function formatGateResult(result) {
|
|
3463
3660
|
const { facts } = result;
|
|
3464
3661
|
const lines = [
|
|
3465
|
-
result.passed ? `\u2714 Piwi gate passed \u2014 ${facts.projectName} run #${facts.runId}` : `\u2716 Piwi gate failed \u2014 ${facts.projectName} run #${facts.runId}`,
|
|
3662
|
+
result.verdict === "inconclusive" ? `? Piwi gate inconclusive \u2014 ${facts.projectName} run #${facts.runId}` : result.passed ? `\u2714 Piwi gate passed \u2014 ${facts.projectName} run #${facts.runId}` : `\u2716 Piwi gate failed \u2014 ${facts.projectName} run #${facts.runId}`,
|
|
3466
3663
|
` ${facts.totalTests} tests, ${facts.failedTests} failed, ${facts.newRegressions} new, ${facts.newFlaky} newly flaky, ${facts.flakyTests} flaky`
|
|
3467
3664
|
];
|
|
3468
3665
|
if (facts.quarantinedFailures > 0) {
|
|
@@ -3474,6 +3671,10 @@ function formatGateResult(result) {
|
|
|
3474
3671
|
const { leaks, newLeaks } = facts.resources;
|
|
3475
3672
|
lines.push(` ${leaks.length} ${leaks.length === 1 ? "leak" : "leaks"}, ${newLeaks.length} new`);
|
|
3476
3673
|
}
|
|
3674
|
+
if (facts.incident) {
|
|
3675
|
+
lines.push(` ? The run is an environment incident: ${facts.incident.reason}`);
|
|
3676
|
+
lines.push(" Re-run once the environment is back, or clear the flag on the run page if it is wrong.");
|
|
3677
|
+
}
|
|
3477
3678
|
for (const violation of result.violations) lines.push(` \u2716 ${violation.message}`);
|
|
3478
3679
|
for (const warning of result.warnings ?? []) lines.push(` \u26A0 ${warning}`);
|
|
3479
3680
|
lines.push(` ${facts.runUrl}`);
|
|
@@ -3484,6 +3685,7 @@ function formatGateResult(result) {
|
|
|
3484
3685
|
var EXIT_OK4 = 0;
|
|
3485
3686
|
var EXIT_VIOLATED = 1;
|
|
3486
3687
|
var EXIT_ERROR5 = 2;
|
|
3688
|
+
var EXIT_INCONCLUSIVE = 3;
|
|
3487
3689
|
var USAGE5 = `
|
|
3488
3690
|
piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
|
|
3489
3691
|
|
|
@@ -3519,7 +3721,7 @@ Other:
|
|
|
3519
3721
|
--json Print the raw result as JSON instead of a summary
|
|
3520
3722
|
-h, --help Show this help
|
|
3521
3723
|
|
|
3522
|
-
Exit codes: 0 satisfied, 1 violated, 2 could not evaluate.
|
|
3724
|
+
Exit codes: 0 satisfied, 1 violated, 2 could not evaluate, 3 inconclusive (an environment incident).
|
|
3523
3725
|
`.trim();
|
|
3524
3726
|
function readOption4(argv, name) {
|
|
3525
3727
|
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
@@ -3580,7 +3782,7 @@ function parseGateArgs(argv, env) {
|
|
|
3580
3782
|
};
|
|
3581
3783
|
}
|
|
3582
3784
|
async function requestGate(args) {
|
|
3583
|
-
const headers = { "Content-Type": "application/json" };
|
|
3785
|
+
const headers = { "Content-Type": "application/json", "X-Piwi-Client": "cli" };
|
|
3584
3786
|
if (args.apiKey) headers["X-API-Key"] = args.apiKey;
|
|
3585
3787
|
const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
|
|
3586
3788
|
method: "POST",
|
|
@@ -3619,6 +3821,7 @@ async function runGate(argv, env = process.env) {
|
|
|
3619
3821
|
} else {
|
|
3620
3822
|
console.log(formatGateResult(result));
|
|
3621
3823
|
}
|
|
3824
|
+
if (result.verdict === "inconclusive") return EXIT_INCONCLUSIVE;
|
|
3622
3825
|
return result.passed ? EXIT_OK4 : EXIT_VIOLATED;
|
|
3623
3826
|
}
|
|
3624
3827
|
|
|
@@ -3840,6 +4043,58 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
3840
4043
|
// src/cli/skills.ts
|
|
3841
4044
|
var fs11 = __toESM(require("fs"));
|
|
3842
4045
|
var path13 = __toESM(require("path"));
|
|
4046
|
+
|
|
4047
|
+
// ../core/src/skill-stamp.ts
|
|
4048
|
+
var SKILL_VERSION_KEY = "piwi-version";
|
|
4049
|
+
var SKILL_HASH_KEY = "piwi-hash";
|
|
4050
|
+
var FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
4051
|
+
function fnv32(input, seed) {
|
|
4052
|
+
let h = seed >>> 0;
|
|
4053
|
+
for (let i = 0; i < input.length; i++) {
|
|
4054
|
+
h ^= input.charCodeAt(i);
|
|
4055
|
+
h = Math.imul(h, 16777619);
|
|
4056
|
+
}
|
|
4057
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
4058
|
+
}
|
|
4059
|
+
function hash16(input) {
|
|
4060
|
+
return fnv32(input, 2166136261) + fnv32(input, 16777619);
|
|
4061
|
+
}
|
|
4062
|
+
function withoutHashLine(markdown) {
|
|
4063
|
+
return markdown.replace(new RegExp(`^${SKILL_HASH_KEY}:.*\\r?\\n`, "m"), "");
|
|
4064
|
+
}
|
|
4065
|
+
function skillContentHash(markdown) {
|
|
4066
|
+
return hash16(withoutHashLine(markdown).replace(/\r\n/g, "\n"));
|
|
4067
|
+
}
|
|
4068
|
+
function stampSkill(template, version) {
|
|
4069
|
+
const match = FRONT_MATTER.exec(template);
|
|
4070
|
+
if (!match) return template;
|
|
4071
|
+
const front = match[1].split(/\r?\n/).filter((line) => !line.startsWith(`${SKILL_VERSION_KEY}:`) && !line.startsWith(`${SKILL_HASH_KEY}:`));
|
|
4072
|
+
const versionLine = `${SKILL_VERSION_KEY}: ${version}`;
|
|
4073
|
+
const versioned = template.replace(match[0], () => `---
|
|
4074
|
+
${[...front, versionLine].join("\n")}
|
|
4075
|
+
---`);
|
|
4076
|
+
const hash = skillContentHash(versioned);
|
|
4077
|
+
return versioned.replace(`${versionLine}
|
|
4078
|
+
---`, () => `${versionLine}
|
|
4079
|
+
${SKILL_HASH_KEY}: ${hash}
|
|
4080
|
+
---`);
|
|
4081
|
+
}
|
|
4082
|
+
function readSkillStamp(markdown) {
|
|
4083
|
+
const lines = FRONT_MATTER.exec(markdown)?.[1]?.split(/\r?\n/) ?? [];
|
|
4084
|
+
const read = (key) => {
|
|
4085
|
+
const line = lines.find((l) => l.startsWith(`${key}:`));
|
|
4086
|
+
return line ? line.slice(key.length + 1).trim() || null : null;
|
|
4087
|
+
};
|
|
4088
|
+
return { version: read(SKILL_VERSION_KEY), hash: read(SKILL_HASH_KEY) };
|
|
4089
|
+
}
|
|
4090
|
+
function classifyInstalledSkill(installed, stamped) {
|
|
4091
|
+
if (installed.replace(/\r\n/g, "\n") === stamped.replace(/\r\n/g, "\n")) return "current";
|
|
4092
|
+
const stamp = readSkillStamp(installed);
|
|
4093
|
+
if (!stamp.hash) return "unstamped";
|
|
4094
|
+
return skillContentHash(installed) === stamp.hash ? "outdated" : "edited";
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
// src/cli/skills.ts
|
|
3843
4098
|
var SETUP_SKILL = "setup-piwi";
|
|
3844
4099
|
var WORKFLOW_SKILLS = [
|
|
3845
4100
|
"investigate-failure",
|
|
@@ -3871,6 +4126,14 @@ function readFrontMatter(markdown) {
|
|
|
3871
4126
|
}
|
|
3872
4127
|
return out;
|
|
3873
4128
|
}
|
|
4129
|
+
function readTemplatesVersion(templatesDir) {
|
|
4130
|
+
try {
|
|
4131
|
+
const pkg = JSON.parse(fs11.readFileSync(path13.join(templatesDir, "..", "package.json"), "utf-8"));
|
|
4132
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
4133
|
+
} catch {
|
|
4134
|
+
return "0.0.0";
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
3874
4137
|
function templatePath(templatesDir, slug) {
|
|
3875
4138
|
return path13.join(templatesDir, "skills", slug, "SKILL.md");
|
|
3876
4139
|
}
|
|
@@ -3895,23 +4158,37 @@ function installSkills(opts) {
|
|
|
3895
4158
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
3896
4159
|
continue;
|
|
3897
4160
|
}
|
|
3898
|
-
const
|
|
4161
|
+
const version = opts.version ?? readTemplatesVersion(opts.templatesDir);
|
|
4162
|
+
const contents = stampSkill(fs11.readFileSync(source, "utf-8"), version);
|
|
3899
4163
|
const exists = fs11.existsSync(dest);
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
4164
|
+
let detail = "installed skill";
|
|
4165
|
+
if (exists) {
|
|
4166
|
+
const installed = fs11.readFileSync(dest, "utf-8");
|
|
4167
|
+
const state = classifyInstalledSkill(installed, contents);
|
|
4168
|
+
const from = readSkillStamp(installed).version;
|
|
4169
|
+
if (state === "current") {
|
|
4170
|
+
results.push({ step, file: relDest, status: "already", detail: `skill already installed (${version})` });
|
|
4171
|
+
continue;
|
|
4172
|
+
}
|
|
4173
|
+
if (state === "outdated") {
|
|
4174
|
+
detail = `outdated (${from ?? "unknown version"}) \u2014 updated to ${version}`;
|
|
4175
|
+
} else if (!opts.force) {
|
|
4176
|
+
results.push({
|
|
4177
|
+
step,
|
|
4178
|
+
file: relDest,
|
|
4179
|
+
status: "skipped",
|
|
4180
|
+
detail: state === "edited" ? `edited since ${from ?? "it was"} installed \u2014 pass --force to replace it with ${version}` : `skill exists and differs \u2014 pass --force to replace it with ${version}`
|
|
4181
|
+
});
|
|
4182
|
+
continue;
|
|
4183
|
+
} else {
|
|
4184
|
+
detail = state === "edited" ? `edited \u2014 replaced with ${version}` : `replaced with ${version}`;
|
|
4185
|
+
}
|
|
3909
4186
|
}
|
|
3910
4187
|
if (!opts.dryRun) {
|
|
3911
4188
|
fs11.mkdirSync(path13.dirname(dest), { recursive: true });
|
|
3912
4189
|
fs11.writeFileSync(dest, contents);
|
|
3913
4190
|
}
|
|
3914
|
-
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail
|
|
4191
|
+
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
|
|
3915
4192
|
}
|
|
3916
4193
|
return results;
|
|
3917
4194
|
}
|
|
@@ -3929,7 +4206,8 @@ Commands:
|
|
|
3929
4206
|
Options for "add":
|
|
3930
4207
|
--dir <path> Directory to install into (default: ${DEFAULT_SKILLS_DIR})
|
|
3931
4208
|
--cwd <path> Project root to operate on (default: current directory)
|
|
3932
|
-
--force
|
|
4209
|
+
--force Replace a skill that was edited since it was installed
|
|
4210
|
+
(an untouched skill from an older release is always updated)
|
|
3933
4211
|
--dry-run Report what would be written without writing
|
|
3934
4212
|
--json Print the results as JSON
|
|
3935
4213
|
|
|
@@ -4617,7 +4895,7 @@ async function runRunImpact(args, env) {
|
|
|
4617
4895
|
printWarnings(impact);
|
|
4618
4896
|
if (impact.impact.widened) {
|
|
4619
4897
|
console.error(
|
|
4620
|
-
`piwi run: impact widened to the full suite (${impact.impact.unmappedSourceFiles.length}
|
|
4898
|
+
`piwi run: impact widened to the full suite (${impact.impact.unmappedSourceFiles.length} changed file(s) not fully mapped to tests)`
|
|
4621
4899
|
);
|
|
4622
4900
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
4623
4901
|
}
|
|
@@ -5966,7 +6244,10 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5966
6244
|
console.error("piwi preflight: no test reaches this change \u2014 nothing to run");
|
|
5967
6245
|
return code;
|
|
5968
6246
|
}
|
|
5969
|
-
const runCode = await spawnPlaywrightForRun(args.pkgRunner, [...runArgs, ...args.extra],
|
|
6247
|
+
const runCode = await spawnPlaywrightForRun(args.pkgRunner, [...runArgs, ...args.extra], {
|
|
6248
|
+
...env,
|
|
6249
|
+
...originEnv("preflight")
|
|
6250
|
+
});
|
|
5970
6251
|
if (runCode !== 0) code = EXIT_BREAKS;
|
|
5971
6252
|
}
|
|
5972
6253
|
return code;
|