319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { harness, props, text } from './helpers/education-review.mjs';
|
|
const form = 'education-review';
|
|
|
|
function historyPage(page = 1) {
|
|
return {
|
|
data: [], total: 24, current_page: page, last_page: 2,
|
|
prev_page_url: page === 2 ? '/management/education/demands/one?scope=review&history_page=1' : null,
|
|
next_page_url: page === 1 ? '/management/education/demands/one?scope=review&history_page=2' : null,
|
|
};
|
|
}
|
|
|
|
test('history paging keeps the prepared review and does not ask to discard it', () => {
|
|
const h = harness(props({ history: historyPage() }));
|
|
h.change(form, reason, 'SYNTHETIC reasoning still being prepared.');
|
|
const next = h.all().find(node => node.type === 'a' && text(node) === 'Seguinte');
|
|
assert.equal(next.props.preserveState, true);
|
|
assert.equal(next.props.preserveScroll, true);
|
|
assert.match(next.props.href, /scope=review&history_page=2$/);
|
|
assert.equal(h.leave(next.props.preserveState), false);
|
|
assert.equal(h.confirmCalls, 0);
|
|
h.render({ ...h.props, history: historyPage(2) });
|
|
assert.equal(h.field(form, reason).props.value, 'SYNTHETIC reasoning still being prepared.');
|
|
const previous = h.all().find(node => node.type === 'a' && text(node) === 'Anterior');
|
|
assert.equal(previous.props.preserveState, true);
|
|
assert.equal(previous.props.preserveScroll, true);
|
|
assert.equal(h.sent.length, 0);
|
|
assert.equal(h.leave(), true); // Leaving the declaration still protects unsaved work.
|
|
});
|
|
|
|
test('history paging keeps an uncertain attempt recoverable under the same command key', () => {
|
|
const h = harness(props({ history: historyPage() }));
|
|
const request = prepared(h);
|
|
h.fail(request);
|
|
h.render({ ...h.props, history: historyPage(2) });
|
|
const retry = h.submit(form);
|
|
assert.deepEqual(retry.data, request.data);
|
|
assert.equal(h.sent.length, 2); // One explicit retry; paging itself sends no decision.
|
|
});
|
|
|
|
test('history paging cannot silently approve a newer declaration version', () => {
|
|
const h = harness(props({ history: historyPage() }));
|
|
h.change(form, reason, 'SYNTHETIC review prepared against version one.');
|
|
h.render({
|
|
...h.props, history: historyPage(2),
|
|
declaration: { ...h.props.declaration, version_no: 2, quantity: 31 },
|
|
});
|
|
h.submit(form);
|
|
assert.equal(h.sent.length, 0);
|
|
assert.equal(h.field(form, reason).props.value, 'SYNTHETIC review prepared against version one.');
|
|
assert.match(text(h.tree), /A declaração mudou/);
|
|
h.click('Confirmar a leitura desta versão');
|
|
const request = h.submit(form);
|
|
assert.equal(request.data.version_no, 2);
|
|
});
|
|
|
|
test('history paging with a different actor remounts instead of disclosing the prepared reason', () => {
|
|
const h = harness(props({ history: historyPage() }));
|
|
h.change(form, reason, 'SYNTHETIC private reasoning of the previous actor.');
|
|
h.render({
|
|
...h.props, actorId: 3, history: historyPage(2),
|
|
declaration: { ...h.props.declaration, review_context: { actor_id: 3, commands: [] } },
|
|
});
|
|
assert.equal(h.field(form, reason).props.value, '');
|
|
assert.equal(h.sent.length, 0);
|
|
});
|
|
|
|
test('the private detail renders preserved reasons, unavailable entries and contextual history pagination', () => {
|
|
const h = harness(
|
|
props({
|
|
history: {
|
|
data: [
|
|
{
|
|
id: 'one',
|
|
version: 3,
|
|
actor: 'SYNTHETIC reviewer',
|
|
occurred_at: '2026-11-03',
|
|
available: true,
|
|
status: 'rejected',
|
|
reason: 'SYNTHETIC preserved reason',
|
|
previous_reason: 'SYNTHETIC older reason',
|
|
},
|
|
{
|
|
id: 'two',
|
|
version: 2,
|
|
actor: null,
|
|
occurred_at: '2026-11-02',
|
|
available: false,
|
|
status: null,
|
|
reason: null,
|
|
previous_reason: null,
|
|
},
|
|
],
|
|
total: 12,
|
|
current_page: 1,
|
|
last_page: 2,
|
|
prev_page_url: null,
|
|
next_page_url:
|
|
'/management/education/demands/one?scope=mine&history_page=2',
|
|
},
|
|
}),
|
|
);
|
|
assert.match(text(h.tree), /Registos de revisão disponíveis/);
|
|
assert.match(text(h.tree), /SYNTHETIC preserved reason/);
|
|
assert.match(
|
|
text(h.tree),
|
|
/Fundamento anterior capturado: SYNTHETIC older reason/,
|
|
);
|
|
assert.match(text(h.tree), /Fundamento indisponível para leitura/);
|
|
assert.ok(h.tree);
|
|
});
|
|
test('the submitter sees the declaration without being asked to review it', () => {
|
|
const initial = props();
|
|
initial.declaration.can_review = false;
|
|
initial.declaration.review_unavailable = 'self';
|
|
const h = harness(initial);
|
|
assert.equal(h.button('Guardar decisão'), undefined);
|
|
assert.match(text(h.tree), /outro gestor/);
|
|
});
|
|
test('a newer declaration cannot silently replace the version on which the review was prepared', () => {
|
|
const h = harness();
|
|
h.change(
|
|
form,
|
|
'Justificação e evidência da revisão',
|
|
'SYNTHETIC prepared reason with evidence.',
|
|
);
|
|
h.render({
|
|
...h.props,
|
|
declaration: { ...h.props.declaration, version_no: 2, quantity: 30 },
|
|
});
|
|
h.submit(form);
|
|
assert.equal(h.sent.length, 0);
|
|
});
|
|
|
|
const reason = 'Justificação e evidência da revisão';
|
|
function prepared(h) {
|
|
h.change(form, reason, 'SYNTHETIC independent review with evidence.');
|
|
return h.submit(form);
|
|
}
|
|
function accepted(h, request = h.sent.at(-1), patch = {}) {
|
|
return {
|
|
...h.props,
|
|
declaration: {
|
|
...h.props.declaration,
|
|
version_no: request.data.version_no + 1,
|
|
status: request.data.status,
|
|
review_reason: request.data.review_reason,
|
|
review_context: {
|
|
actor_id: h.props.actorId,
|
|
commands: [
|
|
{
|
|
key: request.data.client_key,
|
|
version: request.data.version_no + 1,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
...patch,
|
|
};
|
|
}
|
|
test('explicit comparison adopts the current version without discarding the prepared decision', () => {
|
|
const h = harness();
|
|
h.change(form, reason, 'SYNTHETIC prepared reason with evidence.');
|
|
h.render({
|
|
...h.props,
|
|
declaration: { ...h.props.declaration, version_no: 2, quantity: 30 },
|
|
});
|
|
assert.match(text(h.tree), /Analisada: 5.*Actual: 30/);
|
|
h.click('Confirmar a leitura desta versão');
|
|
const request = h.submit(form);
|
|
assert.equal(request.data.version_no, 2);
|
|
assert.equal(
|
|
request.data.review_reason,
|
|
'SYNTHETIC prepared reason with evidence.',
|
|
);
|
|
});
|
|
test('confirmation clears only sent event values and preserves later edits', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.change(form, reason, 'SYNTHETIC later reason with evidence.');
|
|
h.change(form, 'Decisão interna', 'rejected');
|
|
h.success(accepted(h, request), request);
|
|
assert.equal(
|
|
h.field(form, reason).props.value,
|
|
'SYNTHETIC later reason with evidence.',
|
|
);
|
|
assert.equal(h.field(form, 'Decisão interna').props.value, 'rejected');
|
|
const next = h.submit(form);
|
|
assert.equal(next.data.version_no, 2);
|
|
assert.notEqual(next.data.client_key, request.data.client_key);
|
|
});
|
|
test('an uncertain result retries exactly the original version, decision and key', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.fail(request);
|
|
h.change(form, reason, 'SYNTHETIC later reason with evidence.');
|
|
const retry = h.submit(form);
|
|
assert.deepEqual(retry.data, request.data);
|
|
h.success(accepted(h, retry), retry);
|
|
assert.equal(
|
|
h.field(form, reason).props.value,
|
|
'SYNTHETIC later reason with evidence.',
|
|
);
|
|
});
|
|
test('a fresh authorised read confirms the receipt without sending another decision', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.fail(request, 500);
|
|
h.click('Actualizar dados da declaração');
|
|
assert.deepEqual(h.reloads[0].only, [
|
|
'declaration',
|
|
'references',
|
|
'today',
|
|
'actorId',
|
|
'kind',
|
|
'history',
|
|
]);
|
|
h.refreshed(accepted(h, request));
|
|
assert.equal(h.sent.length, 1);
|
|
assert.equal(h.field(form, reason).props.value, '');
|
|
assert.equal(h.leave(), false);
|
|
assert.match(text(h.tree), /Revisão registada/);
|
|
});
|
|
test('another actor cannot acknowledge or erase a local pending review', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
request.options.onSuccess({ props: accepted(h, request, { actorId: 3 }) });
|
|
request.form.processing = false;
|
|
h.render();
|
|
assert.equal(
|
|
h.field(form, reason).props.value,
|
|
'SYNTHETIC independent review with evidence.',
|
|
);
|
|
assert.deepEqual(h.submit(form).data, request.data);
|
|
});
|
|
test('an obsolete callback cannot alter the draft of the next account', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.render({
|
|
...h.props,
|
|
actorId: 3,
|
|
declaration: {
|
|
...h.props.declaration,
|
|
review_context: { actor_id: 3, commands: [] },
|
|
},
|
|
});
|
|
h.change(form, reason, 'SYNTHETIC next account review.');
|
|
request.options.onSuccess({ props: accepted(h, request, { actorId: 1 }) });
|
|
request.form.processing = false;
|
|
h.render();
|
|
assert.equal(
|
|
h.field(form, reason).props.value,
|
|
'SYNTHETIC next account review.',
|
|
);
|
|
});
|
|
test('a later decision remains current when an earlier receipt is acknowledged', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.fail(request);
|
|
const next = accepted(h, request);
|
|
next.declaration.version_no = 3;
|
|
next.declaration.status = 'rejected';
|
|
h.render(next);
|
|
assert.match(text(h.tree), /A declaração mudou/);
|
|
assert.equal(h.sent.length, 1);
|
|
h.submit(form);
|
|
assert.equal(h.sent.length, 1);
|
|
h.click('Confirmar a leitura desta versão');
|
|
const fresh = h.submit(form);
|
|
assert.equal(fresh.data.version_no, 3);
|
|
assert.notEqual(fresh.data.client_key, request.data.client_key);
|
|
});
|
|
test('a competing newer version without the receipt requires explicit adoption before a new decision', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.fail(request, 409);
|
|
h.click('Actualizar declaração');
|
|
h.refreshed({
|
|
...h.props,
|
|
declaration: { ...h.props.declaration, version_no: 2, quantity: 20 },
|
|
});
|
|
h.click('Confirmar a leitura desta versão');
|
|
const next = h.submit(form);
|
|
assert.equal(next.data.version_no, 2);
|
|
assert.notEqual(next.data.client_key, request.data.client_key);
|
|
});
|
|
test('expired declarations retain prepared text without a new decision form', () => {
|
|
const h = harness(),
|
|
request = prepared(h);
|
|
h.fail(request, 422);
|
|
h.render({
|
|
...h.props,
|
|
declaration: {
|
|
...h.props.declaration,
|
|
can_review: false,
|
|
review_unavailable: 'expired',
|
|
},
|
|
});
|
|
assert.equal(h.button('Guardar decisão'), undefined);
|
|
assert.equal(h.button('Recuperar a mesma revisão'), undefined);
|
|
assert.match(text(h.tree), /Fundamento preparado nesta página/);
|
|
assert.match(text(h.tree), /SYNTHETIC independent review/);
|
|
});
|
|
test('validation preserves the draft and a decision-only change has one leave guard', () => {
|
|
const h = harness();
|
|
h.change(form, 'Decisão interna', 'rejected');
|
|
assert.equal(h.leave(), true);
|
|
assert.equal(h.confirmCalls, 1);
|
|
const request = prepared(h);
|
|
h.validation({ review_reason: 'SYNTHETIC validation' }, request);
|
|
assert.equal(
|
|
h.field(form, reason).props.value,
|
|
'SYNTHETIC independent review with evidence.',
|
|
);
|
|
const next = h.submit(form);
|
|
assert.notEqual(next.data.client_key, request.data.client_key);
|
|
});
|