Skip to content

Commit

Permalink
Feat/practice session - start/stop control API and displayhint (#4008)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickelr authored Dec 16, 2024
1 parent 9902ae8 commit 71f56bf
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 9 deletions.
6 changes: 6 additions & 0 deletions packages/@webex/plugin-meetings/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
17 changes: 17 additions & 0 deletions packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ interface IInMeetingActions {
canShowStageView?: boolean;
canEnableStageView?: boolean;
canDisableStageView?: boolean;
isPracticeSessionOn?: boolean;
isPracticeSessionOff?: boolean;
canStartPracticeSession?: boolean;
canStopPracticeSession?: boolean;
}

/**
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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,
});

/**
Expand Down
18 changes: 18 additions & 0 deletions packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down Expand Up @@ -3516,6 +3517,7 @@ export default class Meeting extends StatelessWebexPlugin {
emailAddress: string;
email: string;
phoneNumber: string;
roles: Array<string>;
},
alertIfActive = true
) {
Expand Down Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions packages/@webex/plugin-meetings/src/members/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
48 changes: 40 additions & 8 deletions packages/@webex/plugin-meetings/src/webinar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
},

/**
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ describe('plugin-meetings', () => {
canShowStageView: null,
canEnableStageView: null,
canDisableStageView: null,
isPracticeSessionOn : null,
isPracticeSessionOff : null,
canStartPracticeSession: null,
canStopPracticeSession: null,

...expected,
};

Expand Down Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -9052,6 +9054,7 @@ describe('plugin-meetings', () => {
{state}
);

assert.calledOnceWithExactly( meeting.webinar.updatePracticeSessionStatus, state);
assert.calledWith(
TriggerProxy.trigger,
meeting,
Expand Down
95 changes: 95 additions & 0 deletions packages/@webex/plugin-meetings/test/unit/spec/members/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
roles: ['role1', 'role2'],
},
alertIfActive: true,
};

assert.deepEqual(MembersUtil.getAddMemberBody(options), {
invitees: [
{
address: '[email protected]',
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: '[email protected]',
},
alertIfActive: true,
};

assert.deepEqual(MembersUtil.getAddMemberBody(options), {
invitees: [
{
address: '[email protected]',
},
],
alertIfActive: true,
});
});

it('handles missing `alertIfActive` gracefully', () => {
const options = {
invitee: {
emailAddress: '[email protected]',
roles: ['role1'],
},
};

assert.deepEqual(MembersUtil.getAddMemberBody(options), {
invitees: [
{
address: '[email protected]',
roles: ['role1'],
},
],
alertIfActive: undefined,
});
});

it('ignores roles if not provided', () => {
const options = {
invitee: {
emailAddress: '[email protected]',
},
alertIfActive: false,
};

assert.deepEqual(MembersUtil.getAddMemberBody(options), {
invitees: [
{
address: '[email protected]',
},
],
alertIfActive: false,
});
});
});
});
});
47 changes: 47 additions & 0 deletions packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});


})
})

0 comments on commit 71f56bf

Please sign in to comment.