Skip to content

Commit c590001

Browse files
Copilotkobenguyent
andauthored
fix: browser test connectivity issues, add dynamic syntax highlighting, and comprehensive e2e test coverage (#573)
* Initial plan * Initial analysis: browser tests working but need server startup improvements Co-authored-by: kobenguyent <[email protected]> * Fix browser test connectivity with improved server startup and error handling Co-authored-by: kobenguyent <[email protected]> * Add dynamic language detection for code editor syntax highlighting Co-authored-by: kobenguyent <[email protected]> * Fix failed e2e tests and add comprehensive e2e test coverage Co-authored-by: kobenguyent <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: kobenguyent <[email protected]>
1 parent da98cce commit c590001

File tree

10 files changed

+617
-61
lines changed

10 files changed

+617
-61
lines changed

bin/codecept-ui.js

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,27 @@ const io = require('socket.io')({
1717
methods: ["GET", "POST"],
1818
transports: ['websocket', 'polling']
1919
},
20-
allowEIO3: true // Support for older Socket.IO clients
20+
allowEIO3: true, // Support for older Socket.IO clients
21+
// Add additional configuration for better reliability
22+
pingTimeout: 60000,
23+
pingInterval: 25000,
24+
connectTimeout: 45000,
25+
serveClient: true,
26+
// Allow connections from localhost variations
27+
allowRequest: (req, callback) => {
28+
const origin = req.headers.origin;
29+
const host = req.headers.host;
30+
31+
// Allow localhost connections and same-host connections
32+
if (!origin ||
33+
origin.includes('localhost') ||
34+
origin.includes('127.0.0.1') ||
35+
(host && origin.includes(host.split(':')[0]))) {
36+
callback(null, true);
37+
} else {
38+
callback(null, true); // Allow all for now, can be more restrictive if needed
39+
}
40+
}
2141
});
2242

2343
const { events } = require('../lib/model/ws-events');
@@ -65,17 +85,71 @@ codeceptjsFactory.create({}, options).then(() => {
6585
const applicationPort = options.port;
6686
const webSocketsPort = options.wsPort;
6787

68-
io.listen(webSocketsPort);
69-
app.listen(applicationPort);
70-
71-
// eslint-disable-next-line no-console
72-
console.log('🌟 CodeceptUI started!');
73-
74-
// eslint-disable-next-line no-console
75-
console.log(`👉 Open http://localhost:${applicationPort} to see CodeceptUI in a browser\n\n`);
88+
// Start servers with proper error handling and readiness checks
89+
let httpServer;
90+
let wsServer;
91+
92+
try {
93+
// Start WebSocket server first
94+
wsServer = io.listen(webSocketsPort);
95+
debug(`WebSocket server started on port ${webSocketsPort}`);
96+
97+
// Start HTTP server
98+
httpServer = app.listen(applicationPort, () => {
99+
// eslint-disable-next-line no-console
100+
console.log('🌟 CodeceptUI started!');
101+
// eslint-disable-next-line no-console
102+
console.log(`👉 Open http://localhost:${applicationPort} to see CodeceptUI in a browser\n\n`);
103+
// eslint-disable-next-line no-console
104+
debug(`Listening for websocket connections on port ${webSocketsPort}`);
105+
});
106+
107+
// Handle server errors
108+
httpServer.on('error', (err) => {
109+
if (err.code === 'EADDRINUSE') {
110+
console.error(`❌ Port ${applicationPort} is already in use. Please try a different port or stop the service using this port.`);
111+
} else {
112+
console.error(`❌ Failed to start HTTP server: ${err.message}`);
113+
}
114+
process.exit(1);
115+
});
116+
117+
wsServer.on('error', (err) => {
118+
if (err.code === 'EADDRINUSE') {
119+
console.error(`❌ WebSocket port ${webSocketsPort} is already in use. Please try a different port or stop the service using this port.`);
120+
} else {
121+
console.error(`❌ Failed to start WebSocket server: ${err.message}`);
122+
}
123+
process.exit(1);
124+
});
125+
126+
} catch (error) {
127+
console.error(`❌ Server startup failed: ${error.message}`);
128+
process.exit(1);
129+
}
76130

77-
// eslint-disable-next-line no-console
78-
debug(`Listening for websocket connections on port ${webSocketsPort}`);
131+
// Graceful shutdown handling
132+
const gracefulShutdown = () => {
133+
console.log('\n🛑 Shutting down CodeceptUI...');
134+
if (httpServer) {
135+
httpServer.close(() => {
136+
debug('HTTP server closed');
137+
});
138+
}
139+
if (wsServer) {
140+
wsServer.close(() => {
141+
debug('WebSocket server closed');
142+
});
143+
}
144+
process.exit(0);
145+
};
146+
147+
process.on('SIGINT', gracefulShutdown);
148+
process.on('SIGTERM', gracefulShutdown);
149+
process.on('uncaughtException', (err) => {
150+
console.error('❌ Uncaught Exception:', err);
151+
gracefulShutdown();
152+
});
79153

80154
if (options.app) {
81155
// open electron app

codecept.conf.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
tests: './test/e2e/tests/*.js',
3+
output: './output',
4+
helpers: {
5+
Playwright: {
6+
url: 'http://localhost:3000',
7+
browser: 'chromium',
8+
show: false,
9+
}
10+
},
11+
include: {},
12+
bootstrap: null,
13+
mocha: {},
14+
name: 'ui-test'
15+
};

src/components/ScenarioSource.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
v-if="!isEditing"
66
class="source-view"
77
>
8-
<pre v-highlightjs="displaySource"><code class="javascript" /></pre>
8+
<pre v-highlightjs="displaySource"><code :class="detectedLanguage.highlightjs" /></pre>
99
<div class="source-actions">
1010
<b-button
1111
v-if="file"
@@ -39,7 +39,7 @@
3939
Editing: {{ file }}
4040
</h5>
4141
<p class="is-size-7 has-text-grey">
42-
Lines {{ currentStartLine }}-{{ currentEndLine }} | CodeceptJS {{ mode }} Mode
42+
Lines {{ currentStartLine }}-{{ currentEndLine }} | {{ languageDisplayName }} | CodeceptJS {{ mode }} Mode
4343
</p>
4444
</div>
4545
<div class="column is-narrow">
@@ -75,7 +75,7 @@
7575
v-model="editorContent"
7676
:options="editorOptions"
7777
@editorDidMount="onEditorMounted"
78-
language="javascript"
78+
:language="detectedLanguage.monaco"
7979
theme="vs-light"
8080
height="400"
8181
/>
@@ -140,6 +140,7 @@
140140
<script>
141141
import axios from 'axios';
142142
import EditorNotFound from './EditorNotFound';
143+
const { detectLanguage, getLanguageDisplayName } = require('../utils/languageDetection');
143144
144145
export default {
145146
name: 'ScenarioSource',
@@ -212,6 +213,14 @@ export default {
212213
213214
hasChanges() {
214215
return this.editorContent !== this.originalContent;
216+
},
217+
218+
detectedLanguage() {
219+
return detectLanguage(this.file);
220+
},
221+
222+
languageDisplayName() {
223+
return getLanguageDisplayName(this.file);
215224
}
216225
},
217226
mounted() {

src/main.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,35 @@ const store = require('./store').default;
3131
// Use relative paths for reverse proxy setups
3232
wsConnection = baseUrl.replace('http', 'ws');
3333
} else {
34-
// Standard configuration - fetch port info
35-
try {
36-
const response = await axios.get('/api/ports');
37-
const data = await response.data;
38-
wsConnection = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:${data.wsPort}`;
39-
} catch (err) {
40-
// Fallback to same origin if port fetch fails
41-
wsConnection = baseUrl.replace('http', 'ws');
34+
// Standard configuration - fetch port info with retry logic
35+
let retryCount = 0;
36+
const maxRetries = 3;
37+
38+
while (retryCount < maxRetries) {
39+
try {
40+
const response = await axios.get('/api/ports', { timeout: 5000 });
41+
const data = await response.data;
42+
wsConnection = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:${data.wsPort}`;
43+
console.log('✅ Successfully fetched WebSocket port info:', data);
44+
break;
45+
} catch (err) {
46+
retryCount++;
47+
console.warn(`⚠️ Failed to fetch port info (attempt ${retryCount}/${maxRetries}):`, err.message);
48+
49+
if (retryCount >= maxRetries) {
50+
console.warn('🔄 Using fallback WebSocket connection to same origin');
51+
// Fallback to same origin if port fetch fails after retries
52+
wsConnection = baseUrl.replace('http', 'ws');
53+
} else {
54+
// Wait before retrying
55+
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
56+
}
57+
}
4258
}
4359
}
4460

61+
console.log('🔌 Connecting to WebSocket:', wsConnection);
62+
4563
Vue.use(new VueSocketIO({
4664
debug: true,
4765
connection: wsConnection,
@@ -50,6 +68,14 @@ const store = require('./store').default;
5068
actionPrefix: 'SOCKET_',
5169
mutationPrefix: 'SOCKET_'
5270
},
71+
options: {
72+
// Add connection options for better reliability
73+
timeout: 10000,
74+
reconnection: true,
75+
reconnectionAttempts: 5,
76+
reconnectionDelay: 1000,
77+
forceNew: false
78+
}
5379
}));
5480
})();
5581
Vue.config.productionTip = false;

src/utils/languageDetection.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Language detection utility for syntax highlighting
3+
* Maps file extensions to Monaco Editor and highlight.js language identifiers
4+
*/
5+
6+
/**
7+
* Get the appropriate language identifier for syntax highlighting
8+
* @param {string} filename - The filename or file path
9+
* @returns {Object} Object containing language identifiers for both Monaco and highlight.js
10+
*/
11+
function detectLanguage(filename) {
12+
if (!filename || typeof filename !== 'string') {
13+
return {
14+
monaco: 'javascript',
15+
highlightjs: 'javascript'
16+
};
17+
}
18+
19+
// Extract file extension
20+
const extension = filename.split('.').pop().toLowerCase();
21+
22+
// Map extensions to language identifiers
23+
const languageMap = {
24+
// JavaScript and related
25+
'js': { monaco: 'javascript', highlightjs: 'javascript' },
26+
'mjs': { monaco: 'javascript', highlightjs: 'javascript' },
27+
'jsx': { monaco: 'javascript', highlightjs: 'javascript' },
28+
29+
// TypeScript
30+
'ts': { monaco: 'typescript', highlightjs: 'typescript' },
31+
'tsx': { monaco: 'typescript', highlightjs: 'typescript' },
32+
33+
// Data formats
34+
'json': { monaco: 'json', highlightjs: 'json' },
35+
'jsonc': { monaco: 'json', highlightjs: 'json' },
36+
37+
// Configuration files
38+
'yaml': { monaco: 'yaml', highlightjs: 'yaml' },
39+
'yml': { monaco: 'yaml', highlightjs: 'yaml' },
40+
'toml': { monaco: 'ini', highlightjs: 'ini' },
41+
'ini': { monaco: 'ini', highlightjs: 'ini' },
42+
'conf': { monaco: 'ini', highlightjs: 'ini' },
43+
44+
// Markup and web
45+
'html': { monaco: 'html', highlightjs: 'html' },
46+
'htm': { monaco: 'html', highlightjs: 'html' },
47+
'xml': { monaco: 'xml', highlightjs: 'xml' },
48+
'css': { monaco: 'css', highlightjs: 'css' },
49+
'scss': { monaco: 'scss', highlightjs: 'scss' },
50+
'sass': { monaco: 'sass', highlightjs: 'sass' },
51+
'less': { monaco: 'less', highlightjs: 'less' },
52+
53+
// Documentation
54+
'md': { monaco: 'markdown', highlightjs: 'markdown' },
55+
'markdown': { monaco: 'markdown', highlightjs: 'markdown' },
56+
'txt': { monaco: 'plaintext', highlightjs: 'plaintext' },
57+
58+
// Testing frameworks
59+
'feature': { monaco: 'gherkin', highlightjs: 'gherkin' },
60+
'gherkin': { monaco: 'gherkin', highlightjs: 'gherkin' },
61+
62+
// Shell and scripts
63+
'sh': { monaco: 'shell', highlightjs: 'bash' },
64+
'bash': { monaco: 'shell', highlightjs: 'bash' },
65+
'zsh': { monaco: 'shell', highlightjs: 'bash' },
66+
'fish': { monaco: 'shell', highlightjs: 'bash' },
67+
'ps1': { monaco: 'powershell', highlightjs: 'powershell' },
68+
69+
// Other programming languages
70+
'py': { monaco: 'python', highlightjs: 'python' },
71+
'rb': { monaco: 'ruby', highlightjs: 'ruby' },
72+
'php': { monaco: 'php', highlightjs: 'php' },
73+
'java': { monaco: 'java', highlightjs: 'java' },
74+
'c': { monaco: 'c', highlightjs: 'c' },
75+
'cpp': { monaco: 'cpp', highlightjs: 'cpp' },
76+
'cs': { monaco: 'csharp', highlightjs: 'csharp' },
77+
'go': { monaco: 'go', highlightjs: 'go' },
78+
'rs': { monaco: 'rust', highlightjs: 'rust' },
79+
'swift': { monaco: 'swift', highlightjs: 'swift' },
80+
'kt': { monaco: 'kotlin', highlightjs: 'kotlin' },
81+
'scala': { monaco: 'scala', highlightjs: 'scala' },
82+
83+
// SQL
84+
'sql': { monaco: 'sql', highlightjs: 'sql' }
85+
};
86+
87+
// Return mapped language or default to JavaScript
88+
return languageMap[extension] || {
89+
monaco: 'javascript',
90+
highlightjs: 'javascript'
91+
};
92+
}
93+
94+
/**
95+
* Get user-friendly language name for display
96+
* @param {string} filename - The filename or file path
97+
* @returns {string} Human-readable language name
98+
*/
99+
function getLanguageDisplayName(filename) {
100+
const languages = detectLanguage(filename);
101+
102+
const displayNames = {
103+
'javascript': 'JavaScript',
104+
'typescript': 'TypeScript',
105+
'json': 'JSON',
106+
'yaml': 'YAML',
107+
'html': 'HTML',
108+
'css': 'CSS',
109+
'scss': 'SCSS',
110+
'sass': 'Sass',
111+
'less': 'Less',
112+
'markdown': 'Markdown',
113+
'gherkin': 'Gherkin (BDD)',
114+
'shell': 'Shell Script',
115+
'bash': 'Bash',
116+
'powershell': 'PowerShell',
117+
'python': 'Python',
118+
'ruby': 'Ruby',
119+
'php': 'PHP',
120+
'java': 'Java',
121+
'c': 'C',
122+
'cpp': 'C++',
123+
'csharp': 'C#',
124+
'go': 'Go',
125+
'rust': 'Rust',
126+
'swift': 'Swift',
127+
'kotlin': 'Kotlin',
128+
'scala': 'Scala',
129+
'sql': 'SQL',
130+
'ini': 'Configuration',
131+
'xml': 'XML',
132+
'plaintext': 'Plain Text'
133+
};
134+
135+
return displayNames[languages.monaco] || 'JavaScript';
136+
}
137+
138+
/**
139+
* Check if a language is supported by Monaco Editor
140+
* @param {string} language - Monaco language identifier
141+
* @returns {boolean} Whether the language is supported
142+
*/
143+
function isMonacoLanguageSupported(language) {
144+
// Monaco Editor built-in languages
145+
const supportedLanguages = [
146+
'javascript', 'typescript', 'json', 'html', 'css', 'scss', 'less',
147+
'markdown', 'yaml', 'xml', 'shell', 'powershell', 'python', 'ruby',
148+
'php', 'java', 'c', 'cpp', 'csharp', 'go', 'rust', 'swift', 'kotlin',
149+
'scala', 'sql', 'ini', 'plaintext'
150+
];
151+
152+
return supportedLanguages.includes(language);
153+
}
154+
155+
// Export for CommonJS (Node.js tests) and ES modules (Vue.js components)
156+
module.exports = {
157+
detectLanguage,
158+
getLanguageDisplayName,
159+
isMonacoLanguageSupported
160+
};

0 commit comments

Comments
 (0)