UNPKG

chromancer

Version:

A powerful command-line interface for automating Chrome browser using Playwright. Perfect for web scraping, automation, testing, and browser workflows.

291 lines (290 loc) โ€ข 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); const base_js_1 = require("../base.js"); const progress_js_1 = require("../utils/progress.js"); const chalk = { green: (s) => `\x1b[32m${s}\x1b[0m`, blue: (s) => `\x1b[34m${s}\x1b[0m`, yellow: (s) => `\x1b[33m${s}\x1b[0m`, cyan: (s) => `\x1b[36m${s}\x1b[0m`, gray: (s) => `\x1b[90m${s}\x1b[0m`, red: (s) => `\x1b[31m${s}\x1b[0m`, }; class Quick extends base_js_1.BaseCommand { static description = 'Perform comprehensive site testing, capture screenshots, extract data, and run accessibility checks - all with single commands'; static examples = [ '<%= config.bin %> <%= command.id %> check example.com', '<%= config.bin %> <%= command.id %> capture https://example.com output.png', '<%= config.bin %> <%= command.id %> extract https://news.site "article h2"', ]; static args = { action: core_1.Args.string({ description: 'Quick action to perform', required: true, options: ['check', 'capture', 'extract', 'test'], }), target: core_1.Args.string({ description: 'URL or target for the action', required: true, }), extra: core_1.Args.string({ description: 'Extra parameter (output file, selector, etc)', }), }; static flags = { ...base_js_1.BaseCommand.baseFlags, json: core_1.Flags.boolean({ description: 'Output in JSON format', default: false, }), }; async run() { const { args, flags } = await this.parse(Quick); // Connect to Chrome const progress = new progress_js_1.ProgressIndicator('Connecting to Chrome...'); progress.start(); await this.connectToChrome(flags.port, flags.host, flags.launch, flags.profile, flags.headless, flags.verbose, flags.keepOpen); progress.stop(); if (!this.page) { this.error('Failed to connect to Chrome'); } switch (args.action) { case 'check': await this.checkSite(args.target); break; case 'capture': await this.captureSite(args.target, args.extra || 'quick-capture.png'); break; case 'extract': await this.extractData(args.target, args.extra || 'body'); break; case 'test': await this.testSite(args.target); break; default: this.error(`Unknown action: ${args.action}`); } } async checkSite(url) { const progress = new progress_js_1.ProgressIndicator('Checking site...'); progress.start(); try { // Navigate if (!url.startsWith('http')) { url = 'https://' + url; } const startTime = Date.now(); await this.page.goto(url, { waitUntil: 'load' }); const loadTime = Date.now() - startTime; // Get page info const title = await this.page.title(); const metrics = await this.page.evaluate(() => ({ links: document.querySelectorAll('a').length, images: document.querySelectorAll('img').length, scripts: document.querySelectorAll('script').length, styles: document.querySelectorAll('link[rel="stylesheet"], style').length, })); progress.stop(); console.log(chalk.green('\nโœ… Site is accessible!\n')); console.log(`๐Ÿ“ URL: ${chalk.blue(this.page.url())}`); console.log(`๐Ÿ“„ Title: ${title}`); console.log(`โฑ๏ธ Load time: ${loadTime}ms`); console.log(`\n๐Ÿ“Š Page stats:`); console.log(` โ€ข Links: ${metrics.links}`); console.log(` โ€ข Images: ${metrics.images}`); console.log(` โ€ข Scripts: ${metrics.scripts}`); console.log(` โ€ข Styles: ${metrics.styles}`); } catch (error) { progress.stop(); this.error(`Failed to check site: ${error.message}`); } } async captureSite(url, output) { const progress = new progress_js_1.ProgressIndicator('Capturing page...'); progress.start(); try { // Navigate if (!url.startsWith('http')) { url = 'https://' + url; } await this.page.goto(url, { waitUntil: 'networkidle' }); progress.update('Taking screenshot...'); // Take screenshot await this.page.screenshot({ path: output, fullPage: true, }); progress.stop(); console.log(chalk.green(`โœ… Captured to: ${output}`)); } catch (error) { progress.stop(); this.error(`Failed to capture: ${error.message}`); } } async extractData(url, selector) { const progress = new progress_js_1.ProgressIndicator('Extracting data...'); progress.start(); try { // Navigate if (!url.startsWith('http')) { url = 'https://' + url; } await this.page.goto(url, { waitUntil: 'load' }); progress.update('Finding elements...'); // Extract data const data = await this.page.evaluate((sel) => { const elements = document.querySelectorAll(sel); return Array.from(elements).map(el => ({ text: el.textContent?.trim(), tag: el.tagName.toLowerCase(), href: el.href, src: el.src, })); }, selector); progress.stop(); if (data.length === 0) { this.warn(`No elements found matching: ${selector}`); return; } const { flags } = await this.parse(Quick); if (flags.json) { console.log(JSON.stringify(data, null, 2)); } else { console.log(chalk.green(`\nโœ… Found ${data.length} elements:\n`)); data.slice(0, 10).forEach((item, i) => { console.log(`${i + 1}. [${chalk.blue(item.tag)}] ${item.text}`); if (item.href) console.log(` ${chalk.gray('โ†’')} ${item.href}`); }); if (data.length > 10) { console.log(chalk.gray(`\n... and ${data.length - 10} more`)); } } } catch (error) { progress.stop(); this.error(`Failed to extract: ${error.message}`); } } async testSite(url) { console.log(chalk.cyan('\n๐Ÿงช Running site tests...\n')); const tests = [ { name: 'Accessibility', fn: () => this.testAccessibility(url) }, { name: 'Performance', fn: () => this.testPerformance(url) }, { name: 'Mobile View', fn: () => this.testMobile(url) }, { name: 'Console Errors', fn: () => this.testConsole(url) }, ]; let passed = 0; for (const test of tests) { try { const progress = new progress_js_1.ProgressIndicator(`Testing ${test.name}...`); progress.start(); const result = await test.fn(); progress.stop(); if (result.passed) { console.log(chalk.green(`โœ… ${test.name}: ${result.message}`)); passed++; } else { console.log(chalk.yellow(`โš ๏ธ ${test.name}: ${result.message}`)); } } catch (error) { console.log(chalk.red(`โŒ ${test.name}: ${error.message}`)); } } console.log(chalk.cyan(`\n๐Ÿ“Š Test Summary: ${passed}/${tests.length} passed\n`)); } async testAccessibility(url) { if (!url.startsWith('http')) url = 'https://' + url; await this.page.goto(url, { waitUntil: 'load' }); const results = await this.page.evaluate(() => { const issues = []; // Check images for alt text const imagesWithoutAlt = document.querySelectorAll('img:not([alt])'); if (imagesWithoutAlt.length > 0) { issues.push(`${imagesWithoutAlt.length} images without alt text`); } // Check headings hierarchy const h1Count = document.querySelectorAll('h1').length; if (h1Count !== 1) { issues.push(`${h1Count} H1 tags (should be 1)`); } // Check form labels const inputsWithoutLabels = document.querySelectorAll('input:not([aria-label]):not([id])'); if (inputsWithoutLabels.length > 0) { issues.push(`${inputsWithoutLabels.length} inputs without labels`); } return issues; }); return { passed: results.length === 0, message: results.length === 0 ? 'No major issues found' : results.join(', ') }; } async testPerformance(url) { if (!url.startsWith('http')) url = 'https://' + url; const startTime = Date.now(); await this.page.goto(url, { waitUntil: 'networkidle' }); const loadTime = Date.now() - startTime; const metrics = await this.page.evaluate(() => { const perf = performance; return { resources: performance.getEntriesByType('resource').length, jsHeap: perf.memory ? Math.round(perf.memory.usedJSHeapSize / 1048576) : 0, }; }); return { passed: loadTime < 3000, message: `Load time: ${loadTime}ms, Resources: ${metrics.resources}, JS Heap: ${metrics.jsHeap}MB` }; } async testMobile(url) { if (!url.startsWith('http')) url = 'https://' + url; // Set mobile viewport await this.page.setViewportSize({ width: 375, height: 667 }); await this.page.goto(url, { waitUntil: 'load' }); const results = await this.page.evaluate(() => { const issues = []; // Check viewport meta tag const viewport = document.querySelector('meta[name="viewport"]'); if (!viewport) { issues.push('No viewport meta tag'); } // Check for horizontal scroll if (document.documentElement.scrollWidth > window.innerWidth) { issues.push('Horizontal scroll detected'); } return issues; }); // Reset viewport await this.page.setViewportSize({ width: 1280, height: 720 }); return { passed: results.length === 0, message: results.length === 0 ? 'Mobile-friendly' : results.join(', ') }; } async testConsole(url) { const errors = []; this.page.on('console', (msg) => { if (msg.type() === 'error') { errors.push(msg.text()); } }); if (!url.startsWith('http')) url = 'https://' + url; await this.page.goto(url, { waitUntil: 'load' }); return { passed: errors.length === 0, message: errors.length === 0 ? 'No console errors' : `${errors.length} console errors` }; } } exports.default = Quick;