mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
* app: video comments and description distinguish pending from empty useVideoExtras initialised both values to null and the page treated null the same as "none", so every video open (and every prev/next) showed "No comments have been downloaded. Refresh the video?" with a live Refresh button until the comments arrived. The Description tab showed "No description available." the same way. A failed fetch was also rendered as "none", and the previous video's data lingered until the next video's responses arrived. Invariants now enforced and tested: * null = pending, undefined = fetch failed, [] / '' = loaded and empty. The API serialises a missing info_json field as null, so the hook normalises that to empty; consumers never see null for "none". * Switching video resets both to pending, and a slow response for the previous video is dropped. * The page renders a placeholder while pending, an error message on failure, and offers Refresh only once the server confirmed there are no comments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * app: useVideoExtras drops any request that is not the newest Matching on the video id let a stale request land when the user left and returned to the same video, or saved an edit while the page's first description request was still in flight. Each fetch now takes a generation number and only the newest generation may write state, per value, so a description refetch never invalidates a comments request. The edit dialog adopts a description that arrives while it is open only if the user has not typed, so a draft written over the placeholder is not wiped by the empty or failed value that follows. Tests: 7 -> none -> 8 -> 7 with the first request rejecting last; a save refetch beating the initial request; the draft surviving; the pending placeholder asserted; the console spy restored in finally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
66 lines
3.2 KiB
JavaScript
66 lines
3.2 KiB
JavaScript
import React from 'react';
|
|
import {render, screen} from '../test-utils';
|
|
import {Comments, VideoDescription} from './VideoPlayer';
|
|
|
|
// What the video page shows for each state of useVideoExtras. The states are null (pending),
|
|
// undefined (fetch failed), and an empty value (loaded, nothing to show). "No comments have been
|
|
// downloaded" carries a Refresh button that re-downloads the video, so it must only appear once
|
|
// the server has confirmed there are none.
|
|
|
|
const video = {id: 7, url: 'https://example.com/v'};
|
|
const NO_COMMENTS = /No comments have been/;
|
|
const NO_DESCRIPTION = /No description available/;
|
|
|
|
describe('Comments', () => {
|
|
test('pending: shows a placeholder, not "no comments" or an error', () => {
|
|
const {container} = render(<Comments comments={null} video={video}/>);
|
|
expect(container.querySelector('.wrolpi-placeholder')).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_COMMENTS)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/Could not/)).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('button', {name: 'Refresh'})).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('failed: shows an error, not "no comments"', () => {
|
|
render(<Comments comments={undefined} video={video}/>);
|
|
expect(screen.getByText(/Could not fetch the comments/)).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_COMMENTS)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('empty: shows "no comments" with the Refresh button', () => {
|
|
render(<Comments comments={[]} video={video}/>);
|
|
expect(screen.getByText(NO_COMMENTS)).toBeInTheDocument();
|
|
expect(screen.getByRole('button', {name: 'Refresh'})).toBeInTheDocument();
|
|
});
|
|
|
|
test('loaded: renders the comments', () => {
|
|
render(<Comments comments={[{id: 'c1', parent: 'root', text: 'first!', author: 'a'}]} video={video}/>);
|
|
expect(screen.getByText('first!')).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_COMMENTS)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('VideoDescription', () => {
|
|
test('pending: shows a placeholder, not "no description" or an error', () => {
|
|
const {container} = render(<VideoDescription description={null} setVideoTime={() => null}/>);
|
|
expect(container.querySelector('.wrolpi-placeholder')).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_DESCRIPTION)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/Could not/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('failed: shows an error, not "no description"', () => {
|
|
render(<VideoDescription description={undefined} setVideoTime={() => null}/>);
|
|
expect(screen.getByText(/Could not fetch the description/)).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_DESCRIPTION)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('empty: shows "no description"', () => {
|
|
render(<VideoDescription description='' setVideoTime={() => null}/>);
|
|
expect(screen.getByText(NO_DESCRIPTION)).toBeInTheDocument();
|
|
});
|
|
|
|
test('loaded: renders the description', () => {
|
|
render(<VideoDescription description='A fine video.' setVideoTime={() => null}/>);
|
|
expect(screen.getByText('A fine video.')).toBeInTheDocument();
|
|
expect(screen.queryByText(NO_DESCRIPTION)).not.toBeInTheDocument();
|
|
});
|
|
});
|