Files
Investidor-V2/tests/frontend/partnership-candidacies.test.mjs
T

612 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
harness,
props,
original,
nodes,
text,
} from './helpers/partnership-candidacy.mjs';
const ownProps = (patch = {}) =>
props({
owner: true,
canEdit: true,
actorId: 1,
interests: [{ ...structuredClone(original), can_edit: false }],
...patch,
});
test('current candidacy card links to its history without changing a prepared submission', () => {
const h = harness();
h.change('submission:10', 'Proposta curta', 'SYNTHETIC unsaved');
assert.ok(
nodes(h.render()).some(
(node) =>
node.props?.href === '/parcerias/candidaturas/10/historico',
),
);
assert.equal(h.submit('submission:10').data.proposal, 'SYNTHETIC unsaved');
});
const row = (scope, label) =>
nodes(scope).find(
(node) => node.type === 'tr' && text(node).startsWith(label),
);
function consent(h, name, checked) {
const input = nodes(h.form(name)).find(
(node) => node.type === 'input' && node.props.type === 'checkbox',
);
input.props.onChange({ target: { checked } });
h.render();
}
function saved(h, request = h.sent.at(-1), patch = {}) {
const action = request.data.action ?? 'submit';
const entry = structuredClone(
h.props.interests.find(
(interest) => interest.id === request.data.interest_id,
) ??
h.props.interests[0] ??
original,
);
entry.version = Number(request.data.version ?? 0) + 1;
if (action === 'submit') {
entry.organization_id = request.data.organization_id || null;
entry.proposal = request.data.proposal.trim();
entry.payload = Object.fromEntries(
Object.entries(request.data.payload).map(([key, value]) => [
key,
typeof value === 'string' ? value.trim() || null : value,
]),
);
entry.assessment = [];
entry.status = 'submitted';
entry.can_edit = true;
} else if (action === 'assess')
entry.assessment = structuredClone(request.data.assessment);
else
entry.status = {
clarify: 'clarification',
shortlist: 'shortlisted',
reject: 'rejected',
connect: 'negotiation',
withdraw: 'withdrawn',
}[action];
entry.commands = [
...(entry.commands ?? []),
{ key: request.data.client_key, version: entry.version, action },
];
return {
...h.props,
canStartInterest: false,
interests: [{ ...entry, ...patch }],
};
}
test('editing identifies the exact candidature and current proposal version, preserving accepted consent', () => {
const h = harness();
h.change('submission:10', 'Proposta curta', 'SYNTHETIC revised proposal');
const request = h.submit('submission:10');
assert.equal(request.data.interest_id, 10);
assert.equal(request.data.version, 1);
assert.equal(request.data.partnership_version, 4);
assert.equal(request.data.payload.consent, true);
assert.equal(request.data.payload.capacity, original.payload.capacity);
assert.match(text(h.form('submission:10')), /reinicia a avaliação actual/);
});
test('batched declaration and consent edits do not erase siblings', () => {
const h = harness();
h.change(
'submission:10',
'Capacidade disponível',
'SYNTHETIC new capacity',
false,
);
h.change(
'submission:10',
'Disponibilidade',
'SYNTHETIC new availability',
false,
);
h.render();
consent(h, 'submission:10', false);
const request = h.submit('submission:10');
assert.equal(request.data.payload.capacity, 'SYNTHETIC new capacity');
assert.equal(
request.data.payload.availability,
'SYNTHETIC new availability',
);
assert.equal(
request.data.payload.contribution,
original.payload.contribution,
);
assert.equal(request.data.payload.consent, false);
});
test('read only representation offers consultation without update or withdrawal forms', () => {
const h = harness(
props({
interests: [{ ...structuredClone(original), can_edit: false }],
}),
);
assert.equal(h.form('submission:10'), undefined);
assert.equal(h.form('decision:10'), undefined);
assert.equal(h.form('submission:new'), undefined);
assert.match(text(h.tree), /SYNTHETIC original proposal/);
});
test('remote edits require an explicit choice and merge untouched declarations only after adoption', () => {
const h = harness();
h.change(
'submission:10',
'Capacidade disponível',
'SYNTHETIC local capacity',
);
h.render({
...h.props,
interests: [
{
...structuredClone(original),
version: 2,
payload: {
...original.payload,
capacity: 'SYNTHETIC remote capacity',
availability: 'SYNTHETIC remote availability',
},
},
],
});
assert.equal(
h.button('Guardar interesse', h.form('submission:10')).props.disabled,
true,
);
h.click(
'Manter minha alteração',
row(h.form('submission:10'), 'Capacidade disponível'),
);
h.click(
'Usar este contexto e conservar as alterações escolhidas',
h.form('submission:10'),
);
const request = h.submit('submission:10');
assert.equal(request.data.version, 2);
assert.equal(request.data.payload.capacity, 'SYNTHETIC local capacity');
assert.equal(
request.data.payload.availability,
'SYNTHETIC remote availability',
);
});
test('a deliberate return to the original declaration is not treated as untouched', () => {
const h = harness();
h.change('submission:10', 'Proposta curta', 'SYNTHETIC temporary');
h.change('submission:10', 'Proposta curta', original.proposal);
h.render({
...h.props,
interests: [
{
...structuredClone(original),
version: 2,
proposal: 'SYNTHETIC remote proposal',
},
],
});
assert.match(
text(row(h.form('submission:10'), 'Proposta curta')),
/Escolha o valor a conservar/,
);
});
test('creation becomes the same editable candidature and preserves input made after sending', () => {
const h = harness(props({ interests: [], canStartInterest: true }));
h.change('submission:new', 'Proposta curta', 'SYNTHETIC first proposal');
consent(h, 'submission:new', true);
const first = h.submit('submission:new');
h.change('submission:new', 'Capacidade disponível', 'SYNTHETIC after send');
h.success(saved(h, first, { id: 12 }), first);
assert.equal(h.form('submission:12'), undefined);
assert.equal(
h.field('submission:new', 'Capacidade disponível').props.value,
'SYNTHETIC after send',
);
const next = h.submit('submission:new');
assert.equal(next.data.interest_id, 12);
assert.equal(next.data.version, 1);
assert.notEqual(next.data.client_key, first.data.client_key);
});
test('losing access to a candidature keeps only its local preparation mounted and disables commands', () => {
const h = harness();
h.change(
'submission:10',
'Capacidade disponível',
'SYNTHETIC retained private preparation',
);
h.render({ ...h.props, interests: [], canStartInterest: false });
assert.equal(
h.field('submission:10', 'Capacidade disponível').props.value,
'SYNTHETIC retained private preparation',
);
assert.equal(
h.button('Guardar interesse', h.form('submission:10')).props.disabled,
true,
);
assert.match(text(h.tree), /deixou de estar disponível/);
assert.equal(h.form('submission:new'), undefined);
});
test('withdrawal remains available when the proposal changes or its submission window closes', () => {
const h = harness();
h.change(
'decision:10',
'Motivo da retirada',
'SYNTHETIC voluntary withdrawal',
);
h.render({
...h.props,
acceptingInterests: false,
record: { ...h.props.record, version: 9, status: 'archived' },
});
assert.equal(h.button('Retirar candidatura').props.disabled, false);
const request = h.click('Retirar candidatura');
assert.equal(request.data.action, 'withdraw');
assert.equal(request.data.partnership_version, undefined);
assert.equal(request.data.weights_hash, undefined);
});
test('assessment edits keep state and evidence atomic across batched events', () => {
const h = harness(ownProps());
h.change('decision:10', 'Capacidade técnica/produtiva', 'met', false);
h.change(
'decision:10',
'Referência da evidência — Capacidade técnica/produtiva',
'SYNTHETIC checked evidence',
false,
);
h.render();
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC evidence reviewed',
);
const request = h.click('Guardar avaliação');
assert.deepEqual(request.data.assessment.capacity, {
state: 'met',
evidence: 'SYNTHETIC checked evidence',
});
assert.equal(request.data.partnership_version, 4);
assert.equal(request.data.weights_hash, 'a'.repeat(64));
});
test('proposal and criteria changes force review without silently changing prepared assessment', () => {
const h = harness(ownProps());
h.change('decision:10', 'Capacidade técnica/produtiva', 'met');
h.change(
'decision:10',
'Referência da evidência — Capacidade técnica/produtiva',
'SYNTHETIC own evidence',
);
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC review',
);
h.render({
...h.props,
record: { ...h.props.record, version: 5 },
interests: [
{
...h.props.interests[0],
match: {
...h.props.interests[0].match,
weights_hash: 'b'.repeat(64),
},
},
],
});
assert.equal(h.button('Guardar avaliação').props.disabled, true);
assert.equal(
h.field('decision:10', 'Capacidade técnica/produtiva').props.value,
'met',
);
h.click(
'Usar este contexto e conservar as alterações escolhidas',
h.form('decision:10'),
);
const request = h.click('Guardar avaliação');
assert.equal(request.data.partnership_version, 5);
assert.equal(request.data.weights_hash, 'b'.repeat(64));
});
test('separate criterion conflicts retain independent choices while the reason is edited', () => {
const h = harness(ownProps());
h.change('decision:10', 'Capacidade técnica/produtiva', 'met');
h.change('decision:10', 'Experiência comprovada', 'met');
h.render({
...h.props,
interests: [
{
...h.props.interests[0],
version: 2,
assessment: {
capacity: {
state: 'unmet',
evidence: 'SYNTHETIC remote capacity',
},
experience: {
state: 'unmet',
evidence: 'SYNTHETIC remote experience',
},
},
},
],
});
h.click(
'Manter minha alteração',
row(h.form('decision:10'), 'Capacidade técnica/produtiva'),
);
h.click(
'Usar valor actual',
row(h.form('decision:10'), 'Experiência comprovada'),
);
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC combined review',
);
assert.equal(
h.button(
'Usar este contexto e conservar as alterações escolhidas',
h.form('decision:10'),
).props.disabled,
false,
);
h.click(
'Usar este contexto e conservar as alterações escolhidas',
h.form('decision:10'),
);
assert.equal(
h.field('decision:10', 'Capacidade técnica/produtiva').props.value,
'met',
);
assert.equal(
h.field('decision:10', 'Experiência comprovada').props.value,
'unmet',
);
});
test('uncertain submission repeats the frozen command and retains later declarations', () => {
const h = harness();
h.change('submission:10', 'Proposta curta', 'SYNTHETIC first command');
const first = h.submit('submission:10');
h.fail(first);
h.change('submission:10', 'Proposta curta', 'SYNTHETIC later text');
const retry = h.submit('submission:10');
assert.deepEqual(retry.data, first.data);
h.success(saved(h, retry), retry);
assert.equal(
h.field('submission:10', 'Proposta curta').props.value,
'SYNTHETIC later text',
);
const next = h.submit('submission:10');
assert.equal(next.data.version, 2);
assert.notEqual(next.data.client_key, first.data.client_key);
});
test('uncertain owner decisions expose one recovery action and do not resend later changes', () => {
const h = harness(ownProps());
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC original decision',
);
const first = h.click('Guardar avaliação');
h.fail(first);
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC later reason',
);
assert.equal(h.button('Adicionar à lista curta'), undefined);
const retry = h.click('Recuperar acção enviada');
assert.deepEqual(retry.data, first.data);
h.success(saved(h, retry), retry);
assert.equal(
h.field('decision:10', 'Motivo da decisão ou pedido de esclarecimento')
.props.value,
'SYNTHETIC later reason',
);
});
test('non-assessment decisions use saved criteria and preserve unsubmitted assessment edits', () => {
const h = harness(ownProps());
h.change('decision:10', 'Capacidade técnica/produtiva', 'met');
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC shortlist saved unknown assessment',
);
assert.match(
text(h.form('decision:10')),
/outras decisões usam a avaliação actualmente guardada/,
);
const request = h.click('Adicionar à lista curta');
assert.deepEqual(request.data.assessment, {});
h.success(saved(h, request), request);
assert.equal(
h.field('decision:10', 'Capacidade técnica/produtiva').props.value,
'met',
);
assert.equal(
h.field('decision:10', 'Motivo da decisão ou pedido de esclarecimento')
.props.value,
'',
);
});
test('obsolete criteria remain in local preparation but are excluded from the current command', () => {
const h = harness(ownProps());
h.change(
'decision:10',
'Referência da evidência — Experiência comprovada',
'SYNTHETIC retired evidence',
);
h.render({
...h.props,
interests: [
{
...h.props.interests[0],
match: {
...h.props.interests[0].match,
weights_hash: 'b'.repeat(64),
criteria: [h.props.interests[0].match.criteria[0]],
},
},
],
});
assert.match(text(h.form('decision:10')), /SYNTHETIC retired evidence/);
h.click(
'Usar este contexto e conservar as alterações escolhidas',
h.form('decision:10'),
);
h.change(
'decision:10',
'Motivo da decisão ou pedido de esclarecimento',
'SYNTHETIC current criteria only',
);
const request = h.click('Guardar avaliação');
assert.equal(request.data.assessment.experience, undefined);
h.success(saved(h, request), request);
assert.equal(
h.button(
'Usar este contexto e conservar as alterações escolhidas',
h.form('decision:10'),
),
undefined,
);
h.click(
'Descartar preparação dos critérios anteriores',
h.form('decision:10'),
);
assert.equal(h.unload(), false);
});
test('an acknowledgement of an older committed operation cannot accept someone else’s later values', () => {
const h = harness();
h.change('submission:10', 'Proposta curta', 'SYNTHETIC submitted text');
const request = h.submit('submission:10');
h.change(
'submission:10',
'Contribuição proposta',
'SYNTHETIC unsent contribution',
);
const later = saved(h, request, {
version: 3,
payload: {
...request.data.payload,
contribution: 'SYNTHETIC remote contribution',
},
});
h.success(later, request);
assert.equal(
h.field('submission:10', 'Contribuição proposta').props.value,
'SYNTHETIC unsent contribution',
);
assert.equal(
h.button('Guardar interesse', h.form('submission:10')).props.disabled,
true,
);
assert.match(text(h.form('submission:10')), /dados posteriores/);
});
test('wrong record acknowledgement conserves preparation and never authorizes another target', () => {
const h = harness();
h.change('submission:10', 'Proposta curta', 'SYNTHETIC local preparation');
const request = h.submit('submission:10');
h.success({ ...h.props, record: { ...h.props.record, id: 999 } }, request);
assert.equal(
h.field('submission:10', 'Proposta curta').props.value,
'SYNTHETIC local preparation',
);
assert.equal(
h.button('Recuperar submissão enviada', h.form('submission:10')).props
.disabled,
true,
);
});
test('one navigation guard covers independent candidature drafts and permits preserved comparison refresh', () => {
const h = harness();
h.change('submission:10', 'Capacidade disponível', 'SYNTHETIC draft');
h.change('decision:10', 'Motivo da retirada', 'SYNTHETIC reason');
assert.equal(h.leave(), true);
assert.equal(h.confirmCalls, 1);
assert.equal(h.leave(true), false);
assert.equal(h.unload(), true);
});
test('a different authenticated account receives no private unsent preparation', () => {
const h = harness();
h.change(
'submission:10',
'Proposta curta',
'SYNTHETIC private unsent text',
);
h.render({ ...h.props, actorId: 3 });
assert.equal(
h.field('submission:10', 'Proposta curta').props.value,
original.proposal,
);
});
test('real API empty-array assessments and legacy accepted consent remain editable without fabricated assessment', () => {
const h = harness(
props({
interests: [
{
...structuredClone(original),
assessment: [],
payload: { ...original.payload, consent: 'yes' },
},
],
}),
);
assert.ok(h.form('submission:10'));
const input = nodes(h.form('submission:10')).find(
(node) => node.type === 'input' && node.props.type === 'checkbox',
);
assert.equal(input.props.checked, true);
const owner = harness(ownProps());
assert.equal(
owner.field('decision:10', 'Capacidade técnica/produtiva').props.value,
'unknown',
);
});
test('an indeterminate matching range is explained without a false percentage', () => {
const h = harness(
props({
interests: [
{
...structuredClone(original),
match: {
...original.match,
score_min: null,
score_max: null,
},
},
],
}),
);
assert.match(text(h.tree), /Correspondência: Por determinar/);
assert.doesNotMatch(text(h.tree), /–%/);
});
test('saved matching weights, mandatory status, evidence and explanation remain visible to the parties', () => {
const criterion = {
...original.match.criteria[0],
state: 'met',
evidence: 'SYNTHETIC preserved source',
explanation: 'SYNTHETIC preserved explanation',
};
const h = harness(
props({
interests: [
{
...structuredClone(original),
assessment: {
capacity: {
state: 'met',
evidence: criterion.evidence,
},
},
match: { ...original.match, criteria: [criterion] },
},
],
}),
);
const item = nodes(h.tree).find(
(node) => node.type === 'li' && text(node).includes(criterion.label),
);
assert.match(text(item), /peso 20/);
assert.match(text(item), /Obrigatório/);
assert.match(text(item), /Evidência: SYNTHETIC preserved source/);
assert.match(text(item), /SYNTHETIC preserved explanation/);
});