Skip to content
Draft
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
10 changes: 8 additions & 2 deletions src/renderer/components/AnalysisPrioritySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export function AnalysisPrioritySection({

if (!isEditing) {
return (
<div className="px-6 py-4 border-t border-gray-100 dark:border-gray-700/50 bg-gray-50 dark:bg-gray-800/50">
<div
data-testid="analysis-priority-section"
className="flex-shrink-0 px-6 py-4 border-t border-gray-100 dark:border-gray-700/50 bg-gray-50 dark:bg-gray-800/50"
>
<div className="flex items-center gap-3 text-sm">
<span
className={`font-medium ${
Expand All @@ -87,7 +90,10 @@ export function AnalysisPrioritySection({
}

return (
<div className="px-6 py-4 border-t border-gray-100 dark:border-gray-700/50 bg-gray-50 dark:bg-gray-800/50">
<div
data-testid="analysis-priority-section"
className="flex-shrink-0 px-6 py-4 border-t border-gray-100 dark:border-gray-700/50 bg-gray-50 dark:bg-gray-800/50"
>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-500 dark:text-gray-400 text-xs font-medium">
Expand Down
18 changes: 13 additions & 5 deletions src/renderer/components/EmailDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ function ThreadMessage({
// White card for rich HTML emails in dark mode, dark card otherwise
return (
<div
className={`group/msg ${
className={`group/msg flex-1 ${
useWhiteCard
? `bg-white rounded-lg${isFocused ? " ring-1 ring-blue-300 dark:ring-blue-500" : ""}`
: `relative before:absolute before:left-[-6px] before:top-0 before:bottom-0 before:rounded-full bg-gray-50/50 dark:bg-gray-800/30 ${
Expand Down Expand Up @@ -3586,7 +3586,11 @@ function EmailDetailInner({ isFullView = false }: EmailDetailProps) {
)}

{/* Single scroll container for entire thread */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto">
<div
ref={scrollContainerRef}
data-testid="email-detail-scroll"
className="flex-1 min-h-0 overflow-y-auto flex flex-col"
>
{/* Thread header */}
<div className="px-6 pt-6 pb-4 border-b border-gray-100 dark:border-gray-700/50">
<div className="flex items-start justify-between">
Expand Down Expand Up @@ -3812,9 +3816,13 @@ function EmailDetailInner({ isFullView = false }: EmailDetailProps) {
)}

{/* Thread messages - single scroll, no nested scrolls */}
<div className="px-6">
{threadEmails.map((email) => (
<div key={email.id} data-email-id={email.id}>
<div data-testid="email-thread-messages" className="px-6 flex-1 flex flex-col">
{threadEmails.map((email, index) => (
<div
key={email.id}
data-email-id={email.id}
className={index === threadEmails.length - 1 ? "flex-1 flex flex-col" : undefined}
>
<ThreadMessage
email={email}
isExpanded={expandedMessagesRef.current.has(email.id)}
Expand Down
95 changes: 95 additions & 0 deletions tests/e2e/email-detail-layout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { test, expect, type ElectronApplication, type Page } from "@playwright/test";
import { closeApp, launchElectronApp } from "./launch-helpers";

async function openEmail(page: Page, subject: string) {
const email = page.locator("button").filter({ hasText: subject }).first();
await expect(email).toBeVisible({ timeout: 10000 });
await email.click();
await expect(page.getByTestId("email-detail-scroll")).toBeVisible({ timeout: 5000 });
}

test.describe("Email detail layout", () => {
test.describe.configure({ mode: "serial" });

let electronApp: ElectronApplication;
let page: Page;

test.beforeAll(async ({}, testInfo) => {
const launched = await launchElectronApp({ workerIndex: testInfo.workerIndex });
electronApp = launched.app;
page = launched.page;
await page.locator("[data-thread-id]").first().waitFor({ timeout: 10000 });
});

test.afterAll(async () => {
if (electronApp) await closeApp(electronApp);
});

test("a short email fills the pane and keeps classification at the bottom", async () => {
await openEmail(page, "CI workflow failed on main");

const scroller = page.getByTestId("email-detail-scroll");
const messages = page.getByTestId("email-thread-messages");
const message = page.locator('[data-email-id="demo-006"] .group\\/msg');
const analysis = page.getByTestId("analysis-priority-section");

await expect(analysis).toBeVisible();

const [scrollerBox, messagesBox, messageBox, analysisBox] = await Promise.all([
scroller.boundingBox(),
messages.boundingBox(),
message.boundingBox(),
analysis.boundingBox(),
]);

expect(scrollerBox).not.toBeNull();
expect(messagesBox).not.toBeNull();
expect(messageBox).not.toBeNull();
expect(analysisBox).not.toBeNull();

const scrollerBottom = scrollerBox!.y + scrollerBox!.height;
const analysisBottom = analysisBox!.y + analysisBox!.height;
const messageBottom = messageBox!.y + messageBox!.height;

expect(Math.abs(scrollerBottom - analysisBottom)).toBeLessThanOrEqual(1);
expect(Math.abs(messageBottom - analysisBox!.y)).toBeLessThanOrEqual(1);
expect(messagesBox!.height).toBeGreaterThan(messageBox!.height - 1);
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight))
.toBe(await scroller.evaluate((element) => element.clientHeight));
});

test("a long email still scrolls to the classification row", async () => {
await page.getByRole("button", { name: "Back" }).click();
await openEmail(page, "Weekly Product Update");

const scroller = page.getByTestId("email-detail-scroll");
const analysis = page.getByTestId("analysis-priority-section");
const iframe = page.locator('iframe[title="Email content"]').first();

await expect(iframe).toBeVisible();
await expect.poll(async () => (await iframe.boundingBox())?.height ?? 0).toBeGreaterThan(400);

const dimensions = await scroller.evaluate((element) => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
}));
expect(dimensions.scrollHeight).toBeGreaterThan(dimensions.clientHeight);

await expect
.poll(() =>
scroller.evaluate((element) => {
element.scrollTop = element.scrollHeight;
const analysisSection = element.querySelector(
'[data-testid="analysis-priority-section"]',
);
if (!analysisSection) return Number.POSITIVE_INFINITY;
const scrollerBox = element.getBoundingClientRect();
const analysisBox = analysisSection.getBoundingClientRect();
return Math.abs(scrollerBox.bottom - analysisBox.bottom);
}),
)
.toBeLessThanOrEqual(1);
await expect(analysis).toBeVisible();
});
});
Loading