fix(player): keep double-tap seek working over the play overlay (DR-098)
Publish Documentation / Build & publish docs to gitea-pages (push) Has been cancelled
Traceability Validation / Check Requirement Traces (push) Has been cancelled
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 7m5s
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been skipped

The control-surface guard added in the previous commit killed
double-tap-to-seek. The first tap pauses, which renders the full-screen
<button> play overlay over the video, so the SECOND tap lands on a
button — and the guard discarded it as "a tap on a control".

Mark that overlay `data-player-surface`: visually it IS the video, so it
must keep taking tap gestures despite being a <button>. The marker wins
over the interactive-tag check in isControlSurfaceTouch.

Adds VideoPlayer.tapSurface.test.ts, which renders the REAL component
and dispatches real touch/click events at whatever element is genuinely
on top. This is the gap that let four bugs ship in a row: the pure-unit
tests over registerTap/isControlSurfaceTouch/isSynthesizedTouchClick all
passed throughout, because each helper behaved exactly as specified —
every bug was in the composition, i.e. which element actually receives a
tap after Svelte re-renders. Modelling that DOM by hand in a test would
just re-encode the same wrong assumption, so these render it instead.

The new double-tap test was verified to fail with the fix reverted and
pass with it applied, in both directions.
This commit is contained in:
2026-07-30 15:44:38 +02:00
parent dc8b732465
commit b12e99b7e1
7 changed files with 240 additions and 12 deletions
+11 -4
View File
@@ -45,12 +45,19 @@ export const TOUCH_CLICK_SUPPRESS_MS = 700;
* testable without a DOM.
*/
export function isControlSurfaceTouch(
ancestors: Array<{ tag: string; isPlayerControls?: boolean }>
ancestors: Array<{ tag: string; isPlayerControls?: boolean; isPlayerSurface?: boolean }>
): boolean {
const INTERACTIVE = new Set(["button", "a", "input", "select", "textarea", "label"]);
return ancestors.some(
(node) => node.isPlayerControls === true || INTERACTIVE.has(node.tag.toLowerCase())
);
for (const node of ancestors) {
// `data-player-surface` wins over the tag check: the full-screen play overlay
// is a <button> but is visually the video itself, and must keep taking tap
// gestures — otherwise the second tap of a double tap (which lands on it,
// because the first tap paused and raised it) is discarded and seeking dies.
if (node.isPlayerSurface === true) return false;
if (node.isPlayerControls === true) return true;
if (INTERACTIVE.has(node.tag.toLowerCase())) return true;
}
return false;
}
/**