Testing directives with template URL - testing

I'm trying to test an angularJS directive that uses a templateURL. For the life of me I can't get the compiler to actually load the templateURL, even when it has been put into the templateCache. I realize karma preprocesses all the template contents and creates modules for each that preloads the templateCache, but I expected that this would have been equivalent.
Here is a http://jsfiddle.net/devshorts/cxN22/2/ that demonstrates whats going on.
angular.module("app", [])
.directive("test", function(){
return {
templateUrl:"some.html",
replace:true
}
});
//--- SPECS -------------------------
describe("template url test", function() {
var element, scope, manualCompiledElement;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller, $compile, $templateCache){
$templateCache.put("some.html", "<div>hello</div>");
scope = $rootScope.$new();
element = $compile(angular.element('<test></test>'))(scope);
manualCompiledElement = $compile(angular.element($templateCache.get('some.html')))(scope);
scope.$digest();
}));
it("has hello", function() {
expect(element.text()).toContain('hello');
});
it("template cache has contents", function(){
expect(manualCompiledElement.text()).toContain('hello');
});
});
What am I missing?

I realise you no longer necessarily need to know, but it looks to me like there are two problems contributing to this.
The first was pointed out by #Words-Like-Jared. You are defining the directive as restricted to attributes (default) but using it as an element. So you need restrict: 'E'.
The second problem is that your template is never actually retrieved and your compile/link never completes. The request for the contents of the template within the directive are asynchronous so a digest on the root scope is needed to resolve the promise and return them, similar to this answer for another question.
When you perform your manual compilation, the results are not asynchronous and the template is retrieved immediately. Actually the compilation in your manual compilation doesn't do a lot as you are compiling the contents of your template, which doesn't have any directives in.
Now at the end of your beforeEach where you use
$scope.$digest()
you are digesting on the current scope and its children. When you use $rootScope.$digest() or $scope.$apply() you will perform a digest across all scopes. So changing this to
$rootScope.$digest()
// or
$scope.$root.$digest()
in the line after your compilation means both of your tests will now pass. I have updated the fiddle here.

Here is a variation of the solution in coffeescript
expect = chai.expect;
app = angular.module('TM_App')
app.directive "test", ()->
templateUrl:"some.html",
replace :true
describe '| testing | templateUrl',->
element = null
beforeEach ->
module('TM_App')
beforeEach ->
inject ($compile,$rootScope, $templateCache)->
$templateCache.put "some.html", "<div>hello {{name}}</div>"
scope = $rootScope.$new();
element = $compile('<test/>')(scope);
scope.name = 'John'
scope.$digest()
it "has hello", ()->
expect(element.text() ).to.equal 'hello John'
expect(element[0].outerHTML).to.equal '<div class="ng-binding">hello John</div>'

Related

Vue 2 About nextTick

I have read that nextTick allows codes to be executed at the next action. But this does not work in my code, can someone helps me on this? Please correct me. Thanks.
.vue
.....
methods:{
getUserInfo(){
var vm = this
vm.$http.get('/getAuthUser').then((response)=>{
vm.user = response.data
})
Vue.nextTick(()=>{
vm.$http.get('/getShop/'+vm.user.id).then((response)=>{
vm.shop = response.data.data.shop
})
})
},
}
.....
{{user.id}} does work. where this gives me the following error:
GET http://localhost:8000/getShop/undefined 404 (Not Found)
EDIT#1
if i do something like this it works but this should not be the right way to do in my opinion.
.....
methods:{
getUserInfo(){
var vm = this
vm.$http.get('/getAuthUser').then((response)=>{
vm.user = response.data
vm.$http.get('/getShop/'+vm.user.id).then((response)=>{
vm.shop = response.data.data.shop
})
})
},
}
.....
EDIT#2
If I do something like this it wont work coz vm.user.id is not set.
.....
methods:{
getUserInfo(){
var vm = this
vm.$http.get('/getAuthUser').then((response)=>{
vm.user = response.data
})
vm.$http.get('/getShop/'+vm.user.id).then((response)=>{
vm.shop = response.data.data.shop
})
},
}
.....
I think your understanding of what nextTick does is incorrect. If you read the documentation, it says that the callback you pass to the nextTick function will be executed after the next DOM update.
Let's say you have a property that determines whether an element exists or not in the DOM with a v-if directive. If you change the value of the property so that the element exists in the DOM, you might have to wait for Vue to process the change and update the DOM before you can grab a reference of that element, for example. In that case, you should use Vue.nextTick to make sure by the time you want to query the DOM to get that element, it actually exists.
Your scenario doesn't have anything to do with the DOM.
You have 2 asynchronous HTTP calls that you want to execute one after another, because the second relies on the result of the first. Your original implementation and third one (EDIT#2) are flaky because you don't make sure the first HTTP request is complete before firing the second one, which explains why you get errors about vm.user.id not being set.
Your second implementation (EDIT#1) is more correct because the second HTTP request is fired after the first one completes. Still, I'd suggest a minor modification:
getUserInfo() {
vm.$http.get('/getAuthUser')
.then(response => {
vm.user = response.data;
return vm.$http.get('/getShop/' + vm.user.id);
}).then(response => {
vm.shop = response.data.data.shop;
});
}
The first callback returns a Promise which result is fed into the second then call. I like this approach because it avois having nested thens. I would also suggest you to read the MDN docs on Promises.

How to use store.filter / store.find with Ember-Data to implement infinite scrolling?

This was originally posted on discuss.emberjs.com. See:
http://discuss.emberjs.com/t/what-is-the-proper-use-of-store-filter-store-find-for-infinite-scrolling/3798/2
but that site seems to get worse and worse as far as quality of content these days so I'm hoping StackOverflow can rescue me.
Intent: Build a page in ember with ember-data implementing infinite scrolling.
Background Knowledge: Based on the emberjs.com api docs on ember-data, specifically the store.filter and store.find methods ( see: http://emberjs.com/api/data/classes/DS.Store.html#method_filter ) I should be able to set the model hook of a route to the promise of a store filter operation. The response of the promise should be a filtered record array which is a an array of items from the store filtered by a filter function which is suppose to be constantly updated whenever new items are pushed into the store. By combining this with the store.find method which will push items into the store, the filteredRecordArray should automatically update with the new items thus updating the model and resulting in new items showing on the page.
For instance, assume we have a Questions Route, Controller and a model of type Question.
App.QuestionsRoute = Ember.Route.extend({
model: function (urlParams) {
return this.get('store').filter('question', function (q) {
return true;
});
}
});
Then we have a controller with some method that will call store.find, this could be triggered by some event/action whether it be detecting scroll events or the user explicitly clicking to load more, regardless this method would be called to load more questions.
Example:
App.QuestionsController = Ember.ArrayController.extend({
...
loadMore: function (offset) {
return this.get('store').find('question', { skip: currentOffset});
}
...
});
And the template to render the items:
...
{{#each question in controller}}
{{question.title}}
{{/each}}
...
Notice, that with this method we do NOT have to add a function to the store.find promise which explicitly calls this.get('model').pushObjects(questions); In fact, trying to do that once you have already returned a filter record array to the model does not work. Either we manage the content of the model manually, or we let ember-data do the work and I would very much like to let Ember-data do the work.
This is is a very clean API; however, it does not seem to work they way I've written it. Based on the documentation I cannot see anything wrong.
Using the Ember-Inspector tool from chrome I can see that the new questions from the second find call are loaded into the store under the 'question' type but the page does not refresh until I change routes and come back. It seems like the is simply a problem with observers, which made me think that this would be a bug in Ember-Data, but I didn't want to jump to conclusions like that until I asked to see if I'm using Ember-Data as intended.
If someone doesn't know exactly what is wrong but knows how to use store.push/pushMany to recreate this scenario in a jsbin that would also help too. I'm just not familiar with how to use the lower level methods on the store.
Help is much appreciated.
I just made this pattern work for myself, but in the "traditional" way, i.e. without using store.filter().
I managed the "loadMore" part in the router itself :
actions: {
loadMore: function () {
var model = this.controller.get('model'), route = this;
if (!this.get('loading')) {
this.set('loading', true);
this.store.find('question', {offset: model.get('length')}).then(function (records) {
model.addObjects(records);
route.set('loading', false);
});
}
}
}
Since you already tried the traditional way (from what I see in your post on discuss), it seems that the key part is to use addObjects() instead of pushObjects() as you did.
For the records, here is the relevant part of my view to trigger the loadMore action:
didInsertElement: function() {
var controller = this.get('controller');
$(window).on('scroll', function() {
if ($(window).scrollTop() > $(document).height() - ($(window).height()*2)) {
controller.send('loadMore');
}
});
},
willDestroyElement: function() {
$(window).off('scroll');
}
I am now looking to move the loading property to the controller so that I get a nice loader for the user.

How to structure a complex web app with RequireJS

I saw there is somes questions related to mine (like this interesting one), but what I wonders is how to do it correctly, and I couldn't find it via the others questions or the RequireJS documentation.
I'm working on a quite heavy web application that will run in only one html page.
Before RequireJS, I used to do a lot of JS modules with public methods and connecting them via the on event on the Dom READY method, like this :
var DataList = function () {
this.base = arguments[0];
this.onUpdate = function (event) { ... }
}
$(function () {
var dataList = {}; DataList.apply(dataList, [$('#content')]);
$('table.main', dataList.base).on ('update', dataList.onUpdate);
});
With RequireJS, I can easily see that I can split DataList and all others classes like this on individual files, but what about the $(function () {}); part?
Can I still keep it this way, but instead of the DOM ready function of jQuery, I put the events on the main function() of the RequireJS, when my primary libs are loaded?
Or do I have to change the way I create JS "classes", to include a init function maybe, that will be called when I do a, for example :
require(['Datalist'], function(dataList) {
dataList.init($('#content'));
});
What annoys me the most is that since I have only one html file, I'm afraid the require() will have to load a huge list of files, I'd prefer it to load just libs that, them, would load sub libs required to work.
I don't know, the way of thinking with RequireJS lost me a bit :/
How would you do?
"Can I still keep it this way, but instead of the DOM ready function of jQuery, I put the events on the main function() of the RequireJS, when my primary libs are loaded?"
If you separate the functions or 'classes' into modules then you can use the RequireJS domReady function:
require(['module1'], function(module1) {
domReady(function(){
// Some code here ftw
})
});
The benefit here is the domReady function will allow downloading of the modules instantly but won't execute them until your DOM is ready to go.
"Or do I have to change the way I create JS "classes", to include a init function maybe, that will be called when I do a, for example"
You won't need to change the way you interact with your code this way, but you can probably improve it. In your example I would make DataList a module:
define(function(require) {
var $ = require('jquery');
var DataList = function () {
this.base = arguments[0];
};
DataList.prototype.onUpdate = function() {
};
return DataList;
});
require(['data-list'], function(DataList) {
var data = {};
// Call DataList with new and you won't need to set the context with apply
// otherwise it can be used exactly as your example
new DataList(data);
});
"What annoys me the most is that since I have only one html file, I'm afraid the require() will have to load a huge list of files, I'd prefer it to load just libs that, them, would load sub libs required to work."
Make your code as modular as you want/can and then use the optimiser to package it into one JS file.

Testing model binding in Backbone JS with Jasmine

I have a view that contains a model. The view listens for an event from the model and will perform an action once the event is triggered. Below is my code
window.Category = Backbone.Model.extend({})
window.notesDialog = Backbone.View.extend({
initialize: function() {
this.model.bind("notesFetched", this.showNotes, this);
},
showNotes: function(notes) {
//do stuffs here
}
})
I want to test this using Jasmine and below is my test (which doesn't work)
it("should show notes", function() {
var category = new Category;
var notes_dialog = new NotesDialog({model: category})
spyOn(notes_dialog, "showNotes");
category.trigger("notesFetched", "[]");
expect(notes_dialog.showNotes).toHaveBeenCalledWith("[]");
})
Does anyone know why the above test doesn't work? The error I get is "Expected spy showNotes to have been called with [ '[]' ] but it was never called."
I was doing something similar where I had a view, but I couldn't get the spy to work properly unless I added it to the prototype, and before I created the instance of the view.
Here's what eventually worked for me:
view.js
view = Backbone.View.extend({
initialize: function(){
this.collection.bind("change", this.onChange, this);
},
...
onChange: function(){
console.log("Called...");
}
});
jasmine_spec.js
describe("Test Event", function(){
it("Should spy on change event", function(){
var spy = spyOn(view.prototype, 'onChange').andCallThrough()
var v = new view( {collection: some_collection });
// Trigger the change event
some_collection.set();
expect(spy).toHaveBeenCalled()
});
});
I would test initially with the toHaveBeenCalled() expectation and change to the toHaveBeenCalledWith() after you get that working...
Update 5/6/2013: Changed update() to set()
Try to amend your existing test code as follows:
it("should show notes", function() {
var category = new Category;
spyOn(NotesDialog.prototype, "showNotes");
var notes_dialog = new NotesDialog({model: category})
category.trigger("notesFetched", "[]");
expect(notes_dialog.showNotes).toHaveBeenCalledWith("[]");
})
In your original code, the instance of the method you are calling is one defined in the bind closure, whereas the one you are spying on is in the notes_dialog instance. By moving the spy to the prototype, you are replacing it before the bind takes place, and therefore the bind closure encapsulates the spy, not the original method.
Using a spy means to replace the function you spying on. So in your case you replace the bind function with the spy, so the internal logic of the original spy will not call anymore. And thats the right way to go cause you dont wanna test that Backbones bind is work but that you have called bind with the specific paramaters "notesFetched", this.showNotes, this.
So how to test this. As you know every spy has the toHaveBeenCalledWith(arguments) method. In your case it should looks like this:
expect(category.bind).toHaveBeenCalledWith("notesFetched", category. showNotes, showNotes)
So how to test that trigger the "notesFetched" on the model will call your showNotes function.
Every spy saves the all parameters he was called with. You can access the last one with mostRecentCall.args.
category.bind.mostRecentCall.args[1].call(category.bind.mostRecentCall.args[2], "[]");
expect(notes_dialog.showNotes).toHaveBeenCalledWith("[]");
mostRecentCall.args[1] is the the second argument in your bind call (this.showNotes). mostRecentCall.args[2] is the the third argument in your bind call (this).
As we have test that bind was called with your public method showNotes, you can also call the your public method showNotes directly, but sometimes the passed arguments can access from outside so you will use the shown way.
Your code looks fine, except do you have the test wrapped in a describe function, as well as an it function?
describe("show notes", function(){
it("should show notes", function(){
// ... everything you already have here
});
});
Total guess at this point, but since you're not showing the describe function that's all I can think it would be. You must have a describe block for the tests to work, if you don't have one.
You are pretty close ;)
spyOn replaces the function with your spy and returns you the spy.
So if you do:
var dialog_spy = spyOn(notes_dialog, "showNotes");
category.trigger("notesFetched", "[]");
expect(dialog_spy).toHaveBeenCalledWith("[]");
should work just fine!

Safari Extension Questions

I'm in the process of building my first Safari extension--a very simple one--but I've run into a couple of problems. The extension boils down to a single, injected script that attempts to bypass the native feed handler and redirect to an http:// URI. My issues so far are twofold:
The "whitelist" isn't working the way I'd expect. Since all feeds are shown under the "feed://" protocol, I've tried to capture that in the whitelist as "feed://*/*" (with nothing in the blacklist), but I end up in a request loop that I can't understand. If I set blacklist values of "http://*/*" and "https://*/*", everything works as expected.
I can't figure out how to access my settings from my injected script. The script creates a beforeload event handler, but can't access my settings using the safari.extension.settings path indicated in the documentation.
I haven't found anything in Apple's documentation to indicate that settings shouldn't be available from my script. Since extensions are such a new feature, even Google returns limited relevant results and most of those are from the official documentation.
What am I missing?
UPDATE
So I'm hoping that the documentation is incomplete because it's borderline abysmal, but I've learned a bit about settings. It turns out that, from injection scripts, the SafariExtensionSettings object isn't available. Injection scripts only have access to the SafariContentExtension object (which isn't useful at all), but it's aliased in exactly the same manner (safari.extension.settings)--bad idea, IMO. As stated in the injection script documentation:
Important: When you use safari.extension from within an injected script, you are not addressing the SafariExtension class. You are addressing the SafariContentExtension class.
You have to use the messaging system to talk to a global HTML file which has access to the settings. It's kind of loopy, but I have a message being sent to a global.html file that retrieves the settings and will send a message back to my injection script as soon as I figure out how to go about doing that.
Since I'm doing all of my work before the document loads, all of the methods I've found to send message back rely on a page object that I don't have.
Like everyone else at this point, I'm still climbing the learning curve, but here's how I've handled this problem:
I have a simple extension with no chrome and one injected end script (script.js). For the purpose of loading settings I've added a simple global page (proxy.html). When script.js is injected, it sends a getSettings message to proxy.html. proxy.html responds with a setSettings message, and script.js continues initialization.
The most helpful page I've found in the docs on this topic is Messages and Proxies.
proxy.html:
<!doctype html>
<html lang="en">
<head>
<script type="text/javascript">
safari.application.addEventListener( "message", function( e ) {
if( e.name === "getSettings" ) {
e.target.page.dispatchMessage( "setSettings", {
sort_keys: safari.extension.settings.getItem( "sort_keys" )
} );
}
}, false );
</script>
</head>
<body></body>
</html>
script.js:
( function() {
var settings, init = function() {
// do extension stuff
};
// listen for an incoming setSettings message
safari.self.addEventListener( "message", function( e ) {
if( e.name === "setSettings" ) {
settings = e.message;
init();
}
}, false );
// ask proxy.html for settings
safari.self.tab.dispatchMessage( "getSettings" );
}() )
EDIT: like you said in your initial post update, the injected script doesn't have the same kind of access that a global HTML page would have. This is my working solution, imagine you want to know the value of setting "foo" in the injected script:
Injected script code:
function getMessage(msgEvent) {
if (msgEvent.name == "settingValueIs")
alert("Value for asked setting is: " + msgEvent.message);
}
safari.self.tab.dispatchMessage("getSettingValue", "foo"); // ask for value
safari.self.addEventListener("message", getMessage, false); // wait for reply
Global HTML code:
function respondToMessage(messageEvent) {
if (messageEvent.name == "getSettingValue") {
// getItem("foo");
var value = safari.extension.settings.getItem(messageEvent.message);
// return value of foo to injected script
safari.application.activeBrowserWindow.activeTab.page.dispatchMessage("settingValueIs", value);
}
}
safari.application.addEventListener("message",respondToMessage,false);
Hope this helps !
Initial post: I'm having the same 2nd problem as you, I can't access my settings (or secureSettings) from an injected script. In my case the script is loaded after page load, but even that way I can't use safari.extension.settings.
The only way it works is with a toolbar/button, the HTML behind that element can getItem and setItem as expected.
My conclusion is that, for some reason, injected scripts can't access settings (actually, they don't even seem to have access to the safari element). Bug or intended feature, that's left to figure out.
It took me several days, but I think I found a workable solution using the canLoad() messaging method. My injection script retrieves settings by calling the global HTML page like this:
settings = safari.self.tab.canLoad( event );
My global HTML file, in turn, returns those settings as:
settings = {
'setting1': safari.extension.settings.getItem( 'setting1' )
}
msgEvent.message = settings;
It's still a bit more "hacky" than I'd like. I can't seem to simply return the settings object itself, so I have to compile a new object by retrieving each setting manually. Not ideal, but it does seem to be effective.
run into the same problem, but the answer is easier than you can imagine: include the script in your global html.
<!DOCTYPE HTML>
<script type="text/javascript" src="cleanup.js"></script>
<script>
…
</script>
then you can access the settings as described in documentation safari.extension.settings.myKey
you can also upvote #Travis, because I got the idea from his post
//EDIT:
actually I don't really know whats wrong. Calling the settings as the first command works, but not at a later time. Additionally it seems to corrupting my complete script after the 2. injection. Need verification if it's only in my (difficult?) script.
//EDIT2:
now I got it to work to get back the settings object via dispatchMessage()
in your injected.js
function gotSettings(msgEvent) {
if (msgEvent.name === "SETTINGS") {
setts = msgEvent.message;
alert(setts.mySetting1);
// run the programm
}
}
safari.self.addEventListener("message", gotSettings, false);
safari.self.tab.dispatchMessage("getSettings");
and in global.html
switch (event.name) {
case "getSettings":
// send the settings data
event.target.page.dispatchMessage("SETTINGS", safari.extension.settings);
relying on this apple documentation