mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 12:36:22 +03:00
feat(create-playwright): add option to add examples (#8963)
This commit is contained in:
parent
df188d5876
commit
dd5364e445
@ -12,4 +12,4 @@ browser_patches/*/checkout/
|
||||
browser_patches/chromium/output/
|
||||
**/*.d.ts
|
||||
output/
|
||||
/test-results/
|
||||
test-results/
|
||||
|
@ -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\")");
|
||||
|
@ -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');
|
||||
});
|
54
packages/create-playwright/assets/examples/2-actions.spec.ts
Normal file
54
packages/create-playwright/assets/examples/2-actions.spec.ts
Normal file
@ -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');
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
8
packages/create-playwright/assets/examples/README.md
Normal file
8
packages/create-playwright/assets/examples/README.md
Normal file
@ -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
|
||||
```
|
@ -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;
|
14
packages/create-playwright/assets/examples/pom/fixtures.ts
Normal file
14
packages/create-playwright/assets/examples/pom/fixtures.ts
Normal file
@ -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;
|
@ -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([]);
|
||||
});
|
69
packages/create-playwright/assets/examples/pom/pom.spec.ts
Normal file
69
packages/create-playwright/assets/examples/pom/pom.spec.ts
Normal file
@ -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([]);
|
||||
});
|
||||
});
|
@ -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/');
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": 1,
|
||||
"fullName": "John Doe"
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<iframe src="header.html" style="width: 100%; height: 200px; border: 0"/>
|
||||
|
||||
<form action="/api/v1/file-upload" method="POST" encType="multipart/form-data">
|
||||
<input type="file" name="my-file"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
@ -0,0 +1,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
p, h1 {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Playwright 🎭 Examples
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Run them by executing:
|
||||
</p>
|
||||
<pre>
|
||||
# NPM
|
||||
npm run test:e2e-examples
|
||||
# Yarn
|
||||
yarn test:e2e-examples
|
||||
</pre>
|
||||
</body>
|
@ -0,0 +1 @@
|
||||
<iframe src="header.html" style="width: 100%; height: 200px; border: 0"/>
|
@ -0,0 +1,17 @@
|
||||
<iframe src="header.html" style="width: 100%; height: 200px; border: 0"/>
|
||||
|
||||
<button>Load user</button>
|
||||
<p>
|
||||
User:
|
||||
<span id="user-full-name"></span>
|
||||
</p>
|
||||
|
||||
<script>
|
||||
document.querySelector("button").addEventListener("click", () => {
|
||||
fetch("/api/v1/users.json")
|
||||
.then(resp => resp.json())
|
||||
.then(user => {
|
||||
document.querySelector("#user-full-name").innerText = user.fullName
|
||||
})
|
||||
})
|
||||
</script>
|
73
packages/create-playwright/assets/examples/server/index.js
Normal file
73
packages/create-playwright/assets/examples/server/index.js
Normal file
@ -0,0 +1,73 @@
|
||||
// @ts-check
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
|
||||
const port = 4345;
|
||||
|
||||
class Server {
|
||||
constructor() {
|
||||
this._server = http.createServer(this._handle.bind(this));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {import('http').IncomingMessage} req
|
||||
* @param {import('http').ServerResponse} res
|
||||
*/
|
||||
_handle(req, res) {
|
||||
switch (req.url) {
|
||||
case '/api/v1/file-upload':
|
||||
const chunks = [];
|
||||
req.on('data', chunk => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
const lines = Buffer.concat(chunks).toString().split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '') {
|
||||
res.end(lines.slice(0, i - 1).join('\n'))
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
break;
|
||||
|
||||
default:
|
||||
const localFilePath = path.join(__dirname, 'assets', req.url === '/' ? 'index.html' : req.url);
|
||||
function shouldServe() {
|
||||
try {
|
||||
const result = fs.statSync(localFilePath)
|
||||
if (result.isDirectory())
|
||||
return false;
|
||||
return true
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!shouldServe()) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
const extension2ContentType = {
|
||||
'.html': 'text/html',
|
||||
'.json': 'application/json',
|
||||
}
|
||||
const contentType = extension2ContentType[path.extname(localFilePath)];
|
||||
if (contentType)
|
||||
res.setHeader('Content-Type', contentType);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
res.end(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {number} port
|
||||
*/
|
||||
async listen(port) {
|
||||
await new Promise(resolve => this._server.listen(port, () => resolve));
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log(`Listening on http://127.0.0.1:${port}`);
|
||||
await new Server().listen(port);
|
||||
})()
|
@ -19,14 +19,16 @@ import fs from 'fs';
|
||||
import { prompt } from 'enquirer';
|
||||
import colors from 'ansi-colors';
|
||||
|
||||
import { executeCommands, createFiles, determinePackageManager, executeTemplate, Command, languagetoFileExtension } from './utils';
|
||||
import { executeCommands, createFiles, determinePackageManager, executeTemplate, Command, languagetoFileExtension, readDirRecursively } from './utils';
|
||||
|
||||
export type PromptOptions = {
|
||||
testDir: string,
|
||||
installGitHubActions: boolean,
|
||||
language: 'JavaScript' | 'TypeScript'
|
||||
addExamples: boolean,
|
||||
};
|
||||
|
||||
const assetsDir = path.join(__dirname, '..', 'assets');
|
||||
const PACKAGE_JSON_TEST_SCRIPT_CMD = 'test:e2e';
|
||||
|
||||
export class Generator {
|
||||
@ -44,7 +46,7 @@ export class Generator {
|
||||
executeCommands(this.rootDir, commands);
|
||||
await createFiles(this.rootDir, files);
|
||||
this._patchGitIgnore();
|
||||
await this._patchPackageJSON();
|
||||
await this._patchPackageJSON(answers);
|
||||
this._printOutro(answers);
|
||||
}
|
||||
|
||||
@ -78,6 +80,12 @@ export class Generator {
|
||||
message: 'Add a GitHub Actions workflow?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'addExamples',
|
||||
message: 'Add common examples which demonstrate Playwright\'s capabilities?',
|
||||
initial: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@ -100,6 +108,9 @@ export class Generator {
|
||||
|
||||
files.set(path.join(answers.testDir, `example.spec.${fileExtension}`), this._readAsset(`example.spec.${fileExtension}`));
|
||||
|
||||
if (answers.addExamples)
|
||||
await this._collectExamples(answers, files);
|
||||
|
||||
if (!fs.existsSync(path.join(this.rootDir, 'package.json'))) {
|
||||
commands.push({
|
||||
name: `Initializing ${this.packageManager === 'yarn' ? 'Yarn' : 'NPM'} project`,
|
||||
@ -132,16 +143,27 @@ export class Generator {
|
||||
}
|
||||
|
||||
private _readAsset(asset: string): string {
|
||||
const assetsDir = path.join(__dirname, '..', 'assets');
|
||||
return fs.readFileSync(path.join(assetsDir, asset), 'utf-8');
|
||||
return fs.readFileSync(path.isAbsolute(asset) ? asset : path.join(assetsDir, asset), 'utf-8');
|
||||
}
|
||||
|
||||
private async _patchPackageJSON() {
|
||||
private async _collectExamples(answers: PromptOptions, files: Map<string, string>) {
|
||||
const outDir = answers.testDir + '-examples';
|
||||
const examplesDir = path.join(assetsDir, 'examples');
|
||||
|
||||
for (const example of await readDirRecursively(examplesDir)) {
|
||||
const relativePath = path.relative(examplesDir, example);
|
||||
files.set(path.join(outDir, relativePath), this._readAsset(example));
|
||||
}
|
||||
}
|
||||
|
||||
private async _patchPackageJSON(answers: PromptOptions) {
|
||||
const packageJSON = JSON.parse(fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf-8'));
|
||||
if (!packageJSON.scripts)
|
||||
packageJSON.scripts = {};
|
||||
if (packageJSON.scripts['test']?.includes('no test specified'))
|
||||
delete packageJSON.scripts['test'];
|
||||
if (answers.addExamples)
|
||||
packageJSON.scripts['test:e2e-examples'] = `playwright test --config ${path.join(answers.testDir + '-examples', 'playwright.config.ts')}`;
|
||||
packageJSON.scripts[PACKAGE_JSON_TEST_SCRIPT_CMD] = `playwright test`;
|
||||
|
||||
const files = new Map<string, string>();
|
||||
|
@ -72,3 +72,12 @@ export function executeTemplate(input: string, args: Record<string, string>): st
|
||||
export function languagetoFileExtension(language: 'JavaScript' | 'TypeScript'): 'js' | 'ts' {
|
||||
return language === 'JavaScript' ? 'js' : 'ts';
|
||||
}
|
||||
|
||||
export async function readDirRecursively(dir: string): Promise<string[]> {
|
||||
const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
const files = await Promise.all(dirents.map(async (dirent): Promise<string[]|string> => {
|
||||
const res = path.join(dir, dirent.name);
|
||||
return dirent.isDirectory() ? await readDirRecursively(res) : res;
|
||||
}));
|
||||
return files.flat();
|
||||
}
|
||||
|
87
packages/create-playwright/tests/baseFixtures.ts
Normal file
87
packages/create-playwright/tests/baseFixtures.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test as base } from '@playwright/test';
|
||||
import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { PromptOptions } from '../src/generator';
|
||||
|
||||
type TestFixtures = {
|
||||
packageManager: 'npm' | 'yarn';
|
||||
run: (parameters: string[], options: PromptOptions) => Promise<RunResult>,
|
||||
};
|
||||
|
||||
type RunResult = {
|
||||
exitCode: number | null,
|
||||
dir: string,
|
||||
stdout: string,
|
||||
exec: typeof spawnAsync
|
||||
};
|
||||
|
||||
function spawnAsync(cmd: string, args: string[], options?: SpawnOptionsWithoutStdio): Promise<{stdout: string, stderr: string, code: number | null, error?: Error}> {
|
||||
const p = spawn(cmd, args, options);
|
||||
|
||||
return new Promise(resolve => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
if (process.env.CR_PW_DEBUG) {
|
||||
p.stdout.on('data', chunk => process.stdout.write(chunk));
|
||||
p.stderr.on('data', chunk => process.stderr.write(chunk));
|
||||
}
|
||||
if (p.stdout)
|
||||
p.stdout.on('data', data => stdout += data);
|
||||
if (p.stderr)
|
||||
p.stderr.on('data', data => stderr += data);
|
||||
p.on('close', code => resolve({ stdout, stderr, code }));
|
||||
p.on('error', error => resolve({ stdout, stderr, code: 0, error }));
|
||||
});
|
||||
}
|
||||
|
||||
export const test = base.extend<TestFixtures>({
|
||||
packageManager: 'npm',
|
||||
run: async ({ packageManager }, use, testInfo) => {
|
||||
await use(async (parameters: string[], options: PromptOptions): Promise<RunResult> => {
|
||||
fs.mkdirSync(testInfo.outputDir, { recursive: true });
|
||||
const env = packageManager === 'yarn' ? {
|
||||
'npm_config_user_agent': 'yarn'
|
||||
} : undefined;
|
||||
const result = await spawnAsync('node', [path.join(__dirname, '..'), ...parameters], {
|
||||
shell: true,
|
||||
cwd: testInfo.outputDir,
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
'TEST_OPTIONS': JSON.stringify(options),
|
||||
}
|
||||
});
|
||||
const execWrapper = (cmd: string, args: string[], options?: SpawnOptionsWithoutStdio): ReturnType<typeof spawnAsync> => {
|
||||
return spawnAsync(cmd, args, {
|
||||
...options,
|
||||
cwd: testInfo.outputDir,
|
||||
});
|
||||
};
|
||||
return {
|
||||
exitCode: result.code,
|
||||
dir: testInfo.outputDir,
|
||||
stdout: result.stdout,
|
||||
exec: execWrapper,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const expect = test.expect;
|
@ -13,66 +13,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { spawn } from 'child_process';
|
||||
import { test, expect } from './baseFixtures';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import type { PromptOptions } from '../src/generator';
|
||||
|
||||
type TestFixtures = {
|
||||
packageManager: 'npm' | 'yarn';
|
||||
run: (parameters: string[], options: PromptOptions) => Promise<RunResult>,
|
||||
};
|
||||
|
||||
type RunResult = {
|
||||
exitCode: number|null,
|
||||
dir: string,
|
||||
stderr: string,
|
||||
stdout: string,
|
||||
};
|
||||
|
||||
const test = base.extend<TestFixtures>({
|
||||
packageManager: 'npm',
|
||||
run: async ({ packageManager }, use, testInfo) => {
|
||||
await use(async (parameters: string[], options: PromptOptions): Promise<RunResult> => {
|
||||
fs.mkdirSync(testInfo.outputDir, { recursive: true });
|
||||
const env = packageManager === 'yarn' ? {
|
||||
'npm_config_user_agent': 'yarn'
|
||||
} : undefined;
|
||||
const p = spawn('node', [path.join(__dirname, '..'), ...parameters], {
|
||||
shell: true,
|
||||
cwd: testInfo.outputDir,
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
'TEST_OPTIONS': JSON.stringify(options),
|
||||
}
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
p.stdout.on('data', data => stdout += data.toString());
|
||||
p.stderr.on('data', data => stderr += data.toString());
|
||||
let resolve = (result: RunResult) => { };
|
||||
const waitUntilExit = new Promise<RunResult>(r => resolve = r);
|
||||
p.on('exit', exitCode => {
|
||||
resolve({ exitCode, dir: testInfo.outputDir, stdout, stderr });
|
||||
});
|
||||
if (process.env.DEBUG) {
|
||||
p.stdout.on('data', chunk => process.stdout.write(chunk));
|
||||
p.stderr.on('data', chunk => process.stderr.write(chunk));
|
||||
}
|
||||
return await waitUntilExit;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
for (const packageManager of ['npm', 'yarn'] as ('npm' | 'yarn')[]) {
|
||||
test.describe(`Package manager: ${packageManager}`, () => {
|
||||
test.use({ packageManager });
|
||||
|
||||
test('should generate a project in the current directory', async ({ run }) => {
|
||||
const { exitCode, dir, stdout } = await run([], { installGitHubActions: true, testDir: 'e2e', language: 'TypeScript' });
|
||||
const { exitCode, dir, stdout } = await run([], { installGitHubActions: true, testDir: 'e2e', language: 'TypeScript', addExamples: false });
|
||||
expect(exitCode).toBe(0);
|
||||
expect(fs.existsSync(path.join(dir, 'e2e/example.spec.ts'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy();
|
||||
@ -96,7 +46,7 @@ for (const packageManager of ['npm', 'yarn'] as ('npm' | 'yarn')[]) {
|
||||
});
|
||||
|
||||
test('should generate a project in a given directory', async ({ run }) => {
|
||||
const { exitCode, dir } = await run(['foobar'], { installGitHubActions: true, testDir: 'e2e', language: 'TypeScript' });
|
||||
const { exitCode, dir } = await run(['foobar'], { installGitHubActions: true, testDir: 'e2e', language: 'TypeScript', addExamples: false });
|
||||
expect(exitCode).toBe(0);
|
||||
expect(fs.existsSync(path.join(dir, 'foobar/e2e/example.spec.ts'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'foobar/package.json'))).toBeTruthy();
|
||||
@ -109,7 +59,7 @@ for (const packageManager of ['npm', 'yarn'] as ('npm' | 'yarn')[]) {
|
||||
});
|
||||
|
||||
test('should generate a project with JavaScript and without GHA', async ({ run }) => {
|
||||
const { exitCode, dir } = await run([], { installGitHubActions: false, testDir: 'tests', language: 'JavaScript' });
|
||||
const { exitCode, dir } = await run([], { installGitHubActions: false, testDir: 'tests', language: 'JavaScript', addExamples: false });
|
||||
expect(exitCode).toBe(0);
|
||||
expect(fs.existsSync(path.join(dir, 'tests/example.spec.js'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy();
|
||||
@ -120,5 +70,24 @@ for (const packageManager of ['npm', 'yarn'] as ('npm' | 'yarn')[]) {
|
||||
expect(fs.existsSync(path.join(dir, 'playwright.config.js'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should generate be able to run the examples successfully', async ({ run }) => {
|
||||
const { exitCode, dir, exec } = await run([], { installGitHubActions: false, testDir: 'tests', language: 'TypeScript', addExamples: true });
|
||||
expect(exitCode).toBe(0);
|
||||
expect(fs.existsSync(path.join(dir, 'tests/example.spec.ts'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy();
|
||||
|
||||
expect(fs.existsSync(path.join(dir, 'playwright.config.ts'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'tests-examples/playwright.config.ts'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'tests-examples/server/index.js'))).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(dir, 'tests-examples/1-getting-started.spec.ts'))).toBeTruthy();
|
||||
|
||||
let result;
|
||||
if (packageManager === 'npm')
|
||||
result = await exec('npm', ['run', 'test:e2e-examples']);
|
||||
else
|
||||
result = await exec('yarn', ['test:e2e-examples']);
|
||||
expect(result.code).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user