@piwitests/reporter 0.53.0 → 0.54.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/index.js +196 -28
- package/dist/internal/capture/capture-fixtures.d.ts +17 -12
- package/dist/internal/capture/capture-fixtures.js +196 -28
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
- package/templates/skills/stabilize-flaky-tests/SKILL.md +1 -1
- package/templates/skills/write-the-missing-test/SKILL.md +32 -6
package/dist/index.js
CHANGED
|
@@ -5700,6 +5700,9 @@ function boxCaptureFrames(extraPrefixes = []) {
|
|
|
5700
5700
|
} catch {
|
|
5701
5701
|
}
|
|
5702
5702
|
}
|
|
5703
|
+
function callMethod(target, prop, args) {
|
|
5704
|
+
return target[prop](...args);
|
|
5705
|
+
}
|
|
5703
5706
|
function internalCall(owner, run) {
|
|
5704
5707
|
const wrap = owner?._wrapApiCall;
|
|
5705
5708
|
if (typeof wrap !== "function") return run();
|
|
@@ -13157,6 +13160,143 @@ function readPageInteraction() {
|
|
|
13157
13160
|
return out;
|
|
13158
13161
|
}
|
|
13159
13162
|
|
|
13163
|
+
// src/internal/capture/api-requests.ts
|
|
13164
|
+
var SENDING_METHODS = /* @__PURE__ */ new Set(["fetch", "get", "post", "put", "patch", "delete", "head"]);
|
|
13165
|
+
var PROXIES = /* @__PURE__ */ new WeakMap();
|
|
13166
|
+
var OBSERVED = /* @__PURE__ */ new WeakSet();
|
|
13167
|
+
var PATCHED_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
13168
|
+
function observeApiRequests(playwright, recorder) {
|
|
13169
|
+
const apiRequest = playwright?.request;
|
|
13170
|
+
if (!apiRequest || typeof apiRequest !== "object" || typeof apiRequest.newContext !== "function") return;
|
|
13171
|
+
if (PATCHED_REQUESTS.has(apiRequest)) return;
|
|
13172
|
+
PATCHED_REQUESTS.add(apiRequest);
|
|
13173
|
+
const create = apiRequest.newContext.bind(apiRequest);
|
|
13174
|
+
apiRequest.newContext = async function newContext(...args) {
|
|
13175
|
+
const context = await create(...args);
|
|
13176
|
+
return observeApiRequestContext(context, recorder, baseUrlOption(args[0]));
|
|
13177
|
+
};
|
|
13178
|
+
}
|
|
13179
|
+
function observeApiRequestContext(context, recorder, baseURL = null) {
|
|
13180
|
+
if (!context || typeof context !== "object" || OBSERVED.has(context)) return context;
|
|
13181
|
+
const known = PROXIES.get(context);
|
|
13182
|
+
if (known) return known;
|
|
13183
|
+
const forwarders = /* @__PURE__ */ new Map();
|
|
13184
|
+
const proxy = new Proxy(context, {
|
|
13185
|
+
get(target, prop) {
|
|
13186
|
+
const value = Reflect.get(target, prop);
|
|
13187
|
+
if (typeof value !== "function" || prop === "constructor") return value;
|
|
13188
|
+
const cached = forwarders.get(prop);
|
|
13189
|
+
if (cached?.method === value) return cached.forward;
|
|
13190
|
+
const forward = typeof prop === "string" && SENDING_METHODS.has(prop) ? sendingMethod(target, prop, recorder, baseURL) : value.bind(target);
|
|
13191
|
+
forwarders.set(prop, { method: value, forward });
|
|
13192
|
+
return forward;
|
|
13193
|
+
}
|
|
13194
|
+
});
|
|
13195
|
+
PROXIES.set(context, proxy);
|
|
13196
|
+
OBSERVED.add(proxy);
|
|
13197
|
+
return proxy;
|
|
13198
|
+
}
|
|
13199
|
+
function sendingMethod(target, prop, recorder, baseURL) {
|
|
13200
|
+
return function send(...args) {
|
|
13201
|
+
const record = recorder();
|
|
13202
|
+
if (!record) return callMethod(target, prop, args);
|
|
13203
|
+
const startedAt = Date.now();
|
|
13204
|
+
const result = callMethod(target, prop, args);
|
|
13205
|
+
if (typeof result?.then !== "function") return result;
|
|
13206
|
+
const report = (response, error) => {
|
|
13207
|
+
try {
|
|
13208
|
+
record({
|
|
13209
|
+
method: methodOf(prop, args),
|
|
13210
|
+
url: urlOf(args[0]),
|
|
13211
|
+
baseURL,
|
|
13212
|
+
startedAt,
|
|
13213
|
+
endedAt: Date.now(),
|
|
13214
|
+
response,
|
|
13215
|
+
error
|
|
13216
|
+
});
|
|
13217
|
+
} catch {
|
|
13218
|
+
}
|
|
13219
|
+
};
|
|
13220
|
+
return result.then(
|
|
13221
|
+
(response) => {
|
|
13222
|
+
report(response);
|
|
13223
|
+
return response;
|
|
13224
|
+
},
|
|
13225
|
+
(error) => {
|
|
13226
|
+
report(null, error);
|
|
13227
|
+
throw error;
|
|
13228
|
+
}
|
|
13229
|
+
);
|
|
13230
|
+
};
|
|
13231
|
+
}
|
|
13232
|
+
function methodOf(prop, args) {
|
|
13233
|
+
if (prop !== "fetch") return prop.toUpperCase();
|
|
13234
|
+
const option = args[1]?.method;
|
|
13235
|
+
if (typeof option === "string" && option) return option.toUpperCase();
|
|
13236
|
+
const request4 = args[0];
|
|
13237
|
+
const method = typeof request4?.method === "function" ? request4.method() : null;
|
|
13238
|
+
return typeof method === "string" && method ? method.toUpperCase() : "GET";
|
|
13239
|
+
}
|
|
13240
|
+
function urlOf(target) {
|
|
13241
|
+
if (typeof target === "string") return target;
|
|
13242
|
+
const url = typeof target?.url === "function" ? target.url() : null;
|
|
13243
|
+
return typeof url === "string" ? url : null;
|
|
13244
|
+
}
|
|
13245
|
+
function baseUrlOption(options) {
|
|
13246
|
+
const baseURL = options?.baseURL;
|
|
13247
|
+
return typeof baseURL === "string" && baseURL ? baseURL : null;
|
|
13248
|
+
}
|
|
13249
|
+
var MAX_FAILURE_LENGTH = 300;
|
|
13250
|
+
var API_ERROR_PREFIX = /^apiRequestContext\.\w+: /;
|
|
13251
|
+
var STATUS_FAILURE = /^(\d{3}) /;
|
|
13252
|
+
function apiCallEntry(call, baseURL) {
|
|
13253
|
+
const response = call.response;
|
|
13254
|
+
if (response) {
|
|
13255
|
+
const timing = responseTiming(response) ?? { startTime: call.startedAt, duration: call.endedAt - call.startedAt };
|
|
13256
|
+
return {
|
|
13257
|
+
method: call.method,
|
|
13258
|
+
url: response.url(),
|
|
13259
|
+
status: response.status(),
|
|
13260
|
+
duration: timing.duration,
|
|
13261
|
+
startTime: timing.startTime
|
|
13262
|
+
};
|
|
13263
|
+
}
|
|
13264
|
+
const url = resolveUrl(call.url, call.baseURL ?? baseURL);
|
|
13265
|
+
if (!url) return null;
|
|
13266
|
+
const failure = failureText(call.error);
|
|
13267
|
+
const status = STATUS_FAILURE.exec(failure);
|
|
13268
|
+
return {
|
|
13269
|
+
method: call.method,
|
|
13270
|
+
url,
|
|
13271
|
+
status: status ? Number(status[1]) : 0,
|
|
13272
|
+
duration: Math.max(0, call.endedAt - call.startedAt),
|
|
13273
|
+
startTime: call.startedAt,
|
|
13274
|
+
...status ? {} : { failure }
|
|
13275
|
+
};
|
|
13276
|
+
}
|
|
13277
|
+
function responseTiming(response) {
|
|
13278
|
+
if (typeof response.timing !== "function") return null;
|
|
13279
|
+
const timing = response.timing();
|
|
13280
|
+
if (!(timing.startTime > 0) || !(timing.responseEnd >= 0)) return null;
|
|
13281
|
+
return {
|
|
13282
|
+
startTime: timing.startTime,
|
|
13283
|
+
duration: Math.max(0, Math.round(timing.responseEnd - Math.max(0, timing.requestStart)))
|
|
13284
|
+
};
|
|
13285
|
+
}
|
|
13286
|
+
function resolveUrl(url, baseURL) {
|
|
13287
|
+
if (!url) return null;
|
|
13288
|
+
try {
|
|
13289
|
+
return new URL(url, baseURL ?? void 0).toString();
|
|
13290
|
+
} catch {
|
|
13291
|
+
return url;
|
|
13292
|
+
}
|
|
13293
|
+
}
|
|
13294
|
+
function failureText(error) {
|
|
13295
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13296
|
+
const line = message.split("\n", 1)[0].replace(API_ERROR_PREFIX, "").trim();
|
|
13297
|
+
return (line || "failed").slice(0, MAX_FAILURE_LENGTH);
|
|
13298
|
+
}
|
|
13299
|
+
|
|
13160
13300
|
// src/internal/capture/capture-fixtures.ts
|
|
13161
13301
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
13162
13302
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -13217,7 +13357,8 @@ function createSink() {
|
|
|
13217
13357
|
pause: null,
|
|
13218
13358
|
locatorPages: new LocatorPageLog(),
|
|
13219
13359
|
codeReach: null,
|
|
13220
|
-
serverCoverage: /* @__PURE__ */ new Map()
|
|
13360
|
+
serverCoverage: /* @__PURE__ */ new Map(),
|
|
13361
|
+
baseURL: null
|
|
13221
13362
|
};
|
|
13222
13363
|
}
|
|
13223
13364
|
function maybeStartCodeReach(page) {
|
|
@@ -13672,9 +13813,6 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
13672
13813
|
})();
|
|
13673
13814
|
sink.capturePromises.push(resolveAttrs);
|
|
13674
13815
|
}
|
|
13675
|
-
function callMethod(target, prop, args) {
|
|
13676
|
-
return target[prop](...args);
|
|
13677
|
-
}
|
|
13678
13816
|
function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
13679
13817
|
try {
|
|
13680
13818
|
sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
|
|
@@ -13841,6 +13979,7 @@ function instrumentPage(page) {
|
|
|
13841
13979
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
13842
13980
|
INSTRUMENTED_PAGES.add(page);
|
|
13843
13981
|
void maybeStartCodeReach(page);
|
|
13982
|
+
observeOwnApiRequests(page);
|
|
13844
13983
|
const probeSink = currentSink;
|
|
13845
13984
|
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
13846
13985
|
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
@@ -14000,25 +14139,7 @@ function recordRequest(sink, request4, end, response) {
|
|
|
14000
14139
|
if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
|
|
14001
14140
|
if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
|
|
14002
14141
|
}
|
|
14003
|
-
|
|
14004
|
-
const contentType = headers["content-type"];
|
|
14005
|
-
if (contentType) entry.contentType = contentType.split(";")[0].trim();
|
|
14006
|
-
const logHeader = headers["x-piwi-logs"];
|
|
14007
|
-
if (logHeader) {
|
|
14008
|
-
try {
|
|
14009
|
-
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
14010
|
-
} catch {
|
|
14011
|
-
}
|
|
14012
|
-
}
|
|
14013
|
-
const coverageHeader = headers["x-piwi-coverage"];
|
|
14014
|
-
if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
|
|
14015
|
-
const traceHeader = headers["x-piwi-trace"];
|
|
14016
|
-
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
14017
|
-
try {
|
|
14018
|
-
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
14019
|
-
} catch {
|
|
14020
|
-
}
|
|
14021
|
-
}
|
|
14142
|
+
readResponseHeaders(sink, entry, url, response.headers());
|
|
14022
14143
|
}
|
|
14023
14144
|
sink.networkRequests.push(entry);
|
|
14024
14145
|
} catch {
|
|
@@ -14026,6 +14147,48 @@ function recordRequest(sink, request4, end, response) {
|
|
|
14026
14147
|
})();
|
|
14027
14148
|
sink.pendingHandlers.push(p);
|
|
14028
14149
|
}
|
|
14150
|
+
function readResponseHeaders(sink, entry, url, headers) {
|
|
14151
|
+
const contentType = headers["content-type"];
|
|
14152
|
+
if (contentType) entry.contentType = contentType.split(";")[0].trim();
|
|
14153
|
+
const logHeader = headers["x-piwi-logs"];
|
|
14154
|
+
if (logHeader) {
|
|
14155
|
+
try {
|
|
14156
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
14157
|
+
} catch {
|
|
14158
|
+
}
|
|
14159
|
+
}
|
|
14160
|
+
const coverageHeader = headers["x-piwi-coverage"];
|
|
14161
|
+
if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
|
|
14162
|
+
const traceHeader = headers["x-piwi-trace"];
|
|
14163
|
+
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
14164
|
+
try {
|
|
14165
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
14166
|
+
} catch {
|
|
14167
|
+
}
|
|
14168
|
+
}
|
|
14169
|
+
}
|
|
14170
|
+
function recordApiCall(sink, call) {
|
|
14171
|
+
try {
|
|
14172
|
+
const entry = apiCallEntry(call, sink.baseURL);
|
|
14173
|
+
if (!entry) return;
|
|
14174
|
+
if (call.response) readResponseHeaders(sink, entry, String(entry.url), call.response.headers());
|
|
14175
|
+
sink.networkRequests.push(entry);
|
|
14176
|
+
} catch {
|
|
14177
|
+
}
|
|
14178
|
+
}
|
|
14179
|
+
function apiCallRecorder() {
|
|
14180
|
+
const sink = currentSink;
|
|
14181
|
+
return sink ? (call) => recordApiCall(sink, call) : null;
|
|
14182
|
+
}
|
|
14183
|
+
function observeOwnApiRequests(owner) {
|
|
14184
|
+
try {
|
|
14185
|
+
const holder = owner;
|
|
14186
|
+
if (holder.request && typeof holder.request === "object") {
|
|
14187
|
+
holder.request = observeApiRequestContext(holder.request, apiCallRecorder);
|
|
14188
|
+
}
|
|
14189
|
+
} catch {
|
|
14190
|
+
}
|
|
14191
|
+
}
|
|
14029
14192
|
function recordOpenRequests(sink) {
|
|
14030
14193
|
for (const [request4, response] of sink.openResponses) {
|
|
14031
14194
|
RECORDED_OPEN.add(request4);
|
|
@@ -14039,6 +14202,7 @@ async function awaitFlakeConditions() {
|
|
|
14039
14202
|
function instrumentContext(context) {
|
|
14040
14203
|
if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
|
|
14041
14204
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
14205
|
+
observeOwnApiRequests(context);
|
|
14042
14206
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
14043
14207
|
void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
|
|
14044
14208
|
});
|
|
@@ -14293,14 +14457,17 @@ async function flushSink(sink, testInfo) {
|
|
|
14293
14457
|
}
|
|
14294
14458
|
}
|
|
14295
14459
|
var piwiFixtures = {
|
|
14296
|
-
// Auto, worker-scoped: the worker's resource ledger
|
|
14297
|
-
//
|
|
14298
|
-
//
|
|
14299
|
-
//
|
|
14460
|
+
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
14461
|
+
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
14462
|
+
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
14463
|
+
// ledger and records the calls the tests make through it, and torn down after
|
|
14464
|
+
// the browser closed, so its census sees what only the worker's shutdown
|
|
14465
|
+
// closed. Depends on `playwright` alone, which launches nothing.
|
|
14300
14466
|
piwiResources: [
|
|
14301
14467
|
async ({ playwright }, use, workerInfo) => {
|
|
14302
14468
|
boxCaptureFrames();
|
|
14303
14469
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
14470
|
+
observeApiRequests(playwright, apiCallRecorder);
|
|
14304
14471
|
try {
|
|
14305
14472
|
await use();
|
|
14306
14473
|
} finally {
|
|
@@ -14331,12 +14498,13 @@ var piwiFixtures = {
|
|
|
14331
14498
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
14332
14499
|
// requested, so suites that never destructure `page` are still captured.
|
|
14333
14500
|
piwiCapture: [
|
|
14334
|
-
async ({ screenshot }, use, testInfo) => {
|
|
14501
|
+
async ({ screenshot, baseURL }, use, testInfo) => {
|
|
14335
14502
|
boxCaptureFrames();
|
|
14336
14503
|
const conflict = labModeConflict();
|
|
14337
14504
|
if (conflict) throw new Error(conflict);
|
|
14338
14505
|
const sink = createSink();
|
|
14339
14506
|
sink.testInfo = testInfo;
|
|
14507
|
+
sink.baseURL = baseURL ?? null;
|
|
14340
14508
|
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
14341
14509
|
const pauseSet = currentPauseSet();
|
|
14342
14510
|
if (pauseSet.size) {
|
|
@@ -35,6 +35,13 @@ declare class LocatorPageLog {
|
|
|
35
35
|
* layout leaves Playwright's defaults untouched.
|
|
36
36
|
*/
|
|
37
37
|
declare function boxCaptureFrames(extraPrefixes?: readonly string[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Call a Playwright method on its own receiver. Playwright names an API call
|
|
40
|
+
* after the method frame the call enters, so the call keeps its real name
|
|
41
|
+
* (`locator.fill: Timeout …`); a call through `Function.prototype.apply` is
|
|
42
|
+
* named `apply`.
|
|
43
|
+
*/
|
|
44
|
+
declare function callMethod(target: object, prop: string | symbol, args: unknown[]): unknown;
|
|
38
45
|
/**
|
|
39
46
|
* Run a page read as an internal Playwright call: every API call made inside
|
|
40
47
|
* `run` is kept out of the test's steps and the trace. `owner` is the page (or
|
|
@@ -553,13 +560,6 @@ declare function ariaSnapshotBestEffort(target: Locator, timeout?: number): Prom
|
|
|
553
560
|
* yields null and the capture proceeds with the YAML alone.
|
|
554
561
|
*/
|
|
555
562
|
declare function ariaSnapshotJSONBestEffort(target: Locator, timeout?: number): Promise<string | null>;
|
|
556
|
-
/**
|
|
557
|
-
* Call a Playwright method on its own receiver. Playwright names an API call
|
|
558
|
-
* after the method frame the call enters, so the call keeps its real name
|
|
559
|
-
* (`locator.fill: Timeout …`); a call through `Function.prototype.apply` is
|
|
560
|
-
* named `apply`.
|
|
561
|
-
*/
|
|
562
|
-
declare function callMethod(target: object, prop: string | symbol, args: unknown[]): unknown;
|
|
563
563
|
/**
|
|
564
564
|
* The fixtures `piwiFixtures` / `extendPiwiFixtures` contribute. The single
|
|
565
565
|
* added fixture is `piwiCapture`: an auto, test-scoped teardown hook that
|
|
@@ -575,9 +575,11 @@ interface PiwiFixtures {
|
|
|
575
575
|
* The worker fixture `piwiFixtures` / `extendPiwiFixtures` contribute:
|
|
576
576
|
* `piwiResources`, an auto, worker-scoped fixture that keeps the worker's
|
|
577
577
|
* resource ledger (the browsers, contexts, pages and API request contexts its
|
|
578
|
-
* tests open) from before the first hook to the worker's shutdown
|
|
579
|
-
*
|
|
580
|
-
*
|
|
578
|
+
* tests open) from before the first hook to the worker's shutdown, and records
|
|
579
|
+
* the calls tests make through the API request contexts `playwright.request`
|
|
580
|
+
* creates, the `request` fixture's among them. Its name is **reserved** like
|
|
581
|
+
* `piwiCapture`'s: a fixture of your own with that name replaces it and turns
|
|
582
|
+
* the ledger and those calls' capture off.
|
|
581
583
|
*/
|
|
582
584
|
interface PiwiWorkerFixtures {
|
|
583
585
|
piwiResources: void;
|
|
@@ -588,8 +590,11 @@ interface PiwiWorkerFixtures {
|
|
|
588
590
|
*
|
|
589
591
|
* Capture is wired at the `browser` level, so it works whether a test uses the
|
|
590
592
|
* standard `page` fixture or builds its own pages from `browser` /
|
|
591
|
-
* `browser.newContext()`.
|
|
592
|
-
*
|
|
593
|
+
* `browser.newContext()`. The calls a test makes through an API request
|
|
594
|
+
* context (the `request` fixture, `playwright.request.newContext()`,
|
|
595
|
+
* `page.request`) are recorded with its network requests. Collected data is
|
|
596
|
+
* attached as `piwi-*` test-info attachments which the Piwi Dashboard reporter
|
|
597
|
+
* parses on `onTestEnd`.
|
|
593
598
|
*/
|
|
594
599
|
declare const piwiFixtures: Fixtures<PiwiFixtures, PiwiWorkerFixtures, PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
|
|
595
600
|
/**
|
|
@@ -4039,6 +4039,9 @@ function boxCaptureFrames(extraPrefixes = []) {
|
|
|
4039
4039
|
} catch {
|
|
4040
4040
|
}
|
|
4041
4041
|
}
|
|
4042
|
+
function callMethod(target, prop, args) {
|
|
4043
|
+
return target[prop](...args);
|
|
4044
|
+
}
|
|
4042
4045
|
function internalCall(owner, run) {
|
|
4043
4046
|
const wrap = owner?._wrapApiCall;
|
|
4044
4047
|
if (typeof wrap !== "function") return run();
|
|
@@ -5522,6 +5525,143 @@ function readPageInteraction() {
|
|
|
5522
5525
|
return out;
|
|
5523
5526
|
}
|
|
5524
5527
|
|
|
5528
|
+
// src/internal/capture/api-requests.ts
|
|
5529
|
+
var SENDING_METHODS = /* @__PURE__ */ new Set(["fetch", "get", "post", "put", "patch", "delete", "head"]);
|
|
5530
|
+
var PROXIES = /* @__PURE__ */ new WeakMap();
|
|
5531
|
+
var OBSERVED = /* @__PURE__ */ new WeakSet();
|
|
5532
|
+
var PATCHED_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
5533
|
+
function observeApiRequests(playwright, recorder) {
|
|
5534
|
+
const apiRequest = playwright?.request;
|
|
5535
|
+
if (!apiRequest || typeof apiRequest !== "object" || typeof apiRequest.newContext !== "function") return;
|
|
5536
|
+
if (PATCHED_REQUESTS.has(apiRequest)) return;
|
|
5537
|
+
PATCHED_REQUESTS.add(apiRequest);
|
|
5538
|
+
const create = apiRequest.newContext.bind(apiRequest);
|
|
5539
|
+
apiRequest.newContext = async function newContext(...args) {
|
|
5540
|
+
const context = await create(...args);
|
|
5541
|
+
return observeApiRequestContext(context, recorder, baseUrlOption(args[0]));
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
function observeApiRequestContext(context, recorder, baseURL = null) {
|
|
5545
|
+
if (!context || typeof context !== "object" || OBSERVED.has(context)) return context;
|
|
5546
|
+
const known = PROXIES.get(context);
|
|
5547
|
+
if (known) return known;
|
|
5548
|
+
const forwarders = /* @__PURE__ */ new Map();
|
|
5549
|
+
const proxy = new Proxy(context, {
|
|
5550
|
+
get(target, prop) {
|
|
5551
|
+
const value = Reflect.get(target, prop);
|
|
5552
|
+
if (typeof value !== "function" || prop === "constructor") return value;
|
|
5553
|
+
const cached = forwarders.get(prop);
|
|
5554
|
+
if (cached?.method === value) return cached.forward;
|
|
5555
|
+
const forward = typeof prop === "string" && SENDING_METHODS.has(prop) ? sendingMethod(target, prop, recorder, baseURL) : value.bind(target);
|
|
5556
|
+
forwarders.set(prop, { method: value, forward });
|
|
5557
|
+
return forward;
|
|
5558
|
+
}
|
|
5559
|
+
});
|
|
5560
|
+
PROXIES.set(context, proxy);
|
|
5561
|
+
OBSERVED.add(proxy);
|
|
5562
|
+
return proxy;
|
|
5563
|
+
}
|
|
5564
|
+
function sendingMethod(target, prop, recorder, baseURL) {
|
|
5565
|
+
return function send(...args) {
|
|
5566
|
+
const record = recorder();
|
|
5567
|
+
if (!record) return callMethod(target, prop, args);
|
|
5568
|
+
const startedAt = Date.now();
|
|
5569
|
+
const result = callMethod(target, prop, args);
|
|
5570
|
+
if (typeof result?.then !== "function") return result;
|
|
5571
|
+
const report = (response, error) => {
|
|
5572
|
+
try {
|
|
5573
|
+
record({
|
|
5574
|
+
method: methodOf(prop, args),
|
|
5575
|
+
url: urlOf(args[0]),
|
|
5576
|
+
baseURL,
|
|
5577
|
+
startedAt,
|
|
5578
|
+
endedAt: Date.now(),
|
|
5579
|
+
response,
|
|
5580
|
+
error
|
|
5581
|
+
});
|
|
5582
|
+
} catch {
|
|
5583
|
+
}
|
|
5584
|
+
};
|
|
5585
|
+
return result.then(
|
|
5586
|
+
(response) => {
|
|
5587
|
+
report(response);
|
|
5588
|
+
return response;
|
|
5589
|
+
},
|
|
5590
|
+
(error) => {
|
|
5591
|
+
report(null, error);
|
|
5592
|
+
throw error;
|
|
5593
|
+
}
|
|
5594
|
+
);
|
|
5595
|
+
};
|
|
5596
|
+
}
|
|
5597
|
+
function methodOf(prop, args) {
|
|
5598
|
+
if (prop !== "fetch") return prop.toUpperCase();
|
|
5599
|
+
const option = args[1]?.method;
|
|
5600
|
+
if (typeof option === "string" && option) return option.toUpperCase();
|
|
5601
|
+
const request = args[0];
|
|
5602
|
+
const method = typeof request?.method === "function" ? request.method() : null;
|
|
5603
|
+
return typeof method === "string" && method ? method.toUpperCase() : "GET";
|
|
5604
|
+
}
|
|
5605
|
+
function urlOf(target) {
|
|
5606
|
+
if (typeof target === "string") return target;
|
|
5607
|
+
const url = typeof target?.url === "function" ? target.url() : null;
|
|
5608
|
+
return typeof url === "string" ? url : null;
|
|
5609
|
+
}
|
|
5610
|
+
function baseUrlOption(options) {
|
|
5611
|
+
const baseURL = options?.baseURL;
|
|
5612
|
+
return typeof baseURL === "string" && baseURL ? baseURL : null;
|
|
5613
|
+
}
|
|
5614
|
+
var MAX_FAILURE_LENGTH = 300;
|
|
5615
|
+
var API_ERROR_PREFIX = /^apiRequestContext\.\w+: /;
|
|
5616
|
+
var STATUS_FAILURE = /^(\d{3}) /;
|
|
5617
|
+
function apiCallEntry(call, baseURL) {
|
|
5618
|
+
const response = call.response;
|
|
5619
|
+
if (response) {
|
|
5620
|
+
const timing = responseTiming(response) ?? { startTime: call.startedAt, duration: call.endedAt - call.startedAt };
|
|
5621
|
+
return {
|
|
5622
|
+
method: call.method,
|
|
5623
|
+
url: response.url(),
|
|
5624
|
+
status: response.status(),
|
|
5625
|
+
duration: timing.duration,
|
|
5626
|
+
startTime: timing.startTime
|
|
5627
|
+
};
|
|
5628
|
+
}
|
|
5629
|
+
const url = resolveUrl(call.url, call.baseURL ?? baseURL);
|
|
5630
|
+
if (!url) return null;
|
|
5631
|
+
const failure = failureText(call.error);
|
|
5632
|
+
const status = STATUS_FAILURE.exec(failure);
|
|
5633
|
+
return {
|
|
5634
|
+
method: call.method,
|
|
5635
|
+
url,
|
|
5636
|
+
status: status ? Number(status[1]) : 0,
|
|
5637
|
+
duration: Math.max(0, call.endedAt - call.startedAt),
|
|
5638
|
+
startTime: call.startedAt,
|
|
5639
|
+
...status ? {} : { failure }
|
|
5640
|
+
};
|
|
5641
|
+
}
|
|
5642
|
+
function responseTiming(response) {
|
|
5643
|
+
if (typeof response.timing !== "function") return null;
|
|
5644
|
+
const timing = response.timing();
|
|
5645
|
+
if (!(timing.startTime > 0) || !(timing.responseEnd >= 0)) return null;
|
|
5646
|
+
return {
|
|
5647
|
+
startTime: timing.startTime,
|
|
5648
|
+
duration: Math.max(0, Math.round(timing.responseEnd - Math.max(0, timing.requestStart)))
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
function resolveUrl(url, baseURL) {
|
|
5652
|
+
if (!url) return null;
|
|
5653
|
+
try {
|
|
5654
|
+
return new URL(url, baseURL ?? void 0).toString();
|
|
5655
|
+
} catch {
|
|
5656
|
+
return url;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
function failureText(error) {
|
|
5660
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5661
|
+
const line = message.split("\n", 1)[0].replace(API_ERROR_PREFIX, "").trim();
|
|
5662
|
+
return (line || "failed").slice(0, MAX_FAILURE_LENGTH);
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5525
5665
|
// src/internal/capture/capture-fixtures.ts
|
|
5526
5666
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
5527
5667
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -5582,7 +5722,8 @@ function createSink() {
|
|
|
5582
5722
|
pause: null,
|
|
5583
5723
|
locatorPages: new LocatorPageLog(),
|
|
5584
5724
|
codeReach: null,
|
|
5585
|
-
serverCoverage: /* @__PURE__ */ new Map()
|
|
5725
|
+
serverCoverage: /* @__PURE__ */ new Map(),
|
|
5726
|
+
baseURL: null
|
|
5586
5727
|
};
|
|
5587
5728
|
}
|
|
5588
5729
|
function maybeStartCodeReach(page) {
|
|
@@ -6037,9 +6178,6 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
6037
6178
|
})();
|
|
6038
6179
|
sink.capturePromises.push(resolveAttrs);
|
|
6039
6180
|
}
|
|
6040
|
-
function callMethod(target, prop, args) {
|
|
6041
|
-
return target[prop](...args);
|
|
6042
|
-
}
|
|
6043
6181
|
function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
6044
6182
|
try {
|
|
6045
6183
|
sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
|
|
@@ -6206,6 +6344,7 @@ function instrumentPage(page) {
|
|
|
6206
6344
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
6207
6345
|
INSTRUMENTED_PAGES.add(page);
|
|
6208
6346
|
void maybeStartCodeReach(page);
|
|
6347
|
+
observeOwnApiRequests(page);
|
|
6209
6348
|
const probeSink = currentSink;
|
|
6210
6349
|
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
6211
6350
|
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
@@ -6365,25 +6504,7 @@ function recordRequest(sink, request, end, response) {
|
|
|
6365
6504
|
if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
|
|
6366
6505
|
if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
|
|
6367
6506
|
}
|
|
6368
|
-
|
|
6369
|
-
const contentType = headers["content-type"];
|
|
6370
|
-
if (contentType) entry.contentType = contentType.split(";")[0].trim();
|
|
6371
|
-
const logHeader = headers["x-piwi-logs"];
|
|
6372
|
-
if (logHeader) {
|
|
6373
|
-
try {
|
|
6374
|
-
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
6375
|
-
} catch {
|
|
6376
|
-
}
|
|
6377
|
-
}
|
|
6378
|
-
const coverageHeader = headers["x-piwi-coverage"];
|
|
6379
|
-
if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
|
|
6380
|
-
const traceHeader = headers["x-piwi-trace"];
|
|
6381
|
-
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
6382
|
-
try {
|
|
6383
|
-
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
6384
|
-
} catch {
|
|
6385
|
-
}
|
|
6386
|
-
}
|
|
6507
|
+
readResponseHeaders(sink, entry, url, response.headers());
|
|
6387
6508
|
}
|
|
6388
6509
|
sink.networkRequests.push(entry);
|
|
6389
6510
|
} catch {
|
|
@@ -6391,6 +6512,48 @@ function recordRequest(sink, request, end, response) {
|
|
|
6391
6512
|
})();
|
|
6392
6513
|
sink.pendingHandlers.push(p);
|
|
6393
6514
|
}
|
|
6515
|
+
function readResponseHeaders(sink, entry, url, headers) {
|
|
6516
|
+
const contentType = headers["content-type"];
|
|
6517
|
+
if (contentType) entry.contentType = contentType.split(";")[0].trim();
|
|
6518
|
+
const logHeader = headers["x-piwi-logs"];
|
|
6519
|
+
if (logHeader) {
|
|
6520
|
+
try {
|
|
6521
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
6522
|
+
} catch {
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
const coverageHeader = headers["x-piwi-coverage"];
|
|
6526
|
+
if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
|
|
6527
|
+
const traceHeader = headers["x-piwi-trace"];
|
|
6528
|
+
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
6529
|
+
try {
|
|
6530
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
6531
|
+
} catch {
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
}
|
|
6535
|
+
function recordApiCall(sink, call) {
|
|
6536
|
+
try {
|
|
6537
|
+
const entry = apiCallEntry(call, sink.baseURL);
|
|
6538
|
+
if (!entry) return;
|
|
6539
|
+
if (call.response) readResponseHeaders(sink, entry, String(entry.url), call.response.headers());
|
|
6540
|
+
sink.networkRequests.push(entry);
|
|
6541
|
+
} catch {
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
function apiCallRecorder() {
|
|
6545
|
+
const sink = currentSink;
|
|
6546
|
+
return sink ? (call) => recordApiCall(sink, call) : null;
|
|
6547
|
+
}
|
|
6548
|
+
function observeOwnApiRequests(owner) {
|
|
6549
|
+
try {
|
|
6550
|
+
const holder = owner;
|
|
6551
|
+
if (holder.request && typeof holder.request === "object") {
|
|
6552
|
+
holder.request = observeApiRequestContext(holder.request, apiCallRecorder);
|
|
6553
|
+
}
|
|
6554
|
+
} catch {
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6394
6557
|
function recordOpenRequests(sink) {
|
|
6395
6558
|
for (const [request, response] of sink.openResponses) {
|
|
6396
6559
|
RECORDED_OPEN.add(request);
|
|
@@ -6404,6 +6567,7 @@ async function awaitFlakeConditions() {
|
|
|
6404
6567
|
function instrumentContext(context) {
|
|
6405
6568
|
if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
|
|
6406
6569
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
6570
|
+
observeOwnApiRequests(context);
|
|
6407
6571
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
6408
6572
|
void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
|
|
6409
6573
|
});
|
|
@@ -6658,14 +6822,17 @@ async function flushSink(sink, testInfo) {
|
|
|
6658
6822
|
}
|
|
6659
6823
|
}
|
|
6660
6824
|
var piwiFixtures = {
|
|
6661
|
-
// Auto, worker-scoped: the worker's resource ledger
|
|
6662
|
-
//
|
|
6663
|
-
//
|
|
6664
|
-
//
|
|
6825
|
+
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
6826
|
+
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
6827
|
+
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
6828
|
+
// ledger and records the calls the tests make through it, and torn down after
|
|
6829
|
+
// the browser closed, so its census sees what only the worker's shutdown
|
|
6830
|
+
// closed. Depends on `playwright` alone, which launches nothing.
|
|
6665
6831
|
piwiResources: [
|
|
6666
6832
|
async ({ playwright }, use, workerInfo) => {
|
|
6667
6833
|
boxCaptureFrames();
|
|
6668
6834
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
6835
|
+
observeApiRequests(playwright, apiCallRecorder);
|
|
6669
6836
|
try {
|
|
6670
6837
|
await use();
|
|
6671
6838
|
} finally {
|
|
@@ -6696,12 +6863,13 @@ var piwiFixtures = {
|
|
|
6696
6863
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
6697
6864
|
// requested, so suites that never destructure `page` are still captured.
|
|
6698
6865
|
piwiCapture: [
|
|
6699
|
-
async ({ screenshot }, use, testInfo) => {
|
|
6866
|
+
async ({ screenshot, baseURL }, use, testInfo) => {
|
|
6700
6867
|
boxCaptureFrames();
|
|
6701
6868
|
const conflict = labModeConflict();
|
|
6702
6869
|
if (conflict) throw new Error(conflict);
|
|
6703
6870
|
const sink = createSink();
|
|
6704
6871
|
sink.testInfo = testInfo;
|
|
6872
|
+
sink.baseURL = baseURL ?? null;
|
|
6705
6873
|
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
6706
6874
|
const pauseSet = currentPauseSet();
|
|
6707
6875
|
if (pauseSet.size) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|
|
@@ -19,7 +19,7 @@ Prefer the **Piwi MCP server** if it is connected to this agent (tools named `li
|
|
|
19
19
|
|
|
20
20
|
3. **Group by cause.** Call `get_failure_groups` (or `list_clusters` / `get_cluster`) — failures that share a root cause are clustered, so you fix one thing, not five. Work cluster by cluster.
|
|
21
21
|
|
|
22
|
-
4. **Read the evidence per cluster.** Call `get_cluster_context` — the same SCM-grounded context the built-in diagnosis uses: representative errors, test steps, console logs, failing network requests, ARIA snapshots, and the **diff of files changed since the last green run**. If a diagnosis already exists, `get_cluster_diagnosis` returns its root cause and suggested fix.
|
|
22
|
+
4. **Read the evidence per cluster.** Call `get_cluster_context` — the same SCM-grounded context the built-in diagnosis uses: representative errors, test steps, console logs, failing network requests, ARIA snapshots, and the **diff of files changed since the last green run**. If a diagnosis already exists, `get_cluster_diagnosis` returns its root cause and suggested fix. When the failure has no change behind it and the project records coverage, call `compare_coverage` with a failing execution's id: the code this run took that the last passing run did not (an error path, a fallback) is where to look.
|
|
23
23
|
|
|
24
24
|
5. **Form the diagnosis.** Tie the failure to a cause with evidence: a selector that stopped matching, an assertion on changed copy, a slow/500 endpoint (`get_network_requests`), a race, a genuinely flaky test (check `get_test_stability_trend` — if it fails intermittently, treat it as flaky, not a regression). Point at the specific commit/file from the diff when the evidence supports it.
|
|
25
25
|
|
|
@@ -15,7 +15,7 @@ Prefer the **Piwi MCP server** if it is connected (`list_flaky_tests`, `get_flak
|
|
|
15
15
|
|
|
16
16
|
1. **Rank the flaky tests.** Call `list_flaky_tests` for the project and take the top few by impact. Do not try to fix the whole list at once.
|
|
17
17
|
|
|
18
|
-
2. **Read the suspects.** For a chosen test, call `get_flake_profile`. Each suspect carries raw counts ("slow in 7 of 8 failures and 3 of 44 passes"), the condition that would test it, and, once the lab has run, its latest result (`lab`). `experiments` lists what the lab already found; if an experiment reproduced the test, skip to step 4. Read one failing execution (`get_test_run_case` / `explain_failure`) for the error at the moment it flaked.
|
|
18
|
+
2. **Read the suspects.** For a chosen test, call `get_flake_profile`. Each suspect carries raw counts ("slow in 7 of 8 failures and 3 of 44 passes"), the condition that would test it, and, once the lab has run, its latest result (`lab`). `experiments` lists what the lab already found; if an experiment reproduced the test, skip to step 4. Read one failing execution (`get_test_run_case` / `explain_failure`) for the error at the moment it flaked. A test whose root cause is `order` depends on state an earlier test leaves behind: `compare_coverage` with its `testCaseId` names that test and the code whose path changes, so reproduce by running that test before this one in a single worker, and fix the shared state, not the wait.
|
|
19
19
|
|
|
20
20
|
3. **Reproduce it.** Run the lab on the test (`plan_flake_experiment` gives the exact commands and the arms it will run, with an estimate):
|
|
21
21
|
|
|
@@ -14,7 +14,7 @@ drafts it from the graph, and opens it in the same change.
|
|
|
14
14
|
## How you reach Piwi
|
|
15
15
|
|
|
16
16
|
Prefer the **Piwi MCP server** if it is connected (`list_scenario_gaps`,
|
|
17
|
-
`draft_scenario`, `get_change_coverage`, `triage_gap`). Otherwise use the dashboard's **Gaps**
|
|
17
|
+
`draft_scenario`, `get_change_coverage`, `get_coverage`, `triage_gap`). Otherwise use the dashboard's **Gaps**
|
|
18
18
|
view, or the reporter CLI with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` /
|
|
19
19
|
`PIWI_PROJECT_NAME` set.
|
|
20
20
|
|
|
@@ -38,7 +38,11 @@ view, or the reporter CLI with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` /
|
|
|
38
38
|
skeleton: a title, `piwi:` annotations from the nearest test, the graph path
|
|
39
39
|
from a reached page to the gap as the step list, the catalog page-object
|
|
40
40
|
methods that match the page, and a `TODO` assertion naming what to check.
|
|
41
|
-
Nothing is committed — the skeleton is yours to finish.
|
|
41
|
+
Nothing is committed — the skeleton is yours to finish. When the gap points at
|
|
42
|
+
application code (an uncovered file, a handler, an error path), name the target
|
|
43
|
+
lines in it and call `get_coverage` with `projectId`, `file` and `lines`:
|
|
44
|
+
`nearest` is the existing test running most of the function around them, the
|
|
45
|
+
one whose setup to copy.
|
|
42
46
|
|
|
43
47
|
4. **Finish the assertion.** The draft reaches the subject; you write the check.
|
|
44
48
|
For a false-comfort gap, assert the effect the probe fault suppressed (the row
|
|
@@ -51,11 +55,33 @@ view, or the reporter CLI with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` /
|
|
|
51
55
|
for the right reason: temporarily break the behavior it asserts and see it
|
|
52
56
|
fail. Open it in the same pull request as the change that motivated it.
|
|
53
57
|
|
|
54
|
-
6. **
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
6. **Prove it reaches its target lines.** When the project records server
|
|
59
|
+
coverage (an ASP.NET Core backend in a coverage build), run the new test with
|
|
60
|
+
it on and reporting to Piwi:
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
```bash
|
|
63
|
+
PIWI_COVERAGE_SERVER=lines npx playwright test <file> -g "<title>"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
$env:PIWI_COVERAGE_SERVER='lines'; npx playwright test <file> -g "<title>"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then call `get_coverage` with `projectId`, `testFile`, `testTitle`, `file`
|
|
71
|
+
and `lines` (the target lines). `ran` lists the target lines it executed,
|
|
72
|
+
`notRan` those it did not, `notExecutable` those that cannot run (blank lines,
|
|
73
|
+
comments), and `unknown` those the capture could not tell: run it again, and
|
|
74
|
+
check the backend records `lines`, not only files. While a target line did not
|
|
75
|
+
run, compare your test with `nearest`, change the steps or the data that lead
|
|
76
|
+
to that code, and run again, until `allRan` is true. If `get_coverage` says the
|
|
77
|
+
project records no coverage, skip this step and say so in the report.
|
|
78
|
+
|
|
79
|
+
7. **Report.** State which gap you closed (class, subject, score), how the new
|
|
80
|
+
test asserts the missing behavior, and which target lines it ran. If the gap
|
|
81
|
+
had no reachable path, say so — the draft cannot invent an entry point, and
|
|
82
|
+
the gap may need a fixture.
|
|
83
|
+
|
|
84
|
+
8. **Report back to Piwi.** Tell Piwi what you decided about the gap with
|
|
59
85
|
`triage_gap` (reporter or admin access): `verb: "accept"` for the gap you
|
|
60
86
|
wrote the test for, `covered-by` with the covering test's id when a test you
|
|
61
87
|
found already covers it, or `dismiss` with the reason (`not-worth-testing`,
|