Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
* limitations under the License.
*/

import { expect } from '@playwright/test';
import { EntityStatus } from '../../../src/generated/entity/data/searchIndex';
import { COMMON_TIER_TAG } from '../../constant/common';
import { SidebarItem } from '../../constant/sidebar';
import { DataProduct } from '../../support/domain/DataProduct';
import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { MlModelClass } from '../../support/entity/MlModelClass';
import { TableClass } from '../../support/entity/TableClass';
import { TopicClass } from '../../support/entity/TopicClass';
import { Glossary } from '../../support/glossary/Glossary';
Expand All @@ -22,12 +26,17 @@ import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import {
FIELDS,
fillRule,
fillStaticListRule,
OPERATOR,
runRuleGroupTests,
runRuleGroupTestsWithNonExistingValue,
selectOption,
showAdvancedSearchDialog,
verifyAllConditions,
} from '../../utils/advancedSearch';
import { redirectToHomePage } from '../../utils/common';
import { waitForAllLoadersToDisappear } from '../../utils/entity';
import { sidebarClick } from '../../utils/sidebar';
import { test } from '../fixtures/pages';

Expand Down Expand Up @@ -285,8 +294,8 @@ test.describe('Advanced Search', { tag: ['@advanced-search'] }, () => {
table2.entityResponseData.name,
],
'project.keyword': [
EntityDataClass.dashboardDataModel1.entityResponseData.project,
EntityDataClass.dashboardDataModel2.entityResponseData.project,
EntityDataClass.dashboardDataModel1.entityResponseData.project || '',
EntityDataClass.dashboardDataModel2.entityResponseData.project || '',
],
'charts.displayName.keyword': [
EntityDataClass.dashboard1.chartsResponseData.displayName,
Expand Down Expand Up @@ -344,3 +353,280 @@ test.describe('Advanced Search', { tag: ['@advanced-search'] }, () => {
await runRuleGroupTestsWithNonExistingValue(page);
});
});

const ENTITY_STATUSES = Object.values(EntityStatus);

test.describe(
'Advanced Search - Entity Status Filter',
{ tag: ['@advanced-search', '@Discovery'] },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have such tag? please validate it, and for '@discovery' use tag from Domain constant

() => {
// One distinct entity type per status to prove the filter works across entity types
const glossaryForStatus = new Glossary();
const glossaryTermApproved = new GlossaryTerm(glossaryForStatus);
const mlModelDraft = new MlModelClass();
const dataProductInReview = new DataProduct();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pattern gives 409 error, please check


type StatusEntry = {
status: EntityStatus;
endpoint: string;
id: () => string;
displayName: () => string;
fqn: () => string;
};

let statusEntries: StatusEntry[];

test.beforeAll(
'Create mixed entity types with distinct entity statuses',
async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);

await glossaryForStatus.create(apiContext);
await Promise.all([
glossaryTermApproved.create(apiContext),
mlModelDraft.create(apiContext),
dataProductInReview.create(apiContext),
]);

statusEntries = [
{
status: EntityStatus.Approved,
endpoint: 'glossaryTerms',
id: () => glossaryTermApproved.responseData.id,
displayName: () => glossaryTermApproved.data.displayName,
fqn: () => glossaryTermApproved.responseData.fullyQualifiedName,
},
{
status: EntityStatus.Draft,
endpoint: 'mlmodels',
id: () => mlModelDraft.entityResponseData.id,
displayName: () => mlModelDraft.entity.displayName,
fqn: () => mlModelDraft.entityResponseData.fullyQualifiedName,
},
{
status: EntityStatus.InReview,
endpoint: 'dataProducts',
id: () => dataProductInReview.responseData.id ?? '',
displayName: () => dataProductInReview.data.displayName,
fqn: () =>
dataProductInReview.responseData.fullyQualifiedName ?? '',
},
];

// Patch entityStatus on each entity
await Promise.all(
statusEntries.map(({ id, status, endpoint }) =>
apiContext.patch(`/api/v1/${endpoint}/${id()}`, {
data: [{ op: 'add', path: '/entityStatus', value: status }],
headers: { 'Content-Type': 'application/json-patch+json' },
})
)
);

await afterAction();
}
);

test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
await sidebarClick(page, SidebarItem.EXPLORE);
});

test('All entity status options are visible in the Status dropdown', async ({
page,
}) => {
test.slow();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need test.slow?


await test.step('Open advanced search dialog', async () => {
await showAdvancedSearchDialog(page);
});

await test.step('Select Status field and == operator', async () => {
const ruleLocator = page.locator('.rule').nth(0);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Status',
true
);
await selectOption(
page,
ruleLocator.locator('.rule--operator .ant-select'),
'=='
);
});

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 dropdown = page
.locator('.ant-select-dropdown')
.filter({ hasText: EntityStatus.Approved })
.last();

await expect(dropdown).toBeVisible();

for (const status of ENTITY_STATUSES) {
await expect(
dropdown
.locator('.ant-select-item-option')
.filter({ hasText: new RegExp(`^${status}$`, 'i') })
.first()
).toBeVisible();
}
});
});

test('Filtering by status "==" shows matching entity and hides others across entity types', async ({
page,
}) => {
test.slow();

for (const entry of statusEntries) {
const { status } = entry;
const matchedName = entry.displayName();

await test.step(`Apply Status == "${status}" AND Name == "${matchedName}"`, async () => {
await showAdvancedSearchDialog(page);

await fillStaticListRule(page, {
fieldLabel: 'Status',
condition: '==',
value: status,
ruleIndex: 1,
});

await page.getByTestId('advanced-search-add-rule').nth(1).click();

await fillRule(page, {
condition: '==',
field: { id: 'Display Name', name: 'displayName.keyword' },
searchCriteria: matchedName,
index: 2,
});

const searchRes = page.waitForResponse(
'/api/v1/search/query?*index=dataAsset*'
);
await page.getByTestId('apply-btn').click();
await searchRes;
await waitForAllLoadersToDisappear(page);
});

await test.step('Filter chip shows the applied status', async () => {
await expect(
page.getByTestId('advance-search-filter-container')
).toContainText(`'${status}'`);
});

await test.step(`"${matchedName}" (${entry.endpoint}) is visible`, async () => {
await expect(
page.getByTestId(`table-data-card_${entry.fqn()}`)
).toBeVisible();
});

await test.step('Entities with other statuses are not in results', async () => {
const otherEntries = statusEntries.filter((e) => e.status !== status);

for (const other of otherEntries) {
await expect(
page.getByTestId(`table-data-card_${other.fqn()}`)
).not.toBeVisible();
}
});

await page.getByTestId('clear-filters').click();
}
});

test('Filtering by status "!=" excludes matched entity but shows all other entity types', async ({
page,
}) => {
test.slow();

const targetEntry = statusEntries.find(
(e) => e.status === EntityStatus.Approved
)!;
const approvedName = targetEntry.displayName();
const otherEntries = statusEntries.filter(
(e) => e.status !== EntityStatus.Approved
);

await test.step('Apply Status != "Approved" AND Name == approved entity name', async () => {
await showAdvancedSearchDialog(page);

await fillStaticListRule(page, {
fieldLabel: 'Status',
condition: '!=',
value: EntityStatus.Approved,
ruleIndex: 1,
});

await page.getByTestId('advanced-search-add-rule').nth(1).click();

await fillRule(page, {
condition: '==',
field: { id: 'Display Name', name: 'displayName.keyword' },
searchCriteria: approvedName,
index: 2,
});

const searchRes = page.waitForResponse(
'/api/v1/search/query?*index=dataAsset*'
);
await page.getByTestId('apply-btn').click();
await searchRes;
await waitForAllLoadersToDisappear(page);
});

await test.step('Filter chip reflects the != condition', async () => {
await expect(
page.getByTestId('advance-search-filter-container')
).toContainText(`'${EntityStatus.Approved}'`);
});

await test.step('GlossaryTerm with Approved status is not visible', async () => {
await expect(
page.getByTestId(`table-data-card_${targetEntry.fqn()}`)
).not.toBeVisible();
});

await test.step('Draft and In Review entities appear when searched by their name and non-Approved status', async () => {
for (const entry of otherEntries) {
await page.getByTestId('clear-filters').click();
await showAdvancedSearchDialog(page);

await fillStaticListRule(page, {
fieldLabel: 'Status',
condition: '!=',
value: EntityStatus.Approved,
ruleIndex: 1,
});

await page.getByTestId('advanced-search-add-rule').nth(1).click();

await fillRule(page, {
condition: '==',
field: { id: 'Display Name', name: 'displayName.keyword' },
searchCriteria: entry.displayName(),
index: 2,
});

const searchRes = page.waitForResponse(
'/api/v1/search/query?*index=dataAsset*'
);
await page.getByTestId('apply-btn').click();
await searchRes;
await waitForAllLoadersToDisappear(page);

await expect(
page.getByTestId(`table-data-card_${entry.fqn()}`)
).toBeVisible();
}
});

await page.getByTestId('clear-filters').click();
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,43 @@ export const runRuleGroupTestsWithNonExistingValue = async (page: Page) => {
await expect(dropdownText).not.toContainText('Loading...');
};

// For fields backed by hard-coded listValues (no aggregate API call), options are
// rendered immediately — use selectOption directly instead of fillRule which waits
// for a network response that never comes.
export const fillStaticListRule = async (
page: Page,
{
fieldLabel,
condition,
value,
ruleIndex,
}: {
fieldLabel: string;
condition: string;
value: string;
ruleIndex: number;
}
) => {
const ruleLocator = page.locator('.rule').nth(ruleIndex - 1);

await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
fieldLabel,
true
);
await selectOption(
page,
ruleLocator.locator('.rule--operator .ant-select'),
condition
);
await selectOption(
page,
ruleLocator.locator('.widget--widget > .ant-select'),
value
);
};

export const getFieldsSuggestionSearchText = (
fieldLabel: string,
data: Record<string, string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export const GLOSSARY_DROPDOWN_ITEMS = [
},
{
label: 'label.status',
key: EntityFields.GLOSSARY_TERM_STATUS,
key: EntityFields.ENTITY_STATUS,
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export enum EntityFields {
API_COLLECTION = 'apiCollection.displayName.keyword',
CHART = 'charts.displayName.keyword',
TASK = 'tasks.displayName.keyword',
GLOSSARY_TERM_STATUS = 'entityStatus',
ENTITY_STATUS = 'entityStatus',
REQUEST_SCHEMA_FIELD = 'requestSchema.schemaFields.name.keyword',
RESPONSE_SCHEMA_FIELD = 'responseSchema.schemaFields.name.keyword',
SERVICE_NAME = 'service.name.keyword',
Expand Down
Loading
Loading