Files
Investidor-V2/tests/frontend/helpers/resource-incident-form.mjs
T

335 lines
10 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 function props(patch = {}) {
return {
opportunity: {
id: 1,
title: 'SYNTHETIC resource investment',
status: 'draft',
deleted_at: null,
},
etag: 'a'.repeat(64),
resources: { documents: [], images: [], locations: [] },
existingLocations: [],
assessment: [],
imageProcessingAvailable: true,
...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)
: '';
}
export function harness(initial = props()) {
const frames = new Map(),
sent = [],
reloads = [],
unload = new Set();
let active,
cursor = 0,
changed = false,
effects = [],
current = initial,
tree,
sequence = 0;
const windowMock = {
confirm() {
return false;
},
addEventListener(name, callback) {
if (name === 'beforeunload') unload.add(callback);
},
removeEventListener(name, callback) {
if (name === 'beforeunload') unload.delete(callback);
},
};
const state = (initialValue) => {
const frame = active,
i = cursor++;
if (!(i in frame.cells))
frame.cells[i] =
typeof initialValue === 'function'
? initialValue()
: initialValue;
return [
frame.cells[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;
}
},
];
};
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;
},
};
class Form {
constructor() {
this.values = new Map();
}
set(k, v) {
this.values.set(k, v);
}
get(k) {
return this.values.get(k);
}
}
function globals() {
const saved = {
document: globalThis.document,
fetch: globalThis.fetch,
FormData: globalThis.FormData,
setTimeout: globalThis.setTimeout,
};
globalThis.document = {
cookie: 'XSRF-TOKEN=SYNTHETIC',
getElementById: () => ({ focus() {} }),
};
globalThis.FormData = Form;
globalThis.setTimeout = (fn) => {
fn();
return 0;
};
globalThis.fetch = (url, options) =>
new Promise((resolve, reject) =>
sent.push({ url, form: options.body, resolve, reject }),
);
return () => Object.assign(globalThis, saved);
}
const mocks = {
react: hooks,
'@inertiajs/react': {
Link: 'a',
usePage: () => ({
props: {
basePath: '/minhas-oportunidades',
countries: [],
readOnly: false,
},
url: '/minhas-oportunidades/1/resources',
}),
router: { reload: (o) => reloads.push(o) },
},
'@/components/opportunity-library-reuse': { default: () => null },
'@/components/territory-geometry-preview': { default: () => null },
'@/components/investment-territory': { TerritoryLevel: () => null },
'./shared': {
Management: 'main',
Field: ({ label, children }) => ({
type: 'label',
props: { children: [label, children] },
}),
button: '',
control: '',
},
'@/lib/operation-key': {
createOperationKey: () =>
`00000000-0000-4000-8000-${String(++sequence).padStart(12, '0')}`,
},
};
function load(path) {
const code = ts.transpileModule(
fs.readFileSync(
new URL('../../../' + path, import.meta.url),
'utf8',
),
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
},
).outputText;
const module = { exports: {} };
compileFunction(code, ['require', 'module', 'exports'])(
(name) => mocks[name] ?? require(name),
module,
module.exports,
);
return module.exports;
}
mocks['@/lib/technical-incident-labels'] = load(
'resources/js/lib/technical-incident-labels.ts',
);
mocks['@/lib/value-text'] = load('resources/js/lib/value-text.ts');
mocks['@/components/opportunity-technical-incidents'] = load(
'resources/js/components/opportunity-technical-incidents.tsx',
);
const Page = load(
'resources/js/pages/management/opportunity-resources.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();
const field = (label) =>
nodes(tree).find(
(n) =>
['input', 'textarea', 'select'].includes(n.type) &&
n.props['aria-label'] === label,
);
return {
sent,
reloads,
render,
get tree() {
return tree;
},
get props() {
return current;
},
value(label) {
return field(label)?.props.value;
},
change(label, value) {
field(label).props.onChange({ target: { value } });
render();
},
choose(label) {
nodes(tree)
.find((n) => n.type === 'button' && text(n) === label)
.props.onClick();
render();
},
attach(value) {
nodes(tree)
.find((n) => n.type === 'input' && n.props.type === 'file')
.props.onChange({ target: { files: [value] } });
render();
},
submit() {
const restore = globals();
try {
nodes(tree)
.find((n) => n.type === 'form')
.props.onSubmit({ preventDefault() {} });
render();
return sent.at(-1);
} finally {
restore();
}
},
async reply(status, data, request = sent.at(-1)) {
const restore = globals();
try {
request.resolve({
ok: status >= 200 && status < 300,
status,
json: async () => data,
});
for (let i = 0; i < 8; i++) await Promise.resolve();
render();
} finally {
restore();
}
},
};
}