@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,358 @@
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 = 3018;
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
+ }, 30_000);
34
+
35
+ afterAll(() => {
36
+ if (server) {
37
+ server.kill();
38
+ }
39
+ });
40
+
41
+ async function openEditor() {
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}/editor.html?mode=edit`, { waitUntil: "networkidle2", timeout: 30_000 });
57
+ await page.waitForFunction(
58
+ () => {
59
+ const backdrop = document.getElementById("backdrop");
60
+ return Boolean(
61
+ window.ScratchJr
62
+ && window.ScratchJr.stage
63
+ && window.ScratchJr.stage.currentPage
64
+ && backdrop
65
+ && window.getComputedStyle(backdrop).display === "none"
66
+ && !window.ScratchJr.onHold
67
+ );
68
+ },
69
+ { timeout: 30_000 }
70
+ );
71
+
72
+ return { browser, page, errors };
73
+ }
74
+
75
+ async function dragBlockToScripts(page, categorySelector, blockSelector, blockType) {
76
+ const blockCount = await page.evaluate(() => window.ScratchJr.getBlocks().length);
77
+ await page.click(categorySelector);
78
+ await page.waitForSelector(blockSelector, { visible: true });
79
+
80
+ const source = await page.$eval(blockSelector, (node) => {
81
+ const rect = node.getBoundingClientRect();
82
+ return {
83
+ x: rect.left + rect.width / 2,
84
+ y: rect.top + rect.height / 2,
85
+ };
86
+ });
87
+ const target = await page.$eval("#scripts", (node) => {
88
+ const rect = node.getBoundingClientRect();
89
+ return {
90
+ x: rect.left + Math.min(220, rect.width / 2),
91
+ y: rect.top + Math.min(170, rect.height / 2),
92
+ };
93
+ });
94
+
95
+ await page.mouse.move(source.x, source.y);
96
+ await page.mouse.down();
97
+ await page.mouse.move(target.x, target.y, { steps: 12 });
98
+ await page.mouse.up();
99
+
100
+ await page.waitForFunction(
101
+ (before, type) => window.ScratchJr.getBlocks().length > before
102
+ && window.ScratchJr.getBlocks().some((block) => block.blocktype === type),
103
+ { timeout: 30_000 },
104
+ blockCount,
105
+ blockType
106
+ );
107
+ }
108
+
109
+ async function getSelectedSpritePaintState(page) {
110
+ return page.evaluate(() => {
111
+ const sprite = window.ScratchJr && window.ScratchJr.getSprite();
112
+ const thumb = sprite && sprite.thumbnail;
113
+ const brush = thumb && thumb.querySelector(".brush");
114
+ if (!sprite || !thumb || !brush) {
115
+ throw new Error("Selected sprite paint controls were not found");
116
+ }
117
+ return {
118
+ md5: sprite.md5,
119
+ thumbClass: thumb.className,
120
+ brushDisplay: window.getComputedStyle(brush).display,
121
+ blockTypes: window.ScratchJr.getBlocks().map((block) => block.blocktype),
122
+ };
123
+ });
124
+ }
125
+
126
+ describe("sprite paint editing availability", () => {
127
+ it(
128
+ "keeps Marty paint editing disabled after dropping a block",
129
+ async () => {
130
+ const { browser, page, errors } = await openEditor();
131
+
132
+ try {
133
+ const beforeDrop = await getSelectedSpritePaintState(page);
134
+ expect(beforeDrop.md5).toBe("Sprites_Marty.svg");
135
+ expect(beforeDrop.thumbClass).toContain("martynoneditable");
136
+ expect(beforeDrop.brushDisplay).toBe("none");
137
+
138
+ await dragBlockToScripts(page, "#sprite-motion", "#forward_block", "forward");
139
+
140
+ const afterDrop = await getSelectedSpritePaintState(page);
141
+ expect(afterDrop.thumbClass).toContain("martynoneditable");
142
+ expect(afterDrop.brushDisplay).toBe("none");
143
+ expect(afterDrop.blockTypes).toContain("forward");
144
+
145
+ const paintFrameVisible = await page.evaluate(() => {
146
+ const brush = window.ScratchJr.getSprite().thumbnail.querySelector(".brush");
147
+ brush.dispatchEvent(new PointerEvent("pointerdown", {
148
+ bubbles: true,
149
+ cancelable: true,
150
+ pointerType: "mouse",
151
+ }));
152
+ return window.getComputedStyle(document.getElementById("paintframe")).display !== "none";
153
+ });
154
+ expect(paintFrameVisible).toBe(false);
155
+ expect(errors).toEqual([]);
156
+ } finally {
157
+ await browser.close();
158
+ }
159
+ },
160
+ 60_000
161
+ );
162
+
163
+ it(
164
+ "disables paint editing for the animated bee in the library and sprite panel",
165
+ async () => {
166
+ const { browser, page, errors } = await openEditor();
167
+
168
+ try {
169
+ await page.click("#addsprite");
170
+ await page.waitForSelector("#libframe.appear #BeeSprite1\\.svg", { timeout: 30_000 });
171
+ await page.click("#BeeSprite1\\.svg");
172
+ await page.waitForFunction(
173
+ () => document.getElementById("library_paintme").getAttribute("aria-disabled") === "true",
174
+ { timeout: 30_000 }
175
+ );
176
+
177
+ const libraryState = await page.evaluate(() => {
178
+ const paintButton = document.getElementById("library_paintme");
179
+ return {
180
+ ariaDisabled: paintButton.getAttribute("aria-disabled"),
181
+ opacity: paintButton.style.opacity,
182
+ hasClickHandler: typeof paintButton.onclick === "function",
183
+ };
184
+ });
185
+
186
+ expect(libraryState).toEqual({
187
+ ariaDisabled: "true",
188
+ opacity: "0",
189
+ hasClickHandler: false,
190
+ });
191
+
192
+ await new Promise((resolve) => setTimeout(resolve, 250));
193
+ await page.click("#BeeSprite1\\.svg");
194
+ await page.waitForFunction(
195
+ () => {
196
+ const sprite = window.ScratchJr && window.ScratchJr.getSprite();
197
+ return Boolean(sprite && sprite.animationFrames && sprite.animationFrames.length === 3);
198
+ },
199
+ { timeout: 30_000 }
200
+ );
201
+
202
+ const beeThumbId = await page.evaluate(() => window.ScratchJr.getSprite().thumbnail.id);
203
+ await page.click(`[id="${beeThumbId}"]`);
204
+ await page.waitForFunction(
205
+ () => window.ScratchJr.getSprite().thumbnail.className.includes("paintnoneditable"),
206
+ { timeout: 30_000 }
207
+ );
208
+
209
+ const spriteState = await page.evaluate(() => {
210
+ const sprite = window.ScratchJr.getSprite();
211
+ const thumb = sprite.thumbnail;
212
+ const brush = thumb.querySelector(".brush");
213
+ const paintFrame = document.getElementById("paintframe");
214
+ brush.dispatchEvent(new PointerEvent("pointerdown", {
215
+ bubbles: true,
216
+ cancelable: true,
217
+ pointerType: "mouse",
218
+ }));
219
+ return {
220
+ thumbClass: thumb.className,
221
+ brushDisplay: window.getComputedStyle(brush).display,
222
+ paintFrameVisible: window.getComputedStyle(paintFrame).display !== "none",
223
+ };
224
+ });
225
+
226
+ expect(spriteState.thumbClass).toContain("paintnoneditable");
227
+ expect(spriteState.brushDisplay).toBe("none");
228
+ expect(spriteState.paintFrameVisible).toBe(false);
229
+ expect(errors).toEqual([]);
230
+ } finally {
231
+ await browser.close();
232
+ }
233
+ },
234
+ 60_000
235
+ );
236
+
237
+ it(
238
+ "keeps paint editing available and contained within ordinary sprite cards",
239
+ async () => {
240
+ const { browser, page, errors } = await openEditor();
241
+
242
+ try {
243
+ await page.click("#addsprite");
244
+ await page.waitForSelector("#libframe.appear #Star\\.svg", { timeout: 30_000 });
245
+ await page.click("#Star\\.svg");
246
+ await page.waitForFunction(
247
+ () => {
248
+ const star = document.getElementById("Star.svg");
249
+ const paintButton = document.getElementById("library_paintme");
250
+ return Boolean(
251
+ star
252
+ && star.className.includes("on")
253
+ && paintButton.getAttribute("aria-disabled") === "false"
254
+ && typeof paintButton.onclick === "function"
255
+ );
256
+ },
257
+ { timeout: 30_000 }
258
+ );
259
+
260
+ await new Promise((resolve) => setTimeout(resolve, 250));
261
+ await page.click("#Star\\.svg");
262
+ await page.waitForFunction(
263
+ () => {
264
+ const sprite = window.ScratchJr && window.ScratchJr.getSprite();
265
+ return Boolean(sprite && sprite.md5 === "Star.svg");
266
+ },
267
+ { timeout: 30_000 }
268
+ );
269
+
270
+ const starThumbId = await page.evaluate(() => window.ScratchJr.getSprite().thumbnail.id);
271
+ await page.click(`[id="${starThumbId}"]`);
272
+ await page.waitForFunction(
273
+ () => window.ScratchJr.getSprite().thumbnail.className.includes("on"),
274
+ { timeout: 30_000 }
275
+ );
276
+
277
+ const layoutState = await page.evaluate(() => {
278
+ const visibleThumbs = Array.from(document.querySelectorAll("#spritecc .spritethumb"))
279
+ .filter((thumb) => window.getComputedStyle(thumb).display !== "none");
280
+ const selectedThumb = visibleThumbs.find((thumb) => thumb.getAttribute("aria-pressed") === "true");
281
+ const unselectedThumb = visibleThumbs.find((thumb) => thumb.getAttribute("aria-pressed") === "false");
282
+ const addSpriteButton = document.getElementById("addsprite");
283
+ const name = selectedThumb && selectedThumb.querySelector(".sname");
284
+ const brush = selectedThumb && selectedThumb.querySelector(".brush");
285
+
286
+ if (!selectedThumb || !unselectedThumb || !addSpriteButton || !name || !brush) {
287
+ throw new Error("Sprite card layout controls were not found");
288
+ }
289
+
290
+ const rectFor = (element) => {
291
+ const rect = element.getBoundingClientRect();
292
+ return {
293
+ top: rect.top,
294
+ right: rect.right,
295
+ bottom: rect.bottom,
296
+ left: rect.left,
297
+ width: rect.width,
298
+ height: rect.height,
299
+ };
300
+ };
301
+ const selectedStyle = window.getComputedStyle(selectedThumb);
302
+
303
+ return {
304
+ selected: rectFor(selectedThumb),
305
+ unselected: rectFor(unselectedThumb),
306
+ addSprite: rectFor(addSpriteButton),
307
+ name: rectFor(name),
308
+ brush: rectFor(brush),
309
+ selectedBackgroundImage: selectedStyle.backgroundImage,
310
+ selectedBackgroundSize: selectedStyle.backgroundSize,
311
+ };
312
+ });
313
+
314
+ expect(layoutState.selected.width).toBeCloseTo(layoutState.unselected.width, 1);
315
+ expect(layoutState.selected.width).toBeCloseTo(layoutState.addSprite.width, 1);
316
+ expect(layoutState.selected.height).toBeCloseTo(layoutState.unselected.height, 1);
317
+ expect(layoutState.selectedBackgroundImage).toContain("viewOnCompact.png");
318
+ expect(layoutState.selectedBackgroundSize).toBe("100% 100%");
319
+ expect(layoutState.name.right).toBeLessThanOrEqual(layoutState.brush.left + 0.5);
320
+ expect(layoutState.brush.left).toBeGreaterThanOrEqual(layoutState.selected.left);
321
+ expect(layoutState.selected.right - layoutState.brush.right)
322
+ .toBeGreaterThanOrEqual(layoutState.brush.width * 0.1);
323
+ expect(layoutState.brush.top).toBeGreaterThanOrEqual(layoutState.selected.top);
324
+ expect(layoutState.brush.bottom).toBeLessThanOrEqual(layoutState.selected.bottom);
325
+
326
+ const editableState = await page.evaluate(() => {
327
+ const sprite = window.ScratchJr.getSprite();
328
+ const thumb = sprite.thumbnail;
329
+ const brush = thumb.querySelector(".brush");
330
+ brush.dispatchEvent(new PointerEvent("pointerdown", {
331
+ bubbles: true,
332
+ cancelable: true,
333
+ pointerType: "mouse",
334
+ }));
335
+ return {
336
+ thumbClass: thumb.className,
337
+ brushDisplay: window.getComputedStyle(brush).display,
338
+ };
339
+ });
340
+
341
+ await page.waitForFunction(
342
+ () => {
343
+ const paintFrame = document.getElementById("paintframe");
344
+ return paintFrame && window.getComputedStyle(paintFrame).display !== "none";
345
+ },
346
+ { timeout: 30_000 }
347
+ );
348
+
349
+ expect(editableState.thumbClass).not.toContain("paintnoneditable");
350
+ expect(editableState.brushDisplay).not.toBe("none");
351
+ expect(errors).toEqual([]);
352
+ } finally {
353
+ await browser.close();
354
+ }
355
+ },
356
+ 60_000
357
+ );
358
+ });
@@ -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
+ const BACKGROUND_MD5 = "BeachDay.svg";
9
+
10
+ let server;
11
+
12
+ async function waitForServer(maxAttempts = 20) {
13
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
14
+ const ok = await new Promise((resolve) => {
15
+ const req = http.get(`${HOST}/`, (res) => {
16
+ res.destroy();
17
+ resolve(true);
18
+ });
19
+ req.on("error", () => resolve(false));
20
+ });
21
+ if (ok) {
22
+ return;
23
+ }
24
+ await new Promise((resolve) => setTimeout(resolve, 500));
25
+ }
26
+ throw new Error("Local server did not start in time");
27
+ }
28
+
29
+ beforeAll(async () => {
30
+ server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
31
+ stdio: "ignore",
32
+ });
33
+ await waitForServer();
34
+ }, 30_000);
35
+
36
+ afterAll(() => {
37
+ if (server) {
38
+ server.kill();
39
+ }
40
+ });
41
+
42
+ async function openPage(pathname) {
43
+ const browser = await puppeteer.launch({
44
+ headless: true,
45
+ args: ["--no-sandbox"],
46
+ });
47
+ const page = await browser.newPage();
48
+ const errors = [];
49
+
50
+ await page.setViewport({ width: 1024, height: 768 });
51
+
52
+ page.on("pageerror", (err) => errors.push(err.message || String(err)));
53
+ page.on("console", (msg) => {
54
+ if (msg.type() === "error") {
55
+ errors.push(msg.text());
56
+ }
57
+ });
58
+
59
+ await page.goto(`${HOST}${pathname}`, { waitUntil: "networkidle2", timeout: 30_000 });
60
+
61
+ return { browser, page, errors };
62
+ }
63
+
64
+ async function waitForEditorReady(page) {
65
+ await page.waitForFunction(
66
+ () => {
67
+ const backdrop = document.getElementById("backdrop");
68
+ return Boolean(
69
+ window.ScratchJr
70
+ && window.ScratchJr.stage
71
+ && window.ScratchJr.stage.currentPage
72
+ && document.getElementById("setbkg")
73
+ && backdrop
74
+ && window.getComputedStyle(backdrop).display === "none"
75
+ && !window.ScratchJr.onHold
76
+ );
77
+ },
78
+ { timeout: 30_000 }
79
+ );
80
+ }
81
+
82
+ async function openBackgroundLibrary(page) {
83
+ await page.click("#setbkg");
84
+ await page.waitForSelector("#libframe.appear #scrollarea", { timeout: 30_000 });
85
+ await page.waitForSelector(`[id="${BACKGROUND_MD5}"]`, { timeout: 30_000 });
86
+ }
87
+
88
+ async function chooseBackground(page) {
89
+ await openBackgroundLibrary(page);
90
+ await page.click(`[id="${BACKGROUND_MD5}"]`);
91
+ await page.waitForFunction(
92
+ (backgroundMd5) => {
93
+ const thumb = document.getElementById(backgroundMd5);
94
+ return thumb && thumb.className === "assetbox on";
95
+ },
96
+ { timeout: 30_000 },
97
+ BACKGROUND_MD5
98
+ );
99
+ await page.waitFor(300);
100
+ await page.click("#okbut");
101
+ await page.waitForFunction(
102
+ (backgroundMd5) => {
103
+ const libFrame = document.getElementById("libframe");
104
+ return window.ScratchJr.stage.currentPage.md5 === backgroundMd5
105
+ && libFrame
106
+ && libFrame.className === "libframe disappear";
107
+ },
108
+ { timeout: 30_000 },
109
+ BACKGROUND_MD5
110
+ );
111
+ }
112
+
113
+ async function openActiveBackgroundInPaintEditor(page) {
114
+ await openBackgroundLibrary(page);
115
+ await page.click("#library_paintme");
116
+ await page.waitForSelector("#paintframe.appear #layer1", { timeout: 30_000 });
117
+ await page.waitForFunction(
118
+ () => {
119
+ const layer = document.getElementById("layer1");
120
+ return layer && layer.childElementCount > 0;
121
+ },
122
+ { timeout: 30_000 }
123
+ );
124
+ }
125
+
126
+ describe("Background paint editor", () => {
127
+ it(
128
+ "opens the current page background from the library paintbrush",
129
+ async () => {
130
+ const { browser, page, errors } = await openPage("/editor.html?mode=edit");
131
+
132
+ try {
133
+ await waitForEditorReady(page);
134
+ await chooseBackground(page);
135
+ await openActiveBackgroundInPaintEditor(page);
136
+
137
+ const paintState = await page.evaluate((backgroundMd5) => {
138
+ const layer = document.getElementById("layer1");
139
+ const firstChild = layer.firstElementChild;
140
+ return {
141
+ currentPageMd5: window.ScratchJr.stage.currentPage.md5,
142
+ layerChildCount: layer.childElementCount,
143
+ firstChildId: firstChild ? firstChild.id : null,
144
+ hasGeneratedBlankBackground:
145
+ layer.childElementCount === 1 && firstChild && firstChild.id === "staticbkg",
146
+ hasExpectedBackgroundContent: Boolean(layer.querySelector('[id="Tide"]')),
147
+ paintFrameClass: document.getElementById("paintframe").className,
148
+ expectedBackgroundMd5: backgroundMd5,
149
+ };
150
+ }, BACKGROUND_MD5);
151
+
152
+ expect(paintState.currentPageMd5).toBe(BACKGROUND_MD5);
153
+ expect(paintState.hasGeneratedBlankBackground).toBe(false);
154
+ expect(paintState.hasExpectedBackgroundContent).toBe(true);
155
+ expect(errors).toEqual([]);
156
+ } finally {
157
+ await browser.close();
158
+ }
159
+ },
160
+ 60_000
161
+ );
162
+ });