Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 85 additions & 11 deletions bin/codecept-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,27 @@ const io = require('socket.io')({
methods: ["GET", "POST"],
transports: ['websocket', 'polling']
},
allowEIO3: true // Support for older Socket.IO clients
allowEIO3: true, // Support for older Socket.IO clients
// Add additional configuration for better reliability
pingTimeout: 60000,
pingInterval: 25000,
connectTimeout: 45000,
serveClient: true,
// Allow connections from localhost variations
allowRequest: (req, callback) => {
const origin = req.headers.origin;
const host = req.headers.host;

// Allow localhost connections and same-host connections
if (!origin ||
origin.includes('localhost') ||
origin.includes('127.0.0.1') ||
(host && origin.includes(host.split(':')[0]))) {
callback(null, true);
} else {
callback(null, true); // Allow all for now, can be more restrictive if needed
}
}
});

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

io.listen(webSocketsPort);
app.listen(applicationPort);

// eslint-disable-next-line no-console
console.log('🌟 CodeceptUI started!');

// eslint-disable-next-line no-console
console.log(`👉 Open http://localhost:${applicationPort} to see CodeceptUI in a browser\n\n`);
// Start servers with proper error handling and readiness checks
let httpServer;
let wsServer;

try {
// Start WebSocket server first
wsServer = io.listen(webSocketsPort);
debug(`WebSocket server started on port ${webSocketsPort}`);

// Start HTTP server
httpServer = app.listen(applicationPort, () => {
// eslint-disable-next-line no-console
console.log('🌟 CodeceptUI started!');
// eslint-disable-next-line no-console
console.log(`👉 Open http://localhost:${applicationPort} to see CodeceptUI in a browser\n\n`);
// eslint-disable-next-line no-console
debug(`Listening for websocket connections on port ${webSocketsPort}`);
});

// Handle server errors
httpServer.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ Port ${applicationPort} is already in use. Please try a different port or stop the service using this port.`);
} else {
console.error(`❌ Failed to start HTTP server: ${err.message}`);
}
process.exit(1);
});

wsServer.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ WebSocket port ${webSocketsPort} is already in use. Please try a different port or stop the service using this port.`);
} else {
console.error(`❌ Failed to start WebSocket server: ${err.message}`);
}
process.exit(1);
});

} catch (error) {
console.error(`❌ Server startup failed: ${error.message}`);
process.exit(1);
}

// eslint-disable-next-line no-console
debug(`Listening for websocket connections on port ${webSocketsPort}`);
// Graceful shutdown handling
const gracefulShutdown = () => {
console.log('\n🛑 Shutting down CodeceptUI...');
if (httpServer) {
httpServer.close(() => {
debug('HTTP server closed');
});
}
if (wsServer) {
wsServer.close(() => {
debug('WebSocket server closed');
});
}
process.exit(0);
};

process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('uncaughtException', (err) => {
console.error('❌ Uncaught Exception:', err);
gracefulShutdown();
});

if (options.app) {
// open electron app
Expand Down
15 changes: 15 additions & 0 deletions codecept.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
tests: './test/e2e/tests/*.js',
output: './output',
helpers: {
Playwright: {
url: 'http://localhost:3000',
browser: 'chromium',
show: false,
}
},
include: {},
bootstrap: null,
mocha: {},
name: 'ui-test'
};
15 changes: 12 additions & 3 deletions src/components/ScenarioSource.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
v-if="!isEditing"
class="source-view"
>
<pre v-highlightjs="displaySource"><code class="javascript" /></pre>
<pre v-highlightjs="displaySource"><code :class="detectedLanguage.highlightjs" /></pre>
<div class="source-actions">
<b-button
v-if="file"
Expand Down Expand Up @@ -39,7 +39,7 @@
Editing: {{ file }}
</h5>
<p class="is-size-7 has-text-grey">
Lines {{ currentStartLine }}-{{ currentEndLine }} | CodeceptJS {{ mode }} Mode
Lines {{ currentStartLine }}-{{ currentEndLine }} | {{ languageDisplayName }} | CodeceptJS {{ mode }} Mode
</p>
</div>
<div class="column is-narrow">
Expand Down Expand Up @@ -75,7 +75,7 @@
v-model="editorContent"
:options="editorOptions"
@editorDidMount="onEditorMounted"
language="javascript"
:language="detectedLanguage.monaco"
theme="vs-light"
height="400"
/>
Expand Down Expand Up @@ -140,6 +140,7 @@
<script>
import axios from 'axios';
import EditorNotFound from './EditorNotFound';
const { detectLanguage, getLanguageDisplayName } = require('../utils/languageDetection');

export default {
name: 'ScenarioSource',
Expand Down Expand Up @@ -212,6 +213,14 @@ export default {

hasChanges() {
return this.editorContent !== this.originalContent;
},

detectedLanguage() {
return detectLanguage(this.file);
},

languageDisplayName() {
return getLanguageDisplayName(this.file);
}
},
mounted() {
Expand Down
42 changes: 34 additions & 8 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,35 @@ const store = require('./store').default;
// Use relative paths for reverse proxy setups
wsConnection = baseUrl.replace('http', 'ws');
} else {
// Standard configuration - fetch port info
try {
const response = await axios.get('/api/ports');
const data = await response.data;
wsConnection = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:${data.wsPort}`;
} catch (err) {
// Fallback to same origin if port fetch fails
wsConnection = baseUrl.replace('http', 'ws');
// Standard configuration - fetch port info with retry logic
let retryCount = 0;
const maxRetries = 3;

while (retryCount < maxRetries) {
try {
const response = await axios.get('/api/ports', { timeout: 5000 });
const data = await response.data;
wsConnection = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:${data.wsPort}`;
console.log('✅ Successfully fetched WebSocket port info:', data);
break;
} catch (err) {
retryCount++;
console.warn(`⚠️ Failed to fetch port info (attempt ${retryCount}/${maxRetries}):`, err.message);

if (retryCount >= maxRetries) {
console.warn('🔄 Using fallback WebSocket connection to same origin');
// Fallback to same origin if port fetch fails after retries
wsConnection = baseUrl.replace('http', 'ws');
} else {
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
}
}
}
}

console.log('🔌 Connecting to WebSocket:', wsConnection);

Vue.use(new VueSocketIO({
debug: true,
connection: wsConnection,
Expand All @@ -50,6 +68,14 @@ const store = require('./store').default;
actionPrefix: 'SOCKET_',
mutationPrefix: 'SOCKET_'
},
options: {
// Add connection options for better reliability
timeout: 10000,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
forceNew: false
}
}));
})();
Vue.config.productionTip = false;
Expand Down
160 changes: 160 additions & 0 deletions src/utils/languageDetection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Language detection utility for syntax highlighting
* Maps file extensions to Monaco Editor and highlight.js language identifiers
*/

/**
* Get the appropriate language identifier for syntax highlighting
* @param {string} filename - The filename or file path
* @returns {Object} Object containing language identifiers for both Monaco and highlight.js
*/
function detectLanguage(filename) {
if (!filename || typeof filename !== 'string') {
return {
monaco: 'javascript',
highlightjs: 'javascript'
};
}

// Extract file extension
const extension = filename.split('.').pop().toLowerCase();

// Map extensions to language identifiers
const languageMap = {
// JavaScript and related
'js': { monaco: 'javascript', highlightjs: 'javascript' },
'mjs': { monaco: 'javascript', highlightjs: 'javascript' },
'jsx': { monaco: 'javascript', highlightjs: 'javascript' },

// TypeScript
'ts': { monaco: 'typescript', highlightjs: 'typescript' },
'tsx': { monaco: 'typescript', highlightjs: 'typescript' },

// Data formats
'json': { monaco: 'json', highlightjs: 'json' },
'jsonc': { monaco: 'json', highlightjs: 'json' },

// Configuration files
'yaml': { monaco: 'yaml', highlightjs: 'yaml' },
'yml': { monaco: 'yaml', highlightjs: 'yaml' },
'toml': { monaco: 'ini', highlightjs: 'ini' },
'ini': { monaco: 'ini', highlightjs: 'ini' },
'conf': { monaco: 'ini', highlightjs: 'ini' },

// Markup and web
'html': { monaco: 'html', highlightjs: 'html' },
'htm': { monaco: 'html', highlightjs: 'html' },
'xml': { monaco: 'xml', highlightjs: 'xml' },
'css': { monaco: 'css', highlightjs: 'css' },
'scss': { monaco: 'scss', highlightjs: 'scss' },
'sass': { monaco: 'sass', highlightjs: 'sass' },
'less': { monaco: 'less', highlightjs: 'less' },

// Documentation
'md': { monaco: 'markdown', highlightjs: 'markdown' },
'markdown': { monaco: 'markdown', highlightjs: 'markdown' },
'txt': { monaco: 'plaintext', highlightjs: 'plaintext' },

// Testing frameworks
'feature': { monaco: 'gherkin', highlightjs: 'gherkin' },
'gherkin': { monaco: 'gherkin', highlightjs: 'gherkin' },

// Shell and scripts
'sh': { monaco: 'shell', highlightjs: 'bash' },
'bash': { monaco: 'shell', highlightjs: 'bash' },
'zsh': { monaco: 'shell', highlightjs: 'bash' },
'fish': { monaco: 'shell', highlightjs: 'bash' },
'ps1': { monaco: 'powershell', highlightjs: 'powershell' },

// Other programming languages
'py': { monaco: 'python', highlightjs: 'python' },
'rb': { monaco: 'ruby', highlightjs: 'ruby' },
'php': { monaco: 'php', highlightjs: 'php' },
'java': { monaco: 'java', highlightjs: 'java' },
'c': { monaco: 'c', highlightjs: 'c' },
'cpp': { monaco: 'cpp', highlightjs: 'cpp' },
'cs': { monaco: 'csharp', highlightjs: 'csharp' },
'go': { monaco: 'go', highlightjs: 'go' },
'rs': { monaco: 'rust', highlightjs: 'rust' },
'swift': { monaco: 'swift', highlightjs: 'swift' },
'kt': { monaco: 'kotlin', highlightjs: 'kotlin' },
'scala': { monaco: 'scala', highlightjs: 'scala' },

// SQL
'sql': { monaco: 'sql', highlightjs: 'sql' }
};

// Return mapped language or default to JavaScript
return languageMap[extension] || {
monaco: 'javascript',
highlightjs: 'javascript'
};
}

/**
* Get user-friendly language name for display
* @param {string} filename - The filename or file path
* @returns {string} Human-readable language name
*/
function getLanguageDisplayName(filename) {
const languages = detectLanguage(filename);

const displayNames = {
'javascript': 'JavaScript',
'typescript': 'TypeScript',
'json': 'JSON',
'yaml': 'YAML',
'html': 'HTML',
'css': 'CSS',
'scss': 'SCSS',
'sass': 'Sass',
'less': 'Less',
'markdown': 'Markdown',
'gherkin': 'Gherkin (BDD)',
'shell': 'Shell Script',
'bash': 'Bash',
'powershell': 'PowerShell',
'python': 'Python',
'ruby': 'Ruby',
'php': 'PHP',
'java': 'Java',
'c': 'C',
'cpp': 'C++',
'csharp': 'C#',
'go': 'Go',
'rust': 'Rust',
'swift': 'Swift',
'kotlin': 'Kotlin',
'scala': 'Scala',
'sql': 'SQL',
'ini': 'Configuration',
'xml': 'XML',
'plaintext': 'Plain Text'
};

return displayNames[languages.monaco] || 'JavaScript';
}

/**
* Check if a language is supported by Monaco Editor
* @param {string} language - Monaco language identifier
* @returns {boolean} Whether the language is supported
*/
function isMonacoLanguageSupported(language) {
// Monaco Editor built-in languages
const supportedLanguages = [
'javascript', 'typescript', 'json', 'html', 'css', 'scss', 'less',
'markdown', 'yaml', 'xml', 'shell', 'powershell', 'python', 'ruby',
'php', 'java', 'c', 'cpp', 'csharp', 'go', 'rust', 'swift', 'kotlin',
'scala', 'sql', 'ini', 'plaintext'
];

return supportedLanguages.includes(language);
}

// Export for CommonJS (Node.js tests) and ES modules (Vue.js components)
module.exports = {
detectLanguage,
getLanguageDisplayName,
isMonacoLanguageSupported
};
Loading