update file exclusion patterns in workspace settings
webcomponent: Added missing and necessary js file Update .gitignore to specify backend public directory exclusion webcomponent: Add error handling for invalid base URL in OpenViduMeet component webcomponent: Update Jest configuration for improved testing setup webcomponent: Enhance iframe attribute tests and add support for optional query parameters webcomponent: Refactor documentation copying in build_webcomponent_doc function for improved readability and add absolute path resolution Add E2EE_KEY property to WebComponentProperty enum for end-to-end encryption support meet.sh: Enhance build_rest_api_doc function with output file handling and user confirmation for overwriting frontend: replace removeRoomSecretGuard with removeQueryParamsGuard for enhanced query parameter management frontend: add E2EE key handling in room service and update query params guard Updated pnpm-lock.yaml Enables end-to-end encryption (E2EE) Adds E2EE functionality to meeting rooms. Significant changes: - Allows encryption of the participant name - Introduces setting and getting E2EE keys - Ensures recording is disabled when encryption is enabled webcomponent: Added e2e test for checking the e2ee funcionality frontend: Sanitize participant name before request for a token fix: clean up formatting in openvidu-meet.code-workspace
This commit is contained in:
parent
e6d04aca16
commit
b055ef0333
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,7 +37,7 @@ pnpm-debug.log*
|
||||
|
||||
|
||||
**/**/test-results
|
||||
**/**/public/
|
||||
**/backend/public/
|
||||
|
||||
**/*/coverage
|
||||
**/**/test-results
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';
|
||||
import { ErrorReason } from '../models';
|
||||
import {
|
||||
AppDataService,
|
||||
NavigationService,
|
||||
ParticipantService,
|
||||
RoomService,
|
||||
SessionStorageService
|
||||
} from '../services';
|
||||
import { AppDataService, NavigationService, ParticipantService, RoomService, SessionStorageService } from '../services';
|
||||
import { WebComponentProperty } from '@openvidu-meet/typings';
|
||||
|
||||
export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
|
||||
@ -16,7 +10,14 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute
|
||||
const participantService = inject(ParticipantService);
|
||||
const sessionStorageService = inject(SessionStorageService);
|
||||
|
||||
const { roomId, secret: querySecret, participantName, leaveRedirectUrl, showOnlyRecordings } = extractParams(route);
|
||||
const {
|
||||
roomId,
|
||||
secret: querySecret,
|
||||
participantName,
|
||||
leaveRedirectUrl,
|
||||
showOnlyRecordings,
|
||||
e2eeKey
|
||||
} = extractParams(route);
|
||||
const secret = querySecret || sessionStorageService.getRoomSecret();
|
||||
|
||||
// Handle leave redirect URL logic
|
||||
@ -29,6 +30,7 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute
|
||||
|
||||
roomService.setRoomId(roomId);
|
||||
roomService.setRoomSecret(secret);
|
||||
roomService.setE2EEKey(e2eeKey);
|
||||
|
||||
if (participantName) {
|
||||
participantService.setParticipantName(participantName);
|
||||
@ -66,7 +68,8 @@ const extractParams = ({ params, queryParams }: ActivatedRouteSnapshot) => ({
|
||||
secret: queryParams['secret'] as string,
|
||||
participantName: queryParams[WebComponentProperty.PARTICIPANT_NAME] as string,
|
||||
leaveRedirectUrl: queryParams[WebComponentProperty.LEAVE_REDIRECT_URL] as string,
|
||||
showOnlyRecordings: (queryParams[WebComponentProperty.SHOW_ONLY_RECORDINGS] as string) || 'false'
|
||||
showOnlyRecordings: (queryParams[WebComponentProperty.SHOW_ONLY_RECORDINGS] as string) || 'false',
|
||||
e2eeKey: queryParams[WebComponentProperty.E2EE_KEY] as string
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export * from './auth.guard';
|
||||
export * from './extract-query-params.guard';
|
||||
export * from './remove-secret.guard';
|
||||
export * from './remove-query-params.guard';
|
||||
export * from './run-serially.guard';
|
||||
export * from './validate-access.guard';
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, NavigationEnd, Router } from '@angular/router';
|
||||
import { filter, take } from 'rxjs';
|
||||
import { NavigationService } from '../services';
|
||||
|
||||
/**
|
||||
* Guard that removes specified query parameters from the URL after the navigation completes.
|
||||
*
|
||||
* @param params - Array of query parameter names to remove from the URL
|
||||
* @returns A guard function that schedules removal of the specified query parameters after navigation
|
||||
*
|
||||
*/
|
||||
export const removeQueryParamsGuard = (params: string[]): CanActivateFn => {
|
||||
return (route: ActivatedRouteSnapshot) => {
|
||||
const router = inject(Router);
|
||||
const navigationService = inject(NavigationService);
|
||||
|
||||
// Only proceed if there are params to remove
|
||||
if (!params || params.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any of the specified params exist in the current query params
|
||||
const hasParamsToRemove = params.some((param) => route.queryParams[param] !== undefined);
|
||||
|
||||
if (!hasParamsToRemove) {
|
||||
// No params to remove, continue navigation immediately
|
||||
return true;
|
||||
}
|
||||
|
||||
// Schedule param removal AFTER navigation completes
|
||||
// This prevents conflicts with the ongoing navigation
|
||||
router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(async () => {
|
||||
try {
|
||||
await navigationService.removeQueryParamsFromUrl(route.queryParams, params);
|
||||
} catch (error) {
|
||||
console.error('Error removing query params:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Allow the current navigation to proceed
|
||||
return true;
|
||||
};
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, NavigationEnd, Router } from '@angular/router';
|
||||
import { NavigationService } from '../services';
|
||||
import { filter, take } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Guard that intercepts navigation to remove the 'secret' query parameter from the URL
|
||||
* that determine the role of a participant when joining a room or accessing its recordings,
|
||||
* in order to enhance security.
|
||||
*/
|
||||
export const removeRoomSecretGuard: CanActivateFn = (route, _state) => {
|
||||
const router = inject(Router);
|
||||
const navigationService = inject(NavigationService);
|
||||
|
||||
router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(async () => {
|
||||
await navigationService.removeQueryParamFromUrl(route.queryParams, 'secret');
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
@ -52,8 +52,7 @@
|
||||
message but will be unable to see or hear others.
|
||||
</span>
|
||||
</li>
|
||||
<li>Recording is unavailable while encryption is enabled.</li>
|
||||
<li>Chat messages are not protected by end-to-end encryption.</li>
|
||||
<li>Recording is <b>unavailable</b> while encryption is enabled.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
checkUserNotAuthenticatedGuard,
|
||||
extractRecordingQueryParamsGuard,
|
||||
extractRoomQueryParamsGuard,
|
||||
removeRoomSecretGuard,
|
||||
removeQueryParamsGuard,
|
||||
runGuardsSerially,
|
||||
validateRecordingAccessGuard,
|
||||
validateRoomAccessGuard
|
||||
@ -27,6 +27,7 @@ import {
|
||||
ViewRecordingComponent,
|
||||
ConfigComponent
|
||||
} from '../pages';
|
||||
import { WebComponentProperty } from '@openvidu-meet/typings';
|
||||
|
||||
export const baseRoutes: Routes = [
|
||||
{
|
||||
@ -40,9 +41,9 @@ export const baseRoutes: Routes = [
|
||||
canActivate: [
|
||||
runGuardsSerially(
|
||||
extractRoomQueryParamsGuard,
|
||||
removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]),
|
||||
checkParticipantRoleAndAuthGuard,
|
||||
validateRoomAccessGuard,
|
||||
removeRoomSecretGuard
|
||||
validateRoomAccessGuard
|
||||
)
|
||||
]
|
||||
},
|
||||
@ -52,9 +53,9 @@ export const baseRoutes: Routes = [
|
||||
canActivate: [
|
||||
runGuardsSerially(
|
||||
extractRecordingQueryParamsGuard,
|
||||
removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]),
|
||||
checkParticipantRoleAndAuthGuard,
|
||||
validateRecordingAccessGuard,
|
||||
removeRoomSecretGuard
|
||||
validateRecordingAccessGuard
|
||||
)
|
||||
]
|
||||
},
|
||||
|
||||
@ -92,38 +92,48 @@ export class MeetingEventHandlerService {
|
||||
): void {
|
||||
room.on(
|
||||
RoomEvent.DataReceived,
|
||||
async (
|
||||
payload: Uint8Array,
|
||||
_participant?: RemoteParticipant,
|
||||
_kind?: DataPacket_Kind,
|
||||
topic?: string
|
||||
) => {
|
||||
const event = JSON.parse(new TextDecoder().decode(payload));
|
||||
async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => {
|
||||
// Only process topics that this handler is responsible for
|
||||
const relevantTopics = [
|
||||
'recordingStopped',
|
||||
MeetSignalType.MEET_ROOM_CONFIG_UPDATED,
|
||||
MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED
|
||||
];
|
||||
|
||||
switch (topic) {
|
||||
case 'recordingStopped':
|
||||
await this.handleRecordingStopped(
|
||||
context.roomId,
|
||||
context.roomSecret,
|
||||
context.onHasRecordingsChanged
|
||||
);
|
||||
break;
|
||||
if (!topic || !relevantTopics.includes(topic)) {
|
||||
return;
|
||||
}
|
||||
|
||||
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
|
||||
await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret);
|
||||
break;
|
||||
try {
|
||||
const event = JSON.parse(new TextDecoder().decode(payload));
|
||||
|
||||
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
|
||||
await this.handleParticipantRoleUpdated(
|
||||
event,
|
||||
context.roomId,
|
||||
context.participantName,
|
||||
context.localParticipant,
|
||||
context.remoteParticipants,
|
||||
context.onRoomSecretChanged,
|
||||
context.onParticipantRoleUpdated
|
||||
);
|
||||
break;
|
||||
switch (topic) {
|
||||
case 'recordingStopped':
|
||||
await this.handleRecordingStopped(
|
||||
context.roomId,
|
||||
context.roomSecret,
|
||||
context.onHasRecordingsChanged
|
||||
);
|
||||
break;
|
||||
|
||||
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
|
||||
await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret);
|
||||
break;
|
||||
|
||||
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
|
||||
await this.handleParticipantRoleUpdated(
|
||||
event,
|
||||
context.roomId,
|
||||
context.participantName,
|
||||
context.localParticipant,
|
||||
context.remoteParticipants,
|
||||
context.onRoomSecretChanged,
|
||||
context.onParticipantRoleUpdated
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse data message for topic: ${topic}`, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -203,7 +213,7 @@ export class MeetingEventHandlerService {
|
||||
if (error.status === 503) {
|
||||
console.error(
|
||||
'No egress service available. Check CPU usage or Media Node capacity. ' +
|
||||
'By default, a recording uses 2 CPUs per room.'
|
||||
'By default, a recording uses 2 CPUs per room.'
|
||||
);
|
||||
} else {
|
||||
console.error('Error starting recording:', error);
|
||||
|
||||
@ -72,6 +72,10 @@ export class MeetingLobbyService {
|
||||
return value.name.trim();
|
||||
}
|
||||
|
||||
set e2eeKey(key: string) {
|
||||
this.state.participantForm.get('e2eeKey')?.setValue(key);
|
||||
}
|
||||
|
||||
get e2eeKey(): string {
|
||||
const { valid, value } = this.state.participantForm;
|
||||
if (!valid || !value.e2eeKey?.trim()) {
|
||||
@ -93,6 +97,12 @@ export class MeetingLobbyService {
|
||||
// If E2EE is enabled, require e2eeKey
|
||||
if (this.state.isE2EEEnabled) {
|
||||
this.state.participantForm.get('e2eeKey')?.setValidators([Validators.required]);
|
||||
this.e2eeKey = this.roomService.getE2EEKey();
|
||||
|
||||
if (this.e2eeKey) {
|
||||
// when e2eeKey is already set (e.g., from URL or webcomponent), populate and disable field
|
||||
this.state.participantForm.get('e2eeKey')?.disable();
|
||||
}
|
||||
this.state.participantForm.get('e2eeKey')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
@ -145,10 +155,16 @@ export class MeetingLobbyService {
|
||||
}
|
||||
|
||||
async submitAccess(): Promise<void> {
|
||||
if (!this.participantName) {
|
||||
const sanitized = this.participantName
|
||||
.replace(/[^a-zA-Z0-9 _-]/g, '') // remove invalid chars
|
||||
.replace(/\s+/g, ' ') // normalize spaces
|
||||
.trim(); // remove leading/trailing spaces
|
||||
|
||||
if (!sanitized) {
|
||||
console.error('Participant form is invalid. Cannot access meeting.');
|
||||
throw new Error('Participant form is invalid');
|
||||
}
|
||||
this.participantName = sanitized;
|
||||
|
||||
// For E2EE rooms, validate passkey
|
||||
if (this.state.isE2EEEnabled && !this.e2eeKey) {
|
||||
@ -243,11 +259,14 @@ export class MeetingLobbyService {
|
||||
*/
|
||||
protected async generateParticipantToken() {
|
||||
try {
|
||||
this.state.participantToken = await this.participantService.generateToken({
|
||||
roomId: this.state.roomId,
|
||||
secret: this.state.roomSecret,
|
||||
participantName: this.participantName
|
||||
});
|
||||
this.state.participantToken = await this.participantService.generateToken(
|
||||
{
|
||||
roomId: this.state.roomId,
|
||||
secret: this.state.roomSecret,
|
||||
participantName: this.participantName
|
||||
},
|
||||
this.e2eeKey
|
||||
);
|
||||
this.participantName = this.participantService.getParticipantName()!;
|
||||
} catch (error: any) {
|
||||
console.error('Error generating participant token:', error);
|
||||
|
||||
@ -177,8 +177,25 @@ export class NavigationService {
|
||||
* @param param - The parameter to remove
|
||||
*/
|
||||
async removeQueryParamFromUrl(queryParams: Params, param: string): Promise<void> {
|
||||
await this.removeQueryParamsFromUrl(queryParams, [param]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple query parameters from the URL in a single navigation operation.
|
||||
* This is more efficient than removing params one by one, as it only triggers one navigation.
|
||||
*
|
||||
* @param queryParams - The current query parameters
|
||||
* @param params - Array of parameter names to remove
|
||||
*/
|
||||
async removeQueryParamsFromUrl(queryParams: Params, params: string[]): Promise<void> {
|
||||
if (!params || params.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedParams = { ...queryParams };
|
||||
delete updatedParams[param];
|
||||
params.forEach((param) => {
|
||||
delete updatedParams[param];
|
||||
});
|
||||
|
||||
await this.router.navigate([], {
|
||||
queryParams: updatedParams,
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
ParticipantRole
|
||||
} from '@openvidu-meet/typings';
|
||||
import { getValidDecodedToken } from '../utils';
|
||||
import { LoggerService } from 'openvidu-components-angular';
|
||||
import { E2eeService, LoggerService } from 'openvidu-components-angular';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -29,7 +29,8 @@ export class ParticipantService {
|
||||
protected httpService: HttpService,
|
||||
protected featureConfService: FeatureConfigurationService,
|
||||
protected globalConfigService: GlobalConfigService,
|
||||
protected tokenStorageService: TokenStorageService
|
||||
protected tokenStorageService: TokenStorageService,
|
||||
protected e2eeService: E2eeService
|
||||
) {
|
||||
this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService');
|
||||
}
|
||||
@ -53,8 +54,15 @@ export class ParticipantService {
|
||||
* @param participantOptions - The options for the participant, including room ID, participant name, and secret
|
||||
* @return A promise that resolves to the participant token
|
||||
*/
|
||||
async generateToken(participantOptions: ParticipantOptions): Promise<string> {
|
||||
async generateToken(participantOptions: ParticipantOptions, e2EEKey = ''): Promise<string> {
|
||||
const path = `${this.PARTICIPANTS_API}/token`;
|
||||
|
||||
if (participantOptions.participantName && !!e2EEKey) {
|
||||
// Asign E2EE key and encrypt participant name
|
||||
await this.e2eeService.setE2EEKey(e2EEKey);
|
||||
participantOptions.participantName = await this.e2eeService.encrypt(participantOptions.participantName);
|
||||
}
|
||||
|
||||
const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions);
|
||||
|
||||
// Store token in sessionStorage for header mode
|
||||
@ -63,7 +71,7 @@ export class ParticipantService {
|
||||
this.tokenStorageService.setParticipantToken(token);
|
||||
}
|
||||
|
||||
this.updateParticipantTokenInfo(token);
|
||||
await this.updateParticipantTokenInfo(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
@ -83,7 +91,7 @@ export class ParticipantService {
|
||||
this.tokenStorageService.setParticipantToken(token);
|
||||
}
|
||||
|
||||
this.updateParticipantTokenInfo(token);
|
||||
await this.updateParticipantTokenInfo(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
@ -93,13 +101,14 @@ export class ParticipantService {
|
||||
* @param token - The JWT token to set.
|
||||
* @throws Error if the token is invalid or expired.
|
||||
*/
|
||||
protected updateParticipantTokenInfo(token: string): void {
|
||||
protected async updateParticipantTokenInfo(token: string): Promise<void> {
|
||||
try {
|
||||
const decodedToken = getValidDecodedToken(token);
|
||||
const metadata = decodedToken.metadata as MeetTokenMetadata;
|
||||
|
||||
if (decodedToken.sub && decodedToken.name) {
|
||||
this.setParticipantName(decodedToken.name);
|
||||
const decryptedName = await this.e2eeService.decryptOrMask(decodedToken.name);
|
||||
this.setParticipantName(decryptedName);
|
||||
this.participantIdentity = decodedToken.sub;
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ export class RoomService {
|
||||
|
||||
protected roomId: string = '';
|
||||
protected roomSecret: string = '';
|
||||
protected e2eeKey: string = '';
|
||||
|
||||
protected log;
|
||||
|
||||
@ -50,6 +51,14 @@ export class RoomService {
|
||||
}
|
||||
}
|
||||
|
||||
setE2EEKey(e2eeKey: string) {
|
||||
this.e2eeKey = e2eeKey;
|
||||
}
|
||||
|
||||
getE2EEKey(): string {
|
||||
return this.e2eeKey;
|
||||
}
|
||||
|
||||
getRoomSecret(): string {
|
||||
return this.roomSecret;
|
||||
}
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
{
|
||||
"jest.jestCommandLine": "node --experimental-vm-modules ../../../node_modules/.bin/jest --config jest.config.mjs",
|
||||
"jest.jestCommandLine": "pnpm run test:unit",
|
||||
"jest.rootPath": ".",
|
||||
"jest.nodeEnv": {
|
||||
"NODE_OPTIONS": "--experimental-vm-modules"
|
||||
},
|
||||
"jest.runMode": "on-demand"
|
||||
}
|
||||
|
||||
@ -1,25 +1,21 @@
|
||||
import { createDefaultEsmPreset } from 'ts-jest'
|
||||
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
const jestConfig = {
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
displayName: 'webcomponent',
|
||||
...createDefaultEsmPreset({
|
||||
tsconfig: 'tsconfig.json'
|
||||
}),
|
||||
// Set the root directory to the webcomponent folder
|
||||
rootDir: './',
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
useESM: true,
|
||||
tsconfig: 'tsconfig.json'
|
||||
}
|
||||
},
|
||||
resolver: 'ts-jest-resolver',
|
||||
testEnvironment: 'jsdom',
|
||||
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/tests/e2e/'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }]
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json'
|
||||
}
|
||||
'^.+\\.tsx?$': ['ts-jest', { useESM: true }]
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@openvidu-meet/typings$': '<rootDir>/../../typings/src/index.ts',
|
||||
@ -27,4 +23,4 @@ const jestConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
export default jestConfig
|
||||
export default config
|
||||
|
||||
@ -143,19 +143,24 @@ export class OpenViduMeet extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
this.targetIframeOrigin = url.origin;
|
||||
this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||
this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
this.targetIframeOrigin = url.origin;
|
||||
this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||
this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||
|
||||
// Update query params
|
||||
Array.from(this.attributes).forEach((attr) => {
|
||||
if (attr.name !== WebComponentProperty.ROOM_URL && attr.name !== WebComponentProperty.RECORDING_URL) {
|
||||
url.searchParams.set(attr.name, attr.value);
|
||||
}
|
||||
});
|
||||
// Update query params
|
||||
Array.from(this.attributes).forEach((attr) => {
|
||||
if (attr.name !== WebComponentProperty.ROOM_URL && attr.name !== WebComponentProperty.RECORDING_URL) {
|
||||
url.searchParams.set(attr.name, attr.value);
|
||||
}
|
||||
});
|
||||
|
||||
this.iframe.src = url.toString();
|
||||
this.iframe.src = url.toString();
|
||||
} catch (error) {
|
||||
console.error(`Invalid URL provided: ${baseUrl}`, error);
|
||||
alert(`Invalid URL provided: ${baseUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -124,10 +124,10 @@ test.describe('E2EE UI Tests', () => {
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// E2EE SESSION TESTS
|
||||
// E2EE MEETING TESTS
|
||||
// ==========================================
|
||||
|
||||
test.describe('E2EE in Session', () => {
|
||||
test.describe('E2EE in Meeting', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
try {
|
||||
await leaveRoom(page);
|
||||
@ -152,7 +152,6 @@ test.describe('E2EE UI Tests', () => {
|
||||
const page2 = await context.newPage();
|
||||
|
||||
// Participant 1 joins with E2EE key
|
||||
await page.goto(MEET_TESTAPP_URL);
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
await page.click('#join-as-speaker');
|
||||
|
||||
@ -178,7 +177,6 @@ test.describe('E2EE UI Tests', () => {
|
||||
|
||||
// Participant 2 joins with same E2EE key
|
||||
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
||||
await page2.goto(MEET_TESTAPP_URL);
|
||||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||||
await page2.click('#join-as-speaker');
|
||||
|
||||
@ -222,6 +220,16 @@ test.describe('E2EE UI Tests', () => {
|
||||
});
|
||||
await expect(encryptionError2).toBeHidden();
|
||||
|
||||
// Expect video to be flowing (by checking the video element has video tracks)
|
||||
const videoElements = await waitForElementInIframe(page, '.OV_video-element', {
|
||||
state: 'visible',
|
||||
all: true
|
||||
});
|
||||
for (const videoElement of videoElements) {
|
||||
const videoTracks = await videoElement.evaluate((el) => (el as any).srcObject?.getVideoTracks());
|
||||
expect(videoTracks.length).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Cleanup participant 2
|
||||
await leaveRoom(page2);
|
||||
await page2.close();
|
||||
@ -342,6 +350,463 @@ test.describe('E2EE UI Tests', () => {
|
||||
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
||||
await Promise.all([page2.close(), page3.close()]);
|
||||
});
|
||||
|
||||
test('should decrypt participant names and chat messages with correct E2EE key', async ({ page, context }) => {
|
||||
// Enable E2EE
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
const e2eeKey = 'shared-encryption-key-456';
|
||||
const participant1Name = `Alice-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const participant2Name = `Bob-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
// Participant 1 joins with E2EE key
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
await page.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant1Name
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Participant 2 joins with same E2EE key
|
||||
const page2 = await context.newPage();
|
||||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||||
await page2.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page2, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant2Name
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
|
||||
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Wait for participants to connect
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ===== CHECK PARTICIPANT NAMES IN VIDEO GRID =====
|
||||
// Participant 1 should see Participant 2's name decrypted
|
||||
const participantNameElements = await Promise.all([
|
||||
waitForElementInIframe(page, '#participant-name', {
|
||||
state: 'attached',
|
||||
all: true
|
||||
}),
|
||||
waitForElementInIframe(page2, '#participant-name', {
|
||||
state: 'attached',
|
||||
all: true
|
||||
})
|
||||
]);
|
||||
|
||||
for (const participantNameElement of participantNameElements.flat()) {
|
||||
const name = await participantNameElement.evaluate((el) => el.textContent);
|
||||
expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
|
||||
expect(name).not.toContain('*');
|
||||
}
|
||||
|
||||
// ===== CHECK NAMES IN PARTICIPANTS PANEL =====
|
||||
// Open participants panel
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
|
||||
]);
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-participants-panel', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, 'ov-participants-panel', { state: 'visible' })
|
||||
]);
|
||||
// Check that both names are visible and decrypted in the panel
|
||||
const participantsPanelNames = await Promise.all([
|
||||
waitForElementInIframe(page, '.participant-item-name span', {
|
||||
state: 'attached',
|
||||
all: true
|
||||
}),
|
||||
waitForElementInIframe(page2, '.participant-item-name span', {
|
||||
state: 'attached',
|
||||
all: true
|
||||
})
|
||||
]);
|
||||
|
||||
for (const participantPanelName of participantsPanelNames.flat()) {
|
||||
const name = await participantPanelName.evaluate((el) => el.textContent);
|
||||
expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
|
||||
expect(name).not.toContain('*');
|
||||
}
|
||||
|
||||
// Close participants panel
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
|
||||
]);
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-participants-panel', { state: 'hidden' }),
|
||||
waitForElementInIframe(page2, 'ov-participants-panel', { state: 'hidden' })
|
||||
]);
|
||||
|
||||
// ===== CHECK OWN NAME IN SETTINGS PANEL =====
|
||||
// Open settings panel
|
||||
await Promise.all([openMoreOptionsMenu(page), openMoreOptionsMenu(page2)]);
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#toolbar-settings-btn', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '#toolbar-settings-btn', { action: 'click' })
|
||||
]);
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-settings-panel', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, 'ov-settings-panel', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// Check that own name is visible and decrypted
|
||||
const ownNameInputs = await Promise.all([
|
||||
waitForElementInIframe(page, '#participant-name-input', {
|
||||
state: 'visible'
|
||||
}),
|
||||
waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' })
|
||||
]);
|
||||
|
||||
const ownName1 = await ownNameInputs[0].evaluate((el: HTMLInputElement) => el.value);
|
||||
const ownName2 = await ownNameInputs[1].evaluate((el: HTMLInputElement) => el.value);
|
||||
expect(ownName1).toBe(participant1Name);
|
||||
expect(ownName1).not.toContain('*');
|
||||
expect(ownName2).toBe(participant2Name);
|
||||
expect(ownName2).not.toContain('*');
|
||||
|
||||
// Close settings panel
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '.panel-close-button', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '.panel-close-button', { action: 'click' })
|
||||
]);
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-settings-panel', { state: 'hidden' }),
|
||||
waitForElementInIframe(page2, 'ov-settings-panel', { state: 'hidden' })
|
||||
]);
|
||||
await Promise.all([closeMoreOptionsMenu(page), closeMoreOptionsMenu(page2)]);
|
||||
|
||||
// ===== CHECK CHAT MESSAGES =====
|
||||
// Open chat
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '#chat-panel-btn', { action: 'click' })
|
||||
]);
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// ===== MESSAGE: PARTICIPANT 1 → PARTICIPANT 2 =====
|
||||
const testMessage1 = `Hello from ${participant1Name}!`;
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: testMessage1 }),
|
||||
waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
|
||||
]);
|
||||
|
||||
await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
|
||||
|
||||
// Wait for message to be sent
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Open chat on page 2
|
||||
const chatMessages2 = await waitForElementInIframe(page2, '.chat-message', { state: 'visible' });
|
||||
|
||||
// Verify message content
|
||||
const messageText2 = await chatMessages2.evaluate((el) => el.textContent || '');
|
||||
expect(messageText2).toContain(testMessage1);
|
||||
expect(messageText2).not.toContain('*');
|
||||
|
||||
// ===== MESSAGE: PARTICIPANT 2 → PARTICIPANT 1 =====
|
||||
const testMessage2 = `Hi from ${participant2Name}!`;
|
||||
|
||||
// Send message in page2 iframe
|
||||
await interactWithElementInIframe(page2, '#chat-input', { action: 'fill', value: testMessage2 });
|
||||
await interactWithElementInIframe(page2, '#send-btn', { action: 'click' });
|
||||
|
||||
// Wait briefly for message delivery
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Wait for message on participant 1’s side
|
||||
const chatMessages1 = await waitForElementInIframe(page, '.chat-message', { state: 'visible' });
|
||||
|
||||
// Collect all chat messages in the chat panel
|
||||
const allMessages1 = await chatMessages1.evaluate((el) =>
|
||||
Array.from(el.closest('ov-chat-panel')?.querySelectorAll('.chat-message') || []).map(
|
||||
(e) => e.textContent || ''
|
||||
)
|
||||
);
|
||||
|
||||
// Verify received message
|
||||
expect(allMessages1.join(' ')).toContain(testMessage2);
|
||||
expect(allMessages1.join(' ')).not.toContain('*');
|
||||
|
||||
// Cleanup
|
||||
await leaveRoom(page2);
|
||||
await page2.close();
|
||||
});
|
||||
|
||||
test('should show masked names and unreadable messages for participant with wrong E2EE key', async ({
|
||||
page,
|
||||
context
|
||||
}) => {
|
||||
// Enable E2EE
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
const correctKey = 'correct-shared-key-789';
|
||||
const wrongKey = 'wrong-key-999';
|
||||
const participant1Name = `Charlie-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const participant2Name = `David-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const participant3Name = `Eve-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const [page2, page3] = await Promise.all([context.newPage(), context.newPage()]);
|
||||
|
||||
// Prepare for all participants to join the room
|
||||
await Promise.all([
|
||||
prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId),
|
||||
prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId),
|
||||
prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId)
|
||||
]);
|
||||
|
||||
// Join as speaker in all pages
|
||||
await Promise.all([
|
||||
page.click('#join-as-speaker'),
|
||||
page2.click('#join-as-speaker'),
|
||||
page3.click('#join-as-speaker')
|
||||
]);
|
||||
|
||||
// Wait for name and E2EE key inputs to be visible in all pages
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, '#participant-name-input', { state: 'visible' }),
|
||||
waitForElementInIframe(page, '#participant-e2eekey-input', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, '#participant-e2eekey-input', { state: 'visible' }),
|
||||
waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' }),
|
||||
waitForElementInIframe(page3, '#participant-e2eekey-input', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// Fill participant names
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant1Name
|
||||
}),
|
||||
interactWithElementInIframe(page2, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant2Name
|
||||
}),
|
||||
interactWithElementInIframe(page3, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant3Name
|
||||
})
|
||||
]);
|
||||
|
||||
// Fill E2EE keys (two correct, one wrong)
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
|
||||
interactWithElementInIframe(page2, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
|
||||
interactWithElementInIframe(page3, '#participant-e2eekey-input', { action: 'fill', value: wrongKey })
|
||||
]);
|
||||
|
||||
// Join all participants
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' }),
|
||||
interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' })
|
||||
]);
|
||||
|
||||
// Wait for prejoin page in all pages
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' }),
|
||||
waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// Click join button in all pages
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#join-button', { action: 'click' }),
|
||||
interactWithElementInIframe(page2, '#join-button', { action: 'click' }),
|
||||
interactWithElementInIframe(page3, '#join-button', { action: 'click' })
|
||||
]);
|
||||
|
||||
// Wait for session to be visible in all pages
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page, 'ov-session', { state: 'visible' }),
|
||||
waitForElementInIframe(page2, 'ov-session', { state: 'visible' }),
|
||||
waitForElementInIframe(page3, 'ov-session', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// Wait for participants to connect
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Check that participant 3 sees encryption error posters for others
|
||||
// ===== CHECK MASKED NAMES IN VIDEO GRID FOR PARTICIPANT 3 =====
|
||||
const participantNameElements3 = await waitForElementInIframe(
|
||||
page3,
|
||||
'#layout .participant-name-container #participant-name',
|
||||
{
|
||||
state: 'attached',
|
||||
all: true
|
||||
}
|
||||
);
|
||||
const participantNames3 = await Promise.all(
|
||||
participantNameElements3.map((el) => el.evaluate((e) => e.textContent))
|
||||
);
|
||||
|
||||
console.log('Participant Names Seen by Participant 3:', participantNames3);
|
||||
console.log('Expected: 3 names (own + 2 masked), got:', participantNames3.length);
|
||||
|
||||
// Should have exactly 3 participants
|
||||
expect(participantNames3.length).toBe(3);
|
||||
|
||||
// Should NOT all be masked (own name should be visible)
|
||||
expect(participantNames3.every((name) => name?.includes('******'))).toBeFalsy();
|
||||
|
||||
// Should have exactly 2 masked names
|
||||
const maskedNames = participantNames3.filter((name) => name?.includes('******'));
|
||||
expect(maskedNames.length).toBe(2);
|
||||
|
||||
// Should see own name
|
||||
expect(participantNames3).toContain(participant3Name);
|
||||
|
||||
// Should NOT see the actual names of P1 and P2
|
||||
expect(participantNames3.join(' ')).not.toContain(participant1Name);
|
||||
expect(participantNames3.join(' ')).not.toContain(participant2Name);
|
||||
|
||||
// ===== CHECK MASKED NAMES IN PARTICIPANTS PANEL =====
|
||||
await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
|
||||
await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'visible' });
|
||||
|
||||
const participantsPanelNames3 = await waitForElementInIframe(page3, '.participant-name-text', {
|
||||
state: 'visible',
|
||||
all: true
|
||||
});
|
||||
const panelNamesText3 = await Promise.all(
|
||||
participantsPanelNames3.map((el) => el.evaluate((e) => e.textContent))
|
||||
);
|
||||
|
||||
console.log('Panel Names Seen by Participant 3:', panelNamesText3);
|
||||
console.log('Expected: 3 names (own + 2 masked), got:', panelNamesText3.length);
|
||||
|
||||
// Should have exactly 3 participants in panel
|
||||
expect(panelNamesText3.length).toBe(3);
|
||||
|
||||
// Should NOT all be masked (own name should be visible)
|
||||
expect(panelNamesText3.every((name) => name?.includes('******'))).toBeFalsy();
|
||||
|
||||
// Should have exactly 2 masked names
|
||||
const maskedPanelNames = panelNamesText3.filter((name) => name?.includes('******'));
|
||||
expect(maskedPanelNames.length).toBe(2);
|
||||
|
||||
// Should see own name
|
||||
expect(panelNamesText3).toContain(participant3Name);
|
||||
|
||||
// Should NOT see the actual names of P1 and P2
|
||||
expect(panelNamesText3.join(' ')).not.toContain(participant1Name);
|
||||
expect(panelNamesText3.join(' ')).not.toContain(participant2Name);
|
||||
|
||||
await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
|
||||
await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'hidden' });
|
||||
|
||||
// ===== CHECK OWN NAME IN SETTINGS PANEL =====
|
||||
await openMoreOptionsMenu(page3);
|
||||
await interactWithElementInIframe(page3, '#toolbar-settings-btn', { action: 'click' });
|
||||
await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'visible' });
|
||||
|
||||
const ownNameInput3 = await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
|
||||
const ownName3 = await ownNameInput3.evaluate((el: HTMLInputElement) => el.value);
|
||||
expect(ownName3).toBe(participant3Name);
|
||||
expect(ownName3).not.toContain('******');
|
||||
|
||||
await interactWithElementInIframe(page3, '.panel-close-button', { action: 'click' });
|
||||
await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'hidden' });
|
||||
await closeMoreOptionsMenu(page3);
|
||||
|
||||
// ===== SEND MESSAGE FROM PARTICIPANT 1 =====
|
||||
const secretMessage = `Secret message from ${participant1Name}`;
|
||||
await Promise.all([
|
||||
interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
|
||||
waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// Send message
|
||||
await interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: secretMessage });
|
||||
await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
|
||||
|
||||
// Wait for message to be sent and received
|
||||
await Promise.all([
|
||||
waitForElementInIframe(page2, '#chat-panel-btn .mat-badge-content', { state: 'visible' }),
|
||||
waitForElementInIframe(page3, '#chat-panel-btn .mat-badge-content', { state: 'visible' })
|
||||
]);
|
||||
|
||||
// ===== CHECK CHAT MESSAGES ARE UNREADABLE =====
|
||||
await interactWithElementInIframe(page3, '#chat-panel-btn', { action: 'click' });
|
||||
await waitForElementInIframe(page3, 'ov-chat-panel', { state: 'visible' });
|
||||
|
||||
await page3.waitForTimeout(1000);
|
||||
|
||||
const chatMessagesCount = await countElementsInIframe(page3, '.chat-message');
|
||||
expect(chatMessagesCount).toBeGreaterThan(0);
|
||||
|
||||
const chatMessages3 = await waitForElementInIframe(page3, '.chat-message', {
|
||||
state: 'visible',
|
||||
all: true
|
||||
});
|
||||
const messagesText3 = await Promise.all(chatMessages3.map((el) => el.evaluate((e) => e.textContent)));
|
||||
|
||||
console.log('Chat Messages Seen by Participant 3:', messagesText3);
|
||||
console.log('Expected: All messages masked, got:', messagesText3.length, 'messages');
|
||||
|
||||
// All messages should contain the mask
|
||||
expect(messagesText3.every((text) => text?.includes('******'))).toBeTruthy();
|
||||
|
||||
// Should NOT contain the actual secret message
|
||||
expect(messagesText3.join(' ')).not.toContain(secretMessage);
|
||||
|
||||
// ===== VERIFY PARTICIPANTS 1 AND 2 CAN STILL SEE EACH OTHER =====
|
||||
const participantNameElements1 = await waitForElementInIframe(page, '.participant-name', {
|
||||
state: 'visible',
|
||||
all: true
|
||||
});
|
||||
const participantNames1 = await Promise.all(
|
||||
participantNameElements1.map((el) => el.evaluate((e) => e.textContent))
|
||||
);
|
||||
expect(participantNames1.join(' ')).toContain(participant2Name);
|
||||
|
||||
const participantNameElements2 = await waitForElementInIframe(page2, '.participant-name', {
|
||||
state: 'visible',
|
||||
all: true
|
||||
});
|
||||
const participantNames2 = await Promise.all(
|
||||
participantNameElements2.map((el) => el.evaluate((e) => e.textContent))
|
||||
);
|
||||
expect(participantNames2.join(' ')).toContain(participant1Name);
|
||||
|
||||
// Cleanup
|
||||
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
||||
await Promise.all([page2.close(), page3.close()]);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -24,12 +24,43 @@ export async function getIframeInShadowDom(
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an element inside an iframe within Shadow DOM
|
||||
* @param page - Playwright page object
|
||||
* @param elementSelector - Selector for the element inside the iframe
|
||||
* @param options - Optional configuration
|
||||
* @returns Locator for the found element
|
||||
* Waits for one or more elements inside an iframe within a Shadow DOM.
|
||||
*
|
||||
* By default, waits for the first matching element.
|
||||
* If `options.all` is set to `true`, waits for all matching elements and returns an array.
|
||||
*
|
||||
* @param page - Playwright `Page` instance.
|
||||
* @param elementSelector - CSS selector for the target element(s) inside the iframe.
|
||||
* @param options - Optional configuration object.
|
||||
* @param options.componentSelector - Selector for the shadow DOM component that contains the iframe. Defaults to `'openvidu-meet'`.
|
||||
* @param options.iframeSelector - Selector for the iframe inside the shadow DOM. Defaults to `'iframe'`.
|
||||
* @param options.timeout - Maximum time in milliseconds to wait. Defaults to `30000`.
|
||||
* @param options.state - Wait condition: `'attached' | 'detached' | 'visible' | 'hidden'`. Defaults to `'visible'`.
|
||||
* @param options.index - Element index to return when multiple elements match. Defaults to `0`.
|
||||
* @param options.all - If `true`, waits for all matching elements and returns an array of locators. Defaults to `false`.
|
||||
*
|
||||
* @returns A single `Locator` by default, or an array of `Locator[]` when `options.all` is `true`.
|
||||
*
|
||||
* @example
|
||||
* // Wait for the first visible element
|
||||
* const element = await waitForElementInIframe(page, '.participant');
|
||||
*
|
||||
* @example
|
||||
* // Wait for all visible elements
|
||||
* const elements = await waitForElementInIframe(page, '.participant', { all: true });
|
||||
*/
|
||||
export async function waitForElementInIframe(
|
||||
page: Page,
|
||||
elementSelector: string,
|
||||
options?: {
|
||||
componentSelector?: string;
|
||||
iframeSelector?: string;
|
||||
timeout?: number;
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
index?: number;
|
||||
all?: false;
|
||||
}
|
||||
): Promise<Locator>;
|
||||
export async function waitForElementInIframe(
|
||||
page: Page,
|
||||
elementSelector: string,
|
||||
@ -38,24 +69,42 @@ export async function waitForElementInIframe(
|
||||
iframeSelector?: string;
|
||||
timeout?: number;
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
all: true;
|
||||
}
|
||||
): Promise<Locator[]>;
|
||||
export async function waitForElementInIframe(
|
||||
page: Page,
|
||||
elementSelector: string,
|
||||
options: {
|
||||
componentSelector?: string;
|
||||
iframeSelector?: string;
|
||||
timeout?: number;
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
index?: number;
|
||||
all?: boolean;
|
||||
} = {}
|
||||
): Promise<Locator> {
|
||||
): Promise<Locator | Locator[]> {
|
||||
const {
|
||||
componentSelector = 'openvidu-meet',
|
||||
iframeSelector = 'iframe',
|
||||
timeout = 30000,
|
||||
state = 'visible'
|
||||
state = 'visible',
|
||||
index = 0,
|
||||
all = false
|
||||
} = options;
|
||||
|
||||
// Get the iframe
|
||||
const frameLocator = await getIframeInShadowDom(page, componentSelector, iframeSelector);
|
||||
const baseLocator = frameLocator.locator(elementSelector);
|
||||
|
||||
// Get element locator
|
||||
const elementLocator = frameLocator.locator(elementSelector);
|
||||
if (all) {
|
||||
const locators = await baseLocator.all();
|
||||
await Promise.all(locators.map((l) => l.waitFor({ state, timeout })));
|
||||
return locators;
|
||||
}
|
||||
|
||||
// Wait for the element with the specified state
|
||||
await elementLocator.waitFor({ state, timeout });
|
||||
return elementLocator;
|
||||
const target = baseLocator.nth(index);
|
||||
await target.waitFor({ state, timeout });
|
||||
return target;
|
||||
}
|
||||
|
||||
export async function countElementsInIframe(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
||||
import { WebComponentProperty } from '@openvidu-meet/typings';
|
||||
import '../../src/index';
|
||||
|
||||
describe('OpenViduMeet WebComponent Attributes', () => {
|
||||
@ -15,55 +16,391 @@ describe('OpenViduMeet WebComponent Attributes', () => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should render iframe with correct attributes', () => {
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(iframe?.getAttribute('allow')).toContain('camera');
|
||||
expect(iframe?.getAttribute('allow')).toContain('microphone');
|
||||
expect(iframe?.getAttribute('allow')).toContain('display-capture');
|
||||
expect(iframe?.getAttribute('allow')).toContain('fullscreen');
|
||||
expect(iframe?.getAttribute('allow')).toContain('autoplay');
|
||||
expect(iframe?.getAttribute('allow')).toContain('compute-pressure');
|
||||
// ==========================================
|
||||
// IFRAME SETUP
|
||||
// ==========================================
|
||||
describe('Iframe Configuration', () => {
|
||||
it('should render iframe with correct media permissions', () => {
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe).not.toBeNull();
|
||||
|
||||
const allowAttribute = iframe?.getAttribute('allow');
|
||||
expect(allowAttribute).toContain('camera');
|
||||
expect(allowAttribute).toContain('microphone');
|
||||
expect(allowAttribute).toContain('display-capture');
|
||||
expect(allowAttribute).toContain('fullscreen');
|
||||
expect(allowAttribute).toContain('autoplay');
|
||||
expect(allowAttribute).toContain('compute-pressure');
|
||||
});
|
||||
|
||||
it('should have iframe ready in shadow DOM', () => {
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe).toBeInstanceOf(HTMLIFrameElement);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject rendering iframe when "room-url" attribute is missing', () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
// ==========================================
|
||||
// REQUIRED ATTRIBUTES (room-url | recording-url)
|
||||
// ==========================================
|
||||
describe('Required Attributes', () => {
|
||||
it('should reject iframe src when both room-url and recording-url are missing', () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Trigger updateIframeSrc manually
|
||||
(component as any).updateIframeSrc();
|
||||
// Trigger updateIframeSrc manually
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
|
||||
expect(iframe).toBeDefined();
|
||||
expect(iframe?.src).toBeFalsy();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" or "recording-url" attribute is required.');
|
||||
expect(iframe).toBeDefined();
|
||||
expect(iframe?.src).toBeFalsy();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" or "recording-url" attribute is required.');
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should set iframe src when room-url attribute is provided', () => {
|
||||
const roomUrl = 'https://example.com/room/testRoom-123';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe?.src).toBe(roomUrl);
|
||||
});
|
||||
|
||||
it('should set iframe src when recording-url attribute is provided', () => {
|
||||
const recordingUrl = 'https://example.com/recordings/recording-abc-123';
|
||||
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe?.src).toBe(recordingUrl);
|
||||
});
|
||||
|
||||
it('should prefer room-url over recording-url when both are provided', () => {
|
||||
const roomUrl = 'https://example.com/room/testRoom-123';
|
||||
const recordingUrl = 'https://example.com/recordings/recording-abc-123';
|
||||
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
||||
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe?.src).toBe(roomUrl);
|
||||
});
|
||||
|
||||
it('should extract origin from room-url and set as target origin', () => {
|
||||
const domain = 'https://example.com';
|
||||
const roomUrl = `${domain}/room/testRoom-123?secret=123456`;
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
expect((component as any).targetIframeOrigin).toBe(domain);
|
||||
expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
|
||||
expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
|
||||
});
|
||||
|
||||
it('should extract origin from recording-url and set as target origin', () => {
|
||||
const domain = 'https://recordings.example.com';
|
||||
const recordingUrl = `${domain}/recordings/recording-abc-123`;
|
||||
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
expect((component as any).targetIframeOrigin).toBe(domain);
|
||||
expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
|
||||
expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
|
||||
});
|
||||
|
||||
it('should update iframe src when room-url attribute changes', () => {
|
||||
const roomUrl1 = 'https://example.com/room/room-1';
|
||||
const roomUrl2 = 'https://example.com/room/room-2';
|
||||
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl1);
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
let iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe?.src).toBe(roomUrl1);
|
||||
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl2);
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe?.src).toBe(roomUrl2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update iframe src when "room-url" attribute changes', () => {
|
||||
const roomUrl = 'https://example.com/room/testRoom-123?secret=123456';
|
||||
component.setAttribute('room-url', roomUrl);
|
||||
component.setAttribute('user', 'testUser');
|
||||
// ==========================================
|
||||
// OPTIONAL ATTRIBUTES AS QUERY PARAMETERS
|
||||
// ==========================================
|
||||
describe('Optional Attributes as Query Parameters', () => {
|
||||
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||
|
||||
// Manually trigger the update (MutationObserver doesn't always trigger in tests)
|
||||
(component as any).updateIframeSrc();
|
||||
it('should add participant-name as query parameter', () => {
|
||||
const participantName = 'John Doe';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
expect(iframe?.src).toEqual(`${roomUrl}&user=testUser`);
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
||||
});
|
||||
|
||||
it('should add e2ee-key as query parameter', () => {
|
||||
const e2eeKey = 'secret-encryption-key-123';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
|
||||
});
|
||||
|
||||
it('should add leave-redirect-url as query parameter', () => {
|
||||
const redirectUrl = 'https://example.com/goodbye';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
|
||||
});
|
||||
|
||||
it('should add show-only-recordings as query parameter', () => {
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'true');
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('true');
|
||||
});
|
||||
|
||||
it('should add multiple optional attributes as query parameters', () => {
|
||||
const participantName = 'Jane Smith';
|
||||
const e2eeKey = 'encryption-key-456';
|
||||
const redirectUrl = 'https://example.com/thanks';
|
||||
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
||||
component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
|
||||
component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
|
||||
component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'false');
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
||||
expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
|
||||
expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
|
||||
expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('false');
|
||||
});
|
||||
|
||||
it('should NOT add room-url or recording-url as query parameters', () => {
|
||||
const roomUrl = 'https://example.com/room/testRoom?secret=abc';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
// room-url should not be in query params (it's the base URL)
|
||||
expect(url.searchParams.has(WebComponentProperty.ROOM_URL)).toBe(false);
|
||||
expect(url.searchParams.has(WebComponentProperty.RECORDING_URL)).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve existing query parameters in room-url', () => {
|
||||
const roomUrl = 'https://example.com/room/testRoom?secret=abc123&role=moderator';
|
||||
const participantName = 'Alice';
|
||||
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
// Original query params should be preserved
|
||||
expect(url.searchParams.get('secret')).toBe('abc123');
|
||||
expect(url.searchParams.get('role')).toBe('moderator');
|
||||
// New param should be added
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract origin from room-url and set as allowed origin', () => {
|
||||
const domain = 'https://example.com';
|
||||
const roomUrl = `${domain}/room/testRoom-123?secret=123456`;
|
||||
component.setAttribute('room-url', roomUrl);
|
||||
// ==========================================
|
||||
// CUSTOM/UNKNOWN ATTRIBUTES
|
||||
// ==========================================
|
||||
describe('Custom Attributes as Query Parameters', () => {
|
||||
it('should add custom attributes as query parameters', () => {
|
||||
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute('custom-attr', 'custom-value');
|
||||
component.setAttribute('another-param', 'another-value');
|
||||
|
||||
// Trigger update
|
||||
(component as any).updateIframeSrc();
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
// Check if origin was extracted and set
|
||||
expect((component as any).targetIframeOrigin).toBe(domain);
|
||||
expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
|
||||
expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
expect(url.searchParams.get('custom-attr')).toBe('custom-value');
|
||||
expect(url.searchParams.get('another-param')).toBe('another-value');
|
||||
});
|
||||
|
||||
it('should handle attribute names with special characters', () => {
|
||||
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute('data-test-id', '12345');
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
expect(url.searchParams.get('data-test-id')).toBe('12345');
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// EDGE CASES
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string attributes', () => {
|
||||
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, '');
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
// Empty string should still be added as query param
|
||||
expect(url.searchParams.has(WebComponentProperty.PARTICIPANT_NAME)).toBe(true);
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe('');
|
||||
});
|
||||
|
||||
it('should handle special characters in attribute values', () => {
|
||||
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||
const specialName = 'User Name With Spaces & Special=Chars';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, specialName);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
// Should be URL-encoded properly
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(specialName);
|
||||
});
|
||||
|
||||
it('should handle updating attributes after initial render', () => {
|
||||
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const initialSrc = component.shadowRoot?.querySelector('iframe')?.src;
|
||||
|
||||
// Update an attribute
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, 'Updated Name');
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const updatedSrc = component.shadowRoot?.querySelector('iframe')?.src;
|
||||
|
||||
expect(initialSrc).not.toBe(updatedSrc);
|
||||
|
||||
const url = new URL(updatedSrc || '');
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe('Updated Name');
|
||||
});
|
||||
|
||||
it('should handle invalid URL gracefully', () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Set an invalid URL
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, 'not-a-valid-url');
|
||||
|
||||
// Call updateIframeSrc directly - it should catch the error and log it
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
// Verify error was logged with the invalid URL
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Invalid URL provided: not-a-valid-url', expect.anything());
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// INTEGRATION TESTS
|
||||
// ==========================================
|
||||
describe('Integration Tests', () => {
|
||||
it('should handle complete real-world scenario with room-url and multiple attributes', () => {
|
||||
const roomUrl = 'https://meet.example.com/room/team-standup?secret=xyz789';
|
||||
const participantName = 'John Doe';
|
||||
const e2eeKey = 'my-secure-key';
|
||||
const redirectUrl = 'https://example.com/dashboard';
|
||||
|
||||
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
||||
component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
|
||||
component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
// Verify base URL
|
||||
expect(url.origin).toBe('https://meet.example.com');
|
||||
expect(url.pathname).toBe('/room/team-standup');
|
||||
|
||||
// Verify all query parameters
|
||||
expect(url.searchParams.get('secret')).toBe('xyz789');
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
||||
expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
|
||||
expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
|
||||
|
||||
// Verify origin was set correctly
|
||||
expect((component as any).targetIframeOrigin).toBe('https://meet.example.com');
|
||||
});
|
||||
|
||||
it('should handle complete real-world scenario with recording-url', () => {
|
||||
const recordingUrl = 'https://recordings.example.com/view/rec-20231115-abc123';
|
||||
const participantName = 'Viewer';
|
||||
|
||||
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
||||
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
||||
component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'true');
|
||||
|
||||
(component as any).updateIframeSrc();
|
||||
|
||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||
const url = new URL(iframe?.src || '');
|
||||
|
||||
// Verify base URL
|
||||
expect(url.origin).toBe('https://recordings.example.com');
|
||||
expect(url.pathname).toBe('/view/rec-20231115-abc123');
|
||||
|
||||
// Verify query parameters
|
||||
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
||||
expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('true');
|
||||
|
||||
// Verify origin was set correctly
|
||||
expect((component as any).targetIframeOrigin).toBe('https://recordings.example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,6 +14,12 @@ export enum WebComponentProperty {
|
||||
*/
|
||||
PARTICIPANT_NAME = 'participant-name',
|
||||
|
||||
/**
|
||||
* Secret key for end-to-end encryption (E2EE).
|
||||
* If provided, the participant will join the meeting using E2EE key.
|
||||
*/
|
||||
E2EE_KEY = 'e2ee-key',
|
||||
|
||||
/**
|
||||
* URL to redirect to when leaving the meeting.
|
||||
* Redirection occurs after the **`CLOSED` event** fires.
|
||||
|
||||
77
meet.sh
77
meet.sh
@ -622,30 +622,42 @@ build_webcomponent_doc() {
|
||||
mkdir -p "$output_dir"
|
||||
fi
|
||||
|
||||
if [ -f "docs/webcomponent-events.md" ] && [ -f "docs/webcomponent-commands.md" ] && [ -f "docs/webcomponent-attributes.md" ]; then
|
||||
if [ -f "docs/webcomponent-events.md" ] && \
|
||||
[ -f "docs/webcomponent-commands.md" ] && \
|
||||
[ -f "docs/webcomponent-attributes.md" ]; then
|
||||
echo -e "${GREEN}Copying documentation to: $output_dir${NC}"
|
||||
cp docs/webcomponent-events.md "$output_dir/webcomponent-events.md"
|
||||
cp docs/webcomponent-commands.md "$output_dir/webcomponent-commands.md"
|
||||
cp docs/webcomponent-attributes.md "$output_dir/webcomponent-attributes.md"
|
||||
cp docs/webcomponent-{events,commands,attributes}.md "$output_dir"/
|
||||
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
|
||||
rm -f docs/webcomponent-{events,commands,attributes}.md
|
||||
else
|
||||
echo -e "${RED}Error: Documentation files not found in docs/ directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}No output directory specified. Documentation remains in docs/ directory.${NC}"
|
||||
output_dir="docs"
|
||||
fi
|
||||
|
||||
local abs_path
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
abs_path=$(realpath "$output_dir")
|
||||
elif command -v readlink >/dev/null 2>&1; then
|
||||
abs_path=$(readlink -f "$output_dir" 2>/dev/null || (cd "$output_dir" && pwd))
|
||||
else
|
||||
abs_path=$(cd "$output_dir" && pwd)
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}✓ Webcomponent documentation generated successfully!${NC}"
|
||||
echo -e "${YELLOW}Output directory: $output_dir${NC}"
|
||||
rm -f docs/webcomponent-events.md docs/webcomponent-commands.md docs/webcomponent-attributes.md
|
||||
echo -e "${YELLOW}Output directory: ${abs_path}${NC}"
|
||||
}
|
||||
|
||||
|
||||
# Build REST API documentation
|
||||
build_rest_api_doc() {
|
||||
local output_dir="$1"
|
||||
local output_target="$1"
|
||||
CE_REST_API_DOC_PATH="meet-ce/backend/public/openapi/"
|
||||
|
||||
echo -e "${BLUE}=====================================${NC}"
|
||||
echo -e "${BLUE} Building REST API Docs${NC}"
|
||||
echo -e "${BLUE}=====================================${NC}"
|
||||
@ -653,31 +665,56 @@ build_rest_api_doc() {
|
||||
|
||||
check_pnpm
|
||||
|
||||
# Solo instalar si no existen dependencias locales del backend
|
||||
if [ ! -d "node_modules" ] || [ ! -d "meet-ce/backend/node_modules" ]; then
|
||||
echo -e "${YELLOW}Backend dependencies not found. Installing minimal backend deps...${NC}"
|
||||
pnpm --filter @openvidu-meet/backend install
|
||||
else
|
||||
echo -e "${GREEN}Backend dependencies already present. Skipping install.${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Generating REST API documentation...${NC}"
|
||||
pnpm run build:rest-api-docs
|
||||
|
||||
if [ -n "$output_dir" ]; then
|
||||
output_dir="${output_dir%/}"
|
||||
# Determinar si el parámetro es archivo o directorio
|
||||
local output_dir output_file
|
||||
if [[ "$output_target" =~ \.html$ ]]; then
|
||||
output_dir=$(dirname "$output_target")
|
||||
output_file="$output_target"
|
||||
else
|
||||
output_dir="${output_target%/}"
|
||||
output_file="$output_dir/public.html"
|
||||
fi
|
||||
|
||||
if [ ! -d "$output_dir" ]; then
|
||||
echo -e "${YELLOW}Creating output directory: $output_dir${NC}"
|
||||
mkdir -p "$output_dir"
|
||||
fi
|
||||
# Crear carpeta contenedora si no existe
|
||||
if [ ! -d "$output_dir" ]; then
|
||||
echo -e "${YELLOW}Creating output directory: $output_dir${NC}"
|
||||
mkdir -p "$output_dir"
|
||||
fi
|
||||
|
||||
if [ -f "$CE_REST_API_DOC_PATH/public.html" ]; then
|
||||
echo -e "${GREEN}Copying REST API documentation to: $output_dir${NC}"
|
||||
cp "$CE_REST_API_DOC_PATH/public.html" "$output_dir/public.html"
|
||||
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
|
||||
else
|
||||
echo -e "${RED}Error: REST API documentation files not found${NC}"
|
||||
# Preguntar si el archivo ya existe
|
||||
if [ -f "$output_file" ]; then
|
||||
echo -e "${YELLOW}Warning: '$output_file' already exists.${NC}"
|
||||
read -rp "Do you want to overwrite it? [y/N]: " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}Operation cancelled by user.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copiar documentación
|
||||
if [ -f "$CE_REST_API_DOC_PATH/public.html" ]; then
|
||||
echo -e "${GREEN}Copying REST API documentation to: $output_file${NC}"
|
||||
cp "$CE_REST_API_DOC_PATH/public.html" "$output_file"
|
||||
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}No output directory specified. Documentation remains in backend/ directory.${NC}"
|
||||
echo -e "${RED}Error: REST API documentation files not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}✓ REST API documentation generated successfully!${NC}"
|
||||
echo -e "${YELLOW}Output file: $(cd "$(dirname "$output_file")" && pwd)/$(basename "$output_file")${NC}"
|
||||
}
|
||||
|
||||
# Clone private meet-pro repository into repository root
|
||||
|
||||
@ -2,42 +2,42 @@
|
||||
"folders": [
|
||||
{
|
||||
"name": "openvidu-components-angular",
|
||||
"path": "../openvidu/openvidu-components-angular",
|
||||
"path": "../openvidu/openvidu-components-angular"
|
||||
},
|
||||
{
|
||||
"name": "openvidu-meet (root)",
|
||||
"path": ".",
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"name": "openvidu-meet (CE)",
|
||||
"path": "meet-ce",
|
||||
"path": "meet-ce"
|
||||
},
|
||||
{
|
||||
"name": "openvidu-meet (PRO)",
|
||||
"path": "meet-pro",
|
||||
"path": "meet-pro"
|
||||
},
|
||||
{
|
||||
"name": "shared-meet-components",
|
||||
"path": "meet-ce/frontend/projects/shared-meet-components",
|
||||
"path": "meet-ce/frontend/projects/shared-meet-components"
|
||||
},
|
||||
{
|
||||
"name": "meet-testapp",
|
||||
"path": "testapp",
|
||||
"path": "testapp"
|
||||
},
|
||||
{
|
||||
"name": "meet-webcomponent",
|
||||
"path": "meet-ce/frontend/webcomponent",
|
||||
},
|
||||
"path": "meet-ce/frontend/webcomponent"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
"**/meet-ce": true,
|
||||
"**/meet-pro": true,
|
||||
"**/webcomponent": true,
|
||||
"**/frontend/webcomponent": true,
|
||||
"**/webhooks-snippets": false,
|
||||
"**/testapp": true,
|
||||
"**/.angular": true,
|
||||
"**/public": true,
|
||||
"**/public": false,
|
||||
"**/dist": false,
|
||||
"**/node_modules": true,
|
||||
"**/test-results": true,
|
||||
|
||||
494
pnpm-lock.yaml
generated
494
pnpm-lock.yaml
generated
@ -501,6 +501,365 @@ importers:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
meet-pro/backend:
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: 3.846.0
|
||||
version: 3.846.0
|
||||
'@azure/storage-blob':
|
||||
specifier: 12.27.0
|
||||
version: 12.27.0
|
||||
'@google-cloud/storage':
|
||||
specifier: 7.17.1
|
||||
version: 7.17.1(encoding@0.1.13)
|
||||
'@openvidu-meet-pro/typings':
|
||||
specifier: workspace:*
|
||||
version: link:../typings
|
||||
'@openvidu-meet/backend':
|
||||
specifier: workspace:*
|
||||
version: link:../../meet-ce/backend
|
||||
'@sesamecare-oss/redlock':
|
||||
specifier: 1.4.0
|
||||
version: 1.4.0(ioredis@5.6.1)
|
||||
archiver:
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1
|
||||
bcrypt:
|
||||
specifier: 5.1.1
|
||||
version: 5.1.1(encoding@0.1.13)
|
||||
body-parser:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
chalk:
|
||||
specifier: 5.6.2
|
||||
version: 5.6.2
|
||||
cookie-parser:
|
||||
specifier: 1.4.7
|
||||
version: 1.4.7
|
||||
cors:
|
||||
specifier: 2.8.5
|
||||
version: 2.8.5
|
||||
cron:
|
||||
specifier: 4.3.3
|
||||
version: 4.3.3
|
||||
dotenv:
|
||||
specifier: 16.6.1
|
||||
version: 16.6.1
|
||||
express:
|
||||
specifier: 4.21.2
|
||||
version: 4.21.2
|
||||
express-rate-limit:
|
||||
specifier: 7.5.1
|
||||
version: 7.5.1(express@4.21.2)
|
||||
inversify:
|
||||
specifier: 6.2.2
|
||||
version: 6.2.2(reflect-metadata@0.2.2)
|
||||
ioredis:
|
||||
specifier: 5.6.1
|
||||
version: 5.6.1
|
||||
jwt-decode:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
livekit-server-sdk:
|
||||
specifier: 2.13.1
|
||||
version: 2.13.1
|
||||
ms:
|
||||
specifier: 2.1.3
|
||||
version: 2.1.3
|
||||
uid:
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2
|
||||
winston:
|
||||
specifier: 3.18.3
|
||||
version: 3.18.3
|
||||
yamljs:
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/archiver':
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
'@types/bcrypt':
|
||||
specifier: 5.0.2
|
||||
version: 5.0.2
|
||||
'@types/cookie-parser':
|
||||
specifier: 1.4.9
|
||||
version: 1.4.9(@types/express@4.17.23)
|
||||
'@types/cors':
|
||||
specifier: 2.8.19
|
||||
version: 2.8.19
|
||||
'@types/express':
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
'@types/jest':
|
||||
specifier: 29.5.14
|
||||
version: 29.5.14
|
||||
'@types/ms':
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
'@types/node':
|
||||
specifier: 22.16.4
|
||||
version: 22.16.4
|
||||
'@types/supertest':
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
'@types/unzipper':
|
||||
specifier: 0.10.11
|
||||
version: 0.10.11
|
||||
'@types/validator':
|
||||
specifier: 13.15.2
|
||||
version: 13.15.2
|
||||
'@types/yamljs':
|
||||
specifier: 0.2.34
|
||||
version: 0.2.34
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 6.21.0
|
||||
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 6.21.0
|
||||
version: 6.21.0(eslint@8.57.1)(typescript@5.9.2)
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
eslint:
|
||||
specifier: 8.57.1
|
||||
version: 8.57.1
|
||||
eslint-config-prettier:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0(eslint@8.57.1)
|
||||
jest:
|
||||
specifier: 29.7.0
|
||||
version: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.2))
|
||||
jest-fetch-mock:
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3(encoding@0.1.13)
|
||||
jest-junit:
|
||||
specifier: 16.0.0
|
||||
version: 16.0.0
|
||||
nodemon:
|
||||
specifier: 3.1.10
|
||||
version: 3.1.10
|
||||
openapi-generate-html:
|
||||
specifier: 0.5.3
|
||||
version: 0.5.3(@types/node@22.16.4)
|
||||
prettier:
|
||||
specifier: 3.6.2
|
||||
version: 3.6.2
|
||||
supertest:
|
||||
specifier: 7.1.3
|
||||
version: 7.1.3
|
||||
ts-jest:
|
||||
specifier: 29.4.0
|
||||
version: 29.4.0(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.2)))(typescript@5.9.2)
|
||||
ts-jest-resolver:
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
tsx:
|
||||
specifier: 4.20.3
|
||||
version: 4.20.3
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
unzipper:
|
||||
specifier: 0.12.3
|
||||
version: 0.12.3
|
||||
|
||||
meet-pro/frontend:
|
||||
dependencies:
|
||||
'@angular/animations':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||
'@angular/cdk':
|
||||
specifier: 20.2.9
|
||||
version: 20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||
'@angular/common':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||
'@angular/compiler':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4
|
||||
'@angular/core':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||
'@angular/forms':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
||||
'@angular/material':
|
||||
specifier: 20.2.9
|
||||
version: 20.2.9(b517547b325ffc8400ae4cda6a618bfd)
|
||||
'@angular/platform-browser':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||
'@angular/platform-browser-dynamic':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))
|
||||
'@angular/router':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
||||
'@livekit/track-processors':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.11(@types/dom-mediacapture-record@1.0.22))
|
||||
'@openvidu-meet/shared-components':
|
||||
specifier: workspace:*
|
||||
version: link:../../meet-ce/frontend/projects/shared-meet-components
|
||||
'@openvidu-meet/typings':
|
||||
specifier: workspace:*
|
||||
version: link:../../meet-ce/typings
|
||||
autolinker:
|
||||
specifier: 4.1.5
|
||||
version: 4.1.5
|
||||
core-js:
|
||||
specifier: 3.45.1
|
||||
version: 3.45.1
|
||||
jwt-decode:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
livekit-client:
|
||||
specifier: 2.15.11
|
||||
version: 2.15.11(@types/dom-mediacapture-record@1.0.22)
|
||||
openvidu-components-angular:
|
||||
specifier: workspace:*
|
||||
version: link:../../../openvidu/openvidu-components-angular/projects/openvidu-components-angular
|
||||
rxjs:
|
||||
specifier: 7.8.2
|
||||
version: 7.8.2
|
||||
tslib:
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
unique-names-generator:
|
||||
specifier: 4.7.1
|
||||
version: 4.7.1
|
||||
zone.js:
|
||||
specifier: 0.15.1
|
||||
version: 0.15.1
|
||||
devDependencies:
|
||||
'@angular-builders/custom-webpack':
|
||||
specifier: 20.0.0
|
||||
version: 20.0.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
||||
'@angular-devkit/build-angular':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(tsx@4.20.3)(typescript@5.9.2)
|
||||
'@angular-eslint/builder':
|
||||
specifier: 20.3.0
|
||||
version: 20.3.0(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@angular-eslint/eslint-plugin':
|
||||
specifier: 20.3.0
|
||||
version: 20.3.0(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@angular-eslint/eslint-plugin-template':
|
||||
specifier: 20.3.0
|
||||
version: 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.46.1)(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@angular-eslint/schematics':
|
||||
specifier: 20.3.0
|
||||
version: 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.46.1)(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@angular-eslint/template-parser':
|
||||
specifier: 20.3.0
|
||||
version: 20.3.0(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@angular/cli':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@types/node@22.18.8)(chokidar@4.0.3)
|
||||
'@angular/compiler-cli':
|
||||
specifier: 20.3.4
|
||||
version: 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||
'@types/chai':
|
||||
specifier: 4.3.20
|
||||
version: 4.3.20
|
||||
'@types/fluent-ffmpeg':
|
||||
specifier: 2.1.27
|
||||
version: 2.1.27
|
||||
'@types/jasmine':
|
||||
specifier: 5.1.9
|
||||
version: 5.1.9
|
||||
'@types/mocha':
|
||||
specifier: 9.1.1
|
||||
version: 9.1.1
|
||||
'@types/node':
|
||||
specifier: 22.18.8
|
||||
version: 22.18.8
|
||||
'@types/pixelmatch':
|
||||
specifier: 5.2.6
|
||||
version: 5.2.6
|
||||
'@types/pngjs':
|
||||
specifier: 6.0.5
|
||||
version: 6.0.5
|
||||
'@types/selenium-webdriver':
|
||||
specifier: 4.35.1
|
||||
version: 4.35.1
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 8.46.1
|
||||
version: 8.46.1(@typescript-eslint/parser@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 8.46.1
|
||||
version: 8.46.1(eslint@8.57.1)(typescript@5.9.2)
|
||||
chai:
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0
|
||||
chromedriver:
|
||||
specifier: 141.0.0
|
||||
version: 141.0.0
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
eslint:
|
||||
specifier: 8.57.1
|
||||
version: 8.57.1
|
||||
eslint-config-prettier:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0(eslint@8.57.1)
|
||||
fluent-ffmpeg:
|
||||
specifier: 2.1.3
|
||||
version: 2.1.3
|
||||
jasmine-core:
|
||||
specifier: 5.6.0
|
||||
version: 5.6.0
|
||||
jasmine-spec-reporter:
|
||||
specifier: 7.0.0
|
||||
version: 7.0.0
|
||||
karma:
|
||||
specifier: 6.4.4
|
||||
version: 6.4.4
|
||||
karma-chrome-launcher:
|
||||
specifier: 3.2.0
|
||||
version: 3.2.0
|
||||
karma-coverage:
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1
|
||||
karma-jasmine:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0(karma@6.4.4)
|
||||
karma-jasmine-html-reporter:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0(jasmine-core@5.6.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4)
|
||||
mocha:
|
||||
specifier: 10.7.3
|
||||
version: 10.7.3
|
||||
ng-packagr:
|
||||
specifier: 20.3.0
|
||||
version: 20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2)
|
||||
prettier:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
selenium-webdriver:
|
||||
specifier: 4.25.0
|
||||
version: 4.25.0
|
||||
ts-node:
|
||||
specifier: 10.9.2
|
||||
version: 10.9.2(@types/node@22.18.8)(typescript@5.9.2)
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
meet-pro/typings:
|
||||
devDependencies:
|
||||
'@openvidu-meet/typings':
|
||||
specifier: workspace:*
|
||||
version: link:../../meet-ce/typings
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
testapp:
|
||||
dependencies:
|
||||
'@openvidu-meet/typings':
|
||||
@ -6570,6 +6929,7 @@ packages:
|
||||
|
||||
livekit-client@2.15.11:
|
||||
resolution: {integrity: sha512-9cHdAbSibPGyt7wWM+GAUswIOuklQHF9y561Oruzh0nNFNvRzMsE10oqJvjs0k6s2Jl+j/Z5Ar90bzVwLpu1yg==}
|
||||
deprecated: Compatibility issue around AbortSignal.any usage, use >=2.15.12 instead
|
||||
peerDependencies:
|
||||
'@types/dom-mediacapture-record': ^1
|
||||
|
||||
@ -9322,6 +9682,59 @@ snapshots:
|
||||
- webpack-cli
|
||||
- yaml
|
||||
|
||||
'@angular-builders/custom-webpack@20.0.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@angular-builders/common': 4.0.0(@types/node@22.18.8)(chokidar@4.0.3)(typescript@5.9.2)
|
||||
'@angular-devkit/architect': 0.2003.5(chokidar@4.0.3)
|
||||
'@angular-devkit/build-angular': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(tsx@4.20.3)(typescript@5.9.2)
|
||||
'@angular-devkit/core': 20.3.5(chokidar@4.0.3)
|
||||
'@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
||||
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||
lodash: 4.17.21
|
||||
webpack-merge: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@angular/compiler'
|
||||
- '@angular/core'
|
||||
- '@angular/localize'
|
||||
- '@angular/platform-browser'
|
||||
- '@angular/platform-server'
|
||||
- '@angular/service-worker'
|
||||
- '@angular/ssr'
|
||||
- '@rspack/core'
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
- '@types/node'
|
||||
- '@web/test-runner'
|
||||
- browser-sync
|
||||
- bufferutil
|
||||
- chokidar
|
||||
- debug
|
||||
- html-webpack-plugin
|
||||
- jest
|
||||
- jest-environment-jsdom
|
||||
- jiti
|
||||
- karma
|
||||
- less
|
||||
- lightningcss
|
||||
- ng-packagr
|
||||
- node-sass
|
||||
- postcss
|
||||
- protractor
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- tailwindcss
|
||||
- terser
|
||||
- tslib
|
||||
- tsx
|
||||
- typescript
|
||||
- uglify-js
|
||||
- utf-8-validate
|
||||
- vitest
|
||||
- webpack-cli
|
||||
- yaml
|
||||
|
||||
'@angular-devkit/architect@0.2003.4(chokidar@4.0.3)':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
||||
@ -9340,7 +9753,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
||||
'@angular-devkit/build-webpack': 0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2))(webpack@5.101.2)
|
||||
'@angular-devkit/build-webpack': 0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)))(webpack@5.101.2(esbuild@0.25.9))
|
||||
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
||||
'@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.0)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
||||
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||
@ -9354,13 +9767,13 @@ snapshots:
|
||||
'@babel/preset-env': 7.28.3(@babel/core@7.28.3)
|
||||
'@babel/runtime': 7.28.3
|
||||
'@discoveryjs/json-ext': 0.6.3
|
||||
'@ngtools/webpack': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2)
|
||||
'@ngtools/webpack': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))
|
||||
ansi-colors: 4.1.3
|
||||
autoprefixer: 10.4.21(postcss@8.5.6)
|
||||
babel-loader: 10.0.0(@babel/core@7.28.3)(webpack@5.101.2)
|
||||
babel-loader: 10.0.0(@babel/core@7.28.3)(webpack@5.101.2(esbuild@0.25.9))
|
||||
browserslist: 4.26.3
|
||||
copy-webpack-plugin: 13.0.1(webpack@5.101.2)
|
||||
css-loader: 7.1.2(webpack@5.101.2)
|
||||
copy-webpack-plugin: 13.0.1(webpack@5.101.2(esbuild@0.25.9))
|
||||
css-loader: 7.1.2(webpack@5.101.2(esbuild@0.25.9))
|
||||
esbuild-wasm: 0.25.9
|
||||
fast-glob: 3.3.3
|
||||
http-proxy-middleware: 3.0.5
|
||||
@ -9368,32 +9781,32 @@ snapshots:
|
||||
jsonc-parser: 3.3.1
|
||||
karma-source-map-support: 1.4.0
|
||||
less: 4.4.0
|
||||
less-loader: 12.3.0(less@4.4.0)(webpack@5.101.2)
|
||||
license-webpack-plugin: 4.0.2(webpack@5.101.2)
|
||||
less-loader: 12.3.0(less@4.4.0)(webpack@5.101.2(esbuild@0.25.9))
|
||||
license-webpack-plugin: 4.0.2(webpack@5.101.2(esbuild@0.25.9))
|
||||
loader-utils: 3.3.1
|
||||
mini-css-extract-plugin: 2.9.4(webpack@5.101.2)
|
||||
mini-css-extract-plugin: 2.9.4(webpack@5.101.2(esbuild@0.25.9))
|
||||
open: 10.2.0
|
||||
ora: 8.2.0
|
||||
picomatch: 4.0.3
|
||||
piscina: 5.1.3
|
||||
postcss: 8.5.6
|
||||
postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2)
|
||||
postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))
|
||||
resolve-url-loader: 5.0.0
|
||||
rxjs: 7.8.2
|
||||
sass: 1.90.0
|
||||
sass-loader: 16.0.5(sass@1.90.0)(webpack@5.101.2)
|
||||
sass-loader: 16.0.5(sass@1.90.0)(webpack@5.101.2(esbuild@0.25.9))
|
||||
semver: 7.7.2
|
||||
source-map-loader: 5.0.0(webpack@5.101.2)
|
||||
source-map-loader: 5.0.0(webpack@5.101.2(esbuild@0.25.9))
|
||||
source-map-support: 0.5.21
|
||||
terser: 5.43.1
|
||||
tree-kill: 1.2.2
|
||||
tslib: 2.8.1
|
||||
typescript: 5.9.2
|
||||
webpack: 5.101.2(esbuild@0.25.9)
|
||||
webpack-dev-middleware: 7.4.2(webpack@5.101.2)
|
||||
webpack-dev-server: 5.2.2(webpack@5.101.2)
|
||||
webpack-dev-middleware: 7.4.2(webpack@5.101.2(esbuild@0.25.9))
|
||||
webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
|
||||
webpack-merge: 6.0.1
|
||||
webpack-subresource-integrity: 5.1.0(webpack@5.101.2)
|
||||
webpack-subresource-integrity: 5.1.0(webpack@5.101.2(esbuild@0.25.9))
|
||||
optionalDependencies:
|
||||
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||
'@angular/platform-browser': 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||
@ -9426,12 +9839,12 @@ snapshots:
|
||||
- webpack-cli
|
||||
- yaml
|
||||
|
||||
'@angular-devkit/build-webpack@0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2))(webpack@5.101.2)':
|
||||
'@angular-devkit/build-webpack@0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)))(webpack@5.101.2(esbuild@0.25.9))':
|
||||
dependencies:
|
||||
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
||||
rxjs: 7.8.2
|
||||
webpack: 5.101.2(esbuild@0.25.9)
|
||||
webpack-dev-server: 5.2.2(webpack@5.101.2)
|
||||
webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
|
||||
transitivePeerDependencies:
|
||||
- chokidar
|
||||
|
||||
@ -12197,7 +12610,7 @@ snapshots:
|
||||
'@napi-rs/nice-win32-x64-msvc': 1.1.1
|
||||
optional: true
|
||||
|
||||
'@ngtools/webpack@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2)':
|
||||
'@ngtools/webpack@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))':
|
||||
dependencies:
|
||||
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
@ -13528,6 +13941,10 @@ snapshots:
|
||||
dependencies:
|
||||
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
|
||||
|
||||
'@vitejs/plugin-basic-ssl@2.1.0(vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3))':
|
||||
dependencies:
|
||||
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
dependencies:
|
||||
'@webassemblyjs/helper-numbers': 1.13.2
|
||||
@ -13899,7 +14316,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2):
|
||||
babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.3
|
||||
find-up: 5.0.0
|
||||
@ -14486,7 +14903,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-what: 3.14.1
|
||||
|
||||
copy-webpack-plugin@13.0.1(webpack@5.101.2):
|
||||
copy-webpack-plugin@13.0.1(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
glob-parent: 6.0.2
|
||||
normalize-path: 3.0.0
|
||||
@ -14597,7 +15014,7 @@ snapshots:
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
|
||||
css-loader@7.1.2(webpack@5.101.2):
|
||||
css-loader@7.1.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
icss-utils: 5.1.0(postcss@8.5.6)
|
||||
postcss: 8.5.6
|
||||
@ -17148,7 +17565,7 @@ snapshots:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
|
||||
less-loader@12.3.0(less@4.4.0)(webpack@5.101.2):
|
||||
less-loader@12.3.0(less@4.4.0)(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
less: 4.4.0
|
||||
optionalDependencies:
|
||||
@ -17189,7 +17606,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
license-webpack-plugin@4.0.2(webpack@5.101.2):
|
||||
license-webpack-plugin@4.0.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
webpack-sources: 3.3.3
|
||||
optionalDependencies:
|
||||
@ -17445,7 +17862,7 @@ snapshots:
|
||||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
mini-css-extract-plugin@2.9.4(webpack@5.101.2):
|
||||
mini-css-extract-plugin@2.9.4(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.0
|
||||
@ -18153,7 +18570,7 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
ts-node: 10.9.2(@types/node@22.18.8)(typescript@5.7.3)
|
||||
|
||||
postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2):
|
||||
postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
cosmiconfig: 9.0.0(typescript@5.9.2)
|
||||
jiti: 1.21.7
|
||||
@ -18730,7 +19147,7 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass-loader@16.0.5(sass@1.90.0)(webpack@5.101.2):
|
||||
sass-loader@16.0.5(sass@1.90.0)(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
neo-async: 2.6.2
|
||||
optionalDependencies:
|
||||
@ -19038,7 +19455,7 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-loader@5.0.0(webpack@5.101.2):
|
||||
source-map-loader@5.0.0(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
source-map-js: 1.2.1
|
||||
@ -19812,6 +20229,23 @@ snapshots:
|
||||
terser: 5.44.0
|
||||
tsx: 4.20.3
|
||||
|
||||
vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3):
|
||||
dependencies:
|
||||
esbuild: 0.25.10
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.52.3
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 22.18.8
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
less: 4.4.2
|
||||
sass: 1.90.0
|
||||
terser: 5.44.0
|
||||
tsx: 4.20.3
|
||||
|
||||
void-elements@2.0.1: {}
|
||||
|
||||
w3c-xmlserializer@4.0.0:
|
||||
@ -19848,7 +20282,7 @@ snapshots:
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webpack-dev-middleware@7.4.2(webpack@5.101.2):
|
||||
webpack-dev-middleware@7.4.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
memfs: 4.49.0
|
||||
@ -19859,7 +20293,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
webpack: 5.101.2(esbuild@0.25.9)
|
||||
|
||||
webpack-dev-server@5.2.2(webpack@5.101.2):
|
||||
webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
'@types/bonjour': 3.5.13
|
||||
'@types/connect-history-api-fallback': 1.5.4
|
||||
@ -19887,7 +20321,7 @@ snapshots:
|
||||
serve-index: 1.9.1
|
||||
sockjs: 0.3.24
|
||||
spdy: 4.0.2
|
||||
webpack-dev-middleware: 7.4.2(webpack@5.101.2)
|
||||
webpack-dev-middleware: 7.4.2(webpack@5.101.2(esbuild@0.25.9))
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
webpack: 5.101.2(esbuild@0.25.9)
|
||||
@ -19905,7 +20339,7 @@ snapshots:
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
|
||||
webpack-subresource-integrity@5.1.0(webpack@5.101.2):
|
||||
webpack-subresource-integrity@5.1.0(webpack@5.101.2(esbuild@0.25.9)):
|
||||
dependencies:
|
||||
typed-assert: 1.0.9
|
||||
webpack: 5.101.2(esbuild@0.25.9)
|
||||
|
||||
219
testapp/public/js/webcomponent.js
Normal file
219
testapp/public/js/webcomponent.js
Normal file
@ -0,0 +1,219 @@
|
||||
"use strict";
|
||||
const socket = window.io();
|
||||
let meet;
|
||||
let roomId;
|
||||
let showAllWebhooksCheckbox;
|
||||
/**
|
||||
* Add a component event to the events log
|
||||
*/
|
||||
const addEventToLog = (eventType, eventMessage) => {
|
||||
const eventsList = document.getElementById('events-list');
|
||||
if (eventsList) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `event-${eventType}`;
|
||||
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
|
||||
eventsList.insertBefore(li, eventsList.firstChild);
|
||||
}
|
||||
};
|
||||
const escapeHtml = (unsafe) => {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
const getWebhookEventsFromStorage = (roomId) => {
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const map = JSON.parse(data);
|
||||
return map[roomId] || [];
|
||||
};
|
||||
const saveWebhookEventToStorage = (roomId, event) => {
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
const map = data ? JSON.parse(data) : {};
|
||||
if (!map[roomId]) {
|
||||
map[roomId] = [];
|
||||
}
|
||||
map[roomId].push(event);
|
||||
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
|
||||
};
|
||||
const clearWebhookEventsByRoom = (roomId) => {
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
if (!data)
|
||||
return;
|
||||
const map = JSON.parse(data);
|
||||
if (map[roomId]) {
|
||||
map[roomId] = [];
|
||||
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
|
||||
}
|
||||
};
|
||||
const shouldShowWebhook = (event) => {
|
||||
return (showAllWebhooksCheckbox === null || showAllWebhooksCheckbox === void 0 ? void 0 : showAllWebhooksCheckbox.checked) || event.data.roomId === roomId;
|
||||
};
|
||||
const listenWebhookServerEvents = () => {
|
||||
socket.on('webhookEvent', (event) => {
|
||||
console.log('Webhook received:', event);
|
||||
const webhookRoomId = event.data.roomId;
|
||||
if (webhookRoomId) {
|
||||
saveWebhookEventToStorage(webhookRoomId, event);
|
||||
}
|
||||
if (!shouldShowWebhook(event)) {
|
||||
console.log('Ignoring webhook event:', event);
|
||||
return;
|
||||
}
|
||||
addWebhookEventElement(event);
|
||||
// Clean up the previous events
|
||||
const isMeetingEnded = event.event === 'meetingEnded';
|
||||
if (isMeetingEnded)
|
||||
clearWebhookEventsByRoom(webhookRoomId);
|
||||
});
|
||||
};
|
||||
const renderStoredWebhookEvents = (roomId) => {
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
while (webhookLogList.firstChild) {
|
||||
webhookLogList.removeChild(webhookLogList.firstChild);
|
||||
}
|
||||
}
|
||||
const events = getWebhookEventsFromStorage(roomId);
|
||||
events.forEach((event) => addWebhookEventElement(event));
|
||||
};
|
||||
const addWebhookEventElement = (event) => {
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
// Create unique IDs for this accordion item
|
||||
const itemId = event.creationDate;
|
||||
const headerClassName = `webhook-${event.event}`;
|
||||
const collapseId = `collapse-${itemId}`;
|
||||
// Create accordion item container
|
||||
const accordionItem = document.createElement('div');
|
||||
accordionItem.className = 'accordion-item';
|
||||
// Create header
|
||||
const header = document.createElement('h2');
|
||||
header.classList.add(headerClassName, 'accordion-header');
|
||||
// Create header button
|
||||
const button = document.createElement('button');
|
||||
button.className = 'accordion-button';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-bs-toggle', 'collapse');
|
||||
button.setAttribute('data-bs-target', `#${collapseId}`);
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
button.setAttribute('aria-controls', collapseId);
|
||||
button.style.padding = '10px';
|
||||
if (event.event === 'meetingStarted') {
|
||||
button.classList.add('bg-success');
|
||||
}
|
||||
if (event.event === 'meetingEnded') {
|
||||
button.classList.add('bg-danger');
|
||||
}
|
||||
if (event.event.includes('recording')) {
|
||||
button.classList.add('bg-warning');
|
||||
}
|
||||
// Format the header text with event name and timestamp
|
||||
const date = new Date(event.creationDate);
|
||||
const formattedDate = date.toLocaleString('es-ES', {
|
||||
// year: 'numeric',
|
||||
// month: '2-digit',
|
||||
// day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
button.innerHTML = `[${formattedDate}] <strong>${event.event}</strong>`;
|
||||
// Create collapsible content container
|
||||
const collapseDiv = document.createElement('div');
|
||||
collapseDiv.id = collapseId;
|
||||
collapseDiv.className = 'accordion-collapse collapse';
|
||||
collapseDiv.setAttribute('aria-labelledby', headerClassName);
|
||||
collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
|
||||
// Create body content
|
||||
const bodyDiv = document.createElement('div');
|
||||
bodyDiv.className = 'accordion-body';
|
||||
// Format JSON with syntax highlighting if possible
|
||||
const formattedJson = JSON.stringify(event, null, 2);
|
||||
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(formattedJson)}</code></pre>`;
|
||||
// Assemble the components
|
||||
header.appendChild(button);
|
||||
collapseDiv.appendChild(bodyDiv);
|
||||
accordionItem.appendChild(header);
|
||||
accordionItem.appendChild(collapseDiv);
|
||||
// Insert at the top of the list (latest events first)
|
||||
if (webhookLogList.firstChild) {
|
||||
webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
|
||||
}
|
||||
else {
|
||||
webhookLogList.appendChild(accordionItem);
|
||||
}
|
||||
// Limit the number of items to prevent performance issues
|
||||
const maxItems = 50;
|
||||
while (webhookLogList.children.length > maxItems) {
|
||||
webhookLogList.removeChild(webhookLogList.lastChild);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Listen to events from openvidu-meet
|
||||
const listenWebComponentEvents = () => {
|
||||
const meet = document.querySelector('openvidu-meet');
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
meet.on('joined', (event) => {
|
||||
console.log('"joined" event received:', event);
|
||||
addEventToLog('joined', JSON.stringify(event));
|
||||
});
|
||||
meet.on('left', (event) => {
|
||||
console.log('"left" event received:', event);
|
||||
addEventToLog('left', JSON.stringify(event));
|
||||
});
|
||||
meet.on('closed', (event) => {
|
||||
console.log('"closed" event received:', event);
|
||||
addEventToLog('closed', JSON.stringify(event));
|
||||
// Redirect to home page
|
||||
// window.location.href = '/';
|
||||
});
|
||||
};
|
||||
// Set up commands for the web component
|
||||
const setUpWebComponentCommands = () => {
|
||||
var _a, _b, _c;
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
// End meeting button click handler
|
||||
(_a = document.getElementById('end-meeting-btn')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => meet.endMeeting());
|
||||
// Leave room button click handler
|
||||
(_b = document.getElementById('leave-room-btn')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => meet.leaveRoom());
|
||||
// Kick participant button click handler
|
||||
(_c = document.getElementById('kick-participant-btn')) === null || _c === void 0 ? void 0 : _c.addEventListener('click', () => {
|
||||
const participantIdentity = document.getElementById('participant-identity-input').value.trim();
|
||||
if (participantIdentity) {
|
||||
meet.kickParticipant(participantIdentity);
|
||||
}
|
||||
});
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
var _a, _b;
|
||||
roomId = (_b = (_a = document.getElementById('room-id')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
|
||||
showAllWebhooksCheckbox = document.getElementById('show-all-webhooks');
|
||||
meet = document.querySelector('openvidu-meet');
|
||||
if (!roomId) {
|
||||
console.error('Room ID not found in the DOM');
|
||||
alert('Room ID not found in the DOM');
|
||||
return;
|
||||
}
|
||||
renderStoredWebhookEvents(roomId);
|
||||
listenWebhookServerEvents();
|
||||
listenWebComponentEvents();
|
||||
setUpWebComponentCommands();
|
||||
showAllWebhooksCheckbox === null || showAllWebhooksCheckbox === void 0 ? void 0 : showAllWebhooksCheckbox.addEventListener('change', () => {
|
||||
if (roomId)
|
||||
renderStoredWebhookEvents(roomId);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user