@robotical/martyblocksjr 4.1.1 → 4.2.0

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 (62) hide show
  1. package/.firebase/hosting.ZWRpdGlvbnMvZnJlZS9zcmM.cache +599 -534
  2. package/editions/free/src/app.bundle.js +1 -1
  3. package/editions/free/src/assets/blockicons/microbitbuttona.svg +33 -0
  4. package/editions/free/src/assets/blockicons/microbitbuttonab.svg +33 -0
  5. package/editions/free/src/assets/blockicons/microbitbuttonb.svg +33 -0
  6. package/editions/free/src/assets/blockicons/microbitdisplayclear.svg +29 -0
  7. package/editions/free/src/assets/blockicons/microbitdisplayhappy.svg +36 -0
  8. package/editions/free/src/assets/blockicons/microbitdisplayheart.svg +42 -0
  9. package/editions/free/src/assets/blockicons/microbitdisplaytext.svg +29 -0
  10. package/editions/free/src/assets/blockicons/microbitgesturejumped.svg +40 -0
  11. package/editions/free/src/assets/blockicons/microbitgesturemoved.svg +41 -0
  12. package/editions/free/src/assets/blockicons/microbitgestureshaken.svg +45 -0
  13. package/editions/free/src/assets/blockicons/microbittiltany.svg +45 -0
  14. package/editions/free/src/assets/blockicons/microbittiltbackward.svg +39 -0
  15. package/editions/free/src/assets/blockicons/microbittiltforward.svg +39 -0
  16. package/editions/free/src/assets/blockicons/microbittiltleft.svg +39 -0
  17. package/editions/free/src/assets/blockicons/microbittiltright.svg +39 -0
  18. package/editions/free/src/assets/categories/MicroBitLooksOff.svg +17 -0
  19. package/editions/free/src/assets/categories/MicroBitLooksOn.svg +17 -0
  20. package/editions/free/src/assets/categories/MicroBitStartOff.svg +17 -0
  21. package/editions/free/src/assets/categories/MicroBitStartOn.svg +17 -0
  22. package/editions/free/src/css/editor.css +243 -1
  23. package/editions/free/src/css/editorleftpanel.css +70 -9
  24. package/editions/free/src/localizations/bg.json +9 -1
  25. package/editions/free/src/localizations/ca.json +9 -1
  26. package/editions/free/src/localizations/cs.json +9 -1
  27. package/editions/free/src/localizations/cy.json +9 -1
  28. package/editions/free/src/localizations/da.json +9 -1
  29. package/editions/free/src/localizations/de.json +9 -1
  30. package/editions/free/src/localizations/el.json +9 -1
  31. package/editions/free/src/localizations/en.json +9 -1
  32. package/editions/free/src/localizations/es.json +9 -1
  33. package/editions/free/src/localizations/fi.json +9 -1
  34. package/editions/free/src/localizations/fr.json +9 -1
  35. package/editions/free/src/localizations/it.json +9 -1
  36. package/editions/free/src/localizations/ja.json +9 -1
  37. package/editions/free/src/localizations/ko.json +9 -1
  38. package/editions/free/src/localizations/nl.json +9 -1
  39. package/editions/free/src/localizations/no.json +9 -1
  40. package/editions/free/src/localizations/pl.json +9 -1
  41. package/editions/free/src/localizations/pt-br.json +9 -1
  42. package/editions/free/src/localizations/pt.json +9 -1
  43. package/editions/free/src/localizations/sv.json +9 -1
  44. package/editions/free/src/localizations/th.json +9 -1
  45. package/editions/free/src/localizations/tr.json +9 -1
  46. package/editions/free/src/localizations/uk.json +9 -1
  47. package/editions/free/src/localizations/zh-cn.json +9 -1
  48. package/editions/free/src/localizations/zh-tw.json +9 -1
  49. package/editions/free/src/media.json +12 -0
  50. package/editions/free/src/pnglibrary/BeeSprite1.png +0 -0
  51. package/editions/free/src/svglibrary/BeeSprite1.svg +4 -0
  52. package/editions/free/src/svglibrary/BeeSprite2.svg +4 -0
  53. package/editions/free/src/svglibrary/BeeSprite3.svg +4 -0
  54. package/package.json +1 -1
  55. package/tests/e2e/chromium-79-smoke.test.js +226 -0
  56. package/tests/e2e/marty-connection-ui.e2e.test.js +56 -0
  57. package/tests/e2e/microbit-blocks.test.js +162 -0
  58. package/tests/e2e/microbit-extension-project-reload.e2e.test.js +304 -0
  59. package/tests/e2e/microbit-web-bluetooth.test.js +131 -0
  60. package/tests/e2e/page-background-loading.test.js +152 -0
  61. package/tests/e2e/project-load-progress-loop.e2e.test.js +305 -0
  62. package/vitest.config.js +2 -3
@@ -122,6 +122,232 @@ describe("Chromium 79 smoke test", () => {
122
122
  60_000
123
123
  );
124
124
 
125
+ it(
126
+ "hides micro:bit controls until the extension is added",
127
+ async () => {
128
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
129
+
130
+ try {
131
+ await page.waitForSelector("#addExtensionButton", { timeout: 30_000 });
132
+
133
+ const initialState = await page.evaluate(() => {
134
+ const addButton = document.getElementById("addExtensionButton");
135
+ const microBitButton = document.getElementById("microBitConnectionButton");
136
+ return {
137
+ addButtonDisplay: window.getComputedStyle(addButton).display,
138
+ addButtonAriaHidden: addButton.getAttribute("aria-hidden"),
139
+ addButtonDisabled: addButton.disabled,
140
+ microBitButtonDisplay: window.getComputedStyle(microBitButton).display,
141
+ microBitButtonAriaHidden: microBitButton.getAttribute("aria-hidden"),
142
+ microBitButtonDisabled: microBitButton.disabled,
143
+ rightCategoryIds: Array.from(document.querySelectorAll("#selectorsright .category-selector-button")).map(
144
+ (button) => button.id
145
+ ),
146
+ extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
147
+ };
148
+ });
149
+
150
+ expect(initialState).toEqual({
151
+ addButtonDisplay: "flex",
152
+ addButtonAriaHidden: "false",
153
+ addButtonDisabled: false,
154
+ microBitButtonDisplay: "none",
155
+ microBitButtonAriaHidden: "true",
156
+ microBitButtonDisabled: true,
157
+ rightCategoryIds: ["cog-start", "cog-looks", "cog-sound"],
158
+ extensionEnabled: false,
159
+ });
160
+
161
+ await page.click("#addExtensionButton");
162
+ await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
163
+ const libraryState = await page.evaluate(() => ({
164
+ title: document.getElementById("extensionsLibraryTitle").textContent.trim(),
165
+ hasMicroBitCard: Boolean(document.getElementById("microBitExtensionCard")),
166
+ microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
167
+ microBitStillHidden:
168
+ window.getComputedStyle(document.getElementById("microBitConnectionButton")).display === "none",
169
+ }));
170
+
171
+ expect(libraryState).toEqual({
172
+ title: "Extensions",
173
+ hasMicroBitCard: true,
174
+ microBitCardAction: "+",
175
+ microBitStillHidden: true,
176
+ });
177
+
178
+ await page.click("#microBitExtensionCard");
179
+ await page.waitForFunction(
180
+ () => {
181
+ const microBitButton = document.getElementById("microBitConnectionButton");
182
+ const library = document.getElementById("extensionsLibrary");
183
+ return microBitButton
184
+ && window.getComputedStyle(microBitButton).display !== "none"
185
+ && library
186
+ && !library.classList.contains("in");
187
+ },
188
+ { timeout: 30_000 }
189
+ );
190
+
191
+ const enabledState = await page.evaluate(() => {
192
+ const addButton = document.getElementById("addExtensionButton");
193
+ const microBitButton = document.getElementById("microBitConnectionButton");
194
+ return {
195
+ addButtonDisplay: window.getComputedStyle(addButton).display,
196
+ addButtonAriaHidden: addButton.getAttribute("aria-hidden"),
197
+ addButtonDisabled: addButton.disabled,
198
+ microBitButtonDisplay: window.getComputedStyle(microBitButton).display,
199
+ microBitButtonAriaHidden: microBitButton.getAttribute("aria-hidden"),
200
+ microBitButtonDisabled: microBitButton.disabled,
201
+ rightCategoryIds: Array.from(document.querySelectorAll("#selectorsright .category-selector-button")).map(
202
+ (button) => button.id
203
+ ),
204
+ selectedRightCategoryIds: Array.from(
205
+ document.querySelectorAll('#selectorsright .category-selector-button[aria-pressed="true"]')
206
+ ).map((button) => button.id),
207
+ paletteBlockTypes: Array.from(document.querySelectorAll("#palette > div")).map(
208
+ (block) => block.owner && block.owner.blocktype
209
+ ),
210
+ extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
211
+ };
212
+ });
213
+
214
+ expect(enabledState).toEqual({
215
+ addButtonDisplay: "flex",
216
+ addButtonAriaHidden: "false",
217
+ addButtonDisabled: false,
218
+ microBitButtonDisplay: "flex",
219
+ microBitButtonAriaHidden: "false",
220
+ microBitButtonDisabled: false,
221
+ rightCategoryIds: ["cog-start", "cog-looks", "cog-sound", "microbit-start", "microbit-looks"],
222
+ selectedRightCategoryIds: ["microbit-start"],
223
+ paletteBlockTypes: [
224
+ "microbitbuttonpressed",
225
+ "microbitgesture",
226
+ "microbittilted",
227
+ ],
228
+ extensionEnabled: true,
229
+ });
230
+
231
+ await page.click("#microbit-looks");
232
+ await page.waitForFunction(
233
+ () => Array.from(document.querySelectorAll("#palette > div")).some(
234
+ (block) => block.owner && block.owner.blocktype === "microbitdisplaycustom"
235
+ ),
236
+ { timeout: 30_000 }
237
+ );
238
+ const looksPaletteBlockTypes = await page.evaluate(() =>
239
+ Array.from(document.querySelectorAll("#palette > div")).map((block) => block.owner && block.owner.blocktype)
240
+ );
241
+ expect(looksPaletteBlockTypes).toEqual([
242
+ "microbitdisplayheart",
243
+ "microbitdisplayhappy",
244
+ "microbitdisplaycustom",
245
+ "microbitdisplaytext",
246
+ "microbitdisplayclear",
247
+ ]);
248
+
249
+ const customMatrixState = await page.evaluate(() => {
250
+ const scripts = window.ScratchJr.getActiveScript().owner;
251
+ const block = scripts.insertKeyboardBlock(null, [[
252
+ "microbitdisplaycustom",
253
+ "00000/00000/00000/00000/00000",
254
+ 50,
255
+ 50,
256
+ ]]);
257
+ block.arg.pressMicroBitMatrixEditor({
258
+ preventDefault() {},
259
+ stopPropagation() {},
260
+ });
261
+ const cells = Array.from(document.querySelectorAll(".microbitMatrixCell"));
262
+ cells[12].dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
263
+ return {
264
+ argValue: block.getArgValue(),
265
+ cellCount: cells.length,
266
+ menuOpen: Boolean(document.querySelector(".microbitMatrixMenu")),
267
+ activeCells: Array.from(document.querySelectorAll(".microbitMatrixCell.on")).length,
268
+ };
269
+ });
270
+ expect(customMatrixState).toEqual({
271
+ argValue: "00000/00000/00100/00000/00000",
272
+ cellCount: 25,
273
+ menuOpen: true,
274
+ activeCells: 1,
275
+ });
276
+
277
+ const blocksAfterInsert = await page.evaluate(() => {
278
+ const scripts = window.ScratchJr.getActiveScript().owner;
279
+ scripts.insertKeyboardBlock(null, [["microbitdisplayclear", "null", 20, 20]]);
280
+ return window.ScratchJr.getBlocks().map((block) => block.blocktype);
281
+ });
282
+ expect(blocksAfterInsert).toContain("microbitdisplayclear");
283
+
284
+ await page.click("#addExtensionButton");
285
+ await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
286
+ const loadedLibraryState = await page.evaluate(() => ({
287
+ microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
288
+ microBitCardLoaded: document.getElementById("microBitExtensionCard").classList.contains("loaded"),
289
+ }));
290
+ expect(loadedLibraryState).toEqual({
291
+ microBitCardAction: "-",
292
+ microBitCardLoaded: true,
293
+ });
294
+
295
+ await page.click("#microBitExtensionCard");
296
+ await page.waitForSelector("#microBitExtensionRemoveWarning.show", { timeout: 30_000 });
297
+ await page.click("#microBitExtensionRemoveCancel");
298
+ await page.waitForFunction(
299
+ () => !document.getElementById("microBitExtensionRemoveWarning").classList.contains("show"),
300
+ { timeout: 30_000 }
301
+ );
302
+ const cancelUnloadState = await page.evaluate(() => ({
303
+ microBitButtonDisplay: window.getComputedStyle(document.getElementById("microBitConnectionButton")).display,
304
+ hasMicroBitBlock: window.ScratchJr.getBlocks().some((block) => block.blocktype === "microbitdisplayclear"),
305
+ }));
306
+ expect(cancelUnloadState).toEqual({
307
+ microBitButtonDisplay: "flex",
308
+ hasMicroBitBlock: true,
309
+ });
310
+
311
+ await page.click("#microBitExtensionCard");
312
+ await page.waitForSelector("#microBitExtensionRemoveWarning.show", { timeout: 30_000 });
313
+ await page.click("#microBitExtensionRemoveConfirm");
314
+ await page.waitForFunction(
315
+ () => window.getComputedStyle(document.getElementById("microBitConnectionButton")).display === "none"
316
+ && !document.querySelector("#selectorsright #microbit-start")
317
+ && !document.getElementById("extensionsLibrary").classList.contains("in"),
318
+ { timeout: 30_000 }
319
+ );
320
+ const unloadedState = await page.evaluate(() => ({
321
+ addButtonDisplay: window.getComputedStyle(document.getElementById("addExtensionButton")).display,
322
+ microBitButtonDisplay: window.getComputedStyle(document.getElementById("microBitConnectionButton")).display,
323
+ extensionsLibraryOpen: document.getElementById("extensionsLibrary").classList.contains("in"),
324
+ microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
325
+ microBitCardLoaded: document.getElementById("microBitExtensionCard").classList.contains("loaded"),
326
+ rightCategoryIds: Array.from(document.querySelectorAll("#selectorsright .category-selector-button")).map(
327
+ (button) => button.id
328
+ ),
329
+ hasMicroBitBlock: window.ScratchJr.getBlocks().some((block) => block.blocktype.indexOf("microbit") === 0),
330
+ extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
331
+ }));
332
+ expect(unloadedState).toEqual({
333
+ addButtonDisplay: "flex",
334
+ microBitButtonDisplay: "none",
335
+ extensionsLibraryOpen: false,
336
+ microBitCardAction: "+",
337
+ microBitCardLoaded: false,
338
+ rightCategoryIds: ["cog-start", "cog-looks", "cog-sound"],
339
+ hasMicroBitBlock: false,
340
+ extensionEnabled: false,
341
+ });
342
+
343
+ expect(errors).toEqual([]);
344
+ } finally {
345
+ await browser.close();
346
+ }
347
+ },
348
+ 60_000
349
+ );
350
+
125
351
  it(
126
352
  "opens tutorials from the book tab and returns there from tutorial mode",
127
353
  async () => {
@@ -65,6 +65,7 @@ vi.mock('@/utils/accessibility', () => ({
65
65
  setSelectedState: vi.fn()
66
66
  }));
67
67
  vi.mock('@/html-svgs/cog', () => ({ cogSvg: '<svg></svg>' }));
68
+ vi.mock('@/html-svgs/microbit', () => ({ microBitSvg: '<svg></svg>' }));
68
69
  vi.mock('@/html-svgs/sprite', () => ({ spriteSvg: '<svg></svg>' }));
69
70
  vi.mock('@/html-svgs/marty', () => ({ martySvg: '<svg></svg>' }));
70
71
  vi.mock('@/html-svgs/sprite-deselected', () => ({ spriteDeselectedSvg: '<svg></svg>' }));
@@ -126,6 +127,11 @@ describe('Marty connection UI', () => {
126
127
  }),
127
128
  removeMarty: vi.fn(),
128
129
  setMartySensorAvailability: vi.fn()
130
+ },
131
+ microBitManager: {
132
+ addMicroBit: vi.fn(),
133
+ wireMicroBitWithBlocks: vi.fn(),
134
+ removeMicroBit: vi.fn()
129
135
  }
130
136
  };
131
137
  warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
@@ -242,6 +248,37 @@ describe('Marty connection UI', () => {
242
248
  expect(button.classList.contains('connectButtonConnected')).toBe(true);
243
249
  expect(button.querySelector('.iconButtonContainer').classList.contains('connectedButtonContainer')).toBe(true);
244
250
  });
251
+
252
+ it('uses a friendly message when the micro:bit chooser is cancelled', () => {
253
+ const message = UI.getMicroBitConnectionErrorMessage(
254
+ new DOMException('User cancelled the requestDevice() chooser.', 'NotFoundError')
255
+ );
256
+
257
+ expect(message).toBe('No micro:bit was selected. Click Connect again when you are ready.');
258
+ });
259
+
260
+ it('registers micro:bit blocks and restores the button after direct disconnect', () => {
261
+ const button = createConnectionButton();
262
+ const oldOnClick = vi.fn();
263
+ button.onclick = oldOnClick;
264
+ const microBit = createMicroBit();
265
+
266
+ UI.setupMicroBitConnectionButton(button, microBit);
267
+
268
+ expect(window.microBitManager.addMicroBit).toHaveBeenCalledWith(microBit);
269
+ expect(window.microBitManager.wireMicroBitWithBlocks).toHaveBeenCalledWith('microbit-1');
270
+ expect(button.classList.contains('connectButtonConnected')).toBe(true);
271
+
272
+ button.onclick();
273
+ vi.advanceTimersByTime(1000);
274
+
275
+ expect(microBit.disconnect).toHaveBeenCalled();
276
+ expect(window.microBitManager.removeMicroBit).toHaveBeenCalledWith(microBit);
277
+ expect(button.onclick).toBe(oldOnClick);
278
+ expect(button.classList.contains('connectButtonConnected')).toBe(false);
279
+ expect(button.querySelector('.iconButtonContainer').classList.contains('notConnectedButtonContainer')).toBe(true);
280
+ expect(microBit.__disconnectUnsubscribed).toBe(true);
281
+ });
245
282
  });
246
283
 
247
284
  function createMartyRaft() {
@@ -265,6 +302,25 @@ function createConnectionButton() {
265
302
  return button;
266
303
  }
267
304
 
305
+ function createMicroBit() {
306
+ const microBit = {
307
+ id: 'microbit-1',
308
+ getFriendlyName: () => 'micro:bit One',
309
+ disconnect: vi.fn(() => {
310
+ if (microBit.__disconnectCallback) {
311
+ microBit.__disconnectCallback(microBit);
312
+ }
313
+ }),
314
+ addDisconnectListener: vi.fn(callback => {
315
+ microBit.__disconnectCallback = callback;
316
+ return () => {
317
+ microBit.__disconnectUnsubscribed = true;
318
+ };
319
+ })
320
+ };
321
+ return microBit;
322
+ }
323
+
268
324
  class FakeElement {
269
325
  constructor(classes = []) {
270
326
  this.classList = new FakeClassList(classes);
@@ -0,0 +1,162 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { mockPrims } = vi.hoisted(() => ({
4
+ mockPrims: {
5
+ OnMicroBitEvent: vi.fn()
6
+ }
7
+ }));
8
+
9
+ vi.mock('@/editor/engine/Prims', () => ({
10
+ default: mockPrims
11
+ }));
12
+
13
+ vi.mock('../../src/editor/engine/Prims', () => ({
14
+ default: mockPrims
15
+ }));
16
+
17
+ describe('MicroBitBlocks', () => {
18
+ let MicroBitBlocks;
19
+ let Prims;
20
+ let sensorListener;
21
+ let blocks;
22
+
23
+ beforeEach(async () => {
24
+ vi.clearAllMocks();
25
+ sensorListener = null;
26
+ blocks = null;
27
+ const [blocksModule, primsModule] = await Promise.all([
28
+ import('@/microbit/MicroBitBlocks.js'),
29
+ import('@/editor/engine/Prims')
30
+ ]);
31
+ MicroBitBlocks = blocksModule.default;
32
+ Prims = primsModule.default;
33
+ });
34
+
35
+ afterEach(() => {
36
+ if (blocks) {
37
+ blocks.destroy();
38
+ }
39
+ vi.useRealTimers();
40
+ });
41
+
42
+ it('emits individual button events after the combo window', () => {
43
+ vi.useFakeTimers();
44
+ blocks = new MicroBitBlocks(createMicroBit());
45
+
46
+ sensorListener(
47
+ createSensors({buttonA: 1}),
48
+ createSensors({buttonA: 0})
49
+ );
50
+
51
+ expect(Prims.OnMicroBitEvent).not.toHaveBeenCalled();
52
+
53
+ vi.advanceTimersByTime(120);
54
+
55
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledWith('microbitbuttona');
56
+ expect(Prims.OnMicroBitEvent).not.toHaveBeenCalledWith('microbitbuttonab');
57
+ });
58
+
59
+ it('emits A+B button events when both buttons are pressed together', () => {
60
+ blocks = new MicroBitBlocks(createMicroBit());
61
+
62
+ sensorListener(
63
+ createSensors({buttonA: 1, buttonB: 1}),
64
+ createSensors({buttonA: 0, buttonB: 0})
65
+ );
66
+
67
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledWith('microbitbuttonab');
68
+ expect(Prims.OnMicroBitEvent).not.toHaveBeenCalledWith('microbitbuttona');
69
+ expect(Prims.OnMicroBitEvent).not.toHaveBeenCalledWith('microbitbuttonb');
70
+ });
71
+
72
+ it('promotes a near-simultaneous second button press to A+B', () => {
73
+ vi.useFakeTimers();
74
+ blocks = new MicroBitBlocks(createMicroBit());
75
+
76
+ sensorListener(
77
+ createSensors({buttonA: 1}),
78
+ createSensors({buttonA: 0})
79
+ );
80
+ vi.advanceTimersByTime(60);
81
+ sensorListener(
82
+ createSensors({buttonA: 1, buttonB: 1}),
83
+ createSensors({buttonA: 1, buttonB: 0})
84
+ );
85
+ vi.advanceTimersByTime(120);
86
+
87
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledTimes(1);
88
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledWith('microbitbuttonab');
89
+ });
90
+
91
+ it('emits gesture events on gesture bit transitions', () => {
92
+ blocks = new MicroBitBlocks(createMicroBit());
93
+
94
+ sensorListener(
95
+ createSensors({gestureState: 1}),
96
+ createSensors({gestureState: 0})
97
+ );
98
+
99
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledWith('microbitgestureshaken');
100
+ });
101
+
102
+ it('emits tilt events on every tilted sensor update', () => {
103
+ blocks = new MicroBitBlocks(createMicroBit());
104
+
105
+ sensorListener(
106
+ createSensors({tiltX: -180}),
107
+ createSensors({tiltX: 0})
108
+ );
109
+ sensorListener(
110
+ createSensors({tiltX: -180}),
111
+ createSensors({tiltX: -180})
112
+ );
113
+
114
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledWith('microbittiltany');
115
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledWith('microbittiltleft');
116
+ expect(Prims.OnMicroBitEvent).toHaveBeenCalledTimes(4);
117
+ });
118
+
119
+ it('sends matrix rows for display symbols', () => {
120
+ const microBit = createMicroBit();
121
+ blocks = new MicroBitBlocks(microBit);
122
+
123
+ blocks.displaySymbol('microbitdisplayheart');
124
+
125
+ expect(microBit.displayMatrix).toHaveBeenCalledWith(new Uint8Array([10, 31, 31, 14, 4]));
126
+ });
127
+
128
+ it('sends matrix rows for custom display patterns', () => {
129
+ const microBit = createMicroBit();
130
+ blocks = new MicroBitBlocks(microBit);
131
+
132
+ blocks.displayPattern('10000/01000/00100/00010/00001');
133
+
134
+ expect(microBit.displayMatrix).toHaveBeenCalledWith(new Uint8Array([1, 2, 4, 8, 16]));
135
+ });
136
+
137
+ function createMicroBit(sensors = createSensors()) {
138
+ return {
139
+ sensors,
140
+ addSensorListener: vi.fn(callback => {
141
+ sensorListener = callback;
142
+ return vi.fn();
143
+ }),
144
+ isConnected: () => true,
145
+ displayMatrix: vi.fn(),
146
+ displayText: vi.fn(),
147
+ clearDisplay: vi.fn()
148
+ };
149
+ }
150
+
151
+ function createSensors(overrides = {}) {
152
+ return {
153
+ tiltX: 0,
154
+ tiltY: 0,
155
+ buttonA: 0,
156
+ buttonB: 0,
157
+ touchPins: [0, 0, 0],
158
+ gestureState: 0,
159
+ ...overrides
160
+ };
161
+ }
162
+ });