Skip to content

Commit 4685e8a

Browse files
Merge branch '00-00-MA-chore-modern-override-cleanup' (PR #8915) into stage
2 parents 5fabadd + 7e59afe commit 4685e8a

File tree

8 files changed

+96
-111
lines changed

8 files changed

+96
-111
lines changed

.cursor/rules/e2e-testing.mdc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
description: Requirement-driven e2e test writing — no defensive code
3+
globs: apps/**/*e2e*/**/*.ts
4+
alwaysApply: false
5+
---
6+
7+
# E2E Testing Standards
8+
9+
Write tests that reflect exact user requirements. A failing test means a real bug — never hide it.
10+
11+
## Reference elements by the label the user sees in the UI
12+
13+
- GOOD: `getByRole('heading', { name: 'Create Custom Journey' })`
14+
- GOOD: `getByRole('link', { name: 'Analytics' })`
15+
- GOOD: `getByPlaceholder('Paste a YouTube link...')`
16+
- BAD: `locator('div[data-testid="JourneysAdminContainedIconButton"]')`
17+
- BAD: `locator('.MuiCardActionArea-root')`
18+
19+
## Assert exactly what the user should see — no fallbacks
20+
21+
If a required element is not visible, the test must FAIL and surface the bug.
22+
23+
- GOOD: `await expect(page.getByRole('heading', { name: 'Create Custom Journey' })).toBeVisible()`
24+
- BAD: check visibility first and only assert if it happens to be present
25+
- BAD: loop over multiple fallback CSS selectors
26+
- BAD: `try/catch` that swallows an assertion failure
27+
28+
## Always use soft waits — never hard waits
29+
30+
ALL waiting must go through Playwright's built-in auto-waiting. There are zero exceptions in test or page-object code.
31+
32+
Hard waits are banned because they pause unconditionally — they make tests slow on fast machines and still flaky on slow ones. Soft waits retry until the condition is true or the timeout expires.
33+
34+
- GOOD: `await expect(button).toBeEnabled()` — retries until enabled
35+
- GOOD: `await expect(dialog).toBeVisible()` — retries until visible
36+
- GOOD: `await expect(locator).toBeHidden()` — retries until gone
37+
- GOOD: `await page.goto(url)` — Playwright waits for navigation internally
38+
- BAD: `await page.waitForTimeout(2000)` — unconditional pause
39+
- BAD: `await new Promise(resolve => setTimeout(resolve, 1000))` — unconditional pause
40+
- BAD: `sleep(n)` in any form
41+
- BAD: polling loops with `setTimeout` inside tests or page objects
42+
43+
## Wait timeouts must be short, justified, and commented
44+
45+
The default timeout (30s) is enough for stable app states. Only exceed it for known slow operations (cold Vercel SSR, Apollo queries on first load, multi-step server chains). Hard limit is 90s — never set a timeout above 90000ms.
46+
47+
When a timeout exceeds the default, ALWAYS add an inline comment explaining why:
48+
49+
```typescript
50+
// 90s: cold Vercel SSR + TeamProvider Apollo query can take >65s on first run
51+
await expect(createButton).toBeEnabled({ timeout: 90000 })
52+
```
53+
54+
- BAD: `{ timeout: 150000 }` with no comment
55+
- BAD: `{ timeout: 60000 }` as the only value tried when it keeps failing — raise it to 90s and comment
56+
- GOOD: `{ timeout: 30000 }` (default, no comment needed)
57+
- GOOD: `{ timeout: 90000 }` with an inline comment explaining the slow operation
58+
59+
## Scope selectors to the user's visual context
60+
61+
Use the narrowest container that reflects what the user sees. If strict mode fires (multiple matches), that is a real data/state problem — fix the root cause, don't add `.first()`.
62+
63+
- GOOD: `page.locator('div[aria-label="journey-card"]', { hasText: journeyName }).getByRole('link', { name: 'Analytics' })`
64+
- BAD: `page.locator('div[data-testid="AnalyticsItem"] a').first()`

apps/journeys-admin-e2e/src/e2e/customization/youtube-video.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect, test } from '../../fixtures/authenticated'
22
import { CustomizationMediaPage } from '../../pages/customization-media-page'
33

4-
const TEMPLATE_ID = '8d4c24c3-5fe0-428d-b221-af9e46975933'
4+
const TEMPLATE_ID = '00dc45d7-9d37-434e-bbc8-7c89eeb6229a'
55
const YOUTUBE_URL =
66
'https://www.youtube.com/watch?v=JHdB1dYAteA&pp=ygUKam9obiBwaXBlcg%3D%3D'
77

apps/journeys-admin-e2e/src/e2e/profile/profile.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ test.describe('verify profile page functionalities', () => {
4343
const profilePage = new ProfilePage(page)
4444
await profilePage.clickProfileIconInNavBar() // clicking the profile icon in navigation list Item
4545
await profilePage.clickLogout() // clicking the logout button
46-
await profilePage.verifyLogoutToastMsg() // verifying the toast message
4746
await profilePage.verifyloggedOut() // verifying the user is logged out and the login page is displayed
4847
})
4948

apps/journeys-admin-e2e/src/pages/customization-media-page.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export class CustomizationMediaPage {
1818
}
1919

2020
async clickNextButton(): Promise<void> {
21-
await this.page
22-
.getByTestId('CustomizeFlowNextButton')
23-
.click({ timeout: defaultTimeout })
21+
const nextButton = this.page.getByTestId('CustomizeFlowNextButton')
22+
await expect(nextButton).toBeEnabled({ timeout: defaultTimeout })
23+
await nextButton.click({ timeout: defaultTimeout })
2424
}
2525

2626
async navigateToMediaScreen(): Promise<void> {
@@ -62,8 +62,8 @@ export class CustomizationMediaPage {
6262
async waitForAutoSubmitError(): Promise<void> {
6363
const errorText = this.page
6464
.getByTestId('VideosSection-youtube-input')
65-
.locator('.Mui-error, .MuiFormHelperText-root.Mui-error')
66-
await expect(errorText).toBeVisible({ timeout: defaultTimeout })
65+
.locator('p.MuiFormHelperText-root.Mui-error')
66+
await expect(errorText).toBeVisible({ timeout: 90000 })
6767
}
6868

6969
async verifyVideosSectionVisible(): Promise<void> {

apps/journeys-admin-e2e/src/pages/journey-page.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -240,32 +240,18 @@ export class JourneyPage {
240240
}
241241

242242
async clickCreateCustomJourney(): Promise<void> {
243-
const createJourneyLoaderPath = this.page.locator(
243+
const createButton = this.page.getByRole('button', {
244+
name: 'Create Custom Journey'
245+
})
246+
// 90s: cold Vercel SSR + TeamProvider Apollo query can take time on first load
247+
await expect(createButton).toBeEnabled({ timeout: 90000 })
248+
await createButton.click()
249+
const journeyImageLoader = this.page.locator(
244250
'div[data-testid="JourneysAdminImageThumbnail"] span[class*="MuiCircularProgress"]'
245251
)
246-
await this.page
247-
.locator('div[data-testid="JourneysAdminContainedIconButton"] button')
248-
.waitFor({ state: 'visible', timeout: 150000 })
249-
await expect(
250-
this.page.locator(
251-
'div[data-testid="JourneysAdminContainedIconButton"] button'
252-
)
253-
).toBeVisible({ timeout: 150000 })
254-
await expect(createJourneyLoaderPath).toBeHidden({ timeout: 18000 })
255-
await this.page
256-
.locator('div[data-testid="JourneysAdminContainedIconButton"] button')
257-
.click()
258-
try {
259-
await expect(createJourneyLoaderPath, 'Ignore if not found').toBeVisible({
260-
timeout: 5000
261-
})
262-
} catch {
263-
// Ignore if not found
264-
}
265-
await expect(createJourneyLoaderPath).toBeHidden({
252+
await expect(journeyImageLoader).toBeHidden({
266253
timeout: sixtySecondsTimeout
267254
})
268-
//await this.page.waitForLoadState('networkidle')
269255
}
270256

271257
async setJourneyName(journey: string) {
@@ -1075,7 +1061,12 @@ export class JourneyPage {
10751061
}
10761062

10771063
async clickAnalyticsIconInCustomJourneyPage() {
1078-
await this.page.locator('div[data-testid="AnalyticsItem"] a').click()
1064+
await this.page
1065+
.locator('div[aria-label="journey-card"]', {
1066+
hasText: this.existingJourneyName
1067+
})
1068+
.locator('div[data-testid="AnalyticsItem"] a')
1069+
.click()
10791070
}
10801071

10811072
async verifyAnalyticsPageNavigation() {
@@ -1165,10 +1156,12 @@ export class JourneyPage {
11651156
}
11661157

11671158
async clickShareButtonInJourneyPage() {
1168-
await this.page
1159+
const shareButton = this.page
11691160
.locator('div[data-testid="ShareItem"]')
11701161
.getByRole('button', { name: 'Share' })
1171-
.click()
1162+
// 90s: cold Vercel SSR can delay journey page render after navigation from card click
1163+
await expect(shareButton).toBeVisible({ timeout: 90000 })
1164+
await shareButton.click()
11721165
}
11731166

11741167
async clickCopyIconInShareDialog() {

apps/journeys-admin-e2e/src/pages/login-page.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,12 @@ export class LoginPage {
3030
}
3131

3232
async waitUntilDiscoverPageLoaded() {
33+
// 90s: cold Vercel SSR + TeamProvider Apollo query can take >65s on first run
3334
await expect(
34-
this.page.locator('div[data-testid="JourneysAdminContainedIconButton"]')
35-
).toBeVisible({ timeout: 65000 })
35+
this.page.getByRole('button', { name: 'Create Custom Journey' })
36+
).toBeEnabled({ timeout: 90000 })
3637
}
3738

38-
// async verifyCreateCustomJourneyBtn() {
39-
// await expect(this.page.locator('div[aria-haspopup="listbox"]')).toBeVisible({ timeout: 60000 })
40-
// await this.page.reload({ waitUntil: 'domcontentloaded', timeout: 120000 })
41-
// // verifying 'Create custom journey' button is display. if not, then select first team in the catch block to display 'Create custom journey' button.
42-
// await expect(this.page.locator('div[data-testid="JourneysAdminContainedIconButton"] button')).toBeVisible().catch(async () => {
43-
// await this.selectFirstTeam()
44-
// })
45-
// // verifying whether the 'Shared With Me' option is selected or. if it is, then select first team in the catch block to display 'Create custom journey' button.
46-
// await expect(this.page.locator('div[aria-haspopup="listbox"]', { hasText: 'Shared With Me' })).toBeHidden().catch(async () => {
47-
// await this.selectFirstTeam()
48-
// })
49-
50-
// }
51-
// async selectFirstTeam() {
52-
// await this.page.locator('div[aria-haspopup="listbox"]').click({ timeout: 60000 })
53-
// await this.page.locator('ul[role="listbox"] li[role="option"]').first().click()
54-
// }
55-
5639
async login(accountKey: string = 'admin'): Promise<void> {
5740
const email = await getEmail(accountKey)
5841
await this.fillExistingEmail(email)

apps/journeys-admin-e2e/src/pages/profile-page.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,6 @@ export class ProfilePage {
9696
.click()
9797
}
9898

99-
async verifyLogoutToastMsg() {
100-
await expect(
101-
this.page.locator('#notistack-snackbar', { hasText: 'Logout successful' })
102-
).toBeVisible()
103-
await expect(
104-
this.page.locator('#notistack-snackbar', { hasText: 'Logout successful' })
105-
).toBeHidden({ timeout: 30000 })
106-
}
107-
10899
async verifyloggedOut() {
109100
await expect(this.page.locator('input#username')).toBeVisible({
110101
timeout: 30000

apps/journeys-admin-e2e/src/pages/register-Page.ts

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import testData from '../utils/testData.json'
88

99
let randomNumber = ''
1010
const thirtySecondsTimeout = 30000
11-
const seventySecondsTimeout = 70000
12-
1311
export class Register {
1412
readonly page: Page
1513
name: string
@@ -106,7 +104,7 @@ export class Register {
106104
'div[data-testid="JourneysAdminOnboardingPageWrapper"]',
107105
{ hasText: 'Terms and Conditions' }
108106
)
109-
).toBeVisible({ timeout: 60000 })
107+
).toBeVisible({ timeout: 90000 })
110108
}
111109

112110
async clickIAgreeBtn() {
@@ -162,53 +160,10 @@ export class Register {
162160
}
163161

164162
async waitUntilDiscoverPageLoaded() {
165-
// Wait for page navigation to complete
166-
await this.page.waitForLoadState('domcontentloaded', { timeout: 30000 })
167-
168-
// Try multiple selectors for different MUI versions and component structures
169-
const selectors = [
170-
// Primary data-testid selectors
171-
'div[data-testid="JourneysAdminContainedIconButton"]',
172-
'[data-testid="JourneysAdminContainedIconButton"]',
173-
174-
// With nested elements
175-
'div[data-testid="JourneysAdminContainedIconButton"] button',
176-
'[data-testid="JourneysAdminContainedIconButton"] button',
177-
'div[data-testid="JourneysAdminContainedIconButton"] [role="button"]',
178-
179-
// CardActionArea based (MUI Card structure)
180-
'div[data-testid="JourneysAdminContainedIconButton"] .MuiCardActionArea-root',
181-
'[data-testid="JourneysAdminContainedIconButton"] .MuiButtonBase-root',
182-
183-
// Fallback to any clickable element with the testid
184-
'[data-testid*="ContainedIconButton"]',
185-
'div[data-testid*="ContainedIconButton"]'
186-
]
187-
188-
let found = false
189-
for (const selector of selectors) {
190-
try {
191-
await expect(this.page.locator(selector)).toBeVisible({
192-
timeout: 3000
193-
})
194-
found = true
195-
break
196-
} catch (error) {
197-
continue
198-
}
199-
}
200-
201-
if (!found) {
202-
// Get all elements with data-testid for debugging
203-
const allTestIds = await this.page.$$eval(
204-
'[data-testid]',
205-
(elements) => elements.length
206-
)
207-
208-
throw new Error(
209-
`ContainedIconButton not found. Found ${allTestIds} elements with data-testid on the page`
210-
)
211-
}
163+
// 90s: cold Vercel SSR + TeamProvider Apollo query can take >70s on first run
164+
await expect(
165+
this.page.getByRole('button', { name: 'Create Custom Journey' })
166+
).toBeEnabled({ timeout: 90000 })
212167
}
213168

214169
async waitUntilTheToestMsgDisappear() {

0 commit comments

Comments
 (0)