feat: articles format option with markdown new default

This commit is contained in:
fsylvain committed 2026-10-04 19:04:12 +02:00
1 parent 68401d66e0
commit 7e9bd9ba30
61 files changed
+2269 -243

No files matched your search

+26 -26
View File
@@ -192,7 +192,7 @@ afterEach(async () => {
describe('Default — the list loads sorted by last updated, newest first', () => {
it('shows the breadcrumb, tab counts and every row in fixture order', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
})
expect(wrapper.text()).toContain('All 6')
@@ -218,7 +218,7 @@ describe('Loading — skeleton rows while the first request is in flight', () =>
it('shows skeleton rows and aria-busy, then replaces them once resolved', async () => {
let resolveList: (() => void) | null = null
const wrapper = await mountArticles({
'GET /v1/article/': () =>
'GET /v1/article/?format=html': () =>
new Promise((resolve) => {
resolveList = () => resolve(mockResponse(200, sixArticleFixture()))
}),
@@ -239,7 +239,7 @@ describe('Loading — skeleton rows while the first request is in flight', () =>
describe('Row menu open shows Edit, the status-appropriate action, Copy API URL and Delete…', () => {
it('opens a menu named "Actions for Winter checklist for timber windows"', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -260,7 +260,7 @@ describe('Copy API URL succeeds — the absolute URL is copied and confirmed', (
})
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -287,7 +287,7 @@ describe('Copy API URL fails — the clipboard write itself is rejected', () =>
})
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -305,7 +305,7 @@ describe('Copy API URL fails — the clipboard write itself is rejected', () =>
describe('Empty — zero articles', () => {
it('shows the no-articles empty state and opens NewArticleModal', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, []),
'GET /v1/article/?format=html': () => mockResponse(200, []),
})
expect(wrapper.text()).toContain('All 0')
@@ -326,7 +326,7 @@ describe('Empty — zero articles', () => {
describe('Initial load failure — an error must not render the no-articles empty state', () => {
it('does not show "No articles yet" when GET /v1/article/ fails', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(500),
'GET /v1/article/?format=html': () => mockResponse(500),
})
expect(wrapper.text()).not.toContain('No articles yet')
@@ -336,7 +336,7 @@ describe('Initial load failure — an error must not render the no-articles empt
describe('No results — a search matches nothing, but the tab counts still show the full list', () => {
it('shows the no-results empty state, then clears the search', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
})
const searchInput = wrapper.find('input[type="search"]')
@@ -367,7 +367,7 @@ describe('An empty tab shows its own EmptyState, while the other tab still count
.slice(0, 4)
.map((a) => ({ ...a, is_online: true }))
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, onlineOnly),
'GET /v1/article/?format=html': () => mockResponse(200, onlineOnly),
})
await clickTab('Drafts 0')
@@ -384,7 +384,7 @@ describe('An empty tab shows its own EmptyState, while the other tab still count
.slice(0, 4)
.map((a) => ({ ...a, is_online: false }))
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, draftsOnly),
'GET /v1/article/?format=html': () => mockResponse(200, draftsOnly),
})
await clickTab('Online 0')
@@ -400,8 +400,8 @@ describe('An empty tab shows its own EmptyState, while the other tab still count
describe('Action in progress — publishing a row disables only that row', () => {
it('sends the PATCH and shows the busy label for that row alone', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/4/publish': () => new Promise(() => {}),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/4/publish?format=html': () => new Promise(() => {}),
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -420,8 +420,8 @@ describe('Published — a 200 updates the row in place and raises a toast', () =
updated_at: iso(0),
}
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/4/publish': () => mockResponse(200, updated),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/4/publish?format=html': () => mockResponse(200, updated),
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -448,8 +448,8 @@ describe('Action failed, publish — a 500 or a network error leaves the row unc
])('shows Retry for $name and re-sends the same request', async ({ handler }) => {
const patchCall = vi.fn(handler)
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/4/publish': patchCall,
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/4/publish?format=html': patchCall,
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -487,8 +487,8 @@ describe('Action failed, unpublish — a 500 or a network error leaves the row u
])('shows Retry for $name and re-sends the same request', async ({ handler }) => {
const patchCall = vi.fn(handler)
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/3/unpublish': patchCall,
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/3/unpublish?format=html': patchCall,
})
await openRowMenu(wrapper, 'Before and after: the Rue Verte kitchen')
@@ -517,8 +517,8 @@ describe('Action failed, unpublish — a 500 or a network error leaves the row u
describe('Row vanished, draft row — a 404 on publish or delete removes the row and shows an info toast', () => {
it('removes the row on a 404 publish response', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/2/publish': () => mockResponse(404),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/2/publish?format=html': () => mockResponse(404),
})
await openRowMenu(wrapper, 'Choosing lime plaster for old stone walls')
@@ -540,7 +540,7 @@ describe('Row vanished, draft row — a 404 on publish or delete removes the row
it('removes the row when the delete confirmation resolves 404', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'DELETE /v1/article/2': () => mockResponse(404),
})
@@ -565,8 +565,8 @@ describe('Row vanished, draft row — a 404 on publish or delete removes the row
describe('Row vanished, online row — a 404 on unpublish removes the row and shows an info toast', () => {
it('removes the row and updates the tab counts', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/3/unpublish': () => mockResponse(404),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'PATCH /v1/article/3/unpublish?format=html': () => mockResponse(404),
})
await openRowMenu(wrapper, 'Before and after: the Rue Verte kitchen')
@@ -590,7 +590,7 @@ describe('Row vanished, online row — a 404 on unpublish removes the row and sh
describe('Delete confirm — opens the dialog with the row title and image count', () => {
it('opens ConfirmDialog with the title and 3 images', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
})
await openRowMenu(wrapper, 'Winter checklist for timber windows')
@@ -611,7 +611,7 @@ describe('Delete confirm — opens the dialog with the row title and image count
describe('Deleted — a 204 closes the dialog, removes the row, and moves focus to the next row', () => {
it('removes the row, recomputes tabs and raises the success toast', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'DELETE /v1/article/4': () => mockResponse(204),
})
@@ -640,7 +640,7 @@ describe('Deleted — a 204 closes the dialog, removes the row, and moves focus
describe('Delete failed — a 500 keeps the article and rejects with the inline message', () => {
it('throws the inline error message and leaves the article in the list', async () => {
const wrapper = await mountArticles({
'GET /v1/article/': () => mockResponse(200, sixArticleFixture()),
'GET /v1/article/?format=html': () => mockResponse(200, sixArticleFixture()),
'DELETE /v1/article/4': () => mockResponse(500),
})