* 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>
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):
- Pick a stable name:
@feature:<area>— short, lowercase, kebab-case, plural where appropriate. - Tag the relevant
test()ortest.describe()blocks. Multiple tags are fine:tag: ['@feature:chat', '@feature:username']. - Update the list above.
- Submit the tag PR before the plugin's PR — the plugin can then declare
disablesand pass CI.
Adding a new disable plugin
- Confirm the feature you're disabling is tagged in core. If not, propose a tag upstream first.
- Add
"disables": ["@feature:thing"]to yourep.json. - Replace the test invocation in
.github/workflows/frontend-tests.ymlwith a call tobin/run-frontend-tests-with-disables.sh(seeep_disable_chatfor a worked example). - 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.