@robotical/martyblocksjr 4.2.2 → 4.2.4
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/marty-connection-ui.e2e.test.js +177 -12
- 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: "+",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
const { newHTMLMock } = vi.hoisted(() => ({
|
|
4
|
+
newHTMLMock: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
|
|
3
7
|
vi.mock('@/editor/ScratchJr', () => ({
|
|
4
8
|
default: {
|
|
5
9
|
isMartyModeEnabled: false,
|
|
@@ -42,7 +46,7 @@ vi.mock('@/utils/lib', () => ({
|
|
|
42
46
|
gn: vi.fn(),
|
|
43
47
|
CSSTransition: vi.fn(),
|
|
44
48
|
localx: vi.fn(),
|
|
45
|
-
newHTML:
|
|
49
|
+
newHTML: newHTMLMock,
|
|
46
50
|
newButton: vi.fn(),
|
|
47
51
|
scaleMultiplier: 1,
|
|
48
52
|
fullscreenScaleMultiplier: 1,
|
|
@@ -75,18 +79,36 @@ vi.mock('@/html-svgs/sprite_toggle_on', () => ({ spriteToggleOn: '<svg></svg>' }
|
|
|
75
79
|
vi.mock('@/html-svgs/battery-svg', () => ({ batterySvg: () => '<svg id="battery"></svg>' }));
|
|
76
80
|
vi.mock('@/html-svgs/signal-svg', () => ({ signalSvg: () => '<svg id="signal"></svg>' }));
|
|
77
81
|
vi.mock('@/utils/raft-subscription-helpers', () => ({
|
|
78
|
-
createRaftConnectionIssueDetectedHelper:
|
|
79
|
-
subscribe: vi.fn(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
createRaftConnectionIssueDetectedHelper: raft => ({
|
|
83
|
+
subscribe: vi.fn(callback => {
|
|
84
|
+
raft.__connectionIssueDetectedCallback = callback;
|
|
85
|
+
}),
|
|
86
|
+
unsubscribe: vi.fn(() => {
|
|
87
|
+
raft.__connectionIssueDetectedUnsubscribed = true;
|
|
88
|
+
})
|
|
85
89
|
}),
|
|
86
|
-
|
|
87
|
-
subscribe: vi.fn(
|
|
88
|
-
|
|
90
|
+
createRaftConnectionIssueResolvedHelper: raft => ({
|
|
91
|
+
subscribe: vi.fn(callback => {
|
|
92
|
+
raft.__connectionIssueResolvedCallback = callback;
|
|
93
|
+
}),
|
|
94
|
+
unsubscribe: vi.fn(() => {
|
|
95
|
+
raft.__connectionIssueResolvedUnsubscribed = true;
|
|
96
|
+
})
|
|
89
97
|
}),
|
|
98
|
+
raftVerifiedSubscriptionHelper: raft => {
|
|
99
|
+
let callback;
|
|
100
|
+
return {
|
|
101
|
+
subscribe: vi.fn(nextCallback => {
|
|
102
|
+
callback = nextCallback;
|
|
103
|
+
raft.__verifiedCallbacks = raft.__verifiedCallbacks || [];
|
|
104
|
+
raft.__verifiedCallbacks.push(callback);
|
|
105
|
+
}),
|
|
106
|
+
unsubscribe: vi.fn(() => {
|
|
107
|
+
raft.__verifiedUnsubscribeCount = (raft.__verifiedUnsubscribeCount || 0) + 1;
|
|
108
|
+
raft.__verifiedCallbacks = (raft.__verifiedCallbacks || []).filter(item => item !== callback);
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
},
|
|
90
112
|
raftDisconnectedSubscriptionHelper: raft => {
|
|
91
113
|
const helper = {
|
|
92
114
|
subscribe: vi.fn(callback => {
|
|
@@ -128,6 +150,11 @@ describe('Marty connection UI', () => {
|
|
|
128
150
|
removeMarty: vi.fn(),
|
|
129
151
|
setMartySensorAvailability: vi.fn()
|
|
130
152
|
},
|
|
153
|
+
cogManager: {
|
|
154
|
+
addCog: vi.fn(),
|
|
155
|
+
wireCogWithBlocks: vi.fn(),
|
|
156
|
+
removeCog: vi.fn()
|
|
157
|
+
},
|
|
131
158
|
microBitManager: {
|
|
132
159
|
addMicroBit: vi.fn(),
|
|
133
160
|
wireMicroBitWithBlocks: vi.fn(),
|
|
@@ -139,6 +166,13 @@ describe('Marty connection UI', () => {
|
|
|
139
166
|
import('@/editor/ui/UI.js'),
|
|
140
167
|
import('@/editor/ScratchJr')
|
|
141
168
|
]);
|
|
169
|
+
newHTMLMock.mockImplementation((type, className, parent) => {
|
|
170
|
+
const element = new FakeElement(className ? [className] : []);
|
|
171
|
+
if (parent) {
|
|
172
|
+
parent.appendChild(element);
|
|
173
|
+
}
|
|
174
|
+
return element;
|
|
175
|
+
});
|
|
142
176
|
UI = uiModule.default;
|
|
143
177
|
ScratchJr = scratchJrModule.default;
|
|
144
178
|
ScratchJr.isMartyModeEnabled = false;
|
|
@@ -249,6 +283,102 @@ describe('Marty connection UI', () => {
|
|
|
249
283
|
expect(button.querySelector('.iconButtonContainer').classList.contains('connectedButtonContainer')).toBe(true);
|
|
250
284
|
});
|
|
251
285
|
|
|
286
|
+
it('uses one verified subscription instance and cleans it up after setup', () => {
|
|
287
|
+
const button = createConnectionButton();
|
|
288
|
+
const raft = createCogRaft();
|
|
289
|
+
const setupConnectionButton = vi.fn();
|
|
290
|
+
|
|
291
|
+
UI.setupConnectionButtonWhenVerified(button, raft, setupConnectionButton);
|
|
292
|
+
|
|
293
|
+
expect(raft.__verifiedCallbacks).toHaveLength(1);
|
|
294
|
+
const verifiedCallback = raft.__verifiedCallbacks[0];
|
|
295
|
+
raft.isVerified = true;
|
|
296
|
+
verifiedCallback();
|
|
297
|
+
verifiedCallback();
|
|
298
|
+
|
|
299
|
+
expect(setupConnectionButton).toHaveBeenCalledTimes(1);
|
|
300
|
+
expect(setupConnectionButton).toHaveBeenCalledWith(button, raft);
|
|
301
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
302
|
+
expect(raft.__verifiedCallbacks).toHaveLength(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('sets up an already verified Cog even when its verified event was missed', () => {
|
|
306
|
+
const button = createConnectionButton();
|
|
307
|
+
const raft = createCogRaft();
|
|
308
|
+
const setupConnectionButton = vi.fn();
|
|
309
|
+
raft.isVerified = true;
|
|
310
|
+
|
|
311
|
+
UI.setupConnectionButtonWhenVerified(button, raft, setupConnectionButton);
|
|
312
|
+
|
|
313
|
+
expect(setupConnectionButton).toHaveBeenCalledWith(button, raft);
|
|
314
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
315
|
+
expect(raft.__verifiedCallbacks).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reconciles an existing verified Cog after the host application manager is injected', () => {
|
|
319
|
+
const cogButton = createConnectionButton();
|
|
320
|
+
const martyButton = createConnectionButton();
|
|
321
|
+
const raft = createCogRaft();
|
|
322
|
+
raft.isVerified = true;
|
|
323
|
+
const setupCogSpy = vi.spyOn(UI, 'setupCogConnectionButton').mockImplementation(() => {});
|
|
324
|
+
window.applicationManager = undefined;
|
|
325
|
+
|
|
326
|
+
expect(UI.reconcileConnectionButtonsWhenApplicationManagerReady(cogButton, martyButton)).toBe(false);
|
|
327
|
+
window.applicationManager = {
|
|
328
|
+
getTheCurrentlySelectedDeviceOrFirstOfItsKind: vi.fn(type => type === 'Cog' ? raft : undefined)
|
|
329
|
+
};
|
|
330
|
+
vi.advanceTimersByTime(100);
|
|
331
|
+
|
|
332
|
+
expect(setupCogSpy).toHaveBeenCalledWith(cogButton, raft);
|
|
333
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
334
|
+
setupCogSpy.mockRestore();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('disconnects and clears a stale Cog when the connection issue countdown expires', () => {
|
|
338
|
+
const button = createConnectionButton();
|
|
339
|
+
const raft = createCogRaft();
|
|
340
|
+
window.applicationManager.disconnectGeneric = vi.fn((disconnectedRaft, onDisconnected) => {
|
|
341
|
+
onDisconnected();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
UI.setupCogConnectionButton(button, raft);
|
|
345
|
+
raft.__connectionIssueDetectedCallback();
|
|
346
|
+
|
|
347
|
+
expect(button.querySelector('.connIssueOverlay').textContent).toBe('A11Y_CONNECTION_LOST 60');
|
|
348
|
+
expect(button.style.pointerEvents).toBe('none');
|
|
349
|
+
|
|
350
|
+
vi.advanceTimersByTime(59000);
|
|
351
|
+
expect(window.applicationManager.disconnectGeneric).not.toHaveBeenCalled();
|
|
352
|
+
expect(button.querySelector('.connIssueOverlay').textContent).toBe('A11Y_CONNECTION_LOST 1');
|
|
353
|
+
|
|
354
|
+
vi.advanceTimersByTime(1000);
|
|
355
|
+
|
|
356
|
+
expect(window.applicationManager.disconnectGeneric).toHaveBeenCalledWith(raft, expect.any(Function), true);
|
|
357
|
+
expect(window.cogManager.removeCog).toHaveBeenCalledWith(raft);
|
|
358
|
+
expect(button.querySelector('.connIssueOverlay')).toBeNull();
|
|
359
|
+
expect(button.style.pointerEvents).toBe('auto');
|
|
360
|
+
expect(button.classList.contains('connectButtonConnected')).toBe(false);
|
|
361
|
+
expect(raft.__connectionIssueDetectedUnsubscribed).toBe(true);
|
|
362
|
+
expect(raft.__connectionIssueResolvedUnsubscribed).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('cancels the Cog countdown when the connection issue resolves', () => {
|
|
366
|
+
const button = createConnectionButton();
|
|
367
|
+
const raft = createCogRaft();
|
|
368
|
+
|
|
369
|
+
UI.setupCogConnectionButton(button, raft);
|
|
370
|
+
raft.__connectionIssueDetectedCallback();
|
|
371
|
+
vi.advanceTimersByTime(30000);
|
|
372
|
+
raft.__connectionIssueResolvedCallback();
|
|
373
|
+
vi.advanceTimersByTime(31000);
|
|
374
|
+
|
|
375
|
+
expect(window.applicationManager.disconnectGeneric).not.toHaveBeenCalled();
|
|
376
|
+
expect(button.querySelector('.connIssueOverlay')).toBeNull();
|
|
377
|
+
expect(button.style.pointerEvents).toBe('auto');
|
|
378
|
+
|
|
379
|
+
raft.__disconnectCallback();
|
|
380
|
+
});
|
|
381
|
+
|
|
252
382
|
it('uses a friendly message when the micro:bit chooser is cancelled', () => {
|
|
253
383
|
const message = UI.getMicroBitConnectionErrorMessage(
|
|
254
384
|
new DOMException('User cancelled the requestDevice() chooser.', 'NotFoundError')
|
|
@@ -293,6 +423,19 @@ function createMartyRaft() {
|
|
|
293
423
|
};
|
|
294
424
|
}
|
|
295
425
|
|
|
426
|
+
function createCogRaft() {
|
|
427
|
+
return {
|
|
428
|
+
id: 'cog-1',
|
|
429
|
+
isVerified: false,
|
|
430
|
+
hasVerifiedConnection() {
|
|
431
|
+
return this.isVerified;
|
|
432
|
+
},
|
|
433
|
+
getFriendlyName: () => 'Cog One',
|
|
434
|
+
getBatteryStrength: () => 70,
|
|
435
|
+
getRSSI: () => -45
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
296
439
|
function createConnectionButton() {
|
|
297
440
|
const button = new FakeElement();
|
|
298
441
|
button.children['.iconButtonContainer'] = new FakeElement(['iconButtonContainer', 'notConnectedButtonContainer']);
|
|
@@ -325,6 +468,7 @@ class FakeElement {
|
|
|
325
468
|
constructor(classes = []) {
|
|
326
469
|
this.classList = new FakeClassList(classes);
|
|
327
470
|
this.children = {};
|
|
471
|
+
this.childNodes = [];
|
|
328
472
|
this.style = {};
|
|
329
473
|
this.attributes = {};
|
|
330
474
|
this.textContent = '';
|
|
@@ -333,7 +477,28 @@ class FakeElement {
|
|
|
333
477
|
}
|
|
334
478
|
|
|
335
479
|
querySelector(selector) {
|
|
336
|
-
|
|
480
|
+
if (this.children[selector]) {
|
|
481
|
+
return this.children[selector];
|
|
482
|
+
}
|
|
483
|
+
if (selector.startsWith('.')) {
|
|
484
|
+
const className = selector.slice(1);
|
|
485
|
+
return this.childNodes.find(child => child.classList.contains(className)) || null;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
appendChild(child) {
|
|
491
|
+
if (!this.childNodes.includes(child)) {
|
|
492
|
+
this.childNodes.push(child);
|
|
493
|
+
}
|
|
494
|
+
child.parentNode = this;
|
|
495
|
+
return child;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
removeChild(child) {
|
|
499
|
+
this.childNodes = this.childNodes.filter(item => item !== child);
|
|
500
|
+
child.parentNode = null;
|
|
501
|
+
return child;
|
|
337
502
|
}
|
|
338
503
|
|
|
339
504
|
setAttribute(name, value) {
|
|
@@ -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(
|