Files
Investidor-V2/tests/frontend/education-drafts.test.mjs
T

368 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { compileFunction } from 'node:vm';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
const require = createRequire(import.meta.url);
function load(path, mocks) {
const output = 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(output, ['require', 'module', 'exports'])(
(name) => mocks[name] ?? require(name),
module,
module.exports,
);
return module.exports;
}
function nodes(tree) {
if (!tree || typeof tree !== 'object') return [];
if (Array.isArray(tree)) return tree.flatMap(nodes);
return [tree, ...nodes(tree.props?.children)];
}
const emptyProps = {
today: '2026-11-02',
territories: [],
projects: [],
courses: [],
occupations: [],
skills: [],
draftsAvailable: true,
};
class ApiError extends Error {
constructor(status) {
super('Synthetic error');
this.status = status;
this.errors = {};
}
}
function setup(request, props = emptyProps, renderPage = false) {
const states = [];
let cursor = 0,
form;
const Snapshot = () => null;
const saved = [],
dirty = [];
const module = load('../../resources/js/pages/education/index.tsx', {
react: {
useState(initial) {
const index = cursor++;
if (!(index in states))
states[index] =
typeof initial === 'function' ? initial() : initial;
return [
states[index],
(value) => {
states[index] =
typeof value === 'function'
? value(states[index])
: value;
},
];
},
useEffect(fn) {
if (!renderPage) fn();
},
useCallback(fn) {
return fn;
},
},
'@inertiajs/react': {
Link: 'a',
router: {},
useForm(initial) {
if (renderPage)
return {
data: initial,
errors: {},
processing: false,
isDirty: false,
};
form ??= {
data: { ...initial },
defaults: { ...initial },
errors: {},
processing: false,
get isDirty() {
return (
JSON.stringify(this.data) !==
JSON.stringify(this.defaults)
);
},
setData(key, value) {
this.data = { ...this.data, [key]: value };
},
setDefaults(value) {
this.defaults = { ...value };
},
clearErrors() {
this.errors = {};
},
setError(errors) {
this.errors = errors;
},
post() {
throw new Error('Draft must not use legacy create');
},
};
return form;
},
},
'@/lib/portal-api': {
portalRequest: request,
PortalApiError: ApiError,
newSubmissionKey: () => 'synthetic-stable-key',
},
'@/components/education-submitted-snapshot': {
__esModule: true,
default: Snapshot,
},
'../management/shared': {
Management: 'main',
Field: 'label',
control: '',
button: '',
Pagination: () => null,
},
'@/components/investment-territory': {
TerritoryFields: 'territory-fields',
},
});
return {
page() {
cursor = 0;
return module.default(props);
},
render() {
cursor = 0;
return module.Intake({
kind: 'demands',
props,
onSaved: (item) => saved.push(item),
onDirty: (kind, value) => dirty.push([kind, value]),
});
},
get form() {
return form;
},
saved,
dirty,
Snapshot,
};
}
const saveButton = (tree) =>
nodes(tree).find(
(node) => node.type === 'button' && node.props.type === 'button',
);
const flush = () => new Promise((resolve) => setImmediate(resolve));
await test('the full education page renders the private draft list and both intake forms', () => {
const component = setup(
async () => {},
{
...emptyProps,
filters: {},
providers: [],
demands: { data: [] },
capacities: { data: [] },
indicators: {
demand_records: 0,
capacity_records: 0,
expired_demands: 0,
pending_demands: 0,
},
ownDrafts: {
data: [
{
id: 'qa-draft',
kind: 'demands',
state: 'draft',
version_no: 1,
label: 'Synthetic qualification',
updated_at: '2026-11-02T12:00:00Z',
},
],
},
},
true,
);
const html = renderToStaticMarkup(component.page());
assert.ok(html.includes('Os meus rascunhos e submissões'));
assert.ok(html.includes('/management/education?draft=qa-draft'));
assert.ok(html.includes('Synthetic qualification'));
assert.ok(html.includes('Declarar procura de trabalhadores'));
assert.ok(html.includes('Declarar vagas de formação'));
});
await test('incomplete save bypasses native required fields, keeps edits, and submits the saved identity with current values', async () => {
const requests = [];
const component = setup(async (url, options) => {
const data = JSON.parse(options.body);
requests.push({ url, method: options.method, data });
return {
id: 'qa-draft',
kind: 'demands',
version_no: data.version_no + 1,
state: url.endsWith('/submit') ? 'submitted' : 'draft',
payload: data.payload,
references: {},
record_id: null,
updated_at: '2026-11-02T12:00:00Z',
};
});
let tree = component.render();
component.form.setData('employer', 'Synthetic incomplete employer');
tree = component.render();
assert.equal(saveButton(tree).props.type, 'button');
saveButton(tree).props.onClick();
await flush();
assert.equal(requests[0].method, 'POST');
assert.equal(requests[0].data.payload.qualification, '');
assert.equal(component.form.data.employer, 'Synthetic incomplete employer');
assert.equal(component.form.isDirty, false);
tree = component.render();
component.form.setData('quantity', '12');
tree = component.render();
saveButton(tree).props.onClick();
await flush();
assert.equal(requests[1].method, 'PUT');
assert.equal(requests[1].data.version_no, 1);
assert.equal(requests[1].data.client_key, requests[0].data.client_key);
tree = component.render();
nodes(tree)
.find((node) => node.type === 'form')
.props.onSubmit({ preventDefault() {} });
await flush();
assert.equal(
requests[2].url,
'/management/education/drafts/demands/qa-draft/submit',
);
assert.equal(requests[2].data.version_no, 2);
assert.equal(requests[2].data.payload.quantity, '12');
tree = component.render();
assert.equal(tree.type, component.Snapshot);
assert.equal(tree.props.draft.state, 'submitted');
assert.equal(component.saved.length, 3);
});
await test('recovery preserves blank dates and reference IDs, and a conflict does not overwrite current edits', async () => {
const component = setup(
async () => {
throw new ApiError(409);
},
{
...emptyProps,
selectedDraft: {
id: 'recovered',
kind: 'demands',
version_no: 4,
state: 'draft',
payload: {
employer: 'Old',
reference_date: null,
geo_location_id: 'retired-location',
},
references: null,
},
},
);
let tree = component.render();
assert.equal(component.form.data.reference_date, '');
assert.equal(component.form.data.geo_location_id, 'retired-location');
component.form.setData('employer', 'Deliberate current edit');
tree = component.render();
saveButton(tree).props.onClick();
await flush();
tree = component.render();
assert.equal(component.form.data.employer, 'Deliberate current edit');
assert.equal(component.form.isDirty, true);
assert.ok(
nodes(tree).some(
(node) =>
node.props?.role === 'alert' &&
String(node.props.children).includes('noutra sessão'),
),
);
assert.equal(component.saved.length, 0);
});
await test('recovered territorial path remains stable while unrelated fields are edited', () => {
const territories = [
{
id: 'p',
name: 'Province',
parent_id: null,
location_type: 'province',
},
{
id: 'd',
name: 'District',
parent_id: 'p',
location_type: 'district',
},
];
const component = setup(async () => {}, {
...emptyProps,
territories,
selectedDraft: {
id: 'qa',
kind: 'demands',
version_no: 1,
state: 'draft',
payload: { geo_location_id: 'd' },
},
});
let tree = component.render();
const initial = nodes(tree).find((node) => node.type === 'territory-fields')
.props.initial;
assert.deepEqual(
initial.map((item) => item.id),
['p', 'd'],
);
component.form.setData('employer', 'New text');
tree = component.render();
assert.equal(
nodes(tree).find((node) => node.type === 'territory-fields').props
.initial,
initial,
);
});
await test('submitted view uses preserved reference names and escapes source text without another submission form', () => {
const Snapshot = load(
'../../resources/js/components/education-submitted-snapshot.tsx',
{ '@inertiajs/react': { Link: 'a' } },
).default;
const html = renderToStaticMarkup(
Snapshot({
draft: {
kind: 'demands',
record_id: 'qa-record',
payload: { quantity: 20, source: '<script>synthetic</script>' },
references: {
project_id: { name: 'Original project name' },
territory_path: [{ name: 'Original district' }],
},
},
}),
);
assert.ok(html.includes('Original project name'));
assert.ok(html.includes('Original district'));
assert.ok(html.includes('&lt;script&gt;'));
assert.ok(!html.includes('<form'));
assert.ok(
html.includes('/management/education/demands/qa-record/analysis'),
);
});