summaryrefslogtreecommitdiff
path: root/javascript/videojs/test/unit/tracks/text-track.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/test/unit/tracks/text-track.test.js')
-rw-r--r--javascript/videojs/test/unit/tracks/text-track.test.js806
1 files changed, 806 insertions, 0 deletions
diff --git a/javascript/videojs/test/unit/tracks/text-track.test.js b/javascript/videojs/test/unit/tracks/text-track.test.js
new file mode 100644
index 0000000..b96122a
--- /dev/null
+++ b/javascript/videojs/test/unit/tracks/text-track.test.js
@@ -0,0 +1,806 @@
+/* eslint-env qunit */
+import window from 'global/window';
+import EventTarget from '../../../src/js/event-target.js';
+import TrackBaseline from './track-baseline';
+import TechFaker from '../tech/tech-faker';
+import TextTrack from '../../../src/js/tracks/text-track.js';
+import TestHelpers from '../test-helpers.js';
+import sinon from 'sinon';
+import log from '../../../src/js/utils/log.js';
+import XHR from '@videojs/xhr';
+
+QUnit.module('Text Track', {
+ beforeEach() {
+ this.tech = new TechFaker();
+ this.oldXMLHttpRequest = XHR.XMLHttpRequest;
+ this.oldXDomainRequest = XHR.XDomainRequest;
+ this.xhr = sinon.useFakeXMLHttpRequest();
+ XHR.XMLHttpRequest = this.xhr;
+ XHR.XDomainRequest = this.xhr;
+ },
+ afterEach() {
+ this.tech.dispose();
+ this.tech = null;
+ XHR.XMLHttpRequest = this.oldXMLHttpRequest;
+ XHR.XDomainRequest = this.oldXDomainRequest;
+ this.xhr.restore();
+ }
+});
+
+// do baseline track testing
+TrackBaseline(TextTrack, {
+ id: '1',
+ kind: 'subtitles',
+ mode: 'disabled',
+ label: 'English',
+ language: 'en'
+ // tech is added in baseline
+ // tech: new TechFaker()
+});
+
+QUnit.test('requires a tech', function(assert) {
+ const error = new Error('A tech was not provided.');
+
+ assert.throws(() => new TextTrack({}), error, 'a tech is required');
+ assert.throws(() => new TextTrack({tech: null}), error, 'a tech is required');
+});
+
+QUnit.test('can create a TextTrack with a mode property', function(assert) {
+ const mode = 'disabled';
+ const tt = new TextTrack({
+ mode,
+ tech: this.tech
+ });
+
+ assert.equal(tt.mode, mode, 'we have a mode');
+});
+
+QUnit.test('defaults when items not provided', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ assert.equal(tt.kind, 'subtitles', 'kind defaulted to subtitles');
+ assert.equal(tt.mode, 'disabled', 'mode defaulted to disabled');
+ assert.equal(tt.label, '', 'label defaults to empty string');
+ assert.equal(tt.language, '', 'language defaults to empty string');
+});
+
+QUnit.test('kind can only be one of several options, defaults to subtitles', function(assert) {
+ let tt = new TextTrack({
+ tech: this.tech,
+ kind: 'foo'
+ });
+
+ assert.equal(tt.kind, 'subtitles', 'the kind is set to subtitles, not foo');
+ assert.notEqual(tt.kind, 'foo', 'the kind is set to subtitles, not foo');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'subtitles'
+ });
+
+ assert.equal(tt.kind, 'subtitles', 'the kind is set to subtitles');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'captions'
+ });
+
+ assert.equal(tt.kind, 'captions', 'the kind is set to captions');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'descriptions'
+ });
+
+ assert.equal(tt.kind, 'descriptions', 'the kind is set to descriptions');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'chapters'
+ });
+
+ assert.equal(tt.kind, 'chapters', 'the kind is set to chapters');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ kind: 'metadata'
+ });
+
+ assert.equal(tt.kind, 'metadata', 'the kind is set to metadata');
+});
+
+QUnit.test('mode can only be one of several options, defaults to disabled', function(assert) {
+ let tt = new TextTrack({
+ tech: this.tech,
+ mode: 'foo'
+ });
+
+ assert.equal(tt.mode, 'disabled', 'the mode is set to disabled, not foo');
+ assert.notEqual(tt.mode, 'foo', 'the mode is set to disabld, not foo');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ mode: 'disabled'
+ });
+
+ assert.equal(tt.mode, 'disabled', 'the mode is set to disabled');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ mode: 'hidden'
+ });
+
+ assert.equal(tt.mode, 'hidden', 'the mode is set to hidden');
+
+ tt = new TextTrack({
+ tech: this.tech,
+ mode: 'showing'
+ });
+
+ assert.equal(tt.mode, 'showing', 'the mode is set to showing');
+});
+
+QUnit.test('cue and activeCues are read only', function(assert) {
+ const mode = 'disabled';
+ const tt = new TextTrack({
+ mode,
+ tech: this.tech
+ });
+
+ tt.cues = 'foo';
+ tt.activeCues = 'bar';
+
+ assert.notEqual(tt.cues, 'foo', 'cues is still original value');
+ assert.notEqual(tt.activeCues, 'bar', 'activeCues is still original value');
+});
+
+QUnit.test('mode can only be set to a few options', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ tt.mode = 'foo';
+
+ assert.notEqual(tt.mode, 'foo', 'the mode is still the old value, disabled');
+ assert.equal(tt.mode, 'disabled', 'still on the default mode, disabled');
+
+ tt.mode = 'hidden';
+ assert.equal(tt.mode, 'hidden', 'mode set to hidden');
+
+ tt.mode = 'bar';
+ assert.notEqual(tt.mode, 'bar', 'the mode is still the old value, hidden');
+ assert.equal(tt.mode, 'hidden', 'still on the previous mode, hidden');
+
+ tt.mode = 'showing';
+ assert.equal(tt.mode, 'showing', 'mode set to showing');
+
+ tt.mode = 'baz';
+ assert.notEqual(tt.mode, 'baz', 'the mode is still the old value, showing');
+ assert.equal(tt.mode, 'showing', 'still on the previous mode, showing');
+});
+
+QUnit.test('cues and activeCues return a TextTrackCueList', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ assert.ok(tt.cues.getCueById, 'cues are a TextTrackCueList');
+ assert.ok(tt.activeCues.getCueById, 'activeCues are a TextTrackCueList');
+});
+
+QUnit.test('cues can be added and removed from a TextTrack', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+ const cues = tt.cues;
+
+ assert.equal(cues.length, 0, 'start with zero cues');
+
+ tt.addCue({id: '1'});
+
+ assert.equal(cues.length, 1, 'we have one cue');
+
+ tt.removeCue(cues.getCueById('1'));
+
+ assert.equal(cues.length, 0, 'we have removed our one cue');
+
+ tt.addCue({id: '1'});
+ tt.addCue({id: '2'});
+ tt.addCue({id: '3'});
+
+ assert.equal(cues.length, 3, 'we now have 3 cues');
+});
+
+QUnit.test('original cue can be used to remove cue from cues list', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+ const Cue = window.VTTCue ||
+ window.vttjs && window.vttjs.VTTCue ||
+ window.TextTrackCue;
+
+ const cue1 = new Cue(0, 1, 'some-cue');
+
+ assert.equal(tt.cues.length, 0, 'start with zero cues');
+ tt.addCue(cue1);
+ assert.equal(tt.cues.length, 1, 'we have one cue');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 0, 'we have removed cue1');
+});
+
+QUnit.test('non-VTTCue can be used to remove cue from cues list', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ const cue1 = { id: 1, text: 'test' };
+
+ assert.equal(tt.cues.length, 0, 'start with zero cues');
+ tt.addCue(cue1);
+ assert.equal(tt.cues.length, 1, 'we have one cue');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 0, 'we have removed cue1');
+});
+
+QUnit.test('can only remove one cue at a time', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+ const Cue = window.VTTCue ||
+ window.vttjs && window.vttjs.VTTCue ||
+ window.TextTrackCue;
+
+ const cue1 = new Cue(0, 1, 'some-cue');
+
+ assert.equal(tt.cues.length, 0, 'start with zero cues');
+ tt.addCue(cue1);
+ tt.addCue(cue1);
+ assert.equal(tt.cues.length, 2, 'we have two cues');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 1, 'we have removed one instance of cue1');
+
+ tt.removeCue(cue1);
+ assert.equal(tt.cues.length, 0, 'we have removed the other instance of cue1');
+});
+
+QUnit.test('does not include past cues in activeCues', function(assert) {
+ // Testing for the absence of a previous behaviour, which considered cues with equal
+ // start and end times as active 0.5s after ending
+ const player = TestHelpers.makePlayer();
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+ const expectedCue = {
+ id: '2',
+ startTime: 2.555,
+ endTime: 3
+ };
+
+ player.tech_.currentTime = function() {
+ return 2.556;
+ };
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 2.555
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 2.555,
+ endTime: 2.555
+ });
+ // start 2.55
+ tt.addCue(expectedCue);
+
+ player.tech_.trigger('playing');
+
+ assert.equal(tt.activeCues_.length, 1, 'only one cue is present');
+ assert.equal(tt.activeCues_[0].originalCue_, expectedCue, 'correct active cue is present');
+});
+
+QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
+ const done = assert.async();
+ const clock = sinon.useFakeTimers();
+ const player = TestHelpers.makePlayer({techfaker: {autoReady: false}});
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+
+ tt.addCue({
+ id: '1',
+ startTime: 0,
+ endTime: 5
+ });
+
+ tt.oncuechange = cuechangeHandler;
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ player.tech_.currentTime = function() {
+ return 0;
+ };
+
+ // `playing` would trigger rvfc or raf, `timeupdate` for fallback
+ player.tech_.trigger('playing');
+ player.tech_.trigger('timeupdate');
+ assert.equal(changes, 0, 'a cuechange event is not triggered');
+
+ player.tech_.on('ready', function() {
+ player.tech_.currentTime = function() {
+ return 0.2;
+ };
+
+ player.tech_.trigger('playing');
+ clock.tick(1);
+
+ assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');
+
+ player.tech_.trigger('timeupdate');
+ clock.tick(1);
+
+ assert.equal(changes, 2, 'a cuechange event trigger not duplicated by timeupdate');
+
+ tt.off();
+ player.dispose();
+ clock.restore();
+ done();
+ });
+ player.tech_.triggerReady();
+ clock.tick(1);
+});
+
+QUnit.test('fires cuechange when cues become active and inactive', function(assert) {
+ const player = TestHelpers.makePlayer();
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_,
+ mode: 'showing'
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+ let fakeCurrentTime = 0;
+
+ player.tech_.currentTime = function() {
+ return fakeCurrentTime;
+ };
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 5
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 11,
+ endTime: 14
+ });
+
+ tt.oncuechange = cuechangeHandler;
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ fakeCurrentTime = 2;
+ player.tech_.trigger('playing');
+
+ assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange (rvfc/raf)');
+
+ fakeCurrentTime = 7;
+ player.tech_.trigger('playing');
+
+ assert.equal(changes, 4, 'a cuechange event trigger addEventListener and oncuechange (rvfc/raf)');
+
+ fakeCurrentTime = 12;
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 6, 'a cuechange event trigger addEventListener and oncuechange (timeupdate)');
+
+ fakeCurrentTime = 17;
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 8, 'a cuechange event trigger addEventListener and oncuechange (timeupdate)');
+
+ tt.off();
+ player.dispose();
+});
+
+QUnit.test('enabled and disabled cuechange handler when changing mode to hidden', function(assert) {
+ const player = TestHelpers.makePlayer();
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+
+ tt.mode = 'hidden';
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 5
+ });
+
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ player.tech_.currentTime = function() {
+ return 2;
+ };
+ player.tech_.trigger('playing');
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 1, 'a cuechange event trigger');
+
+ changes = 0;
+ // debugger;
+ tt.mode = 'disabled';
+
+ player.tech_.currentTime = function() {
+ return 7;
+ };
+ player.tech_.trigger('playing');
+ player.tech_.trigger('timeupdate');
+
+ assert.equal(changes, 0, 'NO cuechange event trigger');
+
+ tt.off();
+ player.dispose();
+});
+
+QUnit.test('enabled and disabled cuechange handler when changing mode to showing', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const player = TestHelpers.makePlayer();
+ let changes = 0;
+ const tt = new TextTrack({
+ tech: player.tech_
+ });
+ const cuechangeHandler = function() {
+ changes++;
+ };
+
+ tt.mode = 'showing';
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 5
+ });
+
+ tt.addEventListener('cuechange', cuechangeHandler);
+
+ player.tech_.currentTime = function() {
+ return 2;
+ };
+ player.tech_.trigger('playing');
+ clock.tick(10);
+
+ assert.equal(changes, 1, 'a cuechange event trigger');
+
+ changes = 0;
+ tt.mode = 'disabled';
+
+ player.tech_.currentTime = function() {
+ return 7;
+ };
+ player.tech_.trigger('playing');
+
+ assert.equal(changes, 0, 'NO cuechange event trigger');
+
+ tt.off();
+ player.dispose();
+ clock.restore();
+});
+
+QUnit.test('if preloadTextTracks is false, default tracks are not parsed until mode is showing', function(assert) {
+ this.tech.preloadTextTracks = false;
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ let parserCreated = false;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ parserCreated = true;
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ const tt = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com',
+ default: true
+ });
+
+ assert.notOk(reqs.length, 'Default track is not requested');
+ assert.notOk(parserCreated, 'Parser is not created');
+
+ tt.mode = 'showing';
+
+ const req = reqs.pop();
+
+ req.respond(200, null, 'WEBVTT\n');
+
+ assert.ok(parserCreated, 'Parser is created after track is showing');
+
+ clock.restore();
+ tt.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('tracks are parsed if vttjs is loaded', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ let parserCreated = false;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ parserCreated = true;
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ const tt = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com'
+ });
+
+ const req = reqs.pop();
+
+ req.respond(200, null, 'WEBVTT\n');
+
+ assert.ok(parserCreated, 'WebVTT is loaded, so we can just parse');
+ assert.notOk(req.withCredentials, 'the request defaults not to send credentials');
+
+ clock.restore();
+ tt.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('tracks are loaded withCredentials is crossorigin is set to use-credentials', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ this.tech.crossOrigin = () => 'use-credentials';
+
+ const tt = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com'
+ });
+
+ const req = reqs.pop();
+
+ assert.ok(req.withCredentials, 'the request was made withCredentials');
+
+ this.tech.crossOrigin = () => 'anonymous';
+
+ const tt2 = new TextTrack({
+ tech: this.tech,
+ src: 'http://example.com'
+ });
+
+ const req2 = reqs.pop();
+
+ assert.notOk(req2.withCredentials, 'the request was not made withCredentials');
+
+ req.abort();
+ req2.abort();
+ clock.restore();
+ tt.off();
+ tt2.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('tracks are parsed once vttjs is loaded', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const oldVTT = window.WebVTT;
+ let parserCreated = false;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ window.WebVTT = true;
+
+ const testTech = new EventTarget();
+
+ testTech.textTracks = () => {};
+ testTech.currentTime = () => {};
+ testTech.crossOrigin = () => null;
+
+ const tt = new TextTrack({
+ tech: testTech,
+ src: 'http://example.com'
+ });
+
+ reqs.pop().respond(200, null, 'WEBVTT\n');
+
+ assert.ok(!parserCreated, 'WebVTT is not loaded, do not try to parse yet');
+
+ clock.tick(100);
+ assert.ok(!parserCreated, 'WebVTT still not loaded, do not try to parse yet');
+
+ window.WebVTT = () => {};
+ window.WebVTT.StringDecoder = () => {};
+
+ // This needs to be function expression rather than arrow function so it is constructable
+ window.WebVTT.Parser = function() {
+ parserCreated = true;
+ return {
+ oncue() {},
+ onparsingerror() {},
+ onflush() {},
+ parse() {},
+ flush() {}
+ };
+ };
+
+ testTech.trigger('vttjsloaded');
+ assert.ok(parserCreated, 'WebVTT is loaded, so we can parse now');
+
+ clock.restore();
+ tt.off();
+ testTech.off();
+ window.WebVTT = oldVTT;
+});
+
+QUnit.test('stops processing if vttjs loading errored out', function(assert) {
+ const clock = sinon.useFakeTimers();
+ const errorSpy = sinon.spy();
+ const oldVTT = window.WebVTT;
+ const oldLogError = log.error;
+ const reqs = [];
+
+ this.xhr.onCreate = function(req) {
+ reqs.push(req);
+ };
+
+ log.error = errorSpy;
+
+ window.WebVTT = true;
+
+ const testTech = new EventTarget();
+
+ testTech.textTracks = () => {};
+ testTech.currentTime = () => {};
+ testTech.crossOrigin = () => null;
+
+ sinon.stub(testTech, 'off');
+ testTech.off.withArgs('vttjsloaded');
+
+ const tt = new TextTrack({
+ tech: testTech,
+ src: 'http://example.com'
+ });
+
+ reqs.pop().respond(200, null, 'WEBVTT\n');
+
+ testTech.trigger('vttjserror');
+
+ assert.equal(errorSpy.callCount, 1, 'vttjs failed to load, so log.error was called');
+
+ testTech.trigger('vttjserror');
+
+ // vttjserror not called again
+ assert.equal(errorSpy.callCount, 1, 'vttjserror handler not called again');
+
+ clock.restore();
+ window.WebVTT = oldVTT;
+ tt.off();
+ testTech.off.restore();
+ testTech.off();
+ log.error = oldLogError;
+});
+
+QUnit.test('toJSON', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 2.555
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 2.555,
+ endTime: 2.555
+ });
+
+ const jsonTrack = tt.toJSON();
+
+ // Properties we want copied are copied correctly
+ assert.equal(tt.id, jsonTrack.id, 'the id for the copied track stayed the same');
+ assert.equal(tt.mode, jsonTrack.mode, 'the mode for the copied track stayed the same');
+ assert.equal(tt.kind, jsonTrack.kind, 'the kind for the copied track stayed the same');
+
+ // The tech_ property stays on the original track, but is removed from the copy
+ assert.ok(tt.tech_, 'the tech exists on the original track');
+ assert.notOk(jsonTrack.tech_, 'the tech does not exist on the copied track');
+});
+
+QUnit.test('serialize', function(assert) {
+ const tt = new TextTrack({
+ tech: this.tech
+ });
+
+ tt.addCue({
+ id: '1',
+ startTime: 1,
+ endTime: 2.555
+ });
+ tt.addCue({
+ id: '2',
+ startTime: 2.555,
+ endTime: 2.555
+ });
+
+ const serializedTrack = JSON.stringify(tt);
+
+ // Ensure tech was not removed from the actual track
+ assert.ok(tt.tech_, 'the tech exists on the original track');
+
+ // Values from the track should be found in the serialized string
+ assert.ok(serializedTrack.includes(`"id":"${tt.id}"`), 'serialized data should include id');
+ assert.ok(serializedTrack.includes(`"mode":"${tt.mode}"`), 'serialized data should include mode');
+ assert.ok(serializedTrack.includes(`"kind":"${tt.kind}"`), 'serialized data should include cues');
+
+ // tech_ should not be found in the serialized string
+ assert.notOk(serializedTrack.includes('"tech_":'), 'serialized data should not include tech_');
+});