// @vitest-environment jsdom import { afterEach, describe, expect, it } from 'vitest'; import { computeLocator, resolveLocator } from '../locator'; afterEach(() => { document.body.innerHTML = ''; }); describe('computeLocator', () => { it('builds an id selector when the element has a unique id', () => { const el = document.createElement('button'); el.id = 'submit-btn'; el.textContent = 'Submit'; document.body.appendChild(el); const locator = computeLocator(el, 'button'); expect(locator.selector).toBe('#submit-btn'); expect(locator.role).toBe('button'); expect(locator.name).toBe('Submit'); expect(locator.tag).toBe('button'); }); it('prefers data-testid over a structural path', () => { const el = document.createElement('a'); el.setAttribute('data-testid', 'nav-home'); el.textContent = 'Home'; document.body.appendChild(el); expect(computeLocator(el, 'link').selector).toBe('a[data-testid="nav-home"]'); }); it('falls back to a structural path when no stable attribute exists', () => { document.body.innerHTML = '
OneTwo
'; const second = document.body.querySelectorAll('a')[1] as HTMLElement; const locator = computeLocator(second, 'link'); expect(locator.selector).toContain('nth-of-type'); // The structural path must re-find the same element. expect(document.querySelector(locator.selector)).toBe(second); }); }); describe('resolveLocator', () => { it('resolves via selector when structure is intact', () => { const el = document.createElement('button'); el.id = 'go'; el.textContent = 'Go'; document.body.appendChild(el); const locator = computeLocator(el, 'button'); expect(resolveLocator(locator)).toBe(el); }); it('resolves by role + name when the selector breaks', () => { // Capture with a structural path (no stable attribute). document.body.innerHTML = '
Browse Catalog
'; const original = document.body.querySelector('a') as HTMLElement; const locator = computeLocator(original, 'link'); // Re-render shifts the element: extra wrapper + sibling inserted, so // the captured nth-of-type path no longer matches it. document.body.innerHTML = '
'; const resolved = resolveLocator(locator); expect(resolved).not.toBeNull(); expect(resolved?.textContent).toBe('Browse Catalog'); }); it('returns null when the element is genuinely gone', () => { const el = document.createElement('a'); el.textContent = 'Vanishing Link'; document.body.appendChild(el); const locator = computeLocator(el, 'link'); document.body.innerHTML = ''; expect(resolveLocator(locator)).toBeNull(); }); it('matches an ARIA-roled element by role + name', () => { document.body.innerHTML = '
'; const original = document.body.firstElementChild as HTMLElement; const locator = computeLocator(original, 'button'); // Recreate the node — same role + accessible name, new identity. document.body.innerHTML = '
'; const fresh = document.body.querySelectorAll('[role="button"]')[0]; expect(resolveLocator(locator)).toBe(fresh); }); });