diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index e0f78646274..8ec4cb0ca49 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -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 -// QtLoader provides javascript API for managing Qt application modules. -// -// QtLoader provides API on top of Emscripten which supports common lifecycle -// tasks such as displaying placeholder content while the module downloads, -// handing application exits, and checking for browser wasm support. -// -// There are two usage modes: -// * Managed: QtLoader owns and manages the HTML display elements like -// the loader and canvas. -// * External: The embedding HTML page owns the display elements. QtLoader -// provides event callbacks which the page reacts to. -// -// Managed mode usage: -// -// var config = { -// containerElements : [$("container-id")]; -// } -// var qtLoader = new QtLoader(config); -// qtLoader.loadEmscriptenModule("applicationName"); -// -// External mode usage: -// -// var config = { -// canvasElements : [$("canvas-id")], -// showLoader: function() { -// loader.style.display = 'block' -// canvas.style.display = 'hidden' -// }, -// showCanvas: function() { -// loader.style.display = 'hidden' -// canvas.style.display = 'block' -// return canvas; -// } -// } -// 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 : -// 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 : -// Restart attempts limit. The default is 10. -// stdoutEnabled : -// stderrEnabled : -// environment : -// 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) +/** + * Loads the instance of a WASM module. + * + * @param config May contain any key normally accepted by emscripten and the 'qt' extra key, with + * the following sub-keys: + * - environment: { [name:string] : string } + * environment variables set on the instance + * - onExit: (exitStatus: { text: string, code?: number, crashed: bool }) => void + * called when the application has exited for any reason. exitStatus.code is defined in + * case of a normal application exit. This is not called on exit with return code 0, as + * the program does not shutdown its runtime and technically keeps running async. + * - containerElements: HTMLDivElement[] + * Array of host elements for Qt screens. Each of these elements is mapped to a QScreen on + * launch. + * - fontDpi: number + * Specifies font DPI for the instance + * - onLoaded: () => void + * Called when the module has loaded. + * - entryFunction: (emscriptenConfig: object) => Promise + * Qt always uses emscripten's MODULARIZE option. This is the MODULARIZE entry function. + * + * @return Promise<{ + * instance: EmscriptenModule, + * exitStatus?: { text: string, code?: number, crashed: bool } + * }> + * The promise is resolved when the module has been instantiated and its main function has been + * 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 + * later time when (and if) the application exits. + * + * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten for + * EmscriptenModule + */ +async function qtLoad(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) -{ - const self = this; + if (typeof config !== 'object') + throw new Error('config is required, expected an object'); + 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 - // object is created in completeLoadEmscriptenModule(). - self.module = undefined; - self.moduleConfig = config.moduleConfig || {}; + config.qtContainerElements = config.qt.containerElements; + delete config.qt.containerElements; + config.qtFontDpi = config.qt.fontDpi; + delete config.qt.fontDpi; - // Qt properties. These are propagated to the Emscripten module after - // it has been created. - self.qtContainerElements = undefined; - self.qtFontDpi = 96; + // Used for rejecting a failed load's promise where emscripten itself does not allow it, + // like in instantiateWasm below. This allows us to throw in case of a load error instead of + // hanging on a promise to entry function, which emscripten unfortunately does. + let circuitBreakerReject; + const circuitBreaker = new Promise((_, reject) => { circuitBreakerReject = reject; }); - function webAssemblySupported() { - return typeof WebAssembly !== "undefined" - } - - function webGLSupported() { - // We expect that WebGL is supported if WebAssembly is; however - // the GPU may be blacklisted. - try { - var canvas = document.createElement("canvas"); - return !!(window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))); - } 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 = "

" + loadingState + "

"; - 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 = ` ${crashSymbols[symbolIndex]} ` - 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; + // If module async getter is present, use it so that module reuse is possible. + if (config.qt.modulePromise) { + config.instantiateWasm = async (imports, successCallback) => + { + try { + const module = await config.qt.modulePromise; + successCallback( + await WebAssembly.instantiate(module, imports), module); + } catch (e) { + circuitBreakerReject(e); } - }); - } - - 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, - // and is ready to be instantiated. Define the instantiateWasm callback which - // emscripten will call to create the instance. - self.moduleConfig.instantiateWasm = function(imports, successCallback) { - WebAssembly.instantiate(wasmModule, imports).then(function(instance) { - successCallback(instance, wasmModule); - }, function(error) { - handleError(error) + throwIfEnvUsedButNotExported(instance, config); + for (const [name, value] of Object.entries(config.qt.environment ?? {})) + instance.ENV[name] = value; + }; + + config.onRuntimeInitialized = () => config.qt.onLoaded?.(); + + // 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) { - return config.path + filename; - }; + const originalOnAbort = config.onAbort; + config.onAbort = text => + { + originalOnAbort?.(); - // Attach status callbacks - self.moduleConfig.setStatus = self.moduleConfig.setStatus || function(text) { - // Currently the only usable status update from this function - // is "Running..." - 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; + aborted = true; + config.qt.onExit?.({ + text, + crashed: true }); + }; - 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; - - 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; + return instance; } diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp index 5dc96e0f3c4..c8f5072cb83 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -38,14 +38,19 @@ using namespace emscripten; using namespace Qt::StringLiterals; +static void setContainerElements(emscripten::val elementArray) +{ + QWasmIntegration::get()->setContainerElements(elementArray); +} + static void addContainerElement(emscripten::val element) { - QWasmIntegration::get()->addScreen(element); + QWasmIntegration::get()->addContainerElement(element); } static void removeContainerElement(emscripten::val element) { - QWasmIntegration::get()->removeScreen(element); + QWasmIntegration::get()->removeContainerElement(element); } static void resizeContainerElement(emscripten::val element) @@ -66,6 +71,7 @@ static void resizeAllScreens(emscripten::val event) EMSCRIPTEN_BINDINGS(qtQWasmIntegraton) { + function("qtSetContainerElements", &setContainerElements); function("qtAddContainerElement", &addContainerElement); function("qtRemoveContainerElement", &removeContainerElement); function("qtResizeContainerElement", &resizeContainerElement); @@ -94,6 +100,7 @@ QWasmIntegration::QWasmIntegration() // 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 // QWasmScreen ctor. + emscripten::val filtered = emscripten::val::array(); emscripten::val qtContainerElements = val::module_property("qtContainerElements"); if (qtContainerElements.isArray()) { for (int i = 0; i < qtContainerElements["length"].as(); ++i) { @@ -101,13 +108,14 @@ QWasmIntegration::QWasmIntegration() if (element.isNull() || element.isUndefined()) qWarning() << "Skipping null or undefined element in qtContainerElements"; else - addScreen(element); + filtered.call("push", element); } } else { // No screens, which may or may not be intended qWarning() << "The qtContainerElements module property was not set or is invalid. " "Proceeding with no screens."; } + setContainerElements(filtered); // install browser window resize handler emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, @@ -149,7 +157,7 @@ QWasmIntegration::~QWasmIntegration() #endif for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->deleteScreen(); + elementAndScreen.wasmScreen->deleteScreen(); m_screens.clear(); @@ -285,37 +293,93 @@ QPlatformAccessibility *QWasmIntegration::accessibility() const } #endif - -void QWasmIntegration::addScreen(const emscripten::val &element) +void QWasmIntegration::setContainerElements(emscripten::val elementArray) { - QWasmScreen *screen = new QWasmScreen(element); - m_screens.append(qMakePair(element, screen)); - QWindowSystemInterface::handleScreenAdded(screen); + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + QList newScreens; + + QList 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(); ++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(), - [&] (const QPair &candidate) { return candidate.first.equals(element); }); + Q_ASSERT_X(m_screens.end() + == 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()) { - qWarning() << "Attempting to remove non-existing screen for element" - << QString::fromJsString(element["id"]); + qWarning() << "Attempt to remove a nonexistent screen."; 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) { auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair &candidate) { return candidate.first.equals(element); }); + [&] (const ScreenMapping &candidate) { return candidate.emscriptenVal.equals(element); }); if (it == m_screens.end()) { qWarning() << "Attempting to resize non-existing screen for element" << QString::fromJsString(element["id"]); return; } - it->second->updateQScreenAndCanvasRenderSize(); + it->wasmScreen->updateQScreenAndCanvasRenderSize(); } void QWasmIntegration::updateDpi() @@ -325,13 +389,13 @@ void QWasmIntegration::updateDpi() return; qreal dpiValue = dpi.as(); for (const auto &elementAndScreen : m_screens) - QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.second->screen(), dpiValue, dpiValue); + QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.wasmScreen->screen(), dpiValue, dpiValue); } void QWasmIntegration::resizeAllScreens() { for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->updateQScreenAndCanvasRenderSize(); + elementAndScreen.wasmScreen->updateQScreenAndCanvasRenderSize(); } quint64 QWasmIntegration::getTimestamp() diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index decf25009e6..8dfa065d9c0 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -70,8 +70,9 @@ public: QWasmInputContext *getWasmInputContext() { return m_platformInputContext; } static QWasmIntegration *get() { return s_instance; } - void addScreen(const emscripten::val &canvas); - void removeScreen(const emscripten::val &canvas); + void setContainerElements(emscripten::val elementArray); + void addContainerElement(emscripten::val elementArray); + void removeContainerElement(emscripten::val elementArray); void resizeScreen(const emscripten::val &canvas); void resizeAllScreens(); void updateDpi(); @@ -81,10 +82,15 @@ public: int touchPoints; private: + struct ScreenMapping { + emscripten::val emscriptenVal; + QWasmScreen *wasmScreen; + }; + mutable QWasmFontDatabase *m_fontDb; mutable QWasmServices *m_desktopServices; mutable QHash m_backingStores; - QList> m_screens; + QList m_screens; mutable QWasmClipboard *m_clipboard; mutable QWasmAccessibility *m_accessibility; diff --git a/src/plugins/platforms/wasm/wasm_shell.html b/src/plugins/platforms/wasm/wasm_shell.html index aaa121981db..89666776da0 100644 --- a/src/plugins/platforms/wasm/wasm_shell.html +++ b/src/plugins/platforms/wasm/wasm_shell.html @@ -27,42 +27,48 @@

- + diff --git a/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp b/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp index 6866fe229f6..df6df5f4533 100644 --- a/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp +++ b/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp @@ -94,10 +94,8 @@ public: QWasmCompositorTest() : m_window(val::global("window")), m_testSupport(val::object()) { m_window.set("testSupport", m_testSupport); - m_testSupport.set("qtAddContainerElement", - emscripten::val::module_property("qtAddContainerElement")); - m_testSupport.set("qtRemoveContainerElement", - emscripten::val::module_property("qtRemoveContainerElement")); + m_testSupport.set("qtSetContainerElements", + emscripten::val::module_property("qtSetContainerElements")); } ~QWasmCompositorTest() noexcept @@ -118,12 +116,12 @@ private: }); m_cleanup.emplace_back([]() mutable { EM_ASM({ - testSupport.qtRemoveContainerElement(testSupport.screenElement); + testSupport.qtSetContainerElements([]); testSupport.screenElement.parentElement.removeChild(testSupport.screenElement); }); }); - EM_ASM({ testSupport.qtAddContainerElement(testSupport.screenElement); }); + EM_ASM({ testSupport.qtSetContainerElements([testSupport.screenElement]); }); } template diff --git a/tests/manual/wasm/qtloader_integration/CMakeLists.txt b/tests/manual/wasm/qtloader_integration/CMakeLists.txt new file mode 100644 index 00000000000..24c5607cd88 --- /dev/null +++ b/tests/manual/wasm/qtloader_integration/CMakeLists.txt @@ -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) diff --git a/tests/manual/wasm/qtloader_integration/main.cpp b/tests/manual/wasm/qtloader_integration/main.cpp new file mode 100644 index 00000000000..ed8a57bcc6e --- /dev/null +++ b/tests/manual/wasm/qtloader_integration/main.cpp @@ -0,0 +1,155 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +#include + +#include +#include + +#include +#include +#include + +#include + +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("Qt Loader integration tests"); + + m_ui.setLayout(m_layout); + } + + void show() { m_ui.show(); } + + ~AppWindow() = default; + +private: + template + T *addWidget(Args... args) + { + T *widget = new T(std::forward(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" diff --git a/tests/manual/wasm/qtloader_integration/test_body.js b/tests/manual/wasm/qtloader_integration/test_body.js new file mode 100644 index 00000000000..f23db3a978a --- /dev/null +++ b/tests/manual/wasm/qtloader_integration/test_body.js @@ -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(); +})(); diff --git a/tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html b/tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html new file mode 100644 index 00000000000..7aa7528a1db --- /dev/null +++ b/tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html @@ -0,0 +1,13 @@ + + + + + tst_qtloader_integration + + + + + + + + diff --git a/tests/manual/wasm/shared/testrunner.js b/tests/manual/wasm/shared/testrunner.js index da87026b03c..82259b0620b 100644 --- a/tests/manual/wasm/shared/testrunner.js +++ b/tests/manual/wasm/shared/testrunner.js @@ -1,6 +1,71 @@ // Copyright (C) 2022 The Qt Company Ltd. // 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) { const outputLine = document.createElement('div'); @@ -15,10 +80,12 @@ function output(message) export class TestRunner { #testClassInstance + #timeoutSeconds - constructor(testClassInstance) + constructor(testClassInstance, config) { this.#testClassInstance = testClassInstance; + this.#timeoutSeconds = config?.timeoutSeconds ?? 2; } async run(testCase) @@ -39,8 +106,8 @@ export class TestRunner const timeout = window.setTimeout(() => { rejected = true; - reject(new Error('Timeout after 2 seconds')); - }, 2000); + reject(new Error(`Timeout after ${this.#timeoutSeconds} seconds`)); + }, this.#timeoutSeconds * 1000); prototype[testCase].apply(this.#testClassInstance).then(() => { if (!rejected) {