@piwitests/reporter 0.45.0 → 0.46.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 +530 -249
- package/dist/global-setup-module.js +132 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +693 -104
- package/dist/internal/capture/capture-fixtures.js +187 -77
- package/dist/internal/capture/locator-healing.js +9 -7
- package/dist/internal/capture/pick-on-failure.js +130 -37
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -0
- package/templates/skills/investigate-failure/SKILL.md +5 -1
- package/templates/skills/write-the-missing-test/SKILL.md +9 -2
package/dist/index.js
CHANGED
|
@@ -89,6 +89,7 @@ var DEFAULTS = {
|
|
|
89
89
|
sampleAriaOnPass: true,
|
|
90
90
|
uploadManifest: true,
|
|
91
91
|
defaultCapture: true,
|
|
92
|
+
checkBaseUrl: false,
|
|
92
93
|
streaming: true,
|
|
93
94
|
streamingBatchSize: 5,
|
|
94
95
|
streamingBatchDelay: 2e3,
|
|
@@ -131,6 +132,7 @@ var PIWI_ENV_KEYS = {
|
|
|
131
132
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
132
133
|
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
133
134
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
135
|
+
checkBaseUrl: "PIWI_CHECK_BASE_URL",
|
|
134
136
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
135
137
|
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
136
138
|
outputFile: "PIWI_OUTPUT_FILE",
|
|
@@ -151,6 +153,10 @@ var PIWI_SELECTION_ENV = {
|
|
|
151
153
|
hash: "PIWI_SELECTION_HASH",
|
|
152
154
|
count: "PIWI_SELECTION_COUNT"
|
|
153
155
|
};
|
|
156
|
+
var PIWI_ORIGIN_ENV = {
|
|
157
|
+
kind: "PIWI_ORIGIN",
|
|
158
|
+
ref: "PIWI_ORIGIN_REF"
|
|
159
|
+
};
|
|
154
160
|
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
155
161
|
var PIWI_PROBE_ENV = {
|
|
156
162
|
flag: "PIWI_PROBE",
|
|
@@ -196,6 +202,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
196
202
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
197
203
|
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
198
204
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
205
|
+
{ option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
|
|
199
206
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
200
207
|
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
201
208
|
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
@@ -243,6 +250,7 @@ function applyOptionsToEnv(options) {
|
|
|
243
250
|
if (options.label) env[PIWI_ENV_KEYS.label] = options.label;
|
|
244
251
|
if (options.runLabel) env[PIWI_ENV_KEYS.runLabel] = options.runLabel;
|
|
245
252
|
if (options.keep) env[PIWI_ENV_KEYS.keep] = "true";
|
|
253
|
+
if (options.checkBaseUrl !== void 0) env[PIWI_ENV_KEYS.checkBaseUrl] = String(options.checkBaseUrl);
|
|
246
254
|
if (options.captureLocators === false || options.collectPerformanceMetrics === false)
|
|
247
255
|
env[PIWI_ENV_KEYS.captureLocators] = "false";
|
|
248
256
|
else if (options.captureLocators === true) env[PIWI_ENV_KEYS.captureLocators] = "true";
|
|
@@ -1008,6 +1016,8 @@ var CrashRecovery = class {
|
|
|
1008
1016
|
if (!data) return;
|
|
1009
1017
|
this.logger.info("Found saved test data from a previous run, uploading...");
|
|
1010
1018
|
delete data.instanceId;
|
|
1019
|
+
const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
|
|
1020
|
+
data.metadata = { ...metadata, ingestHealth: { submitFallback: { path: "recovery" } } };
|
|
1011
1021
|
try {
|
|
1012
1022
|
await httpClient.postJSON("/api/test-runs/submit", data, auth);
|
|
1013
1023
|
this.logger.info("Successfully uploaded saved test data");
|
|
@@ -1322,6 +1332,35 @@ function formatMiB(bytes) {
|
|
|
1322
1332
|
|
|
1323
1333
|
// src/internal/collect/metadata-collector.ts
|
|
1324
1334
|
var import_node_child_process = require("child_process");
|
|
1335
|
+
|
|
1336
|
+
// ../core/src/wire.ts
|
|
1337
|
+
var RUN_ORIGIN_KINDS = [
|
|
1338
|
+
"ci",
|
|
1339
|
+
"ci-rerun",
|
|
1340
|
+
"local",
|
|
1341
|
+
"desktop",
|
|
1342
|
+
"editor",
|
|
1343
|
+
"preflight",
|
|
1344
|
+
"bug",
|
|
1345
|
+
"flake-lab",
|
|
1346
|
+
"probe",
|
|
1347
|
+
"bisect",
|
|
1348
|
+
"reproduce",
|
|
1349
|
+
"import"
|
|
1350
|
+
];
|
|
1351
|
+
var RUN_ORIGIN_METADATA_KEY = "piwiOrigin";
|
|
1352
|
+
var RUN_ORIGIN_REF_MAX_LENGTH = 200;
|
|
1353
|
+
var RUN_ORIGIN_REF = /^[A-Za-z0-9._:/#@-]+$/;
|
|
1354
|
+
function isRunOriginKind(value) {
|
|
1355
|
+
return typeof value === "string" && RUN_ORIGIN_KINDS.includes(value);
|
|
1356
|
+
}
|
|
1357
|
+
function parseRunOriginRef(value) {
|
|
1358
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
1359
|
+
const ref = String(value).trim();
|
|
1360
|
+
return ref && ref.length <= RUN_ORIGIN_REF_MAX_LENGTH && RUN_ORIGIN_REF.test(ref) ? ref : void 0;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/internal/collect/metadata-collector.ts
|
|
1325
1364
|
var DETACHED_HEAD = "HEAD";
|
|
1326
1365
|
function resolveScmBranch(env, gitBranch) {
|
|
1327
1366
|
const pick = (...names) => {
|
|
@@ -1366,6 +1405,19 @@ function resolveScmBaseBranch(env) {
|
|
|
1366
1405
|
const trimmed = target?.trim();
|
|
1367
1406
|
return trimmed ? normalizeRef(trimmed) : void 0;
|
|
1368
1407
|
}
|
|
1408
|
+
function detectCi(env) {
|
|
1409
|
+
return Boolean(
|
|
1410
|
+
env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.CI && env.CI !== "false" && env.CI !== "0"
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
function resolveRunOrigin(env, warn) {
|
|
1414
|
+
const raw = env[PIWI_ORIGIN_ENV.kind]?.trim();
|
|
1415
|
+
let kind = detectCi(env) ? "ci" : "local";
|
|
1416
|
+
if (raw && isRunOriginKind(raw)) kind = raw;
|
|
1417
|
+
else if (raw) warn?.(`Ignoring ${PIWI_ORIGIN_ENV.kind}=${raw}: not a known run origin, recording "${kind}"`);
|
|
1418
|
+
const ref = parseRunOriginRef(env[PIWI_ORIGIN_ENV.ref]);
|
|
1419
|
+
return ref === void 0 ? { kind } : { kind, ref };
|
|
1420
|
+
}
|
|
1369
1421
|
function normalizeRef(ref) {
|
|
1370
1422
|
return ref.replace(/^refs\/heads\//, "");
|
|
1371
1423
|
}
|
|
@@ -1375,7 +1427,10 @@ var MetadataCollector = class {
|
|
|
1375
1427
|
}
|
|
1376
1428
|
/** Collect all available metadata from the environment, config, and suite */
|
|
1377
1429
|
collect(config, suite, options) {
|
|
1378
|
-
const metadata = {
|
|
1430
|
+
const metadata = {
|
|
1431
|
+
workingDir: process.cwd(),
|
|
1432
|
+
[RUN_ORIGIN_METADATA_KEY]: resolveRunOrigin(process.env, (message) => this.logger.warn(message))
|
|
1433
|
+
};
|
|
1379
1434
|
if (options.projectDescription) metadata.projectDescription = options.projectDescription;
|
|
1380
1435
|
if (options.relatedIssue) metadata.relatedIssue = options.relatedIssue;
|
|
1381
1436
|
if (options.ciInfo) metadata.ciInfo = options.ciInfo;
|
|
@@ -1552,6 +1607,15 @@ var MetadataCollector = class {
|
|
|
1552
1607
|
ci.buildUrl = `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`;
|
|
1553
1608
|
}
|
|
1554
1609
|
ci.jobName = env.AGENT_JOBNAME;
|
|
1610
|
+
} else if (env.BITBUCKET_BUILD_NUMBER) {
|
|
1611
|
+
ci.provider = "Bitbucket Pipelines";
|
|
1612
|
+
ci.buildNumber = env.BITBUCKET_BUILD_NUMBER;
|
|
1613
|
+
ci.pipelineId = env.BITBUCKET_PIPELINE_UUID;
|
|
1614
|
+
ci.jobId = env.BITBUCKET_STEP_UUID;
|
|
1615
|
+
ci.repository = env.BITBUCKET_REPO_FULL_NAME;
|
|
1616
|
+
if (env.BITBUCKET_REPO_FULL_NAME) {
|
|
1617
|
+
ci.buildUrl = `https://bitbucket.org/${env.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`;
|
|
1618
|
+
}
|
|
1555
1619
|
} else if (env.CI) {
|
|
1556
1620
|
ci.provider = "Unknown CI";
|
|
1557
1621
|
ci.detected = true;
|
|
@@ -3700,6 +3764,131 @@ function isDueForAriaSample(testInfo) {
|
|
|
3700
3764
|
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
3701
3765
|
}
|
|
3702
3766
|
|
|
3767
|
+
// src/internal/support/base-url-check.ts
|
|
3768
|
+
var BASE_URL_CHECK_ATTEMPTS = 3;
|
|
3769
|
+
var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
|
|
3770
|
+
var BASE_URL_CHECK_DELAY_MS = 2e3;
|
|
3771
|
+
var GATEWAY_ANSWERS = {
|
|
3772
|
+
502: "bad gateway",
|
|
3773
|
+
503: "service unavailable",
|
|
3774
|
+
504: "gateway timeout"
|
|
3775
|
+
};
|
|
3776
|
+
function cliProjectNames(argv = process.argv) {
|
|
3777
|
+
let names = null;
|
|
3778
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3779
|
+
const arg = argv[i];
|
|
3780
|
+
if (arg.startsWith("--project=")) {
|
|
3781
|
+
(names ??= []).push(arg.slice("--project=".length));
|
|
3782
|
+
} else if (arg === "--project") {
|
|
3783
|
+
names ??= [];
|
|
3784
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith("-")) names.push(argv[++i]);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
return names;
|
|
3788
|
+
}
|
|
3789
|
+
function wildcard(pattern) {
|
|
3790
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
3791
|
+
return new RegExp(`^${escaped.join(".*")}$`, "i");
|
|
3792
|
+
}
|
|
3793
|
+
function selectedProjects(projects, argv = process.argv) {
|
|
3794
|
+
const names = cliProjectNames(argv);
|
|
3795
|
+
if (!names || names.length === 0) return projects;
|
|
3796
|
+
const patterns = names.map(wildcard);
|
|
3797
|
+
const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
3798
|
+
if (!argv.includes("--no-deps")) {
|
|
3799
|
+
const byName = new Map(projects.map((p) => [p.name ?? "", p]));
|
|
3800
|
+
const queue = [...chosen];
|
|
3801
|
+
while (queue.length > 0) {
|
|
3802
|
+
for (const dep of queue.shift().dependencies ?? []) {
|
|
3803
|
+
const project = byName.get(dep);
|
|
3804
|
+
if (project && !chosen.has(project)) {
|
|
3805
|
+
chosen.add(project);
|
|
3806
|
+
queue.push(project);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return projects.filter((p) => chosen.has(p));
|
|
3812
|
+
}
|
|
3813
|
+
function asProxy(value) {
|
|
3814
|
+
const proxy = value;
|
|
3815
|
+
return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
|
|
3816
|
+
}
|
|
3817
|
+
function baseUrlTargets(config, argv = process.argv) {
|
|
3818
|
+
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
3819
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3820
|
+
for (const project of selectedProjects(projects, argv)) {
|
|
3821
|
+
const url = project.use?.baseURL;
|
|
3822
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
3823
|
+
const target = targets.get(url);
|
|
3824
|
+
if (target) {
|
|
3825
|
+
if (project.name) target.projects.push(project.name);
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
targets.set(url, {
|
|
3829
|
+
url,
|
|
3830
|
+
projects: project.name ? [project.name] : [],
|
|
3831
|
+
ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
|
|
3832
|
+
proxy: asProxy(project.use?.proxy)
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
return [...targets.values()];
|
|
3836
|
+
}
|
|
3837
|
+
function requestError(error) {
|
|
3838
|
+
const first = errorMessage(error).split("\n")[0].trim();
|
|
3839
|
+
return first.replace(/^apiRequestContext\.\w+:\s*/, "");
|
|
3840
|
+
}
|
|
3841
|
+
var playwrightProbe = async (target, timeoutMs) => {
|
|
3842
|
+
const { request } = await import("@playwright/test");
|
|
3843
|
+
const context = await request.newContext({ ignoreHTTPSErrors: target.ignoreHTTPSErrors, proxy: target.proxy });
|
|
3844
|
+
try {
|
|
3845
|
+
const response = await context.get(target.url, { timeout: timeoutMs, maxRedirects: 0, failOnStatusCode: false });
|
|
3846
|
+
return { status: response.status() };
|
|
3847
|
+
} catch (error) {
|
|
3848
|
+
return { error: requestError(error) };
|
|
3849
|
+
} finally {
|
|
3850
|
+
await context.dispose().catch(() => {
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
};
|
|
3854
|
+
async function checkBaseUrl(target, options = {}) {
|
|
3855
|
+
const attempts = Math.max(1, options.attempts ?? BASE_URL_CHECK_ATTEMPTS);
|
|
3856
|
+
const timeoutMs = options.timeoutMs ?? BASE_URL_CHECK_TIMEOUT_MS;
|
|
3857
|
+
const delayMs = options.delayMs ?? BASE_URL_CHECK_DELAY_MS;
|
|
3858
|
+
const probe = options.probe ?? playwrightProbe;
|
|
3859
|
+
let reason = "";
|
|
3860
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
3861
|
+
const outcome = await probe(target, timeoutMs);
|
|
3862
|
+
if ("status" in outcome) {
|
|
3863
|
+
const gateway = GATEWAY_ANSWERS[outcome.status];
|
|
3864
|
+
if (!gateway) return null;
|
|
3865
|
+
reason = `${outcome.status} ${gateway}`;
|
|
3866
|
+
} else {
|
|
3867
|
+
reason = outcome.error;
|
|
3868
|
+
}
|
|
3869
|
+
if (attempt < attempts) await new Promise((resolve7) => setTimeout(resolve7, delayMs));
|
|
3870
|
+
}
|
|
3871
|
+
return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
|
|
3872
|
+
}
|
|
3873
|
+
async function checkBaseUrls(config, options = {}) {
|
|
3874
|
+
const targets = baseUrlTargets(config, options.argv);
|
|
3875
|
+
const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
|
|
3876
|
+
const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
|
|
3877
|
+
if (down.length === 0) return targets;
|
|
3878
|
+
const lines = down.map(({ target, reason }) => {
|
|
3879
|
+
const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
|
|
3880
|
+
return ` ${target.url}${used}: ${reason}`;
|
|
3881
|
+
});
|
|
3882
|
+
const message = [
|
|
3883
|
+
`[Piwi Dashboard] The app under test did not answer, so no test ran:`,
|
|
3884
|
+
...lines,
|
|
3885
|
+
`Bring the environment back and run again, or set checkBaseUrl: false (PIWI_CHECK_BASE_URL=false) to run the tests anyway.`
|
|
3886
|
+
].join("\n");
|
|
3887
|
+
const error = new Error(message);
|
|
3888
|
+
error.stack = message;
|
|
3889
|
+
throw error;
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3703
3892
|
// src/internal/manifest/collect.ts
|
|
3704
3893
|
var fs12 = __toESM(require("fs"));
|
|
3705
3894
|
var path11 = __toESM(require("path"));
|
|
@@ -3799,6 +3988,10 @@ function createGlobalSetup(options, userSetup) {
|
|
|
3799
3988
|
if (userSetup) return userSetup(config);
|
|
3800
3989
|
return;
|
|
3801
3990
|
}
|
|
3991
|
+
if (opts.checkBaseUrl === true) {
|
|
3992
|
+
const checked = await checkBaseUrls(config ?? {});
|
|
3993
|
+
for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
|
|
3994
|
+
}
|
|
3802
3995
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
3803
3996
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
3804
3997
|
if (userSetup) return userSetup(config);
|
|
@@ -3906,7 +4099,7 @@ var path13 = __toESM(require("path"));
|
|
|
3906
4099
|
|
|
3907
4100
|
// src/internal/probe/plan.ts
|
|
3908
4101
|
function probeRunMetadata(base = {}) {
|
|
3909
|
-
return { ...base, piwiProbe: true };
|
|
4102
|
+
return { ...base, piwiProbe: true, [RUN_ORIGIN_METADATA_KEY]: { kind: "probe" } };
|
|
3910
4103
|
}
|
|
3911
4104
|
function parseRouteKey(routeKey2) {
|
|
3912
4105
|
const space = routeKey2.indexOf(" ");
|
|
@@ -4515,7 +4708,12 @@ function loadFlakePlan() {
|
|
|
4515
4708
|
}
|
|
4516
4709
|
}
|
|
4517
4710
|
function flakeRunMetadata(stamp, base = {}) {
|
|
4518
|
-
|
|
4711
|
+
const ref = parseRunOriginRef(stamp.experimentId);
|
|
4712
|
+
return {
|
|
4713
|
+
...base,
|
|
4714
|
+
[FLAKE_LAB_RUN_METADATA_KEY]: { experimentId: stamp.experimentId, armId: stamp.armId },
|
|
4715
|
+
[RUN_ORIGIN_METADATA_KEY]: ref === void 0 ? { kind: "flake-lab" } : { kind: "flake-lab", ref }
|
|
4716
|
+
};
|
|
4519
4717
|
}
|
|
4520
4718
|
function relativeTestFile2(file) {
|
|
4521
4719
|
return file ? path14.relative(process.cwd(), file).split(path14.sep).join("/") : null;
|
|
@@ -5099,6 +5297,9 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
5099
5297
|
|
|
5100
5298
|
// src/internal/submit/run-submitter.ts
|
|
5101
5299
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
5300
|
+
function markSubmitFallback(run, path31, reason) {
|
|
5301
|
+
run.metadata.ingestHealth = { submitFallback: { path: path31, reason } };
|
|
5302
|
+
}
|
|
5102
5303
|
var RunSubmitter = class {
|
|
5103
5304
|
/**
|
|
5104
5305
|
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
@@ -5174,13 +5375,21 @@ var RunSubmitter = class {
|
|
|
5174
5375
|
}
|
|
5175
5376
|
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
5176
5377
|
let outcome = { done: false, output: null };
|
|
5177
|
-
|
|
5178
|
-
|
|
5378
|
+
let fallbackReason = null;
|
|
5379
|
+
if (sm?.enabled && sm?.runId != null) {
|
|
5380
|
+
if (sm.lostResults) fallbackReason = "results-lost";
|
|
5381
|
+
else {
|
|
5382
|
+
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
5383
|
+
if (!outcome.done) fallbackReason = "finish-failed";
|
|
5384
|
+
}
|
|
5179
5385
|
}
|
|
5180
5386
|
if (!outcome.done && !this.httpClient.closed && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
5387
|
+
if (fallbackReason) markSubmitFallback(run, "upload", fallbackReason);
|
|
5181
5388
|
outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
|
|
5389
|
+
if (!outcome.done) fallbackReason = "upload-failed";
|
|
5182
5390
|
}
|
|
5183
5391
|
if (!outcome.done) {
|
|
5392
|
+
if (fallbackReason) markSubmitFallback(run, "submit", fallbackReason);
|
|
5184
5393
|
outcome = await this.tryUploadJSON(run, overallStatus, duration, auth);
|
|
5185
5394
|
}
|
|
5186
5395
|
if (outcome.output) {
|
|
@@ -5490,6 +5699,35 @@ function parsePsOutput(text) {
|
|
|
5490
5699
|
}
|
|
5491
5700
|
return out;
|
|
5492
5701
|
}
|
|
5702
|
+
function parseWindowsProcesses(text) {
|
|
5703
|
+
let data;
|
|
5704
|
+
try {
|
|
5705
|
+
data = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
5706
|
+
} catch {
|
|
5707
|
+
return [];
|
|
5708
|
+
}
|
|
5709
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
5710
|
+
const out = [];
|
|
5711
|
+
for (const row of rows) {
|
|
5712
|
+
if (typeof row !== "object" || row === null) continue;
|
|
5713
|
+
const record = row;
|
|
5714
|
+
const pid = Number(record.ProcessId);
|
|
5715
|
+
const ppid = Number(record.ParentProcessId);
|
|
5716
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
|
|
5717
|
+
const workingSet = Number(record.WorkingSetSize);
|
|
5718
|
+
const user = Number(record.UserModeTime);
|
|
5719
|
+
const kernel = Number(record.KernelModeTime);
|
|
5720
|
+
out.push({
|
|
5721
|
+
pid,
|
|
5722
|
+
ppid,
|
|
5723
|
+
rssKb: Number.isFinite(workingSet) ? Math.round(workingSet / 1024) : 0,
|
|
5724
|
+
cpuMs: ((Number.isFinite(user) ? user : 0) + (Number.isFinite(kernel) ? kernel : 0)) / 1e4,
|
|
5725
|
+
name: typeof record.Name === "string" ? record.Name : "",
|
|
5726
|
+
commandLine: typeof record.CommandLine === "string" ? record.CommandLine : ""
|
|
5727
|
+
});
|
|
5728
|
+
}
|
|
5729
|
+
return out;
|
|
5730
|
+
}
|
|
5493
5731
|
function roleOf(comm, cmdline) {
|
|
5494
5732
|
const command = `${comm} ${cmdline}`;
|
|
5495
5733
|
if (/workerProcessEntry/.test(cmdline)) return "worker";
|
|
@@ -6813,10 +7051,27 @@ var path22 = __toESM(require("path"));
|
|
|
6813
7051
|
var import_node_child_process2 = require("child_process");
|
|
6814
7052
|
var WALK_FILE_CAP = 1e4;
|
|
6815
7053
|
var SERIES_CAP = 3600;
|
|
7054
|
+
var TIMED_CAP = 3600;
|
|
6816
7055
|
var PLAYWRIGHT_TEMP = /^(playwright_\w+dev_profile-|playwright-artifacts-)/;
|
|
6817
7056
|
function round12(value) {
|
|
6818
7057
|
return Math.round(value * 10) / 10;
|
|
6819
7058
|
}
|
|
7059
|
+
function mean(a, b) {
|
|
7060
|
+
return round12((a + b) / 2);
|
|
7061
|
+
}
|
|
7062
|
+
function mean2(a, b) {
|
|
7063
|
+
return Math.round((a + b) / 2 * 100) / 100;
|
|
7064
|
+
}
|
|
7065
|
+
function pushTimed(points, point, merge) {
|
|
7066
|
+
points.push(point);
|
|
7067
|
+
if (points.length <= TIMED_CAP) return points;
|
|
7068
|
+
const merged = [];
|
|
7069
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
7070
|
+
const next = points[i + 1];
|
|
7071
|
+
merged.push(next ? [points[i][0], merge(points[i][1], next[1])] : points[i]);
|
|
7072
|
+
}
|
|
7073
|
+
return merged;
|
|
7074
|
+
}
|
|
6820
7075
|
function pct(part, whole) {
|
|
6821
7076
|
return whole > 0 ? round12(part / whole * 100) : null;
|
|
6822
7077
|
}
|
|
@@ -6841,6 +7096,21 @@ function defaultPs() {
|
|
|
6841
7096
|
);
|
|
6842
7097
|
});
|
|
6843
7098
|
}
|
|
7099
|
+
function defaultWindowsQuery() {
|
|
7100
|
+
return new Promise((resolve7, reject) => {
|
|
7101
|
+
(0, import_node_child_process2.execFile)(
|
|
7102
|
+
"powershell.exe",
|
|
7103
|
+
[
|
|
7104
|
+
"-NoProfile",
|
|
7105
|
+
"-NonInteractive",
|
|
7106
|
+
"-Command",
|
|
7107
|
+
"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID -and $_.ParentProcessId -ne $PID } | Select-Object ProcessId,ParentProcessId,WorkingSetSize,UserModeTime,KernelModeTime,Name,CommandLine | ConvertTo-Json -Compress"
|
|
7108
|
+
],
|
|
7109
|
+
{ timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
|
|
7110
|
+
(error, stdout) => error ? reject(error) : resolve7(stdout)
|
|
7111
|
+
);
|
|
7112
|
+
});
|
|
7113
|
+
}
|
|
6844
7114
|
function freeBytes(dir) {
|
|
6845
7115
|
let current = path22.resolve(dir);
|
|
6846
7116
|
for (; ; ) {
|
|
@@ -6888,6 +7158,12 @@ var RunSampler = class {
|
|
|
6888
7158
|
this.cpuStart = null;
|
|
6889
7159
|
this.cpuPrevious = null;
|
|
6890
7160
|
this.series = [];
|
|
7161
|
+
this.cpuPoints = [];
|
|
7162
|
+
this.memoryPoints = [];
|
|
7163
|
+
/** Each worker process's tree, by the worker's process key. */
|
|
7164
|
+
this.workerTrees = /* @__PURE__ */ new Map();
|
|
7165
|
+
/** When the workers' CPU was last read, for the cores they used since. */
|
|
7166
|
+
this.workersReadAt = null;
|
|
6891
7167
|
this.pressureStart = { cpu: null, memory: null };
|
|
6892
7168
|
this.containerStart = null;
|
|
6893
7169
|
this.memoryKind = null;
|
|
@@ -6907,8 +7183,10 @@ var RunSampler = class {
|
|
|
6907
7183
|
this.tmpDir = options.tmpDir ?? os8.tmpdir();
|
|
6908
7184
|
this.intervalMs = options.intervalMs ?? 1e3;
|
|
6909
7185
|
this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
|
|
7186
|
+
this.windowsTreeEvery = Math.max(1, options.windowsTreeEvery ?? 3);
|
|
6910
7187
|
this.now = options.now ?? Date.now;
|
|
6911
7188
|
this.runPs = options.runPs ?? defaultPs;
|
|
7189
|
+
this.runWindows = options.runWindows ?? defaultWindowsQuery;
|
|
6912
7190
|
this.linux = this.platform === "linux" && this.proc.available();
|
|
6913
7191
|
}
|
|
6914
7192
|
/** Take the first sample and sample on a timer that never keeps the process alive. */
|
|
@@ -6931,11 +7209,24 @@ var RunSampler = class {
|
|
|
6931
7209
|
this.timer = null;
|
|
6932
7210
|
try {
|
|
6933
7211
|
await this.running;
|
|
6934
|
-
await this.sample(true);
|
|
7212
|
+
await this.sample(true, true);
|
|
6935
7213
|
} catch {
|
|
6936
7214
|
}
|
|
6937
7215
|
return this.profile(await this.leftoverBytes());
|
|
6938
7216
|
}
|
|
7217
|
+
/** The series read so far, each point at the time it was read. */
|
|
7218
|
+
timedSeries() {
|
|
7219
|
+
return {
|
|
7220
|
+
startedAt: this.startedAt,
|
|
7221
|
+
cpuPct: [...this.cpuPoints],
|
|
7222
|
+
memoryBytes: [...this.memoryPoints],
|
|
7223
|
+
workers: [...this.workerTrees.values()].map((tree) => ({
|
|
7224
|
+
pid: tree.pid,
|
|
7225
|
+
cpuCores: [...tree.cpuCores],
|
|
7226
|
+
memoryBytes: [...tree.memoryBytes]
|
|
7227
|
+
}))
|
|
7228
|
+
};
|
|
7229
|
+
}
|
|
6939
7230
|
schedule() {
|
|
6940
7231
|
if (this.running) return;
|
|
6941
7232
|
const heavy = this.samples % this.heavyEvery === 0;
|
|
@@ -6946,18 +7237,22 @@ var RunSampler = class {
|
|
|
6946
7237
|
cpuTicks() {
|
|
6947
7238
|
return this.linux ? this.proc.cpuTicks() : nodeCpuTicks();
|
|
6948
7239
|
}
|
|
6949
|
-
async sample(heavy) {
|
|
7240
|
+
async sample(heavy, forceTree = false) {
|
|
7241
|
+
const treeDue = forceTree || this.samples % (this.heavyEvery * this.windowsTreeEvery) === 0;
|
|
6950
7242
|
this.samples++;
|
|
7243
|
+
const at = this.now() - this.startedAt;
|
|
6951
7244
|
const ticks = this.cpuTicks();
|
|
6952
7245
|
if (ticks && this.cpuPrevious) {
|
|
6953
7246
|
const busy = pct(ticks.busy - this.cpuPrevious.busy, ticks.total - this.cpuPrevious.total);
|
|
6954
7247
|
if (busy !== null && this.series.length < SERIES_CAP) this.series.push(busy);
|
|
7248
|
+
if (busy !== null) this.cpuPoints = pushTimed(this.cpuPoints, [at, busy], mean);
|
|
6955
7249
|
}
|
|
6956
7250
|
if (ticks) this.cpuPrevious = ticks;
|
|
6957
7251
|
const availableKb = this.linux ? this.proc.memAvailableKb() : Math.round(os8.freemem() / 1024);
|
|
6958
7252
|
if (availableKb !== null) this.lowestAvailableKb = Math.min(this.lowestAvailableKb ?? Infinity, availableKb);
|
|
6959
|
-
if (this.linux) await this.sampleLinuxTree(heavy);
|
|
6960
|
-
else if (this.platform === "darwin" && heavy) await this.samplePsTree();
|
|
7253
|
+
if (this.linux) await this.sampleLinuxTree(heavy, at);
|
|
7254
|
+
else if (this.platform === "darwin" && heavy) await this.samplePsTree(at);
|
|
7255
|
+
else if (this.platform === "win32" && treeDue) await this.sampleWindowsTree(at);
|
|
6961
7256
|
if (heavy) await this.sampleDisk();
|
|
6962
7257
|
}
|
|
6963
7258
|
track(key, role, cpuMs, waitMs) {
|
|
@@ -6977,6 +7272,49 @@ var RunSampler = class {
|
|
|
6977
7272
|
lastWaitMs: waitMs
|
|
6978
7273
|
});
|
|
6979
7274
|
}
|
|
7275
|
+
/**
|
|
7276
|
+
* The worker each process of the tree belongs to: a worker process itself,
|
|
7277
|
+
* or the worker it descends from (its browsers and their children). The
|
|
7278
|
+
* runner and the web server belong to none. The tree lists parents first.
|
|
7279
|
+
*/
|
|
7280
|
+
workersOf(tree, roles, keyOf2) {
|
|
7281
|
+
const out = /* @__PURE__ */ new Map();
|
|
7282
|
+
for (const proc of tree) {
|
|
7283
|
+
if (roles.get(proc.pid) === "worker") out.set(proc.pid, { key: keyOf2(proc), pid: proc.pid });
|
|
7284
|
+
else {
|
|
7285
|
+
const parent = out.get(proc.ppid);
|
|
7286
|
+
if (parent) out.set(proc.pid, parent);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
return out;
|
|
7290
|
+
}
|
|
7291
|
+
/** The CPU a process used since the previous sample: all of it for one born since, none at the first sample. */
|
|
7292
|
+
cpuSince(key, cpuMs) {
|
|
7293
|
+
const known = this.tracked.get(key);
|
|
7294
|
+
if (known) return Math.max(0, cpuMs - known.lastCpuMs);
|
|
7295
|
+
return this.samples === 1 ? 0 : cpuMs;
|
|
7296
|
+
}
|
|
7297
|
+
/**
|
|
7298
|
+
* Add a reading to each worker's tree: the cores its processes used since
|
|
7299
|
+
* the previous reading, and on heavy samples their memory.
|
|
7300
|
+
*/
|
|
7301
|
+
noteWorkers(workers, cpuMsByWorker, bytesByWorker, at) {
|
|
7302
|
+
const elapsed = this.workersReadAt === null ? 0 : at - this.workersReadAt;
|
|
7303
|
+
for (const { key, pid } of new Map([...workers.values()].map((w) => [w.key, w])).values()) {
|
|
7304
|
+
let tree = this.workerTrees.get(key);
|
|
7305
|
+
if (!tree) {
|
|
7306
|
+
tree = { pid, cpuCores: [], memoryBytes: [] };
|
|
7307
|
+
this.workerTrees.set(key, tree);
|
|
7308
|
+
}
|
|
7309
|
+
if (elapsed > 0) {
|
|
7310
|
+
const cores = Math.round((cpuMsByWorker.get(key) ?? 0) / elapsed * 100) / 100;
|
|
7311
|
+
tree.cpuCores = pushTimed(tree.cpuCores, [at, cores], mean2);
|
|
7312
|
+
}
|
|
7313
|
+
const bytes = bytesByWorker?.get(key);
|
|
7314
|
+
if (bytes !== void 0) tree.memoryBytes = pushTimed(tree.memoryBytes, [at, bytes], Math.max);
|
|
7315
|
+
}
|
|
7316
|
+
this.workersReadAt = at;
|
|
7317
|
+
}
|
|
6980
7318
|
/** The role of each process of the tree, a child of the runner being the web server unless it says otherwise. */
|
|
6981
7319
|
resolveRoles(tree, explicit) {
|
|
6982
7320
|
const roles = /* @__PURE__ */ new Map([[this.rootPid, "runner"]]);
|
|
@@ -6987,7 +7325,7 @@ var RunSampler = class {
|
|
|
6987
7325
|
}
|
|
6988
7326
|
return roles;
|
|
6989
7327
|
}
|
|
6990
|
-
async sampleLinuxTree(heavy) {
|
|
7328
|
+
async sampleLinuxTree(heavy, at) {
|
|
6991
7329
|
const table = this.proc.table();
|
|
6992
7330
|
const root = table.get(this.rootPid);
|
|
6993
7331
|
if (!root) return;
|
|
@@ -6998,12 +7336,22 @@ var RunSampler = class {
|
|
|
6998
7336
|
const known = this.tracked.get(key)?.role;
|
|
6999
7337
|
return known && !heavy ? known : roleOf(proc.comm, this.proc.cmdline(proc.pid));
|
|
7000
7338
|
});
|
|
7339
|
+
const workers = this.workersOf(tree, roles, (proc) => `${proc.pid}:${proc.start}`);
|
|
7340
|
+
const cpuMsByWorker = /* @__PURE__ */ new Map();
|
|
7001
7341
|
for (const proc of tree) {
|
|
7342
|
+
const key = `${proc.pid}:${proc.start}`;
|
|
7343
|
+
const worker = workers.get(proc.pid);
|
|
7344
|
+
if (worker) {
|
|
7345
|
+
cpuMsByWorker.set(worker.key, (cpuMsByWorker.get(worker.key) ?? 0) + this.cpuSince(key, proc.cpuMs));
|
|
7346
|
+
}
|
|
7002
7347
|
const waitMs = this.proc.runWaitMs(proc.pid);
|
|
7003
7348
|
if (waitMs !== null) this.waitMeasured = true;
|
|
7004
|
-
this.track(
|
|
7349
|
+
this.track(key, roles.get(proc.pid) ?? "other", proc.cpuMs, waitMs);
|
|
7350
|
+
}
|
|
7351
|
+
if (!heavy) {
|
|
7352
|
+
this.noteWorkers(workers, cpuMsByWorker, null, at);
|
|
7353
|
+
return;
|
|
7005
7354
|
}
|
|
7006
|
-
if (!heavy) return;
|
|
7007
7355
|
let total = 0;
|
|
7008
7356
|
let largest = null;
|
|
7009
7357
|
const memory = await Promise.all(
|
|
@@ -7013,42 +7361,67 @@ var RunSampler = class {
|
|
|
7013
7361
|
return { proc, kb: this.proc.statusKb(proc.pid, "VmRSS"), fallback: true };
|
|
7014
7362
|
})
|
|
7015
7363
|
);
|
|
7364
|
+
const bytesByWorker = /* @__PURE__ */ new Map();
|
|
7016
7365
|
for (const { proc, kb, fallback } of memory) {
|
|
7017
7366
|
if (kb === null) continue;
|
|
7018
7367
|
if (fallback) this.rssFallbackPids.add(`${proc.pid}:${proc.start}`);
|
|
7019
7368
|
total += kb * 1024;
|
|
7020
7369
|
if (!largest || kb * 1024 > largest.bytes) largest = { role: roles.get(proc.pid) ?? "other", bytes: kb * 1024 };
|
|
7370
|
+
const worker = workers.get(proc.pid);
|
|
7371
|
+
if (worker) bytesByWorker.set(worker.key, (bytesByWorker.get(worker.key) ?? 0) + kb * 1024);
|
|
7021
7372
|
}
|
|
7022
7373
|
this.memoryKind = memory.some((m) => m.kb !== null && !m.fallback) ? "pss" : memory.length ? "rss" : null;
|
|
7023
|
-
this.notePeak(total, largest);
|
|
7374
|
+
this.notePeak(total, largest, at);
|
|
7375
|
+
this.noteWorkers(workers, cpuMsByWorker, bytesByWorker, at);
|
|
7024
7376
|
}
|
|
7025
|
-
async samplePsTree() {
|
|
7377
|
+
async samplePsTree(at) {
|
|
7026
7378
|
let list;
|
|
7027
7379
|
try {
|
|
7028
7380
|
list = parsePsOutput(await this.runPs());
|
|
7029
7381
|
} catch {
|
|
7030
7382
|
return;
|
|
7031
7383
|
}
|
|
7384
|
+
this.noteTree(list, (proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
|
|
7385
|
+
}
|
|
7386
|
+
async sampleWindowsTree(at) {
|
|
7387
|
+
let list;
|
|
7388
|
+
try {
|
|
7389
|
+
list = parseWindowsProcesses(await this.runWindows());
|
|
7390
|
+
} catch {
|
|
7391
|
+
return;
|
|
7392
|
+
}
|
|
7393
|
+
this.noteTree(list, (proc) => roleOf(proc.name, proc.commandLine), at);
|
|
7394
|
+
}
|
|
7395
|
+
/** One reading of the run's tree: each process's role, CPU time and memory, and each worker's share of both. */
|
|
7396
|
+
noteTree(list, role, at) {
|
|
7032
7397
|
const root = list.find((proc) => proc.pid === this.rootPid);
|
|
7033
7398
|
if (!root) return;
|
|
7034
7399
|
this.treeMeasured = true;
|
|
7035
7400
|
const tree = [root, ...descendantsOf(this.rootPid, list)];
|
|
7036
|
-
const roles = this.resolveRoles(
|
|
7037
|
-
tree.slice(1),
|
|
7038
|
-
(proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command)
|
|
7039
|
-
);
|
|
7401
|
+
const roles = this.resolveRoles(tree.slice(1), role);
|
|
7040
7402
|
let total = 0;
|
|
7041
7403
|
let largest = null;
|
|
7404
|
+
const workers = this.workersOf(tree, roles, (proc) => String(proc.pid));
|
|
7405
|
+
const cpuMsByWorker = /* @__PURE__ */ new Map();
|
|
7406
|
+
const bytesByWorker = /* @__PURE__ */ new Map();
|
|
7042
7407
|
for (const proc of tree) {
|
|
7043
|
-
const
|
|
7044
|
-
|
|
7408
|
+
const procRole = roles.get(proc.pid) ?? "other";
|
|
7409
|
+
const worker = workers.get(proc.pid);
|
|
7410
|
+
if (worker) {
|
|
7411
|
+
const cpuMs = this.cpuSince(String(proc.pid), proc.cpuMs);
|
|
7412
|
+
cpuMsByWorker.set(worker.key, (cpuMsByWorker.get(worker.key) ?? 0) + cpuMs);
|
|
7413
|
+
bytesByWorker.set(worker.key, (bytesByWorker.get(worker.key) ?? 0) + proc.rssKb * 1024);
|
|
7414
|
+
}
|
|
7415
|
+
this.track(String(proc.pid), procRole, proc.cpuMs, null);
|
|
7045
7416
|
total += proc.rssKb * 1024;
|
|
7046
|
-
if (!largest || proc.rssKb * 1024 > largest.bytes) largest = { role, bytes: proc.rssKb * 1024 };
|
|
7417
|
+
if (!largest || proc.rssKb * 1024 > largest.bytes) largest = { role: procRole, bytes: proc.rssKb * 1024 };
|
|
7047
7418
|
}
|
|
7048
7419
|
this.memoryKind = "rss";
|
|
7049
|
-
this.notePeak(total, largest);
|
|
7420
|
+
this.notePeak(total, largest, at);
|
|
7421
|
+
this.noteWorkers(workers, cpuMsByWorker, bytesByWorker, at);
|
|
7050
7422
|
}
|
|
7051
|
-
notePeak(total, largest) {
|
|
7423
|
+
notePeak(total, largest, at) {
|
|
7424
|
+
this.memoryPoints = pushTimed(this.memoryPoints, [at, total], Math.max);
|
|
7052
7425
|
if (this.peakBytes === null || total > this.peakBytes) {
|
|
7053
7426
|
this.peakBytes = total;
|
|
7054
7427
|
this.peakAtMs = this.now() - this.startedAt;
|
|
@@ -7252,8 +7625,8 @@ function sparkline(series, width = 24) {
|
|
|
7252
7625
|
Math.floor(i * series.length / buckets),
|
|
7253
7626
|
Math.floor((i + 1) * series.length / buckets)
|
|
7254
7627
|
);
|
|
7255
|
-
const
|
|
7256
|
-
out += BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor(
|
|
7628
|
+
const mean3 = slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length);
|
|
7629
|
+
out += BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor(mean3 / 100 * BARS.length)))];
|
|
7257
7630
|
}
|
|
7258
7631
|
return out;
|
|
7259
7632
|
}
|
|
@@ -7357,6 +7730,9 @@ function formatMachinePanel(profile, extras) {
|
|
|
7357
7730
|
var REPORT_FINDINGS_CAP = 100;
|
|
7358
7731
|
var REPORT_SERIES_CAP = 240;
|
|
7359
7732
|
var REPORT_WORKER_POINTS_CAP = 200;
|
|
7733
|
+
var TIMELINE_SERIES_CAP = 600;
|
|
7734
|
+
var TIMELINE_WORKER_POINTS_CAP = 300;
|
|
7735
|
+
var TIMELINE_WORKERS_BUDGET = 6e3;
|
|
7360
7736
|
function downsample(series, cap) {
|
|
7361
7737
|
if (series.length <= cap) return series;
|
|
7362
7738
|
const out = [];
|
|
@@ -7366,6 +7742,18 @@ function downsample(series, cap) {
|
|
|
7366
7742
|
}
|
|
7367
7743
|
return out;
|
|
7368
7744
|
}
|
|
7745
|
+
function downsamplePoints(points, cap, merge) {
|
|
7746
|
+
if (points.length <= cap) return points;
|
|
7747
|
+
const out = [];
|
|
7748
|
+
for (let i = 0; i < cap; i++) {
|
|
7749
|
+
const slice = points.slice(Math.floor(i * points.length / cap), Math.floor((i + 1) * points.length / cap));
|
|
7750
|
+
if (slice.length === 0) continue;
|
|
7751
|
+
const values = slice.map((point) => point[1]);
|
|
7752
|
+
const value = merge === "max" ? Math.max(...values) : Math.round(values.reduce((sum, v) => sum + v, 0) / values.length * 10) / 10;
|
|
7753
|
+
out.push([slice[0][0], value]);
|
|
7754
|
+
}
|
|
7755
|
+
return out;
|
|
7756
|
+
}
|
|
7369
7757
|
function nonZero(bytes) {
|
|
7370
7758
|
const out = Object.fromEntries(Object.entries(bytes).filter(([, n]) => (n ?? 0) > 0));
|
|
7371
7759
|
return Object.keys(out).length > 0 ? out : null;
|
|
@@ -7405,6 +7793,93 @@ function openPagesByWorker(censuses) {
|
|
|
7405
7793
|
}
|
|
7406
7794
|
return out;
|
|
7407
7795
|
}
|
|
7796
|
+
function pagesOverTime(censuses) {
|
|
7797
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
7798
|
+
for (const census of censuses) {
|
|
7799
|
+
const list = byWorker.get(census.worker) ?? [];
|
|
7800
|
+
list.push(census);
|
|
7801
|
+
byWorker.set(census.worker, list);
|
|
7802
|
+
}
|
|
7803
|
+
const out = [];
|
|
7804
|
+
for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
|
|
7805
|
+
const pages = /* @__PURE__ */ new Set();
|
|
7806
|
+
const changes = [];
|
|
7807
|
+
let lastAt = -Infinity;
|
|
7808
|
+
for (const census of list) {
|
|
7809
|
+
lastAt = Math.max(lastAt, census.at);
|
|
7810
|
+
for (const birth of census.born) {
|
|
7811
|
+
if (birth.kind !== "page") continue;
|
|
7812
|
+
pages.add(birth.id);
|
|
7813
|
+
changes.push([birth.at, 1]);
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
for (const census of list) {
|
|
7817
|
+
for (const close of census.closed) if (pages.has(close.id)) changes.push([close.at, -1]);
|
|
7818
|
+
}
|
|
7819
|
+
if (changes.length === 0) continue;
|
|
7820
|
+
changes.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
|
|
7821
|
+
const events = [];
|
|
7822
|
+
let open = 0;
|
|
7823
|
+
for (const [at, delta] of changes) {
|
|
7824
|
+
open = Math.max(0, open + delta);
|
|
7825
|
+
const last = events[events.length - 1];
|
|
7826
|
+
if (last && last[0] === at) last[1] = open;
|
|
7827
|
+
else events.push([at, open]);
|
|
7828
|
+
}
|
|
7829
|
+
if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
|
|
7830
|
+
out.push({ worker, events });
|
|
7831
|
+
}
|
|
7832
|
+
return out;
|
|
7833
|
+
}
|
|
7834
|
+
function workerTreesByIndex(samples, censuses) {
|
|
7835
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
7836
|
+
for (const census of censuses) {
|
|
7837
|
+
const list = byPid.get(census.pid) ?? [];
|
|
7838
|
+
list.push({ worker: census.worker, at: census.at });
|
|
7839
|
+
byPid.set(census.pid, list);
|
|
7840
|
+
}
|
|
7841
|
+
const out = /* @__PURE__ */ new Map();
|
|
7842
|
+
for (const tree of samples.workers) {
|
|
7843
|
+
const candidates = byPid.get(tree.pid);
|
|
7844
|
+
const times = [...tree.cpuCores, ...tree.memoryBytes].map(([at]) => samples.startedAt + at);
|
|
7845
|
+
if (!candidates || times.length === 0) continue;
|
|
7846
|
+
const from = Math.min(...times);
|
|
7847
|
+
const to = Math.max(...times);
|
|
7848
|
+
const distance = (at) => at < from ? from - at : at > to ? at - to : 0;
|
|
7849
|
+
const nearest = candidates.reduce((best, c) => distance(c.at) < distance(best.at) ? c : best);
|
|
7850
|
+
if (!out.has(nearest.worker)) out.set(nearest.worker, { worker: nearest.worker, ...tree });
|
|
7851
|
+
}
|
|
7852
|
+
return [...out.values()].sort((a, b) => a.worker - b.worker);
|
|
7853
|
+
}
|
|
7854
|
+
function resourceTimeline(samples, censuses) {
|
|
7855
|
+
const workers = pagesOverTime(censuses);
|
|
7856
|
+
const hasSamples = samples !== null && (samples.cpuPct.length > 0 || samples.memoryBytes.length > 0);
|
|
7857
|
+
if (!hasSamples && workers.length === 0) return null;
|
|
7858
|
+
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
7859
|
+
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
7860
|
+
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
7861
|
+
const relative13 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
7862
|
+
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
7863
|
+
const perWorker = Math.max(
|
|
7864
|
+
60,
|
|
7865
|
+
Math.min(TIMELINE_WORKER_POINTS_CAP, Math.floor(TIMELINE_WORKERS_BUDGET / Math.max(1, 2 * trees.length)))
|
|
7866
|
+
);
|
|
7867
|
+
return {
|
|
7868
|
+
startedAt,
|
|
7869
|
+
cpuPct: hasSamples ? downsamplePoints(relative13(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
|
|
7870
|
+
memoryBytes: hasSamples ? downsamplePoints(relative13(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
|
|
7871
|
+
pages: workers.map(({ worker, events }) => {
|
|
7872
|
+
const points = relative13(events, -startedAt);
|
|
7873
|
+
const last = points.pop();
|
|
7874
|
+
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
7875
|
+
}),
|
|
7876
|
+
workers: trees.map((tree) => ({
|
|
7877
|
+
worker: tree.worker,
|
|
7878
|
+
cpuCores: downsamplePoints(relative13(tree.cpuCores, shift), perWorker, "mean"),
|
|
7879
|
+
memoryBytes: downsamplePoints(relative13(tree.memoryBytes, shift), perWorker, "max")
|
|
7880
|
+
}))
|
|
7881
|
+
};
|
|
7882
|
+
}
|
|
7408
7883
|
function resourceReportWire(input) {
|
|
7409
7884
|
const { report, profile } = input;
|
|
7410
7885
|
return {
|
|
@@ -7415,7 +7890,8 @@ function resourceReportWire(input) {
|
|
|
7415
7890
|
profile: profile ? { ...profile, cpu: { ...profile.cpu, series: downsample(profile.cpu.series, REPORT_SERIES_CAP) } } : null,
|
|
7416
7891
|
workers: openPagesByWorker(input.censuses),
|
|
7417
7892
|
artifactBytes: nonZero(input.artifacts) ?? {},
|
|
7418
|
-
workerHealth: workerHealthOf(input.censuses)
|
|
7893
|
+
workerHealth: workerHealthOf(input.censuses),
|
|
7894
|
+
timeline: resourceTimeline(input.samples ?? null, input.censuses)
|
|
7419
7895
|
};
|
|
7420
7896
|
}
|
|
7421
7897
|
|
|
@@ -8366,6 +8842,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
8366
8842
|
async reportResources() {
|
|
8367
8843
|
if (this.options.captureResources === false) return;
|
|
8368
8844
|
const profile = this.sampler ? await this.sampler.stop() : null;
|
|
8845
|
+
const samples = this.sampler?.timedSeries() ?? null;
|
|
8369
8846
|
this.sampler = null;
|
|
8370
8847
|
const censuses = [...this.resourceCensuses];
|
|
8371
8848
|
if (this.resourcesFile) {
|
|
@@ -8388,6 +8865,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
8388
8865
|
this.resourceReport = resourceReportWire({
|
|
8389
8866
|
report,
|
|
8390
8867
|
profile,
|
|
8868
|
+
samples,
|
|
8391
8869
|
censuses,
|
|
8392
8870
|
artifacts: this.artifactBytes,
|
|
8393
8871
|
shardIndex: this.shardInfo?.current ?? null
|
|
@@ -8448,6 +8926,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
8448
8926
|
/** Playwright reporter hook: called when the full test run finishes */
|
|
8449
8927
|
async onEnd(result) {
|
|
8450
8928
|
if (this.listMode || !this.enabled) return;
|
|
8929
|
+
if (this.startTime === null) {
|
|
8930
|
+
this.logger.debug("The run stopped before any test began \u2014 nothing to report.");
|
|
8931
|
+
return;
|
|
8932
|
+
}
|
|
8451
8933
|
const unrunReason = resolveUnrunReason(result?.status, {
|
|
8452
8934
|
maxFailures: this.maxFailures,
|
|
8453
8935
|
failures: this.failedTests + this.timedOutTests
|
|
@@ -8564,6 +9046,8 @@ var import_node_zlib2 = require("zlib");
|
|
|
8564
9046
|
// ../picker-dom/src/probe.ts
|
|
8565
9047
|
function probeElementAttrs(el, arg) {
|
|
8566
9048
|
const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
|
|
9049
|
+
const countMatches = arg.countMatches !== false;
|
|
9050
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
8567
9051
|
const FIELD_TAGS = ["input", "select", "textarea"];
|
|
8568
9052
|
const textWithout = (root, skip) => {
|
|
8569
9053
|
if (root === skip) return "";
|
|
@@ -8588,42 +9072,45 @@ function probeElementAttrs(el, arg) {
|
|
|
8588
9072
|
}
|
|
8589
9073
|
};
|
|
8590
9074
|
const attrMap = {};
|
|
8591
|
-
|
|
9075
|
+
const keys = arg.testIdAttribute && keep.indexOf(arg.testIdAttribute) === -1 ? [...keep, arg.testIdAttribute] : keep;
|
|
9076
|
+
for (const key of keys) {
|
|
8592
9077
|
const v = el.getAttribute(key) ?? el[key];
|
|
8593
9078
|
attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
|
|
8594
9079
|
}
|
|
8595
9080
|
const r = el.getBoundingClientRect();
|
|
8596
9081
|
const selectorCounts = {};
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
const
|
|
8622
|
-
|
|
9082
|
+
if (countMatches) {
|
|
9083
|
+
try {
|
|
9084
|
+
const doc = el.ownerDocument;
|
|
9085
|
+
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
9086
|
+
const count = (sel) => {
|
|
9087
|
+
try {
|
|
9088
|
+
return doc.querySelectorAll(sel).length;
|
|
9089
|
+
} catch {
|
|
9090
|
+
return void 0;
|
|
9091
|
+
}
|
|
9092
|
+
};
|
|
9093
|
+
if (attrMap[testIdAttr]) {
|
|
9094
|
+
selectorCounts.testId = count(`[${testIdAttr}=${JSON.stringify(attrMap[testIdAttr])}]`);
|
|
9095
|
+
}
|
|
9096
|
+
if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
|
|
9097
|
+
if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
|
|
9098
|
+
if (attrMap["placeholder"]) {
|
|
9099
|
+
selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
|
|
9100
|
+
}
|
|
9101
|
+
if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
|
|
9102
|
+
if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
|
|
9103
|
+
const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
|
|
9104
|
+
if (classList.length > 0) {
|
|
9105
|
+
const classCounts = {};
|
|
9106
|
+
for (const cls of classList) {
|
|
9107
|
+
const n = count(`.${cssEsc(cls)}`);
|
|
9108
|
+
if (n !== void 0) classCounts[cls] = n;
|
|
9109
|
+
}
|
|
9110
|
+
selectorCounts.classes = classCounts;
|
|
8623
9111
|
}
|
|
8624
|
-
|
|
9112
|
+
} catch {
|
|
8625
9113
|
}
|
|
8626
|
-
} catch {
|
|
8627
9114
|
}
|
|
8628
9115
|
let rolePosition = null;
|
|
8629
9116
|
const ancestors = [];
|
|
@@ -8712,8 +9199,8 @@ function probeElementAttrs(el, arg) {
|
|
|
8712
9199
|
}
|
|
8713
9200
|
return total;
|
|
8714
9201
|
};
|
|
8715
|
-
const nodes = doc.querySelectorAll(roleSources);
|
|
8716
|
-
const nodesUsable = nodes.length <= 4e3;
|
|
9202
|
+
const nodes = countMatches ? doc.querySelectorAll(roleSources) : [];
|
|
9203
|
+
const nodesUsable = countMatches && nodes.length <= 4e3;
|
|
8717
9204
|
const rolesUsable = !!targetRole && nodesUsable;
|
|
8718
9205
|
if (rolesUsable) {
|
|
8719
9206
|
let roleCountAll = 0;
|
|
@@ -8747,17 +9234,18 @@ function probeElementAttrs(el, arg) {
|
|
|
8747
9234
|
};
|
|
8748
9235
|
}
|
|
8749
9236
|
}
|
|
8750
|
-
if (textNeedle) {
|
|
9237
|
+
if (textNeedle && countMatches) {
|
|
8751
9238
|
const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
|
|
8752
9239
|
if (textCount >= 0) selectorCounts.text = textCount;
|
|
8753
9240
|
}
|
|
8754
|
-
if (rolesUsable || textNeedle) {
|
|
9241
|
+
if (rolesUsable || textNeedle || !countMatches) {
|
|
8755
9242
|
const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
|
|
8756
9243
|
const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
|
|
8757
9244
|
const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
|
|
8758
9245
|
const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
|
|
9246
|
+
const testIdName = testIdAttr.toLowerCase();
|
|
8759
9247
|
const usableDataAttr = (name, value) => {
|
|
8760
|
-
if (!name || name.slice(0, 5) !== "data-" || name ===
|
|
9248
|
+
if (!name || name.slice(0, 5) !== "data-" || name.toLowerCase() === testIdName) return false;
|
|
8761
9249
|
if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
|
|
8762
9250
|
return !!value && value.length <= 120;
|
|
8763
9251
|
};
|
|
@@ -8787,7 +9275,13 @@ function probeElementAttrs(el, arg) {
|
|
|
8787
9275
|
return digits <= letters;
|
|
8788
9276
|
};
|
|
8789
9277
|
const usableDiscriminator = (t) => !!t && t.length <= 60 && t !== targetText && namesRatherThanReports(t);
|
|
9278
|
+
const hasAtMost = (root, cap) => {
|
|
9279
|
+
const walker = doc.createTreeWalker(root, 1);
|
|
9280
|
+
for (let n = 0; walker.nextNode(); n++) if (n >= cap) return false;
|
|
9281
|
+
return true;
|
|
9282
|
+
};
|
|
8790
9283
|
const discriminatingText = (anc) => {
|
|
9284
|
+
if (!countMatches && !hasAtMost(anc, 200)) return null;
|
|
8791
9285
|
const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
|
|
8792
9286
|
if (heading) {
|
|
8793
9287
|
const t = rawText(heading);
|
|
@@ -8819,7 +9313,7 @@ function probeElementAttrs(el, arg) {
|
|
|
8819
9313
|
depth++;
|
|
8820
9314
|
const tag = (node.tagName || "").toLowerCase();
|
|
8821
9315
|
if (tag === "body" || tag === "html") break;
|
|
8822
|
-
const testId = node.getAttribute(
|
|
9316
|
+
const testId = node.getAttribute(testIdAttr);
|
|
8823
9317
|
const id = node.getAttribute("id");
|
|
8824
9318
|
const explicitRole = node.getAttribute("role");
|
|
8825
9319
|
const ariaLabel = node.getAttribute("aria-label");
|
|
@@ -8839,10 +9333,10 @@ function probeElementAttrs(el, arg) {
|
|
|
8839
9333
|
}
|
|
8840
9334
|
}
|
|
8841
9335
|
}
|
|
8842
|
-
const scopedTextCount = countTextOwners(node, 2e3);
|
|
9336
|
+
const scopedTextCount = countMatches ? countTextOwners(node, 2e3) : -1;
|
|
8843
9337
|
let filterText = null;
|
|
8844
9338
|
try {
|
|
8845
|
-
if (anchorRole && nodesUsable) filterText = discriminatingText(node);
|
|
9339
|
+
if (anchorRole && (nodesUsable || !countMatches)) filterText = discriminatingText(node);
|
|
8846
9340
|
} catch {
|
|
8847
9341
|
filterText = null;
|
|
8848
9342
|
}
|
|
@@ -8855,11 +9349,13 @@ function probeElementAttrs(el, arg) {
|
|
|
8855
9349
|
ariaLabel: ariaLabel || null,
|
|
8856
9350
|
...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
|
|
8857
9351
|
...scopedTextCount >= 0 ? { scopedTextCount } : {},
|
|
8858
|
-
...testId ? { testIdCount: count(`[
|
|
8859
|
-
...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
9352
|
+
...testId && countMatches ? { testIdCount: count(`[${testIdAttr}=${JSON.stringify(testId)}]`) } : {},
|
|
9353
|
+
...id && countMatches ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
8860
9354
|
...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
|
|
8861
|
-
...filterText ? { filterText
|
|
8862
|
-
...
|
|
9355
|
+
...filterText ? { filterText } : {},
|
|
9356
|
+
...filterText && countMatches ? { filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
|
|
9357
|
+
...dataAttr ? { dataAttr } : {},
|
|
9358
|
+
...dataAttr && countMatches ? { dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
|
|
8863
9359
|
});
|
|
8864
9360
|
}
|
|
8865
9361
|
node = node.parentElement;
|
|
@@ -8897,6 +9393,17 @@ function installPickerOverlay(arg) {
|
|
|
8897
9393
|
return;
|
|
8898
9394
|
}
|
|
8899
9395
|
const Z = 2147483600;
|
|
9396
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
9397
|
+
const words = {
|
|
9398
|
+
banner: "Piwi inspector \u2014 click any element to generate locators for it",
|
|
9399
|
+
keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
9400
|
+
keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
9401
|
+
analyzing: "Analyzing element\u2026"
|
|
9402
|
+
};
|
|
9403
|
+
for (const key of Object.keys(words)) {
|
|
9404
|
+
const given = arg.strings?.[key];
|
|
9405
|
+
if (typeof given === "string" && given) words[key] = given;
|
|
9406
|
+
}
|
|
8900
9407
|
const highlight = doc.createElement("div");
|
|
8901
9408
|
highlight.id = "__piwi_picker_highlight";
|
|
8902
9409
|
highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55),inset 0 0 0 1px rgba(255,255,255,.5);`;
|
|
@@ -8921,27 +9428,53 @@ function installPickerOverlay(arg) {
|
|
|
8921
9428
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
8922
9429
|
const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
|
|
8923
9430
|
const head = doc.createElement("div");
|
|
8924
|
-
|
|
9431
|
+
if (arg.transport === "postMessage") head.textContent = "Click an element to generate locators";
|
|
9432
|
+
else if (arg.failing)
|
|
9433
|
+
head.innerHTML = `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}`;
|
|
9434
|
+
else head.textContent = words.banner;
|
|
8925
9435
|
const locatorLine = doc.createElement("div");
|
|
8926
9436
|
locatorLine.id = "__piwi_picker_locator";
|
|
8927
9437
|
locatorLine.style.cssText = `display:none;margin-top:7px;padding:5px 9px;border-radius:7px;background:#0b1120;border:1px solid #4c1d95;font:13.5px/1.55 ${MONO};word-break:break-word;overflow-wrap:anywhere;`;
|
|
8928
9438
|
const foot = doc.createElement("div");
|
|
8929
9439
|
foot.id = "__piwi_picker_foot";
|
|
8930
9440
|
foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
|
|
8931
|
-
foot.textContent =
|
|
9441
|
+
foot.textContent = words.keys;
|
|
8932
9442
|
banner.appendChild(head);
|
|
8933
9443
|
banner.appendChild(locatorLine);
|
|
8934
9444
|
banner.appendChild(foot);
|
|
8935
9445
|
const label = doc.createElement("div");
|
|
8936
9446
|
label.id = "__piwi_picker_label";
|
|
8937
9447
|
label.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:7px;padding:4px 8px;font:12.5px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
9448
|
+
const mountParent = () => {
|
|
9449
|
+
let modal = null;
|
|
9450
|
+
try {
|
|
9451
|
+
modal = doc.querySelector("dialog:modal");
|
|
9452
|
+
} catch {
|
|
9453
|
+
modal = null;
|
|
9454
|
+
}
|
|
9455
|
+
if (!modal) return doc.body;
|
|
9456
|
+
const s = g.getComputedStyle(modal);
|
|
9457
|
+
const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
|
|
9458
|
+
return holdsFixed ? doc.body : modal;
|
|
9459
|
+
};
|
|
9460
|
+
const mount = (...nodes) => {
|
|
9461
|
+
const parent = mountParent();
|
|
9462
|
+
for (const node of nodes) parent.appendChild(node);
|
|
9463
|
+
if (parent !== doc.body) {
|
|
9464
|
+
parent.addEventListener(
|
|
9465
|
+
"close",
|
|
9466
|
+
() => {
|
|
9467
|
+
for (const node of nodes) if (node.parentNode === parent) doc.body.appendChild(node);
|
|
9468
|
+
},
|
|
9469
|
+
{ once: true }
|
|
9470
|
+
);
|
|
9471
|
+
}
|
|
9472
|
+
};
|
|
9473
|
+
mount(highlight, label, banner);
|
|
8941
9474
|
const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
8942
9475
|
const describe = (el) => {
|
|
8943
9476
|
const tag = (el.tagName || "?").toLowerCase();
|
|
8944
|
-
const testId = el.getAttribute && el.getAttribute(
|
|
9477
|
+
const testId = el.getAttribute && el.getAttribute(testIdAttr);
|
|
8945
9478
|
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
8946
9479
|
if (el.labels && el.labels.length > 0) {
|
|
8947
9480
|
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
@@ -8976,7 +9509,7 @@ function installPickerOverlay(arg) {
|
|
|
8976
9509
|
const el = chain2[i];
|
|
8977
9510
|
const tag = (el.tagName || "").toLowerCase();
|
|
8978
9511
|
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
8979
|
-
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(
|
|
9512
|
+
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(testIdAttr))) return i;
|
|
8980
9513
|
}
|
|
8981
9514
|
return 0;
|
|
8982
9515
|
};
|
|
@@ -9032,7 +9565,7 @@ function installPickerOverlay(arg) {
|
|
|
9032
9565
|
locatorLine.style.display = "block";
|
|
9033
9566
|
}
|
|
9034
9567
|
placeLabel(r);
|
|
9035
|
-
foot.textContent =
|
|
9568
|
+
foot.textContent = words.keysHovering;
|
|
9036
9569
|
};
|
|
9037
9570
|
const redescribe = () => {
|
|
9038
9571
|
labeled = null;
|
|
@@ -9095,6 +9628,13 @@ function installPickerOverlay(arg) {
|
|
|
9095
9628
|
banner.remove();
|
|
9096
9629
|
if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
|
|
9097
9630
|
};
|
|
9631
|
+
const teardown = () => {
|
|
9632
|
+
removePickingListeners();
|
|
9633
|
+
removeSuppressed();
|
|
9634
|
+
doc.removeEventListener("click", stop, true);
|
|
9635
|
+
doc.removeEventListener("keydown", stop, true);
|
|
9636
|
+
cleanup();
|
|
9637
|
+
};
|
|
9098
9638
|
const reportPicked = (el) => {
|
|
9099
9639
|
removePickingListeners();
|
|
9100
9640
|
highlight.style.display = "none";
|
|
@@ -9105,13 +9645,13 @@ function installPickerOverlay(arg) {
|
|
|
9105
9645
|
g.__piwiSnapshotExtras?.onPick?.();
|
|
9106
9646
|
doc.addEventListener("click", stop, true);
|
|
9107
9647
|
doc.addEventListener("keydown", stop, true);
|
|
9108
|
-
foot.textContent =
|
|
9648
|
+
foot.textContent = words.analyzing;
|
|
9109
9649
|
g.parent.postMessage({ type: "elementPicked", attrs }, "*");
|
|
9110
9650
|
} else {
|
|
9111
9651
|
removeSuppressed();
|
|
9112
9652
|
g.__piwiPickedElement = el;
|
|
9113
9653
|
g.__piwiPickState = "picked";
|
|
9114
|
-
foot.textContent =
|
|
9654
|
+
foot.textContent = words.analyzing;
|
|
9115
9655
|
}
|
|
9116
9656
|
};
|
|
9117
9657
|
const reportSkipped = () => {
|
|
@@ -9159,7 +9699,7 @@ function installPickerOverlay(arg) {
|
|
|
9159
9699
|
}
|
|
9160
9700
|
});
|
|
9161
9701
|
};
|
|
9162
|
-
g.__piwiPickCleanup =
|
|
9702
|
+
g.__piwiPickCleanup = teardown;
|
|
9163
9703
|
doc.addEventListener("mousemove", onMove, true);
|
|
9164
9704
|
doc.addEventListener("click", onClick, true);
|
|
9165
9705
|
doc.addEventListener("keydown", onKey, true);
|
|
@@ -9181,6 +9721,26 @@ function showAnchorPicker(arg) {
|
|
|
9181
9721
|
}
|
|
9182
9722
|
const Z = 2147483600;
|
|
9183
9723
|
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
9724
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
9725
|
+
const words = {
|
|
9726
|
+
title: "Scope to stable parents (optional)",
|
|
9727
|
+
hint: "Pick one or more parents to anchor the locator to. Hover a row to see the parent.",
|
|
9728
|
+
noneSelected: "No parents selected \u2014 standard alternatives only.",
|
|
9729
|
+
matchesOne: "\u2713 Selection matches exactly 1 element",
|
|
9730
|
+
matchesMany: "\u2717 Selection matches {count} elements",
|
|
9731
|
+
countUnavailable: "Match count unavailable",
|
|
9732
|
+
containsOne: "contains exactly 1 matching element",
|
|
9733
|
+
containsMany: "contains {count} matching elements",
|
|
9734
|
+
needsTestId: `add a ${testIdAttr} to make this usable`,
|
|
9735
|
+
noHook: "no stable hook",
|
|
9736
|
+
use: "Use selected parents",
|
|
9737
|
+
skip: "Skip (Esc)"
|
|
9738
|
+
};
|
|
9739
|
+
for (const key of Object.keys(words)) {
|
|
9740
|
+
const given = arg.strings?.[key];
|
|
9741
|
+
if (typeof given === "string" && given) words[key] = given;
|
|
9742
|
+
}
|
|
9743
|
+
const counted = (template, n) => template.replace("{count}", String(n));
|
|
9184
9744
|
const roleOf2 = (n) => {
|
|
9185
9745
|
const explicit = n.getAttribute && n.getAttribute("role");
|
|
9186
9746
|
if (explicit) return explicit;
|
|
@@ -9196,9 +9756,11 @@ function showAnchorPicker(arg) {
|
|
|
9196
9756
|
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
9197
9757
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
9198
9758
|
};
|
|
9759
|
+
const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
|
|
9199
9760
|
const leafMatches = (scope) => {
|
|
9200
9761
|
try {
|
|
9201
|
-
if (leafTestId) return scope.querySelectorAll(`[
|
|
9762
|
+
if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
|
|
9763
|
+
if (!leafCountable) return -1;
|
|
9202
9764
|
const nodes = scope.querySelectorAll(roleSources);
|
|
9203
9765
|
if (nodes.length > 2e3) return -1;
|
|
9204
9766
|
let matched = 0;
|
|
@@ -9234,12 +9796,12 @@ function showAnchorPicker(arg) {
|
|
|
9234
9796
|
depth++;
|
|
9235
9797
|
const tag = (node.tagName || "").toLowerCase();
|
|
9236
9798
|
if (tag === "body" || tag === "html") break;
|
|
9237
|
-
const testId = node.getAttribute(
|
|
9799
|
+
const testId = node.getAttribute(testIdAttr);
|
|
9238
9800
|
const id = node.getAttribute("id");
|
|
9239
9801
|
const ariaLabel = node.getAttribute("aria-label");
|
|
9240
9802
|
const role = roleOf2(node);
|
|
9241
9803
|
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
9242
|
-
if (testId) info.testIdCount = count(`[
|
|
9804
|
+
if (testId) info.testIdCount = count(`[${testIdAttr}=${JSON.stringify(testId)}]`);
|
|
9243
9805
|
if (id) {
|
|
9244
9806
|
try {
|
|
9245
9807
|
info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
|
|
@@ -9258,7 +9820,7 @@ function showAnchorPicker(arg) {
|
|
|
9258
9820
|
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
9259
9821
|
}
|
|
9260
9822
|
info.scopedLeafCount = leafMatches(node);
|
|
9261
|
-
const hookLabel = testId ?
|
|
9823
|
+
const hookLabel = testId ? `${testIdAttr}="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : words.noHook;
|
|
9262
9824
|
rows.push({
|
|
9263
9825
|
node,
|
|
9264
9826
|
info,
|
|
@@ -9280,13 +9842,13 @@ function showAnchorPicker(arg) {
|
|
|
9280
9842
|
const pr = el.getBoundingClientRect();
|
|
9281
9843
|
pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
|
|
9282
9844
|
const panel = doc.createElement("div");
|
|
9283
|
-
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
9845
|
+
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:min(340px,calc(100vw - 56px));max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
9284
9846
|
const title = doc.createElement("div");
|
|
9285
9847
|
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
9286
|
-
title.textContent =
|
|
9848
|
+
title.textContent = words.title;
|
|
9287
9849
|
const sub = doc.createElement("div");
|
|
9288
9850
|
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
9289
|
-
sub.textContent =
|
|
9851
|
+
sub.textContent = words.hint;
|
|
9290
9852
|
panel.appendChild(title);
|
|
9291
9853
|
panel.appendChild(sub);
|
|
9292
9854
|
const selected = /* @__PURE__ */ new Set();
|
|
@@ -9294,7 +9856,7 @@ function showAnchorPicker(arg) {
|
|
|
9294
9856
|
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
9295
9857
|
const segMatches = (scope, info) => {
|
|
9296
9858
|
try {
|
|
9297
|
-
if (info.testId) return Array.from(scope.querySelectorAll(`[
|
|
9859
|
+
if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
|
|
9298
9860
|
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
9299
9861
|
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
9300
9862
|
if (nodes.length > 2e3) return [];
|
|
@@ -9318,14 +9880,15 @@ function showAnchorPicker(arg) {
|
|
|
9318
9880
|
let total = 0;
|
|
9319
9881
|
for (const s of scopes) {
|
|
9320
9882
|
const c = leafMatches(s);
|
|
9321
|
-
if (c
|
|
9883
|
+
if (c < 0) return -1;
|
|
9884
|
+
total += c;
|
|
9322
9885
|
if (total > 50) return total;
|
|
9323
9886
|
}
|
|
9324
9887
|
return total;
|
|
9325
9888
|
};
|
|
9326
9889
|
const refreshFooter = () => {
|
|
9327
9890
|
if (selected.size === 0) {
|
|
9328
|
-
footer.textContent =
|
|
9891
|
+
footer.textContent = words.noneSelected;
|
|
9329
9892
|
footer.style.color = "#9ca3af";
|
|
9330
9893
|
g.__piwiPickChainCount = void 0;
|
|
9331
9894
|
return;
|
|
@@ -9333,10 +9896,10 @@ function showAnchorPicker(arg) {
|
|
|
9333
9896
|
const c = chainCount();
|
|
9334
9897
|
g.__piwiPickChainCount = c;
|
|
9335
9898
|
if (c === 1) {
|
|
9336
|
-
footer.textContent =
|
|
9899
|
+
footer.textContent = words.matchesOne;
|
|
9337
9900
|
footer.style.color = "#4ade80";
|
|
9338
9901
|
} else {
|
|
9339
|
-
footer.textContent = c < 0 ?
|
|
9902
|
+
footer.textContent = c < 0 ? words.countUnavailable : counted(words.matchesMany, c);
|
|
9340
9903
|
footer.style.color = "#fbbf24";
|
|
9341
9904
|
}
|
|
9342
9905
|
};
|
|
@@ -9353,7 +9916,8 @@ function showAnchorPicker(arg) {
|
|
|
9353
9916
|
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
9354
9917
|
const hint = doc.createElement("span");
|
|
9355
9918
|
hint.style.cssText = "color:#9ca3af;";
|
|
9356
|
-
|
|
9919
|
+
const inside = row.info.scopedLeafCount ?? -1;
|
|
9920
|
+
hint.textContent = row.selectable ? inside === 1 ? words.containsOne : inside < 0 ? words.countUnavailable : counted(words.containsMany, inside) : words.needsTestId;
|
|
9357
9921
|
text.appendChild(code);
|
|
9358
9922
|
text.appendChild(hint);
|
|
9359
9923
|
line.appendChild(box);
|
|
@@ -9391,6 +9955,7 @@ function showAnchorPicker(arg) {
|
|
|
9391
9955
|
outline.remove();
|
|
9392
9956
|
outlineLabel.remove();
|
|
9393
9957
|
pickedOutline.remove();
|
|
9958
|
+
if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
|
|
9394
9959
|
};
|
|
9395
9960
|
const done = (state) => {
|
|
9396
9961
|
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
@@ -9407,7 +9972,7 @@ function showAnchorPicker(arg) {
|
|
|
9407
9972
|
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
9408
9973
|
const useBtn = doc.createElement("button");
|
|
9409
9974
|
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
9410
|
-
useBtn.textContent =
|
|
9975
|
+
useBtn.textContent = words.use;
|
|
9411
9976
|
useBtn.addEventListener("click", (e) => {
|
|
9412
9977
|
e.preventDefault();
|
|
9413
9978
|
e.stopImmediatePropagation();
|
|
@@ -9415,7 +9980,7 @@ function showAnchorPicker(arg) {
|
|
|
9415
9980
|
});
|
|
9416
9981
|
const skipBtn = doc.createElement("button");
|
|
9417
9982
|
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
9418
|
-
skipBtn.textContent =
|
|
9983
|
+
skipBtn.textContent = words.skip;
|
|
9419
9984
|
skipBtn.addEventListener("click", (e) => {
|
|
9420
9985
|
e.preventDefault();
|
|
9421
9986
|
e.stopImmediatePropagation();
|
|
@@ -9427,10 +9992,32 @@ function showAnchorPicker(arg) {
|
|
|
9427
9992
|
refreshFooter();
|
|
9428
9993
|
g.__piwiAnchorCleanup = cleanup;
|
|
9429
9994
|
doc.addEventListener("keydown", onKey, true);
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9995
|
+
const mountParent = () => {
|
|
9996
|
+
let modal = null;
|
|
9997
|
+
try {
|
|
9998
|
+
modal = doc.querySelector("dialog:modal");
|
|
9999
|
+
} catch {
|
|
10000
|
+
modal = null;
|
|
10001
|
+
}
|
|
10002
|
+
if (!modal) return doc.body;
|
|
10003
|
+
const s = g.getComputedStyle(modal);
|
|
10004
|
+
const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
|
|
10005
|
+
return holdsFixed ? doc.body : modal;
|
|
10006
|
+
};
|
|
10007
|
+
const mount = (...nodes) => {
|
|
10008
|
+
const parent = mountParent();
|
|
10009
|
+
for (const node2 of nodes) parent.appendChild(node2);
|
|
10010
|
+
if (parent !== doc.body) {
|
|
10011
|
+
parent.addEventListener(
|
|
10012
|
+
"close",
|
|
10013
|
+
() => {
|
|
10014
|
+
for (const node2 of nodes) if (node2.parentNode === parent) doc.body.appendChild(node2);
|
|
10015
|
+
},
|
|
10016
|
+
{ once: true }
|
|
10017
|
+
);
|
|
10018
|
+
}
|
|
10019
|
+
};
|
|
10020
|
+
mount(pickedOutline, outline, outlineLabel, panel);
|
|
9434
10021
|
}
|
|
9435
10022
|
|
|
9436
10023
|
// ../picker-dom/src/overlay-confirm.ts
|
|
@@ -9646,7 +10233,7 @@ function isAutoGenerated(value) {
|
|
|
9646
10233
|
return false;
|
|
9647
10234
|
}
|
|
9648
10235
|
var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
|
|
9649
|
-
function generateAlternatives(attrs) {
|
|
10236
|
+
function generateAlternatives(attrs, options = {}) {
|
|
9650
10237
|
const alts = [];
|
|
9651
10238
|
const seen = /* @__PURE__ */ new Set();
|
|
9652
10239
|
const add = (loc) => {
|
|
@@ -9657,12 +10244,14 @@ function generateAlternatives(attrs) {
|
|
|
9657
10244
|
};
|
|
9658
10245
|
const { accessibleName } = attrs;
|
|
9659
10246
|
const tag = attrs.tagName;
|
|
9660
|
-
const role = resolveAriaRole(attrs);
|
|
10247
|
+
const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
|
|
9661
10248
|
const counts = attrs.selectorCounts;
|
|
9662
10249
|
const isUnique = (n) => n == null || n <= 1;
|
|
9663
10250
|
const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
|
|
9664
10251
|
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
9665
|
-
const
|
|
10252
|
+
const testIdAttribute = options.testIdAttribute || "data-testid";
|
|
10253
|
+
const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
|
|
10254
|
+
const testId = attr(attrs, testIdAttribute);
|
|
9666
10255
|
if (testId && isUnique(counts?.testId)) {
|
|
9667
10256
|
add({
|
|
9668
10257
|
locator: `getByTestId('${esc(testId)}')`,
|
|
@@ -9721,7 +10310,7 @@ function generateAlternatives(attrs) {
|
|
|
9721
10310
|
});
|
|
9722
10311
|
}
|
|
9723
10312
|
const id = attr(attrs, "id");
|
|
9724
|
-
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
10313
|
+
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
9725
10314
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
9726
10315
|
add({
|
|
9727
10316
|
locator: `locator('${esc(selector)}')`,
|
|
@@ -9731,7 +10320,7 @@ function generateAlternatives(attrs) {
|
|
|
9731
10320
|
});
|
|
9732
10321
|
}
|
|
9733
10322
|
const name = attr(attrs, "name");
|
|
9734
|
-
if (name && isUnique(counts?.name)) {
|
|
10323
|
+
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
9735
10324
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
9736
10325
|
add({
|
|
9737
10326
|
locator: `locator('${esc(selector)}')`,
|
|
@@ -9776,7 +10365,7 @@ function generateAlternatives(attrs) {
|
|
|
9776
10365
|
score: scores.testId
|
|
9777
10366
|
});
|
|
9778
10367
|
}
|
|
9779
|
-
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
10368
|
+
if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
9780
10369
|
idAnchorDone = true;
|
|
9781
10370
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
9782
10371
|
add({
|
|
@@ -9786,7 +10375,7 @@ function generateAlternatives(attrs) {
|
|
|
9786
10375
|
score: scores.id
|
|
9787
10376
|
});
|
|
9788
10377
|
}
|
|
9789
|
-
if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
|
|
10378
|
+
if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
|
|
9790
10379
|
dataAnchorDone = true;
|
|
9791
10380
|
const { name: name2, value } = anc.dataAttr;
|
|
9792
10381
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|