The existing fix in setChannelState('CONNECTED') calls handleUserChanges(),
but at that point isPendingRevision is still true so changes are blocked.
The real trigger must be in setIsPendingRevision(): when it transitions
from true to false (after all CLIENT_RECONNECT messages are processed),
call handleUserChanges() to flush any locally-queued edits.
Also adds a targeted regression test that simulates the exact reconnect
state transitions and verifies pending edits reach the server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setUpSocket() only runs during initialization. Move handleUserChanges()
to the reconnect code path so pending edits are flushed when the
connection is re-established.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that edits made while disconnected are transmitted to the
server immediately upon reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PRs now run a minimal test matrix; full matrix runs on push to develop.
Changes:
- Backend tests: PRs test on Node 24 only (Linux). Windows tests only
run on push to develop. Reduces from 12 to 2 jobs for PRs.
- Upgrade-from-latest-release: PRs test on Node 24 only (1 job vs 3).
- Frontend admin tests: PRs test on Node 24 only (1 job vs 3).
This reduces PR CI from ~25 jobs to ~10, preventing runner exhaustion
when multiple PRs are merged in succession. The full matrix (3 Node
versions × Linux + Windows) still runs on every push to develop.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling handleUserChanges() synchronously in setUpSocket() caused
"Cannot read properties of null (reading 'changeset')" because the
editor isn't fully initialized on first connect. Deferred with
setTimeout(500ms) to allow initialization to complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After reconnecting, setUpSocket() did not call handleUserChanges(),
so any edits made while disconnected were not sent to the server until
the user made another change. This caused divergent pad state between
users.
Now calls handleUserChanges() after reconnect to immediately transmit
any pending local changesets.
Fixes#5108
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve line attributes on neighboring lines during drag-and-drop
On Chrome and Safari, when dragging a line in a list, the browser's
contentEditable engine merges the removed line with its neighbor,
corrupting the neighbor's line attributes (e.g., changing its list
type).
The drop handler now captures line attributes of the lines adjacent
to the dragged content before the browser processes the drop. After
incorporateUserChanges runs, it checks if those attributes were
corrupted and restores them.
Note: this bug cannot be reproduced in Playwright's headless browsers
(DnD in contentEditable iframes isn't supported), so manual testing
with Chrome/Safari is required.
Fixes#3120
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: also save/restore lines with no list type during DnD
Lines with no list attribute can get corrupted to inherit the dragged
line's list type. Now saves all adjacent lines (including those with
no list type) and properly removes corrupted attributes when restoring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: null-safety for atKey in drop handler
Guard against atKey returning null for dynamically inserted nodes
that aren't in the rep.lines index.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: bold text retains formatting after copy-paste
When pasting bold (or italic, underline, etc.) text, the browser's
contentEditable engine normalized the pasted DOM before Etherpad's
content collector could extract the formatting. The pasted HTML
contained proper <b> tags, but the browser flattened the nested
ace-line divs and stripped the inline formatting in the process.
Now the paste handler checks clipboard HTML for formatting tags. If
found, it prevents default browser paste, parses the HTML in a
detached DOMParser document, and inserts the nodes directly into the
editor. This preserves the formatting tags for the content collector.
Fixes#5037
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: sanitize pasted HTML to prevent XSS via clipboard
Strip dangerous elements (script, style, iframe, object, embed, form,
link, meta) and event handler attributes (onclick, onerror, etc.) from
pasted HTML before inserting into the editor. Also removes javascript:
URLs from href attributes.
DOMParser doesn't execute scripts, but importNode copies all attributes
including event handlers that execute when inserted into the live
document.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Firefox Linux, when typing accented characters with a dead key or
compose key, the space before the character was being deleted. This
happened because the keydown event for the dead key (keyCode 229)
fired before compositionstart, so inInternationalComposition wasn't
set yet and observeChangesAroundSelection() ran prematurely, capturing
a pre-composition DOM state.
Now treats keyCode 229 (the standard IME/composition keyCode) the same
as other half-character inputs: defers the idle timer and suppresses
normalization until the composition completes.
Fixes#5623
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed min-width and max-width on .popup-content to use min() with
viewport-relative units so the popup doesn't overflow on screens
narrower than 300px, keeping the close button accessible.
Fixes#7246
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser extensions (BitWarden, Dashlane, etc.) inject scripts that can
throw errors caught by Etherpad's global exception handler, showing a
scary error popup and sometimes blocking the editor from loading.
Two fixes:
- globalExceptionHandler (pad_utils.ts): Skip errors where the source
URL matches moz-extension://, chrome-extension://, or
safari-extension:// patterns.
- Ace2Editor.init (ace.ts): The eventFired() error callback now checks
if the error event's target src is a browser extension and ignores
it, preventing extension-injected script failures from killing
editor initialization.
Fixes#6802
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: POST API requests with JSON body no longer time out
When express.json() middleware parses the request body before the
OpenAPI handler runs, formidable's IncomingForm hangs forever waiting
for stream data that was already consumed. Now checks req.body first
and only falls back to formidable for multipart/form-data requests.
Also fixed case-insensitive method check (c.request.method may be
uppercase depending on openapi-backend version).
Fixes#7127
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle empty JSON body and missing method safely
- Remove Object.keys().length > 0 check on req.body so empty JSON
objects ({}) don't fall through to formidable (which would hang)
- Guard c.request.method with fallback to empty string to prevent
TypeError if method is undefined
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: prevent parameter pollution by excluding headers from field merge
Previously Object.assign merged headers, params, query, and formData
into a single fields object. This allowed POST body parameters to
override security-sensitive headers like Authorization, or headers to
pollute API parameter values.
Now only merges params, query, and formData. The Authorization header
is passed explicitly as a fallback for legacy API key authentication,
but cannot be overridden by body/query parameters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: customLocaleStrings not applied due to aggressive locale caching
The admin panel's i18next backend used fetch with cache: "force-cache",
causing the browser to serve stale locale JSON even after the server
restarted with new customLocaleStrings in settings.json. The server
already sets appropriate Cache-Control headers (max-age based on
settings.maxAge), so the client-side force-cache was redundant and
prevented custom strings from appearing.
Fixes#6390
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: URL lang param now reliably overrides server default language
getParams() was processing server options first and URL params second,
both calling html10n.localize() for the lang setting. Since localize()
is async, the two calls raced and the result was nondeterministic.
Now processes each setting once: URL param wins if present, otherwise
falls back to server option. This eliminates the race condition.
Fixes#5510
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: window._() localization function always available for plugins
The html10n gettext shortcut window._ was only set if window._ was
undefined, but underscore.js was already setting it via the esbuild
bundle. Since internal code uses underscore via require() not window._,
it's safe to always set window._ to html10n.get so plugins can use
window._() for localization in hooks like documentReady.
Fixes#6627
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: make cookie names configurable with prefix setting
Add cookie.prefix setting (default "ep_") that gets prepended to all
cookie names set by Etherpad. This prevents conflicts with other
applications on the same domain that use generic cookie names like
"sessionID" or "token".
Affected cookies: token, sessionID, language, prefs/prefsHttp,
express_sid.
The prefix is passed to the client via clientVars.cookiePrefix in the
bootstrap templates so it's available before the handshake. Server-side
cookie reads fall back to unprefixed names for backward compatibility
during migration.
Fixes#664
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: default cookie prefix to empty string for backward compatibility
Changing the default to "ep_" would invalidate all existing sessions
on upgrade since express-session only looks for the configured cookie
name. Default to "" (no prefix) so upgrades are non-breaking — users
opt-in to prefixed names by setting cookie.prefix in settings.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — cookie prefix migration and fallbacks
- l10n.ts: Read prefixed language cookie with fallback to unprefixed
- welcome.ts: Use cookiePrefix for token transfer reads
- timeslider.ts: Use prefix for sessionID in socket messages
- pad_cookie.ts: Fall back to unprefixed prefs cookie for migration
- indexBootstrap.js: Pass cookiePrefix via clientVars to welcome page
- specialpages.ts: Pass settings to indexBootstrap template
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: escape regex metacharacters in cookie prefix, document Vite hardcode
- l10n.ts: Escape special regex characters in cookiePrefix before using
it in RegExp constructor to prevent runtime errors
- padViteBootstrap.js: Add comment noting the hardcoded prefix is
dev-only and must match settings.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: validate cookie prefix to prevent header injection
Reject cookie.prefix values containing characters outside
[a-zA-Z0-9_-] to prevent HTTP header injection via crafted cookie
names (e.g., \r\n sequences). Falls back to empty prefix with an
error log.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: list bugs — indent export, renumber performance, and batching
Addresses four list-related bugs:
#4426: Indented text exports as bulleted lists. Added list-style-type:none
to indent-type <ul> elements in ExportHtml.ts so exported indented content
doesn't show bullet markers.
#3504 / #5546: List operations (indent, outdent, toggle) on large lists
are O(n²) because renumberList() runs after each individual line change.
Added _skipRenumber batching flag to setLineListType() — bulk operations
in doInsertList() and doIndentOutdent() now set all line types first,
then renumber once at the end.
#6471: Ordered list numbering in exports — the start attribute is already
read from the pad's atext during export. The client-side renumberList()
correctly sets start attributes which are persisted. Added export test
to verify numbering is preserved across bullet interruptions.
Fixes#4426, #3504, #5546
Related: #6471
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — exception safety, batch removal, renumber scope
- Wrap _skipRenumber in try/finally to prevent permanent disabling on error
- Move list removal (togglingOff) into the batched mods array instead of
calling setLineListType directly (fixes O(n²) for list removal)
- Use firstLine instead of mods[0][0] for renumbering since the first
mod may be an indent/removal that renumberList skips
- Rewrite indent export test to actually create indent lines via setHTML
and unconditionally assert list-style-type:none is present
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rewrite export tests to use importHtml/exportHtml directly
The HTTP API approach (setHTML via supertest) was hanging when tests
ran standalone because the API endpoint waited for something in the
request pipeline. Using importHtml.setPadHTML and exportHtml.getPadHTML
directly is faster and more reliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update importexport test to expect list-style-type on indent ul
The indent export fix adds style="list-style-type: none;" to indent
<ul> elements, which broke the golden test string comparison.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: accessibility — keyboard trap, screen reader support, aria-live
Three accessibility fixes:
#6581 (WCAG 2.1.2 keyboard trap): Escape key now moves focus from the
editor to the first toolbar button, giving keyboard-only users an
escape route. Added a screen-reader-only hint about Escape and Alt+F9.
#7255 (screen reader access): Added role="textbox", aria-multiline="true",
and aria-label="Pad content" to the contenteditable body so screen
readers can identify and interact with the editor content. Fixed
non-standard aria-role="document" to role="document" in pad.html.
#5695 (aria-live character echo): Removed aria-live="assertive" from
every line div in domline.ts. This was causing screen readers to
announce every character typed, overriding users' keyboard echo
settings. The attribute was added in PR #5149 for JAWS compatibility
but aria-live on individual contenteditable lines is a misuse.
Also added .sr-only CSS utility class for visually hidden content.
Fixes#6581, #7255, #5695
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Escape closes gritters first, only exits editor if nothing to dismiss
If gritter popups are visible, Escape closes them and keeps focus in
the editor. Only when there are no popups does Escape move focus to
the toolbar for keyboard trap escape.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — keyboard hint in iframe, aria-readonly
- Move keyboard hint (Escape/Alt+F9) inside the inner iframe with
aria-describedby so screen readers announce it when focusing the
editor. Previously it was on the outer editorcontainer which is a
different document context.
- Set aria-readonly on the editor body when in readonly mode so screen
readers correctly convey editability state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 'list' reporter alongside 'github' reporter in CI. The 'github'
reporter only shows failures as PR annotations. The 'list' reporter
shows each test with pass/fail status in the log output, making it
easy to see which tests ran and passed.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
numConnectedUsers in CLIENT_VARS was computed from roomSockets.length
before the new socket joined the room, so the joining user always saw
a count one less than the actual number. Added +1 to include the
joining user in the count.
Fixes#6145
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pad object's toJSON() intentionally strips the id property (since
it's part of the database key), which caused confusion when plugins
serialized the hook context. Adding padId as a top-level property on
the hook context makes it directly accessible without relying on the
pad object's internal properties.
Fixes#5814
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: appendText API now attributes text to the specified author
spliceText() was calling makeSplice() without passing author attributes,
so inserted text had no authorship attribution in the changeset — even
though the authorId was recorded in the revision metadata. Now passes
[['author', authorId]] and the pool to makeSplice() so the changeset
ops carry the author attribute, making the text show the author's color
in the editor and appear in listAuthorsOfPad.
Also fixed the same issue in pad init (first changeset creation) and
updated PadType interface to include the authorId parameter.
Fixes#6873
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: assert API response code on createPad and anonymous appendText
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: consecutive numbering fails after indented sub-bullets
The applyNumberList() function in renumberList() checked
listType[0] === 'indent' but after regex exec, listType[0] is the
full match (e.g., "indent1"), never just "indent". Changed to
listType[1] which is the capture group containing just the type name.
This caused indent-type lines to not be recognized during renumbering,
breaking the numbering sequence when numbered lists followed indented
content.
Fixes#5718
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: assert sub-bullet indent and remove redundant waitForTimeout
- Assert .list-bullet2 exists after Tab to verify indent precondition
- Remove waitForTimeout(500) since toHaveAttribute already waits 5s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: skip identity changesets during timeslider playback
When a pad's revision history contains an identity changeset (Z:N>0$,
representing no actual change), the timeslider playback would crash or
break because broadcast.ts tried to apply it via mutateAttributionLines
and mutateTextLines.
Now all three applyChangeset call sites in broadcast.ts check for
identity changesets using the existing isIdentity() helper and skip
them. This also prevents errors when compose() produces an identity
changeset from multiple revisions that cancel each other out.
Fixes: https://github.com/ether/etherpad-lite/issues/5214
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move identity changeset check inside applyChangeset
Move the isIdentity() guard from the call sites into applyChangeset()
itself, so that identity changesets still advance currentRevision,
currentTime, slider position, and author UI — just skipping the
mutation (mutateAttributionLines/mutateTextLines). This prevents the
timeslider from getting stuck on a stale revision when an identity
changeset is encountered.
Also removes unused `identity` import.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: improve timeslider identity changeset test coverage
- Verify slider position advances during playback (confirms revisions
including identity changesets are processed, not skipped)
- Scrub through every revision individually instead of just rev 0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: timeslider playback test starts from rev 0
The test was starting playback from the latest revision, so the slider
had nowhere to advance — causing the position assertion to fail in CI.
Now navigates to #0 first so playback progresses through all revisions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove stale identity-skip comment from goToRevision
The isIdentity() check was moved inside applyChangeset() but the old
comment remained at the call sites, creating a misleading code/comment
mismatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: createDiffHTML API fails with "Not a changeset: undefined"
Root cause: An empty prototype override (`PadDiff.prototype._createDeletionChangeset = function() {}`)
silently replaced the real class method with a no-op returning undefined.
This caused `applyToAText(undefined, ...)` to throw "Not a changeset".
Also fixed a crash when `startRev === endRev`: the `self` property used
to access `_authors` was only initialized inside `_addAuthors()`, which
is never called when there are no changesets to process. Replaced all
`this.self!._authors` with direct `this._authors` access.
Fixes#6847
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — random pad ID and assert cleanup
- Use random ID instead of Date.now() to avoid collisions in parallel runs
- Assert HTTP 200 on deletePad in after() hook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: suppress internal error details from users in production mode
In production mode (NODE_ENV=production), the client-side error handler
now shows a generic "reload the page" message with just the ErrorId
instead of leaking internal details like error messages, file paths,
line numbers, stack traces, and user agent strings.
In development mode, the full error details are still shown for
debugging.
The basic_error_handler (pre-initialization errors) now always shows a
generic message and logs details to the console instead of displaying
them in the DOM.
The server-side jserror endpoint still receives full error details for
server-side logging — only the user-facing display is suppressed.
Fixes: https://github.com/ether/etherpad-lite/issues/5765
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: secure-by-default error sanitization and dedup fixes
Address Qodo review concerns:
- Flip mode check to `!== 'development'` (secure by default) so errors
before CLIENT_VARS handshake hide internal details
- Default server-side mode to 'development' when NODE_ENV is unset
- Replace DOM-based gritter dedup with in-memory Set so dedup works in
production mode (where .error-msg element is absent)
- Add Playwright regression tests for error sanitization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: revert server-side mode default to preserve secure-by-default
Don't default mode to 'development' when NODE_ENV is unset — that would
defeat the client-side secure-by-default check. Let mode be undefined so
the client's `!== 'development'` check correctly hides error details.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: report early page load errors to server via sendBeacon
basic_error_handler now sends error details to ../jserror using
navigator.sendBeacon with FormData, matching the format expected by
the server's Formidable parser. Includes an errorId shown to the user
for correlation with server logs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* revert: restore original basic_error_handler with full error details
The basic_error_handler runs before the pad framework loads, so it
should show full details to help developers debug bootstrap failures.
Removes the sendBeacon reporting and error sanitization added in the
previous commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add detailed local test running guide to AGENTS.md
Expand the Testing & Validation section with step-by-step instructions
for running backend (Mocha), frontend E2E (Playwright), and admin panel
tests locally, including prerequisites and single-file examples.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add Playwright browser prerequisite check to AGENTS.md
Agents and developers should verify Playwright browsers and system
dependencies are installed before running frontend/admin tests to
avoid silent failures and timeouts (especially webkit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix plugin test path and browser list per review
- Plugin tests are at repo-root node_modules/, not src/node_modules/
- Playwright runs chromium and firefox only (webkit is disabled)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix install-deps working directory to run from src/
Playwright is a devDependency of the src workspace, so install-deps
must also run from src/ to match the correct Playwright version.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PageDown was broken — it moved the caret to the last visible line of
the current viewport instead of advancing by one page. This caused it
to get "stuck" at the bottom of the viewport.
The old code set the caret to oldVisibleLineRange[1] - 1 (the last
visible line), which was essentially a no-op for scrolling. The fix
mirrors the PageUp logic: advance/retreat by numberOfLinesInViewport.
Also simplified the clamping logic for both selStart and selEnd.
Fixes: https://github.com/ether/etherpad-lite/issues/6710
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: filter already-deleted sessions when deleting a group
deleteSession uses setSub(..., undefined) to remove session references
from group2sessions and author2sessions, but this can leave null entries
in the sessionIDs object. When deleteGroup later iterates Object.keys
of sessionIDs and calls deleteSession on each, it throws "sessionID
does not exist" for the already-deleted sessions.
Now deleteGroup filters out null/falsy session entries before attempting
to delete them.
Fixes: https://github.com/ether/etherpad-lite/issues/5798
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression test for deleteGroup after deleteSession (#5798)
Creates a group, author, and session, then deletes the session first,
then deletes the group. Without the fix, deleteGroup would throw
"sessionID does not exist" when encountering the null session entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an ordered list followed directly after an unordered list (no blank
line between), all OL items showed "1" instead of incrementing. This was
because renumberList's applyNumberList function counted bullet items in
the position counter, so the first OL item got start=3 (after 2 bullet
items) instead of start=1, preventing the CSS counter-reset class from
being applied.
The fix resets the position counter when the list type changes at the
same level (e.g., bullet -> number).
Fixes: https://github.com/ether/etherpad-lite/issues/5160
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: wait for server confirmation before navigating after pad delete
The delete pad handler navigated to '/' immediately after sending the
PAD_DELETE message. Firefox (and some mobile Chrome) would close the
WebSocket before the message reached the server, causing the delete to
silently fail.
Now the client waits for the server's {disconnect: 'deleted'} response
before navigating. Also awaits pad.remove() on the server side to
ensure the operation completes before the response is sent.
Fixes: https://github.com/ether/etherpad-lite/issues/7306
Fixes: https://github.com/ether/etherpad-lite/issues/7311
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle non-creator delete and add timeout fallback
- Listen for 'shout' event to show error when non-creator tries to
delete (server sends shoutMessage instead of deleting)
- Add 5-second timeout fallback in case the server doesn't respond
(socket dropped, server crashed, etc.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When HTML containing <li> elements without a wrapping <ul> or <ol> is
pasted (e.g., from ChatGPT), the contentcollector crashes with
"TypeError: lineAttributes.list is undefined" because it assumes
_enterList() was already called by a parent list element.
The fix defaults bare <li> elements to bullet1 list type and properly
sets oldListTypeOrNull so the list state is cleaned up after the <li>
is processed. Also guards the .indexOf() call on lineAttributes.list.
Fixes: https://github.com/ether/etherpad-lite/issues/6665
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop webkit from CI workflow and Playwright config (Chrome + Firefox
are the supported browsers)
- Set retries: 2 in CI to handle intermittent failures from timing
sensitive operations (list attribute clearing, server restarts)
- Fix clearAuthorship helper to use force:true to bypass toolbar-overlay
div that intermittently intercepts clicks after text selection
- Fix admin restartEtherpad helper: increase poll intervals, add
explicit timeout, use toHaveValue with timeout instead of toBeEmpty
- Convert clear_authorship_color tests to use Playwright auto-retry
assertions (toHaveAttribute) instead of one-shot getAttribute calls
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Enforce 2-space indentation across codebase
Convert all 4-space indented source files to 2-space to match
.editorconfig and project contributor guidelines.
74 files converted: admin UI components, type definitions, security
modules, test files, helpers, and utilities.
No functional changes — 2882 insertions, 2882 deletions (pure
whitespace).
Fixes#7353
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Limit admin tests to chromium and firefox
Webkit is already tested in the dedicated frontend-tests workflow.
Running it again in admin tests causes flaky failures due to slow
socket connections and external API timeouts on webkit CI runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Load tests are slow and don't need to run on every push. Schedule
daily at 08:00 UTC with manual trigger option.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The settings textarea content is populated asynchronously via socket.
On slow CI (especially Node 20 + Firefox), the default 20s timeout
isn't enough. Increase to 30s for all toBeEmpty checks.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix flaky undo keypress test
Each Ctrl+Z may only undo one keystroke depending on how Etherpad
batches undo operations (varies between dev and prod mode). Loop
Ctrl+Z presses until content is restored instead of assuming one
press undoes everything.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix flaky admin restart test causing cascading failures
restartEtherpad used a hardcoded 500ms wait which wasn't enough for
the server to restart on slow CI. Subsequent tests got
ERR_CONNECTION_REFUSED because the server was still down.
- Poll the server until it responds instead of hardcoded timeout
- Re-login after restart since the session cookie is lost
- Remove unnecessary waitForTimeout(5000) at end of test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Speed up restart poll: accept any response, poll at 500ms
The previous check required response.ok() (2xx) but the server
returns redirects (3xx) which caused the loop to run all 30 seconds.
Accept any non-zero status and reduce poll interval to 500ms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix loginToAdmin: navigate to /admin/login directly
The admin SPA only shows the login form at /admin/login. Navigating to
/admin/ loads the HomePage route which doesn't have the login fields.
After a server restart the session is lost, but the SPA doesn't
automatically redirect to /admin/login, causing the test to timeout
waiting for input[name="username"].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix plugin search test: wait for results, increase timeout
The search is debounce-triggered (500ms), not Enter-triggered. The
toHaveCount(1) check passed on the "not found" row before results
loaded. Removed the Enter press and count check, increased timeout
to 30s since the search depends on an external API call to
static.etherpad.org.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix plugin search: normalize numeric keys from registry
The plugin registry at static.etherpad.org/plugins.json changed format
from {ep_name: {data}} to {index: {name: "ep_name", data}}. The search
code iterated keys expecting plugin names starting with "ep_", but got
numeric keys like "41", skipping all plugins. Normalize the data after
fetching to use plugin names as keys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Pin plugins to last-known-good versions in backend tests
Pin ep_font_size@0.4.65, ep_headings2@0.2.76, ep_markdown@10.0.1
to the versions that passed on March 31. The newer versions cause
a template crash: Cannot read properties of undefined (reading
'indexOf') at pad.html:67 in toolbar.menu().
This will help narrow down which plugin update is the culprit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Unpin ep_markdown, 1.0.8 is latest and code-identical to 10.0.1
Only ep_font_size@0.4.65 and ep_headings2@0.2.76 remain pinned to
narrow down which plugin update causes the toolbar template crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use pnpm instead of gnpm for plugin install in backend tests
gnpm ignores version pins — it reports installing the pinned version
but the plugin loader picks up the latest from its store. Switching
to pnpm for the plugin install step so version pins actually work.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use gnpm exec pnpm for plugin install to bypass gnpm caching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove ep_hash_auth from backend test plugin list
ep_hash_auth blocks unauthenticated requests, causing 28 backend tests
to get 500 Internal Server Error when accessing pads. The tests don't
provide credentials, so any auth plugin will break them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ESM/CJS interop for Settings module and harden toolbar
Plugins use require('ep_etherpad-lite/node/utils/Settings') (CJS) but
Settings.ts uses export default (ESM). With tsx, CJS require puts the
default export under .default, so settings.toolbar is undefined and
ep_font_size crashes with "Cannot read properties of undefined
(reading 'indexOf')" when rendering pad.html.
Two fixes:
- Settings.ts: add property getters on module.exports so CJS consumers
can access settings properties directly
- toolbar.ts: guard against undefined buttons array to prevent crashes
if Settings interop doesn't propagate through gnpm's plugin_packages
Tested locally: 735 passing, 0 failing with all plugins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix flaky unordered_list and undo tests
- unordered_list: re-query the DOM element after clearPadContent()
instead of holding a stale reference that detaches when the editor
rebuilds the DOM.
- undo: remove hardcoded waitForTimeout(1000) and re-query the element
so Playwright's auto-retry on toHaveText handles the timing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix flaky tests: wait for DOM settle, undo all keystrokes
- unordered_list: wait for DOM to settle after clearPadContent() with
toHaveCount assertion, then use click() instead of selectText() which
races with async DOM rebuilds.
- undo: press Ctrl+Z three times since writeToPad('foo') produces 3
keystrokes and each Ctrl+Z only undoes one.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix undo keypress test: don't clear pad before typing
Clearing the pad added an undoable operation to the history, so Ctrl+Z
could undo past the typing and restore the original welcome text.
Simplified to match the button-based undo test: type on existing
content and undo once, since Etherpad batches rapid keystrokes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix undo keypress test: restore clear step, mirror button test
writeToPad needs clearPadContent first to ensure focus is in the
editor iframe. Without it, keyboard.type() doesn't reach the editor.
Now mirrors the button-based undo test exactly, just using Ctrl+Z
instead of clicking the undo button, and without the hardcoded
waitForTimeout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix frontend test failures across all browsers
- Fix home button using fragile relative URL (window.location.href +
"/../..") that WebKit doesn't resolve correctly. Use
window.location.origin instead.
- Wait for #editorcontainer.initialized in goToNewPad/goToPad/
appendQueryParams so toolbar, chat, and cookie handlers are fully
set up before tests interact with them.
- Clear cookies in chat test beforeEach to prevent chatAndUsers cookie
from prior tests disabling the sticky chat checkbox.
- Wait for navigation to complete in editbar home button test.
Fixes#7405
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Run frontend tests on pull requests
Playwright runs locally and doesn't need Sauce Labs secrets, so
there's no reason to limit frontend tests to push events only.
Also remove stale Sauce Labs references from workflow names/comments.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix sticky chat test: use click() instead of check()/uncheck()
The stickToScreen() handler manages checkbox state internally with its
own toggle logic and a setTimeout. Playwright's check()/uncheck()
methods verify state after clicking, but race with the async toggle,
causing "Clicking the checkbox did not change its state" errors.
Using click() avoids this — the waitForSelector calls already verify
the final state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix sticky chat handler and reduce parallel workers
- Remove force:true from sticky chat checkbox clicks — it can bypass
jQuery event handlers preventing stickToScreen() from firing.
- Wait for chatbox stickyChat class instead of checkbox state, since
stickToScreen() manages the checkbox asynchronously via setTimeout.
- Reduce workers from 5 to 2 to avoid overloading the single Etherpad
server instance, which causes goToNewPad timeouts on CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up workflows: remove Sauce Labs, load test push-only
- Remove all Sauce Labs references (steps, comments, secrets) from
frontend test workflows — Playwright replaced Sauce Labs
- Remove unused set-output steps and GIT_HASH exports
- Remove stale commented-out code from admin tests
- Restrict load test to push events only (no need on PRs)
- Fix artifact names to not reference undefined matrix.node
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix sticky chat test: click label instead of checkbox
The label element intercepts pointer events on the checkbox (reported
by Webkit). On Chrome/Firefox the checkbox is "not stable" due to
animations. Clicking the label is how a real user interacts with it
and properly triggers the jQuery click handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix home button to preserve subpath installations
Use URL API to resolve '../..' relative to current URL instead of
hardcoding origin + '/'. This preserves any configured base path
(e.g. /etherpad) for reverse-proxy installations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: re-apply retries: 0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Enable globstar in backend-tests template for recursive test discovery
Without shopt -s globstar, bash ** doesn't recurse into subdirectories,
causing mocha to miss test files in paths like specs/api/exportHTML.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set retries to 0 so test failures are reported honestly. With retries: 2,
tests could fail twice and still pass on the third attempt, hiding real
bugs as "flaky" tests that count as passing.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the 2020 workaround that disabled nice-select on Safari due to
a position:fixed + overflow:hidden rendering bug. This bug has been
fixed in modern WebKit, and disabling nice-select meant Safari/WebKit
users got native selects while tests expected the custom dropdowns,
causing all font_type and language tests to fail on webkit.
Fixes#7405
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>