@xtest-cli/cli
Version:
CLI for xtest.ing - AI-powered test generation platform
298 lines ⢠12 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserController = void 0;
const playwright_1 = require("playwright");
const events_1 = require("events");
const ws_1 = require("ws");
const axios_1 = __importDefault(require("axios"));
class BrowserController extends events_1.EventEmitter {
constructor(options) {
super();
this.ws = null;
this.serverSessionId = null;
this.isMirroring = false;
this.options = options;
}
async start() {
try {
// First, create a mirror browser session on the server
this.emit('connecting');
const createResponse = await axios_1.default.post(`${this.options.serverUrl}/api/enhanced-browser/inspector/create`, {
sessionId: this.options.sessionId,
headless: true, // Server browser is always headless
devtools: false,
}, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
'Content-Type': 'application/json',
},
});
if (!createResponse.data.success) {
throw new Error(createResponse.data.error || 'Failed to create server browser session');
}
this.serverSessionId = createResponse.data.sessionId;
console.log(`\nā
Server mirror session created: ${this.serverSessionId}`);
// Start streaming the server browser to dashboard
await this.startServerStreaming();
// Launch local browser
await this.launchLocalBrowser();
// Connect WebSocket for real-time sync
await this.connectWebSocket();
// Start mirroring local actions to server
this.startMirroring();
}
catch (error) {
console.error('Failed to start browser session:', error.message);
throw error;
}
}
async launchLocalBrowser() {
const { mode, browserType, devtools, slowMo, record } = this.options.browserOptions;
// Select browser type
const browserLauncher = {
chromium: playwright_1.chromium,
firefox: playwright_1.firefox,
webkit: playwright_1.webkit,
}[browserType] || playwright_1.chromium;
// Launch options
const launchOptions = {
headless: mode === 'headless',
devtools: devtools && mode === 'inspector',
slowMo,
};
console.log(`Launching local ${browserType} browser...`);
this.browser = await browserLauncher.launch(launchOptions);
// Create context
const contextOptions = {
viewport: { width: 1280, height: 720 },
};
if (record) {
contextOptions.recordVideo = {
dir: './recordings',
size: { width: 1280, height: 720 },
};
}
this.context = await this.browser.newContext(contextOptions);
this.page = await this.context.newPage();
// Set up page event handlers
this.page.on('console', (msg) => {
console.log(`[Browser Console] ${msg.text()}`);
});
this.page.on('pageerror', (error) => {
console.error(`[Browser Error] ${error.message}`);
});
}
async connectWebSocket() {
const wsUrl = this.options.serverUrl.replace('http', 'ws') + '/cli/connect';
console.log(`Connecting to ${wsUrl}...`);
this.ws = new ws_1.WebSocket(wsUrl, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
'x-session-id': this.options.sessionId,
'x-cli-version': '0.3.1',
'x-browser-mode': this.options.browserOptions.mode,
},
});
this.ws.on('open', () => {
this.emit('connected');
console.log('\nā
Connected to xtest.ing');
console.log('š Local browser actions will be mirrored to dashboard');
// Send initial status
this.sendMessage({
type: 'status',
data: {
browser: this.options.browserOptions.browserType,
mode: this.options.browserOptions.mode,
serverSessionId: this.serverSessionId,
mirroring: true,
},
});
});
this.ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleServerMessage(message);
}
catch (error) {
console.error('Invalid message from server:', error);
}
});
this.ws.on('close', () => {
console.log('\nš Disconnected from server');
this.isMirroring = false;
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
}
startMirroring() {
if (!this.page || this.isMirroring)
return;
this.isMirroring = true;
// Mirror navigation
this.page.on('framenavigated', async (frame) => {
if (frame === this.page?.mainFrame()) {
const url = frame.url();
console.log(`[Local Browser] Navigated to: ${url}`);
// Mirror to server browser
if (this.serverSessionId && url !== 'about:blank') {
await this.mirrorNavigation(url);
}
}
});
// Intercept and mirror all requests that might change the page
this.page.on('request', async (request) => {
// Log navigation requests
if (request.isNavigationRequest() && request.frame() === this.page?.mainFrame()) {
console.log(`[Local Browser] Navigation request: ${request.url()}`);
}
});
// Use page evaluation to track clicks and inputs
this.page.addInitScript(() => {
// Track clicks
document.addEventListener('click', (e) => {
const target = e.target;
const selector = target.tagName.toLowerCase() +
(target.id ? `#${target.id}` : '') +
(target.className ? `.${target.className.split(' ').join('.')}` : '');
console.log('Click detected:', selector);
// Send to CLI via console log which we'll intercept
console.log(`__CLI_MIRROR_CLICK__:${selector}`);
}, true);
// Track inputs
document.addEventListener('input', (e) => {
const target = e.target;
const selector = target.tagName.toLowerCase() +
(target.id ? `#${target.id}` : '') +
(target.className ? `.${target.className.split(' ').join('.')}` : '');
console.log(`__CLI_MIRROR_INPUT__:${selector}:${target.value}`);
}, true);
});
// Intercept console messages to handle mirror commands
this.page.on('console', async (msg) => {
const text = msg.text();
if (text.startsWith('__CLI_MIRROR_CLICK__:')) {
const selector = text.replace('__CLI_MIRROR_CLICK__:', '');
console.log(`[Local Browser] Clicked: ${selector}`);
await this.mirrorAction('click', { selector });
}
else if (text.startsWith('__CLI_MIRROR_INPUT__:')) {
const parts = text.replace('__CLI_MIRROR_INPUT__:', '').split(':');
const selector = parts[0];
const value = parts.slice(1).join(':');
console.log(`[Local Browser] Input: ${selector}`);
await this.mirrorAction('type', { selector, text: value });
}
});
}
async mirrorNavigation(url) {
try {
await axios_1.default.post(`${this.options.serverUrl}/api/enhanced-browser/inspector/navigate`, {
sessionId: this.serverSessionId,
url,
}, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
'Content-Type': 'application/json',
},
});
console.log(`[Server Mirror] Navigated to: ${url}`);
}
catch (error) {
console.error('Failed to mirror navigation:', error);
}
}
async mirrorAction(action, params) {
try {
await axios_1.default.post(`${this.options.serverUrl}/api/enhanced-browser/inspector/execute`, {
sessionId: this.serverSessionId,
action,
params,
}, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
'Content-Type': 'application/json',
},
});
console.log(`[Server Mirror] Executed: ${action}`);
}
catch (error) {
console.error(`Failed to mirror ${action}:`, error);
}
}
async startServerStreaming() {
try {
await axios_1.default.post(`${this.options.serverUrl}/api/enhanced-browser/start-stream`, { sessionId: this.serverSessionId }, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
'Content-Type': 'application/json',
},
});
console.log('š„ Server browser streaming started - view in dashboard');
}
catch (error) {
console.error('Failed to start server streaming:', error);
}
}
handleServerMessage(message) {
switch (message.type) {
case 'connected':
console.log(`Session acknowledged: ${message.data.sessionId}`);
break;
default:
// Handle other message types if needed
break;
}
}
sendMessage(message) {
if (this.ws && this.ws.readyState === ws_1.WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
async stop() {
this.isMirroring = false;
// Stop server streaming
if (this.serverSessionId) {
try {
await axios_1.default.post(`${this.options.serverUrl}/api/enhanced-browser/stop-stream`, { sessionId: this.serverSessionId }, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
'Content-Type': 'application/json',
},
});
}
catch (error) {
// Ignore streaming stop errors
}
}
// Close WebSocket
if (this.ws) {
this.ws.close();
this.ws = null;
}
// Close local browser
if (this.browser) {
await this.browser.close();
console.log('ā
Local browser closed');
}
// Close server browser session
if (this.serverSessionId) {
try {
await axios_1.default.delete(`${this.options.serverUrl}/api/enhanced-browser/inspector/${this.serverSessionId}`, {
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
},
});
console.log('ā
Server mirror session closed');
}
catch (error) {
console.error('Failed to close server session:', error);
}
}
}
}
exports.BrowserController = BrowserController;
//# sourceMappingURL=controller.js.map