@@ -366,6 +366,149 @@ test('test that custom billable rate is displayed correctly on project detail pa
366366 ) ;
367367} ) ;
368368
369+ // Tests for estimated time input (Issue #460)
370+ test ( 'test that creating a project with estimated time in human-readable format works' , async ( {
371+ page,
372+ } ) => {
373+ const newProjectName = 'Estimated Time Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
374+ await goToProjectsOverview ( page ) ;
375+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
376+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
377+
378+ // Fill in estimated time using human-readable format
379+ const estimatedTimeInput = page . getByPlaceholder ( 'e.g. 2h 30m or 1.5' ) ;
380+ await estimatedTimeInput . fill ( '2h 30m' ) ;
381+ await estimatedTimeInput . press ( 'Tab' ) ;
382+
383+ await Promise . all ( [
384+ page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ,
385+ page . waitForResponse (
386+ async ( response ) =>
387+ response . url ( ) . includes ( '/projects' ) &&
388+ response . request ( ) . method ( ) === 'POST' &&
389+ response . status ( ) === 201 &&
390+ // 2h 30m = 9000 seconds
391+ ( await response . json ( ) ) . data . estimated_time === 9000
392+ ) ,
393+ ] ) ;
394+
395+ await expect ( page . getByTestId ( 'project_table' ) ) . toContainText ( newProjectName ) ;
396+ } ) ;
397+
398+ test ( 'test that creating a project with estimated time using decimal notation works' , async ( {
399+ page,
400+ } ) => {
401+ const newProjectName = 'Decimal Estimated Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
402+ await goToProjectsOverview ( page ) ;
403+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
404+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
405+
406+ // Fill in estimated time using decimal notation (1.5 hours = 1h 30m)
407+ const estimatedTimeInput = page . getByPlaceholder ( 'e.g. 2h 30m or 1.5' ) ;
408+ await estimatedTimeInput . fill ( '1.5' ) ;
409+ await estimatedTimeInput . press ( 'Tab' ) ;
410+
411+ await Promise . all ( [
412+ page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ,
413+ page . waitForResponse (
414+ async ( response ) =>
415+ response . url ( ) . includes ( '/projects' ) &&
416+ response . request ( ) . method ( ) === 'POST' &&
417+ response . status ( ) === 201 &&
418+ // 1.5 hours = 5400 seconds
419+ ( await response . json ( ) ) . data . estimated_time === 5400
420+ ) ,
421+ ] ) ;
422+
423+ await expect ( page . getByTestId ( 'project_table' ) ) . toContainText ( newProjectName ) ;
424+ } ) ;
425+
426+ test ( 'test that creating a project with estimated time using comma decimal notation works' , async ( {
427+ page,
428+ } ) => {
429+ const newProjectName = 'Comma Decimal Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
430+ await goToProjectsOverview ( page ) ;
431+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
432+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
433+
434+ // Fill in estimated time using comma decimal notation (2,5 hours = 2h 30m)
435+ const estimatedTimeInput = page . getByPlaceholder ( 'e.g. 2h 30m or 1.5' ) ;
436+ await estimatedTimeInput . fill ( '2,5' ) ;
437+ await estimatedTimeInput . press ( 'Tab' ) ;
438+
439+ await Promise . all ( [
440+ page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ,
441+ page . waitForResponse (
442+ async ( response ) =>
443+ response . url ( ) . includes ( '/projects' ) &&
444+ response . request ( ) . method ( ) === 'POST' &&
445+ response . status ( ) === 201 &&
446+ // 2.5 hours = 9000 seconds
447+ ( await response . json ( ) ) . data . estimated_time === 9000
448+ ) ,
449+ ] ) ;
450+
451+ await expect ( page . getByTestId ( 'project_table' ) ) . toContainText ( newProjectName ) ;
452+ } ) ;
453+
454+ test ( 'test that updating estimated time on existing project works' , async ( { page } ) => {
455+ const newProjectName = 'Update Estimated Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
456+ await goToProjectsOverview ( page ) ;
457+
458+ // Create a project first
459+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
460+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
461+ await Promise . all ( [
462+ page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ,
463+ page . waitForResponse (
464+ ( response ) =>
465+ response . url ( ) . includes ( '/projects' ) &&
466+ response . request ( ) . method ( ) === 'POST' &&
467+ response . status ( ) === 201
468+ ) ,
469+ ] ) ;
470+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( { timeout : 10000 } ) ;
471+
472+ // Edit the project to add estimated time
473+ await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
474+ await page . getByRole ( 'menuitem' ) . getByText ( 'Edit' ) . first ( ) . click ( ) ;
475+
476+ // Fill in estimated time
477+ const estimatedTimeInput = page . getByPlaceholder ( 'e.g. 2h 30m or 1.5' ) ;
478+ await estimatedTimeInput . fill ( '4h 15m' ) ;
479+ await estimatedTimeInput . press ( 'Tab' ) ;
480+
481+ await Promise . all ( [
482+ page . getByRole ( 'button' , { name : 'Update Project' } ) . click ( ) ,
483+ page . waitForResponse (
484+ async ( response ) =>
485+ response . url ( ) . includes ( '/projects/' ) &&
486+ response . request ( ) . method ( ) === 'PUT' &&
487+ response . status ( ) === 200 &&
488+ // 4h 15m = 15300 seconds
489+ ( await response . json ( ) ) . data . estimated_time === 15300
490+ ) ,
491+ ] ) ;
492+ } ) ;
493+
494+ test ( 'test that estimated time input displays formatted value after blur' , async ( { page } ) => {
495+ await goToProjectsOverview ( page ) ;
496+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
497+
498+ const estimatedTimeInput = page . getByPlaceholder ( 'e.g. 2h 30m or 1.5' ) ;
499+
500+ // Enter time in various formats and check the displayed value
501+ await estimatedTimeInput . fill ( '90' ) ;
502+ await estimatedTimeInput . press ( 'Tab' ) ;
503+ // 90 hours should be displayed as "90h 00min" (default format)
504+ await expect ( estimatedTimeInput ) . toHaveValue ( / 9 0 h / ) ;
505+
506+ await estimatedTimeInput . fill ( '1:30' ) ;
507+ await estimatedTimeInput . press ( 'Tab' ) ;
508+ // 1:30 should be displayed as "1h 30min"
509+ await expect ( estimatedTimeInput ) . toHaveValue ( / 1 h .* 3 0 / ) ;
510+ } ) ;
511+
369512// Create new project with new Client
370513
371514// Create new project with existing Client
0 commit comments