Bug 1478144 - Add TippyTop RemoteSettings, TaskCluster MochiTests and bug fixes to Activity Stream. r=ursula

MozReview-Commit-ID: CwJ4zU7CvnD

--HG--
extra : rebase_source : 88f29fe32245e7b73faa399df0d69a454eb19865
This commit is contained in:
Ed Lee 2018-07-24 14:02:06 -07:00
parent 40b40ad653
commit 077dad8094
64 changed files with 1193 additions and 823 deletions

View File

@ -1,11 +1,12 @@
options:
merge-default-rules: true
max-warnings: 0
files:
include: 'content-src/**/*.scss'
rules:
class-name-format: [{convention: ["hyphenatedlowercase", "camelcase"]}]
class-name-format: 0
extends-before-declarations: 2
extends-before-mixins: 2
force-element-nesting: 0

View File

@ -0,0 +1,28 @@
version: 0
tasks:
- provisionerId: '{{ taskcluster.docker.provisionerId }}'
workerType: '{{ taskcluster.docker.workerType }}'
extra:
github:
events:
- pull_request.opened
- pull_request.reopened
- pull_request.synchronize
- push
payload:
maxRunTime: 7200
image: piatra/asmochitests
command:
- /bin/bash
- '--login'
- '-c'
- >-
git clone {{event.head.repo.url}} /activity-stream && cd /activity-stream &&
git checkout {{event.head.sha}} && bash ./mochitest.sh
metadata:
name: activitystream
description: run mochitests for PRs
owner: '{{ event.head.user.email }}'
source: '{{ event.head.repo.url }}'
allowPullRequests: public

View File

@ -1,5 +1,7 @@
# activity-stream
[![Task Status](https://github.taskcluster.net/v1/repository/mozilla/activity-stream/master/badge.svg)](https://github.taskcluster.net/v1/repository/mozilla/activity-stream/master/latest)
This system add-on replaces the new tab page in Firefox with a new design and
functionality as part of the Activity Stream project.

View File

@ -137,6 +137,7 @@ export class ASRouterUISurface extends React.PureComponent {
}
sendImpression(extraProps) {
ASRouterUtils.sendMessage({type: "IMPRESSION", data: this.state.message});
this.sendUserActionTelemetry({event: "IMPRESSION", ...extraProps});
}

View File

@ -10,6 +10,9 @@ Field name | Type | Required | Description | Example / Note
`campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly`
`targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes)
`trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation.
`frequency` | `object` | No | A definition for frequency cap information for the message
`frequency.lifetime` | `integer` | No | The maximum number of lifetime impressions for the message.
`frequency.custom` | `array` | No | An array of frequency cap definition objects including `period`, a time period in milliseconds, and `cap`, a max number of impressions for that period.
### Message example
```javascript
@ -19,6 +22,11 @@ Field name | Type | Required | Description | Example / Note
content: {
title: "Find it faster",
body: "Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box."
},
targeting: "hasFxAccount && !addonsInfo.addons['activity-stream@mozilla.org']",
frequency: {
lifetime: 20,
custom: [{period: "daily", cap: 5}, {period: 3600000, cap: 1}]
}
}
```

View File

@ -2,6 +2,7 @@
"title": "ProviderResponse",
"description": "A response object for remote providers of AS Router",
"type": "object",
"version": "0.1.0",
"properties": {
"messages": {
"type": "array",
@ -31,6 +32,49 @@
"trigger": {
"type": "string",
"description": "A string representing what the trigger to show this message is."
},
"frequency": {
"type": "object",
"description": "An object containing frequency cap information for a message.",
"properties": {
"lifetime": {
"type": "integer",
"description": "The maximum lifetime impressions for a message.",
"minimum": 1,
"maximum": 100
},
"custom": {
"type": "array",
"description": "An array of custom frequency cap definitions.",
"items": {
"description": "A frequency cap definition containing time and max impression information",
"type": "object",
"properties": {
"period": {
"oneOf": [
{
"type": "integer",
"description": "Period of time in milliseconds (e.g. 86400000 for one day)"
},
{
"type": "string",
"description": "One of a preset list of short forms for period of time (e.g. 'daily' for one day)",
"enum": ["daily"]
}
]
},
"cap": {
"type": "integer",
"description": "The maximum impressions for the message within the defined period.",
"minimum": 1,
"maximum": 100
}
},
"required": ["period", "cap"]
}
}
}
}
},
"required": ["id", "template", "content"]

View File

@ -27,7 +27,7 @@
},
"text": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"$ref": "#/definitions/richText"},
{"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
]
},

View File

@ -54,16 +54,18 @@ export class ASRouterAdmin extends React.PureComponent {
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.blockList.includes(msg.id);
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
let itemClassName = "message-item";
if (isCurrent) { itemClassName += " current"; }
if (isBlocked) { itemClassName += " blocked"; }
return (<tr className={itemClassName} key={msg.id}>
<td className="message-id"><span>{msg.id}</span></td>
<td className="message-id"><span>{msg.id} <br /></span></td>
<td>
<button className={`button ${(isBlocked ? "" : " primary")}`} onClick={isBlocked ? this.handleUnblock(msg) : this.handleBlock(msg)}>{isBlocked ? "Unblock" : "Block"}</button>
{isBlocked ? null : <button className="button" onClick={this.handleOverride(msg.id)}>Show</button>}
<br />({impressions} impressions)
</td>
<td className="message-summary">
<pre>{JSON.stringify(msg, null, 2)}</pre>

View File

@ -15,10 +15,6 @@
}
main {
.hide-main & {
visibility: hidden;
}
margin: auto;
// Offset the snippets container so things at the bottom of the page are still
// visible when snippets / onboarding are visible. Adjust for other spacing.
@ -45,6 +41,11 @@ main {
margin-bottom: $section-spacing;
position: relative;
}
.hide-main & {
visibility: hidden;
}
}
.base-content-fallback {

View File

@ -247,8 +247,9 @@
&:not(.no-description) .card-title {
font-size: $card-title-font-size;
line-height: $card-title-font-size + 1;
max-height: $card-title-font-size + 1;
max-height: $card-title-font-size + 5;
overflow: hidden;
padding: 0 0 4px;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -72,11 +72,11 @@
}
.background,
body.hide-main {
body.hide-main { // sass-lint:disable-line no-qualifying-elements
width: 100%;
height: 100%;
display: block;
background-image: url('#{$image-path}fox-tail.png'), linear-gradient(to bottom, $blue-70 40%, #004EC2 60%, $blue-60 80%, #0080FF 90%, #00C7FF 100%);
background-image: url('#{$image-path}fox-tail.png'), $about-welcome-gradient;
background-position-x: center;
background-position-y: -200px, top;
background-repeat: no-repeat;
@ -104,7 +104,7 @@ body.hide-main {
font-size: 12px;
max-width: 340px;
margin: 17px 50px;
color: #676F7E;
color: $about-welcome-extra-links;
cursor: default;
a {
@ -239,7 +239,7 @@ body.hide-main {
padding-bottom: 210px;
}
a.firstrun-link {
a.firstrun-link { // sass-lint:disable-line no-qualifying-elements
color: $white;
display: block;
text-decoration: underline;

View File

@ -18,12 +18,12 @@ body {
font-size: 16px;
overflow-y: scroll;
&.hide-onboarding > #onboarding-overlay-button,
&.hide-main > #onboarding-overlay-button {
display: none !important;
&.hide-onboarding > #onboarding-overlay-button, // sass-lint:disable-line no-ids
&.hide-main > #onboarding-overlay-button { // sass-lint:disable-line no-ids
display: none !important; // sass-lint:disable-line no-important
}
&.hide-main > #onboarding-notification-bar {
&.hide-main > #onboarding-notification-bar { // sass-lint:disable-line no-ids
display: none;
}
}

View File

@ -74,62 +74,61 @@ body {
// Snippets
--newtab-snippets-background-color: $white;
--newtab-snippets-hairline-color: transparent;
}
// Dark theme
body[lwt-newtab-brighttext] {
// General styles
--newtab-background-color: $grey-80;
--newtab-border-primary-color: $grey-10-80;
--newtab-border-secondary-color: $grey-10-10;
--newtab-button-primary-color: $blue-60;
--newtab-button-secondary-color: $grey-70;
--newtab-element-active-color: $grey-10-20;
--newtab-element-hover-color: $grey-10-10;
--newtab-icon-primary-color: $grey-10-80;
--newtab-icon-secondary-color: $grey-10-40;
--newtab-icon-tertiary-color: $grey-10-40;
--newtab-inner-box-shadow-color: $grey-10-20;
--newtab-link-primary-color: $blue-40;
--newtab-link-secondary-color: $pocket-teal;
--newtab-text-conditional-color: $grey-10;
--newtab-text-primary-color: $grey-10;
--newtab-text-secondary-color: $grey-10-80;
--newtab-textbox-background-color: $grey-70;
--newtab-textbox-border: $grey-10-20;
@include textbox-focus($blue-40); // sass-lint:disable-line mixins-before-declarations
// Context menu
--newtab-contextmenu-background-color: $grey-60;
--newtab-contextmenu-button-color: $grey-80;
// Modal + overlay
--newtab-modal-color: $grey-80;
--newtab-overlay-color: $grey-90-80;
// Sections
--newtab-section-header-text-color: $grey-10-80;
--newtab-section-navigation-text-color: $grey-10-80;
--newtab-section-active-contextmenu-color: $white;
// Search
--newtab-search-border-color: $grey-10-20;
--newtab-search-dropdown-color: $grey-70;
--newtab-search-dropdown-header-color: $grey-60;
--newtab-search-icon-color: $grey-10-60;
// Top Sites
--newtab-topsites-background-color: $grey-70;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: $grey-10-80;
// Cards
--newtab-card-active-outline-color: $grey-60;
--newtab-card-background-color: $grey-70;
--newtab-card-hairline-color: $grey-10-10;
--newtab-card-shadow: 0 1px 8px 0 $grey-90-20;
// Snippets
--newtab-snippets-background-color: $grey-70;
--newtab-snippets-hairline-color: $white-10;
&[lwt-newtab-brighttext] {
// General styles
--newtab-background-color: $grey-80;
--newtab-border-primary-color: $grey-10-80;
--newtab-border-secondary-color: $grey-10-10;
--newtab-button-primary-color: $blue-60;
--newtab-button-secondary-color: $grey-70;
--newtab-element-active-color: $grey-10-20;
--newtab-element-hover-color: $grey-10-10;
--newtab-icon-primary-color: $grey-10-80;
--newtab-icon-secondary-color: $grey-10-40;
--newtab-icon-tertiary-color: $grey-10-40;
--newtab-inner-box-shadow-color: $grey-10-20;
--newtab-link-primary-color: $blue-40;
--newtab-link-secondary-color: $pocket-teal;
--newtab-text-conditional-color: $grey-10;
--newtab-text-primary-color: $grey-10;
--newtab-text-secondary-color: $grey-10-80;
--newtab-textbox-background-color: $grey-70;
--newtab-textbox-border: $grey-10-20;
@include textbox-focus($blue-40); // sass-lint:disable-line mixins-before-declarations
// Context menu
--newtab-contextmenu-background-color: $grey-60;
--newtab-contextmenu-button-color: $grey-80;
// Modal + overlay
--newtab-modal-color: $grey-80;
--newtab-overlay-color: $grey-90-80;
// Sections
--newtab-section-header-text-color: $grey-10-80;
--newtab-section-navigation-text-color: $grey-10-80;
--newtab-section-active-contextmenu-color: $white;
// Search
--newtab-search-border-color: $grey-10-20;
--newtab-search-dropdown-color: $grey-70;
--newtab-search-dropdown-header-color: $grey-60;
--newtab-search-icon-color: $grey-10-60;
// Top Sites
--newtab-topsites-background-color: $grey-70;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: $grey-10-80;
// Cards
--newtab-card-active-outline-color: $grey-60;
--newtab-card-background-color: $grey-70;
--newtab-card-hairline-color: $grey-10-10;
--newtab-card-shadow: 0 1px 8px 0 $grey-90-20;
// Snippets
--newtab-snippets-background-color: $grey-70;
--newtab-snippets-hairline-color: $white-10;
}
}

View File

@ -53,6 +53,11 @@ $download-icon-fill: #12BC00;
$pocket-icon-fill: #D70022;
$email-input-focus: rgba($blue-50, 0.3);
$email-input-invalid: rgba($red-60, 0.3);
$aw-extra-blue-1: #004EC2;
$aw-extra-blue-2: #0080FF;
$aw-extra-blue-3: #00C7FF;
$about-welcome-gradient: linear-gradient(to bottom, $blue-70 40%, $aw-extra-blue-1 60%, $blue-60 80%, $aw-extra-blue-2 90%, $aw-extra-blue-3 100%);
$about-welcome-extra-links: #676F7E;
// Photon transitions from http://design.firefox.com/photon/motion/duration-and-easing.html
$photon-easing: cubic-bezier(0.07, 0.95, 0, 1);

View File

@ -65,48 +65,47 @@ body {
--newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
--newtab-snippets-background-color: #FFF;
--newtab-snippets-hairline-color: transparent; }
body[lwt-newtab-brighttext] {
--newtab-background-color: #2A2A2E;
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
--newtab-button-primary-color: #0060DF;
--newtab-button-secondary-color: #38383D;
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
--newtab-link-primary-color: #45A1FF;
--newtab-link-secondary-color: #50BCB6;
--newtab-text-conditional-color: #F9F9FA;
--newtab-text-primary-color: #F9F9FA;
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
--newtab-textbox-background-color: #38383D;
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
--newtab-textbox-focus-color: #45A1FF;
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
--newtab-contextmenu-background-color: #4A4A4F;
--newtab-contextmenu-button-color: #2A2A2E;
--newtab-modal-color: #2A2A2E;
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-active-contextmenu-color: #FFF;
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
--newtab-search-dropdown-color: #38383D;
--newtab-search-dropdown-header-color: #4A4A4F;
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
--newtab-topsites-background-color: #38383D;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
--newtab-card-active-outline-color: #4A4A4F;
--newtab-card-background-color: #38383D;
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
--newtab-snippets-background-color: #38383D;
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
body[lwt-newtab-brighttext] {
--newtab-background-color: #2A2A2E;
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
--newtab-button-primary-color: #0060DF;
--newtab-button-secondary-color: #38383D;
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
--newtab-link-primary-color: #45A1FF;
--newtab-link-secondary-color: #50BCB6;
--newtab-text-conditional-color: #F9F9FA;
--newtab-text-primary-color: #F9F9FA;
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
--newtab-textbox-background-color: #38383D;
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
--newtab-textbox-focus-color: #45A1FF;
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
--newtab-contextmenu-background-color: #4A4A4F;
--newtab-contextmenu-button-color: #2A2A2E;
--newtab-modal-color: #2A2A2E;
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-active-contextmenu-color: #FFF;
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
--newtab-search-dropdown-color: #38383D;
--newtab-search-dropdown-header-color: #4A4A4F;
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
--newtab-topsites-background-color: #38383D;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
--newtab-card-active-outline-color: #4A4A4F;
--newtab-card-background-color: #38383D;
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
--newtab-snippets-background-color: #38383D;
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
.icon {
background-position: center center;
@ -335,8 +334,6 @@ main {
margin: auto;
padding-bottom: 68px;
width: 274px; }
.hide-main main {
visibility: hidden; }
@media (min-width: 482px) {
main {
width: 402px; } }
@ -352,6 +349,8 @@ main {
main section {
margin-bottom: 20px;
position: relative; }
.hide-main main {
visibility: hidden; }
.base-content-fallback {
height: 100vh; }
@ -1475,8 +1474,9 @@ a.firstrun-link {
.compact-cards .card-outer .card-text:not(.no-description) .card-title {
font-size: 12px;
line-height: 13px;
max-height: 13px;
max-height: 17px;
overflow: hidden;
padding: 0 0 4px;
text-overflow: ellipsis;
white-space: nowrap; }
.compact-cards .card-outer .card-description {

File diff suppressed because one or more lines are too long

View File

@ -68,48 +68,47 @@ body {
--newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
--newtab-snippets-background-color: #FFF;
--newtab-snippets-hairline-color: transparent; }
body[lwt-newtab-brighttext] {
--newtab-background-color: #2A2A2E;
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
--newtab-button-primary-color: #0060DF;
--newtab-button-secondary-color: #38383D;
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
--newtab-link-primary-color: #45A1FF;
--newtab-link-secondary-color: #50BCB6;
--newtab-text-conditional-color: #F9F9FA;
--newtab-text-primary-color: #F9F9FA;
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
--newtab-textbox-background-color: #38383D;
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
--newtab-textbox-focus-color: #45A1FF;
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
--newtab-contextmenu-background-color: #4A4A4F;
--newtab-contextmenu-button-color: #2A2A2E;
--newtab-modal-color: #2A2A2E;
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-active-contextmenu-color: #FFF;
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
--newtab-search-dropdown-color: #38383D;
--newtab-search-dropdown-header-color: #4A4A4F;
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
--newtab-topsites-background-color: #38383D;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
--newtab-card-active-outline-color: #4A4A4F;
--newtab-card-background-color: #38383D;
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
--newtab-snippets-background-color: #38383D;
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
body[lwt-newtab-brighttext] {
--newtab-background-color: #2A2A2E;
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
--newtab-button-primary-color: #0060DF;
--newtab-button-secondary-color: #38383D;
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
--newtab-link-primary-color: #45A1FF;
--newtab-link-secondary-color: #50BCB6;
--newtab-text-conditional-color: #F9F9FA;
--newtab-text-primary-color: #F9F9FA;
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
--newtab-textbox-background-color: #38383D;
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
--newtab-textbox-focus-color: #45A1FF;
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
--newtab-contextmenu-background-color: #4A4A4F;
--newtab-contextmenu-button-color: #2A2A2E;
--newtab-modal-color: #2A2A2E;
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-active-contextmenu-color: #FFF;
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
--newtab-search-dropdown-color: #38383D;
--newtab-search-dropdown-header-color: #4A4A4F;
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
--newtab-topsites-background-color: #38383D;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
--newtab-card-active-outline-color: #4A4A4F;
--newtab-card-background-color: #38383D;
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
--newtab-snippets-background-color: #38383D;
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
.icon {
background-position: center center;
@ -338,8 +337,6 @@ main {
margin: auto;
padding-bottom: 68px;
width: 274px; }
.hide-main main {
visibility: hidden; }
@media (min-width: 482px) {
main {
width: 402px; } }
@ -355,6 +352,8 @@ main {
main section {
margin-bottom: 20px;
position: relative; }
.hide-main main {
visibility: hidden; }
.base-content-fallback {
height: 100vh; }
@ -1478,8 +1477,9 @@ a.firstrun-link {
.compact-cards .card-outer .card-text:not(.no-description) .card-title {
font-size: 12px;
line-height: 13px;
max-height: 13px;
max-height: 17px;
overflow: hidden;
padding: 0 0 4px;
text-overflow: ellipsis;
white-space: nowrap; }
.compact-cards .card-outer .card-description {

File diff suppressed because one or more lines are too long

View File

@ -65,48 +65,47 @@ body {
--newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
--newtab-snippets-background-color: #FFF;
--newtab-snippets-hairline-color: transparent; }
body[lwt-newtab-brighttext] {
--newtab-background-color: #2A2A2E;
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
--newtab-button-primary-color: #0060DF;
--newtab-button-secondary-color: #38383D;
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
--newtab-link-primary-color: #45A1FF;
--newtab-link-secondary-color: #50BCB6;
--newtab-text-conditional-color: #F9F9FA;
--newtab-text-primary-color: #F9F9FA;
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
--newtab-textbox-background-color: #38383D;
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
--newtab-textbox-focus-color: #45A1FF;
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
--newtab-contextmenu-background-color: #4A4A4F;
--newtab-contextmenu-button-color: #2A2A2E;
--newtab-modal-color: #2A2A2E;
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-active-contextmenu-color: #FFF;
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
--newtab-search-dropdown-color: #38383D;
--newtab-search-dropdown-header-color: #4A4A4F;
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
--newtab-topsites-background-color: #38383D;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
--newtab-card-active-outline-color: #4A4A4F;
--newtab-card-background-color: #38383D;
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
--newtab-snippets-background-color: #38383D;
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
body[lwt-newtab-brighttext] {
--newtab-background-color: #2A2A2E;
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
--newtab-button-primary-color: #0060DF;
--newtab-button-secondary-color: #38383D;
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
--newtab-link-primary-color: #45A1FF;
--newtab-link-secondary-color: #50BCB6;
--newtab-text-conditional-color: #F9F9FA;
--newtab-text-primary-color: #F9F9FA;
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
--newtab-textbox-background-color: #38383D;
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
--newtab-textbox-focus-color: #45A1FF;
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
--newtab-contextmenu-background-color: #4A4A4F;
--newtab-contextmenu-button-color: #2A2A2E;
--newtab-modal-color: #2A2A2E;
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
--newtab-section-active-contextmenu-color: #FFF;
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
--newtab-search-dropdown-color: #38383D;
--newtab-search-dropdown-header-color: #4A4A4F;
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
--newtab-topsites-background-color: #38383D;
--newtab-topsites-icon-shadow: none;
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
--newtab-card-active-outline-color: #4A4A4F;
--newtab-card-background-color: #38383D;
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
--newtab-snippets-background-color: #38383D;
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
.icon {
background-position: center center;
@ -335,8 +334,6 @@ main {
margin: auto;
padding-bottom: 68px;
width: 274px; }
.hide-main main {
visibility: hidden; }
@media (min-width: 482px) {
main {
width: 402px; } }
@ -352,6 +349,8 @@ main {
main section {
margin-bottom: 20px;
position: relative; }
.hide-main main {
visibility: hidden; }
.base-content-fallback {
height: 100vh; }
@ -1475,8 +1474,9 @@ a.firstrun-link {
.compact-cards .card-outer .card-text:not(.no-description) .card-title {
font-size: 12px;
line-height: 13px;
max-height: 13px;
max-height: 17px;
overflow: hidden;
padding: 0 0 4px;
text-overflow: ellipsis;
white-space: nowrap; }
.compact-cards .card-outer .card-description {

File diff suppressed because one or more lines are too long

View File

@ -1078,6 +1078,7 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
}
sendImpression(extraProps) {
ASRouterUtils.sendMessage({ type: "IMPRESSION", data: this.state.message });
this.sendUserActionTelemetry(Object.assign({ event: "IMPRESSION" }, extraProps));
}
@ -1749,6 +1750,7 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.blockList.includes(msg.id);
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
let itemClassName = "message-item";
if (isCurrent) {
@ -1767,7 +1769,9 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"span",
null,
msg.id
msg.id,
" ",
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null)
)
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
@ -1782,7 +1786,11 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
"button",
{ className: "button", onClick: this.handleOverride(msg.id) },
"Show"
)
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null),
"(",
impressions,
" impressions)"
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"td",

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack>
<em:version>2018.07.18.1179-6b6a3463</em:version>
<em:version>2018.07.24.1260-3e33e3e1</em:version>
<em:name>Activity Stream</em:name>
<em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
<em:multiprocessCompatible>true</em:multiprocessCompatible>

View File

@ -127,6 +127,7 @@ class _ASRouter {
lastMessageId: null,
providers: [],
blockList: [],
impressions: {},
messages: [],
...initialState
};
@ -211,6 +212,7 @@ class _ASRouter {
}
}
await this.setState(newState);
await this.cleanupImpressions();
}
}
@ -226,11 +228,13 @@ class _ASRouter {
this.messageChannel = channel;
this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
this._addASRouterPrefListener();
await this.loadMessagesFromAllProviders();
this._storage = storage;
const blockList = await this._storage.get("blockList") || [];
await this.setState({blockList});
const impressions = await this._storage.get("impressions") || {};
await this.setState({blockList, impressions});
await this.loadMessagesFromAllProviders();
// sets .initialized to true and resolves .waitForInitialized promise
this._finishInitializing();
}
@ -264,18 +268,18 @@ class _ASRouter {
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: state});
}
async _findMessage(msgs, target, data = {}) {
async _findMessage(messages, target, data = {}) {
let message;
let {trigger} = data;
const {trigger} = data;
const {impressions} = this.state;
if (trigger) {
// Find a message that matches the targeting context as well as the trigger context
message = await ASRouterTargeting.findMatchingMessageWithTrigger(msgs, target, trigger);
message = await ASRouterTargeting.findMatchingMessageWithTrigger({messages, impressions, target, trigger});
}
if (!message) {
// If there was no messages with this trigger, try finding a regular targeted message
message = await ASRouterTargeting.findMatchingMessage(msgs, target);
message = await ASRouterTargeting.findMatchingMessage({messages, impressions, target});
}
return message;
}
@ -343,6 +347,75 @@ class _ASRouter {
}
}
async addImpression(message) {
// Don't store impressions for messages that don't include any limits on frequency
if (!message.frequency) {
return;
}
await this.setState(state => {
// The destructuring here is to avoid mutating existing objects in state as in redux
// (see https://redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)
const impressions = {...state.impressions};
impressions[message.id] = impressions[message.id] ? [...impressions[message.id]] : [];
impressions[message.id].push(Date.now());
this._storage.set("impressions", impressions);
return {impressions};
});
}
/**
* getLongestPeriod
*
* @param {obj} message An ASRouter message
* @returns {int|null} if the message has custom frequency caps, the longest period found in the list of caps.
if the message has no custom frequency caps, null
* @memberof _ASRouter
*/
getLongestPeriod(message) {
if (!message.frequency || !message.frequency.custom) {
return null;
}
return message.frequency.custom.sort((a, b) => b.period - a.period)[0].period;
}
/**
* cleanupImpressions - this function cleans up obsolete impressions whenever
* messages are refreshed or fetched. It will likely need to be more sophisticated in the future,
* but the current behaviour for when impressions are cleared is as follows:
*
* 1. If the message id for a list of impressions no longer exists in state.messages, it will be cleared.
* 2. If the message has time-bound frequency caps but no lifetime cap, any impressions older
* than the longest time period will be cleared.
*/
async cleanupImpressions() {
await this.setState(state => {
const impressions = {...state.impressions};
let needsUpdate = false;
Object.keys(impressions).forEach(id => {
const [message] = state.messages.filter(msg => msg.id === id);
// Don't keep impressions for messages that no longer exist
if (!message || !message.frequency || !Array.isArray(impressions[id])) {
delete impressions[id];
needsUpdate = true;
return;
}
if (!impressions[id].length) {
return;
}
// If we don't want to store impressions older than the longest period
if (message.frequency.custom && !message.frequency.lifetime) {
const now = Date.now();
impressions[id] = impressions[id].filter(t => (now - t) < this.getLongestPeriod(message));
needsUpdate = true;
}
});
if (needsUpdate) {
this._storage.set("impressions", impressions);
}
return {impressions};
});
}
async sendNextMessage(target, action = {}) {
let {data} = action;
const msgs = this._getUnblockedMessages();
@ -354,8 +427,8 @@ class _ASRouter {
} else {
message = await this._findMessage(msgs, target, data);
}
await this.setState({lastMessageId: message ? message.id : null});
await this.setState({lastMessageId: message ? message.id : null});
await this._sendMessageToTarget(message, target, data);
}
@ -368,10 +441,14 @@ class _ASRouter {
async blockById(idOrIds) {
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
await this.setState(state => {
const blockList = [...state.blockList, ...idsToBlock];
// When a message is blocked, its impressions should be cleared as well
const impressions = {...state.impressions};
idsToBlock.forEach(id => delete impressions[id]);
this._storage.set("blockList", blockList);
return {blockList};
return {blockList, impressions};
});
}
@ -473,6 +550,9 @@ class _ASRouter {
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: this.state});
}
break;
case "IMPRESSION":
this.addImpression(action.data);
break;
}
}
}

View File

@ -2,14 +2,46 @@ ChromeUtils.import("resource://gre/modules/components-utils/FilterExpressions.js
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
ChromeUtils.defineModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm");
ChromeUtils.import("resource://gre/modules/Console.jsm");
ChromeUtils.defineModuleGetter(this, "ShellService",
"resource:///modules/ShellService.jsm");
const FXA_USERNAME_PREF = "services.sync.username";
const ONBOARDING_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouterOnboardingCohort";
// Max possible cap for any message
const MAX_LIFETIME_CAP = 100;
const ONE_DAY = 24 * 60 * 60 * 1000;
const {activityStreamProvider: asProvider} = NewTabUtils;
const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
const FRECENT_SITES_IGNORE_BLOCKED = true;
const FRECENT_SITES_NUM_ITEMS = 50;
const FRECENT_SITES_MIN_FRECENCY = 100;
const TopFrecentSitesCache = {
_lastUpdated: 0,
_topFrecentSites: null,
get topFrecentSites() {
return new Promise(async resolve => {
const now = Date.now();
if (now - this._lastUpdated >= FRECENT_SITES_UPDATE_INTERVAL) {
this._topFrecentSites = await asProvider.getTopFrecentSites({
ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
numItems: FRECENT_SITES_NUM_ITEMS,
topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
onePerDomain: true,
includeFavicon: false
});
this._lastUpdated = now;
}
resolve(this._topFrecentSites);
});
}
};
/**
* removeRandomItemFromArray - Removes a random item from the array and returns it.
@ -84,6 +116,17 @@ const TargetingGetters = {
return Services.prefs.getIntPref("devtools.selfxss.count");
},
get topFrecentSites() {
return TopFrecentSitesCache.topFrecentSites.then(sites => sites.map(site => (
{
url: site.url,
host: (new URL(site.url)).hostname,
frecency: site.frecency,
lastVisitDate: site.lastVisitDate
}
)));
},
// Temporary targeting function for the purposes of running the simplified onboarding experience
get isInExperimentCohort() {
return Services.prefs.getIntPref(ONBOARDING_EXPERIMENT_PREF, 0);
@ -97,6 +140,35 @@ this.ASRouterTargeting = {
return FilterExpressions.eval(filterExpression, context);
},
isBelowFrequencyCap(message, impressionsForMessage) {
if (!message.frequency || !impressionsForMessage || !impressionsForMessage.length) {
return true;
}
if (
message.frequency.lifetime &&
impressionsForMessage.length >= Math.min(message.frequency.lifetime, MAX_LIFETIME_CAP)
) {
return false;
}
if (message.frequency.custom) {
const now = Date.now();
for (const setting of message.frequency.custom) {
let {period} = setting;
if (period === "daily") {
period = ONE_DAY;
}
const impressionsInPeriod = impressionsForMessage.filter(t => (now - t) < period);
if (impressionsInPeriod.length >= setting.cap) {
return false;
}
}
}
return true;
},
/**
* findMatchingMessage - Given an array of messages, returns one message
* whos targeting expression evaluates to true
@ -105,26 +177,37 @@ this.ASRouterTargeting = {
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
* @returns {obj} an AS router message
*/
async findMatchingMessage(messages, target, context) {
async findMatchingMessage({messages, impressions = {}, target, context}) {
const arrayOfItems = [...messages];
let match;
let candidate;
while (!match && arrayOfItems.length) {
candidate = removeRandomItemFromArray(arrayOfItems);
if (candidate && !candidate.trigger && (!candidate.targeting || await this.isMatch(candidate.targeting, target, context))) {
if (
candidate &&
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
!candidate.trigger &&
(!candidate.targeting || await this.isMatch(candidate.targeting, target, context))
) {
match = candidate;
}
}
return match;
},
async findMatchingMessageWithTrigger(messages, target, trigger, context) {
async findMatchingMessageWithTrigger({messages, impressions = {}, target, trigger, context}) {
const arrayOfItems = [...messages];
let match;
let candidate;
while (!match && arrayOfItems.length) {
candidate = removeRandomItemFromArray(arrayOfItems);
if (candidate && candidate.trigger === trigger && (!candidate.targeting || await this.isMatch(candidate.targeting, target, context))) {
if (
candidate &&
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
candidate.trigger === trigger &&
(!candidate.targeting || await this.isMatch(candidate.targeting, target, context))
) {
match = candidate;
}
}

View File

@ -149,10 +149,6 @@ const PREFS_CONFIG = new Map([
title: "Number of rows of Top Stories to display",
value: 1
}],
["tippyTop.service.endpoint", {
title: "Tippy Top service manifest url",
value: "https://activity-stream-icons.services.mozilla.com/v1/icons.json.br"
}],
["sectionOrder", {
title: "The rendering order for the sections",
value: "topsites,topstories,highlights"

View File

@ -5,11 +5,9 @@
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
const {actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm", {});
const {getDomain} = ChromeUtils.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
@ -18,10 +16,6 @@ ChromeUtils.defineModuleGetter(this, "Services",
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
const FIVE_MINUTES = 5 * 60 * 1000;
const ONE_DAY = 24 * 60 * 60 * 1000;
const TIPPYTOP_UPDATE_TIME = ONE_DAY;
const TIPPYTOP_RETRY_DELAY = FIVE_MINUTES;
const MIN_FAVICON_SIZE = 96;
/**
@ -120,97 +114,9 @@ async function fetchIconFromRedirects(url) {
this.FaviconFeed = class FaviconFeed {
constructor() {
this.tippyTopNextUpdate = 0;
this.cache = new PersistentCache("tippytop", true);
this._sitesByDomain = null;
this.numRetries = 0;
this._queryForRedirects = new Set();
}
get endpoint() {
return this.store.getState().Prefs.values["tippyTop.service.endpoint"];
}
async loadCachedData() {
const data = await this.cache.get("sites");
if (data && "_timestamp" in data) {
this._sitesByDomain = data;
this.tippyTopNextUpdate = data._timestamp + TIPPYTOP_UPDATE_TIME;
}
}
async maybeRefresh() {
if (Date.now() >= this.tippyTopNextUpdate) {
await this.refresh();
}
}
async refresh() {
let headers = new Headers();
if (this._sitesByDomain && this._sitesByDomain._etag) {
headers.set("If-None-Match", this._sitesByDomain._etag);
}
let {data, etag, status} = await this.loadFromURL(this.endpoint, headers);
let failedUpdate = false;
if (status === 200) {
this._sitesByDomain = this._sitesArrayToObjectByDomain(data);
this._sitesByDomain._etag = etag;
} else if (status !== 304) {
failedUpdate = true;
}
let delay = TIPPYTOP_UPDATE_TIME;
if (failedUpdate) {
delay = Math.min(TIPPYTOP_UPDATE_TIME, TIPPYTOP_RETRY_DELAY * Math.pow(2, this.numRetries++));
} else {
this._sitesByDomain._timestamp = Date.now();
this.cache.set("sites", this._sitesByDomain);
this.numRetries = 0;
}
this.tippyTopNextUpdate = Date.now() + delay;
}
async loadFromURL(url, headers) {
let data = [];
let etag;
let status;
try {
let response = await fetch(url, {headers});
status = response.status;
if (status === 200) {
data = await response.json();
etag = response.headers.get("ETag");
}
} catch (error) {
Cu.reportError(`Failed to load tippy top manifest from ${url}`);
}
return {data, etag, status};
}
_sitesArrayToObjectByDomain(sites) {
let sitesByDomain = {};
for (const site of sites) {
// The tippy top manifest can have a url property (string) or a
// urls property (array of strings)
for (const domain of site.domains || []) {
sitesByDomain[domain] = {image_url: site.image_url};
}
}
return sitesByDomain;
}
getSitesByDomain() {
// return an already loaded object or a promise for that object
return this._sitesByDomain || (this._sitesByDomain = new Promise(async resolve => {
await this.loadCachedData();
await this.maybeRefresh();
if (this._sitesByDomain instanceof Promise) {
// If _sitesByDomain is still a Promise, no data was loaded from cache or fetch.
this._sitesByDomain = {};
}
resolve(this._sitesByDomain);
}));
}
/**
* fetchIcon attempts to fetch a rich icon for the given url from two sources.
* First, it looks up the tippy top feed, if it's still missing, then it queries
@ -223,44 +129,55 @@ this.FaviconFeed = class FaviconFeed {
return;
}
const sitesByDomain = await this.getSitesByDomain();
const domain = getDomain(url);
if (domain in sitesByDomain) {
let iconUri = Services.io.newURI(sitesByDomain[domain].image_url);
// The #tippytop is to be able to identify them for telemetry.
iconUri = iconUri.mutate().setRef("tippytop").finalize();
PlacesUtils.favicons.setAndFetchFaviconForPage(
Services.io.newURI(url),
iconUri,
false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
null,
Services.scriptSecurityManager.getSystemPrincipal()
);
const site = await this.getSite(getDomain(url));
if (!site) {
if (!this._queryForRedirects.has(url)) {
this._queryForRedirects.add(url);
Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url));
}
return;
}
if (!this._queryForRedirects.has(url)) {
this._queryForRedirects.add(url);
Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url));
let iconUri = Services.io.newURI(site.image_url);
// The #tippytop is to be able to identify them for telemetry.
iconUri = iconUri.mutate().setRef("tippytop").finalize();
PlacesUtils.favicons.setAndFetchFaviconForPage(
Services.io.newURI(url),
iconUri,
false,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
null,
Services.scriptSecurityManager.getSystemPrincipal()
);
}
/**
* Get the site tippy top data from Remote Settings.
*/
async getSite(domain) {
const sites = await this.tippyTop.get({filters: {domain}});
return sites.length ? sites[0] : null;
}
/**
* Get the tippy top collection from Remote Settings.
*/
get tippyTop() {
if (!this._tippyTop) {
this._tippyTop = RemoteSettings("tippytop");
}
return this._tippyTop;
}
/**
* Determine if we should be fetching and saving icons.
*/
get shouldFetchIcons() {
return this.endpoint && Services.prefs.getBoolPref("browser.chrome.site_icons");
return Services.prefs.getBoolPref("browser.chrome.site_icons");
}
onAction(action) {
switch (action.type) {
case at.SYSTEM_TICK:
if (this._sitesByDomain) {
// No need to refresh if we haven't been initialized.
this.maybeRefresh();
}
break;
case at.RICH_ICON_MISSING:
this.fetchIcon(action.data.url);
break;

View File

@ -171,6 +171,7 @@ section_menu_action_remove_section=Abschnitt entfernen
section_menu_action_collapse_section=Abschnitt einklappen
section_menu_action_expand_section=Abschnitt ausklappen
section_menu_action_manage_section=Abschnitt verwalten
section_menu_action_manage_webext=Erweiterung verwalten
section_menu_action_add_topsite=Wichtige Seite hinzufügen
section_menu_action_move_up=Nach oben schieben
section_menu_action_move_down=Nach unten schieben
@ -178,13 +179,25 @@ section_menu_action_privacy_notice=Datenschutzhinweis
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=Firefox für unterwegs
firstrun_content=Nehmen Sie Ihre Lesezeichen, Chronik, Passwörter und andere Einstellungen auf allen Geräten mit.
firstrun_learn_more_link=Jetzt mehr über Firefox Konten erfahren
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=E-Mail-Adresse eingeben
firstrun_form_sub_header=um sich bei Firefox Sync anzumelden.
firstrun_email_input_placeholder=E-Mail
firstrun_invalid_input=Gültige E-Mail-Adresse erforderlich
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Indem Sie fortfahren, stimmen Sie den {terms} und dem {privacy} zu.
firstrun_terms_of_service=Nutzungsbedingungen
firstrun_privacy_notice=Datenschutzhinweis
firstrun_continue_to_login=Weiter
firstrun_skip_login=Diesen Schritt überspringen

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=para acceder a Firefox Sync.
firstrun_email_input_placeholder=Correo electrónico
firstrun_invalid_input=Se requiere un correo válido
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Al proceder, aceptas los {terms} y la {privacy}.

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=برای فعال کردن همگام‌سازی فای
firstrun_email_input_placeholder=پست‌الکترونیکی
firstrun_invalid_input=رایانامهٔ معتبر لازم است
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=با ادامه دادن، شما {terms} و {privacy} قبول می‌کنید.

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=jatkaaksesi Firefox Sync -palveluun.
firstrun_email_input_placeholder=Sähköposti
firstrun_invalid_input=Sähköpostiosoitteen täytyy olla kelvollinen
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Jatkamalla hyväksyt {terms} ja {privacy}.

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=pour continuer avec Firefox Sync.
firstrun_email_input_placeholder=Adresse électronique
firstrun_invalid_input=Adresse électronique valide requise
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=En continuant, vous acceptez les {terms} et la {privacy}.

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=Firefox સમન્વયન ચાલુ રાખવ
firstrun_email_input_placeholder=ઇમેઇલ
firstrun_invalid_input=માન્ય ઇમેઇલ આવશ્યક છે
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=આગળ વધીને, તમે {terms} અને {privacy} સાથે સંમત થાઓ છો.

View File

@ -1,7 +1,7 @@
newtab_page_title=नया टैब
header_top_sites=सर्वोच्च साइटें
header_highlights=झलकिया
header_highlights=प्रमुखताए
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
# of the corresponding content provider.
header_recommended_by={provider} द्वारा अनुशंसित
@ -107,7 +107,7 @@ prefs_highlights_options_pocket_label=पॉकेट में सहेजे
prefs_snippets_description=Mozilla और Firefox से अद्यतन
settings_pane_button_label=अपने नए टैब पृष्ठ को अनुकूलित करें
settings_pane_topsites_header=सर्वोच्च साइटें
settings_pane_highlights_header=झलकिया
settings_pane_highlights_header=प्रमुखताए
settings_pane_highlights_options_bookmarks=पुस्तचिह्न
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
# traditionally on about:home. Alternative translation options: "Small Note" or

View File

@ -144,7 +144,7 @@ pocket_read_more=პოპულარული თემები:
# end of the list of popular topic links.
pocket_read_even_more=მეტი სიახლის ნახვა
highlights_empty_state=დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენი რჩეული სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.
highlights_empty_state=დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენთვის სასურველი სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
# in the space that would have shown a few stories, this is shown instead.
# {provider} is replaced by the name of the content provider for this section.

View File

@ -1,33 +1,24 @@
newtab_page_title=ಹೊಸ ಹಾಳೆ
default_label_loading=ಲೋಡ್ ಆಗುತ್ತಿದೆ…
header_top_sites=ಪ್ರಮುಖ ತಾಣಗಳು
header_stories=ಪ್ರಮುಖ ಸುದ್ದಿಗಳು
header_highlights=ಮುಖ್ಯಾಂಶಗಳು
header_visit_again=ಮತ್ತೆ ಭೇಟಿಕೊಡು
header_bookmarks=ಇತ್ತೀಚಿಗೆ ಮಾಡಲಾದ ಬುಕ್‌ಮಾರ್ಕುಗಳು
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
# of the corresponding content provider.
header_recommended_by={provider} ರಿಂದ ಶಿಫಾರಸುಮಾಡುಲಾಗಿದೆ
# LOCALIZATION NOTE(header_bookmarks_placeholder): This message is
# meant to inform that section contains no information because
# the user hasn't added any bookmarks.
header_bookmarks_placeholder=ನಿಮ್ಮ ಹತ್ತಿರ ಇನ್ನೂ ಯಾವುದೇ ಪುಟಗುರುತುಗಳಿಲ್ಲ.
# LOCALIZATION NOTE(header_stories_from): This is followed by a logo of the
# corresponding content (stories) provider
header_stories_from=ಯಿಂದ
# LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
# the context menu button is focused/active. Title is the label or hostname of
# the site.
# LOCALIZATION NOTE(section_context_menu_button_sr): This is for screen readers when
# the section edit context menu button is focused/active.
# LOCALIZATION NOTE (type_label_*): These labels are associated to pages to give
# context on how the element is related to the user, e.g. type indicates that
# the page is bookmarked, or is currently open on another device
type_label_visited=ಭೇಟಿ ನೀಡಲಾದ‍
type_label_bookmarked=ಪುಟಗುರುತು ಮಾಡಲಾದ
type_label_synced=ಮತ್ತೊಂದು ಸಾಧನದಿಂದ ಸಿಂಕ್ ಮಾಡಲಾಗಿದೆ
type_label_recommended=ಪ್ರಚಲಿತ
# LOCALIZATION NOTE(type_label_open): Open is an adjective, as in "page is open"
type_label_open=ತೆರೆ
type_label_topic=ವಿಷಯ
type_label_now=ಈಗ
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
# menu and are meant as a call to action for a given page.
@ -35,8 +26,6 @@ type_label_now=ಈಗ
# bookmarks"
menu_action_bookmark=ಪುಟ ಗುರುತು
menu_action_remove_bookmark=ಪುಟ ಗುರುತು ತೆಗೆ
menu_action_copy_address=ವಿಳಾಸವನ್ನು ನಕಲಿಸು
menu_action_email_link=ಇಮೈಲ್ ಕೊಂಡಿ…
menu_action_open_new_window=ಹೊಸ ಕಿಟಕಿಯಲ್ಲಿ ತೆರೆ
menu_action_open_private_window=ಹೊಸ ಖಾಸಗಿ ಕಿಟಕಿಯಲ್ಲಿ ತೆರೆ
menu_action_dismiss=ವಜಾಗೊಳಿಸು‍
@ -49,11 +38,19 @@ menu_action_unpin=ಅನ್‌ಪಿನ್
confirm_history_delete_notice_p2=ಈ ಕಾರ್ಯವನ್ನು ರದ್ದುಗೊಳಿಸಲು ಸಾಧ್ಯವಿರುವುದಿಲ್ಲ.
menu_action_save_to_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 ' +
# search_term + 'with:' becomes 'Search for abc with:'
# The search engine name is displayed as an icon and does not need a translation
search_for_something_with={search_term} ಅನ್ನು ಇದರಿಂದ ಹುಡುಕಿ:
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
# found in the context menu of an item that has been downloaded. The intention behind
# "this action" is that it will show where the downloaded file exists on the file system
# for each operating system.
menu_action_show_file_default=ಕಡತ ತೋರಿಸು
menu_action_open_file=ಕಡತವನ್ನು ತೆರೆ
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
# link that belongs to this downloaded item"
menu_action_copy_download_link=ಡೌನ್ಲೋಡ್ ಕೊಂಡಿಯನ್ನು ಪ್ರತಿ ಮಾಡು
menu_action_go_to_download_page=ಡೌನ್ಲೋಡ್ ಪುಟಕ್ಕೆ ತೆರಳು
menu_action_remove_download=ಇತಿಹಾಸದಿಂದ ತೆಗೆದುಹಾಕು
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
@ -67,63 +64,46 @@ search_header={search_engine_name} ನಿಂದ ಹುಡುಕಿ
# LOCALIZATION NOTE (search_web_placeholder): This is shown in the searchbox when
# the user hasn't typed anything yet.
search_web_placeholder=ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ
search_settings=ಹುಡುಕು ಸಿದ್ಧತೆಗಳನ್ನು ಬದಲಾಯಿಸು
# LOCALIZATION NOTE (section_info_option): This is the screenreader text for the
# (?) icon that would show a section's description with optional feedback link.
section_info_option=ಮಾಹಿತಿ
section_info_send_feedback=ಅಭಿಪ್ರಾಯವನ್ನು ಕಳುಹಿಸಿ
section_info_privacy_notice=ಗೌಪ್ಯತಾ ಸೂಚನೆ
# LOCALIZATION NOTE (section_disclaimer_topstories): This is shown below
# the topstories section title to provide additional information about
# how the stories are selected.
# LOCALIZATION NOTE (section_disclaimer_topstories_buttontext): The text of
# the button used to acknowledge, and hide this disclaimer in the future.
# LOCALIZATION NOTE (welcome_*): This is shown as a modal dialog, typically on a
# first-run experience when there's no data to display yet
welcome_title=ಹೊಸ ಹಾಳೆಗೆ ಸುಸ್ವಾಗತ
# LOCALIZATION NOTE (time_label_*): {number} is a placeholder for a number which
# represents a shortened timestamp format, e.g. '10m' means '10 minutes ago'.
time_label_less_than_minute=<1ನಿ
time_label_minute={number}ನಿ
time_label_hour={number}ಗ
time_label_day={number}ದಿ
# LOCALIZATION NOTE (settings_pane_*): This is shown in the Settings Pane sidebar.
# LOCALIZATION NOTE (prefs_*, settings_*): These are shown in about:preferences
# for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
# in English, while "Home" should be localized matching the about:preferences
# sidebar mozilla-central string for the panel that has preferences related to
# what is shown for the homepage, new windows, and new tabs.
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
prefs_search_header=ಜಾಲದ ಹುಡುಕಾಟ
settings_pane_button_label=ಹೊಸ ಹಾಳೆಯ ಪುಟವನ್ನು ಅಗತ್ಯಾನುಗುಣಗೊಳಿಸಿ
settings_pane_header=ಹೊಸ ಹಾಳೆಯ ಆದ್ಯತೆಗಳು
settings_pane_body2=ನೀವು ಈ ಪುಟದಲ್ಲಿ ಏನು ನೋಡಿತ್ತೀರೆಂದು ಆಯ್ಕೆಮಾಡಿ.
settings_pane_search_header=ಹುಡುಕು
settings_pane_search_body=ಹೊಸ ಹಾಳೆಯಿಂದ ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ.
settings_pane_topsites_header=ಪ್ರಮುಖ ತಾಣಗಳು
settings_pane_topsites_body=ನೀವು ಅತಿ ಹೆಚ್ಚು ನೋಡುವ ಜಾಲತಾಣಗಳಿಗೆ ಪ್ರವೇಶದ್ವಾರ.
settings_pane_topsites_options_showmore=ಎರಡು ಸಾಲುಗಳನ್ನು ಪ್ರದರ್ಶಿಸು
settings_pane_bookmarks_header=ಇತ್ತೀಚಿನ ಪುಟಗುರುತುಗಳು
settings_pane_visit_again_header=ಮತ್ತೆ ಭೇಟಿಕೊಡು
settings_pane_highlights_header=ಮುಖ್ಯಾಂಶಗಳು
settings_pane_highlights_options_bookmarks=ಪುಟಗುರುತುಗಳು
settings_pane_highlights_options_visited=ಭೇಟಿ ನೀಡಿದ ತಾಣಗಳು
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
# traditionally on about:home. Alternative translation options: "Small Note" or
# something that expresses the idea of "a small message, shortened from
# something else, and non-essential but also not entirely trivial and useless."
settings_pane_snippets_header=ಉಲ್ಲೇಖಗಳು
settings_pane_done_button=ಆಯಿತು
# LOCALIZATION NOTE (edit_topsites_*): This is shown in the Edit Top Sites modal
# dialog.
edit_topsites_button_text=‍ತಿದ್ದು
edit_topsites_showmore_button=‍ಹೆಚ್ಚು ತೋರಿಸು
edit_topsites_showless_button=ಕೆಲವೊಂದು ತೋರಿಸಿ
edit_topsites_done_button=ಆಯಿತು
edit_topsites_pin_button=ಈ ತಾಣವನ್ನು ಪಿನ್ ಮಾಡು
edit_topsites_unpin_button=ಈ ತಾಣವನ್ನು ಹೊರತೆಗೆ
edit_topsites_edit_button=ಈ ತಾಣವನ್ನು ಸಂಪಾದಿಸು
edit_topsites_dismiss_button=ಈ ತಾಣವನ್ನು ತೆಗೆದುಹಾಕು
edit_topsites_add_button=ಸೇರಿಸು
# LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
topsites_form_add_header=ಹೊಸ ಅಗ್ರ ತಾಣಗಳು
topsites_form_edit_header=ಅಗ್ರ ತಾಣಗಳನ್ನು ಸಂಪಾದಿಸಿ
topsites_form_title_label=ಶೀರ್ಷಿಕೆ
topsites_form_title_placeholder=ಶೀರ್ಷಿಕೆಯನ್ನು ನಮೂದಿಸಿ
topsites_form_url_label=URL
topsites_form_url_placeholder=ಒಂದು URL ಅನ್ನು ಟೈಪಿಸಿ ಅಥವಾ ನಕಲಿಸಿ
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
topsites_form_preview_button=ಮುನ್ನೋಟ
topsites_form_add_button=ಸೇರಿಸು
topsites_form_save_button=ಉಳಿಸು
topsites_form_cancel_button=ರದ್ದು ಮಾಡು
@ -135,10 +115,6 @@ pocket_read_more=ಜನಪ್ರಿಯವಾದ ವಿಷಯಗಳು:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
pocket_read_even_more=ಹೆಚ್ಚು ಕತೆಗಳನ್ನು ನೋಡಿರಿ
# LOCALIZATION NOTE (pocket_feedback_header): This is shown as an introduction
# to Pocket as part of the feedback form.
# LOCALIZATION NOTE (pocket_description): This is shown in the settings pane and
# below (pocket_feedback_header) to provide more information about Pocket.
highlights_empty_state=ವೀಕ್ಷಣೆ ಮಾಡಲು ಶುರುಮಾಡಿ, ಮತ್ತು ನಾವು ಇತ್ತೀಚೆಗೆ ಭೇಟಿ ನೀಡಿದ ಅಥವಾ ಬುಕ್‌ಮಾರ್ಕ್ ಮಾಡಲಾದ ಕೆಲವು ಶ್ರೇಷ್ಠ ಲೇಖನಗಳು, ವೀಡಿಯೊಗಳು ಮತ್ತು ಇತರ ಪುಟಗಳನ್ನು ನಾವು ತೋರಿಸುತ್ತೇವೆ.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
@ -153,3 +129,29 @@ manual_migration_cancel_button=ಪರವಾಗಿಲ್ಲ
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
# of importing another browsers profile profile into Firefox.
manual_migration_import_button=ಈಗ ಆಮದು ಮಾಡು
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
# action link are shown in each section of UI that fails to render
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_move_up=ಮೇಲೆ ಜರುಗಿಸು
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_email_input_placeholder=ಇಮೇಲ್
firstrun_invalid_input=ಸರಿಯಾದ ಇಮೇಲ್ ಬೇಕಾಗಿದೆ
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_terms_of_service=ಸೇವೆಯ ನಿಯಮಗಳು
firstrun_privacy_notice=ಗೌಪ್ಯತಾ ಸೂಚನೆ
firstrun_continue_to_login=ಮುಂದುವರೆ
firstrun_skip_login=ಈ ಹಂತವನ್ನು ಹಾರಿಸಿ

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=norėdami tęsti su „Firefox Sync“.
firstrun_email_input_placeholder=El. paštas
firstrun_invalid_input=Reikalingas galiojantis el. pašto adresas
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Tęsdami sutinkate su {terms} ir {privacy}.

View File

@ -1,22 +1,28 @@
newtab_page_title=नवीन टॅब
default_label_loading=दाखल करीत आहे…
header_top_sites=खास साईट्स
header_highlights=ठळक
header_stories=महत्वाच्या गोष्टी
# LOCALIZATION NOTE(header_stories_from): This is followed by a logo of the
# corresponding content (stories) provider
header_stories_from=कडून
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
# of the corresponding content provider.
header_recommended_by={provider} तर्फे शिफारस
# LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
# the context menu button is focused/active. Title is the label or hostname of
# the site.
context_menu_button_sr={title} साठी संदर्भ मेनू उघडा
# LOCALIZATION NOTE(section_context_menu_button_sr): This is for screen readers when
# the section edit context menu button is focused/active.
section_context_menu_button_sr=विभाग संदर्भ मेनू उघडा
# LOCALIZATION NOTE (type_label_*): These labels are associated to pages to give
# context on how the element is related to the user, e.g. type indicates that
# the page is bookmarked, or is currently open on another device
type_label_visited=भेट दिलेले
type_label_bookmarked=वाचनखुण लावले
type_label_synced=इतर साधनावरुन ताळमेळ केले
# LOCALIZATION NOTE(type_label_open): Open is an adjective, as in "page is open"
type_label_open=उघडा
type_label_topic=विषय
type_label_recommended=प्रचलित
type_label_pocket=Pocket मध्ये जतन झाले
type_label_downloaded=डाउनलोड केलेले
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
# menu and are meant as a call to action for a given page.
@ -24,19 +30,30 @@ type_label_topic=विषय
# bookmarks"
menu_action_bookmark=वाचनखुण
menu_action_remove_bookmark=वाचनखुण काढा
menu_action_copy_address=पत्त्याची प्रत बनवा
menu_action_email_link=दुवा इमेल करा…
menu_action_open_new_window=नवीन पटलात उघडा
menu_action_open_private_window=नवीन खाजगी पटलात उघडा
menu_action_dismiss=रद्द करा
menu_action_delete=इतिहासातून नष्ट करा
menu_action_pin=पिन लावा
menu_action_unpin=पिन काढा
confirm_history_delete_p1=आपल्या इतिहासामधून या पृष्ठातील प्रत्येक उदाहरण खात्रीने हटवू इच्छिता?
# LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
# the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
# page from history.
confirm_history_delete_notice_p2=ही क्रिया पूर्ववत केली जाऊ शकत नाही.
menu_action_save_to_pocket=Pocket मध्ये जतन करा
menu_action_delete_pocket=Pocket मधून हटवा
menu_action_archive_pocket=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 ' +
# search_term + 'with:' becomes 'Search for abc with:'
# The search engine name is displayed as an icon and does not need a translation
search_for_something_with=शोधा {search_term} सोबत:
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
# found in the context menu of an item that has been downloaded. The intention behind
# "this action" is that it will show where the downloaded file exists on the file system
# for each operating system.
menu_action_show_file_mac_os=Finder मध्ये दर्शवा
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
# link that belongs to this downloaded item"
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
@ -50,36 +67,101 @@ search_header={search_engine_name} शोध
# LOCALIZATION NOTE (search_web_placeholder): This is shown in the searchbox when
# the user hasn't typed anything yet.
search_web_placeholder=वेबवर शोधा
search_settings=शोध सेटिंग बदला
# LOCALIZATION NOTE (welcome_*): This is shown as a modal dialog, typically on a
# first-run experience when there's no data to display yet
welcome_title=नवीन टॅबवर स्वागत आहे
# LOCALIZATION NOTE (section_disclaimer_topstories): This is shown below
# the topstories section title to provide additional information about
# how the stories are selected.
section_disclaimer_topstories=आपण जे वाचतो त्यानुसार निवडलेल्या, वेबवरील सर्वात मनोरंजक कथा. Pocket कडून, आता Mozilla चा भाग.
section_disclaimer_topstories_linktext=कसे कार्य करते ते जाणून घ्या.
# LOCALIZATION NOTE (section_disclaimer_topstories_buttontext): The text of
# the button used to acknowledge, and hide this disclaimer in the future.
section_disclaimer_topstories_buttontext=ठीक आहे, समजले
# LOCALIZATION NOTE (time_label_*): {number} is a placeholder for a number which
# represents a shortened timestamp format, e.g. '10m' means '10 minutes ago'.
time_label_less_than_minute=<1मि
time_label_minute={number}मि
time_label_hour={number}ता
time_label_day={number}दि
# LOCALIZATION NOTE (settings_pane_*): This is shown in the Settings Pane sidebar.
# LOCALIZATION NOTE (prefs_*, settings_*): These are shown in about:preferences
# for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
# in English, while "Home" should be localized matching the about:preferences
# sidebar mozilla-central string for the panel that has preferences related to
# what is shown for the homepage, new windows, and new tabs.
prefs_home_header=फायरफॉक्स होम वरील मजकूर
prefs_home_description=आपल्या फायरफॉक्सचा मुख्यपृष्ठवर आपल्याला कोणती माहिती पाहिजे ते निवडा.
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
prefs_search_header=वेब शोध
prefs_topsites_description=आपण सर्वाधिक भेट देता त्या साइट
prefs_topstories_description2=आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री
prefs_topstories_sponsored_learn_more=अधिक जाणून घ्या
prefs_highlights_description=आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा
prefs_snippets_description=Mozilla आणि Firefox कडून अद्यतने
settings_pane_button_label=आपले नवीन टॅब पृष्ठ सानुकूलित करा
settings_pane_header=नवीन टॅब प्राधान्ये
settings_pane_body=नवीन टॅब उघडल्यानंतर काय दिसायला हवे ते निवडा.
settings_pane_search_header=शोध
settings_pane_search_body=आपल्या नवीन टॅब वरून वेबवर शोधा.
settings_pane_topsites_header=शीर्ष साइट्स
settings_pane_highlights_header=ठळक
settings_pane_highlights_options_bookmarks=वाचनखुणा
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
# traditionally on about:home. Alternative translation options: "Small Note" or
# something that expresses the idea of "a small message, shortened from
# something else, and non-essential but also not entirely trivial and useless."
settings_pane_snippets_header=कात्रणे
# LOCALIZATION NOTE (edit_topsites_*): This is shown in the Edit Top Sites modal
# dialog.
edit_topsites_button_text=संपादित करा
edit_topsites_edit_button=ही साइट संपादित करा
# LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
topsites_form_add_header=नवीन खास साइट
topsites_form_edit_header=खास साईट संपादित करा
topsites_form_title_label=शिर्षक
topsites_form_title_placeholder=शिर्षक प्रविष्ट करा
topsites_form_url_placeholder=URL चिकटवा किंवा टाईप करा
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
topsites_form_preview_button=पूर्वावलोकन
topsites_form_add_button=समाविष्ट करा
topsites_form_save_button=जतन करा
topsites_form_cancel_button=रद्द करा
topsites_form_url_validation=वैध URL आवश्यक
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
# trending stories section and precedes a list of links to popular topics.
pocket_read_more=लोकप्रिय विषय:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
# LOCALIZATION NOTE (pocket_feedback_header): This is shown as an introduction
# to Pocket as part of the feedback form.
# LOCALIZATION NOTE (pocket_feedback_body): This is shown below
# (pocket_feedback_header) to provide more information about Pocket.
pocket_read_even_more=अधिक कथा पहा
highlights_empty_state=ब्राउझिंग सुरू करा, आणि आम्ही आपल्याला इथे आपण अलीकडील भेट दिलेले किंवा वाचनखूण लावलेले उत्कृष्ठ लेख, व्हिडिओ, आणि इतर पृष्ठांपैकी काही दाखवू.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
# in the space that would have shown a few stories, this is shown instead.
# {provider} is replaced by the name of the content provider for this section.
topstories_empty_state=तुम्ही सर्व बघितले. {provider} कडून आणखी महत्वाच्या गोष्टी बघण्यासाठी नंतर परत तपासा. प्रतीक्षा करू शकत नाही? वेबवरील छान गोष्टी शोधण्यासाठी लोकप्रिय विषय निवडा.
# LOCALIZATION NOTE (manual_migration_explanation2): This message is shown to encourage users to
# import their browser profile from another browser they might be using.
manual_migration_explanation2=दुसऱ्या ब्राऊझरमधील वाचनखूणा, इतिहास आणि पासवर्ड सोबत Firefox ला वापरून पहा.
# LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the
# process of importing another browsers profile into Firefox.
manual_migration_cancel_button=नाही धन्यवाद
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
# of importing another browsers profile profile into Firefox.
manual_migration_import_button=आता आयात करा
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
# action link are shown in each section of UI that fails to render
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_move_up=वर जा
section_menu_action_move_down=खाली जा
section_menu_action_privacy_notice=गोपनीयता सूचना
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.

View File

@ -90,7 +90,7 @@ section_disclaimer_topstories_buttontext=Ok, entendi
# sidebar mozilla-central string for the panel that has preferences related to
# what is shown for the homepage, new windows, and new tabs.
prefs_home_header=Conteúdo inicial do Firefox
prefs_home_description=Escolha qual conteúdo você quer na sua tela inicial do Firefox.
prefs_home_description=Escolha que conteúdo você quer na sua tela inicial do Firefox.
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
@ -191,6 +191,8 @@ firstrun_form_sub_header=para continuar com o Firefox Sync.
firstrun_email_input_placeholder=E-mail
firstrun_invalid_input=Necessário e-mail válido
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Ao continuar você concorda com os {terms} e {privacy}.

View File

@ -158,10 +158,14 @@ section_menu_action_privacy_notice=رازداری کا نوٹس
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=اپنی ای میل داخل کریں
firstrun_email_input_placeholder=ای میل
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_terms_of_service=خدمت کی شرائط
firstrun_privacy_notice=رازداری کا نوٹس
firstrun_continue_to_login=جاری رکھیں

View File

@ -0,0 +1,24 @@
#!/bin/bash
export SHELL=/bin/bash
# Display required for `browser_parsable_css` tests
export DISPLAY=:99.0
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR
# Pull latest m-c and update tip
cd /mozilla-central && hg pull && hg update -C
# Build Activity Stream and copy the output to m-c
cd /activity-stream && npm install . && npm run buildmc
# Build latest m-c with Activity Stream changes
cd /mozilla-central && ./mach build \
&& ./mach test browser_parsable_css \
&& ./mach lint -l eslint -l codespell browser/extensions/activity-stream \
&& ./mach test browser/extensions/activity-stream --headless \
&& ./mach test browser/components/newtab/tests/browser --headless \
&& ./mach test browser/components/newtab/tests/xpcshell \
&& ./mach test browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js --headless \
&& ./mach test browser/components/preferences/in-content/tests/browser_newtab_menu.js --headless \
&& ./mach test browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js --headless \
&& ./mach test browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_1.js --headless

View File

@ -90,7 +90,7 @@
"mc_dir": "../mozilla-central"
},
"scripts": {
"mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest )",
"mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)",
"mochitest-debug": "(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest)",
"bundle": "npm-run-all bundle:*",
"bundle:locales": "pontoon-to-json --src locales --dest data",

View File

@ -85,21 +85,21 @@ window.gActivityStreamStrings = {
"section_menu_action_collapse_section": "Abschnitt einklappen",
"section_menu_action_expand_section": "Abschnitt ausklappen",
"section_menu_action_manage_section": "Abschnitt verwalten",
"section_menu_action_manage_webext": "Manage Extension",
"section_menu_action_manage_webext": "Erweiterung verwalten",
"section_menu_action_add_topsite": "Wichtige Seite hinzufügen",
"section_menu_action_move_up": "Nach oben schieben",
"section_menu_action_move_down": "Nach unten schieben",
"section_menu_action_privacy_notice": "Datenschutzhinweis",
"firstrun_title": "Take Firefox with You",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_title": "Firefox für unterwegs",
"firstrun_content": "Nehmen Sie Ihre Lesezeichen, Chronik, Passwörter und andere Einstellungen auf allen Geräten mit.",
"firstrun_learn_more_link": "Jetzt mehr über Firefox Konten erfahren",
"firstrun_form_header": "E-Mail-Adresse eingeben",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "Email",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "Terms of Service",
"firstrun_privacy_notice": "Privacy Notice",
"firstrun_continue_to_login": "Continue",
"firstrun_skip_login": "Skip this step"
"firstrun_form_sub_header": "um sich bei Firefox Sync anzumelden.",
"firstrun_email_input_placeholder": "E-Mail",
"firstrun_invalid_input": "Gültige E-Mail-Adresse erforderlich",
"firstrun_extra_legal_links": "Indem Sie fortfahren, stimmen Sie den {terms} und dem {privacy} zu.",
"firstrun_terms_of_service": "Nutzungsbedingungen",
"firstrun_privacy_notice": "Datenschutzhinweis",
"firstrun_continue_to_login": "Weiter",
"firstrun_skip_login": "Diesen Schritt überspringen"
};

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "پست‌الکترونیکی خود را وارد کنید",
"firstrun_form_sub_header": "برای فعال کردن همگام‌سازی فایرفاکس.",
"firstrun_email_input_placeholder": "پست‌الکترونیکی",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "رایانامهٔ معتبر لازم است",
"firstrun_extra_legal_links": "با ادامه دادن، شما {terms} و {privacy} قبول می‌کنید.",
"firstrun_terms_of_service": "قوانین سرویس",
"firstrun_privacy_notice": "نکات حریم‌خصوصی",

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Kirjoita sähköpostisi",
"firstrun_form_sub_header": "jatkaaksesi Firefox Sync -palveluun.",
"firstrun_email_input_placeholder": "Sähköposti",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "Sähköpostiosoitteen täytyy olla kelvollinen",
"firstrun_extra_legal_links": "Jatkamalla hyväksyt {terms} ja {privacy}.",
"firstrun_terms_of_service": "käyttöehdot",
"firstrun_privacy_notice": "tietosuojakäytännön",

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Saisissez votre adresse électronique",
"firstrun_form_sub_header": "pour continuer avec Firefox Sync.",
"firstrun_email_input_placeholder": "Adresse électronique",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "Adresse électronique valide requise",
"firstrun_extra_legal_links": "En continuant, vous acceptez les {terms} et la {privacy}.",
"firstrun_terms_of_service": "Conditions dutilisation",
"firstrun_privacy_notice": "Politique de confidentialité",

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "તમારા ઇમેઇલ દાખલ કરો",
"firstrun_form_sub_header": "Firefox સમન્વયન ચાલુ રાખવા માટે.",
"firstrun_email_input_placeholder": "ઇમેઇલ",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "માન્ય ઇમેઇલ આવશ્યક છે",
"firstrun_extra_legal_links": "આગળ વધીને, તમે {terms} અને {privacy} સાથે સંમત થાઓ છો.",
"firstrun_terms_of_service": "સેવાની શરતો",
"firstrun_privacy_notice": "ખાનગી સૂચના",

View File

@ -2,7 +2,7 @@
window.gActivityStreamStrings = {
"newtab_page_title": "नया टैब",
"header_top_sites": "सर्वोच्च साइटें",
"header_highlights": "झलकियाँ",
"header_highlights": "प्रमुखताएँ",
"header_recommended_by": "{provider} द्वारा अनुशंसित",
"context_menu_button_sr": "{title} के लिए कॉन्टेक्स्ट मेनू खोलें",
"section_context_menu_button_sr": "अनुभाग प्रसंग मेनू खोलें",
@ -53,7 +53,7 @@ window.gActivityStreamStrings = {
"prefs_snippets_description": "Mozilla और Firefox से अद्यतन",
"settings_pane_button_label": "अपने नए टैब पृष्ठ को अनुकूलित करें",
"settings_pane_topsites_header": "सर्वोच्च साइटें",
"settings_pane_highlights_header": "झलकियाँ",
"settings_pane_highlights_header": "प्रमुखताएँ",
"settings_pane_highlights_options_bookmarks": "पुस्तचिह्न",
"settings_pane_snippets_header": "अंश",
"edit_topsites_button_text": "संपादित करें",

View File

@ -74,7 +74,7 @@ window.gActivityStreamStrings = {
"topsites_form_image_validation": "სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL ბმული.",
"pocket_read_more": "პოპულარული თემები:",
"pocket_read_even_more": "მეტი სიახლის ნახვა",
"highlights_empty_state": "დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენი რჩეული სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.",
"highlights_empty_state": "დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენთვის სასურველი სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.",
"topstories_empty_state": "უკვე ყველაფერი წაკითხული გაქვთ. {provider}-იდან ახალი რჩეული სტატიების მისაღებად, მოგვიანებით შემოიარეთ. თუ ვერ ითმენთ, აირჩიეთ რომელიმე მოთხოვნადი თემა, ახალი საინტერესო სტატიების მოსაძიებლად.",
"manual_migration_explanation2": "გადმოიტანეთ სხვა ბრაუზერებიდან თქვენი სანიშნები, ისტორია და პაროლები Firefox-ში.",
"manual_migration_cancel_button": "არა, გმადლობთ",

View File

@ -27,11 +27,11 @@ window.gActivityStreamStrings = {
"menu_action_show_file_mac_os": "Show in Finder",
"menu_action_show_file_windows": "Open Containing Folder",
"menu_action_show_file_linux": "Open Containing Folder",
"menu_action_show_file_default": "Show File",
"menu_action_open_file": "Open File",
"menu_action_copy_download_link": "Copy Download Link",
"menu_action_go_to_download_page": "Go to Download Page",
"menu_action_remove_download": "Remove from History",
"menu_action_show_file_default": "ಕಡತ ತೋರಿಸು",
"menu_action_open_file": "ಕಡತವನ್ನು ತೆರೆ",
"menu_action_copy_download_link": "ಡೌನ್ಲೋಡ್ ಕೊಂಡಿಯನ್ನು ಪ್ರತಿ ಮಾಡು",
"menu_action_go_to_download_page": "ಡೌನ್ಲೋಡ್ ಪುಟಕ್ಕೆ ತೆರಳು",
"menu_action_remove_download": "ಇತಿಹಾಸದಿಂದ ತೆಗೆದುಹಾಕು",
"search_button": "ಹುಡುಕು",
"search_header": "{search_engine_name} ನಿಂದ ಹುಡುಕಿ",
"search_web_placeholder": "ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ",
@ -41,7 +41,7 @@ window.gActivityStreamStrings = {
"prefs_home_header": "Firefox Home Content",
"prefs_home_description": "Choose what content you want on your Firefox Home screen.",
"prefs_section_rows_option": "{num} row;{num} rows",
"prefs_search_header": "Web Search",
"prefs_search_header": "ಜಾಲದ ಹುಡುಕಾಟ",
"prefs_topsites_description": "The sites you visit most",
"prefs_topstories_description2": "Great content from around the web, personalized for you",
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
@ -60,13 +60,13 @@ window.gActivityStreamStrings = {
"edit_topsites_edit_button": "ಈ ತಾಣವನ್ನು ಸಂಪಾದಿಸು",
"topsites_form_add_header": "ಹೊಸ ಅಗ್ರ ತಾಣಗಳು",
"topsites_form_edit_header": "ಅಗ್ರ ತಾಣಗಳನ್ನು ಸಂಪಾದಿಸಿ",
"topsites_form_title_label": "Title",
"topsites_form_title_label": "ಶೀರ್ಷಿಕೆ",
"topsites_form_title_placeholder": "ಶೀರ್ಷಿಕೆಯನ್ನು ನಮೂದಿಸಿ",
"topsites_form_url_label": "URL",
"topsites_form_image_url_label": "Custom Image URL",
"topsites_form_url_placeholder": "ಒಂದು URL ಅನ್ನು ಟೈಪಿಸಿ ಅಥವಾ ನಕಲಿಸಿ",
"topsites_form_use_image_link": "Use a custom image…",
"topsites_form_preview_button": "Preview",
"topsites_form_preview_button": "ಮುನ್ನೋಟ",
"topsites_form_add_button": "ಸೇರಿಸು",
"topsites_form_save_button": "ಉಳಿಸು",
"topsites_form_cancel_button": "ರದ್ದು ಮಾಡು",
@ -87,7 +87,7 @@ window.gActivityStreamStrings = {
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_manage_webext": "Manage Extension",
"section_menu_action_add_topsite": "Add Top Site",
"section_menu_action_move_up": "Move Up",
"section_menu_action_move_up": "ಮೇಲೆ ಜರುಗಿಸು",
"section_menu_action_move_down": "Move Down",
"section_menu_action_privacy_notice": "Privacy Notice",
"firstrun_title": "Take Firefox with You",
@ -95,50 +95,11 @@ window.gActivityStreamStrings = {
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_form_header": "Enter your email",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "Email",
"firstrun_invalid_input": "Valid email required",
"firstrun_email_input_placeholder": "ಇಮೇಲ್",
"firstrun_invalid_input": "ಸರಿಯಾದ ಇಮೇಲ್ ಬೇಕಾಗಿದೆ",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "Terms of Service",
"firstrun_privacy_notice": "Privacy Notice",
"firstrun_continue_to_login": "Continue",
"firstrun_skip_login": "Skip this step",
"default_label_loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ…",
"header_stories": "ಪ್ರಮುಖ ಸುದ್ದಿಗಳು",
"header_visit_again": "ಮತ್ತೆ ಭೇಟಿಕೊಡು",
"header_bookmarks": "ಇತ್ತೀಚಿಗೆ ಮಾಡಲಾದ ಬುಕ್‌ಮಾರ್ಕುಗಳು",
"header_bookmarks_placeholder": "ನಿಮ್ಮ ಹತ್ತಿರ ಇನ್ನೂ ಯಾವುದೇ ಪುಟಗುರುತುಗಳಿಲ್ಲ.",
"header_stories_from": "ಯಿಂದ",
"type_label_synced": "ಮತ್ತೊಂದು ಸಾಧನದಿಂದ ಸಿಂಕ್ ಮಾಡಲಾಗಿದೆ",
"type_label_open": "ತೆರೆ",
"type_label_topic": "ವಿಷಯ",
"type_label_now": "ಈಗ",
"menu_action_copy_address": "ವಿಳಾಸವನ್ನು ನಕಲಿಸು",
"menu_action_email_link": "ಇಮೈಲ್ ಕೊಂಡಿ…",
"search_for_something_with": "{search_term} ಅನ್ನು ಇದರಿಂದ ಹುಡುಕಿ:",
"search_settings": "ಹುಡುಕು ಸಿದ್ಧತೆಗಳನ್ನು ಬದಲಾಯಿಸು",
"section_info_option": "ಮಾಹಿತಿ",
"section_info_send_feedback": "ಅಭಿಪ್ರಾಯವನ್ನು ಕಳುಹಿಸಿ",
"section_info_privacy_notice": "ಗೌಪ್ಯತಾ ಸೂಚನೆ",
"welcome_title": "ಹೊಸ ಹಾಳೆಗೆ ಸುಸ್ವಾಗತ",
"time_label_less_than_minute": "<1ನಿ",
"time_label_minute": "{number}ನಿ",
"time_label_hour": "{number}ಗ",
"time_label_day": "{number}ದಿ",
"settings_pane_header": "ಹೊಸ ಹಾಳೆಯ ಆದ್ಯತೆಗಳು",
"settings_pane_body2": "ನೀವು ಈ ಪುಟದಲ್ಲಿ ಏನು ನೋಡಿತ್ತೀರೆಂದು ಆಯ್ಕೆಮಾಡಿ.",
"settings_pane_search_header": "ಹುಡುಕು",
"settings_pane_search_body": "ಹೊಸ ಹಾಳೆಯಿಂದ ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ.",
"settings_pane_topsites_body": "ನೀವು ಅತಿ ಹೆಚ್ಚು ನೋಡುವ ಜಾಲತಾಣಗಳಿಗೆ ಪ್ರವೇಶದ್ವಾರ.",
"settings_pane_topsites_options_showmore": "ಎರಡು ಸಾಲುಗಳನ್ನು ಪ್ರದರ್ಶಿಸು",
"settings_pane_bookmarks_header": "ಇತ್ತೀಚಿನ ಪುಟಗುರುತುಗಳು",
"settings_pane_visit_again_header": "ಮತ್ತೆ ಭೇಟಿಕೊಡು",
"settings_pane_highlights_options_visited": "ಭೇಟಿ ನೀಡಿದ ತಾಣಗಳು",
"settings_pane_done_button": "ಆಯಿತು",
"edit_topsites_showmore_button": "‍ಹೆಚ್ಚು ತೋರಿಸು",
"edit_topsites_showless_button": "ಕೆಲವೊಂದು ತೋರಿಸಿ",
"edit_topsites_done_button": "ಆಯಿತು",
"edit_topsites_pin_button": "ಈ ತಾಣವನ್ನು ಪಿನ್ ಮಾಡು",
"edit_topsites_unpin_button": "ಈ ತಾಣವನ್ನು ಹೊರತೆಗೆ",
"edit_topsites_dismiss_button": "ಈ ತಾಣವನ್ನು ತೆಗೆದುಹಾಕು",
"edit_topsites_add_button": "ಸೇರಿಸು"
"firstrun_terms_of_service": "ಸೇವೆಯ ನಿಯಮಗಳು",
"firstrun_privacy_notice": "ಗೌಪ್ಯತಾ ಸೂಚನೆ",
"firstrun_continue_to_login": "ಮುಂದುವರೆ",
"firstrun_skip_login": "ಈ ಹಂತವನ್ನು ಹಾರಿಸಿ"
};

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Įveskite savo el. paštą",
"firstrun_form_sub_header": "norėdami tęsti su „Firefox Sync“.",
"firstrun_email_input_placeholder": "El. paštas",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "Reikalingas galiojantis el. pašto adresas",
"firstrun_extra_legal_links": "Tęsdami sutinkate su {terms} ir {privacy}.",
"firstrun_terms_of_service": "paslaugos teikimo nuostatais",
"firstrun_privacy_notice": "privatumo nuostatais",

View File

@ -3,28 +3,28 @@ window.gActivityStreamStrings = {
"newtab_page_title": "नवीन टॅब",
"header_top_sites": "खास साईट्स",
"header_highlights": "ठळक",
"header_recommended_by": "Recommended by {provider}",
"context_menu_button_sr": "Open context menu for {title}",
"section_context_menu_button_sr": "Open the section context menu",
"header_recommended_by": "{provider} तर्फे शिफारस",
"context_menu_button_sr": "{title} साठी संदर्भ मेनू उघडा",
"section_context_menu_button_sr": "विभाग संदर्भ मेनू उघडा",
"type_label_visited": "भेट दिलेले",
"type_label_bookmarked": "वाचनखुण लावले",
"type_label_recommended": "Trending",
"type_label_pocket": "Saved to Pocket",
"type_label_downloaded": "Downloaded",
"type_label_recommended": "प्रचलित",
"type_label_pocket": "Pocket मध्ये जतन झाले",
"type_label_downloaded": "डाउनलोड केलेले",
"menu_action_bookmark": "वाचनखुण",
"menu_action_remove_bookmark": "वाचनखुण काढा",
"menu_action_open_new_window": "नवीन पटलात उघडा",
"menu_action_open_private_window": "नवीन खाजगी पटलात उघडा",
"menu_action_dismiss": "रद्द करा",
"menu_action_delete": "इतिहासातून नष्ट करा",
"menu_action_pin": "Pin",
"menu_action_unpin": "Unpin",
"confirm_history_delete_p1": "Are you sure you want to delete every instance of this page from your history?",
"confirm_history_delete_notice_p2": "This action cannot be undone.",
"menu_action_pin": "पिन लावा",
"menu_action_unpin": "पिन काढा",
"confirm_history_delete_p1": "आपल्या इतिहासामधून या पृष्ठातील प्रत्येक उदाहरण खात्रीने हटवू इच्छिता?",
"confirm_history_delete_notice_p2": "ही क्रिया पूर्ववत केली जाऊ शकत नाही.",
"menu_action_save_to_pocket": "Pocket मध्ये जतन करा",
"menu_action_delete_pocket": "Delete from Pocket",
"menu_action_archive_pocket": "Archive in Pocket",
"menu_action_show_file_mac_os": "Show in Finder",
"menu_action_delete_pocket": "Pocket मधून हटवा",
"menu_action_archive_pocket": "Pocket मध्ये संग्रहित करा",
"menu_action_show_file_mac_os": "Finder मध्ये दर्शवा",
"menu_action_show_file_windows": "Open Containing Folder",
"menu_action_show_file_linux": "Open Containing Folder",
"menu_action_show_file_default": "Show File",
@ -35,50 +35,50 @@ window.gActivityStreamStrings = {
"search_button": "शोधा",
"search_header": "{search_engine_name} शोध",
"search_web_placeholder": "वेबवर शोधा",
"section_disclaimer_topstories": "The most interesting stories on the web, selected based on what you read. From Pocket, now part of Mozilla.",
"section_disclaimer_topstories_linktext": "Learn how it works.",
"section_disclaimer_topstories_buttontext": "Okay, got it",
"prefs_home_header": "Firefox Home Content",
"prefs_home_description": "Choose what content you want on your Firefox Home screen.",
"section_disclaimer_topstories": "आपण जे वाचतो त्यानुसार निवडलेल्या, वेबवरील सर्वात मनोरंजक कथा. Pocket कडून, आता Mozilla चा भाग.",
"section_disclaimer_topstories_linktext": "कसे कार्य करते ते जाणून घ्या.",
"section_disclaimer_topstories_buttontext": "ठीक आहे, समजले",
"prefs_home_header": "फायरफॉक्स होम वरील मजकूर",
"prefs_home_description": "आपल्या फायरफॉक्सचा मुख्यपृष्ठवर आपल्याला कोणती माहिती पाहिजे ते निवडा.",
"prefs_section_rows_option": "{num} row;{num} rows",
"prefs_search_header": "Web Search",
"prefs_topsites_description": "The sites you visit most",
"prefs_topstories_description2": "Great content from around the web, personalized for you",
"prefs_search_header": "वेब शोध",
"prefs_topsites_description": "आपण सर्वाधिक भेट देता त्या साइट",
"prefs_topstories_description2": "आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री",
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
"prefs_topstories_sponsored_learn_more": "Learn more",
"prefs_highlights_description": "A selection of sites that youve saved or visited",
"prefs_topstories_sponsored_learn_more": "अधिक जाणून घ्या",
"prefs_highlights_description": "आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा",
"prefs_highlights_options_visited_label": "Visited Pages",
"prefs_highlights_options_download_label": "Most Recent Download",
"prefs_highlights_options_pocket_label": "Pages Saved to Pocket",
"prefs_snippets_description": "Updates from Mozilla and Firefox",
"prefs_snippets_description": "Mozilla आणि Firefox कडून अद्यतने",
"settings_pane_button_label": "आपले नवीन टॅब पृष्ठ सानुकूलित करा",
"settings_pane_topsites_header": "Top Sites",
"settings_pane_highlights_header": "Highlights",
"settings_pane_highlights_options_bookmarks": "Bookmarks",
"settings_pane_snippets_header": "Snippets",
"edit_topsites_button_text": "Edit",
"edit_topsites_edit_button": "Edit this site",
"topsites_form_add_header": "New Top Site",
"topsites_form_edit_header": "Edit Top Site",
"topsites_form_title_label": "Title",
"topsites_form_title_placeholder": "Enter a title",
"settings_pane_topsites_header": "शीर्ष साइट्स",
"settings_pane_highlights_header": "ठळक",
"settings_pane_highlights_options_bookmarks": "वाचनखुणा",
"settings_pane_snippets_header": "कात्रणे",
"edit_topsites_button_text": "संपादित करा",
"edit_topsites_edit_button": "ही साइट संपादित करा",
"topsites_form_add_header": "नवीन खास साइट",
"topsites_form_edit_header": "खास साईट संपादित करा",
"topsites_form_title_label": "शिर्षक",
"topsites_form_title_placeholder": "शिर्षक प्रविष्ट करा",
"topsites_form_url_label": "URL",
"topsites_form_image_url_label": "Custom Image URL",
"topsites_form_url_placeholder": "Type or paste a URL",
"topsites_form_url_placeholder": "URL चिकटवा किंवा टाईप करा",
"topsites_form_use_image_link": "Use a custom image…",
"topsites_form_preview_button": "Preview",
"topsites_form_add_button": "Add",
"topsites_form_save_button": "Save",
"topsites_form_cancel_button": "Cancel",
"topsites_form_url_validation": "Valid URL required",
"topsites_form_preview_button": "पूर्वावलोकन",
"topsites_form_add_button": "समाविष्ट करा",
"topsites_form_save_button": "जतन करा",
"topsites_form_cancel_button": "रद्द करा",
"topsites_form_url_validation": "वैध URL आवश्यक",
"topsites_form_image_validation": "Image failed to load. Try a different URL.",
"pocket_read_more": "Popular Topics:",
"pocket_read_even_more": "View More Stories",
"highlights_empty_state": "Start browsing, and well show some of the great articles, videos, and other pages youve recently visited or bookmarked here.",
"topstories_empty_state": "Youve caught up. Check back later for more top stories from {provider}. Cant wait? Select a popular topic to find more great stories from around the web.",
"manual_migration_explanation2": "Try Firefox with the bookmarks, history and passwords from another browser.",
"manual_migration_cancel_button": "No Thanks",
"manual_migration_import_button": "Import Now",
"pocket_read_more": "लोकप्रिय विषय:",
"pocket_read_even_more": "अधिक कथा पहा",
"highlights_empty_state": "ब्राउझिंग सुरू करा, आणि आम्ही आपल्याला इथे आपण अलीकडील भेट दिलेले किंवा वाचनखूण लावलेले उत्कृष्ठ लेख, व्हिडिओ, आणि इतर पृष्ठांपैकी काही दाखवू.",
"topstories_empty_state": "तुम्ही सर्व बघितले. {provider} कडून आणखी महत्वाच्या गोष्टी बघण्यासाठी नंतर परत तपासा. प्रतीक्षा करू शकत नाही? वेबवरील छान गोष्टी शोधण्यासाठी लोकप्रिय विषय निवडा.",
"manual_migration_explanation2": "दुसऱ्या ब्राऊझरमधील वाचनखूणा, इतिहास आणि पासवर्ड सोबत Firefox ला वापरून पहा.",
"manual_migration_cancel_button": "नाही धन्यवाद",
"manual_migration_import_button": "आता आयात करा",
"error_fallback_default_info": "Oops, something went wrong loading this content.",
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
"section_menu_action_remove_section": "Remove Section",
@ -87,9 +87,9 @@ window.gActivityStreamStrings = {
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_manage_webext": "Manage Extension",
"section_menu_action_add_topsite": "Add Top Site",
"section_menu_action_move_up": "Move Up",
"section_menu_action_move_down": "Move Down",
"section_menu_action_privacy_notice": "Privacy Notice",
"section_menu_action_move_up": "वर जा",
"section_menu_action_move_down": "खाली जा",
"section_menu_action_privacy_notice": "गोपनीयता सूचना",
"firstrun_title": "Take Firefox with You",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
@ -101,24 +101,5 @@ window.gActivityStreamStrings = {
"firstrun_terms_of_service": "Terms of Service",
"firstrun_privacy_notice": "Privacy Notice",
"firstrun_continue_to_login": "Continue",
"firstrun_skip_login": "Skip this step",
"default_label_loading": "दाखल करीत आहे…",
"header_stories": "महत्वाच्या गोष्टी",
"header_stories_from": "कडून",
"type_label_synced": "इतर साधनावरुन ताळमेळ केले",
"type_label_open": "उघडा",
"type_label_topic": "विषय",
"menu_action_copy_address": "पत्त्याची प्रत बनवा",
"menu_action_email_link": "दुवा इमेल करा…",
"search_for_something_with": "शोधा {search_term} सोबत:",
"search_settings": "शोध सेटिंग बदला",
"welcome_title": "नवीन टॅबवर स्वागत आहे",
"time_label_less_than_minute": "<1मि",
"time_label_minute": "{number}मि",
"time_label_hour": "{number}ता",
"time_label_day": "{number}दि",
"settings_pane_header": "नवीन टॅब प्राधान्ये",
"settings_pane_body": "नवीन टॅब उघडल्यानंतर काय दिसायला हवे ते निवडा.",
"settings_pane_search_header": "शोध",
"settings_pane_search_body": "आपल्या नवीन टॅब वरून वेबवर शोधा."
"firstrun_skip_login": "Skip this step"
};

View File

@ -39,7 +39,7 @@ window.gActivityStreamStrings = {
"section_disclaimer_topstories_linktext": "Saiba como funciona.",
"section_disclaimer_topstories_buttontext": "Ok, entendi",
"prefs_home_header": "Conteúdo inicial do Firefox",
"prefs_home_description": "Escolha qual conteúdo você quer na sua tela inicial do Firefox.",
"prefs_home_description": "Escolha que conteúdo você quer na sua tela inicial do Firefox.",
"prefs_section_rows_option": "{num} linha;{num} linhas",
"prefs_search_header": "Pesquisa na web",
"prefs_topsites_description": "Os sites que você mais visita",
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Insira seu email",
"firstrun_form_sub_header": "para continuar com o Firefox Sync.",
"firstrun_email_input_placeholder": "E-mail",
"firstrun_invalid_input": "Email válido requerido",
"firstrun_invalid_input": "Necessário e-mail válido",
"firstrun_extra_legal_links": "Ao continuar você concorda com os {terms} e {privacy}.",
"firstrun_terms_of_service": "Termos de serviço",
"firstrun_privacy_notice": "Política de privacidade",

View File

@ -93,13 +93,13 @@ window.gActivityStreamStrings = {
"firstrun_title": "Take Firefox with You",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_form_header": "Enter your email",
"firstrun_form_header": "اپنی ای میل داخل کریں",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "ای میل",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "Terms of Service",
"firstrun_privacy_notice": "Privacy Notice",
"firstrun_terms_of_service": "خدمت کی شرائط",
"firstrun_privacy_notice": "رازداری کا نوٹس",
"firstrun_continue_to_login": "جاری رکھیں",
"firstrun_skip_login": "Skip this step"
};

View File

@ -6,6 +6,10 @@ ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "ShellService",
"resource:///modules/ShellService.jsm");
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
"resource://testing-common/PlacesTestUtils.jsm");
const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
@ -28,7 +32,7 @@ add_task(async function find_matching_message() {
];
const context = {FOO: true};
const match = await ASRouterTargeting.findMatchingMessage(messages, {}, context);
const match = await ASRouterTargeting.findMatchingMessage({messages, target: {}, context});
is(match, messages[0], "should match and return the correct message");
});
@ -37,7 +41,7 @@ add_task(async function return_nothing_for_no_matching_message() {
const messages = [{id: "bar", targeting: "!FOO"}];
const context = {FOO: true};
const match = await ASRouterTargeting.findMatchingMessage(messages, {}, context);
const match = await ASRouterTargeting.findMatchingMessage({messages, target: {}, context});
is(match, undefined, "should return nothing since no matching message exists");
});
@ -49,7 +53,7 @@ add_task(async function checkProfileAgeCreated() {
"should return correct profile age creation date");
const message = {id: "foo", targeting: `profileAgeCreated > ${await profileAccessor.created - 100}`};
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by profile age created");
});
@ -59,7 +63,7 @@ add_task(async function checkProfileAgeReset() {
"should return correct profile age reset");
const message = {id: "foo", targeting: `profileAgeReset == ${await profileAccessor.reset}`};
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by profile age reset");
});
@ -69,7 +73,7 @@ add_task(async function checkhasFxAccount() {
"should return true if a fx account is set");
const message = {id: "foo", targeting: "hasFxAccount"};
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by hasFxAccount");
});
@ -89,11 +93,11 @@ add_task(async function checksearchEngines() {
"searchEngines.current should be the current engine name");
const message = {id: "foo", targeting: `searchEngines[.current == ${Services.search.currentEngine.identifier}]`};
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by searchEngines.current");
const message2 = {id: "foo", targeting: `searchEngines[${Services.search.getVisibleEngines()[0].identifier} in .installed]`};
is(await ASRouterTargeting.findMatchingMessage([message2], {}), message2,
is(await ASRouterTargeting.findMatchingMessage({messages: [message2], target: {}}), message2,
"should select correct item by searchEngines.installed");
});
@ -105,7 +109,7 @@ add_task(async function checkisDefaultBrowser() {
is(result, expected,
"isDefaultBrowser should be equal to ShellService.isDefaultBrowser()");
const message = {id: "foo", targeting: `isDefaultBrowser == ${expected.toString()}`};
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by isDefaultBrowser");
});
@ -114,7 +118,7 @@ add_task(async function checkdevToolsOpenedCount() {
is(ASRouterTargeting.Environment.devToolsOpenedCount, 5,
"devToolsOpenedCount should be equal to devtools.selfxss.count pref value");
const message = {id: "foo", targeting: "devToolsOpenedCount >= 5"};
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by devToolsOpenedCount");
});
@ -173,6 +177,53 @@ add_task(async function checkAddonsInfo() {
"should correctly provide `userDisabled` property from full data");
ok(Object.prototype.hasOwnProperty.call(testAddon, "installDate") &&
(Math.abs(new Date() - new Date(testAddon.installDate)) < 60 * 1000),
(Math.abs(Date.now() - new Date(testAddon.installDate)) < 60 * 1000),
"should correctly provide `installDate` property from full data");
});
add_task(async function checkFrecentSites() {
const now = Date.now();
const timeDaysAgo = numDays => now - numDays * 24 * 60 * 60 * 1000;
const visits = [];
for (const [uri, count, visitDate] of [
["https://mozilla1.com/", 10, timeDaysAgo(0)], // frecency 1000
["https://mozilla2.com/", 5, timeDaysAgo(1)], // frecency 500
["https://mozilla3.com/", 1, timeDaysAgo(2)] // frecency 100
]) {
[...Array(count).keys()].forEach(() => visits.push({
uri,
visitDate: visitDate * 1000 // Places expects microseconds
}));
}
await PlacesTestUtils.addVisits(visits);
let message = {id: "foo", targeting: "'mozilla3.com' in topFrecentSites|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item by host in topFrecentSites");
message = {id: "foo", targeting: "'non-existent.com' in topFrecentSites|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
"should not select incorrect item by host in topFrecentSites");
message = {id: "foo", targeting: "'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item when filtering by frecency");
message = {id: "foo", targeting: "'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
"should not select incorrect item when filtering by frecency");
message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host')`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item when filtering by lastVisitDate");
message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(0) - 1}]|mapToProperty('host')`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
"should not select incorrect item when filtering by lastVisitDate");
message = {id: "foo", targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains");
});

View File

@ -12,6 +12,8 @@ import {_ASRouter} from "lib/ASRouter.jsm";
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER];
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
const ONE_DAY = 24 * 60 * 60 * 1000;
// Creates a message object that looks like messages returned by
// RemotePageManager listeners
function fakeAsyncMessage(action) {
@ -23,14 +25,18 @@ describe("ASRouter", () => {
let channel;
let sandbox;
let blockList;
let impressions;
let fetchStub;
let clock;
let getStringPrefStub;
let addObserverStub;
function createFakeStorage() {
const getStub = sandbox.stub();
getStub.withArgs("blockList").returns(Promise.resolve(blockList));
getStub.withArgs("impressions").returns(Promise.resolve(impressions));
return {
get: sandbox.stub().returns(Promise.resolve(blockList)),
get: getStub,
set: sandbox.stub().returns(Promise.resolve())
};
}
@ -43,6 +49,7 @@ describe("ASRouter", () => {
beforeEach(async () => {
blockList = [];
impressions = {};
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
fetchStub = sandbox.stub(global, "fetch")
@ -77,12 +84,22 @@ describe("ASRouter", () => {
assert.calledWith(addObserverStub, "remotePref");
});
it("should set state.blockList to the block list in persistent storage", async () => {
blockList = ["MESSAGE_ID"];
blockList = ["foo"];
Router = new _ASRouter({providers: FAKE_PROVIDERS});
await Router.init(channel, createFakeStorage());
assert.deepEqual(Router.state.blockList, ["MESSAGE_ID"]);
assert.deepEqual(Router.state.blockList, ["foo"]);
});
it("should set state.impressions to the impressions object in persistent storage", async () => {
// Note that impressions are only kept if a message exists in router and has a .frequency property,
// otherwise they will be cleaned up by .cleanupImpressions()
const testMessage = {id: "foo", frequency: {lifetimeCap: 10}};
impressions = {foo: [0, 1, 2]};
Router = new _ASRouter({providers: [{id: "onboarding", type: "local", messages: [testMessage]}]});
await Router.init(channel, createFakeStorage());
assert.deepEqual(Router.state.impressions, impressions);
});
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
Router = new _ASRouter({providers: FAKE_PROVIDERS});
@ -310,7 +327,6 @@ describe("ASRouter", () => {
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
assert.calledOnce(Router._storage.set);
assert.calledWithExactly(Router._storage.set, "blockList", bundleIds);
});
});
@ -326,7 +342,6 @@ describe("ASRouter", () => {
it("should save the blockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.calledOnce(Router._storage.set);
assert.calledWithExactly(Router._storage.set, "blockList", []);
});
});
@ -344,7 +359,6 @@ describe("ASRouter", () => {
it("should save the blockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.calledOnce(Router._storage.set);
assert.calledWithExactly(Router._storage.set, "blockList", []);
});
});
@ -521,4 +535,98 @@ describe("ASRouter", () => {
assert.calledTwice(Cu.reportError);
});
});
describe("impressions", () => {
it("should add an impression and update _storage with the current time if the message frequency caps", async () => {
clock.tick(42);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", frequency: {lifetime: 5}}});
await Router.onMessage(msg);
assert.isArray(Router.state.impressions.foo);
assert.deepEqual(Router.state.impressions.foo, [42]);
assert.calledWith(Router._storage.set, "impressions", {foo: [42]});
});
it("should not add an impression if the message doesn't have frequency caps", async () => {
// Note that storage.set is called during initialization, so it needs to be reset
Router._storage.set.reset();
clock.tick(42);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo"}});
await Router.onMessage(msg);
assert.notProperty(Router.state.impressions, "foo");
assert.notCalled(Router._storage.set);
});
describe("getLongestPeriod", () => {
it("should return the period if there is only one definition", () => {
const message = {id: "foo", frequency: {custom: [{period: 200, cap: 2}]}};
assert.equal(Router.getLongestPeriod(message), 200);
});
it("should return the longest period if there are more than one definitions", () => {
const message = {id: "foo", frequency: {custom: [{period: 1000, cap: 3}, {period: ONE_DAY, cap: 5}, {period: 100, cap: 2}]}};
assert.equal(Router.getLongestPeriod(message), ONE_DAY);
});
it("should return null if there are is no .frequency", () => {
const message = {id: "foo"};
assert.isNull(Router.getLongestPeriod(message));
});
it("should return null if there are is no .frequency.custom", () => {
const message = {id: "foo", frequency: {lifetime: 10}};
assert.isNull(Router.getLongestPeriod(message));
});
});
describe("cleanup on init", () => {
it("should clear impressions for messages which do not exist in state.messages", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}];
impressions = {foo: [0], bar: [0, 1]};
// Impressions for "bar" should be removed since that id does not exist in messages
const result = {foo: [0]};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should clear impressions older than the period if no lifetime impression cap is included", async () => {
const CURRENT_TIME = ONE_DAY * 2;
clock.tick(CURRENT_TIME);
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}]}}];
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
const result = {foo: [CURRENT_TIME - 10]};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should clear impressions older than the longest period if no lifetime impression cap is included", async () => {
const CURRENT_TIME = ONE_DAY * 2;
clock.tick(CURRENT_TIME);
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}, {period: 100, cap: 2}]}}];
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
const result = {foo: [CURRENT_TIME - 10]};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should clear impressions if they are not properly formatted", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}];
// this is impromperly formatted since impressions are supposed to be an array
impressions = {foo: 0};
const result = {};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should not clear impressions for messages which do exist in state.messages", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}, {id: "bar", frequency: {lifetime: 10}}];
impressions = {foo: [0], bar: []};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.notCalled(Router._storage.set);
assert.deepEqual(Router.state.impressions, impressions);
});
});
});
});

View File

@ -0,0 +1,100 @@
import {ASRouterTargeting} from "lib/ASRouterTargeting.jsm";
const ONE_DAY = 24 * 60 * 60 * 1000;
// Note that tests for the ASRouterTargeting environment can be found in
// test/functional/mochitest/browser_asrouter_targeting.js
describe("ASRouterTargeting#isBelowFrequencyCap", () => {
describe("lifetime frequency caps", () => {
it("should return true if .frequency is not defined on the message", () => {
const message = {id: "msg1"};
const impressions = [0, 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return true if there are no impressions", () => {
const message = {id: "msg1", frequency: {lifetime: 10, custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return true if the # of impressions is less than .frequency.lifetime", () => {
const message = {id: "msg1", frequency: {lifetime: 3}};
const impressions = [0, 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return false if the # of impressions is equal to .frequency.lifetime", () => {
const message = {id: "msg1", frequency: {lifetime: 2}};
const impressions = [0, 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should return false if the # of impressions is greater than .frequency.lifetime", () => {
const message = {id: "msg1", frequency: {lifetime: 2}};
const impressions = [0, 1, 2];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
});
describe("custom frequency caps", () => {
let sandbox;
let clock;
beforeEach(() => {
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
});
afterEach(() => {
sandbox.restore();
});
it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
const impressions = [0, ONE_DAY + 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
clock.tick(200);
const message = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}], lifetime: 3}};
const impressions = [0, 160, 161];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY + 200);
const messageTrue = {id: "msg2", frequency: {custom: [{period: 100, cap: 2}]}};
const messageFalse = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}, {period: ONE_DAY, cap: 3}]}};
const impressions = [0, ONE_DAY + 160, ONE_DAY - 100, ONE_DAY - 200];
assert.isTrue(ASRouterTargeting.isBelowFrequencyCap(messageTrue, impressions));
assert.isFalse(ASRouterTargeting.isBelowFrequencyCap(messageFalse, impressions));
});
it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should allow the 'daily' alias for period", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: "daily", cap: 2}]}};
assert.isFalse(ASRouterTargeting.isBelowFrequencyCap(message, [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3]));
assert.isTrue(ASRouterTargeting.isBelowFrequencyCap(message, [0, 1, 2, 3, ONE_DAY + 1]));
});
});
});

View File

@ -33,19 +33,6 @@ describe("FaviconFeed", () => {
siteIconsPref = true;
sandbox.stub(global.Services.prefs, "getBoolPref")
.withArgs("browser.chrome.site_icons").callsFake(() => siteIconsPref);
let fetchStub = globals.sandbox.stub();
globals.set("fetch", fetchStub);
fetchStub.resolves({
ok: true,
status: 200,
json: () => Promise.resolve([{
"domains": ["facebook.com"],
"image_url": "https://www.facebook.com/icon.png"
}, {
"domains": ["gmail.com", "mail.google.com"],
"image_url": "https://iconserver.com/gmail.png"
}])
});
feed = new FaviconFeed();
feed.store = {
@ -63,151 +50,13 @@ describe("FaviconFeed", () => {
assert.instanceOf(feed, FaviconFeed);
});
describe("#getSitesByDomain", () => {
it("should loadCachedData and maybeRefresh if _sitesByDomain isn't set", async () => {
feed.loadCachedData = sinon.spy(() => ([]));
feed.maybeRefresh = sinon.spy(() => {
feed._sitesByDomain = {"mozilla.org": {"image_url": "https://mozilla.org/icon.png"}};
return [];
});
await feed.getSitesByDomain();
assert.calledOnce(feed.loadCachedData);
assert.calledOnce(feed.maybeRefresh);
});
it("should NOT loadCachedData and maybeRefresh if _sitesByDomain is already set", async () => {
feed._sitesByDomain = {"mozilla.org": {"image_url": "https://mozilla.org/icon.png"}};
feed.loadCachedData = sinon.spy(() => ([]));
feed.maybeRefresh = sinon.spy(() => ([]));
await feed.getSitesByDomain();
assert.notCalled(feed.loadCachedData);
assert.notCalled(feed.maybeRefresh);
});
it("should resolve to empty object if there is no cache and fetch fails", async () => {
feed.loadCachedData = sinon.spy(() => ([]));
feed.maybeRefresh = sinon.spy(() => ([]));
await feed.getSitesByDomain();
assert.deepEqual(feed._sitesByDomain, {});
});
});
describe("#loadCachedData", () => {
it("should set _sitesByDomain if there is cached data", async () => {
const cachedData = {
"mozilla.org": {"image_url": "https://mozilla.org/icon.png"},
"_timestamp": Date.now(),
"_etag": "foobaretag1234567890"
};
feed.cache.get = () => cachedData;
await feed.loadCachedData();
assert.deepEqual(feed._sitesByDomain, cachedData);
assert.equal(feed.tippyTopNextUpdate, cachedData._timestamp + 24 * 60 * 60 * 1000);
assert.equal(feed._sitesByDomain._etag, cachedData._etag);
});
it("should NOT set _sitesByDomain if there is no cached data", async () => {
feed.cache.get = () => ({});
await feed.loadCachedData();
assert.isNull(feed._sitesByDomain);
});
});
describe("#maybeRefresh", () => {
it("should refresh if next update is due", () => {
feed.refresh = sinon.spy();
feed.tippyTopNextUpdate = Date.now();
feed.maybeRefresh();
assert.calledOnce(feed.refresh);
});
it("should NOT refresh if next update is in future", () => {
feed.refresh = sinon.spy();
feed.tippyTopNextUpdate = Date.now() + 6000;
feed.maybeRefresh();
assert.notCalled(feed.refresh);
});
});
describe("#refresh", () => {
it("should loadFromURL with the right URL from prefs", async () => {
feed.loadFromURL = sinon.spy(() => ({data: []}));
await feed.refresh();
assert.calledOnce(feed.loadFromURL);
assert.calledWith(feed.loadFromURL, FAKE_ENDPOINT);
});
it("should set _sitesByDomain if new sites are returned from loadFromURL", async () => {
const data = {
data: [
{"domains": ["mozilla.org"], "image_url": "https://mozilla.org/icon.png"},
{"domains": ["facebook.com"], "image_url": "https://facebook.com/icon.png"}
],
etag: "etag1234567890",
status: 200
};
const expectedData = {
"facebook.com": {"image_url": "https://facebook.com/icon.png"},
"mozilla.org": {"image_url": "https://mozilla.org/icon.png"},
"_etag": "etag1234567890",
"_timestamp": Date.now()
};
feed.loadFromURL = sinon.spy(url => data);
feed.cache.set = sinon.spy();
await feed.refresh();
assert.equal(feed._sitesByDomain._etag, data.etag);
assert.deepEqual(feed._sitesByDomain, expectedData);
assert.calledOnce(feed.cache.set);
assert.calledWith(feed.cache.set, "sites", expectedData);
});
it("should pass If-None-Match if we have a last known etag", async () => {
feed.loadFromURL = sinon.spy(url => ({data: [], status: 304}));
feed._sitesByDomain = {};
feed._sitesByDomain._etag = "etag1234567890";
await feed.refresh();
const [, headers] = feed.loadFromURL.getCall(0).args;
assert.equal(headers.get("If-None-Match"), feed._sitesByDomain._etag);
});
it("should not set _sitesByDomain if the remote manifest is not modified since last fetch", async () => {
const data = {"mozilla.org": {"image_url": "https://mozilla.org/icon.png"}};
feed._sitesByDomain = data;
feed._sitesByDomain._timestamp = Date.now() - 1000;
feed.loadFromURL = sinon.spy(url => ({data: [], status: 304}));
feed.cache.set = sinon.spy();
await feed.refresh();
assert.deepEqual(feed._sitesByDomain, data);
assert.calledOnce(feed.cache.set);
assert.calledWith(feed.cache.set, "sites", Object.assign({_timestamp: Date.now()}, data));
});
it("should handle server errors by retrying with exponential backoff", async () => {
const expectedDelay = 5 * 60 * 1000;
feed.loadFromURL = sinon.spy(url => ({data: [], status: 500}));
await feed.refresh();
assert.equal(1, feed.numRetries);
assert.equal(expectedDelay, feed.tippyTopNextUpdate);
await feed.refresh();
assert.equal(2, feed.numRetries);
assert.equal(2 * expectedDelay, feed.tippyTopNextUpdate);
await feed.refresh();
assert.equal(3, feed.numRetries);
assert.equal(2 * 2 * expectedDelay, feed.tippyTopNextUpdate);
await feed.refresh();
assert.equal(4, feed.numRetries);
assert.equal(2 * 2 * 2 * expectedDelay, feed.tippyTopNextUpdate);
// Verify the delay maxes out at 24 hours.
feed.numRetries = 100;
await feed.refresh();
assert.equal(24 * 60 * 60 * 1000, feed.tippyTopNextUpdate);
// Verify the numRetries gets reset on a successful fetch.
feed.loadFromURL = sinon.spy(url => ({data: [], status: 200}));
await feed.refresh();
assert.equal(0, feed.numRetries);
assert.equal(24 * 60 * 60 * 1000, feed.tippyTopNextUpdate);
});
});
describe("#fetchIcon", () => {
let domain;
let url;
beforeEach(() => {
domain = "mozilla.org";
url = `https://${domain}/`;
feed._sitesByDomain = {[domain]: {url, image_url: `${url}/icon.png`}};
feed.getSite = sandbox.stub().returns(Promise.resolve({domain, image_url: `${url}/icon.png`}));
feed._queryForRedirects.clear();
});
@ -230,19 +79,14 @@ describe("FaviconFeed", () => {
assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);
});
it("should NOT setAndFetchFaviconForPage if the endpoint is empty", async () => {
feed.store.state.Prefs.values["tippyTop.service.endpoint"] = "";
await feed.fetchIcon(url);
assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);
});
it("should NOT setAndFetchFaviconForPage if the url is NOT in the TippyTop data", async () => {
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
await feed.fetchIcon("https://example.com");
assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);
});
it("should issue a fetchIconFromRedirects if the url is NOT in the TippyTop data", async () => {
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
sandbox.spy(global.Services.tm, "idleDispatchToMainThread");
await feed.fetchIcon("https://example.com");
@ -250,6 +94,7 @@ describe("FaviconFeed", () => {
assert.calledOnce(global.Services.tm.idleDispatchToMainThread);
});
it("should only issue fetchIconFromRedirects once on the same url", async () => {
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
sandbox.spy(global.Services.tm, "idleDispatchToMainThread");
await feed.fetchIcon("https://example.com");
@ -258,6 +103,7 @@ describe("FaviconFeed", () => {
assert.calledOnce(global.Services.tm.idleDispatchToMainThread);
});
it("should issue fetchIconFromRedirects twice on two different urls", async () => {
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
sandbox.spy(global.Services.tm, "idleDispatchToMainThread");
await feed.fetchIcon("https://example.com");
@ -265,28 +111,29 @@ describe("FaviconFeed", () => {
assert.calledTwice(global.Services.tm.idleDispatchToMainThread);
});
it("should cause sites to initialize with fetched sites if no sites", async () => {
delete feed._sitesByDomain;
});
await feed.fetchIcon(url);
assert.containsAllKeys(feed._sitesByDomain, ["facebook.com", "gmail.com", "mail.google.com"]);
describe("#getSite", () => {
it("should return site data if RemoteSettings has an entry for the domain", async () => {
const get = () => Promise.resolve([{domain: "example.com", image_url: "foo.img"}]);
feed._tippyTop = {get};
const site = await feed.getSite("example.com");
assert.equal(site.domain, "example.com");
});
it("should return null if RemoteSettings doesn't have an entry for the domain", async () => {
const get = () => Promise.resolve([]);
feed._tippyTop = {get};
const site = await feed.getSite("example.com");
assert.isNull(site);
});
it("should lazy init _tippyTop", async () => {
assert.isUndefined(feed._tippyTop);
await feed.getSite("example.com");
assert.ok(feed._tippyTop);
});
});
describe("#onAction", () => {
it("should maybeRefresh on SYSTEM_TICK if initialized", async () => {
feed._sitesByDomain = {"mozilla.org": {}};
feed.maybeRefresh = sinon.spy();
feed.onAction({type: at.SYSTEM_TICK});
assert.calledOnce(feed.maybeRefresh);
});
it("should NOT maybeRefresh on SYSTEM_TICK if NOT initialized", async () => {
feed._sitesByDomain = null;
feed.maybeRefresh = sinon.spy();
feed.onAction({type: at.SYSTEM_TICK});
assert.notCalled(feed.maybeRefresh);
});
it("should fetchIcon on RICH_ICON_MISSING", async () => {
feed.fetchIcon = sinon.spy();
const url = "https://mozilla.org";

View File

@ -45,7 +45,12 @@ const TEST_GLOBAL = {
ChromeUtils: {
defineModuleGetter() {},
generateQI() { return {}; },
import() {}
import(str) {
if (str === "resource://services-settings/remote-settings.js") {
return {RemoteSettings: TEST_GLOBAL.RemoteSettings};
}
return {};
}
},
Components: {isSuccessCode: () => true},
// eslint-disable-next-line object-shorthand
@ -86,6 +91,7 @@ const TEST_GLOBAL = {
fetch() {},
// eslint-disable-next-line object-shorthand
Image: function() {}, // NB: This is a function/constructor
NewTabUtils: {activityStreamProvider: {getTopFrecentSites: () => []}},
PlacesUtils: {
get bookmarks() {
return TEST_GLOBAL.Cc["@mozilla.org/browser/nav-bookmarks-service;1"];
@ -184,7 +190,8 @@ const TEST_GLOBAL = {
},
EventEmitter,
ShellService: {isDefaultBrowser: () => true},
FilterExpressions: {eval() { return Promise.resolve(true); }}
FilterExpressions: {eval() { return Promise.resolve(true); }},
RemoteSettings() { return {get() { return Promise.resolve([]); }}; }
};
overrider.set(TEST_GLOBAL);

View File

@ -5,7 +5,7 @@
scripts:
# Run the activity-stream mochitests
mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest )
mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)
# Run the activity-stream mochitests with the browser toolbox debugger.
# Often handy in combination with adding a "debugger" statement in your