diff --git a/dashboards/admin-web/e2e/rich-media.spec.ts b/dashboards/admin-web/e2e/rich-media.spec.ts new file mode 100644 index 00000000..8d721b1e --- /dev/null +++ b/dashboards/admin-web/e2e/rich-media.spec.ts @@ -0,0 +1,231 @@ +import { test, expect } from '@playwright/test'; + +const ADMIN_EMAIL = 'admin@example.com'; +const ADMIN_PASSWORD = 'Admin123!'; + +async function loginAsAdmin(page: any) { + await page.goto('/login'); + await page.getByLabel('Email').fill(ADMIN_EMAIL); + await page.getByLabel('Password').fill(ADMIN_PASSWORD); + await page.getByRole('button', { name: 'Sign In' }).click(); + await page.waitForURL('**/dashboard', { timeout: 10000 }); +} + +test.describe('Rich Media Broadcasts', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.click('text=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(); + }); + + 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'); + await page.selectOption('select[name="mediaType"]', 'video'); + 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(); + }); + + test('displays media gallery in broadcast preview', async ({ page }) => { + // Create broadcast with media first + await page.click('text=Create broadcast'); + await page.getByLabel('Title').fill('Preview Test'); + 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(); + + // Verify media in preview modal + await expect(page.locator('img[src*="preview.jpg"]')).toBeVisible(); + }); + + test('tracks media engagement in analytics', async ({ page }) => { + // 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(); + await expect(page.getByText('Media Clicks')).toBeVisible(); + } + }); + + 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({ + name: 'test-image.png', + 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(); + }); + + 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(); + }); + + 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'); + }); + + 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(''); + }); +}); + +test.describe('User Dashboard Rich Media', () => { + test.beforeEach(async ({ page }) => { + // Login as regular user + await page.goto('/login'); + await page.getByLabel('Email').fill('user@example.com'); + await page.getByLabel('Password').fill('User123!'); + await page.getByRole('button', { name: 'Sign In' }).click(); + await page.waitForURL('**/portal', { timeout: 10000 }); + }); + + test('displays media in broadcast banner', async ({ page }) => { + // 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)) { + await expect(mediaThumb).toBeVisible(); + } + }); + + 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(); + }); + + test('tracks media view in user dashboard', async ({ page }) => { + // 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(); + }); + + 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)) { + // Click play + await video.click(); + await expect(video).toHaveAttribute('autoplay'); + } + }); +});