@piwitests/reporter 0.49.0 → 0.50.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/global-setup-module.js +13 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +111 -18
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
|
@@ -648,11 +648,17 @@ function wildcard(pattern) {
|
|
|
648
648
|
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
649
649
|
return new RegExp(`^${escaped.join(".*")}$`, "i");
|
|
650
650
|
}
|
|
651
|
-
function selectedProjects(projects, argv = process.argv) {
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
651
|
+
function selectedProjects(projects, argv = process.argv, filtered) {
|
|
652
|
+
let chosen;
|
|
653
|
+
if (Array.isArray(filtered)) {
|
|
654
|
+
const names = new Set(filtered.map((p) => p.name).filter(Boolean));
|
|
655
|
+
chosen = new Set(projects.filter((p) => filtered.includes(p) || !!p.name && names.has(p.name)));
|
|
656
|
+
} else {
|
|
657
|
+
const names = cliProjectNames(argv);
|
|
658
|
+
if (!names || names.length === 0) return projects;
|
|
659
|
+
const patterns = names.map(wildcard);
|
|
660
|
+
chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
661
|
+
}
|
|
656
662
|
if (!argv.includes("--no-deps")) {
|
|
657
663
|
const byName = new Map(projects.map((p) => [p.name ?? "", p]));
|
|
658
664
|
const queue = [...chosen];
|
|
@@ -674,8 +680,9 @@ function asProxy(value) {
|
|
|
674
680
|
}
|
|
675
681
|
function baseUrlTargets(config, argv = process.argv) {
|
|
676
682
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
683
|
+
const filtered = config.projects?.length ? config.filteredProjects : void 0;
|
|
677
684
|
const targets = /* @__PURE__ */ new Map();
|
|
678
|
-
for (const project of selectedProjects(projects, argv)) {
|
|
685
|
+
for (const project of selectedProjects(projects, argv, filtered)) {
|
|
679
686
|
const url = project.use?.baseURL;
|
|
680
687
|
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
681
688
|
const target = targets.get(url);
|
package/dist/index.d.ts
CHANGED
|
@@ -175,7 +175,9 @@ interface PiwiDashboardOptions {
|
|
|
175
175
|
* runs no test and reaches no dashboard. The check runs in the reporter's
|
|
176
176
|
* global setup, after `webServer` has started and before the config's own
|
|
177
177
|
* `globalSetup`, so leave it off when that setup is what starts the app. Only
|
|
178
|
-
* the projects
|
|
178
|
+
* the projects the run selects are checked, with their dependencies: the ones
|
|
179
|
+
* `--project` names, or every project but those declared `default: false`
|
|
180
|
+
* (Playwright 1.64+). **Defaults to `false`**. Can
|
|
179
181
|
* also be set with `PIWI_CHECK_BASE_URL=true`.
|
|
180
182
|
*/
|
|
181
183
|
checkBaseUrl?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -3831,11 +3831,17 @@ function wildcard(pattern) {
|
|
|
3831
3831
|
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
3832
3832
|
return new RegExp(`^${escaped.join(".*")}$`, "i");
|
|
3833
3833
|
}
|
|
3834
|
-
function selectedProjects(projects, argv = process.argv) {
|
|
3835
|
-
|
|
3836
|
-
if (
|
|
3837
|
-
|
|
3838
|
-
|
|
3834
|
+
function selectedProjects(projects, argv = process.argv, filtered) {
|
|
3835
|
+
let chosen;
|
|
3836
|
+
if (Array.isArray(filtered)) {
|
|
3837
|
+
const names = new Set(filtered.map((p) => p.name).filter(Boolean));
|
|
3838
|
+
chosen = new Set(projects.filter((p) => filtered.includes(p) || !!p.name && names.has(p.name)));
|
|
3839
|
+
} else {
|
|
3840
|
+
const names = cliProjectNames(argv);
|
|
3841
|
+
if (!names || names.length === 0) return projects;
|
|
3842
|
+
const patterns = names.map(wildcard);
|
|
3843
|
+
chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
3844
|
+
}
|
|
3839
3845
|
if (!argv.includes("--no-deps")) {
|
|
3840
3846
|
const byName = new Map(projects.map((p) => [p.name ?? "", p]));
|
|
3841
3847
|
const queue = [...chosen];
|
|
@@ -3857,8 +3863,9 @@ function asProxy(value) {
|
|
|
3857
3863
|
}
|
|
3858
3864
|
function baseUrlTargets(config, argv = process.argv) {
|
|
3859
3865
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
3866
|
+
const filtered = config.projects?.length ? config.filteredProjects : void 0;
|
|
3860
3867
|
const targets = /* @__PURE__ */ new Map();
|
|
3861
|
-
for (const project of selectedProjects(projects, argv)) {
|
|
3868
|
+
for (const project of selectedProjects(projects, argv, filtered)) {
|
|
3862
3869
|
const url = project.use?.baseURL;
|
|
3863
3870
|
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
3864
3871
|
const target = targets.get(url);
|
|
@@ -13693,6 +13700,97 @@ function compileFromCandidate(candidate) {
|
|
|
13693
13700
|
return null;
|
|
13694
13701
|
}
|
|
13695
13702
|
|
|
13703
|
+
// src/internal/ai/ground.ts
|
|
13704
|
+
var GROUNDED_METHODS = new Set(LOCATOR_METHODS.filter((method) => method !== "locator"));
|
|
13705
|
+
async function refLocator(page, ref) {
|
|
13706
|
+
if (!/^[a-z0-9]+$/i.test(ref)) return null;
|
|
13707
|
+
const getByRef = page.getByRef;
|
|
13708
|
+
try {
|
|
13709
|
+
const target = typeof getByRef === "function" ? getByRef.call(page, ref) : page.locator(`aria-ref=${ref}`);
|
|
13710
|
+
return await target.count() === 1 ? target : null;
|
|
13711
|
+
} catch {
|
|
13712
|
+
return null;
|
|
13713
|
+
}
|
|
13714
|
+
}
|
|
13715
|
+
async function matchesOnly(candidate, target) {
|
|
13716
|
+
try {
|
|
13717
|
+
return await candidate.count() === 1 && await candidate.and(target).count() === 1;
|
|
13718
|
+
} catch {
|
|
13719
|
+
return false;
|
|
13720
|
+
}
|
|
13721
|
+
}
|
|
13722
|
+
async function includes(candidate, target) {
|
|
13723
|
+
try {
|
|
13724
|
+
return await candidate.and(target).count() === 1;
|
|
13725
|
+
} catch {
|
|
13726
|
+
return false;
|
|
13727
|
+
}
|
|
13728
|
+
}
|
|
13729
|
+
function plainArg(arg, params, mask = true) {
|
|
13730
|
+
switch (arg.type) {
|
|
13731
|
+
case "string":
|
|
13732
|
+
return mask ? maskValues(arg.value, params) : arg.value;
|
|
13733
|
+
case "number":
|
|
13734
|
+
case "boolean":
|
|
13735
|
+
return arg.value;
|
|
13736
|
+
case "object": {
|
|
13737
|
+
const out = {};
|
|
13738
|
+
for (const [key, value] of arg.entries) {
|
|
13739
|
+
const plain = plainArg(value, params, mask);
|
|
13740
|
+
if (plain === void 0) return void 0;
|
|
13741
|
+
out[key] = plain;
|
|
13742
|
+
}
|
|
13743
|
+
return out;
|
|
13744
|
+
}
|
|
13745
|
+
default:
|
|
13746
|
+
return void 0;
|
|
13747
|
+
}
|
|
13748
|
+
}
|
|
13749
|
+
function chainToStructured(calls, params = {}) {
|
|
13750
|
+
const links = [];
|
|
13751
|
+
for (const call of calls) {
|
|
13752
|
+
if (!GROUNDED_METHODS.has(call.method)) return null;
|
|
13753
|
+
const args = [];
|
|
13754
|
+
for (const arg of call.args) {
|
|
13755
|
+
const plain = plainArg(arg, params, call.method !== "getByTestId");
|
|
13756
|
+
if (plain === void 0) return null;
|
|
13757
|
+
args.push(plain);
|
|
13758
|
+
}
|
|
13759
|
+
links.push({ method: call.method, args });
|
|
13760
|
+
}
|
|
13761
|
+
const [head, ...chain] = links;
|
|
13762
|
+
if (!head) return null;
|
|
13763
|
+
return chain.length > 0 ? { ...head, chain } : head;
|
|
13764
|
+
}
|
|
13765
|
+
async function normalizedLocator(target, params) {
|
|
13766
|
+
if (typeof target.normalize !== "function") return null;
|
|
13767
|
+
try {
|
|
13768
|
+
const chain = tryParseLocatorChain(String(await target.normalize()));
|
|
13769
|
+
return chain ? chainToStructured(chain.calls, params) : null;
|
|
13770
|
+
} catch {
|
|
13771
|
+
return null;
|
|
13772
|
+
}
|
|
13773
|
+
}
|
|
13774
|
+
async function groundElement(page, element, params) {
|
|
13775
|
+
const compiled = compileFromCandidate({
|
|
13776
|
+
role: element.role,
|
|
13777
|
+
name: element.name ?? null,
|
|
13778
|
+
level: element.level ?? null
|
|
13779
|
+
});
|
|
13780
|
+
const target = element.ref ? await refLocator(page, element.ref) : null;
|
|
13781
|
+
if (!target) return compiled;
|
|
13782
|
+
if (compiled) {
|
|
13783
|
+
const byName = buildLocator(page, compiled.locator, params);
|
|
13784
|
+
if (await matchesOnly(byName, target)) return compiled;
|
|
13785
|
+
if (!await includes(byName, target)) return compiled;
|
|
13786
|
+
}
|
|
13787
|
+
const normalized = await normalizedLocator(target, params);
|
|
13788
|
+
if (!normalized || !await matchesOnly(buildLocator(page, normalized, params), target)) return compiled;
|
|
13789
|
+
const fingerprint = compiled?.fingerprint ?? { role: element.role, name: element.name ?? null };
|
|
13790
|
+
if (!compiled && element.level != null) fingerprint.level = element.level;
|
|
13791
|
+
return { locator: normalized, fingerprint, score: compiled?.score ?? 0 };
|
|
13792
|
+
}
|
|
13793
|
+
|
|
13696
13794
|
// src/internal/ai/resolution.ts
|
|
13697
13795
|
var MAX_SNAPSHOT_CHARS = 24e3;
|
|
13698
13796
|
var DEFAULT_MAX_STEPS = 20;
|
|
@@ -13713,12 +13811,6 @@ var ServerStepResolver = class {
|
|
|
13713
13811
|
return await res.json();
|
|
13714
13812
|
}
|
|
13715
13813
|
};
|
|
13716
|
-
function candidateOf(element) {
|
|
13717
|
-
return { role: element.role, name: element.name ?? null, level: element.level ?? null };
|
|
13718
|
-
}
|
|
13719
|
-
function locatorFromElement(element) {
|
|
13720
|
-
return compileFromCandidate(candidateOf(element));
|
|
13721
|
-
}
|
|
13722
13814
|
function lazyLocator(resolveOnce) {
|
|
13723
13815
|
let pending = null;
|
|
13724
13816
|
const ensure = () => pending ??= resolveOnce();
|
|
@@ -13730,20 +13822,21 @@ function lazyLocator(resolveOnce) {
|
|
|
13730
13822
|
};
|
|
13731
13823
|
return new Proxy({}, handler);
|
|
13732
13824
|
}
|
|
13733
|
-
function buildPostcondition(resolved, template) {
|
|
13825
|
+
async function buildPostcondition(resolved, template, ctx) {
|
|
13734
13826
|
if (!resolved)
|
|
13735
13827
|
throw new Error(`piwi AI: flow "${template}" resolved without a postcondition (the oracle is mandatory)`);
|
|
13736
13828
|
if (resolved.assert === "url") {
|
|
13737
13829
|
if (!resolved.url) throw new Error(`piwi AI: flow "${template}" postcondition is a url assert without a url`);
|
|
13738
13830
|
return { assert: "url", url: resolved.url };
|
|
13739
13831
|
}
|
|
13740
|
-
const compiled = resolved.element ?
|
|
13832
|
+
const compiled = resolved.element ? await groundElement(ctx.page, resolved.element, ctx.params) : null;
|
|
13741
13833
|
if (!compiled) throw new Error(`piwi AI: flow "${template}" postcondition element could not be compiled`);
|
|
13742
13834
|
return { assert: resolved.assert, locator: compiled.locator };
|
|
13743
13835
|
}
|
|
13744
13836
|
var readMaskedSnapshot = async (page, params) => {
|
|
13745
13837
|
const raw = await ariaSnapshotBestEffort(page.locator("body"));
|
|
13746
|
-
|
|
13838
|
+
if (!raw) return "";
|
|
13839
|
+
return raw.split(/(\[ref=\w+\])/).map((part, i) => i % 2 === 1 ? part : maskValues(part, params)).join("");
|
|
13747
13840
|
};
|
|
13748
13841
|
var observeXhrResponses = async (page, action, settleMs) => {
|
|
13749
13842
|
const urls = [];
|
|
@@ -13795,7 +13888,7 @@ async function resolveLocator(template, ctx) {
|
|
|
13795
13888
|
...screenshot ? { screenshot } : {}
|
|
13796
13889
|
});
|
|
13797
13890
|
if (!decision.element) throw new Error(`piwi AI: resolver returned no element for "${template}"`);
|
|
13798
|
-
const compiled =
|
|
13891
|
+
const compiled = await groundElement(ctx.page, decision.element, ctx.params);
|
|
13799
13892
|
if (!compiled) throw new Error(`piwi AI: element for "${template}" could not be compiled to a stable locator`);
|
|
13800
13893
|
const entry = {
|
|
13801
13894
|
version: ARTIFACT_VERSION,
|
|
@@ -13828,14 +13921,14 @@ async function resolveRun(template, ctx) {
|
|
|
13828
13921
|
...screenshot ? { screenshot } : {}
|
|
13829
13922
|
});
|
|
13830
13923
|
if (decision.done) {
|
|
13831
|
-
const postcondition = buildPostcondition(decision.postcondition, template);
|
|
13924
|
+
const postcondition = await buildPostcondition(decision.postcondition, template, ctx);
|
|
13832
13925
|
await assertPostcondition(postcondition, { page: ctx.page, params: ctx.params });
|
|
13833
13926
|
return { version: ARTIFACT_VERSION, kind: "run", template, steps, postcondition };
|
|
13834
13927
|
}
|
|
13835
13928
|
if (!decision.element || !decision.action) {
|
|
13836
13929
|
throw new Error(`piwi AI: resolver returned neither a step nor done for "${template}"`);
|
|
13837
13930
|
}
|
|
13838
|
-
const compiled =
|
|
13931
|
+
const compiled = await groundElement(ctx.page, decision.element, ctx.params);
|
|
13839
13932
|
if (!compiled) throw new Error(`piwi AI: a step element for "${template}" could not be compiled`);
|
|
13840
13933
|
const step = { locator: compiled.locator, action: decision.action, fingerprint: compiled.fingerprint };
|
|
13841
13934
|
if (decision.value !== void 0) step.value = decision.value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|
|
@@ -28,7 +28,7 @@ Prefer the **Piwi MCP server** if it is connected to this agent (tools named `li
|
|
|
28
28
|
7. **Verify.** Re-run the affected spec(s): `npx playwright test <file>`. Confirm they pass and the new run is green in the dashboard (`get_run_insights` compares against the last green run: regressions cleared, nothing new broken).
|
|
29
29
|
|
|
30
30
|
8. **Report back.** Piwi learns from what you tell it, so end every investigation with these calls (they need reporter or admin access; each is logged with your API key):
|
|
31
|
-
- **Your diagnosis.** If the cluster had no diagnosis, or yours differs, record it with `record_diagnosis
|
|
31
|
+
- **Your diagnosis.** If the cluster had no diagnosis, or yours differs, record it with `record_diagnosis` (`clusterId`, or `executionId` for a failure in no cluster or with a cause of its own): the model you run on and the diagnosis in the schema the tool describes (summary, confidenceScore, severity, ranked hypotheses with their evidence, suggestedFix with the unified-diff `patch`). Piwi validates the patch against the source that failed and shows the diagnosis as written by an agent.
|
|
32
32
|
- **Your rating of Piwi's diagnosis.** If you read one with `get_cluster_diagnosis`, rate it with `submit_diagnosis_feedback`: `up` when it pointed at the real cause, `down` with a one-line note on what it got wrong when it did not. A `down` keeps the next diagnosis from repeating it.
|
|
33
33
|
- **Your fix.** Commit the change with the trailer `get_fix_plan` returns in `verify.commitTrailer` (`Piwi-Cluster: <clusterId>`) as the last line of the commit message, then call `report_fix_attempt` with the cluster, the kind (`patch`, `locator-edit` or `fix-plan`), the commit or the branch, the patch you applied and the `diagnosisId` you followed. When the tests pass on that commit, Piwi records the attempt verified and shows it on the cluster's activity.
|
|
34
34
|
- Leave the cluster's status alone: the run that passes on your commit records the fix.
|