tor-browser

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

utils.test.jsx (10696B)


      1 import React, { useEffect } from "react";
      2 import { mount } from "enzyme";
      3 import {
      4  useIntersectionObserver,
      5  getActiveCardSize,
      6  getActiveColumnLayout,
      7  useConfetti,
      8  selectWeatherPlacement,
      9 } from "content-src/lib/utils.jsx";
     10 
     11 // Test component to use the useIntersectionObserver
     12 function TestComponent({ callback, threshold }) {
     13  const ref = useIntersectionObserver(callback, threshold);
     14  return <div ref={el => ref.current.push(el)}></div>;
     15 }
     16 
     17 function TestConfettiComponent({ count, spread }) {
     18  const [canvasRef, fireConfetti] = useConfetti(count, spread);
     19 
     20  useEffect(() => {
     21    // Trigger the animation once mounted
     22    fireConfetti();
     23  }, [fireConfetti]);
     24 
     25  return <canvas ref={canvasRef} width={100} height={100} />;
     26 }
     27 
     28 describe("useIntersectionObserver", () => {
     29  let callback;
     30  let threshold;
     31  let sandbox;
     32  let observerStub;
     33  let wrapper;
     34 
     35  beforeEach(() => {
     36    sandbox = sinon.createSandbox();
     37    callback = sandbox.spy();
     38    threshold = 0.5;
     39    observerStub = sandbox
     40      .stub(window, "IntersectionObserver")
     41      .callsFake(function (cb) {
     42        this.observe = sandbox.spy();
     43        this.unobserve = sandbox.spy();
     44        this.disconnect = sandbox.spy();
     45        this.callback = cb;
     46      });
     47    wrapper = mount(
     48      <TestComponent callback={callback} threshold={threshold} />
     49    );
     50  });
     51 
     52  afterEach(() => {
     53    sandbox.restore();
     54    wrapper.unmount();
     55  });
     56 
     57  it("should create an IntersectionObserver instance with the correct options", () => {
     58    assert.calledWithNew(observerStub);
     59    assert.calledWith(observerStub, sinon.match.any, { threshold });
     60  });
     61 
     62  it("should observe elements when mounted", () => {
     63    const observerInstance = observerStub.getCall(0).returnValue;
     64    assert.called(observerInstance.observe);
     65  });
     66 
     67  it("should call callback and unobserve element when it intersects", () => {
     68    wrapper = mount(
     69      <TestComponent callback={callback} threshold={threshold} />
     70    );
     71    const observerInstance = observerStub.getCall(0).returnValue;
     72    const observedElement = wrapper.find("div").getDOMNode();
     73 
     74    // Simulate an intersection
     75    observerInstance.callback([
     76      { isIntersecting: true, target: observedElement },
     77    ]);
     78 
     79    assert.calledOnce(callback);
     80    assert.calledWith(callback, observedElement);
     81    assert.calledOnce(observerInstance.unobserve);
     82    assert.calledWith(observerInstance.unobserve, observedElement);
     83  });
     84 
     85  it("should not call callback if element is not intersecting", () => {
     86    wrapper = mount(
     87      <TestComponent callback={callback} threshold={threshold} />
     88    );
     89    const observerInstance = observerStub.getCall(0).returnValue;
     90    const observedElement = wrapper.find("div").getDOMNode();
     91 
     92    // Simulate a non-intersecting entry
     93    observerInstance.callback([
     94      { isIntersecting: false, target: observedElement },
     95    ]);
     96 
     97    assert.notCalled(callback);
     98    assert.notCalled(observerInstance.unobserve);
     99  });
    100 });
    101 
    102 describe("getActiveCardSize", () => {
    103  it("returns 'large-card' for col-4-large and screen width 1920 and sections enabled", () => {
    104    const result = getActiveCardSize(
    105      1920,
    106      "col-4-large col-3-medium col-2-small col-1-small",
    107      true
    108    );
    109    assert.equal(result, "large-card");
    110  });
    111 
    112  it("returns 'medium-card' for col-3-medium and screen width 1200 and sections enabled", () => {
    113    const result = getActiveCardSize(
    114      1200,
    115      "col-4-large col-3-medium col-2-small col-1-small",
    116      true
    117    );
    118    assert.equal(result, "medium-card");
    119  });
    120 
    121  it("returns 'small-card' for col-2-small and screen width 800 and sections enabled", () => {
    122    const result = getActiveCardSize(
    123      800,
    124      "col-4-large col-3-medium col-2-small col-1-medium",
    125      true
    126    );
    127    assert.equal(result, "small-card");
    128  });
    129 
    130  it("returns 'medium-card' for col-1-medium at 500px", () => {
    131    const result = getActiveCardSize(
    132      500,
    133      "col-1-medium col-1-position-0",
    134      true
    135    );
    136    assert.equal(result, "medium-card");
    137  });
    138 
    139  it("returns 'medium-card' for col-1-small at 500px (edge case)", () => {
    140    const result = getActiveCardSize(500, "col-1-small col-1-position-0", true);
    141    assert.equal(result, "medium-card");
    142  });
    143 
    144  it("returns null when no matching card type is found (edge case)", () => {
    145    const result = getActiveCardSize(
    146      1200,
    147      "col-4-position-0 col-3-position-0",
    148      true
    149    );
    150    assert.isNull(result);
    151  });
    152 
    153  it("returns 'medium-card' when required arguments are missing and sections are disabled", () => {
    154    const result = getActiveCardSize(null, null, false);
    155    assert.equal(result, "medium-card");
    156  });
    157 
    158  it("returns null when required arguments are missing and sections are enabled", () => {
    159    const result = getActiveCardSize(null, null, true);
    160    assert.isNull(result);
    161  });
    162 
    163  it("returns 'spoc' when flightId has value", () => {
    164    const result = getActiveCardSize(null, null, false, 123);
    165    assert.equal(result, "spoc");
    166  });
    167 });
    168 
    169 describe("getActiveColumnLayout", () => {
    170  it("returns 'col-4' for screen width 1920", () => {
    171    const result = getActiveColumnLayout(1920);
    172    assert.equal(result, "col-4");
    173  });
    174 
    175  it("returns 'col-3' for screen width 1200", () => {
    176    const result = getActiveColumnLayout(1200);
    177    assert.equal(result, "col-3");
    178  });
    179 
    180  it("returns 'col-2' for screen width 800", () => {
    181    const result = getActiveColumnLayout(800);
    182    assert.equal(result, "col-2");
    183  });
    184 
    185  it("returns 'col-1' for screen width 500", () => {
    186    const result = getActiveColumnLayout(500);
    187    assert.equal(result, "col-1");
    188  });
    189 });
    190 
    191 describe("useConfetti hook", () => {
    192  let sandbox;
    193  let rafStub;
    194  // eslint-disable-next-line no-unused-vars
    195  let cafStub;
    196  let getContextStub;
    197  let fakeContext;
    198 
    199  beforeEach(() => {
    200    sandbox = sinon.createSandbox();
    201 
    202    // Create a fake 2D context
    203    fakeContext = {
    204      clearRect: sandbox.spy(),
    205      setTransform: sandbox.spy(),
    206      rotate: sandbox.spy(),
    207      scale: sandbox.spy(),
    208      fillRect: sandbox.spy(),
    209      globalAlpha: 1,
    210    };
    211 
    212    // Stub getContext on all canvas elements
    213    getContextStub = sandbox
    214      .stub(HTMLCanvasElement.prototype, "getContext")
    215      .withArgs("2d")
    216      .returns(fakeContext);
    217 
    218    sandbox
    219      .stub(window, "matchMedia")
    220      .withArgs("(prefers-reduced-motion: reduce)")
    221      .returns({ matches: false });
    222 
    223    // stub so that it only runs for one frame
    224    rafStub = sandbox.stub(window, "requestAnimationFrame").returns(24);
    225    cafStub = sandbox.stub(window, "cancelAnimationFrame");
    226  });
    227 
    228  afterEach(() => {
    229    sandbox.restore();
    230  });
    231 
    232  it("should initialize and animate confetti when fireConfetti is called", () => {
    233    // Mount the component, which calls fireConfetti in useEffect
    234    mount(<TestConfettiComponent count={5} />);
    235    assert.calledWith(getContextStub, "2d");
    236    assert.ok(fakeContext.clearRect.calledOnce);
    237    assert.equal(fakeContext.fillRect.callCount, 5);
    238    assert.ok(rafStub.calledOnce);
    239  });
    240  it("does nothing when prefers-reduced-motion is enabled", () => {
    241    // simulate prefers reduced motion
    242    window.matchMedia
    243      .withArgs("(prefers-reduced-motion: reduce)")
    244      .returns({ matches: true });
    245 
    246    mount(<TestConfettiComponent count={5} />);
    247 
    248    // Confrim the confetti hasnt been drawn
    249    assert.ok(fakeContext.clearRect.notCalled);
    250    assert.ok(fakeContext.fillRect.notCalled);
    251    assert.ok(rafStub.notCalled);
    252  });
    253 });
    254 
    255 describe("selectWeatherPlacement", () => {
    256  // literal URL used inside the selector
    257  const FEED_URL =
    258    "https://merino.services.mozilla.com/api/v1/curated-recommendations";
    259 
    260  function mockState({
    261    placement,
    262    pocketEnabled = true,
    263    systemEnabled = true,
    264    dailyBriefEnabled = true,
    265    sectionId = "daily_brief",
    266    blocked = false,
    267    sections = [
    268      { sectionKey: "daily_brief", receivedRank: 0 },
    269      { sectionKey: "other", receivedRank: 1 },
    270    ],
    271  } = {}) {
    272    return {
    273      Prefs: {
    274        values: {
    275          // intent pref
    276          "weather.placement": placement,
    277          // story feed prefs used by selector in this file
    278          "feeds.section.topstories": pocketEnabled,
    279          "feeds.system.topstories": systemEnabled,
    280          // daily brief prefs; selector uses trainhopConfig first, falls back to these
    281          "discoverystream.dailyBrief.enabled": dailyBriefEnabled,
    282          "discoverystream.dailyBrief.sectionId": sectionId,
    283          // include trainhopConfig for parity with production (optional)
    284          trainhopConfig: {
    285            dailyBriefing: {
    286              enabled: dailyBriefEnabled,
    287              sectionId,
    288            },
    289          },
    290        },
    291      },
    292      DiscoveryStream: {
    293        sectionPersonalization: {
    294          [sectionId]: { isBlocked: blocked },
    295        },
    296        feeds: {
    297          data: {
    298            [FEED_URL]: {
    299              data: { sections },
    300            },
    301          },
    302        },
    303      },
    304    };
    305  }
    306 
    307  it("returns 'header' when placement pref is missing or 'header'", () => {
    308    const invalidPlacement = mockState({ placement: undefined });
    309 
    310    console.log(
    311      "TESTSTATE: ",
    312      invalidPlacement.Prefs.values["weather.placement"]
    313    );
    314    const headerPLacement = mockState({ placement: "header" });
    315    assert.equal(selectWeatherPlacement(invalidPlacement), "header");
    316    assert.equal(selectWeatherPlacement(headerPLacement), "header");
    317  });
    318 
    319  it("returns 'section' when placement is 'section' and daily brief is enabled, unblocked, and at top", () => {
    320    const state = mockState({ placement: "section" });
    321    assert.equal(selectWeatherPlacement(state), "section");
    322  });
    323 
    324  it("returns 'header' when DB section is not at the top (receivedRank !== 0 || index !== 0)", () => {
    325    const state = mockState({
    326      placement: "section",
    327      sections: [
    328        { sectionKey: "other", receivedRank: 0 },
    329        { sectionKey: "daily_brief", receivedRank: 1 },
    330      ],
    331    });
    332    assert.equal(selectWeatherPlacement(state), "header");
    333  });
    334 
    335  it("returns 'header' when DB section is blocked", () => {
    336    const state = mockState({ blocked: true, placement: "section" });
    337    assert.equal(selectWeatherPlacement(state), "header");
    338  });
    339 
    340  it("returns 'header' when Pocket/topstories is disabled", () => {
    341    const state = mockState({
    342      placement: "section",
    343      pocketEnabled: false,
    344    });
    345    assert.equal(selectWeatherPlacement(state), "header");
    346  });
    347 
    348  it("returns 'header' when sections have not loaded yet", () => {
    349    const state = mockState({
    350      placement: "section",
    351      sections: [], // simulate no feed yet
    352    });
    353    assert.equal(selectWeatherPlacement(state), "header");
    354  });
    355 });