Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4514493
fix(e2e): fix 8 failing tests in profile, analytics, and youtube-vide…
kiran-redhat Mar 26, 2026
c3d9e81
fix: lint issues
autofix-ci[bot] Mar 26, 2026
ec93e9f
fix(e2e): use visible UI label to locate Create Custom Journey button
kiran-redhat Mar 26, 2026
5fa240e
refactor(e2e): apply requirement-driven testing philosophy
kiran-redhat Mar 26, 2026
4beec47
fix(e2e): remove logout toast assertion — toast never exists in app
kiran-redhat Mar 26, 2026
cc37e2e
fix(e2e): increase T&C page timeout to handle Vercel cold starts
kiran-redhat Mar 26, 2026
5fd0811
fix(e2e): fix flaky tests and add timeout/wait-time standards
kiran-redhat Mar 26, 2026
34a5e44
docs(e2e-rules): ban hard waits, require soft waits only
kiran-redhat Mar 26, 2026
7d2e262
fix: lint issues
autofix-ci[bot] Mar 26, 2026
b0790b1
fix(e2e): pre-warm Vercel Lambdas and fix Share button timing
kiran-redhat Mar 26, 2026
983012e
refactor(e2e): enforce soft wait rules and simplify global setup
kiran-redhat Mar 26, 2026
1b3a8a3
refactor(e2e): enhance image handling and button interactions in card…
kiran-redhat Mar 30, 2026
5b01aa2
Merge branch 'main' of https://github.com/JesusFilm/core into cursor/…
kiran-redhat Mar 30, 2026
d4dfbc7
fix(onboarding): ensure lastActiveTeamId is persisted before redirect…
kiran-redhat Mar 30, 2026
1e5b3ae
fix(onboarding): ensure lastActiveTeamId is persisted before redirect…
kiran-redhat Mar 30, 2026
5d8b7bc
fix(register): enhance account registration process and improve page …
kiran-redhat Mar 30, 2026
3249931
fix: lint issues
autofix-ci[bot] Mar 30, 2026
55520f0
fix(onboarding): enhance journey duplication handling and improve tes…
kiran-redhat Mar 31, 2026
3805b19
Merge branch 'main' of https://github.com/JesusFilm/core into cursor/…
kiran-redhat Mar 31, 2026
022fbde
Merge branch 'main' into cursor/fix-e2e-failing-tests
kiran-redhat Mar 31, 2026
1fea2a4
Merge branch 'cursor/fix-e2e-failing-tests' of https://github.com/Jes…
kiran-redhat Mar 31, 2026
29a36f9
fix: lint issues
autofix-ci[bot] Mar 31, 2026
d2b3de5
refactor: improve element selection and error handling in e2e tests
kiran-redhat Apr 1, 2026
bc68e60
fix: lint issues
autofix-ci[bot] Apr 1, 2026
94fc5aa
fix: enhance YouTube URL validation and error handling in VideosSection
kiran-redhat Apr 1, 2026
21faf63
fix: lint issues
autofix-ci[bot] Apr 1, 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
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,11 @@ test.describe('verify card level actions', () => {
await cardLevelActionPage.clickBtnInAddBlockDrawer('Image') // clicking on image button in add block drawer
await cardLevelActionPage.clickSelectImageBtn() // clicking on select image buttom in image properties drawer
await cardLevelActionPage.clickImageSelectionTab('Custom') // clicking on custom tab in image drawer tab list
await cardLevelActionPage.getImageSrc() // getting current image source
await cardLevelActionPage.uploadImageInCustomTab() // uploading image in the custom tab
// await cardLevelActionPage.verifyImgUploadedSuccessMsg() // verifying the 'Upload successful' message
await cardLevelActionPage.verifyImageGotChanged() // verifying the image is updated in the custom tab
await cardLevelActionPage.verifyCustomImageUploaded() // verifying the Cloudflare-hosted image appears in the thumbnail
await cardLevelActionPage.clickImageSelectionTab('Gallery') // clicking on Gallery tab in image drawer tab list
await cardLevelActionPage.getImageSrc() // getting current image source
await cardLevelActionPage.clickImgFromFeatureOfGalleryTab() // selecting an image of Gallery tab in the image drawer
await cardLevelActionPage.verifyImageGotChanged() // verifying the seleted image is updated in the image drawer
await cardLevelActionPage.verifyGalleryImageSelected() // verifying the Unsplash gallery image appears in the thumbnail
await cardLevelActionPage.clickImgDeleteBtn() // deleting the selected image
await cardLevelActionPage.verifyImageIsDeleted() // verifying the image is deleted from the image drawer
})
Expand Down
4 changes: 2 additions & 2 deletions apps/journeys-admin-e2e/src/e2e/lighthouse/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const config = {
}
}

// Set test time out to 4 minutes as it has to run lighthouse audit
test.setTimeout(4 * 60 * 1000)
// Lighthouse + cold remote URL can exceed 4 minutes under parallel CI load
test.setTimeout(6 * 60 * 1000)

test('Homepage', async () => {
const browser = await chromium.launch({
Expand Down
85 changes: 53 additions & 32 deletions apps/journeys-admin-e2e/src/pages/card-level-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,21 @@ export class CardLevelActionPage {
}

async clickBtnInAddBlockDrawer(buttonName: string) {
const button = this.page.locator(
'div[data-testid="SettingsDrawer"] button',
{
hasText: buttonName
}
)
// Use the exact JourneysAdmin test-id for each block type to avoid ambiguous
// matches against other SettingsDrawer buttons that contain the same word
// (e.g. "Image Source" header in the background-image properties drawer).
const testIdMap: Record<string, string> = {
Image: 'JourneysAdminButtonNewImageButton',
Spacer: 'JourneysAdminButtonNewSpacerButton',
Video: 'JourneysAdminButtonNewVideoButton',
Typography: 'JourneysAdminButtonNewTypographyButton'
}
const testId = testIdMap[buttonName]
const button = testId
? this.page.locator(`div[data-testid="${testId}"] button`)
: this.page.locator('div[data-testid="SettingsDrawer"] button', {
hasText: buttonName
})
await button.click({ timeout: sixtySecondsTimeout })
}

Expand Down Expand Up @@ -238,39 +247,51 @@ export class CardLevelActionPage {
.setInputFiles(
require('path').join(__dirname, '../utils/testResource/Flower.jpg')
)
// Scope to the ImageLibrary Drawer to avoid matching the progress bar
// in the properties panel behind it.
const imageLibraryDrawer = this.page.locator(
'div[data-testid="SettingsDrawer"]',
{ has: this.page.locator('button[aria-label="close-image-library"]') }
)
await expect(
this.page.locator(
'div[data-testid="ImageBlockHeader"] div[data-testid="ImageBlockThumbnail"] span[role="progressbar"]'
imageLibraryDrawer.locator(
'div[data-testid="ImageBlockThumbnail"] span[role="progressbar"]'
)
).toBeHidden({ timeout: sixtySecondsTimeout })
}

async getImageSrc() {
if (
await this.page
.locator(
'div[data-testid="ImageSource"] + div div[data-testid="ImageBlockThumbnail"] img'
)
.isVisible()
) {
this.uploadedImgSrc = await this.page
.locator(
'div[data-testid="ImageSource"] + div div[data-testid="ImageBlockThumbnail"] img'
)
.getAttribute('src')
} else {
this.uploadedImgSrc = ''
}
private imageLibraryThumbnail() {
// The ImageLibrary Drawer is identified by its close button (aria-label hardcoded
// to "close-image-library" for all settings drawers). Use .first() because the
// background-image properties panel can also be open at the same time, creating a
// second matching drawer — both show the same img src, so first() is fine.
return this.page
.locator('div[data-testid="SettingsDrawer"]', {
has: this.page.locator('button[aria-label="close-image-library"]')
})
.locator('div[data-testid="ImageBlockThumbnail"] img')
.first()
}
Comment on lines +263 to 274
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the specific file and lines in question
wc -l apps/journeys-admin-e2e/src/pages/card-level-actions.ts

Repository: JesusFilm/core

Length of output: 1867


🏁 Script executed:

# Read the file content around lines 263-274
sed -n '260,280p' apps/journeys-admin-e2e/src/pages/card-level-actions.ts

Repository: JesusFilm/core

Length of output: 1056


🏁 Script executed:

# Search for ImageUpload data-testid usage in the codebase
rg 'ImageUpload' --type ts --type tsx -A 2 -B 2

Repository: JesusFilm/core

Length of output: 85


🏁 Script executed:

# Search for ImageUpload in the codebase more broadly
rg 'ImageUpload' -A 2 -B 2 | head -100

Repository: JesusFilm/core

Length of output: 8448


🏁 Script executed:

# Search for background-image properties panel or settings drawer contexts
rg 'background-image|SettingsDrawer' -A 1 -B 1 | head -100

Repository: JesusFilm/core

Length of output: 8587


🏁 Script executed:

# Check the drawer structure and context to understand the visual hierarchy
rg 'data-testid="SettingsDrawer"' -A 5 -B 5

Repository: JesusFilm/core

Length of output: 10048


🏁 Script executed:

# Search specifically for ImageUpload data-testid in UI components
rg 'data-testid.*ImageUpload|ImageUpload.*data-testid' -A 2 -B 2

Repository: JesusFilm/core

Length of output: 1725


🏁 Script executed:

# Search for ImageBlockThumbnail to understand the structure
rg 'ImageBlockThumbnail' -A 3 -B 3

Repository: JesusFilm/core

Length of output: 30540


🏁 Script executed:

# Look for image library drawer or image upload related components
rg 'image.*library|library.*image' -i --type ts --type tsx -A 2 -B 2 | head -80

Repository: JesusFilm/core

Length of output: 85


Don't use .first() to mask ambiguous drawer matches

The locator matches two drawers when both image library and background-image properties panels are open. Disambiguate using ImageUpload component (unique to the image library drawer) instead of .first().

♻️ Suggested change
-  private imageLibraryThumbnail() {
-    // The ImageLibrary Drawer is identified by its close button (aria-label hardcoded
-    // to "close-image-library" for all settings drawers). Use .first() because the
-    // background-image properties panel can also be open at the same time, creating a
-    // second matching drawer — both show the same img src, so first() is fine.
-    return this.page
-      .locator('div[data-testid="SettingsDrawer"]', {
-        has: this.page.locator('button[aria-label="close-image-library"]')
-      })
-      .locator('div[data-testid="ImageBlockThumbnail"] img')
-      .first()
-  }
+  private imageLibraryThumbnail() {
+    const imageLibraryDrawer = this.page.locator('div[data-testid="SettingsDrawer"]', {
+      has: this.page.locator('div[data-testid="ImageUpload"]')
+    })
+    return imageLibraryDrawer.locator('div[data-testid="ImageBlockThumbnail"] img')
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private imageLibraryThumbnail() {
// The ImageLibrary Drawer is identified by its close button (aria-label hardcoded
// to "close-image-library" for all settings drawers). Use .first() because the
// background-image properties panel can also be open at the same time, creating a
// second matching drawer — both show the same img src, so first() is fine.
return this.page
.locator('div[data-testid="SettingsDrawer"]', {
has: this.page.locator('button[aria-label="close-image-library"]')
})
.locator('div[data-testid="ImageBlockThumbnail"] img')
.first()
}
private imageLibraryThumbnail() {
const imageLibraryDrawer = this.page.locator('div[data-testid="SettingsDrawer"]', {
has: this.page.locator('div[data-testid="ImageUpload"]')
})
return imageLibraryDrawer.locator('div[data-testid="ImageBlockThumbnail"] img')
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/journeys-admin-e2e/src/pages/card-level-actions.ts` around lines 263 -
274, The locator in imageLibraryThumbnail currently uses .first() to pick
between two SettingsDrawer matches; instead narrow the SettingsDrawer by
requiring the ImageLibrary-specific component: when locating
'div[data-testid="SettingsDrawer"]' (which already uses
button[aria-label="close-image-library"]), add has:
this.page.locator('div[data-testid="ImageUpload"]') to disambiguate, then select
'div[data-testid="ImageBlockThumbnail"] img' (remove .first()). This replaces
the brittle .first() approach with a precise check for the ImageUpload component
inside imageLibraryThumbnail.


async verifyImageGotChanged() {
await expect(
this.page.locator(
'div[data-testid="ImageSource"] + div div[data-testid="ImageBlockThumbnail"] img'
)
).not.toHaveAttribute('src', this.uploadedImgSrc, {
timeout: sixtySecondsTimeout
})
async verifyCustomImageUploaded() {
// After a custom file upload, Apollo optimistically sets the src to a
// Cloudflare imagedelivery.net URL. Wait up to 60s for the img to appear
// with that src (upload + Apollo update can be slow on cold Vercel).
await expect(this.imageLibraryThumbnail()).toHaveAttribute(
'src',
/imagedelivery\.net/,
{ timeout: sixtySecondsTimeout }
)
}

async verifyGalleryImageSelected() {
// After selecting an Unsplash gallery image the src always contains
// images.unsplash.com. Wait up to 60s for the img to reflect that.
await expect(this.imageLibraryThumbnail()).toHaveAttribute(
'src',
/images\.unsplash\.com/,
{ timeout: sixtySecondsTimeout }
)
}

async verifyImgUploadedSuccessMsg() {
Expand Down
36 changes: 28 additions & 8 deletions apps/journeys-admin-e2e/src/pages/customization-media-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ export class CustomizationMediaPage {
}

async pasteYouTubeUrl(url: string): Promise<void> {
const input = this.page
.getByTestId('VideosSection-youtube-input')
.locator('input')
await input.fill(url)
await this.page.getByRole('textbox', { name: 'YouTube URL' }).fill(url)
}

async waitForAutoSubmit(): Promise<void> {
Expand All @@ -59,11 +56,34 @@ export class CustomizationMediaPage {
await this.page.waitForLoadState('load')
}

async waitForAutoSubmitError(): Promise<void> {
const errorText = this.page
/**
* VideosSection validates via a debounced useEffect (~800ms) on URL change,
* not on blur. Waits until helper text shows the error (see VideosSection.tsx).
*/
async waitForAutoSubmitError(
expectedInputValue = 'not-a-valid-url'
): Promise<void> {
const input = this.page.getByRole('textbox', { name: 'YouTube URL' })
const videosSection = this.page.getByTestId('VideosSection')
const helper = this.page
.getByTestId('VideosSection-youtube-input')
.locator('p.MuiFormHelperText-root.Mui-error')
await expect(errorText).toBeVisible({ timeout: 90000 })
.locator('.MuiFormHelperText-root')

await expect
.poll(
async () =>
await videosSection.locator('.MuiCircularProgress-root').count(),
{ timeout: defaultTimeout, intervals: [400, 800, 1200] }
)
.toBe(0)

await expect(input).toHaveValue(expectedInputValue, {
timeout: defaultTimeout
})

await expect(helper).toHaveText('Please enter a valid YouTube URL', {
timeout: 90000
})
}

async verifyVideosSectionVisible(): Promise<void> {
Expand Down
8 changes: 4 additions & 4 deletions apps/journeys-admin-e2e/src/pages/journey-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import testData from '../utils/testData.json'
let journeyName = ''
const thirtySecondsTimeout = 30000
const sixtySecondsTimeout = 60000
const ninetySecondsTimeout = 90000
// eslint-disable-next-line no-undef
const downloadFolderPath = path.join(__dirname, '../utils/download/')

Expand Down Expand Up @@ -243,8 +244,7 @@ export class JourneyPage {
const createButton = this.page.getByRole('button', {
name: 'Create Custom Journey'
})
// 90s: cold Vercel SSR + TeamProvider Apollo query can take time on first load
await expect(createButton).toBeEnabled({ timeout: 90000 })
await expect(createButton).toBeEnabled({ timeout: ninetySecondsTimeout })
await createButton.click()
const journeyImageLoader = this.page.locator(
'div[data-testid="JourneysAdminImageThumbnail"] span[class*="MuiCircularProgress"]'
Expand All @@ -269,7 +269,7 @@ export class JourneyPage {
'div[data-testid="CardWrapper"] div[data-testid*="SelectableWrapper"] h3[data-testid="JourneysTypography"]'
)
.first()
.click({ timeout: sixtySecondsTimeout, delay: 1000 })
.click({ timeout: ninetySecondsTimeout, delay: 1000 })
for (let clickRetry = 0; clickRetry < 5; clickRetry++) {
if (
await this.page
Expand Down Expand Up @@ -346,7 +346,7 @@ export class JourneyPage {
this.page.locator(this.journeyNamePath, {
hasText: journeyName
})
).toBeVisible({ timeout: thirtySecondsTimeout })
).toBeVisible({ timeout: sixtySecondsTimeout })
}

async clickOnTheCreatedCustomJourney() {
Expand Down
101 changes: 95 additions & 6 deletions apps/journeys-admin-e2e/src/pages/login-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,26 @@ export class LoginPage {
}

async fillExistingPassword(password: string): Promise<void> {
await this.page.getByPlaceholder('Enter Password').fill(password)
await this.page
.getByPlaceholder('Enter Password')
.fill(password, { timeout: sixtySecondsTimeout })
}

async clickSubmitButton(): Promise<void> {
await this.page.locator('button[type="submit"]').click()
}

async waitUntilDiscoverPageLoaded() {
// 90s: cold Vercel SSR + TeamProvider Apollo query can take >65s on first run
// 90s: cold Vercel SSR + TeamProvider Apollo query can take >65s on first run.
// Projects navigation link is part of the app shell and loads before team data.
await expect(this.getProjectsNavItem()).toBeVisible({ timeout: 90000 })

// TeamSelect is disabled={query.loading} while the teams Apollo query is in
// flight (see TeamSelect.tsx). Enabled means the query resolved and the page
// is fully interactive — regardless of which team (or Shared With Me) is active.
await expect(
this.page.getByRole('button', { name: 'Create Custom Journey' })
).toBeEnabled({ timeout: 90000 })
this.page.getByTestId('TeamSelect').locator('[aria-haspopup="listbox"]')
).toBeEnabled({ timeout: 30000 })
}

async login(accountKey: string = 'admin'): Promise<void> {
Expand All @@ -46,13 +54,94 @@ export class LoginPage {
await this.waitUntilDiscoverPageLoaded()
}

async logInWithCreatedNewUser(userName: string) {
async logInWithCreatedNewUser(userName: string, expectedTeamTitle?: string) {
await this.fillExistingEmail(userName)
console.log(`userName : ${userName}`)
await this.clickSubmitButton()
const password = await getPassword()
await this.fillExistingPassword(password)
await this.clickSubmitButton()
await this.waitUntilDiscoverPageLoaded()
await this.waitUntilNewUserDiscoverPageLoaded(expectedTeamTitle)
}

private async getTeamSelectCombobox() {
return this.page.getByTestId('TeamSelect').locator('[role="combobox"]')
}

private getProjectsNavItem() {
return this.page
.getByRole('link', { name: /Projects/i })
.or(this.page.getByTestId('NavigationListItemProjects'))
}

private getTeamSelectTrigger() {
return this.page
.getByTestId('TeamSelect')
.locator('[aria-haspopup="listbox"]')
}

private async selectFirstAuthoringTeamIfNeeded(): Promise<void> {
const teamSelect = await this.getTeamSelectCombobox()
if (!(await teamSelect.innerText()).includes('Shared With Me')) return

const trigger = this.getTeamSelectTrigger()
await trigger.click()
const authoringTeamOption = this.page
.locator('ul[role="listbox"] li[role="option"]', {
hasNotText: 'Shared With Me'
})
.first()

try {
await expect(authoringTeamOption).toBeVisible({ timeout: 10000 })
await authoringTeamOption.click()
await expect(teamSelect).not.toContainText('Shared With Me', {
timeout: 30000
})
} catch {
await this.page.keyboard.press('Escape')
}
}

async assertSharedWithMeDiscoverState() {
const teamSelect = await this.getTeamSelectCombobox()
await expect(teamSelect).toContainText('Shared With Me', { timeout: 90000 })
await expect(
this.page.getByRole('button', { name: 'Create Custom Journey' })
).toHaveCount(0)
}

async assertCreatedTeamDiscoverState(expectedTeamTitle?: string) {
const teamSelect = await this.getTeamSelectCombobox()
for (let attempt = 0; attempt < 3; attempt++) {
await this.selectFirstAuthoringTeamIfNeeded()
if ((await teamSelect.innerText()).includes('Shared With Me')) {
if (attempt === 2) break
await this.page.reload()
await expect(this.getProjectsNavItem()).toBeVisible({ timeout: 90000 })
await expect(this.getTeamSelectTrigger()).toBeEnabled({
timeout: 90000
})
continue
}
break
}
await expect(teamSelect).not.toContainText('Shared With Me', {
timeout: 90000
})
if (expectedTeamTitle != null && expectedTeamTitle.trim() !== '') {
await expect(teamSelect).toContainText(expectedTeamTitle)
}
await expect(
this.page.getByRole('button', { name: 'Create Custom Journey' })
).toBeEnabled({ timeout: 90000 })
}

private async waitUntilNewUserDiscoverPageLoaded(expectedTeamTitle?: string) {
await expect(this.getProjectsNavItem()).toBeVisible({ timeout: 90000 })

await expect(this.getTeamSelectTrigger()).toBeEnabled({ timeout: 90000 })

await this.assertCreatedTeamDiscoverState(expectedTeamTitle)
}
}
Loading
Loading