diff --git a/jest.config.js b/jest.config.js index b0db96ebd..e4bb14d9b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,8 +13,9 @@ config.transformIgnorePatterns = [ // tight and fails sporadically. Give all tests a bit more room. config.testTimeout = 10000; -// add svelte-jester -config.transform["^.+\\.svelte$"] = "svelte-jester"; +// svelte-jester refuses to run in Jest's CJS mode, so use a custom client +// compile + ESM->CJS transformer (see the tool for the rationale). +config.transform["^.+\\.svelte$"] = path.resolve(__dirname, "./tools/jest-svelte-component.cjs"); // console.log(JSON.stringify(config, null, 4)); module.exports = config; diff --git a/src/pat/contentbrowser/contentbrowser-selection.test.js b/src/pat/contentbrowser/contentbrowser-selection.test.js new file mode 100644 index 000000000..ede08cf6d --- /dev/null +++ b/src/pat/contentbrowser/contentbrowser-selection.test.js @@ -0,0 +1,69 @@ +import "./contentbrowser"; +import registry from "@patternslib/patternslib/src/core/registry"; +import utils from "@patternslib/patternslib/src/core/utils"; + +const vocabularyUrl = "http://localhost/plone/@@getVocabulary"; + +const item = (uid, title) => ({ + UID: uid, + Title: title, + "@id": `http://localhost/plone/${uid}`, + path: `/plone/${uid}`, + portal_type: "Document", + getIcon: "", + is_folderish: false, + review_state: "published", + getURL: `http://localhost/plone/${uid}`, +}); + +function mockVocabulary(items) { + global.fetch = jest.fn(async () => ({ + ok: true, + json: async () => ({ results: items, total: items.length }), + })); +} + +async function mountField(value) { + const options = { vocabularyUrl }; + document.body.innerHTML = ` +
+ +
+ `; + const input = document.getElementById("field"); + const changes = []; + input.addEventListener("change", () => changes.push(input.value)); + registry.scan(document.body); + await utils.timeout(1); + return { input, changes }; +} + +describe("Content Browser selection", () => { + afterEach(() => { + document.body.innerHTML = ""; + delete global.fetch; + }); + + it("does not fire change while rendering the initial selection", async () => { + mockVocabulary([item("uid-1", "One"), item("uid-2", "Two")]); + const { input, changes } = await mountField("uid-1,uid-2"); + await utils.timeout(1); + + expect(document.querySelectorAll(".selected-item").length).toEqual(2); + expect(input.value).toEqual("uid-1,uid-2"); + expect(changes).toEqual([]); + }); + + it("fires change when an item is unselected", async () => { + mockVocabulary([item("uid-1", "One"), item("uid-2", "Two")]); + const { input, changes } = await mountField("uid-1,uid-2"); + await utils.timeout(1); + + document.querySelector(".selected-item button[aria-label=remove]").click(); + await utils.timeout(1); + + expect(input.value).toEqual("uid-2"); + expect(changes).toEqual(["uid-2"]); + }); +}); diff --git a/src/pat/contentbrowser/src/SelectedItems.svelte b/src/pat/contentbrowser/src/SelectedItems.svelte index 026c454f8..babaf42cd 100644 --- a/src/pat/contentbrowser/src/SelectedItems.svelte +++ b/src/pat/contentbrowser/src/SelectedItems.svelte @@ -101,6 +101,12 @@ function setNodeValue(selectedUids) { const node_val = selectedUids.join($config.separator); + if (selectedItemsNode.value === node_val) { + // Nothing changed — the initial selection being written back + // on mount must not look like an edit (pat-formunloadalert + // would otherwise warn on every page leave). + return; + } selectedItemsNode.value = node_val; selectedItemsNode.dispatchEvent(events.change_event()); } diff --git a/tools/jest-svelte-component.cjs b/tools/jest-svelte-component.cjs new file mode 100644 index 000000000..49373fdb3 --- /dev/null +++ b/tools/jest-svelte-component.cjs @@ -0,0 +1,38 @@ +// Jest transformer for Svelte 5 components (.svelte). +// +// svelte-jester refuses to run while Jest is in CJS mode, which is the mode the +// Patternslib base jest config uses — so component-mount tests are otherwise +// impossible here. Compile the component to client JS with the svelte compiler +// and down-convert the emitted ESM to CommonJS so Jest's default runtime can +// require it. Backported from master (tools/jest-svelte-component.cjs). +const babel = require("@babel/core"); +const svelte = require("svelte/compiler"); + +module.exports = { + process(source, filename) { + const compiled = svelte.compile(source, { + filename, + dev: true, + generate: "client", + }).js.code; + + const cjs = babel.transformSync(compiled, { + filename, + babelrc: false, + configFile: false, + sourceMaps: "inline", + plugins: ["@babel/plugin-transform-modules-commonjs"], + }); + + return { code: cjs.code }; + }, + + getCacheKey(source, filename) { + return require("crypto") + .createHash("md5") + .update(source) + .update(filename) + .update("svelte-component-v1") + .digest("hex"); + }, +};