400 lines
13 KiB
JavaScript
400 lines
13 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import { compileFunction } from "node:vm";
|
|
import { createRequire } from "node:module";
|
|
import ts from "typescript";
|
|
import React from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
const require = createRequire(import.meta.url);
|
|
function load(file, mocks = {}) {
|
|
const output = ts.transpileModule(
|
|
fs.readFileSync(
|
|
new URL("../../resources/js/" + file, import.meta.url),
|
|
"utf8",
|
|
),
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ["require", "module", "exports"])(
|
|
(name) =>
|
|
name in mocks
|
|
? mocks[name]
|
|
: name === "@/components/funding-classification"
|
|
? load("components/funding-classification.tsx", mocks)
|
|
: name === "@/pages/investment/shared"
|
|
? { card: "card", input: "input", button: "button" }
|
|
: require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.exports;
|
|
}
|
|
const source = {
|
|
source_url: "https://example.invalid/source",
|
|
source_reference: "SYNTHETIC QA source",
|
|
checked_on: "2026-11-10",
|
|
};
|
|
const product = {
|
|
...source,
|
|
code: "qa-investment",
|
|
title: "SYNTHETIC QA investment",
|
|
instrument: "Investment credit",
|
|
needs: ["investment"],
|
|
coverage: "national",
|
|
coverage_text: "SYNTHETIC national coverage",
|
|
territory_ids: [],
|
|
conditions: "",
|
|
amounts: "",
|
|
availability: "unknown",
|
|
contact_url: "https://example.invalid/contact",
|
|
};
|
|
const details = {
|
|
institution_key: "synthetic",
|
|
institution_nature: "bank",
|
|
contacts: [
|
|
{
|
|
...source,
|
|
kind: "website",
|
|
value: "https://example.invalid/contact",
|
|
},
|
|
],
|
|
locations: [
|
|
{
|
|
...source,
|
|
role: "headquarters",
|
|
label: "SYNTHETIC Maputo office",
|
|
address: "SYNTHETIC address",
|
|
territory_id: "qa-maputo",
|
|
territory_path: [{ name: "Maputo" }],
|
|
},
|
|
],
|
|
products: [product],
|
|
};
|
|
test("actual institution view keeps physical address separate from product coverage and exposes unknown terms, source and contact", () => {
|
|
const { InstitutionView } = load("components/funding-institution.tsx");
|
|
const html = renderToStaticMarkup(
|
|
React.createElement(InstitutionView, { value: details }),
|
|
);
|
|
assert.match(html, /Sede: SYNTHETIC Maputo office/);
|
|
assert.match(html, /Nacional, segundo a fonte/);
|
|
assert.match(html, /Condições: Por confirmar/);
|
|
assert.match(html, /Disponibilidade: Por confirmar/);
|
|
assert.match(html, /https:\/\/example.invalid\/contact/);
|
|
assert.match(html, /SYNTHETIC QA source/);
|
|
assert.match(html, /não é elegibilidade confirmada/);
|
|
});
|
|
test("adding a physical location does not fill product financing coverage", () => {
|
|
const { InstitutionFields } = load("components/funding-institution.tsx");
|
|
let patch;
|
|
const tree = InstitutionFields({
|
|
value: details,
|
|
change: (p) => (patch = p),
|
|
territories: [],
|
|
natures: { bank: "Banco" },
|
|
needs: { investment: "Investimento" },
|
|
});
|
|
const nodes = flatten(tree);
|
|
nodes
|
|
.find(
|
|
(n) => n.type === "button" && text(n) === "Acrescentar sede/balcão",
|
|
)
|
|
.props.onClick();
|
|
assert.equal(patch.locations.length, 2);
|
|
assert.equal(patch.locations[1].territory_id, "");
|
|
assert.equal("products" in patch, false);
|
|
assert.equal(details.products[0].coverage, "national");
|
|
});
|
|
const flatten = (node) =>
|
|
Array.isArray(node)
|
|
? node.flatMap(flatten)
|
|
: node && typeof node === "object"
|
|
? [node, ...flatten(node.props?.children)]
|
|
: [];
|
|
const text = (node) =>
|
|
Array.isArray(node)
|
|
? node.map(text).join("")
|
|
: node && typeof node === "object"
|
|
? text(node.props?.children)
|
|
: String(node ?? "");
|
|
|
|
test("assisted editor resumes after renewed authentication without preparing completed fiches or automatically publishing", async () => {
|
|
const previousStorage = globalThis.sessionStorage;
|
|
const tab = new Map();
|
|
globalThis.sessionStorage = {
|
|
getItem: (key) => tab.get(key) ?? null,
|
|
setItem: (key, value) => tab.set(key, value),
|
|
};
|
|
const states = [],
|
|
refs = [],
|
|
calls = [];
|
|
let si = 0,
|
|
ri = 0,
|
|
lost = true;
|
|
const hooks = {
|
|
...React,
|
|
useEffect(fn) {
|
|
fn();
|
|
},
|
|
useState(initial) {
|
|
const i = si++;
|
|
if (!(i in states))
|
|
states[i] = typeof initial === "function" ? initial() : initial;
|
|
return [
|
|
states[i],
|
|
(v) => {
|
|
states[i] = typeof v === "function" ? v(states[i]) : v;
|
|
},
|
|
];
|
|
},
|
|
useRef(initial) {
|
|
const i = ri++;
|
|
return (refs[i] ??= { current: initial });
|
|
},
|
|
};
|
|
const Assisted = load("pages/funding/assisted.tsx", {
|
|
react: hooks,
|
|
"@inertiajs/react": { Link: "a" },
|
|
"@/pages/investment/shared": {
|
|
Frame: "main",
|
|
card: "card",
|
|
button: "button",
|
|
},
|
|
"@/lib/profile-json": {
|
|
profileJson: async (url, body) => {
|
|
calls.push({ url, body: structuredClone(body) });
|
|
if (url.endsWith("/publicar")) {
|
|
if (lost) {
|
|
lost = false;
|
|
throw new Error("SYNTHETIC lost response");
|
|
}
|
|
return { published: true };
|
|
}
|
|
if (url.endsWith("/preparar"))
|
|
return {
|
|
id: "qa-id",
|
|
matches_reference: true,
|
|
publication: { version: 2, hash: "qa-plan" },
|
|
};
|
|
return { package_sha256: "qa-dataset", hash: "qa-source" };
|
|
},
|
|
},
|
|
}).AssistedEditor;
|
|
const props = {
|
|
batch: {
|
|
dataset_hash: "qa-dataset",
|
|
editor_id: 42,
|
|
items: [
|
|
{
|
|
code: "qa",
|
|
title: "QA institution",
|
|
summary: "QA",
|
|
categories: [],
|
|
types: [],
|
|
locations: [],
|
|
contacts: [],
|
|
source_sha256:
|
|
"119bb5514d2777849b70aebfec21d28a41885cbdc7d9827fbebd764fe994c108",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const render = () => {
|
|
si = 0;
|
|
ri = 0;
|
|
return Assisted(props);
|
|
};
|
|
const click = (tree, prefix) =>
|
|
flatten(tree)
|
|
.find((n) => n.type === "button" && text(n).startsWith(prefix))
|
|
.props.onClick();
|
|
let tree = render();
|
|
click(tree, "Seleccionar");
|
|
tree = render();
|
|
let boxes = flatten(tree).filter((n) => n.type === "input");
|
|
boxes[1].props.onChange({ target: { checked: true } });
|
|
tree = render();
|
|
click(tree, "1.");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(calls.length, 2);
|
|
assert.ok(calls.every((c) => !c.url.endsWith("/publicar")));
|
|
tree = render();
|
|
states.length = 0;
|
|
refs.length = 0; // Full page remount after login/MFA in the same tab.
|
|
tree = render();
|
|
assert.match(text(tree), /1 seleccionadas/);
|
|
assert.match(text(tree), /1 preparadas nesta retoma/);
|
|
boxes = flatten(tree).filter((n) => n.type === "input");
|
|
assert.equal(boxes[1].props.checked, false);
|
|
boxes[1].props.onChange({ target: { checked: true } });
|
|
tree = render();
|
|
click(tree, "1.");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(
|
|
calls.length,
|
|
2,
|
|
"A completed fiche must not be prepared again",
|
|
);
|
|
tree = render();
|
|
assert.equal(
|
|
flatten(tree).find(
|
|
(n) => n.type === "button" && text(n).startsWith("2."),
|
|
).props.disabled,
|
|
true,
|
|
);
|
|
flatten(tree)
|
|
.find((n) => n.type === "textarea")
|
|
.props.onChange({
|
|
target: {
|
|
value: "SYNTHETIC explicit source review and publication.",
|
|
},
|
|
});
|
|
boxes = flatten(tree).filter((n) => n.type === "input");
|
|
boxes[2].props.onChange({ target: { checked: true } });
|
|
tree = render();
|
|
click(tree, "2.");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
tree = render();
|
|
flatten(tree)
|
|
.find((n) => n.type === "textarea")
|
|
.props.onChange({
|
|
target: {
|
|
value: "A later reason must not change an uncertain confirmation.",
|
|
},
|
|
});
|
|
tree = render();
|
|
click(tree, "2.");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(calls[3], calls[2]);
|
|
assert.equal(calls[2].body.confirm, true);
|
|
assert.match(text(render()), /Publicação confirmada/);
|
|
props.batch.editor_id = 43;
|
|
states.length = 0;
|
|
refs.length = 0;
|
|
assert.match(
|
|
text(render()),
|
|
/0 seleccionadas/,
|
|
"Another account must not inherit progress",
|
|
);
|
|
props.batch.editor_id = 42;
|
|
props.batch.dataset_hash = "changed-source";
|
|
states.length = 0;
|
|
refs.length = 0;
|
|
assert.match(
|
|
text(render()),
|
|
/0 seleccionadas/,
|
|
"Changed sources must not inherit receipts",
|
|
);
|
|
if (previousStorage === undefined) delete globalThis.sessionStorage;
|
|
else globalThis.sessionStorage = previousStorage;
|
|
});
|
|
test("actual dossier continuation freezes a lost response retry and retains dossier/opportunity and selected product", async () => {
|
|
const states = [],
|
|
refs = [];
|
|
let stateIndex = 0,
|
|
refIndex = 0;
|
|
const calls = [];
|
|
const hooks = {
|
|
...React,
|
|
useEffect() {},
|
|
useState(initial) {
|
|
const i = stateIndex++;
|
|
if (!(i in states))
|
|
states[i] = typeof initial === "function" ? initial() : initial;
|
|
return [states[i], (value) => (states[i] = value)];
|
|
},
|
|
useRef(initial) {
|
|
const i = refIndex++;
|
|
refs[i] ??= { current: initial };
|
|
return refs[i];
|
|
},
|
|
};
|
|
class Validation extends Error {}
|
|
const { FundingDossierChoice } = load(
|
|
"components/funding-dossier-choice.tsx",
|
|
{
|
|
react: hooks,
|
|
"@inertiajs/react": { Link: "a", router: {} },
|
|
"@/lib/operation-key": { createOperationKey: () => "qa-operation" },
|
|
"@/lib/profile-json": {
|
|
ProfileValidationError: Validation,
|
|
profileJson: async (url, body) => {
|
|
calls.push({ url, body: structuredClone(body) });
|
|
if (calls.length === 1)
|
|
throw new Error("SYNTHETIC lost response");
|
|
return {
|
|
href: "/investimentos/qa-dossier?opportunity=qa#financiamento",
|
|
};
|
|
},
|
|
},
|
|
},
|
|
);
|
|
const props = {
|
|
record: {
|
|
id: "qa-programme",
|
|
version_id: "qa-version",
|
|
payload: { kind: "institution", products: [product] },
|
|
},
|
|
dossier: {
|
|
id: "qa-dossier",
|
|
version: 4,
|
|
opportunity: "qa-opportunity",
|
|
closed: false,
|
|
href: "/investimentos/qa-dossier",
|
|
},
|
|
selection: {
|
|
need: "investment",
|
|
territory: "qa-location",
|
|
territory_label: "SYNTHETIC Nampula",
|
|
},
|
|
needs: { investment: "Investment" },
|
|
destinations: [],
|
|
};
|
|
const render = () => {
|
|
stateIndex = 0;
|
|
refIndex = 0;
|
|
return FundingDossierChoice(props);
|
|
};
|
|
let tree = render();
|
|
const selects = flatten(tree).filter((n) => n.type === "select");
|
|
selects[1].props.onChange({ target: { value: product.code } });
|
|
selects[2].props.onChange({ target: { value: "qa-branch" } });
|
|
flatten(tree)
|
|
.find((n) => n.type === "input" && n.props.type === "checkbox")
|
|
.props.onChange({ target: { checked: true } });
|
|
tree = render();
|
|
flatten(tree)
|
|
.find((n) => n.type === "button")
|
|
.props.onClick();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
tree = render();
|
|
assert.equal(
|
|
flatten(tree).find((n) => n.type === "fieldset").props.disabled,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
text(flatten(tree).find((n) => n.type === "button")),
|
|
"Repetir a mesma intenção",
|
|
);
|
|
flatten(tree)
|
|
.find((n) => n.type === "button")
|
|
.props.onClick();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(calls[1], calls[0]);
|
|
assert.equal(calls[0].body.context, "qa-dossier");
|
|
assert.equal(calls[0].body.opportunity, "qa-opportunity");
|
|
assert.equal(calls[0].body.product, product.code);
|
|
assert.equal(calls[0].body.location_id, "qa-branch");
|
|
tree = render();
|
|
assert.equal(
|
|
flatten(tree).find((n) => n.type === "a").props.href,
|
|
"/investimentos/qa-dossier?opportunity=qa#financiamento",
|
|
);
|
|
});
|