Skip to content

Commit d0940bb

Browse files
committed
Add tests for pix_n_flix
1 parent 72e6c6a commit d0940bb

File tree

10 files changed

+310
-29
lines changed

10 files changed

+310
-29
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
}
1818
}
1919
],
20-
"eslint.validate": [
20+
"eslint.validate": [
2121
"github-actions-workflow",
2222
"javascript",
2323
"javascriptreact",

src/bundles/binary_tree/src/__tests__/index.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import { describe, expect, it } from 'vitest';
22
import * as funcs from '../functions';
3+
import { list } from 'js-slang/dist/stdlib/list';
34

45
describe(funcs.is_tree, () => {
56
it('returns false when argument is not a tree', () => {
67
const arg = [0, [0, null]];
78
expect(funcs.is_tree(arg)).toEqual(false);
89
});
910

11+
it('returns false when argument is a list of 4 elements', () => {
12+
const arg = list(0, funcs.make_empty_tree(), funcs.make_empty_tree(), funcs.make_empty_tree());
13+
expect(funcs.is_tree(arg)).toEqual(false);
14+
});
15+
16+
it('returns false when the branches are not trees', () => {
17+
const not_tree = list(0, 1, 2);
18+
expect(funcs.is_tree(not_tree)).toEqual(false);
19+
20+
const also_not_tree = list(1, not_tree, null);
21+
expect(funcs.is_tree(also_not_tree)).toEqual(false);
22+
});
23+
1024
it('returns true when argument is a tree (simple)', () => {
1125
const arg = [0, [null, [null, null]]];
1226
expect(funcs.is_tree(arg)).toEqual(true);

src/bundles/binary_tree/src/functions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function make_tree(value: any, left: BinaryTree, right: BinaryTree): Bina
4040
* @param value Value to be tested
4141
*/
4242
export function is_tree(value: any): value is BinaryTree {
43+
// TODO: value parameter should be of type unknown
4344
if (!is_list(value)) return false;
4445

4546
if (is_empty_tree(value)) return true;

src/bundles/pix_n_flix/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
"private": true,
55
"devDependencies": {
66
"@sourceacademy/modules-buildtools": "workspace:^",
7-
"typescript": "^5.8.2"
7+
"@types/react": "^19",
8+
"@vitest/browser": "^3.2.3",
9+
"playwright": "^1.54.1",
10+
"react": "^18.3.1",
11+
"react-dom": "^18.3.1",
12+
"typescript": "^5.8.2",
13+
"vitest": "^3.2.3",
14+
"vitest-browser-react": "^1.0.0"
815
},
916
"type": "module",
1017
"exports": {
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { afterEach, beforeEach, describe, expect, it, test as baseTest, vi } from 'vitest';
2+
import { cleanup, render, type RenderResult } from 'vitest-browser-react';
3+
import * as funcs from '../functions';
4+
import type { Pixel, VideoElement } from '../types';
5+
6+
interface Fixtures {
7+
canvas: HTMLCanvasElement;
8+
image: HTMLImageElement;
9+
video: VideoElement;
10+
reinit: () => ReturnType<ReturnType<typeof funcs.start>['init']>;
11+
errLogger: () => void;
12+
}
13+
14+
const test = baseTest.extend<{
15+
screen: RenderResult;
16+
fixtures: Fixtures;
17+
}>({
18+
screen: async ({}, fixture) => {
19+
await fixture(
20+
render(<div>
21+
<canvas title="canvas" />
22+
<video title="vid" />
23+
<img title='img' />
24+
</div>)
25+
);
26+
cleanup();
27+
},
28+
fixtures: async ({ screen }, fixture) => {
29+
const { init, deinit } = funcs.start();
30+
const errLogger = vi.fn();
31+
const canvas = screen
32+
.getByTitle('canvas')
33+
.element() as HTMLCanvasElement;
34+
35+
const image = screen
36+
.getByTitle('img')
37+
.element() as HTMLImageElement;
38+
39+
const video = screen
40+
.getByTitle('vid')
41+
.element() as VideoElement;
42+
43+
const reinit = () => init(image, video, canvas, errLogger, { onClickStill: () => {} });
44+
reinit();
45+
await fixture({
46+
image,
47+
canvas,
48+
video,
49+
errLogger,
50+
reinit
51+
});
52+
deinit();
53+
}
54+
});
55+
56+
const height = funcs.image_height();
57+
const width = funcs.image_width();
58+
59+
beforeEach(() => {
60+
vi.useFakeTimers();
61+
});
62+
63+
afterEach(() => {
64+
vi.runOnlyPendingTimers();
65+
vi.useRealTimers();
66+
});
67+
68+
describe('pixel manipulation functions', () => {
69+
describe(funcs.alpha_of, () => {
70+
it('works', () => {
71+
expect(funcs.alpha_of([0, 0, 0, 255])).toEqual(255);
72+
});
73+
});
74+
75+
describe(funcs.red_of, () => {
76+
it('works', () => {
77+
expect(funcs.red_of([255, 0, 0, 0])).toEqual(255);
78+
});
79+
});
80+
81+
describe(funcs.green_of, () => {
82+
it('works', () => {
83+
expect(funcs.green_of([0, 255, 0, 0])).toEqual(255);
84+
});
85+
});
86+
87+
describe(funcs.blue_of, () => {
88+
it('works', () => {
89+
expect(funcs.blue_of([0, 0, 255, 0])).toEqual(255);
90+
});
91+
});
92+
93+
describe(funcs.set_rgba, () => {
94+
it('works', () => {
95+
const pixel: Pixel = [0, 0, 0, 0];
96+
expect(funcs.set_rgba(pixel, 1, 2, 3, 4)).toBeUndefined();
97+
for (let i = 0; i < 4; i++) {
98+
expect(pixel[i]).toEqual(i + 1);
99+
}
100+
});
101+
});
102+
});
103+
104+
describe(funcs.isPixelValid, () => {
105+
it('returns true if the pixel is valid', () => {
106+
const pixel: Pixel = [0, 1, 2, 3];
107+
expect(funcs.isPixelValid(pixel)).toEqual(true);
108+
for (let i = 0; i < 4; i++) {
109+
expect(pixel[i]).toEqual(i);
110+
}
111+
});
112+
113+
it('returns false and resets the pixel if it is invalid', () => {
114+
const pixel: Pixel = [-1, 0, 0, 0];
115+
expect(funcs.isPixelValid(pixel)).toEqual(false);
116+
for (let i = 0; i < 4; i++) {
117+
expect(pixel[i]).toEqual(0);
118+
}
119+
});
120+
});
121+
122+
describe(funcs.writeToBuffer, () => {
123+
test('with valid data', ({ fixtures: { errLogger } }) => {
124+
const img = funcs.new_image();
125+
funcs.set_rgba(img[0][0], 0, 1, 2, 3);
126+
127+
const imageData = new ImageData(width, height);
128+
const buffer = imageData.data;
129+
funcs.writeToBuffer(buffer, img);
130+
131+
expect(errLogger).not.toHaveBeenCalled();
132+
expect(buffer.length).toBeGreaterThan(0);
133+
134+
for (let i = 0; i < 4; i++) {
135+
expect(buffer[i]).toEqual(i);
136+
}
137+
});
138+
139+
test('with invalid data', ({ fixtures: { errLogger } }) => {
140+
const img = funcs.new_image();
141+
funcs.set_rgba(img[0][0], 999, 999, 999, 999);
142+
143+
const imageData = new ImageData(width, height);
144+
const buffer = imageData.data;
145+
funcs.writeToBuffer(buffer, img);
146+
147+
expect(errLogger).toHaveBeenCalled();
148+
expect(buffer.length).toBeGreaterThan(0);
149+
150+
for (let i = 0; i < 4; i++) {
151+
expect(buffer[i]).toEqual(0);
152+
}
153+
});
154+
});
155+
156+
describe('video functions', () => {
157+
test('startVideo and stopVideo', ({ fixtures: { errLogger } }) => {
158+
const filter = vi.fn(funcs.copy_image);
159+
funcs.install_filter(filter);
160+
funcs.startVideo();
161+
162+
for (let i = 0; i < 67; i++) {
163+
vi.advanceTimersToNextFrame();
164+
}
165+
166+
expect(filter).toHaveBeenCalledTimes(9);
167+
expect(errLogger).not.toHaveBeenCalled();
168+
169+
funcs.stopVideo();
170+
171+
for (let i = 0; i < 67; i++) {
172+
vi.advanceTimersToNextFrame();
173+
}
174+
175+
// Filter should not have been called again after stopVideo was called
176+
expect(filter).toHaveBeenCalledTimes(9);
177+
expect(errLogger).not.toHaveBeenCalled();
178+
});
179+
180+
// Test just doesn't work properly
181+
describe.skip(funcs.set_fps, () => {
182+
test('Setting FPS works', ({ fixtures: { reinit } }) => {
183+
expect(() => funcs.set_fps(20)).not.toThrow();
184+
const { FPS } = reinit();
185+
expect(FPS).toEqual(20);
186+
});
187+
188+
test('Lowest FPS is 1', ({ fixtures: { reinit } }) => {
189+
expect(() => funcs.set_fps(0)).not.toThrow();
190+
const { FPS } = reinit();
191+
expect(FPS).toEqual(1);
192+
});
193+
194+
test('Highest FPS is 60', ({ fixtures: { reinit } }) => {
195+
expect(() => funcs.set_fps(999)).not.toThrow();
196+
const { FPS } = reinit();
197+
expect(FPS).toEqual(60);
198+
});
199+
});
200+
});

src/bundles/pix_n_flix/src/functions.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,26 @@ let displayHeight: number = HEIGHT;
6666
// Module's Private Functions
6767
// =============================================================================
6868

69-
/** @hidden */
69+
/**
70+
* Creates a black image.
71+
*
72+
* @hidden
73+
*/
74+
export function new_image(): Pixels {
75+
const img: Pixels = [];
76+
for (let i = 0; i < HEIGHT; i += 1) {
77+
img[i] = [];
78+
for (let j = 0; j < WIDTH; j += 1) {
79+
img[i][j] = [0, 0, 0, 255];
80+
}
81+
}
82+
return img;
83+
}
84+
85+
/**
86+
* Setup the pixel arrays
87+
* @hidden
88+
*/
7089
function setupData(): void {
7190
for (let i = 0; i < HEIGHT; i += 1) {
7291
pixels[i] = [];
@@ -78,8 +97,14 @@ function setupData(): void {
7897
}
7998
}
8099

81-
/** @hidden */
82-
function isPixelFilled(pixel: Pixel): boolean {
100+
/**
101+
* Determines whether the r,g,b and a values for the pixel
102+
* are valid (i.e between 0 and 255). If that value is out of range,
103+
* then that value gets set to 0.
104+
* Exported for testing.
105+
* @hidden
106+
*/
107+
export function isPixelValid(pixel: Pixel): boolean {
83108
let ok = true;
84109
for (let i = 0; i < 4; i += 1) {
85110
if (pixel[i] >= 0 && pixel[i] <= 255) {
@@ -91,14 +116,19 @@ function isPixelFilled(pixel: Pixel): boolean {
91116
return ok;
92117
}
93118

94-
/** @hidden */
95-
function writeToBuffer(buffer: Uint8ClampedArray, data: Pixels) {
119+
/**
120+
* Write the provided pixel data to the buffer, performing error checking
121+
* and resetting invalid pixels.
122+
* Exported for testing.
123+
* @hidden
124+
*/
125+
export function writeToBuffer(buffer: Uint8ClampedArray, data: Pixels) {
96126
let ok: boolean = true;
97127

98128
for (let i = 0; i < HEIGHT; i += 1) {
99129
for (let j = 0; j < WIDTH; j += 1) {
100130
const p = i * WIDTH * 4 + j * 4;
101-
if (isPixelFilled(data[i][j]) === false) {
131+
if (!isPixelValid(data[i][j])) {
102132
ok = false;
103133
}
104134
buffer[p] = data[i][j][0];
@@ -116,7 +146,10 @@ function writeToBuffer(buffer: Uint8ClampedArray, data: Pixels) {
116146
}
117147
}
118148

119-
/** @hidden */
149+
/**
150+
* Retrieve pixel data from the buffer and convert it to the 2D array format.
151+
* @hidden
152+
*/
120153
function readFromBuffer(pixelData: Uint8ClampedArray, src: Pixels) {
121154
for (let i = 0; i < HEIGHT; i += 1) {
122155
for (let j = 0; j < WIDTH; j += 1) {
@@ -217,7 +250,7 @@ function pauseVideoElement() {
217250
}
218251

219252
/** @hidden */
220-
function startVideo(): void {
253+
export function startVideo(): void {
221254
if (videoIsPlaying) return;
222255
if (inputFeed === InputFeed.Camera) videoIsPlaying = true;
223256
else playVideoElement();
@@ -229,7 +262,7 @@ function startVideo(): void {
229262
*
230263
* @hidden
231264
*/
232-
function stopVideo(): void {
265+
export function stopVideo(): void {
233266
if (!videoIsPlaying) return;
234267
if (inputFeed === InputFeed.Camera) videoIsPlaying = false;
235268
else pauseVideoElement();
@@ -459,6 +492,7 @@ function init(
459492
*/
460493
function deinit(): void {
461494
stopVideo();
495+
queue = () => {};
462496
const stream = videoElement.srcObject;
463497
if (!stream) {
464498
return;
@@ -614,22 +648,6 @@ export function reset_filter(): void {
614648
install_filter(copy_image);
615649
}
616650

617-
/**
618-
* Creates a black image.
619-
*
620-
* @hidden
621-
*/
622-
function new_image(): Pixels {
623-
const img: Pixels = [];
624-
for (let i = 0; i < HEIGHT; i += 1) {
625-
img[i] = [];
626-
for (let j = 0; j < WIDTH; j += 1) {
627-
img[i][j] = [0, 0, 0, 255];
628-
}
629-
}
630-
return img;
631-
}
632-
633651
/**
634652
* Returns a new filter that is equivalent to applying
635653
* filter1 and then filter2.
@@ -662,7 +680,7 @@ export function pause_at(pause_time: number): void {
662680
}
663681

664682
/**
665-
* Sets the diemsions of the displayed images.
683+
* Sets the dimensions of the displayed images.
666684
* Note: Only accepts width and height values within the range of 1 to 500.
667685
*
668686
* @param width The width of the displayed images (default value: 300)

0 commit comments

Comments
 (0)