270 lines
9.3 KiB
JavaScript
270 lines
9.3 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';
|
|
import {
|
|
harness,
|
|
props as detailProps,
|
|
nodes,
|
|
} from './helpers/partnership-candidacy.mjs';
|
|
const require = createRequire(import.meta.url);
|
|
const historyModule = {};
|
|
compileFunction(
|
|
ts.transpileModule(
|
|
fs.readFileSync('resources/js/lib/partnership-history.ts', 'utf8'),
|
|
{ compilerOptions: { module: ts.ModuleKind.CommonJS } },
|
|
).outputText,
|
|
['exports'],
|
|
)(historyModule);
|
|
const labels = compileFunction(
|
|
`return (${fs.readFileSync('resources/js/pages/partnerships/shared.tsx', 'utf8').match(/export const labels: Record<string, string> = (\{[\s\S]*?\n\});/)[1]});`,
|
|
[],
|
|
)();
|
|
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 === '@/lib/partnership-history') return historyModule;
|
|
if (id === './shared')
|
|
return {
|
|
Frame: ({ title, children }) =>
|
|
React.createElement(
|
|
'main',
|
|
{},
|
|
React.createElement('h1', {}, title),
|
|
children,
|
|
),
|
|
labels,
|
|
};
|
|
if (id === '@inertiajs/react')
|
|
return {
|
|
Link: ({ children, ...p }) =>
|
|
React.createElement('a', p, children),
|
|
router: {},
|
|
};
|
|
return require(id);
|
|
},
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports.default;
|
|
}
|
|
const Page = load('resources/js/pages/partnerships/publication-history.tsx'),
|
|
Dashboard = load('resources/js/pages/partnerships/dashboard.tsx');
|
|
const snapshot = () => ({
|
|
availability: 'available',
|
|
version: 2,
|
|
status: 'draft',
|
|
scope_known: true,
|
|
organization_id: null,
|
|
fields: {
|
|
title: 'SYNTHETIC frozen title',
|
|
offer: 'SYNTHETIC frozen offer',
|
|
kind: 'seeking',
|
|
confidentiality: 'private',
|
|
deadline: null,
|
|
},
|
|
sector_recorded: true,
|
|
sector_id: '19',
|
|
consent_known: true,
|
|
consent: false,
|
|
mandatory_known: true,
|
|
mandatory: [],
|
|
origins_recorded: true,
|
|
origins: [{ type: 'project', target_id: 'synthetic-project-id' }],
|
|
locations_recorded: true,
|
|
locations: [
|
|
{
|
|
geo_location_id: 'synthetic-location-id',
|
|
role: 'coverage',
|
|
source_reference: 'SYNTHETIC recorded source',
|
|
},
|
|
],
|
|
complete: true,
|
|
});
|
|
const entry = () => ({
|
|
version: 3,
|
|
restricted: false,
|
|
action: 'edit',
|
|
actor_name: 'SYNTHETIC actor',
|
|
occurred_at: '2026-11-03',
|
|
reason: null,
|
|
previous: snapshot(),
|
|
after: {
|
|
...snapshot(),
|
|
version: 3,
|
|
fields: { ...snapshot().fields, title: 'SYNTHETIC changed title' },
|
|
},
|
|
});
|
|
const props = () => ({
|
|
partnership_id: 1,
|
|
entries: [entry()],
|
|
first_url: '/parcerias/1/historico',
|
|
next_url: null,
|
|
older_page: false,
|
|
partnership_href: '/parcerias/1',
|
|
dashboard_href: '/parcerias/painel',
|
|
});
|
|
const render = (p) => renderToStaticMarkup(React.createElement(Page, p));
|
|
test('publication history compares saved before and after values with recorded references', () => {
|
|
const html = render(props());
|
|
for (const value of [
|
|
'SYNTHETIC frozen title',
|
|
'SYNTHETIC changed title',
|
|
'SYNTHETIC frozen offer',
|
|
'SYNTHETIC recorded source',
|
|
'synthetic-project-id',
|
|
'synthetic-location-id',
|
|
'Edição do rascunho',
|
|
'Antes da alteração',
|
|
'Após a alteração',
|
|
])
|
|
assert.ok(html.includes(value), value);
|
|
assert.ok(!html.includes('href="synthetic-project-id"'));
|
|
});
|
|
test('empty values consent and source lists are distinct from absent or malformed records', () => {
|
|
const p = props();
|
|
p.entries[0].after.fields.offer = '';
|
|
p.entries[0].after.origins = [];
|
|
p.entries[0].previous.origins = [];
|
|
p.entries[0].previous.origins_incomplete = true;
|
|
p.entries[0].previous.complete = false;
|
|
const html = render(p);
|
|
assert.match(html, /O que oferece<\/dt><dd[^>]*>Não indicado/);
|
|
assert.match(html, /Requisitos<\/dt><dd[^>]*>Não consta do registo/);
|
|
assert.ok(html.includes('Não autorizada'));
|
|
assert.ok(html.includes('Sem critérios obrigatórios nesta versão'));
|
|
assert.ok(html.includes('Há referências incompletas'));
|
|
assert.ok(html.includes('Sem origens ligadas nesta versão'));
|
|
});
|
|
test('scope restriction and unknown historical identity are explained without actor or reason disclosure', () => {
|
|
const p = props();
|
|
p.entries = [
|
|
{
|
|
...entry(),
|
|
restricted: true,
|
|
actor_name: null,
|
|
reason: null,
|
|
previous: { availability: 'restricted' },
|
|
after: { availability: 'scope_unknown' },
|
|
},
|
|
];
|
|
const html = render(p);
|
|
assert.ok(html.includes('Conteúdo reservado'));
|
|
assert.ok(html.includes('acesso actual'));
|
|
assert.ok(html.includes('identidade desta versão antiga'));
|
|
assert.ok(!html.includes('SYNTHETIC actor'));
|
|
assert.ok(!html.includes('SYNTHETIC frozen title'));
|
|
});
|
|
test('legacy partial states do not imply personal scope or current proposal content', () => {
|
|
const p = props();
|
|
p.entries[0] = {
|
|
...entry(),
|
|
action: null,
|
|
previous: { availability: 'not_recorded' },
|
|
after: {
|
|
availability: 'available',
|
|
complete: false,
|
|
scope_known: false,
|
|
status: 'returned',
|
|
fields: [],
|
|
origins_recorded: false,
|
|
locations_recorded: false,
|
|
},
|
|
};
|
|
const html = render(p);
|
|
for (const value of [
|
|
'Alteração registada',
|
|
'Registo parcial',
|
|
'Âmbito não registado',
|
|
'Não constam do registo',
|
|
])
|
|
assert.ok(html.includes(value), value);
|
|
assert.ok(!html.includes('Em nome pessoal'));
|
|
assert.ok(!html.includes('SYNTHETIC changed title'));
|
|
});
|
|
test('read-only navigation uses opaque cursors without restoration or publication commands', () => {
|
|
const p = props();
|
|
p.older_page = true;
|
|
p.next_url = '/parcerias/1/historico?before=opaque%2Bcursor';
|
|
const html = render(p);
|
|
assert.ok(html.includes('before=opaque%2Bcursor'));
|
|
assert.ok(html.includes('Versões mais recentes'));
|
|
assert.ok(!/<(?:form|button|input|select|textarea)\b/.test(html));
|
|
assert.ok(!html.includes('/estado'));
|
|
});
|
|
test('stored text is escaped and current registry names are explicitly identified', () => {
|
|
const p = props();
|
|
p.entries[0].after.fields.offer = '<script>synthetic()</script>';
|
|
p.entries[0].reason = '<b>synthetic reason</b>';
|
|
const html = render(p);
|
|
assert.ok(!html.includes('<script>'));
|
|
assert.ok(html.includes('<script>'));
|
|
assert.ok(html.includes('<b>synthetic reason</b>'));
|
|
assert.ok(html.includes('cadastro actual'));
|
|
assert.ok(html.includes('não restaura nem volta a publicar'));
|
|
});
|
|
test('actual proposal detail offers owner history without erasing prepared publication reason', () => {
|
|
const h = harness(
|
|
detailProps({
|
|
owner: true,
|
|
canEdit: true,
|
|
publicationActions: ['archive'],
|
|
}),
|
|
);
|
|
const field = nodes(h.tree).find(
|
|
(n) => n.type === 'label' && n.props.label === 'Motivo da ação',
|
|
);
|
|
nodes(field)
|
|
.find((n) => n.type === 'textarea')
|
|
.props.onChange({ target: { value: 'SYNTHETIC unsent reason' } });
|
|
h.render();
|
|
assert.ok(
|
|
nodes(h.tree).some((n) => n.props?.href === '/parcerias/1/historico'),
|
|
);
|
|
assert.equal(h.leave(), true);
|
|
const publicDetail = harness(
|
|
detailProps({ owner: false, moderator: false }),
|
|
);
|
|
assert.ok(
|
|
!nodes(publicDetail.tree).some(
|
|
(n) => n.props?.href === '/parcerias/1/historico',
|
|
),
|
|
);
|
|
});
|
|
test('dashboard links own and moderation proposals to history without granting a candidate that link', () => {
|
|
const html = renderToStaticMarkup(
|
|
React.createElement(Dashboard, {
|
|
mine: [{ id: 1, title: 'SYNTHETIC mine', status: 'archived' }],
|
|
moderation: [
|
|
{ id: 2, title: 'SYNTHETIC pending', status: 'submitted' },
|
|
],
|
|
interests: [
|
|
{
|
|
id: 10,
|
|
partnership_id: 3,
|
|
title: null,
|
|
partnership_href: null,
|
|
status: 'submitted',
|
|
room_id: null,
|
|
},
|
|
],
|
|
notifications: [],
|
|
}),
|
|
);
|
|
assert.ok(html.includes('href="/parcerias/1/historico"'));
|
|
assert.ok(html.includes('href="/parcerias/2/historico"'));
|
|
assert.ok(!html.includes('href="/parcerias/3/historico"'));
|
|
assert.ok(html.includes('href="/parcerias/candidaturas/10/historico"'));
|
|
});
|