Skip to content

Commit b3d1057

Browse files
Set up testing (#82)
* add jest test scripts * setup jest * Passing tests for BodyPix --------- Co-authored-by: Ziyuan Lin <[email protected]>
1 parent d94f432 commit b3d1057

File tree

11 files changed

+6483
-4220
lines changed

11 files changed

+6483
-4220
lines changed

.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["@babel/preset-env"]
3+
}

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ yarn run build
7171

7272
This will create a production version of the library in `/dist` directory.
7373

74+
75+
## Unit Tests
76+
77+
To run the unit tests, run the following command
78+
79+
```
80+
yarn test
81+
```
82+
7483
## Making Releases
7584

7685
_This section is a temporary guide for contributors who wants to make a alpha release manually._

babel.config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["@babel/preset-env"]
3+
}

jest.config.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* For a detailed explanation regarding each configuration property, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
/** @type {import('jest').Config} */
7+
const config = {
8+
collectCoverage: true,
9+
coverageDirectory: "coverage",
10+
coverageProvider: "v8",
11+
globalSetup: "./setupTests.js",
12+
passWithNoTests: true,
13+
testEnvironment: "jsdom",
14+
testEnvironmentOptions: {
15+
resources: "usable", // Load image resources
16+
},
17+
};
18+
19+
module.exports = config;

package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"build": "webpack --config webpack.config.js --mode production",
99
"start": "webpack serve --config webpack.config.js --mode development",
1010
"format": "prettier --write \"**/*.js\"",
11-
"postinstall": "patch-package"
11+
"postinstall": "patch-package",
12+
"test": "jest"
1213
},
1314
"files": [
1415
"dist"
@@ -26,8 +27,17 @@
2627
"url": "https://github.com/ml5js/ml5-next-gen/issues"
2728
},
2829
"devDependencies": {
30+
"@babel/core": "^7.23.2",
31+
"@babel/preset-env": "^7.23.2",
32+
"@tensorflow/tfjs-backend-webgpu": "^4.17.0",
33+
"@tensorflow/tfjs-node": "^4.17.0",
2934
"all-contributors-cli": "^6.26.1",
35+
"babel-jest": "^29.7.0",
36+
"canvas": "^2.11.2",
37+
"cross-fetch": "^4.0.0",
3038
"html-webpack-plugin": "^5.5.3",
39+
"jest": "^29.7.0",
40+
"jest-environment-jsdom": "^29.7.0",
3141
"patch-package": "^8.0.0",
3242
"postinstall-postinstall": "^2.1.0",
3343
"prettier": "2.8.8",
@@ -50,6 +60,7 @@
5060
"@tensorflow/tfjs": "^4.2.0",
5161
"@tensorflow/tfjs-vis": "^1.5.1",
5262
"axios": "^1.3.4",
63+
"canvas": "^2.11.2",
5364
"webpack-merge": "^5.9.0"
54-
}
65+
}
5566
}

setupTests.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const { ImageData } = require("canvas");
2+
import '@tensorflow/tfjs-node'; // loads the tensorflow/node backend to the registry
3+
import crossFetch from 'cross-fetch';
4+
import * as tf from '@tensorflow/tfjs';
5+
6+
async function setupTests() {
7+
8+
console.log("Beginning setup");
9+
10+
await tf.setBackend('tensorflow');
11+
tf.env().set('IS_BROWSER', false);
12+
tf.env().set('IS_NODE', true);
13+
14+
// Use the node-canvas ImageData polyfill
15+
if (!global.ImageData) {
16+
global.ImageData = ImageData;
17+
}
18+
19+
// Use cross-fetch as a polyfill for the browser fetch
20+
if (!global.fetch) {
21+
global.fetch = crossFetch;
22+
}
23+
24+
console.log("Setup complete");
25+
}
26+
27+
module.exports = setupTests;

src/BodyPose/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ class BodyPose {
215215
this.model = await poseDetection.createDetector(pipeline, modelConfig);
216216

217217
// for compatibility with p5's preload()
218-
if (this.p5PreLoadExists) window._decrementPreload();
218+
if (this.p5PreLoadExists()) window._decrementPreload();
219219

220220
return this;
221221
}

src/BodyPose/index.test.js

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,63 @@
1-
// Copyright (c) 2018 ml5
1+
// Copyright (c) 2018-2024 ml5
22
//
33
// This software is released under the MIT License.
44
// https://opensource.org/licenses/MIT
55

66
import { asyncLoadImage } from "../utils/testingUtils";
7-
import poseNet from "./index";
7+
import bodyPose from "./index";
8+
import crossFetch from "cross-fetch";
89

910
const POSENET_IMG =
1011
"https://github.com/ml5js/ml5-adjacent/raw/master/02_ImageClassification_Video/starter.png";
1112

12-
const POSENET_DEFAULTS = {
13-
architecture: "MobileNetV1",
14-
outputStride: 16,
15-
flipHorizontal: false,
16-
minConfidence: 0.5,
17-
maxPoseDetections: 5,
18-
scoreThreshold: 0.5,
19-
nmsRadius: 20,
20-
detectionType: "multiple",
21-
inputResolution: 256,
22-
multiplier: 0.75,
23-
quantBytes: 2,
24-
};
25-
26-
describe("PoseNet", () => {
27-
let net;
13+
describe("bodypose", () => {
14+
let myBodyPose;
15+
let image;
2816

2917
beforeAll(async () => {
30-
jest.setTimeout(10000);
31-
net = await poseNet();
18+
19+
// TODO: this should not be necessary! Should already be handled by setupTests.js.
20+
if (!global.fetch) {
21+
global.fetch = crossFetch;
22+
}
23+
24+
myBodyPose = bodyPose();
25+
await myBodyPose.ready;
26+
27+
image = await asyncLoadImage(POSENET_IMG);
3228
});
3329

34-
it("instantiates poseNet", () => {
35-
expect(net.architecture).toBe(POSENET_DEFAULTS.architecture);
36-
expect(net.outputStride).toBe(POSENET_DEFAULTS.outputStride);
37-
expect(net.inputResolution).toBe(POSENET_DEFAULTS.inputResolution);
38-
expect(net.multiplier).toBe(POSENET_DEFAULTS.multiplier);
39-
expect(net.quantBytes).toBe(POSENET_DEFAULTS.quantBytes);
30+
it("instantiates bodyPose", () => {
31+
expect(myBodyPose).toBeDefined()
32+
expect(myBodyPose.model).toBeDefined();
4033
});
4134

4235
it("detects poses in image", async () => {
43-
const image = await asyncLoadImage(POSENET_IMG);
4436

45-
// Result should be an array with a single object containing pose and skeleton.
46-
const result = await net.singlePose(image);
37+
// Result should be an array with a single object containing the detection.
38+
const result = await myBodyPose.detect(image);
4739
expect(result).toHaveLength(1);
48-
expect(result[0]).toHaveProperty("pose");
49-
expect(result[0]).toHaveProperty("skeleton");
40+
expect(result[0]).toHaveProperty("box");
41+
expect(result[0]).toHaveProperty("score");
42+
expect(result[0].keypoints.length).toBeGreaterThanOrEqual(5);
5043

5144
// Verify a known outcome.
52-
const nose = result[0].pose.keypoints.find(
53-
(keypoint) => keypoint.part === "nose"
45+
const nose = result[0].keypoints.find(
46+
(keypoint) => keypoint.name === "nose"
5447
);
48+
// Should be {"name": "nose", "score": 0.7217329144477844, "x": 454.1112813949585, "y": 256.606980448618}
5549
expect(nose).toBeTruthy();
56-
expect(nose.position.x).toBeCloseTo(448.6, 0);
57-
expect(nose.position.y).toBeCloseTo(255.9, 0);
58-
expect(nose.score).toBeCloseTo(0.999);
50+
expect(nose.x).toBeCloseTo(454.1, 0);
51+
expect(nose.y).toBeCloseTo(256.6, 0);
52+
expect(nose.score).toBeCloseTo(0.721, 2);
53+
});
54+
55+
it("calls the user's callback",(done) => {
56+
expect.assertions(1);
57+
const callback = (result) => {
58+
expect(result).toHaveLength(1); // don't need to repeat the rest
59+
done();
60+
}
61+
myBodyPose.detect(image, callback);
5962
});
6063
});

src/HandPose/index.test.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This software is released under the MIT License.
44
// https://opensource.org/licenses/MIT
55

6+
import crossFetch from 'cross-fetch';
67
import { asyncLoadImage } from "../utils/testingUtils";
78
import handpose from "./index";
89

@@ -14,10 +15,17 @@ describe("Handpose", () => {
1415

1516
beforeAll(async () => {
1617
jest.setTimeout(10000);
17-
handposeInstance = await handpose();
18+
19+
// TODO: this should not be necessary! Should already be handled by setupTests.js.
20+
if (!global.fetch) {
21+
global.fetch = crossFetch;
22+
}
23+
24+
handposeInstance = handpose();
25+
await handposeInstance.ready;
1826
});
1927

20-
it("detects poses in image", async () => {
28+
it("detects hands in image", async () => {
2129
testImage = await asyncLoadImage(HANDPOSE_IMG);
2230
const handPredictions = await handposeInstance.predict(testImage);
2331
expect(handPredictions).not.toHaveLength(0);

src/utils/testingUtils/index.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// eslint-disable-next-line import/no-extraneous-dependencies
2+
import { createImageData, ImageData } from "canvas";
3+
4+
export const asyncLoadImage = async (src) => {
5+
const img = new Image();
6+
if (src.startsWith("http")) {
7+
img.crossOrigin = "true";
8+
}
9+
img.src = src;
10+
await new Promise((resolve) => {
11+
img.onload = resolve;
12+
});
13+
return img;
14+
};
15+
16+
export const getRobin = async () => {
17+
return asyncLoadImage(
18+
"https://cdn.jsdelivr.net/gh/ml5js/ml5-library@main/assets/bird.jpg"
19+
);
20+
};
21+
22+
export const randomImageData = (width = 200, height = 100) => {
23+
const length = width * height * 4; // 4 channels - RGBA
24+
// Create an array of random pixel values
25+
const array = Uint8ClampedArray.from({ length }, () =>
26+
Math.floor(Math.random() * 256)
27+
);
28+
// Initialize a new ImageData object
29+
return createImageData(array, width, height);
30+
};
31+
32+
export const polyfillImageData = () => {
33+
if (!global.ImageData) {
34+
global.ImageData = ImageData;
35+
}
36+
};

0 commit comments

Comments
 (0)