Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { createRef } from "react";
import { act, render } from "@testing-library/react";
import NewWaveMentionsPlugin, {
WaveMentionTypeaheadOption,
} from "@/components/drops/create/lexical/plugins/waves/WaveMentionsPlugin";

jest.mock("@lexical/react/LexicalComposerContext", () => ({
useLexicalComposerContext: () => [{ update: (fn: () => void) => fn() }],
}));

let capturedProps: any;
jest.mock("@lexical/react/LexicalTypeaheadMenuPlugin", () => ({
LexicalTypeaheadMenuPlugin: (props: any) => {
capturedProps = props;
return <div data-testid="typeahead" />;
},
MenuOption: class {
key: string;
ref: { current: HTMLElement | null };

constructor(key: string) {
this.key = key;
this.ref = { current: null };
}

setRefElement = (element: HTMLElement | null) => {
this.ref.current = element;
};
},
useBasicTypeaheadTriggerMatch: () => jest.fn(() => null),
}));

jest.mock("@/hooks/useWavesSearch", () => ({
useWavesSearch: jest.fn(),
}));

jest.mock("@/components/drops/create/lexical/nodes/WaveMentionNode", () => ({
$createWaveMentionNode: jest.fn(),
}));

jest.mock(
"@/components/drops/create/lexical/utils/codeContextDetection",
() => ({
isInCodeContext: jest.fn(() => false),
})
);

const { useWavesSearch } = require("@/hooks/useWavesSearch");
const {
$createWaveMentionNode,
} = require("@/components/drops/create/lexical/nodes/WaveMentionNode");

describe("WaveMentionsPlugin", () => {
beforeEach(() => {
capturedProps = null;
jest.clearAllMocks();
});

it("builds options from waves and exposes open state", () => {
(useWavesSearch as jest.Mock).mockReturnValue({
waves: [
{ id: "wave-1", name: "Wave Alpha", picture: null },
{ id: "wave-2", name: "Wave Beta", picture: null },
],
});

const ref = createRef<any>();
render(<NewWaveMentionsPlugin onSelect={jest.fn()} ref={ref} />);

expect(capturedProps.options).toHaveLength(2);
expect(capturedProps.options[0]).toBeInstanceOf(WaveMentionTypeaheadOption);

act(() => {
capturedProps.onOpen();
});
expect(ref.current.isWaveMentionsOpen()).toBe(true);

act(() => {
capturedProps.onClose();
});
expect(ref.current.isWaveMentionsOpen()).toBe(false);
});

it("creates wave mention node and emits sanitized mention payload", () => {
(useWavesSearch as jest.Mock).mockReturnValue({
waves: [{ id: "wave-1", name: "Brack]et Wave", picture: null }],
});

const mentionNode = {
select: jest.fn(),
};
($createWaveMentionNode as jest.Mock).mockReturnValue(mentionNode);

const onSelect = jest.fn();
render(<NewWaveMentionsPlugin onSelect={onSelect} ref={createRef()} />);

const closeMenu = jest.fn();
const nodeToReplace = {
replace: jest.fn(),
};

act(() => {
capturedProps.onSelectOption(
capturedProps.options[0],
nodeToReplace,
closeMenu
);
});

expect($createWaveMentionNode).toHaveBeenCalledWith("#Bracket Wave");
expect(nodeToReplace.replace).toHaveBeenCalledWith(mentionNode);
expect(mentionNode.select).toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith({
wave_id: "wave-1",
wave_name_in_content: "Bracket Wave",
});
expect(closeMenu).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import WaveMentionsTypeaheadMenu from "@/components/drops/create/lexical/plugins/waves/WaveMentionsTypeaheadMenu";

function createOption(name: string) {
return {
key: `option-${name}`,
name,
picture: null,
setRefElement: jest.fn(),
};
}

function createAnchorElement() {
return document.createElement("div");
}

describe("WaveMentionsTypeaheadMenu", () => {
const baseRect = {
x: 0,
y: 0,
width: 320,
height: 120,
top: 100,
right: 320,
bottom: 220,
left: 0,
toJSON: () => "",
};

let getBoundingClientRectMock: jest.SpyInstance;

beforeEach(() => {
getBoundingClientRectMock = jest
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
.mockReturnValue(baseRect as DOMRect);
});

afterEach(() => {
getBoundingClientRectMock.mockRestore();
jest.clearAllMocks();
});

it("renders with bottom positioning when there is more space below", () => {
const options = [createOption("Wave Alpha"), createOption("Wave Beta")];
const { container } = render(
<WaveMentionsTypeaheadMenu
selectedIndex={0}
options={options as any}
setHighlightedIndex={jest.fn()}
selectOptionAndCleanUp={jest.fn()}
anchorElement={createAnchorElement()}
/>
);

expect(container.firstChild).toHaveClass("tw-top-full");
expect(container.firstChild).toHaveClass("tw-mt-1");
});

it("switches to top positioning when there is more space above", async () => {
getBoundingClientRectMock.mockReturnValue({
...baseRect,
top: 700,
bottom: 790,
} as DOMRect);

const options = [createOption("Wave Alpha")];
const { container } = render(
<WaveMentionsTypeaheadMenu
selectedIndex={0}
options={options as any}
setHighlightedIndex={jest.fn()}
selectOptionAndCleanUp={jest.fn()}
anchorElement={createAnchorElement()}
/>
);

await waitFor(() => {
expect(container.firstChild).toHaveClass("tw-bottom-full");
expect(container.firstChild).toHaveClass("tw-mb-1");
});
});

it("selects clicked option and remains stable across repeated resize events", () => {
const options = [createOption("Wave Alpha"), createOption("Wave Beta")];
const setHighlightedIndex = jest.fn();
const selectOptionAndCleanUp = jest.fn();

render(
<WaveMentionsTypeaheadMenu
selectedIndex={0}
options={options as any}
setHighlightedIndex={setHighlightedIndex}
selectOptionAndCleanUp={selectOptionAndCleanUp}
anchorElement={createAnchorElement()}
/>
);

fireEvent.click(screen.getByRole("button", { name: "Wave Alpha" }));

expect(setHighlightedIndex).toHaveBeenCalledWith(0);
expect(selectOptionAndCleanUp).toHaveBeenCalledWith(options[0]);

for (let i = 0; i < 5; i += 1) {
fireEvent(window, new Event("resize"));
}

expect(
screen.getByRole("button", { name: "Wave Alpha" })
).toBeInTheDocument();
});

it("recalculates position on scroll events", async () => {
let rect = baseRect as DOMRect;
getBoundingClientRectMock.mockImplementation(() => rect);

const options = [createOption("Wave Alpha")];
const { container } = render(
<WaveMentionsTypeaheadMenu
selectedIndex={0}
options={options as any}
setHighlightedIndex={jest.fn()}
selectOptionAndCleanUp={jest.fn()}
anchorElement={createAnchorElement()}
/>
);

rect = {
...baseRect,
top: 700,
bottom: 790,
} as DOMRect;
fireEvent(window, new Event("scroll"));

await waitFor(() => {
expect(container.firstChild).toHaveClass("tw-bottom-full");
expect(container.firstChild).toHaveClass("tw-mb-1");
});
});

it("observes anchor element and cleans up resize/scroll listeners", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const observe = jest.fn();
const disconnect = jest.fn();
const ResizeObserverMock = jest.fn().mockImplementation(() => ({
observe,
disconnect,
unobserve: jest.fn(),
}));
globalThis.ResizeObserver =
ResizeObserverMock as unknown as typeof ResizeObserver;

const addListenerSpy = jest.spyOn(window, "addEventListener");
const removeListenerSpy = jest.spyOn(window, "removeEventListener");
const anchorElement = createAnchorElement();

try {
const options = [createOption("Wave Alpha")];
const { unmount } = render(
<WaveMentionsTypeaheadMenu
selectedIndex={0}
options={options as any}
setHighlightedIndex={jest.fn()}
selectOptionAndCleanUp={jest.fn()}
anchorElement={anchorElement}
/>
);

expect(addListenerSpy).toHaveBeenCalledWith(
"resize",
expect.any(Function)
);
expect(addListenerSpy).toHaveBeenCalledWith(
"scroll",
expect.any(Function),
{ passive: true }
);
expect(observe).toHaveBeenCalledWith(anchorElement);

unmount();

expect(removeListenerSpy).toHaveBeenCalledWith(
"resize",
expect.any(Function)
);
expect(removeListenerSpy).toHaveBeenCalledWith(
"scroll",
expect.any(Function)
);
expect(disconnect).toHaveBeenCalledTimes(1);
} finally {
addListenerSpy.mockRestore();
removeListenerSpy.mockRestore();
globalThis.ResizeObserver = originalResizeObserver;
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const NewWaveMentionsPlugin = forwardRef<
options={options}
setHighlightedIndex={setHighlightedIndex}
selectOptionAndCleanUp={selectOptionAndCleanUp}
anchorElement={anchorElementRef.current}
/>
</div>,
anchorElementRef.current
Expand Down
Loading