-
Notifications
You must be signed in to change notification settings - Fork 18
TestContext for E2E Debugging #1312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LittorWired
wants to merge
48
commits into
main
Choose a base branch
from
tl/cp-16075-e2e-test-context
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,662
−1,149
Open
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
2428fc3
fix waitForFunction bugs with parameters being incorrect
LittorWired 4875112
in expectToPass use the correct intervals param to fix polling issues
LittorWired 766444a
add polling unit tests for the utils expectToPass and expectPageEvalT…
LittorWired 591ab3c
implement the unit tests for the waitForFunction utility with polling…
LittorWired 1835909
refactor address.spec.ts tests to use the expectPageEvalToPass utility
LittorWired 09fec22
refactor expectCFFinalEvents to utilize expectPageEvalToPass and allo…
LittorWired 12663d0
fix type linting issues
LittorWired b556e92
refactor agent_customer.spec.ts tests to utilize expectPageEvalToPass
LittorWired 7f33331
refactor the first test in audioFlags.spec.ts to utilize expectPageEv…
LittorWired b429a13
refactor audioFlags.spec.ts to improve test structure and utilize exp…
LittorWired 4e1856c
remove unused type
LittorWired d0a1b86
refactor cleanup.spec.ts to improve test structure, utilize expectPag…
LittorWired 53bdd00
refactor conversation.spec.ts to utilize expectPageEvalToPass for ass…
LittorWired c51cd3c
refactor deviceEvent.spec.ts to utilize expectPageEvalToPass
LittorWired e29bc0e
fix util tests after fixing intervals param
LittorWired 42ee800
refactor deviceState.spec.ts to utilize expectPageEvalToPass
LittorWired fbad4bb
refactor dialAddress e2e utility to use multiple expectPageEvalToPass…
LittorWired f5bfb37
increase SW client loglevel to debug
LittorWired 22f15eb
break up tests into multiple projects
LittorWired e65ad3b
update the list of projects in github workflow matrix
LittorWired 3583d15
update playwright names for better debugging
LittorWired ac6602b
update the github project matrix names
LittorWired f327b73
dedupe projects
LittorWired 13872f9
increase the expectCFFinalEvents timeout
LittorWired 52123b3
attach listener before call.start
LittorWired a910632
attach listener for call.play finished before playback stop
LittorWired 46a380e
add optional timeout parameter to expectCFFinalEvents for optionally …
LittorWired 5faaacd
refactor the tts audio swml e2e test to utilize the expectPageEvalToP…
LittorWired 619fea3
remove import
LittorWired dabec90
Merge remote-tracking branch 'origin/main' into tl/cp-15995-refactor-…
LittorWired 96a7772
Merge remote-tracking branch 'origin/main' into tl/cp-15995-refactor-…
LittorWired 5e84770
implement TestContext class for tracking SDK and server events during…
LittorWired b66b5f8
add WebSocket monitoring and test context utilities for the testConte…
LittorWired abf3c19
add to the Playwright TestNameReporter to include SDK Test Context du…
LittorWired 37b0a23
extend Playwright test fixture to include test context setup and atta…
LittorWired 6f778c3
PR feedback: await client.rettach call
LittorWired 2045e01
Add isSerializable utility function to check for serializable values …
LittorWired 7d35c31
tests for isSerializable utility function for e2e tests
LittorWired f60f4ca
add serialization check to expectPageEvalToPass
LittorWired 1866875
fix bug on argument check for expectPageEvalToPass
LittorWired 0a76e14
revert the serializable checks
LittorWired 8d1d355
revert the serializable checks
LittorWired 2bcb78a
PR feedback: address PR feedback on call.reattach and check call inst…
LittorWired 4f7a37c
revert the isCallSession checks as they fail
LittorWired 7255603
PR feedback: assertions against the callSession.id
LittorWired 7524aa2
Merge branch 'tl/cp-15995-refactor-e2e-client-tests-new-util-pattern'…
LittorWired d0e84da
PR feedback: modify categories to be transport/WS, connection, callSe…
LittorWired 1be5820
PR Feedback: use SDKEvent interface
LittorWired File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,307 @@ | ||
| import { randomUUID } from 'crypto' | ||
| import type { JSONRPCMethod } from '@signalwire/core/' | ||
|
|
||
| type EventCategory = | ||
| | 'transport' | ||
| | 'connection' | ||
| | 'callSession' | ||
| | 'conversations' | ||
| | 'tests' | ||
|
|
||
| type Event = | ||
| | 'websocket_open' | ||
| | 'websocket_close' | ||
| | 'websocket_error' | ||
| | 'websocket_message' | ||
| | 'websocket_closed' | ||
|
|
||
| type Params = { | ||
| call_id?: string | ||
| room_session?: { | ||
| id?: string | ||
| } | ||
| } | ||
|
|
||
| type Result = { | ||
| call_id?: string | ||
| room_session?: { | ||
| id?: string | ||
| } | ||
| } | ||
|
|
||
| type Metadata = Record<string, any> | ||
|
|
||
| type Payload = { | ||
| error?: any | ||
| method?: JSONRPCMethod | ||
| event?: Event | ||
| params?: Params | ||
| result?: Result | ||
| metadata?: Metadata | ||
| } | ||
|
|
||
| export class SDKEvent { | ||
| constructor( | ||
| public id: string, | ||
| public timestamp: number, | ||
| public direction: 'send' | 'recv', | ||
| public category: 'request' | 'response' | 'notification' | 'connection', | ||
| public eventType: string, | ||
| public eventCategory: EventCategory, | ||
| public payload: Payload, | ||
| public context: { | ||
| isCallEvent: boolean | ||
| isRoomEvent: boolean | ||
| isConnectionEvent: boolean | ||
| isError: boolean | ||
| callId?: string | ||
| roomId?: string | ||
| }, | ||
| public method?: JSONRPCMethod, | ||
| public metadata?: Metadata | ||
| ) {} | ||
|
|
||
| // Utility methods for common checks | ||
| isCallRelated(): boolean { | ||
| return this.context.isCallEvent | ||
| } | ||
|
|
||
| isRoomRelated(): boolean { | ||
| return this.context.isRoomEvent | ||
| } | ||
|
|
||
| isConnectionRelated(): boolean { | ||
| return this.context.isConnectionEvent | ||
| } | ||
|
|
||
| isError(): boolean { | ||
| return this.context.isError | ||
| } | ||
|
|
||
| isSent(): boolean { | ||
| return this.direction === 'send' | ||
| } | ||
|
|
||
| isReceived(): boolean { | ||
| return this.direction === 'recv' | ||
| } | ||
|
|
||
| // Category checks | ||
| isTransport(): boolean { | ||
| return this.eventCategory === 'transport' | ||
| } | ||
|
|
||
| isConnection(): boolean { | ||
| return this.eventCategory === 'connection' | ||
| } | ||
|
|
||
| isCallSession(): boolean { | ||
| return this.eventCategory === 'callSession' | ||
| } | ||
|
|
||
| isConversations(): boolean { | ||
| return this.eventCategory === 'conversations' | ||
| } | ||
|
|
||
| isTests(): boolean { | ||
| return this.eventCategory === 'tests' | ||
| } | ||
| } | ||
|
|
||
| export interface EventStats { | ||
| totalEvents: number | ||
| sentEvents: number | ||
| receivedEvents: number | ||
| errorEvents: number | ||
| callEvents: number | ||
| roomEvents: number | ||
| connectionEvents: number | ||
| // Category-based counts | ||
| transportCategoryEvents: number | ||
| connectionCategoryEvents: number | ||
| callSessionCategoryEvents: number | ||
| conversationsCategoryEvents: number | ||
| testsCategoryEvents: number | ||
| } | ||
|
|
||
| /** | ||
| * TestContext class for tracking SDK events during e2e tests. | ||
| * | ||
| * Usage: | ||
| * - Automatically set up via Playwright fixtures | ||
| * - Events are captured via WebSocket monitoring | ||
| * - Context dumped to console and attachments on test failures | ||
| */ | ||
| export class TestContext { | ||
| private sdkEvents: SDKEvent[] = [] | ||
| private startTime: number = Date.now() | ||
|
|
||
| addSDKEvent(payload: Payload, direction: 'send' | 'recv') { | ||
| const event = this.categorizeSDKEvent(payload, direction) | ||
| this.sdkEvents.push(event) | ||
| } | ||
|
|
||
| /** | ||
| * Get comprehensive statistics about captured events. | ||
| * Includes both legacy boolean-based counts and new category-based counts. | ||
| */ | ||
| getStats() { | ||
| return { | ||
| totalEvents: this.sdkEvents.length, | ||
| sentEvents: this.sdkEvents.filter((event) => event.isSent()).length, | ||
| receivedEvents: this.sdkEvents.filter((event) => event.isReceived()) | ||
| .length, | ||
| errorEvents: this.sdkEvents.filter((event) => event.isError()).length, | ||
| callEvents: this.sdkEvents.filter((event) => event.isCallRelated()) | ||
| .length, | ||
| roomEvents: this.sdkEvents.filter((event) => event.isRoomRelated()) | ||
| .length, | ||
| connectionEvents: this.sdkEvents.filter((event) => | ||
| event.isConnectionRelated() | ||
| ).length, | ||
| // Category-based counts | ||
| transportCategoryEvents: this.sdkEvents.filter((event) => | ||
| event.isTransport() | ||
| ).length, | ||
| connectionCategoryEvents: this.sdkEvents.filter((event) => | ||
| event.isConnection() | ||
| ).length, | ||
| callSessionCategoryEvents: this.sdkEvents.filter((event) => | ||
| event.isCallSession() | ||
| ).length, | ||
| conversationsCategoryEvents: this.sdkEvents.filter((event) => | ||
| event.isConversations() | ||
| ).length, | ||
| testsCategoryEvents: this.sdkEvents.filter((event) => event.isTests()) | ||
| .length, | ||
| } | ||
| } | ||
|
|
||
| getAllEvents() { | ||
| return [...this.sdkEvents] | ||
| } | ||
|
|
||
| getTestDuration() { | ||
| return Date.now() - this.startTime | ||
| } | ||
|
|
||
| private categorizeSDKEvent( | ||
| payload: Payload, | ||
| direction: 'send' | 'recv' | ||
| ): SDKEvent { | ||
| const timestamp = Date.now() | ||
| const id = `${timestamp}-${randomUUID()}` | ||
|
|
||
| const method = payload.method || '' | ||
|
|
||
| const context = { | ||
| isCallEvent: method.includes('call.') || method.includes('calling.'), | ||
| isRoomEvent: | ||
| method.includes('room.') || | ||
| method.includes('member.') || | ||
| method.includes('layout.'), | ||
| isConnectionEvent: | ||
| method.startsWith('signalwire.') || method.startsWith('subscriber.'), | ||
| isError: Boolean(payload.error), | ||
| callId: payload.params?.call_id || payload.result?.call_id, | ||
| roomId: | ||
| payload.params?.room_session?.id || payload.result?.room_session?.id, | ||
| } | ||
|
|
||
| return new SDKEvent( | ||
| id, | ||
| timestamp, | ||
| direction, | ||
| this.getCategory(payload), | ||
| this.getEventType(payload), | ||
| this.categorizeMethod(payload), | ||
| payload, | ||
| context, | ||
| payload.method, | ||
| payload.metadata | ||
| ) | ||
| } | ||
|
|
||
| private getCategory(payload: Payload) { | ||
| // JSON-RPC 2.0 specification: | ||
| // - Response: Has 'id' and ('result' OR 'error') | ||
| // - Request: Has 'method' and 'id' | ||
| // - Notification: Has 'method' but no 'id' | ||
| const hasId = 'id' in payload | ||
| const hasMethod = 'method' in payload | ||
| const hasResult = 'result' in payload | ||
| const hasError = 'error' in payload | ||
|
|
||
| // Response: has id and (result or error), typically no method | ||
| if (hasId && (hasResult || hasError)) { | ||
| return 'response' as const | ||
| } | ||
|
|
||
| // Request: has both method and id | ||
| if (hasMethod && hasId) { | ||
| return 'request' as const | ||
| } | ||
|
|
||
| // Notification: has method but no id | ||
| if (hasMethod && !hasId) { | ||
| return 'notification' as const | ||
| } | ||
|
|
||
| // Fallback for connection events (websocket events, etc.) | ||
| return 'connection' as const | ||
| } | ||
|
|
||
| private categorizeMethod(payload: Payload): EventCategory { | ||
| const method = payload.method || '' | ||
| const event = payload.event || '' | ||
|
|
||
| if ( | ||
| event === 'websocket_open' || | ||
| event === 'websocket_close' || | ||
| event === 'websocket_error' || | ||
| event === 'websocket_message' || | ||
| event === 'websocket_closed' | ||
| ) { | ||
| return 'transport' | ||
| } | ||
|
|
||
| // SignalWire connection events | ||
| if (method.startsWith('signalwire.') || method.startsWith('subscriber.')) { | ||
| return 'connection' | ||
| } | ||
|
|
||
| // Conversations events | ||
| if (method.startsWith('conversations.')) { | ||
| return 'conversations' | ||
| } | ||
|
|
||
| // Test events (artificial events) | ||
| if (method.startsWith('test.') || method.includes('artificial')) { | ||
| return 'tests' | ||
| } | ||
|
|
||
| // Everything else is callSession (call, room, member, chat, verto, voice, etc.) | ||
| // Note: verto.* are call signaling events, not transport events | ||
| return 'callSession' | ||
| } | ||
|
|
||
| private getEventType(payload: { | ||
| error?: any | ||
| method?: JSONRPCMethod | ||
| event?: Event | ||
| }) { | ||
| if (payload.error) { | ||
| return payload.error.message | ||
| } | ||
|
|
||
| if (payload.method) { | ||
| return payload.method | ||
| } | ||
|
|
||
| if (payload.event) { | ||
| return payload.event | ||
| } | ||
|
|
||
| return 'unknown' | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could have a concrete implementation for the SDKEvent interface with these utilities
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the feedback, I implemented the
SDKEventas its own class.