Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -1564,3 +1564,177 @@ test.describe(
});
}
);

test.describe(
'Custom property enum lazy load in Advanced Search',
{ tag: ['@advanced-search'] },
() => {
// 150 values: initial asyncFetch returns items 0-99, scroll triggers items 100-149
const ENUM_VALUES = Array.from({ length: 150 }, (_, i) =>
`enum_val_${String(i).padStart(3, '0')}`
);
const FIRST_PAGE_VALUE = 'enum_val_000';
const SECOND_PAGE_VALUE = 'enum_val_100';

let enumCPName: string;
Comment thread
Rohit0301 marked this conversation as resolved.
let enumCPId: string;
let lazyLoadTable: TableClass;

test.beforeAll(
'Setup enum custom property with 150 values on table',
async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
try {
lazyLoadTable = new TableClass();
await lazyLoadTable.create(apiContext);

const cpMetadataTypeRes = await apiContext.get(
'/api/v1/metadata/types/name/table?fields=customProperties'
);
const cpMetadataType = await cpMetadataTypeRes.json();

const typesRes = await apiContext.get(
'/api/v1/metadata/types?category=field&limit=20'
);
const types = (await typesRes.json()).data as {
name: string;
id: string;
}[];
const enumTypeId =
types.find((t: { name: string }) => t.name === 'enum')?.id ?? '';

enumCPName = `enum-lazy-${uuid()}`;

const cpRes = await apiContext.put(
`/api/v1/metadata/types/${cpMetadataType.id}`,
{
data: {
name: enumCPName,
description: 'Enum CP for lazy load test',
propertyType: { name: 'enum', type: 'type', id: enumTypeId },
customPropertyConfig: {
config: { values: ENUM_VALUES, multiSelect: true },
},
},
}
);
const cpData = await cpRes.json();
enumCPId = cpData.id;
} finally {
await afterAction();
}
}
);

const openEnumValueDropdown = async (page: Parameters<typeof redirectToHomePage>[0]) => {
await redirectToHomePage(page);
await sidebarClick(page, SidebarItem.EXPLORE);
await showAdvancedSearchDialog(page);

const ruleLocator = page.locator('.rule').nth(0);

await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Custom Properties',
true
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Table',
true
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
enumCPName,
true
);

const valueSelector = ruleLocator.locator(
'.rule--widget .ant-select-selector'
);

await expect(valueSelector).toBeVisible({ timeout: 15000 });
await valueSelector.click();

const dropdown = page.locator('.ant-select-dropdown:visible');

await expect(dropdown).toBeVisible();

return { ruleLocator, valueSelector, 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(
'should find page-2 items via search without clicking Load more',
async ({ page }) => {
test.slow();

const { ruleLocator, dropdown } = await openEnumValueDropdown(page);

// Page 1 items load; page-2 item is not yet visible
await expect(
dropdown.locator(`[title="${FIRST_PAGE_VALUE}"]`)
).toBeVisible({ timeout: 10000 });
await expect(
dropdown.locator(`[title="${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'
);

await searchInput.fill(SECOND_PAGE_VALUE);

// Item appears immediately without clicking Load more
await expect(
dropdown.locator(`[title="${SECOND_PAGE_VALUE}"]`)
).toBeVisible({ timeout: 10000 });
}
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ describe('getCustomPropertiesSubFields', () => {
jest.clearAllMocks();
});

it('should return correct configuration for enum type custom property with keyword suffix', () => {
it('should return asyncFetch configuration for enum type custom property with keyword suffix', () => {
const mockField = {
name: 'statusField',
type: 'enum',
Expand All @@ -334,28 +334,17 @@ describe('getCustomPropertiesSubFields', () => {
};

const mockLabel = 'Status Field';
const mockEnumOptions = [
{ value: 'ACTIVE', title: 'Active' },
{ value: 'INACTIVE', title: 'Inactive' },
{ value: 'PENDING', title: 'Pending' },
];

mockGetEntityName.mockReturnValue(mockLabel);
mockGetCustomPropertyAdvanceSearchEnumOptions.mockReturnValue(
mockEnumOptions
);

const result = advancedSearchClassBase.getCustomPropertiesSubFields(
mockField as CustomPropertySummary,
SearchOutputType.ElasticSearch
);

expect(mockGetEntityName).toHaveBeenCalledWith(mockField);
expect(mockGetCustomPropertyAdvanceSearchEnumOptions).toHaveBeenCalledWith([
'ACTIVE',
'INACTIVE',
'PENDING',
]);
expect(
mockGetCustomPropertyAdvanceSearchEnumOptions
).not.toHaveBeenCalled();

expect(result).toEqual({
subfieldsKey: 'statusField.keyword',
Expand All @@ -365,9 +354,10 @@ describe('getCustomPropertiesSubFields', () => {
label: mockLabel,
operators: MULTISELECT_FIELD_OPERATORS,
fieldSettings: {
listValues: mockEnumOptions,
asyncFetch: expect.any(Function),
showSearch: true,
useAsyncSearch: false,
useAsyncSearch: true,
useLoadMore: true,
},
},
});
Expand Down Expand Up @@ -858,7 +848,7 @@ describe('getCustomPropertiesSubFields', () => {
});
});

it('should use .keyword suffix for enum type with ElasticSearch output', () => {
it('should use asyncFetch with .keyword suffix for enum type with ElasticSearch output', () => {
const mockField = {
name: 'statusField',
type: 'enum',
Expand All @@ -869,11 +859,7 @@ describe('getCustomPropertiesSubFields', () => {
},
};
const mockLabel = 'Status Field';
const mockEnumOptions = { ACTIVE: 'ACTIVE', INACTIVE: 'INACTIVE' };
mockGetEntityName.mockReturnValue(mockLabel);
mockGetCustomPropertyAdvanceSearchEnumOptions.mockReturnValue(
mockEnumOptions
);

const result = advancedSearchClassBase.getCustomPropertiesSubFields(
mockField as CustomPropertySummary,
Expand All @@ -888,15 +874,20 @@ describe('getCustomPropertiesSubFields', () => {
label: mockLabel,
operators: MULTISELECT_FIELD_OPERATORS,
fieldSettings: {
listValues: mockEnumOptions,
asyncFetch: expect.any(Function),
showSearch: true,
useAsyncSearch: false,
useAsyncSearch: true,
useLoadMore: true,
},
},
});

expect(
mockGetCustomPropertyAdvanceSearchEnumOptions
).not.toHaveBeenCalled();
});

it('should use base field name for enum type with JSONLogic output', () => {
it('should use asyncFetch with base field name for enum type with JSONLogic output', () => {
const mockField = {
name: 'statusField',
type: 'enum',
Expand All @@ -907,11 +898,7 @@ describe('getCustomPropertiesSubFields', () => {
},
};
const mockLabel = 'Status Field';
const mockEnumOptions = { ACTIVE: 'ACTIVE', INACTIVE: 'INACTIVE' };
mockGetEntityName.mockReturnValue(mockLabel);
mockGetCustomPropertyAdvanceSearchEnumOptions.mockReturnValue(
mockEnumOptions
);

const result = advancedSearchClassBase.getCustomPropertiesSubFields(
mockField as CustomPropertySummary,
Expand All @@ -926,12 +913,17 @@ describe('getCustomPropertiesSubFields', () => {
label: mockLabel,
operators: MULTISELECT_FIELD_OPERATORS,
fieldSettings: {
listValues: mockEnumOptions,
asyncFetch: expect.any(Function),
showSearch: true,
useAsyncSearch: false,
useAsyncSearch: true,
useLoadMore: true,
},
},
});

expect(
mockGetCustomPropertyAdvanceSearchEnumOptions
).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -1192,3 +1184,65 @@ describe('getCustomPropertiesSubFields', () => {
});
});
});

describe('buildEnumAsyncFetch', () => {
let advancedSearchClassBase: AdvancedSearchClassBase;

beforeEach(() => {
advancedSearchClassBase = new AdvancedSearchClassBase();
});

it('should return first 100 values when search is empty (offset 0)', async () => {
const values = Array.from({ length: 150 }, (_, i) => `VAL_${i}`);
const fetchFn = advancedSearchClassBase.buildEnumAsyncFetch(values);
const result = await fetchFn!('');

expect(result.values).toHaveLength(100);
expect(result.values[0]).toEqual({ value: 'VAL_0', title: 'VAL_0' });
expect(result.hasMore).toBe(true);
});

it('should return next page when offset is provided', async () => {
const values = Array.from({ length: 150 }, (_, i) => `VAL_${i}`);
const fetchFn = advancedSearchClassBase.buildEnumAsyncFetch(values);
const result = await fetchFn!('', 100);

expect(result.values).toHaveLength(50);
expect(result.values[0]).toEqual({ value: 'VAL_100', title: 'VAL_100' });
expect(result.hasMore).toBe(false);
});

it('should return all matching values when search filters below page size', async () => {
const values = ['ALPHA', 'BETA', 'GAMMA', 'ALPHABET'];
const fetchFn = advancedSearchClassBase.buildEnumAsyncFetch(values);
const result = await fetchFn!('alpha');

expect(result.values).toEqual([
{ value: 'ALPHA', title: 'ALPHA' },
{ value: 'ALPHABET', title: 'ALPHABET' },
]);
expect(result.hasMore).toBe(false);
});

it('should return all values when list is smaller than page size', async () => {
const values = ['A', 'B', 'C'];
const fetchFn = advancedSearchClassBase.buildEnumAsyncFetch(values);
const result = await fetchFn!('');

expect(result.values).toEqual([
{ value: 'A', title: 'A' },
{ value: 'B', title: 'B' },
{ value: 'C', title: 'C' },
]);
expect(result.hasMore).toBe(false);
});

it('should be case-insensitive when filtering', async () => {
const values = ['Active', 'ACTIVE', 'inactive'];
const fetchFn = advancedSearchClassBase.buildEnumAsyncFetch(values);
const result = await fetchFn!('active');

expect(result.values).toHaveLength(2);
expect(result.values.map((v) => v.value)).toEqual(['Active', 'ACTIVE']);
});
});
Loading
Loading