feat: translation flows, revision conflicts, delete scopes, article moves and a target site for new articles

This commit is contained in:
Florian Sylvain committed 2026-11-03 00:12:29 +01:00
1 parent 339e646eb8
commit 45fbbc33b1
93 files changed
+7630 -294

No files matched your search

@@ -112,6 +112,7 @@ describe('Existing screens are unaffected', () => {
'localization-list',
'localization-editor',
'localization-conflicts-moves',
'localization-new-article',
])
})
})
+1 -1
View File
@@ -48,7 +48,7 @@ function session(access: unknown): ApiMock {
}
}
function listApi(
export function listApi(
overrides: Record<string, ApiMock> = {},
viewer: StoryViewer = {},
access: unknown = accessFor('owner'),
+290 -32
View File
@@ -28,7 +28,7 @@ const MINUTE_MS = 60 * 1000
const SAVED_MINUTES_AGO = 3
const LOAD_SETTLE_MS = 400
const paragraphs: Record<Lang, string[]> = {
export const paragraphs: Record<Lang, string[]> = {
en: [
'Most quotes fail because they hide the same three costs. Here is how we itemise them before the first tile is ordered.',
'Start with the room, not the catalogue. Measure the walls, note where the pipes run and photograph the existing fixtures.',
@@ -84,11 +84,11 @@ const seoOneSuggestion = {
suggestions: [{ code: 'cover_none', field: 'cover_image_id', params: {} }],
slug: { available: true, suggestion: null },
}
const ok = (body: unknown): ApiMock => ({ status: 200, body })
const fails = (status = 500): ApiMock => ({ status, body: 'Internal server error' })
const hangs: ApiMock = { status: 200, hang: true }
export const ok = (body: unknown): ApiMock => ({ status: 200, body })
export const fails = (status = 500): ApiMock => ({ status, body: 'Internal server error' })
export const hangs: ApiMock = { status: 200, hang: true }
const site = {
export const site = {
id: 1,
name: 'Journal',
key: 'journal',
@@ -104,7 +104,8 @@ const site = {
public_url_pattern: null,
}
interface Setup {
export interface Setup {
search?: string
current?: Lang
capabilities?: string[]
siblings?: Lang[]
@@ -133,7 +134,7 @@ function entry(setup: Setup, lang: Lang) {
}
}
function articleBody(setup: Setup, lang: Lang) {
export function articleBody(setup: Setup, lang: Lang) {
const version = versions[lang]
const current = lang === (setup.current ?? 'fr')
const siblings = setup.siblings ?? ['en', 'fr', 'de']
@@ -146,6 +147,7 @@ function articleBody(setup: Setup, lang: Lang) {
is_online: setup.online?.[lang] ?? version.online,
lang: version.lang,
story_id: 5,
revision: 3,
capabilities: current
? (setup.capabilities ?? FULL)
: (setup.siblingCapabilities?.[lang] ?? FULL),
@@ -196,10 +198,10 @@ function editorApi(setup: Setup = {}, overrides: Record<string, ApiMock> = {}) {
}
function pathOf(setup: Setup) {
return `/article/${versions[setup.current ?? 'fr'].id}/edit`
return `/article/${versions[setup.current ?? 'fr'].id}/edit${setup.search ?? ''}`
}
function state(
export function state(
setup: Setup,
steps?: (page: Page) => Promise<void>,
overrides: Record<string, ApiMock> = {},
@@ -214,13 +216,13 @@ function state(
}
}
const pending = (path: string): VisualState => ({ path, skip: 'pending' })
export const pending = (path: string): VisualState => ({ path, skip: 'pending' })
function narrow(page: Page) {
export function narrow(page: Page) {
return (page.viewportSize()?.width ?? 0) < 1024
}
async function settle(page: Page) {
export async function settle(page: Page) {
await page.waitForTimeout(LOAD_SETTLE_MS)
await page.mouse.move(0, 0)
}
@@ -233,7 +235,7 @@ async function scrollEverythingToTop(page: Page) {
})
}
async function editBody(page: Page) {
export async function editBody(page: Page) {
await page.locator('[role="textbox"][contenteditable="true"]').first().click()
await page.keyboard.press('Control+End')
await page.keyboard.type(' x')
@@ -270,13 +272,13 @@ async function chooseLanguage(page: Page, name: string) {
await page.getByRole('menuitem', { name: new RegExp(`^${name}`) }).click()
}
async function press(page: Page, name: string | RegExp) {
export async function press(page: Page, name: string | RegExp) {
const exact = typeof name === 'string'
await page.getByRole('button', { name, exact }).first().click()
await settle(page)
}
async function openOverflow(page: Page) {
export async function openOverflow(page: Page) {
await page.getByRole('button', { name: 'More actions' }).first().click()
await page.getByRole('menuitem').first().waitFor()
await settle(page)
@@ -296,7 +298,7 @@ async function pressUnpublish(page: Page) {
await settle(page)
}
async function confirmDialog(page: Page, label: string) {
export async function confirmDialog(page: Page, label: string) {
await page.getByRole('dialog').getByRole('button', { name: label, exact: true }).click()
await settle(page)
}
@@ -306,6 +308,13 @@ async function dirtyThenPress(page: Page, label: string | RegExp) {
await press(page, label)
}
async function chooseOverflowItem(page: Page, label: string) {
await openOverflow(page)
await page.getByRole('menuitem', { name: new RegExp(`^${label}`) }).click()
await page.getByRole('dialog').waitFor()
await settle(page)
}
const saveFails: Record<string, ApiMock> = { 'PATCH /v1/article/12': fails() }
const savedFrench = (online = false) =>
ok({ ...articleBody({ current: 'fr', online: { fr: online } }, 'fr') })
@@ -535,11 +544,32 @@ const l8: Record<string, VisualState> = {
await editBody(page)
await pressUnpublish(page)
}),
'loc.l8.delete': pending('/article/12/edit'),
'loc.l8.delete': state({}, (page) => chooseOverflowItem(page, 'Delete French version')),
'loc.l8.deleteSource': pending('/article/10/edit'),
'loc.l8.deleteLast': pending('/article/10/edit'),
'loc.l8.deleteStoryOk': pending('/article/10/edit'),
'loc.l8.deleteStory': pending('/article/12/edit'),
'loc.l8.deleteLast': state(
{
current: 'en',
siblings: ['en'],
online: { en: true },
title: FRENCH_TITLE,
paragraphs: paragraphs.fr,
},
(page) => chooseOverflowItem(page, 'Delete article'),
),
'loc.l8.deleteStoryOk': state(
{ siblings: ['en', 'fr', 'de', 'ar'], online: { fr: true } },
(page) => chooseOverflowItem(page, 'Delete article'),
),
'loc.l8.deleteStory': state(
{ siblings: ['en', 'fr', 'de', 'ar'] },
async (page) => {
await chooseOverflowItem(page, 'Delete article')
await confirmDialog(page, 'Delete article')
await page.getByRole('dialog').getByText("You can't delete this article").waitFor()
await settle(page)
},
{ 'DELETE /v1/article/10': { status: 403, body: 'missing_permission' } },
),
'loc.l8.done': state(
{},
async (page) => {
@@ -573,6 +603,135 @@ const RESTORED_AT = '2026-10-14T14:18:00Z'
const STORED_DRAFT_KEY = 'renewcms:article-draft:7:12'
const olderFrench = paragraphs.fr.slice(0, 2)
const THEIRS_AT = '2026-10-14T14:32:00Z'
const AGAIN_AT = '2026-10-14T14:41:00Z'
const FRENCH_SLUG = 'comment-nous-chiffrons-une-renovation'
const conflictHeaders = { 'X-Current-Revision': '4' }
const staleSave: ApiMock = { status: 409, body: 'revision_conflict', headers: conflictHeaders }
function frenchAtCalls(updatedAtByCall: Record<number, string>, extra: object = {}): ApiMock {
const loaded = articleBody({ suggestion: true }, 'fr')
return {
status: 200,
bodyFor: (_url, call) =>
call === 1
? loaded
: {
...loaded,
...extra,
revision: 4,
updated_at: updatedAtByCall[call] ?? loaded.updated_at,
},
}
}
const conflictApi = (times: Record<number, string> = { 2: THEIRS_AT }) => ({
'PATCH /v1/article/12': staleSave,
'GET /v1/article/12': frenchAtCalls(times),
})
async function openConflict(page: Page) {
await editBody(page)
await press(page, 'Save')
await page.getByRole('dialog').getByText('Someone else saved this version').first().waitFor()
await settle(page)
}
async function storeRestoredDraft(page: Page) {
await page.evaluate(
({ key, body, mirroredAt }) =>
sessionStorage.setItem(
key,
JSON.stringify({ body, revision: 3, mirrored_at: mirroredAt }),
),
{
key: STORED_DRAFT_KEY,
body: paragraphs.fr.map((text) => `<p>${text}</p>`).join(''),
mirroredAt: RESTORED_AT,
},
)
await page.reload()
await page.getByText('Restored unsaved changes from 14:18', { exact: true }).first().waitFor()
await settle(page)
}
async function pressInDialog(page: Page, name: string) {
await page.getByRole('dialog').getByRole('button', { name, exact: true }).click()
await settle(page)
}
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 moveSites = [
boardSite(1, 'journal', 'Journal'),
boardSite(2, 'atelier', 'Atelier'),
boardSite(4, 'archive', 'Archive'),
boardSite(3, 'notes', 'Notes'),
]
const moveSetup: Setup = {
siblings: ['en', 'fr', 'de', 'ar'],
siblingCapabilities: { ar: READ_ONLY },
online: { en: true, fr: false, de: true, ar: false },
}
const movedAll = {
story_id: 5,
site: 'atelier',
site_id: 2,
article_ids: [10, 12, 11, 13],
}
const moveApi = (overrides: Record<string, ApiMock> = {}) => ({
'GET /v1/site/': ok(moveSites),
'GET /v1/site/notes/languages': ok(languagesOf(['en'])),
'GET /v1/site/atelier/languages': ok(languagesOf(['en', 'fr', 'de'])),
'POST /v1/story/5/move': ok(movedAll),
...overrides,
})
async function openMovePanel(page: Page) {
if (narrow(page)) {
await page.getByRole('button', { name: 'Details', exact: true }).click()
await page.getByRole('button', { name: /^Site and authors/ }).click()
await settle(page)
}
await page.getByRole('button', { name: 'Site', exact: true }).first().click()
await page.getByRole('listbox', { name: 'Site' }).waitFor()
await settle(page)
}
async function chooseMoveTarget(page: Page, name: string) {
await openMovePanel(page)
await page.getByRole('option', { name }).click()
await page.getByRole('button', { name: 'Move article…', exact: true }).click()
await page.getByRole('dialog').getByText(`Move to ${name}?`).waitFor()
await settle(page)
}
async function confirmMove(page: Page, name: string) {
await chooseMoveTarget(page, name)
await pressInDialog(page, 'Move article')
}
const mv = (
steps: (page: Page) => Promise<void>,
overrides: Record<string, ApiMock> = {},
setup: Setup = moveSetup,
exception?: DiffException,
) => state({ suggestion: true, ...setup }, steps, moveApi(overrides), exception)
export const conflictsMovesStates: Record<string, VisualState> = {
'loc.l11.session': pending('/article/12/edit'),
'loc.l11.restored': state({ paragraphs: olderFrench }, async (page) => {
@@ -589,17 +748,116 @@ export const conflictsMovesStates: Record<string, VisualState> = {
await page.getByText('Restored unsaved changes from 14:18', { exact: true }).waitFor()
await settle(page)
}),
'loc.conflict': pending('/article/12/edit'),
'loc.l11.conflictRestored': pending('/article/12/edit'),
'loc.l11.reloadConfirm': pending('/article/12/edit'),
'loc.l11.conflict2': pending('/article/12/edit'),
'loc.l11.download': pending('/article/12/edit'),
'loc.l11b.moveSelect': pending('/article/12/edit'),
'loc.conflict': state({ suggestion: true }, openConflict, conflictApi()),
'loc.l11.conflictRestored': state(
{ suggestion: true, paragraphs: olderFrench },
async (page) => {
await storeRestoredDraft(page)
await press(page, 'Save')
await page
.getByRole('dialog')
.getByText('Someone else saved this version')
.first()
.waitFor()
await settle(page)
},
conflictApi(),
),
'loc.l11.reloadConfirm': state(
{ suggestion: true },
async (page) => {
await openConflict(page)
await pressInDialog(page, 'Reload theirs')
},
conflictApi(),
),
'loc.l11.conflict2': state(
{ suggestion: true },
async (page) => {
await openConflict(page)
await pressInDialog(page, 'Save over theirs')
await page.getByText("Couldn't replace theirs").first().waitFor()
await settle(page)
},
conflictApi({ 2: THEIRS_AT, 3: AGAIN_AT }),
),
'loc.l11.download': state(
{ suggestion: true },
async (page) => {
await openConflict(page)
await pressInDialog(page, 'Download my version')
await page.getByText('Download started', { exact: true }).first().waitFor()
await settle(page)
},
conflictApi(),
),
'loc.l11b.moveSelect': mv(openMovePanel),
'loc.l11b.moveSelectHidden': pending('/article/12/edit'),
'loc.l11b.moveConfirm': pending('/article/12/edit'),
'loc.l11b.moveBusy': pending('/article/12/edit'),
'loc.l11b.moveDone': pending('/article/12/edit'),
'loc.l11b.move': pending('/article/12/edit'),
'loc.l11b.moveSlug': pending('/article/12/edit'),
'loc.l11b.movePerm': pending('/article/12/edit'),
'loc.l11b.moveConfirm': mv((page) => chooseMoveTarget(page, 'Atelier')),
'loc.l11b.moveBusy': mv((page) => confirmMove(page, 'Atelier'), {
'POST /v1/story/5/move': hangs,
}),
'loc.l11b.moveDone': mv(
async (page) => {
await confirmMove(page, 'Atelier')
await page.getByText('Moved to Atelier', { exact: true }).first().waitFor()
await settle(page)
},
{ 'GET /v1/article/12': frenchAtCalls({}, { site_id: 2, revision: 4 }) },
),
'loc.l11b.move': mv(
async (page) => {
await confirmMove(page, 'Notes')
await page
.getByRole('dialog')
.getByText("Notes doesn't enable every language")
.waitFor()
await settle(page)
},
{
'POST /v1/story/5/move': {
status: 422,
body: 'language_not_enabled',
headers: { 'X-Missing-Languages': 'fr,de,ar' },
},
},
),
'loc.l11b.moveSlug': mv(
async (page) => {
await confirmMove(page, 'Atelier')
await page
.getByRole('dialog')
.getByText('An address is already used on Atelier')
.waitFor()
await settle(page)
},
{
'POST /v1/story/5/move': {
status: 409,
body: 'slug_conflict',
headers: { 'X-Conflicting-Articles': '12' },
},
'GET /v1/article/12': {
status: 200,
bodyFor: (_url, call) => ({
...articleBody({ suggestion: true }, 'fr'),
...(call > 1 ? { slug: FRENCH_SLUG } : {}),
}),
},
},
moveSetup,
{
maxPercent: { desktop: 10, mobile: 17.4 },
reason: 'The board alert has an Edit slug button; Edit slug stays in slice 6, so the board and slice 6 are to be reconciled.',
approvedBy: 'Florian Sylvain',
},
),
'loc.l11b.movePerm': mv(
async (page) => {
await confirmMove(page, 'Atelier')
await page.getByRole('dialog').getByText("You can't move this article").waitFor()
await settle(page)
},
{ 'POST /v1/story/5/move': { status: 403, body: 'missing_permission' } },
),
}
+325
View File
@@ -0,0 +1,325 @@
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(),
}),
}
+4 -2
View File
@@ -5,6 +5,7 @@ import { approvedExceptions } from './approvedExceptions'
import { articleListStates } from './articleListStates'
import { conflictsMovesStates, editorLocalizationStates } from './editorLocalizationStates'
import { localizationStates } from './localizationStates'
import { newArticleLocalizationStates } from './newArticleLocalizationStates'
import { rolesStates } from './rolesStates'
export interface ApiMock {
@@ -814,7 +815,7 @@ function submitNewArticle() {
return async (page: Page) => {
await openNewArticleModal(page)
await fillNewArticleValid(page)
await page.getByRole('button', { name: 'Create draft', exact: true }).click()
await page.getByRole('button', { name: 'Create article', exact: true }).click()
await page.mouse.move(0, 0)
}
}
@@ -10158,7 +10159,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
await openNewArticleModal(page)
await page.getByPlaceholder(`e.g. ${newArticleTitle}`).fill('Ti')
await page.getByPlaceholder(`e.g. ${newArticleTitle}`).blur()
await page.getByRole('button', { name: 'Create draft', exact: true }).click()
await page.getByRole('button', { name: 'Create article', exact: true }).click()
await page.mouse.move(0, 0)
},
},
@@ -10350,4 +10351,5 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
'localization-list': articleListStates,
'localization-editor': editorLocalizationStates,
'localization-conflicts-moves': conflictsMovesStates,
'localization-new-article': newArticleLocalizationStates,
}