Skip to content

test(ui): add UserButton connected integration test - #9191

Open
alexcarpenter wants to merge 19 commits into
carp/account-button-controllerfrom
carp/account-button-integration
Open

test(ui): add UserButton connected integration test#9191
alexcarpenter wants to merge 19 commits into
carp/account-button-controllerfrom
carp/account-button-integration

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jul 17, 2026

Copy link
Copy Markdown
Member

Stacked on #9185. Adds busy/loading UX to the connected UserButton + an end-to-end integration test.

Changes

Busy/loading states (controller + container)

  • Container tracks single in-flight action via pendingKey. Clicked affordance spins, others disable (blocks double-submit). Popover closes only on success; busy clears on rejection so UI never hangs.
  • Skeleton trigger while controller loads.
  • Async callbacks typed void | Promise<unknown> to satisfy no-misused-promises; fire-and-forget navigations void-wrapped.

Spin-delay (useSpinDelay hook + tests)

  • Leaner rework of smeijer/spin-delay around one nullable T | null flag. Fast actions resolve before delay → no spinner flash; any spinner shown held for min duration → no flicker. Re-entry still guarded on immediate pendingKey; only visuals delayed.

View decomposition (1032 → ~600 lines)

  • Slot recipe → user-button.recipe.ts, data-contract types + userButtonBusyKeysuser-button.types.ts.
  • BusyButton primitive replaces 4 hand-wired busy buttons (inline join/accept, hover sign-out, header actions, footer sign-out-all).
  • View props renamed UserButtonViewProps; dead status field dropped. No behavior change.

Spinner Mosaic primitive.

Integration test (14 tests)

Renders real view through real controller against mocked @clerk/shared/react, drives real popover + rows/actions, asserts Clerk calls + close-on-success.

  • Nothing rendered while loading / no active user
  • Trigger renders, popover opens on click
  • Select org / personal → setActive(...) + close
  • Switch account → setActive({session}) + close
  • Sign out session / all → signOut(...) + close
  • Accept invitation / suggestion → accept() + revalidate() + close
  • Manage org / account, Create org → navigate(...), popover stays open

Covers every closeOnSuccess callback + every navigation through real DOM.

Not re-tested here (owned elsewhere): a11y/focus-trap (popover primitive suite), data derivation/gating (controller.test.tsx). Per-row sign-out is hover-revealed (pointer-events:none), so that one uses fireEvent.click (noted inline).

Parity gaps (tracked separately, not test gaps): no hidePersonal, force-org-selection, or trigger pending-counter badge yet.

Changeset

Empty — feature ships under #9184 / #9185, no user-facing change here.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 5, 2026 6:40pm
swingset Ready Ready Preview Aug 5, 2026 6:40pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 178fb88

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a7f3fe3-0f9b-4531-87b3-6dfbb33b079e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jul 17, 2026
@macroscopeapp

macroscopeapp Bot commented Jul 17, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved

This PR adds integration tests for the AccountButton component with no production code changes. The empty changeset confirms no packages are affected, making this a low-risk test-only addition.

You can customize Macroscope's approvability policy. Learn more.

@vercel
vercel Bot temporarily deployed to Preview – clerk-js-sandbox July 20, 2026 21:20 Inactive
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-integration branch from 22e6748 to 4f890d8 Compare July 30, 2026 18:38
@alexcarpenter alexcarpenter changed the title test(ui): add AccountButton connected integration test test(ui): add UserButton connected integration test Jul 30, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9191

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9191

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9191

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9191

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9191

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9191

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9191

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9191

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9191

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9191

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9191

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9191

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9191

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9191

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9191

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9191

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9191

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9191

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9191

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9191

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9191

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9191

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9191

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9191

commit: 178fb88

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-05T18:42:33.623Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 178fb88.

Render the connected UserButton against stubbed Clerk hooks and drive it through
the popover: organization selection, account switching, sign out, accepting
invitations and suggestions, single-session mode, navigation actions, busy and
loading states, and the paging sentinel.
Every other action — switching account, signing out of one, joining a
suggested or invited workspace — now resolves back into an open popover so
the result is visible where it happened. The swingset prototypes fake the
round trip they make against Clerk, so the spinner and stood-down rows are
demonstrable without a running app.
The sizes track the Icon scale (sm 14px, md 16px) so a spinner can stand in
for the icon it replaces, and the UserButton's trailing column is now one
slot the width of the menu button, so the spinner, the active check, and the
menu all sit on the same centre line.
The trigger carried the avatar alone. It now names what is active beside it — the
organization and its plan wherever one heads the trigger, the account otherwise —
behind `showLabel`, which defaults on.

Badge's `neutral` color was unreadable in both schemes: its fill is a 900 and its
text token is a text color, not an on-fill one. It now rides the same black/white
scrim the button's neutral fill does.
…ccount

The trigger and the popup's header now always name the same workspace. `combined`
carries both switchers, so `modePriority` picks which one it leads with: the active
organization by default, the account with `modePriority="user"`. Both are still listed
either way.
…ser fixture

The controller now reads hasOrganizations off the user resource, so the mocked user needs the field the real one has.
`setActive` swaps the active organization while its promise is still in
flight, so the popup rearranged mid-action: the header renamed itself, the
check jumped rows, and Invite came and went as the permission was re-read.
The connected component now snapshots the controller when an action starts
and renders that until it settles, so the result lands in one step.

Two smaller faults fell out of the same interaction:

- The spinner waited out a delay window before appearing, and the check
  raced ahead of it. Every action here is a network round trip, so there
  is nothing to debounce: `useSpinDelay` takes `delay: 0` and shows the
  value in the same pass, with `minDuration` still steadying it.

- A row going busy swapped its host element from `<button>` to `<div>`,
  remounting the subtree and dropping the avatar back to its initials for
  the length of the action. A row that stands down now stays the button it
  was, disabled, and `Avatar.Image` resolves a browser-cached `src` in a
  layout effect so neither a remount nor a swap flashes the fallback.
The popover stayed up behind the surface it opened. Managing, inviting, creating an
organization, and adding an account now close it on the way out, whether they open a modal
or navigate.
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
The placeholder takes its corner from the mode, so the connected component has to hand it
down before any data lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant