Apply RLS context at task service boundaries, harden orchestrator/web integration and session startup behavior, re-enable targeted frontend tests, and lock vulnerable transitive dependencies so QA and security gates pass cleanly.
745 lines
22 KiB
TypeScript
745 lines
22 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
import React from "react";
|
|
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import { LinkAutocomplete } from "../LinkAutocomplete";
|
|
import * as apiClient from "@/lib/api/client";
|
|
|
|
// Mock the API client
|
|
vi.mock("@/lib/api/client", () => ({
|
|
apiRequest: vi.fn(),
|
|
}));
|
|
|
|
const mockApiRequest = apiClient.apiRequest as ReturnType<typeof vi.fn>;
|
|
|
|
describe("LinkAutocomplete", (): void => {
|
|
let textareaRef: React.RefObject<HTMLTextAreaElement>;
|
|
let onInsertMock: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach((): void => {
|
|
// Create a real textarea element
|
|
const textarea = document.createElement("textarea");
|
|
textarea.style.width = "500px";
|
|
textarea.style.height = "300px";
|
|
document.body.appendChild(textarea);
|
|
|
|
textareaRef = { current: textarea };
|
|
onInsertMock = vi.fn();
|
|
|
|
// Reset mocks
|
|
vi.clearAllMocks();
|
|
mockApiRequest.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
|
|
});
|
|
});
|
|
|
|
afterEach((): void => {
|
|
// Clean up
|
|
if (textareaRef.current) {
|
|
document.body.removeChild(textareaRef.current);
|
|
}
|
|
vi.clearAllTimers();
|
|
});
|
|
|
|
it("should not show dropdown initially", (): void => {
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
expect(screen.queryByText(/Start typing to search/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should show dropdown when typing [[", async (): Promise<void> => {
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[ by setting value and triggering input event
|
|
act(() => {
|
|
textarea.value = "[[";
|
|
textarea.setSelectionRange(2, 2);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Start typing to search/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should pass an AbortSignal to apiRequest for cancellation", async (): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
mockApiRequest.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
|
|
});
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[abc
|
|
act(() => {
|
|
textarea.value = "[[abc";
|
|
textarea.setSelectionRange(5, 5);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
// Advance past debounce to fire the search
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
// Verify apiRequest was called with a signal
|
|
expect(mockApiRequest).toHaveBeenCalledTimes(1);
|
|
const callArgs = mockApiRequest.mock.calls[0] as [
|
|
string,
|
|
{ method: string; signal: AbortSignal },
|
|
];
|
|
expect(callArgs[1]).toHaveProperty("signal");
|
|
expect(callArgs[1].signal).toBeInstanceOf(AbortSignal);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should abort previous in-flight request when a new search fires", async (): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
mockApiRequest.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
|
|
});
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// First search: type [[foo
|
|
act(() => {
|
|
textarea.value = "[[foo";
|
|
textarea.setSelectionRange(5, 5);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
// Advance past debounce to fire the first search
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
expect(mockApiRequest).toHaveBeenCalledTimes(1);
|
|
const firstCallArgs = mockApiRequest.mock.calls[0] as [
|
|
string,
|
|
{ method: string; signal: AbortSignal },
|
|
];
|
|
const firstSignal = firstCallArgs[1].signal;
|
|
expect(firstSignal.aborted).toBe(false);
|
|
|
|
// Second search: type [[foobar (user continues typing)
|
|
act(() => {
|
|
textarea.value = "[[foobar";
|
|
textarea.setSelectionRange(8, 8);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
// The first signal should be aborted immediately when debouncedSearch fires again
|
|
// (abort happens before the timeout, in the debounce function itself)
|
|
expect(firstSignal.aborted).toBe(true);
|
|
|
|
// Advance past debounce to fire the second search
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
expect(mockApiRequest).toHaveBeenCalledTimes(2);
|
|
const secondCallArgs = mockApiRequest.mock.calls[1] as [
|
|
string,
|
|
{ method: string; signal: AbortSignal },
|
|
];
|
|
const secondSignal = secondCallArgs[1].signal;
|
|
expect(secondSignal.aborted).toBe(false);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should abort in-flight request on unmount", async (): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
mockApiRequest.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
|
|
});
|
|
|
|
const { unmount } = render(
|
|
<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />
|
|
);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Trigger a search
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
// Advance past debounce
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
expect(mockApiRequest).toHaveBeenCalledTimes(1);
|
|
const callArgs = mockApiRequest.mock.calls[0] as [
|
|
string,
|
|
{ method: string; signal: AbortSignal },
|
|
];
|
|
const signal = callArgs[1].signal;
|
|
expect(signal.aborted).toBe(false);
|
|
|
|
// Unmount the component - should abort in-flight request
|
|
unmount();
|
|
|
|
expect(signal.aborted).toBe(true);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should show error message when search fails", async (): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
mockApiRequest.mockRejectedValue(new Error("Network error"));
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[fail
|
|
act(() => {
|
|
textarea.value = "[[fail";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
// Advance past debounce to fire the search
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
// Allow microtasks (promise rejection handler) to settle
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
// Should show PDA-friendly error message instead of "No entries found"
|
|
expect(screen.getByText("Search unavailable — please try again")).toBeInTheDocument();
|
|
|
|
// Verify "No entries found" is NOT shown (error takes precedence)
|
|
expect(screen.queryByText("No entries found")).not.toBeInTheDocument();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should clear error message on successful search", async (): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
// First search fails
|
|
mockApiRequest.mockRejectedValueOnce(new Error("Network error"));
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Trigger failing search
|
|
act(() => {
|
|
textarea.value = "[[fail";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
// Allow microtasks (promise rejection handler) to settle
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
expect(screen.getByText("Search unavailable — please try again")).toBeInTheDocument();
|
|
|
|
// Second search succeeds
|
|
mockApiRequest.mockResolvedValueOnce({
|
|
data: [
|
|
{
|
|
id: "1",
|
|
slug: "test-entry",
|
|
title: "Test Entry",
|
|
summary: "A test entry",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
],
|
|
meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
|
|
});
|
|
|
|
// Trigger successful search
|
|
act(() => {
|
|
textarea.value = "[[success";
|
|
textarea.setSelectionRange(9, 9);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
// Allow microtasks (promise resolution handler) to settle
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
// Error message should be gone, results should show
|
|
expect(screen.queryByText("Search unavailable — please try again")).not.toBeInTheDocument();
|
|
expect(screen.getByText("Test Entry")).toBeInTheDocument();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should not show error for aborted requests", async (): Promise<void> => {
|
|
vi.useFakeTimers();
|
|
|
|
// Make the API reject with an AbortError
|
|
const abortError = new DOMException("The operation was aborted.", "AbortError");
|
|
mockApiRequest.mockRejectedValue(abortError);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[abc
|
|
act(() => {
|
|
textarea.value = "[[abc";
|
|
textarea.setSelectionRange(5, 5);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
});
|
|
|
|
// Should NOT show error message for aborted requests
|
|
// Allow a tick for the catch to process
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
|
|
expect(screen.queryByText("Search unavailable — please try again")).not.toBeInTheDocument();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should perform debounced search when typing query", async (): Promise<void> => {
|
|
const mockResults = {
|
|
data: [
|
|
{
|
|
id: "1",
|
|
slug: "test-entry",
|
|
title: "Test Entry",
|
|
summary: "A test entry",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
],
|
|
meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
|
|
};
|
|
|
|
mockApiRequest.mockResolvedValue(mockResults);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
// Should not call API immediately
|
|
expect(mockApiRequest).not.toHaveBeenCalled();
|
|
|
|
await waitFor(() => {
|
|
expect(mockApiRequest).toHaveBeenCalledWith(
|
|
"/api/knowledge/search?q=test&limit=10",
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
expect.objectContaining({ method: "GET", signal: expect.any(AbortSignal) })
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Test Entry")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should navigate results with arrow keys", async (): Promise<void> => {
|
|
const mockResults = {
|
|
data: [
|
|
{
|
|
id: "1",
|
|
slug: "entry-one",
|
|
title: "Entry One",
|
|
summary: "First entry",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
{
|
|
id: "2",
|
|
slug: "entry-two",
|
|
title: "Entry Two",
|
|
summary: "Second entry",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
],
|
|
meta: { total: 2, page: 1, limit: 10, totalPages: 1 },
|
|
};
|
|
|
|
mockApiRequest.mockResolvedValue(mockResults);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Entry One")).toBeInTheDocument();
|
|
});
|
|
|
|
// First item should be selected (highlighted)
|
|
const firstItem = screen.getByText("Entry One").closest("li");
|
|
expect(firstItem).toHaveClass("bg-blue-50");
|
|
|
|
// Press ArrowDown
|
|
fireEvent.keyDown(textarea, { key: "ArrowDown" });
|
|
|
|
// Second item should now be selected
|
|
await waitFor(() => {
|
|
const secondItem = screen.getByText("Entry Two").closest("li");
|
|
expect(secondItem).toHaveClass("bg-blue-50");
|
|
});
|
|
|
|
// Press ArrowUp
|
|
fireEvent.keyDown(textarea, { key: "ArrowUp" });
|
|
|
|
// First item should be selected again
|
|
await waitFor(() => {
|
|
const firstItem = screen.getByText("Entry One").closest("li");
|
|
expect(firstItem).toHaveClass("bg-blue-50");
|
|
});
|
|
});
|
|
|
|
it("should insert link on Enter key", async (): Promise<void> => {
|
|
const mockResults = {
|
|
data: [
|
|
{
|
|
id: "1",
|
|
slug: "test-entry",
|
|
title: "Test Entry",
|
|
summary: "A test entry",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
],
|
|
meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
|
|
};
|
|
|
|
mockApiRequest.mockResolvedValue(mockResults);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Test Entry")).toBeInTheDocument();
|
|
});
|
|
|
|
// Press Enter to select
|
|
fireEvent.keyDown(textarea, { key: "Enter" });
|
|
|
|
await waitFor(() => {
|
|
expect(onInsertMock).toHaveBeenCalledWith("[[test-entry|Test Entry]]");
|
|
});
|
|
});
|
|
|
|
it("should insert link on click", async (): Promise<void> => {
|
|
const mockResults = {
|
|
data: [
|
|
{
|
|
id: "1",
|
|
slug: "test-entry",
|
|
title: "Test Entry",
|
|
summary: "A test entry",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
],
|
|
meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
|
|
};
|
|
|
|
mockApiRequest.mockResolvedValue(mockResults);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Test Entry")).toBeInTheDocument();
|
|
});
|
|
|
|
// Click on the result
|
|
fireEvent.click(screen.getByText("Test Entry"));
|
|
|
|
await waitFor(() => {
|
|
expect(onInsertMock).toHaveBeenCalledWith("[[test-entry|Test Entry]]");
|
|
});
|
|
});
|
|
|
|
it("should close dropdown on Escape key", async (): Promise<void> => {
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("↑↓ Navigate • Enter Select • Esc Cancel")).toBeInTheDocument();
|
|
});
|
|
|
|
// Press Escape
|
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText("↑↓ Navigate • Enter Select • Esc Cancel")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should close dropdown when closing brackets are typed", async (): Promise<void> => {
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("↑↓ Navigate • Enter Select • Esc Cancel")).toBeInTheDocument();
|
|
});
|
|
|
|
// Type closing brackets
|
|
act(() => {
|
|
textarea.value = "[[test]]";
|
|
textarea.setSelectionRange(8, 8);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText("↑↓ Navigate • Enter Select • Esc Cancel")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should show 'No entries found' when search returns no results", async (): Promise<void> => {
|
|
mockApiRequest.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
|
|
});
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[nonexistent
|
|
act(() => {
|
|
textarea.value = "[[nonexistent";
|
|
textarea.setSelectionRange(13, 13);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("No entries found")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should show loading state while searching", async (): Promise<void> => {
|
|
// Mock a slow API response
|
|
let resolveSearch: (value: {
|
|
data: unknown[];
|
|
meta: { total: number; page: number; limit: number; totalPages: number };
|
|
}) => void = () => undefined;
|
|
const searchPromise = new Promise<{
|
|
data: unknown[];
|
|
meta: { total: number; page: number; limit: number; totalPages: number };
|
|
}>((resolve) => {
|
|
resolveSearch = resolve;
|
|
});
|
|
mockApiRequest.mockReturnValue(searchPromise);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Searching...")).toBeInTheDocument();
|
|
});
|
|
|
|
// Resolve the search
|
|
resolveSearch({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText("Searching...")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should display summary preview for entries", async (): Promise<void> => {
|
|
const mockResults = {
|
|
data: [
|
|
{
|
|
id: "1",
|
|
slug: "test-entry",
|
|
title: "Test Entry",
|
|
summary: "This is a helpful summary",
|
|
workspaceId: "workspace-1",
|
|
content: "Content",
|
|
contentHtml: "<p>Content</p>",
|
|
status: "PUBLISHED" as const,
|
|
visibility: "PUBLIC" as const,
|
|
createdBy: "user-1",
|
|
updatedBy: "user-1",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
tags: [],
|
|
},
|
|
],
|
|
meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
|
|
};
|
|
|
|
mockApiRequest.mockResolvedValue(mockResults);
|
|
|
|
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);
|
|
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) throw new Error("Textarea not found");
|
|
|
|
// Simulate typing [[test
|
|
act(() => {
|
|
textarea.value = "[[test";
|
|
textarea.setSelectionRange(6, 6);
|
|
fireEvent.input(textarea);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("This is a helpful summary")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|