/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('UITextDisplayer', () => { /** @type {!HTMLElement} */ let videoContainer; /** @type {!HTMLVideoElement} */ let video; /** @type {shaka.text.UITextDisplayer} */ let textDisplayer; /** @type {number} */ const videoContainerHeight = 450; /** @type {Object} **/ let player; /** * Transform a cssText to an object. * Example: * cssText: 'background-color: black; color: green; font-size: 10px;' * cssObject: { * background-color: 'black', * color: 'green', * font-size: '10px', * } * @param {string} cssStr * @return {!Object} */ function parseCssText(cssStr) { // Remove the white spaces in the string. // Split with ';' and ignore the last one. const css = cssStr.replace(/\s/g, '').substring(0, cssStr.length - 1) .split(';'); const cssObj = {}; for (const cssStyle of css) { const propertyAndValue = cssStyle.split(':'); let value = propertyAndValue[1]; value = isNaN(value) ? value : Number(value); cssObj[propertyAndValue[0]] = value; } return cssObj; } beforeAll(() => { videoContainer = /** @type {!HTMLElement} */ (document.createElement('div')); videoContainer.style.height = `${videoContainerHeight}px`; document.body.appendChild(videoContainer); video = new shaka.test.FakeVideo(); player = { getMediaElement: () => video, getVideoContainer: () => videoContainer, }; }); beforeEach(() => { video.currentTime = 0; /** @suppress {checkTypes} */ textDisplayer = new shaka.text.UITextDisplayer(player); }); afterEach(async () => { await textDisplayer.destroy(); }); afterAll(() => { document.body.removeChild(videoContainer); }); /** * @suppress {visibility} * "suppress visibility" has function scope, so this is a mini-function that * exists solely to suppress visibility rules for these actions. */ function updateCaptions() { // Rather than wait for a timer, which can be unreliable on Safari when the // device is heavily loaded, trigger the update explicitly. textDisplayer.updateCaptions_(); } it('correctly displays styles for cues', () => { /** @type {!shaka.text.Cue} */ const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.'); cue.color = 'green'; cue.backgroundColor = 'black'; cue.direction = shaka.text.Cue.direction.HORIZONTAL_LEFT_TO_RIGHT; const cueSampleFontSize = '10px'; cue.fontSize = cueSampleFontSize; cue.fontWeight = shaka.text.Cue.fontWeight.NORMAL; cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL; cue.lineHeight = '2'; cue.nestedCues = []; cue.textAlign = shaka.text.Cue.textAlign.CENTER; cue.writingMode = shaka.text.Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM; textDisplayer.setTextVisibility(true); textDisplayer.append([cue]); updateCaptions(); const textContainer = videoContainer.querySelector('.shaka-text-container'); const captions = textContainer.querySelector('div'); const cssObj = parseCssText(captions.style.cssText); const expectCssObj = { 'color': 'green', 'direction': 'ltr', 'font-size': cueSampleFontSize, 'font-style': 'normal', 'font-weight': 400, 'text-align': 'center', }; // Either the prefixed or unprefixed version may be present. We will accept // either. Detecting which property the platform has may not work, because // Tizen 3, for example, has a writingMode property, but it is // non-functional. Instead of checking for which properties are on the // platform's style interface, check which properties are in the cssObj. // We expect one or the other to work on all supported platforms. if ('writing-mode' in cssObj) { expectCssObj['writing-mode'] = 'horizontal-tb'; } else { expectCssObj['-webkit-writing-mode'] = 'horizontal-tb'; } expect(cssObj).toEqual(jasmine.objectContaining(expectCssObj)); expect(parseCssText(textContainer.querySelector('span').style.cssText)) .toEqual(jasmine.objectContaining({'background-color': 'black'})); }); it('correctly displays styles for nested cues', () => { /** @type {!shaka.text.Cue} */ const cue = new shaka.text.Cue(0, 100, ''); const nestedCue = new shaka.text.Cue(0, 100, 'Captain\'s log.'); cue.nestedCues = [nestedCue]; nestedCue.textAlign = shaka.text.Cue.textAlign.CENTER; nestedCue.writingMode = shaka.text.Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM; nestedCue.color = 'green'; nestedCue.backgroundColor = 'black'; const cueSampleFontSize = '10px'; nestedCue.fontSize = cueSampleFontSize; nestedCue.fontWeight = shaka.text.Cue.fontWeight.NORMAL; nestedCue.fontStyle = shaka.text.Cue.fontStyle.NORMAL; nestedCue.lineHeight = '2'; nestedCue.nestedCues = []; textDisplayer.setTextVisibility(true); textDisplayer.append([cue]); updateCaptions(); // Verify styles applied to the nested cues. const textContainer = videoContainer.querySelector('.shaka-text-container'); const captions = textContainer.querySelector('span:not(.shaka-text-wrapper)'); const cssObj = parseCssText(captions.style.cssText); const expectCssObj = { 'color': 'green', 'font-size': cueSampleFontSize, 'font-style': 'normal', 'font-weight': 400, 'text-align': 'center', }; // Either the prefixed or unprefixed version may be present. We will accept // either. Detecting which property the platform has may not work, because // Tizen 3, for example, has a writingMode property, but it is // non-functional. Instead of checking for which properties are on the // platform's style interface, check which properties are in the cssObj. // We expect one or the other to work on all supported platforms. if ('writing-mode' in cssObj) { expectCssObj['writing-mode'] = 'horizontal-tb'; } else { expectCssObj['-webkit-writing-mode'] = 'horizontal-tb'; } expect(cssObj).toEqual(jasmine.objectContaining(expectCssObj)); expect(parseCssText(captions.querySelector('span').style.cssText)) .toEqual(jasmine.objectContaining({'background-color': 'black'})); }); it('correctly displays styles for cellResolution units', () => { /** @type {!shaka.text.Cue} */ const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.'); const fontSizeAsCellResolution = 0.80; cue.fontSize = `${fontSizeAsCellResolution}c`; cue.linePadding = '0.50c'; cue.cellResolution = { columns: 60, rows: 20, }; textDisplayer.setTextVisibility(true); textDisplayer.append([cue]); updateCaptions(); // Expected value is calculated based on ttp:cellResolution="60 20" // videoContainerHeight=450px and tts:fontSize="0.80c" on the default style. const calculatedFontSize = (450/20) * fontSizeAsCellResolution; const expectedFontSize = `${calculatedFontSize}px`; // Expected value is calculated based on ttp:cellResolution="60 20" // videoContainerHeight=450px and ebutts:linePadding="0.5c" on the default // style. const expectedLinePadding = '11.25px'; const textContainer = videoContainer.querySelector('.shaka-text-container'); const captions = textContainer.querySelector('div'); const cssObj = parseCssText(captions.style.cssText); expect(cssObj).toEqual( jasmine.objectContaining({ 'font-size': expectedFontSize, 'padding-left': expectedLinePadding, 'padding-right': expectedLinePadding, })); }); it('correctly displays styles for percentages units', () => { /** @type {!shaka.text.Cue} */ const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.'); const cueSampleFontSize = 90; cue.fontSize = `${cueSampleFontSize}%`; cue.cellResolution = { columns: 32, rows: 15, }; textDisplayer.setTextVisibility(true); textDisplayer.append([cue]); updateCaptions(); // Expected value is calculated based on ttp:cellResolution="32 15" // videoContainerHeight=450px and tts:fontSize="90%" on the default style. const calculatedFontSize = (450/15) * (cueSampleFontSize/100); const expectedFontSize = `${calculatedFontSize}px`; const textContainer = videoContainer.querySelector('.shaka-text-container'); const captions = textContainer.querySelector('div'); const cssObj = parseCssText(captions.style.cssText); expect(cssObj).toEqual( jasmine.objectContaining({'font-size': expectedFontSize})); }); it('does not display duplicate cues', () => { // These are identical. const cue1 = new shaka.text.Cue(0, 100, 'Captain\'s log.'); const cue2 = new shaka.text.Cue(0, 100, 'Captain\'s log.'); textDisplayer.setTextVisibility(true); textDisplayer.append([cue1]); updateCaptions(); /** @type {Element} */ const textContainer = videoContainer.querySelector('.shaka-text-container'); let captions = textContainer.querySelectorAll('div'); // Expect textContainer to display this cue. expect(captions.length).toBe(1); textDisplayer.append([cue2]); updateCaptions(); captions = textContainer.querySelectorAll('div'); // Expect textContainer to display one cue without duplication. expect(captions.length).toBe(1); }); it('does not mistake cues with nested cues as duplicates', () => { // These are not identical, but might look like it at the top level. const cue1 = new shaka.text.Cue(0, 100, ''); cue1.nestedCues = [ new shaka.text.Cue(0, 100, 'Nested cue 1.'), ]; const cue2 = new shaka.text.Cue(0, 100, ''); cue2.nestedCues = [ new shaka.text.Cue(0, 100, 'Nested cue 2.'), ]; const cue3 = new shaka.text.Cue(0, 100, ''); cue3.nestedCues = [ new shaka.text.Cue(0, 100, 'Nested cue 3.'), ]; textDisplayer.setTextVisibility(true); textDisplayer.append([cue1]); updateCaptions(); /** @type {Element} */ const textContainer = videoContainer.querySelector('.shaka-text-container'); let captions = textContainer.querySelectorAll('div'); // Expect textContainer to display this cue. expect(captions.length).toBe(1); textDisplayer.append([cue2, cue3]); updateCaptions(); captions = textContainer.querySelectorAll('div'); // Expect textContainer to display all three cues, since they are not truly // duplicates. expect(captions.length).toBe(3); }); it('does not mistake cues with different styles duplicates', () => { // These all have the same text and timing, but different styles. const cue1 = new shaka.text.Cue(0, 100, 'Hello!'); cue1.color = 'green'; const cue2 = new shaka.text.Cue(0, 100, 'Hello!'); cue2.color = 'green'; cue2.fontStyle = shaka.text.Cue.fontStyle.ITALIC; const cue3 = new shaka.text.Cue(0, 100, 'Hello!'); cue3.color = 'blue'; textDisplayer.setTextVisibility(true); textDisplayer.append([cue1]); updateCaptions(); /** @type {Element} */ const textContainer = videoContainer.querySelector('.shaka-text-container'); let captions = textContainer.querySelectorAll('div'); // Expect textContainer to display this cue. expect(captions.length).toBe(1); textDisplayer.append([cue2, cue3]); updateCaptions(); captions = textContainer.querySelectorAll('div'); // Expect textContainer to display all three cues, since they are not truly // duplicates. expect(captions.length).toBe(3); }); it('hides currently displayed cue when removed', () => { const cue = new shaka.text.Cue(0, 50, 'One'); textDisplayer.setTextVisibility(true); textDisplayer.append([cue]); video.currentTime = 10; updateCaptions(); const textContainer = videoContainer.querySelector('.shaka-text-container'); let cueElements = textContainer.querySelectorAll('div'); expect(cueElements.length).toBe(1); expect(cueElements[0].textContent).toBe('One'); textDisplayer.remove(0, 100); cueElements = textContainer.querySelectorAll('div'); expect(cueElements.length).toBe(0); }); it('hides and shows nested cues at appropriate times', () => { const parentCue1 = new shaka.text.Cue(0, 100, ''); const cue1 = new shaka.text.Cue(0, 50, 'One'); const cue2 = new shaka.text.Cue(25, 75, 'Two'); const cue3 = new shaka.text.Cue(50, 100, 'Three'); parentCue1.nestedCues = [cue1, cue2, cue3]; const parentCue2 = new shaka.text.Cue(90, 190, ''); const cue4 = new shaka.text.Cue(90, 130, 'Four'); parentCue2.nestedCues = [cue4]; textDisplayer.setTextVisibility(true); textDisplayer.append([parentCue1, parentCue2]); video.currentTime = 10; updateCaptions(); /** @type {Element} */ const textContainer = videoContainer.querySelector('.shaka-text-container'); let parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(1); expect(parentCueElements[0].textContent).toBe('One'); video.currentTime = 35; updateCaptions(); parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(1); expect(parentCueElements[0].textContent).toBe('OneTwo'); video.currentTime = 60; updateCaptions(); parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(1); expect(parentCueElements[0].textContent).toBe('TwoThree'); video.currentTime = 85; updateCaptions(); parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(1); expect(parentCueElements[0].textContent).toBe('Three'); video.currentTime = 95; updateCaptions(); parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(2); expect(parentCueElements[0].textContent).toBe('Three'); expect(parentCueElements[1].textContent).toBe('Four'); video.currentTime = 105; updateCaptions(); parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(1); expect(parentCueElements[0].textContent).toBe('Four'); video.currentTime = 150; updateCaptions(); parentCueElements = textContainer.querySelectorAll('div'); expect(parentCueElements.length).toBe(1); expect(parentCueElements[0].textContent).toBe(''); }); it('creates separate elements for cue regions', () => { const cueRegion = new shaka.text.CueRegion(); cueRegion.id = 'regionId'; cueRegion.height = 80; cueRegion.heightUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion.width = 80; cueRegion.widthUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion.viewportAnchorX = 10; cueRegion.viewportAnchorY = 10; cueRegion.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE; // These all attach to the same region, but only one region element should // be created. const cues = [ new shaka.text.Cue(0, 100, ''), new shaka.text.Cue(0, 200, ''), new shaka.text.Cue(0, 300, ''), ]; for (const cue of cues) { cue.displayAlign = shaka.text.Cue.displayAlign.CENTER; cue.region = cueRegion; } textDisplayer.setTextVisibility(true); textDisplayer.append(cues); updateCaptions(); const textContainer = videoContainer.querySelector('.shaka-text-container'); const allRegionElements = textContainer.querySelectorAll( '.shaka-text-region'); // Verify that the nested cues are all attached to a single region element. expect(allRegionElements.length).toBe(1); const regionElement = allRegionElements[0]; const children = Array.from(regionElement.childNodes).filter( (e) => e.nodeType == Node.ELEMENT_NODE); expect(children.length).toBe(3); // Verify styles applied to the region element. const regionCssObj = parseCssText(regionElement.style.cssText); const expectRegionCssObj = { 'position': 'absolute', 'height': '80%', 'width': '80%', 'top': '10%', 'left': '10%', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'justify-content': 'center', }; expect(regionCssObj).toEqual(jasmine.objectContaining(expectRegionCssObj)); for (const caption of children) { // Verify that styles applied to the nested cues _DO NOT_ include region // placement. const cueCssObj = parseCssText(caption.style.cssText); expect(Object.keys(cueCssObj)).not.toContain('height'); expect(Object.keys(cueCssObj)).not.toContain('width'); expect(Object.keys(cueCssObj)).not.toContain('top'); expect(Object.keys(cueCssObj)).not.toContain('left'); } }); it('does not lose second item in a region', () => { const cueRegion = new shaka.text.CueRegion(); cueRegion.id = 'regionId'; cueRegion.height = 80; cueRegion.heightUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion.width = 80; cueRegion.widthUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion.viewportAnchorX = 10; cueRegion.viewportAnchorY = 10; cueRegion.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE; // These have identical nested. const cue1 = new shaka.text.Cue(168, 181.84, ''); cue1.nestedCues = [ new shaka.text.Cue(168, 181.84, ''), ]; cue1.region = cueRegion; const nested1 = new shaka.text.Cue(168, 170.92, ''); nested1.nestedCues = [new shaka.text.Cue(0, 170.92, 'Emo look. I mean listen.')]; const nested2 = new shaka.text.Cue(172, 174.84, ''); nested2.nestedCues = [new shaka.text.Cue(172, 174.84, 'You have to learn to listen.')]; const nested3 = new shaka.text.Cue(175.84, 177.64, ''); nested3.nestedCues = [new shaka.text.Cue(175.84, 177.64, 'This is not some game.')]; const nested4 = new shaka.text.Cue(177.68, 181.84, ''); nested4.nestedCues = [new shaka.text.Cue(177.68, 181.84, 'You - I mean we - we could easily die out here.')]; cue1.nestedCues[0].nestedCues = [nested1, nested2, nested3, nested4]; video.currentTime = 170; textDisplayer.setTextVisibility(true); textDisplayer.append([cue1]); updateCaptions(); /** @type {Element} */ const textContainer = videoContainer.querySelector('.shaka-text-container'); let captions = textContainer.querySelectorAll('div'); expect(captions.length).toBe(1); let allRegionElements = textContainer.querySelectorAll( '.shaka-text-region'); // Verify that the nested cues are all attached to a single region element. expect(allRegionElements.length).toBe(1); // Advance time to where there is none to show video.currentTime = 171; updateCaptions(); allRegionElements = textContainer.querySelectorAll( '.shaka-text-region'); expect(allRegionElements.length).toBe(1); // Advance time to where there is something to show video.currentTime = 173; updateCaptions(); allRegionElements = textContainer.querySelectorAll( '.shaka-text-region'); expect(allRegionElements.length).toBe(1); captions = textContainer.querySelectorAll('div'); expect(captions.length).toBe(1); expect(captions[0].textContent).toBe('You have to learn to listen.'); allRegionElements = textContainer.querySelectorAll( '.shaka-text-region'); expect(allRegionElements.length).toBe(1); }); it('creates separate regions when dimensions differ but id same', () => { const identicalRegionId = 'regionId'; const cueRegion1 = new shaka.text.CueRegion(); const cueRegion2 = new shaka.text.CueRegion(); cueRegion1.id = identicalRegionId; cueRegion2.id = identicalRegionId; cueRegion1.height = 80; cueRegion1.heightUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion1.width = 80; cueRegion1.widthUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion2.height = 160; // the only difference! cueRegion2.heightUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion2.width = 80; cueRegion2.widthUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion1.viewportAnchorX = 10; cueRegion1.viewportAnchorY = 10; cueRegion1.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE; cueRegion2.viewportAnchorX = 10; cueRegion2.viewportAnchorY = 10; cueRegion2.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE; // These all attach to the same region, but only one region element should // be created. const firstBatchOfCues = [ new shaka.text.Cue(0, 100, ''), new shaka.text.Cue(0, 200, ''), new shaka.text.Cue(0, 300, ''), ]; for (const cue of firstBatchOfCues) { cue.displayAlign = shaka.text.Cue.displayAlign.CENTER; cue.region = cueRegion1; } // Another batch for the other region const secondBatchOfCues = [ new shaka.text.Cue(0, 100, ''), new shaka.text.Cue(0, 200, ''), new shaka.text.Cue(0, 300, ''), ]; for (const cue of secondBatchOfCues) { cue.displayAlign = shaka.text.Cue.displayAlign.CENTER; cue.region = cueRegion2; } textDisplayer.setTextVisibility(true); textDisplayer.append(firstBatchOfCues); textDisplayer.append(secondBatchOfCues); updateCaptions(); const textContainer = videoContainer.querySelector('.shaka-text-container'); const allRegionElements = textContainer.querySelectorAll( '.shaka-text-region'); // Verify that the nested cues are attached to respective region element. expect(allRegionElements.length).toBe(2); const childrenOfOne = Array.from(allRegionElements[0].childNodes).filter( (e) => e.nodeType == Node.ELEMENT_NODE); expect(childrenOfOne.length).toBe(3); const childrenOfTwo = Array.from(allRegionElements[1].childNodes).filter( (e) => e.nodeType == Node.ELEMENT_NODE); expect(childrenOfTwo.length).toBe(3); }); it('textDisplayer does not crash if destroy is called more than once', () => { textDisplayer.setTextVisibility(true); expect(videoContainer.childNodes.length).toBe(1); textDisplayer.destroy(); textDisplayer.destroy(); expect(videoContainer.childNodes.length).toBe(0); }); it('previews text styles through the normal renderer', () => { const cue = new shaka.text.Cue(0, 100, 'Previewed cue'); const config = shaka.util.PlayerConfiguration.createDefault().textDisplayer; textDisplayer.configure(config); textDisplayer.setTextVisibility(true); textDisplayer.append([cue]); updateCaptions(); const textContainer = videoContainer.querySelector('.shaka-text-container'); let cueElement = textContainer.querySelector('div'); expect(cueElement.textContent).toBe('Previewed cue'); expect(cueElement.style.fontSize).toBe(''); const previewConfig = /** @type {!shaka.extern.TextDisplayerConfiguration} */( Object.assign({}, config, {'fontScaleFactor': 2})); textDisplayer.setTextStylePreview( previewConfig, 'Subtitles example'); cueElement = textContainer.querySelector('div'); expect(cueElement.textContent).toBe('Previewed cue'); expect(cueElement.style.fontSize).toBe('2em'); textDisplayer.clearTextStylePreview(); cueElement = textContainer.querySelector('div'); expect(cueElement.textContent).toBe('Previewed cue'); expect(cueElement.style.fontSize).toBe(''); }); it('shows example text only when no cue is active during preview', () => { const config = shaka.util.PlayerConfiguration.createDefault().textDisplayer; textDisplayer.configure(config); textDisplayer.setTextVisibility(true); textDisplayer.setTextStylePreview(config, 'Subtitles example'); const textContainer = videoContainer.querySelector('.shaka-text-container'); expect(textContainer.textContent).toBe('Subtitles example'); textDisplayer.clearTextStylePreview(); expect(textContainer.textContent).toBe(''); textDisplayer.setTextStylePreview(config, 'Subtitles example'); expect(textContainer.textContent).toBe('Subtitles example'); textDisplayer.append([new shaka.text.Cue(0, 100, 'Real subtitle')]); updateCaptions(); expect(textContainer.textContent).toBe('Real subtitle'); }); it('shows example text while normal text visibility is off', () => { const config = shaka.util.PlayerConfiguration.createDefault().textDisplayer; textDisplayer.configure(config); textDisplayer.setTextStylePreview(config, 'Subtitles example'); const textContainer = videoContainer.querySelector('.shaka-text-container'); expect(textContainer.textContent).toBe('Subtitles example'); textDisplayer.clearTextStylePreview(); expect(videoContainer.querySelector('.shaka-text-container')).toBe(null); }); it('replaces example text during repeated preview updates', () => { const config = shaka.util.PlayerConfiguration.createDefault().textDisplayer; textDisplayer.configure(config); textDisplayer.setTextVisibility(true); textDisplayer.setTextStylePreview(config, 'First example'); const textContainer = videoContainer.querySelector('.shaka-text-container'); let cueElements = textContainer.querySelectorAll('div'); expect(cueElements.length).toBe(1); expect(textContainer.textContent).toBe('First example'); textDisplayer.setTextStylePreview(config, 'Second example'); cueElements = textContainer.querySelectorAll('div'); expect(cueElements.length).toBe(1); expect(textContainer.textContent).toBe('Second example'); }); it('positions cue at top-left when positionArea=TOP_LEFT', () => { /** @type {!shaka.text.Cue} */ const cue = new shaka.text.Cue(0, 100, 'Top-Left'); textDisplayer.setTextVisibility(true); const config = shaka.util.PlayerConfiguration.createDefault().textDisplayer; config.positionArea = shaka.config.PositionArea.TOP_LEFT; textDisplayer.configure(config); textDisplayer.append([cue]); updateCaptions(); /** @type {Element} */ const textContainer = videoContainer.querySelector('.shaka-text-container'); // Top-level cue should be a DIV const cueElement = textContainer.querySelector('div'); // The custom region (CustomRegion) should be created and wrap the cue const regionElement = textContainer.querySelector('.shaka-text-region'); // --- Region validations (CustomRegion: 90% x 90% at top/left 5%) --- const regionCss = parseCssText(regionElement.style.cssText); expect(regionCss).toEqual(jasmine.objectContaining({ 'position': 'absolute', 'height': '90%', 'width': '90%', 'top': '5%', 'left': '5%', 'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', // displayAlign BEFORE => justifyContent 'flex-start' 'justify-content': 'flex-start', })); // --- Cue validations (LEFT + BEFORE) --- const cueCss = parseCssText(cueElement.style.cssText); expect(cueCss).toEqual(jasmine.objectContaining({ 'display': 'flex', 'flex-direction': 'column', // textAlign LEFT => alignItems 'flex-start' and width 100% 'align-items': 'flex-start', 'width': '100%', // displayAlign BEFORE => justifyContent 'flex-start' 'justify-content': 'flex-start', 'text-align': 'left', })); // Text content should be present expect(cueElement.textContent).toBe('Top-Left'); }); });