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:&#160;testing&#160;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:&#160;testing&#160;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&#160;UI&#160;End-to-End&#160;tests&#160;using&#160;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)">&#160;UI&#160;End-to-End&#160;test&#160;options&#160;</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&#160;to&#160;use&#160;for&#160;e2e&#160;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&#160;e2e&#160;tests&#160;in&#160;headed&#160;mode&#160;(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&#160;e2e&#160;tests&#160;in&#160;debug&#160;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&#160;e2e&#160;tests&#160;in&#160;Playwright&#160;UI&#160
 [...]
-</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&#160;pattern&#160;to&#160;filter&#160;test&#160;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&#160;of&#160;parallel&#160;workers&#160;for&#160;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&#160;timeout&#160;in&#160;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&#160;reporter&#160;for&#160;e2e&#160;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)">&#160;Test&#160;environment&#160;for&#160;UI&#160;tests&#160;</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&#160;URL&#160;for&#160;Airflow&#160;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&#160;username&#160;for&#160;e2e&#16
 [...]
-</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&#160;password&#160;for&#160;e2e&#16
 [...]
-</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)">&#160;Advanced&#160;flags&#160;for&#160;UI&#160;e2e&#160;tests&#160;</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&#160;reinstall&#160;UI&#160;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)">&#160;Common&#160;options&#160;</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)">&#160;Docker&#160;image&#160;options&#160;</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:&#160;3.10]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#16
 [...]
+</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)">&#160;UI&#160;End-to-End&#160;test&#160;options&#160;</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&#160;to&#160;use&#160;for&#160;e2e&#160;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&#160;e2e&#160;tests&#160;in&#160;headed&#160;mode&#160;
 [...]
+</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&#160;e2e&#160;tests&#160;in&#160;debug&#160;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&#160;e2e&#160;tests&#160;in&#160;Playwright&#160;UI&#160;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&#160;pattern&#160;to&#160;filter&#160;test&#1
 [...]
+</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&#160;of&#160;parallel&#160;workers&#160;for&#160;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&#160;timeout&#160;in&#160;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&#160;reporter&#160;for&#160;e2e&#160;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)">&#160;Test&#160;environment&#160;for&#160;UI&#160;tests&#160;</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&#160;URL&#160;for&#160;Airflow&#160;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&#160;username&#160;for&#160;e2e&#16
 [...]
+</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&#160;password&#160;for&#160;e2e&#16
 [...]
+</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)">&#160;Advanced&#160;flags&#160;for&#160;UI&#160;e2e&#160;tests&#160;</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&#160;reinstall&#160;UI&#160;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)">&#160;Common&#160;options&#160;</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 == "[]":

Reply via email to