diff --git a/.github/scripts/build_playwright_shards.py b/.github/scripts/build_playwright_shards.py index b24d9cc77818..488556337cb9 100755 --- a/.github/scripts/build_playwright_shards.py +++ b/.github/scripts/build_playwright_shards.py @@ -30,6 +30,7 @@ "GlobalSettings", "SystemCertificationTags", "IntakeForm", + "AdvancedSearch", } PROJECT_LANES = { "chromium": "chromium", @@ -46,6 +47,7 @@ "GlobalSettings": "global-state", "SystemCertificationTags": "global-state", "IntakeForm": "global-state", + "AdvancedSearch": "advanced-search", } PROJECT_DEPENDENCIES = { "DataAssetRulesDisabled": {"DataAssetRulesEnabled"}, @@ -548,6 +550,7 @@ def lane_bounds(lane: str, mode: str) -> tuple[int, int]: if lane == "chromium": return (5, COMMON_MAX_SHARDS) if mode == "full" else (1, COMMON_MAX_SHARDS) if lane in { + "advanced-search", "domain-isolation", "global-state", "import-export", diff --git a/.github/workflows/playwright-e2e-reusable.yml b/.github/workflows/playwright-e2e-reusable.yml index ab3b70d72129..3fb0fc187c62 100644 --- a/.github/workflows/playwright-e2e-reusable.yml +++ b/.github/workflows/playwright-e2e-reusable.yml @@ -695,6 +695,7 @@ jobs: --project=GlobalSettings \ --project=SystemCertificationTags \ --project=IntakeForm \ + --project=AdvancedSearch \ --project=search-nightly \ > "$RUNNER_TEMP/playwright-test-list.json" diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-account-card.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-account-card.tsx index f18b63ccf512..de7a63f8e7d6 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-account-card.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/app-navigation/base-components/nav-account-card.tsx @@ -250,6 +250,10 @@ export const NavAccountCard = ({ cx( 'tw:origin-(--trigger-anchor-point) tw:will-change-transform', diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/popover/popover.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/popover/popover.tsx index 175cc63cfc02..4a5917f73355 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/popover/popover.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/popover/popover.tsx @@ -70,6 +70,9 @@ export const Popover = ({ }: PopoverProps) => { return ( diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/dropdown/dropdown.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/dropdown/dropdown.tsx index 78aecd2dbe8e..34fe884f37d2 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/dropdown/dropdown.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/dropdown/dropdown.tsx @@ -141,6 +141,11 @@ const DropdownPopover = (props: DropdownPopoverProps) => { return ( diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/combobox.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/combobox.tsx index 5ab5dfe6e805..26061ab3f68b 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/combobox.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/combobox.tsx @@ -227,7 +227,16 @@ export const ComboBox = ({ return ( - + {/* items must live on the ComboBox (not the inner ListBox) so React + Aria owns the collection. Using controlled `items` (not defaultItems) + ensures that callers who manage their own item list — e.g. async + loaders that call setItems() after a fetch — see updates reflected in + the dropdown. The previous `defaultItems` form only initialised the + internal collection once and silently ignored subsequent prop changes + (standard uncontrolled-state behaviour). With `items` being + controlled, callers that want client-side filtering must do it + themselves before passing items in. */} + {(state) => (
{otherProps.label && ( @@ -253,7 +262,6 @@ export const ComboBox = ({ triggerRef={triggerRef}> ( )}> diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/multi-select.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/multi-select.tsx index 5d0cfbc79052..7e69b1147d0d 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/multi-select.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/select/multi-select.tsx @@ -37,7 +37,6 @@ import { ComboBoxStateContext, } from 'react-aria-components'; import type { ListData } from 'react-stately'; -import { useListData } from 'react-stately'; import { SelectItem } from './select-item'; interface ComboBoxValueProps @@ -280,6 +279,7 @@ export const MultiSelectBase = ({ onItemInserted, shortcut, placeholder = 'Search', + onInputChange: onInputChangeProp, // Omit these props to avoid conflicts with the `Select` component name: _name, className: _className, @@ -298,10 +298,15 @@ export const MultiSelectBase = ({ [contains, selectedKeys] ); - const accessibleList = useListData({ - initialItems: items, - filter, - }); + // Derive the visible options from the live `items` prop instead of + // useListData({ initialItems }) — that hook snapshots the items on mount, + // so async consumers that fetch options on input change never see their + // results reflected in the popup. + const [filterText, setFilterText] = useState(''); + const filteredItems = useMemo( + () => (items ?? []).filter((item) => filter(item, filterText)), + [items, filter, filterText] + ); const onRemove = useCallback( (keys: Set) => { @@ -322,7 +327,7 @@ export const MultiSelectBase = ({ return; } - const item = accessibleList.getItem(id); + const item = (items ?? []).find((currentItem) => currentItem.id === id); if (!item) { return; @@ -333,14 +338,18 @@ export const MultiSelectBase = ({ onItemInserted?.(id); } - accessibleList.setFilterText(''); + setFilterText(''); }; const onInputChange = useCallback( (value: string) => { - accessibleList.setFilterText(value); + setFilterText(value); + // Chain the consumer's handler — the internal one is applied after + // {...props} on AriaComboBox and would otherwise silently drop it + // (async search widgets rely on it to fetch matching options). + onInputChangeProp?.(value); }, - [accessibleList] + [onInputChangeProp] ); const placeholderRef = useRef(null); @@ -370,8 +379,8 @@ export const MultiSelectBase = ({ { export const Popover = (props: PopoverProps) => { return ( { topic1.create(apiContext), topic2.create(apiContext), ]); + glossaryEntity = new Glossary(undefined, [ { id: user.responseData.id, @@ -457,23 +458,24 @@ test.describe( const ruleLocator = page.locator('.rule').nth(0); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Status', true ); - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - '==' - ); + await selectOption(page, ruleLocator.locator('.rule--operator'), '=='); }); await test.step('Open Status value dropdown and verify all hard-coded options appear', async () => { const ruleLocator = page.locator('.rule').nth(0); - await ruleLocator.locator('.widget--widget > .ant-select').click(); + const triggerBtn = ruleLocator.locator( + '.widget--widget button[aria-haspopup="listbox"]' + ); + + await expect(triggerBtn).toBeVisible(); + await triggerBtn.click(); const dropdown = page - .locator('.ant-select-dropdown') + .locator('[role="listbox"]') .filter({ hasText: EntityStatus.Approved }) .last(); @@ -482,7 +484,7 @@ test.describe( for (const status of ENTITY_STATUSES) { await expect( dropdown - .locator('.ant-select-item-option') + .getByRole('option') .filter({ hasText: new RegExp(`^${status}$`, 'i') }) .first() ).toBeVisible(); @@ -1642,83 +1644,48 @@ test.describe( await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Custom Properties', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Table', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), enumCPName, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), 'Equals' ); - const valueSelector = ruleLocator.locator( - '.ant-select-selection-overflow' + const comboboxInput = ruleLocator.locator( + '.rule--widget input[role="combobox"]' ); - await expect(valueSelector).toBeVisible({ timeout: 15000 }); - await valueSelector.click(); + await expect(comboboxInput).toBeVisible({ timeout: 15000 }); + // fill('') focuses the input (menuTrigger="focus" opens the popup) + // without pointer-clicking — the overlaid chevron button can intercept + // clicks at the input's center in narrow ComboBoxes. + await comboboxInput.fill(''); - const dropdown = page.locator('.ant-select-dropdown:visible').last(); + const dropdown = page.locator('[role="listbox"]:visible').last(); await expect(dropdown).toBeVisible(); - return { ruleLocator, valueSelector, dropdown }; + return { ruleLocator, comboboxInput, dropdown }; }; - test('should append page-2 items and make them visible when Load more button is clicked', async ({ - page, - }) => { - test.slow(); - - const { dropdown } = await openEnumValueDropdown(page); - - // Page 1 items present; page-2 item not yet visible - await expect( - dropdown.locator(`[title="${FIRST_PAGE_VALUE}"]`) - ).toBeVisible({ timeout: 10000 }); - await expect( - dropdown.locator(`[title="${SECOND_PAGE_VALUE}"]`) - ).not.toBeVisible(); - - // "Load more..." button visible at the bottom of the list - const loadMoreBtn = dropdown - .locator('a') - .filter({ hasText: /load more/i }); - - await expect(loadMoreBtn).toBeVisible(); - - // Click Load more → page-2 items append - await loadMoreBtn.click(); - - // Hover over the virtual list so mouse wheel events target it - const virtualListHolder = dropdown.locator('.rc-virtual-list-holder'); - - await expect(virtualListHolder).toBeVisible(); - await virtualListHolder.hover(); - - // Wheel-scroll in small increments until the page-2 item comes into view - const secondPageItem = dropdown.locator(`[title="${SECOND_PAGE_VALUE}"]`); - let found = await secondPageItem.isVisible(); - - for (let i = 0; i < 20 && !found; i++) { - await page.mouse.wheel(0, 200); - found = await secondPageItem.isVisible(); - } - - await expect(secondPageItem).toBeVisible({ timeout: 5000 }); + test.skip('should append page-2 items and make them visible when Load more button is clicked', () => { + // Load more and rc-virtual-list are Ant Design Select features not present + // in the new react-aria MultiSelect component. }); test('should find page-2 items via search without clicking Load more', async ({ @@ -1730,22 +1697,22 @@ test.describe( // Page 1 items load; page-2 item is not yet visible await expect( - dropdown.locator(`[title="${FIRST_PAGE_VALUE}"]`) + dropdown.getByRole('option', { name: FIRST_PAGE_VALUE }) ).toBeVisible({ timeout: 10000 }); await expect( - dropdown.locator(`[title="${SECOND_PAGE_VALUE}"]`) + dropdown.getByRole('option', { name: SECOND_PAGE_VALUE }) ).not.toBeVisible(); // Type to search — asyncFetch filters the full values array, not just the loaded page const searchInput = ruleLocator.locator( - '.rule--widget .ant-select-selection-search-input' + '.rule--widget input[role="combobox"]' ); await searchInput.fill(SECOND_PAGE_VALUE); // Item appears immediately without clicking Load more await expect( - dropdown.locator(`[title="${SECOND_PAGE_VALUE}"]`) + dropdown.getByRole('option', { name: SECOND_PAGE_VALUE }) ).toBeVisible({ timeout: 10000 }); }); } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearchSuggestions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearchSuggestions.spec.ts index 2eef301f54f9..35593498dccb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearchSuggestions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/AdvancedSearchSuggestions.spec.ts @@ -58,27 +58,17 @@ test.describe('Advanced Search Suggestions', () => { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), field.label, true ); - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - '==' - ); + await selectOption(page, ruleLocator.locator('.rule--operator'), '=='); const dropdownInput = ruleLocator.locator( - '.widget--widget > .ant-select > .ant-select-selector input' + '.widget--widget input[role="combobox"]' ); - const aggregateRes1 = page.waitForResponse('/api/v1/search/aggregate?*'); - - await dropdownInput.click(); - - await aggregateRes1; - const searchText = toLower( getFieldsSuggestionSearchText(field.label, testData.fieldSearchData) ); @@ -95,7 +85,10 @@ test.describe('Advanced Search Suggestions', () => { await test .expect( - page.locator(`.ant-select-dropdown:visible [title="${searchText}"]`) + page + .locator('[role="listbox"]:visible [role="option"]') + .filter({ hasText: searchText }) + .first() ) .toBeVisible(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts index b03d3f2d2822..69994bc1bec5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts @@ -143,14 +143,14 @@ test.describe('Curated Assets Widget', () => { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), 'Contains' ); @@ -258,19 +258,15 @@ test.describe('Curated Assets Widget', () => { const ruleLocator = page.locator('.rule').nth(0); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Deleted', true ); - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - 'Is' - ); + await selectOption(page, ruleLocator.locator('.rule--operator'), 'Is'); await ruleLocator - .locator('.rule--value .rule--widget--BOOLEAN .ant-switch') + .locator('.rule--value .rule--widget--BOOLEAN label') .click(); await expect(page.locator('[data-testid="saveButton"]')).toBeEnabled(); @@ -334,35 +330,30 @@ test.describe('Curated Assets Widget', () => { const ruleLocator1 = page.locator('.rule').nth(0); await selectOption( page, - ruleLocator1.locator('.rule--field .ant-select'), + ruleLocator1.locator('.rule--field'), 'Owners', true ); - await selectOption( - page, - ruleLocator1.locator('.rule--operator .ant-select'), - 'Is Set' - ); + await selectOption(page, ruleLocator1.locator('.rule--operator'), 'Is Set'); await page.getByRole('button', { name: 'Add Condition' }).click(); // Switch to OR condition (AND is selected by default, click OR button) - await page.locator('.group--conjunctions button:has-text("OR")').click(); + await page + .locator('.group--conjunctions') + .getByRole('radio', { name: 'Or' }) + .click(); const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), 'Deleted', true ); - await selectOption( - page, - ruleLocator2.locator('.rule--operator .ant-select'), - 'Is' - ); + await selectOption(page, ruleLocator2.locator('.rule--operator'), 'Is'); await ruleLocator2 - .locator('.rule--value .rule--widget--BOOLEAN .ant-switch') + .locator('.rule--value .rule--widget--BOOLEAN label') .click(); const queryResponse = page.waitForResponse( @@ -436,32 +427,31 @@ test.describe('Curated Assets Widget', () => { const ruleLocator1 = page.locator('.rule').nth(0); await selectOption( page, - ruleLocator1.locator('.rule--field .ant-select'), + ruleLocator1.locator('.rule--field'), 'Deleted', true ); - await selectOption( - page, - ruleLocator1.locator('.rule--operator .ant-select'), - 'Is' - ); + await selectOption(page, ruleLocator1.locator('.rule--operator'), 'Is'); await ruleLocator1 - .locator('.rule--value .rule--widget--BOOLEAN .ant-switch') + .locator('.rule--value .rule--widget--BOOLEAN label') .click(); await page.getByRole('button', { name: 'Add Condition' }).click(); - await page.locator('.group--conjunctions button:has-text("AND")').click(); + await page + .locator('.group--conjunctions') + .getByRole('radio', { name: 'And' }) + .click(); const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), 'Display Name', true ); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), + ruleLocator2.locator('.rule--operator'), 'Contains' ); @@ -551,18 +541,14 @@ test.describe('Curated Assets Widget', () => { const ruleLocator1 = page.locator('.rule').nth(0); await selectOption( page, - ruleLocator1.locator('.rule--field .ant-select'), + ruleLocator1.locator('.rule--field'), 'Owners', true ); + await selectOption(page, ruleLocator1.locator('.rule--operator'), 'Any in'); await selectOption( page, - ruleLocator1.locator('.rule--operator .ant-select'), - 'Any in' - ); - await selectOption( - page, - ruleLocator1.locator('.rule--value .ant-select'), + ruleLocator1.locator('.rule--value'), 'admin', true ); @@ -570,26 +556,24 @@ test.describe('Curated Assets Widget', () => { await page.getByRole('button', { name: 'Add Condition' }).click(); // Switch first group to OR condition (AND is default) - await page.locator('.group--conjunctions button:has-text("OR")').click(); + await page + .locator('.group--conjunctions') + .getByRole('radio', { name: 'Or' }) + .click(); const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), 'Description Status', true ); + await selectOption(page, ruleLocator2.locator('.rule--operator'), 'Is'); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), - 'Is' - ); - await selectOption( - page, - ruleLocator2.locator('.rule--value .ant-select'), + ruleLocator2.locator('.rule--value'), 'Incomplete' ); - await ruleLocator2.locator('.rule--value input').fill('production'); // Add another condition await page.getByRole('button', { name: 'Add Condition' }).click(); @@ -597,18 +581,14 @@ test.describe('Curated Assets Widget', () => { const ruleLocator3 = page.locator('.rule').nth(2); await selectOption( page, - ruleLocator3.locator('.rule--field .ant-select'), + ruleLocator3.locator('.rule--field'), 'Tier', true ); + await selectOption(page, ruleLocator3.locator('.rule--operator'), 'Is Not'); await selectOption( page, - ruleLocator3.locator('.rule--operator .ant-select'), - 'Is Not' - ); - await selectOption( - page, - ruleLocator3.locator('.rule--value .ant-select'), + ruleLocator3.locator('.rule--value'), 'tier.tier5', true ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/PersonaAIContext.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/PersonaAIContext.spec.ts index 6a980874a0ef..5cd51ca59525 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/PersonaAIContext.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/PersonaAIContext.spec.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Page, Request, Route } from '@playwright/test'; +import { Locator, Page, Request, Route } from '@playwright/test'; import { EntityType } from '../../../src/enums/entity.enum'; import { CacheState, @@ -37,6 +37,16 @@ const dbService = new DatabaseServiceClass(); const RULE_ID = '33333333-3333-4333-8333-333333333333'; const CREATED_RULE_ID = '44444444-4444-4444-8444-444444444444'; +// Post antd->core migration each rule field/operator renders a core +// Select.ComboBox (OMFieldSelect). The default "owners" rule leaves an empty, +// hidden `
` first in the DOM, so scope to the +// container that actually holds a combobox — the pre-migration +// `.rule--field .ant-select` selector filtered these out implicitly. +const comboboxField = (scope: Page | Locator, className: string): Locator => + scope + .locator(className) + .filter({ has: scope.locator('input[role="combobox"]') }); + // Reusable rule fixture for tests that only need a visible rule card to exist // (cache-state tests, edit-discard test). Keeps the inline mock objects DRY. const PROBE_RULE: ContextRule = { @@ -547,9 +557,13 @@ test.describe.serial('Persona AI Context', () => { } await adminPage.getByTestId('add-context-condition').click(); + // Conjunction toggle is a react-aria ToggleButtonGroup with + // selectionMode="single", which renders a radiogroup whose items expose + // role="radio" (not button) — matching how CuratedAssets and the advanced + // search helper query the same toggle. const orOperator = adminPage .getByRole('dialog') - .getByRole('button', { name: 'Or', exact: true }); + .getByRole('radio', { name: 'Or', exact: true }); await expect(orOperator).toBeVisible(); await orOperator.click(); await expect(adminPage.getByTestId('delete-condition-button')).toHaveCount( @@ -1297,28 +1311,23 @@ test.describe.serial('Persona AI Context', () => { await adminPage.getByTestId('empty-add-context-rule').click(); await adminPage.getByTestId('add-context-condition').click(); - await adminPage - .locator('.rule--field .ant-select') - .first() - .waitFor({ state: 'visible' }); - await selectOption( - adminPage, - adminPage.locator('.rule--field .ant-select').first(), - 'Custom Properties', - true - ); + const fieldContainer = comboboxField(adminPage, '.rule--field').first(); + await fieldContainer.waitFor({ state: 'visible' }); + await selectOption(adminPage, fieldContainer, 'Custom Properties', true); - // The sub-field selector must appear — click it and verify our mocked property - // is listed and "No data" is absent. - const subFieldSelect = adminPage.locator('.rule--field .ant-select').last(); - await subFieldSelect.click(); - const dropdown = adminPage.locator('.ant-select-dropdown:visible').first(); - await dropdown.waitFor({ state: 'visible' }); - await expect(dropdown).toContainText('pw-context-enum-prop'); - await expect(dropdown).not.toContainText('No data'); + const fieldInput = fieldContainer.locator('input[role="combobox"]'); + await fieldInput.fill(''); + await fieldInput.press('ArrowDown'); - await adminPage.keyboard.press('Escape'); + await expect( + adminPage.getByRole('option', { name: 'pw-context-enum-prop' }) + ).toBeVisible(); + await expect( + adminPage.getByRole('option', { name: 'No data' }) + ).toHaveCount(0); + + await fieldInput.blur(); }); // Regression guard for the hasUnfinishedRule bug exercising the async-dropdown @@ -1367,25 +1376,16 @@ test.describe.serial('Persona AI Context', () => { .fill('service-is-regression-test'); await adminPage.getByTestId('add-context-condition').click(); - await adminPage - .locator('.rule--field .ant-select') - .first() - .waitFor({ state: 'visible' }); + const serviceField = comboboxField(adminPage, '.rule--field').first(); + await serviceField.waitFor({ state: 'visible' }); - await selectOption( - adminPage, - adminPage.locator('.rule--field .ant-select').first(), - 'Service', - true - ); + await selectOption(adminPage, serviceField, 'Service', true); - const operatorLocator = adminPage - .locator('.rule--operator .ant-select') - .first(); + const operatorLocator = comboboxField(adminPage, '.rule--operator').first(); await operatorLocator.waitFor({ state: 'visible', timeout: 5000 }); await selectOption(adminPage, operatorLocator, 'Is', false); - const valueSelect = adminPage.locator('.rule--widget .ant-select').first(); + const valueSelect = comboboxField(adminPage, '.rule--widget').first(); await valueSelect.waitFor({ state: 'visible' }); await selectOption(adminPage, valueSelect, dbService.entity.name, true); @@ -1661,11 +1661,11 @@ test.describe.serial('Persona AI Context', () => { adminPage.getByTestId('delete-condition-button') ).toHaveCount(2); - const firstField = drawer.locator('.rule--field .ant-select').first(); + const firstField = comboboxField(adminPage, '.rule--field').first(); await firstField.waitFor({ state: 'visible' }); await selectOption(adminPage, firstField, 'Description', true); - const firstOp = drawer.locator('.rule--operator .ant-select').first(); + const firstOp = comboboxField(adminPage, '.rule--operator').first(); await firstOp.waitFor({ state: 'visible', timeout: 5000 }); await selectOption(adminPage, firstOp, 'Contains', false); const alphaInput = drawer @@ -1691,10 +1691,10 @@ test.describe.serial('Persona AI Context', () => { adminPage.getByTestId('delete-condition-button') ).toHaveCount(3); - const secondField = drawer.locator('.rule--field .ant-select').last(); + const secondField = comboboxField(adminPage, '.rule--field').last(); await selectOption(adminPage, secondField, 'Description', true); - const secondOp = drawer.locator('.rule--operator .ant-select').last(); + const secondOp = comboboxField(adminPage, '.rule--operator').last(); await secondOp.waitFor({ state: 'visible', timeout: 5000 }); await selectOption(adminPage, secondOp, 'Contains', false); const betaInput = drawer @@ -1707,7 +1707,7 @@ test.describe.serial('Persona AI Context', () => { // Only now change the root conjunction to OR — this just flips the // conjunction on the existing two-rule group without any structural // change, so both alpha and beta remain in the serialized query. - await drawer.getByRole('button', { name: 'Or', exact: true }).click(); + await drawer.getByRole('radio', { name: 'Or', exact: true }).click(); }); const createRuleRequest = adminPage.waitForRequest( 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 a4e1415c2975..1208a31a9eaa 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 @@ -850,13 +850,13 @@ test.describe('Curated Assets – Description filter', () => { await selectOption( adminPage, - rule0.locator('.rule--field .ant-select'), + rule0.locator('.rule--field'), 'Description', true ); await selectOption( adminPage, - rule0.locator('.rule--operator .ant-select'), + rule0.locator('.rule--operator'), 'Contains' ); await rule0 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 index d7f915cb24a1..8ef643cc4a2b 100644 --- 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 @@ -943,26 +943,26 @@ ALL_ENTITIES.forEach(({ key, makeInstance }) => { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Custom Properties', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Table', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), durationPropertyName, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), CONDITIONS_MUST.equalTo.name ); @@ -985,7 +985,7 @@ ALL_ENTITIES.forEach(({ key, makeInstance }) => { await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), 'Contains' ); await inputElement.fill(partialSearchValue); @@ -1237,28 +1237,28 @@ ALL_ENTITIES.forEach(({ key, makeInstance }) => { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Custom Properties', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Table', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), propertyName, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), CONDITIONS_MUST.equalTo.name ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts index e2923800943a..796ff9a84c62 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts @@ -101,13 +101,13 @@ const fillSemanticsForm = async ( const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), semanticsData.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), semanticsData.rules[0].operator ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts index 0347c083f812..9cacf8016524 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts @@ -250,18 +250,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_SEMANTICS1.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), user.getUserDisplayName(), true ); @@ -272,13 +272,13 @@ test.describe('Data Contracts', () => { const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), DATA_CONTRACT_SEMANTICS1.rules[1].field, true ); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), + ruleLocator2.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[1].operator ); await page.getByTestId('save-semantic-button').click(); @@ -311,13 +311,13 @@ test.describe('Data Contracts', () => { const ruleLocator3 = page.locator('.group').nth(2); await selectOption( page, - ruleLocator3.locator('.group--field .ant-select'), + ruleLocator3.locator('.group--field'), DATA_CONTRACT_SEMANTICS2.rules[0].field, true ); await selectOption( page, - ruleLocator3.locator('.rule--operator .ant-select'), + ruleLocator3.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS2.rules[0].operator ); await page.getByTestId('save-semantic-button').click(); @@ -1024,18 +1024,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_CONTAIN_SEMANTICS.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_CONTAIN_SEMANTICS.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'Tier.Tier1', true ); @@ -1046,19 +1046,19 @@ test.describe('Data Contracts', () => { const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), DATA_CONTRACT_CONTAIN_SEMANTICS.rules[1].field, true ); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), + ruleLocator2.locator('.rule--operator'), DATA_CONTRACT_CONTAIN_SEMANTICS.rules[1].operator ); await selectOption( page, - ruleLocator2.locator('.rule--value .ant-select'), + ruleLocator2.locator('.rule--value'), testTag.responseData.name, true ); @@ -1072,19 +1072,19 @@ test.describe('Data Contracts', () => { const ruleLocator3 = page.locator('.rule').nth(2); await selectOption( page, - ruleLocator3.locator('.rule--field .ant-select'), + ruleLocator3.locator('.rule--field'), DATA_CONTRACT_CONTAIN_SEMANTICS.rules[2].field, true ); await selectOption( page, - ruleLocator3.locator('.rule--operator .ant-select'), + ruleLocator3.locator('.rule--operator'), DATA_CONTRACT_CONTAIN_SEMANTICS.rules[2].operator ); await selectOption( page, - ruleLocator3.locator('.rule--value .ant-select'), + ruleLocator3.locator('.rule--value'), testGlossaryTerm.responseData.name, true ); @@ -1217,18 +1217,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_NOT_CONTAIN_SEMANTICS.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_NOT_CONTAIN_SEMANTICS.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'Tier.Tier1', true ); @@ -1239,19 +1239,19 @@ test.describe('Data Contracts', () => { const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), DATA_CONTRACT_NOT_CONTAIN_SEMANTICS.rules[1].field, true ); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), + ruleLocator2.locator('.rule--operator'), DATA_CONTRACT_NOT_CONTAIN_SEMANTICS.rules[1].operator ); await selectOption( page, - ruleLocator2.locator('.rule--value .ant-select'), + ruleLocator2.locator('.rule--value'), testTag.responseData.name, true ); @@ -1265,19 +1265,19 @@ test.describe('Data Contracts', () => { const ruleLocator3 = page.locator('.rule').nth(2); await selectOption( page, - ruleLocator3.locator('.rule--field .ant-select'), + ruleLocator3.locator('.rule--field'), DATA_CONTRACT_NOT_CONTAIN_SEMANTICS.rules[2].field, true ); await selectOption( page, - ruleLocator3.locator('.rule--operator .ant-select'), + ruleLocator3.locator('.rule--operator'), DATA_CONTRACT_NOT_CONTAIN_SEMANTICS.rules[2].operator ); await selectOption( page, - ruleLocator3.locator('.rule--value .ant-select'), + ruleLocator3.locator('.rule--value'), testGlossaryTerm.responseData.name, true ); @@ -1598,18 +1598,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_SEMANTICS1.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'admin', true ); @@ -1620,13 +1620,13 @@ test.describe('Data Contracts', () => { const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), DATA_CONTRACT_SEMANTICS1.rules[1].field, true ); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), + ruleLocator2.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[1].operator ); await page.getByTestId('save-semantic-button').click(); @@ -1678,18 +1678,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_SEMANTICS1.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'admin', true ); @@ -1700,13 +1700,13 @@ test.describe('Data Contracts', () => { const ruleLocator2 = page.locator('.rule').nth(1); await selectOption( page, - ruleLocator2.locator('.rule--field .ant-select'), + ruleLocator2.locator('.rule--field'), DATA_CONTRACT_SEMANTICS1.rules[1].field, true ); await selectOption( page, - ruleLocator2.locator('.rule--operator .ant-select'), + ruleLocator2.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[1].operator ); await page.getByTestId('save-semantic-button').click(); @@ -1720,13 +1720,13 @@ test.describe('Data Contracts', () => { const ruleLocator3 = page.locator('.group').nth(2); await selectOption( page, - ruleLocator3.locator('.group--field .ant-select'), + ruleLocator3.locator('.group--field'), DATA_CONTRACT_SEMANTICS2.rules[0].field, true ); await selectOption( page, - ruleLocator3.locator('.rule--operator .ant-select'), + ruleLocator3.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS2.rules[0].operator ); await page.getByTestId('save-semantic-button').click(); @@ -1772,18 +1772,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_SEMANTICS1.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'admin', true ); @@ -1832,18 +1832,18 @@ test.describe('Data Contracts', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_SEMANTICS1.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'admin', true ); @@ -1858,13 +1858,13 @@ test.describe('Data Contracts', () => { const ruleLocator3 = page.locator('.group').nth(2); await selectOption( page, - ruleLocator3.locator('.group--field .ant-select'), + ruleLocator3.locator('.group--field'), DATA_CONTRACT_SEMANTICS2.rules[0].field, true ); await selectOption( page, - ruleLocator3.locator('.rule--operator .ant-select'), + ruleLocator3.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS2.rules[0].operator ); await page.getByTestId('save-semantic-button').click(); @@ -2369,18 +2369,18 @@ entitiesWithDataContracts.forEach((EntityClass) => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), DATA_CONTRACT_SEMANTICS1.rules[0].field, true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTICS1.rules[0].operator ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), 'admin', true ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractsSemanticRules.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractsSemanticRules.spec.ts index e7cf61eb9323..0336c30aebb6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractsSemanticRules.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractsSemanticRules.spec.ts @@ -104,18 +104,18 @@ test.describe('Data Contracts Semantics Rule Owner', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), team.responseData.displayName, true ); @@ -198,18 +198,18 @@ test.describe('Data Contracts Semantics Rule Owner', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), user.getUserDisplayName(), true ); @@ -299,18 +299,18 @@ test.describe('Data Contracts Semantics Rule Owner', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.any_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), user.getUserDisplayName(), true ); @@ -399,18 +399,18 @@ test.describe('Data Contracts Semantics Rule Owner', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.not_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), user.getUserDisplayName(), true ); @@ -492,13 +492,13 @@ test.describe('Data Contracts Semantics Rule Owner', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_set ); @@ -571,13 +571,13 @@ test.describe('Data Contracts Semantics Rule Owner', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not_set ); @@ -652,13 +652,13 @@ test.describe('Data Contracts Semantics Rule Description', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Description', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.contains ); @@ -739,13 +739,13 @@ test.describe('Data Contracts Semantics Rule Description', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Description', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.not_contains ); const inputElement = ruleLocator.locator( @@ -824,13 +824,13 @@ test.describe('Data Contracts Semantics Rule Description', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Description', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_set ); @@ -908,13 +908,13 @@ test.describe('Data Contracts Semantics Rule Description', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Description', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not_set ); @@ -1037,18 +1037,18 @@ test.describe('Data Contracts Semantics Rule Domain', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Domain', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), domain1.responseData.name, true ); @@ -1119,18 +1119,18 @@ test.describe('Data Contracts Semantics Rule Domain', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Domain', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), domain2.responseData.name, true ); @@ -1202,18 +1202,18 @@ test.describe('Data Contracts Semantics Rule Domain', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Domain', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.any_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), domain1.responseData.name, true ); @@ -1283,18 +1283,18 @@ test.describe('Data Contracts Semantics Rule Domain', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Domain', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.not_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), domain1.responseData.name, true ); @@ -1364,13 +1364,13 @@ test.describe('Data Contracts Semantics Rule Domain', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Domain', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_set ); @@ -1437,13 +1437,13 @@ test.describe('Data Contracts Semantics Rule Domain', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Domain', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not_set ); @@ -1516,13 +1516,13 @@ test.describe('Data Contracts Semantics Rule Version', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Version', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is ); @@ -1535,7 +1535,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { // (which starts at 0.1), ensuring the second edit always produces a diff // and the save button stays enabled. await ruleLocator - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input') + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ) .fill('99.9'); contractId = ( @@ -1566,7 +1568,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { const versionInput = page .locator('.group') .first() - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input'); + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ); await versionInput.clear(); await versionInput.fill(actualVersion); @@ -1629,13 +1633,13 @@ test.describe('Data Contracts Semantics Rule Version', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Version', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not ); @@ -1648,7 +1652,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { // (which starts at 0.1), ensuring the second edit always produces a diff // and the save button stays enabled. await ruleLocator - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input') + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ) .fill('99.9'); contractId = ( @@ -1684,7 +1690,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { const versionInput = page .locator('.group') .first() - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input'); + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ); await versionInput.clear(); await versionInput.fill(domainBumpedVersion); @@ -1743,20 +1751,22 @@ test.describe('Data Contracts Semantics Rule Version', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Version', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.less ); // Use 99.9 — any realistic entity version is always below this, so the // check passes regardless of how many version bumps CI introduces. await ruleLocator - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input') + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ) .fill('99.9'); await saveAndTriggerDataContractValidation(page, true); @@ -1778,7 +1788,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { const versionInput = page .locator('.group') .first() - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input'); + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ); await versionInput.clear(); await versionInput.fill('0.01'); @@ -1820,20 +1832,22 @@ test.describe('Data Contracts Semantics Rule Version', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Version', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.greater ); // Use 99.9 — any realistic entity version is always below this, so // entity_version > 99.9 always fails regardless of version bumps in CI. await ruleLocator - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input') + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ) .fill('99.9'); await saveAndTriggerDataContractValidation(page, true); @@ -1856,7 +1870,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { const versionInput = page .locator('.group') .first() - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input'); + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ); await versionInput.clear(); await versionInput.fill('0.01'); @@ -1900,20 +1916,22 @@ test.describe('Data Contracts Semantics Rule Version', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Version', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.less_equal ); // Use 99.9 — any realistic entity version is always below this, so // entity_version <= 99.9 always passes regardless of version bumps in CI. await ruleLocator - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input') + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ) .fill('99.9'); await saveAndTriggerDataContractValidation(page, true); @@ -1935,7 +1953,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { const versionInput = page .locator('.group') .first() - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input'); + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ); await versionInput.clear(); await versionInput.fill('0.01'); @@ -1980,20 +2000,22 @@ test.describe('Data Contracts Semantics Rule Version', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Version', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.greater_equal ); // Use 99.9 — any realistic entity version is always below this, so // entity_version >= 99.9 always fails regardless of version bumps in CI. await ruleLocator - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input') + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ) .fill('99.9'); await saveAndTriggerDataContractValidation(page, true); @@ -2015,7 +2037,9 @@ test.describe('Data Contracts Semantics Rule Version', () => { const versionInput = page .locator('.group') .first() - .locator('.rule--value .rule--widget--NUMBER .ant-input-number-input'); + .locator( + '.rule--value .rule--widget--NUMBER input[data-testid="qb-number-input"]' + ); await versionInput.clear(); await versionInput.fill('0.01'); @@ -2084,19 +2108,19 @@ test.describe('Data Contracts Semantics Rule DataProduct', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Data Product', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), createdDataProducts[0].responseData.name, true ); @@ -2179,19 +2203,19 @@ test.describe('Data Contracts Semantics Rule DataProduct', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Data Product', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), createdDataProducts[0].responseData.name, true ); @@ -2274,18 +2298,18 @@ test.describe('Data Contracts Semantics Rule DataProduct', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Data Product', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.any_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), createdDataProducts[0].responseData.name, true ); @@ -2367,18 +2391,18 @@ test.describe('Data Contracts Semantics Rule DataProduct', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Data Product', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.not_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), createdDataProducts[0].responseData.name, true ); @@ -2495,13 +2519,13 @@ test.describe('Data Contracts Semantics Rule DataProduct', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Data Product', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_set ); @@ -2576,13 +2600,13 @@ test.describe('Data Contracts Semantics Rule DataProduct', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Data Product', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not_set ); @@ -2663,19 +2687,19 @@ test.describe('Data Contracts Semantics Rule DisplayName', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), table.entityResponseData.displayName || '', true ); @@ -2750,19 +2774,19 @@ test.describe('Data Contracts Semantics Rule DisplayName', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), table.entityResponseData.displayName || '', true ); @@ -2836,18 +2860,18 @@ test.describe('Data Contracts Semantics Rule DisplayName', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.any_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), table.entityResponseData.displayName || '', true ); @@ -2922,18 +2946,18 @@ test.describe('Data Contracts Semantics Rule DisplayName', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.not_in ); await selectOption( page, - ruleLocator.locator('.rule--value .ant-select'), + ruleLocator.locator('.rule--value'), table.entityResponseData.displayName || '', true ); @@ -3007,13 +3031,13 @@ test.describe('Data Contracts Semantics Rule DisplayName', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_set ); @@ -3088,13 +3112,13 @@ test.describe('Data Contracts Semantics Rule DisplayName', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Display Name', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.is_not_set ); @@ -3168,20 +3192,20 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Updated on', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.between ); - const startDate = customFormatDateTime(getCurrentMillis(), 'dd.MM.yyyy'); + const startDate = customFormatDateTime(getCurrentMillis(), 'yyyy-MM-dd'); const endDate = customFormatDateTime( getEpochMillisForFutureDays(5), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await selectRange(page, ruleLocator, startDate, endDate); @@ -3210,11 +3234,14 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const newStart = customFormatDateTime( getEpochMillisForFutureDays(1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); - page.getByRole('textbox', { name: 'Enter date from' }).fill(newStart); - await page.press('.ant-picker-input-active input', 'Enter'); - await page.press('.ant-picker-input-active input', 'Enter'); + await page + .locator('.group') + .nth(0) + .locator('.rule--value input[type="date"]') + .nth(0) + .fill(newStart); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3255,20 +3282,20 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Updated on', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.not_between ); - const startDate = customFormatDateTime(getCurrentMillis(), 'dd.MM.yyyy'); + const startDate = customFormatDateTime(getCurrentMillis(), 'yyyy-MM-dd'); const endDate = customFormatDateTime( getEpochMillisForFutureDays(5), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await selectRange(page, ruleLocator, startDate, endDate); @@ -3298,23 +3325,14 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const newStart = customFormatDateTime( getEpochMillisForFutureDays(1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await page .locator('.group') .nth(0) - .locator('.rule--value .ant-picker-range') - .click(); - - await page.locator('.ant-picker-dropdown-range').waitFor({ - state: 'visible', - }); - - await page - .getByRole('textbox', { name: 'Enter date from' }) + .locator('.rule--value input[type="date"]') + .nth(0) .fill(newStart); - await page.press('.ant-picker-input-active input', 'Enter'); - await page.press('.ant-picker-input-active input', 'Enter'); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3354,24 +3372,19 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Updated on', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.less ); - const date = customFormatDateTime(getCurrentMillis(), 'dd.MM.yyyy'); + const date = customFormatDateTime(getCurrentMillis(), 'yyyy-MM-dd'); - await ruleLocator.locator('.rule--value .ant-picker').click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(date); - await page.press('.ant-picker-input input', 'Enter'); + await ruleLocator.locator('.rule--value input[type="date"]').fill(date); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3398,19 +3411,14 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const newDate = customFormatDateTime( getEpochMillisForFutureDays(1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await page .locator('.group') .nth(0) - .locator('.rule--value .ant-picker') - .click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(newDate); - await page.press('.ant-picker-input input', 'Enter'); + .locator('.rule--value input[type="date"]') + .fill(newDate); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3450,27 +3458,22 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Updated on', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.greater ); const date = customFormatDateTime( getEpochMillisForFutureDays(1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); - await ruleLocator.locator('.rule--value .ant-picker').click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(date); - await page.press('.ant-picker-input input', 'Enter'); + await ruleLocator.locator('.rule--value input[type="date"]').fill(date); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3497,19 +3500,14 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const newDate = customFormatDateTime( getEpochMillisForFutureDays(-1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await page .locator('.group') .nth(0) - .locator('.rule--value .ant-picker') - .click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(newDate); - await page.press('.ant-picker-input input', 'Enter'); + .locator('.rule--value input[type="date"]') + .fill(newDate); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3549,27 +3547,22 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Updated on', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.less_equal ); const date = customFormatDateTime( getEpochMillisForFutureDays(1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); - await ruleLocator.locator('.rule--value .ant-picker').click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(date); - await page.press('.ant-picker-input input', 'Enter'); + await ruleLocator.locator('.rule--value input[type="date"]').fill(date); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3595,19 +3588,14 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const newDate = customFormatDateTime( getEpochMillisForFutureDays(-1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await page .locator('.group') .nth(0) - .locator('.rule--value .ant-picker') - .click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(newDate); - await page.press('.ant-picker-input input', 'Enter'); + .locator('.rule--value input[type="date"]') + .fill(newDate); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3651,27 +3639,22 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Updated on', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), DATA_CONTRACT_SEMANTIC_OPERATIONS.greater_equal ); const date = customFormatDateTime( getEpochMillisForFutureDays(-1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); - await ruleLocator.locator('.rule--value .ant-picker').click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(date); - await page.press('.ant-picker-input input', 'Enter'); + await ruleLocator.locator('.rule--value input[type="date"]').fill(date); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3697,19 +3680,14 @@ test.describe('Data Contracts Semantics Rule Updated on', () => { const newDate = customFormatDateTime( getEpochMillisForFutureDays(1), - 'dd.MM.yyyy' + 'yyyy-MM-dd' ); await page .locator('.group') .nth(0) - .locator('.rule--value .ant-picker') - .click(); - await page.locator('.ant-picker-dropdown').waitFor({ - state: 'visible', - }); - await page.locator('.ant-picker-input input').fill(newDate); - await page.press('.ant-picker-input input', 'Enter'); + .locator('.rule--value input[type="date"]') + .fill(newDate); // save and trigger contract validation await saveAndTriggerDataContractValidation(page, true); @@ -3764,13 +3742,13 @@ test.describe('Data Contract - Semantics Fields Validation', () => { const ruleLocator = page.locator('.group').nth(0); await selectOption( page, - ruleLocator.locator('.group--field .ant-select'), + ruleLocator.locator('.group--field'), 'Owners', true ); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), 'Is Set' ); }); @@ -3790,17 +3768,8 @@ test.describe('Data Contract - Semantics Fields Validation', () => { }); await test.step('select Is Set operator and error is hidden', async () => { - await selectOption( - page, - page.locator('.rule--field .ant-select'), - 'Owners', - true - ); - await selectOption( - page, - page.locator('.rule--operator .ant-select'), - 'Is Set' - ); + await selectOption(page, page.locator('.rule--field'), 'Owners', true); + await selectOption(page, page.locator('.rule--operator'), 'Is Set'); await expect(page.getByText(/rule is required/i)).not.toBeVisible(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataMarketplace.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataMarketplace.spec.ts index d711e936791b..1d12c9a50a08 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataMarketplace.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataMarketplace.spec.ts @@ -21,7 +21,11 @@ import { navigateToMarketplace, searchMarketplace, } from '../../utils/dataMarketplace'; -import { fillCommonFormItems, fillDomainForm } from '../../utils/domain'; +import { + clickDrawerSave, + fillCommonFormItems, + fillDomainForm, +} from '../../utils/domain'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; import { test } from '../fixtures/pages'; @@ -173,7 +177,10 @@ test.describe( response.url().includes('/api/v1/dataProducts') && response.request().method() === 'POST' ); - await page.getByTestId('save-btn').click(); + const saveBtn = page.getByTestId('save-btn'); + await expect(saveBtn).toBeVisible(); + await saveBtn.focus(); + await page.keyboard.press('Enter'); const response = await createResponse; expect(response.status()).toBe(201); }); @@ -214,7 +221,7 @@ test.describe( response.url().includes('/api/v1/domains') && response.request().method() === 'POST' ); - await page.getByTestId('save-btn').click(); + await clickDrawerSave(page); const response = await createResponse; expect(response.status()).toBe(201); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/IntakeForm.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/IntakeForm.spec.ts index 810394e0478f..5ae24cdcf8ed 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/IntakeForm.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/IntakeForm.spec.ts @@ -17,7 +17,7 @@ import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { performAdminLogin } from '../../utils/admin'; import { descriptionBox, redirectToHomePage, uuid } from '../../utils/common'; -import { fillDomainForm } from '../../utils/domain'; +import { clickDrawerSave, fillDomainForm } from '../../utils/domain'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; import { openAddGlossaryTermModal } from '../../utils/glossary'; import { sidebarClick } from '../../utils/sidebar'; @@ -210,7 +210,16 @@ const selectExtensionReference = async ({ .first(); await expect(input).toBeVisible({ timeout: 15000 }); - await input.click(); + // Use force:true to bypass Playwright's element-stability check. The + // reference-picker input lives inside a drawer whose body re-renders while + // the form settles, causing the input's bounding box to shift. A plain + // click() would spin until the test times out ("element is not stable"); + // force:true skips that check while still dispatching the pointer events + // that activate the ComboBox popup. Do NOT use focus() here — opening the + // popup without pointer activation leaves the ComboBox in a state where the + // subsequent option.click() triggers unexpected re-renders that continuously + // detach the option element, causing a 3-minute hang. + await input.click({ force: true }); await input.fill(query); await searchResponse; @@ -629,7 +638,7 @@ test.describe( } }; page.on('response', postListener); - await page.getByTestId('save-btn').click(); + await clickDrawerSave(page); // Poll for up to 3s and confirm no POST ever fires. We intentionally // avoid `page.waitForTimeout` (linted as flaky) and instead use @@ -1073,7 +1082,7 @@ test.describe( r.url().endsWith('/api/v1/dataProducts') && r.request().method() === 'POST' ); - await page.getByTestId('save-btn').click(); + await clickDrawerSave(page); const response = await createResponse; expect(response.status()).toBe(201); @@ -1414,7 +1423,7 @@ test.describe( } }; page.on('request', trackCreateRequest); - await page.getByTestId('save-btn').click(); + await clickDrawerSave(page); await expect( page.getByText('URL must use http or https protocol') ).toBeVisible(); @@ -1433,7 +1442,7 @@ test.describe( response.url().endsWith('/api/v1/dataProducts') && response.request().method() === 'POST' ); - await page.getByTestId('save-btn').click(); + await clickDrawerSave(page); const request = await createRequest; const response = await createResponse; @@ -1563,7 +1572,7 @@ test.describe( response.url().endsWith('/api/v1/domains') && response.request().method() === 'POST' ); - await page.getByTestId('save-btn').click(); + await clickDrawerSave(page); const request = await createRequest; const response = await createResponse; 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..cf4c9d464a6f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/advancedSearch.ts @@ -12,7 +12,7 @@ */ import { expect, Locator, Page } from '@playwright/test'; import { clickOutside } from './common'; -import { escapeESReservedCharacters, getEncodedFqn } from './entity'; +import { getEncodedFqn } from './entity'; type EntityFields = { id: string; @@ -174,52 +174,86 @@ export const selectOption = async ( optionTitle: string, isSearchable = false ) => { - if (isSearchable) { - // Wait for dropdown to be visible before clicking - const selector = dropdownLocator.locator('.ant-select-selector'); - await expect(selector).toBeVisible(); - await selector.click(); - - await dropdownLocator - .locator('.ant-select-arrow-loading svg[data-icon="loading"]') - .waitFor({ state: 'detached' }); - - // Clear any existing input and type the new value - const combobox = dropdownLocator.getByRole('combobox'); - await combobox.clear(); - - await dropdownLocator - .locator('.ant-select-arrow-loading svg[data-icon="loading"]') - .waitFor({ state: 'detached' }); + const comboboxInput = dropdownLocator.locator('input[role="combobox"]'); + const triggerButton = dropdownLocator.locator( + 'button[aria-haspopup="listbox"]' + ); - await combobox.fill(optionTitle); + await expect(comboboxInput.or(triggerButton).first()).toBeVisible(); - await dropdownLocator - .locator('.ant-select-arrow-loading svg[data-icon="loading"]') - .waitFor({ state: 'detached' }); + if (isSearchable) { + if ((await triggerButton.count()) === 0) { + // MultiSelect: no chevron overlays the input, so clicking is safe — + // and required, since its popup opens from a mousedown handler. + await comboboxInput.click(); + } + // Single fill (no clear first) — one input event, one async fetch. + await comboboxInput.fill(optionTitle); + // React Aria may close the focus-opened popup while processing the atomic + // fill; ArrowDown deterministically (re)opens it with the filter applied. + await comboboxInput.press('ArrowDown'); + } else if ((await comboboxInput.count()) > 0) { + // Select.ComboBox: fill('') focuses the input (menuTrigger="focus" opens + // the popup) and clears the current-label filter so all options show. + // Never pointer-click the input — in narrow ComboBoxes (e.g. the RAQB + // operator column) the absolutely positioned chevron button covers the + // input's center and intercepts the click, hanging actionability retries. + await comboboxInput.fill(''); + await comboboxInput.press('ArrowDown'); } else { - await dropdownLocator.click(); + // Plain Select (no combobox input): click the trigger button to open. + await triggerButton.click(); } - await expect(dropdownLocator).toHaveClass(/(^|\s)ant-select-focused(\s|$)/); - - await page.locator('.ant-select-dropdown:visible').first().waitFor({ - state: 'visible', - }); - - // CRITICAL: Use :visible selector chain pattern (Rule 4 from deflake guide) - // Use .first() to handle multiple matches (acceptable when scoped to visible dropdown) - const optionLocator = page - .locator('.ant-select-dropdown:visible') - .getByTitle(optionTitle, { exact: true }) - .first(); - await expect(optionLocator).toBeVisible(); - - // Wait for dropdown animations to settle before clicking - // This prevents "element detached from DOM" errors during re-renders - // eslint-disable-next-line playwright/no-wait-for-timeout -- dropdown animation settling - await page.waitForTimeout(100); - await optionLocator.click({ timeout: 10000 }); + // Scope the popup to THIS control via aria-controls (react-aria sets it + // while expanded). Popovers portal to , so a global + // [role="listbox"]:visible could match a popup left open by a previous + // interaction (MultiSelect keeps its popup open by design). The popup can + // also close and reopen under a new id while the builder re-renders, so + // re-resolve it (and reopen if needed) on every retry. + const control = comboboxInput.or(triggerButton).first(); + await expect(async () => { + if ((await control.getAttribute('aria-expanded')) !== 'true') { + await control.press('ArrowDown'); + } + const listboxId = await control.getAttribute('aria-controls'); + if (!listboxId) { + throw new Error('Combobox popup did not open (aria-controls not set)'); + } + const option = page + .locator(`[role="listbox"][id="${listboxId}"]`) + .getByRole('option', { name: optionTitle, exact: true }) + .first(); + if (isSearchable && (await option.count()) === 0) { + await comboboxInput.fill(''); + await comboboxInput.fill(optionTitle); + throw new Error(`Option "${optionTitle}" not present yet; re-searched`); + } + await option.click({ timeout: 2000 }); + }).toPass({ timeout: 30000 }); + + // Close the popup if the click didn't: re-selecting the current value emits + // no selection change (so the popup stays open) and MultiSelect popups stay + // open by design — either would pollute the next interaction's locators. + // The control itself may be GONE by now (selecting a field can morph the + // whole rule row), which also unmounts its popup — tolerate that. + const openListboxId = await control + .getAttribute('aria-controls', { timeout: 1000 }) + .catch(() => null); + if (openListboxId) { + const openListbox = page.locator(`[role="listbox"][id="${openListboxId}"]`); + await openListbox + .waitFor({ state: 'hidden', timeout: 2000 }) + .catch(async () => { + // Blur the control — react-aria comboboxes close their popup when + // focus leaves. NEVER send Escape here: surrounding antd modals and + // forms handle Escape in the capture phase and dismiss themselves. + await control.blur({ timeout: 1000 }).catch(() => undefined); + await openListbox + .waitFor({ state: 'hidden', timeout: 1000 }) + .catch(() => undefined); + }); + } }; export const selectRange = async ( @@ -228,16 +262,14 @@ export const selectRange = async ( startDate: string, endDate: string ) => { - await ruleLocator.locator('.rule--value .ant-picker-range').click(); - - await page.locator('.ant-picker-dropdown-range').waitFor({ - state: 'visible', - }); - - await page.locator('.ant-picker-input-active input').fill(startDate); - await page.press('.ant-picker-input-active input', 'Enter'); - await page.locator('.ant-picker-input-active input').fill(endDate); - await page.press('.ant-picker-input-active input', 'Enter'); + await ruleLocator + .locator('.rule--value input[type="date"]') + .nth(0) + .fill(startDate); + await ruleLocator + .locator('.rule--value input[type="date"]') + .nth(1) + .fill(endDate); }; export const fillRule = async ( @@ -260,19 +292,10 @@ export const fillRule = async ( const ruleLocator = page.locator('.rule').nth(index - 1); // Perform click on rule field - await selectOption( - page, - ruleLocator.locator('.rule--field .ant-select'), - field.id, - true - ); + await selectOption(page, ruleLocator.locator('.rule--field'), field.id, true); // Perform click on operator - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - condition - ); + await selectOption(page, ruleLocator.locator('.rule--operator'), condition); if (searchCriteria) { const inputElement = ruleLocator.locator( @@ -284,47 +307,58 @@ export const fillRule = async ( await inputElement.fill(searchData); } else { const dropdownInput = ruleLocator.locator( - '.widget--widget > .ant-select > .ant-select-selector input' - ); - - const aggregateRes1 = page.waitForResponse('/api/v1/search/aggregate?*'); - - await dropdownInput.click(); - - await aggregateRes1; - - const aggregateRes2 = page.waitForResponse( - `/api/v1/search/aggregate?*${getEncodedFqn( - escapeESReservedCharacters(searchData) - )}*` + '.widget--widget input[role="combobox"]' ); - await dropdownInput.fill(searchData); - - await aggregateRes2; + const countMatchingOptions = async () => { + const listboxId = await dropdownInput.getAttribute('aria-controls'); + if (!listboxId) { + return 0; + } - const dropdown = page.locator('.ant-select-dropdown:visible'); - const exactTitleMatch = dropdown - .locator('[title]') - .filter({ - hasText: new RegExp(`^${escapeRegex(searchData)}$`, 'i'), - }) - .first(); - const partialTextMatch = dropdown - .locator('.ant-select-item-option-content') - .filter({ - hasText: new RegExp(escapeRegex(searchData), 'i'), + return page + .locator(`[role="listbox"][id="${listboxId}"]`) + .getByRole('option') + .filter({ hasText: new RegExp(escapeRegex(searchData), 'i') }) + .count(); + }; + + await expect + .poll( + async () => { + await dropdownInput.fill(''); + await dropdownInput.fill(searchData); + + await page + .waitForResponse( + (response) => + response.url().includes('/api/v1/search/aggregate'), + { timeout: 5_000 } + ) + .catch(() => null); + + return countMatchingOptions(); + }, + { timeout: 30_000, intervals: [1_000, 2_000, 3_000] } + ) + .toBeGreaterThan(0); + + const listboxId = await dropdownInput.getAttribute('aria-controls'); + const dropdown = page.locator(`[role="listbox"][id="${listboxId}"]`); + const exactMatch = dropdown + .getByRole('option', { + name: new RegExp(`^${escapeRegex(searchData)}$`, 'i'), }) .first(); - if (await exactTitleMatch.count()) { - await exactTitleMatch.click(); - } else if (await partialTextMatch.count()) { - await partialTextMatch.click(); + if (await exactMatch.count()) { + await exactMatch.click(); } else { - // Some suggestion backends normalize or delay option text; Enter keeps - // the typed criteria and avoids waiting forever on an exact title match. - await dropdownInput.press('Enter'); + await dropdown + .getByRole('option') + .filter({ hasText: new RegExp(escapeRegex(searchData), 'i') }) + .first() + .click(); } } @@ -579,9 +613,11 @@ export const checkAddRuleOrGroupWithOperator = async ( }); if (operator === 'OR') { + // Conjunction toggle is a react-aria ToggleButtonGroup (selectionMode + // "single"), which exposes role="radio" items — not buttons. await page .getByTestId('advanced-search-modal') - .getByRole('button', { name: 'Or' }) + .getByRole('radio', { name: 'Or' }) .click(); } @@ -664,28 +700,39 @@ export const runRuleGroupTestsWithNonExistingValue = async (page: Page) => { // Perform click on rule field await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Database', true ); - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - '==' - ); + await selectOption(page, ruleLocator.locator('.rule--operator'), '=='); const inputElement = ruleLocator.locator( - '.rule--widget--SELECT .ant-select-selection-search-input' + '.rule--widget--SELECT input[role="combobox"]' ); + await inputElement.fill('non-existing-value'); - const dropdownText = page.locator('.ant-select-item-empty'); + await inputElement.press('ArrowDown'); + + // Scope to this input's own popup — a popup from a previous step may + // still be visible, which would break a global :visible locator. + let listboxId: string | null = null; + await expect(async () => { + listboxId = await inputElement.getAttribute('aria-controls'); + if (!listboxId) { + throw new Error('Combobox popup did not open (aria-controls not set)'); + } + }).toPass({ timeout: 15000 }); + + const listbox = page.locator(`[role="listbox"][id="${listboxId}"]`); - await expect(dropdownText).toContainText('Loading...'); + await expect(listbox).toBeVisible(); // eslint-disable-next-line playwright/no-wait-for-timeout -- search debounce delay await page.waitForTimeout(1000); - await expect(dropdownText).not.toContainText('Loading...'); + // allowsEmptyCollection keeps the popup open and renders the "No data" + // empty state (as an option row) instead of an empty listbox. + await expect(listbox.getByText('No data')).toBeVisible(); }; // For fields backed by hard-coded listValues (no aggregate API call), options are @@ -709,20 +756,12 @@ export const fillStaticListRule = async ( await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), fieldLabel, true ); - await selectOption( - page, - ruleLocator.locator('.rule--operator .ant-select'), - condition - ); - await selectOption( - page, - ruleLocator.locator('.widget--widget > .ant-select'), - value - ); + await selectOption(page, ruleLocator.locator('.rule--operator'), condition); + await selectOption(page, ruleLocator.locator('.widget--widget'), value); }; export const getFieldsSuggestionSearchText = ( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts index 9091264d922e..4edefb04cfb2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts @@ -949,7 +949,7 @@ export const verifyCustomPropertyInAdvancedSearch = async ( // Select "Custom Properties" from the field dropdown await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Custom Properties', true ); @@ -957,7 +957,7 @@ export const verifyCustomPropertyInAdvancedSearch = async ( if (entityType !== 'TableColumn') { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), entityType, true ); @@ -965,26 +965,26 @@ export const verifyCustomPropertyInAdvancedSearch = async ( if (propertyType === 'Time Interval') { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), `${propertyName} (Start)`, true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), `${propertyName} (End)`, true ); } else if (propertyType === 'Hyperlink') { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), `${propertyName} URL`, true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), `${propertyName} Display Text`, true ); @@ -992,7 +992,7 @@ export const verifyCustomPropertyInAdvancedSearch = async ( for (const column of propertyConfig ?? []) { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), `${propertyName} - ${column}`, true ); @@ -1000,7 +1000,7 @@ export const verifyCustomPropertyInAdvancedSearch = async ( } else { await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), propertyName, true ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customPropertyAdvancedSearchUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customPropertyAdvancedSearchUtils.ts index fce8fcc3f64c..5f1457188597 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customPropertyAdvancedSearchUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customPropertyAdvancedSearchUtils.ts @@ -400,6 +400,55 @@ export const getOperatorLabel = (operator: string): string => { return operatorMap[operator] || operator; }; +// The query builder renders native date/datetime inputs, which only accept +// ISO-like fill values — convert from the CP display format +// (dd-MM-yyyy[ HH:mm:ss]). Detection is pattern-based because several call +// sites don't pass propertyType. +const DATE_DISPLAY_PATTERN = /^(\d{2})-(\d{2})-(\d{4})$/; +const DATE_TIME_DISPLAY_PATTERN = + /^(\d{2})-(\d{2})-(\d{4}) (\d{2}:\d{2}:\d{2})$/; + +const toNativeDateInputValue = (value: string | number): string => { + const stringValue = String(value); + let result = stringValue; + + const dateTimeMatch = stringValue.match(DATE_TIME_DISPLAY_PATTERN); + const dateMatch = stringValue.match(DATE_DISPLAY_PATTERN); + + if (dateTimeMatch) { + const [, day, month, year, time] = dateTimeMatch; + result = `${year}-${month}-${day}T${time}`; + } else if (dateMatch) { + const [, day, month, year] = dateMatch; + result = `${year}-${month}-${day}`; + } + + return result; +}; + +// Playwright's fill() rejects datetime-local values that carry seconds even +// when the input has step=1, so converted date values are written through the +// native value setter (fill() is used for everything else). +const fillPropertyValue = async ( + input: ReturnType, + value: string | number +) => { + const nativeValue = toNativeDateInputValue(value); + + if (nativeValue === String(value)) { + await input.fill(String(value)); + } else { + await input.evaluate((element, val) => { + const setter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set; + setter?.call(element, val); + element.dispatchEvent(new Event('input', { bubbles: true })); + }, nativeValue); + } +}; + const handlePropertyValueInput = async ( page: Page, ruleLocator: ReturnType, @@ -413,7 +462,7 @@ const handlePropertyValueInput = async ( // Fill the input only if it's visible if (await inputElement.isVisible()) { // Convert object values to JSON strings - const stringValue = isObject(value) ? JSON.stringify(value) : String(value); + const stringValue = isObject(value) ? JSON.stringify(value) : value; const apiResponsePromise = isEntityRefProperty ? page.waitForResponse('/api/v1/search/aggregate?*value=.%2A*') @@ -425,11 +474,15 @@ const handlePropertyValueInput = async ( await apiResponsePromise; } - await inputElement.fill(stringValue); + await fillPropertyValue(inputElement, stringValue); - // Press Enter for multiselect operators and date types - if ( - MULTISELECT_OPERATORS.includes(operator) || + if (MULTISELECT_OPERATORS.includes(operator)) { + await page + .locator('[role="listbox"]:visible') + .getByRole('option', { name: String(value), exact: true }) + .first() + .click(); + } else if ( ((operator === 'equal' || operator === 'not_equal') && propertyType === 'dateTime-cp') || propertyType === 'date-cp' @@ -440,7 +493,8 @@ const handlePropertyValueInput = async ( // Handle entity reference selection if (isEntityRefProperty) { await page - .locator(`.ant-select-dropdown:visible [title*="${value as string}"]`) + .locator('[role="listbox"]:visible [role="option"]') + .filter({ hasText: value as string }) .first() .click(); } @@ -459,21 +513,21 @@ export const applyCustomPropertyFilter = async ( await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), 'Custom Properties', true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), entityType, true ); await selectOption( page, - ruleLocator.locator('.rule--field .ant-select'), + ruleLocator.locator('.rule--field'), propertyName, true ); @@ -481,7 +535,7 @@ export const applyCustomPropertyFilter = async ( const operatorLabel = getOperatorLabel(operator); await selectOption( page, - ruleLocator.locator('.rule--operator .ant-select'), + ruleLocator.locator('.rule--operator'), operatorLabel ); @@ -495,9 +549,9 @@ export const applyCustomPropertyFilter = async ( const endInput = ruleLocator.locator('.rule--value input').last(); await startInput.click(); - await startInput.fill(String(rangeValue.start)); + await fillPropertyValue(startInput, rangeValue.start); await endInput.click(); - await endInput.fill(String(rangeValue.end)); + await fillPropertyValue(endInput, rangeValue.end); await page.keyboard.press('Enter'); } else { @@ -531,28 +585,37 @@ export const verifySearchResults = async ( if (shouldBeVisible) { if (!(await dashboardCard.isVisible())) { - await expect - .poll( - async () => { - const retryResponse = await page.request.get(response.url()); - - if (!retryResponse.ok()) { - return false; + // Re-query the backend through an authenticated context. `page.request` + // shares cookies but NOT the app's `Authorization: Bearer ` header, + // so re-fetching the search URL with it returns 401 "Token not present" + // and the poll would never resolve. getApiContext attaches the token. + const { apiContext, afterAction } = await getApiContext(page); + try { + await expect + .poll( + async () => { + const retryResponse = await apiContext.get(response.url()); + + if (!retryResponse.ok()) { + return false; + } + + const searchData = + (await retryResponse.json()) as SearchResponseData; + + return searchData.hits.hits.some( + (hit) => hit._source?.fullyQualifiedName === dashboardFQN + ); + }, + { + intervals: [1000, 2000, 5000], + timeout: 30000, } - - const searchData = - (await retryResponse.json()) as SearchResponseData; - - return searchData.hits.hits.some( - (hit) => hit._source?.fullyQualifiedName === dashboardFQN - ); - }, - { - intervals: [1000, 2000, 5000], - timeout: 30000, - } - ) - .toBe(true); + ) + .toBe(true); + } finally { + await afterAction(); + } await showAdvancedSearchDialog(page); const retrySearchResponse = page.waitForResponse(searchResponsePattern); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 5110e6b90d45..b3f5ee5852f0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -618,6 +618,25 @@ export const fillDomainForm = async ( .click(); }; +/** + * Submit the AddDomain/AddDataProduct drawer via its footer Save button. + * + * The drawer is a SlideoutMenu whose footer height is coupled to its + * (re-rendering) body, so the footer — and the Save button inside it — keeps + * shifting while the form settles. A pointer `click()` gates on Playwright's + * "stable" actionability check and can spin until the test times out + * ("element is not stable" → "element was detached from the DOM"), especially + * under CI load. save-btn is a native -
- )} - - + {showFilteredResourceCount && ( + + + + {t('message.click-here-to-view-assets-on-explore')} + + + + )} +
); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/QueryBuilderWidgetV1/query-builder-widget-v1.less b/openmetadata-ui/src/main/resources/ui/src/components/common/QueryBuilderWidgetV1/query-builder-widget-v1.less index 22ded344a604..bf10b8d46aa5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/QueryBuilderWidgetV1/query-builder-widget-v1.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/QueryBuilderWidgetV1/query-builder-widget-v1.less @@ -11,18 +11,8 @@ * limitations under the License. */ -@import (reference) '../../../styles/variables.less'; - .query-builder-card { - background-color: @grey-6; - .ant-alert-info { - background-color: @blue-8; - border-color: @blue-7; - - .ant-alert-icon { - color: @blue-7; - } - } + background-color: var(--color-bg-secondary); } .query-builder-form-field @@ -37,7 +27,7 @@ position: absolute !important; margin-top: 0; right: 0; - top: -56px; // updating this as size of button is increased + top: -56px; display: block; } } @@ -50,12 +40,6 @@ } .query-builder-form-field { - .ant-select-disabled.ant-select:not(.ant-select-customize-input) - .ant-select-selector { - color: @black; - background-color: @background-color; - } - .hide--line.one--child { margin-top: 0; padding-top: var(--om-space-16); @@ -75,10 +59,6 @@ .group--field { width: 180px; - .ant-select { - width: 100% !important; - } - label { font-weight: var(--om-font-weight-regular); margin-bottom: var(--om-space-6); @@ -91,7 +71,7 @@ .rule.group-or-rule { .rule--header { - .ant-btn-group { + .rule--btn-group { margin: 0 !important; align-self: flex-start; } @@ -106,10 +86,6 @@ .group--field { margin: 0; flex: 0 1 25%; - - .ant-select { - min-width: 100% !important; // override the inline min-width style of the select provided by antd - } } } @@ -129,10 +105,6 @@ display: none; } - .rule-container .ant-btn-group { - visibility: visible; - } - .action.action--ADD-RULE { position: static !important; margin-top: var(--om-space-8); @@ -152,19 +124,11 @@ .widget--widget { margin: 0; flex: 1; - - .ant-col { - padding: 0 !important; // remove padding from ant-col inline styling by antd - } } .rule--operator, .rule--value .rule--widget { width: 100%; - - .ant-select { - min-width: 100% !important; // override the inline min-width style of the select provided by antd - } } } } @@ -199,10 +163,6 @@ } } } - - .rule-container .ant-btn-group { - visibility: visible; - } } } @@ -218,47 +178,42 @@ } .json-logic-field-select { - .ant-select-item-group { - padding-left: var(--om-space-8); + .item-group { + padding-left: 8px; position: relative; - color: @text-color; - font-size: var(--om-font-size-sm); - background-color: @grey-6; + color: var(--color-text-primary); + font-size: 14px; + background-color: var(--color-bg-secondary); } - /* Add vertical line for children */ - .ant-select-item-option-grouped { + .item-option-grouped { position: relative; - padding-left: var(--om-space-32); - /* Indentation for child items */ + padding-left: 32px; } - /* Add vertical line before each child */ - .ant-select-item-option-grouped::before { + .item-option-grouped::before { content: ''; position: absolute; left: 16px; top: 0; bottom: 0; width: 1px; - background-color: @border-color; + background-color: var(--color-border-primary); } - /* Adjust line height for last child */ - .ant-select-item-option-grouped:last-child::before { + .item-option-grouped:last-child::before { height: 16px; bottom: auto; } - /* Add horizontal connector for each child */ - .ant-select-item-option-grouped::after { + .item-option-grouped::after { content: ''; position: absolute; left: 16px; top: 16px; width: 10px; height: 1px; - background-color: @border-color; + background-color: var(--color-border-primary); } } @@ -276,20 +231,33 @@ } } -// Persona rule editor (io.collate PersonaAIContext): push the AND/OR conjunction -// toggle up to the group header (as the workflow / JSONLogic builder does) -// instead of the centered default that overlaps the condition rows. Scoped to -// the `.persona-context-rule-builder` wrapper so other elasticsearch query -// builders (ScopeFilter, DataContract) are unaffected. +// Persona rule editor (io.collate PersonaAIContext): render the AND/OR +// conjunction toggle as its own row at the top of the group (as the workflow / +// JSONLogic builder does) instead of the centered default that overlaps the +// condition rows. Scoped to the `.persona-context-rule-builder` wrapper so +// other elasticsearch query builders (ScopeFilter, DataContract) are +// unaffected. .persona-context-rule-builder { .query-builder-card.elasticsearch .query-builder-container { .group-or-rule-container.group-container > .group.group-or-rule > .group--header { order: 0; + // Reserve vertical space below the toggle so it never sits on top of the + // first condition's field label/input. + margin-bottom: var(--om-space-12); .group--conjunctions { - top: -75px; + // The old `top: -75px` targeted a statically-positioned box and did + // nothing — the toggle stayed crammed on top of the first rule where a + // sibling