style: format code with single quotes and add Lamp styling

This commit is contained in:
Loic Coenen
2026-06-24 22:42:53 +02:00
committed by Loic Coenen (aider)
parent 2c84312947
commit d8348a64ea
15 changed files with 331 additions and 175 deletions

View File

@@ -1,54 +1,61 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from "@testing-library/react";
import { BerlinClock } from '../containers/BerlinClock'; import { BerlinClock } from "../containers/BerlinClock";
import { LampState } from '../types'; import { LampState } from "../types";
describe('BerlinClock', () => { describe("BerlinClock", () => {
const testClock = { const testClock = {
secondsRow: [LampState.Y], secondsRow: [LampState.Y],
fiveHours: [LampState.R, LampState.R, LampState.R, LampState.O], fiveHours: [LampState.R, LampState.R, LampState.R, LampState.O],
oneHour: [LampState.R, LampState.R, LampState.R, LampState.R], oneHour: [LampState.R, LampState.R, LampState.R, LampState.R],
fiveMinutes: [ fiveMinutes: [
LampState.Y, LampState.Y, LampState.R, LampState.Y,
LampState.Y, LampState.Y, LampState.R, LampState.Y,
LampState.O, LampState.O, LampState.O, LampState.R,
LampState.O, LampState.O, 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], oneMinute: [LampState.O, LampState.O, LampState.O, LampState.O],
}; };
it('renders nothing when disabled', () => { it("renders nothing when disabled", () => {
const { container } = render(<BerlinClock enabled={false} clock={testClock} />); const { container } = render(
expect(container.innerHTML).toBe(''); <BerlinClock enabled={false} clock={testClock} />,
);
expect(container.innerHTML).toBe("");
}); });
it('renders all five rows when enabled', () => { it("renders all five rows when enabled", () => {
render(<BerlinClock enabled={true} clock={testClock} />); render(<BerlinClock enabled={true} clock={testClock} />);
expect(screen.getByTestId('seconds-row')).toBeInTheDocument(); expect(screen.getByTestId("seconds-row")).toBeInTheDocument();
expect(screen.getByTestId('five-hours-row')).toBeInTheDocument(); expect(screen.getByTestId("five-hours-row")).toBeInTheDocument();
expect(screen.getByTestId('single-hours-row')).toBeInTheDocument(); expect(screen.getByTestId("single-hours-row")).toBeInTheDocument();
expect(screen.getByTestId('five-minutes-row')).toBeInTheDocument(); expect(screen.getByTestId("five-minutes-row")).toBeInTheDocument();
expect(screen.getByTestId('single-minutes-row')).toBeInTheDocument(); expect(screen.getByTestId("single-minutes-row")).toBeInTheDocument();
}); });
it('renders the correct number of lamps in seconds-row', () => { it("renders the correct number of lamps in seconds-row", () => {
render(<BerlinClock enabled={true} clock={testClock} />); render(<BerlinClock enabled={true} clock={testClock} />);
const secRow = screen.getByTestId('seconds-row'); const secRow = screen.getByTestId("seconds-row");
const lampLine = secRow.querySelector('[data-testid="lamp-line"]'); expect(secRow.children).toHaveLength(1);
expect(lampLine?.children).toHaveLength(1);
}); });
it('renders the correct number of lamps in five-hours-row', () => { it("renders the correct number of lamps in five-hours-row", () => {
render(<BerlinClock enabled={true} clock={testClock} />); render(<BerlinClock enabled={true} clock={testClock} />);
const row = screen.getByTestId('five-hours-row'); const row = screen.getByTestId("five-hours-row");
const lampLine = row.querySelector('[data-testid="lamp-line"]'); expect(row.children).toHaveLength(4);
expect(lampLine?.children).toHaveLength(4);
}); });
it('renders correct aria-labels in the seconds lamp', () => { it("renders correct aria-labels in the seconds lamp", () => {
render(<BerlinClock enabled={true} clock={testClock} />); render(<BerlinClock enabled={true} clock={testClock} />);
const secRow = screen.getByTestId('seconds-row'); const secRow = screen.getByTestId("seconds-row");
const span = secRow.querySelector('span'); const span = secRow.querySelector("span");
expect(span).toHaveAttribute('aria-label', 'Y'); expect(span).toHaveAttribute("aria-label", "Y");
}); });
}); });

View File

@@ -1,20 +1,20 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from "@testing-library/react";
import { DigitalClock } from '../containers/DigitalClock'; import { DigitalClock } from "../containers/DigitalClock";
import * as utils from '../utils'; import * as utils from "../utils";
vi.mock('../utils'); vi.mock("../utils");
const mockedDateToDigital = vi.mocked(utils.dateToDigital); const mockedDateToDigital = vi.mocked(utils.dateToDigital);
const mockedDigitalToDate = vi.mocked(utils.digitalToDate); const mockedDigitalToDate = vi.mocked(utils.digitalToDate);
describe('DigitalClock', () => { describe("DigitalClock", () => {
const onTimeChangeMock = vi.fn(); const onTimeChangeMock = vi.fn();
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mockedDateToDigital.mockReturnValue('14:30:15'); mockedDateToDigital.mockReturnValue("14:30:15");
mockedDigitalToDate.mockImplementation((str: string) => { mockedDigitalToDate.mockImplementation((str: string) => {
const parts = str.split(':'); const parts = str.split(":");
if (parts.length !== 3) return new Date(NaN); if (parts.length !== 3) return new Date(NaN);
const [h, m, s] = parts.map(Number); const [h, m, s] = parts.map(Number);
if (isNaN(h) || isNaN(m) || isNaN(s)) return new Date(NaN); if (isNaN(h) || isNaN(m) || isNaN(s)) return new Date(NaN);
@@ -24,58 +24,58 @@ describe('DigitalClock', () => {
}); });
}); });
it('renders nothing when disabled', () => { it("renders nothing when disabled", () => {
const { container } = render( const { container } = render(
<DigitalClock <DigitalClock
enabled={false} enabled={false}
currentTime={new Date(2025,0,1,14,30,15)} currentTime={new Date(2025, 0, 1, 14, 30, 15)}
onTimeChange={onTimeChangeMock} onTimeChange={onTimeChangeMock}
/>, />,
); );
expect(container.innerHTML).toBe(''); expect(container.innerHTML).toBe("");
}); });
it('renders the digital time and input when enabled', () => { it("renders the digital time and input when enabled", () => {
render( render(
<DigitalClock <DigitalClock
enabled={true} enabled={true}
currentTime={new Date(2025,0,1,14,30,15)} currentTime={new Date(2025, 0, 1, 14, 30, 15)}
onTimeChange={onTimeChangeMock} onTimeChange={onTimeChangeMock}
/>, />,
); );
expect(screen.getByTestId('digital-clock')).toHaveTextContent('14:30:15'); expect(screen.getByTestId("digital-clock")).toHaveTextContent("14:30:15");
expect(screen.getByTestId('time-input')).toBeInTheDocument(); expect(screen.getByTestId("time-input")).toBeInTheDocument();
}); });
it('calls onTimeChange when a valid time is entered', () => { it("calls onTimeChange when a valid time is entered", () => {
render( render(
<DigitalClock <DigitalClock
enabled={true} enabled={true}
currentTime={new Date(2025,0,1,14,30,15)} currentTime={new Date(2025, 0, 1, 14, 30, 15)}
onTimeChange={onTimeChangeMock} onTimeChange={onTimeChangeMock}
/>, />,
); );
const input = screen.getByTestId('time-input'); const input = screen.getByTestId("time-input");
fireEvent.change(input, { target: { value: '12:00:00' } }); fireEvent.change(input, { target: { value: "12:00:00" } });
expect(onTimeChangeMock).toHaveBeenCalledTimes(1); expect(onTimeChangeMock).toHaveBeenCalledTimes(1);
const expectedDate = new Date(2025, 0, 1, 12, 0, 0, 0); const expectedDate = new Date(2025, 0, 1, 12, 0, 0, 0);
expect(onTimeChangeMock).toHaveBeenCalledWith(expectedDate); expect(onTimeChangeMock).toHaveBeenCalledWith(expectedDate);
expect(input).toHaveValue('12:00:00'); expect(input).toHaveValue("12:00:00");
}); });
it('does not call onTimeChange when an invalid time is entered', () => { it("does not call onTimeChange when an invalid time is entered", () => {
render( render(
<DigitalClock <DigitalClock
enabled={true} enabled={true}
currentTime={new Date(2025,0,1,14,30,15)} currentTime={new Date(2025, 0, 1, 14, 30, 15)}
onTimeChange={onTimeChangeMock} onTimeChange={onTimeChangeMock}
/>, />,
); );
const input = screen.getByTestId('time-input'); const input = screen.getByTestId("time-input");
fireEvent.change(input, { target: { value: 'notatime' } }); fireEvent.change(input, { target: { value: "notatime" } });
expect(onTimeChangeMock).not.toHaveBeenCalled(); expect(onTimeChangeMock).not.toHaveBeenCalled();
expect(input).toHaveValue('notatime'); expect(input).toHaveValue("notatime");
}); });
}); });

View File

@@ -1,22 +1,22 @@
import { render } from '@testing-library/react'; import { render } from "@testing-library/react";
import { Lamp } from '../components/Lamp'; import { Lamp } from "../components/Lamp";
import { LampState } from '../types'; import { LampState } from "../types";
describe('Lamp', () => { describe("Lamp", () => {
it('renders without crashing', () => { it("renders without crashing", () => {
render(<Lamp state={LampState.Y} />); render(<Lamp state={LampState.Y} />);
// If it doesn't throw, we're good. // If it doesn't throw, we're good.
}); });
it.each([ it.each([
['Y' as LampState, 'Y'], ["Y" as LampState, "Y"],
['R' as LampState, 'R'], ["R" as LampState, "R"],
['O' as LampState, 'O'], ["O" as LampState, "O"],
['N' as LampState, 'N'], ["N" as LampState, "N"],
])('displays aria-label "%s" for state %s', (state, expectedLabel) => { ])('displays aria-label "%s" for state %s', (state, expectedLabel) => {
const { container } = render(<Lamp state={state} />); const { container } = render(<Lamp state={state} />);
// The root element should have the correct aria-label // The root element should have the correct aria-label
const lampElement = container.firstChild as HTMLElement; const lampElement = container.firstChild as HTMLElement;
expect(lampElement).toHaveAttribute('aria-label', expectedLabel); expect(lampElement).toHaveAttribute("aria-label", expectedLabel);
}); });
}); });

View File

@@ -1,33 +1,33 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from "@testing-library/react";
import { LampLine } from '../components/LampLine'; import { LampLine } from "../components/LampLine";
import { LampState } from '../types'; import { LampState } from "../types";
describe('LampLine', () => { describe("LampLine", () => {
it('renders without crashing', () => { it("renders without crashing", () => {
render(<LampLine states={[]} />); render(<LampLine states={[]} />);
}); });
it('renders the correct number of lamps', () => { it("renders the correct number of lamps", () => {
const states = [LampState.Y, LampState.R, LampState.O]; const states = [LampState.Y, LampState.R, LampState.O];
render(<LampLine states={states} />); render(<LampLine states={states} />);
const container = screen.getByTestId('lamp-line'); const container = screen.getByTestId("lamp-line");
expect(container.children).toHaveLength(states.length); expect(container.children).toHaveLength(states.length);
}); });
it('displays correct aria-labels for each lamp', () => { it("displays correct aria-labels for each lamp", () => {
const states = [LampState.Y, LampState.R, LampState.O, LampState.N]; const states = [LampState.Y, LampState.R, LampState.O, LampState.N];
render(<LampLine states={states} />); render(<LampLine states={states} />);
const container = screen.getByTestId('lamp-line'); const container = screen.getByTestId("lamp-line");
const lampSpans = container.querySelectorAll('span'); const lampSpans = container.querySelectorAll("span");
expect(lampSpans).toHaveLength(states.length); expect(lampSpans).toHaveLength(states.length);
lampSpans.forEach((span, index) => { lampSpans.forEach((span, index) => {
expect(span).toHaveAttribute('aria-label', states[index]); expect(span).toHaveAttribute("aria-label", states[index]);
}); });
}); });
it('renders no lamps when given an empty array', () => { it("renders no lamps when given an empty array", () => {
render(<LampLine states={[]} />); render(<LampLine states={[]} />);
const container = screen.getByTestId('lamp-line'); const container = screen.getByTestId("lamp-line");
expect(container.children).toHaveLength(0); expect(container.children).toHaveLength(0);
}); });
}); });

View File

@@ -1,18 +1,29 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from "@testing-library/react";
import { TimeConverter } from '../containers/TimeConverter'; import { TimeConverter } from "../containers/TimeConverter";
import { useCurrentTime } from '../hooks/useCurrentTime'; import { useCurrentTime } from "../hooks/useCurrentTime";
vi.mock('../hooks/useCurrentTime'); vi.mock("../hooks/useCurrentTime");
vi.mock('../containers/BerlinClock', () => ({ vi.mock("../containers/BerlinClock", () => ({
BerlinClock: ({ enabled }: { enabled: boolean }) => ( BerlinClock: ({ enabled }: { enabled: boolean }) => (
<div data-testid="berlin-clock">{enabled ? 'enabled' : 'disabled'}</div> <div data-testid="berlin-clock">{enabled ? "enabled" : "disabled"}</div>
), ),
})); }));
vi.mock('../containers/DigitalClock', () => ({ vi.mock("../containers/DigitalClock", () => ({
DigitalClock: ({ enabled, currentTime, onTimeChange }: { enabled: boolean; currentTime: Date; onTimeChange: (d: Date) => void }) => ( DigitalClock: ({
enabled,
currentTime,
onTimeChange,
}: {
enabled: boolean;
currentTime: Date;
onTimeChange: (d: Date) => void;
}) => (
<div data-testid="digital-clock-wrapper"> <div data-testid="digital-clock-wrapper">
<span data-testid="digital-clock-time">{currentTime.toISOString()}</span> <span data-testid="digital-clock-time">{currentTime.toISOString()}</span>
<button data-testid="set-time" onClick={() => onTimeChange(new Date(2025,0,1,12,0,0))}> <button
data-testid="set-time"
onClick={() => onTimeChange(new Date(2025, 0, 1, 12, 0, 0))}
>
Set time Set time
</button> </button>
{!enabled && <span data-testid="disabled-indicator">disabled</span>} {!enabled && <span data-testid="disabled-indicator">disabled</span>}
@@ -22,7 +33,7 @@ vi.mock('../containers/DigitalClock', () => ({
const mockedUseCurrentTime = vi.mocked(useCurrentTime); const mockedUseCurrentTime = vi.mocked(useCurrentTime);
describe('TimeConverter', () => { describe("TimeConverter", () => {
const setCurrentTimeMock = vi.fn(); const setCurrentTimeMock = vi.fn();
const baseDate = new Date(2025, 0, 1, 14, 30, 15); const baseDate = new Date(2025, 0, 1, 14, 30, 15);
@@ -34,23 +45,25 @@ describe('TimeConverter', () => {
}); });
}); });
it('renders the checkbox, BerlinClock, and DigitalClock', () => { it("renders the checkbox, BerlinClock, and DigitalClock", () => {
render(<TimeConverter />); render(<TimeConverter />);
expect(screen.getByTestId('use-system-time')).toBeInTheDocument(); expect(screen.getByTestId("use-system-time")).toBeInTheDocument();
expect(screen.getByTestId('berlin-clock')).toHaveTextContent('enabled'); expect(screen.getByTestId("berlin-clock")).toHaveTextContent("enabled");
expect(screen.getByTestId('digital-clock-wrapper')).toBeInTheDocument(); expect(screen.getByTestId("digital-clock-wrapper")).toBeInTheDocument();
}); });
it('disables both clocks when checkbox is checked', () => { it("disables both clocks when checkbox is checked", () => {
render(<TimeConverter />); render(<TimeConverter />);
fireEvent.click(screen.getByTestId('use-system-time')); fireEvent.click(screen.getByTestId("use-system-time"));
expect(screen.getByTestId('berlin-clock')).toHaveTextContent('disabled'); expect(screen.getByTestId("berlin-clock")).toHaveTextContent("disabled");
expect(screen.getByTestId('disabled-indicator')).toBeInTheDocument(); expect(screen.getByTestId("disabled-indicator")).toBeInTheDocument();
}); });
it('calls setCurrentTime when DigitalClock triggers onTimeChange', () => { it("calls setCurrentTime when DigitalClock triggers onTimeChange", () => {
render(<TimeConverter />); render(<TimeConverter />);
fireEvent.click(screen.getByTestId('set-time')); fireEvent.click(screen.getByTestId("set-time"));
expect(setCurrentTimeMock).toHaveBeenCalledWith(new Date(2025, 0, 1, 12, 0, 0)); expect(setCurrentTimeMock).toHaveBeenCalledWith(
new Date(2025, 0, 1, 12, 0, 0),
);
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { renderHook, act } from '@testing-library/react'; import { renderHook } from "@testing-library/react";
import { useCurrentTime } from '../hooks/useCurrentTime'; import { useCurrentTime } from "../hooks/useCurrentTime";
describe('useCurrentTime', () => { describe("useCurrentTime", () => {
it('returns a date and setter', () => { it("returns a date and setter", () => {
const { result } = renderHook(() => useCurrentTime()); const { result } = renderHook(() => useCurrentTime());
expect(result.current.currentTime).toBeInstanceOf(Date); expect(result.current.currentTime).toBeInstanceOf(Date);
expect(typeof result.current.setCurrentTime).toBe('function'); expect(typeof result.current.setCurrentTime).toBe("function");
}); });
}); });

View File

@@ -1,86 +1,157 @@
import { dateToDigital, digitalToDate, dateToBerlin, berlinToDate } from '../utils'; import {
import { LampState } from '../types'; dateToDigital,
digitalToDate,
dateToBerlin,
berlinToDate,
} from "../utils";
import { LampState } from "../types";
describe('Utils', () => { describe("Utils", () => {
describe('dateToDigital', () => { describe("dateToDigital", () => {
it('returns "HH:mm:ss" for a given date', () => { it('returns "HH:mm:ss" for a given date', () => {
const date = new Date(2025, 0, 1, 14, 5, 9); const date = new Date(2025, 0, 1, 14, 5, 9);
expect(dateToDigital(date)).toBe('14:05:09'); expect(dateToDigital(date)).toBe("14:05:09");
}); });
it('pads single-digit hours, minutes, seconds', () => { it("pads single-digit hours, minutes, seconds", () => {
const date = new Date(2025, 0, 1, 2, 3, 4); const date = new Date(2025, 0, 1, 2, 3, 4);
expect(dateToDigital(date)).toBe('02:03:04'); expect(dateToDigital(date)).toBe("02:03:04");
}); });
}); });
describe('digitalToDate', () => { describe("digitalToDate", () => {
it('parses a valid digital string', () => { it("parses a valid digital string", () => {
const result = digitalToDate('12:34:56'); const result = digitalToDate("12:34:56");
expect(result).toBeInstanceOf(Date); expect(result).toBeInstanceOf(Date);
expect(result.getHours()).toBe(12); expect(result.getHours()).toBe(12);
expect(result.getMinutes()).toBe(34); expect(result.getMinutes()).toBe(34);
expect(result.getSeconds()).toBe(56); expect(result.getSeconds()).toBe(56);
}); });
it('returns an invalid date for invalid input', () => { it("returns an invalid date for invalid input", () => {
const result = digitalToDate('abc'); const result = digitalToDate("abc");
expect(isNaN(result.getTime())).toBe(true); expect(isNaN(result.getTime())).toBe(true);
}); });
}); });
describe('dateToBerlin', () => { describe("dateToBerlin", () => {
const makeTime = (h: number, m: number, s: number) => { const makeTime = (h: number, m: number, s: number) => {
const d = new Date(2025, 0, 1, h, m, s); const d = new Date(2025, 0, 1, h, m, s);
return d; return d;
}; };
it('returns correct clock for 00:00:00', () => { it("returns correct clock for 00:00:00", () => {
const clock = dateToBerlin(makeTime(0, 0, 0)); const clock = dateToBerlin(makeTime(0, 0, 0));
expect(clock.secondsRow).toEqual([LampState.Y]); expect(clock.secondsRow).toEqual([LampState.Y]);
expect(clock.fiveHours).toEqual([LampState.O, LampState.O, LampState.O, LampState.O]); expect(clock.fiveHours).toEqual([
expect(clock.oneHour).toEqual([LampState.O, LampState.O, LampState.O, LampState.O]); LampState.O,
expect(clock.fiveMinutes).toEqual([ LampState.O,
LampState.O, LampState.O, LampState.O, LampState.O,
LampState.O, LampState.O, LampState.O, LampState.O,
LampState.O, LampState.O, LampState.O, ]);
LampState.O, LampState.O, expect(clock.oneHour).toEqual([
LampState.O,
LampState.O,
LampState.O,
LampState.O,
]);
expect(clock.fiveMinutes).toEqual([
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
]);
expect(clock.oneMinute).toEqual([
LampState.O,
LampState.O,
LampState.O,
LampState.O,
]); ]);
expect(clock.oneMinute).toEqual([LampState.O, LampState.O, LampState.O, LampState.O]);
}); });
it('returns correct clock for 23:59:59', () => { it("returns correct clock for 23:59:59", () => {
const clock = dateToBerlin(makeTime(23, 59, 59)); const clock = dateToBerlin(makeTime(23, 59, 59));
expect(clock.secondsRow).toEqual([LampState.O]); expect(clock.secondsRow).toEqual([LampState.O]);
expect(clock.fiveHours).toEqual([LampState.R, LampState.R, LampState.R, LampState.R]); expect(clock.fiveHours).toEqual([
expect(clock.oneHour).toEqual([LampState.R, LampState.R, LampState.R, LampState.O]); LampState.R,
expect(clock.fiveMinutes).toEqual([ LampState.R,
LampState.Y, LampState.Y, LampState.R, LampState.R,
LampState.Y, LampState.Y, LampState.R, LampState.R,
LampState.Y, LampState.Y, LampState.R, ]);
LampState.Y, LampState.Y, expect(clock.oneHour).toEqual([
LampState.R,
LampState.R,
LampState.R,
LampState.O,
]);
expect(clock.fiveMinutes).toEqual([
LampState.Y,
LampState.Y,
LampState.R,
LampState.Y,
LampState.Y,
LampState.R,
LampState.Y,
LampState.Y,
LampState.R,
LampState.Y,
LampState.Y,
]);
expect(clock.oneMinute).toEqual([
LampState.Y,
LampState.Y,
LampState.Y,
LampState.Y,
]); ]);
expect(clock.oneMinute).toEqual([LampState.Y, LampState.Y, LampState.Y, LampState.Y]);
}); });
it('returns correct clock for 12:34:00', () => { it("returns correct clock for 12:34:00", () => {
const clock = dateToBerlin(makeTime(12, 34, 0)); const clock = dateToBerlin(makeTime(12, 34, 0));
expect(clock.secondsRow).toEqual([LampState.Y]); expect(clock.secondsRow).toEqual([LampState.Y]);
expect(clock.fiveHours).toEqual([LampState.R, LampState.R, LampState.O, LampState.O]); expect(clock.fiveHours).toEqual([
expect(clock.oneHour).toEqual([LampState.R, LampState.R, LampState.O, LampState.O]); LampState.R,
LampState.R,
LampState.O,
LampState.O,
]);
expect(clock.oneHour).toEqual([
LampState.R,
LampState.R,
LampState.O,
LampState.O,
]);
const expectedFiveMinutes = [ const expectedFiveMinutes = [
LampState.Y, LampState.Y, LampState.R, LampState.Y,
LampState.Y, LampState.Y, LampState.R, LampState.Y,
LampState.O, LampState.O, LampState.O, LampState.R,
LampState.O, LampState.O, LampState.Y,
LampState.Y,
LampState.R,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
]; ];
expect(clock.fiveMinutes).toEqual(expectedFiveMinutes); expect(clock.fiveMinutes).toEqual(expectedFiveMinutes);
expect(clock.oneMinute).toEqual([LampState.Y, LampState.Y, LampState.Y, LampState.Y]); expect(clock.oneMinute).toEqual([
LampState.Y,
LampState.Y,
LampState.Y,
LampState.Y,
]);
}); });
}); });
describe('berlinToDate', () => { describe("berlinToDate", () => {
it('roundtrips correctly: dateToBerlin then berlinToDate returns the original date (seconds floor to even)', () => { it("roundtrips correctly: dateToBerlin then berlinToDate returns the original date (seconds floor to even)", () => {
const original = new Date(2025, 0, 1, 14, 30, 0); const original = new Date(2025, 0, 1, 14, 30, 0);
const clock = dateToBerlin(original); const clock = dateToBerlin(original);
const result = berlinToDate(clock); const result = berlinToDate(clock);
@@ -89,7 +160,7 @@ describe('Utils', () => {
expect(result.getSeconds()).toBe(0); expect(result.getSeconds()).toBe(0);
}); });
it('roundtrips with odd seconds: result seconds will be zero (only parity stored)', () => { it("roundtrips with odd seconds: result seconds will be zero (only parity stored)", () => {
const original = new Date(2025, 0, 1, 23, 59, 59); const original = new Date(2025, 0, 1, 23, 59, 59);
const clock = dateToBerlin(original); const clock = dateToBerlin(original);
const result = berlinToDate(clock); const result = berlinToDate(clock);
@@ -98,16 +169,23 @@ describe('Utils', () => {
expect(result.getSeconds()).toBe(0); expect(result.getSeconds()).toBe(0);
}); });
it('converts a known Berlin clock to the correct time', () => { it("converts a known Berlin clock to the correct time", () => {
const clock = { const clock = {
secondsRow: [LampState.Y], secondsRow: [LampState.Y],
fiveHours: [LampState.R, LampState.R, LampState.O, LampState.O], fiveHours: [LampState.R, LampState.R, LampState.O, LampState.O],
oneHour: [LampState.R, LampState.R, LampState.O, LampState.O], oneHour: [LampState.R, LampState.R, LampState.O, LampState.O],
fiveMinutes: [ fiveMinutes: [
LampState.Y, LampState.Y, LampState.R, LampState.Y,
LampState.Y, LampState.Y, LampState.R, LampState.Y,
LampState.O, LampState.O, LampState.O, LampState.R,
LampState.O, LampState.O, LampState.Y,
LampState.Y,
LampState.R,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
LampState.O,
], ],
oneMinute: [LampState.Y, LampState.Y, LampState.Y, LampState.Y], oneMinute: [LampState.Y, LampState.Y, LampState.Y, LampState.Y],
}; };

View File

@@ -1,10 +1,41 @@
import { LampState } from '../types'; import React from "react";
import { LampState } from "../types";
interface LampProps { interface LampProps {
state: LampState; state: LampState;
id?: string; id?: string;
variant?: "alone" | "first" | "last" | "middle";
} }
export function Lamp({ state, id }: LampProps) { function getBackgroundColor(state: LampState): string {
return <span data-testid={id} aria-label={state} />; if (state === LampState.R) return "#ff3333";
if (state === LampState.Y) return "#ffcc00";
return "#333";
}
function getBorderRadius(variant?: LampProps["variant"]): string {
switch (variant) {
case "alone":
return "50%";
case "first":
return "50% 0 0 50%";
case "last":
return "0 50% 50% 0";
default:
return "0";
}
}
export function Lamp({ state, id, variant }: LampProps) {
const style: React.CSSProperties = {
display: "inline-block",
width: 20,
height: 20,
backgroundColor: getBackgroundColor(state),
borderRadius: getBorderRadius(variant),
margin: 1,
transition: "background-color 0.2s",
};
return <span data-testid={id} aria-label={state} style={style} />;
} }

View File

@@ -1,5 +1,5 @@
import { LampState } from '../types'; import { LampState } from "../types";
import { Lamp } from './Lamp'; import { Lamp } from "./Lamp";
interface LampLineProps { interface LampLineProps {
states: LampState[]; states: LampState[];
@@ -7,11 +7,32 @@ interface LampLineProps {
} }
export function LampLine({ states, rowTestId }: LampLineProps) { export function LampLine({ states, rowTestId }: LampLineProps) {
const len = states.length;
return ( return (
<div data-testid={rowTestId ?? "lamp-line"}> <div
{states.map((state, index) => ( data-testid={rowTestId ?? "lamp-line"}
<Lamp key={index} state={state} id={`lamp-${index}`} /> style={{ display: "flex", gap: 2, marginBottom: 5 }}
))} >
{states.map((state, index) => {
let variant: "alone" | "first" | "last" | "middle" | undefined;
if (len === 1) {
variant = "alone";
} else if (index === 0) {
variant = "first";
} else if (index === len - 1) {
variant = "last";
} else {
variant = "middle";
}
return (
<Lamp
key={index}
state={state}
id={`lamp-${index}`}
variant={variant}
/>
);
})}
</div> </div>
); );
} }

View File

@@ -12,8 +12,11 @@ export function BerlinClock({ enabled, clock }: BerlinClockProps) {
return ( return (
<div> <div>
<div data-testid="seconds-row"> <div
<Lamp state={clock.secondsRow[0]} id="seconds-lamp" /> data-testid="seconds-row"
style={{ display: "flex", gap: 2, marginBottom: 5 }}
>
<Lamp state={clock.secondsRow[0]} id="seconds-lamp" variant="alone" />
</div> </div>
<LampLine states={clock.fiveHours} rowTestId="five-hours-row" /> <LampLine states={clock.fiveHours} rowTestId="five-hours-row" />
<LampLine states={clock.oneHour} rowTestId="single-hours-row" /> <LampLine states={clock.oneHour} rowTestId="single-hours-row" />

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from "react";
import { dateToDigital, digitalToDate } from '../utils'; import { dateToDigital, digitalToDate } from "../utils";
interface DigitalClockProps { interface DigitalClockProps {
enabled: boolean; enabled: boolean;
@@ -7,7 +7,11 @@ interface DigitalClockProps {
onTimeChange: (time: Date) => void; onTimeChange: (time: Date) => void;
} }
export function DigitalClock({ enabled, currentTime, onTimeChange }: DigitalClockProps) { export function DigitalClock({
enabled,
currentTime,
onTimeChange,
}: DigitalClockProps) {
const [inputValue, setInputValue] = useState(dateToDigital(currentTime)); const [inputValue, setInputValue] = useState(dateToDigital(currentTime));
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState } from "react";
export function useCurrentTime(): { export function useCurrentTime(): {
currentTime: Date; currentTime: Date;

View File

@@ -1 +1 @@
import '@testing-library/jest-dom'; import "@testing-library/jest-dom";

View File

@@ -1,8 +1,8 @@
export enum LampState { export enum LampState {
Y = 'Y', // Yellow Y = "Y", // Yellow
R = 'R', // Red R = "R", // Red
O = 'O', // Off O = "O", // Off
N = 'N', // None / undefined N = "N", // None / undefined
} }
export interface BerlinClock { export interface BerlinClock {

View File

@@ -1,13 +1,13 @@
import type { BerlinClock } from '../types'; import type { BerlinClock } from "../types";
import { LampState } from '../types'; import { LampState } from "../types";
export function dateToDigital(date: Date): string { export function dateToDigital(date: Date): string {
const pad = (n: number) => n.toString().padStart(2, '0'); const pad = (n: number) => n.toString().padStart(2, "0");
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
} }
export function digitalToDate(digital: string): Date { export function digitalToDate(digital: string): Date {
const parts = digital.split(':'); const parts = digital.split(":");
if (parts.length !== 3) return new Date(NaN); if (parts.length !== 3) return new Date(NaN);
const [h, m, s] = parts.map(Number); const [h, m, s] = parts.map(Number);
if (isNaN(h) || isNaN(m) || isNaN(s)) return new Date(NaN); if (isNaN(h) || isNaN(m) || isNaN(s)) return new Date(NaN);
@@ -65,8 +65,7 @@ export function berlinToDate(berlin: BerlinClock): Date {
arr.filter((s) => s === LampState.R || s === LampState.Y).length; arr.filter((s) => s === LampState.R || s === LampState.Y).length;
const hours = countR(berlin.fiveHours) * 5 + countR(berlin.oneHour); const hours = countR(berlin.fiveHours) * 5 + countR(berlin.oneHour);
const minutes = const minutes = countYorR(berlin.fiveMinutes) * 5 + countY(berlin.oneMinute);
countYorR(berlin.fiveMinutes) * 5 + countY(berlin.oneMinute);
const d = new Date(); const d = new Date();
d.setHours(hours, minutes, 0, 0); d.setHours(hours, minutes, 0, 0);