feat: 分类接口替换为api 接口
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test, beforeEach } from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
import { apiConfig, resolveApiConfig } from '../miniprogram/config/api.ts';
|
||||
import {
|
||||
apiRequest,
|
||||
ApiRequestError,
|
||||
} from '../miniprogram/services/api-request.ts';
|
||||
import * as contentApi from '../miniprogram/services/content-api.ts';
|
||||
import * as model from '../miniprogram/pages/category/category.model.ts';
|
||||
|
||||
const categories = [
|
||||
{
|
||||
id: 'math',
|
||||
name: '数感启蒙',
|
||||
icon: '📐',
|
||||
parentId: null,
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
const worksheet = (id, extra = {}) => ({
|
||||
id,
|
||||
title: id,
|
||||
subtitle: '',
|
||||
category: 'math',
|
||||
path: `/mathPages/mathDraw/mathDraw?id=${id}`,
|
||||
ageMin: 3,
|
||||
ageMax: 8,
|
||||
difficulty: 1,
|
||||
previewImg: null,
|
||||
likes: '2',
|
||||
downloads: '10',
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
updatedAt: '2025-01-01T00:00:00Z',
|
||||
contentUpdatedAt: null,
|
||||
...extra,
|
||||
});
|
||||
const pageOf = (items, page = 1, total = items.length) => ({
|
||||
items,
|
||||
page,
|
||||
pageSize: 100,
|
||||
total,
|
||||
hasNext: page * 100 < total,
|
||||
});
|
||||
let requests;
|
||||
let navigations;
|
||||
let handler;
|
||||
|
||||
beforeEach(() => {
|
||||
requests = [];
|
||||
navigations = [];
|
||||
apiConfig.developmentEnvironment = 'local';
|
||||
apiConfig.onlineBaseUrl = '';
|
||||
handler = () => ({ data: [] });
|
||||
globalThis.wx = {
|
||||
getAccountInfoSync: () => ({ miniProgram: { envVersion: 'develop' } }),
|
||||
request(options) {
|
||||
requests.push(options);
|
||||
Promise.resolve()
|
||||
.then(() => handler(options))
|
||||
.then((result) => {
|
||||
options.success({
|
||||
statusCode: result.statusCode ?? 200,
|
||||
data: result.raw ?? {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: result.data,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => options.fail({ errMsg: 'request:fail' }));
|
||||
},
|
||||
showToast() {},
|
||||
navigateTo(options) {
|
||||
navigations.push(options.url);
|
||||
},
|
||||
switchTab(options) {
|
||||
navigations.push(options.url);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/** 仅隔离埋点与绘图分享依赖;分类页、请求层及数据模型均使用真实源码。 */
|
||||
function createCategoryPage(api = contentApi) {
|
||||
let options;
|
||||
const source = readFileSync(
|
||||
new URL('../miniprogram/pages/category/category.ts', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const code = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
},
|
||||
}).outputText;
|
||||
const modules = {
|
||||
'../../base/pageMixin': { appSharePageMethods: {} },
|
||||
'../../utils/index': { takePendingCategoryTabId: () => null },
|
||||
'../../utils/tracker': {
|
||||
default: {
|
||||
reportPageView() {},
|
||||
report() {},
|
||||
reportWorksheetClick() {},
|
||||
},
|
||||
},
|
||||
'../../services/api-request': { ApiRequestError },
|
||||
'../../services/content-api': api,
|
||||
'./category.model': model,
|
||||
};
|
||||
vm.runInNewContext(code, {
|
||||
exports: {},
|
||||
wx: globalThis.wx,
|
||||
Error,
|
||||
require(path) {
|
||||
assert.ok(path in modules, path);
|
||||
return modules[path];
|
||||
},
|
||||
Page(value) {
|
||||
options = value;
|
||||
},
|
||||
});
|
||||
const instance = {
|
||||
...options,
|
||||
data: structuredClone(options.data),
|
||||
setData(value) {
|
||||
Object.assign(this.data, value);
|
||||
},
|
||||
};
|
||||
return instance;
|
||||
}
|
||||
|
||||
function useRows(rows, selectedCategories = categories) {
|
||||
handler = ({ url, data }) => {
|
||||
const path = new URL(url).pathname;
|
||||
if (path.endsWith('/categories')) return { data: selectedCategories };
|
||||
if (path.endsWith('/worksheets')) {
|
||||
const page = data.page;
|
||||
return {
|
||||
data: pageOf(
|
||||
rows.slice((page - 1) * 100, page * 100),
|
||||
page,
|
||||
rows.length,
|
||||
),
|
||||
};
|
||||
}
|
||||
const row = rows.find((item) =>
|
||||
path.endsWith(`/${encodeURIComponent(item.id)}`),
|
||||
);
|
||||
return row ? { data: row } : { statusCode: 404 };
|
||||
};
|
||||
}
|
||||
|
||||
test('环境配置:开发版默认本地,正式/体验/未知环境不回退本地,线上要求 HTTPS', () => {
|
||||
assert.equal(
|
||||
resolveApiConfig('develop').baseUrl,
|
||||
'http://127.0.0.1:8800/api/v1',
|
||||
);
|
||||
for (const version of ['release', 'trial', 'unknown'])
|
||||
assert.throws(() => resolveApiConfig(version), /线上/);
|
||||
apiConfig.onlineBaseUrl = 'https://api.example.test/api/v1/';
|
||||
for (const version of ['release', 'trial'])
|
||||
assert.equal(
|
||||
resolveApiConfig(version).baseUrl,
|
||||
'https://api.example.test/api/v1',
|
||||
);
|
||||
apiConfig.developmentEnvironment = 'online';
|
||||
assert.equal(resolveApiConfig('develop').environment, 'online');
|
||||
apiConfig.onlineBaseUrl = 'http://api.example.test/api/v1';
|
||||
assert.throws(() => resolveApiConfig('release'), /配置/);
|
||||
});
|
||||
|
||||
test('请求层:HTTP、业务错误、畸形响应、网络错误均拒绝,不当作空成功', async () => {
|
||||
useRows([]);
|
||||
assert.deepEqual(await apiRequest('/categories'), categories);
|
||||
assert.equal(requests[0].url, 'http://127.0.0.1:8800/api/v1/categories');
|
||||
for (const result of [
|
||||
{ statusCode: 404 },
|
||||
{ statusCode: 503 },
|
||||
{ raw: { code: 1, data: [] } },
|
||||
{ raw: { code: 0 } },
|
||||
{ raw: '<html>error</html>' },
|
||||
]) {
|
||||
handler = () => result;
|
||||
await assert.rejects(apiRequest('/categories'), ApiRequestError);
|
||||
}
|
||||
handler = () => {
|
||||
throw new Error('offline');
|
||||
};
|
||||
await assert.rejects(apiRequest('/categories'), /连接失败/);
|
||||
});
|
||||
|
||||
test('完整分页:121 条无遗漏,计数字符串保留精度,空数据正常结束', async () => {
|
||||
const rows = Array.from({ length: 121 }, (_, index) =>
|
||||
worksheet(`item-${index}`, { downloads: '9223372036854775906' }),
|
||||
);
|
||||
useRows(rows);
|
||||
const actual = await contentApi.listAllWorksheets();
|
||||
assert.deepEqual(actual, rows);
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(actual[120].downloads, '9223372036854775906');
|
||||
useRows([]);
|
||||
assert.deepEqual(await contentApi.listAllWorksheets(), []);
|
||||
});
|
||||
|
||||
test('分页变化、重复记录、缺页及非法计数拒绝返回部分内容', async () => {
|
||||
for (const result of [
|
||||
pageOf([worksheet('one'), worksheet('one')]),
|
||||
pageOf([worksheet('one')], 1, 2),
|
||||
pageOf([worksheet('one', { likes: 9007199254740992 })]),
|
||||
{ ...pageOf([]), hasNext: true },
|
||||
]) {
|
||||
handler = () => ({ data: result });
|
||||
await assert.rejects(contentApi.listAllWorksheets());
|
||||
}
|
||||
const rows = Array.from({ length: 100 }, (_, index) =>
|
||||
worksheet(`item-${index}`),
|
||||
);
|
||||
handler = ({ data }) => ({
|
||||
data:
|
||||
data.page === 1
|
||||
? pageOf(rows, 1, 101)
|
||||
: pageOf([worksheet('last')], 2, 102),
|
||||
});
|
||||
await assert.rejects(contentApi.listAllWorksheets());
|
||||
});
|
||||
|
||||
test('展示模型:采用服务端分类、空图有图标、不恢复失联分类,榜单精确比较 64 位计数', () => {
|
||||
const rows = [
|
||||
worksheet('one', { likes: '9223372036854775806', downloads: '5' }),
|
||||
worksheet('two', {
|
||||
likes: '9223372036854775807',
|
||||
downloads: '9223372036854775906',
|
||||
}),
|
||||
worksheet('new', { likes: '1', updatedAt: '2026-10-01T00:00:00Z' }),
|
||||
worksheet('hidden-category', { category: 'papers' }),
|
||||
];
|
||||
const dataset = model.buildCategoryDataset(
|
||||
[...categories, { ...categories[0], id: 'craft', name: '创意手工' }],
|
||||
rows,
|
||||
);
|
||||
assert.deepEqual(
|
||||
dataset.sidebarTabs.map((row) => row.id),
|
||||
['all', 'hot', 'top-downloads', 'math', 'craft'],
|
||||
);
|
||||
const all = model.getCategoryItems(dataset, 'all');
|
||||
assert.equal(all.length, 3);
|
||||
assert.equal(all[0].id, 'new');
|
||||
assert.equal(all[0].icon, '📐');
|
||||
assert.equal(all[0].previewImg, '');
|
||||
assert.equal(model.getCategoryItems(dataset, 'hot')[0].id, 'two');
|
||||
assert.equal(model.getCategoryItems(dataset, 'top-downloads')[0].id, 'two');
|
||||
assert.equal(model.getCategoryItems(dataset, 'math', 'new').length, 1);
|
||||
assert.equal(model.compareCountDescending('0005', '5'), 0);
|
||||
});
|
||||
|
||||
test('页面:接口成功展示真实数据,失败清空旧目录/内容,重试可恢复,失效分类回到全部', async () => {
|
||||
const page = createCategoryPage();
|
||||
page.onLoad({ id: 'papers' });
|
||||
useRows([worksheet('real')]);
|
||||
await page.loadWorksheets();
|
||||
assert.equal(page.data.activeCategoryId, 'all');
|
||||
assert.equal(page.data.displayItems[0].id, 'real');
|
||||
assert.ok(!page.data.sidebarTabs.some((tab) => tab.id === 'papers'));
|
||||
handler = () => {
|
||||
throw new Error('offline');
|
||||
};
|
||||
await page.loadWorksheets();
|
||||
assert.equal(page.data.displayItems.length, 0);
|
||||
assert.equal(page.data.sidebarTabs.length, 3);
|
||||
assert.ok(page.data.errorMessage);
|
||||
useRows([worksheet('retry')]);
|
||||
await page.loadWorksheets();
|
||||
assert.equal(page.data.errorMessage, '');
|
||||
assert.equal(page.data.displayItems[0].id, 'retry');
|
||||
});
|
||||
|
||||
test('页面:旧请求/离开页面后的响应不会覆盖新请求', async () => {
|
||||
let finishOld;
|
||||
let calls = 0;
|
||||
const page = createCategoryPage({
|
||||
...contentApi,
|
||||
listCategories: () =>
|
||||
++calls === 1
|
||||
? new Promise((resolve) => {
|
||||
finishOld = resolve;
|
||||
})
|
||||
: Promise.resolve(categories),
|
||||
listAllWorksheets: async () => [worksheet(calls === 1 ? 'old' : 'new')],
|
||||
});
|
||||
const first = page.loadWorksheets();
|
||||
await page.loadWorksheets();
|
||||
assert.equal(page.data.displayItems[0].id, 'new');
|
||||
finishOld(categories);
|
||||
await first;
|
||||
assert.equal(page.data.displayItems[0].id, 'new');
|
||||
let finish;
|
||||
const hiddenPage = createCategoryPage({
|
||||
...contentApi,
|
||||
listCategories: () =>
|
||||
new Promise((resolve) => {
|
||||
finish = resolve;
|
||||
}),
|
||||
listAllWorksheets: async () => [],
|
||||
});
|
||||
const pending = hiddenPage.loadWorksheets();
|
||||
hiddenPage.onHide();
|
||||
finish(categories);
|
||||
await pending;
|
||||
assert.equal(hiddenPage.data.loading, true);
|
||||
});
|
||||
|
||||
test('页面:点击前检查详情;正常跳转,404 下架刷新并阻止进入本地页面', async () => {
|
||||
const page = createCategoryPage();
|
||||
const row = worksheet('live');
|
||||
useRows([row]);
|
||||
await page.loadWorksheets();
|
||||
await page.onTapItem({ currentTarget: { dataset: { id: 'live' } } });
|
||||
assert.deepEqual(navigations, [row.path]);
|
||||
navigations.length = 0;
|
||||
useRows([]);
|
||||
await page.onTapItem({ currentTarget: { dataset: { id: 'live' } } });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(navigations.length, 0);
|
||||
assert.equal(page.data.displayItems.length, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
'本地联调:真实 8800 分类、全部题型、页面展示与点击详情一致',
|
||||
{ skip: process.env.TEST_LOCAL_API !== '1' },
|
||||
async () => {
|
||||
const realRequest = globalThis.wx.request;
|
||||
globalThis.wx.request = (options) => {
|
||||
const url = new URL(options.url);
|
||||
for (const [key, value] of Object.entries(options.data || {}))
|
||||
url.searchParams.set(key, String(value));
|
||||
fetch(url, { signal: AbortSignal.timeout(5000) })
|
||||
.then(async (response) =>
|
||||
options.success({
|
||||
statusCode: response.status,
|
||||
data: await response.json(),
|
||||
}),
|
||||
)
|
||||
.catch(() => options.fail({}));
|
||||
};
|
||||
try {
|
||||
const page = createCategoryPage();
|
||||
await page.loadWorksheets();
|
||||
assert.equal(page.data.errorMessage, '');
|
||||
const rows = await contentApi.listAllWorksheets();
|
||||
assert.ok(rows.length > 0);
|
||||
assert.equal(page.data.displayItems.length, rows.length);
|
||||
assert.ok(page.data.displayItems.every((row) => row.icon));
|
||||
await page.onTapItem({
|
||||
currentTarget: {
|
||||
dataset: { id: page.data.displayItems[0].id },
|
||||
},
|
||||
});
|
||||
assert.equal(navigations.length, 1);
|
||||
console.log(
|
||||
`本地联调:${page.data.sidebarTabs.length - 3} 个分类、${rows.length} 个题型。`,
|
||||
);
|
||||
} finally {
|
||||
globalThis.wx.request = realRequest;
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user