From 29f3a83ebc92703d70b286e33446c51d9d188420 Mon Sep 17 00:00:00 2001 From: Severin Siffert Date: Fri, 1 Dec 2023 16:50:49 +0100 Subject: [PATCH 1/7] feat: dfx ledger top-up also accepts canister names (#3458) `dfx ledger top-up` so far only accepts canister principals, but not names. With this PR it will also accept canister names like every other command does. --- CHANGELOG.md | 4 ++++ docs/cli-reference/dfx-ledger.md | 6 +++--- e2e/tests-dfx/ledger.bash | 6 ++++++ src/dfx/src/commands/ledger/top_up.rs | 16 +++++++++------- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c594f4f3c9..515e2de4fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,10 @@ Certain suffixes that replace a number of zeros are now supported. The (case-ins For cycles an additional `c` or `C` is also acceptable. For example: `dfx canister deposit-cycles 3TC mycanister` +### feat: `dfx ledger top-up` also accepts canister names + +Previously, `dfx ledger top-up` only accepted canister principals. Now it accepts both principals and canister names. + ### feat: added `dfx cycles` command This won't work on mainnet yet, but can work locally after installing the cycles ledger. diff --git a/docs/cli-reference/dfx-ledger.md b/docs/cli-reference/dfx-ledger.md index 25906e3a07..20f64b56a6 100644 --- a/docs/cli-reference/dfx-ledger.md +++ b/docs/cli-reference/dfx-ledger.md @@ -276,9 +276,9 @@ dfx ledger top-up [options] canister [flag] --network ic You can specify the following argument for the `dfx ledger top-up` command. -| Argument | Description | -|------------|------------------------------------------------------------------| -| `canister` | Specifies the canister identifier that you would like to top up. | +| Argument | Description | +|------------|--------------------------------------------------------------------------| +| `canister` | Specifies the canister identifier or name that you would like to top up. | ### Options diff --git a/e2e/tests-dfx/ledger.bash b/e2e/tests-dfx/ledger.bash index 56a0375a87..b22c6f0c99 100644 --- a/e2e/tests-dfx/ledger.bash +++ b/e2e/tests-dfx/ledger.bash @@ -184,6 +184,12 @@ tc_to_num() { assert_contains "Using transfer at block height $block_height" "$stdout" # shellcheck disable=SC2154 assert_contains "Canister was topped up with" "$stdout" + + # Top up canister by name instead of principal + dfx_new + assert_command dfx canister create e2e_project_backend + assert_command dfx ledger top-up e2e_project_backend --amount 5 + assert_contains "Canister was topped up with 617283500000000 cycles" } @test "ledger create-canister" { diff --git a/src/dfx/src/commands/ledger/top_up.rs b/src/dfx/src/commands/ledger/top_up.rs index 80265460f0..2c3defd80a 100644 --- a/src/dfx/src/commands/ledger/top_up.rs +++ b/src/dfx/src/commands/ledger/top_up.rs @@ -17,7 +17,7 @@ const MEMO_TOP_UP_CANISTER: u64 = 1347768404_u64; /// Top up a canister with cycles minted from ICP #[derive(Parser)] pub struct TopUpOpts { - /// Specify the canister id to top up + /// Specify the canister id or name to top up canister: String, /// Subaccount to withdraw from @@ -58,12 +58,14 @@ pub async fn exec(env: &dyn Environment, opts: TopUpOpts) -> DfxResult { let memo = Memo(MEMO_TOP_UP_CANISTER); - let to = Principal::from_text(&opts.canister).with_context(|| { - format!( - "Failed to parse {:?} as target canister principal.", - &opts.canister - ) - })?; + let to = Principal::from_text(&opts.canister) + .or_else(|_| env.get_canister_id_store()?.get(&opts.canister)) + .with_context(|| { + format!( + "Failed to parse {:?} as target canister principal or name.", + &opts.canister + ) + })?; let agent = env.get_agent(); From 8732f01781e6fdfaf49dc4e5621b1a481562017b Mon Sep 17 00:00:00 2001 From: Marcin Nowak-Liebiediew Date: Fri, 1 Dec 2023 18:31:34 +0100 Subject: [PATCH 2/7] chore: promote dfx 0.15.2 (#3460) --- CHANGELOG.md | 10 ++++++---- public/manifest.json | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 515e2de4fe..00b780ca57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # UNRELEASED +### feat: `dfx ledger top-up` also accepts canister names + +Previously, `dfx ledger top-up` only accepted canister principals. Now it accepts both principals and canister names. + +# 0.15.2 + ### fix: `dfx canister delete ` removes the related entry from the canister id store Previously, deleting a canister in the project by id rather than by name @@ -48,10 +54,6 @@ Certain suffixes that replace a number of zeros are now supported. The (case-ins For cycles an additional `c` or `C` is also acceptable. For example: `dfx canister deposit-cycles 3TC mycanister` -### feat: `dfx ledger top-up` also accepts canister names - -Previously, `dfx ledger top-up` only accepted canister principals. Now it accepts both principals and canister names. - ### feat: added `dfx cycles` command This won't work on mainnet yet, but can work locally after installing the cycles ledger. diff --git a/public/manifest.json b/public/manifest.json index 9aae0ffe24..d302e1a0e0 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { "tags": { - "latest": "0.15.1" + "latest": "0.15.2" }, "versions": [ "0.5.0", @@ -59,6 +59,7 @@ "0.14.3", "0.14.4", "0.15.0", - "0.15.1" + "0.15.1", + "0.15.2" ] } From 20583ed80121e82b94c21a03129938af5330e35d Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:50:21 -0800 Subject: [PATCH 3/7] feat: publish dfxvm install script from feature branch (#3461) --- .../publish-dfxvm-install-script.yml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/publish-dfxvm-install-script.yml diff --git a/.github/workflows/publish-dfxvm-install-script.yml b/.github/workflows/publish-dfxvm-install-script.yml new file mode 100644 index 0000000000..5ab630cdcc --- /dev/null +++ b/.github/workflows/publish-dfxvm-install-script.yml @@ -0,0 +1,44 @@ +name: Publish dfxvm install script + +on: + push: + branches: + - sdk-1278-dfxvm-install-script + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # When getting Rust dependencies, retry on network error: + CARGO_NET_RETRY: 10 + # Use the local .curlrc + CURL_HOME: . + +jobs: + publish-manifest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install shfmt + run: go install mvdan.cc/sh/v3/cmd/shfmt@latest + - name: Generate + run: | + shellcheck --shell=sh public/install/*.sh --exclude SC2154,SC2034,SC3003,SC3014,SC3043 + ~/go/bin/shfmt -d -p -i 4 -ci -bn -s public/install/*.sh + sed -i "s/@revision@/${GITHUB_SHA}/" public/install/999_footer.sh + mkdir _out + cat public/install/*.sh > _out/install.sh + sed -i " + /#!.*/p + /##.*/p + /^ *$/d + /^ *#/d + s/ *#.*// + " _out/install.sh + - name: Upload Artifacts + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + single_commit: yes + branch: dfxvm-install-script + folder: _out/ From b179e5111af820a37293adf8bf0d26df77a358d5 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:49:40 -0800 Subject: [PATCH 4/7] fix: enable tlsv1.2 if curl supports it (#3464) curl 7.73 (see https://github.com/curl/curl/commit/aa8777f63febca6a13f6b86e141a832232560037) changed the output of `curl --help` to display categories, rather than all of the options, unless you pass `--help all`. This caused the install script to run in a less secure mode. The install script also calls "wget --help". "wget --help all" works at least as far back as 1.5.3 (~2015). Also checked "curl --help all" with curl 7.54.0. It shows the same output as "curl --help" and returns exit code 0. --- CHANGELOG.md | 5 +++++ public/install/200_downloader.sh | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b780ca57..fae22bc965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Previously, `dfx ledger top-up` only accepted canister principals. Now it accepts both principals and canister names. +### fix: installer once again detects if curl supports tlsv1.2 + +A change to `curl --help` output made it so the install script did not detect +that the `--proto` and `--tlsv1.2` options are available. + # 0.15.2 ### fix: `dfx canister delete ` removes the related entry from the canister id store diff --git a/public/install/200_downloader.sh b/public/install/200_downloader.sh index 4916e4a216..db2a8b7f8e 100644 --- a/public/install/200_downloader.sh +++ b/public/install/200_downloader.sh @@ -26,7 +26,7 @@ check_help_for() { fi for _arg in "$@"; do - if ! "$_cmd" --help | grep -q -- "$_arg"; then + if ! "$_cmd" --help all | grep -q -- "$_arg"; then _ok="n" fi done From 4b974c6b6d1a681e50caf449a0e1aca85d5b2691 Mon Sep 17 00:00:00 2001 From: Adam Spofford <93943719+adamspofford-dfinity@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:23:08 -0800 Subject: [PATCH 5/7] chore: delete `src/dfx/.gitignore` (#3467) This file does nothing except confuse other tools like `rg`. --- src/dfx/.gitignore | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/dfx/.gitignore diff --git a/src/dfx/.gitignore b/src/dfx/.gitignore deleted file mode 100644 index 8e97362b93..0000000000 --- a/src/dfx/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# Created by https://www.gitignore.io/api/rust -# Edit at https://www.gitignore.io/?templates=rust - -### Rust ### -# Generated by Cargo -# will have compiled files and executables -target/ - -# The following is commented out since we need to guarantee all engineers and CI -# are using the same crates. -# # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -# **/Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# End of https://www.gitignore.io/api/rust - -### ASCIIDOC ### - -/docs/**/*.png -/docs/**/*.svg -!/docs/assets/*.png -!/docs/assets/*.svg -/docs/**/*.html -/docs/**/.asciidoctor - -README.html - -### Working files ### -.* -!.gitignore -!.cargo -,* -*~ -*.bak From 4b36d597545a433dca8ea025ae6ee20ba5deb5d8 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:52:19 -0800 Subject: [PATCH 6/7] fix: on PRs, run shellcheck for install scripts (#3469) Runs the shellcheck workflow for the install script on PRs as well as merge to master, skipping the upload step for PRs. For the publish-manifest workflow, sets the name to `install-script-shellcheck:required` for later inclusion as a required status. Does not do this for the dfxvm-install-script-shellcheck, since we'd only remove it again soon, but it will still be reported as a status. --- .github/workflows/publish-dfxvm-install-script.yml | 3 +++ .github/workflows/publish-manifest.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/publish-dfxvm-install-script.yml b/.github/workflows/publish-dfxvm-install-script.yml index 5ab630cdcc..14ca33c04b 100644 --- a/.github/workflows/publish-dfxvm-install-script.yml +++ b/.github/workflows/publish-dfxvm-install-script.yml @@ -4,6 +4,7 @@ on: push: branches: - sdk-1278-dfxvm-install-script + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -17,6 +18,7 @@ env: jobs: publish-manifest: + name: dfxvm-install-script-shellcheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -37,6 +39,7 @@ jobs: s/ *#.*// " _out/install.sh - name: Upload Artifacts + if: github.event_name == 'push' uses: JamesIves/github-pages-deploy-action@releases/v3 with: single_commit: yes diff --git a/.github/workflows/publish-manifest.yml b/.github/workflows/publish-manifest.yml index a6fa2840e4..da3dea0c11 100644 --- a/.github/workflows/publish-manifest.yml +++ b/.github/workflows/publish-manifest.yml @@ -4,6 +4,7 @@ on: push: branches: - master + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -17,6 +18,7 @@ env: jobs: publish-manifest: + name: install-script-shellcheck:required runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -38,6 +40,7 @@ jobs: " _out/install.sh cp public/manifest.json _out/manifest.json - name: Upload Artifacts + if: github.event_name == 'push' uses: JamesIves/github-pages-deploy-action@releases/v3 with: single_commit: yes From 826cf17fc6406a69f9b036808369eee467f9dc2c Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 6 Dec 2023 15:47:26 +0100 Subject: [PATCH 7/7] test(release): Add Python script to test UIs (#3232) Authored-by: Marco --- .github/workflows/e2e.yml | 42 ++++- scripts/test-uis.py | 341 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 scripts/test-uis.py diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0d7eacfb55..b3fc7470fd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -152,10 +152,50 @@ jobs: - name: Run e2e test run: timeout 2400 bats "e2e/$E2E_TEST" + ui_test: + runs-on: ${{ matrix.os }} + needs: [build_dfx] + strategy: + matrix: + os: [macos-12, ubuntu-20.04, ubuntu-22.04] + steps: + - name: Checking out repo + uses: actions/checkout@v4 + - name: Setting up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Installing playwright + run: | + pip install playwright==1.40.0 + playwright install + playwright install-deps + - name: Download dfx binary + uses: actions/download-artifact@v3 + with: + name: dfx-${{ matrix.os }}-rs-${{ hashFiles('rust-toolchain.toml') }} + path: /usr/local/bin + - name: Setup dfx binary + run: chmod +x /usr/local/bin/dfx + - name: Deploy default dfx project + run: | + dfx new e2e_project + cd e2e_project + dfx start --background --clean + dfx deploy 2>&1 | tee deploy.log + echo FRONTEND_URL=$(grep "_frontend:" deploy.log | grep -Eo "(http|https)://[a-zA-Z0-9./?=_&%:-]*") >> $GITHUB_ENV + echo CANDID_URL=$(grep "_backend:" deploy.log | grep -Eo "(http|https)://[a-zA-Z0-9./?=_&%:-]*") >> $GITHUB_ENV + - name: Running the Python script + run: | + python scripts/test-uis.py \ + --frontend_url "$FRONTEND_URL" \ + --candid_url "$CANDID_URL" \ + --browser chromium firefox webkit + aggregate: name: e2e:required if: ${{ always() }} - needs: [test, smoke] + needs: [test, smoke, ui_test] runs-on: ubuntu-latest steps: - name: check smoke test result diff --git a/scripts/test-uis.py b/scripts/test-uis.py new file mode 100644 index 0000000000..5752936c29 --- /dev/null +++ b/scripts/test-uis.py @@ -0,0 +1,341 @@ +""" +Automate frontend tests by using Playwright. +The script tests the following UIs: + +1. Frontend UI. +2. Candid UI. + +Examples: + +$ python3 test-uis.py --frontend_url '...' --browser chromium firefox webkit # Only test the frontend UI +$ python3 test-uis.py --candid_url '...' --browser chromium firefox webkit # Only test the Candid UI +$ python3 test-uis.py --frontend_url '...' --candid_url '...' --browser chromium firefox webkit # Test both UIs +""" +import argparse +import logging +import re +import sys +import time +from enum import Enum + +from playwright.sync_api import sync_playwright + +_CHROMIUM_BROWSER = "chromium" +_FIREFOX_BROWSER = "firefox" +_WEBKIT_BROWSER = "webkit" +_SUPPORTED_BROWSERS = { + _CHROMIUM_BROWSER, + _FIREFOX_BROWSER, + _WEBKIT_BROWSER, +} +_CANDID_UI_WARNINGS_TO_IGNORE = [ + ("Error", "/index.js"), + ("Invalid asm.js: Unexpected token", "/index.js"), + ("Expected to find result for path [object Object], but instead found nothing.", "/index.js"), + ( + """ +Error: Server returned an error: + Code: 404 (Not Found) + Body: Custom section name not found. + + at j.readState (http://localhost:4943/index.js:2:11709) + at async http://localhost:4943/index.js:2:97683 + at async Promise.all (index 0) + at async Module.UA (http://localhost:4943/index.js:2:98732) + at async Object.getNames (http://localhost:4943/index.js:2:266156) + at async http://localhost:4943/index.js:2:275479""".strip(), + "/index.js", + ), + ( + """ +Error: Server returned an error: + Code: 404 (Not Found) + Body: Custom section name not found.""".strip(), + "/index.js", + ), +] +_CANDID_UI_ERRORS_TO_IGNORE = [ + ("Failed to load resource: the server responded with a status of 404 (Not Found)", "/read_state"), +] +# `page.route` does not support additional function parameters +_FRONTEND_URL = None + + +class _UI(Enum): + CANDID = 1 + FRONTEND = 2 + + +def _validate_browsers(browser): + if browser not in _SUPPORTED_BROWSERS: + logging.error(f"Browser {browser} not supported") + sys.exit(1) + + return browser + + +def _get_argument_parser(): + parser = argparse.ArgumentParser(description="Test the Frontend and Candid UIs") + + parser.add_argument("--frontend_url", help="Frontend UI url") + parser.add_argument("--candid_url", help="Candid UI url") + + parser.add_argument( + "--browsers", + nargs="+", + type=_validate_browsers, + help=f"Test against the specified browsers ({_SUPPORTED_BROWSERS})", + ) + + return parser + + +def _validate_args(args): + has_err = False + + if not args.frontend_url and not args.candid_url: + logging.error('Either "--frontend_url" or "--candid_url" must be specified to start the tests') + has_err = True + + if not args.browsers: + logging.error("At least one browser must be specified") + logging.error(f"Possible browsers: {_SUPPORTED_BROWSERS}") + has_err = True + + if has_err: + sys.exit(1) + + +def _get_browser_obj(playwright, browser_name): + if browser_name == _CHROMIUM_BROWSER: + return playwright.chromium + if browser_name == _FIREFOX_BROWSER: + return playwright.firefox + if browser_name == _WEBKIT_BROWSER: + return playwright.webkit + + return None + + +def _check_console_logs(console_logs): + logging.info("Checking console logs") + + has_err = False + for log in console_logs: + if log.type not in {"warning", "error"}: + continue + + # Skip all `Error with Permissions-Policy header: Unrecognized feature` warnings + perm_policy_warn = "Error with Permissions-Policy header:" + if perm_policy_warn in log.text: + logging.warning(f'Skipping Permissions-Policy warning. log.text="{log.text}"') + continue + + url = log.location.get("url") + if not url: + raise RuntimeError( + f'Cannot find "url" during log parsing (log.type={log.type}, log.text="{log.text}", log.location="{log.location}")' + ) + + for actual_text, endpoint in ( + _CANDID_UI_ERRORS_TO_IGNORE if log.type == "error" else _CANDID_UI_WARNINGS_TO_IGNORE + ): + if actual_text == log.text.strip() and endpoint in url: + logging.warning( + f'Found {log.type}, but it was expected (log.type="{actual_text}", endpoint="{endpoint}")' + ) + break + else: + logging.error(f'Found unexpected console log {log.type}. Text: "{log.text}", url: {url}') + + has_err = True + + if has_err: + raise RuntimeError("Console has unexpected warnings and/or errors. Check previous logs") + + logging.info("Console logs are ok") + + +def _click_button(page, button): + logging.info(f'Clicking button "{button}"') + page.get_by_role("button", name=button).click() + + +def _set_text(page, text, value): + logging.info(f'Setting text to "{value}"') + page.get_by_role("textbox", name=text).fill(value) + + +def _test_frontend_ui_handler(page): + # Set the name & Click the button + name = "my name" + logging.info(f'Setting name "{name}"') + page.get_by_label("Enter your name:").fill(name) + _click_button(page, "Click Me!") + + # Check if `#greeting` is populated correctly + greeting_id = "#greeting" + timeout_ms = 60000 + greeting_obj = page.wait_for_selector(greeting_id, timeout=timeout_ms) + if greeting_obj: + actual_value = greeting_obj.inner_text() + expected_value = f"Hello, {name}!" + if actual_value == expected_value: + logging.info(f'"{actual_value}" found in "{greeting_id}"') + else: + raise RuntimeError(f'Expected greeting message is "{expected_value}", but found "{actual_value}"') + else: + raise RuntimeError(f"Cannot find {greeting_id} selector") + + +def _test_candid_ui_handler(page): + # Set the text & Click the "Query" button + text = "hello, world" + _set_text(page, "text", text) + _click_button(page, "Query") + + # Check if `#output-list` is populated correctly (after the first click) + output_list_id = "#output-list" + timeout_ms = 60000 + _ = page.wait_for_selector(output_list_id, timeout=timeout_ms) + + # Reset the text & Click the "Random" button + _set_text(page, "text", "") + _click_button(page, "Random") + # ~ + + # Check if `#output-list` is populated correctly (after the second click) + # + # NOTE: `#output-list` already exists, so `wait_for_selector` won't work as expected. + # We noticed that, especially for `Ubuntu 20.04` and `Webkit`, the two additional lines + # created once the `Random` button was clicked, were not created properly. + # + # For this reason there is this simple fallback logic that tries to look at the selector + # for more than once by sleeping for some time. + fallback_retries = 10 + fallback_sleep_sec = 5 + last_err = None + for _ in range(fallback_retries): + try: + output_list_obj = page.wait_for_selector(output_list_id, timeout=timeout_ms) + if not output_list_obj: + raise RuntimeError(f"Cannot find {output_list_id} selector") + + output_list_lines = output_list_obj.inner_text().split("\n") + actual_num_lines, expected_num_lines = len(output_list_lines), 4 + if actual_num_lines != expected_num_lines: + err = [f"Expected {expected_num_lines} lines of text but found {actual_num_lines}"] + err.append("Lines:") + err.extend(output_list_lines) + raise RuntimeError("\n".join(err)) + + # Extract random text from third line + random_text = re.search(r'"([^"]*)"', output_list_lines[2]) + if not random_text: + raise RuntimeError(f"Cannot extract the random text from the third line: {output_list_lines[2]}") + random_text = random_text.group(1) + + for i, text_str in enumerate([text, random_text]): + line1, line2 = (i * 2), (i * 2 + 1) + + # First output line + actual_line, expected_line = output_list_lines[line1], f'› greet("{text_str}")' + if actual_line != expected_line: + raise RuntimeError(f"Expected {expected_line} line, but found {actual_line} (line {line1})") + logging.info(f'"{actual_line}" found in {output_list_id} at position {line1}') + + # Second output line + actual_line, expected_line = output_list_lines[line2], f'("Hello, {text_str}!")' + if actual_line != expected_line: + raise RuntimeError(f"Expected {expected_line} line, but found {actual_line} (line {line2})") + logging.info(f'"{actual_line}" found in {output_list_id} at position {line2}') + + # All good! + last_err = None + logging.info(f"{output_list_id} lines are defined correctly") + break + except RuntimeError as run_err: + last_err = str(run_err) + logging.warning(f"Fallback hit! Sleeping for {fallback_sleep_sec} before continuing") + time.sleep(fallback_sleep_sec) + + if last_err: + raise RuntimeError(last_err) + + +def _handle_route_for_webkit(route): + url = route.request.url.replace("https://", "http://") + + headers = None + if any(map(url.endswith, [".css", ".js", ".svg"])): + global _FRONTEND_URL + assert _FRONTEND_URL + headers = { + "referer": _FRONTEND_URL, + } + + response = route.fetch(url=url, headers=headers) + assert response.status == 200, f"Expected 200 status code, but got {response.status}. Url: {url}" + route.fulfill(response=response) + + +def _test_ui(ui, url, handler, browsers): + logging.info(f'Testing "{str(ui)}" at "{url}"') + + has_err = False + with sync_playwright() as playwright: + for browser_name in browsers: + logging.info(f'Checking "{browser_name}" browser') + browser = _get_browser_obj(playwright, browser_name) + if not browser: + raise RuntimeError(f"Cannot determine browser object for browser {browser_name}") + + try: + browser = browser.launch(headless=True) + context = browser.new_context() + page = context.new_page() + + # Attach a listener to the page's console events + console_logs = [] + page.on("console", lambda msg: console_logs.append(msg)) + + # Webkit forces HTTPS: + # - https://github.com/microsoft/playwright/issues/12975 + # - https://stackoverflow.com/questions/46394682/safari-keeps-forcing-https-on-localhost + if ui == _UI.FRONTEND and browser_name == _WEBKIT_BROWSER: + global _FRONTEND_URL + _FRONTEND_URL = url + page.route("**/*", _handle_route_for_webkit) + + page.goto(url) + + handler(page) + _check_console_logs(console_logs) + except Exception as e: + logging.error(f"Error: {str(e)}") + has_err = True + finally: + if context: + context.close() + if browser: + browser.close() + + if has_err: + sys.exit(1) + + +def _main(): + args = _get_argument_parser().parse_args() + _validate_args(args) + + if args.frontend_url: + _test_ui(_UI.FRONTEND, args.frontend_url, _test_frontend_ui_handler, args.browsers) + if args.candid_url: + _test_ui(_UI.CANDID, args.candid_url, _test_candid_ui_handler, args.browsers) + + logging.info("DONE!") + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + _main()