feat: sites and authors integration into existing screens (& formated all frontend files kinda accidentally)

This commit is contained in:
Florian Sylvain committed 2026-10-15 05:25:44 +02:00
1 parent 3d81d2c717
commit 6b8990c55e
239 files changed
+9194 -1354

No files matched your search

@@ -8,7 +8,9 @@ import RootComponent from '@/App.vue'
import ArticleEditor from '@/components/ArticleEditor.vue'
vi.mock('@nuxt/ui/composables', () => ({
useOverlay: () => ({ create: () => ({ open: () => Promise.resolve(false) }) }),
useOverlay: () => ({
create: () => ({ open: () => Promise.resolve(false) }),
}),
useToast: () => ({ add: vi.fn() }),
}))
@@ -96,7 +98,9 @@ describe('insertImage stamps the inserted image with its numeric id', () => {
const editor = wrapper.findComponent(ArticleEditor)
;(
editor.vm as unknown as { insertImage: (src: string, alt: string, id: number) => void }
editor.vm as unknown as {
insertImage: (src: string, alt: string, id: number) => void
}
).insertImage('/static/uploadedImages/photo.png', 'alt text', 10)
await flushPromises()
@@ -267,7 +267,10 @@ describe('The link button across its states', () => {
try {
await mountEditor()
linkButton().dispatchEvent(
new PointerEvent('pointermove', { bubbles: true, pointerType: 'mouse' }),
new PointerEvent('pointermove', {
bubbles: true,
pointerType: 'mouse',
}),
)
await vi.advanceTimersByTimeAsync(399)
expect(document.body.textContent).not.toContain('Insert link (Ctrl+K)')
@@ -284,7 +287,10 @@ describe('The link button across its states', () => {
try {
await mountEditor()
linkButton().dispatchEvent(
new PointerEvent('pointermove', { bubbles: true, pointerType: 'mouse' }),
new PointerEvent('pointermove', {
bubbles: true,
pointerType: 'mouse',
}),
)
await vi.advanceTimersByTimeAsync(401)
expect(document.body.textContent).toContain('Insert link (⌘K)')
@@ -299,7 +305,10 @@ describe('The link button across its states', () => {
try {
await mountEditor(SENTENCE, PHONE)
linkButton().dispatchEvent(
new PointerEvent('pointermove', { bubbles: true, pointerType: 'touch' }),
new PointerEvent('pointermove', {
bubbles: true,
pointerType: 'touch',
}),
)
await vi.advanceTimersByTimeAsync(1000)
expect(document.body.textContent).not.toContain('Insert link (')
@@ -380,16 +389,27 @@ describe('Ctrl+K', () => {
const Host = defineComponent({
render: () =>
h(UApp, null, () =>
h(ArticleEditor, { modelValue: '<p>Hello</p>', articleId: 1, disabled: true }),
h(ArticleEditor, {
modelValue: '<p>Hello</p>',
articleId: 1,
disabled: true,
}),
),
})
const host = mount(Host, { attachTo: document.body, global: { plugins: [uiPlugin] } })
const host = mount(Host, {
attachTo: document.body,
global: { plugins: [uiPlugin] },
})
await settle()
const vm = host.findComponent(ArticleEditor).vm as unknown as {
$: { setupState: { editorInstance: Editor } }
}
vm.$.setupState.editorInstance.view.dom.dispatchEvent(
new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }),
new KeyboardEvent('keydown', {
key: 'k',
ctrlKey: true,
bubbles: true,
}),
)
await settle()
@@ -403,7 +423,11 @@ describe('Ctrl+K', () => {
preview.click()
await settle()
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }),
new KeyboardEvent('keydown', {
key: 'k',
ctrlKey: true,
bubbles: true,
}),
)
await settle()
@@ -731,7 +755,11 @@ describe('Leaving the dialog without saving', () => {
it('on Esc', async () => {
await openWithChanges()
field('text').dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }),
new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true,
cancelable: true,
}),
)
await settle()
expectUntouched()
@@ -809,7 +837,11 @@ describe('Dialog or sheet by viewport, popover or sheet by input', () => {
editor()
.view.dom.querySelector('a')!
.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
new KeyboardEvent('keydown', {
key: 'Enter',
bubbles: true,
cancelable: true,
}),
)
await settle()
@@ -870,8 +902,14 @@ describe('Toolbar overflow', () => {
function layout(scrollLeft: number) {
const element = toolbar()
Object.defineProperty(element, 'clientWidth', { configurable: true, value: 390 })
Object.defineProperty(element, 'scrollWidth', { configurable: true, value: 1100 })
Object.defineProperty(element, 'clientWidth', {
configurable: true,
value: 390,
})
Object.defineProperty(element, 'scrollWidth', {
configurable: true,
value: 1100,
})
element.scrollLeft = scrollLeft
element.dispatchEvent(new Event('scroll'))
}
@@ -919,7 +957,10 @@ describe('Selections that straddle a link or span blocks', () => {
await mountEditor(BODY)
const [first, last] = [selection.split(' ')[0]!, selection.split(' ').at(-1)!]
const from = positionOf(first)
editor().commands.setTextSelection({ from, to: positionOf(last) + last.length })
editor().commands.setTextSelection({
from,
to: positionOf(last) + last.length,
})
const before = editor().state.doc.textContent
await openDialog()
type(field('url'), '/b')
@@ -15,7 +15,10 @@ let wrapper: VueWrapper | null = null
function stubClipboard() {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
})
return writeText
}
@@ -40,7 +40,13 @@ async function mountShell(path: string, options: ShellOptions = {}) {
setViewportWidth(width)
setActivePinia(createPinia())
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn().mockResolvedValue({ verified: true, username, email, displayName, role }),
getMe: vi.fn().mockResolvedValue({
verified: true,
username,
email,
displayName,
role,
}),
request: vi.fn().mockResolvedValue(undefined),
})
await router.push(path)
@@ -408,7 +414,9 @@ describe('DashboardShell Settings expands to the Media subpages', () => {
)
it('marks Variants current and Settings not current on the variants page', async () => {
const wrapper = await mountShell('/settings/media/variants', { role: 'admin' })
const wrapper = await mountShell('/settings/media/variants', {
role: 'admin',
})
const nav = wrapper.find('nav[aria-label="Main"]')
expect(nav.find('a[href="/settings/media/variants"]').attributes('aria-current')).toBe(
@@ -418,7 +426,9 @@ describe('DashboardShell Settings expands to the Media subpages', () => {
})
it('marks Cache current and Settings not current on the cache page', async () => {
const wrapper = await mountShell('/settings/media/cache', { role: 'admin' })
const wrapper = await mountShell('/settings/media/cache', {
role: 'admin',
})
const nav = wrapper.find('nav[aria-label="Main"]')
expect(nav.find('a[href="/settings/media/cache"]').attributes('aria-current')).toBe('page')
@@ -471,7 +481,9 @@ describe('DashboardShell selected nav rows use sand with on-sand text', () => {
})
it('keeps the unfilled Settings parent bold when a child is selected', async () => {
const wrapper = await mountShell('/settings/media/variants', { role: 'owner' })
const wrapper = await mountShell('/settings/media/variants', {
role: 'owner',
})
const parent = wrapper.find('nav[aria-label="Main"] a[href="/settings"]')
expect(parent.attributes('aria-current')).toBeUndefined()
@@ -21,7 +21,12 @@ const ROWS: Row[] = [
function menu(row: Row): DirectoryMenuItem[] {
return [
{ key: 'edit', label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => undefined },
{
key: 'edit',
label: 'Edit',
icon: 'i-lucide-pencil',
onSelect: () => undefined,
},
{
key: 'delete',
label: 'Delete…',
@@ -103,7 +103,11 @@ describe('DirectoryPanel', () => {
it('shows the failure notice inside the panel', async () => {
await mountInApp(
DirectoryPanel,
{ title: 'T', submitLabel: 'Add', notice: { description: 'Could not save.' } },
{
title: 'T',
submitLabel: 'Add',
notice: { description: 'Could not save.' },
},
{ default: body },
)
+6 -18
View File
@@ -18,9 +18,7 @@ async function mountNotFound(breadcrumb?: string) {
const router = makeRouter()
await router.push('/')
return mount(ErrorView, {
props: breadcrumb
? { kind: 'not-found', breadcrumb }
: { kind: 'not-found' },
props: breadcrumb ? { kind: 'not-found', breadcrumb } : { kind: 'not-found' },
global: {
plugins: [router],
},
@@ -31,30 +29,20 @@ describe('ErrorView not-found kind', () => {
it('renders the not-found copy with a breadcrumb, primary and secondary actions', async () => {
const wrapper = await mountNotFound('Not found')
expect(wrapper.find('nav[aria-label="Breadcrumb"]').text()).toBe(
'Not found',
)
expect(wrapper.find('nav[aria-label="Breadcrumb"]').text()).toBe('Not found')
expect(wrapper.find('h1').text()).toBe('Page not found')
expect(wrapper.text()).toContain(
'The link may be mistyped, or the page may have moved.',
)
expect(wrapper.text()).toContain('The link may be mistyped, or the page may have moved.')
const buttons = wrapper.findAll('button')
expect(buttons.some((button) => button.text() === 'Go to Home')).toBe(
true,
)
expect(buttons.some((button) => button.text() === 'All articles')).toBe(
true,
)
expect(buttons.some((button) => button.text() === 'Go to Home')).toBe(true)
expect(buttons.some((button) => button.text() === 'All articles')).toBe(true)
})
it('routes the primary action to /home and the secondary action to /article', async () => {
const wrapper = await mountNotFound()
const router = wrapper.vm.$router
const primary = wrapper
.findAll('button')
.find((button) => button.text() === 'Go to Home')
const primary = wrapper.findAll('button').find((button) => button.text() === 'Go to Home')
await primary?.trigger('click')
await flushPromises()
expect(router.currentRoute.value.path).toBe('/home')
@@ -45,7 +45,10 @@ vi.mock('@/lib/imageCrop', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/imageCrop')>()
return {
...actual,
readNaturalImageSize: vi.fn(async () => ({ width: 4032, height: 3024 })),
readNaturalImageSize: vi.fn(async () => ({
width: 4032,
height: 3024,
})),
renderCroppedImage: vi.fn(async () => new Blob([new Uint8Array(430080)])),
}
})
@@ -127,7 +130,10 @@ function mib(value: number) {
function fileFixture(name: string, type: string, sizeMB: number) {
const file = new File(['x'], name, { type })
Object.defineProperty(file, 'size', { value: mib(sizeMB), configurable: true })
Object.defineProperty(file, 'size', {
value: mib(sizeMB),
configurable: true,
})
return file
}
@@ -219,7 +225,9 @@ describe('Dragging a file over the drop zone shows the dragging-over state', ()
describe('Dropping a file opens Upload with the file', () => {
it('looks the hash up, sends no list request and opens the crop step', async () => {
installApi()
wrapper = mountUploader({ initialFile: fileFixture('before.png', 'image/png', 3.9) })
wrapper = mountUploader({
initialFile: fileFixture('before.png', 'image/png', 3.9),
})
await openUploader(wrapper)
expect(hashCalls()).toEqual([NORTH_WALL_HASH])
@@ -326,7 +334,9 @@ describe('A file whose content exists shows the duplicate card and sends nothing
})
it('drops the "by" part when the uploader is unknown', async () => {
await openOnUpload({ hash: () => pageBody([mediaItem({ uploaded_by: '' })], null) })
await openOnUpload({
hash: () => pageBody([mediaItem({ uploaded_by: '' })], null),
})
await chooseFile(fileFixture('north-wall.jpg', 'image/jpeg', 3.9))
@@ -544,7 +554,9 @@ describe('A server answer for a file that passed the browser checks', () => {
async ({ file, type, sizeMB, status, title, text, confirm }) => {
await openOnUpload()
const chosen = fileFixture(file, type, sizeMB)
vi.mocked(imageUpload.validateImageFile).mockReturnValueOnce({ ok: true })
vi.mocked(imageUpload.validateImageFile).mockReturnValueOnce({
ok: true,
})
await chooseFile(chosen)
if (findButton('Upload image')) {
await clickButton('Upload image')
@@ -98,7 +98,10 @@ function mib(value: number) {
function fileFixture(name: string, type: string, sizeMB: number) {
const file = new File(['x'], name, { type })
Object.defineProperty(file, 'size', { value: mib(sizeMB), configurable: true })
Object.defineProperty(file, 'size', {
value: mib(sizeMB),
configurable: true,
})
return file
}
@@ -208,9 +211,27 @@ describe('An SVG skips the crop step and uploads immediately', () => {
describe('Choosing an aspect ratio locks the frame to it', () => {
it.each([
{ ratio: '16:9', cropWidth: 4032, cropHeight: 2268, outWidth: 1600, outHeight: 900 },
{ ratio: '4:3', cropWidth: 4032, cropHeight: 3024, outWidth: 1600, outHeight: 1200 },
{ ratio: '1:1', cropWidth: 3024, cropHeight: 3024, outWidth: 1600, outHeight: 1600 },
{
ratio: '16:9',
cropWidth: 4032,
cropHeight: 2268,
outWidth: 1600,
outHeight: 900,
},
{
ratio: '4:3',
cropWidth: 4032,
cropHeight: 3024,
outWidth: 1600,
outHeight: 1200,
},
{
ratio: '1:1',
cropWidth: 3024,
cropHeight: 3024,
outWidth: 1600,
outHeight: 1600,
},
])(
'locks the frame for $ratio',
async ({ ratio, cropWidth, cropHeight, outWidth, outHeight }) => {
@@ -221,7 +242,9 @@ describe('Choosing an aspect ratio locks the frame to it', () => {
await selectRatioPill(ratio)
const rect = (
wrapper.findComponent(ImageUploader).vm as unknown as { cropRect: CropRect }
wrapper.findComponent(ImageUploader).vm as unknown as {
cropRect: CropRect
}
).cropRect
expect(rect.width).toBe(cropWidth)
expect(rect.height).toBe(cropHeight)
@@ -308,7 +331,11 @@ describe('Arrow keys move the crop frame and Shift+arrow keys resize it', () =>
expect(document.body.textContent).toContain('Uploads as 1600 × 1600 px · about')
cropFrame().dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight', shiftKey: true, bubbles: true }),
new KeyboardEvent('keydown', {
key: 'ArrowRight',
shiftKey: true,
bubbles: true,
}),
)
await nextTick()
@@ -329,7 +356,11 @@ describe('Dragging the crop frame moves it, and dragging a corner handle resizes
const before = { ...uploader.cropRect }
cropFrame().dispatchEvent(
new MouseEvent('mousedown', { clientX: 0, clientY: 0, bubbles: true }),
new MouseEvent('mousedown', {
clientX: 0,
clientY: 0,
bubbles: true,
}),
)
window.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 40 }))
window.dispatchEvent(new MouseEvent('mouseup'))
@@ -348,7 +379,13 @@ describe('Dragging the crop frame moves it, and dragging a corner handle resizes
const handle = document.body.querySelectorAll<HTMLElement>(
'[data-testid="crop-handle"]',
)[3]!
handle.dispatchEvent(new MouseEvent('mousedown', { clientX: 0, clientY: 0, bubbles: true }))
handle.dispatchEvent(
new MouseEvent('mousedown', {
clientX: 0,
clientY: 0,
bubbles: true,
}),
)
const afterMove = { ...uploader.cropRect }
window.dispatchEvent(new MouseEvent('mousemove', { clientX: -50, clientY: -50 }))
window.dispatchEvent(new MouseEvent('mouseup'))
@@ -34,7 +34,10 @@ vi.mock('@/lib/imageCrop', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/imageCrop')>()
return {
...actual,
readNaturalImageSize: vi.fn(async () => ({ width: 4032, height: 3024 })),
readNaturalImageSize: vi.fn(async () => ({
width: 4032,
height: 3024,
})),
}
})
@@ -353,9 +356,12 @@ describe('Insert', () => {
[
'presses Escape',
async () => {
document.body
.querySelector('[role="dialog"]')!
.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
document.body.querySelector('[role="dialog"]')!.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true,
}),
)
await settle()
},
],
@@ -82,7 +82,10 @@ describe('MediaField', () => {
})
it('returns the picked image id through Use image and offers Change image and Remove', async () => {
const wrapper = await mountInApp(MediaField, { label: 'Avatar', picker: PICKER })
const wrapper = await mountInApp(MediaField, {
label: 'Avatar',
picker: PICKER,
})
byText('button', 'Choose image')!.click()
await flushPromises()
const picker = document.body.querySelector<HTMLElement>('[role="dialog"]')!
@@ -34,7 +34,10 @@ const Host = defineComponent({
emits: ['created'],
setup(_props, { emit }) {
const open = ref(true)
return { open, onCreated: (article: Article) => emit('created', article) }
return {
open,
onCreated: (article: Article) => emit('created', article),
}
},
template: `<NewArticleModal v-model:open="open" @created="onCreated" />`,
})
@@ -155,7 +158,10 @@ describe('Submitting disables the form and cannot be dismissed', () => {
resolveRequest = resolve
}),
)
vi.mocked(useApi).mockReturnValue({ getMe: vi.fn(), request: requestMock })
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn(),
request: requestMock,
})
const wrapper = mount(Host, {
attachTo: document.body,
global: { plugins: [uiPlugin] },
@@ -199,7 +205,10 @@ describe('Submitting disables the form and cannot be dismissed', () => {
describe('A successful create closes the modal and hands off to the editor with a toast', () => {
it('closes the modal and emits created with the response body', async () => {
requestMock = vi.fn().mockResolvedValue(articleFixture())
vi.mocked(useApi).mockReturnValue({ getMe: vi.fn(), request: requestMock })
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn(),
request: requestMock,
})
const wrapper = mount(Host, {
attachTo: document.body,
global: { plugins: [uiPlugin] },
@@ -219,7 +228,10 @@ describe('A successful create closes the modal and hands off to the editor with
describe("Title rejected — a 400 that got past this form's own check", () => {
it('keeps the values, shows the title error and the form alert', async () => {
requestMock = vi.fn().mockRejectedValue(new ApiError(400))
vi.mocked(useApi).mockReturnValue({ getMe: vi.fn(), request: requestMock })
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn(),
request: requestMock,
})
const wrapper = mount(Host, {
attachTo: document.body,
global: { plugins: [uiPlugin] },
@@ -246,7 +258,10 @@ describe('Error — a 500 or a network failure keeps the text and offers to retr
{ name: 'a network failure', error: () => new NetworkError() },
])('keeps the values and shows the generic alert for $name', async ({ error }) => {
requestMock = vi.fn().mockRejectedValue(error())
vi.mocked(useApi).mockReturnValue({ getMe: vi.fn(), request: requestMock })
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn(),
request: requestMock,
})
const wrapper = mount(Host, {
attachTo: document.body,
global: { plugins: [uiPlugin] },
@@ -269,7 +284,10 @@ describe('Error — a 500 or a network failure keeps the text and offers to retr
describe('Retrying after an Error succeeds', () => {
it('sends the same body again and hands off to the editor on success', async () => {
requestMock = vi.fn().mockRejectedValueOnce(new ApiError(500))
vi.mocked(useApi).mockReturnValue({ getMe: vi.fn(), request: requestMock })
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn(),
request: requestMock,
})
const wrapper = mount(Host, {
attachTo: document.body,
global: { plugins: [uiPlugin] },
@@ -3,11 +3,7 @@ import { mount } from '@vue/test-utils'
import uiPlugin from '@nuxt/ui/vue-plugin'
import PublishControl from '../PublishControl.vue'
function mountControl(props: {
online: boolean
dirty: boolean
disabled?: boolean
}) {
function mountControl(props: { online: boolean; dirty: boolean; disabled?: boolean }) {
return mount(PublishControl, {
props,
global: { plugins: [uiPlugin] },
@@ -46,8 +42,6 @@ describe('Disabled', () => {
disabled: true,
})
expect(
(wrapper.find('button').element as HTMLButtonElement).disabled,
).toBe(true)
expect((wrapper.find('button').element as HTMLButtonElement).disabled).toBe(true)
})
})
@@ -4,7 +4,12 @@ import ReassignDialog from '../ReassignDialog.vue'
import { byText, mountInApp, text, unmountApp } from './directoryKit'
const OPTIONS = [
{ value: 1, label: 'Renew', description: 'renew.example', display: 'Renew · renew.example' },
{
value: 1,
label: 'Renew',
description: 'renew.example',
display: 'Renew · renew.example',
},
{
value: 3,
label: 'Projects',
@@ -75,7 +80,12 @@ describe('ReassignDialog', () => {
it('shows the failure notice with its title and text', async () => {
await mountInApp(
ReassignDialog,
props({ notice: { title: 'Failed', description: 'Nothing was changed.' } }),
props({
notice: {
title: 'Failed',
description: 'Nothing was changed.',
},
}),
)
expect(text(dialog().querySelector('[role="alert"]'))).toBe('Failed Nothing was changed.')
@@ -77,7 +77,12 @@ describe('SlugField', () => {
it('draws the locked variant with a lock and its own help, with no pencil', async () => {
await mountInApp(
SlugField,
props({ mode: 'saved', value: 'renew', locked: true, lockedHelp: "Can't change." }),
props({
mode: 'saved',
value: 'renew',
locked: true,
lockedHelp: "Can't change.",
}),
)
expect(input().readOnly).toBe(true)
@@ -33,8 +33,7 @@ let currentWrapper: Awaited<ReturnType<typeof mount>> | null = null
async function mountLogin() {
setActivePinia(createPinia())
global.fetch = (() =>
mockResponse(200, { open: false })) as unknown as typeof fetch
global.fetch = (() => mockResponse(200, { open: false })) as unknown as typeof fetch
await router.push('/login')
const wrapper = mount(LoginView, {
attachTo: document.body,
@@ -70,9 +69,7 @@ describe('AppLogo renders the brand mark as inline SVG, and theme-switches', ()
expect(card.element.contains(svg.element)).toBe(true)
const html = wrapper.html()
expect(html.indexOf('<svg')).toBeGreaterThan(
html.indexOf('data-testid="auth-card"'),
)
expect(html.indexOf('<svg')).toBeGreaterThan(html.indexOf('data-testid="auth-card"'))
expect(html.indexOf('<svg')).toBeLessThan(html.indexOf('<h1'))
let centeringAncestor = svg.element.parentElement
@@ -113,9 +110,7 @@ describe("UPageCard's title is the page h1, and its description uses UPageCard's
expect(heading.text()).toBe('Log in')
expect(heading.attributes('tabindex')).toBe('-1')
expect(wrapper.text()).toContain(
'Welcome back. Log in to manage your articles.',
)
expect(wrapper.text()).toContain('Welcome back. Log in to manage your articles.')
expect(wrapper.findAll('p').length).toBe(0)
})
})
@@ -180,12 +175,8 @@ describe('The login form is a UForm', () => {
const form = wrapper.find('form')
expect(form.exists()).toBe(true)
expect(form.attributes('id')).toMatch(/^v-/)
expect(form.find('input[placeholder="Your username"]').exists()).toBe(
true,
)
expect(
form.find('input[autocomplete="current-password"]').exists(),
).toBe(true)
expect(form.find('input[placeholder="Your username"]').exists()).toBe(true)
expect(form.find('input[autocomplete="current-password"]').exists()).toBe(true)
expect(form.find('button[type="submit"]').exists()).toBe(true)
})
})
@@ -194,20 +185,14 @@ describe('"Forgot password?" renders in the Password field\'s hint slot', () =>
it('renders the link through the hint slot, and the label reads only Password', async () => {
const wrapper = await mountLogin()
const passwordInput = wrapper.find(
'input[autocomplete="current-password"]',
)
const fieldRoot = passwordInput.element.closest(
'[data-slot="root"][data-orientation]',
)
const passwordInput = wrapper.find('input[autocomplete="current-password"]')
const fieldRoot = passwordInput.element.closest('[data-slot="root"][data-orientation]')
expect(fieldRoot).toBeTruthy()
const hint = fieldRoot!.querySelector('[data-slot="hint"]')
expect(hint).toBeTruthy()
expect(hint!.textContent?.trim()).toBe('Forgot password?')
expect(
hint!.querySelector('a[href="/register/reset/request"]'),
).toBeTruthy()
expect(hint!.querySelector('a[href="/register/reset/request"]')).toBeTruthy()
const label = fieldRoot!.querySelector('[data-slot="label"]')
expect(label!.textContent?.trim()).toBe('Password')
@@ -219,9 +204,7 @@ describe('Inputs are neutral by default and turn to the error edge only when the
const wrapper = await mountLogin()
const usernameInput = wrapper.find('input[placeholder="Your username"]')
const passwordInput = wrapper.find(
'input[autocomplete="current-password"]',
)
const passwordInput = wrapper.find('input[autocomplete="current-password"]')
expect(usernameInput.classes()).toContain('ring-accented')
expect(usernameInput.classes()).not.toContain('ring-error')
expect(passwordInput.classes()).toContain('ring-accented')
@@ -232,9 +215,7 @@ describe('Inputs are neutral by default and turn to the error edge only when the
await flushPromises()
const usernameAfter = wrapper.find('input[placeholder="Your username"]')
const passwordAfter = wrapper.find(
'input[autocomplete="current-password"]',
)
const passwordAfter = wrapper.find('input[autocomplete="current-password"]')
expect(usernameAfter.classes()).toContain('ring-error')
expect(usernameAfter.classes()).not.toContain('ring-accented')
expect(passwordAfter.classes()).toContain('ring-error')
+4 -1
View File
@@ -25,7 +25,10 @@ export async function mountInApp(
default: () => h(component, props, slots as never),
}),
}),
{ attachTo: document.body, global: { plugins: [router, uiPlugin, pinia] } },
{
attachTo: document.body,
global: { plugins: [router, uiPlugin, pinia] },
},
)
await flushPromises()
return holder.wrapper
@@ -81,7 +81,11 @@ afterEach(async () => {
function press(element: Element, value: string) {
element.dispatchEvent(
new KeyboardEvent('keydown', { key: value, bubbles: true, cancelable: true }),
new KeyboardEvent('keydown', {
key: value,
bubbles: true,
cancelable: true,
}),
)
}
@@ -140,7 +144,11 @@ async function openAccountMenu(surface: Surface) {
if (surface === 'desktop UserMenu popover') {
wrapper = mount(UserMenu, {
attachTo: document.body,
props: { username: 'marta', email: SIGNED_IN.email, collapsed: false },
props: {
username: 'marta',
email: SIGNED_IN.email,
collapsed: false,
},
global: { plugins: [ui, router] },
})
document.body.querySelector<HTMLButtonElement>('button[aria-haspopup]')!.click()
@@ -330,7 +338,9 @@ describe('Choosing a language in the account menu applies it at once and keeps t
true,
true,
true,
expect.objectContaining({ mock: expect.objectContaining({ calls: [] }) }),
expect.objectContaining({
mock: expect.objectContaining({ calls: [] }),
}),
'/profile',
'light',
])
@@ -351,7 +361,10 @@ describe('Choosing a language in the account menu applies it at once and keeps t
signIn()
setViewportWidth(layout === 'desktop' ? DESKTOP_WIDTH : MOBILE_WIDTH)
await router.push('/profile')
wrapper = mount(App, { attachTo: document.body, global: { plugins: [ui, router] } })
wrapper = mount(App, {
attachTo: document.body,
global: { plugins: [ui, router] },
})
await flushPromises()
await wrapper.find('input[autocomplete="off"]').setValue('Alice B.')
if (layout === 'mobile') {
@@ -541,7 +554,9 @@ describe('Auth footer shows the language chip beside the theme chip', () => {
)
it('keeps the footer slot content to the left of the chips', async () => {
await mountAuthShell(false, { footer: '<a href="/register">Create an account</a>' })
await mountAuthShell(false, {
footer: '<a href="/register">Create an account</a>',
})
const footer = document.body.querySelector('footer')!
const link = footer.querySelector('a')!
@@ -635,7 +650,10 @@ describe('Auth language popover', () => {
signOut()
await router.push('/login')
const { default: view } = await import('@/views/LoginView.vue')
wrapper = mount(view, { attachTo: document.body, global: { plugins: [ui, router] } })
wrapper = mount(view, {
attachTo: document.body,
global: { plugins: [ui, router] },
})
await flushPromises()
authChip().focus()
authChip().click()
@@ -712,7 +730,10 @@ describe('Each auth screen carries the language chip', () => {
session.status = 'ready'
await router.push(path)
const { default: view } = await load()
wrapper = mount(view, { attachTo: document.body, global: { plugins: [ui, router] } })
wrapper = mount(view, {
attachTo: document.body,
global: { plugins: [ui, router] },
})
await flushPromises()
expect(authChip().textContent?.trim()).toBe('English')
@@ -724,7 +745,10 @@ describe('Each auth screen carries the language chip', () => {
signOut()
await router.push('/login')
const { default: view } = await import('@/views/LoginView.vue')
wrapper = mount(view, { attachTo: document.body, global: { plugins: [ui, router] } })
wrapper = mount(view, {
attachTo: document.body,
global: { plugins: [ui, router] },
})
await flushPromises()
await wrapper.find('input[autocomplete="username"]').setValue('marta')
+7 -1
View File
@@ -15,7 +15,13 @@ export const FIRST_PAGE_TOTAL = 48
export const PICKER_COUNTS = { all: 48, unused: 2, duplicates: 2 }
export function pageBody(items: MediaItem[] = fixture(), total: number | null = FIRST_PAGE_TOTAL) {
return { items, total: total ?? items.length, page: 1, size: 25, counts: PICKER_COUNTS }
return {
items,
total: total ?? items.length,
page: 1,
size: 25,
counts: PICKER_COUNTS,
}
}
export function secondPage() {