Skip to content

Commit

Permalink
chore: 100% coverage on TimerProvider.jsx
Browse files Browse the repository at this point in the history
  • Loading branch information
rijuma committed Feb 29, 2024
1 parent 118b52c commit c0206a9
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 31 deletions.
3 changes: 2 additions & 1 deletion src/timer/TimerProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const TimerProvider = ({
const processTimeLeft = useCallback((secondsLeft) => {
const emit = (signal) => {
// This prevents spamming
if (lastSignal.current === lastSignal) {
if (lastSignal.current === signal) {
return;
}
Emitter.emit(signal);
Expand Down Expand Up @@ -108,6 +108,7 @@ const TimerProvider = ({

setTimeState(getFormattedRemainingTime(secondsLeft));
// no polling during grace period

if (timerTick % POLL_INTERVAL === 0 && secondsLeft >= 0) {
pollExam();
}
Expand Down
113 changes: 83 additions & 30 deletions src/timer/TimerProvider.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ const TestComponent = () => (
</TimerProvider>
);

const renderComponent = ({ remainingSeconds, timeLimitMins = 2 }) => {
const renderComponent = ({ remainingSeconds, timeLimitMins = 2, pingIntervalSeconds = undefined }) => {
const store = initializeTestStore({
specialExams: {
activeAttempt: appendTimerEnd({
time_remaining_seconds: remainingSeconds,
exam_started_poll_url: 'https://some-poll.endpoint',
desktop_application_js_url: 'https://desktop-application.js?url=42',
ping_interval: 10,
ping_interval: pingIntervalSeconds,
}),
exam: {
time_limit_mins: timeLimitMins,
Expand All @@ -67,12 +67,12 @@ describe('TimerProvider', () => {
let now = testRefDate;

// This syncs up the reference date returned by Date.now() and the jest timers.
const advanceTime = (ms) => {
now += ms;
jest.advanceTimersToNextTimer();
const awaitSeconds = async (seconds = 1) => {
now += 1000 * seconds;
jest.advanceTimersToNextTimer(seconds); // Proc any remaining call.
};

beforeAll(() => jest.useFakeTimers());
beforeAll(() => jest.useFakeTimers('modern'));

beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => now);
Expand All @@ -89,6 +89,8 @@ describe('TimerProvider', () => {
it('should render normally', async () => {
const unmount = renderComponent({ remainingSeconds: 60 });
await act(async () => {
// Since the first update is delayed untill the children are rendered, we need to
// wait on it to validate the update.
await waitFor(() => expect(screen.getByTestId('time-string')).toBeInTheDocument());
});
expect(screen.getByTestId('time-string')).toHaveTextContent('00:01:00');
Expand All @@ -99,7 +101,7 @@ describe('TimerProvider', () => {
}));

await act(async () => {
advanceTime(1000);
awaitSeconds(1);
await waitFor(() => expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:59'));
});

Expand All @@ -111,17 +113,17 @@ describe('TimerProvider', () => {

expect(Emitter.emit).not.toHaveBeenCalled();

// No Poll calls in between
// No Poll calls in between.
expect(pollAttempt).toHaveBeenCalledTimes(1);

// No Ping attempts
// No Ping attempts.
expect(pingAttempt).not.toHaveBeenCalled();

unmount(); // Cleanup
unmount(); // Cleanup.
});
});

describe('when the remaining falls under the warning time', () => {
describe('when the remaining falls under the warning times', () => {
it('should emit TIMER_IS_LOW when the timer falls under the threshold (40%)', async () => {
const unmount = renderComponent({ remainingSeconds: 25 });

Expand All @@ -135,18 +137,16 @@ describe('TimerProvider', () => {

// The next second should trigger the warning.
await act(async () => {
advanceTime(1000);
awaitSeconds(1);
await waitFor(() => expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:24'));
});

expect(Emitter.emit).toHaveBeenCalledTimes(1);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_IS_LOW);

unmount(); // Cleanup
unmount(); // Cleanup.
});
});

describe('when the remaining falls under the critical time', () => {
it('should emit TIMER_IS_LOW when the timer falls under the threshold (10%)', async () => {
const unmount = renderComponent({ remainingSeconds: 7 });

Expand All @@ -155,24 +155,22 @@ describe('TimerProvider', () => {
});
expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:07');

// Low timer warning is called first render
// Low timer warning is called first render.
expect(Emitter.emit).toHaveBeenCalledTimes(1);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_IS_LOW);

// The next second should trigger the critical warning.
await act(async () => {
advanceTime(1000);
awaitSeconds(1);
await waitFor(() => expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:06'));
});

expect(Emitter.emit).toHaveBeenCalledTimes(2);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_IS_CRITICALLY_LOW);

unmount(); // Cleanup
unmount(); // Cleanup.
});
});

describe('when the timer reaches zero and there is a grace period', () => {
it('should emit TIMER_REACHED_NULL when the timer falls under the threshold (10%)', async () => {
const unmount = renderComponent({ remainingSeconds: 1 });

Expand All @@ -181,24 +179,22 @@ describe('TimerProvider', () => {
});
expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:01');

// Critical timer warning is called first render
// Critical timer warning is called first render.
expect(Emitter.emit).toHaveBeenCalledTimes(1);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_IS_CRITICALLY_LOW);

// The next second should trigger the critical warning.
await act(async () => {
advanceTime(1000);
awaitSeconds(1);
await waitFor(() => expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:00'));
});

expect(Emitter.emit).toHaveBeenCalledTimes(2);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_REACHED_NULL);

unmount(); // Cleanup
unmount(); // Cleanup.
});
});

describe('when the grace period ends', () => {
it('should emit TIMER_LIMIT_REACHED when the timer falls under the grace period (5 secs)', async () => {
const unmount = renderComponent({ remainingSeconds: -4 });

Expand All @@ -207,13 +203,13 @@ describe('TimerProvider', () => {
});
expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:00');

// Timer is null is called first render
// Timer is null is called first render.
expect(Emitter.emit).toHaveBeenCalledTimes(1);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_REACHED_NULL);

// The next second should kill the exam.
await act(async () => {
advanceTime(1000);
awaitSeconds(1);
await waitFor(() => expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:00'));
});

Expand All @@ -222,15 +218,72 @@ describe('TimerProvider', () => {

// Lets just wait a couple more seconds and check that the timer was killed as well.
await act(async () => {
advanceTime(3000);
awaitSeconds(3);
await waitFor(() => expect(screen.getByTestId('time-string')).toHaveTextContent('00:00:00'));
});

// Emitter should be exactly as before
// Emitter should be exactly as before.
expect(Emitter.emit).toHaveBeenCalledTimes(2);
expect(Emitter.emit).toHaveBeenCalledWith(TIMER_LIMIT_REACHED);

unmount(); // Cleanup
unmount(); // Cleanup.
});
});

describe('when the poll interval is reached (1 minute)', () => {
it('should call poll attempt each time', async () => {
const unmount = renderComponent({ remainingSeconds: 120 });

await act(async () => {
await waitFor(() => expect(screen.getByTestId('time-string')).toBeInTheDocument());
});

// A first poll attempt on render.
expect(pollAttempt).toHaveBeenCalledTimes(1);

await act(async () => awaitSeconds(60));

// A 2nd poll attempt should fire.
expect(pollAttempt).toHaveBeenCalledTimes(2);

await act(async () => awaitSeconds(60));

// A 3rd one, just in case.
expect(pollAttempt).toHaveBeenCalledTimes(3);

unmount(); // Cleanup.
});
});

describe('when the ping interval is reached', () => {
it('should ping first at half the time, then the full delay onwards', async () => {
const unmount = renderComponent({ remainingSeconds: 120, timeLimitMins: 10, pingIntervalSeconds: 10 });

await act(async () => {
await waitFor(() => expect(screen.getByTestId('time-string')).toBeInTheDocument());
});

// No pings so far.
expect(pingAttempt).not.toHaveBeenCalled();

await act(async () => awaitSeconds(5));

// A ping poll attempt should fire.
expect(pingAttempt).toHaveBeenCalledTimes(1);

// Then one ping after 10.
await act(async () => awaitSeconds(10));

// A ping poll attempt should fire.
expect(pingAttempt).toHaveBeenCalledTimes(2);

// Let's round it up on 6..
await act(async () => awaitSeconds(40));

// A ping poll attempt should fire.
expect(pingAttempt).toHaveBeenCalledTimes(6);

unmount(); // Cleanup.
});
});
});

0 comments on commit c0206a9

Please sign in to comment.