diff --git a/tests/elements/graph.test.ts b/tests/elements/graph.test.ts new file mode 100644 index 0000000..01357aa --- /dev/null +++ b/tests/elements/graph.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { labelsFor } from '../../assets/src/elements/labels' +import { HostFailure, mountWidget, settle } from './host' + +// E9 golem15-seo-graph (UI-SPEC C18): the checking line with aria-busy +// while the request runs, the noRules line per type without Google rules, +// at most one queued request, and the danger line that keeps the last +// graph. The first render and the 800 ms re-check are in schema.test.ts. + +beforeAll(async () => { + await import('../../assets/src/elements/index') +}) + +afterEach(() => { + document.body.replaceChildren() +}) + +const en = labelsFor('en') + +const answer = { + locale: 'en', + url: 'https://example.test/x', + graph: { '@context': 'https://schema.org', '@graph': [] }, + nodes: [ + { id: 'https://example.test/x#webpage', type: 'WebPage', rules: false, findings: [] }, + { id: 'https://example.test/x#service', type: 'Service', rules: false, findings: [] }, + { id: 'https://example.test/x#website', type: 'WebSite', rules: true, findings: [{ level: 'required', property: 'name' }] }, + ], +} + +describe('E9 golem15-seo-graph states', () => { + it('shows the checking line and aria-busy while the request runs, neither after it', async () => { + const host = mountWidget('golem15-seo-graph', { attrs: { 'ui-locale': 'en' }, answer: () => answer }) + await Promise.resolve() + expect(host.node.getAttribute('aria-busy')).toBe('true') + expect(host.node.querySelector('.seo-graph__checking')!.textContent).toBe(en.graphChecking) + await settle() + expect(host.node.getAttribute('aria-busy')).toBe('false') + expect(host.node.querySelector('.seo-graph__checking')).toBeNull() + }) + + it('writes one noRules line per node type without Google rules and the finding of the others', async () => { + const host = mountWidget('golem15-seo-graph', { attrs: { 'ui-locale': 'en' }, answer: () => answer }) + await settle() + const text = host.node.textContent ?? '' + expect(text).toContain(en.noRules.replace(':type', 'WebPage')) + expect(text).toContain(en.noRules.replace(':type', 'Service')) + expect(text).not.toContain(en.noRules.replace(':type', 'WebSite')) + expect(text).toContain('Missing required property “name” (WebSite)') + expect(text).not.toContain(en.allGood) + }) + + it('queues at most one request while one runs', async () => { + const host = mountWidget('golem15-seo-graph', { attrs: { 'ui-locale': 'en' }, answer: () => answer }) + await Promise.resolve() + const node = host.node as HTMLElement & { check(): void } + node.check() + node.check() + node.check() + await settle() + expect(host.posted).toHaveLength(2) + expect(host.node.getAttribute('aria-busy')).toBe('false') + }) + + it('shows the danger line on the first failure and clears it on the next success', async () => { + let fail = true + const host = mountWidget('golem15-seo-graph', { + attrs: { 'ui-locale': 'en' }, + answer: () => { + if (fail) { + throw new HostFailure('down') + } + return answer + }, + }) + await settle() + expect(host.node.querySelector('.seo-graph__error')!.textContent).toBe(en.graphFailed) + expect(host.node.getAttribute('aria-busy')).toBe('false') + fail = false + host.node.querySelector('.seo-graph__button')!.click() + await settle() + expect(host.node.querySelector('.seo-graph__error')).toBeNull() + expect(host.node.textContent).toContain('WebSite') + }) + + it('renders the Polish labels with ui-locale pl', async () => { + const host = mountWidget('golem15-seo-graph', { attrs: { 'ui-locale': 'pl' }, answer: () => ({ ...answer, nodes: [answer.nodes[0]] }) }) + await settle() + expect(host.node.textContent).toContain(labelsFor('pl').noRules.replace(':type', 'WebPage')) + }) +}) diff --git a/tests/elements/links.test.ts b/tests/elements/links.test.ts index 62a7506..44fc90c 100644 --- a/tests/elements/links.test.ts +++ b/tests/elements/links.test.ts @@ -121,6 +121,22 @@ describe('E10 golem15-seo-links', () => { expect(host.node.querySelector('.seo-links__url')!.textContent).toBe('https://example.com/pages/shiitake') }) + it('lists at most 10 pages in each list', async () => { + const many: LinksData = { + ...crawled, + orphan: false, + out: Array.from({ length: 14 }, (_, i) => ({ phrase: `p${i}`, title: `Out ${i}`, url: `https://example.com/o/${i}` })), + in: Array.from({ length: 12 }, (_, i) => ({ phrase: `q${i}`, title: `In ${i}`, adminUrl: `/backend/x/${i}` })), + } + const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => many }) + await settle() + expect(host.node.querySelectorAll('.seo-links__list--out .seo-links__row')).toHaveLength(10) + expect(host.node.querySelectorAll('.seo-links__list--in .seo-links__row')).toHaveLength(10) + expect(host.node.textContent).toContain('Out 9') + expect(host.node.textContent).not.toContain('Out 10') + expect(host.node.querySelector('.seo-links__orphan')).toBeNull() + }) + it('asks again on the ML locale switch', async () => { const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled }) await settle() diff --git a/tests/elements/parent.test.ts b/tests/elements/parent.test.ts new file mode 100644 index 0000000..837868e --- /dev/null +++ b/tests/elements/parent.test.ts @@ -0,0 +1,66 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { labelsFor } from '../../assets/src/elements/labels' +import { parseParent } from '../../assets/src/elements/parent' + +// golem15-seo-parent (P33-23, D-86): the primary category select. The +// first-render case is in schema.test.ts. + +beforeAll(async () => { + await import('../../assets/src/elements/index') +}) + +afterEach(() => { + document.body.replaceChildren() +}) + +function mount(value: unknown, attrs: Record = {}): HTMLElement & { value: unknown } { + const node = document.createElement('golem15-seo-parent') as HTMLElement & { value: unknown } + node.setAttribute('ui-locale', 'en') + for (const [k, v] of Object.entries(attrs)) { + node.setAttribute(k, v) + } + node.value = value + document.body.append(node) + return node +} + +describe('parseParent', () => { + it('reads the loaded object, a plain stored choice and junk', () => { + expect(parseParent('cat:1')).toEqual({ selected: 'cat:1', options: [] }) + expect(parseParent({ selected: 'cat:2', options: [{ value: 'cat:2', label: '' }, { value: 'cat:3', label: 'Three' }, { label: 'no value' }, null, 'x'] })).toEqual({ + selected: 'cat:2', + options: [ + { value: 'cat:2', label: 'cat:2' }, + { value: 'cat:3', label: 'Three' }, + ], + }) + expect(parseParent({ selected: 5, options: 'nope' })).toEqual({ selected: '', options: [] }) + expect(parseParent(null)).toEqual({ selected: '', options: [] }) + }) +}) + +describe('golem15-seo-parent', () => { + it('starts on the empty default option when nothing is stored', () => { + const node = mount({ selected: '', options: [{ value: 'cat:1', label: 'Recipes' }] }) + const select = node.querySelector('select')! + expect(select.options[0]!.value).toBe('') + expect(select.options[0]!.textContent).toBe(labelsFor('en').primaryDefault) + expect(select.value).toBe('') + expect(node.value).toBe('') + }) + + it('keeps its options when the stored choice is set as a string', () => { + const node = mount({ selected: '', options: [{ value: 'cat:1', label: 'Recipes' }, { value: 'cat:2', label: 'Mushrooms' }] }) + node.value = 'cat:2' + expect(node.value).toBe('cat:2') + expect(node.querySelector('select')!.value).toBe('cat:2') + expect(node.querySelectorAll('option')).toHaveLength(3) + }) + + it('disables the select with the disabled attribute and speaks Polish with ui-locale pl', () => { + const node = mount({ selected: '', options: [] }, { disabled: '' }) + expect(node.querySelector('select')!.disabled).toBe(true) + node.setAttribute('ui-locale', 'pl') + expect(node.querySelector('option')!.textContent).toBe(labelsFor('pl').primaryDefault) + }) +}) diff --git a/tests/elements/schema.test.ts b/tests/elements/schema.test.ts index bd4c0cb..5983bbe 100644 --- a/tests/elements/schema.test.ts +++ b/tests/elements/schema.test.ts @@ -89,6 +89,26 @@ describe('E8 golem15-seo-schema', () => { expect(node.querySelector('.seo-schema__status')!.textContent).toBe(labelsFor('en').jsonTooLarge) }) + it('renders the page type options and a fixed ProfilePage for profiles', () => { + const page = mountSchema('page', '') + const en = labelsFor('en') + expect([...page.querySelectorAll('option')].map((o) => o.textContent)).toEqual(['Default (WebPage)', en.tWebPage, en.tAboutPage, en.tContactPage, en.tService]) + document.body.replaceChildren() + const profile = mountSchema('profile', '') + expect(profile.querySelector('select')).toBeNull() + expect(profile.querySelector('.seo-schema__fixed')!.textContent).toBe(en.tProfilePage) + }) + + it('flags an invalid currency and keeps the Service fields out of a non-Service value', () => { + const node = mountSchema('page', JSON.stringify({ type: 'Service', fields: { currency: 'PLN' }, custom: '' })) + const currency = node.querySelectorAll('.seo-schema__grid input')[3] + currency.value = 'ZLOTY' + currency.dispatchEvent(new Event('input')) + expect(node.textContent).toContain(labelsFor('en').currencyInvalid) + expect(JSON.parse(node.value).fields.currency).toBe('PLN') + expect(serializeValue(parseValue(JSON.stringify({ type: 'AboutPage', fields: {}, custom: '' })))).toBe(JSON.stringify({ type: 'AboutPage', fields: {}, custom: '' })) + }) + it('shows a single fixed type for collections', () => { const node = mountSchema('collection', '') expect(node.querySelector('select')).toBeNull()