@robotical/martyblocksjr 3.5.33 → 4.0.0
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/base.css +77 -0
- package/editions/free/src/css/connection-modal.css +6 -1
- package/editions/free/src/css/editor.css +86 -34
- package/editions/free/src/css/editorleftpanel.css +8 -0
- package/editions/free/src/css/editorstage.css +5 -1
- package/editions/free/src/css/lobby.css +28 -9
- package/editions/free/src/css/thumbs.css +18 -0
- package/editions/free/src/css/tutorial.css +6 -1
- package/editions/free/src/home.html +46 -26
- package/editions/free/src/inapp/blocks.html +43 -43
- package/editions/free/src/inapp/interface.html +49 -49
- package/editions/free/src/inapp/paint.html +40 -40
- package/editions/free/src/inapp/style/interface.css +2 -0
- package/editions/free/src/inapp/style/paint.css +2 -0
- package/editions/free/src/inapp/style/style.css +44 -0
- 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 +41 -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 +6 -2
- package/scripts/a11y-structure.mjs +177 -0
- package/tests/e2e/accessibility.e2e.test.js +596 -0
- package/vitest.config.js +2 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import puppeteer from 'puppeteer';
|
|
7
|
+
|
|
8
|
+
const PORT = 3012;
|
|
9
|
+
const HOST = `http://localhost:${PORT}`;
|
|
10
|
+
const AXE_SOURCE = fs.readFileSync(
|
|
11
|
+
path.resolve(process.cwd(), 'node_modules/axe-core/axe.min.js'),
|
|
12
|
+
'utf8'
|
|
13
|
+
);
|
|
14
|
+
const SETTINGS_PATH = path.resolve(process.cwd(), 'editions/free/src/settings.json');
|
|
15
|
+
const BASE_SETTINGS = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
|
|
16
|
+
const AXE_OPTIONS = {
|
|
17
|
+
runOnly: {
|
|
18
|
+
type: 'rule',
|
|
19
|
+
values: [
|
|
20
|
+
'aria-dialog-name',
|
|
21
|
+
'button-name',
|
|
22
|
+
'document-title',
|
|
23
|
+
'frame-title',
|
|
24
|
+
'html-has-lang',
|
|
25
|
+
'image-alt',
|
|
26
|
+
'landmark-one-main',
|
|
27
|
+
'region'
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let server;
|
|
33
|
+
|
|
34
|
+
async function waitForServer(maxAttempts = 20) {
|
|
35
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
36
|
+
const ok = await new Promise((resolve) => {
|
|
37
|
+
const req = http.get(`${HOST}/`, (res) => {
|
|
38
|
+
res.destroy();
|
|
39
|
+
resolve(true);
|
|
40
|
+
});
|
|
41
|
+
req.on('error', () => resolve(false));
|
|
42
|
+
});
|
|
43
|
+
if (ok) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Local server did not start in time');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function createPage(browser, options = {}) {
|
|
52
|
+
const { shareEnabled = false } = options;
|
|
53
|
+
const page = await browser.newPage();
|
|
54
|
+
const errors = [];
|
|
55
|
+
|
|
56
|
+
page.on('pageerror', (err) => {
|
|
57
|
+
errors.push(err.message || String(err));
|
|
58
|
+
});
|
|
59
|
+
page.on('console', (msg) => {
|
|
60
|
+
if (msg.type() === 'error') {
|
|
61
|
+
errors.push(msg.text());
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (shareEnabled) {
|
|
66
|
+
await page.setRequestInterception(true);
|
|
67
|
+
page.on('request', (request) => {
|
|
68
|
+
if (request.url().endsWith('/settings.json')) {
|
|
69
|
+
request.respond({
|
|
70
|
+
status: 200,
|
|
71
|
+
contentType: 'application/json',
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
...BASE_SETTINGS,
|
|
74
|
+
shareEnabled: true
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
request.continue();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { page, errors };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function injectAxe(page) {
|
|
87
|
+
const hasAxe = await page.evaluate(() => Boolean(window.axe));
|
|
88
|
+
if (!hasAxe) {
|
|
89
|
+
await page.addScriptTag({ content: AXE_SOURCE });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runAxe(page, selector = 'html') {
|
|
94
|
+
await injectAxe(page);
|
|
95
|
+
return page.evaluate(async (targetSelector, options) => {
|
|
96
|
+
const target = targetSelector ? document.querySelector(targetSelector) : document;
|
|
97
|
+
return window.axe.run(target || document, options);
|
|
98
|
+
}, selector, AXE_OPTIONS);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function expectNoAxeViolations(results) {
|
|
102
|
+
const summary = results.violations.map((violation) => ({
|
|
103
|
+
id: violation.id,
|
|
104
|
+
impact: violation.impact,
|
|
105
|
+
nodes: violation.nodes.length
|
|
106
|
+
}));
|
|
107
|
+
expect(summary).toEqual([]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function waitForTitle(page, text) {
|
|
111
|
+
await page.waitForFunction((expected) => document.title.includes(expected), {}, text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function tabToSelector(page, selector, maxTabs = 20) {
|
|
115
|
+
for (let index = 0; index < maxTabs; index += 1) {
|
|
116
|
+
await page.keyboard.press('Tab');
|
|
117
|
+
const activeMatches = await page.evaluate((targetSelector) => {
|
|
118
|
+
const active = document.activeElement;
|
|
119
|
+
return Boolean(active && active.matches && active.matches(targetSelector));
|
|
120
|
+
}, selector);
|
|
121
|
+
if (activeMatches) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Failed to tab to ${selector}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function getFocusStyles(page, selector) {
|
|
129
|
+
return page.$eval(selector, (element) => {
|
|
130
|
+
const styles = window.getComputedStyle(element);
|
|
131
|
+
return {
|
|
132
|
+
outlineStyle: styles.outlineStyle,
|
|
133
|
+
outlineWidth: parseFloat(styles.outlineWidth) || 0,
|
|
134
|
+
outlineColor: styles.outlineColor,
|
|
135
|
+
boxShadow: styles.boxShadow
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function expectVisibleFocusIndicator(focusStyles) {
|
|
141
|
+
expect(focusStyles.outlineStyle).not.toBe('none');
|
|
142
|
+
expect(focusStyles.outlineWidth).toBeGreaterThan(0);
|
|
143
|
+
expect(focusStyles.outlineColor).not.toBe('rgba(0, 0, 0, 0)');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function expectNamedControls(page, selectors) {
|
|
147
|
+
const missing = await page.evaluate((buttonSelectors) => {
|
|
148
|
+
return buttonSelectors.filter((selector) => {
|
|
149
|
+
const button = document.querySelector(selector);
|
|
150
|
+
return !button || !button.getAttribute('aria-label');
|
|
151
|
+
});
|
|
152
|
+
}, selectors);
|
|
153
|
+
expect(missing).toEqual([]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
beforeEach(async () => {
|
|
157
|
+
server = spawn('python3', ['-m', 'http.server', `${PORT}`, '--directory', 'editions/free/src'], {
|
|
158
|
+
stdio: 'ignore'
|
|
159
|
+
});
|
|
160
|
+
await waitForServer();
|
|
161
|
+
}, 30_000);
|
|
162
|
+
|
|
163
|
+
afterEach(() => {
|
|
164
|
+
if (server) {
|
|
165
|
+
server.kill();
|
|
166
|
+
server = null;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('Accessibility shell audit', () => {
|
|
171
|
+
it(
|
|
172
|
+
'keeps the home shell accessible and titles the guide iframe',
|
|
173
|
+
async () => {
|
|
174
|
+
const browser = await puppeteer.launch({
|
|
175
|
+
headless: true,
|
|
176
|
+
args: ['--no-sandbox']
|
|
177
|
+
});
|
|
178
|
+
const { page, errors } = await createPage(browser);
|
|
179
|
+
|
|
180
|
+
await page.goto(`${HOST}/home.html`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
181
|
+
await waitForTitle(page, 'My Projects');
|
|
182
|
+
await page.waitForSelector('#skip-link');
|
|
183
|
+
await page.waitForSelector('#logotab[aria-hidden="true"]');
|
|
184
|
+
await page.waitForSelector('#newproject', { timeout: 30_000 });
|
|
185
|
+
|
|
186
|
+
expect(await page.title()).toBe('My Projects - ScratchJr');
|
|
187
|
+
expect(await page.$eval('html', (element) => element.lang)).toBe('en');
|
|
188
|
+
expect(await page.$eval('#skip-link', (element) => element.getAttribute('href'))).toBe('#wrapc');
|
|
189
|
+
expect(await page.$eval('#topbar', (element) => element.getAttribute('role'))).toBe('navigation');
|
|
190
|
+
expect(await page.$eval('#footernav', (element) => element.getAttribute('role'))).toBe('navigation');
|
|
191
|
+
expect(await page.$eval('#wrapc', (element) => element.getAttribute('role'))).toBe('main');
|
|
192
|
+
expect(await page.$eval('#logotab', (element) => element.getAttribute('aria-hidden'))).toBe('true');
|
|
193
|
+
expect(await page.$eval('#logotab', (element) => element.getAttribute('tabindex'))).toBe('-1');
|
|
194
|
+
expect(await page.$eval('#logotab', (element) => {
|
|
195
|
+
const styles = window.getComputedStyle(element);
|
|
196
|
+
return {
|
|
197
|
+
display: styles.display,
|
|
198
|
+
visibility: styles.visibility,
|
|
199
|
+
pointerEvents: styles.pointerEvents,
|
|
200
|
+
};
|
|
201
|
+
})).toEqual({
|
|
202
|
+
display: 'block',
|
|
203
|
+
visibility: 'hidden',
|
|
204
|
+
pointerEvents: 'none',
|
|
205
|
+
});
|
|
206
|
+
expect(await page.$eval('#logotab', (element) => element.getBoundingClientRect().width)).toBeGreaterThan(0);
|
|
207
|
+
|
|
208
|
+
await expectNamedControls(page, [
|
|
209
|
+
'#hometab',
|
|
210
|
+
'#geartab',
|
|
211
|
+
'#booktab',
|
|
212
|
+
'#tabicon'
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
await tabToSelector(page, '#skip-link');
|
|
216
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#skip-link'));
|
|
217
|
+
|
|
218
|
+
await tabToSelector(page, '#hometab');
|
|
219
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#hometab'));
|
|
220
|
+
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
222
|
+
await page.click('#booktab');
|
|
223
|
+
await page.waitForSelector('iframe#htmlcontents', { timeout: 30_000 });
|
|
224
|
+
expect(await page.$eval('iframe#htmlcontents', (element) => element.getAttribute('title'))).toBe('About ScratchJr');
|
|
225
|
+
|
|
226
|
+
expectNoAxeViolations(await runAxe(page));
|
|
227
|
+
expect(errors).toEqual([]);
|
|
228
|
+
|
|
229
|
+
await browser.close();
|
|
230
|
+
},
|
|
231
|
+
90_000
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
it(
|
|
235
|
+
'keeps the guide pages accessible',
|
|
236
|
+
async () => {
|
|
237
|
+
const browser = await puppeteer.launch({
|
|
238
|
+
headless: true,
|
|
239
|
+
args: ['--no-sandbox']
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const guideCases = [
|
|
243
|
+
{
|
|
244
|
+
path: 'inapp/interface.html',
|
|
245
|
+
title: 'Interface Guide',
|
|
246
|
+
focusSelector: '#interface-button-save',
|
|
247
|
+
buttonSelectors: ['#interface-button-save', '#interface-button-stage', '#interface-button-green-flag'],
|
|
248
|
+
decorativeImageSelector: '.ipad-project-view'
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
path: 'inapp/paint.html',
|
|
252
|
+
title: 'Paint Editor Guide',
|
|
253
|
+
focusSelector: '#paint-button-undo',
|
|
254
|
+
buttonSelectors: ['#paint-button-undo', '#paint-button-redo', '#paint-button-save'],
|
|
255
|
+
decorativeImageSelector: '.ipad-project-view'
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
path: 'inapp/blocks.html',
|
|
259
|
+
title: 'Blocks Guide',
|
|
260
|
+
focusSelector: '#skip-link',
|
|
261
|
+
buttonSelectors: [],
|
|
262
|
+
decorativeImageSelector: '.block-image'
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
for (const guideCase of guideCases) {
|
|
267
|
+
const { page, errors } = await createPage(browser);
|
|
268
|
+
|
|
269
|
+
await page.goto(`${HOST}/${guideCase.path}`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
270
|
+
await waitForTitle(page, guideCase.title);
|
|
271
|
+
await page.waitForSelector('#skip-link');
|
|
272
|
+
await page.waitForSelector('#content[role="main"]');
|
|
273
|
+
|
|
274
|
+
expect((await page.title()).includes(guideCase.title)).toBe(true);
|
|
275
|
+
expect(await page.$eval('html', (element) => element.lang)).toBe('en');
|
|
276
|
+
expect(await page.$eval('#skip-link', (element) => element.getAttribute('href'))).toBe('#content');
|
|
277
|
+
expect(await page.$eval('#content', (element) => element.getAttribute('role'))).toBe('main');
|
|
278
|
+
|
|
279
|
+
if (guideCase.buttonSelectors.length > 0) {
|
|
280
|
+
await expectNamedControls(page, guideCase.buttonSelectors);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const decorativeAlts = await page.$$eval(guideCase.decorativeImageSelector, (images) => {
|
|
284
|
+
return images.map((image) => image.getAttribute('alt'));
|
|
285
|
+
});
|
|
286
|
+
expect(decorativeAlts.every((alt) => alt === '')).toBe(true);
|
|
287
|
+
|
|
288
|
+
await tabToSelector(page, guideCase.focusSelector);
|
|
289
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, guideCase.focusSelector));
|
|
290
|
+
|
|
291
|
+
expectNoAxeViolations(await runAxe(page));
|
|
292
|
+
expect(errors).toEqual([]);
|
|
293
|
+
await page.close();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await browser.close();
|
|
297
|
+
},
|
|
298
|
+
120_000
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
it(
|
|
302
|
+
'keeps the editor shell dialogs accessible',
|
|
303
|
+
async () => {
|
|
304
|
+
const browser = await puppeteer.launch({
|
|
305
|
+
headless: true,
|
|
306
|
+
args: ['--no-sandbox']
|
|
307
|
+
});
|
|
308
|
+
const { page, errors } = await createPage(browser, { shareEnabled: true });
|
|
309
|
+
|
|
310
|
+
await page.goto(`${HOST}/home.html`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
311
|
+
await page.waitForSelector('#newproject .card-action-open', { timeout: 30_000 });
|
|
312
|
+
await Promise.all([
|
|
313
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30_000 }),
|
|
314
|
+
page.$eval('#newproject .card-action-open', (button) => button.click())
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
await waitForTitle(page, 'Project Editor');
|
|
318
|
+
await page.waitForSelector('#projectinfo[aria-label]', { timeout: 30_000 });
|
|
319
|
+
await page.waitForSelector('#grid[aria-label]', { timeout: 30_000 });
|
|
320
|
+
await page.waitForSelector('#skip-link');
|
|
321
|
+
await page.waitForSelector('#sprite-start[aria-label]', { timeout: 30_000 });
|
|
322
|
+
await page.waitForSelector('#pagecc[aria-label]', { timeout: 30_000 });
|
|
323
|
+
await page.waitForSelector('#spritecc[aria-label]', { timeout: 30_000 });
|
|
324
|
+
await page.waitForSelector('#emptypage[aria-label]', { timeout: 30_000 });
|
|
325
|
+
await page.waitForFunction(() => {
|
|
326
|
+
const projectInfoButton = document.getElementById('projectinfo');
|
|
327
|
+
return Boolean(projectInfoButton && !projectInfoButton.disabled);
|
|
328
|
+
}, { timeout: 30_000 });
|
|
329
|
+
|
|
330
|
+
expect(await page.title()).toBe('Project Editor - ScratchJr');
|
|
331
|
+
expect(await page.$eval('html', (element) => element.lang)).toBe('en');
|
|
332
|
+
expect(await page.$eval('#skip-link', (element) => element.getAttribute('href'))).toBe('#frame');
|
|
333
|
+
expect(await page.$eval('#frame', (element) => element.getAttribute('role'))).toBe('main');
|
|
334
|
+
expect(await page.$eval('#pagecc', (element) => element.getAttribute('role'))).toBe('group');
|
|
335
|
+
expect(await page.$eval('#pagecc', (element) => element.getAttribute('aria-label'))).toBe('Pages');
|
|
336
|
+
expect(await page.$eval('#spritecc', (element) => element.getAttribute('role'))).toBe('group');
|
|
337
|
+
expect(await page.$eval('#spritecc', (element) => element.getAttribute('aria-label'))).toBe('Characters');
|
|
338
|
+
expect(await page.$eval('#selectors', (element) => element.getAttribute('role'))).toBe('toolbar');
|
|
339
|
+
expect(await page.$eval('#selectors', (element) => element.getAttribute('aria-label'))).toBe('Block Categories');
|
|
340
|
+
expect(await page.$eval('#palette', (element) => element.getAttribute('role'))).toBe('group');
|
|
341
|
+
expect(await page.$eval('#palette', (element) => element.getAttribute('aria-label'))).toBe('Blocks Palette');
|
|
342
|
+
expect(await page.$eval('#scripts', (element) => element.getAttribute('role'))).toBe('group');
|
|
343
|
+
expect(await page.$eval('#scripts', (element) => element.getAttribute('aria-label'))).toBe('Programming Area');
|
|
344
|
+
expect(await page.$eval('#scriptscontainer', (element) => element.getAttribute('role'))).toBe('list');
|
|
345
|
+
expect(await page.$eval('#scriptscontainer', (element) => element.getAttribute('aria-label'))).toBe('Programming Script');
|
|
346
|
+
|
|
347
|
+
await expectNamedControls(page, [
|
|
348
|
+
'#projectinfo',
|
|
349
|
+
'#grid',
|
|
350
|
+
'#traceBtn',
|
|
351
|
+
'#go',
|
|
352
|
+
'#resetall',
|
|
353
|
+
'#full',
|
|
354
|
+
'#sprite-start',
|
|
355
|
+
'#sprite-motion',
|
|
356
|
+
'#emptypage'
|
|
357
|
+
]);
|
|
358
|
+
await tabToSelector(page, '#sprite-start', 50);
|
|
359
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#sprite-start'));
|
|
360
|
+
await page.keyboard.press('ArrowRight');
|
|
361
|
+
expect(await page.evaluate(() => document.activeElement.id)).toBe('sprite-motion');
|
|
362
|
+
await page.keyboard.press('Enter');
|
|
363
|
+
await page.waitForFunction(() => {
|
|
364
|
+
const motionButton = document.getElementById('sprite-motion');
|
|
365
|
+
return motionButton && motionButton.getAttribute('aria-pressed') === 'true';
|
|
366
|
+
}, { timeout: 5_000 });
|
|
367
|
+
await page.waitForSelector('#forward_block[role="button"]', { timeout: 30_000 });
|
|
368
|
+
|
|
369
|
+
await expectNamedControls(page, ['#forward_block', '#back_block']);
|
|
370
|
+
await page.waitForFunction(() => {
|
|
371
|
+
const buttons = Array.from(document.querySelectorAll('#palette [role="button"]'));
|
|
372
|
+
return buttons.length > 1 && buttons.every((button) => button.getAttribute('aria-label'));
|
|
373
|
+
}, { timeout: 5_000 });
|
|
374
|
+
|
|
375
|
+
await page.focus('#forward_block');
|
|
376
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#forward_block'));
|
|
377
|
+
await page.keyboard.press('ArrowRight');
|
|
378
|
+
expect(await page.evaluate(() => document.activeElement.getAttribute('data-blocktype'))).toBe('back');
|
|
379
|
+
|
|
380
|
+
const blockCountBefore = await page.evaluate(() => window.ScratchJr.getBlocks().length);
|
|
381
|
+
await page.keyboard.press('Enter');
|
|
382
|
+
await page.waitForFunction((before) => {
|
|
383
|
+
return window.ScratchJr.getBlocks().length === before + 1
|
|
384
|
+
&& window.ScratchJr.getBlocks().some((block) => block.blocktype === 'back');
|
|
385
|
+
}, { timeout: 30_000 }, blockCountBefore);
|
|
386
|
+
await page.waitForSelector('#scriptscontainer .keyboard-script-strip[role="button"]', { timeout: 30_000 });
|
|
387
|
+
await expectNamedControls(page, ['#scriptscontainer .keyboard-script-strip[role="button"]']);
|
|
388
|
+
|
|
389
|
+
await page.focus('#scriptscontainer .keyboard-script-strip[role="button"]');
|
|
390
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#scriptscontainer .keyboard-script-strip[role="button"]'));
|
|
391
|
+
expect(await page.$eval('#scriptscontainer .keyboard-script-strip[role="button"]',
|
|
392
|
+
(element) => element.getAttribute('aria-pressed'))).toBe('true');
|
|
393
|
+
|
|
394
|
+
const stripBlockCountBefore = await page.evaluate(() => window.ScratchJr.getBlocks().length);
|
|
395
|
+
await page.focus('#back_block');
|
|
396
|
+
await page.keyboard.press('Enter');
|
|
397
|
+
await page.waitForFunction((before) => {
|
|
398
|
+
return window.ScratchJr.getBlocks().length === before + 1
|
|
399
|
+
&& window.ScratchJr.getActiveScript().owner.gettopblocks().length === 1;
|
|
400
|
+
}, { timeout: 30_000 }, stripBlockCountBefore);
|
|
401
|
+
|
|
402
|
+
await page.focus('#scriptscontainer .keyboard-script-strip[role="button"]');
|
|
403
|
+
await page.keyboard.press('Delete');
|
|
404
|
+
await page.waitForFunction(() => {
|
|
405
|
+
return window.ScratchJr.getBlocks().length === 0
|
|
406
|
+
&& document.querySelectorAll('#scriptscontainer .keyboard-script-strip[role="button"]').length === 0;
|
|
407
|
+
}, { timeout: 30_000 });
|
|
408
|
+
|
|
409
|
+
await tabToSelector(page, '#emptypage', 80);
|
|
410
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#emptypage'));
|
|
411
|
+
await page.keyboard.press('Enter');
|
|
412
|
+
await page.waitForFunction(() => {
|
|
413
|
+
return document.querySelectorAll('#pagecc .pagethumb[role="button"]').length === 3;
|
|
414
|
+
}, { timeout: 30_000 });
|
|
415
|
+
|
|
416
|
+
const initialPageIds = await page.$$eval('#pagecc .pagethumb[data-owner]', (thumbs) => {
|
|
417
|
+
return thumbs.map((thumb) => thumb.getAttribute('data-owner'));
|
|
418
|
+
});
|
|
419
|
+
const firstPageId = initialPageIds[0];
|
|
420
|
+
const secondPageId = initialPageIds[1];
|
|
421
|
+
|
|
422
|
+
expect(await page.$$eval('#pagecc .pagethumb .thumb-action', (buttons) => buttons.length)).toBe(0);
|
|
423
|
+
|
|
424
|
+
const pageOneSpriteCountBefore = await page.evaluate((pageId) => {
|
|
425
|
+
return JSON.parse(document.getElementById(pageId).owner.sprites).length;
|
|
426
|
+
}, firstPageId);
|
|
427
|
+
await page.focus(`.pagethumb[data-owner="${firstPageId}"]`);
|
|
428
|
+
await page.keyboard.press('c');
|
|
429
|
+
await page.waitForFunction((pageId, countBefore) => {
|
|
430
|
+
return JSON.parse(document.getElementById(pageId).owner.sprites).length === countBefore + 1;
|
|
431
|
+
}, { timeout: 30_000 }, firstPageId, pageOneSpriteCountBefore);
|
|
432
|
+
|
|
433
|
+
await page.waitForFunction((pageId) => {
|
|
434
|
+
const target = document.querySelector(`.pagethumb[data-owner="${pageId}"]`);
|
|
435
|
+
if (!target) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
target.focus();
|
|
439
|
+
const activeElement = document.activeElement;
|
|
440
|
+
return activeElement === target;
|
|
441
|
+
}, { timeout: 5_000 }, secondPageId);
|
|
442
|
+
await page.evaluate((pageId) => {
|
|
443
|
+
const target = document.querySelector(`.pagethumb[data-owner="${pageId}"]`);
|
|
444
|
+
target.dispatchEvent(new KeyboardEvent('keydown', {
|
|
445
|
+
key: 'ArrowLeft',
|
|
446
|
+
shiftKey: true,
|
|
447
|
+
bubbles: true,
|
|
448
|
+
cancelable: true
|
|
449
|
+
}));
|
|
450
|
+
}, secondPageId);
|
|
451
|
+
await page.waitForFunction((pageId) => {
|
|
452
|
+
const firstThumb = document.querySelector('#pagecc .pagethumb[data-owner]');
|
|
453
|
+
return firstThumb && firstThumb.getAttribute('data-owner') === pageId;
|
|
454
|
+
}, { timeout: 30_000 }, secondPageId);
|
|
455
|
+
|
|
456
|
+
await page.evaluate(() => {
|
|
457
|
+
const pageThumbs = Array.from(document.querySelectorAll('#pagecc .pagethumb[role="button"]'))
|
|
458
|
+
.filter((thumb) => thumb.id !== 'emptypage');
|
|
459
|
+
pageThumbs[1].focus();
|
|
460
|
+
});
|
|
461
|
+
await page.keyboard.press('Enter');
|
|
462
|
+
await page.waitForFunction((pageId) => {
|
|
463
|
+
const currentThumb = document.querySelector('#pagecc .pagethumb[aria-current="page"]');
|
|
464
|
+
return currentThumb && currentThumb.getAttribute('data-owner') === pageId;
|
|
465
|
+
}, { timeout: 30_000 }, firstPageId);
|
|
466
|
+
|
|
467
|
+
await page.focus(`.pagethumb[data-owner="${secondPageId}"]`);
|
|
468
|
+
await page.keyboard.press('Delete');
|
|
469
|
+
await page.waitForFunction((pageId) => {
|
|
470
|
+
return !document.querySelector(`.pagethumb[data-owner="${pageId}"]`)
|
|
471
|
+
&& document.querySelectorAll('#pagecc .pagethumb[data-owner]').length === 1;
|
|
472
|
+
}, { timeout: 30_000 }, secondPageId);
|
|
473
|
+
|
|
474
|
+
await page.evaluate(() => {
|
|
475
|
+
window.ScratchJr.stage.currentPage.addSprite(0.5, window.ScratchJr.defaultSprite, 'Helper');
|
|
476
|
+
});
|
|
477
|
+
await page.waitForFunction(() => {
|
|
478
|
+
const visibleThumbs = Array.from(document.querySelectorAll('#spritecc .spritethumb[role="button"]'))
|
|
479
|
+
.filter((thumb) => window.getComputedStyle(thumb).display !== 'none');
|
|
480
|
+
return visibleThumbs.length >= 2;
|
|
481
|
+
}, { timeout: 30_000 });
|
|
482
|
+
|
|
483
|
+
const unnamedSpriteThumbs = await page.$$eval('#spritecc .spritethumb[role="button"]', (thumbs) => {
|
|
484
|
+
return thumbs.filter((thumb) => {
|
|
485
|
+
return window.getComputedStyle(thumb).display !== 'none' && !thumb.getAttribute('aria-label');
|
|
486
|
+
}).length;
|
|
487
|
+
});
|
|
488
|
+
expect(unnamedSpriteThumbs).toBe(0);
|
|
489
|
+
|
|
490
|
+
await page.evaluate(() => {
|
|
491
|
+
const visibleThumbs = Array.from(document.querySelectorAll('#spritecc .spritethumb[role="button"]'))
|
|
492
|
+
.filter((thumb) => window.getComputedStyle(thumb).display !== 'none');
|
|
493
|
+
visibleThumbs[1].focus();
|
|
494
|
+
});
|
|
495
|
+
await page.keyboard.press('Enter');
|
|
496
|
+
await page.waitForFunction(() => {
|
|
497
|
+
const visibleThumbs = Array.from(document.querySelectorAll('#spritecc .spritethumb[role="button"]'))
|
|
498
|
+
.filter((thumb) => window.getComputedStyle(thumb).display !== 'none');
|
|
499
|
+
return visibleThumbs.length >= 2
|
|
500
|
+
&& visibleThumbs[1].getAttribute('aria-pressed') === 'true'
|
|
501
|
+
&& visibleThumbs[0].getAttribute('aria-pressed') === 'false';
|
|
502
|
+
}, { timeout: 30_000 });
|
|
503
|
+
|
|
504
|
+
const spriteThumbIds = await page.$$eval('#spritecc .spritethumb[data-owner]', (thumbs) => {
|
|
505
|
+
return thumbs
|
|
506
|
+
.filter((thumb) => window.getComputedStyle(thumb).display !== 'none')
|
|
507
|
+
.map((thumb) => thumb.getAttribute('data-owner'));
|
|
508
|
+
});
|
|
509
|
+
const deletableSpriteId = spriteThumbIds[1];
|
|
510
|
+
await page.focus(`.spritethumb[data-owner="${deletableSpriteId}"]`);
|
|
511
|
+
await page.keyboard.press('Delete');
|
|
512
|
+
await page.waitForFunction((spriteId) => {
|
|
513
|
+
return !document.querySelector(`.spritethumb[data-owner="${spriteId}"]`);
|
|
514
|
+
}, { timeout: 30_000 }, deletableSpriteId);
|
|
515
|
+
|
|
516
|
+
expectNoAxeViolations(await runAxe(page));
|
|
517
|
+
|
|
518
|
+
await page.focus('#projectinfo');
|
|
519
|
+
await page.keyboard.press('Enter');
|
|
520
|
+
await page.waitForSelector('#infobox[role="dialog"]', { timeout: 30_000 });
|
|
521
|
+
expect(await page.$eval('#infobox', (element) => element.getAttribute('aria-modal'))).toBe('true');
|
|
522
|
+
await page.waitForFunction(() => {
|
|
523
|
+
const dialog = document.getElementById('infobox');
|
|
524
|
+
return Boolean(dialog && dialog.contains(document.activeElement));
|
|
525
|
+
}, { timeout: 5_000 });
|
|
526
|
+
|
|
527
|
+
await page.waitForSelector('#infoboxParentsSectionButton', { timeout: 30_000 });
|
|
528
|
+
await page.click('#infoboxParentsSectionButton');
|
|
529
|
+
await page.waitForSelector('#parentalgate[role="dialog"]', { timeout: 30_000 });
|
|
530
|
+
expect(await page.$eval('#parentalgate', (element) => element.getAttribute('aria-modal'))).toBe('true');
|
|
531
|
+
await page.waitForFunction(() => {
|
|
532
|
+
const active = document.activeElement;
|
|
533
|
+
return Boolean(active && active.classList && active.classList.contains('parentalgatechoice'));
|
|
534
|
+
}, { timeout: 5_000 });
|
|
535
|
+
|
|
536
|
+
await page.evaluate(() => {
|
|
537
|
+
const buttons = document.querySelectorAll('#parentalgate .parentalgatechoice');
|
|
538
|
+
buttons[buttons.length - 1].focus();
|
|
539
|
+
});
|
|
540
|
+
await page.keyboard.press('Tab');
|
|
541
|
+
expect(await page.evaluate(() => document.activeElement.classList.contains('paintdone'))).toBe(true);
|
|
542
|
+
|
|
543
|
+
await page.click('#parentalgate .paintdone');
|
|
544
|
+
await page.waitForFunction(() => !document.getElementById('parentalgate'));
|
|
545
|
+
expect(await page.evaluate(() => document.activeElement.id)).toBe('infoboxParentsSectionButton');
|
|
546
|
+
|
|
547
|
+
await page.keyboard.press('Escape');
|
|
548
|
+
await page.waitForFunction(() => {
|
|
549
|
+
const dialog = document.getElementById('infobox');
|
|
550
|
+
return dialog && dialog.className.indexOf(' in') === -1;
|
|
551
|
+
});
|
|
552
|
+
expect(await page.evaluate(() => document.activeElement.id)).toBe('projectinfo');
|
|
553
|
+
|
|
554
|
+
const editorUrl = page.url();
|
|
555
|
+
await page.goto(`${editorUrl}&tutorial=cog-jrblocks-1`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
556
|
+
await page.waitForSelector('#tutorialMenuBar', { timeout: 40_000 });
|
|
557
|
+
await page.waitForSelector('#nextStep', { timeout: 30_000 });
|
|
558
|
+
|
|
559
|
+
await expectNamedControls(page, ['#closeTutorial', '#tutorialReadAloud', '#tutorialHelp', '#previousStep', '#nextStep']);
|
|
560
|
+
await tabToSelector(page, '#closeTutorial');
|
|
561
|
+
expectVisibleFocusIndicator(await getFocusStyles(page, '#closeTutorial'));
|
|
562
|
+
|
|
563
|
+
expectNoAxeViolations(await runAxe(page));
|
|
564
|
+
|
|
565
|
+
await page.focus('#nextStep');
|
|
566
|
+
await page.keyboard.press('Enter');
|
|
567
|
+
await page.waitForSelector('.tutorialImage', { timeout: 30_000 });
|
|
568
|
+
await page.$eval('.tutorialImage', (image) => image.click());
|
|
569
|
+
await page.waitForFunction(() => {
|
|
570
|
+
const modal = document.getElementById('tutorialModal');
|
|
571
|
+
return modal && window.getComputedStyle(modal).display === 'block';
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(await page.$eval('#tutorialModal', (element) => element.getAttribute('role'))).toBe('dialog');
|
|
575
|
+
expect(await page.$eval('#tutorialModal', (element) => element.getAttribute('aria-modal'))).toBe('true');
|
|
576
|
+
await page.waitForFunction(() => {
|
|
577
|
+
const active = document.activeElement;
|
|
578
|
+
return Boolean(active && active.classList && active.classList.contains('closeModal'));
|
|
579
|
+
}, { timeout: 5_000 });
|
|
580
|
+
|
|
581
|
+
await page.keyboard.press('Tab');
|
|
582
|
+
expect(await page.evaluate(() => document.activeElement.classList.contains('closeModal'))).toBe(true);
|
|
583
|
+
|
|
584
|
+
await page.keyboard.press('Escape');
|
|
585
|
+
await page.waitForFunction(() => {
|
|
586
|
+
const modal = document.getElementById('tutorialModal');
|
|
587
|
+
return modal && window.getComputedStyle(modal).display === 'none';
|
|
588
|
+
});
|
|
589
|
+
expect(await page.evaluate(() => document.activeElement.id)).toBe('nextStep');
|
|
590
|
+
|
|
591
|
+
expect(errors).toEqual([]);
|
|
592
|
+
await browser.close();
|
|
593
|
+
},
|
|
594
|
+
150_000
|
|
595
|
+
);
|
|
596
|
+
});
|