350 lines
12 KiB
JavaScript
350 lines
12 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 { renderToStaticMarkup } from 'react-dom/server';
|
|
import ts from 'typescript';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
function load(file, mocks, suffix = '') {
|
|
const output = ts.transpileModule(
|
|
fs.readFileSync(new URL(file, import.meta.url), 'utf8') + suffix,
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ['require', 'module', 'exports'])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
function nodes(tree) {
|
|
if (!tree || typeof tree !== 'object') return [];
|
|
if (Array.isArray(tree)) return tree.flatMap(nodes);
|
|
return [tree, ...nodes(tree.props?.children)];
|
|
}
|
|
function form(initial) {
|
|
return {
|
|
data: initial,
|
|
errors: {},
|
|
processing: false,
|
|
setData(value, next) {
|
|
this.data =
|
|
typeof value === 'function'
|
|
? value(this.data)
|
|
: { ...this.data, [value]: next };
|
|
},
|
|
};
|
|
}
|
|
const service = {
|
|
code: 'project-change',
|
|
version: 2,
|
|
effective_from: '2026-11-12',
|
|
activeVersion: 1,
|
|
definition: {
|
|
title: 'SYNTHETIC scheduled form',
|
|
theme: 'QA',
|
|
profile: 'Investor',
|
|
entity: 'Internal',
|
|
result: 'Internal preparation',
|
|
stage: 'E10',
|
|
fields: {
|
|
question: {
|
|
label: 'Synthetic question',
|
|
type: 'text',
|
|
required: false,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
function editorHarness() {
|
|
let saved, sent;
|
|
const Editor = load(
|
|
'../../resources/js/pages/desk/service-editor.tsx',
|
|
{
|
|
react: { useState: (initial) => [initial, () => {}] },
|
|
'@inertiajs/react': {
|
|
Link: 'a',
|
|
useForm(key, initial) {
|
|
saved ??= {
|
|
...form(initial),
|
|
put(url) {
|
|
sent = { url, data: structuredClone(this.data) };
|
|
},
|
|
};
|
|
return saved;
|
|
},
|
|
},
|
|
'@/pages/investment/shared': {
|
|
Frame: 'main',
|
|
card: '',
|
|
input: '',
|
|
button: '',
|
|
},
|
|
'@/components/desk-service-history': { default: () => null },
|
|
},
|
|
'\nexport { Editor };',
|
|
).Editor;
|
|
return {
|
|
render: (props = {}) =>
|
|
Editor({
|
|
service,
|
|
schedulingAvailable: true,
|
|
today: '2026-11-02',
|
|
calendarTimezone: 'UTC',
|
|
...props,
|
|
}),
|
|
data: () => saved,
|
|
sent: () => sent,
|
|
};
|
|
}
|
|
|
|
await test('editing a scheduled edition keeps its future date until changed deliberately and date-only changes require fresh confirmation', () => {
|
|
const before = JSON.stringify(service);
|
|
const h = editorHarness();
|
|
let tree = h.render();
|
|
assert.equal(h.data().data.effective_from, '2026-11-12');
|
|
const date = nodes(tree).find(
|
|
(node) => node.type === 'input' && node.props.type === 'date',
|
|
);
|
|
assert.equal(date.props.min, '2026-11-02');
|
|
h.data().data.confirm = true;
|
|
date.props.onChange({ target: { value: '2026-11-02' } });
|
|
assert.equal(h.data().data.confirm, false);
|
|
tree = h.render();
|
|
let html = renderToStaticMarkup(tree);
|
|
assert.match(html, /Versão actualmente disponível para novos pedidos: 1/);
|
|
assert.match(html, /Entrada em vigor proposta: 2026-11-02/);
|
|
assert.match(html, /não cancela uma edição já agendada/);
|
|
assert.match(html, /Calendário do portal: UTC/);
|
|
h.data().data.confirm = true;
|
|
tree = h.render();
|
|
const publish = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Publicar nova versão',
|
|
);
|
|
assert.equal(publish.props.disabled, false);
|
|
tree.props.onSubmit({ preventDefault() {} });
|
|
assert.equal(h.sent().data.effective_from, '2026-11-02');
|
|
assert.equal(h.sent().data.base_version, 2);
|
|
assert.equal(JSON.stringify(service), before);
|
|
date.props.onChange({ target: { value: '' } });
|
|
html = renderToStaticMarkup(h.render());
|
|
assert.match(html, /Entrada em vigor proposta: 2026-11-02/);
|
|
});
|
|
|
|
await test('destinations without the migration retain immediate publication and cannot silently present a scheduling control', () => {
|
|
const h = editorHarness();
|
|
const tree = h.render({ schedulingAvailable: false });
|
|
assert.equal(h.data().data.effective_from, '');
|
|
assert.equal(
|
|
nodes(tree).some(
|
|
(node) => node.type === 'input' && node.props.type === 'date',
|
|
),
|
|
false,
|
|
);
|
|
assert.match(
|
|
renderToStaticMarkup(tree),
|
|
/agendamento requer a actualização/,
|
|
);
|
|
});
|
|
|
|
await test('advancing the calendar does not silently propose an unchanged edition, and upcoming editions remain visible', () => {
|
|
const h = editorHarness();
|
|
const props = {
|
|
service: {
|
|
...service,
|
|
effective_from: '2026-11-01',
|
|
activeVersion: 2,
|
|
upcoming: [{ version: 1, effective_from: '2026-11-12' }],
|
|
},
|
|
};
|
|
let tree = h.render(props);
|
|
h.data().data.confirm = true;
|
|
tree = h.render(props);
|
|
let publish = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Publicar nova versão',
|
|
);
|
|
assert.equal(publish.props.disabled, true);
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.match(html, /Edições já agendadas/);
|
|
assert.match(html, /Versão 1: 2026-11-12/);
|
|
nodes(tree)
|
|
.find((node) => node.type === 'input' && node.props.type === 'date')
|
|
.props.onChange({ target: { value: '2026-11-02' } });
|
|
h.data().data.confirm = true;
|
|
tree = h.render(props);
|
|
publish = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Publicar nova versão',
|
|
);
|
|
assert.equal(publish.props.disabled, false);
|
|
});
|
|
|
|
await test('refreshing services retains other case forms and requires an explicit choice when the selected edition is no longer listed', () => {
|
|
let cursor = 0,
|
|
stateCursor = 0,
|
|
refreshed,
|
|
submitted;
|
|
const forms = [],
|
|
states = [];
|
|
const Case = load('../../resources/js/pages/desk/case.tsx', {
|
|
react: {
|
|
useState(initial) {
|
|
const index = stateCursor++;
|
|
if (!(index in states)) states[index] = initial;
|
|
return [
|
|
states[index],
|
|
(value) => {
|
|
states[index] =
|
|
typeof value === 'function'
|
|
? value(states[index])
|
|
: value;
|
|
},
|
|
];
|
|
},
|
|
useRef(initial) {
|
|
const index = stateCursor++;
|
|
states[index] ??= { current: initial };
|
|
return states[index];
|
|
},
|
|
},
|
|
'@/lib/operation-key': { createOperationKey: () => 'qa-operation' },
|
|
'@inertiajs/react': {
|
|
Link: 'a',
|
|
usePage: () => ({
|
|
props: { auth: { user: { id: 3 } } },
|
|
url: '/balcao-virtual/dossiers/qa',
|
|
}),
|
|
router: {
|
|
reload(options) {
|
|
refreshed = options;
|
|
},
|
|
},
|
|
useForm(initial) {
|
|
const index = cursor++;
|
|
forms[index] ??= {
|
|
...form(initial),
|
|
post(url) {
|
|
submitted = { url, data: this.data };
|
|
},
|
|
};
|
|
return forms[index];
|
|
},
|
|
},
|
|
'@/pages/investment/shared': {
|
|
Frame: 'main',
|
|
card: '',
|
|
input: '',
|
|
button: '',
|
|
},
|
|
'@/components/investor-guidance': { InvestorGuidance: () => null },
|
|
'@/components/desk-case-state-form': { default: () => null },
|
|
'@/components/desk-document-picker': { default: () => null },
|
|
'@/components/desk-internal-operations': { DocumentReview: () => null },
|
|
}).default;
|
|
const props = {
|
|
case: { id: 'qa', title: 'Synthetic case', version: 1, workflow: {} },
|
|
canWrite: true,
|
|
manager: false,
|
|
requests: [],
|
|
services: [{ id: 'v1', title: 'Current', version: 1 }],
|
|
documents: [],
|
|
workflow: {},
|
|
stepStates: {},
|
|
states: {},
|
|
guidance: null,
|
|
events: [],
|
|
links: [],
|
|
legacyRequest: null,
|
|
};
|
|
const render = (services) => {
|
|
cursor = 0;
|
|
stateCursor = 0;
|
|
return Case({ ...props, services });
|
|
};
|
|
let tree = render(props.services);
|
|
forms[1].data.reason = 'Unsaved stage evidence';
|
|
tree = render([
|
|
{
|
|
id: 'v2',
|
|
title: 'New current',
|
|
version: 2,
|
|
effective_from: '2026-11-03',
|
|
},
|
|
]);
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.match(html, /versão seleccionada já não está na lista/);
|
|
assert.match(html, /desde 2026-11-03/);
|
|
const create = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Preparar rascunho de pedido',
|
|
);
|
|
assert.equal(create.props.disabled, true);
|
|
const requestForm = nodes(tree).find(
|
|
(node) => node.type === 'form' && nodes(node).includes(create),
|
|
);
|
|
requestForm.props.onSubmit({ preventDefault() {} });
|
|
assert.equal(submitted, undefined);
|
|
nodes(tree)
|
|
.find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Actualizar versões disponíveis',
|
|
)
|
|
.props.onClick();
|
|
assert.deepEqual(refreshed.only, ['services']);
|
|
assert.equal(forms[1].data.reason, 'Unsaved stage evidence');
|
|
const select = nodes(tree).find(
|
|
(node) => node.type === 'select' && node.props.value === 'v1',
|
|
);
|
|
select.props.onChange({ target: { value: 'v2' } });
|
|
tree = render([{ id: 'v2', title: 'New current', version: 2 }]);
|
|
const ready = nodes(tree).find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Preparar rascunho de pedido',
|
|
);
|
|
assert.equal(ready.props.disabled, false);
|
|
});
|
|
|
|
await test('catalogue shows the selected calendar date with the distinction between new and existing requests', () => {
|
|
const Catalogue = load('../../resources/js/pages/desk/catalogue.tsx', {
|
|
'@inertiajs/react': { Link: 'a' },
|
|
'@/pages/investment/shared': { Frame: 'main', card: '' },
|
|
}).default;
|
|
const html = renderToStaticMarkup(
|
|
Catalogue({
|
|
installed: true,
|
|
services: [
|
|
{
|
|
id: 'v2',
|
|
version: 2,
|
|
effective_from: '2026-11-03',
|
|
definition: {
|
|
title: '<Synthetic public form>',
|
|
result: 'Internal preparation',
|
|
},
|
|
},
|
|
],
|
|
prepared: {},
|
|
}),
|
|
);
|
|
assert.match(html, /novos pedidos desde 2026-11-03/);
|
|
assert.match(html, /Pedidos anteriores mantêm/);
|
|
assert.match(html, /<Synthetic public form>/);
|
|
});
|