diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ad143ca36..c734a32e5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -281,3 +281,6 @@ jobs: - name: Test CC Widgets run: yarn run test:cc-widgets + + - name: Test Meetings Widget + run: yarn run test:meetings-widget diff --git a/package.json b/package.json index 78c839418..44a4242cd 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,12 @@ "scripts": { "clean": "yarn workspaces foreach --all --topological --parallel run clean && rm -rf node_modules", "clean:dist": "yarn workspaces foreach --all --topological --parallel run clean:dist", - "test:unit": "yarn run test:tooling && yarn run test:cc-widgets", + "test:unit": "yarn run test:tooling && yarn run test:cc-widgets && yarn run test:meetings-widget", "test:e2e": "yarn playwright test", "test:styles": "yarn workspaces foreach --all --exclude webex-widgets run test:styles", "test:tooling": "jest --coverage", "test:cc-widgets": "yarn workspaces foreach --all --exclude webex-widgets --exclude samples-cc-wc-app --exclude samples-cc-react-app run test:unit", + "test:meetings-widget": "yarn workspaces foreach --all --verbose --include @webex/widgets run test:unit", "build:dev": "NODE_ENV=development yarn build", "build:prod": "NODE_ENV=production yarn build:serial", "build": "NODE_OPTIONS=--max-old-space-size=4096 yarn workspaces foreach --all --parallel --topological --exclude samples-cc-react-app --exclude samples-cc-wc-app --exclude samples-meeting-app run build:src", diff --git a/packages/@webex/widgets/jest.config.js b/packages/@webex/widgets/jest.config.js new file mode 100644 index 000000000..385314bf2 --- /dev/null +++ b/packages/@webex/widgets/jest.config.js @@ -0,0 +1,14 @@ +const jestConfig = require('../../../jest.config.js'); + +jestConfig.rootDir = '../../../'; +jestConfig.testMatch = ['**/@webex/widgets/tests/**/*.test.{js,jsx}']; +jestConfig.globals = { + ...jestConfig.globals, + __appVersion__: '1.0.0-test', +}; +jestConfig.coveragePathIgnorePatterns = [ + ...(jestConfig.coveragePathIgnorePatterns || []), + 'WebexLogo\\.jsx$', +]; + +module.exports = jestConfig; diff --git a/packages/@webex/widgets/package.json b/packages/@webex/widgets/package.json index cb908d17f..3dec22ace 100644 --- a/packages/@webex/widgets/package.json +++ b/packages/@webex/widgets/package.json @@ -17,6 +17,7 @@ "release:debug": "semantic-release --debug", "release:dry-run": "semantic-release --dry-run", "start": "npm run demo:serve", + "test:unit": "jest --config jest.config.js --coverage", "test:e2e": "npm run demo:build && wdio wdio.conf.js", "test:eslint": "echo 'Broken eslint tests'", "test:eslint:broken": "eslint src/" @@ -57,6 +58,9 @@ "@momentum-ui/react": "^23.21.4", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.6.2", + "@testing-library/react": "16.0.1", "@wdio/cli": "^7.3.1", "@wdio/jasmine-framework": "^7.4.6", "@wdio/junit-reporter": "^7.4.2", diff --git a/packages/@webex/widgets/tests/WebexMeetings/WebexMeetings.test.jsx b/packages/@webex/widgets/tests/WebexMeetings/WebexMeetings.test.jsx new file mode 100644 index 000000000..a592a9949 --- /dev/null +++ b/packages/@webex/widgets/tests/WebexMeetings/WebexMeetings.test.jsx @@ -0,0 +1,582 @@ +import React, {Component} from 'react'; +import {render, fireEvent, act} from '@testing-library/react'; +import '@testing-library/jest-dom'; + +let capturedAdapterFactory; + +jest.mock('@webex/components', () => ({ + WebexMediaAccess: (props) => ( +
+ ), + WebexMeeting: (props) => ( +
+ ), + withAdapter: (WrappedComponent, factory) => { + capturedAdapterFactory = factory; + return WrappedComponent; + }, + withMeeting: (WrappedComponent) => WrappedComponent, +})); + +jest.mock('@webex/components/dist/css/webex-components.css', () => {}); + +jest.mock('webex', () => jest.fn((config) => ({__mockWebex: true, ...config}))); +jest.mock('@webex/sdk-component-adapter', () => jest.fn((webex) => ({__mockAdapter: true, webex}))); + +const Webex = require('webex'); +const WebexSDKAdapter = require('@webex/sdk-component-adapter'); + +const WebexMeetingsWidget = require('../../src/widgets/WebexMeetings/WebexMeetings').default; +const adapterFactory = capturedAdapterFactory; + +class TestErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = {hasError: false}; + } + + static getDerivedStateFromError() { + return {hasError: true}; + } + + componentDidCatch(error) { + if (this.props.onError) { + this.props.onError(error); + } + } + + render() { + if (this.state.hasError) { + return null; + } + return this.props.children; + } +} + +const baseMeeting = { + ID: 'meeting-123', + localAudio: {permission: 'GRANTED'}, + localVideo: {permission: 'GRANTED'}, +}; + +const baseProps = { + accessToken: 'test-token', + meetingDestination: 'test@webex.com', + meeting: baseMeeting, +}; + +describe('WebexMeetingsWidget', () => { + beforeEach(() => { + capturedAdapterFactory = undefined; + jest.clearAllMocks(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + describe('Rendering', () => { + it('renders wrapper div with class "webex-meetings-widget" and tabIndex 0', () => { + const {container} = render(); + + const wrapper = container.firstChild; + expect(wrapper).toHaveClass('webex-meetings-widget'); + expect(wrapper).toHaveAttribute('tabindex', '0'); + }); + + it('renders WebexMediaAccess for microphone when audioPermission is ASKING', () => { + const meeting = {...baseMeeting, localAudio: {permission: 'ASKING'}}; + const {getByTestId, queryByTestId} = render( + + ); + + expect(getByTestId('webex-media-access')).toBeInTheDocument(); + expect(getByTestId('webex-media-access')).toHaveAttribute('data-media', 'microphone'); + expect(queryByTestId('webex-meeting')).not.toBeInTheDocument(); + }); + + it('renders WebexMediaAccess for camera when videoPermission is ASKING', () => { + const meeting = {...baseMeeting, localVideo: {permission: 'ASKING'}}; + const {getByTestId, queryByTestId} = render( + + ); + + expect(getByTestId('webex-media-access')).toBeInTheDocument(); + expect(getByTestId('webex-media-access')).toHaveAttribute('data-media', 'camera'); + expect(queryByTestId('webex-meeting')).not.toBeInTheDocument(); + }); + + it('audio ASKING takes priority over video ASKING', () => { + const meeting = { + ...baseMeeting, + localAudio: {permission: 'ASKING'}, + localVideo: {permission: 'ASKING'}, + }; + const {getByTestId} = render(); + + expect(getByTestId('webex-media-access')).toHaveAttribute('data-media', 'microphone'); + }); + + it('passes correct meetingID to WebexMediaAccess (microphone case)', () => { + const meeting = {...baseMeeting, localAudio: {permission: 'ASKING'}}; + const {getByTestId} = render(); + + expect(getByTestId('webex-media-access')).toHaveAttribute('data-meeting-id', 'meeting-123'); + }); + + it('passes correct meetingID to WebexMediaAccess (camera case)', () => { + const meeting = {...baseMeeting, localVideo: {permission: 'ASKING'}}; + const {getByTestId} = render(); + + expect(getByTestId('webex-media-access')).toHaveAttribute('data-meeting-id', 'meeting-123'); + }); + + it('renders WebexMeeting when no permission is ASKING', () => { + const {getByTestId, queryByTestId} = render(); + + expect(getByTestId('webex-meeting')).toBeInTheDocument(); + expect(queryByTestId('webex-media-access')).not.toBeInTheDocument(); + }); + + it('passes correct props to WebexMeeting', () => { + const controlsFn = jest.fn(); + const props = { + ...baseProps, + meetingPasswordOrPin: 'secret123', + participantName: 'Test User', + layout: 'Focus', + controls: controlsFn, + controlsCollapseRangeStart: 1, + controlsCollapseRangeEnd: -1, + }; + const {getByTestId} = render(); + + const meetingEl = getByTestId('webex-meeting'); + expect(meetingEl).toHaveAttribute('data-meeting-id', 'meeting-123'); + expect(meetingEl).toHaveAttribute('data-password', 'secret123'); + expect(meetingEl).toHaveAttribute('data-participant', 'Test User'); + expect(meetingEl).toHaveAttribute('data-layout', 'Focus'); + expect(meetingEl).toHaveAttribute('data-collapse-start', '1'); + expect(meetingEl).toHaveAttribute('data-collapse-end', '-1'); + }); + + it('applies custom className to wrapper', () => { + const {container} = render(); + + expect(container.firstChild).toHaveClass('webex-meetings-widget'); + expect(container.firstChild).toHaveClass('my-custom'); + }); + + it('applies custom style to wrapper', () => { + const customStyle = {backgroundColor: 'red', width: '500px'}; + const {container} = render(); + + expect(container.firstChild).toHaveStyle({backgroundColor: 'red', width: '500px'}); + }); + }); + + describe('Default Props', () => { + it('layout defaults to Grid', () => { + const {getByTestId} = render(); + + expect(getByTestId('webex-meeting')).toHaveAttribute('data-layout', 'Grid'); + }); + + it('className defaults to empty string', () => { + const {container} = render(); + + expect(container.firstChild.className).toBe('webex-meetings-widget '); + }); + + it('meetingPasswordOrPin defaults to empty string', () => { + const {getByTestId} = render(); + + expect(getByTestId('webex-meeting')).toHaveAttribute('data-password', ''); + }); + + it('participantName defaults to empty string', () => { + const {getByTestId} = render(); + + expect(getByTestId('webex-meeting')).toHaveAttribute('data-participant', ''); + }); + }); + + describe('Error Handling', () => { + it('should render null when the widget throws due to invalid meeting prop', () => { + const onError = jest.fn(); + const {container} = render( + + + + ); + + expect(container.firstChild).toBeNull(); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('Accessibility - Focus Management', () => { + it('on widget focus, sets tabIndex on media containers', () => { + const {container} = render(); + const wrapper = container.firstChild; + + const mediaContainer = document.createElement('div'); + mediaContainer.classList.add('wxc-interstitial-meeting__media-container'); + wrapper.querySelector('.webex-meetings-widget__content').appendChild(mediaContainer); + + act(() => { + fireEvent.focus(wrapper); + jest.advanceTimersByTime(0); + }); + + expect(mediaContainer.tabIndex).toBe(0); + }); + + it('on widget focus, falls back to focusing join button when no media containers exist', () => { + const {container} = render(); + const wrapper = container.firstChild; + + const joinButton = document.createElement('button'); + joinButton.setAttribute('aria-label', 'Join meeting'); + joinButton.focus = jest.fn(); + wrapper.appendChild(joinButton); + + act(() => { + fireEvent.focus(wrapper); + jest.advanceTimersByTime(0); + }); + + expect(joinButton.focus).toHaveBeenCalled(); + }); + + it('Tab on media container focuses join button', () => { + const {container} = render(); + const wrapper = container.firstChild; + + const mediaContainer = document.createElement('div'); + mediaContainer.classList.add('wxc-in-meeting__media-container'); + wrapper.querySelector('.webex-meetings-widget__content').appendChild(mediaContainer); + + act(() => { + fireEvent.focus(wrapper); + jest.advanceTimersByTime(0); + }); + + const joinButton = document.createElement('button'); + joinButton.setAttribute('aria-label', 'Join meeting'); + joinButton.focus = jest.fn(); + wrapper.appendChild(joinButton); + + const originalActiveElement = Object.getOwnPropertyDescriptor(document, 'activeElement'); + Object.defineProperty(document, 'activeElement', { + value: mediaContainer, + writable: true, + configurable: true, + }); + + try { + const tabEvent = new KeyboardEvent('keydown', { + key: 'Tab', + code: 'Tab', + bubbles: true, + cancelable: true, + }); + Object.defineProperty(tabEvent, 'currentTarget', {value: mediaContainer}); + mediaContainer.dispatchEvent(tabEvent); + + expect(joinButton.focus).toHaveBeenCalled(); + } finally { + if (originalActiveElement) { + Object.defineProperty(document, 'activeElement', originalActiveElement); + } else { + delete document.activeElement; + } + } + }); + + it('Shift+Tab on media container focuses widget container', () => { + const {container} = render(); + const wrapper = container.firstChild; + + const mediaContainer = document.createElement('div'); + mediaContainer.classList.add('wxc-interstitial-meeting__media-container'); + wrapper.querySelector('.webex-meetings-widget__content').appendChild(mediaContainer); + + act(() => { + fireEvent.focus(wrapper); + jest.advanceTimersByTime(0); + }); + + wrapper.focus = jest.fn(); + + const originalActiveElement = Object.getOwnPropertyDescriptor(document, 'activeElement'); + Object.defineProperty(document, 'activeElement', { + value: mediaContainer, + writable: true, + configurable: true, + }); + + try { + const shiftTabEvent = new KeyboardEvent('keydown', { + key: 'Tab', + code: 'Tab', + shiftKey: true, + bubbles: true, + cancelable: true, + }); + Object.defineProperty(shiftTabEvent, 'currentTarget', {value: mediaContainer}); + mediaContainer.dispatchEvent(shiftTabEvent); + + expect(wrapper.focus).toHaveBeenCalled(); + } finally { + if (originalActiveElement) { + Object.defineProperty(document, 'activeElement', originalActiveElement); + } else { + delete document.activeElement; + } + } + }); + + it('content div focus polls for inner meeting media container and focuses it', () => { + const {container} = render(); + const wrapper = container.firstChild; + const contentDiv = wrapper.querySelector('.webex-meetings-widget__content'); + + expect(contentDiv).toBeTruthy(); + + const innerMeeting = document.createElement('div'); + innerMeeting.classList.add('wxc-in-meeting__media-container'); + innerMeeting.focus = jest.fn(); + + contentDiv.dispatchEvent(new Event('focus')); + + contentDiv.appendChild(innerMeeting); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(innerMeeting.focus).toHaveBeenCalled(); + expect(innerMeeting.tabIndex).toBe(0); + }); + + it('content div focus attaches one-time Tab handler to move focus to first interactive element', () => { + const {container} = render(); + const wrapper = container.firstChild; + const contentDiv = wrapper.querySelector('.webex-meetings-widget__content'); + + expect(contentDiv).toBeTruthy(); + + const innerMeeting = document.createElement('div'); + innerMeeting.classList.add('wxc-in-meeting__media-container'); + contentDiv.appendChild(innerMeeting); + + const interactiveBtn = document.createElement('button'); + interactiveBtn.focus = jest.fn(); + innerMeeting.appendChild(interactiveBtn); + + contentDiv.dispatchEvent(new Event('focus')); + + act(() => { + jest.advanceTimersByTime(0); + }); + + const tabEvent = new KeyboardEvent('keydown', { + key: 'Tab', + bubbles: true, + cancelable: true, + }); + innerMeeting.dispatchEvent(tabEvent); + + expect(interactiveBtn.focus).toHaveBeenCalled(); + }); + + it('arrow keys cycle through control buttons', () => { + const {container} = render(); + const wrapper = container.firstChild; + + const controlBar = document.createElement('div'); + controlBar.classList.add('wxc-meeting-control-bar__controls'); + + const btn1 = document.createElement('button'); + btn1.focus = jest.fn(); + const btn2 = document.createElement('button'); + btn2.focus = jest.fn(); + const btn3 = document.createElement('button'); + btn3.focus = jest.fn(); + + controlBar.appendChild(btn1); + controlBar.appendChild(btn2); + controlBar.appendChild(btn3); + wrapper.appendChild(controlBar); + + act(() => { + jest.advanceTimersByTime(700); + }); + + act(() => { + btn1.onkeydown({key: 'ArrowRight', preventDefault: jest.fn()}); + }); + expect(btn2.focus).toHaveBeenCalled(); + + act(() => { + btn1.onkeydown({key: 'ArrowLeft', preventDefault: jest.fn()}); + }); + expect(btn3.focus).toHaveBeenCalled(); + }); + + it('MutationObserver re-attaches listeners on DOM changes', () => { + let observerCallback; + const OriginalMutationObserver = window.MutationObserver; + + window.MutationObserver = class MockMutationObserver { + constructor(callback) { + observerCallback = callback; + } + observe() {} + disconnect() {} + }; + + try { + const {container} = render(); + const wrapper = container.firstChild; + + const controlBar = document.createElement('div'); + controlBar.classList.add('wxc-meeting-control-bar__controls'); + + const btn1 = document.createElement('button'); + controlBar.appendChild(btn1); + wrapper.appendChild(controlBar); + + act(() => { + jest.advanceTimersByTime(700); + }); + + expect(btn1.onkeydown).toBeTruthy(); + + const newBtn = document.createElement('button'); + newBtn.focus = jest.fn(); + controlBar.appendChild(newBtn); + + act(() => { + observerCallback(); + }); + + expect(newBtn.onkeydown).toBeTruthy(); + } finally { + window.MutationObserver = OriginalMutationObserver; + } + }); + }); + + describe('Cleanup', () => { + it('disconnects MutationObserver on unmount', () => { + const disconnectSpy = jest.fn(); + const OriginalMutationObserver = window.MutationObserver; + + window.MutationObserver = class MockMutationObserver { + constructor(callback) { + this.callback = callback; + } + observe() {} + disconnect() { + disconnectSpy(); + } + }; + + try { + const {unmount} = render(); + + unmount(); + + expect(disconnectSpy).toHaveBeenCalled(); + } finally { + window.MutationObserver = OriginalMutationObserver; + } + }); + }); + + describe('Adapter Factory', () => { + it('creates Webex with correct access_token', () => { + adapterFactory({accessToken: 'my-token', fedramp: false}); + + expect(Webex).toHaveBeenCalledWith( + expect.objectContaining({ + credentials: {access_token: 'my-token'}, + }) + ); + }); + + it('passes fedramp config', () => { + adapterFactory({accessToken: 'token', fedramp: true}); + + expect(Webex).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({fedramp: true}), + }) + ); + }); + + it('passes meeting experimental config', () => { + adapterFactory({accessToken: 'token', fedramp: false}); + + expect(Webex).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + meetings: { + experimental: { + enableUnifiedMeetings: true, + enableAdhocMeetings: true, + }, + }, + }), + }) + ); + }); + + it('passes appVersion from __appVersion__ global', () => { + adapterFactory({accessToken: 'token', fedramp: false}); + + expect(Webex).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({appVersion: '1.0.0-test'}), + }) + ); + }); + + it('creates WebexSDKAdapter from Webex instance', () => { + adapterFactory({accessToken: 'token', fedramp: false}); + + expect(WebexSDKAdapter).toHaveBeenCalledTimes(1); + const webexInstance = Webex.mock.results[Webex.mock.results.length - 1].value; + expect(WebexSDKAdapter).toHaveBeenCalledWith(webexInstance); + }); + + it('uses dev appName when NODE_ENV is not production', () => { + adapterFactory({accessToken: 'token', fedramp: false}); + + expect(Webex).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({appName: 'webex-widgets-meetings-dev'}), + }) + ); + }); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f6c2d52f0..cca76508c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,10 +12,10 @@ __metadata: languageName: node linkType: hard -"@aml-org/amf-antlr-parsers@npm:0.8.28": - version: 0.8.28 - resolution: "@aml-org/amf-antlr-parsers@npm:0.8.28" - checksum: 10c0/ef31cfe06b35017d7855eb3eb3d9c64853e36ea7ad0398cb0754c70c48bfa6abd70d5b7906853877e1ab479c4b01fab3804526eebdfb6adf983ceda35426b16e +"@aml-org/amf-antlr-parsers@npm:0.8.34": + version: 0.8.34 + resolution: "@aml-org/amf-antlr-parsers@npm:0.8.34" + checksum: 10c0/0a8fa2f13df8dd027364e27a258ae23fe6592ea8c55cd898424132b663d3b39ab98d1f1e44f63d808b1c5b47f1e9ec55752ac495b9a56159944c506579eef778 languageName: node linkType: hard @@ -8085,9 +8085,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.182": - version: 4.17.16 - resolution: "@types/lodash@npm:4.17.16" - checksum: 10c0/cf017901b8ab1d7aabc86d5189d9288f4f99f19a75caf020c0e2c77b8d4cead4db0d0b842d009b029339f92399f49f34377dd7c2721053388f251778b4c23534 + version: 4.17.24 + resolution: "@types/lodash@npm:4.17.24" + checksum: 10c0/b72f60d4daacdad1fa643edb3faba204c02a01eb1ac00a83ff73496a6d236fc55e459c06106e8ced42277dba932d087d8fc090f8de4ef590d3f91e6d6f7ce85a languageName: node linkType: hard @@ -9934,15 +9934,15 @@ __metadata: linkType: hard "@webex/event-dictionary-ts@npm:^1.0.1930": - version: 1.0.1947 - resolution: "@webex/event-dictionary-ts@npm:1.0.1947" + version: 1.0.2091 + resolution: "@webex/event-dictionary-ts@npm:1.0.2091" dependencies: amf-client-js: "npm:^5.2.6" json-schema-to-typescript: "npm:^12.0.0" minimist: "npm:^1.2.8" shelljs: "npm:^0.8.5" webapi-parser: "npm:^0.5.0" - checksum: 10c0/3b563f15ca895134a7ed24707daf1a937d78e237fe272f5f77e937628b6063e273eb2888d4f14d6a0d8460546d94f9da42819e74142e3d4d931ce7186361fc6c + checksum: 10c0/20d0983cebc323593d7a15c8dd26cb7c9d373b0d5e810d6dd818cbb891d42fba844c45fe859ceda810c9daf283fffe939fafcfe0b0068563d34c9731bf8e183e languageName: node linkType: hard @@ -12556,7 +12556,7 @@ __metadata: languageName: node linkType: hard -"@webex/ts-sdp@npm:1.8.2": +"@webex/ts-sdp@npm:1.8.2, @webex/ts-sdp@npm:^1.8.1": version: 1.8.2 resolution: "@webex/ts-sdp@npm:1.8.2" checksum: 10c0/d336f6d3599cbee418de6f02621028266a53c07a0a965e82eb18c9e3993ab9d29d569f5398072de43d9448972776734ca76c1ae2f257859a38793e33f8a519aa @@ -12570,13 +12570,6 @@ __metadata: languageName: node linkType: hard -"@webex/ts-sdp@npm:^1.8.1": - version: 1.8.1 - resolution: "@webex/ts-sdp@npm:1.8.1" - checksum: 10c0/9dc7c63d3274cdbf1cf42c17a2d7bc5afef640bf8200e7c812732c9a19f97d3a84df5bfecba9abc349c19c199ede22c9b7d0db32c1cf802af3d5eb56fda3fefa - languageName: node - linkType: hard - "@webex/web-capabilities@npm:^1.6.1": version: 1.6.1 resolution: "@webex/web-capabilities@npm:1.6.1" @@ -12777,6 +12770,9 @@ __metadata: "@momentum-ui/react": "npm:^23.21.4" "@semantic-release/changelog": "npm:^6.0.0" "@semantic-release/git": "npm:^10.0.0" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.6.2" + "@testing-library/react": "npm:16.0.1" "@wdio/cli": "npm:^7.3.1" "@wdio/jasmine-framework": "npm:^7.4.6" "@wdio/junit-reporter": "npm:^7.4.2" @@ -13207,15 +13203,15 @@ __metadata: linkType: hard "amf-client-js@npm:^5.2.6": - version: 5.7.0 - resolution: "amf-client-js@npm:5.7.0" + version: 5.10.0 + resolution: "amf-client-js@npm:5.10.0" dependencies: - "@aml-org/amf-antlr-parsers": "npm:0.8.28" + "@aml-org/amf-antlr-parsers": "npm:0.8.34" ajv: "npm:6.12.6" avro-js: "npm:1.11.3" bin: amf: bin/amf - checksum: 10c0/0bea2694b22de128d90696115ea2e716179841d5c076eef8e7ca4dba87f6a24090bac2cb8fb702641b3cc0ea445a000cf5f7260d7a3555ae6c6d3d2502f84909 + checksum: 10c0/50f7b9d546a719df4ddb2ae40dbf3721849c3be9a2544db3bf133c2336cb047a95057bbaf3c89df539d9c6ec0609a4f6ac934cb82e53ca404aa1407f3032a714 languageName: node linkType: hard @@ -34489,9 +34485,9 @@ __metadata: linkType: hard "underscore@npm:^1.13.2": - version: 1.13.7 - resolution: "underscore@npm:1.13.7" - checksum: 10c0/fad2b4aac48847674aaf3c30558f383399d4fdafad6dd02dd60e4e1b8103b52c5a9e5937e0cc05dacfd26d6a0132ed0410ab4258241240757e4a4424507471cd + version: 1.13.8 + resolution: "underscore@npm:1.13.8" + checksum: 10c0/6677688daeda30484823e77c0b89ce4dcf29964a77d5a06f37299c007ab4bb1c66a0ff75e0d274620b62a1fe2a6ba29879f8214533ca611d71a1ae504f2bfc9b languageName: node linkType: hard