refac: DELETE /v1/user/{id}?expected_role_id=N, user.remove audit event, 409 target_role_not_found, GetAll returns its error, Own-scope visibility filter moved into repos, JSON tags removed from use-case types
This commit is contained in:
1 parent
5d6f378fce
commit
b39a41be88
124 files changed
+8816
-523
No files matched your search
+172
-4
@@ -45,8 +45,64 @@ const PAGINATION_STATES = [
|
||||
]
|
||||
|
||||
const schemaBoard = 'RenewCMS Screens Schema.dc.html'
|
||||
const rolesBoard = 'RenewCMS Screens Roles.dc.html'
|
||||
|
||||
const extraSources: Record<string, Source[]> = {
|
||||
roles: [
|
||||
{
|
||||
board: rolesBoard,
|
||||
group: '12 Toasts',
|
||||
states: [
|
||||
'Role changed',
|
||||
'Role edited elsewhere (409)',
|
||||
'Role deleted elsewhere',
|
||||
"Role not changed: member's role changed first (409)",
|
||||
"Ownership not transferred: target's role changed first (409)",
|
||||
"User not removed: member's role changed first (409)",
|
||||
],
|
||||
alias: {},
|
||||
},
|
||||
{
|
||||
board: rolesBoard,
|
||||
group: '13 Inline errors',
|
||||
states: [
|
||||
'Role editor: save failed (500)',
|
||||
'Change role: failed (500)',
|
||||
'Delete role: target role deleted',
|
||||
'Delete role: role gained members',
|
||||
'Delete role: move failed (500)',
|
||||
'Invite: invalid email',
|
||||
'Invite: address already has an account (409)',
|
||||
'Invite: role above yours (403)',
|
||||
'Invite: send failed (500)',
|
||||
],
|
||||
alias: {},
|
||||
},
|
||||
{
|
||||
board: rolesBoard,
|
||||
group: '14 Read = Own',
|
||||
states: [
|
||||
'Role editor: Read set to Own',
|
||||
'Articles list: Read = Own',
|
||||
'Articles list: no drafts of their own',
|
||||
'Entry list: Read = Own',
|
||||
'Entry list: no entries of their own',
|
||||
],
|
||||
alias: {},
|
||||
},
|
||||
{
|
||||
board: rolesBoard,
|
||||
group: '15 409 conflicts',
|
||||
states: [
|
||||
'Stale save: banner',
|
||||
'Stale save: review differences',
|
||||
'Stale save: edits kept after reload',
|
||||
"Change role: member's role changed first (409)",
|
||||
"Transfer ownership: target's role changed first (409)",
|
||||
],
|
||||
alias: {},
|
||||
},
|
||||
],
|
||||
schema: [
|
||||
{
|
||||
board: schemaBoard,
|
||||
@@ -131,7 +187,7 @@ const extraSources: Record<string, Source[]> = {
|
||||
{
|
||||
board: schemaBoard,
|
||||
group: '7 Relations',
|
||||
states: ['Relation in the entry form'],
|
||||
states: ['Relation in the entry form', 'Relation picker: loading more'],
|
||||
alias: {},
|
||||
},
|
||||
{
|
||||
@@ -342,6 +398,12 @@ const extraSources: Record<string, Source[]> = {
|
||||
},
|
||||
],
|
||||
profile: [
|
||||
{
|
||||
board: rolesBoard,
|
||||
group: '9 Profile',
|
||||
states: ['Role card: custom role', 'Role card: Owner'],
|
||||
alias: {},
|
||||
},
|
||||
{
|
||||
board: officeBoard,
|
||||
group: 'Profile: author profile',
|
||||
@@ -765,10 +827,25 @@ function referenceWithoutGroup(name: string, markdown: string) {
|
||||
return group && board ? [board[0], board[1]!, `"${group}"`] : null
|
||||
}
|
||||
|
||||
function rolesScreen(): Screen {
|
||||
const sources = extraSources.roles!
|
||||
const [first] = sources
|
||||
return {
|
||||
name: 'roles',
|
||||
board: first!.board,
|
||||
group: first!.group,
|
||||
states: sources.flatMap((source) => source.states),
|
||||
sources,
|
||||
}
|
||||
}
|
||||
|
||||
async function readScreen(name: string): Promise<Screen> {
|
||||
if (name === 'image-variants') {
|
||||
return variantsScreen()
|
||||
}
|
||||
if (name === 'roles') {
|
||||
return rolesScreen()
|
||||
}
|
||||
const markdown = await readFile(join(designRoot, 'screens', `${name}.md`), 'utf8')
|
||||
const reference = VISUAL_REFERENCE.exec(markdown) ?? referenceWithoutGroup(name, markdown)
|
||||
if (!reference) {
|
||||
@@ -955,12 +1032,78 @@ async function openBoard(browser: Browser, origin: string, screen: Screen, theme
|
||||
await serveIconsLocally(context)
|
||||
const page = await context.newPage()
|
||||
await page.goto(`${origin}/${encodeURIComponent(screen.board)}`)
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
await page.reload()
|
||||
await page.locator(`section[data-screen-label="${screen.group}"]`).waitFor()
|
||||
await page.getByRole('radio', { name: theme === 'dark' ? 'Dark' : 'Light' }).click()
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
return page
|
||||
}
|
||||
|
||||
const REALIGNED_BOARD_STATES = new Set(['9j. Navigation: scroll-spy on Content types'])
|
||||
const SCROLL_REALIGN_MAX_DRIFT = 60
|
||||
const BOARD_SCROLL_OFFSETS = {
|
||||
desktop: [32],
|
||||
mobile: [64],
|
||||
}
|
||||
|
||||
async function realignBoardScroll(frame: ReturnType<Page['locator']>, viewport: Viewport) {
|
||||
await frame.evaluate(
|
||||
(root, { offsets, maxDrift }) => {
|
||||
const anchors = [...root.querySelectorAll<HTMLElement>('[data-anchor]')].filter(
|
||||
(anchor) => !anchor.dataset.anchor?.startsWith('ep-'),
|
||||
)
|
||||
const first = anchors[0]
|
||||
if (!first) {
|
||||
return
|
||||
}
|
||||
let scroller: HTMLElement | null = first.parentElement
|
||||
while (scroller && !['auto', 'scroll'].includes(getComputedStyle(scroller).overflowY)) {
|
||||
scroller = scroller.parentElement
|
||||
}
|
||||
if (!scroller || scroller.scrollTop === 0) {
|
||||
return
|
||||
}
|
||||
const offsetTopWithin = (element: HTMLElement, within: HTMLElement) => {
|
||||
const parent = element.offsetParent as HTMLElement | null
|
||||
return element.offsetTop + (parent && parent !== within ? parent.offsetTop : 0)
|
||||
}
|
||||
let best: number | null = null
|
||||
for (const anchor of anchors) {
|
||||
for (const offset of offsets) {
|
||||
const target = Math.max(0, offsetTopWithin(anchor, scroller) - offset)
|
||||
const drift = Math.abs(target - scroller.scrollTop)
|
||||
if (
|
||||
drift <= maxDrift &&
|
||||
(best === null || drift < Math.abs(best - scroller.scrollTop))
|
||||
) {
|
||||
best = target
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best !== null) {
|
||||
scroller.scrollTop = best
|
||||
}
|
||||
},
|
||||
{ offsets: BOARD_SCROLL_OFFSETS[viewport], maxDrift: SCROLL_REALIGN_MAX_DRIFT },
|
||||
)
|
||||
}
|
||||
|
||||
const STABLE_SAMPLES = 3
|
||||
const STABLE_INTERVAL_MS = 200
|
||||
const STABLE_MAX_POLLS = 40
|
||||
|
||||
async function waitForStableFrame(board: Page, frame: ReturnType<Page['locator']>) {
|
||||
let previous: Buffer | null = null
|
||||
let steady = 0
|
||||
for (let poll = 0; poll < STABLE_MAX_POLLS && steady < STABLE_SAMPLES; poll++) {
|
||||
const shot = await frame.screenshot({ animations: 'disabled' })
|
||||
steady = previous !== null && shot.equals(previous) ? steady + 1 : 0
|
||||
previous = shot
|
||||
await board.waitForTimeout(STABLE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
async function captureDesign(
|
||||
board: Page,
|
||||
screen: Screen,
|
||||
@@ -969,10 +1112,14 @@ async function captureDesign(
|
||||
path: string,
|
||||
) {
|
||||
const found = await board.evaluate(
|
||||
({ group, state, width, height }) => {
|
||||
({ group, state, width, height, widths }) => {
|
||||
document
|
||||
.querySelectorAll('[data-visual-check]')
|
||||
.forEach((element) => element.removeAttribute('data-visual-check'))
|
||||
document.querySelectorAll<HTMLElement>('[data-visual-hidden]').forEach((element) => {
|
||||
element.style.visibility = ''
|
||||
element.removeAttribute('data-visual-hidden')
|
||||
})
|
||||
const section = [...document.querySelectorAll('section[data-screen-label]')].find(
|
||||
(element) => element.getAttribute('data-screen-label') === group,
|
||||
)
|
||||
@@ -1007,9 +1154,26 @@ async function captureDesign(
|
||||
container.style.height = `${height}px`
|
||||
target.style.zoom = '1'
|
||||
target.dataset.visualCheck = 'frame'
|
||||
const frameWidths = new Set(widths.map((value) => `${value}px`))
|
||||
for (const other of section!.querySelectorAll<HTMLElement>('div')) {
|
||||
if (
|
||||
other !== target &&
|
||||
frameWidths.has(other.style.width) &&
|
||||
!other.contains(target) &&
|
||||
!target.contains(other)
|
||||
) {
|
||||
other.dataset.visualHidden = 'true'
|
||||
other.style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ group: screen.group, state, ...frames[viewport] },
|
||||
{
|
||||
group: screen.group,
|
||||
state,
|
||||
...frames[viewport],
|
||||
widths: [frames.desktop.width, frames.mobile.width],
|
||||
},
|
||||
)
|
||||
if (!found) {
|
||||
throw new Error(`State "${state}" not found in group "${screen.group}" of ${screen.board}`)
|
||||
@@ -1037,6 +1201,10 @@ async function captureDesign(
|
||||
)
|
||||
})
|
||||
await board.waitForTimeout(settleMs)
|
||||
if (REALIGNED_BOARD_STATES.has(state)) {
|
||||
await realignBoardScroll(frame, viewport)
|
||||
}
|
||||
await waitForStableFrame(board, frame)
|
||||
await frame.screenshot({ path, animations: 'disabled' })
|
||||
}
|
||||
|
||||
@@ -1064,7 +1232,7 @@ async function captureApp(
|
||||
viewport: { width, height },
|
||||
colorScheme: theme,
|
||||
reducedMotion: 'reduce',
|
||||
...(setup.now ? { timezoneId: 'UTC' } : {}),
|
||||
...(setup.now || setup.timezone ? { timezoneId: setup.timezone ?? 'UTC' } : {}),
|
||||
})
|
||||
await serveIconsLocally(context)
|
||||
const unmocked = new Set<string>()
|
||||
|
||||
Reference in new issue
Block a user