adds playwright tests

This commit is contained in:
Christian Beutel
2024-03-11 18:15:27 +01:00
parent 02c82d3e46
commit 2e3c3abbe4
17 changed files with 4689 additions and 4425 deletions

9
web/.gitignore vendored
View File

@@ -8,3 +8,12 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
test-results/
playwright-report/
blob-report/
playwright/.cache/
test-results/
playwright-report/
blob-report/
playwright/.cache/
playwright/.auth

40
web/package-lock.json generated
View File

@@ -28,12 +28,13 @@
"yup": "^1.3.3"
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@playwright/test": "^1.42.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/leaflet": "^1.9.8",
"@types/leaflet-gpx": "^1.3.7",
"@types/node": "^20.11.25",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"svelte": "^4.2.7",
@@ -585,12 +586,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.41.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz",
"integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==",
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
"integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"dev": true,
"dependencies": {
"playwright": "1.41.1"
"playwright": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@@ -1073,6 +1074,15 @@
"@types/leaflet": "*"
}
},
"node_modules/@types/node": {
"version": "20.11.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
"integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==",
"devOptional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@@ -2860,12 +2870,12 @@
}
},
"node_modules/playwright": {
"version": "1.41.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz",
"integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==",
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
"integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"dev": true,
"dependencies": {
"playwright-core": "1.41.1"
"playwright-core": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@@ -2878,9 +2888,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.41.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz",
"integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==",
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
"integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -3930,6 +3940,12 @@
"integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==",
"dev": true
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"devOptional": true
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",

View File

@@ -13,12 +13,13 @@
"test:unit": "vitest"
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@playwright/test": "^1.42.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/leaflet": "^1.9.8",
"@types/leaflet-gpx": "^1.3.7",
"@types/node": "^20.11.25",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"svelte": "^4.2.7",

View File

@@ -1,12 +1,88 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { defineConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;
/* Configure projects for major browsers */
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
// {
// name: 'firefox',
// use: {
// ...devices['Desktop Firefox'],
// storageState: 'playwright/.auth/user.json',
// },
// dependencies: ['setup']
// },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
// dependencies: ['setup']
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -23,7 +23,7 @@
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-danger" type="button" on:click={confirm}
<button class="btn-danger" type="button" on:click={confirm} name="delete"
>{$_("delete")}</button
>
</div></Modal

View File

@@ -106,7 +106,7 @@
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-primary" type="submit" form="list-form"
<button class="btn-primary" type="submit" form="list-form" name="save"
>{$_("save")}</button
>
</div>

View File

@@ -116,10 +116,12 @@
</div>
<hr class="my-6 border-input-border" />
<div class="flex flex-col basis-full">
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new"
{#if $currentUser}
<a
class="btn-primary btn-large text-center mx-4"
href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-trail")}</a
>
{#if $currentUser}
<div class="basis-full"></div>
<hr class="border-input-border" />
<div class="flex gap-4 items-center m-4">
@@ -138,6 +140,10 @@
></i></button
>
</div>
{:else}
<a class="btn-primary btn-large text-center mx-4" href="/login"
>{$_("login")}</a
>
{/if}
</div>
</Drawer>
@@ -179,7 +185,9 @@
: 'moon'}"
on:click={() => toggleTheme()}
></button>
<a class="btn-primary btn-large" href="/trail/edit/new"
<a
class="btn-primary btn-large"
href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-trail")}</a
>
<Dropdown

View File

@@ -29,9 +29,7 @@
openListModal();
}
async function saveList(
e: CustomEvent<{ list: List; avatar?: File }>,
) {
async function saveList(e: CustomEvent<{ list: List; avatar?: File }>) {
const result = e.detail;
if (result.list.id) {
await lists_update(result.list, result.avatar);
@@ -39,7 +37,6 @@
await lists_create(result.list, result.avatar);
}
await lists_index();
}
async function handleDropdownClick(
@@ -82,12 +79,13 @@
on:click={beforeListModalOpen}
>
<i class="fa fa-plus text-xl aspect-square"></i>
<h5 class="text-xl font-semibold">{$_("create-new-list")}</h5>
<h5 id="create-list-button" class="text-xl font-semibold">
{$_("create-new-list")}
</h5>
</button>
<hr class="border-separator my-2" />
<div>
{#each $lists as item, i}
<li on:click={() => list.set(item)} role="presentation">
<li class="list-list-item" on:click={() => list.set(item)} role="presentation">
<ListCard
list={item}
on:change={(e) => handleDropdownClick(e, item)}
@@ -98,7 +96,6 @@
{/if}
</li>
{/each}
</div>
</ul>
<TrailList
bind:filter

View File

@@ -0,0 +1,19 @@
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login', { waitUntil: 'networkidle' });
await page.locator('input[name="username"]').fill('Test');
await page.locator('input[name="password"]').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('/');
// End of authentication steps.
await page.context().storageState({ path: authFile });
});

View File

@@ -0,0 +1,8 @@
import { expect, test } from '@playwright/test';
import { IndexPage } from '../../pages/index_page';
test('index page does not show error', async ({ page }) => {
const indexPage = new IndexPage(page);
await indexPage.goto()
await indexPage.hasNoError()
});

View File

@@ -0,0 +1,30 @@
import { test as base, expect } from '@playwright/test';
import { ListsPage } from '../../pages/lists_page';
const test = base.extend<{ listsPage: ListsPage }>({
listsPage: async ({ page }, use) => {
const listsPage = new ListsPage(page);
await listsPage.goto();
await listsPage.create();
await use(listsPage);
await listsPage.removeAll();
},
});
test('shows a list card', async ({ listsPage }) => {
const listItemCount = await listsPage.listItems.count();
await listsPage.create();
expect(listsPage.listItems).toHaveCount(listItemCount + 1);
expect(listsPage.listItemsImage).toBeVisible();
});
test('update a list card', async ({ listsPage }) => {
await listsPage.update();
expect(listsPage.listItems.first()).toContainText("Updated List");
expect(listsPage.listItems.first()).toContainText("New Description");
});
test('delete a list card', async ({ listsPage }) => {
await listsPage.delete();
expect(listsPage.listItems).toHaveCount(0);
});

View File

@@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';
test('logs the user out', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'avatar' }).click();
await page.locator(".menu .menu-item").filter({ hasText: "Logout" }).click();
const cookies = await page.context().cookies();
const pbAuthCookie = cookies.find(cookie => cookie.name === 'pb_auth');
expect(pbAuthCookie).toBeFalsy();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@@ -0,0 +1,19 @@
import { expect, type Locator, type Page } from '@playwright/test';
export class IndexPage {
readonly page: Page;
readonly error: Locator;
constructor(page: Page) {
this.page = page;
this.error = page.getByText("Internal Error");
}
async goto() {
await this.page.goto('/');
}
async hasNoError() {
await expect(this.error).toHaveCount(0);
}
}

View File

@@ -0,0 +1,83 @@
import { type Locator, type Page } from '@playwright/test';
export class ListsPage {
readonly page: Page;
readonly createListButton: Locator;
readonly listItems: Locator;
readonly listItemsImage: Locator;
readonly listModal: Locator;
readonly listModalAvatar: Locator;
readonly listModalName: Locator;
readonly listModalDescription: Locator;
readonly listModalSaveButton: Locator;
readonly confirmModal: Locator;
readonly confirmModalConfirmButton: Locator;
constructor(page: Page) {
this.page = page;
this.createListButton = page.locator("#create-list-button");
this.listModal = page.locator("#list-modal");
this.listModalAvatar = this.listModal.locator('input[name="avatar"]')
this.listModalName = this.listModal.locator('input[name="name"]')
this.listModalDescription = this.listModal.locator('textarea[name="description"]')
this.listModalSaveButton = this.listModal.getByText('Save');
this.listItems = page.locator('.list-list-item');
this.listItemsImage = page.locator('.list-list-item img');
this.confirmModal = page.locator("#confirm-modal");
this.confirmModalConfirmButton = this.confirmModal.locator("button").filter({ hasText: "Delete" });
}
async goto() {
await this.page.goto('/lists', { waitUntil: 'networkidle' });
}
async create(name: string = "Test List") {
await this.createListButton.click();
await this.listModalName.fill(name);
await this.listModalAvatar.setInputFiles([
"./tests/playwright/fixtures/avatar.webp"
]);
await Promise.all([
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
this.listModalSaveButton.click()
]);
}
async update(name: string = "Updated List", description = "New Description") {
await this.listItems.first().locator(".dropdown button").click();
await this.listItems.first().locator(".menu .menu-item").filter({ hasText: "Edit" }).click();
await this.listModalName.fill(name);
await this.listModalDescription.fill(description);
await Promise.all([
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
this.listModalSaveButton.click()
]);
}
async delete() {
await this.listItems.first().locator(".dropdown button").click();
await this.listItems.first().locator(".menu .menu-item").filter({ hasText: "Delete" }).click();
await Promise.all([
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
this.confirmModalConfirmButton.click()
]);
}
async removeAll() {
while ((await this.listItems.count()) > 0) {
await this.delete();
}
}
}

View File

@@ -1,6 +0,0 @@
import { expect, test } from '@playwright/test';
test('index page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
});