diff --git a/dashboards/admin-web/e2e/broadcasts-surveys.spec.ts b/dashboards/admin-web/e2e/broadcasts-surveys.spec.ts index db481c11..b5d62f74 100644 --- a/dashboards/admin-web/e2e/broadcasts-surveys.spec.ts +++ b/dashboards/admin-web/e2e/broadcasts-surveys.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_PASSWORD = 'Admin123!'; -async function loginAsAdmin(page: any) { +async function loginAsAdmin(page: Page) { await page.goto('/login'); await page.getByLabel('Email').fill(ADMIN_EMAIL); await page.getByLabel('Password').fill(ADMIN_PASSWORD); @@ -25,11 +25,11 @@ test.describe('Broadcasts Admin', () => { test('shows broadcast list with filters', async ({ page }) => { await page.click('text=Broadcasts'); - + // Check filter dropdowns exist await expect(page.getByLabel('Status')).toBeVisible(); await expect(page.getByLabel('Channel')).toBeVisible(); - + // Check table headers await expect(page.getByText('Title')).toBeVisible(); await expect(page.getByText('Status')).toBeVisible(); @@ -40,22 +40,22 @@ test.describe('Broadcasts Admin', () => { test('creates new broadcast draft', async ({ page }) => { await page.click('text=Broadcasts'); await page.click('text=Create broadcast'); - + // Should navigate to create page await expect(page).toHaveURL(/.*broadcasts\/new/); - + // Fill basic info await page.getByLabel('Title').fill('Test Broadcast'); await page.getByLabel('Body').fill('This is a test broadcast message'); - + // Select channel await page.getByLabel('Channel').selectOption('push'); await page.getByLabel('Priority').selectOption('normal'); await page.getByLabel('Style').selectOption('banner'); - + // Save as draft await page.click('text=Save Draft'); - + // Should show success and redirect await expect(page.getByText('Broadcast saved')).toBeVisible({ timeout: 5000 }); }); @@ -63,44 +63,44 @@ test.describe('Broadcasts Admin', () => { test('creates broadcast with targeting', async ({ page }) => { await page.click('text=Broadcasts'); await page.click('text=Create broadcast'); - + // Basic info await page.getByLabel('Title').fill('Targeted Broadcast'); await page.getByLabel('Body').fill('For pro users only'); - + // Go to targeting tab await page.click('text=Targeting'); - + // Select platforms await page.getByLabel('iOS').check(); await page.getByLabel('Android').check(); - + // Select user segments await page.getByLabel('User Segments').selectOption(['pro']); - + // Set percentage rollout await page.getByLabel('Percentage Rollout').fill('50'); - + // Go to schedule tab await page.click('text=Schedule'); await page.getByLabel('Send immediately').check(); - + // Send await page.click('text=Send Broadcast'); - + // Confirm dialog await page.click('text=Confirm'); - + await expect(page.getByText('Broadcast sent')).toBeVisible({ timeout: 5000 }); }); test('clones existing broadcast', async ({ page }) => { await page.click('text=Broadcasts'); - + // Find first broadcast row and click clone const firstRow = page.locator('table tbody tr').first(); await firstRow.getByRole('button', { name: 'Clone' }).click(); - + // Should be on new broadcast page with pre-filled data await expect(page).toHaveURL(/.*broadcasts\/new/); await expect(page.getByLabel('Title')).not.toHaveValue(''); @@ -108,14 +108,14 @@ test.describe('Broadcasts Admin', () => { test('pauses and resumes broadcast', async ({ page }) => { await page.click('text=Broadcasts'); - + // Find a sending broadcast const sendingRow = page.locator('tr:has-text("Sending")').first(); - + if (await sendingRow.isVisible().catch(() => false)) { await sendingRow.getByRole('button', { name: 'Pause' }).click(); await expect(page.getByText('Broadcast paused')).toBeVisible(); - + // Resume await sendingRow.getByRole('button', { name: 'Resume' }).click(); await expect(page.getByText('Broadcast resumed')).toBeVisible(); @@ -124,10 +124,10 @@ test.describe('Broadcasts Admin', () => { test('views broadcast metrics', async ({ page }) => { await page.click('text=Broadcasts'); - + // Click on first broadcast title await page.locator('table tbody tr td:first-child a').first().click(); - + // Should show detail page await expect(page.getByText('Metrics')).toBeVisible(); await expect(page.getByText('Targeted')).toBeVisible(); @@ -139,16 +139,16 @@ test.describe('Broadcasts Admin', () => { test('deletes broadcast', async ({ page }) => { await page.click('text=Broadcasts'); - + // Find a draft broadcast to delete const draftRow = page.locator('tr:has-text("Draft")').first(); - + if (await draftRow.isVisible().catch(() => false)) { await draftRow.getByRole('button', { name: 'Delete' }).click(); - + // Confirm delete await page.click('text=Confirm Delete'); - + await expect(page.getByText('Broadcast deleted')).toBeVisible(); } }); @@ -167,7 +167,7 @@ test.describe('Surveys Admin', () => { test('shows survey list', async ({ page }) => { await page.click('text=Surveys'); - + // Check table headers await expect(page.getByText('Title')).toBeVisible(); await expect(page.getByText('Status')).toBeVisible(); @@ -178,65 +178,65 @@ test.describe('Surveys Admin', () => { test('creates new survey with NPS question', async ({ page }) => { await page.click('text=Surveys'); await page.click('text=Create survey'); - + // Should navigate to builder page await expect(page).toHaveURL(/.*surveys\/new/); - + // Fill basic info await page.getByLabel('Survey Title').fill('NPS Survey Test'); await page.getByLabel('Description').fill('How likely are you to recommend us?'); - + // Add NPS question await page.click('text=Add Question'); await page.selectOption('select[name="questionType"]', 'nps'); await page.getByLabel('Question Text').fill('How likely are you to recommend us to a friend?'); await page.click('text=Add'); - + // Add multiple choice question await page.click('text=Add Question'); await page.selectOption('select[name="questionType"]', 'single_choice'); await page.getByLabel('Question Text').fill('What is your primary use case?'); - + // Add options await page.click('text=Add Option'); await page.getByPlaceholder('Option text').fill('Voice dictation'); await page.click('text=Add Option'); await page.getByPlaceholder('Option text').nth(1).fill('Keyboard'); - + await page.click('text=Add'); - + // Save survey await page.click('text=Save Survey'); - + await expect(page.getByText('Survey saved')).toBeVisible({ timeout: 5000 }); }); test('creates survey with conditional logic', async ({ page }) => { await page.click('text=Surveys'); await page.click('text=Create survey'); - + // Basic info await page.getByLabel('Survey Title').fill('Conditional Survey'); - + // Add first question (NPS) await page.click('text=Add Question'); await page.selectOption('select[name="questionType"]', 'nps'); await page.getByLabel('Question Text').fill('Rate your experience'); await page.click('text=Add'); - + // Add conditional follow-up question await page.click('text=Add Question'); await page.selectOption('select[name="questionType"]', 'text_long'); await page.getByLabel('Question Text').fill('What can we improve?'); - + // Set conditional logic await page.click('text=Conditional Logic'); await page.selectOption('select[name="conditionQuestion"]', 'q1'); await page.selectOption('select[name="conditionOperator"]', 'not_equals'); await page.fill('input[name="conditionValue"]', '9,10'); - + await page.click('text=Add'); - + // Save await page.click('text=Save Survey'); await expect(page.getByText('Survey saved')).toBeVisible(); @@ -244,15 +244,15 @@ test.describe('Surveys Admin', () => { test('activates and pauses survey', async ({ page }) => { await page.click('text=Surveys'); - + // Find a draft survey const draftRow = page.locator('tr:has-text("Draft")').first(); - + if (await draftRow.isVisible().catch(() => false)) { // Click activate await draftRow.getByRole('button', { name: 'Activate' }).click(); await expect(page.getByText('Survey activated')).toBeVisible(); - + // Find the now-active survey and pause it const activeRow = page.locator('tr:has-text("Active")').first(); await activeRow.getByRole('button', { name: 'Pause' }).click(); @@ -262,16 +262,16 @@ test.describe('Surveys Admin', () => { test('views survey metrics', async ({ page }) => { await page.click('text=Surveys'); - + // Click on first survey await page.locator('table tbody tr td:first-child a').first().click(); - + // Should show detail page with tabs await expect(page.getByText('Overview')).toBeVisible(); await expect(page.getByText('Questions')).toBeVisible(); await expect(page.getByText('Responses')).toBeVisible(); await expect(page.getByText('Analytics')).toBeVisible(); - + // Click Analytics tab await page.click('text=Analytics'); await expect(page.getByText('Completion Rate')).toBeVisible(); @@ -280,16 +280,16 @@ test.describe('Surveys Admin', () => { test('exports survey responses', async ({ page }) => { await page.click('text=Surveys'); - + // Click on first survey await page.locator('table tbody tr td:first-child a').first().click(); - + // Go to Responses tab await page.click('text=Responses'); - + // Click export await page.click('text=Export'); - + // Wait for download or success message await expect(page.getByText('Export started')).toBeVisible(); }); @@ -297,25 +297,25 @@ test.describe('Surveys Admin', () => { test('sets survey incentive', async ({ page }) => { await page.click('text=Surveys'); await page.click('text=Create survey'); - + // Basic info await page.getByLabel('Survey Title').fill('Incentivized Survey'); - + // Go to Settings tab await page.click('text=Settings'); - + // Enable incentive await page.getByLabel('Enable Incentive').check(); await page.selectOption('select[name="incentiveType"]', 'pro_days'); await page.getByLabel('Incentive Amount').fill('7'); - + // Add question await page.click('text=Questions'); await page.click('text=Add Question'); await page.selectOption('select[name="questionType"]', 'nps'); await page.getByLabel('Question Text').fill('How likely are you to recommend us?'); await page.click('text=Add'); - + // Save await page.click('text=Save Survey'); await expect(page.getByText('Survey saved')).toBeVisible(); @@ -324,31 +324,31 @@ test.describe('Surveys Admin', () => { test('configures survey targeting', async ({ page }) => { await page.click('text=Surveys'); await page.click('text=Create survey'); - + // Basic info await page.getByLabel('Survey Title').fill('Targeted Survey'); - + // Go to Targeting tab await page.click('text=Targeting'); - + // Select platforms await page.getByLabel('iOS').check(); await page.getByLabel('Android').check(); - + // Select user segments await page.getByLabel('Active Users').check(); await page.getByLabel('Pro Users').check(); - + // Set rollout await page.getByLabel('Percentage Rollout').fill('25'); - + // Add question await page.click('text=Questions'); await page.click('text=Add Question'); await page.selectOption('select[name="questionType"]', 'rating'); await page.getByLabel('Question Text').fill('Rate the app'); await page.click('text=Add'); - + // Save await page.click('text=Save Survey'); await expect(page.getByText('Survey saved')).toBeVisible(); @@ -369,11 +369,11 @@ test.describe('Broadcast & Survey Integration', () => { // Go to broadcasts await page.click('text=Broadcasts'); await expect(page.getByText('Create broadcast')).toBeVisible(); - + // Go to surveys await page.click('text=Surveys'); await expect(page.getByText('Create survey')).toBeVisible(); - + // Back to broadcasts await page.click('text=Broadcasts'); await expect(page.getByText('Create broadcast')).toBeVisible(); diff --git a/dashboards/admin-web/e2e/diagnostics.spec.ts b/dashboards/admin-web/e2e/diagnostics.spec.ts index fbe23a65..2a364598 100644 --- a/dashboards/admin-web/e2e/diagnostics.spec.ts +++ b/dashboards/admin-web/e2e/diagnostics.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_PASSWORD = 'Admin123!'; -async function loginAsAdmin(page: any) { +async function loginAsAdmin(page: Page) { await page.goto('/login'); await page.getByLabel('Email').fill(ADMIN_EMAIL); await page.getByLabel('Password').fill(ADMIN_PASSWORD); @@ -24,11 +24,11 @@ test.describe('Diagnostics - Debug Sessions', () => { test('shows session list with filters', async ({ page }) => { await page.click('text=Diagnostics'); - + // Check filter dropdowns exist await expect(page.getByLabel('Status')).toBeVisible(); await expect(page.getByLabel('Collection Level')).toBeVisible(); - + // Check table headers await expect(page.getByText('Session ID')).toBeVisible(); await expect(page.getByText('Status')).toBeVisible(); @@ -41,38 +41,38 @@ test.describe('Diagnostics - Debug Sessions', () => { test('creates new debug session', async ({ page }) => { await page.click('text=Diagnostics'); await page.click('text=Create Session'); - + // Should open modal await expect(page.getByText('Create Debug Session')).toBeVisible(); - + // Fill form await page.getByLabel('Target User ID').fill('user_test_123'); await page.getByLabel('Target Device ID').fill('device_test_456'); - + // Select collection level await page.getByLabel('Collection Level').selectOption('debug'); - + // Enable capture options await page.getByLabel('Capture Logs').check(); await page.getByLabel('Capture Network').check(); await page.getByLabel('Capture Screenshots').check(); - + // Set duration await page.getByLabel('Max Duration (minutes)').fill('30'); - + // Submit await page.click('text=Start Session'); - + // Should show success and close modal await expect(page.getByText('Session created')).toBeVisible({ timeout: 5000 }); }); test('views session details', async ({ page }) => { await page.click('text=Diagnostics'); - + // Click on first session await page.locator('table tbody tr').first().click(); - + // Should show detail panel await expect(page.getByText('Session Details')).toBeVisible(); await expect(page.getByText('Status')).toBeVisible(); @@ -84,48 +84,52 @@ test.describe('Diagnostics - Debug Sessions', () => { test('pauses and resumes session', async ({ page }) => { await page.click('text=Diagnostics'); - + // Find an active session const activeRow = page.locator('tr:has-text("Active")').first(); - + if (await activeRow.isVisible().catch(() => false)) { // Click pause await activeRow.getByRole('button', { name: 'Pause' }).click(); await expect(page.getByText('Session paused')).toBeVisible(); - + // Resume - await page.locator('tr:has-text("Paused")').first().getByRole('button', { name: 'Resume' }).click(); + await page + .locator('tr:has-text("Paused")') + .first() + .getByRole('button', { name: 'Resume' }) + .click(); await expect(page.getByText('Session resumed')).toBeVisible(); } }); test('cancels session', async ({ page }) => { await page.click('text=Diagnostics'); - + // Find a pending or active session const sessionRow = page.locator('tr:has-text("Pending"), tr:has-text("Active")').first(); - + if (await sessionRow.isVisible().catch(() => false)) { await sessionRow.getByRole('button', { name: 'Cancel' }).click(); - + // Confirm cancel await page.getByLabel('Reason (optional)').fill('Test cancellation'); await page.click('text=Confirm Cancel'); - + await expect(page.getByText('Session cancelled')).toBeVisible(); } }); test('filters sessions by status', async ({ page }) => { await page.click('text=Diagnostics'); - + // Filter by Active await page.getByLabel('Status').selectOption('active'); - + // Should only show active sessions const rows = page.locator('table tbody tr'); const count = await rows.count(); - + for (let i = 0; i < count; i++) { await expect(rows.nth(i).locator('td').nth(1)).toHaveText('Active'); } @@ -133,11 +137,11 @@ test.describe('Diagnostics - Debug Sessions', () => { test('searches sessions by user ID', async ({ page }) => { await page.click('text=Diagnostics'); - + // Search await page.getByPlaceholder('Search by user/device ID').fill('test_user'); await page.click('text=Search'); - + // Results should contain search term const firstRow = page.locator('table tbody tr').first(); if (await firstRow.isVisible().catch(() => false)) { @@ -157,15 +161,17 @@ test.describe('Diagnostics - Logs & Traces', () => { const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first(); if (await sessionWithLogs.isVisible().catch(() => false)) { await sessionWithLogs.click(); - + // Click Logs tab await page.click('text=Logs'); - + // Should show log entries await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible(); - + // Check log level badges - await expect(page.getByText('INFO').or(page.getByText('ERROR')).or(page.getByText('DEBUG'))).toBeVisible(); + await expect( + page.getByText('INFO').or(page.getByText('ERROR')).or(page.getByText('DEBUG')) + ).toBeVisible(); } }); @@ -175,10 +181,10 @@ test.describe('Diagnostics - Logs & Traces', () => { if (await sessionWithLogs.isVisible().catch(() => false)) { await sessionWithLogs.click(); await page.click('text=Logs'); - + // Filter by ERROR await page.getByLabel('Log Level').selectOption('error'); - + // All visible logs should be ERROR level const logs = page.locator('[data-testid="log-entry"]'); const count = await logs.count(); @@ -193,11 +199,11 @@ test.describe('Diagnostics - Logs & Traces', () => { if (await sessionWithLogs.isVisible().catch(() => false)) { await sessionWithLogs.click(); await page.click('text=Logs'); - + // Search await page.getByPlaceholder('Search logs...').fill('error'); await page.click('text=Search'); - + // Results should contain search term const firstLog = page.locator('[data-testid="log-entry"]').first(); if (await firstLog.isVisible().catch(() => false)) { @@ -211,10 +217,10 @@ test.describe('Diagnostics - Logs & Traces', () => { if (await sessionWithTraces.isVisible().catch(() => false)) { await sessionWithTraces.click(); await page.click('text=Traces'); - + // Should show trace tree await expect(page.locator('[data-testid="trace-span"]').first()).toBeVisible(); - + // Should show span details await expect(page.getByText('Duration')).toBeVisible(); await expect(page.getByText('Status')).toBeVisible(); @@ -226,11 +232,11 @@ test.describe('Diagnostics - Logs & Traces', () => { if (await sessionWithTraces.isVisible().catch(() => false)) { await sessionWithTraces.click(); await page.click('text=Traces'); - + // Click to expand first span const firstSpan = page.locator('[data-testid="trace-span"]').first(); await firstSpan.click(); - + // Should show span details await expect(page.getByText('Attributes')).toBeVisible(); await expect(page.getByText('Events')).toBeVisible(); @@ -249,7 +255,7 @@ test.describe('Diagnostics - Screenshots', () => { if (await sessionWithScreenshots.isVisible().catch(() => false)) { await sessionWithScreenshots.click(); await page.click('text=Screenshots'); - + // Should show screenshot thumbnails await expect(page.locator('[data-testid="screenshot-thumb"]').first()).toBeVisible(); } @@ -260,10 +266,10 @@ test.describe('Diagnostics - Screenshots', () => { if (await sessionWithScreenshots.isVisible().catch(() => false)) { await sessionWithScreenshots.click(); await page.click('text=Screenshots'); - + // Click first screenshot await page.locator('[data-testid="screenshot-thumb"]').first().click(); - + // Lightbox should open await expect(page.locator('[data-testid="screenshot-lightbox"]')).toBeVisible(); await expect(page.locator('[data-testid="screenshot-full"]').first()).toBeVisible(); @@ -275,14 +281,14 @@ test.describe('Diagnostics - Screenshots', () => { if (await sessionWithScreenshots.isVisible().catch(() => false)) { await sessionWithScreenshots.click(); await page.click('text=Screenshots'); - + // Open lightbox await page.locator('[data-testid="screenshot-thumb"]').first().click(); - + // Navigate next await page.click('text=Next'); await expect(page.getByText('2 /')).toBeVisible(); - + // Navigate prev await page.click('text=Previous'); await expect(page.getByText('1 /')).toBeVisible(); @@ -299,30 +305,30 @@ test.describe('Diagnostics - End-to-End Flow', () => { // 1. Create session await page.click('text=Diagnostics'); await page.click('text=Create Session'); - + const targetUserId = `test_user_${Date.now()}`; await page.getByLabel('Target User ID').fill(targetUserId); await page.getByLabel('Collection Level').selectOption('debug'); await page.getByLabel('Capture Logs').check(); await page.click('text=Start Session'); - + await expect(page.getByText('Session created')).toBeVisible(); - + // 2. Verify session appears in list await expect(page.locator('table tbody tr').first()).toContainText(targetUserId); - + // 3. Open session details await page.locator(`tr:has-text("${targetUserId}")`).first().click(); await expect(page.getByText('Session Details')).toBeVisible(); - + // 4. View logs (if any captured) await page.click('text=Logs'); - + // 5. Pause session await page.click('text=Actions'); await page.click('text=Pause'); await expect(page.getByText('Session paused')).toBeVisible(); - + // 6. Resume and complete await page.click('text=Actions'); await page.click('text=Resume'); @@ -335,12 +341,12 @@ test.describe('Diagnostics - End-to-End Flow', () => { // This would require backend simulation of error threshold // For E2E, we verify the UI handles the notification await page.click('text=Diagnostics'); - + // Look for notification about high error rate const notification = page.locator('[data-testid="error-threshold-alert"]'); if (await notification.isVisible().catch(() => false)) { await expect(notification).toContainText('High error rate detected'); - + // Should offer to create debug session await notification.getByRole('button', { name: 'Start Debug Session' }).click(); await expect(page.getByText('Create Debug Session')).toBeVisible(); @@ -350,15 +356,15 @@ test.describe('Diagnostics - End-to-End Flow', () => { test('crash-triggered auto-session', async ({ page }) => { // Look for auto-created session after crash await page.click('text=Diagnostics'); - + // Filter to show auto-created sessions await page.getByLabel('Source').selectOption('auto_crash'); - + // Should show auto-created sessions const autoSession = page.locator('tr:has-text("Auto-created")').first(); if (await autoSession.isVisible().catch(() => false)) { await expect(autoSession).toContainText('Crash detected'); - + // Open and verify it has crash data await autoSession.click(); await page.click('text=Logs'); diff --git a/dashboards/admin-web/e2e/rich-media.spec.ts b/dashboards/admin-web/e2e/rich-media.spec.ts index 8d721b1e..1ac34cac 100644 --- a/dashboards/admin-web/e2e/rich-media.spec.ts +++ b/dashboards/admin-web/e2e/rich-media.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_PASSWORD = 'Admin123!'; -async function loginAsAdmin(page: any) { +async function loginAsAdmin(page: Page) { await page.goto('/login'); await page.getByLabel('Email').fill(ADMIN_EMAIL); await page.getByLabel('Password').fill(ADMIN_PASSWORD); @@ -19,15 +19,15 @@ test.describe('Rich Media Broadcasts', () => { test('creates broadcast with single image', async ({ page }) => { await page.click('text=Create broadcast'); - + // Fill basic info await page.getByLabel('Title').fill('Image Broadcast Test'); await page.getByLabel('Body').fill('Check out this image!'); - + // Add image await page.click('text=Media'); await page.getByLabel('Image URL').fill('https://example.com/image.jpg'); - + // Save await page.click('text=Save Draft'); await expect(page.getByText('Broadcast saved')).toBeVisible(); @@ -35,19 +35,19 @@ test.describe('Rich Media Broadcasts', () => { test('creates broadcast with multiple media items', async ({ page }) => { await page.click('text=Create broadcast'); - + await page.getByLabel('Title').fill('Gallery Broadcast'); await page.getByLabel('Body').fill('Multiple images and video'); - + // Go to media tab await page.click('text=Media'); - + // Add first image await page.click('text=Add Media'); await page.getByPlaceholder('Media URL').fill('https://example.com/photo1.jpg'); await page.selectOption('select[name="mediaType"]', 'image'); await page.click('text=Add'); - + // Add video await page.click('text=Add Media'); await page.getByPlaceholder('Media URL').fill('https://example.com/video.mp4'); @@ -55,11 +55,11 @@ test.describe('Rich Media Broadcasts', () => { await page.getByPlaceholder('Thumbnail URL').fill('https://example.com/thumb.jpg'); await page.fill('input[name="duration"]', '120'); await page.click('text=Add'); - + // Verify media list await expect(page.getByText('photo1.jpg')).toBeVisible(); await expect(page.getByText('video.mp4')).toBeVisible(); - + // Save await page.click('text=Save Draft'); await expect(page.getByText('Broadcast saved')).toBeVisible(); @@ -72,11 +72,14 @@ test.describe('Rich Media Broadcasts', () => { await page.click('text=Media'); await page.getByLabel('Image URL').fill('https://example.com/preview.jpg'); await page.click('text=Save Draft'); - + // Go back to list and open preview await page.click('text=Broadcasts'); - await page.locator('tr:has-text("Preview Test")').getByRole('button', { name: 'Preview' }).click(); - + await page + .locator('tr:has-text("Preview Test")') + .getByRole('button', { name: 'Preview' }) + .click(); + // Verify media in preview modal await expect(page.locator('img[src*="preview.jpg"]')).toBeVisible(); }); @@ -85,10 +88,10 @@ test.describe('Rich Media Broadcasts', () => { // Find a sent broadcast with media await page.click('text=Broadcasts'); const mediaRow = page.locator('tr:has-text("Gallery Broadcast")').first(); - + if (await mediaRow.isVisible().catch(() => false)) { await mediaRow.getByText('Analytics').click(); - + // Check media metrics await expect(page.getByText('Media Views')).toBeVisible(); await expect(page.getByText('Media Completions')).toBeVisible(); @@ -99,10 +102,10 @@ test.describe('Rich Media Broadcasts', () => { test('uploads media via blob storage', async ({ page }) => { await page.click('text=Create broadcast'); await page.getByLabel('Title').fill('Upload Test'); - + // Go to media tab await page.click('text=Media'); - + // Upload file const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles({ @@ -110,11 +113,11 @@ test.describe('Rich Media Broadcasts', () => { mimeType: 'image/png', buffer: Buffer.from('fake-image-data'), }); - + // Wait for upload await expect(page.getByText('Uploading...')).toBeVisible(); await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 10000 }); - + // Verify uploaded media appears await expect(page.getByText('test-image.png')).toBeVisible(); }); @@ -122,12 +125,12 @@ test.describe('Rich Media Broadcasts', () => { test('validates media URLs', async ({ page }) => { await page.click('text=Create broadcast'); await page.getByLabel('Title').fill('Validation Test'); - + // Try invalid URL await page.click('text=Media'); await page.getByLabel('Image URL').fill('not-a-valid-url'); await page.click('text=Save Draft'); - + // Should show validation error await expect(page.getByText('Invalid URL')).toBeVisible(); }); @@ -135,21 +138,21 @@ test.describe('Rich Media Broadcasts', () => { test('reorders media items', async ({ page }) => { await page.click('text=Create broadcast'); await page.getByLabel('Title').fill('Reorder Test'); - + // Add multiple media await page.click('text=Media'); await page.click('text=Add Media'); await page.getByPlaceholder('Media URL').fill('https://example.com/first.jpg'); await page.click('text=Add'); - + await page.click('text=Add Media'); await page.getByPlaceholder('Media URL').fill('https://example.com/second.jpg'); await page.click('text=Add'); - + // Reorder (move second to first) const secondItem = page.locator('[data-testid="media-item"]').nth(1); await secondItem.getByRole('button', { name: 'Move up' }).click(); - + // Verify order changed const items = page.locator('[data-testid="media-item"]'); await expect(items.first()).toContainText('second.jpg'); @@ -158,14 +161,14 @@ test.describe('Rich Media Broadcasts', () => { test('removes media from broadcast', async ({ page }) => { await page.click('text=Create broadcast'); await page.getByLabel('Title').fill('Remove Test'); - + // Add then remove media await page.click('text=Media'); await page.getByLabel('Image URL').fill('https://example.com/temp.jpg'); - + // Remove button should appear await page.getByRole('button', { name: 'Remove media' }).click(); - + // Verify removed await expect(page.getByLabel('Image URL')).toHaveValue(''); }); @@ -185,7 +188,7 @@ test.describe('User Dashboard Rich Media', () => { // Wait for banner with media to appear const banner = page.locator('[data-testid="broadcast-banner"]').first(); await expect(banner).toBeVisible({ timeout: 10000 }); - + // Check for media thumbnail const mediaThumb = banner.locator('img'); if (await mediaThumb.isVisible().catch(() => false)) { @@ -196,10 +199,10 @@ test.describe('User Dashboard Rich Media', () => { test('opens media lightbox on click', async ({ page }) => { const banner = page.locator('[data-testid="broadcast-banner"]').first(); await expect(banner).toBeVisible({ timeout: 10000 }); - + // Click on media await banner.locator('img').click(); - + // Lightbox should open await expect(page.locator('[data-testid="media-lightbox"]')).toBeVisible(); }); @@ -208,10 +211,10 @@ test.describe('User Dashboard Rich Media', () => { // Open broadcast with media const banner = page.locator('[data-testid="broadcast-banner"]').first(); await expect(banner).toBeVisible({ timeout: 10000 }); - + // Click to view media await banner.locator('img').click(); - + // Analytics event should fire await expect(page.getByText('Media viewed')).toBeVisible(); }); @@ -219,7 +222,7 @@ test.describe('User Dashboard Rich Media', () => { test('plays video in modal', async ({ page }) => { const banner = page.locator('[data-testid="broadcast-banner"]').first(); await expect(banner).toBeVisible({ timeout: 10000 }); - + // Check if banner has video const video = banner.locator('video'); if (await video.isVisible().catch(() => false)) { diff --git a/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx b/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx index c9a9b45a..58a9f728 100644 --- a/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx @@ -23,11 +23,23 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Progress } from '@/components/ui/progress'; -import type { CreateExperimentInput, ExperimentSuggestion } from '@/lib/experiments-types'; +import type { + AllocationStrategy, + CreateExperimentInput, + ExperimentSuggestion, + MetricType, + PrimaryMetric, +} from '@/lib/experiments-types'; const steps = [ { id: 'hypothesis', title: 'Hypothesis', icon: Lightbulb }, @@ -49,7 +61,13 @@ export default function NewExperimentPage() { description: '', hypothesis: '', variants: [ - { key: 'control', name: 'Control', description: 'Current implementation', isControl: true, flagConfig: {} }, + { + key: 'control', + name: 'Control', + description: 'Current implementation', + isControl: true, + flagConfig: {}, + }, { key: 'variant_a', name: 'Variant A', description: '', isControl: false, flagConfig: {} }, ], allocationStrategy: 'random', @@ -160,8 +178,8 @@ export default function NewExperimentPage() { isActive ? 'bg-primary text-white' : isCompleted - ? 'bg-green-100 text-green-600' - : 'bg-muted' + ? 'bg-green-100 text-green-600' + : 'bg-muted' }`} > @@ -191,18 +209,10 @@ export default function NewExperimentPage() { onApplySuggestion={applyAiSuggestion} /> )} - {currentStep === 1 && ( - - )} - {currentStep === 2 && ( - - )} - {currentStep === 3 && ( - - )} - {currentStep === 4 && ( - - )} + {currentStep === 1 && } + {currentStep === 2 && } + {currentStep === 3 && } + {currentStep === 4 && } @@ -312,9 +322,15 @@ function HypothesisStep({ {aiSuggestions.length > 0 ? (
{aiSuggestions.map((suggestion, index) => ( - onApplySuggestion(suggestion)}> + onApplySuggestion(suggestion)} + > -

{suggestion.hypothesis.primary}

+

+ {suggestion.hypothesis.primary} +

Impact: {suggestion.hypothesis.impactScore}/100 @@ -329,7 +345,8 @@ function HypothesisStep({
) : (

- Click "Load Suggestions" to see AI-generated experiment ideas based on your product usage patterns. + Click "Load Suggestions" to see AI-generated experiment ideas based on your + product usage patterns.

)}
@@ -383,7 +400,9 @@ function VariantsStep({
{variant.isControl ? ( - Control + + Control + ) : ( Variant )} @@ -429,7 +448,8 @@ function VariantsStep({ ))}

- You need at least 2 variants: a Control (current implementation) and at least one Treatment variant. + You need at least 2 variants: a Control (current implementation) and at least one Treatment + variant.

); @@ -488,7 +508,7 @@ function MetricsStep({ onValueChange={v => setFormData({ ...formData, - primaryMetric: { ...formData.primaryMetric!, type: v as any }, + primaryMetric: { ...formData.primaryMetric!, type: v as MetricType }, }) } > @@ -511,7 +531,10 @@ function MetricsStep({ onValueChange={v => setFormData({ ...formData, - primaryMetric: { ...formData.primaryMetric!, direction: v as any }, + primaryMetric: { + ...formData.primaryMetric!, + direction: v as PrimaryMetric['direction'], + }, }) } > @@ -536,7 +559,9 @@ function MetricsStep({