fix: 稳定推荐页运行态封面遮罩

This commit is contained in:
2026-06-07 16:00:29 +08:00
parent c810e255a5
commit 8dca8a6443
6 changed files with 672 additions and 34 deletions

View File

@@ -41,6 +41,7 @@ import {
const {
mockQrCodeToDataUrl,
mockRedirectToPaymentUrl,
mockRequestJson,
mockBuildReferralCenter,
mockBuildTaskCenter,
mockClaimRpgProfileTaskReward,
@@ -128,6 +129,7 @@ const {
return {
mockQrCodeToDataUrl: qrCodeToDataUrl,
mockRedirectToPaymentUrl: redirectToPaymentUrl,
mockRequestJson: vi.fn(),
mockBuildReferralCenter: buildReferralCenter,
mockBuildTaskCenter: buildTaskCenter,
mockGetRpgProfileReferralInviteCenter: vi.fn(async () =>
@@ -349,6 +351,10 @@ const {
}));
vi.mock('../../services/apiClient', () => ({
BACKGROUND_AUTH_REQUEST_OPTIONS: {
authImpact: 'background',
},
requestJson: mockRequestJson,
refreshStoredAccessToken: mockRefreshStoredAccessToken,
}));
@@ -3906,6 +3912,234 @@ test('mobile recommend startup keeps cover visible without loading copy', () =>
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
});
test('mobile recommend preloads every fetched work cover', () => {
const preloadedSrcs: string[] = [];
mockRequestJson.mockResolvedValue({
read: {
objectKey: 'generated-recommend/current.png',
signedUrl: 'https://signed.example.com/current-cover.png',
expiresAt: '2099-01-01T00:10:00Z',
},
});
class MockPreloadImage {
decoding = 'auto';
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
set src(value: string) {
preloadedSrcs.push(value);
}
}
vi.stubGlobal('Image', MockPreloadImage);
const firstEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-feed-1',
profileId: 'puzzle-profile-feed-1',
ownerUserId: 'user-feed-1',
publicWorkCode: 'PZ-FEED1',
worldName: '当前拼图',
coverImageSrc: '/generated-recommend/current.png',
} satisfies PlatformPublicGalleryCard;
const secondEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-feed-2',
profileId: 'puzzle-profile-feed-2',
ownerUserId: 'user-feed-2',
publicWorkCode: 'PZ-FEED2',
worldName: '下一拼图',
coverImageSrc: 'next-cover.png',
} satisfies PlatformPublicGalleryCard;
const thirdEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-feed-3',
profileId: 'puzzle-profile-feed-3',
ownerUserId: 'user-feed-3',
publicWorkCode: 'PZ-FEED3',
worldName: '上一拼图',
coverImageSrc: 'previous-cover.png',
} satisfies PlatformPublicGalleryCard;
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [firstEntry, secondEntry, thirdEntry],
activeRecommendEntryKey: 'puzzle:user-feed-1:puzzle-profile-feed-1',
});
expect(preloadedSrcs).toEqual(
expect.arrayContaining([
'next-cover.png',
'previous-cover.png',
'/creation-type-references/puzzle.webp',
]),
);
expect(preloadedSrcs).not.toContain('/generated-recommend/current.png');
return waitFor(() => {
expect(preloadedSrcs).toContain('https://signed.example.com/current-cover.png');
});
});
test('mobile recommend runtime cover keeps the labeled work card visual', () => {
const carouselEntry = buildCarouselPuzzleEntry(
'recommend-card-cover',
'轮播拼图',
'card-cover',
);
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [carouselEntry],
activeRecommendEntryKey:
'puzzle:user-2:puzzle-profile-recommend-card-cover',
isStartingRecommendEntry: true,
recommendRuntimeContent: null,
});
const cover = document.querySelector('.platform-recommend-runtime-cover');
const coverImage = cover?.querySelector('img');
expect(coverImage?.getAttribute('src')).toBe('card-cover-1.png');
expect(coverImage?.getAttribute('src')).not.toBe('card-cover-fallback.png');
expect(cover?.querySelector('.platform-recommend-runtime-preview')).toBeTruthy();
expect(cover?.textContent).toContain('拼图');
expect(cover?.textContent).toContain('轮播拼图');
});
test('mobile recommend runtime cover does not swap to a late signed cover', async () => {
const preloadImages: Array<{
srcValue: string;
onload: (() => void) | null;
}> = [];
mockRequestJson.mockResolvedValue({
read: {
objectKey: 'generated-recommend/late-current.png',
signedUrl: 'https://signed.example.com/late-current-cover.png',
expiresAt: '2099-01-01T00:10:00Z',
},
});
class MockPreloadImage {
decoding = 'auto';
complete = false;
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
srcValue = '';
set src(value: string) {
this.srcValue = value;
preloadImages.push(this);
}
}
vi.stubGlobal('Image', MockPreloadImage);
const generatedCoverEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-late-cover',
profileId: 'puzzle-profile-late-cover',
ownerUserId: 'user-late-cover',
publicWorkCode: 'PZ-LATE1',
worldName: '迟到封面拼图',
coverImageSrc: '/generated-recommend/late-current.png',
} satisfies PlatformPublicGalleryCard;
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [generatedCoverEntry],
activeRecommendEntryKey: 'puzzle:user-late-cover:puzzle-profile-late-cover',
isStartingRecommendEntry: true,
recommendRuntimeContent: null,
});
const cover = document.querySelector('.platform-recommend-runtime-cover');
const coverImage = cover?.querySelector('img');
expect(coverImage?.getAttribute('src')).toBe(
'/creation-type-references/puzzle.webp',
);
await waitFor(() => {
expect(
preloadImages.some(
(image) =>
image.srcValue ===
'https://signed.example.com/late-current-cover.png',
),
).toBe(true);
});
act(() => {
preloadImages
.filter(
(image) =>
image.srcValue ===
'https://signed.example.com/late-current-cover.png',
)
.forEach((image) => image.onload?.());
});
await waitFor(() => expect(mockRequestJson).toHaveBeenCalled());
expect(
document
.querySelector('.platform-recommend-runtime-cover img')
?.getAttribute('src'),
).toBe('/creation-type-references/puzzle.webp');
expect(
Array.from(
document.querySelectorAll('.platform-recommend-runtime-cover img'),
).some(
(image) =>
image.getAttribute('src') ===
'https://signed.example.com/late-current-cover.png',
),
).toBe(false);
});
test('mobile recommend cover waits until runtime images are ready', async () => {
vi.useFakeTimers();
const animationCallbacks: FrameRequestCallback[] = [];
Object.defineProperty(window, 'requestAnimationFrame', {
configurable: true,
writable: true,
value: vi.fn((callback: FrameRequestCallback) => {
animationCallbacks.push(callback);
return animationCallbacks.length;
}),
});
Object.defineProperty(window, 'cancelAnimationFrame', {
configurable: true,
writable: true,
value: vi.fn(),
});
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
isRecommendRuntimeReady: true,
recommendRuntimeContent: (
<img src="/runtime-sprite.png" alt="runtime sprite" />
),
});
act(() => {
animationCallbacks.splice(0).forEach((callback) => callback(16));
});
expect(
document.querySelector('.platform-recommend-runtime-cover')?.className,
).not.toContain('platform-recommend-runtime-cover--hidden');
fireEvent.load(screen.getByAltText('runtime sprite'));
act(() => {
animationCallbacks.splice(0).forEach((callback) => callback(32));
});
expect(
document.querySelector('.platform-recommend-runtime-cover')?.className,
).not.toContain('platform-recommend-runtime-cover--hidden');
await act(async () => {
vi.advanceTimersByTime(520);
});
expect(
document.querySelector('.platform-recommend-runtime-cover')?.className,
).toContain('platform-recommend-runtime-cover--hidden');
});
test('mobile recommend keeps runtime visual stable when active entry changes', async () => {
const animationCallbacks: FrameRequestCallback[] = [];
Object.defineProperty(window, 'requestAnimationFrame', {
@@ -4119,7 +4353,9 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(
document.querySelectorAll('.platform-recommend-runtime-preview'),
document.querySelectorAll(
'.platform-recommend-runtime-preview:not(.platform-recommend-runtime-preview--cover)',
),
).toHaveLength(2);
expect(
document.querySelectorAll('.platform-recommend-swipe-card'),
@@ -4170,6 +4406,10 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
expect(onSelectPreviousRecommendEntry).not.toHaveBeenCalled();
expect(rail?.style.transform).toBe('translate3d(0, 0px, 0)');
expect(rail?.className).toContain(
'platform-recommend-swipe-rail--resetting',
);
vi.useRealTimers();
});