feat: remove the 10,000-character article body limit and make the New article body optional

This commit is contained in:
Florian Sylvain committed 2026-10-11 08:49:11 +02:00
1 parent 5e05b606a7
commit 10ec74c8cf
28 files changed
+860 -397

No files matched your search

+88 -36
View File
@@ -355,13 +355,45 @@ async function markDirty(page: Page) {
await page.keyboard.press('Backspace')
}
async function typeExactBodyLength(page: Page, totalLength: number) {
const editable = page.locator('[contenteditable="true"]')
await editable.click()
await page.keyboard.press('Control+a')
await page.keyboard.press('Delete')
const inner = 'a'.repeat(Math.max(0, totalLength - 7))
await page.keyboard.insertText(inner)
const PARAGRAPH_TAGS_LENGTH = 7
const veryLongBodyLength = 120000
const veryLongBodyWords = 20000
const EDITOR_LIST_ITEM_PARAGRAPH_TAGS = 21
function wordsIn(html: string) {
return html
.replace(/<\/(?:p|h[1-6]|li)>/g, ' ')
.replace(/<[^>]+>/g, '')
.split(/\s+/)
.filter(Boolean).length
}
function paragraphOfWords(totalLength: number, wordCount: number) {
const letters = totalLength - PARAGRAPH_TAGS_LENGTH - (wordCount - 1)
const shortWords = wordCount * 5 - letters
const words = Array.from({ length: wordCount }, (_, index) =>
index < shortWords ? 'abcd' : 'abcde',
)
return `<p>${words.join(' ')}</p>`
}
const veryLongArticle = {
...editorArticle,
body:
bathroomBody +
paragraphOfWords(
veryLongBodyLength - bathroomBody.length - EDITOR_LIST_ITEM_PARAGRAPH_TAGS,
veryLongBodyWords - wordsIn(bathroomBody),
),
}
function veryLongApi(overrides: Record<string, ApiMock> = {}) {
return editorApi({ 'GET /v1/article/12': { status: 200, body: veryLongArticle }, ...overrides })
}
async function markVeryLongDirty(page: Page) {
await markDirty(page)
await focusBoldSample(page)
}
function mib(value: number) {
@@ -550,27 +582,19 @@ async function freezeUploadProgress(page: Page, loaded: number, total: number) {
}
const newArticleTitle = 'Choosing tiles for a small bathroom'
const newArticleBody = 'Small rooms show every grout line, so we start with the floor.'
function newArticleHomeApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiMock> {
return {
'GET /v1/auth/me': verifiedSession,
'GET /v1/activity': homeActivityMock,
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
...overrides,
}
return articlesApi(overrides)
}
async function openNewArticleModal(page: Page) {
await page.getByRole('button', { name: 'New article' }).click()
await page.getByRole('dialog').waitFor()
await page.mouse.move(0, 0)
}
async function fillNewArticleValid(page: Page) {
await page.getByPlaceholder(`e.g. ${newArticleTitle}`).fill(newArticleTitle)
await page
.getByPlaceholder("Start writing. You'll continue in the editor.")
.fill(newArticleBody)
}
function submitNewArticle() {
@@ -4709,20 +4733,48 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
await focusBoldSample(page)
},
},
'Near the limit': {
'Very long article': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await typeExactBodyLength(page, 9240)
api: veryLongApi(),
steps: markVeryLongDirty,
exception: {
maxPercent: { desktop: 10, mobile: 14.5 },
reason: 'The mobile editor already misses the 10 % budget for every ordinary state at HEAD (11 to 21 %); the very long article states inherit the same layout gap',
approvedBy: 'Florian Sylvain',
},
},
'Too long': {
'Very long article: saving': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
api: veryLongApi({
'PATCH /v1/article/12': { status: 200, body: veryLongArticle, hang: true },
}),
exception: {
maxPercent: { desktop: 10, mobile: 14.5 },
reason: 'The mobile editor already misses the 10 % budget for every ordinary state at HEAD (11 to 21 %); the very long article states inherit the same layout gap',
approvedBy: 'Florian Sylvain',
},
steps: async (page) => {
await typeExactBodyLength(page, 10214)
await markVeryLongDirty(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
},
},
'Very long article: saved': { path: '/article/12/edit', skip: 'clock-sensitive' },
'Very long article: save failed': {
path: '/article/12/edit',
now: editorNow,
api: veryLongApi({ 'PATCH /v1/article/12': { status: 500 } }),
exception: {
maxPercent: { desktop: 10, mobile: 14.5 },
reason: 'The mobile editor already misses the 10 % budget for every ordinary state at HEAD (11 to 21 %); the very long article states inherit the same layout gap',
approvedBy: 'Florian Sylvain',
},
steps: async (page) => {
await markVeryLongDirty(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
await page.getByText('Save failed').waitFor()
await focusBoldSample(page)
},
},
Preview: {
@@ -4871,38 +4923,38 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
},
'new-article': {
Default: {
path: '/home',
now: boardNow,
path: '/article',
now: articlesNow,
api: newArticleHomeApi(),
steps: openNewArticleModal,
},
Validation: {
path: '/home',
now: boardNow,
path: '/article',
now: articlesNow,
api: newArticleHomeApi(),
steps: async (page) => {
await openNewArticleModal(page)
await page.getByPlaceholder(`e.g. ${newArticleTitle}`).fill('Ti')
await page.getByPlaceholder(`e.g. ${newArticleTitle}`).blur()
await page.getByPlaceholder("Start writing. You'll continue in the editor.").click()
await page.getByPlaceholder("Start writing. You'll continue in the editor.").blur()
await page.getByRole('button', { name: 'Create draft', exact: true }).click()
await page.mouse.move(0, 0)
},
},
Submitting: {
path: '/home',
now: boardNow,
path: '/article',
now: articlesNow,
api: newArticleHomeApi({ 'POST /v1/article/': { status: 201, hang: true } }),
steps: submitNewArticle(),
},
'Title rejected (400)': {
path: '/home',
now: boardNow,
path: '/article',
now: articlesNow,
api: newArticleHomeApi({ 'POST /v1/article/': { status: 400 } }),
steps: submitNewArticle(),
},
Error: {
path: '/home',
now: boardNow,
path: '/article',
now: articlesNow,
api: newArticleHomeApi({ 'POST /v1/article/': { status: 500 } }),
steps: submitNewArticle(),
},