kt-template-admin/test/api/core/menu.spec.ts

501 lines
14 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import ts from 'typescript';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const requestClientGet = vi.fn();
const messagePushMenuNames = [
'QqBotMessageSubscription',
'QqBotMessageTemplate',
'QqBotMessageSubscriptionList',
'QqBotMessageSubscriptionCreate',
'QqBotMessageSubscriptionUpdate',
'QqBotMessageSubscriptionDelete',
'QqBotMessageSubscriptionToggle',
'QqBotMessageTemplateList',
'QqBotMessageTemplateCreate',
'QqBotMessageTemplateUpdate',
'QqBotMessageTemplateDelete',
'QqBotMessageTemplateToggle',
'QqBotMessageTemplatePreview',
'QqBotAccountMessagePushList',
'QqBotAccountMessagePushCreate',
'QqBotAccountMessagePushUpdate',
'QqBotAccountMessagePushDelete',
'QqBotAccountMessagePushToggle',
];
function getSupportedAdminMenuNameLiterals() {
const sourceFile = ts.createSourceFile(
'menu.ts',
readFileSync(resolve('apps/web-antdv-next/src/api/core/menu.ts'), 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const declaration = sourceFile.statements
.filter((statement) => ts.isVariableStatement(statement))
.flatMap((statement) => statement.declarationList.declarations)
.find(
(candidate) =>
ts.isIdentifier(candidate.name) &&
candidate.name.text === 'SUPPORTED_ADMIN_MENU_NAMES',
);
expect(declaration?.initializer).toSatisfy(ts.isNewExpression);
const initializer = declaration?.initializer as ts.NewExpression;
expect(initializer.expression).toSatisfy(
(expression: ts.Expression) =>
ts.isIdentifier(expression) && expression.text === 'Set',
);
expect(initializer.arguments).toHaveLength(1);
expect(initializer.arguments?.[0]).toSatisfy(ts.isArrayLiteralExpression);
const elements = (initializer.arguments?.[0] as ts.ArrayLiteralExpression)
.elements;
expect(elements.every((element) => ts.isStringLiteral(element))).toBe(true);
return elements.map((element) => (element as ts.StringLiteral).text);
}
vi.mock('#/api/request', () => ({
requestClient: {
get: requestClientGet,
},
}));
describe('core menu api', () => {
beforeEach(() => {
requestClientGet.mockReset();
});
it('keeps supported dashboard routes from backend menus', async () => {
requestClientGet.mockResolvedValue([
{
name: 'Dashboard',
path: '/dashboard',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
},
],
},
{
name: 'Unsupported',
path: '/unsupported',
component: '/unsupported/index',
},
{
name: 'QqBot',
path: '/qqbot',
children: [
{
name: 'QqBotAccount',
path: '/qqbot/account',
component: '/qqbot/account/list',
children: [
{
name: 'QqBotAccountWebUI',
authCode: 'QqBot:Account:WebUI',
type: 'button',
},
],
},
{
name: 'QqBotAccountNapcatWebui',
path: '/qqbot/account/:accountId/napcat-webui',
component: '/qqbot/account/napcat-webui/index',
meta: {
activePath: '/qqbot/account',
hideInMenu: true,
title: 'NapCat WebUI',
},
},
],
},
]);
const { getAllMenusApi } =
await import('@test-source/apps/web-antdv-next/src/api/core/menu');
const menus = await getAllMenusApi();
expect(requestClientGet).toHaveBeenCalledWith('/menu/all');
expect(menus).toEqual([
{
name: 'Dashboard',
path: '/dashboard',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
},
],
},
{
name: 'QqBot',
path: '/qqbot',
children: [
{
name: 'QqBotAccount',
path: '/qqbot/account',
component: '/qqbot/account/list',
children: [
{
name: 'QqBotAccountWebUI',
authCode: 'QqBot:Account:WebUI',
type: 'button',
},
],
},
{
name: 'QqBotAccountNapcatWebui',
path: '/qqbot/account/:accountId/napcat-webui',
component: '/qqbot/account/napcat-webui/index',
meta: {
activePath: '/qqbot/account',
hideInMenu: true,
title: 'NapCat WebUI',
},
},
],
},
]);
});
it('uses backend sort as the authoritative menu order before menu generation', async () => {
requestClientGet.mockResolvedValue([
{
name: 'Dashboard',
path: '/dashboard',
sort: 200,
meta: {
order: -1,
title: 'Dashboard',
},
},
{
name: 'QqBot',
path: '/qqbot',
sort: 1,
meta: {
title: 'QQBot',
},
children: [
{
name: 'QqBotAccount',
path: '/qqbot/account',
component: '/qqbot/account/list',
sort: 1,
meta: {
title: '账号连接',
},
},
],
},
{
name: 'Blog',
path: '/blog',
sort: 0,
meta: {
order: 100,
title: '博客管理',
},
},
]);
const { getAllMenusApi } =
await import('@test-source/apps/web-antdv-next/src/api/core/menu');
const menus = await getAllMenusApi();
expect(menus.map((menu) => menu.meta?.order)).toEqual([200, 1, 0]);
expect(menus[1]?.children?.[0]?.meta?.order).toBe(1);
});
it('keeps blog article preview hidden route and row action permission from backend menus', async () => {
requestClientGet.mockResolvedValue([
{
name: 'Blog',
path: '/blog',
children: [
{
name: 'BlogArticle',
path: '/blog/article',
component: '/blog/article/list',
children: [
{
name: 'BlogArticlePreviewButton',
authCode: 'Blog:Article:Preview',
type: 'button',
},
],
},
{
name: 'BlogArticlePreview',
path: '/blog/article/:articleId/preview',
component: '/blog/article/preview/index',
meta: {
activePath: '/blog/article',
hideInMenu: true,
title: '文章预览',
},
},
],
},
]);
const { getAllMenusApi } =
await import('@test-source/apps/web-antdv-next/src/api/core/menu');
const menus = await getAllMenusApi();
expect(menus).toEqual([
{
name: 'Blog',
path: '/blog',
children: [
{
name: 'BlogArticle',
path: '/blog/article',
component: '/blog/article/list',
children: [
{
name: 'BlogArticlePreviewButton',
authCode: 'Blog:Article:Preview',
type: 'button',
},
],
},
{
name: 'BlogArticlePreview',
path: '/blog/article/:articleId/preview',
component: '/blog/article/preview/index',
meta: {
activePath: '/blog/article',
hideInMenu: true,
title: '文章预览',
},
},
],
},
]);
});
it('keeps generic network management under System with scoped actions', async () => {
requestClientGet.mockResolvedValue([
{
name: 'System',
path: '/system',
children: [
{
name: 'SystemNetwork',
path: '/system/network',
component: '/system/network/list',
children: [
{
name: 'SystemNetworkPortForwardList',
authCode: 'System:Network:PortForward:List',
type: 'button',
},
{
name: 'SystemNetworkPortForwardCreate',
authCode: 'System:Network:PortForward:Create',
type: 'button',
},
{
name: 'SystemNetworkPortForwardUpdate',
authCode: 'System:Network:PortForward:Update',
type: 'button',
},
{
name: 'SystemNetworkPortForwardDelete',
authCode: 'System:Network:PortForward:Delete',
type: 'button',
},
{
name: 'SystemNetworkPortForwardRetry',
authCode: 'System:Network:PortForward:Retry',
type: 'button',
},
{
name: 'SystemNetworkPortForwardKeeper',
authCode: 'System:Network:PortForward:Keeper',
type: 'button',
},
{
name: 'SystemNetworkPortForwardProbe',
authCode: 'System:Network:PortForward:Probe',
type: 'button',
},
{
name: 'SystemNetworkPortForwardHistory',
authCode: 'System:Network:PortForward:History',
type: 'button',
},
{
name: 'SystemNetworkDdnsList',
authCode: 'System:Network:Ddns:List',
type: 'button',
},
{
name: 'SystemNetworkDdnsCreate',
authCode: 'System:Network:Ddns:Create',
type: 'button',
},
{
name: 'SystemNetworkDdnsUpdate',
authCode: 'System:Network:Ddns:Update',
type: 'button',
},
{
name: 'SystemNetworkDdnsDelete',
authCode: 'System:Network:Ddns:Delete',
type: 'button',
},
{
name: 'SystemNetworkDdnsRetry',
authCode: 'System:Network:Ddns:Retry',
type: 'button',
},
],
},
],
},
]);
const { getAllMenusApi } =
await import('@test-source/apps/web-antdv-next/src/api/core/menu');
const menus = await getAllMenusApi();
expect(menus).toEqual([
{
name: 'System',
path: '/system',
children: [
{
name: 'SystemNetwork',
path: '/system/network',
component: '/system/network/list',
children: [
{
name: 'SystemNetworkPortForwardList',
authCode: 'System:Network:PortForward:List',
type: 'button',
},
{
name: 'SystemNetworkPortForwardCreate',
authCode: 'System:Network:PortForward:Create',
type: 'button',
},
{
name: 'SystemNetworkPortForwardUpdate',
authCode: 'System:Network:PortForward:Update',
type: 'button',
},
{
name: 'SystemNetworkPortForwardDelete',
authCode: 'System:Network:PortForward:Delete',
type: 'button',
},
{
name: 'SystemNetworkPortForwardRetry',
authCode: 'System:Network:PortForward:Retry',
type: 'button',
},
{
name: 'SystemNetworkPortForwardKeeper',
authCode: 'System:Network:PortForward:Keeper',
type: 'button',
},
{
name: 'SystemNetworkPortForwardProbe',
authCode: 'System:Network:PortForward:Probe',
type: 'button',
},
{
name: 'SystemNetworkPortForwardHistory',
authCode: 'System:Network:PortForward:History',
type: 'button',
},
{
name: 'SystemNetworkDdnsList',
authCode: 'System:Network:Ddns:List',
type: 'button',
},
{
name: 'SystemNetworkDdnsCreate',
authCode: 'System:Network:Ddns:Create',
type: 'button',
},
{
name: 'SystemNetworkDdnsUpdate',
authCode: 'System:Network:Ddns:Update',
type: 'button',
},
{
name: 'SystemNetworkDdnsDelete',
authCode: 'System:Network:Ddns:Delete',
type: 'button',
},
{
name: 'SystemNetworkDdnsRetry',
authCode: 'System:Network:Ddns:Retry',
type: 'button',
},
],
},
],
},
]);
});
it('keeps every supported message-push menu name once while filtering unknown nodes', async () => {
requestClientGet.mockResolvedValue([
{
name: 'QqBot',
path: '/qqbot',
children: [
{
name: 'QqBotAccount',
path: '/qqbot/account',
},
...messagePushMenuNames.map((name) => ({
authCode: `QqBot:${name}`,
name,
type: 'button',
})),
{
name: 'UnsupportedMessagePushNode',
type: 'button',
},
],
},
]);
const { getAllMenusApi } =
await import('@test-source/apps/web-antdv-next/src/api/core/menu');
const menus = await getAllMenusApi();
const qqbot = menus.find((menu) => menu.name === 'QqBot');
const retainedNames = qqbot?.children?.map((child) => child.name) ?? [];
expect(retainedNames).toEqual(['QqBotAccount', ...messagePushMenuNames]);
expect(new Set(retainedNames).size).toBe(retainedNames.length);
expect(retainedNames).not.toContain('UnsupportedMessagePushNode');
});
it('declares every locked message-push menu literal exactly once', () => {
const literals = getSupportedAdminMenuNameLiterals();
expect(new Set(literals).size).toBe(literals.length);
for (const name of messagePushMenuNames) {
expect(literals.filter((literal) => literal === name)).toHaveLength(1);
}
});
});