Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/test-utils/vrt.social-settle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { COVER_MIN_LAYOUT_PX, coverIsPainted, paintedCoverCount, SETTLED_CARD_STYLE } from './vrt.social-settle';

describe('coverIsPainted', () => {
it('rejects the intrinsic data-URL size and an incomplete decode', () => {
expect(
coverIsPainted({
complete: true,
naturalWidth: 8,
width: 8,
height: 8,
}),
).toBe(false);
expect(
coverIsPainted({
complete: false,
naturalWidth: 0,
width: 280,
height: 280,
}),
).toBe(false);
expect(COVER_MIN_LAYOUT_PX).toBe(64);
});

it('accepts a decoded cover that has stretched into the card frame', () => {
expect(
coverIsPainted({
complete: true,
naturalWidth: 8,
width: 280,
height: 280,
}),
).toBe(true);
});

it('counts only painted covers', () => {
expect(
paintedCoverCount([
{ complete: true, naturalWidth: 8, width: 8, height: 8 },
{ complete: true, naturalWidth: 8, width: 280, height: 280 },
]),
).toBe(1);
});
});

describe('SETTLED_CARD_STYLE', () => {
it('pins the enter animation end state, not the from-frame', () => {
expect(SETTLED_CARD_STYLE).toEqual({
animation: 'none',
opacity: '1',
transform: 'none',
});
});
});
123 changes: 123 additions & 0 deletions src/test-utils/vrt.social-settle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Firefox-only settle for MarketplaceSocialSurfaces captures.
*
* `.marketplace-card-enter` is `opacity: 0` + `translateY(18px)` during its
* fill delay. The suite stylesheet sets `animation: none`, and Playwright
* finishes finite animations before a screenshot, but Firefox under load
* still composites that from-frame. The boots cover then stays invisible
* and the gradient icon shows through. That is the hot-marketplace-modules
* desktop delta (the re-pinned baseline has the cover; the previous one
* does not). Pin the settled frame on the card itself, then wait until each
* expected cover has a real layout box — an 8×8 data URL that has not
* stretched into the media frame does not count.
*/

export const SETTLED_CARD_STYLE = {
animation: 'none',
opacity: '1',
transform: 'none',
} as const;

/** Below this, the element is still the intrinsic data-URL size, not the card frame. */
export const COVER_MIN_LAYOUT_PX = 64;

export interface CoverBox {
complete: boolean;
naturalWidth: number;
width: number;
height: number;
}

export function coverIsPainted(image: CoverBox): boolean {
return (
image.complete &&
image.naturalWidth > 0 &&
image.width >= COVER_MIN_LAYOUT_PX &&
image.height >= COVER_MIN_LAYOUT_PX
);
}

export function paintedCoverCount(images: readonly CoverBox[]): number {
return images.filter(coverIsPainted).length;
}

function nextFrame(): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(() => resolve());
});
}

function coverBox(image: HTMLImageElement): CoverBox {
const box = image.getBoundingClientRect();
return {
complete: image.complete,
naturalWidth: image.naturalWidth,
width: box.width,
height: box.height,
};
}

export async function settleMarketplaceSocialCapture(expectedCovers: number, timeoutMs = 2_000): Promise<void> {
const root = document.querySelector('[data-testid="vrt-root"]');
if (!(root instanceof HTMLElement)) {
throw new Error('VRT social settle: no [data-testid="vrt-root"] element');
}

if (typeof root.getAnimations === 'function') {
for (const animation of root.getAnimations({ subtree: true })) {
try {
animation.cancel();
} catch {
// Already finished or not cancelable.
}
}
}

for (const card of root.querySelectorAll('.marketplace-card-enter')) {
if (!(card instanceof HTMLElement)) continue;
card.style.setProperty('animation', SETTLED_CARD_STYLE.animation, 'important');
card.style.setProperty('opacity', SETTLED_CARD_STYLE.opacity, 'important');
card.style.setProperty('transform', SETTLED_CARD_STYLE.transform, 'important');
}

if (document.fonts?.load) {
await Promise.all(
['400 12px "Inter Tight"', '500 11px "Inter Tight"', '700 16px "Inter Tight"', '700 20px "Inter Tight"'].map(
(font) => document.fonts.load(font).catch(() => []),
),
);
await document.fonts.ready;
}

// Flush layout after the inline settle so Firefox measures the stretched cover.
root.getBoundingClientRect();

const deadline = performance.now() + timeoutMs;
let images: HTMLImageElement[] = [];
while (performance.now() < deadline) {
images = [...root.querySelectorAll('img')].filter(
(node): node is HTMLImageElement => node instanceof HTMLImageElement,
);
if (paintedCoverCount(images.map(coverBox)) >= expectedCovers) {
await nextFrame();
await nextFrame();
images = [...root.querySelectorAll('img')].filter(
(node): node is HTMLImageElement => node instanceof HTMLImageElement,
);
if (paintedCoverCount(images.map(coverBox)) >= expectedCovers) {
return;
}
}
await nextFrame();
}

const detail = images
.map((image) => {
const box = coverBox(image);
return `${image.currentSrc || image.src} complete=${box.complete} naturalWidth=${box.naturalWidth} box=${box.width.toFixed(1)}x${box.height.toFixed(1)}`;
})
.join('; ');
throw new Error(
`VRT social settle: expected ${expectedCovers} painted cover(s) within ${timeoutMs}ms, saw ${paintedCoverCount(images.map(coverBox))}. ${detail}`,
);
}
22 changes: 16 additions & 6 deletions src/test/vrt/marketplace/MarketplaceSocialSurfaces.vrt.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Intentional import order — browser-mode mock factories rely on stable aliases.
/* eslint-disable simple-import-sort/imports */
import { describe, expect, it, vi } from 'vitest';
import { settleMarketplaceSocialCapture } from '@/test-utils/vrt.social-settle';
import { renderForVRT, VRT_ROOT_TESTID } from '@/test-utils/vrt';
import { VRT_VIEWPORT_DESKTOP, VRT_VIEWPORT_MOBILE } from '@/test-utils/vrt.viewports';
import { MarketplaceFollowedSellersShelf } from '@/organisms/Marketplace/MarketplaceFollowedSellersShelf';
Expand Down Expand Up @@ -166,6 +167,15 @@ function SurfaceHost({ children }: { children: React.ReactNode }) {
);
}

async function expectSocialScreenshot(
screen: Awaited<ReturnType<typeof renderForVRT>>,
name: string,
expectedCovers: number,
) {
await settleMarketplaceSocialCapture(expectedCovers);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot(name);
}

describe('Marketplace social surfaces — visual regression', () => {
it('renders the followed-sellers shelf as a horizontal card strip at desktop viewport', async () => {
const f = await fixtures;
Expand All @@ -176,7 +186,7 @@ describe('Marketplace social surfaces — visual regression', () => {
</SurfaceHost>,
{ viewport: VRT_VIEWPORT_DESKTOP, disableHover: true },
);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot('followed-sellers-shelf-desktop');
await expectSocialScreenshot(screen, 'followed-sellers-shelf-desktop', 1);
});

it('renders the followed-sellers shelf at mobile viewport with overflow cards off-screen', async () => {
Expand All @@ -188,7 +198,7 @@ describe('Marketplace social surfaces — visual regression', () => {
</SurfaceHost>,
{ viewport: VRT_VIEWPORT_MOBILE, disableHover: true },
);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot('followed-sellers-shelf-mobile');
await expectSocialScreenshot(screen, 'followed-sellers-shelf-mobile', 1);
});

it('renders nothing at all for the shelf when no followed seller has active listings', async () => {
Expand All @@ -199,7 +209,7 @@ describe('Marketplace social surfaces — visual regression', () => {
</SurfaceHost>,
{ viewport: VRT_VIEWPORT_DESKTOP, disableHover: true },
);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot('followed-sellers-shelf-absent');
await expectSocialScreenshot(screen, 'followed-sellers-shelf-absent', 0);
});

it('renders the Hot-page ending-soon and fresh-listings modules at desktop viewport', async () => {
Expand All @@ -211,7 +221,7 @@ describe('Marketplace social surfaces — visual regression', () => {
</SurfaceHost>,
{ viewport: VRT_VIEWPORT_DESKTOP, disableHover: true },
);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot('hot-marketplace-modules-desktop');
await expectSocialScreenshot(screen, 'hot-marketplace-modules-desktop', 1);
});

it('renders only the fresh-listings module when no auction has known end terms', async () => {
Expand All @@ -223,7 +233,7 @@ describe('Marketplace social surfaces — visual regression', () => {
</SurfaceHost>,
{ viewport: VRT_VIEWPORT_DESKTOP, disableHover: true },
);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot('hot-marketplace-fresh-only-desktop');
await expectSocialScreenshot(screen, 'hot-marketplace-fresh-only-desktop', 1);
});

it('renders nothing at all on Hot when the index has no listings', async () => {
Expand All @@ -234,6 +244,6 @@ describe('Marketplace social surfaces — visual regression', () => {
</SurfaceHost>,
{ viewport: VRT_VIEWPORT_DESKTOP, disableHover: true },
);
await expect(screen.getByTestId(VRT_ROOT_TESTID)).toMatchScreenshot('hot-marketplace-modules-absent');
await expectSocialScreenshot(screen, 'hot-marketplace-modules-absent', 0);
});
});
Loading