test: regression tests for Settings CJS compat (#7543) (#7551)

#7421 fixed the ESM/CJS interop bug where plugins using
require('ep_etherpad-lite/node/utils/Settings') got an object whose
.toolbar (and every other top-level field) was undefined, crashing
ep_font_color/ep_font_size/ep_plugin_helpers with "Cannot read
properties of undefined (reading 'indexOf')" during pad.html rendering.
That fix landed without a regression test.

Pin the contract: top-level settings fields must be reachable via a
CJS require(), the toolbar must keep its {left, right, timeslider}
shape, and setters on the shim must propagate to the underlying
settings object so reloadSettings() is visible to plugins.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-04-19 12:12:37 +01:00 committed by GitHub
parent e58dfa4752
commit c4add0260d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -89,4 +89,62 @@ describe(__filename, function () {
assert.deepEqual(settings!.ep_webrtc, {"enabled": true});
})
})
// Regression test for https://github.com/ether/etherpad/issues/7543.
// Plugins (ep_font_color, ep_font_size, ep_plugin_helpers, …) consume
// Settings via CommonJS require(), which under tsx/ESM interop would place
// the default export under .default and leave top-level fields undefined.
// That broke template rendering with:
// TypeError: Cannot read properties of undefined (reading 'indexOf')
// when plugins called settings.toolbar.left / etc.
//
// The CJS compat layer in Settings.ts re-exposes every top-level field on
// module.exports via accessor properties, so require(...).<field> resolves
// even though the source uses `export default`. This test asserts that
// contract so a future refactor can't regress it silently.
describe('CJS compatibility for plugin consumers', function () {
it('exposes top-level fields directly on require() result', function () {
const cjs = require('../../../node/utils/Settings');
// The three fields most commonly read by first-party plugins.
assert.notStrictEqual(cjs.toolbar, undefined,
'settings.toolbar must be reachable via CJS require');
assert.notStrictEqual(cjs.skinName, undefined,
'settings.skinName must be reachable via CJS require');
assert.notStrictEqual(cjs.padOptions, undefined,
'settings.padOptions must be reachable via CJS require');
});
it('toolbar has the shape plugins index into (left/right/timeslider)', function () {
const cjs = require('../../../node/utils/Settings');
// ep_font_color and friends JSON.stringify(settings.toolbar) then call
// .indexOf on the result, so the object must be present and well-formed.
assert.ok(cjs.toolbar && typeof cjs.toolbar === 'object');
assert.ok(Array.isArray(cjs.toolbar.left));
assert.ok(Array.isArray(cjs.toolbar.right));
assert.ok(Array.isArray(cjs.toolbar.timeslider));
});
it('does not hide the real value under a .default wrapper', function () {
const cjs = require('../../../node/utils/Settings');
// If export-default handling regresses, consumers end up seeing a
// {default: {...}} wrapper and .toolbar on the wrapper is undefined.
// Either shape is acceptable as long as .toolbar is directly present,
// which is what the CJS compat shim guarantees.
if (cjs.default != null && cjs.default.toolbar != null) {
assert.strictEqual(cjs.toolbar, cjs.default.toolbar,
'require().toolbar must be the same object as require().default.toolbar');
}
});
it('setters propagate so reloadSettings() changes are visible to plugins', function () {
const cjs = require('../../../node/utils/Settings');
const original = cjs.title;
try {
cjs.title = 'cjs-shim-test';
assert.strictEqual(cjs.title, 'cjs-shim-test');
} finally {
cjs.title = original;
}
});
});
});