184 lines
4.6 KiB
TypeScript
184 lines
4.6 KiB
TypeScript
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)
|
|
},
|
|
},
|
|
}
|