diff options
| author | Alassane Yattara <alassane.yattara@savoirfairelinux.com> | 2023-12-08 02:53:19 +0100 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2023-12-08 17:17:42 +0000 |
| commit | 23d3e2c718d6eab5151a425e835554bd4fe20a8b (patch) | |
| tree | 804d0fa917bf4e03c93d8d3b96ed3a64b5ddc4c0 | |
| parent | d5a6e3b546e7d6691150a5906e531e3b3d6919ee (diff) | |
| download | poky-23d3e2c718d6eab5151a425e835554bd4fe20a8b.tar.gz | |
bitbake: toaster/tests: Bug fixes, functional tests dependent on each other
refactor test_create_project and test_project_page to remove their dependencies
(Bitbake rev: 54f7c0bb6ff435c4936c3422532aa071bd5b66e8)
Signed-off-by: Alassane Yattara <alassane.yattara@savoirfairelinux.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
| -rw-r--r-- | bitbake/lib/toaster/tests/functional/test_create_new_project.py | 1 | ||||
| -rw-r--r-- | bitbake/lib/toaster/tests/functional/test_project_page.py | 164 |
2 files changed, 79 insertions, 86 deletions
diff --git a/bitbake/lib/toaster/tests/functional/test_create_new_project.py b/bitbake/lib/toaster/tests/functional/test_create_new_project.py index dc7d1fc20b..bbda0cf4e6 100644 --- a/bitbake/lib/toaster/tests/functional/test_create_new_project.py +++ b/bitbake/lib/toaster/tests/functional/test_create_new_project.py | |||
| @@ -16,6 +16,7 @@ from selenium.webdriver.common.by import By | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | @pytest.mark.django_db | 18 | @pytest.mark.django_db |
| 19 | @pytest.mark.order("last") | ||
| 19 | class TestCreateNewProject(SeleniumFunctionalTestCase): | 20 | class TestCreateNewProject(SeleniumFunctionalTestCase): |
| 20 | 21 | ||
| 21 | def _create_test_new_project( | 22 | def _create_test_new_project( |
diff --git a/bitbake/lib/toaster/tests/functional/test_project_page.py b/bitbake/lib/toaster/tests/functional/test_project_page.py index 03f64f8fef..077badb0c2 100644 --- a/bitbake/lib/toaster/tests/functional/test_project_page.py +++ b/bitbake/lib/toaster/tests/functional/test_project_page.py | |||
| @@ -6,88 +6,89 @@ | |||
| 6 | # SPDX-License-Identifier: GPL-2.0-only | 6 | # SPDX-License-Identifier: GPL-2.0-only |
| 7 | # | 7 | # |
| 8 | 8 | ||
| 9 | import os | ||
| 9 | import random | 10 | import random |
| 10 | import string | 11 | import string |
| 12 | from unittest import skip | ||
| 11 | import pytest | 13 | import pytest |
| 12 | from time import sleep | ||
| 13 | from django.urls import reverse | 14 | from django.urls import reverse |
| 14 | from django.utils import timezone | 15 | from django.utils import timezone |
| 15 | from selenium.webdriver.common.keys import Keys | 16 | from selenium.webdriver.common.keys import Keys |
| 16 | from selenium.webdriver.support.select import Select | 17 | from selenium.webdriver.support.select import Select |
| 17 | from selenium.common.exceptions import NoSuchElementException, TimeoutException | 18 | from selenium.common.exceptions import TimeoutException |
| 18 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | 19 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase |
| 19 | from orm.models import Build, Project, Target | 20 | from orm.models import Build, Project, Target |
| 20 | from selenium.webdriver.common.by import By | 21 | from selenium.webdriver.common.by import By |
| 21 | 22 | ||
| 23 | from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled | ||
| 24 | |||
| 22 | 25 | ||
| 23 | @pytest.mark.django_db | 26 | @pytest.mark.django_db |
| 27 | @pytest.mark.order("last") | ||
| 24 | class TestProjectPage(SeleniumFunctionalTestCase): | 28 | class TestProjectPage(SeleniumFunctionalTestCase): |
| 29 | project_id = None | ||
| 30 | PROJECT_NAME = 'TestProjectPage' | ||
| 25 | 31 | ||
| 26 | def setUp(self): | 32 | def _create_project(self, project_name): |
| 27 | super().setUp() | ||
| 28 | release = '3' | ||
| 29 | project_name = 'project_' + self.generate_random_string() | ||
| 30 | self._create_test_new_project( | ||
| 31 | project_name, | ||
| 32 | release, | ||
| 33 | False, | ||
| 34 | ) | ||
| 35 | |||
| 36 | def generate_random_string(self, length=10): | ||
| 37 | characters = string.ascii_letters + string.digits # alphabetic and numerical characters | ||
| 38 | random_string = ''.join(random.choice(characters) for _ in range(length)) | ||
| 39 | return random_string | ||
| 40 | |||
| 41 | def _create_test_new_project( | ||
| 42 | self, | ||
| 43 | project_name, | ||
| 44 | release, | ||
| 45 | merge_toaster_settings, | ||
| 46 | ): | ||
| 47 | """ Create/Test new project using: | 33 | """ Create/Test new project using: |
| 48 | - Project Name: Any string | 34 | - Project Name: Any string |
| 49 | - Release: Any string | 35 | - Release: Any string |
| 50 | - Merge Toaster settings: True or False | 36 | - Merge Toaster settings: True or False |
| 51 | """ | 37 | """ |
| 52 | self.get(reverse('newproject')) | 38 | self.get(reverse('newproject')) |
| 53 | self.driver.find_element(By.ID, | 39 | self.wait_until_visible('#new-project-name') |
| 54 | "new-project-name").send_keys(project_name) | 40 | self.find("#new-project-name").send_keys(project_name) |
| 55 | 41 | select = Select(self.find("#projectversion")) | |
| 56 | select = Select(self.find('#projectversion')) | 42 | select.select_by_value('3') |
| 57 | select.select_by_value(release) | ||
| 58 | 43 | ||
| 59 | # check merge toaster settings | 44 | # check merge toaster settings |
| 60 | checkbox = self.find('.checkbox-mergeattr') | 45 | checkbox = self.find('.checkbox-mergeattr') |
| 61 | if merge_toaster_settings: | 46 | if not checkbox.is_selected(): |
| 62 | if not checkbox.is_selected(): | 47 | checkbox.click() |
| 63 | checkbox.click() | 48 | |
| 49 | if self.PROJECT_NAME != 'TestProjectPage': | ||
| 50 | # Reset project name if it's not the default one | ||
| 51 | self.PROJECT_NAME = 'TestProjectPage' | ||
| 52 | |||
| 53 | self.find("#create-project-button").click() | ||
| 54 | |||
| 55 | try: | ||
| 56 | self.wait_until_visible('#hint-error-project-name') | ||
| 57 | url = reverse('project', args=(TestProjectPage.project_id, )) | ||
| 58 | self.get(url) | ||
| 59 | self.wait_until_visible('#config-nav', poll=3) | ||
| 60 | except TimeoutException: | ||
| 61 | self.wait_until_visible('#config-nav', poll=3) | ||
| 62 | |||
| 63 | def _random_string(self, length): | ||
| 64 | return ''.join( | ||
| 65 | random.choice(string.ascii_letters) for _ in range(length) | ||
| 66 | ) | ||
| 67 | |||
| 68 | def _navigate_to_project_page(self): | ||
| 69 | # Navigate to project page | ||
| 70 | if TestProjectPage.project_id is None: | ||
| 71 | self._create_project(project_name=self._random_string(10)) | ||
| 72 | current_url = self.driver.current_url | ||
| 73 | TestProjectPage.project_id = get_projectId_from_url(current_url) | ||
| 64 | else: | 74 | else: |
| 65 | if checkbox.is_selected(): | 75 | url = reverse('project', args=(TestProjectPage.project_id,)) |
| 66 | checkbox.click() | 76 | self.get(url) |
| 67 | 77 | self.wait_until_visible('#config-nav') | |
| 68 | self.driver.find_element(By.ID, "create-project-button").click() | ||
| 69 | 78 | ||
| 70 | def _get_create_builds(self, **kwargs): | 79 | def _get_create_builds(self, **kwargs): |
| 71 | """ Create a build and return the build object """ | 80 | """ Create a build and return the build object """ |
| 72 | # parameters for builds to associate with the projects | 81 | # parameters for builds to associate with the projects |
| 73 | now = timezone.now() | 82 | now = timezone.now() |
| 74 | release = '3' | ||
| 75 | project_name = 'projectmaster' | ||
| 76 | self._create_test_new_project( | ||
| 77 | project_name+"2", | ||
| 78 | release, | ||
| 79 | False, | ||
| 80 | ) | ||
| 81 | |||
| 82 | self.project1_build_success = { | 83 | self.project1_build_success = { |
| 83 | 'project': Project.objects.get(id=1), | 84 | 'project': Project.objects.get(id=TestProjectPage.project_id), |
| 84 | 'started_on': now, | 85 | 'started_on': now, |
| 85 | 'completed_on': now, | 86 | 'completed_on': now, |
| 86 | 'outcome': Build.SUCCEEDED | 87 | 'outcome': Build.SUCCEEDED |
| 87 | } | 88 | } |
| 88 | 89 | ||
| 89 | self.project1_build_failure = { | 90 | self.project1_build_failure = { |
| 90 | 'project': Project.objects.get(id=1), | 91 | 'project': Project.objects.get(id=TestProjectPage.project_id), |
| 91 | 'started_on': now, | 92 | 'started_on': now, |
| 92 | 'completed_on': now, | 93 | 'completed_on': now, |
| 93 | 'outcome': Build.FAILED | 94 | 'outcome': Build.FAILED |
| @@ -180,9 +181,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 180 | 181 | ||
| 181 | def _navigate_to_config_nav(self, nav_id, nav_index): | 182 | def _navigate_to_config_nav(self, nav_id, nav_index): |
| 182 | # navigate to the project page | 183 | # navigate to the project page |
| 183 | url = reverse("project", args=(1,)) | 184 | self._navigate_to_project_page() |
| 184 | self.get(url) | ||
| 185 | self.wait_until_visible('#config-nav') | ||
| 186 | # click on "Software recipe" tab | 185 | # click on "Software recipe" tab |
| 187 | soft_recipe = self._get_config_nav_item(nav_index) | 186 | soft_recipe = self._get_config_nav_item(nav_index) |
| 188 | soft_recipe.click() | 187 | soft_recipe.click() |
| @@ -211,29 +210,6 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 211 | if row_to_show not in to_skip: | 210 | if row_to_show not in to_skip: |
| 212 | test_show_rows(row_to_show, show_row_link) | 211 | test_show_rows(row_to_show, show_row_link) |
| 213 | 212 | ||
| 214 | def _wait_until_build(self, state): | ||
| 215 | timeout = 10 | ||
| 216 | start_time = 0 | ||
| 217 | while True: | ||
| 218 | if start_time > timeout: | ||
| 219 | raise TimeoutException( | ||
| 220 | f'Build did not reach {state} state within {timeout} seconds' | ||
| 221 | ) | ||
| 222 | try: | ||
| 223 | last_build_state = self.driver.find_element( | ||
| 224 | By.XPATH, | ||
| 225 | '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', | ||
| 226 | ) | ||
| 227 | build_state = last_build_state.get_attribute( | ||
| 228 | 'data-build-state') | ||
| 229 | state_text = state.lower().split() | ||
| 230 | if any(x in str(build_state).lower() for x in state_text): | ||
| 231 | break | ||
| 232 | except NoSuchElementException: | ||
| 233 | continue | ||
| 234 | start_time += 1 | ||
| 235 | sleep(1) # take a breath and try again | ||
| 236 | |||
| 237 | def _mixin_test_table_search_input(self, **kwargs): | 213 | def _mixin_test_table_search_input(self, **kwargs): |
| 238 | input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() | 214 | input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() |
| 239 | # Test search input | 215 | # Test search input |
| @@ -245,11 +221,19 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 245 | rows = self.find_all(f'#{table_selector} tbody tr') | 221 | rows = self.find_all(f'#{table_selector} tbody tr') |
| 246 | self.assertTrue(len(rows) > 0) | 222 | self.assertTrue(len(rows) > 0) |
| 247 | 223 | ||
| 224 | def test_create_project(self): | ||
| 225 | """ Create/Test new project using: | ||
| 226 | - Project Name: Any string | ||
| 227 | - Release: Any string | ||
| 228 | - Merge Toaster settings: True or False | ||
| 229 | """ | ||
| 230 | self._create_project(project_name=self.PROJECT_NAME) | ||
| 231 | |||
| 248 | def test_image_recipe_editColumn(self): | 232 | def test_image_recipe_editColumn(self): |
| 249 | """ Test the edit column feature in image recipe table on project page """ | 233 | """ Test the edit column feature in image recipe table on project page """ |
| 250 | self._get_create_builds(success=10, failure=10) | 234 | self._get_create_builds(success=10, failure=10) |
| 251 | 235 | ||
| 252 | url = reverse('projectimagerecipes', args=(1,)) | 236 | url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,)) |
| 253 | self.get(url) | 237 | self.get(url) |
| 254 | self.wait_until_present('#imagerecipestable tbody tr') | 238 | self.wait_until_present('#imagerecipestable tbody tr') |
| 255 | 239 | ||
| @@ -276,8 +260,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 276 | - AT RIGHT -> button "New project", displayed, clickable | 260 | - AT RIGHT -> button "New project", displayed, clickable |
| 277 | """ | 261 | """ |
| 278 | # navigate to the project page | 262 | # navigate to the project page |
| 279 | url = reverse("project", args=(1,)) | 263 | self._navigate_to_project_page() |
| 280 | self.get(url) | ||
| 281 | 264 | ||
| 282 | # check page header | 265 | # check page header |
| 283 | # AT LEFT -> Logo of Yocto project | 266 | # AT LEFT -> Logo of Yocto project |
| @@ -360,8 +343,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 360 | - Check project name is changed | 343 | - Check project name is changed |
| 361 | """ | 344 | """ |
| 362 | # navigate to the project page | 345 | # navigate to the project page |
| 363 | url = reverse("project", args=(1,)) | 346 | self._navigate_to_project_page() |
| 364 | self.get(url) | ||
| 365 | 347 | ||
| 366 | # click on "Edit" icon button | 348 | # click on "Edit" icon button |
| 367 | self.wait_until_visible('#project-name-container') | 349 | self.wait_until_visible('#project-name-container') |
| @@ -388,8 +370,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 388 | Check search box used to build recipes | 370 | Check search box used to build recipes |
| 389 | """ | 371 | """ |
| 390 | # navigate to the project page | 372 | # navigate to the project page |
| 391 | url = reverse("project", args=(1,)) | 373 | self._navigate_to_project_page() |
| 392 | self.get(url) | ||
| 393 | 374 | ||
| 394 | # check "configuration" tab | 375 | # check "configuration" tab |
| 395 | self.wait_until_visible('#topbar-configuration-tab') | 376 | self.wait_until_visible('#topbar-configuration-tab') |
| @@ -397,7 +378,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 397 | self.assertTrue(config_tab.get_attribute('class') == 'active') | 378 | self.assertTrue(config_tab.get_attribute('class') == 'active') |
| 398 | self.assertTrue('Configuration' in str(config_tab.text)) | 379 | self.assertTrue('Configuration' in str(config_tab.text)) |
| 399 | self.assertTrue( | 380 | self.assertTrue( |
| 400 | f"/toastergui/project/1" in str(self.driver.current_url) | 381 | f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url) |
| 401 | ) | 382 | ) |
| 402 | 383 | ||
| 403 | def get_tabs(): | 384 | def get_tabs(): |
| @@ -420,7 +401,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 420 | check_tab_link( | 401 | check_tab_link( |
| 421 | 1, | 402 | 1, |
| 422 | 'Builds', | 403 | 'Builds', |
| 423 | f"/toastergui/project/1/builds" | 404 | f"/toastergui/project/{TestProjectPage.project_id}/builds" |
| 424 | ) | 405 | ) |
| 425 | 406 | ||
| 426 | # check "Import layers" tab | 407 | # check "Import layers" tab |
| @@ -429,7 +410,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 429 | check_tab_link( | 410 | check_tab_link( |
| 430 | 2, | 411 | 2, |
| 431 | 'Import layer', | 412 | 'Import layer', |
| 432 | f"/toastergui/project/1/importlayer" | 413 | f"/toastergui/project/{TestProjectPage.project_id}/importlayer" |
| 433 | ) | 414 | ) |
| 434 | 415 | ||
| 435 | # check "New custom image" tab | 416 | # check "New custom image" tab |
| @@ -438,7 +419,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 438 | check_tab_link( | 419 | check_tab_link( |
| 439 | 3, | 420 | 3, |
| 440 | 'New custom image', | 421 | 'New custom image', |
| 441 | f"/toastergui/project/1/newcustomimage" | 422 | f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage" |
| 442 | ) | 423 | ) |
| 443 | 424 | ||
| 444 | # check search box can be use to build recipes | 425 | # check search box can be use to build recipes |
| @@ -480,12 +461,20 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 480 | '//td[@class="add-del-layers"]//a[1]' | 461 | '//td[@class="add-del-layers"]//a[1]' |
| 481 | ) | 462 | ) |
| 482 | build_btn.click() | 463 | build_btn.click() |
| 483 | self._wait_until_build('parsing starting cloning queued') | 464 | build_state = wait_until_build(self, 'parsing starting cloning queued') |
| 484 | lastest_builds = self.driver.find_elements( | 465 | lastest_builds = self.driver.find_elements( |
| 485 | By.XPATH, | 466 | By.XPATH, |
| 486 | '//div[@id="latest-builds"]/div' | 467 | '//div[@id="latest-builds"]/div' |
| 487 | ) | 468 | ) |
| 488 | self.assertTrue(len(lastest_builds) > 0) | 469 | self.assertTrue(len(lastest_builds) > 0) |
| 470 | last_build = lastest_builds[0] | ||
| 471 | cancel_button = last_build.find_element( | ||
| 472 | By.XPATH, | ||
| 473 | '//span[@class="cancel-build-btn pull-right alert-link"]', | ||
| 474 | ) | ||
| 475 | cancel_button.click() | ||
| 476 | if 'starting' not in build_state: # change build state when cancelled in starting state | ||
| 477 | wait_until_build_cancelled(self) | ||
| 489 | 478 | ||
| 490 | # check software recipe table feature(show/hide column, pagination) | 479 | # check software recipe table feature(show/hide column, pagination) |
| 491 | self._navigate_to_config_nav('softwarerecipestable', 4) | 480 | self._navigate_to_config_nav('softwarerecipestable', 4) |
| @@ -547,6 +536,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 547 | searchBtn_selector='search-submit-machinestable', | 536 | searchBtn_selector='search-submit-machinestable', |
| 548 | table_selector='machinestable' | 537 | table_selector='machinestable' |
| 549 | ) | 538 | ) |
| 539 | self.wait_until_visible('#machinestable tbody tr', poll=3) | ||
| 550 | rows = self.find_all('#machinestable tbody tr') | 540 | rows = self.find_all('#machinestable tbody tr') |
| 551 | machine_to_add = rows[0] | 541 | machine_to_add = rows[0] |
| 552 | add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]') | 542 | add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]') |
| @@ -593,6 +583,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 593 | table_selector='layerstable' | 583 | table_selector='layerstable' |
| 594 | ) | 584 | ) |
| 595 | # check "Add layer" button works | 585 | # check "Add layer" button works |
| 586 | self.wait_until_visible('#layerstable tbody tr', poll=3) | ||
| 596 | rows = self.find_all('#layerstable tbody tr') | 587 | rows = self.find_all('#layerstable tbody tr') |
| 597 | layer_to_add = rows[0] | 588 | layer_to_add = rows[0] |
| 598 | add_btn = layer_to_add.find_element( | 589 | add_btn = layer_to_add.find_element( |
| @@ -601,7 +592,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 601 | ) | 592 | ) |
| 602 | add_btn.click() | 593 | add_btn.click() |
| 603 | # check modal is displayed | 594 | # check modal is displayed |
| 604 | self.wait_until_visible('#dependencies-modal', poll=2) | 595 | self.wait_until_visible('#dependencies-modal', poll=3) |
| 605 | list_dependencies = self.find_all('#dependencies-list li') | 596 | list_dependencies = self.find_all('#dependencies-list li') |
| 606 | # click on add-layers button | 597 | # click on add-layers button |
| 607 | add_layers_btn = self.driver.find_element( | 598 | add_layers_btn = self.driver.find_element( |
| @@ -615,6 +606,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 615 | f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text) | 606 | f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text) |
| 616 | ) | 607 | ) |
| 617 | # check "Remove layer" button works | 608 | # check "Remove layer" button works |
| 609 | self.wait_until_visible('#layerstable tbody tr', poll=3) | ||
| 618 | rows = self.find_all('#layerstable tbody tr') | 610 | rows = self.find_all('#layerstable tbody tr') |
| 619 | layer_to_remove = rows[0] | 611 | layer_to_remove = rows[0] |
| 620 | remove_btn = layer_to_remove.find_element( | 612 | remove_btn = layer_to_remove.find_element( |
| @@ -706,7 +698,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 706 | - Check layer summary | 698 | - Check layer summary |
| 707 | - Check layer description | 699 | - Check layer description |
| 708 | """ | 700 | """ |
| 709 | url = reverse("layerdetails", args=(1, 8)) | 701 | url = reverse("layerdetails", args=(TestProjectPage.project_id, 8)) |
| 710 | self.get(url) | 702 | self.get(url) |
| 711 | self.wait_until_visible('.page-header') | 703 | self.wait_until_visible('.page-header') |
| 712 | # check title is displayed | 704 | # check title is displayed |
| @@ -765,7 +757,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): | |||
| 765 | - Check recipe: name, summary, description, Version, Section, | 757 | - Check recipe: name, summary, description, Version, Section, |
| 766 | License, Approx. packages included, Approx. size, Recipe file | 758 | License, Approx. packages included, Approx. size, Recipe file |
| 767 | """ | 759 | """ |
| 768 | url = reverse("recipedetails", args=(1, 53428)) | 760 | url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428)) |
| 769 | self.get(url) | 761 | self.get(url) |
| 770 | self.wait_until_visible('.page-header') | 762 | self.wait_until_visible('.page-header') |
| 771 | # check title is displayed | 763 | # check title is displayed |
