From 665396d28ae4402180694d98269bb44ece17139e Mon Sep 17 00:00:00 2001 From: Daniel Hara Date: Sun, 21 Jan 2024 23:04:17 +0100 Subject: [PATCH 1/3] feat: allow text matching with a regex --- README.md | 11 +- src/cellByRowAndColumnHeaders.ts | 17 +- src/columnCellsByHeaderText.ts | 31 +++- src/queries.test.ts | 229 +++++++++++++++++++-------- src/rowByFirstCellText.ts | 32 ++-- src/utils/columnIndexByHeaderText.ts | 14 +- 6 files changed, 235 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 21cc147..139e657 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,11 @@ expect(getRowByFirstCellText('John Smith')).toBeVisible() fireEvent.click(within(getRowByFirstCellText('John Smith')).getByText('Delete')) ``` -Users will generally find rows by scanning the content in the first column, then reading across the row. This finds that row (rather than just the first cell), which can then be used to identify other items within that row. +Users will generally find rows by scanning the content in the first column, then reading across the row. This finds that row (rather than just the first cell), which can then be used to identify other items within that row. You can also use regular expression matching instead of looking for an exact text: + +```js +expect(getRowByFirstCellText(/John Smith/)).toBeVisible() +``` ### Column cells by header text @@ -125,7 +129,7 @@ ageCells.forEach((cell, index) => { }) ``` -Returns an array of cells based on the text in the column header. Note that there is no DOM 'column' element, so it is an array of cells. If multiple columns have the same header text, the first is used. Optionally, this also supports an index (starting from zero) to support having multiple header rows: +Returns an array of cells based on the text (or a regex) in the column header. Note that there is no DOM 'column' element, so it is an array of cells. If multiple columns have the same header text, the first is used. Optionally, this also supports an index (starting from zero) to support having multiple header rows: ```js const { getAllColumnCellsByHeaderText } = render() @@ -141,7 +145,7 @@ expect(getCellByRowAndColumnHeaders('John Smith', 'Age')).toHaveTextContent( ) ``` -If a user is trying to find a specific value for a specific entity, they might scan from the row and column headers. This finds cells based on those headers. Like column cells by header text, it only uses the first column with the specified header text (but will handle multiple rows), and supports a header index. +If a user is trying to find a specific value for a specific entity, they might scan from the row and column headers. This finds cells based on those headers. Like column cells by header text (or regex), it only uses the first column with the specified header text (or regex) (but will handle multiple rows), and supports a header index. ## Examples @@ -150,7 +154,6 @@ See [example tests](./example/src/SimpleTable.test.js) ## Future changes - Address the first column limitation -- Allow custom text normalisation/matching - Allow Nth cell in a row, rather than just first ## Development diff --git a/src/cellByRowAndColumnHeaders.ts b/src/cellByRowAndColumnHeaders.ts index c889ed0..3511889 100644 --- a/src/cellByRowAndColumnHeaders.ts +++ b/src/cellByRowAndColumnHeaders.ts @@ -6,15 +6,15 @@ import { nthHeaderError } from './utils/nthHeaderError' function queryAllCellsByRowAndColumnHeaders( container: HTMLElement, - rowHeaderText: string, - columnheaderText: string, + rowHeaderTextQuery: string | RegExp, + columnheaderTextQuery: string | RegExp, headerRowIndex = 0 ) { - const rows = queryAllRowsByFirstCellText(container, rowHeaderText) + const rows = queryAllRowsByFirstCellText(container, rowHeaderTextQuery) const columnIndex = getColumnIndexByHeaderText( container, - columnheaderText, + columnheaderTextQuery, headerRowIndex ) @@ -25,17 +25,18 @@ function queryAllCellsByRowAndColumnHeaders( const getMultipleError = ( _c: Element | null, - rowHeaderText: string, - columnheaderText: string, + rowHeaderText: string | RegExp, + columnheaderText: string | RegExp, headerRowIndex = 0 ) => `Found multiple cells with ${rowHeaderText} in the first column and ${columnheaderText} in the ${nthHeaderError( headerRowIndex )}` + const getMissingError = ( _c: Element | null, - rowHeaderText: string, - columnheaderText: string, + rowHeaderText: string | RegExp, + columnheaderText: string | RegExp, headerRowIndex = 0 ) => `Found no rows with ${rowHeaderText} in the first column and ${columnheaderText} in the ${nthHeaderError( diff --git a/src/columnCellsByHeaderText.ts b/src/columnCellsByHeaderText.ts index 3d2d60b..c33997b 100644 --- a/src/columnCellsByHeaderText.ts +++ b/src/columnCellsByHeaderText.ts @@ -6,12 +6,12 @@ import { nthHeaderError } from './utils/nthHeaderError' function queryAllColumnCellsByHeaderText( container: HTMLElement, - textContent: string, + textQuery: string | RegExp, headerRowIndex = 0 ) { const cellIndex = getColumnIndexByHeaderText( container, - textContent, + textQuery, headerRowIndex ) @@ -24,18 +24,33 @@ function queryAllColumnCellsByHeaderText( const getMultipleError = ( _c: Element | null, - textContent: string, + textQuery: string | RegExp, headerRowIndex = 0 -) => - `Found multiple cells with ${textContent} in the ${nthHeaderError( +) => { + if (typeof textQuery === 'string') { + return `Found multiple cells with ${textQuery} in the ${nthHeaderError( + headerRowIndex + )}` + } + return `Found multiple cells matching ${textQuery} in the ${nthHeaderError( headerRowIndex )}` +} + const getMissingError = ( _c: Element | null, - textContent: string, + textQuery: string | RegExp, headerRowIndex = 0 -) => - `Found no rows with ${textContent} in the ${nthHeaderError(headerRowIndex)}` +) => { + if (typeof textQuery === 'string') { + return `Found no rows with ${textQuery} in the ${nthHeaderError( + headerRowIndex + )}` + } + return `Found no rows matching ${textQuery} in the ${nthHeaderError( + headerRowIndex + )}` +} const [ queryColumnCellByHeaderText, diff --git a/src/queries.test.ts b/src/queries.test.ts index 6aba795..0226a63 100644 --- a/src/queries.test.ts +++ b/src/queries.test.ts @@ -206,77 +206,174 @@ Ignored nodes: comments, script, style expect(queries.getAllCells(container)).toHaveLength(48) }) - it('should find cells by row and column headings', () => { - const container = render(simpleTable) - expect( - queries.getCellByRowAndColumnHeaders(container, 'trouble', 'Status').id - ).toEqual('body-cell-29') - expect( - queries.getCellByRowAndColumnHeaders(container, 'reason', 'Age').id - ).toEqual('body-cell-21') - expect( - queries.queryCellByRowAndColumnHeaders(container, 'NOT A ROW', 'Status') - ).toBeNull() - expect( - queries.queryCellByRowAndColumnHeaders( + describe('should find cells by row and column headings', () => { + it('using exact test match', () => { + const container = render(simpleTable) + expect( + queries.getCellByRowAndColumnHeaders(container, 'trouble', 'Status').id + ).toEqual('body-cell-29') + expect( + queries.getCellByRowAndColumnHeaders(container, 'reason', 'Age').id + ).toEqual('body-cell-21') + expect( + queries.queryCellByRowAndColumnHeaders(container, 'NOT A ROW', 'Status') + ).toBeNull() + expect( + queries.queryCellByRowAndColumnHeaders( + container, + 'trouble', + 'Not a column' + ) + ).toBeNull() + }) + it('using a regex', () => { + const container = render(simpleTable) + expect( + queries.getCellByRowAndColumnHeaders(container, /trouble/, /Status/).id + ).toEqual('body-cell-29') + expect( + queries.getCellByRowAndColumnHeaders(container, /reason/, /Age/).id + ).toEqual('body-cell-21') + expect( + queries.queryCellByRowAndColumnHeaders(container, /NOT A ROW/, /Status/) + ).toBeNull() + expect( + queries.queryCellByRowAndColumnHeaders( + container, + /trouble/, + /Not a column/ + ) + ).toBeNull() + expect( + queries.queryCellByRowAndColumnHeaders(container, /.*/, /Not a column/) + ).toBeNull() + expect( + queries.queryAllCellsByRowAndColumnHeaders(container, /.*/, /Age/) + ).toHaveLength(8) + expect( + queries.queryAllCellsByRowAndColumnHeaders(container, /reason/, /.*/) + ).toHaveLength(1) + expect( + queries.queryAllCellsByRowAndColumnHeaders(container, /.*/, /.*/) + ).toHaveLength(8) + }) + }) + + describe('should find column cells by header text', () => { + it('using exact test match', () => { + const container = render(simpleTable) + expect( + queries.queryAllColumnCellsByHeaderText(container, 'NOT A COLUMN') + ).toHaveLength(0) + const ageCells = queries.getAllColumnCellsByHeaderText(container, 'Age') + expect(ageCells).toHaveLength(8) + expect(ageCells.map((cell) => cell.id)).toEqual([ + 'header-cell-3', + 'body-cell-3', + 'body-cell-9', + 'body-cell-15', + 'body-cell-21', + 'body-cell-27', + 'body-cell-33', + 'body-cell-39' + ]) + const statusCells = queries.getAllColumnCellsByHeaderText( container, - 'trouble', - 'Not a column' + /Status/ ) - ).toBeNull() - }) + expect(statusCells).toHaveLength(8) + expect(statusCells.map((cell) => cell.id)).toEqual([ + 'header-cell-5', + 'body-cell-5', + 'body-cell-11', + 'body-cell-17', + 'body-cell-23', + 'body-cell-29', + 'body-cell-35', + 'body-cell-41' + ]) + }) - it('should find column cells by header text', () => { - const container = render(simpleTable) - expect( - queries.queryAllColumnCellsByHeaderText(container, 'NOT A COLUMN') - ).toHaveLength(0) - const ageCells = queries.getAllColumnCellsByHeaderText(container, 'Age') - expect(ageCells).toHaveLength(8) - expect(ageCells.map((cell) => cell.id)).toEqual([ - 'header-cell-3', - 'body-cell-3', - 'body-cell-9', - 'body-cell-15', - 'body-cell-21', - 'body-cell-27', - 'body-cell-33', - 'body-cell-39' - ]) - const statusCells = queries.getAllColumnCellsByHeaderText( - container, - 'Status' - ) - expect(statusCells).toHaveLength(8) - expect(statusCells.map((cell) => cell.id)).toEqual([ - 'header-cell-5', - 'body-cell-5', - 'body-cell-11', - 'body-cell-17', - 'body-cell-23', - 'body-cell-29', - 'body-cell-35', - 'body-cell-41' - ]) + it('using a regex match', () => { + const container = render(simpleTable) + expect( + queries.queryAllColumnCellsByHeaderText(container, /NOT A COLUMN/) + ).toHaveLength(0) + expect( + queries.queryAllColumnCellsByHeaderText(container, /.*/) + ).toHaveLength(8) + const ageCells = queries.getAllColumnCellsByHeaderText(container, /Age/) + expect(ageCells).toHaveLength(8) + expect(ageCells.map((cell) => cell.id)).toEqual([ + 'header-cell-3', + 'body-cell-3', + 'body-cell-9', + 'body-cell-15', + 'body-cell-21', + 'body-cell-27', + 'body-cell-33', + 'body-cell-39' + ]) + const statusCells = queries.getAllColumnCellsByHeaderText( + container, + /Status/ + ) + expect(statusCells).toHaveLength(8) + expect(statusCells.map((cell) => cell.id)).toEqual([ + 'header-cell-5', + 'body-cell-5', + 'body-cell-11', + 'body-cell-17', + 'body-cell-23', + 'body-cell-29', + 'body-cell-35', + 'body-cell-41' + ]) + }) }) - it('should find rows by the first cell text', () => { - const container = render(simpleTable) - expect( - queries.queryAllRowsByFirstCellText(container, 'NOT A ROW') - ).toHaveLength(0) - expect(queries.getAllRowsByFirstCellText(container, 'reason')).toHaveLength( - 1 - ) - expect(queries.getRowByFirstCellText(container, 'reason').id).toEqual( - 'body-row-4' - ) - expect(queries.getRowByFirstCellText(container, 'First Name').id).toEqual( - 'header-row' - ) - expect(queries.getRowByFirstCellText(container, 'midnight').id).toEqual( - 'body-row-2' - ) + describe('should find rows by the first cell text', () => { + it('using exact text match', () => { + const container = render(simpleTable) + expect( + queries.queryAllRowsByFirstCellText(container, 'NOT A ROW') + ).toHaveLength(0) + expect( + queries.getAllRowsByFirstCellText(container, 'reason') + ).toHaveLength(1) + expect(queries.getRowByFirstCellText(container, 'reason').id).toEqual( + 'body-row-4' + ) + expect(queries.getRowByFirstCellText(container, 'First Name').id).toEqual( + 'header-row' + ) + expect(queries.getRowByFirstCellText(container, 'midnight').id).toEqual( + 'body-row-2' + ) + }) + + it('using a regex', () => { + const container = render(simpleTable) + expect( + queries.queryAllRowsByFirstCellText( + container, + /this-regex-has-no-match/ + ) + ).toHaveLength(0) + expect( + queries.getAllRowsByFirstCellText(container, /reas*/) + ).toHaveLength(1) + expect(queries.getAllRowsByFirstCellText(container, /.*/)).toHaveLength(8) + expect(queries.getRowByFirstCellText(container, 'reason').id).toEqual( + 'body-row-4' + ) + expect(queries.getRowByFirstCellText(container, 'First Name').id).toEqual( + 'header-row' + ) + expect(queries.getRowByFirstCellText(container, 'midnight').id).toEqual( + 'body-row-2' + ) + }) }) it('should find rowgroups', () => { diff --git a/src/rowByFirstCellText.ts b/src/rowByFirstCellText.ts index a8523ab..44307f6 100644 --- a/src/rowByFirstCellText.ts +++ b/src/rowByFirstCellText.ts @@ -4,7 +4,7 @@ import { queryAllCells } from './cells' function queryAllRowsByFirstCellText( container: HTMLElement, - textContent: string + textQuery: string | RegExp, ) { const rows = queryAllRows(container) return rows.filter((row) => { @@ -13,17 +13,29 @@ function queryAllRowsByFirstCellText( return false } - // TODO - make normaliser customisable, support textmatch - return ( - getDefaultNormalizer()(cellsInRow[0].textContent || '') === textContent - ) + const cellNormalizedTextContent = getDefaultNormalizer()(cellsInRow[0].textContent || ''); + + if (typeof textQuery === 'string') { + return cellNormalizedTextContent === textQuery + } + + return textQuery.test(cellNormalizedTextContent) }) } -const getMultipleError = (_c: Element | null, textContent: string) => - `Found multiple rows with ${textContent} in the first cell` -const getMissingError = (_c: Element | null, textContent: string) => - `Found no rows with ${textContent} in the first cell` +const getMultipleError = (_c: Element | null, textQuery: string | RegExp) => { + if (typeof textQuery === 'string') { + return `Found multiple rows with ${textQuery} in the first cell` + } + return `Found multiple rows matching ${textQuery} in the first cell` +} + +const getMissingError = (_c: Element | null, textQuery: string | RegExp) => { + if (typeof textQuery === 'string') { + return `Found no rows with ${textQuery} in the first cell` + } + return `Found no rows matching ${textQuery} in the first cell` +} const [ queryRowByFirstCellText, @@ -31,7 +43,7 @@ const [ getRowByFirstCellText, findAllRowsByFirstCellText, findRowByFirstCellText -] = queryHelpers.buildQueries( +] = queryHelpers.buildQueries<[string | RegExp]>( queryAllRowsByFirstCellText, getMultipleError, getMissingError diff --git a/src/utils/columnIndexByHeaderText.ts b/src/utils/columnIndexByHeaderText.ts index 05aa8a2..dc9dd29 100644 --- a/src/utils/columnIndexByHeaderText.ts +++ b/src/utils/columnIndexByHeaderText.ts @@ -6,7 +6,7 @@ import { getColspan } from './colspan' export const getColumnIndexByHeaderText = ( container: HTMLElement, - textContent: string, + textQuery: string | RegExp, headerRowIndex = 0 ) => { const headerCellsByRow = queryAllRowsByRowgroupType( @@ -21,9 +21,17 @@ export const getColumnIndexByHeaderText = ( const headerRowToUse = headerCellsByRow[headerRowIndex] const cellIndex = headerRowToUse.findIndex((cell) => { - // TODO - allow normaliser to be overridden - return getDefaultNormalizer()(cell.textContent || '') === textContent + const cellNormalizedTextContent = getDefaultNormalizer()( + cell.textContent || '' + ) + + if (typeof textQuery === 'string') { + return cellNormalizedTextContent === textQuery + } + + return textQuery.test(cellNormalizedTextContent) }) + if (cellIndex === -1) { return -1 } From 5cdf27ebcc097c7291738cf873e7bb2041b4161d Mon Sep 17 00:00:00 2001 From: Alex Anthony Date: Sun, 18 May 2025 18:14:51 +0100 Subject: [PATCH 2/3] Tidy other errors --- src/cellByRowAndColumnHeaders.ts | 17 +++++++++++------ src/columnCellsByHeaderText.ts | 21 +++++---------------- src/queries.test.ts | 4 ++-- src/utils/stringOrRegexError.ts | 2 ++ 4 files changed, 20 insertions(+), 24 deletions(-) create mode 100644 src/utils/stringOrRegexError.ts diff --git a/src/cellByRowAndColumnHeaders.ts b/src/cellByRowAndColumnHeaders.ts index 3511889..dc7d80e 100644 --- a/src/cellByRowAndColumnHeaders.ts +++ b/src/cellByRowAndColumnHeaders.ts @@ -3,6 +3,7 @@ import { queryAllRowsByFirstCellText } from './rowByFirstCellText' import { getColumnIndexByHeaderText } from './utils/columnIndexByHeaderText' import { getCellInRowByIndex } from './utils/cellInRowByIndex' import { nthHeaderError } from './utils/nthHeaderError' +import { stringOrRegexError } from './utils/stringOrRegexError' function queryAllCellsByRowAndColumnHeaders( container: HTMLElement, @@ -29,9 +30,11 @@ const getMultipleError = ( columnheaderText: string | RegExp, headerRowIndex = 0 ) => - `Found multiple cells with ${rowHeaderText} in the first column and ${columnheaderText} in the ${nthHeaderError( - headerRowIndex - )}` + `Found multiple cells ${stringOrRegexError( + rowHeaderText + )} in the first column and ${stringOrRegexError( + columnheaderText + )} in the ${nthHeaderError(headerRowIndex)}` const getMissingError = ( _c: Element | null, @@ -39,9 +42,11 @@ const getMissingError = ( columnheaderText: string | RegExp, headerRowIndex = 0 ) => - `Found no rows with ${rowHeaderText} in the first column and ${columnheaderText} in the ${nthHeaderError( - headerRowIndex - )}` + `Found no rows ${stringOrRegexError( + rowHeaderText + )} in the first column and ${stringOrRegexError( + columnheaderText + )} in the ${nthHeaderError(headerRowIndex)}` const [ queryCellByRowAndColumnHeaders, diff --git a/src/columnCellsByHeaderText.ts b/src/columnCellsByHeaderText.ts index c33997b..f2d73be 100644 --- a/src/columnCellsByHeaderText.ts +++ b/src/columnCellsByHeaderText.ts @@ -3,6 +3,7 @@ import { queryAllRows } from './rows' import { getColumnIndexByHeaderText } from './utils/columnIndexByHeaderText' import { getCellInRowByIndex } from './utils/cellInRowByIndex' import { nthHeaderError } from './utils/nthHeaderError' +import { stringOrRegexError } from './utils/stringOrRegexError' function queryAllColumnCellsByHeaderText( container: HTMLElement, @@ -26,31 +27,19 @@ const getMultipleError = ( _c: Element | null, textQuery: string | RegExp, headerRowIndex = 0 -) => { - if (typeof textQuery === 'string') { - return `Found multiple cells with ${textQuery} in the ${nthHeaderError( - headerRowIndex - )}` - } - return `Found multiple cells matching ${textQuery} in the ${nthHeaderError( +) => + `Found mutiple cells ${stringOrRegexError(textQuery)} in the ${nthHeaderError( headerRowIndex )}` -} const getMissingError = ( _c: Element | null, textQuery: string | RegExp, headerRowIndex = 0 -) => { - if (typeof textQuery === 'string') { - return `Found no rows with ${textQuery} in the ${nthHeaderError( - headerRowIndex - )}` - } - return `Found no rows matching ${textQuery} in the ${nthHeaderError( +) => + `Found no rows ${stringOrRegexError(textQuery)} in the ${nthHeaderError( headerRowIndex )}` -} const [ queryColumnCellByHeaderText, diff --git a/src/queries.test.ts b/src/queries.test.ts index 0226a63..d7d3dcc 100644 --- a/src/queries.test.ts +++ b/src/queries.test.ts @@ -144,7 +144,7 @@ Ignored nodes: comments, script, style `) expect(() => queries.getCellByRowAndColumnHeaders(container, 'A', 'B')) .toThrowErrorMatchingInlineSnapshot(` -"Found no rows with A in the first column and B in the header +"Found no rows with A in the first column and with B in the header 
 
 @@ -152,7 +152,7 @@ Ignored nodes: comments, script, style `) expect(() => queries.getCellByRowAndColumnHeaders(container, 'A', 'B', 1)) .toThrowErrorMatchingInlineSnapshot(` -"Found no rows with A in the first column and B in the 2nd header +"Found no rows with A in the first column and with B in the 2nd header 
 
 diff --git a/src/utils/stringOrRegexError.ts b/src/utils/stringOrRegexError.ts new file mode 100644 index 0000000..9f2dce5 --- /dev/null +++ b/src/utils/stringOrRegexError.ts @@ -0,0 +1,2 @@ +export const stringOrRegexError = (text: string | RegExp) => + typeof text === 'string' ? `with ${text}` : `matching ${text}` From dd9d0156ec8ba0d197853ddf932c592adbec7ac3 Mon Sep 17 00:00:00 2001 From: Alex Anthony Date: Sun, 18 May 2025 18:23:21 +0100 Subject: [PATCH 3/3] Fix v8+ snapshot --- src/queries.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/queries.test.ts b/src/queries.test.ts index d7d3dcc..a04e48c 100644 --- a/src/queries.test.ts +++ b/src/queries.test.ts @@ -64,7 +64,7 @@ Ignored nodes: comments, script, style `) expect(() => queries.getCellByRowAndColumnHeaders(container, 'A', 'B')) .toThrowErrorMatchingInlineSnapshot(` -"Found no rows with A in the first column and B in the header +"Found no rows with A in the first column and with B in the header Ignored nodes: comments, script, style 
 @@ -73,7 +73,7 @@ Ignored nodes: comments, script, style `) expect(() => queries.getCellByRowAndColumnHeaders(container, 'A', 'B', 1)) .toThrowErrorMatchingInlineSnapshot(` -"Found no rows with A in the first column and B in the 2nd header +"Found no rows with A in the first column and with B in the 2nd header Ignored nodes: comments, script, style