clientScript() function in testcafe isn't working on all pages - javascript

According to Testcafe's documentation, I should be able to inject a clientScript into all pages: https://testcafe.io/documentation/402843/guides/advanced-guides/inject-client-scripts#add-client-scripts-to-all-tests
I currently have it set up to inject this script so that it can dismiss notifications that pop up in our application which overlay buttons that we need to interact with.
const notifications_div = document.querySelector('.notifications')
if (notifications_div) {
// Creates the MutationObserver and triggers the callback
const mutationObserver = new MutationObserver(() => {
// Checks to see if the style is set to 'block'
if (notifications_div.style.display == 'block') {
// Set the dismiss button to a variable each time since the previous will no longer exist.
let dismiss_button = document.querySelector("a[data-turbo-method='delete']")
// Click the dismiss button; Timeout is needed to avoid race condition errors.
setTimeout(() => { dismiss_button.click(); }, 3000);
// Hide the notifications_div again since it never truly goes away; Timeout is needed to avoid race condition errors.
setTimeout(() => { notifications_div.style.display = 'none'; }, 3000);
}
})
// Starts the observation of the notifications_div and checks for a change on 'style'
mutationObserver.observe(notifications_div, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['style']
})
}
When I run this code in the console and then trigger a notification, it works just fine. When I run a testcafe suite I still end up seeing notifications (that asynchronously pop up), cover the button that I need to interact with, and never close.
When does the code actually get injected? Is it every page load?
Video of the script working fine via the console: https://www.loom.com/share/1a5b96d054a345748e4f018bc56af413

The Client Script injection should work even after a new page is loaded. The following code snippet demonstrates that the script is injected:
fixture`f`
.page`http://example.com`;
const clientScript = `
console.log('location: ' + window.location.href);
`;
test
('My test', async t => {
await t.navigateTo('http://google.com');
await t.debug();
})
.clientScripts({ content: clientScript });
If this code does not help, please create a separate issue in the TestCafe official repository using the following template and share an example where the problem is reproduced: https://github.com/DevExpress/testcafe/issues/new?assignees=&labels=TYPE%3A+bug&template=bug_report.yaml.

Related

How to trigger JavaScript when img-tag appears in DOM or src-attribute changes Or: Adding Wheelzoom to h5ai

For days now I am trying to figure out something and I can't get it to work - maybe somebody can help.
My (global) goal: I am trying to add a "zoom by mousewheel" to the image-viewer of the php-web-server-index-tool h5ai.
My idea how to do it:
I found the tool "Wheelzoom" which adds the possibility to zoom to every img-tag on the page without changing the DOM by only doing 2 things:
linking to the wheelzoom.js on the page
calling "wheelzoom(document.querySelectorAll('img#pv-content-img'));" after the page has loaded
With h5ai I can insert JavaScript by placing it in the "ext"-Folder and adding it to the options.json - this adds a script-tag to the header; so no problem to get the wheelzoom.js loaded.
But I am struggling with the command "wheelzoom(document.querySelectorAll('img#pv-content-img'));". So far I put it in an extra .js-file, also calling it in the header. But I need it to be triggered more often:
h5ai has always an empty <div id=pv-overlay> in the DOM. Only when you click on an image-filename, it fills it with "<div id=pv-container><img id=pv-content-img src=""...>", changing the src-attribute of the image by clicking "back/forward".
(You can check out my project here, to see it live)
How do I trigger the wheelzoom-command after the img-tag appears and/or the src-attribute changes? I tried a lot, now something like this:
var insertedNodes = [];
var observer = new WebKitMutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log("Change!"); //here the wheelzoom-function should be triggered
})
});
observer.observe(document, {
childList: true
});
But with all my trying - either I run into a loop that crashes the site, or it doesn't get triggered at all (like the version above).
Now I wonder: Is my idea possible? Is there a better way without changing the sourcecode of h5ai? When I use the browser-console and type in the command after an image is loaded-it works perfectly!
Thanks for your help!
edit 2022/11/15 - my last version, sadly still not working:
document.addEventListener('readystatechange', event => {
switch (document.readyState) {
case "interactive":
console.log("document.readyState: ", document.readyState,
`- The document has finished loading DOM. `,
`- "DOMContentLoaded" event`
);
break;
case "complete":
console.log("document.readyState: ", document.readyState,
`- The page DOM with Sub-resources are now fully loaded. `,
`- "load" event`
);
const container = document.querySelector("pv-container");
const observer = new MutationObserver(() => container.querySelectorAll("#pv-content-img").forEach(e => wheelzoom(e)));
observer.observe(container, { childList: true });
break;
}
});
You can observe the container and when a new element pops in, you can search inside that container for the image and just call wheelzoom on it.
window.addEventListener("load", () => {
const container = document.getElementById("pv-container");
const observer = new MutationObserver(() =>
container.querySelectorAll("#pv-content-img").forEach(e =>
wheelzoom(e)));
observer.observe(container, { childList: true });
});

Navigator.share only working once in iOS, second click throws error "request is not allowed by the user agent..."

Has anyone else ran into this issue? I'm implementing the Web Share API to add a share button to a number of listings on a page. The code seems to be working fine initially, as I can click any of the "Share" buttons and the dialog will appear correctly.
However, if I close the dialog and try to click it again, I get an error saying "The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission."
Stranger still, I'm experiencing this same behavior on all these tutorial sites on how to implement the Web Share API (example: https://alligator.io/js/web-share-api/ - Try clicking the "Share Me!" button halfway down the page more than once on iOS Safari.)
Here's my code for reference:
const shareButtons = document.querySelectorAll('.share');
for (const button of shareButtons) {
button.addEventListener("click", async () => {
if (navigator.share) {
try {
await navigator.share({
url: button.getAttribute('data-url')
});
} catch (err) {
alert(err.message);
}
} else {
// fallback
}
});
}
Appreciate any insight I can get on this - Thanks much
Which version of IOS are you using?
It looks like there is a known bug in IOS 14.0.1 where the initial promise does not resolve.
https://developer.apple.com/forums/thread/662629
suggested fix from the article (which worked for me) is:
navigator.share(...)
.then(() => {
// this will never happen in IOS 14.0.1
})
.catch(error => {
//this will catch the second share attempt
window.location.reload(true); // now share works again
});
or in your case with try/catch
const shareButtons = document.querySelectorAll('.share');
for (const button of shareButtons) {
button.addEventListener("click", async () => {
if (navigator.share) {
try {
await navigator.share({
url: button.getAttribute('data-url')
});
} catch (err) {
// this will catch the second share attempt on ios14
window.location.reload(true); // now share works again
alert(err.message);
}
} else {
// fallback
}
});
}
The downside is it actually means the user will have to click twice to trigger the second share if they are using IOS14 - but it won't happen to other users
EDIT: I have found a more concise solution that seems to solve this problem. It basically creates an iframe and uses the iframe's navigator to share instead of the page, that way you can hard reload the iframe every time you share to prevent it from hanging.
// create an invisible "sharing iframe" once
var sharingIframe = document.createElement("iframe");
var sharingIframeBlob = new Blob([`<!DOCTYPE html><html>`],{type:"text/html"});
sharingIframe.src = URL.createObjectURL(sharingIframeBlob);
sharingIframe.style.display = "none"; // make it so that it is hidden
document.documentElement.appendChild(sharingIframe); // add it to the DOM
// do not revoke the blob url because you will be reloading it later.
// also note that the following function must only be run after the iframe
// loads (and the iframe.contentWindow.navigator, which is what we are using)
function share(file){
sharingIframe.contentWindow.navigator.share({files:[file]}).then(()=>{
console.log("files shared");
sharingIframe.contentWindow.location.reload(true); // reload sharing iframe to fix iOS bug
}).catch((err)=>{
console.error("user cancelled share");
sharingIframe.contentWindow.location.reload(true); // reload sharing iframe to fix iOS bug
});
}
My old, messier solution from earlier:
I encountered the same annoying bug, even with IOS 15+ safari still acts weird with navigator.share. my solution was to essentially create a temporary iframe when sharing something, using the iframe's contentWindow.navigator instead of the top window, and then closing/removing the iframe to kill the reference every time so it doesn't hang.
function share(file){
// first, create temporary iframe and construct a temporary HTML file
// this serves as a sort of modal to click thru and sits on top
// of the main window until it is dismissed
var iframe = document.createElement("iframe");
var iframeText = `
<!DOCTYPE html>
<html style="position:absolute;width:100%;height:100%;background-color:rgba(0,0,0,0.5);">
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);padding:10px;background-color:rgba(255,255,255,0.5);;">
<button id="cancel">cancel</button>
<button id="download">download</button>
</div>
</html>
`;
var iframeBlob = new Blob([iframeText],{type:"text/html"});
iframe.src = URL.createObjectURL(iframeBlob);
iframe.style.all = "unset";
iframe.style.position = "fixed";
iframe.style.width = "100%";
iframe.style.height = "100%";
document.body.appendChild(iframe);
iframe.onload=()=>{
URL.revokeObjectURL(iframe.src); // delete the object URL
// select the elements in the iframe and add event handlers
// to either share the file or dismiss and close the dialogue
// (the iframe will close automatically after being dismissed
// or if the user cancels share by tapping away)
iframe.contentDocument.querySelector("#download").onclick=()=>{
iframe.contentWindow.navigator.share({files:[file]}).then(()=>{
console.log("files shared");
iframe.contentWindow.close();
iframe.remove();
}).catch((err)=>{
console.error("user cancelled share");
iframe.contentWindow.close();
iframe.remove();
});
};
iframe.contentDocument.querySelector("#cancel").onclick=()=>{
iframe.contentWindow.close();
iframe.remove();
};
};
}
Thanks for all the answers on this really useful. I've played around with it and found the best solution for my use is to not use async and await and to use a promise instead as the share returns multiple times.
root.navigator.share({ title, url })
.then(() => {
/*
this will be hit even if the message is logged
due to iOS being a bit rubbish ATM
*/
})
.catch((err) => {
const { message = '' } = err || {};
if (message.indexOf('user denied permission') !== -1) {
console.log(message);
}
});

How do i monitor when an specific ID appears on the screen with javascript mutations?

I am using a site that is kind of a slideshow but doesn't show the "next" link after a few seconds. I want to do a script that clicks on that link as soon as it appears.
I researched and found out "mutations" however i have little experience with javascript. Here's what i got so far. The link i want to click shows up in chrome as:
a href="#" class="btn btn-step-active" id="btn-next-step">Next
Code:
function clickButton() {
document.getElementById('btn-next-step').click();
}
const app = document.querySelector('#btn-next-step');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes) {
const nextStepAdded = [...mutation.addedNodes].some((node) => node.className === 'btn btn-step active');
if (nextStepAdded) {
clickButton();
}
}
});
});
observer.observe(app, { childList: true, subtree: true });
However this doesn't seem to work, i get error "failed to execute observe on mutationobserver: parameter 1 is not of type node". any ideas?
EDIT: it appears the error is caused because the link doesn't exist... which is the whole purpose of what i am trying to do. but i put a parent id that is actually present when the page loads and now it worked!
The first parameter to observer.observe needs to be a node that is an ancestor of the node you are looking for not the node itself.
Something like:
observer.observe(document.body, ....)

Capture screenshot of Electron window before quitting

In an Electron app, I can take a screenshot of my window from the main process using this:
let win = new BrowserWindow(/* ... */);
let capturedPicFilePath = /* where I want that saved */
win.capturePage((img) => {
fs.writeFile(capturedPicFilePath, img.toPng(), () => console.log(`Saved ${capturedPicFilePath}`))
})
Awesome. Now I'd like to do that right before app quits. Electron emits a particular event for that, that I tried to use:
Event: 'before-quit' : emitted before the application starts closing its windows.
Problem: if I use the same code as above in a handler for that event, the file is created but empty.
I'm guessing it's because the screenshot is taken in an asynchronous way, and the window is already closed when it happens.
So this does not work for me:
app.on('before-quit', (event) => {
win.capturePage(function(img) {
fs.writeFile(capturedPicFilePath, img.toPng(), () => console.log(`Saved ${capturedPicFilePath}`))
})
})
Edit 1 : Doing this in the renderer process with window.onbeforeunload fails too. It's also too late to perform the screenshot. I get this in the main console (i.e. it goes to terminal):
Attempting to call a function in a renderer window that has been closed or released.
Context: for now I'm exploring the limits of what is possible to do with screen capture (essentially for support purposes), and found that one. Not sure yet what I would do with that edge case, I thought about it not just for support, I'm also considering displaying at startup a blurred pic of previous state (some parts of my app take 1-2 seconds to load).
Any ideas?
I have had a similar problem before, I got around it by using the window close event and then preventing it from closing. Once my action had performed I then ran app.quit().
window.on('close', function (event) {
event.preventDefault();
let capturedPicFilePath = /* where you want it saved */
window.capturePage((img) => {
fs.writeFile(capturedPicFilePath, img.toPng(), () =>
console.log(`Saved ${capturedPicFilePath}`));
app.quit(); // quit once screenshot has saved.
});
});
Hope this helps!

Issue with Protractor while testing a non angular js website

I am currently testing a non Angular js website with protractor. My code is as follows :
describe("Login ", function () {
it("test", co.wrap(function* () {
browser.ignoreSynchronization = true;
yield browser.get("URL");
var elmOK = element(by.css('a[href="#partner"]'))
yield elmOK.click();
expect(browser.getCurrentUrl()).toContain('login');
}));
});
Test Scenario : My test opens the URL mentioned and selects the link with href=#partner. A login page which should pop up. But when I run the test and the link is clicked the login page doesn't popp up.
Please tell me what I am doing wrong?
Protractor appears synchronous because it waits for angular to be stable prior to moving to the next action. So if the above was an angular page, it would:
load the page, then Protractor would wait for angular (wait for the page to be stable)
tell selenium webdriver to find the element and click on it.
In a non-angular page, setting the ignore synchronization set to true in the right direction. What you need to do is to come up with your own sleep between getting the page and clicking the element. After you click on the element, you'll also need to wait for the next window to pop up, change focus to the next window to see if the url contains login.
browser.ignoreSynchronization = true;
browser.get("URL");
// let the page load
browser.sleep(2000);
element(by.css('a[href="#partner"]')).click().then(() => {
// may have to add sleep in for the page to load
browser.sleep(2000);
// switch to the next window for the popup
browser.driver.getAllWindowHandles().then((windowHandles) => {
let popupLoginHandle = windowHandles[1];
browser.driver.switchTo().window(popupLoginHandle).then(() => {
// check to see if the pop up has a url that contains 'login'
expect(browser.getCurrentUrl()).toContain('login');
});
});
});
Putting Hard coded waits is not an good practice.
If you are using jasmine for reporting , you may put the following code inside conf.js:
jasmineNodeOpts: {
defaultTimeoutInterval: 600000,
},

Categories