Files
Investidor-V2/tests/frontend/funding-directory.test.mjs
T

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",
);
});