diff --git a/e2e-tests/attach_image.spec.ts b/e2e-tests/attach_image.spec.ts new file mode 100644 index 0000000..6ff887f --- /dev/null +++ b/e2e-tests/attach_image.spec.ts @@ -0,0 +1,30 @@ +import { test } from "./helpers/test_helper"; + +// attach image is implemented in two separate components +// - HomeChatInput +// - ChatInput +// so we need to test both +test("attach image - home chat", async ({ po }) => { + await po.setUp(); + + await po + .getHomeChatInputContainer() + .locator("input[type='file']") + .setInputFiles("e2e-tests/fixtures/images/logo.png"); + await po.sendPrompt("[dump]"); + await po.snapshotServerDump({ onlyLastMessage: true }); + await po.snapshotMessages({ replaceDumpPath: true }); +}); + +test("attach image - chat", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.sendPrompt("basic"); + + await po + .getChatInputContainer() + .locator("input[type='file']") + .setInputFiles("e2e-tests/fixtures/images/logo.png"); + await po.sendPrompt("[dump]"); + await po.snapshotServerDump({ onlyLastMessage: true }); + await po.snapshotMessages({ replaceDumpPath: true }); +}); diff --git a/e2e-tests/fixtures/images/logo.png b/e2e-tests/fixtures/images/logo.png new file mode 100644 index 0000000..7f90ff4 Binary files /dev/null and b/e2e-tests/fixtures/images/logo.png differ diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index bcd2545..3eadb98 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -20,7 +20,25 @@ class PageObject { await this.selectTestModel(); } - async snapshotMessages() { + async snapshotMessages({ + replaceDumpPath = false, + }: { replaceDumpPath?: boolean } = {}) { + if (replaceDumpPath) { + // Update page so that "[[dyad-dump-path=*]]" is replaced with a placeholder path + // which is stable across runs. + await this.page.evaluate(() => { + const messagesList = document.querySelector( + "[data-testid=messages-list]", + ); + if (!messagesList) { + throw new Error("Messages list not found"); + } + messagesList.innerHTML = messagesList.innerHTML.replace( + /\[\[dyad-dump-path=([^\]]+)\]\]/, + "[[dyad-dump-path=*]]", + ); + }); + } await expect(this.page.getByTestId("messages-list")).toMatchAriaSnapshot(); } @@ -41,7 +59,9 @@ class PageObject { await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot(); } - async snapshotServerDump() { + async snapshotServerDump({ + onlyLastMessage = false, + }: { onlyLastMessage?: boolean } = {}) { // Get the text content of the messages list const messagesListText = await this.page .getByTestId("messages-list") @@ -62,7 +82,9 @@ class PageObject { const dumpContent = fs.readFileSync(dumpFilePath, "utf-8"); // Perform snapshot comparison - expect(prettifyDump(dumpContent)).toMatchSnapshot("server-dump.txt"); + expect(prettifyDump(dumpContent, { onlyLastMessage })).toMatchSnapshot( + "server-dump.txt", + ); } async waitForChatCompletion() { @@ -85,13 +107,21 @@ class PageObject { return this.page.getByRole("button", { name: "Undo" }); } + getHomeChatInputContainer() { + return this.page.getByTestId("home-chat-input-container"); + } + + getChatInputContainer() { + return this.page.getByTestId("chat-input-container"); + } + + getChatInput() { + return this.page.getByRole("textbox", { name: "Ask Dyad to build..." }); + } + async sendPrompt(prompt: string) { - await this.page - .getByRole("textbox", { name: "Ask Dyad to build..." }) - .click(); - await this.page - .getByRole("textbox", { name: "Ask Dyad to build..." }) - .fill(prompt); + await this.getChatInput().click(); + await this.getChatInput().fill(prompt); await this.page.getByRole("button", { name: "Send message" }).click(); await this.waitForChatCompletion(); } @@ -310,22 +340,29 @@ export const test = base.extend<{ ], }); -function prettifyDump(dumpContent: string) { +function prettifyDump( + dumpContent: string, + { onlyLastMessage = false }: { onlyLastMessage?: boolean } = {}, +) { const parsedDump = JSON.parse(dumpContent) as Array<{ role: string; content: string; }>; - return parsedDump + const messages = onlyLastMessage ? parsedDump.slice(-1) : parsedDump; + + return messages .map((message) => { - const content = message.content - // We remove package.json because it's flaky. - // Depending on whether pnpm install is run, it will be modified, - // and the contents and timestamp (thus affecting order) will be affected. - .replace( - /\n[\s\S]*?<\/dyad-file>\n/g, - "", - ); + const content = Array.isArray(message.content) + ? JSON.stringify(message.content) + : message.content + // We remove package.json because it's flaky. + // Depending on whether pnpm install is run, it will be modified, + // and the contents and timestamp (thus affecting order) will be affected. + .replace( + /\n[\s\S]*?<\/dyad-file>\n/g, + "", + ); return `===\nrole: ${message.role}\nmessage: ${content}`; }) .join("\n\n"); diff --git a/e2e-tests/snapshots/attach_image.spec.ts_attach-image---chat-1.aria.yml b/e2e-tests/snapshots/attach_image.spec.ts_attach-image---chat-1.aria.yml new file mode 100644 index 0000000..7b84ebe --- /dev/null +++ b/e2e-tests/snapshots/attach_image.spec.ts_attach-image---chat-1.aria.yml @@ -0,0 +1,25 @@ +- paragraph: basic +- 'button "Thinking `<dyad-write>`: I''ll think about the problem and write a bug report. <dyad-write> <dyad-write path=\"file1.txt\"> Fake dyad write </dyad-write>"': + - img + - img + - paragraph: + - code: "`<dyad-write>`" + - text: ": I'll think about the problem and write a bug report." + - paragraph: <dyad-write> + - paragraph: <dyad-write path="file1.txt"> Fake dyad write </dyad-write> +- img +- text: file1.txt +- img +- text: file1.txt +- paragraph: More EOM +- img +- text: Approved +- paragraph: "[dump]" +- paragraph: "Attachments:" +- list: + - listitem: logo.png (image/png) +- paragraph: "[[dyad-dump-path=*]]" +- img +- text: Approved +- button "Retry": + - img \ No newline at end of file diff --git a/e2e-tests/snapshots/attach_image.spec.ts_attach-image---home-chat-1.aria.yml b/e2e-tests/snapshots/attach_image.spec.ts_attach-image---home-chat-1.aria.yml new file mode 100644 index 0000000..437e0dd --- /dev/null +++ b/e2e-tests/snapshots/attach_image.spec.ts_attach-image---home-chat-1.aria.yml @@ -0,0 +1,7 @@ +- paragraph: "[dump]" +- paragraph: "Attachments:" +- list: + - listitem: logo.png (image/png) +- paragraph: "[[dyad-dump-path=*]]" +- button "Retry": + - img \ No newline at end of file diff --git a/e2e-tests/snapshots/attach_image.spec.ts_server-dump.txt b/e2e-tests/snapshots/attach_image.spec.ts_server-dump.txt new file mode 100644 index 0000000..a9b5f75 --- /dev/null +++ b/e2e-tests/snapshots/attach_image.spec.ts_server-dump.txt @@ -0,0 +1,3 @@ +=== +role: user +message: [{"type":"text","text":"[dump]\n\nAttachments:\n- logo.png (image/png)"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAACbElEQVQ4EY1TTWgTQRR+M7NpQoKmPVTPQi/qoaDWQAsxIigqRS/JzYNHxVz0oAcrG6hQoV68ePNQECUpeBFBIqQiVBrbaqvgoXjRSzVJf8zfzu7MG2eWbGywFN8e3ux73/fNm/dmAP7TFCgSQHeurSC4t1eEAFET4+WjzPIq5AX5ZURMjO5GNEk7VbJKKWU9Or8WBg2cvPRxlLXiX7arVtFWNjVkzSX/VJBPK0YKRMIciI6477kjhsDrAxfJoebVUwd0bl1vBD0ChpzR5JkrKzGvjum2B8MtrphLRXGTY5RKCaioi7wr/lcgID+//Omsu4EzERg4GIIoWAygxrezwOvNhmiAoCrkCtZtqF9BPp33d86nl46jw173aWKtXWtzWi21kELDgzOo+mJCCRBIFIowBr3rOQK4bDJC90PVrf5Ayi/efDP62QBvn14Y5m0sKogOCoWy/nvL55syqOl4ppCRT6+9GxAKjgmtLYg3ldVkM4Hs0Fr4QSmxwi28T1gUHE+J9rpGdozqRvoWcmKWUMpyEXWH2IYJiq0KtQYr/qgRWc3T4lJ/IbNVx/xmmCrMXJ+ML3+wZP+Jmre5MN8/NVYoFKTB2XbJ+vYyPi9l/4hLq+XZpZMJ0BxzP3z1XGpO99qUDg897VFGEkd+3lm9lVy8e2NsceL7q/iqwvCIohIYU9MGm+pwuuOwbUVtm+D0uXIO5b57noxCWzJoSQJNIaAhm8BxMze7PGbboLFA/El0BYxqcJTJC+Vkg5PrLU4PO1KBg/jVo87jZ++Tb4PSDX5XMxdq14QOpvfI9XDMxTKPKQim9DqtY8H/Tv8HGFE+AZtzYdAAAAAASUVORK5CYII="}}] \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index 7c14f89..a9561e7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -28,7 +28,7 @@ const config: PlaywrightTestConfig = { }, webServer: { - command: `cd testing/fake-llm-server && npm start`, + command: `cd testing/fake-llm-server && npm run build && npm start`, url: "http://localhost:3500/health", }, }; diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 9086261..0ce0ea9 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -253,7 +253,7 @@ export function ChatInput({ chatId }: { chatId?: number }) { Error loading proposal: {proposalError} )} -
+
-
+
+ part.type === "text" && part.text.includes("[dump]"), + ) + : lastMessage.content.includes("[dump]")) + ) { const timestamp = Date.now(); const generatedDir = path.join(__dirname, "generated"); @@ -241,6 +250,7 @@ function chatCompletionHandler(req: Request, res: Response) { if ( lastMessage && lastMessage.content && + typeof lastMessage.content === "string" && lastMessage.content.startsWith("tc=") ) { const testCaseName = lastMessage.content.slice(3); // Remove "tc=" prefix