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
Related
Goal: Use playwright for testing without having to log in multiple times as per documentation here.
Problem: Playwright tests not picking up session token from Next-Auth and hence am not able to reuse an authenticated status across mutiple tests.
My understanding is that Next-Auth checks for next-auth.session-token to validate the auth status. However following the above docs, I've noticed that the session token never get's stored in the storageState.json. If I manually add in the session token to the JSON then the tests work as expected.
Here is a very barebones repo I've setup with just a login button and one test.
Here's the typical [...nextAuth].js
import GithubProvider from "next-auth/providers/github";
export const authOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
secret: process.env.NEXTAUTH_SECRET,
debug: true,
};
export default NextAuth(authOptions);
Here's the global-setup.js for Playwright with email/password swapped out
module.exports = async () => {
const browser = await chromium.launch({
headless: false,
});
const page = await browser.newPage();
await page.goto('http://localhost:3000/');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
await page
.getByLabel('Username or email address')
.fill('email');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
};
Here's the e2e test I'm running
const { test, expect } = require("#playwright/test");
test.use({
storageState: "storageState.json",
});
test("sign in through github", async ({ page }) => {
await page.goto("http://localhost:3000");
await expect(page.getByText("Signed in as")).not.toBeEmpty();
});
I expect the auth state to be stored in storageState.json from the code in global-setup.js. It does indeed store information there but it's missing the next-auth.session-token for some reason and hence causes the tests I'm running to fail.
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.
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();
});
});```
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,
}
})
// ...
So I just started work on protractor tests and I'm facing the following problem - my tests fail inconsistently. Sometimes the test may pass and the next time it fails. Reasons to fail is very different, it may because it failed to find an element on a page or element does not have text in it (even if it has).
I'm running on Ubuntu 14.04, the same problem relevant for Chrome Version 71.0.3578.80 and Firefox Version 60.0.2. AngularJS Version 1.7.2 and Protractor Version 5.4.0. I believe the problem is somewhere in my code, so here below I provided an example of an existing code base.
Here is my protractor config
exports.config = {
rootElement: '[ng-app="myapp"]',
framework: 'jasmine',
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['./e2e/**/*protractor.js'],
SELENIUM_PROMISE_MANAGER: false,
baseUrl: 'https://localhost/',
allScriptsTimeout: 20000,
jasmineNodeOpts: {
defaultTimeoutInterval: 100000,
},
capabilities: {
browserName: 'firefox',
marionette: true,
acceptInsecureCerts: true,
'moz:firefoxOptions': {
args: ['--headless'],
},
},
}
And here capabilities for chrome browser
capabilities: {
browserName: 'chrome',
chromeOptions: {
args: [ "--headless", "--disable-gpu", "--window-size=1920,1080" ]
}
},
And finally, my test kit that failed a few times
const InsurerViewDriver = require('./insurer-view.driver');
const InsurerRefundDriver = require('./insurer-refund.driver');
const { PageDriver } = require('#utils/page');
const { NotificationsDriver } = require('#utils/service');
const moment = require('moment');
describe(InsurerViewDriver.pageUrl, () => {
beforeAll(async () => {
await InsurerViewDriver.goToPage();
});
it('- should test "Delete" button', async () => {
await InsurerViewDriver.clickDelete();
await NotificationsDriver.toBeShown('success');
await PageDriver.userToBeNavigated('#/setup/insurers');
await InsurerViewDriver.goToPage();
});
describe('Should test Refunds section', () => {
it('- should test refund list content', async () => {
expect(await InsurerRefundDriver.getTitle()).toEqual('REFUNDS');
const refunds = InsurerRefundDriver.getRefunds();
expect(await refunds.count()).toBe(1);
const firstRow = refunds.get(0);
expect(await firstRow.element(by.binding('item.name')).getText()).toEqual('Direct');
expect(await firstRow.element(by.binding('item.amount')).getText()).toEqual('$ 50.00');
expect(await firstRow.element(by.binding('item.number')).getText()).toEqual('');
expect(await firstRow.element(by.binding('item.date')).getText()).toEqual(moment().format('MMMM DD YYYY'));
});
it('- should test add refund action', async () => {
await InsurerRefundDriver.openNewRefundForm();
const NewRefundFormDriver = InsurerRefundDriver.getNewRefundForm();
await NewRefundFormDriver.setPayment(`#555555, ${moment().format('MMMM DD YYYY')} (amount: $2,000, rest: $1,500)`);
await NewRefundFormDriver.setPaymentMethod('Credit Card');
expect(await NewRefundFormDriver.getAmount()).toEqual('0');
await NewRefundFormDriver.setAmount(200.05);
await NewRefundFormDriver.setAuthorization('qwerty');
await NewRefundFormDriver.submit();
await NotificationsDriver.toBeShown('success');
const interactions = InsurerRefundDriver.getRefunds();
expect(await interactions.count()).toBe(2);
expect(await InsurerViewDriver.getInsurerTitleValue('Balance:')).toEqual('Balance: $ 2,200.05');
expect(await InsurerViewDriver.getInsurerTitleValue('Wallet:')).toEqual('Wallet: $ 4,799.95');
});
});
});
And here some functions from driver's, that I'm referencing in the test above
// PageDriver.userToBeNavigated
this.userToBeNavigated = async function(url) {
return await browser.wait(
protractor.ExpectedConditions.urlContains(url),
5000,
`Expectation failed - user to be navigated to "${url}"`
);
};
this.pageUrl = '#/insurer/33';
// InsurerViewDriver.goToPage
this.goToPage = async () => {
await browser.get(this.pageUrl);
};
// InsurerViewDriver.clickDelete()
this.clickDelete = async () => {
await $('[ng-click="$ctrl.removeInsurer()"]').click();
await DialogDriver.toBeShown('Are you sure you want to remove this entry?');
await DialogDriver.confirm();
};
// NotificationsDriver.toBeShown
this.toBeShown = async (type, text) => {
const awaitSeconds = 6;
return await browser.wait(
protractor.ExpectedConditions.presenceOf(
text ? element(by.cssContainingText('.toast-message', text)) : $(`.toast-${type}`)
),
awaitSeconds * 1000,
`${type} notification should be shown within ${awaitSeconds} sec`
);
}
// InsurerRefundDriver.getRefunds()
this.getRefunds = () => $('list-refunds-component').all(by.repeater('item in $data'));
// InsurerViewDriver.getInsurerTitleValue
this.getInsurerTitleValue = async (text) => {
return await element(by.cssContainingText('header-content p', text)).getText();
};
I can't upload the whole code here to give you better understanding because I have a lot of code till this moment, but the code provided above is the exact sample of approach I'm using everywhere, does anyone see a problem in my code? Thanks.
First of all add this block before exporting your config
process.on("unhandledRejection", ({message}) => {
console.log("\x1b[36m%s\x1b[0m", `Unhandled rejection: ${message}`);
});
this essentially colorfully logs to the console if you missed async/await anywhere, and it'll give confidence that you didn't miss anything.
Second, I would install "protractor-console" plugin, to make sure there is no errors/rejections in the browser console (i.e. exclude possibility of issues from your app side) and add to your config
plugins: [{
package: "protractor-console",
logLevels: [ "severe" ]
}]
Then the next problem that I would expect with these signs is incorrect waiting functions. Ideally you have to test them separately as you develop your e2e project, but since it's all written already I'll tell you how I debugged them. Note, this approach won't probably help you if your actions are less than a sec (i.e. you can't notice them). Otherwise follow this chain.
1) I created run configuration in WebStorm, as described in my comment here (find mine) How to debug angular protractor tests in WebStorm
2) Set a breakpoint in the first line of the test I want to debug
3) Then execute your test line by line, using the created run config.
When you start debugging process, webstorm opens up a panel with three sections: frames, console, variables. When the variables section has a message connected to localhost and no variables listed, this means your step is still being executed. Once loading completed you can see all your variables and you can execute next command. So the main principle here is you click Step Over button and watch for variables section. IF VARIABLES APPEAR BEFORE THE APPS LOADING COMPLETED (the waiting method executed, but the app is still loading, which is wrong) then you need to work on this method. By going this way I identified a lot of gaps in my custom waiting methods.
And finally if this doesn't work, please attach stack trace of your errors and ping me
I'm concerned about this code snippet
describe(InsurerViewDriver.pageUrl, () => {
beforeAll(async () => {
await InsurerViewDriver.goToPage();
});
it('- should test "Delete" button', async () => {
await InsurerViewDriver.clickDelete();
await NotificationsDriver.toBeShown('success');
await PageDriver.userToBeNavigated('#/setup/insurers');
await InsurerViewDriver.goToPage(); // WHY IS THIS HERE?
});
describe('Should test Refunds section', () => {
it('- should test refund list content', async () => {
// DOESN'T THIS NEED SOME SETUP?
expect(await InsurerRefundDriver.getTitle()).toEqual('REFUNDS');
// <truncated>
You should not depend on the first it clause to set up the suite below it. You didn't post the code for InsurerRefundDriver.getTitle() but if that code does not send the browser to the correct URL and then wait for the page to finish loading, it is a problem. You should probably have await InsurerViewDriver.goToPage(); in a beforeEach clause.
After some time research I found what was the problem. The cause was the way I'm navigate through the app.
this.goToPage = async () => {
await browser.get(this.pageUrl);
};
Turns out, that browser.get method is being resolved when url changed, but now when angularjs done compile. I used the same approach in every test kit, that's why my tests were failing inconsistently, sometimes page was not fully loaded before test start.
So here is an approach that did the trick
this.goToPage = async () => {
await browser.get(this.pageUrl);
await browser.wait(EC.presenceOf(`some important element`), 5000, 'Element did not appear after route change');
};
You should ensure that page done all the compiling job before moving on.
It seems this could be due to asynchronous javascript.
browser.ignoreSynchronization = true; has a global effect for all your tests. you may have to set it back to false, so protractor waits for angular to be finished rendering the page. e.g. in or before your second beforeEach function