text: /** @license
   * Copyright 2016 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  describe('UI', () => {
    const Util = shaka.test.Util;
    const UiUtils = shaka.test.UiUtils;
  
    /** @type {!jasmine.Spy} */
    let onErrorSpy;
  
    /** @type {!HTMLVideoElement} */
    let video;
    /** @type {!HTMLElement} */
    let videoContainer;
    /** @type {!shaka.Player} */
    let player;
    /** @type {shaka.util.EventManager} */
    let eventManager;
    /** @type {shaka.test.Waiter} */
    let waiter;
    /** @type {!Element} */
    let cssLink;
    /** @type {!shaka.ui.Controls} */
    let controls;
  
    let compiledShaka;
  
    beforeAll(async () => {
      cssLink = document.createElement('link');
      await UiUtils.setupCSS(cssLink);
  
      compiledShaka = await Util.loadShaka(getClientArg('uncompiled'));
      await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
    });
  
    beforeEach(async () => {
      video = shaka.test.UiUtils.createVideoElement();
  
      videoContainer = shaka.util.Dom.createHTMLElement('div');
      videoContainer.appendChild(video);
      document.body.appendChild(videoContainer);
      player = new compiledShaka.Player(video);
  
      // Create UI
      // Add all of the buttons we have
      const config = {
        controlPanelElements: [
          'time_and_duration',
          'mute',
          'volume',
          'fullscreen',
          'overflow_menu',
          'fast_forward',
          'rewind',
        ],
        overflowMenuButtons: [
          'captions',
          'quality',
          'language',
          'picture_in_picture',
          'cast',
        ],
        // TODO: Cast receiver id to test chromecast integration
      };
  
      const ui = new compiledShaka.ui.Overlay(player, videoContainer, video);
      ui.configure(config);
  
      // Grab event manager from the uncompiled library:
      eventManager = new shaka.util.EventManager();
      waiter = new shaka.test.Waiter(eventManager);
  
      controls = ui.getControls();
  
      onErrorSpy = jasmine.createSpy('onError');
      onErrorSpy.and.callFake((event) => {
        fail(event.detail);
      });
      eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
      eventManager.listen(controls, 'error', Util.spyFunc(onErrorSpy));
  
      await player.load('test:sintel_multi_lingual_multi_res_compiled');
      // For this event, we ignore a timeout, since we sometimes miss this event
      // on Tizen.  But expect that the video is ready anyway.
      await waiter.failOnTimeout(false).waitForEvent(video, 'canplay');
      expect(video.readyState).not.toBe(0);
  
      // All other events after this should fail on timeout (the default).
      await waiter.failOnTimeout(true);
    });
  
    afterEach(async () => {
      eventManager.release();
      waiter = null;
      await UiUtils.cleanupUI();
    });
  
    afterAll(() => {
      document.head.removeChild(cssLink);
    });
  
  
    describe('language selections', () => {
      /** @type {!Map.<string, !HTMLElement>} */
      let languagesToButtons;
      /** @type {!Array.<string>} */
      let langsFromContent;
      /** @type {!Array.<!HTMLElement>} */
      let languageButtons;
      /** @type {!Element} */
      let languageMenu;
      /** @type {string} */
      let oldLanguage;
      /** @type {string} */
      let newLanguage;
  
      describe('audio', () => {
        beforeEach(() => {
          oldLanguage = 'en';
          newLanguage = 'es';
          languageMenu = shaka.util.Dom.getElementByClassName(
              'shaka-audio-languages', videoContainer);
          setupLanguageTests(player.getAudioLanguagesAndRoles());
        });
  
        it('contains all the languages', () => {
          verifyLanguages();
        });
  
        it('choosing language through UI has effect on player', async () => {
          await verifyLanguageChangeViaUI(
              'variantchanged', () => player.getVariantTracks());
        });
  
        it('choosing language through API has effect on UI', async () => {
          await verifyLanguageChangeViaAPI(
              'languageselectionupdated',
              () => player.getVariantTracks(),
              (language) => player.selectAudioLanguage(language));
        });
      });
  
  
      describe('caption selection', () => {
        beforeEach(() => {
          oldLanguage = 'zh';
          newLanguage = 'fr';
          languageMenu = shaka.util.Dom.getElementByClassName(
              'shaka-text-languages', videoContainer);
          setupLanguageTests(player.getTextLanguagesAndRoles());
        });
  
        it('contains all the languages', () => {
          verifyLanguages();
        });
  
        it('choosing caption language through UI has effect on player',
            async () => {
              await verifyLanguageChangeViaUI(
                  'textchanged', () => player.getTextTracks());
            });
  
        it('choosing language through API has effect on UI', async () => {
          // Enable & verify the text, or else the text won't be streamed and the
          // language selection won't do anything.
          await player.setTextTrackVisibility(true);
          expect(player.isTextTrackVisible()).toBe(true);
  
          await verifyLanguageChangeViaAPI(
              'captionselectionupdated',
              () => player.getTextTracks(),
              (language) => player.selectTextLanguage(language));
        });
  
        it('turning captions off through UI has effect on player', async () => {
          // Enable & verify the text.
          await player.setTextTrackVisibility(true);
          expect(player.isTextTrackVisible()).toBe(true);
  
          // Find and click the 'Off' button
          getOffButton().click();
          // Wait for the change to take effect
          await waiter.waitForEvent(player, 'texttrackvisibility');
  
          expect(player.isTextTrackVisible()).toBe(false);
        });
  
        it('turning captions off through API has effect on UI', async () => {
          // This test is invalid if the text is not initially visible, because
          // setTextTrackVisibility() does nothing if there are no changes.
          await player.setTextTrackVisibility(true);
          expect(player.isTextTrackVisible()).toBe(true);
  
          const p = waiter.waitForEvent(controls, 'captionselectionupdated');
  
          // Disable & verify the text.
          await player.setTextTrackVisibility(false);
          expect(player.isTextTrackVisible()).toBe(false);
  
          // Wait for the change to take effect
          await p;
  
          const offButtonChosen =
              getOffButton().querySelector('.shaka-chosen-item');
          expect(offButtonChosen).not.toBe(null);
        });
  
  
        /**
         * @return {Element}
         */
        function getOffButton() {
          const offButton =
              languageMenu.querySelector('.shaka-turn-captions-off-button');
  
          expect(offButton).not.toBe(null);
          return offButton;
        }
      });
  
  
      /**
       * @param {!Array.<shaka.extern.LanguageRole>} languagesAndRoles
       */
      function setupLanguageTests(languagesAndRoles) {
        langsFromContent = languagesAndRoles.map((langAndRole) => {
          return langAndRole.language;
        });
  
        languageButtons = filterButtons(languageMenu.childNodes,
            ['shaka-back-to-overflow-button', 'shaka-turn-captions-off-button']);
  
        languagesToButtons = mapChoicesToButtons(
            /* allButtons= */ languageButtons,
            /* choices= */ langsFromContent,
            /* modifier= */ getNativeName,
        );
      }
  
  
      /**
       * @param {string} language
       * @return {string}
       */
      function getNativeName(language) {
        return mozilla.LanguageMapping[language].nativeName;
      }
  
  
      /**
       * Make sure languages specified by the manifest match what we show on UI.
       */
      function verifyLanguages() {
        const langsFromContentNative = langsFromContent.map((lang) => {
          return getNativeName(lang);
        });
  
        verifyItems(langsFromContentNative, languageButtons);
      }
  
  
      /**
       * @param {string} playerEventName
       * @param {function():!Array.<!shaka.extern.Track>} getTracks
       */
      async function verifyLanguageChangeViaUI(playerEventName, getTracks) {
        expect(getSelectedTrack(getTracks()).language).toBe(oldLanguage);
  
        const button = languagesToButtons.get(newLanguage);
        button.click();
  
        // Wait for the change to take effect
        await waiter.waitForEvent(player, playerEventName);
        expect(getSelectedTrack(getTracks()).language).toBe(newLanguage);
      }
  
  
      /**
       * @param {string} controlsEventName
       * @param {function():!Array.<!shaka.extern.Track>} getTracks
       * @param {function(string)} selectLanguage
       */
      async function verifyLanguageChangeViaAPI(
          controlsEventName, getTracks, selectLanguage) {
        expect(getSelectedTrack(getTracks()).language).toBe(oldLanguage);
  
        const p = waiter.waitForEvent(controls, controlsEventName);
  
        selectLanguage(newLanguage);
  
        // Wait for the UI to get updated
        await p;
  
        // Buttons were re-created on variant change
        languageButtons = filterButtons(languageMenu.childNodes,
            ['shaka-back-to-overflow-button', 'shaka-turn-captions-off-button']);
  
        languagesToButtons = mapChoicesToButtons(
            /* allButtons= */ languageButtons,
            /* choices= */ langsFromContent,
            /* modifier= */ getNativeName
        );
  
        const button = languagesToButtons.get(newLanguage);
        const isChosen = button.querySelector('.shaka-chosen-item');
  
        expect(isChosen).not.toBe(null);
      }
    });
  
  
    describe('resolution selection', () => {
      /** @type {!Map.<number, !HTMLElement>} */
      let resolutionsToButtons;
      /** @type {!Array.<number>} */
      let resolutionsFromContent;
      /** @type {!Array.<!HTMLElement>} */
      let resolutionButtons;
      /** @type {!Element} */
      let resolutionsMenu;
      /** @type {number} */
      let oldResolution;
      /** @type {number} */
      let newResolution;
      /** @type {!Array.<shaka.extern.Track>} */
      let tracks;
      /** @type {string} */
      let preferredLanguage;
      /** @type {!shaka.extern.Track} */
      let oldResolutionTrack;
  
  
      beforeEach(async () => {
        oldResolution = 182;
        newResolution = 272;
        // Chosen language affects which resolutions get
        // displayed in the UI.
        preferredLanguage = 'en';
  
        // Disable abr for the resolution tests
        player.configure('abr.enabled', false);
  
        const selectedLanguage =
            getSelectedTrack(player.getVariantTracks()).language;
        if (selectedLanguage != preferredLanguage) {
          player.selectAudioLanguage(preferredLanguage);
          await waiter.waitForEvent(player, 'variantchanged');
        }
  
        resolutionsMenu = shaka.util.Dom.getElementByClassName(
            'shaka-resolutions', videoContainer);
  
        updateResolutionButtonsAndMap();
  
        oldResolutionTrack = findTrackWithHeight(tracks, oldResolution);
      });
  
  
      it('contains all the relevant resolutions', () => {
        const formattedResolutions = resolutionsFromContent.map((res) => {
          return formatResolution(res);
        });
        verifyItems(formattedResolutions, resolutionButtons);
      });
  
  
      it('changing resolution via UI has effect on the player', async () => {
        player.selectVariantTrack(oldResolutionTrack);
  
        // Wait for the change to take effect
        await waiter.waitForEvent(player, 'variantchanged');
        // Update the tracks
        tracks = player.getVariantTracks();
        expect(getSelectedTrack(tracks).height).toBe(oldResolution);
  
        const button = resolutionsToButtons.get(newResolution);
        button.click();
  
        // Wait for the change to take effect
        await waiter.waitForEvent(player, 'variantchanged');
        // Update the tracks
        tracks = player.getVariantTracks();
        expect(getSelectedTrack(tracks).height).toBe(newResolution);
      });
  
  
      it('changing resolution via API has effect on the UI', async () => {
        // Start with the old resolution
        player.selectVariantTrack(oldResolutionTrack);
  
        // Wait for the change to take effect
        await waiter.waitForEvent(player, 'variantchanged');
        updateResolutionButtonsAndMap();
        expect(getSelectedTrack(tracks).height).toBe(oldResolution);
  
        const p = waiter.waitForEvent(controls, 'resolutionselectionupdated');
  
        const newResolutionTrack = findTrackWithHeight(tracks, newResolution);
        player.selectVariantTrack(newResolutionTrack);
  
        // Wait for the change to take effect
        await p;
  
        updateResolutionButtonsAndMap();
  
        expect(getSelectedTrack(tracks).height).toBe(newResolution);
  
        const button = resolutionsToButtons.get(newResolution);
        const isChosen = button.querySelector('.shaka-chosen-item');
  
        expect(isChosen).not.toBe(null);
      });
  
  
      it('selecting Auto via UI enables ABR', async () => {
        // We disabled abr in beforeEach()
        expect(player.getConfiguration().abr.enabled).toBe(false);
  
        const p = waiter.waitForEvent(controls, 'resolutionselectionupdated');
  
        // Find the 'Auto' button
        const auto = getAutoButton();
        auto.click();
  
        await p;
        expect(player.getConfiguration().abr.enabled).toBe(true);
      });
  
  
      it('selecting specific resolution disables ABR', async () => {
        const config = {abr: {enabled: true}};
        player.configure(config);
  
        const p = waiter.waitForEvent(controls, 'resolutionselectionupdated');
  
        // Any resolution would works
        const button = resolutionsToButtons.get(newResolution);
        button.click();
  
        await p;
        expect(player.getConfiguration().abr.enabled).toBe(false);
      });
  
  
      it('enabling ABR via API gets the Auto button selected', async () => {
        expect(player.getConfiguration().abr.enabled).toBe(false);
  
        // Setup listener to the ui event. The event, trigerring the update
        // is dispatched inside player.configure(), so we need to start
        // listening before calling it.
        const uiReady = waiter.waitForEvent(
            controls, 'resolutionselectionupdated');
        const config = {abr: {enabled: true}};
  
        player.configure(config);
  
        await uiReady;
  
        const auto = getAutoButton();
        const isChosen = auto.querySelector('.shaka-chosen-item');
  
        expect(isChosen).not.toBe(null);
      });
  
      it('restores the resolutions menu after audio-only playback', async () => {
        /** @type {HTMLElement} */
        const resolutionButton = shaka.util.Dom.getElementByClassName(
            'shaka-resolution-button', videoContainer);
  
        // Load an audio-only clip.  The menu should be hidden.
        await player.load('test:sintel_audio_only_compiled');
        expect(player.isAudioOnly()).toBe(true);
        expect(resolutionButton.classList.contains('shaka-hidden')).toBe(true);
  
        // Load an audio-video clip.  The menu should be visible again.
        await player.load('test:sintel_multi_lingual_multi_res_compiled');
        expect(player.isAudioOnly()).toBe(false);
        expect(resolutionButton.classList.contains('shaka-hidden')).toBe(false);
      });
  
  
      /**
       * @return {Element}
       */
      function getAutoButton() {
        const auto =
            resolutionsMenu.querySelector('.shaka-enable-abr-button');
  
        expect(auto).not.toBe(null);
        return auto;
      }
  
  
      /**
       * Gets the resolution to the same format it
       * appears in the UI: height + 'p'.
       *
       * @param {number} height
       * @return {string}
       */
      function formatResolution(height) {
        return height.toString() + 'p';
      }
  
  
      /**
       * @param {!Array.<!shaka.extern.Track>} tracks
       * @param {number} height
       * @return {shaka.extern.Track}
       */
      function findTrackWithHeight(tracks, height) {
        let trackWithRes = null;
        for (const track of tracks) {
          if (track.height == height) {
            trackWithRes = track;
          }
        }
        goog.asserts.assert(trackWithRes != null,
            'Should have found track!');
  
        return trackWithRes;
      }
  
  
      function updateResolutionButtonsAndMap() {
        tracks = player.getVariantTracks();
        tracks = tracks.filter((track) => {
          return track.language == preferredLanguage;
        });
  
        resolutionsFromContent = tracks.map((track) => {
          return track.height;
        });
  
        resolutionButtons = filterButtons(
            /* buttons= */ resolutionsMenu.childNodes,
            /* excludeClasses= */ [
              'shaka-back-to-overflow-button',
              'shaka-enable-abr-button',
            ]);
  
        resolutionsToButtons = mapChoicesToButtons(
            /* buttons= */ resolutionButtons,
            /* choices= */ resolutionsFromContent,
            /* modifier= */ formatResolution);
      }
    });
  
    /**
     * @param {!Array.<!shaka.extern.Track>} tracks
     * @return {!shaka.extern.Track}
     */
    function getSelectedTrack(tracks) {
      const activeTracks = tracks.filter((track) => {
        return track.active == true;
      });
  
      return activeTracks[0];
    }
  
    /**
      * @param {!Array.<!HTMLElement>} buttons
      * @param {!Array.<string>} choices
      * @param {function(string):string|function(number):string} modifier
      * @return {!Map.<string, !HTMLElement>|!Map.<number, !HTMLElement>}
      */
    function mapChoicesToButtons(buttons, choices, modifier) {
      expect(buttons.length).toBe(choices.length);
  
      const map = new Map();
  
      // Find which choice corresponds to which button
      for (const choice of choices) {
        for (const button of buttons) {
          expect(button.childNodes.length).toBeGreaterThan(0);
          const uiOption = button.childNodes[0].textContent;
          const contentOption = modifier(choice);
          if (contentOption == uiOption) {
            map.set(choice, button);
          }
        }
      }
  
      return map;
    }
  
  
    /**
     * Filter out buttons with given classes.
     *
     * @param {!NodeList} buttons
     * @param {!Array.<string>} excludeClasses
     * @return {!Array.<!HTMLElement>}
     */
    function filterButtons(buttons, excludeClasses) {
      return shaka.util.Iterables.filter(buttons,
          (node) => {
            const button = shaka.util.Dom.asHTMLElement(node);
            for (const excludeClass of excludeClasses) {
              if (button.classList.contains(excludeClass)) {
                return false;
              }
            }
            return true;
          });
    }
  
  
    /**
     * Make sure elements from content match their UI representation.
     * (The order doesn't matter).
     *
     * @param {!Array.<string>} elementsFromContent
     * @param {!Array.<!HTMLElement>} elementsFromUI
     */
    function verifyItems(elementsFromContent, elementsFromUI) {
      for (const element of elementsFromUI) {
        expect(element.childNodes.length).toBeGreaterThan(0);
        const elementName = element.childNodes[0].textContent;
        expect(elementsFromContent.indexOf(elementName)).not.toBe(-1);
      }
    }
  });
  