import { Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { insertSideMenuItem } from './vue-side-menu'; const INTERNAL_APP = ` `; // A header-layout app has no side menu at all, so no array to insert into. const HEADER_APP = ` `; describe('insertSideMenuItem', () => { let host: Tree; beforeEach(() => { host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); it('inserts an item into the app nav array', () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); insertSideMenuItem(host, 'apps/staff', { label: 'Review queue', to: '/queue', }); const app = host.read('apps/staff/src/App.vue').toString(); expect(app).toContain("{ label: 'Review queue', to: '/queue' },"); // The seeded Home entry survives. expect(app).toContain("{ label: 'Home', to: '/' },"); }); it('is idempotent on the same route', () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); const item = { label: 'Review queue', to: '/queue' }; insertSideMenuItem(host, 'apps/staff', item); insertSideMenuItem(host, 'apps/staff', item); const app = host.read('apps/staff/src/App.vue').toString(); expect(app.split("to: '/queue'").length - 1).toBe(1); }); it('does not re-add a route already present under a different label', () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue' }); insertSideMenuItem(host, 'apps/staff', { label: 'Renamed', to: '/queue' }); const app = host.read('apps/staff/src/App.vue').toString(); expect(app).toContain("label: 'Queue'"); expect(app).not.toContain("label: 'Renamed'"); }); it('is a silent no-op on a header-layout app with no side menu', () => { host.write('apps/public/src/App.vue', HEADER_APP); expect(() => insertSideMenuItem(host, 'apps/public', { label: 'Review queue', to: '/queue', }), ).not.toThrow(); expect(host.read('apps/public/src/App.vue').toString()).toBe(HEADER_APP); }); it('is a silent no-op when there is no App.vue at all', () => { expect(() => insertSideMenuItem(host, 'apps/missing', { label: 'X', to: '/x' }), ).not.toThrow(); }); // The original spec asserted both items were present but never their relative // order, so it passed while the function prepended and pushed Home to the // bottom of every generated app's nav. it("appends after the shell's own Home entry rather than before it", () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); insertSideMenuItem(host, 'apps/staff', { label: 'Review queue', to: '/queue', }); const app = host.read('apps/staff/src/App.vue')?.toString() ?? ''; expect(app.indexOf("to: '/'")).toBeLessThan(app.indexOf("to: '/queue'")); }); it('accumulates multiple items in the order they were added', () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue' }); insertSideMenuItem(host, 'apps/staff', { label: 'Livestock categories', to: '/livestock', }); const app = host.read('apps/staff/src/App.vue')?.toString() ?? ''; expect(app.indexOf("to: '/'")).toBeLessThan(app.indexOf("to: '/queue'")); expect(app.indexOf("to: '/queue'")).toBeLessThan( app.indexOf("to: '/livestock'"), ); }); it('emits the icon, which the element needs to render a non-blank item', () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue', icon: 'list', }); expect(host.read('apps/staff/src/App.vue')?.toString()).toContain( "{ label: 'Queue', to: '/queue', icon: 'list' },", ); }); it('appends validly when the array literal has no trailing comma', () => { host.write( 'apps/staff/src/App.vue', ` `, ); insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue' }); const app = host.read('apps/staff/src/App.vue')?.toString() ?? ''; expect(app).toContain("{ label: 'Home', to: '/' },"); expect(app.indexOf("to: '/'")).toBeLessThan(app.indexOf("to: '/queue'")); }); it('does not end its scan early on a bracket inside a label', () => { host.write( 'apps/staff/src/App.vue', ` `, ); insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue' }); const app = host.read('apps/staff/src/App.vue')?.toString() ?? ''; expect(app.indexOf("to: '/reports'")).toBeLessThan( app.indexOf("to: '/queue'"), ); }); it('is a silent no-op on an unterminated array literal', () => { const broken = ` `; host.write('apps/staff/src/App.vue', broken); insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue' }); expect(host.read('apps/staff/src/App.vue')?.toString()).toBe(broken); }); it('does not leave blank lines between accumulated entries', () => { host.write('apps/staff/src/App.vue', INTERNAL_APP); insertSideMenuItem(host, 'apps/staff', { label: 'A', to: '/a', icon: 'list', }); insertSideMenuItem(host, 'apps/staff', { label: 'B', to: '/b', icon: 'list', }); const app = host.read('apps/staff/src/App.vue')?.toString() ?? ''; const array = app.slice( app.indexOf('const primaryItems = ['), app.indexOf('];') + 2, ); expect(array).not.toMatch(/\n\s*\n/); }); });