Files
Investidor-V2/tests/frontend/helpers/partnership-candidacy.mjs
T

549 lines
18 KiB
JavaScript

import fs from 'node:fs';
import { createRequire } from 'node:module';
import { compileFunction } from 'node:vm';
import ts from 'typescript';
const require = createRequire(import.meta.url);
export const criteria = [
{
code: 'capacity',
label: 'Capacidade técnica/produtiva',
weight: 20,
mandatory: true,
state: 'unknown',
evidence: null,
explanation: 'SYNTHETIC unknown',
},
{
code: 'experience',
label: 'Experiência comprovada',
weight: 10,
mandatory: false,
state: 'unknown',
evidence: null,
explanation: 'SYNTHETIC unknown',
},
];
export const original = {
id: 10,
user_id: 2,
partnership_id: 1,
version: 1,
status: 'submitted',
organization_id: null,
proposal: 'SYNTHETIC original proposal',
payload: {
capacity: 'SYNTHETIC original capacity',
availability: 'SYNTHETIC original availability',
contribution: 'SYNTHETIC original contribution',
conflicts: 'SYNTHETIC original conflicts',
consent: true,
},
assessment: [],
decision_reason: null,
can_edit: true,
commands: [],
match: {
weights_hash: 'a'.repeat(64),
score_min: 0,
score_max: 100,
eligibility: 'unknown',
criteria,
},
room_id: null,
};
export function props(patch = {}) {
return {
actorId: 2,
record: {
id: 1,
version: 4,
title: 'SYNTHETIC partnership',
kind: 'seeking',
partnership_type: 'joint_venture',
sector_id: null,
deadline: null,
status: 'published',
offer: 'SYNTHETIC offer',
seeking: 'SYNTHETIC cooperation',
payload: {
summary: 'SYNTHETIC summary',
requirements: 'SYNTHETIC requirements',
consent: true,
},
},
owner: false,
moderator: false,
canEdit: false,
acceptingInterests: true,
canStartInterest: false,
interests: [structuredClone(original)],
organizations: [],
types: { joint_venture: 'Consórcio' },
sectors: [],
territories: [],
criteria: {},
sources: [],
origins: [],
locations: [],
historySummary: Object.hasOwn(patch, 'historySummary')
? patch.historySummary
: patch.owner || patch.moderator
? {
limit: 5,
entries: [],
has_more: false,
href: `/parcerias/${patch.record?.id ?? 1}/historico`,
}
: null,
publicationActions: [],
publicationCommands: [],
...patch,
};
}
export function nodes(tree) {
return Array.isArray(tree)
? tree.flatMap(nodes)
: tree && typeof tree === 'object'
? [tree, ...nodes(tree.props?.children)]
: [];
}
export function text(tree) {
return Array.isArray(tree)
? tree.map(text).join('')
: tree?.props
? text(tree.props.children)
: typeof tree === 'string' || typeof tree === 'number'
? String(tree)
: '';
}
const clone = (value) =>
Array.isArray(value)
? value.map(clone)
: value && Object.getPrototypeOf(value) === Object.prototype
? Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, clone(v)]),
)
: value;
export function harness(initial = props()) {
const frames = new Map(),
sent = [],
reloads = [],
before = new Set(),
unload = new Set();
let active,
cursor = 0,
changed = false,
effects = [],
current = initial,
tree,
sequence = 0,
confirmCalls = 0;
const windowMock = {
confirm() {
confirmCalls++;
return false;
},
addEventListener(name, callback) {
if (name === 'beforeunload') unload.add(callback);
},
removeEventListener(name, callback) {
if (name === 'beforeunload') unload.delete(callback);
},
};
const setters = new WeakMap();
const state = (initialValue) => {
const frame = active,
i = cursor++;
if (!(i in frame.cells))
frame.cells[i] =
typeof initialValue === 'function'
? initialValue()
: initialValue;
if (!setters.has(frame)) setters.set(frame, new Map());
const stable = setters.get(frame);
if (!stable.has(i))
stable.set(i, (value) => {
const next =
typeof value === 'function' ? value(frame.cells[i]) : value;
if (!Object.is(next, frame.cells[i])) {
frame.cells[i] = next;
changed = true;
}
});
return [frame.cells[i], stable.get(i)];
};
const hooks = {
useState: state,
useRef(value) {
const i = cursor++;
active.cells[i] ??= { current: value };
return active.cells[i];
},
useEffect(callback, deps) {
const frame = active,
i = cursor++,
prior = frame.cells[i];
if (!prior || !deps.every((value, j) => value === prior.deps[j])) {
frame.cells[i] = { deps };
effects.push(() => {
prior?.cleanup?.();
frame.cells[i].cleanup = callback();
});
}
},
useCallback(callback, deps) {
const i = cursor++,
prior = active.cells[i];
if (!prior || !deps.every((value, j) => value === prior.deps[j]))
active.cells[i] = { deps, callback };
return active.cells[i].callback;
},
};
const mocks = {
react: hooks,
'@inertiajs/react': {
usePage: () => ({
props: {
auth: {
user:
current.actorId === null
? null
: { id: current.actorId ?? 2 },
},
},
}),
Link: 'a',
router: {
reload(options) {
reloads.push(options);
},
on(name, callback) {
if (name === 'before') before.add(callback);
return () => before.delete(callback);
},
post(url, data) {
sent.push({ method: 'post', url, data, options: {} });
},
},
useForm(initialData) {
const i = cursor++;
if (active.cells[i]) return active.cells[i];
const form = {
data: clone(initialData),
defaults: clone(initialData),
errors: {},
processing: false,
setData(key, value) {
this.data =
typeof key === 'function'
? key(this.data)
: typeof key === 'object'
? key
: { ...this.data, [key]: value };
changed = true;
},
setDefaults(data) {
this.defaults = clone(data);
},
clearErrors() {
this.errors = {};
changed = true;
},
transform(callback) {
this.transformer = callback;
},
};
for (const method of ['post', 'put'])
form[method] = (url, options) => {
form.processing = true;
changed = true;
sent.push({
method,
url,
options,
data: clone(
form.transformer
? form.transformer(form.data)
: form.data,
),
form,
});
};
active.cells[i] = form;
return form;
},
},
'@/pages/partnerships/shared': {
labels: {
negotiation: 'Em negociação',
agreement_recorded: 'Acordo registado',
agreed: 'Confirmado pelas partes',
execution: 'Em execução',
closed: 'Concluída',
cancelled: 'Cancelada',
pending: 'Pendente',
progress: 'Em curso',
completed: 'Concluído',
},
},
'./shared': {
Frame: 'main',
Field: 'label',
Errors: 'errors',
labels: {
pending: 'Submetida — legado',
progress: 'Em curso',
completed: 'Concluído',
},
},
};
const operation = {
createOperationKey: () =>
`00000000-0000-4000-8000-${String(++sequence).padStart(12, '0')}`,
};
mocks['./operation-key'] = operation;
mocks['@/lib/operation-key'] = operation;
function load(path) {
const code = ts.transpileModule(
fs.readFileSync(
new URL('../../../' + path, import.meta.url),
'utf8',
),
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
jsx: ts.JsxEmit.ReactJSX,
target: ts.ScriptTarget.ES2022,
},
},
).outputText;
const module = { exports: {} };
compileFunction(code, ['require', 'module', 'exports'])(
(name) => mocks[name] ?? require(name),
module,
module.exports,
);
return module.exports;
}
mocks['./shared'].labels = {
draft: 'Rascunho',
pending: 'Submetida — legado',
submitted: 'Submetida',
returned: 'Devolvida',
rejected: 'Rejeitada',
published: 'Publicada',
suspended: 'Suspensa',
archived: 'Arquivada',
withdrawn: 'Retirada',
clarification: 'Em esclarecimento',
shortlisted: 'Lista curta',
negotiation: 'Em negociação',
agreement_recorded: 'Acordo registado',
agreed: 'Confirmado pelas partes',
execution: 'Em execução',
closed: 'Concluída',
cancelled: 'Cancelada',
progress: 'Em curso',
completed: 'Concluído',
met: 'Comprovadamente cumprido',
unmet: 'Comprovadamente incumprido',
unknown: 'Informação insuficiente',
};
mocks['@/pages/partnerships/shared'] = mocks['./shared'];
mocks['@/components/investment-complements'] = {
ComplementEntryActions: 'complements',
};
mocks['@/components/structure-geography'] = {
__esModule: true,
default: 'geography',
};
mocks['@/lib/partnership-candidacy-edit'] = load(
'resources/js/lib/partnership-candidacy-edit.ts',
);
mocks['@/components/partnership-candidacies'] = load(
'resources/js/components/partnership-candidacies.tsx',
);
mocks['@/lib/partnership-publication'] = load(
'resources/js/lib/partnership-publication.ts',
);
mocks['@/components/partnership-publication'] = load(
'resources/js/components/partnership-publication.tsx',
);
mocks['@/lib/partnership-history'] = load(
'resources/js/lib/partnership-history.ts',
);
mocks['@/components/partnership-publication-summary'] = load(
'resources/js/components/partnership-publication-summary.tsx',
);
const Page = load('resources/js/pages/partnerships/detail.tsx').default;
function expand(value, path, seen) {
if (Array.isArray(value))
return value.map((child, i) =>
expand(child, path + '/' + (child?.key ?? i), seen),
);
if (!value || typeof value !== 'object' || !value.props) return value;
if (typeof value.type === 'function') {
const id = path + '/' + value.type.name + ':' + (value.key ?? '');
seen.add(id);
const frame = frames.get(id) ?? { cells: [] };
frames.set(id, frame);
const previous = active,
priorCursor = cursor;
active = frame;
cursor = 0;
const rendered = value.type(value.props);
active = previous;
cursor = priorCursor;
return {
type: 'component',
props: {
name: value.type.name,
children: expand(rendered, id, seen),
},
};
}
return {
...value,
props: {
...value.props,
children: expand(
value.props.children,
path + '/children',
seen,
),
},
};
}
function render(next = current) {
current = next;
const previousWindow = globalThis.window;
globalThis.window = windowMock;
try {
let loops = 0;
do {
changed = false;
const seen = new Set();
tree = expand(
{ type: Page, key: 'page', props: current },
'root',
seen,
);
for (const [id, frame] of frames)
if (!seen.has(id)) {
frame.cells.forEach((cell) => cell?.cleanup?.());
frames.delete(id);
}
const queue = effects;
effects = [];
queue.forEach((effect) => effect());
if (++loops > 12) throw Error('Component state did not settle');
} while (changed);
return tree;
} finally {
globalThis.window = previousWindow;
}
}
render();
return {
sent,
reloads,
render,
get props() {
return current;
},
get tree() {
return tree;
},
all: () => nodes(tree),
form(name) {
return nodes(tree).find(
(node) => node.props?.['data-candidacy-form'] === name,
);
},
button(label, scope = tree) {
return nodes(scope).find(
(node) => node.type === 'button' && text(node) === label,
);
},
field(name, label) {
const root = this.form(name);
const field = nodes(root).find(
(node) => node.type === 'label' && node.props.label === label,
);
return nodes(field).find((node) =>
['input', 'select', 'textarea'].includes(node.type),
);
},
change(name, label, value, rerender = true) {
const field = this.field(name, label);
if (!field) throw Error('Missing field ' + name + '/' + label);
field.props.onChange({
target: { value, checked: value, files: value ? [value] : [] },
});
if (rerender) render();
},
submit(name) {
const form = this.form(name);
form.props.onSubmit({ preventDefault() {} });
render();
return sent.at(-1);
},
click(label, scope = tree) {
const button = this.button(label, scope);
if (!button) throw Error('Missing button ' + label);
button.props.onClick();
render();
return sent.at(-1);
},
success(next, request = sent.at(-1)) {
render(next);
request.options.onSuccess({ props: next });
request.form.processing = false;
render(next);
},
fail(request = sent.at(-1), status = null) {
if (status === null) request.options.onNetworkError();
else request.options.onHttpException({ status });
request.form.processing = false;
render();
},
refreshed(next, request = reloads.at(-1)) {
render(next);
request.onSuccess?.({ props: next });
request.onFinish?.();
render(next);
},
leave(preserveState = false) {
let prevented = false;
const previousWindow = globalThis.window;
globalThis.window = windowMock;
try {
for (const fn of before)
fn({
detail: { visit: { method: 'get', preserveState } },
preventDefault() {
prevented = true;
},
});
} finally {
globalThis.window = previousWindow;
}
return prevented;
},
unload() {
let prevented = false;
for (const fn of unload)
fn({
preventDefault() {
prevented = true;
},
returnValue: null,
});
return prevented;
},
get confirmCalls() {
return confirmCalls;
},
};
}