feat: public API, SEO and existing surfaces
This commit is contained in:
1 parent
45fbbc33b1
commit
c390fcfe8d
103 files changed
+4476
-231
No files matched your search
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user