How to get classList on a selector in playwright? - javascript

This is my start with playwright and I try to test my React app with it.
It might be that there is a similar question somewhere here, however I've tried all the possible non-specific answers from StackOverflow and Github issues.
This is my test:
import {expect, test} from "#playwright/test";
test.describe('App general functionality', () => {
test('Theme switches normally', async ({page}) => {
const app = await page.getByTestId('app');
const themeSwitch = await page.getByTestId('themeSwitch');
const classList = await app.evaluate(node => {
console.log(node);
});
// const classList = await app.getAttribute('class');
});
});
I've tried installing extended expect types for toHaveClass and checked if app is present. In console it returns locator and elements inside the app. App is a test id on the root div of the application.
However the error is constant:
locator.evaluate: Target closed
=========================== logs ===========================
waiting for getByTestId('app')
============================================================
And it is one this line:
const classList = await app.evaluate // or app.getAttribute('class')
The app div:
<div data-test-id={'app'} className={`app ${appStore.isDarkTheme ? 'dark' : 'light'}`}>
Git url

I don't think the test id is set properly. It should be data-testid based on the following experiment:
import {expect, test} from "#playwright/test"; // ^1.30.0
const html = `<!DOCTYPE html>
<html><body><div data-testid="app" class="foo bar baz">hi</div></body></html>`;
test.describe("App general functionality", () => {
test("Theme switches normally", async ({page}) => {
await page.setContent(html); // for easy reproducibility
const el = await page.getByTestId("app");
const classList = await el.evaluate(el => [...el.classList]);
expect(classList).toEqual(["foo", "bar", "baz"]);
});
});
If you can't change the element's property, maybe try
const el = await page.$('[data-test-id="app"]');
Pehaps a bit more idiomatic (assuming you've fixed testid):
await expect(page.getByTestId("app")).toHaveClass(/\bfoo\b/);
await expect(page.getByTestId("app")).toHaveClass(/\bbar\b/);
await expect(page.getByTestId("app")).toHaveClass(/\bbaz\b/);
This handles extra classes and different ordering of the classes. See also Check if element class contains string using playwright.

Related

How to use PageObject in the tab which has been created during test run?

When I try to use my pageObjects in a newly created tab (during the test run), the test tries to interact with the base page.
Working with a newly created tab is working as long as I use const = [newPage], eg. newPage.locator('someLocator').click().
I want to avoid using detailed actions in the test, I just want to make function in the pageObject and reuse it with newPage.
my code:
pageObject:
export class SharedPage {
/**
* #param {import('#playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.addToCartButton = page.locator('text=Add to cart');
}
async addToCartButtonClick() {
await this.addToCartButton.click();
}
}
Test:
import { test } from '#playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { ProductsPage } from '../../pages/ProductsPage';
import { SharedPage } from '../../pages/SharedPage';
const newPageLocators = {
sauceLabsOnesie: 'text=Sauce Labs Onesie',
sauceLabsBackpack: 'Sauce Labs Backpack',
};
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goTo('inventory.html');
});
test('As a user I want to open a new tab and visit it', async ({
page,
context,
}) => {
const productsPage = new ProductsPage(page);
const [newPage] = await Promise.all([
context.waitForEvent('page'),
productsPage.openNewTabWithProduct(newPageLocators.sauceLabsBackpack),
]);
const sharedPage = new SharedPage(newPage);
productsPage.selectProductByText(newPage, newPageLocators.sauceLabsOnesie);
newPage.locator(newPageLocators.sauceLabsOnesie).click(); // it works
sharedPage.addToCartButtonClick(); // it doesn't work, test tries to perform this step on the base page
});
The fundamental problem is missing awaits in front of asynchronous Playwright API calls as well as your custom wrappers on them:
// ...
const sharedPage = new SharedPage(newPage);
await productsPage.selectProductByText(newPage, newPageLocators.sauceLabsOnesie);
await newPage.locator(newPageLocators.sauceLabsOnesie).click();
await sharedPage.addToCartButtonClick();
// ...
For your constructor, async/await isn't possible. See Async/Await Class Constructor. Luckily, Playwright lets you chain locator().click() with a single await so your class code looks OK for now, but it's something to bear in mind as you add more code.

Cannot access element in Playwright global setup script

I'm trying to use Playwright to automate authentication in my web application.
When I did the authentication test in a typical .spec.ts file, it succeeded:
test('bnblnlnnl', async ({ page }) => {
await page.goto('/');
await page.getByTestId('auth-github-auth-button').click();
await page.getByLabel('Username or email address').fill('automations#blabla');
await page.getByLabel('Password').fill('sdfgsdgsdfgfgf');
await page.getByRole('button', { name: 'Sign in' }).click();
const authorizeElement = page.getByRole('button', { name: 'Authorize blabla' });
const shouldAuthorize = await authorizeElement.isVisible();
if (shouldAuthorize) {
await authorizeElement.click();
}
const navElemnt = page.getByTestId('nav');
await expect(navElemnt).toBeVisible();
await expect(page).toHaveURL('/');
});
So this test successfully completes. Then, according to this documentation: https://playwright.dev/docs/auth
I can authenticate already in the global setup script, instead of authenticating before each test block. To do so, I have this script for my global setup file:
import { chromium } from '#playwright/test';
const globalSetup = async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:8080/');
await page.getByTestId('auth-github-auth-button').click();
await page.getByLabel('Username or email address').fill('gfsdagdf');
await page.getByLabel('Password').fill('sadfsdfsdfs');
await page.getByRole('button', { name: 'Sign in' }).click();
const authorizeElement = page.getByRole('button', { name: 'Authorize dfssd' });
const shouldAuthorize = await authorizeElement.isVisible();
if (shouldAuthorize) {
await authorizeElement.click();
}
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
};
export default globalSetup;
But when I run playwright test I get a timeout from this statement: await page.getByTestId('auth-github-auth-button').click();.
The error message:
{
"name": "TimeoutError"
}
So I checked, during test process- I browsed to http://localhost:8080 and I saw my web app is running, and the element with id auth-github-auth-button does present, including its data-test-id attribute. So why playwright fails to locate it?
This is my playwright.config.ts file:
import { defineConfig } from '#playwright/test';
const configuration = defineConfig({
testDir: './tests',
testIgnore: 'scripts',
globalSetup: './tests/scripts/global-setup.ts',
globalTeardown: './tests/scripts/global-teardown.ts',
reporter: [['html', { open: 'never' }]],
use: {
testIdAttribute: 'data-test-id',
baseURL: 'http://localhost:8080',
storageState: 'storageState.json',
},
});
export default configuration;
As you noted in your answer, the issue was that the config doesn’t affect the global setup, and so Playwright tried to use the default data-testid attribute instead of your custom attribute.
While one solution would be to switch to using data-testid attributes instead to match the default, I wanted to offer up an alternative to keep your custom attribute. According to the Playwright docs on setting a custom test id attribute, “you can configure it in your test config or by calling selectors.setTestIdAttribute().” While the config option won’t automatically work for the global setup as you mentioned in your answer, you should be able to use it as passed into your setup along with selectors.setTestIdAttribute() to use your custom attribute as expected.
So this suggested change to the top of your setup file should theoretically make it work as you expected:
import { chromium, selectors, FullConfig } from '#playwright/test';
const globalSetup = async (config: FullConfig) => {
const { testIdAttribute } = config.projects[0].use;
selectors.setTestIdAttribute(testIdAttribute);
const browser = await chromium.launch();
See the docs about global setup for their example of using the config object inside the setup to reuse values. Theirs uses baseURL and storageState, which you may find value in as well.
Hope that helps!
The issue is that I'm using data-test-id but in global setup script only data-testid will work as it's not configurable. Changing all my attributes to data-testid solved it

unable to get ElementHandle when filtering puppeteer elements

I am trying to use Puppeteer to scrape data from https://pagespeed.web.dev - I need to be able to take screenshots of the results and while it would be much simpler to use Google's own API to get the raw data, I can't screenshot the actual results that way. The challenge I'm having is filtering out DOM elements while still retaining the ElementHandle nature of the objects. For example, getting the "This URL" / "Origin" buttons:
In a normal JS console, I would run this:
[...document.querySelectorAll('button')].filter(b => b.innerText === 'This URL')
This would give me an Array of DOM elements that I could then run click() on or whatever.
I have tried a number of ways to get Puppeteer to give me a usable ElementHandle object and they have all returned an array of objects, with the sole member of that object being an __incrementalDOMData object:
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto(`https://pagespeed.web.dev/report?url=${url}`)
await page.waitForSelector(homepageWaitSelector, { visible: true })
// here's where the fun starts
const buttons = await page.$$eval('button', buttons => buttons.filter(button => button.innerText === 'This URL'))
const buttons = await page.$$eval('button', buttons => buttons.map(b => b.innerText).filter(t => t === 'This URL'))
// This one seems to run because the list of elements returned has the right length, but I can never get a breakpoint to catch inside the `evaluate` method, nor does a `console.log` statement actually print.
const buttons = await page.evaluate(() => {
const b = [...document.querySelectorAll('button')].filter(b => b.innerText === 'This URL')
return b
})
All of those methods end up returning something like this:
[{
__incrementalDOMData: {
j: ['class', 'the-classes-used'],
key: 'a key',
v: true
}]
Because there are so many buttons, and because the class names are all random and reused, I can't just target the ones I want (I mean I suppose I could build a super precise selector), filter them, and then return not just the filtered data but the actual elements themselves. Is what I'm asking for even possible?
Thanks to ggorlen's comment above I simplified my approach and got something working without an overly complex selector or logic:
const buttonContainer = await page.$('div.R3HzDf.ucyZQe')
const thisUrlButton = await buttonContainer.$(':first-child')
await thisUrlButton.evaluate(b => b.click())
await page.screenshot({ path: 'homepage.png' })
const originButton = await buttonContainer.$(':last-child')
await originButton.evaluate(b => b.click())
await page.screenshot({ path: 'origin.png' })

Unable to scrape certain elements with Cheerio

I'm trying to scrape a product page using pupeteer and Cheerio. (this page)
I'm using a data id to scrape the title and image. The problem is that the title never gets scraped while the image does every time.
I've tried scraping the title by class name but that doesn't work either. Does this have something to do with the specfic website I'm trying to scrape? Thank you.
my code:
// Load cheerio
const $ = cheerio.load(data);
/* Scrape Product Page */
const product = [];
// Title
$('[data-testid="product-name"]').each(() => {
product.push({
title: $(this).text(),
});
});
// Image
$('[data-testid="product-detail-image"]').each((index, value) => {
const imgSrc = $(value).attr('src');
product.push({
image: imgSrc,
});
});
As I mentioned in the comments, there's almost no use case I can think of that makes sense with both Puppeteer and Cheerio at once. If the data is static, use Cheerio alongside a simple request library like Axios, otherwise use Puppeteer and skip Cheerio entirely in favor of native Puppeteer selectors.
One other potential reason to use Puppeteer is if your requests library is being blocked by the server's robot detector, as appears to be the case here.
This script worked for me:
const puppeteer = require("puppeteer");
let browser;
(async () => {
browser = await puppeteer.launch({headless: true});
const [page] = await browser.pages();
const url = "https://stockx.com/nike-air-force-1-low-white-07";
await page.goto(url);
const nameSel = '[data-testid="product-name"]';
await page.waitForSelector(nameSel, {timeout: 60000});
const name = await page.$eval(nameSel, el => el.textContent);
const imgSel = '[data-testid="product-detail-image"]';
await page.waitForSelector(imgSel);
const src = await page.$eval(imgSel, el => el.src);
console.log(name, src);
})()
.catch(err => console.error(err))
.finally(() => browser?.close())
;

testcafe running different tests based on browser

I was wondering if there is a way to somehow pass a parameter to let your fixture or even all tests know which browser they are running in.
In my particular case, I would use that parameter to simply assign a corresponding value to a variable inside my tests.
For example,
switch(browser) {
case 'chrome':
chrome = 'chrome.com';
break;
case 'firefox':
link = 'firefox.com';
break;
case 'safari':
link = 'safari.com';
break;
default:
break;
}
Currently, I was able to achieve something similar by adding a global node variable and it looks something like this:
"chrome": "BROWSER=1 node runner.js"
However, this makes me create a separate runner for every browser (safari-runner, chrome-runner etc.) and I would like to have everything in one place.
So at the end of the day, I would need to make this work:
const createTestCafe = require('testcafe');
let testcafe = null;
createTestCafe('localhost', 1337, 1338)
.then(tc => {
testcafe = tc;
const runner = testcafe.createRunner();
return runner
.src('test.js')
.browsers(['all browsers'])
.run({
passBrowserId: true // I guess it would look something like this
});
})
.then(failedCount => {
console.log('Tests failed: ' + failedCount);
testcafe.close();
})
.catch(error => {
console.log(error);
testcafe.close();
});
There are several ways to get browser info:
Get navigator.userAgent from the browser using ClientFunction. Optionally you can use a module to parse an user agent string, for example: ua-parser-js.
import { ClientFunction } from 'testcafe';
import uaParser from 'ua-parser-js';
fixture `get ua`
.page `https://testcafe.devexpress.com/`;
const getUA = ClientFunction(() => navigator.userAgent);
test('get ua', async t => {
const ua = await getUA();
console.log(uaParser(ua).browser.name);
});
Use RequestLogger to obtain browser info. For example:
import { RequestLogger } from 'testcafe';
const logger = RequestLogger('https://testcafe.devexpress.com/');
fixture `test`
.page('https://testcafe.devexpress.com')
.requestHooks(logger);
test('test 1', async t => {
await t.expect(logger.contains(record => record.response.statusCode === 200)).ok();
const logRecord = logger.requests[0];
console.log(logRecord.userAgent);
});
The TestCafe team is working on the t.browserInfo function, which solves the issue in the future.
Just to update this question, testcafe has now implemented t.browser, which will allow you to check the browser.name or browser.alias to determine which browser you're running in.
import { t } from 'testcafe';
const browserIncludes = b => t.browser.name.includes(b);
const isBrowserStack = () => t.browser.alias.includes('browserstack');
fixture `test`
.page('https://testcafe.devexpress.com')
test('is Chrome?', async () => {
console.log(t.browser.name);
await t.expect(browserIncludes('Chrome').ok();
await t.expect(isBrowserStack()).notOk();
});

Categories

Resources