@srsholmes/tauri-playwright 0.1.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/README.md +483 -0
- package/dist/index.d.ts +935 -0
- package/dist/index.js +1732 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1732 @@
|
|
|
1
|
+
// src/fixture.ts
|
|
2
|
+
import { test as base, chromium } from "@playwright/test";
|
|
3
|
+
|
|
4
|
+
// src/expect.ts
|
|
5
|
+
import { expect as baseExpect } from "@playwright/test";
|
|
6
|
+
async function pollUntil(fn, check, timeout, interval = 100) {
|
|
7
|
+
const deadline = Date.now() + timeout;
|
|
8
|
+
let lastValue = void 0;
|
|
9
|
+
while (Date.now() < deadline) {
|
|
10
|
+
try {
|
|
11
|
+
lastValue = await fn();
|
|
12
|
+
if (check(lastValue)) return { pass: true, value: lastValue };
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
16
|
+
}
|
|
17
|
+
return { pass: false, value: lastValue };
|
|
18
|
+
}
|
|
19
|
+
var DEFAULT_TIMEOUT = 5e3;
|
|
20
|
+
function getTimeout(options) {
|
|
21
|
+
return options?.timeout ?? DEFAULT_TIMEOUT;
|
|
22
|
+
}
|
|
23
|
+
var tauriExpect = baseExpect.extend({
|
|
24
|
+
// ── State assertions ──────────────────────────────────────────────
|
|
25
|
+
async toBeVisible(locator, options) {
|
|
26
|
+
const assertionName = "toBeVisible";
|
|
27
|
+
const timeout = getTimeout(options);
|
|
28
|
+
const { pass } = await pollUntil(
|
|
29
|
+
() => locator.isVisible(),
|
|
30
|
+
(v) => v === true,
|
|
31
|
+
timeout
|
|
32
|
+
);
|
|
33
|
+
return {
|
|
34
|
+
pass,
|
|
35
|
+
name: assertionName,
|
|
36
|
+
message: () => this.isNot ? `expected element not to be visible` : `expected element to be visible within ${timeout}ms`
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
async toBeHidden(locator, options) {
|
|
40
|
+
const timeout = getTimeout(options);
|
|
41
|
+
const { pass } = await pollUntil(
|
|
42
|
+
() => locator.isHidden(),
|
|
43
|
+
(v) => v === true,
|
|
44
|
+
timeout
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
pass,
|
|
48
|
+
name: "toBeHidden",
|
|
49
|
+
message: () => this.isNot ? `expected element not to be hidden` : `expected element to be hidden within ${timeout}ms`
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async toBeEnabled(locator, options) {
|
|
53
|
+
const timeout = getTimeout(options);
|
|
54
|
+
const { pass } = await pollUntil(
|
|
55
|
+
() => locator.isEnabled(),
|
|
56
|
+
(v) => v === true,
|
|
57
|
+
timeout
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
pass,
|
|
61
|
+
name: "toBeEnabled",
|
|
62
|
+
message: () => this.isNot ? `expected element not to be enabled` : `expected element to be enabled within ${timeout}ms`
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
async toBeDisabled(locator, options) {
|
|
66
|
+
const timeout = getTimeout(options);
|
|
67
|
+
const { pass } = await pollUntil(
|
|
68
|
+
() => locator.isDisabled(),
|
|
69
|
+
(v) => v === true,
|
|
70
|
+
timeout
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
pass,
|
|
74
|
+
name: "toBeDisabled",
|
|
75
|
+
message: () => this.isNot ? `expected element not to be disabled` : `expected element to be disabled within ${timeout}ms`
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
async toBeEditable(locator, options) {
|
|
79
|
+
const timeout = getTimeout(options);
|
|
80
|
+
const { pass } = await pollUntil(
|
|
81
|
+
() => locator.isEditable(),
|
|
82
|
+
(v) => v === true,
|
|
83
|
+
timeout
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
pass,
|
|
87
|
+
name: "toBeEditable",
|
|
88
|
+
message: () => this.isNot ? `expected element not to be editable` : `expected element to be editable within ${timeout}ms`
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
async toBeChecked(locator, options) {
|
|
92
|
+
const timeout = getTimeout(options);
|
|
93
|
+
const { pass } = await pollUntil(
|
|
94
|
+
() => locator.isChecked(),
|
|
95
|
+
(v) => v === true,
|
|
96
|
+
timeout
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
pass,
|
|
100
|
+
name: "toBeChecked",
|
|
101
|
+
message: () => this.isNot ? `expected element not to be checked` : `expected element to be checked within ${timeout}ms`
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
async toBeAttached(locator, options) {
|
|
105
|
+
const timeout = getTimeout(options);
|
|
106
|
+
const { pass } = await pollUntil(
|
|
107
|
+
() => locator.count(),
|
|
108
|
+
(c) => c > 0,
|
|
109
|
+
timeout
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
pass,
|
|
113
|
+
name: "toBeAttached",
|
|
114
|
+
message: () => this.isNot ? `expected element not to be attached` : `expected element to be attached within ${timeout}ms`
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
async toBeEmpty(locator, options) {
|
|
118
|
+
const timeout = getTimeout(options);
|
|
119
|
+
const { pass, value } = await pollUntil(
|
|
120
|
+
async () => {
|
|
121
|
+
try {
|
|
122
|
+
return await locator.inputValue();
|
|
123
|
+
} catch {
|
|
124
|
+
return await locator.textContent() ?? "";
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
(v) => v.trim() === "",
|
|
128
|
+
timeout
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
pass,
|
|
132
|
+
name: "toBeEmpty",
|
|
133
|
+
message: () => this.isNot ? `expected element not to be empty, got "${value}"` : `expected element to be empty, got "${value}"`
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
// ── Content assertions ────────────────────────────────────────────
|
|
137
|
+
async toContainText(locator, expected, options) {
|
|
138
|
+
const timeout = getTimeout(options);
|
|
139
|
+
const { pass, value } = await pollUntil(
|
|
140
|
+
() => locator.textContent(),
|
|
141
|
+
(text) => {
|
|
142
|
+
if (!text) return false;
|
|
143
|
+
return typeof expected === "string" ? text.includes(expected) : expected.test(text);
|
|
144
|
+
},
|
|
145
|
+
timeout
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
pass,
|
|
149
|
+
name: "toContainText",
|
|
150
|
+
message: () => this.isNot ? `expected "${value}" not to contain "${expected}"` : `expected "${value}" to contain "${expected}"`
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
async toHaveText(locator, expected, options) {
|
|
154
|
+
const timeout = getTimeout(options);
|
|
155
|
+
const { pass, value } = await pollUntil(
|
|
156
|
+
async () => (await locator.textContent() ?? "").trim(),
|
|
157
|
+
(text) => {
|
|
158
|
+
return typeof expected === "string" ? text === expected : expected.test(text);
|
|
159
|
+
},
|
|
160
|
+
timeout
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
pass,
|
|
164
|
+
name: "toHaveText",
|
|
165
|
+
message: () => this.isNot ? `expected "${value}" not to equal "${expected}"` : `expected "${value}" to equal "${expected}"`
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
async toHaveValue(locator, expected, options) {
|
|
169
|
+
const timeout = getTimeout(options);
|
|
170
|
+
const { pass, value } = await pollUntil(
|
|
171
|
+
() => locator.inputValue(),
|
|
172
|
+
(val) => typeof expected === "string" ? val === expected : expected.test(val),
|
|
173
|
+
timeout
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
pass,
|
|
177
|
+
name: "toHaveValue",
|
|
178
|
+
message: () => this.isNot ? `expected value "${value}" not to be "${expected}"` : `expected value "${value}" to be "${expected}"`
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
// ── Attribute/CSS assertions ──────────────────────────────────────
|
|
182
|
+
async toHaveAttribute(locator, name, value, options) {
|
|
183
|
+
const timeout = getTimeout(options);
|
|
184
|
+
const { pass, value: actual } = await pollUntil(
|
|
185
|
+
() => locator.getAttribute(name),
|
|
186
|
+
(attr) => {
|
|
187
|
+
if (attr === null) return false;
|
|
188
|
+
if (value === void 0) return true;
|
|
189
|
+
return typeof value === "string" ? attr === value : value.test(attr);
|
|
190
|
+
},
|
|
191
|
+
timeout
|
|
192
|
+
);
|
|
193
|
+
return {
|
|
194
|
+
pass,
|
|
195
|
+
name: "toHaveAttribute",
|
|
196
|
+
message: () => this.isNot ? `expected attribute "${name}" not to be "${actual}"` : `expected attribute "${name}" to be "${value}", got "${actual}"`
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
async toHaveClass(locator, expected, options) {
|
|
200
|
+
const timeout = getTimeout(options);
|
|
201
|
+
const { pass, value } = await pollUntil(
|
|
202
|
+
() => locator.getAttribute("class"),
|
|
203
|
+
(cls) => {
|
|
204
|
+
if (!cls) return false;
|
|
205
|
+
return typeof expected === "string" ? cls.split(/\s+/).includes(expected) : expected.test(cls);
|
|
206
|
+
},
|
|
207
|
+
timeout
|
|
208
|
+
);
|
|
209
|
+
return {
|
|
210
|
+
pass,
|
|
211
|
+
name: "toHaveClass",
|
|
212
|
+
message: () => this.isNot ? `expected class "${value}" not to contain "${expected}"` : `expected class "${value}" to contain "${expected}"`
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
async toHaveId(locator, expected, options) {
|
|
216
|
+
const timeout = getTimeout(options);
|
|
217
|
+
const { pass, value } = await pollUntil(
|
|
218
|
+
() => locator.getAttribute("id"),
|
|
219
|
+
(id) => id === expected,
|
|
220
|
+
timeout
|
|
221
|
+
);
|
|
222
|
+
return {
|
|
223
|
+
pass,
|
|
224
|
+
name: "toHaveId",
|
|
225
|
+
message: () => this.isNot ? `expected id "${value}" not to be "${expected}"` : `expected id "${value}" to be "${expected}"`
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
// ── Collection assertions ─────────────────────────────────────────
|
|
229
|
+
async toHaveCount(locator, expected, options) {
|
|
230
|
+
const timeout = getTimeout(options);
|
|
231
|
+
const { pass, value } = await pollUntil(
|
|
232
|
+
() => locator.count(),
|
|
233
|
+
(c) => c === expected,
|
|
234
|
+
timeout
|
|
235
|
+
);
|
|
236
|
+
return {
|
|
237
|
+
pass,
|
|
238
|
+
name: "toHaveCount",
|
|
239
|
+
message: () => this.isNot ? `expected count ${value} not to be ${expected}` : `expected count ${value} to be ${expected}`
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
// ── Focus assertion ───────────────────────────────────────────────
|
|
243
|
+
async toBeFocused(locator, options) {
|
|
244
|
+
const timeout = getTimeout(options);
|
|
245
|
+
const fn = locator.isFocused ? () => locator.isFocused() : async () => false;
|
|
246
|
+
const { pass } = await pollUntil(fn, (v) => v === true, timeout);
|
|
247
|
+
return {
|
|
248
|
+
pass,
|
|
249
|
+
name: "toBeFocused",
|
|
250
|
+
message: () => this.isNot ? `expected element not to be focused` : `expected element to be focused within ${timeout}ms`
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
// ── CSS assertion ─────────────────────────────────────────────────
|
|
254
|
+
async toHaveCSS(locator, property, expected, options) {
|
|
255
|
+
const timeout = getTimeout(options);
|
|
256
|
+
const { pass, value } = await pollUntil(
|
|
257
|
+
() => locator.getAttribute("style").then(() => {
|
|
258
|
+
if ("evaluate" in locator && typeof locator.evaluate === "function") {
|
|
259
|
+
return locator.evaluate(
|
|
260
|
+
`(el) => getComputedStyle(el).getPropertyValue(${JSON.stringify(property)})`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return Promise.resolve("");
|
|
264
|
+
}),
|
|
265
|
+
(val) => {
|
|
266
|
+
if (!val) return false;
|
|
267
|
+
return typeof expected === "string" ? val.trim() === expected : expected.test(val);
|
|
268
|
+
},
|
|
269
|
+
timeout
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
pass,
|
|
273
|
+
name: "toHaveCSS",
|
|
274
|
+
message: () => this.isNot ? `expected CSS "${property}" not to be "${expected}", got "${value}"` : `expected CSS "${property}" to be "${expected}", got "${value}"`
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
// ── Page-level assertions ─────────────────────────────────────────
|
|
278
|
+
async toHaveURL(page, expected, options) {
|
|
279
|
+
const timeout = getTimeout(options);
|
|
280
|
+
const { pass, value } = await pollUntil(
|
|
281
|
+
() => page.url(),
|
|
282
|
+
(url) => typeof expected === "string" ? url.includes(expected) : expected.test(url),
|
|
283
|
+
timeout
|
|
284
|
+
);
|
|
285
|
+
return {
|
|
286
|
+
pass,
|
|
287
|
+
name: "toHaveURL",
|
|
288
|
+
message: () => this.isNot ? `expected URL "${value}" not to match "${expected}"` : `expected URL "${value}" to match "${expected}"`
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
async toHaveTitle(page, expected, options) {
|
|
292
|
+
const timeout = getTimeout(options);
|
|
293
|
+
const { pass, value } = await pollUntil(
|
|
294
|
+
() => page.title(),
|
|
295
|
+
(title) => typeof expected === "string" ? title === expected : expected.test(title),
|
|
296
|
+
timeout
|
|
297
|
+
);
|
|
298
|
+
return {
|
|
299
|
+
pass,
|
|
300
|
+
name: "toHaveTitle",
|
|
301
|
+
message: () => this.isNot ? `expected title "${value}" not to match "${expected}"` : `expected title "${value}" to match "${expected}"`
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// src/ipc-mock.ts
|
|
307
|
+
function generateIpcMockScript(mocks) {
|
|
308
|
+
const mockEntries = Object.entries(mocks).map(([cmd, handler]) => {
|
|
309
|
+
return ` ${JSON.stringify(cmd)}: ${handler.toString()}`;
|
|
310
|
+
});
|
|
311
|
+
return `
|
|
312
|
+
(function() {
|
|
313
|
+
"use strict";
|
|
314
|
+
|
|
315
|
+
var mockHandlers = {
|
|
316
|
+
${mockEntries.join(",\n")}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Track all invoke calls for test assertions
|
|
320
|
+
window.__TAURI_MOCK_CALLS__ = [];
|
|
321
|
+
|
|
322
|
+
// Track event listeners for mock event emission
|
|
323
|
+
window.__TAURI_MOCK_LISTENERS__ = {};
|
|
324
|
+
|
|
325
|
+
// Internal invoke handler
|
|
326
|
+
function handleInvoke(cmd, args) {
|
|
327
|
+
// Intercept event plugin commands
|
|
328
|
+
if (cmd === "plugin:event|listen") {
|
|
329
|
+
var event = args && args.event;
|
|
330
|
+
var handler = args && args.handler;
|
|
331
|
+
if (event && handler) {
|
|
332
|
+
if (!window.__TAURI_MOCK_LISTENERS__[event]) {
|
|
333
|
+
window.__TAURI_MOCK_LISTENERS__[event] = [];
|
|
334
|
+
}
|
|
335
|
+
window.__TAURI_MOCK_LISTENERS__[event].push(handler);
|
|
336
|
+
}
|
|
337
|
+
return Promise.resolve(Math.floor(Math.random() * 1000000));
|
|
338
|
+
}
|
|
339
|
+
if (cmd === "plugin:event|unlisten") {
|
|
340
|
+
return Promise.resolve();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Record the call
|
|
344
|
+
window.__TAURI_MOCK_CALLS__.push({
|
|
345
|
+
cmd: cmd,
|
|
346
|
+
args: args || {},
|
|
347
|
+
timestamp: Date.now()
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Return mock response \u2014 call the handler with args for dynamic mocks
|
|
351
|
+
if (cmd in mockHandlers) {
|
|
352
|
+
try {
|
|
353
|
+
var response = mockHandlers[cmd](args);
|
|
354
|
+
return Promise.resolve(
|
|
355
|
+
response !== null && typeof response === "object"
|
|
356
|
+
? JSON.parse(JSON.stringify(response))
|
|
357
|
+
: response
|
|
358
|
+
);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.error("[tauri-playwright] Mock handler error for", cmd, e);
|
|
361
|
+
return Promise.reject(e);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.warn("[tauri-playwright] Unhandled invoke:", cmd, args);
|
|
366
|
+
return Promise.resolve(null);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Set up the mock global
|
|
370
|
+
window.__TAURI_INTERNALS__ = {
|
|
371
|
+
invoke: handleInvoke,
|
|
372
|
+
convertFileSrc: function(path) { return path; },
|
|
373
|
+
transformCallback: function(callback) {
|
|
374
|
+
var id = Math.random().toString(36).slice(2);
|
|
375
|
+
window["_" + id] = callback;
|
|
376
|
+
return id;
|
|
377
|
+
},
|
|
378
|
+
metadata: {
|
|
379
|
+
currentWindow: { label: "main" },
|
|
380
|
+
currentWebview: { label: "main" }
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Helper: emit a mock Tauri event from test code
|
|
385
|
+
window.__TAURI_EMIT_MOCK_EVENT__ = function(event, payload) {
|
|
386
|
+
var listeners = window.__TAURI_MOCK_LISTENERS__[event] || [];
|
|
387
|
+
listeners.forEach(function(handlerId) {
|
|
388
|
+
var callback = window["_" + handlerId];
|
|
389
|
+
if (callback) {
|
|
390
|
+
callback({ event: event, payload: payload });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// Helper: get all captured invoke calls (for assertions)
|
|
396
|
+
window.__TAURI_GET_MOCK_CALLS__ = function() {
|
|
397
|
+
return window.__TAURI_MOCK_CALLS__;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Helper: clear captured invoke calls
|
|
401
|
+
window.__TAURI_CLEAR_MOCK_CALLS__ = function() {
|
|
402
|
+
window.__TAURI_MOCK_CALLS__ = [];
|
|
403
|
+
};
|
|
404
|
+
})();
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/socket-client.ts
|
|
409
|
+
import { createConnection } from "net";
|
|
410
|
+
import { createInterface } from "readline";
|
|
411
|
+
var PluginClient = class {
|
|
412
|
+
constructor(socketPath, tcpPort) {
|
|
413
|
+
this.socketPath = socketPath;
|
|
414
|
+
this.tcpPort = tcpPort;
|
|
415
|
+
this.socket = null;
|
|
416
|
+
this.readline = null;
|
|
417
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
418
|
+
this.seq = 0;
|
|
419
|
+
this.responseQueue = [];
|
|
420
|
+
this.waitingForResponse = null;
|
|
421
|
+
}
|
|
422
|
+
/** Connect to the plugin's socket server. */
|
|
423
|
+
async connect() {
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
if (this.socketPath) {
|
|
426
|
+
this.socket = createConnection({ path: this.socketPath });
|
|
427
|
+
} else if (this.tcpPort) {
|
|
428
|
+
this.socket = createConnection({ host: "127.0.0.1", port: this.tcpPort });
|
|
429
|
+
} else {
|
|
430
|
+
reject(new Error("No socket path or TCP port specified"));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.socket.on("connect", () => {
|
|
434
|
+
this.readline = createInterface({ input: this.socket });
|
|
435
|
+
this.readline.on("line", (line) => {
|
|
436
|
+
if (this.waitingForResponse) {
|
|
437
|
+
const cb = this.waitingForResponse;
|
|
438
|
+
this.waitingForResponse = null;
|
|
439
|
+
cb(line);
|
|
440
|
+
} else {
|
|
441
|
+
this.responseQueue.push(line);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
resolve();
|
|
445
|
+
});
|
|
446
|
+
this.socket.on("error", (err) => {
|
|
447
|
+
reject(err);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/** Disconnect from the server. */
|
|
452
|
+
disconnect() {
|
|
453
|
+
this.readline?.close();
|
|
454
|
+
this.socket?.destroy();
|
|
455
|
+
this.socket = null;
|
|
456
|
+
this.readline = null;
|
|
457
|
+
}
|
|
458
|
+
/** Send a command and wait for the response. */
|
|
459
|
+
async send(command) {
|
|
460
|
+
if (!this.socket) {
|
|
461
|
+
throw new Error("Not connected");
|
|
462
|
+
}
|
|
463
|
+
const json = JSON.stringify(command) + "\n";
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
this.socket.write(json, (err) => {
|
|
466
|
+
if (err) {
|
|
467
|
+
reject(err);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.waitForLine().then((line) => {
|
|
471
|
+
try {
|
|
472
|
+
const response = JSON.parse(line);
|
|
473
|
+
resolve(response);
|
|
474
|
+
} catch {
|
|
475
|
+
reject(new Error(`Invalid JSON response: ${line}`));
|
|
476
|
+
}
|
|
477
|
+
}).catch(reject);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
waitForLine() {
|
|
482
|
+
if (this.responseQueue.length > 0) {
|
|
483
|
+
return Promise.resolve(this.responseQueue.shift());
|
|
484
|
+
}
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
this.waitingForResponse = resolve;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/tauri-page.ts
|
|
492
|
+
import { Buffer } from "buffer";
|
|
493
|
+
var TauriPage = class {
|
|
494
|
+
constructor(client) {
|
|
495
|
+
this.client = client;
|
|
496
|
+
this._defaultTimeout = 5e3;
|
|
497
|
+
// ── Keyboard & Mouse ────────────────────────────────────────────────────
|
|
498
|
+
this.keyboard = new TauriKeyboard(this);
|
|
499
|
+
this.mouse = new TauriMouse(this);
|
|
500
|
+
}
|
|
501
|
+
/** Set the default timeout for all auto-waiting operations (ms). */
|
|
502
|
+
setDefaultTimeout(timeout) {
|
|
503
|
+
this._defaultTimeout = timeout;
|
|
504
|
+
}
|
|
505
|
+
_t(options) {
|
|
506
|
+
return options?.timeout ?? this._defaultTimeout;
|
|
507
|
+
}
|
|
508
|
+
// ── Evaluation ──────────────────────────────────────────────────────────
|
|
509
|
+
/** Execute arbitrary JavaScript in the webview and return the result. */
|
|
510
|
+
async evaluate(script) {
|
|
511
|
+
const resp = await this.command("eval", { script });
|
|
512
|
+
return resp.data;
|
|
513
|
+
}
|
|
514
|
+
// ── Interactions ────────────────────────────────────────────────────────
|
|
515
|
+
/** Click an element matching the CSS selector. Auto-waits. */
|
|
516
|
+
async click(selector, options) {
|
|
517
|
+
await this.command("click", { selector, timeout_ms: this._t(options) });
|
|
518
|
+
}
|
|
519
|
+
/** Double-click an element. Auto-waits. */
|
|
520
|
+
async dblclick(selector, options) {
|
|
521
|
+
await this.command("dblclick", { selector, timeout_ms: this._t(options) });
|
|
522
|
+
}
|
|
523
|
+
/** Hover over an element. Auto-waits. */
|
|
524
|
+
async hover(selector, options) {
|
|
525
|
+
await this.command("hover", { selector, timeout_ms: this._t(options) });
|
|
526
|
+
}
|
|
527
|
+
/** Clear and fill an input element with text. Auto-waits. */
|
|
528
|
+
async fill(selector, text, options) {
|
|
529
|
+
await this.command("fill", { selector, text, timeout_ms: this._t(options) });
|
|
530
|
+
}
|
|
531
|
+
/** Type text character by character into an element. Auto-waits. */
|
|
532
|
+
async type(selector, text, options) {
|
|
533
|
+
await this.command("type_text", { selector, text, timeout_ms: this._t(options) });
|
|
534
|
+
}
|
|
535
|
+
/** Press a key on an element (e.g., 'Enter', 'Tab', 'Escape'). Auto-waits. */
|
|
536
|
+
async press(selector, key, options) {
|
|
537
|
+
await this.command("press", { selector, key, timeout_ms: this._t(options) });
|
|
538
|
+
}
|
|
539
|
+
/** Check a checkbox or radio button. Auto-waits. */
|
|
540
|
+
async check(selector, options) {
|
|
541
|
+
await this.command("check", { selector, timeout_ms: this._t(options) });
|
|
542
|
+
}
|
|
543
|
+
/** Uncheck a checkbox. Auto-waits. */
|
|
544
|
+
async uncheck(selector, options) {
|
|
545
|
+
await this.command("uncheck", { selector, timeout_ms: this._t(options) });
|
|
546
|
+
}
|
|
547
|
+
/** Select an option from a <select> element by value. Auto-waits. */
|
|
548
|
+
async selectOption(selector, value, options) {
|
|
549
|
+
const resp = await this.command("select_option", {
|
|
550
|
+
selector,
|
|
551
|
+
value,
|
|
552
|
+
timeout_ms: this._t(options)
|
|
553
|
+
});
|
|
554
|
+
return resp.data;
|
|
555
|
+
}
|
|
556
|
+
/** Focus an element. Auto-waits. */
|
|
557
|
+
async focus(selector, options) {
|
|
558
|
+
await this.command("focus", { selector, timeout_ms: this._t(options) });
|
|
559
|
+
}
|
|
560
|
+
/** Blur (unfocus) an element. Auto-waits. */
|
|
561
|
+
async blur(selector, options) {
|
|
562
|
+
await this.command("blur", { selector, timeout_ms: this._t(options) });
|
|
563
|
+
}
|
|
564
|
+
// ── Queries ─────────────────────────────────────────────────────────────
|
|
565
|
+
/** Get the text content of an element. Auto-waits for element. */
|
|
566
|
+
async textContent(selector, options) {
|
|
567
|
+
const resp = await this.command("text_content", { selector, timeout_ms: this._t(options) });
|
|
568
|
+
return resp.data;
|
|
569
|
+
}
|
|
570
|
+
/** Get the innerHTML of an element. Auto-waits for element. */
|
|
571
|
+
async innerHTML(selector, options) {
|
|
572
|
+
const resp = await this.command("inner_html", { selector, timeout_ms: this._t(options) });
|
|
573
|
+
return resp.data;
|
|
574
|
+
}
|
|
575
|
+
/** Get the innerText of an element (visible text only). Auto-waits for element. */
|
|
576
|
+
async innerText(selector, options) {
|
|
577
|
+
const resp = await this.command("inner_text", { selector, timeout_ms: this._t(options) });
|
|
578
|
+
return resp.data;
|
|
579
|
+
}
|
|
580
|
+
/** Get textContent of all elements matching a selector. No wait (works on zero matches). */
|
|
581
|
+
async allTextContents(selector) {
|
|
582
|
+
const resp = await this.command("all_text_contents", { selector });
|
|
583
|
+
return resp.data;
|
|
584
|
+
}
|
|
585
|
+
/** Get innerText of all elements matching a selector. No wait (works on zero matches). */
|
|
586
|
+
async allInnerTexts(selector) {
|
|
587
|
+
const resp = await this.command("all_inner_texts", { selector });
|
|
588
|
+
return resp.data;
|
|
589
|
+
}
|
|
590
|
+
/** Get an attribute value from an element. Auto-waits for element. */
|
|
591
|
+
async getAttribute(selector, name, options) {
|
|
592
|
+
const resp = await this.command("get_attribute", {
|
|
593
|
+
selector,
|
|
594
|
+
name,
|
|
595
|
+
timeout_ms: this._t(options)
|
|
596
|
+
});
|
|
597
|
+
return resp.data;
|
|
598
|
+
}
|
|
599
|
+
/** Get the input value of a form element. Auto-waits for element. */
|
|
600
|
+
async inputValue(selector, options) {
|
|
601
|
+
const resp = await this.command("input_value", { selector, timeout_ms: this._t(options) });
|
|
602
|
+
return resp.data ?? "";
|
|
603
|
+
}
|
|
604
|
+
/** Get the bounding box of an element. Auto-waits for element. */
|
|
605
|
+
async boundingBox(selector, options) {
|
|
606
|
+
const resp = await this.command("bounding_box", { selector, timeout_ms: this._t(options) });
|
|
607
|
+
return resp.data;
|
|
608
|
+
}
|
|
609
|
+
// ── State checks ────────────────────────────────────────────────────────
|
|
610
|
+
/** Check if an element matching the selector is visible. */
|
|
611
|
+
async isVisible(selector) {
|
|
612
|
+
const resp = await this.command("is_visible", { selector });
|
|
613
|
+
return resp.data;
|
|
614
|
+
}
|
|
615
|
+
/** Check if an element is checked (checkbox/radio). */
|
|
616
|
+
async isChecked(selector) {
|
|
617
|
+
const resp = await this.command("is_checked", { selector });
|
|
618
|
+
return resp.data;
|
|
619
|
+
}
|
|
620
|
+
/** Check if an element is disabled. */
|
|
621
|
+
async isDisabled(selector) {
|
|
622
|
+
const resp = await this.command("is_disabled", { selector });
|
|
623
|
+
return resp.data;
|
|
624
|
+
}
|
|
625
|
+
/** Check if an element is editable. */
|
|
626
|
+
async isEditable(selector) {
|
|
627
|
+
const resp = await this.command("is_editable", { selector });
|
|
628
|
+
return resp.data;
|
|
629
|
+
}
|
|
630
|
+
/** Check if an element is hidden. */
|
|
631
|
+
async isHidden(selector) {
|
|
632
|
+
return !await this.isVisible(selector);
|
|
633
|
+
}
|
|
634
|
+
/** Check if an element is enabled. */
|
|
635
|
+
async isEnabled(selector) {
|
|
636
|
+
return !await this.isDisabled(selector);
|
|
637
|
+
}
|
|
638
|
+
// ── Waiting ─────────────────────────────────────────────────────────────
|
|
639
|
+
/** Wait for an element matching the selector to become visible. */
|
|
640
|
+
async waitForSelector(selector, timeout = 5e3) {
|
|
641
|
+
await this.command("wait_for_selector", { selector, timeout_ms: timeout });
|
|
642
|
+
}
|
|
643
|
+
/** Wait for a JS expression to return truthy. */
|
|
644
|
+
async waitForFunction(expression, timeout = 5e3) {
|
|
645
|
+
await this.command("wait_for_function", { expression, timeout_ms: timeout });
|
|
646
|
+
}
|
|
647
|
+
// ── Counting ────────────────────────────────────────────────────────────
|
|
648
|
+
/** Count elements matching a selector. */
|
|
649
|
+
async count(selector) {
|
|
650
|
+
const resp = await this.command("count", { selector });
|
|
651
|
+
return resp.data;
|
|
652
|
+
}
|
|
653
|
+
// ── Page info ───────────────────────────────────────────────────────────
|
|
654
|
+
/** Get the page title. */
|
|
655
|
+
async title() {
|
|
656
|
+
const resp = await this.command("title", {});
|
|
657
|
+
return resp.data;
|
|
658
|
+
}
|
|
659
|
+
/** Get the current URL. */
|
|
660
|
+
async url() {
|
|
661
|
+
const resp = await this.command("url", {});
|
|
662
|
+
return resp.data;
|
|
663
|
+
}
|
|
664
|
+
/** Get the full page HTML. */
|
|
665
|
+
async content() {
|
|
666
|
+
const resp = await this.command("content", {});
|
|
667
|
+
return resp.data;
|
|
668
|
+
}
|
|
669
|
+
// ── Navigation ──────────────────────────────────────────────────────────
|
|
670
|
+
/** Navigate to a URL. */
|
|
671
|
+
async goto(url) {
|
|
672
|
+
await this.command("goto", { url });
|
|
673
|
+
}
|
|
674
|
+
/** Reload the page. */
|
|
675
|
+
async reload() {
|
|
676
|
+
await this.command("reload", {});
|
|
677
|
+
}
|
|
678
|
+
/** Navigate back in history. */
|
|
679
|
+
async goBack() {
|
|
680
|
+
await this.command("go_back", {});
|
|
681
|
+
}
|
|
682
|
+
/** Navigate forward in history. */
|
|
683
|
+
async goForward() {
|
|
684
|
+
await this.command("go_forward", {});
|
|
685
|
+
}
|
|
686
|
+
/** Wait for the URL to contain or match a pattern. */
|
|
687
|
+
async waitForURL(pattern, options) {
|
|
688
|
+
await this.command("wait_for_url", { pattern, timeout_ms: this._t(options) });
|
|
689
|
+
}
|
|
690
|
+
/** Check if an element is the active/focused element. */
|
|
691
|
+
async isFocused(selector) {
|
|
692
|
+
const resp = await this.command("is_focused", { selector });
|
|
693
|
+
return resp.data;
|
|
694
|
+
}
|
|
695
|
+
/** Get a computed CSS style value from an element. Auto-waits. */
|
|
696
|
+
async getComputedStyle(selector, property, options) {
|
|
697
|
+
const resp = await this.command("get_computed_style", {
|
|
698
|
+
selector,
|
|
699
|
+
property,
|
|
700
|
+
timeout_ms: this._t(options)
|
|
701
|
+
});
|
|
702
|
+
return resp.data;
|
|
703
|
+
}
|
|
704
|
+
/** Dispatch a custom DOM event on an element. Auto-waits. */
|
|
705
|
+
async dispatchEvent(selector, eventType, options) {
|
|
706
|
+
await this.command("dispatch_event", {
|
|
707
|
+
selector,
|
|
708
|
+
event_type: eventType,
|
|
709
|
+
timeout_ms: this._t(options)
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// ── Drag and drop ───────────────────────────────────────────────────────
|
|
713
|
+
/** Drag one element onto another. Auto-waits. */
|
|
714
|
+
async dragAndDrop(source, target, options) {
|
|
715
|
+
await this.command("drag_and_drop", { source, target, timeout_ms: this._t(options) });
|
|
716
|
+
}
|
|
717
|
+
// ── File upload ─────────────────────────────────────────────────────────
|
|
718
|
+
/** Set files on a file input element. */
|
|
719
|
+
async setInputFiles(selector, files, options) {
|
|
720
|
+
const payload = files.map((f) => ({
|
|
721
|
+
name: f.name,
|
|
722
|
+
mime_type: f.mimeType,
|
|
723
|
+
base64: f.buffer.toString("base64")
|
|
724
|
+
}));
|
|
725
|
+
const resp = await this.command("set_input_files", {
|
|
726
|
+
selector,
|
|
727
|
+
files: payload,
|
|
728
|
+
timeout_ms: this._t(options)
|
|
729
|
+
});
|
|
730
|
+
return resp.data;
|
|
731
|
+
}
|
|
732
|
+
// ── Dialog handling ─────────────────────────────────────────────────────
|
|
733
|
+
/** Install dialog interception (alert/confirm/prompt). */
|
|
734
|
+
async installDialogHandler(options) {
|
|
735
|
+
await this.command("install_dialog_handler", {
|
|
736
|
+
default_confirm: options?.defaultConfirm ?? true,
|
|
737
|
+
default_prompt_text: options?.defaultPromptText
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
/** Get captured dialogs since last check. */
|
|
741
|
+
async getDialogs() {
|
|
742
|
+
const resp = await this.command("get_dialogs", {});
|
|
743
|
+
return resp.data ?? [];
|
|
744
|
+
}
|
|
745
|
+
/** Clear captured dialogs. */
|
|
746
|
+
async clearDialogs() {
|
|
747
|
+
await this.command("clear_dialogs", {});
|
|
748
|
+
}
|
|
749
|
+
// ── Network mocking ─────────────────────────────────────────────────────
|
|
750
|
+
/** Add a network route that intercepts matching fetch/XHR requests. */
|
|
751
|
+
async route(pattern, response) {
|
|
752
|
+
await this.command("add_network_route", {
|
|
753
|
+
pattern,
|
|
754
|
+
status: response.status ?? 200,
|
|
755
|
+
body: response.body ?? "",
|
|
756
|
+
content_type: response.contentType
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/** Remove a network route. */
|
|
760
|
+
async unroute(pattern) {
|
|
761
|
+
await this.command("remove_network_route", { pattern });
|
|
762
|
+
}
|
|
763
|
+
/** Clear all network routes. */
|
|
764
|
+
async clearRoutes() {
|
|
765
|
+
await this.command("clear_network_routes", {});
|
|
766
|
+
}
|
|
767
|
+
/** Get captured network requests. */
|
|
768
|
+
async getNetworkRequests() {
|
|
769
|
+
const resp = await this.command("get_network_requests", {});
|
|
770
|
+
return resp.data ?? [];
|
|
771
|
+
}
|
|
772
|
+
/** Clear captured network requests. */
|
|
773
|
+
async clearNetworkRequests() {
|
|
774
|
+
await this.command("clear_network_requests", {});
|
|
775
|
+
}
|
|
776
|
+
// ── Capture ─────────────────────────────────────────────────────────────
|
|
777
|
+
/** Take a native screenshot of the Tauri window. Returns PNG buffer. */
|
|
778
|
+
async screenshot(options) {
|
|
779
|
+
const resp = await this.command("native_screenshot", {
|
|
780
|
+
path: options?.path
|
|
781
|
+
});
|
|
782
|
+
const data = resp.data;
|
|
783
|
+
if (options?.path) {
|
|
784
|
+
return Buffer.alloc(0);
|
|
785
|
+
}
|
|
786
|
+
return Buffer.from(data.base64, "base64");
|
|
787
|
+
}
|
|
788
|
+
/** Start recording the Tauri window as video (native frame capture). */
|
|
789
|
+
async startRecording(options) {
|
|
790
|
+
const dir = options?.path ?? `/tmp/tauri-playwright-recording-${Date.now()}`;
|
|
791
|
+
const resp = await this.command("start_recording", {
|
|
792
|
+
path: dir,
|
|
793
|
+
fps: options?.fps ?? 10
|
|
794
|
+
});
|
|
795
|
+
return resp.data;
|
|
796
|
+
}
|
|
797
|
+
/** Stop recording and return the video path (if ffmpeg is available). */
|
|
798
|
+
async stopRecording() {
|
|
799
|
+
const resp = await this.command("stop_recording", {});
|
|
800
|
+
return resp.data;
|
|
801
|
+
}
|
|
802
|
+
// ── Locator ─────────────────────────────────────────────────────────────
|
|
803
|
+
/** Create a locator for chained operations. */
|
|
804
|
+
locator(selector) {
|
|
805
|
+
return new TauriLocator(this, selector);
|
|
806
|
+
}
|
|
807
|
+
// ── Semantic selectors ──────────────────────────────────────────────────
|
|
808
|
+
getByTestId(testId) {
|
|
809
|
+
return new TauriLocator(this, `[data-testid="${testId}"]`);
|
|
810
|
+
}
|
|
811
|
+
getByPlaceholder(text, options) {
|
|
812
|
+
return new TauriLocator(
|
|
813
|
+
this,
|
|
814
|
+
options?.exact ? `[placeholder="${text}"]` : `[placeholder*="${text}"]`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
getByAltText(text, options) {
|
|
818
|
+
return new TauriLocator(this, options?.exact ? `[alt="${text}"]` : `[alt*="${text}"]`);
|
|
819
|
+
}
|
|
820
|
+
getByTitle(text, options) {
|
|
821
|
+
return new TauriLocator(this, options?.exact ? `[title="${text}"]` : `[title*="${text}"]`);
|
|
822
|
+
}
|
|
823
|
+
getByRole(role, options) {
|
|
824
|
+
if (options?.name) {
|
|
825
|
+
return new TauriLocator(this, `[role="${role}"][aria-label="${options.name}"]`);
|
|
826
|
+
}
|
|
827
|
+
return new TauriLocator(this, `[role="${role}"]`);
|
|
828
|
+
}
|
|
829
|
+
getByText(text, options) {
|
|
830
|
+
const escaped = JSON.stringify(text);
|
|
831
|
+
const exact = options?.exact ?? false;
|
|
832
|
+
const jsSelector = exact ? `[data-pw-text-exact=${escaped}]` : `[data-pw-text=${escaped}]`;
|
|
833
|
+
return new TauriLocator(this, jsSelector, {
|
|
834
|
+
jsFind: `Array.from(document.querySelectorAll('*')).find(el => {
|
|
835
|
+
if (el.children.length > 0 && el.querySelector('*:not(br):not(wbr)')) {
|
|
836
|
+
var direct = Array.from(el.childNodes).filter(n => n.nodeType === 3).map(n => n.textContent).join('');
|
|
837
|
+
if (!direct.trim()) return false;
|
|
838
|
+
return ${exact} ? direct.trim() === ${escaped} : direct.includes(${escaped});
|
|
839
|
+
}
|
|
840
|
+
var t = el.textContent || '';
|
|
841
|
+
return ${exact} ? t.trim() === ${escaped} : t.includes(${escaped});
|
|
842
|
+
})`
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
getByLabel(text, options) {
|
|
846
|
+
const escaped = JSON.stringify(text);
|
|
847
|
+
const exact = options?.exact ?? false;
|
|
848
|
+
return new TauriLocator(this, `[data-pw-label=${escaped}]`, {
|
|
849
|
+
jsFind: `(function() {
|
|
850
|
+
var labels = document.querySelectorAll('label');
|
|
851
|
+
for (var i = 0; i < labels.length; i++) {
|
|
852
|
+
var t = labels[i].textContent || '';
|
|
853
|
+
if (${exact} ? t.trim() === ${escaped} : t.includes(${escaped})) {
|
|
854
|
+
var f = labels[i].getAttribute('for');
|
|
855
|
+
if (f) return document.getElementById(f);
|
|
856
|
+
return labels[i].querySelector('input,textarea,select');
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
})()`
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/** Send a command to the plugin and handle errors. */
|
|
864
|
+
async command(type, params) {
|
|
865
|
+
const resp = await this.client.send({ type, ...params });
|
|
866
|
+
if (!resp.ok) {
|
|
867
|
+
throw new Error(`TauriPage command '${type}' failed: ${resp.error}`);
|
|
868
|
+
}
|
|
869
|
+
return resp;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
var TauriLocator = class _TauriLocator {
|
|
873
|
+
constructor(page, selector, options) {
|
|
874
|
+
this.page = page;
|
|
875
|
+
this.selector = selector;
|
|
876
|
+
this._jsFind = options?.jsFind;
|
|
877
|
+
}
|
|
878
|
+
/** Whether this locator uses JS-based element resolution. */
|
|
879
|
+
get isJsBased() {
|
|
880
|
+
return !!this._jsFind;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Execute an action/query. For CSS selectors, delegates to TauriPage.
|
|
884
|
+
* For JS-based selectors, uses evaluate with the JS find expression.
|
|
885
|
+
*/
|
|
886
|
+
async _eval(script) {
|
|
887
|
+
if (!this._jsFind) throw new Error("_eval only for JS-based locators");
|
|
888
|
+
return this.page.evaluate(script);
|
|
889
|
+
}
|
|
890
|
+
_actionScript(actionBody, timeout = 5e3) {
|
|
891
|
+
const find = this._jsFind;
|
|
892
|
+
return `(async function(){ var dl=Date.now()+${timeout}; while(Date.now()<dl){ var el=${find}; if(el){ var r=el.getBoundingClientRect(); var st=getComputedStyle(el); if(r.width>0&&r.height>0&&st.visibility!=='hidden'&&st.display!=='none'&&parseFloat(st.opacity)>0){ ${actionBody}; }} await new Promise(function(r){setTimeout(r,50)}); } throw new Error('timeout waiting for element'); })()`;
|
|
893
|
+
}
|
|
894
|
+
_queryScript(returnExpr, timeout = 5e3) {
|
|
895
|
+
const find = this._jsFind;
|
|
896
|
+
return `(async function(){ var dl=Date.now()+${timeout}; while(Date.now()<dl){ var el=${find}; if(el){ return ${returnExpr}; } await new Promise(function(r){setTimeout(r,50)}); } throw new Error('timeout waiting for element'); })()`;
|
|
897
|
+
}
|
|
898
|
+
// ── Actions ─────────────────────────────────────────────────────────
|
|
899
|
+
async click(options) {
|
|
900
|
+
if (!this._jsFind) return this.page.click(this.selector, options);
|
|
901
|
+
await this._eval(
|
|
902
|
+
this._actionScript('el.scrollIntoView({block:"center"}); el.click(); return null')
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
async dblclick(options) {
|
|
906
|
+
if (!this._jsFind) return this.page.dblclick(this.selector, options);
|
|
907
|
+
await this._eval(
|
|
908
|
+
this._actionScript(
|
|
909
|
+
'el.scrollIntoView({block:"center"}); el.dispatchEvent(new MouseEvent("dblclick",{bubbles:true})); return null'
|
|
910
|
+
)
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
async hover(options) {
|
|
914
|
+
if (!this._jsFind) return this.page.hover(this.selector, options);
|
|
915
|
+
await this._eval(
|
|
916
|
+
this._actionScript(
|
|
917
|
+
'el.scrollIntoView({block:"center"}); el.dispatchEvent(new MouseEvent("mouseenter",{bubbles:true})); el.dispatchEvent(new MouseEvent("mouseover",{bubbles:true})); return null'
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
async fill(text, options) {
|
|
922
|
+
if (!this._jsFind) return this.page.fill(this.selector, text, options);
|
|
923
|
+
const t = JSON.stringify(text);
|
|
924
|
+
await this._eval(
|
|
925
|
+
this._actionScript(
|
|
926
|
+
`el.focus(); var desc=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value'); if(desc&&desc.set) desc.set.call(el,${t}); else el.value=${t}; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); return null`
|
|
927
|
+
)
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
async press(key, options) {
|
|
931
|
+
if (!this._jsFind) return this.page.press(this.selector, key, options);
|
|
932
|
+
const k = JSON.stringify(key);
|
|
933
|
+
await this._eval(
|
|
934
|
+
this._actionScript(
|
|
935
|
+
`el.focus(); var o={key:${k},bubbles:true}; el.dispatchEvent(new KeyboardEvent('keydown',o)); el.dispatchEvent(new KeyboardEvent('keypress',o)); el.dispatchEvent(new KeyboardEvent('keyup',o)); return null`
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
async check(options) {
|
|
940
|
+
if (!this._jsFind) return this.page.check(this.selector, options);
|
|
941
|
+
await this._eval(this._actionScript("if(!el.checked){ el.click(); } return null"));
|
|
942
|
+
}
|
|
943
|
+
async uncheck(options) {
|
|
944
|
+
if (!this._jsFind) return this.page.uncheck(this.selector, options);
|
|
945
|
+
await this._eval(this._actionScript("if(el.checked){ el.click(); } return null"));
|
|
946
|
+
}
|
|
947
|
+
async selectOption(value, options) {
|
|
948
|
+
if (!this._jsFind) return this.page.selectOption(this.selector, value, options);
|
|
949
|
+
const v = JSON.stringify(value);
|
|
950
|
+
return this._eval(
|
|
951
|
+
this._actionScript(
|
|
952
|
+
`el.value=${v}; el.dispatchEvent(new Event('change',{bubbles:true})); return el.value`
|
|
953
|
+
)
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
async focus(options) {
|
|
957
|
+
if (!this._jsFind) return this.page.focus(this.selector, options);
|
|
958
|
+
await this._eval(this._queryScript("(function(){ el.focus(); return null; })()"));
|
|
959
|
+
}
|
|
960
|
+
async blur(options) {
|
|
961
|
+
if (!this._jsFind) return this.page.blur(this.selector, options);
|
|
962
|
+
await this._eval(this._queryScript("(function(){ el.blur(); return null; })()"));
|
|
963
|
+
}
|
|
964
|
+
async clear() {
|
|
965
|
+
return this.fill("");
|
|
966
|
+
}
|
|
967
|
+
/** Type text character-by-character (Playwright's replacement for deprecated type()). */
|
|
968
|
+
async pressSequentially(text, options) {
|
|
969
|
+
if (!this._jsFind) {
|
|
970
|
+
return this.page.type(this.selector, text);
|
|
971
|
+
}
|
|
972
|
+
for (const char of text) {
|
|
973
|
+
await this.press(char);
|
|
974
|
+
if (options?.delay) await new Promise((r) => setTimeout(r, options.delay));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/** Dispatch a custom DOM event on the element. */
|
|
978
|
+
async dispatchEvent(eventType, options) {
|
|
979
|
+
if (!this._jsFind) return this.page.dispatchEvent(this.selector, eventType, options);
|
|
980
|
+
const e = JSON.stringify(eventType);
|
|
981
|
+
await this._eval(
|
|
982
|
+
this._actionScript(`el.dispatchEvent(new Event(${e},{bubbles:true})); return null`)
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
/** Run a JS function on the matched element. The function receives `el` as argument. */
|
|
986
|
+
async evaluate(fn) {
|
|
987
|
+
if (!this._jsFind) {
|
|
988
|
+
return this.page.evaluate(
|
|
989
|
+
`(function(){ var el=document.querySelector(${JSON.stringify(this.selector)}); if(!el) throw new Error('not found'); return (${fn})(el); })()`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
return this._eval(
|
|
993
|
+
`(function(){ var el=${this._jsFind}; if(!el) throw new Error('not found'); return (${fn})(el); })()`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
/** Check if this element is the active/focused element. */
|
|
997
|
+
async isFocused() {
|
|
998
|
+
if (!this._jsFind) return this.page.isFocused(this.selector);
|
|
999
|
+
return this._eval(
|
|
1000
|
+
`(function(){ var el=${this._jsFind}; return el!==null&&document.activeElement===el; })()`
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
async scrollIntoViewIfNeeded() {
|
|
1004
|
+
if (!this._jsFind) {
|
|
1005
|
+
await this.page.evaluate(
|
|
1006
|
+
`document.querySelector(${JSON.stringify(this.selector)})?.scrollIntoView({block:'center'})`
|
|
1007
|
+
);
|
|
1008
|
+
} else {
|
|
1009
|
+
await this._eval(
|
|
1010
|
+
`(function(){ var el=${this._jsFind}; if(el) el.scrollIntoView({block:'center'}); return null; })()`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// ── Queries ─────────────────────────────────────────────────────────
|
|
1015
|
+
async textContent(options) {
|
|
1016
|
+
if (!this._jsFind) return this.page.textContent(this.selector, options);
|
|
1017
|
+
return this._eval(this._queryScript("el.textContent"));
|
|
1018
|
+
}
|
|
1019
|
+
async innerHTML(options) {
|
|
1020
|
+
if (!this._jsFind) return this.page.innerHTML(this.selector, options);
|
|
1021
|
+
return this._eval(this._queryScript("el.innerHTML"));
|
|
1022
|
+
}
|
|
1023
|
+
async innerText(options) {
|
|
1024
|
+
if (!this._jsFind) return this.page.innerText(this.selector, options);
|
|
1025
|
+
return this._eval(this._queryScript("el.innerText"));
|
|
1026
|
+
}
|
|
1027
|
+
async getAttribute(name, options) {
|
|
1028
|
+
if (!this._jsFind) return this.page.getAttribute(this.selector, name, options);
|
|
1029
|
+
return this._eval(this._queryScript(`el.getAttribute(${JSON.stringify(name)})`));
|
|
1030
|
+
}
|
|
1031
|
+
async inputValue(options) {
|
|
1032
|
+
if (!this._jsFind) return this.page.inputValue(this.selector, options);
|
|
1033
|
+
return this._eval(this._queryScript("el.value||''"));
|
|
1034
|
+
}
|
|
1035
|
+
async boundingBox() {
|
|
1036
|
+
if (!this._jsFind) return this.page.boundingBox(this.selector);
|
|
1037
|
+
return this._eval(
|
|
1038
|
+
`(function(){ var el=${this._jsFind}; if(!el) return null; var r=el.getBoundingClientRect(); return {x:r.left,y:r.top,width:r.width,height:r.height}; })()`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
// ── State ───────────────────────────────────────────────────────────
|
|
1042
|
+
async isVisible() {
|
|
1043
|
+
if (!this._jsFind) return this.page.isVisible(this.selector);
|
|
1044
|
+
return this._eval(
|
|
1045
|
+
`(function(){ var el=${this._jsFind}; if(!el) return false; var r=el.getBoundingClientRect(); var st=getComputedStyle(el); return r.width>0&&r.height>0&&st.visibility!=='hidden'&&st.display!=='none'&&parseFloat(st.opacity)>0; })()`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
async isHidden() {
|
|
1049
|
+
return !await this.isVisible();
|
|
1050
|
+
}
|
|
1051
|
+
async isChecked() {
|
|
1052
|
+
if (!this._jsFind) return this.page.isChecked(this.selector);
|
|
1053
|
+
return this._eval(
|
|
1054
|
+
`(function(){ var el=${this._jsFind}; if(!el) return false; return !!el.checked; })()`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
async isDisabled() {
|
|
1058
|
+
if (!this._jsFind) return this.page.isDisabled(this.selector);
|
|
1059
|
+
return this._eval(
|
|
1060
|
+
`(function(){ var el=${this._jsFind}; if(!el) return true; return el.disabled===true||el.hasAttribute('disabled'); })()`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
async isEditable() {
|
|
1064
|
+
if (!this._jsFind) return this.page.isEditable(this.selector);
|
|
1065
|
+
return this._eval(
|
|
1066
|
+
`(function(){ var el=${this._jsFind}; if(!el) return false; if(el.disabled||el.readOnly) return false; var tag=el.tagName; return tag==='INPUT'||tag==='TEXTAREA'||tag==='SELECT'||el.isContentEditable; })()`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
async isEnabled() {
|
|
1070
|
+
return !await this.isDisabled();
|
|
1071
|
+
}
|
|
1072
|
+
// ── Waiting ─────────────────────────────────────────────────────────
|
|
1073
|
+
async waitFor(timeout = 5e3) {
|
|
1074
|
+
if (!this._jsFind) return this.page.waitForSelector(this.selector, timeout);
|
|
1075
|
+
await this._eval(this._queryScript("true", timeout));
|
|
1076
|
+
}
|
|
1077
|
+
// ── Counting ────────────────────────────────────────────────────────
|
|
1078
|
+
async count() {
|
|
1079
|
+
if (!this._jsFind) return this.page.count(this.selector);
|
|
1080
|
+
return this._eval(`document.querySelectorAll(${JSON.stringify(this.selector)}).length`);
|
|
1081
|
+
}
|
|
1082
|
+
// ── Refinement ──────────────────────────────────────────────────────
|
|
1083
|
+
nth(index) {
|
|
1084
|
+
return new _TauriLocator(this.page, `${this.selector}:nth-match(${index})`, {
|
|
1085
|
+
jsFind: `document.querySelectorAll(${JSON.stringify(this.selector)})[${index}]`
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
first() {
|
|
1089
|
+
return this.nth(0);
|
|
1090
|
+
}
|
|
1091
|
+
last() {
|
|
1092
|
+
return new _TauriLocator(this.page, this.selector, {
|
|
1093
|
+
jsFind: `(function(){ var all=document.querySelectorAll(${JSON.stringify(this.selector)}); return all[all.length-1]||null; })()`
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
filter(options) {
|
|
1097
|
+
const sel = JSON.stringify(this.selector);
|
|
1098
|
+
if (options.hasText) {
|
|
1099
|
+
const match = typeof options.hasText === "string" ? `t.includes(${JSON.stringify(options.hasText)})` : `${options.hasText.toString()}.test(t)`;
|
|
1100
|
+
return new _TauriLocator(this.page, this.selector, {
|
|
1101
|
+
jsFind: `Array.from(document.querySelectorAll(${sel})).find(function(el){ var t=el.textContent||''; return ${match}; })`
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
return this;
|
|
1105
|
+
}
|
|
1106
|
+
async all() {
|
|
1107
|
+
const c = await this.count();
|
|
1108
|
+
return Array.from({ length: c }, (_, i) => this.nth(i));
|
|
1109
|
+
}
|
|
1110
|
+
// ── Semantic selectors (scoped to this locator) ─────────────────────
|
|
1111
|
+
getByTestId(testId) {
|
|
1112
|
+
return new _TauriLocator(this.page, `${this.selector} [data-testid="${testId}"]`);
|
|
1113
|
+
}
|
|
1114
|
+
getByPlaceholder(text, options) {
|
|
1115
|
+
const attr = options?.exact ? `="${text}"` : `*="${text}"`;
|
|
1116
|
+
return new _TauriLocator(this.page, `${this.selector} [placeholder${attr}]`);
|
|
1117
|
+
}
|
|
1118
|
+
getByText(text, options) {
|
|
1119
|
+
return this.page.getByText(text, options);
|
|
1120
|
+
}
|
|
1121
|
+
getByRole(role, options) {
|
|
1122
|
+
return this.page.getByRole(role, options);
|
|
1123
|
+
}
|
|
1124
|
+
locator(selector) {
|
|
1125
|
+
return new _TauriLocator(this.page, `${this.selector} ${selector}`);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
var TauriKeyboard = class {
|
|
1129
|
+
constructor(page) {
|
|
1130
|
+
this.page = page;
|
|
1131
|
+
this._modifiers = /* @__PURE__ */ new Set();
|
|
1132
|
+
}
|
|
1133
|
+
async press(key) {
|
|
1134
|
+
const parts = key.split("+");
|
|
1135
|
+
const modifiers = parts.slice(0, -1);
|
|
1136
|
+
const mainKey = parts[parts.length - 1];
|
|
1137
|
+
for (const mod of modifiers) await this.down(mod);
|
|
1138
|
+
const k = JSON.stringify(mainKey);
|
|
1139
|
+
await this.page.evaluate(`(function(){
|
|
1140
|
+
var el=document.activeElement||document.body;
|
|
1141
|
+
var o={key:${k},bubbles:true,ctrlKey:${this._modifiers.has("Control")},shiftKey:${this._modifiers.has("Shift")},altKey:${this._modifiers.has("Alt")},metaKey:${this._modifiers.has("Meta")}};
|
|
1142
|
+
el.dispatchEvent(new KeyboardEvent('keydown',o));
|
|
1143
|
+
el.dispatchEvent(new KeyboardEvent('keypress',o));
|
|
1144
|
+
el.dispatchEvent(new KeyboardEvent('keyup',o));
|
|
1145
|
+
})()`);
|
|
1146
|
+
for (const mod of modifiers) await this.up(mod);
|
|
1147
|
+
}
|
|
1148
|
+
async down(key) {
|
|
1149
|
+
this._modifiers.add(key);
|
|
1150
|
+
await this.page.evaluate(
|
|
1151
|
+
`(function(){ var el=document.activeElement||document.body; el.dispatchEvent(new KeyboardEvent('keydown',{key:${JSON.stringify(key)},bubbles:true})); })()`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
async up(key) {
|
|
1155
|
+
this._modifiers.delete(key);
|
|
1156
|
+
await this.page.evaluate(
|
|
1157
|
+
`(function(){ var el=document.activeElement||document.body; el.dispatchEvent(new KeyboardEvent('keyup',{key:${JSON.stringify(key)},bubbles:true})); })()`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
async type(text, options) {
|
|
1161
|
+
for (const char of text) {
|
|
1162
|
+
const c = JSON.stringify(char);
|
|
1163
|
+
await this.page.evaluate(`(function(){
|
|
1164
|
+
var el=document.activeElement||document.body;
|
|
1165
|
+
el.dispatchEvent(new KeyboardEvent('keydown',{key:${c},bubbles:true}));
|
|
1166
|
+
if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){
|
|
1167
|
+
var desc=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value');
|
|
1168
|
+
if(desc&&desc.set) desc.set.call(el,el.value+${c}); else el.value+=${c};
|
|
1169
|
+
el.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1170
|
+
}
|
|
1171
|
+
el.dispatchEvent(new KeyboardEvent('keyup',{key:${c},bubbles:true}));
|
|
1172
|
+
})()`);
|
|
1173
|
+
if (options?.delay) await new Promise((r) => setTimeout(r, options.delay));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async insertText(text) {
|
|
1177
|
+
await this.page.evaluate(`document.execCommand('insertText', false, ${JSON.stringify(text)})`);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
var TauriMouse = class {
|
|
1181
|
+
constructor(page) {
|
|
1182
|
+
this.page = page;
|
|
1183
|
+
}
|
|
1184
|
+
async click(x, y, options) {
|
|
1185
|
+
const btn = options?.button === "right" ? 2 : options?.button === "middle" ? 1 : 0;
|
|
1186
|
+
await this.page.evaluate(`(function(){
|
|
1187
|
+
var el=document.elementFromPoint(${x},${y}); if(!el) return;
|
|
1188
|
+
var o={bubbles:true,clientX:${x},clientY:${y},button:${btn}};
|
|
1189
|
+
el.dispatchEvent(new MouseEvent('mousedown',o));
|
|
1190
|
+
el.dispatchEvent(new MouseEvent('mouseup',o));
|
|
1191
|
+
el.dispatchEvent(new MouseEvent('click',o));
|
|
1192
|
+
})()`);
|
|
1193
|
+
}
|
|
1194
|
+
async dblclick(x, y) {
|
|
1195
|
+
await this.page.evaluate(`(function(){
|
|
1196
|
+
var el=document.elementFromPoint(${x},${y}); if(!el) return;
|
|
1197
|
+
var o={bubbles:true,clientX:${x},clientY:${y}};
|
|
1198
|
+
el.dispatchEvent(new MouseEvent('dblclick',o));
|
|
1199
|
+
})()`);
|
|
1200
|
+
}
|
|
1201
|
+
async move(x, y) {
|
|
1202
|
+
await this.page.evaluate(`(function(){
|
|
1203
|
+
var el=document.elementFromPoint(${x},${y})||document.body;
|
|
1204
|
+
el.dispatchEvent(new MouseEvent('mousemove',{bubbles:true,clientX:${x},clientY:${y}}));
|
|
1205
|
+
})()`);
|
|
1206
|
+
}
|
|
1207
|
+
async down(options) {
|
|
1208
|
+
const btn = options?.button === "right" ? 2 : options?.button === "middle" ? 1 : 0;
|
|
1209
|
+
await this.page.evaluate(
|
|
1210
|
+
`document.activeElement?.dispatchEvent(new MouseEvent('mousedown',{bubbles:true,button:${btn}}))`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
async up(options) {
|
|
1214
|
+
const btn = options?.button === "right" ? 2 : options?.button === "middle" ? 1 : 0;
|
|
1215
|
+
await this.page.evaluate(
|
|
1216
|
+
`document.activeElement?.dispatchEvent(new MouseEvent('mouseup',{bubbles:true,button:${btn}}))`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
async wheel(deltaX, deltaY) {
|
|
1220
|
+
await this.page.evaluate(
|
|
1221
|
+
`document.activeElement?.dispatchEvent(new WheelEvent('wheel',{bubbles:true,deltaX:${deltaX},deltaY:${deltaY}}))`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
// src/browser-page-adapter.ts
|
|
1227
|
+
var BrowserPageAdapter = class {
|
|
1228
|
+
constructor(page) {
|
|
1229
|
+
this.page = page;
|
|
1230
|
+
// ── Dialog handling ─────────────────────────────────────────────────
|
|
1231
|
+
this.dialogHandler = null;
|
|
1232
|
+
this.capturedDialogs = [];
|
|
1233
|
+
// ── Network mocking ─────────────────────────────────────────────────
|
|
1234
|
+
this.capturedRequests = [];
|
|
1235
|
+
this.activeRoutes = [];
|
|
1236
|
+
}
|
|
1237
|
+
/** Set the default timeout for all auto-waiting operations (ms). */
|
|
1238
|
+
setDefaultTimeout(timeout) {
|
|
1239
|
+
this.page.setDefaultTimeout(timeout);
|
|
1240
|
+
}
|
|
1241
|
+
// ── Expose the underlying Playwright Page for direct access ─────────
|
|
1242
|
+
get playwrightPage() {
|
|
1243
|
+
return this.page;
|
|
1244
|
+
}
|
|
1245
|
+
// ── Evaluation ──────────────────────────────────────────────────────
|
|
1246
|
+
async evaluate(script) {
|
|
1247
|
+
return this.page.evaluate(script);
|
|
1248
|
+
}
|
|
1249
|
+
// ── Interactions ────────────────────────────────────────────────────
|
|
1250
|
+
async click(selector) {
|
|
1251
|
+
await this.page.click(selector);
|
|
1252
|
+
}
|
|
1253
|
+
async dblclick(selector) {
|
|
1254
|
+
await this.page.dblclick(selector);
|
|
1255
|
+
}
|
|
1256
|
+
async hover(selector) {
|
|
1257
|
+
await this.page.hover(selector);
|
|
1258
|
+
}
|
|
1259
|
+
async fill(selector, text) {
|
|
1260
|
+
await this.page.fill(selector, text);
|
|
1261
|
+
}
|
|
1262
|
+
async type(selector, text) {
|
|
1263
|
+
await this.page.locator(selector).pressSequentially(text);
|
|
1264
|
+
}
|
|
1265
|
+
async press(selector, key) {
|
|
1266
|
+
await this.page.press(selector, key);
|
|
1267
|
+
}
|
|
1268
|
+
async check(selector) {
|
|
1269
|
+
await this.page.check(selector);
|
|
1270
|
+
}
|
|
1271
|
+
async uncheck(selector) {
|
|
1272
|
+
await this.page.uncheck(selector);
|
|
1273
|
+
}
|
|
1274
|
+
async selectOption(selector, value) {
|
|
1275
|
+
const result = await this.page.selectOption(selector, value);
|
|
1276
|
+
return Array.isArray(result) ? result[0] : result;
|
|
1277
|
+
}
|
|
1278
|
+
async focus(selector) {
|
|
1279
|
+
await this.page.focus(selector);
|
|
1280
|
+
}
|
|
1281
|
+
async blur(selector) {
|
|
1282
|
+
await this.page.locator(selector).blur();
|
|
1283
|
+
}
|
|
1284
|
+
// ── Queries ─────────────────────────────────────────────────────────
|
|
1285
|
+
async textContent(selector) {
|
|
1286
|
+
return this.page.textContent(selector);
|
|
1287
|
+
}
|
|
1288
|
+
async innerHTML(selector) {
|
|
1289
|
+
return this.page.innerHTML(selector);
|
|
1290
|
+
}
|
|
1291
|
+
async innerText(selector) {
|
|
1292
|
+
return this.page.innerText(selector);
|
|
1293
|
+
}
|
|
1294
|
+
async allTextContents(selector) {
|
|
1295
|
+
return this.page.locator(selector).allTextContents();
|
|
1296
|
+
}
|
|
1297
|
+
async allInnerTexts(selector) {
|
|
1298
|
+
return this.page.locator(selector).allInnerTexts();
|
|
1299
|
+
}
|
|
1300
|
+
async getAttribute(selector, name) {
|
|
1301
|
+
return this.page.getAttribute(selector, name);
|
|
1302
|
+
}
|
|
1303
|
+
async inputValue(selector) {
|
|
1304
|
+
return this.page.inputValue(selector);
|
|
1305
|
+
}
|
|
1306
|
+
async boundingBox(selector) {
|
|
1307
|
+
return this.page.locator(selector).boundingBox();
|
|
1308
|
+
}
|
|
1309
|
+
// ── State checks ────────────────────────────────────────────────────
|
|
1310
|
+
async isVisible(selector) {
|
|
1311
|
+
return this.page.isVisible(selector);
|
|
1312
|
+
}
|
|
1313
|
+
async isChecked(selector) {
|
|
1314
|
+
return this.page.isChecked(selector);
|
|
1315
|
+
}
|
|
1316
|
+
async isDisabled(selector) {
|
|
1317
|
+
return this.page.isDisabled(selector);
|
|
1318
|
+
}
|
|
1319
|
+
async isEditable(selector) {
|
|
1320
|
+
return this.page.isEditable(selector);
|
|
1321
|
+
}
|
|
1322
|
+
async isHidden(selector) {
|
|
1323
|
+
return this.page.isHidden(selector);
|
|
1324
|
+
}
|
|
1325
|
+
async isEnabled(selector) {
|
|
1326
|
+
return this.page.isEnabled(selector);
|
|
1327
|
+
}
|
|
1328
|
+
// ── Waiting ─────────────────────────────────────────────────────────
|
|
1329
|
+
async waitForSelector(selector, timeout = 5e3) {
|
|
1330
|
+
await this.page.waitForSelector(selector, { timeout });
|
|
1331
|
+
}
|
|
1332
|
+
async waitForFunction(expression, timeout = 5e3) {
|
|
1333
|
+
await this.page.waitForFunction(expression, void 0, { timeout });
|
|
1334
|
+
}
|
|
1335
|
+
// ── Counting ────────────────────────────────────────────────────────
|
|
1336
|
+
async count(selector) {
|
|
1337
|
+
return this.page.locator(selector).count();
|
|
1338
|
+
}
|
|
1339
|
+
// ── Page info ───────────────────────────────────────────────────────
|
|
1340
|
+
async title() {
|
|
1341
|
+
return this.page.title();
|
|
1342
|
+
}
|
|
1343
|
+
async url() {
|
|
1344
|
+
return this.page.url();
|
|
1345
|
+
}
|
|
1346
|
+
async content() {
|
|
1347
|
+
return this.page.content();
|
|
1348
|
+
}
|
|
1349
|
+
// ── Navigation ──────────────────────────────────────────────────────
|
|
1350
|
+
async goto(url) {
|
|
1351
|
+
await this.page.goto(url);
|
|
1352
|
+
}
|
|
1353
|
+
async reload() {
|
|
1354
|
+
await this.page.reload();
|
|
1355
|
+
}
|
|
1356
|
+
async goBack() {
|
|
1357
|
+
await this.page.goBack();
|
|
1358
|
+
}
|
|
1359
|
+
async goForward() {
|
|
1360
|
+
await this.page.goForward();
|
|
1361
|
+
}
|
|
1362
|
+
async waitForURL(pattern, options) {
|
|
1363
|
+
await this.page.waitForURL(`**${pattern}**`, options);
|
|
1364
|
+
}
|
|
1365
|
+
async isFocused(selector) {
|
|
1366
|
+
return this.page.locator(selector).evaluate((el) => document.activeElement === el);
|
|
1367
|
+
}
|
|
1368
|
+
async getComputedStyle(selector, property) {
|
|
1369
|
+
return this.page.locator(selector).evaluate((el, prop) => getComputedStyle(el).getPropertyValue(prop), property);
|
|
1370
|
+
}
|
|
1371
|
+
async dispatchEvent(selector, eventType) {
|
|
1372
|
+
await this.page.locator(selector).dispatchEvent(eventType);
|
|
1373
|
+
}
|
|
1374
|
+
// ── Drag and drop ───────────────────────────────────────────────────
|
|
1375
|
+
async dragAndDrop(source, target) {
|
|
1376
|
+
await this.page.dragAndDrop(source, target);
|
|
1377
|
+
}
|
|
1378
|
+
// ── File upload ─────────────────────────────────────────────────────
|
|
1379
|
+
async setInputFiles(selector, files) {
|
|
1380
|
+
await this.page.setInputFiles(
|
|
1381
|
+
selector,
|
|
1382
|
+
files.map((f) => ({ name: f.name, mimeType: f.mimeType, buffer: f.buffer }))
|
|
1383
|
+
);
|
|
1384
|
+
return files.length;
|
|
1385
|
+
}
|
|
1386
|
+
async installDialogHandler(options) {
|
|
1387
|
+
const confirm = options?.defaultConfirm ?? true;
|
|
1388
|
+
const promptText = options?.defaultPromptText;
|
|
1389
|
+
if (this.dialogHandler) {
|
|
1390
|
+
this.page.removeListener("dialog", this.dialogHandler);
|
|
1391
|
+
}
|
|
1392
|
+
this.dialogHandler = async (dialog) => {
|
|
1393
|
+
this.capturedDialogs.push({
|
|
1394
|
+
type: dialog.type(),
|
|
1395
|
+
message: dialog.message(),
|
|
1396
|
+
default: dialog.defaultValue?.() ?? ""
|
|
1397
|
+
});
|
|
1398
|
+
if (dialog.type() === "prompt") {
|
|
1399
|
+
await dialog.accept(promptText ?? "");
|
|
1400
|
+
} else if (dialog.type() === "confirm") {
|
|
1401
|
+
if (confirm) await dialog.accept();
|
|
1402
|
+
else await dialog.dismiss();
|
|
1403
|
+
} else {
|
|
1404
|
+
await dialog.accept();
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
this.page.on("dialog", this.dialogHandler);
|
|
1408
|
+
}
|
|
1409
|
+
async getDialogs() {
|
|
1410
|
+
return this.capturedDialogs;
|
|
1411
|
+
}
|
|
1412
|
+
async clearDialogs() {
|
|
1413
|
+
this.capturedDialogs = [];
|
|
1414
|
+
}
|
|
1415
|
+
async route(pattern, response) {
|
|
1416
|
+
const handler = async (route) => {
|
|
1417
|
+
this.capturedRequests.push({
|
|
1418
|
+
url: route.request().url(),
|
|
1419
|
+
method: route.request().method(),
|
|
1420
|
+
timestamp: Date.now()
|
|
1421
|
+
});
|
|
1422
|
+
await route.fulfill({
|
|
1423
|
+
status: response.status ?? 200,
|
|
1424
|
+
body: response.body ?? "",
|
|
1425
|
+
contentType: response.contentType ?? "application/json"
|
|
1426
|
+
});
|
|
1427
|
+
};
|
|
1428
|
+
this.activeRoutes.push({ pattern, handler });
|
|
1429
|
+
await this.page.route(`**${pattern}**`, handler);
|
|
1430
|
+
}
|
|
1431
|
+
async unroute(pattern) {
|
|
1432
|
+
const route = this.activeRoutes.find((r) => r.pattern === pattern);
|
|
1433
|
+
if (route) {
|
|
1434
|
+
await this.page.unroute(`**${pattern}**`, route.handler);
|
|
1435
|
+
this.activeRoutes = this.activeRoutes.filter((r) => r.pattern !== pattern);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async clearRoutes() {
|
|
1439
|
+
for (const route of this.activeRoutes) {
|
|
1440
|
+
await this.page.unroute(`**${route.pattern}**`, route.handler);
|
|
1441
|
+
}
|
|
1442
|
+
this.activeRoutes = [];
|
|
1443
|
+
}
|
|
1444
|
+
async getNetworkRequests() {
|
|
1445
|
+
return this.capturedRequests;
|
|
1446
|
+
}
|
|
1447
|
+
async clearNetworkRequests() {
|
|
1448
|
+
this.capturedRequests = [];
|
|
1449
|
+
}
|
|
1450
|
+
// ── Semantic selectors ───────────────────────────────────────────────
|
|
1451
|
+
getByTestId(testId) {
|
|
1452
|
+
return this.page.getByTestId(testId);
|
|
1453
|
+
}
|
|
1454
|
+
getByPlaceholder(text, options) {
|
|
1455
|
+
return this.page.getByPlaceholder(text, options);
|
|
1456
|
+
}
|
|
1457
|
+
getByAltText(text, options) {
|
|
1458
|
+
return this.page.getByAltText(text, options);
|
|
1459
|
+
}
|
|
1460
|
+
getByTitle(text, options) {
|
|
1461
|
+
return this.page.getByTitle(text, options);
|
|
1462
|
+
}
|
|
1463
|
+
getByRole(role, options) {
|
|
1464
|
+
return this.page.getByRole(role, options);
|
|
1465
|
+
}
|
|
1466
|
+
getByText(text, options) {
|
|
1467
|
+
return this.page.getByText(text, options);
|
|
1468
|
+
}
|
|
1469
|
+
getByLabel(text, options) {
|
|
1470
|
+
return this.page.getByLabel(text, options);
|
|
1471
|
+
}
|
|
1472
|
+
// ── Keyboard & Mouse ────────────────────────────────────────────────
|
|
1473
|
+
get keyboard() {
|
|
1474
|
+
return this.page.keyboard;
|
|
1475
|
+
}
|
|
1476
|
+
get mouse() {
|
|
1477
|
+
return this.page.mouse;
|
|
1478
|
+
}
|
|
1479
|
+
// ── Capture ─────────────────────────────────────────────────────────
|
|
1480
|
+
async screenshot(options) {
|
|
1481
|
+
return this.page.screenshot({ path: options?.path });
|
|
1482
|
+
}
|
|
1483
|
+
async startRecording() {
|
|
1484
|
+
return { dir: "", fps: 0 };
|
|
1485
|
+
}
|
|
1486
|
+
async stopRecording() {
|
|
1487
|
+
return { dir: "", frame_count: 0, fps: 0, video: null };
|
|
1488
|
+
}
|
|
1489
|
+
// ── Locator ─────────────────────────────────────────────────────────
|
|
1490
|
+
locator(selector) {
|
|
1491
|
+
return this.page.locator(selector);
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// src/process-manager.ts
|
|
1496
|
+
import { spawn } from "child_process";
|
|
1497
|
+
import { existsSync } from "fs";
|
|
1498
|
+
var TauriProcessManager = class {
|
|
1499
|
+
constructor(config) {
|
|
1500
|
+
this.config = config;
|
|
1501
|
+
this.process = null;
|
|
1502
|
+
this.socketPath = config.socketPath ?? "/tmp/tauri-playwright.sock";
|
|
1503
|
+
this.tcpPort = config.tcpPort;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Start the Tauri app and wait for the plugin socket to become available.
|
|
1507
|
+
* Returns the socket path or TCP port to connect to.
|
|
1508
|
+
*/
|
|
1509
|
+
async start() {
|
|
1510
|
+
const cmd = this.config.command ?? "cargo";
|
|
1511
|
+
const args = this.config.args ?? ["tauri", "dev"];
|
|
1512
|
+
if (this.config.features?.length) {
|
|
1513
|
+
args.push("--features", this.config.features.join(","));
|
|
1514
|
+
}
|
|
1515
|
+
return new Promise((resolve, reject) => {
|
|
1516
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
1517
|
+
this.process = spawn(cmd, args, {
|
|
1518
|
+
cwd,
|
|
1519
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1520
|
+
env: {
|
|
1521
|
+
...process.env,
|
|
1522
|
+
// Ensure the plugin knows where to listen
|
|
1523
|
+
TAURI_PLAYWRIGHT_SOCKET: this.socketPath
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
const timeout = setTimeout(
|
|
1527
|
+
() => {
|
|
1528
|
+
reject(new Error(`Tauri app did not start within ${this.config.startTimeout ?? 120}s`));
|
|
1529
|
+
this.stop();
|
|
1530
|
+
},
|
|
1531
|
+
(this.config.startTimeout ?? 120) * 1e3
|
|
1532
|
+
);
|
|
1533
|
+
const onData = (data) => {
|
|
1534
|
+
const text = data.toString();
|
|
1535
|
+
if (text.includes("tauri-plugin-playwright: listening on unix:")) {
|
|
1536
|
+
clearTimeout(timeout);
|
|
1537
|
+
resolve({ socketPath: this.socketPath });
|
|
1538
|
+
} else if (text.includes("tauri-plugin-playwright: listening on tcp://")) {
|
|
1539
|
+
clearTimeout(timeout);
|
|
1540
|
+
const match = text.match(/tcp:\/\/127\.0\.0\.1:(\d+)/);
|
|
1541
|
+
const port = match ? parseInt(match[1]) : this.tcpPort ?? 6274;
|
|
1542
|
+
resolve({ tcpPort: port });
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
this.process.stdout?.on("data", onData);
|
|
1546
|
+
this.process.stderr?.on("data", onData);
|
|
1547
|
+
this.process.on("error", (err) => {
|
|
1548
|
+
clearTimeout(timeout);
|
|
1549
|
+
reject(err);
|
|
1550
|
+
});
|
|
1551
|
+
this.process.on("exit", (code) => {
|
|
1552
|
+
if (code !== null && code !== 0) {
|
|
1553
|
+
clearTimeout(timeout);
|
|
1554
|
+
reject(new Error(`Tauri process exited with code ${code}`));
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Also supports connecting to an already-running Tauri app.
|
|
1561
|
+
* Just waits for the socket file to appear.
|
|
1562
|
+
*/
|
|
1563
|
+
async waitForSocket(timeoutMs = 3e4) {
|
|
1564
|
+
const deadline = Date.now() + timeoutMs;
|
|
1565
|
+
while (Date.now() < deadline) {
|
|
1566
|
+
if (existsSync(this.socketPath)) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1570
|
+
}
|
|
1571
|
+
throw new Error(`Socket ${this.socketPath} did not appear within ${timeoutMs}ms`);
|
|
1572
|
+
}
|
|
1573
|
+
/** Kill the Tauri process. */
|
|
1574
|
+
stop() {
|
|
1575
|
+
if (this.process) {
|
|
1576
|
+
this.process.kill("SIGTERM");
|
|
1577
|
+
setTimeout(() => {
|
|
1578
|
+
if (this.process && !this.process.killed) {
|
|
1579
|
+
this.process.kill("SIGKILL");
|
|
1580
|
+
}
|
|
1581
|
+
}, 5e3);
|
|
1582
|
+
this.process = null;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
// src/fixture.ts
|
|
1588
|
+
function createTauriTest(config) {
|
|
1589
|
+
const tauriTest = base.extend({
|
|
1590
|
+
mode: ["browser", { option: true }],
|
|
1591
|
+
tauriPage: async ({ page, mode }, use, testInfo) => {
|
|
1592
|
+
if (mode === "browser") {
|
|
1593
|
+
if (config.ipcMocks) {
|
|
1594
|
+
await page.addInitScript(generateIpcMockScript(config.ipcMocks));
|
|
1595
|
+
} else {
|
|
1596
|
+
await page.addInitScript(generateIpcMockScript({}));
|
|
1597
|
+
}
|
|
1598
|
+
await page.goto(config.devUrl);
|
|
1599
|
+
await page.waitForLoadState("networkidle");
|
|
1600
|
+
const adapter = new BrowserPageAdapter(page);
|
|
1601
|
+
await use(adapter);
|
|
1602
|
+
} else if (mode === "cdp") {
|
|
1603
|
+
const endpoint = config.cdpEndpoint ?? "http://localhost:9222";
|
|
1604
|
+
let browser;
|
|
1605
|
+
try {
|
|
1606
|
+
browser = await chromium.connectOverCDP(endpoint);
|
|
1607
|
+
const context = browser.contexts()[0];
|
|
1608
|
+
if (!context)
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
"No browser context found \u2014 is the Tauri app running with CDP enabled?"
|
|
1611
|
+
);
|
|
1612
|
+
const cdpPage = context.pages()[0] ?? await context.newPage();
|
|
1613
|
+
await cdpPage.waitForLoadState("domcontentloaded");
|
|
1614
|
+
const adapter = new BrowserPageAdapter(cdpPage);
|
|
1615
|
+
await use(adapter);
|
|
1616
|
+
} finally {
|
|
1617
|
+
browser?.close().catch(() => {
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
} else {
|
|
1621
|
+
let processManager = null;
|
|
1622
|
+
let client = null;
|
|
1623
|
+
let tauriPage;
|
|
1624
|
+
try {
|
|
1625
|
+
const socketPath = config.mcpSocket ?? "/tmp/tauri-playwright.sock";
|
|
1626
|
+
if (config.tauriCommand) {
|
|
1627
|
+
const parts = config.tauriCommand.split(" ");
|
|
1628
|
+
processManager = new TauriProcessManager({
|
|
1629
|
+
command: parts[0],
|
|
1630
|
+
args: parts.slice(1),
|
|
1631
|
+
cwd: config.tauriCwd,
|
|
1632
|
+
features: config.tauriFeatures,
|
|
1633
|
+
socketPath,
|
|
1634
|
+
startTimeout: config.startTimeout ?? 120
|
|
1635
|
+
});
|
|
1636
|
+
await processManager.start();
|
|
1637
|
+
} else {
|
|
1638
|
+
const pm = new TauriProcessManager({ socketPath });
|
|
1639
|
+
await pm.waitForSocket(3e4);
|
|
1640
|
+
}
|
|
1641
|
+
client = new PluginClient(socketPath);
|
|
1642
|
+
await client.connect();
|
|
1643
|
+
const ping = await client.send({ type: "ping" });
|
|
1644
|
+
if (!ping.ok) {
|
|
1645
|
+
throw new Error("Plugin ping failed");
|
|
1646
|
+
}
|
|
1647
|
+
tauriPage = new TauriPage(client);
|
|
1648
|
+
if (config.devUrl) {
|
|
1649
|
+
await tauriPage.evaluate(`window.location.href = ${JSON.stringify(config.devUrl)}`);
|
|
1650
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1651
|
+
await tauriPage.waitForFunction('document.readyState === "complete"');
|
|
1652
|
+
}
|
|
1653
|
+
let recordingDir = null;
|
|
1654
|
+
try {
|
|
1655
|
+
const rec = await tauriPage.startRecording({
|
|
1656
|
+
path: testInfo.outputPath("recording"),
|
|
1657
|
+
fps: 15
|
|
1658
|
+
});
|
|
1659
|
+
recordingDir = rec.dir;
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
await use(tauriPage);
|
|
1663
|
+
let videoPath = null;
|
|
1664
|
+
if (recordingDir) {
|
|
1665
|
+
try {
|
|
1666
|
+
const result = await tauriPage.stopRecording();
|
|
1667
|
+
videoPath = result.video;
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (testInfo.status !== testInfo.expectedStatus) {
|
|
1672
|
+
try {
|
|
1673
|
+
const screenshotBuffer = await tauriPage.screenshot();
|
|
1674
|
+
if (screenshotBuffer.length > 0) {
|
|
1675
|
+
await testInfo.attach("native-screenshot", {
|
|
1676
|
+
body: screenshotBuffer,
|
|
1677
|
+
contentType: "image/png"
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (videoPath) {
|
|
1684
|
+
try {
|
|
1685
|
+
const { readFile } = await import("fs/promises");
|
|
1686
|
+
const videoBuffer = await readFile(videoPath);
|
|
1687
|
+
await testInfo.attach("video", {
|
|
1688
|
+
body: videoBuffer,
|
|
1689
|
+
contentType: "video/mp4"
|
|
1690
|
+
});
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
} finally {
|
|
1695
|
+
client?.disconnect();
|
|
1696
|
+
processManager?.stop();
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
return {
|
|
1702
|
+
test: tauriTest,
|
|
1703
|
+
expect: tauriExpect
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
async function getCapturedInvokes(page) {
|
|
1707
|
+
return page.evaluate(() => window.__TAURI_GET_MOCK_CALLS__());
|
|
1708
|
+
}
|
|
1709
|
+
async function clearCapturedInvokes(page) {
|
|
1710
|
+
await page.evaluate(() => window.__TAURI_CLEAR_MOCK_CALLS__());
|
|
1711
|
+
}
|
|
1712
|
+
async function emitMockEvent(page, event, payload) {
|
|
1713
|
+
await page.evaluate(({ event: event2, payload: payload2 }) => window.__TAURI_EMIT_MOCK_EVENT__(event2, payload2), {
|
|
1714
|
+
event,
|
|
1715
|
+
payload
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
export {
|
|
1719
|
+
BrowserPageAdapter,
|
|
1720
|
+
PluginClient,
|
|
1721
|
+
TauriKeyboard,
|
|
1722
|
+
TauriLocator,
|
|
1723
|
+
TauriMouse,
|
|
1724
|
+
TauriPage,
|
|
1725
|
+
TauriProcessManager,
|
|
1726
|
+
clearCapturedInvokes,
|
|
1727
|
+
createTauriTest,
|
|
1728
|
+
emitMockEvent,
|
|
1729
|
+
generateIpcMockScript,
|
|
1730
|
+
getCapturedInvokes,
|
|
1731
|
+
tauriExpect
|
|
1732
|
+
};
|