import { expect, test } from "@odoo/hoot"; import { setupEditor } from "./_helpers/editor"; import { getActiveElement, press, queryAll, queryOne, waitFor } from "@odoo/hoot-dom"; import { animationFrame } from "@odoo/hoot-mock"; import { contains, onRpc } from "@web/../tests/web_test_helpers"; import { insertText } from "./_helpers/user_actions"; import { getContent } from "./_helpers/selection"; import { ChatGPTPlugin } from "../src/main/chatgpt/chatgpt_plugin"; import { loadLanguages } from "@web/core/l10n/translation"; import { MAIN_PLUGINS } from "@html_editor/plugin_sets"; import { DEFAULT_ALTERNATIVES_MODES } from "../src/main/chatgpt/chatgpt_alternatives_dialog"; import { execCommand } from "./_helpers/userCommands"; const PROMPT_DIALOG_TITLE = "Generate Text with AI"; const ALTERNATIVES_DIALOG_TITLE = "AI Copywriter"; const TRANSLATE_DIALOG_TITLE = "Translate with AI"; const openFromPowerbox = async (editor) => { await insertText(editor, "/ChatGPT"); await animationFrame(); await press("Enter"); }; const openFromToolbar = async () => { await contains(".o-we-toolbar .btn[name='chatgpt']").click(); }; const translateButtonFromToolbar = async () => { await contains(".o-we-toolbar .btn[name='translate']").click(); }; const translateDropdownFromToolbar = async () => { await contains(".lang:contains('French (BE) / Français (BE)')").click(); }; test("ChatGPT dialog opens in prompt mode when selection is collapsed (with Powerbox)", async () => { const { editor } = await setupEditor("
te[]st
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); // Select ChatGPT in the Powerbox. await openFromPowerbox(editor); // Expect the ChatGPT Prompt Dialog to be open. const promptDialogHeaderSelector = `.o_dialog .modal-header:contains("${PROMPT_DIALOG_TITLE}")`; await waitFor(promptDialogHeaderSelector); // Expect the ChatGPT Alternatives Dialog not to be open. const alternativesDialogHeaderSelector = `.o_dialog .modal-header:contains("${ALTERNATIVES_DIALOG_TITLE}")`; expect(alternativesDialogHeaderSelector).toHaveCount(0); }); test("ChatGPT dialog opens in alternatives mode when selection is not collapsed (with toolbar)", async () => { await setupEditor("te[s]t
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); // Select ChatGPT in the toolbar. await openFromToolbar(); // Expect the ChatGPT Alternatives Dialog to be open. const alternativesDialogHeaderSelector = `.o_dialog .modal-header:contains("${ALTERNATIVES_DIALOG_TITLE}")`; await waitFor(alternativesDialogHeaderSelector); // Expect the ChatGPT Prompt Dialog not to be open. const promptDialogHeaderSelector = `.o_dialog .modal-header:contains("${PROMPT_DIALOG_TITLE}")`; expect(promptDialogHeaderSelector).toHaveCount(0); }); test("ChatGPT dialog opens in translate mode when clicked on translate button in toolbar", async () => { await setupEditor("te[s]t
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); // Expect the toolbar to not have translate dropdown. expect(".o-we-toolbar [name='translate'].o-dropdown").toHaveCount(0); // Expect the toolbar to have translate button. expect(".o-we-toolbar .btn[name='translate']").toHaveCount(1); // Select Translate button in the toolbar. await translateButtonFromToolbar(); // Expect the ChatGPT Translate Dialog to be open. const translateDialogHeaderSelector = `.o_dialog .modal-header:contains("${TRANSLATE_DIALOG_TITLE}")`; await waitFor(translateDialogHeaderSelector); // Expect the ChatGPT Alternatives Dialog not to be open. const alternativesDialogHeaderSelector = `.o_dialog .modal-header:contains("${ALTERNATIVES_DIALOG_TITLE}")`; expect(alternativesDialogHeaderSelector).toHaveCount(0); }); test("ChatGPT dialog opens in translate mode when clicked on translate dropdown in toolbar", async () => { loadLanguages.installedLanguages = false; onRpc("/web/dataset/call_kw/res.lang/get_installed", () => { return [ ["en_US", "English (US)"], ["fr_BE", "French (BE) / Français (BE)"], ]; }); await setupEditor("te[s]t
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); // Expect the toolbar to have translate dropdown. expect(".o-we-toolbar [name='translate'].o-dropdown").toHaveCount(1); // Select Translate button in the toolbar. await translateButtonFromToolbar(); await waitFor(".dropdown-menu"); await translateDropdownFromToolbar(); // Expect the ChatGPT Translate Dialog to be open. const translateDialogHeaderSelector = `.o_dialog .modal-header:contains("${TRANSLATE_DIALOG_TITLE}")`; await waitFor(translateDialogHeaderSelector); // Expect the ChatGPT Alternatives Dialog not to be open. const alternativesDialogHeaderSelector = `.o_dialog .modal-header:contains("${ALTERNATIVES_DIALOG_TITLE}")`; expect(alternativesDialogHeaderSelector).toHaveCount(0); loadLanguages.installedLanguages = false; }); test("ChatGPT alternatives dialog generates alternatives for each button", async () => { const { editor } = await setupEditor("[test]
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); let rpcIndex = 1; onRpc("/html_editor/generate_text", () => `Alternative #${rpcIndex++}`); // Select ChatGPT in the toolbar. await openFromToolbar(); // Expect 3 alternatives to have been generated and to be displayed in // reverse order. await waitFor(".o-chatgpt-alternative"); let alternativesCount = 3; expect(".o-chatgpt-alternative").toHaveCount(alternativesCount); const alternatives = editor.document.querySelectorAll(".o-chatgpt-alternative"); expect(getContent(alternatives[0])).toBe("Alternative #3
"); expect(getContent(alternatives[1])).toBe("Alternative #2
"); expect(getContent(alternatives[2])).toBe("Alternative #1
"); // Click on each button and expect a new alternative to be generated each // time. const buttons = editor.document.querySelectorAll("main.modal-body button[data-mode]"); for (const button of buttons) { const dataMode = button.getAttribute("data-mode"); await contains(`main.modal-body button[data-mode=${dataMode}]`).click(); alternativesCount += 1; expect(".o-chatgpt-alternative").toHaveCount(alternativesCount); const newAlternative = editor.document.querySelector(".o-chatgpt-alternative"); expect(getContent(newAlternative)).toBe( `${DEFAULT_ALTERNATIVES_MODES[dataMode]}` + `Alternative #${alternativesCount}
` ); } }); test("insert the response from ChatGPT prompt dialog", async () => { const { editor, el } = await setupEditor("te[]st
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); onRpc( "/html_editor/generate_text", () => ` Here you go! Enjoy :-) ` ); // Select ChatGPT in the Powerbox. await openFromPowerbox(editor); // Ask it to generate a list. await contains(".o_dialog textarea").edit("Write something."); await contains("footer button.btn").click(); // Insert the response. await waitFor(".o-message-insert"); expect(".o-message-insert").toHaveCount(1); // There shouldn't be more that one. await contains(".o-message-insert").click(); // Expect the response to have been inserted in the middle of the text. expect(getContent(el)).toBe(`teHere you go!
Enjoy :-)[]st
`); }); test("insert the response from ChatGPT alternatives dialog", async () => { const { el } = await setupEditor("t[es]t
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); let rpcIndex = 1; onRpc("/html_editor/generate_text", () => `Alternative #${rpcIndex++}`); // Select ChatGPT in the Powerbox. await openFromToolbar(); // Insert the top response. await waitFor(".o-chatgpt-alternative"); expect(".o-chatgpt-alternative").toHaveCount(3); expect("footer button.btn[disabled]").toHaveCount(1); await contains(".o-chatgpt-alternative").click(); expect("footer button.btn[disabled]").toHaveCount(0); await contains("footer button.btn").click(); // Expect the response to have replaced the selected text. expect(getContent(el)).toBe(`tAlternative #3[]t
`); }); test("insert the response from ChatGPT translate dialog", async () => { loadLanguages.installedLanguages = false; onRpc("/web/dataset/call_kw/res.lang/get_installed", () => { return [ ["en_US", "English (US)"], ["fr_BE", "French (BE) / Français (BE)"], ]; }); const { editor, el } = await setupEditor("[Hello]
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); onRpc("/html_editor/generate_text", () => `Bonjour`); // Select Translate button in the toolbar. await translateButtonFromToolbar(); await waitFor(".dropdown-menu"); await translateDropdownFromToolbar(); // Insert the response. await waitFor(".o-chatgpt-translated"); expect("footer button.btn[disabled]").toHaveCount(0); await contains("footer button.btn").click(); // Expect the response to have been inserted in the middle of the text. expect(getContent(el)).toBe(`Bonjour[]
`); loadLanguages.installedLanguages = false; // Expect to undo and redo the inserted text. execCommand(editor, "historyUndo"); expect(getContent(el)).toBe(`[Hello]
`); execCommand(editor, "historyRedo"); expect(getContent(el)).toBe(`Bonjour[]
`); }); test("ChatGPT prompt dialog properly formats an unordered list", async () => { const { editor } = await setupEditor("te[]st
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); onRpc( "/html_editor/generate_text", () => ` - First item - Second item - Third item ` ); // Select ChatGPT in the Powerbox. await openFromPowerbox(editor); // Ask it to generate a list. await contains(".o_dialog textarea").edit("Generate a list."); await contains("footer button.btn").click(); // Expect it to recognize the response is a list and format it. await waitFor(".o-chatgpt-message"); expect(".o-chatgpt-message").toHaveCount(2); // user message + response expect(getContent(editor.document.querySelectorAll(".o-chatgpt-message div")[1])).toBe( `[abc]
"); await waitFor(".o-we-toolbar"); // Icon should be present. expect(".o-we-toolbar .btn[name='chatgpt'] span.fa-magic").toHaveCount(1); // Text should be present. expect(".o-we-toolbar .btn[name='chatgpt']").toHaveText("AI"); }); test("Translate button should be positioned before ChatGPT button in toolbar", async () => { await setupEditor("[abc]
"); await waitFor(".o-we-toolbar"); const buttons = queryAll(".o-we-toolbar .btn-group[name='ai'] .btn"); expect(buttons).toHaveCount(2); expect(buttons[0]).toHaveAttribute("name", "translate"); expect(buttons[1]).toHaveAttribute("name", "chatgpt"); }); test("press escape to close ChatGPT dialog", async () => { const { editor, el } = await setupEditor("te[]st
", { config: { Plugins: [...MAIN_PLUGINS, ChatGPTPlugin] }, }); // Select ChatGPT in the Powerbox. await openFromPowerbox(editor); // Expect the ChatGPT Prompt Dialog to be open. const promptDialogHeaderSelector = `.o_dialog .modal-header:contains("${PROMPT_DIALOG_TITLE}")`; await waitFor(promptDialogHeaderSelector); expect(getActiveElement()).toBe(queryOne('.modal [name="promptInput"]')); await press("escape"); await animationFrame(); expect(promptDialogHeaderSelector).toHaveCount(0); expect(getContent(el)).toBe("te[]st
"); }); test("AI is an alias to ChatGPT command in the Powerbox", async () => { const { editor } = await setupEditor("[]
[]