mocking a conditional window.open function call with jest - javascript

I am trying to write a test to make sure that, when and when i pass a valid URL arg to a function it runs windows.open(args) to open it. then to make sure that i focus on it.
Test link validity:
export function isValidURL(url: string): boolean {
try {
new URL(url)
return true
} catch (e) {
console.warn(`Invalid URL: ${url}`)
return false
}
}
Open link:
export function openURL(url: string): void {
if (isValidURL(url)) {
const exTab = window.open(url, "_blank")
if (exTab) exTab.focus()
}
}
I thinked that i should mock some function or maybe to fake it's code, then wait for it's number of call or something like that. but i'm new with jest and testing and i feel so confused of how that can be done.
My issay:
describe("Test tools.openURL()", () => {
test("it should open link if valid.", () => {
const { open } = window
delete window.open
window.open = jest.fn()
openURL("htts//url2.de9v")
expect(window.open).not.toHaveBeenCalled()
openURL("https://www.url1.dev")
expect(window.open).toHaveBeenCalled()
window.open = open
// test focus here
})
})
With this code i have succeeded to test the open, now i just need to test focus.

'open' is readonly property.
Instead of jest.spyOn(window, "open") Try:
Object.defineProperty(window, 'open', { value: <your mock> });
in place of <your mock> locate mock object or object that should be returned.

Related

How to test declared function

I'm trying to make a test on FiveM JavaScript codes using Sinon. Is there any workaround for "declared function"? Or can Sinon just skip the "declared function"?
The codes below are the codes on the node_modules file
declare function RegisterCommand(commandName: string, handler: Function, restricted: boolean): void;
I already make a test command like this
const testCommandHandler = (source, args, raws) => {
console.log('source', source, 'args', args[0], raws);
}
RegisterCommand('test', testCommandHandler, false);
module.exports = {
testCommandHandler
}
And already tried test code like the code below
describe('#server', function () {
const sandbox = sinon.createSandbox();
before(() => {
global.window = {
RegisterCommand: sandbox.stub()
}
})
it('should call testCommand', function () {
testCommandHandler(123, ['test', 'command'], 'test command');
});
});
But when I tried to run the test it failed with a message
ReferenceError: RegisterCommand is not defined
Edit:
I have found the solution.. I just move the handler function to another file and test that file. Works like a charm.

Angular Karma JUnit test - SpyOn does not work within private method

I try to test my component, I know the component works fine but my test gives error since Angular version has been updated to 12.
This is my component:
ngOnInit() {
if (versonA) {
this.doThis();
} else {
this.doThat();
}
}
private doThis() {
this.myService.confirm({
message: message,
accept: () => {
this.doAcceptLogic();
}, reject: () => {
console.log('reject')
this.doRejectLogic();
}
});
}
And this is my test:
beforeEach(async () => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.autoDetectChanges();
spyOn(TestBed.get(MyService), 'confirm').and.callFake((params: Confirmation) => {
params.reject();
});
await fixture.whenStable();
});
And still, this spyOn does not seem to work.
I put a lot of console logs into my code and the doThis() method still get called but the log within my confirm method ('reject') does not get written to the console.
I cannot see why.
As I change the doThis() method to public and call it directy from my test as component.doThis(), then it runs to into the mocked myService.
Can anybody explain my why?
Thanks a lot!
You can call private methods in your spec either by casting to any
(component as any).doThis();
or using []
component['doThis']();
You call fixture.autoDetectChanges() it may call ngOnInit which is calling doThis before even you created the spy.

Jest + React: IntersectionObserver mock not working?

In my component.test.js, I tried mocking the IntersectionObserver by doing something like this:
const mock = ()=>({
observe: jest.fn(),
disconnect: jest.fn()
});
window.IntersectionObserver = jest.fn().mockImplementation(mock);
describe("Component", ()=>{
it("test 1", ()=>{
render(<Component/>);
...
}
});
My component.js looks something like this (it does that infinite pagination thing):
//ref to last item loaded >> load more items once it enters view
const observer = useRef();
const lastEntryRef = useCallback((node)=>{
...
observer.current.disconnect(); //ERROR LINE
}
...
);
When I run the tests, however, I get TypeError: observer.current.disconnect is not a function; same goes for observer.current.observe() if it runs. I tried testing it inside the it() block of component.test.js itself by instantiating an IntersectionObserver and then calling those methods and the same message showed when I re-ran the tests, so the errors look unrelated to how IntersectionObserver was set up in component.js. Am I not mocking IntersectionObserver correctly? If so, how do I fix it?
I recommend you to replace the arrow function for a normal function because you need to use the new operator to create an InterceptionObserver object:
const mock = function() {
return {
observe: jest.fn(),
disconnect: jest.fn(),
};
};
//--> assign mock directly without jest.fn
window.IntersectionObserver = mock;
The you can check if window.InterceptionObserver.observe has been called.

NodeJS events triggering multiple times in electron-react app

I have a package (Let's say PACKAGE_A) written to do some tasks. Then it is required by PACKAGE_B. PACKAGE_A is a node script for some automation work. It has this Notifier module to create and export an EventEmitter. (The Whole project is a Monorepo)
const EventEmitter = require('events');
let myNotifier = new EventEmitter();
module.exports = myNotifier;
So in some functions in PACKAGE_A it emits event by requiring myNotifier, and also in the index.js of PACKAGE_A, I export functions (API exposed to the other packages) and the myNotifier by requiring it again.
const myNotifier = require('./myNotifier);
const func1 = () => {
// some function
return something;
}
module.exports = {func1, myNotifier}
Then I import the PACKAGE_A in PACKAGE_B and use the API functions exposed with the notifier. PACKAGE_B is an electron app with a React UI.
Below is how the program works.
I have a console output window in the electron app (React UI, UI_A). <= (keep this in mind)
When I click a button in UI_A it fires a redux action (button_action). Inside the action, a notification is sent to an event which is listened in the electron code using ipcRenderer.
ipcRenderer.send('button-clicked', data); // <= this is not the full code of the action. It's bellow.
Then in the electron code (index.js), I require another file (UI_A_COM.js which houses the code related to UI_A in electron side). The reason is code separation. Here's part of the code in index.js related to the electron.
const ui_a_com = require('./electron/UI_A_COM');
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
resizable: false,
});
mainWindow.loadURL('http://localhost:3000');
const mainMenu = Menu.buildFromTemplate(menuTemplate);
ui_a_com (mainWindow);
};
Alright. Then in UI_A_COM.js, I listen to that triggered event button-clicked.
ipcMain.on('button-clicked', someFunction);
which runs the code from PACKAGE_A and return a result. So now when PACKAGE_A runs, it emits some events using myNotifier. I listen to them in the same file (UI_A_COM.js), and when those events are captured, I again send some events to React UI, which is subscribed when button_action fired.
myNotifier.on('pac_a_event_a', msg => {
mainWindow.webContents.send('ui_event_a', msg); // code in `UI_A_COM.js`
});
Here's the full code for the action. (Did not provide earlier because you'll get confused)
export const buttonAction = runs => {
return dispatch => {
ipcRenderer.send('button-clicked', data);
ipcRenderer.on('ui_event_a', (event, msg) => {
dispatch({ type: SOME_TYPE, payload: { type: msg } });
});
};
};
This will show the msg in the UI_A console.
So this is the task I'm doing. The problem is when I click the button; it works perfectly for the first time. But when I click the button on the second time, it received two messages. Then when I click the button again, three messages and it keeps growing. (but the functions in the PACKAGE_A only executes one time per button press).
Let's say the message from PACKAGE_A emitted is 'Hello there' per execution.
When I press the button 1st time a perfect result => Hello there, When I click the button again => Hello there Hello there, When I click it again => Hello there Hello there Hello there.
It's kept so on. I think my implementation of EventEmitter has some flows. So why it's happening like this? Is it EventEmitter or something else? What am I doing wrong here?
By default the electron-react boilerplate doesnt define the ipcRenderer.removeAllListeners method. So you have to first go to the main/preloads.ts file and add them :
removeListener(channel: string, func: (...args: unknown[]) => void) {
ipcRenderer.removeListener(channel, (_event, ...args) => func(...args));
},
removeAllListeners(channel: string) {
ipcRenderer.removeAllListeners(channel);
},
Then go to the renderer/preload.t.s declaration file and add the declarations too:
removeListener(
channel: string,
func: (...args: unknown[]) => void
): void;
removeAllListeners(channel: string): void;
After that make sure to clean all listeners in the cleanup function of your useEffects each time you listen to an event fired. This will prevent multiple firing.
useEffect(() => {
window.electron.ipcRenderer.on('myChannel', (event, arg) => {
// do stuffs
});
return () => {
window.electron.ipcRenderer.removeAllListeners('myChannel');
};
});
I think you should return a function that call ipcRenderer.removeAllListeners() in your component's useEffect().
Because every time you click your custom button, the ipcRenderer.on(channel, listener) is called, so you set a listener to that channel agin and agin...
Example:
useEffect(() => {
electron.ipcRenderer.on('myChannel', (event, arg) => {
dispatch({ type: arg });
});
return () => {
electron.ipcRenderer.removeAllListeners('myChannel');
};
});

How to fix Error: Not implemented: navigation (except hash changes)

I am implementing unit test for a file that contain window.location.href and I need to check it.
My jest version is 22.0.4. Everything is fine when I run my test on node version >=10
But I get this error when I run it on v8.9.3
console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29
Error: Not implemented: navigation (except hash changes)
I have no idea about it. I have searched on many page to find out the solution or any hint about this to figure out what happened here.
[UPDATE] - I took a look deep to source code and I think this error is from jsdom.
at module.exports (webapp/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17)
at navigateFetch (webapp/node_modules/jsdom/lib/jsdom/living/window/navigation.js:74:3)
navigation.js file
exports.evaluateJavaScriptURL = (window, urlRecord) => {
const urlString = whatwgURL.serializeURL(urlRecord);
const scriptSource = whatwgURL.percentDecode(Buffer.from(urlString)).toString();
if (window._runScripts === "dangerously") {
try {
return window.eval(scriptSource);
} catch (e) {
reportException(window, e, urlString);
}
}
return undefined;
};
exports.navigate = (window, newURL, flags) => {
// This is NOT a spec-compliant implementation of navigation in any way. It implements a few selective steps that
// are nice for jsdom users, regarding hash changes and JavaScript URLs. Full navigation support is being worked on
// and will likely require some additional hooks to be implemented.
const document = idlUtils.implForWrapper(window._document);
const currentURL = document._URL;
if (!flags.reloadTriggered && urlEquals(currentURL, newURL, { excludeFragments: true })) {
if (newURL.fragment !== currentURL.fragment) {
navigateToFragment(window, newURL, flags);
}
return;
}
// NOT IMPLEMENTED: Prompt to unload the active document of browsingContext.
// NOT IMPLEMENTED: form submission algorithm
// const navigationType = 'other';
// NOT IMPLEMENTED: if resource is a response...
if (newURL.scheme === "javascript") {
window.setTimeout(() => {
const result = exports.evaluateJavaScriptURL(window, newURL);
if (typeof result === "string") {
notImplemented("string results from 'javascript:' URLs", window);
}
}, 0);
return;
}
navigateFetch(window);
};
not-implemented.js
module.exports = function (nameForErrorMessage, window) {
if (!window) {
// Do nothing for window-less documents.
return;
}
const error = new Error(`Not implemented: ${nameForErrorMessage}`);
error.type = "not implemented";
window._virtualConsole.emit("jsdomError", error);
};
I see some weird logics in these file.
const scriptSource = whatwgURL.percentDecode(Buffer.from(urlString)).toString();
then check string and return error
Alternative version that worked for me with jest only:
let assignMock = jest.fn();
delete window.location;
window.location = { assign: assignMock };
afterEach(() => {
assignMock.mockClear();
});
Reference:
https://remarkablemark.org/blog/2018/11/17/mock-window-location/
Alternate solution: You could mock the location object
const mockResponse = jest.fn();
Object.defineProperty(window, 'location', {
value: {
hash: {
endsWith: mockResponse,
includes: mockResponse,
},
assign: mockResponse,
},
writable: true,
});
I faced a similar issue in one of my unit tests. Here's what I did to resolve it.
Replace window.location.href with window.location.assign(url) OR
window.location.replace(url)
JSDOM will still complain about window.location.assign not implemented.
Error: Not implemented: navigation (except hash changes)
Then, in one of your unit tests for the above component / function containing window.assign(url) or window.replace(url) define the following
sinon.stub(window.location, 'assign');
sinon.stub(window.location, 'replace');
Make sure you import sinon import sinon from 'sinon';
Hopefully, this should fix the issue for you as it did for me.
The reason JSDOM complains about the Error: Not implemented: navigation (except hash changes) is because JSDOM does not implement methods like window.alert, window.location.assign, etc.
References:
http://xxd3vin.github.io/2018/03/13/error-not-implemented-navigation-except-hash-changes.html
https://www.npmjs.com/package/jsdom#virtual-consoles
You can use jest-location-mock package for that
Usage example with CRA (create-react-app)
// src/setupTests.ts
import "jest-location-mock";
I found a good reference that explain and solve the problem: https://remarkablemark.org/blog/2018/11/17/mock-window-location/
Because the tests are running under Node, it can't understand window.location, so we need to mock the function:
Ex:
delete window.location;
window.location = { reload: jest.fn() }
Following worked for me:
const originalHref = window.location.href;
afterEach(() => {
window.history.replaceState({}, "", decodeURIComponent(originalHref));
});
happy coding :)
it('test', () => {
const { open } = window;
delete window.open;
window.open = jest.fn();
jest.spyOn(window, 'open');
// then call the function that's calling the window
expect(window.open).toHaveBeenCalled();
window.open = open;
});

Categories