@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@robotical/martyblocksjr",
3
- "version": "4.2.3",
3
+ "version": "4.2.4",
4
4
  "description": "ScratchJr",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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: vi.fn(),
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
- unsubscribe: vi.fn()
81
- }),
82
- createRaftConnectionIssueResolvedHelper: () => ({
83
- subscribe: vi.fn(),
84
- unsubscribe: vi.fn()
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
- raftVerifiedSubscriptionHelper: () => ({
87
- subscribe: vi.fn(),
88
- unsubscribe: vi.fn()
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
- return this.children[selector] || null;
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) {