chromancer
Version:
A powerful command-line interface for automating Chrome browser using Playwright. Perfect for web scraping, automation, testing, and browser workflows.
208 lines (207 loc) âĸ 8.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const base_js_1 = require("../base.js");
const fs_1 = require("fs");
class Record extends base_js_1.BaseCommand {
static description = 'Record browser interactions and generate reusable automation scripts in JSON or JavaScript - perfect for creating test scenarios';
static examples = [
'<%= config.bin %> <%= command.id %> --output recording.json',
'<%= config.bin %> <%= command.id %> --output script.js --format js',
'<%= config.bin %> <%= command.id %> --duration 30000',
];
static flags = {
...base_js_1.BaseCommand.baseFlags,
output: core_1.Flags.string({
char: 'o',
description: 'Output file for the recording',
default: 'recording.json',
}),
format: core_1.Flags.string({
char: 'f',
description: 'Output format (json or js)',
options: ['json', 'js'],
default: 'json',
}),
duration: core_1.Flags.integer({
char: 'd',
description: 'Maximum recording duration in milliseconds',
default: 60000, // 1 minute
}),
};
actions = [];
startTime = Date.now();
async run() {
const { flags } = await this.parse(Record);
await this.connectToChrome(flags.port, flags.host, flags.launch, flags.profile, flags.headless, flags.verbose, true // Always keep open for recording
);
if (!this.page) {
this.error('Failed to connect to Chrome');
}
await this.startRecording(this.page);
}
async startRecording(page) {
const { flags } = await this.parse(Record);
this.log('đ´ Recording started. Interact with the page...');
this.log(`Press Ctrl+C to stop recording (max duration: ${flags.duration}ms)`);
// Track navigation
page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) {
this.actions.push({
type: 'navigate',
url: frame.url(),
timestamp: Date.now() - this.startTime,
});
}
});
// Inject recording script into every frame
await page.addInitScript(() => {
// Store actions in window object
window.__recordedActions = [];
// Override addEventListener to capture all events
const originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options) {
if (type === 'click' || type === 'change' || type === 'input') {
const wrappedListener = function (event) {
const target = event.target;
// Generate selector
let selector = '';
if (target.id) {
selector = `#${target.id}`;
}
else if (target.className) {
selector = `.${target.className.split(' ')[0]}`;
}
else {
selector = target.tagName.toLowerCase();
}
const action = {
type: type,
selector: selector,
timestamp: Date.now(),
};
// Add value for input events
if (type === 'input' && target.value) {
action.value = target.value;
}
// Add selected value for select elements
if (type === 'change' && target.tagName === 'SELECT') {
action.value = target.value;
}
window.__recordedActions.push(action);
// Call original listener
return listener.call(this, event);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
};
});
// Set up polling to collect recorded actions
const pollInterval = setInterval(async () => {
try {
const actions = await page.evaluate(() => {
const recorded = window.__recordedActions || [];
window.__recordedActions = [];
return recorded;
});
// Process and add actions
for (const action of actions) {
if (action.type === 'click') {
this.actions.push({
type: 'click',
selector: action.selector,
timestamp: action.timestamp - this.startTime,
});
}
else if (action.type === 'input') {
this.actions.push({
type: 'type',
selector: action.selector,
value: action.value,
timestamp: action.timestamp - this.startTime,
});
}
else if (action.type === 'change') {
this.actions.push({
type: 'select',
selector: action.selector,
value: action.value,
timestamp: action.timestamp - this.startTime,
});
}
}
}
catch (error) {
// Page might have navigated, ignore errors
}
}, 100);
// Set up timeout
const timeout = setTimeout(() => {
this.stopRecording(pollInterval);
}, flags.duration);
// Handle Ctrl+C
process.on('SIGINT', () => {
clearTimeout(timeout);
this.stopRecording(pollInterval);
});
}
async stopRecording(pollInterval) {
const { flags } = await this.parse(Record);
clearInterval(pollInterval);
this.log('âšī¸ Recording stopped');
this.log(`Captured ${this.actions.length} actions`);
// Generate output
let output;
if (flags.format === 'js') {
output = this.generateJavaScript();
}
else {
output = JSON.stringify(this.actions, null, 2);
}
// Write to file
await fs_1.promises.writeFile(flags.output, output);
this.log(`â
Recording saved to: ${flags.output}`);
process.exit(0);
}
generateJavaScript() {
const lines = [
'// Generated by chromancer record',
'// Run with: node script.js',
'',
'const { chromium } = require("playwright");',
'',
'(async () => {',
' const browser = await chromium.launch({ headless: false });',
' const page = await browser.newPage();',
'',
];
let lastTimestamp = 0;
for (const action of this.actions) {
// Add delay if needed
const delay = action.timestamp - lastTimestamp;
if (delay > 100) {
lines.push(` await page.waitForTimeout(${delay});`);
}
lastTimestamp = action.timestamp;
// Generate action code
switch (action.type) {
case 'navigate':
lines.push(` await page.goto('${action.url}');`);
break;
case 'click':
lines.push(` await page.click('${action.selector}');`);
break;
case 'type':
lines.push(` await page.fill('${action.selector}', '${action.value}');`);
break;
case 'select':
lines.push(` await page.selectOption('${action.selector}', '${action.value}');`);
break;
}
}
lines.push('', ' // await browser.close();', '})();');
return lines.join('\n');
}
}
exports.default = Record;