Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/e2e/src/__tests__/attachment-preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test.describe("Attachment preview", () => {
await page.getByText("Message sent successfully").waitFor({ state: "visible" });

await page.getByRole("link", { name: "Sent" }).click();
await page.getByRole("link", { name: subject }).first().click();
await page.getByRole("option", { name: subject }).first().click();
await page.getByRole("heading", { name: subject, level: 2 }).waitFor({ state: "visible" });
}

Expand Down
7 changes: 5 additions & 2 deletions src/e2e/src/__tests__/mailbox-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ test.describe("Mailbox settings modal", () => {

// The switcher renders a single-select dropdown, so its entries expose a
// `menuitemradio`/`option` role depending on the design-system version; match
// both so the assertion does not hinge on that internal detail.
const options = page.locator('[role^="menuitem"], [role="option"]');
// both so the assertion does not hinge on that internal detail. Scope to the
// dropdown popover: it is portalled outside the modal, and the thread list is
// itself a listbox whose `option` entries would otherwise be matched too.
const switcherPopover = page.getByRole("menu");
const options = switcherPopover.locator('[role^="menuitem"], [role="option"]');
await expect(options).toHaveCount(2);
await expect(
options.filter({ hasText: `user.e2e.${browserName}@example.local` }),
Expand Down
2 changes: 1 addition & 1 deletion src/e2e/src/__tests__/message-import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ test.describe("Import Message", () => {

// Then expect the new message to be visible in the thread list
await expect(
page.getByRole("link", { name: "Sardine 18/11/2025 An old message" })
page.getByRole("option", { name: "Sardine 18/11/2025 An old message" })
).toBeVisible();
});

Expand Down
4 changes: 2 additions & 2 deletions src/e2e/src/__tests__/message-inline-image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ test.describe("Inline Image in Composer", () => {

// Verify the message appears in sentbox
await page.getByRole("link", { name: "Sent" }).click();
const sentItem = page.getByRole("link", { name: "Message with inline image" }).first();
const sentItem = page.getByRole("option", { name: "Message with inline image" }).first();
await expect(sentItem).toBeVisible();

// Open the message and check content
Expand All @@ -117,7 +117,7 @@ test.describe("Inline Image in Composer", () => {
await page.getByRole("link", { name: "Inbox" }).click();

// Open the message and check content
const receivedItem = await page.getByRole("link", { name: "Message with inline image" }).first();
const receivedItem = await page.getByRole("option", { name: "Message with inline image" }).first();
await expect(receivedItem).toBeVisible();
await receivedItem.click();
await page.getByRole("heading", { name: "Message with inline image", level: 2 }).waitFor({ state: "visible" });
Expand Down
4 changes: 2 additions & 2 deletions src/e2e/src/__tests__/message-send.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ test.describe("Send Message", () => {

// 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();
const threadItem = page.getByRole("option", { name: "Hello everyone!" }).first();
await expect(threadItem).toBeVisible();
expect(await threadItem.textContent()).toMatch(new RegExp(`User E2E ${browserName}`, "i"));

Expand All @@ -64,7 +64,7 @@ test.describe("Send Message", () => {

await page.getByRole("link", { name: "Inbox" }).click();

const messageItem = page.getByRole("link", { name: "Hello everyone!" }).first();
const messageItem = page.getByRole("option", { name: "Hello everyone!" }).first();
await expect(messageItem).toBeVisible();
expect(await messageItem.textContent()).toMatch(new RegExp(`User E2E ${browserName}`, "i"));

Expand Down
16 changes: 8 additions & 8 deletions src/e2e/src/__tests__/outbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test.describe("Delivery failures", () => {

// The thread with delivery issues should be visible in the list
const threadItem = page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first();
await expect(threadItem).toBeVisible();
});
Expand All @@ -39,7 +39,7 @@ test.describe("Delivery failures", () => {

// Click on the thread to open it
const threadItem = page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first();
await threadItem.click();

Expand Down Expand Up @@ -79,7 +79,7 @@ test.describe("Delivery failures", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first()
.click();

Expand Down Expand Up @@ -110,7 +110,7 @@ test.describe("Delivery failures", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first()
.click();

Expand Down Expand Up @@ -144,7 +144,7 @@ test.describe("Delivery failures", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first()
.click();

Expand Down Expand Up @@ -180,7 +180,7 @@ test.describe("Delivery failures", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first()
.click();

Expand Down Expand Up @@ -225,7 +225,7 @@ test.describe("Delivery pending (retry only)", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Test message with pending delivery" })
.getByRole("option", { name: "Test message with pending delivery" })
.first()
.click();

Expand Down Expand Up @@ -258,7 +258,7 @@ test.describe("Delivery pending (retry only)", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Test message with pending delivery" })
.getByRole("option", { name: "Test message with pending delivery" })
.first()
.click();

Expand Down
10 changes: 5 additions & 5 deletions src/e2e/src/__tests__/thread-event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function navigateToSharedThread(page: Page, browserName: BrowserName) {
await inboxFolderLink(page).click();
await page.waitForLoadState("networkidle");
await page
.getByRole("link", { name: "Shared inbox thread for IM" })
.getByRole("option", { name: "Shared inbox thread for IM" })
.first()
.click();
await page
Expand Down Expand Up @@ -93,7 +93,7 @@ test.describe("Thread Events (Internal Messages)", () => {
await page.waitForLoadState("networkidle");

await page
.getByRole("link", { name: "Inbox thread alpha" })
.getByRole("option", { name: "Inbox thread alpha" })
.first()
.click();
await page
Expand Down Expand Up @@ -489,7 +489,7 @@ test.describe("Thread Events (Internal Messages)", () => {
// "Unread mention" badge. The thread list is re-fetched on navigation,
// which makes it the most reliable indicator that the mention landed.
const threadLink = page
.getByRole("link", { name: "Shared inbox thread for IM" })
.getByRole("option", { name: "Shared inbox thread for IM" })
.first();
await expect(
threadLink.getByLabel("Unread mention").first(),
Expand Down Expand Up @@ -591,7 +591,7 @@ test.describe("Thread Events (Assignations)", () => {
await inboxFolderLink(page).click();
await page.waitForLoadState("networkidle");
await page
.getByRole("link", { name: "Inbox thread alpha" })
.getByRole("option", { name: "Inbox thread alpha" })
.first()
.click();
await page
Expand Down Expand Up @@ -778,7 +778,7 @@ test.describe("Thread Events (Assignations)", () => {
await page.waitForLoadState("networkidle");

await expect(
page.getByRole("link", { name: "Shared inbox thread for IM" }).first(),
page.getByRole("option", { name: "Shared inbox thread for IM" }).first(),
).toBeVisible();

// Cleanup: drop the assignment so the test is rerun-safe even when
Expand Down
28 changes: 14 additions & 14 deletions src/e2e/src/__tests__/thread-starred-read.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test.describe("Thread starred", () => {

// Open the first thread
await page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first()
.click();
await page
Expand Down Expand Up @@ -63,7 +63,7 @@ test.describe("Thread starred", () => {

// Open the thread (starred from previous test)
await page
.getByRole("link", { name: "Test message with delivery failure" })
.getByRole("option", { name: "Test message with delivery failure" })
.first()
.click();
await page
Expand Down Expand Up @@ -112,7 +112,7 @@ test.describe("Thread read / unread", () => {

// Open the thread (the IntersectionObserver auto-marks messages as read)
await page
.getByRole("link", { name: "Inbox thread alpha" })
.getByRole("option", { name: "Inbox thread alpha" })
.first()
.click();
await page
Expand Down Expand Up @@ -154,15 +154,15 @@ test.describe("Thread read / unread", () => {

// Both threads should be visible (both unread)
await expect(
page.getByRole("link", { name: "Inbox thread alpha" }).first(),
page.getByRole("option", { name: "Inbox thread alpha" }).first(),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Inbox thread beta" }).first(),
page.getByRole("option", { name: "Inbox thread beta" }).first(),
).toBeVisible();

// Open a thread — the IntersectionObserver auto-marks it as read
await page
.getByRole("link", { name: "Inbox thread alpha" })
.getByRole("option", { name: "Inbox thread alpha" })
.first()
.click();
await page
Expand All @@ -176,7 +176,7 @@ test.describe("Thread read / unread", () => {
// The thread should still be visible in the list thanks to thread pinning logic
// Check @/features/providers/mailbox-cache.ts
await expect(
page.getByRole("link", { name: "Inbox thread alpha" }).first(),
page.getByRole("option", { name: "Inbox thread alpha" }).first(),
).toBeVisible();
});

Expand All @@ -200,15 +200,15 @@ test.describe("Thread read / unread", () => {

// Verify both threads are visible initially
await expect(
page.getByRole("link", { name: "Inbox thread alpha" }).first(),
page.getByRole("option", { name: "Inbox thread alpha" }).first(),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Inbox thread beta" }).first(),
page.getByRole("option", { name: "Inbox thread beta" }).first(),
).toBeVisible();

// Open the first thread to mark it as read (IntersectionObserver auto-read)
await page
.getByRole("link", { name: "Inbox thread alpha" })
.getByRole("option", { name: "Inbox thread alpha" })
.first()
.click();
await page
Expand All @@ -232,12 +232,12 @@ test.describe("Thread read / unread", () => {

// The read thread should be filtered out
await expect(
page.getByRole("link", { name: "Inbox thread alpha" }),
page.getByRole("option", { name: "Inbox thread alpha" }),
).not.toBeVisible();

// The unread thread (not opened) should still be visible
await expect(
page.getByRole("link", { name: "Inbox thread beta" }).first(),
page.getByRole("option", { name: "Inbox thread beta" }).first(),
).toBeVisible();

// Click the filter button again to clear the filter
Expand All @@ -246,10 +246,10 @@ test.describe("Thread read / unread", () => {

// Both threads should be visible again
await expect(
page.getByRole("link", { name: "Inbox thread alpha" }).first(),
page.getByRole("option", { name: "Inbox thread alpha" }).first(),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Inbox thread beta" }).first(),
page.getByRole("option", { name: "Inbox thread beta" }).first(),
).toBeVisible();
});
});
3 changes: 1 addition & 2 deletions src/frontend/public/locales/common/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,6 @@
"Description": "Description",
"Description must be less than 255 characters.": "Description must be less than 255 characters.",
"Deselect all threads": "Deselect all threads",
"Deselect thread": "Deselect thread",
"Did you forget an attachment?": "Did you forget an attachment?",
"Disable thread selection": "Disable thread selection",
"Display those images": "Display those images",
Expand Down Expand Up @@ -654,7 +653,6 @@
"Select a thread": "Select a thread",
"Select all threads": "Select all threads",
"Select the mailbox to configure": "Select the mailbox to configure",
"Select thread": "Select thread",
"Select threads": "Select threads",
"Send": "Send",
"Send and archive": "Send and archive",
Expand Down Expand Up @@ -774,6 +772,7 @@
"This will move this message and all following messages to a new thread. Continue?": "This will move this message and all following messages to a new thread. Continue?",
"Thread access removed": "Thread access removed",
"Thread has been split successfully.": "Thread has been split successfully.",
"Thread list": "Thread list",
"Thursday": "Thursday",
"Timezone": "Timezone",
"To": "To",
Expand Down
3 changes: 1 addition & 2 deletions src/frontend/public/locales/common/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,6 @@
"Description": "Description",
"Description must be less than 255 characters.": "La description ne peut pas excéder 255 caractères.",
"Deselect all threads": "Désélectionner toutes les conversations",
"Deselect thread": "Désélectionner la conversation",
"Did you forget an attachment?": "N'avez-vous pas oublié une pièce jointe ?",
"Disable thread selection": "Désactiver la sélection",
"Display those images": "Afficher ces images",
Expand Down Expand Up @@ -735,7 +734,6 @@
"Select a thread": "Sélectionner une conversation",
"Select all threads": "Sélectionner toutes les conversations",
"Select the mailbox to configure": "Sélectionner la boîte aux lettres à configurer",
"Select thread": "Sélectionner une conversation",
"Select threads": "Sélectionner des conversations",
"Send": "Envoyer",
"Send and archive": "Envoyer et archiver",
Expand Down Expand Up @@ -859,6 +857,7 @@
"This will move this message and all following messages to a new thread. Continue?": "Cela déplacera ce message et tous les messages suivants dans une nouvelle conversation. Continuer ?",
"Thread access removed": "Accès à la conversation supprimé",
"Thread has been split successfully.": "La conversation a été séparée avec succès.",
"Thread list": "Liste des conversations",
"Thursday": "Jeudi",
"Timezone": "Fuseau horaire",
"To": "À",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
border-color: var(--c--contextuals--border--semantic--neutral--tertiary);
}

// Keyboard focus ring, drawn inward so the scroll container does not clip
// it. Outline does not conflict with the state backgrounds, so it stays
// visible on selected and active rows too.
.thread-item:focus-visible {
outline: 2px solid var(--c--contextuals--border--focus);
outline-offset: -2px;
}

.thread-item.thread-item--active {
background-color: var(--c--contextuals--background--semantic--brand--tertiary);
border-color: var(--c--contextuals--border--semantic--brand--tertiary);
Expand Down Expand Up @@ -78,6 +86,16 @@
flex: 1;
}

// This wrapper carries aria-hidden="true", so it must stay a real box in the
// layout. `display: contents` is handled inconsistently across browser/AT
// combinations and can drop the aria-hidden boundary, leaking the checkbox
// semantics into the parent role="option". inline-flex shrink-wraps the
// checkbox so the visual layout stays identical.
.thread-item__checkbox-wrapper {
display: inline-flex;
flex-shrink: 0;
}

.thread-item__checkbox {
flex-shrink: 0;
margin: 0;
Expand Down
Loading