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]]
|
||||
name = "mangalord"
|
||||
version = "0.112.0"
|
||||
version = "0.113.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mangalord"
|
||||
version = "0.112.0"
|
||||
version = "0.113.0"
|
||||
edition = "2021"
|
||||
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');
|
||||
});
|
||||
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mangalord-frontend",
|
||||
"version": "0.112.0",
|
||||
"version": "0.113.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import ContinueReadingShelf from '$lib/components/ContinueReadingShelf.svelte';
|
||||
import RecommendationShelf from '$lib/components/RecommendationShelf.svelte';
|
||||
import MangaCard from '$lib/components/MangaCard.svelte';
|
||||
import MangaGridSkeleton from '$lib/components/MangaGridSkeleton.svelte';
|
||||
import Pager from '$lib/components/Pager.svelte';
|
||||
import SegmentedControl from '$lib/components/SegmentedControl.svelte';
|
||||
import Sheet from '$lib/components/Sheet.svelte';
|
||||
@@ -659,7 +660,9 @@
|
||||
</Sheet>
|
||||
|
||||
{#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}
|
||||
<p class="error" data-testid="error" role="alert">{error}</p>
|
||||
{:else if mangas.length === 0}
|
||||
|
||||
Reference in New Issue
Block a user