(e2e) add first tests

Add first test to test import and send message

Resolve #238
This commit is contained in:
jbpenrath
2025-11-25 23:28:21 +01:00
parent 369bf83e05
commit a87d8f4a46
16 changed files with 531 additions and 12 deletions
+13
View File
@@ -56,6 +56,19 @@ jobs:
- name: Run frontend tests
run: make front-test
test-e2e:
runs-on: ubuntu-latest
env:
DOCKER_USER: 1001
PW_TEST_HTML_REPORT_OPEN: 'never'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create env files
run: make create-env-files
- name: Run E2E tests only for Chromium browser to speed up the tests
run: make e2e-test-ci args="--project chromium"
lint-front:
runs-on: ubuntu-latest
steps:
+2 -3
View File
@@ -289,11 +289,9 @@ e2e-test-dev: ## Setup, run and teardown e2e tests in UI mode with dev frontend
e2e-test-ci: ## Setup and run e2e tests in CI mode
@$(MAKE) e2e-setup
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(MAKE) e2e-run-test -- $${args:-${1}}
@$(MAKE) e2e-run-test args="$(args)"
.PHONY: e2e-test-ci
e2e-build: ## Build the e2e services
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
$(COMPOSE_E2E) build --no-cache $${args:-${1}}
@@ -565,3 +563,4 @@ mta-in-poetry-lock: ## lock the dependencies
mta-out-poetry-lock: ## lock the dependencies
@$(COMPOSE) run --rm --build mta-out-poetry poetry lock
.PHONY: mta-out-poetry-lock
+2
View File
@@ -158,6 +158,8 @@ services:
- BACKEND_BASE_URL=http://nginx
- KEYCLOAK_BASE_URL=http://keycloak:8802
- DOCKER_HOST=tcp://docker-sock-proxy:2375
- CI=${CI:-false}
- PW_TEST_HTML_REPORT_OPEN=${PW_TEST_HTML_REPORT_OPEN:-false}
volumes:
- ./src:/app/src
- ./playwright.config.ts:/app/playwright.config.ts
+11 -3
View File
@@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test';
import { getStorageStatePathIfExists } from './src/utils';
/**
* See https://playwright.dev/docs/test-configuration.
@@ -41,15 +42,22 @@ export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
...devices['Desktop Chrome'],
storageState: getStorageStatePathIfExists('user.e2e.chromium'),
},
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
use: { ...devices['Desktop Firefox'],
storageState: getStorageStatePathIfExists('user.e2e.firefox'),
},
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
use: { ...devices['Desktop Safari'],
storageState: getStorageStatePathIfExists('user.e2e.webkit'),
},
},
],
+21
View File
@@ -0,0 +1,21 @@
import { test, expect } from '@playwright/test';
import { signInKeycloakIfNeeded } from '../utils-test';
import { getStorageStatePath } from '../utils';
test.describe('Authentication with empty storage state', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('should authenticate', async ({ page, browserName }) => {
const username = `user.e2e.${browserName}`;
await page.goto('/');
await signInKeycloakIfNeeded({ page, username });
});
});
test.describe('Authentication with existing storage state', () => {
test('should authenticate', async ({ page, browserName }) => {
const username = `user.e2e.${browserName}`;
await page.goto('/');
await signInKeycloakIfNeeded({ page, username });
});
});
@@ -0,0 +1,130 @@
import { test, expect } from "@playwright/test";
import { FIXTURES_PATH } from "../constants";
import path from "path";
import { getMailboxEmail, resetDatabase } from "../utils";
import { signInKeycloakIfNeeded } from "../utils-test";
test.describe("Import Message", () => {
test.beforeAll(async () => {
await resetDatabase();
});
test.beforeEach(async ({ page, browserName }) => {
await page.goto("/");
await signInKeycloakIfNeeded({ page, username: `user.e2e.${browserName}` });
});
test.afterEach(async () => {
await resetDatabase();
});
test("should import an eml archive file", async ({ page, browserName }) => {
const email = `user.e2e.${browserName}@example.local`;
await page.waitForLoadState("networkidle");
// As the database is fresh, there should be no threads and the Import messages button should be visible
const noThreads = page.getByText("No threads.");
await expect(page.getByRole("link", { name: "Import messages" })).toBeVisible();
const header = page.locator(".c__header");
const settingsButton = header.getByRole("button", { name: "More options" });
await settingsButton.click();
const menuItem = page.getByRole("menuitem", { name: "Import messages" });
await menuItem.click();
const importModal = page.getByRole("dialog");
const modalTitle = importModal.locator(".c__modal__title");
expect(await modalTitle.textContent()).toBe(
`Import your old messages in ${email}`
);
const fileInput = page.locator('input[type="file"][name="archive_file"]');
// Import a wrong file type should show an error
const importButton = page.getByRole("button", { name: "Import" });
await fileInput.setInputFiles(path.join(FIXTURES_PATH, "attachment.png"));
await importButton.click();
const errorBanner = page.getByRole("alert", {
name: "An error occurred while uploading the archive file.",
});
await errorBanner.waitFor({ state: "visible" });
await fileInput.setInputFiles(path.join(FIXTURES_PATH, "old-message.eml"));
await importButton.click();
await expect(errorBanner).not.toBeVisible();
expect(
page.getByRole("heading", { name: "Uploading your archive" })
).toBeVisible();
expect(importButton).toBeDisabled();
expect(await importButton.getAttribute("aria-busy")).toBe("true");
const uploadCompleteResponse = await page.waitForResponse((response) => {
return (
response.url().includes("/api/v1.0/import/file/") &&
response.status() === 202
);
});
const uploadCompleteData = await uploadCompleteResponse.json();
const taskId = uploadCompleteData.task_id;
expect(page.getByText("Importing...")).toBeVisible();
await page.waitForResponse(async (response) => {
if (response.url().includes(`/api/v1.0/tasks/${taskId}/`)) {
const taskData = await response.json();
return taskData.status === "SUCCESS";
}
return false;
});
await expect(
page.getByText("Your messages have been imported successfully!")
).toBeVisible();
const closeButton = page.getByRole("button", {
name: "Close",
exact: true,
});
await closeButton.click();
await importModal.waitFor({ state: "hidden" });
// Then expect the new message to be visible in the thread list
await expect(
page.getByRole("link", { name: "Sardine An old message" })
).toBeVisible();
});
test("should not be able to import message if not mailbox admin", async ({
page,
browserName,
}) => {
const email = `user.e2e.${browserName}@example.local`;
await page.waitForLoadState("networkidle");
// Go to the shared mailbox and check if the user has sender rights
await page.getByRole("button", { name: email }).click();
await page
.getByRole("menuitem", { name: getMailboxEmail("shared") })
.click();
await page.waitForLoadState("networkidle");
// As the database is fresh, there should be no threads but the Import messages button should not be visible
// as the user does not have admin rights to the shared mailbox
await expect(page.getByText("No threads.")).toBeVisible();
await expect(
page.getByRole("link", { name: "Import messages" })
).not.toBeVisible();
// And also in the header settings menu, there should be no entry to import messages
const header = page.locator(".c__header");
const settingsButton = header.getByRole("button", { name: "More options" });
await settingsButton.click();
await expect(
page.getByRole("menuitem", { name: "Import messages" })
).not.toBeVisible();
});
});
@@ -0,0 +1,86 @@
import test, { expect } from "@playwright/test";
import { getMailboxEmail, resetDatabase } from "../utils";
import { signInKeycloakIfNeeded } from "../utils-test";
import path from "path";
import { FIXTURES_PATH } from "../constants";
test.describe("Send Message", () => {
test.beforeEach(async ({ page, browserName }) => {
await page.goto("/");
await signInKeycloakIfNeeded({ page, username: `user.e2e.${browserName}` });
});
test("should send a message then receive it", async ({ page, browserName }) => {
await page.waitForLoadState("networkidle");
const newMessageButton = page.getByRole("link", { name: "New message" });
await newMessageButton.click();
await page.waitForURL("/mailbox/*/new");
const draftBoxLink = page.getByRole("link", { name: "Drafts" });
let initialDraftCount = "0";
if (await draftBoxLink.locator("span.mailbox__item-counter").isVisible()) {
initialDraftCount = (await draftBoxLink.locator("span.mailbox__item-counter").textContent()) ?? "0";
}
const formHeading = page.getByRole("heading", { name: "New message" });
await formHeading.waitFor({ state: "visible" });
await page.getByRole("combobox", { name: "To" }).fill(getMailboxEmail('shared'));
await page.getByRole("textbox", { name: "Subject" }).fill("Hello everyone!");
await page.locator(".ProseMirror").pressSequentially("# E2E testing\n\nThis is a test message");
const fileChooserPromise = page.waitForEvent("filechooser");
await page.getByRole("button", { name: "Add attachments" }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(FIXTURES_PATH, "attachment.png"));
await page.getByText("Draft saved").waitFor({ state: "visible" });
// The number of drafts should be incremented by 1
await draftBoxLink.locator("span.mailbox__item-counter", { hasText: (parseInt(initialDraftCount!) + 1).toString() }).waitFor({ state: "visible" });
await page.getByRole("button", { name: "Send" }).click();
await page.getByText("Sending message...").waitFor({ state: "visible" });
await page.getByText("Message sent successfully").waitFor({ state: "visible" });
// Once message is sent, the number of drafts should be decremented by 1
if (initialDraftCount !== "0") {
await draftBoxLink.locator("span.mailbox__item-counter", { hasText: initialDraftCount! }).waitFor({ state: "visible" });
} else {
await draftBoxLink.locator("span.mailbox__item-counter").waitFor({ state: "hidden" });
}
// Go to the sentbox and check if the message is there
await page.getByRole("link", { name: "Sent" }).click();
const threadItem = page.getByRole("link", { name: "Hello everyone!" }).first();
await expect(threadItem).toBeVisible();
expect(await threadItem.textContent()).toMatch(new RegExp(`User E2E ${browserName}`, "i"));
// Go the shared mailbox and check if the message is there
await page.getByRole("button", { name: getMailboxEmail('user', browserName) }).click();
await page.getByRole("menuitem", { name: getMailboxEmail('shared') }).click();
await page.waitForLoadState("networkidle");
await page.getByRole("link", { name: "Inbox" }).click();
const messageItem = page.getByRole("link", { name: "Hello everyone!" }).first();
await expect(messageItem).toBeVisible();
expect(await messageItem.textContent()).toMatch(new RegExp(`User E2E ${browserName}`, "i"));
// Open the message and check its content
await messageItem.click();
await page.getByRole("heading", { name: "Hello everyone!", level: 2 }).waitFor({ state: "visible" });
const threadList = page.locator('.thread-view__messages-list');
const senderItem = threadList.getByRole("button", { name: getMailboxEmail('user', browserName) });
await expect(senderItem).toBeVisible();
const recipientItem = threadList.getByRole("button", { name: getMailboxEmail('shared') });
await expect(recipientItem).toBeVisible();
const iframeContent = page.locator('iframe').contentFrame();
await iframeContent.getByRole('heading', { name: 'E2E testing' }).waitFor({ state: "visible" });
const messageBodyText = iframeContent.getByText("This is a test message");
await expect(messageBodyText).toBeVisible();
await expect(page.getByText("1 attachment")).toBeVisible();
await expect(page.getByText("attachment.png")).toBeVisible();
});
});
+11
View File
@@ -0,0 +1,11 @@
import path from 'path';
if (!process.env.FRONTEND_BASE_URL || !process.env.BACKEND_BASE_URL || !process.env.KEYCLOAK_BASE_URL) {
throw new Error('FRONTEND_BASE_URL, BACKEND_BASE_URL and KEYCLOAK_BASE_URL must be set');
}
export const CLIENT_URL = process.env.FRONTEND_BASE_URL;
export const API_URL = process.env.BACKEND_BASE_URL;
export const AUTHENTICATION_URL = process.env.KEYCLOAK_BASE_URL;
export const STORAGE_STATE_PATH = path.join(__dirname, `./__tests__/.auth`);
export const FIXTURES_PATH = path.join(__dirname, `./fixtures`);
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

+152
View File
@@ -0,0 +1,152 @@
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=example.local;
i=@example.local; q=dns/txt; s=stmessages; t=1763499505; h=to : cc :
from : subject : message-id : reply-to : in-reply-to : references :
date; bh=/gryh6mpJfhXsnFl3xd1wGBWx4pWqs7Wk83EDFk2j4A=;
b=K9tuR3AuMIvZ2BVcJB9OAaVA/7nxJm7vdbwGk12MLl/OPahEZFRGGspuz0phgKdfyC8ZY
vqC2cjMhnSkFikmJc++8iD3DGM/ECnsLLYIKCKQnuAMFjlpRw+n8F35fxE5d9rIumZShYyV
+dcV05YZ4ZGc1O6U42Z6FxKoyZNmFLVRIPbEEMV2PxXcX3wyPPsIVMAu3mdhPOZO52cBOUM
K3cuMPPT/zOlYua2c2zRHpd6RL614oEm7rzPg4/ThD2qIUxuGOPv6QWzemWPrM+6BPPAJMx
ecFz41BS2mzroPPg044VM/ZPrRiek/OEa/v3udQfNOaZMsJejFoMC/QTqqOQ==
Message-Id: <ibaPxwjPQb-U-Gi4AUGd0A@_lst.example.local>
Date: Tue, 18 Nov 2025 20:58:25 +0000
To: "e2e.old.account" <e2e.old.account@example.local>
From: Sardine <sardine@example.local>
Subject: An old message
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="0891b8b8a4a646c8906e23db65c02896"
--0891b8b8a4a646c8906e23db65c02896
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="c9502976030147e9bfdf42924dd8b244"
--c9502976030147e9bfdf42924dd8b244
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="ascii"
Mime-Version: 1.0
A good old message.
--c9502976030147e9bfdf42924dd8b244
Content-Transfer-Encoding: 7bit
Content-Type: text/html; charset="ascii"
Mime-Version: 1.0
<p>A good old message.</p>
--c9502976030147e9bfdf42924dd8b244--
--0891b8b8a4a646c8906e23db65c02896
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="sardine.png"
Content-Type: image/png; name="sardine.png"
Mime-Version: 1.0
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
+6E6ocLQnjDJsOiw5rD5cNfwsvDRCOOIdRHXIhUiuZFdUdiosKimqLmVbiv3rJyItogujB5epb0q
e9WV1QqrU1afjpGMYcaciEXHhsceiX3P9Gc2MOfivOJq42ZZLqy9rGdsR3Y5e5pjxynjTMbbxZfF
TyXYJexOmE50SKxInOG6cKu5L5I8k+qS5pP9kw8lf0oJT2lLxaXGpp7kyfCSeb1pymnZaYPp+umF
6aNrbNbsWTPL9+E3ZUAZqzK6BFTRz1S/UEe4RTiWaZ9Zk/kmKyzrRLZ0Ni+7P0cvZ3vOZK577rdr
UWtZa3vyVPM25Y2tc1pXvx5aH7e+Z4P6hoINExs9Nh7eRNiUvOmnfJP8svxXm8M3dxcoFWwsGN/i
saWlUKKQXziy1XZr3TbUNu62ge3m26u2fyxiF10tNimuKH5fwiq5+o3pN5XffNoRv2Og1LJ0/07M
Tt7O4V0Ouw6XSZfllo3v9tvdUU4vLyp/tSdmz5WKZRV1ewl7hXtHK30ru6o0qnZWva9OrL5d41zT
VqtYu712fh9739B+x/2tdUp1xXXvDnAP3Kn3qO9o0GqoOIg5mHnwSWNYY9+3jG+bmxSaips+HOId
Gj0cdLi32aq5+YjikdIWuEXYMn00+uiN71y/62o1bK1vo7UVHwPHhMeefh/7/fBxn+M9JxgnWn/Q
/KG2ndJe1AF15HTMdiZ2jnZFdg2e9D7Z023b3f6j0Y+HTqmeqjkte7r0DOFMwZlPZ3PPzp1LPzdz
PuH8eE9Mz/0LERdu9Qb2Dlz0uXj5kvulC31OfWcv210+dcXmysmrjKud1yyvdfRb9Lf/ZPFT+4Dl
QMd1q+tdN6xvdA8uHzwz5DB0/qbrzUu3vG5du73i9uBw6PCdkeiR0TvsO1N3U+6+uJd5b+H+xgfo
B0UPpR5WPFJ81PCz7s9to5ajp8dcx/ofBz++P84af/ZLxi/vJwqekJ9UTKpMNk+ZTZ2adp++8XTl
04ln6c8WZgp/lf619rnO8x9+c/ytfzZiduIF/8Wn30teyr889GrZq565gLlHr1NfL8wXvZF/c/gt
423fu/B3kwtZ77HvKz/ofuj+6PPxwafUT5/+BQOY8/ximktCAAAACXBIWXMAAAsTAAALEwEAmpwY
AAAHfmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJX
NU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczpt
ZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDMgNzkuOTY5MGE4N2ZjLCAyMDI1
LzAzLzA2LTIwOjUwOjE2ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cu
dzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjph
Ym91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRj
PSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRw
Oi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFk
b2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFw
LzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3No
b3AgMjYuMTEgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDI1LTExLTE4VDIxOjU2OjI0
KzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyNS0xMS0xOFQyMTo1Nzo0MSswMTowMCIgeG1wOk1l
dGFkYXRhRGF0ZT0iMjAyNS0xMS0xOFQyMTo1Nzo0MSswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9w
bmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElF
QzYxOTY2LTIuMSIgcGhvdG9zaG9wOkhpc3Rvcnk9IjIwMjUtMTEtMThUMjE6NTY6MjQrMDE6MDAm
I3g5O0ZpY2hpZXIgU2FucyB0aXRyZS0xIG91dmVydCYjeEE7MjAyNS0xMS0xOFQyMTo1Nzo0MSsw
MTowMCYjeDk7RmljaGllciBzYXJkaW5lLnBuZyBlbnJlZ2lzdHLDqSYjeEE7IiB4bXBNTTpJbnN0
YW5jZUlEPSJ4bXAuaWlkOmQ5ZWQ0NjhjLTU5YWQtNDk0Yi05OTU4LWEzZmFkODdiMmQwYiIgeG1w
TU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjIzM2MxN2M4LTJlZDMtMGE0MS1h
YzU3LTBmZDg2NWExYjI3ZSIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIyODA0
YmFkLWYyMjAtNDc3Yi05MmEzLWFmOTA2NWZkNzRlNyI+IDxwaG90b3Nob3A6VGV4dExheWVycz4g
PHJkZjpCYWc+IDxyZGY6bGkgcGhvdG9zaG9wOkxheWVyTmFtZT0i8J+QnyIgcGhvdG9zaG9wOkxh
eWVyVGV4dD0i8J+QnyIvPiA8L3JkZjpCYWc+IDwvcGhvdG9zaG9wOlRleHRMYXllcnM+IDx4bXBN
TTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0
Omluc3RhbmNlSUQ9InhtcC5paWQ6MjI4MDRiYWQtZjIyMC00NzdiLTkyYTMtYWY5MDY1ZmQ3NGU3
IiBzdEV2dDp3aGVuPSIyMDI1LTExLTE4VDIxOjU2OjI0KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFn
ZW50PSJBZG9iZSBQaG90b3Nob3AgMjYuMTEgKE1hY2ludG9zaCkiLz4gPHJkZjpsaSBzdEV2dDph
Y3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBhcHBsaWNhdGlvbi92bmQu
YWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2
ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZDllZDQ2OGMtNTlhZC00OTRiLTk5NTgtYTNm
YWQ4N2IyZDBiIiBzdEV2dDp3aGVuPSIyMDI1LTExLTE4VDIxOjU3OjQxKzAxOjAwIiBzdEV2dDpz
b2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjYuMTEgKE1hY2ludG9zaCkiIHN0RXZ0OmNo
YW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9u
PiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+qxaAlgAABadJREFU
SMftlktsE0cYx2f25X3Y69hOYuM4aRwCSQgJoUSEoJQ+AAGqVCGhUqFWQkXc2gPi1kuF1J5a9VBu
PUClClAFqKiipQgKiEDFI0EF0hAngNM4tmM7fj/W3t3ZnY5DFeDAo2mpVKmz0kqWR/P75vv+3/df
iDEGL3JR4AWv/z6AeeaOiqqZJrZwLE1TLwCA8ZkLQ7FEprvT/5LP46yRLRb2nwNgMxhKHDx6IRDJ
dzSPdfjdPR2N7a0+X4ObkJ4TAJ8iU4SMj788/uPNmORwAorSFcUo5WxY2djbMrB6WdeyxS6HDCBc
KAAbZ64Evjh6TePtktU6t8tEmlYu5EqplJFLbHut/c31fd2dSxiG/osAbEZimbNXJ46cn8ghhmF5
ACmMITBNiHULY1oEFhlaZiYKc/EdW3rf276pvrbmeQG6rg8O3/vhenw0pXN2pyCKSEeQtTAMZ5gY
qZqmFJCSY3CZBno6Op2enHh3y6q9H+6QBP7ZRS6X1VODI0MxaGnpeLXXygkcY6FS8WQmU5SdLo7n
IaB1hJRSJZfKKJkkK4gUxN+eGupb1b7x9f75cug6mo4mmhs9FEUxj8SOLly5o0Bx51utkOXCRVCA
QMPAYXON3kqphVmrWC+JPMtw0M3pTdZCvn42nhJsVgpQh46fXdLS5G9umBM2PnD4pGDhmrZvpuY7
mbTSeDASjBc2vdK2WIZ3L5/6+qPdgcGzFoDsMuNyyvGZaDKeLBYKhl5hoSGLlNcjtHd4l/Z0+lf3
BXPU/ckw0cVsMvvZ/sP7D5yUbRI1d6M/b1BSypeuT/SsXFYn0JFI7NA3h04cO6ZbbG29vRTrcDpd
anEkEp6BAFPYoKAIMMcwUOAon9dOU0sNJT+dQ0dOXPz+9JWbgbBp0o4aO5gHkPCnwrMagMvbFpGf
ssMxsG7dbCy6eeMGnhGzBUMUeJ6j48mkLEuSyFg4miIosxoiSXKd05pu8v40OjYTnMmkWSA4WTUj
ihYIHt4Ah8Lx8WDi9kR0Ratb5IX3d+16e/s7NMffSqiVssoIkijyWiynqSpCumEg06ANQCgAAURk
KMt8QuJtsgUZVA5VaASZ6uAi+oRVAITw1bUr3PWum6Oh26OR7g5vh9/F84Kult2cMVPEaoWGFGBp
SNMYm4aJCAUaiJxvYqJdZNJYrZdwthhDJbIDcCzLMMxDmRKAwHOiwNkE2OiwjwaihVK5v9OtarqT
w80SniiVSexWWapmigjTNHQdg2qWsGkaPEY+WDKVlKboFUB6vshAyLL0Y35gmmYikRq88VuqmF/c
6HRJrKrqJBeajlrtjJ9VWIBq3S7JKjAsY5KT547mgNHAoRammAveGx8LRVMqUhXSjTr508SPNRqZ
Jyu7WodHApFoxO3iZVuNUtFIDefaHFu1XH+zk66pVwFFSksejjatlGkDqp7N3AlMmQhs3TLQMnp/
8Pp4pFxWoFFR9Qcqovft2/eAYeG4RW7X5Wsj45OxdLGiY4omFgOhqqNsodLsdbW4+AYJLhKBmzOd
UAWFbPB+6NylkalQbM/urQN9XevWdPk8Nfn0bHA68Ub/8pZmL0n+I6MCAn+jZ+e2Dd+d/mXoRiDw
e7LB4yISlCXSrSIsG4VyERs6SV02X5pJZKem48V8KZdMG2p5LlbMsuzm9WvIuc6DJ9LZ3BOnaUmp
jN0NXf01cG4ooJq0IPBWK1EpxzGUopSLxVKlVMmnM92tnr0f7KCAGU+kfT5PXa1j3hmKpTLZWVfr
JOAn+AHGJMzJUDQUnc1kSaCKqiGSL5qifr44HAon1VJxbY//4FefkElOSl59L8DRqqZmGFU56fqc
KjCZjqFw7Pzg8MXLN9KpzOef7lnd2/W3TJ+haUYkin446+12a4O3/uUVbePjwUpFe9CuC/Tkpyzi
CpqmkwsJvGXhpv//p+O/A/gDpTvzFFZ1BKMAAAAASUVORK5CYII=
--0891b8b8a4a646c8906e23db65c02896--
+1
View File
@@ -0,0 +1 @@
Im a dummy text file.
+39
View File
@@ -0,0 +1,39 @@
import { expect, Page } from "@playwright/test";
import { AUTHENTICATION_URL } from "./constants";
import { getStorageStatePath } from "./utils";
export const signInKeycloakIfNeeded = async ({ page, username }: { page: Page, username: string }) => {
// Await that GET request /api/v1.0/users/me is completed.
const meResponse = await page.waitForResponse((response) => response.url().includes('/api/v1.0/users/me/') && [200, 401].includes(response.status()));
const isAuthenticated = meResponse.status() === 200;
if (isAuthenticated) return;
const email = `${username}@example.local`;
const storageStatePath = getStorageStatePath(username);
const proConnectButton = page.locator('button.pro-connect-button');
proConnectButton.click();
await page.waitForURL(`${AUTHENTICATION_URL}/realms/messages/protocol/openid-connect/auth**`);
const attemptedUsernameInput = page.locator('input[id="kc-attempted-username"]');
if (await attemptedUsernameInput.isVisible()) {
if (await attemptedUsernameInput.inputValue() !== email) {
const restartLoginButton = page.getByRole('button', { name: 'Restart login' });
await restartLoginButton.click();
await page.fill('input[name="username"]', email);
}
} else {
await page.fill('input[name="username"]', email);
}
await page.fill('input[name="password"]', 'e2e');
await page.click('button[type="submit"]');
await page.waitForURL(`/`, { waitUntil: 'networkidle' });
expect(proConnectButton).not.toBeVisible();
const mailboxName = await page.getByRole('button', { name: email });
expect(mailboxName).toBeVisible();
await page.context().storageState({ path: storageStatePath });
};
+55
View File
@@ -0,0 +1,55 @@
/**
* Utility functions for E2E tests.
*/
import { exec } from 'child_process';
import { STORAGE_STATE_PATH } from './constants';
import path from 'path';
import fs from 'fs';
/**
* Execute a npm command in the runner container.
*/
async function runNpmCommand(command: string, args: string[] = [], timeout: number = 1000): Promise<string> {
const commandArgs = [command, ...args].join(' ');
const fullCommand = `npm run ${commandArgs}`;
if (timeout) {
await new Promise((resolve) => { setTimeout(resolve, timeout) });
}
return new Promise((resolve, reject) => {
exec(fullCommand, (error, stdout) => {
if (error) reject(error);
else resolve(stdout);
});
});
}
/**
* Reset the database by flushing all data (keeps schema) then
* bootstrapping the demo data.
*/
export async function resetDatabase(): Promise<void> {
await runNpmCommand('db:reset');
}
export const getStorageStatePath = (username: string): string => {
return path.join(STORAGE_STATE_PATH, `user-${username}.json`);
};
/**
* Helper function to get storage state path if it exists, otherwise undefined.
* This allows setup projects to reuse existing authentication if available.
*/
export const getStorageStatePathIfExists = (username: string): string | undefined => {
const path = getStorageStatePath(username);
return fs.existsSync(path) ? path : undefined;
};
export const getMailboxEmail = (username: 'user' | 'mailbox_admin' | 'domain_admin' | 'super_admin' | 'shared', browserName?: 'chromium' | 'firefox' | 'webkit'): string => {
if (browserName) return `${username}.e2e.${browserName}@example.local`;
return `${username}.e2e@example.local`;
};
@@ -358,7 +358,7 @@ export const MessageForm = ({
*/
const saveDraft = async () => {
const data = form.getValues();
if (!canWriteMessages) return;
if (!canWriteMessages || isSavingDraft) return;
const saveDraftNeeded = (
Object.keys(form.formState.dirtyFields).length > 0
@@ -4,6 +4,7 @@ import { Button } from "@openfun/cunningham-react";
import { useMailboxContext } from "@/features/providers/mailbox";
import { useLayoutContext } from "../../../main";
import useAbility, { Abilities } from "@/hooks/use-ability";
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
export const MailboxPanelActions = () => {
const { t } = useTranslation();
@@ -28,7 +29,7 @@ export const MailboxPanelActions = () => {
<Button
onClick={goToNewMessageForm}
href={`/mailbox/${selectedMailbox.id}/new`}
icon={<span className="material-icons">edit_note</span>}
icon={<Icon name="edit_note" type={IconType.OUTLINED} aria-hidden="true" />}
disabled={!canWriteMessages}
>
{t("New message")}
@@ -1,6 +1,6 @@
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { useId } from "react";
type BannerProps = {
children: React.ReactNode;
@@ -15,14 +15,15 @@ type BannerProps = {
* TODO: Migrate this component into our ui-kit
*/
export const Banner = ({ children, type = 'info', icon, compact = false, fullWidth = false }: BannerProps) => {
const { t } = useTranslation();
const ariaLabelId = useId();
return (
<div
className={clsx("banner", `banner--${type}`, { "banner--compact": compact, "banner--full-width": fullWidth })}
role="alert"
aria-live="polite"
aria-label={t(`aria.labels.banner.${type}`)}
data-testid="banner"
aria-labelledby={ariaLabelId}
>
<div className="banner__content">
<div
@@ -35,7 +36,7 @@ export const Banner = ({ children, type = 'info', icon, compact = false, fullWid
)
}
</div>
<div className="banner__content__text">
<div className="banner__content__text" id={ariaLabelId}>
{children}
</div>
</div>