Files
memind/user-feedback.test.mjs
john 18ea4f82fd feat(feedback): 新增用户反馈提交、分页列表与语音描述
支持 Bug/需求提交、截图与聊天同款语音输入,全站反馈分页浏览与详情页,并从聊天与空间页提供入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 20:49:15 +08:00

163 lines
4.6 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createFeedbackService } from './user-feedback.mjs';
function createMockPool() {
const inserts = [];
return {
inserts,
async query(sql, params) {
if (/INSERT INTO h5_user_feedback/.test(sql)) {
inserts.push(params);
return [{ affectedRows: 1 }];
}
throw new Error(`Unexpected query: ${sql}`);
},
};
}
test('submit feedback stores normalized payload', async () => {
const pool = createMockPool();
const service = createFeedbackService(pool);
const tinyPng = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
'base64',
);
const result = await service.submit('user-1', {
type: 'bug',
title: '页面无法保存',
description: '点击保存后没有反应',
contact: 'test@example.com',
images: [
{
filename: 'screen.png',
mimeType: 'image/png',
dataBase64: tinyPng.toString('base64'),
},
],
context: {
pagePath: '/space/page/abc',
userAgent: 'Mozilla/5.0',
},
});
assert.ok(result.id);
assert.equal(result.type, 'bug');
assert.equal(result.status, 'pending');
assert.equal(pool.inserts.length, 1);
assert.equal(pool.inserts[0][1], 'user-1');
assert.equal(pool.inserts[0][2], 'bug');
});
test('submit feedback rejects invalid type', async () => {
const service = createFeedbackService(createMockPool());
await assert.rejects(
() =>
service.submit('user-1', {
type: 'invalid',
title: 'x',
description: 'y',
}),
/反馈类型/,
);
});
test('submit feedback rejects empty description', async () => {
const service = createFeedbackService(createMockPool());
await assert.rejects(
() =>
service.submit('user-1', {
type: 'feature',
title: '想要导出 PDF',
description: ' ',
}),
/详细描述/,
);
});
test('listAll returns paginated board items without contact or images', async () => {
let queryCount = 0;
const pool = {
async query(sql, params) {
if (/SELECT COUNT\(\*\)/.test(sql)) {
return [[{ total: 23 }]];
}
if (/FROM h5_user_feedback f/.test(sql) && /LIMIT \? OFFSET \?/.test(sql)) {
queryCount += 1;
assert.equal(params[0], 10);
assert.equal(params[1], 10);
return [
[
{
id: 'fb-1',
user_id: 'user-2',
type: 'bug',
title: '按钮无响应',
description: '点击保存没有反应',
status: 'pending',
images_json: JSON.stringify([{ mimeType: 'image/png', dataBase64: 'abc' }]),
context_json: JSON.stringify({ pagePath: '/space' }),
created_at: 1000,
updated_at: 1000,
submitter_display_name: 'Alice',
},
],
];
}
throw new Error(`Unexpected query: ${sql}`);
},
};
const service = createFeedbackService(pool);
const result = await service.listAll({ page: 2, limit: 10 });
assert.equal(queryCount, 1);
assert.equal(result.page, 2);
assert.equal(result.pageSize, 10);
assert.equal(result.total, 23);
assert.equal(result.totalPages, 3);
assert.equal(result.items.length, 1);
assert.equal(result.items[0].submitterDisplayName, 'Alice');
assert.equal(result.items[0].imageCount, 1);
assert.equal(result.items[0].images, undefined);
assert.equal(result.items[0].contact, undefined);
});
test('getById hides contact from non-owner', async () => {
const pool = {
async query(sql, params) {
if (/WHERE f.id = \?/.test(sql)) {
assert.equal(params[0], 'fb-1');
return [
[
{
id: 'fb-1',
user_id: 'user-2',
type: 'bug',
title: '按钮无响应',
description: '详细内容',
contact: 'secret@example.com',
status: 'pending',
images_json: '[]',
context_json: '{}',
created_at: 1000,
updated_at: 1000,
submitter_display_name: 'Alice',
},
],
];
}
throw new Error(`Unexpected query: ${sql}`);
},
};
const service = createFeedbackService(pool);
const item = await service.getById('fb-1', 'viewer-1');
assert.equal(item.title, '按钮无响应');
assert.equal(item.contact, undefined);
const ownItem = await service.getById('fb-1', 'user-2');
assert.equal(ownItem.contact, 'secret@example.com');
});