Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions airflow/ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Playwright
test-results/
playwright-report/
.playwright/
playwright/.cache/
11 changes: 9 additions & 2 deletions airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
"preview": "vite preview",
"codegen": "openapi-rq -i \"../api_fastapi/core_api/openapi/v1-generated.yaml\" -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:chromium": "playwright test --project=chromium",
"test:e2e:firefox": "playwright test --project=firefox",
"test:e2e:webkit": "playwright test --project=webkit"
},
"dependencies": {
"@chakra-ui/anatomy": "^2.2.2",
Expand Down Expand Up @@ -53,6 +59,7 @@
},
"devDependencies": {
"@7nohe/openapi-react-query-codegen": "^1.6.0",
"@playwright/test": "^1.48.2",
"@eslint/compat": "^1.1.1",
"@eslint/js": "^9.10.0",
"@stylistic/eslint-plugin": "^2.8.0",
Expand Down Expand Up @@ -86,4 +93,4 @@
"vitest": "^2.1.9",
"web-worker": "^1.3.0"
}
}
}
40 changes: 40 additions & 0 deletions airflow/ui/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*!
* 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";

export default defineConfig({
testDir: "./tests/e2e/specs",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: process.env.AIRFLOW_BASE_URL || "http://localhost:8080",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
});
143 changes: 143 additions & 0 deletions airflow/ui/tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Airflow UI End-to-End Tests

This directory contains E2E tests for the Apache Airflow UI using Playwright.

## Prerequisites

- Node.js (v16 or later)
- pnpm package manager
- A running Airflow instance (for tests to interact with)

## Setup

1. Install dependencies:
```bash
cd airflow/ui
pnpm install
```

2. Install Playwright browsers:
```bash
pnpm exec playwright install
```

## Running Tests

### Run all E2E tests:
```bash
pnpm test:e2e
```

### Run tests in UI mode (interactive):
```bash
pnpm test:e2e:ui
```

### Run tests in debug mode:
```bash
pnpm test:e2e:debug
```

### Run tests on a specific browser:
```bash
pnpm test:e2e:chromium # Chrome
pnpm test:e2e:firefox # Firefox
pnpm test:e2e:webkit # Safari
```

### Run a specific test file:
```bash
pnpm exec playwright test tests/e2e/specs/variables.spec.ts
```

## Configuration

Test configuration is managed in `testConfig.ts`. You can override values using environment variables:

```bash
# Set custom Airflow URL
export AIRFLOW_BASE_URL=http://localhost:8080

# Set custom credentials
export AIRFLOW_USERNAME=admin
export AIRFLOW_PASSWORD=admin

# Run tests
pnpm test:e2e
```

## Test Structure

```
tests/e2e/
├── pages/ # Page Object Models
│ ├── BasePage.ts # Base class for all pages
│ ├── LoginPage.ts # Login page interactions
│ └── VariablesPage.ts # Variables page interactions
├── specs/ # Test specifications
│ └── variables.spec.ts # Variables page tests
└── testConfig.ts # Test configuration
```

## Page Object Model (POM)

All tests follow the Page Object Model pattern:
- **Page Objects** encapsulate UI interactions
- **Test Specs** use page objects to test functionality
- This keeps tests maintainable and readable

## Writing New Tests

1. Create a new Page Object in `pages/` (extend `BasePage`)
2. Create a new test spec in `specs/`
3. Use the page object methods in your tests

Example:
```typescript
import { test } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
import { MyNewPage } from "../pages/MyNewPage";

test.describe("My Feature", () => {
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login();
});

test("should do something", async ({ page }) => {
const myPage = new MyNewPage(page);
await myPage.doSomething();
// Add assertions
});
});
```

## Reports

After running tests, view the HTML report:
```bash
pnpm exec playwright show-report
```

## Troubleshooting

### Tests failing to connect to Airflow

Make sure Airflow is running and accessible at the URL specified in `testConfig.ts` or `AIRFLOW_BASE_URL` environment variable.

### Authentication issues

Verify that the credentials in `testConfig.ts` or environment variables (`AIRFLOW_USERNAME`, `AIRFLOW_PASSWORD`) are correct.

### Timeout errors

Increase timeouts in `testConfig.ts` if your Airflow instance is slow to respond.

## Contributing

When adding new E2E tests:
- Follow the existing POM pattern
- Ensure tests clean up their own data in `afterAll` hooks
- Use unique test data prefixes to avoid conflicts
- Make tests work across all browsers (Chromium, Firefox, WebKit)
- Avoid hardcoded values - use `testConfig` or dynamic generation
73 changes: 73 additions & 0 deletions airflow/ui/tests/e2e/pages/BasePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*!
* 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";

export class BasePage {
readonly page: Page;

constructor(page: Page) {
this.page = page;
}

async navigateTo(path: string): Promise<void> {
await this.page.goto(path);
}

async waitForElement(selector: string, timeout = 30000): Promise<Locator> {
return this.page.waitForSelector(selector, { timeout });
}

async waitForNavigation(action: () => Promise<void>): Promise<void> {
await Promise.all([this.page.waitForLoadState("networkidle"), action()]);
}

async click(selector: string): Promise<void> {
await this.page.click(selector);
}

async fill(selector: string, value: string): Promise<void> {
await this.page.fill(selector, value);
}

async getText(selector: string): Promise<string> {
return this.page.textContent(selector) || "";
}

async isVisible(selector: string): Promise<boolean> {
try {
return await this.page.isVisible(selector);
} catch {
return false;
}
}

async exists(selector: string): Promise<boolean> {
const element = await this.page.$(selector);
return element !== null;
}

async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}

async wait(ms: number): Promise<void> {
await this.page.waitForTimeout(ms);
}
}
60 changes: 60 additions & 0 deletions airflow/ui/tests/e2e/pages/LoginPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*!
* 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 } from "@playwright/test";

import { BasePage } from "./BasePage";
import { testConfig } from "../testConfig";

export class LoginPage extends BasePage {
constructor(page: Page) {
super(page);
}

async goto(): Promise<void> {
await this.navigateTo(testConfig.paths.login);
}

async login(
username: string = testConfig.username,
password: string = testConfig.password,
): Promise<void> {
// Navigate to login page
await this.goto();

// Check if already logged in
const isLoggedIn = await this.isVisible('a[href="/logout"]');
if (isLoggedIn) {
return;
}

// Fill login form
await this.fill('input[name="username"]', username);
await this.fill('input[name="password"]', password);

// Submit and wait for navigation
await this.waitForNavigation(async () => {
await this.click('button[type="submit"]');
});
}

async isOnLoginPage(): Promise<boolean> {
return this.exists('input[name="username"]');
}
}
Loading