Mega push vol 5, working on messaging now
This commit is contained in:
147
tests/e2e/app.spec.ts
Normal file
147
tests/e2e/app.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login, TEST_ORG_SLUG, TEST_EMAIL, waitForHydration, navigateTo } from './helpers';
|
||||
|
||||
test('should log in and reach org dashboard', async ({ page }) => {
|
||||
await login(page);
|
||||
await expect(page).toHaveURL(new RegExp(`/${TEST_ORG_SLUG}`));
|
||||
});
|
||||
|
||||
test.describe('Sidebar', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
});
|
||||
|
||||
test('should display logo in sidebar', async ({ page }) => {
|
||||
const logo = page.locator('aside svg');
|
||||
await expect(logo.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display user avatar and email in sidebar', async ({ page }) => {
|
||||
const userMenuBtn = page.locator('.user-menu-container button').first();
|
||||
await expect(userMenuBtn).toBeVisible();
|
||||
// Should show user email
|
||||
await expect(page.locator('.user-menu-container').getByText(TEST_EMAIL)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show user dropdown on avatar click with correct items', async ({ page }) => {
|
||||
const btn = page.locator('.user-menu-container button[aria-haspopup="true"]');
|
||||
await expect(btn).toBeVisible();
|
||||
await btn.click();
|
||||
await expect(btn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
await expect(page.getByText('Account Settings').first()).toBeVisible();
|
||||
await expect(page.getByText('Switch Organization')).toBeVisible();
|
||||
await expect(page.getByText('Log Out')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should close user dropdown on Escape', async ({ page }) => {
|
||||
const btn = page.locator('.user-menu-container button[aria-haspopup="true"]');
|
||||
await btn.click();
|
||||
await expect(btn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(btn).toHaveAttribute('aria-expanded', 'false', { timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should navigate to account settings from dropdown', async ({ page }) => {
|
||||
const btn = page.locator('.user-menu-container button[aria-haspopup="true"]');
|
||||
await btn.click();
|
||||
await expect(btn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
await page.getByText('Account Settings').first().click();
|
||||
await page.waitForURL(new RegExp(`/${TEST_ORG_SLUG}/account`), { timeout: 10000 });
|
||||
await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('sidebar nav items should be visible', async ({ page }) => {
|
||||
const aside = page.locator('aside');
|
||||
await expect(aside).toBeVisible();
|
||||
const navLinks = aside.locator('nav a');
|
||||
const count = await navLinks.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Account Settings Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/account`);
|
||||
});
|
||||
|
||||
test('should display Profile section with all elements', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible();
|
||||
await expect(page.getByText('Display Name')).toBeVisible();
|
||||
await expect(page.getByText('Email', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sync Google' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Save Profile' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display Appearance section with color picker', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Appearance' })).toBeVisible();
|
||||
await expect(page.getByText('Accent Color')).toBeVisible();
|
||||
// Custom color picker label
|
||||
const colorPicker = page.locator('label[title="Custom color"]');
|
||||
await expect(colorPicker).toBeVisible();
|
||||
// Org theme toggle
|
||||
await expect(page.getByText('Use Organization Theme')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Save Preferences' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display Security & Sessions section', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Security & Sessions' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Send Reset Email' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sign Out Other Sessions' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('accent color swatches should be clickable', async ({ page }) => {
|
||||
const swatches = page.locator('.flex.flex-wrap button[type="button"]');
|
||||
const count = await swatches.count();
|
||||
expect(count).toBeGreaterThanOrEqual(8);
|
||||
// Click the third swatch (Red)
|
||||
await swatches.nth(2).click();
|
||||
await expect(swatches.nth(2)).toHaveClass(/border-white/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Kanban Page - ContextMenu', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/kanban`);
|
||||
});
|
||||
|
||||
test('should load kanban page', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Kanban' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have context menu button in header', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
await expect(moreBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open context menu on click', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
await moreBtn.click();
|
||||
await expect(moreBtn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
// Menu dropdown should be visible
|
||||
const menu = page.locator('.context-menu-container div.absolute');
|
||||
await expect(menu).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Calendar Page - ContextMenu', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/calendar`);
|
||||
});
|
||||
|
||||
test('should load calendar page', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Calendar' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have context menu button in header', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
await expect(moreBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show Refresh Events in context menu', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
await moreBtn.click();
|
||||
await expect(moreBtn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
await expect(page.getByText('Refresh Events')).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
});
|
||||
18
tests/e2e/auth.setup.ts
Normal file
18
tests/e2e/auth.setup.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test as setup, expect } from '@playwright/test';
|
||||
import { TEST_EMAIL, TEST_PASSWORD, TEST_ORG_SLUG } from './helpers';
|
||||
|
||||
const authFile = 'tests/e2e/.auth/user.json';
|
||||
|
||||
setup('authenticate', async ({ page }) => {
|
||||
// networkidle ensures Svelte 5 hydration completes (event delegation attached)
|
||||
await page.goto('/login', { waitUntil: 'networkidle' });
|
||||
await page.fill('input[type="email"]', TEST_EMAIL);
|
||||
await page.fill('input[type="password"]', TEST_PASSWORD);
|
||||
await page.click('button[type="submit"]');
|
||||
// After login, the app redirects to "/" (org selector)
|
||||
await page.waitForURL('/', { timeout: 30000 });
|
||||
// Verify we see the org selector
|
||||
await expect(page.getByRole('link', { name: /root-test/i }).first()).toBeVisible({ timeout: 10000 });
|
||||
// Save auth state
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
191
tests/e2e/cleanup.ts
Normal file
191
tests/e2e/cleanup.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Global teardown: delete all test-created data from Supabase.
|
||||
* Runs after all Playwright tests complete.
|
||||
*
|
||||
* Matches documents/folders/kanbans by name prefixes used in tests:
|
||||
* "Test Folder", "Test Doc", "Test Board", "Nav Folder", "Rename Me", "Renamed"
|
||||
* Matches kanban boards by name prefix: "PW Board", "Board A", "Board B"
|
||||
* Matches org_invites by email pattern: "playwright-test-*@example.com"
|
||||
* Matches org_roles by name prefix: "Tester"
|
||||
*/
|
||||
|
||||
// Load .env manually since we're outside Vite
|
||||
function loadEnv() {
|
||||
try {
|
||||
const envPath = path.resolve(process.cwd(), '.env');
|
||||
const content = fs.readFileSync(envPath, 'utf-8');
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eqIdx = trimmed.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
const key = trimmed.slice(0, eqIdx);
|
||||
const value = trimmed.slice(eqIdx + 1);
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
} catch { /* .env not found — rely on process.env */ }
|
||||
}
|
||||
loadEnv();
|
||||
|
||||
const SUPABASE_URL = process.env.PUBLIC_SUPABASE_URL || '';
|
||||
const SUPABASE_KEY = process.env.PUBLIC_SUPABASE_ANON_KEY || '';
|
||||
|
||||
// Name prefixes used by tests when creating data
|
||||
const DOC_PREFIXES = ['Test Folder', 'Test Doc', 'Test Board', 'Nav Folder', 'Rename Me', 'Renamed'];
|
||||
const BOARD_PREFIXES = ['PW Board', 'PW Card Board', 'PW Detail Board', 'Board A', 'Board B'];
|
||||
const ROLE_PREFIX = 'Tester';
|
||||
const TAG_PREFIX = 'PW Tag';
|
||||
const EVENT_PREFIXES = ['PW Event', 'PW Detail', 'PW Delete'];
|
||||
const INVITE_EMAIL_PATTERN = 'playwright-test-%@example.com';
|
||||
|
||||
export default async function globalTeardown() {
|
||||
if (!SUPABASE_KEY) {
|
||||
console.log('[cleanup] No SUPABASE_ANON_KEY — skipping cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
// Authenticate using the test user credentials directly
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const { error: authError } = await supabase.auth.signInWithPassword({
|
||||
email: 'tipilan@ituk.ee',
|
||||
password: 'gu&u6QTMbJK7nT',
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
console.log('[cleanup] Auth failed — skipping cleanup:', authError.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the org ID for root-test
|
||||
const { data: org } = await supabase
|
||||
.from('organizations')
|
||||
.select('id')
|
||||
.eq('slug', 'root-test')
|
||||
.single();
|
||||
|
||||
if (!org) {
|
||||
console.log('[cleanup] root-test org not found — skipping cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
const orgId = org.id;
|
||||
let totalDeleted = 0;
|
||||
|
||||
// 1. Delete test documents (folders, docs, kanbans)
|
||||
for (const prefix of DOC_PREFIXES) {
|
||||
const { data: docs } = await supabase
|
||||
.from('documents')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('name', `${prefix}%`);
|
||||
|
||||
if (docs && docs.length > 0) {
|
||||
const ids = docs.map(d => d.id);
|
||||
const { error } = await supabase
|
||||
.from('documents')
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
|
||||
if (!error) {
|
||||
totalDeleted += docs.length;
|
||||
} else {
|
||||
console.log(`[cleanup] Failed to delete docs with prefix "${prefix}":`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete test kanban boards
|
||||
for (const prefix of BOARD_PREFIXES) {
|
||||
const { data: boards } = await supabase
|
||||
.from('kanban_boards')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('name', `${prefix}%`);
|
||||
|
||||
if (boards && boards.length > 0) {
|
||||
const ids = boards.map(b => b.id);
|
||||
const { error } = await supabase
|
||||
.from('kanban_boards')
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
|
||||
if (!error) {
|
||||
totalDeleted += boards.length;
|
||||
} else {
|
||||
console.log(`[cleanup] Failed to delete boards with prefix "${prefix}":`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Delete test invites (playwright-test-*@example.com)
|
||||
const { data: invites } = await supabase
|
||||
.from('org_invites')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('email', INVITE_EMAIL_PATTERN);
|
||||
|
||||
if (invites && invites.length > 0) {
|
||||
const ids = invites.map(i => i.id);
|
||||
await supabase.from('org_invites').delete().in('id', ids);
|
||||
totalDeleted += invites.length;
|
||||
}
|
||||
|
||||
// 4. Delete test roles
|
||||
const { data: roles } = await supabase
|
||||
.from('org_roles')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('name', `${ROLE_PREFIX}%`);
|
||||
|
||||
if (roles && roles.length > 0) {
|
||||
const ids = roles.map(r => r.id);
|
||||
await supabase.from('org_roles').delete().in('id', ids);
|
||||
totalDeleted += roles.length;
|
||||
}
|
||||
|
||||
// 5. Delete test tags
|
||||
const { data: tags } = await supabase
|
||||
.from('tags')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('name', `${TAG_PREFIX}%`);
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
const ids = tags.map(t => t.id);
|
||||
await supabase.from('tags').delete().in('id', ids);
|
||||
totalDeleted += tags.length;
|
||||
}
|
||||
|
||||
// 6. Delete test calendar events
|
||||
for (const prefix of EVENT_PREFIXES) {
|
||||
const { data: events } = await supabase
|
||||
.from('calendar_events')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('title', `${prefix}%`);
|
||||
|
||||
if (events && events.length > 0) {
|
||||
const ids = events.map(e => e.id);
|
||||
const { error } = await supabase
|
||||
.from('calendar_events')
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
|
||||
if (!error) {
|
||||
totalDeleted += events.length;
|
||||
} else {
|
||||
console.log(`[cleanup] Failed to delete events with prefix "${prefix}":`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalDeleted > 0) {
|
||||
console.log(`[cleanup] Deleted ${totalDeleted} test-created items from root-test org`);
|
||||
} else {
|
||||
console.log('[cleanup] No test data to clean up');
|
||||
}
|
||||
}
|
||||
550
tests/e2e/features.spec.ts
Normal file
550
tests/e2e/features.spec.ts
Normal file
@@ -0,0 +1,550 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login, TEST_ORG_SLUG, TEST_EMAIL, waitForHydration, navigateTo } from './helpers';
|
||||
|
||||
// ─── File Management ────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('File Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/documents`);
|
||||
});
|
||||
|
||||
test('should load files page with header and + New button', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Files' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '+ New' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open Create New modal with type selectors', async ({ page }) => {
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Create New')).toBeVisible();
|
||||
// Type selector buttons
|
||||
await expect(modal.getByText('Document')).toBeVisible();
|
||||
await expect(modal.getByText('Folder')).toBeVisible();
|
||||
await expect(modal.getByText('Kanban')).toBeVisible();
|
||||
// Name input and Create button
|
||||
await expect(modal.getByText('Name')).toBeVisible();
|
||||
await expect(modal.getByRole('button', { name: 'Create' })).toBeVisible();
|
||||
await expect(modal.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a folder and see it in the file list', async ({ page }) => {
|
||||
const folderName = `Test Folder ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
// Select Folder type
|
||||
await modal.getByText('Folder').click();
|
||||
// Fill name
|
||||
await modal.getByPlaceholder('Folder name').fill(folderName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
// Modal should close
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
// Folder should appear in the file list
|
||||
await expect(page.getByText(folderName)).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should create a document and navigate to editor', async ({ page }) => {
|
||||
const docName = `Test Doc ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
// Document type is default
|
||||
await modal.getByPlaceholder('Document name').fill(docName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
// Should navigate to the file editor page
|
||||
await page.waitForURL(new RegExp(`/${TEST_ORG_SLUG}/documents/file/`), { timeout: 10000 });
|
||||
await expect(page.getByText(docName).first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should create a kanban board from files and navigate to it', async ({ page }) => {
|
||||
const boardName = `Test Board ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
// Select Kanban type
|
||||
await modal.getByText('Kanban').click();
|
||||
await modal.getByPlaceholder('Kanban board name').fill(boardName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
// Should navigate to the kanban file page
|
||||
await page.waitForURL(new RegExp(`/${TEST_ORG_SLUG}/documents/file/`), { timeout: 10000 });
|
||||
await waitForHydration(page);
|
||||
await expect(page.getByText(boardName).first()).toBeVisible({ timeout: 5000 });
|
||||
// Default columns should be created
|
||||
await expect(page.getByText('To Do').first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('In Progress').first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Done').first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should navigate into a folder and see breadcrumbs', async ({ page }) => {
|
||||
// First create a folder
|
||||
const folderName = `Nav Folder ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByText('Folder').click();
|
||||
await modal.getByPlaceholder('Folder name').fill(folderName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
// Click the folder button to navigate into it
|
||||
const folderBtn = page.locator('button', { hasText: folderName }).first();
|
||||
await folderBtn.scrollIntoViewIfNeeded();
|
||||
await folderBtn.click();
|
||||
await page.waitForURL(new RegExp(`/${TEST_ORG_SLUG}/documents/folder/`), { timeout: 10000 });
|
||||
await waitForHydration(page);
|
||||
// Breadcrumb should show Home > FolderName
|
||||
const breadcrumb = page.locator('nav');
|
||||
await expect(breadcrumb.getByText('Home')).toBeVisible({ timeout: 3000 });
|
||||
await expect(breadcrumb.getByText(folderName)).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should rename a file via right-click context menu', async ({ page }) => {
|
||||
// Create a document first
|
||||
const originalName = `Rename Me ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByText('Folder').click();
|
||||
await modal.getByPlaceholder('Folder name').fill(originalName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(originalName).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Right-click to open context menu
|
||||
await page.getByText(originalName).first().click({ button: 'right' });
|
||||
// Context menu should appear — find the Rename button inside the fixed z-50 context menu
|
||||
const contextMenuPanel = page.locator('.fixed.z-50.bg-night');
|
||||
await expect(contextMenuPanel).toBeVisible({ timeout: 3000 });
|
||||
const renameBtn = contextMenuPanel.locator('button', { hasText: 'Rename' });
|
||||
await expect(renameBtn).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Click Rename
|
||||
await renameBtn.click();
|
||||
const renameModal = page.getByRole('dialog');
|
||||
await expect(renameModal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Clear and type new name
|
||||
const newName = `Renamed ${Date.now()}`;
|
||||
const nameInput = renameModal.locator('input');
|
||||
await nameInput.clear();
|
||||
await nameInput.fill(newName);
|
||||
await renameModal.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(renameModal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// New name should be visible
|
||||
await expect(page.getByText(newName)).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should toggle between grid and list view', async ({ page }) => {
|
||||
// Default is grid view - look for the toggle button
|
||||
const toggleBtn = page.locator('button[title="Toggle view"]');
|
||||
await expect(toggleBtn).toBeVisible();
|
||||
// Click to switch to list view
|
||||
await toggleBtn.click();
|
||||
// The file list container should use list layout (flex-col)
|
||||
const listContainer = page.locator('div[role="list"].flex.flex-col');
|
||||
await expect(listContainer).toBeVisible({ timeout: 3000 });
|
||||
// Click again to switch back to grid
|
||||
await toggleBtn.click();
|
||||
const gridContainer = page.locator('div[role="list"].grid');
|
||||
await expect(gridContainer).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Kanban Board ───────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Kanban Board Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/kanban`);
|
||||
});
|
||||
|
||||
test('should load kanban page with header', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Kanban' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '+ New' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open Create Board modal and create a board', async ({ page }) => {
|
||||
const boardName = `PW Board ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Create Board')).toBeVisible();
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(boardName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
// Board should be selected and show default columns
|
||||
await expect(page.getByText('To Do')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('In Progress')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Done')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should show board selector when multiple boards exist', async ({ page }) => {
|
||||
// Create two boards
|
||||
for (const name of [`Board A ${Date.now()}`, `Board B ${Date.now()}`]) {
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(name);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
// Wait for board to load
|
||||
await expect(page.getByText('To Do')).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
// Board selector pills should be visible
|
||||
const boardButtons = page.locator('button.rounded-\\[32px\\]');
|
||||
const count = await boardButtons.count();
|
||||
expect(count).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Settings: Members & Roles ──────────────────────────────────────────────
|
||||
|
||||
test.describe('Settings Page - Members', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/settings`);
|
||||
});
|
||||
|
||||
test('should load settings page with tabs', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'General' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Members' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Roles' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Integrations' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should switch to Members tab and show team members', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Members' }).click();
|
||||
// Team Members heading should show a non-zero count
|
||||
const heading = page.locator('h2').filter({ hasText: 'Team Members' });
|
||||
await expect(heading).toBeVisible({ timeout: 5000 });
|
||||
await expect(heading).not.toHaveText('Team Members (0)');
|
||||
await expect(page.getByRole('button', { name: 'Invite Member' })).toBeVisible();
|
||||
// The test user's email should appear in the members list (scope to main to avoid sidebar match)
|
||||
const main = page.locator('main');
|
||||
await expect(main.getByText(TEST_EMAIL).first()).toBeVisible({ timeout: 5000 });
|
||||
// Each member should have a role badge
|
||||
await expect(main.getByText('owner').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open Invite Member modal with email and role fields', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Members' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Invite Member' })).toBeVisible({ timeout: 3000 });
|
||||
await page.getByRole('button', { name: 'Invite Member' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Invite Member')).toBeVisible();
|
||||
await expect(modal.getByText('Email address')).toBeVisible();
|
||||
await expect(modal.getByText('Role')).toBeVisible();
|
||||
await expect(modal.getByRole('button', { name: 'Send Invite' })).toBeVisible();
|
||||
await expect(modal.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should send an invite and show it in pending invites', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Members' }).click();
|
||||
await page.getByRole('button', { name: 'Invite Member' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
const testInviteEmail = `playwright-test-${Date.now()}@example.com`;
|
||||
await modal.getByPlaceholder('colleague@example.com').fill(testInviteEmail);
|
||||
await modal.getByRole('button', { name: 'Send Invite' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Pending invite should appear
|
||||
await expect(page.getByText('Pending Invites')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(testInviteEmail).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Clean up: cancel the invite — find the row containing the email, then its Cancel button
|
||||
const inviteRow = page.locator('.bg-light\\/5').filter({ hasText: testInviteEmail });
|
||||
await inviteRow.getByRole('button', { name: 'Cancel' }).click();
|
||||
await expect(page.getByText(testInviteEmail)).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should show pending invites section when invites exist', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Members' }).click();
|
||||
await expect(page.getByText('Team Members').first()).toBeVisible({ timeout: 5000 });
|
||||
// Check if Pending Invites section exists (from previous test runs or this session)
|
||||
const hasPending = await page.getByText('Pending Invites').isVisible().catch(() => false);
|
||||
if (hasPending) {
|
||||
// Each invite should have Copy Link and Cancel buttons
|
||||
await expect(page.getByRole('button', { name: 'Copy Link' }).first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Settings Page - Roles', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/settings`);
|
||||
});
|
||||
|
||||
test('should switch to Roles tab and show existing roles', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Roles' }).click();
|
||||
await expect(page.getByText('Roles', { exact: true }).first()).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByRole('button', { name: 'Create Role' })).toBeVisible();
|
||||
// System roles should be visible
|
||||
await expect(page.getByText('Owner')).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should open Create Role modal with name, color, and permissions', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Roles' }).click();
|
||||
await page.getByRole('button', { name: 'Create Role' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Create Role')).toBeVisible();
|
||||
await expect(modal.getByText('Name')).toBeVisible();
|
||||
await expect(modal.getByText('Color')).toBeVisible();
|
||||
await expect(modal.getByText('Permissions')).toBeVisible();
|
||||
// Permission groups
|
||||
await expect(modal.getByText('Documents')).toBeVisible();
|
||||
await expect(modal.getByText('Kanban')).toBeVisible();
|
||||
await expect(modal.getByText('Calendar')).toBeVisible();
|
||||
await expect(modal.getByText('Members')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a custom role and see it in the list', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const roleName = `Tester ${Date.now()}`;
|
||||
await page.getByRole('button', { name: 'Roles' }).click();
|
||||
await page.getByRole('button', { name: 'Create Role' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await modal.getByPlaceholder('e.g., Moderator').fill(roleName);
|
||||
// Select a color (click the second color swatch)
|
||||
const colorSwatches = modal.locator('button.rounded-full');
|
||||
await colorSwatches.nth(1).click();
|
||||
// Create
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Role should appear in the list
|
||||
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Clean up: delete the role — set up dialog handler BEFORE clicking
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
// Find the specific role card that contains the role name and click its Delete button
|
||||
const deleteBtn = page.getByRole('button', { name: 'Delete' }).last();
|
||||
await deleteBtn.click();
|
||||
await expect(page.getByText(roleName)).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Settings: Tags ─────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Settings Page - Tags', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/settings`);
|
||||
});
|
||||
|
||||
test('should switch to Tags tab and show tag management UI', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Tags' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Create Tag' })).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should create a tag and see it in the list', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const tagName = `PW Tag ${Date.now()}`;
|
||||
await page.getByRole('button', { name: 'Tags' }).click();
|
||||
await page.getByRole('button', { name: 'Create Tag' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g., Bug').fill(tagName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(tagName)).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Clean up: delete the tag
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
const tagCard = page.locator('div').filter({ hasText: tagName }).last();
|
||||
const deleteBtn = tagCard.getByRole('button').filter({ hasText: /delete/i }).first();
|
||||
if (await deleteBtn.isVisible()) {
|
||||
await deleteBtn.click();
|
||||
await expect(page.getByText(tagName)).not.toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Settings: General ──────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Settings Page - General', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/settings`);
|
||||
});
|
||||
|
||||
test('should show General tab by default with org info', async ({ page }) => {
|
||||
// General tab should be active by default - check for the Settings heading
|
||||
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible({ timeout: 3000 });
|
||||
// General button should be the active tab
|
||||
await expect(page.getByRole('button', { name: 'General' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Calendar CRUD ──────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Calendar - Event CRUD', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/calendar`);
|
||||
});
|
||||
|
||||
test('should open create event modal via + New button', async ({ page }) => {
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Title')).toBeVisible();
|
||||
await expect(modal.getByText('Date')).toBeVisible();
|
||||
await expect(modal.getByRole('button', { name: 'Create' })).toBeVisible();
|
||||
await expect(modal.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create an event and see it on the calendar', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const eventTitle = `PW Event ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Fill in event details
|
||||
await modal.getByLabel('Title').fill(eventTitle);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Event should appear on the calendar
|
||||
await expect(page.getByText(eventTitle).first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should click an event to view details', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
// Create an event first
|
||||
const eventTitle = `PW Detail ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByLabel('Title').fill(eventTitle);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click the event
|
||||
await page.getByText(eventTitle).first().click();
|
||||
const detailModal = page.getByRole('dialog');
|
||||
await expect(detailModal).toBeVisible({ timeout: 3000 });
|
||||
await expect(detailModal.getByText(eventTitle)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should delete an event', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const eventTitle = `PW Delete ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByLabel('Title').fill(eventTitle);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(eventTitle).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click event to open detail modal, then delete
|
||||
await page.getByText(eventTitle).first().click();
|
||||
const detailModal = page.getByRole('dialog');
|
||||
await expect(detailModal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
const deleteBtn = detailModal.getByRole('button', { name: 'Delete' });
|
||||
if (await deleteBtn.isVisible()) {
|
||||
await deleteBtn.click();
|
||||
await expect(detailModal).not.toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Kanban Card CRUD ───────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Kanban - Card CRUD', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/kanban`);
|
||||
});
|
||||
|
||||
test('should create a board and add a card to a column', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const boardName = `PW Card Board ${Date.now()}`;
|
||||
|
||||
// Create board
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(boardName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('To Do')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Add a card — click the + button in the first column
|
||||
const addCardBtn = page.locator('button[title="Add card"]').first();
|
||||
if (await addCardBtn.isVisible()) {
|
||||
await addCardBtn.click();
|
||||
} else {
|
||||
// Fallback: look for any add button in the column area
|
||||
const plusBtn = page.locator('button').filter({ hasText: '+' }).first();
|
||||
await plusBtn.click();
|
||||
}
|
||||
|
||||
// Card creation modal or inline input should appear
|
||||
const cardModal = page.getByRole('dialog');
|
||||
const hasModal = await cardModal.isVisible().catch(() => false);
|
||||
if (hasModal) {
|
||||
const cardTitle = `PW Card ${Date.now()}`;
|
||||
await cardModal.getByLabel('Title').fill(cardTitle);
|
||||
await cardModal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(cardModal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(cardTitle).first()).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('should open card detail modal on card click', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const boardName = `PW Detail Board ${Date.now()}`;
|
||||
|
||||
// Create board
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(boardName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('To Do')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Add a card via the + button
|
||||
const addCardBtn = page.locator('button[title="Add card"]').first();
|
||||
if (await addCardBtn.isVisible()) {
|
||||
await addCardBtn.click();
|
||||
const cardModal = page.getByRole('dialog');
|
||||
const hasModal = await cardModal.isVisible().catch(() => false);
|
||||
if (hasModal) {
|
||||
const cardTitle = `PW Click Card ${Date.now()}`;
|
||||
await cardModal.getByLabel('Title').fill(cardTitle);
|
||||
await cardModal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(cardModal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click the card to open detail modal
|
||||
await page.getByText(cardTitle).first().click();
|
||||
const detailModal = page.getByRole('dialog');
|
||||
await expect(detailModal).toBeVisible({ timeout: 5000 });
|
||||
await expect(detailModal.getByText(cardTitle)).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Settings: Integrations ─────────────────────────────────────────────────
|
||||
|
||||
test.describe('Settings Page - Integrations', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/settings`);
|
||||
});
|
||||
|
||||
test('should show Integrations tab with Google Calendar card', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Integrations' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Google Calendar' })).toBeVisible({ timeout: 3000 });
|
||||
// Discord and Slack should show as "Coming soon"
|
||||
await expect(page.getByRole('heading', { name: 'Discord' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Slack' })).toBeVisible();
|
||||
await expect(page.getByText('Coming soon').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
41
tests/e2e/helpers.ts
Normal file
41
tests/e2e/helpers.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
export const TEST_EMAIL = 'tipilan@ituk.ee';
|
||||
export const TEST_PASSWORD = 'gu&u6QTMbJK7nT';
|
||||
export const TEST_ORG_SLUG = 'root-test';
|
||||
|
||||
/**
|
||||
* Navigate to the org dashboard. Auth state is pre-loaded via storageState.
|
||||
* If we land on the org selector, click through to the org.
|
||||
*/
|
||||
export async function login(page: Page) {
|
||||
await page.goto(`/${TEST_ORG_SLUG}`, { timeout: 45000 });
|
||||
|
||||
// If we got redirected to org selector instead of the org page
|
||||
if (page.url() === 'http://localhost:5173/' || page.url().endsWith(':5173/')) {
|
||||
await page.getByRole('link', { name: /root-test/i }).first().click();
|
||||
await page.waitForURL(new RegExp(`/${TEST_ORG_SLUG}`), { timeout: 15000 });
|
||||
}
|
||||
|
||||
await waitForHydration(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate directly to a sub-page (skips the dashboard hop).
|
||||
* Auth state is pre-loaded via storageState.
|
||||
*/
|
||||
export async function navigateTo(page: Page, path: string) {
|
||||
await page.goto(path, { timeout: 45000 });
|
||||
await waitForHydration(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Svelte 5 hydration on the current page.
|
||||
* Svelte 5 uses event delegation — handlers only work after hydration.
|
||||
*/
|
||||
export async function waitForHydration(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const els = document.querySelectorAll('button');
|
||||
return Array.from(els).some((el) => (el as any).__click !== undefined);
|
||||
}, { timeout: 15000 });
|
||||
}
|
||||
Reference in New Issue
Block a user