diff --git a/.eslintignore b/.eslintignore index b7bf13a44ea76..aaf6dccdca3da 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,4 +12,4 @@ browser_patches/*/checkout/ browser_patches/chromium/output/ **/*.d.ts output/ -/test-results/ +test-results/ diff --git a/docs/src/selectors.md b/docs/src/selectors.md index b99752ca0235a..0586bdddfa95f 100644 --- a/docs/src/selectors.md +++ b/docs/src/selectors.md @@ -75,7 +75,7 @@ methods accept [`param: selector`] as their first argument. - Combine css and text selectors ```js await page.click('article:has-text("Playwright")'); - await page.click('#nav-bar :text("Contact us")'); + await page.click('#nav-bar >> text=Contact Us'); ``` ```java page.click("article:has-text(\"Playwright\")"); diff --git a/packages/create-playwright/assets/examples/1-getting-started.spec.ts b/packages/create-playwright/assets/examples/1-getting-started.spec.ts new file mode 100644 index 0000000000000..0b484685c842a --- /dev/null +++ b/packages/create-playwright/assets/examples/1-getting-started.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; + +/** + * Inside every test you get a new isolated page instance. + * @see https://playwright.dev/docs/intro + * @see https://playwright.dev/docs/api/class-page + */ +test('basic test', async ({ page }) => { + await page.goto('https://todomvc.com/examples/vanilla-es6/'); + + const inputBox = page.locator('input.new-todo'); + const todoList = page.locator('.todo-list'); + + await inputBox.fill('Learn Playwright'); + await inputBox.press('Enter'); + await expect(todoList).toHaveText('Learn Playwright'); +}); diff --git a/packages/create-playwright/assets/examples/2-actions.spec.ts b/packages/create-playwright/assets/examples/2-actions.spec.ts new file mode 100644 index 0000000000000..18634ff0df63d --- /dev/null +++ b/packages/create-playwright/assets/examples/2-actions.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://todomvc.com/examples/vanilla-es6/'); +}); + +/** + * Locators are used to represent a selector on a page and re-use them. They have + * strictMode enabled by default. This option will throw an error if the selector + * will resolve to multiple elements. + * In this example we create a todo item, assert that it exists and then filter + * by the completed items to ensure that the item is not visible anymore. + * @see https://playwright.dev/docs/api/class-locator + */ +test('basic interaction', async ({ page }) => { + const inputBox = page.locator('input.new-todo'); + const todoList = page.locator('.todo-list'); + + await inputBox.fill('Learn Playwright'); + await inputBox.press('Enter'); + await expect(todoList).toHaveText('Learn Playwright'); + await page.locator('.filters >> text=Completed').click(); + await expect(todoList).not.toHaveText('Learn Playwright'); +}); + +/** + * Playwright supports different selector engines which you can combine with '>>'. + * @see https://playwright.dev/docs/selectors + */ +test('element selectors', async ({ page }) => { + // When no selector engine is specified, Playwright will use the css selector engine. + await page.type('.header input', 'Learn Playwright'); + // So the selector above is the same as the following: + await page.press('css=.header input', 'Enter'); + + // select by text with the text selector engine: + await page.click('text=All'); + + // css allows you to select by attribute: + await page.click('[id="toggle-all"]'); + + // Combine css and text selectors (https://playwright.dev/docs/selectors/#text-selector) + await page.click('.todo-list > li:has-text("Playwright")'); + await page.click('.todoapp .footer >> text=Completed'); + + // Selecting based on layout, with css selector + expect(await page.innerText('a:right-of(:text("Active"))')).toBe('Completed'); + + // Only visible elements, with css selector + await page.click('text=Completed >> visible=true'); + + // XPath selector + await page.click('xpath=//html/body/section/section/label'); +}); diff --git a/packages/create-playwright/assets/examples/3-assertions.spec.ts b/packages/create-playwright/assets/examples/3-assertions.spec.ts new file mode 100644 index 0000000000000..044fd3e89f1bc --- /dev/null +++ b/packages/create-playwright/assets/examples/3-assertions.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://todomvc.com/examples/vanilla-es6/'); +}); + +/** + * All available test assertions are listed here: + * @see https://playwright.dev/docs/test-assertions/ + */ +test('should be able to use assertions', async ({ page }) => { + await test.step('toHaveTitle/toHaveURL', async () => { + await expect(page).toHaveTitle('Vanilla ES6 • TodoMVC'); + await expect(page).toHaveURL('https://todomvc.com/examples/vanilla-es6/'); + }); + + await test.step('toBeEmpty/toHaveValue', async () => { + const input = page.locator('input.new-todo'); + await expect(input).toBeEmpty(); + await input.fill('Buy milk'); + await expect(input).toHaveValue('Buy milk'); + await input.press('Enter'); + }); + + await test.step('toHaveCount/toHaveText/toContainText', async () => { + const items = page.locator('.todo-list li'); + await expect(items).toHaveCount(1); + await expect(items.first()).toHaveText('Buy milk'); + await expect(items).toHaveText(['Buy milk']); + await expect(items.first()).toContainText('milk'); + }); + + await test.step('toBeChecked', async () => { + const firstItemCheckbox = page.locator('input[type=checkbox]:left-of(:text("Buy milk"))'); + await expect(firstItemCheckbox).not.toBeChecked(); + await firstItemCheckbox.check(); + await expect(firstItemCheckbox).toBeChecked(); + }); + + await test.step('toBeVisible/toBeHidden', async () => { + await expect(page.locator('text=Buy milk')).toBeVisible(); + await page.click('text=Active'); + await expect(page.locator('text=Buy milk')).toBeHidden(); + }); + + await test.step('toHaveClass/toHaveCSS', async () => { + await expect(page.locator('[placeholder="What needs to be done?"]')).toHaveClass('new-todo'); + await page.click('text=Clear completed'); + await expect(page.locator('.main')).toHaveCSS('display', 'none'); + }); +}); diff --git a/packages/create-playwright/assets/examples/4-file-uploads.spec.ts b/packages/create-playwright/assets/examples/4-file-uploads.spec.ts new file mode 100644 index 0000000000000..eda467a5bfd4f --- /dev/null +++ b/packages/create-playwright/assets/examples/4-file-uploads.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; + +const fileToUpload = __filename; // '__filename' is the current test file. + +/** + * In this test we wait for an file chooser to appear while we click on an + * input. Once the event was emitted we set the file and submit the form. + * @see https://playwright.dev/docs/api/class-filechooser + */ +test('should be able to upload files', async ({ page, context }) => { + await page.goto('/file-uploads.html'); + const [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser'), + page.click('input') + ]); + await fileChooser.setFiles(fileToUpload); + await page.click('input[type=submit]'); + await expect(page.locator('text=4-file-uploads.spec.ts')).toBeVisible(); +}); diff --git a/packages/create-playwright/assets/examples/5-networking.spec.ts b/packages/create-playwright/assets/examples/5-networking.spec.ts new file mode 100644 index 0000000000000..e6402943c8c6d --- /dev/null +++ b/packages/create-playwright/assets/examples/5-networking.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +/** + * This test clicks on an element with the text 'Load user' and waits for a + * specific HTTP response. This response contains a JSON body where we assert + * some properties. + */ +test('should be able to read a response body', async ({ page }) => { + await page.goto('/network.html'); + const [response] = await Promise.all([ + page.waitForResponse('/api/v1/users.json'), + page.click('text=Load user') + ]); + await expect(page.locator('#user-full-name')).toContainText('John Doe'); + const responseBody = await response.json(); + expect(responseBody.id).toBe(1); + expect(responseBody.fullName).toBe('John Doe'); +}); + +test.describe('mocked responses', () => { + /** + * Before every test set the request interception handler and fulfill the + * requests with a mocked response. See here: + * @see https://playwright.dev/docs/network#handle-requests + */ + test.beforeEach(async ({ context }) => { + await context.route('/api/v1/users.json', route => route.fulfill({ + body: JSON.stringify({ + 'id': 2, + 'fullName': 'James Bond' + }), + contentType: 'application/json' + })); + }); + + test('be able to mock responses', async ({ page }) => { + await page.goto('/network.html'); + await page.click('text=Load user'); + await expect(page.locator('p')).toHaveText('User: James Bond'); + }); +}); diff --git a/packages/create-playwright/assets/examples/README.md b/packages/create-playwright/assets/examples/README.md new file mode 100644 index 0000000000000..ad807c663df3f --- /dev/null +++ b/packages/create-playwright/assets/examples/README.md @@ -0,0 +1,8 @@ +# Playwright examples + +This directory contains examples for Playwright. Run them with the following command: + +```sh +npm run test:e2e-examples +yarn test:e2e-examples +``` diff --git a/packages/create-playwright/assets/examples/playwright.config.ts b/packages/create-playwright/assets/examples/playwright.config.ts new file mode 100644 index 0000000000000..021eeb313dd57 --- /dev/null +++ b/packages/create-playwright/assets/examples/playwright.config.ts @@ -0,0 +1,13 @@ +import { PlaywrightTestConfig } from '@playwright/test'; + +// Reference: https://playwright.dev/docs/test-configuration +const config: PlaywrightTestConfig = { + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: 'node ./server', + port: 4345, + cwd: __dirname, + }, +}; +export default config; diff --git a/packages/create-playwright/assets/examples/pom/fixtures.ts b/packages/create-playwright/assets/examples/pom/fixtures.ts new file mode 100644 index 0000000000000..1cd939586c532 --- /dev/null +++ b/packages/create-playwright/assets/examples/pom/fixtures.ts @@ -0,0 +1,14 @@ +import { test as base } from '@playwright/test'; +import { TodoPage } from './todoPage.pom'; + +/** + * This adds a todoPage fixture which has access to the page instance + * @see https://playwright.dev/docs/test-fixtures + */ +export const test = base.extend<{ todoPage: TodoPage }>({ + todoPage: async ({ page }, use) => { + await use(new TodoPage(page)); + }, +}); + +export const expect = test.expect; diff --git a/packages/create-playwright/assets/examples/pom/pom-with-fixtures.spec.ts b/packages/create-playwright/assets/examples/pom/pom-with-fixtures.spec.ts new file mode 100644 index 0000000000000..a0445ccf13999 --- /dev/null +++ b/packages/create-playwright/assets/examples/pom/pom-with-fixtures.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from './fixtures'; + +/** + * Fixtures are used here to create a TodoApp instance for every test. These are + * defined inside the 'fixtures.ts' file. This will reduce the amount of + * boilerplate created for each test and makes it more reusable. + * @see https://playwright.dev/docs/test-fixtures + */ +test.beforeEach(async ({ todoPage }) => { + await todoPage.goto(); +}); + +test('should display zero initial items', async ({ todoPage }) => { + await expect(todoPage.listItems).toHaveCount(0); +}); + +test('should be able to add new items', async ({ todoPage }) => { + await todoPage.addItem('Example #1'); + await todoPage.addItem('Example #2'); + await expect(todoPage.listItems).toHaveText(['Example #1', 'Example #2']); +}); + +test('should be able to mark items as completed', async ({ todoPage }) => { + await todoPage.addItem('Example #1'); + const firstListItem = todoPage.listItems.first(); + await expect(firstListItem).not.toHaveClass('completed'); + await firstListItem.locator('.toggle').check(); + await expect(firstListItem).toHaveClass('completed'); +}); + +test('should still show the items after a page reload', async ({ page, todoPage }) => { + await todoPage.addItem('Example #1'); + await expect(todoPage.listItems).toHaveText(['Example #1']); + await page.reload(); + await expect(todoPage.listItems).toHaveText(['Example #1']); +}); + +test('should be able to filter by uncompleted items', async ({ todoPage }) => { + await todoPage.addItem('Example #1'); + await todoPage.addItem('Example #2'); + await todoPage.addItem('Example #3'); + await todoPage.listItems.last().locator('.toggle').check(); + await todoPage.filterByActiveItemsButton.click(); + await expect(todoPage.listItems).toHaveCount(2); + await expect(todoPage.listItems).toHaveText(['Example #1', 'Example #2']); +}); + +test('should be able to filter by completed items', async ({ todoPage }) => { + await todoPage.addItem('Example #1'); + await todoPage.addItem('Example #2'); + await todoPage.addItem('Example #3'); + await todoPage.listItems.last().locator('.toggle').check(); + await todoPage.filterByCompletedItemsButton.click(); + await expect(todoPage.listItems).toHaveText(['Example #3']); +}); + +test('should be able to delete completed items', async ({ todoPage }) => { + await todoPage.addItem('Example #1'); + await todoPage.listItems.last().locator('.toggle').check(); + await expect(todoPage.listItems).toHaveText(['Example #1']); + await todoPage.listItems.first().locator('button.destroy').click(); + await expect(todoPage.listItems).toHaveText([]); +}); diff --git a/packages/create-playwright/assets/examples/pom/pom.spec.ts b/packages/create-playwright/assets/examples/pom/pom.spec.ts new file mode 100644 index 0000000000000..1051ba4e91981 --- /dev/null +++ b/packages/create-playwright/assets/examples/pom/pom.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { TodoPage } from './todoPage.pom'; + +test.describe('ToDo App', () => { + test('should display zero initial items', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await expect(todoPage.listItems).toHaveCount(0); + }); + + test('should be able to add new items', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await todoPage.addItem('Example #1'); + await todoPage.addItem('Example #2'); + await expect(todoPage.listItems).toHaveText(['Example #1', 'Example #2']); + }); + + test('should be able to mark items as completed', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await todoPage.addItem('Example #1'); + const firstListItem = todoPage.listItems.first(); + await expect(firstListItem).not.toHaveClass('completed'); + await firstListItem.locator('.toggle').check(); + await expect(firstListItem).toHaveClass('completed'); + }); + + test('should still show the items after a page reload', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await todoPage.addItem('Example #1'); + await expect(todoPage.listItems).toHaveText(['Example #1']); + await page.reload(); + await expect(todoPage.listItems).toHaveText(['Example #1']); + }); + + test('should be able to filter by uncompleted items', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await todoPage.addItem('Example #1'); + await todoPage.addItem('Example #2'); + await todoPage.addItem('Example #3'); + await todoPage.listItems.last().locator('.toggle').check(); + await todoPage.filterByActiveItemsButton.click(); + await expect(todoPage.listItems).toHaveText(['Example #1', 'Example #2']); + }); + + test('should be able to filter by completed items', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await todoPage.addItem('Example #1'); + await todoPage.addItem('Example #2'); + await todoPage.addItem('Example #3'); + await todoPage.listItems.last().locator('.toggle').check(); + await todoPage.filterByCompletedItemsButton.click(); + await expect(todoPage.listItems).toHaveText(['Example #3']); + }); + + test('should be able to delete completed items', async ({ page }) => { + const todoPage = new TodoPage(page); + await todoPage.goto(); + await todoPage.addItem('Example #1'); + await todoPage.listItems.last().locator('.toggle').check(); + await expect(todoPage.listItems).toHaveText(['Example #1']); + await todoPage.listItems.first().locator('button.destroy').click(); + await expect(todoPage.listItems).toHaveText([]); + }); +}); diff --git a/packages/create-playwright/assets/examples/pom/todoPage.pom.ts b/packages/create-playwright/assets/examples/pom/todoPage.pom.ts new file mode 100644 index 0000000000000..efafc68089f5d --- /dev/null +++ b/packages/create-playwright/assets/examples/pom/todoPage.pom.ts @@ -0,0 +1,28 @@ +import { Page } from '@playwright/test'; + +/** + * This is a Page Object Model (POM) class for the application's Todo page. It + * provides locators and common operations that make writing tests easier. + * @see https://playwright.dev/docs/test-pom + */ +export class TodoPage { + /** + * Locators are used to reflect a element on the page with a selector. + * @see https://playwright.dev/docs/api/class-locator + */ + listItems = this.page.locator('.todo-list li'); + inputBox = this.page.locator('input.new-todo'); + filterByActiveItemsButton = this.page.locator('.filters >> text=Active'); + filterByCompletedItemsButton = this.page.locator('.filters >> text=Completed'); + + constructor(public readonly page: Page) { } + + async addItem(text: string) { + await this.inputBox.fill(text); + await this.inputBox.press('Enter'); + } + + async goto() { + await this.page.goto('https://todomvc.com/examples/vanilla-es6/'); + } +} diff --git a/packages/create-playwright/assets/examples/server/assets/api/v1/users.json b/packages/create-playwright/assets/examples/server/assets/api/v1/users.json new file mode 100644 index 0000000000000..dfdc0555fc9f3 --- /dev/null +++ b/packages/create-playwright/assets/examples/server/assets/api/v1/users.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "fullName": "John Doe" +} diff --git a/packages/create-playwright/assets/examples/server/assets/file-uploads.html b/packages/create-playwright/assets/examples/server/assets/file-uploads.html new file mode 100644 index 0000000000000..808a3046e29e9 --- /dev/null +++ b/packages/create-playwright/assets/examples/server/assets/file-uploads.html @@ -0,0 +1,6 @@ +