1import { act, useLayoutEffect } from "react";
2import { createRoot, type Root } from "react-dom/client";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
4
5import { shouldUseRestingComposerLayout } from "../composerFooterLayout";
6import { useComposerFocusState } from "./useComposerFocusState";
7
8let root: Root;
9let composer: ReturnType<typeof useComposerFocusState>;
10let isResting: boolean;
11
12function ComposerProbe() {
13 const state = useComposerFocusState();
14 useLayoutEffect(() => {
15 composer = state;
16 isResting = shouldUseRestingComposerLayout({
17 isExistingThread: true,
18 isMobileViewport: false,
19 isScrollCollapsed: state.isComposerScrollCollapsed,
20 hasExpandedChrome: false,
21 hasMultilinePrompt: false,
22 timelineOverflows: true,
23 });
24 });
25 return null;
26}
27
28beforeEach(async () => {
29 // The probe has no DOM output, but ReactDOM needs an event target.
30 const document = {
31 nodeType: 9,
32 addEventListener() {},
33 removeEventListener() {},
34 };
35 const container = {
36 nodeType: 1,
37 tagName: "DIV",
38 namespaceURI: "http://www.w3.org/1999/xhtml",
39 ownerDocument: document,
40 addEventListener() {},
41 removeEventListener() {},
42 };
43 vi.stubGlobal("document", document);
44 vi.stubGlobal("window", { document, HTMLIFrameElement: EventTarget });
45 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
46 root = createRoot(container as unknown as HTMLElement);
47 await act(() => root.render(<ComposerProbe />));
48});
49
50afterEach(async () => {
51 await act(() => root.unmount());
52 vi.unstubAllGlobals();
53});
54
55describe("composer focus state", () => {
56 it("stays expanded when the composer loses focus", async () => {
57 await act(() => composer.setIsComposerFocused(true));
58 expect(isResting).toBe(false);
59
60 // A tool disclosure takes focus away from the editor.
61 await act(() => composer.setIsComposerFocused(false));
62 expect(isResting).toBe(false);
63 });
64
65 it("can collapse again on the next scroll after returning to the end", async () => {
66 await act(() => composer.setIsComposerScrollCollapsed(true));
67 expect(isResting).toBe(true);
68
69 await act(() => composer.restoreAfterTimelineReachedEnd());
70 expect(isResting).toBe(false);
71
72 await act(() => composer.setIsComposerScrollCollapsed(true));
73 expect(isResting).toBe(true);
74 });
75
76 it("does not move focus into the composer when the timeline reaches the end", async () => {
77 await act(() => composer.setIsComposerScrollCollapsed(true));
78 await act(() => composer.restoreAfterTimelineReachedEnd());
79 expect(isResting).toBe(false);
80 expect(composer.isComposerFocused).toBe(false);
81 });
82});