refactor: replace mocked time with injected clock prop and implement conversion logic
This commit is contained in:
committed by
Loic Coenen (aider)
parent
88ce663b44
commit
2c84312947
@@ -1,74 +1,54 @@
|
|||||||
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';
|
||||||
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', () => {
|
describe('BerlinClock', () => {
|
||||||
beforeEach(() => {
|
const testClock = {
|
||||||
// Set a fixed date for the hook
|
secondsRow: [LampState.Y],
|
||||||
mockedUseCurrentTime.mockReturnValue({
|
fiveHours: [LampState.R, LampState.R, LampState.R, LampState.O],
|
||||||
currentTime: new Date(2025, 0, 1, 14, 30, 0),
|
oneHour: [LampState.R, LampState.R, LampState.R, LampState.R],
|
||||||
setCurrentTime: vi.fn(),
|
fiveMinutes: [
|
||||||
});
|
LampState.Y, LampState.Y, LampState.R,
|
||||||
|
LampState.Y, LampState.Y, LampState.R,
|
||||||
// Provide a controlled BerlinClock output for that time
|
LampState.O, LampState.O, LampState.O,
|
||||||
mockedDateToBerlin.mockReturnValue({
|
LampState.O, LampState.O,
|
||||||
secondsRow: [LampState.Y],
|
],
|
||||||
fiveHours: [LampState.R, LampState.R, LampState.R, LampState.O],
|
oneMinute: [LampState.O, LampState.O, LampState.O, 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', () => {
|
it('renders nothing when disabled', () => {
|
||||||
const { container } = render(<BerlinClock enabled={false} />);
|
const { container } = render(<BerlinClock enabled={false} clock={testClock} />);
|
||||||
expect(container.innerHTML).toBe('');
|
expect(container.innerHTML).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders all five rows when enabled', () => {
|
it('renders all five rows when enabled', () => {
|
||||||
render(<BerlinClock enabled={true} />);
|
render(<BerlinClock enabled={true} clock={testClock} />);
|
||||||
|
|
||||||
expect(screen.getByTestId('seconds-lamp')).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 seconds lamp with correct aria-label', () => {
|
it('renders the correct number of lamps in seconds-row', () => {
|
||||||
render(<BerlinClock enabled={true} />);
|
render(<BerlinClock enabled={true} clock={testClock} />);
|
||||||
const secLamp = screen.getByTestId('seconds-lamp').querySelector('[data-testid="lamp-0"]');
|
const secRow = screen.getByTestId('seconds-row');
|
||||||
expect(secLamp).toHaveAttribute('aria-label', 'Y');
|
const lampLine = secRow.querySelector('[data-testid="lamp-line"]');
|
||||||
|
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} />);
|
render(<BerlinClock enabled={true} clock={testClock} />);
|
||||||
const container = screen.getByTestId('five-hours-row').querySelector('[data-testid="lamp-line"]');
|
const row = screen.getByTestId('five-hours-row');
|
||||||
expect(container?.children).toHaveLength(4);
|
const lampLine = row.querySelector('[data-testid="lamp-line"]');
|
||||||
|
expect(lampLine?.children).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correct aria-labels in five-hours-row', () => {
|
it('renders correct aria-labels in the seconds lamp', () => {
|
||||||
render(<BerlinClock enabled={true} />);
|
render(<BerlinClock enabled={true} clock={testClock} />);
|
||||||
const container = screen.getByTestId('five-hours-row').querySelector('[data-testid="lamp-line"]');
|
const secRow = screen.getByTestId('seconds-row');
|
||||||
const spans = container?.querySelectorAll('span');
|
const span = secRow.querySelector('span');
|
||||||
expect(spans).toHaveLength(4);
|
expect(span).toHaveAttribute('aria-label', 'Y');
|
||||||
const expected = ['R', 'R', 'R', 'O'];
|
|
||||||
spans?.forEach((span, i) => {
|
|
||||||
expect(span).toHaveAttribute('aria-label', expected[i]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe('Utils', () => {
|
|||||||
fiveMinutes: [
|
fiveMinutes: [
|
||||||
LampState.Y, LampState.Y, LampState.R,
|
LampState.Y, LampState.Y, LampState.R,
|
||||||
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,
|
LampState.O, LampState.O,
|
||||||
],
|
],
|
||||||
oneMinute: [LampState.Y, LampState.Y, LampState.Y, LampState.Y],
|
oneMinute: [LampState.Y, LampState.Y, LampState.Y, LampState.Y],
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { LampState } from '../types';
|
|||||||
|
|
||||||
interface LampProps {
|
interface LampProps {
|
||||||
state: LampState;
|
state: LampState;
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Lamp({ state }: LampProps) {
|
export function Lamp({ state, id }: LampProps) {
|
||||||
return <span aria-label={state} />;
|
return <span data-testid={id} aria-label={state} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { Lamp } from './Lamp';
|
|||||||
|
|
||||||
interface LampLineProps {
|
interface LampLineProps {
|
||||||
states: LampState[];
|
states: LampState[];
|
||||||
|
rowTestId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LampLine({ states }: LampLineProps) {
|
export function LampLine({ states, rowTestId }: LampLineProps) {
|
||||||
return (
|
return (
|
||||||
<div data-testid="lamp-line">
|
<div data-testid={rowTestId ?? "lamp-line"}>
|
||||||
{states.map((state, index) => (
|
{states.map((state, index) => (
|
||||||
<Lamp key={index} state={state} />
|
<Lamp key={index} state={state} id={`lamp-${index}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,49 +1,24 @@
|
|||||||
import { useState } from 'react';
|
import { Lamp } from "../components/Lamp";
|
||||||
import { Lamp } from '../components/Lamp';
|
import { LampLine } from "../components/LampLine";
|
||||||
import { LampLine } from '../components/LampLine';
|
import type { BerlinClock as BerlinClockType } from "../types";
|
||||||
import { useCurrentTime } from '../hooks/useCurrentTime';
|
|
||||||
import { dateToBerlin } from '../utils';
|
|
||||||
|
|
||||||
interface BerlinClockProps {
|
interface BerlinClockProps {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
clock: BerlinClockType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BerlinClock({ enabled }: BerlinClockProps) {
|
export function BerlinClock({ enabled, clock }: BerlinClockProps) {
|
||||||
const { currentTime } = useCurrentTime();
|
|
||||||
const berlin = dateToBerlin(currentTime);
|
|
||||||
|
|
||||||
if (!enabled) return null;
|
if (!enabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Seconds lamp */}
|
<div data-testid="seconds-row">
|
||||||
<div data-testid="seconds-lamp">
|
<Lamp state={clock.secondsRow[0]} id="seconds-lamp" />
|
||||||
{berlin.secondsRow.length > 0 && (
|
|
||||||
<span data-testid="lamp-0" aria-label={berlin.secondsRow[0]}>
|
|
||||||
{/* optional visual */}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Five hours row */}
|
|
||||||
<div data-testid="five-hours-row">
|
|
||||||
<LampLine states={berlin.fiveHours} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Single hours row */}
|
|
||||||
<div data-testid="single-hours-row">
|
|
||||||
<LampLine states={berlin.oneHour} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Five minutes row */}
|
|
||||||
<div data-testid="five-minutes-row">
|
|
||||||
<LampLine states={berlin.fiveMinutes} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Single minutes row */}
|
|
||||||
<div data-testid="single-minutes-row">
|
|
||||||
<LampLine states={berlin.oneMinute} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<LampLine states={clock.fiveHours} rowTestId="five-hours-row" />
|
||||||
|
<LampLine states={clock.oneHour} rowTestId="single-hours-row" />
|
||||||
|
<LampLine states={clock.fiveMinutes} rowTestId="five-minutes-row" />
|
||||||
|
<LampLine states={clock.oneMinute} rowTestId="single-minutes-row" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { BerlinClock } from './BerlinClock';
|
import { BerlinClock } from "./BerlinClock";
|
||||||
import { DigitalClock } from './DigitalClock';
|
import { DigitalClock } from "./DigitalClock";
|
||||||
import { useCurrentTime } from '../hooks/useCurrentTime';
|
import { useCurrentTime } from "../hooks/useCurrentTime";
|
||||||
|
import { dateToBerlin } from "../utils";
|
||||||
|
|
||||||
export function TimeConverter() {
|
export function TimeConverter() {
|
||||||
const { currentTime, setCurrentTime } = useCurrentTime();
|
const { currentTime, setCurrentTime } = useCurrentTime();
|
||||||
const [useSystemTime, setUseSystemTime] = useState(false);
|
const [useSystemTime, setUseSystemTime] = useState(false);
|
||||||
|
const enabled = !useSystemTime;
|
||||||
const handleTimeChange = (newTime: Date) => {
|
const berlinClock = dateToBerlin(currentTime);
|
||||||
setCurrentTime(newTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -22,11 +21,11 @@ export function TimeConverter() {
|
|||||||
/>
|
/>
|
||||||
Use system time
|
Use system time
|
||||||
</label>
|
</label>
|
||||||
<BerlinClock enabled={!useSystemTime} />
|
<BerlinClock enabled={enabled} clock={berlinClock} />
|
||||||
<DigitalClock
|
<DigitalClock
|
||||||
enabled={!useSystemTime}
|
enabled={enabled}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
onTimeChange={handleTimeChange}
|
onTimeChange={setCurrentTime}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,25 +1,74 @@
|
|||||||
import type { BerlinClock } from '../types';
|
import type { BerlinClock } from '../types';
|
||||||
|
import { LampState } from '../types';
|
||||||
|
|
||||||
export function dateToDigital(date: Date): string {
|
export function dateToDigital(date: Date): string {
|
||||||
// returns "HH:mm:ss"
|
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||||
return '';
|
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function digitalToDate(digital: string): Date {
|
export function digitalToDate(digital: string): Date {
|
||||||
// parses "HH:mm:ss"
|
const parts = digital.split(':');
|
||||||
return new Date();
|
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 {
|
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 {
|
return {
|
||||||
secondsRow: [],
|
secondsRow: secondsLamp,
|
||||||
fiveHours: [],
|
fiveHours,
|
||||||
oneHour: [],
|
oneHour,
|
||||||
fiveMinutes: [],
|
fiveMinutes,
|
||||||
oneMinute: [],
|
oneMinute,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function berlinToDate(berlin: BerlinClock): Date {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user