Skip to content
Open
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
Binary file added artifacts/login-2fa.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/login-email.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/login-password.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/login-sso.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 15 additions & 25 deletions playwright/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,47 @@
import type { Page } from '@playwright/test'
import { expect, test } from '../support/commands'

test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login/')
})

async function continueToPasswordStep(page: Page, email: string) {
await page.fill('[data-test="email"]', email)
await page.click('[data-test="continue"]')
await page.waitForSelector('[data-test="password"]')
}

test('should show loading state during domain check', async ({ page }) => {
await page.route('**/private/sso/check-domain', async (route) => {
await new Promise(resolve => setTimeout(resolve, 500))
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ has_sso: false }),
})
})

await page.fill('[data-test="email"]', 'test@capgo.app')
await page.click('[data-test="continue"]')
await expect(page.locator('[data-test="loading"]')).toBeVisible()
await expect(page.locator('[data-test="password"]')).toBeVisible()
test('should keep the password field in the form but hidden until the domain is known', async ({ page }) => {
await expect(page.locator('[data-test="email"]')).toBeVisible()
await expect(page.locator('[data-test="submit"]')).toBeHidden()
await expect(page.locator('[data-test="sso-login"]')).toHaveCount(0)
await expect(page.locator('[data-test="password"]')).toHaveCount(1)
await expect(page.locator('[data-password-ready="false"]')).toHaveCount(1)
await expect(page.locator('input[autocomplete="one-time-code"]')).toHaveCount(1)
})

test('should show error for invalid credentials', async ({ page }) => {
await continueToPasswordStep(page, 'wrong@example.com')
await page.fill('[data-test="email"]', 'wrong@example.com')
await expect(page.locator('[data-test="submit"]')).toBeVisible({ timeout: 10000 })
await page.fill('[data-test="password"]', 'wrongpass')
await page.click('[data-test="submit"]')
await expect(page.locator('[data-test="form-error"]')).toContainText('Invalid login credentials')
})

test('should show error for deleted account', async ({ page }) => {
await continueToPasswordStep(page, 'deleted@capgo.app')
await page.fill('[data-test="email"]', 'deleted@capgo.app')
await expect(page.locator('[data-test="submit"]')).toBeVisible({ timeout: 10000 })
await page.fill('[data-test="password"]', 'password')
await page.click('[data-test="submit"]')
await expect(page.locator('[data-test="form-error"]')).toContainText('Account with this email used to exist, cannot recreate')
})

test('should login successfully and redirect', async ({ page }) => {
await continueToPasswordStep(page, 'test@capgo.app')
await page.fill('[data-test="email"]', 'test@capgo.app')
await expect(page.locator('[data-test="submit"]')).toBeVisible({ timeout: 10000 })
await page.fill('[data-test="password"]', 'testtest')
await page.click('[data-test="submit"]')
await page.waitForURL(/\/(apps|dashboard)(\/|$)/)
})

test('should keep email when navigating to forgot password page', async ({ page }) => {
const email = 'test@capgo.app'
await continueToPasswordStep(page, email)
await page.fill('[data-test="email"]', email)
await expect(page.locator('[data-test="forgot-password"]')).toBeVisible({ timeout: 10000 })
await page.click('[data-test="forgot-password"]')
await expect(page).toHaveURL('/forgot_password')
await expect(page.locator('[data-test="email"]')).toHaveValue(email)
Expand Down
2 changes: 1 addition & 1 deletion playwright/e2e/register.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ test.describe('Registration', () => {
await page.click('[data-test="onboarding-logout"]')

await page.waitForURL(/\/login\/?$/)
await expectProtectedRouteRedirect(page, '/apps', /\/login/, '[data-test="continue"]')
await expectProtectedRouteRedirect(page, '/apps', /\/login/, '[data-test="email"]')
})

test('should show error for existing email', async ({ page }) => {
Expand Down
87 changes: 44 additions & 43 deletions playwright/e2e/sso-login.spec.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,65 @@
import { expect, test } from '../support/commands'

test.describe('SSO Login Flow (Two-Step)', () => {
test.describe('SSO Login Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login/')
})

test('should show email step first (Step 1)', async ({ page }) => {
test('should show email only on first paint', async ({ page }) => {
await expect(page.locator('[data-test="email"]')).toBeVisible()
await expect(page.locator('[data-test="continue"]')).toBeVisible()

await expect(page.locator('[data-test="password"]')).not.toBeVisible()
await expect(page.locator('[data-test="submit"]')).not.toBeVisible()
await expect(page.locator('[data-test="submit"]')).toBeHidden()
await expect(page.locator('[data-test="sso-login"]')).toHaveCount(0)
await expect(page.locator('[data-test="password"]')).toHaveCount(1)
await expect(page.locator('[data-password-ready="false"]')).toHaveCount(1)
})

test('should show password field for non-SSO domain after Continue (Step 2)', async ({ page }) => {
await page.fill('[data-test="email"]', 'test@example.com')
await page.click('[data-test="continue"]')
test('should reveal password for non-SSO domains', async ({ page }) => {
await page.route('**/private/sso/check-domain', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ has_sso: false }),
})
})

await expect(page.locator('[data-test="password"]')).toBeVisible({ timeout: 10000 })
const domainCheck = page.waitForResponse(response => response.url().includes('/private/sso/check-domain'))
await page.fill('[data-test="email"]', 'test@example.com')
Comment thread
riderx marked this conversation as resolved.
await domainCheck
await expect(page.locator('[data-password-ready="true"]')).toHaveCount(1)
await expect(page.locator('[data-test="password"]')).toBeVisible()
await expect(page.locator('[data-test="submit"]')).toBeVisible()

await expect(page.locator('[data-test="continue"]')).not.toBeVisible()
await expect(page.locator('[data-test="sso-login"]')).toHaveCount(0)
})

test('should return to Step 1 when clicking Back from password step', async ({ page }) => {
await page.fill('[data-test="email"]', 'test@example.com')
await page.click('[data-test="continue"]')

await expect(page.locator('[data-test="password"]')).toBeVisible({ timeout: 10000 })

await page.locator('[data-test="back-to-email"]').click()

await expect(page.locator('[data-test="email"]')).toBeVisible()
await expect(page.locator('[data-test="continue"]')).toBeVisible()
await expect(page.locator('[data-test="password"]')).not.toBeVisible()
test('should use SSO only when the domain has SSO', async ({ page }) => {
await page.route('**/private/sso/check-domain', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ has_sso: true, enforce_sso: false }),
})
})

await page.fill('[data-test="email"]', 'user@sso.example')
await expect(page.locator('[data-test="sso-login"]')).toBeVisible({ timeout: 10000 })
await expect(page.locator('[data-password-ready="false"]')).toHaveCount(1)
await expect(page.locator('[data-test="submit"]')).toBeHidden()
})
Comment thread
riderx marked this conversation as resolved.

test('should keep Back visible and tappable on mobile password step', async ({ page }) => {
test('should keep email editable on mobile when the domain has SSO', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 })
await page.route('**/private/sso/check-domain', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ has_sso: true }),
})
})

const longEmail = 'avery.long.email.address.with-many-segments@very-long-example-domain-for-mobile-testing.example.com'
await page.fill('[data-test="email"]', longEmail)
await page.click('[data-test="continue"]')

await expect(page.locator('[data-test="password"]')).toBeVisible({ timeout: 10000 })

const backButton = page.locator('[data-test="back-to-email"]')
await expect(backButton).toBeVisible()

const selectedEmail = page.locator('[data-test="selected-email"]')
await expect(selectedEmail).toHaveText(longEmail)

const buttonBox = await backButton.boundingBox()
const emailBox = await selectedEmail.boundingBox()
expect(buttonBox?.height).toBeGreaterThanOrEqual(44)
expect(emailBox?.y).toBeGreaterThan((buttonBox?.y ?? 0) + (buttonBox?.height ?? 0))
expect((emailBox?.x ?? 0) + (emailBox?.width ?? 0)).toBeLessThanOrEqual(375)
expect(emailBox?.height).toBeGreaterThan(32)

await backButton.click()
await expect(page.locator('[data-test="email"]')).toBeFocused()
await expect(page.locator('[data-test="sso-login"]')).toBeVisible({ timeout: 10000 })
await expect(page.locator('[data-test="email"]')).toHaveValue(longEmail)
await expect(page.locator('[data-test="email"]')).toBeEditable()
})
})
3 changes: 1 addition & 2 deletions playwright/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const test = base.extend({
page.login = async (email: string, password: string, targetUrl = /\/(apps|dashboard)(\/|$)/) => {
await page.goto('/login/')
await page.fill('[data-test="email"]', email)
await page.click('[data-test="continue"]')
await page.waitForSelector('[data-test="password"]')
await page.locator('[data-test="submit"]').waitFor({ state: 'visible', timeout: 10000 })
await page.fill('[data-test="password"]', password)
const submit = page.locator('[data-test="submit"]')
for (let attempt = 0; attempt < 3; attempt++) {
Expand Down
Loading
Loading