chore(admin-web): clear repo-wide lint errors
This commit is contained in:
parent
5e40cd1b6e
commit
631784e551
@ -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_EMAIL = 'admin@example.com';
|
||||||
const ADMIN_PASSWORD = 'Admin123!';
|
const ADMIN_PASSWORD = 'Admin123!';
|
||||||
|
|
||||||
async function loginAsAdmin(page: any) {
|
async function loginAsAdmin(page: Page) {
|
||||||
await page.goto('/login');
|
await page.goto('/login');
|
||||||
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
||||||
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
||||||
@ -25,11 +25,11 @@ test.describe('Broadcasts Admin', () => {
|
|||||||
|
|
||||||
test('shows broadcast list with filters', async ({ page }) => {
|
test('shows broadcast list with filters', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
|
|
||||||
// Check filter dropdowns exist
|
// Check filter dropdowns exist
|
||||||
await expect(page.getByLabel('Status')).toBeVisible();
|
await expect(page.getByLabel('Status')).toBeVisible();
|
||||||
await expect(page.getByLabel('Channel')).toBeVisible();
|
await expect(page.getByLabel('Channel')).toBeVisible();
|
||||||
|
|
||||||
// Check table headers
|
// Check table headers
|
||||||
await expect(page.getByText('Title')).toBeVisible();
|
await expect(page.getByText('Title')).toBeVisible();
|
||||||
await expect(page.getByText('Status')).toBeVisible();
|
await expect(page.getByText('Status')).toBeVisible();
|
||||||
@ -40,22 +40,22 @@ test.describe('Broadcasts Admin', () => {
|
|||||||
test('creates new broadcast draft', async ({ page }) => {
|
test('creates new broadcast draft', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
|
|
||||||
// Should navigate to create page
|
// Should navigate to create page
|
||||||
await expect(page).toHaveURL(/.*broadcasts\/new/);
|
await expect(page).toHaveURL(/.*broadcasts\/new/);
|
||||||
|
|
||||||
// Fill basic info
|
// Fill basic info
|
||||||
await page.getByLabel('Title').fill('Test Broadcast');
|
await page.getByLabel('Title').fill('Test Broadcast');
|
||||||
await page.getByLabel('Body').fill('This is a test broadcast message');
|
await page.getByLabel('Body').fill('This is a test broadcast message');
|
||||||
|
|
||||||
// Select channel
|
// Select channel
|
||||||
await page.getByLabel('Channel').selectOption('push');
|
await page.getByLabel('Channel').selectOption('push');
|
||||||
await page.getByLabel('Priority').selectOption('normal');
|
await page.getByLabel('Priority').selectOption('normal');
|
||||||
await page.getByLabel('Style').selectOption('banner');
|
await page.getByLabel('Style').selectOption('banner');
|
||||||
|
|
||||||
// Save as draft
|
// Save as draft
|
||||||
await page.click('text=Save Draft');
|
await page.click('text=Save Draft');
|
||||||
|
|
||||||
// Should show success and redirect
|
// Should show success and redirect
|
||||||
await expect(page.getByText('Broadcast saved')).toBeVisible({ timeout: 5000 });
|
await expect(page.getByText('Broadcast saved')).toBeVisible({ timeout: 5000 });
|
||||||
});
|
});
|
||||||
@ -63,44 +63,44 @@ test.describe('Broadcasts Admin', () => {
|
|||||||
test('creates broadcast with targeting', async ({ page }) => {
|
test('creates broadcast with targeting', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
|
|
||||||
// Basic info
|
// Basic info
|
||||||
await page.getByLabel('Title').fill('Targeted Broadcast');
|
await page.getByLabel('Title').fill('Targeted Broadcast');
|
||||||
await page.getByLabel('Body').fill('For pro users only');
|
await page.getByLabel('Body').fill('For pro users only');
|
||||||
|
|
||||||
// Go to targeting tab
|
// Go to targeting tab
|
||||||
await page.click('text=Targeting');
|
await page.click('text=Targeting');
|
||||||
|
|
||||||
// Select platforms
|
// Select platforms
|
||||||
await page.getByLabel('iOS').check();
|
await page.getByLabel('iOS').check();
|
||||||
await page.getByLabel('Android').check();
|
await page.getByLabel('Android').check();
|
||||||
|
|
||||||
// Select user segments
|
// Select user segments
|
||||||
await page.getByLabel('User Segments').selectOption(['pro']);
|
await page.getByLabel('User Segments').selectOption(['pro']);
|
||||||
|
|
||||||
// Set percentage rollout
|
// Set percentage rollout
|
||||||
await page.getByLabel('Percentage Rollout').fill('50');
|
await page.getByLabel('Percentage Rollout').fill('50');
|
||||||
|
|
||||||
// Go to schedule tab
|
// Go to schedule tab
|
||||||
await page.click('text=Schedule');
|
await page.click('text=Schedule');
|
||||||
await page.getByLabel('Send immediately').check();
|
await page.getByLabel('Send immediately').check();
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
await page.click('text=Send Broadcast');
|
await page.click('text=Send Broadcast');
|
||||||
|
|
||||||
// Confirm dialog
|
// Confirm dialog
|
||||||
await page.click('text=Confirm');
|
await page.click('text=Confirm');
|
||||||
|
|
||||||
await expect(page.getByText('Broadcast sent')).toBeVisible({ timeout: 5000 });
|
await expect(page.getByText('Broadcast sent')).toBeVisible({ timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clones existing broadcast', async ({ page }) => {
|
test('clones existing broadcast', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
|
|
||||||
// Find first broadcast row and click clone
|
// Find first broadcast row and click clone
|
||||||
const firstRow = page.locator('table tbody tr').first();
|
const firstRow = page.locator('table tbody tr').first();
|
||||||
await firstRow.getByRole('button', { name: 'Clone' }).click();
|
await firstRow.getByRole('button', { name: 'Clone' }).click();
|
||||||
|
|
||||||
// Should be on new broadcast page with pre-filled data
|
// Should be on new broadcast page with pre-filled data
|
||||||
await expect(page).toHaveURL(/.*broadcasts\/new/);
|
await expect(page).toHaveURL(/.*broadcasts\/new/);
|
||||||
await expect(page.getByLabel('Title')).not.toHaveValue('');
|
await expect(page.getByLabel('Title')).not.toHaveValue('');
|
||||||
@ -108,14 +108,14 @@ test.describe('Broadcasts Admin', () => {
|
|||||||
|
|
||||||
test('pauses and resumes broadcast', async ({ page }) => {
|
test('pauses and resumes broadcast', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
|
|
||||||
// Find a sending broadcast
|
// Find a sending broadcast
|
||||||
const sendingRow = page.locator('tr:has-text("Sending")').first();
|
const sendingRow = page.locator('tr:has-text("Sending")').first();
|
||||||
|
|
||||||
if (await sendingRow.isVisible().catch(() => false)) {
|
if (await sendingRow.isVisible().catch(() => false)) {
|
||||||
await sendingRow.getByRole('button', { name: 'Pause' }).click();
|
await sendingRow.getByRole('button', { name: 'Pause' }).click();
|
||||||
await expect(page.getByText('Broadcast paused')).toBeVisible();
|
await expect(page.getByText('Broadcast paused')).toBeVisible();
|
||||||
|
|
||||||
// Resume
|
// Resume
|
||||||
await sendingRow.getByRole('button', { name: 'Resume' }).click();
|
await sendingRow.getByRole('button', { name: 'Resume' }).click();
|
||||||
await expect(page.getByText('Broadcast resumed')).toBeVisible();
|
await expect(page.getByText('Broadcast resumed')).toBeVisible();
|
||||||
@ -124,10 +124,10 @@ test.describe('Broadcasts Admin', () => {
|
|||||||
|
|
||||||
test('views broadcast metrics', async ({ page }) => {
|
test('views broadcast metrics', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
|
|
||||||
// Click on first broadcast title
|
// Click on first broadcast title
|
||||||
await page.locator('table tbody tr td:first-child a').first().click();
|
await page.locator('table tbody tr td:first-child a').first().click();
|
||||||
|
|
||||||
// Should show detail page
|
// Should show detail page
|
||||||
await expect(page.getByText('Metrics')).toBeVisible();
|
await expect(page.getByText('Metrics')).toBeVisible();
|
||||||
await expect(page.getByText('Targeted')).toBeVisible();
|
await expect(page.getByText('Targeted')).toBeVisible();
|
||||||
@ -139,16 +139,16 @@ test.describe('Broadcasts Admin', () => {
|
|||||||
|
|
||||||
test('deletes broadcast', async ({ page }) => {
|
test('deletes broadcast', async ({ page }) => {
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
|
|
||||||
// Find a draft broadcast to delete
|
// Find a draft broadcast to delete
|
||||||
const draftRow = page.locator('tr:has-text("Draft")').first();
|
const draftRow = page.locator('tr:has-text("Draft")').first();
|
||||||
|
|
||||||
if (await draftRow.isVisible().catch(() => false)) {
|
if (await draftRow.isVisible().catch(() => false)) {
|
||||||
await draftRow.getByRole('button', { name: 'Delete' }).click();
|
await draftRow.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
// Confirm delete
|
// Confirm delete
|
||||||
await page.click('text=Confirm Delete');
|
await page.click('text=Confirm Delete');
|
||||||
|
|
||||||
await expect(page.getByText('Broadcast deleted')).toBeVisible();
|
await expect(page.getByText('Broadcast deleted')).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -167,7 +167,7 @@ test.describe('Surveys Admin', () => {
|
|||||||
|
|
||||||
test('shows survey list', async ({ page }) => {
|
test('shows survey list', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
|
|
||||||
// Check table headers
|
// Check table headers
|
||||||
await expect(page.getByText('Title')).toBeVisible();
|
await expect(page.getByText('Title')).toBeVisible();
|
||||||
await expect(page.getByText('Status')).toBeVisible();
|
await expect(page.getByText('Status')).toBeVisible();
|
||||||
@ -178,65 +178,65 @@ test.describe('Surveys Admin', () => {
|
|||||||
test('creates new survey with NPS question', async ({ page }) => {
|
test('creates new survey with NPS question', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
await page.click('text=Create survey');
|
await page.click('text=Create survey');
|
||||||
|
|
||||||
// Should navigate to builder page
|
// Should navigate to builder page
|
||||||
await expect(page).toHaveURL(/.*surveys\/new/);
|
await expect(page).toHaveURL(/.*surveys\/new/);
|
||||||
|
|
||||||
// Fill basic info
|
// Fill basic info
|
||||||
await page.getByLabel('Survey Title').fill('NPS Survey Test');
|
await page.getByLabel('Survey Title').fill('NPS Survey Test');
|
||||||
await page.getByLabel('Description').fill('How likely are you to recommend us?');
|
await page.getByLabel('Description').fill('How likely are you to recommend us?');
|
||||||
|
|
||||||
// Add NPS question
|
// Add NPS question
|
||||||
await page.click('text=Add Question');
|
await page.click('text=Add Question');
|
||||||
await page.selectOption('select[name="questionType"]', 'nps');
|
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.getByLabel('Question Text').fill('How likely are you to recommend us to a friend?');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Add multiple choice question
|
// Add multiple choice question
|
||||||
await page.click('text=Add Question');
|
await page.click('text=Add Question');
|
||||||
await page.selectOption('select[name="questionType"]', 'single_choice');
|
await page.selectOption('select[name="questionType"]', 'single_choice');
|
||||||
await page.getByLabel('Question Text').fill('What is your primary use case?');
|
await page.getByLabel('Question Text').fill('What is your primary use case?');
|
||||||
|
|
||||||
// Add options
|
// Add options
|
||||||
await page.click('text=Add Option');
|
await page.click('text=Add Option');
|
||||||
await page.getByPlaceholder('Option text').fill('Voice dictation');
|
await page.getByPlaceholder('Option text').fill('Voice dictation');
|
||||||
await page.click('text=Add Option');
|
await page.click('text=Add Option');
|
||||||
await page.getByPlaceholder('Option text').nth(1).fill('Keyboard');
|
await page.getByPlaceholder('Option text').nth(1).fill('Keyboard');
|
||||||
|
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Save survey
|
// Save survey
|
||||||
await page.click('text=Save Survey');
|
await page.click('text=Save Survey');
|
||||||
|
|
||||||
await expect(page.getByText('Survey saved')).toBeVisible({ timeout: 5000 });
|
await expect(page.getByText('Survey saved')).toBeVisible({ timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates survey with conditional logic', async ({ page }) => {
|
test('creates survey with conditional logic', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
await page.click('text=Create survey');
|
await page.click('text=Create survey');
|
||||||
|
|
||||||
// Basic info
|
// Basic info
|
||||||
await page.getByLabel('Survey Title').fill('Conditional Survey');
|
await page.getByLabel('Survey Title').fill('Conditional Survey');
|
||||||
|
|
||||||
// Add first question (NPS)
|
// Add first question (NPS)
|
||||||
await page.click('text=Add Question');
|
await page.click('text=Add Question');
|
||||||
await page.selectOption('select[name="questionType"]', 'nps');
|
await page.selectOption('select[name="questionType"]', 'nps');
|
||||||
await page.getByLabel('Question Text').fill('Rate your experience');
|
await page.getByLabel('Question Text').fill('Rate your experience');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Add conditional follow-up question
|
// Add conditional follow-up question
|
||||||
await page.click('text=Add Question');
|
await page.click('text=Add Question');
|
||||||
await page.selectOption('select[name="questionType"]', 'text_long');
|
await page.selectOption('select[name="questionType"]', 'text_long');
|
||||||
await page.getByLabel('Question Text').fill('What can we improve?');
|
await page.getByLabel('Question Text').fill('What can we improve?');
|
||||||
|
|
||||||
// Set conditional logic
|
// Set conditional logic
|
||||||
await page.click('text=Conditional Logic');
|
await page.click('text=Conditional Logic');
|
||||||
await page.selectOption('select[name="conditionQuestion"]', 'q1');
|
await page.selectOption('select[name="conditionQuestion"]', 'q1');
|
||||||
await page.selectOption('select[name="conditionOperator"]', 'not_equals');
|
await page.selectOption('select[name="conditionOperator"]', 'not_equals');
|
||||||
await page.fill('input[name="conditionValue"]', '9,10');
|
await page.fill('input[name="conditionValue"]', '9,10');
|
||||||
|
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
await page.click('text=Save Survey');
|
await page.click('text=Save Survey');
|
||||||
await expect(page.getByText('Survey saved')).toBeVisible();
|
await expect(page.getByText('Survey saved')).toBeVisible();
|
||||||
@ -244,15 +244,15 @@ test.describe('Surveys Admin', () => {
|
|||||||
|
|
||||||
test('activates and pauses survey', async ({ page }) => {
|
test('activates and pauses survey', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
|
|
||||||
// Find a draft survey
|
// Find a draft survey
|
||||||
const draftRow = page.locator('tr:has-text("Draft")').first();
|
const draftRow = page.locator('tr:has-text("Draft")').first();
|
||||||
|
|
||||||
if (await draftRow.isVisible().catch(() => false)) {
|
if (await draftRow.isVisible().catch(() => false)) {
|
||||||
// Click activate
|
// Click activate
|
||||||
await draftRow.getByRole('button', { name: 'Activate' }).click();
|
await draftRow.getByRole('button', { name: 'Activate' }).click();
|
||||||
await expect(page.getByText('Survey activated')).toBeVisible();
|
await expect(page.getByText('Survey activated')).toBeVisible();
|
||||||
|
|
||||||
// Find the now-active survey and pause it
|
// Find the now-active survey and pause it
|
||||||
const activeRow = page.locator('tr:has-text("Active")').first();
|
const activeRow = page.locator('tr:has-text("Active")').first();
|
||||||
await activeRow.getByRole('button', { name: 'Pause' }).click();
|
await activeRow.getByRole('button', { name: 'Pause' }).click();
|
||||||
@ -262,16 +262,16 @@ test.describe('Surveys Admin', () => {
|
|||||||
|
|
||||||
test('views survey metrics', async ({ page }) => {
|
test('views survey metrics', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
|
|
||||||
// Click on first survey
|
// Click on first survey
|
||||||
await page.locator('table tbody tr td:first-child a').first().click();
|
await page.locator('table tbody tr td:first-child a').first().click();
|
||||||
|
|
||||||
// Should show detail page with tabs
|
// Should show detail page with tabs
|
||||||
await expect(page.getByText('Overview')).toBeVisible();
|
await expect(page.getByText('Overview')).toBeVisible();
|
||||||
await expect(page.getByText('Questions')).toBeVisible();
|
await expect(page.getByText('Questions')).toBeVisible();
|
||||||
await expect(page.getByText('Responses')).toBeVisible();
|
await expect(page.getByText('Responses')).toBeVisible();
|
||||||
await expect(page.getByText('Analytics')).toBeVisible();
|
await expect(page.getByText('Analytics')).toBeVisible();
|
||||||
|
|
||||||
// Click Analytics tab
|
// Click Analytics tab
|
||||||
await page.click('text=Analytics');
|
await page.click('text=Analytics');
|
||||||
await expect(page.getByText('Completion Rate')).toBeVisible();
|
await expect(page.getByText('Completion Rate')).toBeVisible();
|
||||||
@ -280,16 +280,16 @@ test.describe('Surveys Admin', () => {
|
|||||||
|
|
||||||
test('exports survey responses', async ({ page }) => {
|
test('exports survey responses', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
|
|
||||||
// Click on first survey
|
// Click on first survey
|
||||||
await page.locator('table tbody tr td:first-child a').first().click();
|
await page.locator('table tbody tr td:first-child a').first().click();
|
||||||
|
|
||||||
// Go to Responses tab
|
// Go to Responses tab
|
||||||
await page.click('text=Responses');
|
await page.click('text=Responses');
|
||||||
|
|
||||||
// Click export
|
// Click export
|
||||||
await page.click('text=Export');
|
await page.click('text=Export');
|
||||||
|
|
||||||
// Wait for download or success message
|
// Wait for download or success message
|
||||||
await expect(page.getByText('Export started')).toBeVisible();
|
await expect(page.getByText('Export started')).toBeVisible();
|
||||||
});
|
});
|
||||||
@ -297,25 +297,25 @@ test.describe('Surveys Admin', () => {
|
|||||||
test('sets survey incentive', async ({ page }) => {
|
test('sets survey incentive', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
await page.click('text=Create survey');
|
await page.click('text=Create survey');
|
||||||
|
|
||||||
// Basic info
|
// Basic info
|
||||||
await page.getByLabel('Survey Title').fill('Incentivized Survey');
|
await page.getByLabel('Survey Title').fill('Incentivized Survey');
|
||||||
|
|
||||||
// Go to Settings tab
|
// Go to Settings tab
|
||||||
await page.click('text=Settings');
|
await page.click('text=Settings');
|
||||||
|
|
||||||
// Enable incentive
|
// Enable incentive
|
||||||
await page.getByLabel('Enable Incentive').check();
|
await page.getByLabel('Enable Incentive').check();
|
||||||
await page.selectOption('select[name="incentiveType"]', 'pro_days');
|
await page.selectOption('select[name="incentiveType"]', 'pro_days');
|
||||||
await page.getByLabel('Incentive Amount').fill('7');
|
await page.getByLabel('Incentive Amount').fill('7');
|
||||||
|
|
||||||
// Add question
|
// Add question
|
||||||
await page.click('text=Questions');
|
await page.click('text=Questions');
|
||||||
await page.click('text=Add Question');
|
await page.click('text=Add Question');
|
||||||
await page.selectOption('select[name="questionType"]', 'nps');
|
await page.selectOption('select[name="questionType"]', 'nps');
|
||||||
await page.getByLabel('Question Text').fill('How likely are you to recommend us?');
|
await page.getByLabel('Question Text').fill('How likely are you to recommend us?');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
await page.click('text=Save Survey');
|
await page.click('text=Save Survey');
|
||||||
await expect(page.getByText('Survey saved')).toBeVisible();
|
await expect(page.getByText('Survey saved')).toBeVisible();
|
||||||
@ -324,31 +324,31 @@ test.describe('Surveys Admin', () => {
|
|||||||
test('configures survey targeting', async ({ page }) => {
|
test('configures survey targeting', async ({ page }) => {
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
await page.click('text=Create survey');
|
await page.click('text=Create survey');
|
||||||
|
|
||||||
// Basic info
|
// Basic info
|
||||||
await page.getByLabel('Survey Title').fill('Targeted Survey');
|
await page.getByLabel('Survey Title').fill('Targeted Survey');
|
||||||
|
|
||||||
// Go to Targeting tab
|
// Go to Targeting tab
|
||||||
await page.click('text=Targeting');
|
await page.click('text=Targeting');
|
||||||
|
|
||||||
// Select platforms
|
// Select platforms
|
||||||
await page.getByLabel('iOS').check();
|
await page.getByLabel('iOS').check();
|
||||||
await page.getByLabel('Android').check();
|
await page.getByLabel('Android').check();
|
||||||
|
|
||||||
// Select user segments
|
// Select user segments
|
||||||
await page.getByLabel('Active Users').check();
|
await page.getByLabel('Active Users').check();
|
||||||
await page.getByLabel('Pro Users').check();
|
await page.getByLabel('Pro Users').check();
|
||||||
|
|
||||||
// Set rollout
|
// Set rollout
|
||||||
await page.getByLabel('Percentage Rollout').fill('25');
|
await page.getByLabel('Percentage Rollout').fill('25');
|
||||||
|
|
||||||
// Add question
|
// Add question
|
||||||
await page.click('text=Questions');
|
await page.click('text=Questions');
|
||||||
await page.click('text=Add Question');
|
await page.click('text=Add Question');
|
||||||
await page.selectOption('select[name="questionType"]', 'rating');
|
await page.selectOption('select[name="questionType"]', 'rating');
|
||||||
await page.getByLabel('Question Text').fill('Rate the app');
|
await page.getByLabel('Question Text').fill('Rate the app');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
await page.click('text=Save Survey');
|
await page.click('text=Save Survey');
|
||||||
await expect(page.getByText('Survey saved')).toBeVisible();
|
await expect(page.getByText('Survey saved')).toBeVisible();
|
||||||
@ -369,11 +369,11 @@ test.describe('Broadcast & Survey Integration', () => {
|
|||||||
// Go to broadcasts
|
// Go to broadcasts
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
await expect(page.getByText('Create broadcast')).toBeVisible();
|
await expect(page.getByText('Create broadcast')).toBeVisible();
|
||||||
|
|
||||||
// Go to surveys
|
// Go to surveys
|
||||||
await page.click('text=Surveys');
|
await page.click('text=Surveys');
|
||||||
await expect(page.getByText('Create survey')).toBeVisible();
|
await expect(page.getByText('Create survey')).toBeVisible();
|
||||||
|
|
||||||
// Back to broadcasts
|
// Back to broadcasts
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
await expect(page.getByText('Create broadcast')).toBeVisible();
|
await expect(page.getByText('Create broadcast')).toBeVisible();
|
||||||
|
|||||||
@ -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_EMAIL = 'admin@example.com';
|
||||||
const ADMIN_PASSWORD = 'Admin123!';
|
const ADMIN_PASSWORD = 'Admin123!';
|
||||||
|
|
||||||
async function loginAsAdmin(page: any) {
|
async function loginAsAdmin(page: Page) {
|
||||||
await page.goto('/login');
|
await page.goto('/login');
|
||||||
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
||||||
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
||||||
@ -24,11 +24,11 @@ test.describe('Diagnostics - Debug Sessions', () => {
|
|||||||
|
|
||||||
test('shows session list with filters', async ({ page }) => {
|
test('shows session list with filters', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Check filter dropdowns exist
|
// Check filter dropdowns exist
|
||||||
await expect(page.getByLabel('Status')).toBeVisible();
|
await expect(page.getByLabel('Status')).toBeVisible();
|
||||||
await expect(page.getByLabel('Collection Level')).toBeVisible();
|
await expect(page.getByLabel('Collection Level')).toBeVisible();
|
||||||
|
|
||||||
// Check table headers
|
// Check table headers
|
||||||
await expect(page.getByText('Session ID')).toBeVisible();
|
await expect(page.getByText('Session ID')).toBeVisible();
|
||||||
await expect(page.getByText('Status')).toBeVisible();
|
await expect(page.getByText('Status')).toBeVisible();
|
||||||
@ -41,38 +41,38 @@ test.describe('Diagnostics - Debug Sessions', () => {
|
|||||||
test('creates new debug session', async ({ page }) => {
|
test('creates new debug session', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
await page.click('text=Create Session');
|
await page.click('text=Create Session');
|
||||||
|
|
||||||
// Should open modal
|
// Should open modal
|
||||||
await expect(page.getByText('Create Debug Session')).toBeVisible();
|
await expect(page.getByText('Create Debug Session')).toBeVisible();
|
||||||
|
|
||||||
// Fill form
|
// Fill form
|
||||||
await page.getByLabel('Target User ID').fill('user_test_123');
|
await page.getByLabel('Target User ID').fill('user_test_123');
|
||||||
await page.getByLabel('Target Device ID').fill('device_test_456');
|
await page.getByLabel('Target Device ID').fill('device_test_456');
|
||||||
|
|
||||||
// Select collection level
|
// Select collection level
|
||||||
await page.getByLabel('Collection Level').selectOption('debug');
|
await page.getByLabel('Collection Level').selectOption('debug');
|
||||||
|
|
||||||
// Enable capture options
|
// Enable capture options
|
||||||
await page.getByLabel('Capture Logs').check();
|
await page.getByLabel('Capture Logs').check();
|
||||||
await page.getByLabel('Capture Network').check();
|
await page.getByLabel('Capture Network').check();
|
||||||
await page.getByLabel('Capture Screenshots').check();
|
await page.getByLabel('Capture Screenshots').check();
|
||||||
|
|
||||||
// Set duration
|
// Set duration
|
||||||
await page.getByLabel('Max Duration (minutes)').fill('30');
|
await page.getByLabel('Max Duration (minutes)').fill('30');
|
||||||
|
|
||||||
// Submit
|
// Submit
|
||||||
await page.click('text=Start Session');
|
await page.click('text=Start Session');
|
||||||
|
|
||||||
// Should show success and close modal
|
// Should show success and close modal
|
||||||
await expect(page.getByText('Session created')).toBeVisible({ timeout: 5000 });
|
await expect(page.getByText('Session created')).toBeVisible({ timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('views session details', async ({ page }) => {
|
test('views session details', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Click on first session
|
// Click on first session
|
||||||
await page.locator('table tbody tr').first().click();
|
await page.locator('table tbody tr').first().click();
|
||||||
|
|
||||||
// Should show detail panel
|
// Should show detail panel
|
||||||
await expect(page.getByText('Session Details')).toBeVisible();
|
await expect(page.getByText('Session Details')).toBeVisible();
|
||||||
await expect(page.getByText('Status')).toBeVisible();
|
await expect(page.getByText('Status')).toBeVisible();
|
||||||
@ -84,48 +84,52 @@ test.describe('Diagnostics - Debug Sessions', () => {
|
|||||||
|
|
||||||
test('pauses and resumes session', async ({ page }) => {
|
test('pauses and resumes session', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Find an active session
|
// Find an active session
|
||||||
const activeRow = page.locator('tr:has-text("Active")').first();
|
const activeRow = page.locator('tr:has-text("Active")').first();
|
||||||
|
|
||||||
if (await activeRow.isVisible().catch(() => false)) {
|
if (await activeRow.isVisible().catch(() => false)) {
|
||||||
// Click pause
|
// Click pause
|
||||||
await activeRow.getByRole('button', { name: 'Pause' }).click();
|
await activeRow.getByRole('button', { name: 'Pause' }).click();
|
||||||
await expect(page.getByText('Session paused')).toBeVisible();
|
await expect(page.getByText('Session paused')).toBeVisible();
|
||||||
|
|
||||||
// Resume
|
// 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();
|
await expect(page.getByText('Session resumed')).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cancels session', async ({ page }) => {
|
test('cancels session', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Find a pending or active session
|
// Find a pending or active session
|
||||||
const sessionRow = page.locator('tr:has-text("Pending"), tr:has-text("Active")').first();
|
const sessionRow = page.locator('tr:has-text("Pending"), tr:has-text("Active")').first();
|
||||||
|
|
||||||
if (await sessionRow.isVisible().catch(() => false)) {
|
if (await sessionRow.isVisible().catch(() => false)) {
|
||||||
await sessionRow.getByRole('button', { name: 'Cancel' }).click();
|
await sessionRow.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
// Confirm cancel
|
// Confirm cancel
|
||||||
await page.getByLabel('Reason (optional)').fill('Test cancellation');
|
await page.getByLabel('Reason (optional)').fill('Test cancellation');
|
||||||
await page.click('text=Confirm Cancel');
|
await page.click('text=Confirm Cancel');
|
||||||
|
|
||||||
await expect(page.getByText('Session cancelled')).toBeVisible();
|
await expect(page.getByText('Session cancelled')).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filters sessions by status', async ({ page }) => {
|
test('filters sessions by status', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Filter by Active
|
// Filter by Active
|
||||||
await page.getByLabel('Status').selectOption('active');
|
await page.getByLabel('Status').selectOption('active');
|
||||||
|
|
||||||
// Should only show active sessions
|
// Should only show active sessions
|
||||||
const rows = page.locator('table tbody tr');
|
const rows = page.locator('table tbody tr');
|
||||||
const count = await rows.count();
|
const count = await rows.count();
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
await expect(rows.nth(i).locator('td').nth(1)).toHaveText('Active');
|
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 }) => {
|
test('searches sessions by user ID', async ({ page }) => {
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
await page.getByPlaceholder('Search by user/device ID').fill('test_user');
|
await page.getByPlaceholder('Search by user/device ID').fill('test_user');
|
||||||
await page.click('text=Search');
|
await page.click('text=Search');
|
||||||
|
|
||||||
// Results should contain search term
|
// Results should contain search term
|
||||||
const firstRow = page.locator('table tbody tr').first();
|
const firstRow = page.locator('table tbody tr').first();
|
||||||
if (await firstRow.isVisible().catch(() => false)) {
|
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();
|
const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first();
|
||||||
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
||||||
await sessionWithLogs.click();
|
await sessionWithLogs.click();
|
||||||
|
|
||||||
// Click Logs tab
|
// Click Logs tab
|
||||||
await page.click('text=Logs');
|
await page.click('text=Logs');
|
||||||
|
|
||||||
// Should show log entries
|
// Should show log entries
|
||||||
await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible();
|
await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible();
|
||||||
|
|
||||||
// Check log level badges
|
// 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)) {
|
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
||||||
await sessionWithLogs.click();
|
await sessionWithLogs.click();
|
||||||
await page.click('text=Logs');
|
await page.click('text=Logs');
|
||||||
|
|
||||||
// Filter by ERROR
|
// Filter by ERROR
|
||||||
await page.getByLabel('Log Level').selectOption('error');
|
await page.getByLabel('Log Level').selectOption('error');
|
||||||
|
|
||||||
// All visible logs should be ERROR level
|
// All visible logs should be ERROR level
|
||||||
const logs = page.locator('[data-testid="log-entry"]');
|
const logs = page.locator('[data-testid="log-entry"]');
|
||||||
const count = await logs.count();
|
const count = await logs.count();
|
||||||
@ -193,11 +199,11 @@ test.describe('Diagnostics - Logs & Traces', () => {
|
|||||||
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
||||||
await sessionWithLogs.click();
|
await sessionWithLogs.click();
|
||||||
await page.click('text=Logs');
|
await page.click('text=Logs');
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
await page.getByPlaceholder('Search logs...').fill('error');
|
await page.getByPlaceholder('Search logs...').fill('error');
|
||||||
await page.click('text=Search');
|
await page.click('text=Search');
|
||||||
|
|
||||||
// Results should contain search term
|
// Results should contain search term
|
||||||
const firstLog = page.locator('[data-testid="log-entry"]').first();
|
const firstLog = page.locator('[data-testid="log-entry"]').first();
|
||||||
if (await firstLog.isVisible().catch(() => false)) {
|
if (await firstLog.isVisible().catch(() => false)) {
|
||||||
@ -211,10 +217,10 @@ test.describe('Diagnostics - Logs & Traces', () => {
|
|||||||
if (await sessionWithTraces.isVisible().catch(() => false)) {
|
if (await sessionWithTraces.isVisible().catch(() => false)) {
|
||||||
await sessionWithTraces.click();
|
await sessionWithTraces.click();
|
||||||
await page.click('text=Traces');
|
await page.click('text=Traces');
|
||||||
|
|
||||||
// Should show trace tree
|
// Should show trace tree
|
||||||
await expect(page.locator('[data-testid="trace-span"]').first()).toBeVisible();
|
await expect(page.locator('[data-testid="trace-span"]').first()).toBeVisible();
|
||||||
|
|
||||||
// Should show span details
|
// Should show span details
|
||||||
await expect(page.getByText('Duration')).toBeVisible();
|
await expect(page.getByText('Duration')).toBeVisible();
|
||||||
await expect(page.getByText('Status')).toBeVisible();
|
await expect(page.getByText('Status')).toBeVisible();
|
||||||
@ -226,11 +232,11 @@ test.describe('Diagnostics - Logs & Traces', () => {
|
|||||||
if (await sessionWithTraces.isVisible().catch(() => false)) {
|
if (await sessionWithTraces.isVisible().catch(() => false)) {
|
||||||
await sessionWithTraces.click();
|
await sessionWithTraces.click();
|
||||||
await page.click('text=Traces');
|
await page.click('text=Traces');
|
||||||
|
|
||||||
// Click to expand first span
|
// Click to expand first span
|
||||||
const firstSpan = page.locator('[data-testid="trace-span"]').first();
|
const firstSpan = page.locator('[data-testid="trace-span"]').first();
|
||||||
await firstSpan.click();
|
await firstSpan.click();
|
||||||
|
|
||||||
// Should show span details
|
// Should show span details
|
||||||
await expect(page.getByText('Attributes')).toBeVisible();
|
await expect(page.getByText('Attributes')).toBeVisible();
|
||||||
await expect(page.getByText('Events')).toBeVisible();
|
await expect(page.getByText('Events')).toBeVisible();
|
||||||
@ -249,7 +255,7 @@ test.describe('Diagnostics - Screenshots', () => {
|
|||||||
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
||||||
await sessionWithScreenshots.click();
|
await sessionWithScreenshots.click();
|
||||||
await page.click('text=Screenshots');
|
await page.click('text=Screenshots');
|
||||||
|
|
||||||
// Should show screenshot thumbnails
|
// Should show screenshot thumbnails
|
||||||
await expect(page.locator('[data-testid="screenshot-thumb"]').first()).toBeVisible();
|
await expect(page.locator('[data-testid="screenshot-thumb"]').first()).toBeVisible();
|
||||||
}
|
}
|
||||||
@ -260,10 +266,10 @@ test.describe('Diagnostics - Screenshots', () => {
|
|||||||
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
||||||
await sessionWithScreenshots.click();
|
await sessionWithScreenshots.click();
|
||||||
await page.click('text=Screenshots');
|
await page.click('text=Screenshots');
|
||||||
|
|
||||||
// Click first screenshot
|
// Click first screenshot
|
||||||
await page.locator('[data-testid="screenshot-thumb"]').first().click();
|
await page.locator('[data-testid="screenshot-thumb"]').first().click();
|
||||||
|
|
||||||
// Lightbox should open
|
// Lightbox should open
|
||||||
await expect(page.locator('[data-testid="screenshot-lightbox"]')).toBeVisible();
|
await expect(page.locator('[data-testid="screenshot-lightbox"]')).toBeVisible();
|
||||||
await expect(page.locator('[data-testid="screenshot-full"]').first()).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)) {
|
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
||||||
await sessionWithScreenshots.click();
|
await sessionWithScreenshots.click();
|
||||||
await page.click('text=Screenshots');
|
await page.click('text=Screenshots');
|
||||||
|
|
||||||
// Open lightbox
|
// Open lightbox
|
||||||
await page.locator('[data-testid="screenshot-thumb"]').first().click();
|
await page.locator('[data-testid="screenshot-thumb"]').first().click();
|
||||||
|
|
||||||
// Navigate next
|
// Navigate next
|
||||||
await page.click('text=Next');
|
await page.click('text=Next');
|
||||||
await expect(page.getByText('2 /')).toBeVisible();
|
await expect(page.getByText('2 /')).toBeVisible();
|
||||||
|
|
||||||
// Navigate prev
|
// Navigate prev
|
||||||
await page.click('text=Previous');
|
await page.click('text=Previous');
|
||||||
await expect(page.getByText('1 /')).toBeVisible();
|
await expect(page.getByText('1 /')).toBeVisible();
|
||||||
@ -299,30 +305,30 @@ test.describe('Diagnostics - End-to-End Flow', () => {
|
|||||||
// 1. Create session
|
// 1. Create session
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
await page.click('text=Create Session');
|
await page.click('text=Create Session');
|
||||||
|
|
||||||
const targetUserId = `test_user_${Date.now()}`;
|
const targetUserId = `test_user_${Date.now()}`;
|
||||||
await page.getByLabel('Target User ID').fill(targetUserId);
|
await page.getByLabel('Target User ID').fill(targetUserId);
|
||||||
await page.getByLabel('Collection Level').selectOption('debug');
|
await page.getByLabel('Collection Level').selectOption('debug');
|
||||||
await page.getByLabel('Capture Logs').check();
|
await page.getByLabel('Capture Logs').check();
|
||||||
await page.click('text=Start Session');
|
await page.click('text=Start Session');
|
||||||
|
|
||||||
await expect(page.getByText('Session created')).toBeVisible();
|
await expect(page.getByText('Session created')).toBeVisible();
|
||||||
|
|
||||||
// 2. Verify session appears in list
|
// 2. Verify session appears in list
|
||||||
await expect(page.locator('table tbody tr').first()).toContainText(targetUserId);
|
await expect(page.locator('table tbody tr').first()).toContainText(targetUserId);
|
||||||
|
|
||||||
// 3. Open session details
|
// 3. Open session details
|
||||||
await page.locator(`tr:has-text("${targetUserId}")`).first().click();
|
await page.locator(`tr:has-text("${targetUserId}")`).first().click();
|
||||||
await expect(page.getByText('Session Details')).toBeVisible();
|
await expect(page.getByText('Session Details')).toBeVisible();
|
||||||
|
|
||||||
// 4. View logs (if any captured)
|
// 4. View logs (if any captured)
|
||||||
await page.click('text=Logs');
|
await page.click('text=Logs');
|
||||||
|
|
||||||
// 5. Pause session
|
// 5. Pause session
|
||||||
await page.click('text=Actions');
|
await page.click('text=Actions');
|
||||||
await page.click('text=Pause');
|
await page.click('text=Pause');
|
||||||
await expect(page.getByText('Session paused')).toBeVisible();
|
await expect(page.getByText('Session paused')).toBeVisible();
|
||||||
|
|
||||||
// 6. Resume and complete
|
// 6. Resume and complete
|
||||||
await page.click('text=Actions');
|
await page.click('text=Actions');
|
||||||
await page.click('text=Resume');
|
await page.click('text=Resume');
|
||||||
@ -335,12 +341,12 @@ test.describe('Diagnostics - End-to-End Flow', () => {
|
|||||||
// This would require backend simulation of error threshold
|
// This would require backend simulation of error threshold
|
||||||
// For E2E, we verify the UI handles the notification
|
// For E2E, we verify the UI handles the notification
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Look for notification about high error rate
|
// Look for notification about high error rate
|
||||||
const notification = page.locator('[data-testid="error-threshold-alert"]');
|
const notification = page.locator('[data-testid="error-threshold-alert"]');
|
||||||
if (await notification.isVisible().catch(() => false)) {
|
if (await notification.isVisible().catch(() => false)) {
|
||||||
await expect(notification).toContainText('High error rate detected');
|
await expect(notification).toContainText('High error rate detected');
|
||||||
|
|
||||||
// Should offer to create debug session
|
// Should offer to create debug session
|
||||||
await notification.getByRole('button', { name: 'Start Debug Session' }).click();
|
await notification.getByRole('button', { name: 'Start Debug Session' }).click();
|
||||||
await expect(page.getByText('Create Debug Session')).toBeVisible();
|
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 }) => {
|
test('crash-triggered auto-session', async ({ page }) => {
|
||||||
// Look for auto-created session after crash
|
// Look for auto-created session after crash
|
||||||
await page.click('text=Diagnostics');
|
await page.click('text=Diagnostics');
|
||||||
|
|
||||||
// Filter to show auto-created sessions
|
// Filter to show auto-created sessions
|
||||||
await page.getByLabel('Source').selectOption('auto_crash');
|
await page.getByLabel('Source').selectOption('auto_crash');
|
||||||
|
|
||||||
// Should show auto-created sessions
|
// Should show auto-created sessions
|
||||||
const autoSession = page.locator('tr:has-text("Auto-created")').first();
|
const autoSession = page.locator('tr:has-text("Auto-created")').first();
|
||||||
if (await autoSession.isVisible().catch(() => false)) {
|
if (await autoSession.isVisible().catch(() => false)) {
|
||||||
await expect(autoSession).toContainText('Crash detected');
|
await expect(autoSession).toContainText('Crash detected');
|
||||||
|
|
||||||
// Open and verify it has crash data
|
// Open and verify it has crash data
|
||||||
await autoSession.click();
|
await autoSession.click();
|
||||||
await page.click('text=Logs');
|
await page.click('text=Logs');
|
||||||
|
|||||||
@ -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_EMAIL = 'admin@example.com';
|
||||||
const ADMIN_PASSWORD = 'Admin123!';
|
const ADMIN_PASSWORD = 'Admin123!';
|
||||||
|
|
||||||
async function loginAsAdmin(page: any) {
|
async function loginAsAdmin(page: Page) {
|
||||||
await page.goto('/login');
|
await page.goto('/login');
|
||||||
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
||||||
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
||||||
@ -19,15 +19,15 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
|
|
||||||
test('creates broadcast with single image', async ({ page }) => {
|
test('creates broadcast with single image', async ({ page }) => {
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
|
|
||||||
// Fill basic info
|
// Fill basic info
|
||||||
await page.getByLabel('Title').fill('Image Broadcast Test');
|
await page.getByLabel('Title').fill('Image Broadcast Test');
|
||||||
await page.getByLabel('Body').fill('Check out this image!');
|
await page.getByLabel('Body').fill('Check out this image!');
|
||||||
|
|
||||||
// Add image
|
// Add image
|
||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
await page.getByLabel('Image URL').fill('https://example.com/image.jpg');
|
await page.getByLabel('Image URL').fill('https://example.com/image.jpg');
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
await page.click('text=Save Draft');
|
await page.click('text=Save Draft');
|
||||||
await expect(page.getByText('Broadcast saved')).toBeVisible();
|
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 }) => {
|
test('creates broadcast with multiple media items', async ({ page }) => {
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
|
|
||||||
await page.getByLabel('Title').fill('Gallery Broadcast');
|
await page.getByLabel('Title').fill('Gallery Broadcast');
|
||||||
await page.getByLabel('Body').fill('Multiple images and video');
|
await page.getByLabel('Body').fill('Multiple images and video');
|
||||||
|
|
||||||
// Go to media tab
|
// Go to media tab
|
||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
|
|
||||||
// Add first image
|
// Add first image
|
||||||
await page.click('text=Add Media');
|
await page.click('text=Add Media');
|
||||||
await page.getByPlaceholder('Media URL').fill('https://example.com/photo1.jpg');
|
await page.getByPlaceholder('Media URL').fill('https://example.com/photo1.jpg');
|
||||||
await page.selectOption('select[name="mediaType"]', 'image');
|
await page.selectOption('select[name="mediaType"]', 'image');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Add video
|
// Add video
|
||||||
await page.click('text=Add Media');
|
await page.click('text=Add Media');
|
||||||
await page.getByPlaceholder('Media URL').fill('https://example.com/video.mp4');
|
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.getByPlaceholder('Thumbnail URL').fill('https://example.com/thumb.jpg');
|
||||||
await page.fill('input[name="duration"]', '120');
|
await page.fill('input[name="duration"]', '120');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Verify media list
|
// Verify media list
|
||||||
await expect(page.getByText('photo1.jpg')).toBeVisible();
|
await expect(page.getByText('photo1.jpg')).toBeVisible();
|
||||||
await expect(page.getByText('video.mp4')).toBeVisible();
|
await expect(page.getByText('video.mp4')).toBeVisible();
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
await page.click('text=Save Draft');
|
await page.click('text=Save Draft');
|
||||||
await expect(page.getByText('Broadcast saved')).toBeVisible();
|
await expect(page.getByText('Broadcast saved')).toBeVisible();
|
||||||
@ -72,11 +72,14 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
await page.getByLabel('Image URL').fill('https://example.com/preview.jpg');
|
await page.getByLabel('Image URL').fill('https://example.com/preview.jpg');
|
||||||
await page.click('text=Save Draft');
|
await page.click('text=Save Draft');
|
||||||
|
|
||||||
// Go back to list and open preview
|
// Go back to list and open preview
|
||||||
await page.click('text=Broadcasts');
|
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
|
// Verify media in preview modal
|
||||||
await expect(page.locator('img[src*="preview.jpg"]')).toBeVisible();
|
await expect(page.locator('img[src*="preview.jpg"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
@ -85,10 +88,10 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
// Find a sent broadcast with media
|
// Find a sent broadcast with media
|
||||||
await page.click('text=Broadcasts');
|
await page.click('text=Broadcasts');
|
||||||
const mediaRow = page.locator('tr:has-text("Gallery Broadcast")').first();
|
const mediaRow = page.locator('tr:has-text("Gallery Broadcast")').first();
|
||||||
|
|
||||||
if (await mediaRow.isVisible().catch(() => false)) {
|
if (await mediaRow.isVisible().catch(() => false)) {
|
||||||
await mediaRow.getByText('Analytics').click();
|
await mediaRow.getByText('Analytics').click();
|
||||||
|
|
||||||
// Check media metrics
|
// Check media metrics
|
||||||
await expect(page.getByText('Media Views')).toBeVisible();
|
await expect(page.getByText('Media Views')).toBeVisible();
|
||||||
await expect(page.getByText('Media Completions')).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 }) => {
|
test('uploads media via blob storage', async ({ page }) => {
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
await page.getByLabel('Title').fill('Upload Test');
|
await page.getByLabel('Title').fill('Upload Test');
|
||||||
|
|
||||||
// Go to media tab
|
// Go to media tab
|
||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
|
|
||||||
// Upload file
|
// Upload file
|
||||||
const fileInput = page.locator('input[type="file"]');
|
const fileInput = page.locator('input[type="file"]');
|
||||||
await fileInput.setInputFiles({
|
await fileInput.setInputFiles({
|
||||||
@ -110,11 +113,11 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
buffer: Buffer.from('fake-image-data'),
|
buffer: Buffer.from('fake-image-data'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for upload
|
// Wait for upload
|
||||||
await expect(page.getByText('Uploading...')).toBeVisible();
|
await expect(page.getByText('Uploading...')).toBeVisible();
|
||||||
await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 10000 });
|
await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// Verify uploaded media appears
|
// Verify uploaded media appears
|
||||||
await expect(page.getByText('test-image.png')).toBeVisible();
|
await expect(page.getByText('test-image.png')).toBeVisible();
|
||||||
});
|
});
|
||||||
@ -122,12 +125,12 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
test('validates media URLs', async ({ page }) => {
|
test('validates media URLs', async ({ page }) => {
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
await page.getByLabel('Title').fill('Validation Test');
|
await page.getByLabel('Title').fill('Validation Test');
|
||||||
|
|
||||||
// Try invalid URL
|
// Try invalid URL
|
||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
await page.getByLabel('Image URL').fill('not-a-valid-url');
|
await page.getByLabel('Image URL').fill('not-a-valid-url');
|
||||||
await page.click('text=Save Draft');
|
await page.click('text=Save Draft');
|
||||||
|
|
||||||
// Should show validation error
|
// Should show validation error
|
||||||
await expect(page.getByText('Invalid URL')).toBeVisible();
|
await expect(page.getByText('Invalid URL')).toBeVisible();
|
||||||
});
|
});
|
||||||
@ -135,21 +138,21 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
test('reorders media items', async ({ page }) => {
|
test('reorders media items', async ({ page }) => {
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
await page.getByLabel('Title').fill('Reorder Test');
|
await page.getByLabel('Title').fill('Reorder Test');
|
||||||
|
|
||||||
// Add multiple media
|
// Add multiple media
|
||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
await page.click('text=Add Media');
|
await page.click('text=Add Media');
|
||||||
await page.getByPlaceholder('Media URL').fill('https://example.com/first.jpg');
|
await page.getByPlaceholder('Media URL').fill('https://example.com/first.jpg');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
await page.click('text=Add Media');
|
await page.click('text=Add Media');
|
||||||
await page.getByPlaceholder('Media URL').fill('https://example.com/second.jpg');
|
await page.getByPlaceholder('Media URL').fill('https://example.com/second.jpg');
|
||||||
await page.click('text=Add');
|
await page.click('text=Add');
|
||||||
|
|
||||||
// Reorder (move second to first)
|
// Reorder (move second to first)
|
||||||
const secondItem = page.locator('[data-testid="media-item"]').nth(1);
|
const secondItem = page.locator('[data-testid="media-item"]').nth(1);
|
||||||
await secondItem.getByRole('button', { name: 'Move up' }).click();
|
await secondItem.getByRole('button', { name: 'Move up' }).click();
|
||||||
|
|
||||||
// Verify order changed
|
// Verify order changed
|
||||||
const items = page.locator('[data-testid="media-item"]');
|
const items = page.locator('[data-testid="media-item"]');
|
||||||
await expect(items.first()).toContainText('second.jpg');
|
await expect(items.first()).toContainText('second.jpg');
|
||||||
@ -158,14 +161,14 @@ test.describe('Rich Media Broadcasts', () => {
|
|||||||
test('removes media from broadcast', async ({ page }) => {
|
test('removes media from broadcast', async ({ page }) => {
|
||||||
await page.click('text=Create broadcast');
|
await page.click('text=Create broadcast');
|
||||||
await page.getByLabel('Title').fill('Remove Test');
|
await page.getByLabel('Title').fill('Remove Test');
|
||||||
|
|
||||||
// Add then remove media
|
// Add then remove media
|
||||||
await page.click('text=Media');
|
await page.click('text=Media');
|
||||||
await page.getByLabel('Image URL').fill('https://example.com/temp.jpg');
|
await page.getByLabel('Image URL').fill('https://example.com/temp.jpg');
|
||||||
|
|
||||||
// Remove button should appear
|
// Remove button should appear
|
||||||
await page.getByRole('button', { name: 'Remove media' }).click();
|
await page.getByRole('button', { name: 'Remove media' }).click();
|
||||||
|
|
||||||
// Verify removed
|
// Verify removed
|
||||||
await expect(page.getByLabel('Image URL')).toHaveValue('');
|
await expect(page.getByLabel('Image URL')).toHaveValue('');
|
||||||
});
|
});
|
||||||
@ -185,7 +188,7 @@ test.describe('User Dashboard Rich Media', () => {
|
|||||||
// Wait for banner with media to appear
|
// Wait for banner with media to appear
|
||||||
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
||||||
await expect(banner).toBeVisible({ timeout: 10000 });
|
await expect(banner).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// Check for media thumbnail
|
// Check for media thumbnail
|
||||||
const mediaThumb = banner.locator('img');
|
const mediaThumb = banner.locator('img');
|
||||||
if (await mediaThumb.isVisible().catch(() => false)) {
|
if (await mediaThumb.isVisible().catch(() => false)) {
|
||||||
@ -196,10 +199,10 @@ test.describe('User Dashboard Rich Media', () => {
|
|||||||
test('opens media lightbox on click', async ({ page }) => {
|
test('opens media lightbox on click', async ({ page }) => {
|
||||||
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
||||||
await expect(banner).toBeVisible({ timeout: 10000 });
|
await expect(banner).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// Click on media
|
// Click on media
|
||||||
await banner.locator('img').click();
|
await banner.locator('img').click();
|
||||||
|
|
||||||
// Lightbox should open
|
// Lightbox should open
|
||||||
await expect(page.locator('[data-testid="media-lightbox"]')).toBeVisible();
|
await expect(page.locator('[data-testid="media-lightbox"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
@ -208,10 +211,10 @@ test.describe('User Dashboard Rich Media', () => {
|
|||||||
// Open broadcast with media
|
// Open broadcast with media
|
||||||
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
||||||
await expect(banner).toBeVisible({ timeout: 10000 });
|
await expect(banner).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// Click to view media
|
// Click to view media
|
||||||
await banner.locator('img').click();
|
await banner.locator('img').click();
|
||||||
|
|
||||||
// Analytics event should fire
|
// Analytics event should fire
|
||||||
await expect(page.getByText('Media viewed')).toBeVisible();
|
await expect(page.getByText('Media viewed')).toBeVisible();
|
||||||
});
|
});
|
||||||
@ -219,7 +222,7 @@ test.describe('User Dashboard Rich Media', () => {
|
|||||||
test('plays video in modal', async ({ page }) => {
|
test('plays video in modal', async ({ page }) => {
|
||||||
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
const banner = page.locator('[data-testid="broadcast-banner"]').first();
|
||||||
await expect(banner).toBeVisible({ timeout: 10000 });
|
await expect(banner).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// Check if banner has video
|
// Check if banner has video
|
||||||
const video = banner.locator('video');
|
const video = banner.locator('video');
|
||||||
if (await video.isVisible().catch(() => false)) {
|
if (await video.isVisible().catch(() => false)) {
|
||||||
|
|||||||
@ -23,11 +23,23 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Progress } from '@/components/ui/progress';
|
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 = [
|
const steps = [
|
||||||
{ id: 'hypothesis', title: 'Hypothesis', icon: Lightbulb },
|
{ id: 'hypothesis', title: 'Hypothesis', icon: Lightbulb },
|
||||||
@ -49,7 +61,13 @@ export default function NewExperimentPage() {
|
|||||||
description: '',
|
description: '',
|
||||||
hypothesis: '',
|
hypothesis: '',
|
||||||
variants: [
|
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: {} },
|
{ key: 'variant_a', name: 'Variant A', description: '', isControl: false, flagConfig: {} },
|
||||||
],
|
],
|
||||||
allocationStrategy: 'random',
|
allocationStrategy: 'random',
|
||||||
@ -160,8 +178,8 @@ export default function NewExperimentPage() {
|
|||||||
isActive
|
isActive
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-white'
|
||||||
: isCompleted
|
: isCompleted
|
||||||
? 'bg-green-100 text-green-600'
|
? 'bg-green-100 text-green-600'
|
||||||
: 'bg-muted'
|
: 'bg-muted'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
@ -191,18 +209,10 @@ export default function NewExperimentPage() {
|
|||||||
onApplySuggestion={applyAiSuggestion}
|
onApplySuggestion={applyAiSuggestion}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && <VariantsStep formData={formData} setFormData={setFormData} />}
|
||||||
<VariantsStep formData={formData} setFormData={setFormData} />
|
{currentStep === 2 && <MetricsStep formData={formData} setFormData={setFormData} />}
|
||||||
)}
|
{currentStep === 3 && <TargetingStep formData={formData} setFormData={setFormData} />}
|
||||||
{currentStep === 2 && (
|
{currentStep === 4 && <ReviewStep formData={formData} />}
|
||||||
<MetricsStep formData={formData} setFormData={setFormData} />
|
|
||||||
)}
|
|
||||||
{currentStep === 3 && (
|
|
||||||
<TargetingStep formData={formData} setFormData={setFormData} />
|
|
||||||
)}
|
|
||||||
{currentStep === 4 && (
|
|
||||||
<ReviewStep formData={formData} />
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -312,9 +322,15 @@ function HypothesisStep({
|
|||||||
{aiSuggestions.length > 0 ? (
|
{aiSuggestions.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{aiSuggestions.map((suggestion, index) => (
|
{aiSuggestions.map((suggestion, index) => (
|
||||||
<Card key={index} className="cursor-pointer hover:border-primary" onClick={() => onApplySuggestion(suggestion)}>
|
<Card
|
||||||
|
key={index}
|
||||||
|
className="cursor-pointer hover:border-primary"
|
||||||
|
onClick={() => onApplySuggestion(suggestion)}
|
||||||
|
>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<p className="text-sm font-medium line-clamp-2">{suggestion.hypothesis.primary}</p>
|
<p className="text-sm font-medium line-clamp-2">
|
||||||
|
{suggestion.hypothesis.primary}
|
||||||
|
</p>
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
Impact: {suggestion.hypothesis.impactScore}/100
|
Impact: {suggestion.hypothesis.impactScore}/100
|
||||||
@ -329,7 +345,8 @@ function HypothesisStep({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -383,7 +400,9 @@ function VariantsStep({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{variant.isControl ? (
|
{variant.isControl ? (
|
||||||
<Badge variant="outline" className="border-blue-300 text-blue-700">Control</Badge>
|
<Badge variant="outline" className="border-blue-300 text-blue-700">
|
||||||
|
Control
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline">Variant</Badge>
|
<Badge variant="outline">Variant</Badge>
|
||||||
)}
|
)}
|
||||||
@ -429,7 +448,8 @@ function VariantsStep({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -488,7 +508,7 @@ function MetricsStep({
|
|||||||
onValueChange={v =>
|
onValueChange={v =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
primaryMetric: { ...formData.primaryMetric!, type: v as any },
|
primaryMetric: { ...formData.primaryMetric!, type: v as MetricType },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -511,7 +531,10 @@ function MetricsStep({
|
|||||||
onValueChange={v =>
|
onValueChange={v =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
primaryMetric: { ...formData.primaryMetric!, direction: v as any },
|
primaryMetric: {
|
||||||
|
...formData.primaryMetric!,
|
||||||
|
direction: v as PrimaryMetric['direction'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -536,7 +559,9 @@ function MetricsStep({
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<Select
|
<Select
|
||||||
value={formData.allocationStrategy}
|
value={formData.allocationStrategy}
|
||||||
onValueChange={v => setFormData({ ...formData, allocationStrategy: v as any })}
|
onValueChange={v =>
|
||||||
|
setFormData({ ...formData, allocationStrategy: v as AllocationStrategy })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@ -590,7 +615,9 @@ function TargetingStep({
|
|||||||
{['ios', 'android', 'web'].map(platform => (
|
{['ios', 'android', 'web'].map(platform => (
|
||||||
<Badge
|
<Badge
|
||||||
key={platform}
|
key={platform}
|
||||||
variant={formData.targeting?.platforms?.includes(platform) ? 'default' : 'outline'}
|
variant={
|
||||||
|
formData.targeting?.platforms?.includes(platform) ? 'default' : 'outline'
|
||||||
|
}
|
||||||
className="cursor-pointer capitalize"
|
className="cursor-pointer capitalize"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current = formData.targeting?.platforms || [];
|
const current = formData.targeting?.platforms || [];
|
||||||
@ -615,7 +642,9 @@ function TargetingStep({
|
|||||||
{['free', 'pro', 'enterprise'].map(segment => (
|
{['free', 'pro', 'enterprise'].map(segment => (
|
||||||
<Badge
|
<Badge
|
||||||
key={segment}
|
key={segment}
|
||||||
variant={formData.targeting?.userSegments?.includes(segment) ? 'default' : 'outline'}
|
variant={
|
||||||
|
formData.targeting?.userSegments?.includes(segment) ? 'default' : 'outline'
|
||||||
|
}
|
||||||
className="cursor-pointer capitalize"
|
className="cursor-pointer capitalize"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current = formData.targeting?.userSegments || [];
|
const current = formData.targeting?.userSegments || [];
|
||||||
@ -668,7 +697,10 @@ function TargetingStep({
|
|||||||
onChange={e =>
|
onChange={e =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
guardrails: { ...formData.guardrails!, maxDurationDays: parseInt(e.target.value) },
|
guardrails: {
|
||||||
|
...formData.guardrails!,
|
||||||
|
maxDurationDays: parseInt(e.target.value),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
@ -686,7 +718,10 @@ function TargetingStep({
|
|||||||
onChange={e =>
|
onChange={e =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
guardrails: { ...formData.guardrails!, winnerThreshold: parseInt(e.target.value) },
|
guardrails: {
|
||||||
|
...formData.guardrails!,
|
||||||
|
winnerThreshold: parseInt(e.target.value),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
@ -713,7 +748,10 @@ function ReviewStep({ formData }: { formData: Partial<CreateExperimentInput> })
|
|||||||
<ReviewItem label="Primary Metric" value={formData.primaryMetric?.name} />
|
<ReviewItem label="Primary Metric" value={formData.primaryMetric?.name} />
|
||||||
<ReviewItem label="Allocation Strategy" value={formData.allocationStrategy} />
|
<ReviewItem label="Allocation Strategy" value={formData.allocationStrategy} />
|
||||||
<ReviewItem label="Target Traffic" value={`${formData.targetPercent}%`} />
|
<ReviewItem label="Target Traffic" value={`${formData.targetPercent}%`} />
|
||||||
<ReviewItem label="Auto Stop" value={formData.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'} />
|
<ReviewItem
|
||||||
|
label="Auto Stop"
|
||||||
|
value={formData.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert>
|
<Alert>
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export default function ExperimentsPage() {
|
|||||||
const [experiments, setExperiments] = useState<ExperimentDoc[]>([]);
|
const [experiments, setExperiments] = useState<ExperimentDoc[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [now] = useState(() => Date.now());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchExperiments();
|
fetchExperiments();
|
||||||
@ -113,9 +114,7 @@ export default function ExperimentsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
<CardTitle className="text-sm font-medium text-muted-foreground">Running</CardTitle>
|
||||||
Running
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-3xl font-bold text-green-600">{runningCount}</div>
|
<div className="text-3xl font-bold text-green-600">{runningCount}</div>
|
||||||
@ -123,9 +122,7 @@ export default function ExperimentsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
<CardTitle className="text-sm font-medium text-muted-foreground">Completed</CardTitle>
|
||||||
Completed
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-3xl font-bold text-blue-600">{completedCount}</div>
|
<div className="text-3xl font-bold text-blue-600">{completedCount}</div>
|
||||||
@ -149,12 +146,14 @@ export default function ExperimentsPage() {
|
|||||||
<TabsTrigger value="all">All ({experiments.length})</TabsTrigger>
|
<TabsTrigger value="all">All ({experiments.length})</TabsTrigger>
|
||||||
<TabsTrigger value="running">Running ({runningCount})</TabsTrigger>
|
<TabsTrigger value="running">Running ({runningCount})</TabsTrigger>
|
||||||
<TabsTrigger value="completed">Completed ({completedCount})</TabsTrigger>
|
<TabsTrigger value="completed">Completed ({completedCount})</TabsTrigger>
|
||||||
<TabsTrigger value="draft">Drafts ({experiments.filter(e => e.status === 'draft').length})</TabsTrigger>
|
<TabsTrigger value="draft">
|
||||||
|
Drafts ({experiments.filter(e => e.status === 'draft').length})
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="all" className="space-y-4">
|
<TabsContent value="all" className="space-y-4">
|
||||||
{experiments.map(experiment => (
|
{experiments.map(experiment => (
|
||||||
<ExperimentCard key={experiment.id} experiment={experiment} />
|
<ExperimentCard key={experiment.id} experiment={experiment} now={now} />
|
||||||
))}
|
))}
|
||||||
{experiments.length === 0 && (
|
{experiments.length === 0 && (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
@ -168,7 +167,7 @@ export default function ExperimentsPage() {
|
|||||||
{experiments
|
{experiments
|
||||||
.filter(e => e.status === 'running')
|
.filter(e => e.status === 'running')
|
||||||
.map(experiment => (
|
.map(experiment => (
|
||||||
<ExperimentCard key={experiment.id} experiment={experiment} />
|
<ExperimentCard key={experiment.id} experiment={experiment} now={now} />
|
||||||
))}
|
))}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@ -176,7 +175,7 @@ export default function ExperimentsPage() {
|
|||||||
{experiments
|
{experiments
|
||||||
.filter(e => e.status === 'completed')
|
.filter(e => e.status === 'completed')
|
||||||
.map(experiment => (
|
.map(experiment => (
|
||||||
<ExperimentCard key={experiment.id} experiment={experiment} />
|
<ExperimentCard key={experiment.id} experiment={experiment} now={now} />
|
||||||
))}
|
))}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@ -184,7 +183,7 @@ export default function ExperimentsPage() {
|
|||||||
{experiments
|
{experiments
|
||||||
.filter(e => e.status === 'draft')
|
.filter(e => e.status === 'draft')
|
||||||
.map(experiment => (
|
.map(experiment => (
|
||||||
<ExperimentCard key={experiment.id} experiment={experiment} />
|
<ExperimentCard key={experiment.id} experiment={experiment} now={now} />
|
||||||
))}
|
))}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@ -192,12 +191,12 @@ export default function ExperimentsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExperimentCard({ experiment }: { experiment: ExperimentDoc }) {
|
function ExperimentCard({ experiment, now }: { experiment: ExperimentDoc; now: number }) {
|
||||||
const status = statusConfig[experiment.status] || statusConfig.draft;
|
const status = statusConfig[experiment.status] || statusConfig.draft;
|
||||||
const StatusIcon = status.icon;
|
const StatusIcon = status.icon;
|
||||||
|
|
||||||
const daysRunning = experiment.startedAt
|
const daysRunning = experiment.startedAt
|
||||||
? Math.floor((Date.now() - new Date(experiment.startedAt).getTime()) / (1000 * 60 * 60 * 24))
|
? Math.floor((now - new Date(experiment.startedAt).getTime()) / (1000 * 60 * 60 * 24))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,23 +1,22 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface TextareaProps
|
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Textarea.displayName = "Textarea"
|
Textarea.displayName = 'Textarea';
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user