diff --git a/package-lock.json b/package-lock.json index f2d7a61d2b..be2f134c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3240,7 +3240,7 @@ "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", "dev": true, "requires": { "traverse": ">=0.3.0 <0.4" @@ -6041,7 +6041,7 @@ "fn-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", - "integrity": "sha512-oIDB1rXf3BUnn00bh2jVM0byuqr94rBh6g7ZfdKcbmp1we2GQtPzKdloyvBXHs+q3fvxB8EqX5ecFba3RwCSjA==", + "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=", "dev": true }, "follow-redirects": { @@ -10193,7 +10193,7 @@ "npm-prefix": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/npm-prefix/-/npm-prefix-1.2.0.tgz", - "integrity": "sha512-EkGZ7jtA2onsULFpnZ/P5S0DPy8w9qH1TVytPhY54s+dmtLXBmp1evt8W9nfg5JEay24K3bX9WWTIHR8WQcOJA==", + "integrity": "sha1-5hlFX3B0ulTMZtbQ033Z8b5ry8A=", "dev": true, "requires": { "rc": "^1.1.0", @@ -10525,7 +10525,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, "os-tmpdir": { @@ -12820,7 +12820,7 @@ "shellsubstitute": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shellsubstitute/-/shellsubstitute-1.2.0.tgz", - "integrity": "sha512-CI1ViFC5a3ub86aaBmBVQ7kqg8eFypZLgBh+Bmq+ehHy9g7vu9kqCj5hS82cPzLwfdJRgiPB2hNHnd6oetiakQ==", + "integrity": "sha1-5PcCpQxRiw9v6YRRiQ1wWvKba3A=", "dev": true }, "showdown": { @@ -13932,7 +13932,7 @@ "trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", "dev": true }, "trim-newlines": { @@ -14240,7 +14240,7 @@ "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { "remove-trailing-separator": "^1.0.1" @@ -14275,7 +14275,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { "is-extendable": "^0.1.0" @@ -14306,7 +14306,7 @@ "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -14318,7 +14318,7 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { "is-extendable": "^0.1.0" @@ -14340,7 +14340,7 @@ "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { "is-glob": "^3.1.0", @@ -14350,7 +14350,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -14361,7 +14361,7 @@ "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, "requires": { "binary-extensions": "^1.0.0" @@ -14376,7 +14376,7 @@ "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { "kind-of": "^3.0.2" @@ -14385,7 +14385,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { "is-buffer": "^1.1.5" @@ -14443,7 +14443,7 @@ "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "requires": { "is-number": "^3.0.0", @@ -14497,7 +14497,7 @@ "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { "error-ex": "^1.3.1", @@ -14667,7 +14667,7 @@ "untildify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", + "integrity": "sha1-F+soB5h/dpUunASF/DEdBqgmouA=", "dev": true, "requires": { "os-homedir": "^1.0.0" @@ -15032,7 +15032,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, "string-width": { @@ -15048,7 +15048,7 @@ "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { "ansi-regex": "^3.0.0" diff --git a/src/js/control-bar/progress-control/play-progress-bar.js b/src/js/control-bar/progress-control/play-progress-bar.js index 479790815f..cc22e99595 100644 --- a/src/js/control-bar/progress-control/play-progress-bar.js +++ b/src/js/control-bar/progress-control/play-progress-bar.js @@ -56,17 +56,25 @@ class PlayProgressBar extends Component { * @param {number} seekBarPoint * A number from 0 to 1, representing a horizontal reference point * from the left edge of the {@link SeekBar} + * + * @param {Event} [event] + * The `timeupdate` event that caused this function to run. */ - update(seekBarRect, seekBarPoint) { + update(seekBarRect, seekBarPoint, event) { const timeTooltip = this.getChild('timeTooltip'); if (!timeTooltip) { return; } - const time = (this.player_.scrubbing()) ? - this.player_.getCache().currentTime : - this.player_.currentTime(); + // Combined logic: if an event with a valid pendingSeekTime getter exists, use it. + const time = (event && + event.target && + typeof event.target.pendingSeekTime === 'function') ? + event.target.pendingSeekTime() : + (this.player_.scrubbing() ? + this.player_.getCache().currentTime : + this.player_.currentTime()); timeTooltip.updateTime(seekBarRect, seekBarPoint, time); } diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js index aae7f003e9..9a70d163e5 100644 --- a/src/js/control-bar/progress-control/seek-bar.js +++ b/src/js/control-bar/progress-control/seek-bar.js @@ -44,16 +44,16 @@ class SeekBar extends Slider { // Avoid mutating the prototype's `children` array by creating a copy options.children = [...options.children]; - const shouldDisableSeekWhileScrubbingOnMobile = player.options_.disableSeekWhileScrubbingOnMobile && (IS_IOS || IS_ANDROID); + const shouldDisableSeekWhileScrubbing = (player.options_.disableSeekWhileScrubbingOnMobile && (IS_IOS || IS_ANDROID)) || (player.options_.disableSeekWhileScrubbingOnSTV); // Add the TimeTooltip as a child if we are on desktop, or on mobile with `disableSeekWhileScrubbingOnMobile: true` - if ((!IS_IOS && !IS_ANDROID) || shouldDisableSeekWhileScrubbingOnMobile) { + if ((!IS_IOS && !IS_ANDROID) || shouldDisableSeekWhileScrubbing) { options.children.splice(1, 0, 'mouseTimeDisplay'); } super(player, options); - this.shouldDisableSeekWhileScrubbingOnMobile_ = shouldDisableSeekWhileScrubbingOnMobile; + this.shouldDisableSeekWhileScrubbing_ = shouldDisableSeekWhileScrubbing; this.pendingSeekTime_ = null; this.setEventHandlers_(); @@ -196,7 +196,7 @@ class SeekBar extends Slider { // update the progress bar time tooltip with the current time if (this.bar) { - this.bar.update(Dom.getBoundingClientRect(this.el()), this.getProgress()); + this.bar.update(Dom.getBoundingClientRect(this.el()), this.getProgress(), event); } }); @@ -233,6 +233,20 @@ class SeekBar extends Slider { this.player_.currentTime(); } + /** + * Getter and setter for pendingSeekTime. + * Acts as a setter if a value is provided, or as a getter if no argument is given. + * + * @param {number|null} [time] - Optional. The new pending seek time, can be a number or null. + * @return {number|null} - The current pending seek time. + */ + pendingSeekTime(time) { + if (time !== undefined) { + this.pendingSeekTime_ = time; + } + return this.pendingSeekTime_; + } + /** * Get the percentage of media played so far. * @@ -242,8 +256,8 @@ class SeekBar extends Slider { getPercent() { // If we have a pending seek time, we are scrubbing on mobile and should set the slider percent // to reflect the current scrub location. - if (this.pendingSeekTime_) { - return this.pendingSeekTime_ / this.player_.duration(); + if (this.pendingSeekTime() !== null) { + return this.pendingSeekTime() / this.player_.duration(); } const currentTime = this.getCurrentTime_(); @@ -284,7 +298,7 @@ class SeekBar extends Slider { // Don't pause if we are on mobile and `disableSeekWhileScrubbingOnMobile: true`. // In that case, playback should continue while the player scrubs to a new location. - if (!this.shouldDisableSeekWhileScrubbingOnMobile_) { + if (!this.shouldDisableSeekWhileScrubbing_) { this.player_.pause(); } @@ -351,8 +365,8 @@ class SeekBar extends Slider { } // if on mobile and `disableSeekWhileScrubbingOnMobile: true`, keep track of the desired seek point but we won't initiate the seek until 'touchend' - if (this.shouldDisableSeekWhileScrubbingOnMobile_) { - this.pendingSeekTime_ = newTime; + if (this.shouldDisableSeekWhileScrubbing_) { + this.pendingSeekTime(newTime); } else { this.userSeek_(newTime); } @@ -402,10 +416,10 @@ class SeekBar extends Slider { this.player_.scrubbing(false); // If we have a pending seek time, then we have finished scrubbing on mobile and should initiate a seek. - if (this.pendingSeekTime_) { - this.userSeek_(this.pendingSeekTime_); + if (this.pendingSeekTime() !== null) { + this.userSeek_(this.pendingSeekTime()); - this.pendingSeekTime_ = null; + this.pendingSeekTime(null); } /** @@ -429,14 +443,40 @@ class SeekBar extends Slider { * Move more quickly fast forward for keyboard-only users */ stepForward() { - this.userSeek_(this.player_.currentTime() + this.options().stepSeconds); + // if `disableSeekWhileScrubbingOnSTV: true`, keep track of the desired seek point but we won't initiate the seek + if (this.shouldDisableSeekWhileScrubbing_) { + if (!this.player_.paused()) { + this.player_.pause(); + } + const currentPos = this.pendingSeekTime() !== null ? + this.pendingSeekTime() : + this.player_.currentTime(); + + this.pendingSeekTime(currentPos + this.options().stepSeconds); + this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); + } else { + this.userSeek_(this.player_.currentTime() + this.options().stepSeconds); + } } /** * Move more quickly rewind for keyboard-only users */ stepBack() { - this.userSeek_(this.player_.currentTime() - this.options().stepSeconds); + // if `disableSeekWhileScrubbingOnSTV: true`, keep track of the desired seek point but we won't initiate the seek + if (this.shouldDisableSeekWhileScrubbing_) { + if (!this.player_.paused()) { + this.player_.pause(); + } + const currentPos = this.pendingSeekTime() !== null ? + this.pendingSeekTime() : + this.player_.currentTime(); + + this.pendingSeekTime(currentPos - this.options().stepSeconds); + this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); + } else { + this.userSeek_(this.player_.currentTime() - this.options().stepSeconds); + } } /** @@ -448,6 +488,10 @@ class SeekBar extends Slider { * */ handleAction(event) { + if (this.pendingSeekTime() !== null) { + this.userSeek_(this.pendingSeekTime()); + this.pendingSeekTime(null); + } if (this.player_.paused()) { this.player_.play(); } else { diff --git a/src/js/control-bar/time-controls/current-time-display.js b/src/js/control-bar/time-controls/current-time-display.js index 34bfa3868b..f252b6311a 100644 --- a/src/js/control-bar/time-controls/current-time-display.js +++ b/src/js/control-bar/time-controls/current-time-display.js @@ -35,6 +35,8 @@ class CurrentTimeDisplay extends TimeDisplay { if (this.player_.ended()) { time = this.player_.duration(); + } else if (event && event.target && typeof event.target.pendingSeekTime === 'function') { + time = event.target.pendingSeekTime(); } else { time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); } diff --git a/src/js/player.js b/src/js/player.js index 0345f93e5a..6b2f424a74 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -5577,7 +5577,8 @@ Player.prototype.options_ = { }, // Default smooth seeking to false enableSmoothSeeking: false, - disableSeekWhileScrubbingOnMobile: false + disableSeekWhileScrubbingOnMobile: false, + disableSeekWhileScrubbingOnSTV: false }; TECH_EVENTS_RETRIGGER.forEach(function(event) { diff --git a/src/js/slider/slider.js b/src/js/slider/slider.js index 5a17435b49..8762863517 100644 --- a/src/js/slider/slider.js +++ b/src/js/slider/slider.js @@ -325,6 +325,10 @@ class Slider extends Component { event.stopPropagation(); this.stepForward(); } else { + if (this.pendingSeekTime()) { + this.pendingSeekTime(null); + this.userSeek_(this.player_.currentTime()); + } super.handleKeyDown(event); } diff --git a/test/unit/controls.test.js b/test/unit/controls.test.js index 1019350940..e11d5409de 100644 --- a/test/unit/controls.test.js +++ b/test/unit/controls.test.js @@ -320,11 +320,11 @@ QUnit.test('Seek bar percent should represent scrub location if we are scrubbing const seekBar = player.controlBar.progressControl.seekBar; player.duration(100); - seekBar.pendingSeekTime_ = 20; + seekBar.pendingSeekTime(20); assert.equal(seekBar.getPercent(), 0.2, 'seek bar percent set correctly to pending seek time'); - seekBar.pendingSeekTime_ = 50; + seekBar.pendingSeekTime(50); assert.equal(seekBar.getPercent(), 0.5, 'seek bar percent set correctly to next pending seek time'); }); @@ -680,3 +680,114 @@ QUnit.test('Remaing time negative sign can be optional', function(assert) { rtd2.dispose(); player.dispose(); }); + +QUnit.module('SmartTV UI Updates (Progress Bar & Time Display)', function(hooks) { + let player; + let seekBar; + let currentTimeDisplay; + + hooks.beforeEach(function() { + player = TestHelpers.makePlayer({ + spatialNavigation: { enabled: true }, + disableSeekWhileScrubbingOnSTV: true, + controlBar: { + progressControl: { + seekBar: { + stepSeconds: 5 + } + } + } + }); + + seekBar = player.controlBar.progressControl.seekBar; + currentTimeDisplay = player.controlBar.getChild('currentTimeDisplay'); + + player.duration(100); + }); + + hooks.afterEach(function() { + player.dispose(); + }); + + QUnit.test('Step forward updates seek bar progress and current-time display', function(assert) { + player.currentTime(40); + seekBar.stepForward(); + + assert.equal( + seekBar.pendingSeekTime(), + 45, + 'pendingSeekTime should be 45 (40 + 5) after stepForward' + ); + + assert.equal( + seekBar.getPercent(), + 0.45, + 'Seek bar progress should reflect 45% progress after stepForward' + ); + + assert.equal( + currentTimeDisplay.formattedTime_, + '0:45', + 'Current-time-display should update to 45s after stepForward' + ); + }); + + QUnit.test('Step back updates seek bar progress and current-time display', function(assert) { + player.currentTime(40); + seekBar.stepBack(); + + assert.equal( + seekBar.pendingSeekTime(), + 35, + 'pendingSeekTime should be 35 (40 - 5) after stepBack' + ); + + assert.equal( + seekBar.getPercent(), + 0.35, + 'Seek bar progress should reflect 35% progress after stepBack' + ); + + assert.equal( + currentTimeDisplay.formattedTime_, + '0:35', + 'Current-time-display should update to 35s after stepBack' + ); + }); + + QUnit.test('Pressing enter finalizes the seek and updates UI', function(assert) { + player.currentTime(40); + seekBar.stepForward(); + + seekBar.handleAction(); + + assert.equal( + seekBar.pendingSeekTime(), + null, + 'pendingSeekTime should be reset to null after seeking' + ); + + assert.equal( + seekBar.getPercent(), + 0.45, + 'Seek bar progress should remain at 45% after seeking' + ); + + assert.equal( + currentTimeDisplay.formattedTime_, + '0:45', + 'Current-time-display should remain at 45s after seeking' + ); + }); + + QUnit.test('Resets pendingSeekTime when SmartTV focus moves away without confirmation', function(assert) { + const userSeekSpy = sinon.spy(seekBar, 'userSeek_'); + + seekBar.trigger({ type: 'keydown', key: 'ArrowUp' }); + assert.ok(seekBar.pendingSeekTime() !== null, 'pendingSeekTime should be set after ArrowUp keydown'); + seekBar.trigger({ type: 'keydown', key: 'ArrowLeft' }); + assert.equal(seekBar.pendingSeekTime(), null, 'pendingSeekTime should be reset when SeekBar loses focus'); + assert.ok(userSeekSpy.calledWith(player.currentTime()), 'userSeek_ should be called with current player time'); + userSeekSpy.restore(); + }); +}); diff --git a/test/unit/player.test.js b/test/unit/player.test.js index 9895a9d4ce..7e8aa7d05a 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -16,6 +16,7 @@ import * as middleware from '../../src/js/tech/middleware.js'; import * as Events from '../../src/js/utils/events.js'; import pkg from '../../package.json'; import * as Guid from '../../src/js/utils/guid.js'; +import SeekBar from '../../src/js/control-bar/progress-control/seek-bar'; QUnit.module('Player', { beforeEach() { @@ -3731,7 +3732,7 @@ QUnit.test('Seek should occur when scrubbing completes on mobile when disableSee // Simulate a source loaded player.duration(10); - seekBar.pendingSeekTime_ = targetSeekTime; + seekBar.pendingSeekTime(targetSeekTime); // Simulate scrubbing completion seekBar.handleMouseUp(); @@ -3848,3 +3849,109 @@ QUnit.test('removeSourceElement returns false if no tech', function(assert) { assert.notOk(removed, 'Returned false'); player.dispose(); }); + +QUnit.module('SmartTV Seek Logic', function(hooks) { + let player; + let seekBar; + + hooks.beforeEach(function() { + player = TestHelpers.makePlayer({ + disableSeekWhileScrubbingOnSTV: true, + controlBar: { + progressControl: { + seekBar: { + stepSeconds: 5 + } + } + } + }); + + seekBar = player.controlBar.progressControl.seekBar; + player.duration(100); + }); + + hooks.afterEach(function() { + player.dispose(); + }); + + QUnit.test('Step forward updates pendingSeekTime but does not seek immediately', function(assert) { + player.currentTime(40); + seekBar.stepForward(); + + assert.equal( + seekBar.pendingSeekTime(), + 45, + 'pendingSeekTime should be 45 (40 + 5) after stepForward' + ); + + assert.equal( + player.currentTime(), + 40, + 'Player currentTime remains unchanged (no immediate seek)' + ); + }); + + QUnit.test('Step back updates pendingSeekTime but does not seek immediately', function(assert) { + player.currentTime(40); + seekBar.stepBack(); + + assert.equal( + seekBar.pendingSeekTime(), + 35, + 'pendingSeekTime should be 35 (40 - 5) after stepBack' + ); + + assert.equal( + player.currentTime(), + 40, + 'Player currentTime remains unchanged (no immediate seek)' + ); + }); + + QUnit.test('Pressing Enter seeks to pendingSeekTime and resets it', function(assert) { + seekBar.pendingSeekTime(50); + + const userSeekSpy = sinon.spy(seekBar, 'userSeek_'); + + seekBar.handleAction(); + + assert.ok( + userSeekSpy.calledWith(50), + 'Pressing Enter should trigger seek to pendingSeekTime (50)' + ); + + assert.equal( + seekBar.pendingSeekTime(), + null, + 'pendingSeekTime should be reset to null after seeking' + ); + + assert.equal( + player.currentTime(), + 50, + 'Player currentTime should be updated to 50 after pressing Enter' + ); + }); + + QUnit.test('Step forward/back seeks immediately when disableSeekWhileScrubbingOnSTV is false', function(assert) { + player.options_.disableSeekWhileScrubbingOnSTV = false; + seekBar = new SeekBar(player); + player.currentTime(40); + + seekBar.stepForward(); + + assert.equal( + player.currentTime(), + 45, + 'Player currentTime should update immediately when stepping forward' + ); + + seekBar.stepBack(); + + assert.equal( + player.currentTime(), + 40, + 'Player currentTime should update immediately when stepping back' + ); + }); +});