134 lines
7.5 KiB
JavaScript
134 lines
7.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import { compileFunction } from 'node:vm';
|
|
import { createRequire } from 'node:module';
|
|
import ts from 'typescript';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
function nodes(tree) {
|
|
if (Array.isArray(tree)) return tree.flatMap(nodes);
|
|
if (!tree || typeof tree !== 'object') return [];
|
|
return [tree, ...nodes(tree.props?.children)];
|
|
}
|
|
function text(tree) {
|
|
if (typeof tree === 'string') return tree;
|
|
if (Array.isArray(tree)) return tree.map(text).join('');
|
|
return tree?.props ? text(tree.props.children) : '';
|
|
}
|
|
function harness() {
|
|
const memory = [], effects = [], calls = [], reloads = [];
|
|
let cursor = 0, pending = [], dirty = false, serial = 0, handler;
|
|
let props = { record: { id: 1, title: 'QA first', featured: false }, etag: 'a'.repeat(64), history: { data: [], current_page: 1 }, basePath: '/management/opportunities' };
|
|
const code = ts.transpileModule(fs.readFileSync(new URL('../../resources/js/pages/management/opportunity-curation.tsx', import.meta.url), 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, target: ts.ScriptTarget.ES2022 },
|
|
}).outputText;
|
|
const module = { exports: {} };
|
|
const mocks = {
|
|
react: {
|
|
useState(initial) {
|
|
const i = cursor++;
|
|
if (!(i in memory)) memory[i] = typeof initial === 'function' ? initial() : initial;
|
|
return [memory[i], (value) => {
|
|
const next = typeof value === 'function' ? value(memory[i]) : value;
|
|
if (!Object.is(next, memory[i])) { memory[i] = next; dirty = true; }
|
|
}];
|
|
},
|
|
useRef(initial) { const i = cursor++; memory[i] ??= { current: initial }; return memory[i]; },
|
|
useEffect(effect, dependencies) {
|
|
const i = cursor++;
|
|
if (!effects[i] || dependencies.some((value, index) => value !== effects[i][index])) {
|
|
effects[i] = dependencies; pending.push(effect);
|
|
}
|
|
},
|
|
},
|
|
'@inertiajs/react': { Link: 'a', router: { reload: (options) => reloads.push(options) } },
|
|
'@/lib/operation-key': { createOperationKey: () => `aaaaaaaa-aaaa-4aaa-8aaa-${String(++serial).padStart(12, '0')}` },
|
|
'@/lib/profile-json': {
|
|
ProfileValidationError: class extends Error {},
|
|
profileJson: (url, data) => { calls.push({ url, data: structuredClone(data) }); return handler(url, data); },
|
|
},
|
|
'./shared': { Management: 'main', button: '', control: '' },
|
|
};
|
|
compileFunction(code, ['require', 'module', 'exports'])((name) => mocks[name] ?? require(name), module, module.exports);
|
|
let tree;
|
|
function render() {
|
|
do { dirty = false; cursor = 0; pending = []; tree = module.exports.default(props); for (const effect of pending) effect(); } while (dirty);
|
|
return tree;
|
|
}
|
|
return {
|
|
calls, reloads, render,
|
|
setHandler(fn) { handler = fn; },
|
|
setProps(next) { props = next; return render(); },
|
|
button(label) { return nodes(render()).find(node => node.type === 'button' && text(node) === label); },
|
|
input(type) { return nodes(render()).find(node => node.type === type); },
|
|
get props() { return props; },
|
|
};
|
|
}
|
|
|
|
test('editing rationale invalidates both the preview and its confirmation without submitting', async () => {
|
|
const h = harness();
|
|
h.setHandler(async () => ({ hash: 'b'.repeat(64), title: 'QA first', before: false, featured: true, reason: 'Rationale for QA editorial selection' }));
|
|
h.input('textarea').props.onChange({ target: { value: 'Rationale for QA editorial selection' } });
|
|
await h.button('Conferir prévia').props.onClick();
|
|
assert.equal(h.button('Confirmar decisão de destaque').props.disabled, true);
|
|
h.input('input').props.onChange({ target: { checked: true } });
|
|
assert.equal(h.button('Confirmar decisão de destaque').props.disabled, false);
|
|
h.input('textarea').props.onChange({ target: { value: 'Changed editorial rationale must be reviewed again' } });
|
|
assert.equal(h.button('Confirmar decisão de destaque'), undefined);
|
|
assert.equal(h.calls.length, 1);
|
|
assert.equal(h.calls[0].data.featured, true);
|
|
});
|
|
|
|
test('in-flight preview is single; uncertain confirmation keeps the same intent for recovery', async () => {
|
|
const h = harness();
|
|
let release;
|
|
h.setHandler(() => new Promise(resolve => { release = resolve; }));
|
|
h.input('textarea').props.onChange({ target: { value: 'Rationale for QA editorial selection' } });
|
|
const first = h.button('Conferir prévia').props.onClick();
|
|
await h.button('Conferir prévia').props.onClick();
|
|
assert.equal(h.calls.length, 1);
|
|
assert.equal(h.input('fieldset').props.disabled, true);
|
|
release({ hash: 'b'.repeat(64), title: 'QA first', before: false, featured: true, reason: 'Rationale for QA editorial selection' });
|
|
await first;
|
|
h.input('input').props.onChange({ target: { checked: true } });
|
|
h.setHandler(async () => { throw new Error('Response lost'); });
|
|
await h.button('Confirmar decisão de destaque').props.onClick();
|
|
const uncertain = h.calls.at(-1).data;
|
|
h.setHandler(async () => ({ replayed: true, featured: true, etag: 'c'.repeat(64) }));
|
|
await h.button('Confirmar decisão de destaque').props.onClick();
|
|
assert.deepEqual(h.calls.at(-1).data, uncertain);
|
|
assert.equal(h.reloads.length, 1);
|
|
assert.deepEqual(h.reloads[0].only, ['record', 'etag', 'history']);
|
|
assert.match(text(h.render()), /Decisão já registada/);
|
|
});
|
|
|
|
test('a delayed preview cannot be adopted after another record and version arrive', async () => {
|
|
const h = harness();
|
|
let release;
|
|
h.setHandler(() => new Promise(resolve => { release = resolve; }));
|
|
h.input('textarea').props.onChange({ target: { value: 'Rationale for QA editorial selection' } });
|
|
const request = h.button('Conferir prévia').props.onClick();
|
|
h.setProps({ ...h.props, record: { id: 2, title: 'QA second', featured: false }, etag: 'd'.repeat(64) });
|
|
release({ hash: 'b'.repeat(64), title: 'QA first', before: false, featured: true, reason: 'Old rationale' });
|
|
await request;
|
|
assert.equal(h.button('Confirmar decisão de destaque'), undefined);
|
|
assert.match(text(h.render()), /QA second/);
|
|
assert.doesNotMatch(text(h.render()), /Old rationale/);
|
|
assert.equal(h.input('textarea').props.value, '');
|
|
});
|
|
|
|
test('changing only the history page retains the unsubmitted rationale and reviewed decision', async () => {
|
|
const h = harness();
|
|
h.setHandler(async () => ({ hash: 'b'.repeat(64), title: 'QA first', before: false, featured: true, reason: 'Reviewed QA rationale retained during paging' }));
|
|
h.input('textarea').props.onChange({ target: { value: 'Reviewed QA rationale retained during paging' } });
|
|
await h.button('Conferir prévia').props.onClick();
|
|
h.input('input').props.onChange({ target: { checked: true } });
|
|
h.setProps({ ...h.props, history: { data: [], current_page: 2, prev_page_url: '/management/opportunities/1/destaque?page=1' } });
|
|
assert.equal(h.input('textarea').props.value, 'Reviewed QA rationale retained during paging');
|
|
assert.equal(h.button('Confirmar decisão de destaque').props.disabled, false);
|
|
const link = nodes(h.render()).find(node => node.type === 'a' && text(node) === 'Anterior');
|
|
assert.equal(link.props.preserveState, true);
|
|
assert.equal(link.props.preserveScroll, true);
|
|
});
|