@robotical/martyblocksjr 3.5.27 → 3.5.28
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.28",
|
|
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",
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"eslint": "^1.10.3",
|
|
40
42
|
"expose-loader": "^3.0.0",
|
|
41
43
|
"strip-sourcemap-loader": "0.0.1",
|
|
44
|
+
"vitest": "^4.0.14",
|
|
42
45
|
"webpack": "^5.73.0",
|
|
43
46
|
"webpack-cli": "^4.10.0",
|
|
44
47
|
"webpack-notifier": "^1.6.0"
|
|
@@ -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
|
+
});
|