etherpad-lite/doc/PLUGIN_FEATURE_DISABLES.md
John McLear aee2b79b6a
test: tag rtl_url_param toggle-off specs with @feature:rtl-toggle (#7661)
* test: tag rtl_url_param toggle-off specs with @feature:rtl-toggle

The two cases that require RTL to be flippable away from the
plugin-forced default — `?rtl=false` overriding a prior `?rtl=true`
and a no-param reload falling back to the cookie — are exactly what
ep_right_to_left intentionally disables. Tag them so the plugin can
declare `disables: ["@feature:rtl-toggle"]` and pass the disables
contract's honesty check.

Also list the new tag (and the previously omitted @feature:line-numbers)
in doc/PLUGIN_FEATURE_DISABLES.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: also tag chat title-bar layout spec with @feature:rtl-toggle

The leftGap/rightGap symmetry assertion in this test is LTR-only:
colibris ships a one-sided #titlebar padding rule (the existing
asymmetric pad is fine in LTR because the buttons are on the right
where the larger pad sits) that throws gaps apart by ~170px when
body[dir=rtl] reverses the flex item order.

Without a fix to colibris's chat header padding (out of scope here),
plugins that force RTL on can't pass this assertion. Add the second
tag so they can declare the disable instead of false-failing it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:26:50 +08:00

4.4 KiB

Feature-disabling plugins

Some Etherpad plugins exist specifically to remove a baseline feature — ep_disable_chat, ep_disable_change_author_name, ep_disable_error_messages, and so on. When the plugin is installed, the feature it disables is intentionally absent.

This is awkward for CI: the core test suite asserts the disabled feature works. Without coordination, every disable plugin's CI is permanently red, every dependency bump is permanently stuck, and the green/red signal on etherpad.org/plugins becomes meaningless.

To fix that — without giving plugins a free pass to opt out of arbitrary tests — Etherpad uses a small declared-disables contract.

How it works

1. Core specs are tagged by feature

Tests that exercise a single feature carry a Playwright tag like @feature:chat:

test('opens chat, sends a message, makes sure it exists on the page and hides chat', {
  tag: '@feature:chat',
}, async ({page}) => { ... });

test.describe('error sanitization', { tag: '@feature:error-gritter' }, () => { ... });

Tags currently in use:

  • @feature:chat
  • @feature:username
  • @feature:clear-authorship
  • @feature:error-gritter
  • @feature:line-numbers
  • @feature:rtl-toggle

2. A plugin declares the features it disables in its ep.json

{
  "name": "ep_disable_chat",
  "description": "Disable chat",
  "disables": ["@feature:chat"],
  "parts": [...]
}

The disables field is publicly visible in the plugin's metadata and surfaces on etherpad.org/plugins, so users see the contract before installing.

3. The plugin's CI runs the two-pass test script

bin/run-frontend-tests-with-disables.sh enforces the contract:

# .github/workflows/frontend-tests.yml
- name: Run the frontend tests
  working-directory: ./etherpad-lite/src
  run: ../bin/run-frontend-tests-with-disables.sh -- --project=chromium

The script reads disables (from EP_PLUGIN_DISABLES, an explicit --plugin-ep-json PATH, or auto-detection in plugin_packages/) and runs two passes:

Pass What it runs Must
1. Regression Every spec not tagged with a disabled feature Pass — proves the plugin doesn't break anything beyond what it claims to disable.
2. Honesty Every spec that is tagged with a disabled feature Fail — proves the plugin is genuinely disabling the feature it declares. If those tests pass, the plugin's disables claim is wrong.

Both passes have to succeed for CI to be green.

What this catches

Failure mode Caught by
Plugin breaks an unrelated feature Pass 1 — its tests aren't excluded, they fail, CI red.
Plugin claims to disable a feature but the feature still works Pass 2 — tagged tests pass when they should fail, script exits non-zero.
Plugin breaks a feature without declaring it (so etherpad.org/plugins shows it as harmless) Pass 1 — the feature's tests aren't excluded, they fail, CI red.
Plugin lists a feature in disables it doesn't actually disable Pass 2.

A plugin cannot ship green with broken functionality the user can't see ahead of time.

Adding a new feature tag

When a core spec needs a new feature tag (because a new disable plugin needs to opt out of it):

  1. Pick a stable name: @feature:<area> — short, lowercase, kebab-case, plural where appropriate.
  2. Tag the relevant test() or test.describe() blocks. Multiple tags are fine: tag: ['@feature:chat', '@feature:username'].
  3. Update the list above.
  4. Submit the tag PR before the plugin's PR — the plugin can then declare disables and pass CI.

Adding a new disable plugin

  1. Confirm the feature you're disabling is tagged in core. If not, propose a tag upstream first.
  2. Add "disables": ["@feature:thing"] to your ep.json.
  3. Replace the test invocation in .github/workflows/frontend-tests.yml with a call to bin/run-frontend-tests-with-disables.sh (see ep_disable_chat for a worked example).
  4. Confirm both passes go green locally before opening the PR.

Why not just --grep-invert?

The earlier draft of this design just told plugin maintainers to add --grep-invert "<pattern>" in CI. That works for the regression case (pass 1 above), but it lets a careless or malicious plugin silently skip arbitrary tests and present green CI on etherpad.org/plugins despite breaking unrelated functionality. Pass 2 — and the requirement that disables be declared in ep.json rather than inferred from a CI argument — closes that gap.