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:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user