From 71f56bfa4a0de954bedab404940885dde6e9589e Mon Sep 17 00:00:00 2001 From: mickelr <121160648+mickelr@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:44:38 +0800 Subject: [PATCH] Feat/practice session - start/stop control API and displayhint (#4008) --- .../@webex/plugin-meetings/src/constants.ts | 6 ++ .../src/meeting/in-meeting-actions.ts | 17 ++++ .../plugin-meetings/src/meeting/index.ts | 18 ++++ .../plugin-meetings/src/members/util.ts | 1 + .../plugin-meetings/src/webinar/index.ts | 48 ++++++++-- .../unit/spec/meeting/in-meeting-actions.ts | 12 ++- .../test/unit/spec/meeting/index.js | 3 + .../test/unit/spec/members/utils.js | 95 +++++++++++++++++++ .../test/unit/spec/webinar/index.ts | 47 +++++++++ 9 files changed, 238 insertions(+), 9 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 6c2a155a86c..5231d3e0184 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -988,6 +988,12 @@ export const DISPLAY_HINTS = { STAGE_VIEW_INACTIVE: 'STAGE_VIEW_INACTIVE', ENABLE_STAGE_VIEW: 'ENABLE_STAGE_VIEW', DISABLE_STAGE_VIEW: 'DISABLE_STAGE_VIEW', + + // Practice Session + PRACTICE_SESSION_ON: 'PRACTICE_SESSION_ON', + PRACTICE_SESSION_OFF: 'PRACTICE_SESSION_OFF', + SHOW_PRACTICE_SESSION_START: 'SHOW_PRACTICE_SESSION_START', + SHOW_PRACTICE_SESSION_STOP: 'SHOW_PRACTICE_SESSION_STOP', }; export const INTERSTITIAL_DISPLAY_HINTS = [DISPLAY_HINTS.VOIP_IS_ENABLED]; diff --git a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts index 3d1810ad539..f803251507c 100644 --- a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts @@ -93,6 +93,10 @@ interface IInMeetingActions { canShowStageView?: boolean; canEnableStageView?: boolean; canDisableStageView?: boolean; + isPracticeSessionOn?: boolean; + isPracticeSessionOff?: boolean; + canStartPracticeSession?: boolean; + canStopPracticeSession?: boolean; } /** @@ -266,6 +270,15 @@ export default class InMeetingActions implements IInMeetingActions { canEnableStageView = null; canDisableStageView = null; + + isPracticeSessionOn = null; + + isPracticeSessionOff = null; + + canStartPracticeSession = null; + + canStopPracticeSession = null; + /** * Returns all meeting action options * @returns {Object} @@ -354,6 +367,10 @@ export default class InMeetingActions implements IInMeetingActions { canShowStageView: this.canShowStageView, canEnableStageView: this.canEnableStageView, canDisableStageView: this.canDisableStageView, + isPracticeSessionOn: this.isPracticeSessionOn, + isPracticeSessionOff: this.isPracticeSessionOff, + canStartPracticeSession: this.canStartPracticeSession, + canStopPracticeSession: this.canStopPracticeSession, }); /** diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 9ab2f03b185..ba985aef88f 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -2660,6 +2660,7 @@ export default class Meeting extends StatelessWebexPlugin { }); this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, ({state}) => { + this.webinar.updatePracticeSessionStatus(state); Trigger.trigger( this, {file: 'meeting/index', function: 'setupLocusControlsListener'}, @@ -3516,6 +3517,7 @@ export default class Meeting extends StatelessWebexPlugin { emailAddress: string; email: string; phoneNumber: string; + roles: Array; }, alertIfActive = true ) { @@ -3915,6 +3917,22 @@ export default class Meeting extends StatelessWebexPlugin { requiredHints: [DISPLAY_HINTS.DISABLE_STAGE_VIEW], displayHints: this.userDisplayHints, }), + isPracticeSessionOn: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_ON], + displayHints: this.userDisplayHints, + }), + isPracticeSessionOff: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_OFF], + displayHints: this.userDisplayHints, + }), + canStartPracticeSession: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_START], + displayHints: this.userDisplayHints, + }), + canStopPracticeSession: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_STOP], + displayHints: this.userDisplayHints, + }), canShareFile: (ControlsOptionsUtil.hasHints({ requiredHints: [DISPLAY_HINTS.SHARE_FILE], diff --git a/packages/@webex/plugin-meetings/src/members/util.ts b/packages/@webex/plugin-meetings/src/members/util.ts index 449d686f22a..930d187b2d7 100644 --- a/packages/@webex/plugin-meetings/src/members/util.ts +++ b/packages/@webex/plugin-meetings/src/members/util.ts @@ -46,6 +46,7 @@ const MembersUtil = { { address: options.invitee.emailAddress || options.invitee.email || options.invitee.phoneNumber, + ...(options.invitee.roles ? {roles: options.invitee.roles} : {}), }, ], alertIfActive: options.alertIfActive, diff --git a/packages/@webex/plugin-meetings/src/webinar/index.ts b/packages/@webex/plugin-meetings/src/webinar/index.ts index 709ca7f9cde..f185161d8b6 100644 --- a/packages/@webex/plugin-meetings/src/webinar/index.ts +++ b/packages/@webex/plugin-meetings/src/webinar/index.ts @@ -3,9 +3,10 @@ */ import {WebexPlugin} from '@webex/webex-core'; import {get} from 'lodash'; -import {MEETINGS, SELF_ROLES} from '../constants'; +import {HTTP_VERBS, MEETINGS, SELF_ROLES} from '../constants'; import WebinarCollection from './collection'; +import LoggerProxy from '../common/logs/logger-proxy'; /** * @class Webinar @@ -22,6 +23,7 @@ const Webinar = WebexPlugin.extend({ canManageWebcast: 'boolean', // appears the ability to manage webcast selfIsPanelist: 'boolean', // self is panelist selfIsAttendee: 'boolean', // self is attendee + practiceSessionEnabled: 'boolean', // practice session enabled }, /** @@ -59,18 +61,48 @@ const Webinar = WebexPlugin.extend({ * @returns {{isPromoted: boolean, isDemoted: boolean}} Role transition states */ updateRoleChanged(payload) { + const oldRoles = get(payload, 'oldRoles', []); + const newRoles = get(payload, 'newRoles', []); + const isPromoted = - get(payload, 'oldRoles', []).includes(SELF_ROLES.ATTENDEE) && - get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST); + oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST); const isDemoted = - get(payload, 'oldRoles', []).includes(SELF_ROLES.PANELIST) && - get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE); - this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST)); - this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE)); - this.updateCanManageWebcast(get(payload, 'newRoles', []).includes(SELF_ROLES.MODERATOR)); + oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE); + this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST)); + this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE)); + this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR)); return {isPromoted, isDemoted}; }, + + /** + * start or stop practice session for webinar + * @param {boolean} enabled + * @returns {Promise} + */ + setPracticeSessionState(enabled) { + return this.request({ + method: HTTP_VERBS.PATCH, + uri: `${this.locusUrl}/controls`, + body: { + practiceSession: { + enabled, + }, + }, + }).catch((error) => { + LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error); + throw error; + }); + }, + + /** + * update practice session status + * @param {object} payload + * @returns {void} + */ + updatePracticeSessionStatus(payload) { + this.set('practiceSessionEnabled', payload.enabled); + }, }); export default Webinar; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts index c64833dd0f6..d6b2dec374b 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts @@ -88,6 +88,11 @@ describe('plugin-meetings', () => { canShowStageView: null, canEnableStageView: null, canDisableStageView: null, + isPracticeSessionOn : null, + isPracticeSessionOff : null, + canStartPracticeSession: null, + canStopPracticeSession: null, + ...expected, }; @@ -181,7 +186,12 @@ describe('plugin-meetings', () => { 'canShowStageView', 'canEnableStageView', 'canDisableStageView', - ].forEach((key) => { + 'isPracticeSessionOn', + 'isPracticeSessionOff', + 'canStartPracticeSession', + 'canStopPracticeSession', + + ].forEach((key) => { it(`get and set for ${key} work as expected`, () => { const inMeetingActions = new InMeetingActions(); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 791f04e0c39..71a930a9253 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -9044,6 +9044,8 @@ describe('plugin-meetings', () => { }); it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => { + meeting.webinar.updatePracticeSessionStatus = sinon.stub(); + const state = {example: 'value'}; await meeting.locusInfo.emitScoped( @@ -9052,6 +9054,7 @@ describe('plugin-meetings', () => { {state} ); + assert.calledOnceWithExactly( meeting.webinar.updatePracticeSessionStatus, state); assert.calledWith( TriggerProxy.trigger, meeting, diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js index c5477c8b340..e79aca4e490 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js @@ -262,5 +262,100 @@ describe('plugin-meetings', () => { testParams(false); }); }); + + describe('#getAddMemberBody', () => { + it('returns the correct body with email address and roles', () => { + const options = { + invitee: { + emailAddress: 'test@example.com', + roles: ['role1', 'role2'], + }, + alertIfActive: true, + }; + + assert.deepEqual(MembersUtil.getAddMemberBody(options), { + invitees: [ + { + address: 'test@example.com', + roles: ['role1', 'role2'], + }, + ], + alertIfActive: true, + }); + }); + + it('returns the correct body with phone number and no roles', () => { + const options = { + invitee: { + phoneNumber: '1234567890', + }, + alertIfActive: false, + }; + + assert.deepEqual(MembersUtil.getAddMemberBody(options), { + invitees: [ + { + address: '1234567890', + }, + ], + alertIfActive: false, + }); + }); + + it('returns the correct body with fallback to email', () => { + const options = { + invitee: { + email: 'fallback@example.com', + }, + alertIfActive: true, + }; + + assert.deepEqual(MembersUtil.getAddMemberBody(options), { + invitees: [ + { + address: 'fallback@example.com', + }, + ], + alertIfActive: true, + }); + }); + + it('handles missing `alertIfActive` gracefully', () => { + const options = { + invitee: { + emailAddress: 'test@example.com', + roles: ['role1'], + }, + }; + + assert.deepEqual(MembersUtil.getAddMemberBody(options), { + invitees: [ + { + address: 'test@example.com', + roles: ['role1'], + }, + ], + alertIfActive: undefined, + }); + }); + + it('ignores roles if not provided', () => { + const options = { + invitee: { + emailAddress: 'test@example.com', + }, + alertIfActive: false, + }; + + assert.deepEqual(MembersUtil.getAddMemberBody(options), { + invitees: [ + { + address: 'test@example.com', + }, + ], + alertIfActive: false, + }); + }); + }); }); }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts index 8a06824220c..d466995c637 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts @@ -122,5 +122,52 @@ describe('plugin-meetings', () => { }); }); + describe("#setPracticeSessionState", () => { + [true, false].forEach((enabled) => { + it(`sends a PATCH request to ${enabled ? "enable" : "disable"} the practice session`, async () => { + const result = await webinar.setPracticeSessionState(enabled); + assert.calledOnce(webex.request); + assert.calledWith(webex.request, { + method: "PATCH", + uri: `${webinar.locusUrl}/controls`, + body: { + practiceSession: { enabled } + } + }); + assert.equal(result, "REQUEST_RETURN_VALUE", "should return the resolved value from the request"); + }); + }); + + it('handles API call failures gracefully', async () => { + webex.request.rejects(new Error('API_ERROR')); + const errorLogger = sinon.stub(LoggerProxy.logger, 'error'); + + try { + await webinar.setPracticeSessionState(true); + assert.fail('setPracticeSessionState should throw an error'); + } catch (error) { + assert.equal(error.message, 'API_ERROR', 'should throw the correct error'); + assert.calledOnce(errorLogger); + assert.calledWith(errorLogger, 'Meeting:webinar#setPracticeSessionState failed', sinon.match.instanceOf(Error)); + } + + errorLogger.restore(); + }); + }); + + describe('#updatePracticeSessionStatus', () => { + it('sets PS state true', () => { + webinar.updatePracticeSessionStatus({enabled: true}); + + assert.equal(webinar.practiceSessionEnabled, true); + }); + it('sets PS state true', () => { + webinar.updatePracticeSessionStatus({enabled: false}); + + assert.equal(webinar.practiceSessionEnabled, false); + }); + }); + + }) })