Touch-Interaktionen automatisiert testen
PASS
expect()
Testing · Touch Events · Cypress · Playwright
Touch-Interaktionen automatisiert testen
Tap, Swipe und Pinch-Zoom zuverlässig prüfen

Mobile Shops leben von Touch-Gesten: Wischen durch Bildergalerien, Antippen von Filterschaltern, Ziehen von Off-Canvas-Menüs. Wer diese Interaktionen nur mit simulierten Mausklicks testet, übersieht Ghost-Clicks, blockierte Scrollrichtungen und Hover-Menüs, die auf einem Touchscreen nie erreichbar sind. Dieser Artikel zeigt, wie echte Touch-Events in Cypress und Playwright zuverlässig simuliert werden.

16 Min. Lesezeit Touch Events · Ghost-Click · touch-action Cypress · Playwright · Mobile QA

1. Warum Touch-Interaktionen eigene Teststrategien brauchen

Die meisten E2E-Frameworks simulieren standardmäßig einen Mausklick, selbst wenn der Test einen mobilen Viewport emuliert. Ein cy.click() oder page.click() dispatcht intern eine Kette von MouseEvents, unabhängig davon, ob viewport('iphone-x') gesetzt ist. Das Problem: Der überwiegende Teil der Besucher eines Magento- oder Hyvä-Shops navigiert real über einen Touchscreen, nicht über eine Maus. Ein Test, der nur MouseEvents auslöst, prüft also ein Interaktionsmodell, das die eigentliche Zielgruppe gar nicht verwendet.

In der Praxis führt das zu einer trügerischen grünen Testsuite: Ein Dropdown-Menü, das per CSS :hover geöffnet wird, besteht jeden Klick-basierten Test anstandslos, weil Selenium, Cypress und Playwright per Default auch einen Hover-Zustand simulieren können. Auf einem echten Smartphone gibt es diesen Hover-Zustand aber nicht, das Menü bleibt für reale Nutzer unerreichbar. Genauso verhält es sich mit Swipe-Karussells und Off-Canvas-Filtern, deren JavaScript-Logik ausschließlich auf touchstart, touchmove und touchend lauscht. Wer solche Muster testen will, muss die Ebene der synthetischen Events verlassen und echte Touch-Sequenzen nachbilden.

2. Touch Events, Pointer Events und MouseEvents im Vergleich

Ein TouchEvent trägt drei Listen mit Kontaktpunkten: touches (alle aktuellen Kontakte auf dem Bildschirm), targetTouches (Kontakte auf dem aktuellen Zielelement) und changedTouches (die Kontakte, die das aktuelle Event ausgelöst haben). Jeder Eintrag besitzt eigene clientX/clientY-Koordinaten und eine identifier-ID, über die sich mehrere gleichzeitige Finger auseinanderhalten lassen, etwa für Pinch-Zoom-Gesten mit zwei Kontaktpunkten.

Das PointerEvent-Interface vereinheitlicht Maus, Touch und Stift unter einer gemeinsamen API und trägt zusätzlich ein pointerType-Attribut (mouse, touch oder pen). Moderne Browser feuern bei einer echten Berührung eine feste Reihenfolge: pointerdown, gefolgt von touchstart, und erst danach, zur Abwärtskompatibilität mit altem Code, synthetische mousedown/click-Events. Genau diese Kompatibilitätsschicht ist der Grund, warum ein simpler click()-Aufruf in Tests häufig funktioniert, obwohl er den touchbasierten Code-Pfad der Anwendung nie durchläuft: Der Test löst direkt das synthetische Ende der Kette aus und überspringt touchstart/touchmove vollständig.

3. Der Klick-Delay und das Ghost-Click-Problem

Mobile Browser warteten historisch rund 300 Millisekunden nach touchend, bevor sie das click-Event feuerten, um einen möglichen Doppel-Tap zum Zoomen von einem einzelnen Tap zu unterscheiden. Mit <meta name="viewport" content="width=device-width"> entfällt dieser Delay in modernen Browsern zwar meist, das Grundproblem der Event-Verdopplung bleibt aber bestehen: Auf manchen Geräten und in Hybrid-WebViews feuert eine einzelne Berührung sowohl einen echten Touch-Handler als auch ein nachgelagertes synthetisches click, wodurch Aktionen wie „In den Warenkorb" versehentlich zweimal ausgelöst werden.

Dieses als Ghost-Click bekannte Phänomen entsteht typischerweise, wenn ein touchend-Handler bereits eine Aktion auslöst und preventDefault() vergessen wird, sodass der Browser zusätzlich das nachfolgende click feuert. Ein Test, der ausschließlich click() aufruft, kann diesen Bug grundsätzlich nicht aufdecken, weil er nie die vollständige Touch-Sequenz mit touchstart und touchend dispatcht, aus der der Doppel-Trigger überhaupt erst entsteht. Nur ein Test, der echte Touch-Events sendet und danach die Anzahl der ausgelösten Netzwerk-Requests oder Store-Updates zählt, deckt Ghost-Clicks zuverlässig auf.

4. CSS touch-action und Scroll-vs-Gesture-Konflikte

Die CSS-Eigenschaft touch-action legt fest, welche nativen Gesten der Browser für ein Element noch selbst verarbeitet und welche er an JavaScript abgibt. touch-action: pan-y erlaubt weiterhin vertikales Scrollen der Seite, deaktiviert aber horizontales Panning, sodass ein Karussell die horizontale Wischgeste per touchmove-Handler selbst auswerten kann, ohne mit dem nativen Scrollverhalten zu kollidieren. touch-action: none entzieht dem Browser die komplette Gestenverarbeitung für ein Element, etwa für ein per Drag verschiebbares Off-Canvas-Panel.

Ein klassischer Bug: Fehlt touch-action auf einem Karussell-Container, interpretiert der Browser jede horizontale Wischgeste zunächst als Versuch, die Seite zu scrollen, und das Karussell reagiert erst nach einer spürbaren Verzögerung oder gar nicht. Für Tests bedeutet das, dass ein Browser-Kontext mit aktivierter Touch-Emulation (hasTouch: true) nötig ist, weil touch-action ausschließlich bei echten oder emulierten Touch-Pointern greift. CI-Runner unterstützen das über das Chrome-DevTools-Protokoll (Input.dispatchTouchEvent), auf das sowohl Cypress-Plugins als auch Playwrights native Touch-API intern zurückgreifen.

5. Touch-Gesten in Playwright automatisieren

Playwright bringt mit page.touchscreen.tap(x, y) eine native Touch-API mit, die aber nur einzelne Taps abdeckt, keine Swipe- oder Pinch-Gesten. Voraussetzung ist ein Browser-Kontext mit hasTouch: true, was bei den mitgelieferten Geräteprofilen wie devices['iPhone 13'] bereits automatisch gesetzt ist. Für Swipe-Gesten muss die rohe Ereigniskette selbst zusammengesetzt werden, über locator.dispatchEvent() mit einem konstruierten Touch-Objekt oder direkt über das Chrome-DevTools-Protokoll für eine realistischere, mehrstufige Bewegung mit Zwischenpositionen.

Wichtig ist, dass eine simulierte Swipe-Geste aus mehreren touchmove-Schritten besteht und nicht nur aus einem einzigen Sprung von Start- zu Endposition, da viele Karussell-Implementierungen die Geschwindigkeit aus der Differenz mehrerer Zwischenpunkte berechnen, um zwischen einem versehentlichen Wackeln und einer bewussten Wischgeste zu unterscheiden. Das folgende Beispiel zeigt Hilfsfunktionen für Tap, Swipe und Long-Press, die auf genau diesem Prinzip aufbauen.


// tests/utils/touch-helpers.js: Reusable touch gesture helpers for Playwright
const { expect } = require('@playwright/test');

/**
 * Perform a native tap using Playwright's built-in touchscreen API.
 * Requires a browser context created with hasTouch: true.
 */
async function tap(page, x, y) {
  await page.touchscreen.tap(x, y);
}

/**
 * Simulate a swipe gesture with multiple intermediate touchmove steps,
 * so velocity-based gesture handlers can detect a real swipe.
 */
async function swipe(page, locator, { fromX, fromY, toX, toY, steps = 8 }) {
  const box = await locator.boundingBox();
  const startX = box.x + fromX;
  const startY = box.y + fromY;
  const endX = box.x + toX;
  const endY = box.y + toY;

  await locator.dispatchEvent('touchstart', {
    touches: [{ identifier: 1, clientX: startX, clientY: startY }],
    changedTouches: [{ identifier: 1, clientX: startX, clientY: startY }],
  });

  for (let i = 1; i <= steps; i++) {
    const progress = i / steps;
    const x = startX + (endX - startX) * progress;
    const y = startY + (endY - startY) * progress;
    await locator.dispatchEvent('touchmove', {
      touches: [{ identifier: 1, clientX: x, clientY: y }],
      changedTouches: [{ identifier: 1, clientX: x, clientY: y }],
    });
    await page.waitForTimeout(16); // roughly one frame per step
  }

  await locator.dispatchEvent('touchend', {
    touches: [],
    changedTouches: [{ identifier: 1, clientX: endX, clientY: endY }],
  });
}

/**
 * Simulate a long-press by holding touchstart for a fixed duration
 * before dispatching touchend, useful for context menus on product cards.
 */
async function longPress(page, locator, durationMs = 600) {
  const box = await locator.boundingBox();
  const centerX = box.x + box.width / 2;
  const centerY = box.y + box.height / 2;

  await locator.dispatchEvent('touchstart', {
    touches: [{ identifier: 1, clientX: centerX, clientY: centerY }],
    changedTouches: [{ identifier: 1, clientX: centerX, clientY: centerY }],
  });
  await page.waitForTimeout(durationMs);
  await locator.dispatchEvent('touchend', {
    touches: [],
    changedTouches: [{ identifier: 1, clientX: centerX, clientY: centerY }],
  });
}

module.exports = { tap, swipe, longPress };

6. Touch-Gesten in Cypress automatisieren

Cypress bringt anders als Playwright keine native Touch-API mit. Mit dem Community-Plugin cypress-real-events lassen sich echte Betriebssystem-Events über das Chrome-DevTools-Protokoll auslösen, inklusive realTouch und realSwipe. Steht das Plugin nicht zur Verfügung oder soll die Simulation ohne zusätzliche Abhängigkeit auskommen, lässt sich dieselbe Ereigniskette manuell über cy.wrap(element).trigger('touchstart', { touches: [...] }) nachbauen, wobei trigger() ein rohes DOM-Event mit beliebigen Eigenschaften dispatcht.

Ein sauberes Custom Command kapselt diese Logik einmalig und macht sie im gesamten Test-Suite wiederverwendbar, ähnlich wie bei bestehenden Login- oder Fixture-Commands. Wichtig ist, den mobilen Viewport vorab mit cy.viewport('iphone-x') zu setzen, da manche Anwendungen ihre Touch-Handler erst ab einer bestimmten Breakpoint-Breite registrieren und ein Test im Desktop-Viewport die Handler sonst nie anhängt, unabhängig davon, welche Events anschließend gesendet werden.


// cypress/support/commands.js: Custom command for a swipe gesture via raw touch events
Cypress.Commands.add('swipe', { prevSubject: 'element' }, (subject, direction = 'left') => {
  const el = subject[0];
  const rect = el.getBoundingClientRect();
  const startX = direction === 'left' ? rect.right - 20 : rect.left + 20;
  const endX = direction === 'left' ? rect.left + 20 : rect.right - 20;
  const y = rect.top + rect.height / 2;

  const touchObj = (x, id) => ({
    identifier: id,
    target: el,
    clientX: x,
    clientY: y,
    pageX: x,
    pageY: y,
  });

  cy.wrap(el)
    .trigger('touchstart', {
      touches: [touchObj(startX, 1)],
      changedTouches: [touchObj(startX, 1)],
    })
    // Two intermediate steps so velocity-based swipe detection can fire
    .trigger('touchmove', {
      touches: [touchObj(startX + (endX - startX) * 0.5, 1)],
      changedTouches: [touchObj(startX + (endX - startX) * 0.5, 1)],
    })
    .trigger('touchmove', {
      touches: [touchObj(endX, 1)],
      changedTouches: [touchObj(endX, 1)],
    })
    .trigger('touchend', {
      touches: [],
      changedTouches: [touchObj(endX, 1)],
    });
});

// Usage in a spec: cy.viewport('iphone-x'); cy.get('.gallery').swipe('left');

7. Swipeable Bildergalerien und Karussells testen

Für ein Produktbild-Karussell reicht es nicht, nur zu prüfen, dass ein Klick auf einen Pfeil-Button den Slide wechselt. Der eigentliche mobile Bedienweg läuft über eine horizontale Wischgeste, deren Handler ausschließlich an touchstart/touchmove/touchend gebunden ist. Ein vollständiger Test simuliert die Geste mit mehreren Zwischenschritten, prüft anschließend, ob sich der aria-current- oder Index-Zustand der Galerie korrekt erhöht, und kontrolliert die CSS-transform-Eigenschaft des Slide-Containers auf den erwarteten Versatz.

Zusätzlich lohnt sich ein Edge-Case-Test für das Verhalten am Anfang und Ende der Galerie: Viele Implementierungen zeigen dort einen Bounce-Effekt oder verhindern das Weiterwischen komplett, statt zum ersten Bild zurückzuspringen. Ebenso wichtig ist ein Test für zu kurze oder zu langsame Wischgesten, die unterhalb eines Geschwindigkeits- oder Distanz-Schwellenwerts liegen und deshalb keinen Slide-Wechsel auslösen dürfen, sondern zur Ausgangsposition zurückschnappen sollten. Ein reiner Klick-Test kann all diese Zustände nicht abdecken, weil er nie eine kontinuierliche Bewegung mit variabler Geschwindigkeit erzeugt.


// tests/gallery.spec.js: Playwright test for a swipeable product image gallery
const { test, expect } = require('@playwright/test');
const { swipe } = require('./utils/touch-helpers');

test.use({ hasTouch: true, viewport: { width: 390, height: 844 } });

test('swiping left advances the product gallery to the next slide', async ({ page }) => {
  await page.goto('/catalog/product/view/id/42');
  const gallery = page.locator('.product-gallery');
  const track = page.locator('.product-gallery__track');

  await expect(gallery).toHaveAttribute('data-active-index', '0');

  await swipe(page, gallery, { fromX: 300, fromY: 100, toX: 40, toY: 100 });

  await expect(gallery).toHaveAttribute('data-active-index', '1');
  const transform = await track.evaluate((el) => getComputedStyle(el).transform);
  expect(transform).not.toBe('none');
});

test('swiping past the last slide does not wrap around', async ({ page }) => {
  await page.goto('/catalog/product/view/id/42');
  const gallery = page.locator('.product-gallery');

  // Swipe left repeatedly to reach the last slide, then once more
  for (let i = 0; i < 5; i++) {
    await swipe(page, gallery, { fromX: 300, fromY: 100, toX: 40, toY: 100 });
  }

  const lastIndex = await gallery.getAttribute('data-active-index');
  await swipe(page, gallery, { fromX: 300, fromY: 100, toX: 40, toY: 100 });
  await expect(gallery).toHaveAttribute('data-active-index', lastIndex);
});

8. Mobile Filter-Drawer und Off-Canvas-Panels testen

Ein mobiler Filter-Drawer wird meist über einen Tap auf einen Button geöffnet, was auch mit einem klassischen Klick-Test funktioniert, da der öffnende Button in der Regel einen normalen click-Handler besitzt. Interessant wird es beim Schließen: Viele Implementierungen erlauben zusätzlich ein Zuwischen des Panels per Drag-Geste sowie ein Antippen des halbtransparenten Backdrops außerhalb des Panels, wobei Letzteres oft an touchstart statt an click gebunden ist, um Verzögerungen durch den historischen Klick-Delay zu vermeiden.

Ein vollständiger Test prüft drei Dinge: erstens, dass der Body-Scroll gesperrt wird, sobald das Panel offen ist, meist über eine overflow: hidden-Klasse am body-Element. Zweitens, dass ein Tap auf den Backdrop das Panel wieder schließt und aria-hidden="true" korrekt gesetzt wird. Drittens, dass eine Wisch-nach-links-Geste auf dem Panel selbst denselben Effekt hat wie ein Tap auf den Schließen-Button, sofern die Anwendung diese Geste unterstützt. Gerade der dritte Punkt lässt sich ausschließlich mit echten Touch-Events reproduzieren, ein simulierter Klick auf den Panel-Rand hätte keinerlei Wirkung.


// cypress/e2e/filter-drawer.cy.js: Testing a mobile off-canvas filter drawer
describe('Mobile filter drawer', () => {
  beforeEach(() => {
    cy.viewport('iphone-x');
    cy.visit('/catalog/category/view/id/15');
  });

  it('opens on tap and locks body scroll', () => {
    cy.get('[data-testid="filter-toggle"]').click();
    cy.get('[data-testid="filter-drawer"]').should('have.attr', 'aria-hidden', 'false');
    cy.get('body').should('have.class', 'overflow-hidden');
  });

  it('closes when the backdrop is tapped', () => {
    cy.get('[data-testid="filter-toggle"]').click();
    cy.get('[data-testid="filter-backdrop"]').trigger('touchstart', {
      touches: [{ identifier: 1, clientX: 350, clientY: 400 }],
    });
    cy.get('[data-testid="filter-backdrop"]').trigger('touchend', { touches: [] });
    cy.get('[data-testid="filter-drawer"]').should('have.attr', 'aria-hidden', 'true');
  });

  it('closes on a left swipe gesture on the panel itself', () => {
    cy.get('[data-testid="filter-toggle"]').click();
    cy.get('[data-testid="filter-drawer"]').swipe('left');
    cy.get('[data-testid="filter-drawer"]').should('have.attr', 'aria-hidden', 'true');
  });
});

9. Klick-Simulation vs. echte Touch-Event-Simulation

Die Wahl zwischen einer einfachen Klick-Simulation und einer echten Touch-Event-Sequenz ist keine Frage des Testaufwands, sondern entscheidet direkt darüber, welche Bugs überhaupt sichtbar werden. Die folgende Übersicht fasst die wichtigsten Unterschiede zusammen.

Kriterium Klick-Simulation (click()) Echte Touch-Event-Simulation
Feuert touchstart/touchend Nein Ja, vollständige Sequenz
Deckt Ghost-Click-Bugs auf Nein, Bug bleibt unsichtbar Ja, Doppel-Trigger messbar
Swipe-/Drag-Gesten Nicht abbildbar Mit touchmove-Schritten simulierbar
Hover-Fallback-Verhalten Besteht Test trotz fehlendem Hover Deckt fehlenden Tap-Fallback auf
touch-action-Konflikte Nicht prüfbar Scroll-vs-Gesture-Konflikt sichtbar
Realitätsnähe für mobile Nutzer Gering Hoch

Der letzte Punkt der Tabelle betrifft eines der hartnäckigsten Testprobleme: hover-abhängige UI. Dropdown-Menüs, Tooltips und Mega-Menüs, die per CSS :hover geöffnet werden, haben auf einem Touchscreen schlicht keinen Hover-Zustand, den sie öffnen könnten. Manche mobilen Browser interpretieren den ersten Tap auf ein solches Element als Hover-Simulation und den zweiten Tap erst als eigentlichen Klick, ein Verhalten, das sich zwischen iOS Safari, Chrome für Android und verschiedenen WebViews unterscheidet und sich nicht verlässlich testen oder gar als UX-Pattern empfehlen lässt. Der robuste Fix liegt selten im Test, sondern im UI-Code: Ein Mega-Menü sollte über @media (hover: hover) and (pointer: fine) zwischen echtem Hover auf Desktop-Geräten und einem expliziten Tap-Toggle mit aria-expanded auf Touch-Geräten unterscheiden. Der Test prüft dann beide Pfade getrennt, mit zwei Playwright-Projekten für unterschiedliche pointerType-Emulation.


/* styles/mega-menu.css: differentiate hover-capable pointers from touch pointers */
.mega-menu__panel {
  display: none;
  touch-action: pan-y; /* allow vertical page scroll inside the open panel */
}

/* Desktop with a real mouse: open on hover, no JS needed */
@media (hover: hover) and (pointer: fine) {
  .mega-menu__trigger:hover + .mega-menu__panel {
    display: block;
  }
}

/* Touch devices: panel is opened only via the aria-expanded JS toggle below */
@media (hover: none), (pointer: coarse) {
  .mega-menu__panel[data-open="true"] {
    display: block;
  }
}

/*
  Corresponding Playwright test: assert the menu is reachable via tap
  on a touch viewport, since :hover never applies there.

  test.use({ hasTouch: true, viewport: { width: 390, height: 844 } });

  test('mega menu opens via tap on touch viewports', async ({ page }) => {
    await page.goto('/');
    const trigger = page.locator('.mega-menu__trigger');
    const panel = page.locator('.mega-menu__panel');

    await expect(panel).toBeHidden();
    await trigger.dispatchEvent('touchstart', { touches: [{ identifier: 1, clientX: 40, clientY: 20 }] });
    await trigger.dispatchEvent('touchend', { touches: [] });

    await expect(trigger).toHaveAttribute('aria-expanded', 'true');
    await expect(panel).toBeVisible();
  });
*/

Mironsoft

Touch-Testing, mobile UX-Absicherung und E2E-Testautomatisierung für Magento- und Hyvä-Shops

Mobile Bedienpfade wirklich abgesichert?

Wir bauen echte Touch-Gesten-Tests für eure Bildergalerien, Filter-Drawer und Mega-Menüs, decken Ghost-Click-Bugs auf und stellen sicher, dass hover-abhängige UI auch auf Touch-Geräten bedienbar bleibt.

Touch-Test-Audit

Bestehende Klick-basierte Tests auf echte Touch-Abdeckung prüfen

Gesture-Test-Setup

Swipe-, Tap- und Long-Press-Helfer für Cypress und Playwright aufbauen

Hover-Fallback-Fixes

Mega-Menüs und Tooltips für Touch-Geräte auf Tap-Bedienung umstellen

10. Zusammenfassung

Touch-Interaktionen automatisiert testen heißt vor allem, die Bequemlichkeit der Klick-Simulation zu verlassen und echte Ereignisketten nachzubilden. TouchEvents mit touches, targetTouches und changedTouches lösen einen anderen Code-Pfad aus als synthetische MouseEvents, und nur diese Sequenz deckt Ghost-Click-Bugs, fehlerhafte touch-action-Konfigurationen und tatsächlich nicht erreichbare Hover-Menüs zuverlässig auf. Playwrights touchscreen-API und manuell zusammengesetzte touchstart/touchmove/touchend-Sequenzen in Cypress liefern beide die technische Grundlage dafür.

Swipeable Galerien, Off-Canvas-Filter-Drawer und Mega-Menüs sind die drei UI-Muster, an denen sich der Unterschied am deutlichsten zeigt: Alle drei bestehen einen klick-basierten Test scheinbar problemlos, versagen aber für reale mobile Nutzer, wenn die zugrunde liegende Touch-Logik nie ausgeführt wird. Wer zusätzlich @media (hover: hover) konsequent zur Trennung von Desktop- und Touch-Verhalten nutzt und beide Pfade getrennt testet, schließt genau die Lücke, die reine Klick-Tests systematisch übersehen.

Touch-Interaktionen automatisiert testen - Das Wichtigste auf einen Blick

Echte Touch-Events statt Klicks

touchstart/touchmove/touchend statt click() dispatchen, um den echten mobilen Code-Pfad zu testen.

Ghost-Clicks aufdecken

Vollständige Touch-Sequenz senden und Anzahl ausgelöster Aktionen zählen, um Doppel-Trigger zu erkennen.

Swipe mit mehreren Schritten

Mehrere touchmove-Zwischenpositionen statt eines einzelnen Sprungs, für realistische Geschwindigkeitserkennung.

Hover-Fallback testen

@media (hover: hover) im UI-Code nutzen, Tap-Toggle-Pfad und Hover-Pfad getrennt in eigenen Testprojekten prüfen.

11. FAQ: Touch-Interaktionen automatisiert testen

1Warum reicht cy.click() nicht für Touch-Tests?
click() dispatcht ein synthetisches MouseEvent, nie touchstart/touchmove/touchend. Code, der ausschließlich auf Touch-Events lauscht, wird im Test nie ausgeführt.
2Was ist der Unterschied zwischen TouchEvent und PointerEvent?
TouchEvent trägt mehrere gleichzeitige Kontaktpunkte für Multi-Touch. PointerEvent vereinheitlicht Maus, Touch und Stift über ein pointerType-Attribut.
3Was ist das Ghost-Click-Problem?
Ein touchend-Handler ohne preventDefault() löst zusätzlich ein synthetisches click aus, wodurch eine Aktion doppelt feuert. Nur mit echter Touch-Simulation sichtbar.
4Wie simuliert Playwright echte Touch-Gesten?
page.touchscreen.tap() für Taps in einem hasTouch-Kontext. Swipe/Pinch über dispatchEvent() mit konstruierten Touch-Objekten und mehreren touchmove-Schritten.
5Wie teste ich Swipe-Gesten in Cypress ohne natives touchscreen-API?
Über trigger('touchstart', ...) mit mehreren touchmove-Aufrufen und touchend, gekapselt in einem Custom Command, oder mit dem Plugin cypress-real-events.
6Was macht touch-action: pan-y und wann brauche ich es?
Erlaubt vertikales Scrollen, deaktiviert horizontales Panning, sodass JavaScript horizontale Wischgesten selbst auswerten kann, ohne mit dem Scrollverhalten zu kollidieren.
7Wie teste ich, ob ein Hover-Menü auf Touch-Geräten erreichbar ist?
Mit einem separaten Testlauf im Touch-Viewport, der einen Tap statt Hover auslöst und den aria-expanded-Toggle prüft. Ein Desktop-Hover-Test deckt diese Lücke nicht auf.
8Muss ich Pinch-to-Zoom in E2E-Tests abdecken?
Nur eigene Pinch-Handler, etwa für Produktbild-Zoom. Systemweites Browser-Zoomen nicht. Simulierbar mit zwei parallelen Touch-Identifiern in touchmove.
9Wie stelle ich sicher, dass ein Off-Canvas-Filter-Drawer per Swipe schließbar bleibt?
Mit einer vollständigen Swipe-Simulation auf dem Panel und anschließender Prüfung von aria-hidden sowie der entfernten Body-Scroll-Sperre.
10Brauche ich echte Geräte oder reicht Emulation im CI?
Emulation über hasTouch: true und CDP reicht für die meisten Touch-Logik-Tests. Für Performance-Edge-Cases zusätzlich eine Stichprobe auf echten Geräten empfehlenswert.