Skip to content

Commit 8ed7ccc

Browse files
authored
Merge pull request #238 from MageStudio/tests/added-tests-to-the-engine
tests: added tests coverage, added github workflow to run tests on PRs
2 parents 77450cf + 52be580 commit 8ed7ccc

164 files changed

Lines changed: 25479 additions & 15760 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: [master, main]
6+
push:
7+
branches: [master, main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Use Node.js 20
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
cache: npm
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Lint
26+
run: npm run lint
27+
28+
- name: Run tests
29+
run: npm test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ dist/mage.node.js
1313

1414
**/.DS_Store
1515

16+
coverage/

__mocks__/three.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Lightweight mock of THREE.js for unit tests.
2+
// Only stubs the APIs actually used by src/lib/ modules.
3+
// Grow this file incrementally as more tests need additional THREE APIs.
4+
5+
class Vector3 {
6+
constructor(x = 0, y = 0, z = 0) {
7+
this.x = x;
8+
this.y = y;
9+
this.z = z;
10+
}
11+
set(x, y, z) { this.x = x; this.y = y; this.z = z; return this; }
12+
clone() { return new Vector3(this.x, this.y, this.z); }
13+
add(v) { this.x += v.x; this.y += v.y; this.z += v.z; return this; }
14+
sub(v) { this.x -= v.x; this.y -= v.y; this.z -= v.z; return this; }
15+
normalize() {
16+
const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
17+
if (len > 0) { this.x /= len; this.y /= len; this.z /= len; }
18+
return this;
19+
}
20+
multiplyScalar(s) { this.x *= s; this.y *= s; this.z *= s; return this; }
21+
lerp(v, t) {
22+
this.x += (v.x - this.x) * t;
23+
this.y += (v.y - this.y) * t;
24+
this.z += (v.z - this.z) * t;
25+
return this;
26+
}
27+
}
28+
29+
class Vector2 {
30+
constructor(x = 0, y = 0) {
31+
this.x = x;
32+
this.y = y;
33+
}
34+
}
35+
36+
const MathUtils = {
37+
lerp: (x, y, t) => x + (y - x) * t,
38+
};
39+
40+
class Color {
41+
constructor(color) {
42+
this.value = color;
43+
}
44+
}
45+
46+
class AnimationMixer {
47+
constructor() {
48+
this.time = 0;
49+
}
50+
clipAction() {
51+
return {
52+
reset: function() { return this; },
53+
setLoop: function() { return this; },
54+
setEffectiveTimeScale: function() { return this; },
55+
setEffectiveWeight: function() { return this; },
56+
play: function() {},
57+
stop: function() {},
58+
fadeIn: function() { return this; },
59+
fadeOut: function() { return this; },
60+
isRunning: function() { return false; },
61+
time: 0,
62+
};
63+
}
64+
stopAllAction() {}
65+
addEventListener() {}
66+
update() {}
67+
}
68+
69+
class AnimationClip {
70+
static findByName(clips, name) {
71+
return clips.find(c => c.name === name);
72+
}
73+
}
74+
75+
class EventDispatcher {
76+
constructor() {
77+
this._listeners = {};
78+
}
79+
addEventListener(type, listener) {
80+
if (!this._listeners[type]) this._listeners[type] = [];
81+
this._listeners[type].push(listener);
82+
}
83+
removeEventListener(type, listener) {
84+
if (this._listeners[type]) {
85+
this._listeners[type] = this._listeners[type].filter(l => l !== listener);
86+
}
87+
}
88+
dispatchEvent(event) {
89+
if (this._listeners[event.type]) {
90+
this._listeners[event.type].forEach(l => l(event));
91+
}
92+
}
93+
}
94+
95+
class Quaternion {
96+
constructor(x = 0, y = 0, z = 0, w = 1) {
97+
this.x = x; this.y = y; this.z = z; this.w = w;
98+
}
99+
identity() { this.x = 0; this.y = 0; this.z = 0; this.w = 1; return this; }
100+
copy(q) { this.x = q.x; this.y = q.y; this.z = q.z; this.w = q.w; return this; }
101+
clone() { return new Quaternion(this.x, this.y, this.z, this.w); }
102+
}
103+
104+
class Euler {
105+
constructor(x = 0, y = 0, z = 0, order = "XYZ") {
106+
this.x = x; this.y = y; this.z = z; this.order = order;
107+
}
108+
}
109+
110+
class Matrix4 {
111+
constructor() {
112+
this.elements = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1];
113+
}
114+
identity() { this.elements = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]; return this; }
115+
getInverse() { return this; }
116+
multiplyMatrices() { return this; }
117+
setFromMatrixScale() { return this; }
118+
}
119+
120+
// Material stubs
121+
class MeshBasicMaterial { constructor(opts) { Object.assign(this, opts); } }
122+
class MeshLambertMaterial { constructor(opts) { Object.assign(this, opts); } }
123+
class MeshPhongMaterial { constructor(opts) { Object.assign(this, opts); } }
124+
class MeshDepthMaterial { constructor(opts) { Object.assign(this, opts); } }
125+
class MeshStandardMaterial { constructor(opts) { Object.assign(this, opts); } }
126+
class MeshToonMaterial { constructor(opts) { Object.assign(this, opts); } }
127+
128+
// Encoding constants
129+
const LinearEncoding = 3000;
130+
const sRGBEncoding = 3001;
131+
const GammaEncoding = 3007;
132+
const RGBEEncoding = 3002;
133+
const RGBM7Encoding = 3004;
134+
const RGBM16Encoding = 3005;
135+
const RGBDEncoding = 3006;
136+
const BasicDepthPacking = 3200;
137+
const RGBADepthPacking = 3201;
138+
139+
// Side constants
140+
const FrontSide = 0;
141+
const BackSide = 1;
142+
const DoubleSide = 2;
143+
144+
const LoopRepeat = 2201;
145+
const LoopOnce = 2200;
146+
147+
module.exports = {
148+
Vector3,
149+
Vector2,
150+
Quaternion,
151+
Euler,
152+
Matrix4,
153+
MathUtils,
154+
Color,
155+
AnimationMixer,
156+
AnimationClip,
157+
EventDispatcher,
158+
MeshBasicMaterial,
159+
MeshLambertMaterial,
160+
MeshPhongMaterial,
161+
MeshDepthMaterial,
162+
MeshStandardMaterial,
163+
MeshToonMaterial,
164+
LinearEncoding,
165+
sRGBEncoding,
166+
GammaEncoding,
167+
RGBEEncoding,
168+
RGBM7Encoding,
169+
RGBM16Encoding,
170+
RGBDEncoding,
171+
BasicDepthPacking,
172+
RGBADepthPacking,
173+
FrontSide,
174+
BackSide,
175+
DoubleSide,
176+
LoopRepeat,
177+
LoopOnce,
178+
};

babel.config.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,22 @@ const config = {
1212
["babel-plugin-inferno", {"imports": true}],
1313
"@babel/plugin-proposal-class-properties",
1414
"@babel/plugin-transform-classes"
15-
]
15+
],
16+
env: {
17+
test: {
18+
presets: [
19+
["@babel/preset-env", {
20+
targets: {
21+
node: "current",
22+
},
23+
}],
24+
],
25+
plugins: [
26+
"@babel/plugin-proposal-class-properties",
27+
"@babel/plugin-transform-classes",
28+
],
29+
},
30+
},
1631
};
1732

1833
module.exports = config;

eslint.config.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const eslint = require("@eslint/js");
2+
const prettier = require("eslint-plugin-prettier/recommended");
3+
const globals = require("globals");
4+
5+
module.exports = [
6+
{
7+
ignores: [
8+
"dist/",
9+
"node_modules/",
10+
"__mocks__/",
11+
"config/",
12+
"src/loaders/ColladaLoader.js",
13+
"src/loaders/GLTFLoader.js",
14+
"src/loaders/FBXLoader.js",
15+
"src/lib/fflate.js",
16+
"src/models/SkeletonUtils.js",
17+
"src/controls/Orbit.js",
18+
"src/controls/Transform.js",
19+
"src/controls/TransformGizmo.js",
20+
"src/fx/materials/Ocean.js",
21+
"src/fx/materials/Water.old.js",
22+
"src/fx/materials/Mirror.js",
23+
"src/scripts/builtin/SmoothCameraFollow.js",
24+
],
25+
},
26+
eslint.configs.recommended,
27+
prettier,
28+
{
29+
languageOptions: {
30+
ecmaVersion: 2022,
31+
sourceType: "module",
32+
globals: {
33+
...globals.browser,
34+
requestNextFrame: "readonly",
35+
global: "readonly",
36+
},
37+
},
38+
rules: {
39+
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
40+
"no-console": "off",
41+
"no-dupe-class-members": "warn",
42+
"no-empty": ["error", { allowEmptyCatch: true }],
43+
"prettier/prettier": ["error", { endOfLine: "auto" }],
44+
},
45+
},
46+
{
47+
files: ["**/__tests__/**/*.js", "**/*.test.js"],
48+
languageOptions: {
49+
globals: {
50+
...globals.jest,
51+
...globals.commonjs,
52+
},
53+
},
54+
},
55+
{
56+
files: ["src/physics/worker/**/*.js"],
57+
languageOptions: {
58+
globals: {
59+
Ammo: "readonly",
60+
postMessage: "readonly",
61+
importScripts: "readonly",
62+
},
63+
},
64+
},
65+
{
66+
files: ["src/lib/features.js"],
67+
languageOptions: {
68+
globals: {
69+
ActiveXObject: "readonly",
70+
},
71+
},
72+
},
73+
];

jest.config.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use strict";
2+
3+
module.exports = {
4+
roots: ["<rootDir>/src"],
5+
6+
testMatch: [
7+
"**/__tests__/**/*.test.js",
8+
"**/*.test.js",
9+
],
10+
11+
transform: {
12+
"^.+\\.js$": "babel-jest",
13+
},
14+
15+
transformIgnorePatterns: [
16+
"/node_modules/(?!three/)",
17+
],
18+
19+
collectCoverageFrom: [
20+
"src/lib/**/*.js",
21+
"src/entities/**/*.js",
22+
"src/models/**/*.js",
23+
"src/physics/**/*.js",
24+
"src/images/**/*.js",
25+
"!src/lib/fflate.js",
26+
"!src/lib/workers.js",
27+
"!src/physics/worker/index.js",
28+
],
29+
30+
testEnvironment: "node",
31+
};

0 commit comments

Comments
 (0)