LibreChat/e2e
Marco Beretta 92d4705f79
🧭 refactor: make the side panels behave the same way (#14695)
* style: unify chat input tool badge styling

Every tool badge repeated max-w-fit and its own hand-written checked-state
colour triplet. Move max-w-fit into CheckboxButton's base classes, where
tailwind-merge still lets a consumer override it, and collect the accent
colours into a single map so the palette lives in one place.

Artifacts repeated the amber triplet a second time on its dropdown button;
that now reads from the same map.

* feat: add feedback when resetting model parameters

The button did nothing visible on click, so with parameters already at
their defaults it looked broken. Spin the icon a full turn on press and
announce the change politely, matching the Agent Builder panel which
already announced but had no visual counterpart.

The animation replays on consecutive clicks via a reflow, and is gated
behind motion-reduce.

* fix: keep the prompt editor open when inserting a special variable

Opening the variables menu moved focus out of the textarea, whose blur
handler exits edit mode, so the prompt snapped back to its rendered
preview as if it had been saved.

Guard the blur against focus landing inside a menu, since Ariakit focuses
the menu itself on open, and hand the menu a finalFocus target so focus
returns to the textarea on close. Without the latter the editor stayed
open but unfocused, which quietly broke click-away-to-exit.

* feat: create prompts from a dialog instead of a dedicated page

Prompts now open a dialog from the sidebar, matching how skills and MCP
servers are created, and /prompts/new is gone. The dialog reuses the
existing form rather than duplicating it, with a flag to drop the
page-level chrome that has no place in a modal.

Three things the modal exposed:

- Radix locks pointer events on the body, so the portaled category and
  special-variable menus rendered but could not be clicked. They now
  render inline when hosted in a dialog, as SetKeyDialog already does.
- The floating labels notch out the page surface, which left a visible
  chip against the dialog background in dark mode. The surface is now
  passed in rather than hardcoded.
- Creating gave no indication anything was happening; the button now
  shows a spinner and blocks repeat submits.

Create buttons for both prompts and skills use the submit variant, since
both perform a write.

* style: match prompt action button sizes

The share button sat at 36px next to a 40px Use Prompt button in the
preview. Drop the size override so it takes the icon variant's default,
and bring its row-mates in the editor header along so that row stays
uniform.

* feat: load prompts by scrolling instead of paging

The query was already cursor-based; the nav hook was slicing it back into
one page at a time behind Prev/Next buttons. Flatten the loaded pages and
let the existing scroll hook fetch as the list nears its end.

useNavScrolling only fetched from a scroll event, so a first page that
did not overflow its container produced no event and the rest of the list
was unreachable. It now tops up until the list actually scrolls, which is
why zooming in used to 'fix' it.

* feat: pin panel admin settings and scroll only the panel content

Each side panel scrolled as a whole, so its filter row and toggles slid
away with the list and the scrollbar spanned the full height. Give every
panel a fixed header, a scrolling content region, and a footer that holds
the admin settings.

The skills panel gains the standard filter input in place of its title
and toggle-to-search icon; it also rendered admin settings twice, once
from the filter row and once from the accordion.

Memories drops its client-side paging, which only sliced already-loaded
data, in favour of scrolling the full list.

* fix: repair the skills create menu and icon-only dropdowns

The create menu was built on Dropdown, which is a select rather than an
action menu, and Dropdown applies its className to the popover as well as
the trigger. Sizing the trigger therefore shrank the menu itself to 36px
and clipped both entries. Rebuild it on DropdownPopup, which is what the
rest of the app uses for action menus.

Dropdown's icon-only trigger also kept its horizontal padding and laid the
icon out in a full-width flex row, leaving too little room so the icon
flex-shrank to roughly half its width. That affected every icon-only
consumer, including the prompts category filter.

* fix: correct the gap above the MCP server URL field

The fieldset grouping the connection sections carried display: contents,
which removes its box and with it the margin that space-y puts on it. The
first section inside sat flush against the description while every other
gap kept its 16px.

* refactor: unpin a favorite in one click

The row's overflow menu held a single Unpin entry, so opening it was pure
overhead. Show the unpin button directly instead.

Its hover surface matched the row's own hover colour exactly, so hovering
changed nothing; it now uses a surface that differs in both themes, with
a border carrying the contrast in light mode where the surfaces are close.

Adds the tests for unpinning, which had none.

* fix: stop prompt skeletons stacking on top of the loaded list

The groups were rendered outside the loading branch, so a refetch with
data already cached drew three skeletons above the existing rows instead
of leaving the list alone. The three states are now mutually exclusive.

* feat: add PanelContent to standardize side panel loading states

Each panel decided for itself whether to draw a spinner, a skeleton, or
nothing, and some replaced the whole panel rather than just the list.
PanelContent owns the scroll region and the loading/empty/content
decision so a panel cannot invent a fourth pattern.

It takes isLoading rather than isFetching on purpose: a refetch that
already has rows on screen should leave them alone.

* feat: give the side panels row-shaped loading skeletons

Each panel now loads with a skeleton built from the row it stands in for,
rather than a spinner or nothing: the memory card's key and token pill,
the MCP server's icon over name and description, the bookmark's icon and
count, the prompt card's block.

Memories previously replaced the entire panel while loading, so the
filter you had just typed into disappeared. The skeleton is now confined
to the content region and the header stays put.

Loading also moves out of the list components, which had each grown their
own copy of it, and into the shared PanelContent.

* feat: show a loading state in the bookmarks panel

Bookmarks had no loading state at all: it rendered straight into its
empty state while fetching, so it flashed 'no bookmarks' before the list
appeared. Thread isLoading through and give it the same header, scrolling
content and skeleton as the other panels.

* style: tighten the favorite row and unpin button

Even padding on the row, the unpin button sitting a little closer to the
edge, and no border until it is hovered.

* feat: scroll the bookmarks list instead of paging it

Bookmarks were already fetched in full, so the pager was slicing data
that was sitting in memory. Render the whole list and let it scroll, the
same as the other side panels.

It also removes a latent drag bug: rows were reordered by their index in
the unsliced array while the list rendered a page slice, so dragging on
any page past the first moved the wrong row.

* feat: load skills by scrolling instead of capping the list

The skills panel fetched a single page of 50 and never asked for more, so
a 51st skill was unreachable. Switch it to the cursor-paginated infinite
query that already existed alongside it and wire the shared scroll hook,
matching prompts and the other side panels.

The list and its rows only ever read summary fields, so they now take
TSkillSummary and the response no longer needs casting through unknown.

* fix: stop mocking real modules as virtual in specs

Seven specs mocked @librechat/client and librechat-data-provider with
`virtual: true`, which is for modules that do not exist on disk. These
do, so the flag keyed each mock to a path derived from the spec's own
directory rather than the module's resolved id. The component under test
resolves the real id, so whether it got the mock depended on the module
id cache of whichever worker picked the file up.

UploadSkillDialog was the one that bit: when the mock missed, the real
Radix dialog rendered and portaled its content to the body, so every
assertion reading from the render container failed with the input
"not rendered" while it sat in a portal a few nodes away.

* test: give the lazy bookmark chunk room to load

Waiting for BookmarkNav means waiting for babel to transform its whole
module graph on first require, which does not fit in waitFor's default
second when the transform cache is cold or the machine is busy. The
failure looked like a missed re-render but was just an import in flight.

* build: recycle jest workers before the OS kills them

Coverage maps accumulate for the life of a worker, so a full client run
pushes workers past a gigabyte and the OS kills one, failing whichever
suite it was holding at the time. Capping idle worker memory also cut
the wall clock, since the run no longer swaps.

* fix: give the dialog prompt labels a real backdrop

Floating labels notch out the surface behind them so the input's border
does not run through the text. The dialog variant asked for `bg-background`,
which no longer maps to anything and computes to transparent in both
themes, leaving the border visible through the label. `bg-surface-primary`
is what OGDialogContent actually paints.

* fix: resolve side panel review findings

Send the removed prompt create page to a tombstone route so a stale
/prompts/new cannot render a blank form or fetch the id "new".

Drive the list footer spinner from isFetchingNextPage alone; the old
showLoading flag was set on scroll and only cleared by a later scroll,
so it stuck on after the last page.

Retry the scroll auto-fill through a ResizeObserver: the fill bailed
whenever the panel had no layout yet and nothing asked again once it
got one. A collapsed sidebar keeps its panel mounted and laid out, so
gate fetching on the sidebar being expanded rather than draining the
catalog behind an invisible panel.

Gate the MCP admin footer on the admin role, matching the memories,
prompts and skills panels; the bordered bar rendered empty for
everyone else.

Replay the reset icon spin by remounting the icon. Toggling the class
list lost the animation to the re-render that setConversation causes.

Announce panel loading from a live region carrying its own text. The
skeleton rows and the spinner are both aria-hidden, so labelling the
region left nothing for a screen reader to read out.

Cover the scroll hook, the panel content primitive and the prompt
create dialog with unit tests, and point the prompts e2e spec at the
dialog rather than the deleted page.

* chore: remove unused translation keys

com_ui_pagination and com_ui_select_or_create_prompt lost their last
callers when the prompt list moved to infinite scroll and the empty
prompt preview was dropped. Only the English file is touched; the
other locales are generated externally.

* Fix nav pagination retry loop

* Fix prompt field IDs and skills pagination

* Fix prompt dropdown ARIA IDs

* test: stub syncStaticTools in the server bootstrap specs

initializeMCPs now calls syncStaticTools from services/Config when no MCP
servers are configured. Both bootstrap specs mock that module wholesale, so
the call threw, the post-listen handler ran process.exit(1), and the Jest
worker died four times over before the suite was reported as failing to run.
2026-08-08 23:15:46 -04:00
..
benchmarks perf: Reduce Agent Chat Startup Latency (#14423) 2026-07-25 07:58:20 -04:00
benchmarks-reasoning 🧪 test: Reasoning-Stream Render Perf Benchmark via react-scan (#14494) 2026-07-28 22:18:24 -04:00
config 📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686) 2026-08-08 13:50:21 -04:00
fixtures/deployment-skills/e2e-deployment-skill 🗂️ feat: Add Deployment Skill Directory (#13523) 2026-06-05 10:24:28 -04:00
recordings 🎭 feat: Add Credential-Free Playwright Smoke Suite with a Local Mock LLM (#13472) 2026-06-02 16:36:39 -04:00
setup 📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686) 2026-08-08 13:50:21 -04:00
specs 🧭 refactor: make the side panels behave the same way (#14695) 2026-08-08 23:15:46 -04:00
config.local.example.ts
jestSetup.js v0.8.7 (#13907) 2026-06-24 14:49:32 -04:00
playwright.config.a11y.ts ⚖️ feat: Add Violation Scores (#8304) 2025-07-07 17:08:40 -04:00
playwright.config.benchmark.ts perf: Reduce Agent Chat Startup Latency (#14423) 2026-07-25 07:58:20 -04:00
playwright.config.local.ts 🛟 test: Restore Playwright Smoke E2E (#13020) 2026-05-14 09:49:26 -04:00
playwright.config.mock.ts 📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686) 2026-08-08 13:50:21 -04:00
playwright.config.real.ts 🎯 feat: Tool Intent Label Capability (tool_intents) (#14499) 2026-07-29 15:40:52 -04:00
playwright.config.reasoning-perf.ts 🧪 test: Reasoning-Stream Render Perf Benchmark via react-scan (#14494) 2026-07-28 22:18:24 -04:00
playwright.config.ts 🎭 feat: Add Credential-Free Playwright Smoke Suite with a Local Mock LLM (#13472) 2026-06-02 16:36:39 -04:00
README.md 🧪 test: Run mock E2E against Redis in shards (#14551) 2026-07-31 12:10:43 -04:00
types.ts

LibreChat e2e

The mock e2e profile is the safest default for generated tests. It starts LibreChat with e2e/config/librechat.e2e.yaml, injects an in-process fake LLM (via LIBRECHAT_TEST_RUN_HOOK), creates an authenticated e2e user, and avoids real provider credentials.

Stream Stores and Shards

The mock profile uses the in-memory generation stream store by default. To exercise the same browser scenarios through a real Redis job store and pub/sub transport, start Redis on port 6379 and run:

npm run e2e:mock:redis

Memory mode explicitly disables Redis. Redis mode defaults to database 15 with a LibreChatE2E key prefix, and fails closed: the test server pings Redis and verifies that the generation job manager did not silently fall back to memory. Override REDIS_URI or E2E_REDIS_KEY_PREFIX when needed.

CI runs the complete mock suite in both stream modes. Each mode is split across four Playwright shards, while each shard keeps one worker so tests do not contend for the shard's authenticated user and database:

npx playwright test --config=e2e/playwright.config.mock.ts --shard=1/4

Recording Tests

Use Playwright codegen when you want to turn an exploratory browser session into a draft test:

npm run e2e:record

That command builds the app, starts the LibreChat test server (with an in-process fake LLM) when needed, writes e2e/storageState.json, and opens Playwright codegen at /c/new. The npm script uses http://localhost:3333 so it does not collide with a normal dev server on 3080. Raw recordings are written to e2e/recordings/ and ignored by git.

For a real local LibreChat config instead of the mock profile:

npm run e2e:record:local

Useful direct options:

node e2e/setup/record.js --url=http://localhost:3080/c/new
node e2e/setup/record.js --profile=local --no-output
node e2e/setup/record.js --auth-only
node e2e/setup/record.js --output=e2e/recordings/settings-draft.spec.ts

LLM-Assisted Loop

  1. Start npm run e2e:record.
  2. Let the LLM use Computer Use to operate the headed Playwright browser.
  3. Stop codegen after the workflow is captured.
  4. Move the useful parts from e2e/recordings/ into a committed spec under e2e/specs/mock/.
  5. Replace brittle generated selectors with role, label, text, or data-testid locators.
  6. Add assertions that prove the behavior, not just the clicked path.
  7. Run the finished spec with npm run e2e:mock -- <spec name>.

Generated recordings are a draft, not the final test. The committed version should use the shared helpers in e2e/specs/mock/helpers.ts where possible, wait on network or visible UI state instead of fixed sleeps, and keep test data deterministic.