From 2c84312947093f9264ca2854958e78d7a47447a9 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Wed, 24 Jun 2026 16:54:57 +0200 Subject: [PATCH] refactor: replace mocked time with injected clock prop and implement conversion logic --- src/__tests__/BerlinClock.test.tsx | 78 +++++++++++------------------- src/__tests__/utils.test.ts | 2 +- src/components/Lamp.tsx | 5 +- src/components/LampLine.tsx | 7 +-- src/containers/BerlinClock.tsx | 47 +++++------------- src/containers/TimeConverter.tsx | 21 ++++---- src/utils/index.ts | 69 ++++++++++++++++++++++---- 7 files changed, 117 insertions(+), 112 deletions(-) diff --git a/src/__tests__/BerlinClock.test.tsx b/src/__tests__/BerlinClock.test.tsx index 04d297b..f30e9d2 100644 --- a/src/__tests__/BerlinClock.test.tsx +++ b/src/__tests__/BerlinClock.test.tsx @@ -1,74 +1,54 @@ import { render, screen } from '@testing-library/react'; import { BerlinClock } from '../containers/BerlinClock'; import { LampState } from '../types'; -import { useCurrentTime } from '../hooks/useCurrentTime'; -import * as utils from '../utils'; - -// Mock the hook and the converter -vi.mock('../hooks/useCurrentTime'); -vi.mock('../utils'); - -const mockedUseCurrentTime = vi.mocked(useCurrentTime); -const mockedDateToBerlin = vi.mocked(utils.dateToBerlin); describe('BerlinClock', () => { - beforeEach(() => { - // Set a fixed date for the hook - mockedUseCurrentTime.mockReturnValue({ - currentTime: new Date(2025, 0, 1, 14, 30, 0), - setCurrentTime: vi.fn(), - }); - - // Provide a controlled BerlinClock output for that time - mockedDateToBerlin.mockReturnValue({ - secondsRow: [LampState.Y], - fiveHours: [LampState.R, LampState.R, LampState.R, LampState.O], - oneHour: [LampState.R, LampState.R, LampState.R, LampState.R], - fiveMinutes: [ - LampState.Y, LampState.Y, LampState.R, - LampState.Y, LampState.Y, LampState.R, - LampState.O, LampState.O, LampState.O, - LampState.O, LampState.O, - ], - oneMinute: [LampState.O, LampState.O, LampState.O, LampState.O], - }); - }); + const testClock = { + secondsRow: [LampState.Y], + fiveHours: [LampState.R, LampState.R, LampState.R, LampState.O], + oneHour: [LampState.R, LampState.R, LampState.R, LampState.R], + fiveMinutes: [ + LampState.Y, LampState.Y, LampState.R, + LampState.Y, LampState.Y, LampState.R, + LampState.O, LampState.O, LampState.O, + LampState.O, LampState.O, + ], + oneMinute: [LampState.O, LampState.O, LampState.O, LampState.O], + }; it('renders nothing when disabled', () => { - const { container } = render(); + const { container } = render(); expect(container.innerHTML).toBe(''); }); it('renders all five rows when enabled', () => { - render(); + render(); - expect(screen.getByTestId('seconds-lamp')).toBeInTheDocument(); + expect(screen.getByTestId('seconds-row')).toBeInTheDocument(); expect(screen.getByTestId('five-hours-row')).toBeInTheDocument(); expect(screen.getByTestId('single-hours-row')).toBeInTheDocument(); expect(screen.getByTestId('five-minutes-row')).toBeInTheDocument(); expect(screen.getByTestId('single-minutes-row')).toBeInTheDocument(); }); - it('renders the seconds lamp with correct aria-label', () => { - render(); - const secLamp = screen.getByTestId('seconds-lamp').querySelector('[data-testid="lamp-0"]'); - expect(secLamp).toHaveAttribute('aria-label', 'Y'); + it('renders the correct number of lamps in seconds-row', () => { + render(); + const secRow = screen.getByTestId('seconds-row'); + const lampLine = secRow.querySelector('[data-testid="lamp-line"]'); + expect(lampLine?.children).toHaveLength(1); }); it('renders the correct number of lamps in five-hours-row', () => { - render(); - const container = screen.getByTestId('five-hours-row').querySelector('[data-testid="lamp-line"]'); - expect(container?.children).toHaveLength(4); + render(); + const row = screen.getByTestId('five-hours-row'); + const lampLine = row.querySelector('[data-testid="lamp-line"]'); + expect(lampLine?.children).toHaveLength(4); }); - it('renders correct aria-labels in five-hours-row', () => { - render(); - const container = screen.getByTestId('five-hours-row').querySelector('[data-testid="lamp-line"]'); - const spans = container?.querySelectorAll('span'); - expect(spans).toHaveLength(4); - const expected = ['R', 'R', 'R', 'O']; - spans?.forEach((span, i) => { - expect(span).toHaveAttribute('aria-label', expected[i]); - }); + it('renders correct aria-labels in the seconds lamp', () => { + render(); + const secRow = screen.getByTestId('seconds-row'); + const span = secRow.querySelector('span'); + expect(span).toHaveAttribute('aria-label', 'Y'); }); }); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 5123a9e..359c697 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -106,7 +106,7 @@ describe('Utils', () => { fiveMinutes: [ LampState.Y, LampState.Y, LampState.R, LampState.Y, LampState.Y, LampState.R, - LampState.Y, LampState.Y, LampState.O, + LampState.O, LampState.O, LampState.O, LampState.O, LampState.O, ], oneMinute: [LampState.Y, LampState.Y, LampState.Y, LampState.Y], diff --git a/src/components/Lamp.tsx b/src/components/Lamp.tsx index 5da47ea..84e56b6 100644 --- a/src/components/Lamp.tsx +++ b/src/components/Lamp.tsx @@ -2,8 +2,9 @@ import { LampState } from '../types'; interface LampProps { state: LampState; + id?: string; } -export function Lamp({ state }: LampProps) { - return ; +export function Lamp({ state, id }: LampProps) { + return ; } diff --git a/src/components/LampLine.tsx b/src/components/LampLine.tsx index c0ccafc..92432d6 100644 --- a/src/components/LampLine.tsx +++ b/src/components/LampLine.tsx @@ -3,13 +3,14 @@ import { Lamp } from './Lamp'; interface LampLineProps { states: LampState[]; + rowTestId?: string; } -export function LampLine({ states }: LampLineProps) { +export function LampLine({ states, rowTestId }: LampLineProps) { return ( -
+
{states.map((state, index) => ( - + ))}
); diff --git a/src/containers/BerlinClock.tsx b/src/containers/BerlinClock.tsx index 5e9ff6f..31fb3dc 100644 --- a/src/containers/BerlinClock.tsx +++ b/src/containers/BerlinClock.tsx @@ -1,49 +1,24 @@ -import { useState } from 'react'; -import { Lamp } from '../components/Lamp'; -import { LampLine } from '../components/LampLine'; -import { useCurrentTime } from '../hooks/useCurrentTime'; -import { dateToBerlin } from '../utils'; +import { Lamp } from "../components/Lamp"; +import { LampLine } from "../components/LampLine"; +import type { BerlinClock as BerlinClockType } from "../types"; interface BerlinClockProps { enabled: boolean; + clock: BerlinClockType; } -export function BerlinClock({ enabled }: BerlinClockProps) { - const { currentTime } = useCurrentTime(); - const berlin = dateToBerlin(currentTime); - +export function BerlinClock({ enabled, clock }: BerlinClockProps) { if (!enabled) return null; return (
- {/* Seconds lamp */} -
- {berlin.secondsRow.length > 0 && ( - - {/* optional visual */} - - )} -
- - {/* Five hours row */} -
- -
- - {/* Single hours row */} -
- -
- - {/* Five minutes row */} -
- -
- - {/* Single minutes row */} -
- +
+
+ + + +
); } diff --git a/src/containers/TimeConverter.tsx b/src/containers/TimeConverter.tsx index d536573..1f558bc 100644 --- a/src/containers/TimeConverter.tsx +++ b/src/containers/TimeConverter.tsx @@ -1,15 +1,14 @@ -import { useState } from 'react'; -import { BerlinClock } from './BerlinClock'; -import { DigitalClock } from './DigitalClock'; -import { useCurrentTime } from '../hooks/useCurrentTime'; +import { useState } from "react"; +import { BerlinClock } from "./BerlinClock"; +import { DigitalClock } from "./DigitalClock"; +import { useCurrentTime } from "../hooks/useCurrentTime"; +import { dateToBerlin } from "../utils"; export function TimeConverter() { const { currentTime, setCurrentTime } = useCurrentTime(); const [useSystemTime, setUseSystemTime] = useState(false); - - const handleTimeChange = (newTime: Date) => { - setCurrentTime(newTime); - }; + const enabled = !useSystemTime; + const berlinClock = dateToBerlin(currentTime); return (
@@ -22,11 +21,11 @@ export function TimeConverter() { /> Use system time - +
); diff --git a/src/utils/index.ts b/src/utils/index.ts index 7ec0473..f497273 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,25 +1,74 @@ import type { BerlinClock } from '../types'; +import { LampState } from '../types'; export function dateToDigital(date: Date): string { - // returns "HH:mm:ss" - return ''; + const pad = (n: number) => n.toString().padStart(2, '0'); + return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } export function digitalToDate(digital: string): Date { - // parses "HH:mm:ss" - return new Date(); + const parts = digital.split(':'); + if (parts.length !== 3) return new Date(NaN); + const [h, m, s] = parts.map(Number); + if (isNaN(h) || isNaN(m) || isNaN(s)) return new Date(NaN); + const d = new Date(); + d.setHours(h, m, s, 0); + return d; } export function dateToBerlin(date: Date): BerlinClock { + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + + const secondsLamp = seconds % 2 === 0 ? [LampState.Y] : [LampState.O]; + + const fiveHoursCount = Math.floor(hours / 5); + const fiveHours: LampState[] = Array.from({ length: 4 }, (_, i) => + i < fiveHoursCount ? LampState.R : LampState.O, + ); + + const oneHourCount = hours % 5; + const oneHour: LampState[] = Array.from({ length: 4 }, (_, i) => + i < oneHourCount ? LampState.R : LampState.O, + ); + + const fiveMinutesCount = Math.floor(minutes / 5); + const fiveMinutes: LampState[] = Array.from({ length: 11 }, (_, i) => { + if (i < fiveMinutesCount) { + // third, sixth, ninth positions (indices 2,5,8) are red (R) + return (i + 1) % 3 === 0 ? LampState.R : LampState.Y; + } + return LampState.O; + }); + + const oneMinuteCount = minutes % 5; + const oneMinute: LampState[] = Array.from({ length: 4 }, (_, i) => + i < oneMinuteCount ? LampState.Y : LampState.O, + ); + return { - secondsRow: [], - fiveHours: [], - oneHour: [], - fiveMinutes: [], - oneMinute: [], + secondsRow: secondsLamp, + fiveHours, + oneHour, + fiveMinutes, + oneMinute, }; } export function berlinToDate(berlin: BerlinClock): Date { - return new Date(); + const countR = (arr: LampState[]) => + arr.filter((s) => s === LampState.R).length; + const countY = (arr: LampState[]) => + arr.filter((s) => s === LampState.Y).length; + const countYorR = (arr: LampState[]) => + arr.filter((s) => s === LampState.R || s === LampState.Y).length; + + const hours = countR(berlin.fiveHours) * 5 + countR(berlin.oneHour); + const minutes = + countYorR(berlin.fiveMinutes) * 5 + countY(berlin.oneMinute); + + const d = new Date(); + d.setHours(hours, minutes, 0, 0); + return d; }