285 lines
9.8 KiB
JavaScript
285 lines
9.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { harness, props, text } from './helpers/partnership-room.mjs';
|
|
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';
|
|
|
|
test('a legacy instrument without a confirmation map remains readable without inventing pending confirmations', () => {
|
|
for (const confirmations of [undefined, null, 'SYNTHETIC malformed map']) {
|
|
const initial = props();
|
|
initial.room.agreement.confirmations = confirmations;
|
|
const h = harness(initial);
|
|
assert.match(text(h.tree), /SYNTHETIC instrument/);
|
|
assert.match(text(h.tree), /confirmações.*(não.*registad|estrutura)/i);
|
|
assert.doesNotMatch(text(h.tree), /Confirmações: Pendentes/);
|
|
}
|
|
});
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const modules = {};
|
|
const shared = fs.readFileSync(
|
|
'resources/js/pages/partnerships/shared.tsx',
|
|
'utf8',
|
|
);
|
|
const labels = compileFunction(
|
|
`return (${shared.match(/export const labels: Record<string, string> = (\{[\s\S]*?\n\});/)[1]});`,
|
|
[],
|
|
)();
|
|
function load(file) {
|
|
const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
}).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(code, ['require', 'module', 'exports'])(
|
|
(name) => {
|
|
if (Object.hasOwn(modules, name)) return modules[name];
|
|
if (name === './shared')
|
|
return {
|
|
labels,
|
|
Frame: ({ title, children }) =>
|
|
React.createElement(
|
|
'main',
|
|
{},
|
|
React.createElement('h1', {}, title),
|
|
children,
|
|
),
|
|
};
|
|
if (name === '@inertiajs/react')
|
|
return {
|
|
Link: ({ children, ...attributes }) =>
|
|
React.createElement('a', attributes, children),
|
|
};
|
|
return require(name);
|
|
},
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
modules['@/lib/partnership-agreement'] = load(
|
|
'resources/js/lib/partnership-agreement.ts',
|
|
);
|
|
modules['@/components/partnership-agreement-instrument'] = load(
|
|
'resources/js/components/partnership-agreement-instrument.tsx',
|
|
);
|
|
const Instrument =
|
|
modules['@/components/partnership-agreement-instrument'].default;
|
|
const Page = load(
|
|
'resources/js/pages/partnerships/agreement-history.tsx',
|
|
).default;
|
|
const instrument = () => ({
|
|
title: 'SYNTHETIC frozen title',
|
|
terms: 'SYNTHETIC frozen terms',
|
|
confirmations: {},
|
|
legal_signature_status: 'not_verified',
|
|
});
|
|
const snapshot = () => ({
|
|
availability: 'available',
|
|
version: 2,
|
|
status: 'agreement_recorded',
|
|
instrument_availability: 'available',
|
|
instrument: instrument(),
|
|
instrument_partnership_version: 4,
|
|
instrument_interest_version: 3,
|
|
});
|
|
const entry = () => ({
|
|
version: 2,
|
|
restricted: false,
|
|
action: 'record',
|
|
actor_name: 'SYNTHETIC actor',
|
|
occurred_at: '2026-11-03',
|
|
reason: 'SYNTHETIC reason',
|
|
confirmation_party: null,
|
|
partnership_version: 4,
|
|
interest_version: 3,
|
|
previous: { availability: 'not_recorded' },
|
|
after: snapshot(),
|
|
});
|
|
const history = () => ({
|
|
room_id: 1,
|
|
entries: [entry()],
|
|
first_url: '/parcerias/salas/1/historico',
|
|
next_url: null,
|
|
older_page: false,
|
|
room_href: '/parcerias/salas/1',
|
|
dashboard_href: '/parcerias/painel',
|
|
});
|
|
const render = (p) => renderToStaticMarkup(React.createElement(Page, p));
|
|
|
|
test('the stored empty array is an empty confirmation map while nonempty lists stay unrecognized', () => {
|
|
const value = instrument();
|
|
value.confirmations = [];
|
|
assert.match(
|
|
renderToStaticMarkup(React.createElement(Instrument, { value })),
|
|
/Confirmações: Pendentes/,
|
|
);
|
|
value.confirmations = [{ actor_id: 1 }];
|
|
assert.match(
|
|
renderToStaticMarkup(React.createElement(Instrument, { value })),
|
|
/não têm uma estrutura reconhecida/,
|
|
);
|
|
});
|
|
|
|
test('invalid instrument fields never become React children or fabricated confirmations', () => {
|
|
for (const value of [
|
|
false,
|
|
['SYNTHETIC invalid'],
|
|
{
|
|
title: { secret: 'SYNTHETIC PRIVATE' },
|
|
terms: ['SYNTHETIC PRIVATE'],
|
|
confirmations: null,
|
|
},
|
|
]) {
|
|
const html = renderToStaticMarkup(
|
|
React.createElement(Instrument, { value }),
|
|
);
|
|
assert.doesNotMatch(
|
|
html,
|
|
/SYNTHETIC PRIVATE|\[object Object\]|Pendentes/,
|
|
);
|
|
assert.match(html, /estrutura reconhecida|Título não registado/);
|
|
}
|
|
});
|
|
|
|
test('incomplete and unknown confirmation references do not impersonate a party', () => {
|
|
const initial = props();
|
|
initial.room.agreement.confirmations = {
|
|
proponent: { actor_id: 'wrong', date: {}, reason: [] },
|
|
unknown: { actor_id: 2 },
|
|
};
|
|
const h = harness(initial);
|
|
assert.match(text(h.tree), /Registo de confirmação incompleto/);
|
|
assert.match(text(h.tree), /não substituem a confirmação de cada parte/);
|
|
assert.ok(h.button('Confirmar pela minha parte'));
|
|
assert.equal(h.button('Actualizar confirmação da minha parte'), undefined);
|
|
assert.doesNotMatch(text(h.tree), /\[object Object\]/);
|
|
});
|
|
|
|
test('history renders frozen before and after instruments with their decision context and no write actions', () => {
|
|
const p = history();
|
|
p.entries[0].previous = snapshot();
|
|
p.entries[0].previous.instrument.title = 'SYNTHETIC previous title';
|
|
p.entries[0].after.instrument.confirmations = {
|
|
candidate: {
|
|
actor_id: 2,
|
|
actor_name: 'SYNTHETIC candidate',
|
|
reason: 'SYNTHETIC confirmation reason',
|
|
date: '2026-11-03',
|
|
},
|
|
};
|
|
p.entries[0].confirmation_party = 'candidate';
|
|
const html = render(p);
|
|
for (const fragment of [
|
|
'SYNTHETIC previous title',
|
|
'SYNTHETIC frozen title',
|
|
'SYNTHETIC candidate',
|
|
'SYNTHETIC confirmation reason',
|
|
'Contexto da decisão: proposta 4',
|
|
'candidatura 3',
|
|
'não verificada',
|
|
'Parte que confirmou: Candidata',
|
|
])
|
|
assert.ok(html.includes(fragment), fragment);
|
|
assert.doesNotMatch(html, /<form|<button|Registar versão do acordo/);
|
|
});
|
|
|
|
test('restricted and unknown history states never render supplied private fields', () => {
|
|
for (const availability of [
|
|
'restricted',
|
|
'scope_unknown',
|
|
'not_recorded',
|
|
'malformed',
|
|
]) {
|
|
const p = history();
|
|
p.entries[0].restricted = true;
|
|
p.entries[0].reason = 'SYNTHETIC HIDDEN reason';
|
|
p.entries[0].actor_name = 'SYNTHETIC HIDDEN actor';
|
|
p.entries[0].previous = {
|
|
...snapshot(),
|
|
availability,
|
|
instrument: { title: 'SYNTHETIC HIDDEN instrument' },
|
|
};
|
|
p.entries[0].after = {
|
|
...snapshot(),
|
|
instrument_availability: availability,
|
|
instrument: { title: 'SYNTHETIC HIDDEN instrument' },
|
|
};
|
|
const html = render(p);
|
|
assert.doesNotMatch(html, /SYNTHETIC HIDDEN/);
|
|
assert.match(html, /Conteúdo reservado/);
|
|
}
|
|
});
|
|
|
|
test('legacy absence is explicit and history never fills it with a current instrument', () => {
|
|
const p = history();
|
|
p.entries[0].after = { availability: 'not_recorded' };
|
|
const html = render(p);
|
|
assert.match(html, /Não foi preenchida com a versão actual/);
|
|
assert.doesNotMatch(html, /SYNTHETIC frozen/);
|
|
});
|
|
|
|
test('history navigation uses the scoped server URLs and distinguishes older pages', () => {
|
|
const p = history();
|
|
p.older_page = true;
|
|
p.next_url = '/parcerias/salas/1/historico?before=SYNTHETIC-token';
|
|
const html = render(p);
|
|
for (const fragment of [
|
|
'Voltar à sala',
|
|
'Versões mais recentes',
|
|
'Consultar versões anteriores',
|
|
'before=SYNTHETIC-token',
|
|
'href="/parcerias/painel"',
|
|
])
|
|
assert.ok(html.includes(fragment));
|
|
assert.doesNotMatch(html, /<button/);
|
|
});
|
|
|
|
test('an empty history explains the absence without creating a historical instrument', () => {
|
|
const p = history();
|
|
p.entries = [];
|
|
const html = render(p);
|
|
assert.match(html, /Ainda não existem alterações do acordo guardadas/);
|
|
assert.doesNotMatch(
|
|
html,
|
|
/Consultar versões anteriores|Antes da alteração/,
|
|
);
|
|
});
|
|
|
|
test('stored conditions and reasons are escaped and cannot add links or markup', () => {
|
|
const p = history();
|
|
p.entries[0].reason = '<script>SYNTHETIC</script>';
|
|
p.entries[0].after.instrument.terms =
|
|
'<a href="https://example.invalid">SYNTHETIC</a>';
|
|
const html = render(p);
|
|
assert.ok(html.includes('<script>SYNTHETIC</script>'));
|
|
assert.ok(html.includes('<a href='));
|
|
assert.doesNotMatch(html, /<script>|<a href="https:\/\/example.invalid/);
|
|
});
|
|
|
|
test('the room history link shares the existing leave guard without erasing an unsent reason', () => {
|
|
const h = harness(props());
|
|
h.change('agreement', 'Motivo da ação', 'SYNTHETIC unsent reason');
|
|
const link = h
|
|
.all()
|
|
.find(
|
|
(node) =>
|
|
node.type === 'a' &&
|
|
node.props.href ===
|
|
`/parcerias/salas/${h.props.room.id}/historico`,
|
|
);
|
|
assert.ok(link);
|
|
assert.equal(h.leave(), true);
|
|
assert.equal(
|
|
h.field('agreement', 'Motivo da ação').props.value,
|
|
'SYNTHETIC unsent reason',
|
|
);
|
|
assert.equal(h.sent.length, 0);
|
|
});
|