diff --git a/airflow-core/src/airflow/ui/playwright.config.ts b/airflow-core/src/airflow/ui/playwright.config.ts index 3e00ad21e59b4..2ca34e47ef6d0 100644 --- a/airflow-core/src/airflow/ui/playwright.config.ts +++ b/airflow-core/src/airflow/ui/playwright.config.ts @@ -35,6 +35,9 @@ export const testConfig = { hitlId: process.env.TEST_HITL_DAG_ID ?? "example_hitl_operator", id: process.env.TEST_DAG_ID ?? "example_bash_operator", }, + testTask: { + id: process.env.TEST_TASK_ID ?? "runme_0", + }, xcomDag: { id: process.env.TEST_XCOM_DAG_ID ?? "example_xcom", }, diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx index 2f6d2de810999..840d79aef0dff 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx @@ -206,6 +206,7 @@ export const TaskLogHeader = ({ - + {wrap ? translate("wrap.unwrap") : translate("wrap.wrap")} {translate("wrap.hotkey")} - + {showTimestamp ? translate("timestamp.hide") : translate("timestamp.show")} {translate("timestamp.hotkey")} - + {expanded ? ( <> {translate("expand.collapse")} @@ -234,7 +235,7 @@ export const TaskLogHeader = ({ )} {translate("expand.hotkey")} - + {showSource ? translate("source.hide") : translate("source.show")} {translate("source.hotkey")} @@ -262,6 +263,7 @@ export const TaskLogHeader = ({ { + await this.navigateTo(`/dags/${dagId}`); + } + + public async navigateToTaskInstance(dagId: string, runId: string, taskId: string): Promise { + await this.navigateTo(`/dags/${dagId}/runs/${runId}/tasks/${taskId}`); + } + + public async triggerDagAndWaitForSuccess(dagId: string): Promise { + await this.triggerDagRun(dagId); + await this.waitForDagRunSuccess(); + } + + public async triggerDagRun(dagId: string): Promise { + await this.navigateToDag(dagId); + await this.triggerButton.click(); + await this.confirmTriggerButton.click(); + await this.page.waitForURL(/.*\/runs\/.*/, { timeout: 15_000 }); + } + + public async waitForDagRunSuccess(): Promise { + await expect(this.stateBadge).toContainText("Success", { timeout: 60_000 }); + } +} diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts new file mode 100644 index 0000000000000..f68881126e75c --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts @@ -0,0 +1,128 @@ +/*! + * 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, test } from "@playwright/test"; +import { AUTH_FILE, testConfig } from "playwright.config"; + +import { TaskInstancePage } from "../pages/TaskInstancePage"; + +test.describe("Verify task logs display", () => { + test.describe.configure({ mode: "serial" }); + + const testDagId = testConfig.testDag.id; + const testTaskId = testConfig.testTask.id; + + let dagRunId: string; + + test.beforeAll(async ({ browser }) => { + test.setTimeout(120_000); + + const context = await browser.newContext({ storageState: AUTH_FILE }); + const page = await context.newPage(); + const taskInstancePage = new TaskInstancePage(page); + + await taskInstancePage.triggerDagAndWaitForSuccess(testDagId); + + const url = page.url(); + const match = /runs\/([^/]+)/.exec(url); + + dagRunId = match?.[1] ?? ""; + + if (!dagRunId) { + throw new Error(`Could not extract dagRunId from URL: ${url}`); + } + + await context.close(); + }); + + test.beforeEach(async ({ page }) => { + const taskInstancePage = new TaskInstancePage(page); + + await taskInstancePage.navigateToTaskInstance(testDagId, dagRunId, testTaskId); + }); + + test("Verify log content is displayed", async ({ page }) => { + const virtualizedList = page.locator('[data-testid="virtualized-list"]'); + + await expect(virtualizedList).toBeVisible({ timeout: 30_000 }); + const logItems = page.locator('[data-testid^="virtualized-item-"]'); + + await expect(logItems.first()).toBeVisible({ timeout: 10_000 }); + }); + + test("Verify log levels are visible", async ({ page }) => { + const virtualizedList = page.locator('[data-testid="virtualized-list"]'); + + await expect(virtualizedList).toBeVisible({ timeout: 30_000 }); + + await expect(virtualizedList).toContainText(/INFO|WARNING|ERROR|CRITICAL/); + }); + + test("Verify log timestamp formatting", async ({ page }) => { + const virtualizedList = page.locator('[data-testid="virtualized-list"]'); + + await expect(virtualizedList).toBeVisible({ timeout: 30_000 }); + + await expect(virtualizedList).toContainText(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}]/); + }); + + test("Verify log settings", async ({ page }) => { + const virtualizedList = page.locator('[data-testid="virtualized-list"]'); + + await expect(virtualizedList).toBeVisible({ timeout: 30_000 }); + + // Verify timestamps are visible initially + await expect(virtualizedList).toContainText(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}]/); + + await page.getByTestId("log-settings-button").click(); + await page.getByTestId("log-settings-timestamp").click(); + await expect(virtualizedList).not.toContainText(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}]/); + + // Test Show Source + await page.getByTestId("log-settings-button").click(); + await page.getByTestId("log-settings-source").click(); + await expect(virtualizedList).toContainText(/source/); + + // Test Unwrap + await page.getByTestId("log-settings-button").click(); + const wrapMenuItem = page.getByTestId("log-settings-wrap"); + + await expect(wrapMenuItem).toContainText(/Wrap|Unwrap/); + await wrapMenuItem.click(); + + // Test Expand + await page.getByTestId("log-settings-button").click(); + const expandMenuItem = page.getByTestId("log-settings-expand"); + + await expect(expandMenuItem).toContainText(/Expand|Collapse/); + await expandMenuItem.click(); + }); + + test("Verify logs are getting downloaded fine", async ({ page }) => { + const virtualizedList = page.locator('[data-testid="virtualized-list"]'); + + await expect(virtualizedList).toBeVisible({ timeout: 30_000 }); + + const downloadPromise = page.waitForEvent("download", { timeout: 10_000 }); + + await page.getByTestId("download-logs-button").click(); + const download = await downloadPromise; + + expect(download.suggestedFilename()).toMatch(/^logs_.*\.txt$/); + }); +});