1import { describe, expect, it } from "vite-plus/test";
2import {
3 androidHeaderMenuActions,
4 findHeaderMenuAction,
5 splitAndroidHeaderActions,
6} from "./headerMenu.android";
7import type { ScreenHeaderMenuItem } from "./ScreenHeader.types";
8
9const items: ReadonlyArray<ScreenHeaderMenuItem> = [
10 {
11 id: "sessions",
12 title: "Sessions",
13 items: [
14 {
15 id: "terminal-session:main",
16 title: "Main shell",
17 subtitle: "Running · workspace",
18 selected: true,
19 onPress: () => {},
20 },
21 { id: "closed", title: "Closed shell", disabled: true, onPress: () => {} },
22 ],
23 },
24];
25
26describe("header menus", () => {
27 it("keeps named sections as Android submenus", () => {
28 const actions = androidHeaderMenuActions([
29 {
30 id: "text-size",
31 title: "Text size",
32 inline: true,
33 items: [{ id: "font-increase", title: "A+", onPress: () => {} }],
34 },
35 ]);
36 expect(actions[0]?.title).toBe("Text size");
37 expect(actions[0]?.subactions?.[0]?.id).toBe("font-increase");
38 });
39
40 it("flattens inline groups while keeping their action IDs", () => {
41 const actions = androidHeaderMenuActions([
42 { id: "modes", inline: true, items: [{ id: "code", title: "Code", onPress: () => {} }] },
43 ]);
44 expect(actions.map(({ id }) => id)).toEqual(["code"]);
45 });
46
47 it("keeps stable action IDs, selection, and disabled state inside submenus", () => {
48 const menu = androidHeaderMenuActions(items);
49 expect(menu[0]?.id).toBe("sessions");
50 expect(menu[0]?.subactions?.[0]).toMatchObject({
51 id: "terminal-session:main",
52 title: "Main shell",
53 subtitle: "Running · workspace",
54 state: "on",
55 });
56 expect(menu[0]?.subactions?.[1]).toMatchObject({
57 id: "closed",
58 attributes: { disabled: true },
59 });
60 });
61
62 it("resolves nested action IDs without dispatching group or missing IDs", () => {
63 expect(findHeaderMenuAction(items, "terminal-session:main")).toMatchObject({
64 title: "Main shell",
65 selected: true,
66 });
67 expect(findHeaderMenuAction(items, "sessions")).toBeUndefined();
68 expect(findHeaderMenuAction(items, "removed-session")).toBeUndefined();
69 });
70
71 it("keeps overflow-only Android header actions in the overflow menu", () => {
72 const action = (accessibilityLabel: string, overflowOnly?: boolean) => ({
73 accessibilityLabel,
74 icon: "trash" as const,
75 onPress: () => {},
76 overflowOnly,
77 });
78 const labels = (
79 split: ReturnType<typeof splitAndroidHeaderActions<ReturnType<typeof action>>>,
80 ) => ({
81 visible: split.visibleActions.map((item) => item.accessibilityLabel),
82 overflow: split.overflowActions.map((item) => item.accessibilityLabel),
83 });
84 const open = action("Open");
85 const remove = action("Delete", true);
86 expect(labels(splitAndroidHeaderActions([open, action("Comment")], false))).toEqual({
87 visible: ["Open", "Comment"],
88 overflow: [],
89 });
90 expect(labels(splitAndroidHeaderActions([open, remove], true))).toEqual({
91 visible: ["Open"],
92 overflow: ["Delete"],
93 });
94 expect(labels(splitAndroidHeaderActions([remove], true))).toEqual({
95 visible: [],
96 overflow: ["Delete"],
97 });
98 expect(
99 labels(splitAndroidHeaderActions([action("Review"), action("Comment"), open, remove], true)),
100 ).toEqual({ visible: ["Review", "Comment", "Open"], overflow: ["Delete"] });
101 });
102});