1313
1414import { expect , Page } from '@playwright/test' ;
1515import { redirectToHomePage } from '../../utils/common' ;
16+ import {
17+ countCsvResponseRows ,
18+ getExportCount ,
19+ getExportModalContent ,
20+ openExportScopeModal ,
21+ } from '../../utils/explore' ;
1622import { test } from '../fixtures/pages' ;
1723
1824const navigateToExplorePage = async ( page : Page ) => {
@@ -21,22 +27,11 @@ const navigateToExplorePage = async (page: Page) => {
2127 await expect ( page . getByTestId ( 'explore-page' ) ) . toBeVisible ( ) ;
2228} ;
2329
24- const getExportModalContent = ( page : Page ) =>
25- page . getByTestId ( 'export-scope-modal' ) . locator ( '.ant-modal-content' ) ;
26-
27- const openExportScopeModal = async ( page : Page ) => {
28- await page . getByTestId ( 'export-search-results-button' ) . click ( ) ;
29- await expect ( getExportModalContent ( page ) ) . toBeVisible ( ) ;
30- // Wait for count fetch to complete and OK button to be enabled
31- await expect (
32- getExportModalContent ( page ) . getByRole ( 'button' , { name : 'Export' } )
33- ) . toBeEnabled ( ) ;
34- } ;
35-
3630test . describe ( 'Search Export' , { tag : [ '@Features' , '@Discovery' ] } , ( ) => {
3731 test . beforeEach ( async ( { page } ) => {
3832 await navigateToExplorePage ( page ) ;
3933 } ) ;
34+
4035 test ( 'Export button opens scope modal with correct options' , async ( {
4136 page,
4237 } ) => {
@@ -58,23 +53,27 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
5853 await expect ( modalContent . getByText ( 'Export Scope' ) ) . toBeVisible ( ) ;
5954 } ) ;
6055
61- await test . step ( 'Modal shows Visible results and All matching assets options' , async ( ) => {
56+ await test . step ( 'Modal shows tab-specific scope and All assets options' , async ( ) => {
6257 const modalContent = getExportModalContent ( page ) ;
6358
64- await expect ( modalContent . getByText ( 'Visible results' ) ) . toBeVisible ( ) ;
65- await expect ( modalContent . getByText ( 'All matching assets' ) ) . toBeVisible ( ) ;
59+ await expect (
60+ modalContent . getByTestId ( 'export-scope-visible-card' )
61+ ) . toBeVisible ( ) ;
62+ await expect (
63+ modalContent . getByTestId ( 'export-scope-all-card' )
64+ ) . toBeVisible ( ) ;
6665 } ) ;
6766
68- await test . step ( 'All matching assets is selected by default' , async ( ) => {
67+ await test . step ( 'All assets is selected by default' , async ( ) => {
6968 await expect (
7069 getExportModalContent ( page ) . locator ( 'input[value="all"]' )
7170 ) . toBeChecked ( ) ;
7271 } ) ;
7372
74- await test . step ( 'Selecting Visible results checks the visible radio' , async ( ) => {
73+ await test . step ( 'Selecting the tab-scope card checks the visible radio' , async ( ) => {
7574 const modalContent = getExportModalContent ( page ) ;
7675
77- await modalContent . getByText ( 'Visible results ') . click ( ) ;
76+ await modalContent . locator ( 'input[value="visible"] ') . click ( ) ;
7877 await expect (
7978 modalContent . locator ( 'input[value="visible"]' )
8079 ) . toBeChecked ( ) ;
@@ -89,12 +88,10 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
8988 } ) ;
9089 } ) ;
9190
92- test ( 'All matching assets export calls API with dataAsset index' , async ( {
93- page,
94- } ) => {
91+ test ( 'All assets export calls API with dataAsset index' , async ( { page } ) => {
9592 await openExportScopeModal ( page ) ;
9693
97- await test . step ( 'All matching assets radio is pre-selected' , async ( ) => {
94+ await test . step ( 'All assets radio is pre-selected' , async ( ) => {
9895 await expect (
9996 getExportModalContent ( page ) . locator ( 'input[value="all"]' )
10097 ) . toBeChecked ( ) ;
@@ -116,64 +113,80 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
116113 } ) ;
117114 } ) ;
118115
119- test ( 'Visible results export calls API with size param' , async ( { page } ) => {
116+ test ( 'Search mode visible export downloads CSV with tab-specific row count' , async ( {
117+ page,
118+ } ) => {
119+ test . slow ( ) ;
120+
121+ await page . goto ( '/explore/tables?search=sample_data' ) ;
122+ await expect ( page . getByTestId ( 'explore-page' ) ) . toBeVisible ( ) ;
123+
124+ const countApiPromise = page . waitForResponse (
125+ ( response ) =>
126+ response . url ( ) . includes ( '/api/v1/search/query' ) &&
127+ response . status ( ) === 200
128+ ) ;
129+
120130 await openExportScopeModal ( page ) ;
131+ await countApiPromise ;
121132
122- await test . step ( 'Select Visible results scope' , async ( ) => {
123- const modalContent = getExportModalContent ( page ) ;
133+ const modalContent = getExportModalContent ( page ) ;
124134
125- await modalContent . getByText ( 'Visible results' ) . click ( ) ;
126- await expect (
127- modalContent . locator ( 'input[value="visible"]' )
128- ) . toBeChecked ( ) ;
129- } ) ;
135+ await modalContent . locator ( 'input[value="visible"]' ) . click ( ) ;
130136
131- await test . step ( 'Clicking Export calls /search/export with size param' , async ( ) => {
132- const exportApiPromise = page . waitForRequest (
133- ( req ) =>
134- req . url ( ) . includes ( '/api/v1/search/export' ) && req . method ( ) === 'GET'
135- ) ;
137+ const expectedCount =
138+ await test . step ( 'Read displayed count from Visible Results card' , ( ) =>
139+ getExportCount ( page , 'export-scope-visible-count' ) ) ;
136140
137- await getExportModalContent ( page )
138- . getByRole ( 'button' , { name : 'Export' } )
139- . click ( ) ;
141+ const exportResponsePromise = page . waitForResponse (
142+ ( response ) =>
143+ response . url ( ) . includes ( '/api/v1/search/export' ) &&
144+ response . status ( ) === 200
145+ ) ;
140146
141- const request = await exportApiPromise ;
142- const url = request . url ( ) ;
147+ await modalContent . getByRole ( 'button' , { name : 'Export' } ) . click ( ) ;
148+
149+ await test . step ( 'CSV row count matches the displayed tab count' , async ( ) => {
150+ const csvText = await ( await exportResponsePromise ) . text ( ) ;
143151
144- expect ( url ) . toContain ( 'index=' ) ;
145- expect ( url ) . toContain ( 'size=' ) ;
152+ expect ( countCsvResponseRows ( csvText ) ) . toBe ( expectedCount ) ;
146153 } ) ;
147154 } ) ;
148155
149- test ( 'Visible results export on page 2 sends correct from offset ' , async ( {
156+ test ( 'Browse mode visible export downloads CSV with current page row count ' , async ( {
150157 page,
151158 } ) => {
152159 test . slow ( ) ;
153160
154- // Navigate to page 2 via URL so parsedSearch.page = 2
155- await page . goto ( `${ page . url ( ) . replace ( / \? .* / , '' ) } ?page=2&size=15` ) ;
156- await expect ( page . getByTestId ( 'explore-page' ) ) . toBeVisible ( ) ;
161+ const countApiPromise = page . waitForResponse (
162+ ( response ) =>
163+ response . url ( ) . includes ( '/api/v1/search/query' ) &&
164+ response . status ( ) === 200
165+ ) ;
157166
158167 await openExportScopeModal ( page ) ;
159- await getExportModalContent ( page ) . getByText ( 'Visible results' ) . click ( ) ;
168+ await countApiPromise ;
160169
161- await test . step ( 'Export request includes from= offset matching page 2' , async ( ) => {
162- const exportApiPromise = page . waitForRequest (
163- ( req ) =>
164- req . url ( ) . includes ( '/api/v1/search/export' ) && req . method ( ) === 'GET'
165- ) ;
170+ const modalContent = getExportModalContent ( page ) ;
166171
167- await getExportModalContent ( page )
168- . getByRole ( 'button' , { name : 'Export' } )
169- . click ( ) ;
172+ await modalContent . locator ( 'input[value="visible"]' ) . click ( ) ;
170173
171- const request = await exportApiPromise ;
172- const url = request . url ( ) ;
174+ const expectedCount =
175+ await test . step ( 'Read displayed count from Visible Results card' , ( ) =>
176+ getExportCount ( page , 'export-scope-visible-count' ) ) ;
177+
178+ const exportResponsePromise = page . waitForResponse (
179+ ( response ) =>
180+ response . url ( ) . includes ( '/api/v1/search/export' ) &&
181+ response . status ( ) === 200
182+ ) ;
183+
184+ await modalContent . getByRole ( 'button' , { name : 'Export' } ) . click ( ) ;
173185
174- // page=2, size=15 → from=15
175- expect ( url ) . toContain ( 'from=15' ) ;
176- expect ( url ) . toContain ( 'size=' ) ;
186+ await test . step ( 'CSV row count matches the displayed page count' , async ( ) => {
187+ const csvText = await ( await exportResponsePromise ) . text ( ) ;
188+
189+ expect ( countCsvResponseRows ( csvText ) ) . toBe ( expectedCount ) ;
177190 } ) ;
178191 } ) ;
179192
@@ -191,10 +204,19 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
191204 } ) ;
192205 } ) ;
193206
207+ const countApiPromise = page . waitForResponse (
208+ ( response ) =>
209+ response . url ( ) . includes ( '/api/v1/search/query' ) &&
210+ response . status ( ) === 200
211+ ) ;
212+
194213 await openExportScopeModal ( page ) ;
214+ await countApiPromise ;
215+
216+ const modalContent = getExportModalContent ( page ) ;
195217
196218 await test . step ( 'Export button becomes disabled and shows loading after click' , async ( ) => {
197- const exportButton = getExportModalContent ( page ) . getByRole ( 'button' , {
219+ const exportButton = modalContent . getByRole ( 'button' , {
198220 name : 'Export' ,
199221 } ) ;
200222
@@ -233,41 +255,47 @@ test.describe('Search Export', { tag: ['@Features', '@Discovery'] }, () => {
233255 } ) ;
234256 } ) ;
235257
236- test ( 'Export downloads CSV and closes modal ' , async ( { page } ) => {
237- test . slow ( ) ;
238-
239- await page . route ( '**/api/v1/search/export ?*' , async ( route ) => {
258+ test ( 'Export is disabled when all matching assets exceed limit ' , async ( {
259+ page ,
260+ } ) => {
261+ await page . route ( '**/api/v1/search/query ?*' , async ( route ) => {
240262 await route . fulfill ( {
241263 status : 200 ,
242- contentType : 'text/csv' ,
243- headers : {
244- 'Content-Disposition' : 'attachment; filename="search_export.csv"' ,
245- } ,
246- 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,,,,,,' ,
264+ contentType : 'application/json' ,
265+ body : JSON . stringify ( {
266+ took : 1 ,
267+ hits : {
268+ total : { value : 200001 , relation : 'eq' } ,
269+ hits : [ ] ,
270+ } ,
271+ aggregations : { } ,
272+ } ) ,
247273 } ) ;
248274 } ) ;
249275
250- await openExportScopeModal ( page ) ;
276+ await page . getByTestId ( 'export-search-results-button' ) . click ( ) ;
251277
252- await test . step ( 'Export button shows loading state while downloading' , async ( ) => {
253- await page . route ( '**/api/v1/search/export?*' , async ( route ) => {
254- await new Promise < void > ( ( resolve ) => setTimeout ( resolve , 1500 ) ) ;
255- await route . fulfill ( {
256- status : 200 ,
257- contentType : 'text/csv' ,
258- body : 'Entity Type\ntable' ,
259- } ) ;
260- } ) ;
278+ const modalContent = getExportModalContent ( page ) ;
279+ const exportButton = modalContent . getByRole ( 'button' , { name : 'Export' } ) ;
261280
262- const exportButton = getExportModalContent ( page ) . getByRole ( 'button' , {
263- name : 'Export' ,
264- } ) ;
281+ await test . step ( 'Limit alert is shown in modal' , async ( ) => {
282+ await expect (
283+ modalContent . getByText (
284+ 'Export is limited to 200,000 assets. Please refine your filters or choose visible results.'
285+ )
286+ ) . toBeVisible ( ) ;
287+ } ) ;
265288
266- await exportButton . click ( ) ;
267- await expect ( exportButton ) . toHaveClass ( / a n t - b t n - l o a d i n g / ) ;
289+ await test . step ( 'Export button remains disabled' , async ( ) => {
290+ await expect ( exportButton ) . toBeDisabled ( ) ;
268291 } ) ;
292+ } ) ;
293+
294+ test ( 'Export downloads CSV with correct filename and closes modal' , async ( {
295+ page,
296+ } ) => {
297+ test . slow ( ) ;
269298
270- // Re-open modal for download verification after loading state test
271299 await openExportScopeModal ( page ) ;
272300
273301 await test . step ( 'Clicking Export triggers CSV download with correct filename' , async ( ) => {
0 commit comments