Im recently learning JavaScript and TypeScript and trying to create a test automation framework with Playwright and having a small issue.
At " test.only " block, I have created the destructor. But I want to make this destructor global so I can use it in other test code blocks so I don't need to keep create a Destructor for every test block. I tried to create it on test.beforeEach code block but it gave an error and didn't allow me to use in other blocks. Is there any way or idea to solve this issue so I can just create Destructor once and use it in all code blocks ?
Thank you very much!
import { FeedbackPage } from '../../pages/FeedbackPage';
import { HomePage } from '../../pages/HomePage';
test.describe('Feedback Form', () => {
let homePage: HomePage;
let feedbackPage: FeedbackPage;
test.beforeEach(async ({ page }) => {
homePage = new HomePage(page);
feedbackPage = new FeedbackPage(page);
await homePage.visit();
await homePage.clickOnFeedbackLink();
});
test.only('Submit feedback form', async ({ page }) => {
const {nameInput, emailInput, subjectInput, commentInput} = feedbackPage;
// await feedbackPage.fillForm(
// 'name',
// 'email#email.com',
// 'subject',
// 'my awesome message'
// );
await nameInput.type('Name');
await emailInput.type('email#email.com');
await subjectInput.type('subject');
await commentInput.type('my awesome message');
await page.pause()
await feedbackPage.submitForm();
await feedbackPage.feedbackFormSent();
});
});```
Related
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.
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
I just started coding, and I was wondering if there was a way to open multiple tabs concurrently with one another. Currently, my code goes something like this:
const puppeteer = require("puppeteer");
const rand_url = "https://www.google.com";
async function initBrowser() {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto(rand_url);
await page.setViewport({
width: 1200,
height: 800,
});
return page;
}
async function login(page) {
await page.goto("https://www.google.com");
await page.waitFor(100);
await page.type("input[id ='user_login'", "xxx");
await page.waitFor(100);
await page.type("input[id ='user_password'", "xxx");
}
this is not my exact code, replaced with different aliases, but you get the idea. I was wondering if there was anyone out there that knows the code that allows this same exact browser to be opened on multiple instances, replacing the respective login info only. Of course, it would be great to prevent my IP from getting banned too, so if there was a way to apply proxies to each respective "browser"/ instance, that would be perfect.
Lastly, I would like to know whether or not playwright or puppeteer is superior in the way they can handle these multiple instances. I don't even know if this is a possibility, but please enlighten me. I want to learn more.
You can use multiple browser window as different login/cookies.
For simplicity, you can use the puppeteer-cluster module by Thomas Dondorf.
This module can make your puppeteer launched and queued one by one so that you can use this to automating your login, and even save login cookies for the next launches.
Feel free to go to the Github: https://github.com/thomasdondorf/puppeteer-cluster
const { Cluster } = require('puppeteer-cluster')
(async () => {
const cluster = await Cluster.launch({
concurrency: Cluster.CONCURRENCY_CONTEXT,
maxConcurrency: 2, // <= this is the number of
// parallel task running simultaneously
}) // You can change to the number of CPU
const cpuNumber = require('os').cpus().length // for example
await cluster.task(async ({ page, data: [username, password] }) => {
await page.goto('https://www.example.com')
await page.waitForTimeout(100)
await page.type('input[id ="user_login"', username)
await page.waitForTimeout(100)
await page.type('input[id ="user_password"', password)
const screen = await page.screenshot()
// Store screenshot, Save Cookies, do something else
});
cluster.queue(['myFirstUsername', 'PassW0Rd1'])
cluster.queue(['anotherUsername', 'Secr3tAgent!'])
// cluster.queue([username, password])
// username and password array passed into cluster task function
// many more pages/account
await cluster.idle()
await cluster.close()
})()
For Playwright, sadly still unsupported by the module above,you can use browser pool (cluster) module to automating the Playwright launcher.
And for proxy usage, I recommend Puppeteer library as the legendary one.
Don't forget to choose my answer as the right one, if this helps you.
There are profiling and proxy options; you could combine them to achieve your goal:
Profile, https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
import { chromium } from 'playwright'
const userDataDir = /tmp/ + process.argv[2]
const browserContext = await chromium.launchPersistentContext(userDataDir)
// ...
Proxy, https://playwright.dev/docs/api/class-browsertype#browser-type-launch
import { chromium } from 'playwright'
const proxy = { /* secret */ }
const browser = await chromium.launch({
proxy: { server: 'pre-context' }
})
const browserContext = await browser.newContext({
proxy: {
server: `http://${proxy.ip}:${proxy.port}`,
username: proxy.username,
password: proxy.password,
}
})
// ...
I am a having issues with implementing generic-pool using puppeteer. Below is my relevant part of the code.
UPDATE
Thanks #Jacob for the help and i am more clear about the concept and how it works and the code is also more readable and clear. I am still having issues where a generic pool is getting created on every request. How do i ensure that the same generic pool is used every time instead of creating new one
browser-pool.js
const genericPool = require('generic-pool');
const puppeteer = require('puppeteer');
class BrowserPool {
static async getPool() {
const browserParams = process.env.NODE_ENV == 'development' ? {
headless: false,
devtools: false,
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
}
:
{
headless: true,
devtools: false,
executablePath: 'google-chrome-unstable',
args: ['--no-sandbox', '--disable-dev-shm-usage']
};
const factory = {
create: function() {
return puppeteer.launch(browserParams);
},
destroy: function(instance) {
console.log('closing browser in hrere.....');
instance.close();
}
};
const opts = {
max: 5
};
this.myBrowserPool = genericPool.createPool(factory, opts);
}
static async returnPool() {
if (this.myBrowserPool == "") {
getPool();
}
return this.myBrowserPool.acquire();
}
}
BrowserPool.myBrowserPool = null;
module.exports = BrowserPool;
process-export.js
const BrowserPool = require('./browser-pool');
async function performExport(params){
const myPool = BrowserPool.getPool();
const resp = BrowserPool.myBrowserPool.acquire().then(async function(client){
try {
const url = config.get('url');
const page = await client.newPage();
await page.goto(url, {waitUntil: ['networkidle2', 'domcontentloaded']});
let gotoUrl = `${url}/dashboards/${exportParams.dashboardId}?csv_export_id=${exportParams.csvExportId}`;
//more processing
await page.goto(gotoUrl, {waitUntil: 'networkidle2' })
await myPool().myBrowserPool.release(client);
return Data;
} catch(err) {
try {
const l = await BrowserPool.myBrowserPool.destroy(client);
} catch(e) {
}
return err;
}
}).catch(function(err) {
return err;
});
return resp;
}
module.exports.performExport = performExport;
My understanding is that
1) When the application starts I can spin up for example 2 chromium instances and then when ever i want to visit a page i can use either of the two connections, so the browsers are essentially open and we improve the performance since the browser start can take time. is this correct?
2) Where do I place the acquire() code, I understand this should be in the app.js, so we acquire the instances rite when the app boots, but my pupeteer code is in a different file, how do i pass the browser reference in the file which has my pupeteer code.
When I use the above the code, a new browser instances spins up every time and the max property is not considered and it opens up as many instances are requested.
My apologies if its something very trial and i might have not understood the concept fully. Any help in clarifying this would be really helpful.
When using a pool, you'll need to use .acquire() to obtain an object, and then .release() when you're done so the object is returned to the pool and made available to something else. Without using .release(), you'd might as well have no pool at all. I like to use this helper pattern with pools:
class BrowserPool {
// ...
static async withBrowser(fn) {
const pool = BrowserPool.myBrowserPool;
const browser = await pool.acquire();
try {
await fn(browser);
} finally {
pool.release(browser);
}
}
}
This can be used like this anywhere in your code:
await BrowserPool.withBrowser(async browser => {
await browser.doSomeThing();
await browser.doSomeThingElse();
});
The key is the finally clause makes sure that whether your tasks complete or throw an error, you'll cleanly release the browser back to the pool every time.
It sounds like you might have the concept of the max option backwards as well and are expecting the browser instances to be spawned up to max. Rather, max means "only create up to max number of resources." If you try to acquire a sixth resource without anything having been released, for example, the acquire(...) call will block until one item is returned to the pool.
The min option, on the other hand, means "keep at least this many items on hand at all times", which you can use to pre-allocate resources. If you want 5 items to be created in advance, set min to 5. If you want 5 items and only five items to be created, set both min and max to 5.
Update:
I notice in your original code that you destroy in case of error and release when there isn't an error. Still would prefer the benefit of a wrapper function like mine to centralize all resource acquiring/releasing logic (the SRP approach). Here's how it could be updated to automatically destroy on errors instead:
class BrowserPool {
// ...
static async withBrowser(fn) {
const pool = BrowserPool.myBrowserPool;
const browser = await pool.acquire();
try {
await fn(browser);
pool.release(browser);
} catch (err) {
await pool.destroy(browser);
throw err;
}
}
}
Addendum
Figuring out what's going on in your code will be easier if you embrace the async function instead of mixing async function stuff and Promise callback stuff. Here's how it can be rewritten:
async function performExport(params){
const myPool = BrowserPool.myBrowserPool;
const client = await myPool.acquire();
try {
const url = config.get('url');
const page = await client.newPage();
await page.goto(url, {waitUntil: ['networkidle2', 'domcontentloaded']});
let gotoUrl = `${url}/dashboards/${exportParams.dashboardId}?csv_export_id=${exportParams.csvExportId}`;
//more processing
await page.goto(gotoUrl, {waitUntil: 'networkidle2' })
await myPool.release(client);
return Data;
} catch(err) {
try {
const l = await myPool.destroy(client);
} catch(e) {
}
return err; // Are you sure you want to do this? Would suggest throw err.
}
}
I'm testing a website that needs authentication with different users. It's working most of the time but sometimes, login fails and Testcafé doesn't detect it before running into the actual test code. Rather than raising an error into the login method, it fails when finding a DOM element in the test page. So it keeps the wrong login information and other tests with the same user will fail too.
I know the way to detect a login error on my website but I can't say to Testcafé:
"Hey! Something wrong appends when login, don't save login information for this user and try again in next tests"
EDIT:
Rather than using hardcoded login information, I use a separate file logins.ts with the following structure and I adapt it to add loggedIn and role fields :
adminUserCredentials: { login: 'mylogin', pwd: 'mypass', role: null, loggedIn: false }
Then I use it as follow:
function createUserForSpecificEnv(user: User, baseUrl: string): Role {
if(!user.loggedIn) {
user.role = Role(baseUrl, async t => {
await t
.wait(1000)
.typeText('#loginInput', user.login)
.typeText('#passwordInput', user.pwd)
.click('#Btn')
if(await Selector('#user-info').visible) {
user.loggedIn = true
}
})
}
return user.role
}
const adminUserRole = getRole(adminUserCredentials)
test("test 1", async t => {
t.useRole(adminUserRole)
}) // The test go on the login page, auth failed (expected) and can't find #user-info (expected)
test("test 2", async t => {
t.useRole(adminUserRole)
}) // The test doesn't go to the login page and directly says : can't find #user-info
But it's still not working... TestCafe tries to log in on the first test and then it directly reuses the same login information.
Any help would be appreciated! Thank you :-)
EDIT 2
I clarify the fact that I use variable to store the role (see comments)
If login fail is expected I'd suggest not using Role for this particular test. You can extract an authentication logiс into a separate function and use it in this test directly and in the Role constructor:
const logIn = async t => {
//login steps
};
user.role = Role(baseUrl, logIn);
test("test 1", async t => {
await logIn(t);
}); // The test go on the login page, auth failed (expected) and can't find #user-info (expected)
test("test 2", async t => {
t.useRole(adminUserRole)
});
I think you can check some Selector on the last step of Role initialization to make sure that you are correctly logged in. If you are not, you need to recreate your role for further usage in the next tests. Please see the following code, which demonstrates my idea:
import { Role, Selector } from 'testcafe';
let role = null;
let loggedIn = false;
function getRole() {
if (!loggedIn) {
role = new Role('http://example.com', async t => {
console.log('role initialize');
// await t.typeText('#login', 'login');
// await t.typeText('#password', 'password');
// await t.click('#signin');
// NOTE: ensure that we are actually logged in
if(await Selector('#user_profile').exists)
loggedIn = true;
});
}
return role;
}
fixture `fixture`
.page `../pages/index.html`;
test(`test1`, async t => {
await t.useRole(getRole());
});
test(`test2`, async t => {
await t.useRole(getRole());
});
test(`test3`, async t => {
await t.useRole(getRole());
});