Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/web/e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Page } from '@playwright/test';

import { home } from './demo';
import { expect, test } from './fixtures';

Expand Down Expand Up @@ -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('/');
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/home/HomePage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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();
</script>

Expand All @@ -16,4 +17,6 @@ let { now, home }: { now: Date; home?: Home } = $props();
<YourApps items={home?.items} />
{#if home?.week}
<ThisWeek week={home.week} />
{:else if home?.weekState === 'unavailable'}
<ThisWeek />
{/if}
52 changes: 52 additions & 0 deletions apps/web/src/home/HomePage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down Expand Up @@ -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();
});
2 changes: 1 addition & 1 deletion apps/web/src/home/ThisWeek.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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();
</script>

<Section title="This" accent="week" link={{ href: appHref('time'), label: 'Day report' }}>
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/home/ThisWeek.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
9 changes: 7 additions & 2 deletions apps/web/src/home/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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. */
Expand Down
28 changes: 19 additions & 9 deletions apps/web/src/time/WeekCard.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
<script lang="ts">
import { hoursLabel, type Week, weekPercent } from './week';

let { week }: { week: Week } = $props();
// Without a week, the card says the hours are unavailable in their place, with no label or bar: an empty bar
// would read as nothing logged (decision 187).
let { week }: { week?: Week } = $props();
</script>

<div class="card">
<p class="hours">
<span class="value">{hoursLabel(week)}</span>
<span class="unit">Logged on Jira</span>
</p>
<!-- The hours above say the same in words. The % sits inside the braces, so Svelte adds no `?? ''` fallback. -->
<div class="track" aria-hidden="true">
<div class="fill" style:width={`${weekPercent(week)}%`}></div>
</div>
{#if week}
<p class="hours">
<span class="value">{hoursLabel(week)}</span>
<span class="unit">Logged on Jira</span>
</p>
<!-- The hours above say the same in words. The % sits inside the braces, so Svelte adds no `?? ''` fallback. -->
<div class="track" aria-hidden="true">
<div class="fill" style:width={`${weekPercent(week)}%`}></div>
</div>
{:else}
<p class="hours"><span class="value unavailable">Hours unavailable</span></p>
{/if}
</div>

<style>
Expand All @@ -38,6 +44,10 @@ let { week }: { week: Week } = $props();
line-height: 1.2;
}

.unavailable {
color: var(--ink-muted);
}

.unit {
color: var(--ink-muted);
font: 400 11px / 1.6 var(--font-mono);
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/time/WeekCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ test('shows the hours logged against the target, and fills the bar as far', () =
expect(container.querySelector('.track')).toHaveAttribute('aria-hidden', 'true');
expect(container.querySelector('.fill')).toHaveStyle({ width: '75%' });
});

test('without a week, says the hours are unavailable in their place, with no label or bar', () => {
const { container } = render(WeekCard);

expect(screen.getByText('Hours unavailable')).toBeInTheDocument();
expect(screen.queryByText('Logged on Jira')).not.toBeInTheDocument();
expect(container.querySelector('.track')).not.toBeInTheDocument();
});
Loading
Loading