Both elements sent their first summer-action from connectedCallback and kept it in flight forever when the host dropped it (the v0.5.1 widget host appends the element before it keeps a reference). A shared ActionQueue no longer trusts that an emitted action was taken: one the host did not mark busy synchronously is retried once on the next task, a second drop shows an error, and a disconnect forgets the request in flight. The first request is sent on a microtask. Element tests drive a fake host that mirrors WidgetField's contract, including the append-first ordering, and the robots.txt tester gets its own tests.
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
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<MountOptions> = {}) {
|
|
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<HTMLInputElement>('.seo-robotstester__input')!
|
|
input.value = url
|
|
node.querySelector<HTMLButtonElement>('.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<HTMLInputElement>('.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')
|
|
})
|
|
})
|