test: add 35 E2E tests (auth/roles/creator/navigation) and fix temperature validation bug
This commit is contained in:
parent
6234c27138
commit
0bcba03393
|
|
@ -2,3 +2,5 @@ node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
.env
|
.env
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const testData = require('./fixtures/test-data');
|
||||||
|
const { cleanDatabase, seedExistingUser, disconnect } = require('./fixtures/database');
|
||||||
|
|
||||||
|
test.describe('认证流程', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await cleanDatabase();
|
||||||
|
await seedExistingUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await cleanDatabase();
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('注册新用户成功', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
// 点击「我的 XXX」卡片进入 auth 视图(未登录时)
|
||||||
|
await page.locator('[data-action="open-characters"]').first().click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
|
||||||
|
// 切换到注册 tab
|
||||||
|
await page.locator('.auth-tabs [data-tab="register"]').click();
|
||||||
|
// 等待注册表单的 fadeIn 动画完成
|
||||||
|
await expect(page.locator('[data-form="register"]')).toBeVisible();
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
|
||||||
|
// 填写注册表单
|
||||||
|
await page.fill('[data-form="register"] [name="account"]', testData.users.newUser.account);
|
||||||
|
await page.fill('[data-form="register"] [name="password"]', testData.users.newUser.password);
|
||||||
|
await page.fill('[data-form="register"] [name="confirmPassword"]', testData.users.newUser.password);
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
await page.locator('[data-form="register"] button[type="submit"]').click();
|
||||||
|
|
||||||
|
// 注册成功后自动成为创作者,应跳转到创作者中心
|
||||||
|
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('重复注册同一账号失败', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-characters"]').first().click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
await page.locator('.auth-tabs [data-tab="register"]').click();
|
||||||
|
// 等待注册表单的 fadeIn 动画完成
|
||||||
|
await expect(page.locator('[data-form="register"]')).toBeVisible();
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
|
||||||
|
await page.fill('[data-form="register"] [name="account"]', testData.users.existing.account);
|
||||||
|
await page.fill('[data-form="register"] [name="password"]', testData.users.existing.password);
|
||||||
|
await page.fill('[data-form="register"] [name="confirmPassword"]', testData.users.existing.password);
|
||||||
|
|
||||||
|
// 监听 dialog(错误信息通过 alert 弹出)
|
||||||
|
const dialogPromise = page.waitForEvent('dialog');
|
||||||
|
await page.locator('[data-form="register"] button[type="submit"]').click();
|
||||||
|
const dialog = await dialogPromise;
|
||||||
|
expect(dialog.message()).toContain('已注册');
|
||||||
|
await dialog.accept();
|
||||||
|
|
||||||
|
// 应仍停留在 auth 视图
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('登录已有用户成功', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-characters"]').first().click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
|
||||||
|
// 默认在登录 tab
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 登录成功后(existing 是创作者)应跳转到创作者中心
|
||||||
|
await expect(page.locator('#creator-center')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('错误密码登录失败', async ({ 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"]', 'wrongpassword');
|
||||||
|
|
||||||
|
const dialogPromise = page.waitForEvent('dialog');
|
||||||
|
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||||
|
const dialog = await dialogPromise;
|
||||||
|
expect(dialog.message()).toContain('账号或密码错误');
|
||||||
|
await dialog.accept();
|
||||||
|
|
||||||
|
// 应仍停留在 auth 视图
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('注册时密码不一致提示', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-characters"]').first().click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
await page.locator('.auth-tabs [data-tab="register"]').click();
|
||||||
|
// 等待注册表单的 fadeIn 动画完成
|
||||||
|
await expect(page.locator('[data-form="register"]')).toBeVisible();
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
|
||||||
|
await page.fill('[data-form="register"] [name="account"]', 'mismatch_user');
|
||||||
|
await page.fill('[data-form="register"] [name="password"]', 'password1');
|
||||||
|
await page.fill('[data-form="register"] [name="confirmPassword"]', 'password2');
|
||||||
|
|
||||||
|
// 密码不一致通过原生 setCustomValidity + reportValidity 提示(非 alert)
|
||||||
|
await page.locator('[data-form="register"] button[type="submit"]').click();
|
||||||
|
|
||||||
|
// 验证 confirmPassword 字段的 validationMessage 包含「不一致」
|
||||||
|
const validationMessage = await page
|
||||||
|
.locator('[data-form="register"] [name="confirmPassword"]')
|
||||||
|
.evaluate((el) => el.validationMessage);
|
||||||
|
expect(validationMessage).toContain('不一致');
|
||||||
|
|
||||||
|
// 应仍停留在 auth 视图
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('登出后回到首页', async ({ 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 });
|
||||||
|
|
||||||
|
// 切换到设置 tab(登出按钮在设置面板)
|
||||||
|
await page.locator('[data-center-tab="settings"]').click();
|
||||||
|
await expect(page.locator('#center-settings')).toBeVisible();
|
||||||
|
|
||||||
|
// 点击登出
|
||||||
|
await page.locator('[data-action="logout"]').click();
|
||||||
|
|
||||||
|
// 应回到首页
|
||||||
|
await expect(page.locator('#landing')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('登录态持久化(刷新页面后仍登录)', async ({ 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 });
|
||||||
|
|
||||||
|
// 刷新页面
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
// 首页应可见,且卡片按钮文案应变化(不再是「登录 / 注册」)
|
||||||
|
await expect(page.locator('#landing')).toBeVisible();
|
||||||
|
await expect(page.locator('#characters-btn')).not.toHaveText(/登录/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
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('导航测试');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// 数据库清理 helper
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function cleanDatabase() {
|
||||||
|
await prisma.order.deleteMany();
|
||||||
|
await prisma.role.deleteMany();
|
||||||
|
await prisma.user.deleteMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedExistingUser() {
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const testData = require('./test-data');
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
account: testData.users.existing.account,
|
||||||
|
password: bcrypt.hashSync(testData.users.existing.password, 10),
|
||||||
|
isCreator: true,
|
||||||
|
creatorName: testData.users.existing.account,
|
||||||
|
libraryName: 'E2E测试库',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnect() {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { cleanDatabase, seedExistingUser, disconnect, prisma };
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 测试数据 fixtures
|
||||||
|
const testData = {
|
||||||
|
users: {
|
||||||
|
newUser: {
|
||||||
|
account: `e2e_${Date.now()}`,
|
||||||
|
password: 'Test123456',
|
||||||
|
},
|
||||||
|
existing: {
|
||||||
|
account: 'e2e_existing',
|
||||||
|
password: 'Test123456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
displayName: 'E2E测试角色',
|
||||||
|
gender: 'female',
|
||||||
|
age: '25',
|
||||||
|
relationship: '女友',
|
||||||
|
personality: '温柔,体贴,善解人意',
|
||||||
|
background: '来自江南水乡的女孩,喜欢读书和绘画',
|
||||||
|
speechStyle: '轻声细语,偶尔带点小俏皮',
|
||||||
|
likes: '读书,绘画,猫咪',
|
||||||
|
dislikes: '吵闹,谎言',
|
||||||
|
memories: '一起看过的樱花,第一次约会',
|
||||||
|
secrets: '其实很怕打雷',
|
||||||
|
greeting: '你回来啦~今天过得怎么样?',
|
||||||
|
model: 'gpt-4o',
|
||||||
|
temperature: '0.8',
|
||||||
|
maxTokens: '2048',
|
||||||
|
price: '29.9',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = testData;
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { cleanDatabase, disconnect, prisma } = require('./fixtures/database');
|
||||||
|
|
||||||
|
test.describe('导航与可访问性', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await cleanDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await cleanDatabase();
|
||||||
|
await disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('首页显示两张卡片', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('#landing')).toBeVisible();
|
||||||
|
// 两张卡片:open-characters 和 open-distill
|
||||||
|
const cards = page.locator('[data-action="open-characters"], [data-action="open-distill"]');
|
||||||
|
await expect(cards).toHaveCount(2, { timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('底部 tabBar 三个 tab 可见', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('.tab-bar')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-tab-action="tab-home"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-tab-action="tab-distill"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-tab-action="tab-mine"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tabBar 切换视图', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// 点击蒸馏前任 tab
|
||||||
|
await page.locator('[data-tab-action="tab-distill"]').click();
|
||||||
|
await expect(page.locator('#distill')).toBeVisible();
|
||||||
|
|
||||||
|
// 点击首页 tab
|
||||||
|
await page.locator('[data-tab-action="tab-home"]').click();
|
||||||
|
await expect(page.locator('#landing')).toBeVisible();
|
||||||
|
|
||||||
|
// 点击我的 tab(未登录应跳转到 auth)
|
||||||
|
await page.locator('[data-tab-action="tab-mine"]').click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('首页卡片点击进入对应视图', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// 点击蒸馏前任卡片
|
||||||
|
await page.locator('[data-action="open-distill"]').first().click();
|
||||||
|
await expect(page.locator('#distill')).toBeVisible();
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
await page.locator('[data-tab-action="tab-home"]').click();
|
||||||
|
await expect(page.locator('#landing')).toBeVisible();
|
||||||
|
|
||||||
|
// 点击「我的 XXX」卡片(未登录进入 auth)
|
||||||
|
await page.locator('[data-action="open-characters"]').first().click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('关于页面可访问', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-about"]').first().click();
|
||||||
|
await expect(page.locator('#about')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('创作者入驻页面可访问', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-onboarding"]').first().click();
|
||||||
|
await expect(page.locator('#onboarding')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FAQ 折叠展开', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-about"]').first().click();
|
||||||
|
await expect(page.locator('#about')).toBeVisible();
|
||||||
|
|
||||||
|
// 点击第一个 FAQ 按钮(.faq-q,非 .faq-item__question)
|
||||||
|
const faqButton = page.locator('.faq-q').first();
|
||||||
|
await faqButton.click();
|
||||||
|
|
||||||
|
// 验证 aria-expanded 变为 true
|
||||||
|
await expect(faqButton).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
|
||||||
|
// 再次点击折叠
|
||||||
|
await faqButton.click();
|
||||||
|
await expect(faqButton).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('返回按钮基于历史记录导航', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// 导航路径: landing → about → 返回
|
||||||
|
await page.locator('[data-action="open-about"]').first().click();
|
||||||
|
await expect(page.locator('#about')).toBeVisible();
|
||||||
|
|
||||||
|
// about 视图使用 data-action="back"
|
||||||
|
await page.locator('#about [data-action="back"]').click();
|
||||||
|
await expect(page.locator('#landing')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('跳过链接(a11y)存在且可用', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
const skipLink = page.locator('.skip-link');
|
||||||
|
// 跳过链接应存在
|
||||||
|
await expect(skipLink).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tabBar 有正确的 ARIA 角色', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
const tabbar = page.locator('.tab-bar');
|
||||||
|
await expect(tabbar).toHaveAttribute('role', 'tablist');
|
||||||
|
|
||||||
|
// tab-bar 内的按钮应有 role="tab"
|
||||||
|
const tabs = page.locator('.tab-bar [data-tab-action]');
|
||||||
|
const count = await tabs.count();
|
||||||
|
expect(count).toBe(3);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await expect(tabs.nth(i)).toHaveAttribute('role', 'tab');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('视图切换时 aria-live 播报', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
// #sr-announce 是 aria-live 区域,role="status" aria-live="polite"
|
||||||
|
const liveRegion = page.locator('#sr-announce');
|
||||||
|
await expect(liveRegion).toHaveAttribute('aria-live', 'polite');
|
||||||
|
await expect(liveRegion).toHaveAttribute('role', 'status');
|
||||||
|
|
||||||
|
// 切换视图后 live region 应有内容
|
||||||
|
await page.locator('[data-action="open-about"]').first().click();
|
||||||
|
await expect(liveRegion).not.toHaveText('');
|
||||||
|
const liveText = await liveRegion.textContent();
|
||||||
|
expect(liveText.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('角色卡片支持键盘操作', async ({ page }) => {
|
||||||
|
// 创建非创作者用户 + 角色
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
account: 'e2e_keyboard_user',
|
||||||
|
password: bcrypt.hashSync('Test123456', 10),
|
||||||
|
isCreator: false,
|
||||||
|
libraryName: '键盘测试库',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.role.create({
|
||||||
|
data: {
|
||||||
|
creatorId: user.id,
|
||||||
|
displayName: '键盘测试角色',
|
||||||
|
gender: 'female',
|
||||||
|
personality: '测试',
|
||||||
|
background: '测试',
|
||||||
|
speechStyle: '测试',
|
||||||
|
greeting: '测试',
|
||||||
|
desc: '键盘测试',
|
||||||
|
price: 9.9,
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 登录非创作者(登录后自动进入角色库)
|
||||||
|
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"]', 'e2e_keyboard_user');
|
||||||
|
await page.fill('[data-form="login"] [name="password"]', 'Test123456');
|
||||||
|
await page.locator('[data-form="login"] button[type="submit"]').click();
|
||||||
|
await expect(page.locator('#role-library')).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
// 聚焦角色卡片(role-card 有 tabindex="0")
|
||||||
|
const roleCard = page.locator('#role-list .role-card').first();
|
||||||
|
await roleCard.focus();
|
||||||
|
|
||||||
|
// 按 Enter 应进入详情
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await expect(page.locator('#role-detail')).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
await prisma.role.deleteMany();
|
||||||
|
await prisma.user.deleteMany();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('表单 label 与 input 关联', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.locator('[data-action="open-characters"]').first().click();
|
||||||
|
await expect(page.locator('#auth')).toBeVisible();
|
||||||
|
|
||||||
|
// 检查登录表单的 input 存在
|
||||||
|
const accountInput = page.locator('[data-form="login"] [name="account"]');
|
||||||
|
await expect(accountInput).toBeVisible();
|
||||||
|
|
||||||
|
// 检查是否有对应的 label(包裹式 label 包含账号相关文案)
|
||||||
|
const labelText = await page.locator('label').filter({ hasText: /账号|用户名|Account/i }).count();
|
||||||
|
expect(labelText).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('focus-visible 样式存在', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
// Tab 到第一个可聚焦元素
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
// 聚焦元素应有可见的焦点样式(:focus-visible 使用 outline)
|
||||||
|
const focused = await page.evaluate(() => {
|
||||||
|
const el = document.activeElement;
|
||||||
|
if (!el) return false;
|
||||||
|
const styles = window.getComputedStyle(el);
|
||||||
|
return styles.outlineStyle !== 'none' || styles.boxShadow !== 'none';
|
||||||
|
});
|
||||||
|
expect(focused).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
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测试库',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建一个测试角色(status=running 才会在角色库展示)
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -509,7 +509,7 @@
|
||||||
</label>
|
</label>
|
||||||
<label class="field field--half">
|
<label class="field field--half">
|
||||||
<span class="field__label">Temperature</span>
|
<span class="field__label">Temperature</span>
|
||||||
<input class="field__input" name="temperature" type="number" step="0.1" min="0" max="2" value="0.85" />
|
<input class="field__input" name="temperature" type="number" step="0.05" min="0" max="2" value="0.85" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field field--half">
|
<label class="field field--half">
|
||||||
<span class="field__label">Max Tokens</span>
|
<span class="field__label">Max Tokens</span>
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,31 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.2",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"prisma": "^5.22.0"
|
"prisma": "^5.22.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.54"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.54.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.54.2.tgz",
|
||||||
|
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.54.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
|
|
@ -813,6 +832,53 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.54.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.54.2.tgz",
|
||||||
|
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.54.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.54.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.54.2.tgz",
|
||||||
|
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "5.22.0",
|
"version": "5.22.0",
|
||||||
"resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz",
|
"resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz",
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -10,7 +10,12 @@
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"keywords": ["ai", "eternal", "agent", "hermes"],
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"eternal",
|
||||||
|
"agent",
|
||||||
|
"hermes"
|
||||||
|
],
|
||||||
"author": "chigulong",
|
"author": "chigulong",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -25,5 +30,8 @@
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"prisma": "^5.22.0"
|
"prisma": "^5.22.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.54"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
const { defineConfig } = require('@playwright/test');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: false,
|
||||||
|
retries: 1,
|
||||||
|
workers: 1,
|
||||||
|
reporter: [['list'], ['html', { open: 'never' }]],
|
||||||
|
timeout: 60000,
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3001',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
actionTimeout: 15000,
|
||||||
|
navigationTimeout: 30000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
channel: 'chrome',
|
||||||
|
browserName: 'chromium',
|
||||||
|
viewport: { width: 1280, height: 1200 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'node server.js',
|
||||||
|
url: 'http://localhost:3001',
|
||||||
|
reuseExistingServer: true,
|
||||||
|
timeout: 15000,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue