@robotical/martyblocksjr 4.2.2 → 4.2.3
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/css/editor.css +5 -0
- package/editions/free/src/css/editorleftpanel.css +12 -6
- package/editions/free/src/inapp/about.html +55 -26
- package/editions/free/src/inapp/blocks.html +16 -459
- package/editions/free/src/inapp/images/interface.png +0 -0
- package/editions/free/src/inapp/images/paint.png +0 -0
- package/editions/free/src/inapp/interface.html +136 -123
- package/editions/free/src/inapp/style/about.css +333 -21
- package/editions/free/src/inapp/style/blocks.css +423 -82
- package/editions/free/src/inapp/style/interface.css +178 -453
- package/editions/free/src/localizations/bg.json +36 -2
- package/editions/free/src/localizations/ca.json +36 -2
- package/editions/free/src/localizations/cs.json +36 -2
- package/editions/free/src/localizations/cy.json +36 -2
- package/editions/free/src/localizations/da.json +36 -2
- package/editions/free/src/localizations/de.json +36 -2
- package/editions/free/src/localizations/el.json +36 -2
- package/editions/free/src/localizations/en.json +114 -7
- package/editions/free/src/localizations/es.json +36 -2
- package/editions/free/src/localizations/fi.json +36 -2
- package/editions/free/src/localizations/fr.json +36 -2
- package/editions/free/src/localizations/it.json +36 -2
- package/editions/free/src/localizations/ja.json +36 -2
- package/editions/free/src/localizations/ko.json +36 -2
- package/editions/free/src/localizations/nl.json +36 -2
- package/editions/free/src/localizations/no.json +36 -2
- package/editions/free/src/localizations/pl.json +36 -2
- package/editions/free/src/localizations/pt-br.json +36 -2
- package/editions/free/src/localizations/pt.json +36 -2
- package/editions/free/src/localizations/sv.json +36 -2
- package/editions/free/src/localizations/th.json +36 -2
- package/editions/free/src/localizations/tr.json +36 -2
- package/editions/free/src/localizations/uk.json +36 -2
- package/editions/free/src/localizations/zh-cn.json +36 -2
- package/editions/free/src/localizations/zh-tw.json +36 -2
- package/package.json +1 -1
- package/scripts/a11y-structure.mjs +9 -2
- package/tests/BlockGuideRegistry.test.js +60 -0
- package/tests/MartyManager.test.js +88 -0
- package/tests/e2e/accessibility.e2e.test.js +153 -10
- package/tests/e2e/blocks-jr-hidden-blocks.e2e.test.js +84 -0
- package/tests/e2e/chromium-79-smoke.test.js +12 -17
- package/tests/e2e/microbit-extension-project-reload.e2e.test.js +1 -6
- package/tests/e2e/project-load-progress-loop.e2e.test.js +1 -6
- package/tests/e2e/sidebar-scrollbars.e2e.test.js +179 -0
|
@@ -138,9 +138,11 @@ async function getFocusStyles(page, selector) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function expectVisibleFocusIndicator(focusStyles) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const hasVisibleOutline = focusStyles.outlineStyle !== 'none' &&
|
|
142
|
+
focusStyles.outlineWidth > 0 &&
|
|
143
|
+
focusStyles.outlineColor !== 'rgba(0, 0, 0, 0)';
|
|
144
|
+
const hasVisibleBoxShadow = focusStyles.boxShadow && focusStyles.boxShadow !== 'none';
|
|
145
|
+
expect(hasVisibleOutline || hasVisibleBoxShadow).toBe(true);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
async function expectNamedControls(page, selectors) {
|
|
@@ -231,7 +233,7 @@ describe('Accessibility shell audit', () => {
|
|
|
231
233
|
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
232
234
|
await page.click('#booktab');
|
|
233
235
|
await page.waitForSelector('iframe#htmlcontents', { timeout: 30_000 });
|
|
234
|
-
expect(await page.$eval('iframe#htmlcontents', (element) => element.getAttribute('title'))).toBe('About
|
|
236
|
+
expect(await page.$eval('iframe#htmlcontents', (element) => element.getAttribute('title'))).toBe('About Blocks Jr');
|
|
235
237
|
|
|
236
238
|
expectNoAxeViolations(await runAxe(page));
|
|
237
239
|
expect(errors).toEqual([]);
|
|
@@ -250,11 +252,25 @@ describe('Accessibility shell audit', () => {
|
|
|
250
252
|
});
|
|
251
253
|
|
|
252
254
|
const guideCases = [
|
|
255
|
+
{
|
|
256
|
+
path: 'inapp/about.html',
|
|
257
|
+
title: 'About Blocks Jr',
|
|
258
|
+
focusSelector: '#skip-link',
|
|
259
|
+
buttonSelectors: [],
|
|
260
|
+
decorativeImageSelector: '.about-hero-art img'
|
|
261
|
+
},
|
|
253
262
|
{
|
|
254
263
|
path: 'inapp/interface.html',
|
|
255
264
|
title: 'Interface Guide',
|
|
256
265
|
focusSelector: '#interface-button-save',
|
|
257
|
-
buttonSelectors: [
|
|
266
|
+
buttonSelectors: [
|
|
267
|
+
'#interface-button-save',
|
|
268
|
+
'#interface-button-extensions',
|
|
269
|
+
'#interface-button-stage',
|
|
270
|
+
'#interface-button-green-flag',
|
|
271
|
+
'#interface-button-duplicate-page',
|
|
272
|
+
'#interface-button-zoom'
|
|
273
|
+
],
|
|
258
274
|
decorativeImageSelector: '.ipad-project-view'
|
|
259
275
|
},
|
|
260
276
|
{
|
|
@@ -267,9 +283,13 @@ describe('Accessibility shell audit', () => {
|
|
|
267
283
|
{
|
|
268
284
|
path: 'inapp/blocks.html',
|
|
269
285
|
title: 'Blocks Guide',
|
|
270
|
-
focusSelector: '#
|
|
271
|
-
buttonSelectors: [
|
|
272
|
-
|
|
286
|
+
focusSelector: '#blocks-guide-tab-marty',
|
|
287
|
+
buttonSelectors: [
|
|
288
|
+
'#blocks-guide-tab-marty',
|
|
289
|
+
'#blocks-guide-tab-sprite',
|
|
290
|
+
'#blocks-guide-tab-cog'
|
|
291
|
+
],
|
|
292
|
+
decorativeImageSelector: '.block-guide-icon'
|
|
273
293
|
}
|
|
274
294
|
];
|
|
275
295
|
|
|
@@ -279,12 +299,14 @@ describe('Accessibility shell audit', () => {
|
|
|
279
299
|
await page.goto(`${HOST}/${guideCase.path}`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
280
300
|
await waitForTitle(page, guideCase.title);
|
|
281
301
|
await page.waitForSelector('#skip-link');
|
|
282
|
-
await page.waitForSelector('#content
|
|
302
|
+
await page.waitForSelector('#content');
|
|
283
303
|
|
|
284
304
|
expect((await page.title()).includes(guideCase.title)).toBe(true);
|
|
285
305
|
expect(await page.$eval('html', (element) => element.lang)).toBe('en');
|
|
286
306
|
expect(await page.$eval('#skip-link', (element) => element.getAttribute('href'))).toBe('#content');
|
|
287
|
-
expect(await page.$eval('#content', (element) =>
|
|
307
|
+
expect(await page.$eval('#content', (element) => {
|
|
308
|
+
return element.tagName.toLowerCase() === 'main' || element.getAttribute('role') === 'main';
|
|
309
|
+
})).toBe(true);
|
|
288
310
|
|
|
289
311
|
if (guideCase.buttonSelectors.length > 0) {
|
|
290
312
|
await expectNamedControls(page, guideCase.buttonSelectors);
|
|
@@ -298,6 +320,127 @@ describe('Accessibility shell audit', () => {
|
|
|
298
320
|
await tabToSelector(page, guideCase.focusSelector);
|
|
299
321
|
expectVisibleFocusIndicator(await getFocusStyles(page, guideCase.focusSelector));
|
|
300
322
|
|
|
323
|
+
if (guideCase.path === 'inapp/interface.html') {
|
|
324
|
+
const buttonIds = await page.$$eval('.interface-button', (buttons) => {
|
|
325
|
+
return buttons.map((button) => button.id);
|
|
326
|
+
});
|
|
327
|
+
expect(buttonIds).toHaveLength(24);
|
|
328
|
+
expect(await page.$$eval('.interface-connector', (connectors) => connectors.length)).toBe(24);
|
|
329
|
+
expect(await page.$eval('#interface-key-header', (element) => element.textContent)).toBe('1 | Save');
|
|
330
|
+
|
|
331
|
+
for (let index = 0; index < buttonIds.length; index += 1) {
|
|
332
|
+
const selector = `#${buttonIds[index]}`;
|
|
333
|
+
await page.click(selector);
|
|
334
|
+
const selectedFeature = await page.evaluate((buttonSelector) => {
|
|
335
|
+
const button = document.querySelector(buttonSelector);
|
|
336
|
+
const connector = document.querySelector(
|
|
337
|
+
`.interface-connector[data-guide-key="${button.getAttribute('data-guide-key')}"]`
|
|
338
|
+
);
|
|
339
|
+
const number = button.querySelector('span');
|
|
340
|
+
return {
|
|
341
|
+
ariaCurrent: button.getAttribute('aria-current'),
|
|
342
|
+
ariaLabel: button.getAttribute('aria-label'),
|
|
343
|
+
buttonBackground: window.getComputedStyle(button).backgroundColor,
|
|
344
|
+
connectorIsSelected: connector.classList.contains('interface-connector-selected'),
|
|
345
|
+
connectorLineLength: connector.querySelector('line').getTotalLength(),
|
|
346
|
+
description: document.querySelector('#interface-key-description').textContent,
|
|
347
|
+
header: document.querySelector('#interface-key-header').textContent,
|
|
348
|
+
numberColor: window.getComputedStyle(number).color,
|
|
349
|
+
numberText: number.textContent,
|
|
350
|
+
selectedConnectorCount: document.querySelectorAll(
|
|
351
|
+
'.interface-connector-selected'
|
|
352
|
+
).length,
|
|
353
|
+
selectedCount: document.querySelectorAll('.interface-button[aria-current="page"]').length
|
|
354
|
+
};
|
|
355
|
+
}, selector);
|
|
356
|
+
|
|
357
|
+
expect(selectedFeature.header).toBe(selectedFeature.ariaLabel);
|
|
358
|
+
expect(selectedFeature.header.startsWith(`${index + 1} | `)).toBe(true);
|
|
359
|
+
expect(selectedFeature.header).not.toContain('String missing:');
|
|
360
|
+
expect(selectedFeature.description.trim().length).toBeGreaterThan(0);
|
|
361
|
+
expect(selectedFeature.description).not.toContain('String missing:');
|
|
362
|
+
expect(selectedFeature.ariaCurrent).toBe('page');
|
|
363
|
+
expect(selectedFeature.numberText).toBe(String(index + 1));
|
|
364
|
+
expect(selectedFeature.numberColor).not.toBe(selectedFeature.buttonBackground);
|
|
365
|
+
expect(selectedFeature.connectorIsSelected).toBe(true);
|
|
366
|
+
expect(selectedFeature.connectorLineLength).toBeGreaterThan(0);
|
|
367
|
+
expect(selectedFeature.selectedConnectorCount).toBe(1);
|
|
368
|
+
expect(selectedFeature.selectedCount).toBe(1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await page.click('#interface-button-extensions');
|
|
372
|
+
expect(await page.$eval('#interface-key-header', (element) => element.textContent)).toBe('3 | Extensions');
|
|
373
|
+
expect(await page.$eval('#interface-key-description', (element) => element.textContent)).toContain('micro:bit');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (guideCase.path === 'inapp/blocks.html') {
|
|
377
|
+
const inventory = await page.evaluate(() => {
|
|
378
|
+
const modeCounts = {};
|
|
379
|
+
document.querySelectorAll('[data-guide-mode-panel]').forEach((panel) => {
|
|
380
|
+
modeCounts[panel.getAttribute('data-guide-mode-panel')] =
|
|
381
|
+
panel.querySelectorAll('.block-guide-card').length;
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
activeMode: document.querySelector('[role="tab"][aria-selected="true"]')
|
|
385
|
+
.getAttribute('data-guide-mode'),
|
|
386
|
+
extensionCount: document.querySelectorAll(
|
|
387
|
+
'#blocks-guide-extension .block-guide-card'
|
|
388
|
+
).length,
|
|
389
|
+
modeCounts,
|
|
390
|
+
missingCopy: document.body.textContent.includes('String missing:'),
|
|
391
|
+
tabCount: document.querySelectorAll('[role="tab"]').length
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
expect(inventory).toEqual({
|
|
395
|
+
activeMode: 'marty',
|
|
396
|
+
extensionCount: 8,
|
|
397
|
+
modeCounts: {
|
|
398
|
+
marty: 44,
|
|
399
|
+
sprite: 31,
|
|
400
|
+
cog: 19
|
|
401
|
+
},
|
|
402
|
+
missingCopy: false,
|
|
403
|
+
tabCount: 3
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await page.click('#blocks-guide-tab-sprite');
|
|
407
|
+
expect(await page.$eval('#blocks-guide-panel-sprite', (panel) => panel.hidden)).toBe(false);
|
|
408
|
+
expect(await page.$eval('#blocks-guide-panel-marty', (panel) => panel.hidden)).toBe(true);
|
|
409
|
+
|
|
410
|
+
await page.focus('#blocks-guide-tab-sprite');
|
|
411
|
+
await page.keyboard.press('ArrowRight');
|
|
412
|
+
expect(await page.$eval('#blocks-guide-tab-cog', (tab) => tab.getAttribute('aria-selected'))).toBe('true');
|
|
413
|
+
expect(await page.$eval('#blocks-guide-panel-cog', (panel) => panel.hidden)).toBe(false);
|
|
414
|
+
|
|
415
|
+
for (const modeId of ['marty', 'sprite', 'cog']) {
|
|
416
|
+
await page.evaluate(() => {
|
|
417
|
+
window.scrollTo(0, document.documentElement.scrollHeight);
|
|
418
|
+
});
|
|
419
|
+
await page.click(`#blocks-guide-tab-${modeId}`);
|
|
420
|
+
const selectedMode = await page.evaluate((expectedMode) => {
|
|
421
|
+
const tab = document.querySelector(`[data-guide-mode="${expectedMode}"]`);
|
|
422
|
+
const panel = document.querySelector(`[data-guide-mode-panel="${expectedMode}"]`);
|
|
423
|
+
const firstCard = panel.querySelector('.block-guide-card');
|
|
424
|
+
const tabsShell = document.querySelector('.blocks-guide-tabs-shell');
|
|
425
|
+
const cardRect = firstCard.getBoundingClientRect();
|
|
426
|
+
const tabsRect = tabsShell.getBoundingClientRect();
|
|
427
|
+
return {
|
|
428
|
+
activeMode: document.querySelector('[role="tab"][aria-selected="true"]')
|
|
429
|
+
.getAttribute('data-guide-mode'),
|
|
430
|
+
firstCardVisible: cardRect.top < window.innerHeight && cardRect.bottom > tabsRect.bottom,
|
|
431
|
+
panelHidden: panel.hidden,
|
|
432
|
+
tabSelected: tab.getAttribute('aria-selected')
|
|
433
|
+
};
|
|
434
|
+
}, modeId);
|
|
435
|
+
expect(selectedMode).toEqual({
|
|
436
|
+
activeMode: modeId,
|
|
437
|
+
firstCardVisible: true,
|
|
438
|
+
panelHidden: false,
|
|
439
|
+
tabSelected: 'true'
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
301
444
|
await activateSkipLink(page, 'content');
|
|
302
445
|
|
|
303
446
|
expectNoAxeViolations(await runAxe(page));
|
|
@@ -164,6 +164,14 @@ const getSpriteScriptState = async (page, spriteId) =>
|
|
|
164
164
|
};
|
|
165
165
|
}, spriteId);
|
|
166
166
|
|
|
167
|
+
const getMartySensorPaletteState = page =>
|
|
168
|
+
page.evaluate(() => ({
|
|
169
|
+
colour: Boolean(document.getElementById("martycoloursensed_block")),
|
|
170
|
+
obstacle: Boolean(document.getElementById("martyobstaclesensed_block")),
|
|
171
|
+
light: Boolean(document.getElementById("martylightsensed_block")),
|
|
172
|
+
noise: Boolean(document.getElementById("martynoisesensed_block"))
|
|
173
|
+
}));
|
|
174
|
+
|
|
167
175
|
describe("Blocks Jr hidden block drop regression", () => {
|
|
168
176
|
beforeAll(async () => {
|
|
169
177
|
server = spawn("python3", ["-m", "http.server", String(PORT), "--directory", "editions/free/src"], {
|
|
@@ -259,4 +267,80 @@ describe("Blocks Jr hidden block drop regression", () => {
|
|
|
259
267
|
await browser.close();
|
|
260
268
|
}
|
|
261
269
|
}, 60000);
|
|
270
|
+
|
|
271
|
+
it("always shows standard Marty sensors and gates optional sensor blocks", async () => {
|
|
272
|
+
const { browser, page, errors } = await openEditor();
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await enableMartyMode(page);
|
|
276
|
+
await page.click("#marty-start");
|
|
277
|
+
await page.waitForSelector("#martycoloursensed_block", { visible: true });
|
|
278
|
+
await page.waitForSelector("#martyobstaclesensed_block", { visible: true });
|
|
279
|
+
|
|
280
|
+
const unpairedState = await getMartySensorPaletteState(page);
|
|
281
|
+
expect(unpairedState).toEqual({
|
|
282
|
+
colour: true,
|
|
283
|
+
obstacle: true,
|
|
284
|
+
light: false,
|
|
285
|
+
noise: false
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await page.evaluate(() => {
|
|
289
|
+
window.sensorPolicyTestManager = window.martyManager;
|
|
290
|
+
window.martyManager = undefined;
|
|
291
|
+
window.Palette.selectCategory(window.Palette.numcat);
|
|
292
|
+
});
|
|
293
|
+
const managerUnavailableState = await getMartySensorPaletteState(page);
|
|
294
|
+
expect(managerUnavailableState).toEqual({
|
|
295
|
+
colour: true,
|
|
296
|
+
obstacle: true,
|
|
297
|
+
light: false,
|
|
298
|
+
noise: false
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
await page.evaluate(() => {
|
|
302
|
+
window.martyManager = window.sensorPolicyTestManager;
|
|
303
|
+
delete window.sensorPolicyTestManager;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await page.evaluate(() => {
|
|
307
|
+
window.martyManager.addMarty({ id: "sensor-policy-test" });
|
|
308
|
+
window.martyManager.setMartySensorAvailability("sensor-policy-test", {
|
|
309
|
+
colour: false,
|
|
310
|
+
obstacle: false,
|
|
311
|
+
light: true,
|
|
312
|
+
noise: false
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
await page.waitForSelector("#martylightsensed_block", { visible: true });
|
|
316
|
+
|
|
317
|
+
const lightSensorState = await getMartySensorPaletteState(page);
|
|
318
|
+
expect(lightSensorState).toEqual({
|
|
319
|
+
colour: true,
|
|
320
|
+
obstacle: true,
|
|
321
|
+
light: true,
|
|
322
|
+
noise: false
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await page.evaluate(() => {
|
|
326
|
+
window.martyManager.setMartySensorAvailability("sensor-policy-test", {
|
|
327
|
+
light: false,
|
|
328
|
+
noise: true
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
await page.waitForSelector("#martynoisesensed_block", { visible: true });
|
|
332
|
+
await page.waitForFunction(() => !document.getElementById("martylightsensed_block"));
|
|
333
|
+
|
|
334
|
+
const noiseSensorState = await getMartySensorPaletteState(page);
|
|
335
|
+
expect(noiseSensorState).toEqual({
|
|
336
|
+
colour: true,
|
|
337
|
+
obstacle: true,
|
|
338
|
+
light: false,
|
|
339
|
+
noise: true
|
|
340
|
+
});
|
|
341
|
+
expect(errors).toEqual([]);
|
|
342
|
+
} finally {
|
|
343
|
+
await browser.close();
|
|
344
|
+
}
|
|
345
|
+
}, 60000);
|
|
262
346
|
});
|
|
@@ -108,13 +108,8 @@ async function assertTutorialOpened(page, tutorialId) {
|
|
|
108
108
|
expect(tutorialState.tutorialTitle.length).toBeGreaterThan(0);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
async function
|
|
112
|
-
await page
|
|
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
|
-
});
|
|
111
|
+
async function openExtensionsLibrary(page) {
|
|
112
|
+
await page.click("#addExtensionButton");
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
describe("Chromium 79 smoke test", () => {
|
|
@@ -132,7 +127,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
132
127
|
);
|
|
133
128
|
|
|
134
129
|
it(
|
|
135
|
-
"keeps the extension entry
|
|
130
|
+
"keeps the extension entry visible and reveals micro:bit controls only while loaded",
|
|
136
131
|
async () => {
|
|
137
132
|
const { browser, page, errors } = await openPage("/editor.html?mode=edit");
|
|
138
133
|
|
|
@@ -157,9 +152,9 @@ describe("Chromium 79 smoke test", () => {
|
|
|
157
152
|
});
|
|
158
153
|
|
|
159
154
|
expect(initialState).toEqual({
|
|
160
|
-
addButtonDisplay: "
|
|
161
|
-
addButtonAriaHidden: "
|
|
162
|
-
addButtonDisabled:
|
|
155
|
+
addButtonDisplay: "flex",
|
|
156
|
+
addButtonAriaHidden: "false",
|
|
157
|
+
addButtonDisabled: false,
|
|
163
158
|
microBitButtonDisplay: "none",
|
|
164
159
|
microBitButtonAriaHidden: "true",
|
|
165
160
|
microBitButtonDisabled: true,
|
|
@@ -167,7 +162,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
167
162
|
extensionEnabled: false,
|
|
168
163
|
});
|
|
169
164
|
|
|
170
|
-
await
|
|
165
|
+
await openExtensionsLibrary(page);
|
|
171
166
|
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
172
167
|
const libraryState = await page.evaluate(() => ({
|
|
173
168
|
title: document.getElementById("extensionsLibraryTitle").textContent.trim(),
|
|
@@ -221,9 +216,9 @@ describe("Chromium 79 smoke test", () => {
|
|
|
221
216
|
});
|
|
222
217
|
|
|
223
218
|
expect(enabledState).toEqual({
|
|
224
|
-
addButtonDisplay: "
|
|
225
|
-
addButtonAriaHidden: "
|
|
226
|
-
addButtonDisabled:
|
|
219
|
+
addButtonDisplay: "flex",
|
|
220
|
+
addButtonAriaHidden: "false",
|
|
221
|
+
addButtonDisabled: false,
|
|
227
222
|
microBitButtonDisplay: "flex",
|
|
228
223
|
microBitButtonAriaHidden: "false",
|
|
229
224
|
microBitButtonDisabled: false,
|
|
@@ -290,7 +285,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
290
285
|
});
|
|
291
286
|
expect(blocksAfterInsert).toContain("microbitdisplayclear");
|
|
292
287
|
|
|
293
|
-
await
|
|
288
|
+
await openExtensionsLibrary(page);
|
|
294
289
|
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
295
290
|
const loadedLibraryState = await page.evaluate(() => ({
|
|
296
291
|
microBitCardAction: document.getElementById("microBitExtensionCardAction").textContent.trim(),
|
|
@@ -339,7 +334,7 @@ describe("Chromium 79 smoke test", () => {
|
|
|
339
334
|
extensionEnabled: document.getElementById("connectionButtonsArea").classList.contains("extensionEnabled"),
|
|
340
335
|
}));
|
|
341
336
|
expect(unloadedState).toEqual({
|
|
342
|
-
addButtonDisplay: "
|
|
337
|
+
addButtonDisplay: "flex",
|
|
343
338
|
microBitButtonDisplay: "none",
|
|
344
339
|
extensionsLibraryOpen: false,
|
|
345
340
|
microBitCardAction: "+",
|
|
@@ -119,12 +119,7 @@ async function enableMartyMode(page) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
async function enableMicroBitExtension(page) {
|
|
122
|
-
await page
|
|
123
|
-
if (!button.disabled || window.getComputedStyle(button).display !== "none") {
|
|
124
|
-
throw new Error("Expected the extensions entry point to remain hidden and disabled");
|
|
125
|
-
}
|
|
126
|
-
button.onclick();
|
|
127
|
-
});
|
|
122
|
+
await page.click("#addExtensionButton");
|
|
128
123
|
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
129
124
|
await page.click("#microBitExtensionCard");
|
|
130
125
|
await page.waitForFunction(
|
|
@@ -110,12 +110,7 @@ async function enableMartyMode(page) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
async function enableMicroBitExtension(page) {
|
|
113
|
-
await page
|
|
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
|
-
});
|
|
113
|
+
await page.click("#addExtensionButton");
|
|
119
114
|
await page.waitForSelector("#extensionsLibrary.fade.in", { timeout: 30_000 });
|
|
120
115
|
await page.click("#microBitExtensionCard");
|
|
121
116
|
await page.waitForFunction(
|
|
@@ -0,0 +1,179 @@
|
|
|
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 = 3022;
|
|
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', (error) => errors.push(error.message || String(error)));
|
|
50
|
+
page.on('console', (message) => {
|
|
51
|
+
if (message.type() === 'error') {
|
|
52
|
+
errors.push(message.text());
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await page.goto(`${HOST}/editor.html?mode=edit`, {
|
|
56
|
+
waitUntil: 'networkidle2',
|
|
57
|
+
timeout: 30_000
|
|
58
|
+
});
|
|
59
|
+
await page.waitForFunction(() => {
|
|
60
|
+
const backdrop = document.getElementById('backdrop');
|
|
61
|
+
return Boolean(
|
|
62
|
+
window.ScratchJr &&
|
|
63
|
+
window.ScratchJr.stage &&
|
|
64
|
+
document.getElementById('spritecc') &&
|
|
65
|
+
document.getElementById('pagecc') &&
|
|
66
|
+
document.getElementById('scrollbar') &&
|
|
67
|
+
document.getElementById('pagescrollbar') &&
|
|
68
|
+
backdrop &&
|
|
69
|
+
window.getComputedStyle(backdrop).display === 'none'
|
|
70
|
+
);
|
|
71
|
+
}, { timeout: 30_000 });
|
|
72
|
+
return { browser, page, errors };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('editor sidebar scrollbars', () => {
|
|
76
|
+
it('shows and synchronizes persistent scrollbars when sprites and pages overflow', async () => {
|
|
77
|
+
const { browser, page, errors } = await openEditor();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const naturalSpriteState = await page.evaluate(() => {
|
|
81
|
+
const spriteContainer = document.getElementById('spriteScrollContainer');
|
|
82
|
+
const addSprite = document.getElementById('addsprite');
|
|
83
|
+
return {
|
|
84
|
+
addButtonScrollsWithPanel: addSprite.parentNode === spriteContainer,
|
|
85
|
+
overflow: spriteContainer.scrollHeight - spriteContainer.clientHeight,
|
|
86
|
+
scrollbarVisible: document.getElementById('scrollbar').classList.contains('on')
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
expect(naturalSpriteState.addButtonScrollsWithPanel).toBe(true);
|
|
90
|
+
if (naturalSpriteState.overflow <= 1) {
|
|
91
|
+
expect(naturalSpriteState.scrollbarVisible).toBe(false);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await page.evaluate(() => {
|
|
95
|
+
const spriteSpacer = document.createElement('div');
|
|
96
|
+
spriteSpacer.id = 'sprite-scroll-test-spacer';
|
|
97
|
+
spriteSpacer.style.height = '800px';
|
|
98
|
+
document.getElementById('spritecc').appendChild(spriteSpacer);
|
|
99
|
+
|
|
100
|
+
const pageSpacer = document.createElement('div');
|
|
101
|
+
pageSpacer.id = 'page-scroll-test-spacer';
|
|
102
|
+
pageSpacer.style.height = '800px';
|
|
103
|
+
document.getElementById('pagelist').appendChild(pageSpacer);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await page.waitForFunction(() =>
|
|
107
|
+
document.getElementById('scrollbar').classList.contains('on') &&
|
|
108
|
+
document.getElementById('pagescrollbar').classList.contains('on')
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const initialState = await page.evaluate(() => ({
|
|
112
|
+
spriteRole: document.getElementById('scrollbar').getAttribute('role'),
|
|
113
|
+
spriteLabel: document.getElementById('scrollbar').getAttribute('aria-label'),
|
|
114
|
+
spriteControls: document.getElementById('scrollbar').getAttribute('aria-controls'),
|
|
115
|
+
pageRole: document.getElementById('pagescrollbar').getAttribute('role'),
|
|
116
|
+
pageLabel: document.getElementById('pagescrollbar').getAttribute('aria-label'),
|
|
117
|
+
pageControls: document.getElementById('pagescrollbar').getAttribute('aria-controls'),
|
|
118
|
+
spriteThumbHeight: document.getElementById('sbthumb').offsetHeight,
|
|
119
|
+
pageThumbHeight: document.getElementById('pagesbthumb').offsetHeight,
|
|
120
|
+
addSpriteTop: document.getElementById('addsprite').getBoundingClientRect().top
|
|
121
|
+
}));
|
|
122
|
+
expect(initialState).toEqual({
|
|
123
|
+
spriteRole: 'scrollbar',
|
|
124
|
+
spriteLabel: 'Characters',
|
|
125
|
+
spriteControls: 'spriteScrollContainer',
|
|
126
|
+
pageRole: 'scrollbar',
|
|
127
|
+
pageLabel: 'Pages',
|
|
128
|
+
pageControls: 'pagecc',
|
|
129
|
+
spriteThumbHeight: expect.any(Number),
|
|
130
|
+
pageThumbHeight: expect.any(Number),
|
|
131
|
+
addSpriteTop: expect.any(Number)
|
|
132
|
+
});
|
|
133
|
+
expect(initialState.spriteThumbHeight).toBeGreaterThan(0);
|
|
134
|
+
expect(initialState.pageThumbHeight).toBeGreaterThan(0);
|
|
135
|
+
|
|
136
|
+
await page.evaluate(() => {
|
|
137
|
+
const spriteContainer = document.querySelector('#library .spritethumbs');
|
|
138
|
+
const pageContainer = document.getElementById('pagecc');
|
|
139
|
+
spriteContainer.scrollTop = spriteContainer.scrollHeight;
|
|
140
|
+
pageContainer.scrollTop = pageContainer.scrollHeight;
|
|
141
|
+
});
|
|
142
|
+
await page.waitForFunction(() =>
|
|
143
|
+
Number(document.getElementById('scrollbar').getAttribute('aria-valuenow')) > 0 &&
|
|
144
|
+
Number(document.getElementById('pagescrollbar').getAttribute('aria-valuenow')) > 0
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const scrolledState = await page.evaluate(() => ({
|
|
148
|
+
spriteThumbTop: document.getElementById('sbthumb').offsetTop,
|
|
149
|
+
pageThumbTop: document.getElementById('pagesbthumb').offsetTop,
|
|
150
|
+
spriteValue: Number(document.getElementById('scrollbar').getAttribute('aria-valuenow')),
|
|
151
|
+
pageValue: Number(document.getElementById('pagescrollbar').getAttribute('aria-valuenow')),
|
|
152
|
+
addSpriteTop: document.getElementById('addsprite').getBoundingClientRect().top
|
|
153
|
+
}));
|
|
154
|
+
expect(scrolledState.spriteThumbTop).toBeGreaterThan(0);
|
|
155
|
+
expect(scrolledState.pageThumbTop).toBeGreaterThan(0);
|
|
156
|
+
expect(scrolledState.spriteValue).toBeGreaterThan(0);
|
|
157
|
+
expect(scrolledState.pageValue).toBeGreaterThan(0);
|
|
158
|
+
expect(scrolledState.addSpriteTop).toBeLessThan(initialState.addSpriteTop);
|
|
159
|
+
|
|
160
|
+
await page.evaluate(() => {
|
|
161
|
+
document.getElementById('pagecc').scrollTop = 0;
|
|
162
|
+
});
|
|
163
|
+
const pageScrollBar = await page.$('#pagescrollbar');
|
|
164
|
+
const pageScrollBarBox = await pageScrollBar.boundingBox();
|
|
165
|
+
await page.mouse.click(
|
|
166
|
+
pageScrollBarBox.x + (pageScrollBarBox.width / 2),
|
|
167
|
+
pageScrollBarBox.y + pageScrollBarBox.height - 2
|
|
168
|
+
);
|
|
169
|
+
await page.waitForFunction(() => document.getElementById('pagecc').scrollTop > 0);
|
|
170
|
+
|
|
171
|
+
await page.focus('#pagescrollbar');
|
|
172
|
+
await page.keyboard.press('Home');
|
|
173
|
+
await page.waitForFunction(() => document.getElementById('pagecc').scrollTop === 0);
|
|
174
|
+
expect(errors).toEqual([]);
|
|
175
|
+
} finally {
|
|
176
|
+
await browser.close();
|
|
177
|
+
}
|
|
178
|
+
}, 45_000);
|
|
179
|
+
});
|