From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/unit/content-src/components/Card.test.jsx | 510 +++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 browser/components/newtab/test/unit/content-src/components/Card.test.jsx (limited to 'browser/components/newtab/test/unit/content-src/components/Card.test.jsx') diff --git a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx new file mode 100644 index 0000000000..5f07570b2e --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx @@ -0,0 +1,510 @@ +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { + _Card as Card, + PlaceholderCard, +} from "content-src/components/Card/Card"; +import { combineReducers, createStore } from "redux"; +import { GlobalOverrider } from "test/unit/utils"; +import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; +import { cardContextTypes } from "content-src/components/Card/types"; +import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; +import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; +import { Provider } from "react-redux"; +import React from "react"; +import { shallow, mount } from "enzyme"; + +let DEFAULT_PROPS = { + dispatch: sinon.stub(), + index: 0, + link: { + hostname: "foo", + title: "A title for foo", + url: "http://www.foo.com", + type: "history", + description: "A description for foo", + image: "http://www.foo.com/img.png", + guid: 1, + }, + eventSource: "TOP_STORIES", + shouldSendImpressionStats: true, + contextMenuOptions: ["Separator"], +}; + +let DEFAULT_BLOB_IMAGE = { + path: "/testpath", + data: new Blob([0]), +}; + +function mountCardWithProps(props) { + const store = createStore(combineReducers(reducers), INITIAL_STATE); + return mount( + + + + ); +} + +describe("", () => { + let globals; + let wrapper; + beforeEach(() => { + globals = new GlobalOverrider(); + wrapper = mountCardWithProps(DEFAULT_PROPS); + }); + afterEach(() => { + DEFAULT_PROPS.dispatch.reset(); + globals.restore(); + }); + it("should render a Card component", () => assert.ok(wrapper.exists())); + it("should add the right url", () => { + assert.propertyVal( + wrapper.find("a").props(), + "href", + DEFAULT_PROPS.link.url + ); + + // test that pocket cards get a special open_url href + const pocketLink = Object.assign({}, DEFAULT_PROPS.link, { + open_url: "getpocket.com/foo", + type: "pocket", + }); + wrapper = mount( + + ); + assert.propertyVal(wrapper.find("a").props(), "href", pocketLink.open_url); + }); + it("should display a title", () => + assert.equal(wrapper.find(".card-title").text(), DEFAULT_PROPS.link.title)); + it("should display a description", () => + assert.equal( + wrapper.find(".card-description").text(), + DEFAULT_PROPS.link.description + )); + it("should display a host name", () => + assert.equal(wrapper.find(".card-host-name").text(), "foo")); + it("should have a link menu button", () => + assert.ok(wrapper.find(".context-menu-button").exists())); + it("should render a link menu when button is clicked", () => { + const button = wrapper.find(".context-menu-button"); + assert.equal(wrapper.find(LinkMenu).length, 0); + button.simulate("click", { preventDefault: () => {} }); + assert.equal(wrapper.find(LinkMenu).length, 1); + }); + it("should pass dispatch, source, onUpdate, site, options, and index to LinkMenu", () => { + wrapper + .find(".context-menu-button") + .simulate("click", { preventDefault: () => {} }); + const { dispatch, source, onUpdate, site, options, index } = wrapper + .find(LinkMenu) + .props(); + assert.equal(dispatch, DEFAULT_PROPS.dispatch); + assert.equal(source, DEFAULT_PROPS.eventSource); + assert.ok(onUpdate); + assert.equal(site, DEFAULT_PROPS.link); + assert.equal(options, DEFAULT_PROPS.contextMenuOptions); + assert.equal(index, DEFAULT_PROPS.index); + }); + it("should pass through the correct menu options to LinkMenu if overridden by individual card", () => { + const link = Object.assign({}, DEFAULT_PROPS.link); + link.contextMenuOptions = ["CheckBookmark"]; + + wrapper = mountCardWithProps(Object.assign({}, DEFAULT_PROPS, { link })); + wrapper + .find(".context-menu-button") + .simulate("click", { preventDefault: () => {} }); + const { options } = wrapper.find(LinkMenu).props(); + assert.equal(options, link.contextMenuOptions); + }); + it("should have a context based on type", () => { + wrapper = shallow(); + const context = wrapper.find(".card-context"); + const { icon, fluentID } = cardContextTypes[DEFAULT_PROPS.link.type]; + assert.isTrue(context.childAt(0).hasClass(`icon-${icon}`)); + assert.isTrue(context.childAt(1).hasClass("card-context-label")); + assert.equal(context.childAt(1).prop("data-l10n-id"), fluentID); + }); + it("should support setting custom context", () => { + const linkWithCustomContext = { + type: "history", + context: "Custom", + icon: "icon-url", + }; + + wrapper = shallow( + + ); + const context = wrapper.find(".card-context"); + const { icon } = cardContextTypes[DEFAULT_PROPS.link.type]; + assert.isFalse(context.childAt(0).hasClass(`icon-${icon}`)); + assert.equal( + context.childAt(0).props().style.backgroundImage, + "url('icon-url')" + ); + + assert.isTrue(context.childAt(1).hasClass("card-context-label")); + assert.equal(context.childAt(1).text(), linkWithCustomContext.context); + }); + it("should parse args for fluent correctly", () => { + const title = '"fluent"'; + const link = { ...DEFAULT_PROPS.link, title }; + + wrapper = mountCardWithProps({ ...DEFAULT_PROPS, link }); + let button = wrapper.find(ContextMenuButton).find("button"); + + assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title })); + }); + it("should have .active class, on card-outer if context menu is open", () => { + const button = wrapper.find(ContextMenuButton); + assert.isFalse( + wrapper.find(".card-outer").hasClass("active"), + "does not have active class" + ); + button.simulate("click", { preventDefault: () => {} }); + assert.isTrue( + wrapper.find(".card-outer").hasClass("active"), + "has active class" + ); + }); + it("should send OPEN_DOWNLOAD_FILE if we clicked on a download", () => { + const downloadLink = { + type: "download", + url: "download.mov", + }; + wrapper = mountCardWithProps( + Object.assign({}, DEFAULT_PROPS, { link: downloadLink }) + ); + const card = wrapper.find(".card"); + card.simulate("click", { preventDefault: () => {} }); + assert.calledThrice(DEFAULT_PROPS.dispatch); + + assert.equal( + DEFAULT_PROPS.dispatch.firstCall.args[0].type, + at.OPEN_DOWNLOAD_FILE + ); + assert.deepEqual( + DEFAULT_PROPS.dispatch.firstCall.args[0].data, + downloadLink + ); + }); + it("should send OPEN_LINK if we clicked on anything other than a download", () => { + const nonDownloadLink = { + type: "history", + url: "download.mov", + }; + wrapper = mountCardWithProps( + Object.assign({}, DEFAULT_PROPS, { link: nonDownloadLink }) + ); + const card = wrapper.find(".card"); + const event = { + altKey: "1", + button: "2", + ctrlKey: "3", + metaKey: "4", + shiftKey: "5", + }; + card.simulate( + "click", + Object.assign({}, event, { preventDefault: () => {} }) + ); + assert.calledThrice(DEFAULT_PROPS.dispatch); + + assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK); + }); + describe("card image display", () => { + const DEFAULT_BLOB_URL = "blob://test"; + let url; + beforeEach(() => { + url = { + createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL), + revokeObjectURL: globals.sandbox.spy(), + }; + globals.set("URL", url); + }); + afterEach(() => { + globals.restore(); + }); + it("should display a regular image correctly and not call revokeObjectURL when unmounted", () => { + wrapper = shallow(); + + assert.isUndefined(wrapper.state("cardImage").path); + assert.equal(wrapper.state("cardImage").url, DEFAULT_PROPS.link.image); + assert.equal( + wrapper.find(".card-preview-image").props().style.backgroundImage, + `url(${wrapper.state("cardImage").url})` + ); + + wrapper.unmount(); + assert.notCalled(url.revokeObjectURL); + }); + it("should display a blob image correctly and revoke blob url when unmounted", () => { + const link = Object.assign({}, DEFAULT_PROPS.link, { + image: DEFAULT_BLOB_IMAGE, + }); + wrapper = shallow(); + + assert.equal(wrapper.state("cardImage").path, DEFAULT_BLOB_IMAGE.path); + assert.equal(wrapper.state("cardImage").url, DEFAULT_BLOB_URL); + assert.equal( + wrapper.find(".card-preview-image").props().style.backgroundImage, + `url(${wrapper.state("cardImage").url})` + ); + + wrapper.unmount(); + assert.calledOnce(url.revokeObjectURL); + }); + it("should not show an image if there isn't one and not call revokeObjectURL when unmounted", () => { + const link = Object.assign({}, DEFAULT_PROPS.link); + delete link.image; + + wrapper = shallow(); + + assert.isNull(wrapper.state("cardImage")); + assert.lengthOf(wrapper.find(".card-preview-image"), 0); + + wrapper.unmount(); + assert.notCalled(url.revokeObjectURL); + }); + it("should remove current card image if new image is not present", () => { + wrapper = shallow(); + + const otherLink = Object.assign({}, DEFAULT_PROPS.link); + delete otherLink.image; + wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink })); + + assert.isNull(wrapper.state("cardImage")); + }); + it("should not create or revoke urls if normal image is already in state", () => { + wrapper = shallow(); + + wrapper.setProps(DEFAULT_PROPS); + + assert.notCalled(url.createObjectURL); + assert.notCalled(url.revokeObjectURL); + }); + it("should not create or revoke more urls if blob image is already in state", () => { + const link = Object.assign({}, DEFAULT_PROPS.link, { + image: DEFAULT_BLOB_IMAGE, + }); + wrapper = shallow(); + + assert.calledOnce(url.createObjectURL); + assert.notCalled(url.revokeObjectURL); + + wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link })); + + assert.calledOnce(url.createObjectURL); + assert.notCalled(url.revokeObjectURL); + }); + it("should create blob urls for new blobs and revoke existing ones", () => { + const link = Object.assign({}, DEFAULT_PROPS.link, { + image: DEFAULT_BLOB_IMAGE, + }); + wrapper = shallow(); + + assert.calledOnce(url.createObjectURL); + assert.notCalled(url.revokeObjectURL); + + const otherLink = Object.assign({}, DEFAULT_PROPS.link, { + image: { path: "/newpath", data: new Blob([0]) }, + }); + wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink })); + + assert.calledTwice(url.createObjectURL); + assert.calledOnce(url.revokeObjectURL); + }); + it("should not call createObjectURL and revokeObjectURL for normal images", () => { + wrapper = shallow(); + + assert.notCalled(url.createObjectURL); + assert.notCalled(url.revokeObjectURL); + + const otherLink = Object.assign({}, DEFAULT_PROPS.link, { + image: "https://other/image", + }); + wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink })); + + assert.notCalled(url.createObjectURL); + assert.notCalled(url.revokeObjectURL); + }); + }); + describe("image loading", () => { + let link; + let triggerImage = {}; + let uniqueLink = 0; + beforeEach(() => { + global.Image.prototype = { + addEventListener(event, callback) { + triggerImage[event] = () => Promise.resolve(callback()); + }, + }; + + link = Object.assign({}, DEFAULT_PROPS.link); + link.image += uniqueLink++; + wrapper = shallow(); + }); + it("should have a loaded preview image when the image is loaded", () => { + assert.isFalse(wrapper.find(".card-preview-image").hasClass("loaded")); + + wrapper.setState({ imageLoaded: true }); + + assert.isTrue(wrapper.find(".card-preview-image").hasClass("loaded")); + }); + it("should start not loaded", () => { + assert.isFalse(wrapper.state("imageLoaded")); + }); + it("should be loaded after load", async () => { + await triggerImage.load(); + + assert.isTrue(wrapper.state("imageLoaded")); + }); + it("should be not be loaded after error ", async () => { + await triggerImage.error(); + + assert.isFalse(wrapper.state("imageLoaded")); + }); + it("should be not be loaded if image changes", async () => { + await triggerImage.load(); + const otherLink = Object.assign({}, link, { + image: "https://other/image", + }); + + wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink })); + + assert.isFalse(wrapper.state("imageLoaded")); + }); + }); + describe("placeholder=true", () => { + beforeEach(() => { + wrapper = mount(); + }); + it("should render when placeholder=true", () => { + assert.ok(wrapper.exists()); + }); + it("should add a placeholder class to the outer element", () => { + assert.isTrue(wrapper.find(".card-outer").hasClass("placeholder")); + }); + it("should not have a context menu button or LinkMenu", () => { + assert.isFalse( + wrapper.find(ContextMenuButton).exists(), + "context menu button" + ); + assert.isFalse(wrapper.find(LinkMenu).exists(), "LinkMenu"); + }); + it("should not call onLinkClick when the link is clicked", () => { + const spy = sinon.spy(wrapper.instance(), "onLinkClick"); + const card = wrapper.find(".card"); + card.simulate("click"); + assert.notCalled(spy); + }); + }); + describe("#trackClick", () => { + it("should call dispatch when the link is clicked with the right data", () => { + const card = wrapper.find(".card"); + const event = { + altKey: "1", + button: "2", + ctrlKey: "3", + metaKey: "4", + shiftKey: "5", + }; + card.simulate( + "click", + Object.assign({}, event, { preventDefault: () => {} }) + ); + assert.calledThrice(DEFAULT_PROPS.dispatch); + + // first dispatch call is the AlsoToMain message which will open a link in a window, and send some event data + assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK); + assert.deepEqual( + DEFAULT_PROPS.dispatch.firstCall.args[0].data.event, + event + ); + + // second dispatch call is a UserEvent action for telemetry + assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]); + assert.calledWith( + DEFAULT_PROPS.dispatch.secondCall, + ac.UserEvent({ + event: "CLICK", + source: DEFAULT_PROPS.eventSource, + action_position: DEFAULT_PROPS.index, + }) + ); + + // third dispatch call is to send impression stats + assert.calledWith( + DEFAULT_PROPS.dispatch.thirdCall, + ac.ImpressionStats({ + source: DEFAULT_PROPS.eventSource, + click: 0, + tiles: [{ id: DEFAULT_PROPS.link.guid, pos: DEFAULT_PROPS.index }], + }) + ); + }); + it("should provide card_type to telemetry info if type is not history", () => { + const link = Object.assign({}, DEFAULT_PROPS.link); + link.type = "bookmark"; + wrapper = mount(); + const card = wrapper.find(".card"); + const event = { + altKey: "1", + button: "2", + ctrlKey: "3", + metaKey: "4", + shiftKey: "5", + }; + + card.simulate( + "click", + Object.assign({}, event, { preventDefault: () => {} }) + ); + + assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]); + assert.calledWith( + DEFAULT_PROPS.dispatch.secondCall, + ac.UserEvent({ + event: "CLICK", + source: DEFAULT_PROPS.eventSource, + action_position: DEFAULT_PROPS.index, + value: { card_type: link.type }, + }) + ); + }); + it("should notify Web Extensions with WEBEXT_CLICK if props.isWebExtension is true", () => { + wrapper = mountCardWithProps( + Object.assign({}, DEFAULT_PROPS, { + isWebExtension: true, + eventSource: "MyExtension", + index: 3, + }) + ); + const card = wrapper.find(".card"); + const event = { preventDefault() {} }; + card.simulate("click", event); + assert.calledWith( + DEFAULT_PROPS.dispatch, + ac.WebExtEvent(at.WEBEXT_CLICK, { + source: "MyExtension", + url: DEFAULT_PROPS.link.url, + action_position: 3, + }) + ); + }); + }); +}); + +describe("", () => { + it("should render a Card with placeholder=true", () => { + const wrapper = mount( + + + + ); + assert.isTrue(wrapper.find(Card).props().placeholder); + }); +}); -- cgit v1.2.3