Skip to content

Commit 58c2472

Browse files
author
Pascal Wegner
committed
Connect to interval timer completely
1 parent 7b6152c commit 58c2472

File tree

9 files changed

+361
-21
lines changed

9 files changed

+361
-21
lines changed

packages/pwa/jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const path = require('path');
2+
13
module.exports = {
24
displayName: {
35
name: 'PWA',
@@ -12,6 +14,12 @@ module.exports = {
1214
statements: 100,
1315
},
1416
},
17+
transform: {
18+
'.+\\.(js|jsx|ts|tsx)$': [
19+
'babel-jest',
20+
{ configFile: path.resolve(__dirname, './.babelrc') },
21+
],
22+
},
1523
setupFilesAfterEnv: ['<rootDir>/setup-tests.js'],
1624
testEnvironment: 'jsdom',
1725
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
let interval = null;
2+
self.addEventListener('message', (e) => {
3+
if (e.data === 'START') {
4+
interval = setInterval(() => {
5+
self.postMessage('TICK');
6+
}, 5);
7+
}
8+
if (e.data === 'STOP') {
9+
clearInterval(interval);
10+
}
11+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export class MockWorker {
2+
interval = null;
3+
callback = () => {};
4+
addEventListener = (_event, callback) => {
5+
this.callback = callback;
6+
};
7+
8+
postMessage = (e) => {
9+
if (e === 'START') {
10+
this.interval = setInterval(() => {
11+
this.callback({ data: 'TICK' });
12+
}, 1000);
13+
}
14+
if (e === 'STOP') {
15+
clearInterval(this.interval);
16+
}
17+
};
18+
}
Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,183 @@
1-
import { render, screen } from '@testing-library/react';
2-
import Home from '../pages';
1+
import { fireEvent, screen } from '@testing-library/react';
2+
import { MockWorker } from '../__mocks__/Worker';
3+
import { expectCountDownFrom } from '../test-utils/expectCountDownFrom';
4+
import { makeAdvanceDateNowBy } from '../test-utils/makeAdvanceDateNowBy';
5+
import { renderApp } from '../test-utils/renderApp';
6+
7+
const startDate = 1587574443099;
38

49
describe('Home', () => {
10+
beforeEach(() => {
11+
window.Worker = MockWorker as any;
12+
jest.useFakeTimers('modern');
13+
});
14+
15+
afterEach(() => {
16+
jest.clearAllMocks();
17+
window.Worker = undefined;
18+
});
19+
520
it('should work', () => {
6-
render(<Home />);
7-
expect(screen.getByRole('button', { name: /start/i }));
21+
const {
22+
breakIntervalMinuteInput,
23+
breakIntervalSecondInput,
24+
prepTime,
25+
roundInput,
26+
startButton,
27+
workIntervalMinuteInput,
28+
workIntervalSecondInput,
29+
} = renderApp();
30+
31+
expect(workIntervalMinuteInput).toBeTruthy();
32+
expect(workIntervalSecondInput).toBeTruthy();
33+
expect(breakIntervalMinuteInput).toBeTruthy();
34+
35+
const workMinutesValue = '1';
36+
const workSecondsValue = '1';
37+
const workMinutes = Number(workMinutesValue);
38+
const workSeconds = Number(workSecondsValue);
39+
40+
const breakMinutesValue = '1';
41+
const breakSecondsValue = '1';
42+
const breakMinutes = Number(breakMinutesValue);
43+
const breakSeconds = Number(breakSecondsValue);
44+
45+
const roundsValue = '2';
46+
47+
fireEvent.change(roundInput, { target: { value: roundsValue } });
48+
fireEvent.blur(roundInput);
49+
50+
fireEvent.change(workIntervalMinuteInput, {
51+
target: { value: workMinutesValue },
52+
});
53+
fireEvent.change(workIntervalSecondInput, {
54+
target: { value: workSecondsValue },
55+
});
56+
57+
fireEvent.change(breakIntervalMinuteInput, {
58+
target: { value: breakMinutesValue },
59+
});
60+
fireEvent.change(breakIntervalSecondInput, {
61+
target: { value: breakSecondsValue },
62+
});
63+
64+
const advanceDateNowBy = makeAdvanceDateNowBy(startDate);
65+
66+
fireEvent.click(startButton);
67+
68+
const timeLeftMinutes = screen.getByTestId(
69+
'time-left-minutes'
70+
) as HTMLInputElement;
71+
72+
const timeLeftSeconds = screen.getByTestId(
73+
'time-left-seconds'
74+
) as HTMLInputElement;
75+
76+
const round = screen.getByTestId('round');
77+
78+
expect(round.textContent).toBe('0/2');
79+
80+
expectCountDownFrom({
81+
minutes: 0,
82+
seconds: prepTime,
83+
advanceDateNowBy,
84+
timeLeftMinutes,
85+
timeLeftSeconds,
86+
});
87+
88+
expect(round.textContent).toBe('1/2');
89+
90+
expectCountDownFrom({
91+
minutes: workMinutes,
92+
seconds: workSeconds,
93+
advanceDateNowBy,
94+
timeLeftMinutes,
95+
timeLeftSeconds,
96+
});
97+
98+
expectCountDownFrom({
99+
minutes: breakMinutes,
100+
seconds: breakSeconds,
101+
advanceDateNowBy,
102+
timeLeftMinutes,
103+
timeLeftSeconds,
104+
});
105+
106+
expect(round.textContent).toBe('2/2');
107+
108+
expectCountDownFrom({
109+
minutes: workMinutes,
110+
seconds: workSeconds,
111+
advanceDateNowBy,
112+
timeLeftMinutes,
113+
timeLeftSeconds,
114+
});
115+
});
116+
117+
it('should stop timer when clicking button again', () => {
118+
const {
119+
breakIntervalMinuteInput,
120+
breakIntervalSecondInput,
121+
prepTime,
122+
roundInput,
123+
startButton,
124+
workIntervalMinuteInput,
125+
workIntervalSecondInput,
126+
} = renderApp();
127+
128+
expect(roundInput).toBeTruthy();
129+
expect(workIntervalMinuteInput).toBeTruthy();
130+
expect(workIntervalSecondInput).toBeTruthy();
131+
expect(breakIntervalMinuteInput).toBeTruthy();
132+
expect(breakIntervalSecondInput).toBeTruthy();
133+
expect(startButton).toBeTruthy();
134+
135+
const workMinutesValue = '1';
136+
const workSecondsValue = '1';
137+
138+
const breakMinutesValue = '1';
139+
const breakSecondsValue = '1';
140+
141+
const roundsValue = '2';
142+
143+
fireEvent.change(roundInput, { target: { value: roundsValue } });
144+
fireEvent.blur(roundInput);
145+
146+
fireEvent.change(workIntervalMinuteInput, {
147+
target: { value: workMinutesValue },
148+
});
149+
fireEvent.change(workIntervalSecondInput, {
150+
target: { value: workSecondsValue },
151+
});
152+
153+
fireEvent.change(breakIntervalMinuteInput, {
154+
target: { value: breakMinutesValue },
155+
});
156+
fireEvent.change(breakIntervalSecondInput, {
157+
target: { value: breakSecondsValue },
158+
});
159+
160+
const advanceDateNowBy = makeAdvanceDateNowBy(startDate);
161+
162+
fireEvent.click(startButton);
163+
164+
const timeLeftSeconds = screen.getByTestId(
165+
'time-left-seconds'
166+
) as HTMLInputElement;
167+
const round = screen.getByTestId('round');
168+
169+
expect(round.textContent).toBe('0/2');
170+
171+
// count down from 00:05 and stop after two seconds
172+
expectCountDownFrom({
173+
minutes: 0,
174+
seconds: prepTime,
175+
toSeconds: 3,
176+
advanceDateNowBy,
177+
timeLeftMinutes: { value: '00' } as HTMLInputElement,
178+
timeLeftSeconds,
179+
});
180+
181+
fireEvent.click(startButton);
8182
});
9183
});

packages/pwa/src/components/DurationInput/DurationInput.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type DurationInputProps = {
88
value: Date;
99
label?: string;
1010
readOnly?: boolean;
11-
onChange: (value: Date) => void;
11+
onChange?: (value: Date) => void;
1212
dataTestId?: string;
1313
};
1414

@@ -44,13 +44,13 @@ export function DurationInput({
4444
formattedMinutes,
4545
targetValue
4646
);
47-
onChange(setMinutes(new Date(value.valueOf()), formattedTargetValue));
47+
onChange?.(setMinutes(new Date(value.valueOf()), formattedTargetValue));
4848
} else {
4949
const formattedTargetValue = addNumberAtEndShifting(
5050
formattedSeconds,
5151
targetValue
5252
);
53-
onChange(setSeconds(new Date(value.valueOf()), formattedTargetValue));
53+
onChange?.(setSeconds(new Date(value.valueOf()), formattedTargetValue));
5454
}
5555
}
5656
};
@@ -79,7 +79,7 @@ export function DurationInput({
7979
return (
8080
<div className="flex flex-col items-center">
8181
{label ? (
82-
<label className="text-blue-600 text-lg font-bold ">{label}</label>
82+
<label className="text-blue-600 text-lg font-bold">{label}</label>
8383
) : null}
8484
<div className="flex flex-row justify-center items-center text-black text-6xl box-content w-full">
8585
<input

packages/pwa/src/pages/index.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
import { FormFields } from '../components/FormFields/FormFields';
2-
import { timerMachine } from '@interval-timer/core';
2+
import { timerMachine, timerStates, TTimerStates } from '@interval-timer/core';
33
import { useMachine } from '@xstate/react';
4+
import * as React from 'react';
5+
import { Counter } from '../components/Counter/Counter';
46

57
export default function Home() {
6-
const [state, send] = useMachine(timerMachine);
8+
const [state, send, service] = useMachine(timerMachine);
9+
10+
React.useEffect(() => {
11+
const intervalWorker = new Worker('/intervalWorker.js');
12+
13+
intervalWorker.addEventListener('message', () => {
14+
service.send({ type: 'TICK' });
15+
});
16+
17+
service.subscribe((_state, event) => {
18+
if (event && (event.type === 'START' || event.type === 'STOP')) {
19+
intervalWorker.postMessage(event.type);
20+
}
21+
});
22+
}, [service]);
23+
24+
const toggleTimer = () => {
25+
if (state.value === timerStates.STOPPED) {
26+
send('START');
27+
} else {
28+
send('STOP');
29+
}
30+
};
731

832
const setRounds = (rounds: number) => {
933
send({ type: 'SET_ROUNDS', rounds: Number(rounds) });
@@ -23,26 +47,38 @@ export default function Home() {
2347
});
2448
};
2549

26-
const { breakInterval, rounds, workInterval } = state.context;
50+
const { breakInterval, rounds, workInterval, timeLeft, roundsLeft } =
51+
state.context;
2752

2853
return (
2954
<>
3055
<header />
3156
<main className="flex-1">
3257
<div className="h-full flex flex-col items-stretch bg-blue-600">
3358
<div className="flex flex-col justify-center flex-1 bg-white rounded-b-3xl">
34-
<FormFields
35-
rounds={rounds}
36-
onRoundsChange={setRounds}
37-
breakInterval={breakInterval}
38-
onBreakIntervalChange={setBreakInterval}
39-
workInterval={workInterval}
40-
onWorkIntervalChange={setWorkInterval}
41-
></FormFields>
59+
{state.value === timerStates.STOPPED ? (
60+
<FormFields
61+
rounds={rounds}
62+
onRoundsChange={setRounds}
63+
breakInterval={breakInterval}
64+
onBreakIntervalChange={setBreakInterval}
65+
workInterval={workInterval}
66+
onWorkIntervalChange={setWorkInterval}
67+
></FormFields>
68+
) : (
69+
<Counter
70+
timeLeft={timeLeft}
71+
roundsLeft={roundsLeft}
72+
rounds={rounds}
73+
/>
74+
)}
4275
</div>
4376
<div className="flex flex-col items-center flex-[0.25] pt-8 ">
44-
<button className="text-blue-600 bg-white text-xl px-8 h-14 rounded-full font-semibold">
45-
Start
77+
<button
78+
className="text-blue-600 bg-white text-xl px-8 h-14 rounded-full font-semibold"
79+
onClick={toggleTimer}
80+
>
81+
{state.matches(timerStates.STOPPED) ? 'Start' : 'Stop'}
4682
</button>
4783
</div>
4884
</div>

0 commit comments

Comments
 (0)