Skip to content

Commit 22f3af2

Browse files
committed
Make sure that time entry billable status updates when project changes,
fixes #981
1 parent 7d068fe commit 22f3af2

File tree

5 files changed

+473
-0
lines changed

5 files changed

+473
-0
lines changed

e2e/calendar.spec.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
2+
import { test } from '../playwright/fixtures';
3+
import { expect } from '@playwright/test';
4+
import type { Page } from '@playwright/test';
5+
import { createProject, createBillableProject, createBareTimeEntry } from './utils/reporting';
6+
7+
async function goToCalendar(page: Page) {
8+
await page.goto(PLAYWRIGHT_BASE_URL + '/calendar');
9+
}
10+
11+
/**
12+
* These tests verify that changing the project on a time entry via the calendar
13+
* updates the billable status to match the new project's is_billable setting.
14+
*
15+
* Issue: https://github.com/solidtime-io/solidtime/issues/981
16+
*/
17+
18+
test('test that changing project in calendar edit modal from non-billable to billable updates billable status', async ({
19+
page,
20+
}) => {
21+
const billableProjectName = 'Billable Cal Project ' + Math.floor(1 + Math.random() * 10000);
22+
23+
await createBillableProject(page, billableProjectName);
24+
await createBareTimeEntry(page, 'Test billable calendar', '1h');
25+
26+
await goToCalendar(page);
27+
28+
// Click on the time entry event in the calendar
29+
await page.locator('.fc-event').filter({ hasText: 'Test billable calendar' }).first().click();
30+
await expect(page.getByRole('dialog')).toBeVisible();
31+
32+
// Verify initially non-billable
33+
await expect(
34+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' })
35+
).toBeVisible();
36+
37+
// Select the billable project
38+
await page.getByRole('dialog').getByRole('button', { name: 'No Project' }).click();
39+
await page.getByRole('option', { name: billableProjectName }).click();
40+
41+
// Verify the billable dropdown updated to Billable
42+
await expect(
43+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' })
44+
).toBeVisible();
45+
46+
// Save and verify
47+
const [updateResponse] = await Promise.all([
48+
page.waitForResponse(
49+
(response) =>
50+
response.url().includes('/time-entries/') &&
51+
response.request().method() === 'PUT' &&
52+
response.status() === 200
53+
),
54+
page.getByRole('button', { name: 'Update Time Entry' }).click(),
55+
]);
56+
const responseBody = await updateResponse.json();
57+
expect(responseBody.data.billable).toBe(true);
58+
});
59+
60+
test('test that changing project in calendar edit modal from billable to non-billable updates billable status', async ({
61+
page,
62+
}) => {
63+
const billableProjectName = 'Billable Cal Rev Project ' + Math.floor(1 + Math.random() * 10000);
64+
const nonBillableProjectName =
65+
'NonBillable Cal Rev Project ' + Math.floor(1 + Math.random() * 10000);
66+
67+
await createBillableProject(page, billableProjectName);
68+
await createProject(page, nonBillableProjectName);
69+
await createBareTimeEntry(page, 'Test billable cal reverse', '1h');
70+
71+
await goToCalendar(page);
72+
73+
// Click on the time entry event in the calendar
74+
await page
75+
.locator('.fc-event')
76+
.filter({ hasText: 'Test billable cal reverse' })
77+
.first()
78+
.click();
79+
await expect(page.getByRole('dialog')).toBeVisible();
80+
81+
// First assign the billable project
82+
await page.getByRole('dialog').getByRole('button', { name: 'No Project' }).click();
83+
await page.getByRole('option', { name: billableProjectName }).click();
84+
85+
// Verify billable status flipped to Billable
86+
await expect(
87+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' })
88+
).toBeVisible();
89+
90+
// Now switch to the non-billable project
91+
await page.getByRole('dialog').getByRole('button', { name: billableProjectName }).click();
92+
await page.getByRole('option', { name: nonBillableProjectName }).click();
93+
94+
// Verify billable status reverted to Non-Billable
95+
await expect(
96+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' })
97+
).toBeVisible();
98+
99+
// Save and verify
100+
const [updateResponse] = await Promise.all([
101+
page.waitForResponse(
102+
(response) =>
103+
response.url().includes('/time-entries/') &&
104+
response.request().method() === 'PUT' &&
105+
response.status() === 200
106+
),
107+
page.getByRole('button', { name: 'Update Time Entry' }).click(),
108+
]);
109+
const responseBody = await updateResponse.json();
110+
expect(responseBody.data.billable).toBe(false);
111+
});
112+
113+
test('test that opening calendar edit modal for a time entry with manually overridden billable status preserves that status', async ({
114+
page,
115+
}) => {
116+
const billableProjectName =
117+
'Billable Cal Persist Project ' + Math.floor(1 + Math.random() * 10000);
118+
119+
await createBillableProject(page, billableProjectName);
120+
await createBareTimeEntry(page, 'Test cal persist override', '1h');
121+
122+
await goToCalendar(page);
123+
124+
// Click on the time entry event in the calendar
125+
await page
126+
.locator('.fc-event')
127+
.filter({ hasText: 'Test cal persist override' })
128+
.first()
129+
.click();
130+
await expect(page.getByRole('dialog')).toBeVisible();
131+
132+
// Assign the billable project
133+
await page.getByRole('dialog').getByRole('button', { name: 'No Project' }).click();
134+
await page.getByRole('option', { name: billableProjectName }).click();
135+
136+
// Verify it auto-set to Billable
137+
await expect(
138+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' })
139+
).toBeVisible();
140+
141+
// Now manually override billable to Non-Billable via the dropdown
142+
await page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' }).click();
143+
await page.getByRole('option', { name: 'Non Billable' }).click();
144+
145+
// Verify it shows Non-Billable now
146+
await expect(
147+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' })
148+
).toBeVisible();
149+
150+
// Save
151+
const [firstSaveResponse] = await Promise.all([
152+
page.waitForResponse(
153+
(response) =>
154+
response.url().includes('/time-entries/') &&
155+
response.request().method() === 'PUT' &&
156+
response.status() === 200
157+
),
158+
page.getByRole('button', { name: 'Update Time Entry' }).click(),
159+
]);
160+
const firstBody = await firstSaveResponse.json();
161+
expect(firstBody.data.billable).toBe(false);
162+
163+
// Re-open the edit modal from the calendar — the project_id watcher should NOT override billable
164+
await page
165+
.locator('.fc-event')
166+
.filter({ hasText: 'Test cal persist override' })
167+
.first()
168+
.click();
169+
await expect(page.getByRole('dialog')).toBeVisible();
170+
171+
// The billable dropdown should still show Non-Billable
172+
await expect(
173+
page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' })
174+
).toBeVisible();
175+
176+
// Save without changes and verify the response still has billable=false
177+
const [updateResponse] = await Promise.all([
178+
page.waitForResponse(
179+
(response) =>
180+
response.url().includes('/time-entries/') &&
181+
response.request().method() === 'PUT' &&
182+
response.status() === 200
183+
),
184+
page.getByRole('button', { name: 'Update Time Entry' }).click(),
185+
]);
186+
const responseBody = await updateResponse.json();
187+
expect(responseBody.data.billable).toBe(false);
188+
});

0 commit comments

Comments
 (0)