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

@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import EditorStatusBar from '../EditorStatusBar.vue'
import { LARGE_BODY_CHARACTERS } from '@/lib/bodyCounts'
type State = 'clean' | 'dirty' | 'saving' | 'saved' | 'error' | 'offline'
function mountBar(state: State, characters: number, words = 10) {
return mount(EditorStatusBar, { props: { state, characters, words } })
}
describe('Count', () => {
it('is neutral with thousands separators', () => {
const wrapper = mountBar('clean', 120000, 19845)
expect(wrapper.text()).toBe('19,845 words · 120,000 characters')
})
it('reads zero for an empty body', () => {
expect(mountBar('clean', 0, 0).text()).toBe('0 words · 0 characters')
})
})
describe('Saving note appears from the size threshold', () => {
it('shows no note just below the threshold', () => {
const wrapper = mountBar('saving', LARGE_BODY_CHARACTERS - 1)
expect(wrapper.text()).not.toContain('This can take a few seconds.')
})
it('shows the note at the threshold', () => {
const wrapper = mountBar('saving', LARGE_BODY_CHARACTERS)
expect(wrapper.text()).toContain('Saving 50,000 characters. This can take a few seconds.')
})
it('shows the note at 120,000 characters', () => {
expect(mountBar('saving', 120000).text()).toContain(
'Saving 120,000 characters. This can take a few seconds.',
)
})
})
describe('Failed note appears from the size threshold', () => {
it('shows no note just below the threshold', () => {
const wrapper = mountBar('error', LARGE_BODY_CHARACTERS - 1)
expect(wrapper.text()).not.toContain("Couldn't save.")
})
it('shows the note at the threshold', () => {
const wrapper = mountBar('error', LARGE_BODY_CHARACTERS)
expect(wrapper.text()).toContain("Couldn't save. Your text is kept. Retry.")
})
it('shows no note in other states, however long', () => {
expect(mountBar('dirty', 120000).text()).not.toContain("Couldn't save.")
})
})
@@ -70,12 +70,6 @@ function titleInput() {
)!
}
function bodyTextarea() {
return document.body.querySelector<HTMLTextAreaElement>(
'textarea[placeholder="Start writing. You\'ll continue in the editor."]',
)!
}
function dispatch(element: HTMLElement, type: string) {
element.dispatchEvent(new Event(type, { bubbles: true }))
}
@@ -92,21 +86,8 @@ async function blurTitle() {
await flushPromises()
}
async function fillBody(value: string) {
const textarea = bodyTextarea()
textarea.value = value
dispatch(textarea, 'input')
await flushPromises()
}
async function blurBody() {
dispatch(bodyTextarea(), 'blur')
await flushPromises()
}
async function fillValidForm() {
await fillTitle(TITLE)
await fillBody(BODY)
}
afterEach(() => {
@@ -128,10 +109,8 @@ describe('Opening the modal shows the Default state', () => {
expect(document.body.textContent).toContain('0 / 200')
expect(document.body.textContent).toContain('3 to 200 characters.')
expect(bodyTextarea().value).toBe('')
expect(document.body.textContent).toContain(
'You can format it and add images in the editor.',
)
expect(document.body.textContent).not.toContain('First paragraph')
expect(document.querySelector('textarea')).toBeNull()
expect(findButton('Cancel')).toBeTruthy()
expect(findButton('Create draft')).toBeTruthy()
@@ -154,24 +133,8 @@ describe('Validation — a title under 3 characters on blur', () => {
})
})
describe('Validation — an empty first paragraph on blur', () => {
it('shows the error once the field is left empty and blurred', async () => {
mountModal()
await flushPromises()
await fillTitle(TITLE)
await blurTitle()
await blurBody()
expect(document.body.textContent).toContain(
'Add a first paragraph. It can be a single sentence.',
)
expect(requestMock).not.toHaveBeenCalled()
})
})
describe('Validation — submitting with both fields invalid shows both errors and sends nothing', () => {
it('shows both errors and never sends the request', async () => {
describe('Validation — submitting with an invalid title shows the error and sends nothing', () => {
it('shows the error and never sends the request', async () => {
mountModal()
await flushPromises()
@@ -179,9 +142,6 @@ describe('Validation — submitting with both fields invalid shows both errors a
await clickButton('Create draft')
expect(document.body.textContent).toContain('Use at least 3 characters.')
expect(document.body.textContent).toContain(
'Add a first paragraph. It can be a single sentence.',
)
expect(requestMock).not.toHaveBeenCalled()
})
})
@@ -209,12 +169,11 @@ describe('Submitting disables the form and cannot be dismissed', () => {
'/v1/article/?format=html',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ title: TITLE, body: BODY }),
body: JSON.stringify({ title: TITLE }),
}),
)
expect(titleInput().disabled).toBe(true)
expect(bodyTextarea().disabled).toBe(true)
expect((findButton('Cancel') as HTMLButtonElement).disabled).toBe(true)
expect(findButton('Creating…')).toBeTruthy()
@@ -272,14 +231,10 @@ describe("Title rejected — a 400 that got past this form's own check", () => {
await flushPromises()
expect(titleInput().value).toBe(TITLE)
expect(bodyTextarea().value).toBe(BODY)
expect(document.body.textContent).toContain('Check this title and try again.')
expect(document.body.textContent).toContain(
"Something in the form isn't valid. Check the highlighted fields.",
)
expect(document.body.textContent).not.toContain(
'Add a first paragraph. It can be a single sentence.',
)
expect(titleInput().disabled).toBe(false)
wrapper.unmount()
})
@@ -303,14 +258,10 @@ describe('Error — a 500 or a network failure keeps the text and offers to retr
await flushPromises()
expect(titleInput().value).toBe(TITLE)
expect(bodyTextarea().value).toBe(BODY)
expect(document.body.textContent).toContain(
"Couldn't create the article. Your text is still here. Try again in a moment.",
)
expect(document.body.textContent).not.toContain('Check this title and try again.')
expect(document.body.textContent).not.toContain(
'Add a first paragraph. It can be a single sentence.',
)
wrapper.unmount()
})
})
@@ -338,7 +289,7 @@ describe('Retrying after an Error succeeds', () => {
2,
'/v1/article/?format=html',
expect.objectContaining({
body: JSON.stringify({ title: TITLE, body: BODY }),
body: JSON.stringify({ title: TITLE }),
}),
)
expect(wrapper.emitted('created')).toEqual([[articleFixture()]])