KEMBAR78
Playwright Testing Guide | PDF | Safari (Web Browser) | Java Script
0% found this document useful (0 votes)
288 views97 pages

Playwright Testing Guide

Playwright Test

Uploaded by

dave.cornec
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
288 views97 pages

Playwright Testing Guide

Playwright Test

Uploaded by

dave.cornec
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 97

Playwright Test

Playful testing framework


What is Playwright Test?

● Cross-browser Web Testing Framework


● Node.js: JavaScript / TypeScript
● Free, Open Source, Sponsored by Microsoft
● Extensively used in the industry
Why yet another test runner?

● Historically, JavaScript test frameworks are built for unit tests


● Playwright Test is built for end-to-end tests:
○ Cross-browser — Chrome, Firefox & Safari
○ Parallelisation — tests are fast
○ Isolation — zero-overhead test isolation
○ Flexibility — pytest-like fixture configuration
Agenda

1. Getting Started
2. Fundamentals
3. Configuration
4. Playwright Inspector & CodeGen
5. Playwright Tracing
Chapter 1 Getting Started
Demo: Getting Started
Installation: npm init playwright
Running: npx playwright test
Test: e2e/example.spec.ts
Test: e2e/example.spec.ts
1. Test Isolation
Test: e2e/example.spec.ts
1. Test Isolation

2. Auto-waiting
Test: e2e/example.spec.ts
1. Test Isolation

2. Auto-waiting 3. Web-First Assertions


Chapter 2 Fundamentals
Fundamentals: Test Isolation

❌ Old-School: Browser Restart

● Slow instantiation (>100ms)


● Huge memory overhead

✅ Playwright Test: Browser Contexts

● Full isolation
● Fast instantiation (~1ms)
● Low overhead Browser Context
Fundamentals: Auto-waiting

❌ Old-School: timeouts to await elements

● Time does not exist in the cloud


● Timeouts are inefficient

✅ Playwright Test: built-in auto-waiting


Loading...
● Just Works!
● Happens for all actions (e.g. click, fill, press)
● No need for `setTimeout` calls
Fundamentals: Auto-waiting ✨
Fundamentals: Auto-waiting ✨
Fundamentals: Auto-waiting ✨
Fundamentals: Auto-waiting ✨
Fundamentals: Auto-waiting ✨
Fundamentals: Auto-waiting ✨
Fundamentals: Auto-waiting ✨
Fundamentals: Web-First Assertions

❌ Old-School: assert current state

● Web Applications are highly dynamic


● State is always in flux

✅ Playwright Test: declare expected state

● Wait until the declared state is reached


● Web-First assertions
Fundamentals: Web-First Assertions

expect(locator).toBeChecked() expect(locator).toHaveClass(expected)
expect(locator).toBeDisabled() expect(locator).toHaveCount(count)
expect(locator).toBeEditable() expect(locator).toHaveCSS(name, value)
expect(locator).toBeEmpty() expect(locator).toHaveId(id)
expect(locator).toBeEnabled() expect(locator).toHaveJSProperty(name, value)
expect(locator).toBeFocused() expect(locator).toHaveText(expected)
expect(locator).toBeHidden() expect(page).toHaveTitle(title)
expect(locator).toBeVisible() expect(page).toHaveURL(url)
expect(locator).toContainText(text) expect(locator).toHaveValue(value)
expect(locator).toHaveAttribute(name)
📍 Locators API
● Locator := (page, selector)
● Create locators with page.locator(selector)
● Represents a view to the element(s) on the page
● Re-queries page on each method call
● “strict” by default
● Useful in POMs
Fundamentals: Web-First Assertions

await expect(page.locator('.products .item')).toHaveText(['soap', 'rope']);


Fundamentals: Web-First Assertions

await expect(page.locator('.products .item')).toHaveText(['soap', 'rope']);

1. Must be awaited
Fundamentals: Web-First Assertions

await expect(page.locator('.products .item')).toHaveText(['soap', 'rope']);

1. Must be awaited

2. Re-queries given locator


Fundamentals: Web-First Assertions

await expect(page.locator('.products .item')).toHaveText(['soap', 'rope']);

1. Must be awaited

2. Re-queries given locator

3. Waiting until it has two elements


with given texts
Chapter 3 Configuration
// example.spec.ts
import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {


await page.goto('https://playwright.dev/');
await page.locator('text=Get started').click();
await expect(page).toHaveTitle(/Getting started/);
});
// example.spec.ts
import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {


await page.goto('https://playwright.dev/');
await page.locator('text=Get started').click();
await expect(page).toHaveTitle(/Getting started/);
});
// example.spec.ts // playwright.config.ts
import { test, expect } from '@playwright/test'; import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
test('basic test', async ({ page }) => { projects: [
await page.goto('https://playwright.dev/');
await page.locator('text=Get started').click();
await expect(page).toHaveTitle(/Getting started/);
});

],
};
export default config;
// example.spec.ts // playwright.config.ts
import { test, expect } from '@playwright/test'; import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
test('basic test', async ({ page }) => { projects: [
await page.goto('https://playwright.dev/'); {
await page.locator('text=Get started').click(); name: 'Desktop Chrome',
await expect(page).toHaveTitle(/Getting started/); use: { browserName: 'chromium', },
}); },

],
};
export default config;
// example.spec.ts // playwright.config.ts
import { test, expect } from '@playwright/test'; Run 1
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
test('basic test', async ({ page }) => { projects: [
await page.goto('https://playwright.dev/'); {
await page.locator('text=Get started').click(); name: 'Desktop Chrome',
await expect(page).toHaveTitle(/Getting started/); use: { browserName: 'chromium', },
}); },

],
};
export default config;
// example.spec.ts // playwright.config.ts
import { test, expect } from '@playwright/test'; Run 1
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
test('basic test', async ({ page }) => { projects: [

R
un
2
await page.goto('https://playwright.dev/'); {
await page.locator('text=Get started').click(); name: 'Desktop Chrome',

Run
await expect(page).toHaveTitle(/Getting started/); use: { browserName: 'chromium', },

3
}); },
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// example.spec.ts // playwright.config.ts
import { test, expect } from '@playwright/test'; Run 1
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
test('basic test', async ({ page }) => { projects: [

R
un
2
await page.goto('https://playwright.dev/'); {
await page.locator('text=Get started').click(); name: 'Desktop Chrome',

Run
await expect(page).toHaveTitle(/Getting started/); use: { browserName: 'chromium', },

3
}); },
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
Granular Configuration: france.spec.ts
● Per-file configuration
● Per-suite configuration
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
ip
t
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
// per-file configuration

ip
t
test.use({ locale: 'fr-FR', timezoneId: 'Europe/Paris' });
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
// per-file configuration

ip
t
test.use({ locale: 'fr-FR', timezoneId: 'Europe/Paris' });

test('should work', async ({ page }) => { /* ... test goes here ... */ });
test('should use euro', async ({ page }) => { /* ... */ });
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
// per-file configuration

ip
t
test.use({ locale: 'fr-FR', timezoneId: 'Europe/Paris' });

test('should work', async ({ page }) => { /* ... test goes here ... */ });
test('should use euro', async ({ page }) => { /* ... */ });

test.describe('light theme', () => {

});
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
// per-file configuration

ip
t
test.use({ locale: 'fr-FR', timezoneId: 'Europe/Paris' });

test('should work', async ({ page }) => { /* ... test goes here ... */ });
test('should use euro', async ({ page }) => { /* ... */ });

test.describe('light theme', () => {


test.use({ colorScheme: 'light' }); // per-suite configuration

});
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
// per-file configuration

ip
t
test.use({ locale: 'fr-FR', timezoneId: 'Europe/Paris' });

test('should work', async ({ page }) => { /* ... test goes here ... */ });
test('should use euro', async ({ page }) => { /* ... */ });

test.describe('light theme', () => {


test.use({ colorScheme: 'light' }); // per-suite configuration
test('should be light', async ({ page }) => { /* ... */ });
});
// france.spec.ts

Ty
import { test, expect } from '@playwright/test';

p
eS
cr
// per-file configuration

ip
t
test.use({ locale: 'fr-FR', timezoneId: 'Europe/Paris' });

test('should work', async ({ page }) => { /* ... test goes here ... */ });
test('should use euro', async ({ page }) => { /* ... */ });

test.describe('light theme', () => {


test.use({ colorScheme: 'light' }); // per-suite configuration
test('should be light', async ({ page }) => { /* ... */ });
});
test.describe('dark theme', () => {
test.use({ colorScheme: 'dark' }); // per-suite configuration
test('should be dark', async ({ page }) => { /* ... */ });
});
Configuration Options
https://aka.ms/playwright/fixtures

● acceptDownloads ● javaScriptEnabled
● baseURL ● launchOptions
● browserName ● locale
● bypassCSP ● offline
● channel ● permissions
● colorScheme ● proxy
● deviceScaleFactor ● screenshot
● extraHTPHeaders ● storageState
● geolocation ● timezoneId
● hasTouch ● trace
● headless ● userAgent
● httpCredentials ● video
● ignoreHTTPSErrors ● viewport
Configuration: Data-Driven Tests
Configuration: Data-Driven Tests
Configuration: Data-Driven Tests
Ty
// check-urls.spec.ts

p
eS
cr
import { test, expect } from '@playwright/test';

ip
t
Ty
// check-urls.spec.ts

p
eS
cr
import { test, expect } from '@playwright/test';

ip
t
const urls = require('./urls.json');
Ty
// check-urls.spec.ts

p
eS
cr
import { test, expect } from '@playwright/test';

ip
t
const urls = require('./urls.json');
for (const url of urls) {

}
Ty
// check-urls.spec.ts

p
eS
cr
import { test, expect } from '@playwright/test';

ip
t
const urls = require('./urls.json');
for (const url of urls) {
test(`check ${url}`, async ({ page }) => {
await page.goto(url);
});
}
Ty
// check-urls.spec.ts

p
eS
cr
import { test, expect } from '@playwright/test';

ip
t
const urls = require('./urls.json');
for (const url of urls) {
test(`check ${url}`, async ({ page }) => {
await page.goto(url);
}); NOTE: Make sure to have
} different test titles
Configuration: Reporters
Configuration: Reporters
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: 'dot',
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: process.env.CI ? 'dot' : 'line',
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
Built-in reporters
],
projects: [ ➔ dot
{ ➔ list
name: 'Desktop Chrome', ➔ line
use: { browserName: 'chromium', }, ➔ json
}, ➔ junit
{
name: 'Desktop Firefox', Third-party reporters
use: { browserName: 'firefox', },
➔ allure-playwright
},
{
name: 'Desktop Safari',
https://aka.ms/playwright/reporters
use: { browserName: 'webkit', },
}
],
};
export default config;
Configuration: Devices
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: { browserName: 'chromium', },
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: { browserName: 'firefox', },
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
},
{
name: 'Desktop Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
},
{
name: 'Mobile Safari',
use: { browserName: 'webkit', },
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
},
{
name: 'Mobile Safari',
use: devices['iPhone 12 Pro'],
}
],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
reporter: [
process.env.CI ? ['dot'] : ['list'],
['json', { outputFile: 'test-results.json' }],
],
projects: [
{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
},
{
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
},
{
name: 'Mobile Safari',
use: devices['iPhone 12 Pro'],
}
],
};
export default config;
Playwright
Chapter 4 Inspector & Codegen
Demo: Inspector & CodeGen
Playwright Inspector
Playwright Code Generation
Control Panel

Playwright Inspector
Source Code

Selectors Playground

Actions
Post-mortem Debugging
Chapter 5 Playwright Tracing
Post-Mortem Debugging

● Post-Mortem Debugging – debugging test failures on CI without being


able to debug locally.
● Test Artifacts – any by-product of test running that helps debug test
failures.
○ Logs
○ Screenshots
○ Videos
Unique Artifact: Tracing

videos screenshots

Playwright
Tracing
Playwright Tracing
Playwright Tracing

trace.zip files
● Playwright actions
● Playwright events
● Screencast
● Network log
● Console log
● DOM snapshots 🔥
Playwright Tracing

trace.zip files Trace Viewer


● Playwright actions ● GUI tool to explore trace
● Playwright events files
● Screencast ● Bundled with Playwright
● Network log
● Console log
● DOM snapshots 🔥
Playwright Tracing: Workflow

Enable trace collection in playwright.config.ts

Setup CI to upload trace files

Download & Inspect trace files with Playwright Trace Viewer


// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';

Ty
const config: PlaywrightTestConfig = {

p
eS
reporter: process.env.CI ? 'dot' : 'line',

cr
ip
t
projects: [{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
}, {
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
}, {
name: 'Mobile Safari',
use: devices['iPhone 12 Pro'],
}],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';

Ty
const config: PlaywrightTestConfig = {

p
eS
reporter: process.env.CI ? 'dot' : 'line',

cr
ip
retries: 2,

t
projects: [{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
}, {
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
}, {
name: 'Mobile Safari',
use: devices['iPhone 12 Pro'],
}],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';

Ty
const config: PlaywrightTestConfig = {

p
eS
reporter: process.env.CI ? 'dot' : 'line',

cr
ip
retries: 2,

t
use: {
trace: 'on-first-retry',
},
projects: [{
name: 'Desktop Chrome',
use: devices['Desktop Chrome'],
}, {
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
}, {
name: 'Mobile Safari',
use: devices['iPhone 12 Pro'],
}],
};
export default config;
// playwright.config.ts
import { PlaywrightTestConfig, devices } from '@playwright/test';

Ty
const config: PlaywrightTestConfig = {

p
eS
reporter: process.env.CI ? 'dot' : 'line',

cr
ip
retries: 2,

t
use: {
trace: 'on-first-retry',
},
projects: [{
name: 'Desktop Chrome',
Enabling Trace Collection
use: devices['Desktop Chrome'],
}, {
name: 'Desktop Firefox',
use: devices['Desktop Firefox'],
}, {
name: 'Mobile Safari',
use: devices['iPhone 12 Pro'],
}],
};
export default config;
# .github/workflows/tests.yml
on: [push]
jobs:

G
ith
ub
run_tests:

Ac
runs-on: ubuntu-latest

tio
ns
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
# .github/workflows/tests.yml
on: [push]
jobs:

G
ith
ub
run_tests:

Ac
runs-on: ubuntu-latest

tio
ns
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm ci
- run: npx playwright install --with-deps Uploading Artifacts
- run: npm run test:e2e
- uses: actions/upload-artifact@v2
if: always()
with:
name: test-results
path: test-results
# .github/workflows/tests.yml
on: [push]
jobs:

G
ith
ub
run_tests:

Ac
runs-on: ubuntu-latest

tio
ns
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm ci
- run: npx playwright install --with-deps Uploading Artifacts
- run: npm run test:e2e
- uses: actions/upload-artifact@v2
if: always()
with:
name: test-results
path: test-results ← default folder with all artifacts
Demo: Playwright Trace Viewer
Opening Playwright Trace Viewer
Timeline

Action Details
Actions List

DOM Snapshot 🔥
Playwright Test: Playful Testing Framework

1. Get started with npm init playwright


2. Configure everything at playwright.config.ts
3. Test iPhone, customize reporters, generate tests
4. Debug tests with Playwright Inspector
5. Author tests with Playwright CodeGen
6. Post-mortem with Playwright Tracing
Playwright

● Cross-browser Web Testing and Automation Framework


● Documentation: https://playwright.dev
● Source / Issues: https://github.com/microsoft/playwright
● Social:
○ https://aka.ms/playwright/slack
○ https://aka.ms/playwright/twitter
○ https://aka.ms/playwright/youtube
Q
ue
st
io
ns
?
Andrey Lushnikov Playwright Test

@aslushnikov @playwrightweb

aslushnikov@gmail.com https://aka.ms/playwright-slack

microsoft/playwright

You might also like