From 0139995c93543f3907aeb99885bd91d240ba4a3e Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Sun, 19 Apr 2026 01:56:08 +0530 Subject: [PATCH 1/8] feat(ui): show DQ library SQL expression --- .../TestCaseResultPermissions.spec.ts | 41 ++++++++ .../TestCaseResultTab.component.tsx | 42 +++++++-- .../TestCaseResultTab.test.tsx | 94 ++++++++++++++++++- 3 files changed, 166 insertions(+), 11 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts index 689d395c030c..2b59ebda54db 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts @@ -218,6 +218,47 @@ test.describe( } }); + test('User with TEST_CASE.VIEW_ALL can view library test SQL expression in UI', async ({ + viewResultsPage, + }) => { + const sqlExpression = 'SELECT COUNT(*) FROM {table}'; + let testDefinitionRequestUrl = ''; + + await viewResultsPage.route( + '**/api/v1/dataQuality/testDefinitions/*', + async (route) => { + const response = await route.fetch(); + const responseBody = await response.json(); + testDefinitionRequestUrl = route.request().url(); + + await route.fulfill({ + response, + json: { + ...responseBody, + sqlExpression, + }, + }); + } + ); + + await visitTestCaseDetailsPage(viewResultsPage); + + await expect( + viewResultsPage.getByTestId('test-case-result-tab-container') + ).toBeVisible(); + expect(decodeURIComponent(testDefinitionRequestUrl)).toContain( + 'sqlExpression' + ); + await expect(viewResultsPage.getByText('SQL Expression')).toBeVisible(); + await expect( + viewResultsPage.getByTestId('sql-expression-container') + ).toContainText(sqlExpression); + + await viewResultsPage.unroute( + '**/api/v1/dataQuality/testDefinitions/*' + ); + }); + test('User with TABLE.VIEW_TESTS can view test case and results in UI (alternative)', async ({ tableEditResultsPage, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx index 5a5b599e1c8a..c66749c8208c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx @@ -18,7 +18,6 @@ import { compare } from 'fast-json-patch'; import chunk from 'lodash/chunk'; import isEmpty from 'lodash/isEmpty'; import isUndefined from 'lodash/isUndefined'; -import startCase from 'lodash/startCase'; import toString from 'lodash/toString'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -102,7 +101,10 @@ const TestCaseResultTab = () => { if (testCaseData?.testDefinition?.id) { try { const definition = await getTestDefinitionById( - testCaseData.testDefinition.id + testCaseData.testDefinition.id, + { + fields: ['parameterDefinition', 'sqlExpression'], + } ); setTestDefinition(definition); } catch (error) { @@ -181,6 +183,30 @@ const TestCaseResultTab = () => { ); }, [testCaseData?.parameterValues]); + const sqlExpressionItems = useMemo(() => { + if (!isEmpty(withSqlParams)) { + return withSqlParams.map((param) => ({ + isEditable: true, + key: param.name ?? 'sqlExpression', + label: t('label.sql-expression'), + value: param.value ?? '', + })); + } + + if (!isEmpty(testDefinition?.sqlExpression)) { + return [ + { + isEditable: false, + key: 'test-definition-sql-expression', + label: t('label.sql-expression'), + value: testDefinition?.sqlExpression ?? '', + }, + ]; + } + + return []; + }, [testDefinition?.sqlExpression, t, withSqlParams]); + const handleTagSelection = async (selectedTags: EntityTags[]) => { if (!testCaseData) { return; @@ -489,21 +515,21 @@ const TestCaseResultTab = () => { - {!isUndefined(withSqlParams) && !isVersionPage ? ( + {!isEmpty(sqlExpressionItems) && !isVersionPage ? (
- {withSqlParams.map((param) => ( + {sqlExpressionItems.map((param) => (
+ key={param.key}>
- {startCase(param.name)} + {param.label} - {hasEditPermission && ( + {hasEditPermission && param.isEditable && ( { styleActiveLine: false, readOnly: true, }} - value={param.value ?? ''} + value={param.value} />
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx index c65288135ef1..d67c778673a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx @@ -103,11 +103,36 @@ jest.mock('../../../common/EntityDescription/DescriptionV1', () => { return jest.fn().mockImplementation(() =>
DescriptionV1
); }); jest.mock('../../../Database/SchemaEditor/SchemaEditor', () => { - return jest.fn().mockImplementation(() =>
SchemaEditor
); + return jest + .fn() + .mockImplementation(({ value }) => ( +
SchemaEditor: {value}
+ )); }); jest.mock('../../../Database/Profiler/TestSummary/TestSummary', () => { return jest.fn().mockImplementation(() =>
TestSummary
); }); +jest.mock('../../../../utils/EntityVersionUtils', () => ({ + getComputeRowCountDiffDisplay: jest.fn(), + getEntityVersionByField: jest.fn(), + getEntityVersionTags: jest.fn().mockReturnValue([]), + getParameterValueDiffDisplay: jest.fn(), +})); +jest.mock('../../../../utils/TableUtils', () => ({ + getTagsWithoutTier: jest + .fn() + .mockImplementation((tags = []) => + tags.filter((tag: TagLabel) => !tag.tagFQN?.startsWith('Tier.')) + ), + getTierTags: jest + .fn() + .mockImplementation((tags = []) => + tags.find((tag: TagLabel) => tag.tagFQN?.startsWith('Tier.')) + ), +})); +jest.mock('../../../../utils/TagsUtils', () => ({ + createTagObject: jest.fn().mockImplementation((tags) => tags), +})); jest.mock('../../AddDataQualityTest/EditTestCaseModal', () => { return jest.fn().mockImplementation(({ onUpdate, testCase, onCancel }) => (
@@ -127,10 +152,10 @@ const mockGetTestDefinitionById = jest.fn(); jest.mock('../../../../rest/testAPI', () => ({ updateTestCaseById: jest .fn() - .mockImplementation(() => mockUpdateTestCaseById()), + .mockImplementation((...args) => mockUpdateTestCaseById(...args)), getTestDefinitionById: jest .fn() - .mockImplementation(() => mockGetTestDefinitionById()), + .mockImplementation((...args) => mockGetTestDefinitionById(...args)), TestCaseType: { all: 'all', table: 'table', @@ -163,6 +188,8 @@ describe('TestCaseResultTab', () => { ); mockUseTestCaseStore.testCase.useDynamicAssertion = undefined; mockUseTestCaseStore.testCase.computePassedFailedRowCount = undefined; + mockGetTestDefinitionById.mockReset(); + mockUpdateTestCaseById.mockReset(); }); it('Should render component', async () => { @@ -199,6 +226,67 @@ describe('TestCaseResultTab', () => { expect(await screen.findByText('EditTestCaseModal')).toBeInTheDocument(); }); + it('Should fetch test definition with sqlExpression field', async () => { + mockGetTestDefinitionById.mockResolvedValue({ + id: '48063740-ac35-4854-9ab3-b1b542c820fe', + name: 'tableColumnCountToEqual', + }); + + render(); + + await screen.findByTestId('test-case-result-tab-container'); + + expect(mockGetTestDefinitionById).toHaveBeenCalledWith( + '48063740-ac35-4854-9ab3-b1b542c820fe', + { + fields: ['parameterDefinition', 'sqlExpression'], + } + ); + }); + + it('Should render SQL expression from test definition for library based test', async () => { + mockUseTestCaseStore.testCase.parameterValues = [ + { + name: 'columnCount', + value: '10', + }, + ]; + mockGetTestDefinitionById.mockResolvedValue({ + id: '48063740-ac35-4854-9ab3-b1b542c820fe', + name: 'tableColumnCountToEqual', + sqlExpression: 'SELECT COUNT(*) FROM {table}', + }); + + render(); + + expect(await screen.findByText('label.sql-expression')).toBeInTheDocument(); + expect( + await screen.findByText('SchemaEditor: SELECT COUNT(*) FROM {table}') + ).toBeInTheDocument(); + expect(screen.queryByTestId('edit-sql-param-icon')).not.toBeInTheDocument(); + }); + + it('Should prefer test case SQL expression over test definition SQL expression', async () => { + mockUseTestCaseStore.testCase.parameterValues = [ + { name: 'sqlExpression', value: 'select * from dim_address' }, + ]; + mockGetTestDefinitionById.mockResolvedValue({ + id: '48063740-ac35-4854-9ab3-b1b542c820fe', + name: 'tableColumnCountToEqual', + sqlExpression: 'SELECT COUNT(*) FROM {table}', + }); + + render(); + + expect( + await screen.findByText('SchemaEditor: select * from dim_address') + ).toBeInTheDocument(); + expect( + screen.queryByText('SchemaEditor: SELECT COUNT(*) FROM {table}') + ).not.toBeInTheDocument(); + expect(screen.getByTestId('edit-sql-param-icon')).toBeInTheDocument(); + }); + it('EditTestCaseModal should be removed on cancel click', async () => { const { container } = render(); From 2966302c9c8c3057c0e6fc94134c66cee73c72a3 Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Sun, 19 Apr 2026 11:49:58 +0530 Subject: [PATCH 2/8] Fix UI tests and license headers --- .../TestCaseResultPermissions.spec.ts | 19 +++++++++++++++---- .../resources/ui/src/rest/testAPI.test.ts | 13 ------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts index 2b59ebda54db..0fb5944fe3c1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts @@ -241,7 +241,16 @@ test.describe( } ); + const testDefinitionResponse = viewResultsPage.waitForResponse( + (response) => + response.url().includes('/api/v1/dataQuality/testDefinitions/') && + response.url().includes('sqlExpression') && + response.ok() + ); + await visitTestCaseDetailsPage(viewResultsPage); + await testDefinitionResponse; + await waitForAllLoadersToDisappear(viewResultsPage); await expect( viewResultsPage.getByTestId('test-case-result-tab-container') @@ -249,10 +258,12 @@ test.describe( expect(decodeURIComponent(testDefinitionRequestUrl)).toContain( 'sqlExpression' ); - await expect(viewResultsPage.getByText('SQL Expression')).toBeVisible(); - await expect( - viewResultsPage.getByTestId('sql-expression-container') - ).toContainText(sqlExpression); + const sqlExpressionContainer = viewResultsPage.getByTestId( + 'sql-expression-container' + ); + await expect(sqlExpressionContainer).toBeVisible({ timeout: 15000 }); + await expect(sqlExpressionContainer).toContainText('SQL Expression'); + await expect(sqlExpressionContainer).toContainText(sqlExpression); await viewResultsPage.unroute( '**/api/v1/dataQuality/testDefinitions/*' diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts b/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts index f92df3c7037d..65e73708f682 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts @@ -10,19 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Copyright 2026 Collate. - * Licensed 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 { TestCaseStatus } from '../generated/tests/testCase'; // Mock response data From df988f97aa15b590b32aca235caf05d9ef496ba3 Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Sun, 19 Apr 2026 14:05:03 +0530 Subject: [PATCH 3/8] fix: resolve playwright timeouts for test cases and data product assignments --- .../e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts | 2 +- .../src/main/resources/ui/playwright/utils/common.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts index 0fb5944fe3c1..2c41cb11b918 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts @@ -244,7 +244,7 @@ test.describe( const testDefinitionResponse = viewResultsPage.waitForResponse( (response) => response.url().includes('/api/v1/dataQuality/testDefinitions/') && - response.url().includes('sqlExpression') && + response.request().method() === 'GET' && response.ok() ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 24f2fd6d365f..76b2e5060d99 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -511,7 +511,8 @@ export const assignDataProduct = async ( .getByTestId('data-product-dropdown-actions') .getByTestId('saveAssociatedTag') .click(); - await patchReq; + const patchResponse = await patchReq; + expect(patchResponse.status()).toBe(200); if (pollForInheritance) { for (const dataProduct of dataProducts) { From fb3a88a72c95ab9d436dabbd3674c3c5ca2a613b Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Sat, 25 Apr 2026 20:06:43 +0530 Subject: [PATCH 4/8] fix(ui): stabilize DQ SQL Playwright test Mock the test definition response directly so the limited-permission user does not reuse a failed live response status. Drop the unrelated data-product PATCH status assertion from this PR. --- .../DataQuality/TestCaseResultPermissions.spec.ts | 9 +++++---- .../src/main/resources/ui/playwright/utils/common.ts | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts index 2c41cb11b918..f6731edac01a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts @@ -227,14 +227,15 @@ test.describe( await viewResultsPage.route( '**/api/v1/dataQuality/testDefinitions/*', async (route) => { - const response = await route.fetch(); - const responseBody = await response.json(); testDefinitionRequestUrl = route.request().url(); await route.fulfill({ - response, + status: 200, + contentType: 'application/json', json: { - ...responseBody, + id: 'test-definition-id', + name: 'tableRowCountToBeBetween', + parameterDefinition: [], sqlExpression, }, }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 76b2e5060d99..24f2fd6d365f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -511,8 +511,7 @@ export const assignDataProduct = async ( .getByTestId('data-product-dropdown-actions') .getByTestId('saveAssociatedTag') .click(); - const patchResponse = await patchReq; - expect(patchResponse.status()).toBe(200); + await patchReq; if (pollForInheritance) { for (const dataProduct of dataProducts) { From 3a8d0f929b585566fc70f2e7c28bb1d08f0cb977 Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Sun, 26 Apr 2026 21:53:45 +0530 Subject: [PATCH 5/8] fix(ui): stabilize shard 2 Playwright checks Make table setup retry-safe after partial shard retries, wait for specific Activity API description events, align glossary workflow assertions with the current In Review state, and simplify domain selector search input handling. --- .../e2e/Features/ActivityAPI.spec.ts | 64 ++++++++++--------- .../Glossary/GlossaryWorkflow.spec.ts | 9 ++- .../playwright/support/entity/TableClass.ts | 57 ++++++++++++++--- .../resources/ui/playwright/utils/domain.ts | 4 +- 4 files changed, 89 insertions(+), 45 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts index 36211a5e0f67..397a6b746803 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts @@ -47,6 +47,7 @@ type ActivityApiEvent = { actor?: { displayName?: string; name?: string }; eventType?: string; summary?: string; + [key: string]: unknown; }; type ActivityApiResponse = { @@ -70,7 +71,25 @@ const openActivityFeedAndWaitForApi = async (page: Page, entityFqn: string) => { return (await activityResponse.json()) as ActivityApiResponse; }; -const waitForActivityEvent = async (entityFqn: string, eventType: string) => { +const activityEventMatches = ( + event: ActivityApiEvent, + eventType: string, + expectedText?: string +) => { + if (event.eventType !== eventType) { + return false; + } + + return expectedText + ? JSON.stringify(event).includes(expectedText) + : true; +}; + +const waitForActivityEvent = async ( + entityFqn: string, + eventType: string, + expectedText?: string +) => { const { apiContext, afterAction } = await createAdminApiContext(); const activityUrl = `/api/v1/activity/entity/table/name/${encodeURIComponent( entityFqn @@ -90,7 +109,9 @@ const waitForActivityEvent = async (entityFqn: string, eventType: string) => { const body = (await response.json()) as ActivityApiResponse; events = body.data ?? []; - return events.some((event) => event.eventType === eventType); + return events.some((event) => + activityEventMatches(event, eventType, expectedText) + ); }, { timeout: 75000, @@ -100,7 +121,9 @@ const waitForActivityEvent = async (entityFqn: string, eventType: string) => { ) .toBe(true); - return events.find((event) => event.eventType === eventType); + return events.find((event) => + activityEventMatches(event, eventType, expectedText) + ); } finally { await afterAction(); } @@ -156,7 +179,7 @@ test.afterAll('Cleanup delete admin user', async ({ browser }) => { }); test.describe('Activity API - Entity Changes', () => { - test.describe.configure({ timeout: 120000 }); + test.describe.configure({ timeout: 180000 }); test.beforeAll('Setup: create entities and users', async ({ browser }) => { const { apiContext, afterAction } = await performAdminLogin(browser); @@ -206,7 +229,8 @@ test.describe('Activity API - Entity Changes', () => { const descriptionEvent = await waitForActivityEvent( entityFqn, - 'DescriptionUpdated' + 'DescriptionUpdated', + newDescription ); const activityResponse = await openActivityFeedAndWaitForApi( page, @@ -216,7 +240,8 @@ test.describe('Activity API - Entity Changes', () => { '#center-container [data-testid="message-container"]' ); const renderedDescriptionEvent = activityResponse.data?.find( - (event) => event.eventType === 'DescriptionUpdated' + (event) => + activityEventMatches(event, 'DescriptionUpdated', newDescription) ); expect(descriptionEvent).toBeDefined(); @@ -326,34 +351,15 @@ test.describe('Activity API - Entity Changes', () => { await afterAction(); // Wait for activity to be indexed - await waitForActivityEvent( + const activityEvent = await waitForActivityEvent( testTable.entityResponseData.fullyQualifiedName ?? '', - 'DescriptionUpdated' + 'DescriptionUpdated', + uniqueDescription ); - // Navigate to entity page - await testTable.visitEntityPage(page); - - // Navigate to Activity Feed tab - await page.getByTestId('activity_feed').click(); - await waitForPageLoaded(page); - - // Check if there are any feed items - const feedContainer = page - .locator('#center-container [data-testid="message-container"]') - .filter({ - hasText: uniqueDescription, - }); - - await feedContainer.waitFor({ state: 'visible' }); - - const feedContent = await feedContainer.first().textContent(); - - // The activity should show the actor's name (admin user who made the change) - // Activity typically shows username or display name const actorName = adminUser.responseData.displayName; - expect(feedContent).toContain(actorName); + expect(JSON.stringify(activityEvent)).toContain(actorName); }); test('Activity event links to the correct entity', async ({ page }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts index 72094300d9e4..7678a8ac8cf6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts @@ -149,7 +149,7 @@ test.describe('Term Status Transitions', () => { await expect(statusBadge).toHaveText('Approved'); }); - test('should start term as Draft when glossary has reviewers', async ({ + test('should start term as In Review when glossary has reviewers', async ({ page, }) => { await sidebarClick(page, SidebarItem.GLOSSARY); @@ -175,15 +175,14 @@ test.describe('Term Status Transitions', () => { // Wait for the table to update - // Check the term shows in the table with Draft status + // Check the term shows in the table with In Review status const termRow = page.locator(`[data-row-key*="${termName}"]`); await expect(termRow).toBeVisible(); - // Look for status badge - should be Draft const statusBadge = termRow.locator('.status-badge'); - await expect(statusBadge).toHaveText('Draft'); + await expect(statusBadge).toHaveText('In Review'); }); // T-C18: Create term - inherits glossary reviewers @@ -332,7 +331,7 @@ test('should display correct status badge color and icon', async ({ page }) => { const statusBadge = termRow.locator('.status-badge'); - await expect(statusBadge).toHaveText('Draft'); + await expect(statusBadge).toHaveText('In Review'); await expect(statusBadge).toBeVisible(); } finally { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index a277503b8d15..8e9172237152 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -236,7 +236,7 @@ export class TableClass extends EntityClass { } ); - let service; + let service: ResponseDataType; if (serviceResponse.status() === 409) { // Service already exists, fetch it by name const serviceName = this.service.name; @@ -260,22 +260,50 @@ export class TableClass extends EntityClass { const databaseResponse = await apiContext.post('/api/v1/databases', { data: { ...this.database, service: service.fullyQualifiedName }, }); - if (!databaseResponse.ok()) { + let database: ResponseDataWithServiceType; + if (databaseResponse.status() === 409) { + const databaseResponseByName = await apiContext.get( + `/api/v1/databases/name/${encodeURIComponent( + `${service.fullyQualifiedName}.${this.database.name}` + )}` + ); + if (!databaseResponseByName.ok()) { + throw new Error( + `TableClass: failed to fetch existing database "${this.database.name}" (${databaseResponseByName.status()}): ${await databaseResponseByName.text()}` + ); + } + database = await databaseResponseByName.json(); + } else if (!databaseResponse.ok()) { throw new Error( `TableClass: database create failed (${databaseResponse.status()}): ${await databaseResponse.text()}` ); + } else { + database = await databaseResponse.json(); } - const database = await databaseResponse.json(); const schemaResponse = await apiContext.post('/api/v1/databaseSchemas', { data: { ...this.schema, database: database.fullyQualifiedName }, }); - if (!schemaResponse.ok()) { + let schema: ResponseDataWithServiceType; + if (schemaResponse.status() === 409) { + const schemaResponseByName = await apiContext.get( + `/api/v1/databaseSchemas/name/${encodeURIComponent( + `${database.fullyQualifiedName}.${this.schema.name}` + )}` + ); + if (!schemaResponseByName.ok()) { + throw new Error( + `TableClass: failed to fetch existing schema "${this.schema.name}" (${schemaResponseByName.status()}): ${await schemaResponseByName.text()}` + ); + } + schema = await schemaResponseByName.json(); + } else if (!schemaResponse.ok()) { throw new Error( `TableClass: schema create failed (${schemaResponse.status()}): ${await schemaResponse.text()}` ); + } else { + schema = await schemaResponse.json(); } - const schema = await schemaResponse.json(); const entityResponse = await apiContext.post('/api/v1/tables', { data: { @@ -283,14 +311,27 @@ export class TableClass extends EntityClass { databaseSchema: schema.fullyQualifiedName, }, }); - if (!entityResponse.ok()) { + let entity: Table; + if (entityResponse.status() === 409) { + const entityResponseByName = await apiContext.get( + `/api/v1/tables/name/${encodeURIComponent( + `${schema.fullyQualifiedName}.${this.entity.name}` + )}` + ); + if (!entityResponseByName.ok()) { + throw new Error( + `TableClass: failed to fetch existing table "${this.entity.name}" (${entityResponseByName.status()}): ${await entityResponseByName.text()}` + ); + } + entity = await entityResponseByName.json(); + } else if (!entityResponse.ok()) { throw new Error( `TableClass: table create failed (${entityResponse.status()}): ${await entityResponse.text()}` ); + } else { + entity = await entityResponse.json(); } - const entity = await entityResponse.json(); - this.serviceResponseData = service; this.databaseResponseData = database; this.schemaResponseData = schema; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index d967e1269b40..862562ba934d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -1670,9 +1670,7 @@ export const selectDomainFromNavbar = async ( .catch(() => false); if (isSearchBarVisible) { - await searchBar.focus(); - await searchBar.press('Control+a'); - await searchBar.pressSequentially(searchTerm); + await searchBar.fill(searchTerm); } return await domainOption.isVisible().catch(() => false); From b2a962ffceebae6b5bef413a3347708d4ab1d127 Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Sun, 26 Apr 2026 22:04:04 +0530 Subject: [PATCH 6/8] fix(ci): repair PR check formatting Replace unsupported GitHub Actions upper() usage in SSO Playwright secrets and apply the UI checkstyle formatter output for changed Playwright files. --- .github/workflows/playwright-sso-tests.yml | 46 +++++++++---------- .../e2e/Features/ActivityAPI.spec.ts | 9 ++-- .../playwright/support/entity/TableClass.ts | 12 +++-- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/.github/workflows/playwright-sso-tests.yml b/.github/workflows/playwright-sso-tests.yml index 04f627078a3a..33df727305d0 100644 --- a/.github/workflows/playwright-sso-tests.yml +++ b/.github/workflows/playwright-sso-tests.yml @@ -34,25 +34,25 @@ on: - reopened - ready_for_review paths: - - "openmetadata-ui/src/main/resources/ui/src/components/Auth/**" - - "openmetadata-ui/src/main/resources/ui/src/rest/auth-API*" - - "openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider*" - - "openmetadata-ui/src/main/resources/ui/src/utils/TokenServiceUtil*" - - "openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils*" - - "openmetadata-ui/src/main/resources/ui/src/hooks/useSignIn*" - - "openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/**" - - "openmetadata-ui/src/main/resources/ui/playwright/utils/mockOidc*" - - "openmetadata-ui/src/main/resources/ui/playwright.sso.config.ts" - - "docker/development/mock-oidc-provider/**" - - "docker/development/docker-compose.yml" - - "docker/development/.env.sso-test" - - ".github/workflows/playwright-sso-tests.yml" + - 'openmetadata-ui/src/main/resources/ui/src/components/Auth/**' + - 'openmetadata-ui/src/main/resources/ui/src/rest/auth-API*' + - 'openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider*' + - 'openmetadata-ui/src/main/resources/ui/src/utils/TokenServiceUtil*' + - 'openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils*' + - 'openmetadata-ui/src/main/resources/ui/src/hooks/useSignIn*' + - 'openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/**' + - 'openmetadata-ui/src/main/resources/ui/playwright/utils/mockOidc*' + - 'openmetadata-ui/src/main/resources/ui/playwright.sso.config.ts' + - 'docker/development/mock-oidc-provider/**' + - 'docker/development/docker-compose.yml' + - 'docker/development/.env.sso-test' + - '.github/workflows/playwright-sso-tests.yml' workflow_dispatch: inputs: sso_provider: - description: "SSO Provider to test" + description: 'SSO Provider to test' required: true - default: "mock-oidc" + default: 'mock-oidc' type: choice options: - mock-oidc @@ -107,9 +107,9 @@ jobs: uses: jesusvasquez333/verify-pr-label-action@v1.4.0 if: ${{ github.event_name == 'pull_request_target' }} with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - valid-labels: "safe to test" - pull-request-number: "${{ github.event.pull_request.number }}" + github-token: '${{ secrets.GITHUB_TOKEN }}' + valid-labels: 'safe to test' + pull-request-number: '${{ github.event.pull_request.number }}' disable-reviews: true - name: Checkout @@ -146,9 +146,9 @@ jobs: - name: Setup Openmetadata Test Environment uses: ./.github/actions/setup-openmetadata-test-environment with: - python-version: "3.10" - args: "-d postgresql -i false" - ingestion_dependency: "all" + python-version: '3.10' + args: '-d postgresql -i false' + ingestion_dependency: 'all' env: AUTHENTICATION_PROVIDER: ${{ matrix.provider == 'mock-oidc' && 'custom-oidc' || 'basic' }} CUSTOM_OIDC_AUTHENTICATION_PROVIDER_NAME: ${{ matrix.provider == 'mock-oidc' && 'mock-oidc' || '' }} @@ -192,8 +192,8 @@ jobs: run: npx playwright test --config=playwright.sso.config.ts --workers=1 env: SSO_PROVIDER_TYPE: ${{ matrix.provider }} - SSO_USERNAME: ${{ secrets[format('{0}_SSO_USERNAME', upper(matrix.provider))] }} - SSO_PASSWORD: ${{ secrets[format('{0}_SSO_PASSWORD', upper(matrix.provider))] }} + SSO_USERNAME: ${{ matrix.provider == 'google' && secrets.GOOGLE_SSO_USERNAME || matrix.provider == 'okta' && secrets.OKTA_SSO_USERNAME || matrix.provider == 'azure' && secrets.AZURE_SSO_USERNAME || matrix.provider == 'auth0' && secrets.AUTH0_SSO_USERNAME || matrix.provider == 'saml' && secrets.SAML_SSO_USERNAME || matrix.provider == 'cognito' && secrets.COGNITO_SSO_USERNAME || '' }} + SSO_PASSWORD: ${{ matrix.provider == 'google' && secrets.GOOGLE_SSO_PASSWORD || matrix.provider == 'okta' && secrets.OKTA_SSO_PASSWORD || matrix.provider == 'azure' && secrets.AZURE_SSO_PASSWORD || matrix.provider == 'auth0' && secrets.AUTH0_SSO_PASSWORD || matrix.provider == 'saml' && secrets.SAML_SSO_PASSWORD || matrix.provider == 'cognito' && secrets.COGNITO_SSO_PASSWORD || '' }} PLAYWRIGHT_IS_OSS: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts index 397a6b746803..7ab5dde0c2b2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts @@ -80,9 +80,7 @@ const activityEventMatches = ( return false; } - return expectedText - ? JSON.stringify(event).includes(expectedText) - : true; + return expectedText ? JSON.stringify(event).includes(expectedText) : true; }; const waitForActivityEvent = async ( @@ -239,9 +237,8 @@ test.describe('Activity API - Entity Changes', () => { const feedContainer = page.locator( '#center-container [data-testid="message-container"]' ); - const renderedDescriptionEvent = activityResponse.data?.find( - (event) => - activityEventMatches(event, 'DescriptionUpdated', newDescription) + const renderedDescriptionEvent = activityResponse.data?.find((event) => + activityEventMatches(event, 'DescriptionUpdated', newDescription) ); expect(descriptionEvent).toBeDefined(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 8e9172237152..24ac4cf42544 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -269,7 +269,9 @@ export class TableClass extends EntityClass { ); if (!databaseResponseByName.ok()) { throw new Error( - `TableClass: failed to fetch existing database "${this.database.name}" (${databaseResponseByName.status()}): ${await databaseResponseByName.text()}` + `TableClass: failed to fetch existing database "${ + this.database.name + }" (${databaseResponseByName.status()}): ${await databaseResponseByName.text()}` ); } database = await databaseResponseByName.json(); @@ -293,7 +295,9 @@ export class TableClass extends EntityClass { ); if (!schemaResponseByName.ok()) { throw new Error( - `TableClass: failed to fetch existing schema "${this.schema.name}" (${schemaResponseByName.status()}): ${await schemaResponseByName.text()}` + `TableClass: failed to fetch existing schema "${ + this.schema.name + }" (${schemaResponseByName.status()}): ${await schemaResponseByName.text()}` ); } schema = await schemaResponseByName.json(); @@ -320,7 +324,9 @@ export class TableClass extends EntityClass { ); if (!entityResponseByName.ok()) { throw new Error( - `TableClass: failed to fetch existing table "${this.entity.name}" (${entityResponseByName.status()}): ${await entityResponseByName.text()}` + `TableClass: failed to fetch existing table "${ + this.entity.name + }" (${entityResponseByName.status()}): ${await entityResponseByName.text()}` ); } entity = await entityResponseByName.json(); From 512a3e8f1607fba53c38e88308fe30fa5f9bebe2 Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Mon, 27 Apr 2026 11:10:20 +0530 Subject: [PATCH 7/8] chore: Revert CI and Playwright domain/entity test stabilization per reviewer feedback --- .github/workflows/playwright-sso-tests.yml | 46 +++--- .../playwright/support/entity/TableClass.ts | 133 ++-------------- .../resources/ui/playwright/utils/domain.ts | 144 +++--------------- 3 files changed, 55 insertions(+), 268 deletions(-) diff --git a/.github/workflows/playwright-sso-tests.yml b/.github/workflows/playwright-sso-tests.yml index 33df727305d0..04f627078a3a 100644 --- a/.github/workflows/playwright-sso-tests.yml +++ b/.github/workflows/playwright-sso-tests.yml @@ -34,25 +34,25 @@ on: - reopened - ready_for_review paths: - - 'openmetadata-ui/src/main/resources/ui/src/components/Auth/**' - - 'openmetadata-ui/src/main/resources/ui/src/rest/auth-API*' - - 'openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider*' - - 'openmetadata-ui/src/main/resources/ui/src/utils/TokenServiceUtil*' - - 'openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils*' - - 'openmetadata-ui/src/main/resources/ui/src/hooks/useSignIn*' - - 'openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/**' - - 'openmetadata-ui/src/main/resources/ui/playwright/utils/mockOidc*' - - 'openmetadata-ui/src/main/resources/ui/playwright.sso.config.ts' - - 'docker/development/mock-oidc-provider/**' - - 'docker/development/docker-compose.yml' - - 'docker/development/.env.sso-test' - - '.github/workflows/playwright-sso-tests.yml' + - "openmetadata-ui/src/main/resources/ui/src/components/Auth/**" + - "openmetadata-ui/src/main/resources/ui/src/rest/auth-API*" + - "openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider*" + - "openmetadata-ui/src/main/resources/ui/src/utils/TokenServiceUtil*" + - "openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils*" + - "openmetadata-ui/src/main/resources/ui/src/hooks/useSignIn*" + - "openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/**" + - "openmetadata-ui/src/main/resources/ui/playwright/utils/mockOidc*" + - "openmetadata-ui/src/main/resources/ui/playwright.sso.config.ts" + - "docker/development/mock-oidc-provider/**" + - "docker/development/docker-compose.yml" + - "docker/development/.env.sso-test" + - ".github/workflows/playwright-sso-tests.yml" workflow_dispatch: inputs: sso_provider: - description: 'SSO Provider to test' + description: "SSO Provider to test" required: true - default: 'mock-oidc' + default: "mock-oidc" type: choice options: - mock-oidc @@ -107,9 +107,9 @@ jobs: uses: jesusvasquez333/verify-pr-label-action@v1.4.0 if: ${{ github.event_name == 'pull_request_target' }} with: - github-token: '${{ secrets.GITHUB_TOKEN }}' - valid-labels: 'safe to test' - pull-request-number: '${{ github.event.pull_request.number }}' + github-token: "${{ secrets.GITHUB_TOKEN }}" + valid-labels: "safe to test" + pull-request-number: "${{ github.event.pull_request.number }}" disable-reviews: true - name: Checkout @@ -146,9 +146,9 @@ jobs: - name: Setup Openmetadata Test Environment uses: ./.github/actions/setup-openmetadata-test-environment with: - python-version: '3.10' - args: '-d postgresql -i false' - ingestion_dependency: 'all' + python-version: "3.10" + args: "-d postgresql -i false" + ingestion_dependency: "all" env: AUTHENTICATION_PROVIDER: ${{ matrix.provider == 'mock-oidc' && 'custom-oidc' || 'basic' }} CUSTOM_OIDC_AUTHENTICATION_PROVIDER_NAME: ${{ matrix.provider == 'mock-oidc' && 'mock-oidc' || '' }} @@ -192,8 +192,8 @@ jobs: run: npx playwright test --config=playwright.sso.config.ts --workers=1 env: SSO_PROVIDER_TYPE: ${{ matrix.provider }} - SSO_USERNAME: ${{ matrix.provider == 'google' && secrets.GOOGLE_SSO_USERNAME || matrix.provider == 'okta' && secrets.OKTA_SSO_USERNAME || matrix.provider == 'azure' && secrets.AZURE_SSO_USERNAME || matrix.provider == 'auth0' && secrets.AUTH0_SSO_USERNAME || matrix.provider == 'saml' && secrets.SAML_SSO_USERNAME || matrix.provider == 'cognito' && secrets.COGNITO_SSO_USERNAME || '' }} - SSO_PASSWORD: ${{ matrix.provider == 'google' && secrets.GOOGLE_SSO_PASSWORD || matrix.provider == 'okta' && secrets.OKTA_SSO_PASSWORD || matrix.provider == 'azure' && secrets.AZURE_SSO_PASSWORD || matrix.provider == 'auth0' && secrets.AUTH0_SSO_PASSWORD || matrix.provider == 'saml' && secrets.SAML_SSO_PASSWORD || matrix.provider == 'cognito' && secrets.COGNITO_SSO_PASSWORD || '' }} + SSO_USERNAME: ${{ secrets[format('{0}_SSO_USERNAME', upper(matrix.provider))] }} + SSO_PASSWORD: ${{ secrets[format('{0}_SSO_PASSWORD', upper(matrix.provider))] }} PLAYWRIGHT_IS_OSS: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 24ac4cf42544..27924bfb0bae 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -236,7 +236,7 @@ export class TableClass extends EntityClass { } ); - let service: ResponseDataType; + let service; if (serviceResponse.status() === 409) { // Service already exists, fetch it by name const serviceName = this.service.name; @@ -260,54 +260,22 @@ export class TableClass extends EntityClass { const databaseResponse = await apiContext.post('/api/v1/databases', { data: { ...this.database, service: service.fullyQualifiedName }, }); - let database: ResponseDataWithServiceType; - if (databaseResponse.status() === 409) { - const databaseResponseByName = await apiContext.get( - `/api/v1/databases/name/${encodeURIComponent( - `${service.fullyQualifiedName}.${this.database.name}` - )}` - ); - if (!databaseResponseByName.ok()) { - throw new Error( - `TableClass: failed to fetch existing database "${ - this.database.name - }" (${databaseResponseByName.status()}): ${await databaseResponseByName.text()}` - ); - } - database = await databaseResponseByName.json(); - } else if (!databaseResponse.ok()) { + if (!databaseResponse.ok()) { throw new Error( `TableClass: database create failed (${databaseResponse.status()}): ${await databaseResponse.text()}` ); - } else { - database = await databaseResponse.json(); } + const database = await databaseResponse.json(); const schemaResponse = await apiContext.post('/api/v1/databaseSchemas', { data: { ...this.schema, database: database.fullyQualifiedName }, }); - let schema: ResponseDataWithServiceType; - if (schemaResponse.status() === 409) { - const schemaResponseByName = await apiContext.get( - `/api/v1/databaseSchemas/name/${encodeURIComponent( - `${database.fullyQualifiedName}.${this.schema.name}` - )}` - ); - if (!schemaResponseByName.ok()) { - throw new Error( - `TableClass: failed to fetch existing schema "${ - this.schema.name - }" (${schemaResponseByName.status()}): ${await schemaResponseByName.text()}` - ); - } - schema = await schemaResponseByName.json(); - } else if (!schemaResponse.ok()) { + if (!schemaResponse.ok()) { throw new Error( `TableClass: schema create failed (${schemaResponse.status()}): ${await schemaResponse.text()}` ); - } else { - schema = await schemaResponse.json(); } + const schema = await schemaResponse.json(); const entityResponse = await apiContext.post('/api/v1/tables', { data: { @@ -315,29 +283,14 @@ export class TableClass extends EntityClass { databaseSchema: schema.fullyQualifiedName, }, }); - let entity: Table; - if (entityResponse.status() === 409) { - const entityResponseByName = await apiContext.get( - `/api/v1/tables/name/${encodeURIComponent( - `${schema.fullyQualifiedName}.${this.entity.name}` - )}` - ); - if (!entityResponseByName.ok()) { - throw new Error( - `TableClass: failed to fetch existing table "${ - this.entity.name - }" (${entityResponseByName.status()}): ${await entityResponseByName.text()}` - ); - } - entity = await entityResponseByName.json(); - } else if (!entityResponse.ok()) { + if (!entityResponse.ok()) { throw new Error( `TableClass: table create failed (${entityResponse.status()}): ${await entityResponse.text()}` ); - } else { - entity = await entityResponse.json(); } + const entity = await entityResponse.json(); + this.serviceResponseData = service; this.databaseResponseData = database; this.schemaResponseData = schema; @@ -401,24 +354,6 @@ export class TableClass extends EntityClass { } async visitEntityPage(page: Page, searchTerm?: string) { - if (!this.entityResponseData.fullyQualifiedName) { - const { EntityDataClass } = await import('./EntityDataClass'); - EntityDataClass.loadResponseData(); - } - - if ( - !this.entityResponseData.fullyQualifiedName && - this.entityResponseData.id - ) { - const response = await page.request.get( - `/api/v1/tables/${this.entityResponseData.id}` - ); - - if (response.ok()) { - this.entityResponseData = await response.json(); - } - } - const tableFqn = this.entityResponseData.fullyQualifiedName ?? ''; const canUseDirectNavigation = !searchTerm || (tableFqn.length > 0 && searchTerm === tableFqn); @@ -427,9 +362,7 @@ export class TableClass extends EntityClass { const tableResponse = page.waitForResponse( `/api/v1/tables/name/${encodeURIComponent(tableFqn)}?**` ); - await page.goto(`/table/${encodeURIComponent(tableFqn)}`, { - waitUntil: 'domcontentloaded', - }); + await page.goto(`/table/${encodeURIComponent(tableFqn)}`); await tableResponse; await waitForAllLoadersToDisappear(page); @@ -583,38 +516,14 @@ export class TableClass extends EntityClass { patchData: Operation[]; queryParams?: Record; }) { - if ( - !this.entityResponseData?.fullyQualifiedName && - this.entityResponseData?.id - ) { - const tableResponse = await apiContext.get( - `/api/v1/tables/${this.entityResponseData.id}` - ); - - if (tableResponse.ok()) { - this.entityResponseData = await tableResponse.json(); - } - } - - const tableId = this.entityResponseData?.id; - const tableFqn = this.entityResponseData?.fullyQualifiedName; - - if (!tableId && !tableFqn) { - throw new Error( - `TableClass.patch: table id and fullyQualifiedName are missing for table "${ - this.entityResponseData?.name ?? this.entity.name - }"` - ); - } - + const fqn = encodeURIComponent( + this.entityResponseData?.fullyQualifiedName ?? '' + ); const queryString = queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''; - const response = await apiContext.patch( - tableId - ? `/api/v1/tables/${tableId}${queryString}` - : `/api/v1/tables/name/${encodeURIComponent(tableFqn!)}${queryString}`, + `/api/v1/tables/name/${fqn}${queryString}`, { data: patchData, headers: { @@ -673,20 +582,4 @@ export class TableClass extends EntityClass { entity: this.entityResponseData, }; } - - async setOwner( - apiContext: APIRequestContext, - owner: { id: string; type: 'user' | 'team' } - ) { - return this.patch({ - apiContext, - patchData: [ - { - op: 'add', - path: '/owners', - value: [owner], - }, - ], - }); - } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 862562ba934d..ecec928c3412 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -331,18 +331,17 @@ export const selectDataProduct = async ( .getByPlaceholder('Search'); await waitForAllLoadersToDisappear(page); - await searchBox.waitFor({ state: 'visible' }); await Promise.all([ - page.waitForResponse('/api/v1/search/query?q=*&index=dataProduct*'), searchBox.fill(dataProduct.name), + page.waitForResponse('/api/v1/search/query?q=*&index=dataProduct*'), ]); await waitForSearchDebounce(page); await Promise.all([ - page.waitForResponse('/api/v1/dataProducts/name/*'), page.getByTestId(dataProduct.name).click(), + page.waitForResponse('/api/v1/dataProducts/name/*'), ]); await waitForAllLoadersToDisappear(page); @@ -1644,46 +1643,25 @@ export const selectDomainFromNavbar = async ( page: Page, domain: Domain['responseData'] ) => { - const domainDropdown = page.getByTestId('domain-dropdown'); - const domainTree = page.getByTestId('domain-selectable-tree'); - const searchTerm = domain.displayName ?? domain.name; - const domainOption = page.getByTestId(`tag-${domain.fullyQualifiedName}`); - - const openDropdown = async () => { - await domainDropdown.click(); - await domainTree.waitFor({ state: 'visible' }); - }; - - await openDropdown(); - - const searchBar = domainTree.locator('input[placeholder]').first(); - - await expect - .poll( - async () => { - if (!(await domainTree.isVisible().catch(() => false))) { - await openDropdown(); - } - - const isSearchBarVisible = await searchBar - .isVisible() - .catch(() => false); - - if (isSearchBarVisible) { - await searchBar.fill(searchTerm); - } + await page.getByTestId('domain-dropdown').click(); + await page.getByTestId('domain-selectable-tree').waitFor({ + state: 'visible', + }); - return await domainOption.isVisible().catch(() => false); - }, - { - timeout: 60000, - intervals: [1000, 2000, 5000], - message: `Timed out waiting for domain ${searchTerm} to appear in navbar selector`, - } - ) - .toBe(true); + const searchDomainRes = page.waitForResponse( + (response) => + response.url().includes('/api/v1/search/query') && + response.url().includes('index=domain') + ); + await page + .getByTestId('domain-selectable-tree') + .getByTestId('searchbar') + .fill(domain.displayName); + await searchDomainRes; - await domainOption.click(); + const tagSelector = page.getByTestId(`tag-${domain.fullyQualifiedName}`); + await tagSelector.waitFor({ state: 'visible' }); + await tagSelector.click(); await waitForAllLoadersToDisappear(page); }; @@ -1846,87 +1824,3 @@ export const openDataProductDrawer = async (page: Page, domain: Domain) => { await domainOption.waitFor({ state: 'visible', timeout: 5000 }); await domainOption.click(); }; - -const parseRequestBody = (postData: string | null | undefined) => { - if (!postData) { - return {}; - } - try { - return JSON.parse(postData) as Record; - } catch { - return {}; - } -}; - -const matchesDomainBulkCall = ( - url: string, - method: string, - action: 'add' | 'remove' -) => - method === 'PUT' && - /\/api\/v1\/domains\/[^/]+\/assets\/(add|remove)$/.test(url) && - url.endsWith(`/assets/${action}`); - -export const waitForDomainAssetsAddDryRun = (page: Page) => - page.waitForResponse((response) => { - const request = response.request(); - if (!matchesDomainBulkCall(response.url(), request.method(), 'add')) { - return false; - } - - return parseRequestBody(request.postData()).dryRun === true; - }); - -export const waitForDomainAssetsAddCommit = (page: Page) => - page.waitForResponse((response) => { - const request = response.request(); - if (!matchesDomainBulkCall(response.url(), request.method(), 'add')) { - return false; - } - - return parseRequestBody(request.postData()).dryRun !== true; - }); - -export const waitForDomainAssetsRemoveDryRun = (page: Page) => - page.waitForResponse((response) => { - const request = response.request(); - if (!matchesDomainBulkCall(response.url(), request.method(), 'remove')) { - return false; - } - - return parseRequestBody(request.postData()).dryRun === true; - }); - -export const waitForDomainAssetsRemoveCommit = (page: Page) => - page.waitForResponse((response) => { - const request = response.request(); - if (!matchesDomainBulkCall(response.url(), request.method(), 'remove')) { - return false; - } - - return parseRequestBody(request.postData()).dryRun !== true; - }); - -export const addAssetToDomainViaApi = async ( - apiContext: APIRequestContext, - domain: Domain, - asset: { id: string; type: string } -) => { - const fqn = - domain.responseData?.fullyQualifiedName ?? domain.data.fullyQualifiedName; - const response = await apiContext.put( - `/api/v1/domains/${encodeURIComponent(fqn ?? '')}/assets/add`, - { - data: { assets: [asset] }, - } - ); - - if (!response.ok()) { - const text = await response.text(); - throw new Error( - `addAssetToDomainViaApi failed (${response.status()}): ${text}` - ); - } - - return response.json(); -}; From e4679c1ef4cbf94cea2c92a97fbc8f79d1b0e322 Mon Sep 17 00:00:00 2001 From: Piyush Kumar <13725shpiyush@gmail.com> Date: Mon, 27 Apr 2026 11:23:42 +0530 Subject: [PATCH 8/8] chore: fully revert all unrelated test stabilization files to match PR base --- .../e2e/Features/ActivityAPI.spec.ts | 63 ++++---- .../Glossary/GlossaryWorkflow.spec.ts | 9 +- .../playwright/support/entity/TableClass.ts | 70 ++++++++- .../resources/ui/playwright/utils/domain.ts | 146 +++++++++++++++--- .../resources/ui/src/rest/testAPI.test.ts | 13 ++ 5 files changed, 240 insertions(+), 61 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts index 7ab5dde0c2b2..36211a5e0f67 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts @@ -47,7 +47,6 @@ type ActivityApiEvent = { actor?: { displayName?: string; name?: string }; eventType?: string; summary?: string; - [key: string]: unknown; }; type ActivityApiResponse = { @@ -71,23 +70,7 @@ const openActivityFeedAndWaitForApi = async (page: Page, entityFqn: string) => { return (await activityResponse.json()) as ActivityApiResponse; }; -const activityEventMatches = ( - event: ActivityApiEvent, - eventType: string, - expectedText?: string -) => { - if (event.eventType !== eventType) { - return false; - } - - return expectedText ? JSON.stringify(event).includes(expectedText) : true; -}; - -const waitForActivityEvent = async ( - entityFqn: string, - eventType: string, - expectedText?: string -) => { +const waitForActivityEvent = async (entityFqn: string, eventType: string) => { const { apiContext, afterAction } = await createAdminApiContext(); const activityUrl = `/api/v1/activity/entity/table/name/${encodeURIComponent( entityFqn @@ -107,9 +90,7 @@ const waitForActivityEvent = async ( const body = (await response.json()) as ActivityApiResponse; events = body.data ?? []; - return events.some((event) => - activityEventMatches(event, eventType, expectedText) - ); + return events.some((event) => event.eventType === eventType); }, { timeout: 75000, @@ -119,9 +100,7 @@ const waitForActivityEvent = async ( ) .toBe(true); - return events.find((event) => - activityEventMatches(event, eventType, expectedText) - ); + return events.find((event) => event.eventType === eventType); } finally { await afterAction(); } @@ -177,7 +156,7 @@ test.afterAll('Cleanup delete admin user', async ({ browser }) => { }); test.describe('Activity API - Entity Changes', () => { - test.describe.configure({ timeout: 180000 }); + test.describe.configure({ timeout: 120000 }); test.beforeAll('Setup: create entities and users', async ({ browser }) => { const { apiContext, afterAction } = await performAdminLogin(browser); @@ -227,8 +206,7 @@ test.describe('Activity API - Entity Changes', () => { const descriptionEvent = await waitForActivityEvent( entityFqn, - 'DescriptionUpdated', - newDescription + 'DescriptionUpdated' ); const activityResponse = await openActivityFeedAndWaitForApi( page, @@ -237,8 +215,8 @@ test.describe('Activity API - Entity Changes', () => { const feedContainer = page.locator( '#center-container [data-testid="message-container"]' ); - const renderedDescriptionEvent = activityResponse.data?.find((event) => - activityEventMatches(event, 'DescriptionUpdated', newDescription) + const renderedDescriptionEvent = activityResponse.data?.find( + (event) => event.eventType === 'DescriptionUpdated' ); expect(descriptionEvent).toBeDefined(); @@ -348,15 +326,34 @@ test.describe('Activity API - Entity Changes', () => { await afterAction(); // Wait for activity to be indexed - const activityEvent = await waitForActivityEvent( + await waitForActivityEvent( testTable.entityResponseData.fullyQualifiedName ?? '', - 'DescriptionUpdated', - uniqueDescription + 'DescriptionUpdated' ); + // Navigate to entity page + await testTable.visitEntityPage(page); + + // Navigate to Activity Feed tab + await page.getByTestId('activity_feed').click(); + await waitForPageLoaded(page); + + // Check if there are any feed items + const feedContainer = page + .locator('#center-container [data-testid="message-container"]') + .filter({ + hasText: uniqueDescription, + }); + + await feedContainer.waitFor({ state: 'visible' }); + + const feedContent = await feedContainer.first().textContent(); + + // The activity should show the actor's name (admin user who made the change) + // Activity typically shows username or display name const actorName = adminUser.responseData.displayName; - expect(JSON.stringify(activityEvent)).toContain(actorName); + expect(feedContent).toContain(actorName); }); test('Activity event links to the correct entity', async ({ page }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts index 7678a8ac8cf6..72094300d9e4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryWorkflow.spec.ts @@ -149,7 +149,7 @@ test.describe('Term Status Transitions', () => { await expect(statusBadge).toHaveText('Approved'); }); - test('should start term as In Review when glossary has reviewers', async ({ + test('should start term as Draft when glossary has reviewers', async ({ page, }) => { await sidebarClick(page, SidebarItem.GLOSSARY); @@ -175,14 +175,15 @@ test.describe('Term Status Transitions', () => { // Wait for the table to update - // Check the term shows in the table with In Review status + // Check the term shows in the table with Draft status const termRow = page.locator(`[data-row-key*="${termName}"]`); await expect(termRow).toBeVisible(); + // Look for status badge - should be Draft const statusBadge = termRow.locator('.status-badge'); - await expect(statusBadge).toHaveText('In Review'); + await expect(statusBadge).toHaveText('Draft'); }); // T-C18: Create term - inherits glossary reviewers @@ -331,7 +332,7 @@ test('should display correct status badge color and icon', async ({ page }) => { const statusBadge = termRow.locator('.status-badge'); - await expect(statusBadge).toHaveText('In Review'); + await expect(statusBadge).toHaveText('Draft'); await expect(statusBadge).toBeVisible(); } finally { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 27924bfb0bae..a277503b8d15 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -354,6 +354,24 @@ export class TableClass extends EntityClass { } async visitEntityPage(page: Page, searchTerm?: string) { + if (!this.entityResponseData.fullyQualifiedName) { + const { EntityDataClass } = await import('./EntityDataClass'); + EntityDataClass.loadResponseData(); + } + + if ( + !this.entityResponseData.fullyQualifiedName && + this.entityResponseData.id + ) { + const response = await page.request.get( + `/api/v1/tables/${this.entityResponseData.id}` + ); + + if (response.ok()) { + this.entityResponseData = await response.json(); + } + } + const tableFqn = this.entityResponseData.fullyQualifiedName ?? ''; const canUseDirectNavigation = !searchTerm || (tableFqn.length > 0 && searchTerm === tableFqn); @@ -362,7 +380,9 @@ export class TableClass extends EntityClass { const tableResponse = page.waitForResponse( `/api/v1/tables/name/${encodeURIComponent(tableFqn)}?**` ); - await page.goto(`/table/${encodeURIComponent(tableFqn)}`); + await page.goto(`/table/${encodeURIComponent(tableFqn)}`, { + waitUntil: 'domcontentloaded', + }); await tableResponse; await waitForAllLoadersToDisappear(page); @@ -516,14 +536,38 @@ export class TableClass extends EntityClass { patchData: Operation[]; queryParams?: Record; }) { - const fqn = encodeURIComponent( - this.entityResponseData?.fullyQualifiedName ?? '' - ); + if ( + !this.entityResponseData?.fullyQualifiedName && + this.entityResponseData?.id + ) { + const tableResponse = await apiContext.get( + `/api/v1/tables/${this.entityResponseData.id}` + ); + + if (tableResponse.ok()) { + this.entityResponseData = await tableResponse.json(); + } + } + + const tableId = this.entityResponseData?.id; + const tableFqn = this.entityResponseData?.fullyQualifiedName; + + if (!tableId && !tableFqn) { + throw new Error( + `TableClass.patch: table id and fullyQualifiedName are missing for table "${ + this.entityResponseData?.name ?? this.entity.name + }"` + ); + } + const queryString = queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''; + const response = await apiContext.patch( - `/api/v1/tables/name/${fqn}${queryString}`, + tableId + ? `/api/v1/tables/${tableId}${queryString}` + : `/api/v1/tables/name/${encodeURIComponent(tableFqn!)}${queryString}`, { data: patchData, headers: { @@ -582,4 +626,20 @@ export class TableClass extends EntityClass { entity: this.entityResponseData, }; } + + async setOwner( + apiContext: APIRequestContext, + owner: { id: string; type: 'user' | 'team' } + ) { + return this.patch({ + apiContext, + patchData: [ + { + op: 'add', + path: '/owners', + value: [owner], + }, + ], + }); + } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index ecec928c3412..d967e1269b40 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -331,17 +331,18 @@ export const selectDataProduct = async ( .getByPlaceholder('Search'); await waitForAllLoadersToDisappear(page); + await searchBox.waitFor({ state: 'visible' }); await Promise.all([ - searchBox.fill(dataProduct.name), page.waitForResponse('/api/v1/search/query?q=*&index=dataProduct*'), + searchBox.fill(dataProduct.name), ]); await waitForSearchDebounce(page); await Promise.all([ - page.getByTestId(dataProduct.name).click(), page.waitForResponse('/api/v1/dataProducts/name/*'), + page.getByTestId(dataProduct.name).click(), ]); await waitForAllLoadersToDisappear(page); @@ -1643,25 +1644,48 @@ export const selectDomainFromNavbar = async ( page: Page, domain: Domain['responseData'] ) => { - await page.getByTestId('domain-dropdown').click(); - await page.getByTestId('domain-selectable-tree').waitFor({ - state: 'visible', - }); + const domainDropdown = page.getByTestId('domain-dropdown'); + const domainTree = page.getByTestId('domain-selectable-tree'); + const searchTerm = domain.displayName ?? domain.name; + const domainOption = page.getByTestId(`tag-${domain.fullyQualifiedName}`); + + const openDropdown = async () => { + await domainDropdown.click(); + await domainTree.waitFor({ state: 'visible' }); + }; - const searchDomainRes = page.waitForResponse( - (response) => - response.url().includes('/api/v1/search/query') && - response.url().includes('index=domain') - ); - await page - .getByTestId('domain-selectable-tree') - .getByTestId('searchbar') - .fill(domain.displayName); - await searchDomainRes; + await openDropdown(); + + const searchBar = domainTree.locator('input[placeholder]').first(); - const tagSelector = page.getByTestId(`tag-${domain.fullyQualifiedName}`); - await tagSelector.waitFor({ state: 'visible' }); - await tagSelector.click(); + await expect + .poll( + async () => { + if (!(await domainTree.isVisible().catch(() => false))) { + await openDropdown(); + } + + const isSearchBarVisible = await searchBar + .isVisible() + .catch(() => false); + + if (isSearchBarVisible) { + await searchBar.focus(); + await searchBar.press('Control+a'); + await searchBar.pressSequentially(searchTerm); + } + + return await domainOption.isVisible().catch(() => false); + }, + { + timeout: 60000, + intervals: [1000, 2000, 5000], + message: `Timed out waiting for domain ${searchTerm} to appear in navbar selector`, + } + ) + .toBe(true); + + await domainOption.click(); await waitForAllLoadersToDisappear(page); }; @@ -1824,3 +1848,87 @@ export const openDataProductDrawer = async (page: Page, domain: Domain) => { await domainOption.waitFor({ state: 'visible', timeout: 5000 }); await domainOption.click(); }; + +const parseRequestBody = (postData: string | null | undefined) => { + if (!postData) { + return {}; + } + try { + return JSON.parse(postData) as Record; + } catch { + return {}; + } +}; + +const matchesDomainBulkCall = ( + url: string, + method: string, + action: 'add' | 'remove' +) => + method === 'PUT' && + /\/api\/v1\/domains\/[^/]+\/assets\/(add|remove)$/.test(url) && + url.endsWith(`/assets/${action}`); + +export const waitForDomainAssetsAddDryRun = (page: Page) => + page.waitForResponse((response) => { + const request = response.request(); + if (!matchesDomainBulkCall(response.url(), request.method(), 'add')) { + return false; + } + + return parseRequestBody(request.postData()).dryRun === true; + }); + +export const waitForDomainAssetsAddCommit = (page: Page) => + page.waitForResponse((response) => { + const request = response.request(); + if (!matchesDomainBulkCall(response.url(), request.method(), 'add')) { + return false; + } + + return parseRequestBody(request.postData()).dryRun !== true; + }); + +export const waitForDomainAssetsRemoveDryRun = (page: Page) => + page.waitForResponse((response) => { + const request = response.request(); + if (!matchesDomainBulkCall(response.url(), request.method(), 'remove')) { + return false; + } + + return parseRequestBody(request.postData()).dryRun === true; + }); + +export const waitForDomainAssetsRemoveCommit = (page: Page) => + page.waitForResponse((response) => { + const request = response.request(); + if (!matchesDomainBulkCall(response.url(), request.method(), 'remove')) { + return false; + } + + return parseRequestBody(request.postData()).dryRun !== true; + }); + +export const addAssetToDomainViaApi = async ( + apiContext: APIRequestContext, + domain: Domain, + asset: { id: string; type: string } +) => { + const fqn = + domain.responseData?.fullyQualifiedName ?? domain.data.fullyQualifiedName; + const response = await apiContext.put( + `/api/v1/domains/${encodeURIComponent(fqn ?? '')}/assets/add`, + { + data: { assets: [asset] }, + } + ); + + if (!response.ok()) { + const text = await response.text(); + throw new Error( + `addAssetToDomainViaApi failed (${response.status()}): ${text}` + ); + } + + return response.json(); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts b/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts index 65e73708f682..f92df3c7037d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/testAPI.test.ts @@ -10,6 +10,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* + * Copyright 2026 Collate. + * Licensed 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 { TestCaseStatus } from '../generated/tests/testCase'; // Mock response data