mirror of
https://github.com/google/blockly.git
synced 2026-06-11 13:45:14 +02:00
chore: make ci more robust, fix stack overflow and import problem (#9948)
* chore: add additional logging to CI to catch circular deps and exit mocha on failure * chore: fix blockly import * chore: format * chore: increase webdriver timeout to allow longer tests * fix: stack overflow if a sound is missing * chore: dont fail for any console errors * chore: needs more timeout * chore: run mocha in a subprocess * chore: fix chromedriver cache issues * chore: remove bad error condition
This commit is contained in:
@@ -43,6 +43,10 @@ jobs:
|
||||
- name: Npm Clean Install
|
||||
run: npm ci
|
||||
|
||||
- name: Setup Chrome
|
||||
if: runner.os == 'Linux'
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
|
||||
- name: Linux Test Setup
|
||||
if: runner.os == 'Linux'
|
||||
run: source ./tests/scripts/setup_linux_env.sh
|
||||
|
||||
@@ -106,7 +106,10 @@ export class WorkspaceAudio {
|
||||
source.start();
|
||||
} else if (this.parentWorkspace) {
|
||||
// Maybe a workspace on a lower level knows about this sound.
|
||||
this.parentWorkspace.getAudioManager().play(name, opt_volume);
|
||||
const parentAudio = this.parentWorkspace.getAudioManager();
|
||||
if (parentAudio !== this) {
|
||||
parentAudio.play(name, opt_volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
// Unused import preserved for side-effects. Remove if unneeded.
|
||||
import './events/events_click.js';
|
||||
|
||||
import {IFocusableNode} from './blockly.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {ComponentManager} from './component_manager.js';
|
||||
import * as Css from './css.js';
|
||||
import {EventType} from './events/type.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {IPositionable} from './interfaces/i_positionable.js';
|
||||
import type {UiMetrics} from './metrics_manager.js';
|
||||
import {Msg} from './msg.js';
|
||||
|
||||
@@ -195,6 +195,7 @@ export default [
|
||||
'tests/mocha/.mocharc.js',
|
||||
'tests/migration/validate-renamings.mjs',
|
||||
'tests/scripts/magic_symlink.js',
|
||||
'tests/scripts/webdriver_helpers.js',
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
|
||||
@@ -14,14 +14,12 @@ import * as gulp from 'gulp';
|
||||
import gzip from 'gulp-gzip';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {execSync} from 'child_process';
|
||||
import {spawnSync} from 'child_process';
|
||||
import {rimraf} from 'rimraf';
|
||||
|
||||
import {RELEASE_DIR, TEST_TSC_OUTPUT_DIR} from './config.mjs';
|
||||
|
||||
import {runMochaTestsInBrowser} from '../../tests/mocha/webdriver.js';
|
||||
import {runGeneratorsInBrowser} from '../../tests/generators/webdriver.js';
|
||||
import {runCompileCheckInBrowser} from '../../tests/compile/webdriver.js';
|
||||
|
||||
const OUTPUT_DIR = 'build/generators';
|
||||
const GOLDEN_DIR = 'tests/generators/golden';
|
||||
@@ -121,8 +119,20 @@ function reportTestResult() {
|
||||
* @return {Promise} Asynchronous result.
|
||||
*/
|
||||
async function runTestCommand(id, command) {
|
||||
return runTestTask(id, async() => {
|
||||
return execSync(command, {stdio: 'inherit'});
|
||||
return runTestTask(id, async () => {
|
||||
const result = spawnSync(command, {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Command failed with exit code ${result.status}: ${command}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,24 +267,25 @@ async function metadata() {
|
||||
* Run Mocha tests inside a browser.
|
||||
* @return {Promise} Asynchronous result.
|
||||
*/
|
||||
async function mocha(exitOnCompletion = true) {
|
||||
return runTestTask('mocha', async () => {
|
||||
const result = await runMochaTestsInBrowser(exitOnCompletion).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
if (result) {
|
||||
throw new Error('Mocha tests failed');
|
||||
}
|
||||
console.log('Mocha tests passed');
|
||||
});
|
||||
function mocha() {
|
||||
// Run in a subprocess so webdriverio is not loaded inside gulp's asyncDone
|
||||
// domain (which has been observed to exit the process on CI after ~2s).
|
||||
return runTestCommand('mocha', 'node tests/mocha/webdriver.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Mocha tests inside a browser and keep the browser open upon completion.
|
||||
* @return {Promise} Asynchronous result.
|
||||
*/
|
||||
export async function interactiveMocha() {
|
||||
return mocha(false);
|
||||
export function interactiveMocha() {
|
||||
return runTestTask('interactiveMocha', () => {
|
||||
return runMochaTestsInBrowser(false).then((result) => {
|
||||
if (result) {
|
||||
throw new Error('Mocha tests failed');
|
||||
}
|
||||
console.log('Mocha tests passed');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +346,16 @@ export async function generators() {
|
||||
rimraf.sync(OUTPUT_DIR);
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
|
||||
await runGeneratorsInBrowser(OUTPUT_DIR);
|
||||
const result = spawnSync('node', ['tests/generators/webdriver.js', OUTPUT_DIR], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error('Generator browser tests failed.');
|
||||
}
|
||||
|
||||
const generatorSuffixes = ['js', 'py', 'dart', 'lua', 'php'];
|
||||
let failed = 0;
|
||||
@@ -375,7 +395,10 @@ function advancedCompile() {
|
||||
* @return {Promise} Asynchronous result.
|
||||
*/
|
||||
function advancedCompileInBrowser() {
|
||||
return runTestTask('advanced_compile_in_browser', runCompileCheckInBrowser);
|
||||
return runTestCommand(
|
||||
'advanced_compile_in_browser',
|
||||
'node tests/compile/webdriver.js',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,78 +9,49 @@
|
||||
* Chrome, via webdriver.
|
||||
*/
|
||||
const webdriverio = require('webdriverio');
|
||||
|
||||
const {
|
||||
getWebdriverOptions,
|
||||
runBrowserTestMain,
|
||||
} = require('../scripts/webdriver_helpers.js');
|
||||
|
||||
/**
|
||||
* Run the generator for a given language and save the results to a file.
|
||||
* Run the health check in the browser.
|
||||
* @param {Thenable} browser A Thenable managing the processing of the browser
|
||||
* tests.
|
||||
*/
|
||||
async function runHealthCheckInBrowser(browser) {
|
||||
const result = await browser.execute(() => {
|
||||
return healthCheck();
|
||||
})
|
||||
if (!result) throw Error('Health check failed in advanced compilation test.');
|
||||
return healthCheck();
|
||||
});
|
||||
if (!result) {
|
||||
throw Error('Health check failed in advanced compilation test.');
|
||||
}
|
||||
console.log('Health check completed successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the generator tests in Chrome. It uses webdriverio to
|
||||
* launch Chrome and load index.html. Outputs a summary of the test results
|
||||
* to the console and outputs files for later validation.
|
||||
* @return the Thenable managing the processing of the browser tests.
|
||||
* Runs the compile health check in Chrome.
|
||||
* @return {number} 0 on success.
|
||||
*/
|
||||
async function runCompileCheckInBrowser() {
|
||||
const options = {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
},
|
||||
logLevel: 'warn',
|
||||
};
|
||||
// Run in headless mode on Github Actions.
|
||||
if (process.env.CI) {
|
||||
options.capabilities['goog:chromeOptions'] = {
|
||||
args: [
|
||||
'--headless',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--allow-file-access-from-files',
|
||||
]
|
||||
};
|
||||
} else {
|
||||
// --disable-gpu is needed to prevent Chrome from hanging on Linux with
|
||||
// NVIDIA drivers older than v295.20. See
|
||||
// https://github.com/google/blockly/issues/5345 for details.
|
||||
options.capabilities['goog:chromeOptions'] = {
|
||||
args: ['--allow-file-access-from-files', '--disable-gpu']
|
||||
};
|
||||
}
|
||||
const options = getWebdriverOptions();
|
||||
|
||||
const url = 'file://' + __dirname + '/index.html';
|
||||
|
||||
console.log('Starting webdriverio...');
|
||||
const browser = await webdriverio.remote(options);
|
||||
console.log('Loading url: ' + url);
|
||||
await browser.url(url);
|
||||
|
||||
await runHealthCheckInBrowser(browser);
|
||||
|
||||
await browser.deleteSession();
|
||||
try {
|
||||
console.log('Loading url: ' + url);
|
||||
await browser.url(url);
|
||||
await runHealthCheckInBrowser(browser);
|
||||
} finally {
|
||||
await browser.deleteSession();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runCompileCheckInBrowser().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}).then(function(result) {
|
||||
if (result) {
|
||||
console.log('Compile test failed');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Compile test passed');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
runBrowserTestMain(() => runCompileCheckInBrowser());
|
||||
}
|
||||
|
||||
module.exports = {runCompileCheckInBrowser};
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
/**
|
||||
* @fileoverview Node.js script to run generator tests in Chrome, via webdriver.
|
||||
*/
|
||||
var webdriverio = require('webdriverio');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
const webdriverio = require('webdriverio');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
getWebdriverOptions,
|
||||
runBrowserTestMain,
|
||||
} = require('../scripts/webdriver_helpers.js');
|
||||
|
||||
/**
|
||||
* Run the generator for a given language and save the results to a file.
|
||||
@@ -22,8 +25,8 @@ var path = require('path');
|
||||
*/
|
||||
async function runLangGeneratorInBrowser(browser, filename, codegenFn) {
|
||||
await browser.execute(codegenFn);
|
||||
var elem = await browser.$("#importExport");
|
||||
var result = await elem.getValue();
|
||||
const elem = await browser.$('#importExport');
|
||||
const result = await elem.getValue();
|
||||
fs.writeFileSync(filename, result, function(err) {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
@@ -36,88 +39,64 @@ async function runLangGeneratorInBrowser(browser, filename, codegenFn) {
|
||||
* launch Chrome and load index.html. Outputs a summary of the test results
|
||||
* to the console and outputs files for later validation.
|
||||
* @param {string} outputDir Output directory.
|
||||
* @return The Thenable managing the processing of the browser tests.
|
||||
* @return {number} 0 on success.
|
||||
*/
|
||||
async function runGeneratorsInBrowser(outputDir) {
|
||||
var options = {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: ['--allow-file-access-from-files'],
|
||||
},
|
||||
},
|
||||
logLevel: 'warn',
|
||||
};
|
||||
const options = getWebdriverOptions();
|
||||
|
||||
// Run in headless mode on Github Actions.
|
||||
if (process.env.CI) {
|
||||
options.capabilities['goog:chromeOptions'].args.push(
|
||||
'--headless', '--no-sandbox', '--disable-dev-shm-usage',);
|
||||
} else {
|
||||
// --disable-gpu is needed to prevent Chrome from hanging on Linux with
|
||||
// NVIDIA drivers older than v295.20. See
|
||||
// https://github.com/google/blockly/issues/5345 for details.
|
||||
options.capabilities['goog:chromeOptions'].args.push('--disable-gpu');
|
||||
}
|
||||
|
||||
var url = 'file://' + __dirname + '/index.html';
|
||||
var prefix = path.join(outputDir, 'generated');
|
||||
const url = 'file://' + __dirname + '/index.html';
|
||||
const prefix = path.join(outputDir, 'generated');
|
||||
|
||||
console.log('Starting webdriverio...');
|
||||
const browser = await webdriverio.remote(options);
|
||||
|
||||
// Increase the script timeouts to 2 minutes to allow the generators to finish.
|
||||
await browser.setTimeout({ 'script': 120000 })
|
||||
try {
|
||||
// Increase the script timeouts to 2 minutes to allow generators to finish.
|
||||
await browser.setTimeout({'script': 120000});
|
||||
|
||||
console.log('Loading url: ' + url);
|
||||
await browser.url(url);
|
||||
console.log('Loading url: ' + url);
|
||||
await browser.url(url);
|
||||
|
||||
await browser
|
||||
.$('.blocklySvg .blocklyWorkspace > .blocklyBlockCanvas')
|
||||
.waitForExist({timeout: 2000});
|
||||
await browser
|
||||
.$('.blocklySvg .blocklyWorkspace > .blocklyBlockCanvas')
|
||||
.waitForExist({timeout: 2000});
|
||||
|
||||
await browser.execute(function() {
|
||||
checkAll();
|
||||
return loadSelected();
|
||||
});
|
||||
await browser.execute(function() {
|
||||
checkAll();
|
||||
return loadSelected();
|
||||
});
|
||||
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.js',
|
||||
function() {
|
||||
toJavaScript();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.py',
|
||||
function() {
|
||||
toPython();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.dart',
|
||||
function() {
|
||||
toDart();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.lua',
|
||||
function() {
|
||||
toLua();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.php',
|
||||
function() {
|
||||
toPhp();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.js',
|
||||
function() {
|
||||
toJavaScript();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.py',
|
||||
function() {
|
||||
toPython();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.dart',
|
||||
function() {
|
||||
toDart();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.lua',
|
||||
function() {
|
||||
toLua();
|
||||
});
|
||||
await runLangGeneratorInBrowser(browser, prefix + '.php',
|
||||
function() {
|
||||
toPhp();
|
||||
});
|
||||
} finally {
|
||||
await browser.deleteSession();
|
||||
}
|
||||
|
||||
await browser.deleteSession();
|
||||
console.log('Generator browser tests completed.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runGeneratorsInBrowser('tests/generators/tmp').catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}).then(function(result) {
|
||||
if (result) {
|
||||
console.log('Generator tests failed');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Generator tests passed');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
const outputDir = process.argv[2] || 'tests/generators/tmp';
|
||||
runBrowserTestMain(() => runGeneratorsInBrowser(outputDir));
|
||||
}
|
||||
|
||||
module.exports = {runGeneratorsInBrowser};
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<div id="mocha"></div>
|
||||
<div id="failureCount" style="display: none" tests_failed="unset"></div>
|
||||
<div id="failureMessages" style="display: none"></div>
|
||||
<div id="loadStatus" data-status="pending" style="display: none"></div>
|
||||
<div id="testFocusableTree1">
|
||||
Focusable tree 1
|
||||
<div id="testFocusableTree1.node1" style="margin-left: 1em">
|
||||
@@ -149,6 +150,121 @@
|
||||
failZero: true,
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
const failureDiv = document.getElementById('failureCount');
|
||||
const failureMessages = document.getElementById('failureMessages');
|
||||
const loadStatus = document.getElementById('loadStatus');
|
||||
|
||||
/**
|
||||
* Mark the test run as failed before Mocha starts.
|
||||
* @param {string} message Error message to report.
|
||||
*/
|
||||
function reportLoadFailure(message) {
|
||||
const state = window.__blocklyTestLoadState;
|
||||
if (state?.finished || state?.testsStarted) {
|
||||
return;
|
||||
}
|
||||
state.finished = true;
|
||||
failureDiv.setAttribute('tests_failed', '1');
|
||||
loadStatus.setAttribute('data-status', 'failed');
|
||||
const msg = document.createElement('p');
|
||||
msg.textContent = message;
|
||||
failureMessages.appendChild(msg);
|
||||
}
|
||||
|
||||
window.__blocklyTestLoadState = {
|
||||
finished: false,
|
||||
importsComplete: false,
|
||||
testsStarted: false,
|
||||
errors: [],
|
||||
markImportsComplete() {
|
||||
this.importsComplete = true;
|
||||
loadStatus.setAttribute('data-status', 'imports-complete');
|
||||
},
|
||||
markTestsStarted() {
|
||||
this.testsStarted = true;
|
||||
loadStatus.setAttribute('data-status', 'running');
|
||||
},
|
||||
reportFailure(error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
this.errors.push(message);
|
||||
reportLoadFailure('Failed to load Blockly or tests: ' + message);
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
const state = window.__blocklyTestLoadState;
|
||||
if (state.finished || state.testsStarted) {
|
||||
return;
|
||||
}
|
||||
const message = event.message || 'Unknown script error';
|
||||
const loc = event.filename
|
||||
? ' at ' + event.filename + ':' + event.lineno
|
||||
: '';
|
||||
state.errors.push(message + loc);
|
||||
if (
|
||||
message.includes('initialized before') ||
|
||||
message.includes('before initialization') ||
|
||||
message.includes('Cannot access')
|
||||
) {
|
||||
reportLoadFailure(
|
||||
'Blockly module load error (likely circular dependency): ' +
|
||||
message +
|
||||
loc,
|
||||
);
|
||||
} else if (!state.importsComplete) {
|
||||
reportLoadFailure(
|
||||
'Error loading Blockly test modules: ' + message + loc,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const state = window.__blocklyTestLoadState;
|
||||
if (state.finished || state.testsStarted) {
|
||||
return;
|
||||
}
|
||||
const reason =
|
||||
event.reason instanceof Error
|
||||
? event.reason.message
|
||||
: String(event.reason);
|
||||
state.errors.push('Unhandled rejection: ' + reason);
|
||||
reportLoadFailure(
|
||||
'Unhandled rejection while loading Blockly tests: ' + reason + hint,
|
||||
);
|
||||
});
|
||||
|
||||
// Fail fast if modules never finish loading or mocha never starts.
|
||||
setTimeout(() => {
|
||||
if (failureDiv.getAttribute('tests_failed') !== 'unset') {
|
||||
return;
|
||||
}
|
||||
const state = window.__blocklyTestLoadState;
|
||||
if (state.testsStarted) {
|
||||
return;
|
||||
}
|
||||
const status = loadStatus.getAttribute('data-status');
|
||||
let hint;
|
||||
if (status === 'pending') {
|
||||
hint =
|
||||
'The test module script did not finish loading. This is often ' +
|
||||
'caused by a circular dependency in imports (e.g. importing ' +
|
||||
'from "./blockly.js" or "./utils.js" instead of a specific ' +
|
||||
'file). Check the browser console for "initialized before" ' +
|
||||
'errors.';
|
||||
} else {
|
||||
hint =
|
||||
'Tests finished loading (status: ' +
|
||||
status +
|
||||
') but mocha.run() never started.';
|
||||
}
|
||||
const errors = state.errors.join('; ');
|
||||
reportLoadFailure(hint + (errors ? ' Errors: ' + errors : ''));
|
||||
}, 90000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import {loadScript} from '../scripts/load.mjs';
|
||||
@@ -261,25 +377,60 @@
|
||||
import './xml_test.js';
|
||||
import './zoom_controls_test.js';
|
||||
|
||||
// Make Blockly and generators available via global scope.
|
||||
globalThis.Blockly = Blockly;
|
||||
globalThis.javascriptGenerator = javascriptGenerator;
|
||||
window.__blocklyTestLoadState.markImportsComplete();
|
||||
|
||||
// Load additional scripts.
|
||||
await loadScript('../../build/msg/en.js');
|
||||
await loadScript('../playgrounds/screenshot.js');
|
||||
await loadScript('../../node_modules/@blockly/dev-tools/dist/index.js');
|
||||
/**
|
||||
* Verify Blockly loaded correctly. Partial initialization often
|
||||
* indicates a circular dependency in the module graph.
|
||||
* @param {!Object} blockly The Blockly namespace.
|
||||
*/
|
||||
function verifyBlocklyLoaded(blockly) {
|
||||
// Subset of Blockly namespaces that often fail to load correctly
|
||||
// when a circular dependency is present.
|
||||
const required = ['inject', 'FieldDropdown', 'BlockSvg'];
|
||||
const missing = required.filter((name) => !blockly[name]);
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
'Blockly failed to initialize; missing exports: ' +
|
||||
missing.join(', ') +
|
||||
'. This is often caused by a circular dependency (e.g. ' +
|
||||
'importing from "./blockly.js" or "./utils.js" instead of a ' +
|
||||
'specific module). Check the browser console for ' +
|
||||
'"initialized before" errors.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let runner = mocha.run(function (failures) {
|
||||
var failureDiv = document.getElementById('failureCount');
|
||||
failureDiv.setAttribute('tests_failed', failures);
|
||||
});
|
||||
runner.on('fail', function (test, err) {
|
||||
const msg = document.createElement('p');
|
||||
msg.textContent = `"${test.fullTitle()}" failed: ${err.message}`;
|
||||
const div = document.getElementById('failureMessages');
|
||||
div.appendChild(msg);
|
||||
});
|
||||
try {
|
||||
verifyBlocklyLoaded(Blockly);
|
||||
|
||||
// Make Blockly and generators available via global scope.
|
||||
globalThis.Blockly = Blockly;
|
||||
globalThis.javascriptGenerator = javascriptGenerator;
|
||||
|
||||
// Load additional scripts.
|
||||
await loadScript('../../build/msg/en.js');
|
||||
await loadScript('../playgrounds/screenshot.js');
|
||||
await loadScript('../../node_modules/@blockly/dev-tools/dist/index.js');
|
||||
|
||||
window.__blocklyTestLoadState.markTestsStarted();
|
||||
|
||||
let runner = mocha.run(function (failures) {
|
||||
var failureDiv = document.getElementById('failureCount');
|
||||
failureDiv.setAttribute('tests_failed', failures);
|
||||
document
|
||||
.getElementById('loadStatus')
|
||||
.setAttribute('data-status', 'done');
|
||||
});
|
||||
runner.on('fail', function (test, err) {
|
||||
const msg = document.createElement('p');
|
||||
msg.textContent = `"${test.fullTitle()}" failed: ${err.message}`;
|
||||
const div = document.getElementById('failureMessages');
|
||||
div.appendChild(msg);
|
||||
});
|
||||
} catch (error) {
|
||||
window.__blocklyTestLoadState.reportFailure(error);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="blocklyDiv"></div>
|
||||
|
||||
@@ -883,8 +883,6 @@ suite('Keyboard Shortcut Items', function () {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
// Wait for the live region to update after the event.
|
||||
this.clock.runAll();
|
||||
// The announcement may include an additional non-breaking space.
|
||||
console.log(this.liveRegion.textContent);
|
||||
assert.include(this.liveRegion.textContent, expected);
|
||||
};
|
||||
this.liveRegion = document.getElementById('blocklyAriaAnnounce');
|
||||
|
||||
@@ -11,6 +11,16 @@ const webdriverio = require('webdriverio');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {posixPath} = require('../../scripts/helpers');
|
||||
const {
|
||||
getWebdriverOptions,
|
||||
runBrowserTestMain,
|
||||
} = require('../scripts/webdriver_helpers.js');
|
||||
|
||||
/** @const {number} Max time to wait for the browser test page to finish. */
|
||||
const PAGE_TIMEOUT_MS = 120000;
|
||||
|
||||
/** @const {number} Max time to wait for browser.deleteSession(). */
|
||||
const DELETE_SESSION_TIMEOUT_MS = 15000;
|
||||
|
||||
/**
|
||||
* Ensure browser test imports that use ../../node_modules/* continue to work
|
||||
@@ -34,6 +44,130 @@ function ensureWorkspaceNodeModulesLinks() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a promise with a timeout.
|
||||
* @param {!Promise} promise The promise to run.
|
||||
* @param {number} timeoutMs Timeout in milliseconds.
|
||||
* @param {string} label Description for timeout errors.
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function withTimeout(promise, timeoutMs, label) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(label + ' timed out after ' + timeoutMs + 'ms'));
|
||||
}, timeoutMs);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the browser session, with a timeout so CI cannot hang forever.
|
||||
* @param {?Object} browser The webdriverio browser instance.
|
||||
*/
|
||||
async function closeBrowserSession(browser) {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await withTimeout(
|
||||
browser.deleteSession(),
|
||||
DELETE_SESSION_TIMEOUT_MS,
|
||||
'browser.deleteSession()',
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Failed to close browser session:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable focus emulation via CDP. Wrapped in a timeout because this call
|
||||
* has been observed to hang intermittently on CI.
|
||||
* @param {!Object} browser The webdriverio browser instance.
|
||||
*/
|
||||
async function enableFocusEmulation(browser) {
|
||||
const timeoutMs = 10000;
|
||||
try {
|
||||
await withTimeout(
|
||||
(async () => {
|
||||
const puppeteer = await browser.getPuppeteer();
|
||||
await browser.call(async () => {
|
||||
const page = (await puppeteer.pages())[0];
|
||||
const session = await page.createCDPSession();
|
||||
await session.send('Emulation.setFocusEmulationEnabled', {
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
})(),
|
||||
timeoutMs,
|
||||
'Focus emulation setup',
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Focus emulation setup failed (continuing anyway):',
|
||||
e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print browser-side load diagnostics when tests fail to start or finish.
|
||||
* @param {!Object} browser The webdriverio browser instance.
|
||||
*/
|
||||
async function printBrowserLoadDiagnostics(browser) {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await withTimeout((async () => {
|
||||
console.log('============Blockly Mocha Load Diagnostics================');
|
||||
const loadStatus = await browser.$('#loadStatus')
|
||||
.getAttribute('data-status');
|
||||
console.log('Load status:', loadStatus ?? 'unknown');
|
||||
const loadErrors = await browser.execute(() => {
|
||||
return window.__blocklyTestLoadState?.errors ?? [];
|
||||
});
|
||||
if (loadErrors.length) {
|
||||
console.log('Captured load errors:');
|
||||
for (const error of loadErrors) {
|
||||
console.log(' ' + error);
|
||||
}
|
||||
}
|
||||
const failureMessagesEls = await browser.$$('#failureMessages p');
|
||||
for (const el of failureMessagesEls) {
|
||||
const messageHtml = await el.getHTML();
|
||||
console.log(messageHtml.replace(/<\/?p>/g, ''));
|
||||
}
|
||||
if (loadStatus === 'pending' || loadStatus === 'imports-complete') {
|
||||
console.log(
|
||||
'Blockly or test modules may not have loaded completely.');
|
||||
}
|
||||
console.log('==========================================================');
|
||||
})(), 10000, 'Load diagnostics');
|
||||
} catch (diagErr) {
|
||||
console.warn('Could not collect browser diagnostics:', diagErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until Mocha reports results or the page reports a load failure.
|
||||
* @param {!Object} browser The webdriverio browser instance.
|
||||
*/
|
||||
async function waitForTestCompletion(browser) {
|
||||
await browser.waitUntil(async() => {
|
||||
const failureCount = await browser.$('#failureCount')
|
||||
.getAttribute('tests_failed');
|
||||
if (failureCount !== 'unset') {
|
||||
return true;
|
||||
}
|
||||
const loadStatus = await browser.$('#loadStatus')
|
||||
.getAttribute('data-status');
|
||||
return loadStatus === 'failed';
|
||||
}, {
|
||||
timeout: PAGE_TIMEOUT_MS,
|
||||
timeoutMsg: 'Timed out waiting for Mocha tests to finish. Blockly may ' +
|
||||
'have failed to load; see load diagnostics below.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Mocha tests in this directory in Chrome. It uses webdriverio to
|
||||
@@ -45,67 +179,77 @@ function ensureWorkspaceNodeModulesLinks() {
|
||||
* @return {number} 0 on success, 1 on failure.
|
||||
*/
|
||||
async function runMochaTestsInBrowser(exitOnCompletion = true) {
|
||||
// Gulp may pass a done callback as the first argument.
|
||||
if (typeof exitOnCompletion === 'function') {
|
||||
exitOnCompletion = true;
|
||||
}
|
||||
return runMochaTestsInBrowserImpl(exitOnCompletion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} exitOnCompletion True if the browser should quit after tests.
|
||||
* @return {number} 0 on success, 1 on failure.
|
||||
*/
|
||||
async function runMochaTestsInBrowserImpl(exitOnCompletion) {
|
||||
ensureWorkspaceNodeModulesLinks();
|
||||
|
||||
const options = {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: ['--allow-file-access-from-files'],
|
||||
},
|
||||
},
|
||||
logLevel: 'warn',
|
||||
};
|
||||
|
||||
// Run in headless mode on Github Actions.
|
||||
if (process.env.CI) {
|
||||
options.capabilities['goog:chromeOptions'].args.push(
|
||||
'--headless', '--no-sandbox', '--disable-dev-shm-usage',);
|
||||
} else {
|
||||
// --disable-gpu is needed to prevent Chrome from hanging on Linux with
|
||||
// NVIDIA drivers older than v295.20. See
|
||||
// https://github.com/google/blockly/issues/5345 for details.
|
||||
options.capabilities['goog:chromeOptions'].args.push('--disable-gpu');
|
||||
}
|
||||
const options = getWebdriverOptions();
|
||||
|
||||
const url = 'file://' + posixPath(__dirname) + '/index.html';
|
||||
console.log('Starting webdriverio...');
|
||||
const browser = await webdriverio.remote(options);
|
||||
console.log('Loading URL: ' + url);
|
||||
await browser.url(url);
|
||||
let browser;
|
||||
let numOfFailure = '1';
|
||||
let pageLoaded = false;
|
||||
let testsCompleted = false;
|
||||
try {
|
||||
browser = await webdriverio.remote(options);
|
||||
console.log('Loading URL: ' + url);
|
||||
await browser.url(url);
|
||||
pageLoaded = true;
|
||||
|
||||
// Toggle the devtools setting to emulate focus, so that the window will
|
||||
// always act as if it has focus regardless of the state of the window manager
|
||||
// or operating system. This improves the reliability of FocusManager-related
|
||||
// tests.
|
||||
const puppeteer = await browser.getPuppeteer();
|
||||
await browser.call(async () => {
|
||||
const page = (await puppeteer.pages())[0];
|
||||
const session = await page.createCDPSession();
|
||||
await session.send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
||||
});
|
||||
// Focus emulation via CDP has hung on CI; skip there.
|
||||
if (!process.env.CI) {
|
||||
await enableFocusEmulation(browser);
|
||||
}
|
||||
|
||||
await waitForTestCompletion(browser);
|
||||
testsCompleted = true;
|
||||
|
||||
await browser.waitUntil(async() => {
|
||||
const elem = await browser.$('#failureCount');
|
||||
const text = await elem.getAttribute('tests_failed');
|
||||
return text !== 'unset';
|
||||
}, {
|
||||
timeout: 100000,
|
||||
});
|
||||
numOfFailure = await elem.getAttribute('tests_failed');
|
||||
|
||||
const elem = await browser.$('#failureCount');
|
||||
const numOfFailure = await elem.getAttribute('tests_failed');
|
||||
if (numOfFailure > 0) {
|
||||
console.log('============Blockly Mocha Test Failures================');
|
||||
const failureMessagesEls = await browser.$$('#failureMessages p');
|
||||
if (!failureMessagesEls.length) {
|
||||
console.log('There is at least one test failure, but no messages ' +
|
||||
'reported. Mocha may be failing because no tests are being run.');
|
||||
}
|
||||
for (const el of failureMessagesEls) {
|
||||
const messageHtml = await el.getHTML();
|
||||
console.log(messageHtml.replace(/<\/?p>/g, ''));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
await printBrowserLoadDiagnostics(browser);
|
||||
throw e;
|
||||
} finally {
|
||||
if (exitOnCompletion) {
|
||||
await closeBrowserSession(browser);
|
||||
}
|
||||
}
|
||||
|
||||
if (numOfFailure > 0) {
|
||||
console.log('============Blockly Mocha Test Failures================');
|
||||
const failureMessagesEls = await browser.$$('#failureMessages p');
|
||||
if (!failureMessagesEls.length) {
|
||||
console.log('There is at least one test failure, but no messages reported. Mocha may be failing because no tests are being run.');
|
||||
}
|
||||
for (const el of failureMessagesEls) {
|
||||
const messageHtml = await el.getHTML();
|
||||
console.log(messageHtml.replace('<p>', '').replace('</p>', ''));
|
||||
}
|
||||
if (!pageLoaded) {
|
||||
throw new Error(
|
||||
'Mocha browser tests did not start: Chrome failed to load the test ' +
|
||||
'page. Check chromedriver and Chrome setup.',
|
||||
);
|
||||
}
|
||||
if (!testsCompleted) {
|
||||
throw new Error(
|
||||
'Mocha browser tests did not finish: the test page never reported ' +
|
||||
'results.',
|
||||
);
|
||||
}
|
||||
|
||||
console.log('============Blockly Mocha Test Summary=================');
|
||||
@@ -114,23 +258,11 @@ async function runMochaTestsInBrowser(exitOnCompletion = true) {
|
||||
if (parseInt(numOfFailure) !== 0) {
|
||||
return 1;
|
||||
}
|
||||
if (exitOnCompletion) await browser.deleteSession();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runMochaTestsInBrowser().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}).then(function(result) {
|
||||
if (result) {
|
||||
console.log('Mocha tests failed');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Mocha tests passed');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
runBrowserTestMain(() => runMochaTestsInBrowser());
|
||||
}
|
||||
|
||||
module.exports = {runMochaTestsInBrowser};
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Raspberry Pi Foundation
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Shared helpers for browser-based webdriver test runners.
|
||||
*/
|
||||
|
||||
const {execFileSync} = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Resolve the chromedriver binary for CI, preferring a system install.
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
function resolveChromedriverPath() {
|
||||
if (process.env.CHROMEDRIVER_PATH) {
|
||||
return process.env.CHROMEDRIVER_PATH;
|
||||
}
|
||||
try {
|
||||
return execFileSync('which', ['chromedriver'], {encoding: 'utf8'}).trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Chrome/Chromium binary for CI.
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
function resolveChromeBinary() {
|
||||
if (process.env.CHROME_BIN) {
|
||||
return process.env.CHROME_BIN;
|
||||
}
|
||||
if (process.env.CHROME_PATH) {
|
||||
return process.env.CHROME_PATH;
|
||||
}
|
||||
const candidates = [
|
||||
'/usr/bin/google-chrome',
|
||||
'/usr/bin/google-chrome-stable',
|
||||
'/usr/bin/chromium-browser',
|
||||
'/usr/bin/chromium',
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build webdriverio options with CI-friendly Chrome/chromedriver settings.
|
||||
* @param {!Object=} extraCapabilities Additional capability fields to merge in.
|
||||
* @return {!Object}
|
||||
*/
|
||||
function getWebdriverOptions(extraCapabilities = {}) {
|
||||
const chromeArgs = ['--allow-file-access-from-files'];
|
||||
const googChromeOptions = {args: chromeArgs};
|
||||
|
||||
if (process.env.CI) {
|
||||
chromeArgs.push('--headless', '--no-sandbox', '--disable-dev-shm-usage');
|
||||
} else {
|
||||
// --disable-gpu is needed to prevent Chrome from hanging on Linux with
|
||||
// NVIDIA drivers older than v295.20. See
|
||||
// https://github.com/google/blockly/issues/5345 for details.
|
||||
chromeArgs.push('--disable-gpu');
|
||||
}
|
||||
|
||||
const chromeBinary = resolveChromeBinary();
|
||||
if (chromeBinary) {
|
||||
googChromeOptions.binary = chromeBinary;
|
||||
}
|
||||
|
||||
const chromedriverPath = resolveChromedriverPath();
|
||||
const capabilities = {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': googChromeOptions,
|
||||
...extraCapabilities,
|
||||
};
|
||||
if (chromedriverPath) {
|
||||
capabilities['wdio:chromedriverOptions'] = {binary: chromedriverPath};
|
||||
}
|
||||
|
||||
const options = {
|
||||
capabilities,
|
||||
logLevel: 'warn',
|
||||
};
|
||||
|
||||
if (chromedriverPath) {
|
||||
// Use the system chromedriver; do not download into /tmp.
|
||||
process.env.CHROMEDRIVER_PATH = chromedriverPath;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a browser test script as the main module with reliable exit codes.
|
||||
* @param {function(): Promise<number|void>} runTests Function that returns 0 on
|
||||
* success or a non-zero number on test failure. May throw on setup errors.
|
||||
*/
|
||||
function runBrowserTestMain(runTests) {
|
||||
runTests()
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWebdriverOptions,
|
||||
resolveChromeBinary,
|
||||
resolveChromedriverPath,
|
||||
runBrowserTestMain,
|
||||
};
|
||||
Reference in New Issue
Block a user