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
@@ -171,7 +171,8 @@ describe('Skeletons appear only after 150 ms, never for a fast response', () =>
|
||||
|
||||
it('never shows a skeleton when the response resolves in under 150 ms', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12?format=html': () => Promise.resolve(mockResponse(200, articleFixture())),
|
||||
'GET /v1/article/12?format=html': () =>
|
||||
Promise.resolve(mockResponse(200, articleFixture())),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
@@ -190,7 +191,8 @@ describe('Saved — a freshly loaded article, draft or online', () => {
|
||||
'reads the Saved state for is_online $is_online',
|
||||
async ({ is_online, badge, publishLabel }) => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12?format=html': () => mockResponse(200, articleFixture({ is_online })),
|
||||
'GET /v1/article/12?format=html': () =>
|
||||
mockResponse(200, articleFixture({ is_online })),
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Articles')
|
||||
@@ -199,7 +201,7 @@ describe('Saved — a freshly loaded article, draft or online', () => {
|
||||
expect(wrapper.text()).toContain('Saved at 11:02')
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(wrapper.text()).toContain(publishLabel)
|
||||
expect(wrapper.text()).toContain('4 318 / 10 000')
|
||||
expect(wrapper.text()).toContain('1 words · 4,318 characters')
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -389,40 +391,85 @@ describe('A save against an article deleted elsewhere shows a toast, not the err
|
||||
})
|
||||
})
|
||||
|
||||
describe('Near the limit — the counter warns at 9000 characters without blocking Save', () => {
|
||||
it('shows the warning icon and keeps Save enabled', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12?format=html': () => mockResponse(200, articleFixture()),
|
||||
})
|
||||
await typeBody(wrapper, paragraphOfLength(9240))
|
||||
function longParagraph(totalLength: number) {
|
||||
const words = Math.floor((totalLength - 7) / 6)
|
||||
const inner = 'abcde '.repeat(words).trimEnd()
|
||||
return paragraph(inner + 'x'.repeat(totalLength - 7 - inner.length))
|
||||
}
|
||||
|
||||
expect(wrapper.text()).toContain('9 240 / 10 000')
|
||||
expect(wrapper.find('[data-testid="character-counter-warning-icon"]').exists()).toBe(true)
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(false)
|
||||
describe('Status bar — a neutral count of words and characters', () => {
|
||||
it('reads the loaded article count and follows the body', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12?format=html': () =>
|
||||
mockResponse(200, articleFixture({ body: paragraph('one two three') })),
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('3 words · 20 characters')
|
||||
|
||||
await typeBody(wrapper, '')
|
||||
|
||||
expect(wrapper.text()).toContain('0 words · 0 characters')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Too long — over 10000 characters disables Save and Publish until shortened', () => {
|
||||
it('shows the over-limit text and disables Save and Publish, then re-enables once shortened', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12?format=html': () => mockResponse(200, articleFixture()),
|
||||
})
|
||||
await typeBody(wrapper, paragraphOfLength(10214))
|
||||
describe('Very long article — no length limit', () => {
|
||||
const handlers = (patch: () => Response | Promise<Response>) => ({
|
||||
'GET /v1/article/12?format=html': () => mockResponse(200, articleFixture()),
|
||||
'PATCH /v1/article/12?format=html': patch,
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('10 214 / 10 000')
|
||||
expect(wrapper.text()).toContain(
|
||||
'214 characters over. Shorten the text or remove an image. Formatting counts too.',
|
||||
)
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(true)
|
||||
it('keeps the count neutral and Save and Publish enabled at 120,000 characters', async () => {
|
||||
const wrapper = await mountEditor(handlers(() => mockResponse(200, articleFixture())))
|
||||
await typeBody(wrapper, longParagraph(120000))
|
||||
|
||||
expect(wrapper.text()).toContain('characters')
|
||||
expect(wrapper.text()).toContain('120,000 characters')
|
||||
expect(wrapper.text()).toContain('Unsaved changes')
|
||||
expect(wrapper.find('[data-testid="character-counter-warning-icon"]').exists()).toBe(false)
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(false)
|
||||
const publishButton = wrapper
|
||||
.findAll('button')
|
||||
.find((button) => button.text() === 'Publish' || button.text() === 'Save and publish')
|
||||
expect((publishButton?.element as HTMLButtonElement).disabled).toBe(true)
|
||||
.find((button) => button.text() === 'Save and publish')
|
||||
expect((publishButton?.element as HTMLButtonElement).disabled).toBe(false)
|
||||
})
|
||||
|
||||
await typeBody(wrapper, paragraphOfLength(9000))
|
||||
it('shows the saving copy while the PATCH is pending', async () => {
|
||||
const wrapper = await mountEditor(handlers(() => new Promise<Response>(() => {})))
|
||||
await typeBody(wrapper, longParagraph(120000))
|
||||
await saveButton(wrapper).trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).not.toContain('characters over')
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(false)
|
||||
expect(wrapper.text()).toContain('Saving 120,000 characters. This can take a few seconds.')
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('reads Saved once the PATCH resolves', async () => {
|
||||
const wrapper = await mountEditor(handlers(() => mockResponse(200, articleFixture())))
|
||||
await typeBody(wrapper, longParagraph(120000))
|
||||
await saveButton(wrapper).trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Saved')
|
||||
expect(wrapper.text()).toContain('120,000 characters')
|
||||
expect(wrapper.text()).not.toContain('Saving 120,000')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: '500', handler: () => mockResponse(500) },
|
||||
{ name: '413', handler: () => mockResponse(413) },
|
||||
{
|
||||
name: 'a network failure',
|
||||
handler: () => Promise.reject(new TypeError('Failed to fetch')),
|
||||
},
|
||||
])('keeps the text and offers Retry after $name', async ({ handler }) => {
|
||||
const wrapper = await mountEditor(handlers(handler))
|
||||
await typeBody(wrapper, longParagraph(120000))
|
||||
await saveButton(wrapper).trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain("Couldn't save. Your text is kept. Retry.")
|
||||
expect(wrapper.text()).toContain('Save failed')
|
||||
expect(articleEditorBody(wrapper)).toBe(longParagraph(120000))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -234,11 +234,6 @@ describe('A successful create hands off to the editor with a toast', () => {
|
||||
titleInput.value = created.title
|
||||
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
|
||||
const bodyTextarea = document.body.querySelector<HTMLTextAreaElement>(
|
||||
'textarea[placeholder="Start writing. You\'ll continue in the editor."]',
|
||||
)!
|
||||
bodyTextarea.value = created.body
|
||||
bodyTextarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await flushPromises()
|
||||
|
||||
const confirmButton = Array.from(document.body.querySelectorAll('button')).find(
|
||||
|
||||
Reference in new issue
Block a user