From 7c1031b92bcf890bf84ebc43ff0457f674ee9143 Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Thu, 29 Oct 2026 17:57:52 +0100 Subject: [PATCH] feat(home): say when the week's hours are unavailable Since #155, home's answer says how asking the time service for the week went. When home couldn't get the week, the Home left "This week" out, as if Jira weren't set up. It now keeps the section's head and link, and the week card says "Hours unavailable" in the hours' type, muted, without the label and the bar (decision 187). --- apps/web/e2e/home.spec.ts | 31 ++++++++++++++++++ apps/web/src/home/HomePage.svelte | 5 ++- apps/web/src/home/HomePage.test.ts | 52 ++++++++++++++++++++++++++++++ apps/web/src/home/ThisWeek.svelte | 2 +- apps/web/src/home/ThisWeek.test.ts | 8 +++++ apps/web/src/home/api.ts | 9 ++++-- apps/web/src/time/WeekCard.svelte | 28 ++++++++++------ apps/web/src/time/WeekCard.test.ts | 8 +++++ docs/plan.md | 18 ++++++++--- 9 files changed, 143 insertions(+), 18 deletions(-) diff --git a/apps/web/e2e/home.spec.ts b/apps/web/e2e/home.spec.ts index 0dd7970..793224e 100644 --- a/apps/web/e2e/home.spec.ts +++ b/apps/web/e2e/home.spec.ts @@ -1,3 +1,5 @@ +import type { Page } from '@playwright/test'; + import { home } from './demo'; import { expect, test } from './fixtures'; @@ -25,6 +27,35 @@ test('the Home leaves out This week when its answer has no week', async ({ page await expect(page.locator('main section').last()).toContainText('Your apps'); }); +// The colour --ink-muted gives text in the page's theme. +function inkMuted(page: Page) { + return page.evaluate(() => { + const probe = document.body.appendChild(document.createElement('span')); + probe.style.color = 'var(--ink-muted)'; + const { color } = getComputedStyle(probe); + probe.remove(); + return color; + }); +} + +test("the Home says the hours are unavailable when home couldn't get the week", async ({ page }, testInfo) => { + await page.route('/api/home', (route) => + route.fulfill({ json: { ...home, week: undefined, weekState: 'unavailable' } }) + ); + await page.goto('/'); + + const week = page.locator('main section').last(); + await expect(week.getByRole('heading', { level: 2 })).toHaveText('This week'); + await expect(week.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time'); + // In place of the hours, in their type and muted, without the label or the bar (decision 187). + const hours = week.getByText('Hours unavailable'); + await expect(hours).toHaveCSS('font-size', testInfo.project.name === 'phone' ? '24px' : '28px'); + await expect(hours).toHaveCSS('font-weight', '500'); + await expect(hours).toHaveCSS('color', await inkMuted(page)); + await expect(week.getByText('Logged on Jira')).toHaveCount(0); + await expect(week.locator('.track')).toHaveCount(0); +}); + test('without a session, every page shows the Signed out screen', async ({ page }) => { await page.route('/api/home', (route) => route.fulfill({ status: 401 })); await page.goto('/'); diff --git a/apps/web/src/home/HomePage.svelte b/apps/web/src/home/HomePage.svelte index 70c1885..c09020b 100644 --- a/apps/web/src/home/HomePage.svelte +++ b/apps/web/src/home/HomePage.svelte @@ -5,7 +5,8 @@ import NeedsYou from './NeedsYou.svelte'; import ThisWeek from './ThisWeek.svelte'; import YourApps from './YourApps.svelte'; -// Until the Home first loads, only the greeting and the apps show; the week, only when the Home has one. +// Until the Home first loads, only the greeting and the apps show; the week, when the Home has one, and its +// head with "Hours unavailable" when home couldn't get it from the time service (decision 187). let { now, home }: { now: Date; home?: Home } = $props(); @@ -16,4 +17,6 @@ let { now, home }: { now: Date; home?: Home } = $props(); {#if home?.week} +{:else if home?.weekState === 'unavailable'} + {/if} diff --git a/apps/web/src/home/HomePage.test.ts b/apps/web/src/home/HomePage.test.ts index 7031b10..26a7a2c 100644 --- a/apps/web/src/home/HomePage.test.ts +++ b/apps/web/src/home/HomePage.test.ts @@ -14,6 +14,9 @@ const withoutWeek: Home = { const withWeek: Home = { ...withoutWeek, week: { loggedHours: 31.5, targetHours: 40 } }; +// home asked the time service for the week, and couldn't get it. +const unavailable: Home = { ...withoutWeek, weekState: 'unavailable' }; + // The week's head, its link and its hours; null for each that doesn't show. function weekParts(): (HTMLElement | null)[] { return [ @@ -74,3 +77,52 @@ test('shows no week before the Home first loads', () => { expect(weekParts()).toEqual([null, null, null]); expect(screen.getByRole('heading', { level: 2, name: 'Your apps' })).toBeInTheDocument(); }); + +test("says the hours are unavailable when home couldn't get the week, under its head and link, after the apps", () => { + render(HomePage, { props: { now, home: unavailable } }); + + const [head, link, hours] = weekParts(); + expect(head).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/time'); + expect(hours).toBeNull(); + expect(screen.getByText('Hours unavailable')).toBeInTheDocument(); + const heads = screen.getAllByRole('heading', { level: 2 }).map((heading) => heading.textContent); + expect(heads).toEqual(['Needs you today', 'Your apps', 'This week']); +}); + +test('shows the week when the Home has one, whatever its state says', () => { + render(HomePage, { props: { now, home: { ...withWeek, weekState: 'unavailable' } } }); + + expect(weekParts()).not.toContain(null); + expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument(); +}); + +test('leaves out the week when the time service had none to give, as without Jira', () => { + render(HomePage, { props: { now, home: { ...withoutWeek, weekState: 'ok' } } }); + + expect(weekParts()).toEqual([null, null, null]); + expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument(); +}); + +test('follows each later Home: from the hours to unavailable, to no week, and back', async () => { + const { rerender } = render(HomePage, { props: { now, home: withWeek } }); + + await rerender({ home: unavailable }); + + expect(screen.getByText('Hours unavailable')).toBeInTheDocument(); + expect(screen.queryByText('31h 30m / 40h')).not.toBeInTheDocument(); + + await rerender({ home: withoutWeek }); + + expect(weekParts()).toEqual([null, null, null]); + expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument(); + + await rerender({ home: unavailable }); + + expect(screen.getByText('Hours unavailable')).toBeInTheDocument(); + + await rerender({ home: withWeek }); + + expect(weekParts()).not.toContain(null); + expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument(); +}); diff --git a/apps/web/src/home/ThisWeek.svelte b/apps/web/src/home/ThisWeek.svelte index 2013b1b..ab919c4 100644 --- a/apps/web/src/home/ThisWeek.svelte +++ b/apps/web/src/home/ThisWeek.svelte @@ -4,7 +4,7 @@ import Section from '../shell/Section.svelte'; import WeekCard from '../time/WeekCard.svelte'; import type { Week } from '../time/week'; -let { week }: { week: Week } = $props(); +let { week }: { week?: Week } = $props();
diff --git a/apps/web/src/home/ThisWeek.test.ts b/apps/web/src/home/ThisWeek.test.ts index ba3ffd2..541dce7 100644 --- a/apps/web/src/home/ThisWeek.test.ts +++ b/apps/web/src/home/ThisWeek.test.ts @@ -10,3 +10,11 @@ test("shows the week's card under its head, which links to the day report", () = expect(screen.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time'); expect(screen.getByText('30h / 40h')).toBeInTheDocument(); }); + +test('without a week, keeps its head and link, and its card says the hours are unavailable', () => { + render(ThisWeek); + + expect(screen.getByRole('heading', { level: 2, name: 'This week' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time'); + expect(screen.getByText('Hours unavailable')).toBeInTheDocument(); +}); diff --git a/apps/web/src/home/api.ts b/apps/web/src/home/api.ts index 8364c55..ff442eb 100644 --- a/apps/web/src/home/api.ts +++ b/apps/web/src/home/api.ts @@ -18,9 +18,12 @@ export interface Item { late: boolean; } +/** How home's asking went: the peer answered, or it failed or didn't answer in time. */ +type State = 'ok' | 'unavailable'; + /** How asking one of home's sources went, with only the field the app reads so far. */ interface SourceStatus { - state: 'ok' | 'unavailable'; + state: State; } /** The Home screen's data from GET /api/home, with only the fields the app reads so far. */ @@ -29,8 +32,10 @@ export interface Home { items: Item[]; /** One per source home asks; absent from the answer of a home older than the field. */ sources?: SourceStatus[]; - /** Absent when home has no week, such as before the time service logs hours. */ + /** Absent when home has no week: the time service has none, as without Jira, couldn't give one, or isn't there. */ week?: Week; + /** How asking the time service for the week went; absent without one to ask, or from a home older than the field. */ + weekState?: State; } /** The error fetchHome fails with when the gateway answers 401: the user has no session. */ diff --git a/apps/web/src/time/WeekCard.svelte b/apps/web/src/time/WeekCard.svelte index 6f0e122..f6f64dc 100644 --- a/apps/web/src/time/WeekCard.svelte +++ b/apps/web/src/time/WeekCard.svelte @@ -1,18 +1,24 @@
-

- {hoursLabel(week)} - Logged on Jira -

- - + {#if week} +

+ {hoursLabel(week)} + Logged on Jira +

+ + + {:else} +

Hours unavailable

+ {/if}