You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

participants.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import fs from 'fs';
  2. import jwt from 'jsonwebtoken';
  3. import process from 'node:process';
  4. import { v4 as uuidv4 } from 'uuid';
  5. import { P1_DISPLAY_NAME, P2_DISPLAY_NAME, P3_DISPLAY_NAME, P4_DISPLAY_NAME, Participant } from './Participant';
  6. import { IContext, IJoinOptions } from './types';
  7. const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
  8. /**
  9. * Ensure that there is on participant.
  10. *
  11. * @param {IContext} ctx - The context.
  12. * @param {IJoinOptions} options - The options to use when joining the participant.
  13. * @returns {Promise<void>}
  14. */
  15. export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
  16. await joinTheModeratorAsP1(ctx, options);
  17. }
  18. /**
  19. * Ensure that there are three participants.
  20. *
  21. * @param {Object} ctx - The context.
  22. * @param {IJoinOptions} options - The options to use when joining the participant.
  23. * @returns {Promise<void>}
  24. */
  25. export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  26. await joinTheModeratorAsP1(ctx, options);
  27. // these need to be all, so we get the error when one fails
  28. await Promise.all([
  29. _joinParticipant('participant2', ctx.p2, p => {
  30. ctx.p2 = p;
  31. }, {
  32. displayName: P2_DISPLAY_NAME,
  33. ...options
  34. }),
  35. _joinParticipant('participant3', ctx.p3, p => {
  36. ctx.p3 = p;
  37. }, {
  38. displayName: P3_DISPLAY_NAME,
  39. ...options
  40. })
  41. ]);
  42. const { skipInMeetingChecks } = options;
  43. await Promise.all([
  44. skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(2),
  45. skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(2)
  46. ]);
  47. }
  48. /**
  49. * Creates the second participant instance or prepares one for re-joining.
  50. *
  51. * @param {Object} ctx - The context.
  52. * @param {IJoinOptions} options - The options to use when joining the participant.
  53. * @returns {Promise<void>}
  54. */
  55. export function joinSecondParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  56. return _joinParticipant('participant2', ctx.p2, p => {
  57. ctx.p2 = p;
  58. }, {
  59. displayName: P2_DISPLAY_NAME,
  60. ...options
  61. });
  62. }
  63. /**
  64. * Creates the third participant instance or prepares one for re-joining.
  65. *
  66. * @param {Object} ctx - The context.
  67. * @param {IJoinOptions} options - The options to use when joining the participant.
  68. * @returns {Promise<void>}
  69. */
  70. export function joinThirdParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  71. return _joinParticipant('participant3', ctx.p3, p => {
  72. ctx.p3 = p;
  73. }, {
  74. displayName: P3_DISPLAY_NAME,
  75. ...options
  76. });
  77. }
  78. /**
  79. * Ensure that there are four participants.
  80. *
  81. * @param {Object} ctx - The context.
  82. * @param {IJoinOptions} options - The options to use when joining the participant.
  83. * @returns {Promise<void>}
  84. */
  85. export async function ensureFourParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  86. await joinTheModeratorAsP1(ctx, options);
  87. // these need to be all, so we get the error when one fails
  88. await Promise.all([
  89. _joinParticipant('participant2', ctx.p2, p => {
  90. ctx.p2 = p;
  91. }, {
  92. displayName: P2_DISPLAY_NAME,
  93. ...options
  94. }),
  95. _joinParticipant('participant3', ctx.p3, p => {
  96. ctx.p3 = p;
  97. }, {
  98. displayName: P3_DISPLAY_NAME,
  99. ...options
  100. }),
  101. _joinParticipant('participant4', ctx.p4, p => {
  102. ctx.p4 = p;
  103. }, {
  104. displayName: P4_DISPLAY_NAME,
  105. ...options
  106. })
  107. ]);
  108. const { skipInMeetingChecks } = options;
  109. await Promise.all([
  110. skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(3),
  111. skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3),
  112. skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3)
  113. ]);
  114. }
  115. /**
  116. * Ensure that the first participant is moderator.
  117. *
  118. * @param {Object} ctx - The context.
  119. * @param {IJoinOptions} options - The options to join.
  120. * @returns {Promise<void>}
  121. */
  122. async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
  123. const p1DisplayName = P1_DISPLAY_NAME;
  124. let token;
  125. // if it is jaas create the first one to be moderator and second not moderator
  126. if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
  127. token = getModeratorToken(p1DisplayName);
  128. }
  129. // make sure the first participant is moderator, if supported by deployment
  130. await _joinParticipant('participant1', ctx.p1, p => {
  131. ctx.p1 = p;
  132. }, {
  133. displayName: p1DisplayName,
  134. ...options,
  135. skipInMeetingChecks: true
  136. }, token);
  137. }
  138. /**
  139. * Ensure that there are two participants.
  140. *
  141. * @param {Object} ctx - The context.
  142. * @param {IJoinOptions} options - The options to join.
  143. */
  144. export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  145. await joinTheModeratorAsP1(ctx, options);
  146. const { skipInMeetingChecks } = options;
  147. await Promise.all([
  148. _joinParticipant('participant2', ctx.p2, p => {
  149. ctx.p2 = p;
  150. }, {
  151. displayName: P2_DISPLAY_NAME,
  152. ...options
  153. }),
  154. skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
  155. skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
  156. ]);
  157. }
  158. /**
  159. * Creates a participant instance or prepares one for re-joining.
  160. * @param name - The name of the participant.
  161. * @param p - The participant instance to prepare or undefined if new one is needed.
  162. * @param setter - The setter to use for setting the new participant instance into the context if needed.
  163. * @param {boolean} options - Join options.
  164. * @param {string?} jwtToken - The token to use if any.
  165. */
  166. async function _joinParticipant( // eslint-disable-line max-params
  167. name: string,
  168. p: Participant,
  169. setter: (p: Participant) => void,
  170. options: IJoinOptions = {},
  171. jwtToken?: string) {
  172. if (p) {
  173. if (ctx.iframeAPI) {
  174. await p.switchInPage();
  175. }
  176. if (await p.isInMuc()) {
  177. return;
  178. }
  179. if (ctx.iframeAPI) {
  180. // when loading url make sure we are on the top page context or strange errors may occur
  181. await p.switchToAPI();
  182. }
  183. // Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
  184. await p.driver.url('/base.html');
  185. // we want the participant instance re-recreated so we clear any kept state, like endpoint ID
  186. }
  187. const newParticipant = new Participant(name, jwtToken);
  188. // set the new participant instance, pass it to setter
  189. setter(newParticipant);
  190. await newParticipant.joinConference(ctx, options);
  191. }
  192. /**
  193. * Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
  194. * conference participants sees a specific mute state for the former.
  195. *
  196. * @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
  197. * mute state is to be toggled.
  198. * @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
  199. * the mute state of {@code testee}.
  200. * @returns {Promise<void>}
  201. */
  202. export async function muteAudioAndCheck(testee: Participant, observer: Participant): Promise<void> {
  203. await testee.getToolbar().clickAudioMuteButton();
  204. await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
  205. await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
  206. }
  207. /**
  208. * Unmute audio, checks if the local UI has been updated accordingly and then does the verification from
  209. * the other observer participant perspective.
  210. * @param testee
  211. * @param observer
  212. */
  213. export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
  214. await testee.getToolbar().clickAudioUnmuteButton();
  215. await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
  216. await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
  217. }
  218. /**
  219. * Starts the video on testee and check on observer.
  220. * @param testee
  221. * @param observer
  222. */
  223. export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
  224. await testee.getToolbar().clickVideoUnmuteButton();
  225. await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
  226. await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
  227. }
  228. /**
  229. * Get a JWT token for a moderator.
  230. */
  231. function getModeratorToken(displayName: string) {
  232. const keyid = process.env.JWT_KID;
  233. const headers = {
  234. algorithm: 'RS256',
  235. noTimestamp: true,
  236. expiresIn: '24h',
  237. keyid
  238. };
  239. if (!keyid) {
  240. console.error('JWT_KID is not set');
  241. return;
  242. }
  243. const key = fs.readFileSync(ctx.jwtPrivateKeyPath);
  244. const payload = {
  245. 'aud': 'jitsi',
  246. 'iss': 'chat',
  247. 'sub': keyid.substring(0, keyid.indexOf('/')),
  248. 'context': {
  249. 'user': {
  250. 'name': displayName,
  251. 'id': uuidv4(),
  252. 'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
  253. 'email': 'john.doe@jitsi.org'
  254. }
  255. },
  256. 'room': '*'
  257. };
  258. // @ts-ignore
  259. payload.context.user.moderator = true;
  260. // @ts-ignore
  261. return jwt.sign(payload, key, headers);
  262. }
  263. /**
  264. * Parse a JID string.
  265. * @param str the string to parse.
  266. */
  267. export function parseJid(str: string): {
  268. domain: string;
  269. node: string;
  270. resource: string | undefined;
  271. } {
  272. const parts = str.split('@');
  273. const domainParts = parts[1].split('/');
  274. return {
  275. node: parts[0],
  276. domain: domainParts[0],
  277. resource: domainParts.length > 0 ? domainParts[1] : undefined
  278. };
  279. }
  280. /**
  281. * Check the subject of the participant.
  282. * @param participant
  283. * @param subject
  284. */
  285. export async function checkSubject(participant: Participant, subject: string) {
  286. const localTile = participant.driver.$(SUBJECT_XPATH);
  287. await localTile.waitForExist();
  288. await localTile.moveTo();
  289. const txt = await localTile.getText();
  290. expect(txt.startsWith(subject)).toBe(true);
  291. }
  292. /**
  293. * Check if a screensharing tile is displayed on the observer.
  294. * Expects there was already a video by this participant and screen sharing will be the second video `-v1`.
  295. */
  296. export async function checkForScreensharingTile(sharer: Participant, observer: Participant, reverse = false) {
  297. await observer.driver.$(`//span[@id='participant_${await sharer.getEndpointId()}-v1']`).waitForDisplayed({
  298. timeout: 3_000,
  299. reverse
  300. });
  301. }