Javascript wait for window to close before opening another - javascript

I need to open a series of popup windows, each window must be closed before the next one can be opened.
function openWindows() {
var urls = getListOfUrls();
for (let url of urls) {
openWindow(url);
// Wait for window to close before continuing.
}
}
The only way I found to make sure that a window is closed is to use a setInterval which, as I understand it, causes the function behave asynchronously.
Any idea on how I can achieve this?

Potential alternate suggestion
Without more information, it sounds like what you're trying to accomplish can be completely automated using puppeteer and in-page scripting. If the URLs that you are visiting aren't all in the same origin, then this is the only method which will work (the solution below will not apply).
Interpretation of question
However, let's say that you need to manually perform some tasks on each page in order (one at a time) for whatever reason (maybe the pages you're retrieving often change their DOM in a way that keeps breaking your scripts), but you want to skip the rigor of serially opening the URLs in new tabs, so that you can just focus on the manual tasks.
Solution
JavaScript web APIs don't provide a way to check for the closure of a window (a script would no longer be running at that point), but the last event that you can respond to is the unload event, and using it would look something like this:
References:
Window
Window: unload event
Window.open()
Same-origin policy
async function openEachWindowAfterThePreviousUnloads (urls) {
for (const url of urls) {
console.log(`Opening URL: "${url}"`);
const target = '_blank';
const initialTime = performance.now();
const windowProxy = window.open(url, target);
if (!windowProxy) {
throw new Error(`Could not get window proxy for URL: "${url}"`);
}
await new Promise(resolve => {
windowProxy.addEventListener('unload', ev => {
const delta = performance.now() - initialTime;
const thresholdMs = 1000;
if (delta < thresholdMs) return;
resolve();
});
});
}
}
const tags = [
'javascript',
'puppeteer',
];
const urls = tags.map(tag => `https://stackoverflow.com/questions/tagged/${tag}`);
openEachWindowAfterThePreviousUnloads(urls);
Code in TypeScript Playground
Caveats:
The script will fail if any of the following is not true:
Every URL is in the same origin as that of the invoking window
If your browser blocks pop-ups, the page where you run the script is allowed to create pop-ups. Example error:
You can try the code above in your browser JS console on this page, and (as long as https://stackoverflow.com is allowed to create popups) it should work.

Related

calling window.showSaveFilePicker() outside a safe context in javascript

I'm using the javascript file API in the browser, to force the opening of the dialog window for downloading files, my goal is to make this window always open when there is a download, even if the user has defined the download in their browser if automatic.
So for that I'm using the window.showSaveFilePicker() function to open the window, and then add a file (Blob) coming from a download.
The problem is that the call to window.showSaveFilePicker() can only be performed in a safe context according to the documentation at: https://web.dev/file-system-access/
That is, if I call this function like this:
async function DownloadFile(){
async function startDialogWindow() {
const options = {
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt'],
},
},
],
};
// an exception will be thrown here
const handle = await window.showSaveFilePicker(options);
}
await startDialogWindow()
}
an exception "DOMException: Failed to execute 'showSaveFilePicker' on 'Window': Must be handling a user gesture to show a file picker." will be released.
but if the same call is made like this:
buttonTest.addEventListener('click', async() => {
const handle = await window.showSaveFilePicker(options);
} )
no exception occurs.
Given this, is there any way to run window.showSaveFilePicker within an unsafe context?
I also tried:
let handle = null;
let buttonTest = document.createElement('button');
buttonTest.addEventListener('click', async() => {
handle = await window.showSaveFilePicker(options);
} );
buttonTest.click()
but I was not successful.
I found the answer to the problem.
I can't explain why the solution worked, but the exception stopped being thrown when I removed all the "debugger" keywords from my code, i.e. letting the process flow normally without stopping for debugging solved the problem. this is very strange, but that's what was happening.

How to prevent the browser from closing while running code in Playwright Tests in Javascript

I'm trying to log the network calls in the browser. For my use case I require the browser to be in an open state and should be closed only with the scripts. I am currently using the page.pause() function to prevent the browser from automatically closing. Is there any other way to prevent the browser from closing automatically.
test('Verify home page Load event',async({page})=>{
//const browser = await chromium.launchPersistentContext("",{headless:false});
await page.goto("https://samplesite.com")
await page.on('request',req=>{
const requestUrl = req.url();
if(requestUrl.indexOf("google-analytics.com/collect")>-1){
console.log("Intercepted:->"+requestUrl);
}else{
req.continue
}
})
await page.pause();
})
I tried checking out this [link] (How to keep browser opening by the end of the code running with playwright-python?) for python but could not apply it to JS.
Similar to what was described in the answer to the python question, you need to keep your script alive somehow.
This answer describes a couple of ways to do that.
However, page.pause() is definitely the recommended approach- it exists precisely for this kind of situation where you need to inspect the browser while your script is executing. Your script also has some problems- as it stands when you encounter your target request you are logging something but not calling request.continue() (note that this a method, not a property). That will cause all requests to hang indefinitely until it is continued or aborted.
You probably want to do something like this:
await page.route('**/*', (route, request) => {
const rurl = request.url();
if (rurl.includes('google-analytics.com/collect')) {
console.log(`Intercepted request to ${rurl}`);
// Do other stuff?
}
route.continue();
});
It's not clear what you are trying to accomplish from your snippet- if you just need to wait for a particular request to fire, you can use either:
page.waitForRequest or page.waitForResponse, and do away with worrying about keeping the browser open.
I tried to use await page.pause() and it doesn't work for me, but I found the tricky way, and it works well, just put at the end of your test:
await new Promise(() => {})
Reference on the link.
You can try await Task.Delay(-1)

Puppeteer, distinguish between a redirect or a new html element (without timeouts)

TL;DR: using puppeteer, after triggering a button click, which one is the best way to understand what is happening to a page, knowing that either a redirect / history push could happen (and the url change, in a set of known ones, but not necessarily through redirect but also through push into history object) or a dialog might appear (with a known id)?
I'm trying to write a scraper using Puppeteer (very first experience with it, never used before) to navigate a website with the final goal of retrieving a text code, with the challenge that the path to get there is not always the same, and the code might actually not be given.
In the first page - full of ads, therefore slow as well -, I do something like this to wait for the "get code" button to appear (snippet 1):
// ... code to get the page instance ...
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
while(true) {
// Puppeteer won't complain if I don't await for page reload (to avoid the ads),
// as long as I await for the container div before doing anything else.
page.reload(); // No await
await page.waitForSelector("#code-container");
const hasCode = await page.evaluate(() => {
// I cannot click on it already because I realised it could
// cause a "Execution context was destroyed" error
return document.querySelector('#get-code-button') != null;
});
if(!hasCode) {
await sleep(10000);
}
}
// out of the loop, "#get-code-button" exists
And then I click on it (snippet 2):
// For some reason, this method is more reliable than using
// await page.click('#get-code-button').
await page.evaluate(async () => {
document.querySelector('#get-code-button').click()
});
// ... at this point the real troubles begin ...
Now, after the snippet above, a few scenarios might happen:
A dialog might appear, with the "reveal code" button in it (happy days)
A redirect might happen (url change, but it could be either a redirect either a push in the history object), with ads. After clicking on the div with id "continue-without-ads" (to simplify), I end up in one of the next redirects.
A redirect might happen (as above, url change, but it could be either a redirect either a push in the history object), with the "reveal code" button in it (happy days)
A redirect might happen (same as above), with basically written "error: code not available". If I go back from this page, the "get code" button should stay in place, so I could skip snippet 1 and go straight for snippet 2.
Question is, how can I detect in which scenario am I, and act timely (e.g. without waiting for the waitForSelector timeout to happen if I want to check for element to be there)?
As well, is the idea of using page.goBack() to get to the initial link and make another attempt a stupid one (to avoid waiting for the "get-code-button" to appear again, since the page should now be cached in Chrome)?
I want to avoid the headache of myself mashing the refresh button, clicking the "get-code-button" once it appears and go back to retry until I get the code.
I found an escamotage, but I don't think it's the easiest way to achieve what I wanted, neither the most correct ...
My solution is to have two "aggregators" of waiters: (1) one for selectors (a list of IDs, but any selector is just fine), (2) one for page url changes (a list of urls which are gonna trigger the promise if navigated to). Both this aggregators accepts as input a list of string (in one case selectors, in the other urls), and returns the first one to succeed.
The code to check what changed in the page after the click:
/**
* #param page the page to monitor for changes
* #param urls the list of urls that should trigger the redirect monitor
* #param selectors the list of selectors that should trigger the page change
* #param triggerPromise the promise that triggers the events (e.g. mouse click on a button)
* #returns the url or the selector that resulted as a change
*/
async function waitForWinner(page: Page, urls: string[], selectors: string[], triggerPromise: Promise<any>) {
// waitForUrlChange takes in input a list of urls, and returns the first
// one to succeed
const urlChangeMonitor = waitForUrlChange(page, urls);
// hasSelectors takes in input the list of selectors, and returns
// the first that succeeds
const selectorsPromise = hasSelectors(page, selectors);
const results = await Promise.all([
triggerPromise,
Promise.race([ urlChangeMonitor.promise, selectorsPromise ])
]);
urlChangeMonitor.clear();
const winner = results[1];
// This check is quite stupid, but it works for me:
const isRedirect = !winner.startsWith("#");
const isSelector = winner.startsWith("#");
// ... other custom logic here
// Simplification:
return { winner, isRedirect, isSelector }
}
The hasSelectors is a bit trivial, and it's full of custom logic in my case (when there are cookies it accepts them and then keeps going again), the most interesting part is the one to wait for url change.
In my case I realised there is no redirect, thus I suppose it's a push in the history object. Regardless, this method succeeds in listening for url changes in the page:
const unboundResolve = (url: string) => logger.error("Resolved too early, error.");
const unboundReject = () => logger.error("Rejected too early, error.");
export function waitForUrlChange(page: Page, urls: string[], timeout=60000) {
if (urls.length === 0) {
throw Error("Cannot have 0 lenght array of urls.");
}
const deferred = {
resolve: unboundResolve,
reject: unboundReject
};
const promise: Promise<string> = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
let promiseDone = false;
const checkForUrl = (frame: Frame) => {
const isRoot = frame.parentFrame() === null;
if (isRoot) {
// Frame might change, but page doesn't always change.
// Regardless, in this way I can detect url changes which
// occurs without redirect.
const currentUrl = page.url();
for(let u of urls) {
if (~currentUrl.indexOf(u)) {
// Resolve only once
if(!promiseDone) {
promiseDone = true;
deferred.resolve(currentUrl);
} else {
logger.warn(`Found another redirect of interest, but it's too late now.`);
}
clear();
break;
}
}
}
};
const clear = () => {
if (!promiseDone) {
deferred.reject();
promiseDone = true;
}
// Calling it multiple times doesn't make a difference
page.off("framenavigated", checkForUrl);
}
// If url doesn't change in one minute, call it a day
setTimeout(clear, timeout);
page.on("framenavigated", checkForUrl);
// Provide a way to turn off the listener from outside
return { clear, promise };
}
Main idea is to register to framenavigated event and listen for the url to contain one of the inputs (not the best, but works for me). Rather than directly returning a promise, I wrap it into an object which gives the possibility to clear the listener from outside, to keep things tidy.
The approach I presented has vast room for improvement (e.g. rather than having strings being passed around, I could add metadata and then the metadata is returned as well, to avoid very naive checks like .startsWith("#"), or the check for url could be a pattern or a callback), but it works and shows the main idea behind.

How to override window.open?

I was wodering, is it possible to override window.open? I want to a add delay to every window.open call
Let's say one piece of code opens window using onClick, the other one using window.open, so is it possible to add like setTimeout to global window.open?
First save a reference to window.open, then overwrite it with your own function that calls the saved reference after the timeout:
const origOpen = window.open;
window.open = (url) => {
setTimeout(() => {
origOpen(url);
}, 1000);
};
window.open('https://www.google.com');
(cannot embed into live snippet due to sandboxing constraints, but you can see a live demo here)

How to run javascript function in "background" / without freezing UI

I've done an HTML form which has a lot of questions (coming from a database) in many different tabs. User then gives answers in those questions. Each time a user changes a tab my Javascript creates a save. The problem is that I have to loop through all questions each time the tab is changed and it freezes the form for about 5 seconds every time.
I've been searching for an answer how I can run my save function in the background. Apparently there is no real way to run something in the background and many recommend using setTimeout(); For example this one How to get a group of js function running in background
But none of these examples does explain or take into consideration that even if I use something like setTimeout(saveFunction, 2000); it doesn't solve my problem. It only postpones it by 2 seconds in this case.
Is there a way to solve this problem?
You can use web workers. Some of the older answers here say that they're not widely supported (which I guess they weren't when those answers were written), but today they're supported by all major browsers.
To run a web worker, you need to create an instance of the built-in Worker class. The constructor takes one argument which is the URI of the javascript file containing the code you want to run in the background. For example:
let worker = new Worker("/path/to/script.js");
Web workers are subject to the same origin policy so if you pass a path like this the target script must be on the same domain as the page calling it.
If you don't want to create an new Javascript file just for this, you can also use a data URI:
let worker = new Worker(
`data:text/javascript,
//Enter Javascript code here
`
);
Because of the same origin policy, you can't send an AJAX request from a data URI, so if you need to send an AJAX request in the web worker, you must use a separate Javascript file.
The code that you specify (either in a separate file or in a data URI) will be run as soon as you call the Worker constructor.
Unfortunately, web workers don't have access to neither outside Javascript variables, functions or classes, nor the DOM, but you can get around this by using the postMessage method and the onmessage event. In the outside code, these are members of the worker object (worker in the example above), and inside the worker, these are members of the global context (so they can be called either by using this or just like that with nothing in front).
postMessage and onmessage work both ways, so when worker.postMessage is called in the outside code, onmessage is fired in the worker, and when postMessage is called in the worker, worker.onmessage is fired in the outside code.
postMessage takes one argument, which is the variable you want to pass (but you can pass several variables by passing an array). Unfortunately, functions and DOM elements can't be passed, and when you try to pass an object, only its attributes will be passed, not its methods.
onmessage takes one argument, which is a MessageEvent object. The MessageEvent object has a data attribute, which contains the data sent using the first argument of postMessage.
Here is an example using web workers. In this example, we have a function, functionThatTakesLongTime, which takes one argument and returns a value depending on that argument, and we want to use web workers in order to find functionThatTakesLongTime(foo) without freezing the UI, where foo is some variable in the outside code.
let worker = new Worker(
`data:text/javascript,
function functionThatTakesLongTime(someArgument){
//There are obviously faster ways to do this, I made this function slow on purpose just for the example.
for(let i = 0; i < 1000000000; i++){
someArgument++;
}
return someArgument;
}
onmessage = function(event){ //This will be called when worker.postMessage is called in the outside code.
let foo = event.data; //Get the argument that was passed from the outside code, in this case foo.
let result = functionThatTakesLongTime(foo); //Find the result. This will take long time but it doesn't matter since it's called in the worker.
postMessage(result); //Send the result to the outside code.
};
`
);
worker.onmessage = function(event){ //Get the result from the worker. This code will be called when postMessage is called in the worker.
alert("The result is " + event.data);
}
worker.postMessage(foo); //Send foo to the worker (here foo is just some variable that was defined somewhere previously).
Apparently there is no real way to run something on background...
There is on most modern browsers (but not IE9 and earlier): Web Workers.
But I think you're trying to solve the problem at the wrong level: 1. It should be possible to loop through all of your controls in a lot less than five seconds, and 2. It shouldn't be necessary to loop through all controls when only one of them has changed.
I suggest looking to those problems before trying to offload that processing to the background.
For instance, you could have an object that contains the current value of each item, and then have the UI for each item update that object when the value changes. Then you'd have all the values in that object, without having to loop through all the controls again.
You could take a look at HTML5 web workers, they're not all that widely supported though.
This works in background:
setInterval(function(){ d=new Date();console.log(d.getTime()); }, 500);
If you can't use web workers because you need to access the DOM, you can also use async functions. The idea is to create an async refreshUI function that refreshes the UI, and then call that function regularly in your function that takes long time.
The refreshUI function would look like this:
async function refreshUI(){
await new Promise(r => setTimeout(r, 0));
}
In general, if you put await new Promise(r => setTimeout(r, ms)); in an async function, it will run all the code before that line, then wait for ms milliseconds without freezing the UI, then continues running the code after that line. See this answer for more information.
The refreshUI function above does the same thing except that it waits zero milliseconds without freezing the UI before continuing, which in practice means that it refreshes the UI and then continues.
If you use this function to refresh the UI often enough, the user won't notice the UI freezing.
Refreshing the UI takes time though (not enough time for you to notice if you just do it once, but enough time for you to notice if you do it at every iteration of a long for loop). So if you want the function to run as fast as possible while still not freezing the UI, you need to make sure not to refresh the UI too often. So you need to find a balance between refreshing the UI often enough for the UI not to freeze, but not so often that it makes your code significantly slower. In my use case I found that refreshing the UI every 20 milliseconds is a good balance.
You can rewrite the refreshUI function from above using performance.now() so that it only refreshes the UI once every 20 milliseconds (you can adjust that number in your own code if you want) no matter how often you call it:
let startTime = performance.now();
async function refreshUI(){
if(performance.now() > startTime + 20){ //You can change the 20 to how often you want to refresh the UI in milliseconds
startTime = performance.now();
await new Promise(r => setTimeout(r, 0));
}
}
If you do this, you don't need to worry about calling refreshUI to often (but you still need to make sure to call it often enough).
Since refreshUI is an async function, you need to call it using await refreshUI() and the function calling it must also be an async function.
Here is an example that does the same thing as the example at the end of my other answer, but using this method instead:
let startTime = performance.now();
async function refreshUI(){
if(performance.now() > startTime + 20){ //You can change the 20 to how often you want to refresh the UI in milliseconds
startTime = performance.now();
await new Promise(r => setTimeout(r, 0));
}
}
async function functionThatTakesLongTime(someArgument){
//There are obviously faster ways to do this, I made this function slow on purpose just for the example.
for(let i = 0; i < 1000000000; i++){
someArgument++;
await refreshUI(); //Refresh the UI if needed
}
return someArgument;
}
alert("The result is " + await functionThatTakesLongTime(3));
This library helped me out a lot for a very similar problem that you describe: https://github.com/kmalakoff/background
It basically a sequential background queue based on the WorkerQueue library.
Just create a hidden button. pass the function to its onclick event.
Whenever you want to call that function (in background), call the button's click event.
<html>
<body>
<button id="bgfoo" style="display:none;"></button>
<script>
function bgfoo()
{
var params = JSON.parse(event.target.innerHTML);
}
var params = {"params":"in JSON format"};
$("#bgfoo").html(JSON.stringify(params));
$("#bgfoo").click(bgfoo);
$("#bgfoo").click(bgfoo);
$("#bgfoo").click(bgfoo);
</script>
</body>
</html>

Categories