const { test, expect } = require('@playwright/test'); const bcrypt = require('bcryptjs'); const testData = require('./fixtures/test-data'); const { cleanDatabase, seedExistingUser, disconnect, prisma } = require('./fixtures/database'); // 非创作者用户(用于角色库测试) const NON_CREATOR = { account: 'e2e_noncreator', password: 'Test123456', }; test.describe('角色库与角色详情', () => { let existingUser; let testRole; test.beforeAll(async () => { await cleanDatabase(); await seedExistingUser(); existingUser = await prisma.user.findUnique({ where: { account: testData.users.existing.account }, }); // 创建非创作者用户(open-characters 对非创作者进入 role-library) await prisma.user.create({ data: { account: NON_CREATOR.account, password: bcrypt.hashSync(NON_CREATOR.password, 10), isCreator: false, libraryName: 'E2E测试库', }, }); // 创建一个测试角色(reviewStatus=synced 才会在角色库展示) testRole = await prisma.role.create({ data: { creatorId: existingUser.id, displayName: '测试角色A', gender: 'female', age: '24', relationship: '女友', personality: '温柔可爱,善解人意', background: '来自海边的小镇女孩', speechStyle: '轻柔细腻', greeting: '你好呀~', desc: '温柔可爱的女友角色', price: 29.9, status: 'running', reviewStatus: 'synced', avatar: 'https://example.com/avatar.png', }, }); }); test.afterAll(async () => { await cleanDatabase(); await disconnect(); }); // 以非创作者身份登录,登录后自动进入角色库 async function loginAsNonCreator(page) { await page.goto('/'); await page.locator('[data-action="open-characters"]').first().click(); await expect(page.locator('#auth')).toBeVisible(); // 确保登录 tab 激活 await page.locator('.auth-tabs [data-tab="login"]').click(); await page.locator('[data-form="login"]').waitFor(); await page.fill('[data-form="login"] [name="account"]', NON_CREATOR.account); await page.fill('[data-form="login"] [name="password"]', NON_CREATOR.password); await page.locator('[data-form="login"] button[type="submit"]').click(); await expect(page.locator('#role-library')).toBeVisible({ timeout: 5000 }); } test('角色库显示已上架角色', async ({ page }) => { await loginAsNonCreator(page); // 应显示测试角色 await expect(page.locator('#role-list .role-card')).toHaveCount(1, { timeout: 5000 }); await expect(page.locator('#role-list .role-card__name')).toHaveText('测试角色A'); }); test('空角色库显示空状态', async ({ page }) => { // 清空角色 await prisma.role.deleteMany(); await loginAsNonCreator(page); // 应显示空状态 await expect(page.locator('#library-empty')).toBeVisible({ timeout: 5000 }); }); test('点击角色卡片进入详情页', async ({ page }) => { // 重新创建角色 testRole = await prisma.role.create({ data: { creatorId: existingUser.id, displayName: '详情测试角色', gender: 'male', personality: '阳光开朗', background: '运动少年', speechStyle: '热情直接', greeting: '嘿!你好!', desc: '阳光开朗的男孩', price: 19.9, status: 'running', reviewStatus: 'synced', }, }); await loginAsNonCreator(page); // 点击角色卡片 await page.locator('#role-list .role-card').first().click(); await expect(page.locator('#role-detail')).toBeVisible(); // 验证详情内容 await expect(page.locator('#detail-name')).toHaveText('详情测试角色'); await expect(page.locator('#detail-role-name')).toHaveText('详情测试角色'); await expect(page.locator('#detail-price')).toContainText('19.9'); }); test('角色详情页返回按钮', async ({ page }) => { await loginAsNonCreator(page); await page.locator('#role-list .role-card').first().click(); await expect(page.locator('#role-detail')).toBeVisible(); // 点击返回(role-detail 使用 back-to-library) await page.locator('[data-action="back-to-library"]').click(); await expect(page.locator('#role-library')).toBeVisible(); }); test('角色详情付款流程', async ({ page }) => { await loginAsNonCreator(page); await page.locator('#role-list .role-card').first().click(); await expect(page.locator('#role-detail')).toBeVisible(); // 付款前:pre-pay 按钮区可见,paid 区隐藏 await expect(page.locator('#detail-actions-pre')).toBeVisible(); await expect(page.locator('#detail-paid')).toBeHidden(); // 点击付款按钮(data-action="pay",非 pay-role) await page.locator('[data-action="pay"]').click(); // 应显示已付款区域(含二维码 / 头像下载) await expect(page.locator('#detail-paid')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#detail-actions-pre')).toBeHidden(); await expect(page.locator('[data-action="download-avatar"]')).toBeVisible(); }); });