diff --git a/.github/workflows/mysql-nightly-e2e.yml b/.github/workflows/mysql-nightly-e2e.yml index c7a88991516a..6e09dcf29cb6 100644 --- a/.github/workflows/mysql-nightly-e2e.yml +++ b/.github/workflows/mysql-nightly-e2e.yml @@ -42,8 +42,13 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6] - shardTotal: [6] + # shard 1: DataAssetRules suite + # shards 2-6: chromium tests sharded 1/5 .. 5/5 (note: --shard=N/5 is + # hardcoded in the run step, shardTotal here is matrix metadata only) + # shard 7: standalone `stress` project (full multi-entity + # CustomProperties + DataAssetLineage coverage deferred from per-PR runs) + shardIndex: [1, 2, 3, 4, 5, 6, 7] + shardTotal: [7] steps: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main @@ -94,6 +99,10 @@ jobs: --project=DataAssetRulesEnabled \ --project=DataAssetRulesDisabled + elif [ "${{ matrix.shardIndex }}" -eq "7" ]; then + echo "🔹 Running stress suite (full multi-entity coverage)" + npx playwright test --project=stress + else # Shards 2-6 handle chromium tests (5 shards total) CHROMIUM_SHARD=$(( ${{ matrix.shardIndex }} - 1 )) diff --git a/.github/workflows/postgresql-nightly-e2e.yml b/.github/workflows/postgresql-nightly-e2e.yml index 0fe147188ebd..6fc632e486eb 100644 --- a/.github/workflows/postgresql-nightly-e2e.yml +++ b/.github/workflows/postgresql-nightly-e2e.yml @@ -42,8 +42,13 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6] - shardTotal: [6] + # shard 1: DataAssetRules suite + # shards 2-6: chromium tests sharded 1/5 .. 5/5 (note: --shard=N/5 is + # hardcoded in the run step, shardTotal here is matrix metadata only) + # shard 7: standalone `stress` project (full multi-entity + # CustomProperties + DataAssetLineage coverage deferred from per-PR runs) + shardIndex: [1, 2, 3, 4, 5, 6, 7] + shardTotal: [7] steps: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main @@ -94,6 +99,10 @@ jobs: --project=DataAssetRulesEnabled \ --project=DataAssetRulesDisabled + elif [ "${{ matrix.shardIndex }}" -eq "7" ]; then + echo "🔹 Running stress suite (full multi-entity coverage)" + npx playwright test --project=stress + else # Shards 2-5 handle chromium tests (5 shards total) CHROMIUM_SHARD=$(( ${{ matrix.shardIndex }} - 1 )) diff --git a/openmetadata-ui/src/main/resources/ui/playwright.config.ts b/openmetadata-ui/src/main/resources/ui/playwright.config.ts index 8fae4a8ff09c..df59c8874f24 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright.config.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright.config.ts @@ -48,11 +48,19 @@ export default defineConfig({ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ + /* Retry on CI only. Kept at 2 — dropping to 1 surfaced a long tail of + * backend-propagation flakes (task feed not refreshing after API task + * create, RBAC index lag, search dropdown not rendering on the first + * query response, etc.) that are not deterministically reproducible + * and each take their own round-trip to investigate. maxFailures:50 + * still bails fundamentally broken PRs in minutes. */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 3 : undefined, - maxFailures: 500, + /* Bail early when a PR is fundamentally broken — full-suite has ~4,400 + * tests and 50 genuine failures is already far beyond a normal run. + * Healthy runs see <10 failures, so this only kicks in on broken PRs. */ + maxFailures: 50, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], @@ -109,6 +117,7 @@ export default defineConfig({ teardown: 'entity-data-teardown', testIgnore: [ '**/nightly/**', + '**/stress/**', '**/Search/**', '**/Auth/**', '**/Http2/**', @@ -221,6 +230,17 @@ export default defineConfig({ dependencies: ['setup', 'chromium'], fullyParallel: false, }, + // Stress suite: full multi-entity coverage (CustomProperties, DataAssetLineage) + // that's redundant per-PR. Picks up everything under e2e/stress/**. Run via + // postgresql-nightly-e2e.yml workflow_dispatch — NOT included in PR chromium. + { + name: 'stress', + testMatch: '**/stress/**', + use: { ...devices['Desktop Chrome'] }, + dependencies: ['setup', 'entity-data-setup'], + teardown: 'entity-data-teardown', + fullyParallel: true, + }, { name: 'IntakeForm', testMatch: '**/IntakeForm.spec.ts', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts index 7c7f4149e63a..7f2fe1220bb4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts @@ -129,8 +129,6 @@ test.describe('Activity Stream on Entity Pages', () => { await activityFeedTab.click(); await waitForPageLoaded(page); - await page.waitForTimeout(2000); - const messageContainers = page.locator('[data-testid="message-container"]'); const count = await messageContainers.count(); @@ -188,11 +186,12 @@ test.describe('Activity Stream on Entity Pages', () => { await activityFeedTab.click(); await waitForPageLoaded(page); - await page.waitForTimeout(2000); - const allTabInLeftPanel = page.locator( '[data-testid="global-setting-left-panel"]' ); + await allTabInLeftPanel + .waitFor({ state: 'visible', timeout: 2000 }) + .catch(() => undefined); if (await allTabInLeftPanel.isVisible()) { await expect(allTabInLeftPanel).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts index 7b212b2b82ec..2929a560d691 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts @@ -44,6 +44,7 @@ import { pressKeyXTimes, validateImportStatus, } from '../../utils/importUtils'; +import { waitForSearchIndexed } from '../../utils/polling'; import { visitServiceDetailsPage } from '../../utils/service'; interface GlossaryDetails { @@ -671,6 +672,18 @@ test.describe('Bulk Edit Entity', () => { await glossary.create(apiContext); await glossaryTerm.create(apiContext); + // Wait for the glossary term to be indexed in ES before bulk-edit reads + // the glossary's term list. Otherwise the bulk-edit table comes back + // empty, the test fills row 1 with the term's name, and the system + // creates a new term instead of recognizing the existing one — the + // subsequent status assertion gets "Entity created" instead of + // "Entity updated". + await waitForSearchIndexed( + apiContext, + glossaryTerm.responseData.fullyQualifiedName, + 'glossary_term_search_index' + ); + await test.step('create custom properties for extension edit', async () => { customPropertyRecord = await createCustomPropertiesForEntity( page, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts index 53d035bdfce3..07a7a9407f1b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts @@ -21,6 +21,7 @@ import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { createNewPage, redirectToHomePage } from '../../utils/common'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; import { fillTextInputDetails, pressKeyXTimes } from '../../utils/importUtils'; +import { waitForSearchIndexed } from '../../utils/polling'; import { visitServiceDetailsPage } from '../../utils/service'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -39,6 +40,22 @@ test.describe('BulkEditEntity — OperationBadges and Search (all entity types)' await opGlossary.create(apiContext); await opGlossaryTerm.create(apiContext); await opTable.create(apiContext); + + // Wait for ES indexing of both the term and the table before any test + // opens the bulk-edit view; the bulk-edit table reads from search and + // otherwise comes back empty under load, racing the test's row count + // assertions. + await waitForSearchIndexed( + apiContext, + opGlossaryTerm.responseData.fullyQualifiedName, + 'glossary_term_search_index' + ); + await waitForSearchIndexed( + apiContext, + opTable.entityResponseData.fullyQualifiedName ?? '', + 'table_search_index' + ); + await afterAction(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts index ee1511a2b70e..c6b4bc359c03 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts @@ -49,6 +49,7 @@ import { startCsvPreviewAndWaitForGrid, validateImportStatus, } from '../../utils/importUtils'; +import { waitForSearchIndexed } from '../../utils/polling'; // use the admin user to login test.use({ @@ -105,6 +106,15 @@ const expectImportRowStatusesToContain = async ( page: Page, rowStatus: string[] ) => { + // The result grid populates cells asynchronously after Next-click. Without + // first waiting for the row count to match, the toContainText assertion + // can run mid-render against 0 or partial cells, fail under retry too, + // and never recover. Wait for the expected number of detail cells before + // checking text. + await expect(page.locator('.rdg-cell-details')).toHaveCount( + rowStatus.length, + { timeout: 60_000 } + ); await expect(page.locator('.rdg-cell-details')).toContainText(rowStatus); }; @@ -161,6 +171,14 @@ test.describe('Bulk Import Export', () => { const { apiContext, afterAction } = await getApiContext(page); await dbService.create(apiContext); + // Bulk-import reads the service's children list from ES; wait for the + // service to be indexed before the test fetches its export/edit grid. + await waitForSearchIndexed( + apiContext, + dbService.entityResponseData.fullyQualifiedName, + 'database_service_search_index' + ); + await test.step('create custom properties for extension edit', async () => { customPropertyRecord = await createCustomPropertiesForEntity( page, @@ -613,6 +631,14 @@ test.describe('Bulk Import Export', () => { const { apiContext, afterAction } = await getApiContext(page); await dbSchemaEntity.create(apiContext); + // Bulk-import reads the schema's children list from ES; wait for the + // schema to be indexed before the test fetches its export/edit grid. + await waitForSearchIndexed( + apiContext, + dbSchemaEntity.entityResponseData.fullyQualifiedName, + 'database_schema_search_index' + ); + await test.step('create custom properties for extension edit', async () => { customPropertyRecord = await createCustomPropertiesForEntity( page, @@ -775,6 +801,14 @@ test.describe('Bulk Import Export', () => { const { apiContext, afterAction } = await getApiContext(page); await tableEntity.create(apiContext); + // Bulk-import reads the table's columns from ES; wait for the table + // to be indexed before the test fetches its export/edit grid. + await waitForSearchIndexed( + apiContext, + tableEntity.entityResponseData.fullyQualifiedName ?? '', + 'table_search_index' + ); + await test.step('should export data table details', async () => { await tableEntity.visitEntityPage(page); await performBulkDownload(page, tableEntity.entity.name); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImportWithDotInName.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImportWithDotInName.spec.ts index e56f55344ca9..66d7bf5b3feb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImportWithDotInName.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImportWithDotInName.spec.ts @@ -266,7 +266,7 @@ test.describe('Bulk Import Export with Dot in Service Name', () => { .waitFor({ state: 'detached', timeout: 60000 }); // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for async import processing to complete - await page.waitForTimeout(2000); + await page.waitForTimeout(500); }); // Cleanup @@ -440,7 +440,7 @@ test.describe('Bulk Import Export with Dot in Service Name', () => { .waitFor({ state: 'detached', timeout: 60000 }); // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for async import processing to complete - await page.waitForTimeout(2000); + await page.waitForTimeout(500); }); // Cleanup @@ -697,7 +697,7 @@ test.describe('Bulk Import Export with Dot in Service Name', () => { .waitFor({ state: 'detached', timeout: 60000 }); // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for async import processing to complete - await page.waitForTimeout(2000); + await page.waitForTimeout(500); }); // Cleanup @@ -801,7 +801,7 @@ test.describe('Bulk Import Export with Dot in Service Name', () => { .waitFor({ state: 'detached', timeout: 60000 }); // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for async import processing to complete - await page.waitForTimeout(2000); + await page.waitForTimeout(500); }); // Cleanup diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts index ffedc2485095..df7a40b94397 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts @@ -18,6 +18,7 @@ import { TableClass } from '../../../support/entity/TableClass'; import { UserClass } from '../../../support/user/UserClass'; import { performAdminLogin } from '../../../utils/admin'; import { redirectToHomePage, uuid } from '../../../utils/common'; +import { waitForSearchIndexed } from '../../../utils/polling'; import { cleanupDownloadedCSV, performE2EExportImportFlow, @@ -67,6 +68,18 @@ test.describe( const { apiContext, afterAction } = await performAdminLogin(browser); await table.create(apiContext); await table.createTestCase(apiContext); + + // The export step inside performE2EExportImportFlow reads the table's + // test cases via search. Without waiting for ES indexing here, the + // just-created test case is missing from the exported CSV, so the + // import sees only the 4 added rows (not 5 = 1 existing + 4 added) and + // `processed-row` reports "4" instead of the expected "5". + await waitForSearchIndexed( + apiContext, + table.testCasesResponseData[0]?.fullyQualifiedName ?? '', + 'test_case_search_index' + ); + await afterAction(); }); @@ -128,6 +141,16 @@ test.describe( await table.create(apiContext); await table.createTestCase(apiContext); + + // See the matching note in the Admin describe — without waiting for + // ES indexing the export misses the just-created test case and + // processed-row reports "4" instead of "5". + await waitForSearchIndexed( + apiContext, + table.testCasesResponseData[0]?.fullyQualifiedName ?? '', + 'test_case_search_index' + ); + await afterAction(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryStatusFilterLargeDataset.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryStatusFilterLargeDataset.spec.ts index 96855fb490ee..f191c4f3bfeb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryStatusFilterLargeDataset.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryStatusFilterLargeDataset.spec.ts @@ -157,7 +157,7 @@ test.describe('Glossary Status Filter - Large Dataset', () => { // Ignore timeout }); // eslint-disable-next-line playwright/no-wait-for-timeout -- filter results need time to render - await page.waitForTimeout(500); + await page.waitForTimeout(200); }; // Reusable helper to perform search @@ -296,7 +296,7 @@ test.describe('Glossary Status Filter - Large Dataset', () => { await page.locator('.ant-btn-primary', { hasText: 'Save' }).click(); // eslint-disable-next-line playwright/no-wait-for-timeout -- filter state needs time to settle after save - await page.waitForTimeout(1000); + await page.waitForTimeout(300); const allCount = await getRowCount(page); expect(allCount).toBeGreaterThanOrEqual(filteredCount); @@ -372,7 +372,7 @@ test.describe('Glossary Status Filter - Large Dataset', () => { await clearSearch(page); // eslint-disable-next-line playwright/no-wait-for-timeout -- filter results need time to render after clearing search - await page.waitForTimeout(1000); + await page.waitForTimeout(300); const restoredCount = await getRowCount(page); expect(restoredCount).toBeGreaterThanOrEqual(searchCount); @@ -505,7 +505,7 @@ test.describe('Glossary Status Filter - Large Dataset', () => { await cancelButton.click(); // eslint-disable-next-line playwright/no-wait-for-timeout -- dropdown dismiss animation needs time to settle - await page.waitForTimeout(500); + await page.waitForTimeout(200); // Count should remain the same const afterCancelCount = await getRowCount(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts index 89fc056aae65..0136072400c6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts @@ -962,7 +962,7 @@ test.describe('Impact Analysis', () => { await waitForAllLoadersToDisappear(page); // eslint-disable-next-line playwright/no-wait-for-timeout -- column level lineage data takes time to reflect due to UI processing - await page.waitForTimeout(1000); + await page.waitForTimeout(300); const searchInput = page.getByTestId('searchbar'); await expect(searchInput).toBeVisible(); @@ -972,7 +972,7 @@ test.describe('Impact Analysis', () => { await waitForAllLoadersToDisappear(page); // eslint-disable-next-line playwright/no-wait-for-timeout -- search filtering needs time to complete - await page.waitForTimeout(500); + await page.waitForTimeout(200); const rowsWithColumn = page.locator( `[data-row-key*="${columnName}"], tbody tr:has-text("${columnName}")` diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts index ebc6edc71858..63a183efec9c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts @@ -13,9 +13,9 @@ import { Browser, expect, Page } from '@playwright/test'; import { EntityClass } from '../../../support/entity/EntityClass'; -import { test as baseTest } from '../../../support/fixtures/userPages'; import { UserClass } from '../../../support/user/UserClass'; import { performAdminLogin } from '../../../utils/admin'; +import { test as baseTest } from '../../fixtures/pages'; import { SERVICE_ENTITIES } from '../../../constant/service'; import { waitForAllLoadersToDisappear } from '../../../utils/entity'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts index ef8609dd511b..735a1ef226cf 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts @@ -116,7 +116,16 @@ test.describe('Search index - deeply nested oversized columns', () => { // Indexing: the table indexed despite the >32 KB nested leaf, so it is found by name. await searchInput.click(); - const byNameResponse = page.waitForResponse('/api/v1/search/query?*'); + // Match the specific search response that carries the table name, not + // just any /api/v1/search/query call — the searchBox click triggers an + // empty-query suggestion fetch, and `waitForResponse('?*')` would + // resolve on that response and let the test race the actual name-query + // response, leaving the suggestion dropdown un-rendered. + const byNameResponse = page.waitForResponse( + (r) => + r.url().includes('/api/v1/search/query') && + r.url().includes(encodeURIComponent(table.entity.name)) + ); await searchInput.fill(table.entity.name); await byNameResponse; @@ -126,7 +135,11 @@ test.describe('Search index - deeply nested oversized columns', () => { // Searching: the 25-level-deep column name surfaces the table via columnNamesFuzzy - the // mechanism that replaced the dropped flattened columns.children.name search field. await searchInput.clear(); - const byColumnResponse = page.waitForResponse('/api/v1/search/query?*'); + const byColumnResponse = page.waitForResponse( + (r) => + r.url().includes('/api/v1/search/query') && + r.url().includes(encodeURIComponent(leafColumnName)) + ); await searchInput.fill(leafColumnName); await byColumnResponse; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts index b868a6f26f82..d608ae2d8c80 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts @@ -58,17 +58,29 @@ test.describe('Table pagination sorting search scenarios ', () => { await page.click('[data-testid="test-cases"]'); await waitForAllLoadersToDisappear(page); + // Capture the paginated list responses so we wait for the *new* data + // before counting rows. Without this, the loader can finish for the + // current page while the page-2 fetch is still in flight, and the + // row count assertion runs against an empty table mid-transition. + const sortedResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); await page.getByText('Name', { exact: true }).click(); + await sortedResponse; + const nextPageResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); await page.getByTestId('next').click(); + await nextPageResponse; await waitForAllLoadersToDisappear(page); - expect( - await page - .locator('[data-testid="test-case-table"] tbody tr[data-key]') - .count() - ).toBe(15); + // Use toHaveCount instead of .count() === 15 so Playwright auto-retries + // the assertion until the table re-renders with the page-2 rows. + await expect( + page.locator('[data-testid="test-case-table"] tbody tr[data-key]') + ).toHaveCount(15); }); test('Table search with sorting should work', async ({ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts index 3595264806d8..afb84c98ebeb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts @@ -309,7 +309,6 @@ test.describe('Activity Feed - Filters', () => { await subFilterDropdown.click(); await page.getByRole('menuitem', { name: menuLabel }).click(); await expect(subFilterDropdown).toContainText(new RegExp(menuLabel, 'i')); - await page.waitForTimeout(300); }; await subFilterDropdown.click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts index dc7c288ac900..09282bc41ff7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts @@ -300,8 +300,10 @@ test.describe('Task Comments - @Mention', () => { '.mention-dropdown, .ql-mention-list-container, [data-testid="mention-suggestions"]' ); - // Dropdown should appear (may or may not be visible depending on UI implementation) - await page.waitForTimeout(1000); + await mentionDropdown + .first() + .waitFor({ state: 'visible', timeout: 2000 }) + .catch(() => undefined); } } } @@ -339,12 +341,15 @@ test.describe('Task Comments - @Mention', () => { // Type @ and part of username await page.keyboard.type(`@${mentionedUser.responseData.name}`); - await page.waitForTimeout(500); // Select from dropdown if visible const mentionItem = page.locator( `.mention-item, .ql-mention-list-item:has-text("${mentionedUser.responseData.displayName}")` ); + await mentionItem + .first() + .waitFor({ state: 'visible', timeout: 2000 }) + .catch(() => undefined); if (await mentionItem.isVisible()) { await mentionItem.click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/PersonaFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/PersonaFlow.spec.ts index 0c960120f673..82d168a5fa6f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/PersonaFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/PersonaFlow.spec.ts @@ -14,7 +14,6 @@ import { DELETE_TERM } from '../../constant/common'; import { GlobalSettingOptions } from '../../constant/settings'; import { TableClass } from '../../support/entity/TableClass'; -import { expect, test } from '../../support/fixtures/userPages'; import { PersonaClass } from '../../support/persona/PersonaClass'; import { TeamClass } from '../../support/team/TeamClass'; import { UserClass } from '../../support/user/UserClass'; @@ -41,6 +40,7 @@ import { updatePersonaDisplayName, } from '../../utils/persona'; import { settingClick } from '../../utils/sidebar'; +import { expect, test } from '../fixtures/pages'; const PERSONA_DETAILS = { name: `persona-with-%-${uuid()}`, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomProperties.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomProperties.spec.ts deleted file mode 100644 index e6feab5eb310..000000000000 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomProperties.spec.ts +++ /dev/null @@ -1,3862 +0,0 @@ -/* - * Copyright 2024 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. - */ - -/** - * Consolidated custom property tests for all entity types: - * Table, Container, Dashboard, Topic, Pipeline, - * Database, DatabaseSchema, GlossaryTerm, MlModel, SearchIndex, - * StoredProcedure, DashboardDataModel, Metric, Chart, - * ApiCollection, ApiEndpoint, DataProduct, Domain, TableColumn. - * - * Each entity type has ONE describe.serial block so no two workers can ever run - * CP create/edit/delete operations for the same entity type simultaneously. - * - * Entity setup (prepareCustomProperty) is done in beforeAll, not inside tests, - * so cleanup always runs in afterAll even when a test fails mid-way. - */ - -import { APIRequestContext, expect, test } from '@playwright/test'; -import { - CP_NAME_MAX_LENGTH_VALIDATION_ERROR, - INVALID_NAMES, -} from '../../constant/common'; -import { - CUSTOM_PROPERTIES_ENTITIES, - CUSTOM_PROPERTY_INVALID_NAMES, - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR, - NAME_SUFFIX, -} from '../../constant/customProperty'; -import { - CP_BASE_VALUES, - CP_PARTIAL_SEARCH_VALUES, - CP_RANGE_VALUES, -} from '../../constant/customPropertyAdvancedSearch'; -import { ENDPOINT_TO_EXPLORE_TAB_MAP } from '../../constant/explore'; -import { GlobalSettingOptions } from '../../constant/settings'; -import { SidebarItem } from '../../constant/sidebar'; -import { DataProduct } from '../../support/domain/DataProduct'; -import { Domain } from '../../support/domain/Domain'; -import { ApiCollectionClass } from '../../support/entity/ApiCollectionClass'; -import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass'; -import { ChartClass } from '../../support/entity/ChartClass'; -import { ContainerClass } from '../../support/entity/ContainerClass'; -import { DashboardClass } from '../../support/entity/DashboardClass'; -import { DashboardDataModelClass } from '../../support/entity/DashboardDataModelClass'; -import { DatabaseClass } from '../../support/entity/DatabaseClass'; -import { DatabaseSchemaClass } from '../../support/entity/DatabaseSchemaClass'; -import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; -import { EntityDataClass } from '../../support/entity/EntityDataClass'; -import { MetricClass } from '../../support/entity/MetricClass'; -import { MlModelClass } from '../../support/entity/MlModelClass'; -import { PipelineClass } from '../../support/entity/PipelineClass'; -import { SearchIndexClass } from '../../support/entity/SearchIndexClass'; -import { StoredProcedureClass } from '../../support/entity/StoredProcedureClass'; -import { TableClass } from '../../support/entity/TableClass'; -import { TopicClass } from '../../support/entity/TopicClass'; -import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; -import { UserClass } from '../../support/user/UserClass'; -import { - CONDITIONS_MUST, - selectOption, - showAdvancedSearchDialog, -} from '../../utils/advancedSearch'; -import { advanceSearchSaveFilter } from '../../utils/advancedSearchCustomProperty'; -import { - clickOutside, - createNewPage, - getApiContext, - redirectToHomePage, - uuid, -} from '../../utils/common'; -import { - addCustomPropertiesForEntity, - createCustomPropertyForEntity, - CustomProperty, - CustomPropertyTypeByName, - deleteCreatedProperty, - editCreatedProperty, - fillTableColumnInputDetails, - setValueForProperty, - updateCustomPropertyInRightPanel, - validateValueForProperty, - verifyCustomPropertyInAdvancedSearch, - verifyTableColumnCustomPropertyPersistence, -} from '../../utils/customProperty'; -import { - applyCustomPropertyFilter, - clearAdvancedSearchFilters, - CPASTestData, - setupCustomPropertyAdvancedSearchTest, - verifySearchResults, -} from '../../utils/customPropertyAdvancedSearchUtils'; -import { - getEntityDisplayName, - waitForAllLoadersToDisappear, -} from '../../utils/entity'; -import { getEntityFqn } from '../../utils/entityPanel'; -import { navigateToExploreAndSelectEntity } from '../../utils/explore'; -import { setSliderValue } from '../../utils/searchSettingUtils'; -import { - settingClick, - SettingOptionsType, - sidebarClick, -} from '../../utils/sidebar'; -import { CustomPropertiesPageObject } from '../PageObject/Explore/CustomPropertiesPageObject'; -import { RightPanelPageObject } from '../PageObject/Explore/RightPanelPageObject'; - -test.use({ storageState: 'playwright/.auth/admin.json' }); - -type CustomPropertyEntity = - (typeof CUSTOM_PROPERTIES_ENTITIES)[keyof typeof CUSTOM_PROPERTIES_ENTITIES]; - -type AssetTypes = - | TableClass - | ContainerClass - | DashboardClass - | TopicClass - | PipelineClass - | DatabaseClass - | DatabaseSchemaClass - | MlModelClass - | SearchIndexClass - | StoredProcedureClass - | DashboardDataModelClass - | MetricClass - | ChartClass - | ApiCollectionClass - | ApiEndpointClass; - -type OtherTypes = GlossaryTerm | Domain | DataProduct; - -type CRUDEntity = { - key: keyof typeof CUSTOM_PROPERTIES_ENTITIES; - makeInstance: (() => AssetTypes | OtherTypes) | null; -}; - -type ColumnsTestData = { - customPropertyValue: Record< - string, - { - value: string; - newValue: string; - property: CustomProperty; - } - >; - cleanupUser: (apiContext: APIRequestContext) => Promise; - users: Record; - columnFqn: string; - tableFqn: string; -}; - -const BASIC_PROPERTIES = [ - 'Integer', - 'String', - 'Markdown', - 'Duration', - 'Email', - 'Number', - 'Sql Query', - 'Time Interval', - 'Timestamp', - 'Hyperlink', -]; - -const CONFIG_PROPERTIES: Array<{ - name: string; - getConfig: (e: CustomPropertyEntity) => Record; - editPropertyType?: string; - verifyAdvancedSearch: boolean; - searchTableColumns?: boolean; -}> = [ - { - name: 'Enum', - getConfig: (e) => ({ enumConfig: e.enumConfig }), - editPropertyType: 'Enum', - verifyAdvancedSearch: true, - }, - { - name: 'Table', - getConfig: (e) => ({ tableConfig: e.tableConfig }), - editPropertyType: 'Table', - verifyAdvancedSearch: true, - searchTableColumns: true, - }, - { - name: 'Entity Reference', - getConfig: (e) => ({ entityReferenceConfig: e.entityReferenceConfig }), - editPropertyType: 'Entity Reference', - verifyAdvancedSearch: true, - }, - { - name: 'Entity Reference List', - getConfig: (e) => ({ entityReferenceConfig: e.entityReferenceConfig }), - editPropertyType: 'Entity Reference List', - verifyAdvancedSearch: true, - }, - { - name: 'Date', - getConfig: (e) => ({ formatConfig: e.dateFormatConfig }), - verifyAdvancedSearch: false, - }, - { - name: 'Time', - getConfig: (e) => ({ formatConfig: e.timeFormatConfig }), - verifyAdvancedSearch: true, - }, - { - name: 'Date Time', - getConfig: (e) => ({ formatConfig: e.dateTimeFormatConfig }), - verifyAdvancedSearch: true, - }, -]; - -const ALL_ENTITIES: CRUDEntity[] = [ - // Part-1 entities - { key: 'entity_table', makeInstance: () => new TableClass() }, - { key: 'entity_container', makeInstance: () => new ContainerClass() }, - { key: 'entity_dashboard', makeInstance: () => new DashboardClass() }, - { key: 'entity_topic', makeInstance: () => new TopicClass() }, - { key: 'entity_pipeline', makeInstance: () => new PipelineClass() }, - // Part-2 entities - { key: 'entity_database', makeInstance: () => new DatabaseClass() }, - { - key: 'entity_databaseSchema', - makeInstance: () => new DatabaseSchemaClass(), - }, - { key: 'entity_glossaryTerm', makeInstance: () => new GlossaryTerm() }, - { key: 'entity_mlmodel', makeInstance: () => new MlModelClass() }, - { key: 'entity_searchIndex', makeInstance: () => new SearchIndexClass() }, - { - key: 'entity_storedProcedure', - makeInstance: () => new StoredProcedureClass(), - }, - { - key: 'entity_dashboardDataModel', - makeInstance: () => new DashboardDataModelClass(), - }, - { key: 'entity_metric', makeInstance: () => new MetricClass() }, - { key: 'entity_chart', makeInstance: () => new ChartClass() }, - // Part-3 entities - { key: 'entity_apiCollection', makeInstance: () => new ApiCollectionClass() }, - { key: 'entity_apiEndpoint', makeInstance: () => new ApiEndpointClass() }, - { key: 'entity_dataProduct', makeInstance: () => new DataProduct() }, - { key: 'entity_domain', makeInstance: null }, - { key: 'entity_tableColumn', makeInstance: null }, -]; - -ALL_ENTITIES.forEach(({ key, makeInstance }) => { - const entity = CUSTOM_PROPERTIES_ENTITIES[key]; - - test.describe - .serial(`Add update and delete custom properties for ${entity.name}`, () => { - let mainEntity: AssetTypes | OtherTypes = {} as AssetTypes | OtherTypes; - let responseData: - | AssetTypes['entityResponseData'] - | OtherTypes['responseData']; - - let tableForColumnTest: TableClass | null = null; - const users: UserClass[] = []; - - // Dashboard-specific state - let dashboardTopic1: TopicClass; - let dashboardTopic2: TopicClass; - const cpasTestData: CPASTestData = { - types: [], - cpMetadataType: { name: '', id: '' }, - createdCPData: [], - }; - const propertyNames: Record = {}; - const dashboardSearchPropertyName = `cp-${uuid()}-${ - entity.name - }${NAME_SUFFIX}`; - const dashboardPropertyValue = `EXECUTIVE_DASHBOARD_${uuid()}`; - - // Pipeline-specific state - const pipelineSearchPropertyName = `cp-${uuid()}-${ - entity.name - }${NAME_SUFFIX}`; - const pipelinePropertyValue = `ETL_PRODUCTION_${uuid()}`; - - test.beforeAll(async ({ browser }) => { - const { page, apiContext, afterAction } = await createNewPage(browser); - - if (key === 'entity_tableColumn') { - tableForColumnTest = new TableClass(); - await tableForColumnTest.create(apiContext); - } else if (makeInstance !== null) { - mainEntity = makeInstance(); - await mainEntity.create(apiContext); - await mainEntity.prepareCustomProperty(apiContext); - - if (key === 'entity_table') { - for (let i = 0; i < 5; i++) { - const user = new UserClass(); - await user.create(apiContext); - users.push(user); - } - } else if (key === 'entity_dashboard') { - dashboardTopic1 = new TopicClass(); - dashboardTopic2 = new TopicClass(); - await dashboardTopic1.create(apiContext); - await dashboardTopic2.create(apiContext); - await setupCustomPropertyAdvancedSearchTest( - page, - cpasTestData, - mainEntity as DashboardClass, - dashboardTopic1, - dashboardTopic2 - ); - cpasTestData.createdCPData.forEach((cp) => { - propertyNames[cp.propertyType.name] = cp.name; - }); - } - } - responseData = - (mainEntity as AssetTypes).entityResponseData ?? - (mainEntity as OtherTypes).responseData; - - await afterAction(); - }); - - test.afterAll(async ({ browser }) => { - const { apiContext, afterAction } = await createNewPage(browser); - - if (makeInstance !== null) { - await mainEntity.delete(apiContext); - if (key === 'entity_dataProduct') { - for (const domain of (mainEntity as DataProduct).getDomains()) { - await domain.delete(apiContext); - } - } - } else if (tableForColumnTest !== null) { - await tableForColumnTest.delete(apiContext); - } - if (users.length) { - for (const user of users) { - await user.delete(apiContext); - } - } - if (dashboardTopic1) { - await dashboardTopic1.delete(apiContext); - } - if (dashboardTopic2) { - await dashboardTopic2.delete(apiContext); - } - - await afterAction(); - }); - - test.beforeEach(async ({ page }) => { - await redirectToHomePage(page); - }); - - // ── 17 CRUD tests ────────────────────────────────────────────────────── - - BASIC_PROPERTIES.forEach((property) => { - test(property, async ({ page }) => { - test.slow(); - const propertyName = `cp-${uuid()}-${entity.name}${NAME_SUFFIX}`; - - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - await addCustomPropertiesForEntity({ - page, - propertyName, - customPropertyData: entity, - customType: property, - }); - - await editCreatedProperty(page, propertyName); - - await verifyCustomPropertyInAdvancedSearch( - page, - propertyName.toUpperCase(), - entity.name.charAt(0).toUpperCase() + entity.name.slice(1), - property - ); - - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - await deleteCreatedProperty(page, propertyName); - }); - }); - - CONFIG_PROPERTIES.forEach((propertyConfig) => { - test(propertyConfig.name, async ({ page }) => { - test.slow(); - const propertyName = `cp-${uuid()}-${entity.name}${NAME_SUFFIX}`; - - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - await addCustomPropertiesForEntity({ - page, - propertyName, - customPropertyData: entity, - customType: propertyConfig.name, - ...propertyConfig.getConfig(entity), - }); - - if (propertyConfig.editPropertyType) { - await editCreatedProperty( - page, - propertyName, - propertyConfig.editPropertyType - ); - } else { - await editCreatedProperty(page, propertyName); - } - - if (propertyConfig.verifyAdvancedSearch) { - if (propertyConfig.searchTableColumns) { - await verifyCustomPropertyInAdvancedSearch( - page, - propertyName.toUpperCase(), - entity.name.charAt(0).toUpperCase() + entity.name.slice(1), - propertyConfig.name, - entity.tableConfig.columns - ); - } else { - await verifyCustomPropertyInAdvancedSearch( - page, - propertyName.toUpperCase(), - entity.name.charAt(0).toUpperCase() + entity.name.slice(1) - ); - } - } - - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - await deleteCreatedProperty(page, propertyName); - }); - }); - - // ── Set & Update all CP types (entities with a UI entity page) ────────── - - if (makeInstance !== null) { - test(`Set & Update all CP types on ${entity.name}`, async ({ page }) => { - // 5 minutes timeout since the test handles set->update operation on all - // custom property types sequentially - test.setTimeout(300000); - const properties = Object.values(CustomPropertyTypeByName); - - await test.step('Set all CP types', async () => { - await mainEntity.visitEntityPage(page); - for (const type of properties) { - await mainEntity.updateCustomProperty( - page, - mainEntity.customPropertyValue[type].property, - mainEntity.customPropertyValue[type].value - ); - } - }); - - await test.step('Update all CP types', async () => { - await mainEntity.visitEntityPage(page); - for (const type of properties) { - await mainEntity.updateCustomProperty( - page, - mainEntity.customPropertyValue[type].property, - mainEntity.customPropertyValue[type].newValue - ); - } - }); - - await test.step('Update all CP types in Right Panel', async () => { - for (const [index, type] of properties.entries()) { - await updateCustomPropertyInRightPanel({ - page, - entityName: getEntityDisplayName(responseData), - propertyDetails: mainEntity.customPropertyValue[type].property, - value: mainEntity.customPropertyValue[type].value, - endpoint: mainEntity.endpoint, - skipNavigation: index > 0, - exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], - entityFQN: responseData.fullyQualifiedName, - }); - } - }); - }); - } - - // ── Table-specific extra tests ────────────────────────────────────────── - - if (key === 'entity_table') { - test('sqlQuery shows scrollable CodeMirror container and no expand toggle', async ({ - page, - }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.SQL_QUERY] - .property.name; - - await test.step('Set multi-line SQL value', async () => { - await mainEntity.visitEntityPage(page); - await waitForAllLoadersToDisappear(page); - await page.getByTestId('custom_properties').click(); - - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - const editButton = container.getByTestId('edit-icon'); - await editButton.scrollIntoViewIfNeeded(); - await expect(editButton).toBeVisible(); - await expect(editButton).toBeEnabled(); - await editButton.click(); - - await page.locator("pre[role='presentation']").last().click(); - const value = - "SELECT id, name, email\nFROM users\nWHERE active = true\nAND department = 'engineering'\nORDER BY created_at DESC\nLIMIT 100"; - await page.keyboard.type(value + '\n' + value); - - const patchResponse = page.waitForResponse( - `/api/v1/${entity.entityApiType}/*` - ); - await container.getByTestId('inline-save-btn').click(); - expect((await patchResponse).status()).toBe(200); - await waitForAllLoadersToDisappear(page); - }); - - await test.step('Verify .CodeMirror-scroll is height-constrained and scrollable', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - const codeMirrorScroll = container.locator('.CodeMirror-scroll'); - await expect(codeMirrorScroll).toBeVisible(); - const isScrollable = await codeMirrorScroll.evaluate( - (el) => el.scrollHeight > el.clientHeight - ); - expect(isScrollable).toBeTruthy(); - }); - - await test.step('Verify expand/collapse toggle is hidden', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - await expect( - container.getByTestId(`toggle-${propertyName}`) - ).not.toBeVisible(); - }); - }); - - test('entityReferenceList shows item count, scrollable list, no expand toggle', async ({ - page, - }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[ - CustomPropertyTypeByName.ENTITY_REFERENCE_LIST - ].property.name; - - await test.step('Set 5 user references as value', async () => { - await redirectToHomePage(page); - await mainEntity.visitEntityPage(page); - await waitForAllLoadersToDisappear(page); - await page.getByTestId('custom_properties').click(); - - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - const editButton = container.getByTestId('edit-icon'); - await editButton.scrollIntoViewIfNeeded(); - await expect(editButton).toBeVisible(); - await expect(editButton).toBeEnabled(); - await editButton.click(); - - for (const user of users) { - const searchApi = `**/api/v1/search/query?q=*${encodeURIComponent( - user.getUserName() - )}*`; - const searchResponse = page.waitForResponse(searchApi); - await page.locator('#entityReference').clear(); - await page.locator('#entityReference').fill(user.getUserName()); - await searchResponse; - await page - .locator(`[data-testid="${user.getUserDisplayName()}"]`) - .click(); - } - await clickOutside(page); - const patchResponse = page.waitForResponse( - `/api/v1/${entity.entityApiType}/*` - ); - await container.getByTestId('inline-save-btn').click(); - expect((await patchResponse).status()).toBe(200); - await waitForAllLoadersToDisappear(page); - }); - - await test.step('Verify item count (7) in property name', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - await expect(container.getByTestId('property-name')).toContainText( - '(7)' - ); - }); - - await test.step('Verify .entity-list-body is scrollable', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - const listBody = container.locator('.entity-list-body'); - await expect(listBody).toBeVisible(); - const isScrollable = await listBody.evaluate( - (el) => el.scrollHeight > el.clientHeight - ); - expect(isScrollable).toBeTruthy(); - }); - - await test.step('Verify expand/collapse toggle is hidden', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - await expect( - container.getByTestId(`toggle-${propertyName}`) - ).not.toBeVisible(); - }); - }); - - test('User visible in right panel when added as entityReferenceList custom property', async ({ - page, - }) => { - test.slow(); - const { apiContext, afterAction } = await getApiContext(page); - const propertyName = - mainEntity.customPropertyValue[ - CustomPropertyTypeByName.ENTITY_REFERENCE_LIST - ].property.name; - const testUser = users[0]; - const userName = testUser.responseData.name; - const userDisplayName = testUser.responseData.displayName ?? userName; - - await (mainEntity as TableClass).patch({ - apiContext, - patchData: [ - { - op: 'add', - path: '/extension', - value: { - [propertyName]: [ - { - id: testUser.responseData.id, - type: 'user', - name: userName, - fullyQualifiedName: - testUser.responseData.fullyQualifiedName, - }, - ], - }, - }, - ], - }); - - await mainEntity.visitEntityPage(page); - - const userElement = page.getByTestId(userName); - const isUserVisible = await userElement.isVisible(); - if (!isUserVisible) { - await page.getByTestId('custom_properties').click(); - } - - const rightPanelSection = page.getByTestId(propertyName); - await expect(rightPanelSection).toBeVisible(); - - const userLink = page.getByTestId(userName).getByRole('link'); - await expect(userLink).toContainText(userName); - - const userDetailsResponse = page.waitForResponse( - '/api/v1/users/name/*' - ); - await userLink.click(); - await userDetailsResponse; - - await expect(page).toHaveURL( - new RegExp(`/users/(%22)?${userName}(%22)?`, 'i') - ); - await expect(page.getByTestId('user-display-name')).toHaveText( - userDisplayName - ); - - await (mainEntity as TableClass).patch({ - apiContext, - patchData: [ - { - op: 'add', - path: `/extension/${propertyName}`, - value: [], - }, - ], - }); - - await afterAction(); - }); - - test('table-cp shows row count, scrollable container, no expand toggle', async ({ - page, - }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.TABLE_CP] - .property.name; - - await test.step('Add 5 rows of data to table property', async () => { - await redirectToHomePage(page); - await mainEntity.visitEntityPage(page); - await waitForAllLoadersToDisappear(page); - await page.getByTestId('custom_properties').click(); - - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - const editButton = container.getByTestId('edit-icon'); - await editButton.scrollIntoViewIfNeeded(); - await expect(editButton).toBeVisible(); - await expect(editButton).toBeEnabled(); - await editButton.click(); - - for (let i = 0; i < 5; i++) { - await page.getByTestId('add-new-row').click(); - await expect(page.locator('.om-rdg')).toBeVisible(); - await fillTableColumnInputDetails( - page, - `row${i + 1}-col1`, - entity.tableConfig.columns[0] - ); - await fillTableColumnInputDetails( - page, - `row${i + 1}-col2`, - entity.tableConfig.columns[1] - ); - } - - const patchResponse = page.waitForResponse( - `/api/v1/${entity.entityApiType}/*` - ); - await page.getByTestId('update-table-type-property').click(); - expect((await patchResponse).status()).toBe(200); - await waitForAllLoadersToDisappear(page); - }); - - await test.step('Verify row count (5) in property name', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - await expect(container.getByTestId('property-name')).toContainText( - '(5)' - ); - }); - - await test.step('Verify .custom-property-scrollable-container is scrollable', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - const scrollContainer = container.locator( - '.custom-property-scrollable-container' - ); - await expect(scrollContainer).toBeVisible(); - const isScrollable = await scrollContainer.evaluate( - (el) => el.scrollHeight > el.clientHeight - ); - expect(isScrollable).toBeTruthy(); - }); - - await test.step('Verify expand/collapse toggle is hidden', async () => { - const container = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - await expect( - container.getByTestId(`toggle-${propertyName}`) - ).not.toBeVisible(); - }); - }); - - test('Enum: Set Value, Verify, Remove Value', async ({ page }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.ENUM].property - .name; - - await mainEntity.visitEntityPage(page); - - await page.locator('[data-testid="custom_properties"]').waitFor({ - state: 'visible', - }); - - const getCustomPropertiesResponse = page.waitForResponse( - 'api/v1/metadata/types/name/table?fields=customProperties*' - ); - await page.getByTestId('custom_properties').click(); - await getCustomPropertiesResponse; - - const propertyCard = page.getByTestId( - `custom-property-${propertyName}-card` - ); - await propertyCard.getByTestId('edit-icon').click(); - - const enumSelect = page.locator('[data-testid="enum-select"]'); - await expect(enumSelect).toBeVisible(); - await enumSelect.click(); - - await page - .locator('.ant-select-item-option-content') - .getByText('medium', { exact: true }) - .click(); - - const saveButton = page.locator('[data-testid="inline-save-btn"]'); - const patchValue1 = page.waitForResponse( - (resp) => - resp.url().includes('/api/v1/tables/') && - resp.request().method() === 'PATCH' - ); - await saveButton.click(); - await patchValue1; - - await expect(propertyCard.getByTestId('enum-value')).toContainText( - 'medium' - ); - - await propertyCard.locator('[data-testid="edit-icon"]').click(); - await enumSelect.hover(); - - const clearIcon = enumSelect.locator('.ant-select-clear'); - if (await clearIcon.isVisible()) { - await clearIcon.click(); - } else { - const enumInput = page.locator('#enumValues'); - await enumInput.click(); - await enumInput.fill(''); - } - - const patchValue2 = page.waitForResponse( - (resp) => - resp.url().includes('/api/v1/tables/') && - resp.request().method() === 'PATCH' - ); - await saveButton.click(); - await patchValue2; - - await expect(propertyCard.getByTestId('no-data')).toBeVisible(); - }); - - test('Duration: advanced search equalTo and Contains operators', async ({ - page, - }) => { - test.slow(); - const durationPropertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.DURATION] - .property.name; - const durationPropertyValue = 'PT1H30M'; - - await test.step('Assign Custom Property Value', async () => { - await mainEntity.visitEntityPage(page); - - const customPropertyResponse = page.waitForResponse( - '/api/v1/metadata/types/name/table?fields=customProperties' - ); - await page.getByTestId('custom_properties').click(); - await customPropertyResponse; - - await page.locator('.ant-skeleton-active').waitFor({ - state: 'detached', - }); - - await page - .getByTestId(`custom-property-${durationPropertyName}-card`) - .getByTestId('edit-icon') - .click(); - - await page.getByTestId('duration-input').fill(durationPropertyValue); - - const saveResponse = page.waitForResponse('/api/v1/tables/*'); - await page.getByTestId('inline-save-btn').click(); - await saveResponse; - }); - - await test.step('Verify Duration Type in Advance Search', async () => { - await sidebarClick(page, SidebarItem.EXPLORE); - await showAdvancedSearchDialog(page); - - const ruleLocator = page.locator('.rule').nth(0); - - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - 'Custom Properties', - true - ); - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - 'Table', - true - ); - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - durationPropertyName, - true - ); - - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - CONDITIONS_MUST.equalTo.name - ); - - const inputElement = ruleLocator.locator( - '.rule--widget--TEXT input[type="text"]' - ); - await inputElement.fill(durationPropertyValue); - - await advanceSearchSaveFilter(page, durationPropertyValue); - - await expect( - page.getByTestId( - `table-data-card_${responseData.fullyQualifiedName ?? ''}` - ) - ).toBeVisible(); - - const partialSearchValue = durationPropertyValue.slice(0, 3); - await page.getByTestId('advance-search-filter-btn').click(); - await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible(); - - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - 'Contains' - ); - await inputElement.fill(partialSearchValue); - - await advanceSearchSaveFilter(page, partialSearchValue); - - await expect( - page.getByTestId( - `table-data-card_${responseData.fullyQualifiedName ?? ''}` - ) - ).toBeVisible(); - }); - }); - - // #27482 – Regression: between operator was dropping the upper bound - if (key === 'entity_table') { - test('Number CP between operator sends gte/lte bounds (Issue #27482)', async ({ - page, - }) => { - test.slow(); - const numberPropertyName = `pwNumberBetweenTest${uuid()}`; - const assignedValue = '55.7'; - - await test.step('Create number custom property and assign value', async () => { - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - - await addCustomPropertiesForEntity({ - page, - propertyName: numberPropertyName, - customPropertyData: entity, - customType: 'Number', - }); - - await mainEntity.visitEntityPage(page); - - const customPropertyResponse = page.waitForResponse( - '/api/v1/metadata/types/name/table?fields=customProperties' - ); - await page.getByTestId('custom_properties').click(); - await customPropertyResponse; - - await page.locator('.ant-skeleton-active').waitFor({ - state: 'detached', - }); - - await setValueForProperty({ - page, - propertyName: numberPropertyName, - value: assignedValue, - propertyType: 'number', - endpoint: EntityTypeEndpoint.Table, - }); - }); - - await test.step('between [50, 60]: query_filter must contain gte:50 and lte:60', async () => { - await sidebarClick(page, SidebarItem.EXPLORE); - await showAdvancedSearchDialog(page); - - await applyCustomPropertyFilter( - page, - numberPropertyName, - 'between', - CP_RANGE_VALUES.number, - 'Table' - ); - - const searchResponse = page.waitForResponse( - '/api/v1/search/query?*index=dataAsset*' - ); - await page.getByTestId('apply-btn').click(); - const res = await searchResponse; - - const url = res.request().url(); - const params = new URLSearchParams(url.split('?')[1]); - const queryFilter = JSON.parse(params.get('query_filter') ?? '{}'); - const queryFilterStr = JSON.stringify(queryFilter); - - expect(queryFilterStr).toContain('"gte":50'); - expect(queryFilterStr).toContain('"lte":60'); - - await expect( - page.getByTestId( - `table-data-card_${responseData.fullyQualifiedName ?? ''}` - ) - ).toBeVisible(); - - await clearAdvancedSearchFilters(page); - }); - - await test.step('not_between [1, 5]: query_filter must contain must_not with gte:1 and lte:5', async () => { - await sidebarClick(page, SidebarItem.EXPLORE); - await showAdvancedSearchDialog(page); - - await applyCustomPropertyFilter( - page, - numberPropertyName, - 'not_between', - { start: 1, end: 5 }, - 'Table' - ); - - const searchResponse = page.waitForResponse( - '/api/v1/search/query?*index=dataAsset*' - ); - await page.getByTestId('apply-btn').click(); - const res = await searchResponse; - - const url = res.request().url(); - const params = new URLSearchParams(url.split('?')[1]); - const queryFilter = JSON.parse(params.get('query_filter') ?? '{}'); - const queryFilterStr = JSON.stringify(queryFilter); - - expect(queryFilterStr).toContain('"must_not"'); - expect(queryFilterStr).toContain('"gte":1'); - expect(queryFilterStr).toContain('"lte":5'); - - await clearAdvancedSearchFilters(page); - }); - - await test.step('between [100, 200]: entity with value 55.7 should NOT be visible', async () => { - await sidebarClick(page, SidebarItem.EXPLORE); - await showAdvancedSearchDialog(page); - - await applyCustomPropertyFilter( - page, - numberPropertyName, - 'between', - { start: 100, end: 200 }, - 'Table' - ); - - const searchResponse = page.waitForResponse( - '/api/v1/search/query?*index=dataAsset*' - ); - await page.getByTestId('apply-btn').click(); - await searchResponse; - - await expect( - page.getByTestId( - `table-data-card_${responseData.fullyQualifiedName ?? ''}` - ) - ).not.toBeVisible(); - - await clearAdvancedSearchFilters(page); - }); - - await test.step('Cleanup', async () => { - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - await deleteCreatedProperty(page, numberPropertyName); - }); - }); - - test('no duplicate card after update', async ({ page }) => { - test.slow(); - - const propertyName = `pw.edge.update.${uuid()}`; - - await test.step('Create property', async () => { - await settingClick( - page, - entity.entityApiType as SettingOptionsType, - true - ); - await addCustomPropertiesForEntity({ - page, - propertyName, - customPropertyData: entity, - customType: 'String', - }); - }); - - await test.step('Set initial value', async () => { - await mainEntity.visitEntityPage(page); - await waitForAllLoadersToDisappear(page); - - await setValueForProperty({ - page, - propertyName, - value: 'initial value', - propertyType: 'string', - endpoint: EntityTypeEndpoint.Table, - }); - - await validateValueForProperty({ - page, - propertyName, - value: 'initial value', - propertyType: 'string', - }); - }); - - await test.step('Update value and verify only one card exists', async () => { - await setValueForProperty({ - page, - propertyName, - value: 'updated value', - propertyType: 'string', - endpoint: EntityTypeEndpoint.Table, - }); - - await validateValueForProperty({ - page, - propertyName, - value: 'updated value', - propertyType: 'string', - }); - - await expect( - page.getByTestId(`custom-property-${propertyName}-card`) - ).toHaveCount(1); - await expect( - page.getByTestId(`custom-property-"${propertyName}"-card`) - ).toHaveCount(0); - }); - - await test.step('Value persists after reload', async () => { - await page.reload(); - await waitForAllLoadersToDisappear(page); - - await validateValueForProperty({ - page, - propertyName, - value: 'updated value', - propertyType: 'string', - }); - - await expect( - page.getByTestId(`custom-property-${propertyName}-card`) - ).toHaveCount(1); - await expect( - page.getByTestId(`custom-property-"${propertyName}"-card`) - ).toHaveCount(0); - }); - - await test.step('Updated value is searchable via Advanced Search', async () => { - await sidebarClick(page, SidebarItem.EXPLORE); - - await showAdvancedSearchDialog(page); - - const ruleLocator = page.locator('.rule').nth(0); - - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - 'Custom Properties', - true - ); - - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - 'Table', - true - ); - - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - propertyName, - true - ); - - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - CONDITIONS_MUST.equalTo.name - ); - - await ruleLocator - .locator('.rule--widget--TEXT input[type="text"]') - .fill('updated value'); - - await advanceSearchSaveFilter(page, 'updated value'); - - await expect( - page.getByTestId( - `table-data-card_${ - (mainEntity as TableClass).entityResponseData - .fullyQualifiedName - }` - ) - ).toBeVisible(); - }); - }); - } - } - - // ── Container-specific extra tests ───────────────────────────────────── - - if (key === 'entity_container') { - test('should show No Data placeholder when hyperlink has no value', async ({ - page, - }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.HYPERLINK_CP] - .property.name; - - await EntityDataClass.container1.visitEntityPage(page); - await page.click('[data-testid="custom_properties"]'); - - const containerLocator = page.locator( - `[data-testid="custom-property-${propertyName}-card"]` - ); - - await expect(containerLocator.getByTestId('no-data')).toBeVisible(); - await expect(containerLocator.getByTestId('no-data')).toContainText( - 'Not set' - ); - }); - - test('should reject javascript: protocol URLs for XSS protection', async ({ - page, - }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.HYPERLINK_CP] - .property.name; - - await EntityDataClass.container1.visitEntityPage(page); - await page.click('[data-testid="custom_properties"]'); - - const editButton = page.locator( - `[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]` - ); - await editButton.scrollIntoViewIfNeeded(); - await editButton.click(); - - await page - .locator('[data-testid="hyperlink-url-input"]') - .fill('javascript:alert("XSS")'); - - await expect( - page.locator('.ant-form-item-explain-error') - ).toContainText('URL must use http or https protocol'); - - await page.locator('[data-testid="inline-cancel-btn"]').click(); - }); - - test('should accept valid http and https URLs', async ({ page }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.HYPERLINK_CP] - .property.name; - - await EntityDataClass.container1.visitEntityPage(page); - - await setValueForProperty({ - page, - propertyName, - value: 'https://openmetadata.io,OpenMetadata Docs', - propertyType: 'hyperlink-cp', - endpoint: EntityTypeEndpoint.Container, - }); - - await validateValueForProperty({ - page, - propertyName, - value: 'https://openmetadata.io,OpenMetadata Docs', - propertyType: 'hyperlink-cp', - }); - - const hyperlinkElement = page - .locator(`[data-testid="custom-property-${propertyName}-card"]`) - .getByTestId('hyperlink-value'); - - await expect(hyperlinkElement).toHaveAttribute( - 'href', - 'https://openmetadata.io' - ); - await expect(hyperlinkElement).toHaveAttribute('target', '_blank'); - await expect(hyperlinkElement).toHaveAttribute( - 'rel', - 'noopener noreferrer' - ); - }); - - test('should display URL when no display text is provided', async ({ - page, - }) => { - test.slow(); - const propertyName = - mainEntity.customPropertyValue[CustomPropertyTypeByName.HYPERLINK_CP] - .property.name; - const propertyValue = - mainEntity.customPropertyValue[CustomPropertyTypeByName.HYPERLINK_CP] - .value; - - await EntityDataClass.container1.visitEntityPage(page); - - await setValueForProperty({ - page, - propertyName, - value: propertyValue, - propertyType: 'hyperlink-cp', - endpoint: EntityTypeEndpoint.Container, - }); - - await validateValueForProperty({ - page, - propertyName, - value: propertyValue, - propertyType: 'hyperlink-cp', - }); - }); - } - - // ── Dashboard-specific extra tests ───────────────────────────────────── - - if (key === 'entity_dashboard') { - test.describe('Dashboard CP Advanced Search - Text Fields', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('String CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['string']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.string - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - CP_BASE_VALUES.string - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.string - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - CP_BASE_VALUES.string - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - CP_PARTIAL_SEARCH_VALUES.string - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - CP_PARTIAL_SEARCH_VALUES.string - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_like', - CP_BASE_VALUES.string - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - CP_BASE_VALUES.string - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('String CP with numeric-like string value', async ({ - browser, - page, - }) => { - test.slow(); - const numericStringDashboard = new DashboardClass(); - - await test.step('Setup dashboard with numeric-like string value', async () => { - const { apiContext, afterAction } = await createNewPage(browser); - - await numericStringDashboard.create(apiContext); - - await apiContext.patch( - `/api/v1/dashboards/${numericStringDashboard.entityResponseData.id}`, - { - data: [ - { - op: 'add', - path: '/extension', - value: { [propertyNames['string']]: '100' }, - }, - ], - headers: { - 'Content-Type': 'application/json-patch+json', - }, - } - ); - - await afterAction(); - }); - - await test.step('Equal operator finds dashboard with string value "100"', async () => { - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyNames['string'], - 'equal', - '100' - ); - await verifySearchResults( - page, - numericStringDashboard.entityResponseData.fullyQualifiedName, - true, - '100' - ); - await clearAdvancedSearchFilters(page); - }); - - await test.step('Not_equal operator excludes dashboard with string value "100"', async () => { - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyNames['string'], - 'not_equal', - '100' - ); - await verifySearchResults( - page, - numericStringDashboard.entityResponseData.fullyQualifiedName, - false, - '100' - ); - await clearAdvancedSearchFilters(page); - }); - - await test.step('Contains operator finds dashboard with partial numeric-like string "10"', async () => { - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyNames['string'], - 'like', - '10' - ); - await verifySearchResults( - page, - numericStringDashboard.entityResponseData.fullyQualifiedName, - true, - '10' - ); - await clearAdvancedSearchFilters(page); - }); - - await test.step('Not contains operator excludes dashboard with partial numeric-like string "10"', async () => { - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyNames['string'], - 'not_like', - '10' - ); - await verifySearchResults( - page, - numericStringDashboard.entityResponseData.fullyQualifiedName, - false, - '10' - ); - await clearAdvancedSearchFilters(page); - }); - - await test.step('Is not null operator finds dashboard with numeric-like string value', async () => { - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyNames['string'], - 'is_not_null', - '' - ); - await verifySearchResults( - page, - numericStringDashboard.entityResponseData.fullyQualifiedName, - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test('Email CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['email']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.email - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - CP_BASE_VALUES.email - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.email - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - CP_PARTIAL_SEARCH_VALUES.email - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Markdown CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['markdown']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - CP_PARTIAL_SEARCH_VALUES.markdown - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_like', - CP_BASE_VALUES.markdown - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('SQL Query CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['sqlQuery']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - CP_PARTIAL_SEARCH_VALUES.sqlQuery - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Duration CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['duration']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.duration - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - CP_PARTIAL_SEARCH_VALUES.duration - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Time CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['time-cp']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.timeCp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - CP_PARTIAL_SEARCH_VALUES.timeCp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test.describe('Dashboard CP Advanced Search - Number Fields', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('Integer CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['integer']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.integer - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.integer - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'between', - CP_RANGE_VALUES.integer - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'not_between', { - start: CP_BASE_VALUES.integer - 2, - end: CP_BASE_VALUES.integer + 4, - }); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Number CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['number']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.number - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.number - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'between', - CP_RANGE_VALUES.number - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Timestamp CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['timestamp']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.timestamp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.timestamp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test.describe('Dashboard CP Advanced Search - Entity References', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('Entity Reference CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['entityReference']; - const containsText = - dashboardTopic1.entityResponseData.displayName?.substring(1, 5); - const regexpText = `${dashboardTopic1.entityResponseData.displayName?.substring( - 0, - 2 - )}.*${dashboardTopic1.entityResponseData.displayName?.substring( - 5, - 7 - )}.*`; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'select_equals', - dashboardTopic1.entityResponseData.displayName ?? '', - 'Dashboard', - 'entityReference' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'select_not_equals', - dashboardTopic1.entityResponseData.displayName ?? '', - 'Dashboard', - 'entityReference' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - containsText ?? '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - containsText - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_like', - containsText ?? '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - containsText - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'regexp', - regexpText - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - regexpText - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Entity Reference List CP with all operators', async ({ - page, - }) => { - test.slow(); - const propertyName = propertyNames['entityReferenceList']; - const containsText = - dashboardTopic1.entityResponseData.displayName?.substring(1, 5); - const regexpText = `${dashboardTopic1.entityResponseData.displayName?.substring( - 0, - 2 - )}.*${dashboardTopic1.entityResponseData.displayName?.substring( - 5, - 7 - )}.*`; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'select_equals', - dashboardTopic1.entityResponseData.displayName ?? '', - 'Dashboard', - 'entityReferenceList' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'select_equals', - dashboardTopic2.entityResponseData.displayName ?? '', - 'Dashboard', - 'entityReferenceList' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'select_not_equals', - dashboardTopic2.entityResponseData.displayName ?? '', - 'Dashboard', - 'entityReferenceList' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'like', - containsText ?? '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - containsText - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_like', - containsText ?? '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - containsText - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'regexp', - regexpText - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - regexpText - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test.describe('Dashboard CP Advanced Search - Date/Time Fields', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('DateTime CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['dateTime-cp']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.dateTimeCp, - undefined, - 'dateTime-cp' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.dateTimeCp, - undefined, - 'dateTime-cp' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'between', - CP_RANGE_VALUES.dateTimeCp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_between', - CP_RANGE_VALUES.dateTimeCp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Date CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['date-cp']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'equal', - CP_BASE_VALUES.dateCp, - undefined, - 'date-cp' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_equal', - CP_BASE_VALUES.dateCp, - undefined, - 'date-cp' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'between', - CP_RANGE_VALUES.dateCp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'not_between', - CP_RANGE_VALUES.dateCp - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test.describe('Dashboard CP Advanced Search - Enum Fields', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('Enum CP with all operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['enum']; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'multiselect_equals', - CP_BASE_VALUES.enum[0] - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'multiselect_contains', - CP_BASE_VALUES.enum[0] - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'multiselect_not_equals', - CP_BASE_VALUES.enum[0] - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'multiselect_not_contains', - CP_BASE_VALUES.enum[0] - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - propertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test.describe('Dashboard CP Advanced Search - Special Types', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('Time Interval CP with operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['timeInterval']; - const startPropertyName = `${propertyName} (Start)`; - const endPropertyName = `${propertyName} (End)`; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - startPropertyName, - 'equal', - CP_BASE_VALUES.timeInterval.start - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - String(CP_BASE_VALUES.timeInterval.start) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - startPropertyName, - 'not_equal', - CP_BASE_VALUES.timeInterval.start - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - String(CP_BASE_VALUES.timeInterval.start) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, startPropertyName, 'between', { - start: CP_BASE_VALUES.timeInterval.start - 2, - end: CP_BASE_VALUES.timeInterval.start + 4, - }); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - startPropertyName, - 'not_between', - { - start: CP_BASE_VALUES.timeInterval.start - 2, - end: CP_BASE_VALUES.timeInterval.start + 4, - } - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - startPropertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - startPropertyName, - 'is_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - endPropertyName, - 'equal', - CP_BASE_VALUES.timeInterval.end - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - String(CP_BASE_VALUES.timeInterval.end) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - endPropertyName, - 'not_equal', - CP_BASE_VALUES.timeInterval.end - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - String(CP_BASE_VALUES.timeInterval.end) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, endPropertyName, 'between', { - start: CP_BASE_VALUES.timeInterval.end - 2, - end: CP_BASE_VALUES.timeInterval.end + 4, - }); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - endPropertyName, - 'not_between', - { - start: CP_BASE_VALUES.timeInterval.end - 2, - end: CP_BASE_VALUES.timeInterval.end + 4, - } - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - endPropertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, endPropertyName, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - }); - - test('Hyperlink CP with operators', async ({ page }) => { - test.slow(); - const propertyName = propertyNames['hyperlink-cp']; - const urlProperty = `${propertyName} URL`; - const displayTextProperty = `${propertyName} Display Text`; - const urlPartialValue = CP_BASE_VALUES.hyperlinkCp.url.substring( - 3, - 9 - ); - const displayTextPartialValue = - CP_BASE_VALUES.hyperlinkCp.displayText.substring(2, 6); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - urlProperty, - 'equal', - CP_BASE_VALUES.hyperlinkCp.url - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - String(CP_BASE_VALUES.hyperlinkCp.url) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - urlProperty, - 'not_equal', - CP_BASE_VALUES.hyperlinkCp.url - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - String(CP_BASE_VALUES.hyperlinkCp.url) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - urlProperty, - 'like', - urlPartialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - urlPartialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - urlProperty, - 'not_like', - urlPartialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - urlPartialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, urlProperty, 'is_not_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter(page, urlProperty, 'is_null', ''); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - displayTextProperty, - 'equal', - CP_BASE_VALUES.hyperlinkCp.displayText - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - String(CP_BASE_VALUES.hyperlinkCp.displayText) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - displayTextProperty, - 'not_equal', - CP_BASE_VALUES.hyperlinkCp.displayText - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - String(CP_BASE_VALUES.hyperlinkCp.displayText) - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - displayTextProperty, - 'like', - displayTextPartialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - displayTextPartialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - displayTextProperty, - 'not_like', - displayTextPartialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - displayTextPartialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - displayTextProperty, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - displayTextProperty, - 'is_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test.describe('Dashboard CP Advanced Search - Table CP', () => { - test.beforeEach(async ({ page }) => { - await sidebarClick(page, SidebarItem.EXPLORE); - }); - - test('Table CP - Name column with all operators', async ({ page }) => { - test.slow(); - const value = CP_BASE_VALUES.tableCp.rows[0]['Name']; - const partialValue = value.substring(1, 4); - const basePropertyName = propertyNames['table-cp']; - const columnPropertyName = `${basePropertyName} - Name`; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'equal', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - value - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'not_equal', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'like', - partialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - partialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'not_like', - partialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - partialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'is_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Table CP - Role column with all operators', async ({ page }) => { - test.slow(); - const value = CP_BASE_VALUES.tableCp.rows[0]['Role']; - const partialValue = value.substring(1, 4); - const basePropertyName = propertyNames['table-cp']; - const columnPropertyName = `${basePropertyName} - Role`; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'equal', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - value - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'not_equal', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'like', - partialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - partialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'not_like', - partialValue - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - partialValue - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'is_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - - test('Table CP - Sr No column with all operators', async ({ page }) => { - test.slow(); - const value = CP_BASE_VALUES.tableCp.rows[1]['Sr No']; - const basePropertyName = propertyNames['table-cp']; - const columnPropertyName = `${basePropertyName} - Sr No`; - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'equal', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - value - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'not_equal', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - value - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'like', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true, - value - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'not_like', - value - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false, - value - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'is_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - false - ); - await clearAdvancedSearchFilters(page); - - await showAdvancedSearchDialog(page); - await applyCustomPropertyFilter( - page, - columnPropertyName, - 'is_not_null', - '' - ); - await verifySearchResults( - page, - responseData.fullyQualifiedName ?? '', - true - ); - await clearAdvancedSearchFilters(page); - }); - }); - - test('Create custom property and configure search for Dashboard', async ({ - page, - }) => { - test.slow(true); - - await test.step('Create and assign custom property to Dashboard', async () => { - await settingClick(page, GlobalSettingOptions.DASHBOARDS, true); - await addCustomPropertiesForEntity({ - page, - propertyName: dashboardSearchPropertyName, - customPropertyData: CUSTOM_PROPERTIES_ENTITIES['entity_dashboard'], - customType: 'String', - }); - - await mainEntity.visitEntityPage(page); - - const customPropertyResponse = page.waitForResponse( - '/api/v1/metadata/types/name/dashboard?fields=customProperties' - ); - await page.getByTestId('custom_properties').click(); - await customPropertyResponse; - - await page.locator('.ant-skeleton-active').waitFor({ - state: 'detached', - }); - - await setValueForProperty({ - page, - propertyName: dashboardSearchPropertyName, - value: dashboardPropertyValue, - propertyType: 'string', - endpoint: EntityTypeEndpoint.Dashboard, - }); - - await page.reload(); - - const customPropertiesTab = page.getByTestId('custom_properties'); - await customPropertiesTab.click(); - await page.locator('.ant-skeleton-active').waitFor({ - state: 'detached', - }); - - await expect(page.getByText(dashboardPropertyValue)).toBeVisible(); - }); - - await test.step('Configure search settings for Dashboard custom property', async () => { - await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); - - const dashboardCard = page.getByTestId( - 'preferences.search-settings.dashboards' - ); - await dashboardCard.click(); - - await expect(page).toHaveURL( - /settings\/preferences\/search-settings\/dashboards$/ - ); - - await waitForAllLoadersToDisappear(page); - - await page.getByTestId('add-field-btn').click(); - - const customPropertyOption = page.getByText( - `extension.${dashboardSearchPropertyName}`, - { exact: true } - ); - await customPropertyOption.click(); - - const fieldPanel = page.getByTestId( - `field-configuration-panel-extension.${dashboardSearchPropertyName}` - ); - await expect(fieldPanel).toBeVisible(); - - const customPropertyBadge = fieldPanel.getByTestId( - 'custom-property-badge' - ); - await expect(customPropertyBadge).toBeVisible(); - - await fieldPanel.click(); - await setSliderValue(page, 'field-weight-slider', 20); - - const matchTypeSelect = page.getByTestId('match-type-select'); - await matchTypeSelect.click(); - await page - .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') - .waitFor({ state: 'visible' }); - await page - .locator('.ant-select-item-option[title="Standard Match"]') - .click(); - - const searchSettingsSaveResponse = page.waitForResponse( - '/api/v1/system/settings' - ); - - await page.getByTestId('save-btn').click(); - - await searchSettingsSaveResponse; - - await expect( - page.getByTestId( - `field-configuration-panel-extension.${dashboardSearchPropertyName}` - ) - ).toBeVisible(); - }); - - await test.step('Search for Dashboard using custom property value', async () => { - await redirectToHomePage(page); - - const searchInput = page.getByTestId('searchBox'); - await searchInput.click(); - await searchInput.fill(dashboardPropertyValue); - await searchInput.press('Enter'); - - await page.getByTestId('dashboards-tab').click(); - - await waitForAllLoadersToDisappear(page); - - const searchResults = page.getByTestId('search-results'); - const dashboardCard = searchResults.getByTestId( - `table-data-card_${responseData.fullyQualifiedName ?? ''}` - ); - await expect(dashboardCard).toBeVisible(); - }); - }); - - test('Verify Dashboard custom property persists in search settings', async ({ - page, - }) => { - await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); - - const dashboardCard = page.getByTestId( - 'preferences.search-settings.dashboards' - ); - await dashboardCard.click(); - - await waitForAllLoadersToDisappear(page); - - const customPropertyField = page.getByTestId( - `field-configuration-panel-extension.${dashboardSearchPropertyName}` - ); - await expect(customPropertyField).toBeVisible(); - }); - } - - // ── Pipeline-specific extra tests ────────────────────────────────────── - - if (key === 'entity_pipeline') { - test('Create custom property and configure search for Pipeline', async ({ - page, - }) => { - test.slow(true); - - await test.step('Create and assign custom property to Pipeline', async () => { - await settingClick(page, GlobalSettingOptions.PIPELINES, true); - await addCustomPropertiesForEntity({ - page, - propertyName: pipelineSearchPropertyName, - customPropertyData: CUSTOM_PROPERTIES_ENTITIES['entity_pipeline'], - customType: 'String', - }); - - await mainEntity.visitEntityPage(page); - - const customPropertyResponse = page.waitForResponse( - '/api/v1/metadata/types/name/pipeline?fields=customProperties' - ); - await page.getByTestId('custom_properties').click(); - await customPropertyResponse; - - await page.locator('.ant-skeleton-active').waitFor({ - state: 'detached', - }); - - await setValueForProperty({ - page, - propertyName: pipelineSearchPropertyName, - value: pipelinePropertyValue, - propertyType: 'string', - endpoint: EntityTypeEndpoint.Pipeline, - }); - }); - - await test.step('Configure search settings for Pipeline custom property', async () => { - await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); - - const pipelineCard = page.getByTestId( - 'preferences.search-settings.pipelines' - ); - await pipelineCard.click(); - - await expect(page).toHaveURL( - /settings\/preferences\/search-settings\/pipelines$/ - ); - - await waitForAllLoadersToDisappear(page); - - await page.getByTestId('add-field-btn').click(); - - const customPropertyOption = page.getByText( - `extension.${pipelineSearchPropertyName}`, - { exact: true } - ); - await customPropertyOption.click(); - - const fieldPanel = page.getByTestId( - `field-configuration-panel-extension.${pipelineSearchPropertyName}` - ); - await expect(fieldPanel).toBeVisible(); - - const customPropertyBadge = fieldPanel.getByTestId( - 'custom-property-badge' - ); - await expect(customPropertyBadge).toBeVisible(); - - await fieldPanel.click(); - await setSliderValue(page, 'field-weight-slider', 12); - - const matchTypeSelect = page.getByTestId('match-type-select'); - await matchTypeSelect.click(); - await page - .locator('.ant-select-item-option[title="Phrase Match"]') - .click(); - - const searchSettingsSaveResponse = page.waitForResponse( - '/api/v1/system/settings' - ); - - await page.getByTestId('save-btn').click(); - await searchSettingsSaveResponse; - }); - - await test.step('Search for Pipeline using custom property value', async () => { - await redirectToHomePage(page); - - const searchInput = page.getByTestId('searchBox'); - await searchInput.click(); - await searchInput.clear(); - await searchInput.fill(pipelinePropertyValue); - await searchInput.press('Enter'); - - await page.getByTestId('pipelines-tab').click(); - - await waitForAllLoadersToDisappear(page); - - const searchResults = page.getByTestId('search-results'); - const pipelineCard = searchResults.getByTestId( - `table-data-card_${responseData.fullyQualifiedName ?? ''}` - ); - await expect(pipelineCard).toBeVisible(); - }); - }); - - test('Verify Pipeline custom property persists in search settings', async ({ - page, - }) => { - await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); - - const pipelineCard = page.getByTestId( - 'preferences.search-settings.pipelines' - ); - await pipelineCard.click(); - - await waitForAllLoadersToDisappear(page); - - const customPropertyField = page.getByTestId( - `field-configuration-panel-extension.${pipelineSearchPropertyName}` - ); - await expect(customPropertyField).toBeVisible(); - }); - } - - // ── TableColumn-specific extra test ──────────────────────────────────── - - if (key === 'entity_tableColumn') { - test.describe('Set & update column-level custom property', async () => { - const testData: ColumnsTestData = {} as ColumnsTestData; - - test.beforeAll(async ({ browser }) => { - const { apiContext, afterAction } = await createNewPage(browser); - - const data = await createCustomPropertyForEntity( - apiContext, - EntityTypeEndpoint.TableColumn - ); - testData.customPropertyValue = data.customProperties; - testData.cleanupUser = data.cleanupUser; - testData.users = data.userNames; - - testData.columnFqn = - tableForColumnTest?.entityResponseData.columns[0] - .fullyQualifiedName ?? ''; - testData.tableFqn = - tableForColumnTest?.entityResponseData.fullyQualifiedName ?? ''; - - await afterAction(); - }); - - test.afterAll(async ({ browser }) => { - const { apiContext, afterAction } = await createNewPage(browser); - - await testData.cleanupUser?.(apiContext); - await afterAction(); - }); - - for (const type of Object.values(CustomPropertyTypeByName)) { - test(`Set ${type} custom property on column and verify in UI`, async ({ - page, - }) => { - await verifyTableColumnCustomPropertyPersistence({ - page, - columnFqn: testData.columnFqn, - tableFqn: testData.tableFqn, - propertyName: testData.customPropertyValue[type].property.name, - propertyType: type, - users: testData.users, - }); - }); - } - }); - } - - // ── Explore right-panel CP tab tests ─────────────────────────────────── - - if (makeInstance !== null) { - test(`Should display custom properties for ${entity.name} in right panel`, async ({ - page, - }) => { - const rightPanel = new RightPanelPageObject(page); - const customProperties = new CustomPropertiesPageObject(rightPanel); - rightPanel.setEntityConfig(mainEntity); - // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type - test.skip( - !rightPanel.isTabAvailable('custom property'), - `Custom Property tab not available for ${entity.name}` - ); - const fqn = getEntityFqn(mainEntity); - await navigateToExploreAndSelectEntity({ - page, - entityName: getEntityDisplayName(responseData), - endpoint: mainEntity.endpoint, - fullyQualifiedName: fqn, - exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], - }); - await rightPanel.waitForPanelVisible(); - await customProperties.navigateToCustomPropertiesTab(); - await customProperties.shouldShowCustomPropertiesContainer(); - const propertyName = Object.values(mainEntity.customPropertyValue)[0] - ?.property?.name; - if (propertyName) { - await customProperties.shouldShowCustomProperty(propertyName); - } - }); - - test(`Should search custom properties for ${entity.name} in right panel`, async ({ - page, - }) => { - const rightPanel = new RightPanelPageObject(page); - const customProperties = new CustomPropertiesPageObject(rightPanel); - rightPanel.setEntityConfig(mainEntity); - // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type - test.skip( - !rightPanel.isTabAvailable('custom property'), - `Custom Property tab not available for ${entity.name}` - ); - const fqn = getEntityFqn(mainEntity); - await navigateToExploreAndSelectEntity({ - page, - entityName: getEntityDisplayName(responseData), - endpoint: mainEntity.endpoint, - fullyQualifiedName: fqn, - exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], - }); - await rightPanel.waitForPanelVisible(); - await customProperties.navigateToCustomPropertiesTab(); - await customProperties.shouldShowCustomPropertiesContainer(); - const propertyName = Object.values(mainEntity.customPropertyValue)[0] - ?.property?.name; - if (propertyName) { - await customProperties.searchCustomProperties(propertyName); - await customProperties.shouldShowCustomProperty(propertyName); - } - }); - - test(`Should clear search and show all properties for ${entity.name} in right panel`, async ({ - page, - }) => { - const rightPanel = new RightPanelPageObject(page); - const customProperties = new CustomPropertiesPageObject(rightPanel); - rightPanel.setEntityConfig(mainEntity); - // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type - test.skip( - !rightPanel.isTabAvailable('custom property'), - `Custom Property tab not available for ${entity.name}` - ); - const fqn = getEntityFqn(mainEntity); - await navigateToExploreAndSelectEntity({ - page, - entityName: getEntityDisplayName(responseData), - endpoint: mainEntity.endpoint, - fullyQualifiedName: fqn, - exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], - }); - await rightPanel.waitForPanelVisible(); - await customProperties.navigateToCustomPropertiesTab(); - await customProperties.shouldShowCustomPropertiesContainer(); - const propertyName = Object.values(mainEntity.customPropertyValue)[0] - ?.property?.name; - if (propertyName) { - await customProperties.searchCustomProperties(propertyName); - await customProperties.shouldShowCustomProperty(propertyName); - await customProperties.clearSearch(); - await customProperties.shouldShowCustomPropertiesContainer(); - } - }); - - test(`Should verify property name is visible for ${entity.name} in right panel`, async ({ - page, - }) => { - const rightPanel = new RightPanelPageObject(page); - const customProperties = new CustomPropertiesPageObject(rightPanel); - rightPanel.setEntityConfig(mainEntity); - // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type - test.skip( - !rightPanel.isTabAvailable('custom property'), - `Custom Property tab not available for ${entity.name}` - ); - const fqn = getEntityFqn(mainEntity); - await navigateToExploreAndSelectEntity({ - page, - entityName: getEntityDisplayName(responseData), - endpoint: mainEntity.endpoint, - fullyQualifiedName: fqn, - exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], - }); - await rightPanel.waitForPanelVisible(); - await customProperties.navigateToCustomPropertiesTab(); - await customProperties.shouldShowCustomPropertiesContainer(); - const propertyName = Object.values(mainEntity.customPropertyValue)[0] - ?.property?.name; - if (propertyName) { - await customProperties.verifyPropertyType(propertyName); - } - }); - } - }); -}); - -test.describe('Custom property name validation', () => { - test.use({ storageState: 'playwright/.auth/admin.json' }); - - test.beforeEach(async ({ page }) => { - await redirectToHomePage(page); - await settingClick(page, GlobalSettingOptions.TABLES, true); - await page.click('[data-testid="add-field-button"]'); - }); - - const nameInput = '[data-testid="name"] input'; - const nameError = '#name_help'; - - test('should show error when name starts with a non-alphanumeric character', async ({ - page, - }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.STARTS_WITH_SPECIAL_CHAR - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a colon', async ({ page }) => { - await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_COLON); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a dollar sign', async ({ - page, - }) => { - await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_DOLLAR); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a caret', async ({ page }) => { - await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_CARET); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a double quote', async ({ - page, - }) => { - await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_QUOTE); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a backslash', async ({ page }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_BACKSLASH - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a less-than sign', async ({ - page, - }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_LESS_THAN - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a greater-than sign', async ({ - page, - }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_GREATER_THAN - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains an ampersand', async ({ - page, - }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_AMPERSAND - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains an asterisk', async ({ page }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_ASTERISK - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a forward slash', async ({ - page, - }) => { - await page.fill( - nameInput, - CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_FORWARD_SLASH - ); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should show error when name contains a tilde', async ({ page }) => { - await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_TILDE); - - await expect(page.locator(nameError)).toContainText( - CUSTOM_PROPERTY_NAME_VALIDATION_ERROR - ); - }); - - test('should accept a valid name starting with a letter', async ({ - page, - }) => { - await page.fill(nameInput, 'validName_123'); - - await expect(page.locator(nameError)).not.toBeVisible(); - }); - - test('should accept a valid name with allowed special characters', async ({ - page, - }) => { - await page.fill(nameInput, "valid Name.!@#%`()_-=+{}[]|;',.?"); - - await expect(page.locator(nameError)).not.toBeVisible(); - }); - - test('should show error when name exceeds 256 characters', async ({ - page, - }) => { - await page.fill( - nameInput, - `${INVALID_NAMES.MAX_LENGTH}${INVALID_NAMES.MAX_LENGTH}` - ); - - await expect(page.locator(nameError)).toContainText( - CP_NAME_MAX_LENGTH_VALIDATION_ERROR - ); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomThemeConfig.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomThemeConfig.spec.ts index a7f5af745a70..7d298c6037df 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomThemeConfig.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomThemeConfig.spec.ts @@ -203,6 +203,11 @@ test.describe('Custom Theme Config Page', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await page.locator('[data-testid="save-btn"]').click(); await saveResponse; + // This wait IS the test's coverage window: a duplicate monogram + // request that arrives between 500ms and 2000ms after save would not + // be counted under a shorter wait, so the duplicate-request assertion + // below could pass even when the regression it guards against is + // present (false negative). Keep at 2000ms. // eslint-disable-next-line playwright/no-wait-for-timeout -- wait to catch any additional monogram requests after save await page.waitForTimeout(2000); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DashboardAdvanceSearchCustomProperty.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DashboardAdvanceSearchCustomProperty.spec.ts new file mode 100644 index 000000000000..e100e8bdfc0c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DashboardAdvanceSearchCustomProperty.spec.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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. + */ + +/** + * PR-side custom property suite for Dashboard. Runs Dashboard's full describe + * block — BASIC/CONFIG CRUD plus the Dashboard-only advanced search describes + * and search-config tests, which test Dashboard-specific custom property + * behavior that isn't covered by the Table-only PR suite. + * + * Stress excludes Dashboard to avoid redundancy with this spec. + */ + +import { test } from '@playwright/test'; +import { + ALL_ENTITIES, + registerCustomPropertiesEntityTests, +} from '../../shared/customPropertiesEntityTests'; + +test.use({ storageState: 'playwright/.auth/admin.json' }); + +registerCustomPropertiesEntityTests( + ALL_ENTITIES.filter((e) => e.key === 'entity_dashboard') +); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts index 679dc912ec23..3e137c921bdb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts @@ -176,7 +176,7 @@ test.describe.serial( const { apiContext } = await getApiContext(page); // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for pipeline run to start before polling status - await page.waitForTimeout(2000); + await page.waitForTimeout(500); // Check data insight success status (assuming this is a custom function you need to implement) await expect diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts index d93ef785381f..cf4a15f24a11 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts @@ -36,6 +36,7 @@ import { selectDomain, } from '../../utils/domain'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { waitForSearchIndexed } from '../../utils/polling'; import { sidebarClick } from '../../utils/sidebar'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -271,8 +272,12 @@ test.describe('Data Product Comprehensive Tests', () => { } if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainDataProductsRightPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainDataProductsRightPanel.spec.ts index e447dd00d4e8..8e7868c6a03e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainDataProductsRightPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainDataProductsRightPanel.spec.ts @@ -13,7 +13,6 @@ import { DataProduct } from '../../support/domain/DataProduct'; import { Domain } from '../../support/domain/Domain'; -import { expect, test } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { ClassificationClass } from '../../support/tag/ClassificationClass'; @@ -22,6 +21,7 @@ import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; import { uuid } from '../../utils/common'; import { navigateToDomainDataProductsAndOpenPanel } from '../../utils/rightPanelNavigation'; +import { expect, test } from '../fixtures/pages'; import { OverviewPageObject } from '../PageObject/Explore/OverviewPageObject'; import { RightPanelPageObject } from '../PageObject/Explore/RightPanelPageObject'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts index 8aa68e01dea8..d5a20867d570 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts @@ -27,6 +27,7 @@ import { selectDomain, } from '../../utils/domain'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { waitForSearchIndexed } from '../../utils/polling'; import { sidebarClick } from '../../utils/sidebar'; const test = base.extend<{ @@ -93,8 +94,12 @@ test.describe('Domain Owner Management', () => { } if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } @@ -244,8 +249,12 @@ test.describe('Domain Expert Management', () => { // Wait before retry (ES indexing delay) if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } @@ -431,8 +440,12 @@ test.describe('Data Product UI Operations', () => { } if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts index a88b5eaea3a2..68170c8c786c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts @@ -324,7 +324,14 @@ test.describe('Domains', () => { }); test('Create DataProducts and add remove assets', async ({ page }) => { - test.slow(true); + // The 7-step body runs in ~90s on a clean main run (verified against + // prior all-pass run 28041706073). Cap at 3 min via explicit setTimeout + // — ~2x headroom for CI variance, and firm enough to kill hung runs + // fast. Without the cap, stacking `test.slow(true)` at this scope on + // top of the describe-level slow at line 116 produced 27-min runaway + // hangs (browser would close, the test would keep waiting). Explicit + // setTimeout is enforced firmly by Playwright and overrides slow(). + test.setTimeout(3 * 60 * 1000); const { afterAction, apiContext } = await getApiContext(page); const { assets, assetCleanup } = await setupAssetsForDomain(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts index aa790c8d9d6a..27daf87fb940 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts @@ -260,14 +260,21 @@ test.describe( await test.step('Selecting a database service type narrows the browse tree directionally', async () => { await expandTreeNode(page, 'Databases'); + // Explicit visibility wait before clicking. The expandTreeNode helper + // only waits for loaders to disappear, but the tree's child rows can + // continue to animate/reposition for a beat after that — the + // subsequent .click() then times out with "waiting for element to be + // visible, enabled and stable". toBeVisible polls until the element + // is stable too, which lets the click land cleanly. + const serviceTitle = page.getByTestId( + `explore-tree-title-${table.service.serviceType.toLowerCase()}` + ); + await expect(serviceTitle).toBeVisible(); + const browseRes = page.waitForResponse( '/api/v1/search/query?*index=dataAsset*' ); - await page - .getByTestId( - `explore-tree-title-${table.service.serviceType.toLowerCase()}` - ) - .click(); + await serviceTitle.click(); await browseRes; await waitForAllLoadersToDisappear(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts index 9359e951abd8..43938e6ffd60 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts @@ -24,7 +24,6 @@ import { PipelineClass } from '../../support/entity/PipelineClass'; import { SearchIndexClass } from '../../support/entity/SearchIndexClass'; import { TableClass } from '../../support/entity/TableClass'; import { TopicClass } from '../../support/entity/TopicClass'; -import { expect, test as baseTest } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { ClassificationClass } from '../../support/tag/ClassificationClass'; @@ -41,6 +40,7 @@ import { import { getEntityFqn } from '../../utils/entityPanel'; import { navigateToExploreAndSelectEntity } from '../../utils/explore'; import { connectEdgeBetweenNodesViaAPI } from '../../utils/lineage'; +import { expect, test as baseTest } from '../fixtures/pages'; import { CustomPropertiesPageObject } from '../PageObject/Explore/CustomPropertiesPageObject'; import { DataQualityPageObject } from '../PageObject/Explore/DataQualityPageObject'; import { LineagePageObject } from '../PageObject/Explore/LineagePageObject'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts index 870bc35ce109..de6770863189 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts @@ -12,7 +12,6 @@ */ import { KnowledgeCenterClass } from '../../support/entity/KnowledgeCenterClass'; -import { expect, test as baseTest } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { ClassificationClass } from '../../support/tag/ClassificationClass'; @@ -25,6 +24,7 @@ import { waitForAllLoadersToDisappear, } from '../../utils/entity'; import { performUserLogin } from '../../utils/user'; +import { expect, test as baseTest } from '../fixtures/pages'; import { OverviewPageObject } from '../PageObject/Explore/OverviewPageObject'; import { RightPanelPageObject, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryTermRightPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryTermRightPanel.spec.ts index 1b90f701ced9..5ed23ec000a0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryTermRightPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryTermRightPanel.spec.ts @@ -13,7 +13,6 @@ import { Domain } from '../../support/domain/Domain'; import { TableClass } from '../../support/entity/TableClass'; -import { expect, test } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { ClassificationClass } from '../../support/tag/ClassificationClass'; @@ -23,6 +22,7 @@ import { performAdminLogin } from '../../utils/admin'; import { uuid } from '../../utils/common'; import { getEntityFqn } from '../../utils/entityPanel'; import { navigateToGlossaryTermAssetsAndOpenPanel } from '../../utils/rightPanelNavigation'; +import { expect, test } from '../fixtures/pages'; import { OverviewPageObject } from '../PageObject/Explore/OverviewPageObject'; import { RightPanelPageObject } from '../PageObject/Explore/RightPanelPageObject'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/DataAssetLineage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/TableLineage.spec.ts similarity index 69% rename from openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/DataAssetLineage.spec.ts rename to openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/TableLineage.spec.ts index 551f51750143..1779362e416d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/DataAssetLineage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/TableLineage.spec.ts @@ -10,8 +10,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * PR-side data asset lineage suite. The parameterized "verify create lineage + * for entity - X" loop runs for Table only (other entities run in + * stress/Lineage/DataAssetLineageAllEntities.spec.ts — identical UI flow, + * redundant per-PR). The non-parameterized Column Level Lineage / Temp / + * Settings describes stay here since they provide unique coverage. + */ + import { expect } from '@playwright/test'; -import { get, startCase } from 'lodash'; +import { get } from 'lodash'; +import { registerDataAssetLineageEntityTests } from '../../../shared/dataAssetLineageEntityTests'; import { ApiEndpointClass } from '../../../support/entity/ApiEndpointClass'; import { ContainerClass } from '../../../support/entity/ContainerClass'; import { DashboardClass } from '../../../support/entity/DashboardClass'; @@ -38,46 +48,18 @@ import { activateColumnLayer, addColumnLineage, addPipelineBetweenNodes, - applyPipelineFromModal, - clickLineageNode, - connectEdgeBetweenNodes, - deleteEdge, deleteNode, - editLineage, editLineageClick, getEntityColumns, - performZoomOut, - rearrangeNodes, removeColumnLineage, toggleLineageFilters, verifyColumnLineageInCSV, - verifyExportLineageCSV, - verifyExportLineagePNG, - verifyNodePresent, verifyPlatformLineageForEntity, visitLineageTab, } from '../../../utils/lineage'; import { test } from '../../fixtures/pages'; // Contains list of entity supported -const allEntities = { - table: TableClass, - container: ContainerClass, - topic: TopicClass, - dashboard: DashboardClass, - mlmodel: MlModelClass, - pipeline: PipelineClass, - storedProcedure: StoredProcedureClass, - searchIndex: SearchIndexClass, - dataModel: DashboardDataModelClass, - apiEndpoint: ApiEndpointClass, - metric: MetricClass, - directory: DirectoryClass, - file: FileClass, - spreadsheet: SpreadsheetClass, - worksheet: WorksheetClass, -}; - const columnLevelEntities = { table: TableClass, container: ContainerClass, @@ -106,157 +88,7 @@ type EntityClassUnion = | SpreadsheetClass | WorksheetClass; -test.describe('Data asset lineage', () => { - const pipeline = new PipelineClass(); - const entities: EntityClassUnion[] = []; - - test.beforeAll( - 'setup lineage creation with other entity creation', - async ({ browser }) => { - const { apiContext, afterAction } = await getDefaultAdminAPIContext( - browser - ); - - Object.values(allEntities).forEach((EntityClass) => { - const lineageEntity = new EntityClass(); - - entities.push(lineageEntity); - }); - - await pipeline.create(apiContext); - await Promise.all(entities.map((entity) => entity.create(apiContext))); - - await afterAction(); - } - ); - - test.beforeEach(async ({ page }) => { - await redirectToHomePage(page); - }); - - Object.entries(allEntities).forEach(([key, EntityClass]) => { - const lineageEntity = new EntityClass(); - - test(`verify create lineage for entity - ${startCase(key)}`, async ({ - page, - }) => { - // 5 minute timeout - test.setTimeout(5 * 60 * 1000); - - await test.step('prepare entity', async () => { - const { apiContext } = await getApiContext(page); - - await lineageEntity.create(apiContext); - await lineageEntity.visitEntityPage(page); - await visitLineageTab(page); - await editLineageClick(page); - }); - - await test.step('should create lineage with normal edge', async () => { - for (const entity of entities) { - await connectEdgeBetweenNodes(page, lineageEntity, entity); - await rearrangeNodes(page); - await performZoomOut(page); - } - - const lineageRes = page.waitForResponse('/api/v1/lineage/getLineage?*'); - await page.reload(); - await lineageRes; - await page.getByTestId('edit-lineage').waitFor({ - state: 'visible', - }); - - await waitForAllLoadersToDisappear(page); - await page - .getByTestId( - `lineage-node-${lineageEntity.entityResponseData.fullyQualifiedName}` - ) - .waitFor(); - await rearrangeNodes(page); - await performZoomOut(page); - - for (const entity of entities) { - await verifyNodePresent(page, entity); - } - - // Check the Entity Drawer - await performZoomOut(page); - - for (const entity of entities) { - const toNodeFqn = get( - entity, - 'entityResponseData.fullyQualifiedName', - '' - ); - const entityName = get( - entity, - 'entityResponseData.displayName', - get(entity, 'entityResponseData.name', '') - ); - - await clickLineageNode(page, toNodeFqn); - - await expect( - page - .locator('.lineage-entity-panel') - .getByTestId('entity-header-title') - ).toHaveText(entityName); - - await page.getByTestId('drawer-close-icon').click(); - - // Panel should not be visible after closing it - await expect(page.locator('.lineage-entity-panel')).not.toBeVisible(); - } - }); - - await test.step('should create lineage with edge having pipeline', async () => { - await editLineage(page); - - await page.getByTestId('fit-screen').click(); - await page.getByRole('menuitem', { name: 'Fit to screen' }).click(); - await performZoomOut(page, 8); - await waitForAllLoadersToDisappear(page); - - const fromNodeFqn = get( - lineageEntity, - 'entityResponseData.fullyQualifiedName', - '' - ); - - await clickLineageNode(page, fromNodeFqn); - - for (const entity of entities) { - await applyPipelineFromModal(page, lineageEntity, entity, pipeline); - } - }); - - await test.step('Verify Lineage Export CSV', async () => { - await editLineageClick(page); - await waitForAllLoadersToDisappear(page); - await performZoomOut(page); - await verifyExportLineageCSV(page, lineageEntity, entities, pipeline); - }); - - await test.step('Verify Lineage Export PNG', async () => { - await verifyExportLineagePNG(page); - }); - - await test.step('Remove lineage between nodes for the entity', async () => { - await editLineage(page); - await page.getByTestId('fit-screen').click(); - await page.getByRole('menuitem', { name: 'Fit to screen' }).click(); - await waitForAllLoadersToDisappear(page); - - await performZoomOut(page); - - for (const entity of entities) { - await deleteEdge(page, lineageEntity, entity); - } - }); - }); - }); -}); - +registerDataAssetLineageEntityTests({ table: TableClass }); test.describe('Column Level Lineage', () => { const entities: Map = new Map(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts index 36c2cdd8e7dc..cdc09c429258 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts @@ -32,6 +32,11 @@ const invalidPassword = 'testUsers@123'; test.describe.configure({ // 5 minutes max for refresh token tests timeout: 5 * 60 * 1000, + // Disable retries for this file. Two tests in it sleep on intentional + // JWT-expiry waits (3 min and ~2 min); a single retry under the global + // retries:1 would waste ~5 min reliving those sleeps before reporting. + // Token-expiry flakes are almost always real, not transient. + retries: 0, }); test.describe('Login flow should work properly', () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TableCustomProperty.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TableCustomProperty.spec.ts new file mode 100644 index 000000000000..1b8aeff86670 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TableCustomProperty.spec.ts @@ -0,0 +1,216 @@ +/* + * Copyright 2024 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. + */ + +/** + * PR-side custom property suite. Runs the per-entity CRUD describe for Table + * only (other-entity CRUD is identical UI plumbing and runs in + * stress/CustomPropertiesAllEntities.spec.ts instead). Also keeps the + * entity-independent name-validation describe on PR — small (~5 min) and + * worth gating PR on. + */ + +import { expect, test } from '@playwright/test'; +import { + CP_NAME_MAX_LENGTH_VALIDATION_ERROR, + INVALID_NAMES, +} from '../../constant/common'; +import { + CUSTOM_PROPERTY_INVALID_NAMES, + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR, +} from '../../constant/customProperty'; +import { GlobalSettingOptions } from '../../constant/settings'; +import { + ALL_ENTITIES, + registerCustomPropertiesEntityTests, +} from '../../shared/customPropertiesEntityTests'; +import { redirectToHomePage } from '../../utils/common'; +import { settingClick } from '../../utils/sidebar'; + +test.use({ storageState: 'playwright/.auth/admin.json' }); + +registerCustomPropertiesEntityTests( + ALL_ENTITIES.filter((e) => e.key === 'entity_table') +); + +test.describe('Custom property name validation', () => { + test.use({ storageState: 'playwright/.auth/admin.json' }); + + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + await settingClick(page, GlobalSettingOptions.TABLES, true); + await page.click('[data-testid="add-field-button"]'); + }); + + const nameInput = '[data-testid="name"] input'; + const nameError = '#name_help'; + + test('should show error when name starts with a non-alphanumeric character', async ({ + page, + }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.STARTS_WITH_SPECIAL_CHAR + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a colon', async ({ page }) => { + await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_COLON); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a dollar sign', async ({ + page, + }) => { + await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_DOLLAR); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a caret', async ({ page }) => { + await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_CARET); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a double quote', async ({ + page, + }) => { + await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_QUOTE); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a backslash', async ({ page }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_BACKSLASH + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a less-than sign', async ({ + page, + }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_LESS_THAN + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a greater-than sign', async ({ + page, + }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_GREATER_THAN + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains an ampersand', async ({ + page, + }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_AMPERSAND + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains an asterisk', async ({ page }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_ASTERISK + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a forward slash', async ({ + page, + }) => { + await page.fill( + nameInput, + CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_FORWARD_SLASH + ); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should show error when name contains a tilde', async ({ page }) => { + await page.fill(nameInput, CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_TILDE); + + await expect(page.locator(nameError)).toContainText( + CUSTOM_PROPERTY_NAME_VALIDATION_ERROR + ); + }); + + test('should accept a valid name starting with a letter', async ({ + page, + }) => { + await page.fill(nameInput, 'validName_123'); + + await expect(page.locator(nameError)).not.toBeVisible(); + }); + + test('should accept a valid name with allowed special characters', async ({ + page, + }) => { + await page.fill(nameInput, "valid Name.!@#%`()_-=+{}[]|;',.?"); + + await expect(page.locator(nameError)).not.toBeVisible(); + }); + + test('should show error when name exceeds 256 characters', async ({ + page, + }) => { + await page.fill( + nameInput, + `${INVALID_NAMES.MAX_LENGTH}${INVALID_NAMES.MAX_LENGTH}` + ); + + await expect(page.locator(nameError)).toContainText( + CP_NAME_MAX_LENGTH_VALIDATION_ERROR + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TagPageRightPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TagPageRightPanel.spec.ts index c5abcabd97af..9614ed6f9eba 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TagPageRightPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TagPageRightPanel.spec.ts @@ -13,7 +13,6 @@ import { Domain } from '../../support/domain/Domain'; import { TableClass } from '../../support/entity/TableClass'; -import { expect, test } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { ClassificationClass } from '../../support/tag/ClassificationClass'; @@ -23,6 +22,7 @@ import { performAdminLogin } from '../../utils/admin'; import { uuid } from '../../utils/common'; import { getEntityFqn } from '../../utils/entityPanel'; import { navigateToTagAssetsAndOpenPanel } from '../../utils/rightPanelNavigation'; +import { expect, test } from '../fixtures/pages'; import { OverviewPageObject } from '../PageObject/Explore/OverviewPageObject'; import { RightPanelPageObject } from '../PageObject/Explore/RightPanelPageObject'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts index 6a55a4d016de..44e6e1868f3c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts @@ -528,10 +528,6 @@ test.describe('Task Comments - UI Tests', () => { .click(); await taskFeeds; - // Wait for task cards to load - await page.waitForTimeout(1000); - - // Look for the task card - could be task-feed-card or feed-card-v2 const taskCard = page .locator('[data-testid="task-feed-card"], .task-feed-card-v1-new') .first(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts index 888db0953fdf..7bebf0a6e222 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts @@ -458,7 +458,6 @@ test.describe('Task Activity Feed Integration', () => { await test.step('Resolve task and verify it moves to Closed', async () => { await resolveTaskWithApproval(page); - await page.waitForTimeout(1000); await page.reload(); await navigateToActivityFeedTasks(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TeamAssetsRightPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TeamAssetsRightPanel.spec.ts index 35cee796fbab..00db4d863d11 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TeamAssetsRightPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TeamAssetsRightPanel.spec.ts @@ -13,7 +13,6 @@ import { Domain } from '../../support/domain/Domain'; import { TableClass } from '../../support/entity/TableClass'; -import { expect, test } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { ClassificationClass } from '../../support/tag/ClassificationClass'; @@ -24,6 +23,7 @@ import { performAdminLogin } from '../../utils/admin'; import { uuid } from '../../utils/common'; import { getEntityFqn } from '../../utils/entityPanel'; import { navigateToTeamAssetsAndOpenPanel } from '../../utils/rightPanelNavigation'; +import { expect, test } from '../fixtures/pages'; import { OverviewPageObject } from '../PageObject/Explore/OverviewPageObject'; import { RightPanelPageObject } from '../PageObject/Explore/RightPanelPageObject'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/fixtures/pages.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/fixtures/pages.ts index 07fea4623928..d8f15612aa2c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/fixtures/pages.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/fixtures/pages.ts @@ -15,6 +15,7 @@ import { Page, test as base } from '@playwright/test'; // Define the type for our custom fixtures export type CustomFixtures = { page: Page; + adminPage: Page; dataConsumerPage: Page; dataStewardPage: Page; editDescriptionPage: Page; @@ -35,6 +36,17 @@ export const test = base.extend({ await use(adminPage); await adminPage.close(); }, + // Explicit alias for the default admin page — kept so specs migrating + // from the deprecated support/fixtures/userPages fixture don't have to + // rename `adminPage` → `page` at every call site. + adminPage: async ({ browser }, use) => { + const page = await browser.newPage({ + storageState: 'playwright/.auth/admin.json', + }); + + await use(page); + await page.close(); + }, dataConsumerPage: async ({ browser }, use) => { const page = await browser.newPage({ storageState: 'playwright/.auth/dataConsumer.json', @@ -92,3 +104,5 @@ export const test = base.extend({ await page.close(); }, }); + +export { expect } from '@playwright/test'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/stress/CustomPropertiesAllEntities.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/stress/CustomPropertiesAllEntities.spec.ts new file mode 100644 index 000000000000..2b44341d9eb3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/stress/CustomPropertiesAllEntities.spec.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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. + */ + +/** + * Stress-side custom property suite. Runs the per-entity CRUD describe for + * every entity EXCEPT Table and Dashboard — those run on PR via + * TableCustomProperty.spec.ts and DashboardAdvanceSearchCustomProperty.spec.ts. + * + * Triggered manually via postgresql-nightly-e2e.yml (workflow_dispatch) under + * the standalone `stress` Playwright project. + */ + +import { test } from '@playwright/test'; +import { + ALL_ENTITIES, + registerCustomPropertiesEntityTests, +} from '../../shared/customPropertiesEntityTests'; + +test.use({ storageState: 'playwright/.auth/admin.json' }); + +registerCustomPropertiesEntityTests( + ALL_ENTITIES.filter( + (e) => e.key !== 'entity_table' && e.key !== 'entity_dashboard' + ) +); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/stress/Lineage/DataAssetLineageAllEntities.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/stress/Lineage/DataAssetLineageAllEntities.spec.ts new file mode 100644 index 000000000000..7f64096e21de --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/stress/Lineage/DataAssetLineageAllEntities.spec.ts @@ -0,0 +1,56 @@ +/* + * 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. + */ + +/** + * Stress-side data asset lineage suite. Runs the parameterized "verify create + * lineage for entity - X" loop for every entity EXCEPT Table — Table runs on + * PR via Lineage/TableLineage.spec.ts. The non-parameterized describes + * (Column Level Lineage / Temp / Settings) live in TableLineage.spec.ts and + * are not duplicated here. + * + * Triggered manually via postgresql-nightly-e2e.yml (workflow_dispatch) under + * the standalone `stress` Playwright project. + */ + +import { registerDataAssetLineageEntityTests } from '../../../shared/dataAssetLineageEntityTests'; +import { ApiEndpointClass } from '../../../support/entity/ApiEndpointClass'; +import { ContainerClass } from '../../../support/entity/ContainerClass'; +import { DashboardClass } from '../../../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../../../support/entity/DashboardDataModelClass'; +import { DirectoryClass } from '../../../support/entity/DirectoryClass'; +import { FileClass } from '../../../support/entity/FileClass'; +import { MetricClass } from '../../../support/entity/MetricClass'; +import { MlModelClass } from '../../../support/entity/MlModelClass'; +import { PipelineClass } from '../../../support/entity/PipelineClass'; +import { SearchIndexClass } from '../../../support/entity/SearchIndexClass'; +import { SpreadsheetClass } from '../../../support/entity/SpreadsheetClass'; +import { StoredProcedureClass } from '../../../support/entity/StoredProcedureClass'; +import { TopicClass } from '../../../support/entity/TopicClass'; +import { WorksheetClass } from '../../../support/entity/WorksheetClass'; + +registerDataAssetLineageEntityTests({ + container: ContainerClass, + topic: TopicClass, + dashboard: DashboardClass, + mlmodel: MlModelClass, + pipeline: PipelineClass, + storedProcedure: StoredProcedureClass, + searchIndex: SearchIndexClass, + dataModel: DashboardDataModelClass, + apiEndpoint: ApiEndpointClass, + metric: MetricClass, + directory: DirectoryClass, + file: FileClass, + spreadsheet: SpreadsheetClass, + worksheet: WorksheetClass, +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/shared/customPropertiesEntityTests.ts b/openmetadata-ui/src/main/resources/ui/playwright/shared/customPropertiesEntityTests.ts new file mode 100644 index 000000000000..0b0d38ea7d85 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/shared/customPropertiesEntityTests.ts @@ -0,0 +1,3735 @@ +/* + * Copyright 2024 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. + */ + +/** + * Helper that registers the per-entity custom property describe blocks for + * the entities passed in. Single source of truth for the per-entity test body. + * + * Used by: + * - e2e/Pages/TableCustomProperty.spec.ts (PR: [Table]) + * - e2e/Pages/DashboardAdvanceSearchCustomProperty.spec.ts (PR: [Dashboard]) + * - e2e/stress/CustomPropertiesAllEntities.spec.ts (Stress: 17 others) + * + * The Dashboard branch contains the 7 advanced-search describes + the 2 + * search-config tests — those fire only when Dashboard is in the entity list, + * so they run on PR via the Dashboard spec but NOT in stress (Dashboard is + * excluded there to avoid redundancy with PR). + */ + +/** + * Consolidated custom property tests for all entity types: + * Table, Container, Dashboard, Topic, Pipeline, + * Database, DatabaseSchema, GlossaryTerm, MlModel, SearchIndex, + * StoredProcedure, DashboardDataModel, Metric, Chart, + * ApiCollection, ApiEndpoint, DataProduct, Domain, TableColumn. + * + * Each entity type has ONE describe.serial block so no two workers can ever run + * CP create/edit/delete operations for the same entity type simultaneously. + * + * Entity setup (prepareCustomProperty) is done in beforeAll, not inside tests, + * so cleanup always runs in afterAll even when a test fails mid-way. + */ + +import { APIRequestContext, expect, test } from '@playwright/test'; +import { + CUSTOM_PROPERTIES_ENTITIES, + NAME_SUFFIX, +} from '../constant/customProperty'; +import { + CP_BASE_VALUES, + CP_PARTIAL_SEARCH_VALUES, + CP_RANGE_VALUES, +} from '../constant/customPropertyAdvancedSearch'; +import { ENDPOINT_TO_EXPLORE_TAB_MAP } from '../constant/explore'; +import { GlobalSettingOptions } from '../constant/settings'; +import { SidebarItem } from '../constant/sidebar'; +import { CustomPropertiesPageObject } from '../e2e/PageObject/Explore/CustomPropertiesPageObject'; +import { RightPanelPageObject } from '../e2e/PageObject/Explore/RightPanelPageObject'; +import { DataProduct } from '../support/domain/DataProduct'; +import { Domain } from '../support/domain/Domain'; +import { ApiCollectionClass } from '../support/entity/ApiCollectionClass'; +import { ApiEndpointClass } from '../support/entity/ApiEndpointClass'; +import { ChartClass } from '../support/entity/ChartClass'; +import { ContainerClass } from '../support/entity/ContainerClass'; +import { DashboardClass } from '../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../support/entity/DashboardDataModelClass'; +import { DatabaseClass } from '../support/entity/DatabaseClass'; +import { DatabaseSchemaClass } from '../support/entity/DatabaseSchemaClass'; +import { EntityTypeEndpoint } from '../support/entity/Entity.interface'; +import { EntityDataClass } from '../support/entity/EntityDataClass'; +import { MetricClass } from '../support/entity/MetricClass'; +import { MlModelClass } from '../support/entity/MlModelClass'; +import { PipelineClass } from '../support/entity/PipelineClass'; +import { SearchIndexClass } from '../support/entity/SearchIndexClass'; +import { StoredProcedureClass } from '../support/entity/StoredProcedureClass'; +import { TableClass } from '../support/entity/TableClass'; +import { TopicClass } from '../support/entity/TopicClass'; +import { GlossaryTerm } from '../support/glossary/GlossaryTerm'; +import { UserClass } from '../support/user/UserClass'; +import { + CONDITIONS_MUST, + selectOption, + showAdvancedSearchDialog, +} from '../utils/advancedSearch'; +import { advanceSearchSaveFilter } from '../utils/advancedSearchCustomProperty'; +import { + clickOutside, + createNewPage, + getApiContext, + redirectToHomePage, + uuid, +} from '../utils/common'; +import { + addCustomPropertiesForEntity, + createCustomPropertyForEntity, + CustomProperty, + CustomPropertyTypeByName, + deleteCreatedProperty, + editCreatedProperty, + fillTableColumnInputDetails, + setValueForProperty, + updateCustomPropertyInRightPanel, + validateValueForProperty, + verifyCustomPropertyInAdvancedSearch, + verifyTableColumnCustomPropertyPersistence, +} from '../utils/customProperty'; +import { + applyCustomPropertyFilter, + clearAdvancedSearchFilters, + CPASTestData, + setupCustomPropertyAdvancedSearchTest, + verifySearchResults, +} from '../utils/customPropertyAdvancedSearchUtils'; +import { + getEntityDisplayName, + waitForAllLoadersToDisappear, +} from '../utils/entity'; +import { getEntityFqn } from '../utils/entityPanel'; +import { navigateToExploreAndSelectEntity } from '../utils/explore'; +import { setSliderValue } from '../utils/searchSettingUtils'; +import { + settingClick, + SettingOptionsType, + sidebarClick, +} from '../utils/sidebar'; + +type CustomPropertyEntity = + (typeof CUSTOM_PROPERTIES_ENTITIES)[keyof typeof CUSTOM_PROPERTIES_ENTITIES]; + +type AssetTypes = + | TableClass + | ContainerClass + | DashboardClass + | TopicClass + | PipelineClass + | DatabaseClass + | DatabaseSchemaClass + | MlModelClass + | SearchIndexClass + | StoredProcedureClass + | DashboardDataModelClass + | MetricClass + | ChartClass + | ApiCollectionClass + | ApiEndpointClass; + +type OtherTypes = GlossaryTerm | Domain | DataProduct; + +export type CRUDEntity = { + key: keyof typeof CUSTOM_PROPERTIES_ENTITIES; + makeInstance: (() => AssetTypes | OtherTypes) | null; +}; + +type ColumnsTestData = { + customPropertyValue: Record< + string, + { + value: string; + newValue: string; + property: CustomProperty; + } + >; + cleanupUser: (apiContext: APIRequestContext) => Promise; + users: Record; + columnFqn: string; + tableFqn: string; +}; + +const BASIC_PROPERTIES = [ + 'Integer', + 'String', + 'Markdown', + 'Duration', + 'Email', + 'Number', + 'Sql Query', + 'Time Interval', + 'Timestamp', + 'Hyperlink', +]; + +const CONFIG_PROPERTIES: Array<{ + name: string; + getConfig: (e: CustomPropertyEntity) => Record; + editPropertyType?: string; + verifyAdvancedSearch: boolean; + searchTableColumns?: boolean; +}> = [ + { + name: 'Enum', + getConfig: (e) => ({ enumConfig: e.enumConfig }), + editPropertyType: 'Enum', + verifyAdvancedSearch: true, + }, + { + name: 'Table', + getConfig: (e) => ({ tableConfig: e.tableConfig }), + editPropertyType: 'Table', + verifyAdvancedSearch: true, + searchTableColumns: true, + }, + { + name: 'Entity Reference', + getConfig: (e) => ({ entityReferenceConfig: e.entityReferenceConfig }), + editPropertyType: 'Entity Reference', + verifyAdvancedSearch: true, + }, + { + name: 'Entity Reference List', + getConfig: (e) => ({ entityReferenceConfig: e.entityReferenceConfig }), + editPropertyType: 'Entity Reference List', + verifyAdvancedSearch: true, + }, + { + name: 'Date', + getConfig: (e) => ({ formatConfig: e.dateFormatConfig }), + verifyAdvancedSearch: false, + }, + { + name: 'Time', + getConfig: (e) => ({ formatConfig: e.timeFormatConfig }), + verifyAdvancedSearch: true, + }, + { + name: 'Date Time', + getConfig: (e) => ({ formatConfig: e.dateTimeFormatConfig }), + verifyAdvancedSearch: true, + }, +]; + +export const ALL_ENTITIES: CRUDEntity[] = [ + // Part-1 entities + { key: 'entity_table', makeInstance: () => new TableClass() }, + { key: 'entity_container', makeInstance: () => new ContainerClass() }, + { key: 'entity_dashboard', makeInstance: () => new DashboardClass() }, + { key: 'entity_topic', makeInstance: () => new TopicClass() }, + { key: 'entity_pipeline', makeInstance: () => new PipelineClass() }, + // Part-2 entities + { key: 'entity_database', makeInstance: () => new DatabaseClass() }, + { + key: 'entity_databaseSchema', + makeInstance: () => new DatabaseSchemaClass(), + }, + { key: 'entity_glossaryTerm', makeInstance: () => new GlossaryTerm() }, + { key: 'entity_mlmodel', makeInstance: () => new MlModelClass() }, + { key: 'entity_searchIndex', makeInstance: () => new SearchIndexClass() }, + { + key: 'entity_storedProcedure', + makeInstance: () => new StoredProcedureClass(), + }, + { + key: 'entity_dashboardDataModel', + makeInstance: () => new DashboardDataModelClass(), + }, + { key: 'entity_metric', makeInstance: () => new MetricClass() }, + { key: 'entity_chart', makeInstance: () => new ChartClass() }, + // Part-3 entities + { key: 'entity_apiCollection', makeInstance: () => new ApiCollectionClass() }, + { key: 'entity_apiEndpoint', makeInstance: () => new ApiEndpointClass() }, + { key: 'entity_dataProduct', makeInstance: () => new DataProduct() }, + { key: 'entity_domain', makeInstance: null }, + { key: 'entity_tableColumn', makeInstance: null }, +]; + +export const registerCustomPropertiesEntityTests = (entities: CRUDEntity[]) => { + entities.forEach(({ key, makeInstance }) => { + const entity = CUSTOM_PROPERTIES_ENTITIES[key]; + + test.describe + .serial(`Add update and delete custom properties for ${entity.name}`, () => { + let mainEntity: AssetTypes | OtherTypes = {} as AssetTypes | OtherTypes; + let responseData: + | AssetTypes['entityResponseData'] + | OtherTypes['responseData']; + + let tableForColumnTest: TableClass | null = null; + const users: UserClass[] = []; + + // Dashboard-specific state + let dashboardTopic1: TopicClass; + let dashboardTopic2: TopicClass; + const cpasTestData: CPASTestData = { + types: [], + cpMetadataType: { name: '', id: '' }, + createdCPData: [], + }; + const propertyNames: Record = {}; + const dashboardSearchPropertyName = `cp-${uuid()}-${ + entity.name + }${NAME_SUFFIX}`; + const dashboardPropertyValue = `EXECUTIVE_DASHBOARD_${uuid()}`; + + // Pipeline-specific state + const pipelineSearchPropertyName = `cp-${uuid()}-${ + entity.name + }${NAME_SUFFIX}`; + const pipelinePropertyValue = `ETL_PRODUCTION_${uuid()}`; + + test.beforeAll(async ({ browser }) => { + const { page, apiContext, afterAction } = await createNewPage(browser); + + if (key === 'entity_tableColumn') { + tableForColumnTest = new TableClass(); + await tableForColumnTest.create(apiContext); + } else if (makeInstance !== null) { + mainEntity = makeInstance(); + await mainEntity.create(apiContext); + await mainEntity.prepareCustomProperty(apiContext); + + if (key === 'entity_table') { + for (let i = 0; i < 5; i++) { + const user = new UserClass(); + await user.create(apiContext); + users.push(user); + } + } else if (key === 'entity_dashboard') { + dashboardTopic1 = new TopicClass(); + dashboardTopic2 = new TopicClass(); + await dashboardTopic1.create(apiContext); + await dashboardTopic2.create(apiContext); + await setupCustomPropertyAdvancedSearchTest( + page, + cpasTestData, + mainEntity as DashboardClass, + dashboardTopic1, + dashboardTopic2 + ); + cpasTestData.createdCPData.forEach((cp) => { + propertyNames[cp.propertyType.name] = cp.name; + }); + } + } + responseData = + (mainEntity as AssetTypes).entityResponseData ?? + (mainEntity as OtherTypes).responseData; + + await afterAction(); + }); + + test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + if (makeInstance !== null) { + await mainEntity.delete(apiContext); + if (key === 'entity_dataProduct') { + for (const domain of (mainEntity as DataProduct).getDomains()) { + await domain.delete(apiContext); + } + } + } else if (tableForColumnTest !== null) { + await tableForColumnTest.delete(apiContext); + } + if (users.length) { + for (const user of users) { + await user.delete(apiContext); + } + } + if (dashboardTopic1) { + await dashboardTopic1.delete(apiContext); + } + if (dashboardTopic2) { + await dashboardTopic2.delete(apiContext); + } + + await afterAction(); + }); + + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + }); + + // ── 17 CRUD tests ────────────────────────────────────────────────────── + + BASIC_PROPERTIES.forEach((property) => { + test(property, async ({ page }) => { + test.slow(); + const propertyName = `cp-${uuid()}-${entity.name}${NAME_SUFFIX}`; + + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + await addCustomPropertiesForEntity({ + page, + propertyName, + customPropertyData: entity, + customType: property, + }); + + await editCreatedProperty(page, propertyName); + + await verifyCustomPropertyInAdvancedSearch( + page, + propertyName.toUpperCase(), + entity.name.charAt(0).toUpperCase() + entity.name.slice(1), + property + ); + + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + await deleteCreatedProperty(page, propertyName); + }); + }); + + CONFIG_PROPERTIES.forEach((propertyConfig) => { + test(propertyConfig.name, async ({ page }) => { + test.slow(); + const propertyName = `cp-${uuid()}-${entity.name}${NAME_SUFFIX}`; + + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + await addCustomPropertiesForEntity({ + page, + propertyName, + customPropertyData: entity, + customType: propertyConfig.name, + ...propertyConfig.getConfig(entity), + }); + + if (propertyConfig.editPropertyType) { + await editCreatedProperty( + page, + propertyName, + propertyConfig.editPropertyType + ); + } else { + await editCreatedProperty(page, propertyName); + } + + if (propertyConfig.verifyAdvancedSearch) { + if (propertyConfig.searchTableColumns) { + await verifyCustomPropertyInAdvancedSearch( + page, + propertyName.toUpperCase(), + entity.name.charAt(0).toUpperCase() + entity.name.slice(1), + propertyConfig.name, + entity.tableConfig.columns + ); + } else { + await verifyCustomPropertyInAdvancedSearch( + page, + propertyName.toUpperCase(), + entity.name.charAt(0).toUpperCase() + entity.name.slice(1) + ); + } + } + + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + await deleteCreatedProperty(page, propertyName); + }); + }); + + // ── Set & Update all CP types (entities with a UI entity page) ────────── + + if (makeInstance !== null) { + test(`Set & Update all CP types on ${entity.name}`, async ({ + page, + }) => { + // 5 minutes timeout since the test handles set->update operation on all + // custom property types sequentially + test.setTimeout(300000); + const properties = Object.values(CustomPropertyTypeByName); + + await test.step('Set all CP types', async () => { + await mainEntity.visitEntityPage(page); + for (const type of properties) { + await mainEntity.updateCustomProperty( + page, + mainEntity.customPropertyValue[type].property, + mainEntity.customPropertyValue[type].value + ); + } + }); + + await test.step('Update all CP types', async () => { + await mainEntity.visitEntityPage(page); + for (const type of properties) { + await mainEntity.updateCustomProperty( + page, + mainEntity.customPropertyValue[type].property, + mainEntity.customPropertyValue[type].newValue + ); + } + }); + + await test.step('Update all CP types in Right Panel', async () => { + for (const [index, type] of properties.entries()) { + await updateCustomPropertyInRightPanel({ + page, + entityName: getEntityDisplayName(responseData), + propertyDetails: mainEntity.customPropertyValue[type].property, + value: mainEntity.customPropertyValue[type].value, + endpoint: mainEntity.endpoint, + skipNavigation: index > 0, + exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], + entityFQN: responseData.fullyQualifiedName, + }); + } + }); + }); + } + + // ── Table-specific extra tests ────────────────────────────────────────── + + if (key === 'entity_table') { + test('sqlQuery shows scrollable CodeMirror container and no expand toggle', async ({ + page, + }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[CustomPropertyTypeByName.SQL_QUERY] + .property.name; + + await test.step('Set multi-line SQL value', async () => { + await mainEntity.visitEntityPage(page); + await waitForAllLoadersToDisappear(page); + await page.getByTestId('custom_properties').click(); + + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + const editButton = container.getByTestId('edit-icon'); + await editButton.scrollIntoViewIfNeeded(); + await expect(editButton).toBeVisible(); + await expect(editButton).toBeEnabled(); + await editButton.click(); + + await page.locator("pre[role='presentation']").last().click(); + const value = + "SELECT id, name, email\nFROM users\nWHERE active = true\nAND department = 'engineering'\nORDER BY created_at DESC\nLIMIT 100"; + await page.keyboard.type(value + '\n' + value); + + const patchResponse = page.waitForResponse( + `/api/v1/${entity.entityApiType}/*` + ); + await container.getByTestId('inline-save-btn').click(); + expect((await patchResponse).status()).toBe(200); + await waitForAllLoadersToDisappear(page); + }); + + await test.step('Verify .CodeMirror-scroll is height-constrained and scrollable', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + const codeMirrorScroll = container.locator('.CodeMirror-scroll'); + await expect(codeMirrorScroll).toBeVisible(); + const isScrollable = await codeMirrorScroll.evaluate( + (el) => el.scrollHeight > el.clientHeight + ); + expect(isScrollable).toBeTruthy(); + }); + + await test.step('Verify expand/collapse toggle is hidden', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + await expect( + container.getByTestId(`toggle-${propertyName}`) + ).not.toBeVisible(); + }); + }); + + test('entityReferenceList shows item count, scrollable list, no expand toggle', async ({ + page, + }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.ENTITY_REFERENCE_LIST + ].property.name; + + await test.step('Set 5 user references as value', async () => { + await redirectToHomePage(page); + await mainEntity.visitEntityPage(page); + await waitForAllLoadersToDisappear(page); + await page.getByTestId('custom_properties').click(); + + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + const editButton = container.getByTestId('edit-icon'); + await editButton.scrollIntoViewIfNeeded(); + await expect(editButton).toBeVisible(); + await expect(editButton).toBeEnabled(); + await editButton.click(); + + for (const user of users) { + const searchApi = `**/api/v1/search/query?q=*${encodeURIComponent( + user.getUserName() + )}*`; + const searchResponse = page.waitForResponse(searchApi); + await page.locator('#entityReference').clear(); + await page.locator('#entityReference').fill(user.getUserName()); + await searchResponse; + await page + .locator(`[data-testid="${user.getUserDisplayName()}"]`) + .click(); + } + await clickOutside(page); + const patchResponse = page.waitForResponse( + `/api/v1/${entity.entityApiType}/*` + ); + await container.getByTestId('inline-save-btn').click(); + expect((await patchResponse).status()).toBe(200); + await waitForAllLoadersToDisappear(page); + }); + + await test.step('Verify item count (7) in property name', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + await expect(container.getByTestId('property-name')).toContainText( + '(7)' + ); + }); + + await test.step('Verify .entity-list-body is scrollable', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + const listBody = container.locator('.entity-list-body'); + await expect(listBody).toBeVisible(); + const isScrollable = await listBody.evaluate( + (el) => el.scrollHeight > el.clientHeight + ); + expect(isScrollable).toBeTruthy(); + }); + + await test.step('Verify expand/collapse toggle is hidden', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + await expect( + container.getByTestId(`toggle-${propertyName}`) + ).not.toBeVisible(); + }); + }); + + test('User visible in right panel when added as entityReferenceList custom property', async ({ + page, + }) => { + test.slow(); + const { apiContext, afterAction } = await getApiContext(page); + const propertyName = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.ENTITY_REFERENCE_LIST + ].property.name; + const testUser = users[0]; + const userName = testUser.responseData.name; + const userDisplayName = testUser.responseData.displayName ?? userName; + + await (mainEntity as TableClass).patch({ + apiContext, + patchData: [ + { + op: 'add', + path: '/extension', + value: { + [propertyName]: [ + { + id: testUser.responseData.id, + type: 'user', + name: userName, + fullyQualifiedName: + testUser.responseData.fullyQualifiedName, + }, + ], + }, + }, + ], + }); + + await mainEntity.visitEntityPage(page); + + const userElement = page.getByTestId(userName); + const isUserVisible = await userElement.isVisible(); + if (!isUserVisible) { + await page.getByTestId('custom_properties').click(); + } + + const rightPanelSection = page.getByTestId(propertyName); + await expect(rightPanelSection).toBeVisible(); + + const userLink = page.getByTestId(userName).getByRole('link'); + await expect(userLink).toContainText(userName); + + const userDetailsResponse = page.waitForResponse( + '/api/v1/users/name/*' + ); + await userLink.click(); + await userDetailsResponse; + + await expect(page).toHaveURL( + new RegExp(`/users/(%22)?${userName}(%22)?`, 'i') + ); + await expect(page.getByTestId('user-display-name')).toHaveText( + userDisplayName + ); + + await (mainEntity as TableClass).patch({ + apiContext, + patchData: [ + { + op: 'add', + path: `/extension/${propertyName}`, + value: [], + }, + ], + }); + + await afterAction(); + }); + + test('table-cp shows row count, scrollable container, no expand toggle', async ({ + page, + }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[CustomPropertyTypeByName.TABLE_CP] + .property.name; + + await test.step('Add 5 rows of data to table property', async () => { + await redirectToHomePage(page); + await mainEntity.visitEntityPage(page); + await waitForAllLoadersToDisappear(page); + await page.getByTestId('custom_properties').click(); + + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + const editButton = container.getByTestId('edit-icon'); + await editButton.scrollIntoViewIfNeeded(); + await expect(editButton).toBeVisible(); + await expect(editButton).toBeEnabled(); + await editButton.click(); + + for (let i = 0; i < 5; i++) { + await page.getByTestId('add-new-row').click(); + await expect(page.locator('.om-rdg')).toBeVisible(); + await fillTableColumnInputDetails( + page, + `row${i + 1}-col1`, + entity.tableConfig.columns[0] + ); + await fillTableColumnInputDetails( + page, + `row${i + 1}-col2`, + entity.tableConfig.columns[1] + ); + } + + const patchResponse = page.waitForResponse( + `/api/v1/${entity.entityApiType}/*` + ); + await page.getByTestId('update-table-type-property').click(); + expect((await patchResponse).status()).toBe(200); + await waitForAllLoadersToDisappear(page); + }); + + await test.step('Verify row count (5) in property name', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + await expect(container.getByTestId('property-name')).toContainText( + '(5)' + ); + }); + + await test.step('Verify .custom-property-scrollable-container is scrollable', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + const scrollContainer = container.locator( + '.custom-property-scrollable-container' + ); + await expect(scrollContainer).toBeVisible(); + const isScrollable = await scrollContainer.evaluate( + (el) => el.scrollHeight > el.clientHeight + ); + expect(isScrollable).toBeTruthy(); + }); + + await test.step('Verify expand/collapse toggle is hidden', async () => { + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + await expect( + container.getByTestId(`toggle-${propertyName}`) + ).not.toBeVisible(); + }); + }); + + test('Enum: Set Value, Verify, Remove Value', async ({ page }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[CustomPropertyTypeByName.ENUM] + .property.name; + + await mainEntity.visitEntityPage(page); + + await page.locator('[data-testid="custom_properties"]').waitFor({ + state: 'visible', + }); + + const getCustomPropertiesResponse = page.waitForResponse( + 'api/v1/metadata/types/name/table?fields=customProperties*' + ); + await page.getByTestId('custom_properties').click(); + await getCustomPropertiesResponse; + + const propertyCard = page.getByTestId( + `custom-property-${propertyName}-card` + ); + await propertyCard.getByTestId('edit-icon').click(); + + const enumSelect = page.locator('[data-testid="enum-select"]'); + await expect(enumSelect).toBeVisible(); + await enumSelect.click(); + + await page + .locator('.ant-select-item-option-content') + .getByText('medium', { exact: true }) + .click(); + + const saveButton = page.locator('[data-testid="inline-save-btn"]'); + const patchValue1 = page.waitForResponse( + (resp) => + resp.url().includes('/api/v1/tables/') && + resp.request().method() === 'PATCH' + ); + await saveButton.click(); + await patchValue1; + + await expect(propertyCard.getByTestId('enum-value')).toContainText( + 'medium' + ); + + await propertyCard.locator('[data-testid="edit-icon"]').click(); + await enumSelect.hover(); + + const clearIcon = enumSelect.locator('.ant-select-clear'); + if (await clearIcon.isVisible()) { + await clearIcon.click(); + } else { + const enumInput = page.locator('#enumValues'); + await enumInput.click(); + await enumInput.fill(''); + } + + const patchValue2 = page.waitForResponse( + (resp) => + resp.url().includes('/api/v1/tables/') && + resp.request().method() === 'PATCH' + ); + await saveButton.click(); + await patchValue2; + + await expect(propertyCard.getByTestId('no-data')).toBeVisible(); + }); + + test('Duration: advanced search equalTo and Contains operators', async ({ + page, + }) => { + test.slow(); + const durationPropertyName = + mainEntity.customPropertyValue[CustomPropertyTypeByName.DURATION] + .property.name; + const durationPropertyValue = 'PT1H30M'; + + await test.step('Assign Custom Property Value', async () => { + await mainEntity.visitEntityPage(page); + + const customPropertyResponse = page.waitForResponse( + '/api/v1/metadata/types/name/table?fields=customProperties' + ); + await page.getByTestId('custom_properties').click(); + await customPropertyResponse; + + await page.locator('.ant-skeleton-active').waitFor({ + state: 'detached', + }); + + await page + .getByTestId(`custom-property-${durationPropertyName}-card`) + .getByTestId('edit-icon') + .click(); + + await page + .getByTestId('duration-input') + .fill(durationPropertyValue); + + const saveResponse = page.waitForResponse('/api/v1/tables/*'); + await page.getByTestId('inline-save-btn').click(); + await saveResponse; + }); + + await test.step('Verify Duration Type in Advance Search', async () => { + await sidebarClick(page, SidebarItem.EXPLORE); + await showAdvancedSearchDialog(page); + + const ruleLocator = page.locator('.rule').nth(0); + + await selectOption( + page, + ruleLocator.locator('.rule--field .ant-select'), + 'Custom Properties', + true + ); + await selectOption( + page, + ruleLocator.locator('.rule--field .ant-select'), + 'Table', + true + ); + await selectOption( + page, + ruleLocator.locator('.rule--field .ant-select'), + durationPropertyName, + true + ); + + await selectOption( + page, + ruleLocator.locator('.rule--operator .ant-select'), + CONDITIONS_MUST.equalTo.name + ); + + const inputElement = ruleLocator.locator( + '.rule--widget--TEXT input[type="text"]' + ); + await inputElement.fill(durationPropertyValue); + + await advanceSearchSaveFilter(page, durationPropertyValue); + + await expect( + page.getByTestId( + `table-data-card_${responseData.fullyQualifiedName ?? ''}` + ) + ).toBeVisible(); + + const partialSearchValue = durationPropertyValue.slice(0, 3); + await page.getByTestId('advance-search-filter-btn').click(); + await expect( + page.locator('[role="dialog"].ant-modal') + ).toBeVisible(); + + await selectOption( + page, + ruleLocator.locator('.rule--operator .ant-select'), + 'Contains' + ); + await inputElement.fill(partialSearchValue); + + await advanceSearchSaveFilter(page, partialSearchValue); + + await expect( + page.getByTestId( + `table-data-card_${responseData.fullyQualifiedName ?? ''}` + ) + ).toBeVisible(); + }); + }); + + // #27482 – Regression: between operator was dropping the upper bound + if (key === 'entity_table') { + test('Number CP between operator sends gte/lte bounds (Issue #27482)', async ({ + page, + }) => { + test.slow(); + const numberPropertyName = `pwNumberBetweenTest${uuid()}`; + const assignedValue = '55.7'; + + await test.step('Create number custom property and assign value', async () => { + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + + await addCustomPropertiesForEntity({ + page, + propertyName: numberPropertyName, + customPropertyData: entity, + customType: 'Number', + }); + + await mainEntity.visitEntityPage(page); + + const customPropertyResponse = page.waitForResponse( + '/api/v1/metadata/types/name/table?fields=customProperties' + ); + await page.getByTestId('custom_properties').click(); + await customPropertyResponse; + + await page.locator('.ant-skeleton-active').waitFor({ + state: 'detached', + }); + + await setValueForProperty({ + page, + propertyName: numberPropertyName, + value: assignedValue, + propertyType: 'number', + endpoint: EntityTypeEndpoint.Table, + }); + }); + + await test.step('between [50, 60]: query_filter must contain gte:50 and lte:60', async () => { + await sidebarClick(page, SidebarItem.EXPLORE); + await showAdvancedSearchDialog(page); + + await applyCustomPropertyFilter( + page, + numberPropertyName, + 'between', + CP_RANGE_VALUES.number, + 'Table' + ); + + const searchResponse = page.waitForResponse( + '/api/v1/search/query?*index=dataAsset*' + ); + await page.getByTestId('apply-btn').click(); + const res = await searchResponse; + + const url = res.request().url(); + const params = new URLSearchParams(url.split('?')[1]); + const queryFilter = JSON.parse( + params.get('query_filter') ?? '{}' + ); + const queryFilterStr = JSON.stringify(queryFilter); + + expect(queryFilterStr).toContain('"gte":50'); + expect(queryFilterStr).toContain('"lte":60'); + + await expect( + page.getByTestId( + `table-data-card_${responseData.fullyQualifiedName ?? ''}` + ) + ).toBeVisible(); + + await clearAdvancedSearchFilters(page); + }); + + await test.step('not_between [1, 5]: query_filter must contain must_not with gte:1 and lte:5', async () => { + await sidebarClick(page, SidebarItem.EXPLORE); + await showAdvancedSearchDialog(page); + + await applyCustomPropertyFilter( + page, + numberPropertyName, + 'not_between', + { start: 1, end: 5 }, + 'Table' + ); + + const searchResponse = page.waitForResponse( + '/api/v1/search/query?*index=dataAsset*' + ); + await page.getByTestId('apply-btn').click(); + const res = await searchResponse; + + const url = res.request().url(); + const params = new URLSearchParams(url.split('?')[1]); + const queryFilter = JSON.parse( + params.get('query_filter') ?? '{}' + ); + const queryFilterStr = JSON.stringify(queryFilter); + + expect(queryFilterStr).toContain('"must_not"'); + expect(queryFilterStr).toContain('"gte":1'); + expect(queryFilterStr).toContain('"lte":5'); + + await clearAdvancedSearchFilters(page); + }); + + await test.step('between [100, 200]: entity with value 55.7 should NOT be visible', async () => { + await sidebarClick(page, SidebarItem.EXPLORE); + await showAdvancedSearchDialog(page); + + await applyCustomPropertyFilter( + page, + numberPropertyName, + 'between', + { start: 100, end: 200 }, + 'Table' + ); + + const searchResponse = page.waitForResponse( + '/api/v1/search/query?*index=dataAsset*' + ); + await page.getByTestId('apply-btn').click(); + await searchResponse; + + await expect( + page.getByTestId( + `table-data-card_${responseData.fullyQualifiedName ?? ''}` + ) + ).not.toBeVisible(); + + await clearAdvancedSearchFilters(page); + }); + + await test.step('Cleanup', async () => { + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + await deleteCreatedProperty(page, numberPropertyName); + }); + }); + + test('no duplicate card after update', async ({ page }) => { + test.slow(); + + const propertyName = `pw.edge.update.${uuid()}`; + + await test.step('Create property', async () => { + await settingClick( + page, + entity.entityApiType as SettingOptionsType, + true + ); + await addCustomPropertiesForEntity({ + page, + propertyName, + customPropertyData: entity, + customType: 'String', + }); + }); + + await test.step('Set initial value', async () => { + await mainEntity.visitEntityPage(page); + await waitForAllLoadersToDisappear(page); + + await setValueForProperty({ + page, + propertyName, + value: 'initial value', + propertyType: 'string', + endpoint: EntityTypeEndpoint.Table, + }); + + await validateValueForProperty({ + page, + propertyName, + value: 'initial value', + propertyType: 'string', + }); + }); + + await test.step('Update value and verify only one card exists', async () => { + await setValueForProperty({ + page, + propertyName, + value: 'updated value', + propertyType: 'string', + endpoint: EntityTypeEndpoint.Table, + }); + + await validateValueForProperty({ + page, + propertyName, + value: 'updated value', + propertyType: 'string', + }); + + await expect( + page.getByTestId(`custom-property-${propertyName}-card`) + ).toHaveCount(1); + await expect( + page.getByTestId(`custom-property-"${propertyName}"-card`) + ).toHaveCount(0); + }); + + await test.step('Value persists after reload', async () => { + await page.reload(); + await waitForAllLoadersToDisappear(page); + + await validateValueForProperty({ + page, + propertyName, + value: 'updated value', + propertyType: 'string', + }); + + await expect( + page.getByTestId(`custom-property-${propertyName}-card`) + ).toHaveCount(1); + await expect( + page.getByTestId(`custom-property-"${propertyName}"-card`) + ).toHaveCount(0); + }); + + await test.step('Updated value is searchable via Advanced Search', async () => { + await sidebarClick(page, SidebarItem.EXPLORE); + + await showAdvancedSearchDialog(page); + + const ruleLocator = page.locator('.rule').nth(0); + + await selectOption( + page, + ruleLocator.locator('.rule--field .ant-select'), + 'Custom Properties', + true + ); + + await selectOption( + page, + ruleLocator.locator('.rule--field .ant-select'), + 'Table', + true + ); + + await selectOption( + page, + ruleLocator.locator('.rule--field .ant-select'), + propertyName, + true + ); + + await selectOption( + page, + ruleLocator.locator('.rule--operator .ant-select'), + CONDITIONS_MUST.equalTo.name + ); + + await ruleLocator + .locator('.rule--widget--TEXT input[type="text"]') + .fill('updated value'); + + await advanceSearchSaveFilter(page, 'updated value'); + + await expect( + page.getByTestId( + `table-data-card_${ + (mainEntity as TableClass).entityResponseData + .fullyQualifiedName + }` + ) + ).toBeVisible(); + }); + }); + } + } + + // ── Container-specific extra tests ───────────────────────────────────── + + if (key === 'entity_container') { + test('should show No Data placeholder when hyperlink has no value', async ({ + page, + }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.HYPERLINK_CP + ].property.name; + + await EntityDataClass.container1.visitEntityPage(page); + await page.click('[data-testid="custom_properties"]'); + + const containerLocator = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + + await expect(containerLocator.getByTestId('no-data')).toBeVisible(); + await expect(containerLocator.getByTestId('no-data')).toContainText( + 'Not set' + ); + }); + + test('should reject javascript: protocol URLs for XSS protection', async ({ + page, + }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.HYPERLINK_CP + ].property.name; + + await EntityDataClass.container1.visitEntityPage(page); + await page.click('[data-testid="custom_properties"]'); + + const editButton = page.locator( + `[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]` + ); + await editButton.scrollIntoViewIfNeeded(); + await editButton.click(); + + await page + .locator('[data-testid="hyperlink-url-input"]') + .fill('javascript:alert("XSS")'); + + await expect( + page.locator('.ant-form-item-explain-error') + ).toContainText('URL must use http or https protocol'); + + await page.locator('[data-testid="inline-cancel-btn"]').click(); + }); + + test('should accept valid http and https URLs', async ({ page }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.HYPERLINK_CP + ].property.name; + + await EntityDataClass.container1.visitEntityPage(page); + + await setValueForProperty({ + page, + propertyName, + value: 'https://openmetadata.io,OpenMetadata Docs', + propertyType: 'hyperlink-cp', + endpoint: EntityTypeEndpoint.Container, + }); + + await validateValueForProperty({ + page, + propertyName, + value: 'https://openmetadata.io,OpenMetadata Docs', + propertyType: 'hyperlink-cp', + }); + + const hyperlinkElement = page + .locator(`[data-testid="custom-property-${propertyName}-card"]`) + .getByTestId('hyperlink-value'); + + await expect(hyperlinkElement).toHaveAttribute( + 'href', + 'https://openmetadata.io' + ); + await expect(hyperlinkElement).toHaveAttribute('target', '_blank'); + await expect(hyperlinkElement).toHaveAttribute( + 'rel', + 'noopener noreferrer' + ); + }); + + test('should display URL when no display text is provided', async ({ + page, + }) => { + test.slow(); + const propertyName = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.HYPERLINK_CP + ].property.name; + const propertyValue = + mainEntity.customPropertyValue[ + CustomPropertyTypeByName.HYPERLINK_CP + ].value; + + await EntityDataClass.container1.visitEntityPage(page); + + await setValueForProperty({ + page, + propertyName, + value: propertyValue, + propertyType: 'hyperlink-cp', + endpoint: EntityTypeEndpoint.Container, + }); + + await validateValueForProperty({ + page, + propertyName, + value: propertyValue, + propertyType: 'hyperlink-cp', + }); + }); + } + + // ── Dashboard-specific extra tests ───────────────────────────────────── + + if (key === 'entity_dashboard') { + test.describe('Dashboard CP Advanced Search - Text Fields', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('String CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['string']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.string + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + CP_BASE_VALUES.string + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.string + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + CP_BASE_VALUES.string + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + CP_PARTIAL_SEARCH_VALUES.string + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + CP_PARTIAL_SEARCH_VALUES.string + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_like', + CP_BASE_VALUES.string + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + CP_BASE_VALUES.string + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('String CP with numeric-like string value', async ({ + browser, + page, + }) => { + test.slow(); + const numericStringDashboard = new DashboardClass(); + + await test.step('Setup dashboard with numeric-like string value', async () => { + const { apiContext, afterAction } = await createNewPage(browser); + + await numericStringDashboard.create(apiContext); + + await apiContext.patch( + `/api/v1/dashboards/${numericStringDashboard.entityResponseData.id}`, + { + data: [ + { + op: 'add', + path: '/extension', + value: { [propertyNames['string']]: '100' }, + }, + ], + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + + await afterAction(); + }); + + await test.step('Equal operator finds dashboard with string value "100"', async () => { + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyNames['string'], + 'equal', + '100' + ); + await verifySearchResults( + page, + numericStringDashboard.entityResponseData.fullyQualifiedName, + true, + '100' + ); + await clearAdvancedSearchFilters(page); + }); + + await test.step('Not_equal operator excludes dashboard with string value "100"', async () => { + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyNames['string'], + 'not_equal', + '100' + ); + await verifySearchResults( + page, + numericStringDashboard.entityResponseData.fullyQualifiedName, + false, + '100' + ); + await clearAdvancedSearchFilters(page); + }); + + await test.step('Contains operator finds dashboard with partial numeric-like string "10"', async () => { + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyNames['string'], + 'like', + '10' + ); + await verifySearchResults( + page, + numericStringDashboard.entityResponseData.fullyQualifiedName, + true, + '10' + ); + await clearAdvancedSearchFilters(page); + }); + + await test.step('Not contains operator excludes dashboard with partial numeric-like string "10"', async () => { + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyNames['string'], + 'not_like', + '10' + ); + await verifySearchResults( + page, + numericStringDashboard.entityResponseData.fullyQualifiedName, + false, + '10' + ); + await clearAdvancedSearchFilters(page); + }); + + await test.step('Is not null operator finds dashboard with numeric-like string value', async () => { + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyNames['string'], + 'is_not_null', + '' + ); + await verifySearchResults( + page, + numericStringDashboard.entityResponseData.fullyQualifiedName, + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test('Email CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['email']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.email + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + CP_BASE_VALUES.email + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.email + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + CP_PARTIAL_SEARCH_VALUES.email + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Markdown CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['markdown']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + CP_PARTIAL_SEARCH_VALUES.markdown + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_like', + CP_BASE_VALUES.markdown + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('SQL Query CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['sqlQuery']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + CP_PARTIAL_SEARCH_VALUES.sqlQuery + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Duration CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['duration']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.duration + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + CP_PARTIAL_SEARCH_VALUES.duration + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Time CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['time-cp']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.timeCp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + CP_PARTIAL_SEARCH_VALUES.timeCp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test.describe('Dashboard CP Advanced Search - Number Fields', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('Integer CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['integer']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.integer + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.integer + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'between', + CP_RANGE_VALUES.integer + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'not_between', { + start: CP_BASE_VALUES.integer - 2, + end: CP_BASE_VALUES.integer + 4, + }); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Number CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['number']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.number + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.number + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'between', + CP_RANGE_VALUES.number + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Timestamp CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['timestamp']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.timestamp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.timestamp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test.describe('Dashboard CP Advanced Search - Entity References', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('Entity Reference CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['entityReference']; + const containsText = + dashboardTopic1.entityResponseData.displayName?.substring(1, 5); + const regexpText = `${dashboardTopic1.entityResponseData.displayName?.substring( + 0, + 2 + )}.*${dashboardTopic1.entityResponseData.displayName?.substring( + 5, + 7 + )}.*`; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'select_equals', + dashboardTopic1.entityResponseData.displayName ?? '', + 'Dashboard', + 'entityReference' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'select_not_equals', + dashboardTopic1.entityResponseData.displayName ?? '', + 'Dashboard', + 'entityReference' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + containsText ?? '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + containsText + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_like', + containsText ?? '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + containsText + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'regexp', + regexpText + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + regexpText + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Entity Reference List CP with all operators', async ({ + page, + }) => { + test.slow(); + const propertyName = propertyNames['entityReferenceList']; + const containsText = + dashboardTopic1.entityResponseData.displayName?.substring(1, 5); + const regexpText = `${dashboardTopic1.entityResponseData.displayName?.substring( + 0, + 2 + )}.*${dashboardTopic1.entityResponseData.displayName?.substring( + 5, + 7 + )}.*`; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'select_equals', + dashboardTopic1.entityResponseData.displayName ?? '', + 'Dashboard', + 'entityReferenceList' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'select_equals', + dashboardTopic2.entityResponseData.displayName ?? '', + 'Dashboard', + 'entityReferenceList' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'select_not_equals', + dashboardTopic2.entityResponseData.displayName ?? '', + 'Dashboard', + 'entityReferenceList' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'like', + containsText ?? '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + containsText + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_like', + containsText ?? '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + containsText + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'regexp', + regexpText + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + regexpText + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test.describe('Dashboard CP Advanced Search - Date/Time Fields', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('DateTime CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['dateTime-cp']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.dateTimeCp, + undefined, + 'dateTime-cp' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.dateTimeCp, + undefined, + 'dateTime-cp' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'between', + CP_RANGE_VALUES.dateTimeCp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_between', + CP_RANGE_VALUES.dateTimeCp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Date CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['date-cp']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'equal', + CP_BASE_VALUES.dateCp, + undefined, + 'date-cp' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_equal', + CP_BASE_VALUES.dateCp, + undefined, + 'date-cp' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'between', + CP_RANGE_VALUES.dateCp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'not_between', + CP_RANGE_VALUES.dateCp + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test.describe('Dashboard CP Advanced Search - Enum Fields', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('Enum CP with all operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['enum']; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'multiselect_equals', + CP_BASE_VALUES.enum[0] + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'multiselect_contains', + CP_BASE_VALUES.enum[0] + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'multiselect_not_equals', + CP_BASE_VALUES.enum[0] + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'multiselect_not_contains', + CP_BASE_VALUES.enum[0] + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, propertyName, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + propertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test.describe('Dashboard CP Advanced Search - Special Types', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('Time Interval CP with operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['timeInterval']; + const startPropertyName = `${propertyName} (Start)`; + const endPropertyName = `${propertyName} (End)`; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + startPropertyName, + 'equal', + CP_BASE_VALUES.timeInterval.start + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + String(CP_BASE_VALUES.timeInterval.start) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + startPropertyName, + 'not_equal', + CP_BASE_VALUES.timeInterval.start + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + String(CP_BASE_VALUES.timeInterval.start) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + startPropertyName, + 'between', + { + start: CP_BASE_VALUES.timeInterval.start - 2, + end: CP_BASE_VALUES.timeInterval.start + 4, + } + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + startPropertyName, + 'not_between', + { + start: CP_BASE_VALUES.timeInterval.start - 2, + end: CP_BASE_VALUES.timeInterval.start + 4, + } + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + startPropertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + startPropertyName, + 'is_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + endPropertyName, + 'equal', + CP_BASE_VALUES.timeInterval.end + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + String(CP_BASE_VALUES.timeInterval.end) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + endPropertyName, + 'not_equal', + CP_BASE_VALUES.timeInterval.end + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + String(CP_BASE_VALUES.timeInterval.end) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, endPropertyName, 'between', { + start: CP_BASE_VALUES.timeInterval.end - 2, + end: CP_BASE_VALUES.timeInterval.end + 4, + }); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + endPropertyName, + 'not_between', + { + start: CP_BASE_VALUES.timeInterval.end - 2, + end: CP_BASE_VALUES.timeInterval.end + 4, + } + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + endPropertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + endPropertyName, + 'is_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + }); + + test('Hyperlink CP with operators', async ({ page }) => { + test.slow(); + const propertyName = propertyNames['hyperlink-cp']; + const urlProperty = `${propertyName} URL`; + const displayTextProperty = `${propertyName} Display Text`; + const urlPartialValue = CP_BASE_VALUES.hyperlinkCp.url.substring( + 3, + 9 + ); + const displayTextPartialValue = + CP_BASE_VALUES.hyperlinkCp.displayText.substring(2, 6); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + urlProperty, + 'equal', + CP_BASE_VALUES.hyperlinkCp.url + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + String(CP_BASE_VALUES.hyperlinkCp.url) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + urlProperty, + 'not_equal', + CP_BASE_VALUES.hyperlinkCp.url + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + String(CP_BASE_VALUES.hyperlinkCp.url) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + urlProperty, + 'like', + urlPartialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + urlPartialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + urlProperty, + 'not_like', + urlPartialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + urlPartialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + urlProperty, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter(page, urlProperty, 'is_null', ''); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + displayTextProperty, + 'equal', + CP_BASE_VALUES.hyperlinkCp.displayText + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + String(CP_BASE_VALUES.hyperlinkCp.displayText) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + displayTextProperty, + 'not_equal', + CP_BASE_VALUES.hyperlinkCp.displayText + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + String(CP_BASE_VALUES.hyperlinkCp.displayText) + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + displayTextProperty, + 'like', + displayTextPartialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + displayTextPartialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + displayTextProperty, + 'not_like', + displayTextPartialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + displayTextPartialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + displayTextProperty, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + displayTextProperty, + 'is_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test.describe('Dashboard CP Advanced Search - Table CP', () => { + test.beforeEach(async ({ page }) => { + await sidebarClick(page, SidebarItem.EXPLORE); + }); + + test('Table CP - Name column with all operators', async ({ + page, + }) => { + test.slow(); + const value = CP_BASE_VALUES.tableCp.rows[0]['Name']; + const partialValue = value.substring(1, 4); + const basePropertyName = propertyNames['table-cp']; + const columnPropertyName = `${basePropertyName} - Name`; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'equal', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + value + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'not_equal', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'like', + partialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + partialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'not_like', + partialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + partialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'is_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Table CP - Role column with all operators', async ({ + page, + }) => { + test.slow(); + const value = CP_BASE_VALUES.tableCp.rows[0]['Role']; + const partialValue = value.substring(1, 4); + const basePropertyName = propertyNames['table-cp']; + const columnPropertyName = `${basePropertyName} - Role`; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'equal', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + value + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'not_equal', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'like', + partialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + partialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'not_like', + partialValue + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + partialValue + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'is_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + + test('Table CP - Sr No column with all operators', async ({ + page, + }) => { + test.slow(); + const value = CP_BASE_VALUES.tableCp.rows[1]['Sr No']; + const basePropertyName = propertyNames['table-cp']; + const columnPropertyName = `${basePropertyName} - Sr No`; + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'equal', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + value + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'not_equal', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + value + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'like', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true, + value + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'not_like', + value + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false, + value + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'is_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + false + ); + await clearAdvancedSearchFilters(page); + + await showAdvancedSearchDialog(page); + await applyCustomPropertyFilter( + page, + columnPropertyName, + 'is_not_null', + '' + ); + await verifySearchResults( + page, + responseData.fullyQualifiedName ?? '', + true + ); + await clearAdvancedSearchFilters(page); + }); + }); + + test('Create custom property and configure search for Dashboard', async ({ + page, + }) => { + test.slow(true); + + await test.step('Create and assign custom property to Dashboard', async () => { + await settingClick(page, GlobalSettingOptions.DASHBOARDS, true); + await addCustomPropertiesForEntity({ + page, + propertyName: dashboardSearchPropertyName, + customPropertyData: + CUSTOM_PROPERTIES_ENTITIES['entity_dashboard'], + customType: 'String', + }); + + await mainEntity.visitEntityPage(page); + + const customPropertyResponse = page.waitForResponse( + '/api/v1/metadata/types/name/dashboard?fields=customProperties' + ); + await page.getByTestId('custom_properties').click(); + await customPropertyResponse; + + await page.locator('.ant-skeleton-active').waitFor({ + state: 'detached', + }); + + await setValueForProperty({ + page, + propertyName: dashboardSearchPropertyName, + value: dashboardPropertyValue, + propertyType: 'string', + endpoint: EntityTypeEndpoint.Dashboard, + }); + + await page.reload(); + + const customPropertiesTab = page.getByTestId('custom_properties'); + await customPropertiesTab.click(); + await page.locator('.ant-skeleton-active').waitFor({ + state: 'detached', + }); + + await expect(page.getByText(dashboardPropertyValue)).toBeVisible(); + }); + + await test.step('Configure search settings for Dashboard custom property', async () => { + await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); + + const dashboardCard = page.getByTestId( + 'preferences.search-settings.dashboards' + ); + await dashboardCard.click(); + + await expect(page).toHaveURL( + /settings\/preferences\/search-settings\/dashboards$/ + ); + + await waitForAllLoadersToDisappear(page); + + await page.getByTestId('add-field-btn').click(); + + const customPropertyOption = page.getByText( + `extension.${dashboardSearchPropertyName}`, + { exact: true } + ); + await customPropertyOption.click(); + + const fieldPanel = page.getByTestId( + `field-configuration-panel-extension.${dashboardSearchPropertyName}` + ); + await expect(fieldPanel).toBeVisible(); + + const customPropertyBadge = fieldPanel.getByTestId( + 'custom-property-badge' + ); + await expect(customPropertyBadge).toBeVisible(); + + await fieldPanel.click(); + await setSliderValue(page, 'field-weight-slider', 20); + + const matchTypeSelect = page.getByTestId('match-type-select'); + await matchTypeSelect.click(); + await page + .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') + .waitFor({ state: 'visible' }); + await page + .locator('.ant-select-item-option[title="Standard Match"]') + .click(); + + const searchSettingsSaveResponse = page.waitForResponse( + '/api/v1/system/settings' + ); + + await page.getByTestId('save-btn').click(); + + await searchSettingsSaveResponse; + + await expect( + page.getByTestId( + `field-configuration-panel-extension.${dashboardSearchPropertyName}` + ) + ).toBeVisible(); + }); + + await test.step('Search for Dashboard using custom property value', async () => { + await redirectToHomePage(page); + + const searchInput = page.getByTestId('searchBox'); + await searchInput.click(); + await searchInput.fill(dashboardPropertyValue); + await searchInput.press('Enter'); + + await page.getByTestId('dashboards-tab').click(); + + await waitForAllLoadersToDisappear(page); + + const searchResults = page.getByTestId('search-results'); + const dashboardCard = searchResults.getByTestId( + `table-data-card_${responseData.fullyQualifiedName ?? ''}` + ); + await expect(dashboardCard).toBeVisible(); + }); + }); + + test('Verify Dashboard custom property persists in search settings', async ({ + page, + }) => { + await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); + + const dashboardCard = page.getByTestId( + 'preferences.search-settings.dashboards' + ); + await dashboardCard.click(); + + await waitForAllLoadersToDisappear(page); + + const customPropertyField = page.getByTestId( + `field-configuration-panel-extension.${dashboardSearchPropertyName}` + ); + await expect(customPropertyField).toBeVisible(); + }); + } + + // ── Pipeline-specific extra tests ────────────────────────────────────── + + if (key === 'entity_pipeline') { + test('Create custom property and configure search for Pipeline', async ({ + page, + }) => { + test.slow(true); + + await test.step('Create and assign custom property to Pipeline', async () => { + await settingClick(page, GlobalSettingOptions.PIPELINES, true); + await addCustomPropertiesForEntity({ + page, + propertyName: pipelineSearchPropertyName, + customPropertyData: CUSTOM_PROPERTIES_ENTITIES['entity_pipeline'], + customType: 'String', + }); + + await mainEntity.visitEntityPage(page); + + const customPropertyResponse = page.waitForResponse( + '/api/v1/metadata/types/name/pipeline?fields=customProperties' + ); + await page.getByTestId('custom_properties').click(); + await customPropertyResponse; + + await page.locator('.ant-skeleton-active').waitFor({ + state: 'detached', + }); + + await setValueForProperty({ + page, + propertyName: pipelineSearchPropertyName, + value: pipelinePropertyValue, + propertyType: 'string', + endpoint: EntityTypeEndpoint.Pipeline, + }); + }); + + await test.step('Configure search settings for Pipeline custom property', async () => { + await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); + + const pipelineCard = page.getByTestId( + 'preferences.search-settings.pipelines' + ); + await pipelineCard.click(); + + await expect(page).toHaveURL( + /settings\/preferences\/search-settings\/pipelines$/ + ); + + await waitForAllLoadersToDisappear(page); + + await page.getByTestId('add-field-btn').click(); + + const customPropertyOption = page.getByText( + `extension.${pipelineSearchPropertyName}`, + { exact: true } + ); + await customPropertyOption.click(); + + const fieldPanel = page.getByTestId( + `field-configuration-panel-extension.${pipelineSearchPropertyName}` + ); + await expect(fieldPanel).toBeVisible(); + + const customPropertyBadge = fieldPanel.getByTestId( + 'custom-property-badge' + ); + await expect(customPropertyBadge).toBeVisible(); + + await fieldPanel.click(); + await setSliderValue(page, 'field-weight-slider', 12); + + const matchTypeSelect = page.getByTestId('match-type-select'); + await matchTypeSelect.click(); + await page + .locator('.ant-select-item-option[title="Phrase Match"]') + .click(); + + const searchSettingsSaveResponse = page.waitForResponse( + '/api/v1/system/settings' + ); + + await page.getByTestId('save-btn').click(); + await searchSettingsSaveResponse; + }); + + await test.step('Search for Pipeline using custom property value', async () => { + await redirectToHomePage(page); + + const searchInput = page.getByTestId('searchBox'); + await searchInput.click(); + await searchInput.clear(); + await searchInput.fill(pipelinePropertyValue); + await searchInput.press('Enter'); + + await page.getByTestId('pipelines-tab').click(); + + await waitForAllLoadersToDisappear(page); + + const searchResults = page.getByTestId('search-results'); + const pipelineCard = searchResults.getByTestId( + `table-data-card_${responseData.fullyQualifiedName ?? ''}` + ); + await expect(pipelineCard).toBeVisible(); + }); + }); + + test('Verify Pipeline custom property persists in search settings', async ({ + page, + }) => { + await settingClick(page, GlobalSettingOptions.SEARCH_SETTINGS); + + const pipelineCard = page.getByTestId( + 'preferences.search-settings.pipelines' + ); + await pipelineCard.click(); + + await waitForAllLoadersToDisappear(page); + + const customPropertyField = page.getByTestId( + `field-configuration-panel-extension.${pipelineSearchPropertyName}` + ); + await expect(customPropertyField).toBeVisible(); + }); + } + + // ── TableColumn-specific extra test ──────────────────────────────────── + + if (key === 'entity_tableColumn') { + test.describe('Set & update column-level custom property', async () => { + const testData: ColumnsTestData = {} as ColumnsTestData; + + test.beforeAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + const data = await createCustomPropertyForEntity( + apiContext, + EntityTypeEndpoint.TableColumn + ); + testData.customPropertyValue = data.customProperties; + testData.cleanupUser = data.cleanupUser; + testData.users = data.userNames; + + testData.columnFqn = + tableForColumnTest?.entityResponseData.columns[0] + .fullyQualifiedName ?? ''; + testData.tableFqn = + tableForColumnTest?.entityResponseData.fullyQualifiedName ?? ''; + + await afterAction(); + }); + + test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + await testData.cleanupUser?.(apiContext); + await afterAction(); + }); + + for (const type of Object.values(CustomPropertyTypeByName)) { + test(`Set ${type} custom property on column and verify in UI`, async ({ + page, + }) => { + await verifyTableColumnCustomPropertyPersistence({ + page, + columnFqn: testData.columnFqn, + tableFqn: testData.tableFqn, + propertyName: testData.customPropertyValue[type].property.name, + propertyType: type, + users: testData.users, + }); + }); + } + }); + } + + // ── Explore right-panel CP tab tests ─────────────────────────────────── + + if (makeInstance !== null) { + test(`Should display custom properties for ${entity.name} in right panel`, async ({ + page, + }) => { + const rightPanel = new RightPanelPageObject(page); + const customProperties = new CustomPropertiesPageObject(rightPanel); + rightPanel.setEntityConfig(mainEntity); + // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type + test.skip( + !rightPanel.isTabAvailable('custom property'), + `Custom Property tab not available for ${entity.name}` + ); + const fqn = getEntityFqn(mainEntity); + await navigateToExploreAndSelectEntity({ + page, + entityName: getEntityDisplayName(responseData), + endpoint: mainEntity.endpoint, + fullyQualifiedName: fqn, + exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], + }); + await rightPanel.waitForPanelVisible(); + await customProperties.navigateToCustomPropertiesTab(); + await customProperties.shouldShowCustomPropertiesContainer(); + const propertyName = Object.values(mainEntity.customPropertyValue)[0] + ?.property?.name; + if (propertyName) { + await customProperties.shouldShowCustomProperty(propertyName); + } + }); + + test(`Should search custom properties for ${entity.name} in right panel`, async ({ + page, + }) => { + const rightPanel = new RightPanelPageObject(page); + const customProperties = new CustomPropertiesPageObject(rightPanel); + rightPanel.setEntityConfig(mainEntity); + // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type + test.skip( + !rightPanel.isTabAvailable('custom property'), + `Custom Property tab not available for ${entity.name}` + ); + const fqn = getEntityFqn(mainEntity); + await navigateToExploreAndSelectEntity({ + page, + entityName: getEntityDisplayName(responseData), + endpoint: mainEntity.endpoint, + fullyQualifiedName: fqn, + exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], + }); + await rightPanel.waitForPanelVisible(); + await customProperties.navigateToCustomPropertiesTab(); + await customProperties.shouldShowCustomPropertiesContainer(); + const propertyName = Object.values(mainEntity.customPropertyValue)[0] + ?.property?.name; + if (propertyName) { + await customProperties.searchCustomProperties(propertyName); + await customProperties.shouldShowCustomProperty(propertyName); + } + }); + + test(`Should clear search and show all properties for ${entity.name} in right panel`, async ({ + page, + }) => { + const rightPanel = new RightPanelPageObject(page); + const customProperties = new CustomPropertiesPageObject(rightPanel); + rightPanel.setEntityConfig(mainEntity); + // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type + test.skip( + !rightPanel.isTabAvailable('custom property'), + `Custom Property tab not available for ${entity.name}` + ); + const fqn = getEntityFqn(mainEntity); + await navigateToExploreAndSelectEntity({ + page, + entityName: getEntityDisplayName(responseData), + endpoint: mainEntity.endpoint, + fullyQualifiedName: fqn, + exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], + }); + await rightPanel.waitForPanelVisible(); + await customProperties.navigateToCustomPropertiesTab(); + await customProperties.shouldShowCustomPropertiesContainer(); + const propertyName = Object.values(mainEntity.customPropertyValue)[0] + ?.property?.name; + if (propertyName) { + await customProperties.searchCustomProperties(propertyName); + await customProperties.shouldShowCustomProperty(propertyName); + await customProperties.clearSearch(); + await customProperties.shouldShowCustomPropertiesContainer(); + } + }); + + test(`Should verify property name is visible for ${entity.name} in right panel`, async ({ + page, + }) => { + const rightPanel = new RightPanelPageObject(page); + const customProperties = new CustomPropertiesPageObject(rightPanel); + rightPanel.setEntityConfig(mainEntity); + // eslint-disable-next-line playwright/no-skipped-test -- conditional skip based on entity type + test.skip( + !rightPanel.isTabAvailable('custom property'), + `Custom Property tab not available for ${entity.name}` + ); + const fqn = getEntityFqn(mainEntity); + await navigateToExploreAndSelectEntity({ + page, + entityName: getEntityDisplayName(responseData), + endpoint: mainEntity.endpoint, + fullyQualifiedName: fqn, + exploreTab: ENDPOINT_TO_EXPLORE_TAB_MAP[mainEntity.endpoint], + }); + await rightPanel.waitForPanelVisible(); + await customProperties.navigateToCustomPropertiesTab(); + await customProperties.shouldShowCustomPropertiesContainer(); + const propertyName = Object.values(mainEntity.customPropertyValue)[0] + ?.property?.name; + if (propertyName) { + await customProperties.verifyPropertyType(propertyName); + } + }); + } + }); + }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/shared/dataAssetLineageEntityTests.ts b/openmetadata-ui/src/main/resources/ui/playwright/shared/dataAssetLineageEntityTests.ts new file mode 100644 index 000000000000..a2691c4a9e4d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/shared/dataAssetLineageEntityTests.ts @@ -0,0 +1,261 @@ +/* + * 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. + */ + +/** + * Helper that registers the parameterized "verify create lineage for entity" + * tests. Sibling entities (used as edge targets in the lineage UI) are always + * the full 15 types; the `testEntities` parameter controls which entity types + * we parameterize the test loop over. + * + * Used by: + * - e2e/Pages/Lineage/TableLineage.spec.ts (PR: { table }) + * - e2e/stress/Lineage/DataAssetLineageAllEntities.spec.ts (Stress: 14 others) + * + * The non-parameterized describes from the original spec (Column Level + * Lineage, Temp lineage table nodes, Lineage Settings modal) live inline in + * TableLineage.spec.ts since they're not entity-redundant. + */ + +import { expect } from '@playwright/test'; +import { get, startCase } from 'lodash'; +import { test } from '../e2e/fixtures/pages'; +import { ApiEndpointClass } from '../support/entity/ApiEndpointClass'; +import { ContainerClass } from '../support/entity/ContainerClass'; +import { DashboardClass } from '../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../support/entity/DashboardDataModelClass'; +import { DirectoryClass } from '../support/entity/DirectoryClass'; +import { FileClass } from '../support/entity/FileClass'; +import { MetricClass } from '../support/entity/MetricClass'; +import { MlModelClass } from '../support/entity/MlModelClass'; +import { PipelineClass } from '../support/entity/PipelineClass'; +import { SearchIndexClass } from '../support/entity/SearchIndexClass'; +import { SpreadsheetClass } from '../support/entity/SpreadsheetClass'; +import { StoredProcedureClass } from '../support/entity/StoredProcedureClass'; +import { TableClass } from '../support/entity/TableClass'; +import { TopicClass } from '../support/entity/TopicClass'; +import { WorksheetClass } from '../support/entity/WorksheetClass'; +import { + getApiContext, + getDefaultAdminAPIContext, + redirectToHomePage, +} from '../utils/common'; +import { waitForAllLoadersToDisappear } from '../utils/entity'; +import { + applyPipelineFromModal, + clickLineageNode, + connectEdgeBetweenNodes, + deleteEdge, + editLineage, + editLineageClick, + performZoomOut, + rearrangeNodes, + verifyExportLineageCSV, + verifyExportLineagePNG, + verifyNodePresent, + visitLineageTab, +} from '../utils/lineage'; + +// Contains list of entity supported +const allEntities = { + table: TableClass, + container: ContainerClass, + topic: TopicClass, + dashboard: DashboardClass, + mlmodel: MlModelClass, + pipeline: PipelineClass, + storedProcedure: StoredProcedureClass, + searchIndex: SearchIndexClass, + dataModel: DashboardDataModelClass, + apiEndpoint: ApiEndpointClass, + metric: MetricClass, + directory: DirectoryClass, + file: FileClass, + spreadsheet: SpreadsheetClass, + worksheet: WorksheetClass, +}; + +type EntityClassUnion = + | TableClass + | ContainerClass + | TopicClass + | DashboardClass + | MlModelClass + | PipelineClass + | StoredProcedureClass + | SearchIndexClass + | DashboardDataModelClass + | ApiEndpointClass + | MetricClass + | DirectoryClass + | FileClass + | SpreadsheetClass + | WorksheetClass; + +export const registerDataAssetLineageEntityTests = ( + testEntities: Record EntityClassUnion> +) => { + test.describe('Data asset lineage', () => { + const pipeline = new PipelineClass(); + const entities: EntityClassUnion[] = []; + + test.beforeAll( + 'setup lineage creation with other entity creation', + async ({ browser }) => { + const { apiContext, afterAction } = await getDefaultAdminAPIContext( + browser + ); + + Object.values(allEntities).forEach((EntityClass) => { + const lineageEntity = new EntityClass(); + + entities.push(lineageEntity); + }); + + await pipeline.create(apiContext); + await Promise.all(entities.map((entity) => entity.create(apiContext))); + + await afterAction(); + } + ); + + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + }); + + Object.entries(testEntities).forEach(([key, EntityClass]) => { + const lineageEntity = new EntityClass(); + + test(`verify create lineage for entity - ${startCase(key)}`, async ({ + page, + }) => { + // 5 minute timeout + test.setTimeout(5 * 60 * 1000); + + await test.step('prepare entity', async () => { + const { apiContext } = await getApiContext(page); + + await lineageEntity.create(apiContext); + await lineageEntity.visitEntityPage(page); + await visitLineageTab(page); + await editLineageClick(page); + }); + + await test.step('should create lineage with normal edge', async () => { + for (const entity of entities) { + await connectEdgeBetweenNodes(page, lineageEntity, entity); + await rearrangeNodes(page); + await performZoomOut(page); + } + + const lineageRes = page.waitForResponse( + '/api/v1/lineage/getLineage?*' + ); + await page.reload(); + await lineageRes; + await page.getByTestId('edit-lineage').waitFor({ + state: 'visible', + }); + + await waitForAllLoadersToDisappear(page); + await page + .getByTestId( + `lineage-node-${lineageEntity.entityResponseData.fullyQualifiedName}` + ) + .waitFor(); + await rearrangeNodes(page); + await performZoomOut(page); + + for (const entity of entities) { + await verifyNodePresent(page, entity); + } + + // Check the Entity Drawer + await performZoomOut(page); + + for (const entity of entities) { + const toNodeFqn = get( + entity, + 'entityResponseData.fullyQualifiedName', + '' + ); + const entityName = get( + entity, + 'entityResponseData.displayName', + get(entity, 'entityResponseData.name', '') + ); + + await clickLineageNode(page, toNodeFqn); + + await expect( + page + .locator('.lineage-entity-panel') + .getByTestId('entity-header-title') + ).toHaveText(entityName); + + await page.getByTestId('drawer-close-icon').click(); + + // Panel should not be visible after closing it + await expect( + page.locator('.lineage-entity-panel') + ).not.toBeVisible(); + } + }); + + await test.step('should create lineage with edge having pipeline', async () => { + await editLineage(page); + + await page.getByTestId('fit-screen').click(); + await page.getByRole('menuitem', { name: 'Fit to screen' }).click(); + await performZoomOut(page, 8); + await waitForAllLoadersToDisappear(page); + + const fromNodeFqn = get( + lineageEntity, + 'entityResponseData.fullyQualifiedName', + '' + ); + + await clickLineageNode(page, fromNodeFqn); + + for (const entity of entities) { + await applyPipelineFromModal(page, lineageEntity, entity, pipeline); + } + }); + + await test.step('Verify Lineage Export CSV', async () => { + await editLineageClick(page); + await waitForAllLoadersToDisappear(page); + await performZoomOut(page); + await verifyExportLineageCSV(page, lineageEntity, entities, pipeline); + }); + + await test.step('Verify Lineage Export PNG', async () => { + await verifyExportLineagePNG(page); + }); + + await test.step('Remove lineage between nodes for the entity', async () => { + await editLineage(page); + await page.getByTestId('fit-screen').click(); + await page.getByRole('menuitem', { name: 'Fit to screen' }).click(); + await waitForAllLoadersToDisappear(page); + + await performZoomOut(page); + + for (const entity of entities) { + await deleteEdge(page, lineageEntity, entity); + } + }); + }); + }); + }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/fixtures/userPages.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/fixtures/userPages.ts deleted file mode 100644 index f81386082a9f..000000000000 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/fixtures/userPages.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2025 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 { Browser, Page, test as base } from '@playwright/test'; - -// Declare the types of your fixtures -type UserPages = { - adminPage: Page; - dataConsumerPage: Page; - dataStewardPage: Page; - ownerPage: Page; - editDescriptionPage: Page; - editTagsPage: Page; - editGlossaryTermPage: Page; -}; - -// Extend the base test type with your fixtures -export const test = base.extend({ - adminPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/admin.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, - dataConsumerPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/dataConsumer.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, - dataStewardPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/dataSteward.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, - ownerPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/owner.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, - editDescriptionPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/editDescription.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, - editTagsPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/editTags.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, - editGlossaryTermPage: async ({ browser }: { browser: Browser }, use) => { - const context = await browser.newContext({ - storageState: 'playwright/.auth/editGlossaryTerm.json', - }); - const page = await context.newPage(); - await use(page); - await context.close(); - }, -}); - -export { expect } from '@playwright/test'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts index 24cb7a7b6d95..232af9512fb8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts @@ -683,7 +683,7 @@ export const runRuleGroupTestsWithNonExistingValue = async (page: Page) => { await expect(dropdownText).toContainText('Loading...'); // eslint-disable-next-line playwright/no-wait-for-timeout -- search debounce delay - await page.waitForTimeout(1000); + await page.waitForTimeout(300); await expect(dropdownText).not.toContainText('Loading...'); }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts index 3f13e846cc1d..7e10442304ad 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts @@ -477,7 +477,7 @@ export const selectAssetTypes = async ( // Search for the asset type await page.keyboard.type(searchTerm); // eslint-disable-next-line playwright/no-wait-for-timeout -- search debounce delay - await page.waitForTimeout(500); + await page.waitForTimeout(200); // Try to click the filtered result const filteredElement = page.locator(`[data-testid="${index}-option"]`); @@ -618,7 +618,7 @@ export const verifyWidgetEntityNavigation = async ( // Wait again for any widget-specific loaders await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); // eslint-disable-next-line playwright/no-wait-for-timeout -- widget rendering delay - await page.waitForTimeout(1000); + await page.waitForTimeout(300); // Check for entity items in the widget const entityItems = widget.locator(entitySelector); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index d3517b9dc85f..a04e0454d332 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -1220,7 +1220,7 @@ export const removeGlossaryTerm = async ( .getByTestId('edit-button') .click(); // eslint-disable-next-line playwright/no-wait-for-timeout -- avoid popup collision with click - await page.waitForTimeout(500); + await page.waitForTimeout(200); await page .getByTestId('glossary-container') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts index 40e9633b5a21..ca8723db72bb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts @@ -200,7 +200,7 @@ export const dragAndDropNode = async ( destinationSelector: string ) => { // eslint-disable-next-line playwright/no-wait-for-timeout -- canvas stabilization before drag operation - await page.waitForTimeout(1000); + await page.waitForTimeout(300); const destinationElement = page.locator(destinationSelector); await destinationElement.waitFor(); await page.hover(originSelector); @@ -1024,7 +1024,7 @@ export const verifyPlatformLineageForEntity = async ( await page.getByTestId(`node-suggestion-${fromFqn}`).click(); // eslint-disable-next-line playwright/no-wait-for-timeout -- canvas stabilization after node selection - await page.waitForTimeout(500); + await page.waitForTimeout(200); const fromNode = page.getByTestId(`lineage-node-${fromFqn}`); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/metric.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/metric.ts index 86dc7f734266..29f0e329e796 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/metric.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/metric.ts @@ -203,7 +203,7 @@ export const updateRelatedMetric = async ( ); // eslint-disable-next-line playwright/no-wait-for-timeout -- right panel rendering delay - await page.waitForTimeout(1000); + await page.waitForTimeout(300); // Wait for the metrics API call to complete const metricsResponsePromise2 = page.waitForResponse( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts index 2b767aa2ca64..a8ef72ffe84a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts @@ -60,7 +60,12 @@ export const exploreShouldShowEntity = async ( if (shouldSee) { await expect(resultCard.first()).toBeVisible(); } else { - await expect(resultCard).toHaveCount(0); + // RBAC enforcement against newly-assigned user roles lags the patch + // call by several seconds — the search-index user doc needs to update + // before queries get filtered against the role's policies. Use a + // longer timeout on the negative assertion so the result-card has + // time to drop out as the role takes effect. + await expect(resultCard).toHaveCount(0, { timeout: 45_000 }); } }; @@ -82,16 +87,22 @@ export const exploreTreeCategories = async ( await exploreRes; await waitForAllLoadersToDisappear(page); - for (const category of visible) { - await expect( - page.getByTestId(`explore-tree-title-${category}`) - ).toBeVisible(); - } - for (const category of hidden) { - await expect( - page.getByTestId(`explore-tree-title-${category}`) - ).toHaveCount(0); - } + // The explore tree fires multiple aggregation queries to compute per-category + // counts; the tree DOM updates asynchronously as those resolve. Poll the + // category visibility/absence assertions to ride out renders that haven't + // settled yet, rather than relying on a single search-query wait. + await expect(async () => { + for (const category of visible) { + await expect( + page.getByTestId(`explore-tree-title-${category}`) + ).toBeVisible({ timeout: 2_000 }); + } + for (const category of hidden) { + await expect( + page.getByTestId(`explore-tree-title-${category}`) + ).toHaveCount(0, { timeout: 2_000 }); + } + }).toPass({ timeout: 20_000, intervals: [500, 1_000, 2_000] }); }; export const enableDisableSearchRBAC = async ( @@ -135,9 +146,14 @@ export const searchForEntityShouldWork = async ( await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); - await page.getByTestId('searchBox').press('Enter'); - await page.waitForResponse(`api/v1/search/query?**`); + // Register waitForResponse BEFORE the action that triggers it. Registering + // after `press('Enter')` is racy — the search response can return before + // the listener is attached, leaving the assertion to run against stale UI + // state. + const searchResponse = page.waitForResponse(`api/v1/search/query?**`); + await page.getByTestId('searchBox').press('Enter'); + await searchResponse; await waitForAllLoadersToDisappear(page); @@ -169,9 +185,12 @@ export const searchForEntityShouldWorkShowNoResult = async ( await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); - await page.getByTestId('searchBox').press('Enter'); - await page.waitForResponse(`api/v1/search/query?**`); + // Register waitForResponse BEFORE the action that triggers it. See the + // matching note in searchForEntityShouldWork for why. + const searchResponse = page.waitForResponse(`api/v1/search/query?**`); + await page.getByTestId('searchBox').press('Enter'); + await searchResponse; await waitForAllLoadersToDisappear(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts index f9693772863d..341e30925c8f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts @@ -393,7 +393,11 @@ export const getTaskCard = (page: Page, task: CreatedTask) => { export const openTaskDetails = async (page: Page, task: CreatedTask) => { const taskCard = getTaskCard(page, task); logTaskDebug('openTaskDetails:waitingForCard', task.taskId); - await expect(taskCard).toBeVisible({ timeout: 15000 }); + // The activity-feed UI re-fetches its list after the task is created via + // API; under Basic-project parallelism the refresh can lag past 15s and + // the card never appears within the default timeout. 45s gives the feed + // enough time to propagate without slowing healthy runs. + await expect(taskCard).toBeVisible({ timeout: 45000 }); logTaskDebug('openTaskDetails:click', task.taskId); await taskCard.click(); await expect(page.locator(TASK_TAB_SELECTOR)).toBeVisible();