Compare commits

..
Author SHA1 Message Date
dtourolle 8e98e1c37a test(player): answer the commands the tap-surface tests actually render
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 22m57s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m50s
Traceability Validation / Check Requirement Traces (push) Successful in 24s
Build & Release / Run Tests (push) Successful in 7m21s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 10m3s
Build & Release / Build Linux (push) Successful in 20m32s
Build & Release / Build Windows (push) Successful in 14m29s
Build & Release / Build Android (push) Successful in 31m5s
Build & Release / Create Release (push) Successful in 12s
VideoPlayer.tapSurface.test.ts deliberately does not mock $lib/api/bindings — it
renders the real component against the real bindings, which bottom out in the
globally mocked `invoke`. That mock resolves `undefined` for every command, so
any command whose result is *rendered* blows up: the quality picker assigns the
result straight to state and the template then reads `streamingQualities.length`,
which throws on undefined.

It threw asynchronously, outside any test, so the suite reported 4 unhandled
errors while every test still passed — the state vitest warns "might cause false
positive tests". Answering the two rendered commands removes them.

Authored in the main checkout; brought in here and verified: 83 files, 1009
tests, and the unhandled-error count drops from 4 to 0.
2026-08-16 21:20:55 +02:00
dtourolle 440d7a01a9 chore(release): 0.6.0
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 6m2s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 32s
Traceability Validation / Check Requirement Traces (push) Successful in 13s
Build & Release / Run Tests (push) Failing after 6m7s
Build & Release / Build Linux (push) Skipped
Build & Release / Build Windows (push) Skipped
Build & Release / Build Android (push) Skipped
Build & Release / Create Release (push) Skipped
Android native video renders a picture, and its transport works.

The path shipped once as audio with no picture and was reverted with the
compositing named as the suspect. It was not the compositing: five independent
defects sat between ExoPlayer and the screen, each able to produce that symptom
on its own — the app shell painting over the surface through a CSS rule aimed at
an attribute nothing set, a poster card with no way to lift on a path that
renders no <video>, JS bridges racing the page load and losing permanently, a
SurfaceView that was never detached, and a frontend that told Rust a webview
element was playing when none existed, so every play/pause intent was aimed at
something that was not there.

Native video stays opt-in. Turning it on surfaced a further unverified path —
the background-audio return is written only for the webview element — and
rotation still needs device confirmation.

Minor rather than patch: the player's touch behaviour changes for everyone (the
control bar now auto-hides on touchscreens, and the system bars go away with the
player), not only for those who opt into native video.
2026-08-16 21:14:08 +02:00
6 changed files with 81 additions and 4 deletions
+59
View File
@@ -9,6 +9,65 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
For how long each fixed defect had been shipping before it was found, see For how long each fixed defect had been shipping before it was found, see
[docs/defect-windows.md](docs/defect-windows.md). [docs/defect-windows.md](docs/defect-windows.md).
## v0.6.0
### 🐛 Fixes
- **Android native video actually shows a picture.** It shipped once as *audio
with no picture* and was reverted with the compositing named as the suspect
(DR-172). The compositing was not at fault; five independent defects sat
between ExoPlayer and the screen, each able to produce that symptom alone. The
app shell painted over the video surface through a CSS rule targeting
`[data-app-shell]`, an attribute no component had ever set in any commit
(DR-185). The poster/title card had no way to lift on a path that renders no
`<video>` element, so a black card covered the surface for the whole session
(DR-182). The JavaScript bridges were installed by a 500 ms tree walk that
raced the page load — and lost permanently when it lost, because the
re-injection guard then declined to retry — so `setTransparent(true)` could
never arrive (DR-183). The `SurfaceView` was never detached, leaking one per
video and leaving picture-in-picture's gate stuck open (DR-184). Verified on a
device: logcat now carries `WebView transparent = true` and
`Marking media ready` with video on screen, the pair the original
investigation went looking for and could not find.
(UR-003, UR-004, UR-041 → DR-182, DR-183, DR-184, DR-185)
- **Play and pause reach the player that is actually rendering.** Transport did
nothing on the native video path — from the on-screen tap, from the control
bar, and from a direct command invocation — while seek and skip kept working,
because those decide elsewhere. Rust routes play/pause to the webview `<video>`
whenever it believes one is active, and the player route mirrored element state
into that belief unconditionally, including from a ten-second progress
interval. So on the native path the frontend re-declared every ten seconds that
an element was playing when none existed, and every intent was emitted at
something that was not there. The mirror now lives where `useHtml5Element` is
known. This also explains the flashing transport controls, since they key off
the play state that was being contradicted on every tick.
(UR-005, UR-003 → DR-193, DR-195)
- **The player's controls hide themselves on a touchscreen.** The auto-hide timer
was armed only from `mousemove`, which a touch device never fires, so the
control bar stayed over the video for the whole film. It is now armed on entry
and on every touch, and pinned open while paused, seeking, or with a menu open.
(UR-003, UR-066 → DR-189)
- **The system bars go away with the player.** Immersive mode had exactly one
caller — the fullscreen button — so opening a video left the status and
navigation bars painted over it until the user pressed a control most never
press. (UR-066, UR-003 → DR-187)
### 🔬 Internal
- Native video presents through a `TextureView` rather than a `SurfaceView`. A
SurfaceView renders on its own layer outside the app window and punches a
transparent region through it, and Android's own graphics documentation warns
that overlays do not composite reliably above one. (UR-003, UR-004 → DR-192)
- Native Android video remains **opt-in**, and is not yet the default. Turning it
on surfaced a further unverified path: returning from background audio is
implemented only for the webview element, so playback stays dead on the native
path (DR-190, proposed). Rotation still needs device confirmation (DR-194).
(UR-003 → DR-188)
## v0.5.5 ## v0.5.5
### ✨ Features ### ✨ Features
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jellytau", "name": "jellytau",
"version": "0.5.5", "version": "0.6.0",
"description": "", "description": "",
"type": "module", "type": "module",
"packageManager": "bun@1.3.5", "packageManager": "bun@1.3.5",
+1 -1
View File
@@ -2018,7 +2018,7 @@ dependencies = [
[[package]] [[package]]
name = "jellytau" name = "jellytau"
version = "0.5.5" version = "0.6.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "jellytau" name = "jellytau"
version = "0.5.5" version = "0.6.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "jellytau", "productName": "jellytau",
"version": "0.5.5", "version": "0.6.0",
"identifier": "com.dtourolle.jellytau", "identifier": "com.dtourolle.jellytau",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",
@@ -32,6 +32,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render } from "@testing-library/svelte"; import { render } from "@testing-library/svelte";
import { tick } from "svelte"; import { tick } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import VideoPlayer from "./VideoPlayer.svelte"; import VideoPlayer from "./VideoPlayer.svelte";
import { SEEK_FORWARD_SECONDS } from "./tapGestures"; import { SEEK_FORWARD_SECONDS } from "./tapGestures";
@@ -128,6 +129,23 @@ function renderPlayer() {
describe("VideoPlayer tap surface (real component)", () => { describe("VideoPlayer tap surface (real component)", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
// This file deliberately does NOT mock `$lib/api/bindings` — it renders the
// real component against the real bindings, which bottom out in the globally
// mocked `invoke`. That mock resolves `undefined` for every command, so the
// commands whose results are *rendered* have to be answered here: the
// quality picker assigns the result straight to state and then does
// `streamingQualities.length` in the template, which throws (asynchronously,
// outside any test) on undefined and fails the run with an unhandled error.
vi.mocked(invoke).mockImplementation(async (cmd: string) => {
switch (cmd) {
case "player_get_streaming_qualities":
return [];
case "player_get_video_settings":
return { streamingQuality: "original" };
default:
return undefined;
}
});
}); });
it("a single tap on the video toggles play/pause exactly once", async () => { it("a single tap on the video toggles play/pause exactly once", async () => {