mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Live error: "Uncaught (in promise) TypeError: PlayerMediaHealth.shouldShowIdle is not a function" inside the device:paired socket handler. WHAT WAS ACTUALLY WRONG: not a missing/misnamed definition — the module DOES export shouldShowIdle (served + unit-tested). It's a VERSION SKEW: /player/* is served network-first by the service worker, so a transient module-fetch failure falls back to the STALE cache (an older player-media-health.js that predates shouldShowIdle) while index.html loads fresh with the call. window.PlayerMediaHealth then exists but lacks the method, and the call site guarded the OBJECT (`window.PlayerMediaHealth ? ...`) not the METHOD — so it threw, aborting the rest of the device:paired handler (the showStatus after it was skipped). FIX: guard the METHOD at both call sites (typeof X.method === 'function') so a stale/partial module can never throw — it falls back to the safe inline default (!isPlaying for the idle decision) and the handler runs to completion. SIBLING (errors travel in pairs): the needsReattach call in the "Playlist unchanged" branch had the SAME object-not-method guard. It was inside a try/catch so it couldn't throw uncaught, but a stale module would silently skip the re-attach/hideStatus. Guarded it the same way. No service-worker change needed: network-first already self-heals on the next good load; the method-guard covers the transient/offline-fallback skew permanently. Tests: player-media-health.test.js +1 module-surface test (both needsReattach and shouldShowIdle are exported functions — catches the define-vs-call class). Inline player JS syntax-checked; both guards verified present. Suite 319/319. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
3.5 KiB
JavaScript
65 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
// #146 web-player fix — the no-change-refresh branch-selection decision. This is the unit
|
|
// the refresh handler consults to decide whether to re-attach the media surface. It is the
|
|
// smallest testable piece of the "no new content lost the video but not the audio" fix.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const PlayerMediaHealth = require('../lib/player-media-health');
|
|
const { needsReattach, shouldShowIdle } = PlayerMediaHealth;
|
|
|
|
// Guards the define-vs-call class of bug: every method the player calls on
|
|
// PlayerMediaHealth must actually be exported (a missing one throws "X is not a function"
|
|
// at the browser call site and aborts the socket handler).
|
|
test('module surface: needsReattach AND shouldShowIdle are both exported functions', () => {
|
|
assert.equal(typeof PlayerMediaHealth.needsReattach, 'function');
|
|
assert.equal(typeof PlayerMediaHealth.shouldShowIdle, 'function');
|
|
});
|
|
|
|
const video = (o) => ({ isPlaying: true, hasCurrentItem: true, itemKind: 'video', videoEl: o, surfaceAttached: true });
|
|
|
|
test('healthy attached+live video on a no-change refresh -> NO re-attach (no flicker)', () => {
|
|
assert.equal(needsReattach(video({ attached: true, ended: false, errored: false })), false);
|
|
});
|
|
|
|
test('THE BUG: a detached-but-playing <video> (audio persists, surface gone) -> re-attach', () => {
|
|
assert.equal(needsReattach(video({ attached: false, ended: false, errored: false })), true);
|
|
});
|
|
|
|
test('video element gone entirely -> re-attach', () => {
|
|
assert.equal(needsReattach({ isPlaying: true, hasCurrentItem: true, itemKind: 'video', videoEl: null }), true);
|
|
});
|
|
|
|
test('ended or errored video -> re-attach', () => {
|
|
assert.equal(needsReattach(video({ attached: true, ended: true, errored: false })), true);
|
|
assert.equal(needsReattach(video({ attached: true, ended: false, errored: true })), true);
|
|
});
|
|
|
|
test('idle / no current item -> never re-attach (leave the waiting screen)', () => {
|
|
assert.equal(needsReattach({ isPlaying: false, hasCurrentItem: true, itemKind: 'video', videoEl: null }), false);
|
|
assert.equal(needsReattach({ isPlaying: true, hasCurrentItem: false, itemKind: 'video', videoEl: null }), false);
|
|
assert.equal(needsReattach(undefined), false);
|
|
});
|
|
|
|
test('non-video surface (image/youtube/widget): re-attach only when the surface is missing', () => {
|
|
const base = { isPlaying: true, hasCurrentItem: true };
|
|
assert.equal(needsReattach({ ...base, itemKind: 'image', surfaceAttached: true }), false);
|
|
assert.equal(needsReattach({ ...base, itemKind: 'image', surfaceAttached: false }), true);
|
|
assert.equal(needsReattach({ ...base, itemKind: 'youtube', surfaceAttached: false }), true);
|
|
assert.equal(needsReattach({ ...base, itemKind: 'widget', surfaceAttached: true }), false);
|
|
});
|
|
|
|
// shouldShowIdle — the reconnect reset guard (device:paired / unchanged reconciliation).
|
|
test('THE RECONNECT BUG: already playing -> NEVER show the idle "Waiting" screen', () => {
|
|
// reconnect re-emits device:paired while content is playing: must stay on the content
|
|
assert.equal(shouldShowIdle({ isPlaying: true, hasContent: true }), false);
|
|
assert.equal(shouldShowIdle({ isPlaying: true, hasContent: false }), false);
|
|
});
|
|
|
|
test('idle screen shows ONLY when genuinely no content and nothing playing', () => {
|
|
assert.equal(shouldShowIdle({ isPlaying: false, hasContent: false }), true); // first pair, empty
|
|
assert.equal(shouldShowIdle({ isPlaying: false, hasContent: true }), false); // content present, about to render
|
|
assert.equal(shouldShowIdle(undefined), true);
|
|
});
|