@robotical/martyblocksjr 3.5.27 → 3.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robotical/martyblocksjr",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.29",
|
|
4
4
|
"description": "ScratchJr",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"start": "npx serve --listen tcp://0.0.0.0:3011 ./editions/free/src",
|
|
16
16
|
"build:copy": "npm run build-dev && cp -f src/build/bundles/* editions/free/src",
|
|
17
17
|
"build-prod:copy": "npm run build && cp -f src/build/bundles/* editions/free/src",
|
|
18
|
-
"build:start": "npm run build:copy && npm start"
|
|
18
|
+
"build:start": "npm run build:copy && npm start",
|
|
19
|
+
"test": "vitest",
|
|
20
|
+
"test:e2e": "vitest run tests/e2e"
|
|
19
21
|
},
|
|
20
22
|
"author": "Massachusetts Institute of Technology",
|
|
21
23
|
"license": "BSD-3-Clause",
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
"homepage": "https://github.com/llk/scratchjr",
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@babel/polyfill": "^7.10.4",
|
|
30
|
+
"@babel/preset-env": "^7.25.4",
|
|
28
31
|
"babel-core": "^6.4.0",
|
|
29
32
|
"babel-eslint": "^4.1.6",
|
|
30
33
|
"babel-loader": "^8.2.5",
|
|
@@ -38,7 +41,9 @@
|
|
|
38
41
|
"esformatter-semicolons": "^1.1.2",
|
|
39
42
|
"eslint": "^1.10.3",
|
|
40
43
|
"expose-loader": "^3.0.0",
|
|
44
|
+
"puppeteer": "^2.1.1",
|
|
41
45
|
"strip-sourcemap-loader": "0.0.1",
|
|
46
|
+
"vitest": "^4.0.14",
|
|
42
47
|
"webpack": "^5.73.0",
|
|
43
48
|
"webpack-cli": "^4.10.0",
|
|
44
49
|
"webpack-notifier": "^1.6.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import puppeteer from "puppeteer";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import http from "http";
|
|
5
|
+
|
|
6
|
+
const PORT = 3011;
|
|
7
|
+
const HOST = `http://localhost:${PORT}`;
|
|
8
|
+
|
|
9
|
+
let server;
|
|
10
|
+
|
|
11
|
+
async function waitForServer(maxAttempts = 20) {
|
|
12
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
13
|
+
const ok = await new Promise((resolve) => {
|
|
14
|
+
const req = http.get(`${HOST}/`, (res) => {
|
|
15
|
+
res.destroy();
|
|
16
|
+
resolve(true);
|
|
17
|
+
});
|
|
18
|
+
req.on("error", () => resolve(false));
|
|
19
|
+
});
|
|
20
|
+
if (ok) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Local server did not start in time");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
|
|
30
|
+
stdio: "ignore",
|
|
31
|
+
});
|
|
32
|
+
await waitForServer();
|
|
33
|
+
}, 30_000);
|
|
34
|
+
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
if (server) {
|
|
37
|
+
server.kill();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Chromium 79 smoke test", () => {
|
|
42
|
+
it(
|
|
43
|
+
"loads home page without console errors",
|
|
44
|
+
async () => {
|
|
45
|
+
const browser = await puppeteer.launch({
|
|
46
|
+
headless: true,
|
|
47
|
+
args: ["--no-sandbox"],
|
|
48
|
+
});
|
|
49
|
+
const page = await browser.newPage();
|
|
50
|
+
const errors = [];
|
|
51
|
+
|
|
52
|
+
page.on("pageerror", (err) => errors.push(err.message || String(err)));
|
|
53
|
+
page.on("console", (msg) => {
|
|
54
|
+
if (msg.type() === "error") {
|
|
55
|
+
errors.push(msg.text());
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await page.goto(`${HOST}/home.html`, { waitUntil: "networkidle2", timeout: 30_000 });
|
|
60
|
+
const title = await page.title();
|
|
61
|
+
|
|
62
|
+
await browser.close();
|
|
63
|
+
|
|
64
|
+
expect(title.toLowerCase()).toContain("scratch");
|
|
65
|
+
expect(errors).toEqual([]);
|
|
66
|
+
},
|
|
67
|
+
60_000
|
|
68
|
+
);
|
|
69
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import ApplicationManagerMock from '@/utils/ApplicationManagerMock';
|
|
3
|
+
|
|
4
|
+
const noop = () => {};
|
|
5
|
+
|
|
6
|
+
vi.mock('@/editor/ScratchJr', () => {
|
|
7
|
+
const runtime = {
|
|
8
|
+
threadsRunning: [],
|
|
9
|
+
restartThread: vi.fn((spr, block) => ({ spr, firstBlock: block, isRunning: true })),
|
|
10
|
+
isScriptRunningForThatBlock: vi.fn(() => false)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
default: {
|
|
15
|
+
runtime,
|
|
16
|
+
stage: null,
|
|
17
|
+
startCurrentPageStrips: vi.fn(),
|
|
18
|
+
stopStrips: vi.fn(),
|
|
19
|
+
updateRunStopButtons: vi.fn()
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('@/utils/ScratchAudio', () => ({
|
|
25
|
+
default: {
|
|
26
|
+
sndFX: vi.fn()
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('@/editor/ui/UI', () => ({ default: {} }));
|
|
31
|
+
vi.mock('@/editor/ui/Grid', () => ({ default: { size: 24 } }));
|
|
32
|
+
vi.mock('@/utils/lib', () => ({
|
|
33
|
+
gn: () => null,
|
|
34
|
+
rgbToHex: value => value
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('Marty movement blocks', () => {
|
|
38
|
+
let Prims;
|
|
39
|
+
let MartyBlocks;
|
|
40
|
+
let mockMarty;
|
|
41
|
+
let consoleSpy;
|
|
42
|
+
let appManager;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
vi.resetModules();
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
global.window = { navigator: { userAgent: '' } };
|
|
49
|
+
global.document = { documentElement: {} };
|
|
50
|
+
consoleSpy = vi.spyOn(console, 'log').mockImplementation(noop);
|
|
51
|
+
|
|
52
|
+
const [primsModule, martyBlocksModule] = await Promise.all([
|
|
53
|
+
import('@/editor/engine/Prims.js'),
|
|
54
|
+
import('@/marty/MartyBlocks.js')
|
|
55
|
+
]);
|
|
56
|
+
Prims = primsModule.default;
|
|
57
|
+
MartyBlocks = martyBlocksModule.default;
|
|
58
|
+
Prims.init();
|
|
59
|
+
|
|
60
|
+
appManager = new ApplicationManagerMock();
|
|
61
|
+
window.applicationManager = appManager;
|
|
62
|
+
mockMarty = createMartyFromApplicationManagerMock(appManager);
|
|
63
|
+
Prims.martyBlocks = new MartyBlocks(mockMarty);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
consoleSpy.mockRestore();
|
|
68
|
+
vi.runAllTimers();
|
|
69
|
+
vi.useRealTimers();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('keeps one-to-one steps for a simple walk cycle', () => {
|
|
73
|
+
const head = buildBlocks([
|
|
74
|
+
['martyStepForward', 1],
|
|
75
|
+
['martyStepLeft', 1],
|
|
76
|
+
['martyStepRight', 1],
|
|
77
|
+
['martyStepBackward', 1]
|
|
78
|
+
]);
|
|
79
|
+
const sprite = new TestSprite();
|
|
80
|
+
runScript(Prims, head, sprite);
|
|
81
|
+
|
|
82
|
+
expect(sentCommands(mockMarty)).toEqual([
|
|
83
|
+
'traj/step/1/?moveTime=1500&stepLength=25',
|
|
84
|
+
'traj/sidestep/1/?side=0&moveTime=1500&stepLength=35',
|
|
85
|
+
'traj/sidestep/1/?side=1&moveTime=1500&stepLength=35',
|
|
86
|
+
'traj/step/1/?moveTime=1500&stepLength=-25'
|
|
87
|
+
]);
|
|
88
|
+
expect(loggedActions(consoleSpy)).toEqual([
|
|
89
|
+
'stepForward',
|
|
90
|
+
'stepLeft',
|
|
91
|
+
'stepRight',
|
|
92
|
+
'stepBackward'
|
|
93
|
+
]);
|
|
94
|
+
expect(sprite.xcoor).toBeCloseTo(0, 5);
|
|
95
|
+
expect(sprite.ycoor).toBeCloseTo(0, 5);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('turn blocks respect their arguments and complete the sequence', () => {
|
|
99
|
+
const head = buildBlocks([
|
|
100
|
+
['martyTurnLeft', 1],
|
|
101
|
+
['martyTurnRight', 1]
|
|
102
|
+
]);
|
|
103
|
+
const sprite = new TestSprite();
|
|
104
|
+
runScript(Prims, head, sprite);
|
|
105
|
+
|
|
106
|
+
expect(sentCommands(mockMarty)).toEqual([
|
|
107
|
+
'traj/step/1/?moveTime=1500&turn=10&stepLength=1',
|
|
108
|
+
'traj/step/1/?moveTime=1500&turn=-10&stepLength=1'
|
|
109
|
+
]);
|
|
110
|
+
expect(loggedActions(consoleSpy)).toEqual([
|
|
111
|
+
'turnLeft',
|
|
112
|
+
'turnRight'
|
|
113
|
+
]);
|
|
114
|
+
expect(sprite.angle).toBeCloseTo(0, 5);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not resend commands when previous timeouts fire mid-sequence', () => {
|
|
118
|
+
const head = buildBlocks([
|
|
119
|
+
['martyStepForward', 1],
|
|
120
|
+
['martyStepLeft', 1],
|
|
121
|
+
['martyStepRight', 1],
|
|
122
|
+
['martyStepBackward', 1]
|
|
123
|
+
]);
|
|
124
|
+
const sprite = new TestSprite();
|
|
125
|
+
runScript(Prims, head, sprite, { advanceMsPerTick: 200 });
|
|
126
|
+
|
|
127
|
+
expect(sentCommands(mockMarty)).toEqual([
|
|
128
|
+
'traj/step/1/?moveTime=1500&stepLength=25',
|
|
129
|
+
'traj/sidestep/1/?side=0&moveTime=1500&stepLength=35',
|
|
130
|
+
'traj/sidestep/1/?side=1&moveTime=1500&stepLength=35',
|
|
131
|
+
'traj/step/1/?moveTime=1500&stepLength=-25'
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function runScript(Prims, head, sprite, options = {}) {
|
|
137
|
+
const { advanceMsPerTick = 0 } = options;
|
|
138
|
+
const strip = {
|
|
139
|
+
spr: sprite,
|
|
140
|
+
thisblock: head,
|
|
141
|
+
firstBlock: head,
|
|
142
|
+
oldblock: null,
|
|
143
|
+
stack: [],
|
|
144
|
+
firstTime: true,
|
|
145
|
+
count: -1,
|
|
146
|
+
waitTimer: 0,
|
|
147
|
+
distance: -1,
|
|
148
|
+
vector: { x: 0, y: 0 },
|
|
149
|
+
called: [],
|
|
150
|
+
isRunning: true,
|
|
151
|
+
cmdSent: false
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let ticks = 0;
|
|
155
|
+
const maxTicks = 10000;
|
|
156
|
+
|
|
157
|
+
while (strip.thisblock && ticks < maxTicks) {
|
|
158
|
+
if (strip.waitTimer > 0) {
|
|
159
|
+
strip.waitTimer -= 1;
|
|
160
|
+
ticks += 1;
|
|
161
|
+
if (advanceMsPerTick) {
|
|
162
|
+
vi.advanceTimersByTime(advanceMsPerTick);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const prim = Prims.table[strip.thisblock.blocktype];
|
|
167
|
+
if (!prim) {
|
|
168
|
+
throw new Error(`No prim registered for ${strip.thisblock.blocktype}`);
|
|
169
|
+
}
|
|
170
|
+
prim(strip);
|
|
171
|
+
ticks += 1;
|
|
172
|
+
if (advanceMsPerTick) {
|
|
173
|
+
vi.advanceTimersByTime(advanceMsPerTick);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (ticks >= maxTicks) {
|
|
178
|
+
throw new Error('Script execution did not finish');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return strip;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildBlocks(sequence) {
|
|
185
|
+
const blocks = sequence.map(([type, arg]) => new TestBlock(type, arg));
|
|
186
|
+
for (let i = 0; i < blocks.length - 1; i += 1) {
|
|
187
|
+
blocks[i].next = blocks[i + 1];
|
|
188
|
+
blocks[i + 1].previousBlock = blocks[i];
|
|
189
|
+
}
|
|
190
|
+
return blocks[0] || null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class TestBlock {
|
|
194
|
+
constructor(type, argValue) {
|
|
195
|
+
this.blocktype = type;
|
|
196
|
+
this.argValue = argValue;
|
|
197
|
+
this.next = null;
|
|
198
|
+
this.previousBlock = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getArgValue() {
|
|
202
|
+
return this.argValue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
findFirst() {
|
|
206
|
+
let node = this;
|
|
207
|
+
while (node.previousBlock) {
|
|
208
|
+
node = node.previousBlock;
|
|
209
|
+
}
|
|
210
|
+
return node;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
closeBalloon() {
|
|
214
|
+
// no-op stub for compatibility with Thread.stop
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class TestSprite {
|
|
219
|
+
constructor() {
|
|
220
|
+
this.xcoor = 0;
|
|
221
|
+
this.ycoor = 0;
|
|
222
|
+
this.angle = 0;
|
|
223
|
+
this.speed = 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setPos(x, y) {
|
|
227
|
+
this.xcoor = x;
|
|
228
|
+
this.ycoor = y;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setHeading(angle) {
|
|
232
|
+
this.angle = angle;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
closeBalloon() {
|
|
236
|
+
// no-op stub for compatibility with Thread.stop
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function createMartyFromApplicationManagerMock(appManager) {
|
|
241
|
+
const raft = appManager.getTheCurrentlySelectedDeviceOrFirstOfItsKind('Marty');
|
|
242
|
+
raft.sendRestMessage = vi.fn();
|
|
243
|
+
raft.getRaftVersion = () => '2.0.0';
|
|
244
|
+
raft.publishedDataAnalyser = {
|
|
245
|
+
eventsMap: {
|
|
246
|
+
colourSensed: {
|
|
247
|
+
red: 'red',
|
|
248
|
+
green: 'green',
|
|
249
|
+
blue: 'blue',
|
|
250
|
+
purple: 'purple',
|
|
251
|
+
yellow: 'yellow',
|
|
252
|
+
air: 'air',
|
|
253
|
+
unclear: 'unclear'
|
|
254
|
+
},
|
|
255
|
+
objectSense: {
|
|
256
|
+
near: 'near',
|
|
257
|
+
none: 'none'
|
|
258
|
+
},
|
|
259
|
+
lightSense: {
|
|
260
|
+
none: 'light-none',
|
|
261
|
+
mid: 'light-mid',
|
|
262
|
+
high: 'light-high'
|
|
263
|
+
},
|
|
264
|
+
noiseSense: {
|
|
265
|
+
high: 'noise-high',
|
|
266
|
+
low: 'noise-low'
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
on: vi.fn()
|
|
270
|
+
};
|
|
271
|
+
return raft;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sentCommands(fakeMarty) {
|
|
275
|
+
return fakeMarty.sendRestMessage.mock.calls.map(args => args[0]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function loggedActions(spy) {
|
|
279
|
+
return spy.mock.calls
|
|
280
|
+
.filter(call => typeof call[0] === 'string' && call[0].startsWith('[MartyBlocks]'))
|
|
281
|
+
.map(call => call[1]?.action);
|
|
282
|
+
}
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { defineConfig } = require('vitest/config');
|
|
3
|
+
|
|
4
|
+
module.exports = defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
'@': path.resolve(__dirname, './src')
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
test: {
|
|
11
|
+
environment: 'node',
|
|
12
|
+
include: ['tests/**/*.test.js']
|
|
13
|
+
}
|
|
14
|
+
});
|
package/webpack.config.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
var WebpackNotifierPlugin = require("webpack-notifier");
|
|
2
|
+
const path = require("path");
|
|
2
3
|
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
3
4
|
|
|
4
5
|
const mode = process.argv.find(arg => arg.startsWith('--mode=')).split('=')[1];
|
|
5
6
|
const isProduction = mode === 'production';
|
|
7
|
+
const sqlJsPath = path.dirname(require.resolve("sql.js/package.json"));
|
|
8
|
+
|
|
9
|
+
const babelLoaderOptions = {
|
|
10
|
+
presets: [
|
|
11
|
+
["@babel/preset-env", {
|
|
12
|
+
targets: {
|
|
13
|
+
chrome: "60",
|
|
14
|
+
firefox: "60",
|
|
15
|
+
safari: "11"
|
|
16
|
+
},
|
|
17
|
+
bugfixes: true,
|
|
18
|
+
}]
|
|
19
|
+
]
|
|
20
|
+
};
|
|
6
21
|
|
|
7
22
|
module.exports = {
|
|
8
23
|
resolve: {
|
|
@@ -44,16 +59,20 @@ module.exports = {
|
|
|
44
59
|
// },
|
|
45
60
|
},
|
|
46
61
|
{
|
|
47
|
-
loader: "babel-loader",
|
|
48
|
-
exclude: /node_modules/,
|
|
49
62
|
test: /\.jsx?$/,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
exclude: /node_modules/,
|
|
64
|
+
use: {
|
|
65
|
+
loader: "babel-loader",
|
|
66
|
+
options: babelLoaderOptions,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
test: /\.js$/,
|
|
71
|
+
include: [sqlJsPath],
|
|
72
|
+
use: {
|
|
73
|
+
loader: "babel-loader",
|
|
74
|
+
options: babelLoaderOptions,
|
|
75
|
+
},
|
|
57
76
|
},
|
|
58
77
|
],
|
|
59
78
|
},
|