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
31 changes: 0 additions & 31 deletions scripts/update-pr-branches.sh

This file was deleted.

7 changes: 1 addition & 6 deletions tests/playwright/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* // Wait utilities
* waitForPageReady,
* waitForWebSocketConnection,
* waitForFontsLoaded,
* waitForVRTReady,
*
* // Assertions
* assertPageSnapshot,
Expand All @@ -38,7 +38,6 @@ export {
// Wait functions
waitForPageReady,
waitForWebSocketConnection,
waitForFontsLoaded,
waitForElementStable,
waitForNetworkIdle,
waitForApiResponse,
Expand Down Expand Up @@ -71,9 +70,6 @@ export {
VRT_MASK_SELECTORS,
// Mask helpers
getDynamicContentMasks,
getHrMasks,
getTimerMasks,
getSpotifyMasks,
} from './masks'

// ============================================================================
Expand Down Expand Up @@ -153,7 +149,6 @@ export {
takeScreenshot,
takeDashboardScreenshot,
assertFixedDimensions,
waitForVRTReady,
} from './visual'

// ============================================================================
Expand Down
37 changes: 1 addition & 36 deletions tests/playwright/lib/masks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,5 @@ export const VRT_CONFIG = {
* @returns An array of Locators to be used in the `mask` option of `toHaveScreenshot`.
*/
export function getDynamicContentMasks(page: Page): Locator[] {
return [
page.locator(VRT_MASK_SELECTORS.bpmPercent),
page.locator(VRT_MASK_SELECTORS.bpmValue),
page.locator(VRT_MASK_SELECTORS.caloriesValue),
page.locator(VRT_MASK_SELECTORS.timerCountdown),
page.locator(VRT_MASK_SELECTORS.timerPhaseLabel),
page.locator(VRT_MASK_SELECTORS.hrTimeSeriesChart),
page.locator('.variable-text-container'),
]
}

/**
* Returns an array of locators specifically for heart rate (HR) related elements.
*
* @param page - The Playwright Page object.
* @returns An array of Locators for HR elements to be masked.
*/
export function getHrMasks(page: Page): Locator[] {
// Since we rely on global selectors now, we return those
return getDynamicContentMasks(page)
}

/**
* Returns an array of locators specifically for timer-related elements.
*
* @param page - The Playwright Page object.
* @returns An array of Locators for timer elements to be masked.
*/
export function getTimerMasks(page: Page): Locator[] {
// Since we rely on global selectors now, we return those
return getDynamicContentMasks(page)
}

export function getSpotifyMasks(page: Page): Locator[] {
// Since we rely on global selectors now, we return those
return getDynamicContentMasks(page)
return VRT_MASK_SELECTORS.map((selector) => page.locator(selector))
}
10 changes: 5 additions & 5 deletions tests/playwright/lib/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { getBaseURL } from '../../../utils/urls'
import { mockGoogleDocIframe } from './mocks'
import { APIRequestContext } from '@playwright/test'
import {
waitForFontsLoaded,
waitForPageReady,
waitForVRTReady,
waitForWebSocketConnection,
} from './waits'

Expand Down Expand Up @@ -270,9 +270,9 @@ export async function setupVisualRegressionTest(browser: Browser): Promise<{

// Ensure all custom fonts are loaded to prevent visual shifts
await Promise.all([
waitForFontsLoaded(dashboardPage),
waitForFontsLoaded(controlPage),
waitForFontsLoaded(mockPage),
waitForVRTReady(dashboardPage),
waitForVRTReady(controlPage),
waitForVRTReady(mockPage),
])

// Stop any running timers to ensure a consistent initial state
Expand Down Expand Up @@ -506,5 +506,5 @@ export async function startMockHrStreaming(mockPage: Page): Promise<void> {
export async function prepareForVisualRegression(
...pages: Page[]
): Promise<void> {
await Promise.all(pages.map((page) => waitForFontsLoaded(page)))
await Promise.all(pages.map((page) => waitForVRTReady(page)))
}
27 changes: 2 additions & 25 deletions tests/playwright/lib/visual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type ScreenshotOptions,
} from '@playwright/test'
import { checkAccessibility } from './accessibility'
import { getHrMasks, getTimerMasks } from '.'
import { getDynamicContentMasks } from '.'

/**
* Default options for `toHaveScreenshot` to ensure consistency.
Expand All @@ -33,27 +33,6 @@ export const SCREENSHOT_OPTIONS = {
maxDiffPixelRatio: 0.1,
}

/**
* Waits for the page to be ready for visual regression testing.
*
* @param page - The Playwright Page object to prepare.
* @param targetWidth - Optional expected viewport width; polls until `clientWidth` matches.
*/
export async function waitForVRTReady(
page: Page,
targetWidth?: number
): Promise<void> {
await page.evaluateHandle(() => document.fonts.ready)
await page.waitForLoadState('networkidle')
await page.evaluate(() => document.body.offsetHeight)
if (targetWidth !== undefined) {
await page.waitForFunction(
(w) => document.body.clientWidth === w,
targetWidth
)
}
}

/**
* Takes a screenshot of a page or locator with a standardized set of options.
*
Expand Down Expand Up @@ -177,13 +156,11 @@ export async function takeDashboardScreenshot(
...options,
clip: clipOption, // Apply the determined clip region
mask: [
...getTimerMasks(page),
...getHrMasks(page),
...getDynamicContentMasks(page),
page.getByTestId('calorie-count'),
page.getByTestId('google-doc-viewer-iframe'),
page.getByTestId('workout-table-header'),
page.locator('.MUI-Charts-root'),
page.locator('.variable-text-container'),
],
maxDiffPixelRatio: 0.1,
})
Expand Down
39 changes: 15 additions & 24 deletions tests/playwright/lib/waits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,42 +98,33 @@ export async function waitForWebSocketConnection(
)
}

/**
* Wait for all fonts to be fully loaded before taking snapshots.
* This eliminates font-related layout shifts in visual regression tests.
*
* @param page - The Playwright Page object
*/
export async function waitForFontsLoaded(page: Page): Promise<void> {
await page.evaluate(async () => {
await document.fonts.ready
})
}

/**
* Wait for a "Quiet State" before Visual Regression Testing (VRT).
* Ensures Next.js hydration is complete, network is idle, and fonts are ready.
* This prevents sub-pixel anti-aliasing flakiness and MUI transition artifacts.
*
* @param page - The Playwright Page object
* @param targetWidth - Optional expected viewport width; polls until `clientWidth` matches.
*/
export async function waitForVRTReady(page: Page): Promise<void> {
// Wait for network requests to settle (images, data)
try {
await page.waitForLoadState('networkidle', {
timeout: WAIT_TIMEOUTS.NETWORK_IDLE,
})
} catch {
// Ignore networkidle timeouts if some polling requests are keeping it alive
console.warn(
'[waitForVRTReady] networkidle timeout, proceeding to font check'
)
}
export async function waitForVRTReady(
page: Page,
targetWidth?: number
): Promise<void> {
await page.waitForLoadState('domcontentloaded')

// Ensure no active CSS transitions are running / fonts are loaded
await page.evaluate(async () => {
await document.fonts.ready
})

await page.evaluate(() => document.body.offsetHeight)

if (targetWidth !== undefined) {
await page.waitForFunction(
(w) => document.body.clientWidth === w,
targetWidth
)
}
}

/**
Expand Down
9 changes: 2 additions & 7 deletions tests/playwright/vrt-components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
mockSpotifyPlaybackState,
mockLoggedInSession,
resetServerState,
getSpotifyMasks,
getDynamicContentMasks,
} from './lib'
import { checkAccessibility } from './lib/accessibility'
import { takeScreenshot } from './lib/visual'
Expand Down Expand Up @@ -71,7 +71,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({
status: 200,
Expand All @@ -96,9 +95,6 @@ test.describe('Component-Specific VRT', () => {
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
Expand Down Expand Up @@ -153,13 +149,12 @@ test.describe('Component-Specific VRT', () => {
// 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', {
threshold: 0.2, // Tighter threshold for the Paper element
skipA11y: true, // Accessibility checked manually above
mask: getSpotifyMasks(dashboardPage),
mask: getDynamicContentMasks(dashboardPage),
})
})

Expand Down
21 changes: 4 additions & 17 deletions tests/playwright/vrt-hr-components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { type BrowserContext, type Page, expect } from '@playwright/test'
import { test } from './fixtures'
import {
getDynamicContentMasks,
getHrMasks,
setupVisualRegressionTest,
mockMultipleHrDevices,
resetServerState,
Expand Down Expand Up @@ -89,10 +88,7 @@ test.describe('Visual Regression Tests', () => {

await takeScreenshot(dashboard, 'dashboard-with-hr-data.png', {
maxDiffPixelRatio: 0.15,
mask: [
...getDynamicContentMasks(dashboardPage),
...getHrMasks(dashboardPage),
],
mask: [...getDynamicContentMasks(dashboardPage)],
})
})

Expand Down Expand Up @@ -134,10 +130,7 @@ test.describe('Visual Regression Tests', () => {
const dashboard = dashboardPage.getByTestId('dashboard')
await takeScreenshot(dashboard, 'dashboard-with-2-hr-devices.png', {
maxDiffPixelRatio: 0.15,
mask: [
...getDynamicContentMasks(dashboardPage),
...getHrMasks(dashboardPage),
],
mask: [...getDynamicContentMasks(dashboardPage)],
})
})

Expand All @@ -159,10 +152,7 @@ test.describe('Visual Regression Tests', () => {
const dashboard = dashboardPage.getByTestId('dashboard')
await takeScreenshot(dashboard, `dashboard-hr-zone-${zone}.png`, {
maxDiffPixelRatio: 0.1,
mask: [
...getDynamicContentMasks(dashboardPage),
...getHrMasks(dashboardPage),
],
mask: [...getDynamicContentMasks(dashboardPage)],
})
})
}
Expand All @@ -173,10 +163,7 @@ test.describe('Visual Regression Tests', () => {
const dashboard = dashboardPage.getByTestId('dashboard')
await takeScreenshot(dashboard, 'dashboard-hr-disconnected.png', {
maxDiffPixelRatio: 0.1,
mask: [
...getDynamicContentMasks(dashboardPage),
...getHrMasks(dashboardPage),
],
mask: [...getDynamicContentMasks(dashboardPage)],
})
})
})
Expand Down
Loading