Skip to content

Commit

Permalink
docs: add examples (#28)
Browse files Browse the repository at this point in the history
* docs: add examples

* chore: cleanup basic playwright config

* ci: run tests from `examples` directory during CI build

* test: add basic suite to examples directory

* docs: refactor readme
  • Loading branch information
SebastianSedzik authored Dec 31, 2023
1 parent dbc769b commit df030be
Show file tree
Hide file tree
Showing 14 changed files with 1,075 additions and 41 deletions.
22 changes: 15 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,26 @@ on:

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: with NodeJS
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- run: npm ci
- run: npx playwright install chromium
- run: npm run lint
- run: npm run build
- run: npm test
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test (library)
run: |
npx playwright install chromium
npm t
- name: Test (examples)
run: |
cd examples
npx playwright install chromium
npm t
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,32 @@ npm i playwright-decorators
## 🏗️ Usage
Declare tests using `@suite` and `@test` decorators
```ts
import { suite, test } from 'playwright-decorators';
import { suite, test, slow, annotation } from 'playwright-decorators';

@suite() // <-- Decorate class with @suite
class MyTestSuite {
@test() // <-- Decorate test method with @test
async myTest({ page }) {
@test()
async myTest({ page }) { // <-- Decorate test method with @test
// ...
}

@annotation(['team-x']) // <-- Add custom annotation to test
@slow('Processing a new user takes a long time') // <-- Mark test as "slow"
@test()
async userCreation({ page }) {
// ...
}

@withUser({ features: ['payment'] }) // <- Use your own custom decorators
@test()
async userShouldBeAbleToCancelSubscription({ page }) {
// ...
}
}
```
For more advanced use cases, please see [custom decorators](#custom-decorators) section.
To view all the available decorators, check the [documentation](#-documentation) section.
For guidance on creating custom decorators, refer to the [custom decorators](#custom-decorators) section.
Explore additional examples in the [examples](./examples) directory.

## 📝 Documentation
### Creating a test suite: `@suite(options?)`
Expand Down Expand Up @@ -322,7 +337,8 @@ class MyTestSuite {


### Custom decorators
Custom decorators can be created using `createTestDecorator` and `createSuiteDecorator` functions
Custom decorators can be created using `createTestDecorator` and `createSuiteDecorator` functions.
Simple usage examples are provided below. For more advanced examples, please refer to [example decorators](./examples/tests/decorators) directory.

#### Test decorator
The `createTestDecorator` function enables the generation of custom test decorators.
Expand Down
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Examples of using the `playwright-decorators` library
To see all available decorators, refer to the [documentation](../README.md#-documentation)

## Tests
The tests are located in the [./tests](./tests) directory.
Each test file contains a description that explains the purpose of the test.

## Custom decorators
Custom decorators are located in the [./tests/decorators](./tests/decorators) directory.
Each decorator includes code comments to help understand its functionality.
118 changes: 118 additions & 0 deletions examples/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const express = require('express');
const expressSession = require('express-session');
const { faker } = require('@faker-js/faker');
const app = express();
const usersDB = [];

app.use(expressSession({
secret: 'secret_key',
resave: false,
saveUninitialized: true
}))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))

function isAuthenticated (req, res, next) {
if (req.session.userId) {
return next()
}

return res.redirect('/sign-in');
}

/**
* Create a test user
*/
app.post('/create-user', (req, res) => {
const user = {
userId: faker.datatype.uuid(),
username: faker.internet.userName(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
password: faker.internet.password(),
features: req.body.features
}

usersDB.push(user);
res.json(user);
})

/**
* Sign in
*/
app.get('/sign-in', (req, res) => {
res.send(`
<h1>Sign in</h1>
<form action="/sign-in" method="POST">
<input name="email" data-testid="sign-in-email"/>
<input name="password" data-testid="sign-in-password" type="password"/>
<button type="submit" data-testid="sign-in-submit">sign in</button>
</form>
`)
})

/**
* Sign in request
*/
app.post('/sign-in', (req, res) => {
const { email, password } = req.body;
const user = usersDB.find(user => user.email === email && user.password === password);

if (!user) {
return res.redirect('/sign-in');
}

req.session.regenerate(function (err) {
if (err) next(err)

req.session.userId = user.userId
res.redirect('/');
})
});


/**
* Sign in SSO (in progress) - response is delayed by 1 second
*/
app.get('/sign-in/sso', (req, res) => {
setTimeout(() => {
res.send(`
<h1 data-testid="page-title">SSO Login</h1>
<h2>Select provider</h2>
<ul>
<li>X</li>
<li>Y</li>
</ul>
`)
}, 1000)
})

/**
* Authenticated page
*/
app.get('/', isAuthenticated, (req, res) => {
const user = usersDB.find(user => user.userId === req.session.userId);

res.send(`
<h1 data-testid="page-title">Hello ${user.username} 👋</h1>
<a href="/settings">Settings</a>
`)
})

/**
* Settings page
*/
app.get('/settings', isAuthenticated, (req, res) => {
const user = usersDB.find(user => user.userId === req.session.userId);

res.send(`
<h1>Settings</h1>
<h2>Features</h2>
<ul>
${ user.features.map(feature => `<li data-testid="settings-feature">${feature}</li>`).join('\n') }
</ul>
`)
})

app.listen(3000);
console.log("Server listening on port 3000");
17 changes: 17 additions & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "examples",
"private": true,
"scripts": {
"start": "node app.js",
"test": "playwright test"
},
"devDependencies": {
"@faker-js/faker": "7.6.0",
"@playwright/test": "^1.36.1",
"body-parser": "1.20.2",
"express": "4.18.2",
"express-session": "1.17.3",
"node-fetch": "2.7.0",
"playwright-decorators": "../"
}
}
30 changes: 30 additions & 0 deletions examples/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineConfig, devices } from '@playwright/test';

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'html',
timeout: 500,
use: {
trace: 'on-first-retry'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
}
],
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
stdout: 'ignore',
stderr: 'pipe',
},
});
48 changes: 48 additions & 0 deletions examples/tests/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {expect} from "@playwright/test";
import {
beforeEach,
suite,
test,
tag,
fixme, slow, annotation
} from "playwright-decorators";

@tag(['x-api-consumer'])
@suite()
class SignInSuite {
@beforeEach()
async setPageRoute({ page }) {
// set sign-in page context for each test in the suite
await page.goto('http://localhost:3000/sign-in');
}

@test()
async userShouldNotBeAbleToSignInWithInvalidCredentials({ page }) {
// when user fills invalid credentials & submits the form
await page.getByTestId('sign-in-email').fill("[email protected]");
await page.getByTestId('sign-in-password').fill("example password");
await page.getByTestId('sign-in-submit').click();
// then user is redirected to sign-in page
await expect(page).toHaveURL('http://localhost:3000/sign-in')
}

@fixme("Unstable test")
@annotation({
type: 'issue',
description: 'jira.com/issue-123'
})
@test()
async userShouldBeAbleToResetPassword({ page }) {
await page.goto('http://localhost:3000/sign-in/reset');
// ...
}

@tag(['team-y'])
@slow("/sign-in/sso page is under the development, and needs more then 500ms to load")
@test()
async userShouldBeAbleToLoginViaSSO({ page }) {
await page.goto('http://localhost:3000/sign-in/sso');
await expect(page.getByTestId('page-title')).toHaveText('SSO Login');
// ...
}
}
42 changes: 42 additions & 0 deletions examples/tests/custom-decorators.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { suite, test } from "playwright-decorators";
import { withUser, withRoute } from './decorators';
import { expect } from "@playwright/test";

/**
* This suite is an example of usage of custom decorators.
* Suite is using `withUser` suite-decorator that provides logged-in user context for each @test in the suite.
* Also, you can find here `withRoute` test-decorator that navigates to specific route before specific @test.
*/
@withUser({ features: ['feature-a', 'feature-b'] }) // <- usage of custom `withUser` decorator
@suite()
class AuthorizedUserSuite {
@test()
async shouldBeLogged({ page }) {
// When on `/` route
await page.goto('http://localhost:3000/')

// Then username should be displayed
await expect(page.getByTestId('page-title')).toHaveText(/Hello (?<userName>.*) 👋/)
}

@withRoute('settings') // <- usage of custom `withRoute` decorator
@test()
async shouldHaveRequestedFeatures({ page }) {
// When on `/settings` route
await expect(page).toHaveURL('http://localhost:3000/settings')

// Then all requested features should be available (features passed in @withUser decorator)
await expect(page.getByTestId('settings-feature'))
.toHaveText(['feature-a', 'feature-b']);
}
}


@suite() // <- no `withUser` decorator, so user in @tests are not logged in
class UnauthorizedUserSuite {
@withRoute('settings') // <- usage of custom `withRoute` decorator
@test()
async shouldBeRedirectedToSignInPage({ page }) {
await expect(page).toHaveURL(/sign-in/)
}
}
2 changes: 2 additions & 0 deletions examples/tests/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './withRoute';
export * from './withUser';
21 changes: 21 additions & 0 deletions examples/tests/decorators/withRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {createTestDecorator} from "playwright-decorators";
import playwright, {Page} from "@playwright/test";

/**
* Navigate to specific route before given @test.
* Please use it with `@test` decorator.
* @param url
*/
export const withRoute = (url: string) => createTestDecorator('withRoute', ({ test }) => {
let _page: Page;

// #1 Extract `page` from test context
playwright.beforeEach(({ page }) => {
_page = page;
})

// #2 Go to specific route before test
test.beforeTest(async () => {
await _page.goto(`http://localhost:3000/${url}`);
});
})
Loading

0 comments on commit df030be

Please sign in to comment.