diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 79cb464..a7d26be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,14 +15,11 @@ jobs: strategy: fail-fast: false matrix: - CONDA_PY: ["36", "37", "38", "39"] + #python: ["3.6", "3.7", "3.8", "3.9"] + python: ["3.6"] steps: - uses: actions/checkout@v2 - # We need to install Qt on the system otherwise tests in - # tests/test_qt_js_integration.py crash. - - name: Install Qt - uses: jurplel/install-qt-action@v2 - uses: conda-incubator/setup-miniconda@v2 with: auto-update-conda: true @@ -32,7 +29,7 @@ jobs: channel-priority: true - name: Install env: - CONDA_PY: ${{ matrix.CONDA_PY }} + PYTHON_VERSION: ${{ matrix.python }} run: | conda config --system --set always_yes yes --set changeps1 no conda install -c conda-forge conda-devenv @@ -40,6 +37,7 @@ jobs: export TEST_QMXGRAPH=1 conda devenv -n qmxgraph conda install -n qmxgraph coveralls pytest-cov + conda list -n qmxgraph conda init bash - name: Tests shell: bash -l {0} diff --git a/.gitignore b/.gitignore index 9cafdac..c06cd28 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ resource_*.py environment.yml docs_environment.yml esss_environment.yml +mxgraph/ diff --git a/_base_environment.devenv.yml b/_base_environment.devenv.yml index 3dffab8..d30969c 100644 --- a/_base_environment.devenv.yml +++ b/_base_environment.devenv.yml @@ -11,7 +11,7 @@ dependencies: - oop-ext - pip - pre-commit - - pyqt =5.12 + - pyqt >=5.12 environment: PYTHONPATH: diff --git a/environment.devenv.yml b/environment.devenv.yml index f29d696..89985d7 100644 --- a/environment.devenv.yml +++ b/environment.devenv.yml @@ -1,6 +1,5 @@ {% set TEST_QMXGRAPH = os.environ.get('TEST_QMXGRAPH', '0') != '0' %} -{% set PYTHON_VERSION = os.environ.get('PYTHON_VERSION', '3.6') %} -{% set PYTHON_VERSION = os.environ.get('TRAVIS_PYTHON_VERSION', PYTHON_VERSION) %} +{% set PYTHON_VERSION = os.environ['PYTHON_VERSION'] %} name: qmxgraph @@ -10,7 +9,7 @@ includes: environment: # Tip: Use `--driver ` to change driver for a single run {% if TEST_QMXGRAPH %} - PYTEST_ADDOPTS: --driver PhantomJS + PYTEST_ADDOPTS: --driver Firefox {% endif %} dependencies: @@ -18,14 +17,16 @@ dependencies: {% if TEST_QMXGRAPH %} - cherrypy - - hypothesis ==3.11.0 + - hypothesis =3.11 + - firefox + - geckodriver - pytest-mock - pytest-qt - pytest-rerunfailures - - pytest-selenium >=1,<2 + - pytest-selenium - pytest-timeout - {% if sys.platform != 'win32' %} - - pytest-xvfb - {% endif %} - + - typing-extensions <4.2.0 # [PYTHON_VERSION=="3.6"] + # Pin importlib_resources due to #151. + - importlib_resources <5.10 + - pytest-xvfb # [linux] {% endif %} diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 7ec1393..9585a0d 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -1,6 +1,5 @@ #!/bin/bash git clone --depth=1 --branch v3.7.5 https://github.com/jgraph/mxgraph.git -export PYTEST_ADDOPTS="--driver PhantomJS" export MXGRAPHPATH=mxgraph inv qrc inv test diff --git a/tasks.py b/tasks.py index 9d97a88..443d21e 100644 --- a/tasks.py +++ b/tasks.py @@ -137,7 +137,7 @@ def docs(ctx, python_version=None): @invoke.task def test(ctx): print_message('test'.format(), color=Fore.BLUE, bright=True) - cmd = 'pytest --cov=qmxgraph --timeout=30 -v --durations=10' + cmd = 'pytest --cov=qmxgraph --timeout=60 -v --durations=10' import subprocess diff --git a/tests/conftest.py b/tests/conftest.py index fcc8792..9205ac7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import os + import pytest @@ -7,45 +9,16 @@ def pytest_configure(config): # of pytest so they can't reliably be removed by a fixture. config.cache.set('qmxgraph/ports', []) - import os - lock_file = _get_port_lock_filename(config.rootdir) if os.path.isfile(lock_file): os.remove(lock_file) - import socket - - socket.setdefaulttimeout(15.0) - # Fixtures -------------------------------------------------------------------- - - @pytest.fixture -def phantomjs_driver(capabilities, driver_path, port): - """ - Overrides default `phantomjs_driver` driver from pytest-selenium. - - Default implementation uses ephemeral ports just as our tests but - it doesn't provide any way to configure them, for this reason we basically - recreate the driver fixture using port fixture. - """ - kwargs = {} - if capabilities: - kwargs['desired_capabilities'] = capabilities - if driver_path is not None: - kwargs['executable_path'] = driver_path - - kwargs['port'] = port.get() - - from selenium.webdriver import PhantomJS - - return PhantomJS(**kwargs) - - -@pytest.fixture -def driver_args(): - return ['--debug=true'] +def firefox_options(firefox_options): + firefox_options.headless = True + return firefox_options @pytest.fixture(autouse=True) @@ -320,24 +293,28 @@ def __init__(self, selenium, host): _wait_graph_page_ready(host=host, selenium=selenium) selenium.execute_script( - 'callback = function(cellIds) {' + 'window.callback = function(cellIds) {' ' if (!window.__added__) {' ' window.__added__ = [];' ' }' ' window.__added__.push.apply(window.__added__, cellIds);' '}' ) - self.eval_js_function('api.registerCellsAddedHandler', qmxgraph.js.Variable('callback')) + self.eval_js_function( + 'api.registerCellsAddedHandler', qmxgraph.js.Variable('window.callback') + ) selenium.execute_script( - 'callback = function(cellId, newLabel, oldLabel) {' + 'window.callback = function(cellId, newLabel, oldLabel) {' ' if (!window.__labels__) {' ' window.__labels__ = [];' ' }' ' window.__labels__.push({cellId: cellId, newLabel: newLabel, oldLabel: oldLabel});' # noqa '}' ) - self.eval_js_function('api.registerLabelChangedHandler', qmxgraph.js.Variable('callback')) + self.eval_js_function( + 'api.registerLabelChangedHandler', qmxgraph.js.Variable('window.callback') + ) def get_container(self): """ @@ -944,25 +921,24 @@ def _wait_graph_page_ready(host, selenium): import socket from selenium.common.exceptions import TimeoutException - timeout = 15 + timeout = 30 timeout_exceptions = (TimeoutException, TimeoutError, socket.timeout) - selenium.set_page_load_timeout(1) + selenium.set_page_load_timeout(timeout) refresh = True try: selenium.get(host.address) refresh = False except timeout_exceptions: pass - + tries = 3 if refresh: - for n in range(timeout): + for n in range(tries): try: selenium.refresh() break except timeout_exceptions: - pass - else: - raise TimeoutException("All page load tries resulted in timeout") + if n == tries - 1: + raise from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.common.by import By diff --git a/tests/test_js_graph.py b/tests/test_js_graph.py index 5c9ce48..d809ba5 100644 --- a/tests/test_js_graph.py +++ b/tests/test_js_graph.py @@ -3,6 +3,7 @@ from typing import List import pytest +from pytestqt.qtbot import QtBot from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import WebDriverException from selenium.webdriver import ActionChains @@ -152,16 +153,14 @@ def test_insert_edge_error_endpoint_not_found(graph_cases, selenium_extras) -> N with pytest.raises(WebDriverException) as e: graph.eval_js_function("api.insertEdge", invalid_source_id, graph.get_id(vertex)) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - invalid_source_id - ) + expected_msg = f"Unable to find cell with id {invalid_source_id}" + assert expected_msg in selenium_extras.get_exception_message(e) with pytest.raises(WebDriverException) as e: graph.eval_js_function("api.insertEdge", graph.get_id(vertex), invalid_target_id) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - invalid_target_id - ) + expected_msg = f"Unable to find cell with id {invalid_target_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_insert_decoration(graph_cases) -> None: @@ -441,16 +440,14 @@ def test_set_visible_error_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.set_visible(cell_id, False) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) with pytest.raises(WebDriverException) as e: graph.is_visible(cell_id) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_get_geometry_plain(graph_cases) -> None: @@ -465,7 +462,7 @@ def test_get_geometry_plain(graph_cases) -> None: # Table geometry is dependent on how the contents are rendered. # Using `pytest.approx` to account for platform differences. obtained_table_geometry = graph.get_geometry(graph.get_tables()[0]) - assert pytest.approx(obtained_table_geometry, rel=0.1) == [20, 60, 108, 72] + assert obtained_table_geometry == pytest.approx([20, 60, 96, 72], rel=0.1) def test_get_geometry_error_not_found(graph_cases, selenium_extras) -> None: @@ -479,9 +476,8 @@ def test_get_geometry_error_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.get_geometry(cell_id) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_insert_table(graph_cases) -> None: @@ -619,9 +615,8 @@ def test_update_table_error_not_found(graph_cases, selenium_extras) -> None: js.prepare_js_call('api.updateTable', table_id, contents, title) ) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - table_id - ) + expected_msg = f"Unable to find cell with id {table_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_update_table_error_not_table(graph_cases, selenium_extras) -> None: @@ -640,7 +635,8 @@ def test_update_table_error_not_table(graph_cases, selenium_extras) -> None: js.prepare_js_call('api.updateTable', table_id, contents, title) ) - assert selenium_extras.get_exception_message(e) == "Cell is not a table" + expected_msg = "Cell is not a table" + assert expected_msg in selenium_extras.get_exception_message(e) def test_remove_cells(graph_cases) -> None: @@ -671,9 +667,8 @@ def test_remove_cells_error_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.eval_js_function('api.removeCells', [cell_id]) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_on_cells_removed(graph_cases) -> None: @@ -682,8 +677,8 @@ def test_on_cells_removed(graph_cases) -> None: """ graph = graph_cases('2v_1e') - graph.selenium.execute_script('callback = function(cellIds) {window.cellIds = cellIds;}') - graph.eval_js_function('api.registerCellsRemovedHandler', js.Variable('callback')) + graph.selenium.execute_script('window.callback = function(cellIds) {window.cellIds = cellIds;}') + graph.eval_js_function('api.registerCellsRemovedHandler', js.Variable('window.callback')) cell_ids = [ graph.get_id(graph.get_vertices()[0]), @@ -819,9 +814,8 @@ def test_get_label_error_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.get_label(cell_id) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_has_cell(graph_cases) -> None: @@ -867,9 +861,8 @@ def test_get_cell_type_error_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.eval_js_function("api.getCellType", cell_id) - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) @pytest.mark.parametrize( @@ -891,7 +884,7 @@ def test_insert_with_tags(graph_cases, cell_type) -> None: # Listen to on cells added event to be sure tags are already configured # as soon as cell is created graph.selenium.execute_script( - 'callback = function(cellIds) {' + 'window.callback = function(cellIds) {' ' window.tags = cellIds.map(' ' function(cellId) {' ' return api.hasTag(cellId, "tagTest")? api.getTag(cellId, "tagTest") : null;' # noqa @@ -899,7 +892,7 @@ def test_insert_with_tags(graph_cases, cell_type) -> None: ' );' '}' ) - graph.eval_js_function('api.registerCellsAddedHandler', js.Variable('callback')) + graph.eval_js_function('api.registerCellsAddedHandler', js.Variable('window.callback')) tags = {'tagTest': '1'} cell_id = insert_by_parametrized_type(graph, cell_type, tags=tags) @@ -928,12 +921,14 @@ def test_insert_with_tags_error_value_not_string(graph_cases, cell_type, seleniu """ graph = graph_cases('empty') - tags = {'tagTest': 999} + test_tag_name = 'tagTest' + tags = {test_tag_name: 999} with pytest.raises(WebDriverException) as e: insert_by_parametrized_type(graph, cell_type, tags=tags) - assert selenium_extras.get_exception_message(e) == "Tag '{}' is not a string".format("tagTest") + expected_msg = f"Tag '{test_tag_name}' is not a string" + assert expected_msg in selenium_extras.get_exception_message(e) @pytest.mark.parametrize( @@ -978,14 +973,14 @@ def test_set_get_tag_error_tag_not_found(graph_cases, cell_type, selenium_extras graph = graph_cases('empty') cell_id = insert_by_parametrized_type(graph, cell_type) - assert not graph.eval_js_function("api.hasTag", cell_id, "test") + test_tag_name = "test" + assert not graph.eval_js_function("api.hasTag", cell_id, test_tag_name) with pytest.raises(WebDriverException) as e: - graph.eval_js_function("api.getTag", cell_id, "test") + graph.eval_js_function("api.getTag", cell_id, test_tag_name) - assert selenium_extras.get_exception_message( - e - ) == "Tag '{}' not found in cell with id {}".format("test", cell_id) + expected_msg = f"Tag '{test_tag_name}' not found in cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) @pytest.mark.parametrize( @@ -1006,11 +1001,13 @@ def test_set_get_tag_error_value_not_string(graph_cases, cell_type, selenium_ext graph = graph_cases('empty') cell_id = insert_by_parametrized_type(graph, cell_type) + test_tag_name = "test" with pytest.raises(WebDriverException) as e: - graph.eval_js_function("api.setTag", cell_id, "test", 999) + graph.eval_js_function("api.setTag", cell_id, test_tag_name, 999) - assert selenium_extras.get_exception_message(e) == "Tag '{}' is not a string".format("test") + expected_msg = f"Tag '{test_tag_name}' is not a string" + assert expected_msg in selenium_extras.get_exception_message(e) @pytest.mark.parametrize( @@ -1050,23 +1047,20 @@ def test_set_get_tag_error_cell_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.eval_js_function("api.setTag", cell_id, "test", "foo") - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) with pytest.raises(WebDriverException) as e: graph.eval_js_function("api.getTag", cell_id, "test") - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) with pytest.raises(WebDriverException) as e: graph.eval_js_function("api.hasTag", cell_id, "test") - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_set_get_tag_without_initial_tag_support(graph_cases) -> None: @@ -1174,9 +1168,8 @@ def test_set_label_error_not_found(graph_cases, selenium_extras) -> None: with pytest.raises(WebDriverException) as e: graph.eval_js_function('api.setLabel', cell_id, 'foo') - assert selenium_extras.get_exception_message(e) == "Unable to find cell with id {}".format( - cell_id - ) + expected_msg = f"Unable to find cell with id {cell_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_set_double_click_handler(graph_cases) -> None: @@ -1187,14 +1180,16 @@ def test_set_double_click_handler(graph_cases) -> None: vertex_id = graph.get_id(graph.get_vertex()) graph.selenium.execute_script( - 'callback = function(cellId) {' + 'window.callback = function(cellId) {' ' if (!window.__dblClick__) {' ' window.__dblClick__ = [];' ' }' ' window.__dblClick__.push(cellId);' '}' ) - graph.eval_js_function('api.registerDoubleClickHandler', qmxgraph.js.Variable('callback')) + graph.eval_js_function( + 'api.registerDoubleClickHandler', qmxgraph.js.Variable('window.callback') + ) actions = ActionChains(graph.selenium) actions.double_click(graph.get_vertex()) @@ -1213,14 +1208,16 @@ def test_add_selection_change_handler(graph_cases) -> None: edge = graph.get_edge(source, target) graph.selenium.execute_script( - 'callback = function(cellIds) {' + 'window.callback = function(cellIds) {' ' if (!window.__selectionChange__) {' ' window.__selectionChange__ = [];' ' }' ' window.__selectionChange__.push(cellIds);' '}' ) - graph.eval_js_function('api.registerSelectionChangedHandler', qmxgraph.js.Variable('callback')) + graph.eval_js_function( + 'api.registerSelectionChangedHandler', qmxgraph.js.Variable('window.callback') + ) # Select all cells. actions = ActionChains(graph.selenium) @@ -1255,7 +1252,7 @@ def test_add_selection_change_handler(graph_cases) -> None: ] -def test_set_popup_menu_handler(graph_cases) -> None: +def test_set_popup_menu_handler(graph_cases, qtbot: QtBot) -> None: """ :type graph_cases: qmxgraph.tests.conftest.GraphCaseFactory """ @@ -1264,23 +1261,26 @@ def test_set_popup_menu_handler(graph_cases) -> None: vertex_id = graph.get_id(graph.get_vertex()) graph.selenium.execute_script( - 'callback = function(cellId, x, y) {' + 'window.callback = function(cellId, x, y) {' ' if (!window.__popupMenu__) {' ' window.__popupMenu__ = [];' ' }' ' window.__popupMenu__.push([cellId, x, y]);' '}' ) - graph.eval_js_function('api.registerPopupMenuHandler', qmxgraph.js.Variable('callback')) + graph.eval_js_function('api.registerPopupMenuHandler', qmxgraph.js.Variable('window.callback')) vertex_label_el = graph.get_label_element(graph.get_vertex()) actions = ActionChains(graph.selenium) actions.context_click(vertex_label_el) actions.perform() - x = vertex_label_el.location['x'] + vertex_label_el.size['width'] // 2 - y = vertex_label_el.location['y'] + vertex_label_el.size['height'] // 2 - assert graph.selenium.execute_script('return window.__popupMenu__') == [[vertex_id, x, y]] + def check() -> None: + x = vertex_label_el.location['x'] + vertex_label_el.size['width'] // 2 + y = vertex_label_el.location['y'] + vertex_label_el.size['height'] // 2 + assert graph.selenium.execute_script('return window.__popupMenu__') == [[vertex_id, x, y]] + + qtbot.waitUntil(check) @pytest.mark.parametrize( @@ -1485,9 +1485,8 @@ def test_get_edge_terminals_error_edge_not_found(graph_cases, selenium_extras) - with pytest.raises(WebDriverException) as e: graph.eval_js_function('api.getEdgeTerminals', edge_id) - assert selenium_extras.get_exception_message(e) == "Unable to find edge with id {}".format( - edge_id - ) + expected_msg = f"Unable to find edge with id {edge_id}" + assert expected_msg in selenium_extras.get_exception_message(e) def test_get_edge_terminals_error_not_an_edge(graph_cases, selenium_extras) -> None: @@ -1501,9 +1500,8 @@ def test_get_edge_terminals_error_not_an_edge(graph_cases, selenium_extras) -> N with pytest.raises(WebDriverException) as e: graph.eval_js_function('api.getEdgeTerminals', graph.get_id(vertex)) - assert selenium_extras.get_exception_message(e) == "Cell with id {} is not an edge".format( - graph.get_id(vertex) - ) + expected_msg = f"Cell with id {graph.get_id(vertex)} is not an edge" + assert expected_msg in selenium_extras.get_exception_message(e) def test_custom_font_family(graph_cases_factory, port) -> None: