Udemy Playwright: Web Automation Testing From Zero to Hero: Difference between revisions
Jump to navigation
Jump to search
Udemy Playwright: Web Automation Testing From Zero to Hero (view source)
Revision as of 21:36, 12 June 2024
, 12 Juneno edit summary
m (→Test Retries) |
mNo edit summary |
||
(17 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
* | <span id="BackToTop"></span> | ||
<div class="noprint" style="background-color:#FAFAFA; position:fixed; bottom:2%; left:0.25%; padding:0; margin:0;"> | |||
[[#BackToTop|Back to the Top]] | |||
</div> | |||
* Udemy: [https://www.udemy.com/course/playwright-from-zero-to-hero Udemy Playwright: Web Automation Testing From Zero to Hero] | |||
* | * My GitHub Playwright Udemy Course [https://github.com/VincentDirks/Playwright-Udemy-Course my repo #1] & [https://github.com/VincentDirks/Playwright-Udemy-Course-2 my repo #2] | ||
Note: This page is now massive, but it's handy to have it all in one place so that I can use ctrl-f to find notes... I'll be using this page as my cheatsheet for Playwright | |||
== Section 1: Preparation == | == Section 1: Preparation == | ||
Line 44: | Line 50: | ||
== Section 3: Playwright Hands-On Overview == | == Section 3: Playwright Hands-On Overview == | ||
* create new folder & <code>npm init playwright@latest</code> | * create new folder & <code>npm init playwright@latest</code> | ||
* install browsers <code>npx playwright install</code> | |||
=== Ways to Run & Debug === | === Ways to Run & Debug === | ||
* CLI Test Executions | * CLI Test Executions | ||
Line 1,511: | Line 1,518: | ||
=== Parallel Execution === | === Parallel Execution === | ||
* worker per <code>*.spec.ts</code> file | |||
* inside <code>*.spec.ts</code> files test cases are execute sequentially | |||
In file <code>playwright.config.ts</code> | |||
<nowiki> | |||
fullyParallel: false, | |||
workers: process.env.CI ? 1 : undefined, | |||
</nowiki> | |||
* <code>fullyParallel</code> is used to guide within a spec file | |||
* undefined workers allows playwright to decide, usually one per spec file | |||
Can override config values inside <code>*.spec.ts</code> files | |||
<nowiki> | |||
test.describe.configure({mode:'parallel'}) // spec file root level | |||
... | |||
test.describe.parallel("Forms Layouts page", () => { // in test suite | |||
... | |||
})</nowiki> | |||
=== Screenshots and Videos === | === Screenshots and Videos === | ||
=== Environment Variables === | |||
=== Configuration File === | In Code | ||
=== Fixtures === | <nowiki> | ||
=== Project Setup and Teardown === | await page.screenshot({ path: "screenshots/formsLayoutsPage.png" }) | ||
=== Test Tags === | |||
=== Mobile Device Emulator === | const buffer = await page.screenshot() | ||
=== | console.log(buffer.toString('base64')) | ||
await page.locator("nb-card", {hasText: "Inline form" }).screenshot({ path: "screenshots/inlineForm.png" })</nowiki> | |||
In file <code>playwright.config.ts</code> | |||
<nowiki> | |||
export default defineConfig({ | |||
... | |||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | |||
use: { | |||
... | |||
video: 'on', | |||
.. or .. | |||
video: { | |||
mode: "on", | |||
size: { width: 1920, height: 1080 }, | |||
}, | |||
}, | |||
/* Configure projects for major browsers */ | |||
projects: [ | |||
{ | |||
name: "chromium", | |||
use: { | |||
...devices["Desktop Chrome"], | |||
viewport: { width: 1920, height: 1080 }, | |||
}, | |||
}, | |||
...</nowiki> | |||
=== Environment Variables === | |||
There are many ways to specify environment specific values | |||
In file <code>playwright.config.ts</code> | |||
<nowiki> | |||
export default defineConfig<TestOptions>({ | |||
... | |||
use: { | |||
/* Base URL to use in actions like `await page.goto('/')`. */ | |||
baseURL: 'http://localhost:4200', | |||
.. or .. | |||
baseURL: process.env.DEV ? 'http://the.dev.env.com' | |||
: process.env.STAGING ? 'http://the.staging.env.com' | |||
: 'http://localhost:4200', | |||
... | |||
}, | |||
.. or .. | |||
/* Configure projects for major browsers */ | |||
projects: [ | |||
{ | |||
name: "Dev", | |||
use: { | |||
baseURL: 'http://the.dev.env.com', | |||
}, | |||
}, | |||
{ | |||
name: "Staging", | |||
use: { | |||
baseURL: 'http://the.staging.env.com', | |||
}, | |||
}, | |||
... | |||
</nowiki> | |||
Or Extend | |||
Create file <code>test-options.ts</code> | |||
<nowiki> | |||
import {test as base} from '@playwright/test' | |||
export type TestOptions = { | |||
globalsQaURL: string | |||
} | |||
export const test = base.extend<TestOptions>({ | |||
globalsQaURL: ['', {option:true}] | |||
})</nowiki> | |||
<nowiki> | |||
import type { TestOptions } from "./test-options" | |||
... | |||
export default defineConfig<TestOptions>({ | |||
... | |||
use: { | |||
... | |||
globalsQaURL: 'https://www.globalsqa.com/demo-site/draganddrop/', | |||
... | |||
}, | |||
.. or .. | |||
/* Configure projects for major browsers */ | |||
projects: [ | |||
{ | |||
name: "Dev", | |||
use: { | |||
... | |||
globalsQaURL: 'https://dev.globalsqa.com/demo-site/draganddrop/', | |||
... | |||
}, | |||
}, | |||
... | |||
</nowiki> | |||
Then in <code>*.spec.ts</code> | |||
<nowiki> | |||
test("drag and drop with iframe", async ({ page, globalsQaURL }) => { | |||
await page.goto(globalsQaURL) | |||
...</nowiki> | |||
Using <code>process.env</code> OS environment variables (but there'll be shell and OS variations though) | |||
Assuming bash shell. | |||
Specify variable in file <code>package.json</code> | |||
<nowiki> | |||
"scripts": { | |||
... | |||
"pageObjects-chrome": "npx playwright test usePageObjects.spec.ts --project=chromium", | |||
"autoWait-dev": "URL=http://uitestingplayground.com/ajax npm run pageObjects-chrome" | |||
},</nowiki> | |||
Then in <code>*.spec.ts</code> | |||
<nowiki> | |||
test.beforeEach(async ({ page }) => { | |||
await page.goto(process.env.URL) | |||
... | |||
}) | |||
...</nowiki> | |||
Or use a <code>.env</code> file | |||
In file <code>playwright.config.ts</code> | |||
<nowiki> | |||
... | |||
/** | |||
* Read environment variables from file. | |||
* https://github.com/motdotla/dotenv | |||
*/ | |||
require('dotenv').config(); //or ...config({ path: '/custom/path/to/.env' }) | |||
...</nowiki. | |||
<nowiki> | |||
URL=http://uitestingplayground.com/ajax | |||
TESTUSERNAME=test@test.com | |||
PASSWORD=Welcome1</nowiki> | |||
Making sure to add <code>.env</code> to <code>.gitignore</code> to avoid leaking credentials to repo. | |||
I like to create a <code>.env.template</code> file | |||
<nowiki> | |||
URL=http://uitestingplayground.com/ajax | |||
TESTUSERNAME=test@test.com | |||
PASSWORD=***REPLACE-ME***</nowiki> | |||
=== Configuration File === | |||
Cleaning up <code>playwright.config.ts</code> | |||
* removing default settings | |||
* removing comments | |||
There are | |||
* global settings, and | |||
* global runtime settings in the <code>use:{...}</code> block. | |||
Then inside the <code>projects[{...},{...} ...]</code> array each node has a name, and can then override any of the global settings, and runtime settings in a <code>use:{...}</code> subblock. | |||
You can also create and use entirely separate <code>*.config.ts</code> files and run them with <code>npx playwright test --config=playwright-prod.config.ts</code> | |||
Also see [https://playwright.dev/docs/test-configuration test configuration] and [https://playwright.dev/docs/test-use-options test use options] in the Playwright documentation. | |||
=== Fixtures === | |||
* Power tool to setup the test environment | |||
* custom fixtures extend the base test object | |||
* you can auto load fixtures | |||
* you can create dependencies between fixtures | |||
* you can also specify tear down code | |||
Update file <code>test-options.ts</code> | |||
<nowiki> | |||
import { test as base } from "@playwright/test" | |||
import { PageManager } from "./page-objects/pageManager" | |||
export type TestOptions = { | |||
globalsQaURL: string | |||
formLayoutsPage: string | |||
pageManager: PageManager | |||
} | |||
export const test = base.extend<TestOptions>({ | |||
globalsQaURL: ["", { option: true }], | |||
formLayoutsPage: async ({ page }, use) => { | |||
await page.goto("/") | |||
await page.getByText("Forms").click() | |||
await page.getByText("Form Layouts").click() | |||
await use("") | |||
console.log('Tear Down formLayoutsPage') | |||
}, | |||
// { auto: true }], | |||
pageManager: async ({ page, formLayoutsPage }, use) => { // pageManager depends on formLayoutsPage | |||
const pm = new PageManager(page) | |||
await use(pm) | |||
console.log('Tear Down pageManager') | |||
}, | |||
})</nowiki> | |||
Create new file <code>tests\testWithFictures.spec.ts</code> | |||
<nowiki> | |||
import { test } from "../test-options" | |||
import { faker } from "@faker-js/faker" | |||
//test("parameterised methods", async ({ page, formLayoutsPage }) => { // using formLayoutsPage fixture | |||
//test("parameterised methods", async ({ page }) => { // using {auto:true} in formLayoutsPage fixture | |||
test("parameterised methods", async ({ pageManager }) => { // using new pageManager fixture | |||
const randomFullName = faker.person.fullName() | |||
const randomEmail = `${randomFullName.replace(/ /g, "")}${faker.number.int( | |||
1000 | |||
)}@test.com` | |||
await pageManager | |||
.onFormLayoutsPage() | |||
.submitUsingTheGridFormWithCredentialsAndSelectOption( | |||
process.env.TESTUSERNAME, | |||
process.env.PASSWORD, | |||
"Option 2" | |||
) | |||
await pageManager | |||
.onFormLayoutsPage() | |||
.submitInLineFormWithNameEmailAndCheckbox(randomFullName, randomEmail, true) | |||
})</nowiki> | |||
=== Project Setup and Teardown === | |||
1. Create <code>tests\newArticle.setup.ts</code> to create a new article via API | |||
<nowiki> | |||
import { expect, test as setup } from "@playwright/test" | |||
setup("create new article", async ({ request }) => { | |||
const articleResponse = await request.post( | |||
"https://conduit-api.bondaracademy.com/api/articles/", | |||
{ | |||
data: { | |||
article: { | |||
title: "Likes Test Article", | |||
description: "Test description", | |||
body: "Test body", | |||
tagList: [], | |||
}, | |||
}, | |||
} | |||
) | |||
expect(articleResponse.status()).toEqual(201) | |||
const response = await articleResponse.json() | |||
const slugId = response.article.slug | |||
process.env["SLUGID"] = slugId | |||
})</nowiki> | |||
2. Create <code>tests\likesCounter.spec.ts</code> to test the like counter | |||
<nowiki> | |||
... | |||
test("Like counter increase", async ({ page }) => { | |||
await page.goto("https://conduit.bondaracademy.com/") | |||
await page.getByText("Global Feed").click() | |||
const firstLikeButton = page | |||
.locator("app-article-preview") | |||
.first() | |||
.locator("button") | |||
await expect(firstLikeButton).toContainText("0") | |||
await firstLikeButton.click() | |||
await expect(firstLikeButton).toContainText("1") | |||
})</nowiki> | |||
3. Create <code>tests\articleCleanUp.setup.ts</code> to clean up (ie. delete) the article created via API | |||
<nowiki> | |||
... | |||
setup("delete article", async ({ request }) => { | |||
// Clean up | |||
// delete the article using the slug extracted earlier | |||
const articleDeleteResponse = await request.delete( | |||
`https://conduit-api.bondaracademy.com/api/articles/${process.env.SLUGID}` | |||
) | |||
expect(articleDeleteResponse.status()).toEqual(204) | |||
})</nowiki> | |||
4. Update <code>playwright.config.ts</code> to create new projects | |||
:* articleSetup (with dependency on setup to fetch auth token, and using teardown) | |||
:* likeCounter (with dependency on articleSetup) | |||
:* articleCleanUp | |||
<nowiki> | |||
... | |||
export default defineConfig({ | |||
... | |||
projects: [ | |||
{ name: "setup", testMatch: "auth.setup.ts" }, | |||
{ | |||
name: "articleSetup", | |||
testMatch: "newArticle.setup.ts", | |||
dependencies: ["setup"], | |||
teardown: "articleCleanUp", | |||
}, | |||
{ | |||
name: "articleCleanUp", | |||
testMatch: "articleCleanUp.setup.ts", | |||
}, | |||
{ | |||
name: "likeCounter", | |||
testMatch: "likesCounter.spec.ts", | |||
use: { ...devices["Desktop Chrome"], storageState: ".auth/user.json" }, | |||
dependencies: ["articleSetup"], | |||
}, | |||
], | |||
})</nowiki> | |||
=== Global Setup and Teardown === | |||
1. Create file <code>global-setup.ts</code> with more or less same code as file <code>tests\newArticle.setup.ts</code> | |||
<nowiki> | |||
import { expect, request } from "@playwright/test" | |||
import user from "./.auth/user.json" | |||
import fs from "fs" | |||
async function globalSetup() { | |||
const authfile = ".auth/user.json" | |||
const context = await request.newContext() | |||
const responseToken = await context.post(... ) | |||
const responseBody = await responseToken.json() | |||
const accessToken = responseBody.user.token | |||
user.origins[0].localStorage[0].value = accessToken | |||
fs.writeFileSync(authfile, JSON.stringify(user)) | |||
process.env["ACCESS_TOKEN"] = accessToken | |||
const articleResponse = await context.post(...) | |||
expect(articleResponse.status()).toEqual(201) | |||
const articleResponseBody = await articleResponse.json() | |||
const slugId = articleResponseBody.article.slug | |||
process.env["SLUGID"] = slugId | |||
} | |||
export default globalSetup</nowiki> | |||
2. Create file <code>global-teardown.ts</code> | |||
<nowiki> | |||
import { request, expect } from "@playwright/test" | |||
async function globalTeardown() { | |||
const context = await request.newContext() | |||
// Clean up | |||
// delete the article using the slug extracted earlier | |||
const articleDeleteResponse = await context.delete( | |||
`https://conduit-api.bondaracademy.com/api/articles/${process.env.SLUGID}`, | |||
{ | |||
headers: { Authorization: `Token ${process.env.ACCESS_TOKEN}` }, | |||
} | |||
) | |||
expect(articleDeleteResponse.status()).toEqual(204) | |||
} | |||
export default globalTeardown</nowiki> | |||
3. Create file <code>tests\likesCounterGlobal.spec.ts</code> | |||
<nowiki> | |||
import { test, expect, request } from "@playwright/test" | |||
test("Like counter increase", async ({ page }) => { | |||
await page.goto("https://conduit.bondaracademy.com/") | |||
await page.getByText("Global Feed").click() | |||
const firstLikeButton = page | |||
.locator("app-article-preview") | |||
.first() | |||
.locator("button") | |||
await expect(firstLikeButton).toContainText("0") | |||
await firstLikeButton.click() | |||
await expect(firstLikeButton).toContainText("1") | |||
})</nowiki> | |||
4. Update file <code>playwright.config.ts</code> | |||
<nowiki> | |||
export default defineConfig({ | |||
... | |||
globalSetup: require.resolve("./global-setup.ts"), | |||
globalTeardown: require.resolve("./global-teardown.ts"), | |||
projects: [ | |||
... | |||
{ | |||
name: "likeCounterGlobal", | |||
testMatch: "likesCounterGlobal.spec.ts", | |||
use: { ...devices["Desktop Chrome"], storageState: ".auth/user.json" }, | |||
}, | |||
], | |||
})</nowiki> | |||
=== Test Tags === | |||
<nowiki> | |||
test("navigate to all pages @smoke @regression", async ({ page }) => { | |||
... | |||
test("parameterised methods @smoke", async ({ page }) => { | |||
... | |||
test.describe("Forms Layouts page @block", () => { | |||
test.describe.configure({ retries: 2 }) | |||
test("input fields", async ({ page }) => { | |||
...</nowiki> | |||
<nowiki> | |||
npx playwright test --project=chromium --grep @smoke | |||
npx playwright test --project=chromium --grep @regression | |||
npx playwright test --project=chromium --grep @block | |||
npx playwright test --project=chromium --grep "@block|@smoke"</nowiki> | |||
=== Mobile Device Emulator === | |||
1. Create new file <code>tests/testMobile.spec.ts</code> | |||
<nowiki> | |||
import { test } from "@playwright/test" | |||
test("input fields", async ({ page }, testInfo) => { | |||
await page.goto("/") | |||
const isMobile = testInfo.project.name === 'mobile' | |||
isMobile && await page.locator('.sidebar-toggle').click() // to show the sidebar which is hidden for mobile devices | |||
await page.getByText("Forms").click() | |||
await page.getByText("Form Layouts").click() | |||
isMobile && await page.locator('.sidebar-toggle').click() | |||
const usingTheGridEmailInput = page | |||
.locator("nb-card", { hasText: "Using the Grid" }) | |||
.getByRole("textbox", { name: "Email" }) | |||
await usingTheGridEmailInput.fill("test@test.com") | |||
await usingTheGridEmailInput.clear() | |||
await usingTheGridEmailInput.fill("test2@test.com") | |||
})</nowiki> | |||
2. Update <code>playwright.config.ts</code> | |||
<nowiki> | |||
export default defineConfig<TestOptions>({ | |||
... | |||
projects: [ | |||
... | |||
{ | |||
name: "mobile", | |||
testMatch: "testMobile.spec.ts", | |||
use: { | |||
...devices["iPhone 13 Pro"], | |||
// ..OR.. | |||
// viewport: { width: 414, height: 800 }, | |||
}, | |||
}, | |||
], | |||
})</nowiki> | |||
=== Reporters === | |||
Configure reporters in <code>playwright.config.ts</code> as per | |||
<nowiki> | |||
... | |||
export default defineConfig<TestOptions>({ | |||
... | |||
reporter: [ | |||
["json", { outputFile: "test-results/jsonReport.json" }], | |||
["junit", { outputFile: "test-results/junitReport.xml" }], | |||
['allure-playwright'] | |||
], | |||
...</nowiki> | |||
==== Allure ==== | |||
Setup for windows | |||
* [https://www.java.com/en/download/ java download] (FYI: I did not need to set JAVA_HOME) | |||
* [https://scoop.sh/ Scoop] | |||
<nowiki> | |||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser | |||
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression</nowiki> | |||
* [https://allurereport.org/docs/install-for-windows/ Allure report install for windows] | |||
<nowiki>scoop install allure</nowiki> | |||
* [https://www.npmjs.com/package/allure-playwright/v/2.15.1 npm:allure-playwright] | |||
<nowiki> | |||
npm i -D @playwright/test allure-playwright --force</nowiki> | |||
Now you can run some tests followed by | |||
<nowiki> | |||
allure generate allure-results -o allure-report --clean | |||
allure open allure-report</nowiki> | |||
Don't forget to add <code>allure-results</code> and <code>allure-report</code> to <code>.gitignore</code> | |||
Allure looks cool but also looks like a bit of a learning curve ... | |||
PS. it seems pretty straight forward to create your own custom reporter as per these instructions [https://playwright.dev/docs/api/class-reporter Playwright class reporter] | |||
=== Visual Testing === | === Visual Testing === | ||
This is so cool! Simply call <code>await expect(locator).toHaveScreenshot(...)</code> on an element. The first run through creates screenshot(s) inside a subfolder from <code>tests</code> folder. Second run compares current actual with previously expected. If there's any significant differences they are saved to subfolders in the <code>test-results</code> folder, and the html reporter has a very nice way to see and compare snapshots. | |||
You can control settings in the code | |||
<nowiki> | |||
await expect(locator).toHaveScreenshot({maxDiffPixels:150, maxDiffPixelRatio: 0.01})</nowiki> | |||
as well as in file <code>playwright.config.ts</code> | |||
<nowiki> | |||
export default defineConfig<TestOptions>({ | |||
expect: { | |||
... | |||
toMatchSnapshot: { maxDiffPixels: 50 }, | |||
}, | |||
...</nowiki> | |||
If you need to update a lot of snapshots use | |||
<nowiki> | |||
npx playwright test --update-snapshots</nowiki> | |||
=== Playwright with Docker Container === | === Playwright with Docker Container === | ||
=== Github Actions and Argos CI === | === Github Actions and Argos CI === |