Skip to content

Commit

Permalink
ChatWrapper, tests and docs
Browse files Browse the repository at this point in the history
ChatWrapper, tests and docs
  • Loading branch information
edspencer authored Aug 23, 2024
2 parents d664315 + 4b0532f commit 709d301
Show file tree
Hide file tree
Showing 20 changed files with 968 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-melons-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inform-ai": patch
---

More jest tests
5 changes: 5 additions & 0 deletions .changeset/funny-tomatoes-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inform-ai": minor
---

Built-in ChatWrapper component
5 changes: 5 additions & 0 deletions .changeset/thick-moons-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inform-ai": patch
---

Docs and tests
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const jestConfig: JestConfigWithTsJest = {
// "^.+\\.(t|j)sx?$": ["@swc/jest"],
},
moduleNameMapper: {
"^ai/rsc$": "<rootDir>/node_modules/ai/rsc/dist",
"^@/(.*)$": "<rootDir>/$1",
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
},
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@
"@types/react": "npm:[email protected]",
"@types/react-dom": "npm:[email protected]",
"@types/uuid": "^10.0.0",
"ai": "^3.3.6",
"autoprefixer": "^10.4.20",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.41",
Expand All @@ -56,7 +58,6 @@
"@ai-sdk/openai": "^0.0.44",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0",
"ai": "^3.3.6",
"clsx": "^2.1.1",
"nanoid": "^5.0.7",
"react": "19.0.0-rc-cc1ec60d0d-20240607",
Expand All @@ -65,5 +66,8 @@
"rollup-plugin-postcss": "^4.0.2",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"peerDependencies": {
"ai": "^3.3.6"
}
}
22 changes: 19 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions src/InformAIContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { createContext, useContext, useState, ReactNode } from "react";
import { createContext, useContext, useState, ReactNode } from "react";
import { randomId } from "./utils";

import { StateMessage, EventMessage, Message, ComponentState, ComponentEvent, Conversation } from "./types";
Expand Down Expand Up @@ -40,7 +40,20 @@ interface InformAIProviderProps {
}

/**
* The internal implementation of the InformAIProvider component.
* The internal implementation of the InformAIProvider component. Sample usage:
*
* import { InformAIProvider } from 'inform-ai';
*
* export default function MyComponent() {
* return (
* <InformAIProvider>
* {children}
* </InformAIProvider>
* );
* }
*
* Now within child React components you can use useInformAI() or <InformAI /> to surface
* information about your components to the LLM.
*/
export const InformAIProvider = ({ children, onEvent }: InformAIProviderProps) => {
const [messages, setMessages] = useState<Message[]>([]);
Expand Down
54 changes: 54 additions & 0 deletions src/test/ChatBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ChatBox } from "../ui/ChatBox";

describe("ChatBox", () => {
it("renders correctly", () => {
render(<ChatBox onSubmit={async () => true} />);
expect(screen.getByRole("textbox")).toBeInTheDocument();
});

it("honors autoFocus", () => {
render(<ChatBox onSubmit={async () => true} autoFocus={true} />);
expect(screen.getByRole("textbox")).toHaveFocus();
});

it("honors placeholder", () => {
render(<ChatBox onSubmit={async () => true} placeholder="test placeholder" />);
expect(screen.getByPlaceholderText("test placeholder")).toBeInTheDocument();
});

it('clears the input if "onSubmit" returns true', async () => {
const onSubmit = jest.fn(async () => true);

render(<ChatBox onSubmit={onSubmit} />);
const input = screen.getByRole("textbox");
const button = screen.getByRole("button");

fireEvent.change(input, { target: { value: "test" } });
expect(input).toHaveValue("test");

fireEvent.click(button);

await waitFor(() => {
expect(input).toHaveValue("");
});
expect(onSubmit).toHaveBeenCalledTimes(1);
});

it('does not clear the input if "onSubmit" returns false', async () => {
const onSubmit = jest.fn(async () => false);

render(<ChatBox onSubmit={onSubmit} />);
const input = screen.getByRole("textbox");
const form = screen.getByRole("form");

fireEvent.change(input, { target: { value: "test" } });

expect(input).toHaveValue("test");

fireEvent.submit(form);

expect(onSubmit).toHaveBeenCalledTimes(1);
expect(input).toHaveValue("test");
});
});
136 changes: 136 additions & 0 deletions src/test/ChatWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { ChatWrapper } from "../ui/ChatWrapper";
import { InformAIProvider, useInformAIContext } from "../InformAIContext";
import { useState } from "react";
import { useInformAI } from "../useInformAI";
import { FormattedMessage } from "../types";

describe("ChatWrapper", () => {
let currentMessages: any[];

const AppChatWrapper = ({ submitUserMessage = jest.fn() }: { submitUserMessage?: jest.Mock }) => {
const [messages, setMessages] = useState<any[]>([]);

currentMessages = messages;

return <ChatWrapper setMessages={setMessages} submitUserMessage={submitUserMessage} messages={messages} />;
};

it("renders correctly", () => {
render(
<InformAIProvider>
<AppChatWrapper />
</InformAIProvider>
);

//just checks that the component renders the ChatBox
expect(screen.getByRole("textbox")).toBeInTheDocument();
});

it("renders user messages correctly", async () => {
render(
<InformAIProvider>
<AppChatWrapper />
</InformAIProvider>
);

fireEvent.change(screen.getByRole("textbox"), {
target: { value: "test message" },
});
fireEvent.submit(screen.getByRole("form"));

await waitFor(() => screen.getByText("test message"));
});

describe("collecting messages to send to the LLM", () => {
let contextValues: ReturnType<typeof useInformAIContext> | undefined = undefined;
let mockSubmitUserMessage: jest.Mock;

const AppComponent = () => {
const { addState } = useInformAI({
name: "MyAppComponent",
props: { key: "value" },
prompt: "MyAppComponent prompt",
});

contextValues = useInformAIContext();

const handleClick = () => {
addState({ props: { key: "newValue" } });
};

return <div onClick={handleClick}>clickable element</div>;
};

beforeEach(async () => {
mockSubmitUserMessage = jest.fn(async () => ({
id: "response-id",
content: "response message",
role: "assistant",
}));

render(
<InformAIProvider>
<AppComponent />
<AppChatWrapper submitUserMessage={mockSubmitUserMessage} />
</InformAIProvider>
);

//after this we should have 2 state messages in the context
fireEvent.click(screen.getByText("clickable element"));

expect(contextValues!.messages.length).toBe(2);

//submit a message from the user
fireEvent.change(screen.getByRole("textbox"), {
target: { value: "test message" },
});

await act(async () => {
fireEvent.submit(screen.getByRole("form"));
});
});

it("sends the correct deduped state and user messages to submitUserMessage", async () => {
//test that the correct messages were sent to submitUserMessage
expect(mockSubmitUserMessage).toHaveBeenCalledTimes(1);
const [submittedMessages] = mockSubmitUserMessage.mock.calls[0];

//the 2 state messages for the component should have been deduped into 1 (plus the user message)
expect(submittedMessages.length).toBe(2);

const stateMessage = submittedMessages[0] as FormattedMessage;
expect(stateMessage.content).toContain('Component props: {"key":"newValue"}');
expect(stateMessage.role).toEqual("system");
expect(stateMessage.id.length).toEqual(10);

const userMessage = submittedMessages[1] as FormattedMessage;
expect(userMessage.content).toEqual("test message");
expect(userMessage.role).toEqual("user");
expect(userMessage.id.length).toEqual(10);
});

it("ends up with the correct messages", async () => {
//we should end up with 3 messages - a state message, a user message, and an LLM response
expect(currentMessages.length).toBe(3);

const stateMessage = currentMessages[0] as FormattedMessage;
expect(stateMessage.content).toContain('Component props: {"key":"newValue"}');
expect(stateMessage.role).toEqual("system");
expect(stateMessage.id.length).toEqual(10);

const userMessage = currentMessages[1] as FormattedMessage;
expect(userMessage.role).toEqual("user");
expect(userMessage.id.length).toEqual(10);

const responseMessage = currentMessages[2] as FormattedMessage;
expect(responseMessage.role).toEqual("assistant");
expect(responseMessage.content).toEqual("response message");
expect(responseMessage.id).toEqual("response-id");
});

it("clears the text input", async () => {
expect(screen.getByRole("textbox")).toHaveValue("");
});
});
});
Loading

0 comments on commit 709d301

Please sign in to comment.