修复精选请求并发与缓存响应头
Project CI / Repository checks (pull_request) Successful in 1m5s
Project CI / Frontend tests (pull_request) Successful in 3m18s
Project CI / Backend tests (pull_request) Successful in 3m45s
Project CI / Native shell tests (pull_request) Successful in 13m34s

累计追加 Authorization 到 Vary 并保留已有响应字段
将精选请求 generation 绑定到已提交 effect 并在 cleanup 失效
补充中间件与 Suspense 放弃渲染回归测试
同步精选点赞决策与排障文档
This commit is contained in:
2026-08-10 14:19:03 +08:00
parent 95fa252933
commit 79336a13d5
5 changed files with 145 additions and 14 deletions
@@ -9,7 +9,7 @@ import {
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { ContextType } from 'react';
import { type ContextType, startTransition, Suspense } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AuthUiContext } from '../auth/AuthUiContext';
@@ -144,6 +144,13 @@ function triggerIntersection(observer: {
});
}
function SuspendAfterLandingRender({ active }: { active: boolean }) {
if (active) {
throw new Promise<never>(() => undefined);
}
return null;
}
describe('CreationLandingView', () => {
beforeEach(() => {
listPublicEditorProjectResourcesMock.mockResolvedValue([]);
@@ -924,6 +931,73 @@ describe('CreationLandingView', () => {
expect(screen.getByText('新账号素材')).toBeTruthy();
});
it('keeps the committed showcase request valid when a viewer render is abandoned', async () => {
let resolveCurrentPage!: (value: unknown[]) => void;
const currentPage = new Promise<unknown[]>((resolve) => {
resolveCurrentPage = resolve;
});
listEditorProjectsMock.mockResolvedValue([]);
listPublicEditorProjectResourcesMock.mockReturnValueOnce(currentPage);
const currentAuth = createAuthValue();
const abandonedAuth = createAuthValue({
user: {
...createAuthValue().user!,
id: 'user-abandoned',
publicUserCode: '100099',
},
});
const result = render(
<Suspense fallback={<span></span>}>
<AuthUiContext.Provider value={currentAuth}>
<CreationLandingView
onOpenProject={vi.fn()}
onOpenProjects={vi.fn()}
/>
<SuspendAfterLandingRender active={false} />
</AuthUiContext.Provider>
</Suspense>,
);
await waitFor(() =>
expect(listPublicEditorProjectResourcesMock).toHaveBeenCalledTimes(1),
);
act(() => {
startTransition(() => {
result.rerender(
<Suspense fallback={<span></span>}>
<AuthUiContext.Provider value={abandonedAuth}>
<CreationLandingView
onOpenProject={vi.fn()}
onOpenProjects={vi.fn()}
/>
<SuspendAfterLandingRender active />
</AuthUiContext.Provider>
</Suspense>,
);
});
});
await act(async () => {
resolveCurrentPage([
{
resourceId: 'current-account-resource',
projectId: 'editor-showcase',
label: '当前账号素材',
imageSrc: 'data:image/png;base64,current',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'current account',
viewerLiked: false,
},
]);
await currentPage;
});
expect(await screen.findByText('当前账号素材')).toBeTruthy();
expect(screen.queryByText('切换中')).toBeNull();
});
it('ignores an old account like response after an account switch', async () => {
const user = userEvent.setup();
let resolveOldLike!: (value: {
@@ -897,12 +897,6 @@ export function CreationLandingView({
scope: showcaseRequestScope,
generation: 0,
});
if (showcaseRequestStateRef.current.scope !== showcaseRequestScope) {
showcaseRequestStateRef.current = {
scope: showcaseRequestScope,
generation: showcaseRequestStateRef.current.generation + 1,
};
}
const refreshRecentProjects = useCallback(() => {
if (!isAuthenticated) {
@@ -938,10 +932,22 @@ export function CreationLandingView({
}, [refreshRecentProjects]);
useEffect(() => {
const requestState = { ...showcaseRequestStateRef.current };
const requestState = {
scope: showcaseRequestScope,
generation: showcaseRequestStateRef.current.generation + 1,
};
showcaseRequestStateRef.current = requestState;
const isCurrentRequest = () =>
showcaseRequestStateRef.current.scope === requestState.scope &&
showcaseRequestStateRef.current.generation === requestState.generation;
const invalidateRequest = () => {
if (isCurrentRequest()) {
showcaseRequestStateRef.current = {
scope: requestState.scope,
generation: requestState.generation + 1,
};
}
};
setIsLoadingShowcase(true);
setIsLoadingMoreShowcase(false);
@@ -956,7 +962,7 @@ export function CreationLandingView({
setPendingLikeShowcaseIds(new Set());
setSelectedShowcaseItem(null);
if (!canLoadShowcaseViewer) {
return;
return invalidateRequest;
}
listPublicEditorProjectResources({
@@ -988,6 +994,7 @@ export function CreationLandingView({
setIsLoadingShowcase(false);
}
});
return invalidateRequest;
}, [canLoadShowcaseViewer, showcaseRequestScope, showcaseViewerUserId]);
const loadMoreShowcaseResources = useCallback(