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> (cherry picked from commit b9491daad0ed1c4b9c74e0c3b23f87eb7ad4f37d)
This commit is contained in:
parent
f1fa472c9f
commit
f5e52d209e
@ -1,588 +1,134 @@
|
|||||||
// Copyright (C) 2018 The Qt Company Ltd.
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
// QtLoader provides javascript API for managing Qt application modules.
|
/**
|
||||||
//
|
* Loads the instance of a WASM module.
|
||||||
// QtLoader provides API on top of Emscripten which supports common lifecycle
|
*
|
||||||
// tasks such as displaying placeholder content while the module downloads,
|
* @param config May contain any key normally accepted by emscripten and the 'qt' extra key, with
|
||||||
// handing application exits, and checking for browser wasm support.
|
* the following sub-keys:
|
||||||
//
|
* - environment: { [name:string] : string }
|
||||||
// There are two usage modes:
|
* environment variables set on the instance
|
||||||
// * Managed: QtLoader owns and manages the HTML display elements like
|
* - onExit: (exitStatus: { text: string, code?: number, crashed: bool }) => void
|
||||||
// the loader and canvas.
|
* called when the application has exited for any reason. exitStatus.code is defined in
|
||||||
// * External: The embedding HTML page owns the display elements. QtLoader
|
* case of a normal application exit. This is not called on exit with return code 0, as
|
||||||
// provides event callbacks which the page reacts to.
|
* the program does not shutdown its runtime and technically keeps running async.
|
||||||
//
|
* - containerElements: HTMLDivElement[]
|
||||||
// Managed mode usage:
|
* Array of host elements for Qt screens. Each of these elements is mapped to a QScreen on
|
||||||
//
|
* launch.
|
||||||
// var config = {
|
* - fontDpi: number
|
||||||
// containerElements : [$("container-id")];
|
* Specifies font DPI for the instance
|
||||||
// }
|
* - onLoaded: () => void
|
||||||
// var qtLoader = new QtLoader(config);
|
* Called when the module has loaded.
|
||||||
// qtLoader.loadEmscriptenModule("applicationName");
|
* - entryFunction: (emscriptenConfig: object) => Promise<EmscriptenModule>
|
||||||
//
|
* Qt always uses emscripten's MODULARIZE option. This is the MODULARIZE entry function.
|
||||||
// External mode usage:
|
*
|
||||||
//
|
* @return Promise<{
|
||||||
// var config = {
|
* instance: EmscriptenModule,
|
||||||
// canvasElements : [$("canvas-id")],
|
* exitStatus?: { text: string, code?: number, crashed: bool }
|
||||||
// showLoader: function() {
|
* }>
|
||||||
// loader.style.display = 'block'
|
* The promise is resolved when the module has been instantiated and its main function has been
|
||||||
// canvas.style.display = 'hidden'
|
* called. The returned exitStatus is defined if the application crashed or exited immediately
|
||||||
// },
|
* after its entry function has been called. Otherwise, config.onExit will get called at a
|
||||||
// showCanvas: function() {
|
* later time when (and if) the application exits.
|
||||||
// loader.style.display = 'hidden'
|
*
|
||||||
// canvas.style.display = 'block'
|
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten for
|
||||||
// return canvas;
|
* EmscriptenModule
|
||||||
// }
|
*/
|
||||||
// }
|
async function qtLoad(config)
|
||||||
// var qtLoader = new QtLoader(config);
|
|
||||||
// qtLoader.loadEmscriptenModule("applicationName");
|
|
||||||
//
|
|
||||||
// Config keys
|
|
||||||
//
|
|
||||||
// moduleConfig : {}
|
|
||||||
// Emscripten module configuration
|
|
||||||
// containerElements : [container-element, ...]
|
|
||||||
// One or more HTML elements. QtLoader will display loader elements
|
|
||||||
// on these while loading the application, and replace the loader with a
|
|
||||||
// canvas on load complete.
|
|
||||||
// canvasElements : [canvas-element, ...]
|
|
||||||
// One or more canvas elements.
|
|
||||||
// showLoader : function(status, containerElement)
|
|
||||||
// Optional loading element constructor function. Implement to create
|
|
||||||
// a custom loading screen. This function may be called multiple times,
|
|
||||||
// while preparing the application binary. "status" is a string
|
|
||||||
// containing the loading sub-status, and may be either "Downloading",
|
|
||||||
// or "Compiling". The browser may be using streaming compilation, in
|
|
||||||
// which case the wasm module is compiled during downloading and the
|
|
||||||
// there is no separate compile step.
|
|
||||||
// showCanvas : function(containerElement)
|
|
||||||
// Optional canvas constructor function. Implement to create custom
|
|
||||||
// canvas elements.
|
|
||||||
// showExit : function(crashed, exitCode, containerElement)
|
|
||||||
// Optional exited element constructor function.
|
|
||||||
// showError : function(crashed, exitCode, containerElement)
|
|
||||||
// Optional error element constructor function.
|
|
||||||
// statusChanged : function(newStatus)
|
|
||||||
// Optional callback called when the status of the app has changed
|
|
||||||
//
|
|
||||||
// path : <string>
|
|
||||||
// Prefix path for wasm file, realative to the loading HMTL file.
|
|
||||||
// restartMode : "DoNotRestart", "RestartOnExit", "RestartOnCrash"
|
|
||||||
// Controls whether the application should be reloaded on exits. The default is "DoNotRestart"
|
|
||||||
// restartType : "RestartModule", "ReloadPage"
|
|
||||||
// restartLimit : <int>
|
|
||||||
// Restart attempts limit. The default is 10.
|
|
||||||
// stdoutEnabled : <bool>
|
|
||||||
// stderrEnabled : <bool>
|
|
||||||
// environment : <object>
|
|
||||||
// key-value environment variable pairs.
|
|
||||||
//
|
|
||||||
// QtLoader object API
|
|
||||||
//
|
|
||||||
// webAssemblySupported : bool
|
|
||||||
// webGLSupported : bool
|
|
||||||
// canLoadQt : bool
|
|
||||||
// Reports if WebAssembly and WebGL are supported. These are requirements for
|
|
||||||
// running Qt applications.
|
|
||||||
// loadEmscriptenModule(applicationName)
|
|
||||||
// Loads the application from the given emscripten javascript module file and wasm file
|
|
||||||
// status
|
|
||||||
// One of "Created", "Loading", "Running", "Exited".
|
|
||||||
// crashed
|
|
||||||
// Set to true if there was an unclean exit.
|
|
||||||
// exitCode
|
|
||||||
// main()/emscripten_force_exit() return code. Valid on status change to
|
|
||||||
// "Exited", iff crashed is false.
|
|
||||||
// exitText
|
|
||||||
// Abort/exit message.
|
|
||||||
// addCanvasElement
|
|
||||||
// Add canvas at run-time. Adds a corresponding QScreen,
|
|
||||||
// removeCanvasElement
|
|
||||||
// Remove canvas at run-time. Removes the corresponding QScreen.
|
|
||||||
// resizeCanvasElement
|
|
||||||
// Signals to the application that a canvas has been resized.
|
|
||||||
// setFontDpi
|
|
||||||
// Sets the logical font dpi for the application.
|
|
||||||
// module
|
|
||||||
// Returns the Emscripten module object, or undefined if the module
|
|
||||||
// has not been created yet. Note that the module object becomes available
|
|
||||||
// at the very end of the loading sequence, _after_ the transition from
|
|
||||||
// Loading to Running occurs.
|
|
||||||
|
|
||||||
|
|
||||||
// Forces the use of constructor on QtLoader instance.
|
|
||||||
// This passthrough makes both the old-style:
|
|
||||||
//
|
|
||||||
// const loader = QtLoader(config);
|
|
||||||
//
|
|
||||||
// and the new-style:
|
|
||||||
//
|
|
||||||
// const loader = new QtLoader(config);
|
|
||||||
//
|
|
||||||
// instantiation types work.
|
|
||||||
function QtLoader(config)
|
|
||||||
{
|
{
|
||||||
return new _QtLoader(config);
|
const throwIfEnvUsedButNotExported = (instance, config) =>
|
||||||
}
|
{
|
||||||
|
const environment = config.environment;
|
||||||
|
if (!environment || Object.keys(environment).length === 0)
|
||||||
|
return;
|
||||||
|
const isEnvExported = typeof instance.ENV === 'object';
|
||||||
|
if (!isEnvExported)
|
||||||
|
throw new Error('ENV must be exported if environment variables are passed');
|
||||||
|
};
|
||||||
|
|
||||||
function _QtLoader(config)
|
if (typeof config !== 'object')
|
||||||
{
|
throw new Error('config is required, expected an object');
|
||||||
const self = this;
|
if (typeof config.qt !== 'object')
|
||||||
|
throw new Error('config.qt is required, expected an object');
|
||||||
|
if (typeof config.qt.entryFunction !== 'function')
|
||||||
|
config.qt.entryFunction = window.createQtAppInstance;
|
||||||
|
|
||||||
// The Emscripten module and module configuration object. The module
|
config.qtContainerElements = config.qt.containerElements;
|
||||||
// object is created in completeLoadEmscriptenModule().
|
delete config.qt.containerElements;
|
||||||
self.module = undefined;
|
config.qtFontDpi = config.qt.fontDpi;
|
||||||
self.moduleConfig = config.moduleConfig || {};
|
delete config.qt.fontDpi;
|
||||||
|
|
||||||
// Qt properties. These are propagated to the Emscripten module after
|
// Used for rejecting a failed load's promise where emscripten itself does not allow it,
|
||||||
// it has been created.
|
// like in instantiateWasm below. This allows us to throw in case of a load error instead of
|
||||||
self.qtContainerElements = undefined;
|
// hanging on a promise to entry function, which emscripten unfortunately does.
|
||||||
self.qtFontDpi = 96;
|
let circuitBreakerReject;
|
||||||
|
const circuitBreaker = new Promise((_, reject) => { circuitBreakerReject = reject; });
|
||||||
|
|
||||||
function webAssemblySupported() {
|
// If module async getter is present, use it so that module reuse is possible.
|
||||||
return typeof WebAssembly !== "undefined"
|
if (config.qt.modulePromise) {
|
||||||
}
|
config.instantiateWasm = async (imports, successCallback) =>
|
||||||
|
{
|
||||||
function webGLSupported() {
|
try {
|
||||||
// We expect that WebGL is supported if WebAssembly is; however
|
const module = await config.qt.modulePromise;
|
||||||
// the GPU may be blacklisted.
|
successCallback(
|
||||||
try {
|
await WebAssembly.instantiate(module, imports), module);
|
||||||
var canvas = document.createElement("canvas");
|
} catch (e) {
|
||||||
return !!(window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")));
|
circuitBreakerReject(e);
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function canLoadQt() {
|
|
||||||
// The current Qt implementation requires WebAssembly (asm.js is not in use),
|
|
||||||
// and also WebGL (there is no raster fallback).
|
|
||||||
return webAssemblySupported() && webGLSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeChildren(element) {
|
|
||||||
while (element.firstChild) element.removeChild(element.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default state handler functions
|
|
||||||
if (config.containerElements !== undefined) {
|
|
||||||
config.showError = config.showError || function(errorText, container) {
|
|
||||||
removeChildren(container);
|
|
||||||
var errorTextElement = document.createElement("text");
|
|
||||||
errorTextElement.className = "QtError"
|
|
||||||
errorTextElement.innerHTML = errorText;
|
|
||||||
return errorTextElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.showLoader = config.showLoader || function(loadingState, container) {
|
|
||||||
removeChildren(container);
|
|
||||||
var loadingText = document.createElement("text");
|
|
||||||
loadingText.className = "QtLoading"
|
|
||||||
loadingText.innerHTML = "<p><center>" + loadingState + "</center><p>";
|
|
||||||
return loadingText;
|
|
||||||
};
|
|
||||||
|
|
||||||
config.showCanvas = config.showCanvas || function(canvas, container) {
|
|
||||||
removeChildren(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.showExit = config.showExit || function(crashed, exitCode, container) {
|
|
||||||
if (!crashed)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
removeChildren(container);
|
|
||||||
var fontSize = 54;
|
|
||||||
var crashSymbols = ["\u{1F615}", "\u{1F614}", "\u{1F644}", "\u{1F928}", "\u{1F62C}",
|
|
||||||
"\u{1F915}", "\u{2639}", "\u{1F62E}", "\u{1F61E}", "\u{1F633}"];
|
|
||||||
var symbolIndex = Math.floor(Math.random() * crashSymbols.length);
|
|
||||||
var errorHtml = `<font size='${fontSize}'> ${crashSymbols[symbolIndex]} </font>`
|
|
||||||
var errorElement = document.createElement("text");
|
|
||||||
errorElement.className = "QtExit"
|
|
||||||
errorElement.innerHTML = errorHtml;
|
|
||||||
return errorElement;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.containerElements = config.canvasElements
|
|
||||||
}
|
|
||||||
|
|
||||||
config.restartMode = config.restartMode || "DoNotRestart";
|
|
||||||
config.restartLimit = config.restartLimit || 10;
|
|
||||||
|
|
||||||
if (config.stdoutEnabled === undefined) config.stdoutEnabled = true;
|
|
||||||
if (config.stderrEnabled === undefined) config.stderrEnabled = true;
|
|
||||||
|
|
||||||
// Make sure config.path is defined and ends with "/" if needed
|
|
||||||
if (config.path === undefined)
|
|
||||||
config.path = "";
|
|
||||||
if (config.path.length > 0 && !config.path.endsWith("/"))
|
|
||||||
config.path = config.path.concat("/");
|
|
||||||
|
|
||||||
if (config.environment === undefined)
|
|
||||||
config.environment = {};
|
|
||||||
|
|
||||||
var publicAPI = {};
|
|
||||||
publicAPI.webAssemblySupported = webAssemblySupported();
|
|
||||||
publicAPI.webGLSupported = webGLSupported();
|
|
||||||
publicAPI.canLoadQt = canLoadQt();
|
|
||||||
publicAPI.canLoadApplication = canLoadQt();
|
|
||||||
publicAPI.status = undefined;
|
|
||||||
publicAPI.loadEmscriptenModule = loadEmscriptenModule;
|
|
||||||
publicAPI.addCanvasElement = addCanvasElement;
|
|
||||||
publicAPI.removeCanvasElement = removeCanvasElement;
|
|
||||||
publicAPI.resizeCanvasElement = resizeCanvasElement;
|
|
||||||
publicAPI.setFontDpi = setFontDpi;
|
|
||||||
publicAPI.fontDpi = fontDpi;
|
|
||||||
publicAPI.module = module;
|
|
||||||
|
|
||||||
self.restartCount = 0;
|
|
||||||
|
|
||||||
function handleError(error) {
|
|
||||||
self.error = error;
|
|
||||||
setStatus("Error");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchResource(filePath) {
|
|
||||||
var fullPath = config.path + filePath;
|
|
||||||
return fetch(fullPath).then(function(response) {
|
|
||||||
if (!response.ok) {
|
|
||||||
let err = response.status + " " + response.statusText + " " + response.url;
|
|
||||||
handleError(err);
|
|
||||||
return Promise.reject(err)
|
|
||||||
} else {
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchText(filePath) {
|
|
||||||
return fetchResource(filePath).then(function(response) {
|
|
||||||
return response.text();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchThenCompileWasm(response) {
|
|
||||||
return response.arrayBuffer().then(function(data) {
|
|
||||||
self.loaderSubState = "Compiling";
|
|
||||||
setStatus("Loading") // trigger loaderSubState update
|
|
||||||
return WebAssembly.compile(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchCompileWasm(filePath) {
|
|
||||||
return fetchResource(filePath).then(function(response) {
|
|
||||||
if (typeof WebAssembly.compileStreaming !== "undefined") {
|
|
||||||
self.loaderSubState = "Downloading/Compiling";
|
|
||||||
setStatus("Loading");
|
|
||||||
return WebAssembly.compileStreaming(response).catch(function(error) {
|
|
||||||
// compileStreaming may/will fail if the server does not set the correct
|
|
||||||
// mime type (application/wasm) for the wasm file. Fall back to fetch,
|
|
||||||
// then compile in this case.
|
|
||||||
return fetchThenCompileWasm(response);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fall back to fetch, then compile if compileStreaming is not supported
|
|
||||||
return fetchThenCompileWasm(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadEmscriptenModule(applicationName) {
|
|
||||||
|
|
||||||
// Loading in qtloader.js goes through four steps:
|
|
||||||
// 1) Check prerequisites
|
|
||||||
// 2) Download resources
|
|
||||||
// 3) Configure the emscripten Module object
|
|
||||||
// 4) Start the emcripten runtime, after which emscripten takes over
|
|
||||||
|
|
||||||
// Check for Wasm & WebGL support; set error and return before downloading resources if missing
|
|
||||||
if (!webAssemblySupported()) {
|
|
||||||
handleError("Error: WebAssembly is not supported");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!webGLSupported()) {
|
|
||||||
handleError("Error: WebGL is not supported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue waiting if loadEmscriptenModule() is called again
|
|
||||||
if (publicAPI.status == "Loading")
|
|
||||||
return;
|
|
||||||
self.loaderSubState = "Downloading";
|
|
||||||
setStatus("Loading");
|
|
||||||
|
|
||||||
// Fetch emscripten generated javascript runtime
|
|
||||||
var emscriptenModuleSource = undefined
|
|
||||||
var emscriptenModuleSourcePromise = fetchText(applicationName + ".js").then(function(source) {
|
|
||||||
emscriptenModuleSource = source
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch and compile wasm module
|
|
||||||
var wasmModule = undefined;
|
|
||||||
var wasmModulePromise = fetchCompileWasm(applicationName + ".wasm").then(function (module) {
|
|
||||||
wasmModule = module;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for all resources ready
|
|
||||||
Promise.all([emscriptenModuleSourcePromise, wasmModulePromise]).then(function(){
|
|
||||||
completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule);
|
|
||||||
}).catch(function(error) {
|
|
||||||
handleError(error);
|
|
||||||
// An error here is fatal, abort
|
|
||||||
self.moduleConfig.onAbort(error)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule) {
|
const originalPreRun = config.preRun;
|
||||||
|
config.preRun = instance =>
|
||||||
|
{
|
||||||
|
originalPreRun?.();
|
||||||
|
|
||||||
// The wasm binary has been compiled into a module during resource download,
|
throwIfEnvUsedButNotExported(instance, config);
|
||||||
// and is ready to be instantiated. Define the instantiateWasm callback which
|
for (const [name, value] of Object.entries(config.qt.environment ?? {}))
|
||||||
// emscripten will call to create the instance.
|
instance.ENV[name] = value;
|
||||||
self.moduleConfig.instantiateWasm = function(imports, successCallback) {
|
};
|
||||||
WebAssembly.instantiate(wasmModule, imports).then(function(instance) {
|
|
||||||
successCallback(instance, wasmModule);
|
config.onRuntimeInitialized = () => config.qt.onLoaded?.();
|
||||||
}, function(error) {
|
|
||||||
handleError(error)
|
// This is needed for errors which occur right after resolving the instance promise but
|
||||||
|
// before exiting the function (i.e. on call to main before stack unwinding).
|
||||||
|
let loadTimeException = undefined;
|
||||||
|
// We don't want to issue onExit when aborted
|
||||||
|
let aborted = false;
|
||||||
|
const originalQuit = config.quit;
|
||||||
|
config.quit = (code, exception) =>
|
||||||
|
{
|
||||||
|
originalQuit?.(code, exception);
|
||||||
|
|
||||||
|
if (exception)
|
||||||
|
loadTimeException = exception;
|
||||||
|
if (!aborted && code !== 0) {
|
||||||
|
config.qt.onExit?.({
|
||||||
|
text: exception.message,
|
||||||
|
code,
|
||||||
|
crashed: false
|
||||||
});
|
});
|
||||||
return {};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.moduleConfig.locateFile = self.moduleConfig.locateFile || function(filename) {
|
const originalOnAbort = config.onAbort;
|
||||||
return config.path + filename;
|
config.onAbort = text =>
|
||||||
};
|
{
|
||||||
|
originalOnAbort?.();
|
||||||
|
|
||||||
// Attach status callbacks
|
aborted = true;
|
||||||
self.moduleConfig.setStatus = self.moduleConfig.setStatus || function(text) {
|
config.qt.onExit?.({
|
||||||
// Currently the only usable status update from this function
|
text,
|
||||||
// is "Running..."
|
crashed: true
|
||||||
if (text.startsWith("Running"))
|
|
||||||
setStatus("Running");
|
|
||||||
};
|
|
||||||
self.moduleConfig.monitorRunDependencies = self.moduleConfig.monitorRunDependencies || function(left) {
|
|
||||||
// console.log("monitorRunDependencies " + left)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach standard out/err callbacks.
|
|
||||||
self.moduleConfig.print = self.moduleConfig.print || function(text) {
|
|
||||||
if (config.stdoutEnabled)
|
|
||||||
console.log(text)
|
|
||||||
};
|
|
||||||
self.moduleConfig.printErr = self.moduleConfig.printErr || function(text) {
|
|
||||||
if (config.stderrEnabled)
|
|
||||||
console.warn(text)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Error handling: set status to "Exited", update crashed and
|
|
||||||
// exitCode according to exit type.
|
|
||||||
// Emscripten will typically call printErr with the error text
|
|
||||||
// as well. Note that emscripten may also throw exceptions from
|
|
||||||
// async callbacks. These should be handled in window.onerror by user code.
|
|
||||||
self.moduleConfig.onAbort = self.moduleConfig.onAbort || function(text) {
|
|
||||||
publicAPI.crashed = true;
|
|
||||||
publicAPI.exitText = text;
|
|
||||||
setStatus("Exited");
|
|
||||||
};
|
|
||||||
self.moduleConfig.quit = self.moduleConfig.quit || function(code, exception) {
|
|
||||||
|
|
||||||
// Emscripten (and Qt) supports exiting from main() while keeping the app
|
|
||||||
// running. Don't transition into the "Exited" state for clean exits.
|
|
||||||
if (code == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (exception.name == "ExitStatus") {
|
|
||||||
// Clean exit with code
|
|
||||||
publicAPI.exitText = undefined
|
|
||||||
publicAPI.exitCode = code;
|
|
||||||
} else {
|
|
||||||
publicAPI.exitText = exception.toString();
|
|
||||||
publicAPI.crashed = true;
|
|
||||||
// Print stack trace to console
|
|
||||||
console.log(exception);
|
|
||||||
}
|
|
||||||
setStatus("Exited");
|
|
||||||
};
|
|
||||||
|
|
||||||
self.moduleConfig.preRun = self.moduleConfig.preRun || []
|
|
||||||
self.moduleConfig.preRun.push(function(module) {
|
|
||||||
// Set environment variables
|
|
||||||
for (var [key, value] of Object.entries(config.environment)) {
|
|
||||||
module.ENV[key.toUpperCase()] = value;
|
|
||||||
}
|
|
||||||
// Propagate Qt module properties
|
|
||||||
module.qtContainerElements = self.qtContainerElements;
|
|
||||||
module.qtFontDpi = self.qtFontDpi;
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
self.moduleConfig.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'});
|
// Call app/emscripten module entry function. It may either come from the emscripten
|
||||||
|
// runtime script or be customized as needed.
|
||||||
|
const instance = await Promise.race(
|
||||||
|
[circuitBreaker, config.qt.entryFunction(config)]);
|
||||||
|
if (loadTimeException && loadTimeException.name !== 'ExitStatus')
|
||||||
|
throw loadTimeException;
|
||||||
|
|
||||||
self.qtContainerElements = config.containerElements;
|
return instance;
|
||||||
|
|
||||||
config.restart = function() {
|
|
||||||
|
|
||||||
// Restart by reloading the page. This will wipe all state which means
|
|
||||||
// reload loops can't be prevented.
|
|
||||||
if (config.restartType == "ReloadPage") {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart by readling the emscripten app module.
|
|
||||||
++self.restartCount;
|
|
||||||
if (self.restartCount > config.restartLimit) {
|
|
||||||
handleError("Error: This application has crashed too many times and has been disabled. Reload the page to try again.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadEmscriptenModule(applicationName);
|
|
||||||
};
|
|
||||||
|
|
||||||
publicAPI.exitCode = undefined;
|
|
||||||
publicAPI.exitText = undefined;
|
|
||||||
publicAPI.crashed = false;
|
|
||||||
|
|
||||||
// Load the Emscripten application module. This is done by eval()'ing the
|
|
||||||
// javascript runtime generated by Emscripten, and then calling
|
|
||||||
// createQtAppInstance(), which was added to the global scope.
|
|
||||||
eval(emscriptenModuleSource);
|
|
||||||
createQtAppInstance(self.moduleConfig).then(function(module) {
|
|
||||||
self.module = module;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setErrorContent() {
|
|
||||||
if (config.containerElements === undefined) {
|
|
||||||
if (config.showError !== undefined)
|
|
||||||
config.showError(self.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (container of config.containerElements) {
|
|
||||||
var errorElement = config.showError(self.error, container);
|
|
||||||
container.appendChild(errorElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLoaderContent() {
|
|
||||||
if (config.containerElements === undefined) {
|
|
||||||
if (config.showLoader !== undefined)
|
|
||||||
config.showLoader(self.loaderSubState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (container of config.containerElements) {
|
|
||||||
var loaderElement = config.showLoader(self.loaderSubState, container);
|
|
||||||
if (loaderElement !== undefined)
|
|
||||||
container.appendChild(loaderElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCanvasContent() {
|
|
||||||
if (config.containerElements === undefined) {
|
|
||||||
if (config.showCanvas !== undefined)
|
|
||||||
config.showCanvas();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < config.containerElements.length; ++i) {
|
|
||||||
var container = config.containerElements[i];
|
|
||||||
var canvas = undefined;
|
|
||||||
if (config.canvasElements !== undefined)
|
|
||||||
canvas = config.canvasElements[i];
|
|
||||||
config.showCanvas(canvas, container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setExitContent() {
|
|
||||||
|
|
||||||
// publicAPI.crashed = true;
|
|
||||||
|
|
||||||
if (publicAPI.status != "Exited")
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (config.containerElements === undefined) {
|
|
||||||
if (config.showExit !== undefined)
|
|
||||||
config.showExit(publicAPI.crashed, publicAPI.exitCode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!publicAPI.crashed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (container of config.containerElements) {
|
|
||||||
var loaderElement = config.showExit(publicAPI.crashed, publicAPI.exitCode, container);
|
|
||||||
if (loaderElement !== undefined)
|
|
||||||
container.appendChild(loaderElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var committedStatus = undefined;
|
|
||||||
function handleStatusChange() {
|
|
||||||
if (publicAPI.status != "Loading" && committedStatus == publicAPI.status)
|
|
||||||
return;
|
|
||||||
committedStatus = publicAPI.status;
|
|
||||||
|
|
||||||
if (publicAPI.status == "Error") {
|
|
||||||
setErrorContent();
|
|
||||||
} else if (publicAPI.status == "Loading") {
|
|
||||||
setLoaderContent();
|
|
||||||
} else if (publicAPI.status == "Running") {
|
|
||||||
setCanvasContent();
|
|
||||||
} else if (publicAPI.status == "Exited") {
|
|
||||||
if (config.restartMode == "RestartOnExit" ||
|
|
||||||
config.restartMode == "RestartOnCrash" && publicAPI.crashed) {
|
|
||||||
committedStatus = undefined;
|
|
||||||
config.restart();
|
|
||||||
} else {
|
|
||||||
setExitContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send status change notification
|
|
||||||
if (config.statusChanged)
|
|
||||||
config.statusChanged(publicAPI.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(status) {
|
|
||||||
if (status != "Loading" && publicAPI.status == status)
|
|
||||||
return;
|
|
||||||
publicAPI.status = status;
|
|
||||||
|
|
||||||
window.setTimeout(function() { handleStatusChange(); }, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCanvasElement(element) {
|
|
||||||
if (publicAPI.status == "Running")
|
|
||||||
self.module.qtAddContainerElement(element);
|
|
||||||
else
|
|
||||||
console.log("Error: addCanvasElement can only be called in the Running state");
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeCanvasElement(element) {
|
|
||||||
if (publicAPI.status == "Running")
|
|
||||||
self.module.qtRemoveContainerElement(element);
|
|
||||||
else
|
|
||||||
console.log("Error: removeCanvasElement can only be called in the Running state");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeCanvasElement(element) {
|
|
||||||
if (publicAPI.status == "Running")
|
|
||||||
self.module.qtResizeContainerElement(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFontDpi(dpi) {
|
|
||||||
self.qtFontDpi = dpi;
|
|
||||||
if (publicAPI.status == "Running")
|
|
||||||
self.module.qtUpdateDpi();
|
|
||||||
}
|
|
||||||
|
|
||||||
function fontDpi() {
|
|
||||||
return self.qtFontDpi;
|
|
||||||
}
|
|
||||||
|
|
||||||
function module() {
|
|
||||||
return self.module;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus("Created");
|
|
||||||
|
|
||||||
return publicAPI;
|
|
||||||
}
|
}
|
||||||
|
@ -38,14 +38,19 @@ using namespace emscripten;
|
|||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
static void setContainerElements(emscripten::val elementArray)
|
||||||
|
{
|
||||||
|
QWasmIntegration::get()->setContainerElements(elementArray);
|
||||||
|
}
|
||||||
|
|
||||||
static void addContainerElement(emscripten::val element)
|
static void addContainerElement(emscripten::val element)
|
||||||
{
|
{
|
||||||
QWasmIntegration::get()->addScreen(element);
|
QWasmIntegration::get()->addContainerElement(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void removeContainerElement(emscripten::val element)
|
static void removeContainerElement(emscripten::val element)
|
||||||
{
|
{
|
||||||
QWasmIntegration::get()->removeScreen(element);
|
QWasmIntegration::get()->removeContainerElement(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void resizeContainerElement(emscripten::val element)
|
static void resizeContainerElement(emscripten::val element)
|
||||||
@ -66,6 +71,7 @@ static void resizeAllScreens(emscripten::val event)
|
|||||||
|
|
||||||
EMSCRIPTEN_BINDINGS(qtQWasmIntegraton)
|
EMSCRIPTEN_BINDINGS(qtQWasmIntegraton)
|
||||||
{
|
{
|
||||||
|
function("qtSetContainerElements", &setContainerElements);
|
||||||
function("qtAddContainerElement", &addContainerElement);
|
function("qtAddContainerElement", &addContainerElement);
|
||||||
function("qtRemoveContainerElement", &removeContainerElement);
|
function("qtRemoveContainerElement", &removeContainerElement);
|
||||||
function("qtResizeContainerElement", &resizeContainerElement);
|
function("qtResizeContainerElement", &resizeContainerElement);
|
||||||
@ -94,6 +100,7 @@ QWasmIntegration::QWasmIntegration()
|
|||||||
// div element. Qt historically supported supplying canvas for screen elements - these elements
|
// div element. Qt historically supported supplying canvas for screen elements - these elements
|
||||||
// will be transformed into divs and warnings about deprecation will be printed. See
|
// will be transformed into divs and warnings about deprecation will be printed. See
|
||||||
// QWasmScreen ctor.
|
// QWasmScreen ctor.
|
||||||
|
emscripten::val filtered = emscripten::val::array();
|
||||||
emscripten::val qtContainerElements = val::module_property("qtContainerElements");
|
emscripten::val qtContainerElements = val::module_property("qtContainerElements");
|
||||||
if (qtContainerElements.isArray()) {
|
if (qtContainerElements.isArray()) {
|
||||||
for (int i = 0; i < qtContainerElements["length"].as<int>(); ++i) {
|
for (int i = 0; i < qtContainerElements["length"].as<int>(); ++i) {
|
||||||
@ -101,13 +108,14 @@ QWasmIntegration::QWasmIntegration()
|
|||||||
if (element.isNull() || element.isUndefined())
|
if (element.isNull() || element.isUndefined())
|
||||||
qWarning() << "Skipping null or undefined element in qtContainerElements";
|
qWarning() << "Skipping null or undefined element in qtContainerElements";
|
||||||
else
|
else
|
||||||
addScreen(element);
|
filtered.call<void>("push", element);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No screens, which may or may not be intended
|
// No screens, which may or may not be intended
|
||||||
qWarning() << "The qtContainerElements module property was not set or is invalid. "
|
qWarning() << "The qtContainerElements module property was not set or is invalid. "
|
||||||
"Proceeding with no screens.";
|
"Proceeding with no screens.";
|
||||||
}
|
}
|
||||||
|
setContainerElements(filtered);
|
||||||
|
|
||||||
// install browser window resize handler
|
// install browser window resize handler
|
||||||
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE,
|
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE,
|
||||||
@ -149,7 +157,7 @@ QWasmIntegration::~QWasmIntegration()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
for (const auto &elementAndScreen : m_screens)
|
for (const auto &elementAndScreen : m_screens)
|
||||||
elementAndScreen.second->deleteScreen();
|
elementAndScreen.wasmScreen->deleteScreen();
|
||||||
|
|
||||||
m_screens.clear();
|
m_screens.clear();
|
||||||
|
|
||||||
@ -285,37 +293,93 @@ QPlatformAccessibility *QWasmIntegration::accessibility() const
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void QWasmIntegration::setContainerElements(emscripten::val elementArray)
|
||||||
void QWasmIntegration::addScreen(const emscripten::val &element)
|
|
||||||
{
|
{
|
||||||
QWasmScreen *screen = new QWasmScreen(element);
|
const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen;
|
||||||
m_screens.append(qMakePair(element, screen));
|
QList<ScreenMapping> newScreens;
|
||||||
QWindowSystemInterface::handleScreenAdded(screen);
|
|
||||||
|
QList<QWasmScreen *> screensToDelete;
|
||||||
|
std::transform(m_screens.begin(), m_screens.end(), std::back_inserter(screensToDelete),
|
||||||
|
[](const ScreenMapping &mapping) { return mapping.wasmScreen; });
|
||||||
|
|
||||||
|
for (int i = 0; i < elementArray["length"].as<int>(); ++i) {
|
||||||
|
const auto element = elementArray[i];
|
||||||
|
const auto it = std::find_if(
|
||||||
|
m_screens.begin(), m_screens.end(),
|
||||||
|
[&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; });
|
||||||
|
QWasmScreen *screen;
|
||||||
|
if (it != m_screens.end()) {
|
||||||
|
screen = it->wasmScreen;
|
||||||
|
screensToDelete.erase(std::remove_if(screensToDelete.begin(), screensToDelete.end(),
|
||||||
|
[screen](const QWasmScreen *removedScreen) {
|
||||||
|
return removedScreen == screen;
|
||||||
|
}),
|
||||||
|
screensToDelete.end());
|
||||||
|
} else {
|
||||||
|
screen = new QWasmScreen(element);
|
||||||
|
QWindowSystemInterface::handleScreenAdded(screen);
|
||||||
|
}
|
||||||
|
newScreens.push_back({element, screen});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::for_each(screensToDelete.begin(), screensToDelete.end(),
|
||||||
|
[](QWasmScreen *removed) { removed->deleteScreen(); });
|
||||||
|
|
||||||
|
m_screens = newScreens;
|
||||||
|
auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen;
|
||||||
|
if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore)
|
||||||
|
QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QWasmIntegration::removeScreen(const emscripten::val &element)
|
void QWasmIntegration::addContainerElement(emscripten::val element)
|
||||||
{
|
{
|
||||||
auto it = std::find_if(m_screens.begin(), m_screens.end(),
|
Q_ASSERT_X(m_screens.end()
|
||||||
[&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(element); });
|
== std::find_if(m_screens.begin(), m_screens.end(),
|
||||||
|
[&element](const ScreenMapping &screen) {
|
||||||
|
return screen.emscriptenVal == element;
|
||||||
|
}),
|
||||||
|
Q_FUNC_INFO, "Double-add of an element");
|
||||||
|
|
||||||
|
QWasmScreen *screen = new QWasmScreen(element);
|
||||||
|
QWindowSystemInterface::handleScreenAdded(screen);
|
||||||
|
m_screens.push_back({element, screen});
|
||||||
|
}
|
||||||
|
|
||||||
|
void QWasmIntegration::removeContainerElement(emscripten::val element)
|
||||||
|
{
|
||||||
|
const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen;
|
||||||
|
|
||||||
|
const auto it =
|
||||||
|
std::find_if(m_screens.begin(), m_screens.end(),
|
||||||
|
[&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; });
|
||||||
if (it == m_screens.end()) {
|
if (it == m_screens.end()) {
|
||||||
qWarning() << "Attempting to remove non-existing screen for element"
|
qWarning() << "Attempt to remove a nonexistent screen.";
|
||||||
<< QString::fromJsString(element["id"]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
it->second->deleteScreen();
|
|
||||||
m_screens.erase(it);
|
QWasmScreen *removedScreen = it->wasmScreen;
|
||||||
|
removedScreen->deleteScreen();
|
||||||
|
|
||||||
|
m_screens.erase(std::remove_if(m_screens.begin(), m_screens.end(),
|
||||||
|
[removedScreen](const ScreenMapping &mapping) {
|
||||||
|
return removedScreen == mapping.wasmScreen;
|
||||||
|
}),
|
||||||
|
m_screens.end());
|
||||||
|
auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen;
|
||||||
|
if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore)
|
||||||
|
QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QWasmIntegration::resizeScreen(const emscripten::val &element)
|
void QWasmIntegration::resizeScreen(const emscripten::val &element)
|
||||||
{
|
{
|
||||||
auto it = std::find_if(m_screens.begin(), m_screens.end(),
|
auto it = std::find_if(m_screens.begin(), m_screens.end(),
|
||||||
[&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(element); });
|
[&] (const ScreenMapping &candidate) { return candidate.emscriptenVal.equals(element); });
|
||||||
if (it == m_screens.end()) {
|
if (it == m_screens.end()) {
|
||||||
qWarning() << "Attempting to resize non-existing screen for element"
|
qWarning() << "Attempting to resize non-existing screen for element"
|
||||||
<< QString::fromJsString(element["id"]);
|
<< QString::fromJsString(element["id"]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
it->second->updateQScreenAndCanvasRenderSize();
|
it->wasmScreen->updateQScreenAndCanvasRenderSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QWasmIntegration::updateDpi()
|
void QWasmIntegration::updateDpi()
|
||||||
@ -325,13 +389,13 @@ void QWasmIntegration::updateDpi()
|
|||||||
return;
|
return;
|
||||||
qreal dpiValue = dpi.as<qreal>();
|
qreal dpiValue = dpi.as<qreal>();
|
||||||
for (const auto &elementAndScreen : m_screens)
|
for (const auto &elementAndScreen : m_screens)
|
||||||
QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.second->screen(), dpiValue, dpiValue);
|
QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.wasmScreen->screen(), dpiValue, dpiValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QWasmIntegration::resizeAllScreens()
|
void QWasmIntegration::resizeAllScreens()
|
||||||
{
|
{
|
||||||
for (const auto &elementAndScreen : m_screens)
|
for (const auto &elementAndScreen : m_screens)
|
||||||
elementAndScreen.second->updateQScreenAndCanvasRenderSize();
|
elementAndScreen.wasmScreen->updateQScreenAndCanvasRenderSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
quint64 QWasmIntegration::getTimestamp()
|
quint64 QWasmIntegration::getTimestamp()
|
||||||
|
@ -70,8 +70,9 @@ public:
|
|||||||
QWasmInputContext *getWasmInputContext() { return m_platformInputContext; }
|
QWasmInputContext *getWasmInputContext() { return m_platformInputContext; }
|
||||||
static QWasmIntegration *get() { return s_instance; }
|
static QWasmIntegration *get() { return s_instance; }
|
||||||
|
|
||||||
void addScreen(const emscripten::val &canvas);
|
void setContainerElements(emscripten::val elementArray);
|
||||||
void removeScreen(const emscripten::val &canvas);
|
void addContainerElement(emscripten::val elementArray);
|
||||||
|
void removeContainerElement(emscripten::val elementArray);
|
||||||
void resizeScreen(const emscripten::val &canvas);
|
void resizeScreen(const emscripten::val &canvas);
|
||||||
void resizeAllScreens();
|
void resizeAllScreens();
|
||||||
void updateDpi();
|
void updateDpi();
|
||||||
@ -81,10 +82,15 @@ public:
|
|||||||
int touchPoints;
|
int touchPoints;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct ScreenMapping {
|
||||||
|
emscripten::val emscriptenVal;
|
||||||
|
QWasmScreen *wasmScreen;
|
||||||
|
};
|
||||||
|
|
||||||
mutable QWasmFontDatabase *m_fontDb;
|
mutable QWasmFontDatabase *m_fontDb;
|
||||||
mutable QWasmServices *m_desktopServices;
|
mutable QWasmServices *m_desktopServices;
|
||||||
mutable QHash<QWindow *, QWasmBackingStore *> m_backingStores;
|
mutable QHash<QWindow *, QWasmBackingStore *> m_backingStores;
|
||||||
QList<QPair<emscripten::val, QWasmScreen *>> m_screens;
|
QList<ScreenMapping> m_screens;
|
||||||
mutable QWasmClipboard *m_clipboard;
|
mutable QWasmClipboard *m_clipboard;
|
||||||
mutable QWasmAccessibility *m_accessibility;
|
mutable QWasmAccessibility *m_accessibility;
|
||||||
|
|
||||||
|
@ -27,42 +27,48 @@
|
|||||||
</figure>
|
</figure>
|
||||||
<div id="screen"></div>
|
<div id="screen"></div>
|
||||||
|
|
||||||
<script type='text/javascript'>
|
<script type="text/javascript">
|
||||||
let qtLoader = undefined;
|
async function init()
|
||||||
function init() {
|
{
|
||||||
var spinner = document.querySelector('#qtspinner');
|
const spinner = document.querySelector('#qtspinner');
|
||||||
var canvas = document.querySelector('#screen');
|
const screen = document.querySelector('#screen');
|
||||||
var status = document.querySelector('#qtstatus')
|
const status = document.querySelector('#qtstatus');
|
||||||
|
|
||||||
qtLoader = new QtLoader({
|
const showUi = (ui) => {
|
||||||
canvasElements : [canvas],
|
[spinner, screen].forEach(element => element.style.display = 'none');
|
||||||
showLoader: function(loaderStatus) {
|
if (screen === ui)
|
||||||
spinner.style.display = 'block';
|
screen.style.position = 'default';
|
||||||
canvas.style.display = 'none';
|
ui.style.display = 'block';
|
||||||
status.innerHTML = loaderStatus + "...";
|
}
|
||||||
},
|
|
||||||
showError: function(errorText) {
|
try {
|
||||||
status.innerHTML = errorText;
|
showUi(spinner);
|
||||||
spinner.style.display = 'block';
|
status.innerHTML = 'Loading...';
|
||||||
canvas.style.display = 'none';
|
|
||||||
},
|
const instance = await qtLoad({
|
||||||
showExit: function() {
|
qt: {
|
||||||
status.innerHTML = "Application exit";
|
onLoaded: () => showUi(screen),
|
||||||
if (qtLoader.exitCode !== undefined)
|
onExit: exitData =>
|
||||||
status.innerHTML += " with code " + qtLoader.exitCode;
|
{
|
||||||
if (qtLoader.exitText !== undefined)
|
status.innerHTML = 'Application exit';
|
||||||
status.innerHTML += " (" + qtLoader.exitText + ")";
|
status.innerHTML +=
|
||||||
spinner.style.display = 'block';
|
exitData.code !== undefined ? ` with code ${exitData.code}` : '';
|
||||||
canvas.style.display = 'none';
|
status.innerHTML +=
|
||||||
},
|
exitData.text !== undefined ? ` (${exitData.text})` : '';
|
||||||
showCanvas: function() {
|
showUi(spinner);
|
||||||
spinner.style.display = 'none';
|
},
|
||||||
canvas.style.display = 'block';
|
entryFunction: window.createQtAppInstance,
|
||||||
},
|
containerElements: [screen],
|
||||||
});
|
}
|
||||||
qtLoader.loadEmscriptenModule("@APPNAME@");
|
});
|
||||||
}
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
status.innerHTML = e.message;
|
||||||
|
showUi(spinner);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="@APPNAME@.js"></script>
|
||||||
<script type="text/javascript" src="qtloader.js"></script>
|
<script type="text/javascript" src="qtloader.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -94,10 +94,8 @@ public:
|
|||||||
QWasmCompositorTest() : m_window(val::global("window")), m_testSupport(val::object())
|
QWasmCompositorTest() : m_window(val::global("window")), m_testSupport(val::object())
|
||||||
{
|
{
|
||||||
m_window.set("testSupport", m_testSupport);
|
m_window.set("testSupport", m_testSupport);
|
||||||
m_testSupport.set("qtAddContainerElement",
|
m_testSupport.set("qtSetContainerElements",
|
||||||
emscripten::val::module_property("qtAddContainerElement"));
|
emscripten::val::module_property("qtSetContainerElements"));
|
||||||
m_testSupport.set("qtRemoveContainerElement",
|
|
||||||
emscripten::val::module_property("qtRemoveContainerElement"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~QWasmCompositorTest() noexcept
|
~QWasmCompositorTest() noexcept
|
||||||
@ -118,12 +116,12 @@ private:
|
|||||||
});
|
});
|
||||||
m_cleanup.emplace_back([]() mutable {
|
m_cleanup.emplace_back([]() mutable {
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
testSupport.qtRemoveContainerElement(testSupport.screenElement);
|
testSupport.qtSetContainerElements([]);
|
||||||
testSupport.screenElement.parentElement.removeChild(testSupport.screenElement);
|
testSupport.screenElement.parentElement.removeChild(testSupport.screenElement);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
EM_ASM({ testSupport.qtAddContainerElement(testSupport.screenElement); });
|
EM_ASM({ testSupport.qtSetContainerElements([testSupport.screenElement]); });
|
||||||
}
|
}
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
|
39
tests/manual/wasm/qtloader_integration/CMakeLists.txt
Normal file
39
tests/manual/wasm/qtloader_integration/CMakeLists.txt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
qt_internal_add_manual_test(tst_qtloader_integration
|
||||||
|
GUI
|
||||||
|
SOURCES
|
||||||
|
main.cpp
|
||||||
|
LIBRARIES
|
||||||
|
Qt::Core
|
||||||
|
Qt::Gui
|
||||||
|
Qt::GuiPrivate
|
||||||
|
Qt::Widgets
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(tst_qtloader_integration PROPERTIES QT_WASM_EXTRA_EXPORTED_METHODS "ENV")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET tst_qtloader_integration POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/tst_qtloader_integration.html
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/tst_qtloader_integration.html)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET tst_qtloader_integration POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../../../../src/plugins/platforms/wasm/qtloader.js
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/qtloader.js)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET tst_qtloader_integration POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../shared/testrunner.js
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/testrunner.js)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET tst_qtloader_integration POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/test_body.js
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/test_body.js)
|
155
tests/manual/wasm/qtloader_integration/main.cpp
Normal file
155
tests/manual/wasm/qtloader_integration/main.cpp
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
#include <QtWidgets/QtWidgets>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <emscripten/bind.h>
|
||||||
|
#include <emscripten/val.h>
|
||||||
|
#include <emscripten.h>
|
||||||
|
|
||||||
|
#include <QtGui/qpa/qplatformscreen.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int ExitValueImmediateReturn = 42;
|
||||||
|
constexpr int ExitValueFromExitApp = 22;
|
||||||
|
|
||||||
|
std::string screenInformation()
|
||||||
|
{
|
||||||
|
auto screens = qGuiApp->screens();
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "[";
|
||||||
|
const char *separator = "";
|
||||||
|
for (const auto &screen : screens) {
|
||||||
|
out << separator;
|
||||||
|
out << "[" << std::to_string(screen->geometry().x()) << ","
|
||||||
|
<< std::to_string(screen->geometry().y()) << ","
|
||||||
|
<< std::to_string(screen->geometry().width()) << ","
|
||||||
|
<< std::to_string(screen->geometry().height()) << "]";
|
||||||
|
separator = ",";
|
||||||
|
}
|
||||||
|
out << "]";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string logicalDpi()
|
||||||
|
{
|
||||||
|
auto screens = qGuiApp->screens();
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "[";
|
||||||
|
const char *separator = "";
|
||||||
|
for (const auto &screen : screens) {
|
||||||
|
out << separator;
|
||||||
|
out << "[" << std::to_string(screen->handle()->logicalDpi().first) << ", "
|
||||||
|
<< std::to_string(screen->handle()->logicalDpi().second) << "]";
|
||||||
|
separator = ",";
|
||||||
|
}
|
||||||
|
out << "]";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void crash()
|
||||||
|
{
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void exitApp()
|
||||||
|
{
|
||||||
|
exit(ExitValueFromExitApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void produceOutput()
|
||||||
|
{
|
||||||
|
fprintf(stdout, "Sample output!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string retrieveArguments()
|
||||||
|
{
|
||||||
|
auto arguments = QApplication::arguments();
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "[";
|
||||||
|
const char *separator = "";
|
||||||
|
for (const auto &argument : arguments) {
|
||||||
|
out << separator;
|
||||||
|
out << "'" << argument.toStdString() << "'";
|
||||||
|
separator = ",";
|
||||||
|
}
|
||||||
|
out << "]";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getEnvironmentVariable(std::string name) {
|
||||||
|
return QString::fromLatin1(qgetenv(name.c_str())).toStdString();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class AppWindow : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
AppWindow() : m_layout(new QVBoxLayout(&m_ui))
|
||||||
|
{
|
||||||
|
addWidget<QLabel>("Qt Loader integration tests");
|
||||||
|
|
||||||
|
m_ui.setLayout(m_layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show() { m_ui.show(); }
|
||||||
|
|
||||||
|
~AppWindow() = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
template<class T, class... Args>
|
||||||
|
T *addWidget(Args... args)
|
||||||
|
{
|
||||||
|
T *widget = new T(std::forward<Args>(args)..., &m_ui);
|
||||||
|
m_layout->addWidget(widget);
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget m_ui;
|
||||||
|
QVBoxLayout *m_layout;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
QApplication application(argc, argv);
|
||||||
|
const auto arguments = application.arguments();
|
||||||
|
const bool exitImmediately =
|
||||||
|
std::find(arguments.begin(), arguments.end(), QStringLiteral("--exit-immediately"))
|
||||||
|
!= arguments.end();
|
||||||
|
if (exitImmediately)
|
||||||
|
return ExitValueImmediateReturn;
|
||||||
|
|
||||||
|
const bool crashImmediately =
|
||||||
|
std::find(arguments.begin(), arguments.end(), QStringLiteral("--crash-immediately"))
|
||||||
|
!= arguments.end();
|
||||||
|
if (crashImmediately)
|
||||||
|
crash();
|
||||||
|
|
||||||
|
const bool noGui = std::find(arguments.begin(), arguments.end(), QStringLiteral("--no-gui"))
|
||||||
|
!= arguments.end();
|
||||||
|
if (!noGui) {
|
||||||
|
AppWindow window;
|
||||||
|
window.show();
|
||||||
|
return application.exec();
|
||||||
|
}
|
||||||
|
return application.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_BINDINGS(qtLoaderIntegrationTest)
|
||||||
|
{
|
||||||
|
emscripten::constant("EXIT_VALUE_IMMEDIATE_RETURN", ExitValueImmediateReturn);
|
||||||
|
emscripten::constant("EXIT_VALUE_FROM_EXIT_APP", ExitValueFromExitApp);
|
||||||
|
|
||||||
|
emscripten::function("screenInformation", &screenInformation);
|
||||||
|
emscripten::function("logicalDpi", &logicalDpi);
|
||||||
|
emscripten::function("crash", &crash);
|
||||||
|
emscripten::function("exitApp", &exitApp);
|
||||||
|
emscripten::function("produceOutput", &produceOutput);
|
||||||
|
emscripten::function("retrieveArguments", &retrieveArguments);
|
||||||
|
emscripten::function("getEnvironmentVariable", &getEnvironmentVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "main.moc"
|
445
tests/manual/wasm/qtloader_integration/test_body.js
Normal file
445
tests/manual/wasm/qtloader_integration/test_body.js
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
// 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();
|
||||||
|
})();
|
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-us">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>tst_qtloader_integration</title>
|
||||||
|
<script src='tst_qtloader_integration.js'></script>
|
||||||
|
<script src="qtloader.js" defer></script>
|
||||||
|
<script type="module" src="test_body.js" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,6 +1,71 @@
|
|||||||
// Copyright (C) 2022 The Qt Company Ltd.
|
// Copyright (C) 2022 The Qt Company Ltd.
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
export class assert
|
||||||
|
{
|
||||||
|
static isFalse(value)
|
||||||
|
{
|
||||||
|
if (value !== false)
|
||||||
|
throw new Error(`Assertion failed, expected to be false, was ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isTrue(value)
|
||||||
|
{
|
||||||
|
if (value !== true)
|
||||||
|
throw new Error(`Assertion failed, expected to be true, was ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isUndefined(value)
|
||||||
|
{
|
||||||
|
if (typeof value !== 'undefined')
|
||||||
|
throw new Error(`Assertion failed, expected to be undefined, was ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isNotUndefined(value)
|
||||||
|
{
|
||||||
|
if (typeof value === 'undefined')
|
||||||
|
throw new Error(`Assertion failed, expected not to be undefined, was ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static equal(expected, actual)
|
||||||
|
{
|
||||||
|
if (expected !== actual)
|
||||||
|
throw new Error(`Assertion failed, expected to be ${expected}, was ${actual}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static notEqual(expected, actual)
|
||||||
|
{
|
||||||
|
if (expected === actual)
|
||||||
|
throw new Error(`Assertion failed, expected not to be ${expected}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Mock extends Function
|
||||||
|
{
|
||||||
|
#calls = [];
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super()
|
||||||
|
const proxy = new Proxy(this, {
|
||||||
|
apply: (target, _, args) => target.onCall(...args)
|
||||||
|
});
|
||||||
|
proxy.thisMock = this;
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
get calls()
|
||||||
|
{
|
||||||
|
return this.thisMock.#calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCall(...args)
|
||||||
|
{
|
||||||
|
this.#calls.push(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function output(message)
|
function output(message)
|
||||||
{
|
{
|
||||||
const outputLine = document.createElement('div');
|
const outputLine = document.createElement('div');
|
||||||
@ -15,10 +80,12 @@ function output(message)
|
|||||||
export class TestRunner
|
export class TestRunner
|
||||||
{
|
{
|
||||||
#testClassInstance
|
#testClassInstance
|
||||||
|
#timeoutSeconds
|
||||||
|
|
||||||
constructor(testClassInstance)
|
constructor(testClassInstance, config)
|
||||||
{
|
{
|
||||||
this.#testClassInstance = testClassInstance;
|
this.#testClassInstance = testClassInstance;
|
||||||
|
this.#timeoutSeconds = config?.timeoutSeconds ?? 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(testCase)
|
async run(testCase)
|
||||||
@ -39,8 +106,8 @@ export class TestRunner
|
|||||||
const timeout = window.setTimeout(() =>
|
const timeout = window.setTimeout(() =>
|
||||||
{
|
{
|
||||||
rejected = true;
|
rejected = true;
|
||||||
reject(new Error('Timeout after 2 seconds'));
|
reject(new Error(`Timeout after ${this.#timeoutSeconds} seconds`));
|
||||||
}, 2000);
|
}, this.#timeoutSeconds * 1000);
|
||||||
prototype[testCase].apply(this.#testClassInstance).then(() =>
|
prototype[testCase].apply(this.#testClassInstance).then(() =>
|
||||||
{
|
{
|
||||||
if (!rejected) {
|
if (!rejected) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user