I'm trying to automate a process with puppeteer. When adding a new feature that implied the usage of a new tab opened in a different window, I started having a Target closed error (stack below). I'm familiar with this error in other situations, but now I don't have a clue as to why this is happening. The version of puppeteer I'm using is 19.0.0.
This is the error stack:
Target closed
at node_modules/puppeteer-core/src/common/Page.ts:1599:26
at onceHandler (node_modules/puppeteer-core/src/common/EventEmitter.ts:130:7)
at node_modules/puppeteer-core/lib/cjs/third_party/mitt/index.js:3:232
at Array.map (<anonymous>)
at Object.emit (node_modules/puppeteer-core/lib/cjs/third_party/mitt/index.js:3:216)
at CDPSessionImpl.emit (node_modules/puppeteer-core/src/common/EventEmitter.ts:118:18)
at CDPSessionImpl._onClosed (node_modules/puppeteer-core/src/common/Connection.ts:457:10)
at Connection.onMessage (node_modules/puppeteer-core/src/common/Connection.ts:164:17)
at WebSocket.<anonymous> (node_modules/puppeteer-core/src/common/NodeWebSocketTransport.ts:50:24)
at WebSocket.onMessage (node_modules/puppeteer-core/node_modules/ws/lib/event-target.js:199:18)
When skipping the procedure that involves the usage of a second window, the error doesn't show.
This is my cleanup method, which is run when the process finished running:
public async destroy() {
let browserIsConnected: boolean = !!this._browser?.isConnected();
if (this._browser && browserIsConnected) {
for (let pg of await this._browser.pages()) {
this.logger.debug(`Closing page ${pg.url()}`);
await pg.close();
}
this.logger.debug(`Closing browser instance...`);
await this._browser?.close();
this.logger.log(`Closed browser connection`);
} else
this.logger.log(`Browser already destroyed`);
delete this._browser;
}
I tried omitting the page.close() calls but it didn't affect anything, and try/catching every library call in the method, but none throw. When running the code, the error is logged in parallel with this._browser?.close(), framed by the logs above and below it. However, the stack does not relate to the function call and I don't know how to catch it. Other than this, the process runs smoothly and the browser closes successfully, but this error is making my integration tests fail. Sorry about not sharing a reproducible case, but I couldn't reproduce it without disclosing my business logic.
My question is: why is this happening? is there any way to avoid it?
I eventually figured this out, and the source of this problem wasn't in the code described below but in the event handling logic during the process. While waiting the popup to show up, I was doing the following:
page.once('popup', async (newPage: Page) => {
// capture the information inside the page, with many awaits
});
What I didn't know was that mitt, puppeteer's underlying event handling library, doesn't support asynchronous event handlers, not awaiting my event properly. This was solved by resolving the promise from the handler and awaiting it further in the code:
let pagePromiseResolve: Function;
let pagePromise: Promise<Page> = new Promise(resolve => {
pagePromiseResolve = resolve;
});
page.once('popup', newPage => pagePromiseResolve(newPage));
let newPage = await pagePromise;
// capture the information inside the page, with many awaits
I'm leaving this here in case it helps somebody, either with this specific use-case or with awaiting events with a library alike mitt.
Related
I'm new to cypress and have ran into an issue. I have my base URL set to the domain I want to test, the issue is when I want to test the ability to login on my base url site I need to verify the user on another site, once I click apply on site number 2 the page on my base url reloads and I would then be able to test the rest of the site.
When I try to visit site 2 from my test I get an error
cy.visit() failed because you are attempting to visit a URL that is of
a different origin.
The new URL is considered a different origin because the following
parts of the URL are different:
superdomain
You may only cy.visit() same-origin URLs within a single test.
I read this https://docs.cypress.io/guides/guides/web-security.html#Set-chromeWebSecurity-to-false I've tried setting "chromeWebSecurity": false in cypress.json but I still get the same issue (I'm running in chrome)
Is there something I am missing?
As a temporary but solid work around, I was able to find this script in one of the Cypress Git issue threads (I don't remember where I found it so I can't link back to it)
Add the below to your cypress commands file
Cypress.Commands.add('forceVisit', url => {
cy.window().then(win => {
return win.open(url, '_self');
});
});
and in your tests you can call
cy.forceVisit("www.google.com")
From version 9.6.0 of cypress, you can use cy.origin.
If you want to use it, you must first set the "experimentalSessionAndOrigin" record to true.
{
"experimentalSessionAndOrigin": true
}
And here's how to use it.
cy.origin('www.example.com', () => {
cy.visit('/')
})
cy.origin change the baseurl, so you can link to another external link via cy.visit('/').
You can stub the redirect from login site to base site, and assert the URL that was called.
Based on Cypress tips and tricks here is a custom command to do the stubbing.
The login page may be using one of several methods to redirect, so besides the replace(<new-url>) stub given in the tip I've added href = <new-url> and assign(<new-url>).
Stubbing command
Cypress.Commands.add('stubRedirect', () => {
cy.once('window:before:load', (win) => {
win.__location = { // set up the stub
replace: cy.stub().as('replace'),
assign: cy.stub().as('assign'),
href: null,
}
cy.stub(win.__location, 'href').set(cy.stub().as('href'))
})
cy.intercept('GET', '*.html', (req) => { // catch the page as it loads
req.continue(res => {
res.body = res.body
.replaceAll('window.location.replace', 'window.__location.replace')
.replaceAll('window.location.assign', 'window.__location.assign')
.replaceAll('window.location.href', 'window.__location.href')
})
}).as('index')
})
Test
it('checks that login page redirects to baseUrl', () => {
cy.stubRedirect()
cy.visit(<url-for-verifying-user>)
cy.wait('#index') // waiting for the window load
cy.('button').contains('Apply').click() // trigger the redirect
const alias = '#replace' // or '#assign' or '#href'
// depending on the method used to redirect
// if you don't know which, try each one
cy.get(alias)
.should('have.been.calledOnceWith', <base-url-expected-in-redirect>)
})
You can't!
But, maybe it will be possible soon. See Cypress ticket #944.
Meanwhile you can refer to my lighthearted comment in the same thread where I describe how I cope with the issue while Cypress devs are working on multi-domain support:
For everyone following this, I feel your pain! #944 (comment) really gives hope, so while we're patiently waiting, here's a workaround that I'm using to write multi-domain e2e cypress tests today. Yes, it is horrible, but I hope you will forgive me my sins. Here are the four easy steps:
Given that you can only have one cy.visit() per it, write multiple its.
Yes, your tests now depend on each other. Add cypress-fail-fast to make sure you don't even attempt to run other tests if something failed (your whole describe is a single test now, and it makes sense in this sick alternate reality).
It is very likely that you will need to pass data between your its. Remember, we're already on this crazy “wrong” path, so nothing can stop us naughty people. Just use cy.writeFile() to save your state (whatever you might need), and use cy.readFile() to restore it at the beginning of your next it.
Sue me.
All I care about at this point is that my system has tests. If cypress adds proper support for multiple domains, fantastic! I'll refactor my tests then. Until that happens, I'd have to live with horrible non-retriable tests. Better than not having proper e2e tests, right? Right?
You could set the window.location.href manually which triggers a page load, this works for me:
const url = 'http://localhost:8000';
cy.visit(url);
// second "visit"
cy.window().then(win => win.location.href = url);
You will also need to add "chromeWebSecurity": false to your cypress.json configuration.
Note: setting the window to navigate won't tell cypress to wait for the page load, you need to wait for the page to load yourself, or use timeout on get.
In Google apps script when using a client sided .HTML file you can call a server sided script using google.script.run.(Function name).
You can see the related documentation here: https://developers.google.com/apps-script/guides/html/reference/run
Now this script has been working with no problems over the first 6 months of its lifetime or so. I have not touched the program and I have not been notified or have located any newly deprecated code.
Over the course of the last couple months however, my users have been reporting that when they finish interacting with the HTML document, nothing happens when they close it and they have to repeat the entire process 3 or sometimes even 4 times before they will get it to go through.,
This means that when the user closes the client sided HTML window, the server sided function should be called to handle the remaining tasks but in some cases is not. This issue is completely random, and does not seem to be caused by anything specific.
I have taken some steps myself to attempt to solve the issue. I have wrapped the entirety of the code in try catch blocks, including the .HTML and .GS files. This means that if literally ANYTHING goes wrong in ANY script, I will be notified of it immediately. However, despite this being the case I am yet to receive any emails of it failing even though I watch it fail with my own eyes. I have added log commands before and after this function to see if it stops working all together or continues. In every case regardless of whether the function call is successful or not the log commands go through.
To me this can only mean that for some reason the function google.script.run is not working properly, and is failing to run the associated function, but is not returning an error message or stopping the script.
I am at an absolute loss since I have no error message, no reproducible steps, and no history of this being a problem before while suddenly starting to get worse and worse over time. I have checked Google's issue tracker to no results. If anyone else is using this function and is having problems I would love you to share your experiences here. If you have a solution please let me know as soon as possible. If I can't fix this issue I am going to have to use a new platform entirely.
Edit 10/2:
After looking further into this issue I have discovered a list of all executions on this project. I can see what functions were executed, when, and how long they took to execute. I can see that when the function that opens the HTML service is ran, the next function that should run does not always appear in the list. And when it doesn't, I can see that the user repeated their steps until it did run. This supports my theory that the function just isn't running when it should be after being called my script.run
Tl;dr: The affected computers are running so slowly that google.script.host.close would run before google.script.run.functionName() is able to be called and the information passed from the client to server, causing the function to never run but also not return an error. Adding Utilities.sleep(1000) fixes the issue.
I'm answering here in the situation that someone stumbles upon this thread in the future because they're having similar problems.
I was able to fix the issue by adding two lines of code between
google.script.run and google.script.host.close.
I added Google's Utilities.sleep(1000) to force the computer to wait one second between executing the function and closing the HTML window. I also added an HTML alert that shows that the function was called and didn't suffer from a runtime error.
I don't know exactly why this seems to have fixed the issue but I have a theory.
I have about 20 computers this spreadsheet runs on. Only about 6 of them were having the issue, and this wasn't brought to my attention until recently. As it turns out the 6 computers that were having the issue were the slowest computers of the bunch.
My theory is that the computers were so slow, and the internet bandwidth was fluctuating so much that the computer simply didn't have time to call google.script.run and pass off the information from the client sided HTML window that it simply got closed and cut off when google.script.host.close was run. This means that the function will not exist in the execution transcripts or history, nor will there be any sort of runtime error. All of those things were true in my situation. This also explains why I never had the issue on any of my own equipment in a testing environment since it didn't suffer from any slowdowns the other computers were having.
By adding both Utilities.sleep(1000) and the UI alert this forces the javascript to not continue to google.script.host.close until the user interacts with the UI alert (Which is just a confirmation window with an OK button) and afterwards waits a full second. This sacrifices a tiny bit of user friendly-ness for a more functional script. Since I have implemented this "fix" none of my users are reporting any issues and all of my execution history looks just fine.
Hopefully this helps any future passerbys.
In the comments you posted this function snippet:
Here is a basic copy of the script that utilizes google.script.run:
function onFailure(error) {
MailApp.sendEmail("sparkycbass#gmail.com", "Order book eror", "ERROR: " + error.message);
google.script.host.close();
}
function handleFormSubmit(formObject) {
google.script.run.withFailureHandler(onFailure).processForm(formObject)
google.script.host.close();
}
The problem here is that google.script.run is asynchronous - the call to your server-side function processForm is not guaranteed to be even initiated before the call to google.script.host.close() is made:
Client-side calls to server-side functions are asynchronous: after the browser requests that the server run the function doSomething(), the browser continues immediately to the next line of code without waiting for a response. This means that server function calls may not execute in the order you expect. If you make two function calls at the same time, there is no way to know which function will run first; the result may differ each time you load the page. In this situation, success handlers and failure handlers help control the flow of your code.
A proper pattern is to only call "destructive" commands - such as closing the host and therefore unloading all the relevant Apps Script instances - after the server has indicated the async operation completed. This is within the success handler of the google.script.run call:
.html
function onFailure(error) { // server function threw an unhandled exception
google.script.run.sendMeAnEmail("Order book error", "ERROR: " + error.message);
console.log(error);
document.getElementById("some element id").textContent = "There was an error processing that form. Perhaps try again?"
}
function onSuccess(serverFunctionOutput, userObj) {
// do stuff with `serverFunctionOutput` and `userObj`
// ...
google.script.host.close();
}
function handleFormSubmit(formObject) {
google.script.run
.withFailureHandler(onFailure)
.withSuccessHandler(onSuccess)
.processForm(formObject);
}
.gs
function processForm(formData) {
console.log({message: "Processing form data", input: formData});
// ...
}
function sendMeAnEmail(subject, message) {
console.log({message: "There was a boo-boo", email: {message: message, subject: subject}});
MailApp.sendEmail("some email", subject, message);
}
I have a page running in a headless Chromium instance, and I'm manipulating it via the DevTools protocol, using the Puppeteer NPM package in Node.
I'm injecting a script into the page. At some point, I want the script to call me back and send me some information (via some event exposed by the DevTools protocol or some other means).
What is the best way to do this? It'd be great if it can be done using Puppeteer, but I'm not against getting my hands dirty and listening for protocol messages by hand.
I know I can sort-of do this by manipulating the DOM and listening to DOM changes, but that doesn't sound like a good idea.
Okay, I've discovered a built-in way to do this in Puppeteer. Puppeteer defines a method called exposeFunction.
page.exposeFunction(name, puppeteerFunction)
This method defines a function with the given name on the window object of the page. The function is async on the page's side. When it's called, the puppeteerFunction you define is executed as a callback, with the same arguments. The arguments aren't JSON-serialized, but passed as JSHandles so they expose the objects themselves. Personally, I chose to JSON-serialize the values before sending them.
I've looked at the code, and it actually just works by sending console messages, just like in Pasi's answer, which the Puppeteer console hooks ignore. However, if you listen to the console directly (i.e. by piping stdout). You'll still see them, along with the regular messages.
Since the console information is actually sent by WebSocket, it's pretty efficient. I was a bit averse to using it because in most processes, the console transfers data via stdout which has issues.
Example
Node
async function example() {
const puppeteer = require("puppeteer");
let browser = await puppeteer.launch({
//arguments
});
let page = await browser.newPage();
await page.exposeFunction("callPuppeteer", function(data) {
console.log("Node receives some data!", data);
});
await page.goto("http://www.example.com/target");
}
Page
Inside the page's javascript:
window.callPuppeteer(JSON.stringify({
thisCameFromThePage : "hello!"
}));
Update: DevTools protocol support
There is DevTools protocol support for something like puppeteer.exposeFunction.
https://chromedevtools.github.io/devtools-protocol/tot/Runtime#method-addBinding
If executionContextId is empty, adds binding with the given name on
the global objects of all inspected contexts, including those created
later, bindings survive reloads. If executionContextId is specified,
adds binding only on global object of given execution context. Binding
function takes exactly one argument, this argument should be string,
in case of any other input, function throws an exception. Each binding
function call produces Runtime.bindingCalled notification.
.
If the script sends all its data back in one call, the simplest approach would be to use page.evaluate and return a Promise from it:
const dataBack = page.evaluate(`new Promise((resolve, reject) => {
setTimeout(() => resolve('some data'), 1000)
})`)
dataBack.then(value => { console.log('got data back', value) })
This could be generalized to sending data back twice, etc. For sending back an arbitrary stream of events, perhaps console.log would be slightly less of a hack than DOM events? At least it's super-easy to do with Puppeteer:
page.on('console', message => {
if (message.text.startsWith('dataFromMyScript')) {
message.args[1].jsonValue().then(value => console.log('got data back', value))
}
})
page.evaluate(`setInterval(() => console.log('dataFromMyScript', {ts: Date.now()}), 1000)`)
(The example uses a magic prefix to distinguish these log messages from all others.)
First off the code, which runs without error but isn't covering everything I need it to:
public async Task SaveData()
{
// MessageDialog restartMessage;
// ^^ This was made global and is defined in SafeToSave()
// Check if it's safe to save
await SafeToSave();
if (restartMessage != null)
{
CoreDispatcher cD = Window.Current.CoreWindow.Dispatcher;
await cD.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
await restartMessage.ShowAsync();
// This WILL invoke 'Application.Current.Exit()'
});
return;
// Issue is right around here...
}
// Other code...
}
The basic premise is that if it is not safe to save, a message dialog will tell the user that the app needs to be restarted and then perform 'Application.Current.Exit()' when they then tap the 'Okay' button.
My problem is that I need my code to stop executing when that restart is hit. Right now 'return' only ends this method, but after that it will continue to do other things in the method that called SaveData() until my restartMessage takes effect, and that's not good.
I considered putting 'Application.Current.Exit()' above the 'return' statement, but that will shut things down before my error message can be seen. Which is also not good.
The other solution was skipping the whole CoreDispatcher thing and just running ShowAsync() by itself, except that would trigger a different known error. Basically I have a few MessageDialogs that call the SaveData() method and opening a MessageDialog when another MessageDialog is already open is, again, not good.
That's about it: I need something to stop my code form executing without killing the whole application or preventing my error message from displaying. What can I do?
Short version, I used this guy's solution: MessageDialog ShowAsync throws accessdenied exception on second dialog
Since I can't control when the Dispatcher is going to occur, it's necessary to scrap it. Now I said I couldn't do that because it was preventing another bug, so I used the advice in that link to prevent that bug in a different way... so now I can ditch the Dispatcher and force my application to deal with my message dialog without it continuing along its multi-threaded path and doing things it shouldn't!
Code for my solution is moved into the SafeToSave() method. I did what the guy in that link did and then changed this:
CoreDispatcher cD = Window.Current.CoreWindow.Dispatcher;
await cD.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
await restartMessage.ShowAsync();
});
Into this:
await restartMessage.ShowAsync();
And unlike before I'm not getting an access error for calling ShowAsync() while another MessageDialog is already up, because I'm forcibly closing it first.
We have images that redirect from our media server to a CDN that I'm trying to exclude from my service worker logic to work around the bug in Chrome 40. In Canary the same worker is able to work just fine. I thought there was an event.default() to fall back to the standard behavior but I don't see that in Chrome's implementation, and reading the spec it seems like the current recommendation is to just use fetch(event.request).
So the problem I have is do I have to wait until 99% of all of our users move to Chrome 41+ in order to use service workers in this scenario, or is there some sort of way I can opt out for certain requests?
The core of my logic is below:
worker.addEventListener('install', function(event){
event.waitUntil(getDefaultCache().then(function(cache){
return cache.addAll(precacheUrls);
}));
});
worker.addEventListener('fetch', function(event){
event.respondWith(getDefaultCache().then(function(cache){
return cache.match(event.request).then(function(response){
if (!response){
return fetch(event.request.clone()).then(function(response){
if (cacheablePatterns.some(function(pattern){
return pattern.test(event.request.url);
})) {
cache.put(event.request, response.clone());
}
return response;
});
}
return response;
});
}));
});
Once you're inside a event.respondWith() you do need to issue a response or you'll incur a Network Error. You're correct that event.default() isn't currently implemented.
A general solution is to not enter the event.respondWith() if you can determine synchronously that you don't want to handle the event. A basic example is something like:
function fetchHandler(event) {
if (event.request.url.indexOf('abc') >= 0) {
event.respondWith(abcResponseLogic);
} else if (event.request.url.indexOf('def') >= 0) {
event.respondWith(defResponseLogic);
}
}
self.addEventListener('fetch', fetchHandler);
If event.respondWith() isn't called, then this fetch handler is a no-op, and any additional registered fetch handlers get a shot at the request. Multiple fetch handlers are called in the order in which they're added via addEventListener, one at a time, until the first one calls event.respondWith().
If no fetch handlers call event.respondWith(), then the user agent makes the request exactly as it normally would if there were no service worker involvement.
The one tricky thing to take into account is that the determination as to whether to call event.respondWith() needs to be done synchronously inside each fetch handler. Anything that relies on asynchronous promise resolution can't be used to determine whether or not to call event.respondWith(). If you attempt to do something asynchronous and then call event.respondWith(), you'll end up with a race condition, and likely will see errors in the service worker console about how you can't respond to an event that was already handled.