Skip to content

Commit 0d0dc69

Browse files
committed
Add comprehensive permission-based UI end-to-end tests
1 parent 649937a commit 0d0dc69

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { expect } from "@playwright/test";
2+
import { test } from "@shared/e2e/fixtures/page-auth";
3+
import { createTestContext, expectToastMessage } from "@shared/e2e/utils/test-assertions";
4+
import { completeSignupFlow, getVerificationCode, testUser } from "@shared/e2e/utils/test-data";
5+
import { step } from "@shared/e2e/utils/test-step-wrapper";
6+
7+
test.describe("@smoke", () => {
8+
/**
9+
* PERMISSION-BASED UI ACCESS CONTROL TESTS
10+
*
11+
* Tests the permission-based UI behavior ensuring UI elements accurately reflect
12+
* what actions users can perform based on backend authorization rules by creating
13+
* users with different roles and testing the UI behavior in the same session.
14+
*
15+
* Note: Current test fixtures infrastructure creates only Owner users, so we test
16+
* by creating users with different roles and switching between them in a single session.
17+
*/
18+
test("should enforce permission-based UI visibility and self-action restrictions", async ({ page }) => {
19+
const context = createTestContext(page);
20+
const owner = testUser();
21+
const member = testUser();
22+
23+
// Create owner and member users
24+
await step("Create owner account and set up tenant")(async () => {
25+
await completeSignupFlow(page, expect, owner, context);
26+
await expect(page.getByRole("heading", { name: "Welcome home" })).toBeVisible();
27+
})();
28+
29+
await step("Navigate to users page as Owner & verify invite button is visible")(async () => {
30+
await page.goto("/admin/users");
31+
32+
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
33+
await expect(page.getByRole("button", { name: "Invite user" })).toBeVisible();
34+
})();
35+
36+
await step("Navigate to account settings as Owner & verify tenant name field is editable")(async () => {
37+
await page.goto("/admin/account");
38+
39+
await expect(page.getByRole("heading", { name: "Account settings" })).toBeVisible();
40+
await expect(page.getByRole("textbox", { name: "Account name" })).toBeEnabled();
41+
await expect(page.getByRole("textbox", { name: "Account name" })).not.toHaveAttribute("readonly");
42+
await expect(page.getByRole("button", { name: "Save changes" })).toBeVisible();
43+
})();
44+
45+
await step("Verify danger zone is visible for Owner")(async () => {
46+
await expect(page.getByRole("heading", { name: "Danger zone" })).toBeVisible();
47+
await expect(page.getByRole("button", { name: "Delete account" })).toBeVisible();
48+
await expect(
49+
page.getByText("Delete your account and all data. This action is irreversible—proceed with caution.")
50+
).toBeVisible();
51+
})();
52+
53+
await step("Verify self-action restrictions work for Owner (cannot delete self or change own role)")(async () => {
54+
await page.goto("/admin/users");
55+
56+
// Find the owner's own row by looking for the Owner role badge
57+
const ownerRow = page.locator("tbody tr").filter({ hasText: "Owner" });
58+
await ownerRow.getByLabel("User actions").click();
59+
60+
// Verify delete menu item is disabled (self-protection)
61+
await expect(page.getByRole("menuitem", { name: "Delete" })).toBeDisabled();
62+
63+
// Verify change role menu item is disabled (self-protection)
64+
await expect(page.getByRole("menuitem", { name: "Change role" })).toBeDisabled();
65+
66+
await page.keyboard.press("Escape");
67+
})();
68+
69+
await step("Invite member user and test non-Owner permissions after role switch")(async () => {
70+
// Invite member user
71+
await page.getByRole("button", { name: "Invite user" }).click();
72+
await page.getByRole("textbox", { name: "Email" }).fill(member.email);
73+
await page.getByRole("button", { name: "Send invite" }).click();
74+
await expectToastMessage(context, "User invited successfully");
75+
await expect(page.getByRole("dialog")).not.toBeVisible();
76+
})();
77+
78+
await step("Log out from owner and log in as member to test non-Owner UI restrictions")(async () => {
79+
await page.getByRole("button", { name: "User profile menu" }).click();
80+
await page.getByRole("menuitem", { name: "Log out" }).click();
81+
82+
// Accept whatever return path we get
83+
await expect(page.getByRole("heading", { name: "Welcome back" })).toBeVisible();
84+
85+
// Login as member
86+
await page.getByRole("textbox", { name: "Email" }).fill(member.email);
87+
await page.getByRole("button", { name: "Continue" }).click();
88+
await expect(page.getByRole("heading", { name: "Enter your verification code" })).toBeVisible();
89+
await page.keyboard.type(getVerificationCode());
90+
91+
// Wait for navigation to complete after verification
92+
await page.waitForURL(/\/admin/, { timeout: 10000 });
93+
})();
94+
95+
await step("Complete member profile setup")(async () => {
96+
await expect(page.getByRole("dialog", { name: "User profile" })).toBeVisible();
97+
await page.getByRole("textbox", { name: "First name" }).fill(member.firstName);
98+
await page.getByRole("textbox", { name: "Last name" }).fill(member.lastName);
99+
await page.getByRole("textbox", { name: "Title" }).fill("Team Member");
100+
await page.getByRole("button", { name: "Save changes" }).click();
101+
102+
await expectToastMessage(context, "Profile updated successfully");
103+
await expect(page.getByRole("dialog")).not.toBeVisible();
104+
})();
105+
106+
await step("Navigate to users page as Member & verify invite button is hidden")(async () => {
107+
await page.goto("/admin/users");
108+
109+
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
110+
await expect(page.getByRole("button", { name: "Invite user" })).not.toBeVisible();
111+
})();
112+
113+
await step("Navigate to account settings as Member & verify tenant name field is readonly")(async () => {
114+
await page.goto("/admin/account");
115+
116+
await expect(page.getByRole("heading", { name: "Account settings" })).toBeVisible();
117+
await expect(page.getByRole("textbox", { name: "Account name" })).toHaveAttribute("readonly");
118+
await expect(page.getByText("Only account owners can modify the account name")).toBeVisible();
119+
await expect(page.getByRole("button", { name: "Save changes" })).not.toBeVisible();
120+
})();
121+
122+
await step("Verify danger zone is hidden for Member")(async () => {
123+
await expect(page.getByRole("heading", { name: "Danger zone" })).not.toBeVisible();
124+
await expect(page.getByRole("button", { name: "Delete account" })).not.toBeVisible();
125+
await expect(
126+
page.getByText("Delete your account and all data. This action is irreversible—proceed with caution.")
127+
).not.toBeVisible();
128+
})();
129+
130+
await step("Verify self-action restrictions work for Member (cannot delete self or change own role)")(async () => {
131+
await page.goto("/admin/users");
132+
133+
// Find the member's own row by filtering by email
134+
const memberRow = page.locator("tbody tr").filter({ hasText: member.email });
135+
await memberRow.getByLabel("User actions").click();
136+
137+
// Verify delete and change role menu items are not visible (members don't see these options)
138+
await expect(page.getByRole("menuitem", { name: "Delete" })).not.toBeVisible();
139+
await expect(page.getByRole("menuitem", { name: "Change role" })).not.toBeVisible();
140+
141+
// Verify only View profile is available
142+
await expect(page.getByRole("menuitem", { name: "View profile" })).toBeVisible();
143+
144+
await page.keyboard.press("Escape");
145+
})();
146+
});
147+
148+
/**
149+
* BULK DELETE PERMISSION TESTS
150+
*
151+
* Tests that bulk delete functionality is only available to Owners.
152+
*/
153+
test("should show bulk delete controls only for Owners", async ({ page }) => {
154+
const context = createTestContext(page);
155+
const owner = testUser();
156+
const member = testUser();
157+
158+
await step("Create owner account and multiple test users")(async () => {
159+
await completeSignupFlow(page, expect, owner, context);
160+
await page.goto("/admin/users");
161+
162+
const user1 = testUser();
163+
const user2 = testUser();
164+
165+
// Invite first user
166+
await page.getByRole("button", { name: "Invite user" }).click();
167+
await page.getByRole("textbox", { name: "Email" }).fill(user1.email);
168+
await page.getByRole("button", { name: "Send invite" }).click();
169+
await expectToastMessage(context, "User invited successfully");
170+
await expect(page.getByRole("dialog")).not.toBeVisible();
171+
172+
// Invite second user
173+
await page.getByRole("button", { name: "Invite user" }).click();
174+
await page.getByRole("textbox", { name: "Email" }).fill(user2.email);
175+
await page.getByRole("button", { name: "Send invite" }).click();
176+
await expectToastMessage(context, "User invited successfully");
177+
await expect(page.getByRole("dialog")).not.toBeVisible();
178+
179+
// Invite member user for role testing
180+
await page.getByRole("button", { name: "Invite user" }).click();
181+
await page.getByRole("textbox", { name: "Email" }).fill(member.email);
182+
await page.getByRole("button", { name: "Send invite" }).click();
183+
await expectToastMessage(context, "User invited successfully");
184+
await expect(page.getByRole("dialog")).not.toBeVisible();
185+
186+
// Should now have owner + 3 invited users = 4 total
187+
await expect(page.locator("tbody tr")).toHaveCount(4);
188+
})();
189+
190+
await step("Select multiple users as Owner & verify bulk delete button appears")(async () => {
191+
// Set viewport to 2xl to avoid side pane backdrop issues
192+
await page.setViewportSize({ width: 1536, height: 1024 });
193+
194+
// Select the first two invited users
195+
const rows = page.locator("tbody tr");
196+
const secondRow = rows.nth(1); // First invited user
197+
const thirdRow = rows.nth(2); // Second invited user
198+
199+
// Select first user
200+
await secondRow.click();
201+
await expect(secondRow).toHaveAttribute("aria-selected", "true");
202+
203+
// Select second user with Ctrl/Cmd modifier
204+
await thirdRow.click({ modifiers: ["ControlOrMeta"] });
205+
await expect(thirdRow).toHaveAttribute("aria-selected", "true");
206+
await expect(secondRow).toHaveAttribute("aria-selected", "true");
207+
208+
// Verify bulk delete button is visible for Owner
209+
await expect(page.getByRole("button", { name: "Delete 2 users" })).toBeVisible();
210+
211+
// Reset viewport
212+
await page.setViewportSize({ width: 1280, height: 720 });
213+
})();
214+
215+
await step("Log out as owner and log in as member to test bulk delete restrictions")(async () => {
216+
await page.getByRole("button", { name: "User profile menu" }).click();
217+
await page.getByRole("menuitem", { name: "Log out" }).click();
218+
219+
// Accept whatever return path we get
220+
await expect(page.getByRole("heading", { name: "Welcome back" })).toBeVisible();
221+
222+
// Login as member
223+
await page.getByRole("textbox", { name: "Email" }).fill(member.email);
224+
await page.getByRole("button", { name: "Continue" }).click();
225+
await expect(page.getByRole("heading", { name: "Enter your verification code" })).toBeVisible();
226+
await page.keyboard.type(getVerificationCode());
227+
228+
// Wait for navigation to complete after verification
229+
await page.waitForURL(/\/admin/, { timeout: 10000 });
230+
})();
231+
232+
await step("Complete member profile setup")(async () => {
233+
await expect(page.getByRole("dialog", { name: "User profile" })).toBeVisible();
234+
await page.getByRole("textbox", { name: "First name" }).fill(member.firstName);
235+
await page.getByRole("textbox", { name: "Last name" }).fill(member.lastName);
236+
await page.getByRole("textbox", { name: "Title" }).fill("Team Member");
237+
await page.getByRole("button", { name: "Save changes" }).click();
238+
239+
await expectToastMessage(context, "Profile updated successfully");
240+
await expect(page.getByRole("dialog")).not.toBeVisible();
241+
})();
242+
243+
await step("Navigate to users page as Member & verify no bulk operations available")(async () => {
244+
await page.goto("/admin/users");
245+
246+
// Ensure we can see the users that were created
247+
await expect(page.locator("tbody tr")).toHaveCount(4);
248+
249+
// Try to select rows (member can still select, but no bulk actions should appear)
250+
// Set viewport to 2xl to avoid side pane backdrop issues
251+
await page.setViewportSize({ width: 1536, height: 1024 });
252+
253+
const rows = page.locator("tbody tr");
254+
const secondRow = rows.nth(1);
255+
const thirdRow = rows.nth(2);
256+
257+
// Select users as Member
258+
await secondRow.click();
259+
await expect(secondRow).toHaveAttribute("aria-selected", "true");
260+
261+
await thirdRow.click({ modifiers: ["ControlOrMeta"] });
262+
await expect(thirdRow).toHaveAttribute("aria-selected", "true");
263+
264+
// Verify bulk delete button is NOT visible for Member even with selections
265+
await expect(page.getByRole("button", { name: "Delete 2 users" })).not.toBeVisible();
266+
await expect(page.getByRole("button", { name: "Delete user" })).not.toBeVisible();
267+
268+
// Reset viewport
269+
await page.setViewportSize({ width: 1280, height: 720 });
270+
})();
271+
});
272+
});

0 commit comments

Comments
 (0)