Skip to content

Commit 44e9477

Browse files
Stabilize and Standardize VRT Infrastructure (#9651)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: arii <342438+arii@users.noreply.github.com>
1 parent 683b4ee commit 44e9477

9 files changed

Lines changed: 184 additions & 197 deletions

File tree

tests/playwright/fixtures.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
*/
55
import type { Page } from '@playwright/test'
66
import { test as base, expect } from '@playwright/test'
7+
import {
8+
mockLoggedInSession,
9+
resetServerState,
10+
freezeUIForVRT,
11+
stopTimer,
12+
} from './lib'
713

814
type PageFixtures = {
915
dashboardPage: Page
@@ -13,8 +19,13 @@ type PageFixtures = {
1319
}
1420

1521
export const test = base.extend<PageFixtures>({
16-
dashboardPage: async ({ context }, applyFixture) => {
22+
dashboardPage: async ({ context, request }, use) => {
1723
const page = await context.newPage()
24+
25+
await mockLoggedInSession(context)
26+
await resetServerState(request)
27+
await freezeUIForVRT(page)
28+
1829
page.on('console', (msg) => {
1930
const text = msg.text()
2031
// Filter out expected noise
@@ -27,22 +38,44 @@ export const test = base.extend<PageFixtures>({
2738

2839
console.log(`Console ${msg.type()}: ${text}`)
2940
})
30-
await applyFixture(page)
41+
42+
await use(page)
3143
},
3244

33-
controlPage: async ({ context }, applyFixture) => {
45+
controlPage: async ({ context }, use) => {
3446
const page = await context.newPage()
35-
await applyFixture(page)
47+
48+
await mockLoggedInSession(context)
49+
await freezeUIForVRT(page)
50+
51+
await use(page)
52+
53+
try {
54+
await stopTimer(page)
55+
} catch (error) {
56+
console.warn(
57+
'Failed to stop timer during teardown, WS state may linger:',
58+
error
59+
)
60+
}
3661
},
3762

38-
mockPage: async ({ context }, applyFixture) => {
63+
mockPage: async ({ context }, use) => {
3964
const page = await context.newPage()
40-
await applyFixture(page)
65+
66+
await mockLoggedInSession(context)
67+
await freezeUIForVRT(page)
68+
69+
await use(page)
4170
},
4271

43-
connectPage: async ({ context }, applyFixture) => {
72+
connectPage: async ({ context }, use) => {
4473
const page = await context.newPage()
45-
await applyFixture(page)
74+
75+
await mockLoggedInSession(context)
76+
await freezeUIForVRT(page)
77+
78+
await use(page)
4679
},
4780
})
4881

tests/playwright/lib/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export {
111111
setupComprehensiveTest,
112112
setupCoreTest,
113113
resetServerState,
114+
freezeUIForVRT,
115+
mockLoggedInSession,
116+
mockSpotifyAccessToken,
114117
// Timer utilities
115118
stopTimer,
116119
configureTimer,
@@ -137,8 +140,6 @@ export {
137140
mockGoogleDocIframe,
138141
mockMultipleHrDevices,
139142
mockSpotifyPlaybackState,
140-
mockLoggedInSession,
141-
mockSpotifyAccessToken,
142143
mockSpotifyPlaylists,
143144
} from './mocks'
144145

tests/playwright/lib/mocks.ts

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ import type {
1111
SpotifyData as SpotifyPlaybackState,
1212
} from '../../../types/websocket'
1313

14-
/**
15-
* A consistent, offline-safe 1x1 transparent PNG image for VRT.
16-
* Prevents flaky tests caused by external placeholder services.
17-
*/
18-
const MOCK_IMAGE =
19-
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=='
14+
import { MOCK_IMAGE } from './setup'
2015

2116
const STABLE_WORKOUT_HTML = `
2217
<!DOCTYPE html>
@@ -138,35 +133,6 @@ export async function mockSpotifyPlaybackState(
138133
}, payload)
139134
}
140135

141-
/**
142-
* Mocks the NextAuth session to simulate a logged-in user.
143-
*
144-
* @param context - The Playwright BrowserContext object.
145-
*/
146-
export async function mockLoggedInSession(
147-
context: BrowserContext
148-
): Promise<void> {
149-
// Mock the session endpoint
150-
await context.route('**/api/auth/session', (route) => {
151-
route.fulfill({
152-
status: 200,
153-
contentType: 'application/json',
154-
body: JSON.stringify({
155-
user: {
156-
name: 'Test User',
157-
email: 'test@example.com',
158-
image: MOCK_IMAGE,
159-
},
160-
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
161-
accessToken: 'mock-access-token',
162-
}),
163-
})
164-
})
165-
166-
// Mock the Spotify access token endpoint as it's required for the control panel
167-
await mockSpotifyAccessToken(context)
168-
}
169-
170136
/**
171137
* Mocks the Spotify playlists endpoint.
172138
*
@@ -222,24 +188,3 @@ export async function mockSpotifyPlaylists(
222188
})
223189
})
224190
}
225-
226-
/**
227-
* Mocks the Spotify access token endpoint.
228-
*
229-
* @param context - The Playwright BrowserContext object.
230-
* @param accessToken - The mock access token to return.
231-
*/
232-
export async function mockSpotifyAccessToken(
233-
context: BrowserContext,
234-
accessToken: string = 'mock-spotify-access-token'
235-
): Promise<void> {
236-
await context.route('**/api/spotify/access-token', (route) => {
237-
route.fulfill({
238-
status: 200,
239-
contentType: 'application/json',
240-
body: JSON.stringify({
241-
accessToken,
242-
}),
243-
})
244-
})
245-
}

tests/playwright/lib/setup.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,19 @@ export async function navigateAndWait(
142142
}
143143

144144
// Stabilize VRT by disabling animations, transitions, and backdrop filters
145-
await page.addStyleTag({
146-
content: `
145+
await freezeUIForVRT(page)
146+
147+
await waitForPageReady(page)
148+
}
149+
150+
/**
151+
* Aggressively disable animations, transitions, and scrollbars.
152+
*/
153+
export async function freezeUIForVRT(page: Page): Promise<void> {
154+
await page.addInitScript(() => {
155+
const style = document.createElement('style')
156+
style.id = 'vrt-freeze-styles'
157+
style.textContent = `
147158
*, *::before, *::after {
148159
transition: none !important;
149160
animation: none !important;
@@ -161,10 +172,9 @@ export async function navigateAndWait(
161172
opacity: 1 !important;
162173
transform: none !important;
163174
}
164-
`,
175+
`
176+
document.head.appendChild(style)
165177
})
166-
167-
await waitForPageReady(page)
168178
}
169179

170180
/**
@@ -372,6 +382,61 @@ export async function setupCoreTest(options: { page: Page }): Promise<void> {
372382
)
373383
}
374384

385+
/**
386+
* A consistent, offline-safe 1x1 transparent PNG image for VRT.
387+
* Prevents flaky tests caused by external placeholder services.
388+
*/
389+
export const MOCK_IMAGE =
390+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=='
391+
392+
/**
393+
* Mocks the NextAuth session.
394+
*/
395+
export async function mockLoggedInSession(
396+
context: BrowserContext
397+
): Promise<void> {
398+
// Mock the session endpoint
399+
await context.route('**/api/auth/session', (route) => {
400+
route.fulfill({
401+
status: 200,
402+
contentType: 'application/json',
403+
body: JSON.stringify({
404+
user: {
405+
name: 'Test User',
406+
email: 'test@example.com',
407+
image: MOCK_IMAGE,
408+
},
409+
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
410+
accessToken: 'mock-access-token',
411+
}),
412+
})
413+
})
414+
415+
// Mock the Spotify access token endpoint as it's required for the control panel
416+
await mockSpotifyAccessToken(context)
417+
}
418+
419+
/**
420+
* Mocks the Spotify access token endpoint.
421+
*
422+
* @param context - The Playwright BrowserContext object.
423+
* @param accessToken - The mock access token to return.
424+
*/
425+
export async function mockSpotifyAccessToken(
426+
context: BrowserContext,
427+
accessToken: string = 'mock-spotify-access-token'
428+
): Promise<void> {
429+
await context.route('**/api/spotify/access-token', (route) => {
430+
route.fulfill({
431+
status: 200,
432+
contentType: 'application/json',
433+
body: JSON.stringify({
434+
accessToken,
435+
}),
436+
})
437+
})
438+
}
439+
375440
/**
376441
* Stop any running timer on the control page.
377442
* Useful for ensuring tests start from a clean state.

tests/playwright/lib/visual.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ export async function takeScreenshot(
7979
await checkAccessibility(target)
8080
}
8181

82+
// Automatically mask variable text containers globally to ensure stability
83+
const page = isLocator ? (target as Locator).page() : (target as Page)
84+
const existingMasks = [screenshotOptions.mask].flat().filter(Boolean)
85+
8286
const finalOptions = {
8387
scale: 'css', // Prevent high-DPI (Retina) scaling mismatches in CI
8488
...SCREENSHOT_OPTIONS,
8589
...screenshotOptions,
90+
mask: [...existingMasks, page.locator('.variable-text-container')],
8691
}
8792

8893
// Remove fullPage option if the target is a Locator, as it's only valid for Page screenshots.

tests/playwright/vrt-components.spec.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { test } from './fixtures'
33
import {
44
setupMinimalVisualRegressionTest,
55
mockSpotifyPlaybackState,
6-
mockLoggedInSession,
7-
resetServerState,
86
getSpotifyMasks,
97
} from './lib'
108
import { checkAccessibility } from './lib/accessibility'
@@ -13,22 +11,20 @@ import { waitForPageReady, waitForWebSocketConnection } from './lib/waits'
1311
import { VRT_TIMEOUTS } from './lib/timeouts'
1412

1513
test.describe('Component-Specific VRT', () => {
16-
test.beforeEach(async ({ dashboardPage, request }) => {
17-
await resetServerState(request)
18-
await setupMinimalVisualRegressionTest(dashboardPage, '/')
19-
})
20-
2114
test('BottomNavBar highlights correct icon', async ({ dashboardPage }) => {
15+
await setupMinimalVisualRegressionTest(dashboardPage, '/')
2216
const bottomNav = dashboardPage.getByTestId('bottom-nav-bar')
2317
await takeScreenshot(bottomNav, 'bottom-nav-bar.png')
2418
})
2519

2620
test('Footer rendering', async ({ dashboardPage }) => {
21+
await setupMinimalVisualRegressionTest(dashboardPage, '/')
2722
const footer = dashboardPage.getByTestId('footer')
2823
await takeScreenshot(footer, 'footer.png')
2924
})
3025

3126
test('LoadingIndicator visibility', async ({ dashboardPage }) => {
27+
await setupMinimalVisualRegressionTest(dashboardPage, '/')
3228
// Force visibility and pause animation for VRT
3329
await dashboardPage.evaluate(() => {
3430
const el = document.querySelector(
@@ -87,11 +83,8 @@ test.describe('Component-Specific VRT', () => {
8783
await takeScreenshot(tableHeader, 'workout-table-header.png')
8884
})
8985

90-
test('SpotifyDeviceSelector menu', async ({ dashboardPage, context }) => {
91-
// Mock session to appear logged in
92-
await mockLoggedInSession(context)
93-
await dashboardPage.reload()
94-
await waitForPageReady(dashboardPage)
86+
test('SpotifyDeviceSelector menu', async ({ dashboardPage }) => {
87+
await setupMinimalVisualRegressionTest(dashboardPage, '/')
9588

9689
// Wait for the WebSocket to fully reconnect after the reload
9790
// before applying mocks, otherwise the server's initial STATE_SYNC
@@ -162,6 +155,7 @@ test.describe('Component-Specific VRT', () => {
162155
})
163156

164157
test('RefreshIconButton states', async ({ dashboardPage }) => {
158+
await setupMinimalVisualRegressionTest(dashboardPage, '/')
165159
const refreshButton = dashboardPage.getByTestId('refresh-icon-button')
166160
await takeScreenshot(refreshButton, 'refresh-icon-button.png')
167161
await refreshButton.hover()

0 commit comments

Comments
 (0)