-
Notifications
You must be signed in to change notification settings - Fork 245
chore(components): upgrade LeafyGreen modal components COMPASS-9642 #7594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
7b62353 to
694ac4e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR upgrades LeafyGreen modal-related packages to their latest versions. The upgrade introduces breaking changes in the LeafyGreen Modal API, requiring updates to modal usage patterns throughout the codebase. Key changes include updating modal prop names, refactoring modal state detection in tests, and adjusting focus management behavior.
- Updated LeafyGreen modal packages and related dependencies to latest versions
- Refactored modal prop usage from deprecated patterns (e.g.,
contentClassName→className,onButtonClick→buttonProps) - Implemented new modal state detection utilities for E2E tests using dialog element attributes
- Updated autofocus handling to use explicit
autoFocusprops instead ofinitialFocus
Reviewed changes
Copilot reviewed 102 out of 103 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/compass-components/package.json | Updated LeafyGreen dependencies to latest versions |
| packages/compass-components/src/components/modals/* | Refactored modal components to use new API patterns |
| packages/compass-e2e-tests/helpers/commands/* | Added new modal state detection utilities and updated test helpers |
| packages/compass-e2e-tests/tests/* | Updated E2E tests to use new modal detection methods |
| configs/testing-library-compass/src/assertions.ts | Added custom chai assertions for dialog open/closed states |
| configs/mocha-config-compass/register/jsdom-extra-mocks-register.js | Added HTMLDialogElement polyfill for jsdom |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| userEvent.click(startButton); | ||
| await waitFor(() => { | ||
| expect(() => screen.getByTestId('welcome-modal')).to.throw(); | ||
| expect(screen.getByTestId('welcome-modal')).be.closed; |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing 'to' in the chai assertion chain. Should be expect(screen.getByTestId('welcome-modal')).to.be.closed;
| userEvent.click(closeButton); | ||
| await waitFor(() => { | ||
| expect(() => screen.getByTestId('welcome-modal')).to.throw(); | ||
| expect(screen.getByTestId('welcome-modal')).be.closed; |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing 'to' in the chai assertion chain. Should be expect(screen.getByTestId('welcome-modal')).to.be.closed;
| userEvent.click(settingsLink); | ||
| await waitFor(() => { | ||
| expect(() => screen.getByTestId('welcome-modal')).to.throw(); | ||
| expect(screen.getByTestId('welcome-modal')).be.closed; |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing 'to' in the chai assertion chain. Should be expect(screen.getByTestId('welcome-modal')).to.be.closed;
bc02a6d to
4467057
Compare
bac36cb to
8c0d915
Compare
| expect(screen.getByTestId('assistant-confirm-clear-chat-modal')).to | ||
| .exist; | ||
| expect( | ||
| screen.getByTestId('assistant-confirm-clear-chat-modal').firstChild |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using .firstChild here because the modal root (dialog element) always exists, regardless of it being open or not.
| declare global { | ||
| // eslint-disable-next-line @typescript-eslint/no-namespace | ||
| export namespace Chai { | ||
| interface Assertion { | ||
| /** Asserts that a dialog is open */ | ||
| get open(): Assertion; | ||
| /** Asserts that a dialog is closed */ | ||
| get closed(): Assertion; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| util.addProperty( | ||
| Assertion.prototype, | ||
| 'open', | ||
| function (this: typeof Assertion) { | ||
| const obj = util.flag(this, 'object'); | ||
| new Assertion(obj).to.be.instanceof(HTMLDialogElement); | ||
| new Assertion(obj as HTMLDialogElement).has.property( | ||
| 'open', | ||
| true, | ||
| 'Expected dialog to be open' | ||
| ); | ||
| } | ||
| ); | ||
|
|
||
| util.addProperty( | ||
| Assertion.prototype, | ||
| 'closed', | ||
| function (this: typeof Assertion) { | ||
| const obj = util.flag(this, 'object'); | ||
| new Assertion(obj).to.be.instanceof(HTMLDialogElement); | ||
| new Assertion(obj as HTMLDialogElement).has.property( | ||
| 'open', | ||
| false, | ||
| 'Expected dialog to be closed' | ||
| ); | ||
| } | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding new assertions to determine if a dialog element is open.
I'm putting this here (instead of the @mongodb-js/mocha-config-compass package) since this package is TypeScript and the other is pure JavaScript at the moment.
|
|
||
| await waitForElementToBeRemoved(() => | ||
| screen.getByTestId('assistant-confirm-clear-chat-modal') | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI this changes the test because waitForRemoved specifically exists to check first that the element exists before being removed, just waitFor doesn't do it, but I guess it's good enough if we checked before
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check first that the element exists before being removed
I agree that does change the test - but also, that behavior seems like a race-condition to me 🤔 It depends on an implementation detail of the userEvent.click(confirmButton); firing (and re-rendering the modal), asynchronously.
This might be an inherent property of the testing library?
| export namespace Chai { | ||
| interface Assertion { | ||
| /** Asserts that a dialog is open */ | ||
| get open(): Assertion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Anemy FYI for the eslint plugin you are working on in devtools-shared, new terminator assertion
| renderModal(); | ||
| const button = screen.getByText('Confirm').closest('button'); | ||
| const { container } = renderModal(); | ||
| const button = within(container).getByText('Confirm').closest('button'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious why this change is required: testing-library supposed to clean-up after itself, so there should be no other elements on the screen with text Confirm in them and using screen over returned values from render is the recommended way to use testing-library (not a strict requirement, but still)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reverted this change 👍 It was introduced before mongodb/leafygreen-ui#3189 was merged and released.
| expect(screen.getByText('I am ready!')).to.exist; | ||
| }); | ||
|
|
||
| it('should render only the context menu when content is not ready on the first render', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did we ended up with context menu (and now modal tests) in this component?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also doesn't seem to be used anymore, so might as well just drop this whole thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the behavior of the new modal to include the dialog element, regardless of the modal being open or not.
It's also doesn't seem to be used anymore
What specifically doesn't seem to be used anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - the value of this test deteriorated beyond being useful. I'd be happy to delete the two components and their tests all together. Would that suffice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #7615
| children: confirmationProps.buttonText ?? 'Confirm', | ||
| onClick: handleConfirm, | ||
| ...confirmationProps.confirmButtonProps, | ||
| 'data-lgid': 'lg-confirmation_modal-footer-confirm_button', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgid doesn't look like something we should be touching or overriding, is there no way we can pass our own id or another data attribute to the confirm button so that we don't depend on leafygreen for this? Can we autoFocus it instead like in the other cases in this patch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reverted this line and updated the initialFocus prop below:
compass/packages/compass-components/src/hooks/use-confirmation.tsx
Lines 204 to 207 in 406b9a4
| (confirmationProps.requiredInputText | |
| ? 'auto' | |
| : // TODO: Update this once https://jira.mongodb.org/browse/LG-5735 gets resolved | |
| '[data-testid=lg-confirmation_modal-footer-cancel_button]') |
This aligns with an upcoming change in LG (https://jira.mongodb.org/browse/LG-5735)
| for (const modal of modals) { | ||
| // Ensure any modals are interactable if open | ||
| await modal.waitForClickable({ | ||
| timeout: 500, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unexpected to see defaults overriden in shared helpers, we configure them for the whole test suite via env usually because different environments might require slightly different configuration (we're also pretty generous with those usually especially for CI), so setting it to a value different from default by default doesn't seem right to me: it might be 500ms on your machine, but 5s next time windows host in GHA has issues with performance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree and updated this to pass these options instead of overriding the default:
compass/packages/compass-e2e-tests/helpers/commands/wait-for-open-modal.ts
Lines 10 to 35 in 406b9a4
| await browser.waitUntil( | |
| async () => { | |
| const modals = await browser.getOpenModals(selector); | |
| const count = await modals.length; | |
| if (reverse) { | |
| return count === 0; | |
| } else if (count === 0) { | |
| return false; | |
| } else { | |
| for (const modal of modals) { | |
| // Ensure any modals are interactable if open | |
| await modal.waitForClickable({ | |
| timeoutMsg: 'Timeout waiting for open modal to become clickable', | |
| ...options, | |
| }); | |
| } | |
| return true; | |
| } | |
| }, | |
| { | |
| timeoutMsg: `Timeout waiting for modal '${inspect(selector)}' to ${ | |
| reverse ? 'close' : 'open' | |
| }`, | |
| ...options, | |
| } | |
| ); |
| ): Promise<void> { | ||
| await browser.waitUntil( | ||
| async () => { | ||
| const modals = await browser.getOpenModals(selector); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect that if I'm calling waitForOpenModal, I'm only looking for one modals, weird to see that this code is dealing with multiple ones instead of throwing if it got more than one back. Is this some quirk of how leafygreen renders those that forces us to do this? Is it just always adding a new modal on the screen or something instead of re-rendering?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this some quirk of how leafygreen renders those that forces us to do this?
The selector is often matching a data-testid which is put on the dialog element, which is mounted into the DOM even when the modal is closed. If two components rendering a modal internally is mounted at the same time, multiple elements will match the selector. Waiting for the modal to open then becomes a matter waiting for one of those modals to open.
One alternative I considered was appending a [open] attribute to the selector. I ultimately scrapped that because it felt too brittle. For one, it would not work for selector lists (selectors separated with ,) 🤔 It would be great if WebDriver IO had a way to impose "additional constraints". This is why I'm currently filtering the modals based on their open attribute in getOpenModals:
| const open = await element.getAttribute('open'); | |
| return open === 'true'; |
| initialFocus={ | ||
| confirmationProps.initialFocus ?? | ||
| '[data-lgid=lg-confirmation_modal-footer-confirm_button]' | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes the behavior when confirmation modal is used with required input in an unexpected way and puts focus on a fake-disabled button instead of input as it happens right now. Same for showPrompt where the focus should land on the input we're showing, not on confirm button right away
3e406d7 to
081bd47
Compare
… more important with the new dialog
7f0ee01 to
3595882
Compare
…to move the code later
3595882 to
406b9a4
Compare
|
I added a more robust "locator strategy" to find open dialogs:
The benefit of this is that all the code to select and filter runs in the browser, instead of passing back and forth between the webdriver client and the browser using async operations. This makes the check much more reliable when elements are entering and leaving the document. |

Description
Stacked on #7582.
This PR upgrades the modal related LG packages to their latests version.
Checklist
Motivation and Context
Open Questions
Dependents
Types of changes