Files
PaperClipAI/ui/src/components/StatusIcon.test.tsx
T
DottaandPaperclip 7e0f051d3b Align task property icons with assignee avatars (#13319)
## Thinking Path

> - Paperclip is the open source app that people use to manage AI agents
for work.
> - The task properties panel helps operators inspect and update task
data.
> - The status, assignee, and project rows used different leading icon
sizes.
> - The mixed sizes made the rows look uneven.
> - The assignee avatar already provides the correct 24px visual size.
> - This pull request sets the status and project visuals to that same
size.
> - The benefit is a tidy and consistent properties panel.

## Linked Issues or Issue Description

**What happened?**

The task properties panel showed a 12px status icon, a 24px assignee
avatar, and a 16px project tile.

**Expected behavior**

The status, assignee, and project rows must use the same 24px leading
visual size.

**Steps to reproduce**

1. Open a task.
2. Open the Properties panel.
3. Compare the Status, Assignee, and Project rows.

## What Changed

- Keep the Status glyph at 16px and use spacing to give it the 24px
avatar footprint.
- Set the Project row tile to the 24px small tile size.
- Add a regression check for the Status row size.

## Verification

- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx` (72
tests passed)
- `pnpm check:token-gates` (all gates clean)
- `pnpm --filter @paperclipai/ui typecheck` (passed)
- Full repository typecheck and build reached the Rust runner step, but
this environment does not contain `cargo`.
- The full test command was stopped after unrelated chat integration
tests did not finish. The focused UI suite passed.

## Risks

- Low risk. This change only changes visual sizes in three property
rows.
- The wider status footprint can use slightly more horizontal space in a
narrow panel.

> This fix does not overlap with planned core work in `ROADMAP.md`.

## Model Used

- OpenAI Codex, GPT-5.6, tool use and code execution enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-27 11:35:39 +00:00

141 lines
5.5 KiB
TypeScript

// @vitest-environment node
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { StatusIcon } from "./StatusIcon";
import { IssueStatusBadge } from "./StatusBadge";
/**
* StatusIcon renders the unified {@link StatusGlyph} (one shape per status) at
* every standalone status surface. These tests lock the glyph rendering, the
* covered-blocked → "in queue" mapping, the accessible blocked labels, and the
* size prop.
*/
describe("StatusIcon", () => {
it("renders the unified glyph (24-unit viewBox), not a bespoke ring", () => {
const html = renderToStaticMarkup(<StatusIcon status="in_progress" />);
expect(html).toContain('viewBox="0 0 24 24"');
expect(html).not.toContain("rounded-full border-2");
});
it("drives the glyph colour from the status icon var", () => {
const html = renderToStaticMarkup(<StatusIcon status="todo" />);
expect(html).toContain("var(--status-task-icon-todo)");
});
it("maps covered-blocked → In queue (blue in_queue var, no cyan markers)", () => {
const html = renderToStaticMarkup(
<StatusIcon
status="blocked"
blockerAttention={{
state: "covered",
reason: "active_child",
unresolvedBlockerCount: 1,
coveredBlockerCount: 1,
attentionBlockerCount: 0,
stalledBlockerCount: 0,
sampleBlockerIdentifier: "PAP-9",
sampleStalledBlockerIdentifier: null,
}}
/>,
);
expect(html).toContain("var(--status-task-icon-in_queue)");
expect(html).not.toContain("bg-cyan");
expect(html).not.toContain("border-cyan");
// Full blocked reason still rides on the accessible label.
expect(html).toContain("Blocked · waiting on active sub-task PAP-9");
});
it("surfaces attention-required blocked copy and keeps the blocked glyph", () => {
const html = renderToStaticMarkup(
<StatusIcon
status="blocked"
blockerAttention={{
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 3,
coveredBlockerCount: 2,
stalledBlockerCount: 0,
attentionBlockerCount: 3,
sampleBlockerIdentifier: "PAP-3541",
sampleStalledBlockerIdentifier: null,
}}
/>,
);
expect(html).toContain("Blocked · 3 blockers need attention; 2 covered by active work");
// needs_attention is not "covered", so it keeps the blocked glyph (not in_queue).
expect(html).toContain("var(--status-task-icon-blocked)");
expect(html).not.toContain("var(--status-task-icon-in_queue)");
});
it("surfaces stalled-review blocked copy on the accessible label", () => {
const html = renderToStaticMarkup(
<StatusIcon
status="blocked"
blockerAttention={{
state: "stalled",
reason: "stalled_review",
unresolvedBlockerCount: 1,
coveredBlockerCount: 0,
stalledBlockerCount: 1,
attentionBlockerCount: 0,
sampleBlockerIdentifier: "PAP-2279",
sampleStalledBlockerIdentifier: "PAP-2279",
}}
/>,
);
expect(html).toContain("Blocked · review stalled on PAP-2279");
});
it("uses an accessible native button for the icon-only picker trigger", () => {
const html = renderToStaticMarkup(<StatusIcon status="todo" onChange={() => {}} />);
expect(html).toContain('viewBox="0 0 24 24"');
expect(html).toContain('<button type="button"');
expect(html).toContain('aria-label="Change status (current: Todo)"');
});
});
describe("StatusIcon — glyph size (PAP-243a)", () => {
it('forwards size="lg" as a 20px glyph', () => {
const html = renderToStaticMarkup(<StatusIcon status="todo" size="lg" />);
expect(html).toContain('width="20"');
expect(html).toContain('height="20"');
});
it("defaults to a 16px (md) glyph when size is omitted", () => {
const html = renderToStaticMarkup(<StatusIcon status="todo" />);
expect(html).toContain('width="16"');
expect(html).toContain('height="16"');
});
it("can place the default glyph in a larger layout footprint without resizing it", () => {
const html = renderToStaticMarkup(
<StatusIcon status="todo" glyphContainerClassName="size-6" showLabel />,
);
expect(html).toContain('data-status-glyph-container="true"');
expect(html).toContain('class="size-6"');
expect(html).toContain('width="16"');
expect(html).toContain('height="16"');
});
});
describe("Slack idle status presentation", () => {
it("labels verified waiting conversations Idle in icons, labels, badges and picker triggers", () => {
const props = { status: "in_review", externalConversationState: "waiting" as const };
expect(renderToStaticMarkup(<StatusIcon {...props} showLabel />)).toContain(">Idle<");
expect(renderToStaticMarkup(<StatusIcon {...props} onChange={() => {}} />)).toContain('current: Idle');
expect(renderToStaticMarkup(<IssueStatusBadge {...props} />)).toContain(">Idle<");
});
it.each(["in_review", "blocked", "done", "cancelled"])("preserves %s without verified waiting", (status) => {
const html = renderToStaticMarkup(<StatusIcon status={status} externalConversationState="active" showLabel />);
expect(html).not.toContain("Idle");
});
it("preserves an explicit terminal outcome even with stale waiting data", () => {
expect(renderToStaticMarkup(<IssueStatusBadge status="done" externalConversationState="waiting" />)).toContain(">Done<");
});
});