Is it possible to retrieve a native interface from the Browser or Page instance in order to check if an object is an instanceof this interface?
For instance, in a jest testing context (where for some reasons CanvasRenderingContext2D isn't available since it is a Node context and not a JSDOM or an other emulation of browser APIs):
it("should create an instance of CanvasRenderingContext2D", async () => {
expect.assertions(1);
const context = await page.evaluate(() => {
return document.createElement("canvas").getContext("2d");
});
// Could a JSHandle be used somehow?
const CanvasRenderingContext2DInterface = await page.evaluateHandle(() => CanvasRenderingContext2D);
expect(context).toBeInstanceOf(CanvasRenderingContext2DInterface);
});
Instance check should be evaluated in the Puppeteer browser/page directly otherwise the execution contexts are different.
it("should create an instance of CanvasRenderingContext2D", async () => {
expect.assertions(1);
const isInstanceOfCanvasRenderingContext2D = await page.evaluate(
() =>
document.createElement("canvas").getContext("2d") instanceof CanvasRenderingContext2D
);
expect(isInstanceOfCanvasRenderingContext2D).toBeTruthy();
});
Related
I have a web scraper that uses Puppeteer. I am writing tests for my initial method: loadMainPage
loadMainPage:
const loadMainPage = async () => {
try {
// load puppeteer headless browser
const browser = await puppeteer.launch({
headless: true,
});
const mainPage = await browser.newPage();
await mainPage.goto(URL, { waitUntil: ["domcontentloaded"] });
// make sure page loaded.
console.log(URL + " loaded...");
const links = await getPackLinks(mainPage);
// close mainPage
await mainPage.close();
// loop through all links/pages and run the scraper
if (mainPage.isClosed()) {
await loadSubPage(links[6], browser);
console.log("Closing browser session...");
await browser.close();
}
} catch (e) {
console.error(e);
}
};
My test file:
const puppeteer = require("puppeteer");
const { loadMainPage } = require("./scraper");
jest.mock("puppeteer");
describe("loadMainPage()", () => {
it("should launch a new browser session", () => {
loadMainPage();
expect(puppeteer.launch).toBeCalled();
});
it("should open a new page", () => {
loadMainPage();
expect(???)
});
});
All I want to do is test whether certain methods in the puppeteer module are being called. My first test, checking for puppeteer.launch to be called, works just fine. The launch method returns a new instance of a Puppeteer object (a Browser), on which there is a newPage() method. How can I test to see if this method was called? newPage() itself returns another object (a Page), with its own methods that I will also need to test. I tried mocking my own implementations with the factory function that jest.mock accepts, but it was getting to be too much. I felt like I was missing something. Any help?
What I'm trying to accomplish
I am currently trying to create a wrapper for a db connection (to Neo4j) that works similar to the following:
Instantiate driver
Expose the executor for the driver so a session can be created
Pass my logic
Close the connection
Since there's no destructor in JavasScript, it's making this unnecessarily difficult with properly closing the session. The logic for creating and closing a connection is extremely repetitive and I'm trying to simplify repetitive scripts so that it's easier to call.
What I've tried.
Inject promise in chain
I thought something like the following could work, but I just cannot seem to create the logic properly. Passing session back to my inserted promise is challenging.
const connect = () => {
var driver;
var session;
return Promise.resolve(() => {
driver = my.driver(uri, creds);
}).then(() => {
// insert my promise here, exposing driver.session() function for executor
// if possible, bind it to the session var so we can properly close it after
// this would also determine the return value
}).catch((e) => console.error(e))
.finally(() => {
session.close();
driver.close();
})
});
Create class wrapper with procedural logic
I then also tried another approach, similar to:
var driver = my.driver(uri, creds);
var session;
function exitHandler(options) {
// ...
session.close();
driver.close();
}
// I have process.on for exit, SIGINT, SIGUSR1, SIGUSR2, and uncaughtException
process.on('exit', exitHandler.bind(null, options));
// ...
class Connection extends Promise<any> {
constructor(executor: Function) {
super((resolve, reject) => executor(resolve, reject));
executor(driver.session.bind(null, this));
}
}
export default Connection;
And calling it like
// ...
const handler = async () => await new Connection((session) => {
const s = session();
// do stuff here
});
The problem with this approach is that the driver is not instantiated before session is used (and so it's undefined). It also feels a little hacky with the process.on calls.
Question
Neither method works (or any of my other attempts). How can I properly wrap db connections to ensure they're consistent and deduplicate my existing code?
A sample of the Neo4j connection script can be found here. This is, essentially, what I'm trying to deduplicate across my scripts (pass everything from line 11 to 42 - inclusive) but have the init of driver, catch, finally, session.close(), driver.close() logic in my wrapper.
Ideally, I would like to expose the session function call so that I can pass parameters to it if needed: See the Session API for more info. If possible, I also want to bind the rxSession reactive session.
A sample of the Neo4j connection script can be found here. This is, essentially, what I'm trying to deduplicate across my scripts (pass everything from line 11 to 42 - inclusive) but have the init of driver, catch, finally, session.close(), driver.close() logic in my wrapper.
OK, the above part of what you are asking is what I was able to best parse and work with.
Taking the code you reference and factoring out lines 11 to 42 such that everything outside of those is shared and everything inside of those is customizable by the caller, this is what I get for the reusable part, designed to be in a module by itself:
// dbwrapper.js
const neo4j = require('neo4j-driver')
const uri = 'neo4j+s://<Bolt url for Neo4j Aura database>';
const user = '<Username for Neo4j Aura database>';
const password = '<Password for Neo4j Aura database>';
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
let driverOpen = true;
async function runDBOperation(opCallback, sessOpts = {}) {
const session = driver.session(sessOpts);
try {
await opCallback(session);
} catch (e) {
console.log(e);
throw e;
} finally {
await session.close();
}
}
async function shutdownDb() {
if (driverOpen) {
driverOpen = false;
await driver.close();
}
}
process.on('exit', shutdownDb);
module.exports = { runDBOperation, shutdownDb };
Then, you could use this from some other module like this:
const { runDBOperation, shutdownDB } = require('./dbwrapper.js');
runDBOperation(async (session) => {
const person1Name = 'Alice'
const person2Name = 'David'
// To learn more about the Cypher syntax, see https://neo4j.com/docs/cypher-manual/current/
// The Reference Card is also a good resource for keywords https://neo4j.com/docs/cypher-refcard/current/
const writeQuery = `MERGE (p1:Person { name: $person1Name })
MERGE (p2:Person { name: $person2Name })
MERGE (p1)-[:KNOWS]->(p2)
RETURN p1, p2`
// Write transactions allow the driver to handle retries and transient errors
const writeResult = await session.writeTransaction(tx =>
tx.run(writeQuery, { person1Name, person2Name })
)
writeResult.records.forEach(record => {
const person1Node = record.get('p1')
const person2Node = record.get('p2')
console.log(
`Created friendship between: ${person1Node.properties.name}, ${person2Node.properties.name}`
)
})
const readQuery = `MATCH (p:Person)
WHERE p.name = $personName
RETURN p.name AS name`
const readResult = await session.readTransaction(tx =>
tx.run(readQuery, { personName: person1Name })
)
readResult.records.forEach(record => {
console.log(`Found person: ${record.get('name')}`)
})
}).then(result => {
console.log("all done");
}).catch(err => {
console.log(err);
});
This can be made more flexible or more extensible according to requirements, but obviously the general idea is to keep it simple so that simple uses of the common code don't require a lot of code.
I'm trying to setup tests for my database crawler program and I can't manage to replace what the class method I'm testing imports.
So as not to write down too much code I'll just lay out the general form of the problem. In my test function I have:
describe("test",()=>{
let result1;
beforeAll(async ()=>{
await createConnection();
})
afterAll(async ()=>{
getConnection().close();
})
test("setup test",async () => {
result1 = await WeatherController.startForecastAPI();
expect(result1.status).toBe(Status.SUCCESS);
})
})
The WeatherController.ts file (... where code was taken out):
...
import AccessTokenService from '../services/AccessTokenService';
export default class WeatherController{
...
static async startForecastAPI(){
...
const accessToken = AccessTokenService.getAccessToken();//get and validate token
...
}
}
Inside the WeatherController class, startForecastAPI is defined as a static async method. The class imports multiple other classes, among them the AccessTokenService class which is used to get valid access tokens. AccessTokenService.getAccessToken() should return an object with several properties that it gets through a http request.
I want to mock the results of calling AccessTokenService but I'm not calling it directly in my test function, I'm calling WeatherController and WeatherController is calling AccessTokenService. How can I replace what WeatherController calls when I test it but without touching the WeatherController code? I've tried going through the jest docs but I'm fairly new to all of this and they're confusing. I'm not entirely clear how scoping works here either (I tried defining a function in the test code and calling it in the tested function but it's out of scope).
The await WeatherController.startForecastAPI() call in the test function returns undefined but the code works fine when I hard-code accessToken to be a valid object, I just can't find a way to inject that object into the code through the test function.
Assuming AccessTokenService.getAccessToken returns a promise or is an async function, then you can use jest.spyOn(...).mockResolvedValue() to prevent calling the server
describe("test",()=>{
let result1;
beforeAll(async ()=>{
await createConnection();
})
afterAll(async ()=>{
getConnection().close();
})
test("setup test",async () => {
const expectedResultFromGetToken = {property: 'property 1'};
const getTokenSpy = jest.spyOn(AccessTokenService, 'getAccessToken')
.mockResolvedValue(expectedResultFromGetToken)
result1 = await WeatherController.startForecastAPI();
expect(result1.status).toBe(Status.SUCCESS);
expect(getTokenSpy).toHaveBeenCalled()
})
})
if the AccessTokenService.getAccessToken is not an async function then you have to use jest.spyOn(...).mockReturnValue()
If inside your class you have
const AccessToken = require('access-token');
you can mock it with
jest.mock('access-token', () => {
function getToken() {
return 'fakeToken'
}
);
const WeatherController = require('weather-controller');
describe("test",()=>{
let result1;
beforeAll(async ()=>{
await createConnection();
})
afterAll(async ()=>{
getConnection().close();
})
test("setup test",async () => {
result1 = await WeatherController.startForecastAPI();
expect(result1.status).toBe(Status.SUCCESS);
})
})
I'm writing an AudioRecorder class whose init() accesses Navigator.mediaDevices.getUserMedia({audio: true}).
Is there a way to accept the user permission request in the DOM from Jasmine?
Source:
export default class AudioRecorder {
async init() {
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.recorder = new MediaRecorder(audioStream)
}
Jasmine test:
it('init()', done => {
const record = new AudioRecorder();
record.init().then(() => {
done();
});
});
You should be spying on navigator.mediaDevices.getUserMedia, forcing it to return a promise and resolving it with the value that you want.
You can put something like this in a beforeEach block, or at the beginning of the test:
const audioStream = { ... }; // create a mock audio stream with the appropriate methods spied on
let promise = Promise.resolve(audioStream);
spyOn(navigator.mediaDevices, 'getUserMedia', promise);
I'd also recommend that you convert your test to async, like this:
it('init()', async done => {
const record = new AudioRecorder();
const result = await record.init();
// test something with the result of init
done();
});
I think it is much easier to follow the control flow here.
I'm trying to do some testing in Jest but getting stuck with a mock/spy. I've managed to get the test working but only by changing my implementation (which I feel dirty about).
Here's the test:
import * as postmark from 'postmark';
jest.mock('postmark');
const mockGetServers = jest.fn();
const AccountClient = jest.fn(() => {
return {
getServers: mockGetServers
};
});
postmark.AccountClient = AccountClient;
import accountApi from './account-api';
describe('account-api', () => {
describe('listServers', () => {
it('calls postmark listServers', async () => {
await accountApi.listServers();
expect(mockGetServers).toHaveBeenCalledTimes(1);
});
});
});
Here's the working implementation:
import * as postmark from 'postmark';
const accountToken = 'some-token-number';
const listServers = async () => {
try {
const accountClient = postmark.AccountClient(accountToken);
const servers = await accountClient.getServers();
return servers;
} catch (e) {
console.log('ERROR', e);
}
};
export default {
listServers
}
Here's the original implementation:
import * as postmark from 'postmark';
const accountToken = 'some-token-number';
const accountClient = postmark.AccountClient(accountToken);
const listServers = async () => {
try {
const servers = await accountClient.getServers();
return servers;
} catch (e) {
console.log('ERROR', e);
}
};
export default {
listServers
}
The only change is where in the code the accountClient is created (either inside or outside of the listServers function). The original implementation would complete and jest would report the mock hadn't been called.
I'm stumped as to why this doesn't work to start with and guessing it's something to do with context of the mock. Am I missing something about the way jest works under the hood? As the implementation of accountApi will have more functions all using the same client it makes sense to create one for all functions rather than per function. Creating it per function doesn't sit right with me.
What is different about the way I have created the accountClient that means the mock can be spied on in the test? Is there a way I can mock (and spy on) the object that is created at class level not at function level?
Thanks
Am I missing something about the way jest works under the hood?
Two things to note:
ES6 import calls are hoisted to the top of the current scope
babel-jest hoists calls to jest.mock to the top of their code block (above everything including any ES6 import calls in the block)
What is different about the way I have created the accountClient that means the mock can be spied on in the test?
In both cases this runs first:
jest.mock('postmark');
...which will auto-mock the postmark module.
Then this runs:
import accountApi from './account-api';
In the original implementation this line runs:
const accountClient = postmark.AccountClient(accountToken);
...which captures the result of calling postmark.AccountClient and saves it in accountClient. The auto-mock of postmark will have stubbed AccountClient with a mock function that returns undefined, so accountClient will be set to undefined.
In both cases the test code now starts running which sets up the mock for postmark.AccountClient.
Then during the test this line runs:
await accountApi.listServers();
In the original implementation that call ends up running this:
const servers = await accountClient.getServers();
...which drops to the catch since accountClient is undefined, the error is logged, and the test continues until it fails on this line:
expect(mockGetServers).toHaveBeenCalledTimes(1);
...since mockGetServers was never called.
On the other hand, in the working implementation this runs:
const accountClient = postmark.AccountClient(accountToken);
const servers = await accountClient.getServers();
...and since postmark is mocked by this point it uses the mock and the test passes.
Is there a way I can mock (and spy on) the object that is created at class level not at function level?
Yes.
Because the original implementation captures the result of calling postmark.AccountClient as soon as it is imported, you just have to make sure your mock is set up before you import the original implementation.
One of the easiest ways to do that is to set up your mock with a module factory during the call to jest.mock since it gets hoisted and runs first.
Here is an updated test that works with the original implementation:
import * as postmark from 'postmark';
jest.mock('postmark', () => { // use a module factory
const mockGetServers = jest.fn();
const AccountClient = jest.fn(() => {
return {
getServers: mockGetServers // NOTE: this returns the same mockGetServers every time
};
});
return {
AccountClient
}
});
import accountApi from './account-api';
describe('account-api', () => {
describe('listServers', () => {
it('calls postmark listServers', async () => {
await accountApi.listServers();
const mockGetServers = postmark.AccountClient().getServers; // get mockGetServers
expect(mockGetServers).toHaveBeenCalledTimes(1); // Success!
});
});
});
I think you might want to look at proxyquire.
import * as postmark from 'postmark';
import * as proxyquire from 'proxyquire';
jest.mock('postmark');
const mockGetServers = jest.fn();
const AccountClient = jest.fn(() => {
return {
getServers: mockGetServers
};
});
postmark.AccountClient = AccountClient;
import accountApi from proxyquire('./account-api', postmark);
describe('account-api', () => {
describe('listServers', () => {
it('calls postmark listServers', async () => {
await accountApi.listServers();
expect(mockGetServers).toHaveBeenCalledTimes(1);
});
});
});
Note that I have not tested this implementation; tweaking may be required.