@nyssance/tauri-hasgard 0.1.2 → 0.2.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/dist/index.d.ts +2 -2
- package/dist/index.js +629 -23
- package/dist/types.d.ts +205 -0
- package/dist/window.d.ts +173 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { createHasgardExpect } from "./expect.js";
|
|
2
1
|
export { hasgardEndpointPath } from "./endpoint.js";
|
|
2
|
+
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 { HasgardFixtures, HasgardLaunchConfig, HasgardTarget, HasgardTestConfig, HasgardWorkerFixtures, JsonPrimitive, JsonValue, RoleQuery, Snapshot, SnapshotElement, SnapshotOptions, WaitOptions, WindowInfo, WindowState } from "./types.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
7
|
export { byPoint, byRef, bySelector, HasgardApplication, HasgardLocator, HasgardWindow } from "./window.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
// src/endpoint.ts
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
function endpointPathForPlatform(name, platform, temporaryDirectory) {
|
|
5
|
+
if (platform === "win32") return `\\\\.\\pipe\\${name}`;
|
|
6
|
+
return join(temporaryDirectory, `${name}.sock`);
|
|
7
|
+
}
|
|
8
|
+
function hasgardEndpointPath(name) {
|
|
9
|
+
return endpointPathForPlatform(name, process.platform, tmpdir());
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
// src/expect.ts
|
|
2
13
|
async function poll(read, matches, timeoutMs) {
|
|
3
14
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -49,17 +60,6 @@ function createHasgardExpect(playwrightExpect) {
|
|
|
49
60
|
});
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
// src/endpoint.ts
|
|
53
|
-
import { tmpdir } from "os";
|
|
54
|
-
import { join } from "path";
|
|
55
|
-
function endpointPathForPlatform(name, platform, temporaryDirectory) {
|
|
56
|
-
if (platform === "win32") return `\\\\.\\pipe\\${name}`;
|
|
57
|
-
return join(temporaryDirectory, `${name}.sock`);
|
|
58
|
-
}
|
|
59
|
-
function hasgardEndpointPath(name) {
|
|
60
|
-
return endpointPathForPlatform(name, process.platform, tmpdir());
|
|
61
|
-
}
|
|
62
|
-
|
|
63
63
|
// src/process-manager.ts
|
|
64
64
|
import { spawn } from "child_process";
|
|
65
65
|
import { access, rm } from "fs/promises";
|
|
@@ -270,6 +270,8 @@ var HasgardRpcClient = class {
|
|
|
270
270
|
};
|
|
271
271
|
|
|
272
272
|
// src/window.ts
|
|
273
|
+
import { readFile } from "fs/promises";
|
|
274
|
+
import { basename } from "path";
|
|
273
275
|
var locks = /* @__PURE__ */ new WeakMap();
|
|
274
276
|
function lockFor(rpc, label) {
|
|
275
277
|
let clientLocks = locks.get(rpc);
|
|
@@ -290,11 +292,26 @@ var byPoint = (x, y) => ({ x, y });
|
|
|
290
292
|
function withWindow(window, params) {
|
|
291
293
|
return params ? { ...params, window } : { window };
|
|
292
294
|
}
|
|
295
|
+
async function readFileInputs(files) {
|
|
296
|
+
const list = Array.isArray(files) ? files : [files];
|
|
297
|
+
return Promise.all(
|
|
298
|
+
list.map(async (entry) => {
|
|
299
|
+
if (typeof entry !== "string") return entry;
|
|
300
|
+
const contents = await readFile(entry);
|
|
301
|
+
return { name: basename(entry), data: contents.toString("base64") };
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
}
|
|
293
305
|
function targetParams(target) {
|
|
294
306
|
if ("ref" in target) return { ref: target.ref };
|
|
295
|
-
if ("selector" in target)
|
|
307
|
+
if ("selector" in target) {
|
|
308
|
+
return target.index === void 0 ? { selector: target.selector } : { selector: target.selector, index: target.index };
|
|
309
|
+
}
|
|
296
310
|
return { x: target.x, y: target.y };
|
|
297
311
|
}
|
|
312
|
+
function absoluteIndex(index, total) {
|
|
313
|
+
return index < 0 ? total + index : index;
|
|
314
|
+
}
|
|
298
315
|
function expectRecord(value, context) {
|
|
299
316
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
300
317
|
throw new Error(`${context} must be an object`);
|
|
@@ -313,6 +330,106 @@ function expectNumber(value, context) {
|
|
|
313
330
|
if (typeof value !== "number") throw new Error(`${context} must be a number`);
|
|
314
331
|
return value;
|
|
315
332
|
}
|
|
333
|
+
function expectArray(value, context) {
|
|
334
|
+
if (!Array.isArray(value)) throw new Error(`${context} must be an array`);
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
337
|
+
function expectNullableString(value, context) {
|
|
338
|
+
if (value === null) return null;
|
|
339
|
+
return expectString(value, context);
|
|
340
|
+
}
|
|
341
|
+
function optionalString(value, context) {
|
|
342
|
+
return value === void 0 ? void 0 : expectString(value, context);
|
|
343
|
+
}
|
|
344
|
+
function parseConsoleLogEntry(value, index) {
|
|
345
|
+
const entry = expectRecord(value, `console.getLogs[${index}]`);
|
|
346
|
+
return {
|
|
347
|
+
id: expectNumber(entry.id, `console.getLogs[${index}].id`),
|
|
348
|
+
timestamp: expectNumber(entry.timestamp, `console.getLogs[${index}].timestamp`),
|
|
349
|
+
level: expectString(entry.level, `console.getLogs[${index}].level`),
|
|
350
|
+
args: expectArray(entry.args, `console.getLogs[${index}].args`),
|
|
351
|
+
source: expectNullableString(entry.source, `console.getLogs[${index}].source`)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function parseNetworkRequestEntry(value, index) {
|
|
355
|
+
const entry = expectRecord(value, `network.getRequests[${index}]`);
|
|
356
|
+
return {
|
|
357
|
+
id: expectNumber(entry.id, `network.getRequests[${index}].id`),
|
|
358
|
+
timestamp: expectNumber(entry.timestamp, `network.getRequests[${index}].timestamp`),
|
|
359
|
+
method: expectString(entry.method, `network.getRequests[${index}].method`),
|
|
360
|
+
url: expectString(entry.url, `network.getRequests[${index}].url`),
|
|
361
|
+
status: expectNumber(entry.status, `network.getRequests[${index}].status`),
|
|
362
|
+
duration_ms: expectNumber(entry.duration_ms, `network.getRequests[${index}].duration_ms`),
|
|
363
|
+
error: expectNullableString(entry.error, `network.getRequests[${index}].error`),
|
|
364
|
+
request_size: expectNumber(entry.request_size, `network.getRequests[${index}].request_size`),
|
|
365
|
+
response_size: expectNumber(entry.response_size, `network.getRequests[${index}].response_size`)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function parseFormField(value, context) {
|
|
369
|
+
const field = expectRecord(value, context);
|
|
370
|
+
const raw = field.value;
|
|
371
|
+
const parsed = {
|
|
372
|
+
tag: expectString(field.tag, `${context}.tag`),
|
|
373
|
+
type: expectNullableString(field.type, `${context}.type`),
|
|
374
|
+
name: expectString(field.name, `${context}.name`),
|
|
375
|
+
value: Array.isArray(raw) ? raw.map((option, index) => expectString(option, `${context}.value[${index}]`)) : expectString(raw, `${context}.value`)
|
|
376
|
+
};
|
|
377
|
+
if (field.checked !== void 0) parsed.checked = expectBoolean(field.checked, `${context}.checked`);
|
|
378
|
+
return parsed;
|
|
379
|
+
}
|
|
380
|
+
function parseFormEntry(value, index) {
|
|
381
|
+
const context = `forms.dump.forms[${index}]`;
|
|
382
|
+
const form = expectRecord(value, context);
|
|
383
|
+
const parsed = {
|
|
384
|
+
id: expectString(form.id, `${context}.id`),
|
|
385
|
+
name: expectString(form.name, `${context}.name`),
|
|
386
|
+
action: expectString(form.action, `${context}.action`),
|
|
387
|
+
method: expectString(form.method, `${context}.method`),
|
|
388
|
+
fields: expectArray(form.fields, `${context}.fields`).map(
|
|
389
|
+
(field, fieldIndex) => parseFormField(field, `${context}.fields[${fieldIndex}]`)
|
|
390
|
+
)
|
|
391
|
+
};
|
|
392
|
+
if (form.fieldsTruncated !== void 0) {
|
|
393
|
+
parsed.fieldsTruncated = expectBoolean(form.fieldsTruncated, `${context}.fieldsTruncated`);
|
|
394
|
+
}
|
|
395
|
+
return parsed;
|
|
396
|
+
}
|
|
397
|
+
function parseWatchNode(value, context) {
|
|
398
|
+
const node = expectRecord(value, context);
|
|
399
|
+
const parsed = { tag: expectString(node.tag, `${context}.tag`) };
|
|
400
|
+
const id = optionalString(node.id, `${context}.id`);
|
|
401
|
+
if (id !== void 0) parsed.id = id;
|
|
402
|
+
const className = optionalString(node.class, `${context}.class`);
|
|
403
|
+
if (className !== void 0) parsed.class = className;
|
|
404
|
+
const text = optionalString(node.text, `${context}.text`);
|
|
405
|
+
if (text !== void 0) parsed.text = text;
|
|
406
|
+
const attr = optionalString(node.attr, `${context}.attr`);
|
|
407
|
+
if (attr !== void 0) parsed.attr = attr;
|
|
408
|
+
const attrValue = optionalString(node.value, `${context}.value`);
|
|
409
|
+
if (attrValue !== void 0) parsed.value = attrValue;
|
|
410
|
+
return parsed;
|
|
411
|
+
}
|
|
412
|
+
function parseWatchNodes(value, context) {
|
|
413
|
+
return expectArray(value, context).map((node, index) => parseWatchNode(node, `${context}[${index}]`));
|
|
414
|
+
}
|
|
415
|
+
function parseDiffChange(value, index) {
|
|
416
|
+
const change = expectRecord(value, `diff.changed[${index}]`);
|
|
417
|
+
return {
|
|
418
|
+
old: parseSnapshotElement(change.old, index),
|
|
419
|
+
new: parseSnapshotElement(change.new, index),
|
|
420
|
+
changes: expectArray(change.changes, `diff.changed[${index}].changes`).map(
|
|
421
|
+
(entry, entryIndex) => expectString(entry, `diff.changed[${index}].changes[${entryIndex}]`)
|
|
422
|
+
)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function parseRecorderEntry(value, index) {
|
|
426
|
+
const entry = expectRecord(value, `record.stop.entries[${index}]`);
|
|
427
|
+
return {
|
|
428
|
+
...entry,
|
|
429
|
+
action: expectString(entry.action, `record.stop.entries[${index}].action`),
|
|
430
|
+
timestamp: expectNumber(entry.timestamp, `record.stop.entries[${index}].timestamp`)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
316
433
|
var HasgardApplication = class {
|
|
317
434
|
rpc;
|
|
318
435
|
constructor(rpc) {
|
|
@@ -330,11 +447,15 @@ var HasgardApplication = class {
|
|
|
330
447
|
if (!Array.isArray(value.windows)) throw new Error("windows.list.windows must be an array");
|
|
331
448
|
return value.windows.map((entry, index) => {
|
|
332
449
|
const window = expectRecord(entry, `windows[${index}]`);
|
|
333
|
-
|
|
450
|
+
const info = {
|
|
334
451
|
label: expectString(window.label, `windows[${index}].label`),
|
|
335
452
|
url: expectString(window.url, `windows[${index}].url`),
|
|
336
453
|
title: expectString(window.title, `windows[${index}].title`)
|
|
337
454
|
};
|
|
455
|
+
if (window.native_id !== void 0) {
|
|
456
|
+
info.nativeId = expectNumber(window.native_id, `windows[${index}].native_id`);
|
|
457
|
+
}
|
|
458
|
+
return info;
|
|
338
459
|
});
|
|
339
460
|
}
|
|
340
461
|
async waitForWindow(label, timeoutMs) {
|
|
@@ -363,6 +484,33 @@ var HasgardApplication = class {
|
|
|
363
484
|
window(label) {
|
|
364
485
|
return new HasgardWindow(this.rpc, label);
|
|
365
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Action recorder. Captures the RPC calls this connection makes into a
|
|
489
|
+
* replayable scenario — this is not video capture.
|
|
490
|
+
*/
|
|
491
|
+
recorder = {
|
|
492
|
+
start: async () => {
|
|
493
|
+
await this.rpc.call("record.start");
|
|
494
|
+
},
|
|
495
|
+
stop: async () => {
|
|
496
|
+
const value = expectRecord(await this.rpc.call("record.stop"), "record.stop result");
|
|
497
|
+
return {
|
|
498
|
+
entries: expectArray(value.entries, "record.stop.entries").map(parseRecorderEntry),
|
|
499
|
+
count: expectNumber(value.count, "record.stop.count")
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
status: async () => {
|
|
503
|
+
const value = expectRecord(await this.rpc.call("record.status"), "record.status result");
|
|
504
|
+
return {
|
|
505
|
+
active: expectBoolean(value.active, "record.status.active"),
|
|
506
|
+
count: expectNumber(value.count, "record.status.count"),
|
|
507
|
+
elapsed_ms: expectNumber(value.elapsed_ms, "record.status.elapsed_ms")
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
add: async (entry) => {
|
|
511
|
+
await this.rpc.call("record.add", entry);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
366
514
|
};
|
|
367
515
|
var HasgardWindow = class {
|
|
368
516
|
label;
|
|
@@ -377,6 +525,31 @@ var HasgardWindow = class {
|
|
|
377
525
|
getByRole(role, query = {}) {
|
|
378
526
|
return new HasgardLocator(this, { kind: "role", role, query });
|
|
379
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Locate by visible text. Matching runs in the page against the live DOM, not
|
|
530
|
+
* against a snapshot's `name`: that field collapses aria-label, alt, `<label>`,
|
|
531
|
+
* placeholder, and text into one 50-character string, so a button with an
|
|
532
|
+
* aria-label would never be findable by the text it actually shows.
|
|
533
|
+
*/
|
|
534
|
+
getByText(text, query = {}) {
|
|
535
|
+
return new HasgardLocator(this, { kind: "query", by: "text", value: text, query });
|
|
536
|
+
}
|
|
537
|
+
getByLabel(text, query = {}) {
|
|
538
|
+
return new HasgardLocator(this, { kind: "query", by: "label", value: text, query });
|
|
539
|
+
}
|
|
540
|
+
getByPlaceholder(text, query = {}) {
|
|
541
|
+
return new HasgardLocator(this, { kind: "query", by: "placeholder", value: text, query });
|
|
542
|
+
}
|
|
543
|
+
getByAltText(text, query = {}) {
|
|
544
|
+
return new HasgardLocator(this, { kind: "query", by: "alt", value: text, query });
|
|
545
|
+
}
|
|
546
|
+
getByTitle(text, query = {}) {
|
|
547
|
+
return new HasgardLocator(this, { kind: "query", by: "title", value: text, query });
|
|
548
|
+
}
|
|
549
|
+
/** Test ids always match exactly, so `save` cannot select `save-draft`. */
|
|
550
|
+
getByTestId(testId) {
|
|
551
|
+
return new HasgardLocator(this, { kind: "query", by: "testid", value: testId, query: {} });
|
|
552
|
+
}
|
|
380
553
|
async snapshot(options = {}) {
|
|
381
554
|
const raw = expectRecord(
|
|
382
555
|
await this.rpc.call("snapshot", withWindow(this.label, options)),
|
|
@@ -406,9 +579,127 @@ var HasgardWindow = class {
|
|
|
406
579
|
plugin_version: expectString(value.plugin_version, "state.plugin_version")
|
|
407
580
|
};
|
|
408
581
|
}
|
|
582
|
+
async url() {
|
|
583
|
+
return expectString(await this.rpc.call("url", withWindow(this.label)), "url result");
|
|
584
|
+
}
|
|
585
|
+
async title() {
|
|
586
|
+
return expectString(await this.rpc.call("title", withWindow(this.label)), "title result");
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Navigate this webview by assigning `location.href`. Unlike Playwright's
|
|
590
|
+
* `goto`, this does not wait for the load to finish and returns no response —
|
|
591
|
+
* follow it with `waitFor` or `waitForWindowReady`.
|
|
592
|
+
*/
|
|
593
|
+
async navigate(url) {
|
|
594
|
+
await this.rpc.call("navigate", withWindow(this.label, { url }));
|
|
595
|
+
}
|
|
596
|
+
/** Scroll the document by a relative amount, or to `top`/`bottom`. */
|
|
597
|
+
async scrollBy(options) {
|
|
598
|
+
await this.rpc.call("scroll", withWindow(this.label, options));
|
|
599
|
+
}
|
|
409
600
|
async press(key) {
|
|
410
601
|
await this.rpc.call("press", withWindow(this.label, { key }));
|
|
411
602
|
}
|
|
603
|
+
async diff(options = {}) {
|
|
604
|
+
const { reference, ...snapshotOptions } = options;
|
|
605
|
+
const params = { ...snapshotOptions };
|
|
606
|
+
if (reference) params.reference = reference;
|
|
607
|
+
const value = expectRecord(await this.rpc.call("diff", withWindow(this.label, params)), "diff result");
|
|
608
|
+
return {
|
|
609
|
+
added: expectArray(value.added, "diff.added").map(parseSnapshotElement),
|
|
610
|
+
removed: expectArray(value.removed, "diff.removed").map(parseSnapshotElement),
|
|
611
|
+
changed: expectArray(value.changed, "diff.changed").map(parseDiffChange)
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Poll a JavaScript expression in the webview until it returns a truthy
|
|
616
|
+
* value, and return that value. The expression is polled rather than driven
|
|
617
|
+
* by DOM mutations, so predicates that flip on a timer or a resolved fetch
|
|
618
|
+
* are caught too. A predicate that throws rejects instead of being retried.
|
|
619
|
+
*/
|
|
620
|
+
async waitForFunction(expression, options = {}) {
|
|
621
|
+
const params = { expression };
|
|
622
|
+
if (options.timeoutMs !== void 0) params.timeout = options.timeoutMs;
|
|
623
|
+
if (options.pollMs !== void 0) params.poll = options.pollMs;
|
|
624
|
+
const value = expectRecord(await this.rpc.call("wait", withWindow(this.label, params)), "wait result");
|
|
625
|
+
return value.value;
|
|
626
|
+
}
|
|
627
|
+
/** Observe DOM mutations until they settle, or reject when nothing changes. */
|
|
628
|
+
async watch(options = {}) {
|
|
629
|
+
const params = {};
|
|
630
|
+
if (options.selector !== void 0) params.selector = options.selector;
|
|
631
|
+
if (options.timeoutMs !== void 0) params.timeout = options.timeoutMs;
|
|
632
|
+
if (options.stableMs !== void 0) params.stable = options.stableMs;
|
|
633
|
+
if (options.requireMutation !== void 0) params.requireMutation = options.requireMutation;
|
|
634
|
+
const value = expectRecord(await this.rpc.call("watch", withWindow(this.label, params)), "watch result");
|
|
635
|
+
return {
|
|
636
|
+
added: parseWatchNodes(value.added, "watch.added"),
|
|
637
|
+
removed: parseWatchNodes(value.removed, "watch.removed"),
|
|
638
|
+
modified: parseWatchNodes(value.modified, "watch.modified"),
|
|
639
|
+
truncated: expectBoolean(value.truncated, "watch.truncated")
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async consoleLogs(options = {}) {
|
|
643
|
+
const value = await this.rpc.call(
|
|
644
|
+
"console.getLogs",
|
|
645
|
+
withWindow(this.label, options)
|
|
646
|
+
);
|
|
647
|
+
return expectArray(value, "console.getLogs result").map(parseConsoleLogEntry);
|
|
648
|
+
}
|
|
649
|
+
async clearConsole() {
|
|
650
|
+
await this.rpc.call("console.clear", withWindow(this.label));
|
|
651
|
+
}
|
|
652
|
+
async networkRequests(options = {}) {
|
|
653
|
+
const value = await this.rpc.call(
|
|
654
|
+
"network.getRequests",
|
|
655
|
+
withWindow(this.label, options)
|
|
656
|
+
);
|
|
657
|
+
return expectArray(value, "network.getRequests result").map(parseNetworkRequestEntry);
|
|
658
|
+
}
|
|
659
|
+
async clearNetwork() {
|
|
660
|
+
await this.rpc.call("network.clear", withWindow(this.label));
|
|
661
|
+
}
|
|
662
|
+
/** `localStorage` by default; pass `{ session: true }` for `sessionStorage`. */
|
|
663
|
+
storage = {
|
|
664
|
+
get: async (key, options = {}) => {
|
|
665
|
+
const value = expectRecord(
|
|
666
|
+
await this.rpc.call("storage.get", withWindow(this.label, { key, ...options })),
|
|
667
|
+
"storage.get result"
|
|
668
|
+
);
|
|
669
|
+
if (!expectBoolean(value.found, "storage.get.found")) return null;
|
|
670
|
+
return expectString(value.value, "storage.get.value");
|
|
671
|
+
},
|
|
672
|
+
set: async (key, value, options = {}) => {
|
|
673
|
+
await this.rpc.call("storage.set", withWindow(this.label, { key, value, ...options }));
|
|
674
|
+
},
|
|
675
|
+
list: async (options = {}) => {
|
|
676
|
+
const value = expectRecord(
|
|
677
|
+
await this.rpc.call("storage.list", withWindow(this.label, { ...options })),
|
|
678
|
+
"storage.list result"
|
|
679
|
+
);
|
|
680
|
+
return {
|
|
681
|
+
entries: expectArray(value.entries, "storage.list.entries").map((entry, index) => {
|
|
682
|
+
const parsed = expectRecord(entry, `storage.list.entries[${index}]`);
|
|
683
|
+
return {
|
|
684
|
+
key: expectString(parsed.key, `storage.list.entries[${index}].key`),
|
|
685
|
+
value: expectString(parsed.value, `storage.list.entries[${index}].value`)
|
|
686
|
+
};
|
|
687
|
+
}),
|
|
688
|
+
truncated: expectBoolean(value.truncated, "storage.list.truncated")
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
clear: async (options = {}) => {
|
|
692
|
+
await this.rpc.call("storage.clear", withWindow(this.label, { ...options }));
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
async forms(selector) {
|
|
696
|
+
const params = selector ? { selector } : void 0;
|
|
697
|
+
const value = expectRecord(await this.rpc.call("forms.dump", withWindow(this.label, params)), "forms.dump result");
|
|
698
|
+
return {
|
|
699
|
+
forms: expectArray(value.forms, "forms.dump.forms").map(parseFormEntry),
|
|
700
|
+
truncated: expectBoolean(value.truncated, "forms.dump.truncated")
|
|
701
|
+
};
|
|
702
|
+
}
|
|
412
703
|
async evaluate(script) {
|
|
413
704
|
return this.rpc.call("eval", withWindow(this.label, { script }));
|
|
414
705
|
}
|
|
@@ -422,6 +713,90 @@ var HasgardWindow = class {
|
|
|
422
713
|
if (!dataUrl.startsWith(prefix)) throw new Error("screenshot result is not a PNG data URL");
|
|
423
714
|
return Buffer.from(dataUrl.slice(prefix.length), "base64");
|
|
424
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Capture this window through the native compositor, writing a PNG to
|
|
718
|
+
* `outputPath`. macOS only.
|
|
719
|
+
*
|
|
720
|
+
* Unlike `screenshot`, which renders the DOM inside the page, this records
|
|
721
|
+
* what the window server actually shows — so it sees native chrome, layered
|
|
722
|
+
* content, and anything the DOM renderer cannot reproduce.
|
|
723
|
+
*
|
|
724
|
+
* The operating-system window id is resolved from this window's label, so
|
|
725
|
+
* callers do not have to discover it themselves.
|
|
726
|
+
*/
|
|
727
|
+
async screenshotNative(options) {
|
|
728
|
+
const windowId = options.windowId ?? await this.nativeId();
|
|
729
|
+
const value = expectRecord(
|
|
730
|
+
await this.rpc.call("screenshot_native", {
|
|
731
|
+
window_id: windowId,
|
|
732
|
+
output_path: options.outputPath
|
|
733
|
+
}),
|
|
734
|
+
"screenshot_native result"
|
|
735
|
+
);
|
|
736
|
+
return {
|
|
737
|
+
output_path: expectString(value.output_path, "screenshot_native.output_path"),
|
|
738
|
+
window_id: expectNumber(value.window_id, "screenshot_native.window_id"),
|
|
739
|
+
width: expectNumber(value.width, "screenshot_native.width"),
|
|
740
|
+
height: expectNumber(value.height, "screenshot_native.height"),
|
|
741
|
+
scale_factor: expectNumber(value.scale_factor, "screenshot_native.scale_factor"),
|
|
742
|
+
byte_size: expectNumber(value.byte_size, "screenshot_native.byte_size"),
|
|
743
|
+
backend: expectString(value.backend, "screenshot_native.backend"),
|
|
744
|
+
tcc_denied: expectBoolean(value.tcc_denied, "screenshot_native.tcc_denied")
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Scroll the page by wheel.
|
|
749
|
+
*
|
|
750
|
+
* The document sees a real `wheel` event first and may cancel it; the scroll
|
|
751
|
+
* only follows if it does not. Resolves to whether the page cancelled — which
|
|
752
|
+
* is how you tell "the app intercepted my wheel" from "nothing was
|
|
753
|
+
* listening". Use the locator method to scroll a nested scrollport.
|
|
754
|
+
*/
|
|
755
|
+
async wheel(deltaX, deltaY) {
|
|
756
|
+
const value = expectRecord(await this.call("wheel", { deltaX, deltaY }), "wheel result");
|
|
757
|
+
return expectBoolean(value.defaultPrevented, "wheel.defaultPrevented");
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* The page's modal dialogs (`alert`, `confirm`, `prompt`).
|
|
761
|
+
*
|
|
762
|
+
* These block the webview until answered, so the bridge always answers them —
|
|
763
|
+
* dismissing by default, as Playwright does. A test that never touches this
|
|
764
|
+
* namespace still cannot hang on a `confirm()`, and can read back what the
|
|
765
|
+
* app tried to ask.
|
|
766
|
+
*/
|
|
767
|
+
dialogs = {
|
|
768
|
+
/** Answer every subsequent dialog with OK. `promptText` fills a `prompt`. */
|
|
769
|
+
accept: async (promptText) => {
|
|
770
|
+
const params = { action: "accept" };
|
|
771
|
+
if (promptText !== void 0) params.promptText = promptText;
|
|
772
|
+
return parseDialogPolicy(await this.call("dialog.handle", params));
|
|
773
|
+
},
|
|
774
|
+
/** Answer every subsequent dialog with Cancel. This is the default. */
|
|
775
|
+
dismiss: async () => parseDialogPolicy(await this.call("dialog.handle", { action: "dismiss" })),
|
|
776
|
+
/** Every dialog the page has opened so far, oldest first. */
|
|
777
|
+
list: async () => {
|
|
778
|
+
const value = expectRecord(await this.call("dialog.list"), "dialog.list result");
|
|
779
|
+
if (!Array.isArray(value.dialogs)) throw new Error("dialog.list.dialogs must be an array");
|
|
780
|
+
return {
|
|
781
|
+
dialogs: value.dialogs.map((entry, index) => parseDialogRecord(entry, index)),
|
|
782
|
+
policy: parseDialogPolicy(value.policy)
|
|
783
|
+
};
|
|
784
|
+
},
|
|
785
|
+
/** Forget the recorded dialogs. Leaves the accept/dismiss policy alone. */
|
|
786
|
+
clear: async () => {
|
|
787
|
+
await this.call("dialog.clear");
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
/** This window's operating-system window id, where the platform exposes one. */
|
|
791
|
+
async nativeId() {
|
|
792
|
+
const listing = await new HasgardApplication(this.rpc).windows();
|
|
793
|
+
const self = listing.find((entry) => entry.label === this.label);
|
|
794
|
+
if (!self) throw new Error(`Window '${this.label}' is no longer open`);
|
|
795
|
+
if (self.nativeId === void 0) {
|
|
796
|
+
throw new Error(`Window '${this.label}' has no native window id on this platform`);
|
|
797
|
+
}
|
|
798
|
+
return self.nativeId;
|
|
799
|
+
}
|
|
425
800
|
async call(method, params) {
|
|
426
801
|
return this.rpc.call(method, withWindow(this.label, params));
|
|
427
802
|
}
|
|
@@ -441,7 +816,7 @@ var HasgardWindow = class {
|
|
|
441
816
|
}
|
|
442
817
|
}
|
|
443
818
|
};
|
|
444
|
-
var HasgardLocator = class {
|
|
819
|
+
var HasgardLocator = class _HasgardLocator {
|
|
445
820
|
window;
|
|
446
821
|
query;
|
|
447
822
|
constructor(window, query) {
|
|
@@ -457,12 +832,95 @@ var HasgardLocator = class {
|
|
|
457
832
|
async type(text) {
|
|
458
833
|
await this.withTarget((target) => this.window.call("type", { ...targetParams(target), text }));
|
|
459
834
|
}
|
|
835
|
+
/** Empty the control, firing `input` and `change` as `fill` does. */
|
|
836
|
+
async clear() {
|
|
837
|
+
await this.fill("");
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Select files on an `<input type="file">` without a native file chooser,
|
|
841
|
+
* which an embedded webview gives no way to drive.
|
|
842
|
+
*
|
|
843
|
+
* Strings are read from the test machine's disk; `DropFile` payloads let you
|
|
844
|
+
* supply content that never existed as a file. An empty array deselects.
|
|
845
|
+
*/
|
|
846
|
+
async setInputFiles(files) {
|
|
847
|
+
const payloads = await readFileInputs(files);
|
|
848
|
+
await this.withTarget(
|
|
849
|
+
(target) => this.window.call("setInputFiles", {
|
|
850
|
+
...targetParams(target),
|
|
851
|
+
files: payloads
|
|
852
|
+
})
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Scroll this element by wheel.
|
|
857
|
+
*
|
|
858
|
+
* The page sees a real `wheel` event first and may cancel it; the scroll only
|
|
859
|
+
* follows if it does not — the same two steps a physical wheel produces.
|
|
860
|
+
* Resolves to whether the page cancelled.
|
|
861
|
+
*/
|
|
862
|
+
async wheel(deltaX, deltaY) {
|
|
863
|
+
return this.withTarget(async (target) => {
|
|
864
|
+
const value = expectRecord(
|
|
865
|
+
await this.window.call("wheel", { ...targetParams(target), deltaX, deltaY }),
|
|
866
|
+
"wheel result"
|
|
867
|
+
);
|
|
868
|
+
return expectBoolean(value.defaultPrevented, "wheel.defaultPrevented");
|
|
869
|
+
});
|
|
870
|
+
}
|
|
460
871
|
async selectOption(value) {
|
|
461
872
|
await this.withTarget((target) => this.window.call("select", { ...targetParams(target), value }));
|
|
462
873
|
}
|
|
874
|
+
/** Drive the checkbox to checked. A no-op when it is already checked. */
|
|
463
875
|
async check() {
|
|
876
|
+
await this.withTarget((target) => this.window.call("check", { ...targetParams(target), checked: true }));
|
|
877
|
+
}
|
|
878
|
+
/** Drive the checkbox to unchecked. A no-op when it is already unchecked. */
|
|
879
|
+
async uncheck() {
|
|
880
|
+
await this.withTarget((target) => this.window.call("check", { ...targetParams(target), checked: false }));
|
|
881
|
+
}
|
|
882
|
+
/** Flip the checkbox, whatever its current state. */
|
|
883
|
+
async toggle() {
|
|
464
884
|
await this.withTarget((target) => this.window.call("check", targetParams(target)));
|
|
465
885
|
}
|
|
886
|
+
async hover() {
|
|
887
|
+
await this.withTarget((target) => this.window.call("hover", targetParams(target)));
|
|
888
|
+
}
|
|
889
|
+
async dblclick() {
|
|
890
|
+
await this.withTarget((target) => this.window.call("dblclick", targetParams(target)));
|
|
891
|
+
}
|
|
892
|
+
async focus() {
|
|
893
|
+
await this.withTarget((target) => this.window.call("focus", targetParams(target)));
|
|
894
|
+
}
|
|
895
|
+
async blur() {
|
|
896
|
+
await this.withTarget((target) => this.window.call("blur", targetParams(target)));
|
|
897
|
+
}
|
|
898
|
+
/** Scroll this element's own scrollport. */
|
|
899
|
+
async scrollBy(options) {
|
|
900
|
+
await this.withTarget(
|
|
901
|
+
(target) => this.window.call("scroll", { ...targetParams(target), ...options })
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
async boundingBox() {
|
|
905
|
+
return this.withTarget(async (target) => {
|
|
906
|
+
const value = expectRecord(await this.window.call("boundingBox", targetParams(target)), "boundingBox result");
|
|
907
|
+
return {
|
|
908
|
+
x: expectNumber(value.x, "boundingBox.x"),
|
|
909
|
+
y: expectNumber(value.y, "boundingBox.y"),
|
|
910
|
+
width: expectNumber(value.width, "boundingBox.width"),
|
|
911
|
+
height: expectNumber(value.height, "boundingBox.height")
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
async isDisabled() {
|
|
916
|
+
return this.withTarget(async (target) => {
|
|
917
|
+
const value = expectRecord(await this.window.call("disabled", targetParams(target)), "disabled result");
|
|
918
|
+
return expectBoolean(value.disabled, "disabled.disabled");
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
async isEnabled() {
|
|
922
|
+
return !await this.isDisabled();
|
|
923
|
+
}
|
|
466
924
|
async textContent() {
|
|
467
925
|
return this.withTarget(
|
|
468
926
|
async (target) => expectString(await this.window.call("text", targetParams(target)), "text result")
|
|
@@ -473,6 +931,44 @@ var HasgardLocator = class {
|
|
|
473
931
|
async (target) => expectString(await this.window.call("value", targetParams(target)), "value result")
|
|
474
932
|
);
|
|
475
933
|
}
|
|
934
|
+
async innerHTML() {
|
|
935
|
+
return this.withTarget(
|
|
936
|
+
async (target) => expectString(await this.window.call("html", targetParams(target)), "html result")
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
async getAttribute(name) {
|
|
940
|
+
const attributes = await this.attributes();
|
|
941
|
+
return Object.prototype.hasOwnProperty.call(attributes, name) ? attributes[name] : null;
|
|
942
|
+
}
|
|
943
|
+
/** Drag this element onto another one in the same window. */
|
|
944
|
+
async dragTo(target) {
|
|
945
|
+
if (target.window !== this.window) {
|
|
946
|
+
throw new Error("dragTo requires both locators to belong to the same window");
|
|
947
|
+
}
|
|
948
|
+
await this.window.runExclusive(async () => {
|
|
949
|
+
const source = await this.resolveUnique();
|
|
950
|
+
const destination = await target.resolveUnique();
|
|
951
|
+
await this.window.call("drag", {
|
|
952
|
+
source: targetParams(source),
|
|
953
|
+
target: targetParams(destination)
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
/** Drag this element by a pixel offset from its centre. */
|
|
958
|
+
async dragBy(offset) {
|
|
959
|
+
await this.withTarget(
|
|
960
|
+
(target) => this.window.call("drag", { source: targetParams(target), offset: { x: offset.x, y: offset.y } })
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
/** Dispatch a file drop onto this element. `data` is base64-encoded. */
|
|
964
|
+
async dropFiles(files) {
|
|
965
|
+
await this.withTarget(
|
|
966
|
+
(target) => this.window.call("drop", {
|
|
967
|
+
...targetParams(target),
|
|
968
|
+
files
|
|
969
|
+
})
|
|
970
|
+
);
|
|
971
|
+
}
|
|
476
972
|
async attributes() {
|
|
477
973
|
return this.withTarget(async (target) => {
|
|
478
974
|
const value = expectRecord(await this.window.call("attrs", targetParams(target)), "attrs result");
|
|
@@ -494,12 +990,16 @@ var HasgardLocator = class {
|
|
|
494
990
|
});
|
|
495
991
|
}
|
|
496
992
|
async count() {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
993
|
+
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,
|
|
995
|
+
"count.count"
|
|
996
|
+
) : (await this.resolveAll()).length;
|
|
997
|
+
if (this.query.index === void 0) return total;
|
|
998
|
+
const position = absoluteIndex(this.query.index, total);
|
|
999
|
+
return position >= 0 && position < total ? 1 : 0;
|
|
500
1000
|
}
|
|
501
1001
|
async waitFor(options) {
|
|
502
|
-
if (this.query.kind === "selector") {
|
|
1002
|
+
if (this.query.kind === "selector" && this.query.index === void 0 && !this.query.filters?.length) {
|
|
503
1003
|
await this.window.call("wait", {
|
|
504
1004
|
selector: this.query.selector,
|
|
505
1005
|
gone: options.state === "detached",
|
|
@@ -509,17 +1009,57 @@ var HasgardLocator = class {
|
|
|
509
1009
|
}
|
|
510
1010
|
const deadline = Date.now() + options.timeoutMs;
|
|
511
1011
|
while (Date.now() <= deadline) {
|
|
512
|
-
const found =
|
|
1012
|
+
const found = await this.count() > 0;
|
|
513
1013
|
if (found === (options.state === "attached")) return;
|
|
514
1014
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
515
1015
|
}
|
|
516
|
-
throw new Error(`
|
|
1016
|
+
throw new Error(`Locator did not become ${options.state} within ${options.timeoutMs}ms`);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Narrow this locator to a single match by position. A negative index counts
|
|
1020
|
+
* back from the end, so `last()` stays one round trip.
|
|
1021
|
+
*/
|
|
1022
|
+
nth(index) {
|
|
1023
|
+
if (!Number.isInteger(index)) throw new Error("nth requires an integer index");
|
|
1024
|
+
return new _HasgardLocator(this.window, { ...this.query, index });
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Narrow this locator to the elements whose text matches.
|
|
1028
|
+
*
|
|
1029
|
+
* Filters accumulate, so `filter({hasText}).filter({hasNotText})` applies
|
|
1030
|
+
* both. Ordering matters against `nth`: filtering happens first, so
|
|
1031
|
+
* `filter({hasText: "x"}).first()` is the first *matching* element, which is
|
|
1032
|
+
* the reading that makes `first()` mean what it says.
|
|
1033
|
+
*/
|
|
1034
|
+
filter(options) {
|
|
1035
|
+
if (options.hasText === void 0 && options.hasNotText === void 0) {
|
|
1036
|
+
throw new Error("filter requires hasText or hasNotText");
|
|
1037
|
+
}
|
|
1038
|
+
return new _HasgardLocator(this.window, {
|
|
1039
|
+
...this.query,
|
|
1040
|
+
filters: [...this.query.filters ?? [], options]
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
first() {
|
|
1044
|
+
return this.nth(0);
|
|
1045
|
+
}
|
|
1046
|
+
last() {
|
|
1047
|
+
return this.nth(-1);
|
|
517
1048
|
}
|
|
518
1049
|
async resolveUnique() {
|
|
519
|
-
if (this.query.kind === "selector"
|
|
1050
|
+
if (this.query.kind === "selector" && !this.query.filters?.length) {
|
|
1051
|
+
return this.query.index === void 0 ? bySelector(this.query.selector) : { selector: this.query.selector, index: this.query.index };
|
|
1052
|
+
}
|
|
520
1053
|
const elements = await this.resolveAll();
|
|
1054
|
+
if (this.query.index !== void 0) {
|
|
1055
|
+
const element = elements[absoluteIndex(this.query.index, elements.length)];
|
|
1056
|
+
if (!element) {
|
|
1057
|
+
throw new Error(`Locator has no element at index ${this.query.index} (${elements.length} matched)`);
|
|
1058
|
+
}
|
|
1059
|
+
return byRef(element.ref);
|
|
1060
|
+
}
|
|
521
1061
|
if (elements.length !== 1) {
|
|
522
|
-
throw new Error(`
|
|
1062
|
+
throw new Error(`Locator resolved to ${elements.length} elements; expected exactly one`);
|
|
523
1063
|
}
|
|
524
1064
|
return byRef(elements[0].ref);
|
|
525
1065
|
}
|
|
@@ -527,8 +1067,16 @@ var HasgardLocator = class {
|
|
|
527
1067
|
return this.window.runExclusive(async () => operation(await this.resolveUnique()));
|
|
528
1068
|
}
|
|
529
1069
|
async resolveAll() {
|
|
1070
|
+
return this.applyFilters(await this.resolveBase());
|
|
1071
|
+
}
|
|
1072
|
+
async resolveBase() {
|
|
530
1073
|
if (this.query.kind === "selector") {
|
|
531
|
-
|
|
1074
|
+
return this.runQuery({ by: "selector", value: this.query.selector });
|
|
1075
|
+
}
|
|
1076
|
+
if (this.query.kind === "query") {
|
|
1077
|
+
const params = { by: this.query.by, value: this.query.value };
|
|
1078
|
+
if (this.query.query.exact !== void 0) params.exact = this.query.query.exact;
|
|
1079
|
+
return this.runQuery(params);
|
|
532
1080
|
}
|
|
533
1081
|
const query = this.query;
|
|
534
1082
|
const snapshot = await this.window.snapshot();
|
|
@@ -541,7 +1089,65 @@ var HasgardLocator = class {
|
|
|
541
1089
|
return query.query.exact ? element.name === expected : element.name.includes(expected);
|
|
542
1090
|
});
|
|
543
1091
|
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Apply each accumulated filter, one round trip per filter.
|
|
1094
|
+
*
|
|
1095
|
+
* Filters run server-side rather than against a snapshot because the text a
|
|
1096
|
+
* filter matches on is not carried in snapshot entries — deliberately, so
|
|
1097
|
+
* that every unrelated `snapshot` call stays small.
|
|
1098
|
+
*/
|
|
1099
|
+
async applyFilters(elements) {
|
|
1100
|
+
const filters = this.query.filters ?? [];
|
|
1101
|
+
let current = elements;
|
|
1102
|
+
for (const filter of filters) {
|
|
1103
|
+
if (current.length === 0) return current;
|
|
1104
|
+
const params = { refs: current.map((element) => element.ref) };
|
|
1105
|
+
if (filter.hasText !== void 0) params.hasText = filter.hasText;
|
|
1106
|
+
if (filter.hasNotText !== void 0) params.hasNotText = filter.hasNotText;
|
|
1107
|
+
if (filter.exact !== void 0) params.exact = filter.exact;
|
|
1108
|
+
current = this.parseElements(await this.window.call("filter", params), "filter");
|
|
1109
|
+
}
|
|
1110
|
+
return current;
|
|
1111
|
+
}
|
|
1112
|
+
async runQuery(params) {
|
|
1113
|
+
return this.parseElements(await this.window.call("query", params), "query");
|
|
1114
|
+
}
|
|
1115
|
+
parseElements(raw, source) {
|
|
1116
|
+
const value = expectRecord(raw, `${source} result`);
|
|
1117
|
+
if (!Array.isArray(value.elements)) throw new Error(`${source}.elements must be an array`);
|
|
1118
|
+
return value.elements.map((entry, index) => parseSnapshotElement(entry, index));
|
|
1119
|
+
}
|
|
544
1120
|
};
|
|
1121
|
+
function parseDialogPolicy(value) {
|
|
1122
|
+
const record = expectRecord(value, "dialog policy");
|
|
1123
|
+
const action = expectString(record.action, "dialog policy.action");
|
|
1124
|
+
if (action !== "accept" && action !== "dismiss") {
|
|
1125
|
+
throw new Error(`dialog policy.action must be "accept" or "dismiss", got ${action}`);
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
action,
|
|
1129
|
+
promptText: record.promptText == null ? null : expectString(record.promptText, "dialog policy.promptText")
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function parseDialogRecord(value, index) {
|
|
1133
|
+
const entry = expectRecord(value, `dialogs[${index}]`);
|
|
1134
|
+
const type = expectString(entry.type, `dialogs[${index}].type`);
|
|
1135
|
+
if (type !== "alert" && type !== "confirm" && type !== "prompt") {
|
|
1136
|
+
throw new Error(`dialogs[${index}].type must be alert, confirm, or prompt, got ${type}`);
|
|
1137
|
+
}
|
|
1138
|
+
const record = {
|
|
1139
|
+
id: expectNumber(entry.id, `dialogs[${index}].id`),
|
|
1140
|
+
timestamp: expectNumber(entry.timestamp, `dialogs[${index}].timestamp`),
|
|
1141
|
+
type,
|
|
1142
|
+
message: expectString(entry.message, `dialogs[${index}].message`),
|
|
1143
|
+
accepted: expectBoolean(entry.accepted, `dialogs[${index}].accepted`)
|
|
1144
|
+
};
|
|
1145
|
+
if (entry.defaultValue !== void 0) {
|
|
1146
|
+
record.defaultValue = expectString(entry.defaultValue, `dialogs[${index}].defaultValue`);
|
|
1147
|
+
}
|
|
1148
|
+
if (entry.returned !== void 0) record.returned = expectString(entry.returned, `dialogs[${index}].returned`);
|
|
1149
|
+
return record;
|
|
1150
|
+
}
|
|
545
1151
|
function parseSnapshotElement(value, index) {
|
|
546
1152
|
const entry = expectRecord(value, `snapshot.elements[${index}]`);
|
|
547
1153
|
const element = {
|
package/dist/types.d.ts
CHANGED
|
@@ -6,10 +6,17 @@ export type HasgardTarget = {
|
|
|
6
6
|
ref: string;
|
|
7
7
|
} | {
|
|
8
8
|
selector: string;
|
|
9
|
+
index?: number;
|
|
9
10
|
} | {
|
|
10
11
|
x: number;
|
|
11
12
|
y: number;
|
|
12
13
|
};
|
|
14
|
+
export interface BoundingBox {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
13
20
|
export interface SnapshotElement {
|
|
14
21
|
ref: string;
|
|
15
22
|
role: string;
|
|
@@ -31,6 +38,8 @@ export interface WindowInfo {
|
|
|
31
38
|
label: string;
|
|
32
39
|
url: string;
|
|
33
40
|
title: string;
|
|
41
|
+
/** Operating-system window id. Present only where native capture exists. */
|
|
42
|
+
nativeId?: number;
|
|
34
43
|
}
|
|
35
44
|
export interface WindowState {
|
|
36
45
|
url: string;
|
|
@@ -54,6 +63,202 @@ export interface WaitOptions {
|
|
|
54
63
|
state: "attached" | "detached";
|
|
55
64
|
timeoutMs: number;
|
|
56
65
|
}
|
|
66
|
+
export interface ScrollOptions {
|
|
67
|
+
direction: "up" | "down" | "left" | "right" | "top" | "bottom";
|
|
68
|
+
amount?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface DragOffset {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
}
|
|
74
|
+
export interface DropFile {
|
|
75
|
+
name: string;
|
|
76
|
+
/** Base64-encoded file contents. */
|
|
77
|
+
data: string;
|
|
78
|
+
type?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A file to hand to the page: either a path on the test machine, which is read
|
|
82
|
+
* and encoded for you, or an in-memory payload for content that never existed
|
|
83
|
+
* as a file.
|
|
84
|
+
*/
|
|
85
|
+
export type FileInput = string | DropFile;
|
|
86
|
+
/** Refinement applied to an existing locator. Both may be combined. */
|
|
87
|
+
export interface FilterOptions {
|
|
88
|
+
/** Keep only elements whose subtree text matches. */
|
|
89
|
+
hasText?: string;
|
|
90
|
+
/** Drop elements whose subtree text matches. */
|
|
91
|
+
hasNotText?: string;
|
|
92
|
+
/** Require a whole-string match instead of a substring one. */
|
|
93
|
+
exact?: boolean;
|
|
94
|
+
}
|
|
95
|
+
export type DialogType = "alert" | "confirm" | "prompt";
|
|
96
|
+
/** A modal the page opened while the standing policy decided its outcome. */
|
|
97
|
+
export interface DialogRecord {
|
|
98
|
+
id: number;
|
|
99
|
+
timestamp: number;
|
|
100
|
+
type: DialogType;
|
|
101
|
+
message: string;
|
|
102
|
+
/** Whether the policy accepted it. A one-button `alert` is always accepted. */
|
|
103
|
+
accepted: boolean;
|
|
104
|
+
/** The page's suggested answer, for `prompt` only. */
|
|
105
|
+
defaultValue?: string;
|
|
106
|
+
/** What the page received back, for an accepted `prompt`. */
|
|
107
|
+
returned?: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* How the next dialog will be answered.
|
|
111
|
+
*
|
|
112
|
+
* This is a standing policy rather than Playwright's per-event handler: the
|
|
113
|
+
* bridge must answer synchronously inside the page's `confirm()` call, with no
|
|
114
|
+
* chance to await a decision from the test process.
|
|
115
|
+
*/
|
|
116
|
+
export interface DialogPolicy {
|
|
117
|
+
action: "accept" | "dismiss";
|
|
118
|
+
promptText: string | null;
|
|
119
|
+
}
|
|
120
|
+
export interface DialogListing {
|
|
121
|
+
dialogs: DialogRecord[];
|
|
122
|
+
policy: DialogPolicy;
|
|
123
|
+
}
|
|
124
|
+
export interface ConsoleLogOptions {
|
|
125
|
+
level?: "log" | "warn" | "error" | "info";
|
|
126
|
+
since?: number;
|
|
127
|
+
sinceId?: number;
|
|
128
|
+
last?: number;
|
|
129
|
+
}
|
|
130
|
+
export interface ConsoleLogEntry {
|
|
131
|
+
id: number;
|
|
132
|
+
timestamp: number;
|
|
133
|
+
level: string;
|
|
134
|
+
args: JsonValue[];
|
|
135
|
+
source: string | null;
|
|
136
|
+
}
|
|
137
|
+
export interface NetworkRequestOptions {
|
|
138
|
+
filter?: string;
|
|
139
|
+
failedOnly?: boolean;
|
|
140
|
+
sinceId?: number;
|
|
141
|
+
last?: number;
|
|
142
|
+
}
|
|
143
|
+
export interface NetworkRequestEntry {
|
|
144
|
+
id: number;
|
|
145
|
+
timestamp: number;
|
|
146
|
+
method: string;
|
|
147
|
+
url: string;
|
|
148
|
+
status: number;
|
|
149
|
+
duration_ms: number;
|
|
150
|
+
error: string | null;
|
|
151
|
+
request_size: number;
|
|
152
|
+
response_size: number;
|
|
153
|
+
}
|
|
154
|
+
export interface StorageOptions {
|
|
155
|
+
/** Read and write `sessionStorage` instead of `localStorage`. */
|
|
156
|
+
session?: boolean;
|
|
157
|
+
}
|
|
158
|
+
export interface StorageEntry {
|
|
159
|
+
key: string;
|
|
160
|
+
value: string;
|
|
161
|
+
}
|
|
162
|
+
export interface StorageListing {
|
|
163
|
+
entries: StorageEntry[];
|
|
164
|
+
truncated: boolean;
|
|
165
|
+
}
|
|
166
|
+
export interface FormField {
|
|
167
|
+
tag: string;
|
|
168
|
+
type: string | null;
|
|
169
|
+
name: string;
|
|
170
|
+
value: string | string[];
|
|
171
|
+
checked?: boolean;
|
|
172
|
+
}
|
|
173
|
+
export interface FormEntry {
|
|
174
|
+
id: string;
|
|
175
|
+
name: string;
|
|
176
|
+
action: string;
|
|
177
|
+
method: string;
|
|
178
|
+
fields: FormField[];
|
|
179
|
+
fieldsTruncated?: boolean;
|
|
180
|
+
}
|
|
181
|
+
export interface FormsDump {
|
|
182
|
+
forms: FormEntry[];
|
|
183
|
+
truncated: boolean;
|
|
184
|
+
}
|
|
185
|
+
export type QueryDimension = "text" | "label" | "placeholder" | "testid" | "alt" | "title";
|
|
186
|
+
export interface TextQuery {
|
|
187
|
+
/** Full, case-sensitive match. Whitespace is normalized either way. */
|
|
188
|
+
exact?: boolean;
|
|
189
|
+
}
|
|
190
|
+
export interface WaitForFunctionOptions {
|
|
191
|
+
timeoutMs?: number;
|
|
192
|
+
/** How often to re-evaluate the expression. Defaults to 50ms in the bridge. */
|
|
193
|
+
pollMs?: number;
|
|
194
|
+
}
|
|
195
|
+
export interface WatchOptions {
|
|
196
|
+
selector?: string;
|
|
197
|
+
timeoutMs?: number;
|
|
198
|
+
stableMs?: number;
|
|
199
|
+
requireMutation?: boolean;
|
|
200
|
+
}
|
|
201
|
+
export interface WatchNode {
|
|
202
|
+
tag: string;
|
|
203
|
+
id?: string;
|
|
204
|
+
class?: string;
|
|
205
|
+
text?: string;
|
|
206
|
+
attr?: string;
|
|
207
|
+
value?: string;
|
|
208
|
+
}
|
|
209
|
+
export interface WatchChanges {
|
|
210
|
+
added: WatchNode[];
|
|
211
|
+
removed: WatchNode[];
|
|
212
|
+
modified: WatchNode[];
|
|
213
|
+
truncated: boolean;
|
|
214
|
+
}
|
|
215
|
+
export interface DiffOptions extends SnapshotOptions {
|
|
216
|
+
/** Compare against this snapshot instead of the plugin's last stored one. */
|
|
217
|
+
reference?: Snapshot;
|
|
218
|
+
}
|
|
219
|
+
export interface DiffChange {
|
|
220
|
+
old: SnapshotElement;
|
|
221
|
+
new: SnapshotElement;
|
|
222
|
+
changes: string[];
|
|
223
|
+
}
|
|
224
|
+
export interface SnapshotDiff {
|
|
225
|
+
added: SnapshotElement[];
|
|
226
|
+
removed: SnapshotElement[];
|
|
227
|
+
changed: DiffChange[];
|
|
228
|
+
}
|
|
229
|
+
export interface NativeScreenshotOptions {
|
|
230
|
+
/** Absolute path; the parent directory must already exist. */
|
|
231
|
+
outputPath: string;
|
|
232
|
+
/**
|
|
233
|
+
* Operating-system window id. Resolved from the window's own label when
|
|
234
|
+
* omitted; pass it only to capture a window Hasgard does not own.
|
|
235
|
+
*/
|
|
236
|
+
windowId?: number;
|
|
237
|
+
}
|
|
238
|
+
export interface NativeScreenshot {
|
|
239
|
+
output_path: string;
|
|
240
|
+
window_id: number;
|
|
241
|
+
width: number;
|
|
242
|
+
height: number;
|
|
243
|
+
scale_factor: number;
|
|
244
|
+
byte_size: number;
|
|
245
|
+
backend: string;
|
|
246
|
+
tcc_denied: boolean;
|
|
247
|
+
}
|
|
248
|
+
export interface RecorderEntry {
|
|
249
|
+
action: string;
|
|
250
|
+
timestamp: number;
|
|
251
|
+
[key: string]: JsonValue;
|
|
252
|
+
}
|
|
253
|
+
export interface RecorderStatus {
|
|
254
|
+
active: boolean;
|
|
255
|
+
count: number;
|
|
256
|
+
elapsed_ms: number;
|
|
257
|
+
}
|
|
258
|
+
export interface RecorderResult {
|
|
259
|
+
entries: RecorderEntry[];
|
|
260
|
+
count: number;
|
|
261
|
+
}
|
|
57
262
|
export interface HasgardLaunchConfig {
|
|
58
263
|
command: string;
|
|
59
264
|
args: string[];
|
package/dist/window.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HasgardRpcClient } from "./rpc-client.js";
|
|
2
|
-
import type { HasgardTarget, JsonValue, RoleQuery, Snapshot, SnapshotOptions, WaitOptions, WindowInfo, WindowState } from "./types.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";
|
|
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;
|
|
@@ -14,6 +14,16 @@ export declare class HasgardApplication {
|
|
|
14
14
|
waitForWindow(label: string, timeoutMs: number): Promise<HasgardWindow>;
|
|
15
15
|
waitForWindowReady(label: string, readySelector: string, timeoutMs: number): Promise<HasgardWindow>;
|
|
16
16
|
window(label: string): HasgardWindow;
|
|
17
|
+
/**
|
|
18
|
+
* Action recorder. Captures the RPC calls this connection makes into a
|
|
19
|
+
* replayable scenario — this is not video capture.
|
|
20
|
+
*/
|
|
21
|
+
readonly recorder: {
|
|
22
|
+
start: () => Promise<void>;
|
|
23
|
+
stop: () => Promise<RecorderResult>;
|
|
24
|
+
status: () => Promise<RecorderStatus>;
|
|
25
|
+
add: (entry: RecorderEntry) => Promise<void>;
|
|
26
|
+
};
|
|
17
27
|
}
|
|
18
28
|
export declare class HasgardWindow {
|
|
19
29
|
readonly label: string;
|
|
@@ -21,22 +31,116 @@ export declare class HasgardWindow {
|
|
|
21
31
|
constructor(rpc: HasgardRpcClient, label: string);
|
|
22
32
|
locator(selector: string): HasgardLocator;
|
|
23
33
|
getByRole(role: string, query?: RoleQuery): HasgardLocator;
|
|
34
|
+
/**
|
|
35
|
+
* Locate by visible text. Matching runs in the page against the live DOM, not
|
|
36
|
+
* against a snapshot's `name`: that field collapses aria-label, alt, `<label>`,
|
|
37
|
+
* placeholder, and text into one 50-character string, so a button with an
|
|
38
|
+
* aria-label would never be findable by the text it actually shows.
|
|
39
|
+
*/
|
|
40
|
+
getByText(text: string, query?: TextQuery): HasgardLocator;
|
|
41
|
+
getByLabel(text: string, query?: TextQuery): HasgardLocator;
|
|
42
|
+
getByPlaceholder(text: string, query?: TextQuery): HasgardLocator;
|
|
43
|
+
getByAltText(text: string, query?: TextQuery): HasgardLocator;
|
|
44
|
+
getByTitle(text: string, query?: TextQuery): HasgardLocator;
|
|
45
|
+
/** Test ids always match exactly, so `save` cannot select `save-draft`. */
|
|
46
|
+
getByTestId(testId: string): HasgardLocator;
|
|
24
47
|
snapshot(options?: SnapshotOptions): Promise<Snapshot>;
|
|
25
48
|
state(): Promise<WindowState>;
|
|
49
|
+
url(): Promise<string>;
|
|
50
|
+
title(): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Navigate this webview by assigning `location.href`. Unlike Playwright's
|
|
53
|
+
* `goto`, this does not wait for the load to finish and returns no response —
|
|
54
|
+
* follow it with `waitFor` or `waitForWindowReady`.
|
|
55
|
+
*/
|
|
56
|
+
navigate(url: string): Promise<void>;
|
|
57
|
+
/** Scroll the document by a relative amount, or to `top`/`bottom`. */
|
|
58
|
+
scrollBy(options: ScrollOptions): Promise<void>;
|
|
26
59
|
press(key: string): Promise<void>;
|
|
60
|
+
diff(options?: DiffOptions): Promise<SnapshotDiff>;
|
|
61
|
+
/**
|
|
62
|
+
* Poll a JavaScript expression in the webview until it returns a truthy
|
|
63
|
+
* value, and return that value. The expression is polled rather than driven
|
|
64
|
+
* by DOM mutations, so predicates that flip on a timer or a resolved fetch
|
|
65
|
+
* are caught too. A predicate that throws rejects instead of being retried.
|
|
66
|
+
*/
|
|
67
|
+
waitForFunction<T = JsonValue>(expression: string, options?: WaitForFunctionOptions): Promise<T>;
|
|
68
|
+
/** Observe DOM mutations until they settle, or reject when nothing changes. */
|
|
69
|
+
watch(options?: WatchOptions): Promise<WatchChanges>;
|
|
70
|
+
consoleLogs(options?: ConsoleLogOptions): Promise<ConsoleLogEntry[]>;
|
|
71
|
+
clearConsole(): Promise<void>;
|
|
72
|
+
networkRequests(options?: NetworkRequestOptions): Promise<NetworkRequestEntry[]>;
|
|
73
|
+
clearNetwork(): Promise<void>;
|
|
74
|
+
/** `localStorage` by default; pass `{ session: true }` for `sessionStorage`. */
|
|
75
|
+
readonly storage: {
|
|
76
|
+
get: (key: string, options?: StorageOptions) => Promise<string | null>;
|
|
77
|
+
set: (key: string, value: string, options?: StorageOptions) => Promise<void>;
|
|
78
|
+
list: (options?: StorageOptions) => Promise<StorageListing>;
|
|
79
|
+
clear: (options?: StorageOptions) => Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
forms(selector?: string): Promise<FormsDump>;
|
|
27
82
|
evaluate<T>(script: string): Promise<T>;
|
|
28
83
|
invoke<T>(command: string, args: Record<string, JsonValue>): Promise<T>;
|
|
29
84
|
screenshot(selector?: string): Promise<Buffer>;
|
|
85
|
+
/**
|
|
86
|
+
* Capture this window through the native compositor, writing a PNG to
|
|
87
|
+
* `outputPath`. macOS only.
|
|
88
|
+
*
|
|
89
|
+
* Unlike `screenshot`, which renders the DOM inside the page, this records
|
|
90
|
+
* what the window server actually shows — so it sees native chrome, layered
|
|
91
|
+
* content, and anything the DOM renderer cannot reproduce.
|
|
92
|
+
*
|
|
93
|
+
* The operating-system window id is resolved from this window's label, so
|
|
94
|
+
* callers do not have to discover it themselves.
|
|
95
|
+
*/
|
|
96
|
+
screenshotNative(options: NativeScreenshotOptions): Promise<NativeScreenshot>;
|
|
97
|
+
/**
|
|
98
|
+
* Scroll the page by wheel.
|
|
99
|
+
*
|
|
100
|
+
* The document sees a real `wheel` event first and may cancel it; the scroll
|
|
101
|
+
* only follows if it does not. Resolves to whether the page cancelled — which
|
|
102
|
+
* is how you tell "the app intercepted my wheel" from "nothing was
|
|
103
|
+
* listening". Use the locator method to scroll a nested scrollport.
|
|
104
|
+
*/
|
|
105
|
+
wheel(deltaX: number, deltaY: number): Promise<boolean>;
|
|
106
|
+
/**
|
|
107
|
+
* The page's modal dialogs (`alert`, `confirm`, `prompt`).
|
|
108
|
+
*
|
|
109
|
+
* These block the webview until answered, so the bridge always answers them —
|
|
110
|
+
* dismissing by default, as Playwright does. A test that never touches this
|
|
111
|
+
* namespace still cannot hang on a `confirm()`, and can read back what the
|
|
112
|
+
* app tried to ask.
|
|
113
|
+
*/
|
|
114
|
+
readonly dialogs: {
|
|
115
|
+
/** Answer every subsequent dialog with OK. `promptText` fills a `prompt`. */
|
|
116
|
+
accept: (promptText?: string) => Promise<DialogPolicy>;
|
|
117
|
+
/** Answer every subsequent dialog with Cancel. This is the default. */
|
|
118
|
+
dismiss: () => Promise<DialogPolicy>;
|
|
119
|
+
/** Every dialog the page has opened so far, oldest first. */
|
|
120
|
+
list: () => Promise<DialogListing>;
|
|
121
|
+
/** Forget the recorded dialogs. Leaves the accept/dismiss policy alone. */
|
|
122
|
+
clear: () => Promise<void>;
|
|
123
|
+
};
|
|
124
|
+
/** This window's operating-system window id, where the platform exposes one. */
|
|
125
|
+
nativeId(): Promise<number>;
|
|
30
126
|
call<T>(method: string, params?: Record<string, JsonValue>): Promise<T>;
|
|
31
127
|
runExclusive<T>(operation: () => Promise<T>): Promise<T>;
|
|
32
128
|
}
|
|
33
|
-
type LocatorQuery = {
|
|
129
|
+
type LocatorQuery = ({
|
|
34
130
|
kind: "selector";
|
|
35
131
|
selector: string;
|
|
36
132
|
} | {
|
|
37
133
|
kind: "role";
|
|
38
134
|
role: string;
|
|
39
135
|
query: RoleQuery;
|
|
136
|
+
} | {
|
|
137
|
+
kind: "query";
|
|
138
|
+
by: QueryDimension;
|
|
139
|
+
value: string;
|
|
140
|
+
query: TextQuery;
|
|
141
|
+
}) & {
|
|
142
|
+
index?: number;
|
|
143
|
+
filters?: FilterOptions[];
|
|
40
144
|
};
|
|
41
145
|
export declare class HasgardLocator {
|
|
42
146
|
readonly window: HasgardWindow;
|
|
@@ -45,17 +149,84 @@ export declare class HasgardLocator {
|
|
|
45
149
|
click(): Promise<void>;
|
|
46
150
|
fill(value: string): Promise<void>;
|
|
47
151
|
type(text: string): Promise<void>;
|
|
152
|
+
/** Empty the control, firing `input` and `change` as `fill` does. */
|
|
153
|
+
clear(): Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* Select files on an `<input type="file">` without a native file chooser,
|
|
156
|
+
* which an embedded webview gives no way to drive.
|
|
157
|
+
*
|
|
158
|
+
* Strings are read from the test machine's disk; `DropFile` payloads let you
|
|
159
|
+
* supply content that never existed as a file. An empty array deselects.
|
|
160
|
+
*/
|
|
161
|
+
setInputFiles(files: FileInput | FileInput[]): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Scroll this element by wheel.
|
|
164
|
+
*
|
|
165
|
+
* The page sees a real `wheel` event first and may cancel it; the scroll only
|
|
166
|
+
* follows if it does not — the same two steps a physical wheel produces.
|
|
167
|
+
* Resolves to whether the page cancelled.
|
|
168
|
+
*/
|
|
169
|
+
wheel(deltaX: number, deltaY: number): Promise<boolean>;
|
|
48
170
|
selectOption(value: string): Promise<void>;
|
|
171
|
+
/** Drive the checkbox to checked. A no-op when it is already checked. */
|
|
49
172
|
check(): Promise<void>;
|
|
173
|
+
/** Drive the checkbox to unchecked. A no-op when it is already unchecked. */
|
|
174
|
+
uncheck(): Promise<void>;
|
|
175
|
+
/** Flip the checkbox, whatever its current state. */
|
|
176
|
+
toggle(): Promise<void>;
|
|
177
|
+
hover(): Promise<void>;
|
|
178
|
+
dblclick(): Promise<void>;
|
|
179
|
+
focus(): Promise<void>;
|
|
180
|
+
blur(): Promise<void>;
|
|
181
|
+
/** Scroll this element's own scrollport. */
|
|
182
|
+
scrollBy(options: ScrollOptions): Promise<void>;
|
|
183
|
+
boundingBox(): Promise<BoundingBox>;
|
|
184
|
+
isDisabled(): Promise<boolean>;
|
|
185
|
+
isEnabled(): Promise<boolean>;
|
|
50
186
|
textContent(): Promise<string>;
|
|
51
187
|
inputValue(): Promise<string>;
|
|
188
|
+
innerHTML(): Promise<string>;
|
|
189
|
+
getAttribute(name: string): Promise<string | null>;
|
|
190
|
+
/** Drag this element onto another one in the same window. */
|
|
191
|
+
dragTo(target: HasgardLocator): Promise<void>;
|
|
192
|
+
/** Drag this element by a pixel offset from its centre. */
|
|
193
|
+
dragBy(offset: DragOffset): Promise<void>;
|
|
194
|
+
/** Dispatch a file drop onto this element. `data` is base64-encoded. */
|
|
195
|
+
dropFiles(files: DropFile[]): Promise<void>;
|
|
52
196
|
attributes(): Promise<Record<string, string>>;
|
|
53
197
|
isVisible(): Promise<boolean>;
|
|
54
198
|
isChecked(): Promise<boolean>;
|
|
55
199
|
count(): Promise<number>;
|
|
56
200
|
waitFor(options: WaitOptions): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* Narrow this locator to a single match by position. A negative index counts
|
|
203
|
+
* back from the end, so `last()` stays one round trip.
|
|
204
|
+
*/
|
|
205
|
+
nth(index: number): HasgardLocator;
|
|
206
|
+
/**
|
|
207
|
+
* Narrow this locator to the elements whose text matches.
|
|
208
|
+
*
|
|
209
|
+
* Filters accumulate, so `filter({hasText}).filter({hasNotText})` applies
|
|
210
|
+
* both. Ordering matters against `nth`: filtering happens first, so
|
|
211
|
+
* `filter({hasText: "x"}).first()` is the first *matching* element, which is
|
|
212
|
+
* the reading that makes `first()` mean what it says.
|
|
213
|
+
*/
|
|
214
|
+
filter(options: FilterOptions): HasgardLocator;
|
|
215
|
+
first(): HasgardLocator;
|
|
216
|
+
last(): HasgardLocator;
|
|
57
217
|
private resolveUnique;
|
|
58
218
|
private withTarget;
|
|
59
219
|
private resolveAll;
|
|
220
|
+
private resolveBase;
|
|
221
|
+
/**
|
|
222
|
+
* Apply each accumulated filter, one round trip per filter.
|
|
223
|
+
*
|
|
224
|
+
* Filters run server-side rather than against a snapshot because the text a
|
|
225
|
+
* filter matches on is not carried in snapshot entries — deliberately, so
|
|
226
|
+
* that every unrelated `snapshot` call stays small.
|
|
227
|
+
*/
|
|
228
|
+
private applyFilters;
|
|
229
|
+
private runQuery;
|
|
230
|
+
private parseElements;
|
|
60
231
|
}
|
|
61
232
|
export {};
|