mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
models/lib/cardBoardConsistency.js and models/lib/restArrayParam.js are CommonJS (module.exports), but being under models/ they were eagerly bundled into the client by rspack, which classifies a no-import/require .js as an ES module and threw at load time: Uncaught Error: ES Modules may not assign module.exports or exports.* ... ./models/lib/cardBoardConsistency.js -> ./models/cards.js This aborted the client bundle on every page. (filenameSanitizer.js uses the same module.exports pattern without issue only because it is never imported into the client bundle.) Both helpers are server-only logic (a REST handler and a Cards.before.update hook), so they are moved to server/lib/ (excluded from the client rspack bundle, CommonJS is fine on the Node server) and enforceCardBoardConsistency is relocated from the isomorphic models/cards.js into server/models/cards.js. The isomorphic models/cards.js no longer imports either helper. Verified in a real headless Chromium against a running dev server: the page now loads with window.Meteor present and zero console/page errors (previously one PAGEERROR from cardBoardConsistency.js). Node unit tests updated to the new paths and pass (npm run test:unit:node, 11 passing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.5 KiB
JavaScript
89 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
// Plain-Node unit test (no Meteor) for the REST members/assignees coercion.
|
|
// Run: node tests/restArrayParam.test.cjs
|
|
//
|
|
// Regression guard for #3697 ("Can't edit members with UI after removing members
|
|
// with RestAPI"): the REST handler used to (a) skip clear payloads (`null` / `""`)
|
|
// behind a truthiness guard and (b) write `null` instead of `[]` for an empty
|
|
// value — and a card with `members: null` breaks UI code that treats it as an
|
|
// array. coerceRestArrayParam normalizes any payload into a clean String[].
|
|
|
|
const assert = require('assert');
|
|
const { coerceRestArrayParam } = require('../server/lib/restArrayParam');
|
|
|
|
let passed = 0;
|
|
function test(name, fn) {
|
|
fn();
|
|
passed += 1;
|
|
console.log(' ok -', name);
|
|
}
|
|
|
|
// A single id string becomes a one-element array.
|
|
test('wraps a single id string into an array', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam('userA'), ['userA']);
|
|
});
|
|
|
|
// The natural "clear" payloads all become an empty array — never null.
|
|
test('empty string clears to []', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam(''), []);
|
|
});
|
|
test('null clears to []', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam(null), []);
|
|
});
|
|
test('undefined clears to []', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam(undefined), []);
|
|
});
|
|
|
|
// Arrays are kept, but filtered to non-empty strings so a stray non-string id
|
|
// cannot reach the document (the schema is [String]).
|
|
test('keeps a valid id array', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam(['a', 'b']), ['a', 'b']);
|
|
});
|
|
test('keeps an explicit empty array', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam([]), []);
|
|
});
|
|
test('filters non-string / empty entries out of an array', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam(['a', null, 123, '', 'b']), ['a', 'b']);
|
|
});
|
|
|
|
// Defensive: any other type is treated as a clear.
|
|
test('a number clears to []', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam(7), []);
|
|
});
|
|
test('an object clears to []', () => {
|
|
assert.deepStrictEqual(coerceRestArrayParam({ userId: 'x' }), []);
|
|
});
|
|
|
|
// The result is ALWAYS an array (never null/undefined) for every input — this is
|
|
// the invariant the UI relies on.
|
|
test('result is always an array for any input', () => {
|
|
for (const input of ['', null, undefined, 'id', [], ['a'], 0, {}, NaN]) {
|
|
assert.ok(Array.isArray(coerceRestArrayParam(input)),
|
|
`expected array for input ${JSON.stringify(input)}`);
|
|
}
|
|
});
|
|
|
|
// --- Negative test: prove this is what fixes #3697 -------------------------
|
|
// Reproduce the OLD handler's logic and show it produced the corrupt state
|
|
// (null) for a clear, whereas the new helper never does.
|
|
test('NEGATIVE: the old logic wrote null for an empty value; the new helper does not', () => {
|
|
const oldCoerce = value => {
|
|
// Old behavior: `if (req.body.members)` guard + string branch writing null.
|
|
if (!value) return undefined; // clear payloads skipped entirely
|
|
if (typeof value === 'string') {
|
|
return value === '' ? null : [value]; // empty string would store null
|
|
}
|
|
return value;
|
|
};
|
|
// A clear via "" under the old logic was a no-op (skipped) — you could not
|
|
// remove the last member; and the dead branch's intent was to store null.
|
|
assert.strictEqual(oldCoerce(''), undefined);
|
|
assert.strictEqual(oldCoerce(null), undefined);
|
|
// New helper turns both into a clean, editable empty array.
|
|
assert.deepStrictEqual(coerceRestArrayParam(''), []);
|
|
assert.deepStrictEqual(coerceRestArrayParam(null), []);
|
|
});
|
|
|
|
console.log(`\n${passed} passing`);
|