Full UI testing
Harness drives Discord web through#clicker → .chrome-clicker (Chrome CDP). Fixtures under tests/e2e/ issue real slash commands and button presses.
npm test reuses Chrome when CDP is already reachable; otherwise its pretest
starts Chrome with a dedicated profile automatically. Log into Discord web once
in that profile. Optional CLICKER_CDP_URL defaults to
http://127.0.0.1:9222. See .chrome-clicker/README.md.
Preprocess does a one-time navigate to
https://discord.com/channels/{TEST_SERVER}/{TEST_LOGS_CHANNEL}, then opens the
harness thread with openThread (sidebar click on the logs channel, then the
thread starter). Mid-run focus never changes the URL — only openChannel /
openThread clicks.
Independent suites
Eachtests/e2e/<area>/<name>.json file is a standalone suite. It must:
- Optionally delete leftover names (
"optional": true) - Seed — create every entity it needs
- Act — behavior under test
- Teardown — delete what it created
panel.json must not assume admin.json already created e2e_embed). Use distinct e2e_* names per suite file when they might collide. Cases inside one file may share state; if an earlier create fails, a later get/press fails — that is enough signal (no DB asserts).
Optional same-file metadata for those chains:
id— stable slug, unique in the file; required if another case lists it independsOndependsOn— string or string[] of same-file ids only (act/seed cases)teardown: true— cleanup case; must not usedependsOn; usually alsooptional: true. On filtered runs, early teardowns (before first act in the file) run first; late teardowns run after the act chain.
npm test -- tickets:ops#ops-claim-close or multiple tickets:ops#ops-claim-close,ops-slash-claim-close. Harness expands dependsOn transitively (topo order, file-order tie-break) and appends file teardowns. If a required dep failed earlier, it is re-run once; if it fails again, the dependent fails with dependency **#N** failed: … (N = 1-based case number).
Global env vs feature .env.json
Example
tests/e2e/tickets/.env.json:
tests/e2e/mod/.env.json:
.env.json for each feature folder in the run, skips it during fixture discovery, and gates before tests: each feature must be activated; each category/role/channel/member name must exist in TEST_SERVER. Failures look like tickets/.env.json: missing role "Support" or mod/.env.json: missing member "e2e-target".
members entries are display names, usernames, or snowflakes. Fixtures use those names in select / slash user:#Name#[1]. For string options that need a snowflake (e.g. /mod log user-id:), use {member:Name} — the harness substitutes the gated id.
Suggestion admin commands use autocomplete name: like ticket embeds — e.g. name:[~e2e_suggest_admin] (contains-match on the option label). Do not use numeric ids or {suggestion:last}.
Fixture select / command values use those display names (e.g. "e2e-ticket-send"), not bag indexes and not blind [1].
Stable targets
Scoping by id
An open thread leaves its parent channel mounted, so two composers and two message lists exist at once. The harness passesthreadId (and the ui-ready messageId when known) to runPlan, and the driver anchors on Discord’s message-row ids (chat-messages-{channelId}-{messageId}):
- type → composer inside the thread panel, exact label before contains
- select → triggers inside that message row, else the thread’s message list
- press / modal → the message row alone when its id is known, else thread panel then page-wide
modal also waits for the dialog to unmount after Submit, since the backdrop outlives the submit and swallows the next step’s click.
Slash commands commit with a single Tab. The Windows plans send two, but on the web the second Tab starts filling the command’s first argument and the trailing Enter then only closes that picker, leaving the command unsent. The composer is also cleared with Ctrl+A before typing, since a committed command chip survives a triple-click selection and a leftover draft stops the command picker from opening at all.
Before each press / modal / select, the driver logs every visible pressable it can see in that scope, so a miss shows what was actually on screen:
Step shape
Each entry insteps is one UI action — no nested plan:
command, press, select, modal, values, focusChannel, focusThread, type, key, wait, payload.
select: 1-based index of Discord select triggers under the scoped message/list.valuesis a string (single) or string array (multi); each entry is a contains-match on the option Name. Options are clicked, not walked with arrow keys, so nothing depends on scroll position, and a multi-select is toggled until exactly the listed values are on — anything the menu had pre-selected is clicked off. An unmatched value fails with the full option list.press/modal: by accessible Name, narrowest scope first (modal/ Submit stay outside the messages list).focusChannel: click that channel in the guild sidebar. Value is an alias (form-channel,test-logs), a display name, or a Snowflake string. If the target is a thread (e.g. an opened ticket), the harness opens its parent then the starter.focusChannel+focusThread: open the parent via the sidebar, then click the thread starter.focusThreadalone is invalid. Aliases:test-logs(parent) +test-thread(harness thread).
UI-ready gate
press/modalsteps wait for CustomFormtest/ui-ready(labels from live message button components only) before acting.ui-readyis bot-side and fires before Chrome paints the edit, so the driver then polls up to 10s for the label inside its scope, re-resolving the row each attempt (Discord re-renders the list and detaches the old node).modalalso fails with “Modal did not open” rather than “no editable fields” when the press lands on a dead component.- Fixture labels may omit Discord’s emoji prefix:
"press": "Customize"matches✨ Customize, and"modal": "About Me"matches📝 About Me. The step then presses the full rendered label. A timeout logs the labels that were actually seen; if nothing was emitted (e.g. ticket welcome Claim/Close), the fixture label is pressed and the driver polls for it. selectvalues may be[1]/[n]to pick by position only when the same case controls that menu; prefer named /.env.jsonvalues for guild picks.- Non-last steps advance as soon as the UI action succeeds (bag stays bound for mid-form flows).
- The last step waits for the form terminal event (
ended/error/warn).
Tickets fixtures
Each file is independent (own seed + teardown). Guild prereqs live intickets/.env.json.
Run any alone:
npm test -- tickets:panel. Folder run npm test -- tickets runs whatever files are discovered (no required order). Best-effort cleanup cases set "optional": true.
QMod fixtures
Guild prereqs:mod/.env.json (qmod, alert channels, Moderator role, two members — Chrome staff user + mute target).
No live
/mod kick / /mod ban (destructive). No native AutoMod message-fire. Run: npm test -- mod or npm test -- mod:mute-ops.
Suggestions fixtures
Guild prereqs:suggest/.env.json (suggest, channel e2e-suggest-logs).
Run:
npm test -- suggest or npm test -- suggest:admin-ops#admin-approve.
Embed Creator fixtures
Guild prereqs:embed/.env.json (embed, channel e2e-embed-send).
Out of v1: Fields tab, color menu, Footer + Images URLs. Run:
npm test -- embed or npm test -- embed:send.
Reaction Roles fixtures
Guild prereqs:reaction/.env.json (reaction, channel e2e-reaction-send, role e2e-react-role below Quarky’s highest role).
Out of v1: member register toggle, custom emoji / Image URL, Remove Role / multi-role. Run:
npm test -- reaction or npm test -- reaction:send.
Custom Forms fixtures
Guild prereqs:form/.env.json (form, channels e2e-form-send + e2e-form-logs; Chrome user must match private-thread name e2e - <username> — members lists Apex).
Out of v1: other question types, Remove/multi-Q/multi-panel, re-submit, Image/emoji validators, ticket attach-form. Run:
npm test -- form or npm test -- form:send-submit#submit-answer.
Poll fixtures
Guild prereqs:poll/.env.json (channel e2e-poll-send; no feature flag — poll is ungated).
Out of v1: multiple/ranked, Details, images/color, global/whats-next. Run:
npm test -- poll or npm test -- poll:send-vote#vote-act.
Giveaway fixtures
Guild prereqs:giveaway/.env.json (giveaway, channel e2e-giveaway-send).
Out of v1: auto-end/schedule, winners/reroll, host Edit, color/image menus. Run:
npm test -- giveaway or npm test -- giveaway:start-join#join-act.