192 lines
5.5 KiB
TypeScript
192 lines
5.5 KiB
TypeScript
import { vi } from 'vitest'
|
|
import { defineComponent, nextTick, ref } from 'vue'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import uiPlugin from '@nuxt/ui/vue-plugin'
|
|
import ImageUploader from '../ImageUploader.vue'
|
|
import { useApi } from '@/composables/useApi'
|
|
import type { MediaItem } from '@/composables/useMedia'
|
|
import { DESKTOP_WIDTH, setViewportWidth } from './viewport'
|
|
import { fixture, textOf } from '@/views/__tests__/mediaTestKit'
|
|
|
|
export { textOf }
|
|
|
|
export const FIRST_PAGE_CURSOR = 'c24'
|
|
export const PICKER_COUNTS = { all: 48, unused: 2, duplicates: 2 }
|
|
|
|
export function pageBody(
|
|
items: MediaItem[] = fixture(),
|
|
next_cursor: string | null = FIRST_PAGE_CURSOR,
|
|
) {
|
|
return { items, next_cursor, counts: PICKER_COUNTS }
|
|
}
|
|
|
|
export function secondPage() {
|
|
return fixture().map((item) => ({
|
|
...item,
|
|
id: item.id + 12,
|
|
filename: `more-${item.filename}`,
|
|
url: `/media/more-${item.filename}`,
|
|
}))
|
|
}
|
|
|
|
export interface PickerRoutes {
|
|
list?: (query: URLSearchParams) => unknown
|
|
hash?: (hash: string) => unknown
|
|
}
|
|
|
|
export const apiKit = { request: vi.fn() }
|
|
|
|
export function installApi(routes: PickerRoutes = {}) {
|
|
apiKit.request = vi.fn(async (path: string) => {
|
|
const url = new URL(path, 'http://localhost')
|
|
const hash = url.searchParams.get('hash')
|
|
if (hash !== null) {
|
|
return (routes.hash ?? (() => pageBody([], null)))(hash)
|
|
}
|
|
return (routes.list ?? (() => pageBody()))(url.searchParams)
|
|
})
|
|
vi.mocked(useApi).mockReturnValue({
|
|
getMe: vi.fn().mockResolvedValue({ verified: true, username: 'marta', id: 1 }),
|
|
request: apiKit.request,
|
|
})
|
|
}
|
|
|
|
export function installEmptyLibrary(routes: PickerRoutes = {}) {
|
|
installApi({ list: () => pageBody([], null), ...routes })
|
|
}
|
|
|
|
export function listCalls() {
|
|
return apiKit.request.mock.calls
|
|
.map(([path]) => new URL(path as string, 'http://localhost'))
|
|
.filter((url) => url.pathname === '/v1/media/' && !url.searchParams.has('hash'))
|
|
.map((url) => url.searchParams)
|
|
}
|
|
|
|
export function hashCalls() {
|
|
return apiKit.request.mock.calls
|
|
.map(([path]) => new URL(path as string, 'http://localhost'))
|
|
.filter((url) => url.searchParams.has('hash'))
|
|
.map((url) => url.searchParams.get('hash'))
|
|
}
|
|
|
|
export const Host = defineComponent({
|
|
components: { ImageUploader },
|
|
props: {
|
|
initialFile: { type: Object as () => File | null, default: null },
|
|
inArticleIds: { type: Array as () => number[], default: () => [1, 4] },
|
|
},
|
|
emits: ['uploaded', 'inserted', 'close'],
|
|
setup(_props, { emit }) {
|
|
const open = ref(false)
|
|
return {
|
|
open,
|
|
onUploaded: (item: MediaItem) => emit('uploaded', item),
|
|
onInserted: (item: MediaItem, alt: string, src: string) =>
|
|
emit('inserted', item, alt, src),
|
|
onClose: () => emit('close'),
|
|
}
|
|
},
|
|
template: `<ImageUploader v-model:open="open" :initial-file="initialFile" :in-article-ids="inArticleIds" @uploaded="onUploaded" @inserted="onInserted" @close="onClose" />`,
|
|
})
|
|
|
|
export function mountUploader(
|
|
props: { initialFile?: File | null; inArticleIds?: number[] } = {},
|
|
width = DESKTOP_WIDTH,
|
|
) {
|
|
setViewportWidth(width)
|
|
setActivePinia(createPinia())
|
|
return mount(Host, {
|
|
props,
|
|
global: { plugins: [uiPlugin] },
|
|
attachTo: document.body,
|
|
})
|
|
}
|
|
|
|
export async function openUploader(wrapper: ReturnType<typeof mountUploader>) {
|
|
;(wrapper.vm as unknown as { open: boolean }).open = true
|
|
await nextTick()
|
|
await nextTick()
|
|
await flushPromises()
|
|
}
|
|
|
|
export async function settle() {
|
|
await flushPromises()
|
|
await nextTick()
|
|
await flushPromises()
|
|
}
|
|
|
|
export function tabButtons() {
|
|
return Array.from(document.body.querySelectorAll<HTMLElement>('[role="tab"]'))
|
|
}
|
|
|
|
export function tabButton(label: string) {
|
|
return tabButtons().find((element) => textOf(element) === label)
|
|
}
|
|
|
|
export function selectedTab() {
|
|
return tabButtons().find((element) => element.getAttribute('aria-selected') === 'true')
|
|
}
|
|
|
|
export function tiles() {
|
|
return Array.from(document.body.querySelectorAll<HTMLButtonElement>('[data-picker-tile]'))
|
|
}
|
|
|
|
export function tile(filename: string) {
|
|
return tiles().find((element) =>
|
|
element.getAttribute('aria-label')?.startsWith(`${filename},`),
|
|
)!
|
|
}
|
|
|
|
export function skeletonTiles() {
|
|
return document.body.querySelectorAll('[data-testid="picker-skeleton-tile"]')
|
|
}
|
|
|
|
export function findButton(label: string) {
|
|
return Array.from(document.body.querySelectorAll<HTMLButtonElement>('button')).find(
|
|
(button) => textOf(button) === label,
|
|
)
|
|
}
|
|
|
|
export async function click(element: Element | null | undefined) {
|
|
;(element as HTMLElement).click()
|
|
await settle()
|
|
}
|
|
|
|
export async function clickTab(label: string) {
|
|
tabButton(label)!.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }))
|
|
await settle()
|
|
}
|
|
|
|
export async function selectTile(filename: string) {
|
|
await click(tile(filename))
|
|
}
|
|
|
|
export function altField() {
|
|
return document.body.querySelector<HTMLInputElement>(
|
|
'input[placeholder="e.g. North wall of the kitchen before plastering"]',
|
|
)
|
|
}
|
|
|
|
export async function typeInto(input: HTMLInputElement, value: string) {
|
|
input.value = value
|
|
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
await nextTick()
|
|
}
|
|
|
|
export function searchField() {
|
|
return document.body.querySelector<HTMLInputElement>('input[aria-label="Search by filename"]')!
|
|
}
|
|
|
|
export function dialogText() {
|
|
return textOf(document.body.querySelector('[role="dialog"]'))
|
|
}
|
|
|
|
export function modalOpen() {
|
|
return document.body.querySelector('[role="dialog"]') !== null
|
|
}
|
|
|
|
export function press(element: Element, key: string) {
|
|
element.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
|
|
}
|