diff --git a/jest.config.js b/jest.config.js index f860c9e2d6c..5384a346758 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,8 +9,6 @@ module.exports = { '/packages/*.*/.cache/*.*' ], roots: ['/packages'], - setupFiles: ['/jest.env.js'], - snapshotSerializers: ['enzyme-to-json/serializer'], transform: { '^.+\\.[jt]sx?$': 'babel-jest' }, diff --git a/jest.env.js b/jest.env.js deleted file mode 100644 index 453a1093871..00000000000 --- a/jest.env.js +++ /dev/null @@ -1,10 +0,0 @@ -import 'raf/polyfill'; -import { configure } from 'enzyme'; -import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; -import MutationObserver from 'mutation-observer'; - -configure({ adapter: new Adapter() }); - -// referenced from '@novnc/nvnc/core/util/events.js' -// The MutationObserver is available in supported browsers, this is workaround for "jest" -global.MutationObserver = MutationObserver; diff --git a/package.json b/package.json index f7b8aaf9237..a875cd1f87b 100644 --- a/package.json +++ b/package.json @@ -36,17 +36,13 @@ "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", - "@types/enzyme": "3.9.0", "@types/jest": "27.0.2", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@typescript-eslint/eslint-plugin": "^4.4.1", "@typescript-eslint/parser": "^4.4.1", - "@wojtekmaj/enzyme-adapter-react-17": "^0.3.1", "babel-jest": "^27.2.5", "concurrently": "^5.3.0", - "enzyme": "3.10.0", - "enzyme-to-json": "3.4.0", "eslint": "^7.11.0", "eslint-plugin-markdown": "^1.0.2", "eslint-plugin-prettier": "^3.1.4", diff --git a/packages/react-console/src/components/AccessConsoles/__tests__/AccessConsole.test.tsx b/packages/react-console/src/components/AccessConsoles/__tests__/AccessConsole.test.tsx deleted file mode 100644 index c722c126a90..00000000000 --- a/packages/react-console/src/components/AccessConsoles/__tests__/AccessConsole.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { mount } from 'enzyme'; - -import { AccessConsoles } from '../AccessConsoles'; -import { SerialConsole } from '../../SerialConsole'; -import { VncConsole } from '../../VncConsole'; -import { DesktopViewer } from '../../DesktopViewer'; -import { constants } from '../../common/constants'; - -const { SERIAL_CONSOLE_TYPE, VNC_CONSOLE_TYPE, LOADING } = constants; - -const MyVncConsoleTestWrapper = () =>

This can be VncConsole component or a wrapper

; - -const vnc = { - address: 'my.host.com', - port: 5902, - tlsPort: '5903' -}; - -test('AccessConsoles with SerialConsole as a single child', () => { - const view = render( - - - - ); - expect(view.container).toMatchSnapshot(); -}); - -test('AccessConsoles with VncConsole as a single child', () => { - const view = render( - - - - ); - expect(view.container).toMatchSnapshot(); -}); - -test('AccessConsoles with SerialConsole and VncConsole as children', () => { - const view = render( - - - - - ); - expect(view.container).toMatchSnapshot(); -}); - -const SerialConsoleConnected = () => ( -

- Whatever component, preferably wrapping SerialConsole with callbacks adapted to a particular backend. -

-); - -test('AccessConsoles with wrapped SerialConsole as a child', () => { - const view = render( - - - - ); - expect(view.container).toMatchSnapshot(); -}); - -test('AccessConsoles with preselected SerialConsole', () => { - const wrapper = render( - - - - ); - expect(wrapper.container).toMatchSnapshot(); -}); - -test('AccessConsoles switching SerialConsole and VncConsole', () => { - const wrapper = mount( - - - - - ); - - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('SerialConsole')).toHaveLength(0); - expect(wrapper.find('MyVncConsoleTestWrapper')).toHaveLength(0); - - const button = wrapper.find('button#pf-c-console__type-selector'); - button.simulate('click'); - expect(wrapper.find('SerialConsole')).toHaveLength(0); - - let consoleItems = wrapper.find('ul li'); - expect(consoleItems).toHaveLength(2); - consoleItems - .at(1) - .find('button') - .simulate('click'); - expect(consoleItems).toMatchSnapshot(); - expect(wrapper.find('SerialConsole')).toHaveLength(1); - expect(wrapper.find('MyVncConsoleTestWrapper')).toHaveLength(0); - - button.simulate('click'); - consoleItems = wrapper.find('ul li'); - expect(consoleItems).toHaveLength(2); - consoleItems - .at(0) - .find('button') - .simulate('click'); - expect(consoleItems).toMatchSnapshot(); - expect(wrapper.find('SerialConsole')).toHaveLength(0); - expect(wrapper.find('MyVncConsoleTestWrapper')).toHaveLength(1); -}); - -test('AccessConsoles default setting', () => { - const wrapperKeepConnection = mount( - - - - - ); - wrapperKeepConnection.find('button#pf-c-console__type-selector').simulate('click'); - wrapperKeepConnection - .find('ul li') - .first() - .find('button') - .simulate('click'); // Select SerialConsole -}); - -test('Empty AccessConsoles', () => { - const view = render(); - expect(view.container).toMatchSnapshot(); -}); - -test('AccessConsoles with DesktopViewer', () => { - const view = render( - - - - ); - expect(view.container).toMatchSnapshot(); -}); diff --git a/packages/react-console/src/components/AccessConsoles/__tests__/AccessConsoles.test.tsx b/packages/react-console/src/components/AccessConsoles/__tests__/AccessConsoles.test.tsx new file mode 100644 index 00000000000..1290ce52c55 --- /dev/null +++ b/packages/react-console/src/components/AccessConsoles/__tests__/AccessConsoles.test.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +import { AccessConsoles } from '../AccessConsoles'; +import { SerialConsole } from '../../SerialConsole'; +import { VncConsole } from '../../VncConsole'; +import { DesktopViewer } from '../../DesktopViewer'; +import { constants } from '../../common/constants'; + +const { SERIAL_CONSOLE_TYPE, LOADING } = constants; + +const MyVncConsoleTestWrapper = () =>

VNC console text

; +const SerialConsoleConnected = () =>

Serial console text

; + +const vnc = { + address: 'my.host.com', + port: 5902, + tlsPort: '5903' +}; + +describe('AccessConsoles', () => { + beforeAll(() => { + window.HTMLCanvasElement.prototype.getContext = () => ({ canvas: {} } as any); + }); + + test('with SerialConsole as a single child', () => { + const { asFragment } = render( + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with VncConsole as a single child', () => { + const { asFragment } = render( + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with SerialConsole and VncConsole as children', () => { + const { asFragment } = render( + + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with wrapped SerialConsole as a child', () => { + const { asFragment } = render( + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with preselected SerialConsole', () => { + const { asFragment } = render( + + + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + test('switching SerialConsole and VncConsole', () => { + render( + + + + + ); + + // VNC (first option) is initially selected + expect(screen.queryByText(/Loading/)).toBeNull(); + expect(screen.getByText('VNC console text')).toBeInTheDocument(); + + // Open dropdown and select "Serial console" option + userEvent.click(screen.getByRole('button', { name: 'Options menu' })); + userEvent.click(screen.getByText('Serial console', { selector: 'button' })); + + // VNC content is no longer visible, and loading contents of the Serial console are visible. + expect(screen.getByText(/Loading/)).toBeInTheDocument(); + expect(screen.queryByText('VNC console text')).toBeNull(); + }); + + test('Empty', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with DesktopViewer', () => { + const { asFragment } = render( + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/react-console/src/components/AccessConsoles/__tests__/__snapshots__/AccessConsole.test.tsx.snap b/packages/react-console/src/components/AccessConsoles/__tests__/__snapshots__/AccessConsole.test.tsx.snap deleted file mode 100644 index 72d3027ded0..00000000000 --- a/packages/react-console/src/components/AccessConsoles/__tests__/__snapshots__/AccessConsole.test.tsx.snap +++ /dev/null @@ -1,390 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AccessConsoles switching SerialConsole and VncConsole 1`] = ` - -
-
- -
-
-
-`; - -exports[`AccessConsoles switching SerialConsole and VncConsole 2`] = ` -Array [ - , - , -] -`; - -exports[`AccessConsoles switching SerialConsole and VncConsole 3`] = ` -Array [ - , - , -] -`; - -exports[`AccessConsoles with DesktopViewer 1`] = ` -
-
-
-`; - -exports[`AccessConsoles with SerialConsole and VncConsole as children 1`] = ` -
-
-
-
- -
-
-
-
-`; - -exports[`AccessConsoles with SerialConsole as a single child 1`] = ` -
-
-
-`; - -exports[`AccessConsoles with VncConsole as a single child 1`] = ` -
-
-
-`; - -exports[`AccessConsoles with preselected SerialConsole 1`] = ` -
-
-

- Whatever component, preferably wrapping - - SerialConsole - - with callbacks adapted to a particular backend. -

-
-
-`; - -exports[`AccessConsoles with wrapped SerialConsole as a child 1`] = ` -
-
-
-`; - -exports[`Empty AccessConsoles 1`] = ` -
-
-
-`; diff --git a/packages/react-console/src/components/AccessConsoles/__tests__/__snapshots__/AccessConsoles.test.tsx.snap b/packages/react-console/src/components/AccessConsoles/__tests__/__snapshots__/AccessConsoles.test.tsx.snap new file mode 100644 index 00000000000..15c7a43efe1 --- /dev/null +++ b/packages/react-console/src/components/AccessConsoles/__tests__/__snapshots__/AccessConsoles.test.tsx.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccessConsoles Empty 1`] = ` + +
+ +`; + +exports[`AccessConsoles with DesktopViewer 1`] = ` + +
+ +`; + +exports[`AccessConsoles with SerialConsole and VncConsole as children 1`] = ` + +
+
+
+ +
+
+
+
+`; + +exports[`AccessConsoles with SerialConsole as a single child 1`] = ` + +
+ +`; + +exports[`AccessConsoles with VncConsole as a single child 1`] = ` + +
+ +`; + +exports[`AccessConsoles with preselected SerialConsole 1`] = ` + +
+
+ + +
+
+
+
+
+ + + + + +
+
+ Loading ... +
+
+
+
+
+
+`; + +exports[`AccessConsoles with wrapped SerialConsole as a child 1`] = ` + +
+

+ Serial console text +

+
+
+`; diff --git a/packages/react-console/src/components/DesktopViewer/__tests__/DesktopViewer.test.tsx b/packages/react-console/src/components/DesktopViewer/__tests__/DesktopViewer.test.tsx index 270b69ac976..7d32eade352 100644 --- a/packages/react-console/src/components/DesktopViewer/__tests__/DesktopViewer.test.tsx +++ b/packages/react-console/src/components/DesktopViewer/__tests__/DesktopViewer.test.tsx @@ -1,6 +1,8 @@ import React from 'react'; -import { render } from '@testing-library/react'; -import { mount } from 'enzyme'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; import { DesktopViewer } from '../DesktopViewer'; import { MoreInformationDefaultContent } from '../MoreInformationDefaultContent'; @@ -30,77 +32,79 @@ const rdp2 = { port: 1234 }; -test('DesktopViewer empty', () => { - const wrapper = render(); - expect(wrapper.container).toMatchSnapshot(); -}); +describe('DesktopViewer', () => { + test('empty', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); -test('DesktopViewer with Spice and VNC', () => { - const wrapper = render(); - expect(wrapper.container).toMatchSnapshot(); -}); + test('with Spice and VNC', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); -test('DesktopViewer with Spice, VNC and RDP', () => { - const wrapper = render(); - expect(wrapper.container).toMatchSnapshot(); -}); + test('with Spice, VNC and RDP', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); -test('DesktopViewer with Spice, VNC and RDP (different hostname)', () => { - const wrapper = render(); - expect(wrapper.container).toMatchSnapshot(); -}); + test('with Spice, VNC and RDP (different hostname)', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); -test('DesktopViewer launch button', () => { - const onDownload = jest.fn(); - const onGenerate = jest.fn().mockReturnValue({ content: 'Foo' }); - const wrapper = mount(); - const launchButton = wrapper.find('button.pf-c-console__remote-viewer-launch-vv'); - expect(launchButton).toHaveLength(1); - launchButton.simulate('click'); - expect(onGenerate).toHaveBeenCalledTimes(1); - expect(onDownload).toHaveBeenCalledTimes(1); -}); + test('launch button', () => { + const onDownload = jest.fn(); + const onGenerate = jest.fn().mockReturnValue({ content: 'Foo' }); -test('DesktopViewer RDP launch button', () => { - const onDownload = jest.fn(); - const onGenerate = jest.fn().mockReturnValue({ content: 'Foo' }); - const wrapper = mount(); - const launchButton = wrapper.find('button.pf-c-console__remote-viewer-launch-rdp'); - expect(launchButton).toHaveLength(1); - launchButton.simulate('click'); - expect(onGenerate).toHaveBeenCalledTimes(1); - expect(onDownload).toHaveBeenCalledTimes(1); -}); + render(); -test('DesktopViewer with custom more-info content', () => { - const wrapper = mount( - -

My more-info content

-
- ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('#custom-more-info')).toHaveLength(0); - const linkMoreInfo = wrapper.find('.pf-c-expandable-section__toggle'); - expect(linkMoreInfo).toHaveLength(1); - linkMoreInfo.simulate('click'); - expect(wrapper.find('.pf-c-expandable-section__content')).toHaveLength(1); -}); + userEvent.click(screen.getByRole('button', { name: 'Launch Remote Viewer' })); + expect(onGenerate).toHaveBeenCalledTimes(1); + expect(onDownload).toHaveBeenCalledTimes(1); + }); -test('default MoreInformationContent', () => { - const wrapper = render(); - expect(wrapper.container).toMatchSnapshot(); -}); + test('RDP launch button', () => { + const onDownload = jest.fn(); + const onGenerate = jest.fn().mockReturnValue({ content: 'Foo' }); -test('default implementation of generateVVFile()', () => { - const output = generateDescriptorFile(spice, SPICE_CONSOLE_TYPE); - expect(output.mimeType).toMatch('application/x-virt-viewer'); - expect(output.content).toMatch( - '[virt-viewer]\ntype=spice\nhost=my.host.com\nport=5900\ndelete-this-file=1\nfullscreen=0\n' - ); -}); + render(); + + userEvent.click(screen.getByRole('button', { name: 'Launch Remote Desktop' })); + expect(onGenerate).toHaveBeenCalledTimes(1); + expect(onDownload).toHaveBeenCalledTimes(1); + }); + + test('with custom more-info content', () => { + render( + +

My more-info content

+
+ ); + + expect(screen.queryByText('My more-info content')).toBeNull(); + + userEvent.click(screen.getByRole('button', { name: 'Remote Viewer Details' })); + // If one of the items is shown in the description list, the rest will be in the document as well. + expect(screen.getByText('RHEL, CentOS')).toBeInTheDocument(); + }); + + test('default MoreInformationContent', () => { + render(); + expect(screen.getByText(/Launch Remote Viewer/).outerHTML).toMatchSnapshot(); + }); + + test('default implementation of generateVVFile()', () => { + const output = generateDescriptorFile(spice, SPICE_CONSOLE_TYPE); + expect(output.mimeType).toMatch('application/x-virt-viewer'); + expect(output.content).toMatch( + '[virt-viewer]\ntype=spice\nhost=my.host.com\nport=5900\ndelete-this-file=1\nfullscreen=0\n' + ); + }); -test('default implementation of generateRDPFile()', () => { - const output = generateDescriptorFile(rdp, RDP_CONSOLE_TYPE); - expect(output.mimeType).toMatch('application/rdp'); - expect(output.content).toEqual(expect.stringContaining('full address:s:my.host.com:3389\n')); // the rest is a constant so far + test('default implementation of generateRDPFile()', () => { + const output = generateDescriptorFile(rdp, RDP_CONSOLE_TYPE); + expect(output.mimeType).toMatch('application/rdp'); + expect(output.content).toEqual(expect.stringContaining('full address:s:my.host.com:3389\n')); // the rest is a constant so far + }); }); diff --git a/packages/react-console/src/components/DesktopViewer/__tests__/__snapshots__/DesktopViewer.test.tsx.snap b/packages/react-console/src/components/DesktopViewer/__tests__/__snapshots__/DesktopViewer.test.tsx.snap index bac39ec86e9..ed545b28d0c 100644 --- a/packages/react-console/src/components/DesktopViewer/__tests__/__snapshots__/DesktopViewer.test.tsx.snap +++ b/packages/react-console/src/components/DesktopViewer/__tests__/__snapshots__/DesktopViewer.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`DesktopViewer default MoreInformationContent 1`] = `"

Clicking \\"Launch Remote Viewer\\" will download a .vv file and launch Remote Viewer

"`; + exports[`DesktopViewer empty 1`] = ` -
+
@@ -43,11 +45,11 @@ exports[`DesktopViewer empty 1`] = ` />
-
+
`; exports[`DesktopViewer with Spice and VNC 1`] = ` -
+
@@ -104,7 +106,6 @@ exports[`DesktopViewer with Spice and VNC 1`] = ` hidden="" id="" > -
@@ -193,8 +194,7 @@ exports[`DesktopViewer with Spice and VNC 1`] = ` class="pf-c-description-list__text" >
- Download the MSI from - + Download the MSI from
-
+ `; exports[`DesktopViewer with Spice, VNC and RDP (different hostname) 1`] = ` -
+ `; diff --git a/packages/react-console/src/components/SerialConsole/__tests__/SerialConsole.test.tsx b/packages/react-console/src/components/SerialConsole/__tests__/SerialConsole.test.tsx index 2af5f23a354..cf0e42056b4 100644 --- a/packages/react-console/src/components/SerialConsole/__tests__/SerialConsole.test.tsx +++ b/packages/react-console/src/components/SerialConsole/__tests__/SerialConsole.test.tsx @@ -1,51 +1,74 @@ import React from 'react'; -import { shallow, render } from 'enzyme'; + +import { render } from '@testing-library/react'; + import { SerialConsole } from '../SerialConsole'; import { constants } from '../../common/constants'; const { CONNECTED, DISCONNECTED, LOADING } = constants; -test('SerialConsole in the LOADING state', () => { - const view = shallow( - - ); - expect(view).toMatchSnapshot(); -}); +describe('SerialConsole', () => { + beforeAll(() => { + window.HTMLCanvasElement.prototype.getContext = () => ({ canvas: {} } as any); + }); -test('SerialConsole in the DISCONNECTED state', () => { - const view = shallow( - - ); - expect(view).toMatchSnapshot(); -}); + test('in the LOADING state', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); -const connectedState = ( - -); - -test('SerialConsole in the CONNECTED state', () => { - const view = shallow(connectedState); - expect(view).toMatchSnapshot(); -}); + test('in the DISCONNECTED state', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + describe('with CONNECTED state', () => { + beforeAll(() => { + window.HTMLCanvasElement.prototype.getContext = () => + ({ canvas: {}, createLinearGradient: jest.fn(), fillRect: jest.fn() } as any); + global.window.matchMedia = () => ({ + addListener: jest.fn(), + removeListener: jest.fn(), + matches: true, + media: undefined, + onchange: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn() + }); + }); + + test('renders', () => { + const { asFragment } = render( + + ); -test('Render SerialConsole in the CONNECTED state', () => { - const view = render(connectedState); - expect(view).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); + }); + }); }); diff --git a/packages/react-console/src/components/SerialConsole/__tests__/XTerm.test.tsx b/packages/react-console/src/components/SerialConsole/__tests__/XTerm.test.tsx index 308e226416c..efa6bb45ddc 100644 --- a/packages/react-console/src/components/SerialConsole/__tests__/XTerm.test.tsx +++ b/packages/react-console/src/components/SerialConsole/__tests__/XTerm.test.tsx @@ -1,8 +1,26 @@ import React from 'react'; -import { render } from 'enzyme'; +import { render } from '@testing-library/react'; import { XTerm } from '../XTerm'; -test('Render empty XTerm component', () => { - const view = render(); - expect(view).toMatchSnapshot(); +describe('XTerm', () => { + beforeAll(() => { + window.HTMLCanvasElement.prototype.getContext = () => + ({ canvas: {}, createLinearGradient: jest.fn(), fillRect: jest.fn() } as any); + + global.window.matchMedia = () => ({ + addListener: jest.fn(), + removeListener: jest.fn(), + matches: true, + media: undefined, + onchange: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn() + }); + }); + + test('Render empty XTerm component', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); }); diff --git a/packages/react-console/src/components/SerialConsole/__tests__/__snapshots__/SerialConsole.test.tsx.snap b/packages/react-console/src/components/SerialConsole/__tests__/__snapshots__/SerialConsole.test.tsx.snap index 1d646d7bbf2..1cd2ff3ade0 100644 --- a/packages/react-console/src/components/SerialConsole/__tests__/__snapshots__/SerialConsole.test.tsx.snap +++ b/packages/react-console/src/components/SerialConsole/__tests__/__snapshots__/SerialConsole.test.tsx.snap @@ -1,7 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Render SerialConsole in the CONNECTED state 1`] = ` -Array [ +exports[`SerialConsole in the DISCONNECTED state 1`] = ` + +
+
+
+
+ My text for Disconnected +
+ +
+
+
+
+`; + +exports[`SerialConsole in the LOADING state 1`] = ` +
@@ -13,7 +45,7 @@ Array [ data-ouia-safe="true" type="button" > - My text for Disconnect + Disconnect -
, +
-
, -] -`; - -exports[`SerialConsole in the CONNECTED state 1`] = ` - -`; - -exports[`SerialConsole in the DISCONNECTED state 1`] = ` - + class="pf-c-empty-state" + > +
+
+ + + + + +
+
+ My text for Loading +
+
+
+
+ `; -exports[`SerialConsole in the LOADING state 1`] = ` - +exports[`SerialConsole with CONNECTED state renders 1`] = ` + +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+