/*
* 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 .
*/
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: '
'},
}));
vi.mock('@/store', async () => {
const actual = await vi.importActual('@/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: ''}},
{path: '/login', name: 'login', component: {template: ''}},
],
});
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: ''},
RouterView: {template: ''},
},
},
});
}
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');
});
});