Skip to content
150 changes: 150 additions & 0 deletions internal/e2e-js/SDKReporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type {
Reporter,
FullConfig,
Suite,
TestCase,
TestResult,
FullResult,
TestError,
TestStep,
} from '@playwright/test/reporter'

/**
* A custom SDK reporter that implements Playwright Reporter interface methods.
*/
export default class SDKReporter implements Reporter {
private totalTests = 0
private completedTests = 0
private failedTests = 0
private passedTests = 0
private skippedTests = 0
private timedOutTests = 0

/**
* Called once before running tests.
*/
onBegin(_config: FullConfig, suite: Suite): void {
this.totalTests = suite.allTests().length
console.log('============================================')
console.log(`Starting the run with ${this.totalTests} tests...`)
console.log('============================================')
}

/**
* Called after all tests have run, or the run was interrupted.
*/
async onEnd(result: FullResult): Promise<void> {
console.log('\n\n')
console.log('============================================')
console.log(`Test run finished with status: ${result.status.toUpperCase()}`)
console.log('--------------------------------------------')
console.log(`Total Tests: ${this.totalTests}`)
console.log(`Passed: ${this.passedTests}`)
console.log(`Failed: ${this.failedTests}`)
console.log(`Skipped: ${this.skippedTests}`)
console.log(`Timed Out: ${this.timedOutTests}`)
console.log('============================================')
console.log('\n\n')
}

/**
* Called on a global error, for example an unhandled exception in the test.
*/
onError(error: TestError): void {
console.log('============================================')
console.log(`Global Error: ${error.message}`)
console.log(error)
console.log('============================================')
}

/**
* Called immediately before the test runner exits, after onEnd() and all
* reporters have finished.
* If required: upload logs to a server here.
*/
async onExit(): Promise<void> {
console.log('[SDKReporter] Exit')
}

/**
* Called when a test step (i.e., `test.step(...)`) begins in the worker.
*/
onStepBegin(_test: TestCase, _result: TestResult, step: TestStep): void {
/**
* Playwright creates some internal steps as well.
* We do not care about those steps.
* We only log our own custom test steps.
*/
if (step.category === 'test.step') {
console.log(`--- STEP BEGIN: "${step.title}"`)
}
}

/**
* Called when a test step finishes.
*/
onStepEnd(_test: TestCase, _result: TestResult, step: TestStep): void {
if (step.category === 'test.step') {
if (step.error) {
console.log(`--- STEP FAILED: "${step.title}"`)
console.log(step.error)
} else {
console.log(`--- STEP FINISHED: "${step.title}"`)
}
}
}

/**
* Called when a test begins in the worker process.
*/
onTestBegin(test: TestCase, _result: TestResult): void {
console.log('--------------------------------------------')
console.log(`⏯️ Test Started: ${test.title}`)
console.log('--------------------------------------------')
}

/**
* Called when a test ends (pass, fail, timeout, etc.).
*/
onTestEnd(test: TestCase, result: TestResult): void {
console.log('--------------------------------------------')
this.completedTests += 1
switch (result.status) {
case 'passed':
this.passedTests += 1
console.log(`✅ Test Passed: ${test.title}`)
break
case 'failed':
this.failedTests += 1
console.log(`❌ Test Failed: ${test.title}`)
if (result.error) {
console.log(`📧 Error: ${result.error.message}`)
if (result.error.stack) {
console.log(`📚 Stack: ${result.error.stack}`)
}
}
break
case 'timedOut':
this.timedOutTests += 1
console.log(`⏰ Test Timed Out: ${test.title}`)
break
case 'skipped':
this.skippedTests += 1
console.log(`↩️ Test Skipped: ${test.title}`)
break
default:
console.log(`Test Ended with status "${result.status}": ${test.title}`)
break
}
console.log('--------------------------------------------')
console.log('\n\n')
}

/**
* Indicates this reporter does not handle stdout and stderr printing.
* So that Playwright print those logs.
*/
printsToStdio(): boolean {
return false
}
}
8 changes: 8 additions & 0 deletions internal/e2e-js/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const test = baseTest.extend<CustomFixture>({
try {
await use(maker)
} finally {
console.log('====================================')
console.log('Cleaning up pages..')
console.log('====================================')

/**
* If we have a __roomObj in the page means we tested the Video/Fabric APIs
* so we must leave the room.
Expand All @@ -75,6 +78,8 @@ const test = baseTest.extend<CustomFixture>({
* Make sure we cleanup the client as well.
*/
await Promise.all(context.pages().map(disconnectClient))

await context.close()
}
},
createCustomVanillaPage: async ({ context }, use) => {
Expand Down Expand Up @@ -112,7 +117,10 @@ const test = baseTest.extend<CustomFixture>({
try {
await use(resource)
} finally {
console.log('====================================')
console.log('Cleaning up resources..')
console.log('====================================')

// Clean up resources after use
const deleteResources = resources.map(async (resource) => {
try {
Expand Down
5 changes: 3 additions & 2 deletions internal/e2e-js/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,19 @@ const useDesktopChrome = {

const config: PlaywrightTestConfig = {
testDir: 'tests',
reporter: process.env.CI ? 'github' : 'list',
reporter: [[process.env.CI ? 'github' : 'list'], ['./SDKReporter.ts']],
globalSetup: require.resolve('./global-setup'),
testMatch: undefined,
testIgnore: undefined,
timeout: 120_000,
workers: 1,
maxFailures: 1,
expect: {
// Default is 5000
timeout: 10_000,
},
// Forbid test.only on CI
forbidOnly: !!process.env.CI,
workers: 1,
projects: [
{
name: 'default',
Expand Down
2 changes: 1 addition & 1 deletion internal/e2e-js/tests/roomSessionDemotePromote.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '../fixtures'

Check failure on line 1 in internal/e2e-js/tests/roomSessionDemotePromote.spec.ts

View workflow job for this annotation

GitHub Actions / Browser SDK production / Run E2E tests (20.x, demote)

[demote] › roomSessionDemotePromote.spec.ts:15:7 › RoomSession demote participant and then promote again › should demote participant and then promote again

1) [demote] › roomSessionDemotePromote.spec.ts:15:7 › RoomSession demote participant and then promote again › should demote participant and then promote again Test timeout of 120000ms exceeded.
import type { Video } from '@signalwire/js'
import {
SERVER_URL,
Expand Down Expand Up @@ -137,7 +137,7 @@

await pageTwo.waitForTimeout(1000)

// --------------- Promote audience from pageOne and resolve on `member.joined` ---------------
// --------------- Promote audience from pageOne and resolve on `member.joined` and `room.joined` ---------------
const promiseMemberWaitingForMemberJoin = pageOne.evaluate(
async ({ promoteMemberId }) => {
// @ts-expect-error
Expand Down
4 changes: 2 additions & 2 deletions packages/js/src/utils/interfaces/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export interface BaseRoomSessionContract {
*/
screenShareList: RoomSessionScreenShare[]
/**
* Leaves the room. This detaches all the locally originating streams from the room.
* Leaves the room immediately. This detaches all the locally originating streams from the room.
*/
leave(): Promise<void>
leave(): void
/**
* Return the member overlay on top of the root element
*/
Expand Down
58 changes: 48 additions & 10 deletions packages/webrtc/src/BaseConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,19 +195,34 @@ export class BaseConnection<
this.logger.debug('Set RTCPeer', rtcPeer.uuid, rtcPeer)
this.rtcPeerMap.set(rtcPeer.uuid, rtcPeer)

const setActivePeer = (peerId: string) => {
this.logger.debug('>>> Replace active RTCPeer with', peerId)
this.activeRTCPeerId = peerId
}

/**
* In case of the promote/demote, a new peer is created.
* Hence, we hangup the old peer.
*/
if (this.peer && this.peer.instance && this.callId !== rtcPeer.uuid) {
const oldPeerId = this.peer.uuid
this.logger.debug('>>> Stop old RTCPeer', oldPeerId)
// Hangup the previous RTCPeer
this.hangup(oldPeerId).catch(console.error)

// Stop transceivers and then the Peer
this.peer.detachAndStop()

// Set the new peer as active peer
setActivePeer(rtcPeer.uuid)

// Send "verto.bye" to the server
this.hangup(oldPeerId)

// Remove RTCPeer from local cache to stop answering to ping/pong
// this.rtcPeerMap.delete(oldPeerId)
} else {
// Set the new peer as active peer
setActivePeer(rtcPeer.uuid)
}

this.logger.debug('>>> Replace RTCPeer with', rtcPeer.uuid)
this.activeRTCPeerId = rtcPeer.uuid
}

// Overload for BaseConnection events
Expand Down Expand Up @@ -357,8 +372,18 @@ export class BaseConnection<
try {
this.logger.debug('Build a new RTCPeer')
const rtcPeer = this._buildPeer('offer')
this.logger.debug('Trigger start for the new RTCPeer!')
this.logger.debug('Trigger start for the new RTCPeer!', rtcPeer.uuid)
await rtcPeer.start()

/**
* Ideally, the SDK set the active peer when the `room.subscribed` or
* `verto.display` event is received. However, in some cases, while
* promoting/demoting, the RTC Peer negotiates successfully but then
* starts the negotiation again.
* So, without waiting for the events, we can safely set the active
* Peer once the initial negotiation succeeds.
*/
this.setActiveRTCPeer(rtcPeer.uuid)
} catch (error) {
this.logger.error('Error building new RTCPeer to promote/demote', error)
}
Expand Down Expand Up @@ -814,26 +839,39 @@ export class BaseConnection<
}

this.logger.debug('UpdateMedia response', response)
if (!this.peer) {

/**
* At a time, there can be multiple RTC Peers.
* The {@link executeUpdateMedia} is called with a Peer ID
* We need to make sure we set the remote SDP coming from the server
* on the appropriate Peer.
* The appropriate Peer may or may not be the current/active (this.peer) one.
*/
const peer = this.getRTCPeerById(rtcPeerId)
if (!peer) {
return this.logger.error('Invalid RTCPeer to updateMedia')
}
await this.peer.onRemoteSdp(response.sdp)
await peer.onRemoteSdp(response.sdp)
} catch (error) {
this.logger.error('UpdateMedia error', error)
// this.setState('hangup')
throw error
}
}

async hangup(id?: string) {
hangup(id?: string) {
const rtcPeerId = id ?? this.callId
if (!rtcPeerId) {
throw new Error('Invalid RTCPeer ID to hangup')
}

try {
const message = VertoBye(this.dialogParams(rtcPeerId))
await this.vertoExecute({
/**
* Fire-and-Forget
* For privacy reasons, the user should be allowed to leave the call immediately.
*/
this.vertoExecute({
message,
callID: rtcPeerId,
node_id: this.nodeId,
Expand Down
Loading