fix(ui): язык интерфейса по системе (не по боту) + pinch-zoom не триггерит back #65
@@ -0,0 +1,45 @@
|
|||||||
|
import { expect, test, type Page } from './fixtures';
|
||||||
|
|
||||||
|
// The edge swipe-back gesture (components/Screen.svelte) against the mock transport.
|
||||||
|
// Real multi-touch pinch is not reproducible in Playwright, so this drives the handler
|
||||||
|
// directly with synthetic PointerEvents (it listens at the window in the capture phase):
|
||||||
|
// a lone finger swiping right from the left edge navigates back; a two-finger gesture
|
||||||
|
// (a pinch-zoom, whose release would otherwise read as a swipe) does not.
|
||||||
|
|
||||||
|
async function loginLobby(page: Page): Promise<void> {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('button', { name: /guest/i }).click();
|
||||||
|
await expect(page.getByText('Your turn')).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointer(page: Page, type: string, x: number, y: number, id: number): Promise<void> {
|
||||||
|
return page.evaluate(
|
||||||
|
({ type, x, y, id }) => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new PointerEvent(type, { clientX: x, clientY: y, pointerId: id, pointerType: 'touch', bubbles: true, cancelable: true }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ type, x, y, id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('edge swipe-back: a lone finger navigates back, a pinch does not', async ({ page }) => {
|
||||||
|
await loginLobby(page);
|
||||||
|
|
||||||
|
// Open the Settings hub (back = lobby).
|
||||||
|
await page.getByRole('button', { name: /Settings/ }).click();
|
||||||
|
await expect(page.getByText('Your turn')).toBeHidden();
|
||||||
|
|
||||||
|
// A two-finger gesture from the left edge is a pinch (its second finger joins after the
|
||||||
|
// first): releasing it must NOT navigate back — we stay on Settings.
|
||||||
|
await pointer(page, 'pointerdown', 8, 300, 1);
|
||||||
|
await pointer(page, 'pointerdown', 60, 300, 2);
|
||||||
|
await pointer(page, 'pointerup', 260, 300, 1);
|
||||||
|
await pointer(page, 'pointerup', 90, 300, 2);
|
||||||
|
await expect(page.getByText('Your turn')).toBeHidden();
|
||||||
|
|
||||||
|
// A single finger swiping right from the left edge does return to the lobby.
|
||||||
|
await pointer(page, 'pointerdown', 8, 300, 1);
|
||||||
|
await pointer(page, 'pointerup', 260, 300, 1);
|
||||||
|
await expect(page.getByText('Your turn')).toBeVisible();
|
||||||
|
});
|
||||||
@@ -42,22 +42,53 @@
|
|||||||
// a zoomed-in board (it pans), and text inputs.
|
// a zoomed-in board (it pans), and text inputs.
|
||||||
const EDGE_FRACTION = 0.5;
|
const EDGE_FRACTION = 0.5;
|
||||||
const SWIPE_SKIP = '[data-rack], .cell.pending, .viewport.zoomed, input, textarea, select';
|
const SWIPE_SKIP = '[data-rack], .cell.pending, .viewport.zoomed, input, textarea, select';
|
||||||
|
// A pinch-zoom on the board is two fingers; its second finger lands after the gesture began
|
||||||
|
// (the board is not yet .zoomed at the first touch, so the hit-test above can't skip it), and
|
||||||
|
// releasing it would otherwise read as a horizontal back-swipe. Track the live pointer count
|
||||||
|
// (in the capture phase, so we see every touch even if the board stops propagation) and cancel
|
||||||
|
// the swipe the moment a second finger joins. The swipe fires only for a lone finger.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let active = 0; // pointers currently down
|
||||||
|
let tracking = false; // a candidate single-finger edge swipe is in progress
|
||||||
|
let pinched = false; // a second finger joined — not a back-swipe
|
||||||
|
|
||||||
function onDown(e: PointerEvent) {
|
function onDown(e: PointerEvent) {
|
||||||
|
active++;
|
||||||
|
if (active > 1) {
|
||||||
|
pinched = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tracking = false;
|
||||||
|
pinched = false;
|
||||||
if (!back || e.pointerType === 'mouse' || e.clientX > window.innerWidth * EDGE_FRACTION) return;
|
if (!back || e.pointerType === 'mouse' || e.clientX > window.innerWidth * EDGE_FRACTION) return;
|
||||||
if (document.elementFromPoint(e.clientX, e.clientY)?.closest(SWIPE_SKIP)) return;
|
if (document.elementFromPoint(e.clientX, e.clientY)?.closest(SWIPE_SKIP)) return;
|
||||||
const x0 = e.clientX;
|
startX = e.clientX;
|
||||||
const y0 = e.clientY;
|
startY = e.clientY;
|
||||||
const onUp = (ev: PointerEvent) => {
|
tracking = true;
|
||||||
window.removeEventListener('pointerup', onUp, true);
|
|
||||||
const dx = ev.clientX - x0;
|
|
||||||
const dy = ev.clientY - y0;
|
|
||||||
if (back && dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back);
|
|
||||||
};
|
|
||||||
window.addEventListener('pointerup', onUp, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUp(e: PointerEvent) {
|
||||||
|
if (active > 0) active--;
|
||||||
|
if (active > 0) return; // wait until every finger has lifted
|
||||||
|
if (tracking && !pinched && back) {
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
if (dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back);
|
||||||
|
}
|
||||||
|
tracking = false;
|
||||||
|
pinched = false;
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('pointerdown', onDown, true);
|
window.addEventListener('pointerdown', onDown, true);
|
||||||
return () => window.removeEventListener('pointerdown', onDown, true);
|
window.addEventListener('pointerup', onUp, true);
|
||||||
|
window.addEventListener('pointercancel', onUp, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointerdown', onDown, true);
|
||||||
|
window.removeEventListener('pointerup', onUp, true);
|
||||||
|
window.removeEventListener('pointercancel', onUp, true);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user