Skip to content

Commit 10c683c

Browse files
authored
e2e: Improve setup and utils (#642)
* e2e(setup): Auth context and utils as fixtures * fix: typo * fix(book/seed): Avoir unique title, author constraint failure * fix: feedbacks * fix(package.json): remove flaky script
1 parent 059ae2a commit 10c683c

File tree

14 files changed

+231
-75
lines changed

14 files changed

+231
-75
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ src/server/db/generated
5757
/test-results/
5858
/playwright-report/
5959
/playwright/.cache/
60+
e2e/.auth
6061

6162
certificates
6263

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,13 @@ If you want to use the same set of custom duotone icons that Start UI is already
101101
E2E tests are setup with Playwright.
102102

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

109+
> [!WARNING]
110+
> 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.
108111
## Production
109112

110113
```bash

e2e/api-schema.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@playwright/test';
1+
import { expect, test } from 'e2e/utils';
22

33
test.describe('App Rest API Schema', () => {
44
test(`App API schema is building without error`, async ({ request }) => {

e2e/login.spec.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
1-
import { expect, test } from '@playwright/test';
1+
import { expect, test } from 'e2e/utils';
22
import { ADMIN_EMAIL, USER_EMAIL } from 'e2e/utils/constants';
3-
import { pageUtils } from 'e2e/utils/page-utils';
43

54
test.describe('Login flow', () => {
65
test('Login as admin', async ({ page }) => {
7-
const utils = pageUtils(page);
8-
await page.goto('/login');
9-
await utils.login({ email: ADMIN_EMAIL });
6+
await page.to('/login');
7+
await page.login({ email: ADMIN_EMAIL });
108
await page.waitForURL('/manager');
119
await expect(page.getByTestId('layout-manager')).toBeVisible();
1210
});
1311

1412
test('Login as user', async ({ page }) => {
15-
const utils = pageUtils(page);
16-
await page.goto('/login');
17-
await utils.login({ email: USER_EMAIL });
13+
await page.to('/login');
14+
await page.login({ email: USER_EMAIL });
1815
await page.waitForURL('/app');
1916
await expect(page.getByTestId('layout-app')).toBeVisible();
2017
});
2118

2219
test('Login with redirect', async ({ page }) => {
23-
const utils = pageUtils(page);
24-
await page.goto('/app');
25-
await utils.login({ email: ADMIN_EMAIL });
20+
await page.to('/app');
21+
await page.login({ email: ADMIN_EMAIL });
2622
await page.waitForURL('/app');
2723
await expect(page.getByTestId('layout-app')).toBeVisible();
2824
});

e2e/setup/auth.setup.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect } from '@playwright/test';
2+
import { test as setup } from 'e2e/utils';
3+
import {
4+
ADMIN_EMAIL,
5+
ADMIN_FILE,
6+
USER_EMAIL,
7+
USER_FILE,
8+
} from 'e2e/utils/constants';
9+
10+
/**
11+
* @see https://playwright.dev/docs/auth#multiple-signed-in-roles
12+
*/
13+
14+
setup('authenticate as admin', async ({ page }) => {
15+
await page.to('/login');
16+
await page.login({ email: ADMIN_EMAIL });
17+
18+
await page.waitForURL('/manager');
19+
await expect(page.getByTestId('layout-manager')).toBeVisible();
20+
21+
await page.context().storageState({ path: ADMIN_FILE });
22+
});
23+
24+
setup('authenticate as user', async ({ page }) => {
25+
await page.to('/login');
26+
await page.login({ email: USER_EMAIL });
27+
28+
await page.waitForURL('/app');
29+
await expect(page.getByTestId('layout-app')).toBeVisible();
30+
31+
await page.context().storageState({ path: USER_FILE });
32+
});

e2e/users.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect, test } from 'e2e/utils';
2+
import { ADMIN_FILE, USER_FILE } from 'e2e/utils/constants';
3+
import { randomString } from 'remeda';
4+
5+
test.describe('User management as user', () => {
6+
test.use({ storageState: USER_FILE });
7+
8+
test('Should not have access', async ({ page }) => {
9+
await page.to('/manager/users');
10+
11+
await expect(
12+
page.getByText("You don't have access to this page")
13+
).toBeVisible();
14+
});
15+
});
16+
17+
test.describe('User management as manager', () => {
18+
test.use({ storageState: ADMIN_FILE });
19+
20+
test.beforeEach(async ({ page }) => {
21+
await page.to('/manager/users');
22+
});
23+
24+
test('Create a user', async ({ page }) => {
25+
await page.getByText('New User').click();
26+
27+
const randomId = randomString(8);
28+
const uniqueEmail = `new-user-${randomId}@user.com`;
29+
30+
// Fill the form
31+
await page.waitForURL('/manager/users/new');
32+
await page.getByLabel('Name').fill('New user');
33+
await page.getByLabel('Email').fill(uniqueEmail);
34+
await page.getByText('Create').click();
35+
36+
await page.waitForURL('/manager/users');
37+
await page.getByPlaceholder('Search...').fill('new-user');
38+
await expect(page.getByText(uniqueEmail)).toBeVisible();
39+
});
40+
41+
test('Edit a user', async ({ page }) => {
42+
await page.getByText('admin@admin.com').click({
43+
force: true,
44+
});
45+
46+
await page.getByRole('link', { name: 'Edit user' }).click();
47+
48+
const randomId = randomString(8);
49+
const newAdminName = `Admin ${randomId}`;
50+
await page.getByLabel('Name').fill(newAdminName);
51+
await page.getByText('Save').click();
52+
53+
await expect(page.getByText(newAdminName).first()).toBeVisible();
54+
});
55+
56+
test('Delete a user', async ({ page }) => {
57+
await page
58+
.getByText('user', {
59+
exact: true,
60+
})
61+
.first()
62+
.click({ force: true });
63+
64+
await page.getByRole('button', { name: 'Delete' }).click();
65+
66+
await expect(
67+
page.getByText('You are about to permanently delete this user.')
68+
).toBeVisible();
69+
70+
await page.getByRole('button', { name: 'Delete' }).click();
71+
72+
await expect(page.getByText('User deleted')).toBeVisible();
73+
});
74+
});

e2e/utils/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1+
const AUTH_FILE_BASE = 'e2e/.auth';
2+
3+
export const USER_FILE = `${AUTH_FILE_BASE}/user.json`;
14
export const USER_EMAIL = 'user@user.com';
5+
6+
export const ADMIN_FILE = `${AUTH_FILE_BASE}/admin.json`;
27
export const ADMIN_EMAIL = 'admin@admin.com';

e2e/utils/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test as base } from '@playwright/test';
2+
import { ExtendedPage, pageWithUtils } from 'e2e/utils/page';
3+
4+
const test = base.extend<ExtendedPage>({
5+
page: pageWithUtils,
6+
});
7+
8+
export * from '@playwright/test';
9+
export { test };

e2e/utils/page-utils.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

e2e/utils/page.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, Page } from '@playwright/test';
2+
import { CustomFixture } from 'e2e/utils/types';
3+
4+
import { DEFAULT_LANGUAGE_KEY } from '@/lib/i18n/constants';
5+
6+
import {
7+
AUTH_EMAIL_OTP_MOCKED,
8+
AUTH_SIGNUP_ENABLED,
9+
} from '@/features/auth/config';
10+
import locales from '@/locales';
11+
import { FileRouteTypes } from '@/routeTree.gen';
12+
13+
interface PageUtils {
14+
/**
15+
* Utility used to authenticate a user on the app
16+
*/
17+
login: (input: { email: string; code?: string }) => Promise<void>;
18+
19+
/**
20+
* Override of the `page.goto` method with typed routes from the app
21+
*/
22+
to: (
23+
url: FileRouteTypes['to'],
24+
options?: Parameters<Page['goto']>[1]
25+
) => ReturnType<Page['goto']>;
26+
}
27+
28+
export type ExtendedPage = { page: PageUtils };
29+
30+
export const pageWithUtils: CustomFixture<Page & PageUtils> = async (
31+
{ page },
32+
apply
33+
) => {
34+
page.login = async function login(input: { email: string; code?: string }) {
35+
const routeLogin = '/login' satisfies FileRouteTypes['to'];
36+
const routeLoginVerify = '/login/verify' satisfies FileRouteTypes['to'];
37+
await page.waitForURL(`**${routeLogin}**`);
38+
39+
await expect(
40+
page.getByText(
41+
locales[DEFAULT_LANGUAGE_KEY].auth.pageLoginWithSignUp.title
42+
)
43+
).toBeVisible();
44+
45+
await page
46+
.getByPlaceholder(locales[DEFAULT_LANGUAGE_KEY].auth.common.email.label)
47+
.fill(input.email);
48+
49+
await page
50+
.getByRole('button', {
51+
name: locales[DEFAULT_LANGUAGE_KEY].auth[
52+
AUTH_SIGNUP_ENABLED ? 'pageLoginWithSignUp' : 'pageLogin'
53+
].loginWithEmail,
54+
})
55+
.click();
56+
57+
await page.waitForURL(`**${routeLoginVerify}**`);
58+
await page
59+
.getByText(locales[DEFAULT_LANGUAGE_KEY].auth.common.otp.label)
60+
.fill(input.code ?? AUTH_EMAIL_OTP_MOCKED);
61+
};
62+
63+
page.to = page.goto;
64+
65+
await apply(page);
66+
};

0 commit comments

Comments
 (0)