fix: 稳定推荐页运行态封面遮罩
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user