feat: media picker and image removal
This commit is contained in:
1 parent
008b0dfbb9
commit
b3793d5dbb
54 files changed
+3769
-1412
No files matched your search
@@ -0,0 +1,190 @@
|
||||
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) => emit('inserted', item, alt),
|
||||
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 }))
|
||||
}
|
||||
Reference in new issue
Block a user