From 46e67c388bd5b7462561c45c0c9ed18ecab7a084 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Oct 2025 11:50:47 +0000 Subject: [PATCH 1/2] feat: Add workspace switching e2e tests Co-authored-by: tom.a.smith96 --- .../workspace/workspace-switching.spec.ts | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts diff --git a/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts b/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts new file mode 100644 index 0000000000..99e90d7dde --- /dev/null +++ b/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts @@ -0,0 +1,263 @@ +import { test } from '@/__tests__/e2e/support/fixtures/test-hooks'; +import { expect } from '@playwright/test'; +import { AuthPage, DashboardPage } from '@/__tests__/e2e/support/page-objects'; +import { createTestWorkspaceScenario, createTestWorkspace } from '@/__tests__/e2e/support/fixtures/database'; +import { selectors } from '@/__tests__/e2e/support/fixtures/selectors'; + +/** + * Workspace Switching E2E Tests + * + * Tests the ability for users to switch between workspaces using the + * workspace switcher in the top left of the dashboard. + */ +test.describe('Workspace Switching', () => { + let authPage: AuthPage; + let dashboardPage: DashboardPage; + let firstWorkspaceSlug: string; + let secondWorkspaceSlug: string; + let firstWorkspaceName: string; + let secondWorkspaceName: string; + + test.beforeEach(async ({ page }) => { + // Create first workspace scenario + const firstScenario = await createTestWorkspaceScenario({ + owner: { + name: "E2E Test Owner", + email: "e2e-owner@example.com", + withGitHubAuth: true, + githubUsername: "e2e-test-owner", + }, + workspace: { + name: "First Workspace", + slug: `e2e-workspace-1-${Date.now()}`, + description: "First workspace for switching test", + }, + }); + + // Create second workspace for the same user + const secondWorkspace = await createTestWorkspace({ + ownerId: firstScenario.owner.id, + name: "Second Workspace", + slug: `e2e-workspace-2-${Date.now()}`, + description: "Second workspace for switching test", + }); + + firstWorkspaceSlug = firstScenario.workspace.slug; + secondWorkspaceSlug = secondWorkspace.slug; + firstWorkspaceName = firstScenario.workspace.name; + secondWorkspaceName = secondWorkspace.name; + + authPage = new AuthPage(page); + dashboardPage = new DashboardPage(page); + + // Sign in and navigate to first workspace + await authPage.goto(); + await authPage.signInWithMock(); + await page.waitForURL(/\/w\/.*/, { timeout: 10000 }); + }); + + test('should display workspace switcher with current workspace', async ({ page }) => { + await test.step('verify workspace switcher is visible', async () => { + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + + await expect(workspaceSwitcher).toBeVisible(); + }); + }); + + test('should switch to another workspace from dashboard', async ({ page }) => { + await test.step('navigate to first workspace dashboard', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + expect(page.url()).toContain(`/w/${firstWorkspaceSlug}`); + }); + + await test.step('open workspace switcher dropdown', async () => { + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + + await workspaceSwitcher.click(); + + // Wait for dropdown to be visible + await page.waitForSelector('[role="menu"], .dropdown-menu', { + state: 'visible', + timeout: 5000 + }); + }); + + await test.step('select second workspace from dropdown', async () => { + // Click on the second workspace in the dropdown + const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + + await expect(secondWorkspaceOption).toBeVisible(); + await secondWorkspaceOption.click(); + }); + + await test.step('verify navigation to second workspace', async () => { + // Wait for URL to change to second workspace + await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}`), { timeout: 10000 }); + + // Verify we're on the dashboard of the second workspace + expect(page.url()).toContain(`/w/${secondWorkspaceSlug}`); + await expect(page.locator(selectors.pageTitle.dashboard)).toBeVisible(); + }); + + await test.step('verify workspace switcher shows new workspace', async () => { + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + + await expect(workspaceSwitcher).toBeVisible(); + }); + }); + + test('should persist workspace context after reload', async ({ page }) => { + await test.step('switch to second workspace', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + + // Open dropdown and switch + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + await workspaceSwitcher.click(); + + await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + + const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await secondWorkspaceOption.click(); + + await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}`), { timeout: 10000 }); + }); + + await test.step('reload page and verify workspace persists', async () => { + await page.reload(); + await dashboardPage.waitForLoad(); + + // Verify we're still on the second workspace + expect(page.url()).toContain(`/w/${secondWorkspaceSlug}`); + + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await expect(workspaceSwitcher).toBeVisible(); + }); + }); + + test('should switch workspaces and navigate to different pages', async ({ page }) => { + await test.step('start on first workspace dashboard', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + expect(page.url()).toContain(`/w/${firstWorkspaceSlug}`); + }); + + await test.step('navigate to tasks page on first workspace', async () => { + await page.locator(selectors.navigation.tasksLink).first().click(); + await page.waitForURL(new RegExp(`/w/${firstWorkspaceSlug}/tasks`), { timeout: 10000 }); + expect(page.url()).toContain(`/w/${firstWorkspaceSlug}/tasks`); + }); + + await test.step('switch to second workspace from tasks page', async () => { + // Open workspace switcher + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + await workspaceSwitcher.click(); + + await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + + // Select second workspace + const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await secondWorkspaceOption.click(); + + // Should navigate to dashboard of second workspace + await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}`), { timeout: 10000 }); + }); + + await test.step('verify we are on second workspace dashboard', async () => { + expect(page.url()).toContain(`/w/${secondWorkspaceSlug}`); + await expect(page.locator(selectors.pageTitle.dashboard)).toBeVisible(); + + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await expect(workspaceSwitcher).toBeVisible(); + }); + }); + + test('should show both workspaces in dropdown menu', async ({ page }) => { + await test.step('navigate to first workspace', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + }); + + await test.step('open workspace switcher and verify both workspaces listed', async () => { + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + await workspaceSwitcher.click(); + + await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + + // Verify first workspace is shown (current workspace) + const firstWorkspaceItem = page.locator('[role="menuitem"]').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + await expect(firstWorkspaceItem).toBeVisible(); + + // Verify second workspace is shown in the list + const secondWorkspaceItem = page.locator('[role="menuitem"]').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await expect(secondWorkspaceItem).toBeVisible(); + }); + }); + + test('should maintain workspace context when navigating between pages', async ({ page }) => { + await test.step('switch to second workspace', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(firstWorkspaceName, 'i') + }).first(); + await workspaceSwitcher.click(); + + await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + + const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await secondWorkspaceOption.click(); + + await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}`), { timeout: 10000 }); + }); + + await test.step('navigate to tasks page', async () => { + await page.locator(selectors.navigation.tasksLink).first().click(); + await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}/tasks`), { timeout: 10000 }); + expect(page.url()).toContain(`/w/${secondWorkspaceSlug}/tasks`); + }); + + await test.step('navigate back to dashboard', async () => { + await page.locator(selectors.navigation.dashboardLink).first().click(); + await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}$`), { timeout: 10000 }); + expect(page.url()).toMatch(new RegExp(`/w/${secondWorkspaceSlug}(/)?$`)); + }); + + await test.step('verify still on second workspace', async () => { + const workspaceSwitcher = page.locator('button').filter({ + hasText: new RegExp(secondWorkspaceName, 'i') + }).first(); + await expect(workspaceSwitcher).toBeVisible(); + }); + }); +}); From 396d423ee179ad7591b5ead6e788da7880e7aeee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Oct 2025 11:54:12 +0000 Subject: [PATCH 2/2] Refactor: Improve workspace switcher E2E tests and component Co-authored-by: tom.a.smith96 --- .../workspace/workspace-switching.spec.ts | 170 ++++++++++-------- .../e2e/support/fixtures/selectors.ts | 22 +++ src/components/WorkspaceSwitcher.tsx | 15 +- 3 files changed, 131 insertions(+), 76 deletions(-) diff --git a/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts b/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts index 99e90d7dde..cf7c4509ef 100644 --- a/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts +++ b/src/__tests__/e2e/specs/workspace/workspace-switching.spec.ts @@ -2,7 +2,7 @@ import { test } from '@/__tests__/e2e/support/fixtures/test-hooks'; import { expect } from '@playwright/test'; import { AuthPage, DashboardPage } from '@/__tests__/e2e/support/page-objects'; import { createTestWorkspaceScenario, createTestWorkspace } from '@/__tests__/e2e/support/fixtures/database'; -import { selectors } from '@/__tests__/e2e/support/fixtures/selectors'; +import { selectors, dynamicSelectors } from '@/__tests__/e2e/support/fixtures/selectors'; /** * Workspace Switching E2E Tests @@ -58,11 +58,9 @@ test.describe('Workspace Switching', () => { test('should display workspace switcher with current workspace', async ({ page }) => { await test.step('verify workspace switcher is visible', async () => { - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - - await expect(workspaceSwitcher).toBeVisible(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await expect(switcherTrigger).toBeVisible(); + await expect(switcherTrigger).toContainText(firstWorkspaceName); }); }); @@ -74,24 +72,19 @@ test.describe('Workspace Switching', () => { }); await test.step('open workspace switcher dropdown', async () => { - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - - await workspaceSwitcher.click(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); // Wait for dropdown to be visible - await page.waitForSelector('[role="menu"], .dropdown-menu', { - state: 'visible', - timeout: 5000 - }); + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible({ timeout: 5000 }); }); await test.step('select second workspace from dropdown', async () => { - // Click on the second workspace in the dropdown - const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); + // Click on the second workspace option using slug selector + const secondWorkspaceOption = page.locator( + dynamicSelectors.workspaceSwitcherOptionBySlug(secondWorkspaceSlug) + ); await expect(secondWorkspaceOption).toBeVisible(); await secondWorkspaceOption.click(); @@ -107,11 +100,9 @@ test.describe('Workspace Switching', () => { }); await test.step('verify workspace switcher shows new workspace', async () => { - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); - - await expect(workspaceSwitcher).toBeVisible(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await expect(switcherTrigger).toBeVisible(); + await expect(switcherTrigger).toContainText(secondWorkspaceName); }); }); @@ -121,16 +112,15 @@ test.describe('Workspace Switching', () => { await dashboardPage.waitForLoad(); // Open dropdown and switch - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - await workspaceSwitcher.click(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); - await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible(); - const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); + const secondWorkspaceOption = page.locator( + dynamicSelectors.workspaceSwitcherOptionBySlug(secondWorkspaceSlug) + ); await secondWorkspaceOption.click(); await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}`), { timeout: 10000 }); @@ -143,10 +133,8 @@ test.describe('Workspace Switching', () => { // Verify we're still on the second workspace expect(page.url()).toContain(`/w/${secondWorkspaceSlug}`); - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); - await expect(workspaceSwitcher).toBeVisible(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await expect(switcherTrigger).toContainText(secondWorkspaceName); }); }); @@ -165,17 +153,16 @@ test.describe('Workspace Switching', () => { await test.step('switch to second workspace from tasks page', async () => { // Open workspace switcher - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - await workspaceSwitcher.click(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); - await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible(); // Select second workspace - const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); + const secondWorkspaceOption = page.locator( + dynamicSelectors.workspaceSwitcherOptionBySlug(secondWorkspaceSlug) + ); await secondWorkspaceOption.click(); // Should navigate to dashboard of second workspace @@ -186,10 +173,8 @@ test.describe('Workspace Switching', () => { expect(page.url()).toContain(`/w/${secondWorkspaceSlug}`); await expect(page.locator(selectors.pageTitle.dashboard)).toBeVisible(); - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); - await expect(workspaceSwitcher).toBeVisible(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await expect(switcherTrigger).toContainText(secondWorkspaceName); }); }); @@ -200,24 +185,23 @@ test.describe('Workspace Switching', () => { }); await test.step('open workspace switcher and verify both workspaces listed', async () => { - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - await workspaceSwitcher.click(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); - await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible(); - // Verify first workspace is shown (current workspace) - const firstWorkspaceItem = page.locator('[role="menuitem"]').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - await expect(firstWorkspaceItem).toBeVisible(); + // Verify current workspace is shown + const currentWorkspace = page.locator(selectors.workspaceSwitcher.currentWorkspace); + await expect(currentWorkspace).toBeVisible(); + await expect(currentWorkspace).toContainText(firstWorkspaceName); // Verify second workspace is shown in the list - const secondWorkspaceItem = page.locator('[role="menuitem"]').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); - await expect(secondWorkspaceItem).toBeVisible(); + const secondWorkspaceOption = page.locator( + dynamicSelectors.workspaceSwitcherOptionBySlug(secondWorkspaceSlug) + ); + await expect(secondWorkspaceOption).toBeVisible(); + await expect(secondWorkspaceOption).toContainText(secondWorkspaceName); }); }); @@ -226,16 +210,15 @@ test.describe('Workspace Switching', () => { await dashboardPage.goto(firstWorkspaceSlug); await dashboardPage.waitForLoad(); - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(firstWorkspaceName, 'i') - }).first(); - await workspaceSwitcher.click(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); - await page.waitForSelector('[role="menu"], .dropdown-menu', { state: 'visible' }); + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible(); - const secondWorkspaceOption = page.locator('[role="menuitem"]').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); + const secondWorkspaceOption = page.locator( + dynamicSelectors.workspaceSwitcherOptionBySlug(secondWorkspaceSlug) + ); await secondWorkspaceOption.click(); await page.waitForURL(new RegExp(`/w/${secondWorkspaceSlug}`), { timeout: 10000 }); @@ -254,10 +237,49 @@ test.describe('Workspace Switching', () => { }); await test.step('verify still on second workspace', async () => { - const workspaceSwitcher = page.locator('button').filter({ - hasText: new RegExp(secondWorkspaceName, 'i') - }).first(); - await expect(workspaceSwitcher).toBeVisible(); + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await expect(switcherTrigger).toContainText(secondWorkspaceName); + }); + }); + + test('should display create workspace option in dropdown', async ({ page }) => { + await test.step('navigate to workspace dashboard', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + }); + + await test.step('open workspace switcher and verify create option', async () => { + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); + + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible(); + + // Verify create workspace button is visible + const createButton = page.locator(selectors.workspaceSwitcher.createButton); + await expect(createButton).toBeVisible(); + await expect(createButton).toContainText(/Create new workspace/i); + }); + }); + + test('should close dropdown when clicking outside', async ({ page }) => { + await test.step('navigate to workspace dashboard', async () => { + await dashboardPage.goto(firstWorkspaceSlug); + await dashboardPage.waitForLoad(); + }); + + await test.step('open and close workspace switcher dropdown', async () => { + const switcherTrigger = page.locator(selectors.workspaceSwitcher.trigger); + await switcherTrigger.click(); + + const dropdown = page.locator(selectors.workspaceSwitcher.dropdown); + await expect(dropdown).toBeVisible(); + + // Click outside the dropdown + await page.locator(selectors.pageTitle.dashboard).click(); + + // Dropdown should be hidden + await expect(dropdown).not.toBeVisible({ timeout: 2000 }); }); }); }); diff --git a/src/__tests__/e2e/support/fixtures/selectors.ts b/src/__tests__/e2e/support/fixtures/selectors.ts index 2531bb0a9b..2f5cd705f2 100644 --- a/src/__tests__/e2e/support/fixtures/selectors.ts +++ b/src/__tests__/e2e/support/fixtures/selectors.ts @@ -46,6 +46,16 @@ export const selectors = { createButton: 'button:has-text("Create")', }, + // Workspace Switcher + workspaceSwitcher: { + container: '[data-testid="workspace-switcher-container"]', + trigger: '[data-testid="workspace-switcher-trigger"]', + dropdown: '[data-testid="workspace-switcher-dropdown"]', + currentWorkspace: '[data-testid="workspace-switcher-current"]', + option: '[data-testid="workspace-switcher-option"]', + createButton: '[data-testid="workspace-switcher-create"]', + }, + workspaceMembers: { card: '[data-testid="workspace-members-card"]', addButton: '[data-testid="add-member-button"]', @@ -175,4 +185,16 @@ export const dynamicSelectors = { workspaceMemberRoleBadgeByUsername: (username: string) => `[data-testid="workspace-member-row"][data-member-username="${username}"] [data-testid="member-role-badge"]`, + + /** + * Select workspace switcher option by workspace slug + */ + workspaceSwitcherOptionBySlug: (slug: string) => + `[data-testid="workspace-switcher-option"][data-workspace-slug="${slug}"]`, + + /** + * Select workspace switcher option by workspace ID + */ + workspaceSwitcherOptionById: (id: string) => + `[data-testid="workspace-switcher-option"][data-workspace-id="${id}"]`, }; diff --git a/src/components/WorkspaceSwitcher.tsx b/src/components/WorkspaceSwitcher.tsx index a4f353520e..52922ff2e6 100644 --- a/src/components/WorkspaceSwitcher.tsx +++ b/src/components/WorkspaceSwitcher.tsx @@ -103,10 +103,11 @@ export function WorkspaceSwitcher({ } return ( -
+