feat: public API, SEO and existing surfaces

This commit is contained in:
Florian Sylvain committed 2026-11-03 03:58:10 +01:00
1 parent 45fbbc33b1
commit c390fcfe8d
103 files changed
+4476 -231

No files matched your search

+183
View File
@@ -0,0 +1,183 @@
import type { Page } from 'playwright'
import { accessFor } from '../src/test/access'
import { siteLanguagesBody } from '../src/test/storyLists'
import {
articleBody,
fails,
narrow,
ok,
press,
settle,
site,
state,
type Setup,
} from './editorLocalizationStates'
import type { ApiMock, VisualState } from './states'
const PATTERN = 'https://journal.example.com/{lang}/{slug}'
const FRENCH_SLUG = 'comment-nous-chiffrons-une-renovation'
const FRENCH_TITLE = 'Comment nous chiffrons une rénovation de salle de bain'
const alternates = [
{
id: 10,
lang: 'en',
story_id: 5,
slug: 'how-we-price-a-bathroom-renovation',
url: 'https://journal.example.com/en/how-we-price-a-bathroom-renovation',
},
{
id: 11,
lang: 'de',
story_id: 5,
slug: 'so-kalkulieren-wir-eine-badsanierung',
url: 'https://journal.example.com/de/so-kalkulieren-wir-eine-badsanierung',
},
]
function resolved(locale: string, canonical = `https://journal.example.com/fr/${FRENCH_SLUG}`) {
return {
title: `${FRENCH_TITLE} | Journal`,
description: '',
canonical_url: canonical,
robots: 'index, follow',
og: {
type: 'article',
title: '',
description: '',
image: '',
image_alt: '',
url: canonical,
site_name: 'Journal',
locale,
locale_alternates: [],
},
twitter: { card: 'summary' },
image: { url: '', alt: '', source: 'none' },
authors: [],
published_at: null,
modified_at: '2026-10-14T09:12:00Z',
fallbacks: {
title: 'title',
description: 'none',
image: 'none',
canonical: 'pattern',
site_text_skipped: true,
},
json_ld: {},
lang: 'fr',
alternates: canonical.includes('/fr/') ? alternates : [],
}
}
function languages(social: boolean) {
const body = siteLanguagesBody('journal')
return {
...body,
languages: body.languages.map((row) =>
row.lang === 'fr'
? {
...row,
social_locale: social ? 'fr_FR' : null,
social_locale_state: social ? 'set' : 'not_set',
}
: row,
),
}
}
function frenchArticle(setup: Setup) {
return { ...articleBody(setup, 'fr'), slug: FRENCH_SLUG, title: FRENCH_TITLE }
}
export async function openSeoPanel(page: Page) {
if (narrow(page)) {
await page
.getByRole('button', { name: /^Details/ })
.first()
.click()
await page.getByRole('dialog').waitFor()
}
const body = page.locator('[data-testid="seo-body"]:visible')
if (!(await body.count())) {
await page.locator('[data-testid="seo-toggle"]:visible').first().click()
}
await page.locator('[data-testid="seo-edit"]:visible').first().click()
await page.locator('[data-testid="seo-language-cards"]').waitFor()
await settle(page)
}
export function seoState(
setup: Setup,
pattern: string | null,
locale: string,
social: boolean,
steps: (page: Page) => Promise<void> = openSeoPanel,
): VisualState {
const canonical = pattern
? `https://journal.example.com/fr/${FRENCH_SLUG}`
: `https://journal.example.com/${FRENCH_SLUG}`
const overrides: Record<string, ApiMock> = {
'GET /v1/site/': ok([{ ...site, public_url_pattern: pattern }]),
'GET /v1/auth/me': {
status: 200,
body: {
id: 7,
is_verified: true,
username: 'marta',
email: 'marta@renew.example',
display_name: 'Marta Alves',
access: accessFor('owner'),
},
},
'GET /v1/article/12': ok(frenchArticle(setup)),
'GET /v1/article/12/seo': ok(resolved(locale, canonical)),
'POST /v1/article/12/seo/check': ok({
status: 'ready',
suggestions: [],
slug: { available: true, suggestion: null },
}),
}
return state({ languages: languages(social), ...setup }, steps, overrides)
}
async function openEditSlug(page: Page) {
await openSeoPanel(page)
await press(page, 'Edit slug')
await page.getByRole('dialog').last().waitFor()
await settle(page)
}
export const metadataApiStates: Record<string, VisualState> = {
'loc.seo.noFallback': seoState({ suggestion: false }, PATTERN, 'fr_FR', true),
'loc.slug.published': seoState(
{ suggestion: false, online: { fr: true } },
PATTERN,
'fr_FR',
true,
openEditSlug,
),
'loc.url.unset': seoState({ suggestion: false }, null, 'fr_FR', true),
'loc.l10.noSocial': seoState({ suggestion: false }, PATTERN, '', false),
'loc.l10.api': {
path: '/integration',
api: {
'GET /v1/auth/me': {
status: 200,
body: {
id: 7,
is_verified: true,
username: 'marta',
email: 'marta@renew.example',
display_name: 'Marta Alves',
access: accessFor('owner'),
},
},
'GET /v1/image-variants': fails(500),
},
steps: async (page: Page) => {
await page.locator('[data-testid="integration-languages"]').scrollIntoViewIfNeeded()
await settle(page)
},
},
}