mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
✅(e2e) add first tests
Add first test to test import and send message Resolve #238
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAKNWlDQ1BzUkdCIElFQzYxOTY2LTIu
|
||||
MQAASMedlndUVNcWh8+9d3qhzTACUobeu8AA0nuTXkVhmBlgKAMOMzSxIaICEUVEmiJIUMSA0VAk
|
||||
VkSxEBRUsAckCCgxGEVULG9G1ouurLz38vL746xv7bP3ufvsvc9aFwCSpy+XlwZLAZDKE/CDPJzp
|
||||
EZFRdOwAgAEeYIApAExWRrpfsHsIEMnLzYWeIXICXwQB8HpYvAJw09AzgE4H/5+kWel8geiYABGb
|
||||
szkZLBEXiDglS5Auts+KmBqXLGYYJWa+KEERy4k5YZENPvsssqOY2ak8tojFOaezU9li7hXxtkwh
|
||||
R8SIr4gLM7mcLBHfErFGijCVK+I34thUDjMDABRJbBdwWIkiNhExiR8S5CLi5QDgSAlfcdxXLOBk
|
||||
C8SXcklLz+FzExIFdB2WLt3U2ppB9+RkpXAEAsMAJiuZyWfTXdJS05m8HAAW7/xZMuLa0kVFtjS1
|
||||
trQ0NDMy/apQ/3Xzb0rc20V6Gfi5ZxCt/4vtr/zSGgBgzIlqs/OLLa4KgM4tAMjd+2LTOACApKhv
|
||||
Hde/ug9NPC+JAkG6jbFxVlaWEZfDMhIX9A/9T4e/oa++ZyQ+7o/y0F058UxhioAurhsrLSVNyKdn
|
||||
pDNZHLrhn4f4Hwf+dR4GQZx4Dp/DE0WEiaaMy0sQtZvH5gq4aTw6l/efmvgPw/6kxbkWidL4EVBj
|
||||
jIDUdSpAfu0HKAoRINH7xV3/o2+++DAgfnnhKpOLc//vN/1nwaXiJYOb8DnOJSiEzhLyMxf3xM8S
|
||||
oAEBSAIqkAfKQB3oAENgBqyALXAEbsAb+IMQEAlWAxZIBKmAD7JAHtgECkEx2An2gGpQBxpBM2gF
|
||||
x0EnOAXOg0vgGrgBboP7YBRMgGdgFrwGCxAEYSEyRIHkIRVIE9KHzCAGZA+5Qb5QEBQJxUIJEA8S
|
||||
QnnQZqgYKoOqoXqoGfoeOgmdh65Ag9BdaAyahn6H3sEITIKpsBKsBRvDDNgJ9oFD4FVwArwGzoUL
|
||||
4B1wJdwAH4U74PPwNfg2PAo/g+cQgBARGqKKGCIMxAXxR6KQeISPrEeKkAqkAWlFupE+5CYyiswg
|
||||
b1EYFAVFRxmibFGeqFAUC7UGtR5VgqpGHUZ1oHpRN1FjqFnURzQZrYjWR9ugvdAR6AR0FroQXYFu
|
||||
QrejL6JvoyfQrzEYDA2jjbHCeGIiMUmYtZgSzD5MG+YcZhAzjpnDYrHyWH2sHdYfy8QKsIXYKuxR
|
||||
7FnsEHYC+wZHxKngzHDuuCgcD5ePq8AdwZ3BDeEmcQt4Kbwm3gbvj2fjc/Cl+EZ8N/46fgK/QJAm
|
||||
aBPsCCGEJMImQiWhlXCR8IDwkkgkqhGtiYFELnEjsZJ4jHiZOEZ8S5Ih6ZFcSNEkIWkH6RDpHOku
|
||||
6SWZTNYiO5KjyALyDnIz+QL5EfmNBEXCSMJLgi2xQaJGokNiSOK5JF5SU9JJcrVkrmSF5AnJ65Iz
|
||||
UngpLSkXKabUeqkaqZNSI1Jz0hRpU2l/6VTpEukj0lekp2SwMloybjJsmQKZgzIXZMYpCEWd4kJh
|
||||
UTZTGikXKRNUDFWb6kVNohZTv6MOUGdlZWSXyYbJZsvWyJ6WHaUhNC2aFy2FVko7ThumvVuitMRp
|
||||
CWfJ9iWtS4aWzMstlXOU48gVybXJ3ZZ7J0+Xd5NPlt8l3yn/UAGloKcQqJClsF/hosLMUupS26Ws
|
||||
pUVLjy+9pwgr6ikGKa5VPKjYrzinpKzkoZSuVKV0QWlGmabsqJykXK58RnlahaJir8JVKVc5q/KU
|
||||
Lkt3oqfQK+m99FlVRVVPVaFqveqA6oKatlqoWr5am9pDdYI6Qz1evVy9R31WQ0XDTyNPo0XjniZe
|
||||
k6GZqLlXs09zXktbK1xrq1an1pS2nLaXdq52i/YDHbKOg84anQadW7oYXYZusu4+3Rt6sJ6FXqJe
|
||||
jd51fVjfUp+rv09/0ABtYG3AM2gwGDEkGToZZhq2GI4Z0Yx8jfKNOo2eG2sYRxnvMu4z/mhiYZJi
|
||||
0mhy31TG1Ns037Tb9HczPTOWWY3ZLXOyubv5BvMu8xfL9Jdxlu1fdseCYuFnsdWix+KDpZUl37LV
|
||||
ctpKwyrWqtZqhEFlBDBKGJet0dbO1husT1m/tbG0Edgct/nN1tA22faI7dRy7eWc5Y3Lx+3U7Jh2
|
||||
9Xaj9nT7WPsD9qMOqg5MhwaHx47qjmzHJsdJJ12nJKejTs+dTZz5zu3O8y42Lutczrkirh6uRa4D
|
||||
bjJuoW7Vbo/c1dwT3FvcZz0sPNZ6nPNEe/p47vIc8VLyYnk1e816W3mv8+71IfkE+1T7PPbV8+X7
|
||||
dvvBft5+u/0erNBcwVvR6Q/8vfx3+z8M0A5YE/BjICYwILAm8EmQaVBeUF8wJTgm+Ejw6xDnkNKQ
|
||||
+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--
|
||||
@@ -0,0 +1 @@
|
||||
I’m a dummy text 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 });
|
||||
};
|
||||
@@ -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
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user