Skip to content

Commit

Permalink
test(e2e): setup playwright, add tests for login scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
Callenowy committed Jan 30, 2024
2 parents f0fba3a + 2a4e9be commit 6ba7a2d
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 3 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ NEXT_PUBLIC_HOST=http://localhost:3000
DB_PATH=./src/db
APP_PEPPER=
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000

E2E_TEST_USERNAME=
E2E_TEST_PASSWORD=
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
src/db/db.sqlite
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/tests/playwright/.auth/
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@
"test:unit": "vitest",
"test:unit:coverage": "vitest run --coverage",
"test:unit:ui": "vitest --ui",
"postinstall": "husky install",
"test:e2e": "npx playwright test",
"test:e2e:show-report": "npx playwright show-report",
"test:e2e:ui": "npx playwright test --ui",
"test:e2e:run": "cross-env CI=true playwright test",
"test:e2e:install": "npx playwright install --with-deps chromium",
"test:e2e:webserver": "run-s build start",
"db:migration:generate": "drizzle-kit generate:sqlite",
"db:migration:run": "tsx ./src/db/migrate.ts",
"db:check": "drizzle-kit up:sqlite",
"db:studio": "npx drizzzle-kit studio",
"db:seed": "tsx ./src/db/seed.ts",
"db:user:create": "tsx ./src/db/userCreate.ts"
"db:user:create": "tsx ./src/db/userCreate.ts",
"postinstall": "run-s husky:install test:e2e:install",
"husky:install": "husky install"
},
"dependencies": {
"@auth/drizzle-adapter": "^0.3.17",
Expand Down Expand Up @@ -51,6 +58,7 @@
"@commitlint/config-conventional": "^18.4.4",
"@faker-js/faker": "^8.3.1",
"@inquirer/prompts": "^3.3.0",
"@playwright/test": "^1.41.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^11.1.0",
"@semantic-release/git": "^10.0.1",
Expand Down
56 changes: 56 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { defineConfig, devices } from '@playwright/test';

const PORT = process.env.PORT || '3000';

export default defineConfig({
testDir: './tests/playwright',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: !!process.env.CI ? 'github' : 'html',
use: {
baseURL: `http://localhost:${PORT}/`,
trace: 'on-first-retry',
},

projects: [
{ name: 'setup', testMatch: /\/tests\/playwright\/.*\.setup\.ts/ },

{
name: 'Authenticated user',
use: {
...devices['Desktop Chrome'],
storageState: 'tests/playwright/.auth/user.json',
},
testIgnore: /\/tests\/playwright\/.*\.login\.spec\.ts/,
dependencies: ['setup'],
},

{
name: 'Unauthenticated user',
use: {
...devices['Desktop Chrome'],
},
testMatch: /\/tests\/playwright\/.*\.login\.spec\.ts/,
},
],

timeout: 15 * 1000,
expect: {
timeout: 5 * 1000,
},

webServer: {
// @FIXME: Optimize webserver command
//command: 'npm run test:e2e:webserver',
command: 'npm run dev',
port: Number(PORT),
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
env: {
PORT,
},
},
});
10 changes: 10 additions & 0 deletions tests/playwright/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test as setup } from '@playwright/test';
import { loginToApplication } from './auth.utils';

const authFile = 'tests/playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
await loginToApplication(page);

await page.context().storageState({ path: authFile });
});
89 changes: 89 additions & 0 deletions tests/playwright/auth.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'dotenv/config';

import { type Page, expect, test } from '@playwright/test';
import { ROUTE } from '@/routes';

export type Credentials = {
username: string;
password: string;
};

type CredentialsAssertion = [string[], Credentials];
type EmptyCredentialsAssertion = [string[], Partial<Credentials>];

const getCredentials = (): Credentials => {
const username = process.env.E2E_TEST_USERNAME;
const password = process.env.E2E_TEST_PASSWORD;

if (!username || !password) {
throw new Error(
'Please set `E2E_TEST_USERNAME` and `E2E_TEST_PASSWORD` environment variables to authenticate.'
);
}

return { username, password };
};

export async function loginToApplication(page: Page) {
test.setTimeout(120000);
const { username, password } = getCredentials();

await page.goto(ROUTE.LOGIN);
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Log in' }).click();

await page.waitForURL(ROUTE.SERVER_LIST);

await expect(
page.getByRole('heading', { name: 'Server list' })
).toBeVisible();
}

export const INCORRECT_CREDENTIALS = ((): CredentialsAssertion[] => {
const { username, password } = getCredentials();

return [
[
['password'],
{
username,
password: 'incorrect',
},
],
[
['username'],
{
username: 'incorrect',
password,
},
],
[
['username', 'password'],
{
username: 'incorrect',
password: 'incorrect',
},
],
];
})();

export const EMPTY_CREDENTIALS = ((): EmptyCredentialsAssertion[] => {
const { username, password } = getCredentials();

return [
[
['password'],
{
username,
},
],
[
['username'],
{
password,
},
],
[['username', 'password'], {}],
];
})();
66 changes: 66 additions & 0 deletions tests/playwright/login.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test, expect } from '@playwright/test';
import {
loginToApplication,
INCORRECT_CREDENTIALS,
EMPTY_CREDENTIALS,
} from './auth.utils';
import { ROUTE } from '@/routes';

const ERROR_MESSAGE = 'Unable to authenticate with the given credentials.';

test.use({ storageState: { cookies: [], origins: [] } });

test.beforeEach(async ({ page }) => {
await page.goto(ROUTE.LOGIN);
});

test.describe('Login page', () => {
test('Correct page title', async ({ page }) => {
await page.goto(ROUTE.LOGIN);

await expect(page).toHaveTitle('Login - The Stack');
});

test('Login into app with given credentials', async ({ page }) => {
await loginToApplication(page);

await expect(page).toHaveURL('/server-list');
});

for (const [fieldType, { username, password }] of INCORRECT_CREDENTIALS) {
test(`Attempt to login with incorrect ${fieldType.join(' and ')}`, async ({
page,
}) => {
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);

await page.getByRole('button', { name: 'Log in' }).click();

await expect(page.getByTestId('login-error')).toBeVisible();
await expect(page.getByTestId('login-error')).toContainText(
ERROR_MESSAGE
);
});
}

for (const [emptyFields, credentials] of EMPTY_CREDENTIALS) {
test(`Attempt to login with empty ${emptyFields.join(' and ')}`, async ({
page,
}) => {
if (credentials.password) {
await page.getByLabel('Password').fill(credentials.password);
}

if (credentials.username) {
await page.getByLabel('Username').fill(credentials.username);
}

await page.getByRole('button', { name: 'Log in' }).click();

await expect(page.getByRole('status')).toHaveCount(emptyFields.length);
await expect(page.getByRole('status')).toHaveText(
emptyFields.map(field => new RegExp(`${field} is required`, 'i'))
);
});
}
});

0 comments on commit 6ba7a2d

Please sign in to comment.