Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -13,6 +13,12 @@

import { expect, Page } from '@playwright/test';
import { redirectToHomePage } from '../../utils/common';
import {
countCsvResponseRows,
getExportCount,
getExportModalContent,
openExportScopeModal,
} from '../../utils/explore';
import { test } from '../fixtures/pages';

const navigateToExplorePage = async (page: Page) => {
Expand All @@ -21,22 +27,11 @@ const navigateToExplorePage = async (page: Page) => {
await expect(page.getByTestId('explore-page')).toBeVisible();
};

const getExportModalContent = (page: Page) =>
page.getByTestId('export-scope-modal').locator('.ant-modal-content');

const openExportScopeModal = async (page: Page) => {
await page.getByTestId('export-search-results-button').click();
await expect(getExportModalContent(page)).toBeVisible();
// Wait for count fetch to complete and OK button to be enabled
await expect(
getExportModalContent(page).getByRole('button', { name: 'Export' })
).toBeEnabled();
};

test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
test.beforeEach(async ({ page }) => {
await navigateToExplorePage(page);
});

test('Export button opens scope modal with correct options', async ({
page,
}) => {
Expand All @@ -58,23 +53,27 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
await expect(modalContent.getByText('Export Scope')).toBeVisible();
});

await test.step('Modal shows Visible results and All matching assets options', async () => {
await test.step('Modal shows tab-specific scope and All assets options', async () => {
const modalContent = getExportModalContent(page);

await expect(modalContent.getByText('Visible results')).toBeVisible();
await expect(modalContent.getByText('All matching assets')).toBeVisible();
await expect(
modalContent.getByTestId('export-scope-visible-card')
).toBeVisible();
await expect(
modalContent.getByTestId('export-scope-all-card')
).toBeVisible();
});

await test.step('All matching assets is selected by default', async () => {
await test.step('All assets is selected by default', async () => {
await expect(
getExportModalContent(page).locator('input[value="all"]')
).toBeChecked();
});

await test.step('Selecting Visible results checks the visible radio', async () => {
await test.step('Selecting the tab-scope card checks the visible radio', async () => {
const modalContent = getExportModalContent(page);

await modalContent.getByText('Visible results').click();
await modalContent.locator('input[value="visible"]').click();
await expect(
modalContent.locator('input[value="visible"]')
).toBeChecked();
Expand All @@ -89,12 +88,10 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
});
});

test('All matching assets export calls API with dataAsset index', async ({
page,
}) => {
test('All assets export calls API with dataAsset index', async ({ page }) => {
await openExportScopeModal(page);

await test.step('All matching assets radio is pre-selected', async () => {
await test.step('All assets radio is pre-selected', async () => {
await expect(
getExportModalContent(page).locator('input[value="all"]')
).toBeChecked();
Expand All @@ -116,64 +113,80 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
});
});

test('Visible results export calls API with size param', async ({ page }) => {
test('Search mode visible export downloads CSV with tab-specific row count', async ({
page,
}) => {
test.slow();

await page.goto('/explore/tables?search=sample_data');
await expect(page.getByTestId('explore-page')).toBeVisible();

const countApiPromise = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/search/query') &&
response.status() === 200
);

await openExportScopeModal(page);
await countApiPromise;

await test.step('Select Visible results scope', async () => {
const modalContent = getExportModalContent(page);
const modalContent = getExportModalContent(page);

await modalContent.getByText('Visible results').click();
await expect(
modalContent.locator('input[value="visible"]')
).toBeChecked();
});
await modalContent.locator('input[value="visible"]').click();

await test.step('Clicking Export calls /search/export with size param', async () => {
const exportApiPromise = page.waitForRequest(
(req) =>
req.url().includes('/api/v1/search/export') && req.method() === 'GET'
);
const expectedCount =
await test.step('Read displayed count from Visible Results card', () =>
getExportCount(page, 'export-scope-visible-count'));

await getExportModalContent(page)
.getByRole('button', { name: 'Export' })
.click();
const exportResponsePromise = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/search/export') &&
response.status() === 200
);

const request = await exportApiPromise;
const url = request.url();
await modalContent.getByRole('button', { name: 'Export' }).click();

await test.step('CSV row count matches the displayed tab count', async () => {
const csvText = await (await exportResponsePromise).text();

expect(url).toContain('index=');
expect(url).toContain('size=');
expect(countCsvResponseRows(csvText)).toBe(expectedCount);
});
});

test('Visible results export on page 2 sends correct from offset', async ({
test('Browse mode visible export downloads CSV with current page row count', async ({
page,
}) => {
test.slow();

// Navigate to page 2 via URL so parsedSearch.page = 2
await page.goto(`${page.url().replace(/\?.*/, '')}?page=2&size=15`);
await expect(page.getByTestId('explore-page')).toBeVisible();
const countApiPromise = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/search/query') &&
response.status() === 200
);

await openExportScopeModal(page);
await getExportModalContent(page).getByText('Visible results').click();
await countApiPromise;

await test.step('Export request includes from= offset matching page 2', async () => {
const exportApiPromise = page.waitForRequest(
(req) =>
req.url().includes('/api/v1/search/export') && req.method() === 'GET'
);
const modalContent = getExportModalContent(page);

await getExportModalContent(page)
.getByRole('button', { name: 'Export' })
.click();
await modalContent.locator('input[value="visible"]').click();

const request = await exportApiPromise;
const url = request.url();
const expectedCount =
await test.step('Read displayed count from Visible Results card', () =>
getExportCount(page, 'export-scope-visible-count'));

const exportResponsePromise = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/search/export') &&
response.status() === 200
);

await modalContent.getByRole('button', { name: 'Export' }).click();

// page=2, size=15 → from=15
expect(url).toContain('from=15');
expect(url).toContain('size=');
await test.step('CSV row count matches the displayed page count', async () => {
const csvText = await (await exportResponsePromise).text();

expect(countCsvResponseRows(csvText)).toBe(expectedCount);
});
});

Expand All @@ -191,10 +204,19 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
});
});

const countApiPromise = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/search/query') &&
response.status() === 200
);

await openExportScopeModal(page);
await countApiPromise;

const modalContent = getExportModalContent(page);

await test.step('Export button becomes disabled and shows loading after click', async () => {
const exportButton = getExportModalContent(page).getByRole('button', {
const exportButton = modalContent.getByRole('button', {
name: 'Export',
});

Expand Down Expand Up @@ -233,41 +255,47 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
});
});

test('Export downloads CSV and closes modal', async ({ page }) => {
test.slow();

await page.route('**/api/v1/search/export?*', async (route) => {
test('Export is disabled when all matching assets exceed limit', async ({
page,
}) => {
await page.route('**/api/v1/search/query?*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/csv',
headers: {
'Content-Disposition': 'attachment; filename="search_export.csv"',
},
body: 'Entity Type,Service Name,Service Type,FQN,Name,Display Name,Description,Owners,Tags,Glossary Terms,Domains,Tier\ntable,mysql,Mysql,sample_data.ecommerce_db.shopify.dim_address,dim_address,dim_address,,,,,,',
contentType: 'application/json',
body: JSON.stringify({
took: 1,
hits: {
total: { value: 200001, relation: 'eq' },
hits: [],
},
aggregations: {},
}),
});
});

await openExportScopeModal(page);
await page.getByTestId('export-search-results-button').click();

await test.step('Export button shows loading state while downloading', async () => {
await page.route('**/api/v1/search/export?*', async (route) => {
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
await route.fulfill({
status: 200,
contentType: 'text/csv',
body: 'Entity Type\ntable',
});
});
const modalContent = getExportModalContent(page);
const exportButton = modalContent.getByRole('button', { name: 'Export' });

const exportButton = getExportModalContent(page).getByRole('button', {
name: 'Export',
});
await test.step('Limit alert is shown in modal', async () => {
await expect(
modalContent.getByText(
'Export is limited to 200,000 assets. Please refine your filters or choose visible results.'
)
).toBeVisible();
});

await exportButton.click();
await expect(exportButton).toHaveClass(/ant-btn-loading/);
await test.step('Export button remains disabled', async () => {
await expect(exportButton).toBeDisabled();
});
});

test('Export downloads CSV with correct filename and closes modal', async ({
page,
}) => {
test.slow();

// Re-open modal for download verification after loading state test
await openExportScopeModal(page);

await test.step('Clicking Export triggers CSV download with correct filename', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,29 @@ export const navigateToExploreAndSelectEntity = async (

await openEntitySummaryPanel(page, entityName, endpoint, fullyQualifiedName);
};

export const getExportModalContent = (page: Page) =>
page.getByTestId('export-scope-modal').locator('.ant-modal-content');

export const openExportScopeModal = async (page: Page) => {
await page.getByTestId('export-search-results-button').click();
const modalContent = getExportModalContent(page);
await expect(modalContent).toBeVisible();
Comment thread
gitar-bot[bot] marked this conversation as resolved.
await expect(
modalContent.getByRole('button', { name: 'Export' })
).toBeEnabled();
};

export const countCsvResponseRows = (csvText: string): number =>
csvText.split('\n').filter((line: string) => line.trim().length > 0).length -
1;

export const getExportCount = async (
page: Page,
testId: string
): Promise<number> => {
const text = await page.getByTestId(testId).textContent();
const match = text?.match(/(\d[\d,]*)/);

return match ? parseInt(match[1].replace(/,/g, ''), 10) : 0;
};
Loading
Loading