Targeting a nested iframe using puppeteer - javascript

At the moment I'm trying to create some E2E tests which require logging into Excel online and then uploading an extension.
I was able to login, open Excel and click the upload plugin button, however, I cannot get any further.
So far I've figured out there are 2 iframes, one nested in another.
I access the first one once I open Excel
let targetIFrame = await this.page.frames().find(f => f.name() === 'sdx_ow_iframe');
The tricky part about the second one is that it only appears in the DOM after I click the "Upload Plugin" button and it is nested in the one I accessed above.
I've tried different delays etc, but it just looks like puppeteer does not see it.

Base on my research, you can construct an implementation to find the iframe include in the parent frame.
Please test the code as below:
/**
* #return {!Promise<ElementHandle>}
*/
async ownerFrame() {
if (!this._frame._parentFrame)
throw new Error('No parent frame');
const rootElementHandle = await this._frame.$('*');
const rootElementDescriptionHandle = await this._client.send('DOM.describeNode', { objectId: rootElementHandle._remoteObject.objectId });
const parentsIframes = await this._frame._parentFrame.$$('iframe');
if (!parentsIframes.length)
throw new Error('No iframe elements found in parent');
return parentsIframes.find(async parentsIframe => {
const iframeDescription = await this._client.send('DOM.describeNode', { objectId: parentsIframe._remoteObject.objectId, pierce: true, depth: 2 });
const iframesRootDescription = iframeDescription.node.contentDocument.children[0];
return iframesRootDescription.backendNodeId === rootElementDescriptionHandle.node.backendNodeId;
});
}

Related

Firestore DocumentSnapshot.data() returns undefined, but in the console it definetly has and it works for other documents

In this Firebase Function I'm getting two DocumentSnapshots, the first works fine, I can get the data (emailNonce) from the db, but the second DocumentSnapshot somehow has no data, the object is there, I can see it in the logs, but calling .data() on it returns undefined:
const addRentalFct = async (data, context) => {
// this works:
const secretsRef = db.collection('user-secrets').doc('Yv3gZU8TeJTixl0njm7kUXXpvhc2');
const secretsSnap = await secretsRef.get();
const dbNonce = secretsSnap.data().emailNonce;
functions.logger.log('got the dbNonce: ', dbNonce);
// this doesn't work, but ir's the same logic as above:
const boxesSecretsRef = db.collection('box-secrets').doc('CB8lNQ8ZUnv4FDT6ZXGW');
const boxSecretsSnap = await boxesSecretsRef.get();
functions.logger.log('got the boxSecretsSnap: ', boxSecretsSnap);
functions.logger.log('got the boxSecretsSnap.data(): ', boxSecretsSnap.data());
const boxPassword = boxSecretsSnap.data().password;
functions.logger.log('the box secret is: ', boxPassword);
...
}
The DB:
box-secrets collection
user-secrets:
(the secrets are from my dev environment)
The problem was that I copied the id for the new document from an already existing document in the console like this:
Automatically there was a space added in front. When I created the new doc, the space was not visible, but I could create another doc with the same id, without the space in front. Here you see that it
s not that obvious, it looks like there are two docs with the exact same id:
When having it like this, the firebase function didn't find any of the two docs. I had delete both and readd it without space, then it worked.

Playwright js - im trying to validate when clicking on a button it opens a new tab , then validates the title

So i have written a function that in short clicks an FAQs button and validate that the new tab is open on the same browser Context. the issue i have when running my test is i get:
TypeError: Cannot read property 'title' of undefined
Heres my function - PATH src/tests/logoutAndFaqs.spec.ts:
async shouldSeeFaqsInNewTab() {
const browserName = await chromium.launch();
const context = await browserName.newContext();
const pages = await context.pages();
await this.click(HomeScreen.faqButton);
await this.page.waitForTimeout(1000);
expect(await pages[1].title())?.toBe("Title");
}
and heres is the function getting called - PATH src/pages/home.page.ts:
import test from "../../helpers/base.page";
test.describe("Ensure you land on the home page when logged in", () => {
test.beforeEach(async ({ Home }) => {
await home.gotoHomePage();
});
test.only("Validate that FAQs opens in a new tab", async ({ home }) => {
await home.shouldSeeFaqsInNewTab();
});
});
i know that this line expect(await pages[1].title())?.toBe("Title") specifically is accessing the tab opened within the window and within the browserContext its validating the title expecting a string to equal "Title".
specifically title() is causing an error due to being an unassign value. im trying to understand why this error? and how to fix it. thanks
In this cases given your info, you should check if you're instantiating your page object correctly with the correct innerHTML.
If not, it could not create the object properly and because of that you're getting undefined.
So i manage to to solve this part by using the Promise.all method, which resolves by waiting for the popup when clicking on the button that opens a new tab, .waitForEvent("popup") method - heres the method i wrote to solve this:
async navigateAndValidatePopUpPage() {
const [popUpPage] = await Promise.all([this.page.waitForEvent("popup"), this.click(rmsHomeScreen.newTabPageButton)]);
await this.waitForDomToLoad(popUpPage);
expect(await popUpPage.title()).toBe("Enter title of the popup page here");
}
as for this line await this.waitForDomToLoad(popUpPage);, specifically this .waitForDomToLoad, is simply assign to this method :
async waitForDomToLoad(pageToWait: Page) {
await pageToWait.waitForLoadState("domcontentloaded");
}
waiting for event
Promise.all()
waitForLoadState

How to target the selector on new tab after clicking a[target="_blank"] - Failing to activate the new tab created

After clicking a[target="_blank"] new tab opens. How to get code to get new page object so I can access password input field?
Using NodeJS, JavaScript, Puppeteer.
Navigation is working up to the point included below.
EDIT: I used the page.url() method to retrieve current URL and the URL of the newly created tab does not log to console, previous page logs.
I tried adjusting the script and received following errors
Cannot read properties of undefined (reading 'page') - I thought adding a time delay would solve this but no go.
I was having this error but as the code is below I do not get this error: No node found for selector: #Password
I have looked at related issues
I came across dheerajbhaskar GitHub issue and read up on related issues
#386
#3535
#978
and more
I tried to implement code from an accepted answer without any success.
Using Puppeteer to get a handle to the new page after "_blank" click?
try {
await sleep(2300)
// This block creates a new tab
// I was previously using a selector and not mouse click API
await Promise.all([
page.mouse.click(xToolsBtn, yToolsBtn, { delay: 2000 }),
])
// NEW TARGET CREATED
// Below is a snippet from an accepted answer but the the type method
// does not work
// Seems like page is still not activated
const [newTarget] = await Promise.all([
// Await new target to be created with the proper opener
new Promise((x) =>
browser.on("targetcreated", (target) => {
if (target.opener() !== page.target()) return
browser.removeListener("targetcreated", arguments.callee)
x()
})
),
// page.click('link')
])
// Trying to input password without success
const newPage = await newTarget.newPage()
await newPage.type("#Password", process.env.PASSWORD, {
delay: randomGenerator,
})
} catch (err) {
console.error(
"LOGIN BUTTON FAIL",
err.message
)
}
Alternatively atempt#1: I tried to select the input via mouse x, y co-ordinates which activates the input field but this returns the following error"
No node found for selector: #Password
Alternatively atempt#2:
//* WAIT FOR TARGET
try {
await sleep(2300)
await Promise.all([
page.mouse.click(xToolsBtn, yToolsBtn, { delay: 2000 }),
])
sleep(5000)
await page.evaluate(() => window.open(`${loginUrl3}`))
const newWindowTarget = await browser.waitForTarget(
(target) => target.url() === `${loginUrl3}`
)
console.log("GOT TARGET")
await newWindowTarget.type("#Password", process.env.PASSWORD, {
delay: randomGenerator,
})
} catch (err) {
console.log("WAIT FOR TARGET FAILED")
}
Note: URLS are randomly generated so I would be curious what if any work around there is to use current URL. I would assume the new tab created would still need to be activated...
managed to solve this together (Linker :)
Process
First, we mapped the target being created to check for focus
browser.on('targetcreated', function (target) {
console.log('New tab:');
console.log(target);
});
We saw the URL is trying to open - for some reason the URLs in the target were empty. We re-installed stuff to rule out weird dependency bugs, then figured there's a focus issue.
Workaround
To solve it, we need to wait for the .newPage() to open before goto'ing to the URL, calling bringToFront() and then waiting for it to load (short sleep is the easy way). Once we did that, we had a working POC to start from.
Relevant part from the solution:
let mappedURL = tabs
.map((e, index) => e.url())
.filter((e, idx) => idx == 2)
console.log("MAPPED URL ", mappedURL)
sleep(2500)
const page3 = await browser.newPage()
await page3.goto(`${mappedURL}`)
await page3.bringToFront()
Ref
Here's a cool SO Answer showing how to use once syntax to test the event. Happy we were able to solve it, and I hope the process helps other people out.
Addressing just the question in the title, "How to target the selector on new tab after clicking a[target="_blank"]" -
Handling newly opened tabs in Playwright is far from intuitive if you're not used to it. A summary of how they work:
If you click a link in your test with target="_blank", which opens a new tab, the page object you're working with still refers to the original page/tab you opened the link on.
To get ahold of the new page, you have to:
const [newPage] = await Promise.all([
context.waitForEvent('page'), // get `context` by destructuring with `page` in the test params; 'page' is a built-in event, and **you must wait for this like this,**, or `newPage` will just be the response object, rather than an actual Playwright page object.
page.locator('text=Click me').click() // note that, like all waiting in Playwright, this is somewhat unintuitive. This is the action which is *causing the navigation*; you have to set up the wait *before* it happens, hence the use of Promise.all().
])
await newPage.waitForLoadState(); // wait for the new tab to fully load
// now, use `newPage` to access the newly opened tab, rather than `page`, which will still refer to the original page/tab.
await expect(newPage).toHaveURL('http://www.someURL.com');
await newPage.locator('text=someText');

How to access React Event Handlers with Puppeteer

I'm not entirely sure I understand what I'm asking for, and I'm hoping someone can explain. I'm attempting to scrape a website using Puppeteer on NodeJS. I've gotten as far as selecting the element I need and accessing it's properties, however, I cannot access the property I need to pull the information I want. The information I want is within the green box below, however I cannot get past the __reactEventHandlers$kq2rgk91p6 as that just returns undefined.
I used the following selector, which works and accesses all other properties, just not the one I want.
const checked = await page.evaluate(() => document.querySelector(stockSelector));
If I understand correctly (without the URL and minimal reproducible code it is hard to guess), this is the issue: according to the docs, various eval functions can transfer only serializable data (roughly, the data JSON can handle, with some additions). Your code returns a DOM element, which is not serializable (it has methods and circular references). Try to retrieve the data in the browser context and returns only serializable data. For example:
const data = await page.evaluate(
selector => document.querySelector(selector)
.__reactEventHandlers$kq2rgk91p6.children[1].props.record.Stock,
selector,
);
If the array in the .Stockproperty is serializable, you will get the data.
I am using this function to extract React props, it helps to deal with the random characters at the end of react event handler. If you are not sure which childIndex to use, check React Chrome extension to navigate to the element.
const extractProps = async (elementHandle, childIndex) => {
let elementHandlerProperties = await elementHandle.getProperties()
for (let elProp of elementHandlerProperties) {
let key = elProp[0]
if (key.startsWith("__reactEventHandler")) {
let reactEventHandler = elProp[1]
let children = await reactEventHandler.getProperty("children")
let child = await children.getProperty(childIndex.toString())
let reactProps = await child.getProperty("props")
return reactProps
}
}
return null
}
Usage:
const selector = ".some-class"
const elementHandle = await page.$(selector);
let reactProps = await extractProps(elementHandle, 1)
let prop1 = await reactProps.getProperty("prop1")
console.log(await prop1.jsonValue())

Can't access innerText property using Puppeteer - .$$eval and .$$ is not yielding results - JavaScript

I am working on a web scraper that searches Google for certain things and then pulls text from the result page, and I am having an issue getting Puppeteer to return the text I need. What I want to return is an array of strings.
Let's say I have a couple nested divs within a div, and each has text like so:
<div class='mainDiv'>
<div>Mary Doe </div>
<div> James Dean </div>
</div>
In the DOM, I can do the following to get the result I need:
document.querySelectorAll('.mainDiv')[0].innerText.split('\n')
This yields: ["Mary Doe", "James Dean"].
I understand that Puppeteer doesn't return NodeLists, and instead it uses JSHandles, but I still can't figure out how to get any information using the prescribed methods. See below for what I have tried in Puppeteer and the corresponding console output:
In every scenario, I do await page.waitFor('selector') to start.
Scenario 1 (using .$$eval()):
const genreElements = await page.$$eval('div.mainDiv', el => el);
console.log(genreElements) // []
Scenario 2 (using evaluate):
function extractItems() {
const extractedElements = document.querySelectorAll('div.mainDiv')[0].innerText.split('\n')
return extractedElements
}
let items = await page.evaluate(extractItems)
console.log(items) // UnhandledPromiseRejectionWarning: Error: Evaluation failed: TypeError: Cannot read property 'innerText' of undefined
Scenario 3 (using evaluateHandle):
const selectorHandle = await page.evaluateHandle(() => document.querySelectorAll('div.mainDiv'))
const resultHandle = await page.evaluate(x => x[0], selectorHandle)
console.log(resultHandle) // undefined
Any help or guidance on how I am implementing or how to achieve what I am looking to do is much appreciated. Thank you!
Use page.$$eval() or page.evaluate():
You can use page.$$eval() or page.evaluate() to run Array.from(document.querySelectorAll()) within the page context and map() the innerText of each element to the result array:
const names_1 = await page.$$eval('.mainDiv > div', divs => divs.map(div => div.innerText));
const names_2 = await page.evaluate(() => Array.from(document.querySelectorAll('.mainDiv > div'), div => div.innerText));
Note: Keep in mind that if you use Puppeteer to automate searches on Google, you may be temporarily blocked and end up with an "Unusual traffic from your computer network" notice, requiring you to solve a reCAPTCHA. This may break your web scraper, so proceed with caution.
Try it like this:
let names = page.evaluate(() => [...document.querySelectorAll('.mainDiv div')].map(div => div.innerText))
That way you can test the whole thing in the chrome console.
Using page.$eval:
const names = await page.$eval('.mainDiv', (element) => {
return element.innerText
});
Here the element is retrieved by selector and directly passed to the function to be evaluated.
Using page.evaluate:
const namesElem = await page.$('.mainDiv');
const names = await page.evaluate(namesElem => namesElem.innerText, namesElem);
This is basically the first method split up into two steps. The interesting part is that ElementHandles can be passed as arguments in page.evaluate() and can be evaluated like JSHandles.
Note that for simplicity and clarification I used the methods for retrieving single elements. But page.$$() and page.$$eval() work the same way while selecting multiple elements and returning an array instead.

Categories