@piwitests/reporter 0.36.0 → 0.38.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 +281 -8
- package/dist/global-setup-module.js +168 -15
- package/dist/index.d.ts +33 -4
- package/dist/index.js +1085 -151
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +1 -0
- package/dist/internal/capture/capture-fixtures.js +911 -85
- package/dist/internal/capture/pick-on-failure.d.ts +5 -1
- package/dist/internal/capture/pick-on-failure.js +284 -79
- package/package.json +2 -2
- package/templates/skills/write-the-missing-test/SKILL.md +69 -0
package/dist/cli/index.js
CHANGED
|
@@ -422,6 +422,7 @@ function formatGateResult(result) {
|
|
|
422
422
|
);
|
|
423
423
|
}
|
|
424
424
|
for (const violation of result.violations) lines.push(` \u2716 ${violation.message}`);
|
|
425
|
+
for (const warning of result.warnings ?? []) lines.push(` \u26A0 ${warning}`);
|
|
425
426
|
lines.push(` ${facts.runUrl}`);
|
|
426
427
|
return lines.join("\n");
|
|
427
428
|
}
|
|
@@ -456,6 +457,9 @@ Policy (at least one is required):
|
|
|
456
457
|
--fail-on-flaky Fail when this run contains any flaky test
|
|
457
458
|
--require-selection <key> Fail when a test the named selection matches did not run or failed
|
|
458
459
|
|
|
460
|
+
Warn-only (reported, never fails the build):
|
|
461
|
+
--max-uncovered-changes <n> Warn when more than n changed files have no observed test reach
|
|
462
|
+
|
|
459
463
|
Other:
|
|
460
464
|
--json Print the raw result as JSON instead of a summary
|
|
461
465
|
-h, --help Show this help
|
|
@@ -477,9 +481,9 @@ function readCount(argv, name) {
|
|
|
477
481
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
478
482
|
return Math.floor(n);
|
|
479
483
|
}
|
|
480
|
-
function readRunIdFromFile(
|
|
484
|
+
function readRunIdFromFile(path13) {
|
|
481
485
|
try {
|
|
482
|
-
const parsed = JSON.parse(fs3.readFileSync(
|
|
486
|
+
const parsed = JSON.parse(fs3.readFileSync(path13, "utf-8"));
|
|
483
487
|
const runId = Number(parsed.runId);
|
|
484
488
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
485
489
|
} catch {
|
|
@@ -510,7 +514,13 @@ function parseGateArgs(argv, env) {
|
|
|
510
514
|
failOnFlaky: argv.includes("--fail-on-flaky"),
|
|
511
515
|
requireSelection: readOption2(argv, "--require-selection")
|
|
512
516
|
};
|
|
513
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
serverUrl,
|
|
519
|
+
apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
520
|
+
runId,
|
|
521
|
+
policy,
|
|
522
|
+
maxUncoveredChanges: readCount(argv, "--max-uncovered-changes")
|
|
523
|
+
};
|
|
514
524
|
}
|
|
515
525
|
async function requestGate(args) {
|
|
516
526
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -518,7 +528,7 @@ async function requestGate(args) {
|
|
|
518
528
|
const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
|
|
519
529
|
method: "POST",
|
|
520
530
|
headers,
|
|
521
|
-
body: JSON.stringify(args.policy)
|
|
531
|
+
body: JSON.stringify({ ...args.policy, maxUncoveredChanges: args.maxUncoveredChanges })
|
|
522
532
|
});
|
|
523
533
|
if (!res.ok) {
|
|
524
534
|
const body = await res.json().catch(() => ({}));
|
|
@@ -778,7 +788,8 @@ var WORKFLOW_SKILLS = [
|
|
|
778
788
|
"investigate-failure",
|
|
779
789
|
"apply-locator-healing",
|
|
780
790
|
"stabilize-flaky-tests",
|
|
781
|
-
"run-the-right-tests"
|
|
791
|
+
"run-the-right-tests",
|
|
792
|
+
"write-the-missing-test"
|
|
782
793
|
];
|
|
783
794
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
784
795
|
var DEFAULT_SKILLS_DIR = path7.join(".claude", "skills");
|
|
@@ -1675,8 +1686,267 @@ async function runRun(argv, env = process.env) {
|
|
|
1675
1686
|
return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
|
|
1676
1687
|
}
|
|
1677
1688
|
|
|
1678
|
-
// src/cli/
|
|
1689
|
+
// src/cli/probe.ts
|
|
1690
|
+
var import_node_child_process4 = require("child_process");
|
|
1691
|
+
var import_node_module3 = require("module");
|
|
1692
|
+
var fs10 = __toESM(require("fs"));
|
|
1693
|
+
var os2 = __toESM(require("os"));
|
|
1694
|
+
var path12 = __toESM(require("path"));
|
|
1695
|
+
|
|
1696
|
+
// src/internal/config/desktop.ts
|
|
1697
|
+
var fs9 = __toESM(require("fs"));
|
|
1698
|
+
var os = __toESM(require("os"));
|
|
1699
|
+
var path11 = __toESM(require("path"));
|
|
1700
|
+
|
|
1701
|
+
// src/internal/config/env.ts
|
|
1702
|
+
var DEFAULTS = {
|
|
1703
|
+
projectName: "default-project",
|
|
1704
|
+
uploadTraces: true,
|
|
1705
|
+
uploadReport: true,
|
|
1706
|
+
liveFileUploads: true,
|
|
1707
|
+
collectScmInfo: true,
|
|
1708
|
+
collectCiInfo: true,
|
|
1709
|
+
collectPerformanceMetrics: true,
|
|
1710
|
+
captureLocators: true,
|
|
1711
|
+
capturePageState: true,
|
|
1712
|
+
// Off by default: the page inventory reads control and link names from every
|
|
1713
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
1714
|
+
capturePageInventory: false,
|
|
1715
|
+
captureServerTraces: true,
|
|
1716
|
+
sampleAriaOnPass: true,
|
|
1717
|
+
uploadManifest: true,
|
|
1718
|
+
defaultCapture: true,
|
|
1719
|
+
streaming: true,
|
|
1720
|
+
streamingBatchSize: 5,
|
|
1721
|
+
streamingBatchDelay: 2e3,
|
|
1722
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
1723
|
+
failOnFlakyTests: false,
|
|
1724
|
+
username: null,
|
|
1725
|
+
password: null,
|
|
1726
|
+
apiKey: null,
|
|
1727
|
+
verbose: false
|
|
1728
|
+
};
|
|
1729
|
+
var PIWI_ENV_KEYS = {
|
|
1730
|
+
serverUrl: "PIWI_DASHBOARD_URL",
|
|
1731
|
+
projectName: "PIWI_PROJECT_NAME",
|
|
1732
|
+
verbose: "PIWI_VERBOSE",
|
|
1733
|
+
apiKey: "PIWI_API_KEY",
|
|
1734
|
+
username: "PIWI_USERNAME",
|
|
1735
|
+
password: "PIWI_PASSWORD",
|
|
1736
|
+
environment: "PIWI_ENVIRONMENT",
|
|
1737
|
+
label: "PIWI_LABEL",
|
|
1738
|
+
runLabel: "PIWI_RUN_LABEL",
|
|
1739
|
+
keep: "PIWI_KEEP",
|
|
1740
|
+
streaming: "PIWI_STREAMING",
|
|
1741
|
+
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
1742
|
+
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
1743
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
1744
|
+
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
1745
|
+
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
1746
|
+
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
1747
|
+
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
1748
|
+
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
1749
|
+
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
1750
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
1751
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
1752
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
1753
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
1754
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
1755
|
+
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
1756
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
1757
|
+
outputFile: "PIWI_OUTPUT_FILE",
|
|
1758
|
+
aiMode: "PIWI_AI",
|
|
1759
|
+
aiDir: "PIWI_AI_DIR",
|
|
1760
|
+
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
1761
|
+
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
1762
|
+
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
1763
|
+
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
1764
|
+
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
1765
|
+
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
1766
|
+
};
|
|
1767
|
+
var PIWI_PROBE_ENV = {
|
|
1768
|
+
flag: "PIWI_PROBE",
|
|
1769
|
+
plan: "PIWI_PROBE_PLAN",
|
|
1770
|
+
results: "PIWI_PROBE_RESULTS",
|
|
1771
|
+
secret: "PIWI_PROBE_SECRET"
|
|
1772
|
+
};
|
|
1773
|
+
var ENV_FALLBACK_SPECS = [
|
|
1774
|
+
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
1775
|
+
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
1776
|
+
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
1777
|
+
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
1778
|
+
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
1779
|
+
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
1780
|
+
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
1781
|
+
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
1782
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
1783
|
+
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
1784
|
+
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
1785
|
+
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
1786
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
1787
|
+
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
1788
|
+
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
1789
|
+
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
1790
|
+
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
1791
|
+
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
1792
|
+
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
1793
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
1794
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
1795
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
1796
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
1797
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
1798
|
+
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
1799
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
1800
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
1801
|
+
];
|
|
1802
|
+
|
|
1803
|
+
// src/cli/probe.ts
|
|
1804
|
+
var EXIT_OK4 = 0;
|
|
1805
|
+
var EXIT_ERROR4 = 2;
|
|
1679
1806
|
var USAGE6 = `
|
|
1807
|
+
piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
|
|
1808
|
+
|
|
1809
|
+
Usage:
|
|
1810
|
+
npx @piwitests/reporter probe [options] [-- <playwright args>]
|
|
1811
|
+
|
|
1812
|
+
Options:
|
|
1813
|
+
--server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL)
|
|
1814
|
+
--api-key <key> Reporter API key (or PIWI_API_KEY)
|
|
1815
|
+
--project <name|id> Project name or id (or PIWI_PROJECT_NAME)
|
|
1816
|
+
--budget <n> Max pairs to probe this run (default the server's)
|
|
1817
|
+
|
|
1818
|
+
Everything after \`--\` is passed to \`playwright test\`.
|
|
1819
|
+
`.trim();
|
|
1820
|
+
function readOption6(argv, name) {
|
|
1821
|
+
const i = argv.indexOf(name);
|
|
1822
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : void 0;
|
|
1823
|
+
}
|
|
1824
|
+
function escapeRegex(s) {
|
|
1825
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1826
|
+
}
|
|
1827
|
+
function resolvePlaywrightCli2() {
|
|
1828
|
+
const require2 = (0, import_node_module3.createRequire)(path12.join(process.cwd(), "noop.js"));
|
|
1829
|
+
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
1830
|
+
try {
|
|
1831
|
+
return require2.resolve(id);
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
function spawnPlaywright2(playwrightArgs, env) {
|
|
1838
|
+
const cli = resolvePlaywrightCli2();
|
|
1839
|
+
const child = cli ? (0, import_node_child_process4.spawn)(process.execPath, [cli, "test", ...playwrightArgs], { stdio: "inherit", env }) : (0, import_node_child_process4.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs], {
|
|
1840
|
+
stdio: "inherit",
|
|
1841
|
+
env
|
|
1842
|
+
});
|
|
1843
|
+
return new Promise((resolve6) => {
|
|
1844
|
+
child.on("error", (err) => {
|
|
1845
|
+
console.error(`piwi probe: could not start Playwright \u2014 ${err.message}`);
|
|
1846
|
+
resolve6(EXIT_ERROR4);
|
|
1847
|
+
});
|
|
1848
|
+
child.on("exit", (code) => resolve6(code ?? EXIT_ERROR4));
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
async function runProbe(argv) {
|
|
1852
|
+
const sep3 = argv.indexOf("--");
|
|
1853
|
+
const own = sep3 < 0 ? argv : argv.slice(0, sep3);
|
|
1854
|
+
const playwrightArgs = sep3 < 0 ? [] : argv.slice(sep3 + 1);
|
|
1855
|
+
if (own.includes("-h") || own.includes("--help")) {
|
|
1856
|
+
console.log(USAGE6);
|
|
1857
|
+
return EXIT_OK4;
|
|
1858
|
+
}
|
|
1859
|
+
const env = process.env;
|
|
1860
|
+
const serverUrl = (readOption6(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
|
|
1861
|
+
if (!serverUrl) {
|
|
1862
|
+
console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
1863
|
+
return EXIT_ERROR4;
|
|
1864
|
+
}
|
|
1865
|
+
const apiKey = readOption6(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
|
|
1866
|
+
const project = readOption6(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
|
|
1867
|
+
const budget = readOption6(own, "--budget");
|
|
1868
|
+
let projectId;
|
|
1869
|
+
try {
|
|
1870
|
+
projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
console.error(`piwi probe: ${e.message}`);
|
|
1873
|
+
return EXIT_ERROR4;
|
|
1874
|
+
}
|
|
1875
|
+
const authHeaders2 = apiKey ? { "X-API-Key": apiKey } : {};
|
|
1876
|
+
const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
|
|
1877
|
+
let plan;
|
|
1878
|
+
try {
|
|
1879
|
+
const res = await fetch(planUrl, { headers: authHeaders2 });
|
|
1880
|
+
if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
|
|
1881
|
+
plan = await res.json();
|
|
1882
|
+
} catch (e) {
|
|
1883
|
+
console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
|
|
1884
|
+
return EXIT_ERROR4;
|
|
1885
|
+
}
|
|
1886
|
+
if (!plan.items || plan.items.length === 0) {
|
|
1887
|
+
console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
|
|
1888
|
+
return EXIT_OK4;
|
|
1889
|
+
}
|
|
1890
|
+
console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
|
|
1891
|
+
const dir = fs10.mkdtempSync(path12.join(os2.tmpdir(), "piwi-probe-"));
|
|
1892
|
+
const planFile = path12.join(dir, "plan.json");
|
|
1893
|
+
const resultsFile = path12.join(dir, "results.jsonl");
|
|
1894
|
+
const runOutputFile = path12.join(dir, "run.json");
|
|
1895
|
+
fs10.writeFileSync(planFile, JSON.stringify(plan));
|
|
1896
|
+
fs10.writeFileSync(resultsFile, "");
|
|
1897
|
+
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex).join("|");
|
|
1898
|
+
const pwArgs = [...playwrightArgs];
|
|
1899
|
+
if (grep && !playwrightArgs.includes("--grep") && !playwrightArgs.includes("-g")) pwArgs.push("--grep", grep);
|
|
1900
|
+
const childEnv = {
|
|
1901
|
+
...env,
|
|
1902
|
+
[PIWI_PROBE_ENV.flag]: "1",
|
|
1903
|
+
[PIWI_PROBE_ENV.plan]: planFile,
|
|
1904
|
+
[PIWI_PROBE_ENV.results]: resultsFile,
|
|
1905
|
+
// The reporter creates the probe run and knows its server-assigned id; have
|
|
1906
|
+
// it write the run output here so the results POST can name the run.
|
|
1907
|
+
[PIWI_ENV_KEYS.outputFile]: runOutputFile
|
|
1908
|
+
};
|
|
1909
|
+
await spawnPlaywright2(pwArgs, childEnv);
|
|
1910
|
+
const lines = fs10.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
|
|
1911
|
+
const results = [];
|
|
1912
|
+
for (const line of lines) {
|
|
1913
|
+
try {
|
|
1914
|
+
results.push(JSON.parse(line));
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (results.length === 0) {
|
|
1919
|
+
console.error("piwi probe: no probe outcomes were recorded (are the Piwi capture fixtures active?).");
|
|
1920
|
+
return EXIT_OK4;
|
|
1921
|
+
}
|
|
1922
|
+
let runId = null;
|
|
1923
|
+
try {
|
|
1924
|
+
const out = JSON.parse(fs10.readFileSync(runOutputFile, "utf8"));
|
|
1925
|
+
if (typeof out.runId === "number") runId = out.runId;
|
|
1926
|
+
} catch {
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
const res = await fetch(`${serverUrl}/api/projects/${projectId}/probes/results`, {
|
|
1930
|
+
method: "POST",
|
|
1931
|
+
headers: { "Content-Type": "application/json", ...authHeaders2 },
|
|
1932
|
+
body: JSON.stringify({ runId, results })
|
|
1933
|
+
});
|
|
1934
|
+
if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
|
|
1935
|
+
const body = await res.json();
|
|
1936
|
+
const noticed = results.filter((r) => r.outcome === "noticed").length;
|
|
1937
|
+
const notNoticed = results.filter((r) => r.outcome === "not-noticed").length;
|
|
1938
|
+
console.error(
|
|
1939
|
+
`piwi probe: recorded ${body.recorded ?? results.length} outcome(s) \u2014 ${noticed} noticed, ${notNoticed} not noticed.`
|
|
1940
|
+
);
|
|
1941
|
+
} catch (e) {
|
|
1942
|
+
console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
|
|
1943
|
+
return EXIT_ERROR4;
|
|
1944
|
+
}
|
|
1945
|
+
return EXIT_OK4;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/cli/index.ts
|
|
1949
|
+
var USAGE7 = `
|
|
1680
1950
|
piwi \u2014 companion commands for the Piwi Dashboard reporter
|
|
1681
1951
|
|
|
1682
1952
|
Usage:
|
|
@@ -1688,6 +1958,7 @@ Commands:
|
|
|
1688
1958
|
gate Fail a CI job on the dashboard's analysis of a run
|
|
1689
1959
|
select Print the Playwright args for a saved test selection
|
|
1690
1960
|
run Run a saved test selection with playwright test
|
|
1961
|
+
probe Run the dashboard's probe plan and record what the suite noticed
|
|
1691
1962
|
ai Manage committed natural-language AI-step artifacts
|
|
1692
1963
|
|
|
1693
1964
|
Run \`npx @piwitests/reporter <command> --help\` for a command's options.
|
|
@@ -1707,17 +1978,19 @@ async function main() {
|
|
|
1707
1978
|
return runSelect(rest);
|
|
1708
1979
|
case "run":
|
|
1709
1980
|
return runRun(rest);
|
|
1981
|
+
case "probe":
|
|
1982
|
+
return runProbe(rest);
|
|
1710
1983
|
case "ai":
|
|
1711
1984
|
return runAi(rest);
|
|
1712
1985
|
case void 0:
|
|
1713
1986
|
case "-h":
|
|
1714
1987
|
case "--help":
|
|
1715
|
-
console.log(
|
|
1988
|
+
console.log(USAGE7);
|
|
1716
1989
|
return 0;
|
|
1717
1990
|
default:
|
|
1718
1991
|
console.error(`piwi: unknown command "${command}"
|
|
1719
1992
|
`);
|
|
1720
|
-
console.error(
|
|
1993
|
+
console.error(USAGE7);
|
|
1721
1994
|
return 2;
|
|
1722
1995
|
}
|
|
1723
1996
|
}
|
|
@@ -35,7 +35,7 @@ __export(global_setup_module_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(global_setup_module_exports);
|
|
36
36
|
|
|
37
37
|
// src/public/global-setup.ts
|
|
38
|
-
var
|
|
38
|
+
var path5 = __toESM(require("path"));
|
|
39
39
|
|
|
40
40
|
// src/internal/support/errors.ts
|
|
41
41
|
function errorMessage(error) {
|
|
@@ -44,7 +44,7 @@ function errorMessage(error) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// src/public/global-setup.ts
|
|
47
|
-
var
|
|
47
|
+
var fs5 = __toESM(require("fs"));
|
|
48
48
|
|
|
49
49
|
// src/internal/config/desktop.ts
|
|
50
50
|
var fs = __toESM(require("fs"));
|
|
@@ -77,8 +77,12 @@ var DEFAULTS = {
|
|
|
77
77
|
collectPerformanceMetrics: true,
|
|
78
78
|
captureLocators: true,
|
|
79
79
|
capturePageState: true,
|
|
80
|
+
// Off by default: the page inventory reads control and link names from every
|
|
81
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
82
|
+
capturePageInventory: false,
|
|
80
83
|
captureServerTraces: true,
|
|
81
84
|
sampleAriaOnPass: true,
|
|
85
|
+
uploadManifest: true,
|
|
82
86
|
defaultCapture: true,
|
|
83
87
|
streaming: true,
|
|
84
88
|
streamingBatchSize: 5,
|
|
@@ -100,6 +104,7 @@ var PIWI_ENV_KEYS = {
|
|
|
100
104
|
environment: "PIWI_ENVIRONMENT",
|
|
101
105
|
label: "PIWI_LABEL",
|
|
102
106
|
runLabel: "PIWI_RUN_LABEL",
|
|
107
|
+
keep: "PIWI_KEEP",
|
|
103
108
|
streaming: "PIWI_STREAMING",
|
|
104
109
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
105
110
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
@@ -110,8 +115,10 @@ var PIWI_ENV_KEYS = {
|
|
|
110
115
|
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
111
116
|
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
112
117
|
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
118
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
113
119
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
114
120
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
121
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
115
122
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
116
123
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
117
124
|
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
@@ -139,6 +146,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
139
146
|
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
140
147
|
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
141
148
|
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
149
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
142
150
|
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
143
151
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
144
152
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
@@ -149,8 +157,10 @@ var ENV_FALLBACK_SPECS = [
|
|
|
149
157
|
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
150
158
|
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
151
159
|
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
160
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
152
161
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
153
162
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
163
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
154
164
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
155
165
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
156
166
|
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
@@ -351,6 +361,27 @@ var HttpClient = class {
|
|
|
351
361
|
return {};
|
|
352
362
|
}
|
|
353
363
|
}
|
|
364
|
+
/** Send a JSON PUT request. Used for the declared-surface manifest upload. */
|
|
365
|
+
async putJSON(pathname, payload, auth) {
|
|
366
|
+
const body = JSON.stringify(payload);
|
|
367
|
+
const res = await this.request("PUT", pathname, {
|
|
368
|
+
headers: {
|
|
369
|
+
"Content-Type": "application/json",
|
|
370
|
+
"Content-Length": Buffer.byteLength(body)
|
|
371
|
+
},
|
|
372
|
+
body,
|
|
373
|
+
auth
|
|
374
|
+
});
|
|
375
|
+
if (res.status < 200 || res.status >= 300) {
|
|
376
|
+
this.logger.debugError(`Response: ${res.text}`);
|
|
377
|
+
throw new HttpError(res.status);
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
return JSON.parse(res.text);
|
|
381
|
+
} catch {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
354
385
|
/** Send a multipart form-data POST request. Used for report and trace uploads. */
|
|
355
386
|
async postFormData(pathname, form, auth) {
|
|
356
387
|
const headers = form.getHeaders();
|
|
@@ -453,6 +484,36 @@ function detectCiRunLabel() {
|
|
|
453
484
|
return null;
|
|
454
485
|
}
|
|
455
486
|
|
|
487
|
+
// src/internal/collect/metadata-collector.ts
|
|
488
|
+
var import_node_child_process = require("child_process");
|
|
489
|
+
var DETACHED_HEAD = "HEAD";
|
|
490
|
+
function resolveScmBranch(env, gitBranch) {
|
|
491
|
+
const pick = (...names) => {
|
|
492
|
+
for (const name of names) {
|
|
493
|
+
const value = env[name]?.trim();
|
|
494
|
+
if (value) return value;
|
|
495
|
+
}
|
|
496
|
+
return void 0;
|
|
497
|
+
};
|
|
498
|
+
const override = env.PIWI_BRANCH?.trim();
|
|
499
|
+
if (override) return override;
|
|
500
|
+
let provider;
|
|
501
|
+
if (env.JENKINS_URL) provider = pick("CHANGE_BRANCH", "BRANCH_NAME");
|
|
502
|
+
else if (env.GITHUB_ACTIONS) provider = pick("GITHUB_HEAD_REF", "GITHUB_REF_NAME");
|
|
503
|
+
else if (env.GITLAB_CI) provider = pick("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", "CI_COMMIT_REF_NAME");
|
|
504
|
+
else if (env.CIRCLECI) provider = pick("CIRCLE_BRANCH");
|
|
505
|
+
else if (env.TRAVIS) provider = pick("TRAVIS_PULL_REQUEST_BRANCH", "TRAVIS_BRANCH");
|
|
506
|
+
else if (env.TF_BUILD) provider = pick("SYSTEM_PULLREQUEST_SOURCEBRANCH", "BUILD_SOURCEBRANCHNAME");
|
|
507
|
+
else if (env.BITBUCKET_BUILD_NUMBER) provider = pick("BITBUCKET_BRANCH");
|
|
508
|
+
if (provider) return normalizeRef(provider);
|
|
509
|
+
const trimmed = gitBranch?.trim();
|
|
510
|
+
if (trimmed && trimmed !== DETACHED_HEAD) return trimmed;
|
|
511
|
+
return void 0;
|
|
512
|
+
}
|
|
513
|
+
function normalizeRef(ref) {
|
|
514
|
+
return ref.replace(/^refs\/heads\//, "");
|
|
515
|
+
}
|
|
516
|
+
|
|
456
517
|
// src/internal/support/setup-file.ts
|
|
457
518
|
var path2 = __toESM(require("path"));
|
|
458
519
|
var os3 = __toESM(require("os"));
|
|
@@ -500,17 +561,83 @@ function isListMode(argv = process.argv) {
|
|
|
500
561
|
return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
501
562
|
}
|
|
502
563
|
|
|
564
|
+
// src/internal/manifest/collect.ts
|
|
565
|
+
var fs4 = __toESM(require("fs"));
|
|
566
|
+
var path4 = __toESM(require("path"));
|
|
567
|
+
var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
|
|
568
|
+
var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
|
|
569
|
+
var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
|
|
570
|
+
function parseManifestJson(text) {
|
|
571
|
+
let parsed;
|
|
572
|
+
try {
|
|
573
|
+
parsed = JSON.parse(text);
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
578
|
+
const obj = parsed;
|
|
579
|
+
const routes = Array.isArray(obj.routes) ? obj.routes.filter((r) => !!r && typeof r === "object").filter((r) => typeof r.method === "string" && typeof r.pattern === "string").map((r) => ({
|
|
580
|
+
method: r.method,
|
|
581
|
+
pattern: r.pattern,
|
|
582
|
+
handler: typeof r.handler === "string" ? r.handler : void 0,
|
|
583
|
+
responses: Array.isArray(r.responses) ? r.responses.filter((c) => Number.isInteger(c)) : void 0
|
|
584
|
+
})) : void 0;
|
|
585
|
+
const pages = Array.isArray(obj.pages) ? obj.pages.filter((p) => !!p && typeof p === "object").filter((p) => typeof p.pattern === "string").map((p) => ({ pattern: p.pattern, name: typeof p.name === "string" ? p.name : void 0 })) : void 0;
|
|
586
|
+
if (!routes && !pages) return null;
|
|
587
|
+
return { routes, pages };
|
|
588
|
+
}
|
|
589
|
+
function hasInstrumentationHeader(headers) {
|
|
590
|
+
const get = (name) => typeof headers.get === "function" ? headers.get(name) : headers[name];
|
|
591
|
+
return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
|
|
592
|
+
}
|
|
593
|
+
function readCommittedManifest(configDir) {
|
|
594
|
+
const file = path4.join(configDir, COMMITTED_MANIFEST_FILE);
|
|
595
|
+
try {
|
|
596
|
+
if (!fs4.existsSync(file)) return null;
|
|
597
|
+
return parseManifestJson(fs4.readFileSync(file, "utf8"));
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function configDirFromConfig(config) {
|
|
603
|
+
if (config?.configFile) return path4.dirname(config.configFile);
|
|
604
|
+
if (config?.rootDir) return config.rootDir;
|
|
605
|
+
return process.cwd();
|
|
606
|
+
}
|
|
607
|
+
function baseUrlFromConfig(config) {
|
|
608
|
+
const top = config?.use?.baseURL;
|
|
609
|
+
if (typeof top === "string" && top) return top;
|
|
610
|
+
for (const project of config?.projects ?? []) {
|
|
611
|
+
const url = project?.use?.baseURL;
|
|
612
|
+
if (typeof url === "string" && url) return url;
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
|
|
617
|
+
async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
|
|
618
|
+
try {
|
|
619
|
+
const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
|
|
620
|
+
if (!hasInstrumentationHeader(probe.headers)) return null;
|
|
621
|
+
const manifestUrl = new URL(INSTRUMENTATION_MANIFEST_PATH, baseUrl).toString();
|
|
622
|
+
const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
|
|
623
|
+
if (!res.ok) return null;
|
|
624
|
+
return parseManifestJson(await res.text());
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
503
630
|
// src/public/global-setup.ts
|
|
504
631
|
function createGlobalSetup(options, userSetup) {
|
|
505
632
|
return async function globalSetupFn(config) {
|
|
506
|
-
const piwiReporterPath =
|
|
633
|
+
const piwiReporterPath = path5.resolve(__dirname, "./index.js");
|
|
507
634
|
let inlineReporterOptions = {};
|
|
508
635
|
if (Array.isArray(config?.reporter)) {
|
|
509
636
|
for (const r of config.reporter) {
|
|
510
637
|
if (!Array.isArray(r) || typeof r[0] !== "string") continue;
|
|
511
638
|
const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
|
|
512
639
|
try {
|
|
513
|
-
return
|
|
640
|
+
return path5.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
514
641
|
} catch {
|
|
515
642
|
return false;
|
|
516
643
|
}
|
|
@@ -542,7 +669,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
542
669
|
if (!Array.isArray(r) || typeof r[0] !== "string") return false;
|
|
543
670
|
if (r[0].toLowerCase().includes("piwi")) return true;
|
|
544
671
|
try {
|
|
545
|
-
return
|
|
672
|
+
return path5.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
546
673
|
} catch {
|
|
547
674
|
return false;
|
|
548
675
|
}
|
|
@@ -566,6 +693,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
566
693
|
projectDescription: opts.projectDescription,
|
|
567
694
|
environment: opts.environment || null,
|
|
568
695
|
label: opts.label || null,
|
|
696
|
+
keep: opts.keep === true,
|
|
569
697
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
570
698
|
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
571
699
|
shardIndex,
|
|
@@ -574,7 +702,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
574
702
|
auth
|
|
575
703
|
);
|
|
576
704
|
if (response?.runId && response?.setupToken) {
|
|
577
|
-
|
|
705
|
+
fs5.writeFileSync(
|
|
578
706
|
getSetupFilePath(opts.projectName),
|
|
579
707
|
JSON.stringify({
|
|
580
708
|
runId: response.runId,
|
|
@@ -585,18 +713,43 @@ function createGlobalSetup(options, userSetup) {
|
|
|
585
713
|
logger.debug(`Global setup: initializing run #${response.runId}`);
|
|
586
714
|
}
|
|
587
715
|
if (opts.projectName) clearAriaSampleFile(opts.projectName);
|
|
588
|
-
|
|
716
|
+
const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
|
|
717
|
+
const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
|
|
718
|
+
const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
|
|
719
|
+
const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
|
|
720
|
+
let projectId;
|
|
721
|
+
if ((wantsAria || wantsManifest) && opts.projectName) {
|
|
589
722
|
const menu = await httpClient.getJSON("/api/projects/menu", auth);
|
|
590
|
-
|
|
723
|
+
projectId = menu?.items?.find(
|
|
591
724
|
(p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
|
|
592
725
|
)?.id;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
726
|
+
}
|
|
727
|
+
if (wantsAria && projectId != null) {
|
|
728
|
+
const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
|
|
729
|
+
const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
|
|
730
|
+
if (tests) {
|
|
731
|
+
const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
|
|
732
|
+
writeAriaSampleFile(opts.projectName, identities);
|
|
733
|
+
logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (wantsManifest && projectId != null) {
|
|
737
|
+
const branch = resolveScmBranch(process.env) ?? null;
|
|
738
|
+
if (committedManifest) {
|
|
739
|
+
await httpClient.putJSON(
|
|
740
|
+
`/api/projects/${projectId}/surface/manifest`,
|
|
741
|
+
{ source: "committed", branch, manifest: committedManifest },
|
|
742
|
+
auth
|
|
743
|
+
).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
|
|
744
|
+
}
|
|
745
|
+
if (manifestBaseUrl) {
|
|
746
|
+
const declared = await fetchInstrumentationManifest(manifestBaseUrl);
|
|
747
|
+
if (declared) {
|
|
748
|
+
await httpClient.putJSON(
|
|
749
|
+
`/api/projects/${projectId}/surface/manifest`,
|
|
750
|
+
{ source: "instrumentation", branch, manifest: declared },
|
|
751
|
+
auth
|
|
752
|
+
).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
|
|
600
753
|
}
|
|
601
754
|
}
|
|
602
755
|
}
|