From 160b0b52fa5d3e663c93f67cae0fce9460cd1013 Mon Sep 17 00:00:00 2001
From: huyan
Date: Fri, 21 Aug 2026 14:07:04 +0800
Subject: [PATCH 1/2] fix(projects): render the loading shell immediately
The aggregated project request leaves the page blank until list data arrives.
Render the create entry and pixel-matrix project placeholders while the request is pending.
Users retain project context without restoring per-project preview requests.
---
frontend/src/pages/projects/index.tsx | 54 +++++++++++++++++++--------
1 file changed, 39 insertions(+), 15 deletions(-)
diff --git a/frontend/src/pages/projects/index.tsx b/frontend/src/pages/projects/index.tsx
index 920a314f..a9228cf5 100644
--- a/frontend/src/pages/projects/index.tsx
+++ b/frontend/src/pages/projects/index.tsx
@@ -85,21 +85,17 @@ export function ProjectsPage() {
{error}
) : null}
- {projectsPage === null && !error ? (
- 正在读取项目…
- ) : null}
- {projectsPage ? (
-
-
- {projectsPage.items.length > 0 ? (
-
- ) : null}
-
- ) : null}
+
+
+ {projectsPage === null && !error ?
: null}
+ {projectsPage && projectsPage.items.length > 0 ? (
+
+ ) : null}
+
{projectsPage ? (
+
+ 最近项目
+
+
+ {Array.from({ length: 3 }, (_, index) => (
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )
+}
+
function ProjectGallery({
projects,
total,
From bc16df6ea9001bed15e86b6dae4ef29d48e70abf Mon Sep 17 00:00:00 2001
From: huyan
Date: Fri, 21 Aug 2026 14:07:21 +0800
Subject: [PATCH 2/2] test(projects): cover the pending project shell
The loading shell must remain visible before project data resolves.
Gate the project request and assert the create entry, placeholders, and accessible status.
The regression test also verifies cleanup when real cards replace the shell.
---
frontend/src/pages/projects/index.test.tsx | 38 ++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/frontend/src/pages/projects/index.test.tsx b/frontend/src/pages/projects/index.test.tsx
index 550f8dbe..11150196 100644
--- a/frontend/src/pages/projects/index.test.tsx
+++ b/frontend/src/pages/projects/index.test.tsx
@@ -22,6 +22,44 @@ function installBackend() {
}
describe('ProjectsPage', () => {
+ it('renders the project shell while the project list is pending', async () => {
+ const backend = createProjectAssetsBackend()
+ vi.stubEnv('VITE_API_BASE_URL', 'https://api.windup.test')
+ let releaseProjects: (() => void) | undefined
+ const projectsGate = new Promise((resolve) => {
+ releaseProjects = resolve
+ })
+ vi.stubGlobal('fetch', async (input: RequestInfo | URL, init?: RequestInit) => {
+ const request = new Request(input, init)
+ if (new URL(request.url).pathname === '/projects') await projectsGate
+ return backend.fetch(input, init)
+ })
+
+ render(
+
+
+
+
+ ,
+ )
+
+ try {
+ expect(await screen.findByRole('link', { name: '新建项目' })).toBeTruthy()
+ const loadingShell = screen.getByRole('status', { name: '正在读取项目' })
+ expect(loadingShell.getAttribute('aria-busy')).toBe('true')
+ expect(loadingShell.querySelectorAll('[data-project-loading-placeholder]')).toHaveLength(3)
+ expect(loadingShell.querySelectorAll('[data-pixel-matrix-dot]')).toHaveLength(3 * 432)
+ expect(screen.queryByText('正在读取项目…')).toBeNull()
+ expect(screen.queryByRole('link', { name: /打开项目/ })).toBeNull()
+ } finally {
+ releaseProjects?.()
+ }
+
+ expect(await screen.findAllByRole('link', { name: /打开项目/ })).toHaveLength(2)
+ expect(screen.queryByRole('status', { name: '正在读取项目' })).toBeNull()
+ expect(document.querySelector('[data-project-loading-placeholder]')).toBeNull()
+ })
+
it('loads a project card thumbnail and falls back to its original preview', async () => {
const backend = createProjectAssetsBackend()
vi.stubEnv('VITE_API_BASE_URL', 'https://api.windup.test')