327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { harness, props, text } from './helpers/crm-task-preparation.mjs';
|
|
const form = 'new-crm-task';
|
|
const taskId = '20000000-0000-4000-8000-000000000001';
|
|
function fill(h, title = 'SYNTHETIC prepared task') {
|
|
h.change(form, 'Tarefa', title);
|
|
h.change(form, 'Prazo (hora local)', '2027-11-04T10:30');
|
|
}
|
|
function savedContext(h, call = h.draftRequests.at(-1)) {
|
|
return {
|
|
...h.props.taskCreation,
|
|
preparation: {
|
|
installed: true,
|
|
version: call.data.version + 1,
|
|
operation_key: call.data.operation_key,
|
|
snapshot: call.data.snapshot,
|
|
unavailable: false,
|
|
updated_at: '2026-11-03T12:00:00Z',
|
|
},
|
|
};
|
|
}
|
|
function pageWith(context, patch = {}) {
|
|
return props({ taskCreation: context, ...patch });
|
|
}
|
|
test('automatic preparation survives a new page instance and adoption is explicit', async () => {
|
|
const first = harness();
|
|
fill(first);
|
|
await first.tick();
|
|
assert.equal(first.sent.length, 0);
|
|
assert.equal(first.draftRequests.length, 1);
|
|
const context = savedContext(first);
|
|
await first.respond(context);
|
|
const next = harness(pageWith(context));
|
|
assert.equal(next.field(form, 'Tarefa').props.value, '');
|
|
assert.ok(next.button('Comparar preparação guardada'));
|
|
next.click('Comparar preparação guardada');
|
|
next.click('Aplicar escolhas e retomar');
|
|
assert.equal(
|
|
next.field(form, 'Tarefa').props.value,
|
|
'SYNTHETIC prepared task',
|
|
);
|
|
assert.equal(
|
|
new Date(
|
|
next.field(form, 'Prazo (hora local)').props.value,
|
|
).toISOString(),
|
|
context.preparation.snapshot.values.due_at,
|
|
);
|
|
assert.equal(next.sent.length, 0);
|
|
});
|
|
test('creation waits until its frozen command has been checkpointed', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
assert.equal(h.sent.length, 0);
|
|
assert.equal(h.draftRequests.length, 1);
|
|
const context = savedContext(h);
|
|
await h.respond(context);
|
|
assert.equal(h.sent.length, 1);
|
|
assert.deepEqual(
|
|
h.sent[0].data,
|
|
context.preparation.snapshot.pending.values,
|
|
);
|
|
});
|
|
test('an uncertain preparation write retries exactly its key and preserves later edits', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
const first = h.draftRequests[0];
|
|
h.change(form, 'Tarefa', 'SYNTHETIC later');
|
|
await h.rejectDraft(first);
|
|
h.click('Voltar a guardar preparação');
|
|
const retry = h.draftRequests.at(-1);
|
|
assert.deepEqual(retry.data, first.data);
|
|
await h.respond(savedContext(h, retry), retry);
|
|
assert.equal(h.sent.length, 0);
|
|
assert.equal(h.field(form, 'Tarefa').props.value, 'SYNTHETIC later');
|
|
h.submit(form);
|
|
await h.flush();
|
|
assert.equal(h.sent.length, 1);
|
|
assert.equal(h.sent[0].data.title, 'SYNTHETIC prepared task');
|
|
});
|
|
test('a new page recovers an uncertain business command with its original key', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
const context = savedContext(h);
|
|
await h.respond(context);
|
|
const request = h.sent[0];
|
|
h.fail(request, 500);
|
|
const next = harness(pageWith(context));
|
|
next.click('Comparar preparação guardada');
|
|
next.click('Aplicar escolhas e retomar');
|
|
assert.equal(next.sent.length, 0);
|
|
next.submit(form);
|
|
assert.deepEqual(next.sent[0].data, request.data);
|
|
});
|
|
test('a committed creation is acknowledged after reopening without another business request', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
const context = savedContext(h);
|
|
await h.respond(context);
|
|
const request = h.sent[0];
|
|
const stored = structuredClone(context);
|
|
stored.preparation.snapshot.values.title = 'SYNTHETIC later';
|
|
stored.preparation.snapshot.generation.title++;
|
|
stored.commands = [
|
|
{ key: request.data.client_key, task_id: taskId, version: 1 },
|
|
];
|
|
const next = harness(
|
|
pageWith(stored, {
|
|
lead: {
|
|
...h.props.lead,
|
|
tasks: [
|
|
{
|
|
id: taskId,
|
|
title: request.data.title,
|
|
due_at: request.data.due_at,
|
|
assignee_user_id: 1,
|
|
version_no: 1,
|
|
status: 'open',
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
next.click('Comparar preparação guardada');
|
|
next.click('Aplicar escolhas e retomar');
|
|
assert.equal(next.sent.length, 0);
|
|
assert.equal(next.field(form, 'Tarefa').props.value, 'SYNTHETIC later');
|
|
assert.equal(next.field(form, 'Prazo (hora local)').props.value, '');
|
|
assert.match(text(next.tree), /Tarefa registada/);
|
|
});
|
|
test('changes made during the business request are checkpointed with its immutable command', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
await h.respond(savedContext(h));
|
|
const request = h.sent[0];
|
|
h.change(form, 'Tarefa', 'SYNTHETIC later');
|
|
await h.tick();
|
|
const latest = h.draftRequests.at(-1).data;
|
|
assert.equal(latest.snapshot.values.title, 'SYNTHETIC later');
|
|
assert.deepEqual(latest.snapshot.pending.values, request.data);
|
|
});
|
|
test('comparison preserves local edits and requires a new choice after further changes', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
await h.tick();
|
|
const context = savedContext(h);
|
|
const next = harness(pageWith(context));
|
|
next.change(form, 'Tarefa', 'SYNTHETIC local');
|
|
next.click('Comparar preparação guardada');
|
|
next.change(form, 'Valor a retomar: Tarefa', 'saved');
|
|
next.change(form, 'Tarefa', 'SYNTHETIC after choice');
|
|
assert.equal(
|
|
next.button('Aplicar escolhas e retomar').props.disabled,
|
|
true,
|
|
);
|
|
next.change(form, 'Valor a retomar: Tarefa', 'page');
|
|
next.click('Aplicar escolhas e retomar');
|
|
assert.equal(
|
|
next.field(form, 'Tarefa').props.value,
|
|
'SYNTHETIC after choice',
|
|
);
|
|
});
|
|
test('a response for a different account cannot authorise a business send or clear the draft', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
await h.respond({ ...savedContext(h), actor_id: 2 });
|
|
assert.equal(h.sent.length, 0);
|
|
assert.equal(
|
|
h.field(form, 'Tarefa').props.value,
|
|
'SYNTHETIC prepared task',
|
|
);
|
|
assert.match(text(h.tree), /Não foi possível confirmar a preparação/);
|
|
});
|
|
test('a callback from the previous account cannot send a task after navigation', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
const call = h.draftRequests[0],
|
|
context = savedContext(h, call);
|
|
h.render(props({ taskCreation: { ...props().taskCreation, actor_id: 2 } }));
|
|
h.change(form, 'Tarefa', 'SYNTHETIC next account');
|
|
await h.respond(context, call);
|
|
assert.equal(h.sent.length, 0);
|
|
assert.equal(h.field(form, 'Tarefa').props.value, 'SYNTHETIC next account');
|
|
});
|
|
test('an unavailable installation retains the existing manual creation path', () => {
|
|
const initial = props();
|
|
initial.taskCreation.preparation.installed = false;
|
|
const h = harness(initial);
|
|
fill(h);
|
|
const request = h.submit(form);
|
|
assert.equal(h.draftRequests.length, 0);
|
|
assert.equal(request.data.title, 'SYNTHETIC prepared task');
|
|
assert.match(text(h.tree), /criação manual continua disponível/);
|
|
});
|
|
test('another saved preparation cannot replace a locally sent unknown command', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
h.submit(form);
|
|
await h.respond(savedContext(h));
|
|
const request = h.sent[0];
|
|
h.fail(request);
|
|
const newer = savedContext(h);
|
|
newer.preparation.version = 3;
|
|
newer.preparation.operation_key = '00000000-0000-4000-8000-999999999999';
|
|
newer.preparation.snapshot = structuredClone(newer.preparation.snapshot);
|
|
newer.preparation.snapshot.pending.values.client_key =
|
|
'00000000-0000-4000-8000-888888888888';
|
|
h.render({ ...h.props, taskCreation: newer });
|
|
h.click('Comparar preparação guardada');
|
|
h.click('Aplicar escolhas e retomar');
|
|
assert.match(text(h.tree), /outro envio sem confirmação/);
|
|
const retry = h.submit(form);
|
|
assert.deepEqual(retry.data, request.data);
|
|
});
|
|
|
|
test('a confirmed durable preparation does not repeat the leave warning but new edits do', async () => {
|
|
const h = harness();
|
|
fill(h);
|
|
assert.equal(h.leave(), true);
|
|
await h.tick();
|
|
await h.respond(savedContext(h));
|
|
assert.equal(h.leave(), false);
|
|
assert.equal(h.unload(), false);
|
|
h.change(form, 'Tarefa', 'SYNTHETIC unsaved change');
|
|
assert.equal(h.leave(), true);
|
|
assert.equal(h.unload(), true);
|
|
});
|
|
test('validation allows a corrected preparation with a fresh save key without discarding its fields', async () => {
|
|
const h = harness();
|
|
fill(h, 'S'.repeat(201));
|
|
await h.tick();
|
|
const first = h.draftRequests[0];
|
|
await h.validationDraft(
|
|
{ 'snapshot.values.title': 'SYNTHETIC validation' },
|
|
first,
|
|
);
|
|
h.change(form, 'Tarefa', 'SYNTHETIC corrected');
|
|
h.click('Voltar a guardar preparação');
|
|
const corrected = h.draftRequests.at(-1);
|
|
assert.notEqual(corrected.data.operation_key, first.data.operation_key);
|
|
assert.equal(corrected.data.snapshot.values.title, 'SYNTHETIC corrected');
|
|
await h.respond(savedContext(h, corrected), corrected);
|
|
assert.equal(h.field(form, 'Tarefa').props.value, 'SYNTHETIC corrected');
|
|
});
|
|
test('unreadable saved data is explained and the explicit manual path preserves the server preparation', () => {
|
|
const initial = props();
|
|
initial.taskCreation.preparation = {
|
|
...initial.taskCreation.preparation,
|
|
version: 1,
|
|
unavailable: true,
|
|
};
|
|
const h = harness(initial);
|
|
assert.match(text(h.tree), /não pôde ser lida/);
|
|
h.click('Continuar só nesta página');
|
|
fill(h);
|
|
h.submit(form);
|
|
assert.equal(h.sent.length, 1);
|
|
assert.equal(h.draftRequests.length, 0);
|
|
assert.match(text(h.tree), /guardada no servidor foi preservada/);
|
|
});
|
|
|
|
test('cross-time-zone recovery keeps the saved instant including repeated local time and seconds', async () => {
|
|
const previous = process.env.TZ;
|
|
process.env.TZ = 'America/New_York';
|
|
try {
|
|
const initial = props(),
|
|
instant = '2027-11-07T06:30:12.345Z';
|
|
initial.taskCreation.preparation = {
|
|
installed: true,
|
|
version: 1,
|
|
operation_key: '00000000-0000-4000-8000-111111111111',
|
|
unavailable: false,
|
|
updated_at: '2026-11-03T12:00:00Z',
|
|
snapshot: {
|
|
values: {
|
|
title: 'SYNTHETIC saved instant',
|
|
due_at: instant,
|
|
assignee_user_id: '1',
|
|
},
|
|
generation: { title: 1, due_at: 1, assignee_user_id: 0 },
|
|
pending: null,
|
|
},
|
|
};
|
|
const h = harness(initial);
|
|
h.click('Comparar preparação guardada');
|
|
h.click('Aplicar escolhas e retomar');
|
|
h.submit(form);
|
|
assert.equal(
|
|
h.draftRequests[0].data.snapshot.pending.values.due_at,
|
|
instant,
|
|
);
|
|
await h.respond(savedContext(h));
|
|
assert.equal(h.sent[0].data.due_at, instant);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.TZ;
|
|
else process.env.TZ = previous;
|
|
}
|
|
});
|
|
|
|
test('an empty completed preparation matching the page does not ask the user to compare it again', () => {
|
|
const initial = props();
|
|
initial.taskCreation.preparation = {
|
|
installed: true,
|
|
version: 2,
|
|
operation_key: '00000000-0000-4000-8000-111111111111',
|
|
unavailable: false,
|
|
updated_at: '2026-11-03T12:00:00Z',
|
|
snapshot: {
|
|
values: { title: '', due_at: '', assignee_user_id: '1' },
|
|
generation: { title: 2, due_at: 1, assignee_user_id: 0 },
|
|
pending: null,
|
|
},
|
|
};
|
|
const h = harness(initial);
|
|
assert.equal(h.button('Comparar preparação guardada'), undefined);
|
|
assert.equal(h.button('Criar tarefa').props.disabled, false);
|
|
assert.equal(h.leave(), false);
|
|
assert.equal(h.draftRequests.length, 0);
|
|
});
|