LibreChat/e2e/specs/messages.spec.ts
2025-02-12 18:00:16 +01:00

199 lines
No EOL
8 KiB
TypeScript

// messaging.spec.ts
import { expect, test } from '@playwright/test';
import type { Response, Page, BrowserContext } from '@playwright/test';
import { acceptTermsIfPresent } from '../utils/acceptTermsIfPresent';
const basePath = 'http://localhost:3080/c/';
const initialUrl = `${basePath}new`;
function isUUID(uuid: string) {
const regex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
return regex.test(uuid);
}
const initialNewChatSelector = '[data-testid="nav-new-chat-button"]';
const endpoint = 'openAI'; // adjust as needed
const waitForServerStream = async (response: Response) => {
const endpointCheck =
response.url().includes(`/api/ask/${endpoint}`) ||
response.url().includes(`/api/edit/${endpoint}`);
return endpointCheck && response.status() === 200;
};
/**
* Clears conversations by:
* 1. Navigating to the initial URL and accepting the Terms modal (if needed).
* 2. Clicking the nav-user button to open the popover.
* 3. Waiting for and clicking the "Settings" option.
* 4. In the Settings dialog, selecting the "Data controls" tab.
* 5. Locating the container with the "Clear all chats" label and clicking its Delete button.
* 6. Waiting for the confirmation dialog (with accessible name "Confirm Clear") to appear,
* and then clicking its Delete button.
* 7. Finally, closing the settings dialog.
*/
async function clearConvos(page: Page) {
// Navigate to the initial URL.
await page.goto(initialUrl, { timeout: 5000 });
// Accept the Terms modal if it appears.
await acceptTermsIfPresent(page);
// Open the nav-user popover.
await page.getByTestId('nav-user').click();
// Wait for the popover container to appear.
await page.waitForSelector('[data-dialog][role="listbox"]', { state: 'visible', timeout: 5000 });
// Wait for the "Settings" option to be visible and click it.
const settingsOption = page.getByText('Settings');
await settingsOption.waitFor({ state: 'visible', timeout: 5000 });
await settingsOption.click();
// In the Settings dialog, click on the "Data controls" tab.
const dataControlsTab = page.getByRole('tab', { name: 'Data controls' });
await dataControlsTab.waitFor({ state: 'visible', timeout: 5000 });
await dataControlsTab.click();
// Locate the "Clear all chats" label.
const clearChatsLabel = page.getByText('Clear all chats');
await clearChatsLabel.waitFor({ state: 'visible', timeout: 5000 });
// Get the parent container of the label.
const parentContainer = clearChatsLabel.locator('xpath=..');
// Locate the Delete button within that container.
const deleteButtonInContainer = parentContainer.locator('button', { hasText: 'Delete' });
await deleteButtonInContainer.waitFor({ state: 'visible', timeout: 5000 });
await deleteButtonInContainer.click();
// Wait for the confirmation dialog with the accessible name "Confirm Clear" to appear.
const confirmDialog = page.getByRole('dialog', { name: 'Confirm Clear' });
await confirmDialog.waitFor({ state: 'visible', timeout: 5000 });
// In the confirmation dialog, click the Delete button.
const confirmDeleteButton = page.getByRole('button', { name: 'Delete' });
await confirmDeleteButton.waitFor({ state: 'visible', timeout: 5000 });
await confirmDeleteButton.click();
// Close the settings dialog.
await page.getByRole('button', { name: 'Close', exact: true }).click();
}
let beforeAfterAllContext: BrowserContext;
test.beforeAll(async ({ browser }) => {
console.log('Clearing conversations before message tests.');
beforeAfterAllContext = await browser.newContext();
const page = await beforeAfterAllContext.newPage();
await clearConvos(page);
await page.close();
});
// TODO needs to be updated to the new layout
test.describe('Messaging suite', () => {
test('textbox should be focused after generation, test expected navigation, & test editing messages', async ({ page }) => {
test.setTimeout(120000);
const message = 'hi';
// Navigate to the page.
await page.goto(initialUrl, { timeout: 5000 });
// Accept the Terms modal if needed.
await acceptTermsIfPresent(page);
// Click the "New chat" button.
await page.locator(initialNewChatSelector).click();
// Assume endpoint selection is done automatically.
const input = await page.locator('form').getByRole('textbox');
await input.click();
await input.fill(message);
// Press Enter to send the message and wait for the API response.
const [response] = (await Promise.all([
page.waitForResponse(waitForServerStream),
input.press('Enter'),
])) as [Response];
const responseBody = await response.body();
expect(responseBody.toString()).toContain('"final":true');
// Check that the input remains focused.
await page.waitForTimeout(250);
const isTextboxFocused = await page.evaluate(() =>
document.activeElement === document.querySelector('[data-testid="text-input"]')
);
expect(isTextboxFocused).toBeTruthy();
// Click the "New chat" button to clear the conversation.
await page.locator(initialNewChatSelector).click();
expect(page.url()).toBe(initialUrl);
// Open the first conversation by clicking its icon.
// TODO needs to be chnages to otherside.
await page.locator('[data-testid="convo-icon"]').first().click({ timeout: 5000 });
const finalUrl = page.url();
const conversationId = finalUrl.split(basePath).pop() ?? '';
expect(isUUID(conversationId)).toBeTruthy();
// Simulate editing the conversation title.
const convoMenuButton = await page.getByRole('button', { name: /Conversation Menu Options/i });
await convoMenuButton.click();
const renameOption = await page.getByRole('menuitem', { name: 'Rename' });
await renameOption.click();
// Assume a text editor appears.
const textEditor = page.locator('[data-testid="message-text-editor"]');
await textEditor.click();
const editText = 'All work and no play makes Johnny a poor boy';
await textEditor.fill(editText);
// Click the Save button.
await page.getByRole('button', { name: 'Save', exact: true }).click();
// Verify that the new title appears in the conversation list.
const updatedTitle = await page.getByText(editText).first().textContent();
expect(updatedTitle).toContain(editText);
});
// TODO needs to be updated to the new layout
test('message should stop and continue', async ({ page }) => {
const message = 'write me a 10 stanza poem about space';
await page.goto(initialUrl, { timeout: 5000 });
await acceptTermsIfPresent(page);
await page.locator(initialNewChatSelector).click();
// Assume the endpoint is selected automatically.
const input = await page.locator('form').getByRole('textbox');
await input.click();
await input.fill(message);
await Promise.all([
page.waitForResponse(waitForServerStream),
input.press('Enter'),
]);
// Wait briefly then simulate stopping the generation.
await page.waitForTimeout(250);
await page.getByRole('button', { name: 'Stop' }).click();
// Then continue generation.
await Promise.all([
page.waitForResponse(waitForServerStream),
page.getByTestId('continue-generation-button').click(),
]);
// Check that a "Regenerate" button appears.
const regenerateButton = await page.getByRole('button', { name: 'Regenerate' });
expect(await regenerateButton.count()).toBeGreaterThan(0);
// Clear the conversation if needed.
await page.locator('[data-testid="convo-item"]')
.getByRole('button')
.nth(1)
.click();
});
// TODO needs to be updated to the new layout
test('Page navigations', async ({ page }) => {
await page.goto(initialUrl, { timeout: 5000 });
await acceptTermsIfPresent(page);
await page.locator('[data-testid="convo-icon"]').first().click({ timeout: 5000 });
const currentUrl = page.url();
const conversationId = currentUrl.split(basePath).pop() ?? '';
expect(isUUID(conversationId)).toBeTruthy();
await page.locator(initialNewChatSelector).click();
expect(page.url()).toBe(initialUrl);
});
});