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'
}`}
>
{suggestion.hypothesis.primary}
++ {suggestion.hypothesis.primary} +
- 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.
)}- 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.