From 5b598015c49200d33363b90ba83d6fea1e34d151 Mon Sep 17 00:00:00 2001 From: Michael Roberson Date: Fri, 10 Jul 2026 09:25:57 -0400 Subject: [PATCH 1/2] Move getServerRoute to selectors module and add regression test. Fixes mattermost-community/mattermost-plugin-webex#83. --- webapp/src/index.js | 16 +------- webapp/src/index.test.js | 80 ++++++++++++++++++++++++++++++++++++++++ webapp/src/selectors.js | 16 ++++++++ 3 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 webapp/src/index.test.js create mode 100644 webapp/src/selectors.js diff --git a/webapp/src/index.js b/webapp/src/index.js index 3f37b5a..58dcaa0 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -11,6 +11,7 @@ import Icon from './components/icon.jsx'; import PostTypeWebex from './components/post_type_webex'; import {startMeeting} from './actions'; import Client from './client'; +import {getServerRoute} from './selectors'; const {id: pluginId} = manifest; @@ -39,18 +40,3 @@ class Plugin { } window.registerPlugin(pluginId, new Plugin()); - -const getServerRoute = (state) => { - const config = getConfig(state); - - let basePath = ''; - if (config && config.SiteURL) { - basePath = new URL(config.SiteURL).pathname; - - if (basePath && basePath[basePath.length - 1] === '/') { - basePath = basePath.substr(0, basePath.length - 1); - } - } - - return basePath; -}; diff --git a/webapp/src/index.test.js b/webapp/src/index.test.js new file mode 100644 index 0000000..e64a8ad --- /dev/null +++ b/webapp/src/index.test.js @@ -0,0 +1,80 @@ +/** + * @jest-environment jsdom + */ + +/* eslint-disable max-nested-callbacks */ + +jest.mock('@emotion/react', () => ({ + jsx: (type, props) => ({type, props}), +})); + +jest.mock('./components/icon.jsx', () => ({__esModule: true, default: () => null})); +jest.mock('./components/post_type_webex', () => ({__esModule: true, default: () => null})); + +jest.mock('./actions', () => ({ + startMeeting: jest.fn(() => jest.fn()), +})); + +jest.mock('./client', () => ({ + __esModule: true, + default: { + setServerRoute: jest.fn(), + }, +})); + +jest.mock('mattermost-redux/selectors/entities/general', () => ({ + getConfig: jest.fn(() => ({SiteURL: 'https://example.com'})), +})); + +jest.mock('./selectors', () => ({ + getServerRoute: jest.fn(() => ''), +})); + +describe('Plugin initialization', () => { + let mockRegistry; + let mockStore; + + beforeEach(() => { + jest.clearAllMocks(); + + mockRegistry = { + registerChannelHeaderButtonAction: jest.fn(), + registerAppBarComponent: jest.fn(), + registerPostTypeComponent: jest.fn(), + }; + mockStore = { + getState: jest.fn(() => ({})), + dispatch: jest.fn(), + }; + + // Mirror modern Mattermost: window.registerPlugin invokes initialize() + // synchronously during registration. If any module-scoped identifier + // used inside initialize() is still in the temporal dead zone at this + // point (e.g. a const arrow declared after registerPlugin), requiring + // ./index will throw a ReferenceError. + window.registerPlugin = jest.fn((_pluginId, plugin) => { + plugin.initialize(mockRegistry, mockStore); + }); + }); + + test('module evaluation with synchronous initialize() does not throw', () => { + expect(() => { + jest.isolateModules(() => { + require('./index'); // eslint-disable-line global-require + }); + }).not.toThrow(); + }); + + test('registers channel header button, app bar icon, and post type component', () => { + jest.isolateModules(() => { + require('./index'); // eslint-disable-line global-require + }); + + expect(mockRegistry.registerChannelHeaderButtonAction).toHaveBeenCalledTimes(1); + expect(mockRegistry.registerAppBarComponent).toHaveBeenCalledTimes(1); + expect(mockRegistry.registerPostTypeComponent).toHaveBeenCalledWith( + 'custom_webex', + expect.anything(), + ); + }); +}); diff --git a/webapp/src/selectors.js b/webapp/src/selectors.js new file mode 100644 index 0000000..c0aac57 --- /dev/null +++ b/webapp/src/selectors.js @@ -0,0 +1,16 @@ +import {getConfig} from 'mattermost-redux/selectors/entities/general'; + +export const getServerRoute = (state) => { + const config = getConfig(state); + + let basePath = ''; + if (config && config.SiteURL) { + basePath = new URL(config.SiteURL).pathname; + + if (basePath && basePath[basePath.length - 1] === '/') { + basePath = basePath.substr(0, basePath.length - 1); + } + } + + return basePath; +}; From d0a0badab2b7ba628b63e728319b47e04786f82e Mon Sep 17 00:00:00 2001 From: Michael Roberson Date: Tue, 14 Jul 2026 16:01:26 -0400 Subject: [PATCH 2/2] Add selector tests. Add try/catch in getServerRoute to handle malformed site URL values. --- webapp/src/selectors.js | 10 +++++--- webapp/src/selectors.test.js | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 webapp/src/selectors.test.js diff --git a/webapp/src/selectors.js b/webapp/src/selectors.js index c0aac57..f8d6dbc 100644 --- a/webapp/src/selectors.js +++ b/webapp/src/selectors.js @@ -5,10 +5,14 @@ export const getServerRoute = (state) => { let basePath = ''; if (config && config.SiteURL) { - basePath = new URL(config.SiteURL).pathname; + try { + basePath = new URL(config.SiteURL).pathname; - if (basePath && basePath[basePath.length - 1] === '/') { - basePath = basePath.substr(0, basePath.length - 1); + if (basePath && basePath[basePath.length - 1] === '/') { + basePath = basePath.substr(0, basePath.length - 1); + } + } catch (e) { + basePath = ''; } } diff --git a/webapp/src/selectors.test.js b/webapp/src/selectors.test.js new file mode 100644 index 0000000..ddcba7a --- /dev/null +++ b/webapp/src/selectors.test.js @@ -0,0 +1,48 @@ +import {getConfig} from 'mattermost-redux/selectors/entities/general'; + +import {getServerRoute} from './selectors'; + +jest.mock('mattermost-redux/selectors/entities/general', () => ({ + getConfig: jest.fn(), +})); + +describe('getServerRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns empty string when config is missing', () => { + getConfig.mockReturnValue(null); + expect(getServerRoute({})).toBe(''); + }); + + test('returns empty string when SiteURL is missing', () => { + getConfig.mockReturnValue({}); + expect(getServerRoute({})).toBe(''); + }); + + test('returns empty string when SiteURL has no subpath', () => { + getConfig.mockReturnValue({SiteURL: 'https://example.com'}); + expect(getServerRoute({})).toBe(''); + }); + + test('returns pathname when SiteURL has a subpath', () => { + getConfig.mockReturnValue({SiteURL: 'https://example.com/mattermost'}); + expect(getServerRoute({})).toBe('/mattermost'); + }); + + test('strips trailing slash from subpath', () => { + getConfig.mockReturnValue({SiteURL: 'https://example.com/mattermost/'}); + expect(getServerRoute({})).toBe('/mattermost'); + }); + + test('returns empty string when SiteURL is a bare root URL with trailing slash', () => { + getConfig.mockReturnValue({SiteURL: 'https://example.com/'}); + expect(getServerRoute({})).toBe(''); + }); + + test('returns empty string when SiteURL is malformed', () => { + getConfig.mockReturnValue({SiteURL: 'not a valid url'}); + expect(getServerRoute({})).toBe(''); + }); +});