I'm using WebdriverIO and its wdio testrunner with mocha and chai.
I want to build some custom commands, but in this scenario, where and how is the best way to add custom commands?
The best place to keep the custom command is in your wdio config file.
before: function(capabilities, specs) {
// Comment why this command is getting added and what it is supposed to do
browser.addCommand('nameOfCommand', function() {
// your code for the command
});
},
When you are finished with adding the custom command in config file. You can call them anywhere in your whole framework by just calling browser object. Like for above custom command to be called : browser.nameOfCommand() will do the magic.
While building a full-fledged automation harness powered by the exact tech-stack you mentioned (WebdriverIO/Mocha&Chai), I have come to the conclusion that Page Objects are't there yet (it's also a pain to keep them up-to-date) and the best way to harness the complete power of WebdriverIO is to write your own Custom Commands.
The main reasons why I recommend custom commands:
avoid reusing waitUntil() statements (explicitly waiting) for a WebElement to be displayed;
easily access my WebElement modules (files where I mapped my WebElements based on views, routes, or websites);
reuse the same custom commands in other custom commands;
the Mocha it() (test case) statement is much cleaner and easier to follow.
Here's an example of how you can write a custom click() (action.js file):
module.exports = (function() {
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Def: Performs a 'click' action on the given element (WebElement)
* after waiting for the given element to exist and be visible.
* #param: {String} element
* #returns {WebdriverIO.Promise}
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
browser.addCommand('cwClick', function(element) {
var locator = cwElements.<jsonPathTo>[element] ||
thirdPartyElements.<jsonPathTo>[element];
assert.isOk(locator, "\nNo CSS-path for targeted element ('" + element + "')\n");
return browser
.waitUntil(function() {
return browser.isExisting(locator);
}, timeout, "Oups! An error occured.\nReason: element ('" + locator + "') does not exist")
.waitUntil(function() {
return browser.isVisible(locator);
}, timeout, "Oups! An error occured.\nReason: element ('" + locator + "') is not visible")
.waitUntil(function() {
return browser.click(locator);
}, timeout, "Oups! An error occured.\nReason: element ('" + locator + "') could not be clicked")
});
})();
Finally, in your test suite (feature file), import the file that contains your custom command, in this case, action.js: var action = require('.<pathToCommandsFolder>/action.js');.
That's it. You're done! :)
Note: In order to keep my custom-cummand files clean, I've broken them down into several categories:
Actions (action.js):
Contains actions such as: cwClick, cwGetText, cwGetValue, cwSetValue, etc.
Validation (validate.js):
Contains validations such as: isDisplayed, isNotDisplayed, etc.
Navigation (navigate.js):
Contains navigation commands: cwBack, cwRefresh, cwForward, tab-manipulation-custom-commands, etc.
etc. (the skyhardware is the limit!)
Hope this helps. Cheers!
For the record, this is an example of how I'm creating my custom commands inside Page Objects.
var HomePage = function() {
var me = this;
this.clickFlag = function() {
var flag = me.navbar.$('.//li[#class="xtt-popover-click xtt-flags"]'),
flagActive = me.navbar.$('.//li[#class="xtt-popover-click xtt-flags active"]');
flag.click();
flagActive.waitForExist(1000);
}
this.elementThatContainsText = function(tag, text) {
var el;
if (tag) {
el = $('//' + tag + '[contains(content(), "' + text + '")]');
} else {
el = $('//*[contains(content(), "' + text + '")]');
}
return el;
}
this.highlight = function(webElement) {
var id = webElement.getAttribute('id');
if (id) {
browser.execute(function(id) {
$(id).css("background-color", "yellow");
}, id);
}
}
};
Object.defineProperties(HomePage.prototype, {
navbar: {
get: function() {
return $('//div[#class="navbar-right"]');
}
},
comboLanguage: {
get: function() {
return this.navbar.$('.//a[#id="xtt-lang-selector"]');
}
},
ptLink: {
get: function() {
return this.navbar.$('.//a[#href="/institutional/BR/pt/"]');
}
}
});
module.exports = new HomePage();
So, my HomePage have a now a custom clickFlag command, and a highlight command. And properties like navbar and comboLanguage which are selectors.
Related
This is the query I am using:
app.get("/items/:data", async (req, res) => {
const { data } = req.params;
query = `
SELECT items.discount
FROM items
WHERE items.discount #? '$[*] ? (#.discount[*].shift == $1)'
`
try {
const obj = await pool.query(query, [data]);
res.json(obj.rows[0])
} catch(err) {
console.error(err.message);
}
});
I get this error:
error: bind message supplies 1 parameters, but prepared statement "" requires 0
I am using node-postgres package in node.js.
How can I solve this issue?
Use bracket notation instead of dot notation. So instead of obj.key use obj[key]
Updated
all them driver connectors come with their own method to do what you're looking for. node-postgres also have there own
Pool
import { Pool } from 'pg';
const pool = new Pool({
host: 'localhost',
user: 'database-user',
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
/**
* execs the given sql statement.
*
* #param {string} sql - query to run.
* #param {Array} params - an array with the parameter.
* #example
* runQuery("SELECT * FROM users WHERE id = $1", [1]).then(result=> console.log(result))
*/
export async function runQuery (sql, params) {
const connection = await pool.connect()
try {
await connection.query('BEGIN')
const queryText = 'INSERT INTO users(name) VALUES($1) RETURNING id'
const result = await connection.query(sql,params);
// check what result has
console.log(result);
return connection.query('COMMIT').then(result)
} catch (e) {
await connection.query('ROLLBACK')
throw e;
throw e
} finally {
connection.release()
}
}
Pool Config
config = {
// all valid client config options are also valid here
// in addition here are the pool specific configuration parameters:
// number of milliseconds to wait before timing out when connecting a new client
// by default this is 0 which means no timeout
connectionTimeoutMillis?: int,
// number of milliseconds a client must sit idle in the pool and not be checked out
// before it is disconnected from the backend and discarded
// default is 10000 (10 seconds) - set to 0 to disable auto-disconnection of idle clients
idleTimeoutMillis?: int,
// maximum number of clients the pool should contain
// by default this is set to 10.
max?: int,
}
conclution
so basically the structure of a query should be like or less this
const text = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *'
const values = ['brianc', 'brian.m.carlson#gmail.com']
connection
.query(text, values)
.then(res => {
console.log(res.rows[0])
// { name: 'brianc', email: 'brian.m.carlson#gmail.com' }
})
.catch(e => console.error(e.stack))
I am trying to get my bank balances in Google Sheets and decided to use this script https://github.com/bradjasper/ImportJSON to pull data from my Investec bank account via the API here: https://developer.investec.com/programmable-banking/#authorization
I am calling the function ImportJSONBasicAuth("https://openapi.investec.com/identity/v2/oauth2/token","{username}","{secret}","","") with my username and secret, in my Google Sheet... But I get an error as follows
Error Exception: Request failed for https://openapi.investec.com returned code 400 (line 220).
Any ideas what I might be doing wrong?
EDIT
Digging a little deeper into the error...
This is the function I'm calling
function ImportJSONBasicAuth(url, username, password, query, parseOptions) {
var encodedAuthInformation = Utilities.base64Encode(username + ":" + password);
var header = {headers: {Authorization: "Basic " + encodedAuthInformation}};
return ImportJSONAdvanced(url, header, query, parseOptions, includeXPath_, defaultTransform_);
}
Which calls this function in turn - which has the line producing the error i.e. line 220 commented below
function ImportJSONAdvanced(url, fetchOptions, query, parseOptions, includeFunc, transformFunc) {
var jsondata = UrlFetchApp.fetch(url, fetchOptions); //line 220
var object = JSON.parse(jsondata.getContentText());
return parseJSONObject_(object, query, parseOptions, includeFunc, transformFunc);
}
I found a function ImportJSONViaPost in the code, so I tried to combine that with the function ImportJSONBasicAuth that I was using, but also get the same error.
Here is the combined function:
function ImportJSONBasicAuthViaPost(url, username, password, payload, fetchOptions, query, parseOptions) {
var postOptions = parseToObject_(fetchOptions);
var encodedAuthInformation = Utilities.base64Encode(username + ":" + password);
var APIKey = "Basic " + encodedAuthInformation;
if (postOptions["headers.Authorization"] == null) {
postOptions["headers.Authorization"] = APIKey;
}
if (postOptions["method"] == null) {
postOptions["method"] = "POST";
}
if (postOptions["payload"] == null) {
postOptions["payload"] = payload;
}
if (postOptions["contentType"] == null) {
postOptions["contentType"] = "application/x-www-form-urlencoded";
}
if (postOptions["contentType"] == null) {
postOptions["contentType"] = "application/x-www-form-urlencoded";
}
convertToBool_(postOptions, "validateHttpsCertificates");
convertToBool_(postOptions, "useIntranet");
convertToBool_(postOptions, "followRedirects");
convertToBool_(postOptions, "muteHttpExceptions");
return ImportJSONAdvanced(url, postOptions, query, parseOptions, includeXPath_, defaultTransform_);
}
const headers = {
'Authorization': `Basic ${APIKey}`
};
const payload = {
'grant_type': 'client_credentials',
'scope': 'accounts'
}
const options = {
'method': 'POST',
'payload': payload,
'headers': headers
};
Using the answer from #idfurw and rewriting the ImportJSONViaPost function I managed to create a function that gets the Basic Auth information from POST to import to my Google Sheet :)
/**
* Helper function to authenticate with basic auth informations and import a JSON feed via a POST request.
* Returns the results to be inserted into a Google Spreadsheet.
* The JSON feed is flattened to create a two-dimensional array.
* The first row contains the headers, with each column header indicating the path to
* that data in the JSON feed. The remaining rows contain the data.
*
* To retrieve the JSON, a POST request is sent to the URL and the payload is passed as the content of the request using the content
* type "application/x-www-form-urlencoded". If the fetchOptions define a value for "method", "payload" or "contentType", these
* values will take precedent. For example, advanced users can use this to make this function pass XML as the payload using a GET
* request and a content type of "application/xml; charset=utf-8". For more information on the available fetch options, see
* https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app . At this time the "headers" option is not supported.
*
* By default, the returned data gets transformed so it looks more like a normal data import. Specifically:
*
* - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
* of the rows representing their parent elements.
* - Values longer than 256 characters get truncated.
* - Headers have slashes converted to spaces, common prefixes removed and the resulting text converted to title case.
*
* To change this behavior, pass in one of these values in the options parameter:
*
* noInherit: Don't inherit values from parent elements
* noTruncate: Don't truncate values
* rawHeaders: Don't prettify headers
* noHeaders: Don't include headers, only the data
* allHeaders: Include all headers from the query parameter in the order they are listed
* debugLocation: Prepend each value with the row & column it belongs in
*
* For example:
*
* =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json", "user=bob&apikey=xxxx",
* "validateHttpsCertificates=false", "/feed/entry/title,/feed/entry/content", "noInherit,noTruncate,rawHeaders")
*
* #param {url} the URL to a public JSON feed
* #param {username} the Username for authentication
* #param {password} the Password for authentication
* #param {payload} the content to pass with the POST request; usually a URL encoded list of parameters separated by ampersands
* #param {fetchOptions} a comma-separated list of options used to retrieve the JSON feed from the URL
* #param {query} a comma-separated list of paths to import. Any path starting with one of these paths gets imported.
* #param {parseOptions} a comma-separated list of options that alter processing of the data
* #customfunction
*
* #return a two-dimensional array containing the data, with the first row containing headers
**/
function ImportJSONBasicAuthViaPost(url, username, password, payload, fetchOptions, query, parseOptions) {
var postOptions = parseToObject_(fetchOptions);
var APIKey = Utilities.base64Encode(username + ":" + password);
const headers = {
'Authorization': `Basic ${APIKey}`
};
if (postOptions["headers"] == null) {
postOptions["headers"] = headers;
}
if (postOptions["method"] == null) {
postOptions["method"] = "POST";
}
if (postOptions["payload"] == null) {
postOptions["payload"] = payload;
}
if (postOptions["contentType"] == null) {
postOptions["contentType"] = "application/x-www-form-urlencoded";
}
convertToBool_(postOptions, "validateHttpsCertificates");
convertToBool_(postOptions, "useIntranet");
convertToBool_(postOptions, "followRedirects");
convertToBool_(postOptions, "muteHttpExceptions");
return ImportJSONAdvanced(url, postOptions, query, parseOptions, includeXPath_, defaultTransform_);
}
While executing tests on TestCafe using CLI, I am getting below error:
ERROR Cannot prepare tests due to an error.
at Object.<anonymous> (C:\Users\xxxx\Documents\TestCafe_Framework\tests\Test.js:1:1)
at Function._execAsModule (C:\Users\xxxxx\Documents\TestCafe_Framework\node_modules\testcafe\src\compiler\test-file\api-based.js:50:13)
at ESNextTestFileCompiler._runCompiledCode (C:\Users\xxxxx\Documents\TestCafe_Framework\node_modules\testcafe\src\compiler\test-file\api-based.js:150:42)
at ESNextTestFileCompiler.execute (C:\Users\xxxxxx\Documents\TestCafe_Framework\node_modules\testcafe\src\compiler\test-file\api-based.js:174:21)
at ESNextTestFileCompiler.compile (C:\Users\xxxxx\Documents\TestCafe_Framework\node_modules\testcafe\src\compiler\test-file\api-based.js:180:21)
at Compiler._getTests (C:\Users\xxxxxx\Documents\TestCafe_Framework\node_modules\testcafe\src\compiler\index.js:87:31)
at Compiler._compileTestFiles (C:\Users\xxxxx\Documents\TestCafe_Framework\node_modules\testcafe\src\compiler\index.js:99:35)
Initially I have recorded the steps from TestCafe GUI and executed the same. But when I transitioned my code to POM style it is throwing above error
Test File: This is the actual test which will get executed from CLI
import LoginPage from `C:\Users\xxxxxx\Documents\TestCafe_Framework\page_object;`
fixture(`Testing`)
.page(`https://xxxx/login.php`);
test('PVT ', async (t) => {
//var email = Math.floor(Math.random() * 555);
//var random_ascii = Math.floor((Math.random() * 25) + 97);
//var name = String.fromCharCode(random_ascii);
await t.LoginPage.login('hp', 'St');
});
Page File: This file contains selectors and action functions to perform login operations.
import { Selector, t } from 'testcafe';
class LoginPage {
get emailTextBox() { return Selector('#EmailAddr'); }
get passwordTextBox() { return Selector('#Password'); }
get loginButton() { return Selector('#SignIn'); }
async login(username, password) {
await t
.click(this.emailTextBox)
.typeText(this.emailTextBox, username)
.click(this.passwordTextBox)
.typeText(this.passwordTextBox, password)
.click(this.loginButton)
}
}
export default new LoginPage();
I can see two errors in your test code:
The slashes in the import statement are not escaped. This is the cause of the error you are facing.
The second issue is in this line: await t.LoginPage.login('hp', 'St');. LoginPage is imported from a file, so it's not available from testController.
The test should work correctly if you change it in the following way:
import LoginPage from 'C:\\Users\\xxxxxx\\Documents\\TestCafe_Framework\\page_object';
fixture(`Testing`)
.page(`https://xxxx/login.php`);
test('PVT ', async (t) => {
//var email = Math.floor(Math.random() * 555);
//var random_ascii = Math.floor((Math.random() * 25) + 97);
//var name = String.fromCharCode(random_ascii);
await LoginPage.login('hp', 'St');
});
I am using this sample to feed my calendar. I have created a Client ID but after I run this project I get 2 errors in console as is shown:
Code:
<html>
<head>
<script type="text/javascript">
// Your Client ID can be retrieved from your project in the Google
// Developer Console, https://console.developers.google.com
var CLIENT_ID = '633454716537-7npq10974v964a85l2bboc2j08sc649r.apps.googleusercontent.com';
// This quickstart only requires read-only scope, check
// https://developers.google.com/google-apps/calendar/auth if you want to
// request write scope.
var SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
/**
* Check if current user has authorized this application.
*/
function checkAuth() {
gapi.auth.authorize(
{
'client_id': CLIENT_ID,
'scope': SCOPES,
'immediate': true
}, handleAuthResult);
}
/**
* Handle response from authorization server.
*
* #param {Object} authResult Authorization result.
*/
function handleAuthResult(authResult) {
var authorizeDiv = document.getElementById('authorize-div');
if (authResult && !authResult.error) {
// Hide auth UI, then load Calendar client library.
authorizeDiv.style.display = 'none';
loadCalendarApi();
} else {
// Show auth UI, allowing the user to initiate authorization by
// clicking authorize button.
authorizeDiv.style.display = 'inline';
}
}
/**
* Initiate auth flow in response to user clicking authorize button.
*
* #param {Event} event Button click event.
*/
function handleAuthClick(event) {
gapi.auth.authorize(
{client_id: CLIENT_ID, scope: SCOPES, immediate: false},
handleAuthResult);
return false;
}
/**
* Load Google Calendar client library. List upcoming events
* once client library is loaded.
*/
function loadCalendarApi() {
gapi.client.load('calendar', 'v3', listUpcomingEvents);
}
/**
* Print the summary and start datetime/date of the next ten events in
* the authorized user's calendar. If no events are found an
* appropriate message is printed.
*/
function listUpcomingEvents() {
var request = gapi.client.calendar.events.list({
'calendarId': 'primary',
'timeMin': (new Date()).toISOString(),
'showDeleted': false,
'singleEvents': true,
'maxResults': 10,
'orderBy': 'startTime'
});
request.execute(function(resp) {
var events = resp.items;
appendPre('Upcoming events:');
if (events.length > 0) {
for (i = 0; i < events.length; i++) {
var event = events[i];
var when = event.start.dateTime;
if (!when) {
when = event.start.date;
}
appendPre(event.summary + ' (' + when + ')')
}
} else {
appendPre('No upcoming events found.');
}
});
}
/**
* Append a pre element to the body containing the given message
* as its text node.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('output');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
</script>
<script src="https://apis.google.com/js/client.js?onload=checkAuth">
</script>
</head>
<body>
<div id="authorize-div" style="display: none">
<span>Authorize access to calendar</span>
<!--Button for the user to click to initiate auth sequence -->
<button id="authorize-button" onclick="handleAuthClick(event)">
Authorize
</button>
</div>
<pre id="output"></pre>
</body>
</html>
Console errors:
[Error] Failed to load resource: the server responded with a status of 400 (Bad Request) (auth, line 0)
[Error] Refused to display 'https://accounts.google.com/o/oauth2/auth?client_id=633454716537-7npq10974v964a85l2bboc2j08sc649r.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly&immediate=true&include_granted_scopes=true&proxy=oauth2relay396521106&redirect_uri=postmessage&origin=file%3A%2F%2F&response_type=token&state=338793751%7C0.4135151437&authuser=0' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'. (about:blank, line 0)
All I want to know is to show the upcoming events from my calendar.
Have anyone any idea how to solve this?
You might try to make sure it is a public calendar.
You can also embed googles calendar in a webpage directly- in calendars/settings
You could also try something like this:
http://mikeclaffey.com/google-calendar-into-html/
I'm coding a basic video marquee and one of the key requirements is that the videos need to be able to advance while keeping the player in full screen.
Using Video.js (4.1.0) I have been able to get everything work correctly except that I cannot get the captions to change when switching to another video.
Either inserting a "track" tag when the player HTML is first created or adding a track to the 'options' object when the player is initialized are the only ways I can get the player to display the "CC" button and show captions. However, I cannot re-initialize the player while in full screen so changing the track that way will not work.
I have tried addTextTrack and addTextTracks and both show that the tracks have been added - using something like console.log(videoObject.textTracks()) - but the player never shows them or the "CC" button.
Here is my code, any help is greatly appreciated:
;(function(window,undefined) {
// VIDEOS OBJECT
var videos = [
{"volume":"70","title":"TEST 1","url":"test1.mp4","type":"mp4"},
{"volume":"80","title":"TEST 2","url":"test2.mp4","type":"mp4"},
{"volume":"90","title":"TEST 3","url":"test3.mp4","type":"mp4"}
];
// CONSTANTS
var VIDEO_BOX_ID = "jbunow_marquee_video_box", NAV_TEXT_ID = "jbunow_marquee_nav_text", NAV_ARROWS_ID = "jbunow_marquee_nav_arrows", VIDEO_OBJ_ID = "jbunow_marquee_video", NAV_PREV_ID = "jbunow_nav_prev", NAV_NEXT_ID = "jbunow_nav_next";
// GLOBAL VARIABLS
var videoObject;
var currentTrack = 0;
var videoObjectCreated = false;
var controlBarHideTimeout;
jQuery(document).ready(function(){
// CREATE NAV ARROWS AND LISTENERS, THEN START MARQUEE
var navArrowsHtml = "<div id='" + NAV_PREV_ID + "' title='Play Previous Video'></div>";
navArrowsHtml += "<div id='" + NAV_NEXT_ID + "' title='Play Next Video'></div>";
jQuery('#' + NAV_ARROWS_ID).html(navArrowsHtml);
jQuery('#' + NAV_PREV_ID).on('click',function() { ChangeVideo(GetPrevVideo()); });
jQuery('#' + NAV_NEXT_ID).on('click',function() { ChangeVideo(GetNextVideo()); });
ChangeVideo(currentTrack);
});
var ChangeVideo = function(newIndex) {
var videoBox = jQuery('#' + VIDEO_BOX_ID);
if (!videoObjectCreated) {
// LOAD PLAYER HTML
videoBox.html(GetPlayerHtml());
// INITIALIZE VIDEO-JS
videojs(VIDEO_OBJ_ID, {}, function(){
videoObject = this;
// LISTENERS
videoObject.on("ended", function() { ChangeVideo(GetNextVideo()); });
videoObject.on("loadeddata", function () { videoObject.play(); });
videoObjectCreated = true;
PlayVideo(newIndex);
});
} else { PlayVideo(newIndex); }
}
var PlayVideo = function(newIndex) {
// TRY ADDING MULTIPLE TRACKS
videoObject.addTextTracks([{ kind: 'captions', label: 'English2', language: 'en', srclang: 'en', src: 'track2.vtt' }]);
// TRY ADDING HTML
//jQuery('#' + VIDEO_OBJ_ID + ' video').eq(0).append("<track kind='captions' src='track2.vtt' srclang='en' label='English' default />");
// TRY ADDING SINGLE TRACK THEN SHOWING USING RETURNED ID
//var newTrack = videoObject.addTextTrack('captions', 'English2', 'en', { kind: 'captions', label: 'English2', language: 'en', srclang: 'en', src: 'track2.vtt' });
//videoObject.showTextTrack(newTrack.id_, newTrack.kind_);
videoObject.volume(parseFloat(videos[newIndex]["volume"]) / 100); // SET START VOLUME
videoObject.src({ type: "video/" + videos[newIndex]["type"], src: videos[newIndex]["url"] }); // SET NEW SRC
videoObject.load();
videoObject.ready(function () {
videoObject.play();
clearTimeout(controlBarHideTimeout);
controlBarHideTimeout = setTimeout(function() { videoObject.controlBar.fadeOut(); }, 2000);
jQuery('#' + NAV_TEXT_ID).fadeOut(150, function() {
currentTrack = newIndex;
var navHtml = "";
navHtml += "<h1>Now Playing</h1><h2>" + videos[newIndex]["title"] + "</h2>";
if (videos.length > 1) { navHtml += "<h1>Up Next</h1><h2>" + videos[GetNextVideo()]["title"] + "</h2>"; }
jQuery('#' + NAV_TEXT_ID).html(navHtml).fadeIn(250);
});
});
}
var GetPlayerHtml = function() {
var playerHtml = "";
playerHtml += "<video id='" + VIDEO_OBJ_ID + "' class='video-js vjs-default-skin' controls='controls' preload='auto' width='560' height='315'>";
playerHtml += "<source src='' type='video/mp4' />";
//playerHtml += "<track kind='captions' src='track.vtt' srclang='en' label='English' default='default' />";
playerHtml += "</video>";
return playerHtml;
}
var GetNextVideo = function() {
if (currentTrack >= videos.length - 1) { return 0; }
else { return (currentTrack + 1); }
}
var GetPrevVideo = function() {
if (currentTrack <= 0) { return videos.length - 1; }
else { return (currentTrack - 1); }
}
})(window);
The current VideoJS implementation (4.4.2) loads every kind of text tracks (subtitles, captions, chapters) on initialization time of the player itself, so it grabs correctly only those, which are defined between the <video> tags.
EDIT: I meant it does load them when calling addTextTrack, but the player UI will never update after initialization time, and will always show the initialization time text tracks.
One possible workaround is if you destroy the complete videojs player and re-create it on video source change after you have refreshed the content between the <video> tags. So this way you don't update the source via the videojs player, but via dynamically adding the required DOM elements and initializing a new player on them. Probably this solution will cause some UI flashes, and is quite non-optimal for the problem. Here is a link about destroying the videojs player
Second option is to add the dynamic text track handling to the existing code, which is not as hard as it sounds if one knows where to look (I did it for only chapters, but could be similar for other text tracks as well). The code below works with the latest official build 4.4.2. Note that I'm using jQuery for removing the text track elements, so if anyone applies these changes as is, jQuery needs to be loaded before videojs.
Edit the video.dev.js file as follows:
1: Add a clearTextTracks function to the Player
vjs.Player.prototype.clearTextTracks = function() {
var tracks = this.textTracks_ = this.textTracks_ || [];
for (var i = 0; i != tracks.length; ++i)
$(tracks[i].el()).remove();
tracks.splice(0, tracks.length);
this.trigger("textTracksChanged");
};
2: Add the new 'textTracksChanged' event trigger to the end of the existing addTextTrack method
vjs.Player.prototype.addTextTrack = function(kind, label, language, options) {
...
this.trigger("textTracksChanged");
}
3: Handle the new event in the TextTrackButton constructor function
vjs.TextTrackButton = vjs.MenuButton.extend({
/** #constructor */
init: function(player, options) {
vjs.MenuButton.call(this, player, options);
if (this.items.length <= 1) {
this.hide();
}
player.on('textTracksChanged', vjs.bind(this, this.refresh));
}
});
4: Implement the refresh method on the TextTrackButton
// removes and recreates the texttrack menu
vjs.TextTrackButton.prototype.refresh = function () {
this.removeChild(this.menu);
this.menu = this.createMenu();
this.addChild(this.menu);
if (this.items && this.items.length <= this.kind_ == "chapters" ? 0 : 1) {
this.hide();
} else
this.show();
};
Sorry, but for now I cannot link to a real working example, I hope the snippets above will be enough as a starting point to anyone intrested in this.
You can use this code when you update the source to a new video. Just call the clearTextTracks method, and add the new text tracks with the addTextTrack method, and the menus now should update themselves.
Doing the exact same thing (or rather NOT doing the exact same thing)... really need to figure out how to dynamically change / add a caption track.
This works to get it playing via the underlying HTML5, but it does not show the videojs CC button:
document.getElementById("HtmlFiveMediaPlayer_html5_api").innerHTML = '<track label="English Captions" srclang="en" kind="captions" src="http://localhost/media/captiontest/demo_Brian/demo_h264_1.vtt" type="text/vtt" default />';