import { GlobalOverrider } from "test/unit/utils"; import { mount } from "enzyme"; import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; import { combineReducers, createStore } from "redux"; import { Provider } from "react-redux"; import { ExternalComponentWrapper } from "content-src/components/ExternalComponentWrapper/ExternalComponentWrapper"; import React from "react"; const DEFAULT_PROPS = { type: "SEARCH", className: "test-wrapper", }; const flushPromises = () => new Promise(resolve => queueMicrotask(resolve)); const createMockConfig = (overrides = {}) => ({ type: "SEARCH", componentURL: "chrome://test/content/component.mjs", tagName: "test-component", l10nURLs: [], ...overrides, }); const createStateWithConfig = config => ({ ...INITIAL_STATE, ExternalComponents: { components: [config], }, }); const createMockElement = sandbox => { const element = document.createElement("div"); sandbox.spy(element, "setAttribute"); sandbox.spy(element.style, "setProperty"); return element; }; // Wrap this around any component that uses useSelector, // or any mount that uses a child that uses redux. function WrapWithProvider({ children, state = INITIAL_STATE }) { let store = createStore(combineReducers(reducers), state); return {children}; } describe("", () => { let globals; let sandbox; const TestWrapper = ExternalComponentWrapper; beforeEach(() => { globals = new GlobalOverrider(); sandbox = globals.sandbox; }); afterEach(() => { globals.restore(); }); const stubCreateElement = handlers => { const originalCreateElement = document.createElement.bind(document); return sandbox.stub(document, "createElement").callsFake(tagName => { if (handlers[tagName]) { return handlers[tagName](); } return originalCreateElement(tagName); }); }; it("should render a container div", () => { const wrapper = mount( ); assert.ok(wrapper.exists()); assert.equal(wrapper.find("div").length, 1); }); it("should apply className to container div", () => { const wrapper = mount( ); assert.equal(wrapper.find("div.test-wrapper").length, 1); }); it("should warn when no configuration is found for type", async () => { const consoleWarnStub = sandbox.stub(console, "warn"); mount( ); await flushPromises(); assert.calledWith( consoleWarnStub, "No external component configuration found for type: SEARCH" ); }); it("should not render custom element without configuration", async () => { const importModuleStub = sandbox.stub().resolves(); const wrapper = mount( ); await flushPromises(); assert.notCalled(importModuleStub); wrapper.unmount(); }); it("should load component module when configuration is available", async () => { const mockConfig = createMockConfig(); const stateWithConfig = createStateWithConfig(mockConfig); const importModuleStub = sandbox.stub().resolves(); const wrapper = mount( ); await flushPromises(); assert.calledWith(importModuleStub, mockConfig.componentURL); wrapper.unmount(); }); it("should create custom element with correct tag name", async () => { const mockConfig = createMockConfig(); const stateWithConfig = createStateWithConfig(mockConfig); const mockElement = createMockElement(sandbox); const importModuleStub = sandbox.stub().resolves(); const createElementStub = stubCreateElement({ "test-component": () => mockElement, }); const wrapper = mount( ); await flushPromises(); assert.calledWith(createElementStub, "test-component"); wrapper.unmount(); }); it("should add l10n link elements to document head", async () => { const mockConfig = createMockConfig({ l10nURLs: ["browser/test.ftl", "browser/test2.ftl"], }); const stateWithConfig = createStateWithConfig(mockConfig); const mockLinkElement = { rel: "", href: "", remove: sandbox.spy() }; const importModuleStub = sandbox.stub().resolves(); stubCreateElement({ link: () => mockLinkElement, "test-component": () => createMockElement(sandbox), }); const appendChildStub = sandbox.stub(document.head, "appendChild"); const wrapper = mount( ); await flushPromises(); assert.equal(appendChildStub.callCount, 2, "Should append two l10n links"); assert.equal(mockLinkElement.rel, "localization"); wrapper.unmount(); }); it("should set attributes on custom element", async () => { const mockConfig = createMockConfig({ attributes: { "data-test": "value", role: "search", }, }); const stateWithConfig = createStateWithConfig(mockConfig); const mockElement = createMockElement(sandbox); const importModuleStub = sandbox.stub().resolves(); stubCreateElement({ "test-component": () => mockElement, }); const wrapper = mount( ); await flushPromises(); assert.calledWith(mockElement.setAttribute, "data-test", "value"); assert.calledWith(mockElement.setAttribute, "role", "search"); wrapper.unmount(); }); it("should set CSS variables on custom element", async () => { const mockConfig = createMockConfig({ cssVariables: { "--test-color": "blue", "--test-size": "10px", }, }); const stateWithConfig = createStateWithConfig(mockConfig); const mockElement = createMockElement(sandbox); const importModuleStub = sandbox.stub().resolves(); stubCreateElement({ "test-component": () => mockElement, }); const wrapper = mount( ); await flushPromises(); assert.calledWith(mockElement.style.setProperty, "--test-color", "blue"); assert.calledWith(mockElement.style.setProperty, "--test-size", "10px"); wrapper.unmount(); }); it("should handle component load errors gracefully", async () => { const mockConfig = createMockConfig(); const stateWithConfig = createStateWithConfig(mockConfig); const consoleErrorStub = sandbox.stub(console, "error"); const importModuleStub = sandbox .stub() .rejects(new Error("Module load failed")); const wrapper = mount( ); await flushPromises(); wrapper.update(); assert.calledWith( consoleErrorStub, "Failed to load external component for type SEARCH:", sinon.match.instanceOf(Error) ); assert.equal(wrapper.html(), "", "Should render null on error"); wrapper.unmount(); }); it("should clean up l10n links on unmount", async () => { const mockConfig = createMockConfig({ l10nURLs: ["browser/test.ftl"], }); const stateWithConfig = createStateWithConfig(mockConfig); const mockLinkElements = []; const importModuleStub = sandbox.stub().resolves(); stubCreateElement({ "test-component": () => createMockElement(sandbox), link: () => { const linkEl = { remove: sandbox.spy() }; mockLinkElements.push(linkEl); return linkEl; }, }); sandbox.stub(document.head, "appendChild"); const wrapper = mount( ); await flushPromises(); assert.equal(mockLinkElements.length, 1, "Should create one l10n link"); wrapper.unmount(); assert.called(mockLinkElements[0].remove); }); it("should not create duplicate elements on multiple renders", async () => { const mockConfig = createMockConfig(); const stateWithConfig = createStateWithConfig(mockConfig); const mockElement = createMockElement(sandbox); const importModuleStub = sandbox.stub().resolves(); const createElementStub = stubCreateElement({ "test-component": () => mockElement, }); const wrapper = mount( ); await flushPromises(); const initialCallCount = createElementStub.callCount; wrapper.setProps({ className: "new-class" }); await flushPromises(); assert.equal( createElementStub.callCount, initialCallCount, "Should not create element again on re-render with same type" ); wrapper.unmount(); }); });