241 lines
11 KiB
JavaScript
241 lines
11 KiB
JavaScript
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('导航测试');
|
||
});
|
||
});
|