Merge pull request #3975 from sarracini/bug_1432657

Fix Bug 1432657 - Add pocket card type to highlight types
This commit is contained in:
Ursula Sarracini 2018-02-23 13:46:44 -05:00 коммит произвёл GitHub
Родитель f6dc30bfa1 5b5fb32674
Коммит b033185d8e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 381 добавлений и 38 удалений

Просмотреть файл

@ -33,6 +33,7 @@ type_label_visited=Visited
type_label_bookmarked=Bookmarked
type_label_synced=Synced from another device
type_label_recommended=Trending
type_label_pocket=Saved to Pocket
# LOCALIZATION NOTE(type_label_open): Open is an adjective, as in "page is open"
type_label_open=Open
type_label_topic=Topic
@ -58,6 +59,8 @@ confirm_history_delete_p1=Are you sure you want to delete every instance of this
# page from history.
confirm_history_delete_notice_p2=This action cannot be undone.
menu_action_save_to_pocket=Save to Pocket
menu_action_delete_pocket=Delete from Pocket
menu_action_archive_pocket=Archive in Pocket
# LOCALIZATION NOTE (search_for_something_with): {search_term} is a placeholder
# for what the user has typed in the search input field, e.g. 'Search for ' +

Просмотреть файл

@ -25,9 +25,11 @@ this.globalImportContext = globalImportContext;
// }
const actionTypes = {};
for (const type of [
"ARCHIVE_FROM_POCKET",
"BLOCK_URL",
"BOOKMARK_URL",
"DELETE_BOOKMARK_BY_ID",
"DELETE_FROM_POCKET",
"DELETE_HISTORY_URL",
"DELETE_HISTORY_URL_CONFIRM",
"DIALOG_CANCEL",
@ -53,6 +55,7 @@ for (const type of [
"PLACES_HISTORY_CLEARED",
"PLACES_LINKS_DELETED",
"PLACES_LINK_BLOCKED",
"PLACES_SAVED_TO_POCKET",
"PREFS_INITIAL_VALUES",
"PREF_CHANGED",
"RICH_ICON_MISSING",

Просмотреть файл

@ -265,6 +265,22 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
return item;
})
}));
case at.PLACES_SAVED_TO_POCKET:
if (!action.data) {
return prevState;
}
return prevState.map(section => Object.assign({}, section, {
rows: section.rows.map(item => {
if (item.url === action.data.url) {
return Object.assign({}, item, {
pocket_id: action.data.pocket_id,
title: action.data.title,
type: "pocket"
});
}
return item;
})
}));
case at.PLACES_BOOKMARK_REMOVED:
if (!action.data) {
return prevState;
@ -291,6 +307,10 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
case at.PLACES_LINK_BLOCKED:
return prevState.map(section =>
Object.assign({}, section, {rows: section.rows.filter(site => site.url !== action.data.url)}));
case at.DELETE_FROM_POCKET:
case at.ARCHIVE_FROM_POCKET:
return prevState.map(section =>
Object.assign({}, section, {rows: section.rows.filter(site => site.pocket_id !== action.data.pocket_id)}));
default:
return prevState;
}

Просмотреть файл

@ -14,5 +14,9 @@ export const cardContextTypes = {
now: {
intlID: "type_label_now",
icon: "now"
},
pocket: {
intlID: "type_label_pocket",
icon: "pocket-small"
}
};

Просмотреть файл

@ -34,7 +34,7 @@ export class ContextMenu extends React.PureComponent {
<ul role="menu" className="context-menu-list">
{this.props.options.map((option, i) => (option.type === "separator" ?
(<li key={i} className="separator" />) :
(<ContextMenuItem key={i} option={option} hideContext={this.hideContext} />)
(option.type !== "empty" && <ContextMenuItem key={i} option={option} hideContext={this.hideContext} />)
))}
</ul>
</span>);

Просмотреть файл

@ -7,6 +7,7 @@ import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
*/
export const LinkMenuOptions = {
Separator: () => ({type: "separator"}),
EmptyItem: () => ({type: "empty"}),
RemoveBookmark: site => ({
id: "menu_action_remove_bookmark",
icon: "bookmark-added",
@ -48,7 +49,7 @@ export const LinkMenuOptions = {
icon: "dismiss",
action: ac.AlsoToMain({
type: at.BLOCK_URL,
data: site.url
data: {url: site.url, pocket_id: site.pocket_id}
}),
impression: ac.ImpressionStats({
source: eventSource,
@ -77,7 +78,7 @@ export const LinkMenuOptions = {
type: at.DIALOG_OPEN,
data: {
onConfirm: [
ac.AlsoToMain({type: at.DELETE_HISTORY_URL, data: {url: site.url, forceBlock: site.bookmarkGuid}}),
ac.AlsoToMain({type: at.DELETE_HISTORY_URL, data: {url: site.url, pocket_id: site.pocket_id, forceBlock: site.bookmarkGuid}}),
ac.UserEvent({event: "DELETE", source: eventSource, action_position: index})
],
eventSource,
@ -121,6 +122,24 @@ export const LinkMenuOptions = {
}),
userEvent: "SAVE_TO_POCKET"
}),
DeleteFromPocket: site => ({
id: "menu_action_delete_pocket",
icon: "delete",
action: ac.AlsoToMain({
type: at.DELETE_FROM_POCKET,
data: {pocket_id: site.pocket_id}
}),
userEvent: "DELETE_FROM_POCKET"
}),
ArchiveFromPocket: site => ({
id: "menu_action_archive_pocket",
icon: "check",
action: ac.AlsoToMain({
type: at.ARCHIVE_FROM_POCKET,
data: {pocket_id: site.pocket_id}
}),
userEvent: "ARCHIVE_FROM_POCKET"
}),
EditTopSite: (site, index) => ({
id: "edit_topsites_button_text",
icon: "edit",
@ -130,5 +149,8 @@ export const LinkMenuOptions = {
}
}),
CheckBookmark: site => (site.bookmarkGuid ? LinkMenuOptions.RemoveBookmark(site) : LinkMenuOptions.AddBookmark(site)),
CheckPinTopSite: (site, index) => (site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index))
CheckPinTopSite: (site, index) => (site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index)),
CheckSavedToPocket: (site, index) => (site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.SaveToPocket(site, index)),
CheckBookmarkOrArchive: site => (site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.CheckBookmark(site)),
CheckDeleteHistoryOrEmpty: (site, index, eventSource) => (site.pocket_id ? LinkMenuOptions.EmptyItem() : LinkMenuOptions.DeleteUrl(site, index, eventSource))
};

Просмотреть файл

@ -84,6 +84,11 @@
background-image: url('#{$image-path}glyph-pocket-16.svg');
}
&.icon-pocket-small {
background-image: url('#{$image-path}glyph-pocket-16.svg');
background-size: $smaller-icon-size;
}
&.icon-historyItem { // sass-lint:disable-line class-name-format
background-image: url('#{$image-path}glyph-historyItem-16.svg');
}

Просмотреть файл

@ -121,6 +121,10 @@ const PREFS_CONFIG = new Map([
title: "Collapse the Highlights section",
value: false
}],
["section.highlights.includePocket", {
title: "Boolean flag that decides whether or not to show saved Pocket stories in highlights.",
value: true
}],
["section.topstories.collapsed", {
title: "Collapse the Top Stories section",
value: false

Просмотреть файл

@ -36,8 +36,8 @@ this.HighlightsFeed = class HighlightsFeed {
}
_dedupeKey(site) {
// Treat bookmarks as un-dedupable, otherwise show one of a url
return site && (site.type === "bookmark" ? {} : site.url);
// Treat bookmarks and pocket items as un-dedupable, otherwise show one of a url
return site && ((site.pocket_id || site.type === "bookmark") ? {} : site.url);
}
init() {
@ -86,8 +86,10 @@ this.HighlightsFeed = class HighlightsFeed {
// Request more than the expected length to allow for items being removed by
// deduping against Top Sites or multiple history from the same domain, etc.
// Until bug 1425496 lands, do not include saved Pocket items in highlights
const manyPages = await this.linksCache.request({numItems: MANY_EXTRA_LENGTH, excludePocket: true});
const manyPages = await this.linksCache.request({
numItems: MANY_EXTRA_LENGTH,
excludePocket: !this.store.getState().Prefs.values["section.highlights.includePocket"]
});
// Remove adult highlights if we need to
const checkedAdult = this.store.getState().Prefs.values.filterAdult ?
@ -112,14 +114,20 @@ this.HighlightsFeed = class HighlightsFeed {
this.fetchImage(page);
}
// Adjust the type for 'history' items that are also 'bookmarked'
if (page.type === "history" && page.bookmarkGuid) {
page.type = "bookmark";
}
// We want the page, so update various fields for UI
Object.assign(page, {
hasImage: true, // We always have an image - fall back to a screenshot
hostname,
type: page.bookmarkGuid ? "bookmark" : page.type
type: page.type,
pocket_id: page.pocket_id
});
// Add the "bookmark" or not-skipped "history"
// Add the "bookmark", "pocket", or not-skipped "history"
highlights.push(page);
hosts.add(hostname);
@ -152,6 +160,34 @@ this.HighlightsFeed = class HighlightsFeed {
});
}
/**
* Deletes an item from a user's saved to Pocket feed and then refreshes highlights
* @param {int} itemID
* The unique ID given by Pocket for that item; used to look the item up when deleting
*/
async deleteFromPocket(itemID) {
try {
await NewTabUtils.activityStreamLinks.deletePocketEntry(itemID);
this.fetchHighlights({broadcast: true});
} catch (err) {
Cu.reportError(err);
}
}
/**
* Archives an item from a user's saved to Pocket feed and then refreshes highlights
* @param {int} itemID
* The unique ID given by Pocket for that item; used to look the item up when archiving
*/
async archiveFromPocket(itemID) {
try {
await NewTabUtils.activityStreamLinks.archivePocketEntry(itemID);
this.fetchHighlights({broadcast: true});
} catch (err) {
Cu.reportError(err);
}
}
onAction(action) {
switch (action.type) {
case at.INIT:
@ -166,8 +202,15 @@ this.HighlightsFeed = class HighlightsFeed {
case at.PLACES_LINK_BLOCKED:
this.fetchHighlights({broadcast: true});
break;
case at.DELETE_FROM_POCKET:
this.deleteFromPocket(action.data.pocket_id);
break;
case at.ARCHIVE_FROM_POCKET:
this.archiveFromPocket(action.data.pocket_id);
break;
case at.PLACES_BOOKMARK_ADDED:
case at.PLACES_BOOKMARK_REMOVED:
case at.PLACES_SAVED_TO_POCKET:
this.linksCache.expire();
this.fetchHighlights({broadcast: false});
break;
@ -181,4 +224,4 @@ this.HighlightsFeed = class HighlightsFeed {
}
};
this.EXPORTED_SYMBOLS = ["HighlightsFeed", "SECTION_ID"];
this.EXPORTED_SYMBOLS = ["HighlightsFeed", "SECTION_ID", "MANY_EXTRA_LENGTH"];

Просмотреть файл

@ -12,8 +12,6 @@ ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
ChromeUtils.defineModuleGetter(this, "Pocket",
"chrome://pocket/content/Pocket.jsm");
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
@ -262,6 +260,21 @@ class PlacesFeed {
win.openLinkIn(action.data.url, where || win.whereToOpenLink(event), params);
}
async saveToPocket(site, browser) {
const {url, title} = site;
try {
let data = await NewTabUtils.activityStreamLinks.addPocketEntry(url, title, browser);
if (data) {
this.store.dispatch(ac.BroadcastToContent({
type: at.PLACES_SAVED_TO_POCKET,
data: {url, title, pocket_id: data.item.item_id}
}));
}
} catch (err) {
Cu.reportError(err);
}
}
onAction(action) {
switch (action.type) {
case at.INIT:
@ -271,9 +284,11 @@ class PlacesFeed {
case at.UNINIT:
this.removeObservers();
break;
case at.BLOCK_URL:
NewTabUtils.activityStreamLinks.blockURL({url: action.data});
case at.BLOCK_URL: {
const {url, pocket_id} = action.data;
NewTabUtils.activityStreamLinks.blockURL({url, pocket_id});
break;
}
case at.BOOKMARK_URL:
NewTabUtils.activityStreamLinks.addBookmark(action.data, action._target.browser);
break;
@ -281,10 +296,10 @@ class PlacesFeed {
NewTabUtils.activityStreamLinks.deleteBookmark(action.data);
break;
case at.DELETE_HISTORY_URL: {
const {url, forceBlock} = action.data;
const {url, forceBlock, pocket_id} = action.data;
NewTabUtils.activityStreamLinks.deleteHistoryEntry(url);
if (forceBlock) {
NewTabUtils.activityStreamLinks.blockURL({url});
NewTabUtils.activityStreamLinks.blockURL({url, pocket_id});
}
break;
}
@ -295,7 +310,7 @@ class PlacesFeed {
this.openLink(action, "window", true);
break;
case at.SAVE_TO_POCKET:
Pocket.savePage(action._target.browser, action.data.site.url, action.data.site.title);
this.saveToPocket(action.data.site, action._target.browser);
break;
case at.OPEN_LINK: {
this.openLink(action);

Просмотреть файл

@ -42,7 +42,7 @@ const BUILT_IN_SECTIONS = {
},
privacyNoticeURL: options.privacy_notice_link || "https://www.mozilla.org/privacy/firefox/#suggest-relevant-content",
maxRows: 1,
availableLinkMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
availableLinkMenuOptions: ["CheckBookmarkOrArchive", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
emptyState: {
message: {id: "topstories_empty_state", values: {provider: options.provider_name}},
icon: "check"
@ -62,7 +62,7 @@ const BUILT_IN_SECTIONS = {
icon: "highlights",
title: {id: "header_highlights"},
maxRows: 3,
availableLinkMenuOptions: ["CheckBookmark", "SaveToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
availableLinkMenuOptions: ["CheckBookmarkOrArchive", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "CheckDeleteHistoryOrEmpty"],
emptyState: {
message: {id: "highlights_empty_state"},
icon: "highlights"

Просмотреть файл

@ -83,7 +83,9 @@ export const UserEventAction = Joi.object().keys({
"SECTION_MENU_EXPAND",
"SECTION_MENU_MANAGE",
"SECTION_MENU_ADD_TOPSITE",
"SECTION_MENU_PRIVACY_NOTICE"
"SECTION_MENU_PRIVACY_NOTICE",
"DELETE_FROM_POCKET",
"ARCHIVE_FROM_POCKET"
]).required(),
source: Joi.valid(["TOP_SITES", "TOP_STORIES", "HIGHLIGHTS"]),
action_position: Joi.number().integer()

Просмотреть файл

@ -181,7 +181,7 @@ describe("Reducers", () => {
id: `foo_bar_${i}`,
title: `Foo Bar ${i}`,
initialized: false,
rows: [{url: "www.foo.bar"}, {url: "www.other.url"}],
rows: [{url: "www.foo.bar", pocket_id: 123}, {url: "www.other.url"}],
order: i,
type: "history"
}));
@ -353,6 +353,20 @@ describe("Reducers", () => {
assert.lengthOf(section.rows, 0);
});
});
it("should remove all removed pocket urls", () => {
const removeAction = {type: at.DELETE_FROM_POCKET, data: {pocket_id: 123}};
const newBlockState = Sections(oldState, removeAction);
newBlockState.forEach(section => {
assert.deepEqual(section.rows, [{url: "www.other.url"}]);
});
});
it("should archive all archived pocket urls", () => {
const removeAction = {type: at.ARCHIVE_FROM_POCKET, data: {pocket_id: 123}};
const newBlockState = Sections(oldState, removeAction);
newBlockState.forEach(section => {
assert.deepEqual(section.rows, [{url: "www.other.url"}]);
});
});
it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => {
const nextState = Sections(undefined, {type: at.PLACES_BOOKMARK_ADDED});
assert.equal(nextState, INITIAL_STATE.Sections);
@ -411,6 +425,32 @@ describe("Reducers", () => {
assert.isUndefined(newRow.bookmarkTitle);
assert.isUndefined(newRow.bookmarkDateCreated);
// old row is unchanged
assert.equal(oldRow, oldState[0].rows[1]);
});
it("should not update state for empty action.data on PLACES_SAVED_TO_POCKET", () => {
const nextState = Sections(undefined, {type: at.PLACES_SAVED_TO_POCKET});
assert.equal(nextState, INITIAL_STATE.Sections);
});
it("should add a pocked item on PLACES_SAVED_TO_POCKET", () => {
const action = {
type: at.PLACES_SAVED_TO_POCKET,
data: {
url: "www.foo.bar",
pocket_id: 1234,
title: "Title for bar.com"
}
};
const nextState = Sections(oldState, action);
// check a section to ensure the correct url was saved to pocket
const [newRow, oldRow] = nextState[0].rows;
// new row has pocket data
assert.equal(newRow.url, action.data.url);
assert.equal(newRow.type, "pocket");
assert.equal(newRow.pocket_id, action.data.pocket_id);
assert.equal(newRow.title, action.data.title);
// old row is unchanged
assert.equal(oldRow, oldState[0].rows[1]);
});

Просмотреть файл

@ -75,6 +75,41 @@ describe("<LinkMenu>", () => {
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_bookmark")));
});
it("should show Save to Pocket option for an unsaved Pocket item if CheckSavedToPocket in options list", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: "", bookmarkGuid: 0}} source={"HIGHLIGHTS"} options={["CheckSavedToPocket"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_save_to_pocket")));
});
it("should show Delete from Pocket option for a saved Pocket item if CheckSavedToPocket in options list", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: "", pocket_id: 1234}} source={"HIGHLIGHTS"} options={["CheckSavedToPocket"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_delete_pocket")));
});
it("should show Archive from Pocket option for a saved Pocket item if CheckBookmarkOrArchive", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: "", pocket_id: 1234}} source={"HIGHLIGHTS"} options={["CheckBookmarkOrArchive"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_archive_pocket")));
});
it("should show Bookmark option for an unbookmarked site if CheckBookmarkOrArchive in options list and no pocket_id", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: ""}} source={"HIGHLIGHTS"} options={["CheckBookmarkOrArchive"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_bookmark")));
});
it("should show Unbookmark option for a bookmarked site if CheckBookmarkOrArchive in options list and no pocket_id", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: "", bookmarkGuid: 1234}} source={"HIGHLIGHTS"} options={["CheckBookmarkOrArchive"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_remove_bookmark")));
});
it("should show Delete from History option for a site that is visited but not from Pocket if CheckDeleteHistoryOrEmpty", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: history}} source={"HIGHLIGHTS"} options={["CheckDeleteHistoryOrEmpty"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_delete")));
});
it("should not show a Delete from History option for a site that is Pocket'ed if CheckDeleteHistoryOrEmpty", () => {
wrapper = shallowWithIntl(<LinkMenu site={{url: "", pocket_id: 1234}} source={"HIGHLIGHTS"} options={["CheckDeleteHistoryOrEmpty"]} dispatch={() => {}} />);
const {options} = wrapper.find(ContextMenu).props();
assert.isUndefined(options.find(o => (o.id && o.id === "menu_action_delete")));
});
it("should show Edit option", () => {
const props = {url: "foo", label: "label"};
const index = 5;
@ -104,20 +139,22 @@ describe("<LinkMenu>", () => {
describe(".onClick", () => {
const FAKE_INDEX = 3;
const FAKE_SOURCE = "TOP_SITES";
const FAKE_SITE = {url: "https://foo.com", referrer: "https://foo.com/ref", title: "bar", bookmarkGuid: 1234, hostname: "foo", type: "history"};
const FAKE_SITE = {url: "https://foo.com", pocket_id: "1234", referrer: "https://foo.com/ref", title: "bar", bookmarkGuid: 1234, hostname: "foo", type: "history"};
const dispatch = sinon.stub();
const propOptions = ["Separator", "RemoveBookmark", "AddBookmark", "OpenInNewWindow", "OpenInPrivateWindow", "BlockUrl", "DeleteUrl", "PinTopSite", "UnpinTopSite", "SaveToPocket", "WebExtDismiss"];
const propOptions = ["Separator", "RemoveBookmark", "AddBookmark", "OpenInNewWindow", "OpenInPrivateWindow", "BlockUrl", "DeleteUrl", "PinTopSite", "UnpinTopSite", "SaveToPocket", "DeleteFromPocket", "ArchiveFromPocket", "WebExtDismiss"];
const expectedActionData = {
menu_action_remove_bookmark: FAKE_SITE.bookmarkGuid,
menu_action_bookmark: {url: FAKE_SITE.url, title: FAKE_SITE.title, type: FAKE_SITE.type},
menu_action_open_new_window: {url: FAKE_SITE.url, referrer: FAKE_SITE.referrer},
menu_action_open_private_window: {url: FAKE_SITE.url, referrer: FAKE_SITE.referrer},
menu_action_dismiss: FAKE_SITE.url,
menu_action_dismiss: {url: FAKE_SITE.url, pocket_id: FAKE_SITE.pocket_id},
menu_action_webext_dismiss: {source: "TOP_SITES", url: FAKE_SITE.url, action_position: 3},
menu_action_delete: {url: FAKE_SITE.url, forceBlock: FAKE_SITE.bookmarkGuid},
menu_action_delete: {url: FAKE_SITE.url, pocket_id: FAKE_SITE.pocket_id, forceBlock: FAKE_SITE.bookmarkGuid},
menu_action_pin: {site: {url: FAKE_SITE.url}, index: FAKE_INDEX},
menu_action_unpin: {site: {url: FAKE_SITE.url}},
menu_action_save_to_pocket: {site: {url: FAKE_SITE.url, title: FAKE_SITE.title}}
menu_action_save_to_pocket: {site: {url: FAKE_SITE.url, title: FAKE_SITE.title}},
menu_action_delete_pocket: {pocket_id: "1234"},
menu_action_archive_pocket: {pocket_id: "1234"}
};
const {options} = shallowWithIntl(<LinkMenu site={FAKE_SITE} dispatch={dispatch} index={FAKE_INDEX} options={propOptions} source={FAKE_SOURCE} shouldSendImpressionStats={true} />)

Просмотреть файл

@ -12,6 +12,7 @@ const FAKE_IMAGE = "data123";
describe("Highlights Feed", () => {
let HighlightsFeed;
let SECTION_ID;
let MANY_EXTRA_LENGTH;
let feed;
let globals;
let sandbox;
@ -26,7 +27,13 @@ describe("Highlights Feed", () => {
beforeEach(() => {
globals = new GlobalOverrider();
sandbox = globals.sandbox;
fakeNewTabUtils = {activityStreamLinks: {getHighlights: sandbox.spy(() => Promise.resolve(links))}};
fakeNewTabUtils = {
activityStreamLinks: {
getHighlights: sandbox.spy(() => Promise.resolve(links)),
deletePocketEntry: sandbox.spy(() => Promise.resolve({})),
archivePocketEntry: sandbox.spy(() => Promise.resolve({}))
}
};
sectionsManagerStub = {
onceInitialized: sinon.stub().callsFake(callback => callback()),
enableSection: sinon.spy(),
@ -49,7 +56,7 @@ describe("Highlights Feed", () => {
globals.set("NewTabUtils", fakeNewTabUtils);
globals.set("PageThumbs", fakePageThumbs);
({HighlightsFeed, SECTION_ID} = injector({
({HighlightsFeed, SECTION_ID, MANY_EXTRA_LENGTH} = injector({
"lib/FilterAdult.jsm": {filterAdult: filterAdultStub},
"lib/ShortURL.jsm": {shortURL: shortURLStub},
"lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
@ -61,7 +68,7 @@ describe("Highlights Feed", () => {
dispatch: sinon.spy(),
getState() { return this.state; },
state: {
Prefs: {values: {filterAdult: false}},
Prefs: {values: {"filterAdult": false, "section.highlights.includePocket": false}},
TopSites: {
initialized: true,
rows: Array(12).fill(null).map((v, i) => ({url: `http://www.topsite${i}.com`}))
@ -232,6 +239,31 @@ describe("Highlights Feed", () => {
assert.equal(highlights[0].url, links[0].url);
assert.equal(highlights[1].url, links[2].url);
});
it("should take both a bookmark and a pocket of the same hostname", async () => {
links = [
{url: "https://site.com/bookmark", type: "bookmark"},
{url: "https://site.com/pocket", type: "pocket"}
];
const highlights = await fetchHighlights();
assert.equal(highlights.length, 2);
assert.equal(highlights[0].url, links[0].url);
assert.equal(highlights[1].url, links[1].url);
});
it("should includePocket pocket items when pref is true", async () => {
feed.store.state.Prefs.values["section.highlights.includePocket"] = true;
sandbox.spy(feed.linksCache, "request");
await feed.fetchHighlights();
assert.calledWith(feed.linksCache.request, {numItems: MANY_EXTRA_LENGTH, excludePocket: false});
});
it("should not includePocket pocket items when pref is false", async () => {
sandbox.spy(feed.linksCache, "request");
await feed.fetchHighlights();
assert.calledWith(feed.linksCache.request, {numItems: MANY_EXTRA_LENGTH, excludePocket: true});
});
it("should set type to bookmark if there is a bookmarkGuid", async () => {
links = [{url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890"}];
@ -394,6 +426,16 @@ describe("Highlights Feed", () => {
assert.calledOnce(feed.linksCache.expire);
});
it("should fetch highlights and expire the cache on PLACES_SAVED_TO_POCKET", async () => {
await feed.fetchHighlights();
feed.fetchHighlights = sinon.spy();
sandbox.stub(feed.linksCache, "expire");
feed.onAction({type: at.PLACES_SAVED_TO_POCKET});
assert.calledOnce(feed.fetchHighlights);
assert.calledWith(feed.fetchHighlights, {broadcast: false});
assert.calledOnce(feed.linksCache.expire);
});
it("should call fetchHighlights with broadcast false on TOP_SITES_UPDATED", () => {
sandbox.stub(feed, "fetchHighlights");
feed.onAction({type: at.TOP_SITES_UPDATED});
@ -401,5 +443,59 @@ describe("Highlights Feed", () => {
assert.calledOnce(feed.fetchHighlights);
assert.calledWithExactly(feed.fetchHighlights, {broadcast: false});
});
it("should call deleteFromPocket on DELETE_FROM_POCKET", () => {
sandbox.stub(feed, "deleteFromPocket");
feed.onAction({type: at.DELETE_FROM_POCKET, data: {pocket_id: 12345}});
assert.calledOnce(feed.deleteFromPocket);
assert.calledWithExactly(feed.deleteFromPocket, 12345);
});
it("should call fetchHighlights when deleting from Pocket", async () => {
feed.fetchHighlights = sinon.spy();
await feed.deleteFromPocket(12345);
assert.calledOnce(feed.fetchHighlights);
assert.calledWithExactly(feed.fetchHighlights, {broadcast: true});
});
it("should catch if deletePocketEntry throws", async () => {
sandbox.spy(global.Cu, "reportError");
fakeNewTabUtils.activityStreamLinks.deletePocketEntry = sandbox.stub().rejects("not ok");
await feed.deleteFromPocket(12345);
assert.calledOnce(global.Cu.reportError);
});
it("should call NewTabUtils.deletePocketEntry when deleting from Pocket", async () => {
await feed.deleteFromPocket(12345);
assert.calledOnce(global.NewTabUtils.activityStreamLinks.deletePocketEntry);
assert.calledWith(global.NewTabUtils.activityStreamLinks.deletePocketEntry, 12345);
});
it("should call archiveFromPocket on ARCHIVE_FROM_POCKET", () => {
sandbox.stub(feed, "archiveFromPocket");
feed.onAction({type: at.ARCHIVE_FROM_POCKET, data: {pocket_id: 12345}});
assert.calledOnce(feed.archiveFromPocket);
assert.calledWithExactly(feed.archiveFromPocket, 12345);
});
it("should call fetchHighlights when archiving from Pocket", async () => {
feed.fetchHighlights = sinon.spy();
await feed.archiveFromPocket(12345);
assert.calledOnce(feed.fetchHighlights);
assert.calledWithExactly(feed.fetchHighlights, {broadcast: true});
});
it("should catch if archiveFromPocket throws", async () => {
sandbox.spy(global.Cu, "reportError");
fakeNewTabUtils.activityStreamLinks.archivePocketEntry = sandbox.stub().rejects("not ok");
await feed.archiveFromPocket(12345);
assert.calledOnce(global.Cu.reportError);
});
it("should call NewTabUtils.archivePocketEntry when deleting from Pocket", async () => {
await feed.archiveFromPocket(12345);
assert.calledOnce(global.NewTabUtils.activityStreamLinks.archivePocketEntry);
assert.calledWith(global.NewTabUtils.activityStreamLinks.archivePocketEntry, 12345);
});
});
});

Просмотреть файл

@ -25,7 +25,8 @@ describe("PlacesFeed", () => {
addBookmark: sandbox.spy(),
deleteBookmark: sandbox.spy(),
deleteHistoryEntry: sandbox.spy(),
blockURL: sandbox.spy()
blockURL: sandbox.spy(),
addPocketEntry: sandbox.spy(() => Promise.resolve())
}
});
globals.set("PlacesUtils", {
@ -41,7 +42,6 @@ describe("PlacesFeed", () => {
SOURCES
}
});
globals.set("Pocket", {savePage: sandbox.spy()});
global.Cc["@mozilla.org/browser/nav-history-service;1"] = {
getService() {
return global.PlacesUtils.history;
@ -96,8 +96,8 @@ describe("PlacesFeed", () => {
assert.calledWith(global.Services.obs.removeObserver, feed, BLOCKED_EVENT);
});
it("should block a url on BLOCK_URL", () => {
feed.onAction({type: at.BLOCK_URL, data: "apple.com"});
assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "apple.com"});
feed.onAction({type: at.BLOCK_URL, data: {url: "apple.com", pocket_id: 1234}});
assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "apple.com", pocket_id: 1234});
});
it("should bookmark a url on BOOKMARK_URL", () => {
const data = {url: "pear.com", title: "A pear"};
@ -117,7 +117,7 @@ describe("PlacesFeed", () => {
it("should delete a history entry on DELETE_HISTORY_URL and force a site to be blocked if specified", () => {
feed.onAction({type: at.DELETE_HISTORY_URL, data: {url: "guava.com", forceBlock: "g123kd"}});
assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "guava.com"});
assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "guava.com", pocket_id: undefined});
});
it("should call openLinkIn with the correct url and where on OPEN_NEW_WINDOW", () => {
const openLinkIn = sinon.stub();
@ -182,9 +182,58 @@ describe("PlacesFeed", () => {
assert.propertyVal(params, "referrerPolicy", 5);
assert.propertyVal(params.referrerURI, "spec", "foo.com/ref");
});
it("should save to Pocket on SAVE_TO_POCKET", () => {
feed.onAction({type: at.SAVE_TO_POCKET, data: {site: {url: "raspberry.com", title: "raspberry"}}, _target: {browser: {}}});
assert.calledWith(global.Pocket.savePage, {}, "raspberry.com", "raspberry");
it("should call saveToPocket on SAVE_TO_POCKET", () => {
const action = {
type: at.SAVE_TO_POCKET,
data: {site: {url: "raspberry.com", title: "raspberry"}},
_target: {browser: {}}
};
sinon.stub(feed, "saveToPocket");
feed.onAction(action);
assert.calledWithExactly(feed.saveToPocket, action.data.site, action._target.browser);
});
it("should call NewTabUtils.activityStreamLinks.addPocketEntry if we are saving a pocket story", async () => {
const action = {
data: {site: {url: "raspberry.com", title: "raspberry"}},
_target: {browser: {}}
};
await feed.saveToPocket(action.data.site, action._target.browser);
assert.calledOnce(global.NewTabUtils.activityStreamLinks.addPocketEntry);
assert.calledWithExactly(global.NewTabUtils.activityStreamLinks.addPocketEntry, action.data.site.url, action.data.site.title, action._target.browser);
});
it("should reject the promise if NewTabUtils.activityStreamLinks.addPocketEntry rejects", async () => {
const e = new Error("Error");
const action = {
data: {site: {url: "raspberry.com", title: "raspberry"}},
_target: {browser: {}}
};
global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox.stub().rejects(e);
await feed.saveToPocket(action.data.site, action._target.browser);
assert.calledWith(global.Cu.reportError, e);
});
it("should broadcast to content if we successfully added a link to Pocket", async () => {
// test in the form that the API returns data based on: https://getpocket.com/developer/docs/v3/add
global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox.stub().resolves({item: {item_id: 1234}});
const action = {
data: {site: {url: "raspberry.com", title: "raspberry"}},
_target: {browser: {}}
};
await feed.saveToPocket(action.data.site, action._target.browser);
assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PLACES_SAVED_TO_POCKET);
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
url: "raspberry.com",
title: "raspberry",
pocket_id: 1234
});
});
it("should only broadcast if we got some data back from addPocketEntry", async () => {
global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox.stub().resolves(null);
const action = {
data: {site: {url: "raspberry.com", title: "raspberry"}},
_target: {browser: {}}
};
await feed.saveToPocket(action.data.site, action._target.browser);
assert.notCalled(feed.store.dispatch);
});
});