@@ -124,8 +124,15 @@ test('test that updating billable rate works with existing time entries', async
124124
125125 await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
126126 await page . getByRole ( 'menuitem' ) . getByText ( 'Edit' ) . first ( ) . click ( ) ;
127- await page . getByText ( 'Non-Billable' ) . click ( ) ;
128- await page . getByText ( 'Custom Rate' ) . click ( ) ;
127+
128+ // Set billable default to Billable
129+ await page . getByRole ( 'dialog' ) . locator ( '#billable' ) . click ( ) ;
130+ await page . getByRole ( 'option' , { name : 'Billable' , exact : true } ) . click ( ) ;
131+
132+ // Set billable rate to Custom Rate
133+ await page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) . click ( ) ;
134+ await page . getByRole ( 'option' , { name : 'Custom Rate' } ) . click ( ) ;
135+
129136 await page . getByPlaceholder ( 'Billable Rate' ) . fill ( newBillableRate . toString ( ) ) ;
130137 await page . getByRole ( 'button' , { name : 'Update Project' } ) . click ( ) ;
131138
@@ -153,6 +160,180 @@ test('test that updating billable rate works with existing time entries', async
153160 ) . toBeVisible ( ) ;
154161} ) ;
155162
163+ test ( 'test that creating a project with default billable rate works' , async ( { page } ) => {
164+ const newProjectName = 'Default Rate Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
165+ await goToProjectsOverview ( page ) ;
166+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
167+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
168+
169+ // Set billable default to Billable (leaves rate type as Default Rate)
170+ await page . getByRole ( 'dialog' ) . locator ( '#billable' ) . click ( ) ;
171+ await page . getByRole ( 'option' , { name : 'Billable' , exact : true } ) . click ( ) ;
172+
173+ // Verify rate type is "Default Rate" and the rate input is disabled
174+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) ) . toContainText (
175+ 'Default Rate'
176+ ) ;
177+ await expect ( page . getByPlaceholder ( 'Billable Rate' ) ) . toBeDisabled ( ) ;
178+
179+ await Promise . all ( [
180+ page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ,
181+ page . waitForResponse (
182+ async ( response ) =>
183+ response . url ( ) . includes ( '/projects' ) &&
184+ response . request ( ) . method ( ) === 'POST' &&
185+ response . status ( ) === 201 &&
186+ ( await response . json ( ) ) . data . is_billable === true &&
187+ ( await response . json ( ) ) . data . billable_rate === null
188+ ) ,
189+ ] ) ;
190+
191+ await expect ( page . getByTestId ( 'project_table' ) ) . toContainText ( newProjectName ) ;
192+ } ) ;
193+
194+ test ( 'test that creating a non-billable project works' , async ( { page } ) => {
195+ const newProjectName = 'Non-Billable Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
196+ await goToProjectsOverview ( page ) ;
197+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
198+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
199+
200+ // Billable default should already be "Non-billable" by default
201+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billable' ) ) . toContainText ( 'Non-billable' ) ;
202+
203+ await Promise . all ( [
204+ page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ,
205+ page . waitForResponse (
206+ async ( response ) =>
207+ response . url ( ) . includes ( '/projects' ) &&
208+ response . request ( ) . method ( ) === 'POST' &&
209+ response . status ( ) === 201 &&
210+ ( await response . json ( ) ) . data . is_billable === false &&
211+ ( await response . json ( ) ) . data . billable_rate === null
212+ ) ,
213+ ] ) ;
214+
215+ await expect ( page . getByTestId ( 'project_table' ) ) . toContainText ( newProjectName ) ;
216+ } ) ;
217+
218+ test ( 'test that switching from custom rate to default rate clears billable rate' , async ( {
219+ page,
220+ ctx,
221+ } ) => {
222+ const newProjectName = 'Rate Switch Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
223+ // Create a project with an existing custom billable rate
224+ await createProjectViaApi ( ctx , {
225+ name : newProjectName ,
226+ is_billable : true ,
227+ billable_rate : 15000 ,
228+ } ) ;
229+
230+ await goToProjectsOverview ( page ) ;
231+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( { timeout : 10000 } ) ;
232+
233+ await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
234+ await page . getByRole ( 'menuitem' ) . getByText ( 'Edit' ) . first ( ) . click ( ) ;
235+
236+ // Verify it loaded as Billable with Custom Rate
237+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billable' ) ) . toContainText ( 'Billable' ) ;
238+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) ) . toContainText (
239+ 'Custom Rate'
240+ ) ;
241+
242+ // Switch to Default Rate
243+ await page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) . click ( ) ;
244+ await page . getByRole ( 'option' , { name : 'Default Rate' } ) . click ( ) ;
245+
246+ // Rate input should now be disabled
247+ await expect ( page . getByPlaceholder ( 'Billable Rate' ) ) . toBeDisabled ( ) ;
248+
249+ // Submit — billable_rate changes from 15000 to null, so confirmation dialog appears
250+ await page . getByRole ( 'button' , { name : 'Update Project' } ) . click ( ) ;
251+ await Promise . all ( [
252+ page . locator ( 'button' ) . filter ( { hasText : 'Yes, update existing time' } ) . click ( ) ,
253+ page . waitForResponse (
254+ async ( response ) =>
255+ response . url ( ) . includes ( '/projects/' ) &&
256+ response . request ( ) . method ( ) === 'PUT' &&
257+ response . status ( ) === 200 &&
258+ ( await response . json ( ) ) . data . is_billable === true &&
259+ ( await response . json ( ) ) . data . billable_rate === null
260+ ) ,
261+ ] ) ;
262+ } ) ;
263+
264+ test ( 'test that switching from billable to non-billable preserves rate settings' , async ( {
265+ page,
266+ ctx,
267+ } ) => {
268+ const newProjectName = 'Billable Reset Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
269+ // Create a project with a custom billable rate
270+ await createProjectViaApi ( ctx , {
271+ name : newProjectName ,
272+ is_billable : true ,
273+ billable_rate : 20000 ,
274+ } ) ;
275+
276+ await goToProjectsOverview ( page ) ;
277+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( { timeout : 10000 } ) ;
278+
279+ await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
280+ await page . getByRole ( 'menuitem' ) . getByText ( 'Edit' ) . first ( ) . click ( ) ;
281+
282+ // Verify it loaded correctly as Billable with Custom Rate
283+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billable' ) ) . toContainText ( 'Billable' ) ;
284+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) ) . toContainText (
285+ 'Custom Rate'
286+ ) ;
287+
288+ // Switch to Non-billable
289+ await page . getByRole ( 'dialog' ) . locator ( '#billable' ) . click ( ) ;
290+ await page . getByRole ( 'option' , { name : 'Non-billable' } ) . click ( ) ;
291+
292+ // Rate type should still be Custom Rate (not reset)
293+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) ) . toContainText (
294+ 'Custom Rate'
295+ ) ;
296+
297+ // Submit and verify project is non-billable but keeps its custom rate
298+ await Promise . all ( [
299+ page . getByRole ( 'button' , { name : 'Update Project' } ) . click ( ) ,
300+ page . waitForResponse (
301+ async ( response ) =>
302+ response . url ( ) . includes ( '/projects/' ) &&
303+ response . request ( ) . method ( ) === 'PUT' &&
304+ response . status ( ) === 200 &&
305+ ( await response . json ( ) ) . data . is_billable === false &&
306+ ( await response . json ( ) ) . data . billable_rate === 20000
307+ ) ,
308+ ] ) ;
309+ } ) ;
310+
311+ test ( 'test that editing an existing billable project with default rate loads correctly' , async ( {
312+ page,
313+ ctx,
314+ } ) => {
315+ const newProjectName = 'Default Rate Edit Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
316+ // Create a project that is billable but has no custom rate (= default rate)
317+ await createProjectViaApi ( ctx , {
318+ name : newProjectName ,
319+ is_billable : true ,
320+ billable_rate : null ,
321+ } ) ;
322+
323+ await goToProjectsOverview ( page ) ;
324+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( { timeout : 10000 } ) ;
325+
326+ await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
327+ await page . getByRole ( 'menuitem' ) . getByText ( 'Edit' ) . first ( ) . click ( ) ;
328+
329+ // Verify it loaded as Billable with Default Rate
330+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billable' ) ) . toContainText ( 'Billable' ) ;
331+ await expect ( page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) ) . toContainText (
332+ 'Default Rate'
333+ ) ;
334+ await expect ( page . getByPlaceholder ( 'Billable Rate' ) ) . toBeDisabled ( ) ;
335+ } ) ;
336+
156337// Sorting tests
157338test ( 'test that sorting projects by name works' , async ( { page } ) => {
158339 await goToProjectsOverview ( page ) ;
@@ -296,8 +477,15 @@ test('test that custom billable rate is displayed correctly on project detail pa
296477 // Edit the project to set a custom billable rate
297478 await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
298479 await page . getByRole ( 'menuitem' ) . getByText ( 'Edit' ) . first ( ) . click ( ) ;
299- await page . getByText ( 'Non-Billable' ) . click ( ) ;
300- await page . getByText ( 'Custom Rate' ) . click ( ) ;
480+
481+ // Set billable default to Billable
482+ await page . getByRole ( 'dialog' ) . locator ( '#billable' ) . click ( ) ;
483+ await page . getByRole ( 'option' , { name : 'Billable' , exact : true } ) . click ( ) ;
484+
485+ // Set billable rate to Custom Rate
486+ await page . getByRole ( 'dialog' ) . locator ( '#billableRateType' ) . click ( ) ;
487+ await page . getByRole ( 'option' , { name : 'Custom Rate' } ) . click ( ) ;
488+
301489 await page . getByPlaceholder ( 'Billable Rate' ) . fill ( newBillableRate . toString ( ) ) ;
302490 await page . getByRole ( 'button' , { name : 'Update Project' } ) . click ( ) ;
303491
0 commit comments