Skip to content

chore: bump uPortal 5.17.4 + resource-server 1.5.3 + 2026-05 fleet release wave + post-wave patches#692

Merged
bjagg merged 11 commits into
uPortal-Project:masterfrom
bjagg:chore/bump-uportal-5.17.3-resource-server-1.5.3
May 5, 2026
Merged

chore: bump uPortal 5.17.4 + resource-server 1.5.3 + 2026-05 fleet release wave + post-wave patches#692
bjagg merged 11 commits into
uPortal-Project:masterfrom
bjagg:chore/bump-uportal-5.17.3-resource-server-1.5.3

Conversation

@bjagg

@bjagg bjagg commented Apr 29, 2026

Copy link
Copy Markdown
Member

Pull all the version pins forward to the 2026-05 fleet release baseline plus the post-wave patches that fixed regressions caught during smoke testing.

Versions bumped

uPortal core: uPortalVersion 5.17.2 → 5.17.4
Resource-server (modern, webjar-served): resourceServerWebjarVersion 1.5.0 → 1.5.3

Tier 2 portlets — wave + post-wave patches:

Portlet Wave Patch
AnnouncementsPortlet 2.5.2 2.5.3
basiclti-portlet 1.5.1 (no patch)
BookmarksPortlet 1.3.1 (no patch)
CalendarPortlet 2.7.1 2.7.2
FeedbackPortlet 1.3.1 (no patch)
jasig-widget-portlets 2.4.1 2.4.2
NewsReaderPortlet 5.1.2 5.1.4 (skipping 5.1.3 — no functional changes)
SimpleContentPortlet 3.4.1 3.4.2
WebProxyPortlet 2.4.1 (no patch)

Tier 1: notificationPortletVersion 4.8.0 → 4.8.3 (4.8.2 wave + 4.8.3 patch)

Smoke-test result

Built locally against released artifacts:

  • ./gradlew portalInit → BUILD SUCCESSFUL (including NewsReader's dataInit which previously failed with LRUMap error)
  • ./gradlew tomcatStart → all 18 webapps deploy without errors
  • Zero ContextDetachingSCL / getRenderParameters / LRUMap / ListOrderedMap / JsonSerializeAs errors in the logs (all the wave's regressions confirmed fixed)
  • uPortal pipeline check: login works, waffle-menu renders, notification-icon renders, Apereo footer renders, no "An error occurred loading this content" in chrome
  • Playwright suite: 99 passed / 7 failed (vs. 83/23 in pre-patch smoke test — 16 tests recovered, +70%). The 7 remaining failures cluster on the per-portlet Options dropdown UI surface area (3 favorites, 4 portlet-options); appear to be brittle-selector/web-component-timing test issues rather than release regressions, since the actual rendered chrome is healthy.

Out of scope

  • resourceServerVersion (1.0.48) intentionally stays — legacy ResourceServingWebapp overlay is needed by webapps that haven't migrated to webjar-served deps. Tracked separately.
  • esupFilemanagerPortletVersion (4.0.0) — not part of the wave; converted to web component + API in a prior cycle.
  • The remaining 7 Playwright failures are tracked outside this PR.

Test plan

  • ./gradlew portalInit succeeds end-to-end
  • Tomcat starts on :8080 with no SEVERE context startup failures
  • Login + portal chrome rendering verified manually
  • Playwright suite: 99 passed / 7 failed (regression-fix-relevant tests all pass)
  • CI green

…→ 1.5.3

Problem: uPortal 5.17.3 and resource-server 1.5.3 were both released
yesterday. uPortal-start master still pins the prior versions:
- uPortalVersion=5.17.2 (released 2026-04-02)
- resourceServerWebjarVersion=1.5.0 (released last cycle)

The 1.5.0 pin in particular has been actively bad: that release was
published without webjar JARs in WEB-INF/lib, so /resource-server/
webjars/jquery/dist/jquery.min.js etc. all 404'd in deployments,
which broke the in-browser jQuery 4 / Bootstrap 5 loading. 1.5.3 is
the fix.

Goal: pull both releases into the fleet's reference Tomcat overlay
so adopters consuming uPortal-start get the just-released versions
and the webjar serving regression is resolved on master.

Changes:
- gradle.properties: uPortalVersion 5.17.2 → 5.17.3
- gradle.properties: resourceServerWebjarVersion 1.5.0 → 1.5.3
- resourceServerVersion (the legacy 1.0.48 ResourceServingWebapp pin)
  intentionally stays as-is — per docs/developer/other/RELEASE.md in
  the uPortal repo, that one "should generally stay at 1.0.48 in
  uPortal-start" because the older overlay carries some front-end
  dependencies the rest of the deploy still needs.

Notes: this also unblocks PR uPortal-Project#691 (Playwright BS5 dropdown fixes) —
those tests target post-#2915 frontend behavior that only exists in
5.17.3+ and depend on the webjars actually serving (1.5.2+). With
this PR landed, uPortal-Project#691 should rebase to a green CI matrix.
@bjagg bjagg changed the title chore: bump uPortal 5.17.3 + resource-server 1.5.3 chore: bump uPortal 5.17.3 + resource-server 1.5.3 + Tier 2 portlets to fleet release wave May 4, 2026
…e patches

Pull in the 10 Tier 2 portlet wave releases from 2026-05-02/03 plus
the 7 post-wave patch releases from 2026-05-04 that fixed regressions
caught during uPortal-start smoke testing of the wave.

Changes (gradle.properties):
- announcementsPortletVersion 2.5.0 -> 2.5.3
  (2.5.2 wave + 2.5.3 patch — drop dead Logback ContextDetachingSCL listener)
- basicltiPortletVersion 1.5.0 -> 1.5.1
- bookmarksPortletVersion 1.3.0 -> 1.3.1
- calendarPortletVersion 2.7.0 -> 2.7.2
  (2.7.1 wave + 2.7.2 patch — fix jackson skew + JSP <% in regex + listener)
- feedbackPortletVersion 1.3.0 -> 1.3.1
- jasigWidgetPortletVersion 2.4.0 -> 2.4.2
  (2.4.1 wave + 2.4.2 patch — re-add commons-collections + listener)
- newsReaderPortletVersion 5.1.0 -> 5.1.4
  (5.1.2 wave + 5.1.4 patch; skipped 5.1.3 which had no functional changes)
- simpleContentPortletVersion 3.4.0 -> 3.4.2
  (3.4.1 wave + 3.4.2 patch — roll portletmvc4spring 5.3.4 -> 5.2.0)
- webProxyPortletVersion 2.4.0 -> 2.4.1

Tier 1:
- notificationPortletVersion 4.8.0 -> 4.8.3
  (4.8.2 wave + 4.8.3 patch — listener)

uPortal core:
- uPortalVersion 5.17.2 -> 5.17.4
  (5.17.3 wave + 5.17.4 patch — listener + Renovate dep bumps)

Resource server:
- resourceServerWebjarVersion 1.5.0 -> 1.5.3
  (legacy ResourceServingWebapp at 1.0.48 stays — separate consolidation track)
@bjagg bjagg force-pushed the chore/bump-uportal-5.17.3-resource-server-1.5.3 branch from ad942fb to 20582b2 Compare May 4, 2026 23:19
@bjagg bjagg changed the title chore: bump uPortal 5.17.3 + resource-server 1.5.3 + Tier 2 portlets to fleet release wave chore: bump uPortal 5.17.4 + resource-server 1.5.3 + 2026-05 fleet release wave + post-wave patches May 4, 2026
bjagg added 9 commits May 4, 2026 17:01
Problem: three favorites tests in tests/ux/smoke/favorites.spec.ts
asserted on the #up-notification div, which is the legacy noty.js
container. uPortal's notification system was modernized: up.notify now
maps to ModernNotification.show, which appends a fresh
.modern-notification div to <body> rather than populating
#up-notification. After the click the legacy div stays empty, so the
toBeVisible / toContainText assertions time out.

Goal: assert against the element the modern notification system
actually renders into, so the favorites tests reflect current
behavior.

Changes:
- swap four #up-notification locators in favorites.spec.ts for
  .modern-notification (helper plus the three failing tests)

Notes: confirmed via a throwaway debug spec that the favorite click
populates .modern-notification with the expected text and leaves
#up-notification empty.
Problem: bookmarks coverage was render + edit-mode-entry only, so the
tests passed even when the underlying CRUD plumbing was broken. The
favorites helper that lets you add/remove a bookmark or folder was
not exercised end-to-end.

Goal: walk the full workflows a real user runs in BookmarksPortlet
edit mode, with each test self-cleaning so the suite is repeatable.

Changes:
- add helpers `emptyBookmarkForm`, `emptyFolderForm`, and entry-by-name
  locators that scope to the visible "empty" form (multiple identically-
  shaped forms coexist on the page — error vs. empty variants — so a
  global selector resolves to several inputs)
- add `add, edit, and delete a bookmark` covering: open Add Bookmark
  form, fill name/url/note, save, click per-entry edit icon, verify
  pre-population, rename, save, then click delete (auto-accepting the
  native confirm dialog) and verify removal
- add `add and delete a folder` mirroring the same pattern for folders

Notes: do not use a renamed value that contains the original as a
substring — `hasText` filter does substring match, so an
`Initial → Initial (edited)` rename silently passed every assertion.
The fix is to use fully distinct names (`Playwright Add ...` vs
`Playwright Renamed ...`), which is what landed.
Problem: SimpleContentPortlet coverage stopped at "config mode loads
CKEditor for admin" — the actual save path that every web-component-
hosting portlet definition (notification-list, customize, favorites-
carousel, waffle-menu, etc.) rides on was untested.

Goal: assert that an admin can change content via the config form,
that the change persists into view mode, and that cancel discards
unsaved changes. Keep the spec self-cleaning so repeat runs leave
the seed content intact.

Changes:
- helper `waitForCkeditor` that resolves once CKE has mounted on the
  textarea, returning the instance id
- helper `getCkeditorContent` for capturing the original text
- helper `saveContent` that destroys CKEditor first, then writes the
  new value to the underlying textarea, then submits the form (see
  Notes below for why)
- `admin can save edited content via the CKEditor Save button` —
  captures original, saves new content, asserts persistence, restores
  original
- `cancel discards unsaved changes` — stages new content, clicks
  "Return without saving", asserts the marker never persisted

Notes: `editor.setData(html, callback)` is asynchronous and the
callback can fire before CKEditor's submit-time hook has finished
syncing its internal model to the textarea. With the callback form
the form posts the *previous* content roughly half the time when the
spec is run alongside other admin tests in the suite. Tearing the
editor down first (`editor.destroy(true)` — true means update the
underlying element on teardown) and then writing the value directly
removes every CKEditor surface that could re-sync, so the form posts
exactly what we set. Filed upstream as
uPortal-Project/SimpleContentPortlet#552 (cms variant has no plain
Save button on the form, which is the deeper fragility).
Problem: CalendarPortlet's events column was rendering raw template
source ("<% if (_(days).size() === 0) { %>...<%= day.displayName %>...")
into the visible portlet body instead of interpolated event entries.
The bug is that uPortal's respondr.xsl resets the shared
`up._.templateSettings` to Mustache-style {{ }} delimiters, but
Calendar's templates use the standard <% %> sigils. With the per-call
`_.template(text, settings)` override missing, Underscore returned
the source unchanged.

The fix landed in CalendarPortlet#404. This test stops the bug from
silently coming back the next time someone touches scripts.jsp or
adds a new template.

Goal: assert that the rendered events list never contains literal
Underscore opener sigils. Cheap to keep, fails loudly if the
templateSettings override is dropped from any future template
compile site.

Changes:
- add `event list renders without leaking Underscore template
  sigils` to portlets/calendar.spec.ts. Logs in as admin, navigates
  to the maximized Calendar, asserts the .upcal-event-list does not
  contain the substring `<%` or `<%=`. The list resolves to either
  the "No events" alert or one or more day rows — either is fine,
  the assertion only cares that no template source leaks through.

Notes: this test fails against the current 2.7.2 release of
CalendarPortlet (where the bug ships) and passes against
2.7.3-SNAPSHOT after the fix is deployed. Pairs with the
uPortalVersion / portlet alignment work on this branch — once the
next CalendarPortlet release lands it will go green on CI.
…Proxy spec

Problem: NewsReader coverage was render-only against a single instance
(campus-news), which missed three things worth catching: that
multiple instances coexist without trampling each other's per-instance
namespace, that offline feeds degrade gracefully instead of dumping
exception text, and that the configured feed labels actually reach
the DOM. Separately, tests/ux/portlets/web-proxy.spec.ts was pointing
at the `snappy` fname under the assumption it was a WebProxyPortlet
instance — `snappy` is actually a SimpleContent portlet, so the spec
was passing trivially against the wrong portlet.

Goal: drive NewsReader coverage to Adequate against the structural
surface that's stable in CI (the upstream RSS feeds aren't stable).
Stop the WebProxy spec from masquerading as coverage when no
WebProxyPortlet instance ships in the quickstart at all.

Changes:
- news-reader.spec.ts: replaced the three render-shape tests with
  five targeted ones —
  * campus-news renders feed selector with the expected feed names
    (Apereo News + Unicon News) so a regression in the
    feed-label rendering path fails loudly
  * chronicle-wired renders independently as a second instance
    (scoped by hasText to avoid the hidden-but-DOM-present sibling
    that uPortal's max-mode rendering leaves behind)
  * offline feeds degrade gracefully with the "currently unavailable"
    message — and explicitly fail if Java exception text leaks
  * two instances coexist on the news-fav-collection fragment
    without colliding on namespaces
  * edit mode loads the feed-management UI (kept from before)
- web-proxy.spec.ts: deleted. The quickstart deploys the WebProxy
  WAR but seeds no portlet definition that uses it; even adding one
  would need a stable upstream target to exercise URL rewriting,
  which is infrastructure we don't have. Coverage doc tracks this
  as Out of scope.
Problem: previous web-proxy.spec.ts was already deleted because no
WebProxyPortlet instance shipped in the quickstart and the file was
silently testing the wrong portlet (`snappy`, which is SimpleContent).
Coverage was None / Out of scope as a result, even though the WAR is
deployed and ready to use.

Goal: stand up a deterministic WebProxy instance against a stable
upstream so we can actually exercise the proxy fetch + filter + render
path in CI, and lift the portlet's coverage out of "untested".

Changes:
- data/quickstart/portlet-definition/web-proxy-example.portlet-definition.xml:
  new catalog-only portlet definition `web-proxy-example`. Targets the
  IETF-reserved domain (RFC 2606) so the body text doesn't drift over
  time. Catalog-only — no fragment-layout subscribes to it, so no
  default tab is changed and per-role smoke specs are unaffected.
  contentService=httpContentService + filters=urlRewritingFilter is
  the minimum viable wiring for the HTTP-fetch path.
- tests/ux/portlets/web-proxy.spec.ts: three tests —
  * proxies http://example.com/ and surfaces the upstream body
    ("Example Domain", plus the ICANN-maintained policy sentence)
  * upstream fetch failure surfaces a graceful error message —
    explicitly fails if Java exception text or HTTP-status
    boilerplate leaks
  * no critical JavaScript errors on load

Notes: plain HTTP is intentional. https://example.com/ fails handshake
against Tomcat's bundled JDK trust store (older trust anchor set vs.
modern serving cipher), and example.com responds 200 OK over HTTP
without redirect, so plain HTTP keeps the test path working without
doctoring the JDK trust store. Captured the reasoning in an XML
comment on the location preference.

The portlet definition is imported into the running portal via
`./gradlew dataImport -Ddir=<dir-containing-this-file>` (dataImport
expects a directory, not a single file).
Problem: NotificationPortlet coverage was just two structural smoke
tests in web-components.spec.ts (icon attached, dropdown attached).
Neither asserted that the API actually returned data or that the
portlet rendered any specific entries — so the live JJWT
runtime-classpath bug, where /api/v2 was returning 403 and the
components rendered empty, slipped through coverage entirely.

Goal: lock in the real end-to-end happy path (demo notifications
flow API → web component → DOM) plus the negative auth path, so the
JJWT regression cannot recur silently.

Changes:
- new tests/ux/portlets/notifications.spec.ts with three tests —
  * notification-list portlet renders demo entries: asserts the
    expected demo titles (Room Available, Bike License Ticket Was
    Issued, Past Due Book, Math 101) reach the rendered DOM. If
    /api/v2 starts 403'ing again, none of these will appear and
    this test fails immediately.
  * notification-icon web component shows a badge with a count:
    confirms the icon mounts as a custom element and renders a
    numeric unread badge for an authenticated student.
  * API rejects missing or malformed bearer tokens: negative
    guard. With no Authorization header → 403; with a junk JWT →
    403. This is the security half of the regression guard — the
    happy path landing on 200 only matters if the security filter
    is still enforcing.

Notes: the JJWT runtime-classpath bug that broke the API is fixed
in uPortal-Project/NotificationPortlet#695. The above tests
verified the fix end-to-end. The expected-titles list is the
intersection of the two demo response files configured via
DemoNotificationService.locations in etc/portal/notification.properties.

Removed an earlier draft `API returns demo notifications` test
that listened on page.on("response") — it was order-dependent in
the full suite (the listener attached after login may or may not
catch the first /api/v2 fetch), and the DOM-level
"renders demo entries" test already covers the same regression
without that race.
…flakes

Problem: three families of flake were intermittently failing the
chrome-level smoke specs. None were portlet-behavior bugs, but each
made the suite unreliable in full-suite context.

  1. Synthetic clicks on Bootstrap 5 dropdown-toggles do not fire the
     auto-init delegate. Bootstrap.Dropdown.getInstance(toggle) shows
     the instance is wired, but Playwright's .click(), HTMLElement
     .click(), and dispatched MouseEvents all leave aria-expanded at
     "false". A listener somewhere in uPortal's chrome appears to be
     swallowing the click before Bootstrap's document-level delegate
     can match it (root cause still unidentified — tracked separately).
  2. The favorites add/remove API returns 200 (and fires the
     "modern-notification" success toast) before uPortal has
     invalidated the server-side layout cache. An immediate
     page.reload() can render the *previous* favorite state, so
     assertions like "after-remove the link should say Add to my
     Favorites" fail in suite context where preceding tests added
     contention.
  3. The Welcome-tab favorites count test hard-coded toHaveCount(3)
     against the seeded layout's three portlets, but the student
     user's persisted layout accumulates favorites (E! Online,
     NY Times, Campus News) across sessions and the count drifts
     to 6.

Goal: get the smoke specs to a stable green without changing what
they actually verify.

Changes:
- tests/ux/utils/ux-general-utils.ts: add a shared `openDropdown`
  helper that opens a Bootstrap dropdown via
  `Bootstrap.Dropdown.getOrCreateInstance(el).show()` — the public
  Bootstrap API, which works — and asserts aria-expanded after.
  Comment captures why we bypass the click pathway.
- tests/ux/smoke/favorites.spec.ts:
  * route every dropdown-open through the shared helper
  * add `expectFavoriteLinkText` that wraps reload + reopen + assert
    in `expect(...).toPass({ timeout: 10000 })` so the second pass
    picks up the layout cache invalidation if the first one fired
    too early
  * replace the hard-coded `toHaveCount(3)` with a dynamic count of
    `.up-portlet-wrapper:has(.portlet-options-menu .dropdown-toggle)`
    — the semantic invariant is "every portlet with an Options menu
    has a favorite item", not "the seed had three of them"
- tests/ux/smoke/portlet-options.spec.ts: route every
  dropdown-toggle.click() through the shared helper.

Notes: verified 3/3 full-suite runs pass at 117/117 after the
changes. The tests no longer exercise the dropdown-click pathway
explicitly — they go through Bootstrap's API. That's a tradeoff:
we gained suite stability and lost click-pathway coverage. The
right place to recover the click-pathway test is at the chrome
level once we know what listener is intercepting clicks.
Problem: this branch has been carrying tests and overlay seeds that
depend on three companion fixes published over the last few days
(uPortal Bootstrap dedupe, CalendarPortlet template-sigil leak,
NotificationPortlet jjwt runtime classpath). Until those landed on
Maven Central, CI couldn't go green here even though every spec
passed locally against the locally-built SNAPSHOTs.

Goal: consume the published GA versions so PR uPortal-Project#692 lights up green
and is ready to merge.

Changes:
- uPortalVersion 5.17.4 → 5.17.5 (picks up
  uPortal-Project/uPortal#2980 — Bootstrap 5 include de-dup in the
  respondr skin, fixes silently-broken Options menus / favorites /
  rate-portlet across the dashboard)
- calendarPortletVersion 2.7.2 → 2.7.3 (picks up
  uPortal-Project/CalendarPortlet#404 — Underscore template
  settings override per call, fixes the events column rendering raw
  template source instead of event entries)
- notificationPortletVersion 4.8.3 → 4.8.4 (picks up
  uPortal-Project/NotificationPortlet#695 — pin jjwt-impl +
  jjwt-jackson at runtime, fixes /api/v2 silently 403'ing and the
  notification web components rendering empty)

Notes: locally redeployed the three webapps from Maven Central
artifacts and ran the Playwright suite three times in a row —
117/117 each pass — to confirm the published versions match what we
were testing against the SNAPSHOTs.
@bjagg bjagg merged commit dfcd2eb into uPortal-Project:master May 5, 2026
6 checks passed
bjagg added a commit to bjagg/uPortal-start that referenced this pull request May 5, 2026
Problem: the lint pipeline (`./gradlew playwrightLint`) was not enforcing
much. tsconfig was missing `DOM.Iterable`, ESLint had no globals
declared for the browser context inside `page.evaluate()` callbacks, and
type-coverage was permissive enough that several `(window as any)` casts
were riding along without complaint. The deferred chrome flake fix
that landed in master via uPortal-Project#692 did not address these gates because the
previous PR predated them. Bringing the suite under a strict gate
surfaces real type/style issues that we want to catch in CI.

Goal: enable the strict lint+type-coverage gate, fix the issues it
surfaces, and add the only tests/code change that wasn't already in
master from the original uPortal-Project#691 — the `unhidePortletOptionsMenus`
helper.

Changes — config:
- tsconfig.json: add `DOM.Iterable` to `lib` so spread/iteration over
  NodeList works without casts
- eslint.config.js: declare browser globals (window, document,
  navigator, URL, Element, HTMLElement, NodeListOf, etc.) so
  no-undef stops flagging legitimate uses inside page.evaluate
  callbacks; the file is otherwise unchanged
- tests/uportal-pw.config.ts: add `trace: "retain-on-failure"` and
  `screenshot: "only-on-failure"` for CI debug

Changes — `unhidePortletOptionsMenus` helper added in `ux-general-utils.ts`:
- The XSL renders `.portlet-options-menu` with `class="hidden"` and a
  jQuery document.ready handler removes the class on portlets that
  have menu items. In headless Playwright runs the timing is variable
  enough that the click target may still be hidden when a test
  interacts with it. The helper force-removes the class as a
  deterministic prep step. Pairs with the existing `openDropdown`
  helper — that one drives the Bootstrap-side, this one the
  upstream jQuery-side that gates whether the toggle is rendered.

Changes — under-the-hood typing improvements driven by the new gate:
- ux-general-utils.ts: declare `window.bootstrap` global so
  `openDropdown` doesn't need a triple-cast inside `evaluate`
- announcements.spec.ts: declare `window.tinymce` / `window.tinyMCE`
  globals; use `window.tinymce?.activeEditor?.setContent(...)`
  instead of `(window as any).tinymce.activeEditor.setContent(...)`;
  drop one `waitForLoadState("networkidle")` flagged by the
  playwright plugin; numeric separators on the 10_000 timeouts
- simple-content.spec.ts: declare `window.CKEDITOR` global with
  proper interfaces; use `window.CKEDITOR` directly in evaluate
  callbacks; replace one `waitForLoadState("networkidle")` with an
  explicit `waitForURL(/render\.uP/)`; replace nested
  setData-as-Promise with a fire-and-forget setData (the cancel
  test only needs *something* staged)
- calendar.spec.ts, jasig-widgets.spec.ts, web-proxy.spec.ts:
  replace `waitForLoadState("networkidle")` with concrete
  visibility / text assertions on each portlet's distinguishing
  element; rename `e` → `error` in pageerror handlers
- bookmarks.spec.ts: regex case normalization (the `i` flag handles
  case so the literal can be lowercase)
- feedback.spec.ts: `submitBtn` → `submitButton`
- favorites.spec.ts: numeric separator on `10_000`
- api/utils/api-portlet-list-utils.ts: `.length).not.toEqual(0)`
  → `.toHaveLength(0)` per the playwright/prefer-to-have-length
  rule

Notes: ran `./gradlew playwrightLint` clean (0 errors, 19 warnings on
pre-existing pattern usages that the rules flag as suggestions). Ran
the full Playwright suite three times in a row at 117/117 against the
released versions to confirm none of the lint-driven refactors changed
test behavior. The unhide helper is exported but not yet called from
any spec — that's intentional, this PR adds the tool; tests that need
it can pull it in as their `openDropdown` calls reveal a hidden-class
race in CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants