@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,414 @@
|
|
|
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 = "Beach.svg";
|
|
9
|
+
const SCRIPT_BLOCK_TYPE = "forward";
|
|
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
|
+
|
|
51
|
+
page.on("pageerror", (err) => errors.push(err.message || String(err)));
|
|
52
|
+
page.on("console", (msg) => {
|
|
53
|
+
if (msg.type() === "error") {
|
|
54
|
+
errors.push(msg.text());
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await page.goto(`${HOST}${pathname}`, { waitUntil: "networkidle2", timeout: 30_000 });
|
|
59
|
+
|
|
60
|
+
return { browser, page, errors };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function waitForEditorReady(page) {
|
|
64
|
+
await page.waitForFunction(
|
|
65
|
+
() => {
|
|
66
|
+
const backdrop = document.getElementById("backdrop");
|
|
67
|
+
return Boolean(
|
|
68
|
+
window.ScratchJr
|
|
69
|
+
&& window.ScratchJr.stage
|
|
70
|
+
&& window.ScratchJr.stage.currentPage
|
|
71
|
+
&& document.getElementById("pagecc")
|
|
72
|
+
&& document.getElementById("sprite-motion")
|
|
73
|
+
&& backdrop
|
|
74
|
+
&& window.getComputedStyle(backdrop).display === "none"
|
|
75
|
+
&& !window.ScratchJr.onHold
|
|
76
|
+
&& window.ScratchJr.getActiveScript()
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
{ timeout: 30_000 }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function prepareSourcePage(page) {
|
|
84
|
+
await page.evaluate((backgroundMd5) => new Promise((resolve) => {
|
|
85
|
+
const currentPage = window.ScratchJr.stage.currentPage;
|
|
86
|
+
currentPage.setBackground(backgroundMd5, () => {
|
|
87
|
+
currentPage.updateThumb();
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
}), BACKGROUND_MD5);
|
|
91
|
+
|
|
92
|
+
await page.click("#sprite-motion");
|
|
93
|
+
await page.waitForFunction(
|
|
94
|
+
(blockType) => Boolean(document.querySelector(`#palette [data-blocktype="${blockType}"]`)),
|
|
95
|
+
{ timeout: 30_000 },
|
|
96
|
+
SCRIPT_BLOCK_TYPE
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await page.evaluate((blockType) => {
|
|
100
|
+
const blockElement = document.querySelector(`#palette [data-blocktype="${blockType}"]`);
|
|
101
|
+
window.Palette.insertBlockFromKeyboard(blockElement, new Event("duplicate-page-e2e"));
|
|
102
|
+
}, SCRIPT_BLOCK_TYPE);
|
|
103
|
+
|
|
104
|
+
await page.waitForFunction(
|
|
105
|
+
(blockType) => window.ScratchJr.getBlocks().some((block) => block.blocktype === blockType),
|
|
106
|
+
{ timeout: 30_000 },
|
|
107
|
+
SCRIPT_BLOCK_TYPE
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function activateDuplicatePageAction(page, requestedPageId) {
|
|
112
|
+
const result = await page.evaluate((pageId) => {
|
|
113
|
+
const targetPageId = pageId || window.ScratchJr.stage.currentPage.id;
|
|
114
|
+
const action = document.querySelector(`#pageactions .duplicatepage[data-owner="${targetPageId}"]`);
|
|
115
|
+
|
|
116
|
+
if (!action) {
|
|
117
|
+
return {
|
|
118
|
+
activated: false,
|
|
119
|
+
pageActions: Array.from(document.querySelectorAll("#pageactions .duplicatepage")).map((element) => ({
|
|
120
|
+
id: element.id || null,
|
|
121
|
+
owner: element.getAttribute("data-owner"),
|
|
122
|
+
label: element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "",
|
|
123
|
+
})),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (action.disabled || action.getAttribute("aria-disabled") === "true") {
|
|
128
|
+
return { activated: false, disabled: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
action.dispatchEvent(new MouseEvent("click", {
|
|
132
|
+
bubbles: true,
|
|
133
|
+
cancelable: true,
|
|
134
|
+
view: window,
|
|
135
|
+
}));
|
|
136
|
+
return { activated: true };
|
|
137
|
+
}, requestedPageId);
|
|
138
|
+
|
|
139
|
+
if (!result.activated) {
|
|
140
|
+
throw new Error(`No enabled accessible duplicate-page action found in the page strip: ${JSON.stringify(result)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pageHasBlock(pageData, blockType) {
|
|
145
|
+
return Boolean(pageData && Array.isArray(pageData.sprites) && pageData.sprites.some((spriteId) => {
|
|
146
|
+
const sprite = pageData[spriteId];
|
|
147
|
+
return sprite && Array.isArray(sprite.scripts) && sprite.scripts.some((strip) => {
|
|
148
|
+
return Array.isArray(strip) && strip.some((block) => Array.isArray(block) && block[0] === blockType);
|
|
149
|
+
});
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function waitForPageDuplication(page, beforeCount) {
|
|
154
|
+
await page.waitForFunction(
|
|
155
|
+
(count) => window.ScratchJr.stage.pages.length === count + 1
|
|
156
|
+
&& !window.ScratchJr.stage.duplicatingPage,
|
|
157
|
+
{ timeout: 30_000 },
|
|
158
|
+
beforeCount
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
describe("duplicate page", () => {
|
|
163
|
+
it(
|
|
164
|
+
"duplicates the current page with its scripts and background from the page strip",
|
|
165
|
+
async () => {
|
|
166
|
+
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await waitForEditorReady(page);
|
|
170
|
+
await prepareSourcePage(page);
|
|
171
|
+
|
|
172
|
+
const sourceState = await page.evaluate(() => {
|
|
173
|
+
const sourcePage = window.ScratchJr.stage.currentPage;
|
|
174
|
+
return {
|
|
175
|
+
pageId: sourcePage.id,
|
|
176
|
+
pageData: sourcePage.encodePage(),
|
|
177
|
+
pageCount: window.ScratchJr.stage.pages.length,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
expect(sourceState.pageData.md5).toBe(BACKGROUND_MD5);
|
|
181
|
+
expect(pageHasBlock(sourceState.pageData, SCRIPT_BLOCK_TYPE)).toBe(true);
|
|
182
|
+
|
|
183
|
+
const pageActionState = await page.evaluate((sourcePageId) => {
|
|
184
|
+
const action = document.querySelector(`#pageactions .duplicatepage[data-owner="${sourcePageId}"]`);
|
|
185
|
+
return {
|
|
186
|
+
actionCount: document.querySelectorAll("#pageactions .duplicatepage").length,
|
|
187
|
+
isNestedInPageThumb: Boolean(action && action.closest(".pagethumb")),
|
|
188
|
+
label: action && action.getAttribute("aria-label"),
|
|
189
|
+
};
|
|
190
|
+
}, sourceState.pageId);
|
|
191
|
+
expect(pageActionState).toEqual({
|
|
192
|
+
actionCount: sourceState.pageCount,
|
|
193
|
+
isNestedInPageThumb: false,
|
|
194
|
+
label: "Duplicate page 1",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await activateDuplicatePageAction(page, sourceState.pageId);
|
|
198
|
+
|
|
199
|
+
await page.waitForFunction(
|
|
200
|
+
(beforeCount, sourcePageId) => window.ScratchJr.stage.pages.length === beforeCount + 1
|
|
201
|
+
&& !window.ScratchJr.stage.duplicatingPage
|
|
202
|
+
&& window.ScratchJr.stage.currentPage.id !== sourcePageId,
|
|
203
|
+
{ timeout: 30_000 },
|
|
204
|
+
sourceState.pageCount,
|
|
205
|
+
sourceState.pageId
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const duplicatedState = await page.evaluate((sourcePageId) => {
|
|
209
|
+
const sourceIndex = window.ScratchJr.stage.pages.findIndex((stagePage) => stagePage.id === sourcePageId);
|
|
210
|
+
const duplicatePage = window.ScratchJr.stage.pages[sourceIndex + 1];
|
|
211
|
+
return {
|
|
212
|
+
duplicatePageId: duplicatePage ? duplicatePage.id : null,
|
|
213
|
+
duplicatePageData: duplicatePage ? duplicatePage.encodePage() : null,
|
|
214
|
+
pageIds: window.ScratchJr.stage.getPagesID(),
|
|
215
|
+
};
|
|
216
|
+
}, sourceState.pageId);
|
|
217
|
+
|
|
218
|
+
expect(duplicatedState.duplicatePageId).not.toBe(sourceState.pageId);
|
|
219
|
+
expect(duplicatedState.duplicatePageData.md5).toBe(BACKGROUND_MD5);
|
|
220
|
+
expect(pageHasBlock(duplicatedState.duplicatePageData, SCRIPT_BLOCK_TYPE)).toBe(true);
|
|
221
|
+
expect(duplicatedState.duplicatePageData.sprites).not.toEqual(sourceState.pageData.sprites);
|
|
222
|
+
expect(duplicatedState.pageIds).toHaveLength(sourceState.pageCount + 1);
|
|
223
|
+
expect(errors).toEqual([]);
|
|
224
|
+
|
|
225
|
+
await page.click("#id_undo");
|
|
226
|
+
await page.waitForFunction(
|
|
227
|
+
(duplicatePageId, beforeCount) => !document.getElementById(duplicatePageId)
|
|
228
|
+
&& window.ScratchJr.stage.pages.length === beforeCount,
|
|
229
|
+
{ timeout: 30_000 },
|
|
230
|
+
duplicatedState.duplicatePageId,
|
|
231
|
+
sourceState.pageCount
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
await page.click("#id_redo");
|
|
235
|
+
await page.waitForFunction(
|
|
236
|
+
(duplicatePageId, beforeCount) => document.getElementById(duplicatePageId)
|
|
237
|
+
&& window.ScratchJr.stage.pages.length === beforeCount + 1,
|
|
238
|
+
{ timeout: 30_000 },
|
|
239
|
+
duplicatedState.duplicatePageId,
|
|
240
|
+
sourceState.pageCount
|
|
241
|
+
);
|
|
242
|
+
const redonePageData = await page.evaluate((duplicatePageId) => {
|
|
243
|
+
return document.getElementById(duplicatePageId).owner.encodePage();
|
|
244
|
+
}, duplicatedState.duplicatePageId);
|
|
245
|
+
expect(redonePageData.md5).toBe(BACKGROUND_MD5);
|
|
246
|
+
expect(pageHasBlock(redonePageData, SCRIPT_BLOCK_TYPE)).toBe(true);
|
|
247
|
+
} finally {
|
|
248
|
+
await browser.close();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
60_000
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
it(
|
|
255
|
+
"respects the four-page project limit",
|
|
256
|
+
async () => {
|
|
257
|
+
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await waitForEditorReady(page);
|
|
261
|
+
for (let pageCount = 1; pageCount < 4; pageCount += 1) {
|
|
262
|
+
await activateDuplicatePageAction(page);
|
|
263
|
+
await waitForPageDuplication(page, pageCount);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const limitState = await page.evaluate(() => {
|
|
267
|
+
const duplicateButtons = Array.from(document.querySelectorAll("#pageactions .duplicatepage"));
|
|
268
|
+
return {
|
|
269
|
+
pageCount: window.ScratchJr.stage.pages.length,
|
|
270
|
+
buttonCount: duplicateButtons.length,
|
|
271
|
+
disabled: duplicateButtons.every((button) => button.disabled),
|
|
272
|
+
ariaDisabled: duplicateButtons.every((button) => button.getAttribute("aria-disabled") === "true"),
|
|
273
|
+
canDuplicate: window.ScratchJr.stage.canDuplicatePage(window.ScratchJr.stage.currentPage.id),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
expect(limitState).toEqual({
|
|
277
|
+
pageCount: 4,
|
|
278
|
+
buttonCount: 4,
|
|
279
|
+
disabled: true,
|
|
280
|
+
ariaDisabled: true,
|
|
281
|
+
canDuplicate: false,
|
|
282
|
+
});
|
|
283
|
+
expect(errors).toEqual([]);
|
|
284
|
+
} finally {
|
|
285
|
+
await browser.close();
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
60_000
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
it(
|
|
292
|
+
"duplicates the page attached to the clicked action when another page is selected",
|
|
293
|
+
async () => {
|
|
294
|
+
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
await waitForEditorReady(page);
|
|
298
|
+
await prepareSourcePage(page);
|
|
299
|
+
|
|
300
|
+
const sourcePageId = await page.evaluate(() => window.ScratchJr.stage.currentPage.id);
|
|
301
|
+
await activateDuplicatePageAction(page, sourcePageId);
|
|
302
|
+
await waitForPageDuplication(page, 1);
|
|
303
|
+
|
|
304
|
+
const selectedPageId = await page.evaluate(() => new Promise((resolve) => {
|
|
305
|
+
const selectedPage = window.ScratchJr.stage.currentPage;
|
|
306
|
+
selectedPage.setBackground("none", () => {
|
|
307
|
+
selectedPage.updateThumb();
|
|
308
|
+
resolve(selectedPage.id);
|
|
309
|
+
});
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
await activateDuplicatePageAction(page, sourcePageId);
|
|
313
|
+
await waitForPageDuplication(page, 2);
|
|
314
|
+
|
|
315
|
+
const targetedState = await page.evaluate(({ sourceId, previousSelectedId }) => {
|
|
316
|
+
const sourceIndex = window.ScratchJr.stage.pages.findIndex((stagePage) => stagePage.id === sourceId);
|
|
317
|
+
const targetedDuplicate = window.ScratchJr.stage.pages[sourceIndex + 1];
|
|
318
|
+
const previousSelectedIndex = window.ScratchJr.stage.pages.findIndex(
|
|
319
|
+
(stagePage) => stagePage.id === previousSelectedId
|
|
320
|
+
);
|
|
321
|
+
return {
|
|
322
|
+
currentPageId: window.ScratchJr.stage.currentPage.id,
|
|
323
|
+
targetedDuplicateId: targetedDuplicate.id,
|
|
324
|
+
targetedDuplicateData: targetedDuplicate.encodePage(),
|
|
325
|
+
previousSelectedIndex,
|
|
326
|
+
};
|
|
327
|
+
}, { sourceId: sourcePageId, previousSelectedId: selectedPageId });
|
|
328
|
+
|
|
329
|
+
expect(targetedState.currentPageId).toBe(targetedState.targetedDuplicateId);
|
|
330
|
+
expect(targetedState.targetedDuplicateId).not.toBe(selectedPageId);
|
|
331
|
+
expect(targetedState.targetedDuplicateData.md5).toBe(BACKGROUND_MD5);
|
|
332
|
+
expect(pageHasBlock(targetedState.targetedDuplicateData, SCRIPT_BLOCK_TYPE)).toBe(true);
|
|
333
|
+
expect(targetedState.previousSelectedIndex).toBe(2);
|
|
334
|
+
expect(errors).toEqual([]);
|
|
335
|
+
} finally {
|
|
336
|
+
await browser.close();
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
60_000
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
it(
|
|
343
|
+
"always shows the duplicate glyph and hides the action during long-press delete mode",
|
|
344
|
+
async () => {
|
|
345
|
+
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
await waitForEditorReady(page);
|
|
349
|
+
const pageId = await page.evaluate(() => window.ScratchJr.stage.currentPage.id);
|
|
350
|
+
await activateDuplicatePageAction(page, pageId);
|
|
351
|
+
await waitForPageDuplication(page, 1);
|
|
352
|
+
const restingState = await page.evaluate((currentPageId) => {
|
|
353
|
+
const action = document.querySelector(
|
|
354
|
+
`#pageactions .duplicatepage[data-owner="${currentPageId}"]`
|
|
355
|
+
);
|
|
356
|
+
const icon = action.querySelector(".duplicatepageicon");
|
|
357
|
+
const backSquare = window.getComputedStyle(icon, "::before");
|
|
358
|
+
const frontSquare = window.getComputedStyle(icon, "::after");
|
|
359
|
+
return {
|
|
360
|
+
actionVisibility: window.getComputedStyle(action).visibility,
|
|
361
|
+
backContent: backSquare.content,
|
|
362
|
+
frontContent: frontSquare.content,
|
|
363
|
+
backZIndex: backSquare.zIndex,
|
|
364
|
+
frontZIndex: frontSquare.zIndex,
|
|
365
|
+
};
|
|
366
|
+
}, pageId);
|
|
367
|
+
|
|
368
|
+
expect(restingState.actionVisibility).toBe("visible");
|
|
369
|
+
expect(restingState.backContent).not.toBe("none");
|
|
370
|
+
expect(restingState.frontContent).not.toBe("none");
|
|
371
|
+
expect(restingState.backZIndex).not.toBe("-1");
|
|
372
|
+
expect(restingState.frontZIndex).not.toBe("-1");
|
|
373
|
+
|
|
374
|
+
await page.evaluate((currentPageId) => new Promise((resolve) => {
|
|
375
|
+
const thumb = document.querySelector(`.pagethumb[data-owner="${currentPageId}"]`);
|
|
376
|
+
const bounds = thumb.getBoundingClientRect();
|
|
377
|
+
thumb.dispatchEvent(new PointerEvent("pointerdown", {
|
|
378
|
+
bubbles: true,
|
|
379
|
+
cancelable: true,
|
|
380
|
+
clientX: bounds.left + (bounds.width / 2),
|
|
381
|
+
clientY: bounds.top + (bounds.height / 2),
|
|
382
|
+
pointerId: 1,
|
|
383
|
+
pointerType: "mouse",
|
|
384
|
+
isPrimary: true,
|
|
385
|
+
}));
|
|
386
|
+
window.setTimeout(resolve, 600);
|
|
387
|
+
}), pageId);
|
|
388
|
+
await page.waitForFunction((currentPageId) => {
|
|
389
|
+
const thumb = document.querySelector(`.pagethumb[data-owner="${currentPageId}"]`);
|
|
390
|
+
const action = document.querySelector(
|
|
391
|
+
`#pageactions .duplicatepage[data-owner="${currentPageId}"]`
|
|
392
|
+
);
|
|
393
|
+
return thumb.classList.contains("shakeme")
|
|
394
|
+
&& window.getComputedStyle(action).visibility === "hidden"
|
|
395
|
+
&& window.getComputedStyle(thumb.querySelector(".deletethumb")).visibility === "visible";
|
|
396
|
+
}, { timeout: 5_000 }, pageId);
|
|
397
|
+
|
|
398
|
+
await page.evaluate(() => window.ScratchJr.clearSelection());
|
|
399
|
+
await page.waitForFunction((currentPageId) => {
|
|
400
|
+
const thumb = document.querySelector(`.pagethumb[data-owner="${currentPageId}"]`);
|
|
401
|
+
const action = document.querySelector(
|
|
402
|
+
`#pageactions .duplicatepage[data-owner="${currentPageId}"]`
|
|
403
|
+
);
|
|
404
|
+
return !thumb.classList.contains("shakeme")
|
|
405
|
+
&& window.getComputedStyle(action).visibility === "visible";
|
|
406
|
+
}, { timeout: 5_000 }, pageId);
|
|
407
|
+
expect(errors).toEqual([]);
|
|
408
|
+
} finally {
|
|
409
|
+
await browser.close();
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
60_000
|
|
413
|
+
);
|
|
414
|
+
});
|
|
@@ -65,6 +65,7 @@ vi.mock('@/utils/accessibility', () => ({
|
|
|
65
65
|
setSelectedState: vi.fn()
|
|
66
66
|
}));
|
|
67
67
|
vi.mock('@/html-svgs/cog', () => ({ cogSvg: '<svg></svg>' }));
|
|
68
|
+
vi.mock('@/html-svgs/microbit', () => ({ microBitSvg: '<svg></svg>' }));
|
|
68
69
|
vi.mock('@/html-svgs/sprite', () => ({ spriteSvg: '<svg></svg>' }));
|
|
69
70
|
vi.mock('@/html-svgs/marty', () => ({ martySvg: '<svg></svg>' }));
|
|
70
71
|
vi.mock('@/html-svgs/sprite-deselected', () => ({ spriteDeselectedSvg: '<svg></svg>' }));
|
|
@@ -126,6 +127,11 @@ describe('Marty connection UI', () => {
|
|
|
126
127
|
}),
|
|
127
128
|
removeMarty: vi.fn(),
|
|
128
129
|
setMartySensorAvailability: vi.fn()
|
|
130
|
+
},
|
|
131
|
+
microBitManager: {
|
|
132
|
+
addMicroBit: vi.fn(),
|
|
133
|
+
wireMicroBitWithBlocks: vi.fn(),
|
|
134
|
+
removeMicroBit: vi.fn()
|
|
129
135
|
}
|
|
130
136
|
};
|
|
131
137
|
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
@@ -242,6 +248,37 @@ describe('Marty connection UI', () => {
|
|
|
242
248
|
expect(button.classList.contains('connectButtonConnected')).toBe(true);
|
|
243
249
|
expect(button.querySelector('.iconButtonContainer').classList.contains('connectedButtonContainer')).toBe(true);
|
|
244
250
|
});
|
|
251
|
+
|
|
252
|
+
it('uses a friendly message when the micro:bit chooser is cancelled', () => {
|
|
253
|
+
const message = UI.getMicroBitConnectionErrorMessage(
|
|
254
|
+
new DOMException('User cancelled the requestDevice() chooser.', 'NotFoundError')
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(message).toBe('No micro:bit was selected. Click Connect again when you are ready.');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('registers micro:bit blocks and restores the button after direct disconnect', () => {
|
|
261
|
+
const button = createConnectionButton();
|
|
262
|
+
const oldOnClick = vi.fn();
|
|
263
|
+
button.onclick = oldOnClick;
|
|
264
|
+
const microBit = createMicroBit();
|
|
265
|
+
|
|
266
|
+
UI.setupMicroBitConnectionButton(button, microBit);
|
|
267
|
+
|
|
268
|
+
expect(window.microBitManager.addMicroBit).toHaveBeenCalledWith(microBit);
|
|
269
|
+
expect(window.microBitManager.wireMicroBitWithBlocks).toHaveBeenCalledWith('microbit-1');
|
|
270
|
+
expect(button.classList.contains('connectButtonConnected')).toBe(true);
|
|
271
|
+
|
|
272
|
+
button.onclick();
|
|
273
|
+
vi.advanceTimersByTime(1000);
|
|
274
|
+
|
|
275
|
+
expect(microBit.disconnect).toHaveBeenCalled();
|
|
276
|
+
expect(window.microBitManager.removeMicroBit).toHaveBeenCalledWith(microBit);
|
|
277
|
+
expect(button.onclick).toBe(oldOnClick);
|
|
278
|
+
expect(button.classList.contains('connectButtonConnected')).toBe(false);
|
|
279
|
+
expect(button.querySelector('.iconButtonContainer').classList.contains('notConnectedButtonContainer')).toBe(true);
|
|
280
|
+
expect(microBit.__disconnectUnsubscribed).toBe(true);
|
|
281
|
+
});
|
|
245
282
|
});
|
|
246
283
|
|
|
247
284
|
function createMartyRaft() {
|
|
@@ -265,6 +302,25 @@ function createConnectionButton() {
|
|
|
265
302
|
return button;
|
|
266
303
|
}
|
|
267
304
|
|
|
305
|
+
function createMicroBit() {
|
|
306
|
+
const microBit = {
|
|
307
|
+
id: 'microbit-1',
|
|
308
|
+
getFriendlyName: () => 'micro:bit One',
|
|
309
|
+
disconnect: vi.fn(() => {
|
|
310
|
+
if (microBit.__disconnectCallback) {
|
|
311
|
+
microBit.__disconnectCallback(microBit);
|
|
312
|
+
}
|
|
313
|
+
}),
|
|
314
|
+
addDisconnectListener: vi.fn(callback => {
|
|
315
|
+
microBit.__disconnectCallback = callback;
|
|
316
|
+
return () => {
|
|
317
|
+
microBit.__disconnectUnsubscribed = true;
|
|
318
|
+
};
|
|
319
|
+
})
|
|
320
|
+
};
|
|
321
|
+
return microBit;
|
|
322
|
+
}
|
|
323
|
+
|
|
268
324
|
class FakeElement {
|
|
269
325
|
constructor(classes = []) {
|
|
270
326
|
this.classList = new FakeClassList(classes);
|