d3ro-voice/apps/web/e2e/history.spec.ts
2026-08-29 18:33:45 +09:00

172 lines
7.7 KiB
TypeScript

import { randomUUID } from 'node:crypto'
import { expect, test } from '@playwright/test'
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
const serviceRoleKey = process.env.E2E_SUPABASE_SERVICE_ROLE_KEY
const hasSupabaseFixtureAccess = Boolean(supabaseUrl && anonKey && serviceRoleKey)
test.describe('History Supabase SSOT', () => {
test.skip(!hasSupabaseFixtureAccess, 'Local Supabase fixture credentials are required')
test.describe.configure({ mode: 'serial' })
const runId = randomUUID()
const ownerEmail = `history-owner-${runId}@example.test`
const otherEmail = `history-other-${runId}@example.test`
const password = `History-${runId}-Aa1!`
const entryIds = Array.from({ length: 22 }, () => randomUUID())
const favoriteId = entryIds[1]
const favoriteTitle = `FAVORITE ${runId}`
const searchId = entryIds[2]
const searchTitle = `NEEDLE QUERY ${runId}`
const favoriteRollbackId = entryIds[3]
const deleteRollbackId = entryIds[4]
const oldestId = entryIds[21]
const oldestTitle = `OLDEST PAGE ${runId}`
const foreignTitle = `FOREIGN SECRET ${runId}`
let admin: SupabaseClient
let ownerId = ''
let otherId = ''
test.beforeAll(async () => {
admin = createClient(supabaseUrl!, serviceRoleKey!, {
auth: { autoRefreshToken: false, persistSession: false }
})
const ownerResult = await admin.auth.admin.createUser({
email: ownerEmail,
password,
email_confirm: true
})
if (ownerResult.error || !ownerResult.data.user) throw ownerResult.error ?? new Error('Owner fixture was not created')
ownerId = ownerResult.data.user.id
const otherResult = await admin.auth.admin.createUser({
email: otherEmail,
password,
email_confirm: true
})
if (otherResult.error || !otherResult.data.user) throw otherResult.error ?? new Error('Other fixture was not created')
otherId = otherResult.data.user.id
const now = Date.now()
const rows = entryIds.map((id, index) => ({
id,
user_id: ownerId,
title: id === favoriteId
? favoriteTitle
: id === searchId
? searchTitle
: id === oldestId
? oldestTitle
: `HISTORY ${index} ${runId}`,
original_text: `Owner transcript ${index} ${runId}`,
polished_text: index % 2 === 0 ? `Polished transcript ${index} ${runId}` : null,
duration: 10 + index,
word_count: 4,
is_favorite: id === favoriteId,
revision: 1,
created_at: new Date(now - index * 60_000).toISOString(),
updated_at: new Date(now - index * 60_000).toISOString()
}))
const { error: ownerSeedError } = await admin.from('history').insert(rows)
if (ownerSeedError) throw ownerSeedError
const { error: foreignSeedError } = await admin.from('history').insert({
user_id: otherId,
title: foreignTitle,
original_text: foreignTitle,
duration: 1,
word_count: 2
})
if (foreignSeedError) throw foreignSeedError
})
test.afterAll(async () => {
if (ownerId) await admin.auth.admin.deleteUser(ownerId)
if (otherId) await admin.auth.admin.deleteUser(otherId)
})
test('session, RLS, search, favorite, pagination, detail and mutation rollback work end-to-end', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('user@studio.com').fill(ownerEmail)
await page.getByPlaceholder('••••••••').fill(password)
await page.getByRole('button', { name: '로그인', exact: true }).click()
await page.waitForURL(/\/dashboard/, { timeout: 15_000 })
await page.goto('/history')
await expect(page.getByText('TRANSCRIPTION HISTORY')).toBeVisible()
await expect(page.getByText(foreignTitle)).toHaveCount(0)
await expect(page.getByTestId(`history-card-${oldestId}`)).toHaveCount(0)
await page.getByLabel('전사 기록 검색').fill(foreignTitle)
await expect(page.getByText('조건에 맞는 전사 기록이 없습니다.')).toBeVisible()
await expect(page.getByText(foreignTitle)).toHaveCount(0)
await page.getByLabel('전사 기록 검색').fill('NEEDLE QUERY')
await expect(page.getByText(searchTitle)).toBeVisible()
await expect(page.getByTestId(`history-card-${favoriteId}`)).toHaveCount(0)
await page.getByLabel('전사 기록 검색').fill('')
await page.getByText('FAVORITES', { exact: true }).click()
await expect(page.getByText(favoriteTitle)).toBeVisible()
await expect(page.getByTestId(`history-card-${searchId}`)).toHaveCount(0)
await page.getByText('ALL', { exact: true }).click()
await expect(page.getByTestId(`history-card-${favoriteRollbackId}`)).toBeVisible()
await page.route('**/rest/v1/history*', async (route) => {
if (route.request().method() === 'PATCH') {
await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ message: 'fixture update failure' }) })
} else {
await route.continue()
}
})
const favoriteRollbackCard = page.getByTestId(`history-card-${favoriteRollbackId}`)
await favoriteRollbackCard.getByRole('button', { name: '즐겨찾기', exact: true }).click()
await expect(page.getByText('히스토리를 처리하지 못했습니다. 잠시 후 다시 시도해 주세요.')).toBeVisible()
await expect(favoriteRollbackCard.getByRole('button', { name: '즐겨찾기', exact: true })).toBeVisible()
await page.unroute('**/rest/v1/history*')
await favoriteRollbackCard.getByRole('button', { name: '즐겨찾기', exact: true }).click()
await expect(favoriteRollbackCard.getByRole('button', { name: '즐겨찾기 해제', exact: true })).toBeVisible()
await expect.poll(async () => {
const { data } = await admin.from('history').select('is_favorite, revision').eq('id', favoriteRollbackId).single()
return data
}).toEqual({ is_favorite: true, revision: 2 })
await page.route('**/rest/v1/history*', async (route) => {
if (route.request().method() === 'DELETE') {
await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ message: 'fixture delete failure' }) })
} else {
await route.continue()
}
})
page.once('dialog', (dialog) => dialog.accept())
await page.getByTestId(`history-card-${deleteRollbackId}`).getByRole('button', { name: '전사 기록 삭제' }).click()
await expect(page.getByTestId(`history-card-${deleteRollbackId}`)).toBeVisible()
await page.unroute('**/rest/v1/history*')
const { count: retainedAfterFailure } = await admin.from('history').select('id', { count: 'exact', head: true }).eq('id', deleteRollbackId)
expect(retainedAfterFailure).toBe(1)
await page.getByText('더 불러오기').click()
await expect(page.getByText(oldestTitle)).toBeVisible()
await page.getByText(favoriteTitle).click()
await page.waitForURL(new RegExp(`/history/${favoriteId}$`))
await expect(page.getByText('TRANSCRIPTION DETAIL')).toBeVisible()
await expect(page.getByText(`Owner transcript 1 ${runId}`)).toBeVisible()
await page.getByRole('link', { name: '히스토리로 돌아가기' }).click()
await expect(page.getByText('TRANSCRIPTION HISTORY')).toBeVisible()
page.once('dialog', (dialog) => dialog.accept())
await page.getByTestId(`history-card-${deleteRollbackId}`).getByRole('button', { name: '전사 기록 삭제' }).click()
await expect(page.getByTestId(`history-card-${deleteRollbackId}`)).toHaveCount(0)
await expect.poll(async () => {
const { count } = await admin.from('history').select('id', { count: 'exact', head: true }).eq('id', deleteRollbackId)
return count
}).toBe(0)
})
})