LibreChat/e2e/specs/mock/helpers.ts
Danny Avila 6357ea10c1
🧭 feat: Scope Model Spec Skills (#13522)
* feat: scope model spec skills

* style: format skill catalog limit

* fix: serialize model spec skill resolution

* test: satisfy model spec load config typing

* fix: apply model spec skills to added conversations

* fix: support alwaysApply frontmatter alias

* fix: address model spec skills review
2026-06-05 10:22:02 -04:00

162 lines
5.1 KiB
TypeScript

import { expect } from '@playwright/test';
import type { Page, Response } from '@playwright/test';
/** Substring of the reply emitted by the mock LLM server. */
export const MOCK_REPLY_TEXT = 'E2E mock reply';
/** Custom endpoints defined in e2e/config/librechat.e2e.yaml. */
export const MOCK_ENDPOINTS = [
{ label: 'Mock Provider A', model: 'mock-model-a' },
{ label: 'Mock Provider B', model: 'mock-model-b' },
] as const;
export type MockEndpoint = (typeof MOCK_ENDPOINTS)[number];
export const NEW_CHAT_PATH = '/c/new';
type RefreshTokenBody = {
token?: string;
};
export function isAgentsStream(response: Response) {
return response.url().includes('/api/agents') && response.status() === 200;
}
const modelSelectorTrigger = (page: Page) =>
page.getByRole('button', { name: 'Select a model' }).first();
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/** Open the model selector, choose an endpoint, then its model (committed on the model click). */
export async function selectMockEndpoint(page: Page, endpoint: MockEndpoint) {
const trigger = modelSelectorTrigger(page);
await trigger.click();
await page.getByRole('option', { name: endpoint.label }).click();
const modelOption = page.getByRole('option', { name: endpoint.model, exact: true });
if (await modelOption.isVisible({ timeout: 1000 }).catch(() => false)) {
await modelOption.click();
}
await expect(trigger).not.toHaveText('Select a model');
}
/** Open the model selector and choose a configured model spec by label. */
export async function selectModelSpec(page: Page, label: string) {
const trigger = modelSelectorTrigger(page);
await expect(trigger).toBeVisible();
if ((await trigger.textContent())?.includes(label)) {
return;
}
await trigger.click();
await page.getByRole('option', { name: new RegExp(`^${escapeRegExp(label)}\\b`) }).click();
await expect(trigger).toContainText(label);
}
/** Enable the ephemeral Skills capability from the composer tool menu. */
export async function enableSkills(page: Page) {
await page.getByRole('button', { name: 'Tools Options' }).click();
await page.getByTestId('tools-menu-skills').click();
await page.keyboard.press('Escape');
await expect(page.getByRole('button', { name: 'Skills' })).toBeVisible();
}
/** The mock reply as rendered in the conversation, scoped to the messages view. */
export function mockReply(page: Page) {
return page.getByTestId('messages-view').getByText(new RegExp(MOCK_REPLY_TEXT, 'i'));
}
/** Type a message, send it, and wait for the streamed `/api/agents` response. */
export async function sendMessage(page: Page, text: string): Promise<Response> {
const input = page.getByRole('textbox', { name: 'Message input' });
await input.click();
await input.fill(text);
const [response] = await Promise.all([
page.waitForResponse(isAgentsStream, { timeout: 30000 }),
input.press('Enter'),
]);
return response;
}
export async function getAccessToken(page: Page): Promise<string> {
const result = await page.evaluate(async () => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const text = await response.text();
let json: unknown = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, text, json };
});
if (!result.ok) {
throw new Error(
`Expected /api/auth/refresh to return 2xx, got ${result.status}: ${result.text}`,
);
}
const body = result.json as RefreshTokenBody | null;
if (!body?.token) {
throw new Error(`Expected /api/auth/refresh to return a token, got: ${result.text}`);
}
return body.token;
}
export async function requestJson<T>(
page: Page,
params: {
path: string;
token: string;
method?: string;
body?: unknown;
},
): Promise<T> {
const result = await page.evaluate(
async ({ accessToken, body, method, urlPath }) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
};
const init: RequestInit = {
method,
credentials: 'include',
headers,
};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(body);
}
const response = await fetch(urlPath, init);
const text = await response.text();
let json: unknown = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, text, json };
},
{
accessToken: params.token,
body: params.body,
method: params.method ?? 'GET',
urlPath: params.path,
},
);
if (!result.ok) {
throw new Error(
`Expected ${params.method ?? 'GET'} ${params.path} to return 2xx, got ${result.status}: ${result.text}`,
);
}
return result.json as T;
}
export async function fetchJson<T>(page: Page, path: string, token: string): Promise<T> {
return requestJson<T>(page, { path, token });
}