Playwright - tinasamson/QAautomation GitHub Wiki
Create the folder were you want to create your project and then run this command:
npm init playwright
It would install all the depencies you need and then it would be asked is you want to work with typescript or with javascript and other configuration questions.
Then playwright would create all the files and folders you need. The folder structure would be:
- test
- example.spec.js
- package-lock.json
- package.json
- playwright.config.js
Were playwright.config.js is your configuration file for your entire test project.
For running the end-to-end tests:
npx playwright test
To start the interactive UI mode:
npx playwright test --ui
To run the tests only on desktop chrome:
npx playwright test --project=chromium
To run the tests in a specific file:
npx playwright test example
To run the tests in debug mode:
npx playwright test --debug
All test files have .spec.js extension. In this test we have to import one module:
const {test} = requiere('@playwright/test');
These tests are executed in playwright environment that launches the browser and provides a fresh page to each test. So to start a new test, we create a test function that takes two arguments. The first one is your actual test case name and the second argument is the function and is were you will write the code. To only run one test case: test.only()
test("Test example", function(){
//playwright code ->
});
There are also specific test functions that you can use:
beforeEach(): Execute this code before each test.
beforeAll(): Execute this code only one before all tests.
Because javascript is asynchronous we use await so that every step is executed. When we use this we have to change the function to async function, like this:
test("Test example", async function(){
await ;
});
NOTE: If the function does not have a name then it is an anonymous function, so instead of writing function(), you can write ()=>
export default defineConfig({
testDir: './tests',
timeout: 40 * 1000,
expect: {
timeout: 40 * 1000,
},
use: {
browserName: 'chromium',
headless: true,
},
});
testDir: the directory were your test are.
timeout: The default is 30 seconds it is going to wait for a locator or component.
expect: The timeout for assertion validations. The default is also 30 seconds.
use: Is for the browser information.
**use.headless: **The input can be true or false. False -> the browser would be invoked
Browser is a fixture that comes as default with playwright module and is globally available. To use it we have to send it as an parameter of your test function. To let the function know you are going to use playwright fixture we have to wrap between curly braces.
newContext(): To open one fresh and new instance of browser we are going to use the context method of the browser fixture.
newPage(): It create a new page inside your browser.
goto(): Navigate to the desired url.
test("Test example", async ({browser})=>{
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("www.example.com");
});
The page fixture can be send into the function as a parameter inside the curly braces:
test("Test example", async ({page})=>{
await page.goto("www.example.com");
});
To use expect we have to import it like this:
const {test, expect} = require('@playwright/test');
expect(locator).toContainText('string'): Validate that the text of a locator contains this string.
expect(locator).toHaveText('string'): Validate that the text of a locator has this string.
expect(locator).toBeChecked(): Ensures the [locator points to a checked input.
expect(locator).isChecked(): Returns whether the element is checked.
expect(value).toBeFalsy(): Ensures that value is false in a boolean context
expect(value).toBeTruthy(): Ensures that value is true in a boolean context
expect(locator).toHaveAttribute(name, options): Ensures the locator points to an element with given attribute.
When an action is happening inside the expect then we have to use await
expect(await page.locator(locator).isChecked()).toBeFalsy();
In playwright you can use CSS selector or xpath to identify an element.
await page.locator("#username")
id: tagname#id or #id
Class: tagname.class or .class
CSS based attribute: [attribute='value']
CSS traversing parent to child: parenttagname >> childtagname
Locator based on text: "text= string"
Playwright adds custom pseudo-classes like :visible, :has-text(), :has(), :is(), :nth-match() and more.
await page.locator("h3:has-text('Hello world')");
getByLabel(): Allows to locate input elements by the text of the associated aria-labelledby or aria-label attribute. getByPlaceholder(): Allows to locate input elements by the placeholder text. getByRole(): Allows locating elements by their ARIA role, ARIA attributes and accessible name. getByText(): Allows locating elements that contain given text.
await page.getByRole("button", {name: 'submit'});
fill(): method is used to enter something in the edit boxes (type method is deprecated).
click(): method is used to click on a button.
count(): method that tells you how many items there are present.
textContent(): Method to get the text from that element.
isVisible(): Returns whether the element is visible.
waitFor(): Wait until that items are loaded.
toBeVisible(): Method used to verify if an element is visible.
toBeHidden(): Method used to verify if an element is hidden.
hover: method hovers over the element.
await page.locator("#username")fill("username");
await page.locator(".btn").click();
goBack(): It will go back to the last redirect if possible.
goForward(): It will go forward to the last redirect if possible
on(): Method to listen for events so it will emit when it occured. Used to handle dialog/popups
frameLocator(): this method will enter the iframe and allow selecting elements in that iframe
await page.goBack();
await page.goForward();
await page.on("dialog", dialog => dialog.accept()),
page.on("dialog", dialog => dialog.dismiss()),
const framePage = page.frameLocator("#iframe");
await framePage.locator("#btn").click();
When a locator has multiple elements then we use:
nth(): to select which element of the array you want (remember an array always starts with cero).
first(): to select the first element of the array.
last(): to select the last element of the array.
await page.locator("div.block h4").first();
await page.locator("div.block h4").nth(1);
await page.locator("div.block h4").last();
waitForLoadState(): It will wait till it finish loading the domcontentloaded, load or the networkidle. For the networkidle, it will wait till the network come to idle state. This can be flaky.
waitFor(): It is going to wait for an element to load.
await page.waitForLoadState("networkidle");
await page.locator("#btn").first().waitFor();
const dropdown = page.locator(locator);
// Single selection matching the value or label
await dropdown.selectOption('blue');
// single selection matching the label
await dropdown.selectOption({ label: 'Blue' });
// multiple selection
await dropdown.selectOption(['red', 'green', 'blue']);
pressSequentially() is used to press keys one by one if there is a special keyboard handeling.
await locator.pressSequentially("Ar");
await page.locator(locator).last().click();
To uncheck a checkbox we use:
await page.locator(locator).uncheck();
If you click on a link that opens a new page in the browser, there is one way to handle that in playwright. So after clicking on the link that opens a new page, we have to use the context method waitForEvent(). This method waits for pages to open in the background and if a new page opens in the background it returns the new page variable. This method should be in listening stage before you perform any operation of opening the page so that it will catch the event and give you the new page.
test("Test example", async ({browser})=>{
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("www.example.com");
const linkNewPage = await page.locator("href=['www.google.com']");
const [newPage] = await promise.all([
context.waitForEvent('page'),
linkNewPage.click();
])
});
Promise.all(): is used to wrap steps in one array that need to be executed parallelly. It will come out of this array only after all steps in this promise are fulfilled (in iteration).
To open the inspector:
npx playwright test --debug
It will run your test and open an new window with the inspector were you can debug step by step your script.
To open codegen tool:
npx playwright test codegen https://www.google.com
This will open a browser with google and also the inspector with the generated code to go to google. If you click manualy on different elements, it will create automatically the code.
Go to the configuration file and add the screenshot key:
use: {
browserName: 'chromium',
headless: false,
screenshot: 'on',
trace: 'on',
},
This will create a screenshot for every step. The trace key is to get a detailed report each step execution with complete logs.
When running our test, it will generate an html report. There you can see step by step all the screenshots and there is a trace section. When downloading the trace file, you can open that zip file in the trace viewer. It will create a new folder called test-results, there you can find the trace zip and
Playwright Trace Viewer is a GUI tool that helps you explore recorded Playwright traces after the script has run. Traces are a great way for debugging your tests when they fail on CI.
If I only want to generate screenshots and trace files for the tests that have failures then we need to change the trace key in our config file:
trace: 'retain-on-failure',
UI Mode lets you explore, run, and debug tests with a time travel experience complete with a watch mode. All test files are displayed in the testing sidebar, allowing you to expand each file and describe block to individually run, view, watch, and debug each test.
Before starting with the API testing, we have to call the request object. Then you can be able to call API and work with the responses
const {test, expect, request} = require('@playwright/test');
To open a new context session and make a post call. After that we have to grab the response body using json(). To use the objects from this json -> responseJson.object:
test.beforeAll( async ()=>{
const apiContext = await request.newContext();
const response = await apiContext.post(url, data: payload); //where payload is a dict
const responseJson = response.json();
const token = responseJson.token;
})
Note: data would be an object, like the payload -> {user: "test"}.
expect(response.ok()).toBeTruthy: Checks whether the response you got is success.
To separate the API call from the rest of the test, we are going to use the Utils folder. There we create a new file called APIUtils.js The creation of the context has to be send as a parameter to your APIUtils file, for that we are going to create a constructor. So when you create the object for this class it will pass the apiContext
APIUtils()
{
constructor(apiContext){
this.apiContext = apiContext;
}
async getToken(){
// Put here the api call
return token;
}
}
module.exports = {APIUtils}; // we have to export so that the class is globally visible to all the files in the projects
This is how you call the utils class methods:
const {APIUtils} = require('./Utils/APIUtils'); // import Utils class
const apiContext = await request.newContext();
const apiUtils = new APIUtils(apiContext) // Create object
To return multiple values from the utils class, you can create an object (dict) and store each value. Then return the object. For example:
let response = {};
response.token = this.token
response.orderId = orderId
return response
We are going to create a new folder (pageObjects), there we are going to create one file for every page. In the constructor we put all our locators so that it will initialized. And we can create class methods to group steps. for example loginPage.js:
class LoginPage {
constructor(page){
this.page = page;
this.userName = page.locator(".user-name");
this.password = page.locator(".password");
this.signInButton = page.locator(".login-button");
}
async validLogin(username, password){
await this.userName.type(username);
await this.password.type(password);
await this.signInButton.click();
}
}
module.exports = {LoginPage}
The data should come from the test, don´t add this to the pageObject file. For chaining, you don´t separate the sublocator Then we call this in the test file:
const {LoginPage} = require(../pageObject/LoginPage.js);
test("login test case", async({page})=> {
const loginPage = new LoginPage(page);
await loginPage.validLogin(email, password);
} )
We have to put wait when calling the class method in the test.
This is used when you have to create multiple objects inside one single test. So we can create a new file with all this objects and use this file.
const {LoginPage} = require(../pageObject/LoginPage.js);
// other class
class POManager{
constructor(page){
this.page = page;
this.loginPage = new LoginPage(this.page);
// other page objects
}
getLoginPage(){
return this.loginPage;
}
// Other methods
}
module.exports = {POManager};
then in the test file:
const {POManager} = require(../pageObject/POManager.js);
test("login test case", async({page})=> {
const poManager = new POManager(page);
const loginPage = poManager.getLoginPage();
await loginPage.validLogin(email, password);
} )
To organize data, we can use the Utils folder or create a new Data folder. We then create a new json file:
{
"username": "tester",
"password": "password123"
}
We call this into out test file and it is important: JSON -> string -> JS object
const dataSet = JSON.parse(JSON.stringify(require(../utils/testData.json)));
test("login test case", async({page})=> {
....
await loginPage(dataSet.username, dataSet.password);
} );
If your test need to be parameterized (to repeat same test with different datasets) then we have to make the JSON into an array like this:
[
{
"username": "tester",
"password": "password123"
},
{
"username": "tester2",
"password": "password1234"
}
]
To have dynamically test titles so we can differentiate tests, we do that with ${data}. NOTE: It is not possible to run tests with the same title, they have to be unique. We use this in out test file like this:
const dataSet = JSON.parse(JSON.stringify(require(../utils/testData.json)));
for(const data of dataSet){
test(`login test case for user: ${data.username}`, async({page})=> {
....
await loginPage(data.username, data.password);
});
}
Important: It is not possible to do parameterization when sending data as fixtures. We can customize our test behaviour by adding a fixture, this will extend its existing behaviour. We create a new file inside Utils called test-base.js. All the customized options goes inside the curly braces.
const base = require('@playwright/test');
exports.customTest = base.test.extend(
{
testDataLogin : {
username: "tester2",
password: "password1234"
}
}
)
We are exporting a property called test that is available for all our files. We use this in our test file like this:
const {test, expect} = require('@playwright/test');
const {customTest} = require('../utils/test-base');
customTest(`login test case with fixtures`, async({page, testDataLogin})=> {
....
await loginPage(testDataLogin.username, testDataLogin.password);
});