@robotical/martyblocksjr 4.1.2 → 4.2.1

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 (77) 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/assets/ui/viewOnCompact.png +0 -0
  23. package/editions/free/src/assets/ui/viewOnTargetCompact.png +0 -0
  24. package/editions/free/src/css/editor.css +334 -1
  25. package/editions/free/src/css/editorleftpanel.css +86 -26
  26. package/editions/free/src/css/editorstage.css +62 -0
  27. package/editions/free/src/css/paintlook.css +23 -0
  28. package/editions/free/src/localizations/bg.json +10 -1
  29. package/editions/free/src/localizations/ca.json +10 -1
  30. package/editions/free/src/localizations/cs.json +10 -1
  31. package/editions/free/src/localizations/cy.json +10 -1
  32. package/editions/free/src/localizations/da.json +10 -1
  33. package/editions/free/src/localizations/de.json +10 -1
  34. package/editions/free/src/localizations/el.json +10 -1
  35. package/editions/free/src/localizations/en.json +10 -1
  36. package/editions/free/src/localizations/es.json +10 -1
  37. package/editions/free/src/localizations/fi.json +10 -1
  38. package/editions/free/src/localizations/fr.json +10 -1
  39. package/editions/free/src/localizations/it.json +10 -1
  40. package/editions/free/src/localizations/ja.json +10 -1
  41. package/editions/free/src/localizations/ko.json +10 -1
  42. package/editions/free/src/localizations/nl.json +10 -1
  43. package/editions/free/src/localizations/no.json +10 -1
  44. package/editions/free/src/localizations/pl.json +10 -1
  45. package/editions/free/src/localizations/pt-br.json +10 -1
  46. package/editions/free/src/localizations/pt.json +10 -1
  47. package/editions/free/src/localizations/sv.json +10 -1
  48. package/editions/free/src/localizations/th.json +10 -1
  49. package/editions/free/src/localizations/tr.json +10 -1
  50. package/editions/free/src/localizations/uk.json +10 -1
  51. package/editions/free/src/localizations/zh-cn.json +10 -1
  52. package/editions/free/src/localizations/zh-tw.json +10 -1
  53. package/editions/free/src/media.json +15 -0
  54. package/editions/free/src/pnglibrary/BeeSprite1.png +0 -0
  55. package/editions/free/src/svglibrary/BeeSprite1.svg +4 -0
  56. package/editions/free/src/svglibrary/BeeSprite2.svg +4 -0
  57. package/editions/free/src/svglibrary/BeeSprite3.svg +4 -0
  58. package/package.json +1 -1
  59. package/tests/e2e/accessibility.e2e.test.js +18 -2
  60. package/tests/e2e/animated-sprite-paint-disabled.e2e.test.js +270 -0
  61. package/tests/e2e/background-paint-active-selection.e2e.test.js +162 -0
  62. package/tests/e2e/blocks-jr-hidden-blocks.e2e.test.js +262 -0
  63. package/tests/e2e/chromium-79-smoke.test.js +235 -0
  64. package/tests/e2e/delete-last-sprite-mode.e2e.test.js +162 -0
  65. package/tests/e2e/duplicate-page.e2e.test.js +414 -0
  66. package/tests/e2e/marty-connection-ui.e2e.test.js +56 -0
  67. package/tests/e2e/marty-script-activation-paths.e2e.test.js +522 -0
  68. package/tests/e2e/microbit-blocks.test.js +162 -0
  69. package/tests/e2e/microbit-extension-project-reload.e2e.test.js +309 -0
  70. package/tests/e2e/microbit-web-bluetooth.test.js +131 -0
  71. package/tests/e2e/page-background-loading.test.js +152 -0
  72. package/tests/e2e/paint-editor-cancel.e2e.test.js +274 -0
  73. package/tests/e2e/project-load-progress-loop.e2e.test.js +310 -0
  74. package/tests/e2e/project-save-stall.e2e.test.js +230 -0
  75. package/tests/e2e/sprite-watermark-grayscale.e2e.test.js +269 -0
  76. package/tests/e2e/top-toolbar-tooltips.e2e.test.js +152 -0
  77. package/vitest.config.js +2 -3
@@ -108,6 +108,15 @@ async function assertTutorialOpened(page, tutorialId) {
108
108
  expect(tutorialState.tutorialTitle.length).toBeGreaterThan(0);
109
109
  }
110
110
 
111
+ async function openHiddenExtensionsLibrary(page) {
112
+ await page.$eval("#addExtensionButton", (button) => {
113
+ if (!button.disabled || window.getComputedStyle(button).display !== "none") {
114
+ throw new Error("Expected the extensions entry point to remain hidden and disabled");
115
+ }
116
+ button.onclick();
117
+ });
118
+ }
119
+
111
120
  describe("Chromium 79 smoke test", () => {
112
121
  it(
113
122
  "loads home page without console errors",
@@ -122,6 +131,232 @@ describe("Chromium 79 smoke test", () => {
122
131
  60_000
123
132
  );
124
133
 
134
+ it(
135
+ "keeps the extension entry hidden and reveals micro:bit controls only while loaded",
136
+ async () => {
137
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
138
+
139
+ try {
140
+ await page.waitForSelector("#addExtensionButton", { timeout: 30_000 });
141
+
142
+ const initialState = await page.evaluate(() => {
143
+ const addButton = document.getElementById("addExtensionButton");
144
+ const microBitButton = document.getElementById("microBitConnectionButton");
145
+ return {
146
+ addButtonDisplay: window.getComputedStyle(addButton).display,
147
+ addButtonAriaHidden: addButton.getAttribute("aria-hidden"),
148
+ addButtonDisabled: addButton.disabled,
149
+ microBitButtonDisplay: window.getComputedStyle(microBitButton).display,
150
+ microBitButtonAriaHidden: microBitButton.getAttribute("aria-hidden"),
151
+ microBitButtonDisabled: microBitButton.disabled,
152
+ rightCategoryIds: Array.from(document.querySelectorAll("#selectorsright .category-selector-button")).map(
153
+ (button) => button.id
154
+ ),
155
+ extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
156
+ };
157
+ });
158
+
159
+ expect(initialState).toEqual({
160
+ addButtonDisplay: "none",
161
+ addButtonAriaHidden: "true",
162
+ addButtonDisabled: true,
163
+ microBitButtonDisplay: "none",
164
+ microBitButtonAriaHidden: "true",
165
+ microBitButtonDisabled: true,
166
+ rightCategoryIds: ["cog-start", "cog-looks", "cog-sound"],
167
+ extensionEnabled: false,
168
+ });
169
+
170
+ await openHiddenExtensionsLibrary(page);
171
+ await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
172
+ const libraryState = await page.evaluate(() => ({
173
+ title: document.getElementById("extensionsLibraryTitle").textContent.trim(),
174
+ hasMicroBitCard: Boolean(document.getElementById("microBitExtensionCard")),
175
+ microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
176
+ microBitStillHidden:
177
+ window.getComputedStyle(document.getElementById("microBitConnectionButton")).display === "none",
178
+ }));
179
+
180
+ expect(libraryState).toEqual({
181
+ title: "Extensions",
182
+ hasMicroBitCard: true,
183
+ microBitCardAction: "+",
184
+ microBitStillHidden: true,
185
+ });
186
+
187
+ await page.click("#microBitExtensionCard");
188
+ await page.waitForFunction(
189
+ () => {
190
+ const microBitButton = document.getElementById("microBitConnectionButton");
191
+ const library = document.getElementById("extensionsLibrary");
192
+ return microBitButton
193
+ && window.getComputedStyle(microBitButton).display !== "none"
194
+ && library
195
+ && !library.classList.contains("in");
196
+ },
197
+ { timeout: 30_000 }
198
+ );
199
+
200
+ const enabledState = await page.evaluate(() => {
201
+ const addButton = document.getElementById("addExtensionButton");
202
+ const microBitButton = document.getElementById("microBitConnectionButton");
203
+ return {
204
+ addButtonDisplay: window.getComputedStyle(addButton).display,
205
+ addButtonAriaHidden: addButton.getAttribute("aria-hidden"),
206
+ addButtonDisabled: addButton.disabled,
207
+ microBitButtonDisplay: window.getComputedStyle(microBitButton).display,
208
+ microBitButtonAriaHidden: microBitButton.getAttribute("aria-hidden"),
209
+ microBitButtonDisabled: microBitButton.disabled,
210
+ rightCategoryIds: Array.from(document.querySelectorAll("#selectorsright .category-selector-button")).map(
211
+ (button) => button.id
212
+ ),
213
+ selectedRightCategoryIds: Array.from(
214
+ document.querySelectorAll('#selectorsright .category-selector-button[aria-pressed="true"]')
215
+ ).map((button) => button.id),
216
+ paletteBlockTypes: Array.from(document.querySelectorAll("#palette > div")).map(
217
+ (block) => block.owner && block.owner.blocktype
218
+ ),
219
+ extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
220
+ };
221
+ });
222
+
223
+ expect(enabledState).toEqual({
224
+ addButtonDisplay: "none",
225
+ addButtonAriaHidden: "true",
226
+ addButtonDisabled: true,
227
+ microBitButtonDisplay: "flex",
228
+ microBitButtonAriaHidden: "false",
229
+ microBitButtonDisabled: false,
230
+ rightCategoryIds: ["cog-start", "cog-looks", "cog-sound", "microbit-start", "microbit-looks"],
231
+ selectedRightCategoryIds: ["microbit-start"],
232
+ paletteBlockTypes: [
233
+ "microbitbuttonpressed",
234
+ "microbitgesture",
235
+ "microbittilted",
236
+ ],
237
+ extensionEnabled: true,
238
+ });
239
+
240
+ await page.click("#microbit-looks");
241
+ await page.waitForFunction(
242
+ () => Array.from(document.querySelectorAll("#palette > div")).some(
243
+ (block) => block.owner && block.owner.blocktype === "microbitdisplaycustom"
244
+ ),
245
+ { timeout: 30_000 }
246
+ );
247
+ const looksPaletteBlockTypes = await page.evaluate(() =>
248
+ Array.from(document.querySelectorAll("#palette > div")).map((block) => block.owner && block.owner.blocktype)
249
+ );
250
+ expect(looksPaletteBlockTypes).toEqual([
251
+ "microbitdisplayheart",
252
+ "microbitdisplayhappy",
253
+ "microbitdisplaycustom",
254
+ "microbitdisplaytext",
255
+ "microbitdisplayclear",
256
+ ]);
257
+
258
+ const customMatrixState = await page.evaluate(() => {
259
+ const scripts = window.ScratchJr.getActiveScript().owner;
260
+ const block = scripts.insertKeyboardBlock(null, [[
261
+ "microbitdisplaycustom",
262
+ "00000/00000/00000/00000/00000",
263
+ 50,
264
+ 50,
265
+ ]]);
266
+ block.arg.pressMicroBitMatrixEditor({
267
+ preventDefault() {},
268
+ stopPropagation() {},
269
+ });
270
+ const cells = Array.from(document.querySelectorAll(".microbitMatrixCell"));
271
+ cells[12].dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
272
+ return {
273
+ argValue: block.getArgValue(),
274
+ cellCount: cells.length,
275
+ menuOpen: Boolean(document.querySelector(".microbitMatrixMenu")),
276
+ activeCells: Array.from(document.querySelectorAll(".microbitMatrixCell.on")).length,
277
+ };
278
+ });
279
+ expect(customMatrixState).toEqual({
280
+ argValue: "00000/00000/00100/00000/00000",
281
+ cellCount: 25,
282
+ menuOpen: true,
283
+ activeCells: 1,
284
+ });
285
+
286
+ const blocksAfterInsert = await page.evaluate(() => {
287
+ const scripts = window.ScratchJr.getActiveScript().owner;
288
+ scripts.insertKeyboardBlock(null, [["microbitdisplayclear", "null", 20, 20]]);
289
+ return window.ScratchJr.getBlocks().map((block) => block.blocktype);
290
+ });
291
+ expect(blocksAfterInsert).toContain("microbitdisplayclear");
292
+
293
+ await openHiddenExtensionsLibrary(page);
294
+ await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
295
+ const loadedLibraryState = await page.evaluate(() => ({
296
+ microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
297
+ microBitCardLoaded: document.getElementById("microBitExtensionCard").classList.contains("loaded"),
298
+ }));
299
+ expect(loadedLibraryState).toEqual({
300
+ microBitCardAction: "-",
301
+ microBitCardLoaded: true,
302
+ });
303
+
304
+ await page.click("#microBitExtensionCard");
305
+ await page.waitForSelector("#microBitExtensionRemoveWarning.show", { timeout: 30_000 });
306
+ await page.click("#microBitExtensionRemoveCancel");
307
+ await page.waitForFunction(
308
+ () => !document.getElementById("microBitExtensionRemoveWarning").classList.contains("show"),
309
+ { timeout: 30_000 }
310
+ );
311
+ const cancelUnloadState = await page.evaluate(() => ({
312
+ microBitButtonDisplay: window.getComputedStyle(document.getElementById("microBitConnectionButton")).display,
313
+ hasMicroBitBlock: window.ScratchJr.getBlocks().some((block) => block.blocktype === "microbitdisplayclear"),
314
+ }));
315
+ expect(cancelUnloadState).toEqual({
316
+ microBitButtonDisplay: "flex",
317
+ hasMicroBitBlock: true,
318
+ });
319
+
320
+ await page.click("#microBitExtensionCard");
321
+ await page.waitForSelector("#microBitExtensionRemoveWarning.show", { timeout: 30_000 });
322
+ await page.click("#microBitExtensionRemoveConfirm");
323
+ await page.waitForFunction(
324
+ () => window.getComputedStyle(document.getElementById("microBitConnectionButton")).display === "none"
325
+ && !document.querySelector("#selectorsright #microbit-start")
326
+ && !document.getElementById("extensionsLibrary").classList.contains("in"),
327
+ { timeout: 30_000 }
328
+ );
329
+ const unloadedState = await page.evaluate(() => ({
330
+ addButtonDisplay: window.getComputedStyle(document.getElementById("addExtensionButton")).display,
331
+ microBitButtonDisplay: window.getComputedStyle(document.getElementById("microBitConnectionButton")).display,
332
+ extensionsLibraryOpen: document.getElementById("extensionsLibrary").classList.contains("in"),
333
+ microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
334
+ microBitCardLoaded: document.getElementById("microBitExtensionCard").classList.contains("loaded"),
335
+ rightCategoryIds: Array.from(document.querySelectorAll("#selectorsright .category-selector-button")).map(
336
+ (button) => button.id
337
+ ),
338
+ hasMicroBitBlock: window.ScratchJr.getBlocks().some((block) => block.blocktype.indexOf("microbit") === 0),
339
+ extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
340
+ }));
341
+ expect(unloadedState).toEqual({
342
+ addButtonDisplay: "none",
343
+ microBitButtonDisplay: "none",
344
+ extensionsLibraryOpen: false,
345
+ microBitCardAction: "+",
346
+ microBitCardLoaded: false,
347
+ rightCategoryIds: ["cog-start", "cog-looks", "cog-sound"],
348
+ hasMicroBitBlock: false,
349
+ extensionEnabled: false,
350
+ });
351
+
352
+ expect(errors).toEqual([]);
353
+ } finally {
354
+ await browser.close();
355
+ }
356
+ },
357
+ 60_000
358
+ );
359
+
125
360
  it(
126
361
  "opens tutorials from the book tab and returns there from tutorial mode",
127
362
  async () => {
@@ -0,0 +1,162 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import puppeteer from "puppeteer";
3
+ import { spawn } from "child_process";
4
+ import http from "http";
5
+
6
+ const PORT = 3015;
7
+ const HOST = `http://localhost:${PORT}`;
8
+
9
+ let server;
10
+
11
+ async function waitForServer(maxAttempts = 20) {
12
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
13
+ const ok = await new Promise((resolve) => {
14
+ const req = http.get(`${HOST}/`, (res) => {
15
+ res.destroy();
16
+ resolve(true);
17
+ });
18
+ req.on("error", () => resolve(false));
19
+ });
20
+ if (ok) {
21
+ return;
22
+ }
23
+ await new Promise((resolve) => setTimeout(resolve, 500));
24
+ }
25
+ throw new Error("Local server did not start in time");
26
+ }
27
+
28
+ beforeAll(async () => {
29
+ server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
30
+ stdio: "ignore",
31
+ });
32
+ await waitForServer();
33
+ }, 30000);
34
+
35
+ afterAll(() => {
36
+ if (server) {
37
+ server.kill();
38
+ }
39
+ });
40
+
41
+ async function openPage(pathname) {
42
+ const browser = await puppeteer.launch({
43
+ headless: true,
44
+ args: ["--no-sandbox"],
45
+ });
46
+ const page = await browser.newPage();
47
+ const errors = [];
48
+
49
+ page.on("pageerror", (err) => errors.push(err.message || String(err)));
50
+ page.on("console", (msg) => {
51
+ if (msg.type() === "error") {
52
+ errors.push(msg.text());
53
+ }
54
+ });
55
+
56
+ await page.goto(`${HOST}${pathname}`, { waitUntil: "networkidle2", timeout: 30000 });
57
+
58
+ return { browser, page, errors };
59
+ }
60
+
61
+ async function waitForEditorReady(page) {
62
+ await page.waitForFunction(
63
+ () => {
64
+ const backdrop = document.getElementById("backdrop");
65
+ const selectedSpriteThumb = document.querySelector("#spritecc .spritethumb[aria-pressed=\"true\"]");
66
+ return Boolean(
67
+ window.ScratchJr
68
+ && window.ScratchJr.stage
69
+ && window.ScratchJr.stage.currentPage
70
+ && document.getElementById("addsprite")
71
+ && document.getElementById("martyMode")
72
+ && selectedSpriteThumb
73
+ && backdrop
74
+ && window.getComputedStyle(backdrop).display === "none"
75
+ && !window.ScratchJr.onHold
76
+ && window.ScratchJr.getActiveScript()
77
+ );
78
+ },
79
+ { timeout: 30000 }
80
+ );
81
+ }
82
+
83
+ async function getEditorState(page) {
84
+ return page.evaluate(() => {
85
+ const currentPage = window.ScratchJr.stage.currentPage;
86
+ const spriteIds = currentPage.getSprites();
87
+ const normalSpriteIds = spriteIds.filter(
88
+ (spriteId) => spriteId.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) === -1
89
+ );
90
+ const currentSpriteName = currentPage.currentSpriteName || "";
91
+ const addSpriteButton = document.getElementById("addsprite");
92
+
93
+ return {
94
+ currentSpriteName,
95
+ spriteIds,
96
+ normalSpriteIds,
97
+ isMartyModeEnabled: window.ScratchJr.isMartyModeEnabled,
98
+ activeSpriteIsMartyBirdsEye:
99
+ currentSpriteName.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1,
100
+ hasMartyModeSidebarCard: Boolean(document.getElementById("martyModeSidebarCard")),
101
+ addSpriteButtonVisible:
102
+ Boolean(addSpriteButton) && window.getComputedStyle(addSpriteButton).display !== "none",
103
+ addSpriteButtonDisabled: Boolean(addSpriteButton && addSpriteButton.disabled),
104
+ };
105
+ });
106
+ }
107
+
108
+ async function deleteSelectedSpriteWithKeyboard(page) {
109
+ await page.focus("#spritecc .spritethumb[aria-pressed=\"true\"]");
110
+ await page.keyboard.press("Delete");
111
+ await page.waitForFunction(
112
+ () => {
113
+ const currentPage = window.ScratchJr.stage.currentPage;
114
+ return currentPage.getSprites().filter(
115
+ (spriteId) => spriteId.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) === -1
116
+ ).length === 0;
117
+ },
118
+ { timeout: 30000 }
119
+ );
120
+ }
121
+
122
+ describe("deleting the final sprite", () => {
123
+ it(
124
+ "keeps the editor in Sprite mode when no normal sprites remain",
125
+ async () => {
126
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
127
+
128
+ try {
129
+ await waitForEditorReady(page);
130
+
131
+ const beforeDelete = await getEditorState(page);
132
+ expect(beforeDelete).toMatchObject({
133
+ normalSpriteIds: [beforeDelete.currentSpriteName],
134
+ isMartyModeEnabled: false,
135
+ activeSpriteIsMartyBirdsEye: false,
136
+ hasMartyModeSidebarCard: false,
137
+ addSpriteButtonVisible: true,
138
+ addSpriteButtonDisabled: false,
139
+ });
140
+
141
+ await deleteSelectedSpriteWithKeyboard(page);
142
+
143
+ const afterDelete = await getEditorState(page);
144
+ expect(
145
+ afterDelete,
146
+ `Editor state after deleting the last normal sprite: ${JSON.stringify(afterDelete, null, 2)}`
147
+ ).toMatchObject({
148
+ normalSpriteIds: [],
149
+ isMartyModeEnabled: false,
150
+ activeSpriteIsMartyBirdsEye: false,
151
+ hasMartyModeSidebarCard: false,
152
+ addSpriteButtonVisible: true,
153
+ addSpriteButtonDisabled: false,
154
+ });
155
+ expect(errors).toEqual([]);
156
+ } finally {
157
+ await browser.close();
158
+ }
159
+ },
160
+ 90000
161
+ );
162
+ });