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) }) })