@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,269 @@
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 AFFECTED_SPRITES = [
9
+ { name: "Pink Octopus", md5: "Sprites_Octopus-Pink.svg" },
10
+ { name: "Raccoon", md5: "Sprites_Raccoon.svg" },
11
+ { name: "Unicorn", md5: "Sprites_Unicorn.svg" },
12
+ ];
13
+ const LEGACY_SPRITES = [
14
+ { name: "Dog", md5: "Dog.svg", expectsGrayscale: false },
15
+ { name: "Marty", md5: "Sprites_Marty.svg", expectsGrayscale: false },
16
+ { name: "Bee", md5: "BeeSprite1.svg", expectsGrayscale: true },
17
+ ];
18
+
19
+ let server;
20
+
21
+ async function waitForServer(maxAttempts = 20) {
22
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
23
+ const ok = await new Promise((resolve) => {
24
+ const req = http.get(`${HOST}/`, (res) => {
25
+ res.destroy();
26
+ resolve(true);
27
+ });
28
+ req.on("error", () => resolve(false));
29
+ });
30
+ if (ok) {
31
+ return;
32
+ }
33
+ await new Promise((resolve) => setTimeout(resolve, 500));
34
+ }
35
+ throw new Error("Local server did not start in time");
36
+ }
37
+
38
+ beforeAll(async () => {
39
+ server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
40
+ stdio: "ignore",
41
+ });
42
+ await waitForServer();
43
+ }, 30_000);
44
+
45
+ afterAll(() => {
46
+ if (server) {
47
+ server.kill();
48
+ }
49
+ });
50
+
51
+ async function openEditor() {
52
+ const browser = await puppeteer.launch({
53
+ headless: true,
54
+ args: ["--no-sandbox"],
55
+ });
56
+ const page = await browser.newPage();
57
+ const errors = [];
58
+
59
+ page.on("pageerror", (err) => errors.push(err.message || String(err)));
60
+ page.on("console", (msg) => {
61
+ if (msg.type() === "error") {
62
+ errors.push(msg.text());
63
+ }
64
+ });
65
+
66
+ await page.goto(`${HOST}/editor.html?mode=edit`, { waitUntil: "networkidle2", timeout: 30_000 });
67
+ await waitForEditorReady(page);
68
+
69
+ return { browser, page, errors };
70
+ }
71
+
72
+ async function waitForEditorReady(page) {
73
+ await page.waitForFunction(
74
+ () => {
75
+ const backdrop = document.getElementById("backdrop");
76
+ return Boolean(
77
+ window.ScratchJr
78
+ && window.ScratchJr.stage
79
+ && window.ScratchJr.stage.currentPage
80
+ && document.querySelector(".watermark")
81
+ && backdrop
82
+ && window.getComputedStyle(backdrop).display === "none"
83
+ && !window.ScratchJr.onHold
84
+ && window.ScratchJr.getActiveScript()
85
+ );
86
+ },
87
+ { timeout: 30_000 }
88
+ );
89
+ }
90
+
91
+ async function addSpriteAndWait(page, sprite) {
92
+ await page.evaluate((spriteMd5, spriteName) => {
93
+ window.ScratchJr.stage.currentPage.addSprite(0.5, spriteMd5, spriteName);
94
+ }, sprite.md5, sprite.name);
95
+
96
+ await page.waitForFunction(
97
+ (spriteMd5) => {
98
+ const activeSprite = window.ScratchJr && window.ScratchJr.getSprite();
99
+ const watermark = document.querySelector(".watermark img");
100
+ return Boolean(
101
+ activeSprite
102
+ && activeSprite.md5 === spriteMd5
103
+ && watermark
104
+ && watermark.complete
105
+ && (watermark.naturalWidth > 0 || watermark.width > 0)
106
+ );
107
+ },
108
+ { timeout: 30_000 },
109
+ sprite.md5
110
+ );
111
+ }
112
+
113
+ async function measureGrayscaleMatch(page, sprite) {
114
+ return page.evaluate(async (spriteName, spriteMd5) => {
115
+ const watermark = document.querySelector(".watermark img");
116
+ if (watermark.decode) {
117
+ try {
118
+ await watermark.decode();
119
+ } catch (err) {
120
+ // Some SVG data URLs report as already usable even when decode rejects.
121
+ }
122
+ }
123
+
124
+ const original = new Image();
125
+ original.src = `/svglibrary/${spriteMd5}`;
126
+ await original.decode();
127
+
128
+ const width = original.naturalWidth;
129
+ const height = original.naturalHeight;
130
+ const filter = window.getComputedStyle(watermark).filter;
131
+
132
+ const renderPixels = (image, imageFilter) => {
133
+ const canvas = document.createElement("canvas");
134
+ canvas.width = width;
135
+ canvas.height = height;
136
+ const ctx = canvas.getContext("2d");
137
+ ctx.filter = imageFilter;
138
+ ctx.drawImage(image, 0, 0, width, height);
139
+ return ctx.getImageData(0, 0, width, height).data;
140
+ };
141
+
142
+ const actualPixels = renderPixels(watermark, filter);
143
+ const expectedPixels = renderPixels(original, "grayscale(1)");
144
+ let comparedPixels = 0;
145
+ let differentPixels = 0;
146
+ let totalChannelDifference = 0;
147
+ let maxChannelDifference = 0;
148
+ let opaquePixels = 0;
149
+ let coloredPixels = 0;
150
+
151
+ for (let i = 0; i < actualPixels.length; i += 4) {
152
+ if (actualPixels[i + 3] <= 20 && expectedPixels[i + 3] <= 20) {
153
+ continue;
154
+ }
155
+
156
+ comparedPixels += 1;
157
+ let pixelDifference = 0;
158
+ for (let channel = 0; channel < 4; channel += 1) {
159
+ const difference = Math.abs(actualPixels[i + channel] - expectedPixels[i + channel]);
160
+ totalChannelDifference += difference;
161
+ pixelDifference = Math.max(pixelDifference, difference);
162
+ maxChannelDifference = Math.max(maxChannelDifference, difference);
163
+ }
164
+ if (pixelDifference > 2) {
165
+ differentPixels += 1;
166
+ }
167
+
168
+ if (actualPixels[i + 3] > 20) {
169
+ opaquePixels += 1;
170
+ const highestChannel = Math.max(actualPixels[i], actualPixels[i + 1], actualPixels[i + 2]);
171
+ const lowestChannel = Math.min(actualPixels[i], actualPixels[i + 1], actualPixels[i + 2]);
172
+ if (highestChannel - lowestChannel > 2) {
173
+ coloredPixels += 1;
174
+ }
175
+ }
176
+ }
177
+
178
+ return {
179
+ name: spriteName,
180
+ md5: spriteMd5,
181
+ width,
182
+ height,
183
+ filter,
184
+ opaquePixels,
185
+ meanChannelDifference: comparedPixels === 0 ? 0 : totalChannelDifference / (comparedPixels * 4),
186
+ differentPixelRatio: comparedPixels === 0 ? 0 : differentPixels / comparedPixels,
187
+ maxChannelDifference,
188
+ coloredPixelRatio: opaquePixels === 0 ? 0 : coloredPixels / opaquePixels,
189
+ };
190
+ }, sprite.name, sprite.md5);
191
+ }
192
+
193
+ async function getLegacyWatermarkState(page, sprite) {
194
+ return page.evaluate((spriteName, spriteMd5) => {
195
+ const activeSprite = window.ScratchJr.getSprite();
196
+ const watermark = document.querySelector(".watermark img");
197
+ const legacyWatermark = activeSprite.getSVGimage(activeSprite.watermark);
198
+ const sourceWatermark = activeSprite.getSVGimage(activeSprite.svg);
199
+
200
+ return {
201
+ name: spriteName,
202
+ md5: spriteMd5,
203
+ filter: window.getComputedStyle(watermark).filter,
204
+ usesLegacySource: watermark.src === legacyWatermark.src,
205
+ usesOriginalSource: watermark.src === sourceWatermark.src,
206
+ };
207
+ }, sprite.name, sprite.md5);
208
+ }
209
+
210
+ describe("sprite scripts watermark grayscale rendering", () => {
211
+ it(
212
+ "renders affected sprite watermarks from their source SVG in grayscale",
213
+ async () => {
214
+ const { browser, page, errors } = await openEditor();
215
+
216
+ try {
217
+ const metrics = [];
218
+ for (const sprite of AFFECTED_SPRITES) {
219
+ await addSpriteAndWait(page, sprite);
220
+ metrics.push(await measureGrayscaleMatch(page, sprite));
221
+ }
222
+
223
+ for (const metric of metrics) {
224
+ expect(metric.filter).toContain("grayscale");
225
+ expect(metric.opaquePixels).toBeGreaterThan(0);
226
+ expect(metric.meanChannelDifference).toBeLessThan(0.5);
227
+ expect(metric.differentPixelRatio).toBeLessThan(0.01);
228
+ expect(metric.coloredPixelRatio).toBeLessThan(0.01);
229
+ }
230
+ expect(errors).toEqual([]);
231
+ } finally {
232
+ await browser.close();
233
+ }
234
+ },
235
+ 60_000
236
+ );
237
+
238
+ it(
239
+ "preserves the legacy watermark path for existing sprites",
240
+ async () => {
241
+ const { browser, page, errors } = await openEditor();
242
+
243
+ try {
244
+ const states = [];
245
+ for (const sprite of LEGACY_SPRITES) {
246
+ await addSpriteAndWait(page, sprite);
247
+ states.push({
248
+ ...await getLegacyWatermarkState(page, sprite),
249
+ expectsGrayscale: sprite.expectsGrayscale,
250
+ });
251
+ }
252
+
253
+ for (const state of states) {
254
+ if (state.expectsGrayscale) {
255
+ expect(state.filter).toContain("grayscale");
256
+ } else {
257
+ expect(state.filter).toBe("none");
258
+ }
259
+ expect(state.usesLegacySource).toBe(true);
260
+ expect(state.usesOriginalSource).toBe(false);
261
+ }
262
+ expect(errors).toEqual([]);
263
+ } finally {
264
+ await browser.close();
265
+ }
266
+ },
267
+ 60_000
268
+ );
269
+ });
@@ -0,0 +1,152 @@
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 = 3013;
7
+ const HOST = `http://localhost:${PORT}`;
8
+
9
+ const TOP_TOOLBAR_BUTTON_IDS = [
10
+ "full",
11
+ "grid",
12
+ "traceBtn",
13
+ "traceClear",
14
+ "setbkg",
15
+ "addtext",
16
+ "resetall",
17
+ "go",
18
+ ];
19
+
20
+ let server;
21
+
22
+ async function waitForServer(maxAttempts = 20) {
23
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
24
+ const ok = await new Promise((resolve) => {
25
+ const req = http.get(`${HOST}/`, (res) => {
26
+ res.destroy();
27
+ resolve(true);
28
+ });
29
+ req.on("error", () => resolve(false));
30
+ });
31
+ if (ok) {
32
+ return;
33
+ }
34
+ await new Promise((resolve) => setTimeout(resolve, 500));
35
+ }
36
+ throw new Error("Local server did not start in time");
37
+ }
38
+
39
+ beforeAll(async () => {
40
+ server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
41
+ stdio: "ignore",
42
+ });
43
+ await waitForServer();
44
+ }, 30_000);
45
+
46
+ afterAll(() => {
47
+ if (server) {
48
+ server.kill();
49
+ }
50
+ });
51
+
52
+ async function openEditor() {
53
+ const browser = await puppeteer.launch({
54
+ headless: true,
55
+ args: ["--no-sandbox"],
56
+ });
57
+ const page = await browser.newPage();
58
+ const errors = [];
59
+
60
+ page.on("pageerror", (err) => errors.push(err.message || String(err)));
61
+ page.on("console", (msg) => {
62
+ if (msg.type() === "error") {
63
+ errors.push(msg.text());
64
+ }
65
+ });
66
+
67
+ await page.goto(`${HOST}/editor.html?mode=edit`, { waitUntil: "networkidle2", timeout: 30_000 });
68
+
69
+ return { browser, page, errors };
70
+ }
71
+
72
+ describe("top toolbar tooltips", () => {
73
+ it(
74
+ "renders bounded app-owned tooltips for hover and keyboard focus",
75
+ async () => {
76
+ const { browser, page, errors } = await openEditor();
77
+
78
+ try {
79
+ await page.waitForSelector("#stageframe", { timeout: 30_000 });
80
+ await page.waitForFunction(
81
+ (buttonIds) =>
82
+ buttonIds.every((id) => {
83
+ const button = document.getElementById(id);
84
+ return button && button.tagName === "BUTTON" && button.getAttribute("aria-label");
85
+ }),
86
+ { timeout: 30_000 },
87
+ TOP_TOOLBAR_BUTTON_IDS
88
+ );
89
+
90
+ const invalidTooltips = await page.evaluate((buttonIds) => {
91
+ return buttonIds
92
+ .map((id) => {
93
+ const button = document.getElementById(id);
94
+ const label = button.getAttribute("aria-label") || "";
95
+ return {
96
+ id,
97
+ ariaLabel: label,
98
+ nativeTitle: button.getAttribute("title"),
99
+ tooltip: button.getAttribute("data-tooltip") || "",
100
+ };
101
+ })
102
+ .filter(
103
+ (button) =>
104
+ button.nativeTitle !== null ||
105
+ button.tooltip !== button.ariaLabel
106
+ );
107
+ }, TOP_TOOLBAR_BUTTON_IDS);
108
+
109
+ expect(invalidTooltips).toEqual([]);
110
+
111
+ await page.hover("#grid");
112
+ await new Promise((resolve) => setTimeout(resolve, 200));
113
+
114
+ const hoverTooltip = await page.$eval("#grid", (button) => {
115
+ const style = window.getComputedStyle(button, "::after");
116
+ return {
117
+ content: style.content,
118
+ opacity: style.opacity,
119
+ visibility: style.visibility,
120
+ maxWidth: style.maxWidth,
121
+ };
122
+ });
123
+
124
+ expect(hoverTooltip.content).toContain("Grid");
125
+ expect(hoverTooltip.opacity).toBe("1");
126
+ expect(hoverTooltip.visibility).toBe("visible");
127
+ expect(hoverTooltip.maxWidth).toBe("240px");
128
+
129
+ await page.mouse.move(0, 0);
130
+ await page.focus("#traceBtn");
131
+ await new Promise((resolve) => setTimeout(resolve, 200));
132
+
133
+ const focusedTooltip = await page.$eval("#traceBtn", (button) => {
134
+ const style = window.getComputedStyle(button, "::after");
135
+ return {
136
+ focused: document.activeElement === button,
137
+ opacity: style.opacity,
138
+ visibility: style.visibility,
139
+ };
140
+ });
141
+
142
+ expect(focusedTooltip.focused).toBe(true);
143
+ expect(focusedTooltip.opacity).toBe("1");
144
+ expect(focusedTooltip.visibility).toBe("visible");
145
+ expect(errors).toEqual([]);
146
+ } finally {
147
+ await browser.close();
148
+ }
149
+ },
150
+ 60_000
151
+ );
152
+ });