diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index e5c2cafe..be1b37c2 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,7 +10,28 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest - + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + # Wait for postgres to be ready + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4] + shardTotal: [4] + steps: - name: Checkout code uses: actions/checkout@v4 @@ -26,36 +47,56 @@ jobs: - name: Install Playwright Browsers run: bunx playwright install --with-deps - - name: Build application - run: bun run build + - name: Run migrations + run: bun run db:migrate env: - # Add any required environment variables for build - NODE_ENV: production + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres - - name: Start application - run: | - bun run start & - sleep 10 - # Wait for the server to be ready - npx wait-on http://localhost:3000 --timeout 60000 - - - name: Run Playwright tests - run: bun run test:e2e + - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + run: bunx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} env: CI: true + NODE_ENV: production + AUTH_DISABLED_FOR_DEV: true + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres - name: Upload test results uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + name: blob-report-${{ matrix.shardIndex }} + path: blob-report/ + retention-days: 1 + + merge-reports: + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Upload test screenshots + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.5 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Download blobs + uses: actions/download-artifact@v4 + with: + path: all-blobs + pattern: blob-report-* + merge-multiple: true + + - name: Merge reports + run: bunx playwright merge-reports --reporter html ./all-blobs + + - name: Upload merged report uses: actions/upload-artifact@v4 - if: failure() with: - name: playwright-screenshots - path: test-results/ - retention-days: 7 + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index 1acd0e01..e208839d 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -76,4 +76,4 @@ Analyze the user's prompt and the image to provide a holistic understanding of t messages: filteredMessages, schema: resolutionSearchSchema, }) -} \ No newline at end of file +} diff --git a/package.json b/package.json index cdf8a7e3..d9074ef2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "lint": "next lint", "db:migrate": "cross-env EXECUTE_MIGRATIONS=true bun lib/db/migrate.ts", "test:e2e": "playwright test", + "test:e2e:smoke": "playwright test --grep @smoke", + "test:e2e:parallel": "playwright test --workers=4", + "test:e2e:chromium": "playwright test --project=chromium", + "test:e2e:shard": "playwright test --shard=$SHARD_INDEX/$SHARD_TOTAL", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", "test:e2e:debug": "playwright test --debug" diff --git a/playwright.config.ts b/playwright.config.ts index 6f9a310b..97099961 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,11 +2,12 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', + globalSetup: require.resolve('./tests/global-setup'), fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'html', + workers: process.env.CI ? 2 : undefined, + reporter: process.env.CI ? [['list'], ['github'], ['blob']] : 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', @@ -15,28 +16,35 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + testIgnore: /mobile\.spec\.ts/, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, + testIgnore: /mobile\.spec\.ts/, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, + testIgnore: /mobile\.spec\.ts/, }, { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, + testMatch: /mobile\.spec\.ts/, }, { name: 'Mobile Safari', use: { ...devices['iPhone 12'] }, + testMatch: /mobile\.spec\.ts/, }, ], - /* webServer: { - command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev', + webServer: { + command: process.env.CI ? 'bun run build && bun run start' : 'bun run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 600000, - }, */ + stdout: 'pipe', + stderr: 'pipe', + }, }); diff --git a/tests/calendar.spec.ts b/tests/calendar.spec.ts index 4722bed9..d7b956cc 100644 --- a/tests/calendar.spec.ts +++ b/tests/calendar.spec.ts @@ -1,12 +1,7 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; -test.describe('Calendar functionality', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.waitForSelector('[data-testid="calendar-toggle"]'); - }); - - test('should open and close the calendar', async ({ page }) => { +test.describe('Calendar functionality @smoke @calendar', () => { + test('should open and close the calendar', async ({ authenticatedPage: page }) => { // Open calendar await page.click('[data-testid="calendar-toggle"]'); const calendar = page.locator('[data-testid="calendar-notepad"]'); @@ -17,7 +12,7 @@ test.describe('Calendar functionality', () => { await expect(calendar).not.toBeVisible(); }); - test('should display current month and year', async ({ page }) => { + test('should display current month and year', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); const currentDate = new Date(); @@ -30,7 +25,7 @@ test.describe('Calendar functionality', () => { await expect(calendarHeader).toContainText(monthYear); }); - test('should navigate to previous month', async ({ page }) => { + test('should navigate to previous month', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); const initialMonth = await page.locator('[data-testid="calendar-header"]').innerText(); @@ -41,7 +36,7 @@ test.describe('Calendar functionality', () => { expect(newMonth).not.toBe(initialMonth); }); - test('should navigate to next month', async ({ page }) => { + test('should navigate to next month', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); const initialMonth = await page.locator('[data-testid="calendar-header"]').innerText(); @@ -52,7 +47,7 @@ test.describe('Calendar functionality', () => { expect(newMonth).not.toBe(initialMonth); }); - test('should select a date', async ({ page }) => { + test('should select a date', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); // Click on a specific date (e.g., the 15th of the current month) @@ -63,43 +58,46 @@ test.describe('Calendar functionality', () => { await expect(dateButton).toHaveClass(/selected|active/); }); - test('should add a note to a selected date', async ({ page }) => { + test('should add a note to a selected date', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); // Select a date await page.click('[data-testid="calendar-day-15"]'); - // Add a note + // Add a unique note to avoid conflicts during parallel execution + const uniqueNote = `Important meeting ${Date.now()}`; const noteInput = page.locator('[data-testid="calendar-note-input"]'); - await noteInput.fill('Important meeting at 2 PM'); + await noteInput.fill(uniqueNote); await page.click('[data-testid="calendar-save-note"]'); // Verify the note is saved const savedNote = page.locator('[data-testid="calendar-note-content"]'); - await expect(savedNote).toContainText('Important meeting at 2 PM'); + await expect(savedNote).toContainText(uniqueNote); }); - test('should edit an existing note', async ({ page }) => { + test('should edit an existing note', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); // Select a date and add a note await page.click('[data-testid="calendar-day-15"]'); - await page.fill('[data-testid="calendar-note-input"]', 'Original note'); + const originalNote = `Original note ${Date.now()}`; + await page.fill('[data-testid="calendar-note-input"]', originalNote); await page.click('[data-testid="calendar-save-note"]'); // Edit the note await page.click('[data-testid="calendar-edit-note"]'); - await page.fill('[data-testid="calendar-note-input"]', 'Updated note'); + const updatedNote = `Updated note ${Date.now()}`; + await page.fill('[data-testid="calendar-note-input"]', updatedNote); await page.click('[data-testid="calendar-save-note"]'); // Verify the note is updated - const updatedNote = page.locator('[data-testid="calendar-note-content"]'); - await expect(updatedNote).toContainText('Updated note'); - await expect(updatedNote).not.toContainText('Original note'); + const updatedNoteElement = page.locator('[data-testid="calendar-note-content"]'); + await expect(updatedNoteElement).toContainText(updatedNote); + await expect(updatedNoteElement).not.toContainText(originalNote); }); - test('should delete a note', async ({ page }) => { + test('should delete a note', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); // Select a date and add a note @@ -116,12 +114,13 @@ test.describe('Calendar functionality', () => { await expect(noteContent).not.toBeVisible(); }); - test('should persist notes after closing and reopening calendar', async ({ page }) => { + test('should persist notes after closing and reopening calendar', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); - // Add a note + // Add a unique note await page.click('[data-testid="calendar-day-15"]'); - await page.fill('[data-testid="calendar-note-input"]', 'Persistent note'); + const persistentNoteText = `Persistent note ${Date.now()}`; + await page.fill('[data-testid="calendar-note-input"]', persistentNoteText); await page.click('[data-testid="calendar-save-note"]'); // Close calendar @@ -133,22 +132,23 @@ test.describe('Calendar functionality', () => { // Verify the note is still there const persistedNote = page.locator('[data-testid="calendar-note-content"]'); - await expect(persistedNote).toContainText('Persistent note'); + await expect(persistedNote).toContainText(persistentNoteText); }); - test('should highlight dates with notes', async ({ page }) => { + test('should highlight dates with notes', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); // Add a note to a date await page.click('[data-testid="calendar-day-15"]'); - await page.fill('[data-testid="calendar-note-input"]', 'Test note'); + await page.fill('[data-testid="calendar-note-input"]', 'Highlight test note'); await page.click('[data-testid="calendar-save-note"]'); // Close the note panel await page.click('[data-testid="calendar-close-note"]'); - // Verify the date has a visual indicator (dot, badge, etc.) + // Verify the date has a visual indicator const dateWithNote = page.locator('[data-testid="calendar-day-15"]'); await expect(dateWithNote).toHaveClass(/has-note|noted/); }); + }); diff --git a/tests/chat.spec.ts b/tests/chat.spec.ts index 700a1bab..f07d1359 100644 --- a/tests/chat.spec.ts +++ b/tests/chat.spec.ts @@ -1,17 +1,11 @@ -import { test, expect } from '@playwright/test'; -import * as path from 'path'; +import { test, expect } from './fixtures'; -test.describe('Chat functionality', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.waitForSelector('[data-testid="chat-input"]'); - }); - - test('should not allow sending empty messages', async ({ page }) => { +test.describe('Chat functionality @smoke', () => { + test('should not allow sending empty messages', async ({ authenticatedPage: page }) => { await expect(page.locator('[data-testid="chat-submit"]')).toBeDisabled(); }); - test('should allow a user to send a message and see the response', async ({ page }) => { + test('should allow a user to send a message and see the response', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'Hello, world!'); await page.click('[data-testid="chat-submit"]'); @@ -22,14 +16,17 @@ test.describe('Chat functionality', () => { const botMessage = page.locator('[data-testid="bot-message"]'); await expect(botMessage.last()).toBeVisible({ timeout: 15000 }); - // Check for streaming response + // Check for streaming response using deterministic poll const initialResponse = await botMessage.last().innerText(); - await page.waitForTimeout(1000); - const streamingResponse = await botMessage.last().innerText(); - expect(streamingResponse.length).toBeGreaterThan(initialResponse.length); + await expect.poll(async () => { + const currentText = await botMessage.last().innerText(); + return currentText.length; + }, { + timeout: 10000, + }).toBeGreaterThan(initialResponse.length); }); - test('should persist chat history on page reload', async ({ page }) => { + test('should persist chat history on page reload', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'A message that should persist'); await page.click('[data-testid="chat-submit"]'); @@ -43,7 +40,7 @@ test.describe('Chat functionality', () => { await expect(userMessage).toHaveText(/A message that should persist/); }); - test('should correctly render markdown and code blocks', async ({ page }) => { + test('should correctly render markdown and code blocks', async ({ authenticatedPage: page }) => { const markdownMessage = 'Here is some `code` and a **bold** statement.'; await page.fill('[data-testid="chat-input"]', markdownMessage); await page.click('[data-testid="chat-submit"]'); @@ -61,7 +58,7 @@ test.describe('Chat functionality', () => { await expect(boldElement).toBeVisible(); }); - test('should allow a user to attach a file', async ({ page }, testInfo) => { + test('should allow a user to attach a file', async ({ authenticatedPage: page }, testInfo) => { const filePath = testInfo.outputPath('test-file.txt'); await require('fs').promises.writeFile(filePath, 'This is a test file.'); @@ -76,7 +73,7 @@ test.describe('Chat functionality', () => { await expect(page.locator('text=test-file.txt')).not.toBeVisible(); }); - test('should start a new chat', async ({ page }) => { + test('should start a new chat', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'First message'); await page.click('[data-testid="chat-submit"]'); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 00000000..39b93901 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,44 @@ +import { test as base, Page } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +export const test = base.extend<{ + authenticatedPage: Page; + testImage: string; +}>({ + authenticatedPage: async ({ page }, use) => { + await page.goto('/'); + // Increased timeout for CI environments + await page.waitForSelector('[data-testid="chat-input"]', { timeout: 30000 }); + await use(page); + + // Cleanup after test to ensure isolation + await page.evaluate(() => { + localStorage.clear(); + sessionStorage.clear(); + }); + }, + + testImage: async ({}, use) => { + const fixturesDir = path.join(process.cwd(), 'test-fixtures'); + const imagePath = path.join(fixturesDir, 'test-image.png'); + + // Ensure the directory exists + if (!fs.existsSync(fixturesDir)) { + fs.mkdirSync(fixturesDir, { recursive: true }); + } + + // Create a simple 1x1 PNG image if it doesn't exist + if (!fs.existsSync(imagePath)) { + const pngBuffer = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', + 'base64' + ); + fs.writeFileSync(imagePath, pngBuffer); + } + + await use(imagePath); + } +}); + +export { expect } from '@playwright/test'; diff --git a/tests/global-setup.ts b/tests/global-setup.ts new file mode 100644 index 00000000..77b95e76 --- /dev/null +++ b/tests/global-setup.ts @@ -0,0 +1,9 @@ +import { FullConfig } from '@playwright/test'; + +async function globalSetup(config: FullConfig) { + // Global initialization can be added here + // For example, clearing a test database or setting up environment variables + console.log('Starting E2E test suite...'); +} + +export default globalSetup; diff --git a/tests/header.spec.ts b/tests/header.spec.ts index 55065420..a07adee1 100644 --- a/tests/header.spec.ts +++ b/tests/header.spec.ts @@ -1,11 +1,7 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; -test.describe('Header and Navigation', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should toggle the theme', async ({ page }) => { +test.describe('Header and Navigation @smoke', () => { + test('should toggle the theme', async ({ authenticatedPage: page }) => { await page.click('[data-testid="theme-toggle"]'); await page.click('[data-testid="theme-dark"]'); const html = page.locator('html'); @@ -16,15 +12,16 @@ test.describe('Header and Navigation', () => { await expect(html).not.toHaveClass(/(^|\s)dark(\s|$)/); }); - test('should open the profile menu', async ({ page }) => { + test('should open the profile menu', async ({ authenticatedPage: page }) => { await page.click('[data-testid="profile-toggle"]'); const accountMenu = page.locator('[data-testid="profile-account"]'); await expect(accountMenu).toBeVisible(); }); - test('should open the calendar', async ({ page }) => { + test('should open the calendar', async ({ authenticatedPage: page }) => { await page.click('[data-testid="calendar-toggle"]'); const calendar = page.locator('[data-testid="calendar-notepad"]'); await expect(calendar).toBeVisible(); }); + }); diff --git a/tests/images.spec.ts b/tests/images.spec.ts index 2ecc2154..529a25ee 100644 --- a/tests/images.spec.ts +++ b/tests/images.spec.ts @@ -1,47 +1,26 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; import * as path from 'path'; import * as fs from 'fs'; -test.describe('Image attachment and response functionality', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.waitForSelector('[data-testid="chat-input"]'); - }); - - test('should attach an image file', async ({ page }, testInfo) => { - // Create a test image file - const imagePath = testInfo.outputPath('test-image.png'); - - // Create a simple 1x1 PNG image - const pngBuffer = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - await fs.promises.writeFile(imagePath, pngBuffer); +test.describe('Image attachment and response functionality @smoke', () => { + test('should attach an image file', async ({ authenticatedPage: page, testImage }) => { // Attach the image const fileChooserPromise = page.waitForEvent('filechooser'); await page.click('[data-testid="attachment-button"]'); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(imagePath); + await fileChooser.setFiles(testImage); // Verify the image is attached await expect(page.locator('[data-testid="attached-image"]')).toBeVisible(); await expect(page.locator('text=test-image.png')).toBeVisible(); }); - test('should preview attached image', async ({ page }, testInfo) => { - const imagePath = testInfo.outputPath('preview-test.jpg'); - const pngBuffer = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - await fs.promises.writeFile(imagePath, pngBuffer); - + test('should preview attached image', async ({ authenticatedPage: page, testImage }) => { const fileChooserPromise = page.waitForEvent('filechooser'); await page.click('[data-testid="attachment-button"]'); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(imagePath); + await fileChooser.setFiles(testImage); // Check for image preview const imagePreview = page.locator('[data-testid="image-preview"]'); @@ -52,18 +31,11 @@ test.describe('Image attachment and response functionality', () => { expect(imgSrc).toBeTruthy(); }); - test('should remove attached image', async ({ page }, testInfo) => { - const imagePath = testInfo.outputPath('remove-test.png'); - const pngBuffer = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - await fs.promises.writeFile(imagePath, pngBuffer); - + test('should remove attached image', async ({ authenticatedPage: page, testImage }) => { const fileChooserPromise = page.waitForEvent('filechooser'); await page.click('[data-testid="attachment-button"]'); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(imagePath); + await fileChooser.setFiles(testImage); // Verify image is attached await expect(page.locator('[data-testid="attached-image"]')).toBeVisible(); @@ -75,19 +47,12 @@ test.describe('Image attachment and response functionality', () => { await expect(page.locator('[data-testid="attached-image"]')).not.toBeVisible(); }); - test('should send message with attached image', async ({ page }, testInfo) => { - const imagePath = testInfo.outputPath('send-test.png'); - const pngBuffer = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - await fs.promises.writeFile(imagePath, pngBuffer); - + test('should send message with attached image', async ({ authenticatedPage: page, testImage }) => { // Attach image const fileChooserPromise = page.waitForEvent('filechooser'); await page.click('[data-testid="attachment-button"]'); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(imagePath); + await fileChooser.setFiles(testImage); // Type a message await page.fill('[data-testid="chat-input"]', 'What is in this image?'); @@ -105,7 +70,7 @@ test.describe('Image attachment and response functionality', () => { await expect(messageImage).toBeVisible(); }); - test('should reject non-image files', async ({ page }, testInfo) => { + test('should reject non-image files', async ({ authenticatedPage: page }, testInfo) => { // Create a text file const textPath = testInfo.outputPath('test.txt'); await fs.promises.writeFile(textPath, 'This is not an image'); @@ -121,7 +86,7 @@ test.describe('Image attachment and response functionality', () => { await expect(errorMessage).toContainText(/only.*image.*allowed|invalid.*file.*type/i); }); - test('should reject oversized images', async ({ page }, testInfo) => { + test('should reject oversized images', async ({ authenticatedPage: page }, testInfo) => { // Create a large file (simulating a large image) const largePath = testInfo.outputPath('large-image.png'); const largeBuffer = Buffer.alloc(20 * 1024 * 1024); // 20MB @@ -138,7 +103,7 @@ test.describe('Image attachment and response functionality', () => { await expect(errorMessage).toContainText(/too.*large|file.*size|exceed/i); }); - test('should display image in bot response', async ({ page }) => { + test('should display image in bot response', async ({ authenticatedPage: page }) => { // Send a message that would generate an image response await page.fill('[data-testid="chat-input"]', 'Generate an image of a sunset'); await page.click('[data-testid="chat-submit"]'); @@ -157,19 +122,12 @@ test.describe('Image attachment and response functionality', () => { expect(imgSrc).toMatch(/^(http|https|data:image)/); }); - test('should allow clicking on image to view full size', async ({ page }, testInfo) => { - const imagePath = testInfo.outputPath('fullsize-test.png'); - const pngBuffer = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - await fs.promises.writeFile(imagePath, pngBuffer); - + test('should allow clicking on image to view full size', async ({ authenticatedPage: page, testImage }) => { // Attach and send image const fileChooserPromise = page.waitForEvent('filechooser'); await page.click('[data-testid="attachment-button"]'); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(imagePath); + await fileChooser.setFiles(testImage); await page.fill('[data-testid="chat-input"]', 'Test image'); await page.click('[data-testid="chat-submit"]'); @@ -183,7 +141,7 @@ test.describe('Image attachment and response functionality', () => { await expect(imageModal).toBeVisible(); }); - test('should support multiple image formats', async ({ page }, testInfo) => { + test('should support multiple image formats', async ({ authenticatedPage: page }, testInfo) => { const formats = ['png', 'jpg', 'jpeg', 'gif', 'webp']; for (const format of formats) { @@ -207,22 +165,14 @@ test.describe('Image attachment and response functionality', () => { } }); - test('should show loading state while uploading image', async ({ page }, testInfo) => { - const imagePath = testInfo.outputPath('loading-test.png'); - const pngBuffer = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - await fs.promises.writeFile(imagePath, pngBuffer); - + test('should show loading state while uploading image', async ({ authenticatedPage: page, testImage }) => { const fileChooserPromise = page.waitForEvent('filechooser'); await page.click('[data-testid="attachment-button"]'); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(imagePath); + await fileChooser.setFiles(testImage); // Check for loading indicator (might be brief) const loadingIndicator = page.locator('[data-testid="image-uploading"]'); - // Loading state might be too fast to catch, so we use waitFor with a short timeout try { await expect(loadingIndicator).toBeVisible({ timeout: 1000 }); } catch { @@ -232,4 +182,5 @@ test.describe('Image attachment and response functionality', () => { // Verify final state shows the image await expect(page.locator('[data-testid="attached-image"]')).toBeVisible(); }); + }); diff --git a/tests/map.spec.ts b/tests/map.spec.ts index 5d6aa972..21d05005 100644 --- a/tests/map.spec.ts +++ b/tests/map.spec.ts @@ -1,13 +1,15 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; -test.describe('Map functionality', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); +test.describe('Map functionality @smoke', () => { + // Configure retries for map tests which can be flaky due to WebGL rendering + test.describe.configure({ retries: 2 }); + + test.beforeEach(async ({ authenticatedPage: page }) => { // Wait for either the Mapbox or Google Map to be loaded - await page.waitForSelector('.mapboxgl-canvas, gmp-map-3d'); + await page.waitForSelector('.mapboxgl-canvas, gmp-map-3d', { timeout: 30000 }); }); - test('should toggle the map mode', async ({ page }) => { + test('should toggle the map mode', async ({ authenticatedPage: page }) => { const isMapbox = await page.locator('.mapboxgl-canvas').isVisible(); if (!isMapbox) { test.skip(true, 'Drawing mode test is only for Mapbox'); @@ -16,13 +18,12 @@ test.describe('Map functionality', () => { await page.click('[data-testid="map-toggle"]'); await page.click('[data-testid="map-mode-draw"]'); - // Add an assertion here to verify that the map is in drawing mode - // For example, by checking for the presence of a drawing control + const drawControl = page.locator('.mapboxgl-ctrl-draw-btn'); await expect(drawControl).toBeVisible(); }); - test('should zoom in and out using map controls', async ({ page }) => { + test('should zoom in and out using map controls', async ({ authenticatedPage: page }) => { // This test should only run on desktop where the controls are visible if (page.viewportSize()!.width <= 768) { test.skip(true, 'Zoom controls are not visible on mobile'); @@ -54,4 +55,5 @@ test.describe('Map functionality', () => { const zoomedOutZoom = await getZoom(); expect(zoomedOutZoom).toBeLessThan(zoomedInZoom); }); + }); diff --git a/tests/mobile.spec.ts b/tests/mobile.spec.ts index d138f4f6..02ebd0c1 100644 --- a/tests/mobile.spec.ts +++ b/tests/mobile.spec.ts @@ -1,21 +1,16 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; -test.describe('Mobile UI', () => { +test.describe('Mobile UI @smoke', () => { test.use({ viewport: { width: 375, height: 667 } }); // iPhone 8 - test.beforeEach(async ({ page }) => { - await page.goto('/'); - // Wait for the main chat input to be visible, indicating the app is ready - await page.locator('[data-testid="chat-input"]').waitFor({ state: 'visible', timeout: 30000 }); - }); - - test('profile toggle button should be disabled', async ({ page }) => { + test('profile toggle button should be disabled', async ({ authenticatedPage: page }) => { // Check that the profile toggle is disabled on mobile await expect(page.locator('.mobile-icons-bar-content [data-testid="profile-toggle"]')).toBeDisabled(); }); - test('should have an enabled submit button', async ({ page }) => { + test('should have an enabled submit button', async ({ authenticatedPage: page }) => { const submitButton = page.locator('[data-testid="mobile-submit-button"]'); await expect(submitButton).toBeEnabled(); }); + }); diff --git a/tests/responsive.spec.ts b/tests/responsive.spec.ts index a53fcd6e..ed30a6c5 100644 --- a/tests/responsive.spec.ts +++ b/tests/responsive.spec.ts @@ -1,13 +1,11 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; -test.describe('Responsive design - Desktop', () => { - test.use({ viewport: { width: 1920, height: 1080 } }); +test.describe.configure({ mode: 'parallel' }); - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); +test.describe('Responsive design - Desktop @smoke', () => { + test.use({ viewport: { width: 1920, height: 1080 } }); - test('should display desktop layout', async ({ page }) => { + test('should display desktop layout', async ({ authenticatedPage: page }) => { // Sidebar should be visible on desktop const sidebar = page.locator('[data-testid="sidebar"]'); await expect(sidebar).toBeVisible(); @@ -17,20 +15,19 @@ test.describe('Responsive design - Desktop', () => { await expect(mobileMenu).not.toBeVisible(); }); - test('should display full header with all elements', async ({ page }) => { + test('should display full header with all elements', async ({ authenticatedPage: page }) => { await expect(page.locator('[data-testid="theme-toggle"]')).toBeVisible(); await expect(page.locator('[data-testid="profile-toggle"]')).toBeVisible(); await expect(page.locator('[data-testid="calendar-toggle"]')).toBeVisible(); }); - test('should show map alongside chat', async ({ page }) => { + test('should show map alongside chat', async ({ authenticatedPage: page }) => { const chatContainer = page.locator('[data-testid="chat-container"]'); const mapContainer = page.locator('.mapboxgl-canvas'); await expect(chatContainer).toBeVisible(); await expect(mapContainer).toBeVisible(); - // Both should be visible simultaneously on desktop const chatBox = await chatContainer.boundingBox(); const mapBox = await mapContainer.boundingBox(); @@ -42,45 +39,35 @@ test.describe('Responsive design - Desktop', () => { test.describe('Responsive design - Tablet', () => { test.use({ viewport: { width: 768, height: 1024 } }); - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should display tablet layout', async ({ page }) => { - // Check if layout adapts to tablet size + test('should display tablet layout', async ({ authenticatedPage: page }) => { const container = page.locator('[data-testid="main-container"]'); await expect(container).toBeVisible(); - // Sidebar might be collapsible on tablet const sidebar = page.locator('[data-testid="sidebar"]'); const sidebarBox = await sidebar.boundingBox(); if (sidebarBox) { - // If visible, check it's narrower than desktop expect(sidebarBox.width).toBeLessThan(300); } }); - test('should handle touch interactions', async ({ page }) => { - // Test that buttons are appropriately sized for touch + test('should handle touch interactions', async ({ authenticatedPage: page }) => { const chatSubmit = page.locator('[data-testid="chat-submit"]'); const buttonBox = await chatSubmit.boundingBox(); expect(buttonBox).toBeTruthy(); if (buttonBox) { - // Touch target should be at least 44x44 pixels (iOS guideline) expect(buttonBox.height).toBeGreaterThanOrEqual(40); } }); - test('should reflow content appropriately', async ({ page }) => { + test('should reflow content appropriately', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'Test message for tablet'); await page.click('[data-testid="chat-submit"]'); const userMessage = page.locator('[data-testid="user-message"]').last(); await expect(userMessage).toBeVisible(); - // Message should not overflow const messageBox = await userMessage.boundingBox(); const viewportWidth = 768; @@ -91,28 +78,21 @@ test.describe('Responsive design - Tablet', () => { }); }); -test.describe('Responsive design - Mobile', () => { +test.describe('Responsive design - Mobile @smoke', () => { test.use({ viewport: { width: 375, height: 667 } }); // iPhone SE - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should display mobile layout', async ({ page }) => { - // Desktop sidebar should be hidden + test('should display mobile layout', async ({ authenticatedPage: page }) => { const sidebar = page.locator('[data-testid="sidebar"]'); await expect(sidebar).not.toBeVisible(); - // Mobile navigation should be visible const mobileNav = page.locator('[data-testid="mobile-icons-bar"]'); await expect(mobileNav).toBeVisible(); }); - test('should have mobile-optimized input', async ({ page }) => { + test('should have mobile-optimized input', async ({ authenticatedPage: page }) => { const chatInput = page.locator('[data-testid="chat-input"]'); await expect(chatInput).toBeVisible(); - // Input should span most of the width const inputBox = await chatInput.boundingBox(); const viewportWidth = 375; @@ -122,8 +102,7 @@ test.describe('Responsive design - Mobile', () => { } }); - test('should toggle map view on mobile', async ({ page }) => { - // On mobile, map and chat might toggle + test('should toggle map view on mobile', async ({ authenticatedPage: page }) => { const mapToggle = page.locator('[data-testid="map-toggle"]'); if (await mapToggle.isVisible()) { @@ -134,7 +113,7 @@ test.describe('Responsive design - Mobile', () => { } }); - test('should handle mobile menu', async ({ page }) => { + test('should handle mobile menu', async ({ authenticatedPage: page }) => { const menuButton = page.locator('[data-testid="mobile-menu-button"]'); if (await menuButton.isVisible()) { @@ -145,7 +124,7 @@ test.describe('Responsive design - Mobile', () => { } }); - test('should have appropriately sized touch targets', async ({ page }) => { + test('should have appropriately sized touch targets', async ({ authenticatedPage: page }) => { const buttons = [ '[data-testid="mobile-new-chat-button"]', '[data-testid="mobile-submit-button"]', @@ -158,7 +137,6 @@ test.describe('Responsive design - Mobile', () => { const buttonBox = await button.boundingBox(); expect(buttonBox).toBeTruthy(); if (buttonBox) { - // Touch targets should be at least 44x44 pixels expect(buttonBox.height).toBeGreaterThanOrEqual(40); expect(buttonBox.width).toBeGreaterThanOrEqual(40); } @@ -166,26 +144,24 @@ test.describe('Responsive design - Mobile', () => { } }); - test('should prevent horizontal scroll', async ({ page }) => { - // Check that content doesn't cause horizontal overflow + test('should prevent horizontal scroll', async ({ authenticatedPage: page }) => { const bodyWidth = await page.evaluate(() => document.body.scrollWidth); const viewportWidth = 375; - expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); // +1 for rounding + expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); }); - test('should stack elements vertically', async ({ page }) => { + test('should stack elements vertically', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'Mobile test message'); await page.click('[data-testid="mobile-submit-button"]'); const userMessage = page.locator('[data-testid="user-message"]').last(); await expect(userMessage).toBeVisible(); - // Message should take full width (minus padding) const messageBox = await userMessage.boundingBox(); expect(messageBox).toBeTruthy(); if (messageBox) { - expect(messageBox.width).toBeGreaterThan(300); // Most of 375px width + expect(messageBox.width).toBeGreaterThan(300); } }); }); @@ -193,12 +169,7 @@ test.describe('Responsive design - Mobile', () => { test.describe('Responsive design - Small Mobile', () => { test.use({ viewport: { width: 320, height: 568 } }); // iPhone 5/SE - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should work on very small screens', async ({ page }) => { - // Verify basic functionality works + test('should work on very small screens', async ({ authenticatedPage: page }) => { const chatInput = page.locator('[data-testid="chat-input"]'); await expect(chatInput).toBeVisible(); @@ -208,14 +179,13 @@ test.describe('Responsive design - Small Mobile', () => { await expect(submitButton).toBeEnabled(); }); - test('should not have text overflow', async ({ page }) => { + test('should not have text overflow', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'This is a longer message to test text wrapping on very small mobile screens'); await page.click('[data-testid="mobile-submit-button"]'); const userMessage = page.locator('[data-testid="user-message"]').last(); await expect(userMessage).toBeVisible(); - // Check that text wraps and doesn't overflow const messageBox = await userMessage.boundingBox(); expect(messageBox).toBeTruthy(); if (messageBox) { @@ -227,12 +197,7 @@ test.describe('Responsive design - Small Mobile', () => { test.describe('Responsive design - Large Desktop', () => { test.use({ viewport: { width: 2560, height: 1440 } }); // 2K display - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should utilize large screen space', async ({ page }) => { - // Check that layout expands to use available space + test('should utilize large screen space', async ({ authenticatedPage: page }) => { const mainContainer = page.locator('[data-testid="main-container"]'); const containerBox = await mainContainer.boundingBox(); @@ -242,36 +207,30 @@ test.describe('Responsive design - Large Desktop', () => { } }); - test('should maintain readable line lengths', async ({ page }) => { + test('should maintain readable line lengths', async ({ authenticatedPage: page }) => { await page.fill('[data-testid="chat-input"]', 'Test message on large display'); await page.click('[data-testid="chat-submit"]'); const botMessage = page.locator('[data-testid="bot-message"]').last(); await expect(botMessage).toBeVisible({ timeout: 15000 }); - // Text should not span the entire width (max-width should be applied) const messageBox = await botMessage.boundingBox(); expect(messageBox).toBeTruthy(); if (messageBox) { - // Content should have a reasonable max-width for readability expect(messageBox.width).toBeLessThan(1200); } }); }); test.describe('Responsive design - Orientation changes', () => { - test('should handle portrait to landscape transition', async ({ page }) => { - // Start in portrait + test('should handle portrait to landscape transition', async ({ authenticatedPage: page }) => { await page.setViewportSize({ width: 375, height: 667 }); - await page.goto('/'); const chatInput = page.locator('[data-testid="chat-input"]'); await expect(chatInput).toBeVisible(); - // Switch to landscape await page.setViewportSize({ width: 667, height: 375 }); - // UI should still be functional await expect(chatInput).toBeVisible(); await chatInput.fill('Orientation test'); @@ -279,18 +238,14 @@ test.describe('Responsive design - Orientation changes', () => { await expect(submitButton).toBeEnabled(); }); - test('should handle landscape to portrait transition', async ({ page }) => { - // Start in landscape + test('should handle landscape to portrait transition', async ({ authenticatedPage: page }) => { await page.setViewportSize({ width: 667, height: 375 }); - await page.goto('/'); await page.fill('[data-testid="chat-input"]', 'Landscape message'); await page.click('[data-testid="mobile-submit-button"]'); - // Switch to portrait await page.setViewportSize({ width: 375, height: 667 }); - // Message should still be visible const userMessage = page.locator('[data-testid="user-message"]').last(); await expect(userMessage).toBeVisible(); }); diff --git a/tests/sidebar.spec.ts b/tests/sidebar.spec.ts index 5b31a1d7..9c397c19 100644 --- a/tests/sidebar.spec.ts +++ b/tests/sidebar.spec.ts @@ -1,18 +1,14 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; -test.describe('Sidebar and Chat History', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.waitForSelector('[data-testid="chat-input"]'); - }); +test.describe('Sidebar and Chat History @smoke', () => { - test('should open the history panel', async ({ page }) => { + test('should open the history panel', async ({ authenticatedPage: page }) => { await page.click('[data-testid="history-button"]'); const historyPanel = page.locator('[data-testid="history-panel"]'); await expect(historyPanel).toBeVisible(); }); - test('should clear the chat history', async ({ page }) => { + test('should clear the chat history', async ({ authenticatedPage: page }) => { // First, send a message to create a history item await page.fill('[data-testid="chat-input"]', 'Create history'); await page.click('[data-testid="chat-submit"]'); @@ -27,4 +23,5 @@ test.describe('Sidebar and Chat History', () => { const historyItem = page.locator('[data-testid^="history-item-"]'); await expect(historyItem).not.toBeVisible(); }); + });