test(554): pin both clears the hint's single guard depends on
Dropping the onPlaying clear made the `state === 'starting'` render guard the hint's only guard, which moves the burden onto the two paths back to `starting`: each has to clear `autoplayBlocked` itself. Both were assertable but unasserted — either `setAutoplayBlocked(false)` could be deleted with the whole panel suite green, so the invariant the code comment and docs/spa-conventions.md §5b both state was unpinned in both of its named paths. Add one test per path (Retry; a channel switch), each asserting the hint is gone while the panel is back at `starting` — so the render guard cannot be what hid it. Each also asserts the player really re-mounted (loadSource count / last URL, plus a non-null <video>), so the hint cannot be absent merely because the `resolvedSrc` block is unrendered. Measured on this tree, each mutation caught by exactly one test: deleting the onRetry clear reds only 'clicking Retry clears the autoplay-blocked hint' (Tests 1 failed | 25 passed); deleting the render-phase reset clear reds only 'clears the autoplay-blocked hint when switching to a different channel' (1 failed | 25 passed); replacing `autoplayBlocked && state === 'starting' &&` with `autoplayBlocked &&` reds one test too. Unmutated: 26 passed. Refs #554 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
This commit is contained in:
@@ -273,6 +273,45 @@ describe('ChannelPreviewPanel', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// The `state === 'starting'` guard is the hint's ONLY guard, so every path back to `starting` has
|
||||
// to clear the flag itself. Retry is one: it is a fresh attempt that was never blocked, and a hint
|
||||
// carried over from the previous attempt would name a cause that did not happen this time.
|
||||
it('clicking Retry clears the autoplay-blocked hint', async () => {
|
||||
const playSpy = vi
|
||||
.spyOn(HTMLMediaElement.prototype, 'play')
|
||||
.mockRejectedValue(new DOMException('blocked', 'NotAllowedError'));
|
||||
|
||||
try {
|
||||
await renderPanel(
|
||||
<ChannelPreviewPanel channel={channel(available)} nowPlaying={null} onClose={vi.fn()} open />
|
||||
);
|
||||
|
||||
const manifestHandler = getManifestHandler();
|
||||
await act(async () => {
|
||||
manifestHandler();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/autoplay was blocked/i)).toBeInTheDocument();
|
||||
|
||||
// The retried attempt is not blocked.
|
||||
playSpy.mockResolvedValue(undefined);
|
||||
fireEvent.click(screen.getByRole('button', { name: /retry/i }));
|
||||
await flush();
|
||||
|
||||
// The player really did re-mount, so the hint is absent because the flag was cleared — not
|
||||
// because the whole `resolvedSrc` block is still unrendered.
|
||||
expect(hlsMock.loadSource).toHaveBeenCalledTimes(2);
|
||||
expect(document.querySelector('video')).not.toBeNull();
|
||||
expect(screen.queryByText(/autoplay was blocked/i)).not.toBeInTheDocument();
|
||||
// Retry returns to `starting`, so the render guard is not what hid the hint here.
|
||||
expect(screen.getByText('starting')).toBeInTheDocument();
|
||||
} finally {
|
||||
playSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
// A play() interrupted by teardown rejects with AbortError, which is exactly what Retry produces
|
||||
// while the manifest-parsed play() is still pending. The hint names blocked autoplay as the cause,
|
||||
// so it must not appear for a rejection that was not the autoplay policy.
|
||||
@@ -495,6 +534,47 @@ describe('ChannelPreviewPanel', () => {
|
||||
expect(screen.getByText('playing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The other path back to `starting`, and the other half of the hint's single-guard invariant: a
|
||||
// switch must not leave the previous channel's hint standing over a new channel that was never
|
||||
// blocked. The render-phase reset owns this clear — the guard cannot help, since the new channel
|
||||
// starts in `starting` too.
|
||||
it('clears the autoplay-blocked hint when switching to a different channel', async () => {
|
||||
const playSpy = vi
|
||||
.spyOn(HTMLMediaElement.prototype, 'play')
|
||||
.mockRejectedValue(new DOMException('blocked', 'NotAllowedError'));
|
||||
|
||||
try {
|
||||
const { rerender } = await renderPanel(
|
||||
<ChannelPreviewPanel channel={channel(available, undefined, 1)} nowPlaying={null} onClose={vi.fn()} open />
|
||||
);
|
||||
|
||||
const manifestHandler = getManifestHandler();
|
||||
await act(async () => {
|
||||
manifestHandler();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/autoplay was blocked/i)).toBeInTheDocument();
|
||||
|
||||
// The new channel's attempt is not blocked.
|
||||
playSpy.mockResolvedValue(undefined);
|
||||
rerender(
|
||||
<ChannelPreviewPanel channel={channel(available2, undefined, 2)} nowPlaying={null} onClose={vi.fn()} open />
|
||||
);
|
||||
await flush();
|
||||
|
||||
// The new channel's player really is mounted, so the hint is absent because the flag was
|
||||
// cleared — not because the `resolvedSrc` block is still unrendered.
|
||||
expect(hlsMock.loadSource).toHaveBeenLastCalledWith('/iptv/channel/34.1.m3u8');
|
||||
expect(document.querySelector('video')).not.toBeNull();
|
||||
expect(screen.queryByText(/autoplay was blocked/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText('starting')).toBeInTheDocument();
|
||||
} finally {
|
||||
playSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not carry the retry play token over to the next channel', async () => {
|
||||
// Both channels share a manifest URL so the only thing that could re-trigger the attach
|
||||
// effect across the switch is playToken. If the reset failed to zero it out, the token
|
||||
|
||||
Reference in New Issue
Block a user