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
4 changes: 2 additions & 2 deletions docs/.vitepress/tableresult-signatures.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
},
{
"name": "countRows",
"signature": "countRows: () => Promise<number>",
"comment": "/**\n* Counts the number of rows currently on the page.\n* Does not paginate.\n*/"
"signature": "countRows: (filters?: Record<string, FilterValue>, options?: { exact?: boolean; maxPages?: number }) => Promise<number>",
"comment": "/**\n* Counts the number of rows, optionally filtered.\n* Without arguments, counts all rows. With filters, counts only matching rows.\n* Paginates when pagination is configured.\n*/"
},
{
"name": "mapColumn",
Expand Down
2 changes: 1 addition & 1 deletion docs/api/table-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ await table.scrollToColumn('Notes');
### Signature

```typescript
countRows: () => Promise<number>
countRows: (filters?: Record<string, FilterValue>, options?: { exact?: boolean; maxPages?: number }) => Promise<number>
```

<!-- /api-signature: countRows -->
Expand Down
7 changes: 4 additions & 3 deletions src/typeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,10 +866,11 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
scrollToColumn: (columnName: string) => Promise<void>;

/**
* Counts the number of rows currently on the page.
* Does not paginate.
* Counts the number of rows, optionally filtered.
* Without arguments, counts all rows. With filters, counts only matching rows.
* Paginates when pagination is configured.
*/
countRows: () => Promise<number>;
countRows: (filters?: Record<string, FilterValue>, options?: { exact?: boolean; maxPages?: number }) => Promise<number>;

/**
* Iterates over rows and extracts the value of a single column.
Expand Down
7 changes: 4 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,10 +866,11 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
scrollToColumn: (columnName: string) => Promise<void>;

/**
* Counts the number of rows currently on the page.
* Does not paginate.
* Counts the number of rows, optionally filtered.
* Without arguments, counts all rows. With filters, counts only matching rows.
* Paginates when pagination is configured.
*/
countRows: () => Promise<number>;
countRows: (filters?: Record<string, FilterValue>, options?: { exact?: boolean; maxPages?: number }) => Promise<number>;

/**
* Iterates over rows and extracts the value of a single column.
Expand Down
28 changes: 21 additions & 7 deletions src/useTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,27 +322,41 @@ export const useTable = <T = any>(rootLocator: Locator, configOptions: TableConf
return resolve(config.headerSelector as Selector, rootLocator).nth(idx);
},

countRows: async (): Promise<number> => {
countRows: async (filters?: Record<string, FilterValue>, options?: { exact?: boolean; maxPages?: number }): Promise<number> => {
if (tableState.empty) return 0;
await _autoInit();
const filtersRecord = (filters ?? {}) as Record<string, FilterValue>;
const hasFilters = Object.keys(filtersRecord).length > 0;
const effectiveMaxPages = options?.maxPages ?? config.maxPages;
const pag = config.strategies.pagination;
const hasPagination = config.maxPages > 1 && !!(pag?.goNext || pag?.goNextBulk);
const hasPagination = effectiveMaxPages > 1 && !!(pag?.goNext || pag?.goNextBulk);

const resolveRows = () => {
let rows = resolve(config.rowSelector, rootLocator);
if (hasFilters) {
const map = tableMapper.getMapSync();
if (!map) throw new Error('Initialization Error: Table map not available. Call "await table.init()" first.');
rows = filterEngine.applyFilters(rows, filtersRecord, map, options?.exact ?? false, rootLocator.page(), rootLocator);
}
return rows;
};

if (!hasPagination) {
log("countRows: counting rows in current viewport (no pagination)");
return resolve(config.rowSelector, rootLocator).count();
log(`countRows: counting rows in current viewport (no pagination)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
return resolveRows().count();
}

log(`countRows: paginating up to ${config.maxPages} page(s)`);
log(`countRows: paginating up to ${effectiveMaxPages} page(s)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
const tracker = new ElementTracker('countRows');
let total = 0;
let pagesScanned = 1;
let paginationError: unknown;
try {
while (true) {
const newIndices = await tracker.getUnseenIndices(resolve(config.rowSelector, rootLocator));
const newIndices = await tracker.getUnseenIndices(resolveRows());
total += newIndices.length;
log(`countRows: page ${pagesScanned} — ${newIndices.length} row(s) (running total: ${total})`);
if (pagesScanned >= config.maxPages) break;
if (pagesScanned >= effectiveMaxPages) break;
if (!await _advancePage(false)) break;
pagesScanned++;
}
Expand Down
55 changes: 55 additions & 0 deletions tests/functional-methods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,61 @@ test.describe('new API additions', () => {
expect(await page.locator('#current-page').innerText()).toBe('1');
});

test('countRows with filters counts only matching rows across pages', async ({ page }) => {
await page.setContent(TABLE_HTML);
const table = useTable(page.locator('#tbl'), {
maxPages: 3,
strategies: {
pagination: Strategies.Pagination.click({
next: () => page.locator('#next'),
first: () => page.locator('#first'),
}),
},
});

expect(await table.countRows({ Status: 'Inactive' }, { exact: true })).toBe(3);
expect(await page.locator('#current-page').innerText()).toBe('1');
});

test('countRows with filters on single page (no pagination)', async ({ page }) => {
await page.setContent(TABLE_HTML);
const table = useTable(page.locator('#tbl'), {});

expect(await table.countRows({ Status: 'Inactive' }, { exact: true })).toBe(1);
});

test('countRows with maxPages option limits pagination depth', async ({ page }) => {
await page.setContent(TABLE_HTML);
const table = useTable(page.locator('#tbl'), {
maxPages: 3,
strategies: {
pagination: Strategies.Pagination.click({
next: () => page.locator('#next'),
first: () => page.locator('#first'),
}),
},
});

expect(await table.countRows({ Status: 'Inactive' }, { exact: true, maxPages: 2 })).toBe(2);
expect(await page.locator('#current-page').innerText()).toBe('1');
});

test('countRows with no matching filter returns 0', async ({ page }) => {
await page.setContent(TABLE_HTML);
const table = useTable(page.locator('#tbl'), {
maxPages: 3,
strategies: {
pagination: Strategies.Pagination.click({
next: () => page.locator('#next'),
first: () => page.locator('#first'),
}),
},
});

expect(await table.countRows({ Status: 'Deleted' })).toBe(0);
expect(await page.locator('#current-page').innerText()).toBe('1');
});

test('mapColumn collects values for a single column', async ({ page }) => {
await page.setContent(TABLE_HTML);
const table = makeTable(page);
Expand Down
Loading