@robotical/martyblocksjr 4.2.3 → 4.2.4
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,5 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
const { newHTMLMock } = vi.hoisted(() => ({
|
|
4
|
+
newHTMLMock: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
|
|
3
7
|
vi.mock('@/editor/ScratchJr', () => ({
|
|
4
8
|
default: {
|
|
5
9
|
isMartyModeEnabled: false,
|
|
@@ -42,7 +46,7 @@ vi.mock('@/utils/lib', () => ({
|
|
|
42
46
|
gn: vi.fn(),
|
|
43
47
|
CSSTransition: vi.fn(),
|
|
44
48
|
localx: vi.fn(),
|
|
45
|
-
newHTML:
|
|
49
|
+
newHTML: newHTMLMock,
|
|
46
50
|
newButton: vi.fn(),
|
|
47
51
|
scaleMultiplier: 1,
|
|
48
52
|
fullscreenScaleMultiplier: 1,
|
|
@@ -75,18 +79,36 @@ vi.mock('@/html-svgs/sprite_toggle_on', () => ({ spriteToggleOn: '<svg></svg>' }
|
|
|
75
79
|
vi.mock('@/html-svgs/battery-svg', () => ({ batterySvg: () => '<svg id="battery"></svg>' }));
|
|
76
80
|
vi.mock('@/html-svgs/signal-svg', () => ({ signalSvg: () => '<svg id="signal"></svg>' }));
|
|
77
81
|
vi.mock('@/utils/raft-subscription-helpers', () => ({
|
|
78
|
-
createRaftConnectionIssueDetectedHelper:
|
|
79
|
-
subscribe: vi.fn(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
createRaftConnectionIssueDetectedHelper: raft => ({
|
|
83
|
+
subscribe: vi.fn(callback => {
|
|
84
|
+
raft.__connectionIssueDetectedCallback = callback;
|
|
85
|
+
}),
|
|
86
|
+
unsubscribe: vi.fn(() => {
|
|
87
|
+
raft.__connectionIssueDetectedUnsubscribed = true;
|
|
88
|
+
})
|
|
85
89
|
}),
|
|
86
|
-
|
|
87
|
-
subscribe: vi.fn(
|
|
88
|
-
|
|
90
|
+
createRaftConnectionIssueResolvedHelper: raft => ({
|
|
91
|
+
subscribe: vi.fn(callback => {
|
|
92
|
+
raft.__connectionIssueResolvedCallback = callback;
|
|
93
|
+
}),
|
|
94
|
+
unsubscribe: vi.fn(() => {
|
|
95
|
+
raft.__connectionIssueResolvedUnsubscribed = true;
|
|
96
|
+
})
|
|
89
97
|
}),
|
|
98
|
+
raftVerifiedSubscriptionHelper: raft => {
|
|
99
|
+
let callback;
|
|
100
|
+
return {
|
|
101
|
+
subscribe: vi.fn(nextCallback => {
|
|
102
|
+
callback = nextCallback;
|
|
103
|
+
raft.__verifiedCallbacks = raft.__verifiedCallbacks || [];
|
|
104
|
+
raft.__verifiedCallbacks.push(callback);
|
|
105
|
+
}),
|
|
106
|
+
unsubscribe: vi.fn(() => {
|
|
107
|
+
raft.__verifiedUnsubscribeCount = (raft.__verifiedUnsubscribeCount || 0) + 1;
|
|
108
|
+
raft.__verifiedCallbacks = (raft.__verifiedCallbacks || []).filter(item => item !== callback);
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
},
|
|
90
112
|
raftDisconnectedSubscriptionHelper: raft => {
|
|
91
113
|
const helper = {
|
|
92
114
|
subscribe: vi.fn(callback => {
|
|
@@ -128,6 +150,11 @@ describe('Marty connection UI', () => {
|
|
|
128
150
|
removeMarty: vi.fn(),
|
|
129
151
|
setMartySensorAvailability: vi.fn()
|
|
130
152
|
},
|
|
153
|
+
cogManager: {
|
|
154
|
+
addCog: vi.fn(),
|
|
155
|
+
wireCogWithBlocks: vi.fn(),
|
|
156
|
+
removeCog: vi.fn()
|
|
157
|
+
},
|
|
131
158
|
microBitManager: {
|
|
132
159
|
addMicroBit: vi.fn(),
|
|
133
160
|
wireMicroBitWithBlocks: vi.fn(),
|
|
@@ -139,6 +166,13 @@ describe('Marty connection UI', () => {
|
|
|
139
166
|
import('@/editor/ui/UI.js'),
|
|
140
167
|
import('@/editor/ScratchJr')
|
|
141
168
|
]);
|
|
169
|
+
newHTMLMock.mockImplementation((type, className, parent) => {
|
|
170
|
+
const element = new FakeElement(className ? [className] : []);
|
|
171
|
+
if (parent) {
|
|
172
|
+
parent.appendChild(element);
|
|
173
|
+
}
|
|
174
|
+
return element;
|
|
175
|
+
});
|
|
142
176
|
UI = uiModule.default;
|
|
143
177
|
ScratchJr = scratchJrModule.default;
|
|
144
178
|
ScratchJr.isMartyModeEnabled = false;
|
|
@@ -249,6 +283,102 @@ describe('Marty connection UI', () => {
|
|
|
249
283
|
expect(button.querySelector('.iconButtonContainer').classList.contains('connectedButtonContainer')).toBe(true);
|
|
250
284
|
});
|
|
251
285
|
|
|
286
|
+
it('uses one verified subscription instance and cleans it up after setup', () => {
|
|
287
|
+
const button = createConnectionButton();
|
|
288
|
+
const raft = createCogRaft();
|
|
289
|
+
const setupConnectionButton = vi.fn();
|
|
290
|
+
|
|
291
|
+
UI.setupConnectionButtonWhenVerified(button, raft, setupConnectionButton);
|
|
292
|
+
|
|
293
|
+
expect(raft.__verifiedCallbacks).toHaveLength(1);
|
|
294
|
+
const verifiedCallback = raft.__verifiedCallbacks[0];
|
|
295
|
+
raft.isVerified = true;
|
|
296
|
+
verifiedCallback();
|
|
297
|
+
verifiedCallback();
|
|
298
|
+
|
|
299
|
+
expect(setupConnectionButton).toHaveBeenCalledTimes(1);
|
|
300
|
+
expect(setupConnectionButton).toHaveBeenCalledWith(button, raft);
|
|
301
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
302
|
+
expect(raft.__verifiedCallbacks).toHaveLength(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('sets up an already verified Cog even when its verified event was missed', () => {
|
|
306
|
+
const button = createConnectionButton();
|
|
307
|
+
const raft = createCogRaft();
|
|
308
|
+
const setupConnectionButton = vi.fn();
|
|
309
|
+
raft.isVerified = true;
|
|
310
|
+
|
|
311
|
+
UI.setupConnectionButtonWhenVerified(button, raft, setupConnectionButton);
|
|
312
|
+
|
|
313
|
+
expect(setupConnectionButton).toHaveBeenCalledWith(button, raft);
|
|
314
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
315
|
+
expect(raft.__verifiedCallbacks).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reconciles an existing verified Cog after the host application manager is injected', () => {
|
|
319
|
+
const cogButton = createConnectionButton();
|
|
320
|
+
const martyButton = createConnectionButton();
|
|
321
|
+
const raft = createCogRaft();
|
|
322
|
+
raft.isVerified = true;
|
|
323
|
+
const setupCogSpy = vi.spyOn(UI, 'setupCogConnectionButton').mockImplementation(() => {});
|
|
324
|
+
window.applicationManager = undefined;
|
|
325
|
+
|
|
326
|
+
expect(UI.reconcileConnectionButtonsWhenApplicationManagerReady(cogButton, martyButton)).toBe(false);
|
|
327
|
+
window.applicationManager = {
|
|
328
|
+
getTheCurrentlySelectedDeviceOrFirstOfItsKind: vi.fn(type => type === 'Cog' ? raft : undefined)
|
|
329
|
+
};
|
|
330
|
+
vi.advanceTimersByTime(100);
|
|
331
|
+
|
|
332
|
+
expect(setupCogSpy).toHaveBeenCalledWith(cogButton, raft);
|
|
333
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
334
|
+
setupCogSpy.mockRestore();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('disconnects and clears a stale Cog when the connection issue countdown expires', () => {
|
|
338
|
+
const button = createConnectionButton();
|
|
339
|
+
const raft = createCogRaft();
|
|
340
|
+
window.applicationManager.disconnectGeneric = vi.fn((disconnectedRaft, onDisconnected) => {
|
|
341
|
+
onDisconnected();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
UI.setupCogConnectionButton(button, raft);
|
|
345
|
+
raft.__connectionIssueDetectedCallback();
|
|
346
|
+
|
|
347
|
+
expect(button.querySelector('.connIssueOverlay').textContent).toBe('A11Y_CONNECTION_LOST 60');
|
|
348
|
+
expect(button.style.pointerEvents).toBe('none');
|
|
349
|
+
|
|
350
|
+
vi.advanceTimersByTime(59000);
|
|
351
|
+
expect(window.applicationManager.disconnectGeneric).not.toHaveBeenCalled();
|
|
352
|
+
expect(button.querySelector('.connIssueOverlay').textContent).toBe('A11Y_CONNECTION_LOST 1');
|
|
353
|
+
|
|
354
|
+
vi.advanceTimersByTime(1000);
|
|
355
|
+
|
|
356
|
+
expect(window.applicationManager.disconnectGeneric).toHaveBeenCalledWith(raft, expect.any(Function), true);
|
|
357
|
+
expect(window.cogManager.removeCog).toHaveBeenCalledWith(raft);
|
|
358
|
+
expect(button.querySelector('.connIssueOverlay')).toBeNull();
|
|
359
|
+
expect(button.style.pointerEvents).toBe('auto');
|
|
360
|
+
expect(button.classList.contains('connectButtonConnected')).toBe(false);
|
|
361
|
+
expect(raft.__connectionIssueDetectedUnsubscribed).toBe(true);
|
|
362
|
+
expect(raft.__connectionIssueResolvedUnsubscribed).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('cancels the Cog countdown when the connection issue resolves', () => {
|
|
366
|
+
const button = createConnectionButton();
|
|
367
|
+
const raft = createCogRaft();
|
|
368
|
+
|
|
369
|
+
UI.setupCogConnectionButton(button, raft);
|
|
370
|
+
raft.__connectionIssueDetectedCallback();
|
|
371
|
+
vi.advanceTimersByTime(30000);
|
|
372
|
+
raft.__connectionIssueResolvedCallback();
|
|
373
|
+
vi.advanceTimersByTime(31000);
|
|
374
|
+
|
|
375
|
+
expect(window.applicationManager.disconnectGeneric).not.toHaveBeenCalled();
|
|
376
|
+
expect(button.querySelector('.connIssueOverlay')).toBeNull();
|
|
377
|
+
expect(button.style.pointerEvents).toBe('auto');
|
|
378
|
+
|
|
379
|
+
raft.__disconnectCallback();
|
|
380
|
+
});
|
|
381
|
+
|
|
252
382
|
it('uses a friendly message when the micro:bit chooser is cancelled', () => {
|
|
253
383
|
const message = UI.getMicroBitConnectionErrorMessage(
|
|
254
384
|
new DOMException('User cancelled the requestDevice() chooser.', 'NotFoundError')
|
|
@@ -293,6 +423,19 @@ function createMartyRaft() {
|
|
|
293
423
|
};
|
|
294
424
|
}
|
|
295
425
|
|
|
426
|
+
function createCogRaft() {
|
|
427
|
+
return {
|
|
428
|
+
id: 'cog-1',
|
|
429
|
+
isVerified: false,
|
|
430
|
+
hasVerifiedConnection() {
|
|
431
|
+
return this.isVerified;
|
|
432
|
+
},
|
|
433
|
+
getFriendlyName: () => 'Cog One',
|
|
434
|
+
getBatteryStrength: () => 70,
|
|
435
|
+
getRSSI: () => -45
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
296
439
|
function createConnectionButton() {
|
|
297
440
|
const button = new FakeElement();
|
|
298
441
|
button.children['.iconButtonContainer'] = new FakeElement(['iconButtonContainer', 'notConnectedButtonContainer']);
|
|
@@ -325,6 +468,7 @@ class FakeElement {
|
|
|
325
468
|
constructor(classes = []) {
|
|
326
469
|
this.classList = new FakeClassList(classes);
|
|
327
470
|
this.children = {};
|
|
471
|
+
this.childNodes = [];
|
|
328
472
|
this.style = {};
|
|
329
473
|
this.attributes = {};
|
|
330
474
|
this.textContent = '';
|
|
@@ -333,7 +477,28 @@ class FakeElement {
|
|
|
333
477
|
}
|
|
334
478
|
|
|
335
479
|
querySelector(selector) {
|
|
336
|
-
|
|
480
|
+
if (this.children[selector]) {
|
|
481
|
+
return this.children[selector];
|
|
482
|
+
}
|
|
483
|
+
if (selector.startsWith('.')) {
|
|
484
|
+
const className = selector.slice(1);
|
|
485
|
+
return this.childNodes.find(child => child.classList.contains(className)) || null;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
appendChild(child) {
|
|
491
|
+
if (!this.childNodes.includes(child)) {
|
|
492
|
+
this.childNodes.push(child);
|
|
493
|
+
}
|
|
494
|
+
child.parentNode = this;
|
|
495
|
+
return child;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
removeChild(child) {
|
|
499
|
+
this.childNodes = this.childNodes.filter(item => item !== child);
|
|
500
|
+
child.parentNode = null;
|
|
501
|
+
return child;
|
|
337
502
|
}
|
|
338
503
|
|
|
339
504
|
setAttribute(name, value) {
|