head.js (15190B)
1 /** 2 * This function would create a new foreround tab and load the url for it. In 3 * addition, instead of returning a tab element, we return a tab wrapper that 4 * helps us to automatically detect if the media controller of that tab 5 * dispatches the first (activated) and the last event (deactivated) correctly. 6 * @ param url 7 * the page url which tab would load 8 * @ param input window (optional) 9 * if it exists, the tab would be created from the input window. If not, 10 * then the tab would be created in current window. 11 * @ param needCheck (optional) 12 * it decides if we would perform the check for the first and last event 13 * on the media controller. It's default true. 14 */ 15 async function createLoadedTabWrapper( 16 url, 17 { inputWindow = window, needCheck = true } = {} 18 ) { 19 class tabWrapper { 20 constructor(tab, needCheck) { 21 this._tab = tab; 22 this._controller = tab.linkedBrowser.browsingContext.mediaController; 23 this._firstEvent = ""; 24 this._lastEvent = ""; 25 this._events = [ 26 "activated", 27 "deactivated", 28 "metadatachange", 29 "playbackstatechange", 30 "positionstatechange", 31 "supportedkeyschange", 32 ]; 33 this._needCheck = needCheck; 34 if (this._needCheck) { 35 this._registerAllEvents(); 36 } 37 } 38 _registerAllEvents() { 39 for (let event of this._events) { 40 this._controller.addEventListener(event, this._handleEvent.bind(this)); 41 } 42 } 43 _unregisterAllEvents() { 44 for (let event of this._events) { 45 this._controller.removeEventListener( 46 event, 47 this._handleEvent.bind(this) 48 ); 49 } 50 } 51 _handleEvent(event) { 52 info(`handle event=${event.type}`); 53 if (this._firstEvent === "") { 54 this._firstEvent = event.type; 55 } 56 this._lastEvent = event.type; 57 } 58 get linkedBrowser() { 59 return this._tab.linkedBrowser; 60 } 61 get controller() { 62 return this._controller; 63 } 64 get tabElement() { 65 return this._tab; 66 } 67 async close() { 68 info(`wait until finishing close tab wrapper`); 69 const deactivationPromise = this._controller.isActive 70 ? new Promise(r => (this._controller.ondeactivated = r)) 71 : Promise.resolve(); 72 BrowserTestUtils.removeTab(this._tab); 73 await deactivationPromise; 74 if (this._needCheck) { 75 is(this._firstEvent, "activated", "First event should be 'activated'"); 76 is( 77 this._lastEvent, 78 "deactivated", 79 "Last event should be 'deactivated'" 80 ); 81 this._unregisterAllEvents(); 82 } 83 } 84 } 85 const browser = inputWindow ? inputWindow.gBrowser : window.gBrowser; 86 let tab = await BrowserTestUtils.openNewForegroundTab(browser, url); 87 return new tabWrapper(tab, needCheck); 88 } 89 90 /** 91 * Returns a promise that resolves when generated media control keys has 92 * triggered the main media controller's corresponding method and changes its 93 * playback state. 94 * 95 * @param {string} event 96 * The event name of the media control key 97 * @return {Promise} 98 * Resolve when the main controller receives the media control key event 99 * and change its playback state. 100 */ 101 function generateMediaControlKeyEvent(event) { 102 const playbackStateChanged = waitUntilDisplayedPlaybackChanged(); 103 MediaControlService.generateMediaControlKey(event); 104 return playbackStateChanged; 105 } 106 107 /** 108 * Play the specific media and wait until it plays successfully and the main 109 * controller has been updated. 110 * 111 * @param {tab} tab 112 * The tab that contains the media which we would play 113 * @param {string} elementId 114 * The element Id of the media which we would play 115 * @return {Promise} 116 * Resolve when the media has been starting playing and the main 117 * controller has been updated. 118 */ 119 async function playMedia(tab, elementId) { 120 const playbackStatePromise = waitUntilDisplayedPlaybackChanged(); 121 await SpecialPowers.spawn(tab.linkedBrowser, [elementId], async Id => { 122 const video = content.document.getElementById(Id); 123 if (!video) { 124 ok(false, `can't get the media element!`); 125 } 126 ok( 127 await video.play().then( 128 _ => true, 129 _ => false 130 ), 131 "video started playing" 132 ); 133 }); 134 return playbackStatePromise; 135 } 136 137 /** 138 * Pause the specific media and wait until it pauses successfully and the main 139 * controller has been updated. 140 * 141 * @param {tab} tab 142 * The tab that contains the media which we would pause 143 * @param {string} elementId 144 * The element Id of the media which we would pause 145 * @return {Promise} 146 * Resolve when the media has been paused and the main controller has 147 * been updated. 148 */ 149 function pauseMedia(tab, elementId) { 150 const pausePromise = SpecialPowers.spawn( 151 tab.linkedBrowser, 152 [elementId], 153 Id => { 154 const video = content.document.getElementById(Id); 155 if (!video) { 156 ok(false, `can't get the media element!`); 157 } 158 ok(!video.paused, `video is playing before calling pause`); 159 video.pause(); 160 } 161 ); 162 return Promise.all([pausePromise, waitUntilDisplayedPlaybackChanged()]); 163 } 164 165 /** 166 * Returns a promise that resolves when the specific media starts playing. 167 * 168 * @param {tab} tab 169 * The tab that contains the media which we would check 170 * @param {string} elementId 171 * The element Id of the media which we would check 172 * @return {Promise} 173 * Resolve when the media has been starting playing. 174 */ 175 function checkOrWaitUntilMediaStartedPlaying(tab, elementId) { 176 return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => { 177 return new Promise(resolve => { 178 const video = content.document.getElementById(Id); 179 if (!video) { 180 ok(false, `can't get the media element!`); 181 } 182 if (!video.paused) { 183 ok(true, `media started playing`); 184 resolve(); 185 } else { 186 info(`wait until media starts playing`); 187 video.onplaying = () => { 188 video.onplaying = null; 189 ok(true, `media started playing`); 190 resolve(); 191 }; 192 } 193 }); 194 }); 195 } 196 197 /** 198 * Set the playback rate on a media element. 199 * 200 * @param {tab} tab 201 * The tab that contains the media which we would check 202 * @param {string} elementId 203 * The element Id of the media which we would check 204 * @param {number} rate 205 * The playback rate to set 206 * @return {Promise} 207 * Resolve when the playback rate has been set 208 */ 209 function setPlaybackRate(tab, elementId, rate) { 210 return SpecialPowers.spawn( 211 tab.linkedBrowser, 212 [elementId, rate], 213 (Id, rate) => { 214 const video = content.document.getElementById(Id); 215 if (!video) { 216 ok(false, `can't get the media element!`); 217 } 218 video.playbackRate = rate; 219 } 220 ); 221 } 222 223 /** 224 * Set the time on a media element. 225 * 226 * @param {tab} tab 227 * The tab that contains the media which we would check 228 * @param {string} elementId 229 * The element Id of the media which we would check 230 * @param {number} currentTime 231 * The time to set 232 * @return {Promise} 233 * Resolve when the time has been set 234 */ 235 function setCurrentTime(tab, elementId, currentTime) { 236 return SpecialPowers.spawn( 237 tab.linkedBrowser, 238 [elementId, currentTime], 239 (Id, currentTime) => { 240 const video = content.document.getElementById(Id); 241 if (!video) { 242 ok(false, `can't get the media element!`); 243 } 244 video.currentTime = currentTime; 245 } 246 ); 247 } 248 249 /** 250 * Returns a promise that resolves when the specific media stops playing. 251 * 252 * @param {tab} tab 253 * The tab that contains the media which we would check 254 * @param {string} elementId 255 * The element Id of the media which we would check 256 * @return {Promise} 257 * Resolve when the media has been stopped playing. 258 */ 259 function checkOrWaitUntilMediaStoppedPlaying(tab, elementId) { 260 return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => { 261 return new Promise(resolve => { 262 const video = content.document.getElementById(Id); 263 if (!video) { 264 ok(false, `can't get the media element!`); 265 } 266 if (video.paused) { 267 ok(true, `media stopped playing`); 268 resolve(); 269 } else { 270 info(`wait until media stops playing`); 271 video.onpause = () => { 272 video.onpause = null; 273 ok(true, `media stopped playing`); 274 resolve(); 275 }; 276 } 277 }); 278 }); 279 } 280 281 /** 282 * Check if the active metadata is empty. 283 */ 284 function isCurrentMetadataEmpty() { 285 const current = MediaControlService.getCurrentActiveMediaMetadata(); 286 is(current.title, "", `current title should be empty`); 287 is(current.artist, "", `current title should be empty`); 288 is(current.album, "", `current album should be empty`); 289 is(current.artwork.length, 0, `current artwork should be empty`); 290 } 291 292 /** 293 * Check if the active metadata is equal to the given metadata.artwork 294 * 295 * @param {object} metadata 296 * The metadata that would be compared with the active metadata 297 */ 298 function isCurrentMetadataEqualTo(metadata) { 299 const current = MediaControlService.getCurrentActiveMediaMetadata(); 300 is( 301 current.title, 302 metadata.title, 303 `tile '${current.title}' is equal to ${metadata.title}` 304 ); 305 is( 306 current.artist, 307 metadata.artist, 308 `artist '${current.artist}' is equal to ${metadata.artist}` 309 ); 310 is( 311 current.album, 312 metadata.album, 313 `album '${current.album}' is equal to ${metadata.album}` 314 ); 315 is( 316 current.artwork.length, 317 metadata.artwork.length, 318 `artwork length '${current.artwork.length}' is equal to ${metadata.artwork.length}` 319 ); 320 for (let idx = 0; idx < metadata.artwork.length; idx++) { 321 // the current src we got would be a completed path of the image, so we do 322 // not check if they are equal, we check if the current src includes the 323 // metadata's file name. Eg. "http://foo/bar.jpg" v.s. "bar.jpg" 324 ok( 325 current.artwork[idx].src.includes(metadata.artwork[idx].src), 326 `artwork src '${current.artwork[idx].src}' includes ${metadata.artwork[idx].src}` 327 ); 328 is( 329 current.artwork[idx].sizes, 330 metadata.artwork[idx].sizes, 331 `artwork sizes '${current.artwork[idx].sizes}' is equal to ${metadata.artwork[idx].sizes}` 332 ); 333 is( 334 current.artwork[idx].type, 335 metadata.artwork[idx].type, 336 `artwork type '${current.artwork[idx].type}' is equal to ${metadata.artwork[idx].type}` 337 ); 338 } 339 } 340 341 /** 342 * Check if the given tab is using the default metadata. If the tab is being 343 * used in the private browsing mode, `isPrivateBrowsing` should be definded in 344 * the `options`. 345 */ 346 async function isGivenTabUsingDefaultMetadata(tab, options = {}) { 347 const localization = new Localization([ 348 "branding/brand.ftl", 349 "dom/media.ftl", 350 ]); 351 const fallbackTitle = await localization.formatValue( 352 "mediastatus-fallback-title" 353 ); 354 ok(fallbackTitle.length, "l10n fallback title is not empty"); 355 356 const metadata = 357 tab.linkedBrowser.browsingContext.mediaController.getMetadata(); 358 359 await SpecialPowers.spawn( 360 tab.linkedBrowser, 361 [metadata.title, fallbackTitle, options.isPrivateBrowsing], 362 (title, fallbackTitle, isPrivateBrowsing) => { 363 if (isPrivateBrowsing || !content.document.title.length) { 364 is(title, fallbackTitle, "Using a generic default fallback title"); 365 } else { 366 is( 367 title, 368 content.document.title, 369 "Using website title as a default title" 370 ); 371 } 372 } 373 ); 374 is(metadata.artwork.length, 1, "Default metada contains one artwork"); 375 ok( 376 metadata.artwork[0].src.includes("defaultFavicon.svg"), 377 "Using default favicon as a default art work" 378 ); 379 } 380 381 /** 382 * Wait until the main media controller changes its playback state, we would 383 * observe that by listening for `media-displayed-playback-changed` 384 * notification. 385 * 386 * @return {Promise} 387 * Resolve when observing `media-displayed-playback-changed` 388 */ 389 function waitUntilDisplayedPlaybackChanged() { 390 return BrowserUtils.promiseObserved("media-displayed-playback-changed"); 391 } 392 393 /** 394 * Wait until the metadata that would be displayed on the virtual control 395 * interface changes. we would observe that by listening for 396 * `media-displayed-metadata-changed` notification. 397 * 398 * @return {Promise} 399 * Resolve when observing `media-displayed-metadata-changed` 400 */ 401 function waitUntilDisplayedMetadataChanged() { 402 return BrowserUtils.promiseObserved("media-displayed-metadata-changed"); 403 } 404 405 /** 406 * Wait until the main media controller has been changed, we would observe that 407 * by listening for the `main-media-controller-changed` notification. 408 * 409 * @return {Promise} 410 * Resolve when observing `main-media-controller-changed` 411 */ 412 function waitUntilMainMediaControllerChanged() { 413 return BrowserUtils.promiseObserved("main-media-controller-changed"); 414 } 415 416 /** 417 * Wait until any media controller updates its metadata even if it's not the 418 * main controller. The difference between this function and 419 * `waitUntilDisplayedMetadataChanged()` is that the changed metadata might come 420 * from non-main controller so it won't be show on the virtual control 421 * interface. we would observe that by listening for 422 * `media-session-controller-metadata-changed` notification. 423 * 424 * @return {Promise} 425 * Resolve when observing `media-session-controller-metadata-changed` 426 */ 427 function waitUntilControllerMetadataChanged() { 428 return BrowserUtils.promiseObserved( 429 "media-session-controller-metadata-changed" 430 ); 431 } 432 433 /** 434 * Wait until media controller amount changes, we would observe that by 435 * listening for `media-controller-amount-changed` notification. 436 * 437 * @return {Promise} 438 * Resolve when observing `media-controller-amount-changed` 439 */ 440 function waitUntilMediaControllerAmountChanged() { 441 return BrowserUtils.promiseObserved("media-controller-amount-changed"); 442 } 443 444 /** 445 * Wait until the position state that would be displayed on the virtual control 446 * interface changes. we would observe that by listening for 447 * `media-position-state-changed` notification. 448 * 449 * @return {Promise} 450 * Resolve when observing `media-position-state-changed` 451 */ 452 function waitUntilPositionStateChanged() { 453 return BrowserUtils.promiseObserved("media-position-state-changed"); 454 } 455 456 /** 457 * check if the media controll from given tab is active. If not, return a 458 * promise and resolve it when controller become active. 459 */ 460 async function checkOrWaitUntilControllerBecomeActive(tab) { 461 const controller = tab.linkedBrowser.browsingContext.mediaController; 462 if (controller.isActive) { 463 return; 464 } 465 await new Promise(r => (controller.onactivated = r)); 466 } 467 468 /** 469 * Logs all `positionstatechange` events in a tab. 470 */ 471 function logPositionStateChangeEvents(tab) { 472 tab.linkedBrowser.browsingContext.mediaController.addEventListener( 473 "positionstatechange", 474 event => 475 info( 476 `got position state: ${JSON.stringify({ 477 duration: event.duration, 478 playbackRate: event.playbackRate, 479 position: event.position, 480 })}` 481 ) 482 ); 483 }