Skip to content

Conversation

@kraenhansen
Copy link
Contributor

Description

Stacked on #7582.

This PR upgrades the modal related LG packages to their latests version.

Checklist

  • New tests and/or benchmarks are included
  • Documentation is changed or added
  • If this change updates the UI, screenshots/videos are added and a design review is requested
  • If this change could impact the load on the MongoDB cluster, please describe the expected and worst case impact
  • I have signed the MongoDB Contributor License Agreement (https://www.mongodb.com/legal/contributor-agreement)

Motivation and Context

  • Bugfix
  • New feature
  • Dependency update
  • Misc

Open Questions

Dependents

Types of changes

  • Backport Needed
  • Patch (non-breaking change which fixes an issue)
  • Minor (non-breaking change which adds functionality)
  • Major (fix or feature that would cause existing functionality to change)

@kraenhansen kraenhansen self-assigned this Nov 25, 2025
@kraenhansen kraenhansen added dependencies Pull requests that update a dependency file no release notes Fix or feature not for release notes labels Nov 25, 2025
@kraenhansen kraenhansen force-pushed the kh/leafygreen-non-modal-updates branch from 7b62353 to 694ac4e Compare November 25, 2025 14:15
Base automatically changed from kh/leafygreen-non-modal-updates to main November 25, 2025 16:12
@kraenhansen kraenhansen marked this pull request as ready for review November 25, 2025 16:44
@kraenhansen kraenhansen requested a review from a team as a code owner November 25, 2025 16:44
Copy link
Contributor

Copilot AI left a 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., contentClassNameclassName, onButtonClickbuttonProps)
  • Implemented new modal state detection utilities for E2E tests using dialog element attributes
  • Updated autofocus handling to use explicit autoFocus props instead of initialFocus

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;
Copy link

Copilot AI Nov 25, 2025

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;

Copilot uses AI. Check for mistakes.
userEvent.click(closeButton);
await waitFor(() => {
expect(() => screen.getByTestId('welcome-modal')).to.throw();
expect(screen.getByTestId('welcome-modal')).be.closed;
Copy link

Copilot AI Nov 25, 2025

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;

Copilot uses AI. Check for mistakes.
userEvent.click(settingsLink);
await waitFor(() => {
expect(() => screen.getByTestId('welcome-modal')).to.throw();
expect(screen.getByTestId('welcome-modal')).be.closed;
Copy link

Copilot AI Nov 25, 2025

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;

Copilot uses AI. Check for mistakes.
@kraenhansen kraenhansen force-pushed the kh/leafygreen-modal-updates branch 2 times, most recently from bc02a6d to 4467057 Compare November 25, 2025 17:06
@kraenhansen kraenhansen force-pushed the kh/leafygreen-modal-updates branch from bac36cb to 8c0d915 Compare November 26, 2025 08:25
expect(screen.getByTestId('assistant-confirm-clear-chat-modal')).to
.exist;
expect(
screen.getByTestId('assistant-confirm-clear-chat-modal').firstChild
Copy link
Contributor Author

@kraenhansen kraenhansen Nov 26, 2025

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.

Comment on lines 3 to 43
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'
);
}
);
Copy link
Contributor Author

@kraenhansen kraenhansen Nov 26, 2025

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')
);
Copy link
Collaborator

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

Copy link
Contributor Author

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;
Copy link
Collaborator

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');
Copy link
Collaborator

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)

Copy link
Contributor Author

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 () {
Copy link
Collaborator

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?

Copy link
Collaborator

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

Copy link
Contributor Author

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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but why this unrelated component that doesn't render either a modal or a context menu is testing that those two are rendered?

What specifically doesn't seem to be used anymore?

Any of the components in this file:

image

Copy link
Contributor Author

@kraenhansen kraenhansen Nov 26, 2025

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?

Copy link
Contributor Author

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',
Copy link
Collaborator

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?

Copy link
Contributor Author

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:

(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,
Copy link
Collaborator

@gribnoysup gribnoysup Nov 26, 2025

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

Copy link
Contributor Author

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:

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);
Copy link
Collaborator

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?

Copy link
Contributor Author

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';

Comment on lines 203 to 208
initialFocus={
confirmationProps.initialFocus ??
'[data-lgid=lg-confirmation_modal-footer-confirm_button]'
}
Copy link
Collaborator

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

@kraenhansen kraenhansen force-pushed the kh/leafygreen-modal-updates branch 3 times, most recently from 7f0ee01 to 3595882 Compare November 28, 2025 11:44
@kraenhansen
Copy link
Contributor Author

kraenhansen commented Nov 28, 2025

I added a more robust "locator strategy" to find open dialogs:

export function dialogOpenLocator(selector: string) {
// eslint-disable-next-line no-restricted-globals -- This runs in the browser
const result: HTMLDialogElement[] = [];
// eslint-disable-next-line no-restricted-globals -- This runs in the browser
const elements = document.querySelectorAll(selector);
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (
// eslint-disable-next-line no-restricted-globals -- This runs in the browser
element instanceof HTMLDialogElement &&
element.open
) {
result.push(element);
}
}
return result;
}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file no release notes Fix or feature not for release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants