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:
MechaCat02
2026-07-06 19:40:28 +02:00
parent 09f12c8959
commit def97d3087
5 changed files with 52 additions and 4 deletions

2
backend/Cargo.lock generated
View File

@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mangalord"
version = "0.112.0"
version = "0.113.0"
dependencies = [
"anyhow",
"argon2",

View File

@@ -1,6 +1,6 @@
[package]
name = "mangalord"
version = "0.112.0"
version = "0.113.0"
edition = "2021"
default-run = "mangalord"

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "mangalord-frontend",
"version": "0.112.0",
"version": "0.113.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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}