Skip to main content

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

Each tests/e2e/<area>/<name>.json file is a standalone suite. It must:
  1. Optionally delete leftover names ("optional": true)
  2. Seed — create every entity it needs
  3. Act — behavior under test
  4. Teardown — delete what it created
Do not depend on another JSON file (e.g. 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 in dependsOn
  • dependsOn — string or string[] of same-file ids only (act/seed cases)
  • teardown: true — cleanup case; must not use dependsOn; usually also optional: true. On filtered runs, early teardowns (before first act in the file) run first; late teardowns run after the act chain.
Case filter: 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:
QMod example tests/e2e/mod/.env.json:
Omit unused bags. The harness loads .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 passes threadId (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
A known message row is never widened to the thread or the page: earlier runs leave dead copies of the same button in the thread, and clicking one looks like a successful press that Discord silently drops. Clicks settle before they fire: the driver scrolls the target into view, waits for its rect to stop moving (embeds loading and new messages shift the list), and only dispatches once the click point hit-tests back to the target. Without that, the mouse events land on a neighbour and the press looks fine while Discord sends no interaction at all. A click that never gets a clear point names whatever sat on top of it. 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 in steps is one UI action — no nested plan:
Supported keys per step: command, press, select, modal, values, focusChannel, focusThread, type, key, wait, payload.
  • select: 1-based index of Discord select triggers under the scoped message/list. values is 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. focusThread alone is invalid. Aliases: test-logs (parent) + test-thread (harness thread).

UI-ready gate

  • press / modal steps wait for CustomForm test / ui-ready (labels from live message button components only) before acting.
  • ui-ready is 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). modal also 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.
  • select values may be [1] / [n] to pick by position only when the same case controls that menu; prefer named / .env.json values 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 in tickets/.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.