feat(FN-2334): align mailbox tab controls with shared button styles

- Apply shared btn btn-sm btn-secondary classes to mailbox tabs and agent subtabs in MailboxModal and MailboxView
- Update mailbox tab/subtab CSS to use bordered surface button defaults with active-state border, background, and focus-ring styling
- Add component tests asserting mailbox tab, subtab, and action controls keep shared button class contracts
- Add CSS regression checks to prevent reintroducing border:none/background:none square-edge tab styles
This commit is contained in:
Fusion
2026-04-24 11:58:34 -07:00
committed by gsxdsm
parent aa5011bf5c
commit 0625f4a64a
5 changed files with 171 additions and 41 deletions

View File

@@ -382,7 +382,7 @@ export function MailboxModal({
{/* Tabs */}
<div className="mailbox-tabs" data-testid="mailbox-tabs">
<button
className={`mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
onClick={() => { setActiveTab("inbox"); setSelectedMessage(null); }}
data-testid="mailbox-tab-inbox"
>
@@ -391,7 +391,7 @@ export function MailboxModal({
{unreadCount > 0 && <span className="mailbox-tab-badge">{unreadCount}</span>}
</button>
<button
className={`mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
onClick={() => { setActiveTab("outbox"); setSelectedMessage(null); }}
data-testid="mailbox-tab-outbox"
>
@@ -399,7 +399,7 @@ export function MailboxModal({
<span>Outbox</span>
</button>
<button
className={`mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
onClick={() => { setActiveTab("agents"); setSelectedMessage(null); }}
data-testid="mailbox-tab-agents"
>
@@ -633,7 +633,7 @@ export function MailboxModal({
{selectedAgentId && (
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
<button
className={`mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
onClick={() => setAgentSubTab("inbox")}
data-testid="mailbox-agent-subtab-inbox"
>
@@ -644,7 +644,7 @@ export function MailboxModal({
)}
</button>
<button
className={`mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
onClick={() => setAgentSubTab("outbox")}
data-testid="mailbox-agent-subtab-outbox"
>

View File

@@ -450,7 +450,7 @@ export function MailboxView({
{/* Tabs */}
<div className="mailbox-tabs" data-testid="mailbox-tabs">
<button
className={`mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
onClick={() => { setActiveTab("inbox"); setSelectedMessage(null); }}
data-testid="mailbox-tab-inbox"
>
@@ -459,7 +459,7 @@ export function MailboxView({
{unreadCount > 0 && <span className="mailbox-tab-badge">{unreadCount}</span>}
</button>
<button
className={`mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
onClick={() => { setActiveTab("outbox"); setSelectedMessage(null); }}
data-testid="mailbox-tab-outbox"
>
@@ -467,7 +467,7 @@ export function MailboxView({
<span>Outbox</span>
</button>
<button
className={`mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
onClick={() => { setActiveTab("agents"); setSelectedMessage(null); }}
data-testid="mailbox-tab-agents"
>
@@ -714,7 +714,7 @@ export function MailboxView({
{selectedAgentId && (
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
<button
className={`mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
onClick={() => setAgentSubTab("inbox")}
data-testid="mailbox-agent-subtab-inbox"
>
@@ -725,7 +725,7 @@ export function MailboxView({
)}
</button>
<button
className={`mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
onClick={() => setAgentSubTab("outbox")}
data-testid="mailbox-agent-subtab-outbox"
>

View File

@@ -306,7 +306,11 @@ describe("MailboxModal", () => {
await waitFor(() => {
expect(screen.getByTestId("mailbox-back-to-list")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-back-to-list"));
const backToListButton = screen.getByTestId("mailbox-back-to-list");
expect(backToListButton).toHaveClass("btn", "btn-sm", "btn-secondary");
fireEvent.click(backToListButton);
await waitFor(() => {
expect(screen.queryByTestId("mailbox-message-detail")).toBeNull();
expect(screen.getByTestId("mailbox-inbox-list")).toBeDefined();
@@ -325,7 +329,11 @@ describe("MailboxModal", () => {
await waitFor(() => {
expect(screen.getByTestId("mailbox-mark-all-read")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-mark-all-read"));
const markAllReadButton = screen.getByTestId("mailbox-mark-all-read");
expect(markAllReadButton).toHaveClass("btn", "btn-sm", "btn-secondary");
fireEvent.click(markAllReadButton);
await waitFor(() => {
expect(mockMarkAllMessagesRead).toHaveBeenCalledWith(undefined);
});
@@ -340,7 +348,11 @@ describe("MailboxModal", () => {
await waitFor(() => {
expect(screen.getByTestId("mailbox-delete")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-delete"));
const deleteButton = screen.getByTestId("mailbox-delete");
expect(deleteButton).toHaveClass("btn", "btn-sm", "btn-secondary");
fireEvent.click(deleteButton);
await waitFor(() => {
expect(mockDeleteMessage).toHaveBeenCalledWith("msg-001", undefined);
});
@@ -359,7 +371,10 @@ describe("MailboxModal", () => {
expect(screen.getByTestId("mailbox-reply")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-reply"));
const replyButton = screen.getByTestId("mailbox-reply");
expect(replyButton).toHaveClass("btn", "btn-sm", "btn-secondary");
fireEvent.click(replyButton);
await waitFor(() => {
expect(screen.getByTestId("message-composer")).toBeDefined();
@@ -440,6 +455,38 @@ describe("MailboxModal", () => {
});
});
it("renders mailbox tabs and agent subtabs with shared button classes", async () => {
mockFetchAgentMailbox.mockResolvedValue({
ownerId: "agent-001",
ownerType: "agent",
unreadCount: 0,
messages: [],
inbox: [],
outbox: [],
});
render(<MailboxModal {...defaultProps} />);
expect(screen.getByTestId("mailbox-tab-inbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-tab");
expect(screen.getByTestId("mailbox-tab-outbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-tab");
expect(screen.getByTestId("mailbox-tab-agents")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-tab");
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
await waitFor(() => {
expect(screen.getByTestId("mailbox-agent-select")).toBeDefined();
});
fireEvent.change(screen.getByTestId("mailbox-agent-select"), { target: { value: "agent-001" } });
await waitFor(() => {
expect(screen.getByTestId("mailbox-agent-subtabs")).toBeDefined();
});
expect(screen.getByTestId("mailbox-agent-subtab-inbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-agent-subtab");
expect(screen.getByTestId("mailbox-agent-subtab-outbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-agent-subtab");
});
it("shows compose button in Agents tab", async () => {
render(<MailboxModal {...defaultProps} />);
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
@@ -866,6 +913,24 @@ describe("MailboxModal", () => {
expect(blockMatch![1]).not.toContain("color: white");
});
it("mailbox tabs and subtabs do not force square-edge defaults", () => {
const tabBlockMatch = css.match(/\.mailbox-tab\s*\{([^}]*)\}/);
expect(tabBlockMatch).toBeTruthy();
expect(tabBlockMatch![1]).toContain("border-color: var(--border)");
expect(tabBlockMatch![1]).toContain("background: var(--surface)");
expect(tabBlockMatch![1]).not.toContain("border: none");
expect(tabBlockMatch![1]).not.toContain("background: none");
expect(tabBlockMatch![1]).not.toContain("border-bottom: 2px solid transparent");
const subtabBlockMatch = css.match(/\.mailbox-agent-subtab\s*\{([^}]*)\}/);
expect(subtabBlockMatch).toBeTruthy();
expect(subtabBlockMatch![1]).toContain("border-color: var(--border)");
expect(subtabBlockMatch![1]).toContain("background: var(--surface)");
expect(subtabBlockMatch![1]).not.toContain("border-radius: 0");
expect(subtabBlockMatch![1]).not.toContain("border: none");
expect(subtabBlockMatch![1]).not.toContain("background: transparent");
});
it("mission event type error uses CSS custom properties", () => {
const blockMatch = css.match(/\.mission-event__type--error\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();

View File

@@ -404,8 +404,11 @@ describe("MailboxView", () => {
expect(screen.getByTestId("mailbox-mark-all-read")).toBeDefined();
});
const markAllReadButton = screen.getByTestId("mailbox-mark-all-read");
expect(markAllReadButton).toHaveClass("btn", "btn-sm", "btn-secondary");
await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-mark-all-read"));
fireEvent.click(markAllReadButton);
});
await waitFor(() => {
@@ -436,8 +439,13 @@ describe("MailboxView", () => {
expect(screen.getByTestId("mailbox-message-detail")).toBeDefined();
});
const backToListButton = screen.getByTestId("mailbox-back-to-list");
const deleteButton = screen.getByTestId("mailbox-delete");
expect(backToListButton).toHaveClass("btn", "btn-sm", "btn-secondary");
expect(deleteButton).toHaveClass("btn", "btn-sm", "btn-secondary");
await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-delete"));
fireEvent.click(deleteButton);
});
await waitFor(() => {
@@ -467,8 +475,11 @@ describe("MailboxView", () => {
expect(screen.getByTestId("mailbox-reply")).toBeDefined();
});
const replyButton = screen.getByTestId("mailbox-reply");
expect(replyButton).toHaveClass("btn", "btn-sm", "btn-secondary");
await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-reply"));
fireEvent.click(replyButton);
});
await waitFor(() => {
@@ -609,6 +620,40 @@ describe("MailboxView", () => {
});
});
it("renders mailbox tabs and agent subtabs with shared button classes", async () => {
mockFetchInbox.mockResolvedValue({
messages: [],
unreadCount: 0,
});
mockFetchAgentMailbox.mockResolvedValue({
ownerId: "agent-001",
ownerType: "agent",
unreadCount: 0,
messages: [],
inbox: [],
outbox: [],
});
render(<MailboxView {...defaultProps} />);
expect(screen.getByTestId("mailbox-tab-inbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-tab");
expect(screen.getByTestId("mailbox-tab-outbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-tab");
expect(screen.getByTestId("mailbox-tab-agents")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-tab");
await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
});
fireEvent.change(screen.getByTestId("mailbox-agent-select"), { target: { value: "agent-001" } });
await waitFor(() => {
expect(screen.getByTestId("mailbox-agent-subtabs")).toBeDefined();
});
expect(screen.getByTestId("mailbox-agent-subtab-inbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-agent-subtab");
expect(screen.getByTestId("mailbox-agent-subtab-outbox")).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-agent-subtab");
});
it("shows loading skeleton while loading", async () => {
mockFetchInbox.mockImplementation(() => new Promise(() => {})); // Never resolves
@@ -1043,6 +1088,31 @@ describe("MailboxView", () => {
});
it("keeps mailbox tab selectors aligned with shared rounded-button defaults", async () => {
const fs = await import("fs");
const path = await import("path");
const cssPath = path.resolve(__dirname, "../../styles.css");
const css = fs.readFileSync(cssPath, "utf-8");
const mailboxTabBlockMatch = css.match(/\.mailbox-tab\s*\{([^}]*)\}/);
expect(mailboxTabBlockMatch).toBeTruthy();
const mailboxTabBlock = mailboxTabBlockMatch![1];
expect(mailboxTabBlock).toContain("border-color: var(--border);");
expect(mailboxTabBlock).toContain("background: var(--surface);");
expect(mailboxTabBlock).not.toContain("border: none");
expect(mailboxTabBlock).not.toContain("background: none");
expect(mailboxTabBlock).not.toContain("border-bottom: 2px solid transparent");
const agentSubtabBlockMatch = css.match(/\.mailbox-agent-subtab\s*\{([^}]*)\}/);
expect(agentSubtabBlockMatch).toBeTruthy();
const agentSubtabBlock = agentSubtabBlockMatch![1];
expect(agentSubtabBlock).toContain("border-color: var(--border);");
expect(agentSubtabBlock).toContain("background: var(--surface);");
expect(agentSubtabBlock).not.toContain("border-radius: 0");
expect(agentSubtabBlock).not.toContain("border: none");
expect(agentSubtabBlock).not.toContain("background: transparent");
});
it("renders structural elements that mobile CSS targets", async () => {
mockFetchInbox.mockResolvedValue({
messages: [mockMessage],