1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-04 10:34:51 +02:00

fix(liveui): tweaks to prevent jitter (#6405)

This commit is contained in:
Brandon Casey 2020-03-30 17:27:45 -04:00 committed by GitHub
parent 1dd06a26c0
commit 668c7f44d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 276 additions and 231 deletions

View File

@ -261,6 +261,11 @@ class SeekBar extends Slider {
newTime = newTime - 0.1;
}
} else {
if (distance >= 0.99) {
liveTracker.seekToLiveEdge();
return;
}
const seekableStart = liveTracker.seekableStart();
const seekableEnd = liveTracker.liveCurrentTime();

View File

@ -57,7 +57,7 @@ class SeekToLive extends Button {
* Update the state of this button if we are at the live edge
* or not
*/
updateLiveEdgeStatus(e) {
updateLiveEdgeStatus() {
// default to live edge
if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
this.setAttribute('aria-disabled', true);

View File

@ -1,19 +1,43 @@
import Component from './component.js';
import median from './utils/median.js';
import mergeOptions from './utils/merge-options.js';
import document from 'global/document';
import * as browser from './utils/browser.js';
import window from 'global/window';
import * as Fn from './utils/fn.js';
const defaults = {
// Number of seconds of live window (seekableEnd - seekableStart) that
// a video needs to have before the liveui will be shown.
trackingThreshold: 30
trackingThreshold: 30,
liveTolerance: 15
};
/* track when we are at the live edge, and other helpers for live playback */
/*
track when we are at the live edge, and other helpers for live playback */
/**
* A class for checking live current time and determining when the player
* is at or behind the live edge.
*/
class LiveTracker extends Component {
/**
* Creates an instance of this class.
*
* @param {Player} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param {number} [options.trackingThreshold=30]
* Number of seconds of live window (seekableEnd - seekableStart) that
* media needs to have before the liveui will be shown.
*
* @param {number} [options.liveTolerance=15]
* Number of seconds behind live that we have to be
* before we will be considered non-live. Note that this will only
* be used when playing at the live edge. This allows large seekable end
* changes to not effect wether we are live or not.
*/
constructor(player, options) {
// LiveTracker does not need an element
const options_ = mergeOptions(defaults, options, {createEl: false});
@ -32,6 +56,9 @@ class LiveTracker extends Component {
}
}
/**
* toggle tracking based on document visiblility
*/
handleVisibilityChange() {
if (this.player_.duration() !== Infinity) {
return;
@ -44,29 +71,11 @@ class LiveTracker extends Component {
}
}
isBehind_() {
// don't report that we are behind until a timeupdate has been seen
if (!this.timeupdateSeen_) {
return false;
}
const liveCurrentTime = this.liveCurrentTime();
const currentTime = this.player_.currentTime();
// the live edge window is the amount of seconds away from live
// that a player can be, but still be considered live.
// we add 0.07 because the live tracking happens every 30ms
// and we want some wiggle room for short segment live playback
const liveEdgeWindow = (this.seekableIncrement_ * 2) + 0.07;
// on Android liveCurrentTime can bee Infinity, because seekableEnd
// can be Infinity, so we handle that case.
return liveCurrentTime !== Infinity && (liveCurrentTime - liveEdgeWindow) >= currentTime;
}
// all the functionality for tracking when seek end changes
// and for tracking how far past seek end we should be
/**
* all the functionality for tracking when seek end changes
* and for tracking how far past seek end we should be
*/
trackLive_() {
this.pastSeekEnd_ = this.pastSeekEnd_;
const seekable = this.player_.seekable();
// skip undefined seekable
@ -74,37 +83,33 @@ class LiveTracker extends Component {
return;
}
const newSeekEnd = this.seekableEnd();
const newTime = Number(window.performance.now().toFixed(4));
const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
// we can only tell if we are behind live, when seekable changes
// once we detect that seekable has changed we check the new seek
// end against current time, with a fudge value of half a second.
if (newSeekEnd !== this.lastSeekEnd_) {
if (this.lastSeekEnd_) {
// we try to get the best fit value for the seeking increment
// variable from the last 12 values.
this.seekableIncrementList_ = this.seekableIncrementList_.slice(-11);
this.seekableIncrementList_.push(Math.abs(newSeekEnd - this.lastSeekEnd_));
if (this.seekableIncrementList_.length > 3) {
this.seekableIncrement_ = median(this.seekableIncrementList_);
}
}
this.lastTime_ = newTime;
this.pastSeekEnd_ = 0;
this.lastSeekEnd_ = newSeekEnd;
this.trigger('seekableendchange');
this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
const liveCurrentTime = this.liveCurrentTime();
const currentTime = this.player_.currentTime();
// we are behind live if any are true
// 1. the player is paused
// 2. the user seeked to a location 2 seconds away from live
// 3. the difference between live and current time is greater
// liveTolerance which defaults to 15s
let isBehind = this.player_.paused() || this.seekedBehindLive_ ||
Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
// we cannot be behind if
// 1. until we have not seen a timeupdate yet
// 2. liveCurrentTime is Infinity, which happens on Android
if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
isBehind = false;
}
// we should reset pastSeekEnd when the value
// is much higher than seeking increment.
if (this.pastSeekEnd() > this.seekableIncrement_ * 1.5) {
this.pastSeekEnd_ = 0;
} else {
this.pastSeekEnd_ = this.pastSeekEnd() + 0.03;
}
if (this.isBehind_() !== this.behindLiveEdge()) {
this.behindLiveEdge_ = this.isBehind_();
if (isBehind !== this.behindLiveEdge_) {
this.behindLiveEdge_ = isBehind;
this.trigger('liveedgechange');
}
}
@ -143,21 +148,41 @@ class LiveTracker extends Component {
this.trackingInterval_ = this.setInterval(this.trackLive_, Fn.UPDATE_REFRESH_INTERVAL);
this.trackLive_();
this.on(this.player_, 'play', this.trackLive_);
this.on(this.player_, 'pause', this.trackLive_);
this.on(this.player_, ['play', 'pause'], this.trackLive_);
// this is to prevent showing that we are not live
// before a video starts to play
if (!this.timeupdateSeen_) {
this.one(this.player_, 'play', this.handlePlay);
this.handleTimeupdate = () => {
this.timeupdateSeen_ = true;
this.handleTimeupdate = null;
};
this.one(this.player_, 'timeupdate', this.handleTimeupdate);
this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate);
} else {
this.on(this.player_, 'seeked', this.handleSeeked);
}
}
/**
* handle the first timeupdate on the player if it wasn't already playing
* when live tracker started tracking.
*/
handleFirstTimeupdate() {
this.timeupdateSeen_ = true;
this.on(this.player_, 'seeked', this.handleSeeked);
}
/**
* Keep track of what time a seek starts, and listen for seeked
* to find where a seek ends.
*/
handleSeeked() {
const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
this.seekedBehindLive_ = this.skipNextSeeked_ ? false : timeDiff > 2;
this.skipNextSeeked_ = false;
this.trackLive_();
}
/**
* handle the first play on the player, and make sure that we seek
* right to the live edge.
*/
handlePlay() {
this.one(this.player_, 'timeupdate', this.seekToLiveEdge);
}
@ -167,24 +192,22 @@ class LiveTracker extends Component {
* their initial value.
*/
reset_() {
this.lastTime_ = -1;
this.pastSeekEnd_ = 0;
this.lastSeekEnd_ = null;
this.behindLiveEdge_ = null;
this.lastSeekEnd_ = -1;
this.behindLiveEdge_ = true;
this.timeupdateSeen_ = false;
this.seekedBehindLive_ = false;
this.skipNextSeeked_ = false;
this.clearInterval(this.trackingInterval_);
this.trackingInterval_ = null;
this.seekableIncrement_ = 12;
this.seekableIncrementList_ = [];
this.off(this.player_, 'play', this.trackLive_);
this.off(this.player_, 'pause', this.trackLive_);
this.off(this.player_, ['play', 'pause'], this.trackLive_);
this.off(this.player_, 'seeked', this.handleSeeked);
this.off(this.player_, 'play', this.handlePlay);
this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate);
this.off(this.player_, 'timeupdate', this.seekToLiveEdge);
if (this.handleTimeupdate) {
this.off(this.player_, 'timeupdate', this.handleTimeupdate);
this.handleTimeupdate = null;
}
}
/**
@ -195,11 +218,15 @@ class LiveTracker extends Component {
return;
}
this.reset_();
this.trigger('liveedgechange');
}
/**
* A helper to get the player seekable end
* so that we don't have to null check everywhere
*
* @return {number}
* The furthest seekable end or Infinity.
*/
seekableEnd() {
const seekable = this.player_.seekable();
@ -218,6 +245,9 @@ class LiveTracker extends Component {
/**
* A helper to get the player seekable start
* so that we don't have to null check everywhere
*
* @return {number}
* The earliest seekable start or 0.
*/
seekableStart() {
const seekable = this.player_.seekable();
@ -234,7 +264,13 @@ class LiveTracker extends Component {
}
/**
* Get the live time window
* Get the live time window aka
* the amount of time between seekable start and
* live current time.
*
* @return {number}
* The amount of seconds that are seekable in
* the live video.
*/
liveWindow() {
const liveCurrentTime = this.liveCurrentTime();
@ -249,6 +285,9 @@ class LiveTracker extends Component {
/**
* Determines if the player is live, only checks if this component
* is tracking live playback or not
*
* @return {boolean}
* Wether liveTracker is tracking
*/
isLive() {
return this.isTracking();
@ -257,6 +296,9 @@ class LiveTracker extends Component {
/**
* Determines if currentTime is at the live edge and won't fall behind
* on each seekableendchange
*
* @return {boolean}
* Wether playback is at the live edge
*/
atLiveEdge() {
return !this.behindLiveEdge();
@ -264,26 +306,45 @@ class LiveTracker extends Component {
/**
* get what we expect the live current time to be
*
* @return {number}
* The expected live current time
*/
liveCurrentTime() {
return this.pastSeekEnd() + this.seekableEnd();
}
/**
* Returns how far past seek end we expect current time to be
* The number of seconds that have occured after seekable end
* changed. This will be reset to 0 once seekable end changes.
*
* @return {number}
* Seconds past the current seekable end
*/
pastSeekEnd() {
const seekableEnd = this.seekableEnd();
if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
this.pastSeekEnd_ = 0;
}
this.lastSeekEnd_ = seekableEnd;
return this.pastSeekEnd_;
}
/**
* If we are currently behind the live edge, aka currentTime will be
* behind on a seekableendchange
*
* @return {boolean}
* If we are behind the live edge
*/
behindLiveEdge() {
return this.behindLiveEdge_;
}
/**
* Wether live tracker is currently tracking or not.
*/
isTracking() {
return typeof this.trackingInterval_ === 'number';
}
@ -292,18 +353,21 @@ class LiveTracker extends Component {
* Seek to the live edge if we are behind the live edge
*/
seekToLiveEdge() {
this.seekedBehindLive_ = false;
if (this.atLiveEdge()) {
return;
}
// skipNextSeeked_
this.skipNextSeeked_ = true;
this.player_.currentTime(this.liveCurrentTime());
if (this.player_.paused()) {
this.player_.play();
}
}
/**
* Dispose of liveTracker
*/
dispose() {
this.off(document, 'visibilitychange', this.handleVisibilityChange);
this.stopTracking();
super.dispose();
}

View File

@ -1,17 +0,0 @@
/**
* Computes the median of an array.
*
* @param {number[]} arr
* Input array of numbers.
*
* @return {number}
* Median value.
*/
const median = arr => {
const mid = Math.floor(arr.length / 2);
const sortedList = [...arr].sort((a, b) => a - b);
return arr.length % 2 !== 0 ? sortedList[mid] : (sortedList[mid - 1] + sortedList[mid]) / 2;
};
export default median;

View File

@ -69,7 +69,7 @@ QUnit.module('LiveTracker', () => {
}
});
QUnit.test('starts/stop with durationchange and triggers liveedgechange', function(assert) {
QUnit.test('with durationchange and triggers liveedgechange', function(assert) {
let liveEdgeChange = 0;
this.liveTracker.on('liveedgechange', () => {
@ -83,10 +83,11 @@ QUnit.module('LiveTracker', () => {
this.player.duration(5);
assert.notOk(this.liveTracker.isTracking(), 'not started');
assert.equal(liveEdgeChange, 2, 'liveedgechange fired when we stop tracking');
this.player.duration(Infinity);
assert.ok(this.liveTracker.isTracking(), 'started');
assert.equal(liveEdgeChange, 2, 'liveedgechange fired again');
assert.equal(liveEdgeChange, 3, 'liveedgechange fired again');
});
QUnit.module('tracking', {
@ -97,14 +98,10 @@ QUnit.module('LiveTracker', () => {
this.liveTracker = this.player.liveTracker;
this.player.seekable = () => createTimeRanges(0, 30);
this.player.paused = () => false;
this.player.duration(Infinity);
this.liveEdgeChanges = 0;
this.seekableEndChanges = 0;
this.liveTracker.on('seekableendchange', () => {
this.seekableEndChanges++;
});
this.liveTracker.on('liveedgechange', () => {
this.liveEdgeChanges++;
@ -117,8 +114,7 @@ QUnit.module('LiveTracker', () => {
});
QUnit.test('Triggers liveedgechange when we fall behind and catch up', function(assert) {
this.liveTracker.seekableIncrement_ = 6;
this.liveTracker.options_.liveTolerance = 6;
this.player.seekable = () => createTimeRanges(0, 20);
this.player.trigger('timeupdate');
this.player.currentTime = () => 14;
@ -129,31 +125,63 @@ QUnit.module('LiveTracker', () => {
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
assert.ok(this.liveTracker.behindLiveEdge(), 'behind live edge');
this.player.currentTime = () => 20;
this.player.currentTime = () => this.liveTracker.liveCurrentTime();
this.clock.tick(30);
assert.equal(this.liveEdgeChanges, 2, 'should have two live edge change');
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
});
QUnit.test('is behindLiveEdge when paused', function(assert) {
this.liveTracker.options_.liveTolerance = 6;
this.player.seekable = () => createTimeRanges(0, 20);
this.player.trigger('timeupdate');
this.player.currentTime = () => 20;
this.clock.tick(1000);
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
this.player.paused = () => true;
this.player.trigger('pause');
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
assert.ok(this.liveTracker.behindLiveEdge(), 'behindLiveEdge live edge');
});
QUnit.test('is behindLiveEdge when seeking backwards', function(assert) {
this.liveTracker.options_.liveTolerance = 6;
this.player.seekable = () => createTimeRanges(0, 20);
this.player.trigger('timeupdate');
this.player.currentTime = () => 20;
this.clock.tick(1000);
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
this.player.currentTime = () => 17;
this.player.trigger('seeked');
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
assert.ok(this.liveTracker.behindLiveEdge(), 'behindLiveEdge live edge');
});
QUnit.test('pastSeekEnd should update when seekable changes', function(assert) {
assert.strictEqual(this.liveTracker.liveCurrentTime(), 30.03, 'liveCurrentTime is now 30');
assert.strictEqual(this.liveTracker.liveCurrentTime(), 30, 'liveCurrentTime is now 30');
this.clock.tick(2010);
assert.ok(this.liveTracker.pastSeekEnd() > 2, 'pastSeekEnd should be over 2s');
this.player.seekable = () => createTimeRanges(30, 61);
this.player.seekable = () => createTimeRanges(0, 2);
this.clock.tick(30);
assert.strictEqual(this.liveTracker.pastSeekEnd(), 0.03, 'pastSeekEnd start at 0.03 again');
assert.strictEqual(this.liveTracker.liveCurrentTime(), 61.03, 'liveCurrentTime is now 2.03');
assert.strictEqual(this.liveTracker.liveCurrentTime(), 2.03, 'liveCurrentTime is now 2.03');
});
QUnit.test('seeks to live edge on seekableendchange', function(assert) {
QUnit.test('can seek to live edge', function(assert) {
this.player.trigger('timeupdate');
this.player.seekable = () => createTimeRanges(0, 6);
this.liveTracker.seekableIncrement_ = 2;
this.liveTracker.options_.liveTolerance = 2;
let currentTime = 0;
this.player.currentTime = (ct) => {
@ -172,7 +200,7 @@ QUnit.module('LiveTracker', () => {
assert.equal(currentTime, this.liveTracker.liveCurrentTime(), 'should have seeked to liveCurrentTime');
});
QUnit.test('does not seek to to live edge if at live edge', function(assert) {
QUnit.test('does not seek to live edge if at live edge', function(assert) {
let pauseCalls = 0;
let playCalls = 0;
let currentTime = 0;
@ -196,13 +224,12 @@ QUnit.module('LiveTracker', () => {
assert.notOk(this.player.hasClass('vjs-waiting'), 'player should not be waiting');
assert.equal(pauseCalls, 0, 'should not have called pause');
this.clock.tick(2000);
this.clock.tick(2010);
assert.ok(this.liveTracker.pastSeekEnd() > 2, 'pastSeekEnd should be over 2s');
this.player.seekable = () => createTimeRanges(0, 2);
this.clock.tick(30);
assert.equal(this.seekableEndChanges, 1, 'should be one seek end change');
assert.equal(currentTime, 0, 'should not have seeked to seekableEnd');
assert.equal(playCalls, 0, 'should not have called play');
});
@ -215,7 +242,6 @@ QUnit.module('LiveTracker', () => {
assert.ok(this.liveTracker.seekToLiveEdge.notCalled, 'seekToLiveEdge was not called yet');
this.player.trigger('play');
this.player.trigger('playing');
assert.ok(this.liveTracker.seekToLiveEdge.notCalled, 'seekToLiveEdge was not called yet');
this.player.trigger('timeupdate');

View File

@ -4,98 +4,89 @@ import sinon from 'sinon';
import computedStyle from '../../src/js/utils/computed-style.js';
import { createTimeRange } from '../../src/js/utils/time-ranges.js';
QUnit.module('SeekToLive', () => {
QUnit.module('live with liveui', {
beforeEach() {
this.clock = sinon.useFakeTimers();
QUnit.module('SeekToLive', {
beforeEach() {
this.clock = sinon.useFakeTimers();
this.player = TestHelpers.makePlayer();
this.seekToLive = this.player.controlBar.seekToLive;
this.getComputedDisplay = () => {
return computedStyle(this.seekToLive.el(), 'display');
};
this.player = TestHelpers.makePlayer({liveui: true});
this.seekToLive = this.player.controlBar.seekToLive;
this.mockLiveui = () => {
this.player.paused = () => false;
this.player.hasStarted = () => true;
this.player.options_.liveui = true;
this.player.seekable = () => createTimeRange(0, 45);
this.getComputedDisplay = () => {
return computedStyle(this.seekToLive.el(), 'display');
};
// mock live state
this.player.currentTime = () => this.player.liveTracker.liveCurrentTime();
this.player.duration(Infinity);
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
QUnit.test('at live edge if liveTracker says we are', function(assert) {
this.player.liveTracker.behindLiveEdge = () => false;
this.player.liveTracker.trigger('liveedgechange');
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
});
QUnit.test('behind live edge if liveTracker says we are', function(assert) {
this.player.liveTracker.behindLiveEdge = () => true;
this.player.liveTracker.trigger('liveedgechange');
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have live edge class');
});
QUnit.test('switch to non live', function(assert) {
this.player.duration(4);
this.player.trigger('durationchange');
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
});
QUnit.module('live without liveui', {
beforeEach() {
this.clock = sinon.useFakeTimers();
this.player = TestHelpers.makePlayer();
this.seekToLive = this.player.controlBar.seekToLive;
this.player.seekable = () => createTimeRange(0, 45);
this.getComputedDisplay = () => {
return computedStyle(this.seekToLive.el(), 'display');
};
// mock live state
this.player.duration(Infinity);
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
QUnit.test('should be hidden', function(assert) {
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
});
QUnit.module('not live', {
beforeEach() {
this.player = TestHelpers.makePlayer({liveui: true});
this.seekToLive = this.player.controlBar.seekToLive;
this.getComputedDisplay = () => {
return computedStyle(this.seekToLive.el(), 'display');
};
},
afterEach() {
this.player.dispose();
}
});
QUnit.test('should not show or track', function(assert) {
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
});
QUnit.test('switch to live', function(assert) {
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
this.player.seekable = () => createTimeRange(0, 45);
this.player.duration(Infinity);
this.player.trigger('durationchange');
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
});
};
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
QUnit.test('liveui enabled, can switch between at and behind live edge ', function(assert) {
this.mockLiveui();
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
this.player.currentTime = () => 0;
this.player.seekable = () => createTimeRange(0, 38);
this.clock.tick(30);
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
});
QUnit.test('liveui enabled can show/hide on durationchange', function(assert) {
// start out non-live
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
// switch to live
this.mockLiveui();
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
// switch to non-live
this.player.duration(20);
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
// back to live again.
this.mockLiveui();
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
});
QUnit.test('liveui disabled live window is never shown', function(assert) {
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
this.player.paused = () => false;
this.player.hasStarted = () => true;
this.player.currentTime = () => this.player.liveTracker.liveCurrentTime();
// liveui false, seekable range is good though
this.player.options_.liveui = false;
this.player.duration(Infinity);
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
this.player.duration(10);
// liveui false
this.player.options_.liveui = false;
this.player.seekable = () => createTimeRange(0, 29);
this.player.duration(Infinity);
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
});

View File

@ -1,24 +0,0 @@
/* eslint-env qunit */
import median from '../../../src/js/utils/median.js';
QUnit.module('median');
QUnit.test('should compute the median', function(assert) {
let data;
let expected;
data = [2, 4, 5, 3, 8, 2];
expected = 3.5;
assert.equal(median(data), expected, 'median is correct for the first not sorted array');
data = [2, 4, 5, 3, 8, 2, 9];
expected = 4;
assert.equal(median(data), expected, 'median is correct for the second not sorted array');
data = [2, 2, 3, 4, 5, 8, 9];
expected = 4;
assert.equal(median(data), expected, 'median is correct for the sorted array');
});