Skip to content

Commit 923878b

Browse files
test(small): feat: implements granular masking for VRT (#9101)
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 4045968 commit 923878b

21 files changed

Lines changed: 78 additions & 60 deletions

components/HrTile.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { useTheme, alpha } from '@mui/material/styles'
1010
import { isGenericName } from '@/utils/hrm'
1111
import ControlCard from '@/components/shared/ControlCard'
1212
import { HrTileProps } from '@/types'
13+
import { HR_TILE_MIN_HEIGHT } from '@/constants/layout'
1314

1415
const HERO_FONT_FAMILY = 'var(--font-roboto-mono), "Courier New", monospace'
15-
const HR_TILE_MIN_HEIGHT = 180
1616

1717
const IdentityTier = ({ name }: { name: string }) => (
1818
<Box sx={{ pt: 1, textAlign: 'center' }}>
@@ -42,7 +42,7 @@ const HeroTier = ({ percentage }: { percentage: number }) => (
4242
}}
4343
>
4444
<Typography
45-
data-testid="live-hr-percent"
45+
data-testid="bpm-percent"
4646
variant="h2"
4747
component="div"
4848
sx={{
@@ -101,7 +101,11 @@ const DataTier = ({
101101
}}
102102
>
103103
<MetricItem value={value ?? '---'} label="BPM" testId="bpm-value" />
104-
<MetricItem value={Math.floor(calories)} label="KCAL" />
104+
<MetricItem
105+
value={Math.floor(calories)}
106+
label="KCAL"
107+
testId="calories-value"
108+
/>
105109
</Box>
106110
)
107111

constants/layout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const HR_TILE_MIN_HEIGHT = 180

context/WebSocketContext.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ export interface WebSocketContextType extends WebSocketState {
3333
disconnect: () => void
3434
}
3535

36+
// This encapsulates the logic to avoid running it on every render inside the component
37+
const isTestEnvironment = () => {
38+
if (typeof window === 'undefined') return false
39+
return (
40+
process.env.NODE_ENV !== 'production' ||
41+
process.env.NEXT_PUBLIC_TESTING === 'true' ||
42+
window.location.search.includes('testing=true')
43+
)
44+
}
45+
3646
export const WebSocketContext = createContext<WebSocketContextType | null>(null)
3747

3848
export const WebSocketProvider = ({
@@ -110,12 +120,7 @@ export const WebSocketProvider = ({
110120
pendingActions.current = JSON.parse(savedActions)
111121
}
112122

113-
if (
114-
process.env.NODE_ENV !== 'production' ||
115-
process.env.NEXT_PUBLIC_TESTING === 'true' ||
116-
(typeof window !== 'undefined' &&
117-
window.location.search.includes('testing=true'))
118-
) {
123+
if (isTestEnvironment()) {
119124
;(
120125
window as Window & { __TEST_CONTROLS__?: TestControls }
121126
).__TEST_CONTROLS__ = {
@@ -192,8 +197,7 @@ export const WebSocketProvider = ({
192197
setConnectionStatus('Connected')
193198

194199
// Set test flag for Playwright tests - use a more reliable method
195-
if (typeof window !== 'undefined') {
196-
window.__TEST_WEBSOCKET_READY__ = true
200+
if (isTestEnvironment()) {
197201
document.body.dataset.connectionStatus = 'connected'
198202
}
199203

@@ -229,8 +233,7 @@ export const WebSocketProvider = ({
229233
)
230234
setConnectionStatus('Disconnected')
231235

232-
if (typeof window !== 'undefined') {
233-
window.__TEST_WEBSOCKET_READY__ = false
236+
if (isTestEnvironment()) {
234237
document.body.dataset.connectionStatus = 'disconnected'
235238
}
236239

@@ -318,12 +321,7 @@ export const WebSocketProvider = ({
318321
connectRef.current = connect
319322
connect()
320323

321-
if (
322-
typeof window !== 'undefined' &&
323-
(process.env.NODE_ENV !== 'production' ||
324-
process.env.NEXT_PUBLIC_TESTING === 'true' ||
325-
window.location.search.includes('testing=true'))
326-
) {
324+
if (isTestEnvironment()) {
327325
const testControls = (
328326
window as Window & { __TEST_CONTROLS__?: TestControls }
329327
).__TEST_CONTROLS__

tests/playwright/lib/assertions.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,10 @@ export async function assertWebSocketConnected(
101101
): Promise<void> {
102102
const { timeout = 10000 } = options
103103

104-
const isConnected = await page.evaluate((t) => {
105-
return new Promise<boolean>((resolve) => {
106-
const checkConnection = () => {
107-
if (window.__TEST_WEBSOCKET_READY__ === true) {
108-
resolve(true)
109-
return
110-
}
111-
setTimeout(checkConnection, 100)
112-
}
113-
checkConnection()
114-
setTimeout(() => resolve(false), t)
115-
})
116-
}, timeout)
117-
118-
expect(isConnected).toBe(true)
104+
await page.waitForFunction(
105+
() => document.body.dataset.connectionStatus === 'connected',
106+
{ timeout }
107+
)
119108
}
120109

121110
/**

tests/playwright/lib/masks.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import type { Locator, Page } from '@playwright/test'
1515
* from CSS classes or DOM structure, making tests less brittle.
1616
*/
1717
export const VRT_MASK_SELECTORS = {
18-
liveHrValue: '[data-testid="live-hr-value"]',
19-
liveHrPercent: '[data-testid="live-hr-percent"]',
20-
hrTileGridItem: '[data-testid="hr-tile-grid-item"]',
18+
bpmPercent: '[data-testid="bpm-percent"]',
19+
bpmValue: '[data-testid="bpm-value"]',
20+
caloriesValue: '[data-testid="calories-value"]',
2121
timerCountdown: '[data-testid="timer-countdown"]',
2222
timerPhaseLabel: '[data-testid="timer-phase-label"]',
2323
hrTimeSeriesChart: '[data-testid="hr-time-series-chart"]',
@@ -32,8 +32,9 @@ export const VRT_MASK_SELECTORS = {
3232
*/
3333
export function getDynamicContentMasks(page: Page): Locator[] {
3434
return [
35-
page.locator(VRT_MASK_SELECTORS.liveHrValue),
36-
page.locator(VRT_MASK_SELECTORS.liveHrPercent),
35+
page.locator(VRT_MASK_SELECTORS.bpmPercent),
36+
page.locator(VRT_MASK_SELECTORS.bpmValue),
37+
page.locator(VRT_MASK_SELECTORS.caloriesValue),
3738
page.locator(VRT_MASK_SELECTORS.timerCountdown),
3839
page.locator(VRT_MASK_SELECTORS.timerPhaseLabel),
3940
page.locator(VRT_MASK_SELECTORS.hrTimeSeriesChart),
@@ -48,9 +49,9 @@ export function getDynamicContentMasks(page: Page): Locator[] {
4849
*/
4950
export function getHrMasks(page: Page): Locator[] {
5051
return [
51-
page.locator(VRT_MASK_SELECTORS.liveHrValue),
52-
page.locator(VRT_MASK_SELECTORS.liveHrPercent),
53-
page.locator(VRT_MASK_SELECTORS.hrTileGridItem),
52+
page.locator(VRT_MASK_SELECTORS.bpmPercent),
53+
page.locator(VRT_MASK_SELECTORS.bpmValue),
54+
page.locator(VRT_MASK_SELECTORS.caloriesValue),
5455
]
5556
}
5657

tests/playwright/perf-stability.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import { test, expect } from '@playwright/test'
22

3-
interface WindowWithTestFlags extends Window {
4-
__TEST_WEBSOCKET_READY__?: boolean
5-
}
6-
73
test.describe('WebSocket Stability', () => {
84
test('should maintain a stable WebSocket connection', async ({ page }) => {
95
test.setTimeout(40000)
106
await page.goto('/')
117

128
// Wait for the WebSocket connection to be established
139
await page.waitForFunction(
14-
() => (window as WindowWithTestFlags).__TEST_WEBSOCKET_READY__ === true,
10+
() => document.body.dataset.connectionStatus === 'connected',
1511
null,
1612
{
1713
timeout: 10000,

tests/playwright/vrt-dashboard.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,5 +167,14 @@ test.describe('Visual Regression Tests', () => {
167167
maxDiffPixelRatio: 0.3,
168168
})
169169
})
170+
171+
test('large desktop viewport', async () => {
172+
await dashboardPage.setViewportSize({ width: 2560, height: 1440 })
173+
const dashboard = dashboardPage.getByTestId('dashboard')
174+
await takeScreenshot(dashboard, 'dashboard-large-desktop.png', {
175+
mask: getDynamicContentMasks(dashboardPage),
176+
maxDiffPixelRatio: 0.1,
177+
})
178+
})
170179
})
171180
})
59.7 KB
Loading

tests/playwright/vrt-hr-components.spec.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type BrowserContext, type Page } from '@playwright/test'
1+
import { type BrowserContext, type Page, expect } from '@playwright/test'
22
import { test } from './fixtures'
33
import {
44
getDynamicContentMasks,
@@ -9,6 +9,7 @@ import {
99
} from './lib'
1010
import { takeScreenshot, assertFixedDimensions } from './lib/visual'
1111
import { waitForPageReady } from './lib/waits'
12+
import { HR_TILE_MIN_HEIGHT } from '../../constants/layout'
1213

1314
// Test suite configuration
1415
test.describe.configure({ mode: 'serial' })
@@ -61,17 +62,27 @@ test.describe('Visual Regression Tests', () => {
6162
})
6263

6364
test.describe('HR-Related Components', () => {
65+
// Reset devices after each test to prevent state pollution
66+
test.afterEach(async () => {
67+
await mockMultipleHrDevices(dashboardPage, [])
68+
await expect(dashboardPage.getByTestId('hr-tile-card')).toHaveCount(0)
69+
})
70+
6471
test('dashboard with HR data', async () => {
6572
await mockPage.getByLabel('Current BPM').fill('155')
6673
await mockPage.getByRole('button', { name: 'Zone 4' }).click()
6774

68-
// Wait for HR tile to appear on dashboard
75+
// Wait for HR tile to appear
76+
await expect(
77+
dashboardPage.getByTestId('hr-tile-card').first()
78+
).toBeVisible()
79+
80+
// Assert HR tile height is within limits
6981
const hrTile = dashboardPage.getByTestId('hr-tile-card').first()
70-
await hrTile.waitFor({ state: 'visible', timeout: 5000 })
7182

7283
// Assert HR tile height is within limits
7384
await assertFixedDimensions(hrTile, {
74-
minHeight: 180,
85+
minHeight: HR_TILE_MIN_HEIGHT,
7586
})
7687

7788
const dashboard = dashboardPage.getByTestId('dashboard')
@@ -106,12 +117,17 @@ test.describe('Visual Regression Tests', () => {
106117
},
107118
])
108119

109-
// Assert all HR tiles maintain dimensions
120+
// Wait for HR tiles to appear
110121
const hrTiles = dashboardPage.getByTestId('hr-tile-card')
122+
await expect(hrTiles).toHaveCount(2)
123+
await expect(hrTiles.first()).toBeVisible()
124+
await expect(hrTiles.nth(1)).toBeVisible()
125+
126+
// Assert all HR tiles maintain dimensions
111127
const count = await hrTiles.count()
112128
for (let i = 0; i < count; i++) {
113129
await assertFixedDimensions(hrTiles.nth(i), {
114-
minHeight: 180,
130+
minHeight: HR_TILE_MIN_HEIGHT,
115131
})
116132
}
117133

@@ -132,6 +148,11 @@ test.describe('Visual Regression Tests', () => {
132148
await mockPage.getByLabel('Current BPM').fill(String(60 + zone * 20))
133149
await mockPage.getByRole('button', { name: `Zone ${zone}` }).click()
134150

151+
// Wait for HR tile to appear
152+
await expect(
153+
dashboardPage.getByTestId('hr-tile-card').first()
154+
).toBeVisible()
155+
135156
const dashboard = dashboardPage.getByTestId('dashboard')
136157
await takeScreenshot(dashboard, `dashboard-hr-zone-${zone}.png`, {
137158
maxDiffPixelRatio: 0.1,
523 Bytes
Loading

0 commit comments

Comments
 (0)