Files
RenewCMS/web/visual/newArticleLocalizationStates.ts
T

326 lines
9.1 KiB
TypeScript

import type { Page } from 'playwright'
import { siteLanguagesBody } from '../src/test/storyLists'
import { listApi } from './articleListStates'
import {
articleBody,
fails,
hangs,
ok,
pending,
settle,
site,
state,
type Setup,
} from './editorLocalizationStates'
import type { ApiMock, DiffException, VisualState } from './states'
const FRENCH_TITLE_ROOF = "Liste de contrôle pour l'inspection de la toiture"
const FRENCH_TITLE_LONG = `${FRENCH_TITLE_ROOF} avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes`
const FRENCH_TITLE_PRICING = 'Comment nous chiffrons une rénovation de salle de bain'
const ROOF_TITLE = 'Roof inspection checklist'
const AUTHOR_NAMES = [
'Marta Alves',
'Camille Roche',
'Luis Ferrer',
'Ana Pereira',
'Jonas Weber',
'Inès Duarte',
'Paul Martin',
'Sofia Costa',
'Omar Haddad',
]
const COVER_IMAGE_ID = 1
const IMAGE_COUNT = 8
function boardSite(id: number, key: string, name: string) {
return {
...site,
id,
key,
name,
site_name: name,
is_default: id === 1,
base_url: `https://${key}.example.com`,
}
}
const boardSites = [
boardSite(1, 'journal', 'Journal'),
boardSite(2, 'atelier', 'Atelier'),
boardSite(3, 'notes', 'Notes'),
boardSite(4, 'archive', 'Archive'),
]
function languagesOf(tags: string[]) {
const base = siteLanguagesBody('journal')
return {
...base,
revision: `${tags.join('-')}-1`,
languages: base.languages.filter((language) => tags.includes(language.lang)),
}
}
const modalApi = (overrides: Record<string, ApiMock> = {}) =>
listApi({
'GET /v1/site/': ok(boardSites),
'GET /v1/site/atelier/languages': ok(languagesOf(['en', 'fr', 'de'])),
'GET /v1/site/notes/languages': ok(languagesOf(['en'])),
'GET /v1/site/archive/languages': ok(languagesOf(['en'])),
...overrides,
})
const roofStory = {
id: 105,
story_id: 5,
title: ROOF_TITLE,
body: '',
images: [],
is_online: true,
lang: 'en',
site_id: 1,
created_at: '2026-03-12T09:14:00Z',
updated_at: '2026-10-14T09:00:00Z',
story_updated_at: '2026-10-14T09:00:00Z',
story_authors: [],
capabilities: ['read', 'edit', 'publish', 'delete', 'create_translation'],
missing_langs: ['fr', 'ar'],
coverage: [
{
id: 105,
lang: 'en',
title: ROOF_TITLE,
is_online: true,
language_enabled: true,
capabilities: ['read'],
},
{
id: 106,
lang: 'de',
title: 'Checkliste für die Dachprüfung',
is_online: false,
language_enabled: true,
capabilities: ['read'],
},
],
}
const missingPage: ApiMock = {
status: 200,
bodyFor: () => ({
items: [roofStory],
total: 1,
page: 1,
size: 20,
counts: { all: 1, online: 1, draft: 0, missing: 1 },
}),
}
function listWithMissing(): Record<string, ApiMock> {
const base = modalApi()
const list = base['GET /v1/article/']!
return {
'GET /v1/article/': {
status: 200,
bodyFor: (url, call) =>
url.searchParams.get('tab') === 'missing'
? missingPage.bodyFor!(url, call)
: list.bodyFor!(url, call),
},
}
}
async function openModal(page: Page) {
await page.getByRole('button', { name: 'New article', exact: true }).first().click()
await page.getByRole('dialog').getByText('Give the article a title').waitFor()
await settle(page)
}
async function chooseOption(page: Page, field: string, option: string) {
await page.getByRole('combobox', { name: field, exact: true }).click()
await page.getByRole('option', { name: option, exact: true }).click()
await settle(page)
}
async function modalTitle(page: Page, value: string) {
const input = page.getByRole('dialog').getByRole('textbox').first()
await input.fill(value)
}
const modal = (
steps: (page: Page) => Promise<void>,
overrides: Record<string, ApiMock> = {},
exception?: DiffException,
): VisualState => ({
exception,
path: '/article?site=journal&size=5',
timezone: 'UTC',
api: modalApi(overrides),
steps: async (page) => {
await openModal(page)
await steps(page)
await page.mouse.move(0, 0)
},
})
async function startTranslation(page: Page) {
await page.getByRole('radio', { name: /Translation of an existing article/ }).click()
await settle(page)
await chooseOption(page, 'Language', 'French')
await page.getByLabel('Article', { exact: true }).click()
await page.getByRole('option', { name: ROOF_TITLE }).click()
await settle(page)
await page.getByRole('dialog').getByRole('textbox').first().fill(FRENCH_TITLE_ROOF)
await settle(page)
}
const roofEditor: Setup = {
current: 'en',
siblings: ['en', 'de'],
title: ROOF_TITLE,
online: { en: true, de: false },
search: '?dialog=add-translation&lang=fr',
}
const pricingEditor: Setup = {
current: 'en',
siblings: ['en', 'de', 'ar'],
online: { en: true, de: true, ar: false },
search: '?dialog=add-translation&lang=fr',
}
async function dialogTitle(page: Page, value: string) {
const input = page.getByRole('dialog').getByRole('textbox').first()
await input.waitFor()
await input.fill(value)
}
async function submitTranslation(page: Page) {
await page.getByRole('dialog').getByRole('button', { name: 'Create French draft' }).click()
await settle(page)
}
const withoutImages = (setup: Setup): ApiMock => ({
status: 200,
bodyFor: () => ({ ...articleBody(setup, 'en'), images: [] }),
})
const addTranslation = (
setup: Setup,
title: string,
after?: (page: Page) => Promise<void>,
overrides: Record<string, ApiMock> = {},
exception?: DiffException,
) =>
state(
setup,
async (page) => {
await dialogTitle(page, title)
await settle(page)
await after?.(page)
await page.mouse.move(0, 0)
},
{ 'GET /v1/article/10': withoutImages(setup), ...overrides },
exception,
)
const rewindDialog = async (page: Page) => {
await page.keyboard.press('Home')
await page.getByRole('dialog').evaluate((dialog) => {
dialog.querySelectorAll('*').forEach((node) => {
node.scrollTop = 0
})
})
}
const roofForm = (
after?: (page: Page) => Promise<void>,
overrides: Record<string, ApiMock> = {},
exception?: DiffException,
) => addTranslation(roofEditor, FRENCH_TITLE_ROOF, after, overrides, exception)
const duplicate = (overrides: Record<string, ApiMock>) =>
addTranslation(pricingEditor, FRENCH_TITLE_PRICING, submitTranslation, overrides)
const longSource = () => {
const body = articleBody({ ...roofEditor }, 'en')
return {
status: 200,
bodyFor: () => ({
...body,
cover_image_id: COVER_IMAGE_ID,
images: Array.from({ length: IMAGE_COUNT }, (_, index) => ({
id: index + 1,
url: `/images/${index + 1}.png`,
})),
authors: AUTHOR_NAMES.map((name, index) => ({
id: index + 1,
name,
slug: name.toLowerCase().replace(' ', '-'),
})),
}),
} satisfies ApiMock
}
const post = (key: string, response: ApiMock) => ({
[`POST /v1/article/${key}/translations`]: response,
})
export const newArticleLocalizationStates: Record<string, VisualState> = {
'loc.l3.single': modal((page) => chooseOption(page, 'Site', 'Archive')),
'loc.l3.multi': modal(async () => undefined),
'loc.l3.translation': modal(startTranslation, listWithMissing(), {
maxPercent: { desktop: 10, mobile: 23.2 },
reason: 'The board draws no New article / Translation choice cards in translation mode, while the "Switching the choice keeps what is shared" scenario requires them (about 105px taller sheet); the board is to be updated.',
approvedBy: 'Florian Sylvain',
}),
'loc.l3.sitechange': modal(async (page) => {
await modalTitle(page, 'Spring newsletter')
await chooseOption(page, 'Language', 'French')
await chooseOption(page, 'Site', 'Notes')
}),
'loc.l4.form': roofForm(),
'loc.l4.empty': roofForm(
async (page) => {
await page.getByRole('radio', { name: /Start empty/ }).click()
await settle(page)
},
{},
{
maxPercent: { desktop: 16.8, mobile: 32.2 },
reason: 'The board still draws the Source version field and copy note in Start empty, while features/localization_translation_flows.feature says they are no longer shown; the board is to be updated.',
approvedBy: 'Florian Sylvain',
},
),
'loc.l4.saving': roofForm(submitTranslation, post('10', hangs)),
'loc.dup.readable': duplicate(
post('10', {
status: 409,
body: 'language_exists',
headers: { 'X-Existing-Article-Id': '12' },
}),
),
'loc.dup.hidden': duplicate(post('10', { status: 409, body: 'language_unavailable' })),
'loc.l4.fail': roofForm(submitTranslation, post('10', fails())),
'loc.l4.gone': pending('/article/10/edit?dialog=add-translation&lang=fr'),
'loc.l4.unreadable': roofForm(
submitTranslation,
post('10', { status: 404, body: 'source_unavailable' }),
{
maxPercent: { desktop: 12.1, mobile: 33.6 },
reason: 'The board omits the Starting content block and description, while the feature requires the radios so choosing Start empty clears the warning; the board is to be updated.',
approvedBy: 'Florian Sylvain',
},
),
'loc.l4.langOff': roofForm(submitTranslation, {
...post('10', { status: 409, body: 'language_unavailable' }),
'GET /v1/site/journal/languages': {
status: 200,
bodyFor: (_url, call) =>
languagesOf(call === 1 ? ['en', 'fr', 'de', 'ar'] : ['en', 'de', 'ar']),
},
}),
'loc.l4.long': addTranslation(roofEditor, FRENCH_TITLE_LONG, rewindDialog, {
'GET /v1/article/10': longSource(),
}),
}