const { test, expect } = require('@playwright/test'); const testData = require('./fixtures/test-data'); const { cleanDatabase, seedExistingUser, disconnect, prisma } = require('./fixtures/database'); test.describe('创作者中心与角色发布/编辑', () => { let existingUser; let existingRole; test.beforeAll(async () => { await cleanDatabase(); await seedExistingUser(); existingUser = await prisma.user.findUnique({ where: { account: testData.users.existing.account }, }); existingRole = await prisma.role.create({ data: { creatorId: existingUser.id, displayName: '已有角色', gender: 'female', personality: '温柔体贴', background: '来自南方', speechStyle: '轻声细语', greeting: '你好~', desc: '温柔体贴的角色', price: 19.9, status: 'running', temperature: 0.8, maxTokens: 2048, enableMemory: true, enableTools: false, model: 'gpt-4o', }, }); }); test.afterAll(async () => { await cleanDatabase(); await disconnect(); }); async function loginAsExisting(page) { await page.goto('/'); await page.locator('[data-action="open-characters"]').first().click(); await expect(page.locator('#auth')).toBeVisible(); await page.fill('[data-form="login"] [name="account"]', testData.users.existing.account); await page.fill('[data-form="login"] [name="password"]', testData.users.existing.password); await page.locator('[data-form="login"] button[type="submit"]').click(); await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 }); } test('创作者中心显示三个 tab', async ({ page }) => { await loginAsExisting(page); await expect(page.locator('[data-center-tab="roles"]')).toBeVisible(); await expect(page.locator('[data-center-tab="income"]')).toBeVisible(); await expect(page.locator('[data-center-tab="settings"]')).toBeVisible(); }); test('角色 tab 显示已有角色', async ({ page }) => { await loginAsExisting(page); // 默认在 roles tab await expect(page.locator('#creator-role-list .role-card')).toHaveCount(1, { timeout: 5000 }); await expect(page.locator('#creator-role-list .role-card__name')).toHaveText('已有角色'); }); test('收入 tab 显示余额和流水', async ({ page }) => { await loginAsExisting(page); await page.locator('[data-center-tab="income"]').click(); // income 面板可见(#center-income 带 active class) await expect(page.locator('#center-income')).toBeVisible(); // 余额元素可见 await expect(page.locator('#income-balance')).toBeVisible({ timeout: 5000 }); // 流水列表容器存在 await expect(page.locator('#income-list')).toBeVisible(); }); test('设置 tab 可保存昵称和库名', async ({ page }) => { await loginAsExisting(page); await page.locator('[data-center-tab="settings"]').click(); await expect(page.locator('#center-settings')).toBeVisible(); // 修改设置 await page.fill('#settings-form [name="creatorName"]', '新昵称'); await page.fill('#settings-form [name="libraryName"]', '新库名'); // 保存成功会弹 alert「设置已保存」 const dialogPromise = page.waitForEvent('dialog'); await page.locator('#settings-form button[type="submit"]').click(); const dialog = await dialogPromise; expect(dialog.message()).toContain('保存'); await dialog.accept(); }); test('新建角色完整流程(4步表单)', async ({ page }) => { await loginAsExisting(page); // 点击新建角色 await page.locator('[data-action="new-role"]').click(); await expect(page.locator('#creator')).toBeVisible(); // Step 1: 基础身份(agentId 必填,pattern [a-zA-Z0-9_]+;displayName 必填) await page.fill('#character-form [name="agentId"]', 'e2e_test_role'); await page.fill('#character-form [name="displayName"]', testData.role.displayName); await page.selectOption('#character-form [name="gender"]', testData.role.gender); await page.fill('#character-form [name="age"]', testData.role.age); await page.locator('.form-step.active [data-action="next"]').click(); // Step 2: 灵魂设定(background / personality / speechStyle 必填) await page.fill('#character-form [name="background"]', testData.role.background); await page.fill('#character-form [name="personality"]', testData.role.personality); await page.fill('#character-form [name="speechStyle"]', testData.role.speechStyle); await page.fill('#character-form [name="likes"]', testData.role.likes); await page.fill('#character-form [name="dislikes"]', testData.role.dislikes); await page.locator('.form-step.active [data-action="next"]').click(); // Step 3: 关系与记忆(greeting 必填) await page.fill('#character-form [name="relationship"]', testData.role.relationship); await page.fill('#character-form [name="memories"]', testData.role.memories); await page.fill('#character-form [name="secrets"]', testData.role.secrets); await page.fill('#character-form [name="greeting"]', testData.role.greeting); await page.locator('.form-step.active [data-action="next"]').click(); // Step 4: 运行配置(model/temperature/maxTokens 有默认值;无 price 字段) // 捕获可能的错误弹窗 let dialogMessage = ''; page.on('dialog', async (dialog) => { dialogMessage = dialog.message(); await dialog.accept(); }); await page.locator('.form-step.active [data-action="publish"]').click(); // 应显示生成结果面板 await expect(page.locator('#result-panel')).toBeVisible({ timeout: 15000 }); // 验证 Soul.md 内容(单个 #preview-code 元素,默认显示 soul) await expect(page.locator('#preview-code')).toBeVisible(); const soulContent = await page.locator('#preview-code').textContent(); expect(soulContent).toContain(testData.role.displayName); // generateSoulMd 会把 personality 按 [,,] 拆分成 " | " 连接的标签 const personalityTag = testData.role.personality.split(/[,,]/)[0].trim(); expect(soulContent).toContain(personalityTag); // 切换到 config.yaml 预览(preview-tabs 内的 [data-tab="config"]) await page.locator('.preview-tabs [data-tab="config"]').click(); const configContent = await page.locator('#preview-code').textContent(); expect(configContent).toContain('model:'); expect(configContent).toContain('temperature:'); }); test('新建角色后数据库有记录', async ({ page }) => { await loginAsExisting(page); await page.locator('[data-action="new-role"]').click(); await expect(page.locator('#creator')).toBeVisible(); // Step 1: 填写最小必填字段 await page.fill('#character-form [name="agentId"]', 'e2e_db_role'); await page.fill('#character-form [name="displayName"]', 'DB验证角色'); await page.selectOption('#character-form [name="gender"]', 'female'); await page.locator('.form-step.active [data-action="next"]').click(); // Step 2 await page.fill('#character-form [name="personality"]', '测试性格'); await page.fill('#character-form [name="background"]', '测试背景'); await page.fill('#character-form [name="speechStyle"]', '测试风格'); await page.locator('.form-step.active [data-action="next"]').click(); // Step 3 await page.fill('#character-form [name="greeting"]', '测试问候'); await page.locator('.form-step.active [data-action="next"]').click(); // Step 4: 直接发布(无 price 字段,使用默认值) let dialogMessage = ''; page.on('dialog', async (dialog) => { dialogMessage = dialog.message(); await dialog.accept(); }); await page.locator('.form-step.active [data-action="publish"]').click(); await expect(page.locator('#result-panel')).toBeVisible({ timeout: 15000 }); // 验证数据库 const dbRole = await prisma.role.findFirst({ where: { displayName: 'DB验证角色' }, }); expect(dbRole).not.toBeNull(); expect(dbRole.personality).toBe('测试性格'); expect(dbRole.greeting).toBe('测试问候'); expect(dbRole.soulMd).not.toBeNull(); expect(dbRole.configYaml).not.toBeNull(); }); test('编辑角色加载已有数据', async ({ page }) => { await loginAsExisting(page); // 等待角色列表加载,找到已 seeded 的角色(其他测试可能已创建额外角色) await expect(page.locator('#creator-role-list .role-card')).not.toHaveCount(0, { timeout: 5000 }); // 定位到"已有角色"那张卡片的编辑按钮 const seededCard = page.locator('#creator-role-list .role-card', { hasText: '已有角色' }).first(); await expect(seededCard).toBeVisible({ timeout: 5000 }); await seededCard.locator('[data-action="edit-role"]').click(); await expect(page.locator('#creator')).toBeVisible(); // 验证表单已预填(loadRoleForEdit 会拉取 /roles/:id/full 填充表单) await expect(page.locator('#character-form [name="displayName"]')).toHaveValue('已有角色', { timeout: 5000 }); await expect(page.locator('#character-form [name="personality"]')).toHaveValue('温柔体贴'); }); test('表单验证 - 必填字段为空时阻止提交', async ({ page }) => { await loginAsExisting(page); await page.locator('[data-action="new-role"]').click(); await expect(page.locator('#creator')).toBeVisible(); // 不填任何字段直接点下一步 await page.locator('.form-step.active [data-action="next"]').click(); // 应仍停留在 step 0(agentId / displayName 必填,验证失败) // step 0 仍 active => agentId 字段仍可见 await expect(page.locator('#character-form [name="agentId"]')).toBeVisible(); await expect(page.locator('#character-form [name="displayName"]')).toBeVisible(); }); test('步骤导航 - 上一步按钮', async ({ page }) => { await loginAsExisting(page); await page.locator('[data-action="new-role"]').click(); await expect(page.locator('#creator')).toBeVisible(); // 填写 step 1 并前进 await page.fill('#character-form [name="agentId"]', 'e2e_nav_test'); await page.fill('#character-form [name="displayName"]', '导航测试'); await page.selectOption('#character-form [name="gender"]', 'female'); await page.locator('.form-step.active [data-action="next"]').click(); // 进入 step 2 后点上一步(data-action="prev",非 prev-step) await expect(page.locator('#character-form [name="background"]')).toBeVisible(); await page.locator('.form-step.active [data-action="prev"]').click(); // 应回到 step 1,且数据保留 await expect(page.locator('#character-form [name="displayName"]')).toBeVisible(); await expect(page.locator('#character-form [name="displayName"]')).toHaveValue('导航测试'); }); });