@skyramp/mcp 0.4.3 → 0.4.4-rc.1
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/build/playwright/traceRecordingPrompt.js +2 -1
- package/build/prompts/orderAssertionRule.d.ts +9 -0
- package/build/prompts/orderAssertionRule.js +9 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/testbot/testbot-prompts.js +10 -7
- package/build/recommendation/answers.d.ts +54 -19
- package/build/recommendation/answers.js +102 -31
- package/build/recommendation/blockerProbes.d.ts +13 -0
- package/build/recommendation/blockerProbes.js +91 -0
- package/build/recommendation/blockers.d.ts +65 -0
- package/build/recommendation/blockers.js +135 -0
- package/build/recommendation/pullRequestText.d.ts +2 -0
- package/build/recommendation/pullRequestText.js +16 -0
- package/build/recommendation/registerPlan.d.ts +7 -4
- package/build/recommendation/registerPlan.js +37 -11
- package/build/recommendation/types.d.ts +21 -4
- package/build/recommendation/verifierContracts.d.ts +6 -6
- package/build/recommendation/verifierContracts.js +8 -7
- package/build/recommendation/verifiers/citedPath.d.ts +2 -2
- package/build/recommendation/verifiers/citedPath.js +2 -2
- package/build/recommendation/verifiers/coverage.js +25 -25
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +3 -3
- package/build/recommendation/verifiers/issueTraceability.d.ts +1 -5
- package/build/recommendation/verifiers/issueTraceability.js +4 -35
- package/build/tools/executeSkyrampTestTool.js +8 -0
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateE2ERestTool.js +9 -1
- package/build/tools/generate-tests/generateUIRestTool.d.ts +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +9 -1
- package/build/tools/submitReportTool.d.ts +9 -9
- package/build/tools/submitReportTool.js +178 -26
- package/build/tools/test-management/registerTestPlanTool.d.ts +20 -14
- package/build/tools/test-management/registerTestPlanTool.js +30 -14
- package/build/types/TestTypes.d.ts +7 -0
- package/build/types/TestTypes.js +22 -0
- package/build/types/TestbotReport.d.ts +4 -1
- package/build/utils/assertion-verify/metrics.js +12 -0
- package/build/utils/findUp.d.ts +20 -0
- package/build/utils/findUp.js +59 -0
- package/build/utils/language-helper.js +1 -1
- package/build/utils/repoSdkVersion.d.ts +16 -0
- package/build/utils/repoSdkVersion.js +88 -0
- package/build/utils/testDependencyPolicy.d.ts +20 -0
- package/build/utils/testDependencyPolicy.js +64 -4
- package/build/utils/utils-verify/typecheck.js +3 -18
- package/node_modules/playwright/lib/mcp/skyramp/assertOrder.js +66 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +12 -3
- package/node_modules/playwright/lib/mcp/skyramp/common/liveReadError.js +40 -0
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +21 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +96 -16
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +10 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +22 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/elementOrder.js +57 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.uiPTFncY.js → index.0MJW0uG7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +26 -1
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/elementOrder.ts +83 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -2
- package/plugin/prompts/plan-tests.md +9 -17
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -1
- package/plugin/skills/enhance-assertions/reference/ui.md +2 -2
- package/plugin/skills/fix-test-import-errors/SKILL.md +1 -1
|
@@ -45,6 +45,8 @@ var import_log = require("../log");
|
|
|
45
45
|
var import_skyRampExport = require("../test/skyRampExport");
|
|
46
46
|
var import_exportTool = require("./exportTool");
|
|
47
47
|
var import_assertTool = require("./assertTool");
|
|
48
|
+
var import_assertOrder = require("./assertOrder");
|
|
49
|
+
var import_liveReadError = require("./common/liveReadError");
|
|
48
50
|
var import_assertHiddenTool = require("./assertHiddenTool");
|
|
49
51
|
var import_assertApiRequestTool = require("./assertApiRequestTool");
|
|
50
52
|
var import_loadTraceTool = require("./loadTraceTool");
|
|
@@ -246,7 +248,9 @@ class TraceRecordingBackend {
|
|
|
246
248
|
}
|
|
247
249
|
if (name === import_assertTool.assertToolSchema.name) {
|
|
248
250
|
const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
|
|
249
|
-
|
|
251
|
+
if (parsed.type === "order")
|
|
252
|
+
return this._handleAssertOrder(parsed);
|
|
253
|
+
return this._handleAssert({ ...parsed, type: parsed.type });
|
|
250
254
|
}
|
|
251
255
|
if (name === import_assertHiddenTool.assertHiddenToolSchema.name) {
|
|
252
256
|
const parsed = import_assertHiddenTool.assertHiddenToolSchema.inputSchema.parse(args || {});
|
|
@@ -933,6 +937,14 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
|
|
|
933
937
|
return null;
|
|
934
938
|
return { toolName: "browser_assert", code: `assertAttribute:${a.selector}:${a.attrName.trim()}:${a.attrValue}`, args: { type: "attribute", selector: a.selector, property: a.attrName.trim(), expected: a.attrValue } };
|
|
935
939
|
}
|
|
940
|
+
// SKYR-4544: an order assertion needs both ends and its relation; a line
|
|
941
|
+
// missing one is dropped rather than seeded with a hole.
|
|
942
|
+
case "assertOrder": {
|
|
943
|
+
const fields = (0, import_utils.parseOrderFields)(a);
|
|
944
|
+
if (!fields)
|
|
945
|
+
return null;
|
|
946
|
+
return { toolName: "browser_assert", code: `assertOrder:${fields.selector}:${fields.relation}:${fields.otherSelector}`, args: { type: "order", ...fields } };
|
|
947
|
+
}
|
|
936
948
|
case "assertCount":
|
|
937
949
|
if (typeof a.count !== "number" || !Number.isInteger(a.count) || a.count < 0)
|
|
938
950
|
return null;
|
|
@@ -1340,8 +1352,9 @@ Linked GoJS nodes ${params.fromKey} \u2192 ${params.toKey} (${params.fromPort}\u
|
|
|
1340
1352
|
* proper Playwright selector (testid > role > text), then verify
|
|
1341
1353
|
* text/value/checked/disabled/visible/count against the snapshot data, and
|
|
1342
1354
|
* css/class/attribute against the live element (getComputedStyle / classList
|
|
1343
|
-
* / getAttribute, read with the pointer moved off the element).
|
|
1344
|
-
*
|
|
1355
|
+
* / getAttribute, read with the pointer moved off the element). "order" is
|
|
1356
|
+
* dispatched to _handleAssertOrder instead: two refs, a live boundingBox()
|
|
1357
|
+
* comparison, no pointer-off read. Only passing assertions are recorded.
|
|
1345
1358
|
*/
|
|
1346
1359
|
async _handleAssert(params) {
|
|
1347
1360
|
const timestamp = Date.now();
|
|
@@ -1471,15 +1484,7 @@ Could not read the un-hovered state of "${params.element}": the pointer could no
|
|
|
1471
1484
|
details = passed ? `Attribute assertion passed: "${params.element}" has ${name}="${actual}".` : `Attribute assertion FAILED: "${params.element}" has ${name}=${actual === null ? "(absent)" : `"${actual}"`}, expected "${params.expected}".`;
|
|
1472
1485
|
}
|
|
1473
1486
|
} catch (e) {
|
|
1474
|
-
|
|
1475
|
-
if (/strict mode violation/i.test(msg))
|
|
1476
|
-
return { content: [{ type: "text", text: `### Error
|
|
1477
|
-
Target ${parsed.selector} matches multiple elements, so the ${kind} read is ambiguous${unique.pinFailure ? `, and ${unique.pinFailure}` : ""}. Use a more specific target (a data-testid, or role + exact name). ${msg.split("\n")[0]}` }], isError: true };
|
|
1478
|
-
if (e?.name === "TimeoutError")
|
|
1479
|
-
return { content: [{ type: "text", text: `### Error
|
|
1480
|
-
"${params.element}" (${parsed.selector}) did not resolve within ${LIVE_READ_TIMEOUT_MS} ms. Re-snapshot and retry with a fresh ref.` }], isError: true };
|
|
1481
|
-
return { content: [{ type: "text", text: `### Error
|
|
1482
|
-
Could not read ${kind} of "${params.element}": ${msg}` }], isError: true };
|
|
1487
|
+
return (0, import_liveReadError.liveReadError)(e, { selector: parsed.selector, element: params.element, ambiguity: `the ${kind} read is ambiguous`, failedTo: `read ${kind} of`, pinFailure: unique.pinFailure, timeoutMs: LIVE_READ_TIMEOUT_MS });
|
|
1483
1488
|
}
|
|
1484
1489
|
if (passed)
|
|
1485
1490
|
details += unique.note;
|
|
@@ -1624,6 +1629,82 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
|
|
|
1624
1629
|
${details}` }]
|
|
1625
1630
|
};
|
|
1626
1631
|
}
|
|
1632
|
+
/**
|
|
1633
|
+
* browser_assert type "order" (SKYR-4544): the ref's element sits left of /
|
|
1634
|
+
* right of / above / below the other ref's element. A swap or a moved block
|
|
1635
|
+
* is a relation between two elements, which no single-element kind can
|
|
1636
|
+
* express, so both ends are resolved through the same durable-locator path
|
|
1637
|
+
* as the other kinds and their live boxes compared on the relation's axis.
|
|
1638
|
+
* The comparison is the one the generated test runs: each boundingBox()'s
|
|
1639
|
+
* origin — the left edge (x) for left-of/right-of, the top edge (y) for
|
|
1640
|
+
* above/below — compared strictly. It says where each element STARTS, not
|
|
1641
|
+
* that one lies wholly beyond the other.
|
|
1642
|
+
*/
|
|
1643
|
+
async _handleAssertOrder(params) {
|
|
1644
|
+
const timestamp = Date.now();
|
|
1645
|
+
const error = (text) => ({ content: [{ type: "text", text: `### Error
|
|
1646
|
+
${text}` }], isError: true });
|
|
1647
|
+
const failed = (text) => ({ content: [{ type: "text", text: `### Assertion Failed
|
|
1648
|
+
${text}` }], isError: true });
|
|
1649
|
+
const checked = (0, import_assertOrder.checkOrderArgs)(params);
|
|
1650
|
+
if ("error" in checked)
|
|
1651
|
+
return error(checked.error);
|
|
1652
|
+
const { relation, otherElement } = checked;
|
|
1653
|
+
const tab = this._browserBackend.context?.currentTab();
|
|
1654
|
+
if (!tab)
|
|
1655
|
+
return failed("No active page.");
|
|
1656
|
+
const notReady = await this._requireLoadedPage(tab, "domcontentloaded");
|
|
1657
|
+
if (notReady)
|
|
1658
|
+
return notReady;
|
|
1659
|
+
const ends = [];
|
|
1660
|
+
for (const [ref, element] of [[params.ref, params.element], [params.otherRef, otherElement]]) {
|
|
1661
|
+
const resolved = await this._resolveRef(ref, element);
|
|
1662
|
+
if ("error" in resolved)
|
|
1663
|
+
return failed(resolved.error);
|
|
1664
|
+
const lineText = (0, import_snapshotLocators.snapshotLineText)(resolved.refLine) ?? "";
|
|
1665
|
+
const unique = await this._makeUnique(resolved.parsed, { snapLines: resolved.snapLines, ref, element, lineText, exact: false });
|
|
1666
|
+
if (unique.refusal)
|
|
1667
|
+
return unique.refusal;
|
|
1668
|
+
ends.push({ parsed: unique.parsed, note: unique.note, pinFailure: unique.pinFailure, element, selector: unique.parsed.selector });
|
|
1669
|
+
}
|
|
1670
|
+
const [a, b] = ends;
|
|
1671
|
+
if (a.selector === b.selector)
|
|
1672
|
+
return error(`"${a.element}" and "${b.element}" resolve to the same locator (${a.selector}), so their order cannot be asserted. Use refs of two different elements.`);
|
|
1673
|
+
const boxes = [];
|
|
1674
|
+
for (const end of ends) {
|
|
1675
|
+
let box;
|
|
1676
|
+
try {
|
|
1677
|
+
box = await tab.page.locator(end.selector).boundingBox({ timeout: LIVE_READ_TIMEOUT_MS });
|
|
1678
|
+
} catch (e) {
|
|
1679
|
+
return (0, import_liveReadError.liveReadError)(e, { selector: end.selector, element: end.element, ambiguity: "its position is ambiguous", failedTo: "read the position of", pinFailure: end.pinFailure, timeoutMs: LIVE_READ_TIMEOUT_MS });
|
|
1680
|
+
}
|
|
1681
|
+
if (!box)
|
|
1682
|
+
return failed(`"${end.element}" (${end.selector}) has no box on the page (it is not visible), so its position cannot be compared.`);
|
|
1683
|
+
boxes.push(box);
|
|
1684
|
+
}
|
|
1685
|
+
const [boxA, boxB] = boxes;
|
|
1686
|
+
const nested = (0, import_assertOrder.containmentError)(a, b, boxA, boxB);
|
|
1687
|
+
if (nested)
|
|
1688
|
+
return error(nested);
|
|
1689
|
+
const { result, details } = (0, import_assertOrder.describeOrderResult)(relation, a, b, boxA, boxB);
|
|
1690
|
+
if (!result.ok)
|
|
1691
|
+
return { content: [{ type: "text", text: `### Assertion Failed
|
|
1692
|
+
${details}` }] };
|
|
1693
|
+
const args = {
|
|
1694
|
+
type: "order",
|
|
1695
|
+
ref: params.ref,
|
|
1696
|
+
selector: a.selector,
|
|
1697
|
+
locator: a.parsed.locator,
|
|
1698
|
+
otherRef: params.otherRef,
|
|
1699
|
+
otherSelector: b.selector,
|
|
1700
|
+
otherLocator: b.parsed.locator,
|
|
1701
|
+
relation
|
|
1702
|
+
};
|
|
1703
|
+
this._trackedActions.push({ toolName: "browser_assert", args, code: `assertOrder:${a.selector}:${relation}:${b.selector}`, timestamp, pageAlias: this._currentPageAlias });
|
|
1704
|
+
traceDebug(`Assert: assertOrder ${a.selector} ${relation} ${b.selector}`);
|
|
1705
|
+
return { content: [{ type: "text", text: `### Assertion Passed
|
|
1706
|
+
${details}${a.note}${b.note}` }] };
|
|
1707
|
+
}
|
|
1627
1708
|
/**
|
|
1628
1709
|
* Handle browser_assert_table_cell by resolving the cell from (row, column),
|
|
1629
1710
|
* reading its value directly from the page, and recording an assertTableCell
|
|
@@ -1838,10 +1919,9 @@ ${text}` }], isError: true });
|
|
|
1838
1919
|
try {
|
|
1839
1920
|
await tab.page.locator(selector).waitFor({ state: "hidden", timeout: TraceRecordingBackend.HIDDEN_WAIT_TIMEOUT_MS });
|
|
1840
1921
|
} catch (e) {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
return err(`Could not verify absence of "${params.element}": ${e?.message ?? e}`);
|
|
1922
|
+
const error = (0, import_liveReadError.liveReadError)(e, { selector, element: params.element, ambiguity: "toBeHidden() would fail strict mode", failedTo: "verify absence of" });
|
|
1923
|
+
if (error)
|
|
1924
|
+
return error;
|
|
1845
1925
|
passed = false;
|
|
1846
1926
|
}
|
|
1847
1927
|
let details;
|
|
@@ -509,6 +509,16 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
|
|
|
509
509
|
return null;
|
|
510
510
|
return JSON.stringify({ name: "assertAttribute", selector, attrName, attrValue: expected, locator, ...base });
|
|
511
511
|
}
|
|
512
|
+
// SKYR-4544: where the element sits relative to another one — a
|
|
513
|
+
// boundingBox() comparison downstream. Both ends and the relation are
|
|
514
|
+
// needed; a line missing any of them is dropped (reported as skipped).
|
|
515
|
+
case "order": {
|
|
516
|
+
const fields = (0, import_utils.parseOrderFields)({ ...args, selector });
|
|
517
|
+
if (!fields)
|
|
518
|
+
return null;
|
|
519
|
+
const otherLocator = fields.otherLocator ?? selectorToLocator(fields.otherSelector);
|
|
520
|
+
return JSON.stringify({ name: "assertOrder", selector, otherSelector: fields.otherSelector, relation: fields.relation, locator, otherLocator, ...base });
|
|
521
|
+
}
|
|
512
522
|
default:
|
|
513
523
|
return null;
|
|
514
524
|
}
|
package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js
CHANGED
|
@@ -196,6 +196,28 @@ async function performActionImpl(progress, mainFrame, actionInContext) {
|
|
|
196
196
|
});
|
|
197
197
|
return;
|
|
198
198
|
}
|
|
199
|
+
if (raw.name === "assertOrder") {
|
|
200
|
+
const fields = (0, import_utils.parseOrderFields)(raw);
|
|
201
|
+
if (!fields)
|
|
202
|
+
throw new Error(`assertOrder failed: trace line needs "otherSelector" and a relation of ${import_utils.ORDER_RELATIONS.join("/")}, got ${JSON.stringify(raw.relation)}`);
|
|
203
|
+
const otherSelector = (0, import_recorderUtils.buildFullSelector)(actionInContext.frame.framePath, fields.otherSelector);
|
|
204
|
+
const box = async (s) => {
|
|
205
|
+
const handle = await mainFrame.waitForSelector(progress, s, false, { strict: true, state: "visible" });
|
|
206
|
+
try {
|
|
207
|
+
const rect = await handle?.boundingBox();
|
|
208
|
+
if (!rect)
|
|
209
|
+
throw new Error(`assertOrder: ${s} has no box on the page`);
|
|
210
|
+
return rect;
|
|
211
|
+
} finally {
|
|
212
|
+
handle?.dispose();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const [a, b] = [await box(selector), await box(otherSelector)];
|
|
216
|
+
const { ok, axis, edgeA, edgeB } = (0, import_utils.orderHolds)(fields.relation, a, b);
|
|
217
|
+
if (!ok)
|
|
218
|
+
throw new Error(`assertOrder failed: expected ${selector} ${fields.relation} ${otherSelector}, got ${axis} ${edgeA} vs ${edgeB}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
199
221
|
throw new Error("Internal error: unexpected action " + action.name);
|
|
200
222
|
}
|
|
201
223
|
function toClickOptions(action) {
|
package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/elementOrder.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var elementOrder_exports = {};
|
|
20
|
+
__export(elementOrder_exports, {
|
|
21
|
+
ORDER_RELATIONS: () => ORDER_RELATIONS,
|
|
22
|
+
isOrderRelation: () => isOrderRelation,
|
|
23
|
+
orderHolds: () => orderHolds,
|
|
24
|
+
parseOrderFields: () => parseOrderFields
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(elementOrder_exports);
|
|
27
|
+
const ORDER_RELATIONS = ["left-of", "right-of", "above", "below"];
|
|
28
|
+
function isOrderRelation(value) {
|
|
29
|
+
return typeof value === "string" && ORDER_RELATIONS.includes(value);
|
|
30
|
+
}
|
|
31
|
+
function orderHolds(relation, a, b) {
|
|
32
|
+
const axis = relation === "left-of" || relation === "right-of" ? "x" : "y";
|
|
33
|
+
const edgeA = a[axis];
|
|
34
|
+
const edgeB = b[axis];
|
|
35
|
+
const ok = relation === "left-of" || relation === "above" ? edgeA < edgeB : edgeA > edgeB;
|
|
36
|
+
return { ok, axis, edgeA, edgeB };
|
|
37
|
+
}
|
|
38
|
+
function parseOrderFields(raw) {
|
|
39
|
+
if (!raw || typeof raw !== "object")
|
|
40
|
+
return null;
|
|
41
|
+
const { selector, otherSelector, relation, locator, otherLocator } = raw;
|
|
42
|
+
if (typeof selector !== "string" || !selector || typeof otherSelector !== "string" || !otherSelector || !isOrderRelation(relation))
|
|
43
|
+
return null;
|
|
44
|
+
const fields = { selector, otherSelector, relation };
|
|
45
|
+
if (locator && typeof locator === "object")
|
|
46
|
+
fields.locator = locator;
|
|
47
|
+
if (otherLocator && typeof otherLocator === "object")
|
|
48
|
+
fields.otherLocator = otherLocator;
|
|
49
|
+
return fields;
|
|
50
|
+
}
|
|
51
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
52
|
+
0 && (module.exports = {
|
|
53
|
+
ORDER_RELATIONS,
|
|
54
|
+
isOrderRelation,
|
|
55
|
+
orderHolds,
|
|
56
|
+
parseOrderFields
|
|
57
|
+
});
|
|
@@ -25,6 +25,7 @@ module.exports = __toCommonJS(utils_exports);
|
|
|
25
25
|
__reExport(utils_exports, require("./utils/isomorphic/ariaSnapshot"), module.exports);
|
|
26
26
|
__reExport(utils_exports, require("./utils/isomorphic/assert"), module.exports);
|
|
27
27
|
__reExport(utils_exports, require("./utils/isomorphic/colors"), module.exports);
|
|
28
|
+
__reExport(utils_exports, require("./utils/isomorphic/elementOrder"), module.exports);
|
|
28
29
|
__reExport(utils_exports, require("./utils/isomorphic/headers"), module.exports);
|
|
29
30
|
__reExport(utils_exports, require("./utils/isomorphic/locatorGenerators"), module.exports);
|
|
30
31
|
__reExport(utils_exports, require("./utils/isomorphic/locatorParser"), module.exports);
|
|
@@ -72,6 +73,7 @@ var import_utilsBundle = require("./utilsBundle");
|
|
|
72
73
|
...require("./utils/isomorphic/ariaSnapshot"),
|
|
73
74
|
...require("./utils/isomorphic/assert"),
|
|
74
75
|
...require("./utils/isomorphic/colors"),
|
|
76
|
+
...require("./utils/isomorphic/elementOrder"),
|
|
75
77
|
...require("./utils/isomorphic/headers"),
|
|
76
78
|
...require("./utils/isomorphic/locatorGenerators"),
|
|
77
79
|
...require("./utils/isomorphic/locatorParser"),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{T as x,r,a as C,W as F,j as e,D as z,b as I,c as P,d as A,e as O,f as V}from"./assets/defaultSettingsView-DTp2-8Si.js";const $=()=>{const[o,c]=r.useState(!1),[n,l]=r.useState(),[h,u]=r.useState(),[p,b]=r.useState(N),[f,j]=r.useState({done:0,total:0}),[k,v]=r.useState(!1),[y,m]=r.useState(null),[T,R]=r.useState(null),[U,E]=r.useState(!1),g=r.useCallback(t=>{const s=new URL(window.location.href);if(!t.length)return;const i=t.item(0),a=URL.createObjectURL(i);s.searchParams.append("trace",a);const d=s.toString();window.history.pushState({},"",d),l(a),u(i.name),v(!1),m(null)},[]);r.useEffect(()=>{const t=async s=>{var a;if(!((a=s.clipboardData)!=null&&a.files.length))return;const i=["application/zip","application/x-zip-compressed"];for(const d of s.clipboardData.files)if(!i.includes(d.type))return;s.preventDefault(),g(s.clipboardData.files)};return document.addEventListener("paste",t),()=>document.removeEventListener("paste",t)}),r.useEffect(()=>{const t=s=>{const{method:i,params:a}=s.data;if(i!=="load"||!((a==null?void 0:a.trace)instanceof Blob))return;const d=new File([a.trace],"trace.zip",{type:"application/zip"}),w=new DataTransfer;w.items.add(d),g(w.files)};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)});const W=r.useCallback(t=>{t.preventDefault(),g(t.dataTransfer.files)},[g]),M=r.useCallback(t=>{t.preventDefault(),t.target.files&&g(t.target.files)},[g]);r.useEffect(()=>{const t=new URL(window.location.href).searchParams,s=t.get("trace");if(c(t.has("isServer")),s!=null&&s.startsWith("file:")){R(s||null);return}if(t.has("isServer")){const i=new URLSearchParams(window.location.search).get("ws"),a=new URL(`../${i}`,window.location.toString());a.protocol=window.location.protocol==="https:"?"wss:":"ws:";const d=new C(new F(a));d.onLoadTraceRequested(async w=>{l(w.traceUrl),v(!1),m(null)}),d.initialize({}).catch(()=>{})}else s&&!s.startsWith("blob:")&&l(s)},[]);const S=r.useCallback(async t=>{const s=new URLSearchParams;s.set("trace",t);const i=await fetch(`contexts?${s.toString()}`);if(!i.ok){const{error:w}=await i.json();return m(w),w}const a=await i.json(),d=new x(t,a);j({done:0,total:0}),m(null),b(d)},[]);r.useEffect(()=>{(async()=>{if(!n){b(N);return}const t=s=>{s.data.method==="progress"&&j(s.data.params)};try{navigator.serviceWorker.addEventListener("message",t),j({done:0,total:1});let s=await S(n);s!=null&&s.includes("please grant permission for Local Network Access")&&(await fetch(n,{method:"HEAD",headers:{"x-pw-serviceworker":"skip"}}),s=await S(n)),s&&(o||l(void 0))}finally{navigator.serviceWorker.removeEventListener("message",t)}})()},[o,n,h,S]);const D=f.done!==f.total&&f.total!==0&&!y;r.useEffect(()=>{if(D){const t=setTimeout(()=>{E(!0)},200);return()=>clearTimeout(t)}else E(!1)},[D]);const L=!!(!o&&!k&&!T&&(!n||y));return e.jsxs("div",{className:"vbox workbench-loader",onDragOver:t=>{t.preventDefault(),t.dataTransfer.types.includes("Files")&&v(!0)},children:[e.jsxs("div",{className:"hbox header",...L?{inert:!0}:{},children:[e.jsx("div",{className:"logo",children:e.jsx("img",{src:"playwright-logo.svg",alt:"Playwright logo"})}),e.jsx("div",{className:"product",children:"Playwright"}),p.title&&e.jsx("div",{className:"title",children:p.title}),e.jsx("div",{className:"spacer"}),e.jsx(z,{icon:"settings-gear",title:"Settings",dialogDataTestId:"settings-toolbar-dialog",children:e.jsx(I,{location:"trace-viewer"})})]}),e.jsx(P,{model:p,inert:L}),T&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{children:"Trace Viewer uses Service Workers to show traces. To view trace:"}),e.jsxs("div",{style:{paddingTop:20},children:[e.jsxs("div",{children:["1. Click ",e.jsx("a",{href:T,children:"here"})," to put your trace into the download shelf"]}),e.jsxs("div",{children:["2. Go to ",e.jsx("a",{href:"https://trace.playwright.dev",children:"trace.playwright.dev"})]}),e.jsx("div",{children:"3. Drop the trace from the download shelf into the page"})]})]}),e.jsx(A,{open:U,isModal:!0,className:"progress-dialog",children:e.jsxs("div",{className:"progress-content",children:[e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Loading Playwright Trace..."}),e.jsx("div",{className:"progress-wrapper",children:e.jsx("div",{className:"inner-progress",style:{width:f.total?100*f.done/f.total+"%":0}})})]})}),L&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{className:"processing-error",role:"alert",children:y}),e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Drop Playwright Trace to load"}),e.jsx("div",{children:"or"}),e.jsx("button",{onClick:()=>{const t=document.createElement("input");t.type="file",t.click(),t.addEventListener("change",s=>M(s))},type:"button",children:"Select file"}),e.jsx("div",{className:"info",children:"Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally."}),e.jsxs("div",{className:"version",children:["Playwright v","1.58.2-skyramp.8.14.
|
|
1
|
+
import{T as x,r,a as C,W as F,j as e,D as z,b as I,c as P,d as A,e as O,f as V}from"./assets/defaultSettingsView-DTp2-8Si.js";const $=()=>{const[o,c]=r.useState(!1),[n,l]=r.useState(),[h,u]=r.useState(),[p,b]=r.useState(N),[f,j]=r.useState({done:0,total:0}),[k,v]=r.useState(!1),[y,m]=r.useState(null),[T,R]=r.useState(null),[U,E]=r.useState(!1),g=r.useCallback(t=>{const s=new URL(window.location.href);if(!t.length)return;const i=t.item(0),a=URL.createObjectURL(i);s.searchParams.append("trace",a);const d=s.toString();window.history.pushState({},"",d),l(a),u(i.name),v(!1),m(null)},[]);r.useEffect(()=>{const t=async s=>{var a;if(!((a=s.clipboardData)!=null&&a.files.length))return;const i=["application/zip","application/x-zip-compressed"];for(const d of s.clipboardData.files)if(!i.includes(d.type))return;s.preventDefault(),g(s.clipboardData.files)};return document.addEventListener("paste",t),()=>document.removeEventListener("paste",t)}),r.useEffect(()=>{const t=s=>{const{method:i,params:a}=s.data;if(i!=="load"||!((a==null?void 0:a.trace)instanceof Blob))return;const d=new File([a.trace],"trace.zip",{type:"application/zip"}),w=new DataTransfer;w.items.add(d),g(w.files)};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)});const W=r.useCallback(t=>{t.preventDefault(),g(t.dataTransfer.files)},[g]),M=r.useCallback(t=>{t.preventDefault(),t.target.files&&g(t.target.files)},[g]);r.useEffect(()=>{const t=new URL(window.location.href).searchParams,s=t.get("trace");if(c(t.has("isServer")),s!=null&&s.startsWith("file:")){R(s||null);return}if(t.has("isServer")){const i=new URLSearchParams(window.location.search).get("ws"),a=new URL(`../${i}`,window.location.toString());a.protocol=window.location.protocol==="https:"?"wss:":"ws:";const d=new C(new F(a));d.onLoadTraceRequested(async w=>{l(w.traceUrl),v(!1),m(null)}),d.initialize({}).catch(()=>{})}else s&&!s.startsWith("blob:")&&l(s)},[]);const S=r.useCallback(async t=>{const s=new URLSearchParams;s.set("trace",t);const i=await fetch(`contexts?${s.toString()}`);if(!i.ok){const{error:w}=await i.json();return m(w),w}const a=await i.json(),d=new x(t,a);j({done:0,total:0}),m(null),b(d)},[]);r.useEffect(()=>{(async()=>{if(!n){b(N);return}const t=s=>{s.data.method==="progress"&&j(s.data.params)};try{navigator.serviceWorker.addEventListener("message",t),j({done:0,total:1});let s=await S(n);s!=null&&s.includes("please grant permission for Local Network Access")&&(await fetch(n,{method:"HEAD",headers:{"x-pw-serviceworker":"skip"}}),s=await S(n)),s&&(o||l(void 0))}finally{navigator.serviceWorker.removeEventListener("message",t)}})()},[o,n,h,S]);const D=f.done!==f.total&&f.total!==0&&!y;r.useEffect(()=>{if(D){const t=setTimeout(()=>{E(!0)},200);return()=>clearTimeout(t)}else E(!1)},[D]);const L=!!(!o&&!k&&!T&&(!n||y));return e.jsxs("div",{className:"vbox workbench-loader",onDragOver:t=>{t.preventDefault(),t.dataTransfer.types.includes("Files")&&v(!0)},children:[e.jsxs("div",{className:"hbox header",...L?{inert:!0}:{},children:[e.jsx("div",{className:"logo",children:e.jsx("img",{src:"playwright-logo.svg",alt:"Playwright logo"})}),e.jsx("div",{className:"product",children:"Playwright"}),p.title&&e.jsx("div",{className:"title",children:p.title}),e.jsx("div",{className:"spacer"}),e.jsx(z,{icon:"settings-gear",title:"Settings",dialogDataTestId:"settings-toolbar-dialog",children:e.jsx(I,{location:"trace-viewer"})})]}),e.jsx(P,{model:p,inert:L}),T&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{children:"Trace Viewer uses Service Workers to show traces. To view trace:"}),e.jsxs("div",{style:{paddingTop:20},children:[e.jsxs("div",{children:["1. Click ",e.jsx("a",{href:T,children:"here"})," to put your trace into the download shelf"]}),e.jsxs("div",{children:["2. Go to ",e.jsx("a",{href:"https://trace.playwright.dev",children:"trace.playwright.dev"})]}),e.jsx("div",{children:"3. Drop the trace from the download shelf into the page"})]})]}),e.jsx(A,{open:U,isModal:!0,className:"progress-dialog",children:e.jsxs("div",{className:"progress-content",children:[e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Loading Playwright Trace..."}),e.jsx("div",{className:"progress-wrapper",children:e.jsx("div",{className:"inner-progress",style:{width:f.total?100*f.done/f.total+"%":0}})})]})}),L&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{className:"processing-error",role:"alert",children:y}),e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Drop Playwright Trace to load"}),e.jsx("div",{children:"or"}),e.jsx("button",{onClick:()=>{const t=document.createElement("input");t.type="file",t.click(),t.addEventListener("change",s=>M(s))},type:"button",children:"Select file"}),e.jsx("div",{className:"info",children:"Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally."}),e.jsxs("div",{className:"version",children:["Playwright v","1.58.2-skyramp.8.14.14"]})]}),o&&!n&&e.jsx("div",{className:"drop-target",children:e.jsx("div",{className:"title",children:"Select test to see the trace"})}),k&&e.jsx("div",{className:"drop-target",onDragLeave:()=>{v(!1)},onDrop:t=>W(t),children:e.jsx("div",{className:"title",children:"Release to analyse the Playwright Trace"})})]})},N=new x("",[]),q=({traceJson:o})=>{const[c,n]=r.useState(void 0),[l,h]=r.useState(0),u=r.useRef(null);return r.useEffect(()=>(u.current&&clearTimeout(u.current),u.current=setTimeout(async()=>{try{const p=await B(o);n(p)}catch{const p=new x("",[]);n(p)}finally{h(l+1)}},500),()=>{u.current&&clearTimeout(u.current)}),[o,l]),e.jsx(P,{isLive:!0,model:c})};async function B(o){const c=new URLSearchParams;c.set("trace",o);const l=await(await fetch(`contexts?${c.toString()}`)).json();return new x(o,l)}(async()=>{const o=new URLSearchParams(window.location.search);if(O(),window.location.protocol!=="file:"){if(o.get("isUnderTest")==="true"&&await new Promise(h=>setTimeout(h,1e3)),!navigator.serviceWorker)throw new Error(`Service workers are not supported.
|
|
2
2
|
Make sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);navigator.serviceWorker.register("sw.bundle.js"),navigator.serviceWorker.controller||await new Promise(h=>{navigator.serviceWorker.oncontrollerchange=()=>h()}),setInterval(function(){fetch("ping")},1e4)}const c=o.get("trace"),l=(c==null?void 0:c.endsWith(".json"))?e.jsx(q,{traceJson:c}):e.jsx($,{});V.createRoot(document.querySelector("#root")).render(l)})();
|
package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
|
|
8
8
|
<link rel="manifest" href="./manifest.webmanifest">
|
|
9
9
|
<title>Playwright Trace Viewer</title>
|
|
10
|
-
<script type="module" crossorigin src="./index.
|
|
10
|
+
<script type="module" crossorigin src="./index.0MJW0uG7.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-DTp2-8Si.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./defaultSettingsView.7ch9cixO.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="./index.BVu7tZDe.css">
|
|
@@ -118,7 +118,7 @@ const _ACTION_NAMES = new Set<string>([
|
|
|
118
118
|
'navigate', 'openPage', 'press', 'select', 'uncheck',
|
|
119
119
|
'setInputFiles', 'assertText', 'assertValue', 'assertChecked',
|
|
120
120
|
'assertVisible', 'assertDisabled', 'assertCount', 'assertHidden', 'assertSnapshot', 'visualSnapshot', 'assertTableCell',
|
|
121
|
-
'assertCSS', 'assertClass', 'assertAttribute',
|
|
121
|
+
'assertCSS', 'assertClass', 'assertAttribute', 'assertOrder',
|
|
122
122
|
'dragTo', 'diagramNodeAdd', 'diagramLinkAdd', 'selectArea', 'tableSnapshot', 'domSnapshot', 'fileChooser',
|
|
123
123
|
'penTool', 'mouse.wheel', 'mouse.move', 'mouse.down', 'mouse.up',
|
|
124
124
|
'modalOpen', 'modalClose', 'waitForTimeout', 'evaluate',
|
package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { escapeRegExp, serializeExpectedTextValues } from '../../utils';
|
|
17
|
+
import { escapeRegExp, ORDER_RELATIONS, orderHolds, parseOrderFields, serializeExpectedTextValues } from '../../utils';
|
|
18
18
|
import { toKeyboardModifiers } from '../codegen/language';
|
|
19
19
|
import { buildFullSelector, mainFrameForAction } from './recorderUtils';
|
|
20
20
|
import { Progress, ProgressController } from '../progress';
|
|
@@ -240,6 +240,31 @@ async function performActionImpl(progress: Progress, mainFrame: Frame, actionInC
|
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
if (raw.name === 'assertOrder') {
|
|
244
|
+
// SKYR-4544: the order contract (utils/isomorphic/elementOrder) the MCP
|
|
245
|
+
// recorder and replay use too; both ends must be visible first.
|
|
246
|
+
const fields = parseOrderFields(raw);
|
|
247
|
+
if (!fields)
|
|
248
|
+
throw new Error(`assertOrder failed: trace line needs "otherSelector" and a relation of ${ORDER_RELATIONS.join('/')}, got ${JSON.stringify(raw.relation)}`);
|
|
249
|
+
const otherSelector = buildFullSelector(actionInContext.frame.framePath, fields.otherSelector);
|
|
250
|
+
const box = async (s: string) => {
|
|
251
|
+
const handle = await mainFrame.waitForSelector(progress, s, false, { strict: true, state: 'visible' });
|
|
252
|
+
try {
|
|
253
|
+
const rect = await handle?.boundingBox();
|
|
254
|
+
if (!rect)
|
|
255
|
+
throw new Error(`assertOrder: ${s} has no box on the page`);
|
|
256
|
+
return rect;
|
|
257
|
+
} finally {
|
|
258
|
+
handle?.dispose();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const [a, b] = [await box(selector), await box(otherSelector)];
|
|
262
|
+
const { ok, axis, edgeA, edgeB } = orderHolds(fields.relation, a, b);
|
|
263
|
+
if (!ok)
|
|
264
|
+
throw new Error(`assertOrder failed: expected ${selector} ${fields.relation} ${otherSelector}, got ${axis} ${edgeA} vs ${edgeB}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
243
268
|
throw new Error('Internal error: unexpected action ' + (action as any).name);
|
|
244
269
|
}
|
|
245
270
|
|
package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/elementOrder.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// SKYR-4544: the one definition of an order assertion ("A starts before B on an
|
|
18
|
+
// axis"). The MCP recorder, the MCP trace export and seeding, the MCP trace
|
|
19
|
+
// replay and the core recorder replay all use it, so record and replay cannot
|
|
20
|
+
// drift apart. (The generated test's comparison lives in the Go codegen, which
|
|
21
|
+
// cannot import this; its goldens and the live demoshop check pin it.)
|
|
22
|
+
|
|
23
|
+
/** Where the asserted element sits relative to the other one. */
|
|
24
|
+
export const ORDER_RELATIONS = ['left-of', 'right-of', 'above', 'below'] as const;
|
|
25
|
+
export type OrderRelation = typeof ORDER_RELATIONS[number];
|
|
26
|
+
|
|
27
|
+
export function isOrderRelation(value: unknown): value is OrderRelation {
|
|
28
|
+
return typeof value === 'string' && (ORDER_RELATIONS as readonly string[]).includes(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The part of a box the comparison reads: its origin. */
|
|
32
|
+
export type OrderBox = { x: number, y: number };
|
|
33
|
+
|
|
34
|
+
export type OrderResult = {
|
|
35
|
+
ok: boolean,
|
|
36
|
+
/** x for left-of/right-of, y for above/below. */
|
|
37
|
+
axis: 'x' | 'y',
|
|
38
|
+
edgeA: number,
|
|
39
|
+
edgeB: number,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether `a` is `relation` of `b`: each box's origin — the left edge (x) for
|
|
44
|
+
* left-of/right-of, the top edge (y) for above/below — compared strictly. It
|
|
45
|
+
* says where each element STARTS, not that one lies wholly beyond the other,
|
|
46
|
+
* and equal edges never hold.
|
|
47
|
+
*/
|
|
48
|
+
export function orderHolds(relation: OrderRelation, a: OrderBox, b: OrderBox): OrderResult {
|
|
49
|
+
const axis = relation === 'left-of' || relation === 'right-of' ? 'x' : 'y';
|
|
50
|
+
const edgeA = a[axis];
|
|
51
|
+
const edgeB = b[axis];
|
|
52
|
+
const ok = relation === 'left-of' || relation === 'above' ? edgeA < edgeB : edgeA > edgeB;
|
|
53
|
+
return { ok, axis, edgeA, edgeB };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type OrderFields = {
|
|
57
|
+
selector: string,
|
|
58
|
+
otherSelector: string,
|
|
59
|
+
relation: OrderRelation,
|
|
60
|
+
locator?: object,
|
|
61
|
+
otherLocator?: object,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The fields of an order assertion — a JSONL `assertOrder` line or a recorded
|
|
66
|
+
* browser_assert type "order" action's args — or null when it is malformed (no
|
|
67
|
+
* selector, no other end, or a relation outside ORDER_RELATIONS). Export, seeding
|
|
68
|
+
* and both replays parse through this, so they accept and drop exactly the same
|
|
69
|
+
* lines.
|
|
70
|
+
*/
|
|
71
|
+
export function parseOrderFields(raw: any): OrderFields | null {
|
|
72
|
+
if (!raw || typeof raw !== 'object')
|
|
73
|
+
return null;
|
|
74
|
+
const { selector, otherSelector, relation, locator, otherLocator } = raw;
|
|
75
|
+
if (typeof selector !== 'string' || !selector || typeof otherSelector !== 'string' || !otherSelector || !isOrderRelation(relation))
|
|
76
|
+
return null;
|
|
77
|
+
const fields: OrderFields = { selector, otherSelector, relation };
|
|
78
|
+
if (locator && typeof locator === 'object')
|
|
79
|
+
fields.locator = locator;
|
|
80
|
+
if (otherLocator && typeof otherLocator === 'object')
|
|
81
|
+
fields.otherLocator = otherLocator;
|
|
82
|
+
return fields;
|
|
83
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
export * from './utils/isomorphic/ariaSnapshot';
|
|
18
18
|
export * from './utils/isomorphic/assert';
|
|
19
19
|
export * from './utils/isomorphic/colors';
|
|
20
|
+
export * from './utils/isomorphic/elementOrder';
|
|
20
21
|
export * from './utils/isomorphic/headers';
|
|
21
22
|
export * from './utils/isomorphic/locatorGenerators';
|
|
22
23
|
export * from './utils/isomorphic/locatorParser';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4-rc.1",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./build/index.js",
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
"fast-glob": "^3.3.3",
|
|
77
77
|
"js-yaml": "^4.1.1",
|
|
78
78
|
"picomatch": "^4.0.5",
|
|
79
|
-
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.14.
|
|
79
|
+
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.14.14.tgz",
|
|
80
|
+
"semver": "^7.8.5",
|
|
80
81
|
"simple-git": "^3.30.0",
|
|
81
82
|
"smol-toml": "^1.8.0",
|
|
82
83
|
"ts-is-record": "^3.0.5",
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
"@types/mocha": "^10.0.10",
|
|
91
92
|
"@types/node": "^22.15.19",
|
|
92
93
|
"@types/picomatch": "^4.0.3",
|
|
94
|
+
"@types/semver": "^7.8.0",
|
|
93
95
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
94
96
|
"@typescript-eslint/parser": "^8.0.0",
|
|
95
97
|
"eslint": "^9.0.0",
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
## Terms
|
|
5
5
|
|
|
6
|
-
You decide what to test.
|
|
6
|
+
You decide what to test.
|
|
7
7
|
|
|
8
8
|
A **change** is one thing the pull request should make happen, or reject, for a user or a caller. Say on each change whether it is on the API, on a page, or both.
|
|
9
9
|
|
|
10
|
-
A **behaviour** is one rule about one endpoint or one page. One change can carry two behaviours, one on the API and one on the page. A test may prove several behaviours. Every behaviour has at least one test that proves it. Rejections the application already handles share one test, one test case each
|
|
10
|
+
A **behaviour** is one rule about one endpoint or one page. One change can carry two behaviours, one on the API and one on the page. A test may prove several behaviours. Every behaviour has at least one test that proves it. Rejections the application already handles share one test, one test case each.
|
|
11
11
|
|
|
12
12
|
## The procedure
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ Do the steps in this order.
|
|
|
18
18
|
3. **Write the changes.** List every change, one line each, with a short id and where you read it. Write the list in your reply before you go on.
|
|
19
19
|
Keep each line through the rest of the procedure. Do not fold a change into another one or drop it because no test covers it.
|
|
20
20
|
For each edit in the diff, write one line: what it makes different for a user or a caller.
|
|
21
|
-
For each requirement the title, the description or a requirements file states, write one line, even when no edit in the diff serves it.
|
|
21
|
+
For each requirement the title, the description or a requirements file states, write one line, even when no edit in the diff serves it.
|
|
22
22
|
4. **Declare the test cases each request must refuse.**
|
|
23
23
|
- For each input a request takes, declare one test case with a value the request must reject. Declare it even when the code does not reject that value today.
|
|
24
24
|
- For each list a request takes, declare one test case that is valid except for one item in the list. Give the test case one `param` and one `value`: the invalid item. The step that sends the test case carries the rest of the list.
|
|
@@ -27,33 +27,25 @@ Do the steps in this order.
|
|
|
27
27
|
- For each state a record can be in that the request must refuse, declare one test case that puts the record in that state first and then sends the request.
|
|
28
28
|
- A request that names a record by id, in the path, in a query parameter, or in the body, must reject a record that does not exist or does not belong to the parent.
|
|
29
29
|
- A record that belongs to another user is its own test case, even when the answer is the same.
|
|
30
|
-
- For each rule the pull request states, declare the test cases that catch a wrong implementation: each bound, one value past each bound, one value far past a bound, and each value the rule forbids. Take their values from the rule, not from an example.
|
|
30
|
+
- For each rule the pull request states, declare the test cases that catch a wrong implementation: each bound, one value past each bound, one value far past a bound, and each value the rule forbids. Take their values from the rule, not from an example. If an input accepts only values from a list, declare an accept test case for every value in it, even when the values share code or make the test long.
|
|
31
31
|
- A reject test case expects the request to be refused and the record to be unchanged. Declare an exact status code or error text only when the pull request or a requirements file states it.
|
|
32
32
|
5. **Declare what each response value must be.**
|
|
33
33
|
- For each value the response carries that this change decides, declare what that value must be and where you read it: the pull request title or description, a requirements file the description names, or a rule the application already follows in code this pull request did not change.
|
|
34
34
|
- Quote a value only when it holds for every input the test case can send: a fixed constant, a bound, or one of the allowed names. A value the inputs decide is `derived`, even when the source shows it in an example.
|
|
35
|
-
- If no source states the value, declare none and assert its shape instead.
|
|
36
35
|
- Never take the value from the running application: that is the one source a defect cannot contradict.
|
|
37
36
|
6. **Find where each change is served.** Read the code that declares routes and pages, and write each route's full path with its mount prefix. Follow the callers of a changed file that has no route of its own to the endpoint or page that runs it. Read the code that writes each stored value the new code reads; any rule on that value is a rejection to list.
|
|
38
|
-
7. **Review the code that serves each change.** Read the handler and the functions it calls to read or write data. For a changed screen, read the component and what it calls. Read these files even when the diff does not contain them; a defect often sits in the code the change depends on. Write each defect as one line with an id, the file, the line, and what is wrong. Write the list in your reply before you go on. An empty list is a statement: you read the code and found nothing.
|
|
39
|
-
- A computed field that is not recalculated after a mutation, such as `total_amount` unchanged after items are added or removed
|
|
40
|
-
- Incomplete CRUD: a create with no cleanup, an update that adds new records and does not remove the old ones
|
|
41
|
-
- Missing input validation on a new endpoint
|
|
42
|
-
- A frontend rendering error visible in the code, such as an invalid prop, a missing required attribute, or a value shown without the formatting its neighbours use
|
|
43
|
-
- Incorrect arithmetic in business logic, such as a discount calculation or a price aggregation
|
|
37
|
+
7. **Review the code that serves each change.** Read the handler and the functions it calls to read or write data. For a changed screen, read the component and what it calls. Read these files even when the diff does not contain them; a defect often sits in the code the change depends on. Write each defect as one line with an id, the file, the line, and what is wrong. Write the list in your reply before you go on. An empty list is a statement: you read the code and found nothing.
|
|
44
38
|
8. **Read the existing tests that reach a change.** The analysis lists the repository's test files. Read the ones that import a changed file, call a changed route, or open a changed page.
|
|
45
39
|
9. **Group the changes into behaviours and decide each.** A planned test names every change it covers. A change gets no test only when a test already in the repository proves it; the size of the edit or the kind of file is never a reason. A change no behaviour needs gets one sentence that says why.
|
|
46
|
-
When the test that proves a change is one you are editing, declare that test in `maintains` and name the changes it covers. Those changes are then covered and get no planned test of their own
|
|
47
|
-
A requirement the title, the description or a named requirements file states and the code does not meet is a defect; its file is the code that contradicts it. A defect gets a planned test that names it in `defects` and expects to fail.
|
|
40
|
+
When the test that proves a change is one you are editing, or one you will run unchanged as a VERIFY, declare that test in `maintains` and name the changes it covers. Those changes are then covered and get no planned test of their own.
|
|
41
|
+
A requirement the title, the description or a named requirements file states and the code does not meet is a defect; its file is the code that contradicts it. A defect gets a planned test that names it in `defects` and expects to fail. If a test you ran unchanged as a VERIFY failed on the defect, name the defect in that test's `maintains` entry instead; the defect then needs no planned test. A defect gets no test only when something stopped the test: say what.
|
|
48
42
|
If a state comes from time passing, create the record with the nearest allowed deadline and wait for it.
|
|
49
43
|
10. **Walk each page behaviour once, as its test will run.** Start at login, do the steps of the behaviour, run `browser_blueprint` on each page you use, and export the trace when the flow ends. Name only elements from those captures. Do not try other values or read messages; the recording is the test.
|
|
50
|
-
11. **Declare each planned test.**
|
|
44
|
+
11. **Declare each planned test.**
|
|
51
45
|
- A test that expects a rejection sends an accepted request first. That request is the test's setup.
|
|
52
|
-
- Two planned tests on one endpoint each say how they differ.
|
|
53
46
|
- A test that expects to fail proves one test case: its first assertion after setup is the one the defect breaks, and a red step stops every step after it. Declare one such test per test case the defect breaks, so each test case reports on its own.
|
|
54
47
|
- Accepted test cases may share one test that expects to pass.
|
|
55
|
-
|
|
56
|
-
12. **Register the whole plan in one call to `skyramp_register_test_plan`.** The call carries `changes`, `defects` and `plannedTests`.
|
|
48
|
+
12. **Register the whole plan in one call to `skyramp_register_test_plan`.**
|
|
57
49
|
13. **Answer each objection, or change the plan and register it again.**
|
|
58
50
|
An answer that says a state cannot be produced says what you tried.
|
|
59
51
|
14. **Stop when every objection has an answer.** Disagreeing in the answer closes it too.
|
|
@@ -26,7 +26,7 @@ Apply only to new test functions you are adding and existing test functions affe
|
|
|
26
26
|
|
|
27
27
|
Before editing `$2`, you must output a `<thinking>` block. It states the behaviour each test evaluates, then works through the rules for the responses that behaviour depends on, so no rule that applies is overlooked. The JSON array must match the `rule_checklist` template that accompanies the `$0` rules. Use the template's keys verbatim; each key is the slug shown in that rule's heading. Every key must appear — `[]` where the rule does not apply.
|
|
28
28
|
|
|
29
|
-
1. For each test function in scope, name the behaviour it evaluates in one line, taken from the recommendation it came from.
|
|
29
|
+
1. For each test function in scope, name the behaviour it evaluates in one line, taken from the recommendation it came from. A generated test function may carry a name that states its behaviour — trust that name when it does. A function still named `test_integration` or `testUi` claims nothing, so use the name only when it is specific enough to add something, and never treat a generic one as evidence the test has no single behaviour. Then scan `$2` and the expected responses for the code change tested.
|
|
30
30
|
2. Classify each response first by its response status type and then assign the applicable assertion rules to the response.
|
|
31
31
|
- Success with body (2xx with a response body): all assertion rules below may apply — echo-back of request fields, computed response fields, array / items validation, and chained values across steps.
|
|
32
32
|
- Success with no body (200/202/204 with an empty body — e.g. logout/cancel/submit action endpoints): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.
|