qt 6.6.0 clean

This commit is contained in:
kleuter
2023-11-01 22:23:55 +01:00
parent 7b5ada15e7
commit 5d8194efa7
1449 changed files with 134276 additions and 31391 deletions

View File

@ -320,8 +320,8 @@ void FilesTest::selectOneFileWithFileDialog()
QWASMSUCCESS();
});
QWasmLocalFileAccess::openFile(
{QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
QWasmLocalFileAccess::openFile("*", fileSelectedCallback->get(), acceptFileCallback->get(),
fileDataReadyCallback->get());
}
void FilesTest::selectMultipleFilesWithFileDialog()
@ -377,9 +377,9 @@ void FilesTest::selectMultipleFilesWithFileDialog()
}
});
QWasmLocalFileAccess::openFiles(
{QStringLiteral("*")}, QWasmLocalFileAccess::FileSelectMode::MultipleFiles,
fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
QWasmLocalFileAccess::openFiles("*", QWasmLocalFileAccess::FileSelectMode::MultipleFiles,
fileSelectedCallback->get(), acceptFileCallback->get(),
fileDataReadyCallback->get());
}
void FilesTest::cancelFileDialog()
@ -398,8 +398,8 @@ void FilesTest::cancelFileDialog()
auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
auto* fileDataReadyCallback = Own(new MockCallback<void>());
QWasmLocalFileAccess::openFile(
{QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
QWasmLocalFileAccess::openFile("*", fileSelectedCallback->get(), acceptFileCallback->get(),
fileDataReadyCallback->get());
}
void FilesTest::rejectFile()
@ -430,8 +430,8 @@ void FilesTest::rejectFile()
return nullptr;
});
QWasmLocalFileAccess::openFile(
{QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
QWasmLocalFileAccess::openFile("*", fileSelectedCallback->get(), acceptFileCallback->get(),
fileDataReadyCallback->get());
}
void FilesTest::saveFileWithFileDialog()

View File

@ -7,7 +7,7 @@
#include <QtGui/qguiapplication.h>
#include <QtGui/qoffscreensurface.h>
#include <QtGui/qpa/qwindowsysteminterface.h>
#include <QtGui/private/qrhigles2_p.h>
#include <QtGui/rhi/qrhi.h>
#include <qtwasmtestlib.h>
@ -66,7 +66,7 @@ void Window::keyPressEvent(QKeyEvent *)
void Window::init()
{
QRhi::Flags rhiFlags = QRhi::EnableDebugMarkers | QRhi::EnableProfiling;
QRhi::Flags rhiFlags = QRhi::EnableDebugMarkers;
m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface());
QRhiGles2InitParams params;
@ -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<class T>

View File

@ -0,0 +1,45 @@
# 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)
add_custom_command(
TARGET tst_qtloader_integration POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/preload.json
${CMAKE_CURRENT_BINARY_DIR}/preload.json)

View File

@ -0,0 +1,171 @@
// 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();
}
std::string preloadedFiles()
{
QStringList files = QDir("/preload").entryList(QDir::Files);
std::ostringstream out;
out << "[";
const char *separator = "";
for (const auto &file : files) {
out << separator;
out << file.toStdString();
separator = ",";
}
out << "]";
return out.str();
}
void crash()
{
std::abort();
}
void exitApp()
{
emscripten_force_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)
emscripten_force_exit(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("preloadedFiles", &preloadedFiles);
emscripten::function("crash", &crash);
emscripten::function("exitApp", &exitApp);
emscripten::function("produceOutput", &produceOutput);
emscripten::function("retrieveArguments", &retrieveArguments);
emscripten::function("getEnvironmentVariable", &getEnvironmentVariable);
}
#include "main.moc"

View File

@ -0,0 +1,10 @@
[
{
"source": "qtloader.js",
"destination": "/preload/qtloader.js"
},
{
"source": "$QTDIR/qtlogo.svg",
"destination": "/preload/qtlogo.svg"
}
]

View File

@ -0,0 +1,469 @@
// 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 module = 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`,
})],
module,
}
})));
// 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 modulePromiseProvided()
{
await qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: [this.#testScreenContainers[0]],
module: WebAssembly.compileStreaming(
fetch('tst_qtloader_integration.wasm'))
}
});
}
async moduleProvided()
{
await qtLoad({
qt: {
entryFunction: createQtAppInstance,
containerElements: [this.#testScreenContainers[0]],
module: await WebAssembly.compileStreaming(
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]],
module: 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;
}
assert.isUndefined(caughtException);
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.isUndefined(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.isUndefined(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);
}
async preloadFiles()
{
const instance = await qtLoad({
arguments: ["--no-gui"],
qt: {
preload: ['preload.json'],
qtdir: '.',
}
});
const preloadedFiles = instance.preloadedFiles();
// Verify that preloaded file list matches files specified in preload.json
assert.equal("[qtloader.js,qtlogo.svg]", preloadedFiles);
}
#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();
})();

View File

@ -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>

View File

@ -0,0 +1,36 @@
qt_internal_add_manual_test(qwasmwindow_harness
SOURCES
qwasmwindow_harness.cpp
LIBRARIES
Qt::Core
Qt::CorePrivate
Qt::GuiPrivate
)
add_custom_command(
TARGET qwasmwindow_harness POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/qwasmwindow_harness.html
${CMAKE_CURRENT_BINARY_DIR}/qwasmwindow_harness.html
)
add_custom_command(
TARGET qwasmwindow_harness POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/qwasmwindow.py
${CMAKE_CURRENT_BINARY_DIR}/qwasmwindow.py
)
add_custom_command(
TARGET qwasmwindow_harness POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/run.sh
${CMAKE_CURRENT_BINARY_DIR}/run.sh
)
add_custom_command(
TARGET qwasmwindow_harness POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/../../../../util/wasm/qtwasmserver/qtwasmserver.py
${CMAKE_CURRENT_BINARY_DIR}/qtwasmserver.py
)

View File

@ -0,0 +1,445 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
from selenium.webdriver import Chrome
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_actions import PointerActions
from selenium.webdriver.common.actions.interaction import POINTER_TOUCH
from selenium.webdriver.common.actions.pointer_input import PointerInput
from selenium.webdriver.common.by import By
from selenium.webdriver.support.expected_conditions import presence_of_element_located
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
import unittest
from enum import Enum, auto
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self._driver = Chrome()
self._driver.get(
'http://localhost:8001/qwasmwindow_harness.html')
self._test_sandbox_element = WebDriverWait(self._driver, 30).until(
presence_of_element_located((By.ID, 'test-sandbox'))
)
self.addTypeEqualityFunc(Rect, assert_rects_equal)
def test_window_resizing(self):
defaultWindowMinSize = 100
screen = Screen(self._driver, ScreenPosition.FIXED,
x=0, y=0, width=600, height=600)
window = Window(screen, x=100, y=100, width=200, height=200)
self.assertEqual(window.rect, Rect(x=100, y=100, width=200, height=200))
window.drag(Handle.TOP_LEFT, direction=UP(10) + LEFT(10))
self.assertEqual(window.rect, Rect(x=90, y=90, width=210, height=210))
window.drag(Handle.TOP, direction=DOWN(10) + LEFT(100))
self.assertEqual(window.rect, Rect(x=90, y=100, width=210, height=200))
window.drag(Handle.TOP_RIGHT, direction=UP(5) + LEFT(5))
self.assertEqual(window.rect, Rect(x=90, y=95, width=205, height=205))
window.drag(Handle.RIGHT, direction=DOWN(100) + RIGHT(5))
self.assertEqual(window.rect, Rect(x=90, y=95, width=210, height=205))
window.drag(Handle.BOTTOM_RIGHT, direction=UP(5) + LEFT(10))
self.assertEqual(window.rect, Rect(x=90, y=95, width=200, height=200))
window.drag(Handle.BOTTOM, direction=DOWN(20) + LEFT(100))
self.assertEqual(window.rect, Rect(x=90, y=95, width=200, height=220))
window.drag(Handle.BOTTOM_LEFT, direction=DOWN(10) + LEFT(10))
self.assertEqual(window.rect, Rect(x=80, y=95, width=210, height=230))
window.drag(Handle.LEFT, direction=DOWN(343) + LEFT(5))
self.assertEqual(window.rect, Rect(x=75, y=95, width=215, height=230))
window.drag(Handle.BOTTOM_RIGHT, direction=UP(150) + LEFT(150))
self.assertEqual(window.rect, Rect(x=75, y=95, width=defaultWindowMinSize, height=defaultWindowMinSize))
def test_cannot_resize_over_screen_top_edge(self):
screen = Screen(self._driver, ScreenPosition.FIXED,
x=200, y=200, width=300, height=300)
window = Window(screen, x=300, y=300, width=100, height=100)
self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100))
frame_rect_before_resize = window.frame_rect
window.drag(Handle.TOP, direction=UP(200))
self.assertEqual(window.rect.x, 300)
self.assertEqual(window.frame_rect.y, screen.rect.y)
self.assertEqual(window.rect.width, 100)
self.assertEqual(window.frame_rect.y + window.frame_rect.height,
frame_rect_before_resize.y + frame_rect_before_resize.height)
def test_window_move(self):
screen = Screen(self._driver, ScreenPosition.FIXED,
x=200, y=200, width=300, height=300)
window = Window(screen, x=300, y=300, width=100, height=100)
self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100))
window.drag(Handle.TOP_WINDOW_BAR, direction=UP(30))
self.assertEqual(window.rect, Rect(x=300, y=270, width=100, height=100))
window.drag(Handle.TOP_WINDOW_BAR, direction=RIGHT(50))
self.assertEqual(window.rect, Rect(x=350, y=270, width=100, height=100))
window.drag(Handle.TOP_WINDOW_BAR, direction=DOWN(30) + LEFT(70))
self.assertEqual(window.rect, Rect(x=280, y=300, width=100, height=100))
def test_screen_limits_window_moves(self):
screen = Screen(self._driver, ScreenPosition.RELATIVE,
x=200, y=200, width=300, height=300)
window = Window(screen, x=300, y=300, width=100, height=100)
self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100))
window.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300))
self.assertEqual(window.frame_rect.x, screen.rect.x - window.frame_rect.width / 2)
def test_screen_in_scroll_container_limits_window_moves(self):
screen = Screen(self._driver, ScreenPosition.IN_SCROLL_CONTAINER,
x=200, y=2000, width=300, height=300,
container_width=500, container_height=7000)
screen.scroll_to()
window = Window(screen, x=300, y=2100, width=100, height=100)
self.assertEqual(window.rect, Rect(x=300, y=2100, width=100, height=100))
window.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300))
self.assertEqual(window.frame_rect.x, screen.rect.x - window.frame_rect.width / 2)
def test_maximize(self):
screen = Screen(self._driver, ScreenPosition.RELATIVE,
x=200, y=200, width=300, height=300)
window = Window(screen, x=300, y=300, width=100, height=100, title='Maximize')
self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100))
window.maximize()
self.assertEqual(window.frame_rect, Rect(x=200, y=200, width=300, height=300))
def test_multitouch_window_move(self):
screen = Screen(self._driver, ScreenPosition.FIXED,
x=0, y=0, width=800, height=800)
windows = [Window(screen, x=50, y=50, width=100, height=100, title='First'),
Window(screen, x=400, y=400, width=100, height=100, title='Second'),
Window(screen, x=50, y=400, width=100, height=100, title='Third')]
self.assertEqual(windows[0].rect, Rect(x=50, y=50, width=100, height=100))
self.assertEqual(windows[1].rect, Rect(x=400, y=400, width=100, height=100))
self.assertEqual(windows[2].rect, Rect(x=50, y=400, width=100, height=100))
actions = [TouchDragAction(origin=windows[0].at(Handle.TOP_WINDOW_BAR), direction=DOWN(20) + RIGHT(20)),
TouchDragAction(origin=windows[1].at(Handle.TOP_WINDOW_BAR), direction=DOWN(20) + LEFT(20)),
TouchDragAction(origin=windows[2].at(Handle.TOP_WINDOW_BAR), direction=UP(20) + RIGHT(20))]
perform_touch_drag_actions(actions)
self.assertEqual(windows[0].rect, Rect(x=70, y=70, width=100, height=100))
self.assertEqual(windows[1].rect, Rect(x=380, y=420, width=100, height=100))
self.assertEqual(windows[2].rect, Rect(x=70, y=380, width=100, height=100))
def test_multitouch_window_resize(self):
screen = Screen(self._driver, ScreenPosition.FIXED,
x=0, y=0, width=800, height=800)
windows = [Window(screen, x=50, y=50, width=150, height=150, title='First'),
Window(screen, x=400, y=400, width=150, height=150, title='Second'),
Window(screen, x=50, y=400, width=150, height=150, title='Third')]
self.assertEqual(windows[0].rect, Rect(x=50, y=50, width=150, height=150))
self.assertEqual(windows[1].rect, Rect(x=400, y=400, width=150, height=150))
self.assertEqual(windows[2].rect, Rect(x=50, y=400, width=150, height=150))
actions = [TouchDragAction(origin=windows[0].at(Handle.TOP_LEFT), direction=DOWN(20) + RIGHT(20)),
TouchDragAction(origin=windows[1].at(Handle.TOP), direction=DOWN(20) + LEFT(20)),
TouchDragAction(origin=windows[2].at(Handle.BOTTOM_RIGHT), direction=UP(20) + RIGHT(20))]
perform_touch_drag_actions(actions)
self.assertEqual(windows[0].rect, Rect(x=70, y=70, width=130, height=130))
self.assertEqual(windows[1].rect, Rect(x=400, y=420, width=150, height=130))
self.assertEqual(windows[2].rect, Rect(x=50, y=400, width=170, height=130))
def tearDown(self):
self._driver.quit()
class ScreenPosition(Enum):
FIXED = auto()
RELATIVE = auto()
IN_SCROLL_CONTAINER = auto()
class Screen:
def __init__(self, driver, positioning, x, y, width, height, container_width=0, container_height=0):
self.driver = driver
self.x = x
self.y = y
self.width = width
self.height = height
if positioning == ScreenPosition.FIXED:
command = f'initializeScreenWithFixedPosition({self.x}, {self.y}, {self.width}, {self.height})'
elif positioning == ScreenPosition.RELATIVE:
command = f'initializeScreenWithRelativePosition({self.x}, {self.y}, {self.width}, {self.height})'
elif positioning == ScreenPosition.IN_SCROLL_CONTAINER:
command = f'initializeScreenInScrollContainer({container_width}, {container_height}, {self.x}, {self.y}, {self.width}, {self.height})'
self.element = self.driver.execute_script(
f'''
return testSupport.{command};
'''
)
if positioning == ScreenPosition.IN_SCROLL_CONTAINER:
self.element = self.element[1]
screen_information = call_instance_function(
self.driver, 'screenInformation')
if len(screen_information) != 1:
raise AssertionError('Expecting exactly one screen_information!')
self.screen_info = screen_information[0]
@property
def rect(self):
self.screen_info = call_instance_function(
self.driver, 'screenInformation')[0]
geo = self.screen_info['geometry']
return Rect(geo['x'], geo['y'], geo['width'], geo['height'])
def scroll_to(self):
ActionChains(self.driver).scroll_to_element(self.element).perform()
class Window:
def __init__(self, screen, x, y, width, height, title='title'):
self.driver = screen.driver
self.title = title
self.driver.execute_script(
f'''
instance.createWindow({x}, {y}, {width}, {height}, '{screen.screen_info["name"]}', '{title}');
'''
)
self._window_id = self.__window_information()['id']
self.element = screen.element.shadow_root.find_element(
By.CSS_SELECTOR, f'#qt-window-{self._window_id}')
def __window_information(self):
information = call_instance_function(self.driver, 'windowInformation')
return next(filter(lambda e: e['title'] == self.title, information))
@property
def rect(self):
geo = self.__window_information()["geometry"]
return Rect(geo['x'], geo['y'], geo['width'], geo['height'])
@property
def frame_rect(self):
geo = self.__window_information()["frameGeometry"]
return Rect(geo['x'], geo['y'], geo['width'], geo['height'])
def drag(self, handle, direction):
ActionChains(self.driver) \
.move_to_element_with_offset(self.element, *self.at(handle)['offset']) \
.click_and_hold() \
.move_by_offset(*translate_direction_to_offset(direction)) \
.release().perform()
def maximize(self):
maximize_button = self.element.find_element(
By.CSS_SELECTOR, f'.title-bar :nth-child(6)')
maximize_button.click()
def at(self, handle):
""" Returns (window, offset) for given handle on window"""
width = self.frame_rect.width
height = self.frame_rect.height
if handle == Handle.TOP_LEFT:
offset = (-width/2, -height/2)
elif handle == Handle.TOP:
offset = (0, -height/2)
elif handle == Handle.TOP_RIGHT:
offset = (width/2, -height/2)
elif handle == Handle.LEFT:
offset = (-width/2, 0)
elif handle == Handle.RIGHT:
offset = (width/2, 0)
elif handle == Handle.BOTTOM_LEFT:
offset = (-width/2, height/2)
elif handle == Handle.BOTTOM:
offset = (0, height/2)
elif handle == Handle.BOTTOM_RIGHT:
offset = (width/2, height/2)
elif handle == Handle.TOP_WINDOW_BAR:
frame_top = self.frame_rect.y
client_area_top = self.rect.y
top_frame_bar_width = client_area_top - frame_top
offset = (0, -height/2 + top_frame_bar_width/2)
return {'window': self, 'offset': offset}
class TouchDragAction:
def __init__(self, origin, direction):
self.origin = origin
self.direction = direction
self.step = 2
def perform_touch_drag_actions(actions):
driver = actions[0].origin['window'].driver
touch_action_builder = ActionBuilder(driver)
pointers = [PointerActions(source=touch_action_builder.add_pointer_input(
POINTER_TOUCH, f'touch_input_{i}')) for i in range(len(actions))]
for action, pointer in zip(actions, pointers):
pointer.move_to(
action.origin['window'].element, *action.origin['offset'])
pointer.pointer_down(width=10, height=10, pressure=1)
moves = [translate_direction_to_offset(a.direction) for a in actions]
def movement_finished():
for move in moves:
if move != (0, 0):
return False
return True
def sign(num):
if num > 0:
return 1
elif num < 0:
return -1
return 0
while not movement_finished():
for i in range(len(actions)):
pointer = pointers[i]
move = moves[i]
step = actions[i].step
current_move = (
min(abs(move[0]), step) * sign(move[0]), min(abs(move[1]), step) * sign(move[1]))
moves[i] = (move[0] - current_move[0], move[1] - current_move[1])
pointer.move_by(current_move[0],
current_move[1], width=10, height=10)
for pointer in pointers:
pointer.pointer_up()
touch_action_builder.perform()
class TouchDragAction:
def __init__(self, origin, direction):
self.origin = origin
self.direction = direction
self.step = 2
def perform_touch_drag_actions(actions):
driver = actions[0].origin['window'].driver
touch_action_builder = ActionBuilder(driver)
pointers = [PointerActions(source=touch_action_builder.add_pointer_input(
POINTER_TOUCH, f'touch_input_{i}')) for i in range(len(actions))]
for action, pointer in zip(actions, pointers):
pointer.move_to(
action.origin['window'].element, *action.origin['offset'])
pointer.pointer_down(width=10, height=10, pressure=1)
moves = [translate_direction_to_offset(a.direction) for a in actions]
def movement_finished():
for move in moves:
if move != (0, 0):
return False
return True
def sign(num):
if num > 0:
return 1
elif num < 0:
return -1
return 0
while not movement_finished():
for i in range(len(actions)):
pointer = pointers[i]
move = moves[i]
step = actions[i].step
current_move = (
min(abs(move[0]), step) * sign(move[0]), min(abs(move[1]), step) * sign(move[1]))
moves[i] = (move[0] - current_move[0], move[1] - current_move[1])
pointer.move_by(current_move[0],
current_move[1], width=10, height=10)
for pointer in pointers:
pointer.pointer_up()
touch_action_builder.perform()
def translate_direction_to_offset(direction):
return (direction.val[1] - direction.val[3], direction.val[2] - direction.val[0])
def call_instance_function(driver, name):
return driver.execute_script(
f'''let result;
window.{name}Callback = data => result = data;
instance.{name}();
return eval(result);''')
class Direction:
def __init__(self):
self.val = (0, 0, 0, 0)
def __init__(self, north, east, south, west):
self.val = (north, east, south, west)
def __add__(self, other):
return Direction(self.val[0] + other.val[0],
self.val[1] + other.val[1],
self.val[2] + other.val[2],
self.val[3] + other.val[3])
class UP(Direction):
def __init__(self, step=1):
self.val = (step, 0, 0, 0)
class RIGHT(Direction):
def __init__(self, step=1):
self.val = (0, step, 0, 0)
class DOWN(Direction):
def __init__(self, step=1):
self.val = (0, 0, step, 0)
class LEFT(Direction):
def __init__(self, step=1):
self.val = (0, 0, 0, step)
class Handle(Enum):
TOP_LEFT = auto()
TOP = auto()
TOP_RIGHT = auto()
LEFT = auto()
RIGHT = auto()
BOTTOM_LEFT = auto()
BOTTOM = auto()
BOTTOM_RIGHT = auto()
TOP_WINDOW_BAR = auto()
class Rect:
def __init__(self, x, y, width, height) -> None:
self.x = x
self.y = y
self.width = width
self.height = height
def __str__(self):
return f'(x: {self.x}, y: {self.y}, width: {self.width}, height: {self.height})'
def assert_rects_equal(geo1, geo2, msg=None):
if geo1.x != geo2.x or geo1.y != geo2.y or geo1.width != geo2.width or geo1.height != geo2.height:
raise AssertionError(f'Rectangles not equal: \n{geo1} \nvs \n{geo2}')
unittest.main()

View File

@ -0,0 +1,149 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/QEvent>
#include <QtCore/qobject.h>
#include <QtCore/qregularexpression.h>
#include <QtGui/qscreen.h>
#include <QtGui/qwindow.h>
#include <QtGui/qguiapplication.h>
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <memory>
#include <sstream>
#include <vector>
class DeleteOnCloseWindow : public QWindow
{
Q_OBJECT
private:
void closeEvent(QCloseEvent *ev) override
{
Q_UNUSED(ev);
delete this;
}
};
using namespace emscripten;
std::string toJSArray(const std::vector<std::string> &elements)
{
std::ostringstream out;
out << "[";
bool comma = false;
for (const auto &element : elements) {
out << (comma ? "," : "");
out << element;
comma = true;
}
out << "]";
return out.str();
}
std::string toJSString(const QString &qstring)
{
Q_ASSERT_X(([qstring]() {
static QRegularExpression unescapedQuoteRegex(R"re((?:^|[^\\])')re");
return qstring.indexOf(unescapedQuoteRegex) == -1;
})(),
Q_FUNC_INFO, "Unescaped single quotes found");
return "'" + qstring.toStdString() + "'";
}
std::string rectToJSObject(const QRect &rect)
{
std::ostringstream out;
out << "{"
<< " x: " << std::to_string(rect.x()) << ","
<< " y: " << std::to_string(rect.y()) << ","
<< " width: " << std::to_string(rect.width()) << ","
<< " height: " << std::to_string(rect.height()) << "}";
return out.str();
}
std::string screenToJSObject(const QScreen &screen)
{
std::ostringstream out;
out << "{"
<< " name: " << toJSString(screen.name()) << ","
<< " geometry: " << rectToJSObject(screen.geometry()) << "}";
return out.str();
}
std::string windowToJSObject(const QWindow &window)
{
std::ostringstream out;
out << "{"
<< " id: " << std::to_string(window.winId()) << ","
<< " geometry: " << rectToJSObject(window.geometry()) << ","
<< " frameGeometry: " << rectToJSObject(window.frameGeometry()) << ","
<< " title: '" << window.title().toStdString() << "' }";
return out.str();
}
void windowInformation()
{
auto windows = qGuiApp->allWindows();
std::vector<std::string> windowsAsJsObjects;
windowsAsJsObjects.reserve(windows.size());
std::transform(windows.begin(), windows.end(), std::back_inserter(windowsAsJsObjects),
[](const QWindow *window) { return windowToJSObject(*window); });
emscripten::val::global("window").call<void>("windowInformationCallback",
emscripten::val(toJSArray(windowsAsJsObjects)));
}
void screenInformation()
{
auto screens = qGuiApp->screens();
std::vector<std::string> screensAsJsObjects;
screensAsJsObjects.reserve(screens.size());
std::transform(screens.begin(), screens.end(), std::back_inserter(screensAsJsObjects),
[](const QScreen *screen) { return screenToJSObject(*screen); });
emscripten::val::global("window").call<void>("screenInformationCallback",
emscripten::val(toJSArray(screensAsJsObjects)));
}
void createWindow(int x, int y, int w, int h, std::string screenId, std::string title)
{
auto screens = qGuiApp->screens();
auto screen_it = std::find_if(screens.begin(), screens.end(), [&screenId](QScreen *screen) {
return screen->name() == QString::fromLatin1(screenId);
});
if (screen_it == screens.end()) {
qWarning() << "No such screen: " << screenId;
return;
}
auto *window = new DeleteOnCloseWindow;
window->setFlag(Qt::WindowTitleHint);
window->setFlag(Qt::WindowMaximizeButtonHint);
window->setTitle(QString::fromLatin1(title));
window->setGeometry(x, y, w, h);
window->setScreen(*screen_it);
window->showNormal();
}
EMSCRIPTEN_BINDINGS(qwasmwindow)
{
emscripten::function("screenInformation", &screenInformation);
emscripten::function("windowInformation", &windowInformation);
emscripten::function("createWindow", &createWindow);
}
int main(int argc, char **argv)
{
QGuiApplication app(argc, argv);
app.exec();
return 0;
}
#include "qwasmwindow_harness.moc"

View File

@ -0,0 +1,66 @@
<!doctype html>
<head>
<script type="text/javascript" src="qwasmwindow_harness.js"></script>
<script>
(async () => {
const instance = await createQtAppInstance({});
window.instance = instance;
const testSandbox = document.createElement('div');
testSandbox.id = 'test-sandbox';
document.body.appendChild(testSandbox);
const makeSizedDiv = (left, top, width, height) => {
const screenDiv = document.createElement('div');
screenDiv.style.left = `${left}px`;
screenDiv.style.top = `${top}px`;
screenDiv.style.width = `${width}px`;
screenDiv.style.height = `${height}px`;
screenDiv.style.backgroundColor = 'lightblue';
return screenDiv;
};
window.testSupport = {
initializeScreenWithFixedPosition: (left, top, width, height) => {
const screenDiv = makeSizedDiv(left, top, width, height);
testSandbox.appendChild(screenDiv);
screenDiv.style.position = 'fixed';
instance.qtAddContainerElement(screenDiv);
return screenDiv;
},
initializeScreenWithRelativePosition: (left, top, width, height) => {
const screenDiv = makeSizedDiv(left, top, width, height);
testSandbox.appendChild(screenDiv);
screenDiv.style.position = 'relative';
instance.qtAddContainerElement(screenDiv);
return screenDiv;
},
initializeScreenInScrollContainer:
(scrollWidth, scrollHeight, left, top, width, height) => {
const scrollContainer = document.createElement('div');
scrollContainer.style.height = `${scrollHeight}px`;
scrollContainer.style.width = `${scrollWidth}px`;
testSandbox.appendChild(scrollContainer);
const screenDiv = makeSizedDiv(left, top, width, height);
scrollContainer.appendChild(screenDiv);
screenDiv.style.position = 'relative';
instance.qtAddContainerElement(screenDiv);
return [scrollContainer, screenDiv];
}
};
})();
</script>
</head>
<body>
</body>

View File

@ -0,0 +1,23 @@
#! /bin/bash
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
set -m
function removeServer()
{
[ -z "$cleanupPid" ] || kill $cleanupPid
}
trap removeServer EXIT
script_dir=`dirname ${BASH_SOURCE[0]}`
cd "$script_dir"
python3 qtwasmserver.py -p 8001 > /dev/null 2>&1 &
cleanupPid=$!
python3 qwasmwindow.py $@
echo 'Press any key to continue...' >&2
read -n 1

View File

@ -20,7 +20,7 @@ trap removeServer EXIT
script_dir=`dirname ${BASH_SOURCE[0]}`
cd "$script_dir/../../../../"
python3 -m http.server 8001 &
python3 util/wasm/qtwasmserver/qtwasmserver.py -p 8001 &
cleanupPid=$!
cd -

View File

@ -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) {