Source code

Revision control

Copy as Markdown

Other Tools

import { actionCreators as ac, actionTypes as at } from "common/Actions.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",
type: "history",
description: "A description for foo",
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(
<Provider store={store}>
<Card {...props} />
</Provider>
);
}
describe("<Card>", () => {
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(
<Card {...Object.assign({}, DEFAULT_PROPS, { link: pocketLink })} />
);
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: () => {} });
// eslint-disable-next-line no-shadow
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: () => {} });
// eslint-disable-next-line no-shadow
const { options } = wrapper.find(LinkMenu).props();
assert.equal(options, link.contextMenuOptions);
});
it("should have a context based on type", () => {
wrapper = shallow(<Card {...DEFAULT_PROPS} />);
const cardContext = wrapper.find(".card-context");
const { icon, fluentID } = cardContextTypes[DEFAULT_PROPS.link.type];
assert.isTrue(cardContext.childAt(0).hasClass(`icon-${icon}`));
assert.isTrue(cardContext.childAt(1).hasClass("card-context-label"));
assert.equal(cardContext.childAt(1).prop("data-l10n-id"), fluentID);
});
it("should support setting custom context", () => {
const linkWithCustomContext = {
type: "history",
context: "Custom",
icon: "icon-url",
};
wrapper = shallow(
<Card
{...Object.assign({}, DEFAULT_PROPS, { link: linkWithCustomContext })}
/>
);
const cardContext = wrapper.find(".card-context");
const { icon } = cardContextTypes[DEFAULT_PROPS.link.type];
assert.isFalse(cardContext.childAt(0).hasClass(`icon-${icon}`));
assert.equal(
cardContext.childAt(0).props().style.backgroundImage,
"url('icon-url')"
);
assert.isTrue(cardContext.childAt(1).hasClass("card-context-label"));
assert.equal(cardContext.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(<Card {...DEFAULT_PROPS} />);
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(<Card {...DEFAULT_PROPS} link={link} />);
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(<Card {...DEFAULT_PROPS} link={link} />);
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(<Card {...DEFAULT_PROPS} />);
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(<Card {...DEFAULT_PROPS} />);
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(<Card {...DEFAULT_PROPS} link={link} />);
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(<Card {...DEFAULT_PROPS} link={link} />);
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(<Card {...DEFAULT_PROPS} />);
assert.notCalled(url.createObjectURL);
assert.notCalled(url.revokeObjectURL);
const otherLink = Object.assign({}, DEFAULT_PROPS.link, {
});
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(<Card {...DEFAULT_PROPS} link={link} />);
});
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, {
});
wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));
assert.isFalse(wrapper.state("imageLoaded"));
});
});
describe("placeholder=true", () => {
beforeEach(() => {
wrapper = mount(<Card placeholder={true} />);
});
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(<Card {...Object.assign({}, DEFAULT_PROPS, { link })} />);
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("<PlaceholderCard />", () => {
it("should render a Card with placeholder=true", () => {
const wrapper = mount(
<Provider store={createStore(combineReducers(reducers), INITIAL_STATE)}>
<PlaceholderCard />
</Provider>
);
assert.isTrue(wrapper.find(Card).props().placeholder);
});
});