"local-rules/convert-class-to-function-component": "warn",
"local-rules/no-conditional-rendering-of-deferredspinner": "warn",
"local-rules/use-jest-mocked": "warn",
- "local-rules/use-await-expect-tohaveatooltipwithcontent": "warn"
+ "local-rules/use-await-expect-async-matcher": "warn"
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import '@testing-library/jest-dom/extend-expect';
+import { axe, toHaveNoViolations } from 'jest-axe';
+
+expect.extend({
+ async toHaveNoA11yViolations(received: HTMLElement) {
+ const result = await axe(received);
+ return toHaveNoViolations.toHaveNoViolations(result);
+ },
+});
asyncUtilTimeout: 3000,
});
-// Don't forget to update src/main/js/types/jest.d.ts when registering custom matchers.
expect.extend({
async toHaveATooltipWithContent(received: any, content: string) {
if (!(received instanceof Element)) {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+const { RuleTester } = require('eslint');
+const useJestMocked = require('../use-await-expect-async-matcher');
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('@typescript-eslint/parser'),
+});
+
+ruleTester.run('use-await-expect-tohaveatooltipwithcontent', useJestMocked, {
+ valid: [
+ {
+ code: `await expect(node).toHaveATooltipWithContent("Help text");`,
+ },
+ ],
+ invalid: [
+ {
+ code: `expect(node).toHaveATooltipWithContent("Help text");`,
+ errors: [
+ {
+ message:
+ 'expect.toHaveATooltipWithContent() is asynchronous; you must prefix expect() with await',
+ },
+ ],
+ output: `await expect(node).toHaveATooltipWithContent("Help text");`,
+ },
+ ],
+});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-const { RuleTester } = require('eslint');
-const useJestMocked = require('../use-await-expect-tohaveatooltipwithcontent');
-
-const ruleTester = new RuleTester({
- parser: require.resolve('@typescript-eslint/parser'),
-});
-
-ruleTester.run('use-await-expect-tohaveatooltipwithcontent', useJestMocked, {
- valid: [
- {
- code: `await expect(node).toHaveATooltipWithContent("Help text");`,
- },
- ],
- invalid: [
- {
- code: `expect(node).toHaveATooltipWithContent("Help text");`,
- errors: [{ messageId: 'useAwaitExpectToHaveATooltipWithContent' }],
- },
- ],
-});
'use-componentqualifier-enum': require('./use-componentqualifier-enum'),
'use-metrickey-enum': require('./use-metrickey-enum'),
'use-metrictype-enum': require('./use-metrictype-enum'),
- 'use-await-expect-tohaveatooltipwithcontent': require('./use-await-expect-tohaveatooltipwithcontent'),
+ 'use-await-expect-async-matcher': require('./use-await-expect-async-matcher'),
};
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+module.exports = {
+ meta: {
+ fixable: 'code',
+ },
+ create(context) {
+ return {
+ Identifier(node) {
+ if (
+ ['toHaveATooltipWithContent', 'toHaveNoA11yViolations'].includes(node.name) &&
+ node.parent?.parent?.parent?.type !== 'AwaitExpression'
+ ) {
+ context.report({
+ node: node.parent?.parent?.parent,
+ message: `expect.${node.name}() is asynchronous; you must prefix expect() with await`,
+ fix(fixer) {
+ return fixer.insertTextBefore(node.parent?.parent?.parent, 'await ');
+ },
+ });
+ }
+ },
+ };
+ },
+};
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-module.exports = {
- meta: {
- messages: {
- useAwaitExpectToHaveATooltipWithContent:
- 'expect.toHaveATooltipWithContent() is asynchronous; you must prefix expect() with await',
- },
- },
- create(context) {
- return {
- Identifier(node) {
- if (
- node.name === 'toHaveATooltipWithContent' &&
- node.parent?.parent?.parent?.type !== 'AwaitExpression'
- ) {
- context.report({ node, messageId: 'useAwaitExpectToHaveATooltipWithContent' });
- }
- },
- };
- },
-};
'<rootDir>/config/jest/SetupTestEnvironment.ts',
'<rootDir>/config/jest/SetupTheme.js',
],
- setupFilesAfterEnv: ['<rootDir>/config/jest/SetupReactTestingLibrary.ts'],
+ setupFilesAfterEnv: [
+ '<rootDir>/config/jest/SetupReactTestingLibrary.ts',
+ '<rootDir>/config/jest/SetupJestAxe.ts',
+ ],
snapshotSerializers: ['enzyme-to-json/serializer', '@emotion/jest/serializer'],
testEnvironment: 'jsdom',
testPathIgnorePatterns: [
"@types/dompurify": "3.0.2",
"@types/enzyme": "3.10.13",
"@types/jest": "29.5.2",
+ "@types/jest-axe": "3.5.5",
"@types/lodash": "4.14.195",
"@types/node": "18.16.18",
"@types/react": "17.0.39",
"fs-extra": "11.1.1",
"http-proxy": "1.18.1",
"jest": "29.5.0",
+ "jest-axe": "7.0.1",
"jest-environment-jsdom": "29.5.0",
"jest-junit": "16.0.0",
"jsdom": "21.1.1",
qualifierFilter: byRole('combobox', { name: 'projects_management.filter_by_component' }),
analysisDateFilter: byPlaceholderText('last_analysis_before'),
provisionedFilter: byRole('checkbox', {
- name: 'provisioning.only_provisioned provisioning.only_provisioned.tooltip',
+ name: 'provisioning.only_provisioned help',
}),
searchFilter: byRole('searchbox', { name: 'search.search_by_name_or_key' }),
bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }),
scmAddButton: byRole('button', { name: 'add_verb' }),
createUserDialogButton: byRole('button', { name: 'create' }),
+ cancelButton: byRole('button', { name: 'cancel' }),
reloadButton: byRole('button', { name: 'reload' }),
doneButton: byRole('button', { name: 'done' }),
changeButton: byRole('button', { name: 'change_verb' }),
const user = userEvent.setup();
renderUsersApp();
- await act(async () =>
- user.click(
- await within(await ui.aliceRow.find()).findByRole('button', {
- name: 'users.update_users_groups.alice.merveille',
- })
- )
- );
+ await act(async () => user.click(await ui.aliceUpdateGroupButton.find()));
expect(await ui.dialogGroups.find()).toBeInTheDocument();
expect(ui.getGroups()).toHaveLength(2);
const user = userEvent.setup();
renderUsersApp();
- await act(async () =>
- user.click(
- await within(await ui.aliceRow.find()).findByRole('button', {
- name: 'users.manage_user.alice.merveille',
- })
- )
- );
+ await act(async () => user.click(await ui.aliceUpdateButton.find()));
await user.click(
await within(ui.aliceRow.get()).findByRole('button', { name: 'update_details' })
);
const user = userEvent.setup();
renderUsersApp();
- await act(async () =>
- user.click(
- await within(await ui.aliceRow.find()).findByRole('button', {
- name: 'users.manage_user.alice.merveille',
- })
- )
- );
+ await act(async () => user.click(await ui.aliceUpdateButton.find()));
await user.click(
await within(ui.aliceRow.get()).findByRole('button', { name: 'users.deactivate' })
);
const currentUser = mockLoggedInUser({ login: 'alice.merveille' });
renderUsersApp([], currentUser);
- await act(async () =>
- user.click(
- await within(await ui.aliceRow.find()).findByRole('button', {
- name: 'users.manage_user.alice.merveille',
- })
- )
- );
+ await act(async () => user.click(await ui.aliceUpdateButton.find()));
await user.click(
await within(ui.aliceRow.get()).findByRole('button', { name: 'my_profile.password.title' })
);
await user.click(await ui.localFilter.find());
});
- expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
expect(ui.bobRow.query()).not.toBeInTheDocument();
+ expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
});
it('should be able to change tokens of a user', async () => {
authenticationHandler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
- errorMessage: "T'es mauvais Jacques",
+ errorMessage: 'Error Message',
});
renderUsersApp([Feature.GithubProvisioning]);
await act(async () => expect(await ui.githubProvisioningAlert.find()).toBeInTheDocument());
- expect(screen.queryByText("T'es mauvais Jacques")).not.toBeInTheDocument();
-
+ expect(screen.queryByText('Error Message')).not.toBeInTheDocument();
expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
});
authenticationHandler.addProvisioningTask({
status: TaskStatuses.Failed,
executedAt: '2022-02-03T11:45:35+0200',
- errorMessage: "T'es mauvais Jacques",
+ errorMessage: 'Error Message',
});
renderUsersApp([Feature.GithubProvisioning]);
await act(async () => expect(await ui.githubProvisioningAlert.find()).toBeInTheDocument());
- expect(screen.queryByText("T'es mauvais Jacques")).not.toBeInTheDocument();
+ expect(screen.queryByText('Error Message')).not.toBeInTheDocument();
expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
expect(ui.githubProvisioningInProgress.query()).not.toBeInTheDocument();
});
expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/);
});
+it('accessibility', async () => {
+ userHandler.setIsManaged(false);
+ const user = userEvent.setup();
+ renderUsersApp();
+
+ // user list page should be accessible
+ expect(await ui.aliceRow.find()).toBeInTheDocument();
+ await expect(document.body).toHaveNoA11yViolations();
+
+ // user creation dialog should be accessible
+ await user.click(await ui.createUserButton.find());
+ expect(await ui.dialogCreateUser.find()).toBeInTheDocument();
+ await expect(ui.dialogCreateUser.get()).toHaveNoA11yViolations();
+ await user.click(ui.cancelButton.get());
+
+ // users group membership dialog should be accessible
+ user.click(await ui.aliceUpdateGroupButton.find());
+ expect(await ui.dialogGroups.find()).toBeInTheDocument();
+ await expect(await ui.dialogGroups.find()).toHaveNoA11yViolations();
+ await act(async () => {
+ await user.click(ui.doneButton.get());
+ });
+
+ // user update dialog should be accessible
+ await user.click(await ui.aliceUpdateButton.find());
+ await user.click(await ui.aliceRow.byRole('button', { name: 'update_details' }).find());
+ expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
+ await expect(await ui.dialogUpdateUser.find()).toHaveNoA11yViolations();
+ await user.click(ui.cancelButton.get());
+
+ // user tokens dialog should be accessible
+ user.click(
+ await ui.aliceRow
+ .byRole('button', {
+ name: 'users.update_tokens_for_x.Alice Merveille',
+ })
+ .find()
+ );
+ expect(await ui.dialogTokens.find()).toBeInTheDocument();
+ await expect(await ui.dialogTokens.find()).toHaveNoA11yViolations();
+ await user.click(ui.doneButton.get());
+
+ // user password dialog should be accessible
+ await user.click(await ui.aliceUpdateButton.find());
+ await user.click(
+ await ui.aliceRow.byRole('button', { name: 'my_profile.password.title' }).find()
+ );
+ expect(await ui.dialogPasswords.find()).toBeInTheDocument();
+ await expect(await ui.dialogPasswords.find()).toHaveNoA11yViolations();
+});
+
function renderUsersApp(featureList: Feature[] = [], currentUser?: CurrentUser) {
// eslint-disable-next-line testing-library/no-unnecessary-act
renderApp('admin/users', <UsersApp />, {
*/
import * as React from 'react';
import { changePassword } from '../../../api/users';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { Alert } from '../../../components/ui/Alert';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
<MandatoryFieldMarker />
</label>
{/* keep this fake field to hack browser autofill */}
- <input className="hidden" name="old-password-fake" type="password" />
+ <input className="hidden" aria-hidden name="old-password-fake" type="password" />
<input
id="old-user-password"
name="old-password"
<MandatoryFieldMarker />
</label>
{/* keep this fake field to hack browser autofill */}
- <input className="hidden" name="password-fake" type="password" />
+ <input className="hidden" aria-hidden name="password-fake" type="password" />
<input
id="user-password"
name="password"
<MandatoryFieldMarker />
</label>
{/* keep this fake field to hack browser autofill */}
- <input className="hidden" name="confirm-password-fake" type="password" />
+ <input className="hidden" aria-hidden name="confirm-password-fake" type="password" />
<input
id="confirm-user-password"
name="confirm-password"
<HelpIcon
fill={colors.gray60}
size={size}
+ role="img"
+ aria-label={isInteractive ? translate('tooltip_is_interactive') : translate('help')}
description={
isInteractive ? (
<>
data-testid="help-tooltip-activator"
>
<HelpIcon
+ aria-label="help"
description={
<div
className="my-overlay"
/>
}
fill="#888"
+ role="img"
size={12}
/>
</span>
import { uniqueId } from 'lodash';
import * as React from 'react';
-export interface IconProps extends React.AriaAttributes {
+export interface IconProps extends React.HTMLAttributes<SVGSVGElement> {
className?: string;
fill?: string;
size?: number;
description?: React.ReactNode;
}
-interface Props extends React.AriaAttributes {
+interface Props extends React.HTMLAttributes<SVGSVGElement> {
children: React.ReactNode;
className?: string;
size?: number;
width={width}
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlSpace="preserve"
- role="img"
aria-describedby={description && !hidden ? id : undefined}
{...iconProps}
>
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `"<div><svg height="16" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 1.41421;" version="1.1" viewBox="0 0 16 16" width="16" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" role="img"><path d="test-path"></path></svg></div>"`;
+exports[`should render correctly 1`] = `"<div><svg height="16" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 1.41421;" version="1.1" viewBox="0 0 16 16" width="16" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"><path d="test-path"></path></svg></div>"`;
>
<svg
height="16"
- role="img"
space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
version="1.1"
declare namespace jest {
interface Matchers<R> {
toHaveATooltipWithContent(content: string): Promise<CustomMatcherResult>;
+ toHaveNoA11yViolations(): Promise<CustomMatcherResult>;
}
}
languageName: node
linkType: hard
+"@types/jest-axe@npm:3.5.5":
+ version: 3.5.5
+ resolution: "@types/jest-axe@npm:3.5.5"
+ dependencies:
+ "@types/jest": "*"
+ axe-core: ^3.5.5
+ checksum: 535038968034fe80fb466dcd5939ea5d9e9adb3ef00852ded3e41c62536c05137eb30bcbfd608142d2bc571d65c20b8e3563181674fb48594c2662d340bb4da5
+ languageName: node
+ linkType: hard
+
"@types/jest@npm:*":
version: 27.4.0
resolution: "@types/jest@npm:27.4.0"
"@types/dompurify": 3.0.2
"@types/enzyme": 3.10.13
"@types/jest": 29.5.2
+ "@types/jest-axe": 3.5.5
"@types/lodash": 4.14.195
"@types/node": 18.16.18
"@types/react": 17.0.39
fs-extra: 11.1.1
http-proxy: 1.18.1
jest: 29.5.0
+ jest-axe: 7.0.1
jest-environment-jsdom: 29.5.0
jest-junit: 16.0.0
jsdom: 21.1.1
languageName: node
linkType: hard
+"axe-core@npm:4.5.1":
+ version: 4.5.1
+ resolution: "axe-core@npm:4.5.1"
+ checksum: db90c6b41483e9c3452393933072fe0b8c2221e6d9b96ae0e6a03a4ce1e4c35bec539c92e9b6fcd63c4acf6678ad3c3ca7f5ab1d884210d157867cc54acd4f6a
+ languageName: node
+ linkType: hard
+
+"axe-core@npm:^3.5.5":
+ version: 3.5.6
+ resolution: "axe-core@npm:3.5.6"
+ checksum: 000777d2b6bf1f390beb1fb4b8714ed9127797c021c345b032db0c144e07320dbbe8cb0bcb7688b90b79cfbd3cdc1f27a4dc857804e3c61d7e0defb34deeb830
+ languageName: node
+ linkType: hard
+
"axe-core@npm:^4.6.2":
version: 4.6.3
resolution: "axe-core@npm:4.6.3"
languageName: node
linkType: hard
+"jest-axe@npm:7.0.1":
+ version: 7.0.1
+ resolution: "jest-axe@npm:7.0.1"
+ dependencies:
+ axe-core: 4.5.1
+ chalk: 4.1.2
+ jest-matcher-utils: 29.2.2
+ lodash.merge: 4.6.2
+ checksum: 3c9b0b8669f6fe5d143ee74aa0414831ea75bbe9e38551aa82352d9102e2827952a14cb3a6782fbd847a0febf6da3461424d1a2eb77256159719890f502cb01f
+ languageName: node
+ linkType: hard
+
"jest-changed-files@npm:^29.5.0":
version: 29.5.0
resolution: "jest-changed-files@npm:29.5.0"
languageName: node
linkType: hard
+"jest-diff@npm:^29.2.1, jest-diff@npm:^29.5.0":
+ version: 29.5.0
+ resolution: "jest-diff@npm:29.5.0"
+ dependencies:
+ chalk: ^4.0.0
+ diff-sequences: ^29.4.3
+ jest-get-type: ^29.4.3
+ pretty-format: ^29.5.0
+ checksum: dfd0f4a299b5d127779c76b40106c37854c89c3e0785098c717d52822d6620d227f6234c3a9291df204d619e799e3654159213bf93220f79c8e92a55475a3d39
+ languageName: node
+ linkType: hard
+
"jest-diff@npm:^29.3.1":
version: 29.3.1
resolution: "jest-diff@npm:29.3.1"
languageName: node
linkType: hard
-"jest-diff@npm:^29.5.0":
- version: 29.5.0
- resolution: "jest-diff@npm:29.5.0"
- dependencies:
- chalk: ^4.0.0
- diff-sequences: ^29.4.3
- jest-get-type: ^29.4.3
- pretty-format: ^29.5.0
- checksum: dfd0f4a299b5d127779c76b40106c37854c89c3e0785098c717d52822d6620d227f6234c3a9291df204d619e799e3654159213bf93220f79c8e92a55475a3d39
- languageName: node
- linkType: hard
-
"jest-docblock@npm:^29.4.3":
version: 29.4.3
resolution: "jest-docblock@npm:29.4.3"
languageName: node
linkType: hard
+"jest-matcher-utils@npm:29.2.2":
+ version: 29.2.2
+ resolution: "jest-matcher-utils@npm:29.2.2"
+ dependencies:
+ chalk: ^4.0.0
+ jest-diff: ^29.2.1
+ jest-get-type: ^29.2.0
+ pretty-format: ^29.2.1
+ checksum: 97ef2638ab826c25f84bfedea231cef091820ae0876ba316922da81145e950d2b9d2057d3645813b5ee880bb975ed4f22e228dda5d0d26a20715e575b675357d
+ languageName: node
+ linkType: hard
+
"jest-matcher-utils@npm:^29.3.1":
version: 29.3.1
resolution: "jest-matcher-utils@npm:29.3.1"
languageName: node
linkType: hard
-"lodash.merge@npm:^4.6.2":
+"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005
languageName: node
linkType: hard
-"pretty-format@npm:^29.5.0":
+"pretty-format@npm:^29.2.1, pretty-format@npm:^29.5.0":
version: 29.5.0
resolution: "pretty-format@npm:29.5.0"
dependencies: