This is an automated email from the ASF dual-hosted git repository.
rahulvats pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 1e7e40ee595 Add CI workflow for UI e2e tests (#58901)
1e7e40ee595 is described below
commit 1e7e40ee5958a82f6b1a9ee323a221758580c01b
Author: Rahul Vats <[email protected]>
AuthorDate: Wed Dec 10 14:15:17 2025 +0530
Add CI workflow for UI e2e tests (#58901)
Add CI workflow for UI e2e tests (#58901)
---
.github/workflows/additional-prod-image-tests.yml | 40 +++++
.github/workflows/ci-amd-arm.yml | 2 +
.github/workflows/ui-e2e-tests.yml | 150 +++++++++++++++++++
airflow-core/src/airflow/ui/tests/e2e/README.md | 141 +++++++++++++-----
.../doc/images/output_testing_ui-e2e-tests.svg | 80 ++++++----
.../doc/images/output_testing_ui-e2e-tests.txt | 2 +-
.../src/airflow_breeze/commands/common_options.py | 6 +-
.../airflow_breeze/commands/testing_commands.py | 153 ++++++++++++-------
.../commands/testing_commands_config.py | 8 +
.../airflow_breeze/utils/docker_compose_utils.py | 165 +++++++++++++++++++++
.../src/airflow_breeze/utils/selective_checks.py | 4 +
11 files changed, 630 insertions(+), 121 deletions(-)
diff --git a/.github/workflows/additional-prod-image-tests.yml
b/.github/workflows/additional-prod-image-tests.yml
index 69958d1ee2b..d4e2d4061a8 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -64,6 +64,10 @@ on: # yamllint disable-line rule:truthy
description: "Whether to use uv"
required: true
type: string
+ run-ui-e2e-tests:
+ description: "Whether to run UI e2e tests (true/false)"
+ required: true
+ type: string
permissions:
contents: read
jobs:
@@ -218,6 +222,42 @@ jobs:
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "remote_log"
+ test-ui-e2e-chromium:
+ name: "Chromium UI e2e tests with PROD image"
+ uses: ./.github/workflows/ui-e2e-tests.yml
+ with:
+ workflow-name: "Chromium UI e2e tests"
+ runners: ${{ inputs.runners }}
+ platform: ${{ inputs.platform }}
+ default-python-version: "${{ inputs.default-python-version }}"
+ use-uv: ${{ inputs.use-uv }}
+ browser: "chromium"
+ if: inputs.run-ui-e2e-tests == 'true'
+
+ test-ui-e2e-firefox:
+ name: "Firefox UI e2e tests with PROD image"
+ uses: ./.github/workflows/ui-e2e-tests.yml
+ with:
+ workflow-name: "Firefox UI e2e tests"
+ runners: ${{ inputs.runners }}
+ platform: ${{ inputs.platform }}
+ default-python-version: "${{ inputs.default-python-version }}"
+ use-uv: ${{ inputs.use-uv }}
+ browser: "firefox"
+ if: inputs.run-ui-e2e-tests == 'true'
+
+ test-ui-e2e-webkit:
+ name: "WebKit UI e2e tests with PROD image"
+ uses: ./.github/workflows/ui-e2e-tests.yml
+ with:
+ workflow-name: "WebKit UI e2e tests"
+ runners: ${{ inputs.runners }}
+ platform: ${{ inputs.platform }}
+ default-python-version: "${{ inputs.default-python-version }}"
+ use-uv: ${{ inputs.use-uv }}
+ browser: "webkit"
+ if: inputs.run-ui-e2e-tests == 'true'
+
airflow-ctl-integration-tests:
timeout-minutes: 60
name: "Airflow CTL integration tests with PROD image"
diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml
index 1ab45e39154..ca1954acfdb 100644
--- a/.github/workflows/ci-amd-arm.yml
+++ b/.github/workflows/ci-amd-arm.yml
@@ -118,6 +118,7 @@ jobs:
run-task-sdk-integration-tests: ${{
steps.selective-checks.outputs.run-task-sdk-integration-tests }}
runner-type: ${{ steps.selective-checks.outputs.runner-type }}
run-ui-tests: ${{ steps.selective-checks.outputs.run-ui-tests }}
+ run-ui-e2e-tests: ${{ steps.selective-checks.outputs.run-ui-e2e-tests }}
run-unit-tests: ${{ steps.selective-checks.outputs.run-unit-tests }}
run-www-tests: ${{ steps.selective-checks.outputs.run-www-tests }}
selected-providers-list-as-string: >-
@@ -790,6 +791,7 @@ jobs:
run-task-sdk-integration-tests: ${{
needs.build-info.outputs.run-task-sdk-integration-tests }}
canary-run: ${{ needs.build-info.outputs.canary-run }}
use-uv: ${{ needs.build-info.outputs.use-uv }}
+ run-ui-e2e-tests: ${{ needs.build-info.outputs.run-ui-e2e-tests }}
if: needs.build-info.outputs.prod-image-build == 'true'
tests-kubernetes:
diff --git a/.github/workflows/ui-e2e-tests.yml
b/.github/workflows/ui-e2e-tests.yml
new file mode 100644
index 00000000000..802d6266582
--- /dev/null
+++ b/.github/workflows/ui-e2e-tests.yml
@@ -0,0 +1,150 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+
+name: UI End-to-End Tests
+
+permissions:
+ contents: read
+on: # yamllint disable-line rule:truthy
+ workflow_dispatch:
+ inputs:
+ workflow-name:
+ description: "Name of the test"
+ type: string
+ required: true
+ runners:
+ description: "The array of labels (in json form) determining runners."
+ type: string
+ default: '["ubuntu-24.04"]'
+ platform:
+ description: "Platform for the build - 'linux/amd64' or 'linux/arm64'"
+ type: string
+ default: 'linux/amd64'
+ default-python-version:
+ description: "Which version of python should be used by default"
+ type: string
+ default: '3.10'
+ use-uv:
+ description: "Whether to use uv to build the image (true/false)"
+ type: string
+ default: 'true'
+ docker-image-tag:
+ description: "Tag of the Docker image to test"
+ type: string
+ required: true
+ browser:
+ description: "Browser to test (chromium, firefox, webkit, all)"
+ type: string
+ default: "all"
+
+ workflow_call:
+ inputs:
+ workflow-name:
+ description: "Name of the test"
+ type: string
+ required: true
+ runners:
+ description: "The array of labels (in json form) determining runners."
+ required: true
+ type: string
+ platform:
+ description: "Platform for the build - 'linux/amd64' or 'linux/arm64'"
+ required: true
+ type: string
+ default-python-version:
+ description: "Which version of python should be used by default"
+ required: true
+ type: string
+ use-uv:
+ description: "Whether to use uv to build the image (true/false)"
+ required: true
+ type: string
+ docker-image-tag:
+ description: "Tag of the Docker image to test"
+ type: string
+ default: ""
+ browser:
+ description: "Browser to test (chromium, firefox, webkit, all)"
+ type: string
+ default: "all"
+
+jobs:
+ test-ui-e2e-tests:
+ timeout-minutes: 90
+ name: ${{ inputs.workflow-name || 'UI E2E Tests' }}
+ runs-on: ${{ fromJSON(inputs.runners || '["ubuntu-24.04"]') }}
+ env:
+ PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version || '3.10'
}}"
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_USERNAME: ${{ github.actor }}
+ VERBOSE: "true"
+ BROWSER: "${{ inputs.browser || 'all' }}"
+ PLATFORM: "${{ inputs.platform || 'linux/amd64' }}"
+ USE_UV: "${{ inputs.use-uv || 'true' }}"
+ steps:
+ - name: "Cleanup repo"
+ shell: bash
+ run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm
-rf /workspace/*"
+ - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #
v4.2.2
+ with:
+ fetch-depth: 2
+ persist-credentials: false
+ - name: "Prepare breeze & PROD image: ${{ env.PYTHON_MAJOR_MINOR_VERSION
}}"
+ uses: ./.github/actions/prepare_breeze_and_image
+ with:
+ platform: ${{ inputs.platform }}
+ image-type: "prod"
+ python: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
+ use-uv: ${{ inputs.use-uv }}
+ make-mnt-writeable-and-cleanup: true
+ id: breeze
+ if: github.event_name != 'workflow_dispatch'
+ - name: "Install Breeze (manual trigger)"
+ uses: ./.github/actions/breeze
+ if: github.event_name == 'workflow_dispatch'
+ - name: "Setup pnpm"
+ uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 #
v4.0.0
+ with:
+ version: 9
+ run_install: false
+ - name: "Setup node"
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #
v4.4.0
+ with:
+ node-version: 21
+ - name: "Install Playwright browsers and dependencies"
+ run: |
+ cd airflow-core/src/airflow/ui
+ pnpm install --frozen-lockfile
+ pnpm exec playwright install --with-deps
+ - name: "Test UI e2e tests"
+ run: breeze testing ui-e2e-tests --browser "$BROWSER"
+ env:
+ DOCKER_IMAGE: "${{ inputs.docker-image-tag || '' }}"
+ - name: "Upload test results"
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
# v4.6.2
+ with:
+ name: "playwright-report-${{ env.BROWSER }}"
+ path: |
+ airflow-core/src/airflow/ui/playwright-report/
+ airflow-core/src/airflow/ui/test-results/
+ retention-days: 7
+ if-no-files-found: 'warn'
+ if: always()
diff --git a/airflow-core/src/airflow/ui/tests/e2e/README.md
b/airflow-core/src/airflow/ui/tests/e2e/README.md
index 3c805f3b3f4..fb5c8895875 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/README.md
+++ b/airflow-core/src/airflow/ui/tests/e2e/README.md
@@ -17,75 +17,138 @@
under the License.
-->
-# Airflow UI End-to-End Tests
+# UI End-to-End Tests
-UI automation tests using Playwright for critical Airflow workflows.
-
-## Prerequisites
-
-**Requires running Airflow with example DAGs:**
-
-- Airflow UI running on `http://localhost:28080` (default)
-- Admin user: `admin/admin`
-- Example DAGs loaded (uses `example_bash_operator`)
+End-to-end tests for the Airflow UI using Playwright.
## Running Tests
-### Using Breeze
+### Using Breeze (Recommended)
+
+The easiest way to run the tests:
```bash
-# Basic run
breeze testing ui-e2e-tests
-# Specific test with browser visible
-breeze testing ui-e2e-tests --test-pattern "dag-trigger.spec.ts" --headed
+# Run specific browser
+breeze testing ui-e2e-tests --browser firefox
+
+# Run specific test
+breeze testing ui-e2e-tests --test-pattern "dag-trigger.spec.ts"
-# Different browsers
-breeze testing ui-e2e-tests --browser firefox --headed
-breeze testing ui-e2e-tests --browser webkit --headed
+# Debug mode
+breeze testing ui-e2e-tests --debug-e2e
+
+# See the browser
+breeze testing ui-e2e-tests --headed
```
-### Using pnpm directly
+### Direct Execution
+
+If you already have Airflow running on `http://localhost:8080`:
```bash
cd airflow-core/src/airflow/ui
-
-# Install dependencies
pnpm install
-pnpm exec playwright install
-
-# Run tests
-pnpm test:e2e:headed # Show browser
-pnpm test:e2e:ui # Interactive debugging
+pnpm test:e2e:install
+pnpm test:e2e
```
-## Test Structure
+## CI Integration
+
+Tests run in GitHub Actions via workflow dispatch. The workflow uses `breeze
testing ui-e2e-tests` which handles starting Airflow with docker-compose,
running the tests, and cleanup.
+
+To run manually:
+
+1. Go to Actions → UI End-to-End Tests
+2. Click Run workflow
+3. Select browser and other options
+
+## Directory Structure
```
tests/e2e/
-├── pages/ # Page Object Models
+├── pages/ # Page objects
+│ ├── BasePage.ts
+│ ├── LoginPage.ts
+│ └── DagsPage.ts
└── specs/ # Test files
+ └── dag-trigger.spec.ts
```
-## Configuration
+## Writing Tests
-Set environment variables if needed:
+We use the Page Object Model pattern:
-```bash
-export AIRFLOW_UI_BASE_URL=http://localhost:28080
-export TEST_USERNAME=admin
-export TEST_PASSWORD=admin
-export TEST_DAG_ID=example_bash_operator
+```typescript
+// pages/DagPage.ts
+export class DagPage extends BasePage {
+ readonly pauseButton: Locator;
+
+ constructor(page: Page) {
+ super(page);
+ this.pauseButton = page.locator('[data-testid="dag-pause"]');
+ }
+
+ async pause() {
+ await this.pauseButton.click();
+ }
+}
+
+// specs/dag.spec.ts
+test('pause DAG', async ({ page }) => {
+ const dagPage = new DagPage(page);
+ await dagPage.goto();
+ await dagPage.pause();
+ await expect(dagPage.pauseButton).toHaveAttribute('aria-pressed', 'true');
+});
```
+## Configuration
+
+Environment variables (with defaults):
+
+- `AIRFLOW_UI_BASE_URL` - Airflow URL (default: `http://localhost:8080`)
+- `TEST_USERNAME` - Username (default: `airflow`)
+- `TEST_PASSWORD` - Password (default: `airflow`)
+- `TEST_DAG_ID` - Test DAG ID (default: `example_bash_operator`)
+
## Debugging
-```bash
-# Step through tests
-breeze testing ui-e2e-tests --debug-e2e
+View test report after running locally:
-# View test report
+```bash
pnpm test:e2e:report
```
-Find test artifacts in `test-results/` and reports in `playwright-report/`.
+When tests fail in CI, check the uploaded artifacts for screenshots and HTML
reports.
+
+## Breeze Options
+
+```bash
+breeze testing ui-e2e-tests --help
+```
+
+Common options:
+
+- `--browser` - chromium, firefox, webkit, or all
+- `--headed` - Show browser window
+- `--debug-e2e` - Enable Playwright inspector
+- `--ui-mode` - Interactive UI mode
+- `--test-pattern` - Run specific test file
+- `--workers` - Number of parallel workers
+
+## Test Coverage
+
+Current tests:
+
+- Login flow
+- DAG triggering
+- DAG run status
+
+Planned tests:
+
+- DAG pause/unpause
+- Task details
+- Connections
+- Variables
diff --git a/dev/breeze/doc/images/output_testing_ui-e2e-tests.svg
b/dev/breeze/doc/images/output_testing_ui-e2e-tests.svg
index 53b4f84dfc3..1beb52fbd72 100644
--- a/dev/breeze/doc/images/output_testing_ui-e2e-tests.svg
+++ b/dev/breeze/doc/images/output_testing_ui-e2e-tests.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 733.1999999999999"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 879.5999999999999"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -37,13 +37,13 @@
.breeze-testing-ui-e2e-tests-r3 { fill: #c5c8c6;font-weight: bold }
.breeze-testing-ui-e2e-tests-r4 { fill: #68a0b3;font-weight: bold }
.breeze-testing-ui-e2e-tests-r5 { fill: #868887 }
-.breeze-testing-ui-e2e-tests-r6 { fill: #8d7b39 }
-.breeze-testing-ui-e2e-tests-r7 { fill: #98a84b;font-weight: bold }
+.breeze-testing-ui-e2e-tests-r6 { fill: #98a84b;font-weight: bold }
+.breeze-testing-ui-e2e-tests-r7 { fill: #8d7b39 }
</style>
<defs>
<clipPath id="breeze-testing-ui-e2e-tests-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="682.1999999999999" />
+ <rect x="0" y="0" width="1463.0" height="828.5999999999999" />
</clipPath>
<clipPath id="breeze-testing-ui-e2e-tests-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -126,9 +126,27 @@
<clipPath id="breeze-testing-ui-e2e-tests-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-testing-ui-e2e-tests-line-27">
+ <rect x="0" y="660.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-testing-ui-e2e-tests-line-28">
+ <rect x="0" y="684.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-testing-ui-e2e-tests-line-29">
+ <rect x="0" y="709.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-testing-ui-e2e-tests-line-30">
+ <rect x="0" y="733.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-testing-ui-e2e-tests-line-31">
+ <rect x="0" y="757.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-testing-ui-e2e-tests-line-32">
+ <rect x="0" y="782.3" width="1464" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="731.2" rx="8"/><text
class="breeze-testing-ui-e2e-tests-title" fill="#c5c8c6" text-anchor="middle"
x="740" y="27">Command: testing ui-e2e-tests</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="877.6" rx="8"/><text
class="breeze-testing-ui-e2e-tests-title" fill="#c5c8c6" text-anchor="middle"
x="740" y="27">Command: testing ui-e2e-tests</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -143,29 +161,35 @@
</text><text class="breeze-testing-ui-e2e-tests-r1" x="1464" y="68.8"
textLength="12.2" clip-path="url(#breeze-testing-ui-e2e-tests-line-2)">
</text><text class="breeze-testing-ui-e2e-tests-r1" x="12.2" y="93.2"
textLength="500.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-3)">Run UI End-to-End tests using Playwright.</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="93.2" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-3)">
</text><text class="breeze-testing-ui-e2e-tests-r1" x="1464" y="117.6"
textLength="12.2" clip-path="url(#breeze-testing-ui-e2e-tests-line-4)">
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="142"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-5)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="142" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-5)"> UI End-to-End test options </text><text
class="breeze-testing-ui-e2e-tests-r5" x="366" y="142" textLength="1073.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-5)">────────────────── [...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="166.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-6)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="166.4" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-6)">--browser</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="166.4" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-6)">Browser to use for e2e tests</tex
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="190.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-7)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="190.8" textLength="97.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-7)">--headed</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="190.8" textLength="610"
clip-path="url(#breeze-testing-ui-e2e-tests-line-7)">Run e2e tests in headed mode (sh
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="215.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-8)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="215.2" textLength="134.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-8)">--debug-e2e</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="215.2" textLength="329.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-8)">Run e2e tests in debug mode</te
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="239.6"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-9)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="239.6" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-9)">--ui-mode</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="239.6" textLength="427"
clip-path="url(#breeze-testing-ui-e2e-tests-line-9)">Run e2e tests in Playwright UI 
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="264"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-10)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="264" textLength="170.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-10)">--test-pattern</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="264" textLength="402.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-10)">Glob pattern to filter test fil
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="288.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-11)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="288.4" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-11)">--workers</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="288.4" textLength="488"
clip-path="url(#breeze-testing-ui-e2e-tests-line-11)">Number of parallel workers for e
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="312.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-12)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="312.8" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-12)">--timeout</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="312.8" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-12)">Test timeout in milliseconds</text><text
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="337.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-13)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="337.2" textLength="122"
clip-path="url(#breeze-testing-ui-e2e-tests-line-13)">--reporter</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="337.2" textLength="329.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-13)">Test reporter for e2e tests</text><t
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="361.6"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-14)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="361.6" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-14)">
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="386"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-15)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="386" textLength="378.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-15)"> Test environment for UI tests </text><text
class="breeze-testing-ui-e2e-tests-r5" x="402.6" y="386" textLength="1037"
clip-path="url(#breeze-testing-ui-e2e-tests-line-15)">─────── [...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="410.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-16)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="410.4" textLength="256.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-16)">--airflow-ui-base-url</text><text
class="breeze-testing-ui-e2e-tests-r1" x="329.4" y="410.4" textLength="488"
clip-path="url(#breeze-testing-ui-e2e-tests-line-16)">Base URL for Airflow UI
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-17)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="434.8" textLength="256.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-17)">--test-admin-username</text><text
class="breeze-testing-ui-e2e-tests-r1" x="329.4" y="434.8" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-17)">Admin username for e2e
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="459.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-18)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="459.2" textLength="256.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-18)">--test-admin-password</text><text
class="breeze-testing-ui-e2e-tests-r1" x="329.4" y="459.2" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-18)">Admin password for e2e
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="483.6"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-19)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="483.6" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-19)">
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="508"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-20)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="508" textLength="402.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-20)"> Advanced flags for UI e2e tests </text><text
class="breeze-testing-ui-e2e-tests-r5" x="427" y="508" textLength="1012.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-20)"> [...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="532.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-21)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="532.4" textLength="268.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-21)">--force-reinstall-deps</text><text
class="breeze-testing-ui-e2e-tests-r1" x="341.6" y="532.4" textLength="378.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-21)">Force reinstall UI depend
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="556.8"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-22)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="556.8" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-22)">
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="581.2"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-23)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="581.2" textLength="195.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-23)"> Common options </text><text
class="breeze-testing-ui-e2e-tests-r5" x="219.6" y="581.2" textLength="1220"
clip-path="url(#breeze-testing-ui-e2e-tests-line-23)">───────────────────────────────
[...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="605.6"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-24)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="605.6" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-24)">--dry-run</text><text
class="breeze-testing-ui-e2e-tests-r7" x="158.6" y="605.6" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-24)">-D</text><text
class="breeze-testing-ui-e2e-tests-r1" [...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="630"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-25)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="630" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-25)">--verbose</text><text
class="breeze-testing-ui-e2e-tests-r7" x="158.6" y="630" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-25)">-v</text><text
class="breeze-testing-ui-e2e-tests-r1" x="207 [...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="654.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-26)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="654.4" textLength="73.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-26)">--help</text><text
class="breeze-testing-ui-e2e-tests-r7" x="158.6" y="654.4" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-26)">-h</text><text
class="breeze-testing-ui-e2e-tests-r1" x="2 [...]
-</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="678.8"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-27)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="678.8" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-27)">
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="142"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-5)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="142" textLength="268.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-5)"> Docker image options </text><text
class="breeze-testing-ui-e2e-tests-r5" x="292.8" y="142" textLength="1146.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-5)">───────────────────────────
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="166.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-6)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="166.4" textLength="97.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-6)">--python</text><text
class="breeze-testing-ui-e2e-tests-r6" x="280.6" y="166.4" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-6)">-p</text><text
class="breeze-testing-ui-e2e-tests-r1" x="32 [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="190.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-7)">│</text><text
class="breeze-testing-ui-e2e-tests-r5" x="329.4" y="190.8" textLength="732"
clip-path="url(#breeze-testing-ui-e2e-tests-line-7)">[default: 3.10]                               
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="215.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-8)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="215.2" textLength="146.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-8)">--image-name</text><text
class="breeze-testing-ui-e2e-tests-r6" x="280.6" y="215.2" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-8)">-n</text><text
class="breeze-testing-ui-e2e-tests-r1" [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="239.6"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-9)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="239.6" textLength="231.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-9)">--github-repository</text><text
class="breeze-testing-ui-e2e-tests-r6" x="280.6" y="239.6" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-9)">-g</text><text
class="breeze-testing-ui-e2e-tes [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="264"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="264" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-10)">
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="288.4"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-11)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="288.4" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-11)"> UI End-to-End test options </text><text
class="breeze-testing-ui-e2e-tests-r5" x="366" y="288.4" textLength="1073.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-11)">───────── [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="312.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-12)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="312.8" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-12)">--browser</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="312.8" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-12)">Browser to use for e2e tests</
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="337.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-13)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="337.2" textLength="97.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-13)">--headed</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="337.2" textLength="610"
clip-path="url(#breeze-testing-ui-e2e-tests-line-13)">Run e2e tests in headed mode 
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="361.6"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-14)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="361.6" textLength="134.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-14)">--debug-e2e</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="361.6" textLength="329.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-14)">Run e2e tests in debug mode<
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-15)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="386" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-15)">--ui-mode</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="386" textLength="427"
clip-path="url(#breeze-testing-ui-e2e-tests-line-15)">Run e2e tests in Playwright UI mo
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="410.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-16)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="410.4" textLength="170.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-16)">--test-pattern</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="410.4" textLength="402.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-16)">Glob pattern to filter test
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-17)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="434.8" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-17)">--workers</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="434.8" textLength="488"
clip-path="url(#breeze-testing-ui-e2e-tests-line-17)">Number of parallel workers for e
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="459.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-18)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="459.2" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-18)">--timeout</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="459.2" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-18)">Test timeout in milliseconds</text><text
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="483.6"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-19)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="483.6" textLength="122"
clip-path="url(#breeze-testing-ui-e2e-tests-line-19)">--reporter</text><text
class="breeze-testing-ui-e2e-tests-r1" x="244" y="483.6" textLength="329.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-19)">Test reporter for e2e tests</text><t
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="508"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-20)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="508" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-20)">
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="532.4"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-21)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="532.4" textLength="378.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-21)"> Test environment for UI tests </text><text
class="breeze-testing-ui-e2e-tests-r5" x="402.6" y="532.4" textLength="1037"
clip-path="url(#breeze-testing-ui-e2e-tests-line-21)">─ [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="556.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-22)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="556.8" textLength="256.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-22)">--airflow-ui-base-url</text><text
class="breeze-testing-ui-e2e-tests-r1" x="329.4" y="556.8" textLength="488"
clip-path="url(#breeze-testing-ui-e2e-tests-line-22)">Base URL for Airflow UI
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="581.2"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-23)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="581.2" textLength="256.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-23)">--test-admin-username</text><text
class="breeze-testing-ui-e2e-tests-r1" x="329.4" y="581.2" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-23)">Admin username for e2e
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="605.6"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-24)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="605.6" textLength="256.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-24)">--test-admin-password</text><text
class="breeze-testing-ui-e2e-tests-r1" x="329.4" y="605.6" textLength="341.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-24)">Admin password for e2e
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="630"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-25)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="630" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-25)">
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="654.4"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-26)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="654.4" textLength="402.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line-26)"> Advanced flags for UI e2e tests </text><text
class="breeze-testing-ui-e2e-tests-r5" x="427" y="654.4" textLength="1012.6"
clip-path="url(#breeze-testing-ui-e2e-tests-line [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="678.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-27)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="678.8" textLength="268.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-27)">--force-reinstall-deps</text><text
class="breeze-testing-ui-e2e-tests-r1" x="341.6" y="678.8" textLength="378.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-27)">Force reinstall UI depend
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="703.2"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-28)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="703.2" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-28)">
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="727.6"
textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-29)">╭─</text><text
class="breeze-testing-ui-e2e-tests-r5" x="24.4" y="727.6" textLength="195.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-29)"> Common options </text><text
class="breeze-testing-ui-e2e-tests-r5" x="219.6" y="727.6" textLength="1220"
clip-path="url(#breeze-testing-ui-e2e-tests-line-29)">───────────────────────────────
[...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="752"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-30)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="752" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-30)">--dry-run</text><text
class="breeze-testing-ui-e2e-tests-r6" x="158.6" y="752" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-30)">-D</text><text
class="breeze-testing-ui-e2e-tests-r1" x="207 [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="776.4"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-31)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="776.4" textLength="109.8"
clip-path="url(#breeze-testing-ui-e2e-tests-line-31)">--verbose</text><text
class="breeze-testing-ui-e2e-tests-r6" x="158.6" y="776.4" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-31)">-v</text><text
class="breeze-testing-ui-e2e-tests-r1" [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="800.8"
textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-32)">│</text><text
class="breeze-testing-ui-e2e-tests-r4" x="24.4" y="800.8" textLength="73.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-32)">--help</text><text
class="breeze-testing-ui-e2e-tests-r6" x="158.6" y="800.8" textLength="24.4"
clip-path="url(#breeze-testing-ui-e2e-tests-line-32)">-h</text><text
class="breeze-testing-ui-e2e-tests-r1" x="2 [...]
+</text><text class="breeze-testing-ui-e2e-tests-r5" x="0" y="825.2"
textLength="1464"
clip-path="url(#breeze-testing-ui-e2e-tests-line-33)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-ui-e2e-tests-r1" x="1464" y="825.2" textLength="12.2"
clip-path="url(#breeze-testing-ui-e2e-tests-line-33)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_testing_ui-e2e-tests.txt
b/dev/breeze/doc/images/output_testing_ui-e2e-tests.txt
index 475047eb78e..713d531dc28 100644
--- a/dev/breeze/doc/images/output_testing_ui-e2e-tests.txt
+++ b/dev/breeze/doc/images/output_testing_ui-e2e-tests.txt
@@ -1 +1 @@
-37da219fd2514ea3a6027056c903360c
+d64fae90ee8e43f6f76c8e58efbb706e
diff --git a/dev/breeze/src/airflow_breeze/commands/common_options.py
b/dev/breeze/src/airflow_breeze/commands/common_options.py
index 9e058176598..4d8f3fc97c6 100644
--- a/dev/breeze/src/airflow_breeze/commands/common_options.py
+++ b/dev/breeze/src/airflow_breeze/commands/common_options.py
@@ -572,7 +572,7 @@ option_platform_single = click.option(
option_airflow_ui_base_url = click.option(
"--airflow-ui-base-url",
help="Base URL for Airflow UI during e2e tests",
- default="http://localhost:28080",
+ default="http://localhost:8080",
show_default=True,
envvar="AIRFLOW_UI_BASE_URL",
)
@@ -642,7 +642,7 @@ option_e2e_reporter = click.option(
option_test_admin_username = click.option(
"--test-admin-username",
help="Admin username for e2e tests",
- default="admin",
+ default="airflow",
show_default=True,
envvar="TEST_ADMIN_USERNAME",
)
@@ -650,7 +650,7 @@ option_test_admin_username = click.option(
option_test_admin_password = click.option(
"--test-admin-password",
help="Admin password for e2e tests",
- default="admin",
+ default="airflow",
show_default=True,
envvar="TEST_ADMIN_PASSWORD",
)
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index 0dce9cf466a..e0feb90dad3 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -1435,6 +1435,9 @@ def airflow_e2e_tests(
allow_extra_args=True,
),
)
+@option_python
+@option_image_name
+@option_github_repository
@option_airflow_ui_base_url
@option_browser
@option_debug_e2e
@@ -1451,6 +1454,9 @@ def airflow_e2e_tests(
@option_verbose
@click.argument("extra_playwright_args", nargs=-1,
type=click.Path(path_type=str))
def ui_e2e_tests(
+ python: str,
+ image_name: str | None,
+ github_repository: str,
airflow_ui_base_url: str,
browser: str,
debug_e2e: bool,
@@ -1466,76 +1472,118 @@ def ui_e2e_tests(
extra_playwright_args: tuple,
):
"""Run UI end-to-end tests using Playwright."""
+ import shutil
import sys
+ import tempfile
from pathlib import Path
+ from airflow_breeze.params.build_prod_params import BuildProdParams
from airflow_breeze.utils.console import get_console
from airflow_breeze.utils.run_utils import check_pnpm_installed,
run_command
from airflow_breeze.utils.shared_options import get_dry_run, get_verbose
perform_environment_checks()
-
check_pnpm_installed()
airflow_root = Path(__file__).resolve().parents[5]
ui_dir = airflow_root / "airflow-core" / "src" / "airflow" / "ui"
+ docker_compose_source = (
+ airflow_root / "airflow-core" / "docs" / "howto" / "docker-compose" /
"docker-compose.yaml"
+ )
if not ui_dir.exists():
get_console().print(f"[error]UI directory not found: {ui_dir}[/]")
sys.exit(1)
- env_vars = {
- "AIRFLOW_UI_BASE_URL": airflow_ui_base_url,
- "TEST_USERNAME": test_admin_username,
- "TEST_PASSWORD": test_admin_password,
- "TEST_DAG_ID": "example_bash_operator",
- }
+ tmp_dir = Path(tempfile.mkdtemp(prefix="airflow-ui-e2e-"))
+ get_console().print(f"[info]Using temporary directory: {tmp_dir}[/]")
+
+ try:
+ from airflow_breeze.utils.docker_compose_utils import (
+ ensure_image_exists_and_build_if_needed,
+ setup_airflow_docker_compose_environment,
+ start_docker_compose_and_wait_for_health,
+ stop_docker_compose,
+ )
+
+ if image_name is None:
+ image_name = os.environ.get("DOCKER_IMAGE")
+ if image_name is None or image_name.strip() == "":
+ build_params = BuildProdParams(python=python,
github_repository=github_repository)
+ image_name = build_params.airflow_image_name
+
+ get_console().print(f"[info]Running UI E2E tests with PROD image:
{image_name}[/]")
+ ensure_image_exists_and_build_if_needed(image_name, python)
+
+ env_vars = {
+ "AIRFLOW_UID": str(os.getuid()),
+ "AIRFLOW__CORE__LOAD_EXAMPLES": "true",
+ "AIRFLOW_IMAGE_NAME": image_name,
+ }
+
+ tmp_dir, dot_env = setup_airflow_docker_compose_environment(
+ docker_compose_source=docker_compose_source,
+ tmp_dir=tmp_dir,
+ env_vars=env_vars,
+ )
+
+ result = start_docker_compose_and_wait_for_health(tmp_dir,
airflow_base_url=airflow_ui_base_url)
+ if result != 0:
+ sys.exit(result)
+
+ get_console().print("[success]Airflow is ready! Login with default
credentials: airflow/airflow[/]")
+
+ env_vars = {
+ "AIRFLOW_UI_BASE_URL": airflow_ui_base_url,
+ "TEST_USERNAME": test_admin_username,
+ "TEST_PASSWORD": test_admin_password,
+ "TEST_DAG_ID": "example_bash_operator",
+ }
+
+ if force_reinstall_deps:
+ clean_cmd = ["pnpm", "install", "--force"]
+ if not get_dry_run():
+ run_command(clean_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose())
+ else:
+ install_cmd = ["pnpm", "install"]
+ if not get_dry_run():
+ run_command(install_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose())
+
+ install_browsers_cmd = ["pnpm", "exec", "playwright", "install"]
+ if browser != "all":
+ install_browsers_cmd.append(browser)
- if force_reinstall_deps:
- clean_cmd = ["pnpm", "install", "--force"]
- if not get_dry_run():
- run_command(clean_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose())
- else:
- install_cmd = ["pnpm", "install"]
if not get_dry_run():
- run_command(install_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose())
-
- install_browsers_cmd = ["pnpm", "exec", "playwright", "install"]
- if browser != "all":
- install_browsers_cmd.append(browser)
-
- if not get_dry_run():
- run_command(install_browsers_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose())
-
- get_console().print(f"[info]Using Airflow at: {airflow_ui_base_url}[/]")
-
- playwright_cmd = ["pnpm", "exec", "playwright", "test"]
-
- if browser != "all":
- playwright_cmd.extend(["--project", browser])
- if headed:
- playwright_cmd.append("--headed")
- if debug_e2e:
- playwright_cmd.append("--debug")
- if ui_mode:
- playwright_cmd.append("--ui")
- if workers > 1:
- playwright_cmd.extend(["--workers", str(workers)])
- if timeout != 60000:
- playwright_cmd.extend(["--timeout", str(timeout)])
- if reporter != "html":
- playwright_cmd.extend(["--reporter", reporter])
- if test_pattern:
- playwright_cmd.append(test_pattern)
- if extra_playwright_args:
- playwright_cmd.extend(extra_playwright_args)
-
- get_console().print(f"[info]Running: {' '.join(playwright_cmd)}[/]")
-
- if get_dry_run():
- return
+ run_command(install_browsers_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose())
+
+ get_console().print(f"[info]Using Airflow at:
{airflow_ui_base_url}[/]")
+
+ playwright_cmd = ["pnpm", "exec", "playwright", "test"]
+
+ if browser != "all":
+ playwright_cmd.extend(["--project", browser])
+ if headed:
+ playwright_cmd.append("--headed")
+ if debug_e2e:
+ playwright_cmd.append("--debug")
+ if ui_mode:
+ playwright_cmd.append("--ui")
+ if workers > 1:
+ playwright_cmd.extend(["--workers", str(workers)])
+ if timeout != 60000:
+ playwright_cmd.extend(["--timeout", str(timeout)])
+ if reporter != "html":
+ playwright_cmd.extend(["--reporter", reporter])
+ if test_pattern:
+ playwright_cmd.append(test_pattern)
+ if extra_playwright_args:
+ playwright_cmd.extend(extra_playwright_args)
+
+ get_console().print(f"[info]Running: {' '.join(playwright_cmd)}[/]")
+
+ if get_dry_run():
+ return
- try:
result = run_command(
playwright_cmd, cwd=ui_dir, env=env_vars,
verbose_override=get_verbose(), check=False
)
@@ -1544,11 +1592,16 @@ def ui_e2e_tests(
if report_path.exists():
get_console().print(f"[info]Report: file://{report_path}[/]")
+ stop_docker_compose(tmp_dir)
+ shutil.rmtree(tmp_dir, ignore_errors=True)
+
if result.returncode != 0:
sys.exit(result.returncode)
except Exception as e:
get_console().print(f"[error]{str(e)}[/]")
+ stop_docker_compose(tmp_dir)
+ shutil.rmtree(tmp_dir, ignore_errors=True)
sys.exit(1)
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
index 78f327bd45b..68a7150f7f3 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
@@ -315,6 +315,14 @@ TESTING_PARAMETERS: dict[str, list[dict[str, str |
list[str]]]] = {
}
],
"breeze testing ui-e2e-tests": [
+ {
+ "name": "Docker image options",
+ "options": [
+ "--python",
+ "--image-name",
+ "--github-repository",
+ ],
+ },
{
"name": "UI End-to-End test options",
"options": [
diff --git a/dev/breeze/src/airflow_breeze/utils/docker_compose_utils.py
b/dev/breeze/src/airflow_breeze/utils/docker_compose_utils.py
new file mode 100644
index 00000000000..dfd39859425
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/utils/docker_compose_utils.py
@@ -0,0 +1,165 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Utilities for managing Airflow docker-compose environments in tests."""
+
+from __future__ import annotations
+
+import os
+import sys
+import tempfile
+import time
+import urllib.error
+import urllib.request
+from collections.abc import Callable
+from pathlib import Path
+from shutil import copyfile
+
+import yaml
+from cryptography.fernet import Fernet
+
+from airflow_breeze.utils.console import get_console
+from airflow_breeze.utils.run_utils import run_command
+
+
+def setup_airflow_docker_compose_environment(
+ docker_compose_source: Path,
+ tmp_dir: Path | None = None,
+ env_vars: dict[str, str] | None = None,
+ docker_compose_modifications: Callable[[dict, Path], dict] | None = None,
+) -> tuple[Path, Path]:
+ """Set up a temporary directory with docker-compose files for Airflow."""
+ if tmp_dir is None:
+ tmp_dir = Path(tempfile.mkdtemp(prefix="airflow-docker-compose-"))
+
+ docker_compose_path = tmp_dir / "docker-compose.yaml"
+ copyfile(docker_compose_source, docker_compose_path)
+
+ for subdir in ("dags", "logs", "plugins", "config"):
+ (tmp_dir / subdir).mkdir(exist_ok=True)
+
+ env_vars = env_vars or {}
+
+ if "FERNET_KEY" not in env_vars:
+ env_vars["FERNET_KEY"] = Fernet.generate_key().decode()
+
+ if "AIRFLOW_UID" not in env_vars:
+ env_vars["AIRFLOW_UID"] = str(os.getuid())
+
+ dot_env_file = tmp_dir / ".env"
+ env_content = "\n".join([f"{key}={value}" for key, value in
env_vars.items()])
+ dot_env_file.write_text(env_content + "\n")
+
+ if docker_compose_modifications:
+ with open(docker_compose_path) as f:
+ compose_config = yaml.safe_load(f)
+ compose_config = docker_compose_modifications(compose_config, tmp_dir)
+ with open(docker_compose_path, "w") as f:
+ yaml.dump(compose_config, f, default_flow_style=False)
+
+ return tmp_dir, dot_env_file
+
+
+def start_docker_compose_and_wait_for_health(
+ tmp_dir: Path,
+ airflow_base_url: str = "http://localhost:8080",
+ max_wait: int = 180,
+ check_interval: int = 5,
+) -> int:
+ """Start docker-compose and wait for Airflow to be healthy."""
+ health_check_url = f"{airflow_base_url}/api/v2/monitor/health"
+
+ get_console().print("[info]Starting Airflow services with
docker-compose...[/]")
+ compose_up_result = run_command(
+ ["docker", "compose", "up", "-d"], cwd=tmp_dir, check=False,
verbose_override=True
+ )
+ if compose_up_result.returncode != 0:
+ get_console().print("[error]Failed to start docker-compose[/]")
+ return compose_up_result.returncode
+
+ get_console().print(f"[info]Waiting for Airflow at
{health_check_url}...[/]")
+ elapsed = 0
+ while elapsed < max_wait:
+ try:
+ response = urllib.request.urlopen(health_check_url, timeout=5)
+ if response.status == 200:
+ get_console().print("[success]Airflow is ready![/]")
+ return 0
+ except (urllib.error.URLError, urllib.error.HTTPError, Exception):
+ time.sleep(check_interval)
+ elapsed += check_interval
+ if elapsed % 15 == 0:
+ get_console().print(f"[info]Still waiting...
({elapsed}s/{max_wait}s)[/]")
+
+ get_console().print(f"[error]Airflow did not become ready within
{max_wait} seconds[/]")
+ get_console().print("[info]Docker compose logs:[/]")
+ run_command(["docker", "compose", "logs"], cwd=tmp_dir, check=False)
+ return 1
+
+
+def stop_docker_compose(tmp_dir: Path, remove_volumes: bool = True) -> None:
+ """Stop and cleanup docker-compose services."""
+ get_console().print("[info]Stopping docker-compose services...[/]")
+ cmd = ["docker", "compose", "down"]
+ if remove_volumes:
+ cmd.append("-v")
+ run_command(cmd, cwd=tmp_dir, check=False)
+ get_console().print("[success]Docker-compose cleaned up.[/]")
+
+
+def ensure_image_exists_and_build_if_needed(image_name: str, python: str) ->
None:
+ inspect_result = run_command(
+ ["docker", "inspect", image_name], check=False, capture_output=True,
text=True
+ )
+ if inspect_result.returncode != 0:
+ get_console().print(f"[error]Error when inspecting PROD image:
{inspect_result.returncode}[/]")
+ get_console().print(inspect_result.stderr or "", highlight=False)
+ if "no such object" in inspect_result.stderr.lower():
+ get_console().print(
+ f"The image {image_name} does not exist locally. "
+ f"Building it now with: breeze prod-image build --python
{python}"
+ )
+ build_result = run_command(["breeze", "prod-image", "build",
"--python", python], check=False)
+ if build_result.returncode != 0:
+ get_console().print("[error]Failed to build image[/]")
+ sys.exit(1)
+ get_console().print(f"[info]Tagging the built image as
{image_name}[/]")
+ list_images_result = run_command(
+ [
+ "docker",
+ "images",
+ "--format",
+ "{{.Repository}}:{{.Tag}}",
+ "--filter",
+ "reference=*/airflow:latest",
+ ],
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ if list_images_result.returncode == 0 and
list_images_result.stdout.strip():
+ built_image = list_images_result.stdout.strip().split("\n")[0]
+ get_console().print(f"[info]Found built image:
{built_image}[/]")
+ tag_result = run_command(["docker", "tag", built_image,
image_name], check=False)
+ if tag_result.returncode != 0:
+ get_console().print(f"[error]Failed to tag image
{built_image} as {image_name}[/]")
+ sys.exit(1)
+ get_console().print(f"[success]Successfully tagged
{built_image} as {image_name}[/]")
+ else:
+ get_console().print("[warning]Could not find built image to
tag. Docker compose may fail.[/]")
+ else:
+ get_console().print(f"[error]Failed to inspect image
{image_name}[/]")
+ sys.exit(1)
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index a23ac0c919a..e7ed94e27c9 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -868,6 +868,10 @@ class SelectiveChecks:
def run_ui_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.UI_FILES)
+ @cached_property
+ def run_ui_e2e_tests(self) -> bool:
+ return self._should_be_run(FileGroupForCi.UI_FILES)
+
@cached_property
def run_amazon_tests(self) -> bool:
if self.providers_test_types_list_as_strings_in_json == "[]":