wrolpi/app/src/components/VideoExtras.test.js
lrnselfreliance 83362a65c1
app: video comments and description distinguish pending from empty (#727)
* 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>
2026-09-26 11:22:37 -06:00

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();
});
});