mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-02 12:22:22 +00:00
149 lines
6.6 KiB
TypeScript
149 lines
6.6 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
import type { Page } from '@playwright/test';
|
||
import { MOCK_ENDPOINTS, mockReply, selectMockEndpoint, sendMessage } from './helpers';
|
||
|
||
/**
|
||
* Creates a project from the all-projects page and returns its id.
|
||
* Project creation navigates to the project workspace (`/projects/:id`).
|
||
*/
|
||
async function createProject(page: Page, name: string): Promise<string> {
|
||
await page.goto('/projects', { timeout: 10000 });
|
||
await page.getByRole('button', { name: 'New project' }).first().click();
|
||
|
||
const dialog = page.getByRole('dialog');
|
||
await dialog.getByRole('textbox', { name: 'Project name' }).fill(name);
|
||
await dialog.getByRole('button', { name: 'Create project' }).click();
|
||
|
||
await expect(page.getByRole('heading', { name })).toBeVisible();
|
||
const projectId = new URL(page.url()).pathname.split('/projects/')[1];
|
||
expect(projectId).toBeTruthy();
|
||
return projectId;
|
||
}
|
||
|
||
const uniqueName = (prefix: string) => `${prefix} ${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
|
||
|
||
test.describe('chat projects', () => {
|
||
test('creates a project and lists it on the all-projects page', async ({ page }) => {
|
||
test.setTimeout(60000);
|
||
const name = uniqueName('E2E Project');
|
||
|
||
await createProject(page, name);
|
||
|
||
await page.goto('/projects', { timeout: 10000 });
|
||
await expect(page.getByRole('button', { name }).first()).toBeVisible();
|
||
});
|
||
|
||
test('starts a project-scoped chat and persists it under the project', async ({ page }) => {
|
||
test.setTimeout(120000);
|
||
const name = uniqueName('E2E Project');
|
||
const projectId = await createProject(page, name);
|
||
|
||
// The workspace exposes a composer entry to start a chat in the project.
|
||
await expect(page.getByRole('button', { name: `New chat in ${name}` }).first()).toBeVisible();
|
||
|
||
// Open the project-scoped new-chat landing directly.
|
||
await page.goto(`/c/new?projectId=${projectId}`, { timeout: 10000 });
|
||
|
||
// The interactive project chip is present, and the composer is scoped.
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeVisible();
|
||
const input = page.getByRole('textbox', { name: 'Message input' });
|
||
await expect(input).toHaveAttribute('placeholder', new RegExp(name));
|
||
|
||
// Switch to a mock endpoint so the message streams without a real API key;
|
||
// the project scope must be retained across the model switch.
|
||
await selectMockEndpoint(page, MOCK_ENDPOINTS[0]);
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeVisible();
|
||
|
||
// Send the message; it streams a reply and the new chat opens at /c/:id.
|
||
const response = await sendMessage(page, 'hello from a project');
|
||
expect(response.ok()).toBeTruthy();
|
||
await expect(mockReply(page)).toBeVisible({ timeout: 20000 });
|
||
await expect(page).toHaveURL(/\/c\/(?!new)/, { timeout: 15000 });
|
||
|
||
// Expand the project in the sidebar and confirm the chat is filed under it.
|
||
const projectRow = page.getByRole('button', { name }).first();
|
||
if ((await projectRow.getAttribute('aria-expanded')) !== 'true') {
|
||
await projectRow.click();
|
||
}
|
||
await expect(
|
||
page.getByTestId(`project-chats-${projectId}`).getByTestId('convo-item').first(),
|
||
).toBeVisible();
|
||
|
||
const conversationUrl = page.url();
|
||
await page.reload({ timeout: 10000 });
|
||
await expect(page).toHaveURL(conversationUrl);
|
||
|
||
const reloadedProjectRow = page.getByRole('button', { name }).first();
|
||
if ((await reloadedProjectRow.getAttribute('aria-expanded')) !== 'true') {
|
||
await reloadedProjectRow.click();
|
||
}
|
||
await expect(
|
||
page.getByTestId(`project-chats-${projectId}`).getByTestId('convo-item').first(),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test('removes the project scope via the chip ×', async ({ page }) => {
|
||
test.setTimeout(60000);
|
||
const name = uniqueName('E2E Project');
|
||
const projectId = await createProject(page, name);
|
||
|
||
await page.goto(`/c/new?projectId=${projectId}`, { timeout: 10000 });
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeVisible();
|
||
|
||
await page.getByRole('button', { name: 'Remove from project' }).click();
|
||
|
||
// Chip disappears and the URL drops the project scope.
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeHidden();
|
||
await expect(page).toHaveURL((url) => !url.searchParams.has('projectId'));
|
||
});
|
||
|
||
test('drops the project scope when the scoped project is deleted', async ({ page }) => {
|
||
test.setTimeout(90000);
|
||
const name = uniqueName('E2E Project');
|
||
const projectId = await createProject(page, name);
|
||
|
||
await page.goto(`/c/new?projectId=${projectId}`, { timeout: 10000 });
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeVisible();
|
||
|
||
// Delete the scoped project from the sidebar while it is selected on the landing.
|
||
const row = page.getByRole('button', { name, exact: true }).first();
|
||
await expect(row).toBeVisible();
|
||
const item = row.locator('..');
|
||
await item.hover();
|
||
await item.getByRole('button', { name: 'More options' }).click();
|
||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||
await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();
|
||
|
||
// The stale chip is gone, the URL drops the now-dead project scope, and the
|
||
// composer reverts to an unscoped chat (placeholder no longer names the project).
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeHidden();
|
||
await expect(page).toHaveURL((url) => !url.searchParams.has('projectId'));
|
||
await expect(page.getByRole('textbox', { name: 'Message input' })).not.toHaveAttribute(
|
||
'placeholder',
|
||
new RegExp(name),
|
||
);
|
||
});
|
||
|
||
test('switches the project via the chip combobox', async ({ page }) => {
|
||
test.setTimeout(90000);
|
||
const nameA = uniqueName('E2E Project A');
|
||
const nameB = uniqueName('E2E Project B');
|
||
await createProject(page, nameA);
|
||
const projectIdA = new URL(page.url()).pathname.split('/projects/')[1];
|
||
await createProject(page, nameB);
|
||
const projectIdB = new URL(page.url()).pathname.split('/projects/')[1];
|
||
|
||
await page.goto(`/c/new?projectId=${projectIdA}`, { timeout: 10000 });
|
||
await expect(page.getByRole('button', { name: 'Remove from project' })).toBeVisible();
|
||
|
||
// Open the combobox and pick the other project.
|
||
await page.locator('#project-landing-select').click();
|
||
await page.getByRole('option', { name: nameB }).click();
|
||
|
||
await expect(page).toHaveURL(new RegExp(`projectId=${projectIdB}`));
|
||
await expect(page.getByRole('textbox', { name: 'Message input' })).toHaveAttribute(
|
||
'placeholder',
|
||
new RegExp(nameB),
|
||
);
|
||
});
|
||
});
|