326 lines
9.1 KiB
TypeScript
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(),
|
|
}),
|
|
}
|