diff options
Diffstat (limited to 'tests/auto/wasm/selenium')
-rw-r--r-- | tests/auto/wasm/selenium/CMakeLists.txt | 83 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/fshader.glsl | 21 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/qwasmwindow.py | 1042 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/run.bat | 9 | ||||
-rwxr-xr-x | tests/auto/wasm/selenium/run.sh | 58 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/shaders.qrc | 6 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp | 696 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/tst_qwasmwindow_harness.html | 80 | ||||
-rw-r--r-- | tests/auto/wasm/selenium/vshader.glsl | 27 |
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] |