зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound
This commit is contained in:
Коммит
7af37a52bd
|
@ -479,6 +479,11 @@ pref("browser.tabs.drawInTitlebar", false);
|
|||
pref("browser.tabs.drawInTitlebar", true);
|
||||
#endif
|
||||
|
||||
// false - disable the tabbar session restore button
|
||||
// true - enable the tabbar session restore button
|
||||
// To be enabled with shield
|
||||
pref("browser.tabs.restorebutton", false);
|
||||
|
||||
// When tabs opened by links in other tabs via a combination of
|
||||
// browser.link.open_newwindow being set to 3 and target="_blank" etc are
|
||||
// closed:
|
||||
|
|
|
@ -8168,16 +8168,47 @@ var RestoreLastSessionObserver = {
|
|||
init() {
|
||||
if (SessionStore.canRestoreLastSession &&
|
||||
!PrivateBrowsingUtils.isWindowPrivate(window)) {
|
||||
if (Services.prefs.getBoolPref("browser.tabs.restorebutton")) {
|
||||
let {restoreTabsButton} = gBrowser.tabContainer;
|
||||
let restoreTabsButtonWrapper = restoreTabsButton.parentNode;
|
||||
restoreTabsButtonWrapper.setAttribute("session-exists", "true");
|
||||
gBrowser.tabContainer.updateSessionRestoreVisibility();
|
||||
gBrowser.tabContainer.addEventListener("TabOpen", this);
|
||||
}
|
||||
Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", true);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "TabOpen":
|
||||
this.removeRestoreButton();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
removeRestoreButton() {
|
||||
let {restoreTabsButton, restoreTabsButtonWrapperWidth} = gBrowser.tabContainer;
|
||||
let restoreTabsButtonWrapper = restoreTabsButton.parentNode;
|
||||
restoreTabsButtonWrapper.removeAttribute("session-exists");
|
||||
gBrowser.tabContainer.addEventListener("transitionend", function maxWidthTransitionHandler(e) {
|
||||
if (e.propertyName == "max-width") {
|
||||
gBrowser.tabContainer.updateSessionRestoreVisibility();
|
||||
gBrowser.tabContainer.removeEventListener("transitionend", maxWidthTransitionHandler);
|
||||
}
|
||||
});
|
||||
restoreTabsButton.style.maxWidth = `${restoreTabsButtonWrapperWidth}px`;
|
||||
requestAnimationFrame(() => restoreTabsButton.style.maxWidth = 0);
|
||||
gBrowser.tabContainer.removeEventListener("TabOpen", this);
|
||||
},
|
||||
|
||||
observe() {
|
||||
// The last session can only be restored once so there's
|
||||
// no way we need to re-enable our menu item.
|
||||
Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", false);
|
||||
this.removeRestoreButton();
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||
|
|
|
@ -5886,6 +5886,13 @@
|
|||
onmouseover="document.getBindingParent(this)._enterNewTab();"
|
||||
onmouseout="document.getBindingParent(this)._leaveNewTab();"
|
||||
tooltip="dynamic-shortcut-tooltip"/>
|
||||
<xul:hbox class="restore-tabs-button-wrapper"
|
||||
anonid="restore-tabs-button-wrapper">
|
||||
<xul:toolbarbutton anonid="restore-tabs-button"
|
||||
class="restore-tabs-button"
|
||||
onclick="SessionStore.restoreLastSession();"/>
|
||||
</xul:hbox>
|
||||
|
||||
<xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
|
||||
style="width: 0;"/>
|
||||
</xul:arrowscrollbox>
|
||||
|
@ -5896,6 +5903,9 @@
|
|||
<![CDATA[
|
||||
this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
|
||||
|
||||
let { restoreTabsButton } = this;
|
||||
restoreTabsButton.setAttribute("label", this.tabbrowser.mStringBundle.getString("tabs.restoreLastTabs"));
|
||||
|
||||
var tab = this.firstChild;
|
||||
tab.label = this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle");
|
||||
tab.setAttribute("onerror", "this.removeAttribute('image');");
|
||||
|
@ -5938,6 +5948,56 @@
|
|||
<field name="_beforeHoveredTab">null</field>
|
||||
<field name="_afterHoveredTab">null</field>
|
||||
<field name="_hoveredTab">null</field>
|
||||
<field name="restoreTabsButton">
|
||||
document.getAnonymousElementByAttribute(this, "anonid", "restore-tabs-button");
|
||||
</field>
|
||||
<field name="_restoreTabsButtonWrapperWidth">0</field>
|
||||
<field name="windowUtils">
|
||||
window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
</field>
|
||||
|
||||
<property name="restoreTabsButtonWrapperWidth" readonly="true">
|
||||
<getter>
|
||||
if (!this._restoreTabsButtonWrapperWidth) {
|
||||
this._restoreTabsButtonWrapperWidth = this.windowUtils
|
||||
.getBoundsWithoutFlushing(this.restoreTabsButton.parentNode)
|
||||
.width;
|
||||
}
|
||||
return this._restoreTabsButtonWrapperWidth;
|
||||
</getter>
|
||||
</property>
|
||||
|
||||
<method name="updateSessionRestoreVisibility">
|
||||
<body><![CDATA[
|
||||
let {restoreTabsButton, restoreTabsButtonWrapperWidth, windowUtils, mTabstripWidth} = this;
|
||||
let restoreTabsButtonWrapper = restoreTabsButton.parentNode;
|
||||
|
||||
if (!restoreTabsButtonWrapper.getAttribute("session-exists")) {
|
||||
restoreTabsButtonWrapper.removeAttribute("shown");
|
||||
return;
|
||||
}
|
||||
|
||||
let newTabButton = document.getAnonymousElementByAttribute(
|
||||
this, "anonid", "tabs-newtab-button");
|
||||
|
||||
// If there are no pinned tabs it will multiply by 0 and result in 0
|
||||
let pinnedTabsWidth = windowUtils.getBoundsWithoutFlushing(this.firstChild).width * this._lastNumPinned;
|
||||
|
||||
let numUnpinnedTabs = this.childNodes.length - this._lastNumPinned;
|
||||
let unpinnedTabsWidth = windowUtils.getBoundsWithoutFlushing(this.lastChild).width * numUnpinnedTabs;
|
||||
|
||||
let tabbarUsedSpace = pinnedTabsWidth + unpinnedTabsWidth
|
||||
+ windowUtils.getBoundsWithoutFlushing(newTabButton).width;
|
||||
|
||||
// Subtract the elements' widths from the available space to ensure
|
||||
// that showing the restoreTabsButton won't cause any overflow.
|
||||
if ((mTabstripWidth - tabbarUsedSpace) > restoreTabsButtonWrapperWidth) {
|
||||
restoreTabsButtonWrapper.setAttribute("shown", "true");
|
||||
} else {
|
||||
restoreTabsButtonWrapper.removeAttribute("shown");
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="observe">
|
||||
<parameter name="aSubject"/>
|
||||
|
@ -6468,6 +6528,7 @@
|
|||
this.adjustTabstrip();
|
||||
this._handleTabSelect(false);
|
||||
this.mTabstripWidth = width;
|
||||
this.updateSessionRestoreVisibility();
|
||||
}
|
||||
break;
|
||||
case "mouseout":
|
||||
|
|
|
@ -31,7 +31,7 @@ add_task(async function() {
|
|||
|
||||
// Now check that opening a link that does create a new tab works,
|
||||
// and also that it nulls out the opener.
|
||||
let pageLoadPromise = BrowserTestUtils.browserLoaded(appTab.linkedBrowser, "http://example.com/");
|
||||
let pageLoadPromise = BrowserTestUtils.browserLoaded(appTab.linkedBrowser, false, "http://example.com/");
|
||||
await BrowserTestUtils.loadURI(appTab.linkedBrowser, "http://example.com/");
|
||||
info("Started loading example.com");
|
||||
await pageLoadPromise;
|
||||
|
|
|
@ -11,9 +11,9 @@ add_task(async function() {
|
|||
partialURLTab = BrowserTestUtils.addTab(gBrowser);
|
||||
testURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
|
||||
|
||||
let loaded1 = BrowserTestUtils.browserLoaded(deletedURLTab.linkedBrowser, testURL);
|
||||
let loaded2 = BrowserTestUtils.browserLoaded(fullURLTab.linkedBrowser, testURL);
|
||||
let loaded3 = BrowserTestUtils.browserLoaded(partialURLTab.linkedBrowser, testURL);
|
||||
let loaded1 = BrowserTestUtils.browserLoaded(deletedURLTab.linkedBrowser, false, testURL);
|
||||
let loaded2 = BrowserTestUtils.browserLoaded(fullURLTab.linkedBrowser, false, testURL);
|
||||
let loaded3 = BrowserTestUtils.browserLoaded(partialURLTab.linkedBrowser, false, testURL);
|
||||
deletedURLTab.linkedBrowser.loadURI(testURL);
|
||||
fullURLTab.linkedBrowser.loadURI(testURL);
|
||||
partialURLTab.linkedBrowser.loadURI(testURL);
|
||||
|
|
|
@ -27,7 +27,7 @@ add_task(async function() {
|
|||
await cxmenuPromise;
|
||||
let menuitem = document.getAnonymousElementByAttribute(textBox,
|
||||
"anonid", "paste-and-go");
|
||||
let browserLoadedPromise = BrowserTestUtils.browserLoaded(browser, url.replace(/\n/g, ""));
|
||||
let browserLoadedPromise = BrowserTestUtils.browserLoaded(browser, false, url.replace(/\n/g, ""));
|
||||
EventUtils.synthesizeMouseAtCenter(menuitem, {});
|
||||
// Using toSource in order to get the newlines escaped:
|
||||
info("Paste and go, loading " + url.toSource());
|
||||
|
|
|
@ -51,7 +51,7 @@ add_task(async function openKeywordBookmarkWithWindowOpen() {
|
|||
let browser = tab.linkedBrowser;
|
||||
if (!browser.currentURI || browser.currentURI.spec != TEST_URL) {
|
||||
info("Waiting for browser load");
|
||||
await BrowserTestUtils.browserLoaded(browser);
|
||||
await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
|
||||
}
|
||||
is(browser.currentURI && browser.currentURI.spec, TEST_URL, "Tab with expected URL loaded.");
|
||||
info("Waiting to remove tab");
|
||||
|
|
|
@ -180,6 +180,41 @@
|
|||
<label class="header-name" flex="1">&panePrivacySecurity.title;</label>
|
||||
</hbox>
|
||||
|
||||
<!-- Permissions -->
|
||||
<groupbox id="permissionsGroup" data-category="panePrivacy" hidden="true">
|
||||
<caption><label>&permissions.label;</label></caption>
|
||||
<separator class="thin"/>
|
||||
<hbox align="start">
|
||||
<checkbox id="popupPolicy" preference="dom.disable_open_during_load"
|
||||
label="&blockPopups.label;" accesskey="&blockPopups.accesskey;"
|
||||
onsyncfrompreference="return gPrivacyPane.updateButtons('popupPolicyButton',
|
||||
'dom.disable_open_during_load');"
|
||||
flex="1" />
|
||||
<button id="popupPolicyButton"
|
||||
class="accessory-button"
|
||||
label="&popupExceptions.label;"
|
||||
accesskey="&popupExceptions.accesskey;"
|
||||
searchkeywords="&address.label; &button.cancel.label; &button.ok.label;"/>
|
||||
</hbox>
|
||||
<hbox id="addonInstallBox">
|
||||
<checkbox id="warnAddonInstall"
|
||||
label="&warnOnAddonInstall.label;"
|
||||
accesskey="&warnOnAddonInstall.accesskey;"
|
||||
preference="xpinstall.whitelist.required"
|
||||
onsyncfrompreference="return gPrivacyPane.readWarnAddonInstall();"
|
||||
flex="1" />
|
||||
<button id="addonExceptions"
|
||||
class="accessory-button"
|
||||
label="&addonExceptions.label;"
|
||||
accesskey="&addonExceptions.accesskey;"
|
||||
searchkeywords="&address.label;
|
||||
&allow.label;
|
||||
&removepermission.label;
|
||||
&removeallpermissions.label;
|
||||
&button.cancel.label;
|
||||
&button.ok.label;"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
<!-- History -->
|
||||
<groupbox id="historyGroup" data-category="panePrivacy" hidden="true">
|
||||
|
@ -431,23 +466,6 @@
|
|||
</vbox>
|
||||
</groupbox>
|
||||
|
||||
<!-- Pop-ups -->
|
||||
<groupbox id="miscGroup" data-category="panePrivacy" hidden="true">
|
||||
<caption><label>&popups.label;</label></caption>
|
||||
<hbox align="start">
|
||||
<checkbox id="popupPolicy" preference="dom.disable_open_during_load"
|
||||
label="&blockPopups.label;" accesskey="&blockPopups.accesskey;"
|
||||
onsyncfrompreference="return gPrivacyPane.updateButtons('popupPolicyButton',
|
||||
'dom.disable_open_during_load');"
|
||||
flex="1" />
|
||||
<button id="popupPolicyButton"
|
||||
class="accessory-button"
|
||||
label="&popupExceptions.label;"
|
||||
accesskey="&popupExceptions.accesskey;"
|
||||
searchkeywords="&address.label; &button.cancel.label; &button.ok.label;"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
<!-- Notifications -->
|
||||
<groupbox id="notificationsGroup" data-category="panePrivacy" hidden="true">
|
||||
<caption><label>¬ificationsPolicy.label;</label></caption>
|
||||
|
@ -504,30 +522,9 @@
|
|||
</label>
|
||||
</groupbox>
|
||||
|
||||
<!-- addons, forgery (phishing) UI Security -->
|
||||
<groupbox id="addonsPhishingGroup" data-category="panePrivacy" hidden="true">
|
||||
<!-- Forgery (phishing) UI Security -->
|
||||
<groupbox id="phishingGroup" data-category="panePrivacy" hidden="true">
|
||||
<caption><label>&security.label;</label></caption>
|
||||
|
||||
<hbox id="addonInstallBox">
|
||||
<checkbox id="warnAddonInstall"
|
||||
label="&warnOnAddonInstall.label;"
|
||||
accesskey="&warnOnAddonInstall.accesskey;"
|
||||
preference="xpinstall.whitelist.required"
|
||||
onsyncfrompreference="return gPrivacyPane.readWarnAddonInstall();"
|
||||
flex="1" />
|
||||
<button id="addonExceptions"
|
||||
class="accessory-button"
|
||||
label="&addonExceptions.label;"
|
||||
accesskey="&addonExceptions.accesskey;"
|
||||
searchkeywords="&address.label;
|
||||
&allow.label;
|
||||
&removepermission.label;
|
||||
&removeallpermissions.label;
|
||||
&button.cancel.label;
|
||||
&button.ok.label;"/>
|
||||
</hbox>
|
||||
|
||||
<separator class="thin"/>
|
||||
<checkbox id="enableSafeBrowsing"
|
||||
label="&enableSafeBrowsing.label;"
|
||||
accesskey="&enableSafeBrowsing.accesskey;" />
|
||||
|
|
|
@ -42,7 +42,7 @@ add_task(async function() {
|
|||
*/
|
||||
add_task(async function() {
|
||||
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
|
||||
evaluateSearchResults("open pop-up windows", "miscGroup");
|
||||
evaluateSearchResults("open pop-up windows", "permissionsGroup");
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ add_task(async function() {
|
|||
*/
|
||||
add_task(async function() {
|
||||
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
|
||||
evaluateSearchResults("allowed to install add-ons", "addonsPhishingGroup");
|
||||
evaluateSearchResults("allowed to install add-ons", "permissionsGroup");
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
||||
|
||||
|
|
|
@ -4,4 +4,3 @@
|
|||
|
||||
browser.jar:
|
||||
content/browser/content-UITour.js
|
||||
content/browser/UITour-lib.js
|
||||
|
|
|
@ -111,6 +111,8 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
if (this.forceStop) {
|
||||
return;
|
||||
}
|
||||
// Sort addresses by timeLastUsed for showing the lastest used address at top.
|
||||
addresses.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
|
||||
|
||||
let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
|
||||
let result = new ProfileAutoCompleteResult(searchString,
|
||||
|
|
|
@ -128,20 +128,19 @@ FormAutofillHandler.prototype = {
|
|||
}
|
||||
this.changeFieldState(fieldDetail, "AUTO_FILLED");
|
||||
} else if (element instanceof Ci.nsIDOMHTMLSelectElement) {
|
||||
for (let option of element.options) {
|
||||
if (value === option.textContent || value === option.value) {
|
||||
// Do not change value if the option is already selected.
|
||||
// Use case for multiple select is not considered here.
|
||||
if (option.selected) {
|
||||
break;
|
||||
}
|
||||
option.selected = true;
|
||||
element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
|
||||
element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
|
||||
this.changeFieldState(fieldDetail, "AUTO_FILLED");
|
||||
break;
|
||||
}
|
||||
let option = FormAutofillUtils.findSelectOption(element, profile, fieldDetail.fieldName);
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
// Do not change value or dispatch events if the option is already selected.
|
||||
// Use case for multiple select is not considered here.
|
||||
if (!option.selected) {
|
||||
option.selected = true;
|
||||
element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
|
||||
element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
|
||||
}
|
||||
// Autofill highlight appears regardless if value is changed or not
|
||||
this.changeFieldState(fieldDetail, "AUTO_FILLED");
|
||||
}
|
||||
|
||||
// Unlike using setUserInput directly, FormFillController dispatches an
|
||||
|
@ -210,13 +209,25 @@ FormAutofillHandler.prototype = {
|
|||
let element = fieldDetail.elementWeakRef.get();
|
||||
let value = profile[fieldDetail.fieldName] || "";
|
||||
|
||||
// Skip the field that is null or already has text entered
|
||||
if (!element || element.value) {
|
||||
// Skip the field that is null
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
element.previewValue = value;
|
||||
this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
|
||||
if (element instanceof Ci.nsIDOMHTMLSelectElement) {
|
||||
// Unlike text input, select element is always previewed even if
|
||||
// the option is already selected.
|
||||
let option = FormAutofillUtils.findSelectOption(element, profile, fieldDetail.fieldName);
|
||||
element.previewValue = option ? option.text : "";
|
||||
this.changeFieldState(fieldDetail, option ? "PREVIEW" : "NORMAL");
|
||||
} else {
|
||||
// Skip the field if it already has text entered
|
||||
if (element.value) {
|
||||
continue;
|
||||
}
|
||||
element.previewValue = value;
|
||||
this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ const NAME_REFERENCES = "chrome://formautofill/content/nameReferences.js";
|
|||
|
||||
this.EXPORTED_SYMBOLS = ["FormAutofillNameUtils"];
|
||||
|
||||
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
|
||||
|
||||
// FormAutofillNameUtils is initially translated from
|
||||
// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util.cc?rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
|
||||
var FormAutofillNameUtils = {
|
||||
|
@ -204,10 +206,7 @@ var FormAutofillNameUtils = {
|
|||
if (this._dataLoaded) {
|
||||
return;
|
||||
}
|
||||
let sandbox = {};
|
||||
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader);
|
||||
scriptLoader.loadSubScript(NAME_REFERENCES, sandbox, "utf-8");
|
||||
let sandbox = FormAutofillUtils.loadDataFromScript(NAME_REFERENCES);
|
||||
Object.assign(this, sandbox.nameReferences);
|
||||
this._dataLoaded = true;
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ this.EXPORTED_SYMBOLS = ["FormAutofillUtils"];
|
|||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
const ADDRESS_REFERENCES = "chrome://formautofill/content/addressReferences.js";
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
this.FormAutofillUtils = {
|
||||
|
@ -32,6 +34,7 @@ this.FormAutofillUtils = {
|
|||
"cc-exp-month": "creditCard",
|
||||
"cc-exp-year": "creditCard",
|
||||
},
|
||||
_addressDataLoaded: false,
|
||||
|
||||
isAddressField(fieldName) {
|
||||
return !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName);
|
||||
|
@ -159,8 +162,117 @@ this.FormAutofillUtils = {
|
|||
|
||||
return [];
|
||||
},
|
||||
|
||||
loadDataFromScript(url, sandbox = {}) {
|
||||
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader);
|
||||
scriptLoader.loadSubScript(url, sandbox, "utf-8");
|
||||
return sandbox;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the option element from select element.
|
||||
* 1. Try to find the locale using the country from profile.
|
||||
* 2. First pass try to find exact match.
|
||||
* 3. Second pass try to identify values from profile value and options,
|
||||
* and look for a match.
|
||||
* @param {DOMElement} selectEl
|
||||
* @param {object} profile
|
||||
* @param {string} fieldName
|
||||
* @returns {DOMElement}
|
||||
*/
|
||||
findSelectOption(selectEl, profile, fieldName) {
|
||||
let value = profile[fieldName];
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the addressData if needed
|
||||
if (!this._addressDataLoaded) {
|
||||
Object.assign(this, this.loadDataFromScript(ADDRESS_REFERENCES));
|
||||
this._addressDataLoaded = true;
|
||||
}
|
||||
|
||||
// Set dataset to "data/US" as fallback
|
||||
let dataset = this.addressData[`data/${profile.country}`] ||
|
||||
this.addressData["data/US"];
|
||||
let collator = new Intl.Collator(dataset.lang, {sensitivity: "base", ignorePunctuation: true});
|
||||
|
||||
for (let option of selectEl.options) {
|
||||
if (this.strCompare(value, option.value, collator) ||
|
||||
this.strCompare(value, option.text, collator)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldName === "address-level1") {
|
||||
if (!Array.isArray(dataset.sub_keys)) {
|
||||
dataset.sub_keys = dataset.sub_keys.split("~");
|
||||
}
|
||||
if (!Array.isArray(dataset.sub_names)) {
|
||||
dataset.sub_names = dataset.sub_names.split("~");
|
||||
}
|
||||
let keys = dataset.sub_keys;
|
||||
let names = dataset.sub_names;
|
||||
let identifiedValue = this.identifyValue(keys, names, value, collator);
|
||||
|
||||
// No point going any further if we cannot identify value from profile
|
||||
if (identifiedValue === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Go through options one by one to find a match.
|
||||
// Also check if any option contain the address-level1 key.
|
||||
let pattern = new RegExp(`\\b${identifiedValue}\\b`, "i");
|
||||
for (let option of selectEl.options) {
|
||||
let optionValue = this.identifyValue(keys, names, option.value, collator);
|
||||
let optionText = this.identifyValue(keys, names, option.text, collator);
|
||||
if (identifiedValue === optionValue || identifiedValue === optionText || pattern.test(option.value)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldName === "country") {
|
||||
// TODO: Support matching countries (Bug 1375382)
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Try to match value with keys and names, but always return the key.
|
||||
* @param {array<string>} keys
|
||||
* @param {array<string>} names
|
||||
* @param {string} value
|
||||
* @param {object} collator
|
||||
* @returns {string}
|
||||
*/
|
||||
identifyValue(keys, names, value, collator) {
|
||||
let resultKey = keys.find(key => this.strCompare(value, key, collator));
|
||||
if (resultKey) {
|
||||
return resultKey;
|
||||
}
|
||||
|
||||
let index = names.findIndex(name => this.strCompare(value, name, collator));
|
||||
if (index !== -1) {
|
||||
return keys[index];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare if two strings are the same.
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
* @param {object} collator
|
||||
* @returns {boolean}
|
||||
*/
|
||||
strCompare(a = "", b = "", collator) {
|
||||
return !collator.compare(a, b);
|
||||
},
|
||||
};
|
||||
|
||||
this.log = null;
|
||||
this.FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* exported addressData */
|
||||
/* eslint max-len: 0 */
|
||||
|
||||
"use strict";
|
||||
|
||||
// The data below is initially copied from
|
||||
// https://chromium-i18n.appspot.com/ssl-aggregate-address
|
||||
|
||||
var addressData = {
|
||||
"data/US": {"lang": "en", "upper": "CS", "sub_zipexs": "35000,36999~99500,99999~96799~85000,86999~71600,72999~34000,34099~09000,09999~96200,96699~90000,96199~80000,81999~06000,06999~19700,19999~20000,56999~32000,34999~30000,39901~96910,96932~96700,96899~83200,83999~60000,62999~46000,47999~50000,52999~66000,67999~40000,42799~70000,71599~03900,04999~96960,96979~20600,21999~01000,05544~48000,49999~96941,96944~55000,56799~38600,39799~63000,65999~59000,59999~68000,69999~88900,89999~03000,03899~07000,08999~87000,88499~10000,00544~27000,28999~58000,58999~96950,96952~43000,45999~73000,74999~97000,97999~96940~15000,19699~00600,00999~02800,02999~29000,29999~57000,57999~37000,38599~75000,73344~84000,84999~05000,05999~00800,00899~20100,24699~98000,99499~24700,26999~53000,54999~82000,83414", "zipex": "95014,22162-1010", "name": "UNITED STATES", "zip": "(\\d{5})(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "fmt": "%N%n%O%n%A%n%C, %S %Z", "state_name_type": "state", "id": "data/US", "languages": "en", "sub_keys": "AL~AK~AS~AZ~AR~AA~AE~AP~CA~CO~CT~DE~DC~FL~GA~GU~HI~ID~IL~IN~IA~KS~KY~LA~ME~MH~MD~MA~MI~FM~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~MP~OH~OK~OR~PW~PA~PR~RI~SC~SD~TN~TX~UT~VT~VI~VA~WA~WV~WI~WY", "key": "US", "posturl": "https://tools.usps.com/go/ZipLookupAction!input.action", "require": "ACSZ", "sub_names": "Alabama~Alaska~American Samoa~Arizona~Arkansas~Armed Forces (AA)~Armed Forces (AE)~Armed Forces (AP)~California~Colorado~Connecticut~Delaware~District of Columbia~Florida~Georgia~Guam~Hawaii~Idaho~Illinois~Indiana~Iowa~Kansas~Kentucky~Louisiana~Maine~Marshall Islands~Maryland~Massachusetts~Michigan~Micronesia~Minnesota~Mississippi~Missouri~Montana~Nebraska~Nevada~New Hampshire~New Jersey~New Mexico~New York~North Carolina~North Dakota~Northern Mariana Islands~Ohio~Oklahoma~Oregon~Palau~Pennsylvania~Puerto Rico~Rhode Island~South Carolina~South Dakota~Tennessee~Texas~Utah~Vermont~Virgin Islands~Virginia~Washington~West Virginia~Wisconsin~Wyoming", "sub_zips": "3[56]~99[5-9]~96799~8[56]~71[6-9]|72~340~09~96[2-6]~9[0-5]|96[01]~8[01]~06~19[7-9]~20[02-5]|569~3[23]|34[1-9]~3[01]|398|39901~969([1-2]\\d|3[12])~967[0-8]|9679[0-8]|968~83[2-9]~6[0-2]~4[67]~5[0-2]~6[67]~4[01]|42[0-7]~70|71[0-5]~039|04~969[67]~20[6-9]|21~01|02[0-7]|05501|05544~4[89]~9694[1-4]~55|56[0-7]~38[6-9]|39[0-7]~6[3-5]~59~6[89]~889|89~03[0-8]~0[78]~87|88[0-4]~1[0-4]|06390|00501|00544~2[78]~58~9695[0-2]~4[3-5]~7[34]~97~969(39|40)~1[5-8]|19[0-6]~00[679]~02[89]~29~57~37|38[0-5]~7[5-9]|885|73301|73344~84~05~008~201|2[23]|24[0-6]~98|99[0-4]~24[7-9]|2[56]~5[34]~82|83[01]|83414"},
|
||||
};
|
|
@ -16,7 +16,7 @@ function EditDialog() {
|
|||
|
||||
EditDialog.prototype = {
|
||||
init() {
|
||||
this.refs = {
|
||||
this._elements = {
|
||||
controlsContainer: document.getElementById("controls-container"),
|
||||
cancel: document.getElementById("cancel"),
|
||||
save: document.getElementById("save"),
|
||||
|
@ -24,6 +24,11 @@ EditDialog.prototype = {
|
|||
this.attachEventListeners();
|
||||
},
|
||||
|
||||
uninit() {
|
||||
this.detachEventListeners();
|
||||
this._elements = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks FormAutofillParent to save or update an address.
|
||||
* @param {object} data
|
||||
|
@ -84,12 +89,20 @@ EditDialog.prototype = {
|
|||
// Toggle disabled attribute on the save button based on
|
||||
// whether the form is filled or empty.
|
||||
if (Object.keys(this.buildAddressObject()).length == 0) {
|
||||
this.refs.save.setAttribute("disabled", true);
|
||||
this._elements.save.setAttribute("disabled", true);
|
||||
} else {
|
||||
this.refs.save.removeAttribute("disabled");
|
||||
this._elements.save.removeAttribute("disabled");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "unload": {
|
||||
this.uninit();
|
||||
break;
|
||||
}
|
||||
case "keypress": {
|
||||
this.handleKeyPress(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -99,11 +112,10 @@ EditDialog.prototype = {
|
|||
* @param {DOMEvent} event
|
||||
*/
|
||||
handleClick(event) {
|
||||
if (event.target == this.refs.cancel) {
|
||||
this.detachEventListeners();
|
||||
if (event.target == this._elements.cancel) {
|
||||
window.close();
|
||||
}
|
||||
if (event.target == this.refs.save) {
|
||||
if (event.target == this._elements.save) {
|
||||
if (this._address) {
|
||||
this.saveAddress({
|
||||
guid: this._address.guid,
|
||||
|
@ -114,7 +126,17 @@ EditDialog.prototype = {
|
|||
address: this.buildAddressObject(),
|
||||
});
|
||||
}
|
||||
this.detachEventListeners();
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle key press events
|
||||
*
|
||||
* @param {DOMEvent} event
|
||||
*/
|
||||
handleKeyPress(event) {
|
||||
if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
|
@ -123,7 +145,8 @@ EditDialog.prototype = {
|
|||
* Attach event listener
|
||||
*/
|
||||
attachEventListeners() {
|
||||
this.refs.controlsContainer.addEventListener("click", this);
|
||||
window.addEventListener("keypress", this);
|
||||
this._elements.controlsContainer.addEventListener("click", this);
|
||||
document.addEventListener("input", this);
|
||||
},
|
||||
|
||||
|
@ -131,10 +154,10 @@ EditDialog.prototype = {
|
|||
* Remove event listener
|
||||
*/
|
||||
detachEventListeners() {
|
||||
this.refs.controlsContainer.removeEventListener("click", this);
|
||||
window.removeEventListener("keypress", this);
|
||||
this._elements.controlsContainer.removeEventListener("click", this);
|
||||
document.removeEventListener("input", this);
|
||||
},
|
||||
};
|
||||
|
||||
// Pass in argument from openDialog
|
||||
new EditDialog();
|
||||
|
|
|
@ -217,6 +217,10 @@ ManageProfileDialog.prototype = {
|
|||
this.uninit();
|
||||
break;
|
||||
}
|
||||
case "keypress": {
|
||||
this.handleKeyPress(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -235,6 +239,17 @@ ManageProfileDialog.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle key press events
|
||||
*
|
||||
* @param {DOMEvent} event
|
||||
*/
|
||||
handleKeyPress(event) {
|
||||
if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "formautofill-storage-changed": {
|
||||
|
@ -252,6 +267,7 @@ ManageProfileDialog.prototype = {
|
|||
*/
|
||||
attachEventListeners() {
|
||||
window.addEventListener("unload", this, {once: true});
|
||||
window.addEventListener("keypress", this);
|
||||
this._elements.addresses.addEventListener("change", this);
|
||||
this._elements.controlsContainer.addEventListener("click", this);
|
||||
Services.obs.addObserver(this, "formautofill-storage-changed");
|
||||
|
@ -261,6 +277,7 @@ ManageProfileDialog.prototype = {
|
|||
* Remove event listener
|
||||
*/
|
||||
detachEventListeners() {
|
||||
window.removeEventListener("keypress", this);
|
||||
this._elements.addresses.removeEventListener("change", this);
|
||||
this._elements.controlsContainer.removeEventListener("click", this);
|
||||
Services.obs.removeObserver(this, "formautofill-storage-changed");
|
||||
|
|
|
@ -13,6 +13,19 @@ add_task(async function test_cancelEditProfileDialog() {
|
|||
});
|
||||
});
|
||||
|
||||
add_task(async function test_cancelEditProfileDialogWithESC() {
|
||||
await new Promise(resolve => {
|
||||
let win = window.openDialog(EDIT_PROFILE_DIALOG_URL);
|
||||
win.addEventListener("load", () => {
|
||||
win.addEventListener("unload", () => {
|
||||
ok(true, "Edit profile dialog is closed with ESC key");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
|
||||
}, {once: true});
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_saveAddress() {
|
||||
await new Promise(resolve => {
|
||||
let win = window.openDialog(EDIT_PROFILE_DIALOG_URL, null, null, null);
|
||||
|
|
|
@ -35,6 +35,19 @@ add_task(async function test_manageProfilesInitialState() {
|
|||
});
|
||||
});
|
||||
|
||||
add_task(async function test_cancelManageProfileDialogWithESC() {
|
||||
await new Promise(resolve => {
|
||||
let win = window.openDialog(MANAGE_PROFILES_DIALOG_URL);
|
||||
win.addEventListener("load", () => {
|
||||
win.addEventListener("unload", () => {
|
||||
ok(true, "Manage profiles dialog is closed with ESC key");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
|
||||
}, {once: true});
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_removingSingleAndMultipleProfiles() {
|
||||
await saveAddress(TEST_ADDRESS_1);
|
||||
await saveAddress(TEST_ADDRESS_2);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
let formFillChromeScript;
|
||||
let expectingPopup = null;
|
||||
|
||||
function setInput(selector, value) {
|
||||
let input = document.querySelector("input" + selector);
|
||||
|
@ -98,6 +99,28 @@ async function checkAddresses(expectedAddresses) {
|
|||
});
|
||||
}
|
||||
|
||||
// Utils for registerPopupShownListener(in satchel_common.js) that handles dropdown popup
|
||||
// Please call "initPopupListener()" in your test and "await expectPopup()"
|
||||
// if you want to wait for dropdown menu displayed.
|
||||
function expectPopup() {
|
||||
info("expecting a popup");
|
||||
return new Promise(resolve => {
|
||||
expectingPopup = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function popupShownListener() {
|
||||
info("popup shown for test ");
|
||||
if (expectingPopup) {
|
||||
expectingPopup();
|
||||
expectingPopup = null;
|
||||
}
|
||||
}
|
||||
|
||||
function initPopupListener() {
|
||||
registerPopupShownListener(popupShownListener);
|
||||
}
|
||||
|
||||
function formAutoFillCommonSetup() {
|
||||
let chromeURL = SimpleTest.getTestFileURL("formautofill_parent_utils.js");
|
||||
formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
|
||||
|
@ -111,6 +134,7 @@ function formAutoFillCommonSetup() {
|
|||
SimpleTest.registerCleanupFunction(() => {
|
||||
formFillChromeScript.sendAsyncMessage("cleanup");
|
||||
formFillChromeScript.destroy();
|
||||
expectingPopup = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ Form autofill test: autocomplete on an autofocus form
|
|||
|
||||
"use strict";
|
||||
|
||||
let expectingPopup = null;
|
||||
let MOCK_STORAGE = [{
|
||||
organization: "Sesame Street",
|
||||
"street-address": "123 Sesame Street.",
|
||||
|
@ -30,20 +29,7 @@ let MOCK_STORAGE = [{
|
|||
tel: "1-650-903-0800",
|
||||
}];
|
||||
|
||||
function expectPopup() {
|
||||
info("expecting a popup");
|
||||
return new Promise(resolve => {
|
||||
expectingPopup = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function popupShownListener() {
|
||||
info("popup shown for test ");
|
||||
if (expectingPopup) {
|
||||
expectingPopup();
|
||||
expectingPopup = null;
|
||||
}
|
||||
}
|
||||
initPopupListener();
|
||||
|
||||
async function setupAddressStorage() {
|
||||
await addAddress(MOCK_STORAGE[0]);
|
||||
|
@ -59,8 +45,6 @@ add_task(async function check_autocomplete_on_autofocus_field() {
|
|||
));
|
||||
});
|
||||
|
||||
registerPopupShownListener(popupShownListener);
|
||||
|
||||
</script>
|
||||
|
||||
<p id="display"></p>
|
||||
|
|
|
@ -19,34 +19,20 @@ Form autofill test: simple form address autofill
|
|||
|
||||
"use strict";
|
||||
|
||||
let expectingPopup = null;
|
||||
let MOCK_STORAGE = [{
|
||||
organization: "Sesame Street",
|
||||
"street-address": "123 Sesame Street.",
|
||||
tel: "1-345-345-3456",
|
||||
country: "US",
|
||||
"address-level1": "NY",
|
||||
}, {
|
||||
organization: "Mozilla",
|
||||
"street-address": "331 E. Evelyn Avenue",
|
||||
tel: "1-650-903-0800",
|
||||
country: "US",
|
||||
"address-level1": "CA",
|
||||
}];
|
||||
|
||||
function expectPopup() {
|
||||
info("expecting a popup");
|
||||
return new Promise(resolve => {
|
||||
expectingPopup = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function popupShownListener() {
|
||||
info("popup shown for test ");
|
||||
if (expectingPopup) {
|
||||
expectingPopup();
|
||||
expectingPopup = null;
|
||||
}
|
||||
}
|
||||
|
||||
function checkElementFilled(element, expectedvalue) {
|
||||
return [
|
||||
new Promise(resolve => {
|
||||
|
@ -100,6 +86,8 @@ async function setupFormHistory() {
|
|||
]);
|
||||
}
|
||||
|
||||
initPopupListener();
|
||||
|
||||
// Form with history only.
|
||||
add_task(async function history_only_menu_checking() {
|
||||
await setupFormHistory();
|
||||
|
@ -176,8 +164,6 @@ add_task(async function check_form_autofill_resume() {
|
|||
));
|
||||
});
|
||||
|
||||
registerPopupShownListener(popupShownListener);
|
||||
|
||||
</script>
|
||||
|
||||
<p id="display"></p>
|
||||
|
@ -193,7 +179,13 @@ registerPopupShownListener(popupShownListener);
|
|||
<p><label>country: <select id="country" name="country" autocomplete="country">
|
||||
<option/>
|
||||
<option value="US">United States</option>
|
||||
</label></p>
|
||||
</select></label></p>
|
||||
<p><label>states: <select id="address-level1" name="address-level1" autocomplete="address-level1">
|
||||
<option/>
|
||||
<option value="CA">California</option>
|
||||
<option value="NY">New York</option>
|
||||
<option value="WA">Washington</option>
|
||||
</select></label></p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,6 @@ Form autofill test: preview and highlight
|
|||
|
||||
"use strict";
|
||||
|
||||
let expectingPopup = null;
|
||||
let defaultTextColor;
|
||||
const MOCK_STORAGE = [{
|
||||
organization: "Sesame Street",
|
||||
|
@ -33,21 +32,6 @@ const MOCK_STORAGE = [{
|
|||
tel: "2-222-333-444",
|
||||
}];
|
||||
|
||||
function expectPopup() {
|
||||
info("expecting a popup");
|
||||
return new Promise(resolve => {
|
||||
expectingPopup = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function popupShownListener() {
|
||||
info("popup shown for test ");
|
||||
if (expectingPopup) {
|
||||
expectingPopup();
|
||||
expectingPopup = null;
|
||||
}
|
||||
}
|
||||
|
||||
// We could not get ManuallyManagedState of element now, so directly check if
|
||||
// filter and text color style are applied.
|
||||
function checkFieldPreview(elem, expectedText) {
|
||||
|
@ -102,6 +86,8 @@ function confirmAllFieldsFilled(address) {
|
|||
return Promise.all(pendingPromises);
|
||||
}
|
||||
|
||||
initPopupListener();
|
||||
|
||||
add_task(async function setup_storage() {
|
||||
defaultTextColor = window.getComputedStyle(document.querySelector("input")).getPropertyValue("color");
|
||||
|
||||
|
@ -153,8 +139,6 @@ add_task(async function check_filled_highlight() {
|
|||
checkFormFilledFields(MOCK_STORAGE[0]);
|
||||
});
|
||||
|
||||
registerPopupShownListener(popupShownListener);
|
||||
|
||||
</script>
|
||||
|
||||
<p id="display"></p>
|
||||
|
|
|
@ -29,7 +29,9 @@ let TEST_ADDRESSES = [{
|
|||
tel: "1-650-903-0800",
|
||||
}];
|
||||
|
||||
// Autofill the address from dropdown menu.
|
||||
initPopupListener();
|
||||
|
||||
// Submit first address for saving.
|
||||
add_task(async function check_storage_after_form_submitted() {
|
||||
// We already verified the first time use case in browser test
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
|
@ -50,6 +52,32 @@ add_task(async function check_storage_after_form_submitted() {
|
|||
ok(matching, "Address saved as expected");
|
||||
});
|
||||
|
||||
// Submit another new address.
|
||||
add_task(async function check_storage_after_another_address_submitted() {
|
||||
document.querySelector("form").reset();
|
||||
for (let key in TEST_ADDRESSES[1]) {
|
||||
await setInput("#" + key, TEST_ADDRESSES[1][key]);
|
||||
}
|
||||
|
||||
clickOnElement("input[type=submit]");
|
||||
|
||||
// The 2nd test address should be on the top since it's the last used one.
|
||||
let addressesInMenu = TEST_ADDRESSES.slice(1);
|
||||
addressesInMenu.push(TEST_ADDRESSES[0]);
|
||||
|
||||
// let expectedAddresses = TEST_ADDRESSES.slice(0);
|
||||
await onAddressChanged("add");
|
||||
let matching = await checkAddresses(TEST_ADDRESSES);
|
||||
ok(matching, "New address saved as expected");
|
||||
|
||||
await setInput("#organization", "");
|
||||
doKey("down");
|
||||
await expectPopup();
|
||||
checkMenuEntries(addressesInMenu.map(address =>
|
||||
JSON.stringify({primary: address.organization, secondary: address["street-address"]})
|
||||
));
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -250,6 +250,63 @@ const TESTCASES_INPUT_UNCHANGED = [
|
|||
},
|
||||
];
|
||||
|
||||
const TESTCASES_US_STATES = [
|
||||
{
|
||||
description: "Form with US states select elements; with lower case state key",
|
||||
document: `<form><select id="state" autocomplete="shipping address-level1">
|
||||
<option value=""></option>
|
||||
<option value="CA">California</option>
|
||||
</select></form>`,
|
||||
fieldDetails: [
|
||||
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
|
||||
],
|
||||
profileData: {
|
||||
"guid": "123",
|
||||
"country": "US",
|
||||
"address-level1": "ca",
|
||||
},
|
||||
expectedResult: {
|
||||
"state": "CA",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Form with US states select elements; with state name and extra spaces",
|
||||
document: `<form><select id="state" autocomplete="shipping address-level1">
|
||||
<option value=""></option>
|
||||
<option value="CA">CA</option>
|
||||
</select></form>`,
|
||||
fieldDetails: [
|
||||
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
|
||||
],
|
||||
profileData: {
|
||||
"guid": "123",
|
||||
"country": "US",
|
||||
"address-level1": " California ",
|
||||
},
|
||||
expectedResult: {
|
||||
"state": "CA",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Form with US states select elements; with partial state key match",
|
||||
document: `<form><select id="state" autocomplete="shipping address-level1">
|
||||
<option value=""></option>
|
||||
<option value="US-WA">WA-Washington</option>
|
||||
</select></form>`,
|
||||
fieldDetails: [
|
||||
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
|
||||
],
|
||||
profileData: {
|
||||
"guid": "123",
|
||||
"country": "US",
|
||||
"address-level1": "WA",
|
||||
},
|
||||
expectedResult: {
|
||||
"state": "US-WA",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function do_test(testcases, testFn) {
|
||||
for (let tc of testcases) {
|
||||
(function() {
|
||||
|
@ -327,3 +384,16 @@ do_test(TESTCASES_INPUT_UNCHANGED, (testcase, element) => {
|
|||
}),
|
||||
];
|
||||
});
|
||||
|
||||
do_test(TESTCASES_US_STATES, (testcase, element) => {
|
||||
let id = element.id;
|
||||
return [
|
||||
new Promise(resolve => {
|
||||
element.addEventListener("input", () => {
|
||||
Assert.equal(element.value, testcase.expectedResult[id],
|
||||
"Check the " + id + " field was filled with correct data");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
document.getElementById("onboarding-overlay-dialog")
|
||||
document.getElementById("onboarding-overlay")
|
||||
.addEventListener("click", evt => {
|
||||
switch (evt.target.id) {
|
||||
case "onboarding-tour-addons-button":
|
||||
|
@ -24,5 +24,13 @@ document.getElementById("onboarding-overlay-dialog")
|
|||
case "onboarding-tour-search-button":
|
||||
Mozilla.UITour.openSearchPanel(() => {});
|
||||
break;
|
||||
case "onboarding-overlay":
|
||||
case "onboarding-overlay-close-btn":
|
||||
// Dismiss any highlights if a user tries to close the dialog.
|
||||
Mozilla.UITour.hideHighlight();
|
||||
}
|
||||
// Dismiss any highlights if a user tries to change to other tours.
|
||||
if (evt.target.classList.contains("onboarding-tour-item")) {
|
||||
Mozilla.UITour.hideHighlight();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css";
|
|||
const ABOUT_HOME_URL = "about:home";
|
||||
const ABOUT_NEWTAB_URL = "about:newtab";
|
||||
const BUNDLE_URI = "chrome://onboarding/locale/onboarding.properties";
|
||||
const UITOUR_JS_URI = "chrome://browser/content/UITour-lib.js";
|
||||
const UITOUR_JS_URI = "resource://onboarding/lib/UITour-lib.js";
|
||||
const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
|
||||
const BRAND_SHORT_NAME = Services.strings
|
||||
.createBundle("chrome://branding/locale/brand.properties")
|
||||
|
|
|
@ -5,3 +5,7 @@
|
|||
[features/onboarding@mozilla.org] chrome.jar:
|
||||
% resource onboarding %content/
|
||||
content/ (content/*)
|
||||
# Package UITour-lib.js in here rather than under
|
||||
# /browser/components/uitour to avoid "unreferenced files" error when
|
||||
# Onboarding extension is not built.
|
||||
content/lib/UITour-lib.js (/browser/components/uitour/UITour-lib.js)
|
||||
|
|
|
@ -128,7 +128,7 @@ VIAddVersionKey "ProductVersion" "${AppVersion}"
|
|||
!define NOW_INSTALLING_TOP_DU 70u
|
||||
!define INSTALL_BLURB_TOP_DU 137u
|
||||
!define INSTALL_FOOTER_TOP_DU -48u
|
||||
!define INSTALL_FOOTER_WIDTH_DU 300u
|
||||
!define INSTALL_FOOTER_WIDTH_DU 250u
|
||||
!define PROGRESS_BAR_TOP_DU 112u
|
||||
!define APPNAME_BMP_EDGE_DU 19u
|
||||
!define APPNAME_BMP_TOP_DU 12u
|
||||
|
|
|
@ -833,15 +833,17 @@ Function createInstall
|
|||
|
||||
; In some locales, the footer message may be too long to fit on one line.
|
||||
; Figure out how much height it needs and give it that much.
|
||||
${GetTextExtent} "$(STUB_BLURB_FOOTER)" $FontFooter $R1 $R2
|
||||
StrCpy $1 0
|
||||
${While} $R1 > 0
|
||||
IntOp $1 $1 + $R2
|
||||
IntOp $R1 $R1 - ${INSTALL_FOOTER_WIDTH_DU}
|
||||
${EndWhile}
|
||||
nsDialogs::CreateControl STATIC ${DEFAULT_STYLES}|${SS_NOTIFY}|${SS_RIGHT} \
|
||||
${WS_EX_TRANSPARENT} -320u ${INSTALL_FOOTER_TOP_DU} \
|
||||
${INSTALL_FOOTER_WIDTH_DU} "$1u" "$(STUB_BLURB_FOOTER)"
|
||||
${GetTextWidthHeight} "$(STUB_BLURB_FOOTER)" $FontFooter \
|
||||
${INSTALL_FOOTER_WIDTH_DU} $R1 $R2
|
||||
!ifdef ${AB_CD}_rtl
|
||||
nsDialogs::CreateControl STATIC ${DEFAULT_STYLES}|${SS_NOTIFY} \
|
||||
${WS_EX_TRANSPARENT} 30u ${INSTALL_FOOTER_TOP_DU} ${INSTALL_FOOTER_WIDTH_DU} "$R2u" \
|
||||
"$(STUB_BLURB_FOOTER)"
|
||||
!else
|
||||
nsDialogs::CreateControl STATIC ${DEFAULT_STYLES}|${SS_NOTIFY}|${SS_RIGHT} \
|
||||
${WS_EX_TRANSPARENT} 175u ${INSTALL_FOOTER_TOP_DU} ${INSTALL_FOOTER_WIDTH_DU} "$R2u" \
|
||||
"$(STUB_BLURB_FOOTER)"
|
||||
!endif
|
||||
Pop $0
|
||||
SendMessage $0 ${WM_SETFONT} $FontFooter 0
|
||||
SetCtlColors $0 ${INSTALL_BLURB_TEXT_COLOR} transparent
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY popups.label "Pop-ups">
|
||||
|
||||
<!ENTITY blockPopups.label "Block pop-up windows">
|
||||
<!ENTITY blockPopups.accesskey "B">
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<!ENTITY doNotTrack.post.label ".">
|
||||
|
||||
<!ENTITY history.label "History">
|
||||
<!ENTITY permissions.label "Permissions">
|
||||
|
||||
<!ENTITY locationBar.label "Location Bar">
|
||||
|
||||
|
|
|
@ -159,6 +159,7 @@ XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() {
|
|||
"BMB_bookmarksToolbarPopup",
|
||||
"search-go-button",
|
||||
"soundplaying-icon",
|
||||
"restore-tabs-button",
|
||||
]
|
||||
return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
|
||||
.concat(SPECIAL_CASES);
|
||||
|
|
|
@ -1284,3 +1284,8 @@ notification.pluginVulnerable > .notification-inner > .messageCloseButton:not(:h
|
|||
.webextension-popup-browser {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/* Prevent movement in the restore-tabs-button when it's clicked. */
|
||||
.restore-tabs-button:hover:active:not([disabled="true"]) {
|
||||
padding: 3px;
|
||||
}
|
|
@ -14,8 +14,6 @@
|
|||
%include ../shared/browser.inc.css
|
||||
|
||||
:root {
|
||||
--tabs-toolbar-color: #333;
|
||||
|
||||
--toolbarbutton-vertical-text-padding: calc(var(--toolbarbutton-inner-padding) + 1px);
|
||||
|
||||
%ifdef MOZ_PHOTON_THEME
|
||||
|
@ -122,7 +120,11 @@ toolbar:-moz-lwtheme {
|
|||
}
|
||||
|
||||
#main-window:not(:-moz-lwtheme) > #titlebar {
|
||||
%ifdef MOZ_PHOTON_THEME
|
||||
background-color: #232323;
|
||||
%else
|
||||
-moz-appearance: -moz-window-titlebar;
|
||||
%endif
|
||||
}
|
||||
|
||||
#main-window:not([tabsintitlebar]) > #titlebar {
|
||||
|
@ -162,9 +164,11 @@ toolbar:-moz-lwtheme {
|
|||
margin-top: 3px;
|
||||
}
|
||||
|
||||
%ifndef MOZ_PHOTON_THEME
|
||||
#main-window[customize-entered] > #titlebar {
|
||||
-moz-appearance: none;
|
||||
}
|
||||
%endif
|
||||
|
||||
/** End titlebar **/
|
||||
|
||||
|
@ -1524,7 +1528,11 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
|
|||
|
||||
.tabbrowser-tab[visuallyselected=true]:not(:-moz-lwtheme) {
|
||||
/* overriding tabbox.css */
|
||||
%ifdef MOZ_PHOTON_THEME
|
||||
color: hsl(240, 5%, 5%);
|
||||
%else
|
||||
color: inherit;
|
||||
%endif
|
||||
}
|
||||
|
||||
.tabbrowser-tab[visuallyselected=true] {
|
||||
|
@ -1556,10 +1564,17 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
|
|||
}
|
||||
|
||||
#TabsToolbar:not(:-moz-lwtheme) {
|
||||
color: var(--tabs-toolbar-color);
|
||||
color: #333;
|
||||
text-shadow: @loweredShadow@;
|
||||
}
|
||||
|
||||
%ifdef MOZ_PHOTON_THEME
|
||||
:root[tabsintitlebar] #TabsToolbar:not(:-moz-lwtheme) {
|
||||
color: hsl(240, 9%, 98%);
|
||||
text-shadow: none;
|
||||
}
|
||||
%endif
|
||||
|
||||
%ifndef MOZ_PHOTON_THEME
|
||||
#navigator-toolbox[inFullscreen] > #TabsToolbar {
|
||||
padding-top: var(--space-above-tabbar);
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
--chrome-selection-background-color: #5675B9;
|
||||
|
||||
/* Tabs */
|
||||
--tabs-toolbar-color: #F5F7FA;
|
||||
--tab-background-color: #272b35;
|
||||
--tab-hover-background-color: #07090a;
|
||||
--tab-selection-color: #f5f7fa;
|
||||
|
|
|
@ -604,3 +604,56 @@
|
|||
.alltabs-endimage[blocked] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-blocked.svg);
|
||||
}
|
||||
|
||||
.restore-tabs-button-wrapper {
|
||||
visibility: hidden;
|
||||
position: fixed; /* so the button does not take up actual space and cause overflow buttons in the tabbar when hidden */
|
||||
}
|
||||
|
||||
.restore-tabs-button-wrapper[shown] {
|
||||
visibility: visible;
|
||||
position: initial;
|
||||
}
|
||||
|
||||
.restore-tabs-button {
|
||||
box-sizing: border-box;
|
||||
-moz-appearance: none;
|
||||
background-color: hsl(0,0%,0%,.04);
|
||||
border: 1px solid hsla(0,0%,16%,.2);
|
||||
border-radius: 3px;
|
||||
margin: 3px;
|
||||
margin-inline-start: 9px;
|
||||
transition: max-width 300ms;
|
||||
}
|
||||
|
||||
.restore-tabs-button:hover {
|
||||
background-color: hsl(0,0%,0%,.08);
|
||||
}
|
||||
|
||||
.restore-tabs-button:active {
|
||||
background-color: hsl(0,0%,0%,.11);
|
||||
}
|
||||
|
||||
#TabsToolbar[brighttext] .restore-tabs-button {
|
||||
background-color: hsl(0,0%,100%,.07);
|
||||
border-color:currentColor;
|
||||
color: currentColor;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
#TabsToolbar[brighttext] .restore-tabs-button:hover {
|
||||
background-color: hsl(0,0%,100%,.17);
|
||||
}
|
||||
|
||||
#TabsToolbar[brighttext] .restore-tabs-button:active {
|
||||
background-color: hsl(0,0%,100%,.27);
|
||||
}
|
||||
|
||||
.restore-tabs-button > .toolbarbutton-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.restore-tabs-button > .toolbarbutton-text {
|
||||
display: -moz-box;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
|
|
@ -2059,3 +2059,8 @@ notification.pluginVulnerable > .notification-inner > .messageCloseButton {
|
|||
padding-top: .9167em;
|
||||
padding-bottom: .9167em;
|
||||
}
|
||||
|
||||
/* Prevent movement in the restore-tabs-button when it's clicked. */
|
||||
.restore-tabs-button:hover:active:not([disabled="true"]) {
|
||||
padding: 3px;
|
||||
}
|
||||
|
|
|
@ -6,16 +6,6 @@ dnl Add compiler specific options
|
|||
|
||||
AC_DEFUN([MOZ_DEFAULT_COMPILER],
|
||||
[
|
||||
dnl set DEVELOPER_OPTIONS early; MOZ_DEFAULT_COMPILER is usually the first non-setup directive
|
||||
if test -z "$MOZILLA_OFFICIAL"; then
|
||||
DEVELOPER_OPTIONS=1
|
||||
fi
|
||||
MOZ_ARG_ENABLE_BOOL(release,
|
||||
[ --enable-release Build with more conservative, release engineering-oriented options.
|
||||
This may slow down builds.],
|
||||
DEVELOPER_OPTIONS=,
|
||||
DEVELOPER_OPTIONS=1)
|
||||
|
||||
dnl Default to MSVC for win32 and gcc-4.2 for darwin
|
||||
dnl ==============================================================
|
||||
if test -z "$CROSS_COMPILE"; then
|
||||
|
|
|
@ -38,7 +38,6 @@ MACH_MODULES = [
|
|||
'build/valgrind/mach_commands.py',
|
||||
'devtools/shared/css/generated/mach_commands.py',
|
||||
'dom/bindings/mach_commands.py',
|
||||
'dom/media/test/external/mach_commands.py',
|
||||
'layout/tools/reftest/mach_commands.py',
|
||||
'python/mach_commands.py',
|
||||
'python/mach/mach/commands/commandinfo.py',
|
||||
|
|
|
@ -213,7 +213,6 @@ def old_configure_options(*options):
|
|||
'--enable-raw',
|
||||
'--enable-readline',
|
||||
'--enable-reflow-perf',
|
||||
'--enable-release',
|
||||
'--enable-safe-browsing',
|
||||
'--enable-sandbox',
|
||||
'--enable-signmar',
|
||||
|
|
|
@ -1046,3 +1046,18 @@ option(env='RUSTFLAGS',
|
|||
nargs=1,
|
||||
help='Rust compiler flags')
|
||||
set_config('RUSTFLAGS', depends('RUSTFLAGS')(lambda flags: flags))
|
||||
|
||||
|
||||
imply_option('--enable-release', mozilla_official)
|
||||
imply_option('--enable-release', depends_if('MOZ_AUTOMATION')(lambda x: True))
|
||||
|
||||
js_option('--enable-release',
|
||||
help='Build with more conservative, release engineering-oriented '
|
||||
'options. This may slow down builds.')
|
||||
|
||||
@depends('--enable-release')
|
||||
def developer_options(value):
|
||||
if not value:
|
||||
return True
|
||||
|
||||
add_old_configure_assignment('DEVELOPER_OPTIONS', developer_options)
|
||||
|
|
|
@ -14,8 +14,6 @@ mk_add_options AUTOCLOBBER=1
|
|||
|
||||
ac_add_options --enable-crashreporter
|
||||
|
||||
ac_add_options --enable-release
|
||||
|
||||
# Tell the build system where to find llvm-config for builds on automation.
|
||||
export LLVM_CONFIG="${TOOLTOOL_DIR:-$topsrcdir}/clang/bin/llvm-config"
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ mozilla.pth:build/pymake
|
|||
mozilla.pth:config
|
||||
mozilla.pth:dom/bindings
|
||||
mozilla.pth:dom/bindings/parser
|
||||
mozilla.pth:dom/media/test/external
|
||||
mozilla.pth:layout/tools/reftest
|
||||
mozilla.pth:other-licenses/ply/
|
||||
mozilla.pth:taskcluster
|
||||
|
|
|
@ -81,6 +81,7 @@ skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keybo
|
|||
[browser_markup_anonymous_02.js]
|
||||
skip-if = e10s # scratchpad.xul is not loading in e10s window
|
||||
[browser_markup_anonymous_03.js]
|
||||
skip-if = stylo # Stylo doesn't support shadow DOM yet, bug 1293844
|
||||
[browser_markup_anonymous_04.js]
|
||||
[browser_markup_copy_image_data.js]
|
||||
subsuite = clipboard
|
||||
|
|
|
@ -22,6 +22,8 @@ window.onload = function () {
|
|||
require("devtools/shared/dom-node-filter-constants");
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
const isStylo = SpecialPowers.getBoolPref("layout.css.servo.enabled");
|
||||
|
||||
SpecialPowers.pushPrefEnv({"set": [
|
||||
["dom.webcomponents.enabled", true]
|
||||
]});
|
||||
|
@ -144,6 +146,12 @@ window.onload = function () {
|
|||
});
|
||||
|
||||
addAsyncTest(function* testShadowAnonymous() {
|
||||
// Stylo doesn't currently support shadow DOM (bug 1293844)
|
||||
if (isStylo) {
|
||||
runNextTest();
|
||||
return;
|
||||
}
|
||||
|
||||
info("Testing shadow DOM content.");
|
||||
|
||||
let shadow = yield gWalker.querySelector(gWalker.rootNode, "#shadow");
|
||||
|
|
|
@ -2864,7 +2864,9 @@ ArrayBufferClient.prototype = {
|
|||
valid: true,
|
||||
|
||||
slice: DebuggerClient.requester({
|
||||
type: "slice"
|
||||
type: "slice",
|
||||
start: arg(0),
|
||||
count: arg(1)
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ using namespace mozilla;
|
|||
using namespace mozilla::dom;
|
||||
|
||||
FormData::FormData(nsISupports* aOwner)
|
||||
: HTMLFormSubmission(WrapNotNull(UTF_8_ENCODING), nullptr)
|
||||
: HTMLFormSubmission(UTF_8_ENCODING, nullptr)
|
||||
, mOwner(aOwner)
|
||||
{
|
||||
}
|
||||
|
@ -402,7 +402,7 @@ NS_IMETHODIMP
|
|||
FormData::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
|
||||
nsACString& aContentTypeWithCharset, nsACString& aCharset)
|
||||
{
|
||||
FSMultipartFormData fs(WrapNotNull(UTF_8_ENCODING), nullptr);
|
||||
FSMultipartFormData fs(UTF_8_ENCODING, nullptr);
|
||||
|
||||
for (uint32_t i = 0; i < mFormData.Length(); ++i) {
|
||||
if (mFormData[i].wasNullBlob) {
|
||||
|
|
|
@ -6535,9 +6535,8 @@ nsIDocument::ImportNode(nsINode& aNode, bool aDeep, ErrorResult& rv) const
|
|||
case nsIDOMNode::DOCUMENT_TYPE_NODE:
|
||||
{
|
||||
nsCOMPtr<nsINode> newNode;
|
||||
nsCOMArray<nsINode> nodesWithProperties;
|
||||
rv = nsNodeUtils::Clone(imported, aDeep, mNodeInfoManager,
|
||||
nodesWithProperties, getter_AddRefs(newNode));
|
||||
rv = nsNodeUtils::Clone(imported, aDeep, mNodeInfoManager, nullptr,
|
||||
getter_AddRefs(newNode));
|
||||
if (rv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
@ -408,9 +408,7 @@ nsNodeUtils::CloneNodeImpl(nsINode *aNode, bool aDeep, nsINode **aResult)
|
|||
*aResult = nullptr;
|
||||
|
||||
nsCOMPtr<nsINode> newNode;
|
||||
nsCOMArray<nsINode> nodesWithProperties;
|
||||
nsresult rv = Clone(aNode, aDeep, nullptr, nodesWithProperties,
|
||||
getter_AddRefs(newNode));
|
||||
nsresult rv = Clone(aNode, aDeep, nullptr, nullptr, getter_AddRefs(newNode));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
newNode.forget(aResult);
|
||||
|
@ -422,7 +420,7 @@ nsresult
|
|||
nsNodeUtils::CloneAndAdopt(nsINode *aNode, bool aClone, bool aDeep,
|
||||
nsNodeInfoManager *aNewNodeInfoManager,
|
||||
JS::Handle<JSObject*> aReparentScope,
|
||||
nsCOMArray<nsINode> &aNodesWithProperties,
|
||||
nsCOMArray<nsINode> *aNodesWithProperties,
|
||||
nsINode *aParent, nsINode **aResult)
|
||||
{
|
||||
NS_PRECONDITION((!aClone && aNewNodeInfoManager) || !aReparentScope,
|
||||
|
@ -653,10 +651,10 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNode, bool aClone, bool aDeep,
|
|||
}
|
||||
#endif
|
||||
|
||||
if (aNode->HasProperties()) {
|
||||
bool ok = aNodesWithProperties.AppendObject(aNode);
|
||||
if (aNodesWithProperties && aNode->HasProperties()) {
|
||||
bool ok = aNodesWithProperties->AppendObject(aNode);
|
||||
if (aClone) {
|
||||
ok = ok && aNodesWithProperties.AppendObject(clone);
|
||||
ok = ok && aNodesWithProperties->AppendObject(clone);
|
||||
}
|
||||
|
||||
NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY);
|
||||
|
|
|
@ -181,12 +181,13 @@ public:
|
|||
* shouldn't be changed.
|
||||
* @param aNodesWithProperties All nodes (from amongst aNode and its
|
||||
* descendants) with properties. Every node will
|
||||
* be followed by its clone.
|
||||
* be followed by its clone. Null can be passed to
|
||||
* prevent this from being used.
|
||||
* @param aResult *aResult will contain the cloned node.
|
||||
*/
|
||||
static nsresult Clone(nsINode *aNode, bool aDeep,
|
||||
nsNodeInfoManager *aNewNodeInfoManager,
|
||||
nsCOMArray<nsINode> &aNodesWithProperties,
|
||||
nsCOMArray<nsINode> *aNodesWithProperties,
|
||||
nsINode **aResult)
|
||||
{
|
||||
return CloneAndAdopt(aNode, true, aDeep, aNewNodeInfoManager,
|
||||
|
@ -198,9 +199,8 @@ public:
|
|||
*/
|
||||
static nsresult Clone(nsINode *aNode, bool aDeep, nsINode **aResult)
|
||||
{
|
||||
nsCOMArray<nsINode> dummyNodeWithProperties;
|
||||
return CloneAndAdopt(aNode, true, aDeep, nullptr, nullptr,
|
||||
dummyNodeWithProperties, aNode->GetParent(), aResult);
|
||||
return CloneAndAdopt(aNode, true, aDeep, nullptr, nullptr, nullptr,
|
||||
aNode->GetParent(), aResult);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,7 +226,7 @@ public:
|
|||
{
|
||||
nsCOMPtr<nsINode> node;
|
||||
nsresult rv = CloneAndAdopt(aNode, false, true, aNewNodeInfoManager,
|
||||
aReparentScope, aNodesWithProperties,
|
||||
aReparentScope, &aNodesWithProperties,
|
||||
nullptr, getter_AddRefs(node));
|
||||
|
||||
nsMutationGuard::DidMutate();
|
||||
|
@ -299,7 +299,8 @@ private:
|
|||
* @param aNodesWithProperties All nodes (from amongst aNode and its
|
||||
* descendants) with properties. If aClone is
|
||||
* true every node will be followed by its
|
||||
* clone.
|
||||
* clone. Null can be passed to prevent this from
|
||||
* being populated.
|
||||
* @param aParent If aClone is true the cloned node will be appended to
|
||||
* aParent's children. May be null. If not null then aNode
|
||||
* must be an nsIContent.
|
||||
|
@ -309,7 +310,7 @@ private:
|
|||
static nsresult CloneAndAdopt(nsINode *aNode, bool aClone, bool aDeep,
|
||||
nsNodeInfoManager *aNewNodeInfoManager,
|
||||
JS::Handle<JSObject*> aReparentScope,
|
||||
nsCOMArray<nsINode> &aNodesWithProperties,
|
||||
nsCOMArray<nsINode> *aNodesWithProperties,
|
||||
nsINode *aParent, nsINode **aResult);
|
||||
|
||||
enum class AnimationMutationType
|
||||
|
|
|
@ -53,6 +53,10 @@ ChooseValidatorCompileOptions(const ShBuiltInResources& resources,
|
|||
// Work around that Mac drivers handle struct scopes incorrectly.
|
||||
options |= SH_REGENERATE_STRUCT_NAMES;
|
||||
options |= SH_INIT_OUTPUT_VARIABLES;
|
||||
|
||||
// Work around that Intel drivers on Mac OSX handle for-loop incorrectly.
|
||||
if (gl->Vendor() == gl::GLVendor::Intel) {
|
||||
options |= SH_ADD_AND_TRUE_TO_LOOP_CONDITION;
|
||||
#endif
|
||||
|
||||
if (!gl->IsANGLE() && gl->Vendor() == gl::GLVendor::Intel) {
|
||||
|
|
|
@ -889,7 +889,7 @@ GetSubmitEncoding(nsGenericHTMLElement* aForm)
|
|||
if (doc) {
|
||||
return Encoding::ForName(doc->GetDocumentCharacterSet());
|
||||
}
|
||||
return WrapNotNull(UTF_8_ENCODING);
|
||||
return UTF_8_ENCODING;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -536,7 +536,7 @@ ContentChild::RecvSetXPCOMProcessAttributes(const XPCOMInitData& aXPCOMInit,
|
|||
{
|
||||
mLookAndFeelCache = aLookAndFeelIntCache;
|
||||
InitXPCOM(aXPCOMInit, aInitialData);
|
||||
InitGraphicsDeviceData();
|
||||
InitGraphicsDeviceData(aXPCOMInit.contentDeviceData());
|
||||
|
||||
#ifdef NS_PRINTING
|
||||
// Force the creation of the nsPrintingProxy so that it's IPC counterpart,
|
||||
|
@ -998,11 +998,11 @@ ContentChild::AppendProcessId(nsACString& aName)
|
|||
}
|
||||
|
||||
void
|
||||
ContentChild::InitGraphicsDeviceData()
|
||||
ContentChild::InitGraphicsDeviceData(const ContentDeviceData& aData)
|
||||
{
|
||||
// Initialize the graphics platform. This may contact the parent process
|
||||
// to read device preferences.
|
||||
gfxPlatform::GetPlatform();
|
||||
gfxPlatform::InitChild(aData);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -107,7 +107,7 @@ public:
|
|||
void InitXPCOM(const XPCOMInitData& aXPCOMInit,
|
||||
const mozilla::dom::ipc::StructuredCloneData& aInitialData);
|
||||
|
||||
void InitGraphicsDeviceData();
|
||||
void InitGraphicsDeviceData(const ContentDeviceData& aData);
|
||||
|
||||
static ContentChild* GetSingleton()
|
||||
{
|
||||
|
|
|
@ -2290,6 +2290,8 @@ ContentParent::InitInternal(ProcessPriority aInitialPriority,
|
|||
SerializeURI(nullptr, xpcomInit.userContentSheetURL());
|
||||
}
|
||||
|
||||
gfxPlatform::GetPlatform()->BuildContentDeviceData(&xpcomInit.contentDeviceData());
|
||||
|
||||
nsCOMPtr<nsIGfxInfo> gfxInfo = services::GetGfxInfo();
|
||||
if (gfxInfo) {
|
||||
for (int32_t i = 1; i <= nsIGfxInfo::FEATURE_MAX_VALUE; ++i) {
|
||||
|
|
|
@ -268,6 +268,7 @@ struct XPCOMInitData
|
|||
FontFamilyListEntry[] fontFamilies;
|
||||
OptionalURIParams userContentSheetURL;
|
||||
PrefSetting[] prefs;
|
||||
ContentDeviceData contentDeviceData;
|
||||
GfxInfoFeatureStatus[] gfxFeatureStatus;
|
||||
DataStorageEntry[] dataStorage;
|
||||
nsCString[] appLocales;
|
||||
|
|
|
@ -2243,20 +2243,36 @@ MediaCacheStream::SetPlaybackRate(uint32_t aBytesPerSecond)
|
|||
}
|
||||
|
||||
nsresult
|
||||
MediaCacheStream::SeekInternal(int64_t aOffset)
|
||||
MediaCacheStream::Seek(int32_t aWhence, int64_t aOffset)
|
||||
{
|
||||
if (aOffset < 0) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
|
||||
|
||||
mMediaCache->GetReentrantMonitor().AssertCurrentThreadIn();
|
||||
|
||||
if (mClosed) {
|
||||
ReentrantMonitorAutoEnter mon(mMediaCache->GetReentrantMonitor());
|
||||
if (mClosed)
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
int64_t oldOffset = mStreamOffset;
|
||||
mStreamOffset = aOffset;
|
||||
int64_t newOffset = mStreamOffset;
|
||||
switch (aWhence) {
|
||||
case PR_SEEK_END:
|
||||
if (mStreamLength < 0)
|
||||
return NS_ERROR_FAILURE;
|
||||
newOffset = mStreamLength + aOffset;
|
||||
break;
|
||||
case PR_SEEK_CUR:
|
||||
newOffset += aOffset;
|
||||
break;
|
||||
case PR_SEEK_SET:
|
||||
newOffset = aOffset;
|
||||
break;
|
||||
default:
|
||||
NS_ERROR("Unknown whence");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (newOffset < 0)
|
||||
return NS_ERROR_FAILURE;
|
||||
mStreamOffset = newOffset;
|
||||
|
||||
LOG("Stream %p Seek to %" PRId64, this, mStreamOffset);
|
||||
mMediaCache->NoteSeek(this, oldOffset);
|
||||
|
@ -2285,10 +2301,11 @@ MediaCacheStream::Tell()
|
|||
}
|
||||
|
||||
nsresult
|
||||
MediaCacheStream::ReadInternal(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
MediaCacheStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
||||
{
|
||||
mMediaCache->GetReentrantMonitor().AssertCurrentThreadIn();
|
||||
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
|
||||
|
||||
ReentrantMonitorAutoEnter mon(mMediaCache->GetReentrantMonitor());
|
||||
if (mClosed)
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
|
@ -2357,7 +2374,7 @@ MediaCacheStream::ReadInternal(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
|
|||
}
|
||||
|
||||
// No data has been read yet, so block
|
||||
mMediaCache->GetReentrantMonitor().Wait();
|
||||
mon.Wait();
|
||||
if (mClosed) {
|
||||
// We may have successfully read some data, but let's just throw
|
||||
// that out.
|
||||
|
@ -2402,9 +2419,9 @@ MediaCacheStream::ReadAt(int64_t aOffset, char* aBuffer,
|
|||
NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
|
||||
|
||||
ReentrantMonitorAutoEnter mon(mMediaCache->GetReentrantMonitor());
|
||||
nsresult rv = SeekInternal(aOffset);
|
||||
nsresult rv = Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
|
||||
if (NS_FAILED(rv)) return rv;
|
||||
return ReadInternal(aBuffer, aCount, aBytes);
|
||||
return Read(aBuffer, aCount, aBytes);
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
|
|
@ -335,12 +335,17 @@ public:
|
|||
// These methods must be called on a different thread from the main
|
||||
// thread. They should always be called on the same thread for a given
|
||||
// stream.
|
||||
// This can fail when aWhence is NS_SEEK_END and no stream length
|
||||
// is known.
|
||||
nsresult Seek(int32_t aWhence, int64_t aOffset);
|
||||
int64_t Tell();
|
||||
// Seeks to aOffset in the stream then performs a Read operation.
|
||||
// *aBytes gets the number of bytes that were actually read. This can
|
||||
// be less than aCount. If the first byte of data is not in the cache,
|
||||
// this will block until the data is available or the stream is
|
||||
// closed, otherwise it won't block.
|
||||
nsresult Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes);
|
||||
// Seeks to aOffset in the stream then performs a Read operation. See
|
||||
// 'Read' for argument and return details.
|
||||
nsresult ReadAt(int64_t aOffset, char* aBuffer,
|
||||
uint32_t aCount, uint32_t* aBytes);
|
||||
|
||||
|
@ -434,13 +439,6 @@ private:
|
|||
// Update mPrincipal given that data has been received from aPrincipal
|
||||
bool UpdatePrincipal(nsIPrincipal* aPrincipal);
|
||||
|
||||
nsresult SeekInternal(int64_t aOffset);
|
||||
// *aBytes gets the number of bytes that were actually read. This can
|
||||
// be less than aCount. If the first byte of data is not in the cache,
|
||||
// this will block until the data is available or the stream is
|
||||
// closed, otherwise it won't block.
|
||||
nsresult ReadInternal(char* aBuffer, uint32_t aCount, uint32_t* aBytes);
|
||||
|
||||
// Instance of MediaCache to use with this MediaCacheStream.
|
||||
RefPtr<MediaCache> mMediaCache;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include "WidevineUtils.h"
|
||||
#include "WidevineFileIO.h"
|
||||
#include <stdarg.h>
|
||||
#include "base/time.h"
|
||||
|
||||
using namespace cdm;
|
||||
using namespace std;
|
||||
|
@ -238,11 +239,7 @@ WidevineDecryptor::SetTimer(int64_t aDelayMs, void* aContext)
|
|||
Time
|
||||
WidevineDecryptor::GetCurrentWallTime()
|
||||
{
|
||||
GMPTimestamp gmpTime = 0;
|
||||
GMPGetCurrentTime(&gmpTime);
|
||||
double t = (double)gmpTime / 1e3;
|
||||
CDM_LOG("Decryptor::GetCurrentWallTime()= %lf", t);
|
||||
return t;
|
||||
return base::Time::Now().ToDoubleT();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -29,3 +29,5 @@ LOCAL_INCLUDES += [
|
|||
|
||||
if CONFIG['CLANG_CXX']:
|
||||
CXXFLAGS += ['-Wno-error=shadow']
|
||||
|
||||
include('/ipc/chromium/chromium-config.mozbuild')
|
||||
|
|
|
@ -796,7 +796,7 @@ CamerasParent::RecvStartCapture(const CaptureEngine& aCapEngine,
|
|||
new CallbackHelper(static_cast<CaptureEngine>(aCapEngine), capnum, self));
|
||||
|
||||
engine = self->mEngines[aCapEngine];
|
||||
engine->WithEntry(capnum, [capnum, &engine, &error, &ipcCaps, &cbh](VideoEngine::CaptureEntry& cap) {
|
||||
engine->WithEntry(capnum, [&engine, &error, &ipcCaps, &cbh](VideoEngine::CaptureEntry& cap) {
|
||||
error = 0;
|
||||
webrtc::VideoCaptureCapability capability;
|
||||
capability.width = ipcCaps.width();
|
||||
|
@ -841,7 +841,7 @@ CamerasParent::StopCapture(const CaptureEngine& aCapEngine,
|
|||
const int& capnum)
|
||||
{
|
||||
if (auto engine = EnsureInitialized(aCapEngine)) {
|
||||
engine->WithEntry(capnum,[capnum](VideoEngine::CaptureEntry& cap){
|
||||
engine->WithEntry(capnum,[](VideoEngine::CaptureEntry& cap){
|
||||
if (cap.VideoCapture()) {
|
||||
cap.VideoCapture()->StopCapture();
|
||||
cap.VideoCapture()->DeRegisterCaptureDataCallback();
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
exclude MANIFEST.in
|
||||
include requirements.txt
|
||||
recursive-include external_media_harness *
|
||||
recursive-include external_media_tests *
|
|
@ -1,5 +0,0 @@
|
|||
external-media-tests
|
||||
===================
|
||||
|
||||
Documentation for this library has moved to https://developer.mozilla.org/en-US/docs/Mozilla/QA/external-media-tests.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
_build
|
||||
_static
|
||||
_templates
|
|
@ -1,216 +0,0 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ExternalMediaTests.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ExternalMediaTests.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/ExternalMediaTests"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ExternalMediaTests"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
|
@ -1,297 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# External Media Tests documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Mar 15 15:58:18 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'External Media Tests'
|
||||
copyright = u'2015-2016, Mozilla, Inc.'
|
||||
author = u'Syd Polk and Maja Frydrychowicz'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd:
|
||||
try:
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'ExternalMediaTestsdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'ExternalMediaTests.tex', u'External Media Tests Documentation',
|
||||
u'Syd Polk and Maja Frydrychowicz', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'externalmediatests', u'External Media Tests Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'ExternalMediaTests', u'External Media Tests Documentation',
|
||||
author, 'ExternalMediaTests', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
|
@ -1,19 +0,0 @@
|
|||
external_media_harness package
|
||||
==============================
|
||||
|
||||
Test case classes for use in video tests
|
||||
|
||||
external_media_harness.testcase module
|
||||
--------------------------------------
|
||||
|
||||
.. autoclass:: external_media_harness.testcase.MediaTestCase
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: external_media_harness.testcase.NetworkBandwidthTestCase
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: external_media_harness.testcase.VideoPlaybackTestsMixin
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -1,27 +0,0 @@
|
|||
VideoPuppeteer
|
||||
==============
|
||||
|
||||
|
||||
video_puppeteer.VideoPuppeteer
|
||||
------------------------------
|
||||
|
||||
.. autoclass:: external_media_tests.media_utils.video_puppeteer.VideoPuppeteer
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
video_puppeteer.VideoException
|
||||
------------------------------
|
||||
|
||||
.. autoexception:: external_media_tests.media_utils.video_puppeteer.VideoException
|
||||
|
||||
video_puppeteer.playback_started
|
||||
--------------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.media_utils.video_puppeteer.playback_started
|
||||
|
||||
video_puppeteer.playback_done
|
||||
-----------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.media_utils.video_puppeteer.playback_done
|
||||
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
YoutubePuppeteer
|
||||
================
|
||||
|
||||
youtube_puppeteer.YouTubePuppeteer
|
||||
----------------------------------
|
||||
|
||||
.. autoclass:: external_media_tests.media_utils.youtube_puppeteer.YouTubePuppeteer
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
youtube_puppeteer.playback_started
|
||||
----------------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.media_utils.youtube_puppeteer.playback_started
|
||||
|
||||
youtube_puppeteer.playback_done
|
||||
-------------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.media_utils.youtube_puppeteer.playback_done
|
||||
|
||||
youtbue_puppeteer.wait_for_almost_done
|
||||
--------------------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.media_utils.youtube_puppeteer.wait_for_almost_done
|
||||
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
|
||||
external_media_tests package
|
||||
============================
|
||||
|
||||
This document highlights the utility classes for tests. In general, the indvidiual tests are not documented here.
|
||||
|
||||
Test pacakges
|
||||
-------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
external_media_tests.media_tests.video_puppeteer
|
||||
external_media_tests.media_tests.youtube_puppeteer
|
||||
|
||||
|
||||
external_media_tests.utils.verbose_until
|
||||
----------------------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.utils.verbose_until
|
||||
|
||||
external_media_tests.utils.save_memory_report
|
||||
---------------------------------------------
|
||||
|
||||
.. autofunction:: external_media_tests.utils.save_memory_report
|
||||
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
.. py:currentmodule:: external_media_tests
|
||||
|
||||
External Media Tests
|
||||
====================
|
||||
|
||||
External Media Tests is a library built on top of `Firefox Puppeter`_ and the `Marionette python client`_. It is designed to test playback of video elements embedded in web pages, independent of vendor. Using this library, you can write tests which play, pause, and stop videos, as well as inspect properties such as currentTime().
|
||||
|
||||
.. _Marionette python client: http://marionette-client.readthedocs.org/en/latest
|
||||
.. _Firefox Puppeter: http://firefox-puppeteer.readthedocs.org/en/latest/
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
External Media Tests lives in `External Media Tests Source`_. Documentation for installation and usage lives on `External Media Tests`_; this documentation is API documentation for the various pieces of the test library.
|
||||
|
||||
.. _External Media Tests Source: https://hg.mozilla.org/dom/media/test/external
|
||||
.. _External Media Tests: https://developer.mozilla.org/en-US/docs/Mozilla/QA/external-media-tests
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
external_media_harness
|
||||
external_media_tests
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
echo. coverage to run coverage check of the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
REM Check if sphinx-build is available and fallback to Python version if any
|
||||
%SPHINXBUILD% 1>NUL 2>NUL
|
||||
if errorlevel 9009 goto sphinx_python
|
||||
goto sphinx_ok
|
||||
|
||||
:sphinx_python
|
||||
|
||||
set SPHINXBUILD=python -m sphinx.__init__
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:sphinx_ok
|
||||
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ExternalMediaTests.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ExternalMediaTests.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "coverage" (
|
||||
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of coverage in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/coverage/python.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
|
@ -1,5 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from runtests import cli
|
|
@ -1,103 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import mozlog
|
||||
|
||||
from manifestparser import read_ini
|
||||
from marionette_harness import (
|
||||
BaseMarionetteTestRunner,
|
||||
BaseMarionetteArguments,
|
||||
BrowserMobProxyArguments,
|
||||
)
|
||||
from marionette_harness.runtests import MarionetteHarness, cli as mn_cli
|
||||
|
||||
import external_media_tests
|
||||
from testcase import MediaTestCase
|
||||
from external_media_tests.media_utils.video_puppeteer import debug_script
|
||||
|
||||
|
||||
class MediaTestArgumentsBase(object):
|
||||
name = 'Firefox Media Tests'
|
||||
args = [
|
||||
[['--urls'], {
|
||||
'help': 'ini file of urls to make available to all tests',
|
||||
'default': os.path.join(external_media_tests.urls, 'default.ini'),
|
||||
}],
|
||||
]
|
||||
|
||||
def verify_usage_handler(self, args):
|
||||
if args.urls:
|
||||
if not os.path.isfile(args.urls):
|
||||
raise ValueError('--urls must provide a path to an ini file')
|
||||
else:
|
||||
path = os.path.abspath(args.urls)
|
||||
args.video_urls = MediaTestArgumentsBase.get_urls(path)
|
||||
if not args.video_urls:
|
||||
raise ValueError('list of video URLs cannot be empty')
|
||||
|
||||
def parse_args_handler(self, args):
|
||||
if not args.tests:
|
||||
args.tests = [external_media_tests.manifest]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_urls(manifest):
|
||||
with open(manifest, 'r'):
|
||||
return [line[0] for line in read_ini(manifest)]
|
||||
|
||||
|
||||
class MediaTestArguments(BaseMarionetteArguments):
|
||||
def __init__(self, **kwargs):
|
||||
BaseMarionetteArguments.__init__(self, **kwargs)
|
||||
self.register_argument_container(MediaTestArgumentsBase())
|
||||
self.register_argument_container(BrowserMobProxyArguments())
|
||||
|
||||
|
||||
class MediaTestRunner(BaseMarionetteTestRunner):
|
||||
def __init__(self, **kwargs):
|
||||
BaseMarionetteTestRunner.__init__(self, **kwargs)
|
||||
if not self.server_root:
|
||||
self.server_root = external_media_tests.resources
|
||||
# pick up prefs from marionette_driver.geckoinstance.DesktopInstance
|
||||
self.app = 'fxdesktop'
|
||||
self.test_handlers = [MediaTestCase]
|
||||
|
||||
# Used in HTML report (--log-html)
|
||||
def gather_media_debug(test, status):
|
||||
rv = {}
|
||||
marionette = test._marionette_weakref()
|
||||
|
||||
if marionette.session is not None:
|
||||
try:
|
||||
with marionette.using_context(marionette.CONTEXT_CHROME):
|
||||
debug_lines = marionette.execute_script(debug_script)
|
||||
if debug_lines:
|
||||
name = 'mozMediaSourceObject.mozDebugReaderData'
|
||||
rv[name] = '\n'.join(debug_lines)
|
||||
else:
|
||||
logger = mozlog.get_default_logger()
|
||||
logger.info('No data available about '
|
||||
'mozMediaSourceObject')
|
||||
except:
|
||||
logger = mozlog.get_default_logger()
|
||||
logger.warning('Failed to gather test failure media debug',
|
||||
exc_info=True)
|
||||
return rv
|
||||
|
||||
self.result_callbacks.append(gather_media_debug)
|
||||
|
||||
|
||||
class FirefoxMediaHarness(MarionetteHarness):
|
||||
def parse_args(self, *args, **kwargs):
|
||||
return MarionetteHarness.parse_args(self, {'mach': sys.stdout})
|
||||
|
||||
|
||||
def cli():
|
||||
mn_cli(MediaTestRunner, MediaTestArguments, FirefoxMediaHarness)
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
|
@ -1,335 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import re
|
||||
import os
|
||||
|
||||
from marionette_driver import Wait
|
||||
from marionette_driver.errors import TimeoutException
|
||||
from marionette_harness import (
|
||||
BrowserMobProxyTestCaseMixin,
|
||||
MarionetteTestCase,
|
||||
Marionette,
|
||||
SkipTest,
|
||||
)
|
||||
|
||||
from firefox_puppeteer import PuppeteerMixin
|
||||
from external_media_tests.utils import (timestamp_now, verbose_until)
|
||||
from external_media_tests.media_utils.video_puppeteer import (
|
||||
VideoException,
|
||||
VideoPuppeteer
|
||||
)
|
||||
|
||||
|
||||
class MediaTestCase(PuppeteerMixin, MarionetteTestCase):
|
||||
|
||||
"""
|
||||
Necessary methods for MSE playback
|
||||
|
||||
:param video_urls: Urls you are going to play as part of the tests.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.video_urls = kwargs.pop('video_urls', False)
|
||||
super(MediaTestCase, self).__init__(*args, **kwargs)
|
||||
|
||||
def save_screenshot(self):
|
||||
"""
|
||||
Make a screenshot of the window that is currently playing the video
|
||||
element.
|
||||
"""
|
||||
screenshot_dir = os.path.join(self.marionette.instance.workspace or '',
|
||||
'screenshots')
|
||||
filename = ''.join([self.id().replace(' ', '-'),
|
||||
'_',
|
||||
str(timestamp_now()),
|
||||
'.png'])
|
||||
path = os.path.join(screenshot_dir, filename)
|
||||
if not os.path.exists(screenshot_dir):
|
||||
os.makedirs(screenshot_dir)
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
img_data = self.marionette.screenshot()
|
||||
with open(path, 'wb') as f:
|
||||
f.write(img_data.decode('base64'))
|
||||
self.logger.info('Screenshot saved in {}'.format(os.path.abspath(path)))
|
||||
|
||||
def log_video_debug_lines(self, video):
|
||||
"""
|
||||
Log the debugging information that Firefox provides for video elements.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
debug_lines = video.get_debug_lines()
|
||||
if debug_lines:
|
||||
self.logger.info('\n'.join(debug_lines))
|
||||
|
||||
def run_playback(self, video):
|
||||
"""
|
||||
Play the video all of the way through, or for the requested duration,
|
||||
whichever comes first. Raises if the video stalls for too long.
|
||||
|
||||
:param video: VideoPuppeteer instance to play.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
self.logger.info(video.test_url)
|
||||
try:
|
||||
verbose_until(Wait(video, interval=video.interval,
|
||||
timeout=video.expected_duration * 1.3 +
|
||||
video.stall_wait_time),
|
||||
video, VideoPuppeteer.playback_done)
|
||||
except VideoException as e:
|
||||
raise self.failureException(e)
|
||||
|
||||
def check_playback_starts(self, video):
|
||||
"""
|
||||
Check to see if a given video will start. Raises if the video does not
|
||||
start.
|
||||
|
||||
:param video: VideoPuppeteer instance to play.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
self.logger.info(video.test_url)
|
||||
try:
|
||||
verbose_until(Wait(video, timeout=video.timeout),
|
||||
video, VideoPuppeteer.playback_started)
|
||||
except TimeoutException as e:
|
||||
raise self.failureException(e)
|
||||
|
||||
def skipTest(self, reason):
|
||||
"""
|
||||
Skip this test.
|
||||
|
||||
Skip with marionette.marionette_test import SkipTest so that it
|
||||
gets recognized a skip in marionette.marionette_test.CommonTestCase.run
|
||||
"""
|
||||
raise SkipTest(reason)
|
||||
|
||||
|
||||
class NetworkBandwidthTestCase(MediaTestCase):
|
||||
"""
|
||||
Test MSE playback while network bandwidth is limited. Uses browsermobproxy
|
||||
(https://bmp.lightbody.net/). Please see
|
||||
https://developer.mozilla.org/en-US/docs/Mozilla/QA/external-media-tests
|
||||
for more information on setting up browsermob_proxy.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NetworkBandwidthTestCase, self).__init__(*args, **kwargs)
|
||||
BrowserMobProxyTestCaseMixin.__init__(self, *args, **kwargs)
|
||||
self.proxy = None
|
||||
|
||||
def setUp(self):
|
||||
super(NetworkBandwidthTestCase, self).setUp()
|
||||
BrowserMobProxyTestCaseMixin.setUp(self)
|
||||
self.proxy = self.create_browsermob_proxy()
|
||||
|
||||
def tearDown(self):
|
||||
super(NetworkBandwidthTestCase, self).tearDown()
|
||||
BrowserMobProxyTestCaseMixin.tearDown(self)
|
||||
self.proxy = None
|
||||
|
||||
def run_videos(self, timeout=60):
|
||||
"""
|
||||
Run each of the videos in the video list. Raises if something goes
|
||||
wrong in playback.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
video = VideoPuppeteer(self.marionette, url, stall_wait_time=60,
|
||||
set_duration=60, timeout=timeout)
|
||||
self.run_playback(video)
|
||||
|
||||
|
||||
class VideoPlaybackTestsMixin(object):
|
||||
|
||||
"""
|
||||
Test MSE playback in HTML5 video element.
|
||||
|
||||
These tests should pass on any site where a single video element plays
|
||||
upon loading and is uninterrupted (by ads, for example).
|
||||
|
||||
This tests both starting videos and performing partial playback at one
|
||||
minute each, and is the test that should be run frequently in automation.
|
||||
"""
|
||||
|
||||
def test_playback_starts(self):
|
||||
"""
|
||||
Test to make sure that playback of the video element starts for each
|
||||
video.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
try:
|
||||
video = VideoPuppeteer(self.marionette, url, timeout=60)
|
||||
# Second playback_started check in case video._start_time
|
||||
# is not 0
|
||||
self.check_playback_starts(video)
|
||||
video.pause()
|
||||
except TimeoutException as e:
|
||||
raise self.failureException(e)
|
||||
|
||||
def test_video_playback_partial(self):
|
||||
"""
|
||||
Test to make sure that playback of 60 seconds works for each video.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
video = VideoPuppeteer(self.marionette, url,
|
||||
stall_wait_time=10,
|
||||
set_duration=60)
|
||||
self.run_playback(video)
|
||||
|
||||
|
||||
class NetworkBandwidthTestsMixin(object):
|
||||
|
||||
"""
|
||||
Test video urls with various bandwidth settings.
|
||||
"""
|
||||
|
||||
def test_playback_limiting_bandwidth_250(self):
|
||||
self.proxy.limits({'downstream_kbps': 250})
|
||||
self.run_videos(timeout=120)
|
||||
|
||||
def test_playback_limiting_bandwidth_500(self):
|
||||
self.proxy.limits({'downstream_kbps': 500})
|
||||
self.run_videos(timeout=120)
|
||||
|
||||
def test_playback_limiting_bandwidth_1000(self):
|
||||
self.proxy.limits({'downstream_kbps': 1000})
|
||||
self.run_videos(timeout=120)
|
||||
|
||||
|
||||
reset_widevine_gmp_script = """
|
||||
navigator.requestMediaKeySystemAccess('com.widevine.alpha',
|
||||
[{initDataTypes: ['cenc']}]).then(
|
||||
function(access) {
|
||||
marionetteScriptFinished('success');
|
||||
},
|
||||
function(ex) {
|
||||
marionetteScriptFinished(ex);
|
||||
}
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
class EMESetupMixin(object):
|
||||
|
||||
"""
|
||||
An object that needs to use the Widevine GMP system must inherit
|
||||
from this class, and then call check_eme_system() to insure that everything
|
||||
is setup correctly.
|
||||
"""
|
||||
|
||||
version_needs_reset = True
|
||||
|
||||
def check_eme_system(self):
|
||||
"""
|
||||
Download the most current version of the Widevine GMP
|
||||
Plugins. Verify that all MSE and EME prefs are set correctly. Raises
|
||||
if things are not OK.
|
||||
"""
|
||||
self.set_eme_prefs()
|
||||
self.reset_GMP_version()
|
||||
assert(self.check_eme_prefs())
|
||||
|
||||
def set_eme_prefs(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=1187471#c28
|
||||
# 2015-09-28 cpearce says this is no longer necessary, but in case
|
||||
# we are working with older firefoxes...
|
||||
self.marionette.set_pref('media.gmp.trial-create.enabled', False)
|
||||
|
||||
def reset_GMP_version(self):
|
||||
if EMESetupMixin.version_needs_reset:
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
if self.marionette.get_pref('media.gmp-widevinecdm.version'):
|
||||
self.marionette.reset_pref('media.gmp-widevinecdm.version')
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
widevine_result = self.marionette.execute_async_script(
|
||||
reset_widevine_gmp_script,
|
||||
script_timeout=60000)
|
||||
if not widevine_result == 'success':
|
||||
raise VideoException(
|
||||
'ERROR: Resetting Widevine GMP failed {}'
|
||||
.format(widevine_result))
|
||||
|
||||
EMESetupMixin.version_needs_reset = False
|
||||
|
||||
def check_and_log_boolean_pref(self, pref_name, expected_value):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
pref_value = self.marionette.get_pref(pref_name)
|
||||
|
||||
if pref_value is None:
|
||||
self.logger.info('Pref {} has no value.'.format(pref_name))
|
||||
return False
|
||||
else:
|
||||
self.logger.info('Pref {} = {}'.format(pref_name, pref_value))
|
||||
if pref_value != expected_value:
|
||||
self.logger.info('Pref {} has unexpected value.'
|
||||
.format(pref_name))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_and_log_integer_pref(self, pref_name, minimum_value=0):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
pref_value = self.marionette.get_pref(pref_name)
|
||||
|
||||
if pref_value is None:
|
||||
self.logger.info('Pref {} has no value.'.format(pref_name))
|
||||
return False
|
||||
else:
|
||||
self.logger.info('Pref {} = {}'.format(pref_name, pref_value))
|
||||
|
||||
match = re.search('^\d+$', pref_value)
|
||||
if not match:
|
||||
self.logger.info('Pref {} is not an integer'
|
||||
.format(pref_name))
|
||||
return False
|
||||
|
||||
return pref_value >= minimum_value
|
||||
|
||||
def chceck_and_log_version_string_pref(self, pref_name, minimum_value='0'):
|
||||
"""
|
||||
Compare a pref made up of integers separated by stops .s, with a
|
||||
version string of the same format. The number of integers in each
|
||||
string does not need to match. The comparison is done by converting
|
||||
each to an integer array and comparing those. Both version strings
|
||||
must be made up of only integers, or this method will raise an
|
||||
unhandled exception of type ValueError when the conversion to int
|
||||
fails.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
pref_value = self.marionette.get_pref(pref_name)
|
||||
|
||||
if pref_value is None:
|
||||
self.logger.info('Pref {} has no value.'.format(pref_name))
|
||||
return False
|
||||
else:
|
||||
self.logger.info('Pref {} = {}'.format(pref_name, pref_value))
|
||||
|
||||
match = re.search('^\d(.\d+)*$', pref_value)
|
||||
if not match:
|
||||
self.logger.info('Pref {} is not a version string'
|
||||
.format(pref_name))
|
||||
return False
|
||||
|
||||
pref_ints = [int(n) for n in pref_value.split('.')]
|
||||
minumum_ints = [int(n) for n in minimum_value.split('.')]
|
||||
|
||||
return pref_ints >= minumum_ints
|
||||
|
||||
def check_eme_prefs(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CHROME):
|
||||
return all([
|
||||
self.check_and_log_boolean_pref(
|
||||
'media.mediasource.enabled', True),
|
||||
self.check_and_log_boolean_pref(
|
||||
'media.eme.enabled', True),
|
||||
self.check_and_log_boolean_pref(
|
||||
'media.mediasource.mp4.enabled', True),
|
||||
self.check_and_log_boolean_pref(
|
||||
'media.gmp-widevinecdm.enabled', True),
|
||||
self.chceck_and_log_version_string_pref(
|
||||
'media.gmp-widevinecdm.version', '1.0.0.0')
|
||||
])
|
|
@ -1,10 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
|
||||
root = os.path.abspath(os.path.dirname(__file__))
|
||||
manifest = os.path.join(root, 'manifest.ini')
|
||||
resources = os.path.join(root, 'resources')
|
||||
urls = os.path.join(root, 'urls')
|
|
@ -1 +0,0 @@
|
|||
[include:playback/manifest.ini]
|
|
@ -1,192 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
from collections import namedtuple
|
||||
from time import clock
|
||||
|
||||
from marionette_driver import By, expected, Wait
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from video_puppeteer import VideoPuppeteer, TimeRanges
|
||||
from external_media_tests.utils import verbose_until
|
||||
|
||||
|
||||
class TwitchPuppeteer(VideoPuppeteer):
|
||||
"""
|
||||
Wrapper around a Twitch .player element.
|
||||
|
||||
Note that the twitch stream needs to be playing for the puppeteer to work
|
||||
correctly. Since twitch will load a player element for streams that are
|
||||
not currently playing, the puppeteer will still detect a player element,
|
||||
however the time ranges will not increase, and tests will fail.
|
||||
|
||||
Compared to video puppeteer, this class has the benefit of accessing the
|
||||
twitch player element as well as the video element. The twitch player
|
||||
element reports additional information to aid in testing -- such as if an
|
||||
ad is playing. However, the attributes of this element do not appear to be
|
||||
documented, which may leave them vulnerable to undocumented changes in
|
||||
behaviour.
|
||||
"""
|
||||
_player_var_script = (
|
||||
'var player_data_screen = '
|
||||
'arguments[1].wrappedJSObject.getAttribute("data-screen");'
|
||||
)
|
||||
"""
|
||||
A string containing JS that will assign player state to
|
||||
variables. This is similar to `_video_var_script` from
|
||||
`VideoPuppeteer`. See `_video_var_script` for more information on the
|
||||
motivation for this method.
|
||||
"""
|
||||
|
||||
def __init__(self, marionette, url, autostart=True,
|
||||
set_duration=10.0, **kwargs):
|
||||
self.player = None
|
||||
self._last_seen_player_state = None
|
||||
super(TwitchPuppeteer,
|
||||
self).__init__(marionette, url, set_duration=set_duration,
|
||||
autostart=False, **kwargs)
|
||||
wait = Wait(self.marionette, timeout=30)
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
verbose_until(wait, self,
|
||||
expected.element_present(By.CLASS_NAME,
|
||||
'player'))
|
||||
self.player = self.marionette.find_element(By.CLASS_NAME,
|
||||
'player')
|
||||
self.marionette.execute_script("log('.player "
|
||||
"element obtained');")
|
||||
if autostart:
|
||||
self.start()
|
||||
|
||||
def _update_expected_duration(self):
|
||||
if 0 < self._set_duration:
|
||||
self.expected_duration = self._set_duration
|
||||
else:
|
||||
# If we don't have a set duration we don't know how long is left
|
||||
# in the stream.
|
||||
self.expected_duration = float("inf")
|
||||
|
||||
def _calculate_remaining_time(self, played_ranges):
|
||||
"""
|
||||
Override of video_puppeteer's _calculate_remaining_time. See that
|
||||
method for primary documentation.
|
||||
|
||||
This override is in place to adjust how remaining time is handled.
|
||||
Twitch ads can cause small stutters which result in multiple played
|
||||
ranges, despite no seeks being initiated by the tests. As such, when
|
||||
calculating the remaining time, the start time is the min of all
|
||||
played start times, and the end time is the max of played end times.
|
||||
This being sensible behaviour relies on the tests not attempting seeks.
|
||||
|
||||
:param played_ranges: A TimeRanges object containing played ranges.
|
||||
:return: The remaining time expected for this puppeteer.
|
||||
"""
|
||||
min_played_time = min(
|
||||
[played_ranges.start(i) for i in range(0, played_ranges.length)])
|
||||
max_played_time = max(
|
||||
[played_ranges.end(i) for i in range(0, played_ranges.length)])
|
||||
played_duration = max_played_time - min_played_time
|
||||
return self.expected_duration - played_duration
|
||||
|
||||
def _execute_twitch_script(self, script):
|
||||
"""
|
||||
Execute JS script in content context with access to video element and
|
||||
Twitch .player element.
|
||||
|
||||
:param script: script to be executed.
|
||||
|
||||
:return: value returned by script
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
return self.marionette.execute_script(script,
|
||||
script_args=[self.video,
|
||||
self.player])
|
||||
|
||||
@staticmethod
|
||||
def _twitch_state_named_tuple():
|
||||
"""
|
||||
Create a named tuple class that can be used to store state snapshots
|
||||
of the wrapped twitch player. The fields in the tuple should be used
|
||||
as follows:
|
||||
|
||||
player_data_screen: the current displayed content, appears to be set
|
||||
to nothing if no ad has been played, 'advertisement' during ad
|
||||
playback, and 'content' following ad playback.
|
||||
"""
|
||||
return namedtuple('player_state_info',
|
||||
['player_data_screen'])
|
||||
|
||||
def _create_player_state_info(self, **player_state_info_kwargs):
|
||||
"""
|
||||
Create an instance of the state info named tuple. This function
|
||||
expects a dictionary containing the following keys:
|
||||
player_data_screen.
|
||||
|
||||
For more information on the above keys and their values see
|
||||
`_twitch_state_named_tuple`.
|
||||
|
||||
:return: A named tuple 'player_state_info', derived from arguments and
|
||||
state information from the puppeteer.
|
||||
"""
|
||||
# Create player snapshot
|
||||
state_info = self._twitch_state_named_tuple()
|
||||
return state_info(**player_state_info_kwargs)
|
||||
|
||||
@property
|
||||
def _fetch_state_script(self):
|
||||
if not self._fetch_state_script_string:
|
||||
self._fetch_state_script_string = (
|
||||
self._video_var_script +
|
||||
self._player_var_script +
|
||||
'return ['
|
||||
'baseURI,'
|
||||
'currentTime,'
|
||||
'duration,'
|
||||
'[buffered.length, bufferedRanges],'
|
||||
'[played.length, playedRanges],'
|
||||
'totalFrames,'
|
||||
'droppedFrames,'
|
||||
'corruptedFrames,'
|
||||
'player_data_screen];')
|
||||
return self._fetch_state_script_string
|
||||
|
||||
def _refresh_state(self):
|
||||
"""
|
||||
Refresh the snapshot of the underlying video and player state. We do
|
||||
this all in one so that the state doesn't change in between queries.
|
||||
|
||||
We also store information thouat can be derived from the snapshotted
|
||||
information, such as lag. This is stored in the last seen state to
|
||||
stress that it's based on the snapshot.
|
||||
"""
|
||||
values = self._execute_twitch_script(self._fetch_state_script)
|
||||
video_keys = ['base_uri', 'current_time', 'duration',
|
||||
'raw_buffered_ranges', 'raw_played_ranges',
|
||||
'total_frames', 'dropped_frames', 'corrupted_frames']
|
||||
player_keys = ['player_data_screen']
|
||||
# Get video state
|
||||
self._last_seen_video_state = (
|
||||
self._create_video_state_info(**dict(
|
||||
zip(video_keys, values[:len(video_keys)]))))
|
||||
# Get player state
|
||||
self._last_seen_player_state = (
|
||||
self._create_player_state_info(**dict(
|
||||
zip(player_keys, values[-len(player_keys):]))))
|
||||
|
||||
def __str__(self):
|
||||
messages = [super(TwitchPuppeteer, self).__str__()]
|
||||
if not self.player:
|
||||
messages += ['\t.player: None']
|
||||
return '\n'.join(messages)
|
||||
if not self._last_seen_player_state:
|
||||
messages += ['\t.player: No last seen state']
|
||||
return '\n'.join(messages)
|
||||
messages += ['.player: {']
|
||||
for field in self._last_seen_player_state._fields:
|
||||
# For compatibility with different test environments we force
|
||||
# ascii
|
||||
field_ascii = (
|
||||
unicode(getattr(self._last_seen_player_state, field)).encode(
|
||||
'ascii', 'replace'))
|
||||
messages += [('\t{}: {}'.format(field, field_ascii))]
|
||||
messages += '}'
|
||||
return '\n'.join(messages)
|
|
@ -1,459 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from collections import namedtuple
|
||||
from time import clock, sleep
|
||||
|
||||
from marionette_driver import By, expected, Wait
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from external_media_tests.utils import verbose_until
|
||||
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/gavinsharp/aboutmedia/blob/master/chrome/content/aboutmedia.xhtml
|
||||
debug_script = """
|
||||
var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIWebNavigation)
|
||||
.QueryInterface(Components.interfaces.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindow);
|
||||
var tabbrowser = mainWindow.gBrowser;
|
||||
for (var i=0; i < tabbrowser.browsers.length; ++i) {
|
||||
var b = tabbrowser.getBrowserAtIndex(i);
|
||||
var media = b.contentDocumentAsCPOW.getElementsByTagName('video');
|
||||
for (var j=0; j < media.length; ++j) {
|
||||
var ms = media[j].mozMediaSourceObject;
|
||||
if (ms) {
|
||||
debugLines = ms.mozDebugReaderData.split(\"\\n\");
|
||||
return debugLines;
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
class VideoPuppeteer(object):
|
||||
"""
|
||||
Wrapper to control and introspect HTML5 video elements.
|
||||
|
||||
A note about properties like current_time and duration:
|
||||
These describe whatever stream is playing when the state is checked.
|
||||
It is possible that many different streams are dynamically spliced
|
||||
together, so the video stream that is currently playing might be the main
|
||||
video or it might be something else, like an ad, for example.
|
||||
|
||||
:param marionette: The marionette instance this runs in.
|
||||
:param url: the URL of the page containing the video element.
|
||||
:param video_selector: the selector of the element that we want to watch.
|
||||
This is set by default to 'video', which is what most sites use, but
|
||||
others should work.
|
||||
:param interval: The polling interval that is used to check progress.
|
||||
:param set_duration: When set to >0, the polling and checking will stop at
|
||||
the number of seconds specified. Otherwise, this will stop at the end
|
||||
of the video.
|
||||
:param stall_wait_time: The amount of time to wait to see if a stall has
|
||||
cleared. If 0, do not check for stalls.
|
||||
:param timeout: The amount of time to wait until the video starts.
|
||||
"""
|
||||
|
||||
_video_var_script = (
|
||||
'var video = arguments[0];'
|
||||
'var baseURI = video.baseURI;'
|
||||
'var currentTime = video.wrappedJSObject.currentTime;'
|
||||
'var duration = video.wrappedJSObject.duration;'
|
||||
'var buffered = video.wrappedJSObject.buffered;'
|
||||
'var bufferedRanges = [];'
|
||||
'for (var i = 0; i < buffered.length; i++) {'
|
||||
'bufferedRanges.push([buffered.start(i), buffered.end(i)]);'
|
||||
'}'
|
||||
'var played = video.wrappedJSObject.played;'
|
||||
'var playedRanges = [];'
|
||||
'for (var i = 0; i < played.length; i++) {'
|
||||
'playedRanges.push([played.start(i), played.end(i)]);'
|
||||
'}'
|
||||
'var totalFrames = '
|
||||
'video.getVideoPlaybackQuality()["totalVideoFrames"];'
|
||||
'var droppedFrames = '
|
||||
'video.getVideoPlaybackQuality()["droppedVideoFrames"];'
|
||||
'var corruptedFrames = '
|
||||
'video.getVideoPlaybackQuality()["corruptedVideoFrames"];'
|
||||
)
|
||||
"""
|
||||
A string containing JS that assigns video state to variables.
|
||||
The purpose of this string script is to be appended to by this and
|
||||
any inheriting classes to return these and other variables. In the case
|
||||
of an inheriting class the script can be added to in order to fetch
|
||||
further relevant variables -- keep in mind we only want one script
|
||||
execution to prevent races, so it wouldn't do to have child classes
|
||||
run this script then their own, as there is potential for lag in
|
||||
between.
|
||||
|
||||
This script assigns a subset of the vars used later by the
|
||||
`_video_state_named_tuple` function. Please see that function's
|
||||
documentation for further information on these variables.
|
||||
"""
|
||||
|
||||
def __init__(self, marionette, url, video_selector='video', interval=1,
|
||||
set_duration=0, stall_wait_time=0, timeout=60,
|
||||
autostart=True):
|
||||
self.marionette = marionette
|
||||
self.test_url = url
|
||||
self.interval = interval
|
||||
self.stall_wait_time = stall_wait_time
|
||||
self.timeout = timeout
|
||||
self._set_duration = set_duration
|
||||
self.video = None
|
||||
self.expected_duration = 0
|
||||
self._first_seen_time = 0
|
||||
self._first_seen_wall_time = 0
|
||||
self._fetch_state_script_string = None
|
||||
self._last_seen_video_state = None
|
||||
wait = Wait(self.marionette, timeout=self.timeout)
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
self.marionette.navigate(self.test_url)
|
||||
print('URL: {}'.format(self.test_url))
|
||||
verbose_until(wait, self,
|
||||
expected.element_present(By.TAG_NAME, 'video'))
|
||||
videos_found = self.marionette.find_elements(By.CSS_SELECTOR,
|
||||
video_selector)
|
||||
if len(videos_found) > 1:
|
||||
print('{}: multiple video elements found. Using first.'.format(type(self).__name__))
|
||||
if len(videos_found) <= 0:
|
||||
print('{}: no video elements found.'.format(type(self).__name__))
|
||||
return
|
||||
self.video = videos_found[0]
|
||||
print('video element obtained')
|
||||
if autostart:
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
# To get an accurate expected_duration, playback must have started
|
||||
self._refresh_state()
|
||||
wait = Wait(self, timeout=self.timeout)
|
||||
verbose_until(wait, self, VideoPuppeteer.playback_started,
|
||||
"Check if video has played some range")
|
||||
self._first_seen_time = self._last_seen_video_state.current_time
|
||||
self._first_seen_wall_time = clock()
|
||||
self._update_expected_duration()
|
||||
|
||||
def get_debug_lines(self):
|
||||
"""
|
||||
Get Firefox internal debugging for the video element.
|
||||
|
||||
:return: A text string that has Firefox-internal debugging information.
|
||||
"""
|
||||
with self.marionette.using_context('chrome'):
|
||||
debug_lines = self.marionette.execute_script(debug_script)
|
||||
return debug_lines
|
||||
|
||||
def play(self):
|
||||
"""
|
||||
Tell the video element to Play.
|
||||
"""
|
||||
self._execute_video_script('arguments[0].wrappedJSObject.play();')
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Tell the video element to Pause.
|
||||
"""
|
||||
self._execute_video_script('arguments[0].wrappedJSObject.pause();')
|
||||
|
||||
def playback_started(self):
|
||||
"""
|
||||
Determine if video has started
|
||||
|
||||
:param self: The VideoPuppeteer instance that we are interested in.
|
||||
|
||||
:return: True if is playing; False otherwise
|
||||
"""
|
||||
self._refresh_state()
|
||||
try:
|
||||
played_ranges = self._last_seen_video_state.played
|
||||
return (
|
||||
played_ranges.length > 0 and
|
||||
played_ranges.start(0) < played_ranges.end(0) and
|
||||
played_ranges.end(0) > 0.0
|
||||
)
|
||||
except Exception as e:
|
||||
print ('Got exception {}'.format(e))
|
||||
return False
|
||||
|
||||
def playback_done(self):
|
||||
"""
|
||||
If we are near the end and there is still a video element, then
|
||||
we are essentially done. If this happens to be last time we are polled
|
||||
before the video ends, we won't get another chance.
|
||||
|
||||
:param self: The VideoPuppeteer instance that we are interested in.
|
||||
|
||||
:return: True if we are close enough to the end of playback; False
|
||||
otherwise.
|
||||
"""
|
||||
self._refresh_state()
|
||||
|
||||
if self._last_seen_video_state.remaining_time < self.interval:
|
||||
return True
|
||||
|
||||
# Check to see if the video has stalled. Accumulate the amount of lag
|
||||
# since the video started, and if it is too high, then raise.
|
||||
if (self.stall_wait_time and
|
||||
self._last_seen_video_state.lag > self.stall_wait_time):
|
||||
raise VideoException('Video {} stalled.\n{}'
|
||||
.format(self._last_seen_video_state.base_uri,
|
||||
self))
|
||||
|
||||
# We are cruising, so we are not done.
|
||||
return False
|
||||
|
||||
def _update_expected_duration(self):
|
||||
"""
|
||||
Update the duration of the target video at self.test_url (in seconds).
|
||||
This is based on the last seen state, so the state should be,
|
||||
refreshed at least once before this is called.
|
||||
|
||||
expected_duration represents the following: how long do we expect
|
||||
playback to last before we consider the video to be 'complete'?
|
||||
If we only want to play the first n seconds of the video,
|
||||
expected_duration is set to n.
|
||||
"""
|
||||
|
||||
# self._last_seen_video_state.duration is the duration of whatever was
|
||||
# playing when the state was checked. In this case, we assume the video
|
||||
# element always shows the same stream throughout playback (i.e. the
|
||||
# are no ads spliced into the main video, for example), so
|
||||
# self._last_seen_video_state.duration is the duration of the main
|
||||
# video.
|
||||
video_duration = self._last_seen_video_state.duration
|
||||
# Do our best to figure out where the video started playing
|
||||
played_ranges = self._last_seen_video_state.played
|
||||
if played_ranges.length > 0:
|
||||
# If we have a range we should only have on continuous range
|
||||
assert played_ranges.length == 1
|
||||
start_position = played_ranges.start(0)
|
||||
else:
|
||||
# If we don't have a range we should have a current time
|
||||
start_position = self._first_seen_time
|
||||
# In case video starts at t > 0, adjust target time partial playback
|
||||
remaining_video = video_duration - start_position
|
||||
if 0 < self._set_duration < remaining_video:
|
||||
self.expected_duration = self._set_duration
|
||||
else:
|
||||
self.expected_duration = remaining_video
|
||||
|
||||
def _calculate_remaining_time(self, played_ranges):
|
||||
"""
|
||||
Calculate the remaining time expected for this puppeteer. Note that
|
||||
this method accepts a played range rather than reading from the last
|
||||
seen state. This is so when building a new state we are not forced to
|
||||
read from the last one, and can use the played ranges associated with
|
||||
that new state to calculate the remaining time.
|
||||
|
||||
:param played_ranges: A TimeRanges object containing played ranges.
|
||||
For video_puppeteer we expect a single played range, but overrides may
|
||||
expect different behaviour.
|
||||
:return: The remaining time expected for this puppeteer.
|
||||
"""
|
||||
played_duration = played_ranges.end(0) - played_ranges.start(0)
|
||||
return self.expected_duration - played_duration
|
||||
|
||||
@staticmethod
|
||||
def _video_state_named_tuple():
|
||||
"""
|
||||
Create a named tuple class that can be used to store state snapshots
|
||||
of the wrapped element. The fields in the tuple should be used as
|
||||
follows:
|
||||
|
||||
base_uri: the baseURI attribute of the wrapped element.
|
||||
current_time: the current time of the wrapped element.
|
||||
duration: the duration of the wrapped element.
|
||||
buffered: the buffered ranges of the wrapped element. In its raw form
|
||||
this is as a list where the first element is the length and the second
|
||||
element is a list of 2 item lists, where each two items are a buffered
|
||||
range. Once assigned to the tuple this data should be wrapped in the
|
||||
TimeRanges class.
|
||||
played: the played ranges of the wrapped element. In its raw form this
|
||||
is as a list where the first element is the length and the second
|
||||
element is a list of 2 item lists, where each two items are a played
|
||||
range. Once assigned to the tuple this data should be wrapped in the
|
||||
TimeRanges class.
|
||||
lag: the difference in real world time and wrapped element time.
|
||||
Calculated as real world time passed - element time passed.
|
||||
totalFrames: number of total frames for the wrapped element
|
||||
droppedFrames: number of dropped frames for the wrapped element.
|
||||
corruptedFrames: number of corrupted frames for the wrapped.
|
||||
video_src: the src attribute of the wrapped element.
|
||||
|
||||
:return: A 'video_state_info' named tuple class.
|
||||
"""
|
||||
return namedtuple('video_state_info',
|
||||
['base_uri',
|
||||
'current_time',
|
||||
'duration',
|
||||
'remaining_time',
|
||||
'buffered',
|
||||
'played',
|
||||
'lag',
|
||||
'total_frames',
|
||||
'dropped_frames',
|
||||
'corrupted_frames',
|
||||
'video_src'])
|
||||
|
||||
def _create_video_state_info(self, **video_state_info_kwargs):
|
||||
"""
|
||||
Create an instance of the video_state_info named tuple. This function
|
||||
expects a dictionary populated with the following keys: current_time,
|
||||
duration, raw_played_ranges, total_frames, dropped_frames, and
|
||||
corrupted_frames.
|
||||
|
||||
Aside from raw_played_ranges, see `_video_state_named_tuple` for more
|
||||
information on the above keys and values. For raw_played_ranges a
|
||||
list is expected that can be consumed to make a TimeRanges object.
|
||||
|
||||
:return: A named tuple 'video_state_info' derived from arguments and
|
||||
state information from the puppeteer.
|
||||
"""
|
||||
raw_buffered_ranges = video_state_info_kwargs['raw_buffered_ranges']
|
||||
raw_played_ranges = video_state_info_kwargs['raw_played_ranges']
|
||||
# Remove raw ranges from dict as they are not used in the final named
|
||||
# tuple and will provide an unexpected kwarg if kept.
|
||||
del video_state_info_kwargs['raw_buffered_ranges']
|
||||
del video_state_info_kwargs['raw_played_ranges']
|
||||
# Create buffered ranges
|
||||
video_state_info_kwargs['buffered'] = (
|
||||
TimeRanges(raw_buffered_ranges[0], raw_buffered_ranges[1]))
|
||||
# Create played ranges
|
||||
video_state_info_kwargs['played'] = (
|
||||
TimeRanges(raw_played_ranges[0], raw_played_ranges[1]))
|
||||
# Calculate elapsed times
|
||||
elapsed_current_time = (video_state_info_kwargs['current_time'] -
|
||||
self._first_seen_time)
|
||||
elapsed_wall_time = clock() - self._first_seen_wall_time
|
||||
# Calculate lag
|
||||
video_state_info_kwargs['lag'] = (
|
||||
elapsed_wall_time - elapsed_current_time)
|
||||
# Calculate remaining time
|
||||
played_ranages = video_state_info_kwargs['played']
|
||||
if played_ranages.length > 0:
|
||||
video_state_info_kwargs['remaining_time'] = (
|
||||
self._calculate_remaining_time(played_ranages))
|
||||
else:
|
||||
# No playback has happened yet, remaining time is duration
|
||||
video_state_info_kwargs['remaining_time'] = self.expected_duration
|
||||
# Fetch non time critical source information
|
||||
video_state_info_kwargs['video_src'] = self.video.get_attribute('src')
|
||||
# Create video state snapshot
|
||||
state_info = self._video_state_named_tuple()
|
||||
return state_info(**video_state_info_kwargs)
|
||||
|
||||
@property
|
||||
def _fetch_state_script(self):
|
||||
if not self._fetch_state_script_string:
|
||||
self._fetch_state_script_string = (
|
||||
self._video_var_script +
|
||||
'return ['
|
||||
'baseURI,'
|
||||
'currentTime,'
|
||||
'duration,'
|
||||
'[buffered.length, bufferedRanges],'
|
||||
'[played.length, playedRanges],'
|
||||
'totalFrames,'
|
||||
'droppedFrames,'
|
||||
'corruptedFrames];')
|
||||
return self._fetch_state_script_string
|
||||
|
||||
def _refresh_state(self):
|
||||
"""
|
||||
Refresh the snapshot of the underlying video state. We do this all
|
||||
in one so that the state doesn't change in between queries.
|
||||
|
||||
We also store information that can be derived from the snapshotted
|
||||
information, such as lag. This is stored in the last seen state to
|
||||
stress that it's based on the snapshot.
|
||||
"""
|
||||
keys = ['base_uri', 'current_time', 'duration', 'raw_buffered_ranges',
|
||||
'raw_played_ranges', 'total_frames', 'dropped_frames',
|
||||
'corrupted_frames']
|
||||
values = self._execute_video_script(self._fetch_state_script)
|
||||
self._last_seen_video_state = (
|
||||
self._create_video_state_info(**dict(zip(keys, values))))
|
||||
|
||||
def _measure_progress(self):
|
||||
self._refresh_state()
|
||||
initial = self._last_seen_video_state.current_time
|
||||
sleep(1)
|
||||
self._refresh_state()
|
||||
return self._last_seen_video_state.current_time - initial
|
||||
|
||||
def _execute_video_script(self, script):
|
||||
"""
|
||||
Execute JS script in content context with access to video element.
|
||||
|
||||
:param script: script to be executed
|
||||
:return: value returned by script
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
return self.marionette.execute_script(script,
|
||||
script_args=[self.video])
|
||||
|
||||
def __str__(self):
|
||||
messages = ['{} - test url: {}: '
|
||||
.format(type(self).__name__, self.test_url)]
|
||||
if not self.video:
|
||||
messages += ['\tvideo: None']
|
||||
return '\n'.join(messages)
|
||||
if not self._last_seen_video_state:
|
||||
messages += ['\tvideo: No last seen state']
|
||||
return '\n'.join(messages)
|
||||
# Have video and state info
|
||||
messages += [
|
||||
'{',
|
||||
'\t(video)'
|
||||
]
|
||||
messages += ['\tinterval: {}'.format(self.interval)]
|
||||
messages += ['\texpected duration: {}'.format(self.expected_duration)]
|
||||
messages += ['\tstall wait time: {}'.format(self.stall_wait_time)]
|
||||
messages += ['\ttimeout: {}'.format(self.timeout)]
|
||||
# Print each field on its own line
|
||||
for field in self._last_seen_video_state._fields:
|
||||
# For compatibility with different test environments we force ascii
|
||||
field_ascii = (
|
||||
unicode(getattr(self._last_seen_video_state, field))
|
||||
.encode('ascii','replace'))
|
||||
messages += [('\t{}: {}'.format(field, field_ascii))]
|
||||
messages += '}'
|
||||
return '\n'.join(messages)
|
||||
|
||||
|
||||
class VideoException(Exception):
|
||||
"""
|
||||
Exception class to use for video-specific error processing.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TimeRanges:
|
||||
"""
|
||||
Class to represent the TimeRanges data returned by played(). Exposes a
|
||||
similar interface to the JavaScript TimeRanges object.
|
||||
"""
|
||||
def __init__(self, length, ranges):
|
||||
# These should be the same,. Theoretically we don't need the length,
|
||||
# but since this should be used to consume data coming back from
|
||||
# JS exec, this is a valid sanity check.
|
||||
assert length == len(ranges)
|
||||
self.length = length
|
||||
self.ranges = [(pair[0], pair[1]) for pair in ranges]
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
'TimeRanges: length: {}, ranges: {}'
|
||||
.format(self.length, self.ranges)
|
||||
)
|
||||
|
||||
def start(self, index):
|
||||
return self.ranges[index][0]
|
||||
|
||||
def end(self, index):
|
||||
return self.ranges[index][1]
|
|
@ -1,490 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import re
|
||||
|
||||
from collections import namedtuple
|
||||
from json import loads
|
||||
from time import sleep
|
||||
|
||||
from marionette_driver import By, expected, Wait
|
||||
from marionette_driver.errors import TimeoutException, NoSuchElementException
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from video_puppeteer import VideoPuppeteer, VideoException
|
||||
from external_media_tests.utils import verbose_until
|
||||
|
||||
|
||||
class YouTubePuppeteer(VideoPuppeteer):
|
||||
"""
|
||||
Wrapper around a YouTube .html5-video-player element.
|
||||
|
||||
Can be used with youtube videos or youtube videos at embedded URLS. E.g.
|
||||
both https://www.youtube.com/watch?v=AbAACm1IQE0 and
|
||||
https://www.youtube.com/embed/AbAACm1IQE0 should work.
|
||||
|
||||
Using an embedded video has the advantage of not auto-playing more videos
|
||||
while a test is running.
|
||||
|
||||
Compared to video puppeteer, this class has the benefit of accessing the
|
||||
youtube player object as well as the video element. The YT player will
|
||||
report information for the underlying video even if an add is playing (as
|
||||
opposed to the video element behaviour, which will report on whatever
|
||||
is play at the time of query), and can also report if an ad is playing.
|
||||
|
||||
Partial reference: https://developers.google.com/youtube/iframe_api_reference.
|
||||
This reference is useful for site-specific features such as interacting
|
||||
with ads, or accessing YouTube's debug data.
|
||||
"""
|
||||
|
||||
_player_var_script = (
|
||||
'var player_duration = arguments[1].wrappedJSObject.getDuration();'
|
||||
'var player_current_time = '
|
||||
'arguments[1].wrappedJSObject.getCurrentTime();'
|
||||
'var player_playback_quality = '
|
||||
'arguments[1].wrappedJSObject.getPlaybackQuality();'
|
||||
'var player_movie_id = '
|
||||
'arguments[1].wrappedJSObject.getVideoData()["video_id"];'
|
||||
'var player_movie_title = '
|
||||
'arguments[1].wrappedJSObject.getVideoData()["title"];'
|
||||
'var player_url = '
|
||||
'arguments[1].wrappedJSObject.getVideoUrl();'
|
||||
'var player_state = '
|
||||
'arguments[1].wrappedJSObject.getPlayerState();'
|
||||
'var player_ad_state = arguments[1].wrappedJSObject.getAdState();'
|
||||
'var player_breaks_count = '
|
||||
'arguments[1].wrappedJSObject.getOption("ad", "breakscount");'
|
||||
)
|
||||
"""
|
||||
A string containing JS that will assign player state to
|
||||
variables. This is similar to `_video_var_script` from
|
||||
`VideoPuppeteer`. See `_video_var_script` for more information on the
|
||||
motivation for this method.
|
||||
|
||||
This script assigns a subset of the vars used later by the
|
||||
`_yt_state_named_tuple` function. Please see that functions
|
||||
documentation for further information on these variables.
|
||||
"""
|
||||
|
||||
_yt_player_state = {
|
||||
'UNSTARTED': -1,
|
||||
'ENDED': 0,
|
||||
'PLAYING': 1,
|
||||
'PAUSED': 2,
|
||||
'BUFFERING': 3,
|
||||
'CUED': 5
|
||||
}
|
||||
_yt_player_state_name = {v: k for k, v in _yt_player_state.items()}
|
||||
_time_pattern = re.compile('(?P<minute>\d+):(?P<second>\d+)')
|
||||
|
||||
def __init__(self, marionette, url, autostart=True, **kwargs):
|
||||
self.player = None
|
||||
self._last_seen_player_state = None
|
||||
super(YouTubePuppeteer,
|
||||
self).__init__(marionette, url,
|
||||
video_selector='.html5-video-player video',
|
||||
autostart=False,
|
||||
**kwargs)
|
||||
wait = Wait(self.marionette, timeout=30)
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
verbose_until(wait, self,
|
||||
expected.element_present(By.CLASS_NAME,
|
||||
'html5-video-player'))
|
||||
self.player = self.marionette.find_element(By.CLASS_NAME,
|
||||
'html5-video-player')
|
||||
print '.html5-video-player element obtained'
|
||||
# When an ad is playing, self.player_duration indicates the duration
|
||||
# of the spliced-in ad stream, not the duration of the main video, so
|
||||
# we attempt to skip the ad first.
|
||||
for attempt in range(5):
|
||||
sleep(1)
|
||||
self.process_ad()
|
||||
if (self._last_seen_player_state.player_ad_inactive and
|
||||
self._last_seen_video_state.duration and not
|
||||
self._last_seen_player_state.player_buffering):
|
||||
break
|
||||
self._update_expected_duration()
|
||||
if autostart:
|
||||
self.start()
|
||||
|
||||
def player_play(self):
|
||||
"""
|
||||
Play via YouTube API.
|
||||
"""
|
||||
self._execute_yt_script('arguments[1].wrappedJSObject.playVideo();')
|
||||
|
||||
def player_pause(self):
|
||||
"""
|
||||
Pause via YouTube API.
|
||||
"""
|
||||
self._execute_yt_script('arguments[1].wrappedJSObject.pauseVideo();')
|
||||
|
||||
def _player_measure_progress(self):
|
||||
"""
|
||||
Determine player progress. Refreshes state.
|
||||
|
||||
:return: Playback progress in seconds via YouTube API with snapshots.
|
||||
"""
|
||||
self._refresh_state()
|
||||
initial = self._last_seen_player_state.player_current_time
|
||||
sleep(1)
|
||||
self._refresh_state()
|
||||
return self._last_seen_player_state.player_current_time - initial
|
||||
|
||||
def _get_player_debug_dict(self):
|
||||
text = self._execute_yt_script('return arguments[1].'
|
||||
'wrappedJSObject.getDebugText();')
|
||||
if text:
|
||||
try:
|
||||
return loads(text)
|
||||
except ValueError:
|
||||
print 'Error loading JSON: DebugText'
|
||||
|
||||
def _execute_yt_script(self, script):
|
||||
"""
|
||||
Execute JS script in content context with access to video element and
|
||||
YouTube .html5-video-player element.
|
||||
|
||||
:param script: script to be executed.
|
||||
|
||||
:return: value returned by script
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
return self.marionette.execute_script(script,
|
||||
script_args=[self.video,
|
||||
self.player])
|
||||
|
||||
def _check_if_ad_ended(self):
|
||||
self._refresh_state()
|
||||
return self._last_seen_player_state.player_ad_ended
|
||||
|
||||
def process_ad(self):
|
||||
"""
|
||||
Wait for this ad to finish. Refreshes state.
|
||||
"""
|
||||
self._refresh_state()
|
||||
if self._last_seen_player_state.player_ad_inactive:
|
||||
return
|
||||
ad_timeout = (self._search_ad_duration() or 30) + 5
|
||||
wait = Wait(self, timeout=ad_timeout, interval=1)
|
||||
try:
|
||||
print('process_ad: waiting {}s for ad'.format(ad_timeout))
|
||||
verbose_until(wait,
|
||||
self,
|
||||
YouTubePuppeteer._check_if_ad_ended,
|
||||
"Check if ad ended")
|
||||
except TimeoutException:
|
||||
print('Waiting for ad to end timed out')
|
||||
|
||||
def _search_ad_duration(self):
|
||||
"""
|
||||
Try and determine ad duration. Refreshes state.
|
||||
|
||||
:return: ad duration in seconds, if currently displayed in player
|
||||
"""
|
||||
self._refresh_state()
|
||||
if not (self._last_seen_player_state.player_ad_playing or
|
||||
self._player_measure_progress() == 0):
|
||||
return None
|
||||
if (self._last_seen_player_state.player_ad_playing and
|
||||
self._last_seen_video_state.duration):
|
||||
return self._last_seen_video_state.duration
|
||||
selector = '.html5-video-player .videoAdUiAttribution'
|
||||
wait = Wait(self.marionette, timeout=5)
|
||||
try:
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
wait.until(expected.element_present(By.CSS_SELECTOR,
|
||||
selector))
|
||||
countdown = self.marionette.find_element(By.CSS_SELECTOR,
|
||||
selector)
|
||||
ad_time = self._time_pattern.search(countdown.text)
|
||||
if ad_time:
|
||||
ad_minutes = int(ad_time.group('minute'))
|
||||
ad_seconds = int(ad_time.group('second'))
|
||||
return 60 * ad_minutes + ad_seconds
|
||||
except (TimeoutException, NoSuchElementException):
|
||||
print('Could not obtain element {}'.format(selector))
|
||||
return None
|
||||
|
||||
def _player_stalled(self):
|
||||
"""
|
||||
Checks if the player has stalled. Refreshes state.
|
||||
|
||||
:return: True if playback is not making progress for 4-9 seconds. This
|
||||
excludes ad breaks. Note that the player might just be busy with
|
||||
buffering due to a slow network.
|
||||
"""
|
||||
|
||||
# `current_time` stands still while ad is playing
|
||||
def condition():
|
||||
# no ad is playing and current_time stands still
|
||||
return (not self._last_seen_player_state.player_ad_playing and
|
||||
self._measure_progress() < 0.1 and
|
||||
self._player_measure_progress() < 0.1 and
|
||||
(self._last_seen_player_state.player_playing or
|
||||
self._last_seen_player_state.player_buffering))
|
||||
|
||||
if condition():
|
||||
sleep(2)
|
||||
self._refresh_state()
|
||||
if self._last_seen_player_state.player_buffering:
|
||||
sleep(5)
|
||||
self._refresh_state()
|
||||
return condition()
|
||||
else:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _yt_state_named_tuple():
|
||||
"""
|
||||
Create a named tuple class that can be used to store state snapshots
|
||||
of the wrapped youtube player. The fields in the tuple should be used
|
||||
as follows:
|
||||
|
||||
player_duration: the duration as fetched from the wrapped player.
|
||||
player_current_time: the current playback time as fetched from the
|
||||
wrapped player.
|
||||
player_remaining_time: the remaining time as calculated based on the
|
||||
puppeteers expected time and the players current time.
|
||||
player_playback_quality: the playback quality as fetched from the
|
||||
wrapped player. See:
|
||||
https://developers.google.com/youtube/js_api_reference#Playback_quality
|
||||
player_movie_id: the movie id fetched from the wrapped player.
|
||||
player_movie_title: the title fetched from the wrapped player.
|
||||
player_url: the self reported url fetched from the wrapped player.
|
||||
player_state: the current state of playback as fetch from the wrapped
|
||||
player. See:
|
||||
https://developers.google.com/youtube/js_api_reference#Playback_status
|
||||
player_unstarted, player_ended, player_playing, player_paused,
|
||||
player_buffering, and player_cued: these are all shortcuts to the
|
||||
player state, only one should be true at any time.
|
||||
player_ad_state: as player_state, but reports for the current ad.
|
||||
player_ad_state, player_ad_inactive, player_ad_playing, and
|
||||
player_ad_ended: these are all shortcuts to the ad state, only one
|
||||
should be true at any time.
|
||||
player_breaks_count: the number of ads as fetched from the wrapped
|
||||
player. This includes both played and unplayed ads, and includes
|
||||
streaming ads as well as pop up ads.
|
||||
|
||||
:return: A 'player_state_info' named tuple class.
|
||||
"""
|
||||
return namedtuple('player_state_info',
|
||||
['player_duration',
|
||||
'player_current_time',
|
||||
'player_remaining_time',
|
||||
'player_playback_quality',
|
||||
'player_movie_id',
|
||||
'player_movie_title',
|
||||
'player_url',
|
||||
'player_state',
|
||||
'player_unstarted',
|
||||
'player_ended',
|
||||
'player_playing',
|
||||
'player_paused',
|
||||
'player_buffering',
|
||||
'player_cued',
|
||||
'player_ad_state',
|
||||
'player_ad_inactive',
|
||||
'player_ad_playing',
|
||||
'player_ad_ended',
|
||||
'player_breaks_count'
|
||||
])
|
||||
|
||||
def _create_player_state_info(self, **player_state_info_kwargs):
|
||||
"""
|
||||
Create an instance of the state info named tuple. This function
|
||||
expects a dictionary containing the following keys:
|
||||
player_duration, player_current_time, player_playback_quality,
|
||||
player_movie_id, player_movie_title, player_url, player_state,
|
||||
player_ad_state, and player_breaks_count.
|
||||
|
||||
For more information on the above keys and their values see
|
||||
`_yt_state_named_tuple`.
|
||||
|
||||
:return: A named tuple 'player_state_info', derived from arguments and
|
||||
state information from the puppeteer.
|
||||
"""
|
||||
player_state_info_kwargs['player_remaining_time'] = (
|
||||
self.expected_duration -
|
||||
player_state_info_kwargs['player_current_time'])
|
||||
# Calculate player state convenience info
|
||||
player_state = player_state_info_kwargs['player_state']
|
||||
player_state_info_kwargs['player_unstarted'] = (
|
||||
player_state == self._yt_player_state['UNSTARTED'])
|
||||
player_state_info_kwargs['player_ended'] = (
|
||||
player_state == self._yt_player_state['ENDED'])
|
||||
player_state_info_kwargs['player_playing'] = (
|
||||
player_state == self._yt_player_state['PLAYING'])
|
||||
player_state_info_kwargs['player_paused'] = (
|
||||
player_state == self._yt_player_state['PAUSED'])
|
||||
player_state_info_kwargs['player_buffering'] = (
|
||||
player_state == self._yt_player_state['BUFFERING'])
|
||||
player_state_info_kwargs['player_cued'] = (
|
||||
player_state == self._yt_player_state['CUED'])
|
||||
# Calculate ad state convenience info
|
||||
player_ad_state = player_state_info_kwargs['player_ad_state']
|
||||
player_state_info_kwargs['player_ad_inactive'] = (
|
||||
player_ad_state == self._yt_player_state['UNSTARTED'])
|
||||
player_state_info_kwargs['player_ad_playing'] = (
|
||||
player_ad_state == self._yt_player_state['PLAYING'])
|
||||
player_state_info_kwargs['player_ad_ended'] = (
|
||||
player_ad_state == self._yt_player_state['ENDED'])
|
||||
# Create player snapshot
|
||||
state_info = self._yt_state_named_tuple()
|
||||
return state_info(**player_state_info_kwargs)
|
||||
|
||||
@property
|
||||
def _fetch_state_script(self):
|
||||
if not self._fetch_state_script_string:
|
||||
self._fetch_state_script_string = (
|
||||
self._video_var_script +
|
||||
self._player_var_script +
|
||||
'return ['
|
||||
'baseURI,'
|
||||
'currentTime,'
|
||||
'duration,'
|
||||
'[buffered.length, bufferedRanges],'
|
||||
'[played.length, playedRanges],'
|
||||
'totalFrames,'
|
||||
'droppedFrames,'
|
||||
'corruptedFrames,'
|
||||
'player_duration,'
|
||||
'player_current_time,'
|
||||
'player_playback_quality,'
|
||||
'player_movie_id,'
|
||||
'player_movie_title,'
|
||||
'player_url,'
|
||||
'player_state,'
|
||||
'player_ad_state,'
|
||||
'player_breaks_count];')
|
||||
return self._fetch_state_script_string
|
||||
|
||||
def _refresh_state(self):
|
||||
"""
|
||||
Refresh the snapshot of the underlying video and player state. We do
|
||||
this all in one so that the state doesn't change in between queries.
|
||||
|
||||
We also store information that can be derived from the snapshotted
|
||||
information, such as lag. This is stored in the last seen state to
|
||||
stress that it's based on the snapshot.
|
||||
"""
|
||||
values = self._execute_yt_script(self._fetch_state_script)
|
||||
video_keys = ['base_uri', 'current_time', 'duration',
|
||||
'raw_buffered_ranges', 'raw_played_ranges',
|
||||
'total_frames', 'dropped_frames', 'corrupted_frames']
|
||||
player_keys = ['player_duration', 'player_current_time',
|
||||
'player_playback_quality', 'player_movie_id',
|
||||
'player_movie_title', 'player_url', 'player_state',
|
||||
'player_ad_state', 'player_breaks_count']
|
||||
# Get video state
|
||||
self._last_seen_video_state = (
|
||||
self._create_video_state_info(**dict(
|
||||
zip(video_keys, values[:len(video_keys)]))))
|
||||
# Get player state
|
||||
self._last_seen_player_state = (
|
||||
self._create_player_state_info(**dict(
|
||||
zip(player_keys, values[-len(player_keys):]))))
|
||||
|
||||
def mse_enabled(self):
|
||||
"""
|
||||
Check if the video source indicates mse usage for current video.
|
||||
Refreshes state.
|
||||
|
||||
:return: True if MSE is being used, False if not.
|
||||
"""
|
||||
self._refresh_state()
|
||||
return self._last_seen_video_state.video_src.startswith('blob')
|
||||
|
||||
def playback_started(self):
|
||||
"""
|
||||
Check whether playback has started. Refreshes state.
|
||||
|
||||
:return: True if play back has started, False if not.
|
||||
"""
|
||||
self._refresh_state()
|
||||
# usually, ad is playing during initial buffering
|
||||
if (self._last_seen_player_state.player_playing or
|
||||
self._last_seen_player_state.player_buffering):
|
||||
return True
|
||||
if (self._last_seen_video_state.current_time > 0 or
|
||||
self._last_seen_player_state.player_current_time > 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
def playback_done(self):
|
||||
"""
|
||||
Check whether playback is done. Refreshes state.
|
||||
|
||||
:return: True if play back has ended, False if not.
|
||||
"""
|
||||
# in case ad plays at end of video
|
||||
self._refresh_state()
|
||||
if self._last_seen_player_state.player_ad_playing:
|
||||
return False
|
||||
return (self._last_seen_player_state.player_ended or
|
||||
self._last_seen_player_state.player_remaining_time < 1)
|
||||
|
||||
def wait_for_almost_done(self, final_piece=120):
|
||||
"""
|
||||
Allow the given video to play until only `final_piece` seconds remain,
|
||||
skipping ads mid-way as much as possible.
|
||||
`final_piece` should be short enough to not be interrupted by an ad.
|
||||
|
||||
Depending on the length of the video, check the ad status every 10-30
|
||||
seconds, skip an active ad if possible.
|
||||
|
||||
This call refreshes state.
|
||||
|
||||
:param final_piece: The length in seconds of the desired remaining time
|
||||
to wait until.
|
||||
"""
|
||||
self._refresh_state()
|
||||
rest = 10
|
||||
duration = remaining_time = self.expected_duration
|
||||
if duration < final_piece:
|
||||
# video is short so don't attempt to skip more ads
|
||||
return duration
|
||||
elif duration > 600:
|
||||
# for videos that are longer than 10 minutes
|
||||
# wait longer between checks
|
||||
rest = duration / 50
|
||||
|
||||
while remaining_time > final_piece:
|
||||
if self._player_stalled():
|
||||
if self._last_seen_player_state.player_buffering:
|
||||
# fall back on timeout in 'wait' call that comes after this
|
||||
# in test function
|
||||
print('Buffering and no playback progress')
|
||||
break
|
||||
else:
|
||||
message = '\n'.join(['Playback stalled', str(self)])
|
||||
raise VideoException(message)
|
||||
if self._last_seen_player_state.player_breaks_count > 0:
|
||||
self.process_ad()
|
||||
if remaining_time > 1.5 * rest:
|
||||
sleep(rest)
|
||||
else:
|
||||
sleep(rest / 2)
|
||||
# TODO during an ad, remaining_time will be based on ad's
|
||||
# current_time rather than current_time of target video
|
||||
remaining_time = self._last_seen_player_state.player_remaining_time
|
||||
return remaining_time
|
||||
|
||||
def __str__(self):
|
||||
messages = [super(YouTubePuppeteer, self).__str__()]
|
||||
if not self.player:
|
||||
messages += ['\t.html5-media-player: None']
|
||||
return '\n'.join(messages)
|
||||
if not self._last_seen_player_state:
|
||||
messages += ['\t.html5-media-player: No last seen state']
|
||||
return '\n'.join(messages)
|
||||
messages += ['.html5-media-player: {']
|
||||
for field in self._last_seen_player_state._fields:
|
||||
# For compatibility with different test environments we force ascii
|
||||
field_ascii = (
|
||||
unicode(getattr(self._last_seen_player_state, field)).encode(
|
||||
'ascii', 'replace'))
|
||||
messages += [('\t{}: {}'.format(field, field_ascii))]
|
||||
messages += '}'
|
||||
return '\n'.join(messages)
|
|
@ -1 +0,0 @@
|
|||
[test_eme_playback.py]
|
|
@ -1,2 +0,0 @@
|
|||
[test_playback_limiting_bandwidth.py]
|
||||
[test_ultra_low_bandwidth.py]
|
|
@ -1 +0,0 @@
|
|||
[test_video_playback.py]
|
|
@ -1 +0,0 @@
|
|||
[test_eme_playback_limiting_bandwidth.py]
|
|
@ -1,18 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from external_media_harness.testcase import (
|
||||
MediaTestCase,
|
||||
VideoPlaybackTestsMixin,
|
||||
EMESetupMixin
|
||||
)
|
||||
|
||||
|
||||
class TestEMEPlayback(MediaTestCase, VideoPlaybackTestsMixin, EMESetupMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEMEPlayback, self).setUp()
|
||||
self.check_eme_system()
|
||||
|
||||
# Tests are implemented in VideoPlaybackTestsMixin
|
|
@ -1,24 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_harness import BrowserMobProxyTestCaseMixin
|
||||
|
||||
from external_media_harness.testcase import (
|
||||
EMESetupMixin,
|
||||
NetworkBandwidthTestCase,
|
||||
NetworkBandwidthTestsMixin,
|
||||
)
|
||||
|
||||
|
||||
class TestEMEPlaybackLimitingBandwidth(NetworkBandwidthTestCase,
|
||||
BrowserMobProxyTestCaseMixin,
|
||||
NetworkBandwidthTestsMixin,
|
||||
EMESetupMixin):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super(TestEMEPlaybackLimitingBandwidth, self).setUp()
|
||||
self.check_eme_system()
|
||||
|
||||
# Tests in NetworkBandwidthTestsMixin
|
|
@ -1,25 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from external_media_harness.testcase import MediaTestCase
|
||||
from external_media_tests.media_utils.video_puppeteer import VideoPuppeteer
|
||||
|
||||
|
||||
class TestFullPlayback(MediaTestCase):
|
||||
""" Test MSE playback in HTML5 video element.
|
||||
|
||||
These tests should pass on any site where a single video element plays
|
||||
upon loading and is uninterrupted (by ads, for example). This will play
|
||||
the full videos, so it could take a while depending on the videos playing.
|
||||
It should be run much less frequently in automated systems.
|
||||
"""
|
||||
|
||||
def test_video_playback_full(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
video = VideoPuppeteer(self.marionette, url,
|
||||
stall_wait_time=10)
|
||||
self.run_playback(video)
|
|
@ -1,17 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_harness import BrowserMobProxyTestCaseMixin
|
||||
|
||||
from external_media_harness.testcase import (
|
||||
NetworkBandwidthTestCase, NetworkBandwidthTestsMixin
|
||||
)
|
||||
|
||||
|
||||
class TestPlaybackLimitingBandwidth(NetworkBandwidthTestCase,
|
||||
NetworkBandwidthTestsMixin,
|
||||
BrowserMobProxyTestCaseMixin):
|
||||
|
||||
# Tests are in NetworkBandwidthTestsMixin
|
||||
pass
|
|
@ -1,42 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from external_media_harness.testcase import MediaTestCase
|
||||
from external_media_tests.media_utils.video_puppeteer import VideoPuppeteer
|
||||
|
||||
|
||||
class TestShakaPlayback(MediaTestCase):
|
||||
""" Test Widevine playback in shaka-player
|
||||
|
||||
This test takes manifest URLs rather than URLs for pages with videos. These
|
||||
manifests are loaded with shaka-player
|
||||
"""
|
||||
|
||||
def test_video_playback_partial(self):
|
||||
""" Plays 60 seconds of the video from the manifest URLs given
|
||||
"""
|
||||
shakaUrl = "http://shaka-player-demo.appspot.com"
|
||||
self.marionette.set_pref('media.mediasource.webm.enabled', True)
|
||||
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for manifestUrl in self.video_urls:
|
||||
vp = VideoPuppeteer(self.marionette,
|
||||
shakaUrl,
|
||||
stall_wait_time=10,
|
||||
set_duration=60,
|
||||
video_selector="video#video",
|
||||
autostart=False)
|
||||
|
||||
|
||||
manifestInput = self.marionette.find_element("id",
|
||||
"manifestUrlInput")
|
||||
manifestInput.clear()
|
||||
manifestInput.send_keys(manifestUrl)
|
||||
loadButton = self.marionette.find_element("id", "loadButton")
|
||||
loadButton.click()
|
||||
|
||||
vp.start()
|
||||
self.run_playback(vp)
|
|
@ -1,15 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_harness import BrowserMobProxyTestCaseMixin
|
||||
|
||||
from external_media_harness.testcase import NetworkBandwidthTestCase
|
||||
|
||||
|
||||
class TestUltraLowBandwidth(NetworkBandwidthTestCase,
|
||||
BrowserMobProxyTestCaseMixin):
|
||||
|
||||
def test_playback_limiting_bandwidth_160(self):
|
||||
self.proxy.limits({'downstream_kbps': 160})
|
||||
self.run_videos(timeout=120)
|
|
@ -1,15 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from external_media_harness.testcase import (
|
||||
MediaTestCase,
|
||||
VideoPlaybackTestsMixin
|
||||
)
|
||||
|
||||
|
||||
class TestVideoPlayback(MediaTestCase, VideoPlaybackTestsMixin):
|
||||
|
||||
# Tests are actually implemented in VideoPlaybackTestsMixin.
|
||||
|
||||
pass
|
|
@ -1 +0,0 @@
|
|||
[test_basic_stream_playback.py]
|
|
@ -1,30 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_driver.errors import TimeoutException
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from external_media_harness.testcase import MediaTestCase
|
||||
from external_media_tests.media_utils.twitch_puppeteer import TwitchPuppeteer
|
||||
|
||||
|
||||
class TestBasicStreamPlayback(MediaTestCase):
|
||||
def test_video_playback_partial(self):
|
||||
"""
|
||||
Test to make sure that playback of 60 seconds works for each video.
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
stream = TwitchPuppeteer(self.marionette, url,
|
||||
stall_wait_time=10,
|
||||
set_duration=60)
|
||||
self.run_playback(stream)
|
||||
|
||||
def test_playback_starts(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
try:
|
||||
TwitchPuppeteer(self.marionette, url, timeout=60)
|
||||
except TimeoutException as e:
|
||||
raise self.failureException(e)
|
|
@ -1 +0,0 @@
|
|||
[test_basic_playback.py ]
|
|
@ -1,72 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from marionette_driver import Wait
|
||||
from marionette_driver.errors import TimeoutException
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from external_media_tests.utils import verbose_until
|
||||
from external_media_harness.testcase import MediaTestCase
|
||||
from external_media_tests.media_utils.video_puppeteer import VideoException
|
||||
from external_media_tests.media_utils.youtube_puppeteer import YouTubePuppeteer
|
||||
|
||||
|
||||
class TestBasicYouTubePlayback(MediaTestCase):
|
||||
def test_mse_is_enabled_by_default(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
youtube = YouTubePuppeteer(self.marionette, self.video_urls[0],
|
||||
timeout=60)
|
||||
wait = Wait(youtube,
|
||||
timeout=min(300, youtube.expected_duration * 1.3),
|
||||
interval=1)
|
||||
try:
|
||||
verbose_until(wait, youtube,
|
||||
YouTubePuppeteer.mse_enabled,
|
||||
"Failed to find 'blob' in video src url.")
|
||||
except TimeoutException as e:
|
||||
raise self.failureException(e)
|
||||
|
||||
def test_video_playing_in_one_tab(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
self.logger.info(url)
|
||||
youtube = YouTubePuppeteer(self.marionette, url)
|
||||
self.logger.info('Expected duration: {}'
|
||||
.format(youtube.expected_duration))
|
||||
|
||||
final_piece = 60
|
||||
try:
|
||||
time_left = youtube.wait_for_almost_done(
|
||||
final_piece=final_piece)
|
||||
except VideoException as e:
|
||||
raise self.failureException(e)
|
||||
duration = abs(youtube.expected_duration) + 1
|
||||
if duration > 1:
|
||||
self.logger.info('Almost done: {} - {} seconds left.'
|
||||
.format(url, time_left))
|
||||
if time_left > final_piece:
|
||||
self.logger.warn('time_left greater than '
|
||||
'final_piece - {}'
|
||||
.format(time_left))
|
||||
self.save_screenshot()
|
||||
else:
|
||||
self.logger.warn('Duration close to 0 - {}'
|
||||
.format(youtube))
|
||||
self.save_screenshot()
|
||||
try:
|
||||
verbose_until(Wait(youtube,
|
||||
timeout=max(100, time_left) * 1.3,
|
||||
interval=1),
|
||||
youtube,
|
||||
YouTubePuppeteer.playback_done)
|
||||
except TimeoutException as e:
|
||||
raise self.failureException(e)
|
||||
|
||||
def test_playback_starts(self):
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
for url in self.video_urls:
|
||||
try:
|
||||
YouTubePuppeteer(self.marionette, url, timeout=60)
|
||||
except TimeoutException as e:
|
||||
raise self.failureException(e)
|
|
@ -1,46 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from external_media_harness.testcase import MediaTestCase
|
||||
from marionette_driver import Wait
|
||||
|
||||
from external_media_tests.utils import verbose_until
|
||||
from external_media_tests.media_utils.youtube_puppeteer import YouTubePuppeteer
|
||||
|
||||
|
||||
class TestMediaSourcePrefs(MediaTestCase):
|
||||
def setUp(self):
|
||||
MediaTestCase.setUp(self)
|
||||
self.test_urls = self.video_urls[:2]
|
||||
self.max_timeout = 60
|
||||
|
||||
def tearDown(self):
|
||||
MediaTestCase.tearDown(self)
|
||||
|
||||
def test_mse_prefs(self):
|
||||
""" mediasource should only be used if MSE prefs are enabled."""
|
||||
self.set_mse_enabled_prefs(False)
|
||||
self.check_mse_src(False, self.test_urls[0])
|
||||
|
||||
self.set_mse_enabled_prefs(True)
|
||||
self.check_mse_src(True, self.test_urls[0])
|
||||
|
||||
def set_mse_enabled_prefs(self, value):
|
||||
with self.marionette.using_context('chrome'):
|
||||
self.marionette.set_pref('media.mediasource.enabled', value)
|
||||
self.marionette.set_pref('media.mediasource.mp4.enabled', value)
|
||||
self.marionette.set_pref('media.mediasource.webm.enabled', value)
|
||||
|
||||
def check_mse_src(self, mse_expected, url):
|
||||
with self.marionette.using_context('content'):
|
||||
youtube = YouTubePuppeteer(self.marionette, url)
|
||||
wait = Wait(youtube,
|
||||
timeout=min(self.max_timeout,
|
||||
youtube.expected_duration * 1.3),
|
||||
interval=1)
|
||||
|
||||
def cond(y):
|
||||
return y.mse_enabled == mse_expected
|
||||
|
||||
verbose_until(wait, youtube, cond)
|
|
@ -1,45 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<title>Mozilla</title>
|
||||
<link rel="shortcut icon" type="image/ico" href="../images/mozilla_favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a href="mozilla.html">
|
||||
<img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
|
||||
</a>
|
||||
|
||||
<a href="#community">RARARARARARA</a> |
|
||||
<a href="#project">Project</a> |
|
||||
<a href="#organization">Organization</a>
|
||||
|
||||
<div id="content">
|
||||
<h1 id="page-title">
|
||||
<strong>RARARARARARA</strong> that the internet should be public,
|
||||
open and accessible.
|
||||
</h1>
|
||||
|
||||
<h2><a name="community">RARARARARARA</a></h2>
|
||||
<p id="community">
|
||||
We're a global community of thousands who believe in the power
|
||||
of technology to enrich people's lives.
|
||||
<a href="mozilla_community.html">More</a>
|
||||
</p>
|
||||
|
||||
<h2><a name="project">Project</a></h2>
|
||||
<p id="project">
|
||||
We're an open source project whose code is used for some of the
|
||||
Internet's most innovative applications.
|
||||
<a href="mozilla_projects.html">More</a>
|
||||
</p>
|
||||
|
||||
<h2><a name="organization">Organization</a></h2>
|
||||
<p id="organization">
|
||||
We're a public benefit organization dedicated to making the
|
||||
Internet better for everyone.
|
||||
<a href="mozilla_mission.html">More</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче