import { ListEnumerationMismatchError } from '../../errors/ListEnumerationMismatchError'; import { Interactor } from '../../interactor/Interactor'; import { byDataTestId } from '../../locators/byDataTestId'; import type { PartLocator } from '../../locators/PartLocator'; import { ComponentDriver } from '../ComponentDriver'; import { collectItemLabels, getListItemByIndex, getListItemCount, getListItemIterator } from '../listHelper'; class LeafDriver extends ComponentDriver<{}> { get driverName(): string { return 'LeafDriver'; } } const itemLocatorBase = byDataTestId('row'); /** A fake Interactor whose `exists()` answers true for :nth-of-type positions * below `positionCount`, modeling a homogeneous list of exactly that many items. * * `getElementCount` answers `matchCount`, which defaults to agreeing with the * positional view. Passing the two separately is what lets a test model a * NON-homogeneous list — the disagreement between "positions walked" and "elements * matched" IS the defect — but note this fake can only model the disagreement, not * produce it: it parses `:nth-of-type(n)` out of the selector rather than resolving * it, so it encodes the homogeneity assumption by construction. The real-DOM * consequences are covered in dom-core/__tests__/listEnumeration.dom.test.ts. */ function createInteractor(positionCount: number, matchCount: number = positionCount): Interactor { const exists = jest.fn(async (locator: PartLocator) => { const [{ selector }] = locator.slice(-1); const match = /:nth-of-type\((\d+)\)$/.exec(selector); const position = match ? Number(match[1]) : NaN; return position >= 1 && position <= positionCount; }); const getElementCount = jest.fn(async () => matchCount); return { exists, getElementCount } as unknown as Interactor; } describe('getListItemByIndex', () => { it('addresses the i-th item with a Same-positioned :nth-of-type locator', async () => { const interactor = createInteractor(3); const host = new LeafDriver(itemLocatorBase, interactor); const item = await getListItemByIndex(host, itemLocatorBase, 0, LeafDriver); expect(item?.locator.map(loc => loc.selector)).toEqual(['[data-testid="row"]', ':nth-of-type(1)']); expect(item?.locator.at(-1)?.relative).toBe('Same'); }); // Absence is `undefined`, never `null` — ADR-006 §7. Asserted with toBeUndefined // rather than a loose falsy check so a regression back to `null` fails here. it('returns undefined when no element exists at that index', async () => { const interactor = createInteractor(0); const host = new LeafDriver(itemLocatorBase, interactor); const item = await getListItemByIndex(host, itemLocatorBase, 0, LeafDriver); expect(item).toBeUndefined(); }); it('constructs the item driver with the host interactor and commutableOption', async () => { const interactor = createInteractor(1); const host = new LeafDriver(itemLocatorBase, interactor); const item = await getListItemByIndex(host, itemLocatorBase, 0, LeafDriver); expect(item?.interactor).toBe(interactor); }); }); describe('getListItemIterator', () => { it('yields one item per index until the index stops existing', async () => { const interactor = createInteractor(3); const host = new LeafDriver(itemLocatorBase, interactor); const items = []; for await (const item of getListItemIterator(host, itemLocatorBase, LeafDriver)) { items.push(item); } expect(items).toHaveLength(3); expect(items.map(item => item.locator.at(-1)?.selector)).toEqual([ ':nth-of-type(1)', ':nth-of-type(2)', ':nth-of-type(3)', ]); }); it('starts from the given startIndex', async () => { const interactor = createInteractor(3); const host = new LeafDriver(itemLocatorBase, interactor); const items = []; for await (const item of getListItemIterator(host, itemLocatorBase, LeafDriver, 1)) { items.push(item); } expect(items.map(item => item.locator.at(-1)?.selector)).toEqual([':nth-of-type(2)', ':nth-of-type(3)']); }); it('yields nothing when the list is empty', async () => { const interactor = createInteractor(0); const host = new LeafDriver(itemLocatorBase, interactor); const items = []; for await (const item of getListItemIterator(host, itemLocatorBase, LeafDriver)) { items.push(item); } expect(items).toEqual([]); }); // Truncation used to be silent, which made it worse than a failure: the caller got // a short array indistinguishable from a genuinely short list. it('throws when a completed walk reached fewer items than the locator matches', async () => { const interactor = createInteractor(2, 5); const host = new LeafDriver(itemLocatorBase, interactor); const walk = async () => { for await (const _ of getListItemIterator(host, itemLocatorBase, LeafDriver)) { // drain } }; await expect(walk()).rejects.toThrow(ListEnumerationMismatchError); await expect(walk()).rejects.toMatchObject({ matchedCount: 5, enumeratedCount: 2 }); }); // Regression guard for a false positive this check originally had, caught by MUI X's // DataGrid suites. `startIndex` is an offset into TAG positions, not into matched // elements: DataGridRowDriverBase skips a filler