71 lines
3.3 KiB
JavaScript
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'],
|
|
]);
|
|
});
|