@ksp-gonogo/kerbcast-react 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/CameraFeed.d.ts +32 -16
- package/dist/CameraFeed.js +259 -169
- package/dist/FeedActionBar.d.ts +133 -0
- package/dist/FeedActionBar.js +225 -0
- package/dist/KerbalFaceFeed.d.ts +1 -1
- package/dist/KerbalFaceFeed.js +12 -19
- package/dist/RecordingsTray.d.ts +37 -0
- package/dist/RecordingsTray.js +279 -0
- package/dist/hooks/useRecordings.d.ts +35 -0
- package/dist/hooks/useRecordings.js +33 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/menuPositioning.d.ts +20 -0
- package/dist/menuPositioning.js +98 -0
- package/dist/timing.d.ts +4 -0
- package/dist/timing.js +19 -0
- package/package.json +2 -2
- package/src/CameraFeed.test.tsx +545 -0
- package/src/CameraFeed.tsx +421 -309
- package/src/FeedActionBar.test.ts +196 -0
- package/src/FeedActionBar.test.tsx +215 -0
- package/src/FeedActionBar.tsx +391 -0
- package/src/KerbalFaceFeed.tsx +13 -35
- package/src/RecordingsTray.test.tsx +288 -0
- package/src/RecordingsTray.tsx +475 -0
- package/src/hooks/useRecordings.sharedState.test.tsx +117 -0
- package/src/hooks/useRecordings.test.tsx +217 -0
- package/src/hooks/useRecordings.ts +110 -0
- package/src/index.ts +15 -0
- package/src/menuPositioning.ts +126 -0
- package/src/timing.test.ts +34 -0
- package/src/timing.ts +21 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the FeedActionBar overflow-partition logic (issue #6 UI spec).
|
|
3
|
+
*
|
|
4
|
+
* Pure-function tests only; the rendered FeedActionBar component (built-in
|
|
5
|
+
* wiring through CameraFeed / KerbalFaceFeed) is covered by their own test
|
|
6
|
+
* suites, which exercise the real DOM output (the ⋮ trigger, the portaled
|
|
7
|
+
* overflow menu, stable ordering across enable/disable toggles).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { partitionActionBarEntries } from "./FeedActionBar";
|
|
12
|
+
|
|
13
|
+
interface Entry {
|
|
14
|
+
id: string;
|
|
15
|
+
stateful?: boolean;
|
|
16
|
+
order?: number;
|
|
17
|
+
pinnedTrailing?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ids(entries: Entry[]): string[] {
|
|
21
|
+
return entries.map((e) => e.id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("partitionActionBarEntries", () => {
|
|
25
|
+
it("keeps everything primary (inline) when total enabled < 4", () => {
|
|
26
|
+
const entries: Entry[] = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
27
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
28
|
+
expect(ids(primary)).toEqual(["a", "b", "c"]);
|
|
29
|
+
expect(overflow).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("collapses non-stateful entries into overflow at exactly the 4/2 threshold", () => {
|
|
33
|
+
const entries: Entry[] = [
|
|
34
|
+
{ id: "tracking", stateful: true },
|
|
35
|
+
{ id: "quality" },
|
|
36
|
+
{ id: "pip" },
|
|
37
|
+
{ id: "fullscreen" },
|
|
38
|
+
];
|
|
39
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
40
|
+
expect(ids(primary)).toEqual(["tracking"]);
|
|
41
|
+
expect(ids(overflow)).toEqual(["quality", "pip", "fullscreen"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("never collapses a single non-stateful item, even with total >= 4", () => {
|
|
45
|
+
const entries: Entry[] = [
|
|
46
|
+
{ id: "tracking", stateful: true },
|
|
47
|
+
{ id: "rec", stateful: true },
|
|
48
|
+
{ id: "close", stateful: true },
|
|
49
|
+
{ id: "quality" }, // the only non-stateful eligible item
|
|
50
|
+
];
|
|
51
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
52
|
+
expect(ids(primary)).toEqual(["tracking", "rec", "close", "quality"]);
|
|
53
|
+
expect(overflow).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("stays inline when total is below 4 even with 2+ non-stateful eligible", () => {
|
|
57
|
+
const entries: Entry[] = [{ id: "quality" }, { id: "pip" }];
|
|
58
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
59
|
+
expect(ids(primary)).toEqual(["quality", "pip"]);
|
|
60
|
+
expect(overflow).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("stays inline just under the total threshold (3 total, 3 non-stateful)", () => {
|
|
64
|
+
const entries: Entry[] = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
65
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
66
|
+
expect(ids(primary)).toEqual(["a", "b", "c"]);
|
|
67
|
+
expect(overflow).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("all-stateful entries never overflow regardless of count", () => {
|
|
71
|
+
const entries: Entry[] = [
|
|
72
|
+
{ id: "a", stateful: true },
|
|
73
|
+
{ id: "b", stateful: true },
|
|
74
|
+
{ id: "c", stateful: true },
|
|
75
|
+
{ id: "d", stateful: true },
|
|
76
|
+
{ id: "e", stateful: true },
|
|
77
|
+
];
|
|
78
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
79
|
+
expect(ids(primary)).toEqual(["a", "b", "c", "d", "e"]);
|
|
80
|
+
expect(overflow).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sorts by explicit order first, then preserves stable relative order for the rest", () => {
|
|
84
|
+
const entries: Entry[] = [
|
|
85
|
+
{ id: "custom", order: 0 },
|
|
86
|
+
{ id: "quality" },
|
|
87
|
+
{ id: "tracking", stateful: true },
|
|
88
|
+
{ id: "pip" },
|
|
89
|
+
{ id: "fullscreen" },
|
|
90
|
+
];
|
|
91
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
92
|
+
// "custom" (order: 0) sorts first; the rest keep their input order.
|
|
93
|
+
expect(ids(primary)).toEqual(["tracking"]);
|
|
94
|
+
expect(ids(overflow)).toEqual(["custom", "quality", "pip", "fullscreen"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not reflow the remaining chips' relative order when one is disabled", () => {
|
|
98
|
+
// Simulates a re-render where "pip" is no longer enabled (simply omitted).
|
|
99
|
+
// "help" here is just a generic non-stateful, non-pinned example action --
|
|
100
|
+
// NOT the close/remove slot (that's covered in its own describe block
|
|
101
|
+
// below, since it is excluded from this count entirely).
|
|
102
|
+
const before: Entry[] = [
|
|
103
|
+
{ id: "tracking", stateful: true },
|
|
104
|
+
{ id: "quality" },
|
|
105
|
+
{ id: "pip" },
|
|
106
|
+
{ id: "fullscreen" },
|
|
107
|
+
{ id: "help" },
|
|
108
|
+
];
|
|
109
|
+
const after: Entry[] = [
|
|
110
|
+
{ id: "tracking", stateful: true },
|
|
111
|
+
{ id: "quality" },
|
|
112
|
+
{ id: "fullscreen" },
|
|
113
|
+
{ id: "help" },
|
|
114
|
+
];
|
|
115
|
+
const beforeResult = partitionActionBarEntries(before);
|
|
116
|
+
const afterResult = partitionActionBarEntries(after);
|
|
117
|
+
expect(ids(beforeResult.overflow)).toEqual(["quality", "pip", "fullscreen", "help"]);
|
|
118
|
+
// "pip" drops out; "quality", "fullscreen", "help" keep the same relative order.
|
|
119
|
+
expect(ids(afterResult.overflow)).toEqual(["quality", "fullscreen", "help"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("preserves the partition's relative order (stateful entries interspersed in input)", () => {
|
|
123
|
+
const entries: Entry[] = [
|
|
124
|
+
{ id: "quality" },
|
|
125
|
+
{ id: "tracking", stateful: true },
|
|
126
|
+
{ id: "pip" },
|
|
127
|
+
{ id: "rec", stateful: true },
|
|
128
|
+
{ id: "fullscreen" },
|
|
129
|
+
];
|
|
130
|
+
const { primary, overflow } = partitionActionBarEntries(entries);
|
|
131
|
+
expect(ids(primary)).toEqual(["tracking", "rec"]);
|
|
132
|
+
expect(ids(overflow)).toEqual(["quality", "pip", "fullscreen"]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("partitionActionBarEntries -- pinnedTrailing (close/remove)", () => {
|
|
137
|
+
it("returns a pinnedTrailing entry in its own group, separate from primary/overflow", () => {
|
|
138
|
+
const entries: Entry[] = [
|
|
139
|
+
{ id: "spotlight", stateful: true },
|
|
140
|
+
{ id: "close", pinnedTrailing: true },
|
|
141
|
+
];
|
|
142
|
+
const { primary, overflow, pinnedTrailing } = partitionActionBarEntries(entries);
|
|
143
|
+
expect(ids(primary)).toEqual(["spotlight"]);
|
|
144
|
+
expect(overflow).toEqual([]);
|
|
145
|
+
expect(ids(pinnedTrailing)).toEqual(["close"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("excludes pinnedTrailing from BOTH the total and the non-stateful-eligible count", () => {
|
|
149
|
+
// Same 4 actions CrewBar wires per face: spotlight (stateful), fullscreen,
|
|
150
|
+
// pip (both non-stateful, overflow-eligible), close (pinned). Naively
|
|
151
|
+
// counting close toward the total would hit the 4/2 threshold; excluding
|
|
152
|
+
// it drops the non-pinned total to 3, so overflow does NOT trigger --
|
|
153
|
+
// fullscreen/pip stay primary, and close is still separate (never inline
|
|
154
|
+
// with them, never counted).
|
|
155
|
+
const entries: Entry[] = [
|
|
156
|
+
{ id: "spotlight", stateful: true },
|
|
157
|
+
{ id: "fullscreen" },
|
|
158
|
+
{ id: "pip" },
|
|
159
|
+
{ id: "close", pinnedTrailing: true },
|
|
160
|
+
];
|
|
161
|
+
const { primary, overflow, pinnedTrailing } = partitionActionBarEntries(entries);
|
|
162
|
+
expect(ids(primary)).toEqual(["spotlight", "fullscreen", "pip"]);
|
|
163
|
+
expect(overflow).toEqual([]);
|
|
164
|
+
expect(ids(pinnedTrailing)).toEqual(["close"]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("still triggers overflow on the remaining set once it alone crosses the threshold", () => {
|
|
168
|
+
// The Tile.tsx / CameraFeed shape: spotlight + quality + tracking + pip +
|
|
169
|
+
// fullscreen + remove (pinned). Excluding "remove", the remaining 5
|
|
170
|
+
// actions (3 non-stateful: quality/pip/fullscreen) still cross 4/2.
|
|
171
|
+
const entries: Entry[] = [
|
|
172
|
+
{ id: "spotlight", stateful: true },
|
|
173
|
+
{ id: "quality" },
|
|
174
|
+
{ id: "tracking", stateful: true },
|
|
175
|
+
{ id: "pip" },
|
|
176
|
+
{ id: "fullscreen" },
|
|
177
|
+
{ id: "remove", pinnedTrailing: true },
|
|
178
|
+
];
|
|
179
|
+
const { primary, overflow, pinnedTrailing } = partitionActionBarEntries(entries);
|
|
180
|
+
expect(ids(primary)).toEqual(["spotlight", "tracking"]);
|
|
181
|
+
expect(ids(overflow)).toEqual(["quality", "pip", "fullscreen"]);
|
|
182
|
+
expect(ids(pinnedTrailing)).toEqual(["remove"]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("never appears inside the overflow group, even when it is the only non-stateful-looking entry", () => {
|
|
186
|
+
const entries: Entry[] = [
|
|
187
|
+
{ id: "a", stateful: true },
|
|
188
|
+
{ id: "b", stateful: true },
|
|
189
|
+
{ id: "c", stateful: true },
|
|
190
|
+
{ id: "close", pinnedTrailing: true },
|
|
191
|
+
];
|
|
192
|
+
const { overflow, pinnedTrailing } = partitionActionBarEntries(entries);
|
|
193
|
+
expect(overflow).toEqual([]);
|
|
194
|
+
expect(ids(pinnedTrailing)).toEqual(["close"]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendered-DOM tests for `FeedActionBar` -- the actual ⋮ trigger + portaled
|
|
3
|
+
* overflow menu behaviour, on top of the pure partition logic covered in
|
|
4
|
+
* FeedActionBar.test.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { FeedActionBar, type FeedActionBarEntry } from "./FeedActionBar";
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function makeEntry(
|
|
16
|
+
id: string,
|
|
17
|
+
opts: {
|
|
18
|
+
stateful?: boolean;
|
|
19
|
+
order?: number;
|
|
20
|
+
pinnedTrailing?: boolean;
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
} = {},
|
|
23
|
+
): FeedActionBarEntry {
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
label: id,
|
|
27
|
+
stateful: opts.stateful,
|
|
28
|
+
order: opts.order,
|
|
29
|
+
pinnedTrailing: opts.pinnedTrailing,
|
|
30
|
+
render: () => (
|
|
31
|
+
<button type="button" aria-label={id} onClick={opts.onClick}>
|
|
32
|
+
{id}
|
|
33
|
+
</button>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("FeedActionBar", () => {
|
|
39
|
+
it("renders every entry inline (no ⋮ trigger) when below the overflow threshold", () => {
|
|
40
|
+
render(
|
|
41
|
+
<FeedActionBar
|
|
42
|
+
entries={[makeEntry("a"), makeEntry("b"), makeEntry("c")]}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByLabelText("a")).toBeTruthy();
|
|
46
|
+
expect(screen.getByLabelText("b")).toBeTruthy();
|
|
47
|
+
expect(screen.getByLabelText("c")).toBeTruthy();
|
|
48
|
+
expect(screen.queryByLabelText("More actions")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("shows the ⋮ trigger once total >= 4 and non-stateful eligible >= 2, hiding the collapsed entries inline", () => {
|
|
52
|
+
render(
|
|
53
|
+
<FeedActionBar
|
|
54
|
+
entries={[
|
|
55
|
+
makeEntry("tracking", { stateful: true }),
|
|
56
|
+
makeEntry("quality"),
|
|
57
|
+
makeEntry("pip"),
|
|
58
|
+
makeEntry("fullscreen"),
|
|
59
|
+
]}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
// Stateful stays inline.
|
|
63
|
+
expect(screen.getByLabelText("tracking")).toBeTruthy();
|
|
64
|
+
// Non-stateful collapsed: not rendered inline (only inside the closed menu).
|
|
65
|
+
expect(screen.queryByLabelText("quality")).toBeNull();
|
|
66
|
+
expect(screen.getByLabelText("More actions")).toBeTruthy();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("opens the portaled overflow menu on click, listing the collapsed entries", () => {
|
|
70
|
+
render(
|
|
71
|
+
<FeedActionBar
|
|
72
|
+
entries={[
|
|
73
|
+
makeEntry("tracking", { stateful: true }),
|
|
74
|
+
makeEntry("quality"),
|
|
75
|
+
makeEntry("pip"),
|
|
76
|
+
makeEntry("fullscreen"),
|
|
77
|
+
]}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.click(screen.getByLabelText("More actions"));
|
|
82
|
+
|
|
83
|
+
const menu = screen.getByRole("menu", { name: "More actions" });
|
|
84
|
+
expect(menu.parentElement).toBe(document.body);
|
|
85
|
+
// Portaled + fixed, same contract as CameraFeed's other menus.
|
|
86
|
+
expect(getComputedStyle(menu).position).toBe("fixed");
|
|
87
|
+
expect(screen.getByLabelText("quality")).toBeTruthy();
|
|
88
|
+
expect(screen.getByLabelText("pip")).toBeTruthy();
|
|
89
|
+
expect(screen.getByLabelText("fullscreen")).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("clicking an overflow entry fires its own onClick AND closes the overflow menu", () => {
|
|
93
|
+
const onQuality = vi.fn();
|
|
94
|
+
render(
|
|
95
|
+
<FeedActionBar
|
|
96
|
+
entries={[
|
|
97
|
+
makeEntry("tracking", { stateful: true }),
|
|
98
|
+
makeEntry("quality", { onClick: onQuality }),
|
|
99
|
+
makeEntry("pip"),
|
|
100
|
+
makeEntry("fullscreen"),
|
|
101
|
+
]}
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
fireEvent.click(screen.getByLabelText("More actions"));
|
|
106
|
+
expect(screen.getByRole("menu", { name: "More actions" })).toBeTruthy();
|
|
107
|
+
|
|
108
|
+
fireEvent.click(screen.getByLabelText("quality"));
|
|
109
|
+
expect(onQuality).toHaveBeenCalledTimes(1);
|
|
110
|
+
// The menu closes on its own after a leaf action fires, rather than
|
|
111
|
+
// waiting for an outside pointerdown.
|
|
112
|
+
expect(screen.queryByRole("menu", { name: "More actions" })).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("never collapses a single non-stateful item even with 4+ total", () => {
|
|
116
|
+
render(
|
|
117
|
+
<FeedActionBar
|
|
118
|
+
entries={[
|
|
119
|
+
makeEntry("tracking", { stateful: true }),
|
|
120
|
+
makeEntry("rec", { stateful: true }),
|
|
121
|
+
makeEntry("power", { stateful: true }),
|
|
122
|
+
makeEntry("quality"),
|
|
123
|
+
]}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
expect(screen.getByLabelText("quality")).toBeTruthy();
|
|
127
|
+
expect(screen.queryByLabelText("More actions")).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("renders leading content ahead of the partitioned entries", () => {
|
|
131
|
+
render(
|
|
132
|
+
<FeedActionBar
|
|
133
|
+
leading={<span data-testid="leading">step buttons</span>}
|
|
134
|
+
entries={[makeEntry("a")]}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
expect(screen.getByTestId("leading")).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("renders nothing when there is no leading content and no entries", () => {
|
|
141
|
+
const { container } = render(<FeedActionBar entries={[]} />);
|
|
142
|
+
expect(container.firstChild).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("FeedActionBar -- pinnedTrailing (close/remove)", () => {
|
|
147
|
+
it("renders the pinned entry directly, never inside the ⋮ overflow menu", () => {
|
|
148
|
+
// Same shape as CrewBar's per-face row: spotlight (stateful) + fullscreen
|
|
149
|
+
// + pip (non-stateful) + close (pinned). Excluding "close" from the count
|
|
150
|
+
// drops the non-pinned total to 3, so overflow does not even trigger --
|
|
151
|
+
// fullscreen/pip stay inline, and close sits after them.
|
|
152
|
+
render(
|
|
153
|
+
<FeedActionBar
|
|
154
|
+
entries={[
|
|
155
|
+
makeEntry("spotlight", { stateful: true }),
|
|
156
|
+
makeEntry("fullscreen"),
|
|
157
|
+
makeEntry("pip"),
|
|
158
|
+
makeEntry("close", { pinnedTrailing: true }),
|
|
159
|
+
]}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
expect(screen.getByLabelText("fullscreen")).toBeTruthy();
|
|
163
|
+
expect(screen.getByLabelText("pip")).toBeTruthy();
|
|
164
|
+
expect(screen.getByLabelText("close")).toBeTruthy();
|
|
165
|
+
expect(screen.queryByLabelText("More actions")).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("is excluded from the overflow-threshold count: the remaining set can still cross it on its own", () => {
|
|
169
|
+
// Tile.tsx / CameraFeed shape: spotlight + quality + tracking + pip +
|
|
170
|
+
// fullscreen + remove (pinned). Excluding "remove", the remaining 5 (3
|
|
171
|
+
// non-stateful) still cross 4/2, so the ⋮ appears for those three.
|
|
172
|
+
render(
|
|
173
|
+
<FeedActionBar
|
|
174
|
+
entries={[
|
|
175
|
+
makeEntry("spotlight", { stateful: true }),
|
|
176
|
+
makeEntry("quality"),
|
|
177
|
+
makeEntry("tracking", { stateful: true }),
|
|
178
|
+
makeEntry("pip"),
|
|
179
|
+
makeEntry("fullscreen"),
|
|
180
|
+
makeEntry("remove", { pinnedTrailing: true }),
|
|
181
|
+
]}
|
|
182
|
+
/>,
|
|
183
|
+
);
|
|
184
|
+
expect(screen.queryByLabelText("quality")).toBeNull();
|
|
185
|
+
expect(screen.queryByLabelText("pip")).toBeNull();
|
|
186
|
+
expect(screen.queryByLabelText("fullscreen")).toBeNull();
|
|
187
|
+
expect(screen.getByLabelText("More actions")).toBeTruthy();
|
|
188
|
+
// The pinned entry is never swept into the overflow menu.
|
|
189
|
+
expect(screen.getByLabelText("remove")).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("renders as the rightmost control -- after the ⋮ trigger -- in DOM order", () => {
|
|
193
|
+
render(
|
|
194
|
+
<FeedActionBar
|
|
195
|
+
entries={[
|
|
196
|
+
makeEntry("spotlight", { stateful: true }),
|
|
197
|
+
makeEntry("quality"),
|
|
198
|
+
makeEntry("tracking", { stateful: true }),
|
|
199
|
+
makeEntry("pip"),
|
|
200
|
+
makeEntry("fullscreen"),
|
|
201
|
+
makeEntry("remove", { pinnedTrailing: true }),
|
|
202
|
+
]}
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
const bar = screen.getByLabelText("More actions").parentElement;
|
|
206
|
+
expect(bar).toBeTruthy();
|
|
207
|
+
const order = Array.from(bar?.querySelectorAll("button") ?? []).map((el) =>
|
|
208
|
+
el.getAttribute("aria-label"),
|
|
209
|
+
);
|
|
210
|
+
// "More actions" (⋮) immediately precedes "remove" (✕), and nothing
|
|
211
|
+
// pinned/overflow-eligible follows it.
|
|
212
|
+
expect(order.at(-2)).toBe("More actions");
|
|
213
|
+
expect(order.at(-1)).toBe("remove");
|
|
214
|
+
});
|
|
215
|
+
});
|