1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
|
/**
* 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();
};
}
});
});
}
/**
* 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");
}
/**
* 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));
}
|