diff --git a/packages/instantsearch.js/src/widgets/chat/__tests__/chat.test.tsx b/packages/instantsearch.js/src/widgets/chat/__tests__/chat.test.tsx
index e73a493c21..6e3f335e2d 100644
--- a/packages/instantsearch.js/src/widgets/chat/__tests__/chat.test.tsx
+++ b/packages/instantsearch.js/src/widgets/chat/__tests__/chat.test.tsx
@@ -3,8 +3,6 @@
  */
 /** @jsx h */
 import { createSearchClient } from '@instantsearch/mocks';
-import { wait } from '@instantsearch/testutils';
-import userEvent from '@testing-library/user-event';
 
 import instantsearch from '../../../index.es';
 import chat from '../chat';
@@ -29,32 +27,5 @@ describe('chat', () => {
         See documentation: https://www.algolia.com/doc/api-reference/widgets/chat/js/"
       `);
     });
-
-    test('adds custom CSS classes', async () => {
-      const container = document.createElement('div');
-      const searchClient = createSearchClient();
-
-      const search = instantsearch({ indexName: 'indexName', searchClient });
-      const widget = chat({
-        container,
-        agentId: 'agentId',
-        cssClasses: {
-          root: 'ROOT',
-          container: 'CONTAINER',
-        },
-      });
-
-      search.addWidgets([widget]);
-      search.start();
-      await wait(0);
-
-      userEvent.click(container.querySelector('.ais-ChatToggleButton')!);
-      await wait(0);
-
-      expect(container.querySelector('.ais-Chat')).toHaveClass('ROOT');
-      expect(container.querySelector('.ais-Chat-container')).toHaveClass(
-        'CONTAINER'
-      );
-    });
   });
 });
diff --git a/packages/react-instantsearch/src/widgets/__tests__/Chat.test.tsx b/packages/react-instantsearch/src/widgets/__tests__/Chat.test.tsx
deleted file mode 100644
index 3f8ec4a9f5..0000000000
--- a/packages/react-instantsearch/src/widgets/__tests__/Chat.test.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * @jest-environment @instantsearch/testutils/jest-environment-jsdom.ts
- */
-
-import { InstantSearchTestWrapper } from '@instantsearch/testutils';
-import { render } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { SearchIndexToolType } from 'instantsearch.js/es/lib/chat';
-import React from 'react';
-
-import { Chat } from '../Chat';
-
-let mockUseChat: any;
-jest.mock('react-instantsearch-core', () => {
-  const originalModule = jest.requireActual('react-instantsearch-core');
-  return {
-    ...originalModule,
-    useChat: jest.fn(() => mockUseChat),
-  };
-});
-
-describe('Chat', () => {
-  test('opens chat when toggle button is clicked', () => {
-    mockUseChat = {
-      messages: [],
-      sendMessage: jest.fn(),
-      addToolResult: jest.fn(),
-    };
-    const { container } = render(
-      
-        
-      
-    );
-
-    const toggleButton = container.querySelector('.ais-ChatToggleButton');
-    userEvent.click(toggleButton!);
-
-    const root = container.querySelector('.ais-Chat');
-    expect(root).toHaveClass('ROOT');
-  });
-
-  test('closes chat when close button is clicked', () => {
-    mockUseChat = {
-      messages: [],
-      sendMessage: jest.fn(),
-      addToolResult: jest.fn(),
-    };
-    const { container } = render(
-      
-        
-      
-    );
-
-    const toggleButton = container.querySelector('.ais-ChatToggleButton');
-    userEvent.click(toggleButton!);
-    const closeButton = container.querySelector('.ais-ChatHeader-close');
-    userEvent.click(closeButton!);
-
-    const root = container.querySelector('.ais-Chat-container');
-    expect(root).not.toHaveClass('ais-Chat-container--open');
-  });
-
-  test('should send message when form is submitted', () => {
-    const sendMessage = jest.fn();
-    mockUseChat = {
-      messages: [],
-      sendMessage,
-      addToolResult: jest.fn(),
-    };
-    const { container } = render(
-      
-        
-      
-    );
-
-    const toggleButton = container.querySelector('.ais-ChatToggleButton');
-    userEvent.click(toggleButton!);
-
-    const textarea = container.querySelector(
-      '.ais-ChatPrompt-textarea'
-    ) as HTMLTextAreaElement;
-    userEvent.type(textarea, 'Hello, world!');
-
-    const submitBtn = container.querySelector('.ais-ChatPrompt-submit');
-    userEvent.click(submitBtn!);
-
-    expect(sendMessage).toHaveBeenCalledTimes(1);
-    expect(sendMessage).toHaveBeenCalledWith({ text: 'Hello, world!' });
-    expect(textarea.value).toBe('');
-  });
-
-  test('should render tools and call onToolCall', () => {
-    mockUseChat = {
-      messages: [
-        { id: '0', role: 'user', parts: [{ type: 'text', content: 'Hello' }] },
-        { id: '1', role: 'assistant', parts: [{ type: 'tool-hello' }] },
-      ],
-      sendMessage: jest.fn(),
-    };
-    const onToolCall = jest.fn();
-    const { container } = render(
-      
-         (
-                
-                  
The message said hello!
-                
-                  
Custom search index tool
-                
The message said hello!
',
-                },
-              },
-            },
-          },
-          react: {
-            ...commonWidgetParams,
-            tools: {
-              hello: {
-                layoutComponent: () => (
-                  The message said hello!
-                ),
-              },
-            },
-          },
+          javascript: createDefaultWidgetParams(),
+          react: createDefaultWidgetParams(),
           vue: {},
         },
       });
 
-      await act(async () => {
-        await wait(0);
-      });
+      await openChat(act);
 
-      userEvent.click(document.querySelector('.ais-ChatToggleButton')!);
+      userEvent.click(document.querySelector('.ais-ChatHeader-maximize')!);
 
-      await act(async () => {
-        await wait(0);
-      });
+      expect(document.querySelector('.ais-Chat')).toHaveClass(
+        'ais-Chat--maximized'
+      );
+      expect(document.querySelector('.ais-Chat-container')).toHaveClass(
+        'ais-Chat-container--maximized'
+      );
+
+      userEvent.click(document.querySelector('.ais-ChatHeader-maximize')!);
 
-      expect(document.querySelector('#tool-content')!.textContent).toBe(
-        'The message said hello!'
+      expect(document.querySelector('.ais-Chat')).not.toHaveClass(
+        'ais-Chat--maximized'
+      );
+      expect(document.querySelector('.ais-Chat-container')).not.toHaveClass(
+        'ais-Chat-container--maximized'
       );
     });
 
-    test('renders with custom algolia search tool', async () => {
-      const searchClient = createSearchClient();
+    describe('cssClasses', () => {
+      test('adds custom CSS classes', async () => {
+        const searchClient = createSearchClient();
 
-      const chat = new Chat({
-        messages: [
-          {
-            id: '1',
-            role: 'assistant',
-            parts: [
-              {
-                type: `tool-${SearchIndexToolType}`,
-                toolCallId: '1',
-                input: { text: 'hello' },
-                state: 'output-available',
-                output: 'hello',
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              cssClasses: {
+                root: 'ROOT',
+                container: 'CONTAINER',
               },
-            ],
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              classNames: {
+                root: 'ROOT',
+                container: 'CONTAINER',
+              },
+            },
+            vue: {},
           },
-        ],
-        id: 'chat-id',
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.ais-Chat')).toHaveClass('ROOT');
+        expect(document.querySelector('.ais-Chat-container')).toHaveClass(
+          'CONTAINER'
+        );
       });
-      const commonWidgetParams = {
-        agentId: 'agentId',
-        chat: chat as any,
-      };
+    });
 
-      await setup({
-        instantSearchOptions: {
-          indexName: 'indexName',
-          searchClient,
-        },
-        widgetParams: {
-          javascript: {
-            ...commonWidgetParams,
-            tools: {
-              [SearchIndexToolType]: {
-                templates: {
-                  layout:
-                    'The message said hello!
',
+    describe('tools', () => {
+      test('renders with default tools', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({
+          messages: [
+            {
+              id: '1',
+              role: 'assistant',
+              parts: [
+                {
+                  type: `tool-${SearchIndexToolType}`,
+                  toolCallId: '1',
+                  input: { text: 'test' },
+                  state: 'output-available',
+                  output: { hits: [{ objectID: '123' }] },
                 },
-              },
+              ],
             },
+          ],
+          id: 'chat-id',
+        });
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: createDefaultWidgetParams(chat),
+            react: createDefaultWidgetParams(chat),
+            vue: {},
           },
-          react: {
-            ...commonWidgetParams,
-            tools: {
-              [SearchIndexToolType]: {
-                layoutComponent: () => (
-                  The message said hello!
-                ),
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.ais-Carousel')).toBeInTheDocument();
+      });
+
+      test('renders with client side tools', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({
+          messages: [
+            {
+              id: '1',
+              role: 'assistant',
+              parts: [
+                {
+                  type: 'tool-hello',
+                  toolCallId: '1',
+                  input: { text: 'hello' },
+                  state: 'output-available',
+                  output: 'hello',
+                },
+              ],
+            },
+          ],
+          id: 'chat-id',
+        });
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              tools: {
+                hello: {
+                  templates: {
+                    layout:
+                      'The message said hello!
',
+                  },
+                },
               },
             },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              tools: {
+                hello: {
+                  layoutComponent: () => (
+                    The message said hello!
+                  ),
+                },
+              },
+            },
+            vue: {},
           },
-          vue: {},
-        },
-      });
+        });
 
-      await act(async () => {
-        await wait(0);
+        await openChat(act);
+
+        expect(document.querySelector('#tool-content')!.textContent).toBe(
+          'The message said hello!'
+        );
       });
 
-      userEvent.click(document.querySelector('.ais-ChatToggleButton')!);
+      test('renders with custom algolia search tool', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({
+          messages: [
+            {
+              id: '1',
+              role: 'assistant',
+              parts: [
+                {
+                  type: `tool-${SearchIndexToolType}`,
+                  toolCallId: '1',
+                  input: { text: 'hello' },
+                  state: 'output-available',
+                  output: 'hello',
+                },
+              ],
+            },
+          ],
+          id: 'chat-id',
+        });
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              tools: {
+                [SearchIndexToolType]: {
+                  templates: {
+                    layout:
+                      'The message said hello!
',
+                  },
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              tools: {
+                [SearchIndexToolType]: {
+                  layoutComponent: () => (
+                    The message said hello!
+                  ),
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
 
-      await act(async () => {
-        await wait(0);
+        expect(document.querySelector('#tool-content')!.textContent).toBe(
+          'The message said hello!'
+        );
       });
-
-      expect(document.querySelector('#tool-content')!.textContent).toBe(
-        'The message said hello!'
-      );
     });
   });
 }
diff --git a/tests/common/widgets/chat/templates.tsx b/tests/common/widgets/chat/templates.tsx
new file mode 100644
index 0000000000..3474cfddd3
--- /dev/null
+++ b/tests/common/widgets/chat/templates.tsx
@@ -0,0 +1,343 @@
+import { createSearchClient } from '@instantsearch/mocks';
+import { Chat } from 'instantsearch.js/es/lib/chat';
+import React from 'react';
+
+import { createDefaultWidgetParams, openChat } from './utils';
+
+import type { ChatWidgetSetup } from '.';
+import type { TestOptions } from '../../common';
+
+export function createTemplatesTests(
+  setup: ChatWidgetSetup,
+  { act }: Required
+) {
+  describe('templates', () => {
+    describe('header', () => {
+      test('renders with custom header template', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              templates: {
+                header: {
+                  layout: '',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              headerComponent: () => (
+                Custom header
+              ),
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.custom-header')!.textContent).toBe(
+          'Custom header'
+        );
+      });
+
+      test('renders with custom sub components', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              templates: {
+                header: {
+                  titleIcon:
+                    'Custom title icon',
+                  closeIcon:
+                    'Custom close icon',
+                  minimizeIcon:
+                    'Custom minimize icon',
+                  maximizeIcon:
+                    'Custom maximize icon',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              headerTitleIconComponent: () => (
+                Custom title icon
+              ),
+              headerCloseIconComponent: () => (
+                Custom close icon
+              ),
+              headerMinimizeIconComponent: () => (
+                
+                  Custom minimize icon
+                
+              ),
+              headerMaximizeIconComponent: () => (
+                
+                  Custom maximize icon
+                
+              ),
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.custom-title-icon')!.textContent).toBe(
+          'Custom title icon'
+        );
+        expect(document.querySelector('.custom-close-icon')!.textContent).toBe(
+          'Custom close icon'
+        );
+        expect(
+          document.querySelector('.custom-maximize-icon')!.textContent
+        ).toBe('Custom maximize icon');
+      });
+    });
+
+    describe('prompt', () => {
+      test('renders with custom prompt template', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              templates: {
+                prompt: {
+                  layout: 'Custom prompt
',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              promptComponent: () => (
+                Custom prompt
+              ),
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.custom-prompt')!.textContent).toBe(
+          'Custom prompt'
+        );
+      });
+
+      test('renders with custom sub components', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              templates: {
+                prompt: {
+                  header: '',
+                  footer: '',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              promptHeaderComponent: () => (
+                Custom header
+              ),
+              promptFooterComponent: () => (
+                Custom footer
+              ),
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.custom-header')!.textContent).toBe(
+          'Custom header'
+        );
+        expect(document.querySelector('.custom-footer')!.textContent).toBe(
+          'Custom footer'
+        );
+      });
+    });
+
+    describe('messages', () => {
+      test('renders with custom loader', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({});
+        Object.defineProperty(chat, 'status', {
+          get: () => 'submitted',
+        });
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              templates: {
+                messages: {
+                  loader: 'Custom loader
',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              messagesLoaderComponent: () => (
+                Custom loader
+              ),
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.custom-loader')!.textContent).toBe(
+          'Custom loader'
+        );
+      });
+
+      test('renders with custom error', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({});
+        Object.defineProperty(chat, 'status', {
+          get: () => 'error',
+        });
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              templates: {
+                messages: {
+                  error: 'Custom error
',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              messagesErrorComponent: () => (
+                Custom error
+              ),
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(document.querySelector('.custom-error')!.textContent).toBe(
+          'Custom error'
+        );
+      });
+    });
+
+    test('renders with custom actions', async () => {
+      const searchClient = createSearchClient();
+      const chat = new Chat({
+        messages: [
+          {
+            id: '0',
+            role: 'user',
+            parts: [
+              {
+                type: 'text',
+                text: 'hello',
+              },
+            ],
+          },
+          {
+            id: '1',
+            role: 'assistant',
+            parts: [
+              {
+                type: 'tool-hello',
+                toolCallId: '1',
+                input: { text: 'hello' },
+                state: 'output-available',
+                output: 'hello',
+              },
+            ],
+          },
+        ],
+      });
+
+      await setup({
+        instantSearchOptions: {
+          indexName: 'indexName',
+          searchClient,
+        },
+        widgetParams: {
+          javascript: {
+            ...createDefaultWidgetParams(chat),
+            templates: {
+              actions: ({ message }, { html }) =>
+                message.role === 'assistant'
+                  ? html``
+                  : html``,
+            },
+          },
+          react: {
+            ...createDefaultWidgetParams(chat),
+            actionsComponent: ({ message }) => {
+              return message.role === 'assistant' ? (
+                
+              ) : (
+                
+              );
+            },
+          },
+          vue: {},
+        },
+      });
+
+      await openChat(act);
+
+      expect(document.querySelectorAll('.custom-action')[0].textContent).toBe(
+        'Custom user action'
+      );
+      expect(document.querySelectorAll('.custom-action')[1].textContent).toBe(
+        'Custom assistant action'
+      );
+    });
+  });
+}
diff --git a/tests/common/widgets/chat/translations.tsx b/tests/common/widgets/chat/translations.tsx
new file mode 100644
index 0000000000..c097db6d92
--- /dev/null
+++ b/tests/common/widgets/chat/translations.tsx
@@ -0,0 +1,335 @@
+import { createSearchClient } from '@instantsearch/mocks';
+import userEvent from '@testing-library/user-event';
+import { Chat } from 'instantsearch.js/es/lib/chat';
+
+import { createDefaultWidgetParams, openChat } from './utils';
+
+import type { ChatWidgetSetup } from '.';
+import type { TestOptions } from '../../common';
+
+export function createTranslationsTests(
+  setup: ChatWidgetSetup,
+  { act }: Required
+) {
+  describe('translations', () => {
+    describe('header', () => {
+      test('renders with custom translations', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              templates: {
+                header: {
+                  titleText: 'Custom title',
+                  closeLabelText: 'Custom close button label',
+                  clearLabelText: 'Custom clear button label',
+                  maximizeLabelText: 'Custom maximize button label',
+                  minimizeLabelText: 'Custom minimize button label',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              translations: {
+                header: {
+                  title: 'Custom title',
+                  closeLabel: 'Custom close button label',
+                  clearLabel: 'Custom clear button label',
+                  maximizeLabel: 'Custom maximize button label',
+                  minimizeLabel: 'Custom minimize button label',
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(
+          document.querySelector('.ais-ChatHeader-title')!.textContent
+        ).toBe('Custom title');
+        expect(
+          document
+            .querySelector('.ais-ChatHeader-close')!
+            .getAttribute('aria-label')
+        ).toBe('Custom close button label');
+        expect(
+          document.querySelector('.ais-ChatHeader-clear')!.textContent
+        ).toBe('Custom clear button label');
+        expect(
+          document
+            .querySelector('.ais-ChatHeader-maximize')!
+            .getAttribute('aria-label')
+        ).toBe('Custom maximize button label');
+
+        userEvent.click(document.querySelector('.ais-ChatHeader-maximize')!);
+        expect(
+          document
+            .querySelector('.ais-ChatHeader-maximize')!
+            .getAttribute('aria-label')
+        ).toBe('Custom minimize button label');
+      });
+    });
+
+    describe('prompt', () => {
+      test('renders with custom textarea translations', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({});
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              templates: {
+                prompt: {
+                  textareaLabelText: 'Custom message input',
+                  textareaPlaceholderText: 'Custom placeholder',
+                  disclaimerText: 'Custom disclaimer',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              translations: {
+                prompt: {
+                  textareaLabel: 'Custom message input',
+                  textareaPlaceholder: 'Custom placeholder',
+                  disclaimer: 'Custom disclaimer',
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(
+          document
+            .querySelector('.ais-ChatPrompt-textarea')
+            ?.getAttribute('aria-label')
+        ).toBe('Custom message input');
+        expect(
+          document
+            .querySelector('.ais-ChatPrompt-textarea')
+            ?.getAttribute('placeholder')
+        ).toBe('Custom placeholder');
+        expect(
+          document.querySelector('.ais-ChatPrompt-footer')!.textContent
+        ).toBe('Custom disclaimer');
+      });
+
+      test('renders with custom empty message translation', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({});
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              templates: {
+                prompt: {
+                  emptyMessageTooltipText: 'Custom empty message',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              translations: {
+                prompt: {
+                  emptyMessageTooltip: 'Custom empty message',
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(
+          document
+            .querySelector('.ais-ChatPrompt-submit')
+            ?.getAttribute('aria-label')
+        ).toBe('Custom empty message');
+      });
+
+      test('renders with custom send message translation', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(),
+              templates: {
+                prompt: {
+                  sendMessageTooltipText: 'Custom send message',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(),
+              translations: {
+                prompt: {
+                  sendMessageTooltip: 'Custom send message',
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        userEvent.type(
+          document.querySelector('.ais-ChatPrompt-textarea')!,
+          'Hello, world!'
+        );
+
+        expect(
+          document
+            .querySelector('.ais-ChatPrompt-submit')
+            ?.getAttribute('aria-label')
+        ).toBe('Custom send message');
+      });
+
+      test('renders with custom stop response translation', async () => {
+        const searchClient = createSearchClient();
+
+        const chat = new Chat({});
+        Object.defineProperty(chat, 'status', {
+          get: () => 'streaming',
+        });
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(chat),
+              templates: {
+                prompt: {
+                  stopResponseTooltipText: 'Custom stop response',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(chat),
+              translations: {
+                prompt: {
+                  stopResponseTooltip: 'Custom stop response',
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        expect(
+          document
+            .querySelector('.ais-ChatPrompt-submit')
+            ?.getAttribute('aria-label')
+        ).toBe('Custom stop response');
+      });
+    });
+
+    describe('messages', () => {
+      test('renders with custom actions translations', async () => {
+        const searchClient = createSearchClient();
+
+        await setup({
+          instantSearchOptions: {
+            indexName: 'indexName',
+            searchClient,
+          },
+          widgetParams: {
+            javascript: {
+              ...createDefaultWidgetParams(
+                new Chat({
+                  messages: [
+                    {
+                      id: '0',
+                      role: 'assistant',
+                      parts: [
+                        {
+                          type: 'text',
+                          text: 'hello',
+                        },
+                      ],
+                    },
+                  ],
+                })
+              ),
+              templates: {
+                messages: {
+                  copyToClipboardLabelText: 'Custom copy action',
+                  regenerateLabelText: 'Custom regenerate action',
+                },
+              },
+            },
+            react: {
+              ...createDefaultWidgetParams(
+                new Chat({
+                  messages: [
+                    {
+                      id: '0',
+                      role: 'assistant',
+                      parts: [
+                        {
+                          type: 'text',
+                          text: 'hello',
+                        },
+                      ],
+                    },
+                  ],
+                })
+              ),
+              translations: {
+                messages: {
+                  copyToClipboardLabel: 'Custom copy action',
+                  regenerateLabel: 'Custom regenerate action',
+                },
+              },
+            },
+            vue: {},
+          },
+        });
+
+        await openChat(act);
+
+        const actions = document.querySelectorAll('.ais-ChatMessage-action');
+        expect(actions[0].getAttribute('aria-label')).toBe(
+          'Custom copy action'
+        );
+        expect(actions[1].getAttribute('aria-label')).toBe(
+          'Custom regenerate action'
+        );
+      });
+    });
+  });
+}
diff --git a/tests/common/widgets/chat/utils.ts b/tests/common/widgets/chat/utils.ts
new file mode 100644
index 0000000000..cd41984729
--- /dev/null
+++ b/tests/common/widgets/chat/utils.ts
@@ -0,0 +1,22 @@
+import { wait } from '@instantsearch/testutils';
+import userEvent from '@testing-library/user-event';
+import { Chat } from 'instantsearch.js/es/lib/chat';
+
+import type { Act } from '../../common';
+
+export const createDefaultWidgetParams = (chat?: Chat) => ({
+  agentId: 'agentId',
+  chat: chat ?? new Chat({}),
+});
+
+export async function openChat(act: Act) {
+  await act(async () => {
+    await wait(0);
+  });
+
+  userEvent.click(document.querySelector('.ais-ChatToggleButton')!);
+
+  await act(async () => {
+    await wait(0);
+  });
+}