Skip to content

Commit c0dddea

Browse files
committed
Addes tests for modules-lib components
1 parent 5ce8c97 commit c0dddea

File tree

15 files changed

+359
-166
lines changed

15 files changed

+359
-166
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ coverage/
4848
.Trashes
4949
ehthumbs.db
5050
Thumbs.db
51+
52+
**/__tests__/**/__screenshots__

eslint.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ export default tseslint.config(
294294
'**/vitest.*.ts'
295295
],
296296
rules: {
297+
'no-empty-pattern': 'off', // vitest requires certain things to be destructured using an object pattern
298+
297299
'@stylistic/quotes': 'off', // Turn this off to avoid conflicting with snapshots
298300

299301
'vitest/expect-expect': ['error', {

lib/modules-lib/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
"private": true,
55
"type": "module",
66
"devDependencies": {
7-
"@testing-library/react": "^16.3.0",
8-
"@testing-library/user-event": "^14.6.1",
97
"@types/react": "^18.3.1",
108
"@types/react-dom": "^18.3.1",
119
"@vitest/browser": "^3.2.3",
@@ -44,7 +42,7 @@
4442
"build": "tsc --project ./tsconfig.prod.json",
4543
"docs": "typedoc",
4644
"lint": "eslint src",
47-
"prepare": "yarn build",
45+
"prepare": "yarn build && playwright install --with-deps",
4846
"tsc": "tsc --project ./tsconfig.json",
4947
"test": "vitest --project \"Modules Library\""
5048
}

lib/modules-lib/src/tabs/AnimationCanvas.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function AnimationCanvas(props: AnimCanvasProps) {
2828
Math.round(props.animation.duration * 1000)
2929
], [props.animation]);
3030

31-
const { stop, start, reset, changeTimestamp, isPlaying, timestamp, canvasRef } = useAnimation({
31+
const { stop, start, reset, changeTimestamp, isPlaying, timestamp, setCanvas } = useAnimation({
3232
frameDuration,
3333
animationDuration,
3434
autoLoop: isAutoLooping,
@@ -60,11 +60,10 @@ export default function AnimationCanvas(props: AnimCanvasProps) {
6060
<PlayButton
6161
title='PlayButton'
6262
isPlaying={isPlaying}
63-
disabled={Boolean(errored)}
6463
onClick={() => {
6564
if (isPlaying) stop();
6665
else {
67-
if (timestamp >= animationDuration) reset();
66+
if (errored || timestamp >= animationDuration) reset();
6867
start();
6968
}
7069
}}
@@ -164,7 +163,11 @@ export default function AnimationCanvas(props: AnimCanvasProps) {
164163
style={{
165164
flexGrow: 1
166165
}}
167-
ref={canvasRef}
166+
ref={element => {
167+
if (element !== null) {
168+
setCanvas(element);
169+
}
170+
}}
168171
/>
169172
)}
170173
</div>

lib/modules-lib/src/tabs/MultiItemDisplay/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function MultiItemDisplay(props: MultiItemDisplayProps) {
6262
onClick={() => {
6363
changeStep(currentStep - 1);
6464
setStepEditorValue(currentStep.toString());
65-
} }
65+
}}
6666
disabled={currentStep === 0}
6767
>
6868
Previous
@@ -85,7 +85,7 @@ export default function MultiItemDisplay(props: MultiItemDisplayProps) {
8585
placeholder={undefined}
8686
selectAllOnFocus
8787
customInputAttributes={{
88-
tabIndex: 0
88+
tabIndex: 0,
8989
}}
9090
onChange={(newValue) => {
9191
// Disallow non numeric inputs
Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,88 @@
1-
import { cleanup, render } from "@testing-library/react";
2-
import userEvent from '@testing-library/user-event';
3-
import { afterAll, describe, expect, test, vi } from "vitest";
1+
import { afterEach, beforeEach, expect, test, vi } from "vitest";
2+
import { cleanup, render } from 'vitest-browser-react';
43
import type { glAnimation } from "../../types";
54
import AnimationCanvas from "../AnimationCanvas";
65

7-
const testFrame = vi.fn();
8-
9-
const testAnimation: glAnimation = {
10-
duration: 1,
11-
fps: 10,
12-
getFrame(timestamp) {
13-
return {
14-
draw: canvas => {
15-
testFrame(timestamp);
16-
const context = canvas.getContext("2d");
17-
context!.fillStyle = 'green';
18-
context?.fillRect(10, 10, 100, 100);
19-
}
20-
};
21-
},
22-
};
23-
24-
const user = userEvent.setup();
6+
beforeEach(() => {
7+
vi.useFakeTimers();
8+
});
259

26-
afterAll(() => {
27-
// cleanup();
10+
afterEach(() => {
11+
vi.runOnlyPendingTimers();
12+
vi.useRealTimers();
13+
cleanup();
2814
});
2915

30-
test('The animation is played entirely', async () => {
16+
test('Animations are played entirely and stops', async () => {
17+
const testFrame = vi.fn();
18+
19+
const testAnimation: glAnimation = {
20+
duration: 1,
21+
fps: 10,
22+
getFrame(timestamp) {
23+
return {
24+
draw: () => {
25+
testFrame(timestamp);
26+
// const context = canvas.getContext("2d");
27+
// context!.fillStyle = 'green';
28+
// context?.fillRect(10, 10, 100, 100);
29+
}
30+
};
31+
},
32+
};
3133
const component = render(<AnimationCanvas animation={testAnimation} />);
3234

3335
const playButton = component.getByTitle('PlayButton');
3436
const autoLoopSwitch = component.getByRole('checkbox');
3537

36-
await user.click(autoLoopSwitch);
37-
await user.click(playButton);
38+
await autoLoopSwitch.click();
39+
await playButton.click();
3840

39-
await new Promise<void>(resolve => setTimeout(resolve, 1100));
41+
for (let i = 0; i < 160; i++) {
42+
vi.advanceTimersToNextFrame();
43+
}
4044

41-
// Because of the variability in requestAnimationFrame, we may not draw
42-
// 100% of frames, or we might end up calling the frame draw extra times?
43-
const { calls } = testFrame.mock;
44-
expect(calls.length).toBeGreaterThanOrEqual(11);
45+
await expect.poll(() => testFrame).toHaveBeenCalledTimes(10);
4546

4647
// call 0 occurs when the first frame is drawn to the canvas
4748
// to fill the canvas when the animation hasn't started playing
49+
const { calls } = testFrame.mock;
4850
expect(calls[0][0]).toEqual(0);
49-
50-
// so every other call subsequently should start
51-
// from 0 again
52-
for (let i = 0; i < 10; i++) {
53-
const diff = Math.abs(i / 10 - calls[i+1][0]);
54-
expect(diff).toBeLessThan(0.1);
55-
}
51+
expect(calls[calls.length-1][0]).toEqual(1);
5652
});
5753

58-
describe.skip('Test error handling', () => {
54+
test('Gracefully handles animations that error halfway through and can be restarted', async () => {
55+
const testFrame = vi.fn((timestamp: number) => {
56+
return {
57+
draw: () => {
58+
if (timestamp > 0.5) throw new Error('Oh no!');
59+
}
60+
};
61+
});
62+
5963
const errorAnimation: glAnimation = {
6064
duration: 1,
6165
fps: 10,
62-
getFrame(timestamp) {
63-
return {
64-
draw: () => {
65-
if (timestamp > 0.5) throw new Error('Oh no!');
66-
}
67-
};
68-
}
66+
getFrame: testFrame
6967
};
7068

7169
const component = render(<AnimationCanvas animation={errorAnimation} />);
7270
const playButton = component.getByTitle('PlayButton');
71+
await playButton.click();
72+
73+
for (let i = 0; i < 160; i++) {
74+
vi.advanceTimersToNextFrame();
75+
}
7376

74-
test('Gracefully handles animations that error halfway through', async () => {
75-
await userEvent.click(playButton);
76-
await new Promise<void>(resolve => setTimeout(resolve, 600));
77+
await expect.element(component.getByText('Error: Oh no!')).toBeVisible();
78+
await expect.poll(() => testFrame).toHaveBeenCalledTimes(6);
7779

78-
const errorTextElement = component.getByText('Error: Oh no!');
79-
expect(errorTextElement).not.toBeUndefined();
80-
});
80+
await playButton.click();
81+
for (let i = 0; i < 160; i++) {
82+
vi.advanceTimersToNextFrame();
83+
}
8184

82-
test('and the animation can be restarted from there', async () => {
83-
await userEvent.click(playButton);
84-
await new Promise<void>(resolve => setTimeout(resolve, 600));
85-
const errorTextElement = component.getByText('Error: Oh no!');
86-
expect(errorTextElement).not.toBeUndefined();
87-
});
85+
// Total is 6 + 6
86+
await expect.poll(() => testFrame).toHaveBeenCalledTimes(12);
87+
await expect.element(component.getByText('Error: Oh no!')).toBeVisible();
8888
});
Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
/* eslint-disable no-empty-pattern */
2-
import { cleanup, render } from '@testing-library/react';
3-
import userEvent from '@testing-library/user-event';
1+
import { userEvent } from "@vitest/browser/context";
42
import { afterEach, beforeEach, expect, test as baseTest, vi } from "vitest";
3+
import { cleanup, render } from "vitest-browser-react";
54
import MultiItemDisplay from "../MultiItemDisplay";
65

76
const items = [
@@ -22,7 +21,6 @@ afterEach(() => {
2221
});
2322

2423
// Reference for keyboard events: https://testing-library.com/docs/user-event/keyboard/
25-
const user = userEvent.setup();
2624

2725
const test = baseTest.extend<{
2826
component: ReturnType<typeof render>
@@ -32,88 +30,92 @@ const test = baseTest.extend<{
3230

3331
test('Changing the currently selected value using the keyboard', async ({ component }) => {
3432
// Check that the first item is being displayed
35-
expect(component.queryByText('Item One')).not.toBeNull();
33+
await expect.element(component.getByText('Item One')).toBeVisible();
3634

37-
await user.tab();
38-
await user.keyboard('2[Enter]');
35+
await userEvent.keyboard('[Tab]2[Enter]');
3936

4037
// Check that the first item is no longer being displayed
41-
expect(component.queryByText('Item One')).toBeNull();
38+
await expect.element(component.getByText('Item One')).not.toBeInTheDocument();
4239

4340
// Check that the second item is being displayed
44-
expect(component.queryByText('Item Two')).not.toBeNull();
41+
await expect.element(component.getByText('Item Two')).toBeVisible();
4542

4643
// and that the step change handler was called
4744
expect(stepChangeHandler).toHaveBeenCalledTimes(1);
4845
});
4946

5047
test('Changing the currently selected value via the buttons', async ({ component }) => {
5148
// Check that the first item is being displayed
52-
expect(component.queryByText('Item One')).not.toBeNull();
49+
await expect.element(component.getByText('Item One')).toBeVisible();
5350

5451
// Click the "Next" button
5552
const nextButton = component.getByText('Next');
56-
await user.click(nextButton);
53+
await userEvent.click(nextButton);
5754

5855
// Check that the second item is being displayed and not the first
59-
expect(component.queryByText('Item One')).toBeNull();
60-
expect(component.queryByText('Item Two')).not.toBeNull();
56+
await expect.element(component.getByText('Item One')).not.toBeInTheDocument();
57+
await expect.element(component.getByText('Item Two')).toBeVisible();
6158

6259
// Click the "Previous" button
6360
const prevButton = component.getByText('Previous');
64-
await user.click(prevButton);
61+
await userEvent.click(prevButton);
6562

6663
// Check that the first item is being displayed and not the second
67-
expect(component.queryByText('Item Two')).toBeNull();
68-
expect(component.queryByText('Item One')).not.toBeNull();
64+
await expect.element(component.getByText('Item One')).toBeVisible();
65+
await expect.element(component.getByText('Item Two')).not.toBeInTheDocument();
6966

7067
expect(stepChangeHandler).toHaveBeenCalledTimes(2);
7168
});
7269

7370
test('The item selector won\'t accept the too many digits', async ({ component }) => {
7471
// Check that the first item was rendered
75-
expect(component.queryByText('Item One')).not.toBeNull();
72+
await expect.element(component.getByText('Item One')).toBeVisible();
7673

7774
// Try to enter an invalid value
78-
await userEvent.tab();
79-
await userEvent.keyboard('1234[Enter]');
75+
await userEvent.keyboard('[Tab]1234[Enter]');
8076

8177
// Check that the first item is still rendered
82-
expect(component.queryByText('Item One')).not.toBeNull();
78+
await expect.element(component.getByText('Item One')).toBeVisible();
8379

8480
// and that the display didn't change
8581
expect(stepChangeHandler).not.toHaveBeenCalled();
8682
});
8783

8884
test('The item selector will reset when all digits are removed', async ({ component }) => {
8985
// Check that the first item was rendered
90-
expect(component.queryByText('Item One')).not.toBeNull();
86+
await expect.element(component.getByText('Item One')).toBeVisible();
9187

9288
// Clear the selector
93-
await userEvent.tab();
94-
await userEvent.keyboard('[Backspace][Enter]');
89+
await userEvent.keyboard('[Tab][Backspace][Enter]');
9590

9691
// Check that the first item is still rendered
97-
expect(component.queryByText('Item One')).not.toBeNull();
92+
await expect.element(component.getByText('Item One')).toBeVisible();
9893

9994
// and that the display didn't change
10095
expect(stepChangeHandler).not.toHaveBeenCalled();
10196
});
10297

10398
test('The item selector moves to the end when set out of range', async ({ component }) => {
10499
// Check that the first item is rendered
105-
expect(component.queryByText('Item One')).not.toBeNull();
100+
await expect.element(component.getByText('Item One')).toBeVisible();
106101

107-
await userEvent.tab();
108-
await userEvent.keyboard('5[Enter]');
102+
await userEvent.keyboard('[Tab]5[Enter]');
109103

110104
// Check that the first item is no longer rendered
111-
expect(component.queryByText('Item One')).toBeNull();
105+
await expect.element(component.getByText('Item One')).not.toBeInTheDocument();
112106

113107
// Check that the fourth item is now rendered
114-
expect(component.queryByText('Item Four')).not.toBeNull();
108+
await expect.element(component.getByText('Item Four')).toBeVisible();
115109

116110
// and check that the step change only happened once
117111
expect(stepChangeHandler).toHaveBeenCalledTimes(1);
118112
expect(stepChangeHandler).toHaveBeenCalledWith(3, 0);
119113
});
114+
115+
test('Both selector buttons are disabled when there is only one item', async () => {
116+
const component = render(<MultiItemDisplay elements={[<p>Item One</p>]}/>);
117+
const buttonLocator = component.getByRole('button');
118+
119+
await expect.element(buttonLocator.first()).toBeDisabled();
120+
await expect.element(buttonLocator.nth(1)).toBeDisabled();
121+
});

lib/modules-lib/src/tabs/__tests__/__snapshots__/AnimationCanvas.test.tsx.snap

Lines changed: 0 additions & 13 deletions
This file was deleted.

lib/modules-lib/src/tabs/__tests__/__snapshots__/MultiItem.test.tsx.snap

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)