Jest spyOn vs mock - both defined and undefined errors - vue.js

Just upgraded to jsdom-fourteen in my jest configuration. It's working wonderfully, but a single test is failing.
test('Do the thing', () => {
window.location.assign = jest.fn();
});
I inherited this code. It looks like a simple enough jest mock. It complains that it cannot assign the read-only property assign and that makes sense, I assume this is jsdom functionality that was added.
However... I can't do a jest.spyOn either, which seems to be what is suggested. I've not used spyOn before.
jest.spyOn(window.location.assign);
But this gives me an undefined property error:
Cannot spy the undefined property because it is not a function; undefined given instead
The line before this, I added a log just to check. It is definitely a function:
console.log(window.location.assign);
=> [Function: assign]
I'm not sure how these two errors can even coexist - both defined and undefined?

Due to how JavaScript works, it would be impossible to write spyOn function the way that allowed it to work like spyOn(window.location.assign). Inside spyOn, it's possible to retrieve window.location.assign function that was provided as an argument but not window.location object and assign method name to do window.location.assign = jest.fn().
The signature of spyOn is:
jest.spyOn(object, methodName)
It should be:
jest.spyOn(window.location, 'assign');
This may be unworkable as well because window.location.assign is read-only in later JSDOM versions, which is used by Jest to emulate DOM in Node.js. The error confirms that this is the issue.
It may be possible to mock read-only property manually:
const origAssign = Object.getOwnPropertyDescriptor(window.location, 'assign');
beforeEach(() => {
Object.defineProperty(window.location, 'assign', { value: jest.fn() })
});
afterEach(() => {
Object.defineProperty(window.location, 'assign', origAssign)
});
This wouldn't work with real DOM because built-ins may be read-only and non-configurable. This is the issue in Chrome. For testability reasons it may be beneficial to use location.href instead of location.assign.

Eventually worked through some things and found this:
delete global.window.location;
window.location = { assign : jest.fn()};
As it appears later iterations of jsdom lock the location object down further and further until it's completely not modifiable, #Estus' answer will only work in lower versions of jsdom/jest.

Related

Stencil: Sudden build error without message after adding `#Method` to component

I cannot give too much information here (because there really isn't), but only this:
All of the sudden, after adding a #Method function to a stencil component:
#Method()
async setMenuItems(items: Element[]): Promise<void> {
// code here
}
the component stopped compiling with the following - really unhelpful - error:
[ ERROR ] ./src/components/menu-content/menu-content.tsx:63:44
build error
L62: #Method()
L63: async setMenuItems(elements: Element[]): Promise<void> {
L64: const unsupportedChildren = elements.filter(e => !this.isSupportedChild(e)).map(e => e.tagName);
[12:37.1] build failed in 7.02 s
Things to notice
the return type Promise<void> inside the error-message is highlighted red
there are other #Methods that do work within this component (even with the same return type).
the "broken" #Method is structurally equal to those that do work.
TypeScript compiler does not complain about anything
Only stencil compiler fails
I already tried...
to google for this issue - did not find any hints to this problem
to remove the async and add return Promise.resolve()
to rename the method (I mean.. why not)
to move the method to another place in class
to remove the whole method (compiles fine x( )
to remove the #Method decorator (compiled, but of course my method is removed from API)
to delete node_modules folder and reinstall
I remember that I already had this error once, and apparently I somehow fixed it (or not, idk). But if I did, I cannot remember how.
Does anyone have an idea how to debug this - or even better fix?
I figured it out. A more complete version of my component is:
import { Element, ... } from '#stencil/core';
class MenuContent {
#Element() element: MenuContentElement;
#Method()
setMenuItems(elements: Element[]): Promise<void> {
// ------------------^^^^^^^
// Element is meant to be the built-in browser interface for Element
// but stencil thinks that this is the imported Element from '#stencil/core'!
}
}
The exact problem here is, that the stencil-compiler seems to assume that the elements parameter is of type Element that is imported from #stencil/core which is obviously wrong and leads to this strange unhelpful error.
Possible Solutions
1. Use an alias type for the built-in Element type
// types.ts
export type DomElement = Element;
// menu-content.tsx
import { DomElement } from './types';
...
async setMenuItems(elements: DomElement[]): Promise<void> { ... }
2. Switch to HTMLElement
Note: This is only legit, when you don't need to support other Element-types such as SVGElements for example!
async setMenuItems(elements: HTMLElement[]): Promise<void> { ... }
3. Use alias in import statement
Please note: When using #stencil eslint rules, they will complain about your renamed import and say that "own class members are not allowed to be public".
I created a ticket for it here: https://github.com/ionic-team/stencil-eslint/issues/28
import { Element as ElementDecorator} from '#stencil/core';
class MenuContent {
// eslint will complain about that:
// "Own class properties cannot be public."
#ElementDecorator() element: HTMLMenuContentElement;
}
I experienced this same issue not with the Element type but with the Event type, so it appears to be rooted deeper - also about a year after you reported this issue it seems to still be a problem with Stencil

Possible to manipulate CasperJS assertions?

My CasperJS asserts seem to be overly strict. I have a function where I am trying to test the names of client logo images from an array, using Casperjs. However I do not seem to be able to use a variable from a forLoop in casperJS.
I understand there are probably hoisting issues that I am not accounting for, but this does not seem to be the primary problem. I have tried several things to resolve hoisting issues, such as immediately invoked functions, try catch blocks, and using ES6 term "Let" in my loop. None seem to work. Then I notice if I simply hard-code the string my variable should represent, and stick a console.log into my assert of a PASSING test, right before the return, the passing test fails.
Here is my failing code
var clients = 'https://www.google.com/';
var logoArray = ["images/logos/AC.png", "images/logos/Affiny.png", "images/logos/ffintus.png", "images/logos/agileAsset.png"]
function checkClientsArrayTest() {
casper.test.begin('The layout is as expected', 10, function suite(test) {
casper.start(clients, function () {
casper.then(function () {
for (var i = 0; i < logoArray.length; i++) {
try { throw i }
catch (ii) {
console.log(ii);
console.log(i);
test.assertEvalEquals(function () {
return document.querySelectorAll('div.client_logo a img')[ii].getAttribute('src')
.match(logoArray[ii]).toString();
}, logoArray[ii], 'Test searches for Client Logos in DOM.');
}
}
});
}).run(function () {
test.done();
});
});
}
If I change logoArray[ii] to a hardcoded string from the first index of the array, it passes. If I consolelog logoArray[ii], it seems to be what I expect. But if I pass a variable to the assert, or even stick a console.log inside of it, the test fails with the following
Running check for the layout of URL: https://www.google.com
0
0
FAIL Test searches for Client Logos in DOM.
type: assertEvalEquals
file: headlessTester.js
subject: null
fn: undefined
params: undefined
expected: "images/logos/AC.png"
Is this an issue of me getting hoisting wrong (shouldn't fail by sticking in a logger if this is the case afaik), or is this due to strictly structured asserts in CasperJS?

Mocha and jsdom - How to set a window variable

I know you're not supposed to do this, but I'm trying to write some tests with legacy code still using requirejs that have a few window variables floating around.
Basically I'm trying to write a mocha test and include some predefined global variables that a different file would use later. I'm trying to do the following, but it seems the global variable "container" isn't populated when accessing it later.
global.document = require('jsdom').jsdom('<html></html>');
global.window = document.defaultView;
global.$ = require('jquery')(window);
// this should be available everywhere as far as I can tell...
global.container= {};
global.window.container= global.container;
// legacy scripts still using requirejs, so we need to load the require config here
var requirejs = require('testing-setup').requirejs;
// chai is nice
require('chai').should();
describe('model tests', function () {
var model;
// before we begin the tests, we need to require in all the necessary modules
before(function (done) {
window.container= {
dateFormat: false
};
requirejs(['Model', 'common', 'date'], function (Model) {
// load some dummy data out of a file
model= new Model(require('./test-data.js').item);
done();
});
});
// run some more tests down here, I'll spare you those
});
The script being loaded called "common" above has a reference to the global "container" object that lives on the window. Apparently what I have is not correct. Is there no way to set up a shared global variable in jsdom? I know it's not the standard way of doing things, so please spare the lectures. Refactoring all that legacy code right now is not really a feasible option.
Ah, it turns out this is the correct way of doing it. It appears the jsdom/nodejs differentiate the difference between window and global. If you want something to be available everywhere in every file in that session, it needs to be on the global namespace. The window is explicitly window.

How to override dojo's domReady

I want to override dijit._CssStateMixin's domReady() method.
Is there any way to override that instead of changing the listener mechanism in Dojo.
I tried overriding _cssMouseEvent() method in simple javascript, but it still does invoke dijit's _cssMouseEvent() from domReady().
I have tried following approach:
dojoConfig = {
map: {
'dijit/_CssStateMixin': {
'dojo/domReady': 'app/noop'
}
}
};
I have added 'app' folder and then 'noop.js' inside that.
noop.js has nothing in it:
define([], function () {
return function () {};
});
Even after this I can see that dijit.js's _CssStateMaxin domReady() getting called from listener.apply (code snippet pasted below)
var addStopImmediate = function(listener){
return function(event){
if(!event.immediatelyStopped){// check to make sure it hasn't been stopped immediately
event.stopImmediatePropagation = stopImmediatePropagation;
return listener.apply(this, arguments);
}
};
}
If your ultimate goal is to prevent the domReady callback in dijit/_CssStateMixin from running, your simplest bet is likely to re-map dojo/domReady to a different module that doesn't call the callback at all, when loaded via dijit/_CssStateMixin.
NOTE: Stripping out these handlers might have adverse visual effects on Dijit widgets which inherit _CssStateMixin, since it may hinder the application of Dijit CSS classes related to hover and focus. But if your concern is that _CssStateMixin is hampering performance, it may at least be worth a try to confirm or deny your suspicion.
First we have to create a simple module that returns a function that does nothing, which we will later substitute for dojo/domReady when loaded by dijit/_CssStateMixin, so that it can still call domReady but it won't execute the callback it passes.
For simplicity's sake I'll assume you already have a custom package that you can easily add a module to; for this example I'll assume it's called app. Let's create app/noop:
define([], function () {
return function () {};
});
Now let's configure the loader to map app/noop in place of dojo/domReady specifically when loaded by dijit/_CssStateMixin:
var dojoConfig = {
...,
map: {
'dijit/_CssStateMixin': {
'dojo/domReady': 'app/noop'
}
},
...
};
Now the offending domReady callback should no longer be run.
If you're curious about map, you can read more about it in this SitePen FAQ.

Flux architecture circular dependency

I have started learning Facebook's Flux architecture. I am trying to make a simple login screen. I have followed the flux-chat sample app to create the screen. I have a problem of circular dependency between ServerActionCreator and WebAPIUtils. Please see the code below.
ServerActionCreator.js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var Constants = require('../constants/Constants');
var WebAPIUtils = require('../utils/WebAPIUtils');
var ActionTypes = Constants.ActionTypes;
module.exports = {
receiveLoginStatus: function(status){
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVE_LOGIN_STATUS,
status: status
});
},
loginSubmit: function(data){
WebAPIUtils.login(data);
}
}
WebAPIUtils.js
var ServerActionCreator = require('../actions/ServerActionCreator');
module.exports = {
login: function (data) {
//Mock server API call
var status = JSON.parse('{"status":"success"}');
ServerActionCreator.receiveLoginStatus(status);
}
};
As you can see ServerActionCreator depends on WebAPIUtils and WebAPIUtils depends ServerActionCreator.
I think, due to circular dependency WebAPIUtils becomes an empty object and I am getting "undefined is not a function" error when loginSubmit function in ServerActionCreator is called. Screenshot below.
How to handle this scenario? or Is there any alternative way? Any help is much appreciated.
Whenever you have a circular dependency between modules, a common solution is to either combine the modules or to create a third entity that will break the cycle.
In your case, I'd argue that you could move loginSubmit to a different action creators module. It's actually a user action, not a sever action, anyway. So maybe loginSubmit could go in UserActionCreators.js along with any number of other user action creator methods.
Another solution to your problem (and to circular dependencies in general) is to make your methods more pure, removing dependencies and instead passing in dependencies as arguments. So WebAPIUtils.login() could take a second argument, which would be the success callback. Thus:
WebAPIUtils.login(data, ServerActionCreator.receiveLoginStatus)