Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
82 changes: 82 additions & 0 deletions src/components/emoji-picker-design-spec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "fs";
import { resolve } from "path";

/**
* Regression tests for issue #186: UI does not match design spec after PR #185.
*
* These tests verify that emoji-picker and page-icon source files comply with
* the design spec constraints for touch targets, input styling, mobile
* visibility, and responsive width.
*/

function readSource(relativePath: string): string {
return readFileSync(resolve(__dirname, relativePath), "utf-8");
}

describe("emoji-picker design spec compliance", () => {
const source = readSource("./emoji-picker.tsx");

it("emoji buttons have 44px minimum touch target on mobile", () => {
// Design spec: "Touch targets: minimum 44px on mobile."
// Emoji buttons must use min-h-[44px] and min-w-[44px] for mobile.
expect(source).toContain("min-h-[44px]");
expect(source).toContain("min-w-[44px]");
});

it("filter input has border per input spec", () => {
// Design spec: Inputs should have border-white/[0.06] border.
const precedingClass = source.match(
/className="[^"]*border border-white\/\[0\.06\][^"]*"[\s\S]*?aria-label="Filter emojis"/
);
expect(precedingClass).not.toBeNull();
});

it("filter input has focus ring per input spec", () => {
// Design spec: Inputs should have ring-2 ring-ring on focus.
const inputSection = source.match(
/className="[^"]*"[\s\S]*?aria-label="Filter emojis"/
);
expect(inputSection).not.toBeNull();
expect(inputSection![0]).toContain("focus:ring-2");
expect(inputSection![0]).toContain("focus:ring-ring");
});

it("picker uses responsive width to avoid mobile clipping", () => {
// Design spec: "No horizontal scroll on any breakpoint."
// The picker must not use a fixed w-72 without a mobile-responsive alternative.
expect(source).toContain("sm:w-72");
// Must have a viewport-relative width for mobile
expect(source).toMatch(/w-\[calc\(100vw/);
});
});

describe("page-icon design spec compliance", () => {
const source = readSource("./page-icon.tsx");

it("page icon button has 44px minimum touch target on mobile", () => {
// Design spec: "Touch targets: minimum 44px on mobile."
expect(source).toContain("min-h-[44px]");
expect(source).toContain("min-w-[44px]");
});

it("add-icon button is visible on mobile (not hover-only)", () => {
// Design spec: Touch targets must be accessible on mobile.
// The button container must not rely solely on group-hover for visibility.
// It should use max-sm:opacity-100 or similar to be visible on touch devices.
expect(source).toContain("max-sm:opacity-100");
});

it("add-icon button has 44px minimum touch target on mobile", () => {
// The "Add icon" button must meet the 44px minimum on mobile.
const addIconSection = source.match(
/aria-label="Add page icon"[\s\S]{0,50}/
);
expect(addIconSection).not.toBeNull();

const classSection = source.match(
/className="[^"]*min-h-\[44px\][^"]*"[\s\S]*?aria-label="Add page icon"/
);
expect(classSection).not.toBeNull();
});
});
6 changes: 3 additions & 3 deletions src/components/emoji-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function EmojiPicker({
{open && (
<div
ref={floatingRef}
className="fixed z-50 w-72 rounded-sm border border-white/[0.06] bg-popover p-2 shadow-md"
className="fixed z-50 w-[calc(100vw-16px)] rounded-sm border border-white/[0.06] bg-popover p-2 shadow-md sm:w-72"
role="dialog"
aria-label="Emoji picker"
>
Expand All @@ -191,7 +191,7 @@ export function EmojiPicker({
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
className="mb-2 w-full bg-muted px-2 py-1 text-sm text-foreground placeholder:text-muted-foreground outline-none"
className="mb-2 h-9 w-full border border-white/[0.06] bg-muted px-2 py-1 text-sm text-foreground placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:outline-none"
aria-label="Filter emojis"
/>
{hasIcon && onRemove && (
Expand All @@ -213,7 +213,7 @@ export function EmojiPicker({
<button
key={emoji}
onClick={() => handleSelect(emoji)}
className="flex h-8 w-8 items-center justify-center text-lg hover:bg-white/[0.04]"
className="flex min-h-[44px] min-w-[44px] items-center justify-center text-lg hover:bg-white/[0.04] sm:min-h-8 sm:min-w-8"
Comment thread
zacharias-ona marked this conversation as resolved.
aria-label={`Select ${emoji}`}
>
{emoji}
Expand Down
6 changes: 3 additions & 3 deletions src/components/page-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function PageIcon({ pageId, initialIcon }: PageIconProps) {
hasIcon
>
<button
className="flex h-10 w-10 items-center justify-center rounded-sm text-4xl leading-none hover:bg-white/[0.04]"
className="flex min-h-[44px] min-w-[44px] items-center justify-center rounded-sm text-4xl leading-none hover:bg-white/[0.04] sm:min-h-10 sm:min-w-10"
aria-label={`Page icon: ${icon}. Click to change`}
>
{icon}
Expand All @@ -66,7 +66,7 @@ export function PageIcon({ pageId, initialIcon }: PageIconProps) {
}

return (
<div className="mb-1 opacity-0 group-hover/page-header:opacity-100 transition-opacity">
<div className="mb-1 max-sm:opacity-100 opacity-0 group-hover/page-header:opacity-100 transition-opacity">
<EmojiPicker
open={open}
onOpenChange={setOpen}
Expand All @@ -75,7 +75,7 @@ export function PageIcon({ pageId, initialIcon }: PageIconProps) {
hasIcon={false}
>
<button
className="flex h-7 items-center gap-1 rounded-sm px-1.5 text-xs text-muted-foreground hover:bg-white/[0.04]"
className="flex min-h-[44px] items-center gap-1 rounded-sm px-1.5 text-xs text-muted-foreground hover:bg-white/[0.04] sm:min-h-7"
aria-label="Add page icon"
>
<SmilePlus className="h-3.5 w-3.5" />
Expand Down
Loading