feat(channels): an Added column, and About cards that wait for a resting pointer

The channel manager lists when each follow reached the list (the
subscription import that first saw it, or a subscribe made here) as a
sortable Added date after Last upload. The About cell's card opens after the
pointer rests on it for half a second instead of at once, and a scroll puts
an open one away.
This commit is contained in:
2026-11-01 01:30:30 +01:00
parent e06c0688d9
commit 85fd9b6704
10 changed files with 161 additions and 3 deletions
@@ -0,0 +1,69 @@
// @vitest-environment jsdom
//
// The About cell's card opened the instant a pointer crossed the cell, so scrolling the table over
// a column of them popped card after card. It waits for the pointer to rest on the cell now, and a
// scroll puts an open one away.
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { render, screen, fireEvent, act, cleanup } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import AboutCell from "./AboutCell";
import { api } from "../../lib/api";
beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(api, "channelDetail").mockReturnValue(new Promise(() => {}));
});
afterEach(() => {
cleanup();
vi.useRealTimers();
vi.restoreAllMocks();
});
function mount() {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(
<QueryClientProvider client={qc}>
<AboutCell text="An element of truth" channelId="UC1" />
</QueryClientProvider>,
);
return screen.getByText("An element of truth");
}
// Present in the DOM is enough: it paints once floating-ui has placed it, which a fake clock never lets happen.
const card = () => document.querySelector('[role="tooltip"]');
const wait = (ms: number) => act(() => vi.advanceTimersByTime(ms));
const enter = (el: HTMLElement) =>
act(() => {
fireEvent.pointerEnter(el);
fireEvent.mouseEnter(el);
fireEvent.mouseMove(el);
});
describe("AboutCell — the hover card", () => {
it("does not open for a pointer passing over the cell", async () => {
const cell = mount();
enter(cell);
await wait(300);
expect(card()).toBeNull();
fireEvent.mouseLeave(cell);
await wait(600);
expect(card()).toBeNull();
});
it("opens once the pointer rests on it", async () => {
const cell = mount();
enter(cell);
await wait(550);
expect(card()).not.toBeNull();
});
it("goes away when the table scrolls", async () => {
const cell = mount();
enter(cell);
await wait(550);
expect(card()).not.toBeNull();
act(() => void window.dispatchEvent(new Event("scroll")));
await wait(50);
expect(card()).toBeNull();
});
});