Files
sori.studio/tests/playwright/e2e/admin-media-interactions.spec.js

198 lines
10 KiB
JavaScript

import { expect, test } from '@playwright/test'
/**
* 격리된 테스트 관리자로 로그인하고 지연·실패를 제어할 미디어 응답을 준비한다.
* @param {import('@playwright/test').Page} page - 테스트 페이지
* @param {number} count - 테스트 미디어 개수
* @returns {Promise<Object>} 요청과 응답 제어 상태
*/
const openMediaLibrary = async (page, count = 3) => {
await page.goto('/admin/login')
await page.waitForLoadState('networkidle')
await page.getByLabel('이메일').fill('playwright-admin@example.com')
await page.locator('input[type="password"]').fill('playwright-admin-password')
await page.getByRole('button', { name: '로그인', exact: true }).click()
await page.waitForURL(/\/admin\/?$/)
await page.waitForLoadState('networkidle')
const state = {
folders: ['미분류', '보관', '빈 폴더'],
items: Array.from({ length: count }, (_, index) => ({
url: `/uploads/media-interaction-${index}.webp`,
name: `사진-${index + 1}.webp`,
title: `사진 ${index + 1}`,
category: '미분류',
kind: 'image',
usage: index === 0 ? [{ title: '사용 중인 게시물' }] : []
})),
moves: [],
moveGate: null,
folderGate: null,
failMove: false,
failList: false
}
await page.route('**/uploads/media-interaction-*.webp', (route) => route.fulfill({
contentType: 'image/svg+xml',
body: '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><rect width="160" height="160" fill="#dbeafe"/><circle cx="80" cy="80" r="40" fill="#0284c7"/></svg>'
}))
await page.route('**/admin/api/media-folders', async (route) => {
if (route.request().method() === 'POST') {
const { path } = route.request().postDataJSON()
state.folders.push(path)
await route.fulfill({ json: { path } })
return
}
await route.fulfill({ json: state.folders })
})
await page.route(/\/admin\/api\/media(?:\?|$)/, async (route) => {
if (route.request().method() === 'PUT') {
const body = route.request().postDataJSON()
state.moves.push(body)
await state.moveGate?.promise
if (state.failMove) {
await route.fulfill({ status: 500, json: { message: '이동 실패 테스트' } })
return
}
state.items.forEach((item) => {
if (body.urls.includes(item.url)) item.category = body.category
})
await route.fulfill({ json: [] })
return
}
const query = new URL(route.request().url()).searchParams
const folder = query.get('folder') || ''
const pageNumber = Number(query.get('page') || 1)
const pageSize = Number(query.get('pageSize') || 50)
if (folder === '보관') await state.folderGate?.promise
if (state.failList) {
await route.fulfill({ status: 500, json: { message: '목록 실패 테스트' } })
return
}
const items = state.items.filter((item) => !folder || item.category === folder)
const folders = Object.fromEntries(state.folders.map((path) => [path, state.items.filter((item) => item.category === path).length]))
await route.fulfill({
json: {
items: items.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
pagination: { page: pageNumber, pageSize, total: items.length, pageCount: Math.max(1, Math.ceil(items.length / pageSize)) },
counts: { tabs: { library: state.items.length }, folders, kinds: { all: items.length, image: items.length }, usage: { all: items.length, unused: items.filter((item) => !item.usage.length).length } }
}
})
})
await page.getByRole('link', { name: '미디어', exact: true }).click()
await expect(page.locator('.admin-media__item')).toHaveCount(Math.min(count, 50))
return state
}
/**
* 실제 마우스로 썸네일 드래그를 시작하고 대상 폴더 위에서 멈춘다.
* @param {import('@playwright/test').Page} page - 테스트 페이지
* @param {string} folder - 대상 폴더
* @returns {Promise<void>}
*/
const dragMediaOverFolder = async (page, folder) => {
const source = await page.locator('.admin-media__image').first().boundingBox()
const target = await page.getByTitle(folder, { exact: true }).boundingBox()
const sourceX = source.x + source.width - 8
const sourceY = source.y + source.height - 8
await page.mouse.move(sourceX, sourceY)
await page.mouse.down()
await page.mouse.move(sourceX - 15, sourceY, { steps: 5 })
await page.mouse.move(target.x + target.width / 2, target.y + target.height / 2, { steps: 10 })
await page.mouse.move(target.x + target.width / 2 + 1, target.y + target.height / 2)
}
test('다중 선택·폴더 생성·드롭 강조·일괄 이동 상태를 표시한다', async ({ page }, testInfo) => {
const state = await openMediaLibrary(page)
await page.getByRole('button', { name: '전체 선택', exact: true }).click()
await expect(page.locator('.admin-media__selection-count')).toHaveText('3개 선택됨')
await expect(page.locator('.admin-media__item--selected')).toHaveCount(3)
await page.getByRole('button', { name: '폴더 추가', exact: true }).click()
await page.getByLabel('폴더 이름').fill('새 정리 폴더')
await page.getByRole('button', { name: '만들기', exact: true }).click()
await expect(page.getByTitle('새 정리 폴더', { exact: true })).toBeVisible()
await expect(page.locator('.admin-media__selection-count')).toHaveText('3개 선택됨')
state.moveGate = Promise.withResolvers()
await dragMediaOverFolder(page, '새 정리 폴더')
await expect(page.locator('.admin-media__folder-row--drop-target')).toContainText('놓으면 3개 이동')
await expect(page.locator('.admin-media__drag-preview')).toHaveText('미디어 3개 이동')
await expect(page.locator('.admin-media__item--dragging')).toHaveCount(3)
await page.screenshot({ path: testInfo.outputPath('media-drag.png'), fullPage: true, animations: 'disabled' })
await page.mouse.up()
await expect(page.locator('.admin-media__move-status')).toContainText('3개 미디어를 「새 정리 폴더」 폴더로 이동하는 중')
await expect(page.locator('.admin-media__select-all')).toBeDisabled()
expect(state.moves[0]).toEqual({ urls: state.items.map((item) => item.url), category: '새 정리 폴더' })
state.moveGate.resolve()
await expect(page.getByText('3개 미디어를 「새 정리 폴더」 폴더로 이동했습니다.', { exact: true })).toBeVisible()
await expect(page.locator('.admin-media__move-status')).toHaveCount(0)
await expect(page.locator('.admin-media__selection-count')).toHaveCount(0)
await page.getByTitle('새 정리 폴더', { exact: true }).click()
await expect(page.locator('.admin-media__item')).toHaveCount(3)
})
test('드래그 취소·외부 드롭을 무시하고 이동 실패 시 선택을 유지한다', async ({ page }) => {
const state = await openMediaLibrary(page)
await page.getByRole('button', { name: '전체 선택', exact: true }).click()
await dragMediaOverFolder(page, '보관')
await page.keyboard.press('Escape')
await page.mouse.up()
await expect(page.locator('.admin-media__folder-row--drop-target')).toHaveCount(0)
await expect(page.locator('.admin-media__drag-preview')).toHaveCount(0)
await expect(page.locator('.admin-media__item--dragging')).toHaveCount(0)
await page.getByTitle('보관', { exact: true }).dispatchEvent('drop')
expect(state.moves).toHaveLength(0)
state.failMove = true
await dragMediaOverFolder(page, '보관')
await page.mouse.up()
await expect(page.getByText('이동 실패 테스트', { exact: true })).toBeVisible()
await expect(page.locator('.admin-media__selection-count')).toHaveText('3개 선택됨')
await expect(page.locator('.admin-media__move-status')).toHaveCount(0)
await expect(page.locator('.admin-media__select-all')).toBeEnabled()
})
test('폴더 전환은 이전 목록을 숨기고 마지막 응답·오류·재시도를 구분한다', async ({ page }, testInfo) => {
const state = await openMediaLibrary(page)
state.folderGate = Promise.withResolvers()
await page.getByTitle('보관', { exact: true }).click()
await expect(page.locator('.admin-media__loading')).toContainText('보관 불러오는 중')
await expect(page.locator('.admin-media__item')).toHaveCount(0)
await expect(page.locator('.admin-media__empty')).toHaveCount(0)
await expect(page.locator('.admin-media__select-all')).toBeDisabled()
await expect(page.getByTitle('미분류', { exact: true })).toBeVisible()
await expect(page.locator('.admin-media__folder-row').filter({ has: page.getByTitle('보관', { exact: true }) })).toHaveCSS('background-color', 'rgb(21, 23, 26)')
await page.screenshot({ path: testInfo.outputPath('media-loading.png'), fullPage: true, animations: 'disabled' })
await page.getByTitle('미분류', { exact: true }).click()
await expect(page.locator('.admin-media__item')).toHaveCount(3)
state.folderGate.resolve()
await expect(page.locator('.admin-media__folder-title')).toHaveText('미분류')
await expect(page.locator('.admin-media__item')).toHaveCount(3)
state.failList = true
await page.getByTitle('빈 폴더', { exact: true }).click()
await expect(page.locator('.admin-media__load-error')).toBeVisible()
await expect(page.locator('.admin-media__empty')).toHaveCount(0)
state.failList = false
await page.getByRole('button', { name: '다시 시도', exact: true }).click()
await expect(page.locator('.admin-media__empty')).toBeVisible()
await expect(page.locator('.admin-media__load-error')).toHaveCount(0)
})
test('전체 선택은 현재 페이지에만 적용하고 사용 중인 파일도 포함한다', async ({ page }) => {
await openMediaLibrary(page, 55)
await page.getByRole('button', { name: '전체 선택', exact: true }).click()
await expect(page.locator('.admin-media__selection-count')).toHaveText('50개 선택됨')
await expect(page.locator('.admin-media__select-toggle[aria-pressed="true"]')).toHaveCount(50)
await page.getByRole('button', { name: '전체 선택 해제', exact: true }).click()
await expect(page.locator('.admin-media__selection-count')).toHaveCount(0)
await page.getByRole('button', { name: '다음 페이지', exact: true }).click()
await expect(page.locator('.admin-media__item')).toHaveCount(5)
await page.getByRole('button', { name: '전체 선택', exact: true }).click()
await expect(page.locator('.admin-media__selection-count')).toHaveText('5개 선택됨')
})