245 lines
8.3 KiB
JavaScript
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('<script>SYNTHETIC</script>'));
|
|
assert.ok(html.includes('<a>SYNTHETIC actor</a>'));
|
|
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/);
|
|
});
|