修复精选请求并发与缓存响应头
累计追加 Authorization 到 Vary 并保留已有响应字段 将精选请求 generation 绑定到已提交 effect 并在 cleanup 失效 补充中间件与 Suspense 放弃渲染回归测试 同步精选点赞决策与排障文档
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user