feat: sites and authors integration into existing screens (& formated all frontend files kinda accidentally)
This commit is contained in:
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 },
|
||||
)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user