feat: skeleton loading state on the home catalog
Replaces the plain "Loading…" text with a content-shaped MangaGridSkeleton so the catalog grid's layout is reserved while it loads and swaps in without a shift. Keeps the data-testid="loading" wrapper (now role="status") for accessibility and selector stability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.112.0"
|
version = "0.113.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.112.0"
|
version = "0.113.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "mangalord"
|
default-run = "mangalord"
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,51 @@ test('home page renders the Mangalord heading and search input', async ({ page }
|
|||||||
await expect(page.getByTestId('empty')).toContainText('No mangas yet');
|
await expect(page.getByTestId('empty')).toContainText('No mangas yet');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows a skeleton grid while the catalog is loading, then the real grid', async ({ page }) => {
|
||||||
|
await mockAnonymous(page);
|
||||||
|
|
||||||
|
// Hold the catalog fetch open so the loading state is observable.
|
||||||
|
let release: () => void = () => {};
|
||||||
|
const gate = new Promise<void>((resolve) => {
|
||||||
|
release = resolve;
|
||||||
|
});
|
||||||
|
await page.route('**/api/v1/mangas*', async (route) => {
|
||||||
|
await gate;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'm1',
|
||||||
|
title: 'Berserk',
|
||||||
|
status: 'ongoing',
|
||||||
|
alt_titles: [],
|
||||||
|
description: null,
|
||||||
|
cover_image_path: null,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
updated_at: '2026-01-01T00:00:00Z',
|
||||||
|
authors: [],
|
||||||
|
genres: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
page: { limit: 50, offset: 0, total: 1 }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Skeleton stands in for the grid while the fetch is pending.
|
||||||
|
await expect(page.getByTestId('manga-grid-skeleton')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('manga-list')).toHaveCount(0);
|
||||||
|
|
||||||
|
// Once data lands, the real grid replaces the skeleton.
|
||||||
|
release();
|
||||||
|
await expect(page.getByTestId('manga-list')).toContainText('Berserk');
|
||||||
|
await expect(page.getByTestId('manga-grid-skeleton')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
// Anti-drift guard: the desktop sort <select> and the mobile sort sheet must
|
// Anti-drift guard: the desktop sort <select> and the mobile sort sheet must
|
||||||
// both enumerate exactly SORT_FIELD_LABELS, in the same order. The desktop
|
// both enumerate exactly SORT_FIELD_LABELS, in the same order. The desktop
|
||||||
// select used to hard-code its <option>s, so a label rename in mangaSort.ts
|
// select used to hard-code its <option>s, so a label rename in mangaSort.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.112.0",
|
"version": "0.113.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
import ContinueReadingShelf from '$lib/components/ContinueReadingShelf.svelte';
|
import ContinueReadingShelf from '$lib/components/ContinueReadingShelf.svelte';
|
||||||
import RecommendationShelf from '$lib/components/RecommendationShelf.svelte';
|
import RecommendationShelf from '$lib/components/RecommendationShelf.svelte';
|
||||||
import MangaCard from '$lib/components/MangaCard.svelte';
|
import MangaCard from '$lib/components/MangaCard.svelte';
|
||||||
|
import MangaGridSkeleton from '$lib/components/MangaGridSkeleton.svelte';
|
||||||
import Pager from '$lib/components/Pager.svelte';
|
import Pager from '$lib/components/Pager.svelte';
|
||||||
import SegmentedControl from '$lib/components/SegmentedControl.svelte';
|
import SegmentedControl from '$lib/components/SegmentedControl.svelte';
|
||||||
import Sheet from '$lib/components/Sheet.svelte';
|
import Sheet from '$lib/components/Sheet.svelte';
|
||||||
@@ -659,7 +660,9 @@
|
|||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="status" data-testid="loading">Loading…</p>
|
<div data-testid="loading" role="status" aria-label="Loading manga">
|
||||||
|
<MangaGridSkeleton />
|
||||||
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p class="error" data-testid="error" role="alert">{error}</p>
|
<p class="error" data-testid="error" role="alert">{error}</p>
|
||||||
{:else if mangas.length === 0}
|
{:else if mangas.length === 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user