@robotical/martyblocksjr 4.2.10 → 4.2.11

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.
Files changed (52) hide show
  1. package/editions/free/src/30d09ba32a17082ef820.hex +26350 -0
  2. package/editions/free/src/app.bundle.js +1 -1
  3. package/editions/free/src/app.bundle.js.LICENSE.txt +15 -0
  4. package/editions/free/src/assets/blockicons/microbitbuttona.svg +7 -1
  5. package/editions/free/src/assets/blockicons/microbitbuttonab.svg +8 -1
  6. package/editions/free/src/assets/blockicons/microbitbuttonb.svg +7 -1
  7. package/editions/free/src/assets/blockicons/microbitdisplayclear.svg +9 -25
  8. package/editions/free/src/assets/blockicons/microbitdisplayhappy.svg +13 -31
  9. package/editions/free/src/assets/blockicons/microbitdisplayheart.svg +13 -37
  10. package/editions/free/src/assets/blockicons/microbitdisplaytext.svg +10 -25
  11. package/editions/free/src/assets/blockicons/microbittiltleft.svg +1 -1
  12. package/editions/free/src/assets/blockicons/microbittiltright.svg +1 -1
  13. package/editions/free/src/assets/connection/remove_extension-default.svg +5 -0
  14. package/editions/free/src/css/editor.css +171 -5
  15. package/editions/free/src/css/editorleftpanel.css +87 -12
  16. package/editions/free/src/localizations/bg.json +12 -12
  17. package/editions/free/src/localizations/ca.json +12 -12
  18. package/editions/free/src/localizations/cs.json +12 -12
  19. package/editions/free/src/localizations/cy.json +12 -12
  20. package/editions/free/src/localizations/da.json +12 -12
  21. package/editions/free/src/localizations/de.json +12 -12
  22. package/editions/free/src/localizations/el.json +12 -12
  23. package/editions/free/src/localizations/en.json +34 -14
  24. package/editions/free/src/localizations/es.json +12 -12
  25. package/editions/free/src/localizations/fi.json +12 -12
  26. package/editions/free/src/localizations/fr.json +12 -12
  27. package/editions/free/src/localizations/it.json +12 -12
  28. package/editions/free/src/localizations/ja.json +12 -12
  29. package/editions/free/src/localizations/ko.json +12 -12
  30. package/editions/free/src/localizations/nl.json +12 -12
  31. package/editions/free/src/localizations/no.json +12 -12
  32. package/editions/free/src/localizations/pl.json +12 -12
  33. package/editions/free/src/localizations/pt-br.json +12 -12
  34. package/editions/free/src/localizations/pt.json +12 -12
  35. package/editions/free/src/localizations/sv.json +12 -12
  36. package/editions/free/src/localizations/th.json +12 -12
  37. package/editions/free/src/localizations/tr.json +12 -12
  38. package/editions/free/src/localizations/uk.json +12 -12
  39. package/editions/free/src/localizations/zh-cn.json +12 -12
  40. package/editions/free/src/localizations/zh-tw.json +12 -12
  41. package/package.json +4 -1
  42. package/scripts/prepare-microbit-hex.mjs +47 -0
  43. package/tests/BlockGuideRegistry.test.js +4 -2
  44. package/tests/MicroBitBlockDescriptions.test.js +28 -0
  45. package/tests/MicroBitBlockOptions.test.js +34 -0
  46. package/tests/MicroBitButtonIcons.test.js +24 -0
  47. package/tests/MicroBitDisplayIcons.test.js +64 -0
  48. package/tests/e2e/chromium-79-smoke.test.js +126 -10
  49. package/tests/e2e/marty-connection-ui.e2e.test.js +152 -4
  50. package/tests/e2e/microbit-updater.test.js +126 -0
  51. package/vitest.config.js +1 -0
  52. package/webpack.config.js +4 -0
@@ -1,6 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- const { newHTMLMock } = vi.hoisted(() => ({
3
+ const { microBitUpdaterMocks, newHTMLMock } = vi.hoisted(() => ({
4
+ microBitUpdaterMocks: {
5
+ isSupported: vi.fn(() => false),
6
+ selectAndUpdate: vi.fn()
7
+ },
4
8
  newHTMLMock: vi.fn()
5
9
  }));
6
10
 
@@ -30,7 +34,8 @@ vi.mock('@/painteditor/Paint', () => ({ default: {} }));
30
34
  vi.mock('@/utils/Events', () => ({ default: {} }));
31
35
  vi.mock('@/utils/Localization', () => ({
32
36
  default: {
33
- localize: key => key
37
+ localize: key => key,
38
+ localizeOptional: key => key
34
39
  }
35
40
  }));
36
41
  vi.mock('@/utils/ScratchAudio', () => ({ default: {} }));
@@ -127,6 +132,10 @@ vi.mock('@/editor/ui/Trace', () => ({
127
132
  clear: vi.fn()
128
133
  }
129
134
  }));
135
+ vi.mock('@/microbit/MicroBitUpdater', () => ({
136
+ isMicroBitUpdateSupported: microBitUpdaterMocks.isSupported,
137
+ selectAndUpdateMicroBit: microBitUpdaterMocks.selectAndUpdate
138
+ }));
130
139
 
131
140
  describe('Marty connection UI', () => {
132
141
  let UI;
@@ -136,6 +145,7 @@ describe('Marty connection UI', () => {
136
145
  beforeEach(async () => {
137
146
  vi.useFakeTimers();
138
147
  vi.clearAllMocks();
148
+ microBitUpdaterMocks.isSupported.mockReturnValue(false);
139
149
  global.window = {
140
150
  applicationManager: {
141
151
  disconnectGeneric: vi.fn(raft => {
@@ -148,7 +158,8 @@ describe('Marty connection UI', () => {
148
158
  throw new Error('metadata not ready');
149
159
  }),
150
160
  removeMarty: vi.fn(),
151
- setMartySensorAvailability: vi.fn()
161
+ setMartySensorAvailability: vi.fn(),
162
+ getActiveMarty: vi.fn(() => null)
152
163
  },
153
164
  cogManager: {
154
165
  addCog: vi.fn(),
@@ -158,7 +169,8 @@ describe('Marty connection UI', () => {
158
169
  microBitManager: {
159
170
  addMicroBit: vi.fn(),
160
171
  wireMicroBitWithBlocks: vi.fn(),
161
- removeMicroBit: vi.fn()
172
+ removeMicroBit: vi.fn(),
173
+ getActiveMicroBit: vi.fn(() => null)
162
174
  }
163
175
  };
164
176
  warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
@@ -387,6 +399,70 @@ describe('Marty connection UI', () => {
387
399
  expect(message).toBe('No micro:bit was selected. Click Connect again when you are ready.');
388
400
  });
389
401
 
402
+ it('opens the software-or-connect dialog for the direct web path when WebUSB is available', () => {
403
+ const button = createConnectionButton();
404
+ const openDialogSpy = vi.spyOn(UI, 'openMicroBitConnectionDialog').mockImplementation(() => {});
405
+ microBitUpdaterMocks.isSupported.mockReturnValue(true);
406
+
407
+ UI.connectMicroBit(button);
408
+
409
+ expect(openDialogSpy).toHaveBeenCalledWith(button);
410
+ openDialogSpy.mockRestore();
411
+ });
412
+
413
+ it('keeps the host-managed micro:bit connection path unchanged', () => {
414
+ const button = createConnectionButton();
415
+ const microBit = createMicroBit();
416
+ const connectGenericMicroBit = vi.fn(callback => callback(microBit));
417
+ const openDialogSpy = vi.spyOn(UI, 'openMicroBitConnectionDialog').mockImplementation(() => {});
418
+ window.applicationManager.connectGenericMicroBit = connectGenericMicroBit;
419
+ microBitUpdaterMocks.isSupported.mockReturnValue(true);
420
+
421
+ UI.connectMicroBit(button);
422
+
423
+ expect(connectGenericMicroBit).toHaveBeenCalledOnce();
424
+ expect(openDialogSpy).not.toHaveBeenCalled();
425
+ expect(microBit.__mbjrHostManaged).toBe(true);
426
+ openDialogSpy.mockRestore();
427
+ });
428
+
429
+ it('shows installation success after the WebUSB updater completes', async () => {
430
+ const dialog = createMicroBitUpdateDialog();
431
+ const renderSpy = vi.spyOn(UI, 'renderMicroBitConnectionDialog').mockImplementation(() => {});
432
+ microBitUpdaterMocks.selectAndUpdate.mockImplementation(async progress => progress(0.755));
433
+
434
+ await UI.startMicroBitUpdate(dialog);
435
+
436
+ expect(renderSpy).toHaveBeenNthCalledWith(1, dialog, 'updating');
437
+ expect(dialog.microBitControls.progress.attributes.value).toBe('75.5');
438
+ expect(dialog.microBitControls.progressLabel.textContent).toBe('Installing 75%');
439
+ expect(renderSpy).toHaveBeenNthCalledWith(2, dialog, 'success');
440
+ renderSpy.mockRestore();
441
+ });
442
+
443
+ it('keeps keyboard focus on a visible installer control as its state changes', () => {
444
+ const dialog = createCompleteMicroBitUpdateDialog();
445
+
446
+ UI.renderMicroBitConnectionDialog(dialog, 'updating');
447
+ expect(dialog.microBitControls.progress.focus).toHaveBeenCalledOnce();
448
+
449
+ UI.renderMicroBitConnectionDialog(dialog, 'success');
450
+ expect(dialog.microBitControls.connectButton.focus).toHaveBeenCalledOnce();
451
+
452
+ UI.renderMicroBitConnectionDialog(dialog, 'error');
453
+ expect(dialog.microBitControls.updateButton.focus).toHaveBeenCalledOnce();
454
+ });
455
+
456
+ it('uses the micro:bit five-character friendly name when it is advertised', () => {
457
+ const microBit = {
458
+ getFriendlyName: () => 'BBC micro:bit [vopet]'
459
+ };
460
+
461
+ expect(UI.getMicroBitFriendlyName(microBit)).toBe('BBC micro:bit [vopet]');
462
+ expect(UI.getMicroBitDisplayName(microBit)).toBe('vopet');
463
+ expect(UI.getMicroBitDisplayName({getFriendlyName: () => 'micro:bit'})).toBe('micro:bit');
464
+ });
465
+
390
466
  it('registers micro:bit blocks and restores the button after direct disconnect', () => {
391
467
  const button = createConnectionButton();
392
468
  const oldOnClick = vi.fn();
@@ -398,6 +474,9 @@ describe('Marty connection UI', () => {
398
474
  expect(window.microBitManager.addMicroBit).toHaveBeenCalledWith(microBit);
399
475
  expect(window.microBitManager.wireMicroBitWithBlocks).toHaveBeenCalledWith('microbit-1');
400
476
  expect(button.classList.contains('connectButtonConnected')).toBe(true);
477
+ expect(button.querySelector('.iconButtonContainer').textContent).toBe('DISCONNECT');
478
+ expect(button.querySelector('.raftNameConnectButton').textContent).toBe('micro:bit One');
479
+ expect(button.attributes.title).toBe('Disconnect micro:bit One');
401
480
 
402
481
  button.onclick();
403
482
  vi.advanceTimersByTime(1000);
@@ -407,8 +486,45 @@ describe('Marty connection UI', () => {
407
486
  expect(button.onclick).toBe(oldOnClick);
408
487
  expect(button.classList.contains('connectButtonConnected')).toBe(false);
409
488
  expect(button.querySelector('.iconButtonContainer').classList.contains('notConnectedButtonContainer')).toBe(true);
489
+ expect(button.querySelector('.iconButtonContainer').textContent).toBe('');
410
490
  expect(microBit.__disconnectUnsubscribed).toBe(true);
411
491
  });
492
+
493
+ it('switches from Marty mode to Sprite mode when micro:bit connects without Marty', () => {
494
+ const button = createConnectionButton();
495
+ const microBit = createMicroBit();
496
+ ScratchJr.isMartyModeEnabled = true;
497
+
498
+ UI.setupMicroBitConnectionButton(button, microBit);
499
+
500
+ expect(ScratchJr.isMartyModeEnabled).toBe(false);
501
+ });
502
+
503
+ it('preserves Marty mode when micro:bit connects alongside Marty', () => {
504
+ const button = createConnectionButton();
505
+ const microBit = createMicroBit();
506
+ const marty = createMartyRaft();
507
+ window.martyManager.getActiveMarty.mockReturnValue(marty);
508
+ ScratchJr.isMartyModeEnabled = true;
509
+
510
+ UI.setupMicroBitConnectionButton(button, microBit);
511
+
512
+ expect(ScratchJr.isMartyModeEnabled).toBe(true);
513
+ });
514
+
515
+ it('switches to Sprite mode when Marty disconnect leaves micro:bit connected', () => {
516
+ const button = createConnectionButton();
517
+ const raft = createMartyRaft();
518
+ const microBit = createMicroBit();
519
+ window.microBitManager.getActiveMicroBit.mockReturnValue(microBit);
520
+
521
+ UI.setupMartyConnectionButton(button, raft);
522
+ expect(ScratchJr.isMartyModeEnabled).toBe(true);
523
+
524
+ button.onclick();
525
+
526
+ expect(ScratchJr.isMartyModeEnabled).toBe(false);
527
+ });
412
528
  });
413
529
 
414
530
  function createMartyRaft() {
@@ -464,6 +580,33 @@ function createMicroBit() {
464
580
  return microBit;
465
581
  }
466
582
 
583
+ function createMicroBitUpdateDialog() {
584
+ return {
585
+ microBitControls: {
586
+ progress: new FakeElement(),
587
+ progressLabel: new FakeElement()
588
+ }
589
+ };
590
+ }
591
+
592
+ function createCompleteMicroBitUpdateDialog() {
593
+ const dialog = new FakeElement();
594
+ dialog.microBitControls = {
595
+ closeButton: new FakeElement(),
596
+ title: new FakeElement(),
597
+ message: new FakeElement(),
598
+ progressArea: new FakeElement(),
599
+ progress: new FakeElement(),
600
+ progressLabel: new FakeElement(),
601
+ errorDetails: new FakeElement(),
602
+ firmwareHelp: new FakeElement(),
603
+ backButton: new FakeElement(),
604
+ updateButton: new FakeElement(),
605
+ connectButton: new FakeElement()
606
+ };
607
+ return dialog;
608
+ }
609
+
467
610
  class FakeElement {
468
611
  constructor(classes = []) {
469
612
  this.classList = new FakeClassList(classes);
@@ -474,6 +617,7 @@ class FakeElement {
474
617
  this.textContent = '';
475
618
  this.innerHTML = '';
476
619
  this.onclick = null;
620
+ this.focus = vi.fn();
477
621
  }
478
622
 
479
623
  querySelector(selector) {
@@ -504,6 +648,10 @@ class FakeElement {
504
648
  setAttribute(name, value) {
505
649
  this.attributes[name] = value;
506
650
  }
651
+
652
+ getAttribute(name) {
653
+ return this.attributes[name];
654
+ }
507
655
  }
508
656
 
509
657
  class FakeClassList {
@@ -0,0 +1,126 @@
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ dapLinks: [],
5
+ flashError: null,
6
+ isUniversalHex: vi.fn(() => true),
7
+ separateUniversalHex: vi.fn(() => [
8
+ {boardId: 0x9900},
9
+ {boardId: 0x9903}
10
+ ]),
11
+ webUsbDevices: []
12
+ }));
13
+
14
+ vi.mock('@microbit/microbit-universal-hex', () => ({
15
+ isUniversalHex: mocks.isUniversalHex,
16
+ separateUniversalHex: mocks.separateUniversalHex
17
+ }));
18
+
19
+ vi.mock('dapjs', () => {
20
+ class WebUSB {
21
+ constructor (device) {
22
+ this.device = device;
23
+ mocks.webUsbDevices.push(device);
24
+ }
25
+ }
26
+
27
+ class DAPLink {
28
+ static EVENT_PROGRESS = 'progress';
29
+
30
+ constructor (transport) {
31
+ this.transport = transport;
32
+ this.connected = false;
33
+ this.on = vi.fn();
34
+ this.connect = vi.fn(async () => {
35
+ this.connected = true;
36
+ });
37
+ this.flash = vi.fn(async () => {
38
+ if (mocks.flashError) {
39
+ throw mocks.flashError;
40
+ }
41
+ });
42
+ this.disconnect = vi.fn(async () => {
43
+ this.connected = false;
44
+ });
45
+ mocks.dapLinks.push(this);
46
+ }
47
+ }
48
+
49
+ return {DAPLink, WebUSB};
50
+ });
51
+
52
+ describe('MicroBitUpdater', () => {
53
+ let updater;
54
+ let requestDevice;
55
+
56
+ beforeEach(async () => {
57
+ vi.clearAllMocks();
58
+ mocks.dapLinks.length = 0;
59
+ mocks.flashError = null;
60
+ mocks.webUsbDevices.length = 0;
61
+ mocks.isUniversalHex.mockReturnValue(true);
62
+ mocks.separateUniversalHex.mockReturnValue([
63
+ {boardId: 0x9900},
64
+ {boardId: 0x9903}
65
+ ]);
66
+ requestDevice = vi.fn();
67
+ vi.stubGlobal('navigator', {usb: {requestDevice}});
68
+ vi.stubGlobal('fetch', vi.fn(async () => ({
69
+ ok: true,
70
+ text: async () => 'universal micro:bit hex'
71
+ })));
72
+ updater = await import('@/microbit/MicroBitUpdater.js');
73
+ });
74
+
75
+ afterEach(() => {
76
+ vi.unstubAllGlobals();
77
+ });
78
+
79
+ it('selects the micro:bit USB interface and flashes the universal HEX', async () => {
80
+ const device = {serialNumber: '9903ABCDEF'};
81
+ const progress = vi.fn();
82
+ requestDevice.mockResolvedValue(device);
83
+
84
+ await updater.selectAndUpdateMicroBit(progress);
85
+
86
+ expect(requestDevice).toHaveBeenCalledWith({
87
+ filters: [{vendorId: 0x0d28, productId: 0x0204}]
88
+ });
89
+ expect(mocks.webUsbDevices).toEqual([device]);
90
+ expect(mocks.dapLinks).toHaveLength(1);
91
+ const dapLink = mocks.dapLinks[0];
92
+ expect(dapLink.on).toHaveBeenCalledWith('progress', progress);
93
+ expect(dapLink.connect).toHaveBeenCalledOnce();
94
+ expect(dapLink.flash).toHaveBeenCalledOnce();
95
+ expect(dapLink.flash.mock.calls[0][0]).toEqual(new TextEncoder().encode('universal micro:bit hex'));
96
+ expect(dapLink.disconnect).toHaveBeenCalledOnce();
97
+ });
98
+
99
+ it('rejects unsupported board identifiers before connecting to DAPLink', async () => {
100
+ requestDevice.mockResolvedValue({serialNumber: '1234ABCDEF'});
101
+
102
+ await expect(updater.selectAndUpdateMicroBit()).rejects.toThrow(
103
+ 'Could not identify the micro:bit board version.'
104
+ );
105
+
106
+ expect(mocks.dapLinks[0].connect).not.toHaveBeenCalled();
107
+ expect(mocks.dapLinks[0].flash).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('reports when the browser does not expose WebUSB', () => {
111
+ vi.stubGlobal('navigator', {});
112
+
113
+ expect(updater.isMicroBitUpdateSupported()).toBe(false);
114
+ return expect(updater.selectAndUpdateMicroBit()).rejects.toThrow(
115
+ 'Installing micro:bit software is not supported in this browser.'
116
+ );
117
+ });
118
+
119
+ it('disconnects DAPLink when flashing fails', async () => {
120
+ requestDevice.mockResolvedValue({serialNumber: '9901ABCDEF'});
121
+ mocks.flashError = new Error('flash failed');
122
+
123
+ await expect(updater.selectAndUpdateMicroBit()).rejects.toThrow('flash failed');
124
+ expect(mocks.dapLinks[0].disconnect).toHaveBeenCalledOnce();
125
+ });
126
+ });
package/vitest.config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
 
3
3
  module.exports = {
4
+ assetsInclude: ['**/*.hex'],
4
5
  resolve: {
5
6
  alias: {
6
7
  '@': path.resolve(__dirname, './src')
package/webpack.config.js CHANGED
@@ -49,6 +49,10 @@ module.exports = {
49
49
  test: /\.wasm$/,
50
50
  type: "javascript/auto",
51
51
  },
52
+ {
53
+ test: /\.hex$/,
54
+ type: "asset/resource",
55
+ },
52
56
  {
53
57
  test: /\.js$/,
54
58
  include: /node_modules/,