275 lines
9.1 KiB
JavaScript
275 lines
9.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { compileFunction } from 'node:vm';
|
|
import ts from 'typescript';
|
|
import React from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
const require = createRequire(import.meta.url);
|
|
function load(file, mocks = {}, suffix = '') {
|
|
const code = ts.transpileModule(
|
|
fs.readFileSync(new URL(file, import.meta.url), 'utf8') + suffix,
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(code, ['require', 'module', 'exports'])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
const Link = ({ children, href, className }) =>
|
|
React.createElement('a', { href, className }, children);
|
|
const Empty = () => null;
|
|
const mocks = {
|
|
'@inertiajs/react': { Link, Head: Empty },
|
|
'@/pages/management/shared': { states: { received: 'Recebido existente' } },
|
|
'@/pages/crm/types': { taskStates: { open: 'Aberta existente' } },
|
|
};
|
|
const Queue = load(
|
|
'../../resources/js/components/dashboard-work-queue.tsx',
|
|
mocks,
|
|
).default;
|
|
const Dashboard = load('../../resources/js/pages/dashboard.tsx', {
|
|
...mocks,
|
|
'@/components/dashboard-work-queue': { default: Queue },
|
|
'@/components/profile-navigation': { default: Empty },
|
|
'@/components/content-management-entry': { default: Empty },
|
|
'@/routes': { dashboard: () => '/dashboard' },
|
|
}).default;
|
|
const item = (patch = {}) => ({
|
|
id: 'synthetic-one',
|
|
title: 'SYNTHETIC next action',
|
|
reference: 'SYNTHETIC-REF',
|
|
status: 'received',
|
|
due_at: '2026-11-03T10:00:00.000000Z',
|
|
overdue: true,
|
|
href: '/management/interests/1',
|
|
...patch,
|
|
});
|
|
const group = (patch = {}) => ({
|
|
total: 1,
|
|
overdue: 1,
|
|
items: [item()],
|
|
page: 1,
|
|
last_page: 1,
|
|
previous: null,
|
|
next: null,
|
|
...patch,
|
|
});
|
|
const queue = (groups) => ({
|
|
as_of: '2026-11-03T11:00:00.000000Z',
|
|
desk_states: { accepted: 'Aceite internamente — edição configurada' },
|
|
groups,
|
|
});
|
|
const render = (Component, props) =>
|
|
renderToStaticMarkup(React.createElement(Component, props));
|
|
function nodes(tree) {
|
|
return Array.isArray(tree)
|
|
? tree.flatMap(nodes)
|
|
: tree && typeof tree === 'object'
|
|
? [tree, ...nodes(tree.props?.children)]
|
|
: [];
|
|
}
|
|
|
|
test('investor dashboard retains its investment context without an irrelevant operational panel', () => {
|
|
const html = render(Dashboard, {
|
|
summary: null,
|
|
workQueue: null,
|
|
investorPath: {
|
|
contexts: [{ id: 'own-case', title: 'SYNTHETIC own investment' }],
|
|
current: {
|
|
id: 'own-case',
|
|
next: { code: 'S01', title: 'SYNTHETIC next investment step' },
|
|
pending: 2,
|
|
documents: 3,
|
|
results: 1,
|
|
tasks: 4,
|
|
},
|
|
},
|
|
});
|
|
assert.match(html, /SYNTHETIC own investment/);
|
|
assert.match(html, /href="\/investimentos\/own-case#S01"/);
|
|
assert.doesNotMatch(
|
|
html,
|
|
/O meu acompanhamento|Manifestações atribuídas a mim|Gestão de Oportunidades/,
|
|
);
|
|
});
|
|
|
|
test('the three collections keep separate totals and labels from their existing domains', () => {
|
|
const html = render(Queue, {
|
|
queue: queue({
|
|
interests: group({ total: 7, overdue: 2 }),
|
|
desk: group({
|
|
total: 8,
|
|
overdue: 3,
|
|
items: [
|
|
item({
|
|
status: 'accepted',
|
|
href: '/balcao-virtual/pedidos/one',
|
|
}),
|
|
],
|
|
}),
|
|
crm_tasks: group({
|
|
total: 9,
|
|
overdue: 4,
|
|
items: [
|
|
item({
|
|
status: 'open',
|
|
href: '/management/crm/leads/one#crm-task-one',
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
});
|
|
for (const label of [
|
|
'Manifestações atribuídas a mim',
|
|
'Pedidos do Balcão atribuídos a mim',
|
|
'Tarefas do CRM atribuídas a mim',
|
|
'Recebido existente',
|
|
'Aceite internamente — edição configurada',
|
|
'Aberta existente',
|
|
])
|
|
assert.ok(html.includes(label));
|
|
for (const n of [7, 8, 9]) assert.ok(html.includes(`${n} por acompanhar`));
|
|
assert.doesNotMatch(html, /24 por acompanhar|Aprovado|Licença concedida/);
|
|
assert.ok(html.includes('href="/management/crm/leads/one#crm-task-one"'));
|
|
});
|
|
|
|
test('missing groups stay absent and an accessible empty collection shows its real zero', () => {
|
|
const html = render(Queue, {
|
|
queue: queue({ desk: group({ total: 0, overdue: 0, items: [] }) }),
|
|
});
|
|
assert.match(html, /0 por acompanhar/);
|
|
assert.match(html, /Não tem registos activos atribuídos/);
|
|
assert.doesNotMatch(
|
|
html,
|
|
/Manifestações atribuídas|Tarefas do CRM atribuídas|Página/,
|
|
);
|
|
assert.equal(render(Queue, { queue: undefined }), '');
|
|
});
|
|
|
|
test('deadlines use Maputo time with explicit overdue and unknown-deadline information', () => {
|
|
const html = render(Queue, {
|
|
queue: queue({
|
|
interests: group({
|
|
items: [
|
|
item(),
|
|
item({ id: 'no-date', due_at: null, overdue: false }),
|
|
],
|
|
}),
|
|
}),
|
|
});
|
|
assert.ok(html.includes('dateTime="2026-11-03T10:00:00.000000Z"'));
|
|
assert.match(html, /12:00.*?Maputo/);
|
|
assert.match(html, /Prazo ultrapassado/);
|
|
assert.match(html, /Sem prazo definido/);
|
|
});
|
|
|
|
test('each collection paginates through supplied destinations without losing scroll position', () => {
|
|
const tree = Queue({
|
|
queue: queue({
|
|
interests: group({
|
|
total: 13,
|
|
page: 2,
|
|
last_page: 3,
|
|
previous: '/dashboard?interests_page=1&desk_page=2',
|
|
next: '/dashboard?interests_page=3&desk_page=2',
|
|
}),
|
|
}),
|
|
});
|
|
const pagination = nodes(tree).find((node) => node.type === 'nav');
|
|
assert.equal(
|
|
pagination.props['aria-label'],
|
|
'Páginas: Manifestações atribuídas a mim',
|
|
);
|
|
const links = nodes(pagination).filter((node) => node.type === Link);
|
|
assert.deepEqual(
|
|
links.map((link) => link.props.href),
|
|
[
|
|
'/dashboard?interests_page=1&desk_page=2',
|
|
'/dashboard?interests_page=3&desk_page=2',
|
|
],
|
|
);
|
|
assert.ok(links.every((link) => link.props.preserveScroll === true));
|
|
assert.match(
|
|
render(Queue, {
|
|
queue: queue({
|
|
interests: group({ total: 13, page: 2, last_page: 3 }),
|
|
}),
|
|
}),
|
|
/Página 2 de 3/,
|
|
);
|
|
});
|
|
|
|
test('work queue and existing management indicators coexist, escaping record text', () => {
|
|
const html = render(Dashboard, {
|
|
summary: {
|
|
requestsByStatus: { received: 4 },
|
|
publishedOpportunities: 3,
|
|
draftOpportunities: 2,
|
|
activeSectors: 1,
|
|
recentRequests: [],
|
|
canCreateOpportunity: false,
|
|
},
|
|
workQueue: queue({
|
|
interests: group({
|
|
items: [item({ title: '<img src=x onerror=alert(1)>' })],
|
|
}),
|
|
}),
|
|
});
|
|
assert.match(html, /O meu acompanhamento/);
|
|
assert.match(html, /Indicadores de utilização e cobertura/);
|
|
assert.match(html, /Oportunidades publicadas/);
|
|
assert.ok(html.includes('<img src=x onerror=alert(1)>'));
|
|
assert.doesNotMatch(html, /<img/);
|
|
});
|
|
|
|
test('a queue task points to the actual existing CRM form anchor', () => {
|
|
const TaskRow = load(
|
|
'../../resources/js/pages/crm/lead.tsx',
|
|
{
|
|
'@inertiajs/react': { Link, useForm() {} },
|
|
'@/lib/crm-record-edit': {
|
|
useCrmRecordEdit: () => ({
|
|
form: { processing: false },
|
|
values: { status: 'open' },
|
|
error: () => undefined,
|
|
blocked: false,
|
|
}),
|
|
},
|
|
'@/components/crm-edit-comparison': { default: Empty },
|
|
'@/components/crm-task-creation': { default: Empty },
|
|
'../management/shared': {
|
|
Management: Empty,
|
|
Field: Empty,
|
|
button: '',
|
|
control: '',
|
|
},
|
|
'./types': { leadStates: {}, taskStates: { open: 'Aberta' } },
|
|
},
|
|
'\nexport { TaskRow };',
|
|
).TaskRow;
|
|
const form = TaskRow({
|
|
task: {
|
|
id: 'one',
|
|
title: 'SYNTHETIC task',
|
|
status: 'open',
|
|
due_at: null,
|
|
assignee_user_id: 1,
|
|
version_no: 1,
|
|
},
|
|
leadId: 'lead-one',
|
|
});
|
|
const href = '/management/crm/leads/lead-one#crm-task-one';
|
|
assert.equal(form.props.id, href.split('#')[1]);
|
|
});
|