v1.12.21 관리자 미디어 조작 개선 및 CSP 관찰 안내 정리
This commit is contained in:
@@ -10,6 +10,7 @@ const adminPassword = 'playwright-admin-password'
|
||||
*/
|
||||
const loginAdmin = async (page) => {
|
||||
await page.goto('/admin/login')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.getByLabel('이메일').fill(adminEmail)
|
||||
await page.locator('input[type="password"]').fill(adminPassword)
|
||||
|
||||
@@ -17,7 +18,7 @@ const loginAdmin = async (page) => {
|
||||
page.waitForResponse((response) =>
|
||||
response.url().endsWith('/admin/api/auth/login') && response.status() === 200
|
||||
),
|
||||
page.getByRole('button', { name: '로그인' }).click()
|
||||
page.getByRole('button', { name: '로그인', exact: true }).click()
|
||||
])
|
||||
await page.waitForURL(/\/admin(?:\/)?$/)
|
||||
}
|
||||
|
||||
197
tests/playwright/e2e/admin-media-interactions.spec.js
Normal file
197
tests/playwright/e2e/admin-media-interactions.spec.js
Normal file
@@ -0,0 +1,197 @@
|
||||
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개 선택됨')
|
||||
})
|
||||
@@ -5,6 +5,7 @@ test('관리자가 로그인해 게시물을 발행하고 공개 화면에서
|
||||
const slug = 'playwright-e2e-post'
|
||||
|
||||
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')
|
||||
|
||||
@@ -12,7 +13,7 @@ test('관리자가 로그인해 게시물을 발행하고 공개 화면에서
|
||||
page.waitForResponse((response) =>
|
||||
response.url().endsWith('/admin/api/auth/login') && response.status() === 200
|
||||
),
|
||||
page.getByRole('button', { name: '로그인' }).click()
|
||||
page.getByRole('button', { name: '로그인', exact: true }).click()
|
||||
])
|
||||
await page.waitForURL(/\/admin(?:\/)?$/)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const adminPassword = 'playwright-admin-password'
|
||||
*/
|
||||
const loginAdmin = async (page) => {
|
||||
await page.goto('/admin/login')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.getByLabel('이메일').fill(adminEmail)
|
||||
await page.locator('input[type="password"]').fill(adminPassword)
|
||||
|
||||
@@ -17,7 +18,7 @@ const loginAdmin = async (page) => {
|
||||
page.waitForResponse((response) =>
|
||||
response.url().endsWith('/admin/api/auth/login') && response.status() === 200
|
||||
),
|
||||
page.getByRole('button', { name: '로그인' }).click()
|
||||
page.getByRole('button', { name: '로그인', exact: true }).click()
|
||||
])
|
||||
await page.waitForURL(/\/admin(?:\/)?$/)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user