Puppeteer's `page.evaluate` result differs from devTools console - javascript

I need to check for a certain service worker being registered. Unfortunately, page.evaluate returns undefined no matter what I do.
let page = await chrome.newPage();
await page.goto('http://127.0.0.1:8089/');
await page.waitFor(10000);
const isCorrectSW = await page.evaluate(async () => {
await navigator
.serviceWorker
.getRegistrations()
.then(registrations =>
registrations[0].active.scriptURL.endsWith('/target.js')
);
});
console.log(isCorrectSW);
isCorrectSW ends up being undefined, but if I enable devtools and run the same statement in the Chromium instance's devtools, I get the correct result. I can also observe the service worker attached in the browser's dev tools.
Is this a Puppeteer bug, or am I doing something incorrectly?

According to the documentation, page.evaluate returns undefined when the function passed returns a non-serializable value.
In your scenario, the function you are passing into page.evaulate does not return anything.
You are already using async, you can switch the function you are passing to be:
async () => {
const registrations = await navigator.serviceWorker.getRegistrations()
return registrations[0].active.scriptURL.endsWith('/target.js')
}

Related

React testing library - await findBy* vs promise.then

I have a test where I await for some text to be shown:
const allTodosPromise = findByText('All Todos')
My test actually checks if the HTMLElement returned has some style (to see if it is active or not):
expect(await allTodosPromise).toHaveStyle(activeItemStyle);
which fails with message "Unable to find an element with the text 'All Todos'...".
But, if I resolve my promise with a then the test passes, like:
allTodosPromise.then(htmlElement =>
expect(htmlElement).toHaveStyle(activeItemStyle)
);
Why? And yes, I did say that my test function is async.
Also trying to await in the first reference fails:
const allTodosElement = await findByText('All Todos');
You can try the waitFor function: https://testing-library.com/docs/dom-testing-library/api-async/#waitfor

page.waitForFunction not working as expected Playwright.js

I'm trying to debug my logic inside of page.waitForFunction but nothing I log out gets printed. If I try to use the VScode debugger it ignores any breakpoints inside the callback function. If I place a break point before the await page.waitForFunction() call and then try to step through it I just end up in /internal/async_hooks and then get stuck through a bunch of node internals. Again if I try to place a breakpoint inside the callback and jump to that, it doesn't stop.
I have no idea what's going on. Am I using it wrong or something? Or completely misunderstanding how it works?
Here's a simple example.
const { chromium } = require("playwright");
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const i = 0;
await page
.waitForFunction(
(i) => {
i++;
console.log(`---> evaluating # ${i}`);
if (i < 3) return false;
return true;
},
i,
{ timeout: 3000 }
)
.catch(console.error);
console.log("done");
await browser.close();
})();
For this code I expect it to log out
// ---> evaluating #1
// ---> evaluating #2
// done
Instead, nothing from within the callback is logged out and it just logs out
// done
If I always return false in the callback, then it errors with Timeout 3000ms exceeded.Error as expected, so it's like it's working, but I have no way to debug what's going on inside the callback.
A few things there:
First, the function you pass to waitForFunction is being executed inside the browser. So you won't see that log in your app, unless you get the log using the console event.
Second, and related to the previous point. That function will always get 0 in the i argument because i is incremented inside the browser. Which doesn't affect the variable that will be passed over and over.

Checking whether website showDirectoryPicker JS function with Puppeteer

Hello I want to get check whether the website has showDirectoryPicker function with the puppeteer.
Currently my code looks like this:
'use strict';
const puppeteer = require('puppeteer');
(async function main() {
try {
const browser = await puppeteer.launch({ headless:false,executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome', });
const [page] = await browser.pages();
await page.goto('https://example.com');
console.log(await page.evaluate(() => typeof showDirectoryPicker === 'function'));
await browser.close();
} catch (err) {
console.error(err);
}
})();
Currently this statement
console.log(await page.evaluate(() => typeof showDirectoryPicker === 'function'));
returns True for the every website since it is a valid JS function. However, I want to get True if the analyzed website has the showDirectoryPicker function.
If I understand your question correctly, you are trying to evaluate if the page calls the showDirectoryPicker() method, not if the browser supports it. One way to approach this would be to override the method with your own implementation that then reports back to Puppeteer if it gets called by the page. See my StackOverflow answer on overriding a function with a variant that logs whenever it gets called. You can then catch this log output with Puppeteer:
page.on('console', (message) => {
/*
Check that the message is what your overridden
custom variant logs.
*/
});

Is Puppeteer making my NodeJS process to exit?

I'm playing with Puppeteer and wrote this example (should probably never happen in production, but still):
const puppeteer = require('puppeteer');
(async () => {
// creating browser instance and closing it
const browser = await puppeteer.launch({ headless: false })
browser.disconnect()
await browser.close()
console.log('first check') // everything is ok here, message is printed
// opening page on the closed browser instance. Just for testing purposes
const page = await browser.newPage()
await page.goto('http://google.com')
// never printed
console.log('second check')
})()
So basically, I am trying to create a new page on a closed instance of the browser. Obviously, no page is opening because browser instance is closed. But I am expecting some error. Instead nothing happens and the second console.log is never executed!.
Question. If no error is thrown, why does the program never reach the second console.log? Does puppeteer somehow closes the process of my NodeJS application? Or I am missing something?
puppeteer version: latest - 5.3.1 (also 3.0.0)
By the way, if I use some earlier puppeteer version (2.0.0), same code is failing with error as I expect:
Error: WebSocket is not open: readyState 2 (CLOSING)
Update.
After debugging a bit the internals of Puppeteer I found out the following:
They have a Connection class with the map of callbacks as a property. Whenever we call the newPage method, a connection with new id is created as well as a new corresponding Promise. This promise resolve and reject functions are assigned to the callbacks map:
send(method, ...paramArgs) {
const params = paramArgs.length ? paramArgs[0] : undefined;
const id = this._rawSend({ method, params });
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
Then, the Connection class has the _onMessage(message) callback. Whenever some data (message) is received, they inspect the message to find out if it is an OK or an ERROR message. After this they invoke the stored resolve or reject callback.
But since the browser instance is my example is already closed, the message never arrives and the Promise is neither resolved nor rejected.
And after small research, I found out that NodeJS is not able to track such a Promises. Example:
(async () => {
const promise = new Promise((resolve, reject) => {
if (true === false) {
resolve(13) // this will never happen
}
})
const value = await promise
console.log(value) // we never come here
})()
I agree that this seems to be a bug. I see the issue you made and added a potential fix.
Adding this as the first thing in Connection.send() seems to fix the issue:
if (this._closed)
return Promise.reject(new Error(`Protocol error (${method}): Target closed.`));
In the mean time, I have added this to my code so at least it doesn't die silently with no indication that it failed:
process.on('beforeExit', (code) => {
//beforeExit will run if out of callbacks, but not on an exit()
console.log('We seem to be exiting purely because there are no more awaits scheduled instead of having reached and exit. Assuming this is bad behavior from the browser process. previous exit code: ', code);
process.exit(1);
});
//my code goes here
asdf()
process.exit(0);//will exit without triggering the beforeExit message.
Honestly the behavior of Node in silently exiting seems like it is a little lacking. You can set an exitCode, but having a program completely able to run up to an await then die silently without triggering exception handlers or finally blocks is a little gross.
You don't see any error probably because you don't wait for the async function to settle. If you attach a catch handler most likely you'll catch the error:
const puppeteer = require('puppeteer');
(async () => {
// creating browser instance and closing it
const browser = await puppeteer.launch({ headless: false })
browser.disconnect()
await browser.close()
console.log('first check') // everything is ok here, message is printed
// opening page on the closed browser instance. Just for testing purposes
const page = await browser.newPage()
await page.goto('http://google.com')
// never printed
console.log('second check')
})()
.then(() => console.log('done'))
.catch(e => console.error(e)); // <= HERE
Or use try/catch:
const puppeteer = require("puppeteer");
(async () => {
try {
// creating browser instance and closing it
const browser = await puppeteer.launch({ headless: false });
browser.disconnect();
await browser.close();
console.log("first check"); // everything is ok here, message is printed
// opening page on the closed browser instance. Just for testing purposes
const page = await browser.newPage();
await page.goto("http://google.com");
// never printed
console.log("second check");
} catch (e) {
console.error(e);
}
})();

Jest - TypeError: response.json is not a function

We are unit testing a React-Native application (using Jest) which does various API calls using fetch.
We have mocked calls to fetch in our API call functions in order to test them. This works well so far. We also have functions that combine these API calls and operate some logic on them.
For example, here is one function that, given a token, will get the related user's first project (project[0]) and return the list of items from this project.
export async function getAllItems(token) {
try {
const response = await getCurrentUser(token); // fetch called inside
const responseJson = await response.json();
const allItemsResp = await getAllItemsFromSpecificProject(
token,
responseJson.projectIds[0],
); // fetch called inside
return await allItemsResp.json();
} catch (error) {
console.log(error);
return null;
}
}
Both functions getCurrentUser and getAllItemsFromSpecificProject are simple fetch calls and are currently mocked properly. Here one test that tries to test the getAllItems function:
it('Gets all items', async () => {
getAccessTokenMockFetch();
const token = await getAccessToken('usherbrooke#powertree.io', 'test!23');
getAllItemsMockFetch();
const items = await getAllItems(token.response.access_token);
expect(items.response.length).toEqual(3);
});
For clarity, here is how getAccessTokenMockFetch is done. getAllItemsMockFetch is almost identical (with different data in the response):
function getAccessTokenMockFetch() {
global.fetch = jest.fn().mockImplementation(() => {
promise = new Promise((resolve, reject) => {
resolve(accepted);
});
return promise;
});
}
where accepted contains the JSON content of a successful call. When we run this test, we get the following error:
TypeError: Cannot read property 'response' of null
And we console.log this one in the catch:
TypeError: response.json is not a function
which explains why response is null. It seems the json() call is not understood and I don't know how to mock it. I have done tons of research on Stack Overflow and beyond, but we have found nothing that helps me understand how to solve this issue. This might indicate that I am going the wrong way about this, which is quite possible since I'm new to JavaScript, React Native, and Jest.
One thing to try is giving it a fake json to call, like this:
const mockFetch = Promise.resolve({ json: () => Promise.resolve(accepted) });
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);

Categories