summaryrefslogtreecommitdiffstats
path: root/tests/auto/wasm/selenium
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/wasm/selenium')
-rw-r--r--tests/auto/wasm/selenium/CMakeLists.txt83
-rw-r--r--tests/auto/wasm/selenium/fshader.glsl21
-rw-r--r--tests/auto/wasm/selenium/qwasmwindow.py1042
-rw-r--r--tests/auto/wasm/selenium/run.bat9
-rwxr-xr-xtests/auto/wasm/selenium/run.sh58
-rw-r--r--tests/auto/wasm/selenium/shaders.qrc6
-rw-r--r--tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp696
-rw-r--r--tests/auto/wasm/selenium/tst_qwasmwindow_harness.html80
-rw-r--r--tests/auto/wasm/selenium/vshader.glsl27
9 files changed, 2022 insertions, 0 deletions
diff --git a/tests/auto/wasm/selenium/CMakeLists.txt b/tests/auto/wasm/selenium/CMakeLists.txt
new file mode 100644
index 0000000000..445030878e
--- /dev/null
+++ b/tests/auto/wasm/selenium/CMakeLists.txt
@@ -0,0 +1,83 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qwasmwindow_harness LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+qt_internal_add_test(tst_qwasmwindow_harness
+ MANUAL
+ NO_BATCH
+ SOURCES
+ tst_qwasmwindow_harness.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::OpenGL
+ Qt::Widgets
+)
+
+# Resources:
+set(shaders_resource_files
+ "fshader.glsl"
+ "vshader.glsl"
+)
+
+qt6_add_resources(tst_qwasmwindow_harness "shaders"
+ PREFIX
+ "/"
+ FILES
+ ${shaders_resource_files}
+)
+
+if(CMAKE_HOST_WIN32)
+ SET(RUNSHCMD run.bat)
+ SET(RUNSHARG "NotUsed")
+else()
+ SET(RUNSHCMD bash)
+ SET(RUNSHARG run.sh)
+endif()
+
+set_target_properties(tst_qwasmwindow_harness PROPERTIES CROSSCOMPILING_EMULATOR "") # disabling emrun
+qt_internal_create_test_script(NAME tst_qwasmwindow_harness
+ COMMAND ${RUNSHCMD}
+ ARGS ${RUNSHARG}
+ WORKING_DIRECTORY "${test_working_dir}"
+ OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/tst_qwasmwindow_harnessWrapper$<CONFIG>.cmake"
+)
+
+add_custom_command(
+ TARGET tst_qwasmwindow_harness POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/tst_qwasmwindow_harness.html
+ ${CMAKE_CURRENT_BINARY_DIR}/tst_qwasmwindow_harness.html
+)
+
+add_custom_command(
+ TARGET tst_qwasmwindow_harness POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../../util/wasm/qtwasmserver/qtwasmserver.py
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmserver.py
+)
+
+add_custom_command(
+ TARGET tst_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 tst_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 tst_qwasmwindow_harness POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/run.bat
+ ${CMAKE_CURRENT_BINARY_DIR}/run.bat
+)
diff --git a/tests/auto/wasm/selenium/fshader.glsl b/tests/auto/wasm/selenium/fshader.glsl
new file mode 100644
index 0000000000..252735f91c
--- /dev/null
+++ b/tests/auto/wasm/selenium/fshader.glsl
@@ -0,0 +1,21 @@
+// Copyright (C) 2018 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifdef GL_ES
+// Set default precision to medium
+precision mediump int;
+precision mediump float;
+#endif
+
+uniform sampler2D texture;
+
+varying vec2 v_texcoord;
+
+//! [0]
+void main()
+{
+ // Set fragment color from texture
+ gl_FragColor = texture2D(texture, v_texcoord);
+}
+//! [0]
+
diff --git a/tests/auto/wasm/selenium/qwasmwindow.py b/tests/auto/wasm/selenium/qwasmwindow.py
new file mode 100644
index 0000000000..260e9d2d24
--- /dev/null
+++ b/tests/auto/wasm/selenium/qwasmwindow.py
@@ -0,0 +1,1042 @@
+# 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.chrome.service import Service as ChromeService
+from selenium.webdriver.common.action_chains import ActionChains
+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 webdriver_manager.chrome import ChromeDriverManager
+
+import time
+import unittest
+from enum import Enum, auto
+
+class WidgetTestCase(unittest.TestCase):
+ def setUp(self):
+ self._driver = Chrome(service=ChromeService(ChromeDriverManager().install()))
+ self._driver.get(
+ 'http://localhost:8001/tst_qwasmwindow_harness.html')
+ self._test_sandbox_element = WebDriverWait(self._driver, 30).until(
+ presence_of_element_located((By.ID, 'test-sandbox'))
+ )
+ self.addTypeEqualityFunc(Color, assert_colors_equal)
+ self.addTypeEqualityFunc(Rect, assert_rects_equal)
+
+ def test_hasFocus_returnsFalse_whenSetNoFocusShowWasCalled(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=600, height=1200)
+
+ w0 = Widget(self._driver, "w0")
+ w0.show()
+ self.assertEqual(w0.hasFocus(), True)
+
+ w1 = Widget(self._driver, "w1")
+ w1.setNoFocusShow()
+ w1.show()
+ self.assertEqual(w0.hasFocus(), True)
+ self.assertEqual(w1.hasFocus(), False)
+
+ w2 = Widget(self._driver, "w2")
+ w2.show()
+ self.assertEqual(w0.hasFocus(), False)
+ self.assertEqual(w1.hasFocus(), False)
+ self.assertEqual(w2.hasFocus(), True)
+
+ w3 = Widget(self._driver, "w3")
+ w3.setNoFocusShow()
+ w3.show()
+ self.assertEqual(w0.hasFocus(), False)
+ self.assertEqual(w1.hasFocus(), False)
+ self.assertEqual(w2.hasFocus(), True)
+ self.assertEqual(w3.hasFocus(), False)
+ w3.activate();
+ self.assertEqual(w0.hasFocus(), False)
+ self.assertEqual(w1.hasFocus(), False)
+ self.assertEqual(w2.hasFocus(), False)
+ self.assertEqual(w3.hasFocus(), True)
+
+ clearWidgets(self._driver)
+
+ def test_window_resizing(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=600, height=600)
+
+ window = Window(parent=screen, rect=Rect(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=65, height=80))
+
+ 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(parent=screen, rect=Rect(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(parent=screen, rect=Rect(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(parent=screen, rect=Rect(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(parent=screen, rect=Rect(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(parent=screen, rect=Rect(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, rect=Rect(x=50, y=50, width=100, height=100), title='First'),
+ Window(screen, rect=Rect(x=400, y=400, width=100, height=100), title='Second'),
+ Window(screen, rect=Rect(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))
+
+ #TODO FIX IN CI
+ @unittest.skip('Skip temporarily')
+ def test_multitouch_window_resize(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+ windows = [Window(screen, rect=Rect(x=50, y=50, width=150, height=150), title='First'),
+ Window(screen, rect=Rect(x=400, y=400, width=150, height=150), title='Second'),
+ Window(screen, rect=Rect(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 test_newly_created_window_gets_keyboard_focus(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+ window = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root')
+
+ ActionChains(self._driver).key_down('c').key_up('c').perform()
+
+ events = window.events
+ self.assertEqual(len(events), 2)
+ self.assertEqual(events[-2]['type'], 'keyPress')
+ self.assertEqual(events[-2]['key'], 'c')
+ self.assertEqual(events[-1]['type'], 'keyRelease')
+ self.assertEqual(events[-1]['key'], 'c')
+
+ #TODO FIX IN CI
+ @unittest.skip('Does not work in CI')
+ def test_child_window_activation(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+
+ root = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root')
+ w1 = Window(parent=root, rect=Rect(x=100, y=100, width=600, height=600), title='w1')
+ w1_w1 = Window(parent=w1, rect=Rect(x=100, y=100, width=300, height=300), title='w1_w1')
+ w1_w1_w1 = Window(parent=w1_w1, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w1_w1')
+ w1_w1_w2 = Window(parent=w1_w1, rect=Rect(x=150, y=150, width=100, height=100), title='w1_w1_w2')
+ w1_w2 = Window(parent=w1, rect=Rect(x=300, y=300, width=300, height=300), title='w1_w2')
+ w1_w2_w1 = Window(parent=w1_w2, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w2_w1')
+ w2 = Window(parent=root, rect=Rect(x=300, y=300, width=450, height=450), title='w2')
+
+ self.assertEqual(screen.window_stack_at_point(*w1_w1.bounding_box.center),
+ [w2, w1_w1_w2, w1_w1_w1, w1_w1, w1, root])
+
+ self.assertEqual(screen.window_stack_at_point(*w2.bounding_box.center),
+ [w2, w1_w2_w1, w1_w2, w1, root])
+
+ for w in [w1, w1_w1, w1_w1_w1, w1_w1_w2, w1_w2, w1_w2_w1]:
+ self.assertFalse(w.active)
+ self.assertTrue(w2.active)
+
+ w1.click(0, 0)
+
+ for w in [w1, w1_w2, w1_w2_w1]:
+ self.assertTrue(w.active)
+ for w in [w1_w1, w1_w1_w1, w1_w1_w2, w2]:
+ self.assertFalse(w.active)
+
+ self.assertEqual(screen.window_stack_at_point(*w2.bounding_box.center),
+ [w1_w2_w1, w1_w2, w1, w2, root])
+
+ w1_w1_w1.click(0, 0)
+
+ for w in [w1, w1_w1, w1_w1_w1]:
+ self.assertTrue(w.active)
+ for w in [w1_w1_w2, w1_w2, w1_w2_w1, w2]:
+ self.assertFalse(w.active)
+
+ self.assertEqual(screen.window_stack_at_point(*w1_w1_w1.bounding_box.center),
+ [w1_w1_w1, w1_w1_w2, w1_w1, w1, w2, root])
+
+ w1_w1_w2.click(w1_w1_w2.bounding_box.width, w1_w1_w2.bounding_box.height)
+
+ for w in [w1, w1_w1, w1_w1_w2]:
+ self.assertTrue(w.active)
+ for w in [w1_w1_w1, w1_w2, w1_w2_w1, w2]:
+ self.assertFalse(w.active)
+
+ self.assertEqual(screen.window_stack_at_point(w1_w1_w2.bounding_box.x, w1_w1_w2.bounding_box.y),
+ [w1_w1_w2, w1_w1_w1, w1_w1, w1, w2, root])
+
+ def test_window_reparenting(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+
+ bottom = Window(parent=screen, rect=Rect(x=800, y=800, width=300, height=300), title='bottom')
+ w1 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w1')
+ w2 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w2')
+ w3 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w3')
+
+ self.assertTrue(
+ w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ w2.set_parent(w1)
+
+ self.assertTrue(
+ w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ w3.set_parent(w2)
+
+ self.assertTrue(
+ w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ w2.set_parent(screen)
+
+ self.assertTrue(
+ w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ w1.set_parent(w2)
+
+ self.assertTrue(
+ w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ w3.set_parent(screen)
+
+ self.assertTrue(
+ w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ w2.set_parent(w3)
+
+ self.assertTrue(
+ w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w3.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+ self.assertTrue(
+ w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")])
+
+ def test_window_closing(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+
+ bottom = Window(parent=screen, rect=Rect(x=800, y=800, width=300, height=300), title='root')
+ bottom.close()
+
+ w1 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w1')
+ w2 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w2')
+ w3 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w3')
+
+ w3.close()
+
+ self.assertFalse(w3 in screen.query_windows())
+ self.assertTrue(w2 in screen.query_windows())
+ self.assertTrue(w1 in screen.query_windows())
+
+ w4 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w4')
+
+ self.assertTrue(w4 in screen.query_windows())
+ self.assertTrue(w2 in screen.query_windows())
+ self.assertTrue(w1 in screen.query_windows())
+
+ w2.close()
+ w1.close()
+
+ self.assertTrue(w4 in screen.query_windows())
+ self.assertFalse(w2 in screen.query_windows())
+ self.assertFalse(w1 in screen.query_windows())
+
+ w4.close()
+
+ self.assertFalse(w4 in screen.query_windows())
+
+ def test_window_painting(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+ bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=400, height=400), title='root')
+ bottom.set_background_color(Color(r=255, g=0, b=0))
+ wait_for_animation_frame(self._driver)
+
+ self.assertEqual(bottom.color_at(0, 0), Color(r=255, g=0, b=0))
+
+ w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1')
+ w1.set_background_color(Color(r=0, g=255, b=0))
+ wait_for_animation_frame(self._driver)
+
+ self.assertEqual(w1.color_at(0, 0), Color(r=0, g=255, b=0))
+
+ w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1')
+ w1_w1.set_parent(w1)
+ w1_w1.set_background_color(Color(r=0, g=0, b=255))
+ wait_for_animation_frame(self._driver)
+
+ self.assertEqual(w1_w1.color_at(0, 0), Color(r=0, g=0, b=255))
+
+ w1_w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200), title='w1_w1_w1')
+ w1_w1_w1.set_parent(w1_w1)
+ w1_w1_w1.set_background_color(Color(r=255, g=255, b=0))
+ wait_for_animation_frame(self._driver)
+
+ self.assertEqual(w1_w1_w1.color_at(0, 0), Color(r=255, g=255, b=0))
+
+ def test_opengl_painting(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+ bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=400, height=400), title='root',opengl=1)
+ bottom.set_background_color(Color(r=255, g=0, b=0))
+ wait_for_animation_frame(self._driver)
+ time.sleep(1)
+
+ self.assertEqual(bottom.window_color_at_0_0(), Color(r=255, g=0, b=0))
+
+ w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1', opengl=1)
+ w1.set_background_color(Color(r=0, g=255, b=0))
+ wait_for_animation_frame(self._driver)
+ time.sleep(1)
+
+ self.assertEqual(w1.window_color_at_0_0(), Color(r=0, g=255, b=0))
+
+ w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1', opengl=1)
+ w1_w1.set_parent(w1)
+ w1_w1.set_background_color(Color(r=0, g=0, b=255))
+ wait_for_animation_frame(self._driver)
+ time.sleep(1)
+
+ self.assertEqual(w1_w1.window_color_at_0_0(), Color(r=0, g=0, b=255))
+
+ w1_w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200), title='w1_w1_w1', opengl=1)
+ w1_w1_w1.set_parent(w1_w1)
+ w1_w1_w1.set_background_color(Color(r=255, g=255, b=0))
+ wait_for_animation_frame(self._driver)
+ time.sleep(1)
+
+ self.assertEqual(w1_w1_w1.window_color_at_0_0(), Color(r=255, g=255, b=0))
+
+#TODO FIX IN CI
+ @unittest.skip('Does not work in CI')
+ def test_keyboard_input(self):
+ screen = Screen(self._driver, ScreenPosition.FIXED,
+ x=0, y=0, width=800, height=800)
+
+ bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root')
+ w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1')
+ w1_w1 = Window(parent=w1, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1')
+ w1_w1_w1 = Window(parent=w1_w1, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w1_w1')
+ Window(parent=w1_w1, rect=Rect(x=150, y=150, width=100, height=100), title='w1_w1_w2')
+
+ w1_w1_w1.click(0, 0)
+
+ ActionChains(self._driver).key_down('c').key_up('c').perform()
+
+ events = w1_w1_w1.events
+ self.assertEqual(len(events), 2)
+ self.assertEqual(events[-2]['type'], 'keyPress')
+ self.assertEqual(events[-2]['key'], 'c')
+ self.assertEqual(events[-1]['type'], 'keyRelease')
+ self.assertEqual(events[-1]['key'], 'c')
+ self.assertEqual(len(w1_w1.events), 0)
+ self.assertEqual(len(w1.events), 0)
+
+ w1_w1.click(0, 0)
+
+ ActionChains(self._driver).key_down('b').key_up('b').perform()
+
+ events = w1_w1.events
+ self.assertEqual(len(events), 2)
+ self.assertEqual(events[-2]['type'], 'keyPress')
+ self.assertEqual(events[-2]['key'], 'b')
+ self.assertEqual(events[-1]['type'], 'keyRelease')
+ self.assertEqual(events[-1]['key'], 'b')
+ self.assertEqual(len(w1_w1_w1.events), 2)
+ self.assertEqual(len(w1.events), 0)
+
+ w1.click(0, 0)
+
+ ActionChains(self._driver).key_down('a').key_up('a').perform()
+
+ events = w1.events
+ self.assertEqual(len(events), 2)
+ self.assertEqual(events[-2]['type'], 'keyPress')
+ self.assertEqual(events[-2]['key'], 'a')
+ self.assertEqual(events[-1]['type'], 'keyRelease')
+ self.assertEqual(events[-1]['key'], 'a')
+ self.assertEqual(len(w1_w1_w1.events), 2)
+ self.assertEqual(len(w1_w1.events), 2)
+
+ def tearDown(self):
+ self._driver.quit()
+
+class ScreenPosition(Enum):
+ FIXED = auto()
+ RELATIVE = auto()
+ IN_SCROLL_CONTAINER = auto()
+
+class Screen:
+ def __init__(self, driver, positioning=None, x=None, y=None, width=None, height=None, container_width=0, container_height=0, screen_name=None):
+ self.driver = driver
+ if screen_name is not None:
+ 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]
+ self.element = driver.find_element(By.CSS_SELECTOR, f'#test-screen-1')
+ return
+
+ if positioning == ScreenPosition.FIXED:
+ command = f'initializeScreenWithFixedPosition({x}, {y}, {width}, {height})'
+ elif positioning == ScreenPosition.RELATIVE:
+ command = f'initializeScreenWithRelativePosition({x}, {y}, {width}, {height})'
+ elif positioning == ScreenPosition.IN_SCROLL_CONTAINER:
+ command = f'initializeScreenInScrollContainer({container_width}, {container_height}, {x}, {y}, {width}, {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'])
+
+ @property
+ def name(self):
+ return self.screen_info['name']
+
+ def scroll_to(self):
+ ActionChains(self.driver).scroll_to_element(self.element).perform()
+
+ def hit_test_point(self, x, y):
+ return self.driver.execute_script(
+ f'''
+ return testSupport.hitTestPoint({x}, {y}, '{self.element.get_attribute("id")}');
+ '''
+ )
+
+ def window_stack_at_point(self, x, y):
+ return [
+ Window(self, element=element) for element in [
+ *filter(lambda elem: (elem.get_attribute('id') if elem.get_attribute('id') is not None else '')
+ .startswith('qt-window-'), self.hit_test_point(x, y))]]
+
+ def query_windows(self):
+ shadow_container = self.element.find_element(By.CSS_SELECTOR, f'#qt-shadow-container')
+ return [
+ Window(self, element=element) for element in shadow_container.shadow_root.find_elements(
+ By.CSS_SELECTOR, f'div#{self.name} > div.qt-window')]
+
+ def find_element(self, method, query):
+ shadow_container = self.element.find_element(By.CSS_SELECTOR, f'#qt-shadow-container')
+ return shadow_container.shadow_root.find_element(method, query)
+
+def clearWidgets(driver):
+ driver.execute_script(
+ f'''
+ instance.clearWidgets();
+ '''
+ )
+
+class Widget:
+ def __init__(self, driver, name):
+ self.name=name
+ self.driver=driver
+
+ self.driver.execute_script(
+ f'''
+ instance.createWidget('{self.name}');
+ '''
+ )
+
+ def setNoFocusShow(self):
+ self.driver.execute_script(
+ f'''
+ instance.setWidgetNoFocusShow('{self.name}');
+ '''
+ )
+
+ def show(self):
+ self.driver.execute_script(
+ f'''
+ instance.showWidget('{self.name}');
+ '''
+ )
+ def hasFocus(self):
+ focus = call_instance_function_arg(self.driver, 'hasWidgetFocus', self.name)
+ return focus
+
+ def activate(self):
+ self.driver.execute_script(
+ f'''
+ instance.activateWidget('{self.name}');
+ '''
+ )
+
+
+class Window:
+ def __init__(self, parent=None, rect=None, title=None, element=None, visible=True, opengl=0):
+ self.driver = parent.driver
+ self.opengl = opengl
+ if element is not None:
+ self.element = element
+ self.title = element.find_element(
+ By.CSS_SELECTOR, f'.title-bar > .window-name').get_property("textContent")
+ information = self.__window_information()
+ self.screen = Screen(self.driver, screen_name=information['screen']['name'])
+ pass
+ else:
+ self.title = title = title if title is not None else 'window'
+ if isinstance(parent, Window):
+ self.driver.execute_script(
+ f'''
+ instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'window', '{parent.title}', '{title}', {opengl});
+ '''
+ )
+ self.screen = parent.screen
+ else:
+ assert(isinstance(parent, Screen))
+ self.driver.execute_script(
+ f'''
+ instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'screen', '{parent.name}', '{title}', {opengl});
+ '''
+ )
+ self.screen = parent
+ self._window_id = self.__window_information()['id']
+ self.element = self.screen.find_element(
+ By.CSS_SELECTOR, f'#qt-window-{self._window_id}')
+ if visible:
+ self.set_visible(True)
+
+ def __eq__(self, other):
+ return self._window_id == other._window_id if isinstance(other, Window) else False
+
+ 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'])
+
+ @property
+ def events(self):
+ events = self.driver.execute_script(
+ f'''
+ return testSupport.events();
+ '''
+ )
+ return [*filter(lambda e: e['windowTitle'] == self.title, events)]
+
+ def set_visible(self, visible):
+ info = self.__window_information()
+ self.driver.execute_script(
+ f'''instance.setWindowVisible({info['id']}, {'true' if visible else 'false'});''')
+
+ 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}
+
+ @property
+ def bounding_box(self):
+ raw = self.driver.execute_script("""
+ return arguments[0].getBoundingClientRect();
+ """, self.element)
+ return Rect(raw['x'], raw['y'], raw['width'], raw['height'])
+
+ @property
+ def active(self):
+ return not self.inactive
+ # self.assertFalse('inactive' in window_element.get_attribute(
+ # 'class').split(' '), window_element.get_attribute('id'))
+
+ @property
+ def inactive(self):
+ window_chain = [
+ *self.element.find_elements(By.XPATH, "ancestor::div"), self.element]
+ return next(filter(lambda elem: 'qt-window' in elem.get_attribute('class').split(' ') and
+ 'inactive' in elem.get_attribute(
+ 'class').split(' '),
+ window_chain
+ ), None) is not None
+
+ def click(self, x, y):
+ rect = self.bounding_box
+
+ SELENIUM_IMPRECISION_COMPENSATION = 2
+ ActionChains(self.driver).move_to_element(
+ self.element).move_by_offset(-rect.width / 2 + x + SELENIUM_IMPRECISION_COMPENSATION,
+ -rect.height / 2 + y + SELENIUM_IMPRECISION_COMPENSATION).click().perform()
+
+ def set_parent(self, parent):
+ if isinstance(parent, Screen):
+ # TODO won't work with screen that is not parent.screen
+ self.screen = parent
+ self.driver.execute_script(
+ f'''
+ instance.setWindowParent('{self.title}', 'none');
+ '''
+ )
+ else:
+ assert(isinstance(parent, Window))
+ self.screen = parent.screen
+ self.driver.execute_script(
+ f'''
+ instance.setWindowParent('{self.title}', '{parent.title}');
+ '''
+ )
+
+ def close(self):
+ self.driver.execute_script(
+ f'''
+ instance.closeWindow('{self.title}');
+ '''
+ )
+
+ def window_color_at_0_0(self):
+ color = call_instance_function_arg(self.driver, 'getOpenGLColorAt_0_0', self.title)
+
+ wcol = color[0]
+ r = wcol['r']
+ g = wcol['g']
+ b = wcol['b']
+
+ return Color(r,g,b)
+
+ def color_at(self, x, y):
+ raw = self.driver.execute_script(
+ f'''
+ return arguments[0].querySelector('canvas')
+ .getContext('2d').getImageData({x}, {y}, 1, 1).data;
+ ''', self.element)
+ return Color(r=raw[0], g=raw[1], b=raw[2])
+
+ def set_background_color(self, color):
+ return self.driver.execute_script(
+ f'''
+ return instance.setWindowBackgroundColor('{self.title}', {color.r}, {color.g}, {color.b});
+ '''
+ )
+
+
+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);''')
+
+def call_instance_function_arg(driver, name, arg):
+ return driver.execute_script(
+ f'''let result;
+ window.{name}Callback = data => result = data;
+ instance.{name}('{arg}');
+ return eval(result);''')
+
+def wait_for_animation_frame(driver):
+ driver.execute_script(
+ '''
+ window.requestAnimationFrame(() => {
+ const sync = document.createElement('div');
+ sync.id = 'test-sync';
+ document.body.appendChild(sync);
+ });
+ '''
+ )
+ WebDriverWait(driver, 1).until(
+ presence_of_element_located((By.ID, 'test-sync'))
+ )
+ driver.execute_script(
+ '''
+ document.body.removeChild(document.body.querySelector('#test-sync'));
+ '''
+ )
+
+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 Color:
+ def __init__(self, r, g, b):
+ self.r = r
+ self.g = g
+ self.b = b
+
+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})'
+
+ @property
+ def center(self):
+ return self.x + self.width / 2, self.y + self.height / 2,
+
+def assert_colors_equal(color1, color2, msg=None):
+ if color1.r != color2.r or color1.g != color2.g or color1.b != color2.b:
+ raise AssertionError(f'Colors not equal: \n{color1} \nvs \n{color2}')
+
+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()
diff --git a/tests/auto/wasm/selenium/run.bat b/tests/auto/wasm/selenium/run.bat
new file mode 100644
index 0000000000..56305c72c3
--- /dev/null
+++ b/tests/auto/wasm/selenium/run.bat
@@ -0,0 +1,9 @@
+:: Copyright (C) 2024 The Qt Company Ltd.
+:: SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+::
+:: The highest version of python that can be used is 3.11
+:: Download from here: https://www.python.org/downloads/release/python-3118/
+::
+start "qtwasmserver.py" python qtwasmserver.py -p 8001
+python qwasmwindow.py
+taskkill /FI "WINDOWTITLE eq qtwasmserver.py"
diff --git a/tests/auto/wasm/selenium/run.sh b/tests/auto/wasm/selenium/run.sh
new file mode 100755
index 0000000000..b9a41ace8c
--- /dev/null
+++ b/tests/auto/wasm/selenium/run.sh
@@ -0,0 +1,58 @@
+#! /bin/bash
+
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+set -m
+
+function removeServer()
+{
+ [ -z "$cleanupPid" ] || kill $cleanupPid
+}
+trap removeServer EXIT
+
+function compare_python_versions() {
+ python_version=$1
+ required_version=$2
+ if [ "$(printf "%s\n" "$required_version" "$python_version" | sort -V | head -n 1)" != "$required_version" ]; then
+ return 0 # python version is too old
+ else
+ return 1 # python version is legit
+ fi
+}
+
+python_command="python3"
+# At least python 3.7 is required for Selenium 4
+if command -v python3 &> /dev/null; then
+ python_version=$(python3 --version 2>&1 | awk '{print $2}')
+
+ if compare_python_versions "$python_version" "3.7"; then # if Python is older then 3.7, look for newer versions
+ newer_python=""
+ for version in 3.7 3.8 3.9 3.10 3.11; do
+ potential_python=$(command -v "python$version")
+ if [ -n "$potential_python" ]; then
+ newer_python=$potential_python
+ newer_version=$($newer_python --version 2>&1 | awk '{print $2}')
+ break
+ fi
+ done
+
+ if [ -n "$newer_python" ]; then # if newer version is found, use it instead
+ newer_version=$($newer_python --version 2>&1 | awk '{print $2}')
+ python_command=$newer_python
+ else
+ echo "Error: At least Python3.7 is required, currently installed version: Python$python_version"
+ exit 1
+ fi
+ fi
+else
+ echo "Error: Python3 not installed"
+ exit 1
+fi
+
+script_dir=`dirname ${BASH_SOURCE[0]}`
+cd "$script_dir"
+$python_command qtwasmserver.py -p 8001 > /dev/null 2>&1 &
+cleanupPid=$!
+
+$python_command qwasmwindow.py $@
diff --git a/tests/auto/wasm/selenium/shaders.qrc b/tests/auto/wasm/selenium/shaders.qrc
new file mode 100644
index 0000000000..bfc4b25111
--- /dev/null
+++ b/tests/auto/wasm/selenium/shaders.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/">
+ <file>vshader.glsl</file>
+ <file>fshader.glsl</file>
+ </qresource>
+</RCC>
diff --git a/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp
new file mode 100644
index 0000000000..365fc74a34
--- /dev/null
+++ b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp
@@ -0,0 +1,696 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/QEvent>
+#include <QtWidgets/qwidget.h>
+
+#include <QtGui/qevent.h>
+#include <QtCore/qobject.h>
+#include <QtCore/qregularexpression.h>
+#include <QtGui/qpainter.h>
+#include <QtGui/qrasterwindow.h>
+#include <QtGui/qscreen.h>
+#include <QtGui/qwindow.h>
+#include <QtGui/qguiapplication.h>
+#include <QtWidgets/qlineedit.h>
+#include <QApplication>
+#include <QDialog>
+#include <QSysInfo>
+
+#include <QOpenGLWindow>
+#include <QOpenGLFunctions>
+#include <QOpenGLShaderProgram>
+
+#include <emscripten.h>
+#include <emscripten/bind.h>
+#include <emscripten/val.h>
+
+#include <memory>
+#include <sstream>
+#include <vector>
+
+class TestWindowBase
+{
+public:
+ virtual ~TestWindowBase() {}
+ virtual void setBackgroundColor(int r, int g, int b) = 0;
+ virtual void setVisible(bool visible) = 0;
+ virtual void setParent(QWindow *parent) = 0;
+ virtual bool close() = 0;
+ virtual QWindow *qWindow() = 0;
+ virtual void opengl_color_at_0_0(int *r, int *g, int *b) = 0;
+};
+
+class TestWidget : public QDialog
+{
+ Q_OBJECT
+};
+
+class TestWindow : public QRasterWindow, public TestWindowBase
+{
+ Q_OBJECT
+
+public:
+ virtual void setBackgroundColor(int r, int g, int b) override final
+ {
+ m_backgroundColor = QColor::fromRgb(r, g, b);
+ update();
+ }
+ virtual void setVisible(bool visible) override final
+ {
+ QRasterWindow::setVisible(visible);
+ }
+ virtual void setParent(QWindow *parent) override final
+ {
+ QRasterWindow::setParent(parent);
+ }
+ virtual bool close() override final
+ {
+ return QRasterWindow::close();
+ }
+ virtual QWindow *qWindow() override final
+ {
+ return static_cast<QRasterWindow *>(this);
+ }
+ virtual void opengl_color_at_0_0(int *r, int *g, int *b) override final
+ {
+ *r = 0;
+ *g = 0;
+ *b = 0;
+ }
+
+private:
+ void closeEvent(QCloseEvent *ev) override final
+ {
+ Q_UNUSED(ev);
+ delete this;
+ }
+
+ void keyPressEvent(QKeyEvent *event) final
+ {
+ auto data = emscripten::val::object();
+ data.set("type", emscripten::val("keyPress"));
+ data.set("windowId", emscripten::val(winId()));
+ data.set("windowTitle", emscripten::val(title().toStdString()));
+ data.set("key", emscripten::val(event->text().toStdString()));
+ emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data));
+ }
+
+ void keyReleaseEvent(QKeyEvent *event) final
+ {
+ auto data = emscripten::val::object();
+ data.set("type", emscripten::val("keyRelease"));
+ data.set("windowId", emscripten::val(winId()));
+ data.set("windowTitle", emscripten::val(title().toStdString()));
+ data.set("key", emscripten::val(event->text().toStdString()));
+ emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data));
+ }
+
+ void paintEvent(QPaintEvent *e) final
+ {
+ QPainter painter(this);
+ painter.fillRect(e->rect(), m_backgroundColor);
+ }
+
+ QColor m_backgroundColor = Qt::white;
+};
+
+class ContextGuard
+{
+public:
+ ContextGuard(QOpenGLContext *context, QSurface *surface) : m_context(context)
+ {
+ m_contextMutex.lock();
+ m_context->makeCurrent(surface);
+ }
+
+ ~ContextGuard()
+ {
+ m_context->doneCurrent();
+ m_contextMutex.unlock();
+ }
+
+private:
+ QOpenGLContext *m_context = nullptr;
+ static std::mutex m_contextMutex;
+};
+
+std::mutex ContextGuard::m_contextMutex;
+
+class TestOpenGLWindow : public QWindow, QOpenGLFunctions, public TestWindowBase
+{
+ Q_OBJECT
+
+public:
+ TestOpenGLWindow()
+ {
+ setSurfaceType(OpenGLSurface);
+ create();
+
+ //
+ // Create the texture in the share context
+ //
+ m_shareContext = std::make_shared<QOpenGLContext>();
+ m_shareContext->create();
+
+ {
+ ContextGuard guard(m_shareContext.get(), this);
+ initializeOpenGLFunctions();
+
+ m_shaderProgram = std::make_shared<QOpenGLShaderProgram>();
+
+ if (!m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl")
+ || !m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment,
+ ":/fshader.glsl")
+ || !m_shaderProgram->link() || !m_shaderProgram->bind()) {
+
+ qDebug() << " Build problem";
+ qDebug() << "Log " << m_shaderProgram->log();
+
+ m_shaderProgram = nullptr;
+ } else {
+ m_shaderProgram->setUniformValue("texture", 0);
+ }
+
+ //
+ // Texture
+ //
+ glGenTextures(1, &m_TextureId);
+ glBindTexture(GL_TEXTURE_2D, m_TextureId);
+
+ uint8_t pixel[4] = { 255, 255, 255, 128 };
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+
+ const GLfloat triangleData[] = { -1.0, -1.0, 0.0, 0.5, 0.5, 1.0, -1.0, 0.0,
+ 0.5, 0.5, -1.0, 1.0, 0.0, 0.5, 0.5 };
+ const GLushort indices[] = { 0, 1, 2 };
+
+ glGenBuffers(1, &m_vertexBufferId);
+ glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(float[5]) * 3, &triangleData, GL_STATIC_DRAW);
+
+ glGenBuffers(1, &m_indexBufferId);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * 3, indices, GL_STATIC_DRAW);
+
+ glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
+
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float[5]), 0);
+
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float[5]), (void *)(12));
+ }
+
+ //
+ // We will use the texture in this context
+ //
+ m_context = std::make_shared<QOpenGLContext>();
+ m_context->setShareContext(m_shareContext.get());
+ m_context->create();
+
+ {
+ ContextGuard guard(m_context.get(), this);
+ initializeOpenGLFunctions();
+
+ glBindTexture(GL_TEXTURE_2D, m_TextureId);
+ glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId);
+ m_shaderProgram->bind();
+
+ // Tell OpenGL programmable pipeline how to locate vertex position data
+ const int vertexLocation = m_shaderProgram->attributeLocation("a_position");
+ m_shaderProgram->enableAttributeArray(vertexLocation);
+ m_shaderProgram->setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, sizeof(float[5]));
+
+ // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data
+ const int texcoordLocation = m_shaderProgram->attributeLocation("a_texcoord");
+ m_shaderProgram->enableAttributeArray(texcoordLocation);
+ m_shaderProgram->setAttributeBuffer(texcoordLocation, GL_FLOAT, sizeof(float[3]), 2,
+ sizeof(float[5]));
+ }
+
+ renderLater();
+ }
+
+public:
+ virtual void setBackgroundColor(int red, int green, int blue) override final
+ {
+ {
+ ContextGuard guard(m_shareContext.get(), this);
+
+ //
+ // Update texture
+ //
+ const uint8_t pixel[4] = { (uint8_t)red, (uint8_t)green, (uint8_t)blue, 128 };
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+ }
+
+ renderLater();
+ }
+ virtual void setVisible(bool visible) override final { QWindow::setVisible(visible); }
+ virtual void setParent(QWindow *parent) override final { QWindow::setParent(parent); }
+ virtual bool close() override final { return QWindow::close(); }
+ virtual QWindow *qWindow() override final { return static_cast<QWindow *>(this); }
+ virtual void opengl_color_at_0_0(int *r, int *g, int *b) override final
+ {
+ ContextGuard guard(m_context.get(), this);
+
+ *r = m_rgba[0];
+ *g = m_rgba[1];
+ *b = m_rgba[2];
+ }
+
+private:
+ bool event(QEvent *event) override final
+ {
+ switch (event->type()) {
+ case QEvent::UpdateRequest:
+ renderNow();
+ return true;
+ default:
+ return QWindow::event(event);
+ }
+ }
+
+ void exposeEvent(QExposeEvent *event) override final
+ {
+ Q_UNUSED(event);
+
+ if (isExposed())
+ renderNow();
+ }
+
+ void closeEvent(QCloseEvent *ev) override final
+ {
+ Q_UNUSED(ev);
+ delete this;
+ }
+
+ void keyPressEvent(QKeyEvent *event) override final
+ {
+ auto data = emscripten::val::object();
+ data.set("type", emscripten::val("keyPress"));
+ data.set("windowId", emscripten::val(winId()));
+ data.set("windowTitle", emscripten::val(title().toStdString()));
+ data.set("key", emscripten::val(event->text().toStdString()));
+ emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data));
+ }
+
+ void keyReleaseEvent(QKeyEvent *event) override final
+ {
+ auto data = emscripten::val::object();
+ data.set("type", emscripten::val("keyRelease"));
+ data.set("windowId", emscripten::val(winId()));
+ data.set("windowTitle", emscripten::val(title().toStdString()));
+ data.set("key", emscripten::val(event->text().toStdString()));
+ emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data));
+ }
+ void renderLater() { requestUpdate(); }
+ void renderNow()
+ {
+ qDebug() << " Render now";
+ ContextGuard guard(m_context.get(), this);
+ const auto sz = size();
+ glViewport(0, 0, sz.width(), sz.height());
+
+ glClearColor(1, 1, 1, 1);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Draw triangle using indices from VBO
+ glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, nullptr);
+
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, m_rgba);
+ m_context->swapBuffers(this);
+ }
+
+private:
+ std::shared_ptr<QOpenGLShaderProgram> m_shaderProgram;
+ GLuint m_vertexBufferId = 0;
+ GLuint m_indexBufferId = 0;
+ GLuint m_TextureId = 0;
+
+ std::shared_ptr<QOpenGLContext> m_shareContext;
+ std::shared_ptr<QOpenGLContext> m_context;
+ uint8_t m_rgba[4]; // Color at location(0, 0)
+};
+
+namespace {
+TestWindowBase *findWindowByTitle(const std::string &title)
+{
+ auto windows = qGuiApp->allWindows();
+ auto window_it = std::find_if(windows.begin(), windows.end(), [&title](QWindow *window) {
+ return window->title() == QString::fromLatin1(title);
+ });
+ return window_it == windows.end() ? nullptr : dynamic_cast<TestWindowBase *>(*window_it);
+}
+
+class WidgetStorage
+{
+private:
+ ~WidgetStorage()
+ {
+ }
+public:
+ static WidgetStorage *getInstance()
+ {
+ if (!s_instance)
+ {
+ s_instance = new WidgetStorage();
+ }
+ return s_instance;
+ }
+ static void clearInstance()
+ {
+ delete s_instance;
+ s_instance = nullptr;
+ }
+
+ TestWidget *findWidget(const std::string &name)
+ {
+ auto it = m_widgets.find(name);
+ if (it != m_widgets.end())
+ return it->second.get();
+ return nullptr;
+ }
+
+ QLineEdit *findEdit(const std::string &name)
+ {
+ auto it = m_lineEdits.find(name);
+ if (it != m_lineEdits.end())
+ return it->second;
+ return nullptr;
+ }
+
+ void make(const std::string &name)
+ {
+ auto widget = std::make_shared<TestWidget>();
+ auto *lineEdit = new QLineEdit(widget.get());
+
+ widget->setMinimumSize(200, 200);
+ widget->setMaximumSize(200, 200);
+ widget->setGeometry(0, m_widgetY, 200, 200);
+ m_widgetY += 200;
+
+ lineEdit->setText("Hello world");
+
+ m_widgets[name] = widget;
+ m_lineEdits[name] = lineEdit;
+ }
+
+private:
+ using TestWidgetPtr = std::shared_ptr<TestWidget>;
+
+ static WidgetStorage * s_instance;
+ std::map<std::string, TestWidgetPtr> m_widgets;
+ std::map<std::string, QLineEdit *> m_lineEdits;
+ int m_widgetY = 0;
+};
+
+WidgetStorage *WidgetStorage::s_instance = nullptr;
+
+} // namespace
+
+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()) << ","
+ << " screen: " << screenToJSObject(*window.screen()) << ","
+ << " 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 createWidget(const std::string &name)
+{
+ WidgetStorage::getInstance()->make(name);
+}
+
+void setWidgetNoFocusShow(const std::string &name)
+{
+ auto w = WidgetStorage::getInstance()->findWidget(name);
+ if (w)
+ w->setAttribute(Qt::WA_ShowWithoutActivating);
+}
+
+void showWidget(const std::string &name)
+{
+ auto w = WidgetStorage::getInstance()->findWidget(name);
+ if (w)
+ w->show();
+}
+
+void hasWidgetFocus(const std::string &name)
+{
+ bool focus = false;
+ auto le = WidgetStorage::getInstance()->findEdit(name);
+ if (le)
+ focus = le->hasFocus();
+
+ emscripten::val::global("window").call<void>("hasWidgetFocusCallback",
+ emscripten::val(focus));
+}
+
+void activateWidget(const std::string &name)
+{
+ auto w = WidgetStorage::getInstance()->findWidget(name);
+ if (w)
+ w->activateWindow();
+}
+
+void clearWidgets()
+{
+ WidgetStorage::clearInstance();
+}
+
+void createWindow(int x, int y, int w, int h, const std::string &parentType, const std::string &parentId,
+ const std::string &title, bool opengl)
+{
+ QScreen *parentScreen = nullptr;
+ QWindow *parentWindow = nullptr;
+ if (parentType == "screen") {
+ auto screens = qGuiApp->screens();
+ auto screen_it = std::find_if(screens.begin(), screens.end(), [&parentId](QScreen *screen) {
+ return screen->name() == QString::fromLatin1(parentId);
+ });
+ if (screen_it == screens.end()) {
+ qWarning() << "No such screen: " << parentId;
+ return;
+ }
+ parentScreen = *screen_it;
+ } else if (parentType == "window") {
+ auto testWindow = findWindowByTitle(parentId);
+
+ if (!testWindow) {
+ qWarning() << "No parent window: " << parentId;
+ return;
+ }
+ parentWindow = testWindow->qWindow();
+ parentScreen = parentWindow->screen();
+ } else {
+ qWarning() << "Wrong parent type " << parentType;
+ return;
+ }
+
+ if (opengl) {
+ qDebug() << "Making OpenGL window";
+ auto window = new TestOpenGLWindow;
+ window->setFlag(Qt::WindowTitleHint);
+ window->setFlag(Qt::WindowMaximizeButtonHint);
+ window->setTitle(QString::fromLatin1(title));
+ window->setGeometry(x, y, w, h);
+ window->setScreen(parentScreen);
+ window->setParent(parentWindow);
+ } else {
+ qDebug() << "Making Raster window";
+ auto window = new TestWindow;
+ window->setFlag(Qt::WindowTitleHint);
+ window->setFlag(Qt::WindowMaximizeButtonHint);
+ window->setTitle(QString::fromLatin1(title));
+ window->setGeometry(x, y, w, h);
+ window->setScreen(parentScreen);
+ window->setParent(parentWindow);
+ }
+}
+
+void setWindowBackgroundColor(const std::string &title, int r, int g, int b)
+{
+ auto *window = findWindowByTitle(title);
+ if (!window) {
+ qWarning() << "No such window: " << title;
+ return;
+ }
+ window->setBackgroundColor(r, g, b);
+}
+
+void setWindowVisible(int windowId, bool visible)
+{
+ auto windows = qGuiApp->allWindows();
+ auto window_it = std::find_if(windows.begin(), windows.end(), [windowId](QWindow *window) {
+ return window->winId() == WId(windowId);
+ });
+ if (window_it == windows.end()) {
+ qWarning() << "No such window: " << windowId;
+ return;
+ }
+
+ (*window_it)->setVisible(visible);
+}
+
+void setWindowParent(const std::string &windowTitle, const std::string &parentTitle)
+{
+ TestWindowBase *window = findWindowByTitle(windowTitle);
+ if (!window) {
+ qWarning() << "Window could not be found " << windowTitle;
+ return;
+ }
+ TestWindowBase *parent = nullptr;
+ if (parentTitle != "none") {
+ if ((parent = findWindowByTitle(parentTitle)) == nullptr) {
+ qWarning() << "Parent window could not be found " << parentTitle;
+ return;
+ }
+ }
+ window->setParent(parent ? parent->qWindow() : nullptr);
+}
+
+bool closeWindow(const std::string &title)
+{
+ TestWindowBase *window = findWindowByTitle(title);
+ return window ? window->close() : false;
+}
+
+std::string colorToJs(int r, int g, int b)
+{
+ return
+ "[{"
+ " r: " + std::to_string(r) + ","
+ " g: " + std::to_string(g) + ","
+ " b: " + std::to_string(b) + ""
+ "}]";
+}
+
+void getOpenGLColorAt_0_0(const std::string &windowTitle)
+{
+ TestWindowBase *window = findWindowByTitle(windowTitle);
+ int r = 0;
+ int g = 0;
+ int b = 0;
+
+ if (!window) {
+ qWarning() << "Window could not be found " << windowTitle;
+ } else {
+ window->opengl_color_at_0_0(&r, &g, &b);
+ }
+
+ emscripten::val::global("window").call<void>("getOpenGLColorAt_0_0Callback",
+ emscripten::val(colorToJs(r, g, b)));
+}
+
+EMSCRIPTEN_BINDINGS(qwasmwindow)
+{
+ emscripten::function("screenInformation", &screenInformation);
+ emscripten::function("windowInformation", &windowInformation);
+
+ emscripten::function("createWindow", &createWindow);
+ emscripten::function("setWindowVisible", &setWindowVisible);
+ emscripten::function("setWindowParent", &setWindowParent);
+ emscripten::function("closeWindow", &closeWindow);
+ emscripten::function("setWindowBackgroundColor", &setWindowBackgroundColor);
+
+ emscripten::function("getOpenGLColorAt_0_0", &getOpenGLColorAt_0_0);
+
+ emscripten::function("createWidget", &createWidget);
+ emscripten::function("setWidgetNoFocusShow", &setWidgetNoFocusShow);
+ emscripten::function("showWidget", &showWidget);
+ emscripten::function("activateWidget", &activateWidget);
+ emscripten::function("hasWidgetFocus", &hasWidgetFocus);
+ emscripten::function("clearWidgets", &clearWidgets);
+}
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ app.exec();
+ return 0;
+}
+
+#include "tst_qwasmwindow_harness.moc"
diff --git a/tests/auto/wasm/selenium/tst_qwasmwindow_harness.html b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.html
new file mode 100644
index 0000000000..8c2457f302
--- /dev/null
+++ b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.html
@@ -0,0 +1,80 @@
+<!doctype html>
+
+<head>
+ <script type="text/javascript" src="tst_qwasmwindow_harness.js"></script>
+ <script>
+ (async () => {
+ const instance = await tst_qwasmwindow_harness_entry({});
+ window.instance = instance;
+
+ const testSandbox = document.createElement('div');
+ testSandbox.id = 'test-sandbox';
+ let nextScreenId = 1;
+ document.body.appendChild(testSandbox);
+
+ const eventList = [];
+
+ 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';
+ screenDiv.id = `test-screen-${nextScreenId++}`;
+
+ 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];
+ },
+ reportEvent: event => {
+ eventList.push(event);
+ },
+ events: () => eventList,
+ hitTestPoint: (x, y, screenId) => {
+ return document
+ .querySelector(`#${screenId}`)
+ .querySelector('#qt-shadow-container')
+ .shadowRoot.elementsFromPoint(x, y);
+ }
+ };
+ })();
+ </script>
+</head>
+
+<body>
+</body>
diff --git a/tests/auto/wasm/selenium/vshader.glsl b/tests/auto/wasm/selenium/vshader.glsl
new file mode 100644
index 0000000000..95e2bab607
--- /dev/null
+++ b/tests/auto/wasm/selenium/vshader.glsl
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifdef GL_ES
+// Set default precision to medium
+precision mediump int;
+precision mediump float;
+#endif
+
+uniform mat4 mvp_matrix;
+
+attribute vec4 a_position;
+attribute vec2 a_texcoord;
+
+varying vec2 v_texcoord;
+
+//! [0]
+void main()
+{
+ // Calculate vertex position in screen space
+ gl_Position = a_position;
+
+ // Pass texture coordinate to fragment shader
+ // Value will be automatically interpolated to fragments inside polygon faces
+ v_texcoord = a_texcoord;
+}
+//! [0]