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

245 lines
8.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
harness,
props,
text,
nodes,
} from './helpers/partnership-candidacy.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('detail presents the publication decision using business language', () => {
const h = harness(
props({
owner: true,
historySummary: {
limit: 5,
has_more: false,
href: '/parcerias/1/historico',
entries: [
{
version: 5,
restricted: false,
action: 'edit',
actor_name: 'SYNTHETIC editor',
occurred_at: '2026-11-03',
reason: 'SYNTHETIC reviewed content',
status_before: 'draft',
status_after: 'draft',
},
],
},
}),
);
assert.match(text(h.tree), /Edição do rascunho/);
});
const entry = () => ({
version: 4,
restricted: false,
action: 'edit',
actor_name: 'SYNTHETIC editor',
occurred_at: '2026-11-03',
reason: 'SYNTHETIC saved reason',
status_before: 'draft',
status_after: 'draft',
});
const summary = () => ({
limit: 5,
has_more: false,
href: '/parcerias/1/historico',
entries: [entry()],
});
const section = (h) =>
nodes(h.tree).find(
(node) => node.props?.['aria-label'] === 'Histórico e decisões',
);
const reasonField = (h) =>
nodes(
nodes(h.tree).find(
(node) =>
node.type === 'label' && node.props.label === 'Motivo da ação',
),
).find((node) => node.type === 'textarea');
test('reserved metadata never renders even if extra private fields are supplied', () => {
const value = summary();
value.entries[0] = {
...entry(),
restricted: true,
actor_name: 'SYNTHETIC PRIVATE actor',
occurred_at: 'SYNTHETIC PRIVATE date',
reason: 'SYNTHETIC PRIVATE reason',
action: 'publish',
status_before: 'SYNTHETIC PRIVATE state',
status_after: 'published',
actor_id: 999999,
};
const h = harness(props({ owner: true, historySummary: value }));
assert.match(text(section(h)), /Conteúdo reservado/);
assert.doesNotMatch(text(section(h)), /PRIVATE|999999|Publicação/);
});
test('public and candidate details do not display a supplied private summary or its history link', () => {
const h = harness(
props({ owner: false, moderator: false, historySummary: summary() }),
);
assert.equal(section(h), undefined);
assert.equal(
nodes(h.tree).some(
(node) => node.props?.href === '/parcerias/1/historico',
),
false,
);
assert.doesNotMatch(text(h.tree), /SYNTHETIC saved reason/);
});
test('empty and limited summaries explain their scope and have only one complete-history link', () => {
const value = { ...summary(), entries: [] };
const h = harness(props({ owner: true, historySummary: value }));
assert.match(text(section(h)), /Ainda não existem alterações guardadas/);
assert.doesNotMatch(text(section(h)), /Existem alterações anteriores/);
assert.equal(
nodes(h.tree).filter((node) => node.props?.href === value.href).length,
1,
);
h.render({
...h.props,
historySummary: { ...summary(), limit: 1, has_more: true },
});
assert.match(text(section(h)), /Até 1 alteração mais recente/);
assert.match(text(section(h)), /Existem alterações anteriores/);
assert.equal(
nodes(h.tree).filter((node) => node.props?.href === value.href).length,
1,
);
});
test('legacy unknown actions and states are not presented as technical identifiers or inferred decisions', () => {
const value = summary();
value.entries[0] = {
...entry(),
version: null,
action: 'partnership_opportunity.changed.v1',
status_before: null,
status_after: 'legacy_internal_status_v2',
actor_name: null,
occurred_at: null,
reason: null,
actor_id: 999999,
};
const h = harness(props({ owner: true, historySummary: value }));
assert.match(text(section(h)), /Versão não registada/);
assert.match(text(section(h)), /Alteração registada/);
assert.match(text(section(h)), /Autor não registado/);
assert.match(text(section(h)), /Estado não identificado/);
assert.doesNotMatch(
text(section(h)),
/partnership_opportunity|legacy_internal|999999/,
);
});
test('refreshing the summary keeps the prepared publication reason and the existing leave guard', () => {
const h = harness(
props({
owner: true,
canEdit: true,
publicationActions: ['submit', 'archive'],
record: { ...props().record, status: 'draft' },
historySummary: summary(),
}),
);
reasonField(h).props.onChange({
target: { value: 'SYNTHETIC unsent decision reason' },
});
h.render();
h.click('Comparar com a proposta actual');
assert.ok(h.reloads.at(-1).only.includes('historySummary'));
h.refreshed({
...h.props,
historySummary: { ...summary(), has_more: true },
});
h.click('Comparar com os dados actuais da candidatura');
for (const key of [
'historySummary',
'moderator',
'publicationActions',
'publicationCommands',
])
assert.ok(h.reloads.at(-1).only.includes(key), key);
h.refreshed({ ...h.props, historySummary: summary() });
assert.equal(
reasonField(h).props.value,
'SYNTHETIC unsent decision reason',
);
assert.equal(h.leave(), true);
assert.equal(h.sent.length, 0);
assert.equal(
nodes(h.tree).filter(
(node) => node.props?.href === '/parcerias/1/historico',
).length,
1,
);
});
test('summary strings are escaped by the actual React component', () => {
const require = createRequire(import.meta.url);
const modules = {};
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 module = { exports: {} };
const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
compileFunction(code, ['require', 'module', 'exports'])(
(id) =>
modules[id] ??
(id === '@inertiajs/react'
? {
Link: ({ children, ...p }) =>
React.createElement('a', p, children),
}
: id === '@/pages/partnerships/shared'
? { labels }
: require(id)),
module,
module.exports,
);
return module.exports;
}
modules['@/lib/partnership-history'] = load(
'resources/js/lib/partnership-history.ts',
);
const Component = load(
'resources/js/components/partnership-publication-summary.tsx',
).default;
const value = summary();
value.entries[0].reason = '<script>SYNTHETIC</script>';
value.entries[0].actor_name = '<a>SYNTHETIC actor</a>';
const html = renderToStaticMarkup(
React.createElement(Component, { value }),
);
assert.ok(html.includes('&lt;script&gt;SYNTHETIC&lt;/script&gt;'));
assert.ok(html.includes('&lt;a&gt;SYNTHETIC actor&lt;/a&gt;'));
assert.doesNotMatch(html, /<script>|<a>SYNTHETIC/);
});
test('losing the owner or moderator context hides a stale summary without exposing its metadata', () => {
const h = harness(props({ moderator: true, historySummary: summary() }));
assert.ok(section(h));
h.render({ ...h.props, moderator: false, owner: false });
assert.equal(section(h), undefined);
assert.doesNotMatch(text(h.tree), /SYNTHETIC saved reason/);
});