Skip to content

Commit e92fff2

Browse files
test: add E2E tests for workspace settings (#225) (#232)
Co-authored-by: Ona <no-reply@ona.com>
1 parent 3e35a9b commit e92fff2

1 file changed

Lines changed: 253 additions & 0 deletions

File tree

e2e/workspace-settings.spec.ts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { test, expect } from "./fixtures/auth";
2+
import { type Page } from "@playwright/test";
3+
import { createClient } from "@supabase/supabase-js";
4+
5+
function getAdminClient() {
6+
return createClient(
7+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
8+
process.env.SUPABASE_SECRET_KEY!,
9+
{ auth: { autoRefreshToken: false, persistSession: false } }
10+
);
11+
}
12+
13+
/**
14+
* Resolve the test user's ID from their email.
15+
*/
16+
async function resolveTestUserId(): Promise<string> {
17+
const admin = getAdminClient();
18+
const { data } = await admin.auth.admin.listUsers();
19+
const user = data.users.find(
20+
(u) => u.email === process.env.TEST_USER_EMAIL
21+
);
22+
if (!user) throw new Error("Test user not found");
23+
return user.id;
24+
}
25+
26+
/**
27+
* Create a workspace via the admin client (bypasses RLS).
28+
* Also adds the test user as owner.
29+
*/
30+
async function createTestWorkspace(
31+
userId: string,
32+
name: string
33+
): Promise<{ id: string; slug: string }> {
34+
const admin = getAdminClient();
35+
const slug = name
36+
.toLowerCase()
37+
.replace(/[^a-z0-9]+/g, "-")
38+
.replace(/-+/g, "-")
39+
.replace(/^-|-$/g, "");
40+
const uniqueSlug = `${slug}-${Date.now()}`;
41+
42+
const { data: ws, error } = await admin
43+
.from("workspaces")
44+
.insert({
45+
name,
46+
slug: uniqueSlug,
47+
is_personal: false,
48+
created_by: userId,
49+
})
50+
.select("id, slug")
51+
.single();
52+
53+
if (error) throw new Error(`Failed to create workspace: ${error.message}`);
54+
55+
const { error: memberErr } = await admin.from("members").insert({
56+
workspace_id: ws.id,
57+
user_id: userId,
58+
role: "owner",
59+
joined_at: new Date().toISOString(),
60+
});
61+
62+
if (memberErr)
63+
throw new Error(`Failed to add owner member: ${memberErr.message}`);
64+
65+
return { id: ws.id, slug: ws.slug };
66+
}
67+
68+
/**
69+
* Delete a workspace via the admin client.
70+
*/
71+
async function deleteTestWorkspace(workspaceId: string): Promise<void> {
72+
const admin = getAdminClient();
73+
await admin.from("workspaces").delete().eq("id", workspaceId);
74+
}
75+
76+
/**
77+
* Clean up any non-personal workspaces whose name starts with a given prefix.
78+
*/
79+
async function cleanupTestWorkspaces(prefix: string): Promise<void> {
80+
const admin = getAdminClient();
81+
await admin
82+
.from("workspaces")
83+
.delete()
84+
.eq("is_personal", false)
85+
.like("name", `${prefix}%`);
86+
}
87+
88+
/**
89+
* Extract the workspace slug from the current URL.
90+
*/
91+
function extractWorkspaceSlug(page: Page): string {
92+
const url = new URL(page.url());
93+
const segments = url.pathname.split("/").filter(Boolean);
94+
const slug = segments[0];
95+
if (!slug) {
96+
throw new Error(`Could not extract workspace slug from URL: ${page.url()}`);
97+
}
98+
return slug;
99+
}
100+
101+
/**
102+
* Navigate to workspace settings for a given slug.
103+
*/
104+
async function goToSettings(page: Page, slug: string): Promise<void> {
105+
await page.goto(`/${slug}/settings`);
106+
await expect(
107+
page.getByRole("heading", { name: "Workspace settings" })
108+
).toBeVisible({ timeout: 10_000 });
109+
}
110+
111+
test.describe("Workspace settings", () => {
112+
test.describe.configure({ mode: "serial" });
113+
114+
let userId: string;
115+
116+
test.beforeAll(async () => {
117+
userId = await resolveTestUserId();
118+
await cleanupTestWorkspaces("E2E WS").catch(() => {});
119+
});
120+
121+
test.afterAll(async () => {
122+
await cleanupTestWorkspaces("E2E WS").catch(() => {});
123+
await cleanupTestWorkspaces("Renamed E2E").catch(() => {});
124+
});
125+
126+
test("personal workspace shows 'cannot be deleted' message", async ({
127+
authenticatedPage: page,
128+
}) => {
129+
const slug = extractWorkspaceSlug(page);
130+
await goToSettings(page, slug);
131+
132+
// Personal workspace should show the "cannot be deleted" message
133+
await expect(
134+
page.getByText("This is your personal workspace and cannot be deleted.")
135+
).toBeVisible({ timeout: 3_000 });
136+
137+
// There should be no delete button
138+
const deleteButton = page.getByRole("button", {
139+
name: /delete workspace/i,
140+
});
141+
await expect(deleteButton).toHaveCount(0);
142+
});
143+
144+
test("change workspace name and verify sidebar updates", async ({
145+
authenticatedPage: page,
146+
}) => {
147+
const wsName = `E2E WS Rename ${Date.now()}`;
148+
const ws = await createTestWorkspace(userId, wsName);
149+
150+
try {
151+
await goToSettings(page, ws.slug);
152+
153+
// Verify the name input has the current workspace name
154+
const nameInput = page.locator("#ws-name");
155+
await expect(nameInput).toHaveValue(wsName, { timeout: 5_000 });
156+
157+
// Change the name
158+
const newName = `Renamed E2E ${Date.now()}`;
159+
await nameInput.clear();
160+
await nameInput.fill(newName);
161+
162+
// Save changes
163+
await page.getByRole("button", { name: /save changes/i }).click();
164+
165+
// Wait for the success message
166+
await expect(page.getByText("Settings saved.")).toBeVisible({
167+
timeout: 10_000,
168+
});
169+
170+
// Reload the page so the client-side WorkspaceSwitcher re-fetches
171+
await page.reload();
172+
await expect(
173+
page.getByRole("heading", { name: "Workspace settings" })
174+
).toBeVisible({ timeout: 10_000 });
175+
176+
// Verify the sidebar workspace switcher shows the updated name
177+
const sidebar = page.locator("aside, [data-sidebar]").first();
178+
await expect(sidebar).toBeVisible({ timeout: 5_000 });
179+
180+
const workspaceTrigger = sidebar.locator(
181+
'button[aria-label="Switch workspace"]'
182+
);
183+
await expect(workspaceTrigger).toContainText(newName, {
184+
timeout: 10_000,
185+
});
186+
} finally {
187+
// Clean up via admin
188+
await deleteTestWorkspace(ws.id);
189+
}
190+
});
191+
192+
test("delete non-personal workspace with confirmation", async ({
193+
authenticatedPage: page,
194+
}) => {
195+
const wsName = `E2E WS Delete ${Date.now()}`;
196+
const ws = await createTestWorkspace(userId, wsName);
197+
198+
await goToSettings(page, ws.slug);
199+
200+
// The "Danger zone" section should be visible
201+
await expect(page.getByText("Danger zone")).toBeVisible({
202+
timeout: 5_000,
203+
});
204+
205+
// Click the delete button
206+
const deleteButton = page.getByRole("button", {
207+
name: /delete workspace/i,
208+
});
209+
await expect(deleteButton).toBeVisible();
210+
await deleteButton.click();
211+
212+
// The confirmation dialog should appear
213+
const dialog = page.getByRole("alertdialog");
214+
await expect(dialog).toBeVisible({ timeout: 3_000 });
215+
await expect(
216+
dialog.getByText(/are you sure you want to delete/i)
217+
).toBeVisible();
218+
await expect(dialog.getByText(wsName)).toBeVisible();
219+
220+
// Confirm deletion
221+
await dialog.getByRole("button", { name: /delete workspace/i }).click();
222+
223+
// Should redirect to another workspace (personal workspace)
224+
await page.waitForURL((url) => !url.pathname.includes(ws.slug), {
225+
timeout: 15_000,
226+
});
227+
228+
// Verify we landed on a valid workspace page (not an error page)
229+
const currentSlug = extractWorkspaceSlug(page);
230+
expect(currentSlug).toBeTruthy();
231+
expect(currentSlug).not.toBe(ws.slug);
232+
233+
// Reload so the client-side WorkspaceSwitcher re-fetches the list
234+
await page.reload();
235+
const sidebar = page.locator("aside, [data-sidebar]").first();
236+
await expect(sidebar).toBeVisible({ timeout: 10_000 });
237+
238+
// Verify the deleted workspace is no longer in the workspace switcher
239+
const workspaceTrigger = sidebar.locator(
240+
'button[aria-label="Switch workspace"]'
241+
);
242+
await workspaceTrigger.click();
243+
await page.waitForTimeout(500);
244+
245+
// The deleted workspace name should not appear in the dropdown
246+
const menuItems = page.locator('[role="menuitem"]');
247+
const count = await menuItems.count();
248+
for (let i = 0; i < count; i++) {
249+
const text = await menuItems.nth(i).textContent();
250+
expect(text).not.toContain(wsName);
251+
}
252+
});
253+
});

0 commit comments

Comments
 (0)