@piwitests/reporter 0.55.0 → 0.56.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 +1339 -1369
- package/dist/global-setup-module.js +32 -22
- package/dist/index.d.ts +9 -0
- package/dist/index.js +460 -331
- 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;
|
|
674
|
-
try {
|
|
675
|
-
res = await fetch(url, {
|
|
676
|
-
headers: args.apiKey ? { "X-API-Key": args.apiKey } : {},
|
|
677
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
678
|
-
});
|
|
679
|
-
} catch (e) {
|
|
680
|
-
throw new Error(`could not reach ${args.serverUrl} (${e.message})`);
|
|
681
|
-
}
|
|
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
|
-
}
|
|
689
|
-
function repositoryRoot(cwd) {
|
|
523
|
+
function readTextOr(file, fallback) {
|
|
690
524
|
try {
|
|
691
|
-
|
|
692
|
-
cwd,
|
|
693
|
-
encoding: "utf-8",
|
|
694
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
695
|
-
}).trim();
|
|
696
|
-
return out ? path7.resolve(out) : null;
|
|
525
|
+
return fs2.readFileSync(file, "utf8");
|
|
697
526
|
} catch {
|
|
698
|
-
return
|
|
527
|
+
return fallback;
|
|
699
528
|
}
|
|
700
529
|
}
|
|
701
|
-
function
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
+
}
|
|
707
582
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return findings.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
|
|
718
595
|
}
|
|
719
|
-
function
|
|
720
|
-
|
|
596
|
+
function hasBlockingFindings(findings) {
|
|
597
|
+
return findings.some((f) => f.severity === "error");
|
|
598
|
+
}
|
|
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,634 @@ function resolvePlaywrightCli(cwd) {
|
|
|
726
613
|
}
|
|
727
614
|
return null;
|
|
728
615
|
}
|
|
729
|
-
function
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
env: childEnv
|
|
737
|
-
});
|
|
738
|
-
return new Promise((resolve10) => {
|
|
739
|
-
child.on("error", (err) => {
|
|
740
|
-
console.error(`piwi bug: could not start Playwright \u2014 ${err.message}`);
|
|
741
|
-
resolve10(EXIT_ERROR2);
|
|
742
|
-
});
|
|
743
|
-
child.on("exit", (code) => resolve10(code ?? EXIT_ERROR2));
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
async function runBug(argv, env = process.env, cwd = process.cwd()) {
|
|
747
|
-
if (argv.includes("-h") || argv.includes("--help")) {
|
|
748
|
-
console.log(USAGE2);
|
|
749
|
-
return EXIT_OK2;
|
|
750
|
-
}
|
|
751
|
-
let args;
|
|
752
|
-
try {
|
|
753
|
-
args = parseBugArgs(argv, env);
|
|
754
|
-
} catch (e) {
|
|
755
|
-
console.error(`piwi bug: ${e.message}
|
|
756
|
-
|
|
757
|
-
${USAGE2}`);
|
|
758
|
-
return EXIT_ERROR2;
|
|
759
|
-
}
|
|
760
|
-
const root = args.write ? repositoryRoot(cwd) : null;
|
|
761
|
-
const outFile = args.out !== null ? path7.resolve(cwd, args.out) : null;
|
|
762
|
-
let spec;
|
|
763
|
-
try {
|
|
764
|
-
spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path7.dirname(outFile)) : void 0);
|
|
765
|
-
} catch (e) {
|
|
766
|
-
console.error(`piwi bug: ${e.message}`);
|
|
767
|
-
return EXIT_ERROR2;
|
|
768
|
-
}
|
|
769
|
-
for (const warning of spec.warnings) {
|
|
770
|
-
console.error(`piwi bug: warning: step ${warning.step + 1}: ${warning.message}`);
|
|
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;
|
|
771
623
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
try {
|
|
781
|
-
fs4.mkdirSync(path7.dirname(file), { recursive: true });
|
|
782
|
-
fs4.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
|
|
783
|
-
} catch (e) {
|
|
784
|
-
const exists = e.code === "EEXIST";
|
|
785
|
-
console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
|
|
786
|
-
return EXIT_ERROR2;
|
|
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] };
|
|
787
632
|
}
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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) {
|
|
794
641
|
console.error(
|
|
795
|
-
|
|
642
|
+
`${label}: could not find Playwright in ${cwd} \u2014 install it there (npm install --save-dev @playwright/test).`
|
|
796
643
|
);
|
|
797
|
-
return
|
|
644
|
+
return Promise.resolve(COULD_NOT_START);
|
|
798
645
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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 });
|
|
650
|
+
return new Promise((resolve10) => {
|
|
651
|
+
const done = (code) => {
|
|
652
|
+
if (fd !== null) fs3.closeSync(fd);
|
|
653
|
+
fd = null;
|
|
654
|
+
resolve10(code);
|
|
655
|
+
};
|
|
656
|
+
child.on("error", (err) => {
|
|
657
|
+
console.error(`${label}: could not start Playwright \u2014 ${err.message}`);
|
|
658
|
+
done(COULD_NOT_START);
|
|
659
|
+
});
|
|
660
|
+
child.on("exit", (code) => done(code ?? COULD_NOT_START));
|
|
661
|
+
});
|
|
662
|
+
}
|
|
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;
|
|
803
666
|
}
|
|
804
667
|
|
|
805
|
-
// src/cli/
|
|
806
|
-
var
|
|
807
|
-
var
|
|
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]
|
|
808
674
|
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
]);
|
|
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.
|
|
826
681
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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;
|
|
834
706
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
return true;
|
|
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]);
|
|
841
712
|
}
|
|
713
|
+
return values;
|
|
842
714
|
}
|
|
843
|
-
function
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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;
|
|
724
|
+
try {
|
|
725
|
+
findings = checkAiTree(root, { dir });
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.error(`piwi ai check: ${error.message}`);
|
|
728
|
+
return EXIT_ERROR;
|
|
847
729
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
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).`);
|
|
856
740
|
}
|
|
857
|
-
return
|
|
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;
|
|
741
|
+
return hasBlockingFindings(findings) ? EXIT_ISSUES : EXIT_OK;
|
|
867
742
|
}
|
|
868
|
-
function
|
|
869
|
-
const args =
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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);
|
|
877
755
|
}
|
|
878
|
-
return args;
|
|
756
|
+
return { args, env: childEnv };
|
|
879
757
|
}
|
|
880
|
-
function
|
|
881
|
-
if (
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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);
|
|
762
|
+
}
|
|
763
|
+
const invocation = buildResolveInvocation(argv, env);
|
|
764
|
+
return spawnPlaywright(invocation.args, invocation.env, { command: "piwi ai resolve" });
|
|
885
765
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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;
|
|
771
|
+
}
|
|
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;
|
|
896
787
|
}
|
|
897
|
-
return { total, pairs };
|
|
898
788
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
789
|
+
|
|
790
|
+
// src/cli/bug.ts
|
|
791
|
+
var fs5 = __toESM(require("fs"));
|
|
792
|
+
var path8 = __toESM(require("path"));
|
|
793
|
+
var import_node_child_process2 = require("child_process");
|
|
794
|
+
|
|
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");
|
|
801
|
+
}
|
|
802
|
+
function readDesktopConfig(filePath2) {
|
|
803
|
+
try {
|
|
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 };
|
|
810
|
+
} catch {
|
|
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
|
-
|
|
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;
|
|
973
995
|
}
|
|
974
|
-
|
|
975
|
-
|
|
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`);
|
|
976
1000
|
}
|
|
977
|
-
|
|
978
|
-
|
|
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})`);
|
|
979
1027
|
}
|
|
980
|
-
|
|
981
|
-
|
|
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;
|
|
982
1045
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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);
|
|
987
1053
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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;
|
|
993
1072
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const calls = [];
|
|
1003
|
-
for (; ; ) {
|
|
1004
|
-
const before = this.i;
|
|
1005
|
-
if (calls.length > 0) this.i++;
|
|
1006
|
-
this.skipSpace();
|
|
1007
|
-
const start = this.i;
|
|
1008
|
-
try {
|
|
1009
|
-
calls.push(this.parseCall(calls.length === 0));
|
|
1010
|
-
} catch (error) {
|
|
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 };
|
|
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;
|
|
1024
1081
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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;
|
|
1028
1090
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const method = this.identifier();
|
|
1032
|
-
if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
|
|
1033
|
-
if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
|
|
1034
|
-
this.expect("(");
|
|
1035
|
-
const args = [];
|
|
1036
|
-
this.skipSpace();
|
|
1037
|
-
if (this.peek() === ")") {
|
|
1038
|
-
this.i++;
|
|
1039
|
-
return { method, args };
|
|
1040
|
-
}
|
|
1041
|
-
for (; ; ) {
|
|
1042
|
-
args.push(this.parseValue());
|
|
1043
|
-
this.skipSpace();
|
|
1044
|
-
if (this.peek() === ",") {
|
|
1045
|
-
this.i++;
|
|
1046
|
-
this.skipSpace();
|
|
1047
|
-
if (this.peek() === ")") {
|
|
1048
|
-
this.i++;
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
this.expect(")");
|
|
1054
|
-
break;
|
|
1055
|
-
}
|
|
1056
|
-
return { method, args };
|
|
1091
|
+
for (const warning of spec.warnings) {
|
|
1092
|
+
console.error(`piwi bug: warning: step ${warning.step + 1}: ${warning.message}`);
|
|
1057
1093
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
|
|
1062
|
-
if (ch === "/") return this.parseRegex();
|
|
1063
|
-
if (ch === "{") return this.parseObject();
|
|
1064
|
-
if (ch !== void 0 && /[-\d]/.test(ch)) {
|
|
1065
|
-
const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
|
|
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");
|
|
1094
|
+
if (!args.write) {
|
|
1095
|
+
process.stdout.write(spec.code);
|
|
1096
|
+
return EXIT_OK2;
|
|
1081
1097
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const quote4 = this.src[this.i];
|
|
1085
|
-
let out = "";
|
|
1086
|
-
this.i++;
|
|
1087
|
-
while (this.i < this.src.length) {
|
|
1088
|
-
const c = this.src[this.i];
|
|
1089
|
-
if (c === quote4) {
|
|
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");
|
|
1098
|
+
if (!outFile && !root) {
|
|
1099
|
+
console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
|
|
1117
1100
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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 };
|
|
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;
|
|
1137
1109
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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 };
|
|
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;
|
|
1160
1120
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
return new Parser(trimmed).parseTopLevel();
|
|
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
|
+
];
|
|
1142
|
+
var VALUE_MATCHERS = /* @__PURE__ */ new Set([
|
|
1143
|
+
"toHaveText",
|
|
1144
|
+
"toHaveValue",
|
|
1145
|
+
"toHaveAccessibleName",
|
|
1146
|
+
"toHaveURL"
|
|
1147
|
+
]);
|
|
1148
|
+
|
|
1149
|
+
// ../core/src/function-match.ts
|
|
1150
|
+
function paramArgKey(source) {
|
|
1151
|
+
return source.path ? `${source.param}.${source.path}` : source.param;
|
|
1152
|
+
}
|
|
1153
|
+
function globToRegExp(glob) {
|
|
1154
|
+
const escaped = glob.split("**").map((part) => part.replace(/[.?+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*");
|
|
1155
|
+
return new RegExp(`^${escaped}$`);
|
|
1156
|
+
}
|
|
1157
|
+
function urlMatches(pattern, url) {
|
|
1158
|
+
if (!pattern) return true;
|
|
1168
1159
|
try {
|
|
1169
|
-
return
|
|
1160
|
+
return globToRegExp(pattern).test(url);
|
|
1170
1161
|
} catch {
|
|
1171
|
-
return
|
|
1162
|
+
return true;
|
|
1172
1163
|
}
|
|
1173
1164
|
}
|
|
1174
|
-
function
|
|
1175
|
-
|
|
1176
|
-
|
|
1165
|
+
function scoreTargetMatch(pattern, candidate) {
|
|
1166
|
+
if (!candidate) return pattern.role || pattern.testId || pattern.name ? 0 : 0.3;
|
|
1167
|
+
if (pattern.testId) {
|
|
1168
|
+
return candidate.testId && candidate.testId.toLowerCase() === pattern.testId.toLowerCase() ? 1 : 0;
|
|
1169
|
+
}
|
|
1170
|
+
const roleMatches = !pattern.role || candidate.role != null && candidate.role.toLowerCase() === pattern.role.toLowerCase();
|
|
1171
|
+
if (!roleMatches) return 0;
|
|
1172
|
+
if (pattern.name) {
|
|
1173
|
+
const needle = pattern.name.toLowerCase();
|
|
1174
|
+
const haystack = `${candidate.accessibleName ?? ""} ${candidate.text ?? ""}`.toLowerCase().trim();
|
|
1175
|
+
const nameMatches2 = haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
1176
|
+
if (!nameMatches2) return pattern.role ? 0.35 : 0;
|
|
1177
|
+
return pattern.role ? 0.95 : 0.6;
|
|
1178
|
+
}
|
|
1179
|
+
return pattern.role ? 0.7 : 0.3;
|
|
1177
1180
|
}
|
|
1178
|
-
function
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1181
|
+
function actionCompatible(patternAction, recordedAction) {
|
|
1182
|
+
if (patternAction === recordedAction) return true;
|
|
1183
|
+
return patternAction === "click" && recordedAction === "press" || patternAction === "press" && recordedAction === "click";
|
|
1184
|
+
}
|
|
1185
|
+
function stepPairScore(pattern, recorded) {
|
|
1186
|
+
if (!actionCompatible(pattern.action, recorded.action)) return 0;
|
|
1187
|
+
const base = scoreTargetMatch(pattern.target, recorded.target);
|
|
1188
|
+
return pattern.action === recorded.action ? base : base * 0.6;
|
|
1189
|
+
}
|
|
1190
|
+
function extractArgs(entry, window, pairs) {
|
|
1191
|
+
const args = {};
|
|
1192
|
+
const byPatternIndex = new Map(pairs.map(([windowIdx, patternIdx]) => [patternIdx, windowIdx]));
|
|
1193
|
+
for (const source of entry.paramSources) {
|
|
1194
|
+
const windowIdx = byPatternIndex.get(source.stepIndex);
|
|
1195
|
+
if (windowIdx == null) continue;
|
|
1196
|
+
const step = window[windowIdx];
|
|
1197
|
+
const value = source.from === "value" ? step.value : source.from === "testId" ? step.target?.testId ?? null : step.target?.text ?? null;
|
|
1198
|
+
if (value != null) args[paramArgKey(source)] = value;
|
|
1191
1199
|
}
|
|
1200
|
+
return args;
|
|
1192
1201
|
}
|
|
1193
|
-
function
|
|
1194
|
-
|
|
1202
|
+
function sameElement(pattern, recorded) {
|
|
1203
|
+
if (pattern.testId || !pattern.name) return true;
|
|
1204
|
+
const haystack = `${recorded?.accessibleName ?? ""} ${recorded?.text ?? ""}`.toLowerCase().trim();
|
|
1205
|
+
const needle = pattern.name.toLowerCase();
|
|
1206
|
+
return haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
|
|
1195
1207
|
}
|
|
1196
|
-
function
|
|
1197
|
-
|
|
1208
|
+
function contiguousAlignment(steps, startIndex, pattern) {
|
|
1209
|
+
if (startIndex + pattern.length > steps.length) return null;
|
|
1210
|
+
const pairs = [];
|
|
1211
|
+
let total = 0;
|
|
1212
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
1213
|
+
const recorded = steps[startIndex + j];
|
|
1214
|
+
const score = stepPairScore(pattern[j], recorded);
|
|
1215
|
+
if (score <= 0 || !sameElement(pattern[j].target, recorded.target)) return null;
|
|
1216
|
+
total += score;
|
|
1217
|
+
pairs.push([j, j]);
|
|
1218
|
+
}
|
|
1219
|
+
return { total, pairs };
|
|
1198
1220
|
}
|
|
1199
|
-
function
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1221
|
+
function matchFunctionAt(steps, startIndex, catalog) {
|
|
1222
|
+
const anchor = steps[startIndex];
|
|
1223
|
+
if (!anchor) return null;
|
|
1224
|
+
let best = null;
|
|
1225
|
+
for (const entry of catalog) {
|
|
1226
|
+
if (entry.steps.length === 0) continue;
|
|
1227
|
+
if (!urlMatches(entry.urlPattern, anchor.pageUrl)) continue;
|
|
1228
|
+
const aligned = contiguousAlignment(steps, startIndex, entry.steps);
|
|
1229
|
+
if (!aligned) continue;
|
|
1230
|
+
const { total, pairs } = aligned;
|
|
1231
|
+
const window = steps.slice(startIndex, startIndex + entry.steps.length);
|
|
1232
|
+
const score = total / entry.steps.length;
|
|
1233
|
+
const match = {
|
|
1234
|
+
entry,
|
|
1235
|
+
score,
|
|
1236
|
+
matchedIndices: pairs.map(([windowIdx]) => windowIdx + startIndex),
|
|
1237
|
+
complete: true,
|
|
1238
|
+
args: extractArgs(entry, window, pairs)
|
|
1239
|
+
};
|
|
1240
|
+
if (!best || match.score > best.score || match.score === best.score && entry.steps.length > best.entry.steps.length)
|
|
1241
|
+
best = match;
|
|
1242
|
+
}
|
|
1243
|
+
return best;
|
|
1202
1244
|
}
|
|
1203
1245
|
|
|
1204
1246
|
// ../core/src/locator-stability.ts
|
|
@@ -1332,16 +1374,16 @@ function checkFilterOptions(arg, out) {
|
|
|
1332
1374
|
function splitTopLevel(text, separator) {
|
|
1333
1375
|
const parts = [];
|
|
1334
1376
|
let depth = 0;
|
|
1335
|
-
let
|
|
1377
|
+
let quote3 = null;
|
|
1336
1378
|
let start = 0;
|
|
1337
1379
|
for (let i = 0; i < text.length; i++) {
|
|
1338
1380
|
const c = text[i];
|
|
1339
|
-
if (
|
|
1381
|
+
if (quote3) {
|
|
1340
1382
|
if (c === "\\") i++;
|
|
1341
|
-
else if (c ===
|
|
1383
|
+
else if (c === quote3) quote3 = null;
|
|
1342
1384
|
continue;
|
|
1343
1385
|
}
|
|
1344
|
-
if (c === '"' || c === "'")
|
|
1386
|
+
if (c === '"' || c === "'") quote3 = c;
|
|
1345
1387
|
else if (c === "[" || c === "(") depth++;
|
|
1346
1388
|
else if (c === "]" || c === ")") depth = Math.max(0, depth - 1);
|
|
1347
1389
|
else if (depth === 0 && text.startsWith(separator, i)) {
|
|
@@ -1421,7 +1463,7 @@ function tokenizeComplex(selector) {
|
|
|
1421
1463
|
const combinators = [];
|
|
1422
1464
|
let current = "";
|
|
1423
1465
|
let depth = 0;
|
|
1424
|
-
let
|
|
1466
|
+
let quote3 = null;
|
|
1425
1467
|
let pending = null;
|
|
1426
1468
|
const flush = () => {
|
|
1427
1469
|
if (!current) return;
|
|
@@ -1433,14 +1475,14 @@ function tokenizeComplex(selector) {
|
|
|
1433
1475
|
};
|
|
1434
1476
|
for (let i = 0; i < selector.length; i++) {
|
|
1435
1477
|
const c = selector[i];
|
|
1436
|
-
if (
|
|
1478
|
+
if (quote3) {
|
|
1437
1479
|
current += c;
|
|
1438
1480
|
if (c === "\\" && i + 1 < selector.length) current += selector[++i];
|
|
1439
|
-
else if (c ===
|
|
1481
|
+
else if (c === quote3) quote3 = null;
|
|
1440
1482
|
continue;
|
|
1441
1483
|
}
|
|
1442
1484
|
if (c === '"' || c === "'") {
|
|
1443
|
-
|
|
1485
|
+
quote3 = c;
|
|
1444
1486
|
current += c;
|
|
1445
1487
|
} else if (c === "[" || c === "(") {
|
|
1446
1488
|
depth++;
|
|
@@ -1482,16 +1524,16 @@ function checkCompound(compound, out) {
|
|
|
1482
1524
|
};
|
|
1483
1525
|
const readBalanced = (open, close) => {
|
|
1484
1526
|
let depth = 0;
|
|
1485
|
-
let
|
|
1527
|
+
let quote3 = null;
|
|
1486
1528
|
const start = i + 1;
|
|
1487
1529
|
for (; i < compound.length; i++) {
|
|
1488
1530
|
const c = compound[i];
|
|
1489
|
-
if (
|
|
1531
|
+
if (quote3) {
|
|
1490
1532
|
if (c === "\\") i++;
|
|
1491
|
-
else if (c ===
|
|
1533
|
+
else if (c === quote3) quote3 = null;
|
|
1492
1534
|
continue;
|
|
1493
1535
|
}
|
|
1494
|
-
if (c === '"' || c === "'")
|
|
1536
|
+
if (c === '"' || c === "'") quote3 = c;
|
|
1495
1537
|
else if (c === open) depth++;
|
|
1496
1538
|
else if (c === close && --depth === 0) {
|
|
1497
1539
|
i++;
|
|
@@ -1631,17 +1673,7 @@ function pageKey(url) {
|
|
|
1631
1673
|
}
|
|
1632
1674
|
|
|
1633
1675
|
// ../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
|
-
}
|
|
1676
|
+
var quote = quoteJsString;
|
|
1645
1677
|
var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
|
|
1646
1678
|
function argIsSafe(arg) {
|
|
1647
1679
|
switch (arg.type) {
|
|
@@ -1768,7 +1800,7 @@ function locatorForStep(step, index, ctx) {
|
|
|
1768
1800
|
const chosen = chooseLocator(step.target, ctx.options);
|
|
1769
1801
|
if (!chosen) {
|
|
1770
1802
|
ctx.warnings.push({ step: index, code: "no-locator", message: "No locator was captured for this element." });
|
|
1771
|
-
return `${ctx.page}.locator(${
|
|
1803
|
+
return `${ctx.page}.locator(${quote("/* no locator captured */")})`;
|
|
1772
1804
|
}
|
|
1773
1805
|
if (chosen.level === "brittle") {
|
|
1774
1806
|
ctx.warnings.push({
|
|
@@ -1801,12 +1833,12 @@ function renderAssertStep(step, index, ctx) {
|
|
|
1801
1833
|
});
|
|
1802
1834
|
return [` // Step ${index + 1}: ${matcher} without an expected value.`];
|
|
1803
1835
|
}
|
|
1804
|
-
arg =
|
|
1836
|
+
arg = quote(matcher === "toHaveURL" ? urlForCode(assertion.expected, ctx) : assertion.expected);
|
|
1805
1837
|
}
|
|
1806
1838
|
const subject = matcher === "toHaveURL" ? ctx.page : locatorForStep(step, index, ctx);
|
|
1807
1839
|
let line = ` await expect(${subject})${not}.${matcher}(${arg});`;
|
|
1808
1840
|
if (assertion?.actual != null && assertion.actual !== assertion.expected) {
|
|
1809
|
-
line += ` // recorded: ${
|
|
1841
|
+
line += ` // recorded: ${quote(assertion.actual)}`;
|
|
1810
1842
|
}
|
|
1811
1843
|
lines.push(line);
|
|
1812
1844
|
return lines;
|
|
@@ -1816,10 +1848,10 @@ function fileNames(value) {
|
|
|
1816
1848
|
}
|
|
1817
1849
|
function renderRawStep(step, index, ctx) {
|
|
1818
1850
|
const { page } = ctx;
|
|
1819
|
-
if (step.action === "goto") return [` await ${page}.goto(${
|
|
1851
|
+
if (step.action === "goto") return [` await ${page}.goto(${quote(urlForCode(step.value ?? step.pageUrl, ctx))});`];
|
|
1820
1852
|
if (step.action === "assert" || step.action === "assertVisible") return renderAssertStep(step, index, ctx);
|
|
1821
1853
|
if (step.action === "press" && !step.target) {
|
|
1822
|
-
return [` await ${page}.keyboard.press(${
|
|
1854
|
+
return [` await ${page}.keyboard.press(${quote(step.value ?? "Enter")});`];
|
|
1823
1855
|
}
|
|
1824
1856
|
const loc = locatorForStep(step, index, ctx);
|
|
1825
1857
|
switch (step.action) {
|
|
@@ -1840,23 +1872,23 @@ function renderRawStep(step, index, ctx) {
|
|
|
1840
1872
|
detail: names.join(", "),
|
|
1841
1873
|
message: `A file was chosen here (${names.join(", ")}); the spec reads it from the directory Playwright runs in.`
|
|
1842
1874
|
});
|
|
1843
|
-
const files = names.length === 1 ?
|
|
1875
|
+
const files = names.length === 1 ? quote(names[0]) : `[${names.map(quote).join(", ")}]`;
|
|
1844
1876
|
return [` await ${loc}.setInputFiles(${files});`];
|
|
1845
1877
|
}
|
|
1846
1878
|
case "hover":
|
|
1847
1879
|
return [` await ${loc}.hover();`];
|
|
1848
1880
|
case "fill": {
|
|
1849
1881
|
if (readsEnv(step, ctx)) return [` await ${loc}.fill(process.env.${envVarFor(step, index, ctx)} ?? '');`];
|
|
1850
|
-
return [` await ${loc}.fill(${
|
|
1882
|
+
return [` await ${loc}.fill(${quote(step.value ?? "")});`];
|
|
1851
1883
|
}
|
|
1852
1884
|
case "check":
|
|
1853
1885
|
return [` await ${loc}.check();`];
|
|
1854
1886
|
case "uncheck":
|
|
1855
1887
|
return [` await ${loc}.uncheck();`];
|
|
1856
1888
|
case "selectOption":
|
|
1857
|
-
return [` await ${loc}.selectOption(${
|
|
1889
|
+
return [` await ${loc}.selectOption(${quote(step.value ?? "")});`];
|
|
1858
1890
|
case "press":
|
|
1859
|
-
return [` await ${loc}.press(${
|
|
1891
|
+
return [` await ${loc}.press(${quote(step.value ?? "Enter")});`];
|
|
1860
1892
|
}
|
|
1861
1893
|
}
|
|
1862
1894
|
var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
@@ -1939,7 +1971,7 @@ function objectArgExpr(param, match, envArgs) {
|
|
|
1939
1971
|
const env = envArgs.get(key);
|
|
1940
1972
|
const raw = match.args[key];
|
|
1941
1973
|
if (env == null && raw == null) return null;
|
|
1942
|
-
return `${IDENTIFIER_RE.test(field) ? field :
|
|
1974
|
+
return `${IDENTIFIER_RE.test(field) ? field : quote(field)}: ${env ?? quote(raw)}`;
|
|
1943
1975
|
}).filter((pair) => pair != null);
|
|
1944
1976
|
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
1945
1977
|
}
|
|
@@ -1967,7 +1999,7 @@ function argExpr(entry, match, envArgs) {
|
|
|
1967
1999
|
if (raw == null) return p.type === "number" ? "0" : p.type === "boolean" ? "false" : "''";
|
|
1968
2000
|
if (p.type === "number") return String(Number(raw) || 0);
|
|
1969
2001
|
if (p.type === "boolean") return String(raw === "true" || raw === "1");
|
|
1970
|
-
return
|
|
2002
|
+
return quote(raw);
|
|
1971
2003
|
}).join(", ");
|
|
1972
2004
|
}
|
|
1973
2005
|
function renderFunctionCall(match, page, envArgs) {
|
|
@@ -1987,11 +2019,11 @@ function renderImports(usedEntries, given) {
|
|
|
1987
2019
|
const moduleKey = `${entry.module}#${entry.importName}`;
|
|
1988
2020
|
if (!seenModules.has(moduleKey)) {
|
|
1989
2021
|
seenModules.add(moduleKey);
|
|
1990
|
-
lines.push(`import { ${entry.importName} } from ${
|
|
2022
|
+
lines.push(`import { ${entry.importName} } from ${quote(entry.module)};`);
|
|
1991
2023
|
}
|
|
1992
2024
|
} else if (!seenModules.has(entry.module + entry.name)) {
|
|
1993
2025
|
seenModules.add(entry.module + entry.name);
|
|
1994
|
-
lines.push(`import { ${entry.name} } from ${
|
|
2026
|
+
lines.push(`import { ${entry.name} } from ${quote(entry.module)};`);
|
|
1995
2027
|
}
|
|
1996
2028
|
}
|
|
1997
2029
|
return lines;
|
|
@@ -2011,12 +2043,12 @@ function renderDetails(options) {
|
|
|
2011
2043
|
const tags = (options.tags ?? []).map((t) => t.trim()).filter(Boolean);
|
|
2012
2044
|
const annotations = (options.annotations ?? []).filter((a) => a.type.trim());
|
|
2013
2045
|
const lines = [];
|
|
2014
|
-
if (tags.length > 0) lines.push(` tag: [${tags.map((t) =>
|
|
2046
|
+
if (tags.length > 0) lines.push(` tag: [${tags.map((t) => quote(t.startsWith("@") ? t : `@${t}`)).join(", ")}],`);
|
|
2015
2047
|
if (annotations.length > 0) {
|
|
2016
2048
|
lines.push(" annotation: [");
|
|
2017
2049
|
for (const a of annotations) {
|
|
2018
|
-
const description = a.description != null ? `, description: ${
|
|
2019
|
-
lines.push(` { type: ${
|
|
2050
|
+
const description = a.description != null ? `, description: ${quote(a.description)}` : "";
|
|
2051
|
+
lines.push(` { type: ${quote(a.type)}${description} },`);
|
|
2020
2052
|
}
|
|
2021
2053
|
lines.push(" ],");
|
|
2022
2054
|
}
|
|
@@ -2049,7 +2081,7 @@ function wrapInPageSteps(lines, starts, origin) {
|
|
|
2049
2081
|
const out = lines.slice(0, starts[0].line);
|
|
2050
2082
|
starts.forEach((start, n) => {
|
|
2051
2083
|
const end = starts[n + 1]?.line ?? lines.length;
|
|
2052
|
-
out.push(` await test.step(${
|
|
2084
|
+
out.push(` await test.step(${quote(pageStepTitle(start.url, origin))}, async () => {`);
|
|
2053
2085
|
out.push(...lines.slice(start.line, end).map((line) => ` ${line}`));
|
|
2054
2086
|
out.push(" });");
|
|
2055
2087
|
});
|
|
@@ -2135,7 +2167,7 @@ function renderSpec(session, options = {}) {
|
|
|
2135
2167
|
const start = viewportAt.get(0);
|
|
2136
2168
|
const opening = [
|
|
2137
2169
|
...start ? [viewportLine(start, page)] : [],
|
|
2138
|
-
...!sawGoto && session.startUrl ? [` await ${page}.goto(${
|
|
2170
|
+
...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
|
|
2139
2171
|
];
|
|
2140
2172
|
bodyLines.unshift(...opening);
|
|
2141
2173
|
const bodyOffset = opening.length;
|
|
@@ -2172,7 +2204,7 @@ function renderSpec(session, options = {}) {
|
|
|
2172
2204
|
}
|
|
2173
2205
|
const details = renderDetails(options);
|
|
2174
2206
|
const fixture = [.../* @__PURE__ */ new Set([page.split(".")[0], ...fixtureReceivers])].join(", ");
|
|
2175
|
-
const testOpening = details.length > 0 ? [`test(${
|
|
2207
|
+
const testOpening = details.length > 0 ? [`test(${quote(title)}, {`, ...details, `}, async ({ ${fixture} }) => {`] : [`test(${quote(title)}, async ({ ${fixture} }) => {`];
|
|
2176
2208
|
if (options.format === "test") {
|
|
2177
2209
|
const needs = needsLines("");
|
|
2178
2210
|
return result([...needs, ...testOpening, ...testBody, `});`, ``], needs.length + testOpening.length);
|
|
@@ -2181,7 +2213,7 @@ function renderSpec(session, options = {}) {
|
|
|
2181
2213
|
// Package name interpolated rather than adjoining the preceding keyword
|
|
2182
2214
|
// directly, so this generated-code line does not itself look like a real
|
|
2183
2215
|
// import to the boundary test in tests/boundary.test.ts.
|
|
2184
|
-
`import { test, expect } from ${
|
|
2216
|
+
`import { test, expect } from ${quote(options.testImport?.trim() || "@playwright/test")};`,
|
|
2185
2217
|
...importLines,
|
|
2186
2218
|
``,
|
|
2187
2219
|
...testOpening,
|
|
@@ -2285,43 +2317,43 @@ var Checker = class {
|
|
|
2285
2317
|
constructor() {
|
|
2286
2318
|
this.errors = [];
|
|
2287
2319
|
}
|
|
2288
|
-
fail(
|
|
2289
|
-
if (this.errors.length < 20) this.errors.push(`${
|
|
2320
|
+
fail(path24, message) {
|
|
2321
|
+
if (this.errors.length < 20) this.errors.push(`${path24}: ${message}`);
|
|
2290
2322
|
}
|
|
2291
|
-
text(v,
|
|
2323
|
+
text(v, path24, max, nullable) {
|
|
2292
2324
|
if (v == null) {
|
|
2293
|
-
if (!nullable) this.fail(
|
|
2325
|
+
if (!nullable) this.fail(path24, "is required");
|
|
2294
2326
|
return null;
|
|
2295
2327
|
}
|
|
2296
2328
|
if (typeof v !== "string") {
|
|
2297
|
-
this.fail(
|
|
2329
|
+
this.fail(path24, "must be a string");
|
|
2298
2330
|
return null;
|
|
2299
2331
|
}
|
|
2300
|
-
if (v.length > max) this.fail(
|
|
2332
|
+
if (v.length > max) this.fail(path24, `is longer than ${max} characters`);
|
|
2301
2333
|
return v;
|
|
2302
2334
|
}
|
|
2303
|
-
number(v,
|
|
2335
|
+
number(v, path24) {
|
|
2304
2336
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2305
|
-
this.fail(
|
|
2337
|
+
this.fail(path24, "must be a number");
|
|
2306
2338
|
return 0;
|
|
2307
2339
|
}
|
|
2308
2340
|
return v;
|
|
2309
2341
|
}
|
|
2310
2342
|
};
|
|
2311
|
-
function checkTarget(c, v,
|
|
2343
|
+
function checkTarget(c, v, path24) {
|
|
2312
2344
|
if (v == null) return null;
|
|
2313
2345
|
if (!isObject(v)) {
|
|
2314
|
-
c.fail(
|
|
2346
|
+
c.fail(path24, "must be an object");
|
|
2315
2347
|
return null;
|
|
2316
2348
|
}
|
|
2317
2349
|
const alternativesIn = v.alternatives ?? [];
|
|
2318
2350
|
const alternatives = [];
|
|
2319
|
-
if (!Array.isArray(alternativesIn)) c.fail(`${
|
|
2351
|
+
if (!Array.isArray(alternativesIn)) c.fail(`${path24}.alternatives`, "must be a list");
|
|
2320
2352
|
else if (alternativesIn.length > STEPS_LIMITS.alternatives)
|
|
2321
|
-
c.fail(`${
|
|
2353
|
+
c.fail(`${path24}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
|
|
2322
2354
|
else {
|
|
2323
2355
|
alternativesIn.forEach((alt, i) => {
|
|
2324
|
-
const at = `${
|
|
2356
|
+
const at = `${path24}.alternatives[${i}]`;
|
|
2325
2357
|
if (!isObject(alt)) return c.fail(at, "must be an object");
|
|
2326
2358
|
const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
|
|
2327
2359
|
const safe = locator ? safeLocator(locator) : null;
|
|
@@ -2335,31 +2367,31 @@ function checkTarget(c, v, path23) {
|
|
|
2335
2367
|
});
|
|
2336
2368
|
}
|
|
2337
2369
|
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, `${
|
|
2370
|
+
tagName: c.text(v.tagName, `${path24}.tagName`, 64, false) ?? "",
|
|
2371
|
+
role: c.text(v.role, `${path24}.role`, 64, true),
|
|
2372
|
+
accessibleName: c.text(v.accessibleName, `${path24}.accessibleName`, STEPS_LIMITS.textLength, true),
|
|
2373
|
+
testId: c.text(v.testId, `${path24}.testId`, STEPS_LIMITS.textLength, true),
|
|
2374
|
+
text: c.text(v.text, `${path24}.text`, STEPS_LIMITS.textLength, true),
|
|
2343
2375
|
alternatives
|
|
2344
2376
|
};
|
|
2345
2377
|
}
|
|
2346
|
-
function checkAssertion(c, v,
|
|
2378
|
+
function checkAssertion(c, v, path24) {
|
|
2347
2379
|
if (!isObject(v)) {
|
|
2348
|
-
c.fail(
|
|
2380
|
+
c.fail(path24, "is required on an assert step");
|
|
2349
2381
|
return void 0;
|
|
2350
2382
|
}
|
|
2351
2383
|
const matcher = v.matcher;
|
|
2352
2384
|
if (!ASSERTION_MATCHERS.includes(matcher)) {
|
|
2353
|
-
c.fail(`${
|
|
2385
|
+
c.fail(`${path24}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
|
|
2354
2386
|
return void 0;
|
|
2355
2387
|
}
|
|
2356
|
-
const expected = c.text(v.expected, `${
|
|
2388
|
+
const expected = c.text(v.expected, `${path24}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
|
|
2357
2389
|
return {
|
|
2358
2390
|
matcher,
|
|
2359
2391
|
expected: VALUE_MATCHERS.has(matcher) ? expected : null,
|
|
2360
|
-
actual: c.text(v.actual, `${
|
|
2392
|
+
actual: c.text(v.actual, `${path24}.actual`, STEPS_LIMITS.valueLength, true),
|
|
2361
2393
|
negated: v.negated === true,
|
|
2362
|
-
note: c.text(v.note, `${
|
|
2394
|
+
note: c.text(v.note, `${path24}.note`, STEPS_LIMITS.valueLength, true)
|
|
2363
2395
|
};
|
|
2364
2396
|
}
|
|
2365
2397
|
function checkViewports(c, v, stepCount) {
|
|
@@ -2400,40 +2432,40 @@ function checkViewports(c, v, stepCount) {
|
|
|
2400
2432
|
});
|
|
2401
2433
|
return out;
|
|
2402
2434
|
}
|
|
2403
|
-
function checkStep(c, v,
|
|
2435
|
+
function checkStep(c, v, path24) {
|
|
2404
2436
|
if (!isObject(v)) {
|
|
2405
|
-
c.fail(
|
|
2437
|
+
c.fail(path24, "must be an object");
|
|
2406
2438
|
return null;
|
|
2407
2439
|
}
|
|
2408
2440
|
const action = v.action;
|
|
2409
2441
|
if (!ACTIONS.has(action)) {
|
|
2410
|
-
c.fail(`${
|
|
2442
|
+
c.fail(`${path24}.action`, "is not a known action");
|
|
2411
2443
|
return null;
|
|
2412
2444
|
}
|
|
2413
2445
|
const step = {
|
|
2414
2446
|
action: action === "assertVisible" ? "assert" : action,
|
|
2415
|
-
target: checkTarget(c, v.target, `${
|
|
2416
|
-
value: c.text(v.value, `${
|
|
2447
|
+
target: checkTarget(c, v.target, `${path24}.target`),
|
|
2448
|
+
value: c.text(v.value, `${path24}.value`, STEPS_LIMITS.valueLength, true),
|
|
2417
2449
|
redacted: v.redacted === true,
|
|
2418
|
-
pageUrl: c.text(v.pageUrl, `${
|
|
2419
|
-
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${
|
|
2450
|
+
pageUrl: c.text(v.pageUrl, `${path24}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
|
|
2451
|
+
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path24}.timestamp`)
|
|
2420
2452
|
};
|
|
2421
2453
|
if (step.redacted) step.value = null;
|
|
2422
2454
|
if (action === "hover" || action === "dblclick") {
|
|
2423
2455
|
step.value = null;
|
|
2424
|
-
if (!step.target) c.fail(`${
|
|
2456
|
+
if (!step.target) c.fail(`${path24}.target`, `is required on a ${action} step`);
|
|
2425
2457
|
}
|
|
2426
|
-
if (action === "setInputFiles" && !step.target) c.fail(`${
|
|
2458
|
+
if (action === "setInputFiles" && !step.target) c.fail(`${path24}.target`, "is required on a setInputFiles step");
|
|
2427
2459
|
if (action === "dragTo") {
|
|
2428
2460
|
step.value = null;
|
|
2429
|
-
if (!step.target) c.fail(`${
|
|
2430
|
-
step.dropTarget = checkTarget(c, v.dropTarget, `${
|
|
2431
|
-
if (!step.dropTarget) c.fail(`${
|
|
2461
|
+
if (!step.target) c.fail(`${path24}.target`, "is required on a dragTo step");
|
|
2462
|
+
step.dropTarget = checkTarget(c, v.dropTarget, `${path24}.dropTarget`);
|
|
2463
|
+
if (!step.dropTarget) c.fail(`${path24}.dropTarget`, "is required on a dragTo step");
|
|
2432
2464
|
}
|
|
2433
2465
|
if (action === "assertVisible") {
|
|
2434
2466
|
step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
|
|
2435
2467
|
} else if (action === "assert") {
|
|
2436
|
-
step.assertion = checkAssertion(c, v.assertion, `${
|
|
2468
|
+
step.assertion = checkAssertion(c, v.assertion, `${path24}.assertion`);
|
|
2437
2469
|
}
|
|
2438
2470
|
return step;
|
|
2439
2471
|
}
|
|
@@ -2732,8 +2764,8 @@ ${USAGE3}`);
|
|
|
2732
2764
|
}
|
|
2733
2765
|
} else {
|
|
2734
2766
|
try {
|
|
2735
|
-
if (
|
|
2736
|
-
text =
|
|
2767
|
+
if (fs6.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
|
|
2768
|
+
text = fs6.readFileSync(args.file, "utf-8");
|
|
2737
2769
|
} catch (e) {
|
|
2738
2770
|
const reason = e.code === "ENOENT" ? "does not exist" : e.message;
|
|
2739
2771
|
console.error(`piwi codegen: ${args.file} ${reason}`);
|
|
@@ -2765,8 +2797,8 @@ ${USAGE3}`);
|
|
|
2765
2797
|
return EXIT_OK3;
|
|
2766
2798
|
}
|
|
2767
2799
|
try {
|
|
2768
|
-
|
|
2769
|
-
|
|
2800
|
+
fs6.mkdirSync(path9.dirname(args.out), { recursive: true });
|
|
2801
|
+
fs6.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
|
|
2770
2802
|
} catch (e) {
|
|
2771
2803
|
const exists = e.code === "EEXIST";
|
|
2772
2804
|
console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
|
|
@@ -2780,7 +2812,7 @@ ${USAGE3}`);
|
|
|
2780
2812
|
}
|
|
2781
2813
|
|
|
2782
2814
|
// src/cli/coverage.ts
|
|
2783
|
-
var
|
|
2815
|
+
var path14 = __toESM(require("path"));
|
|
2784
2816
|
|
|
2785
2817
|
// ../core/src/codeowners.ts
|
|
2786
2818
|
function patternParts(pattern) {
|
|
@@ -2818,10 +2850,10 @@ function pathPatternMatcher(pattern) {
|
|
|
2818
2850
|
const { segments, floating } = patternParts(pattern);
|
|
2819
2851
|
if (floating) {
|
|
2820
2852
|
const only = segments[0];
|
|
2821
|
-
return (
|
|
2853
|
+
return (path24) => path24.split("/").some((part) => segmentMatches(only, part));
|
|
2822
2854
|
}
|
|
2823
|
-
return (
|
|
2824
|
-
const parts =
|
|
2855
|
+
return (path24) => {
|
|
2856
|
+
const parts = path24.split("/");
|
|
2825
2857
|
let at = new Uint8Array(parts.length);
|
|
2826
2858
|
at[0] = 1;
|
|
2827
2859
|
for (let i = 0; i < segments.length; i++) {
|
|
@@ -2887,8 +2919,8 @@ function parseCoverageScope(raw) {
|
|
|
2887
2919
|
function compileCoverageScope(scope) {
|
|
2888
2920
|
const include = scope.include.map(pathPatternMatcher);
|
|
2889
2921
|
const exclude = scope.exclude.map(pathPatternMatcher);
|
|
2890
|
-
return (
|
|
2891
|
-
const file =
|
|
2922
|
+
return (path24) => {
|
|
2923
|
+
const file = path24.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
2892
2924
|
if (include.length > 0 && !include.some((matches) => matches(file))) return false;
|
|
2893
2925
|
return !exclude.some((matches) => matches(file));
|
|
2894
2926
|
};
|
|
@@ -2903,6 +2935,9 @@ function errorMessage(error) {
|
|
|
2903
2935
|
return String(error);
|
|
2904
2936
|
}
|
|
2905
2937
|
|
|
2938
|
+
// src/internal/support/test-paths.ts
|
|
2939
|
+
var path10 = __toESM(require("path"));
|
|
2940
|
+
|
|
2906
2941
|
// src/internal/collect/metadata-collector.ts
|
|
2907
2942
|
var DETACHED_HEAD = "HEAD";
|
|
2908
2943
|
function resolveScmBranch(env, gitBranch) {
|
|
@@ -2934,14 +2969,14 @@ function normalizeRef(ref) {
|
|
|
2934
2969
|
|
|
2935
2970
|
// src/internal/coverage-upload/git-inventory.ts
|
|
2936
2971
|
var import_node_child_process5 = require("child_process");
|
|
2937
|
-
var
|
|
2938
|
-
var
|
|
2972
|
+
var fs8 = __toESM(require("fs"));
|
|
2973
|
+
var path12 = __toESM(require("path"));
|
|
2939
2974
|
|
|
2940
2975
|
// src/internal/collect/blob-ids.ts
|
|
2941
2976
|
var import_node_child_process4 = require("child_process");
|
|
2942
2977
|
var crypto2 = __toESM(require("crypto"));
|
|
2943
|
-
var
|
|
2944
|
-
var
|
|
2978
|
+
var fs7 = __toESM(require("fs"));
|
|
2979
|
+
var path11 = __toESM(require("path"));
|
|
2945
2980
|
function gitHashObjects(root, paths) {
|
|
2946
2981
|
return new Promise((resolve10) => {
|
|
2947
2982
|
const child = (0, import_node_child_process4.execFile)(
|
|
@@ -3008,7 +3043,7 @@ async function readInventory(root, warn = () => {
|
|
|
3008
3043
|
const changed = [];
|
|
3009
3044
|
for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
|
|
3010
3045
|
if (!file || !inventory.has(file)) continue;
|
|
3011
|
-
if (
|
|
3046
|
+
if (fs8.existsSync(path12.join(root, file))) changed.push(file);
|
|
3012
3047
|
else inventory.delete(file);
|
|
3013
3048
|
}
|
|
3014
3049
|
const ids = await hashPaths(root, changed);
|
|
@@ -3027,8 +3062,8 @@ async function readInventory(root, warn = () => {
|
|
|
3027
3062
|
}
|
|
3028
3063
|
|
|
3029
3064
|
// src/internal/coverage-upload/report-files.ts
|
|
3030
|
-
var
|
|
3031
|
-
var
|
|
3065
|
+
var fs9 = __toESM(require("fs"));
|
|
3066
|
+
var path13 = __toESM(require("path"));
|
|
3032
3067
|
|
|
3033
3068
|
// ../core/src/coverage.ts
|
|
3034
3069
|
var MAX_LINE = 1e6;
|
|
@@ -3107,7 +3142,7 @@ var FileLines = class {
|
|
|
3107
3142
|
this.functions.set(key, { start, end: last, ran });
|
|
3108
3143
|
}
|
|
3109
3144
|
}
|
|
3110
|
-
toFile(
|
|
3145
|
+
toFile(path24) {
|
|
3111
3146
|
const executable = [];
|
|
3112
3147
|
const lines = [];
|
|
3113
3148
|
const push = (ranges, line) => {
|
|
@@ -3127,7 +3162,7 @@ var FileLines = class {
|
|
|
3127
3162
|
if (didRun) ran.push([start, end]);
|
|
3128
3163
|
}
|
|
3129
3164
|
return {
|
|
3130
|
-
path:
|
|
3165
|
+
path: path24,
|
|
3131
3166
|
executable,
|
|
3132
3167
|
lines,
|
|
3133
3168
|
functions: normalizeFunctionRanges(ran),
|
|
@@ -3139,16 +3174,16 @@ var ReportFiles = class {
|
|
|
3139
3174
|
constructor() {
|
|
3140
3175
|
this.byPath = /* @__PURE__ */ new Map();
|
|
3141
3176
|
}
|
|
3142
|
-
of(
|
|
3143
|
-
let file = this.byPath.get(
|
|
3177
|
+
of(path24) {
|
|
3178
|
+
let file = this.byPath.get(path24);
|
|
3144
3179
|
if (!file) {
|
|
3145
3180
|
file = new FileLines();
|
|
3146
|
-
this.byPath.set(
|
|
3181
|
+
this.byPath.set(path24, file);
|
|
3147
3182
|
}
|
|
3148
3183
|
return file;
|
|
3149
3184
|
}
|
|
3150
3185
|
toFiles() {
|
|
3151
|
-
return [...this.byPath].map(([
|
|
3186
|
+
return [...this.byPath].map(([path24, file]) => file.toFile(path24));
|
|
3152
3187
|
}
|
|
3153
3188
|
};
|
|
3154
3189
|
var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
@@ -3231,21 +3266,21 @@ var CoberturaReader = class {
|
|
|
3231
3266
|
if (head.length < 9 && ("<!--".startsWith(head) || "<![CDATA[".startsWith(head))) return -1;
|
|
3232
3267
|
if (head.startsWith("<!--")) return this.through(text, open, "-->", 4);
|
|
3233
3268
|
if (head.startsWith("<![CDATA[")) return this.through(text, open, "]]>", 9);
|
|
3234
|
-
let
|
|
3269
|
+
let quote3 = this.quote;
|
|
3235
3270
|
for (let at = open + Math.max(1, this.scanned); at < text.length; at++) {
|
|
3236
3271
|
const char = text.charCodeAt(at);
|
|
3237
|
-
if (
|
|
3238
|
-
if (char ===
|
|
3272
|
+
if (quote3 !== 0) {
|
|
3273
|
+
if (char === quote3) quote3 = 0;
|
|
3239
3274
|
} else if (char === GREATER_THAN) {
|
|
3240
3275
|
this.scanned = 0;
|
|
3241
3276
|
this.quote = 0;
|
|
3242
3277
|
return at;
|
|
3243
3278
|
} else if (char === DOUBLE_QUOTE || char === SINGLE_QUOTE) {
|
|
3244
|
-
|
|
3279
|
+
quote3 = char;
|
|
3245
3280
|
}
|
|
3246
3281
|
}
|
|
3247
3282
|
this.scanned = text.length - open;
|
|
3248
|
-
this.quote =
|
|
3283
|
+
this.quote = quote3;
|
|
3249
3284
|
return -1;
|
|
3250
3285
|
}
|
|
3251
3286
|
/** Where a comment or CDATA section starting at `open` ends: the last character of `end`; -1 if not yet read. */
|
|
@@ -3487,36 +3522,36 @@ function normalizeSlashPath(raw) {
|
|
|
3487
3522
|
}
|
|
3488
3523
|
return (drive || (absolute ? "/" : "")) + out.join("/");
|
|
3489
3524
|
}
|
|
3490
|
-
function isAbsolutePath(
|
|
3491
|
-
return
|
|
3525
|
+
function isAbsolutePath(path24) {
|
|
3526
|
+
return path24.startsWith("/") || /^[A-Za-z]:\//.test(path24);
|
|
3492
3527
|
}
|
|
3493
|
-
function relativeTo(
|
|
3528
|
+
function relativeTo(path24, root) {
|
|
3494
3529
|
const drive = /^[A-Za-z]:/.test(root);
|
|
3495
3530
|
const base = root.endsWith("/") ? root : `${root}/`;
|
|
3496
|
-
const head =
|
|
3531
|
+
const head = path24.slice(0, base.length);
|
|
3497
3532
|
const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
|
|
3498
3533
|
if (!inside) return null;
|
|
3499
|
-
const
|
|
3500
|
-
return
|
|
3534
|
+
const relative9 = path24.slice(base.length);
|
|
3535
|
+
return relative9 || null;
|
|
3501
3536
|
}
|
|
3502
3537
|
function reportPathCandidates(raw, { repoRoot, bases }) {
|
|
3503
3538
|
const slash = (value) => value.replace(/\\/g, "/");
|
|
3504
3539
|
const root = normalizeSlashPath(slash(repoRoot));
|
|
3505
|
-
const
|
|
3506
|
-
if (!root || !
|
|
3540
|
+
const path24 = slash(raw.trim());
|
|
3541
|
+
if (!root || !path24 || path24.includes("\0")) return [];
|
|
3507
3542
|
const out = [];
|
|
3508
3543
|
const add = (absolute) => {
|
|
3509
3544
|
const normalized = normalizeSlashPath(absolute);
|
|
3510
3545
|
if (!normalized) return;
|
|
3511
|
-
const
|
|
3512
|
-
if (
|
|
3546
|
+
const relative9 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
|
|
3547
|
+
if (relative9 && !out.includes(relative9)) out.push(relative9);
|
|
3513
3548
|
};
|
|
3514
|
-
if (isAbsolutePath(
|
|
3515
|
-
add(
|
|
3549
|
+
if (isAbsolutePath(path24)) {
|
|
3550
|
+
add(path24);
|
|
3516
3551
|
} else {
|
|
3517
3552
|
for (const base of bases) {
|
|
3518
3553
|
const dir = slash(base);
|
|
3519
|
-
if (isAbsolutePath(dir)) add(`${dir}/${
|
|
3554
|
+
if (isAbsolutePath(dir)) add(`${dir}/${path24}`);
|
|
3520
3555
|
}
|
|
3521
3556
|
}
|
|
3522
3557
|
return out;
|
|
@@ -3540,14 +3575,14 @@ function globSegmentRegExp(segment) {
|
|
|
3540
3575
|
function expandPattern(pattern, cwd) {
|
|
3541
3576
|
const normalized = pattern.replace(/\\/g, "/");
|
|
3542
3577
|
if (!/[*?]/.test(normalized)) {
|
|
3543
|
-
const file =
|
|
3544
|
-
return
|
|
3578
|
+
const file = path13.resolve(cwd, normalized);
|
|
3579
|
+
return fs9.existsSync(file) && fs9.statSync(file).isFile() ? [file] : [];
|
|
3545
3580
|
}
|
|
3546
|
-
const absolute =
|
|
3581
|
+
const absolute = path13.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
|
|
3547
3582
|
const segments = normalized.split("/");
|
|
3548
3583
|
const firstWild = segments.findIndex((s) => /[*?]/.test(s));
|
|
3549
3584
|
const baseParts = segments.slice(0, firstWild);
|
|
3550
|
-
const base = absolute ? baseParts.join("/") || "/" :
|
|
3585
|
+
const base = absolute ? baseParts.join("/") || "/" : path13.resolve(cwd, ...baseParts);
|
|
3551
3586
|
const rest = segments.slice(firstWild).filter((s) => s !== "");
|
|
3552
3587
|
const out = [];
|
|
3553
3588
|
const walk = (dir, at) => {
|
|
@@ -3555,14 +3590,14 @@ function expandPattern(pattern, cwd) {
|
|
|
3555
3590
|
const segment = rest[at];
|
|
3556
3591
|
let entries;
|
|
3557
3592
|
try {
|
|
3558
|
-
entries =
|
|
3593
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
3559
3594
|
} catch {
|
|
3560
3595
|
return;
|
|
3561
3596
|
}
|
|
3562
3597
|
if (segment === "**") {
|
|
3563
3598
|
walk(dir, at + 1);
|
|
3564
3599
|
for (const entry of entries) {
|
|
3565
|
-
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(
|
|
3600
|
+
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path13.join(dir, entry.name), at);
|
|
3566
3601
|
}
|
|
3567
3602
|
return;
|
|
3568
3603
|
}
|
|
@@ -3570,7 +3605,7 @@ function expandPattern(pattern, cwd) {
|
|
|
3570
3605
|
const last = at === rest.length - 1;
|
|
3571
3606
|
for (const entry of entries) {
|
|
3572
3607
|
if (!match.test(entry.name)) continue;
|
|
3573
|
-
const full =
|
|
3608
|
+
const full = path13.join(dir, entry.name);
|
|
3574
3609
|
if (last && entry.isFile()) out.push(full);
|
|
3575
3610
|
else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
|
|
3576
3611
|
walk(full, at + 1);
|
|
@@ -3583,7 +3618,7 @@ function expandPattern(pattern, cwd) {
|
|
|
3583
3618
|
async function readReport(file, format) {
|
|
3584
3619
|
let reader = format ? new CoverageReportReader(format) : null;
|
|
3585
3620
|
let head = "";
|
|
3586
|
-
for await (const chunk of
|
|
3621
|
+
for await (const chunk of fs9.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
|
|
3587
3622
|
if (!reader) {
|
|
3588
3623
|
head += chunk;
|
|
3589
3624
|
if (head.length < 4096) continue;
|
|
@@ -3605,18 +3640,18 @@ async function readReport(file, format) {
|
|
|
3605
3640
|
return reader.end();
|
|
3606
3641
|
}
|
|
3607
3642
|
function reportBases(report, reportFile, options) {
|
|
3608
|
-
if (options.root) return [
|
|
3609
|
-
const dir =
|
|
3643
|
+
if (options.root) return [path13.resolve(options.cwd, options.root)];
|
|
3644
|
+
const dir = path13.dirname(path13.resolve(reportFile));
|
|
3610
3645
|
const sources = report.sources.map(
|
|
3611
|
-
(source) =>
|
|
3646
|
+
(source) => path13.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path13.resolve(dir, source)
|
|
3612
3647
|
);
|
|
3613
|
-
return [...sources, options.cwd, dir,
|
|
3648
|
+
return [...sources, options.cwd, dir, path13.dirname(dir), options.repoRoot];
|
|
3614
3649
|
}
|
|
3615
3650
|
function resolveReport(report, reportFile, options, into) {
|
|
3616
3651
|
const bases = reportBases(report, reportFile, options);
|
|
3617
3652
|
for (const file of report.files) {
|
|
3618
3653
|
const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
|
|
3619
|
-
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) =>
|
|
3654
|
+
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs9.existsSync(path13.join(options.repoRoot, candidate)));
|
|
3620
3655
|
if (!match) {
|
|
3621
3656
|
into.unmatched.push(file.path);
|
|
3622
3657
|
continue;
|
|
@@ -3841,7 +3876,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
|
|
|
3841
3876
|
commit: checkout.commit,
|
|
3842
3877
|
branch,
|
|
3843
3878
|
suite: args.suite,
|
|
3844
|
-
reports: reports.map((report) =>
|
|
3879
|
+
reports: reports.map((report) => path14.relative(cwd, report) || report),
|
|
3845
3880
|
files: files.length,
|
|
3846
3881
|
unmatched: resolved.unmatched.length,
|
|
3847
3882
|
outOfScope: resolved.outOfScope,
|
|
@@ -3910,10 +3945,10 @@ ${USAGE4}`);
|
|
|
3910
3945
|
}
|
|
3911
3946
|
|
|
3912
3947
|
// src/cli/flake.ts
|
|
3913
|
-
var
|
|
3948
|
+
var import_node_child_process6 = require("child_process");
|
|
3914
3949
|
var fs11 = __toESM(require("fs"));
|
|
3915
3950
|
var os2 = __toESM(require("os"));
|
|
3916
|
-
var
|
|
3951
|
+
var path16 = __toESM(require("path"));
|
|
3917
3952
|
|
|
3918
3953
|
// ../core/src/error-parse.ts
|
|
3919
3954
|
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
@@ -3932,6 +3967,7 @@ var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
|
|
|
3932
3967
|
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
3933
3968
|
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
3969
|
var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
|
|
3970
|
+
var MAX_PARSED_ERROR_CHARS = 64 * 1024;
|
|
3935
3971
|
|
|
3936
3972
|
// ../core/src/flake-plan.ts
|
|
3937
3973
|
var FLAKE_PLAN_VERSION = 1;
|
|
@@ -4114,8 +4150,8 @@ function withinBudget(startedAt, now, budgetMs) {
|
|
|
4114
4150
|
}
|
|
4115
4151
|
|
|
4116
4152
|
// src/internal/support/connection.ts
|
|
4117
|
-
var
|
|
4118
|
-
var
|
|
4153
|
+
var fs10 = __toESM(require("fs"));
|
|
4154
|
+
var path15 = __toESM(require("path"));
|
|
4119
4155
|
|
|
4120
4156
|
// ../core/src/dotenv.ts
|
|
4121
4157
|
function parseDotEnv(text) {
|
|
@@ -4124,10 +4160,10 @@ function parseDotEnv(text) {
|
|
|
4124
4160
|
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
4125
4161
|
if (!m) continue;
|
|
4126
4162
|
let value = m[2];
|
|
4127
|
-
const
|
|
4128
|
-
if ((
|
|
4163
|
+
const quote3 = value[0];
|
|
4164
|
+
if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
|
|
4129
4165
|
value = value.slice(1, -1);
|
|
4130
|
-
if (
|
|
4166
|
+
if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
4131
4167
|
} else {
|
|
4132
4168
|
value = value.replace(/\s+#.*$/, "");
|
|
4133
4169
|
}
|
|
@@ -4156,7 +4192,7 @@ function resolveCliConnection(flags, env, dirs) {
|
|
|
4156
4192
|
let dotEnv = {};
|
|
4157
4193
|
for (const dir of dirs) {
|
|
4158
4194
|
try {
|
|
4159
|
-
dotEnv = { ...parseDotEnv(
|
|
4195
|
+
dotEnv = { ...parseDotEnv(fs10.readFileSync(path15.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
4160
4196
|
} catch {
|
|
4161
4197
|
}
|
|
4162
4198
|
}
|
|
@@ -4168,44 +4204,6 @@ function resolveCliConnection(flags, env, dirs) {
|
|
|
4168
4204
|
});
|
|
4169
4205
|
}
|
|
4170
4206
|
|
|
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
4207
|
// src/cli/flake.ts
|
|
4210
4208
|
var USAGE5 = `
|
|
4211
4209
|
piwi flake \u2014 make a flaky test fail on demand, then prove the fix
|
|
@@ -4351,7 +4349,7 @@ function parseFlakeArgs(argv) {
|
|
|
4351
4349
|
}
|
|
4352
4350
|
function gitHead() {
|
|
4353
4351
|
try {
|
|
4354
|
-
return (0,
|
|
4352
|
+
return (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4355
4353
|
} catch {
|
|
4356
4354
|
return null;
|
|
4357
4355
|
}
|
|
@@ -4395,7 +4393,7 @@ function playwrightRunner(dir, log) {
|
|
|
4395
4393
|
return {
|
|
4396
4394
|
now: () => Date.now(),
|
|
4397
4395
|
async runBatch(plan, arm, repeat, batch) {
|
|
4398
|
-
const planFile =
|
|
4396
|
+
const planFile = path16.join(dir, `${arm.id}.plan.json`);
|
|
4399
4397
|
if (!fs11.existsSync(planFile)) {
|
|
4400
4398
|
fs11.writeFileSync(
|
|
4401
4399
|
planFile,
|
|
@@ -4408,7 +4406,7 @@ function playwrightRunner(dir, log) {
|
|
|
4408
4406
|
})
|
|
4409
4407
|
);
|
|
4410
4408
|
}
|
|
4411
|
-
const resultsFile =
|
|
4409
|
+
const resultsFile = path16.join(dir, `${arm.id}.${batch}.jsonl`);
|
|
4412
4410
|
fs11.writeFileSync(resultsFile, "");
|
|
4413
4411
|
const env = {
|
|
4414
4412
|
...process.env,
|
|
@@ -4687,8 +4685,8 @@ async function runFlake(argv, runner) {
|
|
|
4687
4685
|
if (!args.json) {
|
|
4688
4686
|
printHeader(plan, commit, estimate, args.budgetMs);
|
|
4689
4687
|
}
|
|
4690
|
-
const dir = fs11.mkdtempSync(
|
|
4691
|
-
const log =
|
|
4688
|
+
const dir = fs11.mkdtempSync(path16.join(os2.tmpdir(), "piwi-flake-"));
|
|
4689
|
+
const log = path16.join(dir, "playwright.log");
|
|
4692
4690
|
let session;
|
|
4693
4691
|
try {
|
|
4694
4692
|
session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
|
|
@@ -4879,9 +4877,9 @@ function readCount(argv, name) {
|
|
|
4879
4877
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
4880
4878
|
return Math.floor(n);
|
|
4881
4879
|
}
|
|
4882
|
-
function readRunIdFromFile(
|
|
4880
|
+
function readRunIdFromFile(path24) {
|
|
4883
4881
|
try {
|
|
4884
|
-
const parsed = JSON.parse(fs12.readFileSync(
|
|
4882
|
+
const parsed = JSON.parse(fs12.readFileSync(path24, "utf-8"));
|
|
4885
4883
|
const runId = Number(parsed.runId);
|
|
4886
4884
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
4887
4885
|
} catch {
|
|
@@ -4973,8 +4971,7 @@ async function runGate(argv, env = process.env) {
|
|
|
4973
4971
|
|
|
4974
4972
|
// src/cli/init.ts
|
|
4975
4973
|
var fs15 = __toESM(require("fs"));
|
|
4976
|
-
var
|
|
4977
|
-
var import_node_child_process8 = require("child_process");
|
|
4974
|
+
var path19 = __toESM(require("path"));
|
|
4978
4975
|
|
|
4979
4976
|
// src/cli/report.ts
|
|
4980
4977
|
var STATUS_MARK = {
|
|
@@ -4992,7 +4989,7 @@ function formatStep(result) {
|
|
|
4992
4989
|
|
|
4993
4990
|
// src/cli/detect.ts
|
|
4994
4991
|
var fs13 = __toESM(require("fs"));
|
|
4995
|
-
var
|
|
4992
|
+
var path17 = __toESM(require("path"));
|
|
4996
4993
|
var CONFIG_NAMES = [
|
|
4997
4994
|
"playwright.config.ts",
|
|
4998
4995
|
"playwright.config.mts",
|
|
@@ -5010,7 +5007,7 @@ var LOCKFILES = [
|
|
|
5010
5007
|
];
|
|
5011
5008
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
5012
5009
|
function readPackageJson(root) {
|
|
5013
|
-
const pkgPath =
|
|
5010
|
+
const pkgPath = path17.join(root, "package.json");
|
|
5014
5011
|
try {
|
|
5015
5012
|
return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
5016
5013
|
} catch {
|
|
@@ -5021,13 +5018,13 @@ function detectPackageManager(root, pkg) {
|
|
|
5021
5018
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
5022
5019
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
5023
5020
|
for (const [file, manager] of LOCKFILES) {
|
|
5024
|
-
if (fs13.existsSync(
|
|
5021
|
+
if (fs13.existsSync(path17.join(root, file))) return manager;
|
|
5025
5022
|
}
|
|
5026
5023
|
return "npm";
|
|
5027
5024
|
}
|
|
5028
5025
|
function findConfig(root) {
|
|
5029
5026
|
for (const name of CONFIG_NAMES) {
|
|
5030
|
-
const candidate =
|
|
5027
|
+
const candidate = path17.join(root, name);
|
|
5031
5028
|
if (fs13.existsSync(candidate)) return candidate;
|
|
5032
5029
|
}
|
|
5033
5030
|
return null;
|
|
@@ -5038,14 +5035,14 @@ function langOf(configPath) {
|
|
|
5038
5035
|
}
|
|
5039
5036
|
function suggestProjectName(root, pkg) {
|
|
5040
5037
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
5041
|
-
return fromPkg ||
|
|
5038
|
+
return fromPkg || path17.basename(root) || "default-project";
|
|
5042
5039
|
}
|
|
5043
5040
|
function hasReporter(pkg) {
|
|
5044
5041
|
if (!pkg) return false;
|
|
5045
5042
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
5046
5043
|
}
|
|
5047
5044
|
function detectProject(root) {
|
|
5048
|
-
const absRoot =
|
|
5045
|
+
const absRoot = path17.resolve(root);
|
|
5049
5046
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
5050
5047
|
const configPath = findConfig(absRoot);
|
|
5051
5048
|
return {
|
|
@@ -5075,9 +5072,7 @@ function installCommand(manager) {
|
|
|
5075
5072
|
// src/cli/configure.ts
|
|
5076
5073
|
var REPORTER_IMPORT = "import { wrapConfig } from '@piwitests/reporter'";
|
|
5077
5074
|
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
|
-
}
|
|
5075
|
+
var quote2 = quoteJsString;
|
|
5081
5076
|
function matchClosingParen(src, openIndex) {
|
|
5082
5077
|
let depth = 0;
|
|
5083
5078
|
let quoteChar = null;
|
|
@@ -5113,8 +5108,8 @@ function matchClosingParen(src, openIndex) {
|
|
|
5113
5108
|
}
|
|
5114
5109
|
function optionsLiteral(opts) {
|
|
5115
5110
|
const lines = [];
|
|
5116
|
-
if (opts.serverUrl) lines.push(` serverUrl: ${
|
|
5117
|
-
lines.push(` projectName: ${
|
|
5111
|
+
if (opts.serverUrl) lines.push(` serverUrl: ${quote2(opts.serverUrl)},`);
|
|
5112
|
+
lines.push(` projectName: ${quote2(opts.projectName)},`);
|
|
5118
5113
|
return `{
|
|
5119
5114
|
${lines.join("\n")}
|
|
5120
5115
|
}`;
|
|
@@ -5188,7 +5183,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
5188
5183
|
|
|
5189
5184
|
// src/cli/skills.ts
|
|
5190
5185
|
var fs14 = __toESM(require("fs"));
|
|
5191
|
-
var
|
|
5186
|
+
var path18 = __toESM(require("path"));
|
|
5192
5187
|
|
|
5193
5188
|
// ../core/src/skill-stamp.ts
|
|
5194
5189
|
var SKILL_VERSION_KEY = "piwi-version";
|
|
@@ -5251,16 +5246,16 @@ var WORKFLOW_SKILLS = [
|
|
|
5251
5246
|
"fix-a-reported-bug"
|
|
5252
5247
|
];
|
|
5253
5248
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
5254
|
-
var DEFAULT_SKILLS_DIR =
|
|
5249
|
+
var DEFAULT_SKILLS_DIR = path18.join(".claude", "skills");
|
|
5255
5250
|
function findTemplatesDir(fromDir) {
|
|
5256
5251
|
let dir = fromDir;
|
|
5257
5252
|
for (let i = 0; i < 6; i++) {
|
|
5258
|
-
if (fs14.existsSync(
|
|
5259
|
-
const parent =
|
|
5253
|
+
if (fs14.existsSync(path18.join(dir, "templates", "skills"))) return path18.join(dir, "templates");
|
|
5254
|
+
const parent = path18.dirname(dir);
|
|
5260
5255
|
if (parent === dir) break;
|
|
5261
5256
|
dir = parent;
|
|
5262
5257
|
}
|
|
5263
|
-
return
|
|
5258
|
+
return path18.resolve(fromDir, "..", "..", "templates");
|
|
5264
5259
|
}
|
|
5265
5260
|
function readFrontMatter(markdown) {
|
|
5266
5261
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -5274,14 +5269,14 @@ function readFrontMatter(markdown) {
|
|
|
5274
5269
|
}
|
|
5275
5270
|
function readTemplatesVersion(templatesDir) {
|
|
5276
5271
|
try {
|
|
5277
|
-
const pkg = JSON.parse(fs14.readFileSync(
|
|
5272
|
+
const pkg = JSON.parse(fs14.readFileSync(path18.join(templatesDir, "..", "package.json"), "utf-8"));
|
|
5278
5273
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
5279
5274
|
} catch {
|
|
5280
5275
|
return "0.0.0";
|
|
5281
5276
|
}
|
|
5282
5277
|
}
|
|
5283
5278
|
function templatePath(templatesDir, slug) {
|
|
5284
|
-
return
|
|
5279
|
+
return path18.join(templatesDir, "skills", slug, "SKILL.md");
|
|
5285
5280
|
}
|
|
5286
5281
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
5287
5282
|
const infos = [];
|
|
@@ -5298,8 +5293,8 @@ function installSkills(opts) {
|
|
|
5298
5293
|
for (const slug of opts.slugs) {
|
|
5299
5294
|
const step = `skill:${slug}`;
|
|
5300
5295
|
const source = templatePath(opts.templatesDir, slug);
|
|
5301
|
-
const relDest =
|
|
5302
|
-
const dest =
|
|
5296
|
+
const relDest = path18.join(opts.skillsDir, slug, "SKILL.md");
|
|
5297
|
+
const dest = path18.join(opts.root, relDest);
|
|
5303
5298
|
if (!fs14.existsSync(source)) {
|
|
5304
5299
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
5305
5300
|
continue;
|
|
@@ -5331,7 +5326,7 @@ function installSkills(opts) {
|
|
|
5331
5326
|
}
|
|
5332
5327
|
}
|
|
5333
5328
|
if (!opts.dryRun) {
|
|
5334
|
-
fs14.mkdirSync(
|
|
5329
|
+
fs14.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
5335
5330
|
fs14.writeFileSync(dest, contents);
|
|
5336
5331
|
}
|
|
5337
5332
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
|
|
@@ -5404,7 +5399,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
5404
5399
|
}
|
|
5405
5400
|
const results = installSkills({
|
|
5406
5401
|
templatesDir,
|
|
5407
|
-
root:
|
|
5402
|
+
root: path18.resolve(readOption5(rest, "--cwd") ?? cwd),
|
|
5408
5403
|
skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
5409
5404
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
5410
5405
|
force: rest.includes("--force"),
|
|
@@ -5475,7 +5470,7 @@ function resolveSkillSlugs(raw) {
|
|
|
5475
5470
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
5476
5471
|
}
|
|
5477
5472
|
function parseInitArgs(argv, env, cwd) {
|
|
5478
|
-
const root =
|
|
5473
|
+
const root = path19.resolve(readOption6(argv, "--cwd") ?? cwd);
|
|
5479
5474
|
const detected = detectProject(root);
|
|
5480
5475
|
return {
|
|
5481
5476
|
root,
|
|
@@ -5524,12 +5519,12 @@ function stepInstallReporter(project, opts) {
|
|
|
5524
5519
|
if (opts.dryRun || !opts.install)
|
|
5525
5520
|
return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
|
|
5526
5521
|
const [command, args] = installArgv(project.packageManager);
|
|
5527
|
-
const
|
|
5528
|
-
if (
|
|
5522
|
+
const status = runPackageManager(command, args, opts.root);
|
|
5523
|
+
if (status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
|
|
5529
5524
|
return {
|
|
5530
5525
|
step,
|
|
5531
5526
|
status: "error",
|
|
5532
|
-
detail: `\`${installCommand(project.packageManager)}\` failed (exit ${
|
|
5527
|
+
detail: `\`${installCommand(project.packageManager)}\` failed (exit ${status ?? "unknown"}) \u2014 install it by hand`
|
|
5533
5528
|
};
|
|
5534
5529
|
}
|
|
5535
5530
|
function stepConfig(project, opts) {
|
|
@@ -5540,7 +5535,7 @@ function stepConfig(project, opts) {
|
|
|
5540
5535
|
status: "manual",
|
|
5541
5536
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
5542
5537
|
};
|
|
5543
|
-
const rel =
|
|
5538
|
+
const rel = path19.relative(project.root, project.configPath) || path19.basename(project.configPath);
|
|
5544
5539
|
const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
|
|
5545
5540
|
serverUrl: opts.serverUrl,
|
|
5546
5541
|
projectName: opts.projectName
|
|
@@ -5550,8 +5545,8 @@ function stepConfig(project, opts) {
|
|
|
5550
5545
|
}
|
|
5551
5546
|
function stepFixtures(project, opts) {
|
|
5552
5547
|
const step = "fixtures";
|
|
5553
|
-
const rel =
|
|
5554
|
-
const dest =
|
|
5548
|
+
const rel = path19.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
5549
|
+
const dest = path19.join(project.root, rel);
|
|
5555
5550
|
if (fs15.existsSync(dest)) {
|
|
5556
5551
|
const current = readFileOr(dest, "");
|
|
5557
5552
|
if (/piwiFixtures/.test(current))
|
|
@@ -5564,7 +5559,7 @@ function stepFixtures(project, opts) {
|
|
|
5564
5559
|
};
|
|
5565
5560
|
}
|
|
5566
5561
|
if (!opts.dryRun) {
|
|
5567
|
-
fs15.mkdirSync(
|
|
5562
|
+
fs15.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
5568
5563
|
fs15.writeFileSync(dest, fixturesContents());
|
|
5569
5564
|
}
|
|
5570
5565
|
return {
|
|
@@ -5576,7 +5571,7 @@ function stepFixtures(project, opts) {
|
|
|
5576
5571
|
}
|
|
5577
5572
|
function stepEnv(project, opts) {
|
|
5578
5573
|
const results = [];
|
|
5579
|
-
const examplePath =
|
|
5574
|
+
const examplePath = path19.join(project.root, ".env.example");
|
|
5580
5575
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
5581
5576
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
5582
5577
|
["PIWI_API_KEY", ""]
|
|
@@ -5589,7 +5584,7 @@ function stepEnv(project, opts) {
|
|
|
5589
5584
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
5590
5585
|
});
|
|
5591
5586
|
if (opts.apiKey) {
|
|
5592
|
-
const envPath =
|
|
5587
|
+
const envPath = path19.join(project.root, ".env");
|
|
5593
5588
|
const env = upsertEnvKeys(readFileOr(envPath, ""), [
|
|
5594
5589
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
5595
5590
|
["PIWI_API_KEY", opts.apiKey]
|
|
@@ -5601,7 +5596,7 @@ function stepEnv(project, opts) {
|
|
|
5601
5596
|
status: env.added.length ? "updated" : "already",
|
|
5602
5597
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
5603
5598
|
});
|
|
5604
|
-
const gitignorePath =
|
|
5599
|
+
const gitignorePath = path19.join(project.root, ".gitignore");
|
|
5605
5600
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
5606
5601
|
if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
|
|
5607
5602
|
results.push({
|
|
@@ -5665,7 +5660,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
5665
5660
|
project: {
|
|
5666
5661
|
root: project.root,
|
|
5667
5662
|
packageManager: project.packageManager,
|
|
5668
|
-
configPath: project.configPath ?
|
|
5663
|
+
configPath: project.configPath ? path19.relative(project.root, project.configPath) : null,
|
|
5669
5664
|
projectName: opts.projectName,
|
|
5670
5665
|
serverUrl: opts.serverUrl
|
|
5671
5666
|
},
|
|
@@ -5690,14 +5685,13 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
5690
5685
|
|
|
5691
5686
|
// src/cli/select.ts
|
|
5692
5687
|
var fs17 = __toESM(require("fs"));
|
|
5693
|
-
var
|
|
5694
|
-
var
|
|
5695
|
-
var import_node_module4 = require("module");
|
|
5688
|
+
var path21 = __toESM(require("path"));
|
|
5689
|
+
var import_node_child_process7 = require("child_process");
|
|
5696
5690
|
|
|
5697
5691
|
// src/cli/add-reporter.ts
|
|
5698
5692
|
var fs16 = __toESM(require("fs"));
|
|
5699
|
-
var
|
|
5700
|
-
var
|
|
5693
|
+
var path20 = __toESM(require("path"));
|
|
5694
|
+
var import_node_module2 = require("module");
|
|
5701
5695
|
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
5702
5696
|
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
5703
5697
|
var ADD_REPORTER_MIN = { major: 1, minor: 63 };
|
|
@@ -5729,9 +5723,9 @@ function resolveConfigPath(cwd, playwrightArgs2) {
|
|
|
5729
5723
|
const arg = playwrightArgs2[i];
|
|
5730
5724
|
if (arg === "--config" || arg === "-c") {
|
|
5731
5725
|
const next = playwrightArgs2[i + 1];
|
|
5732
|
-
if (next) return
|
|
5726
|
+
if (next) return path20.resolve(cwd, next);
|
|
5733
5727
|
} else if (arg.startsWith("--config=")) {
|
|
5734
|
-
return
|
|
5728
|
+
return path20.resolve(cwd, arg.slice("--config=".length));
|
|
5735
5729
|
}
|
|
5736
5730
|
}
|
|
5737
5731
|
return detectProject(cwd).configPath;
|
|
@@ -5746,7 +5740,7 @@ function readConfigSource(configPath) {
|
|
|
5746
5740
|
}
|
|
5747
5741
|
function readInstalledPlaywrightVersion(cwd) {
|
|
5748
5742
|
try {
|
|
5749
|
-
const require2 = (0,
|
|
5743
|
+
const require2 = (0, import_node_module2.createRequire)(path20.join(cwd, "noop.js"));
|
|
5750
5744
|
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
5751
5745
|
try {
|
|
5752
5746
|
return require2(id).version ?? null;
|
|
@@ -5866,7 +5860,7 @@ function parseSelectArgs(argv, env) {
|
|
|
5866
5860
|
extra
|
|
5867
5861
|
};
|
|
5868
5862
|
}
|
|
5869
|
-
var CACHE_FILE =
|
|
5863
|
+
var CACHE_FILE = path21.join(".piwi", "selection-cache.json");
|
|
5870
5864
|
function cacheKey(args) {
|
|
5871
5865
|
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
5872
5866
|
}
|
|
@@ -5886,7 +5880,7 @@ function writeCache(args, resolution) {
|
|
|
5886
5880
|
} catch {
|
|
5887
5881
|
}
|
|
5888
5882
|
store[cacheKey(args)] = resolution;
|
|
5889
|
-
fs17.mkdirSync(
|
|
5883
|
+
fs17.mkdirSync(path21.dirname(CACHE_FILE), { recursive: true });
|
|
5890
5884
|
fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
|
|
5891
5885
|
} catch {
|
|
5892
5886
|
}
|
|
@@ -5911,7 +5905,7 @@ function printWarnings(resolution) {
|
|
|
5911
5905
|
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
5912
5906
|
}
|
|
5913
5907
|
function gitChangedFiles(base) {
|
|
5914
|
-
const out = (0,
|
|
5908
|
+
const out = (0, import_node_child_process7.execFileSync)("git", ["diff", "--name-only", base], {
|
|
5915
5909
|
encoding: "utf-8",
|
|
5916
5910
|
stdio: ["ignore", "pipe", "ignore"]
|
|
5917
5911
|
});
|
|
@@ -5983,34 +5977,10 @@ async function runSelect(argv, env = process.env) {
|
|
|
5983
5977
|
else console.log(resolution.materialization.args.join(" "));
|
|
5984
5978
|
return EXIT_OK6;
|
|
5985
5979
|
}
|
|
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
5980
|
function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
|
|
6011
5981
|
const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
|
|
6012
5982
|
if (decision.log) console.error(decision.log);
|
|
6013
|
-
return
|
|
5983
|
+
return spawnPlaywright([...decision.args, ...playwrightArgs2], env, { runner: pkgRunner, command: "piwi run" });
|
|
6014
5984
|
}
|
|
6015
5985
|
function shardEnv(args) {
|
|
6016
5986
|
return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
|
|
@@ -6118,15 +6088,15 @@ async function runRun(argv, env = process.env) {
|
|
|
6118
6088
|
|
|
6119
6089
|
// src/cli/preflight.ts
|
|
6120
6090
|
var fs18 = __toESM(require("fs"));
|
|
6121
|
-
var
|
|
6122
|
-
var
|
|
6091
|
+
var path22 = __toESM(require("path"));
|
|
6092
|
+
var import_node_child_process8 = require("child_process");
|
|
6123
6093
|
|
|
6124
6094
|
// ../core/src/diff-anchors.ts
|
|
6125
6095
|
var RENAME_FROM = `rename from `;
|
|
6126
6096
|
var RENAME_TO = "rename to ";
|
|
6127
6097
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
6128
|
-
function stripPrefix(
|
|
6129
|
-
const unquoted =
|
|
6098
|
+
function stripPrefix(path24) {
|
|
6099
|
+
const unquoted = path24.startsWith('"') && path24.endsWith('"') ? path24.slice(1, -1) : path24;
|
|
6130
6100
|
return unquoted.replace(/^[ab]\//, "");
|
|
6131
6101
|
}
|
|
6132
6102
|
function parseUnifiedDiff(text) {
|
|
@@ -6236,8 +6206,8 @@ function pathGlobToRegExp(glob) {
|
|
|
6236
6206
|
});
|
|
6237
6207
|
return new RegExp(`^(?:${parts.join("|")})$`);
|
|
6238
6208
|
}
|
|
6239
|
-
function isTranslationFile(
|
|
6240
|
-
const posix =
|
|
6209
|
+
function isTranslationFile(path24, globs = DEFAULT_TRANSLATION_GLOBS) {
|
|
6210
|
+
const posix = path24.replace(/\\/g, "/");
|
|
6241
6211
|
if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
|
|
6242
6212
|
return globs.some((g) => pathGlobToRegExp(g).test(posix));
|
|
6243
6213
|
}
|
|
@@ -6248,7 +6218,7 @@ function keepable(value) {
|
|
|
6248
6218
|
return new RegExp("\\p{L}", "u").test(v);
|
|
6249
6219
|
}
|
|
6250
6220
|
function readQuoted(text, start) {
|
|
6251
|
-
const
|
|
6221
|
+
const quote3 = text[start];
|
|
6252
6222
|
let value = "";
|
|
6253
6223
|
for (let i = start + 1; i < text.length; i++) {
|
|
6254
6224
|
const ch = text[i];
|
|
@@ -6258,8 +6228,8 @@ function readQuoted(text, start) {
|
|
|
6258
6228
|
i++;
|
|
6259
6229
|
continue;
|
|
6260
6230
|
}
|
|
6261
|
-
if (
|
|
6262
|
-
if (ch ===
|
|
6231
|
+
if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
|
|
6232
|
+
if (ch === quote3) return { value, end: i + 1 };
|
|
6263
6233
|
value += ch;
|
|
6264
6234
|
}
|
|
6265
6235
|
return null;
|
|
@@ -6377,13 +6347,13 @@ function unquoteYaml(raw) {
|
|
|
6377
6347
|
function decodeXml2(text) {
|
|
6378
6348
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
6379
6349
|
}
|
|
6380
|
-
function parseTranslationFile(
|
|
6350
|
+
function parseTranslationFile(path24, content) {
|
|
6381
6351
|
const lines = content.split(/\r?\n/);
|
|
6382
6352
|
const out = [];
|
|
6383
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
6353
|
+
const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
|
|
6384
6354
|
if (ext === "json") {
|
|
6385
6355
|
const stack = [];
|
|
6386
|
-
const
|
|
6356
|
+
const path25 = (key) => [...stack.map((f) => f.key), key].join(".");
|
|
6387
6357
|
lines.forEach((text, i) => {
|
|
6388
6358
|
const t = text.trim();
|
|
6389
6359
|
const top = stack[stack.length - 1];
|
|
@@ -6392,14 +6362,14 @@ function parseTranslationFile(path23, content) {
|
|
|
6392
6362
|
const key = JSON.parse(`"${pair[1]}"`);
|
|
6393
6363
|
const rest = pair[2];
|
|
6394
6364
|
const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
|
|
6395
|
-
if (str) out.push({ key:
|
|
6365
|
+
if (str) out.push({ key: path25(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6396
6366
|
else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
|
|
6397
6367
|
return;
|
|
6398
6368
|
}
|
|
6399
6369
|
if (top?.array) {
|
|
6400
6370
|
const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
|
|
6401
6371
|
if (str) {
|
|
6402
|
-
out.push({ key:
|
|
6372
|
+
out.push({ key: path25(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
6403
6373
|
return;
|
|
6404
6374
|
}
|
|
6405
6375
|
if (/^[[{]\s*$/.test(t)) {
|
|
@@ -6463,8 +6433,8 @@ function parseTranslationFile(path23, content) {
|
|
|
6463
6433
|
}
|
|
6464
6434
|
return out;
|
|
6465
6435
|
}
|
|
6466
|
-
function translationLineToken(
|
|
6467
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
6436
|
+
function translationLineToken(path24, text, line) {
|
|
6437
|
+
const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
|
|
6468
6438
|
let key;
|
|
6469
6439
|
let value;
|
|
6470
6440
|
if (ext === "json") {
|
|
@@ -6501,9 +6471,9 @@ function translationLineToken(path23, text, line) {
|
|
|
6501
6471
|
if (value === void 0 || !keepable(value)) return null;
|
|
6502
6472
|
return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
|
|
6503
6473
|
}
|
|
6504
|
-
function lineTokens(
|
|
6474
|
+
function lineTokens(path24, text, line, side, attributes, options) {
|
|
6505
6475
|
const spans = [];
|
|
6506
|
-
const markup = MARKUP_FILE.test(
|
|
6476
|
+
const markup = MARKUP_FILE.test(path24);
|
|
6507
6477
|
return [
|
|
6508
6478
|
...attributeTokens(text, attributes, line, spans),
|
|
6509
6479
|
...translationRefTokens(text, line, side, options.translations, spans),
|
|
@@ -6511,10 +6481,10 @@ function lineTokens(path23, text, line, side, attributes, options) {
|
|
|
6511
6481
|
...textTokens(text, line, markup)
|
|
6512
6482
|
];
|
|
6513
6483
|
}
|
|
6514
|
-
function keysByLine(
|
|
6515
|
-
const content = options.readFile?.(
|
|
6484
|
+
function keysByLine(path24, side, options) {
|
|
6485
|
+
const content = options.readFile?.(path24, side);
|
|
6516
6486
|
if (content === void 0) return null;
|
|
6517
|
-
return new Map(parseTranslationFile(
|
|
6487
|
+
return new Map(parseTranslationFile(path24, content).map((e) => [e.line, e.key]));
|
|
6518
6488
|
}
|
|
6519
6489
|
function tokenId(t) {
|
|
6520
6490
|
return `${t.group}\0${t.value}`;
|
|
@@ -6712,9 +6682,9 @@ function cssSlots(selector, testIds, replace) {
|
|
|
6712
6682
|
const attr = m[1];
|
|
6713
6683
|
const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
|
|
6714
6684
|
if (!names) continue;
|
|
6715
|
-
const
|
|
6685
|
+
const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
|
|
6716
6686
|
const value = m[2] ?? m[3] ?? m[4];
|
|
6717
|
-
slot(value, [attr], (v) => `[${attr}=${
|
|
6687
|
+
slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
|
|
6718
6688
|
}
|
|
6719
6689
|
return slots;
|
|
6720
6690
|
}
|
|
@@ -6814,7 +6784,7 @@ function slotMatches(slot, text) {
|
|
|
6814
6784
|
return textMatches(slot.value, slot.exact, text);
|
|
6815
6785
|
}
|
|
6816
6786
|
}
|
|
6817
|
-
var filePath = (
|
|
6787
|
+
var filePath = (path24) => path24.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6818
6788
|
function sameFilePath(a, b) {
|
|
6819
6789
|
const x = filePath(a);
|
|
6820
6790
|
const y = filePath(b);
|
|
@@ -6908,16 +6878,16 @@ function confidenceOf(anchor, tests, reach) {
|
|
|
6908
6878
|
}
|
|
6909
6879
|
|
|
6910
6880
|
// ../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
|
|
6881
|
+
function escapeForQuote(value, quote3) {
|
|
6882
|
+
let out = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
6883
|
+
out = out.split(quote3).join(`\\${quote3}`);
|
|
6884
|
+
return quote3 === "`" ? out.replace(/\$\{/g, "\\${") : out;
|
|
6915
6885
|
}
|
|
6916
6886
|
function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
6917
6887
|
if (!sourceLine || oldValue === newValue) return null;
|
|
6918
6888
|
for (let i = 0; i < sourceLine.length; i++) {
|
|
6919
|
-
const
|
|
6920
|
-
if (
|
|
6889
|
+
const quote3 = sourceLine[i];
|
|
6890
|
+
if (quote3 !== "'" && quote3 !== '"' && quote3 !== "`") continue;
|
|
6921
6891
|
let value = "";
|
|
6922
6892
|
let end = -1;
|
|
6923
6893
|
for (let j = i + 1; j < sourceLine.length; j++) {
|
|
@@ -6928,8 +6898,8 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
|
6928
6898
|
j++;
|
|
6929
6899
|
continue;
|
|
6930
6900
|
}
|
|
6931
|
-
if (
|
|
6932
|
-
if (ch ===
|
|
6901
|
+
if (quote3 === "`" && ch === "$" && sourceLine[j + 1] === "{") break;
|
|
6902
|
+
if (ch === quote3) {
|
|
6933
6903
|
end = j;
|
|
6934
6904
|
break;
|
|
6935
6905
|
}
|
|
@@ -6937,7 +6907,7 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
|
6937
6907
|
}
|
|
6938
6908
|
if (end === -1) continue;
|
|
6939
6909
|
if (value === oldValue) {
|
|
6940
|
-
const next = sourceLine.slice(0, i + 1) + escapeForQuote(newValue,
|
|
6910
|
+
const next = sourceLine.slice(0, i + 1) + escapeForQuote(newValue, quote3) + sourceLine.slice(end);
|
|
6941
6911
|
return { old: sourceLine, new: next };
|
|
6942
6912
|
}
|
|
6943
6913
|
i = end;
|
|
@@ -7029,16 +6999,16 @@ function parsePreflightArgs(argv) {
|
|
|
7029
6999
|
}
|
|
7030
7000
|
var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
|
|
7031
7001
|
function findPlaywrightRoot(from) {
|
|
7032
|
-
let dir =
|
|
7002
|
+
let dir = path22.resolve(from);
|
|
7033
7003
|
for (; ; ) {
|
|
7034
|
-
if (CONFIG_NAMES2.some((name) => fs18.existsSync(
|
|
7035
|
-
const parent =
|
|
7004
|
+
if (CONFIG_NAMES2.some((name) => fs18.existsSync(path22.join(dir, name)))) return dir;
|
|
7005
|
+
const parent = path22.dirname(dir);
|
|
7036
7006
|
if (parent === dir) return null;
|
|
7037
7007
|
dir = parent;
|
|
7038
7008
|
}
|
|
7039
7009
|
}
|
|
7040
7010
|
function git2(cwd, args) {
|
|
7041
|
-
return (0,
|
|
7011
|
+
return (0, import_node_child_process8.execFileSync)("git", args, {
|
|
7042
7012
|
cwd,
|
|
7043
7013
|
encoding: "utf-8",
|
|
7044
7014
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -7062,7 +7032,7 @@ function translationLookup(repoRoot, base, locale) {
|
|
|
7062
7032
|
const build = (side) => {
|
|
7063
7033
|
const map = /* @__PURE__ */ new Map();
|
|
7064
7034
|
for (const file of files) {
|
|
7065
|
-
const content = side === "new" ? readText(
|
|
7035
|
+
const content = side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
|
|
7066
7036
|
if (content === void 0) continue;
|
|
7067
7037
|
for (const entry of parseTranslationFile(file, content)) {
|
|
7068
7038
|
if (!map.has(entry.key)) map.set(entry.key, entry.value);
|
|
@@ -7086,20 +7056,20 @@ function readText(file) {
|
|
|
7086
7056
|
return void 0;
|
|
7087
7057
|
}
|
|
7088
7058
|
}
|
|
7089
|
-
var CACHE_FILE2 =
|
|
7059
|
+
var CACHE_FILE2 = path22.join(".piwi", "locator-index.json");
|
|
7090
7060
|
function cacheKey2(connection, branch) {
|
|
7091
7061
|
return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
|
|
7092
7062
|
}
|
|
7093
7063
|
function readCache2(root, key) {
|
|
7094
7064
|
try {
|
|
7095
|
-
const store = JSON.parse(fs18.readFileSync(
|
|
7065
|
+
const store = JSON.parse(fs18.readFileSync(path22.join(root, CACHE_FILE2), "utf-8"));
|
|
7096
7066
|
return store[key] ?? null;
|
|
7097
7067
|
} catch {
|
|
7098
7068
|
return null;
|
|
7099
7069
|
}
|
|
7100
7070
|
}
|
|
7101
7071
|
function writeCache2(root, key, index) {
|
|
7102
|
-
const file =
|
|
7072
|
+
const file = path22.join(root, CACHE_FILE2);
|
|
7103
7073
|
try {
|
|
7104
7074
|
let store = {};
|
|
7105
7075
|
try {
|
|
@@ -7107,7 +7077,7 @@ function writeCache2(root, key, index) {
|
|
|
7107
7077
|
} catch {
|
|
7108
7078
|
}
|
|
7109
7079
|
store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
|
|
7110
|
-
fs18.mkdirSync(
|
|
7080
|
+
fs18.mkdirSync(path22.dirname(file), { recursive: true });
|
|
7111
7081
|
fs18.writeFileSync(file, JSON.stringify(store));
|
|
7112
7082
|
} catch {
|
|
7113
7083
|
}
|
|
@@ -7124,7 +7094,7 @@ function planSites(b, testRoot) {
|
|
|
7124
7094
|
const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
|
|
7125
7095
|
return sites.map((callSite) => {
|
|
7126
7096
|
if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
|
|
7127
|
-
const file =
|
|
7097
|
+
const file = path22.resolve(testRoot, callSiteFile(callSite));
|
|
7128
7098
|
const lineNo = callSiteLine(callSite);
|
|
7129
7099
|
const text = readText(file);
|
|
7130
7100
|
if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
|
|
@@ -7140,8 +7110,8 @@ function planSites(b, testRoot) {
|
|
|
7140
7110
|
}
|
|
7141
7111
|
const holders = /* @__PURE__ */ new Set();
|
|
7142
7112
|
for (const [before] of b.replacements) {
|
|
7143
|
-
for (const
|
|
7144
|
-
const out = tryGit(testRoot, ["grep", "-n", "--fixed-strings", `${
|
|
7113
|
+
for (const quote3 of ["'", '"', "`"]) {
|
|
7114
|
+
const out = tryGit(testRoot, ["grep", "-n", "--fixed-strings", `${quote3}${before}${quote3}`]);
|
|
7145
7115
|
for (const row of (out ?? "").split("\n")) {
|
|
7146
7116
|
const m = /^(.+?):(\d+):/.exec(row);
|
|
7147
7117
|
if (m) holders.add(`${m[1]}:${m[2]}`);
|
|
@@ -7288,7 +7258,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7288
7258
|
console.error("piwi preflight: not inside a git repository");
|
|
7289
7259
|
return EXIT_ERROR8;
|
|
7290
7260
|
}
|
|
7291
|
-
const testRoot =
|
|
7261
|
+
const testRoot = path22.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
|
|
7292
7262
|
let files;
|
|
7293
7263
|
try {
|
|
7294
7264
|
files = parseUnifiedDiff(git2(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
|
|
@@ -7325,7 +7295,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7325
7295
|
testIdAttributes: index.testIdAttributes ?? void 0,
|
|
7326
7296
|
isTestFile: (file) => TEST_FILE.test(file),
|
|
7327
7297
|
translations: translationLookup(repoRoot, args.base, args.locale),
|
|
7328
|
-
readFile: (file, side) => side === "new" ? readText(
|
|
7298
|
+
readFile: (file, side) => side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
|
|
7329
7299
|
});
|
|
7330
7300
|
const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
|
|
7331
7301
|
const replacementsOf = /* @__PURE__ */ new Map();
|
|
@@ -7370,8 +7340,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7370
7340
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
7371
7341
|
else {
|
|
7372
7342
|
const rel = (file) => {
|
|
7373
|
-
const abs =
|
|
7374
|
-
const r =
|
|
7343
|
+
const abs = path22.isAbsolute(file) ? file : path22.resolve(testRoot, file);
|
|
7344
|
+
const r = path22.relative(cwd, abs).split(path22.sep).join("/");
|
|
7375
7345
|
return r.startsWith("..") ? file : r || file;
|
|
7376
7346
|
};
|
|
7377
7347
|
console.log(renderPreflight(result, rel));
|
|
@@ -7403,7 +7373,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
7403
7373
|
// src/cli/probe.ts
|
|
7404
7374
|
var fs19 = __toESM(require("fs"));
|
|
7405
7375
|
var os3 = __toESM(require("os"));
|
|
7406
|
-
var
|
|
7376
|
+
var path23 = __toESM(require("path"));
|
|
7407
7377
|
var EXIT_OK8 = 0;
|
|
7408
7378
|
var EXIT_ERROR9 = 2;
|
|
7409
7379
|
var USAGE11 = `
|
|
@@ -7428,9 +7398,9 @@ function escapeRegex2(s) {
|
|
|
7428
7398
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7429
7399
|
}
|
|
7430
7400
|
async function runProbe(argv) {
|
|
7431
|
-
const
|
|
7432
|
-
const own =
|
|
7433
|
-
const playwrightArgs2 =
|
|
7401
|
+
const sep7 = argv.indexOf("--");
|
|
7402
|
+
const own = sep7 < 0 ? argv : argv.slice(0, sep7);
|
|
7403
|
+
const playwrightArgs2 = sep7 < 0 ? [] : argv.slice(sep7 + 1);
|
|
7434
7404
|
if (own.includes("-h") || own.includes("--help")) {
|
|
7435
7405
|
console.log(USAGE11);
|
|
7436
7406
|
return EXIT_OK8;
|
|
@@ -7467,10 +7437,10 @@ async function runProbe(argv) {
|
|
|
7467
7437
|
return EXIT_OK8;
|
|
7468
7438
|
}
|
|
7469
7439
|
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 =
|
|
7440
|
+
const dir = fs19.mkdtempSync(path23.join(os3.tmpdir(), "piwi-probe-"));
|
|
7441
|
+
const planFile = path23.join(dir, "plan.json");
|
|
7442
|
+
const resultsFile = path23.join(dir, "results.jsonl");
|
|
7443
|
+
const runOutputFile = path23.join(dir, "run.json");
|
|
7474
7444
|
fs19.writeFileSync(planFile, JSON.stringify(plan));
|
|
7475
7445
|
fs19.writeFileSync(resultsFile, "");
|
|
7476
7446
|
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
|