From 26b2854688d2630f16d12ec473398fa81800f01d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:31:08 +0000 Subject: [PATCH 1/5] feat: optimize Playwright E2E testing suite for CI efficiency This commit implements several key improvements to the E2E testing infrastructure: - Enables 4 workers in CI and implements 4-way sharding in GitHub Actions. - Introduces `authenticatedPage` and `testImage` fixtures for better isolation and reduced boilerplate. - Replaces arbitrary waits with deterministic `expect.poll` in chat tests. - Optimizes the browser matrix to skip mobile tests on desktop and vice versa. - Configures Playwright `webServer` for automatic app lifecycle management. - Implements `@smoke` tagging strategy for critical path verification. - Ensures test data isolation using unique identifiers and automatic storage cleanup. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .github/workflows/playwright.yml | 29 +++++----- package.json | 4 ++ playwright.config.ts | 15 +++-- tests/calendar.spec.ts | 60 ++++++++++---------- tests/chat.spec.ts | 33 +++++------ tests/fixtures.ts | 44 +++++++++++++++ tests/global-setup.ts | 9 +++ tests/header.spec.ts | 15 ++--- tests/images.spec.ts | 87 +++++++--------------------- tests/map.spec.ts | 20 ++++--- tests/mobile.spec.ts | 15 ++--- tests/responsive.spec.ts | 97 +++++++++----------------------- tests/sidebar.spec.ts | 13 ++--- 13 files changed, 198 insertions(+), 243 deletions(-) create mode 100644 tests/fixtures.ts create mode 100644 tests/global-setup.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index e5c2cafe..a32edc19 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,6 +10,11 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4] + shardTotal: [4] steps: - name: Checkout code @@ -26,29 +31,21 @@ jobs: - name: Install Playwright Browsers run: bunx playwright install --with-deps - - name: Build application - run: bun run build - env: - # Add any required environment variables for build - NODE_ENV: production - - - 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 + run: bun run test:e2e:shard env: CI: true + NODE_ENV: production + SHARD_INDEX: ${{ matrix.shardIndex }} + SHARD_TOTAL: ${{ matrix.shardTotal }} + # Add other necessary env vars here (e.g., Supabase, etc.) + AUTH_DISABLED_FOR_DEV: true - name: Upload test results uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report + name: playwright-report-${{ matrix.shardIndex }} path: playwright-report/ retention-days: 30 @@ -56,6 +53,6 @@ jobs: uses: actions/upload-artifact@v4 if: failure() with: - name: playwright-screenshots + name: playwright-screenshots-${{ matrix.shardIndex }} path: test-results/ retention-days: 7 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..3d3c46db 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,41 +2,48 @@ 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, + workers: process.env.CI ? 4 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', + storageState: undefined, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + testMatch: /.*\.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, - }, */ + }, }); 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(); }); + }); From e7b97c601b40b55316bc2daef72d5a4e792a0e40 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:55:12 +0000 Subject: [PATCH 2/5] chore: improve CI progress visibility for Playwright tests - Added 'list' and 'github' reporters for CI runs. - Enabled stdout/stderr piping for webServer to show build/server logs. - Added shard info to the GitHub Actions test step name. - Addressed PR feedback regarding Playwright progress reporting. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .github/workflows/playwright.yml | 2 +- playwright.config.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a32edc19..c20992f1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -31,7 +31,7 @@ jobs: - name: Install Playwright Browsers run: bunx playwright install --with-deps - - name: Run Playwright tests + - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) run: bun run test:e2e:shard env: CI: true diff --git a/playwright.config.ts b/playwright.config.ts index 3d3c46db..4f03a472 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 4 : undefined, - reporter: 'html', + reporter: process.env.CI ? [['list'], ['github'], ['html']] : 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', @@ -45,5 +45,7 @@ export default defineConfig({ url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 600000, + stdout: 'pipe', + stderr: 'pipe', }, }); From 65c34d2d3b1a70b3beeec01666264cf2639eec19 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:05:45 +0000 Subject: [PATCH 3/5] refactor: refine Playwright config for project isolation and state - Removed redundant `storageState: undefined` from base config. - Updated `chromium` project to ignore mobile specs, matching other desktop projects. - Ensures mobile-only tests do not run in desktop browser projects. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- playwright.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 4f03a472..e4938993 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,13 +11,12 @@ export default defineConfig({ use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', - storageState: undefined, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.spec\.ts/, + testIgnore: /mobile\.spec\.ts/, }, { name: 'firefox', From 24387401e3406de54fc2df9762efa5daf355ab17 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:55:24 +0000 Subject: [PATCH 4/5] fix: stabilize CI testing with DB service and report merging - Added PostgreSQL service container to GitHub Actions. - Added migration step to CI to ensure app readiness. - Reduced CI workers to 2 to prevent resource contention. - Implemented Playwright blob reporting and report merging. - Improved diagnostic logging in CI. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- .github/workflows/playwright.yml | 70 +++++++++++++++++++++++++------ CC BY-NC 4.0.docx | Bin 0 -> 11099 bytes playwright.config.ts | 4 +- 3 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 CC BY-NC 4.0.docx diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index c20992f1..be1b37c2 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,12 +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 @@ -31,28 +47,56 @@ jobs: - name: Install Playwright Browsers run: bunx playwright install --with-deps + - name: Run migrations + run: bun run db:migrate + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres + - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) - run: bun run test:e2e:shard + run: bunx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} env: CI: true NODE_ENV: production - SHARD_INDEX: ${{ matrix.shardIndex }} - SHARD_TOTAL: ${{ matrix.shardTotal }} - # Add other necessary env vars here (e.g., Supabase, etc.) 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-${{ matrix.shardIndex }} - path: playwright-report/ - retention-days: 30 + name: blob-report-${{ matrix.shardIndex }} + path: blob-report/ + retention-days: 1 - - name: Upload test screenshots + merge-reports: + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - 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-${{ matrix.shardIndex }} - path: test-results/ - retention-days: 7 + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/CC BY-NC 4.0.docx b/CC BY-NC 4.0.docx new file mode 100644 index 0000000000000000000000000000000000000000..0d8f7d559328446c99bded42597b2e03fbd0f6c4 GIT binary patch literal 11099 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 Z{l5XJBnt)o&nvJWJNO6FqSXIf{U0 Date: Sat, 31 Jan 2026 11:20:52 +0000 Subject: [PATCH 5/5] chore: synchronize from main and stabilize CI pipeline - Merged the latest changes from `main`. - Stabilized CI testing with a PostgreSQL service container and migrations. - Reduced CI worker count for standard runner stability. - Implemented sharded report merging in GitHub Actions. - Refined Playwright configuration for project isolation and progress reporting. - Addressed PR feedback regarding test progress visibility and diagnostic logs. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- CC BY-NC 4.0.docx | Bin 11099 -> 0 bytes app/actions.tsx | 24 +++++++++++++++++++++--- components/header-search-button.tsx | 1 + lib/agents/resolution-search.tsx | 25 +++++++++++++++++-------- 4 files changed, 39 insertions(+), 11 deletions(-) delete mode 100644 CC BY-NC 4.0.docx diff --git a/CC BY-NC 4.0.docx b/CC BY-NC 4.0.docx deleted file mode 100644 index 0d8f7d559328446c99bded42597b2e03fbd0f6c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11099 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 Z{l5XJBnt)o&nvJWJNO6FqSXIf{U0 [...currentMessages, responseMessage as any]) diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index 88dd38e8..e208839d 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -1,4 +1,4 @@ -import { CoreMessage, generateObject } from 'ai' +import { CoreMessage, streamObject } from 'ai' import { getModel } from '@/lib/utils' import { z } from 'zod' @@ -23,7 +23,14 @@ const resolutionSearchSchema = z.object({ }).describe('A GeoJSON object containing points of interest and classified land features to be overlaid on the map.'), }) -export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC') { +export interface DrawnFeature { + id: string; + type: 'Polygon' | 'LineString'; + measurement: string; + geometry: any; +} + +export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC', drawnFeatures?: DrawnFeature[]) { const localTime = new Date().toLocaleString('en-US', { timeZone: timezone, hour: '2-digit', @@ -38,6 +45,11 @@ export async function resolutionSearch(messages: CoreMessage[], timezone: string const systemPrompt = ` As a geospatial analyst, your task is to analyze the provided satellite image of a geographic location. The current local time at this location is ${localTime}. + +${drawnFeatures && drawnFeatures.length > 0 ? `The user has drawn the following features on the map for your reference: +${drawnFeatures.map(f => `- ${f.type} with measurement ${f.measurement}`).join('\n')} +Use these user-drawn areas/lines as primary areas of interest for your analysis.` : ''} + Your analysis should be comprehensive and include the following components: 1. **Land Feature Classification:** Identify and describe the different types of land cover visible in the image (e.g., urban areas, forests, water bodies, agricultural fields). @@ -57,14 +69,11 @@ Analyze the user's prompt and the image to provide a holistic understanding of t message.content.some(part => part.type === 'image') ) - // Use generateObject to get the full object at once. - const { object } = await generateObject({ + // Use streamObject to get partial results. + return streamObject({ model: await getModel(hasImage), system: systemPrompt, messages: filteredMessages, schema: resolutionSearchSchema, }) - - // Return the complete, validated object. - return object -} \ No newline at end of file +}