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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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 { 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. ctx.p1 = new Participant('participant1');
  17. await ctx.p1.joinConference(ctx, {
  18. ...options,
  19. skipInMeetingChecks: true
  20. });
  21. }
  22. /**
  23. * Ensure that there are three participants.
  24. *
  25. * @param {Object} ctx - The context.
  26. * @returns {Promise<void>}
  27. */
  28. export async function ensureThreeParticipants(ctx: IContext): Promise<void> {
  29. await joinTheModeratorAsP1(ctx);
  30. const p2 = new Participant('participant2');
  31. const p3 = new Participant('participant3');
  32. ctx.p2 = p2;
  33. ctx.p3 = p3;
  34. // these need to be all, so we get the error when one fails
  35. await Promise.all([
  36. p2.joinConference(ctx),
  37. p3.joinConference(ctx)
  38. ]);
  39. await Promise.all([
  40. p2.waitForRemoteStreams(2),
  41. p3.waitForRemoteStreams(2)
  42. ]);
  43. }
  44. /**
  45. * Ensure that the first participant is moderator.
  46. *
  47. * @param {Object} ctx - The context.
  48. * @param {IJoinOptions} options - The options to join.
  49. * @returns {Promise<void>}
  50. */
  51. async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
  52. const p1DisplayName = 'participant1';
  53. let token;
  54. // if it is jaas create the first one to be moderator and second not moderator
  55. if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
  56. token = getModeratorToken(p1DisplayName);
  57. }
  58. // make sure the first participant is moderator, if supported by deployment
  59. await _joinParticipant(p1DisplayName, ctx.p1, p => {
  60. ctx.p1 = p;
  61. }, {
  62. ...options,
  63. skipInMeetingChecks: true
  64. }, token);
  65. }
  66. /**
  67. * Ensure that there are two participants.
  68. *
  69. * @param {Object} ctx - The context.
  70. * @param {IJoinOptions} options - The options to join.
  71. */
  72. export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
  73. await joinTheModeratorAsP1(ctx, options);
  74. const { skipInMeetingChecks } = options;
  75. await Promise.all([
  76. _joinParticipant('participant2', ctx.p2, p => {
  77. ctx.p2 = p;
  78. }, options),
  79. skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
  80. skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
  81. ]);
  82. }
  83. /**
  84. * Creates a participant instance or prepares one for re-joining.
  85. * @param name - The name of the participant.
  86. * @param p - The participant instance to prepare or undefined if new one is needed.
  87. * @param setter - The setter to use for setting the new participant instance into the context if needed.
  88. * @param {boolean} options - Join options.
  89. * @param {string?} jwtToken - The token to use if any.
  90. */
  91. async function _joinParticipant( // eslint-disable-line max-params
  92. name: string,
  93. p: Participant,
  94. setter: (p: Participant) => void,
  95. options: IJoinOptions = {},
  96. jwtToken?: string) {
  97. if (p) {
  98. if (ctx.iframeAPI) {
  99. await p.switchInPage();
  100. }
  101. if (await p.isInMuc()) {
  102. return;
  103. }
  104. if (ctx.iframeAPI) {
  105. // when loading url make sure we are on the top page context or strange errors may occur
  106. await p.switchToAPI();
  107. }
  108. // Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
  109. await p.driver.url('/base.html');
  110. // we want the participant instance re-recreated so we clear any kept state, like endpoint ID
  111. }
  112. const newParticipant = new Participant(name, jwtToken);
  113. // set the new participant instance, pass it to setter
  114. setter(newParticipant);
  115. await newParticipant.joinConference(ctx, options);
  116. }
  117. /**
  118. * Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
  119. * conference participants sees a specific mute state for the former.
  120. *
  121. * @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
  122. * mute state is to be toggled.
  123. * @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
  124. * the mute state of {@code testee}.
  125. * @returns {Promise<void>}
  126. */
  127. export async function muteAudioAndCheck(testee: Participant, observer: Participant): Promise<void> {
  128. await testee.getToolbar().clickAudioMuteButton();
  129. await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
  130. await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
  131. }
  132. /**
  133. * Unmute audio, checks if the local UI has been updated accordingly and then does the verification from
  134. * the other observer participant perspective.
  135. * @param testee
  136. * @param observer
  137. */
  138. export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
  139. await testee.getToolbar().clickAudioUnmuteButton();
  140. await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
  141. await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
  142. }
  143. /**
  144. * Starts the video on testee and check on observer.
  145. * @param testee
  146. * @param observer
  147. */
  148. export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
  149. await testee.getToolbar().clickVideoUnmuteButton();
  150. await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
  151. await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
  152. }
  153. /**
  154. * Get a JWT token for a moderator.
  155. */
  156. function getModeratorToken(displayName: string) {
  157. const keyid = process.env.JWT_KID;
  158. const headers = {
  159. algorithm: 'RS256',
  160. noTimestamp: true,
  161. expiresIn: '24h',
  162. keyid
  163. };
  164. if (!keyid) {
  165. console.error('JWT_KID is not set');
  166. return;
  167. }
  168. const key = fs.readFileSync(ctx.jwtPrivateKeyPath);
  169. const payload = {
  170. 'aud': 'jitsi',
  171. 'iss': 'chat',
  172. 'sub': keyid.substring(0, keyid.indexOf('/')),
  173. 'context': {
  174. 'user': {
  175. 'name': displayName,
  176. 'id': uuidv4(),
  177. 'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
  178. 'email': 'john.doe@jitsi.org'
  179. }
  180. },
  181. 'room': '*'
  182. };
  183. // @ts-ignore
  184. payload.context.user.moderator = true;
  185. // @ts-ignore
  186. return jwt.sign(payload, key, headers);
  187. }
  188. /**
  189. * Parse a JID string.
  190. * @param str the string to parse.
  191. */
  192. export function parseJid(str: string): {
  193. domain: string;
  194. node: string;
  195. resource: string | undefined;
  196. } {
  197. const parts = str.split('@');
  198. const domainParts = parts[1].split('/');
  199. return {
  200. node: parts[0],
  201. domain: domainParts[0],
  202. resource: domainParts.length > 0 ? domainParts[1] : undefined
  203. };
  204. }
  205. /**
  206. * Check the subject of the participant.
  207. * @param participant
  208. * @param subject
  209. */
  210. export async function checkSubject(participant: Participant, subject: string) {
  211. const localTile = participant.driver.$(SUBJECT_XPATH);
  212. await localTile.waitForExist();
  213. await localTile.moveTo();
  214. const txt = await localTile.getText();
  215. expect(txt.startsWith(subject)).toBe(true);
  216. }