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$/);
+ });
+});