Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: touch events emulation guide #34201

Merged
merged 10 commits into from
Jan 8, 2025
Merged
136 changes: 136 additions & 0 deletions docs/src/touch-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---

Check warning on line 1 in docs/src/touch-events.md

View workflow job for this annotation

GitHub Actions / Lint snippets

js linting error

Error: Imports "Locator" are only used as type. > 1 | import { test, expect, devices, Locator } from '@playwright/test'; | ^ 2 | 3 | test.use({ ...devices['Pixel 7'] }); 4 | Unable to lint: import { test, expect, devices, Locator } from '@playwright/test'; test.use({ ...devices['Pixel 7'] }); async function pan(locator: Locator, deltaX?: number, deltaY?: number, steps?: number) { const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => { const bounds = target.getBoundingClientRect(); const centerX = bounds.left + bounds.width / 2; const centerY = bounds.top + bounds.height / 2; return { centerX, centerY }; }); // Providing only clientX and clientY as the app only cares about those. const touches = [{ identifier: 0, clientX: centerX, clientY: centerY, }]; await locator.dispatchEvent('touchstart', { touches, changedTouches: touches, targetTouches: touches }); steps = steps ?? 5; deltaX = deltaX ?? 0; deltaY = deltaY ?? 0; for (let i = 0; i <= steps; i++) { const touches = [{ identifier: 0, clientX: centerX + deltaX * i / steps, clientY: centerY + deltaY * i / steps, }]; await locator.dispatchEvent('touchmove', { touches, changedTouches: touches, targetTouches: touches }); } await locator.dispatchEvent('touchend'); } test(`pan gesture to move the map`, async ({ page }) => { await page.goto('https://www.google.com/maps/place/@37.4117722,-122.0713234,15z', { waitUntil: 'commit' }); await page.getByRole('button', { name: 'Keep using web' }).click(); await expect(page.getByRole('button', { name: 'Keep using web' })).not.toBeVisible(); // Get the map element. const met = page.locator('[data-test-id="met"]'); for (let i = 0; i < 5; i++) await pan(met, 200, 100); // Ensure the map has been moved. await expect(met).toHaveScreenshot(); });

Check warning on line 1 in docs/src/touch-events.md

View workflow job for this annotation

GitHub Actions / Lint snippets

js linting error

Error: Imports "Locator" are only used as type. > 1 | import { test, expect, devices, Locator } from '@playwright/test'; | ^ 2 | 3 | test.use({ ...devices['Pixel 7'] }); 4 | Unable to lint: import { test, expect, devices, Locator } from '@playwright/test'; test.use({ ...devices['Pixel 7'] }); async function pinch(locator: Locator, arg: { deltaX?: number, deltaY?: number, steps?: number, direction?: 'in' | 'out' }) { const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => { const bounds = target.getBoundingClientRect(); const centerX = bounds.left + bounds.width / 2; const centerY = bounds.top + bounds.height / 2; return { centerX, centerY }; }); const deltaX = arg.deltaX ?? 0; const deltaY = arg.deltaY ?? 0; // Two touch points equally distant from the center of the element. const touches = [ { identifier: 0, clientX: centerX + (arg.direction === 'in' ? - deltaX : -10), clientY: centerY, }, { identifier: 1, clientX: centerX + (arg.direction === 'in' ? deltaX : 10), clientY: centerY, }, ]; await locator.dispatchEvent('touchstart', { touches, changedTouches: touches, targetTouches: touches }); // Move the touch points towards or away from each other. const steps = arg.steps ?? 5; for (let i = 0; i < steps; i++) { const touches = [ { identifier: 0, clientX: centerX + deltaX * i / steps + (arg.direction === 'in' ? - deltaX : 0), clientY: centerY + deltaY * i / steps + (arg.direction === 'in' ? - deltaY : 0), }, { identifier: 0, clientX: centerX - deltaX * i / steps + (arg.direction === 'in' ? deltaX : 0), clientY: centerY - deltaY * i / steps + (arg.direction === 'in' ? deltaY : 0), }, ]; await locator.dispatchEvent('touchmove', { touches, changedTouches: touches, targetTouches: touches }); } await locator.dispatchEvent('touchend', { touches: [], changedTouches: [], targetTouches: [] }); } test(`pinch in gesture to zoom out the map`, async ({ page }) => { await page.goto('https://www.google.com/maps/place/@37.4117722,-122.0713234,15z', { waitUntil: 'commit' }); await page.getByRole('button', { name: 'Keep using web' }).click(); await expect(page.getByRole('button', { name: 'Keep using web' })).not.toBeVisible(); // Get the map element. const met = page.locator('[data-test-id="met"]'); for (let i = 0; i < 5; i++) await pinch(met, { deltaX: 40, direction: 'in' }); // Ensure the map has been zoomed out. await expect(met).toHaveScreenshot(); });
id: touch-events
title: "Emulating touch events"
---

## Introduction

Mobile web sites may listen to [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) and react to user touch gestures such swipe, pinch, tap etc. To test such functionality you can manually generate [TouchEvent]s in the page context using [`method: Locator.evaluate`].
yury-s marked this conversation as resolved.
Show resolved Hide resolved

If your web application relies on [pointer events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) instead of touch events, you can use [`method: Locator.click`] and raw mouse events to simulate single touch point events, because pointer events are triggered for both mouse gestures and touch interactions.
yury-s marked this conversation as resolved.
Show resolved Hide resolved

### Dispatching TouchEvent

You can dispatch touch events to the page using [`method: Locator.dispatchEvent`]. [Touch](https://developer.mozilla.org/en-US/docs/Web/API/Touch) points can be passed as arguments, see examples below.

#### Emulating pan gesture

In the example below, we emulate pan gesture that is expected to move the map. We only initialize `clientX/clientY` coordinates of the touch point as the we app reads only those, in a more complex scenario you may need to also set `pageX/pageY/screenX/screenY` if your app needs them.
yury-s marked this conversation as resolved.
Show resolved Hide resolved

```js
import { test, expect, devices, Locator } from '@playwright/test';

test.use({ ...devices['Pixel 7'] });

async function pan(locator: Locator, deltaX?: number, deltaY?: number, steps?: number) {
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
const bounds = target.getBoundingClientRect();
const centerX = bounds.left + bounds.width / 2;
const centerY = bounds.top + bounds.height / 2;
return { centerX, centerY };
});

// Providing only clientX and clientY as the app only cares about those.
const touches = [{
identifier: 0,
clientX: centerX,
clientY: centerY,
}];
await locator.dispatchEvent('touchstart', { touches, changedTouches: touches, targetTouches: touches });

steps = steps ?? 5;
deltaX = deltaX ?? 0;
deltaY = deltaY ?? 0;
for (let i = 0; i <= steps; i++) {
const touches = [{
identifier: 0,
clientX: centerX + deltaX * i / steps,
clientY: centerY + deltaY * i / steps,
}];
await locator.dispatchEvent('touchmove', { touches, changedTouches: touches, targetTouches: touches });
yury-s marked this conversation as resolved.
Show resolved Hide resolved
}

await locator.dispatchEvent('touchend');
}

test(`pan gesture to move the map`, async ({ page }) => {
await page.goto('https://www.google.com/maps/place/@37.4117722,-122.0713234,15z', { waitUntil: 'commit' });
await page.getByRole('button', { name: 'Keep using web' }).click();
await expect(page.getByRole('button', { name: 'Keep using web' })).not.toBeVisible();
// Get the map element.
const met = page.locator('[data-test-id="met"]');
for (let i = 0; i < 5; i++)
await pan(met, 200, 100);
// Ensure the map has been moved.
await expect(met).toHaveScreenshot();
});
```

#### Emulating pinch gesture

In the example below, we emulate pinch gesture, i.e. two touch points moving closer to each other. It is expected to zoom out the map. We only initialize `clientX/clientY` coordinates of the touch point as the we app reads only those, in a more complex scenario you may need to also set `pageX/pageY/screenX/screenY` if your app needs them.
yury-s marked this conversation as resolved.
Show resolved Hide resolved

```js
import { test, expect, devices, Locator } from '@playwright/test';

test.use({ ...devices['Pixel 7'] });

async function pinch(locator: Locator, arg: { deltaX?: number, deltaY?: number, steps?: number, direction?: 'in' | 'out' }) {
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
const bounds = target.getBoundingClientRect();
const centerX = bounds.left + bounds.width / 2;
const centerY = bounds.top + bounds.height / 2;
return { centerX, centerY };
});

const deltaX = arg.deltaX ?? 0;
const deltaY = arg.deltaY ?? 0;

// Two touch points equally distant from the center of the element.
const touches = [
{
identifier: 0,
clientX: centerX + (arg.direction === 'in' ? - deltaX : -10),
clientY: centerY,
},
{
identifier: 1,
clientX: centerX + (arg.direction === 'in' ? deltaX : 10),
clientY: centerY,
},
];
await locator.dispatchEvent('touchstart', { touches, changedTouches: touches, targetTouches: touches });

// Move the touch points towards or away from each other.
const steps = arg.steps ?? 5;
for (let i = 0; i < steps; i++) {
const touches = [
{
identifier: 0,
clientX: centerX + deltaX * i / steps + (arg.direction === 'in' ? - deltaX : 0),
clientY: centerY + deltaY * i / steps + (arg.direction === 'in' ? - deltaY : 0),
},
{
identifier: 0,
clientX: centerX - deltaX * i / steps + (arg.direction === 'in' ? deltaX : 0),
clientY: centerY - deltaY * i / steps + (arg.direction === 'in' ? deltaY : 0),
},
];
await locator.dispatchEvent('touchmove', { touches, changedTouches: touches, targetTouches: touches });
}

await locator.dispatchEvent('touchend', { touches: [], changedTouches: [], targetTouches: [] });
}

test(`pinch in gesture to zoom out the map`, async ({ page }) => {
await page.goto('https://www.google.com/maps/place/@37.4117722,-122.0713234,15z', { waitUntil: 'commit' });
await page.getByRole('button', { name: 'Keep using web' }).click();
await expect(page.getByRole('button', { name: 'Keep using web' })).not.toBeVisible();
// Get the map element.
const met = page.locator('[data-test-id="met"]');
for (let i = 0; i < 5; i++)
await pinch(met, { deltaX: 40, direction: 'in' });
// Ensure the map has been zoomed out.
await expect(met).toHaveScreenshot();
});
```
Loading