Skip to content

Commit 6c319fa

Browse files
committed
add e2e tests for employee restrictions
1 parent d06b063 commit 6c319fa

21 files changed

+1211
-37
lines changed

e2e/calendar.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createBillableProjectViaApi,
77
createProjectViaApi,
88
createBareTimeEntryViaApi,
9+
createTimeEntryViaApi,
910
} from './utils/api';
1011

1112
async function goToCalendar(page: Page) {
@@ -288,3 +289,38 @@ test('test that deleting time entry from calendar modal works', async ({ page, c
288289
// Verify the event is removed from the calendar
289290
await expect(page.locator('.fc-event').filter({ hasText: description })).not.toBeVisible();
290291
});
292+
293+
// =============================================
294+
// Employee Permission Tests
295+
// =============================================
296+
297+
test.describe('Employee Calendar Isolation', () => {
298+
test('employee can only see their own time entries on the calendar', async ({
299+
ctx,
300+
employee,
301+
}) => {
302+
// Owner creates a time entry for today
303+
const ownerDescription = 'OwnerCalEntry ' + Math.floor(Math.random() * 10000);
304+
await createBareTimeEntryViaApi(ctx, ownerDescription, '1h');
305+
306+
// Create a time entry for the employee for today
307+
const employeeDescription = 'EmpCalEntry ' + Math.floor(Math.random() * 10000);
308+
await createTimeEntryViaApi(
309+
{ ...ctx, memberId: employee.memberId },
310+
{ description: employeeDescription, duration: '30min' }
311+
);
312+
313+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/calendar');
314+
await expect(employee.page.locator('.fc')).toBeVisible({ timeout: 10000 });
315+
316+
// Employee's event IS visible
317+
await expect(
318+
employee.page.locator('.fc-event').filter({ hasText: employeeDescription }).first()
319+
).toBeVisible({ timeout: 10000 });
320+
321+
// Owner's event is NOT visible
322+
await expect(
323+
employee.page.locator('.fc-event').filter({ hasText: ownerDescription })
324+
).not.toBeVisible();
325+
});
326+
});

e2e/clients.spec.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { expect } from '@playwright/test';
22
import type { Page } from '@playwright/test';
33
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
44
import { test } from '../playwright/fixtures';
5-
import { createClientViaApi } from './utils/api';
5+
import {
6+
createClientViaApi,
7+
createProjectMemberViaApi,
8+
createProjectViaApi,
9+
createPublicProjectViaApi,
10+
} from './utils/api';
611

712
async function goToClientsOverview(page: Page) {
813
await page.goto(PLAYWRIGHT_BASE_URL + '/clients');
@@ -125,3 +130,86 @@ test('test that deleting a client via actions menu works', async ({ page, ctx })
125130

126131
await expect(page.getByTestId('client_table')).not.toContainText(clientName);
127132
});
133+
134+
// =============================================
135+
// Employee Permission Tests
136+
// =============================================
137+
138+
test.describe('Employee Clients Restrictions', () => {
139+
test('employee can view clients but cannot create', async ({ ctx, employee }) => {
140+
// Create a client with a public project so the employee can see the client
141+
const clientName = 'EmpViewClient ' + Math.floor(Math.random() * 10000);
142+
const client = await createClientViaApi(ctx, { name: clientName });
143+
await createPublicProjectViaApi(ctx, { name: 'EmpClientProj', client_id: client.id });
144+
145+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/clients');
146+
await expect(employee.page.getByTestId('clients_view')).toBeVisible({
147+
timeout: 10000,
148+
});
149+
150+
// Employee can see the client
151+
await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 });
152+
153+
// Employee cannot see Create Client button
154+
await expect(
155+
employee.page.getByRole('button', { name: 'Create Client' })
156+
).not.toBeVisible();
157+
});
158+
159+
test('employee cannot see edit/delete/archive actions on clients', async ({
160+
ctx,
161+
employee,
162+
}) => {
163+
const clientName = 'EmpActionsClient ' + Math.floor(Math.random() * 10000);
164+
const client = await createClientViaApi(ctx, { name: clientName });
165+
await createPublicProjectViaApi(ctx, { name: 'EmpClientActProj', client_id: client.id });
166+
167+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/clients');
168+
await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 });
169+
170+
// Click the actions dropdown trigger to open the menu
171+
const actionsButton = employee.page.locator(
172+
`[aria-label='Actions for Client ${clientName}']`
173+
);
174+
await actionsButton.click();
175+
176+
// The dropdown menu items (Edit, Archive, Delete) should NOT be visible
177+
await expect(
178+
employee.page.locator(`[aria-label='Edit Client ${clientName}']`)
179+
).not.toBeVisible();
180+
await expect(
181+
employee.page.locator(`[aria-label='Archive Client ${clientName}']`)
182+
).not.toBeVisible();
183+
await expect(
184+
employee.page.locator(`[aria-label='Delete Client ${clientName}']`)
185+
).not.toBeVisible();
186+
});
187+
188+
test('employee can see client when they are a member of its private project', async ({
189+
ctx,
190+
employee,
191+
}) => {
192+
const clientName = 'EmpPrivateClient ' + Math.floor(Math.random() * 10000);
193+
const client = await createClientViaApi(ctx, { name: clientName });
194+
195+
// Create a private project under this client
196+
const project = await createProjectViaApi(ctx, {
197+
name: 'PrivateProj',
198+
client_id: client.id,
199+
is_public: false,
200+
});
201+
202+
// Add the employee as a project member
203+
await createProjectMemberViaApi(ctx, project.id, {
204+
member_id: employee.memberId,
205+
});
206+
207+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/clients');
208+
await expect(employee.page.getByTestId('clients_view')).toBeVisible({
209+
timeout: 10000,
210+
});
211+
212+
// Employee can see the client because they are a member of its private project
213+
await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 });
214+
});
215+
});

e2e/command-palette.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,69 @@ test.describe('Command Palette', () => {
400400
});
401401
});
402402
});
403+
404+
// =============================================
405+
// Employee Permission Tests
406+
// =============================================
407+
408+
test.describe('Employee Command Palette Restrictions', () => {
409+
test('employee command palette does not show restricted navigation commands', async ({
410+
employee,
411+
}) => {
412+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
413+
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
414+
timeout: 10000,
415+
});
416+
417+
// Open command palette
418+
await employee.page.getByTestId('command_palette_button').click();
419+
await expect(employee.page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 });
420+
421+
// Available navigation commands
422+
await expect(employee.page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible();
423+
await expect(employee.page.getByRole('option', { name: 'Go to Time' })).toBeVisible();
424+
await expect(employee.page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible();
425+
426+
// Restricted commands should NOT be visible
427+
await expect(
428+
employee.page.getByRole('option', { name: 'Go to Members' })
429+
).not.toBeVisible();
430+
await expect(
431+
employee.page.getByRole('option', { name: 'Go to Settings' })
432+
).not.toBeVisible();
433+
});
434+
435+
test('employee command palette does not show create commands for restricted entities', async ({
436+
employee,
437+
}) => {
438+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
439+
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
440+
timeout: 10000,
441+
});
442+
443+
// Open command palette
444+
await employee.page.getByTestId('command_palette_button').click();
445+
await expect(employee.page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 });
446+
447+
// Search for "Create" to filter
448+
await employee.page.locator('[role="dialog"] input').fill('Create');
449+
await employee.page.waitForTimeout(300);
450+
451+
// Should NOT see create commands for restricted entities
452+
await expect(
453+
employee.page.getByRole('option', { name: 'Create Project' })
454+
).not.toBeVisible();
455+
await expect(
456+
employee.page.getByRole('option', { name: 'Create Client' })
457+
).not.toBeVisible();
458+
await expect(employee.page.getByRole('option', { name: 'Create Tag' })).not.toBeVisible();
459+
await expect(
460+
employee.page.getByRole('option', { name: 'Invite Member' })
461+
).not.toBeVisible();
462+
463+
// Should still see Create Time Entry (employees can create time entries)
464+
await expect(
465+
employee.page.getByRole('option', { name: 'Create Time Entry' })
466+
).toBeVisible();
467+
});
468+
});

e2e/dashboard.spec.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { expect, test } from '../playwright/fixtures';
2+
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
3+
import type { Page } from '@playwright/test';
4+
import {
5+
assertThatTimerHasStarted,
6+
assertThatTimerIsStopped,
7+
newTimeEntryResponse,
8+
startOrStopTimerWithButton,
9+
stoppedTimeEntryResponse,
10+
} from './utils/currentTimeEntry';
11+
import { createBareTimeEntryViaApi } from './utils/api';
12+
13+
async function goToDashboard(page: Page) {
14+
await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
15+
}
16+
17+
test('test that dashboard loads with all expected sections', async ({ page }) => {
18+
await goToDashboard(page);
19+
await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000 });
20+
21+
// Timer section (scoped to dashboard_timer to avoid matching sidebar timer)
22+
await expect(page.getByTestId('time_entry_description')).toBeVisible();
23+
await expect(page.getByTestId('dashboard_timer').getByTestId('timer_button')).toBeVisible();
24+
25+
// Dashboard cards
26+
await expect(page.getByText('Recent Time Entries', { exact: true })).toBeVisible();
27+
await expect(page.getByText('Last 7 Days', { exact: true })).toBeVisible();
28+
await expect(page.getByText('Activity Graph', { exact: true })).toBeVisible();
29+
await expect(page.getByText('Team Activity', { exact: true })).toBeVisible();
30+
31+
// Weekly overview section
32+
await expect(page.getByText('This Week', { exact: true })).toBeVisible();
33+
});
34+
35+
test('test that dashboard shows time entry data after creating entries', async ({ page, ctx }) => {
36+
await createBareTimeEntryViaApi(ctx, 'Dashboard test entry', '1h');
37+
38+
await goToDashboard(page);
39+
await expect(page.getByTestId('dashboard_view')).toBeVisible();
40+
41+
// The "Last 7 Days" or "This Week" section should reflect tracked time
42+
await expect(page.getByText('This Week', { exact: true })).toBeVisible();
43+
});
44+
45+
test('test that timer on dashboard can start and stop', async ({ page }) => {
46+
await goToDashboard(page);
47+
await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
48+
await assertThatTimerHasStarted(page);
49+
50+
await page.waitForTimeout(1500);
51+
52+
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
53+
await assertThatTimerIsStopped(page);
54+
});
55+
56+
test('test that weekly overview section displays stat cards', async ({ page, ctx }) => {
57+
await createBareTimeEntryViaApi(ctx, 'Stats test entry', '2h');
58+
59+
await goToDashboard(page);
60+
61+
// Verify stat card labels are visible
62+
await expect(page.getByText('Spent Time')).toBeVisible();
63+
await expect(page.getByText('Billable Time')).toBeVisible();
64+
await expect(page.getByText('Billable Amount')).toBeVisible();
65+
});
66+
67+
test('test that stopping timer refreshes dashboard data', async ({ page }) => {
68+
await goToDashboard(page);
69+
70+
// Start timer
71+
await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
72+
await assertThatTimerHasStarted(page);
73+
await page.waitForTimeout(1500);
74+
75+
// Stop timer and verify dashboard queries are refetched
76+
await Promise.all([
77+
stoppedTimeEntryResponse(page),
78+
page.waitForResponse(
79+
(response) =>
80+
response.url().includes('/charts/') &&
81+
response.request().method() === 'GET' &&
82+
response.status() === 200
83+
),
84+
startOrStopTimerWithButton(page),
85+
]);
86+
await assertThatTimerIsStopped(page);
87+
});
88+
89+
// =============================================
90+
// Employee Permission Tests
91+
// =============================================
92+
93+
test.describe('Employee Dashboard Restrictions', () => {
94+
test('employee dashboard loads and timer is functional', async ({ employee }) => {
95+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
96+
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
97+
timeout: 10000,
98+
});
99+
100+
// Timer should be available
101+
await expect(
102+
employee.page.getByTestId('dashboard_timer').getByTestId('timer_button')
103+
).toBeVisible();
104+
await expect(employee.page.getByTestId('time_entry_description')).toBeEditable();
105+
});
106+
107+
test('employee cannot see Team Activity card', async ({ employee }) => {
108+
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
109+
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
110+
timeout: 10000,
111+
});
112+
113+
// Other dashboard cards should be visible
114+
await expect(employee.page.getByText('Recent Time Entries', { exact: true })).toBeVisible();
115+
116+
// Team Activity should NOT be visible for employees
117+
await expect(employee.page.getByText('Team Activity', { exact: true })).not.toBeVisible();
118+
});
119+
});

0 commit comments

Comments
 (0)