Merge remote-tracking branch 'upstream/v2.5.x-release' into merge-2526-dec14

This commit is contained in:
Ramón Souza 2022-12-14 13:23:11 -03:00
commit 064b554dfd
21 changed files with 563 additions and 105 deletions

View File

@ -47,6 +47,7 @@ import org.bigbluebutton.api.domain.BreakoutRoomsParams;
import org.bigbluebutton.api.domain.LockSettingsParams;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.Group;
import org.bigbluebutton.api.service.ServiceUtils;
import org.bigbluebutton.api.util.ParamsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -685,7 +686,7 @@ public class ParamsProcessorUtil {
String parentMeetingId = "";
if (isBreakout) {
internalMeetingId = params.get(ApiParams.MEETING_ID);
parentMeetingId = params.get(ApiParams.PARENT_MEETING_ID);
parentMeetingId = ServiceUtils.findMeetingFromMeetingID(params.get(ApiParams.PARENT_MEETING_ID)).getInternalId();
// We rebuild the the external meeting using the has of the parent
// meeting, the shared timestamp and the sequence number
String timeStamp = StringUtils.substringAfter(internalMeetingId, "-");

View File

@ -27,74 +27,52 @@ import Users, { CurrentUser } from '/imports/api/users';
import { Slides, SlidePositions } from '/imports/api/slides';
// Custom Publishers
export const localCurrentPollSync = new AbstractCollection(CurrentPoll, CurrentPoll);
export const localCurrentUserSync = new AbstractCollection(CurrentUser, CurrentUser);
export const localSlidesSync = new AbstractCollection(Slides, Slides);
export const localSlidePositionsSync = new AbstractCollection(SlidePositions, SlidePositions);
export const localPollsSync = new AbstractCollection(Polls, Polls);
export const localPresentationsSync = new AbstractCollection(Presentations, Presentations);
export const localPresentationPodsSync = new AbstractCollection(PresentationPods, PresentationPods);
export const localPresentationUploadTokenSync = new AbstractCollection(PresentationUploadToken, PresentationUploadToken);
export const localScreenshareSync = new AbstractCollection(Screenshare, Screenshare);
export const localUserInfosSync = new AbstractCollection(UserInfos, UserInfos);
export const localUsersPersistentDataSync = new AbstractCollection(UsersPersistentData, UsersPersistentData);
export const localUserSettingsSync = new AbstractCollection(UserSettings, UserSettings);
export const localVideoStreamsSync = new AbstractCollection(VideoStreams, VideoStreams);
export const localVoiceUsersSync = new AbstractCollection(VoiceUsers, VoiceUsers);
export const localWhiteboardMultiUserSync = new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser);
export const localGroupChatSync = new AbstractCollection(GroupChat, GroupChat);
export const localConnectionStatusSync = new AbstractCollection(ConnectionStatus, ConnectionStatus);
export const localCaptionsSync = new AbstractCollection(Captions, Captions);
export const localPadsSync = new AbstractCollection(Pads, Pads);
export const localPadsSessionsSync = new AbstractCollection(PadsSessions, PadsSessions);
export const localPadsUpdatesSync = new AbstractCollection(PadsUpdates, PadsUpdates);
export const localAuthTokenValidationSync = new AbstractCollection(AuthTokenValidation, AuthTokenValidation);
export const localAnnotationsSync = new AbstractCollection(Annotations, Annotations);
export const localRecordMeetingsSync = new AbstractCollection(RecordMeetings, RecordMeetings);
export const localExternalVideoMeetingsSync = new AbstractCollection(ExternalVideoMeetings, ExternalVideoMeetings);
export const localMeetingTimeRemainingSync = new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining);
export const localUsersTypingSync = new AbstractCollection(UsersTyping, UsersTyping);
export const localBreakoutsSync = new AbstractCollection(Breakouts, Breakouts);
export const localBreakoutsHistorySync = new AbstractCollection(BreakoutsHistory, BreakoutsHistory);
export const localGuestUsersSync = new AbstractCollection(guestUsers, guestUsers);
export const localMeetingsSync = new AbstractCollection(Meetings, Meetings);
export const localUsersSync = new AbstractCollection(Users, Users);
export const localNotificationsSync = new AbstractCollection(Notifications, Notifications);
export const localCollectionRegistry = {
localCurrentPollSync: new AbstractCollection(CurrentPoll, CurrentPoll),
localCurrentUserSync: new AbstractCollection(CurrentUser, CurrentUser),
localSlidesSync: new AbstractCollection(Slides, Slides),
localSlidePositionsSync: new AbstractCollection(SlidePositions, SlidePositions),
localPollsSync: new AbstractCollection(Polls, Polls),
localPresentationsSync: new AbstractCollection(Presentations, Presentations),
localPresentationPodsSync: new AbstractCollection(PresentationPods, PresentationPods),
localPresentationUploadTokenSync: new AbstractCollection(
PresentationUploadToken,
PresentationUploadToken,
),
localScreenshareSync: new AbstractCollection(Screenshare, Screenshare),
localUserInfosSync: new AbstractCollection(UserInfos, UserInfos),
localUsersPersistentDataSync: new AbstractCollection(UsersPersistentData, UsersPersistentData),
localUserSettingsSync: new AbstractCollection(UserSettings, UserSettings),
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
localGroupChatSync: new AbstractCollection(GroupChat, GroupChat),
localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
localCaptionsSync: new AbstractCollection(Captions, Captions),
localPadsSync: new AbstractCollection(Pads, Pads),
localPadsSessionsSync: new AbstractCollection(PadsSessions, PadsSessions),
localPadsUpdatesSync: new AbstractCollection(PadsUpdates, PadsUpdates),
localAuthTokenValidationSync: new AbstractCollection(AuthTokenValidation, AuthTokenValidation),
localAnnotationsSync: new AbstractCollection(Annotations, Annotations),
localRecordMeetingsSync: new AbstractCollection(RecordMeetings, RecordMeetings),
localExternalVideoMeetingsSync: new AbstractCollection(
ExternalVideoMeetings,
ExternalVideoMeetings,
),
localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
localUsersTypingSync: new AbstractCollection(UsersTyping, UsersTyping),
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
localGuestUsersSync: new AbstractCollection(guestUsers, guestUsers),
localMeetingsSync: new AbstractCollection(Meetings, Meetings),
localUsersSync: new AbstractCollection(Users, Users),
localNotificationsSync: new AbstractCollection(Notifications, Notifications),
};
const collectionMirrorInitializer = () => {
localCurrentPollSync.setupListeners();
localCurrentUserSync.setupListeners();
localSlidesSync.setupListeners();
localSlidePositionsSync.setupListeners();
localPollsSync.setupListeners();
localPresentationsSync.setupListeners();
localPresentationPodsSync.setupListeners();
localPresentationUploadTokenSync.setupListeners();
localScreenshareSync.setupListeners();
localUserInfosSync.setupListeners();
localUsersPersistentDataSync.setupListeners();
localUserSettingsSync.setupListeners();
localVideoStreamsSync.setupListeners();
localVoiceUsersSync.setupListeners();
localWhiteboardMultiUserSync.setupListeners();
localGroupChatSync.setupListeners();
localConnectionStatusSync.setupListeners();
localCaptionsSync.setupListeners();
localPadsSync.setupListeners();
localPadsSessionsSync.setupListeners();
localPadsUpdatesSync.setupListeners();
localAuthTokenValidationSync.setupListeners();
localAnnotationsSync.setupListeners();
localRecordMeetingsSync.setupListeners();
localExternalVideoMeetingsSync.setupListeners();
localMeetingTimeRemainingSync.setupListeners();
localUsersTypingSync.setupListeners();
localBreakoutsSync.setupListeners();
localBreakoutsHistorySync.setupListeners();
localGuestUsersSync.setupListeners();
localMeetingsSync.setupListeners();
localUsersSync.setupListeners();
localNotificationsSync.setupListeners();
Object.values(localCollectionRegistry).forEach((localCollection) => {
localCollection.setupListeners();
});
};
export default collectionMirrorInitializer;

View File

@ -228,6 +228,7 @@ class App extends Component {
layoutContextDispatch,
numCameras,
presentationIsOpen,
ignorePollNotifications,
} = this.props;
this.renderDarkMode();

View File

@ -311,5 +311,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
isModalOpen: !!getModal(),
ignorePollNotifications: Session.get('ignorePollNotifications'),
};
})(AppContainer)));

View File

@ -10,6 +10,7 @@ import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
import Service from '../service';
import Styled from './styles';
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
import { Session } from 'meteor/session';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
@ -193,9 +194,11 @@ const ChatAlert = (props) => {
const mappedMessage = Service.mapGroupMessage(timeWindow);
let content = null;
let isPollResult = false;
if (mappedMessage) {
if (mappedMessage.id.includes(POLL_RESULT_KEY)) {
content = createPollMessage();
isPollResult = true;
} else {
content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5));
}
@ -218,10 +221,22 @@ const ChatAlert = (props) => {
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
}
onOpen={
() => setUnreadMessages(newUnreadMessages)
() => {
if (isPollResult) {
Session.set('ignorePollNotifications', true);
}
setUnreadMessages(newUnreadMessages);
}
}
onClose={
() => setUnreadMessages(newUnreadMessages)
() => {
if (isPollResult) {
Session.set('ignorePollNotifications', false);
}
setUnreadMessages(newUnreadMessages);
}
}
alertDuration={timeWindow.durationDiff}
layoutContextDispatch={layoutContextDispatch}

View File

@ -8,11 +8,7 @@ import Users from '/imports/api/users';
import AnnotationsTextService from '/imports/ui/components/whiteboard/annotations/text/service';
import { Annotations as AnnotationsLocal } from '/imports/ui/components/whiteboard/service';
import {
localBreakoutsSync,
localBreakoutsHistorySync,
localGuestUsersSync,
localMeetingsSync,
localUsersSync,
localCollectionRegistry,
} from '/client/collection-mirror-initializer';
import SubscriptionRegistry, { subscriptionReactivity } from '../../services/subscription-registry/subscriptionRegistry';
import { isChatEnabled } from '/imports/ui/services/features';
@ -30,6 +26,13 @@ const SUBSCRIPTIONS = [
'pads', 'pads-sessions', 'pads-updates', 'notifications', 'audio-captions',
'layout-meetings',
];
const {
localBreakoutsSync,
localBreakoutsHistorySync,
localGuestUsersSync,
localMeetingsSync,
localUsersSync,
} = localCollectionRegistry;
const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped';
const EVENT_NAME_SUBSCRIPTION_READY = 'bbb-group-chat-messages-subscriptions-ready';
@ -165,6 +168,10 @@ export default withTracker(() => {
},
...subscriptionErrorHandler,
});
Object.values(localCollectionRegistry).forEach(
(localCollection) => localCollection.checkForStaleData(),
);
}
return {

View File

@ -25,13 +25,17 @@ const intlMessages = defineMessages({
let annotationsStreamListener = null;
export function addAnnotationToDiscardedList(annotation) {
if (!discardedList.includes(annotation)) discardedList.push(annotation);
}
const clearPreview = (annotation) => {
UnsentAnnotations.remove({ id: annotation });
};
const clearFakeAnnotations = () => {
UnsentAnnotations.remove({});
Annotations.remove({ id: /-fake/g, annotationType: { $ne: 'text' } });
Annotations.remove({ id: /-fake/g });
}
function handleAddedAnnotation({

View File

@ -238,6 +238,8 @@ export default class WhiteboardOverlay extends Component {
setTextShapeActiveId,
contextMenuHandler,
clearPreview,
addAnnotationToDiscardedList,
undoAnnotation,
updateCursor,
} = this.props;
@ -255,6 +257,8 @@ export default class WhiteboardOverlay extends Component {
setTextShapeActiveId,
contextMenuHandler,
clearPreview,
addAnnotationToDiscardedList,
undoAnnotation,
};
return (
@ -319,4 +323,6 @@ WhiteboardOverlay.propTypes = {
setTextShapeActiveId: PropTypes.func.isRequired,
// Defines a handler to publish cursor position to the server
updateCursor: PropTypes.func.isRequired,
addAnnotationToDiscardedList: PropTypes.func.isRequired,
undoAnnotation: PropTypes.func.isRequired,
};

View File

@ -2,6 +2,7 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import WhiteboardOverlayService from './service';
import WhiteboardToolbarService from '../whiteboard-toolbar/service';
import WhiteboardOverlay from './component';
const WhiteboardOverlayContainer = (props) => {
@ -15,10 +16,12 @@ const WhiteboardOverlayContainer = (props) => {
};
export default withTracker(() => ({
undoAnnotation: WhiteboardToolbarService.undoAnnotation,
clearPreview: WhiteboardOverlayService.clearPreview,
contextMenuHandler: WhiteboardOverlayService.contextMenuHandler,
sendAnnotation: WhiteboardOverlayService.sendAnnotation,
sendLiveSyncPreviewAnnotation: WhiteboardOverlayService.sendLiveSyncPreviewAnnotation,
addAnnotationToDiscardedList: WhiteboardOverlayService.addAnnotationToDiscardedList,
setTextShapeActiveId: WhiteboardOverlayService.setTextShapeActiveId,
resetTextShapeSession: WhiteboardOverlayService.resetTextShapeSession,
drawSettings: WhiteboardOverlayService.getWhiteboardToolbarValues(),

View File

@ -1,6 +1,6 @@
import Storage from '/imports/ui/services/storage/session';
import Auth from '/imports/ui/services/auth';
import { sendAnnotation, sendLiveSyncPreviewAnnotation, clearPreview } from '/imports/ui/components/whiteboard/service';
import { sendAnnotation, sendLiveSyncPreviewAnnotation, clearPreview, addAnnotationToDiscardedList } from '/imports/ui/components/whiteboard/service';
import { publishCursorUpdate } from '/imports/ui/components/cursor/service';
const DRAW_SETTINGS = 'drawSettings';
@ -55,6 +55,7 @@ const updateCursor = (payload) => {
};
export default {
addAnnotationToDiscardedList,
sendAnnotation,
sendLiveSyncPreviewAnnotation,
getWhiteboardToolbarValues,

View File

@ -218,11 +218,12 @@ export default class TextDrawListener extends Component {
}
// second case is when a user finished writing the text and publishes the final result
} else if (isRightClick) {
this.discardAnnotation();
} else {
// publishing the final shape and resetting the state
this.sendLastMessage();
if (isRightClick) {
this.discardAnnotation();
}
}
}
@ -485,15 +486,18 @@ export default class TextDrawListener extends Component {
discardAnnotation() {
const {
actions,
whiteboardId,
} = this.props;
const {
getCurrentShapeId,
clearPreview,
undoAnnotation,
addAnnotationToDiscardedList,
} = actions;
this.resetState();
clearPreview(getCurrentShapeId());
undoAnnotation(whiteboardId);
addAnnotationToDiscardedList(getCurrentShapeId());
}
render() {
@ -598,5 +602,7 @@ TextDrawListener.propTypes = {
resetTextShapeSession: PropTypes.func.isRequired,
// Defines a function that sets a session value for the current active text shape
setTextShapeActiveId: PropTypes.func.isRequired,
undoAnnotation: PropTypes.func.isRequired,
addAnnotationToDiscardedList: PropTypes.func.isRequired,
}).isRequired,
};

View File

@ -2,7 +2,7 @@ import SubscriptionRegistry from '/imports/ui/services/subscription-registry/sub
import CollectionEventsBroker from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
/*
This class connects a local collection with the LiveDataEventBroker, propagating the changes of a server-side published cursor to a local collection.
This class connects a local collection with the LiveDataEventBroker, propagating the changes of a server-side published cursor to a local collection.
It also guarantee that in case of a reconnection or a re-subscription, the data is only removed after subscription is ready, avoiding the situation of missing data during re-synchronization.
*/
@ -15,6 +15,7 @@ class LocalCollectionSynchronizer {
this.lastSubscriptionId = '';
this.options = options;
this.ignoreDeletes = false;
this.checkForStaleData = this.checkForStaleData.bind(this);
}
/*
@ -31,26 +32,12 @@ class LocalCollectionSynchronizer {
const self = this;
const addedCallback = function (item) {
const subscription = SubscriptionRegistry
.getSubscription(self.serverCollection._name);
if (item.id === 'publication-stop-marker' && item.stopped) {
self.ignoreDeletes = true;
return;
}
// If the subscriptionId changes means the subscriptions was redone
// or theres more than one subscription per collection
if (subscription && (self.lastSubscriptionId !== subscription.subscriptionId)) {
const wasEmpty = self.lastSubscriptionId === '';
self.lastSubscriptionId = subscription.subscriptionId;
if (!wasEmpty) {
self.callWhenSubscriptionReady(() => {
self.ignoreDeletes = false;
Session.set('globalIgnoreDeletes', false);
self.removeOldSubscriptionData();
});
}
}
self.checkForStaleData();
const selector = { referenceId: item.referenceId };
const itemExistInCollection = self.localCollection.findOne(selector);
@ -117,6 +104,24 @@ class LocalCollectionSynchronizer {
return tempPromise;
}
checkForStaleData() {
const subscription = SubscriptionRegistry.getSubscription(this.serverCollection._name);
// If the subscriptionId changes means the subscriptions was redone
// or theres more than one subscription per collection
if (subscription && (this.lastSubscriptionId !== subscription.subscriptionId)) {
const wasEmpty = this.lastSubscriptionId === '';
this.lastSubscriptionId = subscription.subscriptionId;
if (!wasEmpty) {
this.callWhenSubscriptionReady(() => {
this.ignoreDeletes = false;
Session.set('globalIgnoreDeletes', false);
this.removeOldSubscriptionData();
});
}
}
}
/*
This method removes data from previous subscriptions after the current one is ready.
*/

View File

@ -16,7 +16,7 @@ const SETTINGS = [
];
const CHANGED_SETTINGS = 'changed_settings';
const DEFAULT_SETTINGS = 'dafault_settings';
const DEFAULT_SETTINGS = 'default_settings';
class Settings {
constructor(defaultValues = {}) {

View File

@ -92,3 +92,18 @@ sudo systemctl reload nginx
Meteor messages for Big Blue Button sessions will now be recorded for later review.
It doesn't seem necessary to relay cookies, but that could be done by giving a `--ws-relay-header=Cookie` argument to `websockify`.
You can print the browser console log to standard output by setting the environment variable `CONSOLE`:
```
$ CONSOLE= npm test chat -- --project=firefox
```
`CONSOLE` can be blank (as in the example), or can be a comma-separated list of the following options:
| Option | Meaning |
| ------ | ------- |
| color | (or "colour") colorize the output |
| label | label each line with the BigBlueButton user |
| norefs | remove JavaScript reference URLs |
| nots | remove timestamps |
| nocl | remove "clientLogger:" strings |

View File

@ -0,0 +1,125 @@
// const util = require('node:util');
const { expect } = require("@playwright/test");
const Page = require('../core/page');
const parameters = require('../core/parameters');
const { apiCall, createMeeting } = require('../core/helpers');
const e = require('../core/elements');
function getMeetings() {
return apiCall('getMeetings', {});
}
function getMeetingInfo(meetingID) {
return apiCall('getMeetingInfo', {meetingID: meetingID});
}
class API {
constructor(browser, context, page) {
this.modPage = new Page(browser, page);
this.browser = browser;
this.context = context;
this.userPages = [];
}
async getNewPageTab() {
return this.browser.newPage();
}
async testGetMeetings() {
const meetingId = await createMeeting(parameters);
const modPage = new Page(this.browser, await this.getNewPageTab());
const userPage = new Page(this.browser, await this.getNewPageTab());
await Promise.all([
modPage.init(true, false, { meetingId, fullName: 'Moderator' }),
userPage.init(false, false, { meetingId, fullName: 'Attendee' }),
]);
await Promise.all([
modPage.joinMicrophone(),
userPage.joinMicrophone()
]);
/* hasJoinedVoice: ['true'] is not part of these expectedUser patterns because it isn't consistently true
* in the API's returned data structures. Is there something we can await on the browser page that
* should ensure that the API will report hasJoinedVoice?
*/
const expectedUsers = [expect.objectContaining({fullName: ['Moderator'],
role: ['MODERATOR'],
isPresenter: ['true'],
}),
expect.objectContaining({fullName: ['Attendee'],
role: ['VIEWER'],
isPresenter: ['false'],
})
];
const expectedMeeting = {meetingName : [meetingId],
running : ['true'],
participantCount : ['2'],
moderatorCount : ['1'],
isBreakout: ['false'],
attendees: [{ attendee: expect.arrayContaining(expectedUsers) }]
};
/* check that this meeting is in the server's list of all meetings */
const response = await getMeetings();
expect(response.response.returncode).toEqual(['SUCCESS']);
expect(response.response.meetings[0].meeting).toContainEqual(expect.objectContaining(expectedMeeting));
await modPage.page.close();
await userPage.page.close();
}
async testGetMeetingInfo() {
const meetingId = await createMeeting(parameters);
const modPage = new Page(this.browser, await this.getNewPageTab());
const userPage = new Page(this.browser, await this.getNewPageTab());
await Promise.all([
modPage.init(true, false, { meetingId, fullName: 'Moderator' }),
userPage.init(false, false, { meetingId, fullName: 'Attendee' }),
]);
await Promise.all([
modPage.joinMicrophone(),
userPage.joinMicrophone()
]);
/* hasJoinedVoice: ['true'] is not part of these expectedUser patterns because it isn't consistently true
* in the API's returned data structures. Is there something we can await on the browser page that
* should ensure that the API will report hasJoinedVoice?
*/
const expectedUsers = [expect.objectContaining({fullName: ['Moderator'],
role: ['MODERATOR'],
isPresenter: ['true'],
}),
expect.objectContaining({fullName: ['Attendee'],
role: ['VIEWER'],
isPresenter: ['false'],
})
];
const expectedMeeting = {meetingName : [meetingId],
running : ['true'],
participantCount : ['2'],
moderatorCount : ['1'],
isBreakout: ['false'],
attendees: [{ attendee: expect.arrayContaining(expectedUsers) }]
};
/* check that we can retrieve this meeting by its meetingId */
const response2 = await getMeetingInfo(meetingId);
expect(response2.response.returncode).toEqual(['SUCCESS']);
expect(response2.response).toMatchObject(expectedMeeting);
/* check that we can retrieve this meeting by its internal meeting ID */
const response3 = await getMeetingInfo(response2.response.internalMeetingID[0]);
expect(response3.response.returncode).toEqual(['SUCCESS']);
expect(response3.response).toMatchObject(expectedMeeting);
await modPage.page.close();
await userPage.page.close();
}
}
exports.API = API;

View File

@ -0,0 +1,16 @@
const { test } = require('@playwright/test');
const { API } = require('./api.js');
test.describe.parallel('API', () => {
test('getMeetings', async ({ browser, context, page }) => {
const api = new API(browser, context, page);
await api.testGetMeetings();
});
test('getMeetingInfo', async ({ browser, context, page }) => {
const api = new API(browser, context, page);
await api.testGetMeetingInfo();
});
});

View File

@ -3,6 +3,11 @@ const sha1 = require('sha1');
const path = require('path');
const axios = require('axios');
const { test } = require('@playwright/test');
const xml2js = require('xml2js');
const { expect } = require("@playwright/test");
const parameters = require('./parameters');
const httpPath = path.join(path.dirname(require.resolve('axios')), 'lib/adapters/http');
const http = require(httpPath);
@ -13,8 +18,21 @@ function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
async function createMeeting(params, customParameter, customMeetingId) {
const meetingID = (customMeetingId) ? customMeetingId : `random-${getRandomInt(1000000, 10000000).toString()}`;
function apiCallUrl(name, callParams) {
const query = new URLSearchParams(callParams).toString();
const apicall = `${name}${query}${parameters.secret}`;
const checksum = sha1(apicall);
const url = `${parameters.server}/${name}?${query}&checksum=${checksum}`;
return url;
}
function apiCall(name, callParams) {
const url = apiCallUrl(name, callParams);
return axios.get(url, { adapter: http }).then(response => xml2js.parseStringPromise(response.data));
}
function createMeetingUrl(params, customParameter) {
const meetingID = `random-${getRandomInt(1000000, 10000000).toString()}`;
const mp = params.moderatorPW;
const ap = params.attendeePW;
const query = customParameter !== undefined ? `name=${meetingID}&meetingID=${meetingID}&attendeePW=${ap}&moderatorPW=${mp}`
@ -24,8 +42,20 @@ async function createMeeting(params, customParameter, customMeetingId) {
const apicall = `create${query}${params.secret}`;
const checksum = sha1(apicall);
const url = `${params.server}/create?${query}&checksum=${checksum}`;
await axios.get(url, { adapter: http });
return meetingID;
return url;
}
function createMeetingPromise(params, customParameter) {
const url = createMeetingUrl(params, customParameter);
return axios.get(url, { adapter: http });
}
async function createMeeting(params, customParameter) {
const promise = createMeetingPromise(params, customParameter);
const response = await promise;
expect(response.status).toEqual(200);
const xmlresponse = await xml2js.parseStringPromise(response.data);
return xmlresponse.response.meetingID[0];
}
function getJoinURL(meetingID, params, moderator, customParameter) {
@ -51,6 +81,10 @@ function sleep(time) {
}
exports.getRandomInt = getRandomInt;
exports.apiCallUrl = apiCallUrl;
exports.apiCall = apiCall;
exports.createMeetingUrl = createMeetingUrl;
exports.createMeetingPromise = createMeetingPromise;
exports.createMeeting = createMeeting;
exports.getJoinURL = getJoinURL;
exports.linkIssue = linkIssue;

View File

@ -1,13 +1,86 @@
require('dotenv').config();
const { expect, default: test } = require('@playwright/test');
const { readFileSync } = require('fs');
const { format } = require('node:util');
// This is version 4 of chalk, not version 5, which uses ESM
const chalk = require('chalk');
const parameters = require('./parameters');
const helpers = require('./helpers');
const e = require('./elements');
const { env } = require('node:process');
const { ELEMENT_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME, VIDEO_LOADING_WAIT_TIME } = require('./constants');
const { checkElement, checkElementLengthEqualTo } = require('./util');
const { generateSettingsData, getSettings } = require('./settings');
function formatWithCss(CONSOLE_options, ...args) {
// For Chrome, args[0] is a format string that we will process using
// node.js's util.format, but that function discards css style
// information from "%c" format specifiers. So first loop over the
// format string, replacing every "%c" with "%s" and replacing the
// corresponding css style with an ANSI color sequence.
//
// See https://console.spec.whatwg.org/ sections 2.2.1 and 2.3.4
let split_arg0 = args[0].split("%");
for (let i=1, j=1; i<split_arg0.length; i++, j++) {
if (split_arg0[i].startsWith('c')) {
split_arg0[i] = 's' + split_arg0[i].substr(1);
const styles = args[j].split(';');
args[j] = '';
for (const style of styles) {
const stdStyle = style.trim().toLowerCase();
if (stdStyle.startsWith('color:') && CONSOLE_options.colorize) {
const color = stdStyle.substr(6).trim();
args[j] = chalk.keyword(color)._styler.open;
} else if (stdStyle.startsWith('font-size:') && CONSOLE_options.drop_references) {
// For Chrome, we "drop references" by discarding everything after a font size change
split_arg0.length = i;
args.length = j;
}
}
} else if (split_arg0[i] == "") {
// format is "%%", so don't do special processing for
// split_arg0[i+1], and only increment i, not j
i ++; // NOSONAR
}
}
args[0] = split_arg0.join('%');
return format(...args);
}
async function console_format(msg, CONSOLE_options) {
// see playwright consoleMessage class documentation
const args = await Promise.all(msg.args().map(itm => itm.jsonValue()));
let result = formatWithCss(CONSOLE_options, ...args);
if (CONSOLE_options.drop_references) {
// For Firefox, we "drop references" by discarding a URL at the end of the line
result = result.replace(/https:\/\/\S*$/, '');
}
if (CONSOLE_options.noClientLogger) {
result = result.replace(/clientLogger: /, '');
}
if (CONSOLE_options.drop_timestamps) {
// timestamp formatting is a bit complicated, with four "%s" fields and corresponding arguments,
// so just filter them out (if requested) after all the other formatting is done
result = result.replace(/\[\d\d:\d\d:\d\d:\d\d\d\d\] /, '');
}
if (CONSOLE_options.line_label) {
if (CONSOLE_options.colorize) {
result = chalk.keyword('green')(CONSOLE_options.line_label) + result;
} else {
result = CONSOLE_options.line_label + result;
}
}
return result;
}
class Page {
constructor(browser, page) {
this.browser = browser;
@ -31,7 +104,19 @@ class Page {
if (fullName) this.initParameters.fullName = fullName;
this.username = this.initParameters.fullName;
this.meetingId = (meetingId) ? meetingId : await helpers.createMeeting(parameters, customParameter, customMeetingId);
if (env.CONSOLE !== undefined) {
const CONSOLE_strings = env.CONSOLE.split(',').map(opt => opt.trim().toLowerCase());
const CONSOLE_options = {
colorize: CONSOLE_strings.includes('color') || CONSOLE_strings.includes('colour'),
drop_references: CONSOLE_strings.includes('norefs'),
drop_timestamps: CONSOLE_strings.includes('nots'),
line_label: CONSOLE_strings.includes('label') ? this.username + " " : undefined,
noClientLogger: CONSOLE_strings.includes('nocl') || CONSOLE_strings.includes('noclientlogger'),
};
this.page.on('console', async (msg) => console.log(await console_format(msg, CONSOLE_options)));
}
this.meetingId = (meetingId) ? meetingId : await helpers.createMeeting(parameters, customParameter);
const joinUrl = helpers.getJoinURL(this.meetingId, this.initParameters, isModerator, customParameter);
const response = await this.page.goto(joinUrl);
await expect(response.ok()).toBeTruthy();

View File

@ -7,9 +7,11 @@
"dependencies": {
"@playwright/test": "^1.25.0",
"axios": "^0.26.1",
"chalk": "^4.1.2",
"dotenv": "^16.0.0",
"playwright": "^1.19.2",
"sha1": "^1.1.1"
"sha1": "^1.1.1",
"xml2js": "^0.4.23"
}
},
"node_modules/@playwright/test": {
@ -43,6 +45,20 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz",
"integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg=="
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
@ -51,6 +67,21 @@
"follow-redirects": "^1.14.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
@ -59,6 +90,22 @@
"node": "*"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
@ -94,6 +141,14 @@
}
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.22.2.tgz",
@ -120,6 +175,11 @@
"node": ">=14"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/sha1": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz",
@ -131,6 +191,37 @@
"engines": {
"node": "*"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"engines": {
"node": ">=4.0"
}
}
},
"dependencies": {
@ -155,6 +246,14 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz",
"integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
@ -163,11 +262,33 @@
"follow-redirects": "^1.14.8"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
@ -183,6 +304,11 @@
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"playwright": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.22.2.tgz",
@ -196,6 +322,11 @@
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.22.2.tgz",
"integrity": "sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"sha1": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz",
@ -204,6 +335,28 @@
"charenc": ">= 0.0.1",
"crypt": ">= 0.0.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
}
},
"xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
}
}
}

View File

@ -11,7 +11,9 @@
"@playwright/test": "^1.25.0",
"playwright": "^1.19.2",
"axios": "^0.26.1",
"chalk": "^4.1.2",
"dotenv": "^16.0.0",
"sha1": "^1.1.1"
"sha1": "^1.1.1",
"xml2js": "^0.4.23"
}
}

View File

@ -1,4 +1,4 @@
. ./opts-global.sh
AKKA_APPS="bbb-fsesl-akka,bbb-apps-akka"
OPTS="$OPTS -t deb -d netcat-openbsd,bbb-html5,bbb-playback-presentation,bbb-playback,bbb-freeswitch-core,$AKKA_APPS"
OPTS="$OPTS -t deb -d netcat-openbsd,stun-client,bbb-html5,bbb-playback-presentation,bbb-playback,bbb-freeswitch-core,$AKKA_APPS"