Files
Investidor-V2/tests/frontend/partnership-publication-history.test.mjs
T

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('&lt;script&gt;'));
assert.ok(html.includes('&lt;b&gt;synthetic reason&lt;/b&gt;'));
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"'));
});