@telnyx/voice-agent-tester 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { VoiceAgentTester } from '../src/voice-agent-tester.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import YAML from 'yaml';
6
+
7
+ describe('Integration Tests', () => {
8
+ let tester;
9
+
10
+ beforeEach(() => {
11
+ tester = new VoiceAgentTester({
12
+ verbose: false,
13
+ headless: true
14
+ });
15
+ });
16
+
17
+ afterEach(async () => {
18
+ if (tester) {
19
+ await tester.close();
20
+ }
21
+ });
22
+
23
+ test('should run a complete scenario', async () => {
24
+ // Create a simple test page
25
+ const testPageContent = `
26
+ <html>
27
+ <body>
28
+ <h1>Voice Agent Test Page</h1>
29
+ <button id="start">Start Test</button>
30
+ <div id="status">Not started</div>
31
+ <div id="speech-output"></div>
32
+ <script>
33
+ document.getElementById('start').addEventListener('click', () => {
34
+ document.getElementById('status').textContent = 'Test started';
35
+ });
36
+
37
+ // Mock speech synthesis for testing
38
+ window.speechSynthesis = {
39
+ speak: (utterance) => {
40
+ document.getElementById('speech-output').textContent = utterance.text;
41
+ }
42
+ };
43
+ window.SpeechSynthesisUtterance = function(text) {
44
+ this.text = text;
45
+ };
46
+
47
+ // Mock __speak function that will be called by the tester
48
+ // This needs to be in the page itself since evaluateOnNewDocument runs before navigation
49
+ window.__speak = (text) => {
50
+ document.getElementById('speech-output').textContent = text;
51
+ // Signal speech end after a small delay to allow waitForAudioEvent to be set up
52
+ setTimeout(() => {
53
+ if (window.__publishEvent) {
54
+ window.__publishEvent('speechend', {});
55
+ }
56
+ }, 10);
57
+ };
58
+ </script>
59
+ </body>
60
+ </html>
61
+ `;
62
+
63
+ // Use data URL to avoid file system dependencies
64
+ const testUrl = `data:text/html,${encodeURIComponent(testPageContent)}`;
65
+
66
+ const appSteps = [
67
+ { action: 'click', selector: '#start' }
68
+ ];
69
+
70
+ const scenarioSteps = [
71
+ { action: 'speak', text: 'Hello, this is a test.' }
72
+ ];
73
+
74
+ await tester.runScenario(testUrl, appSteps, scenarioSteps, 'test-app', 'test-scenario', 1);
75
+
76
+ // The scenario should complete without throwing errors
77
+ expect(true).toBe(true);
78
+ });
79
+
80
+ test('should handle scenario with wait step', async () => {
81
+ const testPageContent = `
82
+ <html>
83
+ <body>
84
+ <h1>Wait Test Page</h1>
85
+ <button id="trigger">Trigger</button>
86
+ <script>
87
+ document.getElementById('trigger').addEventListener('click', () => {
88
+ setTimeout(() => {
89
+ const newDiv = document.createElement('div');
90
+ newDiv.id = 'dynamic-element';
91
+ newDiv.textContent = 'Dynamic content appeared';
92
+ document.body.appendChild(newDiv);
93
+ }, 50);
94
+ });
95
+ </script>
96
+ </body>
97
+ </html>
98
+ `;
99
+
100
+ const testUrl = `data:text/html,${encodeURIComponent(testPageContent)}`;
101
+
102
+ const appSteps = [
103
+ { action: 'click', selector: '#trigger' }
104
+ ];
105
+
106
+ const scenarioSteps = [
107
+ { action: 'wait', selector: '#dynamic-element' }
108
+ ];
109
+
110
+ await tester.runScenario(testUrl, appSteps, scenarioSteps, 'test-app', 'test-scenario', 1);
111
+
112
+ // The scenario should complete without throwing errors (runScenario closes the browser)
113
+ expect(true).toBe(true);
114
+ });
115
+
116
+ test('should handle JavaScript injection', async () => {
117
+ // Create a minimal test scenario to verify JS injection works
118
+ const testPageContent = `
119
+ <html>
120
+ <body>
121
+ <div id="js-test">No JS loaded</div>
122
+ </body>
123
+ </html>
124
+ `;
125
+
126
+ const testUrl = `data:text/html,${encodeURIComponent(testPageContent)}`;
127
+
128
+ await tester.launch(testUrl);
129
+ await tester.page.goto(testUrl);
130
+
131
+ // Call injectJavaScriptFiles (should handle missing directory gracefully)
132
+ await tester.injectJavaScriptFiles();
133
+
134
+ // Verify the page still works after injection attempt
135
+ const content = await tester.page.evaluate(() => document.getElementById('js-test').textContent);
136
+ expect(content).toBe('No JS loaded');
137
+ });
138
+ });
@@ -0,0 +1,190 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { VoiceAgentTester } from '../src/voice-agent-tester.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ describe('VoiceAgentTester', () => {
7
+ let tester;
8
+
9
+ beforeEach(() => {
10
+ tester = new VoiceAgentTester({
11
+ verbose: false,
12
+ headless: true
13
+ });
14
+ });
15
+
16
+ afterEach(async () => {
17
+ if (tester) {
18
+ await tester.close();
19
+ }
20
+ });
21
+
22
+ test('should create instance with default options', () => {
23
+ const defaultTester = new VoiceAgentTester();
24
+ expect(defaultTester.verbose).toBe(false);
25
+ expect(defaultTester.headless).toBe(false);
26
+ expect(defaultTester.browser).toBe(null);
27
+ expect(defaultTester.page).toBe(null);
28
+ });
29
+
30
+ test('should create instance with custom options', () => {
31
+ const customTester = new VoiceAgentTester({
32
+ verbose: true,
33
+ headless: true
34
+ });
35
+ expect(customTester.verbose).toBe(true);
36
+ expect(customTester.headless).toBe(true);
37
+ });
38
+
39
+ test('should launch browser successfully', async () => {
40
+ const testUrl = 'data:text/html,<html><body></body></html>';
41
+ await tester.launch(testUrl);
42
+ expect(tester.browser).not.toBe(null);
43
+ expect(tester.page).not.toBe(null);
44
+ });
45
+
46
+ test('should close browser successfully', async () => {
47
+ const testUrl = 'data:text/html,<html><body></body></html>';
48
+ await tester.launch(testUrl);
49
+ expect(tester.browser).not.toBe(null);
50
+
51
+ await tester.close();
52
+ expect(tester.browser).toBe(null);
53
+ expect(tester.page).toBe(null);
54
+ });
55
+
56
+ test('should handle basic navigation', async () => {
57
+ const testUrl = 'data:text/html,<html><body><h1>Test Page</h1></body></html>';
58
+ await tester.launch(testUrl);
59
+
60
+ // Navigate to a basic page
61
+ await tester.page.goto(testUrl);
62
+
63
+ const title = await tester.page.evaluate(() => document.querySelector('h1').textContent);
64
+ expect(title).toBe('Test Page');
65
+ });
66
+
67
+ test('should execute click step', async () => {
68
+ const testUrl = 'data:text/html,<html><body><button id="test-btn">Click Me</button><div id="result"></div></body></html>';
69
+ await tester.launch(testUrl);
70
+
71
+ // Navigate to the test page
72
+ await tester.page.goto(testUrl);
73
+
74
+ // Add click handler
75
+ await tester.page.evaluate(() => {
76
+ document.getElementById('test-btn').addEventListener('click', () => {
77
+ document.getElementById('result').textContent = 'clicked';
78
+ });
79
+ });
80
+
81
+ // Execute click step
82
+ await tester.executeStep({
83
+ action: 'click',
84
+ selector: '#test-btn'
85
+ }, 0, 'scenario');
86
+
87
+ // Verify the click worked
88
+ const result = await tester.page.evaluate(() => document.getElementById('result').textContent);
89
+ expect(result).toBe('clicked');
90
+ });
91
+
92
+ test('should execute wait step', async () => {
93
+ const testUrl = 'data:text/html,<html><body><div id="container"></div></body></html>';
94
+ await tester.launch(testUrl);
95
+
96
+ // Navigate to the test page
97
+ await tester.page.goto(testUrl);
98
+
99
+ // Add the element after a short delay
100
+ await tester.page.evaluate(() => {
101
+ setTimeout(() => {
102
+ const newDiv = document.createElement('div');
103
+ newDiv.id = 'delayed-element';
104
+ newDiv.textContent = 'I appeared!';
105
+ document.getElementById('container').appendChild(newDiv);
106
+ }, 100);
107
+ });
108
+
109
+ // Execute wait step
110
+ await tester.executeStep({
111
+ action: 'wait',
112
+ selector: '#delayed-element'
113
+ }, 0, 'scenario');
114
+
115
+ // Verify the element exists
116
+ const element = await tester.page.$('#delayed-element');
117
+ expect(element).not.toBe(null);
118
+ });
119
+
120
+ test('should handle speak step', async () => {
121
+ const testUrl = 'data:text/html,<html><body><div id="speech-test"></div></body></html>';
122
+ await tester.launch(testUrl);
123
+
124
+ await tester.page.goto(testUrl);
125
+
126
+ // Mock __speak to capture the speak call and publish speechend event after a small delay
127
+ await tester.page.evaluate(() => {
128
+ window.__speak = (text) => {
129
+ document.getElementById('speech-test').textContent = text;
130
+ // Signal speech end after a small delay to allow waitForAudioEvent to be set up
131
+ setTimeout(() => {
132
+ if (window.__publishEvent) {
133
+ window.__publishEvent('speechend', {});
134
+ }
135
+ }, 10);
136
+ };
137
+ });
138
+
139
+ await tester.executeStep({
140
+ action: 'speak',
141
+ text: 'Hello, this is a test'
142
+ }, 0, 'scenario');
143
+
144
+ // Verify speech was triggered
145
+ const speechText = await tester.page.evaluate(() => document.getElementById('speech-test').textContent);
146
+ expect(speechText).toBe('Hello, this is a test');
147
+ });
148
+
149
+ test('should handle unknown action gracefully', async () => {
150
+ const testUrl = 'data:text/html,<html><body></body></html>';
151
+ await tester.launch(testUrl);
152
+ await tester.page.goto(testUrl);
153
+
154
+ // Mock console.log to capture the output
155
+ const originalLog = console.log;
156
+ let logMessages = [];
157
+ console.log = (message) => {
158
+ logMessages.push(message);
159
+ };
160
+
161
+ await tester.executeStep({
162
+ action: 'unknown_action'
163
+ }, 0, 'scenario');
164
+
165
+ // Find the unknown action message
166
+ const unknownActionMessage = logMessages.find(msg => msg.includes('Unknown action'));
167
+ expect(unknownActionMessage).toBe('Unknown action: unknown_action');
168
+
169
+ // Restore console.log
170
+ console.log = originalLog;
171
+ });
172
+
173
+ test('should throw error for missing required parameters', async () => {
174
+ const testUrl = 'data:text/html,<html><body></body></html>';
175
+ await tester.launch(testUrl);
176
+ await tester.page.goto(testUrl);
177
+
178
+ // Test click without selector
179
+ await expect(tester.executeStep({ action: 'click' }, 0, 'scenario'))
180
+ .rejects.toThrow('No selector specified for click action');
181
+
182
+ // Test wait without selector
183
+ await expect(tester.executeStep({ action: 'wait' }, 0, 'scenario'))
184
+ .rejects.toThrow('No selector specified for wait action');
185
+
186
+ // Test speak without text
187
+ await expect(tester.executeStep({ action: 'speak' }, 0, 'scenario'))
188
+ .rejects.toThrow('No text or file specified for speak action');
189
+ });
190
+ });