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..dc7d80e 100644 --- a/src/cellByRowAndColumnHeaders.ts +++ b/src/cellByRowAndColumnHeaders.ts @@ -3,18 +3,19 @@ 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, - 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,22 +26,27 @@ 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 - )}` + `Found multiple cells ${stringOrRegexError( + rowHeaderText + )} in the first column and ${stringOrRegexError( + 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( - 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 3d2d60b..f2d73be 100644 --- a/src/columnCellsByHeaderText.ts +++ b/src/columnCellsByHeaderText.ts @@ -3,15 +3,16 @@ 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, - textContent: string, + textQuery: string | RegExp, headerRowIndex = 0 ) { const cellIndex = getColumnIndexByHeaderText( container, - textContent, + textQuery, headerRowIndex ) @@ -24,18 +25,21 @@ function queryAllColumnCellsByHeaderText( const getMultipleError = ( _c: Element | null, - textContent: string, + textQuery: string | RegExp, headerRowIndex = 0 ) => - `Found multiple cells with ${textContent} in the ${nthHeaderError( + `Found mutiple cells ${stringOrRegexError(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)}` + `Found no rows ${stringOrRegexError(textQuery)} in the ${nthHeaderError( + headerRowIndex + )}` const [ queryColumnCellByHeaderText, diff --git a/src/queries.test.ts b/src/queries.test.ts index 6aba795..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 
 @@ -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 
 
 @@ -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 } 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}`