qt5base-lts/tests/manual/wasm/qtloader_integration/test_body.js
Mikolaj Boc b9491daad0 Modernize the qtloader
This is a minimal version of qtloader. The load function accepts
the same arguments as emscripten runtime with a few additions:
- qt.environment
- qt.onExit
- qt.containerElements
- qt.fontDpi
- qt.onLoaded
- qt.entryFunction

State handling has been removed in favor of making the load async
(assume loading when the promise is live).

Public APIs getting crashed status, exit text and code have been
refactored into the new qt.onExit event fed to load. No need for
keeping the state in the loader.

The loader is integration-tested. A test module with test APIs
has been created as a test harness.

The runtime APIs exposed by Qt (font dpi and screen API) are handled
by the qtloader seamlessly.

Change-Id: Iaee65702667da0349a475feae6b83244d966d98d
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
2023-06-05 23:14:28 +02:00

446 lines
15 KiB
JavaScript

// Copyright (C) 2023 The Qt Company Ltd.
// SPDXLicenseIdentifier: LicenseRefQtCommercial OR GPL3.0only
import { Mock, assert, TestRunner } from './testrunner.js';
export class QtLoaderIntegrationTests
{
#testScreenContainers = []
async beforeEach()
{
this.#addScreenContainer('screen-container-0', { width: '200px', height: '300px' });
}
async afterEach()
{
this.#testScreenContainers.forEach(screenContainer =>
{
document.body.removeChild(screenContainer);
});
this.#testScreenContainers = [];
}
async missingConfig()
{
let caughtException;
try {
await qtLoad();
} catch (e) {
caughtException = e;
}
assert.isNotUndefined(caughtException);
assert.equal('config is required, expected an object', caughtException.message);
}
async missingQtSection()
{
let caughtException;
try {
await qtLoad({});
} catch (e) {
caughtException = e;
}
assert.isNotUndefined(caughtException);
assert.equal(
'config.qt is required, expected an object', caughtException.message);
}
async useDefaultOnMissingEntryFunction()
{
const instance = await qtLoad({ arguments: ['--no-gui'], qt: {}});
assert.isNotUndefined(instance);
}
async environmentVariables()
{
const instance = await qtLoad({
qt: {
environment: {
variable1: 'value1',
variable2: 'value2'
},
entryFunction: createQtAppInstance,
containerElements: [this.#testScreenContainers[0]]
}
});
assert.isTrue(instance.getEnvironmentVariable('variable1') === 'value1');
assert.isTrue(instance.getEnvironmentVariable('variable2') === 'value2');
}
async screenContainerManipulations()
{
// ... (do other things), then call addContainerElement() to add a new container/screen.
// This can happen either before or after load() is called - loader will route the
// call to instance when it's ready.
this.#addScreenContainer('appcontainer1', { width: '100px', height: '100px' })
const instance = await qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: this.#testScreenContainers
}
});
{
const screenInformation = this.#getScreenInformation(instance);
assert.equal(2, screenInformation.length);
assert.equal(200, screenInformation[0].width);
assert.equal(300, screenInformation[0].height);
assert.equal(100, screenInformation[1].width);
assert.equal(100, screenInformation[1].height);
}
this.#addScreenContainer('appcontainer2', { width: '234px', height: '99px' })
instance.qtSetContainerElements(this.#testScreenContainers);
{
const screenInformation = this.#getScreenInformation(instance);
assert.equal(3, screenInformation.length);
assert.equal(200, screenInformation[0].width);
assert.equal(300, screenInformation[0].height);
assert.equal(100, screenInformation[1].width);
assert.equal(100, screenInformation[1].height);
assert.equal(234, screenInformation[2].width);
assert.equal(99, screenInformation[2].height);
}
document.body.removeChild(this.#testScreenContainers.splice(2, 1)[0]);
instance.qtSetContainerElements(this.#testScreenContainers);
{
const screenInformation = this.#getScreenInformation(instance);
assert.equal(2, screenInformation.length);
assert.equal(200, screenInformation[0].width);
assert.equal(300, screenInformation[0].height);
assert.equal(100, screenInformation[1].width);
assert.equal(100, screenInformation[1].height);
}
}
async primaryScreenIsAlwaysFirst()
{
const instance = await qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: this.#testScreenContainers,
}
});
this.#addScreenContainer(
'appcontainer3', { width: '12px', height: '24px' },
container => this.#testScreenContainers.splice(0, 0, container));
this.#addScreenContainer(
'appcontainer4', { width: '34px', height: '68px' },
container => this.#testScreenContainers.splice(1, 0, container));
instance.qtSetContainerElements(this.#testScreenContainers);
{
const screenInformation = this.#getScreenInformation(instance);
assert.equal(3, screenInformation.length);
// The primary screen (at position 0) is always at 0
assert.equal(12, screenInformation[0].width);
assert.equal(24, screenInformation[0].height);
// Other screens are pushed at the back
assert.equal(200, screenInformation[1].width);
assert.equal(300, screenInformation[1].height);
assert.equal(34, screenInformation[2].width);
assert.equal(68, screenInformation[2].height);
}
this.#testScreenContainers.forEach(screenContainer =>
{
document.body.removeChild(screenContainer);
});
this.#testScreenContainers = [
this.#addScreenContainer('appcontainer5', { width: '11px', height: '12px' }),
this.#addScreenContainer('appcontainer6', { width: '13px', height: '14px' }),
];
instance.qtSetContainerElements(this.#testScreenContainers);
{
const screenInformation = this.#getScreenInformation(instance);
assert.equal(2, screenInformation.length);
assert.equal(11, screenInformation[0].width);
assert.equal(12, screenInformation[0].height);
assert.equal(13, screenInformation[1].width);
assert.equal(14, screenInformation[1].height);
}
}
async multipleInstances()
{
// Fetch/Compile the module once; reuse for each instance. This is also if the page wants to
// initiate the .wasm file download fetch as early as possible, before the browser has
// finished fetching and parsing testapp.js and qtloader.js
const modulePromise = WebAssembly.compileStreaming(fetch('tst_qtloader_integration.wasm'));
const instances = await Promise.all([1, 2, 3].map(i => qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: [this.#addScreenContainer(`screen-container-${i}`, {
width: `${i * 10}px`,
height: `${i * 10}px`,
})],
modulePromise,
}
})));
// Confirm the identity of instances by querying their screen widths and heights
{
const screenInformation = this.#getScreenInformation(instances[0]);
console.log();
assert.equal(1, screenInformation.length);
assert.equal(10, screenInformation[0].width);
assert.equal(10, screenInformation[0].height);
}
{
const screenInformation = this.#getScreenInformation(instances[1]);
assert.equal(1, screenInformation.length);
assert.equal(20, screenInformation[0].width);
assert.equal(20, screenInformation[0].height);
}
{
const screenInformation = this.#getScreenInformation(instances[2]);
assert.equal(1, screenInformation.length);
assert.equal(30, screenInformation[0].width);
assert.equal(30, screenInformation[0].height);
}
}
async consoleMode()
{
// 'Console mode' for autotesting type scenarios
let accumulatedStdout = '';
const instance = await qtLoad({
arguments: ['--no-gui'],
print: output =>
{
accumulatedStdout += output;
},
qt: {
entryFunction: createQtAppInstance,
}
});
this.#callTestInstanceApi(instance, 'produceOutput');
assert.equal('Sample output!', accumulatedStdout);
}
async moduleProvided()
{
await qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: [this.#testScreenContainers[0]],
modulePromise: WebAssembly.compileStreaming(
await fetch('tst_qtloader_integration.wasm'))
}
});
}
async arguments()
{
const instance = await qtLoad({
arguments: ['--no-gui', 'arg1', 'other', 'yetanotherarg'],
qt: {
entryFunction: createQtAppInstance,
}
});
const args = this.#callTestInstanceApi(instance, 'retrieveArguments');
assert.equal(5, args.length);
assert.isTrue('arg1' === args[2]);
assert.equal('other', args[3]);
assert.equal('yetanotherarg', args[4]);
}
async moduleProvided_exceptionThrownInFactory()
{
let caughtException;
try {
await qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: [this.#testScreenContainers[0]],
modulePromise: Promise.reject(new Error('Failed to load')),
}
});
} catch (e) {
caughtException = e;
}
assert.isTrue(caughtException !== undefined);
assert.equal('Failed to load', caughtException.message);
}
async abort()
{
const onExitMock = new Mock();
const instance = await qtLoad({
arguments: ['--no-gui'],
qt: {
onExit: onExitMock,
entryFunction: createQtAppInstance,
}
});
try {
instance.crash();
} catch { }
assert.equal(1, onExitMock.calls.length);
const exitStatus = onExitMock.calls[0][0];
assert.isTrue(exitStatus.crashed);
assert.isUndefined(exitStatus.code);
assert.isNotUndefined(exitStatus.text);
}
async abortImmediately()
{
const onExitMock = new Mock();
let caughtException;
try {
await qtLoad({
arguments: ['--no-gui', '--crash-immediately'],
qt: {
onExit: onExitMock,
entryFunction: createQtAppInstance,
}
});
} catch (e) {
caughtException = e;
}
// An exception should have been thrown from load()
assert.equal('RuntimeError', caughtException.name);
assert.equal(1, onExitMock.calls.length);
const exitStatus = onExitMock.calls[0][0];
assert.isTrue(exitStatus.crashed);
assert.isUndefined(exitStatus.code);
assert.isNotUndefined(exitStatus.text);
}
async userAbortCallbackCalled()
{
const onAbortMock = new Mock();
let instance = await qtLoad({
arguments: ['--no-gui'],
onAbort: onAbortMock,
qt: {
entryFunction: createQtAppInstance,
}
});
try {
instance.crash();
} catch (e) {
// emscripten throws an 'Aborted' error here, which we ignore for the sake of the test
}
assert.equal(1, onAbortMock.calls.length);
}
async exit()
{
const onExitMock = new Mock();
let instance = await qtLoad({
arguments: ['--no-gui'],
qt: {
onExit: onExitMock,
entryFunction: createQtAppInstance,
}
});
// The module is running. onExit should not have been called.
assert.equal(0, onExitMock.calls.length);
try {
instance.exitApp();
} catch (e) {
// emscripten throws a 'Runtime error: unreachable' error here. We ignore it for the
// sake of the test.
}
assert.equal(1, onExitMock.calls.length);
const exitStatus = onExitMock.calls[0][0];
assert.isFalse(exitStatus.crashed);
assert.equal(instance.EXIT_VALUE_FROM_EXIT_APP, exitStatus.code);
assert.isNotUndefined(exitStatus.text);
}
async exitImmediately()
{
const onExitMock = new Mock();
const instance = await qtLoad({
arguments: ['--no-gui', '--exit-immediately'],
qt: {
onExit: onExitMock,
entryFunction: createQtAppInstance,
}
});
assert.equal(1, onExitMock.calls.length);
const exitStatusFromOnExit = onExitMock.calls[0][0];
assert.isFalse(exitStatusFromOnExit.crashed);
assert.equal(instance.EXIT_VALUE_IMMEDIATE_RETURN, exitStatusFromOnExit.code);
assert.isNotUndefined(exitStatusFromOnExit.text);
}
async userQuitCallbackCalled()
{
const quitMock = new Mock();
let instance = await qtLoad({
arguments: ['--no-gui'],
quit: quitMock,
qt: {
entryFunction: createQtAppInstance,
}
});
try {
instance.exitApp();
} catch (e) {
// emscripten throws a 'Runtime error: unreachable' error here. We ignore it for the
// sake of the test.
}
assert.equal(1, quitMock.calls.length);
const [exitCode, exception] = quitMock.calls[0];
assert.equal(instance.EXIT_VALUE_FROM_EXIT_APP, exitCode);
assert.equal('ExitStatus', exception.name);
}
#callTestInstanceApi(instance, apiName)
{
return eval(instance[apiName]());
}
#getScreenInformation(instance)
{
return this.#callTestInstanceApi(instance, 'screenInformation').map(elem => ({
x: elem[0],
y: elem[1],
width: elem[2],
height: elem[3],
}));
}
#addScreenContainer(id, style, inserter)
{
const container = (() =>
{
const container = document.createElement('div');
container.id = id;
container.style.width = style.width;
container.style.height = style.height;
document.body.appendChild(container);
return container;
})();
inserter ? inserter(container) : this.#testScreenContainers.push(container);
return container;
}
}
(async () =>
{
const runner = new TestRunner(new QtLoaderIntegrationTests(), {
timeoutSeconds: 10
});
await runner.runAll();
})();