@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
@@ -0,0 +1,305 @@
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 = 3014;
7
+ const HOST = `http://localhost:${PORT}`;
8
+ const MICROBIT_BLOCK_TYPE = "microbitdisplayclear";
9
+ const STUCK_PROGRESS_LOG = "setProgress 55 30 1 2";
10
+
11
+ let server;
12
+
13
+ async function waitForServer(maxAttempts = 20) {
14
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
15
+ const ok = await new Promise((resolve) => {
16
+ const req = http.get(`${HOST}/`, (res) => {
17
+ res.destroy();
18
+ resolve(true);
19
+ });
20
+ req.on("error", () => resolve(false));
21
+ });
22
+ if (ok) {
23
+ return;
24
+ }
25
+ await new Promise((resolve) => setTimeout(resolve, 500));
26
+ }
27
+ throw new Error("Local server did not start in time");
28
+ }
29
+
30
+ beforeAll(async () => {
31
+ server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
32
+ stdio: "ignore",
33
+ });
34
+ await waitForServer();
35
+ }, 30_000);
36
+
37
+ afterAll(() => {
38
+ if (server) {
39
+ server.kill();
40
+ }
41
+ });
42
+
43
+ async function openPage(pathname) {
44
+ const browser = await puppeteer.launch({
45
+ headless: true,
46
+ args: ["--no-sandbox"],
47
+ });
48
+ const page = await browser.newPage();
49
+ const errors = [];
50
+ const progressLogs = [];
51
+
52
+ page.on("pageerror", (err) => errors.push(err.message || String(err)));
53
+ page.on("console", (msg) => {
54
+ const text = msg.text();
55
+ if (text.indexOf("setProgress") > -1) {
56
+ progressLogs.push(text);
57
+ }
58
+ if (msg.type() === "error") {
59
+ errors.push(text);
60
+ }
61
+ });
62
+
63
+ await page.goto(`${HOST}${pathname}`, { waitUntil: "networkidle2", timeout: 30_000 });
64
+
65
+ return { browser, page, errors, progressLogs };
66
+ }
67
+
68
+ async function createProjectFromHome(page) {
69
+ await page.waitForSelector("#newproject .card-action-open", { timeout: 30_000 });
70
+ await Promise.all([
71
+ page.waitForNavigation({ waitUntil: "networkidle2", timeout: 30_000 }),
72
+ page.click("#newproject .card-action-open"),
73
+ ]);
74
+ await waitForEditorReady(page);
75
+ return page.evaluate(() => window.ScratchJr.currentProject);
76
+ }
77
+
78
+ async function waitForEditorReady(page, timeout = 30_000) {
79
+ await page.waitForFunction(
80
+ () => {
81
+ const backdrop = document.getElementById("backdrop");
82
+ return Boolean(
83
+ window.ScratchJr
84
+ && window.ScratchJr.stage
85
+ && window.ScratchJr.stage.currentPage
86
+ && document.getElementById("addExtensionButton")
87
+ && document.getElementById("martyMode")
88
+ && backdrop
89
+ && window.getComputedStyle(backdrop).display === "none"
90
+ && !window.ScratchJr.onHold
91
+ && window.ScratchJr.getActiveScript()
92
+ );
93
+ },
94
+ { timeout }
95
+ );
96
+ }
97
+
98
+ async function enableMartyMode(page) {
99
+ const alreadyEnabled = await page.evaluate(() => window.ScratchJr.isMartyModeEnabled);
100
+ if (!alreadyEnabled) {
101
+ await page.click("#martyMode");
102
+ }
103
+ await page.waitForFunction(
104
+ () => window.ScratchJr.isMartyModeEnabled
105
+ && window.ScratchJr.stage.currentPage.currentSpriteName
106
+ && window.ScratchJr.stage.currentPage.currentSpriteName.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1
107
+ && window.ScratchJr.getActiveScript(),
108
+ { timeout: 30_000 }
109
+ );
110
+ }
111
+
112
+ async function enableMicroBitExtension(page) {
113
+ await page.click("#addExtensionButton");
114
+ await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
115
+ await page.click("#microBitExtensionCard");
116
+ await page.waitForFunction(
117
+ () => {
118
+ const microBitButton = document.getElementById("microBitConnectionButton");
119
+ const library = document.getElementById("extensionsLibrary");
120
+ return window.ScratchJr.isMicroBitExtensionEnabled
121
+ && microBitButton
122
+ && window.getComputedStyle(microBitButton).display !== "none"
123
+ && document.querySelector("#selectorsright #microbit-start")
124
+ && document.querySelector("#selectorsright #microbit-looks")
125
+ && library
126
+ && !library.classList.contains("in");
127
+ },
128
+ { timeout: 30_000 }
129
+ );
130
+ }
131
+
132
+ async function insertMicroBitBlock(page) {
133
+ await page.click("#microbit-looks");
134
+ await page.waitForFunction(
135
+ (blockType) => Boolean(document.querySelector(`#palette [data-blocktype="${blockType}"]`)),
136
+ { timeout: 30_000 },
137
+ MICROBIT_BLOCK_TYPE
138
+ );
139
+
140
+ await page.evaluate((blockType) => {
141
+ const blockElement = document.querySelector(`#palette [data-blocktype="${blockType}"]`);
142
+ window.Palette.insertBlockFromKeyboard(blockElement, new Event("progress-loop-test"));
143
+ }, MICROBIT_BLOCK_TYPE);
144
+
145
+ await page.waitForFunction(
146
+ (blockType) => window.ScratchJr.getBlocks().some((block) => block.blocktype === blockType),
147
+ { timeout: 30_000 },
148
+ MICROBIT_BLOCK_TYPE
149
+ );
150
+ }
151
+
152
+ async function saveProjectAndWaitForBlock(page, projectId) {
153
+ await page.evaluate(() => new Promise((resolve) => {
154
+ window.ScratchJr.changed = true;
155
+ window.ScratchJr.saveProject(null, resolve);
156
+ }));
157
+
158
+ await waitForPersistedProject(page, projectId, (project) => projectHasBlock(project, MICROBIT_BLOCK_TYPE));
159
+ }
160
+
161
+ async function waitForPersistedProject(page, projectId, predicate) {
162
+ const startedAt = Date.now();
163
+ let lastProject = null;
164
+
165
+ while (Date.now() - startedAt < 15_000) {
166
+ lastProject = await readPersistedProject(page, projectId);
167
+ if (lastProject && predicate(lastProject)) {
168
+ return lastProject;
169
+ }
170
+ await new Promise((resolve) => setTimeout(resolve, 250));
171
+ }
172
+
173
+ throw new Error(`Timed out waiting for saved project state. Last project: ${JSON.stringify(lastProject)}`);
174
+ }
175
+
176
+ async function readPersistedProject(page, projectId) {
177
+ return page.evaluate((id) => new Promise((resolve) => {
178
+ window.OS.query(
179
+ { stmt: "select * from projects where id = ?", values: [Number(id)] },
180
+ (result) => {
181
+ const rows = JSON.parse(result);
182
+ if (!rows.length) {
183
+ resolve(null);
184
+ return;
185
+ }
186
+ const row = Object.fromEntries(Object.entries(rows[0]).map(([key, value]) => [key.toLowerCase(), value]));
187
+ resolve(typeof row.json === "string" ? JSON.parse(row.json) : row.json);
188
+ }
189
+ );
190
+ }), projectId);
191
+ }
192
+
193
+ function projectHasBlock(project, blockType) {
194
+ if (!project || !Array.isArray(project.pages)) {
195
+ return false;
196
+ }
197
+ return project.pages.some((pageId) => {
198
+ const page = project[pageId];
199
+ return page && Array.isArray(page.sprites) && page.sprites.some((spriteId) => {
200
+ const sprite = page[spriteId];
201
+ return sprite && Array.isArray(sprite.scripts) && sprite.scripts.some((strip) => {
202
+ return Array.isArray(strip) && strip.some((block) => Array.isArray(block) && block[0] === blockType);
203
+ });
204
+ });
205
+ });
206
+ }
207
+
208
+ async function hardReload(page) {
209
+ const client = await page.target().createCDPSession();
210
+ await client.send("Network.enable");
211
+ await client.send("Network.setCacheDisabled", { cacheDisabled: true });
212
+ try {
213
+ await page.reload({ waitUntil: "networkidle2", timeout: 30_000 });
214
+ } finally {
215
+ await client.send("Network.setCacheDisabled", { cacheDisabled: false });
216
+ await client.detach();
217
+ }
218
+ }
219
+
220
+ async function waitForLoadCurtainState(page, progressLogs, timeout = 12_000) {
221
+ const startedAt = Date.now();
222
+ let state = null;
223
+
224
+ while (Date.now() - startedAt < timeout) {
225
+ state = await getLoadCurtainState(page, progressLogs);
226
+ if (state.backdropDisplay === "none") {
227
+ return state;
228
+ }
229
+ await new Promise((resolve) => setTimeout(resolve, 250));
230
+ }
231
+
232
+ return getLoadCurtainState(page, progressLogs);
233
+ }
234
+
235
+ async function getLoadCurtainState(page, progressLogs) {
236
+ const state = await page.evaluate(() => {
237
+ const backdrop = document.getElementById("backdrop");
238
+ const currentSpriteName = window.ScratchJr?.stage?.currentPage?.currentSpriteName || "";
239
+ let activeBlocks = [];
240
+ try {
241
+ if (window.ScratchJr?.stage?.currentPage?.currentSpriteName && window.ScratchJr.getActiveScript()) {
242
+ activeBlocks = window.ScratchJr.getBlocks().map((block) => block.blocktype);
243
+ }
244
+ } catch (e) {
245
+ activeBlocks = [];
246
+ }
247
+
248
+ return {
249
+ backdropDisplay: backdrop ? window.getComputedStyle(backdrop).display : null,
250
+ backdropClassName: backdrop ? backdrop.className : null,
251
+ currentProject: window.ScratchJr?.currentProject || null,
252
+ isMartyModeEnabled: window.ScratchJr?.isMartyModeEnabled || false,
253
+ isMicroBitExtensionEnabled: window.ScratchJr?.isMicroBitExtensionEnabled || false,
254
+ activeSpriteIsMartyBirdsEye:
255
+ Boolean(window.ScratchJr) && currentSpriteName.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1,
256
+ hasMicroBitBlockInActiveScript: activeBlocks.indexOf("microbitdisplayclear") > -1,
257
+ };
258
+ });
259
+
260
+ return {
261
+ ...state,
262
+ stuckProgressLoopCount: progressLogs.filter((log) => log.indexOf(STUCK_PROGRESS_LOG) > -1).length,
263
+ progressTail: progressLogs.slice(-12),
264
+ };
265
+ }
266
+
267
+ describe("project load progress loop", () => {
268
+ it(
269
+ "lifts the loading curtain after a hard refresh of a saved Marty micro:bit project",
270
+ async () => {
271
+ const { browser, page, errors, progressLogs } = await openPage("/home.html?place=home");
272
+
273
+ try {
274
+ const projectId = await createProjectFromHome(page);
275
+
276
+ await enableMartyMode(page);
277
+ await enableMicroBitExtension(page);
278
+ await insertMicroBitBlock(page);
279
+ await saveProjectAndWaitForBlock(page, projectId);
280
+
281
+ await page.reload({ waitUntil: "networkidle2", timeout: 30_000 });
282
+ await waitForEditorReady(page);
283
+
284
+ await hardReload(page);
285
+ const state = await waitForLoadCurtainState(page, progressLogs);
286
+
287
+ expect(
288
+ state.backdropDisplay,
289
+ `Load curtain state after hard refresh: ${JSON.stringify(state, null, 2)}`
290
+ ).toBe("none");
291
+ expect(state).toMatchObject({
292
+ currentProject: projectId,
293
+ isMartyModeEnabled: true,
294
+ isMicroBitExtensionEnabled: true,
295
+ activeSpriteIsMartyBirdsEye: true,
296
+ hasMicroBitBlockInActiveScript: true,
297
+ });
298
+ expect(errors).toEqual([]);
299
+ } finally {
300
+ await browser.close();
301
+ }
302
+ },
303
+ 120_000
304
+ );
305
+ });
package/vitest.config.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const path = require('path');
2
- const { defineConfig } = require('vitest/config');
3
2
 
4
- module.exports = defineConfig({
3
+ module.exports = {
5
4
  resolve: {
6
5
  alias: {
7
6
  '@': path.resolve(__dirname, './src')
@@ -12,4 +11,4 @@ module.exports = defineConfig({
12
11
  include: ['tests/**/*.test.js'],
13
12
  fileParallelism: false
14
13
  }
15
- });
14
+ };