379 lines
12 KiB
JavaScript
379 lines
12 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 apply = 'Aplicar actualiza\u00e7\u00e3o do Suporte t\u00e9cnico';
|
|
export const refresh = 'Actualizar pr\u00e9via';
|
|
export const adopt = 'Confirmar leitura da pr\u00e9via actual';
|
|
export const retry = 'Repetir envio original';
|
|
export const reason = 'Actualizacao sintetica explicitamente fundamentada';
|
|
export function props(patch = {}) {
|
|
return {
|
|
actorId: 1,
|
|
target: { database: 'support_profile_installation_test', port: 55441 },
|
|
hash: 'a'.repeat(64),
|
|
code_hash: 'b'.repeat(64),
|
|
installed: false,
|
|
consistent: true,
|
|
repairable: false,
|
|
ready: true,
|
|
action: 'install',
|
|
schema: { state: 'base', ledger: false },
|
|
dependencies: { profiles_installed: true },
|
|
changes: [],
|
|
backupDate: '2026-11-03T00:00:00Z',
|
|
rollback: 'Conservar historico',
|
|
commands: [],
|
|
mfa_confirmed: true,
|
|
...patch,
|
|
};
|
|
}
|
|
export function result(request, patch = {}) {
|
|
return {
|
|
actor_id: 1,
|
|
command_key: request.data.idempotency_key,
|
|
target: props().target,
|
|
migration: '2026_11_03_080000_add_technical_support_profile',
|
|
code_hash: props().code_hash,
|
|
installed: true,
|
|
...patch,
|
|
};
|
|
}
|
|
export function committed(request, patch = {}) {
|
|
return props({
|
|
installed: true,
|
|
action: 'none',
|
|
hash: 'c'.repeat(64),
|
|
schema: { state: 'support', ledger: true },
|
|
commands: [
|
|
{ key: request.data.idempotency_key, code_hash: props().code_hash },
|
|
],
|
|
...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(), mode = 'support') {
|
|
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 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 ApiError extends Error {
|
|
constructor(status, errors = {}) {
|
|
super('SYNTHETIC HTTP ' + status);
|
|
this.status = status;
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
const mocks = {
|
|
react: hooks,
|
|
'@inertiajs/react': {
|
|
Link: 'a',
|
|
router: {
|
|
reload: (options) => reloads.push(options),
|
|
on(name, fn) {
|
|
if (name === 'before') before.add(fn);
|
|
return () => before.delete(fn);
|
|
},
|
|
},
|
|
},
|
|
'../management/shared': { Management: 'main', button: '', control: '' },
|
|
'@/pages/management/shared': {
|
|
Management: 'main',
|
|
button: '',
|
|
control: '',
|
|
},
|
|
};
|
|
const request = (url, options = {}) =>
|
|
new Promise((resolve, reject) => {
|
|
const entry = {
|
|
url,
|
|
data: options.body ? JSON.parse(options.body) : null,
|
|
resolve,
|
|
reject,
|
|
};
|
|
(options.method === 'POST' ? sent : reloads).push(entry);
|
|
});
|
|
mocks['./portal-api'] = {
|
|
PortalApiError: ApiError,
|
|
portalRequest: request,
|
|
};
|
|
mocks['@/lib/profile-json'] = {
|
|
profileJson: (url, data, method = 'POST') =>
|
|
request(url, { method, body: JSON.stringify(data) }),
|
|
};
|
|
mocks['./operation-key'] = mocks['@/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['./page-preparation-guard'] = mocks['@/lib/page-preparation-guard'] =
|
|
load('resources/js/lib/page-preparation-guard.ts');
|
|
mocks['@/lib/profile-support-installation'] = load(
|
|
'resources/js/lib/profile-support-installation.ts',
|
|
);
|
|
const Page = load(
|
|
'resources/js/pages/profiles/' +
|
|
(mode === 'support' ? 'support-installation' : 'installation') +
|
|
'.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;
|
|
},
|
|
get confirmCalls() {
|
|
return confirmCalls;
|
|
},
|
|
get reason() {
|
|
return nodes(tree).find((n) => n.type === 'textarea').props.value;
|
|
},
|
|
button(label) {
|
|
return nodes(tree).find(
|
|
(n) => n.type === 'button' && text(n) === label,
|
|
);
|
|
},
|
|
change(value) {
|
|
nodes(tree)
|
|
.find((n) => n.type === 'textarea')
|
|
.props.onChange({ target: { value } });
|
|
render();
|
|
},
|
|
confirm(value = true) {
|
|
nodes(tree)
|
|
.find((n) => n.type === 'input' && n.props.type === 'checkbox')
|
|
.props.onChange({ target: { checked: value } });
|
|
render();
|
|
},
|
|
number(index, value) {
|
|
nodes(tree)
|
|
.filter((n) => n.type === 'input' && n.props.type === 'number')
|
|
[index].props.onChange({ target: { value } });
|
|
render();
|
|
},
|
|
get guards() {
|
|
return before.size;
|
|
},
|
|
click(label) {
|
|
const node = this.button(label);
|
|
if (!node) throw Error('Missing button ' + label);
|
|
node.props.onClick();
|
|
render();
|
|
return sent.at(-1);
|
|
},
|
|
async reply(value, request = sent.at(-1)) {
|
|
request.resolve(value);
|
|
for (let i = 0; i < 6; i++) await Promise.resolve();
|
|
render();
|
|
},
|
|
async fail(status = null, request = sent.at(-1)) {
|
|
request.reject(
|
|
status === null
|
|
? new Error('SYNTHETIC network loss')
|
|
: new ApiError(
|
|
status,
|
|
status === 422
|
|
? { note: ['SYNTHETIC rejected note'] }
|
|
: {},
|
|
),
|
|
);
|
|
for (let i = 0; i < 6; i++) await Promise.resolve();
|
|
render();
|
|
},
|
|
leave(preserveState = false) {
|
|
let prevented = false;
|
|
const old = globalThis.window;
|
|
globalThis.window = windowMock;
|
|
try {
|
|
for (const fn of before)
|
|
fn({
|
|
detail: { visit: { method: 'get', preserveState } },
|
|
preventDefault() {
|
|
prevented = true;
|
|
},
|
|
});
|
|
} finally {
|
|
globalThis.window = old;
|
|
}
|
|
return prevented;
|
|
},
|
|
};
|
|
}
|