import { expect, test } from '../playwright/fixtures'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import type { Page } from '@playwright/test'; const TIMER_BUTTON_SELECTOR = '[data-testid="dashboard_timer"] [data-testid="timer_button"]'; async function goToDashboard(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); } async function openCommandPalette(page: Page) { await page.getByTestId('command_palette_button').click(); await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); } async function closeCommandPalette(page: Page) { await page.keyboard.press('Escape'); await expect(page.locator('[role="dialog"]')).not.toBeVisible(); } async function searchInCommandPalette(page: Page, query: string) { await page.locator('[role="dialog"] input').fill(query); // Wait for search debounce to settle (command palette uses a debounced search) await page.waitForTimeout(300); } async function selectCommand(page: Page, name: string) { const option = page.getByRole('option', { name, exact: true }); await option.scrollIntoViewIfNeeded(); await option.click(); } async function assertTimerIsRunning(page: Page) { await expect(page.locator(TIMER_BUTTON_SELECTOR).and(page.locator(':visible'))).toHaveClass( /bg-red-400\/80/, { timeout: 10000, } ); } async function assertTimerIsStopped(page: Page) { await expect(page.locator(TIMER_BUTTON_SELECTOR).and(page.locator(':visible'))).toHaveClass( /bg-accent-300\/70/, { timeout: 10000, } ); } test.describe('Command Palette', () => { test.describe('Opening and Closing', () => { test('opens via search button and closes with Escape', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await expect( page.locator('[role="dialog"] input[placeholder*="command"]') ).toBeVisible(); await closeCommandPalette(page); await expect(page.locator('[role="dialog"]')).not.toBeVisible(); }); test('opens with keyboard shortcut', async ({ page }) => { await goToDashboard(page); // Click on body to ensure page has focus await page.locator('body').click(); // Use ControlOrMeta which resolves to Ctrl on Linux/Windows and Meta on macOS await page.keyboard.press('ControlOrMeta+k'); await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); }); test('clears search on close', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'dashboard'); await closeCommandPalette(page); await openCommandPalette(page); await expect(page.locator('[role="dialog"] input')).toHaveValue(''); }); }); test.describe('Command Display', () => { test('displays navigation and timer commands', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); // Navigation commands await expect(page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Go to Time' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible(); // Timer commands await expect(page.getByRole('option', { name: 'Start Timer' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Create Time Entry' })).toBeVisible(); }); test('displays create commands', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await expect(page.getByRole('option', { name: 'Create Project' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Create Client' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Create Tag' })).toBeVisible(); }); }); test.describe('Navigation Commands', () => { // Tests use element visibility assertions for consistency with codebase patterns const navigationTests = [ ['Go to Dashboard', 'dashboard_view', '/time'], ['Go to Time', 'time_view', '/dashboard'], ['Go to Calendar', 'calendar_view', '/dashboard'], ['Go to Projects', 'projects_view', '/dashboard'], ['Go to Clients', 'clients_view', '/dashboard'], ['Go to Members', 'members_view', '/dashboard'], ['Go to Tags', 'tags_view', '/dashboard'], ] as const; for (const [commandName, expectedTestId, startUrl] of navigationTests) { test(`${commandName}`, async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + startUrl); await openCommandPalette(page); await searchInCommandPalette(page, commandName.replace('Go to ', '')); await selectCommand(page, commandName); await expect(page.getByTestId(expectedTestId)).toBeVisible({ timeout: 10000 }); }); } test('Go to Profile', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Profile'); await selectCommand(page, 'Go to Profile'); // Profile page doesn't have a testId, so check for a unique element await expect(page.getByRole('heading', { name: 'Profile Information' })).toBeVisible({ timeout: 10000, }); }); test('Go to Reporting Overview', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Reporting Overview'); await selectCommand(page, 'Go to Reporting Overview'); await expect(page.getByTestId('reporting_view')).toBeVisible({ timeout: 10000 }); }); test('Go to Settings', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Settings'); await selectCommand(page, 'Go to Settings'); // Settings page uses team settings which has an h3 heading await expect( page.getByRole('heading', { name: 'Organization Name', level: 3 }) ).toBeVisible({ timeout: 10000, }); }); }); test.describe('Search and Filtering', () => { test('filters commands when searching', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'dashboard'); await expect(page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); await searchInCommandPalette(page, 'calendar'); await expect(page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible(); }); test('search is case insensitive', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'DASHBOARD'); await expect(page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); }); test('partial word search works', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'proj'); await expect(page.getByRole('option', { name: 'Go to Projects' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Create Project' })).toBeVisible(); }); test('keyboard navigation and selection works', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Enter'); await expect(page.locator('[role="dialog"]')).not.toBeVisible(); }); }); test.describe('Theme Commands', () => { test('switches to dark theme', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Dark Theme'); await selectCommand(page, 'Switch to Dark Theme'); await expect(page.locator('html')).toHaveClass(/dark/); }); test('switches to light theme', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Light Theme'); await selectCommand(page, 'Switch to Light Theme'); await expect(page.locator('html')).toHaveClass(/light/); }); }); test.describe('Timer Commands', () => { test('starts and stops timer', async ({ page }) => { await goToDashboard(page); // Start timer await openCommandPalette(page); await searchInCommandPalette(page, 'Start Timer'); await selectCommand(page, 'Start Timer'); await assertTimerIsRunning(page); // Stop timer await openCommandPalette(page); await searchInCommandPalette(page, 'Stop Timer'); await selectCommand(page, 'Stop Timer'); await assertTimerIsStopped(page); }); test('shows active timer commands when running', async ({ page }) => { await goToDashboard(page); // Start timer await openCommandPalette(page); await searchInCommandPalette(page, 'Start Timer'); await selectCommand(page, 'Start Timer'); await assertTimerIsRunning(page); // Check active timer commands - search for them to ensure visibility await openCommandPalette(page); await searchInCommandPalette(page, 'Set Project'); await expect(page.getByRole('option', { name: 'Set Project' })).toBeVisible(); }); }); test.describe('Create Commands', () => { test('opens create time entry modal', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Create Time Entry'); await selectCommand(page, 'Create Time Entry'); await expect( page.locator('[role="dialog"]').getByText('Create manual time entry') ).toBeVisible(); }); test('opens create project modal', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Create Project'); await selectCommand(page, 'Create Project'); await expect( page.locator('[role="dialog"]').getByRole('heading', { name: 'Create Project' }) ).toBeVisible(); }); test('opens create client modal', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Create Client'); await selectCommand(page, 'Create Client'); await expect( page.locator('[role="dialog"]').getByRole('heading', { name: 'Create Client' }) ).toBeVisible(); }); test('opens create tag modal', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Create Tag'); await selectCommand(page, 'Create Tag'); await expect(page.locator('[role="dialog"]').getByText('Create Tags')).toBeVisible(); }); test('opens invite member modal', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); await searchInCommandPalette(page, 'Invite Member'); await selectCommand(page, 'Invite Member'); // Modal has title with "Invite Member" text - use first() to get the title span await expect( page.locator('[role="dialog"]').getByText('Invite Member').first() ).toBeVisible(); }); }); test.describe('Entity Search', () => { test('searches for projects and navigates on selection', async ({ page }) => { const projectName = 'CmdPalette' + Math.floor(Math.random() * 10000); // Create project first await page.goto(PLAYWRIGHT_BASE_URL + '/projects'); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByPlaceholder('The next big thing').fill(projectName); await page.getByRole('button', { name: 'Create Project' }).click(); // Wait for project to be created and page to update await expect(page.getByText(projectName)).toBeVisible({ timeout: 10000 }); // Search from the projects page where the query cache now has the new project await openCommandPalette(page); await searchInCommandPalette(page, projectName); // Wait for entity search to return results const projectOption = page.getByRole('option').filter({ hasText: projectName }); await expect(projectOption).toBeVisible({ timeout: 5000, }); // Select the project from search results await projectOption.click(); }); }); test.describe('Organization Switching', () => { test('shows switch commands only when multiple organizations exist', async ({ page }) => { await goToDashboard(page); await openCommandPalette(page); // With only one org, no switch commands should appear await searchInCommandPalette(page, 'Switch to'); // Check that no organization switch commands appear (only theme switch commands) const switchOptions = page.getByRole('option', { name: /^Switch to (?!.*Theme)/ }); await expect(switchOptions).toHaveCount(0); }); test('switches organization via command palette', async ({ page }) => { const newOrgName = 'TestOrg' + Math.floor(Math.random() * 10000); // Create a new organization await page.goto(PLAYWRIGHT_BASE_URL + '/teams/create'); await page.getByLabel('Organization Name').fill(newOrgName); await page.getByRole('button', { name: 'Create' }).click(); // Wait for navigation to new org's dashboard await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000 }); // Use visible switcher (desktop sidebar has one, mobile header has another) const orgSwitcher = page.locator('[data-testid="organization_switcher"]:visible'); // Verify we're in the new org by checking the switcher await expect(orgSwitcher).toContainText(newOrgName); // Get the original org name from switcher dropdown await orgSwitcher.click(); await expect(page.getByText('Switch Organizations')).toBeVisible(); // Find the other organization button (has ArrowRightIcon, not CheckCircleIcon) // The button contains an SVG and a div with the org name const otherOrgItem = page.locator('form button').filter({ hasText: /.+/ }).first(); await expect(otherOrgItem).toBeVisible(); const originalOrgName = (await otherOrgItem.innerText()).trim(); await page.keyboard.press('Escape'); // Close dropdown // Now use command palette to switch back to original org await openCommandPalette(page); await searchInCommandPalette(page, 'Switch to'); // Should see the switch command for the original org const switchCommand = page.getByRole('option', { name: new RegExp(`Switch to ${originalOrgName}`), }); await expect(switchCommand).toBeVisible(); await switchCommand.click(); // Wait for organization switch to complete await expect(orgSwitcher).toContainText(originalOrgName, { timeout: 10000, }); }); test('organization switch commands appear in Organization group', async ({ page }) => { const newOrgName = 'GroupTestOrg' + Math.floor(Math.random() * 10000); // Create a new organization to ensure we have multiple await page.goto(PLAYWRIGHT_BASE_URL + '/teams/create'); await page.getByLabel('Organization Name').fill(newOrgName); await page.getByRole('button', { name: 'Create' }).click(); await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000 }); // Open command palette and check for Organization group heading await openCommandPalette(page); // The Organization group should be visible when there are switch commands await expect(page.getByText('Organization', { exact: true })).toBeVisible(); }); }); }); // ============================================= // Employee Permission Tests // ============================================= test.describe('Employee Command Palette Restrictions', () => { test('employee command palette does not show restricted navigation commands', async ({ employee, }) => { await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000, }); // Open command palette await employee.page.getByTestId('command_palette_button').click(); await expect(employee.page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); // Available navigation commands await expect(employee.page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); await expect(employee.page.getByRole('option', { name: 'Go to Time' })).toBeVisible(); await expect(employee.page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible(); // Restricted commands should NOT be visible await expect( employee.page.getByRole('option', { name: 'Go to Members' }) ).not.toBeVisible(); await expect( employee.page.getByRole('option', { name: 'Go to Settings' }) ).not.toBeVisible(); }); test('employee command palette does not show create commands for restricted entities', async ({ employee, }) => { await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000, }); // Open command palette await employee.page.getByTestId('command_palette_button').click(); await expect(employee.page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); // Search for "Create" to filter await employee.page.locator('[role="dialog"] input').fill('Create'); await employee.page.waitForTimeout(300); // Should NOT see create commands for restricted entities await expect( employee.page.getByRole('option', { name: 'Create Project' }) ).not.toBeVisible(); await expect( employee.page.getByRole('option', { name: 'Create Client' }) ).not.toBeVisible(); await expect(employee.page.getByRole('option', { name: 'Create Tag' })).not.toBeVisible(); await expect( employee.page.getByRole('option', { name: 'Invite Member' }) ).not.toBeVisible(); // Should still see Create Time Entry (employees can create time entries) await expect( employee.page.getByRole('option', { name: 'Create Time Entry' }) ).toBeVisible(); }); });