@robotical/martyblocksjr 4.1.2 → 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/.firebase/hosting.ZWRpdGlvbnMvZnJlZS9zcmM.cache +599 -534
- package/editions/free/src/app.bundle.js +1 -1
- package/editions/free/src/assets/blockicons/microbitbuttona.svg +33 -0
- package/editions/free/src/assets/blockicons/microbitbuttonab.svg +33 -0
- package/editions/free/src/assets/blockicons/microbitbuttonb.svg +33 -0
- package/editions/free/src/assets/blockicons/microbitdisplayclear.svg +29 -0
- package/editions/free/src/assets/blockicons/microbitdisplayhappy.svg +36 -0
- package/editions/free/src/assets/blockicons/microbitdisplayheart.svg +42 -0
- package/editions/free/src/assets/blockicons/microbitdisplaytext.svg +29 -0
- package/editions/free/src/assets/blockicons/microbitgesturejumped.svg +40 -0
- package/editions/free/src/assets/blockicons/microbitgesturemoved.svg +41 -0
- package/editions/free/src/assets/blockicons/microbitgestureshaken.svg +45 -0
- package/editions/free/src/assets/blockicons/microbittiltany.svg +45 -0
- package/editions/free/src/assets/blockicons/microbittiltbackward.svg +39 -0
- package/editions/free/src/assets/blockicons/microbittiltforward.svg +39 -0
- package/editions/free/src/assets/blockicons/microbittiltleft.svg +39 -0
- package/editions/free/src/assets/blockicons/microbittiltright.svg +39 -0
- package/editions/free/src/assets/categories/MicroBitLooksOff.svg +17 -0
- package/editions/free/src/assets/categories/MicroBitLooksOn.svg +17 -0
- package/editions/free/src/assets/categories/MicroBitStartOff.svg +17 -0
- package/editions/free/src/assets/categories/MicroBitStartOn.svg +17 -0
- 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 +334 -1
- package/editions/free/src/css/editorleftpanel.css +86 -26
- 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 +10 -1
- package/editions/free/src/localizations/ca.json +10 -1
- package/editions/free/src/localizations/cs.json +10 -1
- package/editions/free/src/localizations/cy.json +10 -1
- package/editions/free/src/localizations/da.json +10 -1
- package/editions/free/src/localizations/de.json +10 -1
- package/editions/free/src/localizations/el.json +10 -1
- package/editions/free/src/localizations/en.json +10 -1
- package/editions/free/src/localizations/es.json +10 -1
- package/editions/free/src/localizations/fi.json +10 -1
- package/editions/free/src/localizations/fr.json +10 -1
- package/editions/free/src/localizations/it.json +10 -1
- package/editions/free/src/localizations/ja.json +10 -1
- package/editions/free/src/localizations/ko.json +10 -1
- package/editions/free/src/localizations/nl.json +10 -1
- package/editions/free/src/localizations/no.json +10 -1
- package/editions/free/src/localizations/pl.json +10 -1
- package/editions/free/src/localizations/pt-br.json +10 -1
- package/editions/free/src/localizations/pt.json +10 -1
- package/editions/free/src/localizations/sv.json +10 -1
- package/editions/free/src/localizations/th.json +10 -1
- package/editions/free/src/localizations/tr.json +10 -1
- package/editions/free/src/localizations/uk.json +10 -1
- package/editions/free/src/localizations/zh-cn.json +10 -1
- package/editions/free/src/localizations/zh-tw.json +10 -1
- package/editions/free/src/media.json +15 -0
- package/editions/free/src/pnglibrary/BeeSprite1.png +0 -0
- package/editions/free/src/svglibrary/BeeSprite1.svg +4 -0
- package/editions/free/src/svglibrary/BeeSprite2.svg +4 -0
- package/editions/free/src/svglibrary/BeeSprite3.svg +4 -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 +235 -0
- 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-connection-ui.e2e.test.js +56 -0
- package/tests/e2e/marty-script-activation-paths.e2e.test.js +522 -0
- package/tests/e2e/microbit-blocks.test.js +162 -0
- package/tests/e2e/microbit-extension-project-reload.e2e.test.js +309 -0
- package/tests/e2e/microbit-web-bluetooth.test.js +131 -0
- package/tests/e2e/page-background-loading.test.js +152 -0
- package/tests/e2e/paint-editor-cancel.e2e.test.js +274 -0
- package/tests/e2e/project-load-progress-loop.e2e.test.js +310 -0
- 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
- package/vitest.config.js +2 -3
|
@@ -0,0 +1,274 @@
|
|
|
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 = 3016;
|
|
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 waitForEditorReady(page);
|
|
58
|
+
|
|
59
|
+
return { browser, page, errors };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function waitForEditorReady(page) {
|
|
63
|
+
await page.waitForFunction(
|
|
64
|
+
() => {
|
|
65
|
+
const backdrop = document.getElementById("backdrop");
|
|
66
|
+
return Boolean(
|
|
67
|
+
window.ScratchJr
|
|
68
|
+
&& window.ScratchJr.stage
|
|
69
|
+
&& window.ScratchJr.stage.currentPage
|
|
70
|
+
&& document.getElementById("setbkg")
|
|
71
|
+
&& document.querySelector("#spritecc .spritethumb.on .brush")
|
|
72
|
+
&& backdrop
|
|
73
|
+
&& window.getComputedStyle(backdrop).display === "none"
|
|
74
|
+
&& !window.ScratchJr.onHold
|
|
75
|
+
&& window.ScratchJr.getActiveScript()
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
{ timeout: 30_000 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function openSpritePaintEditor(page) {
|
|
83
|
+
await page.click("#addsprite");
|
|
84
|
+
await page.waitForSelector("#libframe.appear #Star\\.svg", { timeout: 30_000 });
|
|
85
|
+
await page.click("#Star\\.svg");
|
|
86
|
+
await page.waitForFunction(
|
|
87
|
+
() => {
|
|
88
|
+
const star = document.getElementById("Star.svg");
|
|
89
|
+
const paintButton = document.getElementById("library_paintme");
|
|
90
|
+
return Boolean(
|
|
91
|
+
star
|
|
92
|
+
&& star.className.includes("on")
|
|
93
|
+
&& paintButton.getAttribute("aria-disabled") === "false"
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
{ timeout: 30_000 }
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
100
|
+
await page.click("#Star\\.svg");
|
|
101
|
+
await page.waitForFunction(
|
|
102
|
+
() => {
|
|
103
|
+
const sprite = window.ScratchJr && window.ScratchJr.getSprite();
|
|
104
|
+
return Boolean(sprite && sprite.md5 === "Star.svg");
|
|
105
|
+
},
|
|
106
|
+
{ timeout: 30_000 }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const starThumbId = await page.evaluate(() => window.ScratchJr.getSprite().thumbnail.id);
|
|
110
|
+
await page.click(`[id="${starThumbId}"]`);
|
|
111
|
+
await page.waitForFunction(
|
|
112
|
+
() => {
|
|
113
|
+
const sprite = window.ScratchJr && window.ScratchJr.getSprite();
|
|
114
|
+
const brush = sprite && sprite.thumbnail && sprite.thumbnail.querySelector(".brush");
|
|
115
|
+
return Boolean(
|
|
116
|
+
sprite
|
|
117
|
+
&& sprite.md5 === "Star.svg"
|
|
118
|
+
&& sprite.thumbnail.className.includes("on")
|
|
119
|
+
&& brush
|
|
120
|
+
&& window.getComputedStyle(brush).display !== "none"
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
{ timeout: 30_000 }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
await page.evaluate(() => {
|
|
127
|
+
const sprite = window.ScratchJr.getSprite();
|
|
128
|
+
const brush = sprite && sprite.thumbnail && sprite.thumbnail.querySelector(".brush");
|
|
129
|
+
if (!brush || window.getComputedStyle(brush).display === "none") {
|
|
130
|
+
throw new Error("Selected editable sprite paintbrush control was not found");
|
|
131
|
+
}
|
|
132
|
+
brush.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true, pointerType: "mouse" }));
|
|
133
|
+
});
|
|
134
|
+
await waitForPaintEditor(page);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function openBackgroundPaintEditor(page) {
|
|
138
|
+
await page.click("#setbkg");
|
|
139
|
+
await page.waitForSelector("#libframe.appear #library_paintme", { timeout: 30_000 });
|
|
140
|
+
await page.click("#library_paintme");
|
|
141
|
+
await waitForPaintEditor(page);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function waitForPaintEditor(page) {
|
|
145
|
+
await page.waitForFunction(
|
|
146
|
+
() => {
|
|
147
|
+
const paintFrame = document.getElementById("paintframe");
|
|
148
|
+
const doneCheck = document.getElementById("donecheck");
|
|
149
|
+
const layer = document.getElementById("layer1");
|
|
150
|
+
return Boolean(
|
|
151
|
+
paintFrame
|
|
152
|
+
&& doneCheck
|
|
153
|
+
&& layer
|
|
154
|
+
&& layer.childElementCount > 0
|
|
155
|
+
&& window.getComputedStyle(paintFrame).display !== "none"
|
|
156
|
+
&& window.getComputedStyle(doneCheck).display !== "none"
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
{ timeout: 30_000 }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function getPaintToolbarControls(page) {
|
|
164
|
+
return page.evaluate(() => {
|
|
165
|
+
const paintFrame = document.getElementById("paintframe");
|
|
166
|
+
const toolbar = paintFrame && paintFrame.querySelector(".paintop");
|
|
167
|
+
const controls = toolbar ? Array.from(toolbar.querySelectorAll("*")) : [];
|
|
168
|
+
const visibleControls = controls
|
|
169
|
+
.filter((element) => {
|
|
170
|
+
const styles = window.getComputedStyle(element);
|
|
171
|
+
return styles.display !== "none" && styles.visibility !== "hidden" && element.offsetWidth > 0 && element.offsetHeight > 0;
|
|
172
|
+
})
|
|
173
|
+
.map((element) => {
|
|
174
|
+
const styles = window.getComputedStyle(element);
|
|
175
|
+
return {
|
|
176
|
+
id: element.id,
|
|
177
|
+
className: element.className,
|
|
178
|
+
ariaLabel: element.getAttribute("aria-label") || "",
|
|
179
|
+
title: element.getAttribute("title") || "",
|
|
180
|
+
backgroundImage: styles.backgroundImage,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const isAcceptControl = (control) => (
|
|
185
|
+
control.id === "donecheck"
|
|
186
|
+
|| /\bpaintdone\b/.test(control.className)
|
|
187
|
+
|| /ok\.svg|check|accept|done/i.test(`${control.backgroundImage} ${control.ariaLabel} ${control.title}`)
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const isCancelControl = (control) => (
|
|
191
|
+
/\b(cancelicon|paintcancel|closeModal|closex)\b/.test(control.className)
|
|
192
|
+
|| /\b(cancel|close|dismiss)\b/i.test(`${control.id} ${control.ariaLabel} ${control.title}`)
|
|
193
|
+
|| /cancel\.svg|close/i.test(control.backgroundImage)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
hasToolbar: Boolean(toolbar),
|
|
198
|
+
visibleControls,
|
|
199
|
+
acceptControls: visibleControls.filter(isAcceptControl),
|
|
200
|
+
cancelControls: visibleControls.filter(isCancelControl),
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function cancelPaintEditor(page, errors) {
|
|
206
|
+
const hitTarget = await page.$eval("#cancelcheck", (button) => {
|
|
207
|
+
const rect = button.getBoundingClientRect();
|
|
208
|
+
const target = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
209
|
+
return target ? { id: target.id, className: target.className, tagName: target.tagName } : null;
|
|
210
|
+
});
|
|
211
|
+
await page.click("#cancelcheck");
|
|
212
|
+
try {
|
|
213
|
+
await page.waitForFunction(() => {
|
|
214
|
+
const paintFrame = document.getElementById("paintframe");
|
|
215
|
+
const editorFrame = document.getElementById("frame");
|
|
216
|
+
return paintFrame
|
|
217
|
+
&& editorFrame
|
|
218
|
+
&& window.getComputedStyle(paintFrame).display === "none"
|
|
219
|
+
&& window.getComputedStyle(editorFrame).display !== "none";
|
|
220
|
+
}, { timeout: 5_000 });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const state = await page.evaluate(() => ({
|
|
223
|
+
paintFrameClass: document.getElementById("paintframe").className,
|
|
224
|
+
paintFrameDisplay: window.getComputedStyle(document.getElementById("paintframe")).display,
|
|
225
|
+
editorFrameDisplay: window.getComputedStyle(document.getElementById("frame")).display,
|
|
226
|
+
cancelHasClickHandler: typeof document.getElementById("cancelcheck").onclick === "function",
|
|
227
|
+
}));
|
|
228
|
+
throw new Error(`Cancel control did not close the paint editor: ${JSON.stringify({ hitTarget, state, errors })}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
describe("Paint editor cancel controls", () => {
|
|
233
|
+
it(
|
|
234
|
+
"offers a cancel control in the sprite paint editor",
|
|
235
|
+
async () => {
|
|
236
|
+
const { browser, page, errors } = await openEditor();
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await openSpritePaintEditor(page);
|
|
240
|
+
const spriteToolbar = await getPaintToolbarControls(page);
|
|
241
|
+
|
|
242
|
+
expect(spriteToolbar.hasToolbar).toBe(true);
|
|
243
|
+
expect(spriteToolbar.acceptControls.length).toBeGreaterThan(0);
|
|
244
|
+
expect(spriteToolbar.cancelControls.length).toBeGreaterThan(0);
|
|
245
|
+
await cancelPaintEditor(page, errors);
|
|
246
|
+
expect(errors).toEqual([]);
|
|
247
|
+
} finally {
|
|
248
|
+
await browser.close();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
60_000
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
it(
|
|
255
|
+
"offers a cancel control in the background paint editor",
|
|
256
|
+
async () => {
|
|
257
|
+
const { browser, page, errors } = await openEditor();
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await openBackgroundPaintEditor(page);
|
|
261
|
+
const backgroundToolbar = await getPaintToolbarControls(page);
|
|
262
|
+
|
|
263
|
+
expect(backgroundToolbar.hasToolbar).toBe(true);
|
|
264
|
+
expect(backgroundToolbar.acceptControls.length).toBeGreaterThan(0);
|
|
265
|
+
expect(backgroundToolbar.cancelControls.length).toBeGreaterThan(0);
|
|
266
|
+
await cancelPaintEditor(page, errors);
|
|
267
|
+
expect(errors).toEqual([]);
|
|
268
|
+
} finally {
|
|
269
|
+
await browser.close();
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
60_000
|
|
273
|
+
);
|
|
274
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import puppeteer from "puppeteer";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import http from "http";
|
|
5
|
+
|
|
6
|
+
const PORT = 3014;
|
|
7
|
+
const HOST = `http://localhost:${PORT}`;
|
|
8
|
+
const MICROBIT_BLOCK_TYPE = "microbitdisplayclear";
|
|
9
|
+
const STUCK_PROGRESS_LOG = "setProgress 55 30 1 2";
|
|
10
|
+
|
|
11
|
+
let server;
|
|
12
|
+
|
|
13
|
+
async function waitForServer(maxAttempts = 20) {
|
|
14
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
15
|
+
const ok = await new Promise((resolve) => {
|
|
16
|
+
const req = http.get(`${HOST}/`, (res) => {
|
|
17
|
+
res.destroy();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
req.on("error", () => resolve(false));
|
|
21
|
+
});
|
|
22
|
+
if (ok) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
26
|
+
}
|
|
27
|
+
throw new Error("Local server did not start in time");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
|
|
32
|
+
stdio: "ignore",
|
|
33
|
+
});
|
|
34
|
+
await waitForServer();
|
|
35
|
+
}, 30_000);
|
|
36
|
+
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
if (server) {
|
|
39
|
+
server.kill();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function openPage(pathname) {
|
|
44
|
+
const browser = await puppeteer.launch({
|
|
45
|
+
headless: true,
|
|
46
|
+
args: ["--no-sandbox"],
|
|
47
|
+
});
|
|
48
|
+
const page = await browser.newPage();
|
|
49
|
+
const errors = [];
|
|
50
|
+
const progressLogs = [];
|
|
51
|
+
|
|
52
|
+
page.on("pageerror", (err) => errors.push(err.message || String(err)));
|
|
53
|
+
page.on("console", (msg) => {
|
|
54
|
+
const text = msg.text();
|
|
55
|
+
if (text.indexOf("setProgress") > -1) {
|
|
56
|
+
progressLogs.push(text);
|
|
57
|
+
}
|
|
58
|
+
if (msg.type() === "error") {
|
|
59
|
+
errors.push(text);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await page.goto(`${HOST}${pathname}`, { waitUntil: "networkidle2", timeout: 30_000 });
|
|
64
|
+
|
|
65
|
+
return { browser, page, errors, progressLogs };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function createProjectFromHome(page) {
|
|
69
|
+
await page.waitForSelector("#newproject .card-action-open", { timeout: 30_000 });
|
|
70
|
+
await Promise.all([
|
|
71
|
+
page.waitForNavigation({ waitUntil: "networkidle2", timeout: 30_000 }),
|
|
72
|
+
page.click("#newproject .card-action-open"),
|
|
73
|
+
]);
|
|
74
|
+
await waitForEditorReady(page);
|
|
75
|
+
return page.evaluate(() => window.ScratchJr.currentProject);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function waitForEditorReady(page, timeout = 30_000) {
|
|
79
|
+
await page.waitForFunction(
|
|
80
|
+
() => {
|
|
81
|
+
const backdrop = document.getElementById("backdrop");
|
|
82
|
+
return Boolean(
|
|
83
|
+
window.ScratchJr
|
|
84
|
+
&& window.ScratchJr.stage
|
|
85
|
+
&& window.ScratchJr.stage.currentPage
|
|
86
|
+
&& document.getElementById("addExtensionButton")
|
|
87
|
+
&& document.getElementById("martyMode")
|
|
88
|
+
&& backdrop
|
|
89
|
+
&& window.getComputedStyle(backdrop).display === "none"
|
|
90
|
+
&& !window.ScratchJr.onHold
|
|
91
|
+
&& window.ScratchJr.getActiveScript()
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
{ timeout }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function enableMartyMode(page) {
|
|
99
|
+
const alreadyEnabled = await page.evaluate(() => window.ScratchJr.isMartyModeEnabled);
|
|
100
|
+
if (!alreadyEnabled) {
|
|
101
|
+
await page.click("#martyMode");
|
|
102
|
+
}
|
|
103
|
+
await page.waitForFunction(
|
|
104
|
+
() => window.ScratchJr.isMartyModeEnabled
|
|
105
|
+
&& window.ScratchJr.stage.currentPage.currentSpriteName
|
|
106
|
+
&& window.ScratchJr.stage.currentPage.currentSpriteName.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1
|
|
107
|
+
&& window.ScratchJr.getActiveScript(),
|
|
108
|
+
{ timeout: 30_000 }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function enableMicroBitExtension(page) {
|
|
113
|
+
await page.$eval("#addExtensionButton", (button) => {
|
|
114
|
+
if (!button.disabled || window.getComputedStyle(button).display !== "none") {
|
|
115
|
+
throw new Error("Expected the extensions entry point to remain hidden and disabled");
|
|
116
|
+
}
|
|
117
|
+
button.onclick();
|
|
118
|
+
});
|
|
119
|
+
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
120
|
+
await page.click("#microBitExtensionCard");
|
|
121
|
+
await page.waitForFunction(
|
|
122
|
+
() => {
|
|
123
|
+
const microBitButton = document.getElementById("microBitConnectionButton");
|
|
124
|
+
const library = document.getElementById("extensionsLibrary");
|
|
125
|
+
return window.ScratchJr.isMicroBitExtensionEnabled
|
|
126
|
+
&& microBitButton
|
|
127
|
+
&& window.getComputedStyle(microBitButton).display !== "none"
|
|
128
|
+
&& document.querySelector("#selectorsright #microbit-start")
|
|
129
|
+
&& document.querySelector("#selectorsright #microbit-looks")
|
|
130
|
+
&& library
|
|
131
|
+
&& !library.classList.contains("in");
|
|
132
|
+
},
|
|
133
|
+
{ timeout: 30_000 }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function insertMicroBitBlock(page) {
|
|
138
|
+
await page.click("#microbit-looks");
|
|
139
|
+
await page.waitForFunction(
|
|
140
|
+
(blockType) => Boolean(document.querySelector(`#palette [data-blocktype="${blockType}"]`)),
|
|
141
|
+
{ timeout: 30_000 },
|
|
142
|
+
MICROBIT_BLOCK_TYPE
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await page.evaluate((blockType) => {
|
|
146
|
+
const blockElement = document.querySelector(`#palette [data-blocktype="${blockType}"]`);
|
|
147
|
+
window.Palette.insertBlockFromKeyboard(blockElement, new Event("progress-loop-test"));
|
|
148
|
+
}, MICROBIT_BLOCK_TYPE);
|
|
149
|
+
|
|
150
|
+
await page.waitForFunction(
|
|
151
|
+
(blockType) => window.ScratchJr.getBlocks().some((block) => block.blocktype === blockType),
|
|
152
|
+
{ timeout: 30_000 },
|
|
153
|
+
MICROBIT_BLOCK_TYPE
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function saveProjectAndWaitForBlock(page, projectId) {
|
|
158
|
+
await page.evaluate(() => new Promise((resolve) => {
|
|
159
|
+
window.ScratchJr.changed = true;
|
|
160
|
+
window.ScratchJr.saveProject(null, resolve);
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
await waitForPersistedProject(page, projectId, (project) => projectHasBlock(project, MICROBIT_BLOCK_TYPE));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function waitForPersistedProject(page, projectId, predicate) {
|
|
167
|
+
const startedAt = Date.now();
|
|
168
|
+
let lastProject = null;
|
|
169
|
+
|
|
170
|
+
while (Date.now() - startedAt < 15_000) {
|
|
171
|
+
lastProject = await readPersistedProject(page, projectId);
|
|
172
|
+
if (lastProject && predicate(lastProject)) {
|
|
173
|
+
return lastProject;
|
|
174
|
+
}
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error(`Timed out waiting for saved project state. Last project: ${JSON.stringify(lastProject)}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function readPersistedProject(page, projectId) {
|
|
182
|
+
return page.evaluate((id) => new Promise((resolve) => {
|
|
183
|
+
window.OS.query(
|
|
184
|
+
{ stmt: "select * from projects where id = ?", values: [Number(id)] },
|
|
185
|
+
(result) => {
|
|
186
|
+
const rows = JSON.parse(result);
|
|
187
|
+
if (!rows.length) {
|
|
188
|
+
resolve(null);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const row = Object.fromEntries(Object.entries(rows[0]).map(([key, value]) => [key.toLowerCase(), value]));
|
|
192
|
+
resolve(typeof row.json === "string" ? JSON.parse(row.json) : row.json);
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}), projectId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function projectHasBlock(project, blockType) {
|
|
199
|
+
if (!project || !Array.isArray(project.pages)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return project.pages.some((pageId) => {
|
|
203
|
+
const page = project[pageId];
|
|
204
|
+
return page && Array.isArray(page.sprites) && page.sprites.some((spriteId) => {
|
|
205
|
+
const sprite = page[spriteId];
|
|
206
|
+
return sprite && Array.isArray(sprite.scripts) && sprite.scripts.some((strip) => {
|
|
207
|
+
return Array.isArray(strip) && strip.some((block) => Array.isArray(block) && block[0] === blockType);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function hardReload(page) {
|
|
214
|
+
const client = await page.target().createCDPSession();
|
|
215
|
+
await client.send("Network.enable");
|
|
216
|
+
await client.send("Network.setCacheDisabled", { cacheDisabled: true });
|
|
217
|
+
try {
|
|
218
|
+
await page.reload({ waitUntil: "networkidle2", timeout: 30_000 });
|
|
219
|
+
} finally {
|
|
220
|
+
await client.send("Network.setCacheDisabled", { cacheDisabled: false });
|
|
221
|
+
await client.detach();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function waitForLoadCurtainState(page, progressLogs, timeout = 12_000) {
|
|
226
|
+
const startedAt = Date.now();
|
|
227
|
+
let state = null;
|
|
228
|
+
|
|
229
|
+
while (Date.now() - startedAt < timeout) {
|
|
230
|
+
state = await getLoadCurtainState(page, progressLogs);
|
|
231
|
+
if (state.backdropDisplay === "none") {
|
|
232
|
+
return state;
|
|
233
|
+
}
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return getLoadCurtainState(page, progressLogs);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function getLoadCurtainState(page, progressLogs) {
|
|
241
|
+
const state = await page.evaluate(() => {
|
|
242
|
+
const backdrop = document.getElementById("backdrop");
|
|
243
|
+
const currentSpriteName = window.ScratchJr?.stage?.currentPage?.currentSpriteName || "";
|
|
244
|
+
let activeBlocks = [];
|
|
245
|
+
try {
|
|
246
|
+
if (window.ScratchJr?.stage?.currentPage?.currentSpriteName && window.ScratchJr.getActiveScript()) {
|
|
247
|
+
activeBlocks = window.ScratchJr.getBlocks().map((block) => block.blocktype);
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
activeBlocks = [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
backdropDisplay: backdrop ? window.getComputedStyle(backdrop).display : null,
|
|
255
|
+
backdropClassName: backdrop ? backdrop.className : null,
|
|
256
|
+
currentProject: window.ScratchJr?.currentProject || null,
|
|
257
|
+
isMartyModeEnabled: window.ScratchJr?.isMartyModeEnabled || false,
|
|
258
|
+
isMicroBitExtensionEnabled: window.ScratchJr?.isMicroBitExtensionEnabled || false,
|
|
259
|
+
activeSpriteIsMartyBirdsEye:
|
|
260
|
+
Boolean(window.ScratchJr) && currentSpriteName.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1,
|
|
261
|
+
hasMicroBitBlockInActiveScript: activeBlocks.indexOf("microbitdisplayclear") > -1,
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...state,
|
|
267
|
+
stuckProgressLoopCount: progressLogs.filter((log) => log.indexOf(STUCK_PROGRESS_LOG) > -1).length,
|
|
268
|
+
progressTail: progressLogs.slice(-12),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
describe("project load progress loop", () => {
|
|
273
|
+
it(
|
|
274
|
+
"lifts the loading curtain after a hard refresh of a saved Marty micro:bit project",
|
|
275
|
+
async () => {
|
|
276
|
+
const { browser, page, errors, progressLogs } = await openPage("/home.html?place=home");
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const projectId = await createProjectFromHome(page);
|
|
280
|
+
|
|
281
|
+
await enableMartyMode(page);
|
|
282
|
+
await enableMicroBitExtension(page);
|
|
283
|
+
await insertMicroBitBlock(page);
|
|
284
|
+
await saveProjectAndWaitForBlock(page, projectId);
|
|
285
|
+
|
|
286
|
+
await page.reload({ waitUntil: "networkidle2", timeout: 30_000 });
|
|
287
|
+
await waitForEditorReady(page);
|
|
288
|
+
|
|
289
|
+
await hardReload(page);
|
|
290
|
+
const state = await waitForLoadCurtainState(page, progressLogs);
|
|
291
|
+
|
|
292
|
+
expect(
|
|
293
|
+
state.backdropDisplay,
|
|
294
|
+
`Load curtain state after hard refresh: ${JSON.stringify(state, null, 2)}`
|
|
295
|
+
).toBe("none");
|
|
296
|
+
expect(state).toMatchObject({
|
|
297
|
+
currentProject: projectId,
|
|
298
|
+
isMartyModeEnabled: true,
|
|
299
|
+
isMicroBitExtensionEnabled: true,
|
|
300
|
+
activeSpriteIsMartyBirdsEye: true,
|
|
301
|
+
hasMicroBitBlockInActiveScript: true,
|
|
302
|
+
});
|
|
303
|
+
expect(errors).toEqual([]);
|
|
304
|
+
} finally {
|
|
305
|
+
await browser.close();
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
120_000
|
|
309
|
+
);
|
|
310
|
+
});
|