Files
Investidor-V2/tests/frontend/portal-navigation.test.mjs
T
2026-11-10 18:07:50 +02:00

451 lines
15 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
catalogueDetailHref,
catalogueReturnHref,
} from '../../resources/js/lib/catalogue-context.ts';
import {
portalNavigation,
legacyPortalNavigation,
currentNavigationGroup,
currentNavigationItem,
canShowLegacyManagement,
} from '../../resources/js/lib/portal-navigation.ts';
void test('public and authenticated navigation share groups without revealing management', () => {
const publicGroups = portalNavigation({
authenticated: false,
manager: false,
});
const investor = portalNavigation({ authenticated: true, manager: false });
assert.deepEqual(publicGroups.slice(0, 3), investor.slice(0, 3));
assert.deepEqual(
publicGroups.map((g) => g.id),
['discovery', 'realization', 'support', 'work', 'account'],
);
const hrefs = JSON.stringify(investor);
assert.equal(hrefs.includes('/management/'), false);
assert.equal(JSON.stringify(publicGroups).includes('/login'), true);
assert.equal(hrefs.includes('/investidor/profile'), true);
assert.equal(hrefs.includes('/balcao-virtual'), true);
});
void test('management and optional modules follow permissions and feature flags', () => {
const groups = portalNavigation({
authenticated: true,
manager: true,
businesses: true,
education: true,
ai: true,
});
assert.ok(groups.some((group) => group.id === 'work'));
assert.equal(JSON.stringify(groups).includes('/investidor/negocios'), true);
assert.equal(JSON.stringify(groups).includes('/management/ai'), true);
assert.equal(
JSON.stringify(
portalNavigation({
authenticated: false,
manager: false,
education: false,
}),
).includes('/educacao'),
false,
);
});
void test('detail URLs and filtered catalogue pages retain the current group', () => {
// Preserve the retained catalogue's detail/filter matching independently
// of the reconciled shell's new grouping names.
const groups = legacyPortalNavigation({
authenticated: true,
manager: true,
});
for (const url of [
'/carteira/project/fixture',
'/zonas?category=zee',
'/zonas/fixture',
'/oportunidades/fixture',
])
assert.equal(currentNavigationGroup(groups, url), 'explore');
assert.equal(
currentNavigationGroup(
groups,
'/balcao-virtual/dossiers/fixture?phase=market',
),
'support',
);
assert.equal(
currentNavigationGroup(groups, '/investidor/profile'),
'account',
);
assert.equal(
currentNavigationGroup(groups, '/management/crm/leads/fixture'),
'management',
);
assert.equal(currentNavigationGroup(groups, '/unknown'), null);
assert.equal(
currentNavigationItem(groups, '/carteira?q=QA&kind=project'),
'projects',
);
assert.equal(
currentNavigationItem(groups, '/zonas?category=zee&q=QA'),
'zee',
);
});
void test('reconciled navigation selects discovery, realization and private support correctly', () => {
const groups = portalNavigation({ authenticated: false, manager: false });
assert.equal(
currentNavigationGroup(groups, '/oportunidades/fixture'),
null,
);
assert.equal(currentNavigationGroup(groups, '/simulador-fiscal'), null);
assert.equal(
currentNavigationGroup(groups, '/balcao-virtual/dossiers/fixture'),
'support',
);
assert.equal(currentNavigationGroup(groups, '/management/crm/leads'), null);
});
void test('discovery retains distinct catalogues without repeating main entry points', () => {
const groups = portalNavigation({
authenticated: false,
manager: false,
businesses: true,
education: true,
});
const links = [];
const collect = (items) =>
items.forEach((item) => {
if (item.href) links.push(item.href);
if (item.children) collect(item.children);
});
collect(groups);
assert.equal(new Set(links).size, links.length);
const discovery = groups.find((group) => group.id === 'discovery');
const retained = JSON.stringify(discovery);
for (const id of ['why', 'opportunities', 'simulator', 'sectors', 'start'])
assert.equal(
discovery.children.some((item) => item.id === id),
false,
);
assert.equal(retained.includes('/investimentos/iniciar'), false);
for (const href of [
'/investimentos/iniciar',
'/oportunidades?view=map',
'/carteira',
'/zonas',
'/negocios',
'/educacao',
'/documentos',
'/legislacao',
])
assert.ok(links.includes(href), href);
});
void test('catalogue detail and return preserve the search inside the local catalogue', () => {
const query = 'q=turismo+em+Inhambane&kind=cluster&province_id=fixture';
const href = catalogueDetailHref('/carteira/fixture', query);
assert.equal(catalogueReturnHref('/carteira', href), '/carteira?' + query);
assert.equal(
catalogueReturnHref(
'/zonas',
'/zonas/fixture?catalogue_query=https%3A%2F%2Foutside.invalid',
),
'/zonas?https%3A%2F%2Foutside.invalid=',
);
});
const destinations = (items) =>
items.flatMap((item) => [
...(item.href ? [item.href] : []),
...destinations(item.children ?? []),
]);
void test('both menu consumers respect the existing scoped server decision rather than generic manager status', () => {
assert.equal(
canShowLegacyManagement({
user: { id: 1 },
canManagePortal: true,
legacyManagementAccess: false,
}),
false,
);
assert.equal(
canShowLegacyManagement({
user: { id: 1 },
canManagePortal: true,
legacyManagementAccess: true,
}),
true,
);
assert.equal(
canShowLegacyManagement({
user: null,
canManagePortal: true,
legacyManagementAccess: true,
}),
false,
);
assert.equal(
canShowLegacyManagement({ user: { id: 1 }, canManagePortal: true }),
true,
);
});
void test('work entry is discoverable without exposing private capabilities to an anonymous visitor', () => {
const groups = portalNavigation({
authenticated: false,
manager: true,
contentManagementHref: '/gestao-conteudos',
contentManagementAccess: {
opportunities: {
index: '/management/opportunities',
create: '/management/opportunities/create',
},
},
scopedAdministration: [
{ label: 'Private QA administration', href: '/management/secret' },
],
});
assert.deepEqual(
destinations(groups.find((g) => g.id === 'work').children),
['/acesso-gestao'],
);
assert.equal(JSON.stringify(groups).includes('/management/'), false);
assert.equal(JSON.stringify(groups).includes('/gestao-conteudos'), false);
});
void test('work area exposes existing content capability even when the old dashboard flag has no menu object', () => {
const groups = portalNavigation({
authenticated: true,
manager: true,
opportunityManagement: null,
contentManagementHref: '/gestao-conteudos',
contentManagementAccess: {
opportunities: {
index: '/management/opportunities',
create: '/management/opportunities/create',
},
},
});
const work = groups.find((g) => g.id === 'work');
assert.equal(work.label, 'Área de trabalho');
assert.ok(
work.children.some(
(i) =>
i.label === 'Preparar análise e ideia' &&
i.href ===
'/management/opportunities/create?analysis=proactive',
),
);
assert.ok(work.children.some((i) => i.href === '/gestao-conteudos'));
assert.ok(
work.children.some((i) => i.href === '/management/opportunities'),
);
});
void test('every previously authorised management/profile destination survives the regrouping exactly once', () => {
const access = {
authenticated: true,
manager: true,
businesses: true,
education: true,
ai: true,
contentManagementHref: '/gestao-conteudos',
profileAccess: {
contexts: [],
mine: '/minhas-oportunidades',
create: '/minhas-oportunidades/create',
request: '/perfil/promotor',
review: null,
audit: '/perfis/auditoria',
administration: '/perfis/gestao',
},
scopedAdministration: [
{
label: 'Referências económicas',
href: '/management/economic-references/lot-01',
},
],
};
const old = destinations(
legacyPortalNavigation(access).filter((g) =>
['management', 'profile-opportunities'].includes(g.id),
),
);
const groups = portalNavigation(access);
const after = destinations(groups);
for (const href of old)
assert.equal(after.filter((x) => x === href).length, 1, href);
assert.equal(after.length, new Set(after).size);
assert.ok(
destinations(groups.find((g) => g.id === 'work').children).includes(
'/management/economic-references/lot-01',
),
);
assert.equal(
after.filter((href) => href === '/management/profile-install').length,
1,
'profile installation already present in the old authorised sidebar must survive',
);
});
void test('ordinary authenticated account has an explanation entry and does not acquire creation or management links', () => {
const groups = portalNavigation({ authenticated: true, manager: false });
assert.deepEqual(
destinations(groups.find((g) => g.id === 'work').children),
['/acesso-gestao'],
);
assert.equal(JSON.stringify(groups).includes('analysis=proactive'), false);
assert.equal(JSON.stringify(groups).includes('/management/'), false);
});
void test('direct analysis and ordinary creation preserve distinct selection state in the same work group', () => {
const access = {
authenticated: true,
manager: false,
profileAccess: {
contexts: [],
mine: '/minhas-oportunidades',
create: '/minhas-oportunidades/create',
request: '/perfil/promotor',
review: null,
administration: null,
},
};
const groups = portalNavigation(access);
assert.equal(
currentNavigationGroup(
groups,
'/minhas-oportunidades/create?analysis=proactive',
),
'work',
);
assert.equal(
currentNavigationItem(
groups,
'/minhas-oportunidades/create?analysis=proactive',
),
'prepare-analysis',
);
assert.equal(
currentNavigationItem(groups, '/minhas-oportunidades/create'),
'new-my-opportunity',
);
});
void test('investment entry belongs to realization exactly once', () => {
const groups = portalNavigation({ authenticated: false, manager: false });
const realization = groups.find((g) => g.id === 'realization');
assert.ok(
realization.children.some(
(i) => i.id === 'start' && i.href === '/investimentos/iniciar',
),
);
assert.equal(
destinations(groups).filter((href) => href === '/investimentos/iniciar')
.length,
1,
);
assert.equal(
currentNavigationGroup(groups, '/investimentos/iniciar'),
'realization',
);
});
void test('portfolio and zone fiche return keeps the applied catalogue page and filters', () => {
for (const base of [
'/carteira',
'/zonas',
'/management/portfolio',
'/management/investment/zones',
]) {
const href = catalogueDetailHref(
base + '/synthetic',
'q=energia&province_id=p',
base + '?q=energia&province_id=p&page=3&unrelated=private',
);
const returned = new URL(
catalogueReturnHref(base, href),
'https://portal.invalid',
);
assert.equal(returned.searchParams.get('page'), '3');
assert.equal(returned.searchParams.get('q'), 'energia');
assert.equal(returned.searchParams.get('province_id'), 'p');
assert.equal(returned.searchParams.has('unrelated'), false);
}
});
void test('catalogue pagination inheritance rejects invalid page values', () => {
for (const page of ['0', '-1', '1.5', 'invalid', '9007199254740992']) {
const href = catalogueDetailHref(
'/carteira/synthetic',
'kind=project',
'/carteira?page=' + page,
);
const query = new URLSearchParams(
new URL(href, 'https://portal.invalid').searchParams.get(
'catalogue_query',
),
);
assert.equal(query.has('page'), false);
}
});
void test('funding links require installation and saved list requires authentication', () => {
const base = { authenticated: false, manager: false };
assert.equal(
JSON.stringify(portalNavigation(base)).includes('/fundos'),
false,
);
const publicMenu = JSON.stringify(
portalNavigation({ ...base, funding: true }),
);
assert.equal(publicMenu.includes('/fundos'), true);
assert.equal(publicMenu.includes('/fundos/guardados'), false);
assert.equal(
JSON.stringify(
portalNavigation({ ...base, funding: true, authenticated: true }),
).includes('/fundos/guardados'),
true,
);
});
void test('installed concours has its own primary group and preserves public and private destinations', () => {
const base = { authenticated: false, manager: false };
assert.equal(
portalNavigation(base).some((g) => g.id === 'procurement-area'),
false,
);
for (const authenticated of [false, true]) {
const groups = portalNavigation({
...base,
authenticated,
procurement: true,
});
const group = groups.find((g) => g.id === 'procurement-area');
assert.equal(group.label, 'Concursos e anúncios');
assert.equal(
destinations(groups).filter((h) => h === '/concursos').length,
1,
);
assert.equal(
JSON.stringify(groups.find((g) => g.id === 'discovery')).includes(
'/concursos',
),
false,
);
assert.equal(
currentNavigationGroup(groups, '/concursos?q=obras'),
'procurement-area',
);
assert.equal(
currentNavigationGroup(groups, '/concursos/fixture'),
'procurement-area',
);
assert.equal(
destinations(group.children).includes('/concursos/acompanhar'),
authenticated,
);
if (authenticated)
assert.equal(
currentNavigationItem(groups, '/concursos/acompanhar'),
'procurement-watched',
);
}
});