@nyssance/tauri-hasgard 0.2.1 → 0.3.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/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export { createHasgardExpect } from "./expect.js";
3
3
  export { createHasgardTest } from "./fixture.js";
4
4
  export { HasgardProcess } from "./process-manager.js";
5
5
  export { HasgardRpcClient, HasgardRpcError } from "./rpc-client.js";
6
- export type { BoundingBox, ConsoleLogEntry, ConsoleLogOptions, DialogListing, DialogPolicy, DialogRecord, DialogType, DiffChange, DiffOptions, DragOffset, DropFile, FileInput, FilterOptions, FormEntry, FormField, FormsDump, HasgardFixtures, HasgardLaunchConfig, HasgardTarget, HasgardTestConfig, HasgardWorkerFixtures, JsonPrimitive, JsonValue, NativeScreenshot, NativeScreenshotOptions, NetworkRequestEntry, NetworkRequestOptions, QueryDimension, RecorderEntry, RecorderResult, RecorderStatus, RoleQuery, ScrollOptions, Snapshot, SnapshotDiff, SnapshotElement, SnapshotOptions, StorageEntry, StorageListing, StorageOptions, TextQuery, WaitForFunctionOptions, WaitOptions, WatchChanges, WatchNode, WatchOptions, WindowInfo, WindowState } from "./types.js";
7
- export { byPoint, byRef, bySelector, HasgardApplication, HasgardLocator, HasgardWindow } from "./window.js";
6
+ export type { BoundingBox, ClickOptions, ConsoleLogEntry, ConsoleLogOptions, DialogListing, DialogPolicy, DialogRecord, DialogType, DiffChange, DiffOptions, DragOffset, DropFile, ElementPosition, FileInput, FilterOptions, FormEntry, FormField, FormsDump, FulfillOptions, HasgardFixtures, HasgardLaunchConfig, HasgardTarget, HasgardTestConfig, HasgardWorkerFixtures, InterceptedRequest, JsonPrimitive, JsonValue, KeyModifier, MouseButton, NativeScreenshot, NativeScreenshotOptions, NetworkRequestEntry, NetworkRequestOptions, QueryDimension, RecorderEntry, RecorderResult, RecorderStatus, RoleQuery, RouteListing, RouteOptions, RouteRule, ScrollOptions, Snapshot, SnapshotDiff, SnapshotElement, SnapshotOptions, StorageEntry, StorageListing, StorageOptions, TextQuery, WaitForFunctionOptions, WaitOptions, WatchChanges, WatchNode, WatchOptions, WindowInfo, WindowState } from "./types.js";
7
+ export { byPoint, byRef, bySelector, HasgardApplication, HasgardFrameLocator, HasgardLocator, HasgardWindow } from "./window.js";
package/dist/index.js CHANGED
@@ -309,6 +309,58 @@ function targetParams(target) {
309
309
  }
310
310
  return { x: target.x, y: target.y };
311
311
  }
312
+ function clickParams(options) {
313
+ const params = {};
314
+ if (options.modifiers !== void 0) params.modifiers = options.modifiers;
315
+ if (options.button !== void 0) params.button = options.button;
316
+ if (options.clickCount !== void 0) params.clickCount = options.clickCount;
317
+ if (options.position !== void 0) params.position = { ...options.position };
318
+ return params;
319
+ }
320
+ function routeParams(options) {
321
+ const params = { pattern: options.pattern };
322
+ if (options.method !== void 0) params.method = options.method;
323
+ if (options.times !== void 0) params.times = options.times;
324
+ if ("status" in options && options.status !== void 0) params.status = options.status;
325
+ if ("body" in options && options.body !== void 0) params.body = options.body;
326
+ if ("contentType" in options && options.contentType !== void 0) {
327
+ params.contentType = options.contentType;
328
+ }
329
+ return params;
330
+ }
331
+ function parseRouteAction(value, context) {
332
+ const action = expectString(value, context);
333
+ if (action !== "fulfill" && action !== "abort") {
334
+ throw new Error(`${context} must be "fulfill" or "abort", got ${action}`);
335
+ }
336
+ return action;
337
+ }
338
+ function parseRouteRule(value, index) {
339
+ const context = `route.list.routes[${index}]`;
340
+ const rule = expectRecord(value, context);
341
+ return {
342
+ id: expectNumber(rule.id, `${context}.id`),
343
+ pattern: expectString(rule.pattern, `${context}.pattern`),
344
+ method: expectNullableString(rule.method, `${context}.method`),
345
+ action: parseRouteAction(rule.action, `${context}.action`),
346
+ status: expectNumber(rule.status, `${context}.status`),
347
+ times: rule.times === null ? null : expectNumber(rule.times, `${context}.times`),
348
+ used: expectNumber(rule.used, `${context}.used`)
349
+ };
350
+ }
351
+ function parseInterceptedRequest(value, index) {
352
+ const context = `route.list.intercepted[${index}]`;
353
+ const entry = expectRecord(value, context);
354
+ return {
355
+ id: expectNumber(entry.id, `${context}.id`),
356
+ timestamp: expectNumber(entry.timestamp, `${context}.timestamp`),
357
+ route_id: expectNumber(entry.route_id, `${context}.route_id`),
358
+ method: expectString(entry.method, `${context}.method`),
359
+ url: expectString(entry.url, `${context}.url`),
360
+ action: parseRouteAction(entry.action, `${context}.action`),
361
+ status: expectNumber(entry.status, `${context}.status`)
362
+ };
363
+ }
312
364
  function absoluteIndex(index, total) {
313
365
  return index < 0 ? total + index : index;
314
366
  }
@@ -550,6 +602,17 @@ var HasgardWindow = class {
550
602
  getByTestId(testId) {
551
603
  return new HasgardLocator(this, { kind: "query", by: "testid", value: testId, query: {} });
552
604
  }
605
+ /**
606
+ * Scope locators to a same-origin `<iframe>`.
607
+ *
608
+ * Cross-origin frames are out of reach: the same-origin policy binds injected
609
+ * script exactly as it binds the page's own, so no bridge can see into a
610
+ * third-party payment or OAuth frame. Those raise an error naming the origin
611
+ * problem rather than reporting a missing element.
612
+ */
613
+ frameLocator(selector) {
614
+ return new HasgardFrameLocator(this, [selector]);
615
+ }
553
616
  async snapshot(options = {}) {
554
617
  const raw = expectRecord(
555
618
  await this.rpc.call("snapshot", withWindow(this.label, options)),
@@ -787,6 +850,53 @@ var HasgardWindow = class {
787
850
  await this.call("dialog.clear");
788
851
  }
789
852
  };
853
+ /**
854
+ * Shape what the page's own requests receive, without a real backend.
855
+ *
856
+ * Rules are declarative rather than Playwright's per-request callback: the
857
+ * bridge only ever answers requests, so it cannot call back into this process
858
+ * and await a handler while the page sits inside `fetch`. The same constraint
859
+ * shaped `dialogs`.
860
+ *
861
+ * Rules are checked in registration order and the first match wins, so
862
+ * register the narrow ones before a catch-all. Both `fetch` and
863
+ * `XMLHttpRequest` are covered; resources the webview loads itself -- the
864
+ * document, `<img src>`, stylesheets -- are not, since no page script sees
865
+ * them.
866
+ */
867
+ routes = {
868
+ /** Answer matching requests with this response instead of the network. */
869
+ fulfill: async (options) => {
870
+ const value = expectRecord(
871
+ await this.call("route.add", { ...routeParams(options), action: "fulfill" }),
872
+ "route.add result"
873
+ );
874
+ return { id: expectNumber(value.id, "route.add.id") };
875
+ },
876
+ /** Fail matching requests the way a dropped connection does. */
877
+ abort: async (options) => {
878
+ const value = expectRecord(
879
+ await this.call("route.add", { ...routeParams(options), action: "abort" }),
880
+ "route.add result"
881
+ );
882
+ return { id: expectNumber(value.id, "route.add.id") };
883
+ },
884
+ /** The active rules, their use counts, and every request they answered. */
885
+ list: async () => {
886
+ const value = expectRecord(await this.call("route.list"), "route.list result");
887
+ return {
888
+ routes: expectArray(value.routes, "route.list.routes").map((entry, index) => parseRouteRule(entry, index)),
889
+ intercepted: expectArray(value.intercepted, "route.list.intercepted").map(
890
+ (entry, index) => parseInterceptedRequest(entry, index)
891
+ )
892
+ };
893
+ },
894
+ /** Remove every rule and its interception log. */
895
+ clear: async () => {
896
+ const value = expectRecord(await this.call("route.clear"), "route.clear result");
897
+ return { removed: expectNumber(value.removed, "route.clear.removed") };
898
+ }
899
+ };
790
900
  /** This window's operating-system window id, where the platform exposes one. */
791
901
  async nativeId() {
792
902
  const listing = await new HasgardApplication(this.rpc).windows();
@@ -816,6 +926,49 @@ var HasgardWindow = class {
816
926
  }
817
927
  }
818
928
  };
929
+ var HasgardFrameLocator = class _HasgardFrameLocator {
930
+ windowRef;
931
+ frame;
932
+ constructor(window, frame) {
933
+ this.windowRef = window;
934
+ this.frame = frame;
935
+ }
936
+ /** Descend into a frame nested inside this one. */
937
+ frameLocator(selector) {
938
+ return new _HasgardFrameLocator(this.windowRef, [...this.frame, selector]);
939
+ }
940
+ locator(selector) {
941
+ return new HasgardLocator(this.windowRef, { kind: "selector", selector, frame: [...this.frame] });
942
+ }
943
+ getByRole(role, query = {}) {
944
+ return new HasgardLocator(this.windowRef, { kind: "role", role, query, frame: [...this.frame] });
945
+ }
946
+ getByText(text, query = {}) {
947
+ return this.byDimension("text", text, query);
948
+ }
949
+ getByLabel(text, query = {}) {
950
+ return this.byDimension("label", text, query);
951
+ }
952
+ getByPlaceholder(text, query = {}) {
953
+ return this.byDimension("placeholder", text, query);
954
+ }
955
+ getByAltText(text, query = {}) {
956
+ return this.byDimension("alt", text, query);
957
+ }
958
+ getByTitle(text, query = {}) {
959
+ return this.byDimension("title", text, query);
960
+ }
961
+ getByTestId(testId) {
962
+ return this.byDimension("testid", testId, {});
963
+ }
964
+ /** Snapshot the frame's own document rather than the top one. */
965
+ async snapshot(options = {}) {
966
+ return this.windowRef.snapshot({ ...options, frame: [...this.frame] });
967
+ }
968
+ byDimension(by, value, query) {
969
+ return new HasgardLocator(this.windowRef, { kind: "query", by, value, query, frame: [...this.frame] });
970
+ }
971
+ };
819
972
  var HasgardLocator = class _HasgardLocator {
820
973
  window;
821
974
  query;
@@ -823,14 +976,22 @@ var HasgardLocator = class _HasgardLocator {
823
976
  this.window = window;
824
977
  this.query = query;
825
978
  }
826
- async click() {
827
- await this.withTarget((target) => this.window.call("click", targetParams(target)));
979
+ /**
980
+ * Click this element.
981
+ *
982
+ * A right or middle click raises `contextmenu`/`auxclick` and no `click`, and
983
+ * `clickCount: 2` escalates `detail` across the presses before a single
984
+ * `dblclick` -- both matching what the platform does, so a listener bound to
985
+ * the wrong event fails here rather than in front of a user.
986
+ */
987
+ async click(options = {}) {
988
+ await this.withTarget((target) => this.window.call("click", { ...this.scoped(target), ...clickParams(options) }));
828
989
  }
829
990
  async fill(value) {
830
- await this.withTarget((target) => this.window.call("fill", { ...targetParams(target), value }));
991
+ await this.withTarget((target) => this.window.call("fill", { ...this.scoped(target), value }));
831
992
  }
832
993
  async type(text) {
833
- await this.withTarget((target) => this.window.call("type", { ...targetParams(target), text }));
994
+ await this.withTarget((target) => this.window.call("type", { ...this.scoped(target), text }));
834
995
  }
835
996
  /** Empty the control, firing `input` and `change` as `fill` does. */
836
997
  async clear() {
@@ -847,7 +1008,7 @@ var HasgardLocator = class _HasgardLocator {
847
1008
  const payloads = await readFileInputs(files);
848
1009
  await this.withTarget(
849
1010
  (target) => this.window.call("setInputFiles", {
850
- ...targetParams(target),
1011
+ ...this.scoped(target),
851
1012
  files: payloads
852
1013
  })
853
1014
  );
@@ -862,48 +1023,49 @@ var HasgardLocator = class _HasgardLocator {
862
1023
  async wheel(deltaX, deltaY) {
863
1024
  return this.withTarget(async (target) => {
864
1025
  const value = expectRecord(
865
- await this.window.call("wheel", { ...targetParams(target), deltaX, deltaY }),
1026
+ await this.window.call("wheel", { ...this.scoped(target), deltaX, deltaY }),
866
1027
  "wheel result"
867
1028
  );
868
1029
  return expectBoolean(value.defaultPrevented, "wheel.defaultPrevented");
869
1030
  });
870
1031
  }
871
1032
  async selectOption(value) {
872
- await this.withTarget((target) => this.window.call("select", { ...targetParams(target), value }));
1033
+ await this.withTarget((target) => this.window.call("select", { ...this.scoped(target), value }));
873
1034
  }
874
1035
  /** Drive the checkbox to checked. A no-op when it is already checked. */
875
1036
  async check() {
876
- await this.withTarget((target) => this.window.call("check", { ...targetParams(target), checked: true }));
1037
+ await this.withTarget((target) => this.window.call("check", { ...this.scoped(target), checked: true }));
877
1038
  }
878
1039
  /** Drive the checkbox to unchecked. A no-op when it is already unchecked. */
879
1040
  async uncheck() {
880
- await this.withTarget((target) => this.window.call("check", { ...targetParams(target), checked: false }));
1041
+ await this.withTarget((target) => this.window.call("check", { ...this.scoped(target), checked: false }));
881
1042
  }
882
1043
  /** Flip the checkbox, whatever its current state. */
883
1044
  async toggle() {
884
- await this.withTarget((target) => this.window.call("check", targetParams(target)));
1045
+ await this.withTarget((target) => this.window.call("check", this.scoped(target)));
885
1046
  }
886
1047
  async hover() {
887
- await this.withTarget((target) => this.window.call("hover", targetParams(target)));
1048
+ await this.withTarget((target) => this.window.call("hover", this.scoped(target)));
888
1049
  }
889
- async dblclick() {
890
- await this.withTarget((target) => this.window.call("dblclick", targetParams(target)));
1050
+ /** Shorthand for `click({ clickCount: 2 })`; other options still apply. */
1051
+ async dblclick(options = {}) {
1052
+ await this.click({ ...options, clickCount: 2 });
891
1053
  }
892
1054
  async focus() {
893
- await this.withTarget((target) => this.window.call("focus", targetParams(target)));
1055
+ await this.withTarget((target) => this.window.call("focus", this.scoped(target)));
894
1056
  }
895
1057
  async blur() {
896
- await this.withTarget((target) => this.window.call("blur", targetParams(target)));
1058
+ await this.withTarget((target) => this.window.call("blur", this.scoped(target)));
897
1059
  }
898
1060
  /** Scroll this element's own scrollport. */
899
1061
  async scrollBy(options) {
900
1062
  await this.withTarget(
901
- (target) => this.window.call("scroll", { ...targetParams(target), ...options })
1063
+ (target) => this.window.call("scroll", { ...this.scoped(target), ...options })
902
1064
  );
903
1065
  }
904
1066
  async boundingBox() {
905
1067
  return this.withTarget(async (target) => {
906
- const value = expectRecord(await this.window.call("boundingBox", targetParams(target)), "boundingBox result");
1068
+ const value = expectRecord(await this.window.call("boundingBox", this.scoped(target)), "boundingBox result");
907
1069
  return {
908
1070
  x: expectNumber(value.x, "boundingBox.x"),
909
1071
  y: expectNumber(value.y, "boundingBox.y"),
@@ -914,7 +1076,7 @@ var HasgardLocator = class _HasgardLocator {
914
1076
  }
915
1077
  async isDisabled() {
916
1078
  return this.withTarget(async (target) => {
917
- const value = expectRecord(await this.window.call("disabled", targetParams(target)), "disabled result");
1079
+ const value = expectRecord(await this.window.call("disabled", this.scoped(target)), "disabled result");
918
1080
  return expectBoolean(value.disabled, "disabled.disabled");
919
1081
  });
920
1082
  }
@@ -923,17 +1085,17 @@ var HasgardLocator = class _HasgardLocator {
923
1085
  }
924
1086
  async textContent() {
925
1087
  return this.withTarget(
926
- async (target) => expectString(await this.window.call("text", targetParams(target)), "text result")
1088
+ async (target) => expectString(await this.window.call("text", this.scoped(target)), "text result")
927
1089
  );
928
1090
  }
929
1091
  async inputValue() {
930
1092
  return this.withTarget(
931
- async (target) => expectString(await this.window.call("value", targetParams(target)), "value result")
1093
+ async (target) => expectString(await this.window.call("value", this.scoped(target)), "value result")
932
1094
  );
933
1095
  }
934
1096
  async innerHTML() {
935
1097
  return this.withTarget(
936
- async (target) => expectString(await this.window.call("html", targetParams(target)), "html result")
1098
+ async (target) => expectString(await this.window.call("html", this.scoped(target)), "html result")
937
1099
  );
938
1100
  }
939
1101
  async getAttribute(name) {
@@ -957,21 +1119,21 @@ var HasgardLocator = class _HasgardLocator {
957
1119
  /** Drag this element by a pixel offset from its centre. */
958
1120
  async dragBy(offset) {
959
1121
  await this.withTarget(
960
- (target) => this.window.call("drag", { source: targetParams(target), offset: { x: offset.x, y: offset.y } })
1122
+ (target) => this.window.call("drag", { source: this.scoped(target), offset: { x: offset.x, y: offset.y } })
961
1123
  );
962
1124
  }
963
1125
  /** Dispatch a file drop onto this element. `data` is base64-encoded. */
964
1126
  async dropFiles(files) {
965
1127
  await this.withTarget(
966
1128
  (target) => this.window.call("drop", {
967
- ...targetParams(target),
1129
+ ...this.scoped(target),
968
1130
  files
969
1131
  })
970
1132
  );
971
1133
  }
972
1134
  async attributes() {
973
1135
  return this.withTarget(async (target) => {
974
- const value = expectRecord(await this.window.call("attrs", targetParams(target)), "attrs result");
1136
+ const value = expectRecord(await this.window.call("attrs", this.scoped(target)), "attrs result");
975
1137
  return Object.fromEntries(
976
1138
  Object.entries(value).map(([name, attr]) => [name, expectString(attr, `attribute ${name}`)])
977
1139
  );
@@ -979,19 +1141,22 @@ var HasgardLocator = class _HasgardLocator {
979
1141
  }
980
1142
  async isVisible() {
981
1143
  return this.withTarget(async (target) => {
982
- const value = expectRecord(await this.window.call("visible", targetParams(target)), "visible result");
1144
+ const value = expectRecord(await this.window.call("visible", this.scoped(target)), "visible result");
983
1145
  return expectBoolean(value.visible, "visible.visible");
984
1146
  });
985
1147
  }
986
1148
  async isChecked() {
987
1149
  return this.withTarget(async (target) => {
988
- const value = expectRecord(await this.window.call("checked", targetParams(target)), "checked result");
1150
+ const value = expectRecord(await this.window.call("checked", this.scoped(target)), "checked result");
989
1151
  return expectBoolean(value.checked, "checked.checked");
990
1152
  });
991
1153
  }
992
1154
  async count() {
993
1155
  const total = this.query.kind === "selector" && !this.query.filters?.length ? expectNumber(
994
- expectRecord(await this.window.call("count", { selector: this.query.selector }), "count result").count,
1156
+ expectRecord(
1157
+ await this.window.call("count", { selector: this.query.selector, ...this.frameParams() }),
1158
+ "count result"
1159
+ ).count,
995
1160
  "count.count"
996
1161
  ) : (await this.resolveAll()).length;
997
1162
  if (this.query.index === void 0) return total;
@@ -1003,7 +1168,8 @@ var HasgardLocator = class _HasgardLocator {
1003
1168
  await this.window.call("wait", {
1004
1169
  selector: this.query.selector,
1005
1170
  gone: options.state === "detached",
1006
- timeout: options.timeoutMs
1171
+ timeout: options.timeoutMs,
1172
+ ...this.frameParams()
1007
1173
  });
1008
1174
  return;
1009
1175
  }
@@ -1063,6 +1229,19 @@ var HasgardLocator = class _HasgardLocator {
1063
1229
  }
1064
1230
  return byRef(elements[0].ref);
1065
1231
  }
1232
+ /**
1233
+ * Serialize a target together with the frame chain this locator lives in.
1234
+ *
1235
+ * A ref carries its own document, so the bridge ignores `frame` for one; the
1236
+ * chain still travels so that a selector or coordinate target -- which the
1237
+ * bridge must resolve itself -- lands in the right document.
1238
+ */
1239
+ scoped(target) {
1240
+ return { ...targetParams(target), ...this.frameParams() };
1241
+ }
1242
+ frameParams() {
1243
+ return this.query.frame ? { frame: [...this.query.frame] } : {};
1244
+ }
1066
1245
  async withTarget(operation) {
1067
1246
  return this.window.runExclusive(async () => operation(await this.resolveUnique()));
1068
1247
  }
@@ -1079,7 +1258,7 @@ var HasgardLocator = class _HasgardLocator {
1079
1258
  return this.runQuery(params);
1080
1259
  }
1081
1260
  const query = this.query;
1082
- const snapshot = await this.window.snapshot();
1261
+ const snapshot = await this.window.snapshot(query.frame ? { frame: [...query.frame] } : {});
1083
1262
  return snapshot.elements.filter((element) => {
1084
1263
  if (element.role !== query.role) return false;
1085
1264
  const expected = query.query.name;
@@ -1110,7 +1289,7 @@ var HasgardLocator = class _HasgardLocator {
1110
1289
  return current;
1111
1290
  }
1112
1291
  async runQuery(params) {
1113
- return this.parseElements(await this.window.call("query", params), "query");
1292
+ return this.parseElements(await this.window.call("query", { ...params, ...this.frameParams() }), "query");
1114
1293
  }
1115
1294
  parseElements(raw, source) {
1116
1295
  const value = expectRecord(raw, `${source} result`);
@@ -1206,6 +1385,7 @@ function createHasgardTest(config) {
1206
1385
  }
1207
1386
  export {
1208
1387
  HasgardApplication,
1388
+ HasgardFrameLocator,
1209
1389
  HasgardLocator,
1210
1390
  HasgardProcess,
1211
1391
  HasgardRpcClient,
package/dist/types.d.ts CHANGED
@@ -33,6 +33,11 @@ export interface SnapshotOptions {
33
33
  interactive?: boolean;
34
34
  selector?: string;
35
35
  depth?: number;
36
+ /**
37
+ * Take the snapshot inside this chain of same-origin iframes, one CSS
38
+ * selector per nesting level, instead of in the top document.
39
+ */
40
+ frame?: string[];
36
41
  }
37
42
  export interface WindowInfo {
38
43
  label: string;
@@ -71,6 +76,31 @@ export interface DragOffset {
71
76
  x: number;
72
77
  y: number;
73
78
  }
79
+ /** A held keyboard modifier. Named as the platform reports them on the event. */
80
+ export type KeyModifier = "Alt" | "Control" | "Meta" | "Shift";
81
+ export type MouseButton = "left" | "middle" | "right";
82
+ /** A point inside the element, measured from its top-left corner. */
83
+ export interface ElementPosition {
84
+ x: number;
85
+ y: number;
86
+ }
87
+ export interface ClickOptions {
88
+ /**
89
+ * Modifiers held for the whole gesture. `Meta` is Command on macOS and the
90
+ * Windows key elsewhere -- the platform's own accelerator, which is why
91
+ * multi-select lists usually bind it rather than `Control`.
92
+ */
93
+ modifiers?: KeyModifier[];
94
+ /**
95
+ * Which button. `right` raises `contextmenu` and `middle` raises `auxclick`;
96
+ * neither raises `click`, exactly as a real press does not.
97
+ */
98
+ button?: MouseButton;
99
+ /** Presses in one gesture. `2` also raises a single `dblclick` at the end. */
100
+ clickCount?: number;
101
+ /** Where inside the element to press. Defaults to its centre. */
102
+ position?: ElementPosition;
103
+ }
74
104
  export interface DropFile {
75
105
  name: string;
76
106
  /** Base64-encoded file contents. */
@@ -121,6 +151,49 @@ export interface DialogListing {
121
151
  dialogs: DialogRecord[];
122
152
  policy: DialogPolicy;
123
153
  }
154
+ /** How a matched request is answered. */
155
+ export interface RouteOptions {
156
+ /**
157
+ * Glob against the full URL. `*` stays inside one path segment, `**` crosses
158
+ * separators, and the pattern is anchored at both ends -- so `**\/api` does
159
+ * not also intercept `/v2/api-docs`.
160
+ */
161
+ pattern: string;
162
+ /** Restrict to one HTTP verb. Any verb matches when omitted. */
163
+ method?: string;
164
+ /** Stop matching after this many requests, letting later ones through. */
165
+ times?: number;
166
+ }
167
+ export interface FulfillOptions extends RouteOptions {
168
+ /** Defaults to 200. */
169
+ status?: number;
170
+ body?: string;
171
+ /** Defaults to `text/plain`. */
172
+ contentType?: string;
173
+ }
174
+ export interface RouteRule {
175
+ id: number;
176
+ pattern: string;
177
+ method: string | null;
178
+ action: "fulfill" | "abort";
179
+ status: number;
180
+ times: number | null;
181
+ /** How many requests this rule has already answered. */
182
+ used: number;
183
+ }
184
+ export interface InterceptedRequest {
185
+ id: number;
186
+ timestamp: number;
187
+ route_id: number;
188
+ method: string;
189
+ url: string;
190
+ action: "fulfill" | "abort";
191
+ status: number;
192
+ }
193
+ export interface RouteListing {
194
+ routes: RouteRule[];
195
+ intercepted: InterceptedRequest[];
196
+ }
124
197
  export interface ConsoleLogOptions {
125
198
  level?: "log" | "warn" | "error" | "info";
126
199
  since?: number;
package/dist/window.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { HasgardRpcClient } from "./rpc-client.js";
2
- import type { BoundingBox, ConsoleLogEntry, ConsoleLogOptions, DialogListing, DialogPolicy, DiffOptions, DragOffset, DropFile, FileInput, FilterOptions, FormsDump, HasgardTarget, JsonValue, NativeScreenshot, NativeScreenshotOptions, NetworkRequestEntry, NetworkRequestOptions, QueryDimension, RecorderEntry, RecorderResult, RecorderStatus, RoleQuery, ScrollOptions, Snapshot, SnapshotDiff, SnapshotOptions, StorageListing, StorageOptions, TextQuery, WaitForFunctionOptions, WaitOptions, WatchChanges, WatchOptions, WindowInfo, WindowState } from "./types.js";
2
+ import type { BoundingBox, ClickOptions, ConsoleLogEntry, ConsoleLogOptions, DialogListing, DialogPolicy, DiffOptions, DragOffset, DropFile, FileInput, FilterOptions, FormsDump, FulfillOptions, HasgardTarget, JsonValue, NativeScreenshot, NativeScreenshotOptions, NetworkRequestEntry, NetworkRequestOptions, QueryDimension, RecorderEntry, RecorderResult, RecorderStatus, RoleQuery, RouteListing, RouteOptions, ScrollOptions, Snapshot, SnapshotDiff, SnapshotOptions, StorageListing, StorageOptions, TextQuery, WaitForFunctionOptions, WaitOptions, WatchChanges, WatchOptions, WindowInfo, WindowState } from "./types.js";
3
3
  export declare const byRef: (ref: string) => HasgardTarget;
4
4
  export declare const bySelector: (selector: string) => HasgardTarget;
5
5
  export declare const byPoint: (x: number, y: number) => HasgardTarget;
@@ -44,6 +44,15 @@ export declare class HasgardWindow {
44
44
  getByTitle(text: string, query?: TextQuery): HasgardLocator;
45
45
  /** Test ids always match exactly, so `save` cannot select `save-draft`. */
46
46
  getByTestId(testId: string): HasgardLocator;
47
+ /**
48
+ * Scope locators to a same-origin `<iframe>`.
49
+ *
50
+ * Cross-origin frames are out of reach: the same-origin policy binds injected
51
+ * script exactly as it binds the page's own, so no bridge can see into a
52
+ * third-party payment or OAuth frame. Those raise an error naming the origin
53
+ * problem rather than reporting a missing element.
54
+ */
55
+ frameLocator(selector: string): HasgardFrameLocator;
47
56
  snapshot(options?: SnapshotOptions): Promise<Snapshot>;
48
57
  state(): Promise<WindowState>;
49
58
  url(): Promise<string>;
@@ -121,6 +130,36 @@ export declare class HasgardWindow {
121
130
  /** Forget the recorded dialogs. Leaves the accept/dismiss policy alone. */
122
131
  clear: () => Promise<void>;
123
132
  };
133
+ /**
134
+ * Shape what the page's own requests receive, without a real backend.
135
+ *
136
+ * Rules are declarative rather than Playwright's per-request callback: the
137
+ * bridge only ever answers requests, so it cannot call back into this process
138
+ * and await a handler while the page sits inside `fetch`. The same constraint
139
+ * shaped `dialogs`.
140
+ *
141
+ * Rules are checked in registration order and the first match wins, so
142
+ * register the narrow ones before a catch-all. Both `fetch` and
143
+ * `XMLHttpRequest` are covered; resources the webview loads itself -- the
144
+ * document, `<img src>`, stylesheets -- are not, since no page script sees
145
+ * them.
146
+ */
147
+ readonly routes: {
148
+ /** Answer matching requests with this response instead of the network. */
149
+ fulfill: (options: FulfillOptions) => Promise<{
150
+ id: number;
151
+ }>;
152
+ /** Fail matching requests the way a dropped connection does. */
153
+ abort: (options: RouteOptions) => Promise<{
154
+ id: number;
155
+ }>;
156
+ /** The active rules, their use counts, and every request they answered. */
157
+ list: () => Promise<RouteListing>;
158
+ /** Remove every rule and its interception log. */
159
+ clear: () => Promise<{
160
+ removed: number;
161
+ }>;
162
+ };
124
163
  /** This window's operating-system window id, where the platform exposes one. */
125
164
  nativeId(): Promise<number>;
126
165
  call<T>(method: string, params?: Record<string, JsonValue>): Promise<T>;
@@ -141,12 +180,46 @@ type LocatorQuery = ({
141
180
  }) & {
142
181
  index?: number;
143
182
  filters?: FilterOptions[];
183
+ frame?: string[];
144
184
  };
185
+ /**
186
+ * The locator factories of a window, scoped to a chain of same-origin iframes.
187
+ *
188
+ * Mirrors the window's own factories rather than wrapping a locator, because a
189
+ * frame is a document to search in, not an element to search under: `#pay`
190
+ * inside the frame is not a descendant of `#pay` outside it.
191
+ */
192
+ export declare class HasgardFrameLocator {
193
+ private readonly windowRef;
194
+ private readonly frame;
195
+ constructor(window: HasgardWindow, frame: string[]);
196
+ /** Descend into a frame nested inside this one. */
197
+ frameLocator(selector: string): HasgardFrameLocator;
198
+ locator(selector: string): HasgardLocator;
199
+ getByRole(role: string, query?: RoleQuery): HasgardLocator;
200
+ getByText(text: string, query?: TextQuery): HasgardLocator;
201
+ getByLabel(text: string, query?: TextQuery): HasgardLocator;
202
+ getByPlaceholder(text: string, query?: TextQuery): HasgardLocator;
203
+ getByAltText(text: string, query?: TextQuery): HasgardLocator;
204
+ getByTitle(text: string, query?: TextQuery): HasgardLocator;
205
+ getByTestId(testId: string): HasgardLocator;
206
+ /** Snapshot the frame's own document rather than the top one. */
207
+ snapshot(options?: SnapshotOptions): Promise<Snapshot>;
208
+ private byDimension;
209
+ }
145
210
  export declare class HasgardLocator {
146
211
  readonly window: HasgardWindow;
147
212
  private readonly query;
148
213
  constructor(window: HasgardWindow, query: LocatorQuery);
149
- click(): Promise<void>;
214
+ /**
215
+ * Click this element.
216
+ *
217
+ * A right or middle click raises `contextmenu`/`auxclick` and no `click`, and
218
+ * `clickCount: 2` escalates `detail` across the presses before a single
219
+ * `dblclick` -- both matching what the platform does, so a listener bound to
220
+ * the wrong event fails here rather than in front of a user.
221
+ */
222
+ click(options?: ClickOptions): Promise<void>;
150
223
  fill(value: string): Promise<void>;
151
224
  type(text: string): Promise<void>;
152
225
  /** Empty the control, firing `input` and `change` as `fill` does. */
@@ -175,7 +248,8 @@ export declare class HasgardLocator {
175
248
  /** Flip the checkbox, whatever its current state. */
176
249
  toggle(): Promise<void>;
177
250
  hover(): Promise<void>;
178
- dblclick(): Promise<void>;
251
+ /** Shorthand for `click({ clickCount: 2 })`; other options still apply. */
252
+ dblclick(options?: Omit<ClickOptions, "clickCount">): Promise<void>;
179
253
  focus(): Promise<void>;
180
254
  blur(): Promise<void>;
181
255
  /** Scroll this element's own scrollport. */
@@ -215,6 +289,15 @@ export declare class HasgardLocator {
215
289
  first(): HasgardLocator;
216
290
  last(): HasgardLocator;
217
291
  private resolveUnique;
292
+ /**
293
+ * Serialize a target together with the frame chain this locator lives in.
294
+ *
295
+ * A ref carries its own document, so the bridge ignores `frame` for one; the
296
+ * chain still travels so that a selector or coordinate target -- which the
297
+ * bridge must resolve itself -- lands in the right document.
298
+ */
299
+ private scoped;
300
+ private frameParams;
218
301
  private withTarget;
219
302
  private resolveAll;
220
303
  private resolveBase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyssance/tauri-hasgard",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Playwright Test fixtures for native Tauri Hasgard automation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",