diff --git a/airflow-core/src/airflow/ui/.gitignore b/airflow-core/src/airflow/ui/.gitignore index 1f7e9bfb98934..1b6dfa5ecf028 100644 --- a/airflow-core/src/airflow/ui/.gitignore +++ b/airflow-core/src/airflow/ui/.gitignore @@ -1 +1,7 @@ openapi.merged.json + +# Playwright E2E test results and reports +/test-results/ +/playwright-report/ +/tests/e2e/test-results/ +/tests/e2e/playwright-report/ diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 49b0bdc1faa9a..def7a1a89fa1c 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -16,7 +16,13 @@ "preview": "vite preview", "codegen": "openapi-merge-cli && openapi-rq -i openapi.merged.json -c axios --format prettier -o openapi-gen --operationId", "test": "vitest run", - "coverage": "vitest run --coverage" + "coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", + "test:e2e:headed": "playwright test --headed", + "test:e2e:report": "playwright show-report", + "test:e2e:install": "playwright install" }, "dependencies": { "@chakra-ui/anatomy": "^2.3.4", @@ -105,7 +111,8 @@ "vite": "^7.1.11", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^3.2.4", - "web-worker": "^1.5.0" + "web-worker": "^1.5.0", + "@playwright/test": "^1.56.1" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/airflow-core/src/airflow/ui/playwright.config.ts b/airflow-core/src/airflow/ui/playwright.config.ts new file mode 100644 index 0000000000000..7393fa5ded1ee --- /dev/null +++ b/airflow-core/src/airflow/ui/playwright.config.ts @@ -0,0 +1,104 @@ +/*! + * 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. + */ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright configuration for Airflow UI End-to-End Tests + */ +export const testConfig = { + credentials: { + password: process.env.TEST_PASSWORD ?? "admin", + username: process.env.TEST_USERNAME ?? "admin", + }, + testDag: { + id: process.env.TEST_DAG_ID ?? "example_bash_operator", + }, +}; + +export default defineConfig({ + expect: { + timeout: 5000, + }, + forbidOnly: process.env.CI !== undefined && process.env.CI !== "", + fullyParallel: true, + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + launchOptions: { + args: [ + "--start-maximized", + "--disable-web-security", + "--disable-features=VizDisplayCompositor", + "--window-size=1920,1080", + "--window-position=0,0", + ], + channel: "chrome", + ignoreDefaultArgs: ["--enable-automation"], + }, + }, + }, + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + launchOptions: { + args: [ + "--width=1920", + "--height=1080", + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-web-security", + ], + }, + }, + }, + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + launchOptions: { + args: [], + }, + }, + }, + ], + reporter: [ + ["html", { outputFolder: "playwright-report" }], + ["json", { outputFile: "test-results/results.json" }], + process.env.CI !== undefined && process.env.CI !== "" ? ["github"] : ["list"], + ], + + retries: process.env.CI !== undefined && process.env.CI !== "" ? 2 : 0, + + testDir: "./tests/e2e/specs", + + timeout: 30_000, + use: { + actionTimeout: 10_000, + baseURL: process.env.AIRFLOW_UI_BASE_URL ?? "http://localhost:28080", + screenshot: "only-on-failure", + trace: "on-first-retry", + video: "retain-on-failure", + viewport: undefined, + }, + + workers: process.env.CI !== undefined && process.env.CI !== "" ? 2 : undefined, +}); diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index 62d3e66c2737e..1cf8c881fc875 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: '@eslint/js': specifier: ^9.25.1 version: 9.26.0 + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 '@stylistic/eslint-plugin': specifier: ^2.13.0 version: 2.13.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) @@ -872,6 +875,11 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + engines: {node: '>=18'} + hasBin: true + '@remix-run/router@1.23.0': resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} @@ -2764,6 +2772,11 @@ packages: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3747,6 +3760,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -5448,6 +5471,10 @@ snapshots: '@pkgr/core@0.2.4': {} + '@playwright/test@1.56.1': + dependencies: + playwright: 1.56.1 + '@remix-run/router@1.23.0': {} '@rolldown/pluginutils@1.0.0-beta.32': {} @@ -8098,6 +8125,9 @@ snapshots: dependencies: minipass: 3.3.6 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -9320,6 +9350,14 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 + playwright-core@1.56.1: {} + + playwright@1.56.1: + dependencies: + playwright-core: 1.56.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} diff --git a/airflow-core/src/airflow/ui/tests/e2e/README.md b/airflow-core/src/airflow/ui/tests/e2e/README.md new file mode 100644 index 0000000000000..3c805f3b3f4a5 --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/README.md @@ -0,0 +1,91 @@ + + +# Airflow 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`) + +## Running Tests + +### Using Breeze + +```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 + +# Different browsers +breeze testing ui-e2e-tests --browser firefox --headed +breeze testing ui-e2e-tests --browser webkit --headed +``` + +### Using pnpm directly + +```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 +``` + +## Test Structure + +``` +tests/e2e/ +├── pages/ # Page Object Models +└── specs/ # Test files +``` + +## Configuration + +Set environment variables if needed: + +```bash +export AIRFLOW_UI_BASE_URL=http://localhost:28080 +export TEST_USERNAME=admin +export TEST_PASSWORD=admin +export TEST_DAG_ID=example_bash_operator +``` + +## Debugging + +```bash +# Step through tests +breeze testing ui-e2e-tests --debug-e2e + +# View test report +pnpm test:e2e:report +``` + +Find test artifacts in `test-results/` and reports in `playwright-report/`. diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/BasePage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/BasePage.ts new file mode 100644 index 0000000000000..ae63c31ddec4b --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/BasePage.ts @@ -0,0 +1,61 @@ +/*! + * 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. + */ +import type { Page, Locator } from "@playwright/test"; + +/** + * Base Page Object + */ +export class BasePage { + public readonly page: Page; + public readonly welcomeHeading: Locator; + + public constructor(page: Page) { + this.page = page; + this.welcomeHeading = page.locator('h2.chakra-heading:has-text("Welcome")'); + } + + public async isLoggedIn(): Promise { + try { + await this.welcomeHeading.waitFor({ timeout: 30_000 }); + + return true; + } catch { + const currentUrl = this.page.url(); + + return !currentUrl.includes("/login"); + } + } + + public async maximizeBrowser(): Promise { + try { + await this.page.setViewportSize({ height: 1080, width: 1920 }); + } catch { + // Viewport size could not be set + } + } + + public async navigateTo(path: string): Promise { + await this.page.goto(path); + await this.waitForPageLoad(); + } + + public async waitForPageLoad(): Promise { + await this.page.waitForLoadState("networkidle"); + } +} diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts new file mode 100644 index 0000000000000..6880dd9af50d4 --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts @@ -0,0 +1,183 @@ +/*! + * 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. + */ +import type { Locator, Page } from "@playwright/test"; +import { BasePage } from "tests/e2e/pages/BasePage"; + +import type { DAGRunResponse } from "openapi/requests/types.gen"; + +/** + * DAGs Page Object + */ +export class DagsPage extends BasePage { + // Page URLs + public static get dagsListUrl(): string { + return "/dags"; + } + + // Core page elements + public readonly confirmButton: Locator; + public readonly dagsTable: Locator; + public readonly stateElement: Locator; + public readonly triggerButton: Locator; + + public constructor(page: Page) { + super(page); + this.dagsTable = page.locator('div:has(a[href*="/dags/"])'); + this.triggerButton = page.locator('button[aria-label="Trigger Dag"]:has-text("Trigger")'); + this.confirmButton = page.locator('button:has-text("Trigger")').nth(1); + this.stateElement = page.locator('*:has-text("State") + *').first(); + } + + // URL builders for dynamic paths + public static getDagDetailUrl(dagName: string): string { + return `/dags/${dagName}`; + } + + public static getDagRunDetailsUrl(dagName: string, dagRunId: string): string { + return `/dags/${dagName}/runs/${dagRunId}/details`; + } + + /** + * Navigate to DAGs list page + */ + public async navigate(): Promise { + await this.navigateTo(DagsPage.dagsListUrl); + } + + /** + * Navigate to DAG detail page + */ + public async navigateToDagDetail(dagName: string): Promise { + await this.navigateTo(DagsPage.getDagDetailUrl(dagName)); + } + + /** + * Trigger a DAG run + */ + public async triggerDag(dagName: string): Promise { + await this.navigateToDagDetail(dagName); + await this.triggerButton.waitFor({ state: "visible", timeout: 10_000 }); + await this.triggerButton.click(); + const dagRunId = await this.handleTriggerDialog(); + + return dagRunId; + } + + public async verifyDagRunStatus(dagName: string, dagRunId: string | null): Promise { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (dagRunId === null || dagRunId === undefined || dagRunId === "") { + return; + } + + await this.page.goto(DagsPage.getDagRunDetailsUrl(dagName, dagRunId), { + timeout: 15_000, + waitUntil: "domcontentloaded", + }); + + await this.page.waitForTimeout(2000); + + const maxWaitTime = 5 * 60 * 1000; + const checkInterval = 10_000; + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitTime) { + const currentStatus = await this.getCurrentDagRunStatus(); + + if (currentStatus === "success") { + return; + } else if (currentStatus === "failed") { + throw new Error(`DAG run failed: ${dagRunId}`); + } + + await this.page.waitForTimeout(checkInterval); + + await this.page.reload({ waitUntil: "domcontentloaded" }); + + await this.page.waitForTimeout(2000); + } + + throw new Error(`DAG run did not complete within 5 minutes: ${dagRunId}`); + } + + private async getCurrentDagRunStatus(): Promise { + try { + const statusText = await this.stateElement.textContent().catch(() => ""); + const status = statusText?.trim() ?? ""; + + switch (status) { + case "Failed": + return "failed"; + case "Queued": + return "queued"; + case "Running": + return "running"; + case "Success": + return "success"; + default: + return "unknown"; + } + } catch { + return "unknown"; + } + } + + private async handleTriggerDialog(): Promise { + await this.page.waitForTimeout(1000); + + const responsePromise = this.page + + .waitForResponse( + (response) => { + const url = response.url(); + + const method = response.request().method(); + + return ( + method === "POST" && Boolean(url.includes("dagRuns")) && Boolean(!url.includes("hitlDetails")) + ); + }, + { timeout: 10_000 }, + ) + + .catch(() => undefined); + + await this.confirmButton.waitFor({ state: "visible", timeout: 8000 }); + + await this.page.waitForTimeout(2000); + await this.confirmButton.click({ force: true }); + + const apiResponse = await responsePromise; + + if (apiResponse) { + try { + const responseBody = await apiResponse.text(); + const responseJson = JSON.parse(responseBody) as DAGRunResponse; + + if (Boolean(responseJson.dag_run_id)) { + return responseJson.dag_run_id; + } + } catch { + // Response parsing failed + } + } + + // eslint-disable-next-line unicorn/no-null + return null; + } +} diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/LoginPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/LoginPage.ts new file mode 100644 index 0000000000000..eda41657f8362 --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/LoginPage.ts @@ -0,0 +1,93 @@ +/*! + * 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. + */ +import { expect } from "@playwright/test"; +import type { Locator, Page } from "@playwright/test"; +import { BasePage } from "tests/e2e/pages/BasePage"; + +/** + * Login Page Object + */ +export class LoginPage extends BasePage { + // Page URLs + public static get loginUrl(): string { + return "/auth/login"; + } + + public readonly errorMessage: Locator; + public readonly loginButton: Locator; + public readonly passwordInput: Locator; + public readonly usernameInput: Locator; + + public constructor(page: Page) { + super(page); + + this.usernameInput = page.locator('input[name="username"]'); + this.passwordInput = page.locator('input[name="password"]'); + // Support both SimpleAuthManager and FabAuthManager login buttons + this.loginButton = page.locator('button[type="submit"], input[type="submit"]'); + this.errorMessage = page.locator('span:has-text("Invalid credentials")'); + } + + public async expectLoginSuccess(): Promise { + const currentUrl: string = this.page.url(); + + if (currentUrl.includes("/login")) { + throw new Error(`Expected to be redirected after login, but still on: ${currentUrl}`); + } + + const isLoggedIn: boolean = await this.isLoggedIn(); + + expect(isLoggedIn).toBe(true); + } + public async login(username: string, password: string): Promise { + await this.usernameInput.fill(username); + await this.passwordInput.fill(password); + await this.loginButton.click(); + + try { + await this.page.waitForURL( + (url: URL) => { + const urlString: string = url.toString(); + + return !urlString.includes("/login"); + }, + { timeout: 15_000 }, + ); + } catch (error: unknown) { + const hasError: boolean = await this.errorMessage.isVisible().catch(() => false); + + if (hasError) { + throw new Error("Login failed with error message visible"); + } + + throw error; + } + } + + public async navigate(): Promise { + await this.maximizeBrowser(); + + await this.navigateTo(LoginPage.loginUrl); + } + + public async navigateAndLogin(username: string, password: string): Promise { + await this.navigate(); + await this.login(username, password); + } +} diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-trigger.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-trigger.spec.ts new file mode 100644 index 0000000000000..b7c57de3cb700 --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-trigger.spec.ts @@ -0,0 +1,56 @@ +/*! + * 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. + */ +import { test } from "@playwright/test"; +import { testConfig } from "playwright.config"; +import { DagsPage } from "tests/e2e/pages/DagsPage"; +import { LoginPage } from "tests/e2e/pages/LoginPage"; + +/** + * DAG Trigger E2E Tests + */ + +test.describe("DAG Trigger Workflow", () => { + let loginPage: LoginPage; + let dagsPage: DagsPage; + + // Test configuration from centralized config + + const testCredentials = testConfig.credentials; + + const testDagId = testConfig.testDag.id; + + test.beforeEach(({ page }) => { + loginPage = new LoginPage(page); + dagsPage = new DagsPage(page); + }); + + test("should successfully trigger a DAG run", async () => { + test.setTimeout(7 * 60 * 1000); + + await loginPage.navigateAndLogin(testCredentials.username, testCredentials.password); + + await loginPage.expectLoginSuccess(); + + const dagRunId = await dagsPage.triggerDag(testDagId); + + if (Boolean(dagRunId)) { + await dagsPage.verifyDagRunStatus(testDagId, dagRunId); + } + }); +}); diff --git a/airflow-core/src/airflow/ui/tsconfig.app.json b/airflow-core/src/airflow/ui/tsconfig.app.json index d19c8a8651dcb..59bc815960aa6 100644 --- a/airflow-core/src/airflow/ui/tsconfig.app.json +++ b/airflow-core/src/airflow/ui/tsconfig.app.json @@ -24,8 +24,10 @@ "baseUrl": ".", "paths": { "src/*": ["./src/*"], - "openapi/*": ["./openapi-gen/*"] + "openapi/*": ["./openapi-gen/*"], + "tests/*": ["./tests/*"], + "playwright.config": ["./playwright.config.ts"] } }, - "include": ["src"] + "include": ["src", "tests/**/*.ts", "playwright.config.ts"] } diff --git a/airflow-core/src/airflow/ui/tsconfig.dev.json b/airflow-core/src/airflow/ui/tsconfig.dev.json index a8107d588fd7a..def547860e8fb 100644 --- a/airflow-core/src/airflow/ui/tsconfig.dev.json +++ b/airflow-core/src/airflow/ui/tsconfig.dev.json @@ -8,5 +8,5 @@ "strict": true, "target": "ESNext" }, - "include": ["./*.ts", "./*.js", "./rules/*.js", "./rules/*.ts"] + "include": ["./*.ts", "./*.js", "./rules/*.js", "./rules/*.ts", "./tests/**/*.ts"] } diff --git a/airflow-core/src/airflow/ui/vite.config.ts b/airflow-core/src/airflow/ui/vite.config.ts index 7e49f32a1c822..81bb9e8981246 100644 --- a/airflow-core/src/airflow/ui/vite.config.ts +++ b/airflow-core/src/airflow/ui/vite.config.ts @@ -44,6 +44,7 @@ export default defineConfig({ }, css: true, environment: "happy-dom", + exclude: ["**/node_modules/**", "**/dist/**", "tests/e2e/**"], globals: true, mockReset: true, restoreMocks: true, diff --git a/dev/breeze/doc/05_test_commands.rst b/dev/breeze/doc/05_test_commands.rst index ce937fc7f49e9..9b062a1b2d201 100644 --- a/dev/breeze/doc/05_test_commands.rst +++ b/dev/breeze/doc/05_test_commands.rst @@ -373,6 +373,43 @@ You can override the ``DOCKER_IMAGE`` environment variable to point to the image The Airflow E2E tests are in ``airflow-e2e-tests/`` folder in the main repo. +Running Airflow UI E2E tests +............................. + +You can use Breeze to run the Airflow UI End-to-End tests using Playwright. These tests validate +critical user workflows in the Airflow web interface across multiple browsers (Chromium, Firefox, WebKit). + +.. code-block:: bash + + breeze testing ui-e2e-tests + +For example, to run a specific test pattern in headed mode: + +.. code-block:: bash + + breeze testing ui-e2e-tests --test-pattern "dag-trigger.spec.ts" --headed + +You can also run tests in different browsers: + +.. code-block:: bash + + breeze testing ui-e2e-tests --browser firefox --headed + +Or run tests in Playwright's UI mode for debugging: + +.. code-block:: bash + + breeze testing ui-e2e-tests --ui-mode + +.. image:: ./images/output_testing_ui-e2e-tests.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_testing_ui-e2e-tests.svg + :width: 100% + :alt: Breeze testing ui-e2e-tests + +The tests use Page Object Model pattern and are located in ``airflow-core/src/airflow/ui/tests/e2e/`` folder. +The tests require a running Airflow instance (typically ``http://localhost:28080``) and will install +Playwright browsers automatically if needed. + Running Kubernetes tests ------------------------ diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg index fe81bc3a7eeaf..63e5006c43ddc 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg @@ -1,4 +1,4 @@ - +