779 lines
25 KiB
JavaScript
779 lines
25 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 ts from 'typescript';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
function load(file, mocks = {}, globals = {}) {
|
|
const output = ts.transpileModule(
|
|
fs.readFileSync(new URL(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',
|
|
...Object.keys(globals),
|
|
])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
...Object.values(globals),
|
|
);
|
|
return module.exports;
|
|
}
|
|
const lib = load('../../resources/js/lib/desk-checkpoints.ts');
|
|
class ValidationError extends Error {
|
|
constructor(message, fields = {}) {
|
|
super(message);
|
|
this.fields = fields;
|
|
}
|
|
}
|
|
function nodes(tree) {
|
|
if (Array.isArray(tree)) return tree.flatMap(nodes);
|
|
if (!tree || typeof tree !== 'object') return [];
|
|
return [tree, ...nodes(tree.props?.children)];
|
|
}
|
|
const request = {
|
|
id: 'synthetic-request',
|
|
version: 1,
|
|
status: 'draft',
|
|
payload: { title: 'Original', choices: ['0'], declaration: null },
|
|
editable_fields: [],
|
|
can_edit: true,
|
|
protocol: null,
|
|
analyst_id: null,
|
|
due_at: null,
|
|
};
|
|
const initialView = () => ({
|
|
installed: true,
|
|
request: structuredClone(request),
|
|
checkpoint: null,
|
|
});
|
|
function serverCheckpoint(body) {
|
|
return {
|
|
version: body.version + 1,
|
|
request_version: body.request_version,
|
|
updated_at: '2026-11-03T03:00:00Z',
|
|
payload: structuredClone(body.payload),
|
|
reason: body.reason,
|
|
base_payload: structuredClone(request.payload),
|
|
edited_keys: [...body.edited_keys],
|
|
};
|
|
}
|
|
function harness(handler) {
|
|
const cells = [],
|
|
timers = new Map(),
|
|
listeners = new Map(),
|
|
before = [];
|
|
let cursor = 0,
|
|
dirty = true,
|
|
tree,
|
|
props,
|
|
sequence = 0,
|
|
clock = 0;
|
|
const effects = [],
|
|
calls = [],
|
|
recovered = [],
|
|
blocked = [],
|
|
busy = [];
|
|
const react = {
|
|
useState(initial) {
|
|
const i = cursor++;
|
|
if (!cells[i])
|
|
cells[i] = {
|
|
value: typeof initial === 'function' ? initial() : initial,
|
|
};
|
|
return [
|
|
cells[i].value,
|
|
(value) => {
|
|
const next =
|
|
typeof value === 'function'
|
|
? value(cells[i].value)
|
|
: value;
|
|
if (!Object.is(next, cells[i].value)) {
|
|
cells[i].value = next;
|
|
dirty = true;
|
|
}
|
|
},
|
|
];
|
|
},
|
|
useRef(initial) {
|
|
const i = cursor++;
|
|
cells[i] ??= { current: initial };
|
|
return cells[i];
|
|
},
|
|
useEffect(effect, deps) {
|
|
const i = cursor++,
|
|
old = cells[i];
|
|
if (!old || deps.some((v, j) => !Object.is(v, old.deps[j]))) {
|
|
cells[i] = { deps, cleanup: old?.cleanup };
|
|
effects.push(() => {
|
|
cells[i].cleanup?.();
|
|
cells[i].cleanup = effect();
|
|
});
|
|
}
|
|
},
|
|
};
|
|
const Component = load(
|
|
'../../resources/js/components/desk-draft-recovery.tsx',
|
|
{
|
|
react,
|
|
'@inertiajs/react': {
|
|
router: {
|
|
on(name, callback) {
|
|
before.push(callback);
|
|
return () => {
|
|
before.splice(before.indexOf(callback), 1);
|
|
};
|
|
},
|
|
},
|
|
},
|
|
'@/lib/profile-json': {
|
|
ProfileValidationError: ValidationError,
|
|
async profileJson(url, body = {}, method = 'POST') {
|
|
calls.push({ url, body: structuredClone(body), method });
|
|
return handler(url, body, method);
|
|
},
|
|
},
|
|
'@/lib/operation-key': {
|
|
createOperationKey: () => `synthetic-operation-${++sequence}`,
|
|
},
|
|
'@/lib/desk-checkpoints': lib,
|
|
},
|
|
{
|
|
window: {
|
|
addEventListener: (name, fn) => listeners.set(name, fn),
|
|
removeEventListener: (name) => listeners.delete(name),
|
|
confirm: () => false,
|
|
},
|
|
setTimeout: (fn) => {
|
|
const id = ++clock;
|
|
timers.set(id, fn);
|
|
return id;
|
|
},
|
|
clearTimeout: (id) => timers.delete(id),
|
|
},
|
|
).default;
|
|
props = {
|
|
id: request.id,
|
|
version: 1,
|
|
initial: structuredClone(request.payload),
|
|
payload: structuredClone(request.payload),
|
|
reason: '',
|
|
fields: {
|
|
title: { label: 'Title' },
|
|
choices: { label: 'Choices' },
|
|
declaration: { label: 'Declaration' },
|
|
},
|
|
paused: false,
|
|
onBusy: (value) => busy.push(value),
|
|
onBlocked: (value) => blocked.push(value),
|
|
onRecover: (payload, reason, current) => {
|
|
recovered.push({ payload, reason, current });
|
|
props = { ...props, payload, reason, version: current.version };
|
|
dirty = true;
|
|
},
|
|
};
|
|
function render(patch = {}) {
|
|
props = { ...props, ...patch };
|
|
dirty = true;
|
|
flush();
|
|
return tree;
|
|
}
|
|
function flush() {
|
|
let loops = 0;
|
|
while (dirty || effects.length) {
|
|
if (++loops > 25) throw Error('Effect loop');
|
|
if (dirty) {
|
|
dirty = false;
|
|
cursor = 0;
|
|
tree = Component(props);
|
|
}
|
|
while (effects.length) effects.shift()();
|
|
}
|
|
return tree;
|
|
}
|
|
async function settle() {
|
|
for (let i = 0; i < 10; i++) {
|
|
await Promise.resolve();
|
|
flush();
|
|
}
|
|
return tree;
|
|
}
|
|
return {
|
|
render,
|
|
settle,
|
|
calls,
|
|
recovered,
|
|
blocked,
|
|
busy,
|
|
listeners,
|
|
before,
|
|
timers,
|
|
get tree() {
|
|
return tree;
|
|
},
|
|
get props() {
|
|
return props;
|
|
},
|
|
async advance() {
|
|
for (const [id, fn] of timers) {
|
|
timers.delete(id);
|
|
fn();
|
|
}
|
|
await settle();
|
|
},
|
|
button(label) {
|
|
const node = nodes(tree).find(
|
|
(n) => n.type === 'button' && n.props.children === label,
|
|
);
|
|
assert.ok(node, `Button missing: ${label}`);
|
|
return node;
|
|
},
|
|
unmount() {
|
|
for (const cell of cells) cell?.cleanup?.();
|
|
},
|
|
};
|
|
}
|
|
|
|
await test('recovery preserves typed choices, explicit zero and deliberate reverts; concurrent answers require a choice', () => {
|
|
const base = {
|
|
title: 'Original',
|
|
amount: 0,
|
|
choices: ['0'],
|
|
declaration: 'declined',
|
|
};
|
|
const saved = {
|
|
base_payload: base,
|
|
payload: { ...base, title: 'Saved', choices: ['cotton', '0'] },
|
|
edited_keys: ['title', 'choices', 'declaration'],
|
|
};
|
|
const current = { ...base, title: 'External', declaration: 'accepted' };
|
|
const result = lib.checkpointRecovery(
|
|
base,
|
|
base,
|
|
[],
|
|
current,
|
|
saved,
|
|
Object.keys(base),
|
|
);
|
|
assert.deepEqual(result.conflicts, ['title', 'declaration']);
|
|
const chosen = lib.checkpointRecovery(
|
|
base,
|
|
base,
|
|
[],
|
|
current,
|
|
saved,
|
|
Object.keys(base),
|
|
{ title: 'current', declaration: 'saved' },
|
|
);
|
|
assert.deepEqual(chosen.conflicts, []);
|
|
assert.equal(chosen.payload.title, 'External');
|
|
assert.equal(chosen.payload.declaration, 'declined');
|
|
assert.equal(chosen.payload.amount, 0);
|
|
assert.deepEqual(chosen.payload.choices, ['cotton', '0']);
|
|
const returned = lib.checkpointRecovery(
|
|
base,
|
|
{ ...base, title: 'Forbidden' },
|
|
['title'],
|
|
current,
|
|
saved,
|
|
['amount'],
|
|
);
|
|
assert.equal(returned.payload.title, 'External');
|
|
assert.equal(
|
|
lib.checkpointSignature({ choices: ['0', 'cotton'] }),
|
|
lib.checkpointSignature({ choices: ['cotton', '0'] }),
|
|
);
|
|
});
|
|
|
|
await test('opportunity and its preserved context are recovered as one choice without displaying technical snapshots', () => {
|
|
const base = {
|
|
context_opportunity: 'old',
|
|
context_opportunity_snapshot: JSON.stringify({
|
|
choice: { opportunity: { title: 'Old opportunity' } },
|
|
revision: 1,
|
|
}),
|
|
context_reuse: 'old metadata',
|
|
};
|
|
const page = {
|
|
context_opportunity: 'local',
|
|
context_opportunity_snapshot: JSON.stringify({
|
|
choice: { opportunity: { title: 'Local opportunity' } },
|
|
revision: 2,
|
|
}),
|
|
context_reuse: 'local metadata',
|
|
};
|
|
const current = {
|
|
context_opportunity: 'remote',
|
|
context_opportunity_snapshot: JSON.stringify({
|
|
choice: { opportunity: { title: 'Remote opportunity' } },
|
|
revision: 3,
|
|
}),
|
|
context_reuse: 'remote metadata',
|
|
};
|
|
const result = lib.checkpointRecovery(
|
|
base,
|
|
page,
|
|
Object.keys(page),
|
|
current,
|
|
null,
|
|
Object.keys(page),
|
|
{ context_opportunity: 'page' },
|
|
);
|
|
assert.deepEqual(result.payload, page);
|
|
assert.equal(result.rows.length, 1);
|
|
assert.equal(result.rows[0].page, 'Local opportunity · revisão 2');
|
|
assert.deepEqual(result.intent, Object.keys(page));
|
|
assert.ok(!JSON.stringify(result.rows).includes('local metadata'));
|
|
});
|
|
|
|
await test('autosave is debounced, captures edits during a pending write and never sends submission or consent flags', async () => {
|
|
let view = initialView(),
|
|
resolveFirst;
|
|
const h = harness(async (url, body, method) => {
|
|
if (method === 'GET') return structuredClone(view);
|
|
if (!resolveFirst)
|
|
return new Promise((resolve) => {
|
|
resolveFirst = () => {
|
|
view.checkpoint = serverCheckpoint(body);
|
|
resolve(structuredClone(view));
|
|
};
|
|
});
|
|
view.checkpoint = serverCheckpoint(body);
|
|
return structuredClone(view);
|
|
});
|
|
h.render();
|
|
await h.settle();
|
|
assert.equal(h.timers.size, 0);
|
|
h.render({ payload: { ...request.payload, title: 'First' } });
|
|
assert.equal(h.timers.size, 1);
|
|
await h.advance();
|
|
assert.equal(h.calls.filter((c) => c.method === 'POST').length, 1);
|
|
h.render({ payload: { ...request.payload, title: 'Newer while saving' } });
|
|
assert.equal(h.timers.size, 0);
|
|
resolveFirst();
|
|
await h.settle();
|
|
assert.equal(h.timers.size, 1);
|
|
await h.advance();
|
|
const posts = h.calls.filter((c) => c.method === 'POST');
|
|
assert.equal(posts.length, 2);
|
|
assert.equal(posts[1].body.version, 1);
|
|
assert.equal(posts[1].body.payload.title, 'Newer while saving');
|
|
assert.equal(Object.hasOwn(posts[0].body, 'submit'), false);
|
|
assert.equal(Object.hasOwn(posts[0].body, 'consent'), false);
|
|
assert.equal(Object.hasOwn(posts[0].body, 'context_reviewed'), false);
|
|
assert.equal(h.timers.size, 0);
|
|
assert.deepEqual(h.busy, [true, false, true, false]);
|
|
h.unmount();
|
|
});
|
|
|
|
await test('ambiguous network failure repeats the exact attempt before saving newer local answers', async () => {
|
|
let view = initialView(),
|
|
failed = false;
|
|
const h = harness(async (url, body, method) => {
|
|
if (method === 'GET') return structuredClone(view);
|
|
if (!failed) {
|
|
failed = true;
|
|
view.checkpoint = serverCheckpoint(body);
|
|
throw Error('SYNTHETIC connection lost after commit');
|
|
}
|
|
if (body.version === 0) return structuredClone(view);
|
|
view.checkpoint = serverCheckpoint(body);
|
|
return structuredClone(view);
|
|
});
|
|
h.render();
|
|
await h.settle();
|
|
h.render({
|
|
payload: { ...request.payload, title: 'Committed but unacknowledged' },
|
|
});
|
|
await h.advance();
|
|
h.render({ payload: { ...request.payload, title: 'Newer' } });
|
|
assert.equal(h.timers.size, 0);
|
|
h.button('Tentar novamente a mesma gravação').props.onClick();
|
|
await h.settle();
|
|
let posts = h.calls.filter((c) => c.method === 'POST');
|
|
assert.deepEqual(posts[0].body, posts[1].body);
|
|
assert.equal(h.props.payload.title, 'Newer');
|
|
await h.advance();
|
|
posts = h.calls.filter((c) => c.method === 'POST');
|
|
assert.equal(posts[2].body.version, 1);
|
|
assert.notEqual(posts[2].body.operation_key, posts[1].body.operation_key);
|
|
h.unmount();
|
|
});
|
|
|
|
await test('server normalization does not cause autosave loops and validation failures wait for an actual edit', async () => {
|
|
let view = initialView(),
|
|
invalid = true;
|
|
const h = harness(async (url, body, method) => {
|
|
if (method === 'GET') return structuredClone(view);
|
|
if (invalid)
|
|
throw new ValidationError('SYNTHETIC invalid type', {
|
|
'checkpoint.payload.title': 'Invalid',
|
|
});
|
|
view.checkpoint = serverCheckpoint(body);
|
|
view.checkpoint.payload.title = body.payload.title.trim() || null;
|
|
return structuredClone(view);
|
|
});
|
|
h.render();
|
|
await h.settle();
|
|
h.render({ payload: { ...request.payload, title: ' text ' } });
|
|
await h.advance();
|
|
await h.settle();
|
|
assert.equal(h.timers.size, 0);
|
|
invalid = false;
|
|
h.render({ payload: { ...request.payload, title: ' corrected ' } });
|
|
await h.settle();
|
|
assert.equal(h.timers.size, 1);
|
|
await h.advance();
|
|
assert.equal(h.timers.size, 0);
|
|
assert.equal(h.calls.filter((c) => c.method === 'POST').length, 2);
|
|
h.unmount();
|
|
});
|
|
|
|
await test('stored drafts are compared before adoption and a failed recovery is retried without changing its chosen answers', async () => {
|
|
let view = initialView(),
|
|
failed = false;
|
|
view.request.version = 2;
|
|
view.request.payload.title = 'External';
|
|
view.checkpoint = {
|
|
...serverCheckpoint({
|
|
version: 0,
|
|
request_version: 1,
|
|
payload: {
|
|
...request.payload,
|
|
title: 'Saved',
|
|
declaration: 'declined',
|
|
},
|
|
reason: 'Saved reason',
|
|
edited_keys: ['title', 'declaration'],
|
|
}),
|
|
base_payload: structuredClone(request.payload),
|
|
};
|
|
const h = harness(async (url, body, method) => {
|
|
if (method === 'GET') return structuredClone(view);
|
|
view.checkpoint = serverCheckpoint(body);
|
|
if (!failed) {
|
|
failed = true;
|
|
throw Error('SYNTHETIC recovery response lost');
|
|
}
|
|
return structuredClone(view);
|
|
});
|
|
h.render();
|
|
await h.settle();
|
|
assert.equal(h.recovered.length, 0);
|
|
assert.equal(
|
|
h.button('Recuperar as respostas escolhidas').props.disabled,
|
|
true,
|
|
);
|
|
const select = nodes(h.tree).find(
|
|
(n) => n.type === 'select' && n.props.value === '',
|
|
);
|
|
select.props.onChange({ target: { value: 'saved' } });
|
|
await h.settle();
|
|
assert.equal(
|
|
h.button('Recuperar as respostas escolhidas').props.disabled,
|
|
false,
|
|
);
|
|
h.button('Recuperar as respostas escolhidas').props.onClick();
|
|
await h.settle();
|
|
assert.equal(h.recovered.length, 0);
|
|
h.button('Recuperar as respostas escolhidas').props.onClick();
|
|
await h.settle();
|
|
assert.equal(h.recovered.length, 1);
|
|
assert.equal(h.recovered[0].current.version, 2);
|
|
assert.equal(h.recovered[0].payload.title, 'Saved');
|
|
assert.equal(h.recovered[0].payload.declaration, 'declined');
|
|
const posts = h.calls.filter((c) => c.method === 'POST');
|
|
assert.deepEqual(posts[0].body, posts[1].body);
|
|
assert.equal(h.blocked.at(-1), false);
|
|
h.unmount();
|
|
});
|
|
|
|
await test('manual operations pause autosave, unavailable installation leaves manual access, and unsaved navigation can be cancelled', async () => {
|
|
let view = initialView();
|
|
const h = harness(async (url, body, method) => {
|
|
if (method === 'GET') return structuredClone(view);
|
|
view.checkpoint = serverCheckpoint(body);
|
|
return structuredClone(view);
|
|
});
|
|
h.render();
|
|
await h.settle();
|
|
h.render({
|
|
payload: { ...request.payload, title: 'Unsaved' },
|
|
paused: true,
|
|
});
|
|
assert.equal(h.timers.size, 0);
|
|
let prevented = false;
|
|
h.before[0]({
|
|
detail: { visit: { method: 'get' } },
|
|
preventDefault() {
|
|
prevented = true;
|
|
},
|
|
});
|
|
assert.equal(prevented, true);
|
|
h.render({ paused: false });
|
|
await h.advance();
|
|
assert.equal(h.timers.size, 0);
|
|
h.unmount();
|
|
assert.equal(h.before.length, 0);
|
|
assert.equal(h.listeners.size, 0);
|
|
view.installed = false;
|
|
const fallback = harness(async () => structuredClone(view));
|
|
fallback.render();
|
|
await fallback.settle();
|
|
fallback.render({ payload: { title: 'Manual still available' } });
|
|
assert.equal(fallback.timers.size, 0);
|
|
assert.ok(!fallback.blocked.includes(true));
|
|
fallback.unmount();
|
|
});
|
|
|
|
await test('request recovery adopts the actual server version and returned scope while clearing submission and context approvals', () => {
|
|
const cells = [],
|
|
forms = [],
|
|
sent = [];
|
|
let stateCursor = 0,
|
|
formCursor = 0;
|
|
const Empty = () => null,
|
|
Recovery = () => null,
|
|
Attachment = () => null;
|
|
const Page = load('../../resources/js/pages/desk/request.tsx', {
|
|
react: {
|
|
useState(initial) {
|
|
const i = stateCursor++;
|
|
if (!(i in cells)) cells[i] = initial;
|
|
return [
|
|
cells[i],
|
|
(value) =>
|
|
(cells[i] =
|
|
typeof value === 'function'
|
|
? value(cells[i])
|
|
: value),
|
|
];
|
|
},
|
|
},
|
|
'@inertiajs/react': {
|
|
Link: 'a',
|
|
useForm(initial) {
|
|
const i = formCursor++;
|
|
forms[i] ??= {
|
|
data: structuredClone(initial),
|
|
errors: {},
|
|
processing: false,
|
|
setData(value, next) {
|
|
this.data =
|
|
typeof value === 'function'
|
|
? value(this.data)
|
|
: { ...this.data, [value]: next };
|
|
},
|
|
transform(fn) {
|
|
this.mapping = fn;
|
|
},
|
|
put(url) {
|
|
sent.push({
|
|
url,
|
|
data: this.mapping
|
|
? this.mapping(this.data)
|
|
: this.data,
|
|
});
|
|
},
|
|
};
|
|
return forms[i];
|
|
},
|
|
},
|
|
'@/components/desk-draft-recovery': {
|
|
__esModule: true,
|
|
default: Recovery,
|
|
},
|
|
'@/components/dossier-prefill': { DossierPrefill: Empty },
|
|
'@/components/country-select': { default: Empty },
|
|
'@/components/investment-territory': { TerritoryFields: Empty },
|
|
'@/components/desk-internal-operations': {
|
|
DocumentReview: Empty,
|
|
RequestOperations: Empty,
|
|
},
|
|
'@/components/desk-typed-choice': { default: Empty },
|
|
'@/components/desk-attachment-field': { default: Attachment },
|
|
'@/lib/operation-key': { createOperationKey: () => 'synthetic' },
|
|
'@/lib/desk-field-visibility': load(
|
|
'../../resources/js/lib/desk-field-visibility.ts',
|
|
),
|
|
'@/lib/desk-field-values': load(
|
|
'../../resources/js/lib/desk-field-values.ts',
|
|
),
|
|
'@/pages/investment/shared': {
|
|
Frame: 'main',
|
|
card: '',
|
|
input: '',
|
|
button: '',
|
|
},
|
|
}).default;
|
|
const props = {
|
|
request: { ...request, analyst_id: null, due_at: null },
|
|
case: { id: 'synthetic-case', title: 'Synthetic' },
|
|
definition: {
|
|
title: 'Synthetic',
|
|
result: 'Internal',
|
|
fields: {
|
|
title: { type: 'text', label: 'Title', required: true },
|
|
support: {
|
|
type: 'attachment',
|
|
label: 'Support',
|
|
required: false,
|
|
max_files: 2,
|
|
max_size_kb: 2,
|
|
mime_types: ['text/plain'],
|
|
},
|
|
choices: {
|
|
type: 'multiselect',
|
|
label: 'Choices',
|
|
required: false,
|
|
options: { 0: 'Zero' },
|
|
},
|
|
declaration: {
|
|
type: 'consent',
|
|
label: 'Declaration',
|
|
required: false,
|
|
statement: 'Synthetic',
|
|
},
|
|
},
|
|
},
|
|
owner: true,
|
|
manager: false,
|
|
analyst: false,
|
|
fieldsEditable: true,
|
|
states: { draft: 'Draft', returned: 'Returned' },
|
|
transitions: [],
|
|
findings: [],
|
|
messages: [],
|
|
analysts: [],
|
|
territoryPaths: {},
|
|
events: [],
|
|
documents: [],
|
|
payments: [],
|
|
caseVersion: 1,
|
|
countries: [],
|
|
toolPrefill: null,
|
|
};
|
|
const render = () => {
|
|
stateCursor = 0;
|
|
formCursor = 0;
|
|
return Page(props);
|
|
};
|
|
let tree = render();
|
|
forms[0].data.payload.title = 'Deliberate edit during upload';
|
|
nodes(tree)
|
|
.find((n) => n.type === Attachment)
|
|
.props.onBusy(true);
|
|
tree = render();
|
|
assert.equal(
|
|
nodes(tree).find((n) => n.type === Recovery).props.pendingWork,
|
|
true,
|
|
);
|
|
nodes(tree)
|
|
.find((n) => n.type === 'form')
|
|
.props.onSubmit({ preventDefault() {} });
|
|
assert.equal(sent.length, 0);
|
|
const uploaded = {
|
|
id: 'link-first',
|
|
document_id: 'binary-one',
|
|
file_name: 'Synthetic original',
|
|
status: 'quarantined',
|
|
malware_scan_status: 'pending',
|
|
};
|
|
nodes(tree)
|
|
.find((n) => n.type === Attachment)
|
|
.props.onInventory(
|
|
{ documents: [uploaded], case_version: 2, link_id: uploaded.id },
|
|
true,
|
|
);
|
|
nodes(tree)
|
|
.find((n) => n.type === Attachment)
|
|
.props.onBusy(false);
|
|
tree = render();
|
|
assert.equal(forms[0].data.payload.title, 'Deliberate edit during upload');
|
|
assert.deepEqual(forms[0].data.payload.support, ['link-first']);
|
|
const alias = { ...uploaded, id: 'link-alias' };
|
|
nodes(tree)
|
|
.find((n) => n.type === Attachment)
|
|
.props.onInventory(
|
|
{
|
|
documents: [uploaded, alias],
|
|
case_version: 2,
|
|
link_id: alias.id,
|
|
},
|
|
true,
|
|
);
|
|
tree = render();
|
|
assert.deepEqual(forms[0].data.payload.support, ['link-first']);
|
|
props.caseVersion = 2;
|
|
props.documents = [
|
|
{ ...uploaded, status: 'active', malware_scan_status: 'clean' },
|
|
];
|
|
tree = render();
|
|
assert.equal(
|
|
nodes(tree).find((n) => n.type === Attachment).props.documents[0]
|
|
.malware_scan_status,
|
|
'clean',
|
|
);
|
|
const refreshed = {
|
|
...uploaded,
|
|
malware_scan_status: 'synthetic-new-status',
|
|
};
|
|
nodes(tree)
|
|
.find((n) => n.type === Attachment)
|
|
.props.onInventory({ documents: [refreshed], case_version: 2 }, false);
|
|
tree = render();
|
|
assert.equal(
|
|
nodes(tree).find((n) => n.type === Attachment).props.documents[0]
|
|
.malware_scan_status,
|
|
'synthetic-new-status',
|
|
);
|
|
forms[0].data.consent = true;
|
|
forms[0].data.context_reviewed = true;
|
|
forms[0].data.context_revision = 8;
|
|
const restore = nodes(tree).find((n) => n.type === Recovery);
|
|
restore.props.onRecover(
|
|
{
|
|
title: 'Current official draft',
|
|
choices: ['0'],
|
|
declaration: 'declined',
|
|
},
|
|
'Recovered reason',
|
|
{
|
|
...request,
|
|
version: 3,
|
|
status: 'returned',
|
|
editable_fields: ['choices'],
|
|
},
|
|
);
|
|
tree = render();
|
|
assert.equal(forms[0].data.version, 3);
|
|
assert.equal(forms[0].data.consent, false);
|
|
assert.equal(forms[0].data.context_reviewed, false);
|
|
assert.equal(forms[0].data.context_revision, 0);
|
|
assert.equal(forms[0].data.payload.declaration, 'declined');
|
|
const form = nodes(tree).find((n) => n.type === 'form');
|
|
form.props.onSubmit({ preventDefault() {} });
|
|
assert.equal(sent[0].data.version, 3);
|
|
assert.equal(sent[0].data.submit, false);
|
|
nodes(tree)
|
|
.find((n) => n.type === Recovery)
|
|
.props.onBusy(true);
|
|
tree = render();
|
|
nodes(tree)
|
|
.find((n) => n.type === 'form')
|
|
.props.onSubmit({ preventDefault() {} });
|
|
assert.equal(sent.length, 1);
|
|
});
|