fix: SEO SPA editor last fixes (3/4)

This commit is contained in:
Florian Sylvain committed 2026-10-16 16:31:17 +02:00
1 parent 470584400f
commit cb737b7ae3
19 files changed
+316 -147

No files matched your search

@@ -214,9 +214,9 @@ describe('A late answer never overwrites a newer one', () => {
gates[0]!.resolve(checkAnswer([], 'hidden'))
await flushPromises()
expect(textOf(sectionBody())).toContain('This article has no cover image.')
expect(textOf(sectionBody())).toContain("No cover image. The site's social image is used.")
expect(textOf(sectionBody())).not.toContain('Hidden from search engines')
expect(textOf(panelRoot()!)).toContain('This article has no cover image.')
expect(textOf(panelRoot()!)).toContain("No cover image. The site's social image is used.")
})
})
@@ -222,13 +222,13 @@ describe('Formatted', () => {
})
it.each([
{ key: 'title', source: 'title', chip: 'Using article title' },
{ key: 'description', source: 'body', chip: 'Using first sentence of the body' },
{ key: 'description', source: 'excerpt', chip: 'Using the excerpt' },
{ key: 'description', source: 'site', chip: 'Using the site default description' },
{ key: 'image', source: 'cover', chip: 'Using cover image' },
{ key: 'image', source: 'site', chip: 'Using site social image' },
{ key: 'canonical', source: 'site', chip: 'Using site address and slug' },
{ key: 'title', source: 'title', chip: 'article title + template' },
{ key: 'description', source: 'body', chip: 'first sentence of the body' },
{ key: 'description', source: 'excerpt', chip: 'excerpt' },
{ key: 'description', source: 'site', chip: 'site default description' },
{ key: 'image', source: 'cover', chip: 'cover image' },
{ key: 'image', source: 'site', chip: 'site social image' },
{ key: 'canonical', source: 'site', chip: 'site address + slug' },
{ key: 'image', source: 'none', chip: 'No image' },
])('shows "$chip" when fallbacks.$key is $source', async ({ key, source, chip }) => {
await mountWithDialog({
@@ -427,6 +427,8 @@ describe('Advanced, canonical URL', () => {
it.each([
{ value: 'renew.example/costs', error: true },
{ value: 'http://renew.example/a', error: true },
{ value: ' https://renew.example/a', error: true },
{ value: 'https://renew.example/a ', error: true },
{ value: 'https://renew.example/a', error: false },
{ value: '', error: false },
])('for "$value" the error is shown: $error', async ({ value, error }) => {
@@ -149,6 +149,8 @@ describe('Saving state', () => {
expect(fieldByLabel(panel, 'Meta description')!.disabled).toBe(true)
expect(fieldByLabel(panel, 'Excerpt')!.disabled).toBe(true)
expect(textOf(panel)).toContain('Saving…')
expect(fieldByLabel(panel, 'Meta title')!.className).toContain('disabled:opacity-100')
expect(fieldByLabel(panel, 'Meta title')!.className).not.toContain('opacity-40')
expect(
textOf(document.body.querySelector('[data-testid="save-button"]')!.parentElement),
).toContain('Save')
@@ -286,6 +288,22 @@ describe('The editor Save button treats SEO edits like any other edit', () => {
})
})
describe('Publish with a clean draft', () => {
it('only sends the publish request', async () => {
const backend = await mountSeo(seoBackend())
backend.responders['PATCH /v1/article/12/publish'] = () => ({
...backend.article,
is_online: true,
})
const before = backend.calls.length
await click('Publish')
const patches = backend.calls.slice(before).filter((call) => call.method === 'PATCH')
expect(patches.map((call) => call.path)).toEqual(['/v1/article/12/publish'])
})
})
describe('Leaving with unsaved SEO edits', () => {
it('asks Leave without saving? when the draft holds an unsaved SEO value', async () => {
const { panel } = await failedPanelSave()
@@ -111,7 +111,7 @@ describe('The open section: address, suggestions and actions', () => {
expect(panel).not.toBeNull()
const suggestions = panel.querySelector('[data-testid="seo-panel-suggestions"]')!
expect(document.activeElement).toBe(suggestions)
expect(textOf(suggestions)).toContain('The body is 40 characters.')
expect(textOf(suggestions)).toContain('The article is under 300 words.')
})
it('shows no "+N more" action with three suggestions or fewer', async () => {
@@ -263,28 +263,33 @@ describe('The messages follow the params of the response', () => {
{},
'The body has no headings. H2 headings help readers scan the article.',
],
['title_empty', 'seo_title', {}, 'The meta title is empty.'],
['title_empty', 'seo_title', {}, 'No meta title yet. The article title is used instead.'],
[
'desc_long',
'seo_description',
{ length: 170, max: 160 },
'The description is 170 characters. About 70 to 160 reads best in results.',
'The description is 170 characters. Search engines may cut it off after about 160.',
],
['cover_none', 'cover_image_id', {}, 'This article has no cover image.'],
['cover_none', 'cover_image_id', {}, "No cover image. The site's social image is used."],
[
'slug_long',
'slug',
{ length: 80, max: 75 },
'The URL slug is 80 characters. Shorter addresses are easier to share.',
'The slug is 80 characters. Shorter addresses are easier to share.',
],
[
'no_au',
'author_ids',
{},
'This article has no authors, so the structured data has no author.',
],
['no_au', 'author_ids', {}, 'This article has no author.'],
['body_alt', 'body', { count: 2 }, '2 images in the body have no alt text.'],
['body_alt', 'body', { count: 1 }, '1 image in the body has no alt text.'],
[
'short',
'body',
{ length: 40, min: 300 },
'The body is 40 characters. Longer articles tend to do better in search.',
'The article is under 300 words. That is fine for a short note.',
],
] as const)('renders %s with %j', async (code, field, params, message) => {
const backend = seoBackend()