@robotical/martyblocksjr 4.2.0 → 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.
- package/editions/free/src/app.bundle.js +1 -1
- package/editions/free/src/assets/ui/viewOnCompact.png +0 -0
- package/editions/free/src/assets/ui/viewOnTargetCompact.png +0 -0
- package/editions/free/src/css/editor.css +91 -0
- package/editions/free/src/css/editorleftpanel.css +16 -17
- package/editions/free/src/css/editorstage.css +62 -0
- package/editions/free/src/css/paintlook.css +23 -0
- package/editions/free/src/localizations/bg.json +1 -0
- package/editions/free/src/localizations/ca.json +1 -0
- package/editions/free/src/localizations/cs.json +1 -0
- package/editions/free/src/localizations/cy.json +1 -0
- package/editions/free/src/localizations/da.json +1 -0
- package/editions/free/src/localizations/de.json +1 -0
- package/editions/free/src/localizations/el.json +1 -0
- package/editions/free/src/localizations/en.json +1 -0
- package/editions/free/src/localizations/es.json +1 -0
- package/editions/free/src/localizations/fi.json +1 -0
- package/editions/free/src/localizations/fr.json +1 -0
- package/editions/free/src/localizations/it.json +1 -0
- package/editions/free/src/localizations/ja.json +1 -0
- package/editions/free/src/localizations/ko.json +1 -0
- package/editions/free/src/localizations/nl.json +1 -0
- package/editions/free/src/localizations/no.json +1 -0
- package/editions/free/src/localizations/pl.json +1 -0
- package/editions/free/src/localizations/pt-br.json +1 -0
- package/editions/free/src/localizations/pt.json +1 -0
- package/editions/free/src/localizations/sv.json +1 -0
- package/editions/free/src/localizations/th.json +1 -0
- package/editions/free/src/localizations/tr.json +1 -0
- package/editions/free/src/localizations/uk.json +1 -0
- package/editions/free/src/localizations/zh-cn.json +1 -0
- package/editions/free/src/localizations/zh-tw.json +1 -0
- package/editions/free/src/media.json +3 -0
- package/package.json +1 -1
- package/tests/e2e/accessibility.e2e.test.js +18 -2
- package/tests/e2e/animated-sprite-paint-disabled.e2e.test.js +270 -0
- package/tests/e2e/background-paint-active-selection.e2e.test.js +162 -0
- package/tests/e2e/blocks-jr-hidden-blocks.e2e.test.js +262 -0
- package/tests/e2e/chromium-79-smoke.test.js +19 -10
- package/tests/e2e/delete-last-sprite-mode.e2e.test.js +162 -0
- package/tests/e2e/duplicate-page.e2e.test.js +414 -0
- package/tests/e2e/marty-script-activation-paths.e2e.test.js +522 -0
- package/tests/e2e/microbit-extension-project-reload.e2e.test.js +6 -1
- package/tests/e2e/paint-editor-cancel.e2e.test.js +274 -0
- package/tests/e2e/project-load-progress-loop.e2e.test.js +6 -1
- package/tests/e2e/project-save-stall.e2e.test.js +230 -0
- package/tests/e2e/sprite-watermark-grayscale.e2e.test.js +269 -0
- package/tests/e2e/top-toolbar-tooltips.e2e.test.js +152 -0
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
describe("animated sprite paint editing", () => {
|
|
76
|
+
it(
|
|
77
|
+
"disables paint editing for the animated bee in the library and sprite panel",
|
|
78
|
+
async () => {
|
|
79
|
+
const { browser, page, errors } = await openEditor();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await page.click("#addsprite");
|
|
83
|
+
await page.waitForSelector("#libframe.appear #BeeSprite1\\.svg", { timeout: 30_000 });
|
|
84
|
+
await page.click("#BeeSprite1\\.svg");
|
|
85
|
+
await page.waitForFunction(
|
|
86
|
+
() => document.getElementById("library_paintme").getAttribute("aria-disabled") === "true",
|
|
87
|
+
{ timeout: 30_000 }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const libraryState = await page.evaluate(() => {
|
|
91
|
+
const paintButton = document.getElementById("library_paintme");
|
|
92
|
+
return {
|
|
93
|
+
ariaDisabled: paintButton.getAttribute("aria-disabled"),
|
|
94
|
+
opacity: paintButton.style.opacity,
|
|
95
|
+
hasClickHandler: typeof paintButton.onclick === "function",
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(libraryState).toEqual({
|
|
100
|
+
ariaDisabled: "true",
|
|
101
|
+
opacity: "0",
|
|
102
|
+
hasClickHandler: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
106
|
+
await page.click("#BeeSprite1\\.svg");
|
|
107
|
+
await page.waitForFunction(
|
|
108
|
+
() => {
|
|
109
|
+
const sprite = window.ScratchJr && window.ScratchJr.getSprite();
|
|
110
|
+
return Boolean(sprite && sprite.animationFrames && sprite.animationFrames.length === 3);
|
|
111
|
+
},
|
|
112
|
+
{ timeout: 30_000 }
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const beeThumbId = await page.evaluate(() => window.ScratchJr.getSprite().thumbnail.id);
|
|
116
|
+
await page.click(`[id="${beeThumbId}"]`);
|
|
117
|
+
await page.waitForFunction(
|
|
118
|
+
() => window.ScratchJr.getSprite().thumbnail.className.includes("paintnoneditable"),
|
|
119
|
+
{ timeout: 30_000 }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const spriteState = await page.evaluate(() => {
|
|
123
|
+
const sprite = window.ScratchJr.getSprite();
|
|
124
|
+
const thumb = sprite.thumbnail;
|
|
125
|
+
const brush = thumb.querySelector(".brush");
|
|
126
|
+
const paintFrame = document.getElementById("paintframe");
|
|
127
|
+
brush.dispatchEvent(new PointerEvent("pointerdown", {
|
|
128
|
+
bubbles: true,
|
|
129
|
+
cancelable: true,
|
|
130
|
+
pointerType: "mouse",
|
|
131
|
+
}));
|
|
132
|
+
return {
|
|
133
|
+
thumbClass: thumb.className,
|
|
134
|
+
brushDisplay: window.getComputedStyle(brush).display,
|
|
135
|
+
paintFrameVisible: window.getComputedStyle(paintFrame).display !== "none",
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(spriteState.thumbClass).toContain("paintnoneditable");
|
|
140
|
+
expect(spriteState.brushDisplay).toBe("none");
|
|
141
|
+
expect(spriteState.paintFrameVisible).toBe(false);
|
|
142
|
+
expect(errors).toEqual([]);
|
|
143
|
+
} finally {
|
|
144
|
+
await browser.close();
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
60_000
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
it(
|
|
151
|
+
"keeps paint editing available and contained within ordinary sprite cards",
|
|
152
|
+
async () => {
|
|
153
|
+
const { browser, page, errors } = await openEditor();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await page.click("#addsprite");
|
|
157
|
+
await page.waitForSelector("#libframe.appear #Star\\.svg", { timeout: 30_000 });
|
|
158
|
+
await page.click("#Star\\.svg");
|
|
159
|
+
await page.waitForFunction(
|
|
160
|
+
() => {
|
|
161
|
+
const star = document.getElementById("Star.svg");
|
|
162
|
+
const paintButton = document.getElementById("library_paintme");
|
|
163
|
+
return Boolean(
|
|
164
|
+
star
|
|
165
|
+
&& star.className.includes("on")
|
|
166
|
+
&& paintButton.getAttribute("aria-disabled") === "false"
|
|
167
|
+
&& typeof paintButton.onclick === "function"
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
{ timeout: 30_000 }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
174
|
+
await page.click("#Star\\.svg");
|
|
175
|
+
await page.waitForFunction(
|
|
176
|
+
() => {
|
|
177
|
+
const sprite = window.ScratchJr && window.ScratchJr.getSprite();
|
|
178
|
+
return Boolean(sprite && sprite.md5 === "Star.svg");
|
|
179
|
+
},
|
|
180
|
+
{ timeout: 30_000 }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const starThumbId = await page.evaluate(() => window.ScratchJr.getSprite().thumbnail.id);
|
|
184
|
+
await page.click(`[id="${starThumbId}"]`);
|
|
185
|
+
await page.waitForFunction(
|
|
186
|
+
() => window.ScratchJr.getSprite().thumbnail.className.includes("on"),
|
|
187
|
+
{ timeout: 30_000 }
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const layoutState = await page.evaluate(() => {
|
|
191
|
+
const visibleThumbs = Array.from(document.querySelectorAll("#spritecc .spritethumb"))
|
|
192
|
+
.filter((thumb) => window.getComputedStyle(thumb).display !== "none");
|
|
193
|
+
const selectedThumb = visibleThumbs.find((thumb) => thumb.getAttribute("aria-pressed") === "true");
|
|
194
|
+
const unselectedThumb = visibleThumbs.find((thumb) => thumb.getAttribute("aria-pressed") === "false");
|
|
195
|
+
const addSpriteButton = document.getElementById("addsprite");
|
|
196
|
+
const name = selectedThumb && selectedThumb.querySelector(".sname");
|
|
197
|
+
const brush = selectedThumb && selectedThumb.querySelector(".brush");
|
|
198
|
+
|
|
199
|
+
if (!selectedThumb || !unselectedThumb || !addSpriteButton || !name || !brush) {
|
|
200
|
+
throw new Error("Sprite card layout controls were not found");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const rectFor = (element) => {
|
|
204
|
+
const rect = element.getBoundingClientRect();
|
|
205
|
+
return {
|
|
206
|
+
top: rect.top,
|
|
207
|
+
right: rect.right,
|
|
208
|
+
bottom: rect.bottom,
|
|
209
|
+
left: rect.left,
|
|
210
|
+
width: rect.width,
|
|
211
|
+
height: rect.height,
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const selectedStyle = window.getComputedStyle(selectedThumb);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
selected: rectFor(selectedThumb),
|
|
218
|
+
unselected: rectFor(unselectedThumb),
|
|
219
|
+
addSprite: rectFor(addSpriteButton),
|
|
220
|
+
name: rectFor(name),
|
|
221
|
+
brush: rectFor(brush),
|
|
222
|
+
selectedBackgroundImage: selectedStyle.backgroundImage,
|
|
223
|
+
selectedBackgroundSize: selectedStyle.backgroundSize,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(layoutState.selected.width).toBeCloseTo(layoutState.unselected.width, 1);
|
|
228
|
+
expect(layoutState.selected.width).toBeCloseTo(layoutState.addSprite.width, 1);
|
|
229
|
+
expect(layoutState.selected.height).toBeCloseTo(layoutState.unselected.height, 1);
|
|
230
|
+
expect(layoutState.selectedBackgroundImage).toContain("viewOnCompact.png");
|
|
231
|
+
expect(layoutState.selectedBackgroundSize).toBe("100% 100%");
|
|
232
|
+
expect(layoutState.name.right).toBeLessThanOrEqual(layoutState.brush.left + 0.5);
|
|
233
|
+
expect(layoutState.brush.left).toBeGreaterThanOrEqual(layoutState.selected.left);
|
|
234
|
+
expect(layoutState.brush.right).toBeLessThanOrEqual(layoutState.selected.right + 0.5);
|
|
235
|
+
expect(layoutState.brush.top).toBeGreaterThanOrEqual(layoutState.selected.top);
|
|
236
|
+
expect(layoutState.brush.bottom).toBeLessThanOrEqual(layoutState.selected.bottom);
|
|
237
|
+
|
|
238
|
+
const editableState = await page.evaluate(() => {
|
|
239
|
+
const sprite = window.ScratchJr.getSprite();
|
|
240
|
+
const thumb = sprite.thumbnail;
|
|
241
|
+
const brush = thumb.querySelector(".brush");
|
|
242
|
+
brush.dispatchEvent(new PointerEvent("pointerdown", {
|
|
243
|
+
bubbles: true,
|
|
244
|
+
cancelable: true,
|
|
245
|
+
pointerType: "mouse",
|
|
246
|
+
}));
|
|
247
|
+
return {
|
|
248
|
+
thumbClass: thumb.className,
|
|
249
|
+
brushDisplay: window.getComputedStyle(brush).display,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await page.waitForFunction(
|
|
254
|
+
() => {
|
|
255
|
+
const paintFrame = document.getElementById("paintframe");
|
|
256
|
+
return paintFrame && window.getComputedStyle(paintFrame).display !== "none";
|
|
257
|
+
},
|
|
258
|
+
{ timeout: 30_000 }
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
expect(editableState.thumbClass).not.toContain("paintnoneditable");
|
|
262
|
+
expect(editableState.brushDisplay).not.toBe("none");
|
|
263
|
+
expect(errors).toEqual([]);
|
|
264
|
+
} finally {
|
|
265
|
+
await browser.close();
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
60_000
|
|
269
|
+
);
|
|
270
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
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 = 3017;
|
|
7
|
+
const HOST = `http://localhost:${PORT}`;
|
|
8
|
+
|
|
9
|
+
let server;
|
|
10
|
+
|
|
11
|
+
const waitForServer = () =>
|
|
12
|
+
new Promise((resolve, reject) => {
|
|
13
|
+
const started = Date.now();
|
|
14
|
+
|
|
15
|
+
const check = () => {
|
|
16
|
+
http.get(HOST, res => {
|
|
17
|
+
res.resume();
|
|
18
|
+
resolve();
|
|
19
|
+
}).on("error", err => {
|
|
20
|
+
if (Date.now() - started > 10000) {
|
|
21
|
+
reject(err);
|
|
22
|
+
} else {
|
|
23
|
+
setTimeout(check, 100);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
check();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const openEditor = async () => {
|
|
32
|
+
const browser = await puppeteer.launch({
|
|
33
|
+
headless: true,
|
|
34
|
+
args: ["--no-sandbox"]
|
|
35
|
+
});
|
|
36
|
+
const page = await browser.newPage();
|
|
37
|
+
const errors = [];
|
|
38
|
+
|
|
39
|
+
page.on("pageerror", error => errors.push(error.message));
|
|
40
|
+
page.on("console", msg => {
|
|
41
|
+
if (msg.type() === "error") {
|
|
42
|
+
errors.push(msg.text());
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await page.goto(`${HOST}/editor.html?mode=edit`, { waitUntil: "domcontentloaded" });
|
|
47
|
+
await page.waitForFunction(() =>
|
|
48
|
+
window.ScratchJr &&
|
|
49
|
+
ScratchJr.stage &&
|
|
50
|
+
ScratchJr.stage.currentPage &&
|
|
51
|
+
!ScratchJr.onHold &&
|
|
52
|
+
ScratchJr.getActiveScript &&
|
|
53
|
+
ScratchJr.getActiveScript()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return { browser, page, errors };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const addPinkOctopus = async page => {
|
|
60
|
+
await page.evaluate(() => {
|
|
61
|
+
ScratchJr.stage.currentPage.addSprite(0.5, "Sprites_Octopus-Pink.svg", "Octopus-Pink");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await page.waitForFunction(() => {
|
|
65
|
+
const sprites = JSON.parse(ScratchJr.stage.currentPage.sprites);
|
|
66
|
+
const spriteId = sprites.find(id => id.indexOf("Octopus-Pink") === 0);
|
|
67
|
+
if (!spriteId) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sprite = document.getElementById(spriteId);
|
|
72
|
+
const script = document.getElementById(`${spriteId}_scripts`);
|
|
73
|
+
const thumb = Array.from(document.querySelectorAll("#spritecc .spritethumb"))
|
|
74
|
+
.find(node => node.owner === spriteId);
|
|
75
|
+
|
|
76
|
+
return sprite && script && thumb;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return page.evaluate(() => {
|
|
80
|
+
const sprites = JSON.parse(ScratchJr.stage.currentPage.sprites);
|
|
81
|
+
return sprites.find(id => id.indexOf("Octopus-Pink") === 0);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const setCurrentSprite = async (page, spriteId) => {
|
|
86
|
+
await page.evaluate(id => {
|
|
87
|
+
const sprite = document.getElementById(id).owner;
|
|
88
|
+
ScratchJr.stage.currentPage.setCurrentSprite(sprite);
|
|
89
|
+
}, spriteId);
|
|
90
|
+
|
|
91
|
+
await page.waitForFunction(id =>
|
|
92
|
+
ScratchJr.stage.currentPage.currentSpriteName === id,
|
|
93
|
+
{}, spriteId);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const selectMotionCategory = async page => {
|
|
97
|
+
await page.click("#sprite-motion");
|
|
98
|
+
await page.waitForSelector("#forward_block", { visible: true });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const selectMartyMotionCategory = async page => {
|
|
102
|
+
await page.click("#marty-motion");
|
|
103
|
+
await page.waitForSelector("#martyStepForward_block", { visible: true });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const dragBlockToCanvas = async (page, selector) => {
|
|
107
|
+
const source = await page.$eval(selector, node => {
|
|
108
|
+
const rect = node.getBoundingClientRect();
|
|
109
|
+
return {
|
|
110
|
+
x: rect.left + rect.width / 2,
|
|
111
|
+
y: rect.top + rect.height / 2
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
const target = await page.$eval("#scripts", node => {
|
|
115
|
+
const rect = node.getBoundingClientRect();
|
|
116
|
+
return {
|
|
117
|
+
x: rect.left + Math.min(220, rect.width / 2),
|
|
118
|
+
y: rect.top + Math.min(170, rect.height / 2)
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await page.mouse.move(source.x, source.y);
|
|
123
|
+
await page.mouse.down();
|
|
124
|
+
await page.mouse.move(target.x, target.y, { steps: 12 });
|
|
125
|
+
await page.mouse.up();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const enableMartyMode = async page => {
|
|
129
|
+
const enabled = await page.evaluate(() => ScratchJr.isMartyModeEnabled);
|
|
130
|
+
if (!enabled) {
|
|
131
|
+
await page.click("#martyMode");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await page.waitForFunction(() =>
|
|
135
|
+
ScratchJr.isMartyModeEnabled &&
|
|
136
|
+
ScratchJr.stage.currentPage.currentSpriteName &&
|
|
137
|
+
ScratchJr.stage.currentPage.currentSpriteName.indexOf(ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1 &&
|
|
138
|
+
ScratchJr.getActiveScript()
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const clickSpriteThumb = async (page, spriteId) => {
|
|
143
|
+
await page.click(`#spritecc .spritethumb[data-owner="${spriteId}"]`);
|
|
144
|
+
await page.waitForFunction(id =>
|
|
145
|
+
ScratchJr.stage.currentPage.currentSpriteName === id &&
|
|
146
|
+
window.getComputedStyle(document.getElementById(`${id}_scripts`)).visibility === "visible",
|
|
147
|
+
{}, spriteId);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const getSpriteScriptState = async (page, spriteId) =>
|
|
151
|
+
page.evaluate(id => {
|
|
152
|
+
const script = document.getElementById(`${id}_scripts`);
|
|
153
|
+
const blockTypes = script.owner.getBlocks().map(block => block.blocktype);
|
|
154
|
+
const visibleBlockTypes = script.owner.getBlocks()
|
|
155
|
+
.filter(block => window.getComputedStyle(block.div).visibility !== "hidden")
|
|
156
|
+
.map(block => block.blocktype);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
currentSpriteName: ScratchJr.stage.currentPage.currentSpriteName,
|
|
160
|
+
activeScriptId: ScratchJr.getActiveScript().id,
|
|
161
|
+
scriptVisibility: window.getComputedStyle(script).visibility,
|
|
162
|
+
blockTypes,
|
|
163
|
+
visibleBlockTypes
|
|
164
|
+
};
|
|
165
|
+
}, spriteId);
|
|
166
|
+
|
|
167
|
+
describe("Blocks Jr hidden block drop regression", () => {
|
|
168
|
+
beforeAll(async () => {
|
|
169
|
+
server = spawn("python3", ["-m", "http.server", String(PORT), "--directory", "editions/free/src"], {
|
|
170
|
+
stdio: "ignore"
|
|
171
|
+
});
|
|
172
|
+
await waitForServer();
|
|
173
|
+
}, 15000);
|
|
174
|
+
|
|
175
|
+
afterAll(() => {
|
|
176
|
+
if (server) {
|
|
177
|
+
server.kill();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("shows a dropped block when the current sprite changes outside the thumbnail path", async () => {
|
|
182
|
+
const { browser, page, errors } = await openEditor();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const octopusId = await addPinkOctopus(page);
|
|
186
|
+
const martyId = await page.evaluate(() =>
|
|
187
|
+
Array.from(document.querySelectorAll("#spritecc .spritethumb"))
|
|
188
|
+
.filter(node => node.owner && node.owner.indexOf("Marty ") === 0)
|
|
189
|
+
.find(node => window.getComputedStyle(node).display !== "none").owner
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
await clickSpriteThumb(page, martyId);
|
|
193
|
+
await setCurrentSprite(page, octopusId);
|
|
194
|
+
await selectMotionCategory(page);
|
|
195
|
+
await dragBlockToCanvas(page, "#forward_block");
|
|
196
|
+
|
|
197
|
+
await page.waitForFunction(id => {
|
|
198
|
+
const script = document.getElementById(`${id}_scripts`);
|
|
199
|
+
return script.owner.getBlocks().some(block => block.blocktype === "forward");
|
|
200
|
+
}, {}, octopusId);
|
|
201
|
+
|
|
202
|
+
const state = await getSpriteScriptState(page, octopusId);
|
|
203
|
+
expect(state.currentSpriteName).toBe(octopusId);
|
|
204
|
+
expect(state.activeScriptId).toBe(`${octopusId}_scripts`);
|
|
205
|
+
expect(state.blockTypes).toContain("forward");
|
|
206
|
+
expect(state.scriptVisibility).toBe("visible");
|
|
207
|
+
expect(state.visibleBlockTypes).toContain("forward");
|
|
208
|
+
expect(errors).toEqual([]);
|
|
209
|
+
} finally {
|
|
210
|
+
await browser.close();
|
|
211
|
+
}
|
|
212
|
+
}, 60000);
|
|
213
|
+
|
|
214
|
+
it("keeps Marty blocks visible after adding a page and returning to page 1", async () => {
|
|
215
|
+
const { browser, page, errors } = await openEditor();
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await enableMartyMode(page);
|
|
219
|
+
const pageOne = await page.evaluate(() => ({
|
|
220
|
+
pageId: ScratchJr.stage.currentPage.id,
|
|
221
|
+
spriteId: ScratchJr.stage.currentPage.currentSpriteName,
|
|
222
|
+
pageCount: ScratchJr.stage.pages.length
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
await page.click("#emptypage");
|
|
226
|
+
await page.waitForFunction((pageId, pageCount) =>
|
|
227
|
+
ScratchJr.stage.pages.length === pageCount + 1 &&
|
|
228
|
+
ScratchJr.stage.currentPage.id !== pageId &&
|
|
229
|
+
ScratchJr.isMartyModeEnabled &&
|
|
230
|
+
ScratchJr.stage.currentPage.currentSpriteName &&
|
|
231
|
+
ScratchJr.stage.currentPage.currentSpriteName.indexOf(ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1 &&
|
|
232
|
+
!ScratchJr.onHold &&
|
|
233
|
+
ScratchJr.getActiveScript(),
|
|
234
|
+
{}, pageOne.pageId, pageOne.pageCount);
|
|
235
|
+
|
|
236
|
+
await page.click(`.pagethumb[data-owner="${pageOne.pageId}"]`);
|
|
237
|
+
await page.waitForFunction((pageId, spriteId) =>
|
|
238
|
+
ScratchJr.stage.currentPage.id === pageId &&
|
|
239
|
+
ScratchJr.stage.currentPage.currentSpriteName === spriteId &&
|
|
240
|
+
ScratchJr.isMartyModeEnabled &&
|
|
241
|
+
ScratchJr.getActiveScript().id === `${spriteId}_scripts`,
|
|
242
|
+
{}, pageOne.pageId, pageOne.spriteId);
|
|
243
|
+
|
|
244
|
+
await selectMartyMotionCategory(page);
|
|
245
|
+
await dragBlockToCanvas(page, "#martyStepForward_block");
|
|
246
|
+
await page.waitForFunction(spriteId => {
|
|
247
|
+
const script = document.getElementById(`${spriteId}_scripts`);
|
|
248
|
+
return script.owner.getBlocks().some(block => block.blocktype === "martyStepForward");
|
|
249
|
+
}, {}, pageOne.spriteId);
|
|
250
|
+
|
|
251
|
+
const state = await getSpriteScriptState(page, pageOne.spriteId);
|
|
252
|
+
expect(state.currentSpriteName).toBe(pageOne.spriteId);
|
|
253
|
+
expect(state.activeScriptId).toBe(`${pageOne.spriteId}_scripts`);
|
|
254
|
+
expect(state.blockTypes).toContain("martyStepForward");
|
|
255
|
+
expect(state.scriptVisibility).toBe("visible");
|
|
256
|
+
expect(state.visibleBlockTypes).toContain("martyStepForward");
|
|
257
|
+
expect(errors).toEqual([]);
|
|
258
|
+
} finally {
|
|
259
|
+
await browser.close();
|
|
260
|
+
}
|
|
261
|
+
}, 60000);
|
|
262
|
+
});
|