Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ src/server/db/generated
/test-results/
/playwright-report/
/playwright/.cache/
e2e/.auth

certificates

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ If you want to use the same set of custom duotone icons that Start UI is already
E2E tests are setup with Playwright.

```sh
pnpm e2e # Run tests in headless mode, this is the command executed in CI
pnpm e2e:ui # Open a UI which allow you to run specific tests and see test execution
pnpm e2e # Run tests in headless mode, this is the command executed in CI
pnpm e2e:setup # Setup context to be used across test for more efficient execution
pnpm e2e:ui # Open a UI which allow you to run specific tests and see test execution
```

> [!WARNING]
> The generated e2e context files contain authentication logic. If you make changes to your local database instance, you should re-run `pnpm e2e:setup`. It will be run automatically in a CI context.
## Production

```bash
Expand Down
2 changes: 1 addition & 1 deletion e2e/api-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test';
import { expect, test } from 'e2e/utils';

test.describe('App Rest API Schema', () => {
test(`App API schema is building without error`, async ({ request }) => {
Expand Down
18 changes: 7 additions & 11 deletions e2e/login.spec.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { expect, test } from '@playwright/test';
import { expect, test } from 'e2e/utils';
import { ADMIN_EMAIL, USER_EMAIL } from 'e2e/utils/constants';
import { pageUtils } from 'e2e/utils/page-utils';

test.describe('Login flow', () => {
test('Login as admin', async ({ page }) => {
const utils = pageUtils(page);
await page.goto('/login');
await utils.login({ email: ADMIN_EMAIL });
await page.to('/login');
await page.login({ email: ADMIN_EMAIL });
await page.waitForURL('/manager');
await expect(page.getByTestId('layout-manager')).toBeVisible();
});

test('Login as user', async ({ page }) => {
const utils = pageUtils(page);
await page.goto('/login');
await utils.login({ email: USER_EMAIL });
await page.to('/login');
await page.login({ email: USER_EMAIL });
await page.waitForURL('/app');
await expect(page.getByTestId('layout-app')).toBeVisible();
});

test('Login with redirect', async ({ page }) => {
const utils = pageUtils(page);
await page.goto('/app');
await utils.login({ email: ADMIN_EMAIL });
await page.to('/app');
await page.login({ email: ADMIN_EMAIL });
await page.waitForURL('/app');
await expect(page.getByTestId('layout-app')).toBeVisible();
});
Expand Down
32 changes: 32 additions & 0 deletions e2e/setup/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect } from '@playwright/test';
import { test as setup } from 'e2e/utils';
import {
ADMIN_EMAIL,
ADMIN_FILE,
USER_EMAIL,
USER_FILE,
} from 'e2e/utils/constants';

/**
* @see https://playwright.dev/docs/auth#multiple-signed-in-roles
*/

setup('authenticate as admin', async ({ page }) => {
await page.to('/login');
await page.login({ email: ADMIN_EMAIL });

await page.waitForURL('/manager');
await expect(page.getByTestId('layout-manager')).toBeVisible();

await page.context().storageState({ path: ADMIN_FILE });
});

setup('authenticate as user', async ({ page }) => {
await page.to('/login');
await page.login({ email: USER_EMAIL });

await page.waitForURL('/app');
await expect(page.getByTestId('layout-app')).toBeVisible();

await page.context().storageState({ path: USER_FILE });
});
74 changes: 74 additions & 0 deletions e2e/users.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { expect, test } from 'e2e/utils';
import { randomString } from 'remeda';

test.describe('User management as user', () => {
test.use({ storageState: 'e2e/.auth/user.json' });
test('Should not have access', async ({ page }) => {
await page.to('/manager/users');

await expect(
page.getByText("You don't have access to this page")
).toBeVisible();
});
});

test.describe('User management as manager', () => {
test.use({
storageState: 'e2e/.auth/admin.json',
});

test.beforeEach(async ({ page }) => {
await page.to('/manager/users');
});

test('Create a user', async ({ page }) => {
await page.getByText('New User').click();

const randomId = randomString(8);
const uniqueEmail = `new-user-${randomId}@user.com`;

// Fill the form
await page.waitForURL('/manager/users/new');
await page.getByLabel('Name').fill('New user');
await page.getByLabel('Email').fill(uniqueEmail);
await page.getByText('Create').click();

await page.waitForURL('/manager/users');
await page.getByPlaceholder('Search...').fill('new-user');
await expect(page.getByText(uniqueEmail)).toBeVisible();
});

test('Edit a user', async ({ page }) => {
await page.getByText('[email protected]').click({
force: true,
});

await page.getByRole('link', { name: 'Edit user' }).click();

const randomId = randomString(8);
const newAdminName = `Admin ${randomId}`;
await page.getByLabel('Name').fill(newAdminName);
await page.getByText('Save').click();

await expect(page.getByText(newAdminName).first()).toBeVisible();
});

test('Delete a user', async ({ page }) => {
await page
.getByText('user', {
exact: true,
})
.first()
.click({ force: true });

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

await expect(
page.getByText('You are about to permanently delete this user.')
).toBeVisible();

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

await expect(page.getByText('User deleted')).toBeVisible();
});
});
5 changes: 5 additions & 0 deletions e2e/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
const AUTH_FILE_BASE = 'e2e/.auth';

export const USER_FILE = `${AUTH_FILE_BASE}/user.json`;
export const USER_EMAIL = '[email protected]';

export const ADMIN_FILE = `${AUTH_FILE_BASE}/admin.json`;
export const ADMIN_EMAIL = '[email protected]';
9 changes: 9 additions & 0 deletions e2e/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test as base } from '@playwright/test';
import { ExtendedPage, pageWithUtils } from 'e2e/utils/page';

const test = base.extend<ExtendedPage>({
page: pageWithUtils,
});

export * from '@playwright/test';
export { test };
61 changes: 0 additions & 61 deletions e2e/utils/page-utils.ts

This file was deleted.

66 changes: 66 additions & 0 deletions e2e/utils/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect, Page } from '@playwright/test';
import { CustomFixture } from 'e2e/utils/types';

import { DEFAULT_LANGUAGE_KEY } from '@/lib/i18n/constants';

import {
AUTH_EMAIL_OTP_MOCKED,
AUTH_SIGNUP_ENABLED,
} from '@/features/auth/config';
import locales from '@/locales';
import { FileRouteTypes } from '@/routeTree.gen';

interface PageUtils {
/**
* Utility used to authenticate a user on the app
*/
login: (input: { email: string; code?: string }) => Promise<void>;

/**
* Override of the `page.goto` method with typed routes from the app
*/
to: (
url: FileRouteTypes['to'],
options?: Parameters<Page['goto']>[1]
) => ReturnType<Page['goto']>;
}

export type ExtendedPage = { page: PageUtils };

export const pageWithUtils: CustomFixture<Page & PageUtils> = async (
{ page },
apply
) => {
page.login = async function login(input: { email: string; code?: string }) {
const routeLogin = '/login' satisfies FileRouteTypes['to'];
const routeLoginVerify = '/login/verify' satisfies FileRouteTypes['to'];
await page.waitForURL(`**${routeLogin}**`);

await expect(
page.getByText(
locales[DEFAULT_LANGUAGE_KEY].auth.pageLoginWithSignUp.title
)
).toBeVisible();

await page
.getByPlaceholder(locales[DEFAULT_LANGUAGE_KEY].auth.common.email.label)
.fill(input.email);

await page
.getByRole('button', {
name: locales[DEFAULT_LANGUAGE_KEY].auth[
AUTH_SIGNUP_ENABLED ? 'pageLoginWithSignUp' : 'pageLogin'
].loginWithEmail,
})
.click();

await page.waitForURL(`**${routeLoginVerify}**`);
await page
.getByText(locales[DEFAULT_LANGUAGE_KEY].auth.common.otp.label)
.fill(input.code ?? AUTH_EMAIL_OTP_MOCKED);
};

page.to = page.goto;

apply(page);
};
11 changes: 11 additions & 0 deletions e2e/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
PlaywrightTestArgs,
PlaywrightTestOptions,
TestFixture,
} from '@playwright/test';
import { ExtendedPage } from 'e2e/utils/page';

export type CustomFixture<TReturn> = TestFixture<
TReturn,
PlaywrightTestArgs & PlaywrightTestOptions & ExtendedPage
>;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
"test": "vitest --browser.headless",
"test:ci": "vitest run",
"test:ui": "vitest",
"e2e:setup": "dotenv -- cross-env playwright test --project=setup",
"e2e": "dotenv -- cross-env playwright test",
"e2e:ui": "dotenv -- cross-env playwright test --ui",
"dk:init": "docker compose up -d",
"dk:start": "docker compose start",
"dk:stop": "docker compose stop",
"dk:clear": "docker compose down --volumes",
"db:init": "pnpm db:push && pnpm db:seed",
"db:reset": "pnpm dk:clear && pnpm dk:init && pnpm db:init",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that db:init can fail here if db is not ready in the docker after dk:init 😕

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was more of a quick workaround to get a "Clean db state" between e2e test runs when running them in local !

We should discuss what's the best path forward to have consistent e2e test runs

"db:push": "prisma db push",
"db:ui": "prisma studio",
"db:seed": "dotenv -- cross-env node ./run-jiti ./prisma/seed/index.ts"
Expand Down
5 changes: 5 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ export default defineConfig({

/* Configure projects for major browsers */
projects: [
// eslint-disable-next-line sonarjs/slow-regex
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: process.env.CI ? ['setup'] : [],
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: process.env.CI ? ['setup'] : [],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: process.env.CI ? ['setup'] : [],
},
],

Expand Down
Loading