Skip to content

Commit bdf8c03

Browse files
committed
clean up and tests
1 parent 1d7f824 commit bdf8c03

27 files changed

+2686
-1375
lines changed

apps/array/src/renderer/features/message-editor/components/MessageEditor.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ import { ModelSelector } from "@features/sessions/components/ModelSelector";
33
import { ArrowUp, Paperclip, Stop } from "@phosphor-icons/react";
44
import { Flex, IconButton, Text, Tooltip } from "@radix-ui/themes";
55
import { forwardRef, useImperativeHandle, useRef } from "react";
6-
import {
7-
type EditorContent,
8-
type MentionChip,
9-
useContenteditableEditor,
10-
} from "../hooks/useContenteditableEditor";
11-
import { useMessageEditorStore } from "../stores/messageEditorStore";
6+
import type { EditorContent, MentionChip } from "../core/content";
7+
import { useMessageEditor } from "../hooks/useMessageEditor";
8+
import { useDraftStore } from "../stores/draftStore";
129
import { SuggestionPortal } from "./SuggestionPortal";
1310

1411
export interface MessageEditorHandle {
@@ -50,7 +47,7 @@ export const MessageEditor = forwardRef<
5047
ref,
5148
) => {
5249
const fileInputRef = useRef<HTMLInputElement>(null);
53-
const context = useMessageEditorStore((s) => s.contexts[sessionId]);
50+
const context = useDraftStore((s) => s.contexts[sessionId]);
5451
const taskId = context?.taskId;
5552
const disabled = context?.disabled ?? false;
5653
const isLoading = context?.isLoading ?? false;
@@ -74,7 +71,7 @@ export const MessageEditor = forwardRef<
7471
onPaste,
7572
onCompositionStart,
7673
onCompositionEnd,
77-
} = useContenteditableEditor({
74+
} = useMessageEditor({
7875
sessionId,
7976
taskId,
8077
placeholder,

apps/array/src/renderer/features/message-editor/components/SuggestionList.test.tsx

Lines changed: 66 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,38 @@
1-
import { act, fireEvent, render, screen } from "@testing-library/react";
1+
import { fireEvent, render, screen } from "@testing-library/react";
22
import { describe, expect, it, vi } from "vitest";
3-
import { useMessageEditorStore } from "../stores/messageEditorStore";
4-
import { setupSuggestionTests } from "../test/helpers";
5-
import type { SuggestionItem, SuggestionType } from "../types";
3+
import { useSuggestionStore } from "../stores/suggestionStore";
4+
import { ARIA, CSS, EMPTY_MESSAGES, LOADING_TEXT } from "../test/constants";
5+
import { SUGGESTION_ITEMS } from "../test/fixtures";
6+
import {
7+
enableMouseInteraction,
8+
getListbox,
9+
getOptions,
10+
openSuggestion,
11+
setupSuggestionTests,
12+
} from "../test/helpers";
613
import { SuggestionList } from "./SuggestionList";
714

8-
const SESSION_ID = "session-1";
9-
const SELECTED_CLASS = "suggestion-item-selected";
10-
11-
const ARIA_LABELS = {
12-
file: "File suggestions",
13-
command: "Available commands",
14-
} as const;
15-
16-
const EMPTY_MESSAGES = {
17-
file: "No files found",
18-
command: "No commands available",
19-
} as const;
20-
21-
const MOCK_ITEMS: SuggestionItem[] = [
22-
{ id: "1", label: "file1.ts", description: "src/file1.ts" },
23-
{ id: "2", label: "file2.ts", description: "src/file2.ts" },
24-
{ id: "3", label: "file3.ts" },
25-
];
26-
27-
const getActions = () => useMessageEditorStore.getState().actions;
28-
const getSelectedIndex = () =>
29-
useMessageEditorStore.getState().suggestion.selectedIndex;
30-
31-
const getListbox = () => screen.getByRole("listbox");
32-
const getOptions = () => screen.getAllByRole("option");
33-
34-
interface SuggestionSetup {
35-
items?: SuggestionItem[];
36-
selectedIndex?: number;
37-
type?: SuggestionType;
38-
loadingState?: "idle" | "loading" | "success" | "error";
39-
error?: string;
40-
onSelectItem?: (item: SuggestionItem) => void;
41-
}
42-
43-
function renderSuggestionList(overrides: SuggestionSetup = {}) {
44-
act(() => {
45-
const actions = getActions();
46-
actions.openSuggestion(SESSION_ID, overrides.type ?? "file", {
47-
x: 0,
48-
y: 0,
49-
});
50-
actions.setSuggestionItems(overrides.items ?? MOCK_ITEMS);
51-
if (overrides.selectedIndex !== undefined) {
52-
actions.setSelectedIndex(overrides.selectedIndex);
53-
}
54-
actions.setSuggestionLoadingState(
55-
overrides.loadingState ?? "success",
56-
overrides.error,
57-
);
58-
if (overrides.onSelectItem) {
59-
actions.setOnSelectItem(overrides.onSelectItem);
60-
}
61-
});
62-
return render(<SuggestionList />);
63-
}
64-
65-
function enableMouseInteraction() {
66-
fireEvent.mouseMove(getListbox());
67-
}
15+
const getSelectedIndex = () => useSuggestionStore.getState().selectedIndex;
6816

6917
describe("SuggestionList", () => {
7018
setupSuggestionTests();
7119

7220
describe("rendering items", () => {
7321
it("renders all item labels and descriptions", () => {
74-
renderSuggestionList();
75-
76-
expect(screen.getByText("file1.ts")).toBeInTheDocument();
77-
expect(screen.getByText("file2.ts")).toBeInTheDocument();
78-
expect(screen.getByText("file3.ts")).toBeInTheDocument();
79-
expect(screen.getByText("src/file1.ts")).toBeInTheDocument();
80-
expect(screen.getByText("src/file2.ts")).toBeInTheDocument();
22+
openSuggestion();
23+
render(<SuggestionList />);
24+
25+
for (const item of SUGGESTION_ITEMS) {
26+
expect(screen.getByText(item.label)).toBeInTheDocument();
27+
if (item.description) {
28+
expect(screen.getByText(item.description)).toBeInTheDocument();
29+
}
30+
}
8131
});
8232

8333
it("renders keyboard hints footer", () => {
84-
renderSuggestionList();
34+
openSuggestion();
35+
render(<SuggestionList />);
8536

8637
expect(screen.getByText(/navigate/)).toBeInTheDocument();
8738
expect(screen.getByText(/select/)).toBeInTheDocument();
@@ -91,14 +42,15 @@ describe("SuggestionList", () => {
9142

9243
describe("selected item highlighting", () => {
9344
it("applies selected class and aria-selected to correct item", () => {
94-
renderSuggestionList({ selectedIndex: 1 });
45+
openSuggestion({ selectedIndex: 1 });
46+
render(<SuggestionList />);
9547

9648
const options = getOptions();
97-
expect(options[0]).not.toHaveClass(SELECTED_CLASS);
49+
expect(options[0]).not.toHaveClass(CSS.SELECTED);
9850
expect(options[0]).toHaveAttribute("aria-selected", "false");
99-
expect(options[1]).toHaveClass(SELECTED_CLASS);
51+
expect(options[1]).toHaveClass(CSS.SELECTED);
10052
expect(options[1]).toHaveAttribute("aria-selected", "true");
101-
expect(options[2]).not.toHaveClass(SELECTED_CLASS);
53+
expect(options[2]).not.toHaveClass(CSS.SELECTED);
10254
expect(options[2]).toHaveAttribute("aria-selected", "false");
10355
});
10456
});
@@ -107,7 +59,8 @@ describe("SuggestionList", () => {
10759
it.each(["file", "command"] as const)(
10860
"shows correct empty message for %s type",
10961
(type) => {
110-
renderSuggestionList({ items: [], type, loadingState: "idle" });
62+
openSuggestion({ type, items: [], loadingState: "idle" });
63+
render(<SuggestionList />);
11164

11265
expect(screen.getByText(EMPTY_MESSAGES[type])).toBeInTheDocument();
11366
},
@@ -116,39 +69,47 @@ describe("SuggestionList", () => {
11669

11770
describe("loading and error states", () => {
11871
it("shows loading indicator and hides items", () => {
119-
renderSuggestionList({ loadingState: "loading" });
120-
121-
expect(screen.getByText("Searching...")).toBeInTheDocument();
122-
expect(screen.getByLabelText("Loading suggestions")).toBeInTheDocument();
123-
expect(screen.queryByText("file1.ts")).not.toBeInTheDocument();
72+
openSuggestion({ loadingState: "loading" });
73+
render(<SuggestionList />);
74+
75+
expect(screen.getByText(LOADING_TEXT)).toBeInTheDocument();
76+
expect(screen.getByLabelText(ARIA.LOADING)).toBeInTheDocument();
77+
expect(
78+
screen.queryByText(SUGGESTION_ITEMS[0].label),
79+
).not.toBeInTheDocument();
12480
});
12581

12682
it("shows error message and hides items", () => {
12783
const errorMessage = "Failed to load files";
128-
renderSuggestionList({ loadingState: "error", error: errorMessage });
84+
openSuggestion({ loadingState: "error", error: errorMessage });
85+
render(<SuggestionList />);
12986

13087
expect(screen.getByText(errorMessage)).toBeInTheDocument();
13188
expect(screen.getByRole("alert")).toHaveAttribute(
13289
"aria-label",
133-
"Error loading suggestions",
90+
ARIA.ERROR,
13491
);
135-
expect(screen.queryByText("file1.ts")).not.toBeInTheDocument();
92+
expect(
93+
screen.queryByText(SUGGESTION_ITEMS[0].label),
94+
).not.toBeInTheDocument();
13695
});
13796
});
13897

13998
describe("mouse interactions", () => {
14099
it("calls onSelectItem when clicking an item", () => {
141100
const onSelectItem = vi.fn();
142-
renderSuggestionList({ onSelectItem });
101+
openSuggestion({ onSelectItem });
102+
render(<SuggestionList />);
143103

144104
enableMouseInteraction();
145-
fireEvent.click(screen.getByText("file2.ts"));
105+
fireEvent.click(screen.getByText(SUGGESTION_ITEMS[1].label));
146106

147-
expect(onSelectItem).toHaveBeenCalledWith(MOCK_ITEMS[1]);
107+
expect(onSelectItem).toHaveBeenCalledWith(1);
148108
});
149109

150110
it("updates selectedIndex on hover after mouse movement", () => {
151-
renderSuggestionList();
111+
openSuggestion();
112+
render(<SuggestionList />);
152113

153114
enableMouseInteraction();
154115
fireEvent.mouseEnter(getOptions()[1]);
@@ -157,7 +118,8 @@ describe("SuggestionList", () => {
157118
});
158119

159120
it("ignores hover before any mouse movement", () => {
160-
renderSuggestionList();
121+
openSuggestion();
122+
render(<SuggestionList />);
161123

162124
fireEvent.mouseEnter(getOptions()[1]);
163125

@@ -167,27 +129,34 @@ describe("SuggestionList", () => {
167129

168130
describe("accessibility", () => {
169131
it("has listbox role with option children", () => {
170-
renderSuggestionList();
132+
openSuggestion();
133+
render(<SuggestionList />);
171134

172135
expect(getListbox()).toBeInTheDocument();
173-
expect(getOptions()).toHaveLength(3);
136+
expect(getOptions()).toHaveLength(SUGGESTION_ITEMS.length);
174137
});
175138

176139
it.each(["file", "command"] as const)(
177140
"sets correct aria-label for %s type",
178141
(type) => {
179-
renderSuggestionList({ type });
180-
181-
expect(getListbox()).toHaveAttribute("aria-label", ARIA_LABELS[type]);
142+
const ariaLabels = {
143+
file: ARIA.FILE_SUGGESTIONS,
144+
command: ARIA.COMMAND_SUGGESTIONS,
145+
};
146+
openSuggestion({ type });
147+
render(<SuggestionList />);
148+
149+
expect(getListbox()).toHaveAttribute("aria-label", ariaLabels[type]);
182150
},
183151
);
184152

185153
it("sets aria-activedescendant to selected item id", () => {
186-
renderSuggestionList({ selectedIndex: 1 });
154+
openSuggestion({ selectedIndex: 1 });
155+
render(<SuggestionList />);
187156

188157
expect(getListbox()).toHaveAttribute(
189158
"aria-activedescendant",
190-
"suggestion-2",
159+
`suggestion-${SUGGESTION_ITEMS[1].id}`,
191160
);
192161
});
193162
});

apps/array/src/renderer/features/message-editor/components/SuggestionList.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
2-
import { useMessageEditorStore } from "../stores/messageEditorStore";
2+
import { useSuggestionStore } from "../stores/suggestionStore";
33

44
export function SuggestionList() {
5-
const items = useMessageEditorStore((s) => s.suggestion.items);
6-
const selectedIndex = useMessageEditorStore(
7-
(s) => s.suggestion.selectedIndex,
8-
);
9-
const type = useMessageEditorStore((s) => s.suggestion.type);
10-
const loadingState = useMessageEditorStore((s) => s.suggestion.loadingState);
11-
const error = useMessageEditorStore((s) => s.suggestion.error);
12-
const onSelectItem = useMessageEditorStore((s) => s.suggestion.onSelectItem);
13-
const actions = useMessageEditorStore((s) => s.actions);
5+
const items = useSuggestionStore((s) => s.items);
6+
const selectedIndex = useSuggestionStore((s) => s.selectedIndex);
7+
const type = useSuggestionStore((s) => s.type);
8+
const loadingState = useSuggestionStore((s) => s.loadingState);
9+
const error = useSuggestionStore((s) => s.error);
10+
const actions = useSuggestionStore((s) => s.actions);
1411

1512
const emptyMessage =
1613
type === "command" ? "No commands available" : "No files found";
@@ -130,7 +127,7 @@ export function SuggestionList() {
130127
ref={(el) => {
131128
itemRefs.current[index] = el;
132129
}}
133-
onClick={() => onSelectItem?.(item)}
130+
onClick={() => actions.selectItem(index)}
134131
onMouseEnter={() =>
135132
hasMouseMoved && actions.setSelectedIndex(index)
136133
}

0 commit comments

Comments
 (0)