Merge mozilla-central to mozilla-inbound. a=merge on a CLOSED TREE

This commit is contained in:
Andreea Pavel 2018-07-25 18:13:30 +03:00
Родитель 2bcc8b96f5 943a6cf31a
Коммит da179a7f47
159 изменённых файлов: 11160 добавлений и 1241 удалений

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

@ -199,6 +199,8 @@ var paymentDialogWrapper = {
throw new Error("Invalid PaymentRequest ID");
}
window.addEventListener("unload", this);
// The Request object returned by the Payment Service is live and
// will automatically get updated if event.updateWith is used.
this.request = paymentSrv.getPaymentRequestById(requestId);
@ -624,6 +626,24 @@ var paymentDialogWrapper = {
}
},
/**
* @implement {nsIDOMEventListener}
* @param {Event} event
*/
handleEvent(event) {
switch (event.type) {
case "unload": {
// Remove the observer to avoid message manager errors while the dialog
// is closing and tests are cleaning up autofill storage.
Services.obs.removeObserver(this, "formautofill-storage-changed");
break;
}
default: {
throw new Error("Unexpected event handled");
}
}
},
/**
* @implements {nsIObserver}
* @param {nsISupports} subject

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

@ -110,7 +110,7 @@ export default class AddressPicker extends RichPicker {
this.dropdown.value = selectedAddressGUID;
if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) {
throw new Error(`${this.selectedStateKey} option ${selectedAddressGUID}` +
throw new Error(`${this.selectedStateKey} option ${selectedAddressGUID} ` +
`does not exist in the address picker`);
}
}

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

@ -224,7 +224,6 @@ export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLEleme
_renderPayButton(state) {
let completeStatus = state.request.completeStatus;
switch (completeStatus) {
case "initial":
case "processing":
case "success":
case "unknown": {
@ -232,22 +231,18 @@ export default class PaymentDialog extends PaymentStateSubscriberMixin(HTMLEleme
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
break;
}
case "": // initial/default state
case "fail":
case "timeout": {
// pay button is hidden in these states. Reset its label and disable it
this._payButton.textContent = this._payButton.dataset.initialLabel;
this._payButton.disabled = true;
break;
}
case "": {
completeStatus = "initial";
// pay button is hidden in fail/timeout states.
this._payButton.textContent = this._payButton.dataset.label;
this._payButton.disabled = completeStatus !== "" || state.changesPrevented;
break;
}
default: {
throw new Error(`Invalid completeStatus: ${completeStatus}`);
}
}
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
}
stateChangeCallback(state) {

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

@ -29,3 +29,7 @@ label.block {
button.wide {
width: 100%;
}
#complete-status {
column-count: 2;
}

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

@ -45,17 +45,16 @@
<section class="group">
<h1>States</h1>
<fieldset>
<fieldset id="complete-status">
<legend>Complete Status</legend>
<label class="block"><input type="radio" name="completeStatus" value="initial" checked="checked">Initial (default)</label>
<label class="block"><input type="radio" name="completeStatus" value="processing">Processing</label>
<label class="block"><input type="radio" name="completeStatus" value="success">Success</label>
<label class="block"><input type="radio" name="completeStatus" value="fail">Fail</label>
<label class="block"><input type="radio" name="completeStatus" value="unknown">Unknown</label>
<label class="block"><input type="radio" name="completeStatus" value="timeout">Timeout</label>
<label class="block"><input type="radio" name="setCompleteStatus" value="">(default)</label>
<label class="block"><input type="radio" name="setCompleteStatus" value="processing">Processing</label>
<label class="block"><input type="radio" name="setCompleteStatus" value="fail">Fail</label>
<label class="block"><input type="radio" name="setCompleteStatus" value="unknown">Unknown</label>
<label class="block"><input type="radio" name="setCompleteStatus" value="timeout">Timeout</label>
</fieldset>
<label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
<button id="setCompleteStatus" class="wide">Set Complete Status</button>
<section class="group">
<fieldset>

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

@ -13,22 +13,33 @@ const paymentOptionsUpdater = {
this.render(state);
},
render(state) {
let options = state.request.paymentOptions;
let checkboxes = document.querySelectorAll("#paymentOptions input[type='checkbox']");
for (let input of checkboxes) {
if (options.hasOwnProperty(input.name)) {
input.checked = options[input.name];
let {
completeStatus,
paymentOptions,
} = state.request;
document.getElementById("setChangesPrevented").checked = state.changesPrevented;
let paymentOptionInputs = document.querySelectorAll("#paymentOptions input[type='checkbox']");
for (let input of paymentOptionInputs) {
if (paymentOptions.hasOwnProperty(input.name)) {
input.checked = paymentOptions[input.name];
}
}
let completeStatusInputs = document
.querySelectorAll("input[type='radio'][name='setCompleteStatus']");
for (let input of completeStatusInputs) {
input.checked = input.value == completeStatus;
}
},
};
requestStore.subscribe(paymentOptionsUpdater);
let REQUEST_1 = {
tabId: 9,
topLevelPrincipal: {URI: {displayHost: "tschaeff.github.io"}},
requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
completeStatus: "",
paymentMethods: [],
paymentDetails: {
id: "",
@ -80,6 +91,7 @@ let REQUEST_2 = {
tabId: 9,
topLevelPrincipal: {URI: {displayHost: "example.com"}},
requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
completeStatus: "",
paymentMethods: [],
paymentDetails: {
id: "",
@ -333,15 +345,9 @@ let buttonActions = {
paymentDialog.setStateFromParent({savedBasicCards: BASIC_CARDS_1});
},
setChangesAllowed() {
setChangesPrevented(evt) {
requestStore.setState({
changesPrevented: false,
});
},
setChangesPrevented() {
requestStore.setState({
changesPrevented: true,
changesPrevented: evt.target.checked,
});
},
@ -406,23 +412,23 @@ let buttonActions = {
});
},
setCompleteStatus(e) {
let input = document.querySelector("[name='completionState']:checked");
setCompleteStatus() {
let input = document.querySelector("[name='setCompleteStatus']:checked");
let completeStatus = input.value;
let request = requestStore.getState().request;
requestStore.setStateFromParent({
paymentDialog.setStateFromParent({
request: Object.assign({}, request, { completeStatus }),
});
},
};
window.addEventListener("click", function onButtonClick(evt) {
let id = evt.target.id;
let id = evt.target.id || evt.target.name;
if (!id || typeof(buttonActions[id]) != "function") {
return;
}
buttonActions[id]();
buttonActions[id](evt);
});
window.addEventListener("DOMContentLoaded", function onDCL() {
@ -433,4 +439,7 @@ window.addEventListener("DOMContentLoaded", function onDCL() {
// is manually loaded in a tab but will be shown.
document.getElementById("debugFrame").hidden = false;
}
requestStore.subscribe(paymentOptionsUpdater);
paymentOptionsUpdater.render(requestStore.getState());
});

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

@ -32,7 +32,7 @@ export let requestStore = new PaymentsStore({
// selectedStateKey: "",
},
request: {
completeStatus: "initial",
completeStatus: "",
tabId: null,
topLevelPrincipal: {URI: {displayHost: null}},
requestId: null,

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

@ -135,7 +135,7 @@
<button id="cancel">&cancelPaymentButton.label;</button>
<button id="pay"
class="primary"
data-initial-label="&approvePaymentButton.label;"
data-label="&approvePaymentButton.label;"
data-processing-label="&processingPaymentButton.label;"
data-unknown-label="&unknownPaymentButton.label;"
data-success-label="&successPaymentButton.label;"></button>

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

@ -9,6 +9,11 @@ async function setup() {
let prefilledGuids = await addSampleAddressesAndBasicCard(
[PTU.Addresses.TimBL], [PTU.BasicCards.JohnDoe]);
info("associating the card with the billing address");
formAutofillStorage.creditCards.update(prefilledGuids.card1GUID, {
billingAddressGUID: prefilledGuids.address1GUID,
}, true);
return prefilledGuids;
}

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

@ -2,7 +2,12 @@
async function setup() {
await setupFormAutofillStorage();
await addSampleAddressesAndBasicCard();
let prefilledGuids = await addSampleAddressesAndBasicCard();
info("associating the card with the billing address");
formAutofillStorage.creditCards.update(prefilledGuids.card1GUID, {
billingAddressGUID: prefilledGuids.address1GUID,
}, true);
}
add_task(async function test_change_shipping() {

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

@ -82,7 +82,7 @@ add_task(async function test_onboarding_wizard_without_saved_addresses_and_saved
is(basicCardTitle.textContent, "Add Credit Card", "Basic card page title is correctly shown");
info("Check if the correct billing address is selected in the basic card page");
PTU.DialogContentUtils.waitForState((state) => {
PTU.DialogContentUtils.waitForState(content, (state) => {
let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
return state.selectedShippingAddress == billingAddressSelect.value;
}, "Shipping address is selected as the billing address");
@ -352,7 +352,7 @@ add_task(async function test_onboarding_wizard_with_requestShipping_turned_off()
ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
info("Check if the correct billing address is selected in the basic card page");
PTU.DialogContentUtils.waitForState((state) => {
PTU.DialogContentUtils.waitForState(content, (state) => {
let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
return state["basic-card-page"].billingAddressGUID == billingAddressSelect.value;
}, "Billing Address is correctly shown");

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

@ -313,11 +313,27 @@ function cleanupFormAutofillStorage() {
}
add_task(async function setup_head() {
SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) {
if (msg.isWarning || !msg.errorMessage) {
// Ignore warnings and non-errors.
return;
}
if (msg.category == "CSP_CSPViolationWithURI" && msg.errorMessage.includes("at inline")) {
// Ignore unknown CSP error.
return;
}
if (msg.errorMessage.match(/docShell is null.*BrowserUtils.jsm/)) {
// Bug 1478142 - Console spam from the Find Toolbar.
return;
}
ok(false, msg.message || msg.errorMessage);
});
await setupFormAutofillStorage();
registerCleanupFunction(function cleanup() {
paymentSrv.cleanup();
cleanupFormAutofillStorage();
Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
SpecialPowers.postConsoleSentinel();
});
});

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

@ -1,7 +1,7 @@
"use strict";
/* exported asyncElementRendered, promiseStateChange, promiseContentToChromeMessage, deepClone,
PTU */
PTU, registerConsoleFilter */
const PTU = SpecialPowers.Cu.import("resource://testing-common/PaymentTestUtils.jsm", {})
.PaymentTestUtils;
@ -46,3 +46,35 @@ function promiseContentToChromeMessage(messageType) {
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* If filterFunction is a function which returns true given a console message
* then the test won't fail from that message.
*/
let filterFunction = null;
function registerConsoleFilter(filterFn) {
filterFunction = filterFn;
}
// Listen for errors to fail tests
SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) {
if (msg.isWarning || !msg.errorMessage || msg.errorMessage == "paymentRequest.xhtml:") {
// Ignore warnings and non-errors.
return;
}
if (msg.category == "CSP_CSPViolationWithURI" && msg.errorMessage.includes("at inline")) {
// Ignore unknown CSP error.
return;
}
if (msg.message == "SENTINEL") {
filterFunction = null;
}
if (filterFunction && filterFunction(msg)) {
return;
}
ok(false, msg.message || msg.errorMessage);
});
SimpleTest.registerCleanupFunction(function cleanup() {
SpecialPowers.postConsoleSentinel();
});

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

@ -118,6 +118,10 @@ let elDialog;
let initialState;
add_task(async function setup_once() {
registerConsoleFilter(function consoleFilter(msg) {
return msg.errorMessage.includes("selectedPayerAddress option a9e830667189 does not exist");
});
let templateFrame = document.getElementById("templateFrame");
await SimpleTest.promiseFocus(templateFrame.contentWindow);
@ -135,7 +139,7 @@ add_task(async function setup_once() {
let {request} = elDialog.requestStore.getState();
initialState = Object.assign({}, {
changesPrevented: false,
request: Object.assign({}, request, { completeStatus: "initial" }),
request: Object.assign({}, request, { completeStatus: "" }),
orderDetailsShowing: false,
});
});

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

@ -58,7 +58,7 @@ async function setup() {
let {request} = el1.requestStore.getState();
await el1.requestStore.setState({
changesPrevented: false,
request: Object.assign({}, request, {completeStatus: "initial"}),
request: Object.assign({}, request, {completeStatus: ""}),
orderDetailsShowing: false,
page: {
id: "payment-summary",
@ -135,7 +135,7 @@ add_task(async function test_changesPrevented() {
add_task(async function test_initial_completeStatus() {
await setup();
let {request, page} = el1.requestStore.getState();
is(request.completeStatus, "initial", "completeStatus is initially initial");
is(request.completeStatus, "", "completeStatus is initially empty");
let payButton = document.getElementById("pay");
is(payButton, document.querySelector(`#${page.id} button.primary`),

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

@ -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

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

@ -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

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

@ -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.

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

@ -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});
}

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

@ -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}]
}
}
```

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

@ -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"]

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

@ -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"}
]
},

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

@ -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>

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

@ -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 {

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

@ -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;
}

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

@ -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;

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

@ -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;
}
}

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

@ -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;
}
}

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

@ -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);

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

@ -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 {

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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 {

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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 {

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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>

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

@ -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;
}
}
}

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

@ -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;
}
}

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

@ -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"

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

@ -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;

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

@ -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

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

@ -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}.

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

@ -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} قبول می‌کنید.

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

@ -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}.

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

@ -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}.

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

@ -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} સાથે સંમત થાઓ છો.

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

@ -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

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

@ -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.

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

@ -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=ಈ ಹಂತವನ್ನು ಹಾರಿಸಿ

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

@ -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}.

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

@ -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.

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

@ -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}.

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

@ -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=جاری رکھیں

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

@ -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

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

@ -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",

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

@ -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"
};

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

@ -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": "نکات حریم‌خصوصی",

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

@ -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",

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

@ -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é",

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

@ -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": "ખાનગી સૂચના",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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": "संपादित करें",

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

@ -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": "არა, გმადლობთ",

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

@ -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": "ಈ ಹಂತವನ್ನು ಹಾರಿಸಿ"
};

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

@ -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",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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"
};

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

@ -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",

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

@ -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"
};

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

@ -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");
});

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

@ -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);
});
});
});
});

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

@ -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]));
});
});
});

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

@ -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";

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

@ -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);

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

@ -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

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

@ -6,37 +6,36 @@
// Test that when animations displayed in the timeline are running on the
// compositor, they get a special icon and information in the tooltip.
requestLongerTimeout(2);
add_task(async function() {
await addTab(URL_ROOT + "doc_simple_animation.html");
await removeAnimatedElementsExcept(
[".compositor-all", ".compositor-notall", ".no-compositor"]);
const { animationInspector, inspector, panel } = await openAnimationInspector();
info("Select a test node we know has an animation running on the compositor");
await selectNodeAndWaitForAnimations(".compositor-all", inspector);
const summaryGraphEl = panel.querySelector(".animation-summary-graph");
ok(summaryGraphEl.classList.contains("compositor"),
info("Check animation whose all properties are running on compositor");
const summaryGraphAllEl = findSummaryGraph(".compositor-all", panel);
ok(summaryGraphAllEl.classList.contains("compositor"),
"The element has the compositor css class");
ok(hasTooltip(summaryGraphEl,
ok(hasTooltip(summaryGraphAllEl,
ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
"The element has the right tooltip content");
info("Select a node we know doesn't have an animation on the compositor");
await selectNodeAndWaitForAnimations(".no-compositor", inspector);
ok(!summaryGraphEl.classList.contains("compositor"),
info("Check animation is not running on compositor");
const summaryGraphNoEl = findSummaryGraph(".no-compositor", panel);
ok(!summaryGraphNoEl.classList.contains("compositor"),
"The element does not have the compositor css class");
ok(!hasTooltip(summaryGraphEl,
ok(!hasTooltip(summaryGraphNoEl,
ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
"The element does not have oncompositor tooltip content");
ok(!hasTooltip(summaryGraphEl,
ok(!hasTooltip(summaryGraphNoEl,
ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
"The element does not have oncompositor tooltip content");
info("Select a node we know has animation on the compositor and not on the compositor");
info("Select a node has animation whose some properties are running on compositor");
await selectNodeAndWaitForAnimations(".compositor-notall", inspector);
const summaryGraphEl = panel.querySelector(".animation-summary-graph");
ok(summaryGraphEl.classList.contains("compositor"),
"The element has the compositor css class");
ok(hasTooltip(summaryGraphEl,
@ -71,6 +70,11 @@ add_task(async function() {
"The element should have the compositor css class after resuming");
});
function findSummaryGraph(selector, panel) {
const animationItemEl = findAnimationItemElementsByTargetSelector(panel, selector);
return animationItemEl.querySelector(".animation-summary-graph");
}
function hasTooltip(summaryGraphEl, expected) {
const tooltip = summaryGraphEl.getAttribute("title");
return tooltip.includes(expected);

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

@ -24,11 +24,27 @@ const TEST_URL = `data:text/html;charset=utf-8,
});
</script>`;
// Test reveal link with mouse navigation
add_task(async function() {
const checkWithMouse = checkRevealLink.bind(null, clickOnRevealLink);
await testRevealLink(checkWithMouse, checkWithMouse);
});
// Test reveal link with keyboard navigation (Enter and Spacebar keys)
add_task(async function() {
const checkWithEnter = checkRevealLink.bind(null,
keydownOnRevealLink.bind(null, "KEY_Enter"));
const checkWithSpacebar = checkRevealLink.bind(null,
keydownOnRevealLink.bind(null, " "));
await testRevealLink(checkWithEnter, checkWithSpacebar);
});
async function testRevealLink(revealFnFirst, revealFnSecond) {
await enableWebComponents();
const {inspector} = await openInspectorForURL(TEST_URL);
const {markup} = inspector;
const { inspector } = await openInspectorForURL(TEST_URL);
const { markup } = inspector;
info("Find and expand the test-component shadow DOM host.");
const hostFront = await getNodeFront("test-component", inspector);
@ -46,16 +62,16 @@ add_task(async function() {
const slotChildContainers = slotContainer.getChildContainers();
is(slotChildContainers.length, 2, "Expecting 2 slotted children");
await checkRevealLink(inspector, slotChildContainers[0].node);
await revealFnFirst(inspector, slotChildContainers[0].node);
is(inspector.selection.nodeFront.id, "el1", "The right node was selected");
is(hostContainer.getChildContainers()[1].node, inspector.selection.nodeFront);
await checkRevealLink(inspector, slotChildContainers[1].node);
await revealFnSecond(inspector, slotChildContainers[1].node);
is(inspector.selection.nodeFront.id, "el2", "The right node was selected");
is(hostContainer.getChildContainers()[2].node, inspector.selection.nodeFront);
});
}
async function checkRevealLink(inspector, node) {
async function checkRevealLink(actionFn, inspector, node) {
const slottedContainer = inspector.markup.getContainer(node, true);
info("Select the slotted container for the element");
await selectNode(node, inspector, "no-reason", true);
@ -63,8 +79,11 @@ async function checkRevealLink(inspector, node) {
ok(inspector.markup.getSelectedContainer().isSlotted(),
"The selected container is slotted");
const link = slottedContainer.elt.querySelector(".reveal-link");
is(link.getAttribute("role"), "link", "Reveal link has the role=link attribute");
info("Click on the reveal link and wait for the new node to be selected");
await clickOnRevealLink(inspector, slottedContainer);
await actionFn(inspector, slottedContainer);
const selectedFront = inspector.selection.nodeFront;
is(selectedFront, node, "The same node front is still selected");
ok(!inspector.selection.isSlotted(), "The selection is not the slotted version");

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

@ -723,3 +723,28 @@ async function clickOnRevealLink(inspector, container) {
await onSelection;
}
/**
* Hit `key` on the reveal link in the provided slotted container.
* Will resolve when selection emits "new-node-front".
*/
async function keydownOnRevealLink(key, inspector, container) {
const revealLink = container.elt.querySelector(".reveal-link");
const win = inspector.markup.doc.defaultView;
const root = inspector.markup.getContainer(inspector.markup._rootNode);
root.elt.focus();
// we need to go through a ENTER + TAB key sequence to focus on
// the .reveal-link element with the keyboard
const revealFocused = once(revealLink, "focus");
EventUtils.synthesizeKey("KEY_Enter", {}, win);
EventUtils.synthesizeKey("KEY_Tab", {}, win);
info("Waiting for .reveal-link to be focused");
await revealFocused;
// hit `key` on the .reveal-link
const onSelection = inspector.selection.once("new-node-front");
EventUtils.synthesizeKey(key, {}, win);
await onSelection;
}

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

@ -34,14 +34,25 @@ SlottedNodeContainer.prototype = extend(MarkupContainer.prototype, {
event.stopPropagation();
},
_revealFromSlot() {
const reason = "reveal-from-slot";
this.markup.inspector.selection.setNodeFront(this.node, { reason });
this.markup.telemetry.scalarSet("devtools.shadowdom.reveal_link_clicked", true);
},
_onKeyDown: function(event) {
const isActionKey = event.code == "Enter" || event.code == "Space";
if (event.target.classList.contains("reveal-link") && isActionKey) {
this._revealFromSlot();
}
},
onContainerClick: async function(event) {
if (!event.target.classList.contains("reveal-link")) {
return;
}
const reason = "reveal-from-slot";
this.markup.inspector.selection.setNodeFront(this.node, { reason });
this.markup.telemetry.scalarSet("devtools.shadowdom.reveal_link_clicked", true);
this._revealFromSlot();
},
isDraggable: function() {

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

@ -30,8 +30,10 @@ SlottedNodeEditor.prototype = {
this.elt.appendChild(this.tag);
this.revealLink = doc.createElement("span");
this.revealLink.classList.add("reveal-link");
this.revealLink.setAttribute("role", "link");
this.revealLink.setAttribute("tabindex", -1);
this.revealLink.title = INSPECTOR_L10N.getStr("markupView.revealLink.tooltip");
this.revealLink.classList.add("reveal-link");
this.elt.appendChild(this.revealLink);
},

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

@ -35,7 +35,6 @@ skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes)
[browser_styleinspector_refresh_when_style_changes.js]
[browser_styleinspector_tooltip-background-image.js]
[browser_styleinspector_tooltip-closes-on-new-selection.js]
skip-if = e10s # Bug 1111546 (e10s)
[browser_styleinspector_tooltip-longhand-fontfamily.js]
[browser_styleinspector_tooltip-multiple-background-images.js]
[browser_styleinspector_tooltip-shorthand-fontfamily.js]

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

@ -91,17 +91,22 @@ DocumentTimeline::GetCurrentTimeStamp() const
? refreshTime
: mLastRefreshDriverTime;
nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
// If we don't have a refresh driver and we've never had one use the
// timeline's zero time.
if (result.IsNull()) {
nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
if (timing) {
result = timing->GetNavigationStartTimeStamp();
// Also, let this time represent the current refresh time. This way
// we'll save it as the last refresh time and skip looking up
// navigation timing each time.
refreshTime = result;
}
// In addition, it's possible that our refresh driver's timestamp is behind
// from the navigation start time because the refresh driver timestamp is
// sent through an IPC call whereas the navigation time is set by calling
// TimeStamp::Now() directly. In such cases we also use the timeline's zero
// time.
if (timing &&
(result.IsNull() ||
result < timing->GetNavigationStartTimeStamp())) {
result = timing->GetNavigationStartTimeStamp();
// Also, let this time represent the current refresh time. This way
// we'll save it as the last refresh time and skip looking up
// navigation start time each time.
refreshTime = result;
}
if (!refreshTime.IsNull()) {
@ -284,7 +289,7 @@ TimeStamp
DocumentTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const
{
TimeStamp result;
RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
if (MOZ_UNLIKELY(!timing)) {
return result;
}

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

@ -27,8 +27,8 @@ test(function() {
});
async_test(function(t) {
assert_greater_than(document.timeline.currentTime, 0,
'document.timeline.currentTime is positive');
assert_greater_than_equal(document.timeline.currentTime, 0,
'document.timeline.currentTime is positive or zero');
// document.timeline.currentTime should be set even before document
// load fires. We expect this code to be run before document load and hence
// the above assertion is sufficient.

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

@ -2901,7 +2901,7 @@ nsGenericHTMLElement::NewURIFromString(const nsAString& aURISpec,
}
static bool
IsOrHasAncestorWithDisplayNone(Element* aElement, nsIPresShell* aPresShell)
IsOrHasAncestorWithDisplayNone(Element* aElement)
{
return !aElement->HasServoData() || Servo_Element_IsDisplayNone(aElement);
}
@ -2910,18 +2910,65 @@ void
nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue,
mozilla::ErrorResult& aError)
{
if (!GetPrimaryFrame(FlushType::Layout)) {
nsIPresShell* presShell = nsContentUtils::GetPresShellForContent(this);
// NOTE(emilio): We need to check the presshell is initialized in order to
// ensure the document is styled.
if (!presShell || !presShell->DidInitialize() ||
IsOrHasAncestorWithDisplayNone(this, presShell)) {
GetTextContentInternal(aValue, aError);
return;
// innerText depends on layout. For example, white space processing is
// something that happens during reflow and which must be reflected by
// innerText. So for:
//
// <div style="white-space:normal"> A B C </div>
//
// innerText should give "A B C".
//
// The approach taken here to avoid the expense of reflow is to flush style
// and then see whether it's necessary to flush layout afterwards. Flushing
// layout can be skipped if we can detect that the element or its descendants
// are not dirty.
// Obtain the composed doc to handle elements in Shadow DOM.
nsIDocument* doc = GetComposedDoc();
if (doc) {
doc->FlushPendingNotifications(FlushType::Style);
}
// Elements with `display: content` will not have a frame. To handle Shadow
// DOM, walk the flattened tree looking for parent frame.
nsIFrame* frame = GetPrimaryFrame();
if (IsDisplayContents()) {
for (Element* parent = GetFlattenedTreeParentElement();
parent;
parent = parent->GetFlattenedTreeParentElement())
{
frame = parent->GetPrimaryFrame();
if (frame) {
break;
}
}
}
nsRange::GetInnerTextNoFlush(aValue, aError, this);
// Check for dirty reflow roots in the subtree from targetFrame; this requires
// a reflow flush.
bool dirty = frame && frame->PresShell()->FrameIsAncestorOfDirtyRoot(frame);
// The way we do that is by checking whether the element has either of the two
// dirty bits (NS_FRAME_IS_DIRTY or NS_FRAME_HAS_DIRTY_DESCENDANTS) or if any
// ancestor has NS_FRAME_IS_DIRTY. We need to check for NS_FRAME_IS_DIRTY on
// ancestors since that is something that implies NS_FRAME_IS_DIRTY on all
// descendants.
dirty |= frame && frame->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN);
while (!dirty && frame) {
dirty |= frame->HasAnyStateBits(NS_FRAME_IS_DIRTY);
frame = frame->GetInFlowParent();
}
// Flush layout if we determined a reflow is required.
if (dirty && doc) {
doc->FlushPendingNotifications(FlushType::Layout);
}
if (IsOrHasAncestorWithDisplayNone(this)) {
GetTextContentInternal(aValue, aError);
} else {
nsRange::GetInnerTextNoFlush(aValue, aError, this);
}
}
void

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

@ -1,3 +1,3 @@
#define ANGLE_COMMIT_HASH "fc96a1a98357"
#define ANGLE_COMMIT_HASH "ae3b5a6552ee"
#define ANGLE_COMMIT_HASH_SIZE 12
#define ANGLE_COMMIT_DATE "2018-06-29 19:06:56 -0700"
#define ANGLE_COMMIT_DATE "2018-07-24 15:21:15 -0700"

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

@ -49,6 +49,7 @@ on big endian machines, or a byte-by-byte read if the endianess is unknown.
#include "PMurHash.h"
#include <stdint.h>
/* I used ugly type names in the header to avoid potential conflicts with
* application or system typedefs & defines. Since I'm not including any more
@ -208,7 +209,7 @@ void PMurHash32_Process(uint32_t *ph1, uint32_t *pcarry, const void *key, int le
/* This CPU does not handle unaligned word access */
/* Consume enough so that the next data byte is word aligned */
int i = -(long)ptr & 3;
int i = -(intptr_t)ptr & 3;
if(i && i <= len) {
DOBYTES(i, h1, c, n, ptr, len);
}

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

@ -1,3 +1,17 @@
commit ae3b5a6552ee26164b9a95ebb6f8e51db9494815
Author: Jacek Caban <cjacek@gmail.com>
Date: Wed Jun 27 17:35:19 2018 +0200
Fix PMurHash.cpp mingw clang 64-bit compilation.
Tested with Firefox build, upstream bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=1471632
Change-Id: I4b44847dfc69ee26cf2215b0a0b7573becfd369d
Reviewed-on: https://chromium-review.googlesource.com/1117187
Commit-Queue: Jamie Madill <jmadill@chromium.org>
Reviewed-by: Jamie Madill <jmadill@chromium.org>
commit fc96a1a98357cfa17510bcf701e99a59ab474ce0
Author: Jeff Gilbert <jgilbert@mozilla.com>
Date: Fri Jun 29 19:06:56 2018 -0700

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

@ -13,16 +13,19 @@
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/Logging.h"
#include "mozilla/Move.h"
#include "mozilla/Mutex.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Sprintf.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TimeStamp.h"
#include "nsAppRunner.h"
#include "mozilla/UniquePtr.h"
#include "nsAppRunner.h"
#include "nsAutoPtr.h"
#include "nsContentUtils.h"
#include "nsDataHashtable.h"
#include "nsDebug.h"
#include "nsISupportsImpl.h"
#include "nsContentUtils.h"
#include "nsPrintfCString.h"
#include <math.h>
#ifdef MOZ_TASK_TRACER
@ -502,6 +505,103 @@ public:
NS_IMPL_ISUPPORTS(PendingResponseReporter, nsIMemoryReporter)
class ChannelCountReporter final : public nsIMemoryReporter
{
~ChannelCountReporter() = default;
struct ChannelCounts {
size_t mNow;
size_t mMax;
ChannelCounts() : mNow(0), mMax(0) { }
void Inc() {
++mNow;
if (mMax < mNow) {
mMax = mNow;
}
}
void Dec() {
MOZ_ASSERT(mNow > 0);
--mNow;
}
};
using CountTable = nsDataHashtable<nsDepCharHashKey, ChannelCounts>;
static StaticMutex sChannelCountMutex;
static CountTable* sChannelCounts;
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_IMETHOD
CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData,
bool aAnonymize) override
{
StaticMutexAutoLock countLock(sChannelCountMutex);
if (!sChannelCounts) {
return NS_OK;
}
for (auto iter = sChannelCounts->Iter(); !iter.Done(); iter.Next()) {
nsPrintfCString pathNow("ipc-channels/%s", iter.Key());
nsPrintfCString pathMax("ipc-channels-peak/%s", iter.Key());
nsPrintfCString descNow("Number of IPC channels for"
" top-level actor type %s", iter.Key());
nsPrintfCString descMax("Peak number of IPC channels for"
" top-level actor type %s", iter.Key());
aHandleReport->Callback(EmptyCString(), pathNow, KIND_OTHER,
UNITS_COUNT, iter.Data().mNow, descNow,
aData);
aHandleReport->Callback(EmptyCString(), pathMax, KIND_OTHER,
UNITS_COUNT, iter.Data().mMax, descMax,
aData);
}
return NS_OK;
}
static void
Increment(const char* aName)
{
StaticMutexAutoLock countLock(sChannelCountMutex);
if (!sChannelCounts) {
sChannelCounts = new CountTable;
}
sChannelCounts->GetOrInsert(aName).Inc();
}
static void
Decrement(const char* aName)
{
StaticMutexAutoLock countLock(sChannelCountMutex);
MOZ_ASSERT(sChannelCounts);
sChannelCounts->GetOrInsert(aName).Dec();
}
};
StaticMutex ChannelCountReporter::sChannelCountMutex;
ChannelCountReporter::CountTable* ChannelCountReporter::sChannelCounts;
NS_IMPL_ISUPPORTS(ChannelCountReporter, nsIMemoryReporter)
// In child processes, the first MessageChannel is created before
// XPCOM is initialized enough to construct the memory reporter
// manager. This retries every time a MessageChannel is constructed,
// which is good enough in practice.
template<class Reporter>
static void TryRegisterStrongMemoryReporter()
{
static Atomic<bool> registered;
if (registered.compareExchange(false, true)) {
RefPtr<Reporter> reporter = new Reporter();
if (NS_FAILED(RegisterStrongMemoryReporter(reporter))) {
registered = false;
}
}
}
Atomic<size_t> MessageChannel::gUnresolvedResponses;
MessageChannel::MessageChannel(const char* aName,
@ -510,6 +610,7 @@ MessageChannel::MessageChannel(const char* aName,
mListener(aListener),
mChannelState(ChannelClosed),
mSide(UnknownSide),
mIsCrossProcess(false),
mLink(nullptr),
mWorkerLoop(nullptr),
mChannelErrorTask(nullptr),
@ -553,10 +654,8 @@ MessageChannel::MessageChannel(const char* aName,
MOZ_RELEASE_ASSERT(mEvent, "CreateEvent failed! Nothing is going to work!");
#endif
static Atomic<bool> registered;
if (registered.compareExchange(false, true)) {
RegisterStrongMemoryReporter(new PendingResponseReporter());
}
TryRegisterStrongMemoryReporter<PendingResponseReporter>();
TryRegisterStrongMemoryReporter<ChannelCountReporter>();
}
MessageChannel::~MessageChannel()
@ -749,6 +848,9 @@ MessageChannel::Clear()
mPendingResponses.clear();
mWorkerLoop = nullptr;
if (mLink != nullptr && mIsCrossProcess) {
ChannelCountReporter::Decrement(mName);
}
delete mLink;
mLink = nullptr;
@ -787,6 +889,8 @@ MessageChannel::Open(Transport* aTransport, MessageLoop* aIOLoop, Side aSide)
ProcessLink *link = new ProcessLink(this);
link->Open(aTransport, aIOLoop, aSide); // :TODO: n.b.: sets mChild
mLink = link;
mIsCrossProcess = true;
ChannelCountReporter::Increment(mName);
return true;
}

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

@ -633,6 +633,7 @@ private:
ChannelState mChannelState;
RefPtr<RefCountedMonitor> mMonitor;
Side mSide;
bool mIsCrossProcess;
MessageLink* mLink;
MessageLoop* mWorkerLoop; // thread where work is done
RefPtr<CancelableRunnable> mChannelErrorTask; // NotifyMaybeChannelError runnable

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

@ -4563,6 +4563,27 @@ PresShell::NotifyCounterStylesAreDirty()
mFrameConstructor->NotifyCounterStylesAreDirty();
}
bool
nsIPresShell::FrameIsAncestorOfDirtyRoot(nsIFrame* aFrame) const
{
MOZ_ASSERT(aFrame);
// Look for a path from any dirty roots to aFrame, following GetParent().
// This check mirrors what FrameNeedsReflow() would have done if the reflow
// root didn't get in the way.
for (nsIFrame* dirtyFrame : mDirtyRoots) {
while (dirtyFrame) {
if (dirtyFrame == aFrame) {
return true;
}
dirtyFrame = dirtyFrame->GetParent();
}
}
return false;
}
void
PresShell::ReconstructFrames()
{

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

@ -784,9 +784,6 @@ private:
// we finish reflowing mCurrentReflowRoot.
nsTHashtable<nsPtrHashKey<nsIFrame> > mFramesToDirty;
// Reflow roots that need to be reflowed.
nsTArray<nsIFrame*> mDirtyRoots;
nsTArray<nsAutoPtr<DelayedEvent> > mDelayedEvents;
private:
nsIFrame* mCurrentEventFrame;

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

@ -503,6 +503,8 @@ public:
virtual void NotifyCounterStylesAreDirty() = 0;
bool FrameIsAncestorOfDirtyRoot(nsIFrame* aFrame) const;
/**
* Destroy the frames for aElement, and reconstruct them asynchronously if
* needed.
@ -1761,6 +1763,9 @@ protected:
// A hash table of heap allocated weak frames.
nsTHashtable<nsPtrHashKey<WeakFrame>> mWeakFrames;
// Reflow roots that need to be reflowed.
nsTArray<nsIFrame*> mDirtyRoots;
#ifdef MOZ_GECKO_PROFILER
// These two fields capture call stacks of any changes that require a restyle
// or a reflow. Only the first change per restyle / reflow is recorded (the

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

@ -1237,16 +1237,20 @@ ReflowInput::CalculateBorderPaddingMargin(
nscoord start, end;
// We have to compute the start and end values
if (eStyleUnit_Auto == mStyleMargin->mMargin.GetUnit(startSide)) {
// XXX FIXME (or does CalculateBlockSideMargins do this?)
start = 0; // just ignore
// We set this to 0 for now, and fix it up later in
// InitAbsoluteConstraints (which is caller of this function, via
// CalculateHypotheticalPosition).
start = 0;
} else {
start = nsLayoutUtils::
ComputeCBDependentValue(aContainingBlockSize,
mStyleMargin->mMargin.Get(startSide));
}
if (eStyleUnit_Auto == mStyleMargin->mMargin.GetUnit(endSide)) {
// XXX FIXME (or does CalculateBlockSideMargins do this?)
end = 0; // just ignore
// We set this to 0 for now, and fix it up later in
// InitAbsoluteConstraints (which is caller of this function, via
// CalculateHypotheticalPosition).
end = 0;
} else {
end = nsLayoutUtils::
ComputeCBDependentValue(aContainingBlockSize,
@ -1765,6 +1769,10 @@ ReflowInput::InitAbsoluteConstraints(nsPresContext* aPresContext,
ComputedLogicalBorderPadding().ConvertTo(cbwm, wm);
bool iSizeIsAuto = eStyleUnit_Auto == mStylePosition->ISize(cbwm).GetUnit();
bool marginIStartIsAuto = false;
bool marginIEndIsAuto = false;
bool marginBStartIsAuto = false;
bool marginBEndIsAuto = false;
if (iStartIsAuto) {
// We know 'right' is not 'auto' anymore thanks to the hypothetical
// box code above.
@ -1835,9 +1843,9 @@ ReflowInput::InitAbsoluteConstraints(nsPresContext* aPresContext,
nscoord availMarginSpace =
aCBSize.ISize(cbwm) - offsets.IStartEnd(cbwm) - margin.IStartEnd(cbwm) -
borderPadding.IStartEnd(cbwm) - computedSize.ISize(cbwm);
bool marginIStartIsAuto =
marginIStartIsAuto =
eStyleUnit_Auto == mStyleMargin->mMargin.GetIStartUnit(cbwm);
bool marginIEndIsAuto =
marginIEndIsAuto =
eStyleUnit_Auto == mStyleMargin->mMargin.GetIEndUnit(cbwm);
if (marginIStartIsAuto) {
@ -1923,9 +1931,9 @@ ReflowInput::InitAbsoluteConstraints(nsPresContext* aPresContext,
// * we're dealing with a replaced element
// * bsize was constrained by min- or max-bsize.
nscoord availMarginSpace = autoBSize - computedSize.BSize(cbwm);
bool marginBStartIsAuto =
marginBStartIsAuto =
eStyleUnit_Auto == mStyleMargin->mMargin.GetBStartUnit(cbwm);
bool marginBEndIsAuto =
marginBEndIsAuto =
eStyleUnit_Auto == mStyleMargin->mMargin.GetBEndUnit(cbwm);
if (marginBStartIsAuto) {
@ -1954,7 +1962,19 @@ ReflowInput::InitAbsoluteConstraints(nsPresContext* aPresContext,
ComputedISize() = computedSize.ConvertTo(wm, cbwm).ISize(wm);
SetComputedLogicalOffsets(offsets.ConvertTo(wm, cbwm));
SetComputedLogicalMargin(margin.ConvertTo(wm, cbwm));
LogicalMargin marginInOurWM = margin.ConvertTo(wm, cbwm);
SetComputedLogicalMargin(marginInOurWM);
// If we have auto margins, update our UsedMarginProperty. The property
// will have already been created by InitOffsets if it is needed.
if (marginIStartIsAuto || marginIEndIsAuto ||
marginBStartIsAuto || marginBEndIsAuto) {
nsMargin* propValue = mFrame->GetProperty(nsIFrame::UsedMarginProperty());
MOZ_ASSERT(propValue, "UsedMarginProperty should have been created "
"by InitOffsets.");
*propValue = marginInOurWM.GetPhysicalMargin(wm);
}
}
// This will not be converted to abstract coordinates because it's only

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

@ -20,3 +20,4 @@ test-pref(ui.prefersReducedMotion,0) == mq_prefers_reduced_motion_no_preference.
test-pref(ui.prefersReducedMotion,1) == mq_prefers_reduced_motion_no_preference.html about:blank
test-pref(ui.prefersReducedMotion,0) == mq_prefers_reduced_motion_reduce.html about:blank
test-pref(ui.prefersReducedMotion,1) == mq_prefers_reduced_motion_reduce.html greenbox.html
test-pref(privacy.resistFingerprinting,true) test-pref(ui.prefersReducedMotion,1) == mq_prefers_reduced_motion_reduce.html about:blank

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

@ -525,6 +525,13 @@ GetPrefersReducedMotion(nsIDocument* aDocument,
const nsMediaFeature* aFeature,
nsCSSValue& aResult)
{
const bool isAccessibleFromContentPages =
!(aFeature->mReqFlags & nsMediaFeature::eUserAgentAndChromeOnly);
if (isAccessibleFromContentPages &&
nsContentUtils::ShouldResistFingerprinting(aDocument)) {
return;
}
StylePrefersReducedMotion prefersReducedMotion =
StylePrefersReducedMotion::NoPreference;

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

@ -152,6 +152,7 @@ support-files = file_bug1089417_iframe.html
[test_bug1203766.html]
[test_bug1232829.html]
[test_bug1292447.html]
[test_bug1330375.html]
[test_bug1371488.html]
[test_bug1375944.html]
support-files = file_bug1375944.html Ahem.ttf

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше