Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
906c380
fix(test): stabilize visual regression tests and mock google doc data
google-labs-jules[bot] Mar 14, 2026
5d495ec
fix(test): stabilize visual regression tests and mock google doc data
google-labs-jules[bot] Mar 14, 2026
3907789
fix: stabilize visual regression tests
google-labs-jules[bot] Mar 14, 2026
025833e
fix: address VRT flake in SpotifyDeviceSelector menu
google-labs-jules[bot] Mar 14, 2026
714c193
fix: address lint error in tests/playwright/vrt-components.spec.ts
google-labs-jules[bot] Mar 14, 2026
c5ce22d
fix: stabilize visual regression tests
google-labs-jules[bot] Mar 14, 2026
502f41e
fix: stabilize visual regression tests
google-labs-jules[bot] Mar 15, 2026
6014539
fix: address lint error in tests/playwright/vrt-dashboard.spec.ts
google-labs-jules[bot] Mar 15, 2026
b5e2848
fix: update dashboard visual test snapshots
google-labs-jules[bot] Mar 15, 2026
24feb1e
fix: improve snapshot and accessibility VRT stability
google-labs-jules[bot] Mar 15, 2026
6ee8b31
fix: address lint issues in Playwright tests
google-labs-jules[bot] Mar 15, 2026
77b4d0e
fix: address VRT component sizing instability
google-labs-jules[bot] Mar 15, 2026
9436c68
fix: update VRT snapshots
google-labs-jules[bot] Mar 15, 2026
76db0e6
fix: increase stability tolerance in visual tests
google-labs-jules[bot] Mar 15, 2026
bd3a515
fix: resolve visual regression test flakiness due to font rendering a…
google-labs-jules[bot] Mar 15, 2026
6ee40a9
fix(vrt): address PR feedback by removing redundant configs and hacky…
google-labs-jules[bot] Mar 16, 2026
4801fcd
fix(vrt): improve device status rendering logic in ConnectView and up…
google-labs-jules[bot] Mar 16, 2026
571a609
Merge branch 'leader' into fix-vrt-flakiness-and-google-doc-leak-1312…
arii Mar 16, 2026
9b7711e
test(small): Fix VRT layout flakiness and sub-pixel mismatches (#9533)
arii Mar 16, 2026
70dd94c
test(medium): Fix VRT accessibility timeouts and tablet layout shifts…
arii Mar 16, 2026
ed23617
Update vrt-components.spec.ts
arii Mar 16, 2026
627d25e
fix(test): polish VRT suite by removing anti-patterns (#9542)
arii Mar 17, 2026
0f1035f
chore(small): fix: Refactor VRTs and clean up GOOGLE_DOC_WORKOUT_URL …
arii Mar 18, 2026
a6dea26
Merge branch 'leader' into fix-vrt-flakiness-and-google-doc-leak-1312…
arii Mar 18, 2026
aaad5e9
feat(medium): Refactor VRT Stabilization and Fix Layout Regressions (…
arii Mar 20, 2026
182ba42
Force resolve remaining conflicts using 'theirs'
arii Mar 20, 2026
ac0391a
Merge branch 'leader' into fix-vrt-flakiness-and-google-doc-leak-1312…
arii Mar 20, 2026
3f41095
Refactor VRT Tests to Fix Flakiness and Over-engineering (#9659)
arii Mar 20, 2026
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
6 changes: 5 additions & 1 deletion app/client/connect/ConnectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ export default function ConnectView({
!deviceStatus.includes('Disconnected') && (
<Alert
data-testid="connection-status-alert"
severity={deviceStatus.includes('Failed') ? 'error' : 'info'}
severity={
deviceStatus.toLowerCase().startsWith('failed')
? 'error'
: 'info'
}
sx={{ mb: 2 }}
>
{deviceStatus}
Expand Down
6 changes: 6 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,12 @@ export default defineConfig({
'--disable-dev-shm-usage',
// Hide scrollbars for consistent VRT snapshots
'--hide-scrollbars',
'--font-render-hinting=none',
],
},
contextOptions: {
reducedMotion: 'reduce',
},
viewport: DESKTOP_VIEWPORT,
video: {
mode: 'retain-on-failure',
Expand Down Expand Up @@ -178,6 +182,8 @@ export default defineConfig({
'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit',
GOOGLE_DOC_IFRAME_URL:
'https://docs.google.com/spreadsheets/d/e/2PACX-1vTev5AMiHYi2Jkg9x6zRQoiJ_o2X_wZMqAXVpwgjlSqzlcXelxSc7psjE8n3N-ghzXMFtnv51nc2fJZ/pub?embedded=true',
NEXT_PUBLIC_GOOGLE_DOC_IFRAME_URL:
'https://docs.google.com/spreadsheets/d/e/2PACX-1vTev5AMiHYi2Jkg9x6zRQoiJ_o2X_wZMqAXVpwgjlSqzlcXelxSc7psjE8n3N-ghzXMFtnv51nc2fJZ/pub?embedded=true',
WEBSOCKET_WATCHDOG_INTERVAL: '5000',
},
},
Expand Down
10 changes: 5 additions & 5 deletions tests/playwright/lib/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ export async function checkAccessibility(target: Page | Locator) {
// If the target is a Locator, we need to add a temporary unique attribute
// to it so we can scope the accessibility scan to that element.
if ('page' in target) {
await (target as Locator).waitFor({ state: 'attached' })
await (target as Locator).evaluate(
(node, id) => node.setAttribute(id, ''),
uniqueId
)
// Ensure the target is actually attached to the DOM before evaluation
// This is critical for MUI Portals/Menus
await target.waitFor({ state: 'attached', timeout: 5000 })

await target.evaluate((node, id) => node.setAttribute(id, ''), uniqueId)
selector = `[${uniqueId}]`
}

Expand Down
5 changes: 5 additions & 0 deletions tests/playwright/lib/bluetooth-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { Page } from '@playwright/test'

export const injectBluetoothMocks = async (page: Page) => {
await page.addInitScript(() => {
window.__TEST_CONTROLS__ = window.__TEST_CONTROLS__ || {
setHrmStatus: () => {},
setCustomHrmStatusMessage: () => {},
}

// 2. Internal State for the Mock
const _pairedDevices: MockBluetoothDevice[] = []
let _connectedDevice: MockBluetoothDevice | null = null
Expand Down
3 changes: 2 additions & 1 deletion tests/playwright/lib/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@ export async function setupMinimalVisualRegressionTest(
if (path === '' || path === '/') {
await mockGoogleDocIframe(page)
}
await navigateAndWait(page, path)
const queryParamSeparator = path.includes('?') ? '&' : '?'
await navigateAndWait(page, `${path}${queryParamSeparator}testing=true`)
}

/**
Expand Down
26 changes: 4 additions & 22 deletions tests/playwright/vrt-components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
setupMinimalVisualRegressionTest,
mockSpotifyPlaybackState,
mockLoggedInSession,
resetServerState,
getSpotifyMasks,
resetServerState,
} from './lib'
import { checkAccessibility } from './lib/accessibility'
import { takeScreenshot } from './lib/visual'
Expand All @@ -25,29 +25,27 @@ test.describe('Component-Specific VRT', () => {

test('Footer rendering', async ({ dashboardPage }) => {
const footer = dashboardPage.getByTestId('footer')
await takeScreenshot(footer, 'footer.png')
await takeScreenshot(footer, 'footer.png', {
maxDiffPixelRatio: 0.1,
})
})

test('LoadingIndicator visibility', async ({ dashboardPage }) => {
// Force visibility and pause animation for VRT
await dashboardPage.evaluate(() => {
const el = document.querySelector(
'[data-testid="loading-indicator"]'
) as HTMLElement
if (el) {
el.style.opacity = '1'
el.style.visibility = 'visible'
// Force an opaque background to prevent pixel leakage from underlying content
el.style.backgroundColor = 'rgb(0, 0, 0)'
// Pause any CSS animations/transitions specifically on this element
el.style.animationPlayState = 'paused'
el.style.transition = 'none'
}
})
const loadingIndicator = dashboardPage.getByTestId('loading-indicator')
await expect(loadingIndicator).toBeVisible()

// Mask the animated progress circle as it's highly flaky in VRT
const progress = loadingIndicator.getByTestId('loading-indicator-progress')

await takeScreenshot(loadingIndicator, 'loading-indicator.png', {
Expand All @@ -56,7 +54,6 @@ test.describe('Component-Specific VRT', () => {
})

test('GoogleDocViewer shrunk state', async ({ dashboardPage }) => {
// Ensure we are in non-native mode for this test, and enable testing mode
await dashboardPage.goto('/?native=false&testing=true')
await waitForPageReady(dashboardPage)

Expand All @@ -69,7 +66,6 @@ test.describe('Component-Specific VRT', () => {
})

test('WorkoutTableHeader rendering', async ({ dashboardPage }) => {
// Setup network interception first
await dashboardPage.route('/api/workout*', async (route) => {
await route.fulfill({
json: {
Expand All @@ -78,7 +74,6 @@ test.describe('Component-Specific VRT', () => {
})
})

// Ensure we are in native mode for this test, and enable testing mode
await dashboardPage.goto('/?native=true&testing=true')
await waitForPageReady(dashboardPage)

Expand All @@ -88,17 +83,12 @@ test.describe('Component-Specific VRT', () => {
})

test('SpotifyDeviceSelector menu', async ({ dashboardPage, context }) => {
// Mock session to appear logged in
await mockLoggedInSession(context)
await dashboardPage.reload()
await waitForPageReady(dashboardPage)

// Wait for the WebSocket to fully reconnect after the reload
// before applying mocks, otherwise the server's initial STATE_SYNC
// will immediately overwrite the mock.
await waitForWebSocketConnection(dashboardPage)

// Mock Spotify state with devices to show the component naturally
await mockSpotifyPlaybackState(dashboardPage, {
playback: {
is_playing: true,
Expand Down Expand Up @@ -144,14 +134,10 @@ test.describe('Component-Specific VRT', () => {
const menu = dashboardPage
.locator('[data-testid="spotify-device-selector-menu-paper"]')
.last()

// Give the menu time to mount in the portal and stabilize before checking visibility
await expect(menu).toBeVisible()

// Wait for the opacity transition to finish rendering
await expect(menu).toHaveCSS('opacity', '1')

// Perform manual accessibility check on the specific menu element to ensure context validity
await checkAccessibility(menu)

await takeScreenshot(menu, 'spotify-device-selector-menu.png', {
Expand All @@ -169,13 +155,9 @@ test.describe('Component-Specific VRT', () => {
})

test('ErrorFallback UI', async ({ dashboardPage }) => {
// Navigate to dashboard with test-error=true to trigger the real ErrorBoundary and ErrorFallback component.
// NOTE: This error is now triggered client-side to avoid noisy server logs and 500 responses.
await dashboardPage.goto('/?test-error=true&testing=true')

const errorFallback = dashboardPage.getByTestId('error-fallback')
// Explicit extended timeout for ErrorFallback as triggering the error boundary and
// rendering the fallback UI can be slower on CI environments.
await expect(errorFallback).toBeVisible({
timeout: VRT_TIMEOUTS.EXTENDED,
})
Expand Down
15 changes: 9 additions & 6 deletions tests/playwright/vrt-connect-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { BluetoothConnectionStatus } from '../../types/bluetooth'
test.describe('Visual Regression Tests for /client/connect Page', () => {
test.beforeEach(async ({ connectPage }) => {
await injectBluetoothMocks(connectPage)
await connectPage.goto('/client/connect')

await connectPage.goto('/client/connect?testing=true')
await waitForPageReady(connectPage, { timeout: VRT_TIMEOUTS.STANDARD })
await connectPage.getByLabel('Your Name').fill('VRT Runner')
await connectPage.getByLabel('Your Age').fill('30')

await connectPage.waitForFunction(
() => window.__TEST_CONTROLS__?.setHrmStatus,
() => typeof window.__TEST_CONTROLS__?.setHrmStatus === 'function',
{
timeout: VRT_TIMEOUTS.HYDRATION,
}
Expand Down Expand Up @@ -80,10 +81,12 @@ test.describe('Visual Regression Tests for /client/connect Page', () => {
})

test('no devices found state', async ({ connectPage }) => {
await connectPage.evaluate(
(status) => window.__TEST_CONTROLS__!.setHrmStatus!(status),
BluetoothConnectionStatus.DISCONNECTED
)
await connectPage.evaluate((status) => {
window.__TEST_CONTROLS__!.setHrmStatus!(status)
window.__TEST_CONTROLS__!.setCustomHrmStatusMessage!(
'Connection cancelled. No device selected.'
)
}, BluetoothConnectionStatus.DISCONNECTED)
// The button should be visible and ready for another attempt.
await expect(
connectPage.getByRole('button', { name: 'Connect Bluetooth HRM' })
Expand Down
104 changes: 68 additions & 36 deletions tests/playwright/vrt-dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,23 @@ test.describe('Visual Regression Tests', () => {
})

test.beforeEach(async ({ request }) => {
// 1. Reset server-side state
await dashboardPage.route(
'https://sdk.scdn.co/spotify-player.js',
(route) => route.abort()
)

await resetServerState(request)

// 2. Reload pages to ensure clean client state and fresh WebSocket connection
await dashboardPage.reload()
await controlPage.reload()
await mockPage.reload()

// 3. Wait for pages to be ready and connected
await dashboardPage.mouse.click(0, 0)

await waitForPageReady(dashboardPage)
await waitForPageReady(controlPage)
await waitForPageReady(mockPage)

// Ensure WebSocket is re-established after server reset
await Promise.all([
dashboardPage.waitForFunction(
() => document.body.dataset.connectionStatus === 'connected',
{ timeout: 5000 }
),
controlPage.waitForFunction(
() => document.body.dataset.connectionStatus === 'connected',
{ timeout: 5000 }
),
mockPage.waitForFunction(
() => document.body.dataset.connectionStatus === 'connected',
{ timeout: 5000 }
),
])

await dashboardPage.waitForFunction(
() => document.body.dataset.connectionStatus === 'connected',
{ timeout: 5000 }
)

// Force visibility to avoid flaky screenshots due to animations
await dashboardPage.addStyleTag({
Expand All @@ -77,6 +66,23 @@ test.describe('Visual Regression Tests', () => {
})

test.describe('Dashboard Component', () => {
const BASE_VRT_OPTIONS = {
maxDiffPixelRatio: 0.05,
fullPage: false,
}

const getVrtOptions = (page: Page) => ({
...BASE_VRT_OPTIONS,
mask: [
...getDynamicContentMasks(page),
page.locator('.variable-text-container'),
],
})

test.beforeEach(async () => {
await dashboardPage.evaluateHandle(() => document.fonts.ready)
})

test('initial, empty state', async () => {
const dashboard = dashboardPage.getByTestId('dashboard')
await takeScreenshot(dashboard, 'dashboard-empty.png', {
Expand All @@ -85,9 +91,7 @@ test.describe('Visual Regression Tests', () => {
})
})

// NEW: Active timer with no HR data
test('active timer without HR data', async () => {
// Ensure dashboard is ready
const timerContainer = dashboardPage.getByTestId(
'timer-display-container'
)
Expand All @@ -98,15 +102,13 @@ test.describe('Visual Regression Tests', () => {

await controlPage.getByTestId('start-timer-button').click()

// Wait for timer to transition from idle (00:00) to prepare (e.g. 10 or 05)
await expect(dashboardPage.getByTestId('timer-countdown')).not.toHaveText(
/00:00/,
{
timeout: VRT_TIMEOUTS.STANDARD,
}
)

// Assert timer tile height is fixed
const timerCard = dashboardPage.getByTestId('timer-display-container')
await assertFixedDimensions(timerCard, {
maxHeight: 600,
Expand All @@ -119,21 +121,43 @@ test.describe('Visual Regression Tests', () => {
})
})

// NEW: Active timer WITH HR data (the regression scenario)
test('active timer with HR data', async () => {
// Reload mockPage and controlPage specifically for this test
// because they weren't reloaded in the optimized beforeEach hook
await controlPage.reload()
await mockPage.reload()
await waitForPageReady(controlPage)
await waitForPageReady(mockPage)
await Promise.all([
controlPage.waitForFunction(
() => document.body.dataset.connectionStatus === 'connected',
{ timeout: 5000 }
),
mockPage.waitForFunction(
() => document.body.dataset.connectionStatus === 'connected',
{ timeout: 5000 }
),
])

await mockPage.getByLabel('Current BPM').fill('155')
await mockPage.getByRole('button', { name: 'Zone 4' }).click()
await controlPage.getByTestId('start-timer-button').click()

// Wait for timer to start on dashboard
await expect(dashboardPage.getByTestId('timer-countdown')).not.toHaveText(
/00:00/,
{
timeout: VRT_TIMEOUTS.STANDARD,
}
)

// Assert grid row height is stable
// Ensure data binding worked
await expect(dashboardPage.getByTestId('bpm-value').first()).toHaveText(
'155 BPM',
{
timeout: VRT_TIMEOUTS.STANDARD,
}
)

const topRow = dashboardPage
.locator('[data-testid="dashboard"] > div')
.first()
Expand All @@ -142,19 +166,27 @@ test.describe('Visual Regression Tests', () => {
})

const dashboard = dashboardPage.getByTestId('dashboard')

await dashboardPage.setViewportSize({ width: 1920, height: 1080 })

await takeScreenshot(dashboard, 'dashboard-active-timer-with-hr.png', {
mask: [...getDynamicContentMasks(dashboardPage)],
maxDiffPixelRatio: 0.15, // Higher threshold for complex combined state
mask: [
...getDynamicContentMasks(dashboardPage),
dashboardPage.locator('.variable-text-container'),
],
})
})

test('large desktop viewport', async () => {
await dashboardPage.setViewportSize({ width: 2560, height: 1440 })

const dashboard = dashboardPage.getByTestId('dashboard')
await takeScreenshot(dashboard, 'dashboard-large-desktop.png', {
mask: getDynamicContentMasks(dashboardPage),
maxDiffPixelRatio: 0.1,
})

await takeScreenshot(
dashboard,
'dashboard-large-desktop.png',
getVrtOptions(dashboardPage)
)
})
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading