@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.
- 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/viewOnCompactWideLabel.png +0 -0
- package/editions/free/src/assets/ui/viewOnTargetCompact.png +0 -0
- package/editions/free/src/css/editor.css +96 -0
- package/editions/free/src/css/editorleftpanel.css +21 -22
- 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 +19 -2
- package/tests/e2e/animated-sprite-paint-disabled.e2e.test.js +358 -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 +593 -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,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
|
+
});
|
|
@@ -108,6 +108,15 @@ async function assertTutorialOpened(page, tutorialId) {
|
|
|
108
108
|
expect(tutorialState.tutorialTitle.length).toBeGreaterThan(0);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
async function openHiddenExtensionsLibrary(page) {
|
|
112
|
+
await page.$eval("#addExtensionButton", (button) => {
|
|
113
|
+
if (!button.disabled || window.getComputedStyle(button).display !== "none") {
|
|
114
|
+
throw new Error("Expected the extensions entry point to remain hidden and disabled");
|
|
115
|
+
}
|
|
116
|
+
button.onclick();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
describe("Chromium 79 smoke test", () => {
|
|
112
121
|
it(
|
|
113
122
|
"loads home page without console errors",
|
|
@@ -123,7 +132,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
123
132
|
);
|
|
124
133
|
|
|
125
134
|
it(
|
|
126
|
-
"
|
|
135
|
+
"keeps the extension entry hidden and reveals micro:bit controls only while loaded",
|
|
127
136
|
async () => {
|
|
128
137
|
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
129
138
|
|
|
@@ -148,9 +157,9 @@ describe("Chromium 79 smoke test", () => {
|
|
|
148
157
|
});
|
|
149
158
|
|
|
150
159
|
expect(initialState).toEqual({
|
|
151
|
-
addButtonDisplay: "
|
|
152
|
-
addButtonAriaHidden: "
|
|
153
|
-
addButtonDisabled:
|
|
160
|
+
addButtonDisplay: "none",
|
|
161
|
+
addButtonAriaHidden: "true",
|
|
162
|
+
addButtonDisabled: true,
|
|
154
163
|
microBitButtonDisplay: "none",
|
|
155
164
|
microBitButtonAriaHidden: "true",
|
|
156
165
|
microBitButtonDisabled: true,
|
|
@@ -158,7 +167,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
158
167
|
extensionEnabled: false,
|
|
159
168
|
});
|
|
160
169
|
|
|
161
|
-
await page
|
|
170
|
+
await openHiddenExtensionsLibrary(page);
|
|
162
171
|
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
163
172
|
const libraryState = await page.evaluate(() => ({
|
|
164
173
|
title: document.getElementById("extensionsLibraryTitle").textContent.trim(),
|
|
@@ -212,9 +221,9 @@ describe("Chromium 79 smoke test", () => {
|
|
|
212
221
|
});
|
|
213
222
|
|
|
214
223
|
expect(enabledState).toEqual({
|
|
215
|
-
addButtonDisplay: "
|
|
216
|
-
addButtonAriaHidden: "
|
|
217
|
-
addButtonDisabled:
|
|
224
|
+
addButtonDisplay: "none",
|
|
225
|
+
addButtonAriaHidden: "true",
|
|
226
|
+
addButtonDisabled: true,
|
|
218
227
|
microBitButtonDisplay: "flex",
|
|
219
228
|
microBitButtonAriaHidden: "false",
|
|
220
229
|
microBitButtonDisabled: false,
|
|
@@ -281,7 +290,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
281
290
|
});
|
|
282
291
|
expect(blocksAfterInsert).toContain("microbitdisplayclear");
|
|
283
292
|
|
|
284
|
-
await page
|
|
293
|
+
await openHiddenExtensionsLibrary(page);
|
|
285
294
|
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
286
295
|
const loadedLibraryState = await page.evaluate(() => ({
|
|
287
296
|
microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
|
|
@@ -330,7 +339,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
330
339
|
extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
|
|
331
340
|
}));
|
|
332
341
|
expect(unloadedState).toEqual({
|
|
333
|
-
addButtonDisplay: "
|
|
342
|
+
addButtonDisplay: "none",
|
|
334
343
|
microBitButtonDisplay: "none",
|
|
335
344
|
extensionsLibraryOpen: false,
|
|
336
345
|
microBitCardAction: "+",
|
|
@@ -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
|
+
|
|
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
|
+
}, 30000);
|
|
34
|
+
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
if (server) {
|
|
37
|
+
server.kill();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function openPage(pathname) {
|
|
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}${pathname}`, { waitUntil: "networkidle2", timeout: 30000 });
|
|
57
|
+
|
|
58
|
+
return { browser, page, errors };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function waitForEditorReady(page) {
|
|
62
|
+
await page.waitForFunction(
|
|
63
|
+
() => {
|
|
64
|
+
const backdrop = document.getElementById("backdrop");
|
|
65
|
+
const selectedSpriteThumb = document.querySelector("#spritecc .spritethumb[aria-pressed=\"true\"]");
|
|
66
|
+
return Boolean(
|
|
67
|
+
window.ScratchJr
|
|
68
|
+
&& window.ScratchJr.stage
|
|
69
|
+
&& window.ScratchJr.stage.currentPage
|
|
70
|
+
&& document.getElementById("addsprite")
|
|
71
|
+
&& document.getElementById("martyMode")
|
|
72
|
+
&& selectedSpriteThumb
|
|
73
|
+
&& backdrop
|
|
74
|
+
&& window.getComputedStyle(backdrop).display === "none"
|
|
75
|
+
&& !window.ScratchJr.onHold
|
|
76
|
+
&& window.ScratchJr.getActiveScript()
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
{ timeout: 30000 }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function getEditorState(page) {
|
|
84
|
+
return page.evaluate(() => {
|
|
85
|
+
const currentPage = window.ScratchJr.stage.currentPage;
|
|
86
|
+
const spriteIds = currentPage.getSprites();
|
|
87
|
+
const normalSpriteIds = spriteIds.filter(
|
|
88
|
+
(spriteId) => spriteId.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) === -1
|
|
89
|
+
);
|
|
90
|
+
const currentSpriteName = currentPage.currentSpriteName || "";
|
|
91
|
+
const addSpriteButton = document.getElementById("addsprite");
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
currentSpriteName,
|
|
95
|
+
spriteIds,
|
|
96
|
+
normalSpriteIds,
|
|
97
|
+
isMartyModeEnabled: window.ScratchJr.isMartyModeEnabled,
|
|
98
|
+
activeSpriteIsMartyBirdsEye:
|
|
99
|
+
currentSpriteName.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) > -1,
|
|
100
|
+
hasMartyModeSidebarCard: Boolean(document.getElementById("martyModeSidebarCard")),
|
|
101
|
+
addSpriteButtonVisible:
|
|
102
|
+
Boolean(addSpriteButton) && window.getComputedStyle(addSpriteButton).display !== "none",
|
|
103
|
+
addSpriteButtonDisabled: Boolean(addSpriteButton && addSpriteButton.disabled),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function deleteSelectedSpriteWithKeyboard(page) {
|
|
109
|
+
await page.focus("#spritecc .spritethumb[aria-pressed=\"true\"]");
|
|
110
|
+
await page.keyboard.press("Delete");
|
|
111
|
+
await page.waitForFunction(
|
|
112
|
+
() => {
|
|
113
|
+
const currentPage = window.ScratchJr.stage.currentPage;
|
|
114
|
+
return currentPage.getSprites().filter(
|
|
115
|
+
(spriteId) => spriteId.indexOf(window.ScratchJr.BIRDS_EYE_SPRITE_NAME) === -1
|
|
116
|
+
).length === 0;
|
|
117
|
+
},
|
|
118
|
+
{ timeout: 30000 }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("deleting the final sprite", () => {
|
|
123
|
+
it(
|
|
124
|
+
"keeps the editor in Sprite mode when no normal sprites remain",
|
|
125
|
+
async () => {
|
|
126
|
+
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await waitForEditorReady(page);
|
|
130
|
+
|
|
131
|
+
const beforeDelete = await getEditorState(page);
|
|
132
|
+
expect(beforeDelete).toMatchObject({
|
|
133
|
+
normalSpriteIds: [beforeDelete.currentSpriteName],
|
|
134
|
+
isMartyModeEnabled: false,
|
|
135
|
+
activeSpriteIsMartyBirdsEye: false,
|
|
136
|
+
hasMartyModeSidebarCard: false,
|
|
137
|
+
addSpriteButtonVisible: true,
|
|
138
|
+
addSpriteButtonDisabled: false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await deleteSelectedSpriteWithKeyboard(page);
|
|
142
|
+
|
|
143
|
+
const afterDelete = await getEditorState(page);
|
|
144
|
+
expect(
|
|
145
|
+
afterDelete,
|
|
146
|
+
`Editor state after deleting the last normal sprite: ${JSON.stringify(afterDelete, null, 2)}`
|
|
147
|
+
).toMatchObject({
|
|
148
|
+
normalSpriteIds: [],
|
|
149
|
+
isMartyModeEnabled: false,
|
|
150
|
+
activeSpriteIsMartyBirdsEye: false,
|
|
151
|
+
hasMartyModeSidebarCard: false,
|
|
152
|
+
addSpriteButtonVisible: true,
|
|
153
|
+
addSpriteButtonDisabled: false,
|
|
154
|
+
});
|
|
155
|
+
expect(errors).toEqual([]);
|
|
156
|
+
} finally {
|
|
157
|
+
await browser.close();
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
90000
|
|
161
|
+
);
|
|
162
|
+
});
|