feat(integration): add image variants section and standardize code snippets
This commit is contained in:
1 parent
6f9ae6207a
commit
e6746f2be7
28 files changed
+2256
-422
No files matched your search
+29
-1
@@ -32,6 +32,24 @@ const VISUAL_REFERENCE =
|
||||
const officeBoard = 'RenewCMS Screens Back-office.dc.html'
|
||||
|
||||
const extraSources: Record<string, Source[]> = {
|
||||
integration: [
|
||||
{
|
||||
board: officeBoard,
|
||||
group: 'Integration: images',
|
||||
states: [
|
||||
'I1 · Many variants',
|
||||
'I2 · One variant',
|
||||
'I3 · No variants, admin',
|
||||
'I4 · No variants, not an admin',
|
||||
'I5 · Variant removed',
|
||||
'I6 · Loading',
|
||||
'I7 · Error loading',
|
||||
'I8 · Snippet copied',
|
||||
'I9 · URL builder copied',
|
||||
],
|
||||
alias: {},
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
board: officeBoard,
|
||||
@@ -390,12 +408,22 @@ function groupSectionRows(markdown: string, group: string) {
|
||||
return [...section.matchAll(/^\| \d+ \| ([^|]+?) \|/gm)].map((match) => match[1]!.trim())
|
||||
}
|
||||
|
||||
const referenceGroups: Record<string, string> = {
|
||||
'integration-api-keys': 'API keys',
|
||||
}
|
||||
|
||||
function referenceWithoutGroup(name: string, markdown: string) {
|
||||
const group = referenceGroups[name]
|
||||
const board = /Visual reference:\*{0,2}\s*`(?:source\/)?([^`]+)`/.exec(markdown)
|
||||
return group && board ? [board[0], board[1]!, `"${group}"`] : null
|
||||
}
|
||||
|
||||
async function readScreen(name: string): Promise<Screen> {
|
||||
if (name === 'image-variants') {
|
||||
return variantsScreen()
|
||||
}
|
||||
const markdown = await readFile(join(designRoot, 'screens', `${name}.md`), 'utf8')
|
||||
const reference = VISUAL_REFERENCE.exec(markdown)
|
||||
const reference = VISUAL_REFERENCE.exec(markdown) ?? referenceWithoutGroup(name, markdown)
|
||||
if (!reference) {
|
||||
throw new Error(`docs/design/screens/${name}.md has no visual reference line`)
|
||||
}
|
||||
|
||||
+168
-14
@@ -972,18 +972,118 @@ function inviteWith(email: string, response?: ApiMock) {
|
||||
const integrationOrigin = 'https://cms.renew.example'
|
||||
|
||||
async function clickBaseUrlCopy(page: Page) {
|
||||
await page.getByRole('button', { name: 'Copy', exact: true }).first().click()
|
||||
await page.getByRole('button', { name: 'Copy code', exact: true }).first().click()
|
||||
}
|
||||
|
||||
const integrationImagesOffsets = { desktop: -9, mobile: -40 }
|
||||
|
||||
type ImageVariantRow = [string, number, number | null, string]
|
||||
|
||||
const integrationVariantRows: ImageVariantRow[] = [
|
||||
['thumb', 400, 300, 'webp'],
|
||||
['card', 800, null, 'webp'],
|
||||
['hero', 1200, 630, 'webp'],
|
||||
['square', 600, 600, 'jpeg'],
|
||||
['og', 1200, 630, 'jpeg'],
|
||||
['banner', 1600, 400, 'webp'],
|
||||
['teaser', 640, 360, 'webp'],
|
||||
['sidebar', 320, null, 'webp'],
|
||||
['gallery', 1000, 750, 'webp'],
|
||||
['avatar', 96, 96, 'webp'],
|
||||
['wide', 1920, 800, 'webp'],
|
||||
['mini', 120, 90, 'webp'],
|
||||
]
|
||||
|
||||
function integrationVariant(row: ImageVariantRow, enabled = true) {
|
||||
const [name, width, height, format] = row
|
||||
return {
|
||||
name,
|
||||
width,
|
||||
height,
|
||||
fit: 'cover',
|
||||
format,
|
||||
quality: 82,
|
||||
generate_on_upload: false,
|
||||
enabled,
|
||||
generated_files: 0,
|
||||
cache_size: 0,
|
||||
created_at: '2026-03-12T09:00:00Z',
|
||||
updated_at: '2026-03-12T09:00:00Z',
|
||||
}
|
||||
}
|
||||
|
||||
function integrationVariantsList(rows: ImageVariantRow[], withDisabled = false): ApiMock {
|
||||
const items = rows.map((row) => integrationVariant(row))
|
||||
if (withDisabled) {
|
||||
items.push(integrationVariant(['print', 0, null, 'png'], false))
|
||||
}
|
||||
return { status: 200, body: { items } }
|
||||
}
|
||||
|
||||
const integrationRemovedRows = integrationVariantRows.filter((row) => row[0] !== 'hero').slice(0, 5)
|
||||
|
||||
const integrationManyVariants = integrationVariantsList(integrationVariantRows, true)
|
||||
|
||||
function afterImagesLoaded(steps: VisualState['steps']): VisualState['steps'] {
|
||||
if (!steps) {
|
||||
return undefined
|
||||
}
|
||||
return async (page) => {
|
||||
await page.locator('[data-anchor="images"] h3').first().waitFor()
|
||||
await steps(page)
|
||||
}
|
||||
}
|
||||
|
||||
function integrationState(overrides: Partial<VisualState> = {}): VisualState {
|
||||
return {
|
||||
path: '/integration',
|
||||
origin: integrationOrigin,
|
||||
api: { 'GET /v1/auth/me': verifiedSession },
|
||||
api: {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/image-variants': integrationManyVariants,
|
||||
},
|
||||
...overrides,
|
||||
steps: afterImagesLoaded(overrides.steps),
|
||||
}
|
||||
}
|
||||
|
||||
function integrationImagesState(
|
||||
list: ApiMock | null,
|
||||
overrides: Partial<VisualState> & { session?: ApiMock } = {},
|
||||
): VisualState {
|
||||
const { session = adminSession, ...rest } = overrides
|
||||
return {
|
||||
path: '/integration',
|
||||
origin: integrationOrigin,
|
||||
api: {
|
||||
'GET /v1/auth/me': session,
|
||||
...(list ? { 'GET /v1/image-variants': list } : {}),
|
||||
},
|
||||
steps: (page) => scrollToAnchor(page, 'images', integrationImagesOffsets),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
const memberSession: ApiMock = {
|
||||
status: 200,
|
||||
body: { ...(verifiedSession.body as object), role: 'member' },
|
||||
}
|
||||
|
||||
async function pickBuilderVariant(page: Page, name: string) {
|
||||
await page.locator('button[aria-label="Variant"]').click()
|
||||
await page.getByRole('option', { name, exact: true }).click()
|
||||
}
|
||||
|
||||
async function copyUnder(page: Page, heading: string) {
|
||||
await scrollToAnchor(page, 'images', integrationImagesOffsets)
|
||||
await page
|
||||
.locator('[data-anchor="images"] h3', { hasText: heading })
|
||||
.locator('xpath=..')
|
||||
.getByRole('button', { name: 'Copy code' })
|
||||
.click()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function scrollToAnchor(
|
||||
page: Page,
|
||||
anchor: string,
|
||||
@@ -2378,12 +2478,9 @@ const apiKeysStates: Record<string, VisualState> = {
|
||||
await revealApiKey(page)
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByRole('button', { name: 'Copy', exact: true })
|
||||
.getByRole('button', { name: 'Copy code', exact: true })
|
||||
.click()
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByRole('button', { name: 'Copied', exact: true })
|
||||
.waitFor()
|
||||
await page.getByRole('dialog').getByText('Copied', { exact: true }).waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
@@ -4447,13 +4544,16 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
'3a. Copied: base URL': integrationState({ steps: clickBaseUrlCopy }),
|
||||
'3b. Copied: CodeBlock': integrationState({
|
||||
steps: async (page) => {
|
||||
await scrollToAnchor(page, 'auth')
|
||||
await page.getByRole('button', { name: 'Copy Shell example' }).click()
|
||||
await scrollToAnchor(page, 'auth', { desktop: 29, mobile: 63 })
|
||||
await page
|
||||
.locator('[data-anchor="auth"]')
|
||||
.getByRole('button', { name: 'Copy code' })
|
||||
.click()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
}),
|
||||
'4. Authentication': integrationState({
|
||||
steps: (page) => scrollToAnchor(page, 'auth'),
|
||||
steps: (page) => scrollToAnchor(page, 'auth', { desktop: 29, mobile: 63 }),
|
||||
}),
|
||||
'5. Session only': integrationState({
|
||||
steps: (page) => scrollToAnchor(page, 'session', { desktop: -1, mobile: 55 }),
|
||||
@@ -4464,16 +4564,70 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
},
|
||||
}),
|
||||
'6. Errors': integrationState({
|
||||
steps: (page) => scrollToAnchor(page, 'errors', { desktop: 48, mobile: 47 }),
|
||||
steps: (page) => scrollToAnchor(page, 'errors', { desktop: 60, mobile: 55 }),
|
||||
exception: {
|
||||
maxPercent: 12.5,
|
||||
reason: 'A 409 row is new, the typical-cause text uses the real server messages in place of the longer board prose, and the board scroll position varies between captures',
|
||||
maxPercent: { desktop: 12.5, mobile: 19 },
|
||||
reason: 'A 409 row is new, the typical-cause text uses the real server messages in place of the longer board prose, and the board changed (17.33 % mobile already at HEAD against the updated board)',
|
||||
approvedBy: 'Florian Sylvain',
|
||||
},
|
||||
}),
|
||||
'I1 · Many variants': integrationImagesState(integrationManyVariants),
|
||||
'I2 · One variant': integrationImagesState(
|
||||
integrationVariantsList(integrationVariantRows.slice(0, 1)),
|
||||
{
|
||||
exception: {
|
||||
maxPercent: { desktop: 16, mobile: 10 },
|
||||
reason: 'The board draws the striped preview offset by 32 px',
|
||||
approvedBy: 'Florian Sylvain',
|
||||
},
|
||||
},
|
||||
),
|
||||
'I3 · No variants, admin': integrationImagesState({ status: 200, body: { items: [] } }),
|
||||
'I4 · No variants, not an admin': integrationImagesState(null, { session: memberSession }),
|
||||
'I5 · Variant removed': integrationImagesState(
|
||||
{
|
||||
status: 200,
|
||||
bodyFor: (_url, call) => ({
|
||||
items: (call === 1
|
||||
? integrationVariantRows.slice(0, 5)
|
||||
: integrationRemovedRows
|
||||
).map((row) => integrationVariant(row)),
|
||||
}),
|
||||
},
|
||||
{
|
||||
steps: async (page) => {
|
||||
await scrollToAnchor(page, 'images', integrationImagesOffsets)
|
||||
await pickBuilderVariant(page, 'hero')
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
|
||||
await page.getByText('hero (removed)').waitFor()
|
||||
await scrollToAnchor(page, 'images', integrationImagesOffsets)
|
||||
},
|
||||
},
|
||||
),
|
||||
'I6 · Loading': integrationImagesState({ status: 200, hang: true }),
|
||||
'I7 · Error loading': integrationImagesState({ status: 500 }),
|
||||
'I8 · Snippet copied': integrationImagesState(integrationManyVariants, {
|
||||
exception: {
|
||||
maxPercent: { desktop: 12, mobile: 10 },
|
||||
reason: 'The board draws the striped preview offset by 32 px',
|
||||
approvedBy: 'Florian Sylvain',
|
||||
},
|
||||
steps: async (page) => {
|
||||
await scrollToAnchor(page, 'images', integrationImagesOffsets)
|
||||
await page.getByRole('tab', { name: 'JavaScript', exact: true }).click()
|
||||
await copyUnder(page, 'Request an image')
|
||||
},
|
||||
}),
|
||||
'I9 · URL builder copied': integrationImagesState(integrationManyVariants, {
|
||||
steps: async (page) => {
|
||||
await pickBuilderVariant(page, 'card')
|
||||
await copyUnder(page, 'URL builder')
|
||||
await scrollToAnchor(page, 'images', integrationImagesOffsets)
|
||||
},
|
||||
}),
|
||||
'7. Navigation': integrationState({
|
||||
steps: async (page) => {
|
||||
await scrollToAnchor(page, 'media', { desktop: 14 })
|
||||
await scrollToAnchor(page, 'media', { desktop: -5, mobile: -3 })
|
||||
if ((page.viewportSize()?.width ?? 0) < 1024) {
|
||||
await page.getByRole('button', { name: 'Jump to section' }).click()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user