Files
RenewCMS/web/src/components/__tests__/pickerKit.ts
T

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 }))
}