feat: remove the 10,000-character article body limit and make the New article body optional
This commit is contained in:
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()]])
|
||||
|
||||
Reference in new issue
Block a user