feat: image variants settings screens
This commit is contained in:
1 parent
f1d56ef50e
commit
f567866161
42 files changed
+7192
-11
No files matched your search
@@ -3095,6 +3095,339 @@ const activityStates: Record<string, VisualState> = {
|
||||
Details: activityClockSensitive,
|
||||
}
|
||||
|
||||
const variantsPath = '/settings/media/variants'
|
||||
const variantsMegabyte = 1048576
|
||||
|
||||
const variantsSession: ApiMock = {
|
||||
status: 200,
|
||||
body: {
|
||||
id: 1,
|
||||
is_verified: true,
|
||||
username: 'marta',
|
||||
email: 'marta@renew.example',
|
||||
display_name: 'Marta Alves',
|
||||
role: 'owner',
|
||||
},
|
||||
}
|
||||
|
||||
function boardVariant(
|
||||
name: string,
|
||||
width: number | null,
|
||||
height: number | null,
|
||||
fit: string,
|
||||
format: string,
|
||||
quality: number | null,
|
||||
files: number,
|
||||
megabytes: number,
|
||||
enabled = true,
|
||||
) {
|
||||
return {
|
||||
name,
|
||||
width,
|
||||
height,
|
||||
fit,
|
||||
format,
|
||||
quality,
|
||||
generate_on_upload: false,
|
||||
enabled,
|
||||
generated_files: files,
|
||||
cache_size: megabytes * variantsMegabyte,
|
||||
created_at: '2026-03-12T09:00:00Z',
|
||||
updated_at: '2026-03-12T09:00:00Z',
|
||||
}
|
||||
}
|
||||
|
||||
const variantsBoardList = [
|
||||
boardVariant('thumb', 400, 300, 'cover', 'webp', 80, 412, 21),
|
||||
boardVariant('card', 800, null, 'inside', 'webp', 80, 318, 58),
|
||||
boardVariant('hero', 1200, 630, 'cover', 'webp', 82, 244, 84),
|
||||
boardVariant('square', 600, 600, 'cover', 'jpeg', 85, 140, 19),
|
||||
boardVariant('og', 1200, 630, 'cover', 'jpeg', 80, 96, 9),
|
||||
boardVariant('print', null, 2400, 'inside', 'png', null, 74, 21, false),
|
||||
]
|
||||
|
||||
function variantsApi(
|
||||
items: unknown[] = variantsBoardList,
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
): Record<string, ApiMock> {
|
||||
return {
|
||||
'GET /v1/auth/me': variantsSession,
|
||||
'GET /v1/image-variants': { status: 200, body: { items } },
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function variantsListState(
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
steps?: VisualState['steps'],
|
||||
items: unknown[] = variantsBoardList,
|
||||
): VisualState {
|
||||
return { path: variantsPath, api: variantsApi(items, overrides), steps }
|
||||
}
|
||||
|
||||
async function openVariantMenu(page: Page, name: string) {
|
||||
await page.getByRole('button', { name: `Actions for ${name}`, exact: true }).click()
|
||||
await page.getByRole('menu').waitFor()
|
||||
}
|
||||
|
||||
async function chooseVariantMenuItem(page: Page, name: string, item: string) {
|
||||
await openVariantMenu(page, name)
|
||||
await page.getByRole('menuitem', { name: item, exact: true }).click()
|
||||
}
|
||||
|
||||
async function scrollVariantsPage(page: Page, offset: number) {
|
||||
await page.evaluate((value) => {
|
||||
const body = document
|
||||
.querySelector<HTMLElement>('#dashboard-content')
|
||||
?.closest<HTMLElement>('[data-slot="body"]')
|
||||
if (body) body.scrollTop = value
|
||||
}, offset)
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
|
||||
async function scrollVariantsForm(page: Page, offset: number) {
|
||||
await page.evaluate((value) => {
|
||||
const body = document.querySelector<HTMLElement>('[role="dialog"] [data-slot="body"]')
|
||||
if (body) body.scrollTop = value
|
||||
}, offset)
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
|
||||
function isMobileViewport(page: Page) {
|
||||
return (page.viewportSize()?.width ?? 0) < 640
|
||||
}
|
||||
|
||||
async function openVariantForm(page: Page) {
|
||||
await page.getByRole('button', { name: 'Add variant', exact: true }).first().click()
|
||||
await page.getByRole('dialog').waitFor()
|
||||
}
|
||||
|
||||
function variantField(page: Page, name: string) {
|
||||
return page.locator(`[data-field="${name}"] input:not([aria-hidden="true"])`)
|
||||
}
|
||||
|
||||
async function pickVariantFormat(page: Page, label: string) {
|
||||
await page.getByRole('dialog').getByRole('combobox').click()
|
||||
await page.getByRole('option', { name: label, exact: true }).click()
|
||||
}
|
||||
|
||||
interface VariantFormFill {
|
||||
name: string
|
||||
width: string
|
||||
height: string
|
||||
format: string
|
||||
quality?: string
|
||||
generate?: boolean
|
||||
}
|
||||
|
||||
async function fillVariantForm(page: Page, values: VariantFormFill) {
|
||||
await variantField(page, 'name').fill(values.name)
|
||||
await variantField(page, 'width').fill(values.width)
|
||||
await variantField(page, 'height').fill(values.height)
|
||||
await pickVariantFormat(page, values.format)
|
||||
if (values.quality !== undefined) {
|
||||
await variantField(page, 'quality').fill(values.quality)
|
||||
}
|
||||
if (values.generate) {
|
||||
await page.getByRole('switch', { name: 'Generate on upload', exact: true }).click()
|
||||
}
|
||||
}
|
||||
|
||||
const heroForm: VariantFormFill = {
|
||||
name: 'hero',
|
||||
width: '1200',
|
||||
height: '630',
|
||||
format: 'WebP',
|
||||
quality: '82',
|
||||
generate: true,
|
||||
}
|
||||
|
||||
async function blurFocus(page: Page) {
|
||||
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
async function pressVariantFormButton(page: Page, label: string) {
|
||||
await page.getByRole('dialog').getByRole('button', { name: label, exact: true }).click()
|
||||
}
|
||||
|
||||
async function openEditHero(page: Page) {
|
||||
await chooseVariantMenuItem(page, 'hero', 'Edit')
|
||||
await page.getByRole('dialog').waitFor()
|
||||
}
|
||||
|
||||
const variantsStates: Record<string, VisualState> = {
|
||||
Loaded: variantsListState(),
|
||||
Loading: {
|
||||
path: variantsPath,
|
||||
api: variantsApi(variantsBoardList, {
|
||||
'GET /v1/image-variants': { status: 200, hang: true },
|
||||
}),
|
||||
},
|
||||
Empty: variantsListState({}, undefined, []),
|
||||
'Error loading': variantsListState({ 'GET /v1/image-variants': { status: 500 } }),
|
||||
Forbidden: {
|
||||
path: variantsPath,
|
||||
api: {
|
||||
'GET /v1/auth/me': {
|
||||
status: 200,
|
||||
body: { ...(variantsSession.body as object), role: 'user' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'Row menu': variantsListState({}, (page) => openVariantMenu(page, 'hero')),
|
||||
'Row action in progress': variantsListState(
|
||||
{ 'POST /v1/image-variants/hero/purge': { status: 200, hang: true } },
|
||||
async (page) => {
|
||||
await chooseVariantMenuItem(page, 'hero', 'Purge cache')
|
||||
await page
|
||||
.getByRole('alertdialog')
|
||||
.getByRole('button', { name: 'Purge 244 files' })
|
||||
.click()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
),
|
||||
'Purge confirm': variantsListState({}, async (page) => {
|
||||
await chooseVariantMenuItem(page, 'hero', 'Purge cache')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
}),
|
||||
'Delete confirm': variantsListState({}, async (page) => {
|
||||
await chooseVariantMenuItem(page, 'hero', 'Delete')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
}),
|
||||
'Delete in progress': variantsListState(
|
||||
{ 'DELETE /v1/image-variants/hero': { status: 204, hang: true } },
|
||||
async (page) => {
|
||||
await chooseVariantMenuItem(page, 'hero', 'Delete')
|
||||
await page
|
||||
.getByRole('alertdialog')
|
||||
.getByRole('button', { name: 'Delete variant' })
|
||||
.click()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
),
|
||||
'Entry: sidebar': {
|
||||
path: '/media',
|
||||
origin: mediaOrigin,
|
||||
api: mediaApi({ 'GET /v1/auth/me': variantsSession }),
|
||||
steps: async (page) => {
|
||||
if (isMobileViewport(page)) {
|
||||
await page.locator('[data-slot="toggle"]').first().click()
|
||||
}
|
||||
await page
|
||||
.locator(
|
||||
'nav[aria-label="Main"]:visible a[href="/settings"] [data-slot="linkTrailing"]',
|
||||
)
|
||||
.click()
|
||||
await page.mouse.move(0, 0)
|
||||
await page.waitForTimeout(250)
|
||||
},
|
||||
},
|
||||
'Request reference': {
|
||||
...variantsListState({}, async (page) => {
|
||||
if (isMobileViewport(page)) {
|
||||
await scrollVariantsPage(page, 900)
|
||||
}
|
||||
}),
|
||||
exception: {
|
||||
maxPercent: 21,
|
||||
reason: 'board draws /media/photo.png?v=thumb (2 lines) and the mandated /static/uploadedImages/photo.png?v=thumb wraps to 3; board scroll 900 is unreachable (page max ~861)',
|
||||
approvedBy: 'user',
|
||||
},
|
||||
},
|
||||
'Create: pristine': variantsListState({}, openVariantForm),
|
||||
'Create: dirty': variantsListState({}, async (page) => {
|
||||
await openVariantForm(page)
|
||||
await fillVariantForm(page, heroForm)
|
||||
await page.locator('input[type="range"]').focus()
|
||||
if (isMobileViewport(page)) {
|
||||
await scrollVariantsForm(page, 560)
|
||||
}
|
||||
}),
|
||||
'Field errors': variantsListState({}, async (page) => {
|
||||
await openVariantForm(page)
|
||||
await fillVariantForm(page, {
|
||||
name: 'Hero banner',
|
||||
width: '9000',
|
||||
height: '630',
|
||||
format: 'WebP',
|
||||
quality: '140',
|
||||
})
|
||||
await variantField(page, 'name').blur()
|
||||
await variantField(page, 'width').blur()
|
||||
await variantField(page, 'quality').blur()
|
||||
await blurFocus(page)
|
||||
}),
|
||||
'No dimension': variantsListState({}, async (page) => {
|
||||
await openVariantForm(page)
|
||||
await variantField(page, 'name').fill('banner')
|
||||
await pickVariantFormat(page, 'WebP')
|
||||
await pressVariantFormButton(page, 'Add variant')
|
||||
await blurFocus(page)
|
||||
}),
|
||||
'Name already taken': variantsListState(
|
||||
{
|
||||
'POST /v1/image-variants': {
|
||||
status: 409,
|
||||
body: 'A variant named “thumb” already exists. Choose another name.',
|
||||
},
|
||||
},
|
||||
async (page) => {
|
||||
await openVariantForm(page)
|
||||
await fillVariantForm(page, {
|
||||
name: 'thumb',
|
||||
width: '400',
|
||||
height: '300',
|
||||
format: 'WebP',
|
||||
quality: '80',
|
||||
generate: true,
|
||||
})
|
||||
await pressVariantFormButton(page, 'Add variant')
|
||||
await page.getByText('already exists').waitFor()
|
||||
await blurFocus(page)
|
||||
},
|
||||
),
|
||||
Saving: variantsListState(
|
||||
{ 'POST /v1/image-variants': { status: 201, hang: true } },
|
||||
async (page) => {
|
||||
await openVariantForm(page)
|
||||
await fillVariantForm(page, heroForm)
|
||||
await pressVariantFormButton(page, 'Add variant')
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
),
|
||||
'Save error': variantsListState(
|
||||
{ 'POST /v1/image-variants': { status: 500 } },
|
||||
async (page) => {
|
||||
await openVariantForm(page)
|
||||
await fillVariantForm(page, heroForm)
|
||||
await pressVariantFormButton(page, 'Add variant')
|
||||
await page.getByText("Couldn't save the variant").waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
),
|
||||
'Edit: pristine': variantsListState({}, openEditHero),
|
||||
'Edit: dirty': variantsListState({}, async (page) => {
|
||||
await openEditHero(page)
|
||||
await variantField(page, 'quality').fill('70')
|
||||
await blurFocus(page)
|
||||
}),
|
||||
'Unsaved changes': variantsListState({}, async (page) => {
|
||||
await openEditHero(page)
|
||||
await variantField(page, 'quality').fill('70')
|
||||
await pressVariantFormButton(page, 'Cancel')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
}),
|
||||
'PNG: no quality': variantsListState({}, async (page) => {
|
||||
await openVariantForm(page)
|
||||
await fillVariantForm(page, { ...heroForm, quality: undefined })
|
||||
await pickVariantFormat(page, 'PNG')
|
||||
await blurFocus(page)
|
||||
}),
|
||||
}
|
||||
|
||||
export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
activity: activityStates,
|
||||
profile: {
|
||||
@@ -4233,6 +4566,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
steps: submitAcceptInvite('lea', 'limewash2026'),
|
||||
},
|
||||
},
|
||||
'image-variants': variantsStates,
|
||||
media: { ...mediaStates, ...mediaPurgeStates },
|
||||
'media-picker': pickerStates,
|
||||
}
|
||||
Reference in new issue
Block a user