@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 +2 -2
- package/dist/index.js +210 -30
- package/dist/types.d.ts +73 -0
- package/dist/window.d.ts +86 -3
- package/package.json +1 -1
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
|
-
|
|
827
|
-
|
|
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", { ...
|
|
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", { ...
|
|
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
|
-
...
|
|
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", { ...
|
|
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", { ...
|
|
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", { ...
|
|
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", { ...
|
|
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",
|
|
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",
|
|
1048
|
+
await this.withTarget((target) => this.window.call("hover", this.scoped(target)));
|
|
888
1049
|
}
|
|
889
|
-
|
|
890
|
-
|
|
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",
|
|
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",
|
|
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", { ...
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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:
|
|
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
|
-
...
|
|
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",
|
|
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",
|
|
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",
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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;
|