483 lines
15 KiB
JavaScript
483 lines
15 KiB
JavaScript
/**
|
|
* This function would create a new foreround tab and load the url for it. In
|
|
* addition, instead of returning a tab element, we return a tab wrapper that
|
|
* helps us to automatically detect if the media controller of that tab
|
|
* dispatches the first (activated) and the last event (deactivated) correctly.
|
|
* @ param url
|
|
* the page url which tab would load
|
|
* @ param input window (optional)
|
|
* if it exists, the tab would be created from the input window. If not,
|
|
* then the tab would be created in current window.
|
|
* @ param needCheck (optional)
|
|
* it decides if we would perform the check for the first and last event
|
|
* on the media controller. It's default true.
|
|
*/
|
|
async function createLoadedTabWrapper(
|
|
url,
|
|
{ inputWindow = window, needCheck = true } = {}
|
|
) {
|
|
class tabWrapper {
|
|
constructor(tab, needCheck) {
|
|
this._tab = tab;
|
|
this._controller = tab.linkedBrowser.browsingContext.mediaController;
|
|
this._firstEvent = "";
|
|
this._lastEvent = "";
|
|
this._events = [
|
|
"activated",
|
|
"deactivated",
|
|
"metadatachange",
|
|
"playbackstatechange",
|
|
"positionstatechange",
|
|
"supportedkeyschange",
|
|
];
|
|
this._needCheck = needCheck;
|
|
if (this._needCheck) {
|
|
this._registerAllEvents();
|
|
}
|
|
}
|
|
_registerAllEvents() {
|
|
for (let event of this._events) {
|
|
this._controller.addEventListener(event, this._handleEvent.bind(this));
|
|
}
|
|
}
|
|
_unregisterAllEvents() {
|
|
for (let event of this._events) {
|
|
this._controller.removeEventListener(
|
|
event,
|
|
this._handleEvent.bind(this)
|
|
);
|
|
}
|
|
}
|
|
_handleEvent(event) {
|
|
info(`handle event=${event.type}`);
|
|
if (this._firstEvent === "") {
|
|
this._firstEvent = event.type;
|
|
}
|
|
this._lastEvent = event.type;
|
|
}
|
|
get linkedBrowser() {
|
|
return this._tab.linkedBrowser;
|
|
}
|
|
get controller() {
|
|
return this._controller;
|
|
}
|
|
get tabElement() {
|
|
return this._tab;
|
|
}
|
|
async close() {
|
|
info(`wait until finishing close tab wrapper`);
|
|
const deactivationPromise = this._controller.isActive
|
|
? new Promise(r => (this._controller.ondeactivated = r))
|
|
: Promise.resolve();
|
|
BrowserTestUtils.removeTab(this._tab);
|
|
await deactivationPromise;
|
|
if (this._needCheck) {
|
|
is(this._firstEvent, "activated", "First event should be 'activated'");
|
|
is(
|
|
this._lastEvent,
|
|
"deactivated",
|
|
"Last event should be 'deactivated'"
|
|
);
|
|
this._unregisterAllEvents();
|
|
}
|
|
}
|
|
}
|
|
const browser = inputWindow ? inputWindow.gBrowser : window.gBrowser;
|
|
let tab = await BrowserTestUtils.openNewForegroundTab(browser, url);
|
|
return new tabWrapper(tab, needCheck);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when generated media control keys has
|
|
* triggered the main media controller's corresponding method and changes its
|
|
* playback state.
|
|
*
|
|
* @param {string} event
|
|
* The event name of the media control key
|
|
* @return {Promise}
|
|
* Resolve when the main controller receives the media control key event
|
|
* and change its playback state.
|
|
*/
|
|
function generateMediaControlKeyEvent(event) {
|
|
const playbackStateChanged = waitUntilDisplayedPlaybackChanged();
|
|
MediaControlService.generateMediaControlKey(event);
|
|
return playbackStateChanged;
|
|
}
|
|
|
|
/**
|
|
* Play the specific media and wait until it plays successfully and the main
|
|
* controller has been updated.
|
|
*
|
|
* @param {tab} tab
|
|
* The tab that contains the media which we would play
|
|
* @param {string} elementId
|
|
* The element Id of the media which we would play
|
|
* @return {Promise}
|
|
* Resolve when the media has been starting playing and the main
|
|
* controller has been updated.
|
|
*/
|
|
async function playMedia(tab, elementId) {
|
|
const playbackStatePromise = waitUntilDisplayedPlaybackChanged();
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [elementId], async Id => {
|
|
const video = content.document.getElementById(Id);
|
|
if (!video) {
|
|
ok(false, `can't get the media element!`);
|
|
}
|
|
ok(
|
|
await video.play().then(
|
|
_ => true,
|
|
_ => false
|
|
),
|
|
"video started playing"
|
|
);
|
|
});
|
|
return playbackStatePromise;
|
|
}
|
|
|
|
/**
|
|
* Pause the specific media and wait until it pauses successfully and the main
|
|
* controller has been updated.
|
|
*
|
|
* @param {tab} tab
|
|
* The tab that contains the media which we would pause
|
|
* @param {string} elementId
|
|
* The element Id of the media which we would pause
|
|
* @return {Promise}
|
|
* Resolve when the media has been paused and the main controller has
|
|
* been updated.
|
|
*/
|
|
function pauseMedia(tab, elementId) {
|
|
const pausePromise = SpecialPowers.spawn(
|
|
tab.linkedBrowser,
|
|
[elementId],
|
|
Id => {
|
|
const video = content.document.getElementById(Id);
|
|
if (!video) {
|
|
ok(false, `can't get the media element!`);
|
|
}
|
|
ok(!video.paused, `video is playing before calling pause`);
|
|
video.pause();
|
|
}
|
|
);
|
|
return Promise.all([pausePromise, waitUntilDisplayedPlaybackChanged()]);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the specific media starts playing.
|
|
*
|
|
* @param {tab} tab
|
|
* The tab that contains the media which we would check
|
|
* @param {string} elementId
|
|
* The element Id of the media which we would check
|
|
* @return {Promise}
|
|
* Resolve when the media has been starting playing.
|
|
*/
|
|
function checkOrWaitUntilMediaStartedPlaying(tab, elementId) {
|
|
return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
|
|
return new Promise(resolve => {
|
|
const video = content.document.getElementById(Id);
|
|
if (!video) {
|
|
ok(false, `can't get the media element!`);
|
|
}
|
|
if (!video.paused) {
|
|
ok(true, `media started playing`);
|
|
resolve();
|
|
} else {
|
|
info(`wait until media starts playing`);
|
|
video.onplaying = () => {
|
|
video.onplaying = null;
|
|
ok(true, `media started playing`);
|
|
resolve();
|
|
};
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set the playback rate on a media element.
|
|
*
|
|
* @param {tab} tab
|
|
* The tab that contains the media which we would check
|
|
* @param {string} elementId
|
|
* The element Id of the media which we would check
|
|
* @param {number} rate
|
|
* The playback rate to set
|
|
* @return {Promise}
|
|
* Resolve when the playback rate has been set
|
|
*/
|
|
function setPlaybackRate(tab, elementId, rate) {
|
|
return SpecialPowers.spawn(
|
|
tab.linkedBrowser,
|
|
[elementId, rate],
|
|
(Id, rate) => {
|
|
const video = content.document.getElementById(Id);
|
|
if (!video) {
|
|
ok(false, `can't get the media element!`);
|
|
}
|
|
video.playbackRate = rate;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set the time on a media element.
|
|
*
|
|
* @param {tab} tab
|
|
* The tab that contains the media which we would check
|
|
* @param {string} elementId
|
|
* The element Id of the media which we would check
|
|
* @param {number} currentTime
|
|
* The time to set
|
|
* @return {Promise}
|
|
* Resolve when the time has been set
|
|
*/
|
|
function setCurrentTime(tab, elementId, currentTime) {
|
|
return SpecialPowers.spawn(
|
|
tab.linkedBrowser,
|
|
[elementId, currentTime],
|
|
(Id, currentTime) => {
|
|
const video = content.document.getElementById(Id);
|
|
if (!video) {
|
|
ok(false, `can't get the media element!`);
|
|
}
|
|
video.currentTime = currentTime;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the specific media stops playing.
|
|
*
|
|
* @param {tab} tab
|
|
* The tab that contains the media which we would check
|
|
* @param {string} elementId
|
|
* The element Id of the media which we would check
|
|
* @return {Promise}
|
|
* Resolve when the media has been stopped playing.
|
|
*/
|
|
function checkOrWaitUntilMediaStoppedPlaying(tab, elementId) {
|
|
return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
|
|
return new Promise(resolve => {
|
|
const video = content.document.getElementById(Id);
|
|
if (!video) {
|
|
ok(false, `can't get the media element!`);
|
|
}
|
|
if (video.paused) {
|
|
ok(true, `media stopped playing`);
|
|
resolve();
|
|
} else {
|
|
info(`wait until media stops playing`);
|
|
video.onpause = () => {
|
|
video.onpause = null;
|
|
ok(true, `media stopped playing`);
|
|
resolve();
|
|
};
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the active metadata is empty.
|
|
*/
|
|
function isCurrentMetadataEmpty() {
|
|
const current = MediaControlService.getCurrentActiveMediaMetadata();
|
|
is(current.title, "", `current title should be empty`);
|
|
is(current.artist, "", `current title should be empty`);
|
|
is(current.album, "", `current album should be empty`);
|
|
is(current.artwork.length, 0, `current artwork should be empty`);
|
|
}
|
|
|
|
/**
|
|
* Check if the active metadata is equal to the given metadata.artwork
|
|
*
|
|
* @param {object} metadata
|
|
* The metadata that would be compared with the active metadata
|
|
*/
|
|
function isCurrentMetadataEqualTo(metadata) {
|
|
const current = MediaControlService.getCurrentActiveMediaMetadata();
|
|
is(
|
|
current.title,
|
|
metadata.title,
|
|
`tile '${current.title}' is equal to ${metadata.title}`
|
|
);
|
|
is(
|
|
current.artist,
|
|
metadata.artist,
|
|
`artist '${current.artist}' is equal to ${metadata.artist}`
|
|
);
|
|
is(
|
|
current.album,
|
|
metadata.album,
|
|
`album '${current.album}' is equal to ${metadata.album}`
|
|
);
|
|
is(
|
|
current.artwork.length,
|
|
metadata.artwork.length,
|
|
`artwork length '${current.artwork.length}' is equal to ${metadata.artwork.length}`
|
|
);
|
|
for (let idx = 0; idx < metadata.artwork.length; idx++) {
|
|
// the current src we got would be a completed path of the image, so we do
|
|
// not check if they are equal, we check if the current src includes the
|
|
// metadata's file name. Eg. "http://foo/bar.jpg" v.s. "bar.jpg"
|
|
ok(
|
|
current.artwork[idx].src.includes(metadata.artwork[idx].src),
|
|
`artwork src '${current.artwork[idx].src}' includes ${metadata.artwork[idx].src}`
|
|
);
|
|
is(
|
|
current.artwork[idx].sizes,
|
|
metadata.artwork[idx].sizes,
|
|
`artwork sizes '${current.artwork[idx].sizes}' is equal to ${metadata.artwork[idx].sizes}`
|
|
);
|
|
is(
|
|
current.artwork[idx].type,
|
|
metadata.artwork[idx].type,
|
|
`artwork type '${current.artwork[idx].type}' is equal to ${metadata.artwork[idx].type}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the given tab is using the default metadata. If the tab is being
|
|
* used in the private browsing mode, `isPrivateBrowsing` should be definded in
|
|
* the `options`.
|
|
*/
|
|
async function isGivenTabUsingDefaultMetadata(tab, options = {}) {
|
|
const localization = new Localization([
|
|
"branding/brand.ftl",
|
|
"dom/media.ftl",
|
|
]);
|
|
const fallbackTitle = await localization.formatValue(
|
|
"mediastatus-fallback-title"
|
|
);
|
|
ok(fallbackTitle.length, "l10n fallback title is not empty");
|
|
|
|
const metadata =
|
|
tab.linkedBrowser.browsingContext.mediaController.getMetadata();
|
|
|
|
await SpecialPowers.spawn(
|
|
tab.linkedBrowser,
|
|
[metadata.title, fallbackTitle, options.isPrivateBrowsing],
|
|
(title, fallbackTitle, isPrivateBrowsing) => {
|
|
if (isPrivateBrowsing || !content.document.title.length) {
|
|
is(title, fallbackTitle, "Using a generic default fallback title");
|
|
} else {
|
|
is(
|
|
title,
|
|
content.document.title,
|
|
"Using website title as a default title"
|
|
);
|
|
}
|
|
}
|
|
);
|
|
is(metadata.artwork.length, 1, "Default metada contains one artwork");
|
|
ok(
|
|
metadata.artwork[0].src.includes("defaultFavicon.svg"),
|
|
"Using default favicon as a default art work"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wait until the main media controller changes its playback state, we would
|
|
* observe that by listening for `media-displayed-playback-changed`
|
|
* notification.
|
|
*
|
|
* @return {Promise}
|
|
* Resolve when observing `media-displayed-playback-changed`
|
|
*/
|
|
function waitUntilDisplayedPlaybackChanged() {
|
|
return BrowserUtils.promiseObserved("media-displayed-playback-changed");
|
|
}
|
|
|
|
/**
|
|
* Wait until the metadata that would be displayed on the virtual control
|
|
* interface changes. we would observe that by listening for
|
|
* `media-displayed-metadata-changed` notification.
|
|
*
|
|
* @return {Promise}
|
|
* Resolve when observing `media-displayed-metadata-changed`
|
|
*/
|
|
function waitUntilDisplayedMetadataChanged() {
|
|
return BrowserUtils.promiseObserved("media-displayed-metadata-changed");
|
|
}
|
|
|
|
/**
|
|
* Wait until the main media controller has been changed, we would observe that
|
|
* by listening for the `main-media-controller-changed` notification.
|
|
*
|
|
* @return {Promise}
|
|
* Resolve when observing `main-media-controller-changed`
|
|
*/
|
|
function waitUntilMainMediaControllerChanged() {
|
|
return BrowserUtils.promiseObserved("main-media-controller-changed");
|
|
}
|
|
|
|
/**
|
|
* Wait until any media controller updates its metadata even if it's not the
|
|
* main controller. The difference between this function and
|
|
* `waitUntilDisplayedMetadataChanged()` is that the changed metadata might come
|
|
* from non-main controller so it won't be show on the virtual control
|
|
* interface. we would observe that by listening for
|
|
* `media-session-controller-metadata-changed` notification.
|
|
*
|
|
* @return {Promise}
|
|
* Resolve when observing `media-session-controller-metadata-changed`
|
|
*/
|
|
function waitUntilControllerMetadataChanged() {
|
|
return BrowserUtils.promiseObserved(
|
|
"media-session-controller-metadata-changed"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wait until media controller amount changes, we would observe that by
|
|
* listening for `media-controller-amount-changed` notification.
|
|
*
|
|
* @return {Promise}
|
|
* Resolve when observing `media-controller-amount-changed`
|
|
*/
|
|
function waitUntilMediaControllerAmountChanged() {
|
|
return BrowserUtils.promiseObserved("media-controller-amount-changed");
|
|
}
|
|
|
|
/**
|
|
* Wait until the position state that would be displayed on the virtual control
|
|
* interface changes. we would observe that by listening for
|
|
* `media-position-state-changed` notification.
|
|
*
|
|
* @return {Promise}
|
|
* Resolve when observing `media-position-state-changed`
|
|
*/
|
|
function waitUntilPositionStateChanged() {
|
|
return BrowserUtils.promiseObserved("media-position-state-changed");
|
|
}
|
|
|
|
/**
|
|
* check if the media controll from given tab is active. If not, return a
|
|
* promise and resolve it when controller become active.
|
|
*/
|
|
async function checkOrWaitUntilControllerBecomeActive(tab) {
|
|
const controller = tab.linkedBrowser.browsingContext.mediaController;
|
|
if (controller.isActive) {
|
|
return;
|
|
}
|
|
await new Promise(r => (controller.onactivated = r));
|
|
}
|
|
|
|
/**
|
|
* Logs all `positionstatechange` events in a tab.
|
|
*/
|
|
function logPositionStateChangeEvents(tab) {
|
|
tab.linkedBrowser.browsingContext.mediaController.addEventListener(
|
|
"positionstatechange",
|
|
event =>
|
|
info(
|
|
`got position state: ${JSON.stringify({
|
|
duration: event.duration,
|
|
playbackRate: event.playbackRate,
|
|
position: event.position,
|
|
})}`
|
|
)
|
|
);
|
|
}
|