feat(SPA): screens for Sites and Authors
This commit is contained in:
1 parent
b34a173d83
commit
3d81d2c717
82 files changed
+10676
-37
No files matched your search
@@ -4484,7 +4484,727 @@ function withDetailTabsException(states: Record<string, VisualState>): Record<st
|
||||
}
|
||||
}
|
||||
|
||||
function directorySession(role: string, displayName = 'Marta Alves'): ApiMock {
|
||||
return {
|
||||
status: 200,
|
||||
body: { ...(verifiedSession.body as object), display_name: displayName, role },
|
||||
}
|
||||
}
|
||||
|
||||
const directorySites = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Renew',
|
||||
key: 'renew',
|
||||
base_url: 'https://renew.example',
|
||||
default_social_image_id: 11,
|
||||
default_robots: 'index',
|
||||
locale: 'en',
|
||||
is_default: true,
|
||||
article_count: 268,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Journal',
|
||||
key: 'journal',
|
||||
base_url: 'https://journal.renew.example',
|
||||
default_social_image_id: null,
|
||||
default_robots: 'index',
|
||||
locale: null,
|
||||
is_default: false,
|
||||
article_count: 38,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Projects',
|
||||
key: 'projects',
|
||||
base_url: 'https://projects.renew.example',
|
||||
default_social_image_id: 12,
|
||||
default_robots: 'noindex',
|
||||
locale: 'fr',
|
||||
is_default: false,
|
||||
article_count: 6,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Archive',
|
||||
key: 'archive',
|
||||
base_url: 'https://archive.renew.example',
|
||||
default_social_image_id: null,
|
||||
default_robots: 'noindex',
|
||||
locale: null,
|
||||
is_default: false,
|
||||
article_count: 0,
|
||||
},
|
||||
]
|
||||
|
||||
function sitesApi(
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
role = 'owner',
|
||||
): Record<string, ApiMock> {
|
||||
return {
|
||||
'GET /v1/auth/me': directorySession(role),
|
||||
'GET /v1/site/': { status: 200, body: directorySites },
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const listboxTrigger = 'button[aria-haspopup="listbox"]'
|
||||
|
||||
function isPhone(page: Page) {
|
||||
return (page.viewportSize()?.width ?? 0) < 768
|
||||
}
|
||||
|
||||
async function openDirectoryMenu(page: Page, name: string) {
|
||||
await page.getByRole('button', { name: `Actions for ${name}`, exact: true }).click()
|
||||
await page.getByRole('menuitem').first().waitFor()
|
||||
}
|
||||
|
||||
async function chooseDirectoryMenuItem(page: Page, label: string) {
|
||||
await page.getByRole('menuitem', { name: label, exact: true }).click()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function openDirectoryAdd(page: Page, label: string) {
|
||||
await page.getByRole('button', { name: label, exact: true }).first().click()
|
||||
await page.getByRole('dialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function openSiteEdit(page: Page, name: string) {
|
||||
await openDirectoryMenu(page, name)
|
||||
await chooseDirectoryMenuItem(page, 'Edit')
|
||||
await page.getByRole('dialog').waitFor()
|
||||
}
|
||||
|
||||
async function openSiteDelete(page: Page, name: string) {
|
||||
await openDirectoryMenu(page, name)
|
||||
await chooseDirectoryMenuItem(page, 'Delete…')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
}
|
||||
|
||||
function siteFormField(page: Page, placeholder: string) {
|
||||
return page.getByRole('dialog').getByPlaceholder(placeholder)
|
||||
}
|
||||
|
||||
async function scrollPanelToTop(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
document.querySelectorAll('[role="dialog"] [data-slot="body"]').forEach((body) => {
|
||||
body.scrollTop = 0
|
||||
})
|
||||
})
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function submitDirectoryPanel(page: Page, label: string) {
|
||||
await page.getByRole('dialog').getByRole('button', { name: label, exact: true }).click()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
const sitesStates: Record<string, VisualState> = {
|
||||
Default: { path: '/settings/sites', api: sitesApi() },
|
||||
'Only the default site': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi({
|
||||
'GET /v1/site/': {
|
||||
status: 200,
|
||||
body: [{ ...directorySites[0], article_count: 312 }],
|
||||
},
|
||||
}),
|
||||
},
|
||||
Loading: {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi({ 'GET /v1/site/': { status: 200, hang: true } }),
|
||||
},
|
||||
Error: { path: '/settings/sites', api: sitesApi({ 'GET /v1/site/': { status: 500 } }) },
|
||||
'Row menu': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryMenu(page, 'Journal')
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Default site: delete unavailable': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryMenu(page, 'Renew')
|
||||
if (!isPhone(page)) {
|
||||
await page.getByRole('menuitem', { name: 'Delete…' }).hover()
|
||||
await page.waitForTimeout(TOOLTIP_SETTLE_MS)
|
||||
}
|
||||
},
|
||||
},
|
||||
'Set as default: confirm': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryMenu(page, 'Journal')
|
||||
await chooseDirectoryMenuItem(page, 'Set as default')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
},
|
||||
},
|
||||
'Hidden entry (admin)': { path: '/settings', skip: 'clock-sensitive' },
|
||||
'Forbidden (403)': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi({}, 'admin'),
|
||||
},
|
||||
'Add: empty': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: (page) => openDirectoryAdd(page, 'Add site'),
|
||||
},
|
||||
'Add: key from the name': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add site')
|
||||
await siteFormField(page, 'e.g. Journal').fill('Journal')
|
||||
await siteFormField(page, 'https://blog.example.com').focus()
|
||||
},
|
||||
},
|
||||
'Add: URL not valid': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add site')
|
||||
await siteFormField(page, 'e.g. Journal').fill('Journal')
|
||||
await siteFormField(page, 'https://blog.example.com').fill(
|
||||
'http://journal.renew.example',
|
||||
)
|
||||
await siteFormField(page, 'https://blog.example.com').blur()
|
||||
await page.getByRole('dialog').getByText('Use https. For example').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Add: duplicate key': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add site')
|
||||
await siteFormField(page, 'e.g. Journal').fill('Journal')
|
||||
await page.getByRole('button', { name: 'Edit key', exact: true }).click()
|
||||
await page.getByRole('dialog').getByLabel('Key', { exact: true }).focus()
|
||||
await siteFormField(page, 'https://blog.example.com').fill(
|
||||
'https://journal2.renew.example',
|
||||
)
|
||||
await page.getByRole('dialog').getByText('already exists').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Add: save failed': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi({ 'POST /v1/site/': { status: 500 } }),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add site')
|
||||
await siteFormField(page, 'e.g. Journal').fill('Journal')
|
||||
await siteFormField(page, 'https://blog.example.com').fill(
|
||||
'https://journal.renew.example',
|
||||
)
|
||||
await submitDirectoryPanel(page, 'Add site')
|
||||
await page.getByRole('dialog').getByText("Couldn't save the site").waitFor()
|
||||
},
|
||||
},
|
||||
'Edit: default site': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openSiteEdit(page, 'Renew')
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Edit: other site, noindex': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openSiteEdit(page, 'Projects')
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'With articles': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: (page) => openSiteDelete(page, 'Journal'),
|
||||
},
|
||||
'Choose destination': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: async (page) => {
|
||||
await openSiteDelete(page, 'Journal')
|
||||
await page.getByRole('alertdialog').locator(listboxTrigger).click()
|
||||
await page.getByRole('option').first().waitFor()
|
||||
},
|
||||
},
|
||||
'No articles': {
|
||||
path: '/settings/sites',
|
||||
api: sitesApi(),
|
||||
steps: (page) => openSiteDelete(page, 'Archive'),
|
||||
},
|
||||
}
|
||||
|
||||
const directoryLinks = {
|
||||
camille: [
|
||||
{ type: 'mastodon', url: 'https://mastodon.social/@camille' },
|
||||
{ type: 'linkedin', url: 'linkedin.com/in/camille' },
|
||||
],
|
||||
ines: [{ type: 'linkedin', url: 'https://www.linkedin.com/in/ines-duarte' }],
|
||||
}
|
||||
|
||||
interface DirectoryAuthor {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
bio: string
|
||||
avatar_id: number | null
|
||||
website: string | null
|
||||
social_links: { type: string; url: string }[]
|
||||
user_id: number | null
|
||||
is_linked: boolean
|
||||
article_count: number
|
||||
user_email?: string
|
||||
}
|
||||
|
||||
function directoryAuthors(
|
||||
overrides: Record<string, Partial<DirectoryAuthor>> = {},
|
||||
owner = true,
|
||||
): DirectoryAuthor[] {
|
||||
const base: DirectoryAuthor[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Inès Duarte',
|
||||
slug: 'ines-duarte',
|
||||
bio: 'Site manager and restoration carpenter. Writes about timber and joinery.',
|
||||
avatar_id: 12,
|
||||
website: 'https://ines-duarte.example',
|
||||
social_links: directoryLinks.ines,
|
||||
user_id: 3,
|
||||
is_linked: true,
|
||||
article_count: 9,
|
||||
user_email: 'ines.duarte@renew.example',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jonas Pereira',
|
||||
slug: 'jonas-pereira',
|
||||
bio: 'Heat pumps, insulation and the numbers behind them.',
|
||||
avatar_id: 13,
|
||||
website: null,
|
||||
social_links: [],
|
||||
user_id: 2,
|
||||
is_linked: true,
|
||||
article_count: 6,
|
||||
user_email: 'jonas@renew.example',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Camille Roche',
|
||||
slug: 'camille-roche',
|
||||
bio: 'Freelance writer on lime and stone.',
|
||||
avatar_id: null,
|
||||
website: null,
|
||||
social_links: [],
|
||||
user_id: null,
|
||||
is_linked: false,
|
||||
article_count: 3,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Paul Huber',
|
||||
slug: 'paul-huber',
|
||||
bio: '',
|
||||
avatar_id: 14,
|
||||
website: null,
|
||||
social_links: [],
|
||||
user_id: null,
|
||||
is_linked: false,
|
||||
article_count: 2,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Léa Fontaine',
|
||||
slug: 'lea-fontaine',
|
||||
bio: '',
|
||||
avatar_id: null,
|
||||
website: null,
|
||||
social_links: [],
|
||||
user_id: null,
|
||||
is_linked: false,
|
||||
article_count: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Dr. Alexandra Konstantinopoulou-Whitfield',
|
||||
slug: 'dr-alexandra-konstantinopoulou-whitfield',
|
||||
bio: 'Conservation architect.',
|
||||
avatar_id: null,
|
||||
website: null,
|
||||
social_links: [],
|
||||
user_id: null,
|
||||
is_linked: false,
|
||||
article_count: 1,
|
||||
},
|
||||
]
|
||||
return base.map((author) => {
|
||||
const merged = { ...author, ...(overrides[author.slug] ?? {}) }
|
||||
if (!owner) {
|
||||
delete merged.user_email
|
||||
}
|
||||
return merged
|
||||
})
|
||||
}
|
||||
|
||||
function authorListMock(authors: DirectoryAuthor[]): ApiMock {
|
||||
return {
|
||||
status: 200,
|
||||
bodyFor: (url) => {
|
||||
const q = (url.searchParams.get('q') ?? '').toLowerCase()
|
||||
const matching = authors.filter((author) =>
|
||||
`${author.name} ${author.slug}`.toLowerCase().includes(q),
|
||||
)
|
||||
const size = Number(url.searchParams.get('size') ?? 25)
|
||||
const page = Number(url.searchParams.get('page') ?? 1)
|
||||
return {
|
||||
items: matching.slice((page - 1) * size, page * size),
|
||||
total: matching.length,
|
||||
page,
|
||||
size,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const directoryUsers = [
|
||||
{
|
||||
kind: 'user',
|
||||
id: 3,
|
||||
username: 'ines',
|
||||
display_name: 'Inès Duarte',
|
||||
email: 'ines.duarte@renew.example',
|
||||
role: 'admin',
|
||||
status: 'accepted',
|
||||
created_at: '2026-06-18T09:00:00Z',
|
||||
},
|
||||
{
|
||||
kind: 'user',
|
||||
id: 2,
|
||||
username: 'jonas',
|
||||
display_name: 'Jonas Pereira',
|
||||
email: 'jonas@renew.example',
|
||||
role: 'admin',
|
||||
status: 'accepted',
|
||||
created_at: '2026-04-02T09:00:00Z',
|
||||
},
|
||||
{
|
||||
kind: 'user',
|
||||
id: 1,
|
||||
username: 'marta',
|
||||
display_name: 'Marta Alves',
|
||||
email: 'marta@renew.example',
|
||||
role: 'owner',
|
||||
status: 'accepted',
|
||||
created_at: '2026-03-12T09:00:00Z',
|
||||
},
|
||||
{
|
||||
kind: 'user',
|
||||
id: 4,
|
||||
username: 'rui',
|
||||
display_name: 'Rui Santos',
|
||||
email: 'rui@renew.example',
|
||||
role: 'admin',
|
||||
status: 'accepted',
|
||||
created_at: '2026-05-02T09:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
function authorsApi(
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
options: { role?: string; authors?: DirectoryAuthor[] } = {},
|
||||
): Record<string, ApiMock> {
|
||||
const role = options.role ?? 'owner'
|
||||
return {
|
||||
'GET /v1/auth/me': directorySession(role),
|
||||
'GET /v1/author/': authorListMock(options.authors ?? directoryAuthors({}, role !== 'user')),
|
||||
'GET /v1/user/': { status: 200, body: directoryUsers },
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
async function openAuthorEdit(page: Page, name: string) {
|
||||
await openDirectoryMenu(page, name)
|
||||
await chooseDirectoryMenuItem(page, 'Edit')
|
||||
await page.getByRole('dialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function openAuthorDelete(page: Page, name: string) {
|
||||
await openDirectoryMenu(page, name)
|
||||
await chooseDirectoryMenuItem(page, 'Delete…')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
const avatarPickerNames = [
|
||||
'north-wall.jpg',
|
||||
'kitchen-after.webp',
|
||||
'kitchen-before.webp',
|
||||
'logo-renew.svg',
|
||||
'window-frame.jpg',
|
||||
'lime-plaster.jpg',
|
||||
'floorboards.jpg',
|
||||
'bathroom-tiles.png',
|
||||
]
|
||||
|
||||
const avatarPickerTones = ['0', '1', '3']
|
||||
|
||||
function avatarPickerList() {
|
||||
const library = mediaItems()
|
||||
const items = avatarPickerNames
|
||||
.flatMap((name) => library.filter((item) => item.filename === name))
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
content_type: 'image/jpeg',
|
||||
sha256: `${avatarPickerTones[index % avatarPickerTones.length]}${item.sha256.slice(1)}`,
|
||||
}))
|
||||
return mediaList(items, pickerCounts)
|
||||
}
|
||||
|
||||
const authorsStates: Record<string, VisualState> = {
|
||||
Default: { path: '/authors', api: authorsApi() },
|
||||
Loading: {
|
||||
path: '/authors',
|
||||
api: authorsApi({ 'GET /v1/author/': { status: 200, hang: true } }),
|
||||
},
|
||||
Error: { path: '/authors', api: authorsApi({ 'GET /v1/author/': { status: 500 } }) },
|
||||
'Empty, first run': {
|
||||
path: '/authors',
|
||||
api: authorsApi({}, { authors: [] }),
|
||||
},
|
||||
'No results': { path: '/authors?q=zoe', api: authorsApi() },
|
||||
'Row menu': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryMenu(page, 'Camille Roche')
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Read-only list (other roles)': {
|
||||
path: '/authors',
|
||||
api: authorsApi({}, { role: 'user' }),
|
||||
},
|
||||
'Read-only detail': {
|
||||
path: '/authors',
|
||||
api: authorsApi({}, { role: 'user' }),
|
||||
steps: async (page) => {
|
||||
await page.getByRole('button', { name: 'View Inès Duarte', exact: true }).click()
|
||||
await page.getByRole('dialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Add: empty': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: (page) => openDirectoryAdd(page, 'Add author'),
|
||||
},
|
||||
'Slug from the name': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add author')
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByPlaceholder('e.g. Camille Roche')
|
||||
.fill('Camille Roche')
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByPlaceholder('A sentence or two about the author.')
|
||||
.fill('Freelance writer on lime and stone.')
|
||||
},
|
||||
},
|
||||
'Slug collision': {
|
||||
path: '/authors',
|
||||
api: authorsApi({ 'POST /v1/author/': { status: 409, body: 'author_slug_taken' } }),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add author')
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByPlaceholder('e.g. Camille Roche')
|
||||
.fill('Camille Roche')
|
||||
await page.getByRole('button', { name: 'Edit slug', exact: true }).click()
|
||||
await submitDirectoryPanel(page, 'Add author')
|
||||
await page.getByRole('dialog').getByText('already used').waitFor()
|
||||
},
|
||||
},
|
||||
Validation: {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryAdd(page, 'Add author')
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByPlaceholder('A sentence or two about the author.')
|
||||
.fill('x'.repeat(291))
|
||||
await page.getByRole('dialog').getByPlaceholder('https://example.com').fill('example')
|
||||
await submitDirectoryPanel(page, 'Add author')
|
||||
await page.getByRole('dialog').getByText('Enter a name.').waitFor()
|
||||
await scrollPanelToTop(page)
|
||||
},
|
||||
},
|
||||
'Social links': {
|
||||
path: '/authors',
|
||||
api: authorsApi(
|
||||
{},
|
||||
{
|
||||
authors: directoryAuthors({
|
||||
'camille-roche': { social_links: directoryLinks.camille },
|
||||
}),
|
||||
},
|
||||
),
|
||||
steps: async (page) => {
|
||||
await openAuthorEdit(page, 'Camille Roche')
|
||||
await page.getByRole('button', { name: 'Add link', exact: true }).click()
|
||||
await page.getByRole('dialog').getByLabel('Link URL 3').focus()
|
||||
await scrollPanelToTop(page)
|
||||
},
|
||||
},
|
||||
'Linked user: choose': {
|
||||
path: '/authors',
|
||||
api: authorsApi({}, { authors: directoryAuthors({ 'camille-roche': { bio: '' } }) }),
|
||||
steps: async (page) => {
|
||||
await openAuthorEdit(page, 'Camille Roche')
|
||||
await page.getByRole('dialog').locator(listboxTrigger).click()
|
||||
await page.getByRole('option').first().waitFor()
|
||||
await scrollPanelToTop(page)
|
||||
},
|
||||
},
|
||||
'Edit: linked author': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: (page) => openAuthorEdit(page, 'Inès Duarte'),
|
||||
},
|
||||
'Avatar: picker': {
|
||||
path: '/authors',
|
||||
api: authorsApi(
|
||||
{
|
||||
'GET /v1/media/': { status: 200, bodyFor: avatarPickerList },
|
||||
'GET /v1/image-variants': { status: 403 },
|
||||
},
|
||||
{ authors: directoryAuthors({ 'camille-roche': { bio: '' } }) },
|
||||
),
|
||||
steps: async (page) => {
|
||||
await openAuthorEdit(page, 'Camille Roche')
|
||||
await page.getByRole('button', { name: 'Choose image', exact: true }).click()
|
||||
await waitForPickerTiles(page)
|
||||
await page.locator('[data-picker-tile][aria-label^="kitchen-after.webp,"]').click()
|
||||
await scrollPanelToTop(page)
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Save failed': {
|
||||
path: '/authors',
|
||||
api: authorsApi(
|
||||
{ 'PATCH /v1/author/3': { status: 500 } },
|
||||
{ authors: directoryAuthors({ 'camille-roche': { bio: '' } }) },
|
||||
),
|
||||
steps: async (page) => {
|
||||
await openAuthorEdit(page, 'Camille Roche')
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByPlaceholder('A sentence or two about the author.')
|
||||
.fill('Freelance writer on lime and stone.')
|
||||
await submitDirectoryPanel(page, 'Save changes')
|
||||
await page.getByRole('dialog').getByText("Couldn't save the author").waitFor()
|
||||
},
|
||||
},
|
||||
'Unlink: confirm': {
|
||||
path: '/authors',
|
||||
api: authorsApi(
|
||||
{},
|
||||
{
|
||||
authors: directoryAuthors({
|
||||
'ines-duarte': { social_links: [], bio: '', website: null },
|
||||
}),
|
||||
},
|
||||
),
|
||||
steps: async (page) => {
|
||||
await openAuthorEdit(page, 'Inès Duarte')
|
||||
await page.getByRole('button', { name: 'Unlink', exact: true }).click()
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await scrollPanelToTop(page)
|
||||
},
|
||||
},
|
||||
'Re-link': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: async (page) => {
|
||||
await openDirectoryMenu(page, 'Camille Roche')
|
||||
await chooseDirectoryMenuItem(page, 'Link user')
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await page.getByRole('alertdialog').locator(listboxTrigger).click()
|
||||
await page.getByRole('option').first().waitFor()
|
||||
},
|
||||
},
|
||||
'Reassign (recommended)': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: (page) => openAuthorDelete(page, 'Camille Roche'),
|
||||
},
|
||||
'Choose who': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: async (page) => {
|
||||
await openAuthorDelete(page, 'Camille Roche')
|
||||
await page.getByRole('alertdialog').locator(listboxTrigger).click()
|
||||
await page.getByRole('option').first().waitFor()
|
||||
},
|
||||
},
|
||||
Nobody: {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: async (page) => {
|
||||
await openAuthorDelete(page, 'Camille Roche')
|
||||
await page.getByRole('alertdialog').locator(listboxTrigger).click()
|
||||
await page.getByRole('option', { name: /Nobody/ }).click()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Failed (shared by site and author delete)': {
|
||||
path: '/authors',
|
||||
api: authorsApi({ 'DELETE /v1/author/3': { status: 500 } }),
|
||||
steps: async (page) => {
|
||||
await openAuthorDelete(page, 'Camille Roche')
|
||||
await page
|
||||
.getByRole('alertdialog')
|
||||
.getByRole('button', { name: 'Delete and reassign 3 articles', exact: true })
|
||||
.click()
|
||||
await page.getByRole('alertdialog').getByText("Couldn't delete the author").waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Server fallback (409)': {
|
||||
path: '/authors',
|
||||
api: authorsApi({ 'DELETE /v1/author/3': { status: 409, body: 'author_has_articles' } }),
|
||||
steps: async (page) => {
|
||||
await openAuthorDelete(page, 'Camille Roche')
|
||||
await page
|
||||
.getByRole('alertdialog')
|
||||
.getByRole('button', { name: 'Delete and reassign 3 articles', exact: true })
|
||||
.click()
|
||||
await page.getByRole('alertdialog').getByText("Can't delete this author").waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'No articles': {
|
||||
path: '/authors',
|
||||
api: authorsApi(),
|
||||
steps: (page) => openAuthorDelete(page, 'Léa Fontaine'),
|
||||
},
|
||||
}
|
||||
|
||||
export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
sites: sitesStates,
|
||||
authors: authorsStates,
|
||||
activity: activityStates,
|
||||
profile: {
|
||||
'Account menu': {
|
||||
|
||||
Reference in new issue
Block a user