I want to test if a function in a module generated something in document but I am having trouble defining the document variable. I have simulated it jsdom but I am unable to define it in module.
In my actual project, I use the document, window and MathJax library globally and I don't want to pass it to classes through the constructor unless there is no other way.
Example of a class I want to test:
// example.mjs
export class Example {
createElement() {
document.createElement("div")
}
}
Test file:
import {Example} from './example.mjs';
import {JSDOM} from "jsdom";
describe("Example", function () {
it('should create div', function () {
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
document = dom.window.document; //is it possible to make this defined in all modules
new Example().createElement() // document is not defined
});
})
My question is, is it possible to somehow make the document variable defined, so that it can be referenced in the Example class or do I have to pass it in the constructor?
Related
I'm currently learning Web Components and I wonder if it is possible to have a Component load its own data dynamically, similar to how <img> does from its src attribute, i.e. something like this:
<my-fancy-thingy src='/stuff.json'></my-fancy-thingy>
Obviously this functionality would be useful if stuff.json could be rather large, so it should also be possible to make use of the browser's caching mechanism so the referenced file doesn't get reloaded every time we request the page, unless changed.
Can this be done?
Sure, take inspiration from <load-file> See Dev.to Post
/*
defining the <load-file> Web Component,
yes! the documenation is longer than the code
License: https://unlicense.org/
*/
customElements.define("load-file", class extends HTMLElement {
// declare default connectedCallback as sync so await can be used
async connectedCallback(
// attach a shadowRoot if none exists (prevents displaying error when moving Nodes)
// declare as parameter to save 4 Bytes: 'let '
shadowRoot = this.shadowRoot || this.attachShadow({mode:"open"})
) {
// load SVG file from src="" async, parse to text, add to shadowRoot.innerHTML
shadowRoot.innerHTML = await (await fetch(this.getAttribute("src"))).text()
// append optional <tag [shadowRoot]> Elements from inside <load-svg> after parsed <svg>
shadowRoot.append(...this.querySelectorAll("[shadowRoot]"))
// if "replaceWith" attribute
// then replace <load-svg> with loaded content <load-svg>
// childNodes instead of children to include #textNodes also
this.hasAttribute("replaceWith") && this.replaceWith(...shadowRoot.childNodes)
}
})
Change .text() to .json() and it parses JSON files
Caching can be done by storing the String in localStorage (but a 5MB limit total, I think):
https://en.wikipedia.org/wiki/Web_storage
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
You need to come up with "data has changed" strategy; as the Client has no clue when data actually was changed. Maybe an extra semaphore file/endpoint that provides info if the (large) JSON file was changed.
This works like a charm
export class MonElement extends HTMLElement {
constructor(){
super();
this.attachShadow({mode:'open'});
(...)
this.shadowRoot.appendChild(atemplate);
}
connectedCallback(){...}
static get observedAttributes(){
return ['src'];
}
attributeChangedCallback(nameattr,oldval,newval)
{
if (nameattr==='src') {
this[nameattr]=newval;
here do the fetch for the src value which is newval then update what you got in the innerdom
}
(...)
Consider a following simple example, file country.gs
class Country { }
and file file subcountry.gs
class SubCountry extends Country{ }
function test(){}
Trying to run test() I get
ReferenceError: Country is not defined
If I join files or change loading order, it works fine.
Apparently, I don't want to be dependent on file load order, also clasp changes on push(sorting alphabetically), so it's definitely not a good way to rename files in order they should be compiled.
Is there an appropriate solution for this?
Example:
https://script.google.com/d/1Pipt3YN1FBGkbRRT2PyCHhugd-Xrv3zctIWYwX-cGnAjXfDckwOk7bJh/edit?usp=sharing
As written in the documentation,
This arrangement is identical to how browsers handle multiple tags in one HTML file.
Each file is like a new <script>file content </script> tag and they're added in the order they appear in Apps script editor. This is a problem only when you're using global variables. It's explicitly discouraged to use global variables.
Caution: It's not best practice to rely on a specific file parse order to avoid this issue. The sequence of script file parsing can change if script files are copied, removed, renamed, or otherwise rearranged. It's better to remove any global variable dependency on function calls if possible.
Classes are infact "special functions". You can always enclose the Class in a local scope and call, when needed as recommended in the documentation.
Snippet:
Just moving the calling function to local scope should work
/*subcountry.gs*/
function test(){
/*local scope*/class SubCountry extends Country{ }
}
To avoid declaring class in global scope as well:
/*country.gs*/
var Country;
function main(){
if (Country == undefined) Country = class Country { }
return Country;
}
/*subcountry.gs*/
function test(){
/*initialize class Country*/main()
/*local scope*/class SubCountry extends Country{ }
}
Building off the answer posted by TheMaster and the Bruce Mcpherson article shared by Alan Wells, you could try implementing your own require() function.
/* Code.gs */
function test() {
const SubCountry = require("SubCountry");
const x = new SubCountry();
}
/* SubCountry.gs */
function SubCountry() {
const Country = require("Country");
return class SubCountry extends Country {};
}
/* Country.gs */
function Country() {
return class Country {};
}
/* Require.gs */
function require(moduleName) {
const modules = {
Country: Country,
SubCountry: SubCountry,
};
return modules[moduleName]();
}
Alternatively, you could apply a more direct approach without the use of require(), but I find this to be slightly less intuitive.
/* Code.gs */
function test() {
const x = new (SubCountryClass())();
}
/* SubCountry.gs */
function SubCountryClass() {
return class SubCountry extends CountryClass() {};
}
/* Country.gs */
function CountryClass() {
return class Country {};
}
All files above, for both approaches, are intentionally presented and loaded in an order that would cause a ReferenceError if declaring the classes globally. So this should be fully independent of load order.
I'll probably go with one of solutions described here
TypeScript classes order in Google AppScript Project
using clasp and it's filePushOrder option
{
"scriptId":"1Pipt3YN1FBGkbRRT2PyCHhugd-Xrv3zctIWYwX-cGnAjXfDckwOk7bJh",
"filePushOrder": [
"country.gs",
"subcountry.gs"
]
}
Author example
https://github.com/PopGoesTheWza/clasp-filePushOrder
I enforces me to use clasp, but at least it's easy to maintain.
I have a Methode from an API. It returns a promise which resolves to an $ctrl(?) object. This objects should contain a measurement and will be updated whenever it receive a new data.
getMeasurements.latest(filter) //only a object to filter through all measurements
.then(function (latestMeasurement) {
$ctrl.latestMeasurement = latestMeasurement;
});
My problem is that I don't know how to work with this data or display it in my html file. How does $ctrl work?
Here the documentation of the API
$ctrl is the view model object in your controller. This $ctrl is a name you choose (vm is another most common name), if you check your code you can see the definition as $ctrl = this;, so basically its the this keyword of the controller function.
So now if you are using $ctrl.latestMeasurement = 'someValue', then its like you are adding a property latestMeasurement to controller function.
Now how to use it in HTML?
To access the latestMeasurement property in HTML your code must have <h1>{{$ctrl.latestMeasurement}}</h1> (H1 tag is just an example.)
Here $ctrl is different from what I explained above on controller part. Here $ctrl is the value used for controllerAs property of the controller. But $ctrl is the default value of the controllerAs property, so your code may not have the controllerAs property defined, so Angular will take default value $ctrl in HTML.
This is where most people gets confused. So let me explain,
Assume in your new controller you have declared your this keyword to variable vm, and you set your controllerAs property to myCtrl, i.e;
controllerAs: 'myCtrl' while defining controller properties.
var vm = this; in your controller function.
In this case in js you have to use vm for setting values, and in HTML you have to use myCtrl. For example,
in JS controller function vm.test = 'Hello world';
in HTML <span ng-bind="myCtrl.test"></span>
The result Hello world will be displayed in your page.
Why $ctrl and not $scope?
The view model object model concept is introduced in AngularJS 1.5, it is actually part of migrating to Angular 2 where $scope no longer exsist. So in 1.5 they introduced new approch but did not removed $scope completely.
Hope the answer helped.
For basic Javascript concepts you can see http://javascriptissexy.com/16-javascript-concepts-you-must-know-well/
For more detailed AngularJS $ctrl concept you can see https://johnpapa.net/angularjss-controller-as-and-the-vm-variable/
I suppose you are toking about this.
In this case, the
$ctrl.latestMeasurement
can means:
$ctrl, the controller where you are running this code. You can change it by $scope for example, and get the same result.
latestMeasurement, the variable where you want to store the last value of the measurement.
To explain my point of view let see the code below
<div ng-app="MeasurementApp">
<div ng-controller="MeasurementController">
<h1>{{latestMeasurement2}}</h1>
</div>
</div>
There you can see a simple angularjs app that shows a variable called latestMeasurement2 in a div and its controller called MeasurementController. Then, to display the value let check your code.
angular.module('MeasurementApp', [])
// creating the controller
.controller('MeasurementController', function(c8yMeasurements, $scope) {
// creating the variable and let it empty by now.
$scope.latestMeasurement2 = "";
// Your code
var filter = {
device: 10300,
fragment: 'c8y_Temperature',
series: 'T'
};
var realtime = true;
c8yMeasurements.latest(filter, realtime)
.then(function (latestMeasurement) {
// The latestMeasurement is where the measurement comes
// Here we just assign it into our $scope.latestMeasurement2
$scope.latestMeasurement2 = latestMeasurement;
});
});
As the documentation says
// $scope.latestMeasurement2 will be updated as soon as a new measurement is received.
$scope.latestMeasurement2 = latestMeasurement;
Hope this helps!
I'm new to angularjs trying to test HTML elements but my test gets failed.Actually,i want to test the HTML element's value or text which it contain.
can anyone suggest me how can i achieve it?
here is my controllerspec:
describe('myAppCtrl', function() {
var scope, controller, httpBackend;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller, $httpBackend) {
scope = $rootScope;
controller = $controller;
httpBackend = $httpBackend;
}));
it('should show the text of element',function() {
expect($('#testtext')).toBe('First Angular JS App');
});
});
What you're doing above is with Karma for unit testing, and is only loading up the modules you've specified. In this case it loads the module myApp, but myApp doesn't have any reference to HTML and doesn't load the DOM itself.
If you want to test HTML elements directly, take a look at end to end testing using Protractor:
https://docs.angularjs.org/guide/e2e-testing
I created a directive with a method that should be called from other elements that are not part of the directive. However it looks like this method is not exposed.
Some example jade code to clarify:
//- a controller for the view itself
div(ng-controller="someController")
//- this is part of the view itself, not within the directive
div(ng-repeat="element in elements")
div(ng-click="methodFromDirective(element)") click element {{$index}} to trigger directive
//- this is the directive
div(some-directive)
The someController isn't too important here I think. It has methods but NOT the methodFromDirective(element) one. The methodFromDirective(element) is a method that exists only in the directive.
If I make a directive and put some logging on creation I can clearly see it's created. However the methodFromDirective(element) method isn't exposed so the calls aren't properly triggered.
The methodFromDirective(element) itself will only work on elements from within the directive's template.
some coffeescript to show the definition of the the directive (ignore indentation errors here):
'use strict'
define [], () ->
someDirective = () ->
restrict: 'A'
scope: {
show: '='
}
transclude: false
templateUrl: 'someTemplateHere.html'
controller = ($scope) ->
# exposing the method here
$scope.methodFromDirective(element)->
$scope.theMethod element
link = (scope, element, attr) ->
# this is logged
console.log "init someDirective"
# triggering this method form outside fails
scope.theMethod = (element)->
console.log "method triggered with element", JSON.stringify(element)
I found my issue.
From the angularJS documentation on directives I was looking into the transclude option since that states:
What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.
I combined transclude=false with the controller function since that exposes the method, again from docs:
Savvy readers may be wondering what the difference is between link and controller. The basic difference is that controller can expose an API, and link functions can interact with controllers using require.
However what I missed completely was that I isolated scope within my directive. From docs:
What we want to be able to do is separate the scope inside a directive from the scope outside, and then map the outer scope to a directive's inner scope. We can do this by creating what we call an isolate scope. To do this, we can use a directive's scope option:
So even if you use transclude=false and the controller function you'll still fail to expose methods if you use isolated scope! Lesson learned!
While figuring out what went wrong I also made a fiddle for better understanding: http://jsfiddle.net/qyBEr/1/
html
<div ng-app="directiveScopeExample">
<div ng-controller="Ctrl1">
<p>see if we can trigger a method form the controller that exists in the directive.</p>
<ul>
<li>Method in Controller</li>
<li>Method in Directive</li>
</ul>
<simple-directive/>
</div>
</div>
javascript
angular.module('directiveScopeExample', [])
.controller('Ctrl1', function Ctrl1($scope) {
$scope.methodInController = function(){
alert('Method in controller triggered');
};
})
.directive('simpleDirective', function(){
return {
restrict: 'E',
transclude: false,
controller: function($scope){
$scope.methodInDirective = function(){
// call a method that is defined on scope but only within the directive, this is exposed beause defined within the link function on the $scope
$scope.showMessage('Method in directive triggered');
}
}
// this is the issue, creating a new scope prevents the controller to call the methods from the directive
//, scope: {
// title: '#'
//}
, link: function(scope, element, attrs, tabsCtrl) {
// view related code here
scope.showMessage = function(message){
alert(message);
}
},
//templateUrl: 'some-template-here.html'
};
})
Calling private methods inside directive's link function is very simple
dropOffScope = $('#drop_off_date').scope();
dropOffScope.setMinDate('11/10/2014');
where
$('#drop_off_date') - jQuery function
setMinDate() - private function inside directive
You can call directive function even from outer space.
By default the scope on directive is false meaning directive will use the parent's scope instead of creating a new one. And hence any function or model defined in the directive will be accessible in the parent scope. Check out this.
I think your problem can be solved as follows:
angular.module('directiveScopeExample', [])
.controller('Ctrl1', function Ctrl1($scope) {
$scope.methodInController = function(){
alert('Method in controller triggered');
};
})
.directive('simpleDirective', function(){
return {
restrict: 'E',
scope: false,
link: function(scope, element, attrs, tabsCtrl) {
// view related code here
scope.showMessage = function(message){
alert(message);
}
},
//templateUrl: 'some-template-here.html'
};
This approach might be an issue in case you want to create reusable directives and you are maintaining some state/models in your directive scope. But since you are just creating functions without side-effects, you should be fine.