@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/cli/index.js
CHANGED
|
@@ -932,6 +932,7 @@ var PIWI_ENV_KEYS = {
|
|
|
932
932
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
933
933
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
934
934
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
935
|
+
coverageCss: "PIWI_COVERAGE_CSS",
|
|
935
936
|
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
936
937
|
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
937
938
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
@@ -2320,6 +2321,8 @@ var STEPS_LIMITS = {
|
|
|
2320
2321
|
/** Titles, names, texts, locators. */
|
|
2321
2322
|
textLength: 500,
|
|
2322
2323
|
viewports: 50,
|
|
2324
|
+
/** The options a manual step's instruction names. */
|
|
2325
|
+
options: 50,
|
|
2323
2326
|
/** The widest and tallest viewport, in CSS pixels. */
|
|
2324
2327
|
viewportSize: 1e4,
|
|
2325
2328
|
/** The browser's zoom factors a viewport may name, as browsers allow them. */
|
|
@@ -2491,12 +2494,71 @@ function checkManual(c, v, path25) {
|
|
|
2491
2494
|
}
|
|
2492
2495
|
const description = c.text(v.description, `${path25}.description`, STEPS_LIMITS.valueLength, false);
|
|
2493
2496
|
if (description != null && !description.trim()) c.fail(`${path25}.description`, "must not be empty");
|
|
2497
|
+
const instruction = readInstruction(v.instruction);
|
|
2494
2498
|
return {
|
|
2495
2499
|
description: description ?? "",
|
|
2496
2500
|
location: c.text(v.location, `${path25}.location`, STEPS_LIMITS.textLength, true),
|
|
2497
|
-
call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true)
|
|
2501
|
+
call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true),
|
|
2502
|
+
...instruction ? { instruction } : {}
|
|
2498
2503
|
};
|
|
2499
2504
|
}
|
|
2505
|
+
var isText = (v) => typeof v === "string" && v.length <= STEPS_LIMITS.valueLength;
|
|
2506
|
+
var isTextOrNull = (v) => v == null || isText(v);
|
|
2507
|
+
var isCount = (v) => Number.isInteger(v) && v >= 0;
|
|
2508
|
+
var INSTRUCTION_FIELDS = {
|
|
2509
|
+
locator: isText,
|
|
2510
|
+
call: isText,
|
|
2511
|
+
matcher: isText,
|
|
2512
|
+
what: isText,
|
|
2513
|
+
expected: isTextOrNull,
|
|
2514
|
+
received: isTextOrNull,
|
|
2515
|
+
x: (v) => typeof v === "number" && Number.isFinite(v),
|
|
2516
|
+
y: (v) => typeof v === "number" && Number.isFinite(v),
|
|
2517
|
+
count: isCount,
|
|
2518
|
+
limit: isCount,
|
|
2519
|
+
options: (v) => Array.isArray(v) && v.length > 0 && v.length <= STEPS_LIMITS.options && v.every((o) => isText(o) || isCount(o))
|
|
2520
|
+
};
|
|
2521
|
+
var CHECK_VALUES = ["expected", "received"];
|
|
2522
|
+
var INSTRUCTION_KINDS = {
|
|
2523
|
+
goBack: [],
|
|
2524
|
+
goForward: [],
|
|
2525
|
+
reload: [],
|
|
2526
|
+
gotoUnknown: [],
|
|
2527
|
+
setContent: [],
|
|
2528
|
+
keyboardType: [],
|
|
2529
|
+
mouseAt: ["x", "y", "call"],
|
|
2530
|
+
mouseOrKeyboard: ["call"],
|
|
2531
|
+
failedCall: ["call"],
|
|
2532
|
+
locatorCall: ["locator", "call"],
|
|
2533
|
+
fillUnknown: ["locator"],
|
|
2534
|
+
fillPartial: ["locator"],
|
|
2535
|
+
pressUnknown: ["locator"],
|
|
2536
|
+
selectOptionUnknown: ["locator"],
|
|
2537
|
+
setInputFiles: ["locator"],
|
|
2538
|
+
dragTo: ["locator"],
|
|
2539
|
+
drop: ["locator"],
|
|
2540
|
+
focus: ["locator"],
|
|
2541
|
+
blur: ["locator"],
|
|
2542
|
+
dispatchEvent: ["locator"],
|
|
2543
|
+
scrollIntoViewIfNeeded: ["locator"],
|
|
2544
|
+
selectOptions: ["locator", "options"],
|
|
2545
|
+
expectPoll: ["matcher", ...CHECK_VALUES],
|
|
2546
|
+
expectValue: ["what", ...CHECK_VALUES],
|
|
2547
|
+
expectLocator: ["locator", "matcher", ...CHECK_VALUES],
|
|
2548
|
+
expectURL: CHECK_VALUES,
|
|
2549
|
+
leadingSteps: ["count", "limit"]
|
|
2550
|
+
};
|
|
2551
|
+
function readInstruction(v) {
|
|
2552
|
+
if (!isObject(v) || typeof v.kind !== "string" || !Object.prototype.hasOwnProperty.call(INSTRUCTION_KINDS, v.kind))
|
|
2553
|
+
return null;
|
|
2554
|
+
const kind = v.kind;
|
|
2555
|
+
const out = { kind };
|
|
2556
|
+
for (const field of INSTRUCTION_KINDS[kind]) {
|
|
2557
|
+
if (!INSTRUCTION_FIELDS[field](v[field])) return null;
|
|
2558
|
+
out[field] = v[field] ?? null;
|
|
2559
|
+
}
|
|
2560
|
+
return out;
|
|
2561
|
+
}
|
|
2500
2562
|
function checkViewports(c, v, stepCount) {
|
|
2501
2563
|
if (v == null) return [];
|
|
2502
2564
|
if (!Array.isArray(v)) {
|
|
@@ -6308,7 +6370,7 @@ function attributeMatches(value, exact, actual) {
|
|
|
6308
6370
|
|
|
6309
6371
|
// ../core/src/locator-break.ts
|
|
6310
6372
|
var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "translation", "literal"]);
|
|
6311
|
-
function
|
|
6373
|
+
function isText2(anchor) {
|
|
6312
6374
|
return TEXT_KINDS.has(anchor.kind);
|
|
6313
6375
|
}
|
|
6314
6376
|
function hasAttribute(anchor, names) {
|
|
@@ -6407,7 +6469,7 @@ function directSlots(call, testIds) {
|
|
|
6407
6469
|
const hasTextSlot = (index) => {
|
|
6408
6470
|
const hasText = optionsOf(call.args[index]).get("hasText");
|
|
6409
6471
|
if (!hasText) return null;
|
|
6410
|
-
return stringSlot(hasText, false, "text",
|
|
6472
|
+
return stringSlot(hasText, false, "text", isText2, (next) => withOption(call, index, "hasText", next));
|
|
6411
6473
|
};
|
|
6412
6474
|
const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
|
|
6413
6475
|
switch (call.method) {
|
|
@@ -6420,16 +6482,16 @@ function directSlots(call, testIds) {
|
|
|
6420
6482
|
opts.get("name"),
|
|
6421
6483
|
exact,
|
|
6422
6484
|
"name",
|
|
6423
|
-
(a) =>
|
|
6485
|
+
(a) => isText2(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
|
|
6424
6486
|
(next) => withOption(call, 1, "name", next)
|
|
6425
6487
|
)
|
|
6426
6488
|
);
|
|
6427
6489
|
break;
|
|
6428
6490
|
case "getByText":
|
|
6429
|
-
slots.push(argSlot("text",
|
|
6491
|
+
slots.push(argSlot("text", isText2));
|
|
6430
6492
|
break;
|
|
6431
6493
|
case "getByLabel":
|
|
6432
|
-
slots.push(argSlot("text", (a) =>
|
|
6494
|
+
slots.push(argSlot("text", (a) => isText2(a) || hasAttribute(a, ["aria-label"])));
|
|
6433
6495
|
break;
|
|
6434
6496
|
case "getByPlaceholder":
|
|
6435
6497
|
slots.push(argSlot("text", textOr("placeholder")));
|
|
@@ -202,6 +202,7 @@ var PIWI_ENV_KEYS = {
|
|
|
202
202
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
203
203
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
204
204
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
205
|
+
coverageCss: "PIWI_COVERAGE_CSS",
|
|
205
206
|
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
206
207
|
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
207
208
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
@@ -321,10 +322,14 @@ function resolveOptions(raw, configDir) {
|
|
|
321
322
|
const coverage = { ...mergedRaw.coverage };
|
|
322
323
|
const coverageBrowser = env[PIWI_ENV_KEYS.coverageBrowser];
|
|
323
324
|
const coverageServer = env[PIWI_ENV_KEYS.coverageServer];
|
|
325
|
+
const coverageCss = env[PIWI_ENV_KEYS.coverageCss];
|
|
324
326
|
if (coverage.browser === void 0 && coverageBrowser)
|
|
325
327
|
coverage.browser = coverageBrowser;
|
|
326
328
|
if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
|
|
327
|
-
if (coverage.
|
|
329
|
+
if (coverage.css === void 0 && coverageCss) coverage.css = coverageCss;
|
|
330
|
+
if (coverage.browser !== void 0 || coverage.server !== void 0 || coverage.css !== void 0) {
|
|
331
|
+
mergedRaw.coverage = coverage;
|
|
332
|
+
}
|
|
328
333
|
const externalId = { ...mergedRaw.externalId };
|
|
329
334
|
const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
|
|
330
335
|
const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
|
|
@@ -736,6 +741,7 @@ function isListMode(argv = process.argv) {
|
|
|
736
741
|
}
|
|
737
742
|
|
|
738
743
|
// src/internal/support/base-url-check.ts
|
|
744
|
+
var net = __toESM(require("net"));
|
|
739
745
|
var BASE_URL_CHECK_ATTEMPTS = 3;
|
|
740
746
|
var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
|
|
741
747
|
var BASE_URL_CHECK_DELAY_MS = 2e3;
|
|
@@ -757,9 +763,9 @@ function cliProjectNames(argv = process.argv) {
|
|
|
757
763
|
}
|
|
758
764
|
return names;
|
|
759
765
|
}
|
|
760
|
-
function wildcard(pattern) {
|
|
766
|
+
function wildcard(pattern, flags = "i") {
|
|
761
767
|
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
762
|
-
return new RegExp(`^${escaped.join(".*")}$`,
|
|
768
|
+
return new RegExp(`^${escaped.join(".*")}$`, flags);
|
|
763
769
|
}
|
|
764
770
|
function selectedProjects(projects, argv = process.argv, filtered) {
|
|
765
771
|
let chosen;
|
|
@@ -769,7 +775,7 @@ function selectedProjects(projects, argv = process.argv, filtered) {
|
|
|
769
775
|
} else {
|
|
770
776
|
const names = cliProjectNames(argv);
|
|
771
777
|
if (!names || names.length === 0) return projects;
|
|
772
|
-
const patterns = names.map(wildcard);
|
|
778
|
+
const patterns = names.map((name) => wildcard(name));
|
|
773
779
|
chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
774
780
|
}
|
|
775
781
|
if (!argv.includes("--no-deps")) {
|
|
@@ -791,23 +797,58 @@ function asProxy(value) {
|
|
|
791
797
|
const proxy = value;
|
|
792
798
|
return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
|
|
793
799
|
}
|
|
794
|
-
function
|
|
800
|
+
function exposeNetworkCovers(rule, url) {
|
|
801
|
+
if (!rule) return false;
|
|
802
|
+
let parsed;
|
|
803
|
+
try {
|
|
804
|
+
parsed = new URL(url);
|
|
805
|
+
} catch {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
const host = parsed.hostname;
|
|
809
|
+
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
810
|
+
return rule.split(",").some((token) => {
|
|
811
|
+
const match = token.match(/^(.*?)(?::(\d+))?$/);
|
|
812
|
+
if (!match) return false;
|
|
813
|
+
if (match[2] !== void 0 && Number(match[2]) !== port) return false;
|
|
814
|
+
const tokenHost = match[1];
|
|
815
|
+
if (tokenHost === "<loopback>") {
|
|
816
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]";
|
|
817
|
+
}
|
|
818
|
+
if (tokenHost === "*") return true;
|
|
819
|
+
if (net.isIP(tokenHost)) return host === tokenHost;
|
|
820
|
+
if (net.isIP(host)) return false;
|
|
821
|
+
return wildcard(tokenHost.startsWith(".") ? `*${tokenHost}` : tokenHost, "").test(host);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function reachedRemotely(use, url, env) {
|
|
825
|
+
if (use?.connectOptions === void 0) {
|
|
826
|
+
return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
|
|
827
|
+
}
|
|
828
|
+
const connect = use.connectOptions;
|
|
829
|
+
if (!connect || typeof connect !== "object" || typeof connect.wsEndpoint !== "string") return false;
|
|
830
|
+
return !exposeNetworkCovers(typeof connect.exposeNetwork === "string" ? connect.exposeNetwork : void 0, url);
|
|
831
|
+
}
|
|
832
|
+
function baseUrlTargets(config, argv = process.argv, env = process.env) {
|
|
795
833
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
796
834
|
const filtered = config.projects?.length ? config.filteredProjects : void 0;
|
|
797
835
|
const targets = /* @__PURE__ */ new Map();
|
|
798
836
|
for (const project of selectedProjects(projects, argv, filtered)) {
|
|
799
837
|
const url = project.use?.baseURL;
|
|
800
838
|
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
839
|
+
const remoteBrowser = reachedRemotely(project.use, url, env);
|
|
801
840
|
const target = targets.get(url);
|
|
802
841
|
if (target) {
|
|
803
842
|
if (project.name) target.projects.push(project.name);
|
|
843
|
+
target.remoteBrowser ||= remoteBrowser;
|
|
804
844
|
continue;
|
|
805
845
|
}
|
|
806
846
|
targets.set(url, {
|
|
807
847
|
url,
|
|
808
848
|
projects: project.name ? [project.name] : [],
|
|
809
849
|
ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
|
|
810
|
-
proxy: asProxy(project.use?.proxy)
|
|
850
|
+
proxy: asProxy(project.use?.proxy),
|
|
851
|
+
remoteBrowser
|
|
811
852
|
});
|
|
812
853
|
}
|
|
813
854
|
return [...targets.values()];
|
|
@@ -848,14 +889,21 @@ async function checkBaseUrl(target, options = {}) {
|
|
|
848
889
|
}
|
|
849
890
|
return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
|
|
850
891
|
}
|
|
892
|
+
function remoteBrowserNote(target) {
|
|
893
|
+
if (!target.remoteBrowser) return null;
|
|
894
|
+
const { host } = new URL(target.url);
|
|
895
|
+
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.`;
|
|
896
|
+
}
|
|
851
897
|
async function checkBaseUrls(config, options = {}) {
|
|
852
|
-
const targets = baseUrlTargets(config, options.argv);
|
|
898
|
+
const targets = baseUrlTargets(config, options.argv, options.env);
|
|
853
899
|
const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
|
|
854
900
|
const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
|
|
855
901
|
if (down.length === 0) return targets;
|
|
856
902
|
const lines = down.map(({ target, reason }) => {
|
|
857
903
|
const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
|
|
858
|
-
|
|
904
|
+
const note = remoteBrowserNote(target);
|
|
905
|
+
return ` ${target.url}${used}: ${reason}${note ? `
|
|
906
|
+
${note}` : ""}`;
|
|
859
907
|
});
|
|
860
908
|
const message = [
|
|
861
909
|
`[Piwi Dashboard] The app under test did not answer, so no test ran:`,
|
|
@@ -1008,7 +1056,16 @@ function createGlobalSetup(options, userSetup) {
|
|
|
1008
1056
|
}
|
|
1009
1057
|
if (opts.checkBaseUrl === true) {
|
|
1010
1058
|
const checked = await checkBaseUrls(config ?? {});
|
|
1011
|
-
for (const target of checked)
|
|
1059
|
+
for (const target of checked) {
|
|
1060
|
+
const note = remoteBrowserNote(target);
|
|
1061
|
+
if (note) {
|
|
1062
|
+
logger.info(
|
|
1063
|
+
`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.`
|
|
1064
|
+
);
|
|
1065
|
+
} else {
|
|
1066
|
+
logger.debug(`Base URL answered: ${target.url}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1012
1069
|
}
|
|
1013
1070
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
1014
1071
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
package/dist/index.d.ts
CHANGED
|
@@ -140,6 +140,19 @@ interface PiwiDashboardOptions {
|
|
|
140
140
|
* `PIWI_COVERAGE_SERVER`.
|
|
141
141
|
*/
|
|
142
142
|
server?: 'off' | 'files' | 'functions' | 'lines';
|
|
143
|
+
/**
|
|
144
|
+
* What the browser's style sheets record: `'rules'`, the rules that
|
|
145
|
+
* matched an element during each test, from Chromium's CSS rule usage, each
|
|
146
|
+
* as its first and last line in the application's style sheet (a `.css`
|
|
147
|
+
* file served as written, a Vite dev server's styles, or a sheet with a
|
|
148
|
+
* source map), sent where a script's functions are; `'off'` records
|
|
149
|
+
* nothing. A change to a rule then selects the tests that rendered it. It
|
|
150
|
+
* costs style recalculation time, a little on most pages and up to half
|
|
151
|
+
* the test's time on a page with large style sheets and frequent
|
|
152
|
+
* re-renders, so it is a choice of its own. Chromium only. Defaults to
|
|
153
|
+
* `'off'`. Can also be set with `PIWI_COVERAGE_CSS`.
|
|
154
|
+
*/
|
|
155
|
+
css?: 'off' | 'rules';
|
|
143
156
|
};
|
|
144
157
|
/**
|
|
145
158
|
* Match each test to its history on the dashboard by an external ID, the ID the test has in your own test
|
|
@@ -256,8 +269,11 @@ interface PiwiDashboardOptions {
|
|
|
256
269
|
* `globalSetup`, so leave it off when that setup is what starts the app. Only
|
|
257
270
|
* the projects the run selects are checked, with their dependencies: the ones
|
|
258
271
|
* `--project` names, or every project but those declared `default: false`
|
|
259
|
-
* (Playwright 1.64+).
|
|
260
|
-
*
|
|
272
|
+
* (Playwright 1.64+). The check runs from this machine's network: for a
|
|
273
|
+
* project whose browser runs on another machine (`connectOptions`) and whose
|
|
274
|
+
* `exposeNetwork` rule does not cover the base URL's host and port, its message says
|
|
275
|
+
* so, since that browser reaches the app from its own network. **Defaults to
|
|
276
|
+
* `false`**. Can also be set with `PIWI_CHECK_BASE_URL=true`.
|
|
261
277
|
*/
|
|
262
278
|
checkBaseUrl?: boolean;
|
|
263
279
|
/**
|
|
@@ -525,6 +541,8 @@ declare class PiwiDashboardReporter {
|
|
|
525
541
|
private resourcesFile;
|
|
526
542
|
/** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
|
|
527
543
|
private sampler;
|
|
544
|
+
/** Posts what the sampler and the censuses add while someone has the run open on the dashboard. */
|
|
545
|
+
private liveResources;
|
|
528
546
|
/** Bytes of the files the tests attached, by kind. */
|
|
529
547
|
private artifactBytes;
|
|
530
548
|
/** The run's resource report for the dashboard, built in `onEnd`. */
|