@piwitests/reporter 0.55.0 → 0.57.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 +1392 -1377
- package/dist/global-setup-module.js +32 -22
- package/dist/index.d.ts +9 -0
- package/dist/index.js +555 -347
- package/dist/internal/capture/capture-fixtures.d.ts +2 -1
- package/dist/internal/capture/capture-fixtures.js +1035 -980
- package/dist/internal/capture/locator-healing.js +70 -26
- package/dist/internal/capture/pick-on-failure.js +84 -54
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -24,8 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli/ai.ts
|
|
27
|
-
var
|
|
28
|
-
var import_node_child_process = require("child_process");
|
|
27
|
+
var path6 = __toESM(require("path"));
|
|
29
28
|
|
|
30
29
|
// src/internal/ai/check.ts
|
|
31
30
|
var fs2 = __toESM(require("fs"));
|
|
@@ -38,8 +37,8 @@ var path2 = __toESM(require("path"));
|
|
|
38
37
|
// src/internal/capture/locator-healing.ts
|
|
39
38
|
var path = __toESM(require("path"));
|
|
40
39
|
|
|
41
|
-
// ../core/src/locator-
|
|
42
|
-
var
|
|
40
|
+
// ../core/src/locator-chain.ts
|
|
41
|
+
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
43
42
|
"getByRole",
|
|
44
43
|
"getByTestId",
|
|
45
44
|
"getByText",
|
|
@@ -47,677 +46,565 @@ var LOCATOR_BUILDER_METHODS = [
|
|
|
47
46
|
"getByPlaceholder",
|
|
48
47
|
"getByAltText",
|
|
49
48
|
"getByTitle",
|
|
50
|
-
"locator"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return 25;
|
|
61
|
-
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
62
|
-
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
63
|
-
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
64
|
-
if (className.includes("_")) return 15;
|
|
65
|
-
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
66
|
-
return 15;
|
|
67
|
-
}
|
|
68
|
-
function isAutoGenerated(value) {
|
|
69
|
-
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
70
|
-
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
71
|
-
if (/^[a-z]+-\d{4,}$/.test(value)) return true;
|
|
72
|
-
if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
|
|
73
|
-
value
|
|
74
|
-
)) {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
78
|
-
if (value.startsWith("ng-")) return true;
|
|
79
|
-
if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
|
|
80
|
-
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
81
|
-
if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// src/internal/capture/locator-healing.ts
|
|
86
|
-
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
87
|
-
var ACTION_METHODS = [
|
|
88
|
-
"click",
|
|
89
|
-
"fill",
|
|
90
|
-
"check",
|
|
91
|
-
"uncheck",
|
|
92
|
-
"selectOption",
|
|
93
|
-
"dblclick",
|
|
94
|
-
"tap",
|
|
95
|
-
"hover",
|
|
96
|
-
"press",
|
|
97
|
-
"type",
|
|
98
|
-
"pressSequentially",
|
|
99
|
-
"clear",
|
|
100
|
-
"setInputFiles",
|
|
101
|
-
"dragTo",
|
|
102
|
-
"focus",
|
|
103
|
-
"blur",
|
|
104
|
-
"scrollIntoViewIfNeeded",
|
|
105
|
-
"dispatchEvent",
|
|
106
|
-
"selectText",
|
|
107
|
-
// Not an action, but a successful waitFor proves the element resolved — the
|
|
108
|
-
// closest capture hook available for assertion-style usage of a locator.
|
|
109
|
-
"waitFor"
|
|
110
|
-
];
|
|
111
|
-
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
112
|
-
|
|
113
|
-
// src/internal/ai/artifact.ts
|
|
114
|
-
var ARTIFACT_VERSION = 1;
|
|
115
|
-
var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
|
|
116
|
-
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
117
|
-
var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
|
|
49
|
+
"locator",
|
|
50
|
+
"frameLocator"
|
|
51
|
+
]);
|
|
52
|
+
var NARROWING_METHODS = /* @__PURE__ */ new Set([
|
|
53
|
+
"filter",
|
|
54
|
+
"first",
|
|
55
|
+
"last",
|
|
56
|
+
"nth",
|
|
57
|
+
"and",
|
|
58
|
+
"or",
|
|
118
59
|
"visible",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"url"
|
|
60
|
+
"contentFrame",
|
|
61
|
+
"owner"
|
|
122
62
|
]);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const v = value[key];
|
|
129
|
-
if (v !== void 0) out[key] = sortDeep(v);
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
63
|
+
var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
|
|
64
|
+
var LocatorParseError = class extends Error {
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "LocatorParseError";
|
|
132
68
|
}
|
|
133
|
-
return value;
|
|
134
|
-
}
|
|
135
|
-
function serializeEntry(entry) {
|
|
136
|
-
return `${JSON.stringify(sortDeep(entry), null, 2)}
|
|
137
|
-
`;
|
|
138
|
-
}
|
|
139
|
-
var ArtifactError = class extends Error {
|
|
140
69
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
70
|
+
var SIMPLE_ESCAPES = {
|
|
71
|
+
n: "\n",
|
|
72
|
+
t: " ",
|
|
73
|
+
r: "\r",
|
|
74
|
+
b: "\b",
|
|
75
|
+
f: "\f",
|
|
76
|
+
v: "\v",
|
|
77
|
+
"0": "\0"
|
|
78
|
+
};
|
|
79
|
+
var Parser = class {
|
|
80
|
+
constructor(src) {
|
|
81
|
+
this.src = src;
|
|
82
|
+
this.i = 0;
|
|
153
83
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
assert(typeof step.action === "string", `${where}: action must be a string`);
|
|
160
|
-
assert(ACTION_METHOD_SET.has(step.action), `${where}: action "${String(step.action)}" is not allowlisted`);
|
|
161
|
-
if (step.value !== void 0) assert(typeof step.value === "string", `${where}: value must be a string`);
|
|
162
|
-
if (step.optional !== void 0) assert(typeof step.optional === "boolean", `${where}: optional must be a boolean`);
|
|
163
|
-
if (step.waitForResponse !== void 0) {
|
|
164
|
-
assert(typeof step.waitForResponse === "string", `${where}: waitForResponse must be a string`);
|
|
84
|
+
parseTopLevel() {
|
|
85
|
+
const chain = this.parseChain();
|
|
86
|
+
this.skipSpace();
|
|
87
|
+
if (this.i < this.src.length) this.fail("unexpected text");
|
|
88
|
+
return chain;
|
|
165
89
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
assert(value !== null && typeof value === "object", "postcondition must be an object");
|
|
169
|
-
const post = value;
|
|
170
|
-
assert(
|
|
171
|
-
POSTCONDITION_ASSERTS.has(post.assert),
|
|
172
|
-
`postcondition.assert "${String(post.assert)}" is not supported`
|
|
173
|
-
);
|
|
174
|
-
if (post.assert === "url") {
|
|
175
|
-
assert(typeof post.url === "string", "postcondition.url must be a string for a url assert");
|
|
176
|
-
} else {
|
|
177
|
-
validateStructuredLocator(post.locator, "postcondition.locator");
|
|
90
|
+
fail(message) {
|
|
91
|
+
throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
|
|
178
92
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
let raw;
|
|
182
|
-
try {
|
|
183
|
-
raw = JSON.parse(text);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
throw new ArtifactError(`entry is not valid JSON: ${error.message}`);
|
|
93
|
+
skipSpace() {
|
|
94
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
|
186
95
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
assert(obj.version === ARTIFACT_VERSION, `unsupported entry version ${String(obj.version)}`);
|
|
190
|
-
assert(typeof obj.template === "string", "entry.template must be a string");
|
|
191
|
-
if (obj.kind === "locator") {
|
|
192
|
-
validateStructuredLocator(obj.locator, "entry.locator");
|
|
193
|
-
return obj;
|
|
96
|
+
peek() {
|
|
97
|
+
return this.src[this.i];
|
|
194
98
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return obj;
|
|
99
|
+
expect(ch) {
|
|
100
|
+
this.skipSpace();
|
|
101
|
+
if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
|
|
102
|
+
this.i++;
|
|
200
103
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
var path3 = __toESM(require("path"));
|
|
207
|
-
var DEFAULT_AI_DIR = "__piwi__";
|
|
208
|
-
function normalizeTemplate(template) {
|
|
209
|
-
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// src/internal/ai/check.ts
|
|
213
|
-
function findEntryFiles(root, dir) {
|
|
214
|
-
const out = [];
|
|
215
|
-
const walk = (current) => {
|
|
216
|
-
let items;
|
|
217
|
-
try {
|
|
218
|
-
items = fs2.readdirSync(current, { withFileTypes: true });
|
|
219
|
-
} catch {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
for (const item of items) {
|
|
223
|
-
const full = path4.join(current, item.name);
|
|
224
|
-
if (item.isDirectory()) {
|
|
225
|
-
walk(full);
|
|
226
|
-
} else if (item.isFile() && item.name.endsWith(".json")) {
|
|
227
|
-
const specFileDir = path4.dirname(full);
|
|
228
|
-
const dirName = path4.basename(path4.dirname(specFileDir));
|
|
229
|
-
if (dirName !== dir) continue;
|
|
230
|
-
const specDir = path4.dirname(path4.dirname(specFileDir));
|
|
231
|
-
const specFile = path4.join(specDir, path4.basename(specFileDir));
|
|
232
|
-
out.push({ file: full, specFile, testSlug: item.name.split(".")[0] });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
walk(root);
|
|
237
|
-
return out;
|
|
238
|
-
}
|
|
239
|
-
function readTextOr(file, fallback) {
|
|
240
|
-
try {
|
|
241
|
-
return fs2.readFileSync(file, "utf8");
|
|
242
|
-
} catch {
|
|
243
|
-
return fallback;
|
|
104
|
+
identifier() {
|
|
105
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
|
|
106
|
+
if (!m) this.fail("expected a name");
|
|
107
|
+
this.i += m[0].length;
|
|
108
|
+
return m[0];
|
|
244
109
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
});
|
|
110
|
+
/**
|
|
111
|
+
* A chain of calls joined by `.`. With `lenient`, parsing stops before the
|
|
112
|
+
* first link that doesn't parse instead of failing, so a chain followed by
|
|
113
|
+
* other text (an error message) is read up to where it ends. `spans` receives
|
|
114
|
+
* where each top-level call starts and ends.
|
|
115
|
+
*/
|
|
116
|
+
parseChain(opts = {}) {
|
|
117
|
+
this.skipSpace();
|
|
118
|
+
const calls = [];
|
|
119
|
+
for (; ; ) {
|
|
120
|
+
const before = this.i;
|
|
121
|
+
if (calls.length > 0) this.i++;
|
|
122
|
+
this.skipSpace();
|
|
123
|
+
const start = this.i;
|
|
124
|
+
try {
|
|
125
|
+
calls.push(this.parseCall(calls.length === 0));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!opts.lenient || calls.length === 0) throw error;
|
|
128
|
+
this.i = before;
|
|
129
|
+
break;
|
|
266
130
|
}
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
file: rel(found.file),
|
|
274
|
-
message: `spec file is gone (${rel(found.specFile)}) \u2014 the entry is orphaned`
|
|
275
|
-
});
|
|
276
|
-
} else if (!source.includes(entry.template.trim())) {
|
|
277
|
-
findings.push({
|
|
278
|
-
severity: "error",
|
|
279
|
-
kind: "orphan",
|
|
280
|
-
file: rel(found.file),
|
|
281
|
-
message: `template "${entry.template}" no longer appears in ${rel(found.specFile)} \u2014 the entry is orphaned`
|
|
282
|
-
});
|
|
131
|
+
opts.spans?.push({ start, end: this.i });
|
|
132
|
+
const end = this.i;
|
|
133
|
+
this.skipSpace();
|
|
134
|
+
if (this.peek() !== ".") {
|
|
135
|
+
this.i = end;
|
|
136
|
+
break;
|
|
283
137
|
}
|
|
284
|
-
} catch (error) {
|
|
285
|
-
findings.push({
|
|
286
|
-
severity: "error",
|
|
287
|
-
kind: "invalid",
|
|
288
|
-
file: rel(found.file),
|
|
289
|
-
message: `not a valid entry: ${error.message}`
|
|
290
|
-
});
|
|
291
138
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
139
|
+
return { calls };
|
|
140
|
+
}
|
|
141
|
+
/** Where the parser stopped. */
|
|
142
|
+
get position() {
|
|
143
|
+
return this.i;
|
|
144
|
+
}
|
|
145
|
+
parseCall(first) {
|
|
146
|
+
this.skipSpace();
|
|
147
|
+
const method = this.identifier();
|
|
148
|
+
if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
|
|
149
|
+
if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
|
|
150
|
+
this.expect("(");
|
|
151
|
+
const args = [];
|
|
152
|
+
this.skipSpace();
|
|
153
|
+
if (this.peek() === ")") {
|
|
154
|
+
this.i++;
|
|
155
|
+
return { method, args };
|
|
156
|
+
}
|
|
157
|
+
for (; ; ) {
|
|
158
|
+
args.push(this.parseValue());
|
|
159
|
+
this.skipSpace();
|
|
160
|
+
if (this.peek() === ",") {
|
|
161
|
+
this.i++;
|
|
162
|
+
this.skipSpace();
|
|
163
|
+
if (this.peek() === ")") {
|
|
164
|
+
this.i++;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
this.expect(")");
|
|
170
|
+
break;
|
|
297
171
|
}
|
|
172
|
+
return { method, args };
|
|
298
173
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
174
|
+
parseValue() {
|
|
175
|
+
this.skipSpace();
|
|
176
|
+
const ch = this.peek();
|
|
177
|
+
if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
|
|
178
|
+
if (ch === "/") return this.parseRegex();
|
|
179
|
+
if (ch === "{") return this.parseObject();
|
|
180
|
+
if (ch !== void 0 && /[-\d]/.test(ch)) {
|
|
181
|
+
const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
|
|
182
|
+
if (!m) this.fail("expected a number");
|
|
183
|
+
this.i += m[0].length;
|
|
184
|
+
return { type: "number", value: Number(m[0]) };
|
|
185
|
+
}
|
|
186
|
+
const rest = this.src.slice(this.i);
|
|
187
|
+
if (/^true\b/.test(rest)) {
|
|
188
|
+
this.i += 4;
|
|
189
|
+
return { type: "boolean", value: true };
|
|
190
|
+
}
|
|
191
|
+
if (/^false\b/.test(rest)) {
|
|
192
|
+
this.i += 5;
|
|
193
|
+
return { type: "boolean", value: false };
|
|
308
194
|
}
|
|
195
|
+
if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
|
|
196
|
+
return this.fail("expected a value");
|
|
309
197
|
}
|
|
310
|
-
|
|
198
|
+
/** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
|
|
199
|
+
parseString() {
|
|
200
|
+
const quote3 = this.src[this.i];
|
|
201
|
+
let out = "";
|
|
202
|
+
this.i++;
|
|
203
|
+
while (this.i < this.src.length) {
|
|
204
|
+
const c = this.src[this.i];
|
|
205
|
+
if (c === quote3) {
|
|
206
|
+
this.i++;
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
if (c !== "\\") {
|
|
210
|
+
out += c;
|
|
211
|
+
this.i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const next = this.src[this.i + 1];
|
|
215
|
+
if (next === void 0) break;
|
|
216
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
217
|
+
if (simple !== void 0) {
|
|
218
|
+
out += simple;
|
|
219
|
+
this.i += 2;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
|
|
223
|
+
if (hex) {
|
|
224
|
+
out += String.fromCodePoint(parseInt(hex[1], 16));
|
|
225
|
+
this.i += 1 + hex[0].length;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (next === "u" || next === "x") this.fail("malformed escape");
|
|
229
|
+
out += next;
|
|
230
|
+
this.i += 2;
|
|
231
|
+
}
|
|
232
|
+
return this.fail("unterminated string");
|
|
233
|
+
}
|
|
234
|
+
parseRegex() {
|
|
235
|
+
let j = this.i + 1;
|
|
236
|
+
let inClass = false;
|
|
237
|
+
while (j < this.src.length) {
|
|
238
|
+
const c = this.src[j];
|
|
239
|
+
if (c === "\\") {
|
|
240
|
+
j += 2;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (c === "[") inClass = true;
|
|
244
|
+
else if (c === "]") inClass = false;
|
|
245
|
+
else if (c === "/" && !inClass) break;
|
|
246
|
+
j++;
|
|
247
|
+
}
|
|
248
|
+
if (j >= this.src.length) this.fail("unterminated regex");
|
|
249
|
+
const source = this.src.slice(this.i + 1, j);
|
|
250
|
+
const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
|
|
251
|
+
this.i = j + 1 + flags.length;
|
|
252
|
+
return { type: "regex", source, flags };
|
|
253
|
+
}
|
|
254
|
+
parseObject() {
|
|
255
|
+
this.expect("{");
|
|
256
|
+
const entries = [];
|
|
257
|
+
for (; ; ) {
|
|
258
|
+
this.skipSpace();
|
|
259
|
+
if (this.peek() === "}") {
|
|
260
|
+
this.i++;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
const ch = this.peek();
|
|
264
|
+
const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
|
|
265
|
+
this.expect(":");
|
|
266
|
+
entries.push([key, this.parseValue()]);
|
|
267
|
+
this.skipSpace();
|
|
268
|
+
if (this.peek() === ",") {
|
|
269
|
+
this.i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
this.expect("}");
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
return { type: "object", entries };
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
function parseLocatorChain(expr) {
|
|
279
|
+
const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
|
|
280
|
+
if (!trimmed) throw new LocatorParseError("empty expression");
|
|
281
|
+
return new Parser(trimmed).parseTopLevel();
|
|
311
282
|
}
|
|
312
|
-
function
|
|
313
|
-
|
|
283
|
+
function tryParseLocatorChain(expr) {
|
|
284
|
+
try {
|
|
285
|
+
return parseLocatorChain(expr);
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function quoteJsString(value) {
|
|
291
|
+
const json = JSON.stringify(value).slice(1, -1);
|
|
292
|
+
return `'${json.replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
|
|
293
|
+
}
|
|
294
|
+
function renderArg(arg) {
|
|
295
|
+
switch (arg.type) {
|
|
296
|
+
case "string":
|
|
297
|
+
return quoteJsString(arg.value);
|
|
298
|
+
case "regex":
|
|
299
|
+
return `/${arg.source}/${arg.flags}`;
|
|
300
|
+
case "number":
|
|
301
|
+
case "boolean":
|
|
302
|
+
return String(arg.value);
|
|
303
|
+
case "object":
|
|
304
|
+
return arg.entries.length === 0 ? "{}" : `{ ${arg.entries.map(([k, v]) => `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : quoteJsString(k)}: ${renderArg(v)}`).join(", ")} }`;
|
|
305
|
+
case "chain":
|
|
306
|
+
return renderLocatorChain(arg.chain);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function renderLocatorCall(call) {
|
|
310
|
+
return `${call.method}(${call.args.map(renderArg).join(", ")})`;
|
|
311
|
+
}
|
|
312
|
+
function renderLocatorChain(chain) {
|
|
313
|
+
return chain.calls.map(renderLocatorCall).join(".");
|
|
314
|
+
}
|
|
315
|
+
function canonicalLocator(expr) {
|
|
316
|
+
const chain = tryParseLocatorChain(expr);
|
|
317
|
+
return chain ? renderLocatorChain(chain) : null;
|
|
314
318
|
}
|
|
315
319
|
|
|
316
|
-
// src/
|
|
317
|
-
var
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
var USAGE = `
|
|
321
|
-
Usage: piwi ai <command> [options]
|
|
322
|
-
|
|
323
|
-
Commands:
|
|
324
|
-
check Scan committed AI-step entries for orphans, non-canonical files and
|
|
325
|
-
duplicate templates. Read-only; exits 1 when issues are found.
|
|
326
|
-
resolve Author missing entries by running the suite in resolve mode against
|
|
327
|
-
the configured authoring server (forces --workers=1).
|
|
328
|
-
prune Delete orphaned/dormant entries.
|
|
320
|
+
// ../core/src/locator-fingerprint.ts
|
|
321
|
+
var FUNCTION_WORDS = new Set(
|
|
322
|
+
"the and for with from your you our this that les des une pour avec par sur aux dans vos votre nos notre".split(" ")
|
|
323
|
+
);
|
|
329
324
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
325
|
+
// ../core/src/locator-methods.ts
|
|
326
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
327
|
+
"getByRole",
|
|
328
|
+
"getByTestId",
|
|
329
|
+
"getByText",
|
|
330
|
+
"getByLabel",
|
|
331
|
+
"getByPlaceholder",
|
|
332
|
+
"getByAltText",
|
|
333
|
+
"getByTitle",
|
|
334
|
+
"locator"
|
|
335
|
+
];
|
|
341
336
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
337
|
+
// ../core/src/locator-generation.ts
|
|
338
|
+
function classifyCssStability(className) {
|
|
339
|
+
if (/[a-f0-9]{8,}/i.test(className)) return 10;
|
|
340
|
+
if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
|
|
341
|
+
if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
|
|
342
|
+
className
|
|
343
|
+
))
|
|
344
|
+
return 25;
|
|
345
|
+
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
346
|
+
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
347
|
+
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
348
|
+
if (className.includes("_")) return 15;
|
|
349
|
+
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
350
|
+
return 15;
|
|
354
351
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
352
|
+
function isAutoGenerated(value) {
|
|
353
|
+
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
354
|
+
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
355
|
+
if (/^[a-z]+-\d{4,}$/.test(value)) return true;
|
|
356
|
+
if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
|
|
357
|
+
value
|
|
358
|
+
)) {
|
|
359
|
+
return true;
|
|
360
360
|
}
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return
|
|
366
|
-
|
|
361
|
+
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
362
|
+
if (value.startsWith("ng-")) return true;
|
|
363
|
+
if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
|
|
364
|
+
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
365
|
+
if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
|
|
366
|
+
return false;
|
|
367
367
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
368
|
+
|
|
369
|
+
// src/internal/capture/locator-healing.ts
|
|
370
|
+
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
371
|
+
var ACTION_METHODS = [
|
|
372
|
+
"click",
|
|
373
|
+
"fill",
|
|
374
|
+
"check",
|
|
375
|
+
"uncheck",
|
|
376
|
+
"selectOption",
|
|
377
|
+
"dblclick",
|
|
378
|
+
"tap",
|
|
379
|
+
"hover",
|
|
380
|
+
"press",
|
|
381
|
+
"type",
|
|
382
|
+
"pressSequentially",
|
|
383
|
+
"clear",
|
|
384
|
+
"setInputFiles",
|
|
385
|
+
"dragTo",
|
|
386
|
+
"focus",
|
|
387
|
+
"blur",
|
|
388
|
+
"scrollIntoViewIfNeeded",
|
|
389
|
+
"dispatchEvent",
|
|
390
|
+
"selectText",
|
|
391
|
+
// Not an action, but a successful waitFor proves the element resolved — the
|
|
392
|
+
// closest capture hook available for assertion-style usage of a locator.
|
|
393
|
+
"waitFor"
|
|
394
|
+
];
|
|
395
|
+
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
396
|
+
|
|
397
|
+
// src/internal/ai/artifact.ts
|
|
398
|
+
var ARTIFACT_VERSION = 1;
|
|
399
|
+
var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
|
|
400
|
+
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
401
|
+
var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
|
|
402
|
+
"visible",
|
|
403
|
+
"hidden",
|
|
404
|
+
"attached",
|
|
405
|
+
"url"
|
|
406
|
+
]);
|
|
407
|
+
function sortDeep(value) {
|
|
408
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
409
|
+
if (value !== null && typeof value === "object") {
|
|
410
|
+
const out = {};
|
|
411
|
+
for (const key of Object.keys(value).sort()) {
|
|
412
|
+
const v = value[key];
|
|
413
|
+
if (v !== void 0) out[key] = sortDeep(v);
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
388
416
|
}
|
|
389
|
-
return
|
|
417
|
+
return value;
|
|
390
418
|
}
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
419
|
+
function serializeEntry(entry) {
|
|
420
|
+
return `${JSON.stringify(sortDeep(entry), null, 2)}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
var ArtifactError = class extends Error {
|
|
424
|
+
};
|
|
425
|
+
function assert(condition, message) {
|
|
426
|
+
if (!condition) throw new ArtifactError(message);
|
|
427
|
+
}
|
|
428
|
+
function validateStructuredLocator(value, where) {
|
|
429
|
+
assert(value !== null && typeof value === "object", `${where}: locator must be an object`);
|
|
430
|
+
const loc = value;
|
|
431
|
+
assert(typeof loc.method === "string", `${where}: locator.method must be a string`);
|
|
432
|
+
assert(LOCATOR_METHOD_SET.has(loc.method), `${where}: locator method "${String(loc.method)}" is not allowlisted`);
|
|
433
|
+
assert(Array.isArray(loc.args), `${where}: locator.args must be an array`);
|
|
434
|
+
if (loc.chain !== void 0) {
|
|
435
|
+
assert(Array.isArray(loc.chain), `${where}: locator.chain must be an array`);
|
|
436
|
+
loc.chain.forEach((child, i) => validateStructuredLocator(child, `${where}.chain[${i}]`));
|
|
403
437
|
}
|
|
404
|
-
return { command: "npx", args, env: childEnv };
|
|
405
438
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
439
|
+
function validateRunStep(value, where) {
|
|
440
|
+
assert(value !== null && typeof value === "object", `${where}: step must be an object`);
|
|
441
|
+
const step = value;
|
|
442
|
+
validateStructuredLocator(step.locator, `${where}.locator`);
|
|
443
|
+
assert(typeof step.action === "string", `${where}: action must be a string`);
|
|
444
|
+
assert(ACTION_METHOD_SET.has(step.action), `${where}: action "${String(step.action)}" is not allowlisted`);
|
|
445
|
+
if (step.value !== void 0) assert(typeof step.value === "string", `${where}: value must be a string`);
|
|
446
|
+
if (step.optional !== void 0) assert(typeof step.optional === "boolean", `${where}: optional must be a boolean`);
|
|
447
|
+
if (step.waitForResponse !== void 0) {
|
|
448
|
+
assert(typeof step.waitForResponse === "string", `${where}: waitForResponse must be a string`);
|
|
410
449
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
450
|
+
}
|
|
451
|
+
function validatePostcondition(value) {
|
|
452
|
+
assert(value !== null && typeof value === "object", "postcondition must be an object");
|
|
453
|
+
const post = value;
|
|
454
|
+
assert(
|
|
455
|
+
POSTCONDITION_ASSERTS.has(post.assert),
|
|
456
|
+
`postcondition.assert "${String(post.assert)}" is not supported`
|
|
457
|
+
);
|
|
458
|
+
if (post.assert === "url") {
|
|
459
|
+
assert(typeof post.url === "string", "postcondition.url must be a string for a url assert");
|
|
460
|
+
} else {
|
|
461
|
+
validateStructuredLocator(post.locator, "postcondition.locator");
|
|
416
462
|
}
|
|
417
|
-
return result.status ?? EXIT_ERROR;
|
|
418
463
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
464
|
+
function parseEntry(text) {
|
|
465
|
+
let raw;
|
|
466
|
+
try {
|
|
467
|
+
raw = JSON.parse(text);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
throw new ArtifactError(`entry is not valid JSON: ${error.message}`);
|
|
424
470
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
console.error(USAGE);
|
|
439
|
-
return EXIT_ERROR;
|
|
471
|
+
assert(raw !== null && typeof raw === "object", "entry must be an object");
|
|
472
|
+
const obj = raw;
|
|
473
|
+
assert(obj.version === ARTIFACT_VERSION, `unsupported entry version ${String(obj.version)}`);
|
|
474
|
+
assert(typeof obj.template === "string", "entry.template must be a string");
|
|
475
|
+
if (obj.kind === "locator") {
|
|
476
|
+
validateStructuredLocator(obj.locator, "entry.locator");
|
|
477
|
+
return obj;
|
|
478
|
+
}
|
|
479
|
+
if (obj.kind === "run") {
|
|
480
|
+
assert(Array.isArray(obj.steps), "entry.steps must be an array");
|
|
481
|
+
obj.steps.forEach((step, i) => validateRunStep(step, `entry.steps[${i}]`));
|
|
482
|
+
validatePostcondition(obj.postcondition);
|
|
483
|
+
return obj;
|
|
440
484
|
}
|
|
485
|
+
throw new ArtifactError(`unsupported entry kind "${String(obj.kind)}"`);
|
|
441
486
|
}
|
|
442
487
|
|
|
443
|
-
// src/
|
|
444
|
-
var
|
|
445
|
-
var
|
|
446
|
-
var
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// src/internal/config/desktop.ts
|
|
450
|
-
var fs3 = __toESM(require("fs"));
|
|
451
|
-
var os = __toESM(require("os"));
|
|
452
|
-
var path6 = __toESM(require("path"));
|
|
453
|
-
function defaultDesktopConfigPath() {
|
|
454
|
-
return path6.join(os.homedir(), ".piwi", "desktop.json");
|
|
488
|
+
// src/internal/ai/keys.ts
|
|
489
|
+
var crypto = __toESM(require("crypto"));
|
|
490
|
+
var path3 = __toESM(require("path"));
|
|
491
|
+
var DEFAULT_AI_DIR = "__piwi__";
|
|
492
|
+
function normalizeTemplate(template) {
|
|
493
|
+
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
455
494
|
}
|
|
456
|
-
function readDesktopConfig(filePath2) {
|
|
457
|
-
try {
|
|
458
|
-
const parsed = JSON.parse(fs3.readFileSync(filePath2, "utf8"));
|
|
459
|
-
const url = parsed?.url;
|
|
460
|
-
const token = parsed?.token;
|
|
461
|
-
if (typeof url !== "string" || !url) return null;
|
|
462
|
-
if (typeof token !== "string" || !token) return null;
|
|
463
|
-
return { url, token };
|
|
464
|
-
} catch {
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// src/internal/config/env.ts
|
|
470
|
-
var DEFAULTS = {
|
|
471
|
-
projectName: "default-project",
|
|
472
|
-
uploadTraces: true,
|
|
473
|
-
uploadReport: true,
|
|
474
|
-
liveFileUploads: true,
|
|
475
|
-
collectScmInfo: true,
|
|
476
|
-
collectCiInfo: true,
|
|
477
|
-
collectPerformanceMetrics: true,
|
|
478
|
-
captureLocators: true,
|
|
479
|
-
capturePageState: true,
|
|
480
|
-
// Off by default: the page inventory reads control and link names from every
|
|
481
|
-
// visited page, so it stays opt-in until a project turns it on.
|
|
482
|
-
capturePageInventory: false,
|
|
483
|
-
// Off by default: JavaScript coverage adds time to every test, so a scheduled job opts in.
|
|
484
|
-
captureCodeReach: false,
|
|
485
|
-
captureServerTraces: true,
|
|
486
|
-
captureResources: true,
|
|
487
|
-
leakCheck: "report",
|
|
488
|
-
sampleAriaOnPass: true,
|
|
489
|
-
uploadManifest: true,
|
|
490
|
-
defaultCapture: true,
|
|
491
|
-
checkBaseUrl: false,
|
|
492
|
-
streaming: true,
|
|
493
|
-
streamingBatchSize: 20,
|
|
494
|
-
streamingBatchDelay: 2e3,
|
|
495
|
-
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
496
|
-
submitTimeout: 15 * 60 * 1e3,
|
|
497
|
-
failOnFlakyTests: false,
|
|
498
|
-
username: null,
|
|
499
|
-
password: null,
|
|
500
|
-
apiKey: null,
|
|
501
|
-
verbose: false
|
|
502
|
-
};
|
|
503
|
-
var PIWI_ENV_KEYS = {
|
|
504
|
-
serverUrl: "PIWI_DASHBOARD_URL",
|
|
505
|
-
projectName: "PIWI_PROJECT_NAME",
|
|
506
|
-
verbose: "PIWI_VERBOSE",
|
|
507
|
-
apiKey: "PIWI_API_KEY",
|
|
508
|
-
username: "PIWI_USERNAME",
|
|
509
|
-
password: "PIWI_PASSWORD",
|
|
510
|
-
environment: "PIWI_ENVIRONMENT",
|
|
511
|
-
label: "PIWI_LABEL",
|
|
512
|
-
runLabel: "PIWI_RUN_LABEL",
|
|
513
|
-
keep: "PIWI_KEEP",
|
|
514
|
-
streaming: "PIWI_STREAMING",
|
|
515
|
-
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
516
|
-
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
517
|
-
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
518
|
-
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
519
|
-
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
520
|
-
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
521
|
-
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
522
|
-
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
523
|
-
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
524
|
-
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
525
|
-
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
526
|
-
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
527
|
-
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
528
|
-
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
529
|
-
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
530
|
-
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
531
|
-
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
532
|
-
leakCheck: "PIWI_LEAK_CHECK",
|
|
533
|
-
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
534
|
-
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
535
|
-
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
536
|
-
checkBaseUrl: "PIWI_CHECK_BASE_URL",
|
|
537
|
-
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
538
|
-
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
539
|
-
outputFile: "PIWI_OUTPUT_FILE",
|
|
540
|
-
aiMode: "PIWI_AI",
|
|
541
|
-
aiDir: "PIWI_AI_DIR",
|
|
542
|
-
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
543
|
-
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
544
|
-
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
545
|
-
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
546
|
-
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
547
|
-
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
548
|
-
};
|
|
549
|
-
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
550
|
-
var PIWI_ORIGIN_ENV = {
|
|
551
|
-
kind: "PIWI_ORIGIN",
|
|
552
|
-
ref: "PIWI_ORIGIN_REF"
|
|
553
|
-
};
|
|
554
|
-
function originEnv(kind, ref) {
|
|
555
|
-
return ref === void 0 ? { [PIWI_ORIGIN_ENV.kind]: kind } : { [PIWI_ORIGIN_ENV.kind]: kind, [PIWI_ORIGIN_ENV.ref]: String(ref) };
|
|
556
|
-
}
|
|
557
|
-
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
558
|
-
var PIWI_PROBE_ENV = {
|
|
559
|
-
flag: "PIWI_PROBE",
|
|
560
|
-
plan: "PIWI_PROBE_PLAN",
|
|
561
|
-
results: "PIWI_PROBE_RESULTS",
|
|
562
|
-
secret: "PIWI_PROBE_SECRET"
|
|
563
|
-
};
|
|
564
|
-
var PIWI_FLAKE_ENV = {
|
|
565
|
-
plan: "PIWI_FLAKE_PLAN",
|
|
566
|
-
results: "PIWI_FLAKE_RESULTS"
|
|
567
|
-
};
|
|
568
|
-
var ENV_FALLBACK_SPECS = [
|
|
569
|
-
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
570
|
-
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
571
|
-
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
572
|
-
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
573
|
-
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
574
|
-
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
575
|
-
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
576
|
-
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
577
|
-
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
578
|
-
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
579
|
-
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
580
|
-
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
581
|
-
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
582
|
-
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
583
|
-
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
584
|
-
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
585
|
-
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
586
|
-
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
587
|
-
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
588
|
-
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
589
|
-
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
590
|
-
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
591
|
-
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
592
|
-
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
593
|
-
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
594
|
-
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
595
|
-
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
596
|
-
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
597
|
-
{ option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
|
|
598
|
-
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
599
|
-
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
600
|
-
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
601
|
-
];
|
|
602
|
-
|
|
603
|
-
// src/cli/bug.ts
|
|
604
|
-
var EXIT_OK2 = 0;
|
|
605
|
-
var EXIT_RUN_FAILED = 1;
|
|
606
|
-
var EXIT_ERROR2 = 2;
|
|
607
|
-
var REQUEST_TIMEOUT_MS = 3e4;
|
|
608
|
-
var USAGE2 = `
|
|
609
|
-
piwi bug \u2014 a bug report's failing test, from the dashboard
|
|
610
|
-
|
|
611
|
-
Usage:
|
|
612
|
-
npx @piwitests/reporter bug <id> [options]
|
|
613
|
-
|
|
614
|
-
Prints the spec to commit: the report's steps, test.fail() while the bug
|
|
615
|
-
exists, @bug and piwi:bug <id>, written with the project's settings.
|
|
616
|
-
|
|
617
|
-
Options:
|
|
618
|
-
--write Write it to the project's bugs folder (tests/bugs by default, from the
|
|
619
|
-
repository root), then run it once
|
|
620
|
-
--out <file> Write it to this file instead (then run it once)
|
|
621
|
-
--no-run With --write or --out: write it without running it
|
|
622
|
-
--force Replace an existing file
|
|
623
|
-
--run-mode The spec without test.fail(), as a reproduction runs it
|
|
624
|
-
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
|
|
625
|
-
--api-key <key> API key (env PIWI_API_KEY)
|
|
626
495
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
496
|
+
// src/internal/ai/check.ts
|
|
497
|
+
function findEntryFiles(root, dir) {
|
|
498
|
+
const out = [];
|
|
499
|
+
const walk = (current) => {
|
|
500
|
+
let items;
|
|
501
|
+
try {
|
|
502
|
+
items = fs2.readdirSync(current, { withFileTypes: true });
|
|
503
|
+
} catch {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
for (const item of items) {
|
|
507
|
+
const full = path4.join(current, item.name);
|
|
508
|
+
if (item.isDirectory()) {
|
|
509
|
+
walk(full);
|
|
510
|
+
} else if (item.isFile() && item.name.endsWith(".json")) {
|
|
511
|
+
const specFileDir = path4.dirname(full);
|
|
512
|
+
const dirName = path4.basename(path4.dirname(specFileDir));
|
|
513
|
+
if (dirName !== dir) continue;
|
|
514
|
+
const specDir = path4.dirname(path4.dirname(specFileDir));
|
|
515
|
+
const specFile = path4.join(specDir, path4.basename(specFileDir));
|
|
516
|
+
out.push({ file: full, specFile, testSlug: item.name.split(".")[0] });
|
|
517
|
+
}
|
|
646
518
|
}
|
|
647
|
-
idText = token;
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
const id = Number((idText ?? "").replace(/^#/, ""));
|
|
651
|
-
if (!idText || !Number.isInteger(id) || id <= 0) throw new Error("No bug report id \u2014 usage: piwi bug <id>");
|
|
652
|
-
for (const flag of VALUE_FLAGS) {
|
|
653
|
-
if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
|
|
654
|
-
}
|
|
655
|
-
const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
|
|
656
|
-
if (!serverUrl) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
657
|
-
const out = readOption2(argv, "--out") ?? null;
|
|
658
|
-
const write = argv.includes("--write") || out !== null;
|
|
659
|
-
return {
|
|
660
|
-
id,
|
|
661
|
-
write,
|
|
662
|
-
out,
|
|
663
|
-
run: write && !argv.includes("--no-run"),
|
|
664
|
-
force: argv.includes("--force"),
|
|
665
|
-
mode: argv.includes("--run-mode") ? "run" : "commit",
|
|
666
|
-
serverUrl,
|
|
667
|
-
apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
|
|
668
519
|
};
|
|
520
|
+
walk(root);
|
|
521
|
+
return out;
|
|
669
522
|
}
|
|
670
|
-
|
|
671
|
-
const dir = specDir !== void 0 ? `&specDir=${encodeURIComponent(specDir)}` : "";
|
|
672
|
-
const url = `${args.serverUrl}/api/bug-reports/${args.id}/spec?mode=${args.mode}${dir}`;
|
|
673
|
-
let res;
|
|
523
|
+
function readTextOr(file, fallback) {
|
|
674
524
|
try {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
});
|
|
679
|
-
} catch (e) {
|
|
680
|
-
throw new Error(`could not reach ${args.serverUrl} (${e.message})`);
|
|
525
|
+
return fs2.readFileSync(file, "utf8");
|
|
526
|
+
} catch {
|
|
527
|
+
return fallback;
|
|
681
528
|
}
|
|
682
|
-
if (res.status === 404) throw new Error(`no bug report #${args.id} on ${args.serverUrl}`);
|
|
683
|
-
if (res.status === 401 || res.status === 403) throw new Error(`the dashboard refused the API key (${res.status})`);
|
|
684
|
-
if (!res.ok) throw new Error(`the dashboard answered ${res.status}`);
|
|
685
|
-
const body = await res.json();
|
|
686
|
-
if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
|
|
687
|
-
return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
|
|
688
529
|
}
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
530
|
+
function checkAiTree(root, opts = {}) {
|
|
531
|
+
const dir = opts.dir ?? DEFAULT_AI_DIR;
|
|
532
|
+
const findings = [];
|
|
533
|
+
const rel = (file) => path4.relative(root, file).split(path4.sep).join("/");
|
|
534
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
535
|
+
const duplicates = /* @__PURE__ */ new Map();
|
|
536
|
+
for (const found of findEntryFiles(root, dir)) {
|
|
537
|
+
const text = readTextOr(found.file, null);
|
|
538
|
+
if (text === null) continue;
|
|
539
|
+
let templateNormalized = null;
|
|
540
|
+
try {
|
|
541
|
+
const entry = parseEntry(text);
|
|
542
|
+
templateNormalized = normalizeTemplate(entry.template);
|
|
543
|
+
if (serializeEntry(entry) !== text) {
|
|
544
|
+
findings.push({
|
|
545
|
+
severity: "error",
|
|
546
|
+
kind: "non-canonical",
|
|
547
|
+
file: rel(found.file),
|
|
548
|
+
message: "file is not in canonical form \u2014 re-author it with `piwi ai resolve --update-ai`"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (!sourceCache.has(found.specFile)) sourceCache.set(found.specFile, readTextOr(found.specFile, null));
|
|
552
|
+
const source = sourceCache.get(found.specFile) ?? null;
|
|
553
|
+
if (source === null) {
|
|
554
|
+
findings.push({
|
|
555
|
+
severity: "error",
|
|
556
|
+
kind: "orphan",
|
|
557
|
+
file: rel(found.file),
|
|
558
|
+
message: `spec file is gone (${rel(found.specFile)}) \u2014 the entry is orphaned`
|
|
559
|
+
});
|
|
560
|
+
} else if (!source.includes(entry.template.trim())) {
|
|
561
|
+
findings.push({
|
|
562
|
+
severity: "error",
|
|
563
|
+
kind: "orphan",
|
|
564
|
+
file: rel(found.file),
|
|
565
|
+
message: `template "${entry.template}" no longer appears in ${rel(found.specFile)} \u2014 the entry is orphaned`
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
findings.push({
|
|
570
|
+
severity: "error",
|
|
571
|
+
kind: "invalid",
|
|
572
|
+
file: rel(found.file),
|
|
573
|
+
message: `not a valid entry: ${error.message}`
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (templateNormalized !== null) {
|
|
577
|
+
const key = `${found.specFile}::${found.testSlug}::${templateNormalized}`;
|
|
578
|
+
const list = duplicates.get(key) ?? [];
|
|
579
|
+
list.push(rel(found.file));
|
|
580
|
+
duplicates.set(key, list);
|
|
581
|
+
}
|
|
699
582
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
583
|
+
for (const files of duplicates.values()) {
|
|
584
|
+
if (files.length < 2) continue;
|
|
585
|
+
for (const file of files) {
|
|
586
|
+
findings.push({
|
|
587
|
+
severity: "warning",
|
|
588
|
+
kind: "duplicate-template",
|
|
589
|
+
file,
|
|
590
|
+
message: `duplicate template within the same test \u2014 consider distinct phrasings (${files.join(", ")})`
|
|
591
|
+
});
|
|
592
|
+
}
|
|
707
593
|
}
|
|
708
|
-
return
|
|
709
|
-
}
|
|
710
|
-
function folderInRepository(root, dir) {
|
|
711
|
-
const relative8 = path7.relative(realFolder(root), realFolder(dir));
|
|
712
|
-
if (relative8.startsWith("..") || path7.isAbsolute(relative8)) return void 0;
|
|
713
|
-
return relative8.split(path7.sep).join("/");
|
|
594
|
+
return findings.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
|
|
714
595
|
}
|
|
715
|
-
function
|
|
716
|
-
|
|
717
|
-
return `${escape(path7.basename(path7.dirname(file)))}[\\\\/]${escape(path7.basename(file))}$`;
|
|
596
|
+
function hasBlockingFindings(findings) {
|
|
597
|
+
return findings.some((f) => f.severity === "error");
|
|
718
598
|
}
|
|
719
|
-
|
|
720
|
-
|
|
599
|
+
|
|
600
|
+
// src/internal/support/playwright-spawn.ts
|
|
601
|
+
var import_node_child_process = require("child_process");
|
|
602
|
+
var fs3 = __toESM(require("fs"));
|
|
603
|
+
var import_node_module = require("module");
|
|
604
|
+
var path5 = __toESM(require("path"));
|
|
605
|
+
var COULD_NOT_START = 2;
|
|
606
|
+
function resolvePlaywrightCli(cwd = process.cwd()) {
|
|
607
|
+
const require2 = (0, import_node_module.createRequire)(path5.join(cwd, "noop.js"));
|
|
721
608
|
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
722
609
|
try {
|
|
723
610
|
return require2.resolve(id);
|
|
@@ -726,479 +613,639 @@ function resolvePlaywrightCli(cwd) {
|
|
|
726
613
|
}
|
|
727
614
|
return null;
|
|
728
615
|
}
|
|
729
|
-
function
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
616
|
+
function resolveNpmEntry(name, execPath = process.execPath) {
|
|
617
|
+
const dir = path5.dirname(execPath);
|
|
618
|
+
for (const candidate of [
|
|
619
|
+
path5.join(dir, "node_modules", "npm", "bin", `${name}-cli.js`),
|
|
620
|
+
path5.join(dir, "..", "lib", "node_modules", "npm", "bin", `${name}-cli.js`)
|
|
621
|
+
]) {
|
|
622
|
+
if (fs3.existsSync(candidate)) return candidate;
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
function playwrightInvocation(playwrightArgs2, options = {}) {
|
|
627
|
+
const cli = resolvePlaywrightCli(options.cwd);
|
|
628
|
+
if (cli) return { file: process.execPath, args: [cli, "test", ...playwrightArgs2] };
|
|
629
|
+
const runner = options.runner ?? "npx";
|
|
630
|
+
if ((options.platform ?? process.platform) !== "win32") {
|
|
631
|
+
return { file: runner, args: ["playwright", "test", ...playwrightArgs2] };
|
|
632
|
+
}
|
|
633
|
+
const npx = runner === "npx" ? resolveNpmEntry("npx") : null;
|
|
634
|
+
return npx ? { file: process.execPath, args: [npx, "playwright", "test", ...playwrightArgs2] } : null;
|
|
635
|
+
}
|
|
636
|
+
function spawnPlaywright(playwrightArgs2, env, options = {}) {
|
|
637
|
+
const label = options.command ?? "piwi";
|
|
638
|
+
const cwd = options.cwd ?? process.cwd();
|
|
639
|
+
const invocation = playwrightInvocation(playwrightArgs2, { cwd, runner: options.runner });
|
|
640
|
+
if (!invocation) {
|
|
641
|
+
console.error(
|
|
642
|
+
`${label}: could not find Playwright in ${cwd} \u2014 install it there (npm install --save-dev @playwright/test).`
|
|
643
|
+
);
|
|
644
|
+
return Promise.resolve(COULD_NOT_START);
|
|
645
|
+
}
|
|
646
|
+
const output = options.output ?? "inherit";
|
|
647
|
+
let fd = output === "inherit" ? null : fs3.openSync(output.file, "a");
|
|
648
|
+
const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
|
|
649
|
+
const child = (0, import_node_child_process.spawn)(invocation.file, invocation.args, { stdio, env, cwd });
|
|
738
650
|
return new Promise((resolve10) => {
|
|
651
|
+
const done = (code) => {
|
|
652
|
+
if (fd !== null) fs3.closeSync(fd);
|
|
653
|
+
fd = null;
|
|
654
|
+
resolve10(code);
|
|
655
|
+
};
|
|
739
656
|
child.on("error", (err) => {
|
|
740
|
-
console.error(
|
|
741
|
-
|
|
657
|
+
console.error(`${label}: could not start Playwright \u2014 ${err.message}`);
|
|
658
|
+
done(COULD_NOT_START);
|
|
742
659
|
});
|
|
743
|
-
child.on("exit", (code) =>
|
|
660
|
+
child.on("exit", (code) => done(code ?? COULD_NOT_START));
|
|
744
661
|
});
|
|
745
662
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
let args;
|
|
752
|
-
try {
|
|
753
|
-
args = parseBugArgs(argv, env);
|
|
754
|
-
} catch (e) {
|
|
755
|
-
console.error(`piwi bug: ${e.message}
|
|
663
|
+
function runPackageManager(command, args, cwd) {
|
|
664
|
+
const result = process.platform === "win32" ? (0, import_node_child_process.spawnSync)([command, ...args].join(" "), { cwd, stdio: "inherit", shell: true }) : (0, import_node_child_process.spawnSync)(command, args, { cwd, stdio: "inherit" });
|
|
665
|
+
return result.status;
|
|
666
|
+
}
|
|
756
667
|
|
|
757
|
-
|
|
758
|
-
|
|
668
|
+
// src/cli/ai.ts
|
|
669
|
+
var EXIT_OK = 0;
|
|
670
|
+
var EXIT_ISSUES = 1;
|
|
671
|
+
var EXIT_ERROR = 2;
|
|
672
|
+
var USAGE = `
|
|
673
|
+
Usage: piwi ai <command> [options]
|
|
674
|
+
|
|
675
|
+
Commands:
|
|
676
|
+
check Scan committed AI-step entries for orphans, non-canonical files and
|
|
677
|
+
duplicate templates. Read-only; exits 1 when issues are found.
|
|
678
|
+
resolve Author missing entries by running the suite in resolve mode against
|
|
679
|
+
the configured authoring server (forces --workers=1).
|
|
680
|
+
prune Delete orphaned/dormant entries.
|
|
681
|
+
|
|
682
|
+
Options (check):
|
|
683
|
+
--dir <name> Entry directory name per spec (env PIWI_AI_DIR, default ${DEFAULT_AI_DIR})
|
|
684
|
+
--cwd <path> Root to scan (default: current directory)
|
|
685
|
+
--json Emit findings as JSON
|
|
686
|
+
|
|
687
|
+
Options (resolve):
|
|
688
|
+
--grep <re> Only author entries for matching tests
|
|
689
|
+
--project <name> Author under one Playwright project (a resolve profile)
|
|
690
|
+
--env K=V Extra env for the run (repeatable \u2014 flags/viewport profiles)
|
|
691
|
+
--update-ai Re-author entries that already exist
|
|
692
|
+
(needs PIWI_DASHBOARD_URL / PIWI_API_KEY for the server)
|
|
693
|
+
|
|
694
|
+
Exit codes:
|
|
695
|
+
0 clean (or --help)
|
|
696
|
+
1 hygiene issues found
|
|
697
|
+
2 bad arguments / command unavailable
|
|
698
|
+
`.trim();
|
|
699
|
+
function readOption(argv, name) {
|
|
700
|
+
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
701
|
+
if (withEquals) return withEquals.slice(name.length + 1);
|
|
702
|
+
const index = argv.indexOf(name);
|
|
703
|
+
if (index === -1) return void 0;
|
|
704
|
+
const value = argv[index + 1];
|
|
705
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
706
|
+
}
|
|
707
|
+
function readAll(argv, name) {
|
|
708
|
+
const values = [];
|
|
709
|
+
for (let i = 0; i < argv.length; i++) {
|
|
710
|
+
if (argv[i].startsWith(`${name}=`)) values.push(argv[i].slice(name.length + 1));
|
|
711
|
+
else if (argv[i] === name && argv[i + 1] && !argv[i + 1].startsWith("--")) values.push(argv[++i]);
|
|
759
712
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
713
|
+
return values;
|
|
714
|
+
}
|
|
715
|
+
function formatFinding(finding) {
|
|
716
|
+
const mark = finding.severity === "error" ? "\u2716" : "\u26A0";
|
|
717
|
+
return `${mark} ${finding.file}
|
|
718
|
+
${finding.kind}: ${finding.message}`;
|
|
719
|
+
}
|
|
720
|
+
function runCheck(argv, env) {
|
|
721
|
+
const root = path6.resolve(readOption(argv, "--cwd") ?? process.cwd());
|
|
722
|
+
const dir = readOption(argv, "--dir") ?? env.PIWI_AI_DIR ?? DEFAULT_AI_DIR;
|
|
723
|
+
let findings;
|
|
763
724
|
try {
|
|
764
|
-
|
|
765
|
-
} catch (
|
|
766
|
-
console.error(`piwi
|
|
767
|
-
return
|
|
725
|
+
findings = checkAiTree(root, { dir });
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.error(`piwi ai check: ${error.message}`);
|
|
728
|
+
return EXIT_ERROR;
|
|
768
729
|
}
|
|
769
|
-
|
|
770
|
-
console.
|
|
730
|
+
if (argv.includes("--json")) {
|
|
731
|
+
console.log(JSON.stringify({ findings }, null, 2));
|
|
732
|
+
} else if (findings.length === 0) {
|
|
733
|
+
console.log("piwi ai check: no issues found.");
|
|
734
|
+
} else {
|
|
735
|
+
for (const finding of findings) console.log(formatFinding(finding));
|
|
736
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
737
|
+
const warnings = findings.length - errors;
|
|
738
|
+
console.log(`
|
|
739
|
+
${errors} error(s), ${warnings} warning(s).`);
|
|
771
740
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
741
|
+
return hasBlockingFindings(findings) ? EXIT_ISSUES : EXIT_OK;
|
|
742
|
+
}
|
|
743
|
+
function buildResolveInvocation(argv, env) {
|
|
744
|
+
const args = [];
|
|
745
|
+
const grep = readOption(argv, "--grep");
|
|
746
|
+
if (grep) args.push("--grep", grep);
|
|
747
|
+
const project = readOption(argv, "--project");
|
|
748
|
+
if (project) args.push("--project", project);
|
|
749
|
+
args.push("--workers=1");
|
|
750
|
+
const childEnv = { ...env, PIWI_AI: "resolve" };
|
|
751
|
+
if (argv.includes("--update-ai")) childEnv.PIWI_AI_UPDATE = "true";
|
|
752
|
+
for (const pair of readAll(argv, "--env")) {
|
|
753
|
+
const eq = pair.indexOf("=");
|
|
754
|
+
if (eq > 0) childEnv[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
775
755
|
}
|
|
776
|
-
|
|
777
|
-
|
|
756
|
+
return { args, env: childEnv };
|
|
757
|
+
}
|
|
758
|
+
function runResolve(argv, env) {
|
|
759
|
+
if (!env.PIWI_DASHBOARD_URL) {
|
|
760
|
+
console.error("piwi ai resolve: set PIWI_DASHBOARD_URL (the authoring server) before resolving.");
|
|
761
|
+
return Promise.resolve(EXIT_ERROR);
|
|
778
762
|
}
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
console.
|
|
786
|
-
return
|
|
763
|
+
const invocation = buildResolveInvocation(argv, env);
|
|
764
|
+
return spawnPlaywright(invocation.args, invocation.env, { command: "piwi ai resolve" });
|
|
765
|
+
}
|
|
766
|
+
async function runAi(argv, env = process.env) {
|
|
767
|
+
const [sub, ...rest] = argv;
|
|
768
|
+
if (sub === void 0 || sub === "-h" || sub === "--help") {
|
|
769
|
+
console.log(USAGE);
|
|
770
|
+
return EXIT_OK;
|
|
787
771
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
772
|
+
switch (sub) {
|
|
773
|
+
case "check":
|
|
774
|
+
return runCheck(rest, env);
|
|
775
|
+
case "resolve":
|
|
776
|
+
return runResolve(rest, env);
|
|
777
|
+
case "prune":
|
|
778
|
+
console.error(
|
|
779
|
+
"piwi ai prune: not available in this build yet. `piwi ai check` lists orphaned entries: delete their files by hand, and re-author a non-canonical one with `piwi ai resolve --update-ai`."
|
|
780
|
+
);
|
|
781
|
+
return EXIT_ERROR;
|
|
782
|
+
default:
|
|
783
|
+
console.error(`piwi ai: unknown command "${sub}"
|
|
784
|
+
`);
|
|
785
|
+
console.error(USAGE);
|
|
786
|
+
return EXIT_ERROR;
|
|
798
787
|
}
|
|
799
|
-
console.error(
|
|
800
|
-
args.mode === "commit" ? "piwi bug: the spec did not fail as test.fail() expects \u2014 the bug may be fixed here, or a step no longer matches the page (see the output above)." : "piwi bug: the spec failed \u2014 the bug reproduces here, or a step no longer matches the page (see the output above)."
|
|
801
|
-
);
|
|
802
|
-
return EXIT_RUN_FAILED;
|
|
803
788
|
}
|
|
804
789
|
|
|
805
|
-
// src/cli/
|
|
790
|
+
// src/cli/bug.ts
|
|
806
791
|
var fs5 = __toESM(require("fs"));
|
|
807
792
|
var path8 = __toESM(require("path"));
|
|
793
|
+
var import_node_child_process2 = require("child_process");
|
|
808
794
|
|
|
809
|
-
//
|
|
810
|
-
var
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
"
|
|
815
|
-
"toBeHidden",
|
|
816
|
-
"toBeEnabled",
|
|
817
|
-
"toBeDisabled",
|
|
818
|
-
"toHaveURL"
|
|
819
|
-
];
|
|
820
|
-
var VALUE_MATCHERS = /* @__PURE__ */ new Set([
|
|
821
|
-
"toHaveText",
|
|
822
|
-
"toHaveValue",
|
|
823
|
-
"toHaveAccessibleName",
|
|
824
|
-
"toHaveURL"
|
|
825
|
-
]);
|
|
826
|
-
|
|
827
|
-
// ../core/src/function-match.ts
|
|
828
|
-
function paramArgKey(source) {
|
|
829
|
-
return source.path ? `${source.param}.${source.path}` : source.param;
|
|
830
|
-
}
|
|
831
|
-
function globToRegExp(glob) {
|
|
832
|
-
const escaped = glob.split("**").map((part) => part.replace(/[.?+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*");
|
|
833
|
-
return new RegExp(`^${escaped}$`);
|
|
795
|
+
// src/internal/config/desktop.ts
|
|
796
|
+
var fs4 = __toESM(require("fs"));
|
|
797
|
+
var os = __toESM(require("os"));
|
|
798
|
+
var path7 = __toESM(require("path"));
|
|
799
|
+
function defaultDesktopConfigPath() {
|
|
800
|
+
return path7.join(os.homedir(), ".piwi", "desktop.json");
|
|
834
801
|
}
|
|
835
|
-
function
|
|
836
|
-
if (!pattern) return true;
|
|
802
|
+
function readDesktopConfig(filePath2) {
|
|
837
803
|
try {
|
|
838
|
-
|
|
804
|
+
const parsed = JSON.parse(fs4.readFileSync(filePath2, "utf8"));
|
|
805
|
+
const url = parsed?.url;
|
|
806
|
+
const token = parsed?.token;
|
|
807
|
+
if (typeof url !== "string" || !url) return null;
|
|
808
|
+
if (typeof token !== "string" || !token) return null;
|
|
809
|
+
return { url, token };
|
|
839
810
|
} catch {
|
|
840
|
-
return
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
function scoreTargetMatch(pattern, candidate) {
|
|
844
|
-
if (!candidate) return pattern.role || pattern.testId || pattern.name ? 0 : 0.3;
|
|
845
|
-
if (pattern.testId) {
|
|
846
|
-
return candidate.testId && candidate.testId.toLowerCase() === pattern.testId.toLowerCase() ? 1 : 0;
|
|
847
|
-
}
|
|
848
|
-
const roleMatches = !pattern.role || candidate.role != null && candidate.role.toLowerCase() === pattern.role.toLowerCase();
|
|
849
|
-
if (!roleMatches) return 0;
|
|
850
|
-
if (pattern.name) {
|
|
851
|
-
const needle = pattern.name.toLowerCase();
|
|
852
|
-
const haystack = `${candidate.accessibleName ?? ""} ${candidate.text ?? ""}`.toLowerCase().trim();
|
|
853
|
-
const nameMatches2 = haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
854
|
-
if (!nameMatches2) return pattern.role ? 0.35 : 0;
|
|
855
|
-
return pattern.role ? 0.95 : 0.6;
|
|
856
|
-
}
|
|
857
|
-
return pattern.role ? 0.7 : 0.3;
|
|
858
|
-
}
|
|
859
|
-
function actionCompatible(patternAction, recordedAction) {
|
|
860
|
-
if (patternAction === recordedAction) return true;
|
|
861
|
-
return patternAction === "click" && recordedAction === "press" || patternAction === "press" && recordedAction === "click";
|
|
862
|
-
}
|
|
863
|
-
function stepPairScore(pattern, recorded) {
|
|
864
|
-
if (!actionCompatible(pattern.action, recorded.action)) return 0;
|
|
865
|
-
const base = scoreTargetMatch(pattern.target, recorded.target);
|
|
866
|
-
return pattern.action === recorded.action ? base : base * 0.6;
|
|
867
|
-
}
|
|
868
|
-
function extractArgs(entry, window, pairs) {
|
|
869
|
-
const args = {};
|
|
870
|
-
const byPatternIndex = new Map(pairs.map(([windowIdx, patternIdx]) => [patternIdx, windowIdx]));
|
|
871
|
-
for (const source of entry.paramSources) {
|
|
872
|
-
const windowIdx = byPatternIndex.get(source.stepIndex);
|
|
873
|
-
if (windowIdx == null) continue;
|
|
874
|
-
const step = window[windowIdx];
|
|
875
|
-
const value = source.from === "value" ? step.value : source.from === "testId" ? step.target?.testId ?? null : step.target?.text ?? null;
|
|
876
|
-
if (value != null) args[paramArgKey(source)] = value;
|
|
877
|
-
}
|
|
878
|
-
return args;
|
|
879
|
-
}
|
|
880
|
-
function sameElement(pattern, recorded) {
|
|
881
|
-
if (pattern.testId || !pattern.name) return true;
|
|
882
|
-
const haystack = `${recorded?.accessibleName ?? ""} ${recorded?.text ?? ""}`.toLowerCase().trim();
|
|
883
|
-
const needle = pattern.name.toLowerCase();
|
|
884
|
-
return haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
885
|
-
}
|
|
886
|
-
function contiguousAlignment(steps, startIndex, pattern) {
|
|
887
|
-
if (startIndex + pattern.length > steps.length) return null;
|
|
888
|
-
const pairs = [];
|
|
889
|
-
let total = 0;
|
|
890
|
-
for (let j = 0; j < pattern.length; j++) {
|
|
891
|
-
const recorded = steps[startIndex + j];
|
|
892
|
-
const score = stepPairScore(pattern[j], recorded);
|
|
893
|
-
if (score <= 0 || !sameElement(pattern[j].target, recorded.target)) return null;
|
|
894
|
-
total += score;
|
|
895
|
-
pairs.push([j, j]);
|
|
896
|
-
}
|
|
897
|
-
return { total, pairs };
|
|
898
|
-
}
|
|
899
|
-
function matchFunctionAt(steps, startIndex, catalog) {
|
|
900
|
-
const anchor = steps[startIndex];
|
|
901
|
-
if (!anchor) return null;
|
|
902
|
-
let best = null;
|
|
903
|
-
for (const entry of catalog) {
|
|
904
|
-
if (entry.steps.length === 0) continue;
|
|
905
|
-
if (!urlMatches(entry.urlPattern, anchor.pageUrl)) continue;
|
|
906
|
-
const aligned = contiguousAlignment(steps, startIndex, entry.steps);
|
|
907
|
-
if (!aligned) continue;
|
|
908
|
-
const { total, pairs } = aligned;
|
|
909
|
-
const window = steps.slice(startIndex, startIndex + entry.steps.length);
|
|
910
|
-
const score = total / entry.steps.length;
|
|
911
|
-
const match = {
|
|
912
|
-
entry,
|
|
913
|
-
score,
|
|
914
|
-
matchedIndices: pairs.map(([windowIdx]) => windowIdx + startIndex),
|
|
915
|
-
complete: true,
|
|
916
|
-
args: extractArgs(entry, window, pairs)
|
|
917
|
-
};
|
|
918
|
-
if (!best || match.score > best.score || match.score === best.score && entry.steps.length > best.entry.steps.length)
|
|
919
|
-
best = match;
|
|
811
|
+
return null;
|
|
920
812
|
}
|
|
921
|
-
return best;
|
|
922
813
|
}
|
|
923
814
|
|
|
924
|
-
//
|
|
925
|
-
var
|
|
926
|
-
"
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
"
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
815
|
+
// src/internal/config/env.ts
|
|
816
|
+
var DEFAULTS = {
|
|
817
|
+
projectName: "default-project",
|
|
818
|
+
uploadTraces: true,
|
|
819
|
+
uploadReport: true,
|
|
820
|
+
liveFileUploads: true,
|
|
821
|
+
collectScmInfo: true,
|
|
822
|
+
collectCiInfo: true,
|
|
823
|
+
collectPerformanceMetrics: true,
|
|
824
|
+
captureLocators: true,
|
|
825
|
+
capturePageState: true,
|
|
826
|
+
// Off by default: the page inventory reads control and link names from every
|
|
827
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
828
|
+
capturePageInventory: false,
|
|
829
|
+
// Off by default: JavaScript coverage adds time to every test, so a scheduled job opts in.
|
|
830
|
+
captureCodeReach: false,
|
|
831
|
+
captureServerTraces: true,
|
|
832
|
+
captureResources: true,
|
|
833
|
+
leakCheck: "report",
|
|
834
|
+
sampleAriaOnPass: true,
|
|
835
|
+
uploadManifest: true,
|
|
836
|
+
defaultCapture: true,
|
|
837
|
+
checkBaseUrl: false,
|
|
838
|
+
streaming: true,
|
|
839
|
+
streamingBatchSize: 20,
|
|
840
|
+
streamingBatchDelay: 2e3,
|
|
841
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
842
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
843
|
+
failOnFlakyTests: false,
|
|
844
|
+
username: null,
|
|
845
|
+
password: null,
|
|
846
|
+
apiKey: null,
|
|
847
|
+
verbose: false
|
|
953
848
|
};
|
|
954
|
-
var
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
849
|
+
var PIWI_ENV_KEYS = {
|
|
850
|
+
serverUrl: "PIWI_DASHBOARD_URL",
|
|
851
|
+
projectName: "PIWI_PROJECT_NAME",
|
|
852
|
+
verbose: "PIWI_VERBOSE",
|
|
853
|
+
apiKey: "PIWI_API_KEY",
|
|
854
|
+
username: "PIWI_USERNAME",
|
|
855
|
+
password: "PIWI_PASSWORD",
|
|
856
|
+
environment: "PIWI_ENVIRONMENT",
|
|
857
|
+
label: "PIWI_LABEL",
|
|
858
|
+
runLabel: "PIWI_RUN_LABEL",
|
|
859
|
+
keep: "PIWI_KEEP",
|
|
860
|
+
streaming: "PIWI_STREAMING",
|
|
861
|
+
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
862
|
+
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
863
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
864
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
865
|
+
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
866
|
+
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
867
|
+
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
868
|
+
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
869
|
+
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
870
|
+
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
871
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
872
|
+
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
873
|
+
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
874
|
+
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
875
|
+
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
876
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
877
|
+
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
878
|
+
leakCheck: "PIWI_LEAK_CHECK",
|
|
879
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
880
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
881
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
882
|
+
checkBaseUrl: "PIWI_CHECK_BASE_URL",
|
|
883
|
+
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
884
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
885
|
+
outputFile: "PIWI_OUTPUT_FILE",
|
|
886
|
+
aiMode: "PIWI_AI",
|
|
887
|
+
aiDir: "PIWI_AI_DIR",
|
|
888
|
+
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
889
|
+
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
890
|
+
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
891
|
+
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
892
|
+
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
893
|
+
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
962
894
|
};
|
|
963
|
-
var
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
895
|
+
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
896
|
+
var PIWI_ORIGIN_ENV = {
|
|
897
|
+
kind: "PIWI_ORIGIN",
|
|
898
|
+
ref: "PIWI_ORIGIN_REF"
|
|
899
|
+
};
|
|
900
|
+
function originEnv(kind, ref) {
|
|
901
|
+
return ref === void 0 ? { [PIWI_ORIGIN_ENV.kind]: kind } : { [PIWI_ORIGIN_ENV.kind]: kind, [PIWI_ORIGIN_ENV.ref]: String(ref) };
|
|
902
|
+
}
|
|
903
|
+
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
904
|
+
var PIWI_PROBE_ENV = {
|
|
905
|
+
flag: "PIWI_PROBE",
|
|
906
|
+
plan: "PIWI_PROBE_PLAN",
|
|
907
|
+
results: "PIWI_PROBE_RESULTS",
|
|
908
|
+
secret: "PIWI_PROBE_SECRET"
|
|
909
|
+
};
|
|
910
|
+
var PIWI_FLAKE_ENV = {
|
|
911
|
+
plan: "PIWI_FLAKE_PLAN",
|
|
912
|
+
results: "PIWI_FLAKE_RESULTS"
|
|
913
|
+
};
|
|
914
|
+
var ENV_FALLBACK_SPECS = [
|
|
915
|
+
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
916
|
+
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
917
|
+
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
918
|
+
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
919
|
+
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
920
|
+
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
921
|
+
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
922
|
+
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
923
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
924
|
+
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
925
|
+
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
926
|
+
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
927
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
928
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
929
|
+
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
930
|
+
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
931
|
+
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
932
|
+
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
933
|
+
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
934
|
+
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
935
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
936
|
+
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
937
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
938
|
+
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
939
|
+
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
940
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
941
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
942
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
943
|
+
{ option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
|
|
944
|
+
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
945
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
946
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
947
|
+
];
|
|
948
|
+
|
|
949
|
+
// src/cli/bug.ts
|
|
950
|
+
var EXIT_OK2 = 0;
|
|
951
|
+
var EXIT_RUN_FAILED = 1;
|
|
952
|
+
var EXIT_ERROR2 = 2;
|
|
953
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
954
|
+
var USAGE2 = `
|
|
955
|
+
piwi bug \u2014 a bug report's failing test, from the dashboard
|
|
956
|
+
|
|
957
|
+
Usage:
|
|
958
|
+
npx @piwitests/reporter bug <id> [options]
|
|
959
|
+
|
|
960
|
+
Prints the spec to commit: the report's steps, test.fail() while the bug
|
|
961
|
+
exists, @bug and piwi:bug <id>, written with the project's settings.
|
|
962
|
+
|
|
963
|
+
Options:
|
|
964
|
+
--write Write it to the project's bugs folder (tests/bugs by default, from the
|
|
965
|
+
repository root), then run it once
|
|
966
|
+
--out <file> Write it to this file instead (then run it once)
|
|
967
|
+
--no-run With --write or --out: write it without running it
|
|
968
|
+
--force Replace an existing file
|
|
969
|
+
--run-mode The spec without test.fail(), as a reproduction runs it
|
|
970
|
+
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
|
|
971
|
+
--api-key <key> API key (env PIWI_API_KEY)
|
|
972
|
+
|
|
973
|
+
Exit codes: 0 printed or written (and, when run, the test behaved as its
|
|
974
|
+
test.fail() expects: the bug reproduces), 1 the run did not (the bug may be
|
|
975
|
+
fixed, or a step no longer matches the page), 2 the report could not be read
|
|
976
|
+
or the file could not be written.
|
|
977
|
+
`.trim();
|
|
978
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--out", "--server-url", "--api-key"]);
|
|
979
|
+
function readOption2(argv, name) {
|
|
980
|
+
const index = argv.indexOf(name);
|
|
981
|
+
if (index === -1) return void 0;
|
|
982
|
+
const value = argv[index + 1];
|
|
983
|
+
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
984
|
+
}
|
|
985
|
+
function parseBugArgs(argv, env) {
|
|
986
|
+
let idText;
|
|
987
|
+
for (let i = 0; i < argv.length; i++) {
|
|
988
|
+
const token = argv[i];
|
|
989
|
+
if (token.startsWith("--")) {
|
|
990
|
+
if (VALUE_FLAGS.has(token)) i++;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
idText = token;
|
|
994
|
+
break;
|
|
982
995
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
996
|
+
const id = Number((idText ?? "").replace(/^#/, ""));
|
|
997
|
+
if (!idText || !Number.isInteger(id) || id <= 0) throw new Error("No bug report id \u2014 usage: piwi bug <id>");
|
|
998
|
+
for (const flag of VALUE_FLAGS) {
|
|
999
|
+
if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
|
|
987
1000
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1001
|
+
const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
|
|
1002
|
+
if (!serverUrl) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
1003
|
+
const out = readOption2(argv, "--out") ?? null;
|
|
1004
|
+
const write = argv.includes("--write") || out !== null;
|
|
1005
|
+
return {
|
|
1006
|
+
id,
|
|
1007
|
+
write,
|
|
1008
|
+
out,
|
|
1009
|
+
run: write && !argv.includes("--no-run"),
|
|
1010
|
+
force: argv.includes("--force"),
|
|
1011
|
+
mode: argv.includes("--run-mode") ? "run" : "commit",
|
|
1012
|
+
serverUrl,
|
|
1013
|
+
apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
async function fetchBugSpec(args, specDir) {
|
|
1017
|
+
const dir = specDir !== void 0 ? `&specDir=${encodeURIComponent(specDir)}` : "";
|
|
1018
|
+
const url = `${args.serverUrl}/api/bug-reports/${args.id}/spec?mode=${args.mode}${dir}`;
|
|
1019
|
+
let res;
|
|
1020
|
+
try {
|
|
1021
|
+
res = await fetch(url, {
|
|
1022
|
+
headers: args.apiKey ? { "X-API-Key": args.apiKey } : {},
|
|
1023
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1024
|
+
});
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
throw new Error(`could not reach ${args.serverUrl} (${e.message})`);
|
|
993
1027
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (!opts.lenient || calls.length === 0) throw error;
|
|
1012
|
-
this.i = before;
|
|
1013
|
-
break;
|
|
1014
|
-
}
|
|
1015
|
-
opts.spans?.push({ start, end: this.i });
|
|
1016
|
-
const end = this.i;
|
|
1017
|
-
this.skipSpace();
|
|
1018
|
-
if (this.peek() !== ".") {
|
|
1019
|
-
this.i = end;
|
|
1020
|
-
break;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return { calls };
|
|
1028
|
+
if (res.status === 404) throw new Error(`no bug report #${args.id} on ${args.serverUrl}`);
|
|
1029
|
+
if (res.status === 401 || res.status === 403) throw new Error(`the dashboard refused the API key (${res.status})`);
|
|
1030
|
+
if (!res.ok) throw new Error(`the dashboard answered ${res.status}`);
|
|
1031
|
+
const body = await res.json();
|
|
1032
|
+
if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
|
|
1033
|
+
return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
|
|
1034
|
+
}
|
|
1035
|
+
function repositoryRoot(cwd) {
|
|
1036
|
+
try {
|
|
1037
|
+
const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
|
|
1038
|
+
cwd,
|
|
1039
|
+
encoding: "utf-8",
|
|
1040
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1041
|
+
}).trim();
|
|
1042
|
+
return out ? path8.resolve(out) : null;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return null;
|
|
1024
1045
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1046
|
+
}
|
|
1047
|
+
function realFolder(dir) {
|
|
1048
|
+
const rest = [];
|
|
1049
|
+
let current = dir;
|
|
1050
|
+
while (!fs5.existsSync(current) && path8.dirname(current) !== current) {
|
|
1051
|
+
rest.unshift(path8.basename(current));
|
|
1052
|
+
current = path8.dirname(current);
|
|
1028
1053
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (this.peek() === ")") {
|
|
1048
|
-
this.i++;
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
this.expect(")");
|
|
1054
|
-
break;
|
|
1055
|
-
}
|
|
1056
|
-
return { method, args };
|
|
1054
|
+
return path8.join(fs5.realpathSync(current), ...rest);
|
|
1055
|
+
}
|
|
1056
|
+
function folderInRepository(root, dir) {
|
|
1057
|
+
const relative9 = path8.relative(realFolder(root), realFolder(dir));
|
|
1058
|
+
if (relative9.startsWith("..") || path8.isAbsolute(relative9)) return void 0;
|
|
1059
|
+
return relative9.split(path8.sep).join("/");
|
|
1060
|
+
}
|
|
1061
|
+
function fileFilter(file) {
|
|
1062
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1063
|
+
return `${escape(path8.basename(path8.dirname(file)))}[\\\\/]${escape(path8.basename(file))}$`;
|
|
1064
|
+
}
|
|
1065
|
+
function runPlaywright(file, cwd, id, env) {
|
|
1066
|
+
return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
|
|
1067
|
+
}
|
|
1068
|
+
async function runBug(argv, env = process.env, cwd = process.cwd()) {
|
|
1069
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
1070
|
+
console.log(USAGE2);
|
|
1071
|
+
return EXIT_OK2;
|
|
1057
1072
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
if (!m) this.fail("expected a number");
|
|
1067
|
-
this.i += m[0].length;
|
|
1068
|
-
return { type: "number", value: Number(m[0]) };
|
|
1069
|
-
}
|
|
1070
|
-
const rest = this.src.slice(this.i);
|
|
1071
|
-
if (/^true\b/.test(rest)) {
|
|
1072
|
-
this.i += 4;
|
|
1073
|
-
return { type: "boolean", value: true };
|
|
1074
|
-
}
|
|
1075
|
-
if (/^false\b/.test(rest)) {
|
|
1076
|
-
this.i += 5;
|
|
1077
|
-
return { type: "boolean", value: false };
|
|
1078
|
-
}
|
|
1079
|
-
if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
|
|
1080
|
-
return this.fail("expected a value");
|
|
1073
|
+
let args;
|
|
1074
|
+
try {
|
|
1075
|
+
args = parseBugArgs(argv, env);
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
console.error(`piwi bug: ${e.message}
|
|
1078
|
+
|
|
1079
|
+
${USAGE2}`);
|
|
1080
|
+
return EXIT_ERROR2;
|
|
1081
1081
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
this.i++;
|
|
1091
|
-
return out;
|
|
1092
|
-
}
|
|
1093
|
-
if (c !== "\\") {
|
|
1094
|
-
out += c;
|
|
1095
|
-
this.i++;
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
const next = this.src[this.i + 1];
|
|
1099
|
-
if (next === void 0) break;
|
|
1100
|
-
const simple = SIMPLE_ESCAPES[next];
|
|
1101
|
-
if (simple !== void 0) {
|
|
1102
|
-
out += simple;
|
|
1103
|
-
this.i += 2;
|
|
1104
|
-
continue;
|
|
1105
|
-
}
|
|
1106
|
-
const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
|
|
1107
|
-
if (hex) {
|
|
1108
|
-
out += String.fromCodePoint(parseInt(hex[1], 16));
|
|
1109
|
-
this.i += 1 + hex[0].length;
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
if (next === "u" || next === "x") this.fail("malformed escape");
|
|
1113
|
-
out += next;
|
|
1114
|
-
this.i += 2;
|
|
1115
|
-
}
|
|
1116
|
-
return this.fail("unterminated string");
|
|
1082
|
+
const root = args.write ? repositoryRoot(cwd) : null;
|
|
1083
|
+
const outFile = args.out !== null ? path8.resolve(cwd, args.out) : null;
|
|
1084
|
+
let spec;
|
|
1085
|
+
try {
|
|
1086
|
+
spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path8.dirname(outFile)) : void 0);
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
console.error(`piwi bug: ${e.message}`);
|
|
1089
|
+
return EXIT_ERROR2;
|
|
1117
1090
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
let inClass = false;
|
|
1121
|
-
while (j < this.src.length) {
|
|
1122
|
-
const c = this.src[j];
|
|
1123
|
-
if (c === "\\") {
|
|
1124
|
-
j += 2;
|
|
1125
|
-
continue;
|
|
1126
|
-
}
|
|
1127
|
-
if (c === "[") inClass = true;
|
|
1128
|
-
else if (c === "]") inClass = false;
|
|
1129
|
-
else if (c === "/" && !inClass) break;
|
|
1130
|
-
j++;
|
|
1131
|
-
}
|
|
1132
|
-
if (j >= this.src.length) this.fail("unterminated regex");
|
|
1133
|
-
const source = this.src.slice(this.i + 1, j);
|
|
1134
|
-
const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
|
|
1135
|
-
this.i = j + 1 + flags.length;
|
|
1136
|
-
return { type: "regex", source, flags };
|
|
1091
|
+
for (const warning of spec.warnings) {
|
|
1092
|
+
console.error(`piwi bug: warning: step ${warning.step + 1}: ${warning.message}`);
|
|
1137
1093
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
for (; ; ) {
|
|
1142
|
-
this.skipSpace();
|
|
1143
|
-
if (this.peek() === "}") {
|
|
1144
|
-
this.i++;
|
|
1145
|
-
break;
|
|
1146
|
-
}
|
|
1147
|
-
const ch = this.peek();
|
|
1148
|
-
const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
|
|
1149
|
-
this.expect(":");
|
|
1150
|
-
entries.push([key, this.parseValue()]);
|
|
1151
|
-
this.skipSpace();
|
|
1152
|
-
if (this.peek() === ",") {
|
|
1153
|
-
this.i++;
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
this.expect("}");
|
|
1157
|
-
break;
|
|
1158
|
-
}
|
|
1159
|
-
return { type: "object", entries };
|
|
1094
|
+
if (!args.write) {
|
|
1095
|
+
process.stdout.write(spec.code);
|
|
1096
|
+
return EXIT_OK2;
|
|
1160
1097
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1098
|
+
if (!outFile && !root) {
|
|
1099
|
+
console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
|
|
1100
|
+
}
|
|
1101
|
+
const file = outFile ?? path8.resolve(root ?? cwd, spec.path);
|
|
1102
|
+
try {
|
|
1103
|
+
fs5.mkdirSync(path8.dirname(file), { recursive: true });
|
|
1104
|
+
fs5.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
const exists = e.code === "EEXIST";
|
|
1107
|
+
console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
|
|
1108
|
+
return EXIT_ERROR2;
|
|
1109
|
+
}
|
|
1110
|
+
const shown = path8.relative(cwd, file) || file;
|
|
1111
|
+
console.error(`piwi bug: wrote ${shown}`);
|
|
1112
|
+
if (!args.run) return EXIT_OK2;
|
|
1113
|
+
const code = await runPlaywright(file, cwd, args.id, env);
|
|
1114
|
+
if (code === EXIT_ERROR2 && !resolvePlaywrightCli(cwd)) return EXIT_ERROR2;
|
|
1115
|
+
if (code === 0) {
|
|
1116
|
+
console.error(
|
|
1117
|
+
args.mode === "commit" ? `piwi bug: the bug reproduces \u2014 the spec fails on it, as test.fail() expects. Commit ${shown}; remove test.fail() with the fix.` : "piwi bug: the spec passed \u2014 the bug does not show here."
|
|
1118
|
+
);
|
|
1119
|
+
return EXIT_OK2;
|
|
1120
|
+
}
|
|
1121
|
+
console.error(
|
|
1122
|
+
args.mode === "commit" ? "piwi bug: the spec did not fail as test.fail() expects \u2014 the bug may be fixed here, or a step no longer matches the page (see the output above)." : "piwi bug: the spec failed \u2014 the bug reproduces here, or a step no longer matches the page (see the output above)."
|
|
1123
|
+
);
|
|
1124
|
+
return EXIT_RUN_FAILED;
|
|
1166
1125
|
}
|
|
1167
|
-
|
|
1126
|
+
|
|
1127
|
+
// src/cli/codegen.ts
|
|
1128
|
+
var fs6 = __toESM(require("fs"));
|
|
1129
|
+
var path9 = __toESM(require("path"));
|
|
1130
|
+
|
|
1131
|
+
// ../core/src/recording.ts
|
|
1132
|
+
var ASSERTION_MATCHERS = [
|
|
1133
|
+
"toHaveText",
|
|
1134
|
+
"toHaveValue",
|
|
1135
|
+
"toHaveAccessibleName",
|
|
1136
|
+
"toBeVisible",
|
|
1137
|
+
"toBeHidden",
|
|
1138
|
+
"toBeEnabled",
|
|
1139
|
+
"toBeDisabled",
|
|
1140
|
+
"toHaveURL",
|
|
1141
|
+
"toContainText",
|
|
1142
|
+
"toHaveCount",
|
|
1143
|
+
"toBeChecked"
|
|
1144
|
+
];
|
|
1145
|
+
var VALUE_MATCHERS = /* @__PURE__ */ new Set([
|
|
1146
|
+
"toHaveText",
|
|
1147
|
+
"toHaveValue",
|
|
1148
|
+
"toHaveAccessibleName",
|
|
1149
|
+
"toHaveURL",
|
|
1150
|
+
"toContainText",
|
|
1151
|
+
"toHaveCount"
|
|
1152
|
+
]);
|
|
1153
|
+
|
|
1154
|
+
// ../core/src/function-match.ts
|
|
1155
|
+
function paramArgKey(source) {
|
|
1156
|
+
return source.path ? `${source.param}.${source.path}` : source.param;
|
|
1157
|
+
}
|
|
1158
|
+
function globToRegExp(glob) {
|
|
1159
|
+
const escaped = glob.split("**").map((part) => part.replace(/[.?+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*");
|
|
1160
|
+
return new RegExp(`^${escaped}$`);
|
|
1161
|
+
}
|
|
1162
|
+
function urlMatches(pattern, url) {
|
|
1163
|
+
if (!pattern) return true;
|
|
1168
1164
|
try {
|
|
1169
|
-
return
|
|
1165
|
+
return globToRegExp(pattern).test(url);
|
|
1170
1166
|
} catch {
|
|
1171
|
-
return
|
|
1167
|
+
return true;
|
|
1172
1168
|
}
|
|
1173
1169
|
}
|
|
1174
|
-
function
|
|
1175
|
-
|
|
1176
|
-
|
|
1170
|
+
function scoreTargetMatch(pattern, candidate) {
|
|
1171
|
+
if (!candidate) return pattern.role || pattern.testId || pattern.name ? 0 : 0.3;
|
|
1172
|
+
if (pattern.testId) {
|
|
1173
|
+
return candidate.testId && candidate.testId.toLowerCase() === pattern.testId.toLowerCase() ? 1 : 0;
|
|
1174
|
+
}
|
|
1175
|
+
const roleMatches = !pattern.role || candidate.role != null && candidate.role.toLowerCase() === pattern.role.toLowerCase();
|
|
1176
|
+
if (!roleMatches) return 0;
|
|
1177
|
+
if (pattern.name) {
|
|
1178
|
+
const needle = pattern.name.toLowerCase();
|
|
1179
|
+
const haystack = `${candidate.accessibleName ?? ""} ${candidate.text ?? ""}`.toLowerCase().trim();
|
|
1180
|
+
const nameMatches2 = haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
1181
|
+
if (!nameMatches2) return pattern.role ? 0.35 : 0;
|
|
1182
|
+
return pattern.role ? 0.95 : 0.6;
|
|
1183
|
+
}
|
|
1184
|
+
return pattern.role ? 0.7 : 0.3;
|
|
1177
1185
|
}
|
|
1178
|
-
function
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1186
|
+
function actionCompatible(patternAction, recordedAction) {
|
|
1187
|
+
if (patternAction === recordedAction) return true;
|
|
1188
|
+
return patternAction === "click" && recordedAction === "press" || patternAction === "press" && recordedAction === "click";
|
|
1189
|
+
}
|
|
1190
|
+
function stepPairScore(pattern, recorded) {
|
|
1191
|
+
if (!actionCompatible(pattern.action, recorded.action)) return 0;
|
|
1192
|
+
const base = scoreTargetMatch(pattern.target, recorded.target);
|
|
1193
|
+
return pattern.action === recorded.action ? base : base * 0.6;
|
|
1194
|
+
}
|
|
1195
|
+
function extractArgs(entry, window, pairs) {
|
|
1196
|
+
const args = {};
|
|
1197
|
+
const byPatternIndex = new Map(pairs.map(([windowIdx, patternIdx]) => [patternIdx, windowIdx]));
|
|
1198
|
+
for (const source of entry.paramSources) {
|
|
1199
|
+
const windowIdx = byPatternIndex.get(source.stepIndex);
|
|
1200
|
+
if (windowIdx == null) continue;
|
|
1201
|
+
const step = window[windowIdx];
|
|
1202
|
+
const value = source.from === "value" ? step.value : source.from === "testId" ? step.target?.testId ?? null : step.target?.text ?? null;
|
|
1203
|
+
if (value != null) args[paramArgKey(source)] = value;
|
|
1191
1204
|
}
|
|
1205
|
+
return args;
|
|
1192
1206
|
}
|
|
1193
|
-
function
|
|
1194
|
-
|
|
1207
|
+
function sameElement(pattern, recorded) {
|
|
1208
|
+
if (pattern.testId || !pattern.name) return true;
|
|
1209
|
+
const haystack = `${recorded?.accessibleName ?? ""} ${recorded?.text ?? ""}`.toLowerCase().trim();
|
|
1210
|
+
const needle = pattern.name.toLowerCase();
|
|
1211
|
+
return haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
1195
1212
|
}
|
|
1196
|
-
function
|
|
1197
|
-
|
|
1213
|
+
function contiguousAlignment(steps, startIndex, pattern) {
|
|
1214
|
+
if (startIndex + pattern.length > steps.length) return null;
|
|
1215
|
+
const pairs = [];
|
|
1216
|
+
let total = 0;
|
|
1217
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
1218
|
+
const recorded = steps[startIndex + j];
|
|
1219
|
+
const score = stepPairScore(pattern[j], recorded);
|
|
1220
|
+
if (score <= 0 || !sameElement(pattern[j].target, recorded.target)) return null;
|
|
1221
|
+
total += score;
|
|
1222
|
+
pairs.push([j, j]);
|
|
1223
|
+
}
|
|
1224
|
+
return { total, pairs };
|
|
1198
1225
|
}
|
|
1199
|
-
function
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1226
|
+
function matchFunctionAt(steps, startIndex, catalog) {
|
|
1227
|
+
const anchor = steps[startIndex];
|
|
1228
|
+
if (!anchor) return null;
|
|
1229
|
+
let best = null;
|
|
1230
|
+
for (const entry of catalog) {
|
|
1231
|
+
if (entry.steps.length === 0) continue;
|
|
1232
|
+
if (!urlMatches(entry.urlPattern, anchor.pageUrl)) continue;
|
|
1233
|
+
const aligned = contiguousAlignment(steps, startIndex, entry.steps);
|
|
1234
|
+
if (!aligned) continue;
|
|
1235
|
+
const { total, pairs } = aligned;
|
|
1236
|
+
const window = steps.slice(startIndex, startIndex + entry.steps.length);
|
|
1237
|
+
const score = total / entry.steps.length;
|
|
1238
|
+
const match = {
|
|
1239
|
+
entry,
|
|
1240
|
+
score,
|
|
1241
|
+
matchedIndices: pairs.map(([windowIdx]) => windowIdx + startIndex),
|
|
1242
|
+
complete: true,
|
|
1243
|
+
args: extractArgs(entry, window, pairs)
|
|
1244
|
+
};
|
|
1245
|
+
if (!best || match.score > best.score || match.score === best.score && entry.steps.length > best.entry.steps.length)
|
|
1246
|
+
best = match;
|
|
1247
|
+
}
|
|
1248
|
+
return best;
|
|
1202
1249
|
}
|
|
1203
1250
|
|
|
1204
1251
|
// ../core/src/locator-stability.ts
|
|
@@ -1332,16 +1379,16 @@ function checkFilterOptions(arg, out) {
|
|
|
1332
1379
|
function splitTopLevel(text, separator) {
|
|
1333
1380
|
const parts = [];
|
|
1334
1381
|
let depth = 0;
|
|
1335
|
-
let
|
|
1382
|
+
let quote3 = null;
|
|
1336
1383
|
let start = 0;
|
|
1337
1384
|
for (let i = 0; i < text.length; i++) {
|
|
1338
1385
|
const c = text[i];
|
|
1339
|
-
if (
|
|
1386
|
+
if (quote3) {
|
|
1340
1387
|
if (c === "\\") i++;
|
|
1341
|
-
else if (c ===
|
|
1388
|
+
else if (c === quote3) quote3 = null;
|
|
1342
1389
|
continue;
|
|
1343
1390
|
}
|
|
1344
|
-
if (c === '"' || c === "'")
|
|
1391
|
+
if (c === '"' || c === "'") quote3 = c;
|
|
1345
1392
|
else if (c === "[" || c === "(") depth++;
|
|
1346
1393
|
else if (c === "]" || c === ")") depth = Math.max(0, depth - 1);
|
|
1347
1394
|
else if (depth === 0 && text.startsWith(separator, i)) {
|
|
@@ -1421,7 +1468,7 @@ function tokenizeComplex(selector) {
|
|
|
1421
1468
|
const combinators = [];
|
|
1422
1469
|
let current = "";
|
|
1423
1470
|
let depth = 0;
|
|
1424
|
-
let
|
|
1471
|
+
let quote3 = null;
|
|
1425
1472
|
let pending = null;
|
|
1426
1473
|
const flush = () => {
|
|
1427
1474
|
if (!current) return;
|
|
@@ -1433,14 +1480,14 @@ function tokenizeComplex(selector) {
|
|
|
1433
1480
|
};
|
|
1434
1481
|
for (let i = 0; i < selector.length; i++) {
|
|
1435
1482
|
const c = selector[i];
|
|
1436
|
-
if (
|
|
1483
|
+
if (quote3) {
|
|
1437
1484
|
current += c;
|
|
1438
1485
|
if (c === "\\" && i + 1 < selector.length) current += selector[++i];
|
|
1439
|
-
else if (c ===
|
|
1486
|
+
else if (c === quote3) quote3 = null;
|
|
1440
1487
|
continue;
|
|
1441
1488
|
}
|
|
1442
1489
|
if (c === '"' || c === "'") {
|
|
1443
|
-
|
|
1490
|
+
quote3 = c;
|
|
1444
1491
|
current += c;
|
|
1445
1492
|
} else if (c === "[" || c === "(") {
|
|
1446
1493
|
depth++;
|
|
@@ -1482,16 +1529,16 @@ function checkCompound(compound, out) {
|
|
|
1482
1529
|
};
|
|
1483
1530
|
const readBalanced = (open, close) => {
|
|
1484
1531
|
let depth = 0;
|
|
1485
|
-
let
|
|
1532
|
+
let quote3 = null;
|
|
1486
1533
|
const start = i + 1;
|
|
1487
1534
|
for (; i < compound.length; i++) {
|
|
1488
1535
|
const c = compound[i];
|
|
1489
|
-
if (
|
|
1536
|
+
if (quote3) {
|
|
1490
1537
|
if (c === "\\") i++;
|
|
1491
|
-
else if (c ===
|
|
1538
|
+
else if (c === quote3) quote3 = null;
|
|
1492
1539
|
continue;
|
|
1493
1540
|
}
|
|
1494
|
-
if (c === '"' || c === "'")
|
|
1541
|
+
if (c === '"' || c === "'") quote3 = c;
|
|
1495
1542
|
else if (c === open) depth++;
|
|
1496
1543
|
else if (c === close && --depth === 0) {
|
|
1497
1544
|
i++;
|
|
@@ -1631,17 +1678,7 @@ function pageKey(url) {
|
|
|
1631
1678
|
}
|
|
1632
1679
|
|
|
1633
1680
|
// ../core/src/codegen.ts
|
|
1634
|
-
var
|
|
1635
|
-
"\\": "\\\\",
|
|
1636
|
-
"'": "\\'",
|
|
1637
|
-
"\n": "\\n",
|
|
1638
|
-
"\r": "\\r",
|
|
1639
|
-
"\u2028": "\\u2028",
|
|
1640
|
-
"\u2029": "\\u2029"
|
|
1641
|
-
};
|
|
1642
|
-
function quote2(s) {
|
|
1643
|
-
return `'${s.replace(/[\\'\n\r\u2028\u2029]/g, (c) => QUOTE_ESCAPES[c])}'`;
|
|
1644
|
-
}
|
|
1681
|
+
var quote = quoteJsString;
|
|
1645
1682
|
var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
|
|
1646
1683
|
function argIsSafe(arg) {
|
|
1647
1684
|
switch (arg.type) {
|
|
@@ -1768,7 +1805,7 @@ function locatorForStep(step, index, ctx) {
|
|
|
1768
1805
|
const chosen = chooseLocator(step.target, ctx.options);
|
|
1769
1806
|
if (!chosen) {
|
|
1770
1807
|
ctx.warnings.push({ step: index, code: "no-locator", message: "No locator was captured for this element." });
|
|
1771
|
-
return `${ctx.page}.locator(${
|
|
1808
|
+
return `${ctx.page}.locator(${quote("/* no locator captured */")})`;
|
|
1772
1809
|
}
|
|
1773
1810
|
if (chosen.level === "brittle") {
|
|
1774
1811
|
ctx.warnings.push({
|
|
@@ -1801,12 +1838,12 @@ function renderAssertStep(step, index, ctx) {
|
|
|
1801
1838
|
});
|
|
1802
1839
|
return [` // Step ${index + 1}: ${matcher} without an expected value.`];
|
|
1803
1840
|
}
|
|
1804
|
-
arg =
|
|
1841
|
+
arg = matcher === "toHaveCount" ? String(Number.parseInt(assertion.expected, 10) || 0) : quote(matcher === "toHaveURL" ? urlForCode(assertion.expected, ctx) : assertion.expected);
|
|
1805
1842
|
}
|
|
1806
1843
|
const subject = matcher === "toHaveURL" ? ctx.page : locatorForStep(step, index, ctx);
|
|
1807
1844
|
let line = ` await expect(${subject})${not}.${matcher}(${arg});`;
|
|
1808
1845
|
if (assertion?.actual != null && assertion.actual !== assertion.expected) {
|
|
1809
|
-
line += ` // recorded: ${
|
|
1846
|
+
line += ` // recorded: ${quote(assertion.actual)}`;
|
|
1810
1847
|
}
|
|
1811
1848
|
lines.push(line);
|
|
1812
1849
|
return lines;
|
|
@@ -1814,12 +1851,27 @@ function renderAssertStep(step, index, ctx) {
|
|
|
1814
1851
|
function fileNames(value) {
|
|
1815
1852
|
return (value ?? "").split("\n").map((name) => name.trim().replace(/^.*[\\/]/, "")).filter(Boolean);
|
|
1816
1853
|
}
|
|
1854
|
+
function renderManualStep(step, index, ctx) {
|
|
1855
|
+
const description = commentText(step.manual?.description ?? "") || "a step to do by hand";
|
|
1856
|
+
ctx.warnings.push({
|
|
1857
|
+
step: index,
|
|
1858
|
+
code: "manual-step",
|
|
1859
|
+
detail: description,
|
|
1860
|
+
message: `Step ${index + 1} cannot be written as code: ${description}`
|
|
1861
|
+
});
|
|
1862
|
+
const lines = [` // Manual step: ${description}`];
|
|
1863
|
+
const call = step.manual?.call ? commentText(step.manual.call) : "";
|
|
1864
|
+
const location = step.manual?.location ? commentText(step.manual.location) : "";
|
|
1865
|
+
if (call || location) lines.push(` // ${[call, location ? `at ${location}` : ""].filter(Boolean).join(" ")}`);
|
|
1866
|
+
return lines;
|
|
1867
|
+
}
|
|
1817
1868
|
function renderRawStep(step, index, ctx) {
|
|
1818
1869
|
const { page } = ctx;
|
|
1819
|
-
if (step.action === "goto") return [` await ${page}.goto(${
|
|
1870
|
+
if (step.action === "goto") return [` await ${page}.goto(${quote(urlForCode(step.value ?? step.pageUrl, ctx))});`];
|
|
1820
1871
|
if (step.action === "assert" || step.action === "assertVisible") return renderAssertStep(step, index, ctx);
|
|
1872
|
+
if (step.action === "manual") return renderManualStep(step, index, ctx);
|
|
1821
1873
|
if (step.action === "press" && !step.target) {
|
|
1822
|
-
return [` await ${page}.keyboard.press(${
|
|
1874
|
+
return [` await ${page}.keyboard.press(${quote(step.value ?? "Enter")});`];
|
|
1823
1875
|
}
|
|
1824
1876
|
const loc = locatorForStep(step, index, ctx);
|
|
1825
1877
|
switch (step.action) {
|
|
@@ -1840,23 +1892,23 @@ function renderRawStep(step, index, ctx) {
|
|
|
1840
1892
|
detail: names.join(", "),
|
|
1841
1893
|
message: `A file was chosen here (${names.join(", ")}); the spec reads it from the directory Playwright runs in.`
|
|
1842
1894
|
});
|
|
1843
|
-
const files = names.length === 1 ?
|
|
1895
|
+
const files = names.length === 1 ? quote(names[0]) : `[${names.map(quote).join(", ")}]`;
|
|
1844
1896
|
return [` await ${loc}.setInputFiles(${files});`];
|
|
1845
1897
|
}
|
|
1846
1898
|
case "hover":
|
|
1847
1899
|
return [` await ${loc}.hover();`];
|
|
1848
1900
|
case "fill": {
|
|
1849
1901
|
if (readsEnv(step, ctx)) return [` await ${loc}.fill(process.env.${envVarFor(step, index, ctx)} ?? '');`];
|
|
1850
|
-
return [` await ${loc}.fill(${
|
|
1902
|
+
return [` await ${loc}.fill(${quote(step.value ?? "")});`];
|
|
1851
1903
|
}
|
|
1852
1904
|
case "check":
|
|
1853
1905
|
return [` await ${loc}.check();`];
|
|
1854
1906
|
case "uncheck":
|
|
1855
1907
|
return [` await ${loc}.uncheck();`];
|
|
1856
1908
|
case "selectOption":
|
|
1857
|
-
return [` await ${loc}.selectOption(${
|
|
1909
|
+
return [` await ${loc}.selectOption(${quote(step.value ?? "")});`];
|
|
1858
1910
|
case "press":
|
|
1859
|
-
return [` await ${loc}.press(${
|
|
1911
|
+
return [` await ${loc}.press(${quote(step.value ?? "Enter")});`];
|
|
1860
1912
|
}
|
|
1861
1913
|
}
|
|
1862
1914
|
var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
@@ -1939,7 +1991,7 @@ function objectArgExpr(param, match, envArgs) {
|
|
|
1939
1991
|
const env = envArgs.get(key);
|
|
1940
1992
|
const raw = match.args[key];
|
|
1941
1993
|
if (env == null && raw == null) return null;
|
|
1942
|
-
return `${IDENTIFIER_RE.test(field) ? field :
|
|
1994
|
+
return `${IDENTIFIER_RE.test(field) ? field : quote(field)}: ${env ?? quote(raw)}`;
|
|
1943
1995
|
}).filter((pair) => pair != null);
|
|
1944
1996
|
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
1945
1997
|
}
|
|
@@ -1967,7 +2019,7 @@ function argExpr(entry, match, envArgs) {
|
|
|
1967
2019
|
if (raw == null) return p.type === "number" ? "0" : p.type === "boolean" ? "false" : "''";
|
|
1968
2020
|
if (p.type === "number") return String(Number(raw) || 0);
|
|
1969
2021
|
if (p.type === "boolean") return String(raw === "true" || raw === "1");
|
|
1970
|
-
return
|
|
2022
|
+
return quote(raw);
|
|
1971
2023
|
}).join(", ");
|
|
1972
2024
|
}
|
|
1973
2025
|
function renderFunctionCall(match, page, envArgs) {
|
|
@@ -1987,11 +2039,11 @@ function renderImports(usedEntries, given) {
|
|
|
1987
2039
|
const moduleKey = `${entry.module}#${entry.importName}`;
|
|
1988
2040
|
if (!seenModules.has(moduleKey)) {
|
|
1989
2041
|
seenModules.add(moduleKey);
|
|
1990
|
-
lines.push(`import { ${entry.importName} } from ${
|
|
2042
|
+
lines.push(`import { ${entry.importName} } from ${quote(entry.module)};`);
|
|
1991
2043
|
}
|
|
1992
2044
|
} else if (!seenModules.has(entry.module + entry.name)) {
|
|
1993
2045
|
seenModules.add(entry.module + entry.name);
|
|
1994
|
-
lines.push(`import { ${entry.name} } from ${
|
|
2046
|
+
lines.push(`import { ${entry.name} } from ${quote(entry.module)};`);
|
|
1995
2047
|
}
|
|
1996
2048
|
}
|
|
1997
2049
|
return lines;
|
|
@@ -2011,12 +2063,12 @@ function renderDetails(options) {
|
|
|
2011
2063
|
const tags = (options.tags ?? []).map((t) => t.trim()).filter(Boolean);
|
|
2012
2064
|
const annotations = (options.annotations ?? []).filter((a) => a.type.trim());
|
|
2013
2065
|
const lines = [];
|
|
2014
|
-
if (tags.length > 0) lines.push(` tag: [${tags.map((t) =>
|
|
2066
|
+
if (tags.length > 0) lines.push(` tag: [${tags.map((t) => quote(t.startsWith("@") ? t : `@${t}`)).join(", ")}],`);
|
|
2015
2067
|
if (annotations.length > 0) {
|
|
2016
2068
|
lines.push(" annotation: [");
|
|
2017
2069
|
for (const a of annotations) {
|
|
2018
|
-
const description = a.description != null ? `, description: ${
|
|
2019
|
-
lines.push(` { type: ${
|
|
2070
|
+
const description = a.description != null ? `, description: ${quote(a.description)}` : "";
|
|
2071
|
+
lines.push(` { type: ${quote(a.type)}${description} },`);
|
|
2020
2072
|
}
|
|
2021
2073
|
lines.push(" ],");
|
|
2022
2074
|
}
|
|
@@ -2028,6 +2080,7 @@ function expectFailLine(options) {
|
|
|
2028
2080
|
return reason ? ` test.fail(); // ${reason}` : " test.fail();";
|
|
2029
2081
|
}
|
|
2030
2082
|
function samePage(a, b) {
|
|
2083
|
+
if (!a || !b) return true;
|
|
2031
2084
|
const ka = pageKey(a);
|
|
2032
2085
|
const kb = pageKey(b);
|
|
2033
2086
|
return ka === null || kb === null || ka === kb;
|
|
@@ -2049,7 +2102,7 @@ function wrapInPageSteps(lines, starts, origin) {
|
|
|
2049
2102
|
const out = lines.slice(0, starts[0].line);
|
|
2050
2103
|
starts.forEach((start, n) => {
|
|
2051
2104
|
const end = starts[n + 1]?.line ?? lines.length;
|
|
2052
|
-
out.push(` await test.step(${
|
|
2105
|
+
out.push(` await test.step(${quote(pageStepTitle(start.url, origin))}, async () => {`);
|
|
2053
2106
|
out.push(...lines.slice(start.line, end).map((line) => ` ${line}`));
|
|
2054
2107
|
out.push(" });");
|
|
2055
2108
|
});
|
|
@@ -2079,7 +2132,7 @@ function renderSpec(session, options = {}) {
|
|
|
2079
2132
|
const pageStarts = [];
|
|
2080
2133
|
const enterPageOf = (index) => {
|
|
2081
2134
|
const url = steps[index]?.pageUrl;
|
|
2082
|
-
if (options.testSteps !== "page" || url
|
|
2135
|
+
if (options.testSteps !== "page" || !url) return;
|
|
2083
2136
|
const last = pageStarts[pageStarts.length - 1];
|
|
2084
2137
|
if (!last || !samePage(last.url, url)) pageStarts.push({ line: bodyLines.length, url });
|
|
2085
2138
|
};
|
|
@@ -2091,7 +2144,7 @@ function renderSpec(session, options = {}) {
|
|
|
2091
2144
|
const pattern = pageUrlPattern(next.pageUrl);
|
|
2092
2145
|
enterPageOf(last + 1);
|
|
2093
2146
|
if (pattern) bodyLines.push(` await expect(${page}).toHaveURL(${pattern});`);
|
|
2094
|
-
const acts = next.action !== "assert" && next.action !== "assertVisible";
|
|
2147
|
+
const acts = next.action !== "assert" && next.action !== "assertVisible" && next.action !== "manual";
|
|
2095
2148
|
const chosen = acts && next.target ? chooseLocator(next.target, options) : null;
|
|
2096
2149
|
if (chosen) bodyLines.push(` await expect(${page}.${chosen.text}).toHaveCount(1);`);
|
|
2097
2150
|
};
|
|
@@ -2135,7 +2188,7 @@ function renderSpec(session, options = {}) {
|
|
|
2135
2188
|
const start = viewportAt.get(0);
|
|
2136
2189
|
const opening = [
|
|
2137
2190
|
...start ? [viewportLine(start, page)] : [],
|
|
2138
|
-
...!sawGoto && session.startUrl ? [` await ${page}.goto(${
|
|
2191
|
+
...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
|
|
2139
2192
|
];
|
|
2140
2193
|
bodyLines.unshift(...opening);
|
|
2141
2194
|
const bodyOffset = opening.length;
|
|
@@ -2172,7 +2225,7 @@ function renderSpec(session, options = {}) {
|
|
|
2172
2225
|
}
|
|
2173
2226
|
const details = renderDetails(options);
|
|
2174
2227
|
const fixture = [.../* @__PURE__ */ new Set([page.split(".")[0], ...fixtureReceivers])].join(", ");
|
|
2175
|
-
const testOpening = details.length > 0 ? [`test(${
|
|
2228
|
+
const testOpening = details.length > 0 ? [`test(${quote(title)}, {`, ...details, `}, async ({ ${fixture} }) => {`] : [`test(${quote(title)}, async ({ ${fixture} }) => {`];
|
|
2176
2229
|
if (options.format === "test") {
|
|
2177
2230
|
const needs = needsLines("");
|
|
2178
2231
|
return result([...needs, ...testOpening, ...testBody, `});`, ``], needs.length + testOpening.length);
|
|
@@ -2181,7 +2234,7 @@ function renderSpec(session, options = {}) {
|
|
|
2181
2234
|
// Package name interpolated rather than adjoining the preceding keyword
|
|
2182
2235
|
// directly, so this generated-code line does not itself look like a real
|
|
2183
2236
|
// import to the boundary test in tests/boundary.test.ts.
|
|
2184
|
-
`import { test, expect } from ${
|
|
2237
|
+
`import { test, expect } from ${quote(options.testImport?.trim() || "@playwright/test")};`,
|
|
2185
2238
|
...importLines,
|
|
2186
2239
|
``,
|
|
2187
2240
|
...testOpening,
|
|
@@ -2193,7 +2246,11 @@ function renderSpec(session, options = {}) {
|
|
|
2193
2246
|
}
|
|
2194
2247
|
|
|
2195
2248
|
// ../core/src/steps.ts
|
|
2196
|
-
var STEPS_VERSION =
|
|
2249
|
+
var STEPS_VERSION = 2;
|
|
2250
|
+
var VERSION_2_MATCHERS = /* @__PURE__ */ new Set(["toContainText", "toHaveCount", "toBeChecked"]);
|
|
2251
|
+
function stepsVersionFor(steps) {
|
|
2252
|
+
return steps.some((s) => s.action === "manual" || !!s.assertion && VERSION_2_MATCHERS.has(s.assertion.matcher)) ? 2 : 1;
|
|
2253
|
+
}
|
|
2197
2254
|
var STEPS_LIMITS = {
|
|
2198
2255
|
steps: 200,
|
|
2199
2256
|
alternatives: 10,
|
|
@@ -2220,7 +2277,8 @@ var ACTIONS = /* @__PURE__ */ new Set([
|
|
|
2220
2277
|
"setInputFiles",
|
|
2221
2278
|
"dragTo",
|
|
2222
2279
|
"assertVisible",
|
|
2223
|
-
"assert"
|
|
2280
|
+
"assert",
|
|
2281
|
+
"manual"
|
|
2224
2282
|
]);
|
|
2225
2283
|
function originOf2(url) {
|
|
2226
2284
|
try {
|
|
@@ -2245,7 +2303,7 @@ function toAbsolute(url, origin) {
|
|
|
2245
2303
|
function toStepsDocument(session, meta = {}) {
|
|
2246
2304
|
const origin = originOf2(session.startUrl) ?? originOf2(session.steps[0]?.pageUrl ?? "");
|
|
2247
2305
|
return {
|
|
2248
|
-
v:
|
|
2306
|
+
v: stepsVersionFor(session.steps),
|
|
2249
2307
|
title: meta.title ?? null,
|
|
2250
2308
|
origin,
|
|
2251
2309
|
recordedAt: session.startedAt,
|
|
@@ -2285,43 +2343,43 @@ var Checker = class {
|
|
|
2285
2343
|
constructor() {
|
|
2286
2344
|
this.errors = [];
|
|
2287
2345
|
}
|
|
2288
|
-
fail(
|
|
2289
|
-
if (this.errors.length < 20) this.errors.push(`${
|
|
2346
|
+
fail(path24, message) {
|
|
2347
|
+
if (this.errors.length < 20) this.errors.push(`${path24}: ${message}`);
|
|
2290
2348
|
}
|
|
2291
|
-
text(v,
|
|
2349
|
+
text(v, path24, max, nullable) {
|
|
2292
2350
|
if (v == null) {
|
|
2293
|
-
if (!nullable) this.fail(
|
|
2351
|
+
if (!nullable) this.fail(path24, "is required");
|
|
2294
2352
|
return null;
|
|
2295
2353
|
}
|
|
2296
2354
|
if (typeof v !== "string") {
|
|
2297
|
-
this.fail(
|
|
2355
|
+
this.fail(path24, "must be a string");
|
|
2298
2356
|
return null;
|
|
2299
2357
|
}
|
|
2300
|
-
if (v.length > max) this.fail(
|
|
2358
|
+
if (v.length > max) this.fail(path24, `is longer than ${max} characters`);
|
|
2301
2359
|
return v;
|
|
2302
2360
|
}
|
|
2303
|
-
number(v,
|
|
2361
|
+
number(v, path24) {
|
|
2304
2362
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2305
|
-
this.fail(
|
|
2363
|
+
this.fail(path24, "must be a number");
|
|
2306
2364
|
return 0;
|
|
2307
2365
|
}
|
|
2308
2366
|
return v;
|
|
2309
2367
|
}
|
|
2310
2368
|
};
|
|
2311
|
-
function checkTarget(c, v,
|
|
2369
|
+
function checkTarget(c, v, path24) {
|
|
2312
2370
|
if (v == null) return null;
|
|
2313
2371
|
if (!isObject(v)) {
|
|
2314
|
-
c.fail(
|
|
2372
|
+
c.fail(path24, "must be an object");
|
|
2315
2373
|
return null;
|
|
2316
2374
|
}
|
|
2317
2375
|
const alternativesIn = v.alternatives ?? [];
|
|
2318
2376
|
const alternatives = [];
|
|
2319
|
-
if (!Array.isArray(alternativesIn)) c.fail(`${
|
|
2377
|
+
if (!Array.isArray(alternativesIn)) c.fail(`${path24}.alternatives`, "must be a list");
|
|
2320
2378
|
else if (alternativesIn.length > STEPS_LIMITS.alternatives)
|
|
2321
|
-
c.fail(`${
|
|
2379
|
+
c.fail(`${path24}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
|
|
2322
2380
|
else {
|
|
2323
2381
|
alternativesIn.forEach((alt, i) => {
|
|
2324
|
-
const at = `${
|
|
2382
|
+
const at = `${path24}.alternatives[${i}]`;
|
|
2325
2383
|
if (!isObject(alt)) return c.fail(at, "must be an object");
|
|
2326
2384
|
const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
|
|
2327
2385
|
const safe = locator ? safeLocator(locator) : null;
|
|
@@ -2335,31 +2393,47 @@ function checkTarget(c, v, path23) {
|
|
|
2335
2393
|
});
|
|
2336
2394
|
}
|
|
2337
2395
|
return {
|
|
2338
|
-
tagName: c.text(v.tagName, `${
|
|
2339
|
-
role: c.text(v.role, `${
|
|
2340
|
-
accessibleName: c.text(v.accessibleName, `${
|
|
2341
|
-
testId: c.text(v.testId, `${
|
|
2342
|
-
text: c.text(v.text, `${
|
|
2396
|
+
tagName: c.text(v.tagName, `${path24}.tagName`, 64, false) ?? "",
|
|
2397
|
+
role: c.text(v.role, `${path24}.role`, 64, true),
|
|
2398
|
+
accessibleName: c.text(v.accessibleName, `${path24}.accessibleName`, STEPS_LIMITS.textLength, true),
|
|
2399
|
+
testId: c.text(v.testId, `${path24}.testId`, STEPS_LIMITS.textLength, true),
|
|
2400
|
+
text: c.text(v.text, `${path24}.text`, STEPS_LIMITS.textLength, true),
|
|
2343
2401
|
alternatives
|
|
2344
2402
|
};
|
|
2345
2403
|
}
|
|
2346
|
-
function checkAssertion(c, v,
|
|
2404
|
+
function checkAssertion(c, v, path24) {
|
|
2347
2405
|
if (!isObject(v)) {
|
|
2348
|
-
c.fail(
|
|
2406
|
+
c.fail(path24, "is required on an assert step");
|
|
2349
2407
|
return void 0;
|
|
2350
2408
|
}
|
|
2351
2409
|
const matcher = v.matcher;
|
|
2352
2410
|
if (!ASSERTION_MATCHERS.includes(matcher)) {
|
|
2353
|
-
c.fail(`${
|
|
2411
|
+
c.fail(`${path24}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
|
|
2354
2412
|
return void 0;
|
|
2355
2413
|
}
|
|
2356
|
-
const expected = c.text(v.expected, `${
|
|
2414
|
+
const expected = c.text(v.expected, `${path24}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
|
|
2415
|
+
if (matcher === "toHaveCount" && expected != null && !/^\d{1,6}$/.test(expected)) {
|
|
2416
|
+
c.fail(`${path24}.expected`, "must be a whole number of elements on a toHaveCount assertion");
|
|
2417
|
+
}
|
|
2357
2418
|
return {
|
|
2358
2419
|
matcher,
|
|
2359
2420
|
expected: VALUE_MATCHERS.has(matcher) ? expected : null,
|
|
2360
|
-
actual: c.text(v.actual, `${
|
|
2421
|
+
actual: c.text(v.actual, `${path24}.actual`, STEPS_LIMITS.valueLength, true),
|
|
2361
2422
|
negated: v.negated === true,
|
|
2362
|
-
note: c.text(v.note, `${
|
|
2423
|
+
note: c.text(v.note, `${path24}.note`, STEPS_LIMITS.valueLength, true)
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
function checkManual(c, v, path24) {
|
|
2427
|
+
if (!isObject(v)) {
|
|
2428
|
+
c.fail(path24, "is required on a manual step");
|
|
2429
|
+
return void 0;
|
|
2430
|
+
}
|
|
2431
|
+
const description = c.text(v.description, `${path24}.description`, STEPS_LIMITS.valueLength, false);
|
|
2432
|
+
if (description != null && !description.trim()) c.fail(`${path24}.description`, "must not be empty");
|
|
2433
|
+
return {
|
|
2434
|
+
description: description ?? "",
|
|
2435
|
+
location: c.text(v.location, `${path24}.location`, STEPS_LIMITS.textLength, true),
|
|
2436
|
+
call: c.text(v.call, `${path24}.call`, STEPS_LIMITS.textLength, true)
|
|
2363
2437
|
};
|
|
2364
2438
|
}
|
|
2365
2439
|
function checkViewports(c, v, stepCount) {
|
|
@@ -2400,40 +2474,43 @@ function checkViewports(c, v, stepCount) {
|
|
|
2400
2474
|
});
|
|
2401
2475
|
return out;
|
|
2402
2476
|
}
|
|
2403
|
-
function checkStep(c, v,
|
|
2477
|
+
function checkStep(c, v, path24) {
|
|
2404
2478
|
if (!isObject(v)) {
|
|
2405
|
-
c.fail(
|
|
2479
|
+
c.fail(path24, "must be an object");
|
|
2406
2480
|
return null;
|
|
2407
2481
|
}
|
|
2408
2482
|
const action = v.action;
|
|
2409
2483
|
if (!ACTIONS.has(action)) {
|
|
2410
|
-
c.fail(`${
|
|
2484
|
+
c.fail(`${path24}.action`, "is not a known action");
|
|
2411
2485
|
return null;
|
|
2412
2486
|
}
|
|
2413
2487
|
const step = {
|
|
2414
2488
|
action: action === "assertVisible" ? "assert" : action,
|
|
2415
|
-
target: checkTarget(c, v.target, `${
|
|
2416
|
-
value: c.text(v.value, `${
|
|
2489
|
+
target: checkTarget(c, v.target, `${path24}.target`),
|
|
2490
|
+
value: c.text(v.value, `${path24}.value`, STEPS_LIMITS.valueLength, true),
|
|
2417
2491
|
redacted: v.redacted === true,
|
|
2418
|
-
pageUrl: c.text(v.pageUrl, `${
|
|
2419
|
-
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${
|
|
2492
|
+
pageUrl: c.text(v.pageUrl, `${path24}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
|
|
2493
|
+
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path24}.timestamp`)
|
|
2420
2494
|
};
|
|
2421
2495
|
if (step.redacted) step.value = null;
|
|
2422
2496
|
if (action === "hover" || action === "dblclick") {
|
|
2423
2497
|
step.value = null;
|
|
2424
|
-
if (!step.target) c.fail(`${
|
|
2498
|
+
if (!step.target) c.fail(`${path24}.target`, `is required on a ${action} step`);
|
|
2425
2499
|
}
|
|
2426
|
-
if (action === "setInputFiles" && !step.target) c.fail(`${
|
|
2500
|
+
if (action === "setInputFiles" && !step.target) c.fail(`${path24}.target`, "is required on a setInputFiles step");
|
|
2427
2501
|
if (action === "dragTo") {
|
|
2428
2502
|
step.value = null;
|
|
2429
|
-
if (!step.target) c.fail(`${
|
|
2430
|
-
step.dropTarget = checkTarget(c, v.dropTarget, `${
|
|
2431
|
-
if (!step.dropTarget) c.fail(`${
|
|
2503
|
+
if (!step.target) c.fail(`${path24}.target`, "is required on a dragTo step");
|
|
2504
|
+
step.dropTarget = checkTarget(c, v.dropTarget, `${path24}.dropTarget`);
|
|
2505
|
+
if (!step.dropTarget) c.fail(`${path24}.dropTarget`, "is required on a dragTo step");
|
|
2432
2506
|
}
|
|
2433
2507
|
if (action === "assertVisible") {
|
|
2434
2508
|
step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
|
|
2435
2509
|
} else if (action === "assert") {
|
|
2436
|
-
step.assertion = checkAssertion(c, v.assertion, `${
|
|
2510
|
+
step.assertion = checkAssertion(c, v.assertion, `${path24}.assertion`);
|
|
2511
|
+
} else if (action === "manual") {
|
|
2512
|
+
step.value = null;
|
|
2513
|
+
step.manual = checkManual(c, v.manual, `${path24}.manual`);
|
|
2437
2514
|
}
|
|
2438
2515
|
return step;
|
|
2439
2516
|
}
|
|
@@ -2449,8 +2526,8 @@ function parseSteps(input) {
|
|
|
2449
2526
|
if (!isObject(value)) return { ok: false, errors: ["a steps document must be a JSON object"] };
|
|
2450
2527
|
const c = new Checker();
|
|
2451
2528
|
const legacy = value.v == null && typeof value.startUrl === "string";
|
|
2452
|
-
if (!legacy && value.v !== STEPS_VERSION) {
|
|
2453
|
-
return { ok: false, errors: [`v: this reads
|
|
2529
|
+
if (!legacy && value.v !== 1 && value.v !== STEPS_VERSION) {
|
|
2530
|
+
return { ok: false, errors: [`v: this reads versions 1 to ${STEPS_VERSION}, not ${JSON.stringify(value.v)}`] };
|
|
2454
2531
|
}
|
|
2455
2532
|
if (!Array.isArray(value.steps)) return { ok: false, errors: ["steps: must be a list"] };
|
|
2456
2533
|
if (value.steps.length > STEPS_LIMITS.steps) {
|
|
@@ -2473,7 +2550,7 @@ function parseSteps(input) {
|
|
|
2473
2550
|
if (originText && !origin) c.fail("origin", "must be an http(s) origin");
|
|
2474
2551
|
const viewports = checkViewports(c, value.viewports, steps.length);
|
|
2475
2552
|
doc = {
|
|
2476
|
-
v:
|
|
2553
|
+
v: stepsVersionFor(steps),
|
|
2477
2554
|
title: c.text(value.title, "title", STEPS_LIMITS.textLength, true),
|
|
2478
2555
|
origin,
|
|
2479
2556
|
recordedAt: value.recordedAt == null ? 0 : c.number(value.recordedAt, "recordedAt"),
|
|
@@ -2732,8 +2809,8 @@ ${USAGE3}`);
|
|
|
2732
2809
|
}
|
|
2733
2810
|
} else {
|
|
2734
2811
|
try {
|
|
2735
|
-
if (
|
|
2736
|
-
text =
|
|
2812
|
+
if (fs6.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
|
|
2813
|
+
text = fs6.readFileSync(args.file, "utf-8");
|
|
2737
2814
|
} catch (e) {
|
|
2738
2815
|
const reason = e.code === "ENOENT" ? "does not exist" : e.message;
|
|
2739
2816
|
console.error(`piwi codegen: ${args.file} ${reason}`);
|
|
@@ -2765,8 +2842,8 @@ ${USAGE3}`);
|
|
|
2765
2842
|
return EXIT_OK3;
|
|
2766
2843
|
}
|
|
2767
2844
|
try {
|
|
2768
|
-
|
|
2769
|
-
|
|
2845
|
+
fs6.mkdirSync(path9.dirname(args.out), { recursive: true });
|
|
2846
|
+
fs6.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
|
|
2770
2847
|
} catch (e) {
|
|
2771
2848
|
const exists = e.code === "EEXIST";
|
|
2772
2849
|
console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
|
|
@@ -2780,7 +2857,7 @@ ${USAGE3}`);
|
|
|
2780
2857
|
}
|
|
2781
2858
|
|
|
2782
2859
|
// src/cli/coverage.ts
|
|
2783
|
-
var
|
|
2860
|
+
var path14 = __toESM(require("path"));
|
|
2784
2861
|
|
|
2785
2862
|
// ../core/src/codeowners.ts
|
|
2786
2863
|
function patternParts(pattern) {
|
|
@@ -2818,10 +2895,10 @@ function pathPatternMatcher(pattern) {
|
|
|
2818
2895
|
const { segments, floating } = patternParts(pattern);
|
|
2819
2896
|
if (floating) {
|
|
2820
2897
|
const only = segments[0];
|
|
2821
|
-
return (
|
|
2898
|
+
return (path24) => path24.split("/").some((part) => segmentMatches(only, part));
|
|
2822
2899
|
}
|
|
2823
|
-
return (
|
|
2824
|
-
const parts =
|
|
2900
|
+
return (path24) => {
|
|
2901
|
+
const parts = path24.split("/");
|
|
2825
2902
|
let at = new Uint8Array(parts.length);
|
|
2826
2903
|
at[0] = 1;
|
|
2827
2904
|
for (let i = 0; i < segments.length; i++) {
|
|
@@ -2887,8 +2964,8 @@ function parseCoverageScope(raw) {
|
|
|
2887
2964
|
function compileCoverageScope(scope) {
|
|
2888
2965
|
const include = scope.include.map(pathPatternMatcher);
|
|
2889
2966
|
const exclude = scope.exclude.map(pathPatternMatcher);
|
|
2890
|
-
return (
|
|
2891
|
-
const file =
|
|
2967
|
+
return (path24) => {
|
|
2968
|
+
const file = path24.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
2892
2969
|
if (include.length > 0 && !include.some((matches) => matches(file))) return false;
|
|
2893
2970
|
return !exclude.some((matches) => matches(file));
|
|
2894
2971
|
};
|
|
@@ -2903,6 +2980,9 @@ function errorMessage(error) {
|
|
|
2903
2980
|
return String(error);
|
|
2904
2981
|
}
|
|
2905
2982
|
|
|
2983
|
+
// src/internal/support/test-paths.ts
|
|
2984
|
+
var path10 = __toESM(require("path"));
|
|
2985
|
+
|
|
2906
2986
|
// src/internal/collect/metadata-collector.ts
|
|
2907
2987
|
var DETACHED_HEAD = "HEAD";
|
|
2908
2988
|
function resolveScmBranch(env, gitBranch) {
|
|
@@ -2934,14 +3014,14 @@ function normalizeRef(ref) {
|
|
|
2934
3014
|
|
|
2935
3015
|
// src/internal/coverage-upload/git-inventory.ts
|
|
2936
3016
|
var import_node_child_process5 = require("child_process");
|
|
2937
|
-
var
|
|
2938
|
-
var
|
|
3017
|
+
var fs8 = __toESM(require("fs"));
|
|
3018
|
+
var path12 = __toESM(require("path"));
|
|
2939
3019
|
|
|
2940
3020
|
// src/internal/collect/blob-ids.ts
|
|
2941
3021
|
var import_node_child_process4 = require("child_process");
|
|
2942
3022
|
var crypto2 = __toESM(require("crypto"));
|
|
2943
|
-
var
|
|
2944
|
-
var
|
|
3023
|
+
var fs7 = __toESM(require("fs"));
|
|
3024
|
+
var path11 = __toESM(require("path"));
|
|
2945
3025
|
function gitHashObjects(root, paths) {
|
|
2946
3026
|
return new Promise((resolve10) => {
|
|
2947
3027
|
const child = (0, import_node_child_process4.execFile)(
|
|
@@ -3008,7 +3088,7 @@ async function readInventory(root, warn = () => {
|
|
|
3008
3088
|
const changed = [];
|
|
3009
3089
|
for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
|
|
3010
3090
|
if (!file || !inventory.has(file)) continue;
|
|
3011
|
-
if (
|
|
3091
|
+
if (fs8.existsSync(path12.join(root, file))) changed.push(file);
|
|
3012
3092
|
else inventory.delete(file);
|
|
3013
3093
|
}
|
|
3014
3094
|
const ids = await hashPaths(root, changed);
|
|
@@ -3027,8 +3107,8 @@ async function readInventory(root, warn = () => {
|
|
|
3027
3107
|
}
|
|
3028
3108
|
|
|
3029
3109
|
// src/internal/coverage-upload/report-files.ts
|
|
3030
|
-
var
|
|
3031
|
-
var
|
|
3110
|
+
var fs9 = __toESM(require("fs"));
|
|
3111
|
+
var path13 = __toESM(require("path"));
|
|
3032
3112
|
|
|
3033
3113
|
// ../core/src/coverage.ts
|
|
3034
3114
|
var MAX_LINE = 1e6;
|
|
@@ -3107,7 +3187,7 @@ var FileLines = class {
|
|
|
3107
3187
|
this.functions.set(key, { start, end: last, ran });
|
|
3108
3188
|
}
|
|
3109
3189
|
}
|
|
3110
|
-
toFile(
|
|
3190
|
+
toFile(path24) {
|
|
3111
3191
|
const executable = [];
|
|
3112
3192
|
const lines = [];
|
|
3113
3193
|
const push = (ranges, line) => {
|
|
@@ -3127,7 +3207,7 @@ var FileLines = class {
|
|
|
3127
3207
|
if (didRun) ran.push([start, end]);
|
|
3128
3208
|
}
|
|
3129
3209
|
return {
|
|
3130
|
-
path:
|
|
3210
|
+
path: path24,
|
|
3131
3211
|
executable,
|
|
3132
3212
|
lines,
|
|
3133
3213
|
functions: normalizeFunctionRanges(ran),
|
|
@@ -3139,16 +3219,16 @@ var ReportFiles = class {
|
|
|
3139
3219
|
constructor() {
|
|
3140
3220
|
this.byPath = /* @__PURE__ */ new Map();
|
|
3141
3221
|
}
|
|
3142
|
-
of(
|
|
3143
|
-
let file = this.byPath.get(
|
|
3222
|
+
of(path24) {
|
|
3223
|
+
let file = this.byPath.get(path24);
|
|
3144
3224
|
if (!file) {
|
|
3145
3225
|
file = new FileLines();
|
|
3146
|
-
this.byPath.set(
|
|
3226
|
+
this.byPath.set(path24, file);
|
|
3147
3227
|
}
|
|
3148
3228
|
return file;
|
|
3149
3229
|
}
|
|
3150
3230
|
toFiles() {
|
|
3151
|
-
return [...this.byPath].map(([
|
|
3231
|
+
return [...this.byPath].map(([path24, file]) => file.toFile(path24));
|
|
3152
3232
|
}
|
|
3153
3233
|
};
|
|
3154
3234
|
var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
@@ -3231,21 +3311,21 @@ var CoberturaReader = class {
|
|
|
3231
3311
|
if (head.length < 9 && ("<!--".startsWith(head) || "<![CDATA[".startsWith(head))) return -1;
|
|
3232
3312
|
if (head.startsWith("<!--")) return this.through(text, open, "-->", 4);
|
|
3233
3313
|
if (head.startsWith("<![CDATA[")) return this.through(text, open, "]]>", 9);
|
|
3234
|
-
let
|
|
3314
|
+
let quote3 = this.quote;
|
|
3235
3315
|
for (let at = open + Math.max(1, this.scanned); at < text.length; at++) {
|
|
3236
3316
|
const char = text.charCodeAt(at);
|
|
3237
|
-
if (
|
|
3238
|
-
if (char ===
|
|
3317
|
+
if (quote3 !== 0) {
|
|
3318
|
+
if (char === quote3) quote3 = 0;
|
|
3239
3319
|
} else if (char === GREATER_THAN) {
|
|
3240
3320
|
this.scanned = 0;
|
|
3241
3321
|
this.quote = 0;
|
|
3242
3322
|
return at;
|
|
3243
3323
|
} else if (char === DOUBLE_QUOTE || char === SINGLE_QUOTE) {
|
|
3244
|
-
|
|
3324
|
+
quote3 = char;
|
|
3245
3325
|
}
|
|
3246
3326
|
}
|
|
3247
3327
|
this.scanned = text.length - open;
|
|
3248
|
-
this.quote =
|
|
3328
|
+
this.quote = quote3;
|
|
3249
3329
|
return -1;
|
|
3250
3330
|
}
|
|
3251
3331
|
/** Where a comment or CDATA section starting at `open` ends: the last character of `end`; -1 if not yet read. */
|
|
@@ -3487,36 +3567,36 @@ function normalizeSlashPath(raw) {
|
|
|
3487
3567
|
}
|
|
3488
3568
|
return (drive || (absolute ? "/" : "")) + out.join("/");
|
|
3489
3569
|
}
|
|
3490
|
-
function isAbsolutePath(
|
|
3491
|
-
return
|
|
3570
|
+
function isAbsolutePath(path24) {
|
|
3571
|
+
return path24.startsWith("/") || /^[A-Za-z]:\//.test(path24);
|
|
3492
3572
|
}
|
|
3493
|
-
function relativeTo(
|
|
3573
|
+
function relativeTo(path24, root) {
|
|
3494
3574
|
const drive = /^[A-Za-z]:/.test(root);
|
|
3495
3575
|
const base = root.endsWith("/") ? root : `${root}/`;
|
|
3496
|
-
const head =
|
|
3576
|
+
const head = path24.slice(0, base.length);
|
|
3497
3577
|
const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
|
|
3498
3578
|
if (!inside) return null;
|
|
3499
|
-
const
|
|
3500
|
-
return
|
|
3579
|
+
const relative9 = path24.slice(base.length);
|
|
3580
|
+
return relative9 || null;
|
|
3501
3581
|
}
|
|
3502
3582
|
function reportPathCandidates(raw, { repoRoot, bases }) {
|
|
3503
3583
|
const slash = (value) => value.replace(/\\/g, "/");
|
|
3504
3584
|
const root = normalizeSlashPath(slash(repoRoot));
|
|
3505
|
-
const
|
|
3506
|
-
if (!root || !
|
|
3585
|
+
const path24 = slash(raw.trim());
|
|
3586
|
+
if (!root || !path24 || path24.includes("\0")) return [];
|
|
3507
3587
|
const out = [];
|
|
3508
3588
|
const add = (absolute) => {
|
|
3509
3589
|
const normalized = normalizeSlashPath(absolute);
|
|
3510
3590
|
if (!normalized) return;
|
|
3511
|
-
const
|
|
3512
|
-
if (
|
|
3591
|
+
const relative9 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
|
|
3592
|
+
if (relative9 && !out.includes(relative9)) out.push(relative9);
|
|
3513
3593
|
};
|
|
3514
|
-
if (isAbsolutePath(
|
|
3515
|
-
add(
|
|
3594
|
+
if (isAbsolutePath(path24)) {
|
|
3595
|
+
add(path24);
|
|
3516
3596
|
} else {
|
|
3517
3597
|
for (const base of bases) {
|
|
3518
3598
|
const dir = slash(base);
|
|
3519
|
-
if (isAbsolutePath(dir)) add(`${dir}/${
|
|
3599
|
+
if (isAbsolutePath(dir)) add(`${dir}/${path24}`);
|
|
3520
3600
|
}
|
|
3521
3601
|
}
|
|
3522
3602
|
return out;
|
|
@@ -3540,14 +3620,14 @@ function globSegmentRegExp(segment) {
|
|
|
3540
3620
|
function expandPattern(pattern, cwd) {
|
|
3541
3621
|
const normalized = pattern.replace(/\\/g, "/");
|
|
3542
3622
|
if (!/[*?]/.test(normalized)) {
|
|
3543
|
-
const file =
|
|
3544
|
-
return
|
|
3623
|
+
const file = path13.resolve(cwd, normalized);
|
|
3624
|
+
return fs9.existsSync(file) && fs9.statSync(file).isFile() ? [file] : [];
|
|
3545
3625
|
}
|
|
3546
|
-
const absolute =
|
|
3626
|
+
const absolute = path13.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
|
|
3547
3627
|
const segments = normalized.split("/");
|
|
3548
3628
|
const firstWild = segments.findIndex((s) => /[*?]/.test(s));
|
|
3549
3629
|
const baseParts = segments.slice(0, firstWild);
|
|
3550
|
-
const base = absolute ? baseParts.join("/") || "/" :
|
|
3630
|
+
const base = absolute ? baseParts.join("/") || "/" : path13.resolve(cwd, ...baseParts);
|
|
3551
3631
|
const rest = segments.slice(firstWild).filter((s) => s !== "");
|
|
3552
3632
|
const out = [];
|
|
3553
3633
|
const walk = (dir, at) => {
|
|
@@ -3555,14 +3635,14 @@ function expandPattern(pattern, cwd) {
|
|
|
3555
3635
|
const segment = rest[at];
|
|
3556
3636
|
let entries;
|
|
3557
3637
|
try {
|
|
3558
|
-
entries =
|
|
3638
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
3559
3639
|
} catch {
|
|
3560
3640
|
return;
|
|
3561
3641
|
}
|
|
3562
3642
|
if (segment === "**") {
|
|
3563
3643
|
walk(dir, at + 1);
|
|
3564
3644
|
for (const entry of entries) {
|
|
3565
|
-
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(
|
|
3645
|
+
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path13.join(dir, entry.name), at);
|
|
3566
3646
|
}
|
|
3567
3647
|
return;
|
|
3568
3648
|
}
|
|
@@ -3570,7 +3650,7 @@ function expandPattern(pattern, cwd) {
|
|
|
3570
3650
|
const last = at === rest.length - 1;
|
|
3571
3651
|
for (const entry of entries) {
|
|
3572
3652
|
if (!match.test(entry.name)) continue;
|
|
3573
|
-
const full =
|
|
3653
|
+
const full = path13.join(dir, entry.name);
|
|
3574
3654
|
if (last && entry.isFile()) out.push(full);
|
|
3575
3655
|
else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
|
|
3576
3656
|
walk(full, at + 1);
|
|
@@ -3583,7 +3663,7 @@ function expandPattern(pattern, cwd) {
|
|
|
3583
3663
|
async function readReport(file, format) {
|
|
3584
3664
|
let reader = format ? new CoverageReportReader(format) : null;
|
|
3585
3665
|
let head = "";
|
|
3586
|
-
for await (const chunk of
|
|
3666
|
+
for await (const chunk of fs9.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
|
|
3587
3667
|
if (!reader) {
|
|
3588
3668
|
head += chunk;
|
|
3589
3669
|
if (head.length < 4096) continue;
|
|
@@ -3605,18 +3685,18 @@ async function readReport(file, format) {
|
|
|
3605
3685
|
return reader.end();
|
|
3606
3686
|
}
|
|
3607
3687
|
function reportBases(report, reportFile, options) {
|
|
3608
|
-
if (options.root) return [
|
|
3609
|
-
const dir =
|
|
3688
|
+
if (options.root) return [path13.resolve(options.cwd, options.root)];
|
|
3689
|
+
const dir = path13.dirname(path13.resolve(reportFile));
|
|
3610
3690
|
const sources = report.sources.map(
|
|
3611
|
-
(source) =>
|
|
3691
|
+
(source) => path13.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path13.resolve(dir, source)
|
|
3612
3692
|
);
|
|
3613
|
-
return [...sources, options.cwd, dir,
|
|
3693
|
+
return [...sources, options.cwd, dir, path13.dirname(dir), options.repoRoot];
|
|
3614
3694
|
}
|
|
3615
3695
|
function resolveReport(report, reportFile, options, into) {
|
|
3616
3696
|
const bases = reportBases(report, reportFile, options);
|
|
3617
3697
|
for (const file of report.files) {
|
|
3618
3698
|
const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
|
|
3619
|
-
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) =>
|
|
3699
|
+
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs9.existsSync(path13.join(options.repoRoot, candidate)));
|
|
3620
3700
|
if (!match) {
|
|
3621
3701
|
into.unmatched.push(file.path);
|
|
3622
3702
|
continue;
|
|
@@ -3841,7 +3921,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
|
|
|
3841
3921
|
commit: checkout.commit,
|
|
3842
3922
|
branch,
|
|
3843
3923
|
suite: args.suite,
|
|
3844
|
-
reports: reports.map((report) =>
|
|
3924
|
+
reports: reports.map((report) => path14.relative(cwd, report) || report),
|
|
3845
3925
|
files: files.length,
|
|
3846
3926
|
unmatched: resolved.unmatched.length,
|
|
3847
3927
|
outOfScope: resolved.outOfScope,
|
|
@@ -3910,10 +3990,10 @@ ${USAGE4}`);
|
|
|
3910
3990
|
}
|
|
3911
3991
|
|
|
3912
3992
|
// src/cli/flake.ts
|
|
3913
|
-
var
|
|
3993
|
+
var import_node_child_process6 = require("child_process");
|
|
3914
3994
|
var fs11 = __toESM(require("fs"));
|
|
3915
3995
|
var os2 = __toESM(require("os"));
|
|
3916
|
-
var
|
|
3996
|
+
var path16 = __toESM(require("path"));
|
|
3917
3997
|
|
|
3918
3998
|
// ../core/src/error-parse.ts
|
|
3919
3999
|
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
@@ -3932,6 +4012,7 @@ var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
|
|
|
3932
4012
|
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
3933
4013
|
var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
|
|
3934
4014
|
var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
|
|
4015
|
+
var MAX_PARSED_ERROR_CHARS = 64 * 1024;
|
|
3935
4016
|
|
|
3936
4017
|
// ../core/src/flake-plan.ts
|
|
3937
4018
|
var FLAKE_PLAN_VERSION = 1;
|
|
@@ -4114,8 +4195,8 @@ function withinBudget(startedAt, now, budgetMs) {
|
|
|
4114
4195
|
}
|
|
4115
4196
|
|
|
4116
4197
|
// src/internal/support/connection.ts
|
|
4117
|
-
var
|
|
4118
|
-
var
|
|
4198
|
+
var fs10 = __toESM(require("fs"));
|
|
4199
|
+
var path15 = __toESM(require("path"));
|
|
4119
4200
|
|
|
4120
4201
|
// ../core/src/dotenv.ts
|
|
4121
4202
|
function parseDotEnv(text) {
|
|
@@ -4124,10 +4205,10 @@ function parseDotEnv(text) {
|
|
|
4124
4205
|
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
4125
4206
|
if (!m) continue;
|
|
4126
4207
|
let value = m[2];
|
|
4127
|
-
const
|
|
4128
|
-
if ((
|
|
4208
|
+
const quote3 = value[0];
|
|
4209
|
+
if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
|
|
4129
4210
|
value = value.slice(1, -1);
|
|
4130
|
-
if (
|
|
4211
|
+
if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
4131
4212
|
} else {
|
|
4132
4213
|
value = value.replace(/\s+#.*$/, "");
|
|
4133
4214
|
}
|
|
@@ -4156,7 +4237,7 @@ function resolveCliConnection(flags, env, dirs) {
|
|
|
4156
4237
|
let dotEnv = {};
|
|
4157
4238
|
for (const dir of dirs) {
|
|
4158
4239
|
try {
|
|
4159
|
-
dotEnv = { ...parseDotEnv(
|
|
4240
|
+
dotEnv = { ...parseDotEnv(fs10.readFileSync(path15.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
4160
4241
|
} catch {
|
|
4161
4242
|
}
|
|
4162
4243
|
}
|
|
@@ -4168,44 +4249,6 @@ function resolveCliConnection(flags, env, dirs) {
|
|
|
4168
4249
|
});
|
|
4169
4250
|
}
|
|
4170
4251
|
|
|
4171
|
-
// src/internal/support/playwright-spawn.ts
|
|
4172
|
-
var import_node_child_process6 = require("child_process");
|
|
4173
|
-
var fs10 = __toESM(require("fs"));
|
|
4174
|
-
var import_node_module2 = require("module");
|
|
4175
|
-
var path14 = __toESM(require("path"));
|
|
4176
|
-
function resolvePlaywrightCli2(cwd = process.cwd()) {
|
|
4177
|
-
const require2 = (0, import_node_module2.createRequire)(path14.join(cwd, "noop.js"));
|
|
4178
|
-
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
4179
|
-
try {
|
|
4180
|
-
return require2.resolve(id);
|
|
4181
|
-
} catch {
|
|
4182
|
-
}
|
|
4183
|
-
}
|
|
4184
|
-
return null;
|
|
4185
|
-
}
|
|
4186
|
-
function spawnPlaywright(playwrightArgs2, env, options = {}) {
|
|
4187
|
-
const output = options.output ?? "inherit";
|
|
4188
|
-
let fd = output === "inherit" ? null : fs10.openSync(output.file, "a");
|
|
4189
|
-
const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
|
|
4190
|
-
const cli = resolvePlaywrightCli2();
|
|
4191
|
-
const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
|
|
4192
|
-
stdio,
|
|
4193
|
-
env
|
|
4194
|
-
});
|
|
4195
|
-
return new Promise((resolve10) => {
|
|
4196
|
-
const done = (code) => {
|
|
4197
|
-
if (fd !== null) fs10.closeSync(fd);
|
|
4198
|
-
fd = null;
|
|
4199
|
-
resolve10(code);
|
|
4200
|
-
};
|
|
4201
|
-
child.on("error", (err) => {
|
|
4202
|
-
console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
|
|
4203
|
-
done(2);
|
|
4204
|
-
});
|
|
4205
|
-
child.on("exit", (code) => done(code ?? 2));
|
|
4206
|
-
});
|
|
4207
|
-
}
|
|
4208
|
-
|
|
4209
4252
|
// src/cli/flake.ts
|
|
4210
4253
|
var USAGE5 = `
|
|
4211
4254
|
piwi flake \u2014 make a flaky test fail on demand, then prove the fix
|
|
@@ -4351,7 +4394,7 @@ function parseFlakeArgs(argv) {
|
|
|
4351
4394
|
}
|
|
4352
4395
|
function gitHead() {
|
|
4353
4396
|
try {
|
|
4354
|
-
return (0,
|
|
4397
|
+
return (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4355
4398
|
} catch {
|
|
4356
4399
|
return null;
|
|
4357
4400
|
}
|
|
@@ -4395,7 +4438,7 @@ function playwrightRunner(dir, log) {
|
|
|
4395
4438
|
return {
|
|
4396
4439
|
now: () => Date.now(),
|
|
4397
4440
|
async runBatch(plan, arm, repeat, batch) {
|
|
4398
|
-
const planFile =
|
|
4441
|
+
const planFile = path16.join(dir, `${arm.id}.plan.json`);
|
|
4399
4442
|
if (!fs11.existsSync(planFile)) {
|
|
4400
4443
|
fs11.writeFileSync(
|
|
4401
4444
|
planFile,
|
|
@@ -4408,7 +4451,7 @@ function playwrightRunner(dir, log) {
|
|
|
4408
4451
|
})
|
|
4409
4452
|
);
|
|
4410
4453
|
}
|
|
4411
|
-
const resultsFile =
|
|
4454
|
+
const resultsFile = path16.join(dir, `${arm.id}.${batch}.jsonl`);
|
|
4412
4455
|
fs11.writeFileSync(resultsFile, "");
|
|
4413
4456
|
const env = {
|
|
4414
4457
|
...process.env,
|
|
@@ -4687,8 +4730,8 @@ async function runFlake(argv, runner) {
|
|
|
4687
4730
|
if (!args.json) {
|
|
4688
4731
|
printHeader(plan, commit, estimate, args.budgetMs);
|
|
4689
4732
|
}
|
|
4690
|
-
const dir = fs11.mkdtempSync(
|
|
4691
|
-
const log =
|
|
4733
|
+
const dir = fs11.mkdtempSync(path16.join(os2.tmpdir(), "piwi-flake-"));
|
|
4734
|
+
const log = path16.join(dir, "playwright.log");
|
|
4692
4735
|
let session;
|
|
4693
4736
|
try {
|
|
4694
4737
|
session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
|
|
@@ -4879,9 +4922,9 @@ function readCount(argv, name) {
|
|
|
4879
4922
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
4880
4923
|
return Math.floor(n);
|
|
4881
4924
|
}
|
|
4882
|
-
function readRunIdFromFile(
|
|
4925
|
+
function readRunIdFromFile(path24) {
|
|
4883
4926
|
try {
|
|
4884
|
-
const parsed = JSON.parse(fs12.readFileSync(
|
|
4927
|
+
const parsed = JSON.parse(fs12.readFileSync(path24, "utf-8"));
|
|
4885
4928
|
const runId = Number(parsed.runId);
|
|
4886
4929
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
4887
4930
|
} catch {
|
|
@@ -4973,8 +5016,7 @@ async function runGate(argv, env = process.env) {
|
|
|
4973
5016
|
|
|
4974
5017
|
// src/cli/init.ts
|
|
4975
5018
|
var fs15 = __toESM(require("fs"));
|
|
4976
|
-
var
|
|
4977
|
-
var import_node_child_process8 = require("child_process");
|
|
5019
|
+
var path19 = __toESM(require("path"));
|
|
4978
5020
|
|
|
4979
5021
|
// src/cli/report.ts
|
|
4980
5022
|
var STATUS_MARK = {
|
|
@@ -4992,7 +5034,7 @@ function formatStep(result) {
|
|
|
4992
5034
|
|
|
4993
5035
|
// src/cli/detect.ts
|
|
4994
5036
|
var fs13 = __toESM(require("fs"));
|
|
4995
|
-
var
|
|
5037
|
+
var path17 = __toESM(require("path"));
|
|
4996
5038
|
var CONFIG_NAMES = [
|
|
4997
5039
|
"playwright.config.ts",
|
|
4998
5040
|
"playwright.config.mts",
|
|
@@ -5010,7 +5052,7 @@ var LOCKFILES = [
|
|
|
5010
5052
|
];
|
|
5011
5053
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
5012
5054
|
function readPackageJson(root) {
|
|
5013
|
-
const pkgPath =
|
|
5055
|
+
const pkgPath = path17.join(root, "package.json");
|
|
5014
5056
|
try {
|
|
5015
5057
|
return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
5016
5058
|
} catch {
|
|
@@ -5021,13 +5063,13 @@ function detectPackageManager(root, pkg) {
|
|
|
5021
5063
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
5022
5064
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
5023
5065
|
for (const [file, manager] of LOCKFILES) {
|
|
5024
|
-
if (fs13.existsSync(
|
|
5066
|
+
if (fs13.existsSync(path17.join(root, file))) return manager;
|
|
5025
5067
|
}
|
|
5026
5068
|
return "npm";
|
|
5027
5069
|
}
|
|
5028
5070
|
function findConfig(root) {
|
|
5029
5071
|
for (const name of CONFIG_NAMES) {
|
|
5030
|
-
const candidate =
|
|
5072
|
+
const candidate = path17.join(root, name);
|
|
5031
5073
|
if (fs13.existsSync(candidate)) return candidate;
|
|
5032
5074
|
}
|
|
5033
5075
|
return null;
|
|
@@ -5038,14 +5080,14 @@ function langOf(configPath) {
|
|
|
5038
5080
|
}
|
|
5039
5081
|
function suggestProjectName(root, pkg) {
|
|
5040
5082
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
5041
|
-
return fromPkg ||
|
|
5083
|
+
return fromPkg || path17.basename(root) || "default-project";
|
|
5042
5084
|
}
|
|
5043
5085
|
function hasReporter(pkg) {
|
|
5044
5086
|
if (!pkg) return false;
|
|
5045
5087
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
5046
5088
|
}
|
|
5047
5089
|
function detectProject(root) {
|
|
5048
|
-
const absRoot =
|
|
5090
|
+
const absRoot = path17.resolve(root);
|
|
5049
5091
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
5050
5092
|
const configPath = findConfig(absRoot);
|
|
5051
5093
|
return {
|
|
@@ -5075,9 +5117,7 @@ function installCommand(manager) {
|
|
|
5075
5117
|
// src/cli/configure.ts
|
|
5076
5118
|
var REPORTER_IMPORT = "import { wrapConfig } from '@piwitests/reporter'";
|
|
5077
5119
|
var MANUAL_HINT = "Could not find `export default defineConfig(...)`. Wrap your exported config by hand: import { wrapConfig } from '@piwitests/reporter', then export default wrapConfig(defineConfig({ ... }), { serverUrl, projectName }).";
|
|
5078
|
-
|
|
5079
|
-
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
5080
|
-
}
|
|
5120
|
+
var quote2 = quoteJsString;
|
|
5081
5121
|
function matchClosingParen(src, openIndex) {
|
|
5082
5122
|
let depth = 0;
|
|
5083
5123
|
let quoteChar = null;
|
|
@@ -5113,8 +5153,8 @@ function matchClosingParen(src, openIndex) {
|
|
|
5113
5153
|
}
|
|
5114
5154
|
function optionsLiteral(opts) {
|
|
5115
5155
|
const lines = [];
|
|
5116
|
-
if (opts.serverUrl) lines.push(` serverUrl: ${
|
|
5117
|
-
lines.push(` projectName: ${
|
|
5156
|
+
if (opts.serverUrl) lines.push(` serverUrl: ${quote2(opts.serverUrl)},`);
|
|
5157
|
+
lines.push(` projectName: ${quote2(opts.projectName)},`);
|
|
5118
5158
|
return `{
|
|
5119
5159
|
${lines.join("\n")}
|
|
5120
5160
|
}`;
|
|
@@ -5188,7 +5228,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
5188
5228
|
|
|
5189
5229
|
// src/cli/skills.ts
|
|
5190
5230
|
var fs14 = __toESM(require("fs"));
|
|
5191
|
-
var
|
|
5231
|
+
var path18 = __toESM(require("path"));
|
|
5192
5232
|
|
|
5193
5233
|
// ../core/src/skill-stamp.ts
|
|
5194
5234
|
var SKILL_VERSION_KEY = "piwi-version";
|
|
@@ -5251,16 +5291,16 @@ var WORKFLOW_SKILLS = [
|
|
|
5251
5291
|
"fix-a-reported-bug"
|
|
5252
5292
|
];
|
|
5253
5293
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
5254
|
-
var DEFAULT_SKILLS_DIR =
|
|
5294
|
+
var DEFAULT_SKILLS_DIR = path18.join(".claude", "skills");
|
|
5255
5295
|
function findTemplatesDir(fromDir) {
|
|
5256
5296
|
let dir = fromDir;
|
|
5257
5297
|
for (let i = 0; i < 6; i++) {
|
|
5258
|
-
if (fs14.existsSync(
|
|
5259
|
-
const parent =
|
|
5298
|
+
if (fs14.existsSync(path18.join(dir, "templates", "skills"))) return path18.join(dir, "templates");
|
|
5299
|
+
const parent = path18.dirname(dir);
|
|
5260
5300
|
if (parent === dir) break;
|
|
5261
5301
|
dir = parent;
|
|
5262
5302
|
}
|
|
5263
|
-
return
|
|
5303
|
+
return path18.resolve(fromDir, "..", "..", "templates");
|
|
5264
5304
|
}
|
|
5265
5305
|
function readFrontMatter(markdown) {
|
|
5266
5306
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -5274,14 +5314,14 @@ function readFrontMatter(markdown) {
|
|
|
5274
5314
|
}
|
|
5275
5315
|
function readTemplatesVersion(templatesDir) {
|
|
5276
5316
|
try {
|
|
5277
|
-
const pkg = JSON.parse(fs14.readFileSync(
|
|
5317
|
+
const pkg = JSON.parse(fs14.readFileSync(path18.join(templatesDir, "..", "package.json"), "utf-8"));
|
|
5278
5318
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
5279
5319
|
} catch {
|
|
5280
5320
|
return "0.0.0";
|
|
5281
5321
|
}
|
|
5282
5322
|
}
|
|
5283
5323
|
function templatePath(templatesDir, slug) {
|
|
5284
|
-
return
|
|
5324
|
+
return path18.join(templatesDir, "skills", slug, "SKILL.md");
|
|
5285
5325
|
}
|
|
5286
5326
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
5287
5327
|
const infos = [];
|
|
@@ -5298,8 +5338,8 @@ function installSkills(opts) {
|
|
|
5298
5338
|
for (const slug of opts.slugs) {
|
|
5299
5339
|
const step = `skill:${slug}`;
|
|
5300
5340
|
const source = templatePath(opts.templatesDir, slug);
|
|
5301
|
-
const relDest =
|
|
5302
|
-
const dest =
|
|
5341
|
+
const relDest = path18.join(opts.skillsDir, slug, "SKILL.md");
|
|
5342
|
+
const dest = path18.join(opts.root, relDest);
|
|
5303
5343
|
if (!fs14.existsSync(source)) {
|
|
5304
5344
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
5305
5345
|
continue;
|
|
@@ -5331,7 +5371,7 @@ function installSkills(opts) {
|
|
|
5331
5371
|
}
|
|
5332
5372
|
}
|
|
5333
5373
|
if (!opts.dryRun) {
|
|
5334
|
-
fs14.mkdirSync(
|
|
5374
|
+
fs14.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
5335
5375
|
fs14.writeFileSync(dest, contents);
|
|
5336
5376
|
}
|
|
5337
5377
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
|
|
@@ -5404,7 +5444,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
5404
5444
|
}
|
|
5405
5445
|
const results = installSkills({
|
|
5406
5446
|
templatesDir,
|
|
5407
|
-
root:
|
|
5447
|
+
root: path18.resolve(readOption5(rest, "--cwd") ?? cwd),
|
|
5408
5448
|
skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
5409
5449
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
5410
5450
|
force: rest.includes("--force"),
|
|
@@ -5475,7 +5515,7 @@ function resolveSkillSlugs(raw) {
|
|
|
5475
5515
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
5476
5516
|
}
|
|
5477
5517
|
function parseInitArgs(argv, env, cwd) {
|
|
5478
|
-
const root =
|
|
5518
|
+
const root = path19.resolve(readOption6(argv, "--cwd") ?? cwd);
|
|
5479
5519
|
const detected = detectProject(root);
|
|
5480
5520
|
return {
|
|
5481
5521
|
root,
|
|
@@ -5524,12 +5564,12 @@ function stepInstallReporter(project, opts) {
|
|
|
5524
5564
|
if (opts.dryRun || !opts.install)
|
|
5525
5565
|
return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
|
|
5526
5566
|
const [command, args] = installArgv(project.packageManager);
|
|
5527
|
-
const
|
|
5528
|
-
if (
|
|
5567
|
+
const status = runPackageManager(command, args, opts.root);
|
|
5568
|
+
if (status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
|
|
5529
5569
|
return {
|
|
5530
5570
|
step,
|
|
5531
5571
|
status: "error",
|
|
5532
|
-
detail: `\`${installCommand(project.packageManager)}\` failed (exit ${
|
|
5572
|
+
detail: `\`${installCommand(project.packageManager)}\` failed (exit ${status ?? "unknown"}) \u2014 install it by hand`
|
|
5533
5573
|
};
|
|
5534
5574
|
}
|
|
5535
5575
|
function stepConfig(project, opts) {
|
|
@@ -5540,7 +5580,7 @@ function stepConfig(project, opts) {
|
|
|
5540
5580
|
status: "manual",
|
|
5541
5581
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
5542
5582
|
};
|
|
5543
|
-
const rel =
|
|
5583
|
+
const rel = path19.relative(project.root, project.configPath) || path19.basename(project.configPath);
|
|
5544
5584
|
const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
|
|
5545
5585
|
serverUrl: opts.serverUrl,
|
|
5546
5586
|
projectName: opts.projectName
|
|
@@ -5550,8 +5590,8 @@ function stepConfig(project, opts) {
|
|
|
5550
5590
|
}
|
|
5551
5591
|
function stepFixtures(project, opts) {
|
|
5552
5592
|
const step = "fixtures";
|
|
5553
|
-
const rel =
|
|
5554
|
-
const dest =
|
|
5593
|
+
const rel = path19.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
5594
|
+
const dest = path19.join(project.root, rel);
|
|
5555
5595
|
if (fs15.existsSync(dest)) {
|
|
5556
5596
|
const current = readFileOr(dest, "");
|
|
5557
5597
|
if (/piwiFixtures/.test(current))
|
|
@@ -5564,7 +5604,7 @@ function stepFixtures(project, opts) {
|
|
|
5564
5604
|
};
|
|
5565
5605
|
}
|
|
5566
5606
|
if (!opts.dryRun) {
|
|
5567
|
-
fs15.mkdirSync(
|
|
5607
|
+
fs15.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
5568
5608
|
fs15.writeFileSync(dest, fixturesContents());
|
|
5569
5609
|
}
|
|
5570
5610
|
return {
|
|
@@ -5576,7 +5616,7 @@ function stepFixtures(project, opts) {
|
|
|
5576
5616
|
}
|
|
5577
5617
|
function stepEnv(project, opts) {
|
|
5578
5618
|
const results = [];
|
|
5579
|
-
const examplePath =
|
|
5619
|
+
const examplePath = path19.join(project.root, ".env.example");
|
|
5580
5620
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
5581
5621
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
5582
5622
|
["PIWI_API_KEY", ""]
|
|
@@ -5589,7 +5629,7 @@ function stepEnv(project, opts) {
|
|
|
5589
5629
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
5590
5630
|
});
|
|
5591
5631
|
if (opts.apiKey) {
|
|
5592
|
-
const envPath =
|
|
5632
|
+
const envPath = path19.join(project.root, ".env");
|
|
5593
5633
|
const env = upsertEnvKeys(readFileOr(envPath, ""), [
|
|
5594
5634
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
5595
5635
|
["PIWI_API_KEY", opts.apiKey]
|
|
@@ -5601,7 +5641,7 @@ function stepEnv(project, opts) {
|
|
|
5601
5641
|
status: env.added.length ? "updated" : "already",
|
|
5602
5642
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
5603
5643
|
});
|
|
5604
|
-
const gitignorePath =
|
|
5644
|
+
const gitignorePath = path19.join(project.root, ".gitignore");
|
|
5605
5645
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
5606
5646
|
if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
|
|
5607
5647
|
results.push({
|
|
@@ -5665,7 +5705,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5665
5705
|
project: {
|
|
5666
5706
|
root: project.root,
|
|
5667
5707
|
packageManager: project.packageManager,
|
|
5668
|
-
configPath: project.configPath ?
|
|
5708
|
+
configPath: project.configPath ? path19.relative(project.root, project.configPath) : null,
|
|
5669
5709
|
projectName: opts.projectName,
|
|
5670
5710
|
serverUrl: opts.serverUrl
|
|
5671
5711
|
},
|
|
@@ -5690,14 +5730,13 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
5690
5730
|
|
|
5691
5731
|
// src/cli/select.ts
|
|
5692
5732
|
var fs17 = __toESM(require("fs"));
|
|
5693
|
-
var
|
|
5694
|
-
var
|
|
5695
|
-
var import_node_module4 = require("module");
|
|
5733
|
+
var path21 = __toESM(require("path"));
|
|
5734
|
+
var import_node_child_process7 = require("child_process");
|
|
5696
5735
|
|
|
5697
5736
|
// src/cli/add-reporter.ts
|
|
5698
5737
|
var fs16 = __toESM(require("fs"));
|
|
5699
|
-
var
|
|
5700
|
-
var
|
|
5738
|
+
var path20 = __toESM(require("path"));
|
|
5739
|
+
var import_node_module2 = require("module");
|
|
5701
5740
|
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
5702
5741
|
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
5703
5742
|
var ADD_REPORTER_MIN = { major: 1, minor: 63 };
|
|
@@ -5729,9 +5768,9 @@ function resolveConfigPath(cwd, playwrightArgs2) {
|
|
|
5729
5768
|
const arg = playwrightArgs2[i];
|
|
5730
5769
|
if (arg === "--config" || arg === "-c") {
|
|
5731
5770
|
const next = playwrightArgs2[i + 1];
|
|
5732
|
-
if (next) return
|
|
5771
|
+
if (next) return path20.resolve(cwd, next);
|
|
5733
5772
|
} else if (arg.startsWith("--config=")) {
|
|
5734
|
-
return
|
|
5773
|
+
return path20.resolve(cwd, arg.slice("--config=".length));
|
|
5735
5774
|
}
|
|
5736
5775
|
}
|
|
5737
5776
|
return detectProject(cwd).configPath;
|
|
@@ -5746,7 +5785,7 @@ function readConfigSource(configPath) {
|
|
|
5746
5785
|
}
|
|
5747
5786
|
function readInstalledPlaywrightVersion(cwd) {
|
|
5748
5787
|
try {
|
|
5749
|
-
const require2 = (0,
|
|
5788
|
+
const require2 = (0, import_node_module2.createRequire)(path20.join(cwd, "noop.js"));
|
|
5750
5789
|
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
5751
5790
|
try {
|
|
5752
5791
|
return require2(id).version ?? null;
|
|
@@ -5866,7 +5905,7 @@ function parseSelectArgs(argv, env) {
|
|
|
5866
5905
|
extra
|
|
5867
5906
|
};
|
|
5868
5907
|
}
|
|
5869
|
-
var CACHE_FILE =
|
|
5908
|
+
var CACHE_FILE = path21.join(".piwi", "selection-cache.json");
|
|
5870
5909
|
function cacheKey(args) {
|
|
5871
5910
|
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
5872
5911
|
}
|
|
@@ -5886,7 +5925,7 @@ function writeCache(args, resolution) {
|
|
|
5886
5925
|
} catch {
|
|
5887
5926
|
}
|
|
5888
5927
|
store[cacheKey(args)] = resolution;
|
|
5889
|
-
fs17.mkdirSync(
|
|
5928
|
+
fs17.mkdirSync(path21.dirname(CACHE_FILE), { recursive: true });
|
|
5890
5929
|
fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
|
|
5891
5930
|
} catch {
|
|
5892
5931
|
}
|
|
@@ -5911,7 +5950,7 @@ function printWarnings(resolution) {
|
|
|
5911
5950
|
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
5912
5951
|
}
|
|
5913
5952
|
function gitChangedFiles(base) {
|
|
5914
|
-
const out = (0,
|
|
5953
|
+
const out = (0, import_node_child_process7.execFileSync)("git", ["diff", "--name-only", base], {
|
|
5915
5954
|
encoding: "utf-8",
|
|
5916
5955
|
stdio: ["ignore", "pipe", "ignore"]
|
|
5917
5956
|
});
|
|
@@ -5983,34 +6022,10 @@ async function runSelect(argv, env = process.env) {
|
|
|
5983
6022
|
else console.log(resolution.materialization.args.join(" "));
|
|
5984
6023
|
return EXIT_OK6;
|
|
5985
6024
|
}
|
|
5986
|
-
function resolvePlaywrightCli3() {
|
|
5987
|
-
const require2 = (0, import_node_module4.createRequire)(path20.join(process.cwd(), "noop.js"));
|
|
5988
|
-
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
5989
|
-
try {
|
|
5990
|
-
return require2.resolve(id);
|
|
5991
|
-
} catch {
|
|
5992
|
-
}
|
|
5993
|
-
}
|
|
5994
|
-
return null;
|
|
5995
|
-
}
|
|
5996
|
-
function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
|
|
5997
|
-
const cli = resolvePlaywrightCli3();
|
|
5998
|
-
const child = cli ? (0, import_node_child_process9.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process9.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
|
|
5999
|
-
stdio: "inherit",
|
|
6000
|
-
env
|
|
6001
|
-
});
|
|
6002
|
-
return new Promise((resolve10) => {
|
|
6003
|
-
child.on("error", (err) => {
|
|
6004
|
-
console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
|
|
6005
|
-
resolve10(EXIT_ERROR7);
|
|
6006
|
-
});
|
|
6007
|
-
child.on("exit", (code) => resolve10(code ?? EXIT_ERROR7));
|
|
6008
|
-
});
|
|
6009
|
-
}
|
|
6010
6025
|
function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
|
|
6011
6026
|
const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
|
|
6012
6027
|
if (decision.log) console.error(decision.log);
|
|
6013
|
-
return
|
|
6028
|
+
return spawnPlaywright([...decision.args, ...playwrightArgs2], env, { runner: pkgRunner, command: "piwi run" });
|
|
6014
6029
|
}
|
|
6015
6030
|
function shardEnv(args) {
|
|
6016
6031
|
return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
|
|
@@ -6118,15 +6133,15 @@ async function runRun(argv, env = process.env) {
|
|
|
6118
6133
|
|
|
6119
6134
|
// src/cli/preflight.ts
|
|
6120
6135
|
var fs18 = __toESM(require("fs"));
|
|
6121
|
-
var
|
|
6122
|
-
var
|
|
6136
|
+
var path22 = __toESM(require("path"));
|
|
6137
|
+
var import_node_child_process8 = require("child_process");
|
|
6123
6138
|
|
|
6124
6139
|
// ../core/src/diff-anchors.ts
|
|
6125
6140
|
var RENAME_FROM = `rename from `;
|
|
6126
6141
|
var RENAME_TO = "rename to ";
|
|
6127
6142
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
6128
|
-
function stripPrefix(
|
|
6129
|
-
const unquoted =
|
|
6143
|
+
function stripPrefix(path24) {
|
|
6144
|
+
const unquoted = path24.startsWith('"') && path24.endsWith('"') ? path24.slice(1, -1) : path24;
|
|
6130
6145
|
return unquoted.replace(/^[ab]\//, "");
|
|
6131
6146
|
}
|
|
6132
6147
|
function parseUnifiedDiff(text) {
|
|
@@ -6236,8 +6251,8 @@ function pathGlobToRegExp(glob) {
|
|
|
6236
6251
|
});
|
|
6237
6252
|
return new RegExp(`^(?:${parts.join("|")})$`);
|
|
6238
6253
|
}
|
|
6239
|
-
function isTranslationFile(
|
|
6240
|
-
const posix =
|
|
6254
|
+
function isTranslationFile(path24, globs = DEFAULT_TRANSLATION_GLOBS) {
|
|
6255
|
+
const posix = path24.replace(/\\/g, "/");
|
|
6241
6256
|
if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
|
|
6242
6257
|
return globs.some((g) => pathGlobToRegExp(g).test(posix));
|
|
6243
6258
|
}
|
|
@@ -6248,7 +6263,7 @@ function keepable(value) {
|
|
|
6248
6263
|
return new RegExp("\\p{L}", "u").test(v);
|
|
6249
6264
|
}
|
|
6250
6265
|
function readQuoted(text, start) {
|
|
6251
|
-
const
|
|
6266
|
+
const quote3 = text[start];
|
|
6252
6267
|
let value = "";
|
|
6253
6268
|
for (let i = start + 1; i < text.length; i++) {
|
|
6254
6269
|
const ch = text[i];
|
|
@@ -6258,8 +6273,8 @@ function readQuoted(text, start) {
|
|
|
6258
6273
|
i++;
|
|
6259
6274
|
continue;
|
|
6260
6275
|
}
|
|
6261
|
-
if (
|
|
6262
|
-
if (ch ===
|
|
6276
|
+
if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
|
|
6277
|
+
if (ch === quote3) return { value, end: i + 1 };
|
|
6263
6278
|
value += ch;
|
|
6264
6279
|
}
|
|
6265
6280
|
return null;
|
|
@@ -6377,13 +6392,13 @@ function unquoteYaml(raw) {
|
|
|
6377
6392
|
function decodeXml2(text) {
|
|
6378
6393
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
6379
6394
|
}
|
|
6380
|
-
function parseTranslationFile(
|
|
6395
|
+
function parseTranslationFile(path24, content) {
|
|
6381
6396
|
const lines = content.split(/\r?\n/);
|
|
6382
6397
|
const out = [];
|
|
6383
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
6398
|
+
const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
|
|
6384
6399
|
if (ext === "json") {
|
|
6385
6400
|
const stack = [];
|
|
6386
|
-
const
|
|
6401
|
+
const path25 = (key) => [...stack.map((f) => f.key), key].join(".");
|
|
6387
6402
|
lines.forEach((text, i) => {
|
|
6388
6403
|
const t = text.trim();
|
|
6389
6404
|
const top = stack[stack.length - 1];
|
|
@@ -6392,14 +6407,14 @@ function parseTranslationFile(path23, content) {
|
|
|
6392
6407
|
const key = JSON.parse(`"${pair[1]}"`);
|
|
6393
6408
|
const rest = pair[2];
|
|
6394
6409
|
const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
|
|
6395
|
-
if (str) out.push({ key:
|
|
6410
|
+
if (str) out.push({ key: path25(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6396
6411
|
else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
|
|
6397
6412
|
return;
|
|
6398
6413
|
}
|
|
6399
6414
|
if (top?.array) {
|
|
6400
6415
|
const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
|
|
6401
6416
|
if (str) {
|
|
6402
|
-
out.push({ key:
|
|
6417
|
+
out.push({ key: path25(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6403
6418
|
return;
|
|
6404
6419
|
}
|
|
6405
6420
|
if (/^[[{]\s*$/.test(t)) {
|
|
@@ -6463,8 +6478,8 @@ function parseTranslationFile(path23, content) {
|
|
|
6463
6478
|
}
|
|
6464
6479
|
return out;
|
|
6465
6480
|
}
|
|
6466
|
-
function translationLineToken(
|
|
6467
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
6481
|
+
function translationLineToken(path24, text, line) {
|
|
6482
|
+
const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
|
|
6468
6483
|
let key;
|
|
6469
6484
|
let value;
|
|
6470
6485
|
if (ext === "json") {
|
|
@@ -6501,9 +6516,9 @@ function translationLineToken(path23, text, line) {
|
|
|
6501
6516
|
if (value === void 0 || !keepable(value)) return null;
|
|
6502
6517
|
return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
|
|
6503
6518
|
}
|
|
6504
|
-
function lineTokens(
|
|
6519
|
+
function lineTokens(path24, text, line, side, attributes, options) {
|
|
6505
6520
|
const spans = [];
|
|
6506
|
-
const markup = MARKUP_FILE.test(
|
|
6521
|
+
const markup = MARKUP_FILE.test(path24);
|
|
6507
6522
|
return [
|
|
6508
6523
|
...attributeTokens(text, attributes, line, spans),
|
|
6509
6524
|
...translationRefTokens(text, line, side, options.translations, spans),
|
|
@@ -6511,10 +6526,10 @@ function lineTokens(path23, text, line, side, attributes, options) {
|
|
|
6511
6526
|
...textTokens(text, line, markup)
|
|
6512
6527
|
];
|
|
6513
6528
|
}
|
|
6514
|
-
function keysByLine(
|
|
6515
|
-
const content = options.readFile?.(
|
|
6529
|
+
function keysByLine(path24, side, options) {
|
|
6530
|
+
const content = options.readFile?.(path24, side);
|
|
6516
6531
|
if (content === void 0) return null;
|
|
6517
|
-
return new Map(parseTranslationFile(
|
|
6532
|
+
return new Map(parseTranslationFile(path24, content).map((e) => [e.line, e.key]));
|
|
6518
6533
|
}
|
|
6519
6534
|
function tokenId(t) {
|
|
6520
6535
|
return `${t.group}\0${t.value}`;
|
|
@@ -6712,9 +6727,9 @@ function cssSlots(selector, testIds, replace) {
|
|
|
6712
6727
|
const attr = m[1];
|
|
6713
6728
|
const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
|
|
6714
6729
|
if (!names) continue;
|
|
6715
|
-
const
|
|
6730
|
+
const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
|
|
6716
6731
|
const value = m[2] ?? m[3] ?? m[4];
|
|
6717
|
-
slot(value, [attr], (v) => `[${attr}=${
|
|
6732
|
+
slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
|
|
6718
6733
|
}
|
|
6719
6734
|
return slots;
|
|
6720
6735
|
}
|
|
@@ -6814,7 +6829,7 @@ function slotMatches(slot, text) {
|
|
|
6814
6829
|
return textMatches(slot.value, slot.exact, text);
|
|
6815
6830
|
}
|
|
6816
6831
|
}
|
|
6817
|
-
var filePath = (
|
|
6832
|
+
var filePath = (path24) => path24.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6818
6833
|
function sameFilePath(a, b) {
|
|
6819
6834
|
const x = filePath(a);
|
|
6820
6835
|
const y = filePath(b);
|
|
@@ -6908,16 +6923,16 @@ function confidenceOf(anchor, tests, reach) {
|
|
|
6908
6923
|
}
|
|
6909
6924
|
|
|
6910
6925
|
// ../core/src/locator-edit.ts
|
|
6911
|
-
function escapeForQuote(value,
|
|
6912
|
-
let out = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
|
|
6913
|
-
out = out.split(
|
|
6914
|
-
return
|
|
6926
|
+
function escapeForQuote(value, quote3) {
|
|
6927
|
+
let out = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
6928
|
+
out = out.split(quote3).join(`\\${quote3}`);
|
|
6929
|
+
return quote3 === "`" ? out.replace(/\$\{/g, "\\${") : out;
|
|
6915
6930
|
}
|
|
6916
6931
|
function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
6917
6932
|
if (!sourceLine || oldValue === newValue) return null;
|
|
6918
6933
|
for (let i = 0; i < sourceLine.length; i++) {
|
|
6919
|
-
const
|
|
6920
|
-
if (
|
|
6934
|
+
const quote3 = sourceLine[i];
|
|
6935
|
+
if (quote3 !== "'" && quote3 !== '"' && quote3 !== "`") continue;
|
|
6921
6936
|
let value = "";
|
|
6922
6937
|
let end = -1;
|
|
6923
6938
|
for (let j = i + 1; j < sourceLine.length; j++) {
|
|
@@ -6928,8 +6943,8 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
|
6928
6943
|
j++;
|
|
6929
6944
|
continue;
|
|
6930
6945
|
}
|
|
6931
|
-
if (
|
|
6932
|
-
if (ch ===
|
|
6946
|
+
if (quote3 === "`" && ch === "$" && sourceLine[j + 1] === "{") break;
|
|
6947
|
+
if (ch === quote3) {
|
|
6933
6948
|
end = j;
|
|
6934
6949
|
break;
|
|
6935
6950
|
}
|
|
@@ -6937,7 +6952,7 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
|
6937
6952
|
}
|
|
6938
6953
|
if (end === -1) continue;
|
|
6939
6954
|
if (value === oldValue) {
|
|
6940
|
-
const next = sourceLine.slice(0, i + 1) + escapeForQuote(newValue,
|
|
6955
|
+
const next = sourceLine.slice(0, i + 1) + escapeForQuote(newValue, quote3) + sourceLine.slice(end);
|
|
6941
6956
|
return { old: sourceLine, new: next };
|
|
6942
6957
|
}
|
|
6943
6958
|
i = end;
|
|
@@ -7029,16 +7044,16 @@ function parsePreflightArgs(argv) {
|
|
|
7029
7044
|
}
|
|
7030
7045
|
var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
|
|
7031
7046
|
function findPlaywrightRoot(from) {
|
|
7032
|
-
let dir =
|
|
7047
|
+
let dir = path22.resolve(from);
|
|
7033
7048
|
for (; ; ) {
|
|
7034
|
-
if (CONFIG_NAMES2.some((name) => fs18.existsSync(
|
|
7035
|
-
const parent =
|
|
7049
|
+
if (CONFIG_NAMES2.some((name) => fs18.existsSync(path22.join(dir, name)))) return dir;
|
|
7050
|
+
const parent = path22.dirname(dir);
|
|
7036
7051
|
if (parent === dir) return null;
|
|
7037
7052
|
dir = parent;
|
|
7038
7053
|
}
|
|
7039
7054
|
}
|
|
7040
7055
|
function git2(cwd, args) {
|
|
7041
|
-
return (0,
|
|
7056
|
+
return (0, import_node_child_process8.execFileSync)("git", args, {
|
|
7042
7057
|
cwd,
|
|
7043
7058
|
encoding: "utf-8",
|
|
7044
7059
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -7062,7 +7077,7 @@ function translationLookup(repoRoot, base, locale) {
|
|
|
7062
7077
|
const build = (side) => {
|
|
7063
7078
|
const map = /* @__PURE__ */ new Map();
|
|
7064
7079
|
for (const file of files) {
|
|
7065
|
-
const content = side === "new" ? readText(
|
|
7080
|
+
const content = side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
|
|
7066
7081
|
if (content === void 0) continue;
|
|
7067
7082
|
for (const entry of parseTranslationFile(file, content)) {
|
|
7068
7083
|
if (!map.has(entry.key)) map.set(entry.key, entry.value);
|
|
@@ -7086,20 +7101,20 @@ function readText(file) {
|
|
|
7086
7101
|
return void 0;
|
|
7087
7102
|
}
|
|
7088
7103
|
}
|
|
7089
|
-
var CACHE_FILE2 =
|
|
7104
|
+
var CACHE_FILE2 = path22.join(".piwi", "locator-index.json");
|
|
7090
7105
|
function cacheKey2(connection, branch) {
|
|
7091
7106
|
return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
|
|
7092
7107
|
}
|
|
7093
7108
|
function readCache2(root, key) {
|
|
7094
7109
|
try {
|
|
7095
|
-
const store = JSON.parse(fs18.readFileSync(
|
|
7110
|
+
const store = JSON.parse(fs18.readFileSync(path22.join(root, CACHE_FILE2), "utf-8"));
|
|
7096
7111
|
return store[key] ?? null;
|
|
7097
7112
|
} catch {
|
|
7098
7113
|
return null;
|
|
7099
7114
|
}
|
|
7100
7115
|
}
|
|
7101
7116
|
function writeCache2(root, key, index) {
|
|
7102
|
-
const file =
|
|
7117
|
+
const file = path22.join(root, CACHE_FILE2);
|
|
7103
7118
|
try {
|
|
7104
7119
|
let store = {};
|
|
7105
7120
|
try {
|
|
@@ -7107,7 +7122,7 @@ function writeCache2(root, key, index) {
|
|
|
7107
7122
|
} catch {
|
|
7108
7123
|
}
|
|
7109
7124
|
store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
|
|
7110
|
-
fs18.mkdirSync(
|
|
7125
|
+
fs18.mkdirSync(path22.dirname(file), { recursive: true });
|
|
7111
7126
|
fs18.writeFileSync(file, JSON.stringify(store));
|
|
7112
7127
|
} catch {
|
|
7113
7128
|
}
|
|
@@ -7124,7 +7139,7 @@ function planSites(b, testRoot) {
|
|
|
7124
7139
|
const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
|
|
7125
7140
|
return sites.map((callSite) => {
|
|
7126
7141
|
if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
|
|
7127
|
-
const file =
|
|
7142
|
+
const file = path22.resolve(testRoot, callSiteFile(callSite));
|
|
7128
7143
|
const lineNo = callSiteLine(callSite);
|
|
7129
7144
|
const text = readText(file);
|
|
7130
7145
|
if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
|
|
@@ -7140,8 +7155,8 @@ function planSites(b, testRoot) {
|
|
|
7140
7155
|
}
|
|
7141
7156
|
const holders = /* @__PURE__ */ new Set();
|
|
7142
7157
|
for (const [before] of b.replacements) {
|
|
7143
|
-
for (const
|
|
7144
|
-
const out = tryGit(testRoot, ["grep", "-n", "--fixed-strings", `${
|
|
7158
|
+
for (const quote3 of ["'", '"', "`"]) {
|
|
7159
|
+
const out = tryGit(testRoot, ["grep", "-n", "--fixed-strings", `${quote3}${before}${quote3}`]);
|
|
7145
7160
|
for (const row of (out ?? "").split("\n")) {
|
|
7146
7161
|
const m = /^(.+?):(\d+):/.exec(row);
|
|
7147
7162
|
if (m) holders.add(`${m[1]}:${m[2]}`);
|
|
@@ -7288,7 +7303,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7288
7303
|
console.error("piwi preflight: not inside a git repository");
|
|
7289
7304
|
return EXIT_ERROR8;
|
|
7290
7305
|
}
|
|
7291
|
-
const testRoot =
|
|
7306
|
+
const testRoot = path22.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
|
|
7292
7307
|
let files;
|
|
7293
7308
|
try {
|
|
7294
7309
|
files = parseUnifiedDiff(git2(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
|
|
@@ -7325,7 +7340,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7325
7340
|
testIdAttributes: index.testIdAttributes ?? void 0,
|
|
7326
7341
|
isTestFile: (file) => TEST_FILE.test(file),
|
|
7327
7342
|
translations: translationLookup(repoRoot, args.base, args.locale),
|
|
7328
|
-
readFile: (file, side) => side === "new" ? readText(
|
|
7343
|
+
readFile: (file, side) => side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
|
|
7329
7344
|
});
|
|
7330
7345
|
const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
|
|
7331
7346
|
const replacementsOf = /* @__PURE__ */ new Map();
|
|
@@ -7370,8 +7385,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7370
7385
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
7371
7386
|
else {
|
|
7372
7387
|
const rel = (file) => {
|
|
7373
|
-
const abs =
|
|
7374
|
-
const r =
|
|
7388
|
+
const abs = path22.isAbsolute(file) ? file : path22.resolve(testRoot, file);
|
|
7389
|
+
const r = path22.relative(cwd, abs).split(path22.sep).join("/");
|
|
7375
7390
|
return r.startsWith("..") ? file : r || file;
|
|
7376
7391
|
};
|
|
7377
7392
|
console.log(renderPreflight(result, rel));
|
|
@@ -7403,7 +7418,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7403
7418
|
// src/cli/probe.ts
|
|
7404
7419
|
var fs19 = __toESM(require("fs"));
|
|
7405
7420
|
var os3 = __toESM(require("os"));
|
|
7406
|
-
var
|
|
7421
|
+
var path23 = __toESM(require("path"));
|
|
7407
7422
|
var EXIT_OK8 = 0;
|
|
7408
7423
|
var EXIT_ERROR9 = 2;
|
|
7409
7424
|
var USAGE11 = `
|
|
@@ -7428,9 +7443,9 @@ function escapeRegex2(s) {
|
|
|
7428
7443
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7429
7444
|
}
|
|
7430
7445
|
async function runProbe(argv) {
|
|
7431
|
-
const
|
|
7432
|
-
const own =
|
|
7433
|
-
const playwrightArgs2 =
|
|
7446
|
+
const sep7 = argv.indexOf("--");
|
|
7447
|
+
const own = sep7 < 0 ? argv : argv.slice(0, sep7);
|
|
7448
|
+
const playwrightArgs2 = sep7 < 0 ? [] : argv.slice(sep7 + 1);
|
|
7434
7449
|
if (own.includes("-h") || own.includes("--help")) {
|
|
7435
7450
|
console.log(USAGE11);
|
|
7436
7451
|
return EXIT_OK8;
|
|
@@ -7467,10 +7482,10 @@ async function runProbe(argv) {
|
|
|
7467
7482
|
return EXIT_OK8;
|
|
7468
7483
|
}
|
|
7469
7484
|
console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
|
|
7470
|
-
const dir = fs19.mkdtempSync(
|
|
7471
|
-
const planFile =
|
|
7472
|
-
const resultsFile =
|
|
7473
|
-
const runOutputFile =
|
|
7485
|
+
const dir = fs19.mkdtempSync(path23.join(os3.tmpdir(), "piwi-probe-"));
|
|
7486
|
+
const planFile = path23.join(dir, "plan.json");
|
|
7487
|
+
const resultsFile = path23.join(dir, "results.jsonl");
|
|
7488
|
+
const runOutputFile = path23.join(dir, "run.json");
|
|
7474
7489
|
fs19.writeFileSync(planFile, JSON.stringify(plan));
|
|
7475
7490
|
fs19.writeFileSync(resultsFile, "");
|
|
7476
7491
|
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
|