@specific.dev/spectest 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locator.js +78 -5
- package/package.json +1 -1
package/dist/locator.js
CHANGED
|
@@ -196,15 +196,74 @@ export const desktopStrategy = {
|
|
|
196
196
|
* the press dwell RN Pressables need (playwright's `tap()` has no dwell). */
|
|
197
197
|
export const mobileStrategy = {
|
|
198
198
|
touch: true,
|
|
199
|
-
async tap(backend, loc, opts) {
|
|
199
|
+
async tap(backend, loc, opts, rec) {
|
|
200
200
|
await loc.waitFor({ state: "visible", timeout: opts?.timeout });
|
|
201
201
|
await loc.scrollIntoViewIfNeeded({ timeout: opts?.timeout });
|
|
202
202
|
const box = await loc.boundingBox();
|
|
203
203
|
if (!box)
|
|
204
204
|
throw new Error("tap: element vanished before it could be tapped");
|
|
205
|
-
|
|
205
|
+
const x = box.x + box.width / 2;
|
|
206
|
+
const y = box.y + box.height / 2;
|
|
207
|
+
// Exact, no probe needed: this IS the point the touch is dispatched at,
|
|
208
|
+
// measured after the scroll. Same field the replay reads — see
|
|
209
|
+
// stampActionPoint.
|
|
210
|
+
if (rec) {
|
|
211
|
+
rec.x = Math.round(x);
|
|
212
|
+
rec.y = Math.round(y);
|
|
213
|
+
}
|
|
214
|
+
await backend.rawTap(x, y, opts?.duration);
|
|
206
215
|
},
|
|
207
216
|
};
|
|
217
|
+
/** How long the click-point probe waits for the element. Short on purpose:
|
|
218
|
+
* either it is already on screen and the answer is one round trip away, or
|
|
219
|
+
* the action's own actionability wait — with the author's timeout and a real
|
|
220
|
+
* error message — is the one that should report it. */
|
|
221
|
+
const POINT_PROBE_MS = 250;
|
|
222
|
+
/** Stamp the viewport point an action is about to act on onto its (still
|
|
223
|
+
* open) step, as the `x`/`y` a raw `browser.clickAt` records.
|
|
224
|
+
*
|
|
225
|
+
* The replay cursor is normally positioned from rrweb's own recorded mouse
|
|
226
|
+
* events — but a click that navigates loses them. The page-side event buffer
|
|
227
|
+
* lives on `window`, so everything the click emitted dies with the document
|
|
228
|
+
* before the post-op drain can collect it, and the step arrives carrying only
|
|
229
|
+
* the destination page's load. The step's own coordinates are then the sole
|
|
230
|
+
* record of where the click landed; without them the viewer has nothing to
|
|
231
|
+
* put the cursor on.
|
|
232
|
+
*
|
|
233
|
+
* Best-effort in every direction. A probe failure (not attached yet, strict
|
|
234
|
+
* violation, frame detached) leaves the step unstamped and lets the action
|
|
235
|
+
* itself report the problem. We only stamp a centre that is inside the
|
|
236
|
+
* viewport of the top-level document, because that is exactly the case where
|
|
237
|
+
* playwright clicks the element where it currently sits — one that is
|
|
238
|
+
* off-screen (or in an iframe, whose rect is frame-relative) gets scrolled
|
|
239
|
+
* into view first, which moves the point we measured. */
|
|
240
|
+
async function stampActionPoint(loc, rec, position) {
|
|
241
|
+
try {
|
|
242
|
+
const pt = await loc.evaluate((el, pos) => {
|
|
243
|
+
if (window.top !== window)
|
|
244
|
+
return null;
|
|
245
|
+
const r = el.getBoundingClientRect();
|
|
246
|
+
if (!r.width || !r.height)
|
|
247
|
+
return null;
|
|
248
|
+
// Playwright clicks the element's centre unless the caller named a
|
|
249
|
+
// point — which it takes relative to the PADDING box, so an element
|
|
250
|
+
// with a border (a plain `<button>` has 2px of it) sits that far off
|
|
251
|
+
// the rect we measured.
|
|
252
|
+
const cs = pos ? getComputedStyle(el) : null;
|
|
253
|
+
const x = cs ? r.left + parseFloat(cs.borderLeftWidth) + pos.x : r.left + r.width / 2;
|
|
254
|
+
const y = cs ? r.top + parseFloat(cs.borderTopWidth) + pos.y : r.top + r.height / 2;
|
|
255
|
+
const onScreen = x >= 0 && y >= 0 && x <= window.innerWidth && y <= window.innerHeight;
|
|
256
|
+
return onScreen ? { x, y } : null;
|
|
257
|
+
}, position, { timeout: POINT_PROBE_MS });
|
|
258
|
+
if (pt) {
|
|
259
|
+
rec.x = Math.round(pt.x);
|
|
260
|
+
rec.y = Math.round(pt.y);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
/* Element not ready / gone / strict violation — the action reports it. */
|
|
265
|
+
}
|
|
266
|
+
}
|
|
208
267
|
// ────────────────────────────────────────────────────────────────────────
|
|
209
268
|
// Factory
|
|
210
269
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -216,6 +275,14 @@ export function makeLocator(backend, strategy, chain) {
|
|
|
216
275
|
const extend = (step) => makeLocator(backend, strategy, { steps: [...chain.steps, step] });
|
|
217
276
|
// One recorded event, result NOT wrapped (void/action).
|
|
218
277
|
const act = (action, fields, fn) => backend.pageOp(action, { selector: label, ...fields }, (page) => fn(lower(page, chain), page));
|
|
278
|
+
// One recorded event whose fields the action itself finishes filling in:
|
|
279
|
+
// `rec` is the very object the recorder spreads once `fn` resolves (the
|
|
280
|
+
// same mutate-in-flight seam `screenshot`'s artifact id rides), so the
|
|
281
|
+
// click family can stamp the point it acted on. See stampActionPoint.
|
|
282
|
+
const actAt = (action, fn) => {
|
|
283
|
+
const rec = { selector: label };
|
|
284
|
+
return backend.pageOp(action, rec, (page) => fn(lower(page, chain), rec));
|
|
285
|
+
};
|
|
219
286
|
// One recorded event, result provenance-wrapped for expect().
|
|
220
287
|
const read = (action, fn, fields = {}) => backend.pageOp(action, { selector: label, ...fields }, (page) => fn(lower(page, chain)), {
|
|
221
288
|
wrap: true,
|
|
@@ -250,9 +317,15 @@ export function makeLocator(backend, strategy, chain) {
|
|
|
250
317
|
last: () => extend({ m: "last", args: [] }),
|
|
251
318
|
nth: (i) => extend({ m: "nth", args: [i] }),
|
|
252
319
|
// actions
|
|
253
|
-
click: (opts) =>
|
|
254
|
-
|
|
255
|
-
|
|
320
|
+
click: (opts) => actAt("click", async (l, rec) => {
|
|
321
|
+
await stampActionPoint(l, rec, opts?.position);
|
|
322
|
+
return l.click(opts);
|
|
323
|
+
}),
|
|
324
|
+
dblclick: (opts) => actAt("dblclick", async (l, rec) => {
|
|
325
|
+
await stampActionPoint(l, rec, opts?.position);
|
|
326
|
+
return l.dblclick(opts);
|
|
327
|
+
}),
|
|
328
|
+
tap: (opts) => actAt("tap", (l, rec) => strategy.tap(backend, l, opts, rec)),
|
|
256
329
|
fill: (value, opts) => {
|
|
257
330
|
const t = truncateUtf8(value);
|
|
258
331
|
return act("fill", { text: t.value, textTruncated: t.truncated }, (l) => l.fill(value, { timeout: opts?.timeout }));
|