diff --git a/package-lock.json b/package-lock.json index b62e6f6..8ce95a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,12 @@ "license": "GPL-3.0", "dependencies": { "date-fns": "^4.1.0", + "idb": "^8.0.3", "uuid": "^11.1.0" }, "devDependencies": { "@eslint/js": "^9.32.0", - "@playwright/test": "^1.54.2", + "@playwright/test": "^1.55.0", "@sveltejs/vite-plugin-svelte": "^6.1.0", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.2.0", @@ -825,13 +826,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", - "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.54.2" + "playwright": "1.55.0" }, "bin": { "playwright": "cli.js" @@ -2536,6 +2537,12 @@ "node": ">=8" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2996,13 +3003,13 @@ } }, "node_modules/playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.54.2" + "playwright-core": "1.55.0" }, "bin": { "playwright": "cli.js" @@ -3015,9 +3022,9 @@ } }, "node_modules/playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 443f086..8800d64 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", - "@playwright/test": "^1.54.2", + "@playwright/test": "^1.55.0", "@sveltejs/vite-plugin-svelte": "^6.1.0", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.2.0", @@ -38,6 +38,7 @@ }, "dependencies": { "date-fns": "^4.1.0", + "idb": "^8.0.3", "uuid": "^11.1.0" }, "optionalDependencies": { diff --git a/src/App.svelte b/src/App.svelte index 9da338b..f2060a1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,12 +1,15 @@ + +
+

{@render children?.()}

+
+ {#if !hideOptionsButton} + + {/if} +
+
+ + diff --git a/src/ui/Input.svelte b/src/ui/Input.svelte new file mode 100644 index 0000000..bd0373a --- /dev/null +++ b/src/ui/Input.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/ui/OptionsForm.svelte b/src/ui/OptionsForm.svelte new file mode 100644 index 0000000..c953ada --- /dev/null +++ b/src/ui/OptionsForm.svelte @@ -0,0 +1,76 @@ + + +
{ + e.preventDefault(); + onSubmit({ paidBreakDuration, workdayLength }); + }} +> + + + + +
+ + diff --git a/src/ui/Status.svelte b/src/ui/Status.svelte index 8a74adb..50afc02 100644 --- a/src/ui/Status.svelte +++ b/src/ui/Status.svelte @@ -20,8 +20,9 @@ diff --git a/src/ui/WorkDuration.svelte b/src/ui/WorkDuration.svelte index a354561..e7f9b16 100644 --- a/src/ui/WorkDuration.svelte +++ b/src/ui/WorkDuration.svelte @@ -1,17 +1,27 @@
- Work duration: {timeWorked.hours} hours, {timeWorked.minutes} minutes, {timeWorked.seconds} - seconds + {leftPadNumber(timeWorked.hours)} + hr + {leftPadNumber(timeWorked.minutes)} + min + {leftPadNumber(timeWorked.seconds)} + sec
diff --git a/src/ui/WorkdayEndEstimate.svelte b/src/ui/WorkdayEndEstimate.svelte new file mode 100644 index 0000000..78bd954 --- /dev/null +++ b/src/ui/WorkdayEndEstimate.svelte @@ -0,0 +1,16 @@ + + +
+ Estimated work end: {formatDate(estimate, "HH:mm:ss")} +
+ + diff --git a/src/ui/WorkdayEvents.svelte b/src/ui/WorkdayEvents.svelte index fc14508..abe5b3b 100644 --- a/src/ui/WorkdayEvents.svelte +++ b/src/ui/WorkdayEvents.svelte @@ -42,8 +42,9 @@ diff --git a/tests/e2e.spec.ts b/tests/e2e.spec.ts index d6dd8f0..04a7752 100644 --- a/tests/e2e.spec.ts +++ b/tests/e2e.spec.ts @@ -1,6 +1,6 @@ import { test, expect, Page } from "@playwright/test"; -const delay = (milliseconds) => +const delay = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); test("first visit, full workday", async ({ page }) => { @@ -12,36 +12,34 @@ test("first visit, full workday", async ({ page }) => { "/work-hours-tracker/favicon/initial.ico", ); await expect( - page.getByRole("heading", { name: "Welcome to Work Hours Tracker" }), + page.getByRole("heading", { name: "Work Hours Tracker" }), ).toBeVisible(); await expect(page.getByLabel("Username")).toBeVisible(); await expect(page.getByLabel("Daily paid break")).toBeVisible(); + await expect(page.getByLabel("Workday length")).toBeVisible(); await expect(page.getByLabel("Username")).toContainText(""); await expect(page.getByLabel("Daily paid break")).toContainText(""); + await expect(page.getByTitle("Options")).not.toBeVisible(); // Fill in the initial form await page.getByLabel("Username").fill("Mark S"); await page.getByLabel("Daily paid break").fill("30"); - await page.getByText("Start tracking!").click(); + await page.getByLabel("Workday length").fill("6"); + await page.getByText("Start tracking").click(); // Form is gone await expect(page.getByLabel("Username")).not.toBeVisible(); await expect(page.getByLabel("Daily paid break")).not.toBeVisible(); - await expect(page.getByText("Start tracking!")).not.toBeVisible(); + await expect(page.getByLabel("Workday length")).not.toBeVisible(); + await expect(page.getByText("Start tracking")).not.toBeVisible(); // User work hours tracking interface is shown - await expect( - page.getByRole("heading", { name: "Welcome Mark S" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeEnabled(); + await expect(page.getByRole("heading", { name: "Mark S" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeEnabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); await expect(page.getByText("Not working")).toBeVisible(); await expect(page.getByText("Not working")).toHaveCSS( @@ -49,6 +47,8 @@ test("first visit, full workday", async ({ page }) => { "rgb(255, 0, 0)", ); + await expect(page.getByTitle("Options")).toBeVisible(); + // Favicon updated await expect(page.getByTestId("favicon")).toHaveAttribute( "href", @@ -57,13 +57,11 @@ test("first visit, full workday", async ({ page }) => { // Workday starts at 8:05:00 await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); - await page.getByRole("button", { name: "Start Workday" }).click(); + await page.getByRole("button", { name: "Start Work" }).click(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeDisabled(); await expect(page.getByRole("button", { name: "Start Break" })).toBeEnabled(); - await expect(page.getByRole("button", { name: "End Workday" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeEnabled(); await expect(page.getByText("Working")).toBeVisible(); await expect(page.getByText("Working")).toHaveCSS( "color", @@ -78,12 +76,8 @@ test("first visit, full workday", async ({ page }) => { await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 35, 0)); await page.getByRole("button", { name: "Start Break" }).click(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).not.toBeVisible(); @@ -102,10 +96,8 @@ test("first visit, full workday", async ({ page }) => { await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 5, 0)); await page.getByRole("button", { name: "End Break" }).click(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeDisabled(); - await expect(page.getByRole("button", { name: "End Workday" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeEnabled(); await expect(page.getByRole("button", { name: "Start Break" })).toBeVisible(); await expect( page.getByRole("button", { name: "End Break" }), @@ -113,7 +105,7 @@ test("first visit, full workday", async ({ page }) => { // End work at 16:05:00 await page.clock.setFixedTime(new Date(2025, 2, 2, 16, 5, 0)); - await page.getByRole("button", { name: "End Workday" }).click(); + await page.getByRole("button", { name: "End Work" }).click(); await expect( page.getByRole("heading", { name: "Are you sure?" }), ).toBeVisible(); @@ -147,15 +139,11 @@ test("first visit, full workday", async ({ page }) => { ).not.toBeVisible(); await expect(page.getByRole("button", { name: "Cancel" })).not.toBeVisible(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeDisabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); await expect(page.getByText("Not working")).toBeVisible(); await expect(page.getByText("Not working")).toHaveCSS( "color", @@ -170,7 +158,7 @@ test("first visit, full workday", async ({ page }) => { test("User data persists through reloads", async ({ page }) => { await page.goto("/"); await expect( - page.getByRole("heading", { name: "Welcome to Work Hours Tracker" }), + page.getByRole("heading", { name: "Work Hours Tracker" }), ).toBeVisible(); await expect(page.getByLabel("Username")).toBeVisible(); await expect(page.getByLabel("Daily paid break")).toBeVisible(); @@ -180,58 +168,42 @@ test("User data persists through reloads", async ({ page }) => { // Fill in the initial form await page.getByLabel("Username").fill("Helly R"); await page.getByLabel("Daily paid break").fill("45"); - await page.getByText("Start tracking!").click(); + await page.getByText("Start tracking").click(); - await expect( - page.getByRole("heading", { name: "Welcome Helly R" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeEnabled(); + await expect(page.getByRole("heading", { name: "Helly R" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeEnabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); await page.reload(); - await expect( - page.getByRole("heading", { name: "Welcome Helly R" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeEnabled(); + await expect(page.getByRole("heading", { name: "Helly R" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeEnabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); }); test("Tracking data persists through reloads", async ({ page }) => { await page.goto("/"); await expect( - page.getByRole("heading", { name: "Welcome to Work Hours Tracker" }), + page.getByRole("heading", { name: "Work Hours Tracker" }), ).toBeVisible(); // Fill in the initial form await page.getByLabel("Username").fill("Burt G"); await page.getByLabel("Daily paid break").fill("45"); - await page.getByText("Start tracking!").click(); + await page.getByText("Start tracking").click(); // 8:05:00 hours await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); - await page.getByRole("button", { name: "Start Workday" }).click(); + await page.getByRole("button", { name: "Start Work" }).click(); await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 35, 0)); await page.getByRole("button", { name: "Start Break" }).click(); // Buttons in correct state after starting the break. - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).not.toBeVisible(); @@ -240,12 +212,8 @@ test("Tracking data persists through reloads", async ({ page }) => { await page.reload(); // Buttons in correct state after starting the break even after reloading. - await expect( - page.getByRole("button", { name: "Start Workday" }), - ).toBeDisabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: "Start Work" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeDisabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).not.toBeVisible(); @@ -263,18 +231,18 @@ test("Tracking data persists through reloads", async ({ page }) => { getCancelElement: async (page: Page) => await page.getByRole("alertdialog"), }, ].forEach(({ desc, getCancelElement }) => { - test(`Can close confirm end workday dialog by ${desc}`, async ({ page }) => { + test(`Can close confirm end work dialog by ${desc}`, async ({ page }) => { await page.goto("/"); // Fill in the initial form await page.getByLabel("Username").fill("Mark S"); await page.getByLabel("Daily paid break").fill("30"); - await page.getByText("Start tracking!").click(); + await page.getByText("Start tracking").click(); // Workday starts at 8:05:00 await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); - await page.getByRole("button", { name: "Start Workday" }).click(); + await page.getByRole("button", { name: "Start Work" }).click(); await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 35, 0)); - await page.getByRole("button", { name: "End Workday" }).click(); + await page.getByRole("button", { name: "End Work" }).click(); // Confirmation modal opens. await expect( @@ -290,14 +258,12 @@ test("Tracking data persists through reloads", async ({ page }) => { ).not.toBeVisible(); // Everything else unchanged await expect( - page.getByRole("button", { name: "Start Workday" }), + page.getByRole("button", { name: "Start Work" }), ).toBeDisabled(); await expect( page.getByRole("button", { name: "Start Break" }), ).toBeEnabled(); - await expect( - page.getByRole("button", { name: "End Workday" }), - ).toBeEnabled(); + await expect(page.getByRole("button", { name: "End Work" })).toBeEnabled(); }); }); @@ -305,35 +271,211 @@ test("Display hours worked so far", async ({ page }) => { await page.goto("/"); await page.getByLabel("Username").fill("Mark S"); await page.getByLabel("Daily paid break").fill("30"); - await page.getByText("Start tracking!").click(); + await page.getByText("Start tracking").click(); - await expect(page.getByText(/Work duration/)).not.toBeVisible(); + await expect(page.getByText(/\d+ hr \d+ min \d+ sec/)).not.toBeVisible(); await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); - await page.getByRole("button", { name: "Start Workday" }).click(); + await page.getByRole("button", { name: "Start Work" }).click(); - await expect( - page.getByText("Work duration: 0 hours, 0 minutes, 0 seconds"), - ).toBeVisible(); + await expect(page.getByText("00 hr 00 min 00 sec")).toBeVisible(); await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 5, 0)); await page.reload(); - await expect( - page.getByText("Work duration: 1 hours, 0 minutes, 0 seconds"), - ).toBeVisible(); + await expect(page.getByText("01 hr 00 min 00 sec")).toBeVisible(); await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 15, 25)); await page.reload(); - await expect( - page.getByText("Work duration: 1 hours, 10 minutes, 25 seconds"), - ).toBeVisible(); + await expect(page.getByText("01 hr 10 min 25 sec")).toBeVisible(); await page.clock.setFixedTime(new Date(2025, 2, 2, 16, 10, 30)); - await page.getByRole("button", { name: "End Workday" }).click(); + await expect(page.getByText(/\d+ hr \d+ min \d+ sec/)).toBeVisible(); + await page.getByRole("button", { name: "End Work" }).click(); await page.getByRole("button", { name: "Yes, I'm done for today" }).click(); // Test fails without this delay in headless chromium browser for playwright // versions 1.52.0 or higher. await delay(10); await page.reload(); - await expect(page.getByText(/Work duration/)).not.toBeVisible(); + await expect(page.getByText(/\d+ hr \d+ min \d+ sec/)).not.toBeVisible(); +}); + +test("can change paid break duration through the options menu after workday started", async ({ + page, +}) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await page.getByText("Start tracking").click(); + await expect(page.getByTitle("Options")).toBeVisible(); + // Start work and use the full paid break. + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); + await page.getByRole("button", { name: "Start work" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 5, 0)); + await page.getByRole("button", { name: "Start break" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 55, 0)); + await page.getByRole("button", { name: "End break" }).click(); + await expect(page.getByText("01 hr 45 min 00 sec")).toBeVisible(); + await expect(page.getByLabel("Daily paid break")).not.toBeVisible(); + + await page.getByTitle("Options").click(); + await expect(page.getByLabel("Daily paid break")).toBeVisible(); + await expect( + page.getByRole("spinbutton", { name: "Daily paid break" }), + ).toHaveValue("45"); // default + await page.getByLabel("Daily paid break").fill("35"); + await page.getByRole("button", { name: "Save" }).click(); + + // Form is closed. + await expect(page.getByLabel("Daily paid break")).not.toBeVisible(); + // Smaller paid break should reduce hours worked. + await expect(page.getByText("01 hr 35 min 00 sec")).toBeVisible(); + // Test fails without this delay in headless chromium browser for playwright + // versions 1.52.0 or higher. + await delay(10); + await page.reload(); + await expect( + page.getByText("01 hr 35 min 00 sec"), + "Changes should be saved in the actual database", + ).toBeVisible(); +}); + +test("can change paid break duration through the options menu before workday started", async ({ + page, +}) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await page.getByText("Start tracking").click(); + await expect(page.getByTitle("Options")).toBeVisible(); + + await page.getByTitle("Options").click(); + await expect(page.getByLabel("Daily paid break")).toBeVisible(); + await expect( + page.getByRole("spinbutton", { name: "Daily paid break" }), + ).toHaveValue("45"); // default + await page.getByLabel("Daily paid break").fill("35"); + await page.getByRole("button", { name: "Save" }).click(); + + // Form is closed. + await expect(page.getByLabel("Daily paid break")).not.toBeVisible(); + // Start work, work for one hour, then do a 50 minute break. + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); + await page.getByRole("button", { name: "Start work" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 5, 0)); + await page.getByRole("button", { name: "Start break" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 55, 0)); + await page.getByRole("button", { name: "End break" }).click(); + // Smaller paid break should reduce hours worked. + await expect(page.getByText("01 hr 35 min 00 sec")).toBeVisible(); + // Test fails without this delay in headless chromium browser for playwright + // versions 1.52.0 or higher. + await delay(10); + await page.reload(); + await expect( + page.getByText("01 hr 35 min 00 sec"), + "Changes should be saved in the actual database", + ).toBeVisible(); +}); + +test("can see the current estimate of when the workday will end", async ({ + page, +}) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await expect(page.getByText(/Estimated work end:/)).not.toBeVisible(); + await page.getByText("Start tracking").click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); + await expect(page.getByText(/Estimated work end:/)).not.toBeVisible(); + await page.getByRole("button", { name: "Start work" }).click(); + await expect(page.getByText("Estimated work end: 16:05:00")).toBeVisible(); +}); + +test("can change workday length through the options menu before workday starts", async ({ + page, +}) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await expect(page.getByText(/Estimated work end:/)).not.toBeVisible(); + await page.getByText("Start tracking").click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); + await expect(page.getByText(/Estimated work end:/)).not.toBeVisible(); + + await page.getByTitle("Options").click(); + await expect(page.getByLabel("Workday length")).toBeVisible(); + await expect( + page.getByRole("spinbutton", { name: "Workday length" }), + ).toHaveValue("8"); // default + await page.getByLabel("Workday length").fill("6"); + await page.getByRole("button", { name: "Save" }).click(); + + await page.getByRole("button", { name: "Start work" }).click(); + await expect(page.getByText("Estimated work end: 14:05:00")).toBeVisible(); }); + +test("can change workday length through the options menu after workday starts", async ({ + page, +}) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await page.getByText("Start tracking").click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 5, 0)); + await page.getByRole("button", { name: "Start work" }).click(); + + await page.getByTitle("Options").click(); + await expect(page.getByLabel("Workday length")).toBeVisible(); + await page.getByLabel("Workday length").fill("5"); + await page.getByRole("button", { name: "Save" }).click(); + + await expect(page.getByText("Estimated work end: 13:05:00")).toBeVisible(); +}); + +test("submit Options form without changing values", async ({ page }) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await page.getByText("Start tracking").click(); + + await saveUnchangedOptionsForm(page); + + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 0, 0)); + await page.getByRole("button", { name: "Start work" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 0, 0)); + await page.getByRole("button", { name: "Start break" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 10, 0, 0)); + await page.getByRole("button", { name: "End break" }).click(); + + await expect(page.getByText("Estimated work end: 16:15:00")).toBeVisible(); + await expect(page.getByText("01 hr 45 min 00 sec")).toBeVisible(); + + await saveUnchangedOptionsForm(page); + + await expect(page.getByText("Estimated work end: 16:15:00")).toBeVisible(); + await expect(page.getByText("01 hr 45 min 00 sec")).toBeVisible(); +}); + +test("cancel Options form should close it without saving changes", async ({ + page, +}) => { + await page.goto("/"); + await page.getByLabel("Username").fill("Mark S"); + await page.getByText("Start tracking").click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 8, 0, 0)); + await page.getByRole("button", { name: "Start work" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 9, 0, 0)); + await page.getByRole("button", { name: "Start break" }).click(); + await page.clock.setFixedTime(new Date(2025, 2, 2, 10, 0, 0)); + await page.getByRole("button", { name: "End break" }).click(); + + await page.getByTitle("Options").click(); + await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible(); + await page.getByLabel("Daily paid break").fill("35"); + await page.getByLabel("Workday length").fill("6"); + await page.getByRole("button", { name: "Cancel" }).click(); + + await expect(page.getByText("Estimated work end: 16:15:00")).toBeVisible(); + await expect(page.getByText("01 hr 45 min 00 sec")).toBeVisible(); +}); + +async function saveUnchangedOptionsForm(page: Page) { + await page.getByTitle("Options").click(); + await expect(page.getByLabel("Daily paid break")).toBeVisible(); + await expect(page.getByLabel("Workday length")).toBeVisible(); + await page.getByRole("button", { name: "Save" }).click(); +}