fix playwright tests (#759)
This commit is contained in:
@@ -11,8 +11,8 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests/playwright',
|
testDir: './tests/playwright',
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel - disabled to prevent auth conflicts */
|
||||||
fullyParallel: true,
|
fullyParallel: false,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
@@ -34,8 +34,10 @@ export default defineConfig({
|
|||||||
projects: [
|
projects: [
|
||||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||||
{
|
{
|
||||||
name: 'teardown', testMatch: /.*\.teardown\.ts/, use:
|
name: 'teardown',
|
||||||
{
|
testMatch: /.*\.teardown\.ts/,
|
||||||
|
dependencies: ['chromium'],
|
||||||
|
use: {
|
||||||
storageState: 'playwright/.auth/user.json',
|
storageState: 'playwright/.auth/user.json',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -52,7 +54,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
// dependencies: ['setup'],
|
dependencies: ['setup'],
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Chrome'],
|
...devices['Desktop Chrome'],
|
||||||
storageState: 'playwright/.auth/user.json',
|
storageState: 'playwright/.auth/user.json',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
: $theme === "light"
|
: $theme === "light"
|
||||||
? emptyStateTrailLight
|
? emptyStateTrailLight
|
||||||
: emptyStateTrailDark}
|
: emptyStateTrailDark}
|
||||||
alt="avatar"
|
alt="list avatar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="self-start min-w-0 basis-full transition-transform">
|
<div class="self-start min-w-0 basis-full transition-transform">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
class="rounded-full w-5 aspect-square mx-1 inline"
|
class="rounded-full w-5 aspect-square mx-1 inline"
|
||||||
src={list.expand.author.icon ||
|
src={list.expand.author.icon ||
|
||||||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||||
alt="avatar"
|
alt="author avatar"
|
||||||
/>
|
/>
|
||||||
{handleFromRecordWithIRI(list)}
|
{handleFromRecordWithIRI(list)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
{#snippet children({ toggleMenu: openDropdown })}
|
{#snippet children({ toggleMenu: openDropdown })}
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button
|
<button
|
||||||
|
aria-label="Open user menu"
|
||||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-10 aspect-square"
|
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-10 aspect-square"
|
||||||
onclick={openDropdown}
|
onclick={openDropdown}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { test as teardown } from '@playwright/test';
|
|||||||
teardown('delete user', async ({ page }) => {
|
teardown('delete user', async ({ page }) => {
|
||||||
await page.goto('/settings/account', { waitUntil: 'networkidle' });
|
await page.goto('/settings/account', { waitUntil: 'networkidle' });
|
||||||
await page.locator("#delete-account").click();
|
await page.locator("#delete-account").click();
|
||||||
|
|
||||||
|
// Wait for modal to appear
|
||||||
|
await page.locator("#confirm").waitFor({ state: 'visible' });
|
||||||
await page.locator("#confirm").click();
|
await page.locator("#confirm").click();
|
||||||
|
|
||||||
await page.waitForURL('/');
|
await page.waitForURL('/');
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
import { test as base, expect } from '@playwright/test';
|
import { test as base, expect } from '@playwright/test';
|
||||||
import { ListsPage } from '../../pages/lists_page';
|
import { ListsPage } from '../../pages/lists_page';
|
||||||
|
|
||||||
|
// Run tests serially to avoid race conditions with shared list data
|
||||||
|
base.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
const test = base.extend<{ listsPage: ListsPage }>({
|
const test = base.extend<{ listsPage: ListsPage }>({
|
||||||
listsPage: async ({ page }, use) => {
|
listsPage: async ({ page }, use) => {
|
||||||
const listsPage = new ListsPage(page);
|
const listsPage = new ListsPage(page);
|
||||||
await listsPage.goto();
|
await listsPage.goto();
|
||||||
await listsPage.create();
|
|
||||||
await use(listsPage);
|
await use(listsPage);
|
||||||
await listsPage.removeAll();
|
await listsPage.removeAll();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows a list card', async ({ listsPage }) => {
|
test('shows a list card', async ({ listsPage }) => {
|
||||||
|
const testListName = `Test List ${Date.now()}`;
|
||||||
const listItemCount = await listsPage.listItems.count();
|
const listItemCount = await listsPage.listItems.count();
|
||||||
await listsPage.create();
|
await listsPage.create(testListName);
|
||||||
expect(listsPage.listItems).toHaveCount(listItemCount + 1);
|
|
||||||
expect(listsPage.listItemsImage).toBeVisible();
|
await expect(listsPage.listItems).toHaveCount(listItemCount + 1);
|
||||||
|
const createdListItem = listsPage.listItems.filter({ hasText: testListName });
|
||||||
|
await expect(createdListItem).toBeVisible();
|
||||||
|
await expect(createdListItem.getByRole('img', { name: 'list avatar' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('update a list card', async ({ listsPage }) => {
|
test('update a list card', async ({ listsPage }) => {
|
||||||
|
await listsPage.create("List to Update");
|
||||||
await listsPage.update();
|
await listsPage.update();
|
||||||
expect(listsPage.listItems.first()).toContainText("Updated List");
|
await expect(listsPage.listItems.first()).toContainText("Updated List");
|
||||||
expect(listsPage.listItems.first()).toContainText("New Description");
|
await expect(listsPage.listItems.first()).toContainText("New Description");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete a list card', async ({ listsPage }) => {
|
test('delete a list card', async ({ listsPage }) => {
|
||||||
|
await listsPage.create("List to Delete");
|
||||||
|
const countBefore = await listsPage.listItems.count();
|
||||||
await listsPage.delete();
|
await listsPage.delete();
|
||||||
expect(listsPage.listItems).toHaveCount(0);
|
await expect(listsPage.listItems).toHaveCount(countBefore - 1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
test('logs the user out', async ({ page }) => {
|
test('logs the user out', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
await page.getByRole('button', { name: 'avatar' }).click();
|
await page.getByLabel('Open user menu').click();
|
||||||
await page.locator(".menu .menu-item").filter({ hasText: "Logout" }).click();
|
await page.locator(".menu .menu-item").filter({ hasText: "Logout" }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/');
|
||||||
|
|
||||||
const cookies = await page.context().cookies();
|
const cookies = await page.context().cookies();
|
||||||
const pbAuthCookie = cookies.find(cookie => cookie.name === 'pb_auth');
|
const pbAuthCookie = cookies.find(cookie => cookie.name === 'pb_auth');
|
||||||
expect(pbAuthCookie).toBeFalsy();
|
expect(pbAuthCookie).toBeFalsy();
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ export class IndexPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async goto() {
|
async goto() {
|
||||||
await this.page.goto('/');
|
await this.page.goto('/', { waitUntil: 'networkidle' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async search() {
|
async search() {
|
||||||
|
// Start waiting for response before triggering the search
|
||||||
|
const responsePromise = this.page.waitForResponse(resp =>
|
||||||
|
resp.url().includes('/api/v1/search/multi') && resp.status() === 200
|
||||||
|
);
|
||||||
|
|
||||||
await this.page.locator('input[name="q"]').fill('Munich');
|
await this.page.locator('input[name="q"]').fill('Munich');
|
||||||
await this.page.waitForResponse('**/api/v1/search/multi');
|
await responsePromise;
|
||||||
|
|
||||||
await this.page.locator('.menu-item').first().click();
|
await this.page.locator('.menu-item').first().click();
|
||||||
await this.page.waitForURL('/map?lat=48.13743&lon=11.57549');
|
await this.page.waitForURL(/\/map\?lat=.*&lon=.*/);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ export class ListsPage {
|
|||||||
readonly listItems: Locator;
|
readonly listItems: Locator;
|
||||||
readonly listItemsImage: Locator;
|
readonly listItemsImage: Locator;
|
||||||
|
|
||||||
readonly listModal: Locator;
|
readonly listForm: Locator;
|
||||||
readonly listModalAvatar: Locator;
|
readonly listFormAvatar: Locator;
|
||||||
readonly listModalName: Locator;
|
readonly listFormName: Locator;
|
||||||
readonly listModalDescription: Locator;
|
readonly listFormDescription: Locator;
|
||||||
readonly listModalSaveButton: Locator;
|
readonly listFormSaveButton: Locator;
|
||||||
|
|
||||||
|
readonly dropdownButton: Locator;
|
||||||
|
|
||||||
readonly confirmModal: Locator;
|
readonly confirmModal: Locator;
|
||||||
readonly confirmModalConfirmButton: Locator;
|
readonly confirmModalConfirmButton: Locator;
|
||||||
@@ -20,17 +21,19 @@ export class ListsPage {
|
|||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.createListButton = page.locator("#create-list");
|
this.createListButton = page.getByLabel('New list');
|
||||||
this.listModal = page.locator("#list-modal");
|
this.listForm = page.locator("#list-form");
|
||||||
|
|
||||||
this.listModalAvatar = this.listModal.locator('input[name="avatar"]')
|
this.listFormAvatar = page.locator('#avatar')
|
||||||
this.listModalName = this.listModal.locator('input[name="name"]')
|
this.listFormName = page.locator('input[name="name"]')
|
||||||
this.listModalDescription = this.listModal.locator('textarea[name="description"]')
|
this.listFormDescription = page.locator('.ProseMirror')
|
||||||
this.listModalSaveButton = this.listModal.getByText('Save');
|
this.listFormSaveButton = this.listForm.locator('button[type="submit"]');
|
||||||
|
|
||||||
this.listItems = page.locator('.list-list-item');
|
this.listItems = page.locator('.list-list-item');
|
||||||
this.listItemsImage = page.locator('.list-list-item img');
|
// Only match the main avatar image (first img with aspect-square class in each list item)
|
||||||
|
this.listItemsImage = page.locator('.list-list-item img.aspect-square');
|
||||||
|
|
||||||
|
this.dropdownButton = page.getByLabel('Open dropdown');
|
||||||
|
|
||||||
this.confirmModal = page.locator("#confirm-modal");
|
this.confirmModal = page.locator("#confirm-modal");
|
||||||
this.confirmModalConfirmButton = this.confirmModal.locator("button").filter({ hasText: "Delete" });
|
this.confirmModalConfirmButton = this.confirmModal.locator("button").filter({ hasText: "Delete" });
|
||||||
@@ -40,33 +43,49 @@ export class ListsPage {
|
|||||||
await this.page.goto('/lists', { waitUntil: 'networkidle' });
|
await this.page.goto('/lists', { waitUntil: 'networkidle' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async selectDropdownAction(action: string) {
|
||||||
|
await this.dropdownButton.click();
|
||||||
|
await this.page.locator(".menu .menu-item").filter({ hasText: action }).click();
|
||||||
|
}
|
||||||
|
|
||||||
async create(name: string = "Test List") {
|
async create(name: string = "Test List") {
|
||||||
await this.createListButton.click();
|
await this.createListButton.click();
|
||||||
await this.listModalName.fill(name);
|
await this.listFormName.fill(name);
|
||||||
await this.listModalAvatar.setInputFiles([
|
await this.listFormAvatar.setInputFiles([
|
||||||
"./tests/playwright/fixtures/avatar.webp"
|
"./tests/playwright/fixtures/avatar.webp"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
|
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
|
||||||
this.listModalSaveButton.click()
|
this.listFormSaveButton.click()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Navigate back to lists page and wait for list items to load
|
||||||
|
await this.page.goto('/lists', { waitUntil: 'domcontentloaded' });
|
||||||
|
await this.listItems.first().waitFor({ state: 'visible', timeout: 10000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(name: string = "Updated List", description = "New Description") {
|
async update(name: string = "Updated List", description = "New Description") {
|
||||||
await this.listItems.first().locator(".dropdown button").click();
|
await this.listItems.first().click();
|
||||||
await this.listItems.first().locator(".menu .menu-item").filter({ hasText: "Edit" }).click();
|
await this.selectDropdownAction("Edit");
|
||||||
await this.listModalName.fill(name);
|
|
||||||
await this.listModalDescription.fill(description);
|
await this.listFormName.clear();
|
||||||
|
await this.listFormName.fill(name);
|
||||||
|
await this.listFormDescription.fill(description);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
|
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
|
||||||
this.listModalSaveButton.click()
|
this.listFormSaveButton.click()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Navigate back to lists page
|
||||||
|
await this.page.goto('/lists', { waitUntil: 'domcontentloaded' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
await this.listItems.first().locator(".dropdown button").click();
|
await this.listItems.first().click();
|
||||||
await this.listItems.first().locator(".menu .menu-item").filter({ hasText: "Delete" }).click();
|
await this.selectDropdownAction("Delete");
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
|
this.page.waitForResponse(resp => resp.url().includes('/api/v1/list') && resp.status() === 200),
|
||||||
this.confirmModalConfirmButton.click()
|
this.confirmModalConfirmButton.click()
|
||||||
@@ -74,8 +93,14 @@ export class ListsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeAll() {
|
async removeAll() {
|
||||||
while ((await this.listItems.count()) > 0) {
|
await this.goto();
|
||||||
|
|
||||||
|
let count = await this.listItems.count();
|
||||||
|
while (count > 0) {
|
||||||
await this.delete();
|
await this.delete();
|
||||||
|
// Navigate back to lists page after deletion
|
||||||
|
await this.goto();
|
||||||
|
count = await this.listItems.count();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user