Files
2026-09-03 08:38:17 +08:00

71 lines
3.3 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { parse, compileScript } from 'vue/compiler-sfc';
import * as Vue from 'vue';
import ts from 'typescript';
const source = readFileSync(new URL('../src/components/BottomNav.vue', import.meta.url), 'utf8');
const descriptor = parse(source, { filename: 'BottomNav.vue' }).descriptor;
const css = descriptor.styles.map((style) => style.content).join('\n');
const compiled = compileScript(descriptor, { id: 'bottom-nav-test', inlineTemplate: true,
templateOptions: { compilerOptions: { isCustomElement: (tag) => ['view', 'text'].includes(tag) } },
});
const js = ts.transpileModule(compiled.content, { compilerOptions: {
module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020,
} }).outputText;
function render(unread, active = 'home') {
const calls = [], exports = {};
globalThis.uni = {
reLaunch: ({ url }) => calls.push(['reLaunch', url]),
navigateTo: ({ url }) => calls.push(['navigateTo', url]),
};
new Function('require', 'exports', js)((name) => {
if (name === 'vue') return Vue;
if (name === './AppIcon.vue') return { default: { name: 'AppIcon' } };
if (name === '../utils/app-state') return { clientState: { unreadMessages: unread } };
throw new Error(name);
}, exports);
const result = exports.default.setup({ active }, { expose() {} })({}, []);
function flatten(node) {
if (!node || typeof node !== 'object') return [];
return [node, ...(Array.isArray(node.children) ? node.children.flatMap(flatten) : [])];
}
return { calls, nodes: flatten(result) };
}
test('bottom navigation explicitly overrides App-vue shrinking/clipping defaults', () => {
const button = css.match(/\.nav-item\s*\{([^}]+)\}/)?.[1];
for (const rule of [/\bwidth:\s*100%\s*;/, /\bmargin:\s*0\s*;/, /\boverflow:\s*visible\s*;/, /\bline-height:\s*1\s*;/]) {
assert.match(button, rule);
}
const badge = css.match(/\.badge\s*\{([^}]+)\}/)?.[1];
for (const rule of [/display:\s*flex/, /align-items:\s*center/, /justify-content:\s*center/, /white-space:\s*nowrap/, /pointer-events:\s*none/, /box-sizing:\s*border-box/]) {
assert.match(badge, rule);
}
assert.match(css, /safe-area-inset-bottom/);
});
test('badge appears only on messages, hides zero, and caps long counts at 99+', () => {
for (const [count, expected] of [[-1, null], [0, null], [1, '1'], [9, '9'], [99, '99'], [100, '99+'], [10000, '99+']]) {
const { nodes } = render(count);
const badges = nodes.filter((node) => node.props?.class === 'badge');
assert.equal(badges.length, expected === null ? 0 : 1);
if (expected !== null) assert.equal(badges[0].children.trim(), expected);
assert.equal(nodes.filter((node) => node.type === 'button').length, 5);
}
});
test('full-width tab buttons preserve navigation and the central publish action', () => {
const { calls, nodes } = render(100, 'messages');
const buttons = nodes.filter((node) => node.type === 'button');
assert.match(buttons.find((node) => node.props['aria-label'] === '消息').props.class, /active/);
for (const button of buttons) button.props.onTap();
assert.deepEqual(calls, [
['reLaunch', '/pages/home/index'], ['reLaunch', '/pages/messages/index'],
['navigateTo', '/pages/publish/index'], ['reLaunch', '/pages/feed/index'],
['reLaunch', '/pages/profile/index'],
]);
});