@robotical/martyblocksjr 4.2.0 → 4.2.2

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 (49) hide show
  1. package/editions/free/src/app.bundle.js +1 -1
  2. package/editions/free/src/assets/ui/viewOnCompact.png +0 -0
  3. package/editions/free/src/assets/ui/viewOnCompactWideLabel.png +0 -0
  4. package/editions/free/src/assets/ui/viewOnTargetCompact.png +0 -0
  5. package/editions/free/src/css/editor.css +96 -0
  6. package/editions/free/src/css/editorleftpanel.css +21 -22
  7. package/editions/free/src/css/editorstage.css +62 -0
  8. package/editions/free/src/css/paintlook.css +23 -0
  9. package/editions/free/src/localizations/bg.json +1 -0
  10. package/editions/free/src/localizations/ca.json +1 -0
  11. package/editions/free/src/localizations/cs.json +1 -0
  12. package/editions/free/src/localizations/cy.json +1 -0
  13. package/editions/free/src/localizations/da.json +1 -0
  14. package/editions/free/src/localizations/de.json +1 -0
  15. package/editions/free/src/localizations/el.json +1 -0
  16. package/editions/free/src/localizations/en.json +1 -0
  17. package/editions/free/src/localizations/es.json +1 -0
  18. package/editions/free/src/localizations/fi.json +1 -0
  19. package/editions/free/src/localizations/fr.json +1 -0
  20. package/editions/free/src/localizations/it.json +1 -0
  21. package/editions/free/src/localizations/ja.json +1 -0
  22. package/editions/free/src/localizations/ko.json +1 -0
  23. package/editions/free/src/localizations/nl.json +1 -0
  24. package/editions/free/src/localizations/no.json +1 -0
  25. package/editions/free/src/localizations/pl.json +1 -0
  26. package/editions/free/src/localizations/pt-br.json +1 -0
  27. package/editions/free/src/localizations/pt.json +1 -0
  28. package/editions/free/src/localizations/sv.json +1 -0
  29. package/editions/free/src/localizations/th.json +1 -0
  30. package/editions/free/src/localizations/tr.json +1 -0
  31. package/editions/free/src/localizations/uk.json +1 -0
  32. package/editions/free/src/localizations/zh-cn.json +1 -0
  33. package/editions/free/src/localizations/zh-tw.json +1 -0
  34. package/editions/free/src/media.json +3 -0
  35. package/package.json +1 -1
  36. package/tests/e2e/accessibility.e2e.test.js +19 -2
  37. package/tests/e2e/animated-sprite-paint-disabled.e2e.test.js +358 -0
  38. package/tests/e2e/background-paint-active-selection.e2e.test.js +162 -0
  39. package/tests/e2e/blocks-jr-hidden-blocks.e2e.test.js +262 -0
  40. package/tests/e2e/chromium-79-smoke.test.js +19 -10
  41. package/tests/e2e/delete-last-sprite-mode.e2e.test.js +162 -0
  42. package/tests/e2e/duplicate-page.e2e.test.js +414 -0
  43. package/tests/e2e/marty-script-activation-paths.e2e.test.js +593 -0
  44. package/tests/e2e/microbit-extension-project-reload.e2e.test.js +6 -1
  45. package/tests/e2e/paint-editor-cancel.e2e.test.js +274 -0
  46. package/tests/e2e/project-load-progress-loop.e2e.test.js +6 -1
  47. package/tests/e2e/project-save-stall.e2e.test.js +230 -0
  48. package/tests/e2e/sprite-watermark-grayscale.e2e.test.js +269 -0
  49. package/tests/e2e/top-toolbar-tooltips.e2e.test.js +152 -0
@@ -0,0 +1,593 @@
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 = 3020;
7
+ const HOST = `http://localhost:${PORT}`;
8
+
9
+ let server;
10
+ let sharedBrowser;
11
+
12
+ const waitForServer = () =>
13
+ new Promise((resolve, reject) => {
14
+ const started = Date.now();
15
+
16
+ const check = () => {
17
+ http.get(HOST, response => {
18
+ response.resume();
19
+ resolve();
20
+ }).on("error", error => {
21
+ if (Date.now() - started > 10000) {
22
+ reject(error);
23
+ } else {
24
+ setTimeout(check, 100);
25
+ }
26
+ });
27
+ };
28
+
29
+ check();
30
+ });
31
+
32
+ const openPage = async pathname => {
33
+ const browser = await sharedBrowser.createIncognitoBrowserContext();
34
+ const page = await browser.newPage();
35
+ const errors = [];
36
+
37
+ page.on("pageerror", error => errors.push(error.message));
38
+ page.on("console", message => {
39
+ if (message.type() === "error") {
40
+ errors.push(message.text());
41
+ }
42
+ });
43
+
44
+ await page.goto(`${HOST}${pathname}`, {
45
+ waitUntil: "networkidle2",
46
+ timeout: 30000
47
+ });
48
+
49
+ return { browser, page, errors };
50
+ };
51
+
52
+ const waitForEditorReady = async page => {
53
+ await page.waitForFunction(() => {
54
+ const backdrop = document.getElementById("backdrop");
55
+ return window.ScratchJr &&
56
+ ScratchJr.stage &&
57
+ ScratchJr.stage.currentPage &&
58
+ document.getElementById("martyMode") &&
59
+ document.getElementById("emptypage") &&
60
+ backdrop &&
61
+ window.getComputedStyle(backdrop).display === "none" &&
62
+ !ScratchJr.onHold &&
63
+ ScratchJr.getActiveScript();
64
+ }, { timeout: 30000 });
65
+ };
66
+
67
+ const createProjectFromHome = async page => {
68
+ await page.waitForSelector("#newproject .card-action-open", { timeout: 30000 });
69
+ await Promise.all([
70
+ page.waitForNavigation({ waitUntil: "networkidle2", timeout: 30000 }),
71
+ page.click("#newproject .card-action-open")
72
+ ]);
73
+ await waitForEditorReady(page);
74
+ return page.evaluate(() => ScratchJr.currentProject);
75
+ };
76
+
77
+ const getActiveScriptState = async page =>
78
+ page.evaluate(() => {
79
+ const script = ScratchJr.getActiveScript();
80
+ const blocks = script.owner.getBlocks();
81
+ return {
82
+ pageId: ScratchJr.stage.currentPage.id,
83
+ pageIds: ScratchJr.stage.getPagesID(),
84
+ isMartyModeEnabled: ScratchJr.isMartyModeEnabled,
85
+ spriteId: ScratchJr.stage.currentPage.currentSpriteName,
86
+ scriptId: script.id,
87
+ scriptVisibility: window.getComputedStyle(script).visibility,
88
+ blockTypes: blocks.map(block => block.blocktype),
89
+ visibleBlockTypes: blocks
90
+ .filter(block => window.getComputedStyle(block.div).visibility !== "hidden")
91
+ .map(block => block.blocktype)
92
+ };
93
+ });
94
+
95
+ const setMartyMode = async (page, enabled) => {
96
+ const currentMode = await page.evaluate(() => ScratchJr.isMartyModeEnabled);
97
+ if (currentMode !== enabled) {
98
+ await page.click("#martyMode");
99
+ }
100
+
101
+ await page.waitForFunction(expected => {
102
+ if (ScratchJr.isMartyModeEnabled !== expected || !ScratchJr.getActiveScript()) {
103
+ return false;
104
+ }
105
+ const spriteId = ScratchJr.stage.currentPage.currentSpriteName || "";
106
+ const isBirdsEye = spriteId.indexOf(ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1;
107
+ return expected ? isBirdsEye : !isBirdsEye;
108
+ }, {}, enabled);
109
+ };
110
+
111
+ const reactivateCurrentMartyScript = async page => {
112
+ const pageId = await page.evaluate(() => ScratchJr.stage.currentPage.id);
113
+ await setMartyMode(page, false);
114
+ await setMartyMode(page, true);
115
+ await page.waitForFunction(expectedPageId => {
116
+ const script = ScratchJr.getActiveScript();
117
+ return ScratchJr.stage.currentPage.id === expectedPageId &&
118
+ script &&
119
+ window.getComputedStyle(script).visibility === "visible";
120
+ }, {}, pageId);
121
+ };
122
+
123
+ const createSecondMartyPage = async page => {
124
+ await setMartyMode(page, true);
125
+ const pageOne = await getActiveScriptState(page);
126
+ const pageCount = pageOne.pageIds.length;
127
+
128
+ await page.click("#emptypage");
129
+ await page.waitForFunction((firstPageId, count) =>
130
+ ScratchJr.stage.pages.length === count + 1 &&
131
+ ScratchJr.stage.currentPage.id !== firstPageId &&
132
+ ScratchJr.isMartyModeEnabled &&
133
+ ScratchJr.stage.currentPage.currentSpriteName &&
134
+ ScratchJr.stage.currentPage.currentSpriteName.indexOf(ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1 &&
135
+ !ScratchJr.onHold &&
136
+ ScratchJr.getActiveScript(),
137
+ {}, pageOne.pageId, pageCount);
138
+
139
+ return {
140
+ pageOne,
141
+ pageTwo: await getActiveScriptState(page)
142
+ };
143
+ };
144
+
145
+ const selectPage = async (page, pageId) => {
146
+ await page.click(`.pagethumb[data-owner="${pageId}"]`);
147
+ await page.waitForFunction(id =>
148
+ ScratchJr.stage.currentPage.id === id && ScratchJr.getActiveScript(),
149
+ {}, pageId);
150
+ };
151
+
152
+ const dragBlockToCanvas = async (page, categorySelector, blockSelector, blockType) => {
153
+ const blockCount = await page.evaluate(() => ScratchJr.getBlocks().length);
154
+ await page.click(categorySelector);
155
+ await page.waitForSelector(blockSelector, { visible: true });
156
+
157
+ const source = await page.$eval(blockSelector, node => {
158
+ const rect = node.getBoundingClientRect();
159
+ return {
160
+ x: rect.left + rect.width / 2,
161
+ y: rect.top + rect.height / 2
162
+ };
163
+ });
164
+ const target = await page.$eval("#scripts", node => {
165
+ const rect = node.getBoundingClientRect();
166
+ return {
167
+ x: rect.left + Math.min(220, rect.width / 2),
168
+ y: rect.top + Math.min(170, rect.height / 2)
169
+ };
170
+ });
171
+
172
+ await page.mouse.move(source.x, source.y);
173
+ await page.mouse.down();
174
+ await page.mouse.move(target.x, target.y, { steps: 12 });
175
+ await page.mouse.up();
176
+
177
+ await page.waitForFunction((before, type) =>
178
+ ScratchJr.getBlocks().length > before &&
179
+ ScratchJr.getBlocks().some(block => block.blocktype === type),
180
+ {}, blockCount, blockType);
181
+
182
+ return getActiveScriptState(page);
183
+ };
184
+
185
+ const dragMartyBlock = page =>
186
+ dragBlockToCanvas(page, "#marty-motion", "#martyStepForward_block", "martyStepForward");
187
+
188
+ const dragSpriteBlock = page =>
189
+ dragBlockToCanvas(page, "#sprite-motion", "#forward_block", "forward");
190
+
191
+ const expectVisibleBlock = (state, blockType, pathName) => {
192
+ expect(state.blockTypes, `${pathName}: ${JSON.stringify(state)}`).toContain(blockType);
193
+ expect(state.scriptVisibility, `${pathName}: ${JSON.stringify(state)}`).toBe("visible");
194
+ expect(state.visibleBlockTypes, `${pathName}: ${JSON.stringify(state)}`).toContain(blockType);
195
+ };
196
+
197
+ const readPersistedProject = async (page, projectId) =>
198
+ page.evaluate(id => new Promise(resolve => {
199
+ window.OS.query(
200
+ { stmt: "select * from projects where id = ?", values: [Number(id)] },
201
+ result => {
202
+ const rows = JSON.parse(result);
203
+ if (!rows.length) {
204
+ resolve(null);
205
+ return;
206
+ }
207
+ const row = Object.fromEntries(Object.entries(rows[0])
208
+ .map(([key, value]) => [key.toLowerCase(), value]));
209
+ resolve(typeof row.json === "string" ? JSON.parse(row.json) : row.json);
210
+ }
211
+ );
212
+ }), projectId);
213
+
214
+ const saveProject = async (page, projectId, currentPageId) => {
215
+ await page.evaluate(() => new Promise(resolve => {
216
+ ScratchJr.changed = true;
217
+ ScratchJr.saveProject(null, resolve);
218
+ }));
219
+
220
+ const started = Date.now();
221
+ while (Date.now() - started < 15000) {
222
+ const project = await readPersistedProject(page, projectId);
223
+ if (project && project.currentPage === currentPageId && project.pages.length === 2) {
224
+ return;
225
+ }
226
+ await new Promise(resolve => setTimeout(resolve, 250));
227
+ }
228
+ throw new Error("Timed out waiting for the multi-page Marty project to persist");
229
+ };
230
+
231
+ describe("Marty script activation paths", () => {
232
+ beforeAll(async () => {
233
+ server = spawn("python3", ["-m", "http.server", String(PORT), "--directory", "editions/free/src"], {
234
+ stdio: "ignore"
235
+ });
236
+ await waitForServer();
237
+ sharedBrowser = await puppeteer.launch({
238
+ headless: true,
239
+ args: ["--no-sandbox"]
240
+ });
241
+ }, 30000);
242
+
243
+ afterAll(async () => {
244
+ if (sharedBrowser) {
245
+ await sharedBrowser.close();
246
+ }
247
+ if (server) {
248
+ server.kill();
249
+ }
250
+ });
251
+
252
+ it("keeps the Marty mode sidebar card and full label stable after block drops", async () => {
253
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
254
+ try {
255
+ await waitForEditorReady(page);
256
+ const spriteCardLayout = await page.evaluate(() => {
257
+ const card = document.querySelector("#spritecc .spritethumb[aria-pressed=\"true\"]");
258
+ const label = card && card.querySelector(".sname");
259
+ if (!card || !label) {
260
+ throw new Error("Selected sprite card layout was not found");
261
+ }
262
+ return {
263
+ cardWidth: card.getBoundingClientRect().width,
264
+ labelWidth: label.getBoundingClientRect().width
265
+ };
266
+ });
267
+
268
+ await setMartyMode(page, true);
269
+ const getMartyCardLayout = () => page.evaluate(() => {
270
+ const card = document.getElementById("martyModeSidebarCard");
271
+ const label = card && card.querySelector(".marty-mode-card-label");
272
+ if (!card || !label) {
273
+ throw new Error("Marty mode sidebar card layout was not found");
274
+ }
275
+ const cardStyle = window.getComputedStyle(card);
276
+ const labelStyle = window.getComputedStyle(label);
277
+ const cardRect = card.getBoundingClientRect();
278
+ const labelRect = label.getBoundingClientRect();
279
+ return {
280
+ className: card.className,
281
+ cardWidth: cardRect.width,
282
+ cardLeft: cardRect.left,
283
+ cardRight: cardRect.right,
284
+ labelText: label.textContent,
285
+ labelWidth: labelRect.width,
286
+ labelLeft: labelRect.left,
287
+ labelRight: labelRect.right,
288
+ labelClientWidth: label.clientWidth,
289
+ labelScrollWidth: label.scrollWidth,
290
+ hasBrush: Boolean(card.querySelector(".brush")),
291
+ backgroundImage: cardStyle.backgroundImage,
292
+ backgroundSize: cardStyle.backgroundSize,
293
+ overflow: labelStyle.overflow,
294
+ textOverflow: labelStyle.textOverflow,
295
+ whiteSpace: labelStyle.whiteSpace
296
+ };
297
+ });
298
+ const martyCardLayout = await getMartyCardLayout();
299
+
300
+ expect(martyCardLayout.cardWidth).toBeCloseTo(spriteCardLayout.cardWidth, 1);
301
+ expect(martyCardLayout.labelWidth).toBeGreaterThan(spriteCardLayout.labelWidth);
302
+ expect(martyCardLayout.labelLeft).toBeGreaterThanOrEqual(martyCardLayout.cardLeft);
303
+ expect(martyCardLayout.labelRight).toBeLessThanOrEqual(martyCardLayout.cardRight);
304
+ expect(martyCardLayout.labelText).toBe("Marty Mode");
305
+ expect(martyCardLayout.hasBrush).toBe(false);
306
+ expect(martyCardLayout.backgroundImage).toContain("viewOnCompactWideLabel.png");
307
+ expect(martyCardLayout.backgroundSize).toBe("100% 100%");
308
+ expect(martyCardLayout).toMatchObject({
309
+ overflow: "hidden",
310
+ textOverflow: "ellipsis",
311
+ whiteSpace: "nowrap"
312
+ });
313
+ expect(martyCardLayout.labelScrollWidth).toBeLessThanOrEqual(martyCardLayout.labelClientWidth);
314
+
315
+ await dragMartyBlock(page);
316
+ expect(await getMartyCardLayout()).toEqual(martyCardLayout);
317
+ expect(errors).toEqual([]);
318
+ } finally {
319
+ await browser.close();
320
+ }
321
+ }, 60000);
322
+
323
+ it("activates the script after duplicating a page in Marty mode", async () => {
324
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
325
+ try {
326
+ await waitForEditorReady(page);
327
+ await setMartyMode(page, true);
328
+ const source = await getActiveScriptState(page);
329
+
330
+ await page.evaluate(pageId => ScratchJr.stage.duplicatePage(pageId), source.pageId);
331
+ await page.waitForFunction((pageId, count) =>
332
+ ScratchJr.stage.pages.length === count + 1 &&
333
+ ScratchJr.stage.currentPage.id !== pageId &&
334
+ !ScratchJr.stage.duplicatingPage &&
335
+ !ScratchJr.onHold &&
336
+ ScratchJr.getActiveScript(),
337
+ {}, source.pageId, source.pageIds.length);
338
+
339
+ expectVisibleBlock(await dragMartyBlock(page), "martyStepForward", "duplicate page");
340
+ expect(errors).toEqual([]);
341
+ } finally {
342
+ await browser.close();
343
+ }
344
+ }, 60000);
345
+
346
+ it("activates the remaining page script after deleting the current page", async () => {
347
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
348
+ try {
349
+ await waitForEditorReady(page);
350
+ const { pageOne, pageTwo } = await createSecondMartyPage(page);
351
+
352
+ await page.evaluate(pageId => ScratchJr.stage.deletePage(pageId), pageTwo.pageId);
353
+ await page.waitForFunction(pageId =>
354
+ ScratchJr.stage.pages.length === 1 &&
355
+ ScratchJr.stage.currentPage.id === pageId &&
356
+ ScratchJr.getActiveScript(),
357
+ {}, pageOne.pageId);
358
+
359
+ expectVisibleBlock(await dragMartyBlock(page), "martyStepForward", "delete current page");
360
+ expect(errors).toEqual([]);
361
+ } finally {
362
+ await browser.close();
363
+ }
364
+ }, 60000);
365
+
366
+ it("activates scripts after undoing and redoing page navigation", async () => {
367
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
368
+ try {
369
+ await waitForEditorReady(page);
370
+ const { pageOne, pageTwo } = await createSecondMartyPage(page);
371
+ await selectPage(page, pageOne.pageId);
372
+ await reactivateCurrentMartyScript(page);
373
+
374
+ await page.click("#id_undo");
375
+ await page.waitForFunction(pageId => ScratchJr.stage.currentPage.id === pageId,
376
+ {}, pageTwo.pageId);
377
+ const undoState = await getActiveScriptState(page);
378
+
379
+ await reactivateCurrentMartyScript(page);
380
+ await page.click("#id_redo");
381
+ await page.waitForFunction(pageId => ScratchJr.stage.currentPage.id === pageId,
382
+ {}, pageOne.pageId);
383
+ const redoState = await dragMartyBlock(page);
384
+
385
+ expect({
386
+ undo: undoState.scriptVisibility,
387
+ redo: redoState.scriptVisibility
388
+ }).toEqual({ undo: "visible", redo: "visible" });
389
+ expectVisibleBlock(redoState, "martyStepForward", "redo page navigation");
390
+ expect(errors).toEqual([]);
391
+ } finally {
392
+ await browser.close();
393
+ }
394
+ }, 60000);
395
+
396
+ it("activates scripts after undoing and redoing page ordering", async () => {
397
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
398
+ try {
399
+ await waitForEditorReady(page);
400
+ const { pageOne, pageTwo } = await createSecondMartyPage(page);
401
+
402
+ await page.evaluate(pageId => {
403
+ const thumb = document.querySelector(`.pagethumb[data-owner="${pageId}"]`);
404
+ thumb.dispatchEvent(new KeyboardEvent("keydown", {
405
+ key: "ArrowLeft",
406
+ shiftKey: true,
407
+ bubbles: true,
408
+ cancelable: true
409
+ }));
410
+ }, pageTwo.pageId);
411
+ await page.waitForFunction(pageId => ScratchJr.stage.getPagesID()[0] === pageId,
412
+ {}, pageTwo.pageId);
413
+
414
+ await page.click("#id_undo");
415
+ await page.waitForFunction((firstId, currentId) =>
416
+ ScratchJr.stage.getPagesID()[0] === firstId &&
417
+ ScratchJr.stage.currentPage.id === currentId,
418
+ {}, pageOne.pageId, pageTwo.pageId);
419
+ const undoState = await getActiveScriptState(page);
420
+
421
+ await reactivateCurrentMartyScript(page);
422
+ await page.click("#id_redo");
423
+ await page.waitForFunction((firstId, currentId) =>
424
+ ScratchJr.stage.getPagesID()[0] === firstId &&
425
+ ScratchJr.stage.currentPage.id === currentId,
426
+ {}, pageTwo.pageId, pageTwo.pageId);
427
+ const redoState = await dragMartyBlock(page);
428
+
429
+ expect({
430
+ undo: undoState.scriptVisibility,
431
+ redo: redoState.scriptVisibility
432
+ }).toEqual({ undo: "visible", redo: "visible" });
433
+ expectVisibleBlock(redoState, "martyStepForward", "redo page ordering");
434
+ expect(errors).toEqual([]);
435
+ } finally {
436
+ await browser.close();
437
+ }
438
+ }, 60000);
439
+
440
+ it("activates scripts when undoing and redoing a page deletion", async () => {
441
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
442
+ try {
443
+ await waitForEditorReady(page);
444
+ const { pageOne, pageTwo } = await createSecondMartyPage(page);
445
+
446
+ await page.evaluate(pageId => ScratchJr.stage.deletePage(pageId), pageTwo.pageId);
447
+ await page.waitForFunction(pageId =>
448
+ ScratchJr.stage.pages.length === 1 && ScratchJr.stage.currentPage.id === pageId,
449
+ {}, pageOne.pageId);
450
+ await reactivateCurrentMartyScript(page);
451
+
452
+ await page.click("#id_undo");
453
+ await page.waitForFunction(() =>
454
+ ScratchJr.stage.pages.length === 2 &&
455
+ document.getElementById("id_redo").className.indexOf("enable") > -1 &&
456
+ ScratchJr.getActiveScript());
457
+ await page.waitFor(1000);
458
+ const undoState = await getActiveScriptState(page);
459
+
460
+ await page.click("#id_redo");
461
+ await page.waitForFunction(pageId =>
462
+ ScratchJr.stage.pages.length === 1 &&
463
+ ScratchJr.stage.currentPage.id === pageId &&
464
+ ScratchJr.getActiveScript(),
465
+ {}, pageOne.pageId);
466
+ await page.waitFor(500);
467
+ const redoState = await (await page.evaluate(() => ScratchJr.isMartyModeEnabled) ?
468
+ dragMartyBlock(page) : dragSpriteBlock(page));
469
+
470
+ expect({
471
+ undo: undoState.scriptVisibility,
472
+ redo: redoState.scriptVisibility
473
+ }).toEqual({
474
+ undo: "visible",
475
+ redo: "visible"
476
+ });
477
+ expectVisibleBlock(
478
+ redoState,
479
+ redoState.isMartyModeEnabled ? "martyStepForward" : "forward",
480
+ "redo page deletion"
481
+ );
482
+ expect(errors).toEqual([]);
483
+ } finally {
484
+ await browser.close();
485
+ }
486
+ }, 60000);
487
+
488
+ it("activates the destination script after a go-to-page transition", async () => {
489
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
490
+ try {
491
+ await waitForEditorReady(page);
492
+ const { pageOne } = await createSecondMartyPage(page);
493
+
494
+ await page.evaluate(() => ScratchJr.stage.gotoPage(1));
495
+ await page.waitForFunction(pageId => ScratchJr.stage.currentPage.id === pageId,
496
+ {}, pageOne.pageId);
497
+
498
+ expectVisibleBlock(await dragMartyBlock(page), "martyStepForward", "go to page");
499
+ expect(errors).toEqual([]);
500
+ } finally {
501
+ await browser.close();
502
+ }
503
+ }, 60000);
504
+
505
+ it("activates the destination script after fullscreen page navigation", async () => {
506
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
507
+ try {
508
+ await waitForEditorReady(page);
509
+ const { pageOne } = await createSecondMartyPage(page);
510
+
511
+ await page.click("#full");
512
+ await page.waitForFunction(() => ScratchJr.inFullscreen);
513
+ await page.$eval("#prevpage", button => button.click());
514
+ await page.waitForFunction(pageId => ScratchJr.stage.currentPage.id === pageId,
515
+ {}, pageOne.pageId);
516
+ await page.$eval("#full", button => button.click());
517
+ await page.waitForFunction(() => !ScratchJr.inFullscreen);
518
+
519
+ expectVisibleBlock(await dragMartyBlock(page), "martyStepForward", "fullscreen navigation");
520
+ expect(errors).toEqual([]);
521
+ } finally {
522
+ await browser.close();
523
+ }
524
+ }, 60000);
525
+
526
+ it("activates the saved current script after reloading a multi-page Marty project", async () => {
527
+ const { browser, page, errors } = await openPage("/home.html?place=home");
528
+ try {
529
+ const projectId = await createProjectFromHome(page);
530
+ const { pageOne } = await createSecondMartyPage(page);
531
+ await selectPage(page, pageOne.pageId);
532
+ await reactivateCurrentMartyScript(page);
533
+ expectVisibleBlock(await dragMartyBlock(page), "martyStepForward", "pre-save state");
534
+ await saveProject(page, projectId, pageOne.pageId);
535
+
536
+ await page.reload({ waitUntil: "networkidle2", timeout: 30000 });
537
+ await waitForEditorReady(page);
538
+ await page.waitForFunction(pageId =>
539
+ ScratchJr.stage.currentPage.id === pageId && ScratchJr.isMartyModeEnabled,
540
+ {}, pageOne.pageId);
541
+
542
+ expectVisibleBlock(await dragMartyBlock(page), "martyStepForward", "project reload");
543
+ expect(errors).toEqual([]);
544
+ } finally {
545
+ await browser.close();
546
+ }
547
+ }, 120000);
548
+
549
+ it("reactivates the original script after duplicate-page error recovery", async () => {
550
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
551
+ try {
552
+ await waitForEditorReady(page);
553
+ await setMartyMode(page, false);
554
+ const source = await getActiveScriptState(page);
555
+
556
+ const recovery = await page.evaluate(pageId => {
557
+ const pages = ScratchJr.stage.pages;
558
+ const originalPush = pages.push;
559
+ pages.push = function () {
560
+ throw new Error("injected duplicate-page failure");
561
+ };
562
+ try {
563
+ ScratchJr.stage.duplicatePage(pageId);
564
+ return { caught: false };
565
+ } catch (error) {
566
+ return {
567
+ caught: true,
568
+ message: error.message,
569
+ currentPageId: ScratchJr.stage.currentPage.id
570
+ };
571
+ } finally {
572
+ pages.push = originalPush;
573
+ }
574
+ }, source.pageId);
575
+
576
+ expect(recovery).toMatchObject({
577
+ caught: true,
578
+ message: "injected duplicate-page failure",
579
+ currentPageId: source.pageId
580
+ });
581
+ await page.waitForFunction(pageId =>
582
+ ScratchJr.stage.currentPage.id === pageId &&
583
+ !ScratchJr.stage.duplicatingPage &&
584
+ !ScratchJr.onHold,
585
+ {}, source.pageId);
586
+
587
+ expectVisibleBlock(await dragSpriteBlock(page), "forward", "duplicate error recovery");
588
+ expect(errors).toEqual([]);
589
+ } finally {
590
+ await browser.close();
591
+ }
592
+ }, 60000);
593
+ });
@@ -119,7 +119,12 @@ async function enableMartyMode(page) {
119
119
  }
120
120
 
121
121
  async function enableMicroBitExtension(page) {
122
- await page.click("#addExtensionButton");
122
+ await page.$eval("#addExtensionButton", (button) => {
123
+ if (!button.disabled || window.getComputedStyle(button).display !== "none") {
124
+ throw new Error("Expected the extensions entry point to remain hidden and disabled");
125
+ }
126
+ button.onclick();
127
+ });
123
128
  await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
124
129
  await page.click("#microBitExtensionCard");
125
130
  await page.waitForFunction(