Jasmine - Testing links via Webdriver I/O - webdriver

I have been working on a end-to-end test using Webdriver I/O from Jasmine. One specific scenario has been giving me significant challenges.
I have a page with 5 links on it. The number of links actually challenges as the page is dynamic. I want to test the links to see if each links' title matches the title of the page that it links to. Due to the fact that the links are dynamically generated, I cannot just hard code tests for each link. So, I'm trying the following:
it('should match link titles to page titles', function(done) {
client = webdriverio.remote(settings.capabilities).init()
.url('http://www.example.com')
.elements('a').then(function(links) {
var mappings = [];
// For every link store the link title and corresponding page title
var results = [];
for (var i=0; i<links.value.length; i++) {
mappings.push({ linkTitle: links.value[0].title, pageTitle: '' });
results.push(client.click(links.value[i])
.getTitle().then(function(title, i) {
mappings[i].pageTitle = title;
});
);
}
// Once all promises have resolved, compared each link title to each corresponding page title
Promise.all(results).then(function() {
for (var i=0; i<mappings.length; i++) {
var mapping = mappings[i];
expect(mapping.linkTitle).toBe(mapping.pageTitle);
}
done();
});
});
;
});
I'm unable to even confirm if I'm getting the link title properly. I believe there is something I entirely misunderstand. I am not even getting each links title property. I'm definately not getting the corresponding page title. I think I'm lost in closure world here. Yet, I'm not sure.
UPDATE - NOV 24
I still have not figured this out. However, i believe it has something to do with the fact that Webdriver I/O uses the Q promise library. I came to this conclusion because the following test works:
it('should match link titles to page titles', function(done) {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() { resolve(); }, 1000);
});
promise.then(function() {
var promises = [];
for (var i=0; i<3; i++) {
promises.push(
new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, 500);
})
);
}
Promise.all(promises).then(function() {
expect(true).toBe(true)
done();
});
});
However, the following does NOT work:
it('should match link titles to page titles', function(done) {
client = webdriverio.remote(settings.capabilities).init()
.url('http://www.example.com')
.elements('a').then(function(links) {
var mappings = [];
// For every link store the link title and corresponding page title
var results = [];
for (var i=0; i<links.value.length; i++) {
mappings.push({ linkTitle: links.value[0].title, pageTitle: '' });
results.push(client.click(links.value[i])
.getTitle().then(function(title, i) {
mappings[i].pageTitle = title;
});
);
}
// Once all promises have resolved, compared each link title to each corresponding page title
Q.all(results).then(function() {
for (var i=0; i<mappings.length; i++) {
var mapping = mappings[i];
expect(mapping.linkTitle).toBe(mapping.pageTitle);
}
done();
});
})
;
});
I'm not getting any exceptions. Yet, the code inside of Q.all does not seem to get executed. I'm not sure what to do here.

Reading the WebdriverIO manual, I feel like there are a few things wrong in your approach:
elements('a') returns WebElement JSON objects (https://code.google.com/p/selenium/wiki/JsonWireProtocol#WebElement_JSON_Object) NOT WebElements, so there is no title property thus linkTitle will always be undefined - http://webdriver.io/api/protocol/elements.html
Also, because it's a WebElement JSON object you cannot use it as client.click(..) input, which expects a selector string not an object - http://webdriver.io/api/action/click.html. To click a WebElement JSON Object client.elementIdClick(ID) instead which takes the ELEMENT property value of the WebElement JSON object.
When a client.elementIdClick is executed, the client will navigate to the page, trying to call client.elementIdClick in the next for loop cycle with next ID will fail, cause there is no such element as you moved away from the page. It will sound something like invalid element cache.....
So, I propose another solution for your task:
Find all elements as you did using elements('a')
Read href and title using client.elementIdAttribute(ID) for each of the elements and store in an object
Go through all of the objects, navigate to each of the href-s using client.url('href'), get the title of the page using .getTitle and compare it with the object.title.
The source I experimented with, not run by Jasmine, but should give an idea:
var client = webdriverio
.remote(options)
.init();
client
.url('https://www.google.com')
.elements('a')
.then(function (elements) {
var promises = [];
for (var i = 0; i < elements.value.length; i++) {
var elementId = elements.value[i].ELEMENT;
promises.push(
client
.elementIdAttribute(elementId, 'href')
.then(function (attributeRes) {
return client
.elementIdAttribute(elementId, 'title')
.then(function (titleRes) {
return {href: attributeRes.value, title: titleRes.value};
});
})
);
}
return Q
.all(promises)
.then(function (results) {
console.log(arguments);
var promises = [];
results.forEach(function (result) {
promises.push(
client
.url(result.href)
.getTitle()
.then(function (title) {
console.log('Title of ', result.href, 'is', title, 'but expected', result.title);
})
);
});
return Q.all(promises);
});
})
.then(function () {
client.end();
});
NOTE:
This fails to solve your problem, when the links trigger navigation with JavaScript event handlers not the href attributes.

Related

forcibly rerun publish in meteor not working even with Reactive-var

I am using Reactive-var, trying to republish the entire collection. I am using latest Meteor 1.5. Everything works fine except that the already solved question still appears in the list even though I have exempted them from publishing.
In Below server, once client submits answer to a question, a new question data must be re-published except all correctly answered questions. That is what is not happening here. But when I refresh the page, things work! But I need this in real time manner, without refreshing page.
SERVER CODE
Meteor.publish('Question.Random', function(refreshValue){
var clientQuestions = [];
var clientCorrectQuestions = QuestionHistory.find({userId : this.userId, result : 1}, {_id : 0, questionId : 1});
if(clientCorrectQuestions.count() > 0){
//get array of correct solved questions
clientCorrectQuestions.map( function(correctQuestion) {
clientQuestions.push(correctQuestion.questionId);
});
}
var randomskipCount = Math.floor(Math.random() * 100) + 1;
return Question.find({_id : { $nin: clientQuestions}});
});
CLIENT CODE
Template.temp1.onCreated(function(){
this.refreshValue = new ReactiveVar(0);
Meteor.subscribe('Question.Random', this.refreshValue.get());
});
Template.temp1.events({
"click .myBtn": function(event, template){
event.preventDefault();
Meteor.call('processUserInput', function(error, response){
if(error){
Bert.alert("<strong>Error !", 'danger', 'fixed-top' );
} else if(response){
instance.refreshValue.set(Math.floor(Math.random() * 500));
}
});
return false;
}
});
any help would be greatly appreciated and rewarded.
I see that you are trying to do publication with aggregation. There are more than one way to do it. In this case you just need to wrap the subscribe command inside a reactive block:
Template.temp1.onCreated(function() {
this.refreshValue = new ReactiveVar(0);
this.autorun(() => {
this.subscribe('Question.Random', this.refreshValue.get());
});
});
You should wrap your subscription in autorun:
Template.temp1.onCreated(function(){
this.refreshValue = new ReactiveVar(0);
this.autorun(() => {
Meteor.subscribe('Question.Random', this.refreshValue.get());
});
});

Meteor - Script doesn't load Web Audio Buffers properly on refresh / only on certain routes

https://github.com/futureRobin/meteorAudioIssues
Trying to load audio buffers into memory. When I hit localhost:3000/tides or localhost:3000 it loads my buffers into memory with no problems. When I then click through onto a session e.g. localhost:3000/tides/SOMESESSIONID. the buffers have already loaded from the previous state.
However, when I then refresh the page on "localhost:3000/tides/SOMESESSIONID" the buffers don't load properly and the console just logs an array of file path names.
Crucial to app functionality. Any help would be great!
audio.js
//new context for loadKit
var context = new AudioContext();
var audioContext = null;
var scheduleAheadTime = 0;
var current16thNote = 0;
var bpm = 140;
//array of samples to load first.
var samplesToLoad = [
"ghost_kick.wav", "ghost_snare.wav", "zap.wav", "ghost_knock.wav"
];
//create a class called loadKit for loading the sounds.
function loadKit(inputArg) {
//get the array of 6 file paths from input.
this.drumPath = inputArg;
}
//load prototype runs loadsample function.
loadKit.prototype.load = function() {
//when we call load, call loadsample 6 times
//feed it the id and drumPath index value
for (var i = 0; i < 6; i++) {
this.loadSample(i, this.drumPath[i]);
}
};
//array to hold the samples in.
//now loadKitInstance.kickBuffer will hold the buffer.
var buffers = [
function(buffer) {
this.buffer1 = buffer;
},
function(buffer) {
this.buffer2 = buffer;
},
function(buffer) {
this.buffer3 = buffer;
},
function(buffer) {
this.buffer4 = buffer;
},
function(buffer) {
this.buffer5 = buffer;
},
function(buffer) {
this.buffer6 = buffer;
}
];
//load in the samples.
loadKit.prototype.loadSample = function(id, url) {
//new XML request.
var request = new XMLHttpRequest();
//load the url & set response to arraybuffer
request.open("GET", url, true);
request.responseType = "arraybuffer";
//save the result to sample
var sample = this;
//once loaded decode the output & bind to the buffers array
request.onload = function() {
buffers[id].bind("");
context.decodeAudioData(request.response, buffers[id].bind(sample));
}
//send the request.
request.send();
};
//get the list of drums from the beat.json
//load them into a the var 'loadedkit'.
loadDrums = function(listOfSamples) {
var drums = samplesToLoad;
loadedKit = new loadKit(listOfSamples);
loadedKit.load();
console.log(loadedKit);
}
//create a new audio context.
initContext = function() {
try {
//create new Audio Context, global.
sampleContext = new AudioContext();
//create new Tuna instance, global
console.log("web audio context loaded");
} catch (e) {
//if not then alert
alert('Sorry, your browser does not support the Web Audio API.');
}
}
//inital function, ran on window load.
init = function() {
audioContext = new AudioContext();
timerWorker = new Worker("/timer_worker.js");
}
client/main.js
Meteor.startup(function() {
Meteor.startup(function() {
init();
initContext();
});
router.js
Router.route('/', {
template: 'myTemplate',
subscriptions: function() {
this.subscribe('sessions').wait();
},
// Subscriptions or other things we want to "wait" on. This also
// automatically uses the loading hook. That's the only difference between
// this option and the subscriptions option above.
waitOn: function () {
return Meteor.subscribe('sessions');
},
// A data function that can be used to automatically set the data context for
// our layout. This function can also be used by hooks and plugins. For
// example, the "dataNotFound" plugin calls this function to see if it
// returns a null value, and if so, renders the not found template.
data: function () {
return Sessions.findOne({});
},
action: function () {
loadDrums(["ghost_kick.wav", "ghost_snare.wav", "zap.wav", "ghost_knock.wav"]);
// render all templates and regions for this route
this.render();
}
});
Router.route('/tides/:_id',{
template: 'idTemplate',
// a place to put your subscriptions
subscriptions: function() {
this.subscribe('sessions', this.params._id).wait();
},
// Subscriptions or other things we want to "wait" on. This also
// automatically uses the loading hook. That's the only difference between
// this option and the subscriptions option above.
waitOn: function () {
return Meteor.subscribe('sessions');
},
// A data function that can be used to automatically set the data context for
// our layout. This function can also be used by hooks and plugins. For
// example, the "dataNotFound" plugin calls this function to see if it
// returns a null value, and if so, renders the not found template.
data: function (params) {
return Sessions.findOne(this.params._id);
},
action: function () {
console.log("IN ACTION")
console.log(Sessions.findOne(this.params._id));
var samples = Sessions.findOne(this.params._id)["sampleList"];
console.log(samples);
loadDrums(samples);
// render all templates and regions for this route
this.render();
}
})
Okay so i got a reply on the meteor forums!
https://forums.meteor.com/t/script-doesnt-load-web-audio-buffers-properly-on--id-routes/15270
"it looks like your problem is relative paths, it's trying to load your files from localhost:3000/tides/ghost_*.wav if you change line 58 of your router to go up a directory for each file it should work.
loadDrums(["../ghost_kick.wav", "../ghost_snare.wav", "../zap.wav", "../ghost_knock.wav"]);
This did the trick. Seems odd that Meteor can load stuff fine without using '../' in one route but not in another but there we go. Hope this helps someone in the future.

How to return number of items in collection?

I'm new to Meteor and I want to create a slideshow with items from a collection, in this case simple words. The slideshow should be controlled by back and forward buttons and replace the current word.
In JavaScript/jQuery I would create an array of objects and a control index, with limits via if-statements, so the index never can drop below zero or overflow the length of the array.
See fiddle for working example:
http://jsfiddle.net/j0pqd26w/8/
$(document).ready(function() {
var wordArray = ["hello", "yes", "no", "maybe"];
var arrayIndex = 0;
$('#word').html(wordArray[arrayIndex]);
$("#previous").click(function(){
if (arrayIndex > 0) {
arrayIndex -= 1;
}
$('#word').html(wordArray[arrayIndex]);
});
$("#next").click(function(){
if (arrayIndex < wordArray.length) {
arrayIndex += 1;
}
$('#word').html(wordArray[arrayIndex]);
});
});
Meteor
I'm curious how to implement this in regards to best practice in meteor and abide to the reactive pattern as I'm still trying to wrap my head around this interesting framework. My first hurdle is to translate the
if (arrayIndex < wordArray.length)
// to
if (Session.get("wordIndex") < ( (((length of collection))) )
According to the docs I should do a find on the collection, but I have only manage to return an empty array later with fetch. Sorry if this got long, but any input would be appreciated to help me figure this out.
collection.find([selector], [options])
cursor.fetch()
This is the code I have so far:
Words = new Mongo.Collection("words");
if (Meteor.isClient) {
// word index starts at 0
Session.setDefault("wordIndex", 0);
Template.body.helpers({
words: function () {
return Words.find({});
},
wordIndex: function () {
return Session.get("wordIndex");
}
});
Template.body.events({
"submit .new-word": function (event) {
// This function is called when the word form is submitted
var text = event.target.text.value;
Words.insert({
text: text,
createdAt: new Date() //current time
});
// Clear form
event.target.text.value = "";
// Prevent default form submit
return false;
},
'click #previous': function () {
// decrement the word index when button is clicked
if (Session.get("wordIndex") > 0) {
Session.set("wordIndex", Session.get("wordIndex") - 1);
}
},
'click #next': function () {
// increment the word index when button is clicked
if (Session.get("wordIndex") < 10 ) {
Session.set("wordIndex", Session.get("wordIndex") + 1);
}
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
});
}
.count() will return the number of documents in a collection.
`db.collection.count()`
There is something called Collection helpers, which works similar to other helpers (eg., template, etc.,). More elaborate explanation is covered here: https://medium.com/space-camp/meteor-doesnt-need-an-orm-2ed0edc51bc5

How to translate templates in Meteor?

UPDATED
NOW I try to do this in my app (thx to Akshat)
//common
LANG = 'ru';
Dictionary = new Meteor.Collection("dictionary");
//if server
Meteor.startup(function () {
if (Dictionary.find().count() === 0) {
// code to fill the Dictionary
}
});
Meteor.publish('dictionary', function () {
return Dictionary.find();
});
//endif
//client
t = function(text) {
if (typeof Dictionary === 'undefined') return text;
var res = Dictionary.find({o:text}).fetch()[0];
return res && res.t;
}
Meteor.subscribe('dictionary', function(){
document.title = t('Let the game starts!');
});
Template.help.text = t('How to play');
//html
<body>
{{> help}}
</body>
<template name="help">
{{text}}
</template>
Still doesn't work as we wanted: when templates are rendered Dictionary was undefined. Butt('How to play') in console works perfectly )
Javascript variables aren't shared between the client and server reactively. You have to use a Meteor Collection to store your data e.g
if (Meteor.isServer) {
var Dictionary = new Meteor.Collection("dictionary");
if(Dictionary.find().count() == 0) {
//If the 'dictionary collection is empty (count ==0) then add stuff in
_.each(Assets.getText(LANG+".txt").split(/\r?\n/), function (line) {
// Skip comment lines
if (line.indexOf("//") !== 0) {
var split = line.split(/ = /);
DICTIONARY.insert({o: split[0], t:split[1]});
}
});
}
}
if (Meteor.isClient) {
var Dictionary = new Meteor.Collection("dictionary");
Template.help.text = function() {
return Dictionary.find({o:'Let the game starts!'});
}
}
In the above i'm assuming you have the autopublish package in (its in by default when you create a package so this shouldn't really bother you, but just in case you removed)
With your document title you would have to use a slightly different implementation because remember the data wouldn't be downloaded at the time Meteor.startup is run, since the html and javascript are downloaded first & the data is empty, then the data comes in slowly (and then reactively fills the data up)

Meteor - How can I pass data between helpers and events for a template?

I'm a bit new to Meteor and something I'm having trouble with is reactive data -- particularly in instances where I need to change the data shown based on a mouse or keyboard event. Doing this kind of stuff the normal js way seems to give me trouble in meteor since everything I change gets re-rendered and reset constantly.
So, I thought I'd see if this would be a case in which I could use Meteor's Deps object, however I can't quite grasp it. Here's the code I'm using:
(function(){
var tenants = [];
var selectedTenant = 0;
var tenantsDep = new Deps.Dependency;
Template.tenantsBlock.tenantsList = function()
{
tenants = [];
var property = $properties.findOne({userId: Meteor.userId(), propertyId: Session.get('property')});
var tenancies = _Utils.resolveTenancies(property, true, null, true);
for(var i = 0; i < tenancies.length; i++)
{
if(tenancies[i].tenancyId == Session.get('tenancy'))
{
tenants = tenants.concat(tenancies[i].otherTenants, tenancies[i].primaryTenant);
}
}
tenants[selectedTenant].selected = 'Selected';
tenantsDep.changed();
return tenants;
};
Template.tenantsBlock.onlyOneTenant = function()
{
tenantsDep.depend();
return tenants.length > 1 ? '' : 'OneChild';
};
Template.tenantsBlock.phoneNumber = function()
{
tenantsDep.depend();
for(var i = 0; i < tenants[selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(tenants[selectedTenant].details[i].key) == 'Phone')
return tenants[selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.emailAddress = function()
{
tenantsDep.depend();
for(var i = 0; i < tenants[selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(tenants[selectedTenant].details[i].key) == 'Email')
return tenants[selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.addedDate = function()
{
tenantsDep.depend();
return _Utils.timeToDateString(tenants[selectedTenant].created);
};
Template.tenantsBlock.events({
'click .Name': function(e, template)
{
tenantsDep.depend();
var _this = e.currentTarget;
var tenantName = _this.innerHTML;
$(_this).addClass('Selected');
$(_this).siblings().removeClass('Selected');
for(var i = 0; i < tenants.length; i++)
{
if(tenants[i].name == tenantName)
tenants[i].selected = "Selected";
else
tenants[i].selected = '';
}
}
})
})();
^This seemed to be what they were getting at in the meteor documentation (http://docs.meteor.com/#deps_dependency) for dependency.changed() and dependency.depend(), but all this does is give me an infinite loop.
So can I modify the way I declare deps to get this to make data reactive? Is there a better way to do this all together?
UPDATE:
Although I was skeptical to do so, I've been inclined to try to use Session.set/Session.get in a localized way. So, the next time I have to do this, I'll just do
Session.set('tenantsBlock' {tenants: [], selectedTenant: 0});
and then just access this variable from within helpers and event maps related to Template.tenantsBlock. That way they all have real time access to the data and they all get re-run when the data changes. Here's what I converted this script into (sorry these are both so large):
(function()
{
Template.tenantsBlock.created = Template.tenantsBlock.destroyed =function()
{
_Utils.setSession('tenantsBlock', {
tenants: [],
selectedTenant: 0
})
};
Template.tenantsBlock.tenantsList = function()
{
var localContext = Session.get('tenantsBlock');
localContext.tenants = [];
var property = $properties.findOne({userId: Meteor.userId(), propertyId: Session.get('property')});
var tenancies = _Utils.resolveTenancies(property, true, null, true);
for(var i = 0; i < tenancies.length; i++)
{
if(tenancies[i].tenancyId == Session.get('tenancy'))
{
localContext.tenants = localContext.tenants.concat(tenancies[i].otherTenants, tenancies[i].primaryTenant);
break;
}
}
localContext.tenants[localContext.selectedTenant].selected = 'Selected';
Session.set('tenantsBlock', localContext);
return localContext.tenants;
};
Template.tenantsBlock.onlyOneTenant = function()
{
var localContext = Session.get('tenantsBlock');
return localContext.tenants.length > 1 ? '' : 'OneChild';
};
Template.tenantsBlock.phoneNumber = function()
{
var localContext = Session.get('tenantsBlock');
for(var i = 0; i < localContext.tenants[localContext.selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(localContext.tenants[localContext.selectedTenant].details[i].key) == 'Phone')
return localContext.tenants[localContext.selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.emailAddress = function()
{
var localContext = Session.get('tenantsBlock');
var selectedTenantDetails = localContext.tenants[localContext.selectedTenant].details;
for(var i = 0; i < selectedTenantDetails.length; i++)
if(_Utils.getDynamicContactIconClass(selectedTenantDetails[i].key) == 'Mail')
return selectedTenantDetails[i].value;
return null;
};
Template.tenantsBlock.addedDate = function()
{
var localContext = Session.get('tenantsBlock');
return _Utils.timeToDateString(localContext.tenants[localContext.selectedTenant].created);
};
Template.tenantsBlock.events({
'click .Name': function(e, template)
{
var localContext = Session.get('tenantsBlock');
var _this = e.currentTarget;
var tenantName = _this.innerHTML;
for(var i = 0; i < localContext.tenants.length; i++)
{
if(localContext.tenants[i].name == tenantName)
{
localContext.tenants[i].selected = 'Selected';
localContext.selectedTenant = i;
}
else
{
localContext.tenants[i].selected = '';
}
}
Session.set('tenantsBlock', localContext);
}
})
})();
You'll have to overcome the old-school way of doing it :) Meteor is a lot simpler than you think. A good rule of thumb is that if you're using jQuery to manipulate any DOM elements, you're probably doing it wrong. Additionally, if you're accessing any data without using the collection API, you'd better have good reason to do so.
In your case, you don't need to code up any manual dependencies at all. Manual dependencies are rarely needed in most Meteor applications.
The first thing you need to do is put all your tenants inside a Meteor.Collection, which will make them easier to work with.
Tenants = new Meteor.Collection("tenants");
Your tenantsBlock template should look something like this (modulo some different html elements):
<template name="tenantsBlock">
<ol>
{{#each tenants}}
<li class="name {{selected}}">
<span>Primary Tenant: {{primaryTenant}}</span>
<span>Other Tenants: {{otherTenants}}</span>
<span>Phone Number: {{phoneNumber}}</span>
<span>Email Address: {{emailAddress}}</span>
<span>Added Date: {{addedDate}}</span>
</li>
{{/each}}
</ol>
</template>
Each document in Tenants should look something like the following:
{
primaryTenant: "Joe Blow",
otherTenants: "Mickey Mouse, Minnie Mouse",
phoneNumber: "555-234-5623",
emailAddress: "joe.blow#foo.com",
addedDate: "2005-10-30T10:45Z"
}
Then, all the code you would need is just for the selection/deselection, and you can delete everything else:
Template.tenantsBlock.tenants = function() {
return Tenants.find();
};
Template.tenantsBlock.selected = function() {
return Session.equals("selectedTenant", this._id);
};
Template.tenantsBlock.events({
'click .name': function(e) {
Session.set("selectedTenant", this._id);
}
});
Once again, I reiterate that you should never be doing DOM manipulations with Javascript when using Meteor. You just update your data and your templates will reactively update if everything is done correctly. Declare how you want your data to look, then change the data and watch the magic.
Meteor has really evolved since I posted this back in 2013. I thought
I should post a modern, superior method.
For a while now you've been able to create a ReactiveVar and now you can append those directly to templates. A ReactiveVar, similar to Session, is a reactive data store. ReactiveVar, however, holds only a single value (of any type).
You can add ReactiveVar to the client side of your project by running this in your terminal from your app's root directory:
$meteor add reactive-var
This javascript shows how you can pass the variable between the template's onCreated, onRendered, onDestroyed, events and helpers.
Template.myTemplate.onCreated = function() {
// Appends a reactive variable to the template instance
this.reactiveData = new ReactiveVar('Default Value');
};
Template.myTemplate.events({
'click .someButton': (e, template) => {
// Changes the value of the reactive variable for only this template instance
template.reactiveData.set('New Value');
},
});
Template.myTemplate.helpers({
theData: () => {
// Automatically updates view when reactive variable changes
return Template.instance().reactiveData.get();
},
});
This is superior for a few reasons:
It scopes the variable only to a single template instance. Particularly useful in cases where you might have a dozen instances of a template on a page, all requiring independent states.
It goes away when the template goes away. Using ReactiveVar or Session variables you will have to clear the variable when the template is destroyed (if it is even destroyed predictably).
It's just cleaner code.
Bonus Points: See ReactiveDict for cases in which you have many instances of a template on a page at once, but need to manage a handful of reactive variables and have those variables persist during the session.

Resources