import { afterEach, beforeAll, describe, expect, it } from 'vitest' import { labelsFor } from '../../assets/src/elements/labels' import { HostFailure, mountWidget, settle, type MountOptions } from './host' // E5 golem15-seo-robotstester (UI-SPEC C10) through the widget host // contract: the context request from connectedCallback, the queue behind // `busy`, state=error, the address check and a host that drops actions. beforeAll(async () => { await import('../../assets/src/elements/index') }) afterEach(() => { document.body.replaceChildren() }) type Payload = { op: 'context' } | { op: 'test'; url: string; agent: string } function answer(payload: unknown): unknown { const p = payload as Payload if (p.op === 'context') { return { op: 'context', agents: ['Googlebot', 'Bingbot', 'GPTBot', '*'], bots: ['GPTBot'], staging: false } } return { op: 'test', url: p.url, allowed: p.url !== '/admin', line: p.url === '/admin' ? 2 : 0, rule: p.url === '/admin' ? 'Disallow: /admin' : '', bot: false, agent: p.agent, staging: false } } function tester(options: Partial = {}) { return mountWidget('golem15-seo-robotstester', { attrs: { 'ui-locale': 'en', label: 'Test URL', 'busy-label': 'Testing…' }, answer, ...options, }) } function testURL(node: HTMLElement, url: string): void { const input = node.querySelector('.seo-robotstester__input')! input.value = url node.querySelector('.seo-robotstester__button')!.click() } function resultLine(node: HTMLElement): string { return node.querySelector('.seo-robotstester__result')?.textContent ?? '' } function agents(node: HTMLElement): string[] { return Array.from(node.querySelectorAll('.seo-robotstester__select option')).map((o) => o.textContent ?? '') } describe('E5 host protocol', () => { it('loads the crawler list from a host that appends the element before wiring it (CR-04)', async () => { const host = tester({ order: 'append-first' }) await settle() expect(host.posted[0]).toEqual({ op: 'context' }) expect(agents(host.node)).toEqual(['Googlebot', 'Bingbot', 'GPTBot', '*']) }) it('a test asked while the context loads is queued and sent after busy clears', async () => { const host = tester({ order: 'assign-first' }) await Promise.resolve() expect(host.busy()).toBe(true) // The button is disabled while busy; Enter in the address field still asks. const input = host.node.querySelector('.seo-robotstester__input')! input.value = '/admin' input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })) expect(host.posted).toEqual([{ op: 'context' }]) await settle() expect(host.posted).toEqual([{ op: 'context' }, { op: 'test', url: '/admin', agent: 'Googlebot' }]) expect(resultLine(host.node)).toContain('Disallow: /admin') }) it('the button shows the busy label while a test runs', async () => { const host = tester() await settle() host.node.setAttribute('busy', '') expect(host.node.querySelector('.seo-robotstester__button')?.textContent).toBe('Testing…') host.node.removeAttribute('busy') expect(host.node.querySelector('.seo-robotstester__button')?.textContent).toBe('Test URL') }) it('state=error unblocks the next test', async () => { let failNext = true const host = tester({ answer: (payload) => { if ((payload as Payload).op === 'test' && failNext) { failNext = false throw new HostFailure('test') } return answer(payload) }, }) await settle() testURL(host.node, '/admin') await settle() expect(host.node.getAttribute('state')).toBe('error') testURL(host.node, '/blog') await settle() expect(host.posted.slice(-1)).toEqual([{ op: 'test', url: '/blog', agent: 'Googlebot' }]) expect(resultLine(host.node)).not.toBe('') }) it('retries a test the host dropped once', async () => { const host = tester() await settle() host.drop(1) testURL(host.node, '/admin') await settle() expect(host.emitted.filter((p) => (p as Payload).op === 'test')).toHaveLength(2) expect(host.posted.slice(-1)).toEqual([{ op: 'test', url: '/admin', agent: 'Googlebot' }]) expect(resultLine(host.node)).toContain('Disallow: /admin') }) it('says the test failed when the host never takes it', async () => { const host = tester({ wired: false }) await settle() expect(agents(host.node)).toEqual(['Googlebot', 'Googlebot-Image', 'Bingbot', '*']) testURL(host.node, '/admin') await settle() expect(resultLine(host.node)).toBe(labelsFor('en').testFailed) }) it('an address without a leading slash is refused without an action', async () => { const host = tester() await settle() const before = host.emitted.length testURL(host.node, 'admin') await settle() expect(host.emitted.length).toBe(before) expect(resultLine(host.node)).toBe(labelsFor('en').urlRequired) }) it('a request in flight when the element is disconnected does not block it after reconnecting', async () => { const host = tester() await Promise.resolve() expect(host.busy()).toBe(true) host.node.remove() await settle() document.body.append(host.node) await settle() expect(agents(host.node)).toEqual(['Googlebot', 'Bingbot', 'GPTBot', '*']) testURL(host.node, '/admin') await settle() expect(resultLine(host.node)).toContain('Disallow: /admin') }) })