-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor: Convert Countdown & Timer to TypeScript (#1137)
* refactor: Convert CountDown component to TypeScript * refactor: Improve CountDown component & timer utility * story: Add CountDown component story * test: Add unit tests for Timer utility * type: Add better typing for timer util * test: Add unit tests for CountDown component and Timer utility * fix(lint): Fix linting issues * chore: Remove console.log
- Loading branch information
1 parent
2983d7a
commit 92a5e65
Showing
5 changed files
with
303 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { render, screen, waitFor } from "@testing-library/react"; | ||
import { beforeEach, describe, it, expect, vi, Mock } from "vitest"; | ||
import CountDown from "./CountDown"; | ||
import Timer from "@/util/timer"; | ||
|
||
// Mock the Timer utility | ||
vi.mock("@/util/timer", () => ({ | ||
__esModule: true, | ||
default: vi.fn(), | ||
})); | ||
|
||
const MockedTimer = Timer as Mock; | ||
|
||
describe("CountDown", () => { | ||
beforeEach(() => { | ||
// Clear all mocks before each test | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
it("should display the initial countdown value", () => { | ||
render(<CountDown duration={10} />); | ||
|
||
const score = screen.getByText("10"); | ||
expect(document.body.contains(score)).toBe(true); | ||
}); | ||
|
||
it("should start the timer when running is true", async () => { | ||
const mockTimer = vi.fn(); | ||
MockedTimer.mockImplementation(mockTimer); | ||
|
||
render(<CountDown duration={10} running={true} />); | ||
|
||
expect(mockTimer).toHaveBeenCalled(); | ||
}); | ||
|
||
it("should not start the timer when running is false", () => { | ||
const mockTimer = vi.fn(); | ||
MockedTimer.mockImplementation(mockTimer); | ||
|
||
render(<CountDown duration={10} running={false} />); | ||
|
||
expect(mockTimer).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("should update the countdown value as the timer progresses", async () => { | ||
let onTickCallback: (time: number) => void; | ||
MockedTimer.mockImplementation(({ onTick }) => { | ||
onTickCallback = onTick; | ||
return vi.fn(); | ||
}); | ||
|
||
render(<CountDown duration={10} running={true} />); | ||
|
||
await waitFor(() => { | ||
const score = screen.getByText("10"); | ||
expect(document.body.contains(score)).toBe(true); | ||
}); | ||
|
||
// Simulate timer ticks | ||
onTickCallback!(5); | ||
await waitFor(() => { | ||
const score = screen.getByText("5"); | ||
expect(document.body.contains(score)).toBe(true); | ||
}); | ||
|
||
onTickCallback!(9.5); | ||
await waitFor(() => { | ||
const score = screen.getByText("1"); | ||
expect(document.body.contains(score)).toBe(true); | ||
}); | ||
}); | ||
|
||
it("should display 0 when the timer finishes", async () => { | ||
let onFinishCallback: () => void; | ||
MockedTimer.mockImplementation(({ onFinish }) => { | ||
onFinishCallback = onFinish; | ||
return vi.fn(); | ||
}); | ||
|
||
render(<CountDown duration={10} running={true} />); | ||
|
||
await waitFor(() => { | ||
const score = screen.getByText("10"); | ||
expect(document.body.contains(score)).toBe(true); | ||
}); | ||
|
||
// Simulate timer finish | ||
onFinishCallback!(); | ||
await waitFor(() => { | ||
const score = screen.getByText("0"); | ||
expect(document.body.contains(score)).toBe(true); | ||
}); | ||
}); | ||
|
||
it("should apply the correct classes based on the state", async () => { | ||
const firstContainer = render(<CountDown duration={3} running={true} />); | ||
let firstHeading = firstContainer.getByText("3"); | ||
|
||
MockedTimer.mockImplementation(({ onFinish }) => { | ||
onFinish(); | ||
return vi.fn(); | ||
}); | ||
|
||
const secondContainer = render(<CountDown duration={5} running={true} />); | ||
|
||
const secondHeading = secondContainer.getByText("0"); | ||
|
||
// First countdown should be active and not zero | ||
expect(firstHeading.classList.contains('aha__count-down')).toBe(true); | ||
expect(firstHeading.classList.contains('active')).toBe(true); | ||
expect(firstHeading.classList.contains('zero')).toBe(false); | ||
|
||
// Second countdown should be active and zero | ||
expect(secondHeading.classList.contains('zero')).toBe(true); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import CountDown from "../components/CountDown/CountDown"; | ||
|
||
export default { | ||
title: "CountDown", | ||
component: CountDown, | ||
parameters: { | ||
layout: "fullscreen", | ||
}, | ||
}; | ||
|
||
export const Default = { | ||
args: { | ||
duration: 3, | ||
running: true, | ||
}, | ||
decorators: [ | ||
(Story) => ( | ||
<div | ||
style={{ width: "100%", height: "100%", backgroundColor: "#666", padding: "1rem" }} | ||
> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; | ||
import Timer from './timer'; | ||
|
||
describe('Timer', () => { | ||
let originalRequestAnimationFrame: any; | ||
let now = 0; | ||
|
||
beforeEach(() => { | ||
// Use fake timers | ||
vi.useFakeTimers(); | ||
now = 0; | ||
vi.spyOn(performance, 'now').mockImplementation(() => now); | ||
|
||
// Mock requestAnimationFrame | ||
originalRequestAnimationFrame = window.requestAnimationFrame; | ||
window.requestAnimationFrame = (cb) => { | ||
setTimeout(() => { | ||
now += 1000 / 60; // Simulate 60fps | ||
cb(performance.now()); | ||
}, 1000 / 60); | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
// Clear all timers and mocks | ||
vi.clearAllTimers(); | ||
vi.restoreAllMocks(); | ||
window.requestAnimationFrame = originalRequestAnimationFrame; | ||
}); | ||
|
||
it('should call onTick at intervals', () => { | ||
const onTick = vi.fn(); | ||
const duration = 2; | ||
const interval = 0.5; | ||
const stop = Timer({ | ||
duration, | ||
onTick, | ||
interval, | ||
}); | ||
// The number of ticks should be ceil(duration / interval) - 1 | ||
// as there is no tick when it finishes | ||
const expectedTicks = Math.ceil(duration / interval) - 1; | ||
|
||
// Advance the timer by 3 seconds | ||
vi.advanceTimersByTime(3000); | ||
|
||
// Check that onTick was called the correct number of times | ||
expect(onTick).toHaveBeenCalledTimes(expectedTicks); | ||
|
||
stop(); | ||
}); | ||
|
||
it('should call onFinish when the duration is reached', () => { | ||
const onFinish = vi.fn(); | ||
const duration = 1; | ||
const stop = Timer({ | ||
duration, | ||
onFinish, | ||
}); | ||
|
||
// Advance the timer by 2 seconds | ||
vi.advanceTimersByTime(2000); | ||
|
||
// Check that onFinish was called once | ||
expect(onFinish).toHaveBeenCalledTimes(1); | ||
|
||
stop(); | ||
}); | ||
|
||
it('should stop the timer when stop function is called', () => { | ||
const onTick = vi.fn(); | ||
const duration = 2; | ||
const interval = 0.5; | ||
const stop = Timer({ | ||
duration, | ||
onTick, | ||
interval, | ||
}); | ||
|
||
// Advance the timer by 1 second | ||
vi.advanceTimersByTime(1000); | ||
|
||
// Stop the timer | ||
stop(); | ||
|
||
// Advance the timer by another 1 second | ||
vi.advanceTimersByTime(1000); | ||
|
||
// Check that onTick was called only for the first second | ||
expect(onTick).toHaveBeenCalledTimes(2); // Should be called twice before stopping | ||
|
||
stop(); | ||
}); | ||
|
||
it('should correctly handle the initial time parameter', () => { | ||
const onTick = vi.fn(); | ||
const onFinish = vi.fn(); | ||
const duration = 3; | ||
const interval = 0.5; | ||
const time = 1; | ||
const stop = Timer({ | ||
time, | ||
duration, | ||
onFinish, | ||
onTick, | ||
interval, | ||
}); | ||
|
||
// Advance the timer by 500ms | ||
vi.advanceTimersByTime(500); | ||
|
||
expect(onFinish).not.toHaveBeenCalled(); // Should not have finished yet | ||
expect(onTick).toHaveBeenCalledTimes(1); // Should have ticked once | ||
|
||
// Advance the timer by another 500ms | ||
vi.advanceTimersByTime(500); | ||
|
||
expect(onFinish).not.toHaveBeenCalled(); // Should not have finished yet | ||
expect(onTick).toHaveBeenCalledTimes(2); // Should have ticked twice | ||
|
||
// Advance the timer by another 500ms | ||
vi.advanceTimersByTime(500); | ||
|
||
expect(onFinish).not.toHaveBeenCalled(); // Should not have finished yet | ||
expect(onTick).toHaveBeenCalledTimes(3); // Should have ticked twice | ||
|
||
// Advance the timer by another 500ms | ||
vi.advanceTimersByTime(500); | ||
|
||
// Check that onFinish was called once | ||
expect(onFinish).toHaveBeenCalledTimes(1); | ||
expect(onTick).toHaveBeenCalledTimes(3); // No extra tick after finishing | ||
|
||
stop(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters