🛟 test: Restore Playwright Smoke E2E (#13020)

* test: restore Playwright smoke e2e

* test: harden e2e smoke setup

* test: sync e2e server bindings

* test: normalize e2e auth urls
This commit is contained in:
Danny Avila 2026-05-14 09:49:26 -04:00 committed by GitHub
parent 738ed005b6
commit b993d9fb28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 473 additions and 179 deletions

View file

@ -1,10 +1,15 @@
import { PlaywrightTestConfig } from '@playwright/test';
import mainConfig from './playwright.config';
import { getLocalE2EEnv } from './setup/env';
import path from 'path';
const absolutePath = path.resolve(process.cwd(), 'api/server/index.js');
const rootPath = path.resolve(__dirname, '..');
const serverPath = path.resolve(rootPath, 'e2e/setup/start-server.js');
import dotenv from 'dotenv';
dotenv.config();
const e2eEnv = getLocalE2EEnv();
Object.assign(process.env, e2eEnv);
const config: PlaywrightTestConfig = {
...mainConfig,
retries: 0,
@ -12,43 +17,8 @@ const config: PlaywrightTestConfig = {
globalTeardown: require.resolve('./setup/global-teardown.local'),
webServer: {
...mainConfig.webServer,
command: `node ${absolutePath}`,
env: {
...process.env,
SEARCH: 'false',
NODE_ENV: 'CI',
EMAIL_HOST: '',
TITLE_CONVO: 'false',
SESSION_EXPIRY: '60000',
REFRESH_TOKEN_EXPIRY: '300000',
LOGIN_VIOLATION_SCORE: '0',
REGISTRATION_VIOLATION_SCORE: '0',
CONCURRENT_VIOLATION_SCORE: '0',
MESSAGE_VIOLATION_SCORE: '0',
NON_BROWSER_VIOLATION_SCORE: '0',
FORK_VIOLATION_SCORE: '0',
IMPORT_VIOLATION_SCORE: '0',
TTS_VIOLATION_SCORE: '0',
STT_VIOLATION_SCORE: '0',
FILE_UPLOAD_VIOLATION_SCORE: '0',
RESET_PASSWORD_VIOLATION_SCORE: '0',
VERIFY_EMAIL_VIOLATION_SCORE: '0',
TOOL_CALL_VIOLATION_SCORE: '0',
CONVO_ACCESS_VIOLATION_SCORE: '0',
ILLEGAL_MODEL_REQ_SCORE: '0',
LOGIN_MAX: '20',
LOGIN_WINDOW: '1',
REGISTER_MAX: '20',
REGISTER_WINDOW: '1',
LIMIT_CONCURRENT_MESSAGES: 'false',
CONCURRENT_MESSAGE_MAX: '20',
LIMIT_MESSAGE_IP: 'false',
MESSAGE_IP_MAX: '100',
MESSAGE_IP_WINDOW: '1',
LIMIT_MESSAGE_USER: 'false',
MESSAGE_USER_MAX: '100',
MESSAGE_USER_WINDOW: '1',
},
command: `node ${serverPath}`,
cwd: rootPath,
},
fullyParallel: false, // if you are on Windows, keep this as `false`. On a Mac, `true` could make tests faster (maybe on some Windows too, just try)
// workers: 1,

View file

@ -1,9 +1,15 @@
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
const absolutePath = path.resolve(process.cwd(), 'api/server/index.js');
import { getBaseE2EEnv, getE2EBaseURL } from './setup/env';
const rootPath = path.resolve(__dirname, '..');
const serverPath = path.resolve(rootPath, 'e2e/setup/start-server.js');
import dotenv from 'dotenv';
dotenv.config();
const baseURL = getE2EBaseURL();
const e2eEnv = getBaseE2EEnv();
Object.assign(process.env, e2eEnv);
export default defineConfig({
globalSetup: require.resolve('./setup/global-setup'),
globalTeardown: require.resolve('./setup/global-teardown'),
@ -23,7 +29,7 @@ export default defineConfig({
reporter: [['html', { outputFolder: 'playwright-report' }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL: 'http://localhost:3080',
baseURL,
video: 'on-first-retry',
trace: 'retain-on-failure',
ignoreHTTPSErrors: true,
@ -53,21 +59,12 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: `node ${absolutePath}`,
port: 3080,
command: `node ${serverPath}`,
cwd: rootPath,
url: baseURL,
stdout: 'pipe',
ignoreHTTPSErrors: true,
// url: 'http://localhost:3080',
timeout: 30_000,
timeout: 120_000,
reuseExistingServer: true,
env: {
...process.env,
NODE_ENV: 'CI',
EMAIL_HOST: '',
SEARCH: 'false',
SESSION_EXPIRY: '60000',
ALLOW_REGISTRATION: 'true',
REFRESH_TOKEN_EXPIRY: '300000',
},
},
});

View file

@ -1,17 +1,16 @@
import { Page, FullConfig, chromium } from '@playwright/test';
import { chromium } from '@playwright/test';
import type { FullConfig, Page } from '@playwright/test';
import type { User } from '../types';
import cleanupUser from './cleanupUser';
import dotenv from 'dotenv';
dotenv.config();
const timeout = 6000;
const timeout = Number(process.env.E2E_AUTH_TIMEOUT ?? 15000);
async function register(page: Page, user: User) {
await page.getByRole('link', { name: 'Sign up' }).click();
await page.getByLabel('Full name').click();
await page.getByLabel('Full name').fill('test');
await page.getByText('Username (optional)').click();
await page.getByLabel('Username (optional)').fill('test');
await page.getByLabel('Full name').fill(user.name);
await page.getByLabel('Email').click();
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Email').press('Tab');
@ -22,32 +21,45 @@ async function register(page: Page, user: User) {
await page.getByLabel('Submit registration').click();
}
async function logout(page: Page) {
await page.getByTestId('nav-user').click();
await page.getByRole('button', { name: 'Log out' }).click();
async function registrationErrorIsVisible(page: Page) {
return page
.getByTestId('registration-error')
.isVisible({ timeout: 500 })
.catch(() => false);
}
async function login(page: Page, user: User) {
await page.locator('input[name="email"]').fill(user.email);
await page.locator('input[name="password"]').fill(user.password);
await page.locator('input[name="password"]').press('Enter');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill(user.password);
await page.getByTestId('login-button').click();
}
function appURL(baseURL: string, pathname = '') {
const normalizedBaseURL = baseURL.endsWith('/') ? baseURL : `${baseURL}/`;
return new URL(pathname.replace(/^\/+/, ''), normalizedBaseURL).toString();
}
async function authenticate(config: FullConfig, user: User) {
console.log('🤖: global setup has been started');
const { baseURL, storageState } = config.projects[0].use;
console.log('🤖: using baseURL', baseURL);
console.dir(user, { depth: null });
console.log('🤖: using E2E user:', user.email);
if (typeof storageState !== 'string') {
throw new Error('🤖: storageState must be a file path');
}
const browser = await chromium.launch({
headless: false,
headless: config.projects[0].use.headless ?? true,
});
try {
const page = await browser.newPage();
console.log('🤖: 🗝 authenticating user:', user.email);
if (!baseURL) {
if (typeof baseURL !== 'string') {
throw new Error('🤖: baseURL is not defined');
}
const conversationURL = appURL(baseURL, 'c/new');
const loginURL = appURL(baseURL, 'login');
// Set localStorage before navigating to the page
await page.context().addInitScript(() => {
@ -58,31 +70,27 @@ async function authenticate(config: FullConfig, user: User) {
await page.goto(baseURL, { timeout });
await register(page, user);
try {
await page.waitForURL(`${baseURL}/c/new`, { timeout });
await page.waitForURL(conversationURL, { timeout });
} catch (error) {
console.error('Error:', error);
const userExists = page.getByTestId('registration-error');
if (userExists) {
if (await registrationErrorIsVisible(page)) {
console.log('🤖: 🚨 user already exists');
await cleanupUser(user);
await page.goto(baseURL, { timeout });
await register(page, user);
await page.waitForURL(conversationURL, { timeout });
} else {
throw new Error('🤖: 🚨 user failed to register');
}
}
console.log('🤖: ✔️ user successfully registered');
// Logout
// await logout(page);
// await page.waitForURL(`${baseURL}/login`, { timeout });
// console.log('🤖: ✔️ user successfully logged out');
await page.goto(loginURL, { timeout });
await login(page, user);
await page.waitForURL(`${baseURL}/c/new`, { timeout });
await page.waitForURL(conversationURL, { timeout });
console.log('🤖: ✔️ user successfully authenticated');
await page.context().storageState({ path: storageState as string });
await page.context().storageState({ path: storageState });
console.log('🤖: ✔️ authentication state successfully saved in', storageState);
// await browser.close();
// console.log('🤖: global setup has been finished');

View file

@ -1,15 +1,27 @@
import { connectDb } from '@librechat/backend/db/connect';
import {
findUser,
deleteConvos,
deleteMessages,
deleteAllUserSessions,
} from '@librechat/backend/models';
import { User, Balance, Transaction, AclEntry, Token, Group } from '@librechat/backend/db/models';
import { applyRuntimeEnv } from './runtimeEnv';
type TUser = { email: string; password: string };
export default async function cleanupUser(user: TUser) {
applyRuntimeEnv();
/* eslint-disable @typescript-eslint/no-require-imports */
const { connectDb } = require('@librechat/backend/db/connect');
const {
findUser,
deleteConvos,
deleteMessages,
deleteAllUserSessions,
} = require('@librechat/backend/models');
const {
User,
Balance,
Transaction,
AclEntry,
Token,
Group,
} = require('@librechat/backend/db/models');
/* eslint-enable @typescript-eslint/no-require-imports */
const { email } = user;
try {
console.log('🤖: global teardown has been started');
@ -26,7 +38,14 @@ export default async function cleanupUser(user: TUser) {
console.log('🤖: ✅ Found user in Database');
// Delete all conversations & associated messages
const { deletedCount, messages } = await deleteConvos(userId, {});
const { deletedCount, messages } = await deleteConvos(userId, {}).catch((error) => {
if (error instanceof Error && error.message.includes('Conversation not found')) {
console.log('🤖: ⚠️ No conversations found for user');
return { deletedCount: 0, messages: { deletedCount: 0 } };
}
throw error;
});
if (messages.deletedCount > 0 || deletedCount > 0) {
console.log(`🤖: ✅ Deleted ${deletedCount} convos & ${messages.deletedCount} messages`);

134
e2e/setup/env.ts Normal file
View file

@ -0,0 +1,134 @@
import crypto from 'crypto';
import path from 'path';
const DEFAULT_BASE_URL = 'http://localhost:3080';
const DEFAULT_MONGO_URI = 'mongodb://127.0.0.1:27017/LibreChat-e2e';
const DEFAULT_RUNTIME_ENV_PATH = path.resolve(__dirname, '../specs/.test-results/runtime-env.json');
const GENERATED_CREDS_KEY = crypto.randomBytes(32).toString('hex');
const GENERATED_CREDS_IV = crypto.randomBytes(16).toString('hex');
const GENERATED_JWT_SECRET = crypto.randomBytes(32).toString('hex');
const GENERATED_JWT_REFRESH_SECRET = crypto.randomBytes(32).toString('hex');
const PASSTHROUGH_ENV_KEYS = [
'APPDATA',
'CI',
'FORCE_COLOR',
'HOME',
'LOCALAPPDATA',
'NO_COLOR',
'NO_PROXY',
'NODE_OPTIONS',
'PATH',
'PLAYWRIGHT_BROWSERS_PATH',
'SHELL',
'TEMP',
'TMP',
'TMPDIR',
'USER',
'USERNAME',
'http_proxy',
'https_proxy',
'no_proxy',
'HTTP_PROXY',
'HTTPS_PROXY',
];
export function getE2EBaseURL() {
return process.env.E2E_BASE_URL ?? DEFAULT_BASE_URL;
}
export function getE2EServerAddress(baseURL = getE2EBaseURL()) {
const url = new URL(baseURL);
const host = url.hostname.replace(/^\[(.*)\]$/, '$1');
const port = url.port || (url.protocol === 'https:' ? '443' : '80');
return { host, port };
}
export function getRuntimeEnvPath() {
return process.env.E2E_RUNTIME_ENV_PATH ?? DEFAULT_RUNTIME_ENV_PATH;
}
function getPassthroughEnv(): Record<string, string> {
const env: Record<string, string> = {};
const passthroughKeys = [
...PASSTHROUGH_ENV_KEYS,
...(process.env.E2E_PASSTHROUGH_ENV?.split(',') ?? []),
];
for (const key of passthroughKeys) {
const value = process.env[key.trim()];
if (value != null) {
env[key.trim()] = value;
}
}
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('MONGOMS_') && value != null) {
env[key] = value;
}
}
return env;
}
export function getBaseE2EEnv(): Record<string, string> {
const baseURL = getE2EBaseURL();
const { host, port } = getE2EServerAddress(baseURL);
return {
...getPassthroughEnv(),
NODE_ENV: 'CI',
HOST: process.env.E2E_HOST ?? host,
PORT: process.env.E2E_PORT ?? port,
MONGO_URI: process.env.MONGO_URI ?? DEFAULT_MONGO_URI,
DOMAIN_CLIENT: process.env.E2E_DOMAIN_CLIENT ?? baseURL,
DOMAIN_SERVER: process.env.E2E_DOMAIN_SERVER ?? baseURL,
E2E_RUNTIME_ENV_PATH: getRuntimeEnvPath(),
E2E_USE_MEMORY_MONGO: process.env.E2E_USE_MEMORY_MONGO ?? 'auto',
NO_INDEX: process.env.NO_INDEX ?? 'true',
OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? 'user_provided',
CREDS_KEY: process.env.CREDS_KEY ?? GENERATED_CREDS_KEY,
CREDS_IV: process.env.CREDS_IV ?? GENERATED_CREDS_IV,
JWT_SECRET: process.env.JWT_SECRET ?? GENERATED_JWT_SECRET,
JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET ?? GENERATED_JWT_REFRESH_SECRET,
EMAIL_HOST: '',
SEARCH: 'false',
SESSION_EXPIRY: '60000',
ALLOW_REGISTRATION: 'true',
REFRESH_TOKEN_EXPIRY: '300000',
};
}
export function getLocalE2EEnv(): Record<string, string> {
return {
...getBaseE2EEnv(),
TITLE_CONVO: 'false',
LOGIN_VIOLATION_SCORE: '0',
REGISTRATION_VIOLATION_SCORE: '0',
CONCURRENT_VIOLATION_SCORE: '0',
MESSAGE_VIOLATION_SCORE: '0',
NON_BROWSER_VIOLATION_SCORE: '0',
FORK_VIOLATION_SCORE: '0',
IMPORT_VIOLATION_SCORE: '0',
TTS_VIOLATION_SCORE: '0',
STT_VIOLATION_SCORE: '0',
FILE_UPLOAD_VIOLATION_SCORE: '0',
RESET_PASSWORD_VIOLATION_SCORE: '0',
VERIFY_EMAIL_VIOLATION_SCORE: '0',
TOOL_CALL_VIOLATION_SCORE: '0',
CONVO_ACCESS_VIOLATION_SCORE: '0',
ILLEGAL_MODEL_REQ_SCORE: '0',
LOGIN_MAX: '20',
LOGIN_WINDOW: '1',
REGISTER_MAX: '20',
REGISTER_WINDOW: '1',
LIMIT_CONCURRENT_MESSAGES: 'false',
CONCURRENT_MESSAGE_MAX: '20',
LIMIT_MESSAGE_IP: 'false',
MESSAGE_IP_MAX: '100',
MESSAGE_IP_WINDOW: '1',
LIMIT_MESSAGE_USER: 'false',
MESSAGE_USER_MAX: '100',
MESSAGE_USER_WINDOW: '1',
};
}

View file

@ -1,9 +1,9 @@
import { FullConfig } from '@playwright/test';
import localUser from '../config.local';
import authenticate from './authenticate';
import { getE2EUser } from './user';
async function globalSetup(config: FullConfig) {
await authenticate(config, localUser);
await authenticate(config, getE2EUser());
}
export default globalSetup;

View file

@ -1,14 +1,9 @@
import { FullConfig } from '@playwright/test';
import authenticate from './authenticate';
import { getE2EUser } from './user';
async function globalSetup(config: FullConfig) {
const user = {
name: 'test',
email: String(process.env.E2E_USER_EMAIL),
password: String(process.env.E2E_USER_PASSWORD),
};
await authenticate(config, user);
await authenticate(config, getE2EUser());
}
export default globalSetup;

View file

@ -1,9 +1,9 @@
import localUser from '../config.local';
import cleanupUser from './cleanupUser';
import { getE2EUser } from './user';
async function globalTeardown() {
try {
await cleanupUser(localUser);
await cleanupUser(getE2EUser());
} catch (error) {
console.error('Error:', error);
}

View file

@ -1,13 +1,9 @@
import cleanupUser from './cleanupUser';
import { getE2EUser } from './user';
async function globalTeardown() {
const user = {
email: String(process.env.E2E_USER_EMAIL),
password: String(process.env.E2E_USER_PASSWORD),
};
try {
await cleanupUser(user);
await cleanupUser(getE2EUser());
} catch (error) {
console.error('Error:', error);
}

21
e2e/setup/runtimeEnv.ts Normal file
View file

@ -0,0 +1,21 @@
import fs from 'fs';
import { getRuntimeEnvPath } from './env';
export function applyRuntimeEnv() {
const runtimeEnvPath = getRuntimeEnvPath();
if (!fs.existsSync(runtimeEnvPath)) {
return;
}
const runtimeEnv = JSON.parse(fs.readFileSync(runtimeEnvPath, 'utf8')) as Record<
string,
string | undefined
>;
for (const [key, value] of Object.entries(runtimeEnv)) {
if (value != null) {
process.env[key] = value;
}
}
}

187
e2e/setup/start-server.js Normal file
View file

@ -0,0 +1,187 @@
const fs = require('fs');
const net = require('net');
const path = require('path');
require('dotenv').config();
const DEFAULT_MONGO_URI = 'mongodb://127.0.0.1:27017/LibreChat-e2e';
const DEFAULT_RUNTIME_ENV_PATH = path.resolve(__dirname, '../specs/.test-results/runtime-env.json');
let mongoServer;
function decodeMongoValue(value) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function getMongoScheme(uri) {
const schemeEnd = uri.indexOf('://');
return schemeEnd === -1 ? '' : uri.slice(0, schemeEnd).toLowerCase();
}
function getMongoAuthority(uri) {
const schemeEnd = uri.indexOf('://');
if (schemeEnd === -1) {
return '';
}
const withoutScheme = uri.slice(schemeEnd + 3);
return withoutScheme.split(/[/?#]/, 1)[0];
}
function getMongoDbName(uri) {
const schemeEnd = uri.indexOf('://');
if (schemeEnd === -1) {
return 'LibreChat-e2e';
}
const withoutScheme = uri.slice(schemeEnd + 3);
const pathStart = withoutScheme.indexOf('/');
if (pathStart === -1) {
return 'LibreChat-e2e';
}
const pathname = withoutScheme.slice(pathStart + 1).split(/[?#]/, 1)[0];
const dbName = pathname.split('/', 1)[0];
return dbName ? decodeMongoValue(dbName) : 'LibreChat-e2e';
}
function normalizeMongoPort(port) {
const parsed = Number(port);
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : 27017;
}
function parseMongoHost(hostEntry) {
if (!hostEntry) {
return null;
}
if (hostEntry.startsWith('[')) {
const hostEnd = hostEntry.indexOf(']');
if (hostEnd === -1) {
return null;
}
const host = hostEntry.slice(1, hostEnd);
const port = hostEntry[hostEnd + 1] === ':' ? hostEntry.slice(hostEnd + 2) : '';
return { host, port: normalizeMongoPort(port) };
}
const [host, port] = hostEntry.split(':');
return host ? { host, port: normalizeMongoPort(port) } : null;
}
function parseMongoUri(uri) {
const scheme = getMongoScheme(uri);
const authority = getMongoAuthority(uri);
const hosts = authority
.slice(authority.lastIndexOf('@') + 1)
.split(',')
.filter(Boolean);
const parsedHost =
scheme === 'mongodb+srv' || hosts.length !== 1 ? null : parseMongoHost(hosts[0]);
return {
dbName: getMongoDbName(uri),
host: parsedHost?.host ?? '',
port: parsedHost?.port ?? 27017,
canProbe: Boolean(parsedHost),
};
}
function isLocalHost(host) {
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
}
async function canConnect(host, port) {
return new Promise((resolve) => {
const socket = net.createConnection({ host, port });
const done = (result) => {
socket.destroy();
resolve(result);
};
socket.setTimeout(500);
socket.once('connect', () => done(true));
socket.once('timeout', () => done(false));
socket.once('error', () => done(false));
});
}
function withDbName(uri, dbName) {
const parsed = new URL(uri);
parsed.pathname = `/${dbName}`;
return parsed.toString();
}
function writeRuntimeEnv() {
const runtimeEnvPath = process.env.E2E_RUNTIME_ENV_PATH || DEFAULT_RUNTIME_ENV_PATH;
fs.mkdirSync(path.dirname(runtimeEnvPath), { recursive: true });
fs.writeFileSync(runtimeEnvPath, JSON.stringify({ MONGO_URI: process.env.MONGO_URI }, null, 2));
}
async function maybeStartMemoryMongo() {
const mongoUri = process.env.MONGO_URI ?? DEFAULT_MONGO_URI;
const mode = process.env.E2E_USE_MEMORY_MONGO ?? 'auto';
if (mode === 'false') {
process.env.MONGO_URI = mongoUri;
writeRuntimeEnv();
return;
}
const { dbName, host, port, canProbe } = parseMongoUri(mongoUri);
if (mode === 'auto' && (!canProbe || !isLocalHost(host) || (await canConnect(host, port)))) {
process.env.MONGO_URI = mongoUri;
writeRuntimeEnv();
return;
}
const { MongoMemoryServer } = require('mongodb-memory-server');
mongoServer = await MongoMemoryServer.create({
instance: {
dbName,
ip: '127.0.0.1',
},
});
process.env.MONGO_URI = withDbName(mongoServer.getUri(), dbName);
writeRuntimeEnv();
console.log(`[e2e] Started memory MongoDB at ${process.env.MONGO_URI}`);
}
async function shutdown() {
if (mongoServer) {
await mongoServer.stop();
}
}
process.once('SIGINT', async () => {
await shutdown();
process.exit(130);
});
process.once('SIGTERM', async () => {
await shutdown();
process.exit(143);
});
function startServer() {
return maybeStartMemoryMongo()
.then(() => {
require(path.resolve(__dirname, '../../api/server/index.js'));
})
.catch((error) => {
console.error('[e2e] Failed to start test server:', error);
process.exit(1);
});
}
if (require.main === module) {
startServer();
}
module.exports = {
parseMongoUri,
startServer,
};

15
e2e/setup/user.ts Normal file
View file

@ -0,0 +1,15 @@
import type { User } from '../types';
const DEFAULT_USER: User = {
email: 'testuser@example.com',
name: 'Test User',
password: 'securepassword123',
};
export function getE2EUser(): User {
return {
email: process.env.E2E_USER_EMAIL ?? DEFAULT_USER.email,
name: process.env.E2E_USER_NAME ?? DEFAULT_USER.name,
password: process.env.E2E_USER_PASSWORD ?? DEFAULT_USER.password,
};
}

View file

@ -2,41 +2,18 @@ import { expect, test } from '@playwright/test';
test.describe('Landing suite', () => {
test('Landing title', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
const pageTitle = await page.textContent('#landing-title');
expect(pageTitle?.length).toBeGreaterThan(0);
await page.goto('/', { timeout: 5000 });
await expect(page.getByRole('main')).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Message input' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Select a model' })).toBeVisible();
});
test('Create Conversation', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
await page.goto('/c/new', { timeout: 5000 });
async function getItems() {
const navDiv = await page.waitForSelector('nav > div');
if (!navDiv) {
return [];
}
const items = await navDiv.$$('a.group');
return items || [];
}
// Wait for the page to load and the SVG loader to disappear
await page.waitForSelector('nav > div');
await page.waitForSelector('nav > div > div > svg', { state: 'detached' });
const beforeAdding = (await getItems()).length;
const input = await page.locator('form').getByRole('textbox');
await input.click();
await input.fill('Hi!');
// Send the message
await page.locator('form').getByRole('button').nth(1).click();
// Wait for the message to be sent
await page.waitForTimeout(3500);
const afterAdding = (await getItems()).length;
expect(afterAdding).toBeGreaterThanOrEqual(beforeAdding);
await expect(page).toHaveURL(/\/c\/new$/);
await expect(page.getByRole('link', { name: 'New chat' })).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Message input' })).toBeVisible();
});
});

View file

@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
test.describe('Navigation suite', () => {
test('Navigation bar', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
await page.goto('/', { timeout: 5000 });
await page.getByTestId('nav-user').click();
const navSettings = await page.getByTestId('nav-user').isVisible();
@ -10,49 +10,23 @@ test.describe('Navigation suite', () => {
});
test('Settings modal', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
await page.goto('/', { timeout: 5000 });
await page.getByTestId('nav-user').click();
await page.getByText('Settings').click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible();
expect(modal).toBeTruthy();
const modal = page.getByRole('dialog', { name: /Settings/ });
const modalTitle = await page.getByRole('heading', { name: 'Settings' }).textContent();
const modalHeading = modal.getByRole('heading', { name: 'Settings' });
await expect(modalHeading).toBeVisible();
const modalTitle = await modalHeading.textContent();
expect(modalTitle?.length).toBeGreaterThan(0);
expect(modalTitle).toEqual('Settings');
const modalTabList = await page.getByRole('tablist', { name: 'Settings' }).isVisible();
expect(modalTabList).toBeTruthy();
await expect(modal.getByRole('tablist', { name: 'Settings' })).toBeVisible();
await expect(modal.getByRole('tabpanel', { name: 'General' })).toBeVisible();
await expect(modal.getByRole('combobox', { name: 'Theme' })).toBeVisible();
const generalTabPanel = await page.getByRole('tabpanel', { name: 'General' }).isVisible();
expect(generalTabPanel).toBeTruthy();
const modalClearConvos = await page.getByRole('button', { name: 'Clear' }).isVisible();
expect(modalClearConvos).toBeTruthy();
const modalTheme = page.getByTestId('theme-selector');
expect(modalTheme).toBeTruthy();
async function changeMode(theme: string) {
// Ensure Element Visibility:
await page.waitForSelector('[data-testid="theme-selector"]');
await modalTheme.click();
await page.click(`[data-theme="${theme}"]`);
// Wait for the theme change
await page.waitForTimeout(1000);
// Check if the HTML element has the theme class
const html = await page.$eval(
'html',
(element, selectedTheme) => element.classList.contains(selectedTheme.toLowerCase()),
theme,
);
expect(html).toBeTruthy();
}
await changeMode('dark');
await changeMode('light');
await modal.getByRole('button', { name: 'Close Settings' }).click();
await expect(modalHeading).toBeHidden();
});
});

View file

@ -53,11 +53,12 @@
"frontend": "npm run build:data-provider && npm run build:data-schemas && npm run build:api && npm run build:client-package && cd client && npm run build",
"frontend:ci": "npm run build:data-provider && npm run build:client-package && cd client && npm run build:ci",
"frontend:dev": "cd client && npm run dev",
"e2e": "playwright test --config=e2e/playwright.config.local.ts",
"e2e:headed": "playwright test --config=e2e/playwright.config.local.ts --headed",
"e2e:a11y": "playwright test --config=e2e/playwright.config.a11y.ts --headed",
"e2e:ci": "playwright test --config=e2e/playwright.config.ts",
"e2e:debug": "cross-env PWDEBUG=1 playwright test --config=e2e/playwright.config.local.ts",
"e2e:prepare": "npm run frontend",
"e2e": "npm run e2e:prepare && playwright test --config=e2e/playwright.config.local.ts",
"e2e:headed": "npm run e2e:prepare && playwright test --config=e2e/playwright.config.local.ts --headed",
"e2e:a11y": "npm run e2e:prepare && playwright test --config=e2e/playwright.config.a11y.ts --headed",
"e2e:ci": "npm run e2e:prepare && playwright test --config=e2e/playwright.config.ts",
"e2e:debug": "npm run e2e:prepare && cross-env PWDEBUG=1 playwright test --config=e2e/playwright.config.local.ts",
"e2e:codegen": "npx playwright codegen --load-storage=e2e/storageState.json http://localhost:3080/c/new",
"e2e:login": "npx playwright codegen --save-storage=e2e/auth.json http://localhost:3080/login",
"e2e:github": "act -W .github/workflows/playwright.yml --secret-file my.secrets",
@ -67,7 +68,7 @@
"test:packages:data-provider": "cd packages/data-provider && npm run test:ci",
"test:packages:data-schemas": "cd packages/data-schemas && npm run test:ci",
"test:all": "npm run test:client && npm run test:api && npm run test:packages:api && npm run test:packages:data-provider && npm run test:packages:data-schemas",
"e2e:update": "playwright test --config=e2e/playwright.config.js --update-snapshots",
"e2e:update": "npm run e2e:prepare && playwright test --config=e2e/playwright.config.local.ts --update-snapshots",
"e2e:report": "npx playwright show-report e2e/playwright-report",
"lint:fix": "eslint --fix \"{,!(node_modules|venv)/**/}*.{js,jsx,ts,tsx}\"",
"lint": "eslint \"{,!(node_modules|venv)/**/}*.{js,jsx,ts,tsx}\"",