451 lines
15 KiB
JavaScript
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',
|
|
);
|
|
}
|
|
});
|