tor-browser

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

Base.test.jsx (13625B)


      1 import {
      2  _Base as Base,
      3  BaseContent,
      4  WithDsAdmin,
      5 } from "content-src/components/Base/Base";
      6 import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin";
      7 import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary";
      8 import React from "react";
      9 import { Search } from "content-src/components/Search/Search";
     10 import { shallow } from "enzyme";
     11 import { actionCreators as ac } from "common/Actions.mjs";
     12 
     13 describe("<Base>", () => {
     14  let DEFAULT_PROPS = {
     15    store: { getState: () => {} },
     16    App: { initialized: true },
     17    Prefs: { values: {} },
     18    Sections: [],
     19    DiscoveryStream: { config: { enabled: false } },
     20    dispatch: () => {},
     21    adminContent: {
     22      message: {},
     23    },
     24    document: {
     25      visibilityState: "visible",
     26      addEventListener: sinon.stub(),
     27      removeEventListener: sinon.stub(),
     28    },
     29  };
     30 
     31  it("should render Base component", () => {
     32    const wrapper = shallow(<Base {...DEFAULT_PROPS} />);
     33    assert.ok(wrapper.exists());
     34  });
     35 
     36  it("should render the BaseContent component, passing through all props", () => {
     37    const wrapper = shallow(<Base {...DEFAULT_PROPS} />);
     38    const props = wrapper.find(BaseContent).props();
     39    assert.deepEqual(
     40      props,
     41      DEFAULT_PROPS,
     42      JSON.stringify([props, DEFAULT_PROPS], null, 3)
     43    );
     44  });
     45 
     46  it("should render an ErrorBoundary with class base-content-fallback", () => {
     47    const wrapper = shallow(<Base {...DEFAULT_PROPS} />);
     48 
     49    assert.equal(
     50      wrapper.find(ErrorBoundary).first().prop("className"),
     51      "base-content-fallback"
     52    );
     53  });
     54 
     55  it("should render an WithDsAdmin if the devtools pref is true", () => {
     56    const wrapper = shallow(
     57      <Base
     58        {...DEFAULT_PROPS}
     59        Prefs={{ values: { "asrouter.devtoolsEnabled": true } }}
     60      />
     61    );
     62    assert.lengthOf(wrapper.find(WithDsAdmin), 1);
     63  });
     64 
     65  it("should not render an WithDsAdmin if the devtools pref is false", () => {
     66    const wrapper = shallow(
     67      <Base
     68        {...DEFAULT_PROPS}
     69        Prefs={{ values: { "asrouter.devtoolsEnabled": false } }}
     70      />
     71    );
     72    assert.lengthOf(wrapper.find(WithDsAdmin), 0);
     73  });
     74 });
     75 
     76 describe("<BaseContent>", () => {
     77  let DEFAULT_PROPS = {
     78    store: { getState: () => {} },
     79    App: { initialized: true },
     80    Prefs: { values: {} },
     81    Sections: [],
     82    DiscoveryStream: { config: { enabled: false }, spocs: {} },
     83    Weather: {},
     84    dispatch: () => {},
     85    document: {
     86      visibilityState: "visible",
     87      addEventListener: sinon.stub(),
     88      removeEventListener: sinon.stub(),
     89    },
     90  };
     91 
     92  it("should render an ErrorBoundary with a Search child", () => {
     93    const searchEnabledProps = Object.assign({}, DEFAULT_PROPS, {
     94      Prefs: { values: { showSearch: true } },
     95    });
     96 
     97    const wrapper = shallow(<BaseContent {...searchEnabledProps} />);
     98 
     99    assert.isTrue(wrapper.find(Search).parent().is(ErrorBoundary));
    100  });
    101 
    102  it("should dispatch a user event when the customize menu is opened or closed", () => {
    103    const dispatch = sinon.stub();
    104    const wrapper = shallow(
    105      <BaseContent
    106        {...DEFAULT_PROPS}
    107        dispatch={dispatch}
    108        App={{ customizeMenuVisible: true }}
    109      />
    110    );
    111    wrapper.instance().openCustomizationMenu();
    112    assert.calledWith(dispatch, { type: "SHOW_PERSONALIZE" });
    113    assert.calledWith(dispatch, ac.UserEvent({ event: "SHOW_PERSONALIZE" }));
    114    wrapper.instance().closeCustomizationMenu();
    115    assert.calledWith(dispatch, { type: "HIDE_PERSONALIZE" });
    116    assert.calledWith(dispatch, ac.UserEvent({ event: "HIDE_PERSONALIZE" }));
    117  });
    118 
    119  it("should render only search if no Sections are enabled", () => {
    120    const onlySearchProps = Object.assign({}, DEFAULT_PROPS, {
    121      Sections: [{ id: "highlights", enabled: false }],
    122      Prefs: { values: { showSearch: true } },
    123    });
    124 
    125    const wrapper = shallow(<BaseContent {...onlySearchProps} />);
    126    assert.lengthOf(wrapper.find(".only-search"), 1);
    127  });
    128 
    129  it("should update firstVisibleTimestamp if it is visible immediately with no event listener", () => {
    130    const props = Object.assign({}, DEFAULT_PROPS, {
    131      document: {
    132        visibilityState: "visible",
    133        addEventListener: sinon.spy(),
    134        removeEventListener: sinon.spy(),
    135      },
    136    });
    137 
    138    const wrapper = shallow(<BaseContent {...props} />);
    139    assert.notCalled(props.document.addEventListener);
    140    assert.isDefined(wrapper.state("firstVisibleTimestamp"));
    141  });
    142  it("should attach an event listener for visibility change if it is not visible", () => {
    143    const props = Object.assign({}, DEFAULT_PROPS, {
    144      document: {
    145        visibilityState: "hidden",
    146        addEventListener: sinon.spy(),
    147        removeEventListener: sinon.spy(),
    148      },
    149    });
    150 
    151    const wrapper = shallow(<BaseContent {...props} />);
    152    assert.calledWith(props.document.addEventListener, "visibilitychange");
    153    assert.notExists(wrapper.state("firstVisibleTimestamp"));
    154  });
    155  it("should remove the event listener for visibility change when unmounted", () => {
    156    const props = Object.assign({}, DEFAULT_PROPS, {
    157      document: {
    158        visibilityState: "hidden",
    159        addEventListener: sinon.spy(),
    160        removeEventListener: sinon.spy(),
    161      },
    162    });
    163 
    164    const wrapper = shallow(<BaseContent {...props} />);
    165    const [, listener] = props.document.addEventListener.firstCall.args;
    166 
    167    wrapper.unmount();
    168    assert.calledWith(
    169      props.document.removeEventListener,
    170      "visibilitychange",
    171      listener
    172    );
    173  });
    174  it("should remove the event listener for visibility change after becoming visible", () => {
    175    const listeners = new Set();
    176    const props = Object.assign({}, DEFAULT_PROPS, {
    177      document: {
    178        visibilityState: "hidden",
    179        addEventListener: (ev, cb) => listeners.add(cb),
    180        removeEventListener: (ev, cb) => listeners.delete(cb),
    181      },
    182    });
    183 
    184    const wrapper = shallow(<BaseContent {...props} />);
    185    assert.equal(listeners.size, 1);
    186    assert.notExists(wrapper.state("firstVisibleTimestamp"));
    187 
    188    // Simulate listeners getting called
    189    props.document.visibilityState = "visible";
    190    listeners.forEach(l => l());
    191 
    192    assert.equal(listeners.size, 0);
    193    assert.isDefined(wrapper.state("firstVisibleTimestamp"));
    194  });
    195 });
    196 
    197 describe("WithDsAdmin", () => {
    198  describe("rendering inner content", () => {
    199    it("should not set devtoolsCollapsed state for about:newtab (no hash)", () => {
    200      const wrapper = shallow(<WithDsAdmin hash="" />);
    201      assert.isTrue(
    202        wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed")
    203      );
    204      assert.lengthOf(wrapper.find(BaseContent), 1);
    205    });
    206 
    207    it("should set devtoolsCollapsed state for about:newtab#devtools", () => {
    208      const wrapper = shallow(<WithDsAdmin hash="#devtools" />);
    209      assert.isFalse(
    210        wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed")
    211      );
    212      assert.lengthOf(wrapper.find(BaseContent), 0);
    213    });
    214 
    215    it("should set devtoolsCollapsed state for about:newtab#devtools subroutes", () => {
    216      const wrapper = shallow(<WithDsAdmin hash="#devtools-foo" />);
    217      assert.isFalse(
    218        wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed")
    219      );
    220      assert.lengthOf(wrapper.find(BaseContent), 0);
    221    });
    222  });
    223 
    224  describe("SPOC Placeholder Duration Tracking", () => {
    225    let wrapper;
    226    let instance;
    227    let dispatch;
    228    let clock;
    229    let baseProps;
    230 
    231    beforeEach(() => {
    232      // Setup: Create a component with expired spocs (showing placeholders)
    233      // - useFakeTimers allows us to control time for duration testing
    234      // - lastUpdated is 120000ms (2 mins) ago, exceeding cacheUpdateTime of 60000ms (1 min)
    235      // - In this setup, spocs are expired and placeholders should be visible
    236      clock = sinon.useFakeTimers();
    237      dispatch = sinon.spy();
    238      baseProps = {
    239        store: { getState: () => {} },
    240        App: { initialized: true },
    241        Prefs: { values: {} },
    242        Sections: [],
    243        Weather: {},
    244        document: {
    245          visibilityState: "visible",
    246          addEventListener: sinon.stub(),
    247          removeEventListener: sinon.stub(),
    248        },
    249      };
    250      const props = {
    251        ...baseProps,
    252        dispatch,
    253        DiscoveryStream: {
    254          config: { enabled: true },
    255          spocs: {
    256            onDemand: { enabled: true, loaded: false },
    257            lastUpdated: Date.now() - 120000, // Expired (120s ago)
    258            cacheUpdateTime: 60000, // Cache expires after 60s
    259          },
    260        },
    261      };
    262      wrapper = shallow(<BaseContent {...props} />);
    263      instance = wrapper.instance();
    264      instance.setState({ visible: true });
    265    });
    266 
    267    afterEach(() => {
    268      clock.restore();
    269    });
    270 
    271    it("should start tracking when placeholders become visible", () => {
    272      const prevProps = {
    273        ...baseProps,
    274        DiscoveryStream: {
    275          config: { enabled: true },
    276          spocs: {
    277            onDemand: { enabled: true, loaded: false },
    278            lastUpdated: Date.now() - 30000,
    279            cacheUpdateTime: 60000,
    280          },
    281        },
    282      };
    283 
    284      clock.tick(1000);
    285      instance.trackSpocPlaceholderDuration(prevProps);
    286 
    287      assert.isNotNull(instance.spocPlaceholderStartTime);
    288    });
    289 
    290    it("should record duration when placeholders are replaced", () => {
    291      // Create a fresh wrapper with expired spocs
    292      const freshDispatch = sinon.spy();
    293      const expiredTime = Date.now() - 120000;
    294      const freshWrapper = shallow(
    295        <BaseContent
    296          {...baseProps}
    297          dispatch={freshDispatch}
    298          DiscoveryStream={{
    299            config: { enabled: true },
    300            spocs: {
    301              onDemand: { enabled: true, loaded: false },
    302              lastUpdated: expiredTime,
    303              cacheUpdateTime: 60000,
    304            },
    305          }}
    306        />
    307      );
    308      const freshInstance = freshWrapper.instance();
    309      freshInstance.setState({ visible: true });
    310 
    311      // Advance clock a bit first so startTime is not 0 (which is falsy)
    312      clock.tick(100);
    313 
    314      // Set start time and advance clock
    315      const startTime = Date.now();
    316      freshInstance.spocPlaceholderStartTime = startTime;
    317      clock.tick(150);
    318 
    319      // Update to fresh spocs - this triggers componentDidUpdate
    320      // which automatically calls trackSpocPlaceholderDuration
    321      freshWrapper.setProps({
    322        ...baseProps,
    323        dispatch: freshDispatch,
    324        DiscoveryStream: {
    325          config: { enabled: true },
    326          spocs: {
    327            onDemand: { enabled: true, loaded: false },
    328            lastUpdated: Date.now(),
    329            cacheUpdateTime: 60000,
    330          },
    331        },
    332      });
    333 
    334      // componentDidUpdate should have dispatched the placeholder duration action
    335      const placeholderCall = freshDispatch
    336        .getCalls()
    337        .find(
    338          call =>
    339            call.args[0].type === "DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION"
    340        );
    341 
    342      assert.isNotNull(
    343        placeholderCall,
    344        "Placeholder duration action should be dispatched"
    345      );
    346      const [action] = placeholderCall.args;
    347      assert.equal(action.data.duration, 150);
    348      assert.deepEqual(action.meta, {
    349        from: "ActivityStream:Content",
    350        to: "ActivityStream:Main",
    351        skipLocal: true,
    352      });
    353 
    354      assert.isNull(freshInstance.spocPlaceholderStartTime);
    355    });
    356 
    357    it("should start tracking on onVisible if placeholders already expired", () => {
    358      wrapper.setProps({
    359        DiscoveryStream: {
    360          config: { enabled: true },
    361          spocs: {
    362            onDemand: { enabled: true, loaded: false },
    363            lastUpdated: Date.now() - 120000,
    364            cacheUpdateTime: 60000,
    365          },
    366        },
    367      });
    368 
    369      instance.setState({ visible: false });
    370      instance.spocPlaceholderStartTime = null;
    371 
    372      instance.onVisible();
    373 
    374      assert.isNotNull(instance.spocPlaceholderStartTime);
    375    });
    376 
    377    it("should not start tracking if tab is not visible", () => {
    378      instance.setState({ visible: false });
    379      instance.spocPlaceholderStartTime = null;
    380 
    381      const prevProps = {
    382        ...baseProps,
    383        DiscoveryStream: {
    384          config: { enabled: true },
    385          spocs: {
    386            onDemand: { enabled: true, loaded: false },
    387            lastUpdated: Date.now() - 30000,
    388            cacheUpdateTime: 60000,
    389          },
    390        },
    391      };
    392 
    393      instance.trackSpocPlaceholderDuration(prevProps);
    394 
    395      assert.isNull(instance.spocPlaceholderStartTime);
    396    });
    397 
    398    it("should not start tracking if onDemand is disabled", () => {
    399      // Reset instance to have onDemand disabled from the start
    400      const props = {
    401        ...baseProps,
    402        dispatch,
    403        DiscoveryStream: {
    404          config: { enabled: true },
    405          spocs: {
    406            onDemand: { enabled: false, loaded: false },
    407            lastUpdated: Date.now() - 120000,
    408            cacheUpdateTime: 60000,
    409          },
    410        },
    411      };
    412      wrapper = shallow(<BaseContent {...props} />);
    413      instance = wrapper.instance();
    414      instance.setState({ visible: true });
    415      instance.spocPlaceholderStartTime = null;
    416 
    417      const prevProps = {
    418        ...baseProps,
    419        DiscoveryStream: {
    420          config: { enabled: true },
    421          spocs: {
    422            onDemand: { enabled: false, loaded: false },
    423            lastUpdated: Date.now() - 120000,
    424            cacheUpdateTime: 60000,
    425          },
    426        },
    427      };
    428 
    429      instance.trackSpocPlaceholderDuration(prevProps);
    430 
    431      assert.isNull(instance.spocPlaceholderStartTime);
    432    });
    433  });
    434 });