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: ``, }) 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) { ;(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('[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('[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('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( '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('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 })) }