238 lines
8.0 KiB
JavaScript
238 lines
8.0 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);
|
||
const sharedText = fs.readFileSync(
|
||
'resources/js/pages/partnerships/shared.tsx',
|
||
'utf8',
|
||
);
|
||
const labelSource = sharedText.match(
|
||
/export const labels: Record<string, string> = (\{[\s\S]*?\n\});/,
|
||
)[1];
|
||
const labels = compileFunction(`return (${labelSource});`, [])();
|
||
function load(file) {
|
||
const source = ts.transpileModule(fs.readFileSync(file, 'utf8'), {
|
||
compilerOptions: {
|
||
module: ts.ModuleKind.CommonJS,
|
||
jsx: ts.JsxEmit.ReactJSX,
|
||
},
|
||
}).outputText;
|
||
const module = { exports: {} };
|
||
compileFunction(source, ['require', 'module', 'exports'])(
|
||
(id) => {
|
||
if (id === './shared')
|
||
return {
|
||
Frame: ({ title, children }) =>
|
||
React.createElement(
|
||
'main',
|
||
{},
|
||
React.createElement('h1', {}, title),
|
||
children,
|
||
),
|
||
labels,
|
||
};
|
||
if (id === '@inertiajs/react')
|
||
return {
|
||
Link: ({ children, ...props }) =>
|
||
React.createElement('a', props, children),
|
||
router: {},
|
||
};
|
||
return require(id);
|
||
},
|
||
module,
|
||
module.exports,
|
||
);
|
||
return module.exports.default;
|
||
}
|
||
const Page = load('resources/js/pages/partnerships/interest-history.tsx');
|
||
const Dashboard = load('resources/js/pages/partnerships/dashboard.tsx');
|
||
const snapshot = () => ({
|
||
availability: 'available',
|
||
version: 1,
|
||
status: 'submitted',
|
||
scope_known: true,
|
||
organization_id: null,
|
||
fields: { proposal: 'SYNTHETIC proposal', capacity: null },
|
||
consent_known: true,
|
||
consent: false,
|
||
assessment_recorded: true,
|
||
assessment: [],
|
||
references: [],
|
||
complete: true,
|
||
});
|
||
const entry = () => ({
|
||
version: 2,
|
||
restricted: false,
|
||
action: 'assess',
|
||
actor_name: 'SYNTHETIC owner',
|
||
occurred_at: '2026-11-03',
|
||
reason: 'SYNTHETIC reason',
|
||
previous: snapshot(),
|
||
after: snapshot(),
|
||
partnership_version: 4,
|
||
matching: {
|
||
score_min: 0,
|
||
score_max: 0,
|
||
eligibility: 'unknown',
|
||
algorithm_version: 'synthetic-v1',
|
||
weights_hash: 'synthetic-hash',
|
||
unknown_weight: 0,
|
||
criteria: [
|
||
{
|
||
code: 'capacity',
|
||
label: 'SYNTHETIC frozen label',
|
||
weight: 20,
|
||
mandatory: true,
|
||
state: 'unknown',
|
||
evidence: 'SYNTHETIC frozen evidence',
|
||
explanation: 'SYNTHETIC frozen explanation',
|
||
},
|
||
],
|
||
},
|
||
});
|
||
const props = () => ({
|
||
candidacy_id: 10,
|
||
entries: [entry()],
|
||
next_url: null,
|
||
first_url: '/parcerias/candidaturas/10/historico',
|
||
older_page: false,
|
||
partnership_href: null,
|
||
dashboard_href: '/parcerias/painel',
|
||
});
|
||
const render = (p = props()) =>
|
||
renderToStaticMarkup(React.createElement(Page, p));
|
||
test('history renders frozen decision parameters, including valid zero scores', () => {
|
||
const html = render();
|
||
for (const value of [
|
||
'0–0%',
|
||
'Peso: 20',
|
||
'Obrigatório',
|
||
'SYNTHETIC frozen label',
|
||
'SYNTHETIC frozen evidence',
|
||
'SYNTHETIC frozen explanation',
|
||
'synthetic-v1',
|
||
'synthetic-hash',
|
||
])
|
||
assert.ok(html.includes(value), value);
|
||
assert.ok(!html.includes('Por determinar'));
|
||
});
|
||
test('missing frozen scores and obligations remain unknown instead of false or zero', () => {
|
||
const p = props();
|
||
p.entries[0].matching.score_min = null;
|
||
p.entries[0].matching.criteria[0].mandatory = null;
|
||
p.entries[0].matching.criteria[0].weight = null;
|
||
const html = render(p);
|
||
assert.ok(html.includes('Por determinar'));
|
||
assert.ok(html.includes('Obrigatoriedade não registada'));
|
||
assert.ok(!html.includes('0–0%'));
|
||
assert.ok(!html.includes('Não obrigatório'));
|
||
});
|
||
test('old incomplete events are explicit and never supplied from a current proposal', () => {
|
||
const p = props();
|
||
p.entries[0].after = {
|
||
availability: 'available',
|
||
complete: false,
|
||
fields: [],
|
||
assessment: [],
|
||
assessment_recorded: false,
|
||
scope_known: false,
|
||
};
|
||
p.entries[0].matching = null;
|
||
p.entries[0].action = null;
|
||
const html = render(p);
|
||
for (const value of [
|
||
'Registo parcial',
|
||
'Não consta do registo',
|
||
'Âmbito não registado',
|
||
'Não foi recalculado',
|
||
'Alteração registada',
|
||
])
|
||
assert.ok(html.includes(value), value);
|
||
});
|
||
test('missing and intentionally empty fields or consent are distinguished', () => {
|
||
const html = render();
|
||
assert.match(html, /Capacidade disponível<\/dt><dd[^>]*>Não indicado/);
|
||
assert.match(
|
||
html,
|
||
/Contribuição proposta<\/dt><dd[^>]*>Não consta do registo/,
|
||
);
|
||
assert.ok(html.includes('Não autorizada'));
|
||
assert.ok(html.includes('Sem critérios avaliados nesta versão'));
|
||
});
|
||
test('restricted scope and missing legacy scope have different explanations', () => {
|
||
const p = props();
|
||
p.entries = [
|
||
{
|
||
...entry(),
|
||
restricted: true,
|
||
previous: { availability: 'restricted' },
|
||
after: { availability: 'scope_unknown' },
|
||
matching: null,
|
||
actor_name: null,
|
||
reason: null,
|
||
},
|
||
];
|
||
const html = render(p);
|
||
assert.ok(html.includes('Conteúdo reservado'));
|
||
assert.ok(html.includes('acesso actual'));
|
||
assert.ok(html.includes('âmbito desta versão antiga'));
|
||
assert.ok(!html.includes('SYNTHETIC owner'));
|
||
assert.ok(!html.includes('SYNTHETIC proposal'));
|
||
assert.ok(!html.includes('Correspondência guardada'));
|
||
});
|
||
test('read-only page has no write or restore controls and uses only supplied cursor navigation', () => {
|
||
const p = props();
|
||
p.next_url = '/parcerias/candidaturas/10/historico?before=opaque%2Btoken';
|
||
p.older_page = true;
|
||
const html = render(p);
|
||
assert.ok(html.includes('before=opaque%2Btoken'));
|
||
assert.ok(html.includes('Versões mais recentes'));
|
||
assert.ok(!/<(?:form|button|input|select|textarea)\b/.test(html));
|
||
assert.ok(!html.includes('Consultar parceria'));
|
||
p.partnership_href = '/parcerias/1';
|
||
assert.ok(render(p).includes('href="/parcerias/1"'));
|
||
});
|
||
test('saved user text is escaped and current catalogue names are identified', () => {
|
||
const p = props();
|
||
p.entries[0].after.fields.proposal = '<script>synthetic()</script>';
|
||
p.entries[0].after.assessment = [
|
||
{
|
||
code: 'capacity',
|
||
label: 'SYNTHETIC current catalogue',
|
||
state: 'unknown',
|
||
evidence: 'SYNTHETIC recorded evidence',
|
||
},
|
||
];
|
||
const html = render(p);
|
||
assert.ok(!html.includes('<script>'));
|
||
assert.ok(html.includes('<script>'));
|
||
assert.ok(html.includes('catálogo actual'));
|
||
assert.ok(html.includes('cadastro actual'));
|
||
assert.ok(html.includes('SYNTHETIC recorded evidence'));
|
||
});
|
||
test('candidate dashboard links directly to each history without relying on proposal publication', () => {
|
||
const html = renderToStaticMarkup(
|
||
React.createElement(Dashboard, {
|
||
mine: [],
|
||
interests: [
|
||
{
|
||
id: 10,
|
||
partnership_id: 1,
|
||
title: 'SYNTHETIC',
|
||
status: 'withdrawn',
|
||
room_id: null,
|
||
},
|
||
],
|
||
moderation: [],
|
||
notifications: [],
|
||
}),
|
||
);
|
||
assert.ok(html.includes('href="/parcerias/candidaturas/10/historico"'));
|
||
assert.ok(html.includes('Consultar versões e decisões'));
|
||
});
|