tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ExternalComponentWrapper.test.jsx (9816B)


      1 import { GlobalOverrider } from "test/unit/utils";
      2 import { mount } from "enzyme";
      3 import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
      4 import { combineReducers, createStore } from "redux";
      5 import { Provider } from "react-redux";
      6 import { ExternalComponentWrapper } from "content-src/components/ExternalComponentWrapper/ExternalComponentWrapper";
      7 import React from "react";
      8 
      9 const DEFAULT_PROPS = {
     10  type: "SEARCH",
     11  className: "test-wrapper",
     12 };
     13 
     14 const flushPromises = () => new Promise(resolve => queueMicrotask(resolve));
     15 
     16 const createMockConfig = (overrides = {}) => ({
     17  type: "SEARCH",
     18  componentURL: "chrome://test/content/component.mjs",
     19  tagName: "test-component",
     20  l10nURLs: [],
     21  ...overrides,
     22 });
     23 
     24 const createStateWithConfig = config => ({
     25  ...INITIAL_STATE,
     26  ExternalComponents: {
     27    components: [config],
     28  },
     29 });
     30 
     31 const createMockElement = sandbox => {
     32  const element = document.createElement("div");
     33  sandbox.spy(element, "setAttribute");
     34  sandbox.spy(element.style, "setProperty");
     35  return element;
     36 };
     37 
     38 // Wrap this around any component that uses useSelector,
     39 // or any mount that uses a child that uses redux.
     40 function WrapWithProvider({ children, state = INITIAL_STATE }) {
     41  let store = createStore(combineReducers(reducers), state);
     42  return <Provider store={store}>{children}</Provider>;
     43 }
     44 
     45 describe("<ExternalComponentWrapper>", () => {
     46  let globals;
     47  let sandbox;
     48  const TestWrapper = ExternalComponentWrapper;
     49 
     50  beforeEach(() => {
     51    globals = new GlobalOverrider();
     52    sandbox = globals.sandbox;
     53  });
     54 
     55  afterEach(() => {
     56    globals.restore();
     57  });
     58 
     59  const stubCreateElement = handlers => {
     60    const originalCreateElement = document.createElement.bind(document);
     61    return sandbox.stub(document, "createElement").callsFake(tagName => {
     62      if (handlers[tagName]) {
     63        return handlers[tagName]();
     64      }
     65      return originalCreateElement(tagName);
     66    });
     67  };
     68 
     69  it("should render a container div", () => {
     70    const wrapper = mount(
     71      <WrapWithProvider>
     72        <TestWrapper {...DEFAULT_PROPS} />
     73      </WrapWithProvider>
     74    );
     75    assert.ok(wrapper.exists());
     76    assert.equal(wrapper.find("div").length, 1);
     77  });
     78 
     79  it("should apply className to container div", () => {
     80    const wrapper = mount(
     81      <WrapWithProvider>
     82        <TestWrapper {...DEFAULT_PROPS} />
     83      </WrapWithProvider>
     84    );
     85    assert.equal(wrapper.find("div.test-wrapper").length, 1);
     86  });
     87 
     88  it("should warn when no configuration is found for type", async () => {
     89    const consoleWarnStub = sandbox.stub(console, "warn");
     90    mount(
     91      <WrapWithProvider>
     92        <TestWrapper {...DEFAULT_PROPS} />
     93      </WrapWithProvider>
     94    );
     95 
     96    await flushPromises();
     97 
     98    assert.calledWith(
     99      consoleWarnStub,
    100      "No external component configuration found for type: SEARCH"
    101    );
    102  });
    103 
    104  it("should not render custom element without configuration", async () => {
    105    const importModuleStub = sandbox.stub().resolves();
    106    const wrapper = mount(
    107      <WrapWithProvider>
    108        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    109      </WrapWithProvider>
    110    );
    111 
    112    await flushPromises();
    113 
    114    assert.notCalled(importModuleStub);
    115    wrapper.unmount();
    116  });
    117 
    118  it("should load component module when configuration is available", async () => {
    119    const mockConfig = createMockConfig();
    120    const stateWithConfig = createStateWithConfig(mockConfig);
    121    const importModuleStub = sandbox.stub().resolves();
    122 
    123    const wrapper = mount(
    124      <WrapWithProvider state={stateWithConfig}>
    125        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    126      </WrapWithProvider>
    127    );
    128    await flushPromises();
    129 
    130    assert.calledWith(importModuleStub, mockConfig.componentURL);
    131    wrapper.unmount();
    132  });
    133 
    134  it("should create custom element with correct tag name", async () => {
    135    const mockConfig = createMockConfig();
    136    const stateWithConfig = createStateWithConfig(mockConfig);
    137    const mockElement = createMockElement(sandbox);
    138    const importModuleStub = sandbox.stub().resolves();
    139 
    140    const createElementStub = stubCreateElement({
    141      "test-component": () => mockElement,
    142    });
    143 
    144    const wrapper = mount(
    145      <WrapWithProvider state={stateWithConfig}>
    146        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    147      </WrapWithProvider>
    148    );
    149    await flushPromises();
    150 
    151    assert.calledWith(createElementStub, "test-component");
    152    wrapper.unmount();
    153  });
    154 
    155  it("should add l10n link elements to document head", async () => {
    156    const mockConfig = createMockConfig({
    157      l10nURLs: ["browser/test.ftl", "browser/test2.ftl"],
    158    });
    159    const stateWithConfig = createStateWithConfig(mockConfig);
    160    const mockLinkElement = { rel: "", href: "", remove: sandbox.spy() };
    161    const importModuleStub = sandbox.stub().resolves();
    162 
    163    stubCreateElement({
    164      link: () => mockLinkElement,
    165      "test-component": () => createMockElement(sandbox),
    166    });
    167 
    168    const appendChildStub = sandbox.stub(document.head, "appendChild");
    169 
    170    const wrapper = mount(
    171      <WrapWithProvider state={stateWithConfig}>
    172        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    173      </WrapWithProvider>
    174    );
    175    await flushPromises();
    176 
    177    assert.equal(appendChildStub.callCount, 2, "Should append two l10n links");
    178    assert.equal(mockLinkElement.rel, "localization");
    179    wrapper.unmount();
    180  });
    181 
    182  it("should set attributes on custom element", async () => {
    183    const mockConfig = createMockConfig({
    184      attributes: {
    185        "data-test": "value",
    186        role: "search",
    187      },
    188    });
    189    const stateWithConfig = createStateWithConfig(mockConfig);
    190    const mockElement = createMockElement(sandbox);
    191    const importModuleStub = sandbox.stub().resolves();
    192 
    193    stubCreateElement({
    194      "test-component": () => mockElement,
    195    });
    196 
    197    const wrapper = mount(
    198      <WrapWithProvider state={stateWithConfig}>
    199        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    200      </WrapWithProvider>
    201    );
    202    await flushPromises();
    203 
    204    assert.calledWith(mockElement.setAttribute, "data-test", "value");
    205    assert.calledWith(mockElement.setAttribute, "role", "search");
    206    wrapper.unmount();
    207  });
    208 
    209  it("should set CSS variables on custom element", async () => {
    210    const mockConfig = createMockConfig({
    211      cssVariables: {
    212        "--test-color": "blue",
    213        "--test-size": "10px",
    214      },
    215    });
    216    const stateWithConfig = createStateWithConfig(mockConfig);
    217    const mockElement = createMockElement(sandbox);
    218    const importModuleStub = sandbox.stub().resolves();
    219 
    220    stubCreateElement({
    221      "test-component": () => mockElement,
    222    });
    223 
    224    const wrapper = mount(
    225      <WrapWithProvider state={stateWithConfig}>
    226        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    227      </WrapWithProvider>
    228    );
    229    await flushPromises();
    230 
    231    assert.calledWith(mockElement.style.setProperty, "--test-color", "blue");
    232    assert.calledWith(mockElement.style.setProperty, "--test-size", "10px");
    233    wrapper.unmount();
    234  });
    235 
    236  it("should handle component load errors gracefully", async () => {
    237    const mockConfig = createMockConfig();
    238    const stateWithConfig = createStateWithConfig(mockConfig);
    239    const consoleErrorStub = sandbox.stub(console, "error");
    240    const importModuleStub = sandbox
    241      .stub()
    242      .rejects(new Error("Module load failed"));
    243 
    244    const wrapper = mount(
    245      <WrapWithProvider state={stateWithConfig}>
    246        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    247      </WrapWithProvider>
    248    );
    249    await flushPromises();
    250    wrapper.update();
    251 
    252    assert.calledWith(
    253      consoleErrorStub,
    254      "Failed to load external component for type SEARCH:",
    255      sinon.match.instanceOf(Error)
    256    );
    257 
    258    assert.equal(wrapper.html(), "", "Should render null on error");
    259    wrapper.unmount();
    260  });
    261 
    262  it("should clean up l10n links on unmount", async () => {
    263    const mockConfig = createMockConfig({
    264      l10nURLs: ["browser/test.ftl"],
    265    });
    266    const stateWithConfig = createStateWithConfig(mockConfig);
    267    const mockLinkElements = [];
    268    const importModuleStub = sandbox.stub().resolves();
    269 
    270    stubCreateElement({
    271      "test-component": () => createMockElement(sandbox),
    272      link: () => {
    273        const linkEl = { remove: sandbox.spy() };
    274        mockLinkElements.push(linkEl);
    275        return linkEl;
    276      },
    277    });
    278 
    279    sandbox.stub(document.head, "appendChild");
    280 
    281    const wrapper = mount(
    282      <WrapWithProvider state={stateWithConfig}>
    283        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    284      </WrapWithProvider>
    285    );
    286    await flushPromises();
    287 
    288    assert.equal(mockLinkElements.length, 1, "Should create one l10n link");
    289 
    290    wrapper.unmount();
    291 
    292    assert.called(mockLinkElements[0].remove);
    293  });
    294 
    295  it("should not create duplicate elements on multiple renders", async () => {
    296    const mockConfig = createMockConfig();
    297    const stateWithConfig = createStateWithConfig(mockConfig);
    298    const mockElement = createMockElement(sandbox);
    299    const importModuleStub = sandbox.stub().resolves();
    300 
    301    const createElementStub = stubCreateElement({
    302      "test-component": () => mockElement,
    303    });
    304 
    305    const wrapper = mount(
    306      <WrapWithProvider state={stateWithConfig}>
    307        <TestWrapper {...DEFAULT_PROPS} importModule={importModuleStub} />
    308      </WrapWithProvider>
    309    );
    310    await flushPromises();
    311 
    312    const initialCallCount = createElementStub.callCount;
    313 
    314    wrapper.setProps({ className: "new-class" });
    315    await flushPromises();
    316 
    317    assert.equal(
    318      createElementStub.callCount,
    319      initialCallCount,
    320      "Should not create element again on re-render with same type"
    321    );
    322    wrapper.unmount();
    323  });
    324 });