-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(tauri): add embedded WebDriver provider support #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
goosewobbler
wants to merge
58
commits into
main
Choose a base branch
from
feat/embedded-webdriver
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
f005029
feat(tauri): add embedded WebDriver provider support
goosewobbler d6ad8b8
test(tauri): add Tauri E2E tests with embedded WebDriver provider
goosewobbler 57d9ca1
chore: add `tauri-plugin-webdriver`
goosewobbler fd992c7
chore: install local plugin
goosewobbler d8fca1e
refactor(webdriver): improve JSON serialization handling in Linux pla…
goosewobbler 42f5a34
chore: add webdriver perm
goosewobbler 84cda72
refactor(tauri-plugin): update event emission to use window.emit
goosewobbler e0b4e04
chore: use correct env var
goosewobbler fa6b45a
chore: update webdriver plugin
goosewobbler 19e03fd
refactor(tauri-plugin): enhance JSON schema formatting and permission…
goosewobbler 9a10762
chore: update E2E log display script and CI workflows
goosewobbler d0e8103
chore: add debug information for CI workflows
goosewobbler 38b7ab3
chore: update CodeQL configuration to address false positives
goosewobbler aeb03af
docs: add security note for non-secure cookie handling in WebDriver
goosewobbler fd5e0bc
refactor(tauri-plugin): update event handling to use getCurrentWindow…
goosewobbler 77cd7bc
fix(tauri-plugin): revert to app-level events for tauri-driver compat…
goosewobbler a29ab3a
chore: enhance binary detection in CI workflows
goosewobbler 8323ad8
chore: update tpw again
goosewobbler 6e5d3a2
debug: enhance directory listing in CI workflows for macOS
goosewobbler c50f179
chore: improve macOS binary detection in CI workflows
goosewobbler fcb8efd
chore: refine macOS binary detection in CI workflows
goosewobbler c20882b
chore: update macOS binary detection logic in build manager
goosewobbler b9d1106
chore: enhance Tauri session capabilities and logging tests
goosewobbler 7c3cfbf
chore: update dependency versions and enhance JSON schema validation
goosewobbler e874ebb
chore: refine JSON schema definitions and enhance command execution
goosewobbler 3135a6c
chore: wrap script execution in IIFE for WebDriver compatibility
goosewobbler 5a1fc08
chore: enable NPM OIDC trusted publishing
goosewobbler c4589b9
chore: update permissions for release workflows
goosewobbler 46d4f8a
chore: update script execution wrapping for WebDriver compatibility
goosewobbler dc73fc0
chore: add driver provider option to TauriWorkerService
goosewobbler c899227
chore: enhance log directory naming for driver provider support
goosewobbler ecb9abd
chore: refine script wrapping for embedded WebDriver compatibility
goosewobbler 418ef65
docs: fix repo title
goosewobbler 419d68b
chore: simplify script handling for embedded WebDriver
goosewobbler 505d64f
chore: add `autoInstallTauriDriver` back to standalone opts
goosewobbler 5f77e69
chore: refine script handling for embedded WebDriver
goosewobbler 9000f9c
chore: enhance script execution for embedded WebDriver
goosewobbler d2aff6a
chore: streamline script execution for embedded WebDriver
goosewobbler 2dd2803
chore: fix plugin to evaluate scripts as expressions rather than stat…
goosewobbler ba437a9
feat: implement WebView2 runtime detection from Windows registry
goosewobbler a3c782b
refactor: streamline Edge driver version detection in ensureMsEdgeDriver
goosewobbler 39a4443
feat: enhance embedded WebDriver logging and process management
goosewobbler 43dd43c
feat: add script timeout functionality to platform executors
goosewobbler a5f393e
fix: update unit test mock for browser.execute
goosewobbler 93dc94d
chore: remove deprecated executeAsync from test mocks
goosewobbler 51706f9
fix: restore polling for plugin availability and fix __name test issue
goosewobbler ef56209
refactor: consolidate log handling and improve log capture functionality
goosewobbler c493dbd
refactor: update JSON schemas for permissions and capabilities
goosewobbler 661bc4d
refactor: update JSON schemas for improved consistency and clarity
goosewobbler ad32b2d
refactor: update artifact naming in CI workflows to include architecture
goosewobbler 28ab18b
refactor: improve log directory naming and initialization logic
goosewobbler cd8022d
refactor: enhance logging test structure and exclusions
goosewobbler b1d78c0
refactor: increase script execution timeout for improved reliability
goosewobbler c83cba9
refactor: update dependencies and improve JSON schema consistency
goosewobbler 62b46a6
test: enhance log directory structure based on driver provider
goosewobbler bf95b98
refactor: enhance error handling and status reporting in WebDriver
goosewobbler 86ef8c2
chore: simplify error logging for CoreWebView2 retrieval
goosewobbler b5b05cc
refactor: improve error handling in script execution for WindowsExecutor
goosewobbler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,374 @@ | ||
| name: Tauri E2E Tests - Embedded Provider | ||
|
|
||
| description: 'Runs Tauri end-to-end tests using embedded WebDriver provider (tauri-plugin-webdriver)' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| os: | ||
| description: 'Operating system to run tests on' | ||
| required: true | ||
| type: string | ||
| node-version: | ||
| description: 'Node.js version to use for testing' | ||
| required: true | ||
| type: string | ||
| build-command: | ||
| description: 'Build command for test applications (build or build:mac-universal)' | ||
| type: string | ||
| default: 'build' | ||
| scenario: | ||
| description: 'Test scenario (tauri-basic-embedded)' | ||
| required: true | ||
| type: string | ||
| test-type: | ||
| description: 'Test type (standard, window, multiremote, standalone, deeplink)' | ||
| type: string | ||
| default: 'standard' | ||
| build_id: | ||
| description: 'Build ID from the build job' | ||
| type: string | ||
| required: false | ||
| artifact_size: | ||
| description: 'Size of the build artifact in bytes' | ||
| type: string | ||
| required: false | ||
| cache_key: | ||
| description: 'Cache key to use for downloading package artifacts' | ||
| type: string | ||
| required: false | ||
| tauri_cache_key: | ||
| description: 'Cache key to use for downloading Tauri app binaries' | ||
| type: string | ||
| required: false | ||
|
|
||
| env: | ||
| TURBO_TELEMETRY_DISABLED: 1 | ||
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | ||
| TURBO_TEAM: ${{ secrets.TURBO_TEAM }} | ||
| TURBO_DAEMON: false | ||
|
|
||
| jobs: | ||
| # This job runs Tauri E2E tests using the embedded WebDriver provider | ||
| # The embedded provider works on all platforms including macOS | ||
| e2e-tauri-embedded: | ||
| name: Tauri E2E Tests - Embedded | ||
| runs-on: ${{ inputs.os }} | ||
| strategy: | ||
| # Continue with other tests even if one fails | ||
| fail-fast: false | ||
| steps: | ||
| # Standard checkout with SSH key for private repositories | ||
| - name: 👷 Checkout Repository | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ssh-key: ${{ secrets.DEPLOY_KEY }} | ||
|
|
||
| # Set up Node.js and PNPM using the reusable action | ||
| - name: 🛠️ Setup Development Environment | ||
| uses: ./.github/workflows/actions/setup-workspace | ||
| with: | ||
| node-version: ${{ inputs.node-version }} | ||
|
|
||
| # Install Tauri runtime dependencies on Linux (apps are pre-built, only need runtime libs) | ||
| - name: 🔧 Fix ARM64 Package Sources (Linux) | ||
| if: runner.os == 'Linux' && runner.arch == 'ARM64' | ||
| shell: bash | ||
| run: | | ||
| echo "Fixing ARM64 package sources for Ubuntu..." | ||
| # Add proper arm64 sources for jammy | ||
| echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe multiverse" | sudo tee /etc/apt/sources.list.d/arm64-jammy.list > /dev/null | ||
| echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/arm64-jammy.list > /dev/null | ||
| echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/arm64-jammy.list > /dev/null | ||
| echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/arm64-jammy.list > /dev/null | ||
|
|
||
| # Ensure default sources only use amd64 | ||
| sudo sed -i -e 's/^deb http/deb [arch=amd64] http/' /etc/apt/sources.list | ||
| sudo sed -i -e 's/^deb mirror/deb [arch=amd64] mirror/' /etc/apt/sources.list | ||
|
|
||
| sudo apt-get update | ||
|
|
||
| - name: 🦀 Install Tauri Runtime Dependencies (Linux) | ||
| if: runner.os == 'Linux' | ||
| shell: bash | ||
| run: | | ||
| echo "Installing Tauri runtime dependencies for Linux..." | ||
| # Only add archive.ubuntu.com for amd64, arm64 sources are handled above | ||
| if [ "$(uname -m)" != "aarch64" ]; then | ||
| echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list > /dev/null | ||
| fi | ||
| sudo apt-get update | ||
| sudo apt-get --fix-broken install -y || true | ||
|
|
||
| # Install required packages | ||
| sudo apt-get install -y \ | ||
| libwebkit2gtk-4.1-0 \ | ||
| libgtk-3-0t64 \ | ||
| libayatana-appindicator3-1 | ||
|
|
||
| # NOTE: No Rust toolchain needed - embedded provider doesn't use tauri-driver | ||
| # NOTE: No WebKitWebDriver/msedgedriver needed - embedded provider has built-in WebDriver | ||
|
|
||
| # Download the pre-built packages from the build job | ||
| - name: 📦 Download Package Build Artifacts | ||
| uses: ./.github/workflows/actions/download-archive | ||
| with: | ||
| name: wdio-desktop-mobile | ||
| path: wdio-desktop-mobile-build | ||
| filename: artifact.zip | ||
| cache_key_prefix: wdio-desktop-build | ||
| exact_cache_key: ${{ inputs.cache_key || github.run_id && format('{0}-{1}-{2}-{3}{4}', 'Linux', 'wdio-desktop-build', 'wdio-desktop-mobile', github.run_id, github.run_attempt > 1 && format('-rerun{0}', github.run_attempt) || '') || '' }} | ||
|
|
||
| # Download the pre-built Tauri E2E app binary from the OS-specific build job | ||
| - name: 📦 Download Tauri E2E App Binary | ||
| uses: ./.github/workflows/actions/download-archive | ||
| with: | ||
| name: tauri-e2e-app-${{ runner.os }} | ||
| path: tauri-e2e-app-${{ runner.os }} | ||
| filename: artifact.zip | ||
| cache_key_prefix: tauri-e2e-app | ||
| exact_cache_key: ${{ inputs.tauri_cache_key }} | ||
|
|
||
| # Verify Tauri binaries were extracted correctly | ||
| - name: ✅ Verify Tauri Binaries | ||
| shell: bash | ||
| run: | | ||
| echo "Checking for Tauri binaries in fixtures/e2e-apps/tauri/src-tauri/target/" | ||
|
|
||
| if [ ! -d "fixtures/e2e-apps/tauri" ]; then | ||
| echo "::error::fixtures/e2e-apps/tauri directory not found!" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Find all target directories | ||
| TARGET_DIRS=$(find fixtures/e2e-apps/tauri -name "target" -type d 2>/dev/null || true) | ||
|
|
||
| if [ -z "$TARGET_DIRS" ]; then | ||
| echo "::error::No target directories found! Binaries were not extracted correctly." | ||
| echo "Directory contents:" | ||
| ls -la fixtures/e2e-apps/tauri/ || true | ||
| ls -la fixtures/e2e-apps/tauri/src-tauri/ || true | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "✅ Found target directories:" | ||
| echo "$TARGET_DIRS" | ||
|
|
||
| # Verify binaries exist | ||
| BINARY_COUNT=0 | ||
| for target_dir in $TARGET_DIRS; do | ||
| if [ "${{ runner.os }}" = "Windows" ]; then | ||
| if [ -f "$target_dir/release/*.exe" ] || compgen -G "$target_dir/release/*.exe" > /dev/null; then | ||
| BINARY_COUNT=$((BINARY_COUNT + 1)) | ||
| echo " ✅ Found Windows binary in $target_dir/release/" | ||
| fi | ||
| else | ||
| # Check for executable files on Linux/Mac | ||
| if find "$target_dir/release" -maxdepth 1 -type f -executable 2>/dev/null | grep -q .; then | ||
| BINARY_COUNT=$((BINARY_COUNT + 1)) | ||
| echo " ✅ Found binary in $target_dir/release/" | ||
| fi | ||
| fi | ||
| done | ||
|
|
||
| if [ "$BINARY_COUNT" -eq 0 ]; then | ||
| echo "::error::No binaries found in target directories!" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "✅ Verification complete: Found $BINARY_COUNT app(s) with binaries" | ||
|
|
||
| # Display build information if available | ||
| - name: 📊 Show Build Information | ||
| if: inputs.build_id != '' && inputs.artifact_size != '' | ||
| shell: bash | ||
| run: | | ||
| echo "::notice::Package build: ID=${{ inputs.build_id }}, Size=${{ inputs.artifact_size }} bytes" | ||
| echo "::notice::Tauri binaries: Using pre-built binaries for ${{ runner.os }}" | ||
|
|
||
| # Setup protocol handlers for deeplink testing | ||
| - name: 🔗 Setup Protocol Handlers | ||
| shell: bash | ||
| run: | | ||
| echo "=== Protocol Handler Setup ===" | ||
| echo "Runner OS: ${{ runner.os }}" | ||
| echo "Working directory: $(pwd)" | ||
| echo "Scenario: ${{ inputs.scenario }}" | ||
| echo "" | ||
|
|
||
| echo "Setting up protocol handler for: tauri" | ||
|
|
||
| if [ "${{ runner.os }}" == "Windows" ]; then | ||
| echo "Setting up Windows protocol handler..." | ||
|
|
||
| echo "Checking if src-tauri/target directory exists..." | ||
| ls -la ./fixtures/e2e-apps/tauri/src-tauri/target/ || echo "src-tauri/target/ not found" | ||
| echo "" | ||
|
|
||
| powershell -ExecutionPolicy Bypass -File ./fixtures/e2e-apps/tauri/scripts/setup-protocol-handler.ps1 | ||
| EXIT_CODE=$? | ||
|
|
||
| if [ $EXIT_CODE -ne 0 ]; then | ||
| echo "Error: Protocol handler setup failed for tauri with exit code $EXIT_CODE" | ||
| exit 1 | ||
| fi | ||
|
|
||
| elif [ "${{ runner.os }}" == "Linux" ]; then | ||
| echo "Setting up Linux protocol handler..." | ||
|
|
||
| echo "Checking if src-tauri/target directory exists..." | ||
| ls -la ./fixtures/e2e-apps/tauri/src-tauri/target/ || echo "src-tauri/target/ not found" | ||
| echo "" | ||
|
|
||
| chmod +x ./fixtures/e2e-apps/tauri/scripts/setup-protocol-handler.sh | ||
| ./fixtures/e2e-apps/tauri/scripts/setup-protocol-handler.sh | ||
| EXIT_CODE=$? | ||
|
|
||
| if [ $EXIT_CODE -ne 0 ]; then | ||
| echo "Error: Protocol handler setup failed for tauri with exit code $EXIT_CODE" | ||
| exit 1 | ||
| fi | ||
|
|
||
| else | ||
| echo "macOS: Protocol handlers are registered by the app itself via deep-link configuration" | ||
| echo "No external setup needed - the app registers on first launch" | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "=== Protocol Handler Setup Complete ===" | ||
|
|
||
| # Verify protocol registration after setup | ||
| - name: 🔍 Verify Protocol Registration | ||
| if: runner.os != 'macOS' | ||
| shell: bash | ||
| run: | | ||
| echo "=== Protocol Registration Verification ===" | ||
|
|
||
| if [ "${{ runner.os }}" == "Linux" ]; then | ||
| echo "Linux: Checking xdg-mime registration..." | ||
| xdg-mime query default x-scheme-handler/testapp || echo "WARNING: No handler registered for testapp://" | ||
|
|
||
| echo "" | ||
| echo "Checking .desktop file..." | ||
| if [ -f "$HOME/.local/share/applications/tauri-e2e-app-testapp.desktop" ]; then | ||
| echo "✅ Desktop file exists:" | ||
| cat "$HOME/.local/share/applications/tauri-e2e-app-testapp.desktop" | ||
| else | ||
| echo "❌ Desktop file NOT found at ~/.local/share/applications/tauri-e2e-app-testapp.desktop" | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "Checking desktop database..." | ||
| update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true | ||
|
|
||
| elif [ "${{ runner.os }}" == "Windows" ]; then | ||
| echo "Windows: Checking registry..." | ||
| reg query "HKCR\testapp" 2>nul || echo "❌ Protocol NOT registered in HKCR\testapp" | ||
| reg query "HKCR\testapp\shell\open\command" 2>nul || echo "❌ Command key NOT found" | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "=== Protocol Verification Complete ===" | ||
|
|
||
| # Dynamically generate the Tauri test commands to run | ||
| - name: 🪄 Generate Tauri Test Execution Plan | ||
| id: gen-test | ||
| uses: actions/github-script@v8 | ||
| with: | ||
| result-encoding: string | ||
| script: | | ||
| const ALLOWED_SCENARIOS = ['tauri-basic-embedded']; | ||
| const ALLOWED_TEST_TYPES = ['standard', 'window', 'multiremote', 'standalone', 'deeplink']; | ||
| const scenario = '${{ inputs.scenario }}'.trim(); | ||
| const testType = '${{ inputs.test-type }}'.trim(); | ||
| if (!ALLOWED_SCENARIOS.includes(scenario)) { | ||
| core.setFailed(`Invalid scenario: "${scenario}". Allowed: ${ALLOWED_SCENARIOS.join(', ')}`); | ||
| return ''; | ||
| } | ||
| if (!ALLOWED_TEST_TYPES.includes(testType)) { | ||
| core.setFailed(`Invalid test-type: "${testType}". Allowed: ${ALLOWED_TEST_TYPES.join(', ')}`); | ||
| return ''; | ||
| } | ||
| const baseCommand = `test:e2e:${scenario}`; | ||
| return testType !== 'standard' ? `${baseCommand}:${testType}` : baseCommand; | ||
|
|
||
| # Cleanup any lingering processes from previous test runs | ||
| - name: 🧹 Cleanup Lingering Processes (Windows) | ||
| if: runner.os == 'Windows' | ||
| shell: pwsh | ||
| run: | | ||
| Write-Host "Cleaning up any lingering app processes..." | ||
| Get-Process -Name "tauri-e2e-app" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue | ||
| Write-Host "Windows cleanup complete" | ||
|
|
||
| - name: 🧹 Cleanup Lingering Processes (Unix) | ||
| if: runner.os != 'Windows' | ||
| shell: bash | ||
| run: | | ||
| echo "Cleaning up any lingering app processes..." | ||
| pkill -9 -f tauri-e2e-app || echo "No tauri-e2e-app processes found" | ||
| echo "Unix cleanup complete" | ||
|
|
||
| # Run the Tauri E2E tests with embedded provider | ||
| - name: 🧪 Execute Tauri E2E Tests - Embedded Provider | ||
| shell: pwsh | ||
| working-directory: e2e | ||
| env: | ||
| # Enable Rust backtraces for debugging | ||
| RUST_BACKTRACE: '1' | ||
| # Disable AT-SPI accessibility bus warnings | ||
| NO_AT_BRIDGE: '1' | ||
| # Use embedded WebDriver configuration | ||
| WDIO_TAURI_DRIVER_PROVIDER: 'embedded' | ||
| run: | | ||
| if ($env:RUNNER_OS -eq "Linux") { | ||
| # Use 16-bit color depth to avoid pixbuf issues with GTK/WebKit | ||
| xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x16" ` | ||
| pnpm run ${{ steps.gen-test.outputs.result }} | ||
| } else { | ||
| pnpm run ${{ steps.gen-test.outputs.result }} | ||
| } | ||
|
|
||
| # Cleanup processes after tests complete | ||
| - name: 🧹 Post-Test Cleanup (Windows) | ||
| if: always() && runner.os == 'Windows' | ||
| shell: pwsh | ||
| run: | | ||
| Write-Host "Cleaning up test processes..." | ||
| Get-Process -Name "tauri-e2e-app" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue | ||
| Write-Host "Post-test Windows cleanup complete" | ||
|
|
||
| - name: 🧹 Post-Test Cleanup (Unix) | ||
| if: always() && runner.os != 'Windows' | ||
| shell: bash | ||
| run: | | ||
| echo "Cleaning up test processes..." | ||
| pkill -9 -f tauri-e2e-app || echo "No tauri-e2e-app processes found" | ||
| echo "Post-test Unix cleanup complete" | ||
|
|
||
| # Show comprehensive debug information on failure | ||
| - name: 🐛 Debug Information | ||
| if: always() | ||
| shell: bash | ||
| run: pnpm run ci:e2e:logs | ||
|
|
||
| # Upload logs as artifacts for later analysis | ||
| - name: 📦 Upload Test Logs | ||
| if: always() | ||
| continue-on-error: true | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: e2e-tauri-embedded-logs-${{ inputs.os }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.scenario }}-${{ inputs.test-type }} | ||
| path: e2e/logs/**/*.log | ||
| retention-days: 90 | ||
| if-no-files-found: warn | ||
|
|
||
| # Provide an interactive debugging session on failure | ||
| - name: 🐛 Debug Build on Failure | ||
| if: failure() | ||
| uses: goosewobbler/vscode-server-action@v1.3.0 | ||
| with: | ||
| timeout: '300000' | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.