diff --git a/package.json b/package.json index e92456676c..cb15c87ddf 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,13 @@ "sanitize-html": "^2.3.2", "ua-parser-js": "^0.7.24" }, + "devDependencies-comments": { + "tchap-added" : { + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", + "enzyme": "^3.11.0", + "enzyme-to-json": "^3.6.2" + } + }, "devDependencies": { "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", @@ -102,6 +109,7 @@ "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", "allchange": "^1.0.6", "autoprefixer": "^9.8.6", "babel-jest": "^28.0.0", @@ -111,6 +119,8 @@ "cpx": "^1.5.0", "css-loader": "^3.6.0", "dotenv": "^10.0.0", + "enzyme": "^3.11.0", + "enzyme-to-json": "^3.6.2", "eslint": "8.9.0", "eslint-config-google": "^0.14.0", "eslint-plugin-deprecate": "^0.7.0", @@ -172,15 +182,28 @@ "resolutions": { "@types/react": "17.0.14" }, + "jest-comments": { + "README" : "For the tests to work, you need matrix-react-sdk to be git-cloned and yarn linked into this project.", + "snapshotSerializers" : "used for jest snapshot", + "testEnvironment" :"switch to jsdom like in matrix-react-sdk", + "testMatch" :"execute only tests in unit-tests directory", + "setupFilesAfterEnv" :"duplicate enzyme configuration in our own setup file '/test/setupTests.js'", + "moduleNameMapper" : "use mapper from element-web, helps at mocking {module, ressources} directly with regexp", + "transformIgnorePatterns" : "make regexp inline {matrix-js-sdk|matrix-react-sdk} else it does not work" + }, "jest": { + "snapshotSerializers": [ + "enzyme-to-json/serializer" + ], "testEnvironment": "jsdom", "testEnvironmentOptions": { "url": "http://localhost/" }, "testMatch": [ - "/test/**/*-test.[tj]s?(x)" + "/test/unit-tests/**/*-test.[tj]s?(x)" ], "setupFilesAfterEnv": [ + "/test/setupTests.js", "/node_modules/matrix-react-sdk/test/setupTests.js" ], "moduleNameMapper": { @@ -203,8 +226,7 @@ "RecorderWorklet": "/node_modules/matrix-react-sdk/__mocks__/empty.js" }, "transformIgnorePatterns": [ - "/node_modules/(?!matrix-js-sdk).+$", - "/node_modules/(?!matrix-react-sdk).+$" + "\/node_modules\/(?!matrix-js-sdk|matrix-react-sdk).+$" ], "coverageReporters": [ "text-summary", diff --git a/src/components/views/dialogs/TchapCreateRoomDialog.tsx b/src/components/views/dialogs/TchapCreateRoomDialog.tsx index ee86482ba0..b4c0d6383f 100644 --- a/src/components/views/dialogs/TchapCreateRoomDialog.tsx +++ b/src/components/views/dialogs/TchapCreateRoomDialog.tsx @@ -26,7 +26,7 @@ import BaseDialog from "matrix-react-sdk/src/components/views/dialogs/BaseDialog import TchapUtils from '../../../util/TchapUtils'; import TchapRoomTypeSelector from "./../elements/TchapRoomTypeSelector"; import { TchapRoomType } from "../../../@types/tchap"; -import roomCreateOptions from "../../../lib/createTchapRoom"; +import TchapCreateRoom from "../../../lib/createTchapRoom"; // We leave the same props as Element's version, to avoid unknown props warnings. interface IProps { @@ -123,7 +123,7 @@ export default class TchapCreateRoomDialog extends React.Component(resolve => this.setState({}, resolve)); if (this.state.nameIsValid) { - this.props.onFinished(true, roomCreateOptions( + this.props.onFinished(true, TchapCreateRoom.roomCreateOptions( this.state.name, this.state.tchapRoomType, this.state.isFederated)); diff --git a/src/lib/createTchapRoom.ts b/src/lib/createTchapRoom.ts index f4ad1edaf0..13d3098861 100644 --- a/src/lib/createTchapRoom.ts +++ b/src/lib/createTchapRoom.ts @@ -17,49 +17,53 @@ export const DEFAULT_FEDERATE_VALUE = true; * @param federate is the room federated * @returns rooms options */ -export default function roomCreateOptions( - name: string, tchapRoomType: TchapRoomType, federate: boolean = DEFAULT_FEDERATE_VALUE): IOpts { - const opts: IOpts = {}; - const createRoomOpts: ITchapCreateRoomOpts = {}; - opts.createOpts = createRoomOpts; +export default class TchapCreateRoom { + static roomCreateOptions( + name: string, + tchapRoomType: TchapRoomType, + federate: boolean = DEFAULT_FEDERATE_VALUE): IOpts { + const opts: IOpts = {}; + const createRoomOpts: ITchapCreateRoomOpts = {}; + opts.createOpts = createRoomOpts; - //tchap common options - createRoomOpts.name = name; - opts.guestAccess = false; //guest access are not authorized in tchap + //tchap common options + createRoomOpts.name = name; + opts.guestAccess = false; //guest access are not authorized in tchap - createRoomOpts.creation_content = { 'm.federate': federate }; + createRoomOpts.creation_content = { 'm.federate': federate }; - switch (tchapRoomType) { - case TchapRoomType.Forum: { + switch (tchapRoomType) { + case TchapRoomType.Forum: { //"Forum" only for tchap members and not encrypted - createRoomOpts.accessRule = TchapRoomAccessRule.Restricted; - createRoomOpts.visibility = Visibility.Public; - createRoomOpts.preset = Preset.PublicChat; - opts.joinRule = JoinRule.Public; - opts.encryption = false; - opts.historyVisibility = HistoryVisibility.Shared; - break; - } - case TchapRoomType.Private: { + createRoomOpts.accessRule = TchapRoomAccessRule.Restricted; + createRoomOpts.visibility = Visibility.Public; + createRoomOpts.preset = Preset.PublicChat; + opts.joinRule = JoinRule.Public; + opts.encryption = false; + opts.historyVisibility = HistoryVisibility.Shared; + break; + } + case TchapRoomType.Private: { //"Salon", only for tchap member and encrypted - createRoomOpts.accessRule = TchapRoomAccessRule.Restricted; - createRoomOpts.visibility = Visibility.Private; - createRoomOpts.preset = Preset.PrivateChat; - opts.joinRule = JoinRule.Invite; - opts.encryption = true; - opts.historyVisibility = HistoryVisibility.Invited; - break; - } - case TchapRoomType.External: { + createRoomOpts.accessRule = TchapRoomAccessRule.Restricted; + createRoomOpts.visibility = Visibility.Private; + createRoomOpts.preset = Preset.PrivateChat; + opts.joinRule = JoinRule.Invite; + opts.encryption = true; + opts.historyVisibility = HistoryVisibility.Invited; + break; + } + case TchapRoomType.External: { //open to external and encrypted, - createRoomOpts.accessRule = TchapRoomAccessRule.Unrestricted; - createRoomOpts.visibility = Visibility.Private; - createRoomOpts.preset = Preset.PrivateChat; - opts.joinRule = JoinRule.Invite; - opts.encryption = true; - opts.historyVisibility = HistoryVisibility.Invited; - break; + createRoomOpts.accessRule = TchapRoomAccessRule.Unrestricted; + createRoomOpts.visibility = Visibility.Private; + createRoomOpts.preset = Preset.PrivateChat; + opts.joinRule = JoinRule.Invite; + opts.encryption = true; + opts.historyVisibility = HistoryVisibility.Invited; + break; + } } + return opts; } - return opts; } diff --git a/test/setupTests.js b/test/setupTests.js new file mode 100644 index 0000000000..e4bd838d37 --- /dev/null +++ b/test/setupTests.js @@ -0,0 +1,5 @@ + +//is duplicated from matrix-react-sdk/test/setupTests.js in order to work +import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; +import { configure } from "enzyme"; +configure({ adapter: new Adapter() }); \ No newline at end of file diff --git a/test/unit-tests/components/views/dialogs/TchapCreateRoomDialog-test.tsx b/test/unit-tests/components/views/dialogs/TchapCreateRoomDialog-test.tsx new file mode 100644 index 0000000000..a9053b4cde --- /dev/null +++ b/test/unit-tests/components/views/dialogs/TchapCreateRoomDialog-test.tsx @@ -0,0 +1,293 @@ + +import React from 'react'; +import { mount, ReactWrapper,shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import toJson from 'enzyme-to-json' + +//jest.mock('matrix-react-sdk/src/languageHandler') + +import { TchapRoomType } from '../../../../../src/@types/tchap'; +import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg'; +import { EventEmitter } from "events"; +import { _t } from 'matrix-react-sdk/src/languageHandler'; + +import TchapUtils from '../../../../../src/util/TchapUtils'; + +//mocking module with jest.mock should be done outside the test. Before any import of the mocked module. +//I could not make a mock of TchapCreateRoomDialog, the real implementation was taken each time. Then I used jest spyOn + +/* return { + jest.mock('../../src/lib/createTchapRoom',() => { + __esModule: true, + default: jest.fn(() => 42), + roomCreateOptions: jest.fn(() => 43), + }; +}); +*/ + +import TchapCreateRoomDialog from "../../../../../src/components/views/dialogs/TchapCreateRoomDialog"; + +/* + * This unit test targets the TchapCreateRoomDialog react component. It is mounted virtually to be tested thanks to enzyme framework. + * With the act function, a state is set inside the component, meanwhile the submitForm function submits the form in order to make action on the internal state of the component. + * No html snapshot is used in this test. Jest mocking is used to simulate behaviour of depending modules such as TchapUtils, TchapCreateRoomDialog, MatrixClientPeg + */ +describe("TchapCreateRoomDialog", () => { + let mockClient; + const roomName = "roomName"; + + // default props for the component, will be overriden + const defaultProps = { + defaultPublic: undefined, // unused for Tchap version + defaultName: undefined, // unused for Tchap version + parentSpace: undefined, // unused for Tchap version + defaultEncrypted: undefined, // unused for Tchap version + onFinished: jest.fn() + }; + + //simulate the submit of the form + const submitForm = async (wrapper: ReactWrapper) =>{ + act(() => { + wrapper.find('form').simulate('submit', { preventDefault: () => { } }); + }) + await new Promise(process.nextTick); + } + + // build a new component using enzyme + const getComponent = (props = {}): ReactWrapper => + mount(); + + /* + // Note : you can also build a shallow component https://fr.reactjs.org/docs/shallow-renderer.html + // can be used for simple component + // not used in this test + const getShallowComponent = (props={}) => { + shallow(); + } + */ + + beforeEach(() => { + jest.resetAllMocks(); + + //mock matrix client + mockClient = new MockClient(); + jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient); + + //mock tchap utils + jest.spyOn(TchapUtils, 'getShortDomain').mockReturnValue("AGENT"); + jest.spyOn(TchapUtils, 'getRoomFederationOptions').mockReturnValue({ showRoomFederationOption: true, roomFederationDefault: false }); + }); + + it('should render the whole component', () => { + const component = getComponent(); + // If this breaks too often, it may not be useful, we will remove it. Leaving for now. + expect(toJson(component)).toMatchSnapshot("all the component"); + }); + + it('should render the whole component with with the allow access switch', () => { + jest.spyOn(TchapUtils, 'getRoomFederationOptions').mockReturnValue({ showRoomFederationOption: true, roomFederationDefault: false }); + const component = getComponent(); + const allowAccessSwitch = component.find(".mx_SettingsFlag"); + expect(toJson(allowAccessSwitch)).toMatchSnapshot("allow access switch is present, allows other domain than AGENT"); + }); + + it('should render the room dialog without the allow access switch', () => { + jest.spyOn(TchapUtils, 'getRoomFederationOptions').mockReturnValue({ showRoomFederationOption: false, roomFederationDefault: false }); + const component = getComponent(); + const allowAccessSwitch = component.find(".mx_SettingsFlag"); + expect(allowAccessSwitch).toEqual({}); + }); + + it("Should not create any room wihout a name", async () => { + const onFinished = jest.fn(); + const wrapper = getComponent({ onFinished}); + // set state in component + act(() => { + wrapper.setState({ + name: "", + tchapRoomType: TchapRoomType.Private + }); + }); + + await submitForm(wrapper); + + expect(onFinished).toBeCalledTimes(0); + }) + + it("Should create a room with default value", async () => { + const onFinished = jest.fn(); + const defaultName = "defaultName"; + const wrapper = getComponent({ onFinished, defaultName}); + // set state in component + act(() => { + wrapper.setState({ + tchapRoomType: TchapRoomType.Private + }); + }); + + await submitForm(wrapper); + + expect(onFinished).toBeCalledTimes(1); + }) + + it("Should create a private room", async () => { + const onFinished = jest.fn(); + + const privateRoomExpectedOpts = { + createOpts: { + name: roomName, + creation_content: { + "m.federate": true, + }, + accessRule: "restricted", + visibility: "private", + preset: "private_chat", + }, + guestAccess: false, + joinRule: "invite", + encryption: true, + historyVisibility: "invited", + }; + + const wrapper = getComponent({ onFinished}); + + // set state in component + act(() => { + wrapper.setState({ + name: roomName, + tchapRoomType: TchapRoomType.Private, + isFederated:true + }); + }); + + await submitForm(wrapper); + + expect(onFinished).toHaveBeenCalledWith(true, privateRoomExpectedOpts); + }) + + + it("Should create a public room without federation", async () => { + const onFinished = jest.fn(); + + const publicRoomWithoutFederationExpectedOpts = { + createOpts: { + name: roomName, + creation_content: { + "m.federate": false, + }, + accessRule: "restricted", + visibility: "public", + preset: "public_chat", + }, + guestAccess: false, + joinRule: "public", + encryption: false, + historyVisibility: "shared", + }; + const wrapper = getComponent({ onFinished}); + + // set state in component + act(() => { + wrapper.setState({ + name: roomName, + tchapRoomType: TchapRoomType.Forum, + isFederated:false + }); + }); + + await submitForm(wrapper); + + expect(onFinished).toHaveBeenCalledWith(true, publicRoomWithoutFederationExpectedOpts); + }) + + it("Should create a public room with federation", async () => { + const onFinished = jest.fn(); + + const publicRoomWithFederationExpectedOpts = { + createOpts: { + name: roomName, + creation_content: { + "m.federate": true, + }, + accessRule: "restricted", + visibility: "public", + preset: "public_chat", + }, + guestAccess: false, + joinRule: "public", + encryption: false, + historyVisibility: "shared", + }; + const wrapper = getComponent({ onFinished}); + + // set state in component + act(() => { + wrapper.setState({ + name: roomName, + tchapRoomType: TchapRoomType.Forum, + isFederated:true + }); + }); + + await submitForm(wrapper); + + expect(onFinished).toHaveBeenCalledWith(true, publicRoomWithFederationExpectedOpts); + }) + + it("Should create an external room", async () => { + const onFinished = jest.fn(); + + const externalRoomExpectedOpts = { + createOpts: { + name: roomName, + creation_content: { + "m.federate": true, + }, + accessRule: "unrestricted", + visibility: "private", + preset: "private_chat", + }, + guestAccess: false, + joinRule: "invite", + encryption: true, + historyVisibility: "invited", + }; + const wrapper = getComponent({ onFinished}); + + // set state in component + act(() => { + wrapper.setState({ + name: roomName, + tchapRoomType: TchapRoomType.External, + isFederated:true + }); + }); + + await submitForm(wrapper); + + expect(onFinished).toHaveBeenCalledWith(true, externalRoomExpectedOpts); + }) +}) + + +class MockClient extends EventEmitter { + // getUserId = jest.fn(); + // getKeyBackupVersion = jest.fn().mockResolvedValue(undefined); + // getRooms = jest.fn().mockReturnValue([]); + // doesServerSupportUnstableFeature = jest.fn().mockResolvedValue(true); + // isCrossSigningReady = jest.fn().mockResolvedValue(true); + // isSecretStorageReady = jest.fn().mockResolvedValue(true); + // isCryptoEnabled = jest.fn().mockReturnValue(true); + // isInitialSyncComplete = jest.fn().mockReturnValue(true); + // getKeyBackupEnabled = jest.fn(); + // getStoredDevicesForUser = jest.fn().mockReturnValue([]); + // getCrossSigningId = jest.fn(); + // getStoredCrossSigningForUser = jest.fn(); + // waitForClientWellKnown = jest.fn(); + // downloadKeys = jest.fn(); + // isRoomEncrypted = jest.fn(); + // getClientWellKnown = jest.fn(); + getDomain = jest.fn(); +} + + diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/TchapCreateRoomDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/TchapCreateRoomDialog-test.tsx.snap new file mode 100644 index 0000000000..89fd7cd841 --- /dev/null +++ b/test/unit-tests/components/views/dialogs/__snapshots__/TchapCreateRoomDialog-test.tsx.snap @@ -0,0 +1,858 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TchapCreateRoomDialog should render the whole component with with the allow access switch: allow access switch is present, allows other domain than AGENT 1`] = ` +
+ + Allow access to this room to all users, even outside "AGENT" domain + + <_default + aria-label="Allow access to this room to all users, even outside \\"AGENT\\" domain" + checked={false} + onChange={[Function]} + > + +
+
+
+ + +
+`; + +exports[`TchapCreateRoomDialog should render the whole component: all the component 1`] = ` + + + + + +
+ +
+

+ Create a room +

+
+
+
+
+
+ + +
+
+