@piwitests/reporter 0.60.0 → 0.61.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 +68 -6
- package/dist/global-setup-module.js +66 -9
- package/dist/index.d.ts +20 -2
- package/dist/index.js +758 -123
- package/dist/internal/capture/attachments.d.ts +2 -0
- package/dist/internal/capture/attachments.js +2 -0
- package/dist/internal/capture/capture-fixtures.js +433 -62
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -203,6 +203,7 @@ var PIWI_ENV_KEYS = {
|
|
|
203
203
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
204
204
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
205
205
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
206
|
+
coverageCss: "PIWI_COVERAGE_CSS",
|
|
206
207
|
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
207
208
|
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
208
209
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
@@ -357,10 +358,14 @@ function resolveOptions(raw, configDir) {
|
|
|
357
358
|
const coverage = { ...mergedRaw.coverage };
|
|
358
359
|
const coverageBrowser = env[PIWI_ENV_KEYS.coverageBrowser];
|
|
359
360
|
const coverageServer = env[PIWI_ENV_KEYS.coverageServer];
|
|
361
|
+
const coverageCss = env[PIWI_ENV_KEYS.coverageCss];
|
|
360
362
|
if (coverage.browser === void 0 && coverageBrowser)
|
|
361
363
|
coverage.browser = coverageBrowser;
|
|
362
364
|
if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
|
|
363
|
-
if (coverage.
|
|
365
|
+
if (coverage.css === void 0 && coverageCss) coverage.css = coverageCss;
|
|
366
|
+
if (coverage.browser !== void 0 || coverage.server !== void 0 || coverage.css !== void 0) {
|
|
367
|
+
mergedRaw.coverage = coverage;
|
|
368
|
+
}
|
|
364
369
|
const externalId = { ...mergedRaw.externalId };
|
|
365
370
|
const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
|
|
366
371
|
const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
|
|
@@ -406,9 +411,11 @@ function applyOptionsToEnv(options) {
|
|
|
406
411
|
if (options.collectPerformanceMetrics === false) {
|
|
407
412
|
env[PIWI_ENV_KEYS.coverageBrowser] = "off";
|
|
408
413
|
env[PIWI_ENV_KEYS.coverageServer] = "off";
|
|
414
|
+
env[PIWI_ENV_KEYS.coverageCss] = "off";
|
|
409
415
|
} else {
|
|
410
416
|
if (options.coverage?.browser !== void 0) env[PIWI_ENV_KEYS.coverageBrowser] = options.coverage.browser;
|
|
411
417
|
if (options.coverage?.server !== void 0) env[PIWI_ENV_KEYS.coverageServer] = options.coverage.server;
|
|
418
|
+
if (options.coverage?.css !== void 0) env[PIWI_ENV_KEYS.coverageCss] = options.coverage.css;
|
|
412
419
|
}
|
|
413
420
|
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
414
421
|
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
@@ -1312,6 +1319,8 @@ var ATTACHMENT_NAMES = {
|
|
|
1312
1319
|
codeReach: "piwi-code-reach",
|
|
1313
1320
|
browserCoverage: "piwi-browser-coverage",
|
|
1314
1321
|
browserFunctions: "piwi-browser-functions",
|
|
1322
|
+
cssCoverage: "piwi-css-coverage",
|
|
1323
|
+
cssRules: "piwi-css-rules",
|
|
1315
1324
|
serverCoverageRequests: "piwi-server-coverage-requests",
|
|
1316
1325
|
webVitals: "piwi-web-vitals",
|
|
1317
1326
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
@@ -1478,10 +1487,10 @@ var FileHandler = class {
|
|
|
1478
1487
|
/** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`, `piwi-aria-snapshot-json`) into structured fields on the test case */
|
|
1479
1488
|
parsePerformanceAttachments(testCase, attachments) {
|
|
1480
1489
|
const find = (name) => attachments.find((a) => a.name === name);
|
|
1481
|
-
const
|
|
1482
|
-
if (
|
|
1490
|
+
const net3 = find(ATTACHMENT_NAMES.network);
|
|
1491
|
+
if (net3?.body) {
|
|
1483
1492
|
try {
|
|
1484
|
-
testCase.networkRequests = JSON.parse(
|
|
1493
|
+
testCase.networkRequests = JSON.parse(net3.body.toString());
|
|
1485
1494
|
} catch {
|
|
1486
1495
|
}
|
|
1487
1496
|
}
|
|
@@ -2119,6 +2128,8 @@ var StreamManager = class {
|
|
|
2119
2128
|
this._token = null;
|
|
2120
2129
|
this._auth = null;
|
|
2121
2130
|
this._startPromise = null;
|
|
2131
|
+
/** Called when the dashboard says someone opened the run. */
|
|
2132
|
+
this.onWatched = null;
|
|
2122
2133
|
const maxBytes = this.options.maxStreamBufferBytes ?? 0;
|
|
2123
2134
|
this.pendingEvents = new BoundedEventQueue(maxBytes);
|
|
2124
2135
|
this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
|
|
@@ -2143,6 +2154,10 @@ var StreamManager = class {
|
|
|
2143
2154
|
get startPromise() {
|
|
2144
2155
|
return this._startPromise;
|
|
2145
2156
|
}
|
|
2157
|
+
/** Whether anyone has the run open live, as the dashboard last said; `null` until it says. */
|
|
2158
|
+
get isWatched() {
|
|
2159
|
+
return this.watched;
|
|
2160
|
+
}
|
|
2146
2161
|
/**
|
|
2147
2162
|
* Whether a test-result (`complete`) event never reached the server through
|
|
2148
2163
|
* the live stream: shed under buffer pressure, too large for any request the
|
|
@@ -2393,8 +2408,11 @@ var StreamManager = class {
|
|
|
2393
2408
|
if (this.pendingEvents.length > 0) this.flush();
|
|
2394
2409
|
}, delay);
|
|
2395
2410
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2411
|
+
/**
|
|
2412
|
+
* Take the dashboard's answer on whether anyone watches the run, from any
|
|
2413
|
+
* request of the stream. An answer without the flag (an older dashboard)
|
|
2414
|
+
* keeps step events flowing.
|
|
2415
|
+
*/
|
|
2398
2416
|
noteWatched(response) {
|
|
2399
2417
|
const watched = response?.watched;
|
|
2400
2418
|
const next = typeof watched === "boolean" ? watched : null;
|
|
@@ -2403,7 +2421,9 @@ var StreamManager = class {
|
|
|
2403
2421
|
next ? "The run is open on the dashboard: streaming live steps." : "Nobody is watching the run: live steps are not sent."
|
|
2404
2422
|
);
|
|
2405
2423
|
}
|
|
2424
|
+
const opened = next === true && this.watched !== true;
|
|
2406
2425
|
this.watched = next;
|
|
2426
|
+
if (opened) this.onWatched?.();
|
|
2407
2427
|
}
|
|
2408
2428
|
// Idle time after which a heartbeat goes out.
|
|
2409
2429
|
heartbeatDelay() {
|
|
@@ -3903,7 +3923,7 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
|
3903
3923
|
const maxFrames = opts.maxFrames ?? 4;
|
|
3904
3924
|
const picked = [];
|
|
3905
3925
|
const seen = /* @__PURE__ */ new Set();
|
|
3906
|
-
const
|
|
3926
|
+
const add2 = (absFile, line) => {
|
|
3907
3927
|
const key = `${absFile}:${line}`;
|
|
3908
3928
|
if (seen.has(key)) return;
|
|
3909
3929
|
seen.add(key);
|
|
@@ -3920,12 +3940,12 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
|
3920
3940
|
}
|
|
3921
3941
|
const rel = path11.relative(projectRoot, abs);
|
|
3922
3942
|
if (rel.startsWith("..") || path11.isAbsolute(rel)) continue;
|
|
3923
|
-
if (!isNaN(frame.line))
|
|
3943
|
+
if (!isNaN(frame.line)) add2(abs, frame.line);
|
|
3924
3944
|
}
|
|
3925
3945
|
}
|
|
3926
3946
|
const absTest = path11.resolve(testFile2);
|
|
3927
3947
|
if (!picked.some((p) => p.absFile === absTest)) {
|
|
3928
|
-
|
|
3948
|
+
add2(absTest, extractFailingLine(errorText, testFile2, declLine));
|
|
3929
3949
|
}
|
|
3930
3950
|
const frames = [];
|
|
3931
3951
|
for (const p of picked.slice(0, maxFrames)) {
|
|
@@ -4085,6 +4105,7 @@ function isDueForAriaSample(testInfo) {
|
|
|
4085
4105
|
}
|
|
4086
4106
|
|
|
4087
4107
|
// src/internal/support/base-url-check.ts
|
|
4108
|
+
var net = __toESM(require("net"));
|
|
4088
4109
|
var BASE_URL_CHECK_ATTEMPTS = 3;
|
|
4089
4110
|
var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
|
|
4090
4111
|
var BASE_URL_CHECK_DELAY_MS = 2e3;
|
|
@@ -4106,9 +4127,9 @@ function cliProjectNames(argv = process.argv) {
|
|
|
4106
4127
|
}
|
|
4107
4128
|
return names;
|
|
4108
4129
|
}
|
|
4109
|
-
function wildcard(pattern) {
|
|
4130
|
+
function wildcard(pattern, flags = "i") {
|
|
4110
4131
|
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
4111
|
-
return new RegExp(`^${escaped.join(".*")}$`,
|
|
4132
|
+
return new RegExp(`^${escaped.join(".*")}$`, flags);
|
|
4112
4133
|
}
|
|
4113
4134
|
function selectedProjects(projects, argv = process.argv, filtered) {
|
|
4114
4135
|
let chosen;
|
|
@@ -4118,7 +4139,7 @@ function selectedProjects(projects, argv = process.argv, filtered) {
|
|
|
4118
4139
|
} else {
|
|
4119
4140
|
const names = cliProjectNames(argv);
|
|
4120
4141
|
if (!names || names.length === 0) return projects;
|
|
4121
|
-
const patterns = names.map(wildcard);
|
|
4142
|
+
const patterns = names.map((name) => wildcard(name));
|
|
4122
4143
|
chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
4123
4144
|
}
|
|
4124
4145
|
if (!argv.includes("--no-deps")) {
|
|
@@ -4140,23 +4161,58 @@ function asProxy(value) {
|
|
|
4140
4161
|
const proxy = value;
|
|
4141
4162
|
return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
|
|
4142
4163
|
}
|
|
4143
|
-
function
|
|
4164
|
+
function exposeNetworkCovers(rule, url) {
|
|
4165
|
+
if (!rule) return false;
|
|
4166
|
+
let parsed2;
|
|
4167
|
+
try {
|
|
4168
|
+
parsed2 = new URL(url);
|
|
4169
|
+
} catch {
|
|
4170
|
+
return false;
|
|
4171
|
+
}
|
|
4172
|
+
const host = parsed2.hostname;
|
|
4173
|
+
const port = parsed2.port ? Number(parsed2.port) : parsed2.protocol === "https:" ? 443 : 80;
|
|
4174
|
+
return rule.split(",").some((token) => {
|
|
4175
|
+
const match = token.match(/^(.*?)(?::(\d+))?$/);
|
|
4176
|
+
if (!match) return false;
|
|
4177
|
+
if (match[2] !== void 0 && Number(match[2]) !== port) return false;
|
|
4178
|
+
const tokenHost = match[1];
|
|
4179
|
+
if (tokenHost === "<loopback>") {
|
|
4180
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]";
|
|
4181
|
+
}
|
|
4182
|
+
if (tokenHost === "*") return true;
|
|
4183
|
+
if (net.isIP(tokenHost)) return host === tokenHost;
|
|
4184
|
+
if (net.isIP(host)) return false;
|
|
4185
|
+
return wildcard(tokenHost.startsWith(".") ? `*${tokenHost}` : tokenHost, "").test(host);
|
|
4186
|
+
});
|
|
4187
|
+
}
|
|
4188
|
+
function reachedRemotely(use, url, env) {
|
|
4189
|
+
if (use?.connectOptions === void 0) {
|
|
4190
|
+
return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
|
|
4191
|
+
}
|
|
4192
|
+
const connect2 = use.connectOptions;
|
|
4193
|
+
if (!connect2 || typeof connect2 !== "object" || typeof connect2.wsEndpoint !== "string") return false;
|
|
4194
|
+
return !exposeNetworkCovers(typeof connect2.exposeNetwork === "string" ? connect2.exposeNetwork : void 0, url);
|
|
4195
|
+
}
|
|
4196
|
+
function baseUrlTargets(config, argv = process.argv, env = process.env) {
|
|
4144
4197
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
4145
4198
|
const filtered = config.projects?.length ? config.filteredProjects : void 0;
|
|
4146
4199
|
const targets = /* @__PURE__ */ new Map();
|
|
4147
4200
|
for (const project of selectedProjects(projects, argv, filtered)) {
|
|
4148
4201
|
const url = project.use?.baseURL;
|
|
4149
4202
|
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
4203
|
+
const remoteBrowser = reachedRemotely(project.use, url, env);
|
|
4150
4204
|
const target = targets.get(url);
|
|
4151
4205
|
if (target) {
|
|
4152
4206
|
if (project.name) target.projects.push(project.name);
|
|
4207
|
+
target.remoteBrowser ||= remoteBrowser;
|
|
4153
4208
|
continue;
|
|
4154
4209
|
}
|
|
4155
4210
|
targets.set(url, {
|
|
4156
4211
|
url,
|
|
4157
4212
|
projects: project.name ? [project.name] : [],
|
|
4158
4213
|
ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
|
|
4159
|
-
proxy: asProxy(project.use?.proxy)
|
|
4214
|
+
proxy: asProxy(project.use?.proxy),
|
|
4215
|
+
remoteBrowser
|
|
4160
4216
|
});
|
|
4161
4217
|
}
|
|
4162
4218
|
return [...targets.values()];
|
|
@@ -4197,14 +4253,21 @@ async function checkBaseUrl(target, options = {}) {
|
|
|
4197
4253
|
}
|
|
4198
4254
|
return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
|
|
4199
4255
|
}
|
|
4256
|
+
function remoteBrowserNote(target) {
|
|
4257
|
+
if (!target.remoteBrowser) return null;
|
|
4258
|
+
const { host } = new URL(target.url);
|
|
4259
|
+
return `The check ran from this machine's network, but a project's remote browser reaches ${host} from its own, since its exposeNetwork rule does not cover that address.`;
|
|
4260
|
+
}
|
|
4200
4261
|
async function checkBaseUrls(config, options = {}) {
|
|
4201
|
-
const targets = baseUrlTargets(config, options.argv);
|
|
4262
|
+
const targets = baseUrlTargets(config, options.argv, options.env);
|
|
4202
4263
|
const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
|
|
4203
4264
|
const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
|
|
4204
4265
|
if (down.length === 0) return targets;
|
|
4205
4266
|
const lines = down.map(({ target, reason }) => {
|
|
4206
4267
|
const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
|
|
4207
|
-
|
|
4268
|
+
const note = remoteBrowserNote(target);
|
|
4269
|
+
return ` ${target.url}${used}: ${reason}${note ? `
|
|
4270
|
+
${note}` : ""}`;
|
|
4208
4271
|
});
|
|
4209
4272
|
const message = [
|
|
4210
4273
|
`[Piwi Dashboard] The app under test did not answer, so no test ran:`,
|
|
@@ -4709,7 +4772,7 @@ function instanceOf(answer) {
|
|
|
4709
4772
|
return { id: instance.id, startedAt };
|
|
4710
4773
|
}
|
|
4711
4774
|
function processKeyOf(id) {
|
|
4712
|
-
const dot = id.
|
|
4775
|
+
const dot = id.indexOf(".");
|
|
4713
4776
|
return dot > 0 ? id.slice(0, dot) : null;
|
|
4714
4777
|
}
|
|
4715
4778
|
function withTestCoverage(files, tests) {
|
|
@@ -5127,7 +5190,16 @@ function createGlobalSetup(options, userSetup) {
|
|
|
5127
5190
|
}
|
|
5128
5191
|
if (opts.checkBaseUrl === true) {
|
|
5129
5192
|
const checked = await checkBaseUrls(config ?? {});
|
|
5130
|
-
for (const target of checked)
|
|
5193
|
+
for (const target of checked) {
|
|
5194
|
+
const note = remoteBrowserNote(target);
|
|
5195
|
+
if (note) {
|
|
5196
|
+
logger.info(
|
|
5197
|
+
`Base URL answered: ${target.url}. ${note} A navigation failure from that browser can mean its machine cannot reach the app, not that the app is down.`
|
|
5198
|
+
);
|
|
5199
|
+
} else {
|
|
5200
|
+
logger.debug(`Base URL answered: ${target.url}`);
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5131
5203
|
}
|
|
5132
5204
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
5133
5205
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
@@ -8446,6 +8518,12 @@ async function dirBytes(dir, budget) {
|
|
|
8446
8518
|
}
|
|
8447
8519
|
return bytes;
|
|
8448
8520
|
}
|
|
8521
|
+
function memoryOf(container) {
|
|
8522
|
+
const totalMemory = os8.totalmem();
|
|
8523
|
+
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
|
|
8524
|
+
const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
|
|
8525
|
+
return { memoryBytes: totalMemory, memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null };
|
|
8526
|
+
}
|
|
8449
8527
|
var RunSampler = class {
|
|
8450
8528
|
constructor(options = {}) {
|
|
8451
8529
|
this.timer = null;
|
|
@@ -8514,6 +8592,18 @@ var RunSampler = class {
|
|
|
8514
8592
|
}
|
|
8515
8593
|
return this.profile(await this.leftoverBytes());
|
|
8516
8594
|
}
|
|
8595
|
+
/**
|
|
8596
|
+
* What a live run's memory is drawn against: the machine's memory, or its
|
|
8597
|
+
* container's limit when lower, read once, and how the run's memory is
|
|
8598
|
+
* measured so far.
|
|
8599
|
+
*/
|
|
8600
|
+
liveMemory() {
|
|
8601
|
+
if (this.capacityBytes === void 0) {
|
|
8602
|
+
const machine = memoryOf(this.linux ? this.cgroup.read() : null);
|
|
8603
|
+
this.capacityBytes = machine.memoryLimitBytes ?? machine.memoryBytes;
|
|
8604
|
+
}
|
|
8605
|
+
return { capacityBytes: this.capacityBytes || null, kind: this.memoryKind };
|
|
8606
|
+
}
|
|
8517
8607
|
/** The series read so far, each point at the time it was read. */
|
|
8518
8608
|
timedSeries() {
|
|
8519
8609
|
return {
|
|
@@ -8810,9 +8900,6 @@ var RunSampler = class {
|
|
|
8810
8900
|
if (cpuPressure === null) notMeasured.push("CPU pressure");
|
|
8811
8901
|
const container = this.linux ? this.cgroup.read() : null;
|
|
8812
8902
|
const startContainer = this.containerStart;
|
|
8813
|
-
const totalMemory = os8.totalmem();
|
|
8814
|
-
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
|
|
8815
|
-
const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
|
|
8816
8903
|
const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
|
|
8817
8904
|
const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
|
|
8818
8905
|
return {
|
|
@@ -8820,8 +8907,7 @@ var RunSampler = class {
|
|
|
8820
8907
|
wallMs,
|
|
8821
8908
|
machine: {
|
|
8822
8909
|
cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
|
|
8823
|
-
|
|
8824
|
-
memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
|
|
8910
|
+
...memoryOf(container),
|
|
8825
8911
|
cpuQuotaCores: container?.cpuQuotaCores ?? null
|
|
8826
8912
|
},
|
|
8827
8913
|
cpu: {
|
|
@@ -9093,7 +9179,7 @@ function openPagesByWorker(censuses) {
|
|
|
9093
9179
|
}
|
|
9094
9180
|
return out;
|
|
9095
9181
|
}
|
|
9096
|
-
function pagesOverTime(censuses) {
|
|
9182
|
+
function pagesOverTime(censuses, live = false) {
|
|
9097
9183
|
const byWorker = /* @__PURE__ */ new Map();
|
|
9098
9184
|
for (const census of censuses) {
|
|
9099
9185
|
const list = byWorker.get(census.worker) ?? [];
|
|
@@ -9126,7 +9212,7 @@ function pagesOverTime(censuses) {
|
|
|
9126
9212
|
if (last && last[0] === at) last[1] = open;
|
|
9127
9213
|
else events.push([at, open]);
|
|
9128
9214
|
}
|
|
9129
|
-
if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
|
|
9215
|
+
if (!live && open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
|
|
9130
9216
|
out.push({ worker, events });
|
|
9131
9217
|
}
|
|
9132
9218
|
return out;
|
|
@@ -9195,6 +9281,144 @@ function resourceReportWire(input) {
|
|
|
9195
9281
|
};
|
|
9196
9282
|
}
|
|
9197
9283
|
|
|
9284
|
+
// src/internal/streaming/live-resources.ts
|
|
9285
|
+
var LIVE_INTERVAL_MS = 5e3;
|
|
9286
|
+
function since(points, cursor, cap, merge) {
|
|
9287
|
+
const fresh = cursor === void 0 ? points : points.filter(([at]) => at > cursor);
|
|
9288
|
+
return downsamplePoints(fresh, cap, merge);
|
|
9289
|
+
}
|
|
9290
|
+
var LiveResources = class {
|
|
9291
|
+
constructor(stream, httpClient, options, logger = new Logger()) {
|
|
9292
|
+
this.stream = stream;
|
|
9293
|
+
this.httpClient = httpClient;
|
|
9294
|
+
this.options = options;
|
|
9295
|
+
this.logger = logger;
|
|
9296
|
+
this.timer = null;
|
|
9297
|
+
this.inFlight = null;
|
|
9298
|
+
this.stopped = false;
|
|
9299
|
+
/** Whether the next post must carry every point: the first one, and after the dashboard lost them. */
|
|
9300
|
+
this.reset = true;
|
|
9301
|
+
/** The time of the last point the dashboard took, per series. */
|
|
9302
|
+
this.sent = /* @__PURE__ */ new Map();
|
|
9303
|
+
/** How many censuses the open pages last posted came from. */
|
|
9304
|
+
this.censusCount = 0;
|
|
9305
|
+
}
|
|
9306
|
+
/** Post on a timer that never keeps the process alive. */
|
|
9307
|
+
start() {
|
|
9308
|
+
if (this.timer || this.stopped) return;
|
|
9309
|
+
this.timer = setInterval(() => this.tick(), this.options.intervalMs ?? LIVE_INTERVAL_MS);
|
|
9310
|
+
this.timer.unref?.();
|
|
9311
|
+
}
|
|
9312
|
+
/** Post nothing more, and wait for a post in flight. */
|
|
9313
|
+
async stop() {
|
|
9314
|
+
this.stopped = true;
|
|
9315
|
+
if (this.timer) clearInterval(this.timer);
|
|
9316
|
+
this.timer = null;
|
|
9317
|
+
await this.inFlight;
|
|
9318
|
+
}
|
|
9319
|
+
/** Post now, when the run is watched and no post is in flight: the dashboard says someone just opened it. */
|
|
9320
|
+
tick() {
|
|
9321
|
+
if (this.stopped || this.inFlight) return null;
|
|
9322
|
+
const { enabled, runId, token } = this.stream;
|
|
9323
|
+
if (!enabled || runId === null || !token || this.stream.isWatched !== true) return null;
|
|
9324
|
+
let draft;
|
|
9325
|
+
try {
|
|
9326
|
+
draft = this.draft();
|
|
9327
|
+
} catch (error) {
|
|
9328
|
+
this.logger.debug(`Live resources not read: ${errorMessage(error)}`);
|
|
9329
|
+
return null;
|
|
9330
|
+
}
|
|
9331
|
+
if (!draft) return null;
|
|
9332
|
+
this.inFlight = this.send(runId, token, draft).finally(() => {
|
|
9333
|
+
this.inFlight = null;
|
|
9334
|
+
});
|
|
9335
|
+
return this.inFlight;
|
|
9336
|
+
}
|
|
9337
|
+
/** What is new since the last post the dashboard took; null when nothing is. */
|
|
9338
|
+
draft() {
|
|
9339
|
+
const samples = this.options.sampler?.timedSeries() ?? null;
|
|
9340
|
+
const startedAt = samples?.startedAt || this.options.runStartedAt;
|
|
9341
|
+
const censuses = this.options.censuses();
|
|
9342
|
+
const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
|
|
9343
|
+
const sent = new Map(cursors);
|
|
9344
|
+
let fresh = false;
|
|
9345
|
+
const take = (key, points, cap, merge) => {
|
|
9346
|
+
const out = since(points, cursors.get(key), cap, merge);
|
|
9347
|
+
const last = points[points.length - 1];
|
|
9348
|
+
if (out.length > 0 && last) {
|
|
9349
|
+
sent.set(key, last[0]);
|
|
9350
|
+
fresh = true;
|
|
9351
|
+
}
|
|
9352
|
+
return out;
|
|
9353
|
+
};
|
|
9354
|
+
const timeline = { startedAt, cpuPct: [], memoryBytes: [], pages: [], workers: [] };
|
|
9355
|
+
if (samples) {
|
|
9356
|
+
timeline.cpuPct = take("cpu", samples.cpuPct, TIMELINE_SERIES_CAP, "mean");
|
|
9357
|
+
timeline.memoryBytes = take("memory", samples.memoryBytes, TIMELINE_SERIES_CAP, "max");
|
|
9358
|
+
for (const tree of workerTreesByIndex(samples, censuses)) {
|
|
9359
|
+
const cpuCores = take(`cores:${tree.worker}`, tree.cpuCores, TIMELINE_WORKER_POINTS_CAP, "mean");
|
|
9360
|
+
const memoryBytes = take(`bytes:${tree.worker}`, tree.memoryBytes, TIMELINE_WORKER_POINTS_CAP, "max");
|
|
9361
|
+
if (cpuCores.length > 0 || memoryBytes.length > 0) {
|
|
9362
|
+
timeline.workers.push({ worker: tree.worker, cpuCores, memoryBytes });
|
|
9363
|
+
}
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
if (this.reset || censuses.length !== this.censusCount) {
|
|
9367
|
+
for (const { worker, events } of pagesOverTime(censuses, true)) {
|
|
9368
|
+
const points = events.map(([at, open]) => [at - startedAt, open]);
|
|
9369
|
+
const last = points.pop();
|
|
9370
|
+
timeline.pages.push({
|
|
9371
|
+
worker,
|
|
9372
|
+
points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last]
|
|
9373
|
+
});
|
|
9374
|
+
}
|
|
9375
|
+
fresh ||= timeline.pages.length > 0;
|
|
9376
|
+
}
|
|
9377
|
+
if (!fresh) return null;
|
|
9378
|
+
const memory = this.options.sampler?.liveMemory() ?? { capacityBytes: null, kind: null };
|
|
9379
|
+
return {
|
|
9380
|
+
post: {
|
|
9381
|
+
shardIndex: this.options.shardIndex,
|
|
9382
|
+
reset: this.reset,
|
|
9383
|
+
timeline,
|
|
9384
|
+
memoryCapacityBytes: memory.capacityBytes,
|
|
9385
|
+
memoryKind: memory.kind
|
|
9386
|
+
},
|
|
9387
|
+
sent,
|
|
9388
|
+
censusCount: censuses.length
|
|
9389
|
+
};
|
|
9390
|
+
}
|
|
9391
|
+
async send(runId, token, draft) {
|
|
9392
|
+
try {
|
|
9393
|
+
const answer = await this.httpClient.postJSON(
|
|
9394
|
+
`/api/test-runs/${runId}/live-resources`,
|
|
9395
|
+
{ streamToken: token, ...draft.post },
|
|
9396
|
+
this.stream.auth
|
|
9397
|
+
);
|
|
9398
|
+
if (answer?.declined) {
|
|
9399
|
+
this.logger.debug("The project declined resources: live resources are not sent.");
|
|
9400
|
+
this.stopped = true;
|
|
9401
|
+
return;
|
|
9402
|
+
}
|
|
9403
|
+
this.stream.noteWatched(answer);
|
|
9404
|
+
if (answer?.resync) {
|
|
9405
|
+
this.reset = true;
|
|
9406
|
+
return;
|
|
9407
|
+
}
|
|
9408
|
+
this.sent = draft.sent;
|
|
9409
|
+
this.censusCount = draft.censusCount;
|
|
9410
|
+
this.reset = false;
|
|
9411
|
+
} catch (error) {
|
|
9412
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
9413
|
+
this.logger.debug("The dashboard takes no live resources.");
|
|
9414
|
+
this.stopped = true;
|
|
9415
|
+
return;
|
|
9416
|
+
}
|
|
9417
|
+
this.logger.debug(`Live resources not sent: ${errorMessage(error)}`);
|
|
9418
|
+
}
|
|
9419
|
+
}
|
|
9420
|
+
};
|
|
9421
|
+
|
|
9198
9422
|
// ../core/src/describe-failure.ts
|
|
9199
9423
|
var HEADLINE_MAX_CHARS = 120;
|
|
9200
9424
|
var VALUE_MAX_CHARS = 40;
|
|
@@ -9724,10 +9948,14 @@ function isServerGranularity(value) {
|
|
|
9724
9948
|
return typeof value === "string" && SERVER_VALUES.has(value);
|
|
9725
9949
|
}
|
|
9726
9950
|
function coverageSettings(options) {
|
|
9727
|
-
if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off" };
|
|
9951
|
+
if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off", css: "off" };
|
|
9728
9952
|
const browser = options.coverage?.browser === "functions" ? "functions" : options.coverage?.browser === "files" || options.captureCodeReach === true ? "files" : "off";
|
|
9729
9953
|
const server = isServerGranularity(options.coverage?.server) ? options.coverage.server : "off";
|
|
9730
|
-
|
|
9954
|
+
const css = options.coverage?.css === "rules" ? "rules" : "off";
|
|
9955
|
+
return { browser, server, css };
|
|
9956
|
+
}
|
|
9957
|
+
function cssCoverageInWorker() {
|
|
9958
|
+
return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
|
|
9731
9959
|
}
|
|
9732
9960
|
function browserCoverageInWorker() {
|
|
9733
9961
|
const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
|
|
@@ -9847,10 +10075,10 @@ function joinRoot(root, source) {
|
|
|
9847
10075
|
function decodeLines(raw) {
|
|
9848
10076
|
const sources = [];
|
|
9849
10077
|
const decoded = [];
|
|
9850
|
-
const
|
|
10078
|
+
const add2 = (map, lineOffset, columnOffset) => {
|
|
9851
10079
|
if (map.sections) {
|
|
9852
10080
|
for (const section of map.sections) {
|
|
9853
|
-
if (!
|
|
10081
|
+
if (!add2(section.map, section.offset.line, section.offset.column)) return false;
|
|
9854
10082
|
}
|
|
9855
10083
|
return true;
|
|
9856
10084
|
}
|
|
@@ -9871,7 +10099,7 @@ function decodeLines(raw) {
|
|
|
9871
10099
|
});
|
|
9872
10100
|
return true;
|
|
9873
10101
|
};
|
|
9874
|
-
return
|
|
10102
|
+
return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
|
|
9875
10103
|
}
|
|
9876
10104
|
function compactSegments(lines) {
|
|
9877
10105
|
let total = 0;
|
|
@@ -10047,7 +10275,7 @@ function functionLines(entry, map, text, all) {
|
|
|
10047
10275
|
const read = textOf(entry, text);
|
|
10048
10276
|
const ranges = executedFunctionRanges(entry, read?.length, all);
|
|
10049
10277
|
if (!ranges.length || !read) return out;
|
|
10050
|
-
const
|
|
10278
|
+
const add2 = (source, range) => {
|
|
10051
10279
|
const list = out.get(source);
|
|
10052
10280
|
if (list) list.push(range);
|
|
10053
10281
|
else out.set(source, [range]);
|
|
@@ -10057,7 +10285,7 @@ function functionLines(entry, map, text, all) {
|
|
|
10057
10285
|
const start = offsetToPosition(starts, range.start);
|
|
10058
10286
|
if (!map) {
|
|
10059
10287
|
const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
|
|
10060
|
-
|
|
10288
|
+
add2(-1, [start.line + 1, last.line + 1]);
|
|
10061
10289
|
continue;
|
|
10062
10290
|
}
|
|
10063
10291
|
const at = map.originalAt(start.line, start.column);
|
|
@@ -10067,7 +10295,7 @@ function functionLines(entry, map, text, all) {
|
|
|
10067
10295
|
if (first === null) continue;
|
|
10068
10296
|
const header = first === at.line || first === at.line + 1 ? at.line : first;
|
|
10069
10297
|
const lines = map.originalLinesBetween(at.source, start, end, header);
|
|
10070
|
-
if (lines)
|
|
10298
|
+
if (lines) add2(at.source, [header + 1, lines[1] + 1]);
|
|
10071
10299
|
}
|
|
10072
10300
|
return out;
|
|
10073
10301
|
}
|
|
@@ -10261,7 +10489,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
10261
10489
|
if (listed) listed.push(...functions);
|
|
10262
10490
|
else into.set(path34, [...functions]);
|
|
10263
10491
|
};
|
|
10264
|
-
const
|
|
10492
|
+
const add2 = (file, functions) => addTo(files, file, functions);
|
|
10265
10493
|
const bases = viteBases(entries);
|
|
10266
10494
|
const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
|
|
10267
10495
|
const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
|
|
@@ -10288,14 +10516,14 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
10288
10516
|
const text = textOf2(covered, resolution);
|
|
10289
10517
|
if (resolution.lookup) {
|
|
10290
10518
|
const reached = reachedFunctionLines(covered, resolution.lookup, text);
|
|
10291
|
-
if (reached.size > 0)
|
|
10519
|
+
if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
|
|
10292
10520
|
} else if (scriptRan(covered, text)) {
|
|
10293
|
-
|
|
10521
|
+
add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
|
|
10294
10522
|
}
|
|
10295
10523
|
} else if (resolution.kind === "map") {
|
|
10296
10524
|
for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
|
|
10297
10525
|
const file = mapFile(resolution, index);
|
|
10298
|
-
if (file)
|
|
10526
|
+
if (file) add2(file, functions);
|
|
10299
10527
|
}
|
|
10300
10528
|
}
|
|
10301
10529
|
if (!withKnown || resolution.kind === "none") continue;
|
|
@@ -10374,10 +10602,13 @@ async function markCurrentDocument(page, cdp) {
|
|
|
10374
10602
|
}
|
|
10375
10603
|
function onPaused(page, capture, event) {
|
|
10376
10604
|
const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
|
|
10377
|
-
const read = leaving ?
|
|
10605
|
+
const read = leaving ? Promise.all([
|
|
10606
|
+
capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
|
|
10607
|
+
capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
|
|
10608
|
+
]) : null;
|
|
10378
10609
|
if (read) {
|
|
10379
10610
|
const merging = read.then(
|
|
10380
|
-
(coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
|
|
10611
|
+
([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
|
|
10381
10612
|
);
|
|
10382
10613
|
capture.reading.add(merging);
|
|
10383
10614
|
void merging.finally(() => capture.reading.delete(merging));
|
|
@@ -10391,7 +10622,7 @@ function versionKey(url, hash) {
|
|
|
10391
10622
|
return `${hash} ${url}`;
|
|
10392
10623
|
}
|
|
10393
10624
|
function onScriptParsed(page, capture, event) {
|
|
10394
|
-
if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
10625
|
+
if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
10395
10626
|
capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
|
|
10396
10627
|
if (NEVER_REACHED.test(event.url)) return;
|
|
10397
10628
|
const key = versionKey(event.url, event.hash);
|
|
@@ -10420,6 +10651,53 @@ function readCoverage(page, cdp) {
|
|
|
10420
10651
|
);
|
|
10421
10652
|
return Promise.race([taken, late]).finally(() => clearTimeout(timer));
|
|
10422
10653
|
}
|
|
10654
|
+
function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
|
|
10655
|
+
if (header.isConstructed) return;
|
|
10656
|
+
const sheet = {
|
|
10657
|
+
sourceURL: header.sourceURL,
|
|
10658
|
+
...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
|
|
10659
|
+
isInline: header.isInline,
|
|
10660
|
+
startLine: header.startLine,
|
|
10661
|
+
pageUrl,
|
|
10662
|
+
used: /* @__PURE__ */ new Map()
|
|
10663
|
+
};
|
|
10664
|
+
css.sheets.set(header.styleSheetId, sheet);
|
|
10665
|
+
const reading = (async () => {
|
|
10666
|
+
try {
|
|
10667
|
+
if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
|
|
10668
|
+
const { node } = await internalCall(
|
|
10669
|
+
page,
|
|
10670
|
+
() => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
|
|
10671
|
+
);
|
|
10672
|
+
const attributes = node.attributes ?? [];
|
|
10673
|
+
const at = attributes.indexOf("data-vite-dev-id");
|
|
10674
|
+
if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
|
|
10675
|
+
}
|
|
10676
|
+
if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
|
|
10677
|
+
const { text } = await internalCall(
|
|
10678
|
+
page,
|
|
10679
|
+
() => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
|
|
10680
|
+
);
|
|
10681
|
+
sheet.text = text;
|
|
10682
|
+
} catch {
|
|
10683
|
+
}
|
|
10684
|
+
})();
|
|
10685
|
+
css.reading.add(reading);
|
|
10686
|
+
void reading.finally(() => css.reading.delete(reading));
|
|
10687
|
+
}
|
|
10688
|
+
async function readCssUsage(page, cdp, css) {
|
|
10689
|
+
try {
|
|
10690
|
+
const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
|
|
10691
|
+
for (const rule of coverage) {
|
|
10692
|
+
if (!rule.used) continue;
|
|
10693
|
+
css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
|
|
10694
|
+
startOffset: rule.startOffset,
|
|
10695
|
+
endOffset: rule.endOffset
|
|
10696
|
+
});
|
|
10697
|
+
}
|
|
10698
|
+
} catch {
|
|
10699
|
+
}
|
|
10700
|
+
}
|
|
10423
10701
|
function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
|
|
10424
10702
|
for (const covered of read) {
|
|
10425
10703
|
const script = scripts.get(covered.scriptId);
|
|
@@ -10454,11 +10732,15 @@ async function detach(page, cdp) {
|
|
|
10454
10732
|
}
|
|
10455
10733
|
async function startCodeReach(page) {
|
|
10456
10734
|
if (!isChromiumPage(page) || captures.has(page)) return false;
|
|
10735
|
+
const withCss = cssCoverageInWorker();
|
|
10736
|
+
const js = browserCoverageInWorker() || !withCss;
|
|
10457
10737
|
let cdp = null;
|
|
10458
10738
|
try {
|
|
10459
10739
|
cdp = await internalCall(page, () => page.context().newCDPSession(page));
|
|
10460
10740
|
const capture = {
|
|
10461
10741
|
cdp,
|
|
10742
|
+
js,
|
|
10743
|
+
css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
|
|
10462
10744
|
scripts: /* @__PURE__ */ new Map(),
|
|
10463
10745
|
sources: /* @__PURE__ */ new Map(),
|
|
10464
10746
|
ran: /* @__PURE__ */ new Map(),
|
|
@@ -10468,6 +10750,9 @@ async function startCodeReach(page) {
|
|
|
10468
10750
|
};
|
|
10469
10751
|
const marks = marksOf(page);
|
|
10470
10752
|
cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
|
|
10753
|
+
const css = capture.css;
|
|
10754
|
+
if (css)
|
|
10755
|
+
cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
|
|
10471
10756
|
cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
|
|
10472
10757
|
cdp.on("Page.frameNavigated", (event) => {
|
|
10473
10758
|
if (event.frame.parentId) return;
|
|
@@ -10478,8 +10763,17 @@ async function startCodeReach(page) {
|
|
|
10478
10763
|
page,
|
|
10479
10764
|
() => Promise.all([
|
|
10480
10765
|
capture.cdp.send("Debugger.enable"),
|
|
10481
|
-
|
|
10482
|
-
|
|
10766
|
+
...js ? [
|
|
10767
|
+
capture.cdp.send("Profiler.enable"),
|
|
10768
|
+
capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
|
|
10769
|
+
] : [],
|
|
10770
|
+
// The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
|
|
10771
|
+
// scripts' coverage running.
|
|
10772
|
+
...css ? [
|
|
10773
|
+
capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
|
|
10774
|
+
capture.css = null;
|
|
10775
|
+
})
|
|
10776
|
+
] : [],
|
|
10483
10777
|
capture.cdp.send("Page.enable"),
|
|
10484
10778
|
capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
|
|
10485
10779
|
capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
|
|
@@ -10493,18 +10787,26 @@ async function startCodeReach(page) {
|
|
|
10493
10787
|
return false;
|
|
10494
10788
|
}
|
|
10495
10789
|
}
|
|
10496
|
-
async function
|
|
10790
|
+
async function stopPageCoverage(page) {
|
|
10497
10791
|
const capture = captures.get(page);
|
|
10498
|
-
if (!capture) return null;
|
|
10792
|
+
if (!capture) return { scripts: null, sheets: [] };
|
|
10499
10793
|
captures.delete(page);
|
|
10500
10794
|
const { cdp } = capture;
|
|
10501
|
-
|
|
10795
|
+
const sheets = async () => {
|
|
10796
|
+
if (!capture.css) return [];
|
|
10797
|
+
await readCssUsage(page, cdp, capture.css);
|
|
10798
|
+
await Promise.all(capture.css.reading);
|
|
10799
|
+
await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
|
|
10800
|
+
});
|
|
10801
|
+
return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
|
|
10802
|
+
};
|
|
10803
|
+
const scripts = async () => {
|
|
10804
|
+
if (!capture.js) return null;
|
|
10502
10805
|
const last = await internalCall(page, async () => {
|
|
10503
10806
|
const taken = await cdp.send("Profiler.takePreciseCoverage");
|
|
10504
10807
|
await cdp.send("Profiler.stopPreciseCoverage");
|
|
10505
10808
|
return taken.result;
|
|
10506
10809
|
});
|
|
10507
|
-
await Promise.all(capture.reading);
|
|
10508
10810
|
mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
|
|
10509
10811
|
const entries = [];
|
|
10510
10812
|
for (const [key, { script, functions }] of capture.ran) {
|
|
@@ -10514,8 +10816,11 @@ async function stopCodeReach(page) {
|
|
|
10514
10816
|
entries.push(entry);
|
|
10515
10817
|
}
|
|
10516
10818
|
return entries;
|
|
10517
|
-
}
|
|
10518
|
-
|
|
10819
|
+
};
|
|
10820
|
+
try {
|
|
10821
|
+
await Promise.all(capture.reading);
|
|
10822
|
+
const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
|
|
10823
|
+
return { scripts: read, sheets: styles };
|
|
10519
10824
|
} finally {
|
|
10520
10825
|
await detach(page, cdp);
|
|
10521
10826
|
}
|
|
@@ -10632,7 +10937,7 @@ var SETTLE_WAIT_MS = 15e3;
|
|
|
10632
10937
|
var RUN_COVERAGE_FILES_PER_REQUEST = 5e3;
|
|
10633
10938
|
var RUN_COVERAGE_BYTES_PER_REQUEST = 8 * 1024 * 1024;
|
|
10634
10939
|
var INCOMPLETE = { files: [], incomplete: true };
|
|
10635
|
-
function composeTestCoverage(server, browserFiles, settings) {
|
|
10940
|
+
function composeTestCoverage(server, browserFiles, settings, cssFiles) {
|
|
10636
10941
|
const serverCapture = settings.server !== "off" && server ? reduceCoverage(
|
|
10637
10942
|
{
|
|
10638
10943
|
v: 1,
|
|
@@ -10650,7 +10955,19 @@ function composeTestCoverage(server, browserFiles, settings) {
|
|
|
10650
10955
|
},
|
|
10651
10956
|
settings.browser
|
|
10652
10957
|
) : null;
|
|
10653
|
-
const
|
|
10958
|
+
const wholeFiles = new Set(
|
|
10959
|
+
settings.browser === "functions" ? [] : (browserFiles ?? []).map((file) => typeof file === "string" ? file : file.path)
|
|
10960
|
+
);
|
|
10961
|
+
const cssCapture = settings.css === "rules" && cssFiles?.length ? reduceCoverage(
|
|
10962
|
+
{
|
|
10963
|
+
v: 1,
|
|
10964
|
+
files: cssFiles.map(
|
|
10965
|
+
(file) => wholeFiles.has(file.path) ? { path: file.path, runtime: "browser" } : { ...file, runtime: "browser" }
|
|
10966
|
+
)
|
|
10967
|
+
},
|
|
10968
|
+
"functions"
|
|
10969
|
+
) : null;
|
|
10970
|
+
const merged = mergeCoverage(serverCapture, mergeCoverage(browserCapture, cssCapture));
|
|
10654
10971
|
return merged ? sanitizeCoverage(merged) : null;
|
|
10655
10972
|
}
|
|
10656
10973
|
function addRanges(into, filePath2, ranges) {
|
|
@@ -10710,7 +11027,7 @@ var CoverageCollection = class {
|
|
|
10710
11027
|
this.browserKnown = /* @__PURE__ */ new Map();
|
|
10711
11028
|
this.logger = options.logger ?? new Logger();
|
|
10712
11029
|
this.waitMs = options.waitMs ?? TEST_COVERAGE_WAIT_MS;
|
|
10713
|
-
this.browserBlobs = options.settings.browser === "functions" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
|
|
11030
|
+
this.browserBlobs = options.settings.browser === "functions" || options.settings.css === "rules" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
|
|
10714
11031
|
const granularity = options.settings.server;
|
|
10715
11032
|
this.collector = granularity === "off" ? null : new ServerCoverageCollector({
|
|
10716
11033
|
granularity,
|
|
@@ -10727,28 +11044,40 @@ var CoverageCollection = class {
|
|
|
10727
11044
|
/**
|
|
10728
11045
|
* The browser side of a test's coverage: its code reach paths, or, at
|
|
10729
11046
|
* `functions` granularity, the files of its `piwi-browser-coverage` attachment
|
|
10730
|
-
* with their blob ids, once read.
|
|
11047
|
+
* with their blob ids, once read; with `coverage.css: 'rules'`, the style
|
|
11048
|
+
* sheet files of its `piwi-css-coverage` attachment too. Never rejects.
|
|
10731
11049
|
*/
|
|
10732
11050
|
browserCoverage(testCase, attachments) {
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
const
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
11051
|
+
const { browser, css } = this.options.settings;
|
|
11052
|
+
if (browser === "off" && css !== "rules") return {};
|
|
11053
|
+
const paths = browser === "off" ? void 0 : testCase.codeReach;
|
|
11054
|
+
for (const known of attachments ?? []) {
|
|
11055
|
+
if (browser === "functions" && known.name === ATTACHMENT_NAMES.browserFunctions) this.addKnownFunctions(known);
|
|
11056
|
+
if (css === "rules" && known.name === ATTACHMENT_NAMES.cssRules) this.addKnownFunctions(known);
|
|
11057
|
+
}
|
|
11058
|
+
const scripts = browser === "functions" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.browserCoverage) : [];
|
|
11059
|
+
const sheets = css === "rules" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.cssCoverage) : [];
|
|
11060
|
+
const blobs = this.browserBlobs;
|
|
11061
|
+
if (!blobs || scripts.length === 0 && sheets.length === 0) return { files: paths };
|
|
11062
|
+
const withBlobs = (files) => blobs.withBlobs(files).catch(() => files);
|
|
11063
|
+
return Promise.all([
|
|
11064
|
+
scripts.length > 0 ? withBlobs(scripts) : Promise.resolve(null),
|
|
11065
|
+
sheets.length > 0 ? withBlobs(sheets) : Promise.resolve([])
|
|
11066
|
+
]).then(([files, rules]) => ({ files: files ?? paths, css: rules }));
|
|
11067
|
+
}
|
|
11068
|
+
/** The browser files of a test's attachment holding line ranges, added to the run's. */
|
|
11069
|
+
rangedFiles(attachments, name) {
|
|
11070
|
+
const attachment = attachments?.find((a) => a.name === name);
|
|
11071
|
+
if (!attachment?.body) return [];
|
|
10742
11072
|
let files = [];
|
|
10743
11073
|
try {
|
|
10744
11074
|
files = sanitizeCoverage(JSON.parse(attachment.body.toString("utf8")))?.files ?? [];
|
|
10745
11075
|
} catch {
|
|
10746
|
-
return
|
|
11076
|
+
return [];
|
|
10747
11077
|
}
|
|
10748
11078
|
const browser = files.filter((file) => file.runtime === "browser");
|
|
10749
|
-
if (browser.length === 0) return paths;
|
|
10750
11079
|
for (const file of browser) addRanges(this.browserRan, file.path, file.functions ?? []);
|
|
10751
|
-
return
|
|
11080
|
+
return browser;
|
|
10752
11081
|
}
|
|
10753
11082
|
/** Add the functions of a `piwi-browser-functions` attachment, one of a test's, to the run's. */
|
|
10754
11083
|
addKnownFunctions(attachment) {
|
|
@@ -10803,11 +11132,11 @@ var CoverageCollection = class {
|
|
|
10803
11132
|
const requests = attachment?.body ? parseServerCoverageRequests(attachment.body) : null;
|
|
10804
11133
|
if (!this.collector || !requests) {
|
|
10805
11134
|
if (!(browser instanceof Promise)) {
|
|
10806
|
-
this.assign(testCase, composeTestCoverage(null, browser, this.options.settings));
|
|
11135
|
+
this.assign(testCase, composeTestCoverage(null, browser.files, this.options.settings, browser.css));
|
|
10807
11136
|
return null;
|
|
10808
11137
|
}
|
|
10809
11138
|
const ready = browser.then(
|
|
10810
|
-
(
|
|
11139
|
+
(side) => this.assign(testCase, composeTestCoverage(null, side.files, this.options.settings, side.css))
|
|
10811
11140
|
);
|
|
10812
11141
|
this.browserPending.add(ready);
|
|
10813
11142
|
return ready.finally(() => this.browserPending.delete(ready));
|
|
@@ -10821,8 +11150,8 @@ var CoverageCollection = class {
|
|
|
10821
11150
|
settled = true;
|
|
10822
11151
|
clearTimeout(timer);
|
|
10823
11152
|
this.pending.delete(entry);
|
|
10824
|
-
void Promise.resolve(browser).then((
|
|
10825
|
-
this.assign(testCase, composeTestCoverage(server, files, this.options.settings));
|
|
11153
|
+
void Promise.resolve(browser).then((side) => {
|
|
11154
|
+
this.assign(testCase, composeTestCoverage(server, side.files, this.options.settings, side.css));
|
|
10826
11155
|
resolve11();
|
|
10827
11156
|
});
|
|
10828
11157
|
};
|
|
@@ -10959,6 +11288,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10959
11288
|
this.resourcesFile = null;
|
|
10960
11289
|
/** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
|
|
10961
11290
|
this.sampler = null;
|
|
11291
|
+
/** Posts what the sampler and the censuses add while someone has the run open on the dashboard. */
|
|
11292
|
+
this.liveResources = null;
|
|
10962
11293
|
/** Bytes of the files the tests attached, by kind. */
|
|
10963
11294
|
this.artifactBytes = emptyArtifacts();
|
|
10964
11295
|
/** The run's resource report for the dashboard, built in `onEnd`. */
|
|
@@ -11117,8 +11448,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11117
11448
|
);
|
|
11118
11449
|
}
|
|
11119
11450
|
if (coverage.server !== "off") this.lastTestWatch = new LastTestWatch(this.plannedTests.length);
|
|
11120
|
-
if (coverage.browser !== "off" || coverage.server !== "off") {
|
|
11451
|
+
if (coverage.browser !== "off" || coverage.server !== "off" || coverage.css === "rules") {
|
|
11121
11452
|
if (coverage.browser !== "off") process.env[PIWI_ENV_KEYS.coverageBrowser] = coverage.browser;
|
|
11453
|
+
if (coverage.css === "rules") process.env[PIWI_ENV_KEYS.coverageCss] = coverage.css;
|
|
11122
11454
|
if (coverage.server !== "off") {
|
|
11123
11455
|
process.env[PIWI_ENV_KEYS.coverageServer] = coverage.server;
|
|
11124
11456
|
}
|
|
@@ -11143,6 +11475,22 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11143
11475
|
// the dashboard shows the real total from the first render.
|
|
11144
11476
|
this.plannedTests.length
|
|
11145
11477
|
);
|
|
11478
|
+
if (this.streamManager && this.options.captureResources !== false) {
|
|
11479
|
+
const live = new LiveResources(
|
|
11480
|
+
this.streamManager,
|
|
11481
|
+
this.httpClient,
|
|
11482
|
+
{
|
|
11483
|
+
sampler: this.sampler,
|
|
11484
|
+
censuses: () => this.resourceCensuses,
|
|
11485
|
+
shardIndex: this.shardInfo?.current ?? null,
|
|
11486
|
+
runStartedAt: Date.now()
|
|
11487
|
+
},
|
|
11488
|
+
this.logger
|
|
11489
|
+
);
|
|
11490
|
+
this.streamManager.onWatched = () => void live.tick();
|
|
11491
|
+
live.start();
|
|
11492
|
+
this.liveResources = live;
|
|
11493
|
+
}
|
|
11146
11494
|
}
|
|
11147
11495
|
/**
|
|
11148
11496
|
* Settle each planned test's external ID before any test runs, name the IDs two tests share, and record them on
|
|
@@ -11445,6 +11793,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11445
11793
|
*/
|
|
11446
11794
|
async reportResources() {
|
|
11447
11795
|
if (this.options.captureResources === false) return;
|
|
11796
|
+
await this.liveResources?.stop();
|
|
11797
|
+
this.liveResources = null;
|
|
11448
11798
|
const profile = this.sampler ? await this.sampler.stop() : null;
|
|
11449
11799
|
const samples = this.sampler?.timedSeries() ?? null;
|
|
11450
11800
|
this.sampler = null;
|
|
@@ -13011,7 +13361,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
|
|
|
13011
13361
|
function generateAlternatives(attrs, options = {}) {
|
|
13012
13362
|
const alts = [];
|
|
13013
13363
|
const seen = /* @__PURE__ */ new Set();
|
|
13014
|
-
const
|
|
13364
|
+
const add2 = (loc) => {
|
|
13015
13365
|
if (!seen.has(loc.locator)) {
|
|
13016
13366
|
seen.add(loc.locator);
|
|
13017
13367
|
alts.push(loc);
|
|
@@ -13028,7 +13378,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13028
13378
|
const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
|
|
13029
13379
|
const testId = attr(attrs, testIdAttribute);
|
|
13030
13380
|
if (testId && isUnique(counts?.testId)) {
|
|
13031
|
-
|
|
13381
|
+
add2({
|
|
13032
13382
|
locator: `getByTestId(${quoteJsString(testId)})`,
|
|
13033
13383
|
method: "getByTestId",
|
|
13034
13384
|
args: { testId },
|
|
@@ -13039,7 +13389,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13039
13389
|
const levelPart = level != null ? `, level: ${level}` : "";
|
|
13040
13390
|
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
13041
13391
|
if (role && accessibleName) {
|
|
13042
|
-
|
|
13392
|
+
add2({
|
|
13043
13393
|
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
|
|
13044
13394
|
method: "getByRole",
|
|
13045
13395
|
args: withLevel({ role, name: accessibleName }),
|
|
@@ -13048,7 +13398,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13048
13398
|
}
|
|
13049
13399
|
const ariaLabel = attr(attrs, "aria-label");
|
|
13050
13400
|
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
13051
|
-
|
|
13401
|
+
add2({
|
|
13052
13402
|
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
|
|
13053
13403
|
method: "getByRole",
|
|
13054
13404
|
args: withLevel({ role, name: ariaLabel }),
|
|
@@ -13059,7 +13409,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13059
13409
|
const label = attr(attrs, "aria-label");
|
|
13060
13410
|
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
13061
13411
|
if (labelBacked) {
|
|
13062
|
-
|
|
13412
|
+
add2({
|
|
13063
13413
|
locator: `getByLabel(${quoteJsString(accessibleName)})`,
|
|
13064
13414
|
method: "getByLabel",
|
|
13065
13415
|
args: { label: accessibleName },
|
|
@@ -13069,7 +13419,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13069
13419
|
}
|
|
13070
13420
|
const placeholder = attr(attrs, "placeholder");
|
|
13071
13421
|
if (placeholder) {
|
|
13072
|
-
|
|
13422
|
+
add2({
|
|
13073
13423
|
locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
|
|
13074
13424
|
method: "getByPlaceholder",
|
|
13075
13425
|
args: { placeholder },
|
|
@@ -13077,7 +13427,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13077
13427
|
});
|
|
13078
13428
|
}
|
|
13079
13429
|
if (text && text.length < 80) {
|
|
13080
|
-
|
|
13430
|
+
add2({
|
|
13081
13431
|
locator: `getByText(${quoteJsString(text)})`,
|
|
13082
13432
|
method: "getByText",
|
|
13083
13433
|
args: { text },
|
|
@@ -13087,7 +13437,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13087
13437
|
const id = attr(attrs, "id");
|
|
13088
13438
|
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
13089
13439
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
13090
|
-
|
|
13440
|
+
add2({
|
|
13091
13441
|
locator: `locator(${quoteJsString(selector)})`,
|
|
13092
13442
|
method: "locator",
|
|
13093
13443
|
args: { selector },
|
|
@@ -13097,7 +13447,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13097
13447
|
const name = attr(attrs, "name");
|
|
13098
13448
|
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
13099
13449
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
13100
|
-
|
|
13450
|
+
add2({
|
|
13101
13451
|
locator: `locator(${quoteJsString(selector)})`,
|
|
13102
13452
|
method: "locator",
|
|
13103
13453
|
args: { selector },
|
|
@@ -13106,7 +13456,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13106
13456
|
}
|
|
13107
13457
|
const alt = attr(attrs, "alt");
|
|
13108
13458
|
if (alt) {
|
|
13109
|
-
|
|
13459
|
+
add2({
|
|
13110
13460
|
locator: `getByAltText(${quoteJsString(alt)})`,
|
|
13111
13461
|
method: "getByAltText",
|
|
13112
13462
|
args: { text: alt },
|
|
@@ -13115,7 +13465,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13115
13465
|
}
|
|
13116
13466
|
const title = attr(attrs, "title");
|
|
13117
13467
|
if (title) {
|
|
13118
|
-
|
|
13468
|
+
add2({
|
|
13119
13469
|
locator: `getByTitle(${quoteJsString(title)})`,
|
|
13120
13470
|
method: "getByTitle",
|
|
13121
13471
|
args: { title },
|
|
@@ -13133,7 +13483,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13133
13483
|
if (scopedCount(anc) !== 1) continue;
|
|
13134
13484
|
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
13135
13485
|
testIdAnchorDone = true;
|
|
13136
|
-
|
|
13486
|
+
add2({
|
|
13137
13487
|
locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
|
|
13138
13488
|
method: leaf.method,
|
|
13139
13489
|
args: { ...leaf.args, anchorTestId: anc.testId },
|
|
@@ -13143,7 +13493,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13143
13493
|
if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
13144
13494
|
idAnchorDone = true;
|
|
13145
13495
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
13146
|
-
|
|
13496
|
+
add2({
|
|
13147
13497
|
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
13148
13498
|
method: leaf.method,
|
|
13149
13499
|
args: { ...leaf.args, anchorSelector },
|
|
@@ -13154,7 +13504,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13154
13504
|
dataAnchorDone = true;
|
|
13155
13505
|
const { name: name2, value } = anc.dataAttr;
|
|
13156
13506
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
13157
|
-
|
|
13507
|
+
add2({
|
|
13158
13508
|
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
13159
13509
|
method: leaf.method,
|
|
13160
13510
|
args: { ...leaf.args, anchorSelector },
|
|
@@ -13164,7 +13514,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13164
13514
|
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
13165
13515
|
if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
|
|
13166
13516
|
roleAnchorDone = true;
|
|
13167
|
-
|
|
13517
|
+
add2({
|
|
13168
13518
|
locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
|
|
13169
13519
|
method: leaf.method,
|
|
13170
13520
|
args: { ...leaf.args, anchorRole: ancestorRole },
|
|
@@ -13173,7 +13523,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13173
13523
|
}
|
|
13174
13524
|
if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
|
|
13175
13525
|
filterAnchorDone = true;
|
|
13176
|
-
|
|
13526
|
+
add2({
|
|
13177
13527
|
locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
|
|
13178
13528
|
method: leaf.method,
|
|
13179
13529
|
args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
|
|
@@ -13192,7 +13542,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13192
13542
|
);
|
|
13193
13543
|
const pos = attrs.rolePosition;
|
|
13194
13544
|
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
13195
|
-
|
|
13545
|
+
add2({
|
|
13196
13546
|
locator: `getByRole(${rolePart})`,
|
|
13197
13547
|
method: "getByRole",
|
|
13198
13548
|
args: leafArgs,
|
|
@@ -13211,7 +13561,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13211
13561
|
const countsClasses = counts?.classes;
|
|
13212
13562
|
const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
|
|
13213
13563
|
for (const { cls, score } of classes) {
|
|
13214
|
-
|
|
13564
|
+
add2({
|
|
13215
13565
|
locator: `locator(${quoteJsString(`.${cls}`)})`,
|
|
13216
13566
|
method: "locator",
|
|
13217
13567
|
args: { selector: `.${cls}` },
|
|
@@ -13268,7 +13618,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
13268
13618
|
const seen = /* @__PURE__ */ new Set();
|
|
13269
13619
|
const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
|
|
13270
13620
|
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
13271
|
-
const
|
|
13621
|
+
const add2 = (l) => {
|
|
13272
13622
|
if (!seen.has(l.locator)) {
|
|
13273
13623
|
seen.add(l.locator);
|
|
13274
13624
|
out.push(l);
|
|
@@ -13278,7 +13628,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
13278
13628
|
if (anchor.scopedLeafCount !== 1) continue;
|
|
13279
13629
|
const seg = anchorSegment(anchor);
|
|
13280
13630
|
if (!seg) continue;
|
|
13281
|
-
|
|
13631
|
+
add2({
|
|
13282
13632
|
locator: `${seg.code}.getByRole(${rolePart})`,
|
|
13283
13633
|
method: "getByRole",
|
|
13284
13634
|
args: { ...leafArgs, ...seg.flatArgs },
|
|
@@ -13291,7 +13641,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
13291
13641
|
if (segments.every((s) => s !== null)) {
|
|
13292
13642
|
const innermost = segments[segments.length - 1];
|
|
13293
13643
|
const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
|
|
13294
|
-
|
|
13644
|
+
add2({
|
|
13295
13645
|
locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
|
|
13296
13646
|
method: "getByRole",
|
|
13297
13647
|
// Flat args describe the innermost anchor (what the healing fingerprint
|
|
@@ -14488,7 +14838,7 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
|
|
|
14488
14838
|
|
|
14489
14839
|
// src/internal/capture/record-handoff.ts
|
|
14490
14840
|
var fs26 = __toESM(require("fs"));
|
|
14491
|
-
var
|
|
14841
|
+
var net2 = __toESM(require("net"));
|
|
14492
14842
|
var path29 = __toESM(require("path"));
|
|
14493
14843
|
|
|
14494
14844
|
// ../core/src/ide-recorder.ts
|
|
@@ -14577,7 +14927,7 @@ async function recordFromHere(options) {
|
|
|
14577
14927
|
const use = testInfo.project?.use ?? {};
|
|
14578
14928
|
const context = page.context();
|
|
14579
14929
|
return new Promise((resolve11) => {
|
|
14580
|
-
const socket =
|
|
14930
|
+
const socket = net2.connect(channel.path);
|
|
14581
14931
|
socket.setEncoding("utf8");
|
|
14582
14932
|
const answers = /* @__PURE__ */ new Map();
|
|
14583
14933
|
let calls = 0;
|
|
@@ -14726,6 +15076,267 @@ async function recordFromHere(options) {
|
|
|
14726
15076
|
});
|
|
14727
15077
|
}
|
|
14728
15078
|
|
|
15079
|
+
// src/internal/capture/css-coverage.ts
|
|
15080
|
+
var crypto7 = __toESM(require("crypto"));
|
|
15081
|
+
var fs27 = __toESM(require("fs"));
|
|
15082
|
+
|
|
15083
|
+
// ../core/src/css-rules.ts
|
|
15084
|
+
var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
|
|
15085
|
+
function parseCssRules(text) {
|
|
15086
|
+
const rules = [];
|
|
15087
|
+
const open = [];
|
|
15088
|
+
let preludeStart = -1;
|
|
15089
|
+
for (let i = 0; i < text.length; i++) {
|
|
15090
|
+
const c = text[i];
|
|
15091
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
15092
|
+
const end = text.indexOf("*/", i + 2);
|
|
15093
|
+
i = end < 0 ? text.length : end + 1;
|
|
15094
|
+
continue;
|
|
15095
|
+
}
|
|
15096
|
+
if (c === '"' || c === "'") {
|
|
15097
|
+
if (preludeStart < 0) preludeStart = i;
|
|
15098
|
+
for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
|
|
15099
|
+
continue;
|
|
15100
|
+
}
|
|
15101
|
+
if (c === "\\") {
|
|
15102
|
+
if (preludeStart < 0) preludeStart = i;
|
|
15103
|
+
i++;
|
|
15104
|
+
continue;
|
|
15105
|
+
}
|
|
15106
|
+
if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
|
|
15107
|
+
let j = i + 4;
|
|
15108
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
15109
|
+
if (text[j] !== '"' && text[j] !== "'") {
|
|
15110
|
+
if (preludeStart < 0) preludeStart = i;
|
|
15111
|
+
for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
|
|
15112
|
+
i = j;
|
|
15113
|
+
continue;
|
|
15114
|
+
}
|
|
15115
|
+
}
|
|
15116
|
+
if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
|
|
15117
|
+
i += text.startsWith("<!--", i) ? 3 : 2;
|
|
15118
|
+
continue;
|
|
15119
|
+
}
|
|
15120
|
+
if (c === "{") {
|
|
15121
|
+
const start = preludeStart < 0 ? i : preludeStart;
|
|
15122
|
+
const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
|
|
15123
|
+
const parent = open[open.length - 1];
|
|
15124
|
+
const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
|
|
15125
|
+
open.push({ kind, start, open: i });
|
|
15126
|
+
preludeStart = -1;
|
|
15127
|
+
continue;
|
|
15128
|
+
}
|
|
15129
|
+
if (c === "}") {
|
|
15130
|
+
const block = open.pop();
|
|
15131
|
+
if (block && block.kind !== "other")
|
|
15132
|
+
rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
|
|
15133
|
+
preludeStart = -1;
|
|
15134
|
+
continue;
|
|
15135
|
+
}
|
|
15136
|
+
if (c === ";") {
|
|
15137
|
+
preludeStart = -1;
|
|
15138
|
+
continue;
|
|
15139
|
+
}
|
|
15140
|
+
if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
|
|
15141
|
+
}
|
|
15142
|
+
for (const block of open) {
|
|
15143
|
+
if (block.kind !== "other")
|
|
15144
|
+
rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
|
|
15145
|
+
}
|
|
15146
|
+
return rules.sort((a, b) => a.start - b.start);
|
|
15147
|
+
}
|
|
15148
|
+
function usedCssRules(rules, used) {
|
|
15149
|
+
const styleAt = /* @__PURE__ */ new Map();
|
|
15150
|
+
const groups = [];
|
|
15151
|
+
for (const rule of rules) {
|
|
15152
|
+
if (rule.kind === "style") styleAt.set(rule.start, rule);
|
|
15153
|
+
else groups.push(rule);
|
|
15154
|
+
}
|
|
15155
|
+
const out = /* @__PURE__ */ new Map();
|
|
15156
|
+
for (const range of used) {
|
|
15157
|
+
const style = styleAt.get(range.startOffset);
|
|
15158
|
+
if (style) {
|
|
15159
|
+
out.set(`${style.start}`, [style.start, style.end]);
|
|
15160
|
+
continue;
|
|
15161
|
+
}
|
|
15162
|
+
const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
|
|
15163
|
+
if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
|
|
15164
|
+
}
|
|
15165
|
+
return [...out.values()].sort((a, b) => a[0] - b[0]);
|
|
15166
|
+
}
|
|
15167
|
+
function allCssRules(rules) {
|
|
15168
|
+
return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
|
|
15169
|
+
}
|
|
15170
|
+
function cssRuleLines(text, offsets, options = {}) {
|
|
15171
|
+
const starts = lineStarts(text);
|
|
15172
|
+
const lineOffset = options.lineOffset ?? 0;
|
|
15173
|
+
const out = /* @__PURE__ */ new Map();
|
|
15174
|
+
const add2 = (source, range) => {
|
|
15175
|
+
const ranges = out.get(source);
|
|
15176
|
+
if (ranges) ranges.push(range);
|
|
15177
|
+
else out.set(source, [range]);
|
|
15178
|
+
};
|
|
15179
|
+
for (const [startOffset, endOffset] of offsets) {
|
|
15180
|
+
const start = offsetToPosition(starts, startOffset);
|
|
15181
|
+
const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
|
|
15182
|
+
if (!options.lookup) {
|
|
15183
|
+
add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
|
|
15184
|
+
continue;
|
|
15185
|
+
}
|
|
15186
|
+
const first = options.lookup.originalAt(start.line, start.column);
|
|
15187
|
+
if (!first) continue;
|
|
15188
|
+
const last = options.lookup.originalAt(end.line, end.column);
|
|
15189
|
+
const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
|
|
15190
|
+
add2(first.source, [first.line + 1, lastLine + 1]);
|
|
15191
|
+
}
|
|
15192
|
+
return out;
|
|
15193
|
+
}
|
|
15194
|
+
|
|
15195
|
+
// src/internal/capture/css-coverage.ts
|
|
15196
|
+
var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
|
|
15197
|
+
var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
|
|
15198
|
+
var resolutions = /* @__PURE__ */ new Map();
|
|
15199
|
+
var MAX_CACHED_SHEETS = 2e3;
|
|
15200
|
+
function fileForStyleUrl(url, roots) {
|
|
15201
|
+
let pathname;
|
|
15202
|
+
try {
|
|
15203
|
+
const parsed2 = new URL(url);
|
|
15204
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
|
|
15205
|
+
pathname = parsed2.pathname;
|
|
15206
|
+
} catch {
|
|
15207
|
+
return null;
|
|
15208
|
+
}
|
|
15209
|
+
return sourceFile(decodeURIComponent(pathname), roots);
|
|
15210
|
+
}
|
|
15211
|
+
function sourceFile(source, roots) {
|
|
15212
|
+
const bare = source.replace(/[?#].*$/, "");
|
|
15213
|
+
if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
|
|
15214
|
+
const file = resolveSourcePath(bare, roots);
|
|
15215
|
+
if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
|
|
15216
|
+
return file;
|
|
15217
|
+
}
|
|
15218
|
+
function styleBlockLine(file, index) {
|
|
15219
|
+
let text;
|
|
15220
|
+
try {
|
|
15221
|
+
text = fs27.readFileSync(file, "utf-8");
|
|
15222
|
+
} catch {
|
|
15223
|
+
return 0;
|
|
15224
|
+
}
|
|
15225
|
+
const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
|
|
15226
|
+
const block = blocks[index];
|
|
15227
|
+
if (!block) return 0;
|
|
15228
|
+
return text.slice(0, block.index + block[0].length).split("\n").length - 1;
|
|
15229
|
+
}
|
|
15230
|
+
async function sheetMap(sheet, text, fetchMap) {
|
|
15231
|
+
const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
|
|
15232
|
+
if (!ref) return null;
|
|
15233
|
+
let raw;
|
|
15234
|
+
if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
|
|
15235
|
+
else {
|
|
15236
|
+
try {
|
|
15237
|
+
raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
|
|
15238
|
+
} catch {
|
|
15239
|
+
raw = null;
|
|
15240
|
+
}
|
|
15241
|
+
}
|
|
15242
|
+
if (!raw) return null;
|
|
15243
|
+
try {
|
|
15244
|
+
return decodeSourceMap(JSON.parse(raw));
|
|
15245
|
+
} catch {
|
|
15246
|
+
return null;
|
|
15247
|
+
}
|
|
15248
|
+
}
|
|
15249
|
+
function sameLines(text, rules, file, lineOffset) {
|
|
15250
|
+
let source;
|
|
15251
|
+
try {
|
|
15252
|
+
source = fs27.readFileSync(file, "utf-8");
|
|
15253
|
+
} catch {
|
|
15254
|
+
return false;
|
|
15255
|
+
}
|
|
15256
|
+
const fileLines = source.split(/\r?\n/);
|
|
15257
|
+
const sheetLineStarts = [0];
|
|
15258
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
|
|
15259
|
+
let low = 0;
|
|
15260
|
+
for (const rule of rules) {
|
|
15261
|
+
while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
|
|
15262
|
+
const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
|
|
15263
|
+
if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
|
|
15264
|
+
}
|
|
15265
|
+
return true;
|
|
15266
|
+
}
|
|
15267
|
+
async function resolveSheet(sheet, text, dirs, fetchMap) {
|
|
15268
|
+
const lookup = await sheetMap(sheet, text, fetchMap);
|
|
15269
|
+
const rules = parseCssRules(text);
|
|
15270
|
+
if (lookup) {
|
|
15271
|
+
const sources = lookup.sources.map((source) => {
|
|
15272
|
+
const direct = sourceFile(source, dirs);
|
|
15273
|
+
if (direct) return direct;
|
|
15274
|
+
try {
|
|
15275
|
+
return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
|
|
15276
|
+
} catch {
|
|
15277
|
+
return null;
|
|
15278
|
+
}
|
|
15279
|
+
});
|
|
15280
|
+
return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
|
|
15281
|
+
}
|
|
15282
|
+
if (sheet.devId) {
|
|
15283
|
+
const file2 = sourceFile(sheet.devId, dirs);
|
|
15284
|
+
if (!file2) return null;
|
|
15285
|
+
const index = /[?&]index=(\d+)/.exec(sheet.devId);
|
|
15286
|
+
const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
|
|
15287
|
+
return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
|
|
15288
|
+
}
|
|
15289
|
+
const file = fileForStyleUrl(sheet.sourceURL, dirs);
|
|
15290
|
+
if (!file) return null;
|
|
15291
|
+
const lineOffset = sheet.isInline ? sheet.startLine : 0;
|
|
15292
|
+
return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
|
|
15293
|
+
}
|
|
15294
|
+
function cachedResolution(sheet, text, dirs, fetchMap) {
|
|
15295
|
+
const version = crypto7.createHash("sha1").update(text).digest("hex");
|
|
15296
|
+
const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
|
|
15297
|
+
" "
|
|
15298
|
+
);
|
|
15299
|
+
let pending = resolutions.get(key);
|
|
15300
|
+
if (!pending) {
|
|
15301
|
+
pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
|
|
15302
|
+
resolutions.set(key, pending);
|
|
15303
|
+
if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
|
|
15304
|
+
}
|
|
15305
|
+
return pending;
|
|
15306
|
+
}
|
|
15307
|
+
function add(into, file, ranges, repoRoot, fileLevel = false) {
|
|
15308
|
+
if (ranges.length === 0 && !fileLevel) return;
|
|
15309
|
+
const key = relativeToRepo(file, repoRoot);
|
|
15310
|
+
into.set(key, [...into.get(key) ?? [], ...ranges]);
|
|
15311
|
+
}
|
|
15312
|
+
async function resolveCssCoverage(sheets, roots, fetchMap) {
|
|
15313
|
+
const reached = /* @__PURE__ */ new Map();
|
|
15314
|
+
const known = /* @__PURE__ */ new Map();
|
|
15315
|
+
for (const sheet of sheets) {
|
|
15316
|
+
if (sheet.text === void 0) continue;
|
|
15317
|
+
const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
|
|
15318
|
+
const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
|
|
15319
|
+
if (!resolution) continue;
|
|
15320
|
+
const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
|
|
15321
|
+
if (!resolution.exact) {
|
|
15322
|
+
if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
|
|
15323
|
+
continue;
|
|
15324
|
+
}
|
|
15325
|
+
const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
|
|
15326
|
+
const fileOf2 = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
|
|
15327
|
+
const matched = cssRuleLines(sheet.text, used, options);
|
|
15328
|
+
for (const [source, ranges] of matched) {
|
|
15329
|
+
const file = fileOf2(source);
|
|
15330
|
+
if (file) add(reached, file, ranges, roots.repoRoot);
|
|
15331
|
+
}
|
|
15332
|
+
for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
|
|
15333
|
+
const file = fileOf2(source);
|
|
15334
|
+
if (file) add(known, file, ranges, roots.repoRoot);
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
return { reached, known };
|
|
15338
|
+
}
|
|
15339
|
+
|
|
14729
15340
|
// src/internal/capture/page-interaction.ts
|
|
14730
15341
|
function readPageInteraction() {
|
|
14731
15342
|
const doc = globalThis.document;
|
|
@@ -15029,16 +15640,19 @@ function createSink() {
|
|
|
15029
15640
|
onReleasedRequestsEnded: null,
|
|
15030
15641
|
releasedQuietSince: 0,
|
|
15031
15642
|
locatorPages: new LocatorPageLog(),
|
|
15032
|
-
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
|
|
15643
|
+
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
|
|
15033
15644
|
serverCoverage: /* @__PURE__ */ new Map(),
|
|
15034
15645
|
baseURL: null
|
|
15035
15646
|
};
|
|
15036
15647
|
}
|
|
15037
15648
|
var MAX_CODE_REACH_PAGES = 16;
|
|
15038
15649
|
var CODE_REACH_START_WAIT_MS = 2e3;
|
|
15650
|
+
function pageCoverageInWorker() {
|
|
15651
|
+
return browserCoverageInWorker() || cssCoverageInWorker();
|
|
15652
|
+
}
|
|
15039
15653
|
function maybeStartCodeReach(page, alreadyOpen = false) {
|
|
15040
15654
|
const sink = currentSink;
|
|
15041
|
-
if (!sink || !
|
|
15655
|
+
if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
|
|
15042
15656
|
const known = sink.codeReach.sessions.get(page);
|
|
15043
15657
|
if (known) return known.started;
|
|
15044
15658
|
if (alreadyOpen) {
|
|
@@ -15051,7 +15665,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
|
|
|
15051
15665
|
return started;
|
|
15052
15666
|
}
|
|
15053
15667
|
async function startCodeReachOnOpenPages() {
|
|
15054
|
-
if (!
|
|
15668
|
+
if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
|
|
15055
15669
|
const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
|
|
15056
15670
|
await Promise.race([started, new Promise((resolve11) => setTimeout(resolve11, CODE_REACH_START_WAIT_MS).unref())]);
|
|
15057
15671
|
}
|
|
@@ -15094,22 +15708,28 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
15094
15708
|
async function stopPageCodeReach(sink, page, session, roots) {
|
|
15095
15709
|
try {
|
|
15096
15710
|
if (!await session.started || isPageClosed(page)) return;
|
|
15097
|
-
const
|
|
15098
|
-
|
|
15099
|
-
const
|
|
15711
|
+
const { scripts, sheets } = await stopPageCoverage(page);
|
|
15712
|
+
const fetchMap = pageMapFetcher(page);
|
|
15713
|
+
const append = (into, from) => {
|
|
15714
|
+
for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
|
|
15715
|
+
};
|
|
15716
|
+
if (sheets.length > 0) {
|
|
15717
|
+
const rules = await resolveCssCoverage(sheets, roots, fetchMap);
|
|
15718
|
+
append(sink.codeReach.cssFiles, rules.reached);
|
|
15719
|
+
append(sink.codeReach.cssKnown, rules.known);
|
|
15720
|
+
}
|
|
15721
|
+
if (!scripts) return;
|
|
15722
|
+
const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
|
|
15100
15723
|
known: browserFunctionsInWorker()
|
|
15101
15724
|
});
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
}
|
|
15105
|
-
for (const [file, functions] of known) {
|
|
15106
|
-
sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
|
|
15107
|
-
}
|
|
15725
|
+
append(sink.codeReach.files, reached);
|
|
15726
|
+
append(sink.codeReach.known, known);
|
|
15108
15727
|
} catch {
|
|
15109
15728
|
}
|
|
15110
15729
|
}
|
|
15111
15730
|
var currentSink = null;
|
|
15112
15731
|
var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
|
|
15732
|
+
var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
|
|
15113
15733
|
var PENDING_PROBES = /* @__PURE__ */ new Set();
|
|
15114
15734
|
async function drainPendingProbes(capMs) {
|
|
15115
15735
|
if (PENDING_PROBES.size === 0) return;
|
|
@@ -16265,6 +16885,21 @@ async function flushSink(sink, testInfo) {
|
|
|
16265
16885
|
body: Buffer.from(JSON.stringify(part))
|
|
16266
16886
|
});
|
|
16267
16887
|
}
|
|
16888
|
+
if (cssCoverageInWorker()) {
|
|
16889
|
+
const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
|
|
16890
|
+
if (rules) {
|
|
16891
|
+
await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
|
|
16892
|
+
contentType: "application/json",
|
|
16893
|
+
body: Buffer.from(JSON.stringify(rules))
|
|
16894
|
+
});
|
|
16895
|
+
}
|
|
16896
|
+
for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
|
|
16897
|
+
await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
|
|
16898
|
+
contentType: "application/json",
|
|
16899
|
+
body: Buffer.from(JSON.stringify(part))
|
|
16900
|
+
});
|
|
16901
|
+
}
|
|
16902
|
+
}
|
|
16268
16903
|
if (sink.serverCoverage.size > 0) {
|
|
16269
16904
|
const requests = {};
|
|
16270
16905
|
for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
|
|
@@ -16546,12 +17181,12 @@ function extendPiwiFixtures(test) {
|
|
|
16546
17181
|
}
|
|
16547
17182
|
|
|
16548
17183
|
// src/internal/ai/ai-fixtures.ts
|
|
16549
|
-
var
|
|
17184
|
+
var fs29 = __toESM(require("fs"));
|
|
16550
17185
|
var path33 = __toESM(require("path"));
|
|
16551
17186
|
var import_test = require("@playwright/test");
|
|
16552
17187
|
|
|
16553
17188
|
// src/internal/ai/artifact.ts
|
|
16554
|
-
var
|
|
17189
|
+
var fs28 = __toESM(require("fs"));
|
|
16555
17190
|
var path31 = __toESM(require("path"));
|
|
16556
17191
|
var ARTIFACT_VERSION = 1;
|
|
16557
17192
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
@@ -16645,7 +17280,7 @@ function parseEntry(text) {
|
|
|
16645
17280
|
function readEntry(file) {
|
|
16646
17281
|
let text;
|
|
16647
17282
|
try {
|
|
16648
|
-
text =
|
|
17283
|
+
text = fs28.readFileSync(file, "utf8");
|
|
16649
17284
|
} catch (error) {
|
|
16650
17285
|
if (error.code === "ENOENT") return null;
|
|
16651
17286
|
throw error;
|
|
@@ -16655,28 +17290,28 @@ function readEntry(file) {
|
|
|
16655
17290
|
function writeEntry(file, entry) {
|
|
16656
17291
|
const canonical = serializeEntry(entry);
|
|
16657
17292
|
return withEntryLock(file, () => {
|
|
16658
|
-
if (
|
|
16659
|
-
|
|
17293
|
+
if (fs28.existsSync(file) && fs28.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
17294
|
+
fs28.mkdirSync(path31.dirname(file), { recursive: true });
|
|
16660
17295
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
16661
|
-
|
|
16662
|
-
|
|
17296
|
+
fs28.writeFileSync(tmp, canonical);
|
|
17297
|
+
fs28.renameSync(tmp, file);
|
|
16663
17298
|
return { written: true };
|
|
16664
17299
|
});
|
|
16665
17300
|
}
|
|
16666
17301
|
function withEntryLock(file, fn) {
|
|
16667
17302
|
const lock = `${file}.lock`;
|
|
16668
|
-
|
|
17303
|
+
fs28.mkdirSync(path31.dirname(file), { recursive: true });
|
|
16669
17304
|
const deadline = Date.now() + 5e3;
|
|
16670
17305
|
for (; ; ) {
|
|
16671
17306
|
try {
|
|
16672
|
-
const fd =
|
|
16673
|
-
|
|
17307
|
+
const fd = fs28.openSync(lock, "wx");
|
|
17308
|
+
fs28.closeSync(fd);
|
|
16674
17309
|
break;
|
|
16675
17310
|
} catch (error) {
|
|
16676
17311
|
if (error.code !== "EEXIST") throw error;
|
|
16677
17312
|
if (Date.now() > deadline) {
|
|
16678
17313
|
try {
|
|
16679
|
-
|
|
17314
|
+
fs28.unlinkSync(lock);
|
|
16680
17315
|
} catch {
|
|
16681
17316
|
}
|
|
16682
17317
|
}
|
|
@@ -16686,14 +17321,14 @@ function withEntryLock(file, fn) {
|
|
|
16686
17321
|
return fn();
|
|
16687
17322
|
} finally {
|
|
16688
17323
|
try {
|
|
16689
|
-
|
|
17324
|
+
fs28.unlinkSync(lock);
|
|
16690
17325
|
} catch {
|
|
16691
17326
|
}
|
|
16692
17327
|
}
|
|
16693
17328
|
}
|
|
16694
17329
|
|
|
16695
17330
|
// src/internal/ai/keys.ts
|
|
16696
|
-
var
|
|
17331
|
+
var crypto8 = __toESM(require("crypto"));
|
|
16697
17332
|
var path32 = __toESM(require("path"));
|
|
16698
17333
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
16699
17334
|
function normalizeTemplate(template) {
|
|
@@ -16701,7 +17336,7 @@ function normalizeTemplate(template) {
|
|
|
16701
17336
|
}
|
|
16702
17337
|
function hashTemplate(normalized, ordinal = 0) {
|
|
16703
17338
|
const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
|
|
16704
|
-
return
|
|
17339
|
+
return crypto8.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
16705
17340
|
}
|
|
16706
17341
|
function slug(text, maxLength = 40) {
|
|
16707
17342
|
const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -17320,15 +17955,15 @@ function relativeToCwd(file) {
|
|
|
17320
17955
|
return path33.relative(process.cwd(), file).split(path33.sep).join("/");
|
|
17321
17956
|
}
|
|
17322
17957
|
function recordIntents(intents, entry) {
|
|
17323
|
-
const
|
|
17958
|
+
const add2 = (locator, kind) => {
|
|
17324
17959
|
intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
|
|
17325
17960
|
};
|
|
17326
17961
|
if (entry.kind === "locator") {
|
|
17327
|
-
|
|
17962
|
+
add2(locatorSource2(entry.locator), "locator");
|
|
17328
17963
|
return;
|
|
17329
17964
|
}
|
|
17330
|
-
for (const step of entry.steps)
|
|
17331
|
-
if (entry.postcondition.locator)
|
|
17965
|
+
for (const step of entry.steps) add2(locatorSource2(step.locator), "run");
|
|
17966
|
+
if (entry.postcondition.locator) add2(locatorSource2(entry.postcondition.locator), "run");
|
|
17332
17967
|
}
|
|
17333
17968
|
function missMessage(template, testTitle, relPath) {
|
|
17334
17969
|
return [
|
|
@@ -17344,7 +17979,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
17344
17979
|
const readSource = () => {
|
|
17345
17980
|
if (source === null) {
|
|
17346
17981
|
try {
|
|
17347
|
-
source =
|
|
17982
|
+
source = fs29.readFileSync(testInfo.file, "utf8");
|
|
17348
17983
|
} catch {
|
|
17349
17984
|
source = "";
|
|
17350
17985
|
}
|