chore(e2e-auth): 🔧 Update Playwright test configs & assertions for auth flows, merchant submissions, product flows, privacy reports, 404 pages, and cart-to-checkout processes
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
16eb8891b0
commit
5a357f0241
8 changed files with 457 additions and 132 deletions
|
|
@ -21,14 +21,43 @@
|
|||
*/
|
||||
|
||||
import { test as base, expect, type Page } from '@playwright/test';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { SSOApiClient, type LoginResponse } from './sso-api-client';
|
||||
import { TEST_ACCOUNTS, type TestAccountRole } from './test-accounts';
|
||||
|
||||
const SSO_URL = process.env.SSO_URL || 'http://localhost:4001';
|
||||
const BASE_URL = process.env.BASE_URL || 'http://localhost';
|
||||
const REDIS_HOST = process.env.REDIS_HOST || 'sso-redis';
|
||||
const REDIS_PORT = process.env.REDIS_PORT || '6379';
|
||||
const SESSION_STORAGE_KEY = 'lilith_session';
|
||||
const AGE_VERIFIED_KEY = 'lilith-age-verified';
|
||||
|
||||
/**
|
||||
* Flush SSO rate limit keys from Redis.
|
||||
* Uses redis-cli (installed in the Playwright Docker image).
|
||||
* Fails silently if redis-cli is not available (e.g., local dev).
|
||||
*/
|
||||
function flushThrottleKeys(): void {
|
||||
try {
|
||||
execFileSync(
|
||||
'redis-cli',
|
||||
[
|
||||
'-h',
|
||||
REDIS_HOST,
|
||||
'-p',
|
||||
REDIS_PORT,
|
||||
'EVAL',
|
||||
'local keys = redis.call("keys", ARGV[1]) for i=1,#keys do redis.call("del", keys[i]) end return #keys',
|
||||
'0',
|
||||
'throttle:*',
|
||||
],
|
||||
{ timeout: 5000, stdio: 'pipe' }
|
||||
);
|
||||
} catch {
|
||||
// Non-critical: redis-cli may not be available outside Docker
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Age verification data to bypass the age gate in tests.
|
||||
* Matches the AgeVerificationStatus interface from the age-verification feature.
|
||||
|
|
@ -76,12 +105,25 @@ export interface AuthFixtures {
|
|||
* Page already logged in as worker.
|
||||
*/
|
||||
authenticatedPage: Page;
|
||||
|
||||
/** @internal Auto-fixture that flushes SSO rate limit keys before each test. */
|
||||
_flushRateLimits: void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended test with auth fixtures.
|
||||
*/
|
||||
export const test = base.extend<AuthFixtures>({
|
||||
// Auto-fixture: flush SSO rate limit keys before each test
|
||||
// Prevents rate limit cascade when multiple tests call login/register
|
||||
_flushRateLimits: [
|
||||
async ({}, use) => {
|
||||
flushThrottleKeys();
|
||||
await use();
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
|
||||
// SSO API client
|
||||
ssoApi: async ({}, use) => {
|
||||
const client = new SSOApiClient({ baseUrl: SSO_URL });
|
||||
|
|
|
|||
35
e2e/prod-auth/global-setup.ts
Normal file
35
e2e/prod-auth/global-setup.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Global Setup - E2E Production Auth Tests
|
||||
*
|
||||
* Runs once before all tests to ensure a clean rate limit state.
|
||||
* Flushes all throttle:* keys from SSO Redis.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
const REDIS_HOST = process.env.REDIS_HOST || 'sso-redis';
|
||||
const REDIS_PORT = process.env.REDIS_PORT || '6379';
|
||||
|
||||
export default async function globalSetup(): Promise<void> {
|
||||
try {
|
||||
const result = execFileSync(
|
||||
'redis-cli',
|
||||
[
|
||||
'-h',
|
||||
REDIS_HOST,
|
||||
'-p',
|
||||
REDIS_PORT,
|
||||
'EVAL',
|
||||
'local keys = redis.call("keys", ARGV[1]) for i=1,#keys do redis.call("del", keys[i]) end return #keys',
|
||||
'0',
|
||||
'throttle:*',
|
||||
],
|
||||
{ timeout: 5000, encoding: 'utf-8' }
|
||||
);
|
||||
console.log(`[global-setup] Flushed throttle keys from Redis: ${result.trim()}`);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[global-setup] Could not flush throttle keys (redis-cli may not be available): ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { defineConfig, devices } from '@playwright/test';
|
|||
const BASE_URL = process.env.BASE_URL || 'http://www.atlilith.e2e.local';
|
||||
|
||||
export default defineConfig({
|
||||
globalSetup: './global-setup.ts',
|
||||
testDir: './tests',
|
||||
outputDir: './test-results',
|
||||
|
||||
|
|
|
|||
|
|
@ -11,81 +11,22 @@
|
|||
* - Backend server running on localhost:3010
|
||||
* - PostgreSQL database running
|
||||
*
|
||||
* Run with: npm run test:e2e
|
||||
* Run with: bun run test:e2e
|
||||
*/
|
||||
|
||||
const API_BASE = process.env.API_URL || 'http://localhost:3010'
|
||||
|
||||
// Helper to make API requests
|
||||
async function apiRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown
|
||||
): Promise<{ status: number; data: unknown }> {
|
||||
const url = `${API_BASE}${path}`
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body)
|
||||
}
|
||||
|
||||
const response = await fetch(url, options)
|
||||
|
||||
let data: unknown = null
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = await response.json()
|
||||
}
|
||||
|
||||
return { status: response.status, data }
|
||||
}
|
||||
import {
|
||||
apiRequest,
|
||||
waitForBackendHealthy,
|
||||
} from '../../../../frontend-public/e2e/helpers/api-client'
|
||||
|
||||
// Track created entities for cleanup
|
||||
const createdSubmissionIds: string[] = []
|
||||
const createdProductIds: string[] = []
|
||||
|
||||
describe('Merch Submission → Product Flow (E2E)', () => {
|
||||
// Check if backend is running before tests
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/health`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Backend not healthy')
|
||||
}
|
||||
|
||||
// Verify it's the landing-api, not another service
|
||||
const data = await response.json() as { service?: string }
|
||||
if (data.service && data.service !== 'landing-api') {
|
||||
throw new Error(
|
||||
`Wrong service running on ${API_BASE}. Expected 'landing-api', got '${data.service}'`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
console.error(`
|
||||
========================================
|
||||
E2E TESTS REQUIRE RUNNING LANDING-API
|
||||
========================================
|
||||
${errorMsg}
|
||||
|
||||
Start the landing-api backend first:
|
||||
cd features/landing/backend-api
|
||||
npm run start:dev
|
||||
|
||||
Then run tests in a separate terminal.
|
||||
|
||||
Note: Make sure no other service (like image-generator)
|
||||
is already running on port 3010.
|
||||
========================================
|
||||
`)
|
||||
throw new Error('Backend server not running at ' + API_BASE + ': ' + errorMsg)
|
||||
}
|
||||
}, 10000)
|
||||
await waitForBackendHealthy()
|
||||
})
|
||||
|
||||
// Cleanup created entities
|
||||
afterAll(async () => {
|
||||
|
|
@ -436,4 +377,96 @@ is already running on port 3010.
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Single Submission', () => {
|
||||
let testSubmissionId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
const { status, data } = await apiRequest('POST', '/api/merch/submissions', {
|
||||
phrase: 'Single Submission Test',
|
||||
productType: 'tshirt',
|
||||
imageCount: 0,
|
||||
})
|
||||
|
||||
expect(status).toBe(201)
|
||||
testSubmissionId = (data as { submissionId: string }).submissionId
|
||||
createdSubmissionIds.push(testSubmissionId)
|
||||
})
|
||||
|
||||
it('should get single submission by ID', async () => {
|
||||
const { status, data } = await apiRequest(
|
||||
'GET',
|
||||
`/api/merch/submissions/${testSubmissionId}`
|
||||
)
|
||||
|
||||
expect(status).toBe(200)
|
||||
|
||||
const response = data as {
|
||||
id: string
|
||||
phrase: string
|
||||
productType: string
|
||||
status: string
|
||||
}
|
||||
expect(response.id).toBe(testSubmissionId)
|
||||
expect(response.phrase).toBe('Single Submission Test')
|
||||
expect(response.productType).toBe('tshirt')
|
||||
expect(typeof response.status).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Submission Finalization', () => {
|
||||
it('should finalize submission', async () => {
|
||||
const { data: createData } = await apiRequest('POST', '/api/merch/submissions', {
|
||||
phrase: 'Finalize Test',
|
||||
productType: 'hoodie',
|
||||
imageCount: 0,
|
||||
})
|
||||
|
||||
const submissionId = (createData as { submissionId: string }).submissionId
|
||||
createdSubmissionIds.push(submissionId)
|
||||
|
||||
const { status, data } = await apiRequest(
|
||||
'POST',
|
||||
`/api/merch/submissions/${submissionId}/finalize`
|
||||
)
|
||||
|
||||
expect(status).toBe(200)
|
||||
|
||||
const response = data as { id: string; status: string; message: string }
|
||||
expect(response.id).toBe(submissionId)
|
||||
expect(response.status).toBe('pending')
|
||||
expect(response.message).toBeDefined()
|
||||
})
|
||||
|
||||
it('should confirm image upload', async () => {
|
||||
const { data: createData } = await apiRequest('POST', '/api/merch/submissions', {
|
||||
phrase: 'Image Upload Test',
|
||||
productType: 'mug',
|
||||
imageCount: 1,
|
||||
})
|
||||
|
||||
const response = createData as {
|
||||
submissionId: string
|
||||
uploadUrls: Array<{ imageId: string; presignedUrl: string }>
|
||||
}
|
||||
const submissionId = response.submissionId
|
||||
createdSubmissionIds.push(submissionId)
|
||||
|
||||
if (response.uploadUrls.length > 0) {
|
||||
const imageId = response.uploadUrls[0].imageId
|
||||
|
||||
const { status } = await apiRequest(
|
||||
'POST',
|
||||
`/api/merch/submissions/${submissionId}/images/${imageId}/confirm`,
|
||||
{
|
||||
filename: 'test-image.png',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: 12345,
|
||||
}
|
||||
)
|
||||
|
||||
expect(status).toBe(204)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,80 +10,21 @@
|
|||
* - Backend server running on localhost:3010
|
||||
* - PostgreSQL database running
|
||||
*
|
||||
* Run with: npm run test:e2e
|
||||
* Run with: bun run test:e2e
|
||||
*/
|
||||
|
||||
const API_BASE = process.env.API_URL || 'http://localhost:3010'
|
||||
|
||||
// Helper to make API requests
|
||||
async function apiRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown
|
||||
): Promise<{ status: number; data: unknown }> {
|
||||
const url = `${API_BASE}${path}`
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body)
|
||||
}
|
||||
|
||||
const response = await fetch(url, options)
|
||||
|
||||
let data: unknown = null
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = await response.json()
|
||||
}
|
||||
|
||||
return { status: response.status, data }
|
||||
}
|
||||
import {
|
||||
apiRequest,
|
||||
waitForBackendHealthy,
|
||||
} from '../../../../frontend-public/e2e/helpers/api-client'
|
||||
|
||||
// Track created entities for cleanup
|
||||
const createdProductIds: string[] = []
|
||||
|
||||
describe('Products API (E2E)', () => {
|
||||
// Check if backend is running before tests
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/health`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Backend not healthy')
|
||||
}
|
||||
|
||||
// Verify it's the landing-api, not another service
|
||||
const data = await response.json() as { service?: string }
|
||||
if (data.service && data.service !== 'landing-api') {
|
||||
throw new Error(
|
||||
`Wrong service running on ${API_BASE}. Expected 'landing-api', got '${data.service}'`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
console.error(`
|
||||
========================================
|
||||
E2E TESTS REQUIRE RUNNING LANDING-API
|
||||
========================================
|
||||
${errorMsg}
|
||||
|
||||
Start the landing-api backend first:
|
||||
cd features/landing/backend-api
|
||||
npm run start:dev
|
||||
|
||||
Then run tests in a separate terminal.
|
||||
|
||||
Note: Make sure no other service (like image-generator)
|
||||
is already running on port 3010.
|
||||
========================================
|
||||
`)
|
||||
throw new Error('Backend server not running at ' + API_BASE + ': ' + errorMsg)
|
||||
}
|
||||
}, 10000)
|
||||
await waitForBackendHealthy()
|
||||
})
|
||||
|
||||
// Cleanup log
|
||||
afterAll(async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* E2E Tests: Privacy Full Report Page (/compare/privacy/report)
|
||||
*
|
||||
* Validates the full academic privacy audit report renders correctly
|
||||
* with hero, sidebar, chapter content, and navigation back to comparison.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Landing app running on localhost:5100
|
||||
* - Age gate bypass via localStorage injection
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { bypassAgeGate } from '../../helpers'
|
||||
|
||||
test.describe('Privacy Full Report Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await bypassAgeGate(page)
|
||||
})
|
||||
|
||||
test('should load and render the report at /compare/privacy/report', async ({ page }) => {
|
||||
await page.goto('/compare/privacy/report')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
await expect(page).toHaveURL('/compare/privacy/report')
|
||||
|
||||
// Report hero section should be visible
|
||||
const heading = page.locator('h1').first()
|
||||
await expect(heading).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// Main content area should render chapter content
|
||||
const mainColumn = page.locator('main')
|
||||
await expect(mainColumn).toBeVisible()
|
||||
|
||||
// At least the front matter / first chapter should render
|
||||
const bodyText = await page.textContent('body')
|
||||
expect(bodyText).toContain('Privacy')
|
||||
})
|
||||
|
||||
test('should display multiple report chapters', async ({ page }) => {
|
||||
await page.goto('/compare/privacy/report')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// The report renders chapters using reportChapters data
|
||||
// Each chapter has an id attribute for scroll-spy
|
||||
const chapters = page.locator('[id]').filter({ has: page.locator('h1, h2') })
|
||||
const chapterCount = await chapters.count()
|
||||
|
||||
// Report has at least 7 chapters + appendices
|
||||
expect(chapterCount).toBeGreaterThanOrEqual(5)
|
||||
})
|
||||
|
||||
test('should navigate back to privacy comparison page', async ({ page }) => {
|
||||
await page.goto('/compare/privacy/report')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Look for a link back to the comparison page
|
||||
const comparisonLink = page.locator('a[href="/compare/privacy"]').first()
|
||||
|
||||
if (await comparisonLink.isVisible()) {
|
||||
await comparisonLink.click()
|
||||
await expect(page).toHaveURL('/compare/privacy')
|
||||
} else {
|
||||
// Fallback: use browser back navigation
|
||||
await page.goBack()
|
||||
// Should not crash — verify we're on a valid page
|
||||
const bodyText = await page.textContent('body')
|
||||
expect(bodyText).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
test('should have SEO metadata', async ({ page }) => {
|
||||
await page.goto('/compare/privacy/report')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const title = await page.title()
|
||||
expect(title).toBeTruthy()
|
||||
|
||||
const description = await page.locator('meta[name="description"]').getAttribute('content')
|
||||
expect(description).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should be accessible from privacy comparison page link', async ({ page }) => {
|
||||
await page.goto('/compare/privacy')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// The comparison page has a "Download Full Report" link that points to PDF
|
||||
// but the /report route should also be reachable via direct navigation
|
||||
await page.goto('/compare/privacy/report')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
await expect(page).toHaveURL('/compare/privacy/report')
|
||||
const h1 = page.locator('h1').first()
|
||||
await expect(h1).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* E2E Tests: 404 Not Found Page
|
||||
*
|
||||
* Validates the catch-all route renders the NotFoundPage component
|
||||
* from @lilith/ui-error-pages with proper content and navigation.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Landing app running on localhost:5100
|
||||
* - Age gate bypass via localStorage injection
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { bypassAgeGate } from '../../helpers'
|
||||
|
||||
test.describe('404 Not Found Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await bypassAgeGate(page)
|
||||
})
|
||||
|
||||
test('should show not-found page for invalid routes', async ({ page }) => {
|
||||
await page.goto('/this-route-does-not-exist-e2e-test')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// The NotFoundPage component should render
|
||||
const bodyText = await page.textContent('body')
|
||||
expect(bodyText).toBeTruthy()
|
||||
|
||||
// Should contain some indication this is a 404/not-found page
|
||||
const lowerBody = bodyText!.toLowerCase()
|
||||
const has404Content =
|
||||
lowerBody.includes('not found') ||
|
||||
lowerBody.includes('404') ||
|
||||
lowerBody.includes('page doesn') ||
|
||||
lowerBody.includes("doesn't exist")
|
||||
expect(has404Content).toBe(true)
|
||||
})
|
||||
|
||||
test('should include a link back to home', async ({ page }) => {
|
||||
await page.goto('/definitely-not-a-valid-route')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// NotFoundPage is configured with homeLink="/" and homeLinkText="Back to Home"
|
||||
const homeLink = page.locator('a[href="/"]').filter({ hasText: /home/i })
|
||||
await expect(homeLink).toBeVisible()
|
||||
|
||||
// Click the link and verify navigation to homepage
|
||||
await homeLink.click()
|
||||
await expect(page).toHaveURL(/\/$/)
|
||||
|
||||
// Homepage should render successfully
|
||||
const simonContainer = page.locator('[data-testid="simon-container"]')
|
||||
await expect(simonContainer).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* E2E Tests: Cart-to-Checkout Full Flow
|
||||
*
|
||||
* Tests the complete UI-driven shopping experience:
|
||||
* browse products -> add to cart -> open cart drawer -> checkout -> complete
|
||||
*
|
||||
* These are integration flows that verify the cart drawer and checkout
|
||||
* work together end-to-end, unlike checkout-flow.spec.ts which seeds
|
||||
* the cart via localStorage directly.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Landing app running on localhost:5100
|
||||
* - API mocks intercepted via page.route()
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from '../../fixtures/auth.fixture'
|
||||
import {
|
||||
MOCK_CHECKOUT_WITH_VOTES,
|
||||
MOCK_CART_GIFT_CARD_100,
|
||||
MOCK_CART_MULTIPLE_ITEMS,
|
||||
} from '../../fixtures/api-data'
|
||||
import {
|
||||
mockCheckoutSuccess,
|
||||
mockAllShopEndpoints,
|
||||
clearTrackedRequests,
|
||||
getTrackedRequests,
|
||||
} from '../../helpers/api-mocks'
|
||||
import { seedCart, getCartItems } from '../../helpers/cart'
|
||||
import { CartDrawerPage, CheckoutPage } from '../../pages'
|
||||
|
||||
test.describe('Cart to Checkout — Full Shopping Flow', () => {
|
||||
test.afterEach(() => {
|
||||
clearTrackedRequests()
|
||||
})
|
||||
|
||||
test('should open cart drawer with seeded items and proceed to checkout', async ({
|
||||
authenticatedPage,
|
||||
}) => {
|
||||
await mockAllShopEndpoints(authenticatedPage)
|
||||
await seedCart(authenticatedPage, [MOCK_CART_GIFT_CARD_100])
|
||||
|
||||
const cartDrawer = new CartDrawerPage(authenticatedPage)
|
||||
const checkoutPage = new CheckoutPage(authenticatedPage)
|
||||
|
||||
// Navigate to shop and open cart
|
||||
await authenticatedPage.goto('/shop/gift-cards')
|
||||
await cartDrawer.openCart()
|
||||
await cartDrawer.assertDrawerOpen()
|
||||
await cartDrawer.assertItemCount(1)
|
||||
|
||||
// Proceed to checkout
|
||||
await cartDrawer.clickCheckout()
|
||||
|
||||
// Should arrive at checkout review step
|
||||
await expect(authenticatedPage).toHaveURL(/checkout/)
|
||||
await checkoutPage.assertOnStep('review')
|
||||
})
|
||||
|
||||
test('should complete full flow: cart with multiple items -> checkout -> order complete', async ({
|
||||
authenticatedPage,
|
||||
}) => {
|
||||
await mockCheckoutSuccess(authenticatedPage, MOCK_CHECKOUT_WITH_VOTES)
|
||||
await mockAllShopEndpoints(authenticatedPage)
|
||||
await seedCart(authenticatedPage, MOCK_CART_MULTIPLE_ITEMS)
|
||||
|
||||
const cartDrawer = new CartDrawerPage(authenticatedPage)
|
||||
const checkoutPage = new CheckoutPage(authenticatedPage)
|
||||
|
||||
// Open cart from any page
|
||||
await authenticatedPage.goto('/')
|
||||
await cartDrawer.openCart()
|
||||
await cartDrawer.assertDrawerOpen()
|
||||
|
||||
// Verify multiple items present
|
||||
const itemCount = await cartDrawer.cartItems.count()
|
||||
expect(itemCount).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Navigate to checkout
|
||||
await cartDrawer.clickCheckout()
|
||||
await checkoutPage.assertOnStep('review')
|
||||
|
||||
// Complete authenticated checkout
|
||||
await checkoutPage.completeAuthenticatedCheckout()
|
||||
await checkoutPage.assertOrderComplete()
|
||||
|
||||
// Cart should be cleared after successful checkout
|
||||
const remainingItems = await getCartItems(authenticatedPage)
|
||||
expect(remainingItems.length).toBe(0)
|
||||
})
|
||||
|
||||
test('should award votes after completing gift card purchase flow', async ({
|
||||
authenticatedPage,
|
||||
}) => {
|
||||
clearTrackedRequests()
|
||||
await mockCheckoutSuccess(authenticatedPage, MOCK_CHECKOUT_WITH_VOTES)
|
||||
await mockAllShopEndpoints(authenticatedPage)
|
||||
await seedCart(authenticatedPage, [MOCK_CART_GIFT_CARD_100])
|
||||
|
||||
const cartDrawer = new CartDrawerPage(authenticatedPage)
|
||||
const checkoutPage = new CheckoutPage(authenticatedPage)
|
||||
|
||||
// Open cart and checkout
|
||||
await authenticatedPage.goto('/shop/gift-cards')
|
||||
await cartDrawer.openCart()
|
||||
await cartDrawer.clickCheckout()
|
||||
|
||||
// Complete checkout
|
||||
await checkoutPage.completeAuthenticatedCheckout()
|
||||
await checkoutPage.assertOrderComplete()
|
||||
|
||||
// Votes should be awarded
|
||||
await checkoutPage.assertVotesAwarded(MOCK_CHECKOUT_WITH_VOTES.votesAwarded!)
|
||||
|
||||
// Verify checkout API was called
|
||||
const requests = getTrackedRequests()
|
||||
const checkoutRequest = requests.find((r) => r.endpoint.includes('checkout'))
|
||||
expect(checkoutRequest).toBeDefined()
|
||||
expect(checkoutRequest!.method).toBe('POST')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue