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 @@
+
+
+
+
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 @@
+
+
+
+
+
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();
+}