mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-11-01 02:31:46 +08:00
Land the v7 three-terminal UX decision: drop the width/pointer-based 44px touch enlargement so one 32px density holds across desktop, tablet, and mobile, and rebuild entity pages on a responsive record list (ResponsiveRecordList + EntityMobileCard + per-entity mobile summaries). Tighten the nginx security headers (CSP frame-ancestors 'none', X-Frame-Options DENY) to match the static e2e server. Complete the dc3-web audit remediation, including the remaining LOW items from docs/dc3-web-low-defects-handoff.md: streaming-safe message loads in the agentic store, mock menu reparent cycle rejection, paged-list page clamping, fetch/axios mock request normalization, per-table attribute catalogs, post-flush alarm watchers, rowKey-agnostic delete state, localized card text fallbacks, and e2e-server port fail-fast. Gate: vue-tsc, eslint, vitest 687 passed, responsive e2e 64 passed. Co-Authored-By: Claude Code <noreply@anthropic.com>
130 lines
5.3 KiB
TypeScript
130 lines
5.3 KiB
TypeScript
/*
|
|
* Copyright 2016-present the IoT DC3 original author or authors.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {flushPromises, mount} from '@vue/test-utils';
|
|
import {createMemoryHistory, createRouter} from 'vue-router';
|
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {createPinia, setActivePinia} from 'pinia';
|
|
|
|
import i18n from '@/config/i18n';
|
|
import {createElButtonStub, layoutStubs} from '../setup/stubs/element-plus';
|
|
import {sampleMenuTree} from '../fixtures/menu';
|
|
|
|
// Layout reaches through several stores and the router push singleton.
|
|
// Mock at the boundaries — store, router default export, agentic child.
|
|
const layoutMocks = vi.hoisted(() => ({
|
|
routerPush: vi.fn(() => Promise.resolve()),
|
|
logout: vi.fn(() => Promise.resolve()),
|
|
}));
|
|
|
|
vi.mock('@/config/router', () => ({
|
|
default: {push: layoutMocks.routerPush, currentRoute: {value: {path: '/home'}}},
|
|
}));
|
|
|
|
vi.mock('@/composables/useBreakpoint', async () => {
|
|
const {ref} = await import('vue');
|
|
return {useBreakpoint: () => ({isDesktop: ref(true), isMobile: ref(false), isTablet: ref(false)})};
|
|
});
|
|
|
|
vi.mock('@/components/agentic/AgenticAssistant.vue', () => ({
|
|
default: {name: 'AgenticAssistant', template: '<div class="agentic-stub" />'},
|
|
}));
|
|
|
|
vi.mock('@/store', async () => {
|
|
const actual = await vi.importActual<typeof import('@/store')>('@/store');
|
|
return {
|
|
...actual,
|
|
useAuthStore: () => ({logout: layoutMocks.logout}),
|
|
};
|
|
});
|
|
|
|
async function mountLayout() {
|
|
setActivePinia(createPinia());
|
|
const {useMenuStore} = await import('@/store');
|
|
const menuStore = useMenuStore();
|
|
// Seed the store directly — bootstrap() would otherwise hit the menu API.
|
|
menuStore.tree = sampleMenuTree;
|
|
menuStore.loaded = true;
|
|
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{path: '/', redirect: '/home'},
|
|
{path: '/home', name: 'home', component: {template: '<div class="home-route" />'}},
|
|
{path: '/login', name: 'login', component: {template: '<div />'}},
|
|
],
|
|
});
|
|
await router.push('/home');
|
|
await router.isReady();
|
|
|
|
const Layout = (await import('@/components/layout/Layout.vue')).default;
|
|
|
|
return mount(Layout, {
|
|
global: {
|
|
plugins: [i18n, router],
|
|
stubs: {
|
|
...layoutStubs,
|
|
ElButton: createElButtonStub(),
|
|
ElBacktop: {template: '<div class="el-backtop-stub" />'},
|
|
RouterView: {template: '<div class="router-view-stub" />'},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
describe('Layout', () => {
|
|
beforeEach(() => {
|
|
layoutMocks.routerPush.mockClear();
|
|
layoutMocks.logout.mockClear();
|
|
});
|
|
|
|
it('renders the top nav with the home menu and the menu store tree', async () => {
|
|
const wrapper = await mountLayout();
|
|
await flushPromises();
|
|
|
|
// The fixed shell has exactly two glass capsules: brand on the left,
|
|
// menu + user controls on the right.
|
|
expect(wrapper.findAll('.header_brand_glass')).toHaveLength(1);
|
|
expect(wrapper.find('.brand-lockup__title').text()).toContain('IoT DC3');
|
|
expect(wrapper.find('.brand-lockup__agentic').text()).toBe('AGENTIC');
|
|
expect(wrapper.find('.brand-lockup__spark').exists()).toBe(true);
|
|
expect(wrapper.find('.brand-lockup__signal').text()).toBe('Sense · Reason · Act · Evolve');
|
|
expect(wrapper.findAll('.header_actions_glass')).toHaveLength(1);
|
|
expect(wrapper.find('.header_actions_glass .header_menu_wrap').exists()).toBe(true);
|
|
expect(wrapper.find('.header_actions_glass .app-preferences').exists()).toBe(true);
|
|
expect(wrapper.find('.header_actions_glass .app-preferences__theme').exists()).toBe(true);
|
|
expect(wrapper.find('.header_actions_glass .header_user').exists()).toBe(true);
|
|
expect(wrapper.find('.header_actions_glass .user_trigger').exists()).toBe(true);
|
|
// The trigger is a bare avatar; the name lives in the dropdown identity row.
|
|
const avatar = wrapper.find('.header_actions_glass .user_trigger .user_avatar');
|
|
expect(avatar.exists()).toBe(true);
|
|
expect(avatar.attributes('src')).toContain('images/common/avatar.png');
|
|
expect(wrapper.find('.user_trigger .user_name').exists()).toBe(false);
|
|
expect(wrapper.find('.user_dropdown_identity .user_dropdown_name').exists()).toBe(true);
|
|
|
|
// Home item is always present.
|
|
expect(wrapper.text()).toContain('Home');
|
|
// First top-level node from sampleMenuTree renders via resolveMenuTitle.
|
|
expect(wrapper.text()).toContain(sampleMenuTree[0].menuName);
|
|
|
|
// Router-view is mounted into the body shell.
|
|
expect(wrapper.find('.router-view-stub').exists()).toBe(true);
|
|
expect(wrapper.get('.skip-link').attributes('href')).toBe('#main-content');
|
|
expect(wrapper.get('#main-content').attributes('tabindex')).toBe('-1');
|
|
});
|
|
});
|