diff --git a/packages/browser-playwright/src/commands/pointer.ts b/packages/browser-playwright/src/commands/pointer.ts index a76a5b788..7a52ff5d6 100644 --- a/packages/browser-playwright/src/commands/pointer.ts +++ b/packages/browser-playwright/src/commands/pointer.ts @@ -5,7 +5,6 @@ import type { UserEventCommand } from './utils' import { parseKeyDef } from '@vitest/browser' import { click } from './click' import { hover } from './hover' -import { resolvePageCoordinates } from './utils' type SerializedPointerInput = ElementToSerializedLocator interface PointerReturnData extends Pick { @@ -56,14 +55,10 @@ export const pointer: UserEventCommand = async (context, input, st if (!hasOnlyClickActions) { if (target) { await hover(context, target, { - position: - coords && - (await resolvePageCoordinates(context, { x: coords.x ?? 0, y: coords.y ?? 0 }, true)), + position: coords, }) } else if (coords) { - const pageCoords = await resolvePageCoordinates(context, coords, false) - - await context.page.mouse.move(pageCoords.x, pageCoords.y) + await context.page.mouse.move(coords.x, coords.y) } } @@ -135,28 +130,13 @@ async function keyDefHandler( if (pointerAction.target) { await click(context, pointerAction.target, { ...mouseOptions, - position: - pointerAction.coords && - (await resolvePageCoordinates( - context, - { - x: pointerAction.coords?.x ?? 0, - y: pointerAction.coords?.y ?? 0, - }, - true, - )), + position: pointerAction.coords, }) } else { - const coords = await resolvePageCoordinates( - context, - { - x: pointerAction.coords?.x ?? 0, - y: pointerAction.coords?.y ?? 0, - }, - false, - ) - - await context.page.mouse.click(coords.x, coords.y, mouseOptions) + const x = pointerAction.coords?.x ?? 0 + const y = pointerAction.coords?.y ?? 0 + + await context.page.mouse.click(x, y, mouseOptions) } } else { await context.page.mouse.down(mouseOptions) diff --git a/packages/browser-playwright/src/commands/utils.ts b/packages/browser-playwright/src/commands/utils.ts index a8b7afbcb..34bd84437 100644 --- a/packages/browser-playwright/src/commands/utils.ts +++ b/packages/browser-playwright/src/commands/utils.ts @@ -25,36 +25,3 @@ export function getDescribedLocator( ? iframeLocator.describe(locator) : iframeLocator } - -interface Coordinates { - x?: number - y?: number -} - -export async function resolvePageCoordinates( - context: BrowserCommandContext, - coords: Coordinates, - onlyScale: boolean, -): Promise> { - const x = coords?.x ?? 0 - const y = coords?.y ?? 0 - - if (!context.project.config.browser.ui) { - return { x, y } - } - - const { iframeX, iframeY, scale } = await context.iframe.owner().evaluate((iframe) => { - const { x, y } = iframe.getBoundingClientRect() - const scale = new DOMMatrix(getComputedStyle(iframe).transform).a - - return { iframeX: x, iframeY: y, scale } - }) - - const offsetX = onlyScale ? 0 : iframeX - const offsetY = onlyScale ? 0 : iframeY - - return { - x: offsetX + x * scale, - y: offsetY + y * scale, - } -} diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 9ba79292f..8c0381dcf 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -483,7 +483,9 @@ export type UserEventPointerInput = PointerActionInput | readonly PointerActionI * * @internal */ -export type UserEventPointerInputNormalized = readonly PointerActionInputObject[] +export type UserEventPointerInputNormalized = readonly (PointerActionInputObject & { + coords?: Required +})[] type PointerActionInput = string | PointerActionInputObject type PointerActionInputObject = PointerActionKeys | PointerMoveAction diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 892fbe2a3..3b8fc15ee 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -23,7 +23,13 @@ import { vi } from 'vitest' import { __INTERNAL, stringify } from 'vitest/internal/browser' import { ensureAwaited, getBrowserState, getWorkerState } from '../utils' import { ScreenshotAction } from './action' -import { isLocator, resolveUserEventWheelOptions, serializeElement } from './tester-utils' +import { + getIframeOffset, + getIframeScale, + isLocator, + resolveUserEventWheelOptions, + serializeElement, +} from './tester-utils' import { createBrowserTraceRangeId, recordBrowserTraceEntry } from './trace' // this file should not import anything directly, only types and utils @@ -92,29 +98,28 @@ export function createUserEvent( ? { [K in keyof PIN]: K extends 'target' ? SerializedLocator : PIN[K] } : never + const scale = getIframeScale() + const offset = getIframeOffset() const inputArray = (Array.isArray(input) ? input : [input]) as Extract< UserEventPointerInput, readonly any[] > const serializedInputArray = await Promise.all( - inputArray.map(async (input) => { + inputArray.map>(async (input) => { if (typeof input === 'string') { return { keys: input, - } satisfies SerializedInput + } } - if (input.target) { - const target = await serializeElement(input.target) - - return { - ...input, - target, - } satisfies SerializedInput + return { + ...input, + target: input.target && (await serializeElement(input.target)), + coords: input.coords && { + x: input.coords.x ? input.coords.x * scale + (input.target ? 0 : offset.x) : 0, + y: input.coords.y ? input.coords.y * scale + (input.target ? 0 : offset.y) : 0, + }, } - - // `target` has been serialized but TS doesn't resolve/remove it from whatever's left of `PointerActionInputObject` - return input as SerializedInput }), ) @@ -333,23 +338,29 @@ function createPreviewUserEvent( UserEventPointerInput, readonly any[] > - const normalizedInput = inputArray.map((input) => { + const normalizedInput = inputArray.map((input) => { if (typeof input === 'string') { return { keys: input } satisfies SerializedInput } const target = input.target - if (target && isLocator(target)) { - return { - ...input, - get target() { - return target.element() - }, - } satisfies SerializedInput - } + return { + ...input, + get target() { + const value = isLocator(target) ? target.element() : target - return input as SerializedInput + Reflect.defineProperty(this, 'target', { + enumerable: true, + configurable: true, + writable: true, + value, + }) + + return value + }, + coords: input.coords && { x: input.coords.x ?? 0, y: input.coords.y ?? 0 }, + } }) await userEvent.pointer(normalizedInput) diff --git a/packages/browser/src/client/tester/tester-utils.ts b/packages/browser/src/client/tester/tester-utils.ts index eb3a7b245..52d640b7a 100644 --- a/packages/browser/src/client/tester/tester-utils.ts +++ b/packages/browser/src/client/tester/tester-utils.ts @@ -224,7 +224,7 @@ export class CommandsManager { } } -export function getIframeScale(): number { +function getIframe() { const iframe = window.frameElement if (!iframe) { @@ -233,6 +233,11 @@ export function getIframeScale(): number { ) } + return iframe as HTMLIFrameElement +} + +export function getIframeScale(): number { + const iframe = getIframe() // DOMMatrix parses the computed 2D transform matrix [a, b, c, d, e, f] // `a` and `d` are the x and y scale factors - since we only apply uniform scaling, `a === d` const scale = new DOMMatrix(getComputedStyle(iframe).transform).a @@ -240,6 +245,12 @@ export function getIframeScale(): number { return scale } +export function getIframeOffset(): { x: number; y: number } { + const iframe = getIframe() + + return iframe.getBoundingClientRect() +} + function escapeRegexForSelector(re: RegExp): string { // Unicode mode does not allow "identity character escapes", so we do not escape and // hope that it does not contain quotes and/or >> signs.