@piwitests/reporter 0.58.0 → 0.60.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 +1456 -1305
- package/dist/global-setup-module.js +137 -54
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1321 -494
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +2 -1
- package/dist/internal/capture/capture-fixtures.js +1011 -333
- package/dist/internal/capture/pick-on-failure.d.ts +6 -3
- package/dist/internal/capture/pick-on-failure.js +123 -45
- package/package.json +5 -5
- package/templates/skills/run-the-right-tests/SKILL.md +19 -8
- package/templates/skills/setup-piwi/SKILL.md +4 -1
package/dist/cli/index.js
CHANGED
|
@@ -788,10 +788,13 @@ async function runAi(argv, env = process.env) {
|
|
|
788
788
|
}
|
|
789
789
|
|
|
790
790
|
// src/cli/bug.ts
|
|
791
|
-
var
|
|
792
|
-
var
|
|
791
|
+
var fs6 = __toESM(require("fs"));
|
|
792
|
+
var path9 = __toESM(require("path"));
|
|
793
793
|
var import_node_child_process2 = require("child_process");
|
|
794
794
|
|
|
795
|
+
// src/internal/config/env.ts
|
|
796
|
+
var path8 = __toESM(require("path"));
|
|
797
|
+
|
|
795
798
|
// src/internal/config/desktop.ts
|
|
796
799
|
var fs4 = __toESM(require("fs"));
|
|
797
800
|
var os = __toESM(require("os"));
|
|
@@ -799,6 +802,14 @@ var path7 = __toESM(require("path"));
|
|
|
799
802
|
function defaultDesktopConfigPath() {
|
|
800
803
|
return path7.join(os.homedir(), ".piwi", "desktop.json");
|
|
801
804
|
}
|
|
805
|
+
function processAlive(pid) {
|
|
806
|
+
try {
|
|
807
|
+
process.kill(pid, 0);
|
|
808
|
+
return true;
|
|
809
|
+
} catch (error) {
|
|
810
|
+
return error.code === "EPERM";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
802
813
|
function readDesktopConfig(filePath2) {
|
|
803
814
|
try {
|
|
804
815
|
const parsed = JSON.parse(fs4.readFileSync(filePath2, "utf8"));
|
|
@@ -806,12 +817,59 @@ function readDesktopConfig(filePath2) {
|
|
|
806
817
|
const token = parsed?.token;
|
|
807
818
|
if (typeof url !== "string" || !url) return null;
|
|
808
819
|
if (typeof token !== "string" || !token) return null;
|
|
820
|
+
if (Number.isInteger(parsed?.pid) && parsed.pid > 0 && !processAlive(parsed.pid)) return null;
|
|
809
821
|
return { url, token };
|
|
810
822
|
} catch {
|
|
811
823
|
return null;
|
|
812
824
|
}
|
|
813
825
|
}
|
|
814
826
|
|
|
827
|
+
// src/internal/config/dotenv.ts
|
|
828
|
+
var fs5 = __toESM(require("fs"));
|
|
829
|
+
|
|
830
|
+
// ../core/src/dotenv.ts
|
|
831
|
+
function parseDotEnv(text) {
|
|
832
|
+
const out = {};
|
|
833
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
834
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
835
|
+
if (!m) continue;
|
|
836
|
+
let value = m[2];
|
|
837
|
+
const quote3 = value[0];
|
|
838
|
+
if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
|
|
839
|
+
value = value.slice(1, -1);
|
|
840
|
+
if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
841
|
+
} else {
|
|
842
|
+
value = value.replace(/\s+#.*$/, "");
|
|
843
|
+
}
|
|
844
|
+
out[m[1]] = value;
|
|
845
|
+
}
|
|
846
|
+
return out;
|
|
847
|
+
}
|
|
848
|
+
function resolvePiwiConnection(sources) {
|
|
849
|
+
const own = {
|
|
850
|
+
serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
|
|
851
|
+
apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
|
|
852
|
+
};
|
|
853
|
+
const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
|
|
854
|
+
const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
|
|
855
|
+
const desktop = sources.desktop ?? null;
|
|
856
|
+
const trim = (url) => url.replace(/\/+$/, "");
|
|
857
|
+
const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
|
|
858
|
+
const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
|
|
859
|
+
if (!serverUrl) return null;
|
|
860
|
+
const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
|
|
861
|
+
return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/internal/config/dotenv.ts
|
|
865
|
+
function readDotEnv(filePath2) {
|
|
866
|
+
try {
|
|
867
|
+
return parseDotEnv(fs5.readFileSync(filePath2, "utf-8"));
|
|
868
|
+
} catch {
|
|
869
|
+
return {};
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
815
873
|
// src/internal/config/env.ts
|
|
816
874
|
var DEFAULTS = {
|
|
817
875
|
projectName: "default-project",
|
|
@@ -848,6 +906,7 @@ var DEFAULTS = {
|
|
|
848
906
|
};
|
|
849
907
|
var PIWI_ENV_KEYS = {
|
|
850
908
|
serverUrl: "PIWI_DASHBOARD_URL",
|
|
909
|
+
desktopApp: "PIWI_DESKTOP_APP",
|
|
851
910
|
projectName: "PIWI_PROJECT_NAME",
|
|
852
911
|
verbose: "PIWI_VERBOSE",
|
|
853
912
|
apiKey: "PIWI_API_KEY",
|
|
@@ -1041,7 +1100,7 @@ function repositoryRoot(cwd) {
|
|
|
1041
1100
|
encoding: "utf-8",
|
|
1042
1101
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1043
1102
|
}).trim();
|
|
1044
|
-
return out ?
|
|
1103
|
+
return out ? path9.resolve(out) : null;
|
|
1045
1104
|
} catch {
|
|
1046
1105
|
return null;
|
|
1047
1106
|
}
|
|
@@ -1049,20 +1108,20 @@ function repositoryRoot(cwd) {
|
|
|
1049
1108
|
function realFolder(dir) {
|
|
1050
1109
|
const rest = [];
|
|
1051
1110
|
let current = dir;
|
|
1052
|
-
while (!
|
|
1053
|
-
rest.unshift(
|
|
1054
|
-
current =
|
|
1111
|
+
while (!fs6.existsSync(current) && path9.dirname(current) !== current) {
|
|
1112
|
+
rest.unshift(path9.basename(current));
|
|
1113
|
+
current = path9.dirname(current);
|
|
1055
1114
|
}
|
|
1056
|
-
return
|
|
1115
|
+
return path9.join(fs6.realpathSync(current), ...rest);
|
|
1057
1116
|
}
|
|
1058
1117
|
function folderInRepository(root, dir) {
|
|
1059
|
-
const relative9 =
|
|
1060
|
-
if (relative9.startsWith("..") ||
|
|
1061
|
-
return relative9.split(
|
|
1118
|
+
const relative9 = path9.relative(realFolder(root), realFolder(dir));
|
|
1119
|
+
if (relative9.startsWith("..") || path9.isAbsolute(relative9)) return void 0;
|
|
1120
|
+
return relative9.split(path9.sep).join("/");
|
|
1062
1121
|
}
|
|
1063
1122
|
function fileFilter(file) {
|
|
1064
1123
|
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1065
|
-
return `${escape(
|
|
1124
|
+
return `${escape(path9.basename(path9.dirname(file)))}[\\\\/]${escape(path9.basename(file))}$`;
|
|
1066
1125
|
}
|
|
1067
1126
|
function runPlaywright(file, cwd, id, env) {
|
|
1068
1127
|
return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
|
|
@@ -1082,10 +1141,10 @@ ${USAGE2}`);
|
|
|
1082
1141
|
return EXIT_ERROR2;
|
|
1083
1142
|
}
|
|
1084
1143
|
const root = args.write ? repositoryRoot(cwd) : null;
|
|
1085
|
-
const outFile = args.out !== null ?
|
|
1144
|
+
const outFile = args.out !== null ? path9.resolve(cwd, args.out) : null;
|
|
1086
1145
|
let spec;
|
|
1087
1146
|
try {
|
|
1088
|
-
spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root,
|
|
1147
|
+
spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path9.dirname(outFile)) : void 0);
|
|
1089
1148
|
} catch (e) {
|
|
1090
1149
|
console.error(`piwi bug: ${e.message}`);
|
|
1091
1150
|
return EXIT_ERROR2;
|
|
@@ -1100,16 +1159,16 @@ ${USAGE2}`);
|
|
|
1100
1159
|
if (!outFile && !root) {
|
|
1101
1160
|
console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
|
|
1102
1161
|
}
|
|
1103
|
-
const file = outFile ??
|
|
1162
|
+
const file = outFile ?? path9.resolve(root ?? cwd, spec.path);
|
|
1104
1163
|
try {
|
|
1105
|
-
|
|
1106
|
-
|
|
1164
|
+
fs6.mkdirSync(path9.dirname(file), { recursive: true });
|
|
1165
|
+
fs6.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
|
|
1107
1166
|
} catch (e) {
|
|
1108
1167
|
const exists = e.code === "EEXIST";
|
|
1109
1168
|
console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
|
|
1110
1169
|
return EXIT_ERROR2;
|
|
1111
1170
|
}
|
|
1112
|
-
const shown =
|
|
1171
|
+
const shown = path9.relative(cwd, file) || file;
|
|
1113
1172
|
console.error(`piwi bug: wrote ${shown}`);
|
|
1114
1173
|
if (!args.run) return EXIT_OK2;
|
|
1115
1174
|
const code = await runPlaywright(file, cwd, args.id, env);
|
|
@@ -1127,8 +1186,8 @@ ${USAGE2}`);
|
|
|
1127
1186
|
}
|
|
1128
1187
|
|
|
1129
1188
|
// src/cli/codegen.ts
|
|
1130
|
-
var
|
|
1131
|
-
var
|
|
1189
|
+
var fs7 = __toESM(require("fs"));
|
|
1190
|
+
var path10 = __toESM(require("path"));
|
|
1132
1191
|
|
|
1133
1192
|
// ../core/src/recording.ts
|
|
1134
1193
|
var ASSERTION_MATCHERS = [
|
|
@@ -2168,7 +2227,7 @@ function renderSpec(session, options = {}) {
|
|
|
2168
2227
|
if (viewport) bodyLines.push(viewportLine(viewport, page));
|
|
2169
2228
|
stepStarts[pos] = bodyLines.length;
|
|
2170
2229
|
if (step.action === "goto") {
|
|
2171
|
-
bodyLines.push(...renderRawStep(step, pos, ctx));
|
|
2230
|
+
if (!(options.continuePage && pos === 0)) bodyLines.push(...renderRawStep(step, pos, ctx));
|
|
2172
2231
|
sawGoto = true;
|
|
2173
2232
|
pos++;
|
|
2174
2233
|
continue;
|
|
@@ -2187,10 +2246,10 @@ function renderSpec(session, options = {}) {
|
|
|
2187
2246
|
if (!callAt(pos + 1)) checkNextPage(pos);
|
|
2188
2247
|
pos++;
|
|
2189
2248
|
}
|
|
2190
|
-
const start = viewportAt.get(0);
|
|
2249
|
+
const start = options.continuePage ? void 0 : viewportAt.get(0);
|
|
2191
2250
|
const opening = [
|
|
2192
2251
|
...start ? [viewportLine(start, page)] : [],
|
|
2193
|
-
...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
|
|
2252
|
+
...!sawGoto && !options.continuePage && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
|
|
2194
2253
|
];
|
|
2195
2254
|
bodyLines.unshift(...opening);
|
|
2196
2255
|
const bodyOffset = opening.length;
|
|
@@ -2345,43 +2404,43 @@ var Checker = class {
|
|
|
2345
2404
|
constructor() {
|
|
2346
2405
|
this.errors = [];
|
|
2347
2406
|
}
|
|
2348
|
-
fail(
|
|
2349
|
-
if (this.errors.length < 20) this.errors.push(`${
|
|
2407
|
+
fail(path25, message) {
|
|
2408
|
+
if (this.errors.length < 20) this.errors.push(`${path25}: ${message}`);
|
|
2350
2409
|
}
|
|
2351
|
-
text(v,
|
|
2410
|
+
text(v, path25, max, nullable) {
|
|
2352
2411
|
if (v == null) {
|
|
2353
|
-
if (!nullable) this.fail(
|
|
2412
|
+
if (!nullable) this.fail(path25, "is required");
|
|
2354
2413
|
return null;
|
|
2355
2414
|
}
|
|
2356
2415
|
if (typeof v !== "string") {
|
|
2357
|
-
this.fail(
|
|
2416
|
+
this.fail(path25, "must be a string");
|
|
2358
2417
|
return null;
|
|
2359
2418
|
}
|
|
2360
|
-
if (v.length > max) this.fail(
|
|
2419
|
+
if (v.length > max) this.fail(path25, `is longer than ${max} characters`);
|
|
2361
2420
|
return v;
|
|
2362
2421
|
}
|
|
2363
|
-
number(v,
|
|
2422
|
+
number(v, path25) {
|
|
2364
2423
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2365
|
-
this.fail(
|
|
2424
|
+
this.fail(path25, "must be a number");
|
|
2366
2425
|
return 0;
|
|
2367
2426
|
}
|
|
2368
2427
|
return v;
|
|
2369
2428
|
}
|
|
2370
2429
|
};
|
|
2371
|
-
function checkTarget(c, v,
|
|
2430
|
+
function checkTarget(c, v, path25) {
|
|
2372
2431
|
if (v == null) return null;
|
|
2373
2432
|
if (!isObject(v)) {
|
|
2374
|
-
c.fail(
|
|
2433
|
+
c.fail(path25, "must be an object");
|
|
2375
2434
|
return null;
|
|
2376
2435
|
}
|
|
2377
2436
|
const alternativesIn = v.alternatives ?? [];
|
|
2378
2437
|
const alternatives = [];
|
|
2379
|
-
if (!Array.isArray(alternativesIn)) c.fail(`${
|
|
2438
|
+
if (!Array.isArray(alternativesIn)) c.fail(`${path25}.alternatives`, "must be a list");
|
|
2380
2439
|
else if (alternativesIn.length > STEPS_LIMITS.alternatives)
|
|
2381
|
-
c.fail(`${
|
|
2440
|
+
c.fail(`${path25}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
|
|
2382
2441
|
else {
|
|
2383
2442
|
alternativesIn.forEach((alt, i) => {
|
|
2384
|
-
const at = `${
|
|
2443
|
+
const at = `${path25}.alternatives[${i}]`;
|
|
2385
2444
|
if (!isObject(alt)) return c.fail(at, "must be an object");
|
|
2386
2445
|
const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
|
|
2387
2446
|
const safe = locator ? safeLocator(locator) : null;
|
|
@@ -2395,47 +2454,47 @@ function checkTarget(c, v, path24) {
|
|
|
2395
2454
|
});
|
|
2396
2455
|
}
|
|
2397
2456
|
return {
|
|
2398
|
-
tagName: c.text(v.tagName, `${
|
|
2399
|
-
role: c.text(v.role, `${
|
|
2400
|
-
accessibleName: c.text(v.accessibleName, `${
|
|
2401
|
-
testId: c.text(v.testId, `${
|
|
2402
|
-
text: c.text(v.text, `${
|
|
2457
|
+
tagName: c.text(v.tagName, `${path25}.tagName`, 64, false) ?? "",
|
|
2458
|
+
role: c.text(v.role, `${path25}.role`, 64, true),
|
|
2459
|
+
accessibleName: c.text(v.accessibleName, `${path25}.accessibleName`, STEPS_LIMITS.textLength, true),
|
|
2460
|
+
testId: c.text(v.testId, `${path25}.testId`, STEPS_LIMITS.textLength, true),
|
|
2461
|
+
text: c.text(v.text, `${path25}.text`, STEPS_LIMITS.textLength, true),
|
|
2403
2462
|
alternatives
|
|
2404
2463
|
};
|
|
2405
2464
|
}
|
|
2406
|
-
function checkAssertion(c, v,
|
|
2465
|
+
function checkAssertion(c, v, path25) {
|
|
2407
2466
|
if (!isObject(v)) {
|
|
2408
|
-
c.fail(
|
|
2467
|
+
c.fail(path25, "is required on an assert step");
|
|
2409
2468
|
return void 0;
|
|
2410
2469
|
}
|
|
2411
2470
|
const matcher = v.matcher;
|
|
2412
2471
|
if (!ASSERTION_MATCHERS.includes(matcher)) {
|
|
2413
|
-
c.fail(`${
|
|
2472
|
+
c.fail(`${path25}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
|
|
2414
2473
|
return void 0;
|
|
2415
2474
|
}
|
|
2416
|
-
const expected = c.text(v.expected, `${
|
|
2475
|
+
const expected = c.text(v.expected, `${path25}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
|
|
2417
2476
|
if (matcher === "toHaveCount" && expected != null && !/^\d{1,6}$/.test(expected)) {
|
|
2418
|
-
c.fail(`${
|
|
2477
|
+
c.fail(`${path25}.expected`, "must be a whole number of elements on a toHaveCount assertion");
|
|
2419
2478
|
}
|
|
2420
2479
|
return {
|
|
2421
2480
|
matcher,
|
|
2422
2481
|
expected: VALUE_MATCHERS.has(matcher) ? expected : null,
|
|
2423
|
-
actual: c.text(v.actual, `${
|
|
2482
|
+
actual: c.text(v.actual, `${path25}.actual`, STEPS_LIMITS.valueLength, true),
|
|
2424
2483
|
negated: v.negated === true,
|
|
2425
|
-
note: c.text(v.note, `${
|
|
2484
|
+
note: c.text(v.note, `${path25}.note`, STEPS_LIMITS.valueLength, true)
|
|
2426
2485
|
};
|
|
2427
2486
|
}
|
|
2428
|
-
function checkManual(c, v,
|
|
2487
|
+
function checkManual(c, v, path25) {
|
|
2429
2488
|
if (!isObject(v)) {
|
|
2430
|
-
c.fail(
|
|
2489
|
+
c.fail(path25, "is required on a manual step");
|
|
2431
2490
|
return void 0;
|
|
2432
2491
|
}
|
|
2433
|
-
const description = c.text(v.description, `${
|
|
2434
|
-
if (description != null && !description.trim()) c.fail(`${
|
|
2492
|
+
const description = c.text(v.description, `${path25}.description`, STEPS_LIMITS.valueLength, false);
|
|
2493
|
+
if (description != null && !description.trim()) c.fail(`${path25}.description`, "must not be empty");
|
|
2435
2494
|
return {
|
|
2436
2495
|
description: description ?? "",
|
|
2437
|
-
location: c.text(v.location, `${
|
|
2438
|
-
call: c.text(v.call, `${
|
|
2496
|
+
location: c.text(v.location, `${path25}.location`, STEPS_LIMITS.textLength, true),
|
|
2497
|
+
call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true)
|
|
2439
2498
|
};
|
|
2440
2499
|
}
|
|
2441
2500
|
function checkViewports(c, v, stepCount) {
|
|
@@ -2476,43 +2535,43 @@ function checkViewports(c, v, stepCount) {
|
|
|
2476
2535
|
});
|
|
2477
2536
|
return out;
|
|
2478
2537
|
}
|
|
2479
|
-
function checkStep(c, v,
|
|
2538
|
+
function checkStep(c, v, path25) {
|
|
2480
2539
|
if (!isObject(v)) {
|
|
2481
|
-
c.fail(
|
|
2540
|
+
c.fail(path25, "must be an object");
|
|
2482
2541
|
return null;
|
|
2483
2542
|
}
|
|
2484
2543
|
const action = v.action;
|
|
2485
2544
|
if (!ACTIONS.has(action)) {
|
|
2486
|
-
c.fail(`${
|
|
2545
|
+
c.fail(`${path25}.action`, "is not a known action");
|
|
2487
2546
|
return null;
|
|
2488
2547
|
}
|
|
2489
2548
|
const step = {
|
|
2490
2549
|
action: action === "assertVisible" ? "assert" : action,
|
|
2491
|
-
target: checkTarget(c, v.target, `${
|
|
2492
|
-
value: c.text(v.value, `${
|
|
2550
|
+
target: checkTarget(c, v.target, `${path25}.target`),
|
|
2551
|
+
value: c.text(v.value, `${path25}.value`, STEPS_LIMITS.valueLength, true),
|
|
2493
2552
|
redacted: v.redacted === true,
|
|
2494
|
-
pageUrl: c.text(v.pageUrl, `${
|
|
2495
|
-
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${
|
|
2553
|
+
pageUrl: c.text(v.pageUrl, `${path25}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
|
|
2554
|
+
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path25}.timestamp`)
|
|
2496
2555
|
};
|
|
2497
2556
|
if (step.redacted) step.value = null;
|
|
2498
2557
|
if (action === "hover" || action === "dblclick") {
|
|
2499
2558
|
step.value = null;
|
|
2500
|
-
if (!step.target) c.fail(`${
|
|
2559
|
+
if (!step.target) c.fail(`${path25}.target`, `is required on a ${action} step`);
|
|
2501
2560
|
}
|
|
2502
|
-
if (action === "setInputFiles" && !step.target) c.fail(`${
|
|
2561
|
+
if (action === "setInputFiles" && !step.target) c.fail(`${path25}.target`, "is required on a setInputFiles step");
|
|
2503
2562
|
if (action === "dragTo") {
|
|
2504
2563
|
step.value = null;
|
|
2505
|
-
if (!step.target) c.fail(`${
|
|
2506
|
-
step.dropTarget = checkTarget(c, v.dropTarget, `${
|
|
2507
|
-
if (!step.dropTarget) c.fail(`${
|
|
2564
|
+
if (!step.target) c.fail(`${path25}.target`, "is required on a dragTo step");
|
|
2565
|
+
step.dropTarget = checkTarget(c, v.dropTarget, `${path25}.dropTarget`);
|
|
2566
|
+
if (!step.dropTarget) c.fail(`${path25}.dropTarget`, "is required on a dragTo step");
|
|
2508
2567
|
}
|
|
2509
2568
|
if (action === "assertVisible") {
|
|
2510
2569
|
step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
|
|
2511
2570
|
} else if (action === "assert") {
|
|
2512
|
-
step.assertion = checkAssertion(c, v.assertion, `${
|
|
2571
|
+
step.assertion = checkAssertion(c, v.assertion, `${path25}.assertion`);
|
|
2513
2572
|
} else if (action === "manual") {
|
|
2514
2573
|
step.value = null;
|
|
2515
|
-
step.manual = checkManual(c, v.manual, `${
|
|
2574
|
+
step.manual = checkManual(c, v.manual, `${path25}.manual`);
|
|
2516
2575
|
}
|
|
2517
2576
|
return step;
|
|
2518
2577
|
}
|
|
@@ -2592,13 +2651,14 @@ async function fetchResolution(options, projectId) {
|
|
|
2592
2651
|
}
|
|
2593
2652
|
return await res.json();
|
|
2594
2653
|
}
|
|
2595
|
-
async function fetchImpact(options, projectId, changedFiles) {
|
|
2654
|
+
async function fetchImpact(options, projectId, changedFiles, changes = []) {
|
|
2596
2655
|
const url = `${options.serverUrl}/api/projects/${projectId}/selections/impact`;
|
|
2597
2656
|
const res = await fetch(url, {
|
|
2598
2657
|
method: "POST",
|
|
2599
2658
|
headers: { "Content-Type": "application/json", ...authHeaders(options.apiKey) },
|
|
2600
2659
|
body: JSON.stringify({
|
|
2601
2660
|
changedFiles,
|
|
2661
|
+
...changes.length > 0 ? { changes } : {},
|
|
2602
2662
|
format: options.format ?? "args",
|
|
2603
2663
|
shard: options.shard ?? void 0,
|
|
2604
2664
|
order: options.order ?? void 0
|
|
@@ -2811,8 +2871,8 @@ ${USAGE3}`);
|
|
|
2811
2871
|
}
|
|
2812
2872
|
} else {
|
|
2813
2873
|
try {
|
|
2814
|
-
if (
|
|
2815
|
-
text =
|
|
2874
|
+
if (fs7.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
|
|
2875
|
+
text = fs7.readFileSync(args.file, "utf-8");
|
|
2816
2876
|
} catch (e) {
|
|
2817
2877
|
const reason = e.code === "ENOENT" ? "does not exist" : e.message;
|
|
2818
2878
|
console.error(`piwi codegen: ${args.file} ${reason}`);
|
|
@@ -2844,8 +2904,8 @@ ${USAGE3}`);
|
|
|
2844
2904
|
return EXIT_OK3;
|
|
2845
2905
|
}
|
|
2846
2906
|
try {
|
|
2847
|
-
|
|
2848
|
-
|
|
2907
|
+
fs7.mkdirSync(path10.dirname(args.out), { recursive: true });
|
|
2908
|
+
fs7.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
|
|
2849
2909
|
} catch (e) {
|
|
2850
2910
|
const exists = e.code === "EEXIST";
|
|
2851
2911
|
console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
|
|
@@ -2859,7 +2919,7 @@ ${USAGE3}`);
|
|
|
2859
2919
|
}
|
|
2860
2920
|
|
|
2861
2921
|
// src/cli/coverage.ts
|
|
2862
|
-
var
|
|
2922
|
+
var path15 = __toESM(require("path"));
|
|
2863
2923
|
|
|
2864
2924
|
// ../core/src/codeowners.ts
|
|
2865
2925
|
function patternParts(pattern) {
|
|
@@ -2897,10 +2957,10 @@ function pathPatternMatcher(pattern) {
|
|
|
2897
2957
|
const { segments, floating } = patternParts(pattern);
|
|
2898
2958
|
if (floating) {
|
|
2899
2959
|
const only = segments[0];
|
|
2900
|
-
return (
|
|
2960
|
+
return (path25) => path25.split("/").some((part) => segmentMatches(only, part));
|
|
2901
2961
|
}
|
|
2902
|
-
return (
|
|
2903
|
-
const parts =
|
|
2962
|
+
return (path25) => {
|
|
2963
|
+
const parts = path25.split("/");
|
|
2904
2964
|
let at = new Uint8Array(parts.length);
|
|
2905
2965
|
at[0] = 1;
|
|
2906
2966
|
for (let i = 0; i < segments.length; i++) {
|
|
@@ -2966,8 +3026,8 @@ function parseCoverageScope(raw) {
|
|
|
2966
3026
|
function compileCoverageScope(scope) {
|
|
2967
3027
|
const include = scope.include.map(pathPatternMatcher);
|
|
2968
3028
|
const exclude = scope.exclude.map(pathPatternMatcher);
|
|
2969
|
-
return (
|
|
2970
|
-
const file =
|
|
3029
|
+
return (path25) => {
|
|
3030
|
+
const file = path25.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
2971
3031
|
if (include.length > 0 && !include.some((matches) => matches(file))) return false;
|
|
2972
3032
|
return !exclude.some((matches) => matches(file));
|
|
2973
3033
|
};
|
|
@@ -2983,7 +3043,7 @@ function errorMessage(error) {
|
|
|
2983
3043
|
}
|
|
2984
3044
|
|
|
2985
3045
|
// src/internal/support/test-paths.ts
|
|
2986
|
-
var
|
|
3046
|
+
var path11 = __toESM(require("path"));
|
|
2987
3047
|
|
|
2988
3048
|
// src/internal/collect/metadata-collector.ts
|
|
2989
3049
|
var DETACHED_HEAD = "HEAD";
|
|
@@ -3016,14 +3076,14 @@ function normalizeRef(ref) {
|
|
|
3016
3076
|
|
|
3017
3077
|
// src/internal/coverage-upload/git-inventory.ts
|
|
3018
3078
|
var import_node_child_process5 = require("child_process");
|
|
3019
|
-
var
|
|
3020
|
-
var
|
|
3079
|
+
var fs9 = __toESM(require("fs"));
|
|
3080
|
+
var path13 = __toESM(require("path"));
|
|
3021
3081
|
|
|
3022
3082
|
// src/internal/collect/blob-ids.ts
|
|
3023
3083
|
var import_node_child_process4 = require("child_process");
|
|
3024
3084
|
var crypto2 = __toESM(require("crypto"));
|
|
3025
|
-
var
|
|
3026
|
-
var
|
|
3085
|
+
var fs8 = __toESM(require("fs"));
|
|
3086
|
+
var path12 = __toESM(require("path"));
|
|
3027
3087
|
function gitHashObjects(root, paths) {
|
|
3028
3088
|
return new Promise((resolve10) => {
|
|
3029
3089
|
const child = (0, import_node_child_process4.execFile)(
|
|
@@ -3090,7 +3150,7 @@ async function readInventory(root, warn = () => {
|
|
|
3090
3150
|
const changed = [];
|
|
3091
3151
|
for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
|
|
3092
3152
|
if (!file || !inventory.has(file)) continue;
|
|
3093
|
-
if (
|
|
3153
|
+
if (fs9.existsSync(path13.join(root, file))) changed.push(file);
|
|
3094
3154
|
else inventory.delete(file);
|
|
3095
3155
|
}
|
|
3096
3156
|
const ids = await hashPaths(root, changed);
|
|
@@ -3109,8 +3169,8 @@ async function readInventory(root, warn = () => {
|
|
|
3109
3169
|
}
|
|
3110
3170
|
|
|
3111
3171
|
// src/internal/coverage-upload/report-files.ts
|
|
3112
|
-
var
|
|
3113
|
-
var
|
|
3172
|
+
var fs10 = __toESM(require("fs"));
|
|
3173
|
+
var path14 = __toESM(require("path"));
|
|
3114
3174
|
|
|
3115
3175
|
// ../core/src/coverage.ts
|
|
3116
3176
|
var MAX_LINE = 1e6;
|
|
@@ -3189,7 +3249,7 @@ var FileLines = class {
|
|
|
3189
3249
|
this.functions.set(key, { start, end: last, ran });
|
|
3190
3250
|
}
|
|
3191
3251
|
}
|
|
3192
|
-
toFile(
|
|
3252
|
+
toFile(path25) {
|
|
3193
3253
|
const executable = [];
|
|
3194
3254
|
const lines = [];
|
|
3195
3255
|
const push = (ranges, line) => {
|
|
@@ -3209,7 +3269,7 @@ var FileLines = class {
|
|
|
3209
3269
|
if (didRun) ran.push([start, end]);
|
|
3210
3270
|
}
|
|
3211
3271
|
return {
|
|
3212
|
-
path:
|
|
3272
|
+
path: path25,
|
|
3213
3273
|
executable,
|
|
3214
3274
|
lines,
|
|
3215
3275
|
functions: normalizeFunctionRanges(ran),
|
|
@@ -3221,16 +3281,16 @@ var ReportFiles = class {
|
|
|
3221
3281
|
constructor() {
|
|
3222
3282
|
this.byPath = /* @__PURE__ */ new Map();
|
|
3223
3283
|
}
|
|
3224
|
-
of(
|
|
3225
|
-
let file = this.byPath.get(
|
|
3284
|
+
of(path25) {
|
|
3285
|
+
let file = this.byPath.get(path25);
|
|
3226
3286
|
if (!file) {
|
|
3227
3287
|
file = new FileLines();
|
|
3228
|
-
this.byPath.set(
|
|
3288
|
+
this.byPath.set(path25, file);
|
|
3229
3289
|
}
|
|
3230
3290
|
return file;
|
|
3231
3291
|
}
|
|
3232
3292
|
toFiles() {
|
|
3233
|
-
return [...this.byPath].map(([
|
|
3293
|
+
return [...this.byPath].map(([path25, file]) => file.toFile(path25));
|
|
3234
3294
|
}
|
|
3235
3295
|
};
|
|
3236
3296
|
var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
@@ -3569,23 +3629,23 @@ function normalizeSlashPath(raw) {
|
|
|
3569
3629
|
}
|
|
3570
3630
|
return (drive || (absolute ? "/" : "")) + out.join("/");
|
|
3571
3631
|
}
|
|
3572
|
-
function isAbsolutePath(
|
|
3573
|
-
return
|
|
3632
|
+
function isAbsolutePath(path25) {
|
|
3633
|
+
return path25.startsWith("/") || /^[A-Za-z]:\//.test(path25);
|
|
3574
3634
|
}
|
|
3575
|
-
function relativeTo(
|
|
3635
|
+
function relativeTo(path25, root) {
|
|
3576
3636
|
const drive = /^[A-Za-z]:/.test(root);
|
|
3577
3637
|
const base = root.endsWith("/") ? root : `${root}/`;
|
|
3578
|
-
const head =
|
|
3638
|
+
const head = path25.slice(0, base.length);
|
|
3579
3639
|
const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
|
|
3580
3640
|
if (!inside) return null;
|
|
3581
|
-
const relative9 =
|
|
3641
|
+
const relative9 = path25.slice(base.length);
|
|
3582
3642
|
return relative9 || null;
|
|
3583
3643
|
}
|
|
3584
3644
|
function reportPathCandidates(raw, { repoRoot, bases }) {
|
|
3585
3645
|
const slash = (value) => value.replace(/\\/g, "/");
|
|
3586
3646
|
const root = normalizeSlashPath(slash(repoRoot));
|
|
3587
|
-
const
|
|
3588
|
-
if (!root || !
|
|
3647
|
+
const path25 = slash(raw.trim());
|
|
3648
|
+
if (!root || !path25 || path25.includes("\0")) return [];
|
|
3589
3649
|
const out = [];
|
|
3590
3650
|
const add = (absolute) => {
|
|
3591
3651
|
const normalized = normalizeSlashPath(absolute);
|
|
@@ -3593,12 +3653,12 @@ function reportPathCandidates(raw, { repoRoot, bases }) {
|
|
|
3593
3653
|
const relative9 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
|
|
3594
3654
|
if (relative9 && !out.includes(relative9)) out.push(relative9);
|
|
3595
3655
|
};
|
|
3596
|
-
if (isAbsolutePath(
|
|
3597
|
-
add(
|
|
3656
|
+
if (isAbsolutePath(path25)) {
|
|
3657
|
+
add(path25);
|
|
3598
3658
|
} else {
|
|
3599
3659
|
for (const base of bases) {
|
|
3600
3660
|
const dir = slash(base);
|
|
3601
|
-
if (isAbsolutePath(dir)) add(`${dir}/${
|
|
3661
|
+
if (isAbsolutePath(dir)) add(`${dir}/${path25}`);
|
|
3602
3662
|
}
|
|
3603
3663
|
}
|
|
3604
3664
|
return out;
|
|
@@ -3622,14 +3682,14 @@ function globSegmentRegExp(segment) {
|
|
|
3622
3682
|
function expandPattern(pattern, cwd) {
|
|
3623
3683
|
const normalized = pattern.replace(/\\/g, "/");
|
|
3624
3684
|
if (!/[*?]/.test(normalized)) {
|
|
3625
|
-
const file =
|
|
3626
|
-
return
|
|
3685
|
+
const file = path14.resolve(cwd, normalized);
|
|
3686
|
+
return fs10.existsSync(file) && fs10.statSync(file).isFile() ? [file] : [];
|
|
3627
3687
|
}
|
|
3628
|
-
const absolute =
|
|
3688
|
+
const absolute = path14.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
|
|
3629
3689
|
const segments = normalized.split("/");
|
|
3630
3690
|
const firstWild = segments.findIndex((s) => /[*?]/.test(s));
|
|
3631
3691
|
const baseParts = segments.slice(0, firstWild);
|
|
3632
|
-
const base = absolute ? baseParts.join("/") || "/" :
|
|
3692
|
+
const base = absolute ? baseParts.join("/") || "/" : path14.resolve(cwd, ...baseParts);
|
|
3633
3693
|
const rest = segments.slice(firstWild).filter((s) => s !== "");
|
|
3634
3694
|
const out = [];
|
|
3635
3695
|
const walk = (dir, at) => {
|
|
@@ -3637,14 +3697,14 @@ function expandPattern(pattern, cwd) {
|
|
|
3637
3697
|
const segment = rest[at];
|
|
3638
3698
|
let entries;
|
|
3639
3699
|
try {
|
|
3640
|
-
entries =
|
|
3700
|
+
entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
3641
3701
|
} catch {
|
|
3642
3702
|
return;
|
|
3643
3703
|
}
|
|
3644
3704
|
if (segment === "**") {
|
|
3645
3705
|
walk(dir, at + 1);
|
|
3646
3706
|
for (const entry of entries) {
|
|
3647
|
-
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(
|
|
3707
|
+
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path14.join(dir, entry.name), at);
|
|
3648
3708
|
}
|
|
3649
3709
|
return;
|
|
3650
3710
|
}
|
|
@@ -3652,7 +3712,7 @@ function expandPattern(pattern, cwd) {
|
|
|
3652
3712
|
const last = at === rest.length - 1;
|
|
3653
3713
|
for (const entry of entries) {
|
|
3654
3714
|
if (!match.test(entry.name)) continue;
|
|
3655
|
-
const full =
|
|
3715
|
+
const full = path14.join(dir, entry.name);
|
|
3656
3716
|
if (last && entry.isFile()) out.push(full);
|
|
3657
3717
|
else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
|
|
3658
3718
|
walk(full, at + 1);
|
|
@@ -3665,7 +3725,7 @@ function expandPattern(pattern, cwd) {
|
|
|
3665
3725
|
async function readReport(file, format) {
|
|
3666
3726
|
let reader = format ? new CoverageReportReader(format) : null;
|
|
3667
3727
|
let head = "";
|
|
3668
|
-
for await (const chunk of
|
|
3728
|
+
for await (const chunk of fs10.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
|
|
3669
3729
|
if (!reader) {
|
|
3670
3730
|
head += chunk;
|
|
3671
3731
|
if (head.length < 4096) continue;
|
|
@@ -3687,18 +3747,18 @@ async function readReport(file, format) {
|
|
|
3687
3747
|
return reader.end();
|
|
3688
3748
|
}
|
|
3689
3749
|
function reportBases(report, reportFile, options) {
|
|
3690
|
-
if (options.root) return [
|
|
3691
|
-
const dir =
|
|
3750
|
+
if (options.root) return [path14.resolve(options.cwd, options.root)];
|
|
3751
|
+
const dir = path14.dirname(path14.resolve(reportFile));
|
|
3692
3752
|
const sources = report.sources.map(
|
|
3693
|
-
(source) =>
|
|
3753
|
+
(source) => path14.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path14.resolve(dir, source)
|
|
3694
3754
|
);
|
|
3695
|
-
return [...sources, options.cwd, dir,
|
|
3755
|
+
return [...sources, options.cwd, dir, path14.dirname(dir), options.repoRoot];
|
|
3696
3756
|
}
|
|
3697
3757
|
function resolveReport(report, reportFile, options, into) {
|
|
3698
3758
|
const bases = reportBases(report, reportFile, options);
|
|
3699
3759
|
for (const file of report.files) {
|
|
3700
3760
|
const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
|
|
3701
|
-
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) =>
|
|
3761
|
+
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs10.existsSync(path14.join(options.repoRoot, candidate)));
|
|
3702
3762
|
if (!match) {
|
|
3703
3763
|
into.unmatched.push(file.path);
|
|
3704
3764
|
continue;
|
|
@@ -3923,7 +3983,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
|
|
|
3923
3983
|
commit: checkout.commit,
|
|
3924
3984
|
branch,
|
|
3925
3985
|
suite: args.suite,
|
|
3926
|
-
reports: reports.map((report) =>
|
|
3986
|
+
reports: reports.map((report) => path15.relative(cwd, report) || report),
|
|
3927
3987
|
files: files.length,
|
|
3928
3988
|
unmatched: resolved.unmatched.length,
|
|
3929
3989
|
outOfScope: resolved.outOfScope,
|
|
@@ -3995,7 +4055,7 @@ ${USAGE4}`);
|
|
|
3995
4055
|
var import_node_child_process6 = require("child_process");
|
|
3996
4056
|
var fs11 = __toESM(require("fs"));
|
|
3997
4057
|
var os2 = __toESM(require("os"));
|
|
3998
|
-
var
|
|
4058
|
+
var path17 = __toESM(require("path"));
|
|
3999
4059
|
|
|
4000
4060
|
// ../core/src/error-parse.ts
|
|
4001
4061
|
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
@@ -4197,52 +4257,10 @@ function withinBudget(startedAt, now, budgetMs) {
|
|
|
4197
4257
|
}
|
|
4198
4258
|
|
|
4199
4259
|
// src/internal/support/connection.ts
|
|
4200
|
-
var
|
|
4201
|
-
var path15 = __toESM(require("path"));
|
|
4202
|
-
|
|
4203
|
-
// ../core/src/dotenv.ts
|
|
4204
|
-
function parseDotEnv(text) {
|
|
4205
|
-
const out = {};
|
|
4206
|
-
for (const raw of text.split(/\r?\n/)) {
|
|
4207
|
-
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
4208
|
-
if (!m) continue;
|
|
4209
|
-
let value = m[2];
|
|
4210
|
-
const quote3 = value[0];
|
|
4211
|
-
if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
|
|
4212
|
-
value = value.slice(1, -1);
|
|
4213
|
-
if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
4214
|
-
} else {
|
|
4215
|
-
value = value.replace(/\s+#.*$/, "");
|
|
4216
|
-
}
|
|
4217
|
-
out[m[1]] = value;
|
|
4218
|
-
}
|
|
4219
|
-
return out;
|
|
4220
|
-
}
|
|
4221
|
-
function resolvePiwiConnection(sources) {
|
|
4222
|
-
const own = {
|
|
4223
|
-
serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
|
|
4224
|
-
apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
|
|
4225
|
-
};
|
|
4226
|
-
const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
|
|
4227
|
-
const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
|
|
4228
|
-
const desktop = sources.desktop ?? null;
|
|
4229
|
-
const trim = (url) => url.replace(/\/+$/, "");
|
|
4230
|
-
const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
|
|
4231
|
-
const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
|
|
4232
|
-
if (!serverUrl) return null;
|
|
4233
|
-
const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
|
|
4234
|
-
return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
|
|
4235
|
-
}
|
|
4236
|
-
|
|
4237
|
-
// src/internal/support/connection.ts
|
|
4260
|
+
var path16 = __toESM(require("path"));
|
|
4238
4261
|
function resolveCliConnection(flags, env, dirs) {
|
|
4239
4262
|
let dotEnv = {};
|
|
4240
|
-
for (const dir of dirs) {
|
|
4241
|
-
try {
|
|
4242
|
-
dotEnv = { ...parseDotEnv(fs10.readFileSync(path15.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
4243
|
-
} catch {
|
|
4244
|
-
}
|
|
4245
|
-
}
|
|
4263
|
+
for (const dir of dirs) dotEnv = { ...readDotEnv(path16.join(dir, ".env")), ...dotEnv };
|
|
4246
4264
|
return resolvePiwiConnection({
|
|
4247
4265
|
flags,
|
|
4248
4266
|
env,
|
|
@@ -4440,7 +4458,7 @@ function playwrightRunner(dir, log) {
|
|
|
4440
4458
|
return {
|
|
4441
4459
|
now: () => Date.now(),
|
|
4442
4460
|
async runBatch(plan, arm, repeat, batch) {
|
|
4443
|
-
const planFile =
|
|
4461
|
+
const planFile = path17.join(dir, `${arm.id}.plan.json`);
|
|
4444
4462
|
if (!fs11.existsSync(planFile)) {
|
|
4445
4463
|
fs11.writeFileSync(
|
|
4446
4464
|
planFile,
|
|
@@ -4453,7 +4471,7 @@ function playwrightRunner(dir, log) {
|
|
|
4453
4471
|
})
|
|
4454
4472
|
);
|
|
4455
4473
|
}
|
|
4456
|
-
const resultsFile =
|
|
4474
|
+
const resultsFile = path17.join(dir, `${arm.id}.${batch}.jsonl`);
|
|
4457
4475
|
fs11.writeFileSync(resultsFile, "");
|
|
4458
4476
|
const env = {
|
|
4459
4477
|
...process.env,
|
|
@@ -4732,8 +4750,8 @@ async function runFlake(argv, runner) {
|
|
|
4732
4750
|
if (!args.json) {
|
|
4733
4751
|
printHeader(plan, commit, estimate, args.budgetMs);
|
|
4734
4752
|
}
|
|
4735
|
-
const dir = fs11.mkdtempSync(
|
|
4736
|
-
const log =
|
|
4753
|
+
const dir = fs11.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
|
|
4754
|
+
const log = path17.join(dir, "playwright.log");
|
|
4737
4755
|
let session;
|
|
4738
4756
|
try {
|
|
4739
4757
|
session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
|
|
@@ -4924,9 +4942,9 @@ function readCount(argv, name) {
|
|
|
4924
4942
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
4925
4943
|
return Math.floor(n);
|
|
4926
4944
|
}
|
|
4927
|
-
function readRunIdFromFile(
|
|
4945
|
+
function readRunIdFromFile(path25) {
|
|
4928
4946
|
try {
|
|
4929
|
-
const parsed = JSON.parse(fs12.readFileSync(
|
|
4947
|
+
const parsed = JSON.parse(fs12.readFileSync(path25, "utf-8"));
|
|
4930
4948
|
const runId = Number(parsed.runId);
|
|
4931
4949
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
4932
4950
|
} catch {
|
|
@@ -5018,7 +5036,7 @@ async function runGate(argv, env = process.env) {
|
|
|
5018
5036
|
|
|
5019
5037
|
// src/cli/init.ts
|
|
5020
5038
|
var fs15 = __toESM(require("fs"));
|
|
5021
|
-
var
|
|
5039
|
+
var path20 = __toESM(require("path"));
|
|
5022
5040
|
|
|
5023
5041
|
// src/cli/report.ts
|
|
5024
5042
|
var STATUS_MARK = {
|
|
@@ -5036,7 +5054,7 @@ function formatStep(result) {
|
|
|
5036
5054
|
|
|
5037
5055
|
// src/cli/detect.ts
|
|
5038
5056
|
var fs13 = __toESM(require("fs"));
|
|
5039
|
-
var
|
|
5057
|
+
var path18 = __toESM(require("path"));
|
|
5040
5058
|
var CONFIG_NAMES = [
|
|
5041
5059
|
"playwright.config.ts",
|
|
5042
5060
|
"playwright.config.mts",
|
|
@@ -5054,7 +5072,7 @@ var LOCKFILES = [
|
|
|
5054
5072
|
];
|
|
5055
5073
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
5056
5074
|
function readPackageJson(root) {
|
|
5057
|
-
const pkgPath =
|
|
5075
|
+
const pkgPath = path18.join(root, "package.json");
|
|
5058
5076
|
try {
|
|
5059
5077
|
return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
5060
5078
|
} catch {
|
|
@@ -5065,13 +5083,13 @@ function detectPackageManager(root, pkg) {
|
|
|
5065
5083
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
5066
5084
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
5067
5085
|
for (const [file, manager] of LOCKFILES) {
|
|
5068
|
-
if (fs13.existsSync(
|
|
5086
|
+
if (fs13.existsSync(path18.join(root, file))) return manager;
|
|
5069
5087
|
}
|
|
5070
5088
|
return "npm";
|
|
5071
5089
|
}
|
|
5072
5090
|
function findConfig(root) {
|
|
5073
5091
|
for (const name of CONFIG_NAMES) {
|
|
5074
|
-
const candidate =
|
|
5092
|
+
const candidate = path18.join(root, name);
|
|
5075
5093
|
if (fs13.existsSync(candidate)) return candidate;
|
|
5076
5094
|
}
|
|
5077
5095
|
return null;
|
|
@@ -5082,14 +5100,14 @@ function langOf(configPath) {
|
|
|
5082
5100
|
}
|
|
5083
5101
|
function suggestProjectName(root, pkg) {
|
|
5084
5102
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
5085
|
-
return fromPkg ||
|
|
5103
|
+
return fromPkg || path18.basename(root) || "default-project";
|
|
5086
5104
|
}
|
|
5087
5105
|
function hasReporter(pkg) {
|
|
5088
5106
|
if (!pkg) return false;
|
|
5089
5107
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
5090
5108
|
}
|
|
5091
5109
|
function detectProject(root) {
|
|
5092
|
-
const absRoot =
|
|
5110
|
+
const absRoot = path18.resolve(root);
|
|
5093
5111
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
5094
5112
|
const configPath = findConfig(absRoot);
|
|
5095
5113
|
return {
|
|
@@ -5118,7 +5136,10 @@ function installCommand(manager) {
|
|
|
5118
5136
|
|
|
5119
5137
|
// src/cli/configure.ts
|
|
5120
5138
|
var REPORTER_IMPORT = "import { wrapConfig } from '@piwitests/reporter'";
|
|
5121
|
-
|
|
5139
|
+
function manualHint(opts) {
|
|
5140
|
+
const options = optionsLiteral(opts).replace(/\s+/g, " ");
|
|
5141
|
+
return `Could not find \`export default defineConfig(...)\`. Wrap your exported config by hand: import { wrapConfig } from '@piwitests/reporter', then export default wrapConfig(defineConfig({ ... }), ${options}).`;
|
|
5142
|
+
}
|
|
5122
5143
|
var quote2 = quoteJsString;
|
|
5123
5144
|
function matchClosingParen(src, openIndex) {
|
|
5124
5145
|
let depth = 0;
|
|
@@ -5157,6 +5178,7 @@ function optionsLiteral(opts) {
|
|
|
5157
5178
|
const lines = [];
|
|
5158
5179
|
if (opts.serverUrl) lines.push(` serverUrl: ${quote2(opts.serverUrl)},`);
|
|
5159
5180
|
lines.push(` projectName: ${quote2(opts.projectName)},`);
|
|
5181
|
+
if (opts.desktopApp) lines.push(` desktopApp: ${quote2(opts.desktopApp)},`);
|
|
5160
5182
|
return `{
|
|
5161
5183
|
${lines.join("\n")}
|
|
5162
5184
|
}`;
|
|
@@ -5180,11 +5202,11 @@ function wrapPlaywrightConfig(source, opts) {
|
|
|
5180
5202
|
return { text: source, status: "already", detail: "Piwi reporter is already wired into the config" };
|
|
5181
5203
|
}
|
|
5182
5204
|
const match = /export\s+default\s+defineConfig\s*\(/.exec(source);
|
|
5183
|
-
if (!match) return { text: source, status: "manual", detail:
|
|
5205
|
+
if (!match) return { text: source, status: "manual", detail: manualHint(opts) };
|
|
5184
5206
|
const openParen = match.index + match[0].length - 1;
|
|
5185
5207
|
const closeParen = matchClosingParen(source, openParen);
|
|
5186
5208
|
const callStart = source.indexOf("defineConfig", match.index);
|
|
5187
|
-
if (closeParen === -1 || callStart === -1) return { text: source, status: "manual", detail:
|
|
5209
|
+
if (closeParen === -1 || callStart === -1) return { text: source, status: "manual", detail: manualHint(opts) };
|
|
5188
5210
|
const callExpr = source.slice(callStart, closeParen + 1);
|
|
5189
5211
|
let before = withReporterImport(source.slice(0, match.index));
|
|
5190
5212
|
const after = source.slice(closeParen + 1);
|
|
@@ -5230,7 +5252,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
5230
5252
|
|
|
5231
5253
|
// src/cli/skills.ts
|
|
5232
5254
|
var fs14 = __toESM(require("fs"));
|
|
5233
|
-
var
|
|
5255
|
+
var path19 = __toESM(require("path"));
|
|
5234
5256
|
|
|
5235
5257
|
// ../core/src/skill-stamp.ts
|
|
5236
5258
|
var SKILL_VERSION_KEY = "piwi-version";
|
|
@@ -5293,16 +5315,16 @@ var WORKFLOW_SKILLS = [
|
|
|
5293
5315
|
"fix-a-reported-bug"
|
|
5294
5316
|
];
|
|
5295
5317
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
5296
|
-
var DEFAULT_SKILLS_DIR =
|
|
5318
|
+
var DEFAULT_SKILLS_DIR = path19.join(".claude", "skills");
|
|
5297
5319
|
function findTemplatesDir(fromDir) {
|
|
5298
5320
|
let dir = fromDir;
|
|
5299
5321
|
for (let i = 0; i < 6; i++) {
|
|
5300
|
-
if (fs14.existsSync(
|
|
5301
|
-
const parent =
|
|
5322
|
+
if (fs14.existsSync(path19.join(dir, "templates", "skills"))) return path19.join(dir, "templates");
|
|
5323
|
+
const parent = path19.dirname(dir);
|
|
5302
5324
|
if (parent === dir) break;
|
|
5303
5325
|
dir = parent;
|
|
5304
5326
|
}
|
|
5305
|
-
return
|
|
5327
|
+
return path19.resolve(fromDir, "..", "..", "templates");
|
|
5306
5328
|
}
|
|
5307
5329
|
function readFrontMatter(markdown) {
|
|
5308
5330
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -5316,14 +5338,14 @@ function readFrontMatter(markdown) {
|
|
|
5316
5338
|
}
|
|
5317
5339
|
function readTemplatesVersion(templatesDir) {
|
|
5318
5340
|
try {
|
|
5319
|
-
const pkg = JSON.parse(fs14.readFileSync(
|
|
5341
|
+
const pkg = JSON.parse(fs14.readFileSync(path19.join(templatesDir, "..", "package.json"), "utf-8"));
|
|
5320
5342
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
5321
5343
|
} catch {
|
|
5322
5344
|
return "0.0.0";
|
|
5323
5345
|
}
|
|
5324
5346
|
}
|
|
5325
5347
|
function templatePath(templatesDir, slug) {
|
|
5326
|
-
return
|
|
5348
|
+
return path19.join(templatesDir, "skills", slug, "SKILL.md");
|
|
5327
5349
|
}
|
|
5328
5350
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
5329
5351
|
const infos = [];
|
|
@@ -5340,8 +5362,8 @@ function installSkills(opts) {
|
|
|
5340
5362
|
for (const slug of opts.slugs) {
|
|
5341
5363
|
const step = `skill:${slug}`;
|
|
5342
5364
|
const source = templatePath(opts.templatesDir, slug);
|
|
5343
|
-
const relDest =
|
|
5344
|
-
const dest =
|
|
5365
|
+
const relDest = path19.join(opts.skillsDir, slug, "SKILL.md");
|
|
5366
|
+
const dest = path19.join(opts.root, relDest);
|
|
5345
5367
|
if (!fs14.existsSync(source)) {
|
|
5346
5368
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
5347
5369
|
continue;
|
|
@@ -5373,7 +5395,7 @@ function installSkills(opts) {
|
|
|
5373
5395
|
}
|
|
5374
5396
|
}
|
|
5375
5397
|
if (!opts.dryRun) {
|
|
5376
|
-
fs14.mkdirSync(
|
|
5398
|
+
fs14.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
5377
5399
|
fs14.writeFileSync(dest, contents);
|
|
5378
5400
|
}
|
|
5379
5401
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
|
|
@@ -5446,7 +5468,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
5446
5468
|
}
|
|
5447
5469
|
const results = installSkills({
|
|
5448
5470
|
templatesDir,
|
|
5449
|
-
root:
|
|
5471
|
+
root: path19.resolve(readOption5(rest, "--cwd") ?? cwd),
|
|
5450
5472
|
skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
5451
5473
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
5452
5474
|
force: rest.includes("--force"),
|
|
@@ -5468,6 +5490,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
5468
5490
|
|
|
5469
5491
|
// src/cli/init.ts
|
|
5470
5492
|
var DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
5493
|
+
var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
|
|
5471
5494
|
var USAGE8 = `
|
|
5472
5495
|
piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
|
|
5473
5496
|
|
|
@@ -5484,9 +5507,14 @@ What it does (each step is idempotent and safe to re-run):
|
|
|
5484
5507
|
Options:
|
|
5485
5508
|
--server-url <url> Dashboard URL to write into the config (env PIWI_DASHBOARD_URL,
|
|
5486
5509
|
default ${DEFAULT_SERVER_URL})
|
|
5510
|
+
--no-server-url Write no URL into the config: the reporter finds the Piwi desktop
|
|
5511
|
+
app running on this machine, or reads PIWI_DASHBOARD_URL
|
|
5487
5512
|
--project <name> Project name to report under (default: your package/folder name)
|
|
5488
|
-
--
|
|
5489
|
-
|
|
5513
|
+
--desktop-app <mode> Write desktopApp into the config: "prefer" sends local runs to the
|
|
5514
|
+
Piwi desktop app while it runs, and CI runs to --server-url
|
|
5515
|
+
--api-key <key> API key to write into .env (env PIWI_API_KEY), where the reporter
|
|
5516
|
+
reads it. Omit to keep it a .env.example placeholder you fill in
|
|
5517
|
+
yourself.
|
|
5490
5518
|
--cwd <path> Project root to operate on (default: current directory)
|
|
5491
5519
|
--skills <list> Comma-separated skills to install, or "all" / "none"
|
|
5492
5520
|
(default: ${WORKFLOW_SKILLS.join(", ")})
|
|
@@ -5517,13 +5545,15 @@ function resolveSkillSlugs(raw) {
|
|
|
5517
5545
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
5518
5546
|
}
|
|
5519
5547
|
function parseInitArgs(argv, env, cwd) {
|
|
5520
|
-
const root =
|
|
5548
|
+
const root = path20.resolve(readOption6(argv, "--cwd") ?? cwd);
|
|
5521
5549
|
const detected = detectProject(root);
|
|
5522
5550
|
return {
|
|
5523
5551
|
root,
|
|
5524
|
-
serverUrl: (readOption6(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? DEFAULT_SERVER_URL).replace(/\/$/, ""),
|
|
5552
|
+
serverUrl: argv.includes("--no-server-url") ? "" : (readOption6(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? DEFAULT_SERVER_URL).replace(/\/$/, ""),
|
|
5525
5553
|
projectName: readOption6(argv, "--project") ?? detected.suggestedProjectName,
|
|
5526
|
-
|
|
5554
|
+
desktopApp: argv.includes("--desktop-app") ? readOption6(argv, "--desktop-app") ?? "" : null,
|
|
5555
|
+
// The desktop app needs no key, and a key with no URL keeps the reporter from finding it.
|
|
5556
|
+
apiKey: argv.includes("--no-server-url") ? null : readOption6(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
5527
5557
|
skillsDir: readOption6(argv, "--skills-dir") ?? DEFAULT_SKILLS_DIR,
|
|
5528
5558
|
skillSlugs: resolveSkillSlugs(readOption6(argv, "--skills")),
|
|
5529
5559
|
install: !argv.includes("--no-install"),
|
|
@@ -5582,18 +5612,29 @@ function stepConfig(project, opts) {
|
|
|
5582
5612
|
status: "manual",
|
|
5583
5613
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
5584
5614
|
};
|
|
5585
|
-
const rel =
|
|
5586
|
-
const
|
|
5615
|
+
const rel = path20.relative(project.root, project.configPath) || path20.basename(project.configPath);
|
|
5616
|
+
const source = readFileOr(project.configPath, "");
|
|
5617
|
+
const desktopApp = opts.desktopApp ?? void 0;
|
|
5618
|
+
const edit = wrapPlaywrightConfig(source, {
|
|
5587
5619
|
serverUrl: opts.serverUrl,
|
|
5588
|
-
projectName: opts.projectName
|
|
5620
|
+
projectName: opts.projectName,
|
|
5621
|
+
desktopApp
|
|
5589
5622
|
});
|
|
5590
5623
|
if (edit.status === "updated" && !opts.dryRun) fs15.writeFileSync(project.configPath, edit.text);
|
|
5624
|
+
if (edit.status === "already" && desktopApp && !new RegExp(`\\bdesktopApp\\s*:\\s*['"\`]${desktopApp}['"\`]`).test(source)) {
|
|
5625
|
+
return {
|
|
5626
|
+
step,
|
|
5627
|
+
file: rel,
|
|
5628
|
+
status: "manual",
|
|
5629
|
+
detail: `The Piwi reporter is already wired in \u2014 add \`desktopApp: '${desktopApp}'\` to its options`
|
|
5630
|
+
};
|
|
5631
|
+
}
|
|
5591
5632
|
return { step, file: rel, status: edit.status, detail: edit.detail };
|
|
5592
5633
|
}
|
|
5593
5634
|
function stepFixtures(project, opts) {
|
|
5594
5635
|
const step = "fixtures";
|
|
5595
|
-
const rel =
|
|
5596
|
-
const dest =
|
|
5636
|
+
const rel = path20.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
5637
|
+
const dest = path20.join(project.root, rel);
|
|
5597
5638
|
if (fs15.existsSync(dest)) {
|
|
5598
5639
|
const current = readFileOr(dest, "");
|
|
5599
5640
|
if (/piwiFixtures/.test(current))
|
|
@@ -5606,7 +5647,7 @@ function stepFixtures(project, opts) {
|
|
|
5606
5647
|
};
|
|
5607
5648
|
}
|
|
5608
5649
|
if (!opts.dryRun) {
|
|
5609
|
-
fs15.mkdirSync(
|
|
5650
|
+
fs15.mkdirSync(path20.dirname(dest), { recursive: true });
|
|
5610
5651
|
fs15.writeFileSync(dest, fixturesContents());
|
|
5611
5652
|
}
|
|
5612
5653
|
return {
|
|
@@ -5618,7 +5659,7 @@ function stepFixtures(project, opts) {
|
|
|
5618
5659
|
}
|
|
5619
5660
|
function stepEnv(project, opts) {
|
|
5620
5661
|
const results = [];
|
|
5621
|
-
const examplePath =
|
|
5662
|
+
const examplePath = path20.join(project.root, ".env.example");
|
|
5622
5663
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
5623
5664
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
5624
5665
|
["PIWI_API_KEY", ""]
|
|
@@ -5631,11 +5672,10 @@ function stepEnv(project, opts) {
|
|
|
5631
5672
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
5632
5673
|
});
|
|
5633
5674
|
if (opts.apiKey) {
|
|
5634
|
-
const envPath =
|
|
5635
|
-
const
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
]);
|
|
5675
|
+
const envPath = path20.join(project.root, ".env");
|
|
5676
|
+
const entries = [["PIWI_API_KEY", opts.apiKey]];
|
|
5677
|
+
if (opts.serverUrl) entries.unshift(["PIWI_DASHBOARD_URL", opts.serverUrl]);
|
|
5678
|
+
const env = upsertEnvKeys(readFileOr(envPath, ""), entries);
|
|
5639
5679
|
if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
|
|
5640
5680
|
results.push({
|
|
5641
5681
|
step: "env",
|
|
@@ -5643,7 +5683,7 @@ function stepEnv(project, opts) {
|
|
|
5643
5683
|
status: env.added.length ? "updated" : "already",
|
|
5644
5684
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
5645
5685
|
});
|
|
5646
|
-
const gitignorePath =
|
|
5686
|
+
const gitignorePath = path20.join(project.root, ".gitignore");
|
|
5647
5687
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
5648
5688
|
if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
|
|
5649
5689
|
results.push({
|
|
@@ -5657,16 +5697,26 @@ function stepEnv(project, opts) {
|
|
|
5657
5697
|
}
|
|
5658
5698
|
function nextSteps(opts, steps) {
|
|
5659
5699
|
const out = [];
|
|
5660
|
-
if (!opts.apiKey) {
|
|
5700
|
+
if (!opts.apiKey && opts.serverUrl) {
|
|
5661
5701
|
out.push(
|
|
5662
|
-
"If your dashboard has authentication on, create an API key (Settings \u2192 Account \u2192 API keys)
|
|
5702
|
+
"If your dashboard has authentication on, create an API key (Settings \u2192 Account \u2192 API keys) and put it in .env as PIWI_API_KEY: the reporter reads it from the .env next to your Playwright config. Keep .env out of git; in CI, set PIWI_API_KEY as a secret."
|
|
5663
5703
|
);
|
|
5664
5704
|
}
|
|
5665
5705
|
if (steps.some((s) => s.status === "manual")) {
|
|
5666
5706
|
out.push("Finish the steps marked [manual] above \u2014 each one carries the exact change to make.");
|
|
5667
5707
|
}
|
|
5668
5708
|
out.push("In your specs, import `test` (and `expect`) from your fixtures file instead of `@playwright/test`.");
|
|
5669
|
-
|
|
5709
|
+
if (!opts.serverUrl) {
|
|
5710
|
+
out.push(
|
|
5711
|
+
"Run `npx playwright test` \u2014 the run appears in the Piwi desktop app while it is running. Elsewhere, set PIWI_DASHBOARD_URL to the dashboard the runs go to."
|
|
5712
|
+
);
|
|
5713
|
+
} else if (opts.desktopApp === "prefer") {
|
|
5714
|
+
out.push(
|
|
5715
|
+
`Run \`npx playwright test\` \u2014 the run appears in the Piwi desktop app while it is running, and at ${opts.serverUrl} on CI or with the app closed.`
|
|
5716
|
+
);
|
|
5717
|
+
} else {
|
|
5718
|
+
out.push(`Run \`npx playwright test\` \u2014 the run should appear at ${opts.serverUrl}.`);
|
|
5719
|
+
}
|
|
5670
5720
|
out.push(
|
|
5671
5721
|
"For a machine-readable check, set PIWI_OUTPUT_FILE=piwi-run.json and read runUrl from that file after the run."
|
|
5672
5722
|
);
|
|
@@ -5678,6 +5728,10 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5678
5728
|
return 0;
|
|
5679
5729
|
}
|
|
5680
5730
|
const opts = parseInitArgs(argv, env, cwd);
|
|
5731
|
+
if (opts.desktopApp !== null && !DESKTOP_APP_MODES.includes(opts.desktopApp)) {
|
|
5732
|
+
console.error(`--desktop-app must be one of ${DESKTOP_APP_MODES.join(", ")} (got "${opts.desktopApp}")`);
|
|
5733
|
+
return 1;
|
|
5734
|
+
}
|
|
5681
5735
|
const project = detectProject(opts.root);
|
|
5682
5736
|
const steps = [];
|
|
5683
5737
|
if (!opts.skillsOnly) {
|
|
@@ -5707,9 +5761,10 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5707
5761
|
project: {
|
|
5708
5762
|
root: project.root,
|
|
5709
5763
|
packageManager: project.packageManager,
|
|
5710
|
-
configPath: project.configPath ?
|
|
5764
|
+
configPath: project.configPath ? path20.relative(project.root, project.configPath) : null,
|
|
5711
5765
|
projectName: opts.projectName,
|
|
5712
|
-
serverUrl: opts.serverUrl
|
|
5766
|
+
serverUrl: opts.serverUrl,
|
|
5767
|
+
desktopApp: opts.desktopApp
|
|
5713
5768
|
},
|
|
5714
5769
|
steps,
|
|
5715
5770
|
nextSteps: guidance
|
|
@@ -5732,1197 +5787,1280 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
5732
5787
|
|
|
5733
5788
|
// src/cli/select.ts
|
|
5734
5789
|
var fs17 = __toESM(require("fs"));
|
|
5735
|
-
var
|
|
5790
|
+
var path22 = __toESM(require("path"));
|
|
5736
5791
|
var import_node_child_process7 = require("child_process");
|
|
5737
5792
|
|
|
5738
|
-
// src/
|
|
5739
|
-
var
|
|
5740
|
-
var
|
|
5741
|
-
var
|
|
5742
|
-
var
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
function playwrightSupportsAddReporter(version) {
|
|
5746
|
-
const match = version ? /^(\d+)\.(\d+)/.exec(version) : null;
|
|
5747
|
-
if (!match) return false;
|
|
5748
|
-
const major = Number(match[1]);
|
|
5749
|
-
const minor = Number(match[2]);
|
|
5750
|
-
return major > ADD_REPORTER_MIN.major || major === ADD_REPORTER_MIN.major && minor >= ADD_REPORTER_MIN.minor;
|
|
5793
|
+
// ../core/src/diff-anchors.ts
|
|
5794
|
+
var RENAME_FROM = `rename from `;
|
|
5795
|
+
var RENAME_TO = "rename to ";
|
|
5796
|
+
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
5797
|
+
var INDEX_LINE = /^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: |$)/i;
|
|
5798
|
+
function blobOf(id) {
|
|
5799
|
+
return /^0+$/.test(id) ? void 0 : id.toLowerCase();
|
|
5751
5800
|
}
|
|
5752
|
-
function
|
|
5753
|
-
|
|
5801
|
+
function stripPrefix(path25) {
|
|
5802
|
+
const unquoted = path25.startsWith('"') && path25.endsWith('"') ? path25.slice(1, -1) : path25;
|
|
5803
|
+
return unquoted.replace(/^[ab]\//, "");
|
|
5754
5804
|
}
|
|
5755
|
-
function
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5805
|
+
function parseUnifiedDiff(text) {
|
|
5806
|
+
const files = [];
|
|
5807
|
+
let file = null;
|
|
5808
|
+
let hunk = null;
|
|
5809
|
+
let oldLine = 0;
|
|
5810
|
+
let newLine = 0;
|
|
5811
|
+
let oldLeft = 0;
|
|
5812
|
+
let newLeft = 0;
|
|
5813
|
+
const current = () => {
|
|
5814
|
+
if (!file) {
|
|
5815
|
+
file = { path: "", status: "modified", hunks: [] };
|
|
5816
|
+
files.push(file);
|
|
5817
|
+
}
|
|
5818
|
+
return file;
|
|
5766
5819
|
};
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5820
|
+
for (const line of text.split(/\r?\n/)) {
|
|
5821
|
+
if (hunk) {
|
|
5822
|
+
if (line.startsWith("-") && oldLeft > 0) {
|
|
5823
|
+
hunk.removed.push({ line: oldLine++, text: line.slice(1) });
|
|
5824
|
+
oldLeft--;
|
|
5825
|
+
} else if (line.startsWith("+") && newLeft > 0) {
|
|
5826
|
+
hunk.added.push({ line: newLine++, text: line.slice(1) });
|
|
5827
|
+
newLeft--;
|
|
5828
|
+
} else if ((line.startsWith(" ") || line === "") && oldLeft > 0 && newLeft > 0) {
|
|
5829
|
+
oldLine++;
|
|
5830
|
+
newLine++;
|
|
5831
|
+
oldLeft--;
|
|
5832
|
+
newLeft--;
|
|
5833
|
+
} else if (!line.startsWith("\\")) hunk = null;
|
|
5834
|
+
if (hunk && oldLeft === 0 && newLeft === 0) hunk = null;
|
|
5835
|
+
if (hunk || !line.startsWith("@@")) continue;
|
|
5776
5836
|
}
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
}
|
|
5788
|
-
function readInstalledPlaywrightVersion(cwd) {
|
|
5789
|
-
try {
|
|
5790
|
-
const require2 = (0, import_node_module2.createRequire)(path20.join(cwd, "noop.js"));
|
|
5791
|
-
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
5792
|
-
try {
|
|
5793
|
-
return require2(id).version ?? null;
|
|
5794
|
-
} catch {
|
|
5795
|
-
}
|
|
5837
|
+
const header = HUNK_HEADER.exec(line);
|
|
5838
|
+
if (header) {
|
|
5839
|
+
oldLine = Number(header[1]);
|
|
5840
|
+
newLine = Number(header[3]);
|
|
5841
|
+
oldLeft = header[2] === void 0 ? 1 : Number(header[2]);
|
|
5842
|
+
newLeft = header[4] === void 0 ? 1 : Number(header[4]);
|
|
5843
|
+
hunk = { oldStart: oldLine, newStart: newLine, removed: [], added: [] };
|
|
5844
|
+
current().hunks.push(hunk);
|
|
5845
|
+
if (oldLeft === 0 && newLeft === 0) hunk = null;
|
|
5846
|
+
continue;
|
|
5796
5847
|
}
|
|
5797
|
-
|
|
5848
|
+
if (line.startsWith("diff --git ")) {
|
|
5849
|
+
const m = /^diff --git (\S+|"[^"]+") (\S+|"[^"]+")$/.exec(line);
|
|
5850
|
+
file = { path: m ? stripPrefix(m[2]) : "", status: "modified", hunks: [] };
|
|
5851
|
+
files.push(file);
|
|
5852
|
+
} else if (line.startsWith("--- ")) {
|
|
5853
|
+
const f = file && !file.hunks.length ? file : (file = null, current());
|
|
5854
|
+
const old = line.slice(4).trim();
|
|
5855
|
+
if (old === "/dev/null") f.status = "added";
|
|
5856
|
+
else if (!f.path) f.path = stripPrefix(old);
|
|
5857
|
+
} else if (line.startsWith("+++ ")) {
|
|
5858
|
+
const next = line.slice(4).trim();
|
|
5859
|
+
if (next === "/dev/null") current().status = "deleted";
|
|
5860
|
+
else current().path = stripPrefix(next);
|
|
5861
|
+
} else if (line.startsWith(RENAME_FROM)) {
|
|
5862
|
+
current().oldPath = line.slice(RENAME_FROM.length);
|
|
5863
|
+
current().status = "renamed";
|
|
5864
|
+
} else if (line.startsWith(RENAME_TO)) current().path = line.slice(RENAME_TO.length);
|
|
5865
|
+
else if (INDEX_LINE.test(line)) {
|
|
5866
|
+
const [, oldId, newId] = INDEX_LINE.exec(line);
|
|
5867
|
+
const f = current();
|
|
5868
|
+
const oldBlob = blobOf(oldId);
|
|
5869
|
+
const newBlob = blobOf(newId);
|
|
5870
|
+
if (oldBlob) f.oldBlob = oldBlob;
|
|
5871
|
+
if (newBlob) f.newBlob = newBlob;
|
|
5872
|
+
} else if (line.startsWith("new file mode")) current().status = "added";
|
|
5873
|
+
else if (line.startsWith("deleted file mode")) current().status = "deleted";
|
|
5798
5874
|
}
|
|
5799
|
-
return
|
|
5875
|
+
return files.filter((f) => f.path);
|
|
5800
5876
|
}
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5877
|
+
var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
|
|
5878
|
+
var ANCHOR_ATTRIBUTES = [
|
|
5879
|
+
"id",
|
|
5880
|
+
"name",
|
|
5881
|
+
"aria-label",
|
|
5882
|
+
"placeholder",
|
|
5883
|
+
"alt",
|
|
5884
|
+
"title",
|
|
5885
|
+
"role",
|
|
5886
|
+
"value",
|
|
5887
|
+
"label"
|
|
5888
|
+
];
|
|
5889
|
+
var TRANSLATION_EXTENSIONS = /\.(json|ya?ml|properties|po|resx)$/i;
|
|
5890
|
+
var MARKUP_FILE = /\.(vue|svelte|html?|cshtml|razor|hbs|handlebars|jsx|tsx|astro|njk|liquid|erb|php)$/i;
|
|
5891
|
+
var MIN_LENGTH = 2;
|
|
5892
|
+
var MAX_LENGTH = 200;
|
|
5893
|
+
function expandBraces(glob) {
|
|
5894
|
+
const m = /\{([^{}]*)\}/.exec(glob);
|
|
5895
|
+
if (!m) return [glob];
|
|
5896
|
+
return m[1].split(",").flatMap((alt) => expandBraces(glob.slice(0, m.index) + alt + glob.slice(m.index + m[0].length)));
|
|
5804
5897
|
}
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
--shard <i/n> Keep only shard i of n, balanced by duration, lock-aware
|
|
5826
|
-
--fail-fast Order the least-reliable tests first (fail-fast)
|
|
5827
|
-
--base <ref> For "impact": the ref to diff the working tree against
|
|
5828
|
-
|
|
5829
|
-
Behavior:
|
|
5830
|
-
--strict Fail (exit 2) instead of falling back when unreachable
|
|
5831
|
-
--pkg-runner <cmd> Package runner for the printed command (default npx)
|
|
5832
|
-
--json Print the full resolution as JSON (select only)
|
|
5833
|
-
-h, --help Show this help
|
|
5834
|
-
|
|
5835
|
-
Exit codes: 0 ok, 1 the test run failed (run only), 2 could not resolve.
|
|
5836
|
-
`.trim();
|
|
5837
|
-
var IMPACT_KEY = "impact";
|
|
5838
|
-
var VALUE_FLAGS3 = /* @__PURE__ */ new Set([
|
|
5839
|
-
"--server-url",
|
|
5840
|
-
"--api-key",
|
|
5841
|
-
"--project",
|
|
5842
|
-
"--format",
|
|
5843
|
-
"--budget",
|
|
5844
|
-
"--shard",
|
|
5845
|
-
"--base",
|
|
5846
|
-
"--pkg-runner"
|
|
5847
|
-
]);
|
|
5848
|
-
function readOption7(argv, name) {
|
|
5849
|
-
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
5850
|
-
if (withEquals) return withEquals.slice(name.length + 1);
|
|
5851
|
-
const index = argv.indexOf(name);
|
|
5852
|
-
if (index === -1) return void 0;
|
|
5853
|
-
const value = argv[index + 1];
|
|
5854
|
-
return value && !value.startsWith("-") ? value : void 0;
|
|
5898
|
+
function pathGlobToRegExp(glob) {
|
|
5899
|
+
const parts = expandBraces(glob).map((g) => {
|
|
5900
|
+
let re = "";
|
|
5901
|
+
for (let i = 0; i < g.length; i++) {
|
|
5902
|
+
const c = g[i];
|
|
5903
|
+
if (c === "*") {
|
|
5904
|
+
if (g[i + 1] === "*") {
|
|
5905
|
+
i++;
|
|
5906
|
+
if (g[i + 1] === "/") {
|
|
5907
|
+
i++;
|
|
5908
|
+
re += "(?:.*/)?";
|
|
5909
|
+
} else re += ".*";
|
|
5910
|
+
} else re += "[^/]*";
|
|
5911
|
+
} else if (c === "?") re += "[^/]";
|
|
5912
|
+
else if ("\\^$.|+()[]{}".includes(c)) re += "\\" + c;
|
|
5913
|
+
else re += c;
|
|
5914
|
+
}
|
|
5915
|
+
return re;
|
|
5916
|
+
});
|
|
5917
|
+
return new RegExp(`^(?:${parts.join("|")})$`);
|
|
5855
5918
|
}
|
|
5856
|
-
function
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5919
|
+
function isTranslationFile(path25, globs = DEFAULT_TRANSLATION_GLOBS) {
|
|
5920
|
+
const posix = path25.replace(/\\/g, "/");
|
|
5921
|
+
if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
|
|
5922
|
+
return globs.some((g) => pathGlobToRegExp(g).test(posix));
|
|
5923
|
+
}
|
|
5924
|
+
function keepable(value) {
|
|
5925
|
+
const v = value.trim();
|
|
5926
|
+
if (v.length < MIN_LENGTH || v.length > MAX_LENGTH) return false;
|
|
5927
|
+
if (/^[-+]?[\d.,]+$/.test(v)) return false;
|
|
5928
|
+
return new RegExp("\\p{L}", "u").test(v);
|
|
5929
|
+
}
|
|
5930
|
+
function readQuoted(text, start) {
|
|
5931
|
+
const quote3 = text[start];
|
|
5932
|
+
let value = "";
|
|
5933
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
5934
|
+
const ch = text[i];
|
|
5935
|
+
if (ch === "\\" && i + 1 < text.length) {
|
|
5936
|
+
const next = text[i + 1];
|
|
5937
|
+
value += next === "n" ? "\n" : next === "t" ? " " : next;
|
|
5938
|
+
i++;
|
|
5861
5939
|
continue;
|
|
5862
5940
|
}
|
|
5863
|
-
return
|
|
5941
|
+
if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
|
|
5942
|
+
if (ch === quote3) return { value, end: i + 1 };
|
|
5943
|
+
value += ch;
|
|
5864
5944
|
}
|
|
5865
|
-
return
|
|
5945
|
+
return null;
|
|
5866
5946
|
}
|
|
5867
|
-
function
|
|
5868
|
-
|
|
5869
|
-
if (!match) return null;
|
|
5870
|
-
const value = Number(match[1]);
|
|
5871
|
-
const unit = match[2];
|
|
5872
|
-
const factor = unit === "h" ? 36e5 : unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1;
|
|
5873
|
-
const ms = Math.round(value * factor);
|
|
5874
|
-
return ms > 0 ? ms : null;
|
|
5947
|
+
function escapeRegExp(s) {
|
|
5948
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5875
5949
|
}
|
|
5876
|
-
function
|
|
5877
|
-
const
|
|
5878
|
-
const
|
|
5879
|
-
const
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
if (
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5950
|
+
function attributeTokens(text, attributes, line, spans) {
|
|
5951
|
+
const tokens = [];
|
|
5952
|
+
const names = attributes.map(escapeRegExp).join("|");
|
|
5953
|
+
const re = new RegExp(
|
|
5954
|
+
`(?<![\\w-])(?:v-bind:|:|\\[(?:attr\\.)?)?(["']?)(${names})\\1\\]?\\s*(=|:)\\s*(\\{\\s*)?(["'\`])`,
|
|
5955
|
+
"g"
|
|
5956
|
+
);
|
|
5957
|
+
let m;
|
|
5958
|
+
while ((m = re.exec(text)) !== null) {
|
|
5959
|
+
const bound = m[0].startsWith(":") || m[0].startsWith("v-bind:") || m[0].startsWith("[");
|
|
5960
|
+
const quoteAt = m.index + m[0].length - 1;
|
|
5961
|
+
const outer = readQuoted(text, quoteAt);
|
|
5962
|
+
if (!outer) continue;
|
|
5963
|
+
let value = outer.value;
|
|
5964
|
+
if (bound) {
|
|
5965
|
+
const inner = /^\s*(['"`])(.*)\1\s*$/.exec(value);
|
|
5966
|
+
if (!inner) continue;
|
|
5967
|
+
value = inner[2];
|
|
5968
|
+
}
|
|
5969
|
+
spans.push({ start: m.index, end: outer.end });
|
|
5970
|
+
re.lastIndex = outer.end;
|
|
5971
|
+
if (!keepable(value) || value.includes("${")) continue;
|
|
5972
|
+
const attribute = m[2];
|
|
5973
|
+
tokens.push({ kind: "attribute", group: `attribute:${attribute}`, value, attribute, line });
|
|
5893
5974
|
}
|
|
5894
|
-
return
|
|
5895
|
-
serverUrl,
|
|
5896
|
-
apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
5897
|
-
project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
|
|
5898
|
-
key,
|
|
5899
|
-
format: readOption7(own, "--format") ?? "args",
|
|
5900
|
-
budgetMs,
|
|
5901
|
-
shard,
|
|
5902
|
-
order: own.includes("--fail-fast") ? "failureLikelihood" : null,
|
|
5903
|
-
base: readOption7(own, "--base") ?? null,
|
|
5904
|
-
strict: own.includes("--strict"),
|
|
5905
|
-
pkgRunner: readOption7(own, "--pkg-runner") ?? "npx",
|
|
5906
|
-
json: own.includes("--json"),
|
|
5907
|
-
extra
|
|
5908
|
-
};
|
|
5909
|
-
}
|
|
5910
|
-
var CACHE_FILE = path21.join(".piwi", "selection-cache.json");
|
|
5911
|
-
function cacheKey(args) {
|
|
5912
|
-
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
5975
|
+
return tokens;
|
|
5913
5976
|
}
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5977
|
+
var TRANSLATION_CALL = /(?:(?<![\w$.])(?:\$?t|i18n\.t|translate|formatMessage)\s*\(\s*(?:\{\s*id\s*:\s*)?(['"`])([\w.:-]+)\1|\b\w*[Ll]ocalizer\s*\[\s*"([\w.:-]+)"\s*\])/g;
|
|
5978
|
+
function translationRefTokens(text, line, side, lookup, spans) {
|
|
5979
|
+
const tokens = [];
|
|
5980
|
+
for (const m of text.matchAll(TRANSLATION_CALL)) {
|
|
5981
|
+
spans.push({ start: m.index, end: m.index + m[0].length });
|
|
5982
|
+
const key = m[2] ?? m[3];
|
|
5983
|
+
const value = lookup?.(key, side);
|
|
5984
|
+
if (value === void 0 || !keepable(value)) continue;
|
|
5985
|
+
tokens.push({ kind: "translation", group: "translation-ref", value, key, line });
|
|
5920
5986
|
}
|
|
5987
|
+
return tokens;
|
|
5921
5988
|
}
|
|
5922
|
-
function
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5989
|
+
function inSpans(spans, at) {
|
|
5990
|
+
return spans.some((s) => at >= s.start && at < s.end);
|
|
5991
|
+
}
|
|
5992
|
+
var NON_TEXT_LITERAL = /^(?:\.{0,2}\/|[a-z][\w+.-]*:\/\/|#[\w-]+$|@\/|~\/)|\.(?:m?[jt]sx?|vue|svelte|css|scss|json|png|svg|jpe?g)$/i;
|
|
5993
|
+
function literalTokens(text, line, spans, markup) {
|
|
5994
|
+
const tokens = [];
|
|
5995
|
+
if (/^\s*(?:import\b|export\s+\*|\/\/|\*|\/\*|#include|using\s)/.test(text)) return tokens;
|
|
5996
|
+
for (let i = 0; i < text.length; i++) {
|
|
5997
|
+
const ch = text[i];
|
|
5998
|
+
if (ch !== '"' && ch !== "'" && ch !== "`") continue;
|
|
5999
|
+
const lit = readQuoted(text, i);
|
|
6000
|
+
if (!lit) {
|
|
6001
|
+
if (ch === "'") continue;
|
|
6002
|
+
break;
|
|
5928
6003
|
}
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
6004
|
+
const start = i;
|
|
6005
|
+
i = lit.end - 1;
|
|
6006
|
+
if (inSpans(spans, start)) continue;
|
|
6007
|
+
const before = text.slice(0, start);
|
|
6008
|
+
if (markup && /[\w:@.[\]-]=\s*$/.test(before)) continue;
|
|
6009
|
+
if (/\b(?:from|require|import)\s*\(?\s*$/.test(before)) continue;
|
|
6010
|
+
const value = lit.value;
|
|
6011
|
+
if (!keepable(value) || NON_TEXT_LITERAL.test(value)) continue;
|
|
6012
|
+
if (/^[\w-]+(?:\.[\w-]+)+$/.test(value) && !/\s/.test(value)) continue;
|
|
6013
|
+
tokens.push({ kind: "literal", group: "literal", value, line });
|
|
5933
6014
|
}
|
|
6015
|
+
return tokens;
|
|
5934
6016
|
}
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
} catch (e) {
|
|
5942
|
-
if (args.strict) throw e;
|
|
5943
|
-
const cached = readCache(args);
|
|
5944
|
-
if (cached) {
|
|
5945
|
-
console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
|
|
5946
|
-
return { resolution: cached, fromCache: true };
|
|
5947
|
-
}
|
|
5948
|
-
return { error: e };
|
|
5949
|
-
}
|
|
5950
|
-
}
|
|
5951
|
-
function printWarnings(resolution) {
|
|
5952
|
-
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
5953
|
-
}
|
|
5954
|
-
function gitChangedFiles(base) {
|
|
5955
|
-
const out = (0, import_node_child_process7.execFileSync)("git", ["diff", "--name-only", base], {
|
|
5956
|
-
encoding: "utf-8",
|
|
5957
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
5958
|
-
});
|
|
5959
|
-
return [
|
|
5960
|
-
...new Set(
|
|
5961
|
-
out.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
5962
|
-
)
|
|
5963
|
-
];
|
|
5964
|
-
}
|
|
5965
|
-
async function loadImpact(args, projectId) {
|
|
5966
|
-
const files = gitChangedFiles(args.base);
|
|
5967
|
-
return fetchImpact(args, projectId, files);
|
|
5968
|
-
}
|
|
5969
|
-
async function runSelectImpact(args) {
|
|
5970
|
-
let impact;
|
|
5971
|
-
try {
|
|
5972
|
-
const projectId = await resolveProjectId(args);
|
|
5973
|
-
impact = await loadImpact(args, projectId);
|
|
5974
|
-
} catch (e) {
|
|
5975
|
-
console.error(`piwi select: ${e.message}`);
|
|
5976
|
-
return EXIT_ERROR7;
|
|
5977
|
-
}
|
|
5978
|
-
printWarnings(impact);
|
|
5979
|
-
console.error(
|
|
5980
|
-
`piwi select: ${impact.impact.changedFiles} changed file(s) \u2192 ${impact.estimate.count} impacted test(s)${impact.impact.widened ? " (widened to full suite)" : ""}`
|
|
5981
|
-
);
|
|
5982
|
-
if (args.json) {
|
|
5983
|
-
console.log(JSON.stringify(impact, null, 2));
|
|
5984
|
-
return EXIT_OK6;
|
|
5985
|
-
}
|
|
5986
|
-
console.log(impact.impact.widened ? "" : impact.materialization.args.join(" "));
|
|
5987
|
-
return EXIT_OK6;
|
|
6017
|
+
var CODE_LINE = /^(?:import|export|const|let|var|function|return|if|else|for|while|switch|case|default|class|interface|type|await|async|new|throw|try|catch|public|private|protected|static|using|namespace|@\w+\s*\()\b/;
|
|
6018
|
+
function textSegment(raw, line) {
|
|
6019
|
+
const value = raw.replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
6020
|
+
if (!keepable(value)) return null;
|
|
6021
|
+
if (/[{}]|@[\w(]|<%|%>/.test(value)) return null;
|
|
6022
|
+
return { kind: "text", group: "text", value, line };
|
|
5988
6023
|
}
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
console.error(`piwi select: ${e.message}
|
|
5999
|
-
`);
|
|
6000
|
-
console.error(USAGE9);
|
|
6001
|
-
return EXIT_ERROR7;
|
|
6024
|
+
function textTokens(text, line, markup) {
|
|
6025
|
+
const tokens = [];
|
|
6026
|
+
if (!markup) return tokens;
|
|
6027
|
+
const trimmed = text.trim();
|
|
6028
|
+
if (!trimmed) return tokens;
|
|
6029
|
+
if (!/[<>]/.test(trimmed)) {
|
|
6030
|
+
if (CODE_LINE.test(trimmed) || /[;=(){}[\]"'`]|^\/\/|^\*|^\/\*|^<!--|[,:]$/.test(trimmed)) return tokens;
|
|
6031
|
+
const token = textSegment(trimmed, line);
|
|
6032
|
+
return token ? [token] : tokens;
|
|
6002
6033
|
}
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
return EXIT_ERROR7;
|
|
6007
|
-
}
|
|
6008
|
-
return runSelectImpact(args);
|
|
6034
|
+
for (const m of text.matchAll(/>([^<>]+)</g)) {
|
|
6035
|
+
const token = textSegment(m[1], line);
|
|
6036
|
+
if (token) tokens.push(token);
|
|
6009
6037
|
}
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
const
|
|
6013
|
-
|
|
6014
|
-
} catch (e) {
|
|
6015
|
-
console.error(`piwi select: ${e.message}`);
|
|
6016
|
-
return EXIT_ERROR7;
|
|
6038
|
+
const lead = /^\s*([^<>]+)<\//.exec(text);
|
|
6039
|
+
if (lead && !/[=;(){}]/.test(lead[1])) {
|
|
6040
|
+
const token = textSegment(lead[1], line);
|
|
6041
|
+
if (token) tokens.push(token);
|
|
6017
6042
|
}
|
|
6018
|
-
|
|
6019
|
-
if (
|
|
6020
|
-
|
|
6021
|
-
|
|
6043
|
+
const tail = />([^<>]+)$/.exec(text);
|
|
6044
|
+
if (tail && !/[=;(){}"']/.test(tail[1])) {
|
|
6045
|
+
const token = textSegment(tail[1], line);
|
|
6046
|
+
if (token) tokens.push(token);
|
|
6022
6047
|
}
|
|
6023
|
-
|
|
6024
|
-
else console.log(resolution.materialization.args.join(" "));
|
|
6025
|
-
return EXIT_OK6;
|
|
6048
|
+
return tokens;
|
|
6026
6049
|
}
|
|
6027
|
-
function
|
|
6028
|
-
const
|
|
6029
|
-
if (
|
|
6030
|
-
|
|
6050
|
+
function unquoteYaml(raw) {
|
|
6051
|
+
const v = raw.trim();
|
|
6052
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
6053
|
+
return v.slice(1, -1).replace(/\\"/g, '"').replace(/''/g, "'");
|
|
6054
|
+
}
|
|
6055
|
+
return v.replace(/\s+#.*$/, "");
|
|
6031
6056
|
}
|
|
6032
|
-
function
|
|
6033
|
-
return
|
|
6057
|
+
function decodeXml2(text) {
|
|
6058
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
6034
6059
|
}
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6060
|
+
function parseTranslationFile(path25, content) {
|
|
6061
|
+
const lines = content.split(/\r?\n/);
|
|
6062
|
+
const out = [];
|
|
6063
|
+
const ext = /\.([a-z]+)$/i.exec(path25)?.[1]?.toLowerCase();
|
|
6064
|
+
if (ext === "json") {
|
|
6065
|
+
const stack = [];
|
|
6066
|
+
const path26 = (key) => [...stack.map((f) => f.key), key].join(".");
|
|
6067
|
+
lines.forEach((text, i) => {
|
|
6068
|
+
const t = text.trim();
|
|
6069
|
+
const top = stack[stack.length - 1];
|
|
6070
|
+
const pair = /^"((?:[^"\\]|\\.)*)"\s*:\s*(.*)$/.exec(t);
|
|
6071
|
+
if (pair) {
|
|
6072
|
+
const key = JSON.parse(`"${pair[1]}"`);
|
|
6073
|
+
const rest = pair[2];
|
|
6074
|
+
const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
|
|
6075
|
+
if (str) out.push({ key: path26(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6076
|
+
else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
|
|
6077
|
+
return;
|
|
6078
|
+
}
|
|
6079
|
+
if (top?.array) {
|
|
6080
|
+
const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
|
|
6081
|
+
if (str) {
|
|
6082
|
+
out.push({ key: path26(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6083
|
+
return;
|
|
6084
|
+
}
|
|
6085
|
+
if (/^[[{]\s*$/.test(t)) {
|
|
6086
|
+
stack.push({ key: top.next++, array: t === "[", next: 0 });
|
|
6087
|
+
return;
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
if (/^[\]}]/.test(t)) stack.pop();
|
|
6091
|
+
});
|
|
6092
|
+
return out;
|
|
6064
6093
|
}
|
|
6065
|
-
if (
|
|
6066
|
-
|
|
6067
|
-
|
|
6094
|
+
if (ext === "yaml" || ext === "yml") {
|
|
6095
|
+
const stack = [];
|
|
6096
|
+
lines.forEach((text, i) => {
|
|
6097
|
+
const m = /^(\s*)(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:(?:\s+(.*))?$/.exec(text);
|
|
6098
|
+
if (!m) return;
|
|
6099
|
+
const indent = m[1].length;
|
|
6100
|
+
const key = (m[2] ?? m[3] ?? m[4]).trim();
|
|
6101
|
+
while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
6102
|
+
const raw = m[5]?.trim();
|
|
6103
|
+
if (!raw || raw.startsWith("#") || raw === "|" || raw === ">") {
|
|
6104
|
+
stack.push({ indent, key });
|
|
6105
|
+
return;
|
|
6106
|
+
}
|
|
6107
|
+
out.push({ key: [...stack.map((s) => s.key), key].join("."), value: unquoteYaml(raw), line: i + 1 });
|
|
6108
|
+
});
|
|
6109
|
+
return out;
|
|
6068
6110
|
}
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
...shardEnv(args)
|
|
6076
|
-
};
|
|
6077
|
-
console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)`);
|
|
6078
|
-
return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
|
|
6079
|
-
}
|
|
6080
|
-
async function runRun(argv, env = process.env) {
|
|
6081
|
-
if (argv.includes("-h") || argv.includes("--help")) {
|
|
6082
|
-
console.log(USAGE9);
|
|
6083
|
-
return EXIT_OK6;
|
|
6111
|
+
if (ext === "properties") {
|
|
6112
|
+
lines.forEach((text, i) => {
|
|
6113
|
+
const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
|
|
6114
|
+
if (m && !m[2].endsWith("\\")) out.push({ key: m[1], value: m[2], line: i + 1 });
|
|
6115
|
+
});
|
|
6116
|
+
return out;
|
|
6084
6117
|
}
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6118
|
+
if (ext === "po") {
|
|
6119
|
+
let msgid = null;
|
|
6120
|
+
lines.forEach((text, i) => {
|
|
6121
|
+
const id = /^msgid\s+"(.*)"\s*$/.exec(text);
|
|
6122
|
+
if (id) {
|
|
6123
|
+
msgid = id[1].replace(/\\"/g, '"');
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
const str = /^msgstr(?:\[\d+\])?\s+"(.*)"\s*$/.exec(text);
|
|
6127
|
+
if (str && msgid) out.push({ key: msgid, value: str[1].replace(/\\"/g, '"'), line: i + 1 });
|
|
6128
|
+
});
|
|
6129
|
+
return out;
|
|
6093
6130
|
}
|
|
6094
|
-
if (
|
|
6095
|
-
|
|
6096
|
-
|
|
6131
|
+
if (ext === "resx") {
|
|
6132
|
+
let name = null;
|
|
6133
|
+
lines.forEach((text, i) => {
|
|
6134
|
+
const data = /<data\s+name="([^"]+)"/.exec(text);
|
|
6135
|
+
if (data) name = decodeXml2(data[1]);
|
|
6136
|
+
const value = /<value>(.*)<\/value>/.exec(text);
|
|
6137
|
+
if (value && name) {
|
|
6138
|
+
out.push({ key: name, value: decodeXml2(value[1]), line: i + 1 });
|
|
6139
|
+
name = null;
|
|
6140
|
+
}
|
|
6141
|
+
});
|
|
6142
|
+
return out;
|
|
6097
6143
|
}
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6144
|
+
return out;
|
|
6145
|
+
}
|
|
6146
|
+
function translationLineToken(path25, text, line) {
|
|
6147
|
+
const ext = /\.([a-z]+)$/i.exec(path25)?.[1]?.toLowerCase();
|
|
6148
|
+
let key;
|
|
6149
|
+
let value;
|
|
6150
|
+
if (ext === "json") {
|
|
6151
|
+
const pair = /^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(text);
|
|
6152
|
+
if (pair) {
|
|
6153
|
+
key = JSON.parse(`"${pair[1]}"`);
|
|
6154
|
+
value = JSON.parse(`"${pair[2]}"`);
|
|
6155
|
+
} else {
|
|
6156
|
+
const item = /^\s*"((?:[^"\\]|\\.)*)"\s*,?\s*$/.exec(text);
|
|
6157
|
+
if (item) value = JSON.parse(`"${item[1]}"`);
|
|
6102
6158
|
}
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
} catch (e) {
|
|
6109
|
-
console.error(`piwi run: ${e.message}`);
|
|
6110
|
-
return EXIT_ERROR7;
|
|
6111
|
-
}
|
|
6112
|
-
if ("error" in outcome) {
|
|
6113
|
-
console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
|
|
6114
|
-
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
6115
|
-
}
|
|
6116
|
-
const { resolution } = outcome;
|
|
6117
|
-
printWarnings(resolution);
|
|
6118
|
-
if (resolution.estimate.count === 0) {
|
|
6119
|
-
console.error(`piwi run: "${args.key}" resolved to 0 tests`);
|
|
6120
|
-
return EXIT_ERROR7;
|
|
6121
|
-
}
|
|
6122
|
-
const runEnv = {
|
|
6123
|
-
...env,
|
|
6124
|
-
PIWI_SELECTION: resolution.key ?? args.key,
|
|
6125
|
-
PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
|
|
6126
|
-
PIWI_SELECTION_HASH: resolution.resolvedHash,
|
|
6127
|
-
PIWI_SELECTION_COUNT: String(resolution.estimate.count),
|
|
6128
|
-
...shardEnv(args)
|
|
6129
|
-
};
|
|
6130
|
-
console.error(
|
|
6131
|
-
`piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
|
|
6132
|
-
);
|
|
6133
|
-
return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
|
|
6134
|
-
}
|
|
6135
|
-
|
|
6136
|
-
// src/cli/preflight.ts
|
|
6137
|
-
var fs18 = __toESM(require("fs"));
|
|
6138
|
-
var path22 = __toESM(require("path"));
|
|
6139
|
-
var import_node_child_process8 = require("child_process");
|
|
6140
|
-
|
|
6141
|
-
// ../core/src/diff-anchors.ts
|
|
6142
|
-
var RENAME_FROM = `rename from `;
|
|
6143
|
-
var RENAME_TO = "rename to ";
|
|
6144
|
-
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
6145
|
-
function stripPrefix(path24) {
|
|
6146
|
-
const unquoted = path24.startsWith('"') && path24.endsWith('"') ? path24.slice(1, -1) : path24;
|
|
6147
|
-
return unquoted.replace(/^[ab]\//, "");
|
|
6148
|
-
}
|
|
6149
|
-
function parseUnifiedDiff(text) {
|
|
6150
|
-
const files = [];
|
|
6151
|
-
let file = null;
|
|
6152
|
-
let hunk = null;
|
|
6153
|
-
let oldLine = 0;
|
|
6154
|
-
let newLine = 0;
|
|
6155
|
-
let oldLeft = 0;
|
|
6156
|
-
let newLeft = 0;
|
|
6157
|
-
const current = () => {
|
|
6158
|
-
if (!file) {
|
|
6159
|
-
file = { path: "", status: "modified", hunks: [] };
|
|
6160
|
-
files.push(file);
|
|
6159
|
+
} else if (ext === "yaml" || ext === "yml") {
|
|
6160
|
+
const m = /^\s*(?:-\s+)?(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:\s+(.+)$/.exec(text);
|
|
6161
|
+
if (m) {
|
|
6162
|
+
key = (m[1] ?? m[2] ?? m[3]).trim();
|
|
6163
|
+
value = unquoteYaml(m[4]);
|
|
6161
6164
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
hunk.removed.push({ line: oldLine++, text: line.slice(1) });
|
|
6168
|
-
oldLeft--;
|
|
6169
|
-
} else if (line.startsWith("+") && newLeft > 0) {
|
|
6170
|
-
hunk.added.push({ line: newLine++, text: line.slice(1) });
|
|
6171
|
-
newLeft--;
|
|
6172
|
-
} else if ((line.startsWith(" ") || line === "") && oldLeft > 0 && newLeft > 0) {
|
|
6173
|
-
oldLine++;
|
|
6174
|
-
newLine++;
|
|
6175
|
-
oldLeft--;
|
|
6176
|
-
newLeft--;
|
|
6177
|
-
} else if (!line.startsWith("\\")) hunk = null;
|
|
6178
|
-
if (hunk && oldLeft === 0 && newLeft === 0) hunk = null;
|
|
6179
|
-
if (hunk || !line.startsWith("@@")) continue;
|
|
6165
|
+
} else if (ext === "properties") {
|
|
6166
|
+
const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
|
|
6167
|
+
if (m) {
|
|
6168
|
+
key = m[1];
|
|
6169
|
+
value = m[2];
|
|
6180
6170
|
}
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
newLeft = header[4] === void 0 ? 1 : Number(header[4]);
|
|
6187
|
-
hunk = { oldStart: oldLine, newStart: newLine, removed: [], added: [] };
|
|
6188
|
-
current().hunks.push(hunk);
|
|
6189
|
-
if (oldLeft === 0 && newLeft === 0) hunk = null;
|
|
6190
|
-
continue;
|
|
6171
|
+
} else if (ext === "po") {
|
|
6172
|
+
const m = /^msg(id|str(?:\[\d+\])?)\s+"(.*)"\s*$/.exec(text);
|
|
6173
|
+
if (m) {
|
|
6174
|
+
value = m[2].replace(/\\"/g, '"');
|
|
6175
|
+
if (m[1] === "id") key = value;
|
|
6191
6176
|
}
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
files.push(file);
|
|
6196
|
-
} else if (line.startsWith("--- ")) {
|
|
6197
|
-
const f = file && !file.hunks.length ? file : (file = null, current());
|
|
6198
|
-
const old = line.slice(4).trim();
|
|
6199
|
-
if (old === "/dev/null") f.status = "added";
|
|
6200
|
-
else if (!f.path) f.path = stripPrefix(old);
|
|
6201
|
-
} else if (line.startsWith("+++ ")) {
|
|
6202
|
-
const next = line.slice(4).trim();
|
|
6203
|
-
if (next === "/dev/null") current().status = "deleted";
|
|
6204
|
-
else current().path = stripPrefix(next);
|
|
6205
|
-
} else if (line.startsWith(RENAME_FROM)) {
|
|
6206
|
-
current().oldPath = line.slice(RENAME_FROM.length);
|
|
6207
|
-
current().status = "renamed";
|
|
6208
|
-
} else if (line.startsWith(RENAME_TO)) current().path = line.slice(RENAME_TO.length);
|
|
6209
|
-
else if (line.startsWith("new file mode")) current().status = "added";
|
|
6210
|
-
else if (line.startsWith("deleted file mode")) current().status = "deleted";
|
|
6177
|
+
} else if (ext === "resx") {
|
|
6178
|
+
const m = /<value>(.*)<\/value>/.exec(text);
|
|
6179
|
+
if (m) value = decodeXml2(m[1]);
|
|
6211
6180
|
}
|
|
6212
|
-
|
|
6181
|
+
if (value === void 0 || !keepable(value)) return null;
|
|
6182
|
+
return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
|
|
6213
6183
|
}
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
"value",
|
|
6224
|
-
"label"
|
|
6225
|
-
];
|
|
6226
|
-
var TRANSLATION_EXTENSIONS = /\.(json|ya?ml|properties|po|resx)$/i;
|
|
6227
|
-
var MARKUP_FILE = /\.(vue|svelte|html?|cshtml|razor|hbs|handlebars|jsx|tsx|astro|njk|liquid|erb|php)$/i;
|
|
6228
|
-
var MIN_LENGTH = 2;
|
|
6229
|
-
var MAX_LENGTH = 200;
|
|
6230
|
-
function expandBraces(glob) {
|
|
6231
|
-
const m = /\{([^{}]*)\}/.exec(glob);
|
|
6232
|
-
if (!m) return [glob];
|
|
6233
|
-
return m[1].split(",").flatMap((alt) => expandBraces(glob.slice(0, m.index) + alt + glob.slice(m.index + m[0].length)));
|
|
6184
|
+
function lineTokens(path25, text, line, side, attributes, options) {
|
|
6185
|
+
const spans = [];
|
|
6186
|
+
const markup = MARKUP_FILE.test(path25);
|
|
6187
|
+
return [
|
|
6188
|
+
...attributeTokens(text, attributes, line, spans),
|
|
6189
|
+
...translationRefTokens(text, line, side, options.translations, spans),
|
|
6190
|
+
...literalTokens(text, line, spans, markup),
|
|
6191
|
+
...textTokens(text, line, markup)
|
|
6192
|
+
];
|
|
6234
6193
|
}
|
|
6235
|
-
function
|
|
6236
|
-
const
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
const c = g[i];
|
|
6240
|
-
if (c === "*") {
|
|
6241
|
-
if (g[i + 1] === "*") {
|
|
6242
|
-
i++;
|
|
6243
|
-
if (g[i + 1] === "/") {
|
|
6244
|
-
i++;
|
|
6245
|
-
re += "(?:.*/)?";
|
|
6246
|
-
} else re += ".*";
|
|
6247
|
-
} else re += "[^/]*";
|
|
6248
|
-
} else if (c === "?") re += "[^/]";
|
|
6249
|
-
else if ("\\^$.|+()[]{}".includes(c)) re += "\\" + c;
|
|
6250
|
-
else re += c;
|
|
6251
|
-
}
|
|
6252
|
-
return re;
|
|
6253
|
-
});
|
|
6254
|
-
return new RegExp(`^(?:${parts.join("|")})$`);
|
|
6194
|
+
function keysByLine(path25, side, options) {
|
|
6195
|
+
const content = options.readFile?.(path25, side);
|
|
6196
|
+
if (content === void 0) return null;
|
|
6197
|
+
return new Map(parseTranslationFile(path25, content).map((e) => [e.line, e.key]));
|
|
6255
6198
|
}
|
|
6256
|
-
function
|
|
6257
|
-
|
|
6258
|
-
if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
|
|
6259
|
-
return globs.some((g) => pathGlobToRegExp(g).test(posix));
|
|
6199
|
+
function tokenId(t) {
|
|
6200
|
+
return `${t.group}\0${t.value}`;
|
|
6260
6201
|
}
|
|
6261
|
-
function
|
|
6262
|
-
const
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6202
|
+
function hunkAnchors(file, hunk, removed, added) {
|
|
6203
|
+
const addedLeft = /* @__PURE__ */ new Map();
|
|
6204
|
+
for (const t of added) addedLeft.set(tokenId(t), [...addedLeft.get(tokenId(t)) ?? [], t]);
|
|
6205
|
+
const gone = [];
|
|
6206
|
+
for (const t of removed) {
|
|
6207
|
+
const same = addedLeft.get(tokenId(t));
|
|
6208
|
+
if (same?.length) same.shift();
|
|
6209
|
+
else gone.push(t);
|
|
6210
|
+
}
|
|
6211
|
+
const fresh = [...addedLeft.values()].flat();
|
|
6212
|
+
const anchors = [];
|
|
6213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6214
|
+
for (const t of gone) {
|
|
6215
|
+
const peers = gone.filter((g) => g.group === t.group);
|
|
6216
|
+
const replacements = fresh.filter((a) => a.group === t.group);
|
|
6217
|
+
const rename = peers.length === 1 && replacements.length === 1 ? replacements[0] : void 0;
|
|
6218
|
+
const anchor = {
|
|
6219
|
+
file,
|
|
6220
|
+
line: rename ? rename.line : Math.max(1, hunk.newStart),
|
|
6221
|
+
oldLine: t.line,
|
|
6222
|
+
kind: t.kind,
|
|
6223
|
+
before: t.value
|
|
6224
|
+
};
|
|
6225
|
+
if (t.attribute) anchor.attribute = t.attribute;
|
|
6226
|
+
if (t.key) anchor.key = t.key;
|
|
6227
|
+
if (rename) anchor.after = rename.value;
|
|
6228
|
+
const id = `${anchor.kind}\0${anchor.attribute ?? ""}\0${anchor.key ?? ""}\0${anchor.before}\0${anchor.after ?? ""}`;
|
|
6229
|
+
if (seen.has(id)) continue;
|
|
6230
|
+
seen.add(id);
|
|
6231
|
+
anchors.push(anchor);
|
|
6232
|
+
}
|
|
6233
|
+
return anchors;
|
|
6266
6234
|
}
|
|
6267
|
-
function
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
if (
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6235
|
+
function extractDiffAnchors(files, options = {}) {
|
|
6236
|
+
const attributes = [.../* @__PURE__ */ new Set([...options.testIdAttributes ?? ["data-testid"], ...ANCHOR_ATTRIBUTES])];
|
|
6237
|
+
const globs = options.translationGlobs ?? DEFAULT_TRANSLATION_GLOBS;
|
|
6238
|
+
const anchors = [];
|
|
6239
|
+
for (const file of files) {
|
|
6240
|
+
if (options.isTestFile?.(file.path)) continue;
|
|
6241
|
+
const translation = isTranslationFile(file.path, globs);
|
|
6242
|
+
let oldKeys;
|
|
6243
|
+
let newKeys;
|
|
6244
|
+
for (const hunk of file.hunks) {
|
|
6245
|
+
let removed;
|
|
6246
|
+
let added;
|
|
6247
|
+
if (translation) {
|
|
6248
|
+
oldKeys ??= keysByLine(file.oldPath ?? file.path, "old", options);
|
|
6249
|
+
newKeys ??= keysByLine(file.path, "new", options);
|
|
6250
|
+
const read = (lines, keys) => lines.flatMap((l) => {
|
|
6251
|
+
const token = translationLineToken(file.path, l.text, l.line);
|
|
6252
|
+
if (!token) return [];
|
|
6253
|
+
const full = keys?.get(l.line);
|
|
6254
|
+
if (full) {
|
|
6255
|
+
token.key = full;
|
|
6256
|
+
token.group = `translation:${full}`;
|
|
6257
|
+
}
|
|
6258
|
+
return [token];
|
|
6259
|
+
});
|
|
6260
|
+
removed = read(hunk.removed, oldKeys);
|
|
6261
|
+
added = read(hunk.added, newKeys);
|
|
6262
|
+
} else {
|
|
6263
|
+
removed = hunk.removed.flatMap((l) => lineTokens(file.path, l.text, l.line, "old", attributes, options));
|
|
6264
|
+
added = hunk.added.flatMap((l) => lineTokens(file.path, l.text, l.line, "new", attributes, options));
|
|
6265
|
+
}
|
|
6266
|
+
anchors.push(...hunkAnchors(file.path, hunk, removed, added));
|
|
6277
6267
|
}
|
|
6278
|
-
if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
|
|
6279
|
-
if (ch === quote3) return { value, end: i + 1 };
|
|
6280
|
-
value += ch;
|
|
6281
6268
|
}
|
|
6282
|
-
return
|
|
6269
|
+
return anchors;
|
|
6283
6270
|
}
|
|
6284
|
-
|
|
6285
|
-
|
|
6271
|
+
|
|
6272
|
+
// ../core/src/locator-text-match.ts
|
|
6273
|
+
function normalizeWhiteSpace(text) {
|
|
6274
|
+
return text.replace(/[]/g, "").trim().replace(/\s+/g, " ");
|
|
6286
6275
|
}
|
|
6287
|
-
function
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
`(?<![\\w-])(?:v-bind:|:|\\[(?:attr\\.)?)?(["']?)(${names})\\1\\]?\\s*(=|:)\\s*(\\{\\s*)?(["'\`])`,
|
|
6292
|
-
"g"
|
|
6293
|
-
);
|
|
6294
|
-
let m;
|
|
6295
|
-
while ((m = re.exec(text)) !== null) {
|
|
6296
|
-
const bound = m[0].startsWith(":") || m[0].startsWith("v-bind:") || m[0].startsWith("[");
|
|
6297
|
-
const quoteAt = m.index + m[0].length - 1;
|
|
6298
|
-
const outer = readQuoted(text, quoteAt);
|
|
6299
|
-
if (!outer) continue;
|
|
6300
|
-
let value = outer.value;
|
|
6301
|
-
if (bound) {
|
|
6302
|
-
const inner = /^\s*(['"`])(.*)\1\s*$/.exec(value);
|
|
6303
|
-
if (!inner) continue;
|
|
6304
|
-
value = inner[2];
|
|
6305
|
-
}
|
|
6306
|
-
spans.push({ start: m.index, end: outer.end });
|
|
6307
|
-
re.lastIndex = outer.end;
|
|
6308
|
-
if (!keepable(value) || value.includes("${")) continue;
|
|
6309
|
-
const attribute = m[2];
|
|
6310
|
-
tokens.push({ kind: "attribute", group: `attribute:${attribute}`, value, attribute, line });
|
|
6276
|
+
function textMatcher(value, exact) {
|
|
6277
|
+
if (typeof value !== "string") {
|
|
6278
|
+
const re = value;
|
|
6279
|
+
return { matcher: (text) => re.test(text.full), kind: "regex" };
|
|
6311
6280
|
}
|
|
6312
|
-
|
|
6281
|
+
const needle = normalizeWhiteSpace(value);
|
|
6282
|
+
if (exact) return { matcher: (text) => text.normalized === needle, kind: "strict" };
|
|
6283
|
+
const lower = needle.toLowerCase();
|
|
6284
|
+
return { matcher: (text) => (text.lower ??= text.normalized.toLowerCase()).includes(lower), kind: "lax" };
|
|
6313
6285
|
}
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
const key = m[2] ?? m[3];
|
|
6320
|
-
const value = lookup?.(key, side);
|
|
6321
|
-
if (value === void 0 || !keepable(value)) continue;
|
|
6322
|
-
tokens.push({ kind: "translation", group: "translation-ref", value, key, line });
|
|
6323
|
-
}
|
|
6324
|
-
return tokens;
|
|
6286
|
+
function attributeMatcher(value, exact) {
|
|
6287
|
+
if (typeof value !== "string") return (actual) => !!actual.match(value);
|
|
6288
|
+
if (exact) return (actual) => actual === value;
|
|
6289
|
+
const lower = value.toLowerCase();
|
|
6290
|
+
return (actual) => actual.toLowerCase().includes(lower);
|
|
6325
6291
|
}
|
|
6326
|
-
function
|
|
6327
|
-
|
|
6292
|
+
function nameMatcher(expected, exact) {
|
|
6293
|
+
if (typeof expected !== "string") return (name) => !!name.match(expected);
|
|
6294
|
+
const wanted = normalizeWhiteSpace(expected);
|
|
6295
|
+
if (exact) return (name) => name === wanted;
|
|
6296
|
+
const upper = wanted.toUpperCase();
|
|
6297
|
+
return (name) => name.toUpperCase().includes(upper);
|
|
6328
6298
|
}
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
const tokens = [];
|
|
6332
|
-
if (/^\s*(?:import\b|export\s+\*|\/\/|\*|\/\*|#include|using\s)/.test(text)) return tokens;
|
|
6333
|
-
for (let i = 0; i < text.length; i++) {
|
|
6334
|
-
const ch = text[i];
|
|
6335
|
-
if (ch !== '"' && ch !== "'" && ch !== "`") continue;
|
|
6336
|
-
const lit = readQuoted(text, i);
|
|
6337
|
-
if (!lit) {
|
|
6338
|
-
if (ch === "'") continue;
|
|
6339
|
-
break;
|
|
6340
|
-
}
|
|
6341
|
-
const start = i;
|
|
6342
|
-
i = lit.end - 1;
|
|
6343
|
-
if (inSpans(spans, start)) continue;
|
|
6344
|
-
const before = text.slice(0, start);
|
|
6345
|
-
if (markup && /[\w:@.[\]-]=\s*$/.test(before)) continue;
|
|
6346
|
-
if (/\b(?:from|require|import)\s*\(?\s*$/.test(before)) continue;
|
|
6347
|
-
const value = lit.value;
|
|
6348
|
-
if (!keepable(value) || NON_TEXT_LITERAL.test(value)) continue;
|
|
6349
|
-
if (/^[\w-]+(?:\.[\w-]+)+$/.test(value) && !/\s/.test(value)) continue;
|
|
6350
|
-
tokens.push({ kind: "literal", group: "literal", value, line });
|
|
6351
|
-
}
|
|
6352
|
-
return tokens;
|
|
6299
|
+
function textMatches(value, exact, text) {
|
|
6300
|
+
return textMatcher(value, exact).matcher({ full: text, normalized: normalizeWhiteSpace(text) });
|
|
6353
6301
|
}
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
const value = raw.replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
6357
|
-
if (!keepable(value)) return null;
|
|
6358
|
-
if (/[{}]|@[\w(]|<%|%>/.test(value)) return null;
|
|
6359
|
-
return { kind: "text", group: "text", value, line };
|
|
6302
|
+
function nameMatches(value, exact, name) {
|
|
6303
|
+
return nameMatcher(value, exact)(normalizeWhiteSpace(name));
|
|
6360
6304
|
}
|
|
6361
|
-
function
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6305
|
+
function attributeMatches(value, exact, actual) {
|
|
6306
|
+
return attributeMatcher(value, exact)(actual);
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
// ../core/src/locator-break.ts
|
|
6310
|
+
var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "translation", "literal"]);
|
|
6311
|
+
function isText(anchor) {
|
|
6312
|
+
return TEXT_KINDS.has(anchor.kind);
|
|
6313
|
+
}
|
|
6314
|
+
function hasAttribute(anchor, names) {
|
|
6315
|
+
return anchor.kind === "attribute" && !!anchor.attribute && names.includes(anchor.attribute);
|
|
6316
|
+
}
|
|
6317
|
+
function toRegExp(arg) {
|
|
6318
|
+
try {
|
|
6319
|
+
return new RegExp(arg.source, arg.flags.replace(/[gy]/g, ""));
|
|
6320
|
+
} catch {
|
|
6321
|
+
return null;
|
|
6370
6322
|
}
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6323
|
+
}
|
|
6324
|
+
function optionsOf(arg) {
|
|
6325
|
+
return new Map(arg?.type === "object" ? arg.entries : []);
|
|
6326
|
+
}
|
|
6327
|
+
function isExact(opts) {
|
|
6328
|
+
const exact = opts.get("exact");
|
|
6329
|
+
return exact?.type === "boolean" && exact.value;
|
|
6330
|
+
}
|
|
6331
|
+
function escapeRegExp2(s) {
|
|
6332
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6333
|
+
}
|
|
6334
|
+
function renameValue(value, before, after, exact) {
|
|
6335
|
+
const v = normalizeWhiteSpace(value);
|
|
6336
|
+
const b = normalizeWhiteSpace(before);
|
|
6337
|
+
if (exact ? v === b : v.toLowerCase() === b.toLowerCase()) return after;
|
|
6338
|
+
const re = new RegExp(escapeRegExp2(b).replace(/ /g, "\\s+"), exact ? "" : "i");
|
|
6339
|
+
if (!re.test(value)) return null;
|
|
6340
|
+
return value.replace(re, () => after);
|
|
6341
|
+
}
|
|
6342
|
+
function withArg(call, index, arg) {
|
|
6343
|
+
return { method: call.method, args: call.args.map((a, i) => i === index ? arg : a) };
|
|
6344
|
+
}
|
|
6345
|
+
function withOption(call, index, key, arg) {
|
|
6346
|
+
const obj = call.args[index];
|
|
6347
|
+
if (obj?.type !== "object") return call;
|
|
6348
|
+
return withArg(call, index, {
|
|
6349
|
+
type: "object",
|
|
6350
|
+
entries: obj.entries.map(([k, v]) => [k, k === key ? arg : v])
|
|
6351
|
+
});
|
|
6352
|
+
}
|
|
6353
|
+
function stringSlot(arg, exact, rule, accepts, replace) {
|
|
6354
|
+
if (arg?.type === "regex") {
|
|
6355
|
+
const re = toRegExp(arg);
|
|
6356
|
+
return re ? { value: re, exact, rule, accepts } : null;
|
|
6374
6357
|
}
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6358
|
+
if (arg?.type !== "string") return null;
|
|
6359
|
+
const value = arg.value;
|
|
6360
|
+
return {
|
|
6361
|
+
value,
|
|
6362
|
+
exact,
|
|
6363
|
+
rule,
|
|
6364
|
+
accepts,
|
|
6365
|
+
rewrite: (after, before) => replace({ type: "string", value: renameValue(value, before, after, exact) ?? value }),
|
|
6366
|
+
literal: (after, before) => [value, renameValue(value, before, after, exact) ?? value]
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
function cssSlots(selector, testIds, replace) {
|
|
6370
|
+
const slots = [];
|
|
6371
|
+
const slot = (value, names, render, at, len) => {
|
|
6372
|
+
const edited = (after) => selector.slice(0, at) + render(after) + selector.slice(at + len);
|
|
6373
|
+
slots.push({
|
|
6374
|
+
value,
|
|
6375
|
+
exact: true,
|
|
6376
|
+
rule: "exact",
|
|
6377
|
+
accepts: (a) => hasAttribute(a, names),
|
|
6378
|
+
rewrite: (after) => replace(edited(after)),
|
|
6379
|
+
literal: (after) => [selector, edited(after)]
|
|
6380
|
+
});
|
|
6381
|
+
};
|
|
6382
|
+
for (const m of selector.matchAll(/#((?:[\w-]|\\.)+)/g)) {
|
|
6383
|
+
slot(
|
|
6384
|
+
m[1].replace(/\\(.)/g, "$1"),
|
|
6385
|
+
["id"],
|
|
6386
|
+
(v) => `#${v.replace(/[^\w-]/g, (c) => `\\${c}`)}`,
|
|
6387
|
+
m.index,
|
|
6388
|
+
m[0].length
|
|
6389
|
+
);
|
|
6379
6390
|
}
|
|
6380
|
-
const
|
|
6381
|
-
|
|
6382
|
-
const
|
|
6383
|
-
if (
|
|
6391
|
+
for (const m of selector.matchAll(/\[\s*([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*\]/g)) {
|
|
6392
|
+
const attr = m[1];
|
|
6393
|
+
const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
|
|
6394
|
+
if (!names) continue;
|
|
6395
|
+
const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
|
|
6396
|
+
const value = m[2] ?? m[3] ?? m[4];
|
|
6397
|
+
slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
|
|
6384
6398
|
}
|
|
6385
|
-
return
|
|
6399
|
+
return slots;
|
|
6386
6400
|
}
|
|
6387
|
-
function
|
|
6388
|
-
const
|
|
6389
|
-
|
|
6390
|
-
|
|
6401
|
+
function directSlots(call, testIds) {
|
|
6402
|
+
const first = call.args[0];
|
|
6403
|
+
const opts = optionsOf(call.args[1]);
|
|
6404
|
+
const exact = isExact(opts);
|
|
6405
|
+
const slots = [];
|
|
6406
|
+
const argSlot = (rule, accepts, ex = exact) => stringSlot(first, ex, rule, accepts, (next) => withArg(call, 0, next));
|
|
6407
|
+
const hasTextSlot = (index) => {
|
|
6408
|
+
const hasText = optionsOf(call.args[index]).get("hasText");
|
|
6409
|
+
if (!hasText) return null;
|
|
6410
|
+
return stringSlot(hasText, false, "text", isText, (next) => withOption(call, index, "hasText", next));
|
|
6411
|
+
};
|
|
6412
|
+
const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
|
|
6413
|
+
switch (call.method) {
|
|
6414
|
+
case "getByTestId":
|
|
6415
|
+
slots.push(argSlot("exact", (a) => hasAttribute(a, testIds), true));
|
|
6416
|
+
break;
|
|
6417
|
+
case "getByRole":
|
|
6418
|
+
slots.push(
|
|
6419
|
+
stringSlot(
|
|
6420
|
+
opts.get("name"),
|
|
6421
|
+
exact,
|
|
6422
|
+
"name",
|
|
6423
|
+
(a) => isText(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
|
|
6424
|
+
(next) => withOption(call, 1, "name", next)
|
|
6425
|
+
)
|
|
6426
|
+
);
|
|
6427
|
+
break;
|
|
6428
|
+
case "getByText":
|
|
6429
|
+
slots.push(argSlot("text", isText));
|
|
6430
|
+
break;
|
|
6431
|
+
case "getByLabel":
|
|
6432
|
+
slots.push(argSlot("text", (a) => isText(a) || hasAttribute(a, ["aria-label"])));
|
|
6433
|
+
break;
|
|
6434
|
+
case "getByPlaceholder":
|
|
6435
|
+
slots.push(argSlot("text", textOr("placeholder")));
|
|
6436
|
+
break;
|
|
6437
|
+
case "getByAltText":
|
|
6438
|
+
slots.push(argSlot("text", textOr("alt")));
|
|
6439
|
+
break;
|
|
6440
|
+
case "getByTitle":
|
|
6441
|
+
slots.push(argSlot("text", textOr("title")));
|
|
6442
|
+
break;
|
|
6443
|
+
case "locator":
|
|
6444
|
+
if (first?.type === "string") {
|
|
6445
|
+
slots.push(...cssSlots(first.value, testIds, (next) => withArg(call, 0, { type: "string", value: next })));
|
|
6446
|
+
}
|
|
6447
|
+
slots.push(hasTextSlot(1));
|
|
6448
|
+
break;
|
|
6449
|
+
case "filter":
|
|
6450
|
+
slots.push(hasTextSlot(0));
|
|
6451
|
+
break;
|
|
6391
6452
|
}
|
|
6392
|
-
return
|
|
6393
|
-
}
|
|
6394
|
-
function decodeXml2(text) {
|
|
6395
|
-
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
6453
|
+
return slots.filter((slot) => !!slot);
|
|
6396
6454
|
}
|
|
6397
|
-
function
|
|
6398
|
-
const lines = content.split(/\r?\n/);
|
|
6455
|
+
function nestedChains(call) {
|
|
6399
6456
|
const out = [];
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
const pair = /^"((?:[^"\\]|\\.)*)"\s*:\s*(.*)$/.exec(t);
|
|
6408
|
-
if (pair) {
|
|
6409
|
-
const key = JSON.parse(`"${pair[1]}"`);
|
|
6410
|
-
const rest = pair[2];
|
|
6411
|
-
const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
|
|
6412
|
-
if (str) out.push({ key: path25(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6413
|
-
else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
|
|
6414
|
-
return;
|
|
6415
|
-
}
|
|
6416
|
-
if (top?.array) {
|
|
6417
|
-
const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
|
|
6418
|
-
if (str) {
|
|
6419
|
-
out.push({ key: path25(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6420
|
-
return;
|
|
6421
|
-
}
|
|
6422
|
-
if (/^[[{]\s*$/.test(t)) {
|
|
6423
|
-
stack.push({ key: top.next++, array: t === "[", next: 0 });
|
|
6424
|
-
return;
|
|
6457
|
+
call.args.forEach((arg, index) => {
|
|
6458
|
+
if (arg.type === "chain")
|
|
6459
|
+
out.push({ chain: arg.chain, put: (c) => withArg(call, index, { type: "chain", chain: c }) });
|
|
6460
|
+
if (arg.type === "object") {
|
|
6461
|
+
for (const [key, v] of arg.entries) {
|
|
6462
|
+
if (v.type === "chain") {
|
|
6463
|
+
out.push({ chain: v.chain, put: (c) => withOption(call, index, key, { type: "chain", chain: c }) });
|
|
6425
6464
|
}
|
|
6426
6465
|
}
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
const
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6466
|
+
}
|
|
6467
|
+
});
|
|
6468
|
+
return out;
|
|
6469
|
+
}
|
|
6470
|
+
function chainSlots(chain, testIds) {
|
|
6471
|
+
const out = [];
|
|
6472
|
+
chain.calls.forEach((call, i) => {
|
|
6473
|
+
const replaceAt = (next) => ({ calls: chain.calls.map((c, j) => j === i ? next : c) });
|
|
6474
|
+
for (const slot of directSlots(call, testIds)) {
|
|
6475
|
+
const rewrite = slot.rewrite;
|
|
6476
|
+
out.push({ ...slot, rewrite: rewrite && ((after, before) => replaceAt(rewrite(after, before))) });
|
|
6477
|
+
}
|
|
6478
|
+
for (const n of nestedChains(call)) {
|
|
6479
|
+
for (const slot of chainSlots(n.chain, testIds)) {
|
|
6480
|
+
const rewrite = slot.rewrite;
|
|
6481
|
+
out.push({ ...slot, rewrite: rewrite && ((after, before) => replaceAt(n.put(rewrite(after, before)))) });
|
|
6443
6482
|
}
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6483
|
+
}
|
|
6484
|
+
});
|
|
6485
|
+
return out;
|
|
6486
|
+
}
|
|
6487
|
+
function slotMatches(slot, text) {
|
|
6488
|
+
switch (slot.rule) {
|
|
6489
|
+
case "exact":
|
|
6490
|
+
return attributeMatches(slot.value, true, text);
|
|
6491
|
+
case "name":
|
|
6492
|
+
return nameMatches(slot.value, slot.exact, text);
|
|
6493
|
+
case "text":
|
|
6494
|
+
return textMatches(slot.value, slot.exact, text);
|
|
6447
6495
|
}
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6496
|
+
}
|
|
6497
|
+
var filePath = (path25) => path25.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6498
|
+
function sameFilePath(a, b) {
|
|
6499
|
+
const x = filePath(a);
|
|
6500
|
+
const y = filePath(b);
|
|
6501
|
+
return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
|
|
6502
|
+
}
|
|
6503
|
+
function reachOfIndex(index) {
|
|
6504
|
+
if (!index.reach.some((r) => r.origin === "client")) return void 0;
|
|
6505
|
+
const filesOf = /* @__PURE__ */ new Map();
|
|
6506
|
+
for (const r of index.reach) {
|
|
6507
|
+
const file = index.files[r.file];
|
|
6508
|
+
if (file === void 0) continue;
|
|
6509
|
+
for (const t of r.tests) {
|
|
6510
|
+
const id = index.tests[t]?.id;
|
|
6511
|
+
if (id === void 0) continue;
|
|
6512
|
+
const files = filesOf.get(id);
|
|
6513
|
+
if (files) files.push(file);
|
|
6514
|
+
else filesOf.set(id, [file]);
|
|
6515
|
+
}
|
|
6454
6516
|
}
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6517
|
+
return (testId, file) => (filesOf.get(testId) ?? []).some((reached) => sameFilePath(reached, file));
|
|
6518
|
+
}
|
|
6519
|
+
function callSiteFile(callSite) {
|
|
6520
|
+
return callSite.replace(/:\d+(?::\d+)?$/, "");
|
|
6521
|
+
}
|
|
6522
|
+
function callSiteLine(callSite) {
|
|
6523
|
+
const m = /:(\d+)(?::\d+)?$/.exec(callSite);
|
|
6524
|
+
return m ? Number(m[1]) : null;
|
|
6525
|
+
}
|
|
6526
|
+
function locatorCallSiteFiles(index) {
|
|
6527
|
+
const files = /* @__PURE__ */ new Set();
|
|
6528
|
+
for (const entry of index.locators) {
|
|
6529
|
+
for (const use of entry.uses) for (const site of use.callSites) files.add(callSiteFile(site));
|
|
6467
6530
|
}
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6531
|
+
return files;
|
|
6532
|
+
}
|
|
6533
|
+
var CONFIDENCE_RANK = { likely: 0, possible: 1 };
|
|
6534
|
+
function predictLocatorBreaks(anchors, index, options = {}) {
|
|
6535
|
+
const testIds = options.testIdAttributes ?? index.testIdAttributes ?? ["data-testid"];
|
|
6536
|
+
const siteFiles = [...locatorCallSiteFiles(index)];
|
|
6537
|
+
const live = anchors.filter((a) => !siteFiles.some((f) => sameFilePath(f, a.file)));
|
|
6538
|
+
if (!live.length) return [];
|
|
6539
|
+
const breaks = [];
|
|
6540
|
+
for (const entry of index.locators) {
|
|
6541
|
+
const chain = tryParseLocatorChain(entry.locator);
|
|
6542
|
+
if (!chain) continue;
|
|
6543
|
+
const slots = chainSlots(chain, testIds);
|
|
6544
|
+
if (!slots.length) continue;
|
|
6545
|
+
for (const anchor of live) {
|
|
6546
|
+
const hit = slots.filter(
|
|
6547
|
+
(slot) => slot.accepts(anchor) && slotMatches(slot, anchor.before) && (anchor.after === void 0 || !slotMatches(slot, anchor.after))
|
|
6548
|
+
);
|
|
6549
|
+
if (!hit.length) continue;
|
|
6550
|
+
const tests = [...new Set(entry.uses.map((u) => u.test))].map((t) => index.tests[t]).filter((t) => !!t);
|
|
6551
|
+
const found = {
|
|
6552
|
+
anchor,
|
|
6553
|
+
locator: entry.locator,
|
|
6554
|
+
entry,
|
|
6555
|
+
uses: entry.uses,
|
|
6556
|
+
tests,
|
|
6557
|
+
confidence: confidenceOf(anchor, tests, options.reach)
|
|
6558
|
+
};
|
|
6559
|
+
if (anchor.after !== void 0 && hit.every((slot) => slot.rewrite)) {
|
|
6560
|
+
let rewritten = chain;
|
|
6561
|
+
const replacements = [];
|
|
6562
|
+
for (const slot of hit) {
|
|
6563
|
+
const again = chainSlots(rewritten, testIds).find(
|
|
6564
|
+
(s) => s.rule === slot.rule && String(s.value) === String(slot.value) && s.rewrite
|
|
6565
|
+
);
|
|
6566
|
+
if (!again?.rewrite) continue;
|
|
6567
|
+
rewritten = again.rewrite(anchor.after, anchor.before);
|
|
6568
|
+
if (slot.literal) replacements.push(slot.literal(anchor.after, anchor.before));
|
|
6569
|
+
}
|
|
6570
|
+
const rendered = renderLocatorChain(rewritten);
|
|
6571
|
+
if (rendered !== entry.locator) {
|
|
6572
|
+
found.rewrite = rendered;
|
|
6573
|
+
found.replacements = replacements.filter(([a, b]) => a !== b);
|
|
6574
|
+
}
|
|
6477
6575
|
}
|
|
6478
|
-
|
|
6479
|
-
return out;
|
|
6480
|
-
}
|
|
6481
|
-
return out;
|
|
6482
|
-
}
|
|
6483
|
-
function translationLineToken(path24, text, line) {
|
|
6484
|
-
const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
|
|
6485
|
-
let key;
|
|
6486
|
-
let value;
|
|
6487
|
-
if (ext === "json") {
|
|
6488
|
-
const pair = /^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(text);
|
|
6489
|
-
if (pair) {
|
|
6490
|
-
key = JSON.parse(`"${pair[1]}"`);
|
|
6491
|
-
value = JSON.parse(`"${pair[2]}"`);
|
|
6492
|
-
} else {
|
|
6493
|
-
const item = /^\s*"((?:[^"\\]|\\.)*)"\s*,?\s*$/.exec(text);
|
|
6494
|
-
if (item) value = JSON.parse(`"${item[1]}"`);
|
|
6495
|
-
}
|
|
6496
|
-
} else if (ext === "yaml" || ext === "yml") {
|
|
6497
|
-
const m = /^\s*(?:-\s+)?(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:\s+(.+)$/.exec(text);
|
|
6498
|
-
if (m) {
|
|
6499
|
-
key = (m[1] ?? m[2] ?? m[3]).trim();
|
|
6500
|
-
value = unquoteYaml(m[4]);
|
|
6501
|
-
}
|
|
6502
|
-
} else if (ext === "properties") {
|
|
6503
|
-
const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
|
|
6504
|
-
if (m) {
|
|
6505
|
-
key = m[1];
|
|
6506
|
-
value = m[2];
|
|
6507
|
-
}
|
|
6508
|
-
} else if (ext === "po") {
|
|
6509
|
-
const m = /^msg(id|str(?:\[\d+\])?)\s+"(.*)"\s*$/.exec(text);
|
|
6510
|
-
if (m) {
|
|
6511
|
-
value = m[2].replace(/\\"/g, '"');
|
|
6512
|
-
if (m[1] === "id") key = value;
|
|
6576
|
+
breaks.push(found);
|
|
6513
6577
|
}
|
|
6514
|
-
} else if (ext === "resx") {
|
|
6515
|
-
const m = /<value>(.*)<\/value>/.exec(text);
|
|
6516
|
-
if (m) value = decodeXml2(m[1]);
|
|
6517
6578
|
}
|
|
6518
|
-
|
|
6519
|
-
|
|
6579
|
+
return breaks.sort(
|
|
6580
|
+
(a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence] || b.tests.length - a.tests.length
|
|
6581
|
+
);
|
|
6520
6582
|
}
|
|
6521
|
-
function
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
return
|
|
6525
|
-
|
|
6526
|
-
...translationRefTokens(text, line, side, options.translations, spans),
|
|
6527
|
-
...literalTokens(text, line, spans, markup),
|
|
6528
|
-
...textTokens(text, line, markup)
|
|
6529
|
-
];
|
|
6583
|
+
function confidenceOf(anchor, tests, reach) {
|
|
6584
|
+
if (anchor.kind === "literal") return "possible";
|
|
6585
|
+
if (!reach) return "likely";
|
|
6586
|
+
if (anchor.kind === "translation" && /\.(?:json|ya?ml|properties|po|resx)$/i.test(anchor.file)) return "likely";
|
|
6587
|
+
return tests.some((t) => reach(t.id, anchor.file)) ? "likely" : "possible";
|
|
6530
6588
|
}
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6589
|
+
|
|
6590
|
+
// ../core/src/suspect-lines.ts
|
|
6591
|
+
function deletionPoints(hunk) {
|
|
6592
|
+
const added = hunk.added.map(({ line }) => line).sort((a, b) => a - b);
|
|
6593
|
+
const addedLines = new Set(added);
|
|
6594
|
+
const points = [];
|
|
6595
|
+
let previous = -1;
|
|
6596
|
+
hunk.removed.forEach(({ line }, removedBefore) => {
|
|
6597
|
+
const opensRun = line !== previous + 1;
|
|
6598
|
+
previous = line;
|
|
6599
|
+
if (!opensRun) return;
|
|
6600
|
+
let before;
|
|
6601
|
+
if (line === hunk.oldStart && added.length === 0) before = line === 1 ? 0 : hunk.newStart;
|
|
6602
|
+
else {
|
|
6603
|
+
before = hunk.newStart + (line - 1 - hunk.oldStart) - removedBefore;
|
|
6604
|
+
for (const addedLine of added) {
|
|
6605
|
+
if (addedLine > before) break;
|
|
6606
|
+
before++;
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
if (!addedLines.has(before + 1)) points.push(before);
|
|
6610
|
+
});
|
|
6611
|
+
return points;
|
|
6535
6612
|
}
|
|
6536
|
-
function
|
|
6537
|
-
|
|
6613
|
+
function hunkHeadLines(hunk) {
|
|
6614
|
+
const lines = hunk.added.map(({ line }) => [line, line]);
|
|
6615
|
+
for (const before of deletionPoints(hunk)) lines.push(before >= 1 ? [before, before + 1] : [1, 1]);
|
|
6616
|
+
return normalizeLineRanges(lines);
|
|
6538
6617
|
}
|
|
6539
|
-
function
|
|
6540
|
-
|
|
6541
|
-
for (const t of added) addedLeft.set(tokenId(t), [...addedLeft.get(tokenId(t)) ?? [], t]);
|
|
6542
|
-
const gone = [];
|
|
6543
|
-
for (const t of removed) {
|
|
6544
|
-
const same = addedLeft.get(tokenId(t));
|
|
6545
|
-
if (same?.length) same.shift();
|
|
6546
|
-
else gone.push(t);
|
|
6547
|
-
}
|
|
6548
|
-
const fresh = [...addedLeft.values()].flat();
|
|
6549
|
-
const anchors = [];
|
|
6550
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6551
|
-
for (const t of gone) {
|
|
6552
|
-
const peers = gone.filter((g) => g.group === t.group);
|
|
6553
|
-
const replacements = fresh.filter((a) => a.group === t.group);
|
|
6554
|
-
const rename = peers.length === 1 && replacements.length === 1 ? replacements[0] : void 0;
|
|
6555
|
-
const anchor = {
|
|
6556
|
-
file,
|
|
6557
|
-
line: rename ? rename.line : Math.max(1, hunk.newStart),
|
|
6558
|
-
oldLine: t.line,
|
|
6559
|
-
kind: t.kind,
|
|
6560
|
-
before: t.value
|
|
6561
|
-
};
|
|
6562
|
-
if (t.attribute) anchor.attribute = t.attribute;
|
|
6563
|
-
if (t.key) anchor.key = t.key;
|
|
6564
|
-
if (rename) anchor.after = rename.value;
|
|
6565
|
-
const id = `${anchor.kind}\0${anchor.attribute ?? ""}\0${anchor.key ?? ""}\0${anchor.before}\0${anchor.after ?? ""}`;
|
|
6566
|
-
if (seen.has(id)) continue;
|
|
6567
|
-
seen.add(id);
|
|
6568
|
-
anchors.push(anchor);
|
|
6569
|
-
}
|
|
6570
|
-
return anchors;
|
|
6618
|
+
function hunkBaseLines(hunk) {
|
|
6619
|
+
return hunkHeadLines({ oldStart: hunk.newStart, newStart: hunk.oldStart, removed: hunk.added, added: hunk.removed });
|
|
6571
6620
|
}
|
|
6572
|
-
function
|
|
6573
|
-
const
|
|
6574
|
-
const
|
|
6575
|
-
const anchors = [];
|
|
6621
|
+
function diffImpactInput(files) {
|
|
6622
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
6623
|
+
const changes = [];
|
|
6576
6624
|
for (const file of files) {
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
let removed;
|
|
6583
|
-
let added;
|
|
6584
|
-
if (translation) {
|
|
6585
|
-
oldKeys ??= keysByLine(file.oldPath ?? file.path, "old", options);
|
|
6586
|
-
newKeys ??= keysByLine(file.path, "new", options);
|
|
6587
|
-
const read = (lines, keys) => lines.flatMap((l) => {
|
|
6588
|
-
const token = translationLineToken(file.path, l.text, l.line);
|
|
6589
|
-
if (!token) return [];
|
|
6590
|
-
const full = keys?.get(l.line);
|
|
6591
|
-
if (full) {
|
|
6592
|
-
token.key = full;
|
|
6593
|
-
token.group = `translation:${full}`;
|
|
6594
|
-
}
|
|
6595
|
-
return [token];
|
|
6596
|
-
});
|
|
6597
|
-
removed = read(hunk.removed, oldKeys);
|
|
6598
|
-
added = read(hunk.added, newKeys);
|
|
6599
|
-
} else {
|
|
6600
|
-
removed = hunk.removed.flatMap((l) => lineTokens(file.path, l.text, l.line, "old", attributes, options));
|
|
6601
|
-
added = hunk.added.flatMap((l) => lineTokens(file.path, l.text, l.line, "new", attributes, options));
|
|
6602
|
-
}
|
|
6603
|
-
anchors.push(...hunkAnchors(file.path, hunk, removed, added));
|
|
6604
|
-
}
|
|
6625
|
+
changedFiles.add(file.path);
|
|
6626
|
+
if (file.oldPath) changedFiles.add(file.oldPath);
|
|
6627
|
+
if (file.status !== "modified" || !file.oldBlob) continue;
|
|
6628
|
+
const lines = normalizeLineRanges(file.hunks.flatMap(hunkBaseLines));
|
|
6629
|
+
if (lines.length > 0) changes.push({ filePath: file.path, blob: file.oldBlob, lines });
|
|
6605
6630
|
}
|
|
6606
|
-
return
|
|
6631
|
+
return { changedFiles: [...changedFiles], changes };
|
|
6607
6632
|
}
|
|
6608
6633
|
|
|
6609
|
-
//
|
|
6610
|
-
|
|
6611
|
-
|
|
6634
|
+
// src/cli/add-reporter.ts
|
|
6635
|
+
var fs16 = __toESM(require("fs"));
|
|
6636
|
+
var path21 = __toESM(require("path"));
|
|
6637
|
+
var import_node_module2 = require("module");
|
|
6638
|
+
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
6639
|
+
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
6640
|
+
var ADD_REPORTER_MIN = { major: 1, minor: 63 };
|
|
6641
|
+
function playwrightSupportsAddReporter(version) {
|
|
6642
|
+
const match = version ? /^(\d+)\.(\d+)/.exec(version) : null;
|
|
6643
|
+
if (!match) return false;
|
|
6644
|
+
const major = Number(match[1]);
|
|
6645
|
+
const minor = Number(match[2]);
|
|
6646
|
+
return major > ADD_REPORTER_MIN.major || major === ADD_REPORTER_MIN.major && minor >= ADD_REPORTER_MIN.minor;
|
|
6612
6647
|
}
|
|
6613
|
-
function
|
|
6614
|
-
|
|
6615
|
-
const re = value;
|
|
6616
|
-
return { matcher: (text) => re.test(text.full), kind: "regex" };
|
|
6617
|
-
}
|
|
6618
|
-
const needle = normalizeWhiteSpace(value);
|
|
6619
|
-
if (exact) return { matcher: (text) => text.normalized === needle, kind: "strict" };
|
|
6620
|
-
const lower = needle.toLowerCase();
|
|
6621
|
-
return { matcher: (text) => (text.lower ??= text.normalized.toLowerCase()).includes(lower), kind: "lax" };
|
|
6648
|
+
function configHasPiwiReporter(source) {
|
|
6649
|
+
return new RegExp(REPORTER_PACKAGE2.replace(/[/\\]/g, "\\$&")).test(source);
|
|
6622
6650
|
}
|
|
6623
|
-
function
|
|
6624
|
-
if (
|
|
6625
|
-
if (
|
|
6626
|
-
|
|
6627
|
-
|
|
6651
|
+
function decideAddReporter(configSource, playwrightVersion) {
|
|
6652
|
+
if (configSource === null || configHasPiwiReporter(configSource)) return { args: [], log: null };
|
|
6653
|
+
if (playwrightSupportsAddReporter(playwrightVersion)) {
|
|
6654
|
+
return {
|
|
6655
|
+
args: [ADD_REPORTER_FLAG, REPORTER_PACKAGE2],
|
|
6656
|
+
log: `piwi run: the Playwright config has no Piwi reporter \u2014 appending ${ADD_REPORTER_FLAG} ${REPORTER_PACKAGE2}`
|
|
6657
|
+
};
|
|
6658
|
+
}
|
|
6659
|
+
return {
|
|
6660
|
+
args: [],
|
|
6661
|
+
log: `piwi run: the Playwright config has no Piwi reporter and Playwright ${playwrightVersion ?? "unknown"} predates ${ADD_REPORTER_FLAG} (1.63) \u2014 results will not reach the dashboard`
|
|
6662
|
+
};
|
|
6628
6663
|
}
|
|
6629
|
-
function
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6664
|
+
function resolveConfigPath(cwd, playwrightArgs2) {
|
|
6665
|
+
for (let i = 0; i < playwrightArgs2.length; i++) {
|
|
6666
|
+
const arg = playwrightArgs2[i];
|
|
6667
|
+
if (arg === "--config" || arg === "-c") {
|
|
6668
|
+
const next = playwrightArgs2[i + 1];
|
|
6669
|
+
if (next) return path21.resolve(cwd, next);
|
|
6670
|
+
} else if (arg.startsWith("--config=")) {
|
|
6671
|
+
return path21.resolve(cwd, arg.slice("--config=".length));
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
return detectProject(cwd).configPath;
|
|
6635
6675
|
}
|
|
6636
|
-
function
|
|
6637
|
-
|
|
6676
|
+
function readConfigSource(configPath) {
|
|
6677
|
+
if (!configPath) return null;
|
|
6678
|
+
try {
|
|
6679
|
+
return fs16.readFileSync(configPath, "utf-8");
|
|
6680
|
+
} catch {
|
|
6681
|
+
return null;
|
|
6682
|
+
}
|
|
6638
6683
|
}
|
|
6639
|
-
function
|
|
6640
|
-
|
|
6684
|
+
function readInstalledPlaywrightVersion(cwd) {
|
|
6685
|
+
try {
|
|
6686
|
+
const require2 = (0, import_node_module2.createRequire)(path21.join(cwd, "noop.js"));
|
|
6687
|
+
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
6688
|
+
try {
|
|
6689
|
+
return require2(id).version ?? null;
|
|
6690
|
+
} catch {
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
} catch {
|
|
6694
|
+
}
|
|
6695
|
+
return null;
|
|
6641
6696
|
}
|
|
6642
|
-
function
|
|
6643
|
-
|
|
6697
|
+
function computeAddReporterArgs(cwd, playwrightArgs2) {
|
|
6698
|
+
const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
|
|
6699
|
+
return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
|
|
6644
6700
|
}
|
|
6645
6701
|
|
|
6646
|
-
//
|
|
6647
|
-
var
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6702
|
+
// src/cli/select.ts
|
|
6703
|
+
var EXIT_OK6 = 0;
|
|
6704
|
+
var EXIT_ERROR7 = 2;
|
|
6705
|
+
var USAGE9 = `
|
|
6706
|
+
piwi select / piwi run \u2014 run a saved selection of tests
|
|
6707
|
+
|
|
6708
|
+
Usage:
|
|
6709
|
+
npx @piwitests/reporter select <key> [options] print the Playwright args
|
|
6710
|
+
npx @piwitests/reporter run <key> [options] [-- <playwright args>]
|
|
6711
|
+
npx @piwitests/reporter run impact --base <ref> run the tests your diff impacts
|
|
6712
|
+
|
|
6713
|
+
Connection:
|
|
6714
|
+
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
|
|
6715
|
+
--api-key <key> API key (env PIWI_API_KEY)
|
|
6716
|
+
--project <name|id> Project (env PIWI_PROJECT_NAME)
|
|
6717
|
+
|
|
6718
|
+
Selection:
|
|
6719
|
+
--format <fmt> args (file:line, default) | grep | files | json
|
|
6720
|
+
--budget <duration> Cap total time, e.g. 5m, 90s, 300000 (ms)
|
|
6721
|
+
--shard <i/n> Keep only shard i of n, balanced by duration, lock-aware
|
|
6722
|
+
--fail-fast Order the least-reliable tests first (fail-fast)
|
|
6723
|
+
--base <ref> For "impact": the ref to diff the working tree against
|
|
6724
|
+
|
|
6725
|
+
Behavior:
|
|
6726
|
+
--strict Fail (exit 2) instead of falling back when unreachable
|
|
6727
|
+
--pkg-runner <cmd> Package runner for the printed command (default npx)
|
|
6728
|
+
--json Print the full resolution as JSON (select only)
|
|
6729
|
+
-h, --help Show this help
|
|
6730
|
+
|
|
6731
|
+
Exit codes: 0 ok, 1 the test run failed (run only), 2 could not resolve.
|
|
6732
|
+
`.trim();
|
|
6733
|
+
var IMPACT_KEY = "impact";
|
|
6734
|
+
var NOTHING_TO_RUN_ARGS = ["--pass-with-no-tests", "--grep-invert", "."];
|
|
6735
|
+
var VALUE_FLAGS3 = /* @__PURE__ */ new Set([
|
|
6736
|
+
"--server-url",
|
|
6737
|
+
"--api-key",
|
|
6738
|
+
"--project",
|
|
6739
|
+
"--format",
|
|
6740
|
+
"--budget",
|
|
6741
|
+
"--shard",
|
|
6742
|
+
"--base",
|
|
6743
|
+
"--pkg-runner"
|
|
6744
|
+
]);
|
|
6745
|
+
function readOption7(argv, name) {
|
|
6746
|
+
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
6747
|
+
if (withEquals) return withEquals.slice(name.length + 1);
|
|
6748
|
+
const index = argv.indexOf(name);
|
|
6749
|
+
if (index === -1) return void 0;
|
|
6750
|
+
const value = argv[index + 1];
|
|
6751
|
+
return value && !value.startsWith("-") ? value : void 0;
|
|
6653
6752
|
}
|
|
6654
|
-
function
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6753
|
+
function findKey(argv) {
|
|
6754
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6755
|
+
const tok = argv[i];
|
|
6756
|
+
if (tok.startsWith("-")) {
|
|
6757
|
+
if (!tok.includes("=") && VALUE_FLAGS3.has(tok)) i++;
|
|
6758
|
+
continue;
|
|
6759
|
+
}
|
|
6760
|
+
return tok;
|
|
6659
6761
|
}
|
|
6762
|
+
return void 0;
|
|
6660
6763
|
}
|
|
6661
|
-
function
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
const
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6670
|
-
}
|
|
6671
|
-
function renameValue(value, before, after, exact) {
|
|
6672
|
-
const v = normalizeWhiteSpace(value);
|
|
6673
|
-
const b = normalizeWhiteSpace(before);
|
|
6674
|
-
if (exact ? v === b : v.toLowerCase() === b.toLowerCase()) return after;
|
|
6675
|
-
const re = new RegExp(escapeRegExp2(b).replace(/ /g, "\\s+"), exact ? "" : "i");
|
|
6676
|
-
if (!re.test(value)) return null;
|
|
6677
|
-
return value.replace(re, () => after);
|
|
6678
|
-
}
|
|
6679
|
-
function withArg(call, index, arg) {
|
|
6680
|
-
return { method: call.method, args: call.args.map((a, i) => i === index ? arg : a) };
|
|
6681
|
-
}
|
|
6682
|
-
function withOption(call, index, key, arg) {
|
|
6683
|
-
const obj = call.args[index];
|
|
6684
|
-
if (obj?.type !== "object") return call;
|
|
6685
|
-
return withArg(call, index, {
|
|
6686
|
-
type: "object",
|
|
6687
|
-
entries: obj.entries.map(([k, v]) => [k, k === key ? arg : v])
|
|
6688
|
-
});
|
|
6764
|
+
function parseDuration2(raw) {
|
|
6765
|
+
const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
6766
|
+
if (!match) return null;
|
|
6767
|
+
const value = Number(match[1]);
|
|
6768
|
+
const unit = match[2];
|
|
6769
|
+
const factor = unit === "h" ? 36e5 : unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1;
|
|
6770
|
+
const ms = Math.round(value * factor);
|
|
6771
|
+
return ms > 0 ? ms : null;
|
|
6689
6772
|
}
|
|
6690
|
-
function
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6773
|
+
function parseSelectArgs(argv, env) {
|
|
6774
|
+
const dashIndex = argv.indexOf("--");
|
|
6775
|
+
const own = dashIndex === -1 ? argv : argv.slice(0, dashIndex);
|
|
6776
|
+
const extra = dashIndex === -1 ? [] : argv.slice(dashIndex + 1);
|
|
6777
|
+
const serverUrl = (readOption7(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
|
|
6778
|
+
if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
6779
|
+
const key = findKey(own);
|
|
6780
|
+
if (!key) throw new Error("No selection key \u2014 usage: piwi select <key>");
|
|
6781
|
+
const budgetRaw = readOption7(own, "--budget");
|
|
6782
|
+
let budgetMs = null;
|
|
6783
|
+
if (budgetRaw !== void 0) {
|
|
6784
|
+
budgetMs = parseDuration2(budgetRaw);
|
|
6785
|
+
if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
|
|
6786
|
+
}
|
|
6787
|
+
const shard = readOption7(own, "--shard") ?? null;
|
|
6788
|
+
if (shard !== null && !/^\d+\s*\/\s*\d+$/.test(shard)) {
|
|
6789
|
+
throw new Error(`--shard expects an "i/n" spec like 2/4, got "${shard}"`);
|
|
6694
6790
|
}
|
|
6695
|
-
if (arg?.type !== "string") return null;
|
|
6696
|
-
const value = arg.value;
|
|
6697
6791
|
return {
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6792
|
+
serverUrl,
|
|
6793
|
+
apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
6794
|
+
project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
|
|
6795
|
+
key,
|
|
6796
|
+
format: readOption7(own, "--format") ?? "args",
|
|
6797
|
+
budgetMs,
|
|
6798
|
+
shard,
|
|
6799
|
+
order: own.includes("--fail-fast") ? "failureLikelihood" : null,
|
|
6800
|
+
base: readOption7(own, "--base") ?? null,
|
|
6801
|
+
strict: own.includes("--strict"),
|
|
6802
|
+
pkgRunner: readOption7(own, "--pkg-runner") ?? "npx",
|
|
6803
|
+
json: own.includes("--json"),
|
|
6804
|
+
extra
|
|
6704
6805
|
};
|
|
6705
6806
|
}
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
const edited = (after) => selector.slice(0, at) + render(after) + selector.slice(at + len);
|
|
6710
|
-
slots.push({
|
|
6711
|
-
value,
|
|
6712
|
-
exact: true,
|
|
6713
|
-
rule: "exact",
|
|
6714
|
-
accepts: (a) => hasAttribute(a, names),
|
|
6715
|
-
rewrite: (after) => replace(edited(after)),
|
|
6716
|
-
literal: (after) => [selector, edited(after)]
|
|
6717
|
-
});
|
|
6718
|
-
};
|
|
6719
|
-
for (const m of selector.matchAll(/#((?:[\w-]|\\.)+)/g)) {
|
|
6720
|
-
slot(
|
|
6721
|
-
m[1].replace(/\\(.)/g, "$1"),
|
|
6722
|
-
["id"],
|
|
6723
|
-
(v) => `#${v.replace(/[^\w-]/g, (c) => `\\${c}`)}`,
|
|
6724
|
-
m.index,
|
|
6725
|
-
m[0].length
|
|
6726
|
-
);
|
|
6727
|
-
}
|
|
6728
|
-
for (const m of selector.matchAll(/\[\s*([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*\]/g)) {
|
|
6729
|
-
const attr = m[1];
|
|
6730
|
-
const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
|
|
6731
|
-
if (!names) continue;
|
|
6732
|
-
const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
|
|
6733
|
-
const value = m[2] ?? m[3] ?? m[4];
|
|
6734
|
-
slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
|
|
6735
|
-
}
|
|
6736
|
-
return slots;
|
|
6807
|
+
var CACHE_FILE = path22.join(".piwi", "selection-cache.json");
|
|
6808
|
+
function cacheKey(args) {
|
|
6809
|
+
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
6737
6810
|
}
|
|
6738
|
-
function
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
const hasTextSlot = (index) => {
|
|
6745
|
-
const hasText = optionsOf(call.args[index]).get("hasText");
|
|
6746
|
-
if (!hasText) return null;
|
|
6747
|
-
return stringSlot(hasText, false, "text", isText, (next) => withOption(call, index, "hasText", next));
|
|
6748
|
-
};
|
|
6749
|
-
const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
|
|
6750
|
-
switch (call.method) {
|
|
6751
|
-
case "getByTestId":
|
|
6752
|
-
slots.push(argSlot("exact", (a) => hasAttribute(a, testIds), true));
|
|
6753
|
-
break;
|
|
6754
|
-
case "getByRole":
|
|
6755
|
-
slots.push(
|
|
6756
|
-
stringSlot(
|
|
6757
|
-
opts.get("name"),
|
|
6758
|
-
exact,
|
|
6759
|
-
"name",
|
|
6760
|
-
(a) => isText(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
|
|
6761
|
-
(next) => withOption(call, 1, "name", next)
|
|
6762
|
-
)
|
|
6763
|
-
);
|
|
6764
|
-
break;
|
|
6765
|
-
case "getByText":
|
|
6766
|
-
slots.push(argSlot("text", isText));
|
|
6767
|
-
break;
|
|
6768
|
-
case "getByLabel":
|
|
6769
|
-
slots.push(argSlot("text", (a) => isText(a) || hasAttribute(a, ["aria-label"])));
|
|
6770
|
-
break;
|
|
6771
|
-
case "getByPlaceholder":
|
|
6772
|
-
slots.push(argSlot("text", textOr("placeholder")));
|
|
6773
|
-
break;
|
|
6774
|
-
case "getByAltText":
|
|
6775
|
-
slots.push(argSlot("text", textOr("alt")));
|
|
6776
|
-
break;
|
|
6777
|
-
case "getByTitle":
|
|
6778
|
-
slots.push(argSlot("text", textOr("title")));
|
|
6779
|
-
break;
|
|
6780
|
-
case "locator":
|
|
6781
|
-
if (first?.type === "string") {
|
|
6782
|
-
slots.push(...cssSlots(first.value, testIds, (next) => withArg(call, 0, { type: "string", value: next })));
|
|
6783
|
-
}
|
|
6784
|
-
slots.push(hasTextSlot(1));
|
|
6785
|
-
break;
|
|
6786
|
-
case "filter":
|
|
6787
|
-
slots.push(hasTextSlot(0));
|
|
6788
|
-
break;
|
|
6811
|
+
function readCache(args) {
|
|
6812
|
+
try {
|
|
6813
|
+
const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
|
|
6814
|
+
return store[cacheKey(args)] ?? null;
|
|
6815
|
+
} catch {
|
|
6816
|
+
return null;
|
|
6789
6817
|
}
|
|
6790
|
-
return slots.filter((slot) => !!slot);
|
|
6791
6818
|
}
|
|
6792
|
-
function
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
for (const [key, v] of arg.entries) {
|
|
6799
|
-
if (v.type === "chain") {
|
|
6800
|
-
out.push({ chain: v.chain, put: (c) => withOption(call, index, key, { type: "chain", chain: c }) });
|
|
6801
|
-
}
|
|
6802
|
-
}
|
|
6819
|
+
function writeCache(args, resolution) {
|
|
6820
|
+
try {
|
|
6821
|
+
let store = {};
|
|
6822
|
+
try {
|
|
6823
|
+
store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
|
|
6824
|
+
} catch {
|
|
6803
6825
|
}
|
|
6804
|
-
|
|
6805
|
-
|
|
6826
|
+
store[cacheKey(args)] = resolution;
|
|
6827
|
+
fs17.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
|
|
6828
|
+
fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
|
|
6829
|
+
} catch {
|
|
6830
|
+
}
|
|
6806
6831
|
}
|
|
6807
|
-
function
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
const
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
}
|
|
6832
|
+
async function resolveWithCache(args) {
|
|
6833
|
+
try {
|
|
6834
|
+
const projectId = await resolveProjectId(args);
|
|
6835
|
+
const resolution = await fetchResolution(args, projectId);
|
|
6836
|
+
writeCache(args, resolution);
|
|
6837
|
+
return { resolution, fromCache: false };
|
|
6838
|
+
} catch (e) {
|
|
6839
|
+
if (args.strict) throw e;
|
|
6840
|
+
const cached = readCache(args);
|
|
6841
|
+
if (cached) {
|
|
6842
|
+
console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
|
|
6843
|
+
return { resolution: cached, fromCache: true };
|
|
6820
6844
|
}
|
|
6845
|
+
return { error: e };
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
function printWarnings(resolution) {
|
|
6849
|
+
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
6850
|
+
}
|
|
6851
|
+
function git2(args) {
|
|
6852
|
+
return (0, import_node_child_process7.execFileSync)("git", ["-c", "core.quotePath=false", ...args], {
|
|
6853
|
+
encoding: "utf-8",
|
|
6854
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
6855
|
+
maxBuffer: 256 * 1024 * 1024
|
|
6821
6856
|
});
|
|
6822
|
-
return out;
|
|
6823
6857
|
}
|
|
6824
|
-
function
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6858
|
+
function gitChange(base) {
|
|
6859
|
+
const names = git2(["diff", "--name-only", base]).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
6860
|
+
let fromDiff = { changedFiles: [], changes: [] };
|
|
6861
|
+
try {
|
|
6862
|
+
fromDiff = diffImpactInput(
|
|
6863
|
+
parseUnifiedDiff(
|
|
6864
|
+
git2([
|
|
6865
|
+
"diff",
|
|
6866
|
+
"--full-index",
|
|
6867
|
+
"--unified=0",
|
|
6868
|
+
"--no-color",
|
|
6869
|
+
"--no-ext-diff",
|
|
6870
|
+
"--src-prefix=a/",
|
|
6871
|
+
"--dst-prefix=b/",
|
|
6872
|
+
base,
|
|
6873
|
+
"--"
|
|
6874
|
+
])
|
|
6875
|
+
)
|
|
6876
|
+
);
|
|
6877
|
+
} catch {
|
|
6832
6878
|
}
|
|
6879
|
+
return { changedFiles: [.../* @__PURE__ */ new Set([...names, ...fromDiff.changedFiles])], changes: fromDiff.changes };
|
|
6833
6880
|
}
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
const y = filePath(b);
|
|
6838
|
-
return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
|
|
6881
|
+
async function loadImpact(args, projectId) {
|
|
6882
|
+
const { changedFiles, changes } = gitChange(args.base);
|
|
6883
|
+
return fetchImpact(args, projectId, changedFiles, changes);
|
|
6839
6884
|
}
|
|
6840
|
-
function
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6885
|
+
function lineLevelNote(impact) {
|
|
6886
|
+
const files = impact.impact.lineLevelFiles ?? 0;
|
|
6887
|
+
return files > 0 ? `, ${files} file(s) narrowed by line coverage` : "";
|
|
6888
|
+
}
|
|
6889
|
+
async function runSelectImpact(args) {
|
|
6890
|
+
let impact;
|
|
6891
|
+
try {
|
|
6892
|
+
const projectId = await resolveProjectId(args);
|
|
6893
|
+
impact = await loadImpact(args, projectId);
|
|
6894
|
+
} catch (e) {
|
|
6895
|
+
console.error(`piwi select: ${e.message}`);
|
|
6896
|
+
return EXIT_ERROR7;
|
|
6897
|
+
}
|
|
6898
|
+
printWarnings(impact);
|
|
6899
|
+
console.error(
|
|
6900
|
+
`piwi select: ${impact.impact.changedFiles} changed file(s) \u2192 ${impact.estimate.count} impacted test(s)${impact.impact.widened ? " (widened to full suite)" : lineLevelNote(impact)}`
|
|
6901
|
+
);
|
|
6902
|
+
if (args.json) {
|
|
6903
|
+
console.log(JSON.stringify(impact, null, 2));
|
|
6904
|
+
return EXIT_OK6;
|
|
6905
|
+
}
|
|
6906
|
+
if (impact.impact.widened) console.log("");
|
|
6907
|
+
else if (impact.estimate.count === 0) {
|
|
6908
|
+
console.error(`piwi select: no tests impacted \u2014 printing arguments that run none`);
|
|
6909
|
+
console.log(NOTHING_TO_RUN_ARGS.join(" "));
|
|
6910
|
+
} else console.log(impact.materialization.args.join(" "));
|
|
6911
|
+
return EXIT_OK6;
|
|
6912
|
+
}
|
|
6913
|
+
async function runSelect(argv, env = process.env) {
|
|
6914
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
6915
|
+
console.log(USAGE9);
|
|
6916
|
+
return EXIT_OK6;
|
|
6917
|
+
}
|
|
6918
|
+
let args;
|
|
6919
|
+
try {
|
|
6920
|
+
args = parseSelectArgs(argv, env);
|
|
6921
|
+
} catch (e) {
|
|
6922
|
+
console.error(`piwi select: ${e.message}
|
|
6923
|
+
`);
|
|
6924
|
+
console.error(USAGE9);
|
|
6925
|
+
return EXIT_ERROR7;
|
|
6926
|
+
}
|
|
6927
|
+
if (args.key === IMPACT_KEY) {
|
|
6928
|
+
if (!args.base) {
|
|
6929
|
+
console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
6930
|
+
return EXIT_ERROR7;
|
|
6852
6931
|
}
|
|
6932
|
+
return runSelectImpact(args);
|
|
6853
6933
|
}
|
|
6854
|
-
|
|
6934
|
+
let resolution;
|
|
6935
|
+
try {
|
|
6936
|
+
const projectId = await resolveProjectId(args);
|
|
6937
|
+
resolution = await fetchResolution(args, projectId);
|
|
6938
|
+
} catch (e) {
|
|
6939
|
+
console.error(`piwi select: ${e.message}`);
|
|
6940
|
+
return EXIT_ERROR7;
|
|
6941
|
+
}
|
|
6942
|
+
printWarnings(resolution);
|
|
6943
|
+
if (resolution.estimate.count === 0) {
|
|
6944
|
+
console.error(`piwi select: "${args.key}" resolved to 0 tests`);
|
|
6945
|
+
return EXIT_ERROR7;
|
|
6946
|
+
}
|
|
6947
|
+
if (args.json) console.log(JSON.stringify(resolution, null, 2));
|
|
6948
|
+
else console.log(resolution.materialization.args.join(" "));
|
|
6949
|
+
return EXIT_OK6;
|
|
6855
6950
|
}
|
|
6856
|
-
function
|
|
6857
|
-
|
|
6951
|
+
function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
|
|
6952
|
+
const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
|
|
6953
|
+
if (decision.log) console.error(decision.log);
|
|
6954
|
+
return spawnPlaywright([...decision.args, ...playwrightArgs2], env, { runner: pkgRunner, command: "piwi run" });
|
|
6858
6955
|
}
|
|
6859
|
-
function
|
|
6860
|
-
|
|
6861
|
-
return m ? Number(m[1]) : null;
|
|
6956
|
+
function shardEnv(args) {
|
|
6957
|
+
return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
|
|
6862
6958
|
}
|
|
6863
|
-
function
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6959
|
+
async function runRunImpact(args, env) {
|
|
6960
|
+
let projectId;
|
|
6961
|
+
try {
|
|
6962
|
+
projectId = await resolveProjectId(args);
|
|
6963
|
+
} catch (e) {
|
|
6964
|
+
if (args.strict) {
|
|
6965
|
+
console.error(`piwi run: ${e.message}`);
|
|
6966
|
+
return EXIT_ERROR7;
|
|
6967
|
+
}
|
|
6968
|
+
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
6969
|
+
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
6867
6970
|
}
|
|
6868
|
-
|
|
6971
|
+
let impact;
|
|
6972
|
+
try {
|
|
6973
|
+
impact = await loadImpact(args, projectId);
|
|
6974
|
+
} catch (e) {
|
|
6975
|
+
if (args.strict) {
|
|
6976
|
+
console.error(`piwi run: ${e.message}`);
|
|
6977
|
+
return EXIT_ERROR7;
|
|
6978
|
+
}
|
|
6979
|
+
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
6980
|
+
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
6981
|
+
}
|
|
6982
|
+
printWarnings(impact);
|
|
6983
|
+
if (impact.impact.widened) {
|
|
6984
|
+
console.error(
|
|
6985
|
+
`piwi run: impact widened to the full suite (${impact.impact.unmappedSourceFiles.length} changed file(s) not fully mapped to tests)`
|
|
6986
|
+
);
|
|
6987
|
+
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
6988
|
+
}
|
|
6989
|
+
if (impact.estimate.count === 0) {
|
|
6990
|
+
console.error(`piwi run: no tests impacted by ${impact.impact.changedFiles} changed file(s) \u2014 nothing to run`);
|
|
6991
|
+
return EXIT_OK6;
|
|
6992
|
+
}
|
|
6993
|
+
const runEnv = {
|
|
6994
|
+
...env,
|
|
6995
|
+
PIWI_SELECTION: IMPACT_KEY,
|
|
6996
|
+
PIWI_SELECTION_VERSION: "0",
|
|
6997
|
+
PIWI_SELECTION_HASH: impact.resolvedHash,
|
|
6998
|
+
PIWI_SELECTION_COUNT: String(impact.estimate.count),
|
|
6999
|
+
...shardEnv(args)
|
|
7000
|
+
};
|
|
7001
|
+
console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)${lineLevelNote(impact)}`);
|
|
7002
|
+
return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
|
|
6869
7003
|
}
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
uses: entry.uses,
|
|
6893
|
-
tests,
|
|
6894
|
-
confidence: confidenceOf(anchor, tests, options.reach)
|
|
6895
|
-
};
|
|
6896
|
-
if (anchor.after !== void 0 && hit.every((slot) => slot.rewrite)) {
|
|
6897
|
-
let rewritten = chain;
|
|
6898
|
-
const replacements = [];
|
|
6899
|
-
for (const slot of hit) {
|
|
6900
|
-
const again = chainSlots(rewritten, testIds).find(
|
|
6901
|
-
(s) => s.rule === slot.rule && String(s.value) === String(slot.value) && s.rewrite
|
|
6902
|
-
);
|
|
6903
|
-
if (!again?.rewrite) continue;
|
|
6904
|
-
rewritten = again.rewrite(anchor.after, anchor.before);
|
|
6905
|
-
if (slot.literal) replacements.push(slot.literal(anchor.after, anchor.before));
|
|
6906
|
-
}
|
|
6907
|
-
const rendered = renderLocatorChain(rewritten);
|
|
6908
|
-
if (rendered !== entry.locator) {
|
|
6909
|
-
found.rewrite = rendered;
|
|
6910
|
-
found.replacements = replacements.filter(([a, b]) => a !== b);
|
|
6911
|
-
}
|
|
6912
|
-
}
|
|
6913
|
-
breaks.push(found);
|
|
7004
|
+
async function runRun(argv, env = process.env) {
|
|
7005
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
7006
|
+
console.log(USAGE9);
|
|
7007
|
+
return EXIT_OK6;
|
|
7008
|
+
}
|
|
7009
|
+
let args;
|
|
7010
|
+
try {
|
|
7011
|
+
args = parseSelectArgs(argv, env);
|
|
7012
|
+
} catch (e) {
|
|
7013
|
+
console.error(`piwi run: ${e.message}
|
|
7014
|
+
`);
|
|
7015
|
+
console.error(USAGE9);
|
|
7016
|
+
return EXIT_ERROR7;
|
|
7017
|
+
}
|
|
7018
|
+
if (args.format === "json") {
|
|
7019
|
+
console.error("piwi run: --format json cannot be run; use args, grep or files");
|
|
7020
|
+
return EXIT_ERROR7;
|
|
7021
|
+
}
|
|
7022
|
+
if (args.key === IMPACT_KEY) {
|
|
7023
|
+
if (!args.base) {
|
|
7024
|
+
console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
7025
|
+
return EXIT_ERROR7;
|
|
6914
7026
|
}
|
|
7027
|
+
return runRunImpact(args, env);
|
|
6915
7028
|
}
|
|
6916
|
-
|
|
6917
|
-
|
|
7029
|
+
let outcome;
|
|
7030
|
+
try {
|
|
7031
|
+
outcome = await resolveWithCache(args);
|
|
7032
|
+
} catch (e) {
|
|
7033
|
+
console.error(`piwi run: ${e.message}`);
|
|
7034
|
+
return EXIT_ERROR7;
|
|
7035
|
+
}
|
|
7036
|
+
if ("error" in outcome) {
|
|
7037
|
+
console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
|
|
7038
|
+
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
7039
|
+
}
|
|
7040
|
+
const { resolution } = outcome;
|
|
7041
|
+
printWarnings(resolution);
|
|
7042
|
+
if (resolution.estimate.count === 0) {
|
|
7043
|
+
console.error(`piwi run: "${args.key}" resolved to 0 tests`);
|
|
7044
|
+
return EXIT_ERROR7;
|
|
7045
|
+
}
|
|
7046
|
+
const runEnv = {
|
|
7047
|
+
...env,
|
|
7048
|
+
PIWI_SELECTION: resolution.key ?? args.key,
|
|
7049
|
+
PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
|
|
7050
|
+
PIWI_SELECTION_HASH: resolution.resolvedHash,
|
|
7051
|
+
PIWI_SELECTION_COUNT: String(resolution.estimate.count),
|
|
7052
|
+
...shardEnv(args)
|
|
7053
|
+
};
|
|
7054
|
+
console.error(
|
|
7055
|
+
`piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
|
|
6918
7056
|
);
|
|
7057
|
+
return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
|
|
6919
7058
|
}
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
}
|
|
7059
|
+
|
|
7060
|
+
// src/cli/preflight.ts
|
|
7061
|
+
var fs18 = __toESM(require("fs"));
|
|
7062
|
+
var path23 = __toESM(require("path"));
|
|
7063
|
+
var import_node_child_process8 = require("child_process");
|
|
6926
7064
|
|
|
6927
7065
|
// ../core/src/locator-edit.ts
|
|
6928
7066
|
function escapeForQuote(value, quote3) {
|
|
@@ -7046,15 +7184,15 @@ function parsePreflightArgs(argv) {
|
|
|
7046
7184
|
}
|
|
7047
7185
|
var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
|
|
7048
7186
|
function findPlaywrightRoot(from) {
|
|
7049
|
-
let dir =
|
|
7187
|
+
let dir = path23.resolve(from);
|
|
7050
7188
|
for (; ; ) {
|
|
7051
|
-
if (CONFIG_NAMES2.some((name) => fs18.existsSync(
|
|
7052
|
-
const parent =
|
|
7189
|
+
if (CONFIG_NAMES2.some((name) => fs18.existsSync(path23.join(dir, name)))) return dir;
|
|
7190
|
+
const parent = path23.dirname(dir);
|
|
7053
7191
|
if (parent === dir) return null;
|
|
7054
7192
|
dir = parent;
|
|
7055
7193
|
}
|
|
7056
7194
|
}
|
|
7057
|
-
function
|
|
7195
|
+
function git3(cwd, args) {
|
|
7058
7196
|
return (0, import_node_child_process8.execFileSync)("git", args, {
|
|
7059
7197
|
cwd,
|
|
7060
7198
|
encoding: "utf-8",
|
|
@@ -7064,7 +7202,7 @@ function git2(cwd, args) {
|
|
|
7064
7202
|
}
|
|
7065
7203
|
function tryGit(cwd, args) {
|
|
7066
7204
|
try {
|
|
7067
|
-
return
|
|
7205
|
+
return git3(cwd, args);
|
|
7068
7206
|
} catch {
|
|
7069
7207
|
return void 0;
|
|
7070
7208
|
}
|
|
@@ -7079,7 +7217,7 @@ function translationLookup(repoRoot, base, locale) {
|
|
|
7079
7217
|
const build = (side) => {
|
|
7080
7218
|
const map = /* @__PURE__ */ new Map();
|
|
7081
7219
|
for (const file of files) {
|
|
7082
|
-
const content = side === "new" ? readText(
|
|
7220
|
+
const content = side === "new" ? readText(path23.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
|
|
7083
7221
|
if (content === void 0) continue;
|
|
7084
7222
|
for (const entry of parseTranslationFile(file, content)) {
|
|
7085
7223
|
if (!map.has(entry.key)) map.set(entry.key, entry.value);
|
|
@@ -7103,20 +7241,20 @@ function readText(file) {
|
|
|
7103
7241
|
return void 0;
|
|
7104
7242
|
}
|
|
7105
7243
|
}
|
|
7106
|
-
var CACHE_FILE2 =
|
|
7244
|
+
var CACHE_FILE2 = path23.join(".piwi", "locator-index.json");
|
|
7107
7245
|
function cacheKey2(connection, branch) {
|
|
7108
7246
|
return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
|
|
7109
7247
|
}
|
|
7110
7248
|
function readCache2(root, key) {
|
|
7111
7249
|
try {
|
|
7112
|
-
const store = JSON.parse(fs18.readFileSync(
|
|
7250
|
+
const store = JSON.parse(fs18.readFileSync(path23.join(root, CACHE_FILE2), "utf-8"));
|
|
7113
7251
|
return store[key] ?? null;
|
|
7114
7252
|
} catch {
|
|
7115
7253
|
return null;
|
|
7116
7254
|
}
|
|
7117
7255
|
}
|
|
7118
7256
|
function writeCache2(root, key, index) {
|
|
7119
|
-
const file =
|
|
7257
|
+
const file = path23.join(root, CACHE_FILE2);
|
|
7120
7258
|
try {
|
|
7121
7259
|
let store = {};
|
|
7122
7260
|
try {
|
|
@@ -7124,7 +7262,7 @@ function writeCache2(root, key, index) {
|
|
|
7124
7262
|
} catch {
|
|
7125
7263
|
}
|
|
7126
7264
|
store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
|
|
7127
|
-
fs18.mkdirSync(
|
|
7265
|
+
fs18.mkdirSync(path23.dirname(file), { recursive: true });
|
|
7128
7266
|
fs18.writeFileSync(file, JSON.stringify(store));
|
|
7129
7267
|
} catch {
|
|
7130
7268
|
}
|
|
@@ -7141,7 +7279,7 @@ function planSites(b, testRoot) {
|
|
|
7141
7279
|
const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
|
|
7142
7280
|
return sites.map((callSite) => {
|
|
7143
7281
|
if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
|
|
7144
|
-
const file =
|
|
7282
|
+
const file = path23.resolve(testRoot, callSiteFile(callSite));
|
|
7145
7283
|
const lineNo = callSiteLine(callSite);
|
|
7146
7284
|
const text = readText(file);
|
|
7147
7285
|
if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
|
|
@@ -7305,10 +7443,22 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7305
7443
|
console.error("piwi preflight: not inside a git repository");
|
|
7306
7444
|
return EXIT_ERROR8;
|
|
7307
7445
|
}
|
|
7308
|
-
const testRoot =
|
|
7446
|
+
const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
|
|
7309
7447
|
let files;
|
|
7310
7448
|
try {
|
|
7311
|
-
files = parseUnifiedDiff(
|
|
7449
|
+
files = parseUnifiedDiff(
|
|
7450
|
+
git3(repoRoot, [
|
|
7451
|
+
"diff",
|
|
7452
|
+
"--full-index",
|
|
7453
|
+
"--unified=0",
|
|
7454
|
+
"--no-color",
|
|
7455
|
+
"--no-ext-diff",
|
|
7456
|
+
"--src-prefix=a/",
|
|
7457
|
+
"--dst-prefix=b/",
|
|
7458
|
+
args.base,
|
|
7459
|
+
"--"
|
|
7460
|
+
])
|
|
7461
|
+
);
|
|
7312
7462
|
} catch (e) {
|
|
7313
7463
|
console.error(`piwi preflight: could not diff against ${args.base} \u2014 ${e.message.split("\n")[0]}`);
|
|
7314
7464
|
return EXIT_ERROR8;
|
|
@@ -7342,7 +7492,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7342
7492
|
testIdAttributes: index.testIdAttributes ?? void 0,
|
|
7343
7493
|
isTestFile: (file) => TEST_FILE.test(file),
|
|
7344
7494
|
translations: translationLookup(repoRoot, args.base, args.locale),
|
|
7345
|
-
readFile: (file, side) => side === "new" ? readText(
|
|
7495
|
+
readFile: (file, side) => side === "new" ? readText(path23.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
|
|
7346
7496
|
});
|
|
7347
7497
|
const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
|
|
7348
7498
|
const replacementsOf = /* @__PURE__ */ new Map();
|
|
@@ -7360,14 +7510,15 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7360
7510
|
return pb;
|
|
7361
7511
|
});
|
|
7362
7512
|
let impact = null;
|
|
7363
|
-
const changed = files
|
|
7513
|
+
const { changedFiles: changed, changes } = diffImpactInput(files);
|
|
7364
7514
|
const breakSpecs = [...new Set(breaks.flatMap((b) => b.tests.map((t) => t.file)))];
|
|
7365
7515
|
if (projectId !== null && (changed.length || args.run && breakSpecs.length)) {
|
|
7366
7516
|
try {
|
|
7367
7517
|
impact = await fetchImpact(
|
|
7368
7518
|
{ ...connection, key: "impact", format: "args", pkgRunner: args.pkgRunner },
|
|
7369
7519
|
projectId,
|
|
7370
|
-
args.run ? [.../* @__PURE__ */ new Set([...changed, ...breakSpecs])] : changed
|
|
7520
|
+
args.run ? [.../* @__PURE__ */ new Set([...changed, ...breakSpecs])] : changed,
|
|
7521
|
+
changes
|
|
7371
7522
|
);
|
|
7372
7523
|
} catch (e) {
|
|
7373
7524
|
console.error(`piwi preflight: could not ask which tests reach the changed files \u2014 ${e.message}`);
|
|
@@ -7387,8 +7538,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7387
7538
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
7388
7539
|
else {
|
|
7389
7540
|
const rel = (file) => {
|
|
7390
|
-
const abs =
|
|
7391
|
-
const r =
|
|
7541
|
+
const abs = path23.isAbsolute(file) ? file : path23.resolve(testRoot, file);
|
|
7542
|
+
const r = path23.relative(cwd, abs).split(path23.sep).join("/");
|
|
7392
7543
|
return r.startsWith("..") ? file : r || file;
|
|
7393
7544
|
};
|
|
7394
7545
|
console.log(renderPreflight(result, rel));
|
|
@@ -7420,7 +7571,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7420
7571
|
// src/cli/probe.ts
|
|
7421
7572
|
var fs19 = __toESM(require("fs"));
|
|
7422
7573
|
var os3 = __toESM(require("os"));
|
|
7423
|
-
var
|
|
7574
|
+
var path24 = __toESM(require("path"));
|
|
7424
7575
|
var EXIT_OK8 = 0;
|
|
7425
7576
|
var EXIT_ERROR9 = 2;
|
|
7426
7577
|
var USAGE11 = `
|
|
@@ -7484,10 +7635,10 @@ async function runProbe(argv) {
|
|
|
7484
7635
|
return EXIT_OK8;
|
|
7485
7636
|
}
|
|
7486
7637
|
console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
|
|
7487
|
-
const dir = fs19.mkdtempSync(
|
|
7488
|
-
const planFile =
|
|
7489
|
-
const resultsFile =
|
|
7490
|
-
const runOutputFile =
|
|
7638
|
+
const dir = fs19.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
|
|
7639
|
+
const planFile = path24.join(dir, "plan.json");
|
|
7640
|
+
const resultsFile = path24.join(dir, "results.jsonl");
|
|
7641
|
+
const runOutputFile = path24.join(dir, "run.json");
|
|
7491
7642
|
fs19.writeFileSync(planFile, JSON.stringify(plan));
|
|
7492
7643
|
fs19.writeFileSync(resultsFile, "");
|
|
7493
7644
|
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
|