Can we directly unit test HTML embedded JavaScript functions? - cypress

Suppose we have a webpage with embedded JavaScript as in index.html as follows:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta charset="utf-8" />
<title>Cypress - Testing the Tester</title>
<script>/*<![CDATA[*/
function foo() {
return "bar";
}
function outputMessage(message) {
document.querySelector("output").innerHTML += message + "<br />";
}
window.addEventListener("pageshow", function(){
outputMessage(foo());
});
/*]]>*/</script>
</head>
<body>
<h1>Cypress - Testing the Tester</h1>
<output></output>
</body>
</html>
On page show (e.g. load) this inserts the string "bar" inside <output></output>.
If my_cypress_spec.js is:
describe('The Home Page Tests', () => {
it('Test HTML embedded JavaScript function', () => {
// Assumes, e.g. "baseUrl": "http://127.0.0.1:5500" set in cypress.json
cy.visit('index.html');
expect(foo()).to.equal("bar");
});
});
then in the Cypress test runner I get a "ReferenceError ... foo is not defined". That is, it appears Cypress doesn't recognize the foo() function.
Is there a way for Cypress to directly unit test the foo() function? That is, without relying on ES6/ES2015 export/imports (where the above is refactored to put foo() in its own JavaScript file where it is exported and my_cypress_spec.js imports this)?
Attempting #Fody's suggestion my my_cypress_spec.js becomes:
describe('The Home Page Tests', () => {
it('Test HTML embedded JavaScript function', () => {
// Assumes, e.g. "baseUrl": "http://127.0.0.1:5500" set in cypress.json
cy.visit('index.html');
cy.window().then(win => {
expect(win.foo()).to.eq('bar');
});
});
});
and I get the error "TypeError ... win.foo is not a function".

I'm not sure how well this will apply if you are using a more complex app, e.g created with Angular or React. Those frameworks have specialized test harnesses for unit testing.
For the vanilla JS example you gave, it's quite easy since foo() gets attached to the window of the AUT.
cy.window().then(win => {
expect(win.foo()).to.eq('bar')
})

Fody's answer works. So, in full my_cypress_spec.js can become something like:
describe('The Home Page Tests', () => {
it('Test HTML embedded JavaScript function', () => {
// Assumes, e.g. "baseUrl": "http://127.0.0.1:5500" set in cypress.json
cy.visit('index.html');
cy.window().then(win => {
expect(win.foo()).to.eq('bar');
});
});
});
However, the reason I was getting a "TypeError ... win.foo is not a function" was because in my index.html on my machine (not as originally posted above) I had an opening <script type="module">. Changing that back to <script> made the test pass.
So for completeness I'll tag this as The Answer while noting the insights come from #Fody and #jonrsharpe.

Related

How can we pass parameters to Alpine.data in Alpine.js v3?

I am now using the newest version of Alpine which is v3.
Making reusable components needs to be registered using the Alpine.data.
This is the alpinejs.js
import Alpine from 'alpinejs'
import form from './components/form'
window.Alpine = Alpine
Alpine.data('form', form)
Alpine.start()
This is what I have in the components/form.js
export default (config) => {
return {
open: false,
init() {
console.log(config)
},
get isOpen() { return this.open },
close() { this.open = false },
open() { this.open = true },
}
}
This is the html part:
<div x-data="form({test:'test'})"></div>
This is the error I get in the console:
Any idea how to pass parameters to Alpine.data?
I stumbled over this question, searching for an answer but figured it out now. Maybe its still usefull to someone...
You have do define the parameter when registering the data component:
document.addEventListener('alpine:init', () => {
window.Alpine.data('myThing', (param) => MyModule(param));
});
Now you can use it in your module on init...
export default (param) => ({
init() {
console.log(param);
}
});
... when you init the component
<div x-data="deliveryDate({ foo: 'bar' })"></div>
This likely happens since you imported your script as a module. Therefore, you need another script that handles initialization of data.
I'm using a vanillajs vite setup and here's a working implementation with alpinejs:
index.html
<head>
<!-- Notice the type="module" part -->
<script type="module" src="/main.js" defer></script>
<script src="/initializer.js"></script>
</head>
<body x-data="greetingState">
<button #click="changeText">
<span x-text="message"></span>
</button>
<h2 x-text="globalNumber"></h2>
</body>
main.js
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
// const globalNumber = 10; // Wrong place
initialize.js
document.addEventListener('alpine:init', () => {
Alpine.data('greetingState', () => ({
message: "Hello World!",
changeText() {
this.message = "Hello AlpineJs!";
},
}));
});
const globalNumber = 10; // Correct place
Note that listening to the alpine:init custom event inside of a javascript module will break the app. The same happens if you try to display a variable from a script of type module, in this example globalNumber.

Wait for a module to be loaded to execute the tests it contains

My unit tests are contained into a module that loads from an HTML page using SystemJS.
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script>
System.config({
defaultJSExtensions: true,
map: {
"angular2": 'http://localhost:8000/angular2',
"rxjs": 'node_modules/rxjs'
},
packages: {
angular2: {
defaultExtension: 'js',
}
}
});
System.import('angular2/test/http/backends/xhr_backend_spec')
.then((src) => {
src.main();
});
</script>
My page doesn't display any tests whereas my main method contains a test suite:
export function main() {
console.log('in main');
describe('XHRBackend', () => {
(...)
});
}
I put some traces and I check that the main function and the callback defined in the describe function are called. But the tests themselves aren't executed within the describe callback.
I guess that I need to wait for the module to be loaded before running Jasmine but I don't know how to configure this.
Thanks for your help!
Tell Jasmine to run the imported tests with .then (window.onload)
<script>
System.config({
(...)
System.import('angular2/test/http/backends/xhr_backend_spec')
// wait for all imports to load ...
// then re-execute `window.onload` which
// triggers the Jasmine test-runner start
.then(window.onload)
(...)
</script>
I read how to run Jasmine Tests on angular.io
https://angular.io/docs/ts/latest/testing/first-app-tests.html

Jasmine Testing get the name of the full describes/it's

I was wondering, is it possible to get the full nested describe path for the tests?
Given:
describe('Smoke Testing - Ensuring all pages are rendering correctly and free of JS errors', function () {
describe('app', function () {
describe('app.home', function () {
it('should render this page correctly', function (done) {
//name here should be: Smoke Testing - Ensuring all pages are rendering correctly and free of JS errors app app.home should render this page correctly
done()
})
})
describe('app.dashboard', function () {
describe('app.dashboard.foobar', function () {
it('should render this page correctly', function (done) {
//name here should be: Smoke Testing - Ensuring all pages are rendering correctly and free of JS errors app app.dashboard app.dashboard.foobar should render this page correctly
done()
})
})
})
})
})
Both jasmine.Suite and jasmine.Spec have method getFullName(). Works as you'd expect:
describe("A spec within suite", function() {
it("has a full name", function() {
expect(this.getFullName()).toBe('A spec within suite has a full name.');
});
it("also knows parent suite name", function() {
expect(this.suite.getFullName()).toBe('A spec within suite');
});
});
<script src="http://searls.github.io/jasmine-all/jasmine-all-min.js"></script>
Notice: this answer is now bit dated and uses Jasmine 1.3.1 in the example.
When you are inside the describe callback function this is set to a "suite" object which has the description of the suite (the text you pass to describe) and a property for the parent suite.
The example below gets the concatenation of the description nested describe calls, I'm not sure about how to access the description of the "it". But this will get you part way there.
var getFullDesc = function(suite){
var desc = "";
while(suite.parentSuite){
desc = suite.description + " " + desc;
suite = suite.parentSuite;
}
return desc;
}
describe('Outer describe', function(){
describe('Inner describe', function(){
console.log(getFullDesc(this));
it('some test', function(){
});
});
});

Durandal Weyland/Requirejs optimizer with kendo ui dataviz

I'm building an app with Durandal to bundle with PhoneGap. When I'm trying to run the weyland optimizer I'm running into some issues.
The build and optimization runs fine without any errors (I'm using requirejs as optimizer), but when I run the application my kendo ui chart throws an error saying "Uncaught TypeError: Object [object Object] has no method 'kendoChart'".
If I pause in debug mode in chrome where the kendoChart binding is taking place and type "kendo" in the console I get the kendoobject and can view its properties and so on, so it's definitely in the DOM.
Iv'e google around quite a bit and found some threads here on SO but none of them seem to sort my issue out. For instance this thread or this one.
I have a custom knockout binding for the chart, which is provided below.
My weyland.config looks like this:
exports.config = function (weyland) {
weyland.build('main')
.task.jshint({
include: 'App/**/*.js'
})
.task.uglifyjs({
// not uglyfying anything now...
//include: ['App/**/*.js', 'Scripts/durandal/**/*.js', 'Scripts/custom/**/*.js']
})
.task.rjs({
include: ['App/**/*.{js,html}', 'Scripts/custom/**/*.js', 'Scripts/jquery/*.js', 'Scripts/durandal/**/*.js'],
exclude: ['Scripts/jquery/jquery-2.0.3.intellisense.js', 'App/main.js'],
loaderPluginExtensionMaps: {
'.html': 'text'
},
rjs: {
name: 'main',
baseUrl: 'App',
paths: {
'text': '../Scripts/text',
'durandal': '../Scripts/durandal',
'plugins': '../Scripts/durandal/plugins',
'transitions': '../Scripts/durandal/transitions',
'knockout': '../Scripts/knockout/knockout-2.3.0',
'kendo': 'empty:', <-- tried refering kendo.all.min, or dataviz.chart or the path
'jquery': '../Scripts/jquery/jquery-2.0.3.min',
'Helpers': '../Scripts/custom/helpers',
........ other scripts ......
},
deps: ['knockout', 'ko_mapping', 'command'],
callback: function (ko, mapping, command) {
ko.mapping = mapping;
}
//findNestedDependencies: true, **<-- tried both true and false here**
inlineText: true,
optimize: 'none',
pragmas: {
build: true
},
stubModules: ['text'],
keepBuildDir: false,
out: 'App/main-built.js'
}
});
};
// The custom binding for the kendo chart
define([
'knockout',
'jquery',
'Helpers',
'kendo/kendo.dataviz.chart.min'
], function (
ko,
$,
helpers,
kendoui
) {
function drawChart(element, values, options) {
$(element).kendoChart({ **<-- this is where I get an error**
... options for chart ...
});
}
// kendoUi data viz chart
ko.bindingHandlers.moodChart = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
//set the default rendering mode to svg
kendo.dataviz.ui.Chart.fn.options.renderAs = "svg"; **<-- this renders no errors**
// if this is a mobile device
if (kendo.support.mobileOS) {
// canvas for chart for you!
kendo.dataviz.ui.Chart.fn.options.renderAs = "canvas";
}
var values = ko.unwrap(valueAccessor());
setTimeout(function () {
drawChart(element, values);
}, 125);
}
};
});
I might add that everything works fine running the not optimized code in a web browser (or a phone for that matter).
I've also tried to shim the kendo path in the config file and add a dependency to jquery, which doesn't really seem to do any difference.
Any help would be appreciated!
For large frameworks like kendo that have their own set of dependencies e.g. jquery version, I tend not to bundle them with my own AMD modules. Personal preference, I know.
Take a look at how you could load jquery , knockout and kendo via normal script tags in the .NET example
<body>
<div id="applicationHost"></div>
<script type="text/javascript" src="~/Scripts/jquery-1.9.1.js"></script>
<script type="text/javascript" src="~/Scripts/whateverKendoVersionGoesHere.js"></script>
<script type="text/javascript" src="~/Scripts/knockout-2.3.0.js"></script>
<script type="text/javascript" src="~/Scripts/bootstrap.js"></script>
<script type="text/javascript" src="~/Scripts/require.js" data-main="/App/main"></script>
</body>
That way jquery and knockout will be loaded as globals. In main.js you'd have to define jquery and knockout in order to make them available to Durandal (see main.js) as Durandal internally is still using them as AMD modules.
requirejs.config({
paths: {
'text': '../Scripts/text',
'durandal': '../Scripts/durandal',
'plugins': '../Scripts/durandal/plugins',
'transitions': '../Scripts/durandal/transitions'
}
});
define('jquery', function () { return jQuery; });
define('knockout', ko);
define(['durandal/system', 'durandal/app', 'durandal/viewLocator'], function (system, app, viewLocator) {
...
});

Running into Error while waiting for Protractor to sync with the page with basic protractor test

describe('my homepage', function() {
var ptor = protractor.getInstance();
beforeEach(function(){
// ptor.ignoreSynchronization = true;
ptor.get('http://localhost/myApp/home.html');
// ptor.sleep(5000);
})
describe('login', function(){
var email = element.all(protractor.By.id('email'))
, pass = ptor.findElement(protractor.By.id('password'))
, loginBtn = ptor.findElement(protractor.By.css('#login button'))
;
it('should input and login', function(){
// email.then(function(obj){
// console.log('email', obj)
// })
email.sendKeys('josephine#hotmail.com');
pass.sendKeys('shakalakabam');
loginBtn.click();
})
})
});
the above code returns
Error: Error while waiting for Protractor to sync with the page: {}
and I have no idea why this is, ptor load the page correctly, it seem to be the selection of the elements that fails.
TO SSHMSH:
Thanks, your almost right, and gave me the right philosophy, so the key is to ptor.sleep(3000) to have each page wait til ptor is in sync with the project.
I got the same error message (Angular 1.2.13). My tests were kicked off too early and Protractor didn't seem to wait for Angular to load.
It appeared that I had misconfigured the protractor config file. When the ng-app directive is not defined on the BODY-element, but on a descendant, you have to adjust the rootElement property in your protractor config file to the selector that defines your angular root element, for example:
// protractor-conf.js
rootElement: '.my-app',
when your HTML is:
<div ng-app="myApp" class="my-app">
I'm using ChromeDriver and the above error usually occurs for the first test. I've managed to get around it like this:
ptor.ignoreSynchronization = true;
ptor.get(targetUrl);
ptor.wait(
function() {
return ptor.driver.getCurrentUrl().then(
function(url) {
return targetUrl == url;
});
}, 2000, 'It\'s taking too long to load ' + targetUrl + '!'
);
Essentially you are waiting for the current URL of the browser to become what you've asked for and allow 2s for this to happen.
You probably want to switch the ignoreSynchronization = false afterwards, possibly wrapping it in a ptor.wait(...). Just wondering, would uncommenting the ptor.sleep(5000); not help?
EDIT:
After some experience with Promise/Deferred I've realised the correct way of doing this would be:
loginBtn.click().then(function () {
ptor.getCurrentUrl(targetUrl).then(function (newURL){
expect(newURL).toBe(whatItShouldBe);
});
});
Please note that if you are changing the URL (that is, moving away from the current AngularJS activated page to another, implying the AngularJS library needs to reload and init) than, at least in my experience, there's no way of avoiding the ptor.sleep(...) call. The above will only work if you are staying on the same Angular page, but changing the part of URL after the hashtag.
In my case, I encountered the error with the following code:
describe("application", function() {
it("should set the title", function() {
browser.getTitle().then(function(title) {
expect(title).toEqual("Welcome");
});
});
});
Fixed it by doing this:
describe("application", function() {
it("should set the title", function() {
browser.get("#/home").then(function() {
return browser.getTitle();
}).then(function(title) {
expect(title).toEqual("Welcome");
});
});
});
In other words, I was forgetting to navigate to the page I wanted to test, so Protractor was having trouble finding Angular. D'oh!
The rootElement param of the exports.config object defined in your protractor configuration file must match the element containing your ng-app directive. This doesn't have to be uniquely identifying the element -- 'div' suffices if the directive is in a div, as in my case.
From referenceConf.js:
// Selector for the element housing the angular app - this defaults to
// body, but is necessary if ng-app is on a descendant of <body>
rootElement: 'div',
I got started with Protractor by watching the otherwise excellent egghead.io lecture, where he uses a condensed exports.config. Since rootElement defaults to body, there is no hint as to what is wrong with your configuration if you don't start with a copy of the provided reference configuration, and even then the
Error while waiting for Protractor to sync with the page: {}
message doesn't give much of a clue.
I had to switch from doing this:
describe('navigation', function(){
browser.get('');
var navbar = element(by.css('#nav'));
it('should have a link to home in the navbar', function(){
//validate
});
it('should have a link to search in the navbar', function(){
//validate
});
});
to doing this:
describe('navigation', function(){
beforeEach(function(){
browser.get('');
});
var navbar = element(by.css('#nav'));
it('should have a link to home in the navbar', function(){
//validate
});
it('should have a link to search in the navbar', function(){
//validate
});
});
the key diff being:
beforeEach(function(){
browser.get('');
});
hope this may help someone.
I was getting this error:
Failed: Error while waiting for Protractor to sync with the page: "window.angular is undefined. This could be either because this is a non-angular page or because your test involves client-side navigation, which can interfere with Protractor's bootstrapping. See http://git.io/v4gXM for details"
The solution was to call page.navigateTo() before page.getTitle().
Before:
import { AppPage } from './app.po';
describe('App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should have the correct title', () => {
expect(page.getTitle()).toEqual('...');
})
});
After:
import { AppPage } from './app.po';
describe('App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
page.navigateTo();
});
it('should have the correct title', () => {
expect(page.getTitle()).toEqual('...');
})
});
If you are using
browser.restart()
in your spec some times, it throws the same error.
Try to use
await browser.restart()

Resources