@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.
@@ -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
- const names = cliProjectNames(argv);
653
- if (!names || names.length === 0) return projects;
654
- const patterns = names.map(wildcard);
655
- const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
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 `--project` selects are checked. **Defaults to `false`**. Can
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
- const names = cliProjectNames(argv);
3836
- if (!names || names.length === 0) return projects;
3837
- const patterns = names.map(wildcard);
3838
- const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
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 ? locatorFromElement(resolved.element) : null;
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
- return raw ? maskValues(raw, params) : "";
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 = locatorFromElement(decision.element);
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 = locatorFromElement(decision.element);
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.49.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`: 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.
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.