feat: article editor core

This commit is contained in:
fsylvain committed 2026-09-27 02:11:38 +02:00
1 parent d493a181e1
commit a4734b2c91
55 files changed
+4186 -496

No files matched your search

+8 -5
View File
@@ -206,12 +206,15 @@ async function captureDesign(
const section = [...document.querySelectorAll('section[data-screen-label]')].find(
(element) => element.getAttribute('data-screen-label') === group,
)
const title = section
? [...section.querySelectorAll('span')].find(
(span) => span.textContent?.trim() === state,
const headers = section
? [...section.querySelectorAll<HTMLElement>('div')].filter(
(div) => getComputedStyle(div).maxWidth === '1040px',
)
: undefined
const block = title?.closest('div')?.parentElement
: []
const title = headers.find(
(div) => div.querySelector('span')?.textContent?.trim() === state,
)
const block = title?.parentElement
const target = block
? [...block.querySelectorAll<HTMLElement>('div')].find(
(div) => div.style.width === `${width}px`,
+163
View File
@@ -153,6 +153,56 @@ const verifiedSession: ApiMock = {
body: { is_verified: true, username: 'marta' },
}
const editorNow = '2026-09-26T11:02:00Z'
const bathroomBody =
"<p>Every bathroom is different, but the way we price them isn't. We start with a visit, measure everything, and <strong>write down what stays</strong>.</p>" +
'<h2>What goes into the price</h2>' +
'<ul><li>Stripping out and making good</li><li>Plumbing moves, if any</li><li>Tiles, fittings and a day for silicone</li></ul>' +
'<p>We don’t add a margin on materials you choose yourself.</p>'
const editorArticle = {
id: 12,
title: 'How we price a bathroom renovation',
body: bathroomBody,
images: [
{ id: 1, url: '/images/1.png' },
{ id: 2, url: '/images/2.png' },
{ id: 3, url: '/images/3.png' },
],
is_online: false,
created_at: '2026-03-12T09:14:00Z',
updated_at: editorNow,
}
function editorApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiMock> {
return {
'GET /v1/auth/me': verifiedSession,
'GET /v1/article/12': { status: 200, body: editorArticle },
...overrides,
}
}
async function focusBoldSample(page: Page) {
await page.getByText('write down what stays').click()
}
async function markDirty(page: Page) {
await page.getByText('choose yourself').click()
await page.keyboard.press('End')
await page.keyboard.type('x')
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)
}
export const visualStates: Record<string, Record<string, VisualState>> = {
home: {
Default: {
@@ -319,6 +369,119 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
},
},
},
editor: {
Loading: {
path: '/article/12/edit',
now: editorNow,
api: editorApi({
'GET /v1/article/12': { status: 200, body: editorArticle, hang: true },
}),
},
Saved: {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await focusBoldSample(page)
},
},
'Unsaved changes': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await focusBoldSample(page)
},
},
Saving: {
path: '/article/12/edit',
now: editorNow,
api: editorApi({
'PATCH /v1/article/12': { status: 200, body: editorArticle, hang: true },
}),
steps: async (page) => {
await markDirty(page)
await focusBoldSample(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
},
},
'Save failed': {
path: '/article/12/edit',
now: editorNow,
api: editorApi({ 'PATCH /v1/article/12': { status: 500 } }),
steps: async (page) => {
await markDirty(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
await page.getByText('Save failed').waitFor()
await focusBoldSample(page)
},
},
'Near the limit': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await typeExactBodyLength(page, 9240)
},
},
'Too long': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await typeExactBodyLength(page, 10214)
},
},
Preview: {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
const preview = page.getByRole('switch')
if (await preview.count()) {
await preview.click()
}
},
},
'Leave with unsaved changes': {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
paused: true,
steps: async (page) => {
await markDirty(page)
await focusBoldSample(page)
await page.getByRole('button', { name: 'Back to Articles' }).click()
await page.getByRole('alertdialog').waitFor()
},
},
Offline: {
path: '/article/12/edit',
now: editorNow,
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await focusBoldSample(page)
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', {
configurable: true,
get: () => false,
})
window.dispatchEvent(new Event('offline'))
})
await page.getByText('Waiting for connection').waitFor()
},
},
'Not found (404)': {
path: '/article/9999/edit',
now: editorNow,
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/article/9999': { status: 404 },
},
},
},
verify: {
Verifying: {
path: '/register/validate?c=abc123',