import
This commit is contained in:
parent
70d9ed30de
commit
d6209cd34f
|
@ -0,0 +1,3 @@
|
||||||
|
steps.sh
|
||||||
|
/cremote
|
||||||
|
/cremotedaemon
|
|
@ -0,0 +1,627 @@
|
||||||
|
# LLM Agent Guide: Using cremote for Web Application Testing
|
||||||
|
|
||||||
|
This document provides comprehensive guidance for LLM coding agents on how to use **cremote** (Chrome Remote Daemon) as a testing tool for web applications. cremote enables automated browser testing of public web interfaces through programmatic control of Chrome browser tabs.
|
||||||
|
|
||||||
|
## What is cremote?
|
||||||
|
|
||||||
|
**cremote** is a browser automation tool that allows you to:
|
||||||
|
- Control Chromium browser tabs programmatically
|
||||||
|
- Fill forms and interact with web elements
|
||||||
|
- Navigate web pages and wait for content to load
|
||||||
|
- Extract page content and element HTML
|
||||||
|
- Test user workflows end-to-end
|
||||||
|
|
||||||
|
It uses a daemon-client architecture where a background daemon maintains persistent connections to Chromium, and a command-line client sends testing commands.
|
||||||
|
|
||||||
|
## Prerequisites for Testing
|
||||||
|
|
||||||
|
Before using cremote for web application testing, ensure:
|
||||||
|
|
||||||
|
0. **Check to see if everything is already running, if so you can skip the steps to start it:**
|
||||||
|
```bash
|
||||||
|
cremote status
|
||||||
|
```
|
||||||
|
**Note**: `cremote status` always exits with code 0, whether the daemon is running or not. Check the output message to determine status.
|
||||||
|
|
||||||
|
1. **Chromium/Chrome is running with remote debugging enabled:**
|
||||||
|
```bash
|
||||||
|
# Create a temporary user data directory for the debug instance
|
||||||
|
chromium --remote-debugging-port=9222 --user-data-dir=/tmp/chromium-debug &
|
||||||
|
# or
|
||||||
|
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
|
||||||
|
|
||||||
|
# Alternative: Use a random temporary directory
|
||||||
|
chromium --remote-debugging-port=9222 --user-data-dir=$(mktemp -d) &
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: The `--user-data-dir` flag is required to prevent Chromium from trying to use an existing window. Without it, Chromium will attempt to connect to an already running instance instead of starting a new debug-enabled instance.
|
||||||
|
|
||||||
|
2. **cremote daemon is running:**
|
||||||
|
```bash
|
||||||
|
cremotedaemon &
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: The daemon will automatically check if Chromium is running and provide helpful error messages if:
|
||||||
|
- Chromium is not running on port 9222
|
||||||
|
- Something else is using port 9222 (not Chromium DevTools)
|
||||||
|
- Chromium is running but not accepting connections
|
||||||
|
|
||||||
|
3. **Verify connectivity:**
|
||||||
|
```bash
|
||||||
|
cremote status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Testing Workflow
|
||||||
|
|
||||||
|
### 1. Basic Test Session Setup
|
||||||
|
|
||||||
|
Every testing session follows this pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Open a new browser tab for testing
|
||||||
|
TAB_ID=$(cremote open-tab)
|
||||||
|
|
||||||
|
# 2. Navigate to the application under test
|
||||||
|
cremote load-url --url="https://your-app.com"
|
||||||
|
|
||||||
|
# 3. Perform test actions (forms, clicks, navigation)
|
||||||
|
# ... testing commands ...
|
||||||
|
|
||||||
|
# 4. Clean up (optional)
|
||||||
|
cremote close-tab --tab="$TAB_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Current Tab Feature
|
||||||
|
|
||||||
|
cremote automatically tracks the "current tab" - the most recently used tab. This means you can omit the `--tab` flag in most commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open tab (becomes current tab)
|
||||||
|
cremote open-tab
|
||||||
|
|
||||||
|
# All subsequent commands use current tab automatically
|
||||||
|
cremote load-url --url="https://example.com"
|
||||||
|
cremote fill-form --selector="#username" --value="testuser"
|
||||||
|
cremote click-element --selector="#login-btn"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Essential Testing Commands
|
||||||
|
|
||||||
|
### Navigation and Page Loading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Load a specific URL
|
||||||
|
cremote load-url --url="https://your-app.com/login"
|
||||||
|
|
||||||
|
# Wait for navigation to complete (useful after form submissions)
|
||||||
|
cremote wait-navigation --timeout=10
|
||||||
|
|
||||||
|
# Note: wait-navigation is smart - it returns immediately if no navigation is happening
|
||||||
|
|
||||||
|
# Get current page source for verification
|
||||||
|
cremote get-source
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fill text inputs
|
||||||
|
cremote fill-form --selector="#username" --value="testuser"
|
||||||
|
cremote fill-form --selector="#password" --value="testpass123"
|
||||||
|
|
||||||
|
# Handle checkboxes (check)
|
||||||
|
cremote fill-form --selector="#remember-me" --value="true"
|
||||||
|
cremote fill-form --selector="#terms-agreed" --value="checked"
|
||||||
|
|
||||||
|
# Handle checkboxes (uncheck)
|
||||||
|
cremote fill-form --selector="#newsletter" --value="false"
|
||||||
|
|
||||||
|
# Handle radio buttons
|
||||||
|
cremote fill-form --selector="#payment-credit" --value="true"
|
||||||
|
|
||||||
|
# Submit forms
|
||||||
|
cremote submit-form --selector="form#login-form"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checkbox/Radio Button Values:**
|
||||||
|
- To check/select: `true`, `1`, `yes`, `on`, `checked`
|
||||||
|
- To uncheck/deselect: `false`, `0`, `no`, `off`, or any other value
|
||||||
|
|
||||||
|
### File Upload Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Upload files to file inputs
|
||||||
|
cremote upload-file --selector="input[type=file]" --file="/path/to/test-file.pdf"
|
||||||
|
cremote upload-file --selector="#profile-photo" --file="/tmp/test-image.jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element Interaction
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Click buttons, links, or any clickable elements
|
||||||
|
cremote click-element --selector="button.submit"
|
||||||
|
cremote click-element --selector="a[href='/dashboard']"
|
||||||
|
cremote click-element --selector="#save-changes"
|
||||||
|
|
||||||
|
# Get HTML content of specific elements for verification
|
||||||
|
cremote get-element --selector=".error-message"
|
||||||
|
cremote get-element --selector="#user-profile"
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Execute JavaScript code directly in the page (default 5 second timeout)
|
||||||
|
cremote eval-js --code="document.getElementById('tinymce').innerHTML='Foo!'"
|
||||||
|
|
||||||
|
# Get values from the page
|
||||||
|
cremote eval-js --code="document.querySelector('#result').textContent"
|
||||||
|
|
||||||
|
# Manipulate page elements
|
||||||
|
cremote eval-js --code="document.body.style.backgroundColor = 'red'"
|
||||||
|
|
||||||
|
# Trigger JavaScript events
|
||||||
|
cremote eval-js --code="document.getElementById('submit-btn').click()"
|
||||||
|
|
||||||
|
# Work with complex objects (returns JSON string)
|
||||||
|
cremote eval-js --code="document.querySelectorAll('.item').length"
|
||||||
|
|
||||||
|
# Set form values programmatically (statement - returns "undefined")
|
||||||
|
cremote eval-js --code="document.getElementById('hidden-field').value = 'secret-value'"
|
||||||
|
|
||||||
|
# Get form values (expression - returns the value)
|
||||||
|
cremote eval-js --code="document.getElementById('hidden-field').value"
|
||||||
|
|
||||||
|
# Use custom timeout for long-running JavaScript
|
||||||
|
cremote eval-js --code="await new Promise(resolve => setTimeout(resolve, 8000))" --timeout=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Take a viewport screenshot (default)
|
||||||
|
cremote screenshot --output="/tmp/page-screenshot.png"
|
||||||
|
|
||||||
|
# Take a full page screenshot
|
||||||
|
cremote screenshot --output="/tmp/full-page.png" --full-page
|
||||||
|
|
||||||
|
# Screenshot with custom timeout
|
||||||
|
cremote screenshot --output="/tmp/slow-page.png" --timeout=10
|
||||||
|
|
||||||
|
# Screenshot of specific tab
|
||||||
|
cremote screenshot --tab="$TAB_ID" --output="/tmp/tab-screenshot.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Cases for JavaScript Execution:**
|
||||||
|
- Interact with rich text editors (TinyMCE, CKEditor, etc.)
|
||||||
|
- Trigger JavaScript events that aren't accessible via normal clicks
|
||||||
|
- Extract computed values or complex data structures
|
||||||
|
- Manipulate hidden form fields
|
||||||
|
- Test JavaScript functionality directly
|
||||||
|
- Set up test data or page state
|
||||||
|
|
||||||
|
**Expression vs Statement Handling:**
|
||||||
|
- **Expressions** return values: `document.title`, `element.textContent`, `array.length`
|
||||||
|
- **Statements** perform actions and return "undefined": `element.click()`, `variable = value`
|
||||||
|
- Both types are supported seamlessly in the same command
|
||||||
|
|
||||||
|
### Working with Iframes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Switch to iframe context
|
||||||
|
cremote switch-iframe --selector="iframe#payment-form"
|
||||||
|
|
||||||
|
# All subsequent commands now operate within the iframe
|
||||||
|
cremote fill-form --selector="#card-number" --value="4111111111111111"
|
||||||
|
cremote fill-form --selector="#cvv" --value="123"
|
||||||
|
cremote click-element --selector="#submit-payment"
|
||||||
|
|
||||||
|
# Switch back to main page context
|
||||||
|
cremote switch-main
|
||||||
|
|
||||||
|
# Commands now operate on the main page again
|
||||||
|
cremote get-element --selector=".payment-success"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Iframe Scenarios:**
|
||||||
|
- **Payment Forms**: Credit card processing iframes
|
||||||
|
- **Embedded Widgets**: Social media, maps, chat widgets
|
||||||
|
- **Third-party Content**: Ads, analytics, external forms
|
||||||
|
- **Security Contexts**: Sandboxed content, cross-origin frames
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- Iframe context is maintained per tab
|
||||||
|
- All commands (fill-form, click-element, eval-js, etc.) work within iframe context
|
||||||
|
- Must explicitly switch back to main context with `switch-main`
|
||||||
|
- Iframe context persists until switched back or tab is closed
|
||||||
|
|
||||||
|
### Tab Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all open tabs (current tab marked with *)
|
||||||
|
cremote list-tabs
|
||||||
|
|
||||||
|
# Open multiple tabs for complex testing
|
||||||
|
TAB1=$(cremote open-tab)
|
||||||
|
TAB2=$(cremote open-tab)
|
||||||
|
|
||||||
|
# Work with specific tabs
|
||||||
|
cremote load-url --tab="$TAB1" --url="https://app.com/admin"
|
||||||
|
cremote load-url --tab="$TAB2" --url="https://app.com/user"
|
||||||
|
|
||||||
|
# Close specific tabs
|
||||||
|
cremote close-tab --tab="$TAB1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timeout Configuration
|
||||||
|
|
||||||
|
Many commands support timeout parameters for robust testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wait up to 10 seconds for element to appear, then 5 seconds for action
|
||||||
|
cremote fill-form --selector="#slow-loading-field" --value="test" \
|
||||||
|
--selection-timeout=10 --action-timeout=5
|
||||||
|
|
||||||
|
# Wait for elements that load dynamically
|
||||||
|
cremote click-element --selector=".ajax-button" \
|
||||||
|
--selection-timeout=15 --action-timeout=10
|
||||||
|
|
||||||
|
# Get elements that may take time to render
|
||||||
|
cremote get-element --selector=".dynamic-content" --selection-timeout=20
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timeout Parameters:**
|
||||||
|
- `--selection-timeout`: Seconds to wait for element to appear in DOM (default: 5 seconds)
|
||||||
|
- `--action-timeout`: Seconds to wait for action to complete (default: 5 seconds)
|
||||||
|
- `--timeout`: General timeout for operations (default: 5 seconds)
|
||||||
|
|
||||||
|
**Smart Navigation Waiting:**
|
||||||
|
The `wait-navigation` command intelligently detects if navigation is actually happening:
|
||||||
|
- Returns immediately if the page is already stable and loaded
|
||||||
|
- Monitors for 2 seconds to detect if navigation starts
|
||||||
|
- Only waits for the full timeout if navigation is actually in progress
|
||||||
|
- This prevents hanging when no navigation occurs
|
||||||
|
|
||||||
|
## Common Testing Patterns
|
||||||
|
|
||||||
|
### 1. Login Flow Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Test user login functionality
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
cremote open-tab
|
||||||
|
cremote load-url --url="https://myapp.com/login"
|
||||||
|
|
||||||
|
# Test valid login
|
||||||
|
cremote fill-form --selector="#email" --value="user@example.com"
|
||||||
|
cremote fill-form --selector="#password" --value="validpassword"
|
||||||
|
cremote click-element --selector="#login-button"
|
||||||
|
|
||||||
|
# Wait for redirect and verify success
|
||||||
|
cremote wait-navigation --timeout=10
|
||||||
|
PAGE_SOURCE=$(cremote get-source)
|
||||||
|
|
||||||
|
if echo "$PAGE_SOURCE" | grep -q "Welcome"; then
|
||||||
|
echo "✓ Login successful"
|
||||||
|
else
|
||||||
|
echo "✗ Login failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Form Validation Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Test form validation
|
||||||
|
|
||||||
|
cremote open-tab
|
||||||
|
cremote load-url --url="https://myapp.com/register"
|
||||||
|
|
||||||
|
# Test empty form submission
|
||||||
|
cremote click-element --selector="#submit-btn"
|
||||||
|
|
||||||
|
# Check for validation errors
|
||||||
|
ERROR_MSG=$(cremote get-element --selector=".error-message" --selection-timeout=5)
|
||||||
|
if [ -n "$ERROR_MSG" ]; then
|
||||||
|
echo "✓ Validation working: $ERROR_MSG"
|
||||||
|
else
|
||||||
|
echo "✗ No validation error shown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test invalid email format
|
||||||
|
cremote fill-form --selector="#email" --value="invalid-email"
|
||||||
|
cremote click-element --selector="#submit-btn"
|
||||||
|
|
||||||
|
# Verify email validation
|
||||||
|
EMAIL_ERROR=$(cremote get-element --selector="#email-error" --selection-timeout=5)
|
||||||
|
if echo "$EMAIL_ERROR" | grep -q "valid email"; then
|
||||||
|
echo "✓ Email validation working"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test JavaScript validation directly
|
||||||
|
JS_VALIDATION=$(cremote eval-js --code="document.getElementById('email').validity.valid")
|
||||||
|
if [ "$JS_VALIDATION" = "false" ]; then
|
||||||
|
echo "✓ JavaScript validation also working"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Multi-Step Workflow Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Test complete user workflow
|
||||||
|
|
||||||
|
# Step 1: Registration
|
||||||
|
cremote open-tab
|
||||||
|
cremote load-url --url="https://myapp.com/register"
|
||||||
|
cremote fill-form --selector="#username" --value="newuser123"
|
||||||
|
cremote fill-form --selector="#email" --value="newuser@test.com"
|
||||||
|
cremote fill-form --selector="#password" --value="securepass123"
|
||||||
|
cremote fill-form --selector="#confirm-password" --value="securepass123"
|
||||||
|
cremote click-element --selector="#register-btn"
|
||||||
|
cremote wait-navigation --timeout=15
|
||||||
|
|
||||||
|
# Step 2: Email verification simulation
|
||||||
|
cremote load-url --url="https://myapp.com/verify?token=test-token"
|
||||||
|
cremote wait-navigation --timeout=10
|
||||||
|
|
||||||
|
# Step 3: Profile setup
|
||||||
|
cremote fill-form --selector="#first-name" --value="Test"
|
||||||
|
cremote fill-form --selector="#last-name" --value="User"
|
||||||
|
cremote upload-file --selector="#profile-photo" --file="/tmp/avatar.jpg"
|
||||||
|
cremote click-element --selector="#save-profile"
|
||||||
|
|
||||||
|
# Step 4: Verify completion
|
||||||
|
cremote wait-navigation --timeout=10
|
||||||
|
PROFILE_PAGE=$(cremote get-source)
|
||||||
|
if echo "$PROFILE_PAGE" | grep -q "Profile completed"; then
|
||||||
|
echo "✓ Complete workflow successful"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Error Handling and Edge Cases
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Test error scenarios
|
||||||
|
|
||||||
|
# Test network timeout handling
|
||||||
|
cremote open-tab
|
||||||
|
cremote load-url --url="https://httpbin.org/delay/30"
|
||||||
|
# This should timeout - test how app handles it
|
||||||
|
|
||||||
|
# Test invalid form data
|
||||||
|
cremote load-url --url="https://myapp.com/contact"
|
||||||
|
cremote fill-form --selector="#phone" --value="invalid-phone-123abc"
|
||||||
|
cremote submit-form --selector="#contact-form"
|
||||||
|
|
||||||
|
# Check error handling
|
||||||
|
ERROR_RESPONSE=$(cremote get-element --selector=".validation-error")
|
||||||
|
echo "Error handling: $ERROR_RESPONSE"
|
||||||
|
|
||||||
|
# Test file upload limits
|
||||||
|
cremote upload-file --selector="#file-upload" --file="/path/to/large-file.zip"
|
||||||
|
UPLOAD_ERROR=$(cremote get-element --selector=".upload-error" --selection-timeout=10)
|
||||||
|
|
||||||
|
# Test iframe interaction (e.g., payment form)
|
||||||
|
cremote switch-iframe --selector="iframe.payment-widget"
|
||||||
|
cremote fill-form --selector="#card-number" --value="4111111111111111"
|
||||||
|
cremote fill-form --selector="#expiry" --value="12/25"
|
||||||
|
cremote click-element --selector="#pay-now"
|
||||||
|
|
||||||
|
# Check for payment processing within iframe
|
||||||
|
PAYMENT_STATUS=$(cremote get-element --selector=".payment-status" --selection-timeout=10)
|
||||||
|
echo "Payment status: $PAYMENT_STATUS"
|
||||||
|
|
||||||
|
# Switch back to main page to check results
|
||||||
|
cremote switch-main
|
||||||
|
MAIN_STATUS=$(cremote get-element --selector=".order-confirmation" --selection-timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Best Practices
|
||||||
|
|
||||||
|
### 1. Robust Element Selection
|
||||||
|
|
||||||
|
Use specific, stable selectors:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Good - specific and stable
|
||||||
|
cremote click-element --selector="#submit-button"
|
||||||
|
cremote click-element --selector="button[data-testid='login-submit']"
|
||||||
|
cremote fill-form --selector="input[name='username']" --value="test"
|
||||||
|
|
||||||
|
# Avoid - fragile selectors
|
||||||
|
cremote click-element --selector="div > div:nth-child(3) > button"
|
||||||
|
cremote click-element --selector=".btn.btn-primary.mt-2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Wait for Dynamic Content
|
||||||
|
|
||||||
|
Always use appropriate timeouts for dynamic content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wait for AJAX content to load
|
||||||
|
cremote get-element --selector=".search-results" --selection-timeout=15
|
||||||
|
|
||||||
|
# Wait for form submission to complete
|
||||||
|
cremote submit-form --selector="#payment-form" --action-timeout=30
|
||||||
|
cremote wait-navigation --timeout=20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify Test Results
|
||||||
|
|
||||||
|
Always verify that actions had the expected effect:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After login, verify we're on the dashboard
|
||||||
|
cremote click-element --selector="#login-btn"
|
||||||
|
cremote wait-navigation --timeout=10
|
||||||
|
CURRENT_URL=$(cremote get-source | grep -o 'https://[^"]*dashboard[^"]*')
|
||||||
|
if [ -n "$CURRENT_URL" ]; then
|
||||||
|
echo "✓ Successfully redirected to dashboard"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# After form submission, check for success message
|
||||||
|
cremote submit-form --selector="#contact-form"
|
||||||
|
SUCCESS_MSG=$(cremote get-element --selector=".success-message" --selection-timeout=10)
|
||||||
|
if echo "$SUCCESS_MSG" | grep -q "Thank you"; then
|
||||||
|
echo "✓ Form submitted successfully"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Clean Test Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start each test with a fresh tab
|
||||||
|
cremote open-tab
|
||||||
|
|
||||||
|
# Clear any existing state if needed
|
||||||
|
cremote load-url --url="https://myapp.com/logout"
|
||||||
|
cremote wait-navigation --timeout=5
|
||||||
|
|
||||||
|
# Begin actual test
|
||||||
|
cremote load-url --url="https://myapp.com/test-page"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Iframe Context Management
|
||||||
|
|
||||||
|
Always manage iframe context properly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Good - explicit context management
|
||||||
|
cremote switch-iframe --selector="iframe.payment-form"
|
||||||
|
cremote fill-form --selector="#card-number" --value="4111111111111111"
|
||||||
|
cremote switch-main # Always switch back
|
||||||
|
|
||||||
|
# Good - verify iframe exists before switching
|
||||||
|
IFRAME_EXISTS=$(cremote get-element --selector="iframe.payment-form" --selection-timeout=5)
|
||||||
|
if [ -n "$IFRAME_EXISTS" ]; then
|
||||||
|
cremote switch-iframe --selector="iframe.payment-form"
|
||||||
|
# ... iframe operations ...
|
||||||
|
cremote switch-main
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Avoid - forgetting to switch back to main context
|
||||||
|
cremote switch-iframe --selector="iframe.widget"
|
||||||
|
cremote fill-form --selector="#field" --value="test"
|
||||||
|
# Missing: cremote switch-main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Failed Tests
|
||||||
|
|
||||||
|
### 1. Inspect Current State
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what's currently on the page
|
||||||
|
cremote get-source > debug-page-source.html
|
||||||
|
|
||||||
|
# Check specific elements
|
||||||
|
cremote get-element --selector=".error-message"
|
||||||
|
cremote get-element --selector="form"
|
||||||
|
|
||||||
|
# List all tabs to verify state
|
||||||
|
cremote list-tabs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verify Element Selectors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test if element exists before interacting
|
||||||
|
ELEMENT=$(cremote get-element --selector="#target-button" --selection-timeout=5)
|
||||||
|
if [ -n "$ELEMENT" ]; then
|
||||||
|
cremote click-element --selector="#target-button"
|
||||||
|
else
|
||||||
|
echo "Element not found - check selector"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Increase Timeouts for Slow Pages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For slow-loading applications
|
||||||
|
cremote fill-form --selector="#username" --value="test" \
|
||||||
|
--selection-timeout=30 --action-timeout=15
|
||||||
|
|
||||||
|
cremote wait-navigation --timeout=60
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting Chromium Connection Issues
|
||||||
|
|
||||||
|
### Chromium Not Running
|
||||||
|
If you see: "Chromium is not running with remote debugging enabled on port 9222"
|
||||||
|
|
||||||
|
**Solution**: Start Chromium with the correct flags:
|
||||||
|
```bash
|
||||||
|
chromium --remote-debugging-port=9222 --user-data-dir=/tmp/chromium-debug &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Conflict
|
||||||
|
If you see: "Something is listening on port 9222 but it's not Chromium DevTools protocol"
|
||||||
|
|
||||||
|
**Cause**: Another application is using port 9222
|
||||||
|
**Solution**:
|
||||||
|
1. Find what's using the port: `netstat -tlnp | grep 9222`
|
||||||
|
2. Stop the conflicting process
|
||||||
|
3. Start Chromium with the correct flags
|
||||||
|
|
||||||
|
### Chromium Running But Not Connecting
|
||||||
|
If Chromium appears to be running but cremotedaemon can't connect:
|
||||||
|
|
||||||
|
**Possible causes**:
|
||||||
|
- Chromium started without `--remote-debugging-port=9222`
|
||||||
|
- Chromium started with a different port
|
||||||
|
- Firewall blocking connections
|
||||||
|
|
||||||
|
**Solution**: Restart Chromium with the correct command:
|
||||||
|
```bash
|
||||||
|
pkill -f chromium # Stop existing Chromium
|
||||||
|
chromium --remote-debugging-port=9222 --user-data-dir=/tmp/chromium-debug &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Chromium DevTools is Working
|
||||||
|
You can manually check if Chromium DevTools is responding:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:9222/json/version
|
||||||
|
```
|
||||||
|
This should return JSON with Chromium version information.
|
||||||
|
|
||||||
|
## Integration with Test Suites
|
||||||
|
|
||||||
|
cremote can be integrated into larger test suites:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# test-suite.sh
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
echo "Starting Chromium with remote debugging..."
|
||||||
|
chromium --remote-debugging-port=9222 --user-data-dir=$(mktemp -d) &
|
||||||
|
CHROMIUM_PID=$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "Starting cremote daemon..."
|
||||||
|
cremotedaemon &
|
||||||
|
DAEMON_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
echo "Running login tests..."
|
||||||
|
./test-login.sh
|
||||||
|
|
||||||
|
echo "Running form tests..."
|
||||||
|
./test-forms.sh
|
||||||
|
|
||||||
|
echo "Running workflow tests..."
|
||||||
|
./test-workflows.sh
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
echo "Stopping daemon..."
|
||||||
|
kill $DAEMON_PID
|
||||||
|
echo "Stopping Chromium..."
|
||||||
|
kill $CHROMIUM_PID
|
||||||
|
```
|
||||||
|
|
||||||
|
This guide provides the foundation for using cremote as a comprehensive web application testing tool. Focus on testing real user workflows, handling edge cases, and verifying expected behaviors through the browser interface.
|
|
@ -0,0 +1,26 @@
|
||||||
|
.PHONY: all build clean daemon client install
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build: daemon client
|
||||||
|
|
||||||
|
daemon:
|
||||||
|
go build -o cremotedaemon ./daemon/cmd/cremotedaemon
|
||||||
|
|
||||||
|
client:
|
||||||
|
go build -o cremote .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f cremote cremotedaemon
|
||||||
|
|
||||||
|
install: build
|
||||||
|
@if [ -n "$(GOPATH)" ] && [ -d "$(GOPATH)/bin" ]; then \
|
||||||
|
echo "Installing to $(GOPATH)/bin/"; \
|
||||||
|
cp cremote $(GOPATH)/bin/; \
|
||||||
|
cp cremotedaemon $(GOPATH)/bin/; \
|
||||||
|
else \
|
||||||
|
echo "GOPATH not set or $(GOPATH)/bin doesn't exist, installing to ~/.local/bin/"; \
|
||||||
|
mkdir -p ~/.local/bin; \
|
||||||
|
cp cremote ~/.local/bin/; \
|
||||||
|
cp cremotedaemon ~/.local/bin/; \
|
||||||
|
fi
|
|
@ -0,0 +1,264 @@
|
||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/launcher"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager handles the connection to a Chrome browser instance
|
||||||
|
type Manager struct {
|
||||||
|
browser *rod.Browser
|
||||||
|
tabs map[string]*rod.Page
|
||||||
|
mu sync.Mutex
|
||||||
|
isNewBrowser bool // Tracks if we launched a new browser or connected to existing one
|
||||||
|
storage *TabStorage // Persistent storage for tab IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new browser manager
|
||||||
|
// If launchNew is true, it will launch a new browser instance
|
||||||
|
// Otherwise, it will try to connect to an existing browser instance
|
||||||
|
func NewManager(launchNew bool) (*Manager, error) {
|
||||||
|
var browser *rod.Browser
|
||||||
|
var isNewBrowser bool
|
||||||
|
|
||||||
|
// Initialize tab storage
|
||||||
|
storage, err := NewTabStorage()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize tab storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if launchNew {
|
||||||
|
// Launch a new browser instance
|
||||||
|
u := launcher.New().MustLaunch()
|
||||||
|
browser = rod.New().ControlURL(u).MustConnect()
|
||||||
|
isNewBrowser = true
|
||||||
|
} else {
|
||||||
|
// Connect to an existing browser instance
|
||||||
|
// This assumes Chrome is running with --remote-debugging-port=9222
|
||||||
|
u := launcher.MustResolveURL("")
|
||||||
|
browser = rod.New().ControlURL(u)
|
||||||
|
|
||||||
|
err := browser.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to browser: %w\nMake sure Chrome is running with --remote-debugging-port=9222", err)
|
||||||
|
}
|
||||||
|
isNewBrowser = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Manager{
|
||||||
|
browser: browser,
|
||||||
|
tabs: make(map[string]*rod.Page),
|
||||||
|
isNewBrowser: isNewBrowser,
|
||||||
|
storage: storage,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTab opens a new tab and returns its ID
|
||||||
|
func (m *Manager) OpenTab() (string, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
page, err := m.browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create new tab: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the page ID as the tab ID
|
||||||
|
tabID := string(page.TargetID)
|
||||||
|
m.tabs[tabID] = page
|
||||||
|
|
||||||
|
// Save the tab ID to persistent storage
|
||||||
|
err = m.storage.SaveTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save tab ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTab returns a tab by its ID
|
||||||
|
func (m *Manager) GetTab(tabID string) (*rod.Page, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// First check in-memory cache
|
||||||
|
page, exists := m.tabs[tabID]
|
||||||
|
if exists {
|
||||||
|
return page, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in memory, check persistent storage
|
||||||
|
storedID, exists := m.storage.GetTab(tabID)
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("tab not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the page from the browser
|
||||||
|
pages, err := m.browser.Pages()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get browser pages: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the page with the matching ID
|
||||||
|
for _, p := range pages {
|
||||||
|
if string(p.TargetID) == storedID {
|
||||||
|
// Cache it for future use
|
||||||
|
m.tabs[tabID] = p
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, the tab no longer exists
|
||||||
|
m.storage.RemoveTab(tabID)
|
||||||
|
return nil, errors.New("tab not found or was closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseTab closes a tab by its ID
|
||||||
|
func (m *Manager) CloseTab(tabID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// First check in-memory cache
|
||||||
|
page, exists := m.tabs[tabID]
|
||||||
|
if !exists {
|
||||||
|
// If not in memory, check persistent storage
|
||||||
|
storedID, exists := m.storage.GetTab(tabID)
|
||||||
|
if !exists {
|
||||||
|
return errors.New("tab not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the page from the browser
|
||||||
|
pages, err := m.browser.Pages()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get browser pages: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the page with the matching ID
|
||||||
|
for _, p := range pages {
|
||||||
|
if string(p.TargetID) == storedID {
|
||||||
|
page = p
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
// If we get here, the tab no longer exists, so just remove it from storage
|
||||||
|
m.storage.RemoveTab(tabID)
|
||||||
|
return errors.New("tab not found or was already closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := page.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close tab: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from in-memory cache and persistent storage
|
||||||
|
delete(m.tabs, tabID)
|
||||||
|
m.storage.RemoveTab(tabID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the browser connection and all tabs
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Clear the tabs map
|
||||||
|
m.tabs = make(map[string]*rod.Page)
|
||||||
|
|
||||||
|
// Only close the browser if we launched it
|
||||||
|
if m.isNewBrowser {
|
||||||
|
return m.browser.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For existing browsers, just disconnect without closing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadURL loads a URL in a tab
|
||||||
|
func (m *Manager) LoadURL(tabID, url string) error {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = page.Navigate(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to navigate to URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the page to be loaded
|
||||||
|
err = page.WaitLoad()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wait for page load: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitNavigation waits for a navigation event to happen
|
||||||
|
func (m *Manager) WaitNavigation(tabID string, timeout int) error {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timeout for the navigation wait
|
||||||
|
page = page.Timeout(time.Duration(timeout) * time.Second)
|
||||||
|
|
||||||
|
// Wait for navigation
|
||||||
|
page.WaitNavigation(proto.PageLifecycleEventNameLoad)()
|
||||||
|
|
||||||
|
// Wait for the page to be fully loaded
|
||||||
|
err = page.WaitLoad()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("navigation wait failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPageSource returns the entire source code of a page
|
||||||
|
func (m *Manager) GetPageSource(tabID string) (string, error) {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := page.HTML()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get page HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetElementHTML returns the HTML of an element at the specified selector
|
||||||
|
func (m *Manager) GetElementHTML(tabID, selector string) (string, error) {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the element
|
||||||
|
element, err := page.Element(selector)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to find element: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the HTML of the element
|
||||||
|
html, err := element.HTML()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get element HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html, nil
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FillFormField fills a form field with the specified value
|
||||||
|
func (m *Manager) FillFormField(tabID, selector, value string) error {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the element
|
||||||
|
element, err := page.Element(selector)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find element: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the field first
|
||||||
|
_ = element.SelectAllText()
|
||||||
|
err = element.Input("")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to clear field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input the value
|
||||||
|
err = element.Input(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to input value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFile uploads a file to a file input element
|
||||||
|
func (m *Manager) UploadFile(tabID, selector, filePath string) error {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file exists
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the file input element
|
||||||
|
element, err := page.Element(selector)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find file input element: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the file
|
||||||
|
err = element.SetFiles([]string{absPath})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitForm submits a form
|
||||||
|
func (m *Manager) SubmitForm(tabID, selector string) error {
|
||||||
|
page, err := m.GetTab(tabID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the form element
|
||||||
|
form, err := page.Element(selector)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find form element: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
_, err = form.Eval(`() => this.submit()`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to submit form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the page to load after form submission
|
||||||
|
err = page.WaitLoad()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wait for page load after form submission: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TabStorage manages persistent storage of tab IDs
|
||||||
|
type TabStorage struct {
|
||||||
|
Tabs map[string]string // Maps tab IDs to their internal IDs
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTabStorage creates a new tab storage
|
||||||
|
func NewTabStorage() (*TabStorage, error) {
|
||||||
|
storage := &TabStorage{
|
||||||
|
Tabs: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing tabs from storage
|
||||||
|
err := storage.load()
|
||||||
|
if err != nil {
|
||||||
|
// If the file doesn't exist, that's fine - we'll create it
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("failed to load tab storage: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveTab saves a tab ID to storage
|
||||||
|
func (s *TabStorage) SaveTab(tabID string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.Tabs[tabID] = tabID
|
||||||
|
return s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTab gets a tab ID from storage
|
||||||
|
func (s *TabStorage) GetTab(tabID string) (string, bool) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
internalID, exists := s.Tabs[tabID]
|
||||||
|
return internalID, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTab removes a tab ID from storage
|
||||||
|
func (s *TabStorage) RemoveTab(tabID string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.Tabs, tabID)
|
||||||
|
return s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStoragePath returns the path to the storage file
|
||||||
|
func getStoragePath() (string, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create .cremote directory if it doesn't exist
|
||||||
|
storageDir := filepath.Join(homeDir, ".cremote")
|
||||||
|
err = os.MkdirAll(storageDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create storage directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(storageDir, "tabs.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load loads the tab storage from disk
|
||||||
|
func (s *TabStorage) load() error {
|
||||||
|
path, err := getStoragePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(data, &s.Tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save saves the tab storage to disk
|
||||||
|
func (s *TabStorage) save() error {
|
||||||
|
path, err := getStoragePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(s.Tabs, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal tab storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
|
@ -0,0 +1,604 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the client for communicating with the daemon
|
||||||
|
type Client struct {
|
||||||
|
serverURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command represents a command sent from the client to the daemon
|
||||||
|
type Command struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Params map[string]string `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response represents a response from the daemon to the client
|
||||||
|
type Response struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new client
|
||||||
|
func NewClient(host string, port int) *Client {
|
||||||
|
return &Client{
|
||||||
|
serverURL: fmt.Sprintf("http://%s:%d", host, port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckStatus checks if the daemon is running
|
||||||
|
func (c *Client) CheckStatus() (bool, error) {
|
||||||
|
resp, err := http.Get(c.serverURL + "/status")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabInfo contains information about a tab
|
||||||
|
type TabInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
IsCurrent bool `json:"is_current"`
|
||||||
|
HistoryIndex int `json:"history_index"` // Position in tab history (higher = more recent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTabs returns a list of all open tabs
|
||||||
|
func (c *Client) ListTabs() ([]TabInfo, error) {
|
||||||
|
resp, err := http.Get(c.serverURL + "/status")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return nil, fmt.Errorf("daemon returned error: %s", response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the data
|
||||||
|
data, ok := response.Data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected response format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the tabs
|
||||||
|
tabsData, ok := data["tabs"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected tabs format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current tab
|
||||||
|
currentTab, _ := data["current_tab"].(string)
|
||||||
|
|
||||||
|
// Get the tab history
|
||||||
|
tabHistoryData, ok := data["tab_history"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
tabHistoryData = []interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of tab history indices
|
||||||
|
tabHistoryIndices := make(map[string]int)
|
||||||
|
for i, idInterface := range tabHistoryData {
|
||||||
|
id, ok := idInterface.(string)
|
||||||
|
if ok {
|
||||||
|
tabHistoryIndices[id] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to TabInfo
|
||||||
|
tabs := make([]TabInfo, 0, len(tabsData))
|
||||||
|
for id, urlInterface := range tabsData {
|
||||||
|
url, ok := urlInterface.(string)
|
||||||
|
if !ok {
|
||||||
|
url = "<unknown>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the history index (default to -1 if not in history)
|
||||||
|
historyIndex, inHistory := tabHistoryIndices[id]
|
||||||
|
if !inHistory {
|
||||||
|
historyIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs = append(tabs, TabInfo{
|
||||||
|
ID: id,
|
||||||
|
URL: url,
|
||||||
|
IsCurrent: id == currentTab,
|
||||||
|
HistoryIndex: historyIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCommand sends a command to the daemon
|
||||||
|
func (c *Client) SendCommand(action string, params map[string]string) (*Response, error) {
|
||||||
|
cmd := Command{
|
||||||
|
Action: action,
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(c.serverURL+"/command", "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTab opens a new tab
|
||||||
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) OpenTab(timeout int) (string, error) {
|
||||||
|
params := map[string]string{}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("open-tab", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return "", fmt.Errorf("failed to open tab: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
tabID, ok := resp.Data.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected response data type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadURL loads a URL in a tab
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) LoadURL(tabID, url string, timeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("load-url", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to load URL: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillFormField fills a form field with a value
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
|
||||||
|
func (c *Client) FillFormField(tabID, selector, value string, selectionTimeout, actionTimeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"selector": selector,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeouts if specified
|
||||||
|
if selectionTimeout > 0 {
|
||||||
|
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionTimeout > 0 {
|
||||||
|
params["action-timeout"] = strconv.Itoa(actionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("fill-form", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to fill form field: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFile uploads a file to a file input
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
|
||||||
|
func (c *Client) UploadFile(tabID, selector, filePath string, selectionTimeout, actionTimeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"selector": selector,
|
||||||
|
"file": filePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeouts if specified
|
||||||
|
if selectionTimeout > 0 {
|
||||||
|
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionTimeout > 0 {
|
||||||
|
params["action-timeout"] = strconv.Itoa(actionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("upload-file", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to upload file: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitForm submits a form
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
|
||||||
|
func (c *Client) SubmitForm(tabID, selector string, selectionTimeout, actionTimeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"selector": selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeouts if specified
|
||||||
|
if selectionTimeout > 0 {
|
||||||
|
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionTimeout > 0 {
|
||||||
|
params["action-timeout"] = strconv.Itoa(actionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("submit-form", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to submit form: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPageSource gets the source code of a page
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) GetPageSource(tabID string, timeout int) (string, error) {
|
||||||
|
params := map[string]string{}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("get-source", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return "", fmt.Errorf("failed to get page source: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
source, ok := resp.Data.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected response data type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetElementHTML gets the HTML of an element
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// selectionTimeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) GetElementHTML(tabID, selector string, selectionTimeout int) (string, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"selector": selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if selectionTimeout > 0 {
|
||||||
|
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("get-element", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return "", fmt.Errorf("failed to get element HTML: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
html, ok := resp.Data.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected response data type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return html, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseTab closes a tab
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) CloseTab(tabID string, timeout int) error {
|
||||||
|
params := map[string]string{}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("close-tab", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to close tab: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitNavigation waits for a navigation event
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
func (c *Client) WaitNavigation(tabID string, timeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"timeout": fmt.Sprintf("%d", timeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("wait-navigation", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to wait for navigation: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvalJS executes JavaScript code in a tab and returns the result
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) EvalJS(tabID, jsCode string, timeout int) (string, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"code": jsCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("eval-js", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return "", fmt.Errorf("failed to execute JavaScript: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert result to string if it exists
|
||||||
|
if resp.Data != nil {
|
||||||
|
if result, ok := resp.Data.(string); ok {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
// If it's not a string, convert it to string representation
|
||||||
|
return fmt.Sprintf("%v", resp.Data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TakeScreenshot takes a screenshot of a tab and saves it to a file
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"output": outputPath,
|
||||||
|
"full-page": strconv.FormatBool(fullPage),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("screenshot", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to take screenshot: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchToIframe switches the context to an iframe for subsequent commands
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
func (c *Client) SwitchToIframe(tabID, selector string) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"selector": selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("switch-iframe", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to switch to iframe: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchToMain switches the context back to the main page
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
func (c *Client) SwitchToMain(tabID string) error {
|
||||||
|
params := map[string]string{}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("switch-main", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to switch to main context: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickElement clicks on an element
|
||||||
|
// If tabID is empty, the current tab will be used
|
||||||
|
// selectionTimeout and actionTimeout are in seconds, 0 means no timeout
|
||||||
|
func (c *Client) ClickElement(tabID, selector string, selectionTimeout, actionTimeout int) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"selector": selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include tab ID if it's provided
|
||||||
|
if tabID != "" {
|
||||||
|
params["tab"] = tabID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timeouts if specified
|
||||||
|
if selectionTimeout > 0 {
|
||||||
|
params["selection-timeout"] = strconv.Itoa(selectionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionTimeout > 0 {
|
||||||
|
params["action-timeout"] = strconv.Itoa(actionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.SendCommand("click-element", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
return fmt.Errorf("failed to click element: %s", resp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.teamworkapps.com/shortcut/cremote/daemon"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
daemonHost = flag.String("listen", "localhost", "Listen address")
|
||||||
|
port = flag.Int("port", 8989, "Listen port")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Create and start the daemon
|
||||||
|
d, err := daemon.NewDaemon(*daemonHost, *port)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
fmt.Println("Shutting down daemon...")
|
||||||
|
d.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start the daemon (this blocks until the server is stopped)
|
||||||
|
log.Printf("Starting daemon on port %d", *port)
|
||||||
|
if err := d.Start(); err != nil {
|
||||||
|
log.Fatalf("Failed to start daemon: %v", err)
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
||||||
|
module git.teamworkapps.com/shortcut/cremote
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
|
|
||||||
|
require github.com/go-rod/rod v0.116.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||||
|
github.com/ysmood/goob v0.4.0 // indirect
|
||||||
|
github.com/ysmood/got v0.40.0 // indirect
|
||||||
|
github.com/ysmood/gson v0.7.3 // indirect
|
||||||
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||||
|
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||||
|
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
||||||
|
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
||||||
|
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||||
|
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||||
|
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||||
|
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||||
|
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||||
|
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||||
|
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||||
|
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||||
|
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||||
|
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||||
|
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||||
|
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
|
@ -0,0 +1,496 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.teamworkapps.com/shortcut/cremote/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Global flags
|
||||||
|
daemonHost = flag.String("host", "localhost", "Daemon host")
|
||||||
|
daemonPort = flag.Int("port", 8989, "Daemon port")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Define subcommands
|
||||||
|
openTabCmd := flag.NewFlagSet("open-tab", flag.ExitOnError)
|
||||||
|
loadURLCmd := flag.NewFlagSet("load-url", flag.ExitOnError)
|
||||||
|
fillFormCmd := flag.NewFlagSet("fill-form", flag.ExitOnError)
|
||||||
|
uploadFileCmd := flag.NewFlagSet("upload-file", flag.ExitOnError)
|
||||||
|
submitFormCmd := flag.NewFlagSet("submit-form", flag.ExitOnError)
|
||||||
|
getSourceCmd := flag.NewFlagSet("get-source", flag.ExitOnError)
|
||||||
|
getElementCmd := flag.NewFlagSet("get-element", flag.ExitOnError)
|
||||||
|
clickElementCmd := flag.NewFlagSet("click-element", flag.ExitOnError)
|
||||||
|
closeTabCmd := flag.NewFlagSet("close-tab", flag.ExitOnError)
|
||||||
|
waitNavCmd := flag.NewFlagSet("wait-navigation", flag.ExitOnError)
|
||||||
|
evalJsCmd := flag.NewFlagSet("eval-js", flag.ExitOnError)
|
||||||
|
switchIframeCmd := flag.NewFlagSet("switch-iframe", flag.ExitOnError)
|
||||||
|
switchMainCmd := flag.NewFlagSet("switch-main", flag.ExitOnError)
|
||||||
|
screenshotCmd := flag.NewFlagSet("screenshot", flag.ExitOnError)
|
||||||
|
statusCmd := flag.NewFlagSet("status", flag.ExitOnError)
|
||||||
|
listTabsCmd := flag.NewFlagSet("list-tabs", flag.ExitOnError)
|
||||||
|
|
||||||
|
// Define flags for each subcommand
|
||||||
|
// open-tab flags
|
||||||
|
openTabTimeout := openTabCmd.Int("timeout", 5, "Timeout in seconds for opening the tab")
|
||||||
|
openTabHost := openTabCmd.String("host", "localhost", "Daemon host")
|
||||||
|
openTabPort := openTabCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// load-url flags
|
||||||
|
loadURLTabID := loadURLCmd.String("tab", "", "Tab ID to load URL in (optional, uses current tab if not specified)")
|
||||||
|
loadURLTarget := loadURLCmd.String("url", "", "URL to load")
|
||||||
|
loadURLTimeout := loadURLCmd.Int("timeout", 5, "Timeout in seconds for loading the URL")
|
||||||
|
loadURLHost := loadURLCmd.String("host", "localhost", "Daemon host")
|
||||||
|
loadURLPort := loadURLCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// fill-form flags
|
||||||
|
fillFormTabID := fillFormCmd.String("tab", "", "Tab ID to fill form in (optional, uses current tab if not specified)")
|
||||||
|
fillFormSelector := fillFormCmd.String("selector", "", "CSS selector for the input field")
|
||||||
|
fillFormValue := fillFormCmd.String("value", "", "Value to fill in the form field")
|
||||||
|
fillFormSelectionTimeout := fillFormCmd.Int("selection-timeout", 5, "Timeout in seconds for finding the element")
|
||||||
|
fillFormActionTimeout := fillFormCmd.Int("action-timeout", 5, "Timeout in seconds for the fill action")
|
||||||
|
fillFormHost := fillFormCmd.String("host", "localhost", "Daemon host")
|
||||||
|
fillFormPort := fillFormCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// upload-file flags
|
||||||
|
uploadFileTabID := uploadFileCmd.String("tab", "", "Tab ID to upload file in (optional, uses current tab if not specified)")
|
||||||
|
uploadFileSelector := uploadFileCmd.String("selector", "", "CSS selector for the file input")
|
||||||
|
uploadFilePath := uploadFileCmd.String("file", "", "Path to the file to upload")
|
||||||
|
uploadFileSelectionTimeout := uploadFileCmd.Int("selection-timeout", 5, "Timeout in seconds for finding the element")
|
||||||
|
uploadFileActionTimeout := uploadFileCmd.Int("action-timeout", 5, "Timeout in seconds for the upload action")
|
||||||
|
uploadFileHost := uploadFileCmd.String("host", "localhost", "Daemon host")
|
||||||
|
uploadFilePort := uploadFileCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// submit-form flags
|
||||||
|
submitFormTabID := submitFormCmd.String("tab", "", "Tab ID to submit form in (optional, uses current tab if not specified)")
|
||||||
|
submitFormSelector := submitFormCmd.String("selector", "", "CSS selector for the form")
|
||||||
|
submitFormSelectionTimeout := submitFormCmd.Int("selection-timeout", 5, "Timeout in seconds for finding the element")
|
||||||
|
submitFormActionTimeout := submitFormCmd.Int("action-timeout", 5, "Timeout in seconds for the submit action")
|
||||||
|
submitFormHost := submitFormCmd.String("host", "localhost", "Daemon host")
|
||||||
|
submitFormPort := submitFormCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// get-source flags
|
||||||
|
getSourceTabID := getSourceCmd.String("tab", "", "Tab ID to get source from (optional, uses current tab if not specified)")
|
||||||
|
getSourceTimeout := getSourceCmd.Int("timeout", 5, "Timeout in seconds for getting the page source")
|
||||||
|
getSourceHost := getSourceCmd.String("host", "localhost", "Daemon host")
|
||||||
|
getSourcePort := getSourceCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// get-element flags
|
||||||
|
getElementTabID := getElementCmd.String("tab", "", "Tab ID to get element from (optional, uses current tab if not specified)")
|
||||||
|
getElementSelector := getElementCmd.String("selector", "", "CSS selector for the element")
|
||||||
|
getElementSelectionTimeout := getElementCmd.Int("selection-timeout", 5, "Timeout in seconds for finding the element")
|
||||||
|
getElementHost := getElementCmd.String("host", "localhost", "Daemon host")
|
||||||
|
getElementPort := getElementCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// click-element flags
|
||||||
|
clickElementTabID := clickElementCmd.String("tab", "", "Tab ID to click element in (optional, uses current tab if not specified)")
|
||||||
|
clickElementSelector := clickElementCmd.String("selector", "", "CSS selector for the element to click")
|
||||||
|
clickElementSelectionTimeout := clickElementCmd.Int("selection-timeout", 5, "Timeout in seconds for finding the element")
|
||||||
|
clickElementActionTimeout := clickElementCmd.Int("action-timeout", 5, "Timeout in seconds for the click action")
|
||||||
|
clickElementHost := clickElementCmd.String("host", "localhost", "Daemon host")
|
||||||
|
clickElementPort := clickElementCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// close-tab flags
|
||||||
|
closeTabID := closeTabCmd.String("tab", "", "Tab ID to close (optional, uses current tab if not specified)")
|
||||||
|
closeTabTimeout := closeTabCmd.Int("timeout", 5, "Timeout in seconds for closing the tab")
|
||||||
|
closeTabHost := closeTabCmd.String("host", "localhost", "Daemon host")
|
||||||
|
closeTabPort := closeTabCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// wait-navigation flags
|
||||||
|
waitNavTabID := waitNavCmd.String("tab", "", "Tab ID to wait for navigation (optional, uses current tab if not specified)")
|
||||||
|
waitNavTimeout := waitNavCmd.Int("timeout", 5, "Timeout in seconds")
|
||||||
|
waitNavHost := waitNavCmd.String("host", "localhost", "Daemon host")
|
||||||
|
waitNavPort := waitNavCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// eval-js flags
|
||||||
|
evalJsTabID := evalJsCmd.String("tab", "", "Tab ID to execute JavaScript in (optional, uses current tab if not specified)")
|
||||||
|
evalJsCode := evalJsCmd.String("code", "", "JavaScript code to execute")
|
||||||
|
evalJsTimeout := evalJsCmd.Int("timeout", 5, "Timeout in seconds for JavaScript execution")
|
||||||
|
evalJsHost := evalJsCmd.String("host", "localhost", "Daemon host")
|
||||||
|
evalJsPort := evalJsCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// switch-iframe flags
|
||||||
|
switchIframeTabID := switchIframeCmd.String("tab", "", "Tab ID to switch iframe context in (optional, uses current tab if not specified)")
|
||||||
|
switchIframeSelector := switchIframeCmd.String("selector", "", "CSS selector for the iframe element")
|
||||||
|
switchIframeHost := switchIframeCmd.String("host", "localhost", "Daemon host")
|
||||||
|
switchIframePort := switchIframeCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// switch-main flags
|
||||||
|
switchMainTabID := switchMainCmd.String("tab", "", "Tab ID to switch back to main context (optional, uses current tab if not specified)")
|
||||||
|
switchMainHost := switchMainCmd.String("host", "localhost", "Daemon host")
|
||||||
|
switchMainPort := switchMainCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// screenshot flags
|
||||||
|
screenshotTabID := screenshotCmd.String("tab", "", "Tab ID to take screenshot of (optional, uses current tab if not specified)")
|
||||||
|
screenshotOutput := screenshotCmd.String("output", "", "Output file path for the screenshot (PNG format)")
|
||||||
|
screenshotFullPage := screenshotCmd.Bool("full-page", false, "Capture full page screenshot (default: viewport only)")
|
||||||
|
screenshotTimeout := screenshotCmd.Int("timeout", 5, "Timeout in seconds for taking the screenshot")
|
||||||
|
screenshotHost := screenshotCmd.String("host", "localhost", "Daemon host")
|
||||||
|
screenshotPort := screenshotCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// status flags
|
||||||
|
statusHost := statusCmd.String("host", "localhost", "Daemon host")
|
||||||
|
statusPort := statusCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// list-tabs flags
|
||||||
|
listTabsHost := listTabsCmd.String("host", "localhost", "Daemon host")
|
||||||
|
listTabsPort := listTabsCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
// Check if a subcommand is provided
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the appropriate subcommand
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "open-tab":
|
||||||
|
openTabCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*openTabHost, *openTabPort)
|
||||||
|
|
||||||
|
// Open a new tab
|
||||||
|
tabID, err := c.OpenTab(*openTabTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the tab ID
|
||||||
|
fmt.Println(tabID)
|
||||||
|
|
||||||
|
case "load-url":
|
||||||
|
loadURLCmd.Parse(os.Args[2:])
|
||||||
|
if *loadURLTarget == "" {
|
||||||
|
fmt.Println("Error: url flag is required")
|
||||||
|
loadURLCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*loadURLHost, *loadURLPort)
|
||||||
|
|
||||||
|
// Load the URL
|
||||||
|
err := c.LoadURL(*loadURLTabID, *loadURLTarget, *loadURLTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("URL loaded successfully")
|
||||||
|
|
||||||
|
case "fill-form":
|
||||||
|
fillFormCmd.Parse(os.Args[2:])
|
||||||
|
if *fillFormSelector == "" || *fillFormValue == "" {
|
||||||
|
fmt.Println("Error: selector and value flags are required")
|
||||||
|
fillFormCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*fillFormHost, *fillFormPort)
|
||||||
|
|
||||||
|
// Fill the form field
|
||||||
|
err := c.FillFormField(*fillFormTabID, *fillFormSelector, *fillFormValue, *fillFormSelectionTimeout, *fillFormActionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Form field filled successfully")
|
||||||
|
|
||||||
|
case "upload-file":
|
||||||
|
uploadFileCmd.Parse(os.Args[2:])
|
||||||
|
if *uploadFileSelector == "" || *uploadFilePath == "" {
|
||||||
|
fmt.Println("Error: selector and file flags are required")
|
||||||
|
uploadFileCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*uploadFileHost, *uploadFilePort)
|
||||||
|
|
||||||
|
// Upload the file
|
||||||
|
err := c.UploadFile(*uploadFileTabID, *uploadFileSelector, *uploadFilePath, *uploadFileSelectionTimeout, *uploadFileActionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("File uploaded successfully")
|
||||||
|
|
||||||
|
case "submit-form":
|
||||||
|
submitFormCmd.Parse(os.Args[2:])
|
||||||
|
if *submitFormSelector == "" {
|
||||||
|
fmt.Println("Error: selector flag is required")
|
||||||
|
submitFormCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*submitFormHost, *submitFormPort)
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
err := c.SubmitForm(*submitFormTabID, *submitFormSelector, *submitFormSelectionTimeout, *submitFormActionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Form submitted successfully")
|
||||||
|
|
||||||
|
case "get-source":
|
||||||
|
getSourceCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*getSourceHost, *getSourcePort)
|
||||||
|
|
||||||
|
// Get the page source
|
||||||
|
source, err := c.GetPageSource(*getSourceTabID, *getSourceTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the source
|
||||||
|
fmt.Println(source)
|
||||||
|
|
||||||
|
case "get-element":
|
||||||
|
getElementCmd.Parse(os.Args[2:])
|
||||||
|
if *getElementSelector == "" {
|
||||||
|
fmt.Println("Error: selector flag is required")
|
||||||
|
getElementCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*getElementHost, *getElementPort)
|
||||||
|
|
||||||
|
// Get the element HTML
|
||||||
|
html, err := c.GetElementHTML(*getElementTabID, *getElementSelector, *getElementSelectionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the HTML
|
||||||
|
fmt.Println(html)
|
||||||
|
|
||||||
|
case "click-element":
|
||||||
|
clickElementCmd.Parse(os.Args[2:])
|
||||||
|
if *clickElementSelector == "" {
|
||||||
|
fmt.Println("Error: selector flag is required")
|
||||||
|
clickElementCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*clickElementHost, *clickElementPort)
|
||||||
|
|
||||||
|
// Click the element
|
||||||
|
err := c.ClickElement(*clickElementTabID, *clickElementSelector, *clickElementSelectionTimeout, *clickElementActionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Element clicked successfully")
|
||||||
|
|
||||||
|
case "close-tab":
|
||||||
|
closeTabCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*closeTabHost, *closeTabPort)
|
||||||
|
|
||||||
|
// Close the tab
|
||||||
|
err := c.CloseTab(*closeTabID, *closeTabTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Tab closed successfully")
|
||||||
|
|
||||||
|
case "wait-navigation":
|
||||||
|
waitNavCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*waitNavHost, *waitNavPort)
|
||||||
|
|
||||||
|
// Wait for navigation
|
||||||
|
err := c.WaitNavigation(*waitNavTabID, *waitNavTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Navigation completed")
|
||||||
|
|
||||||
|
case "eval-js":
|
||||||
|
evalJsCmd.Parse(os.Args[2:])
|
||||||
|
if *evalJsCode == "" {
|
||||||
|
fmt.Println("Error: code flag is required")
|
||||||
|
evalJsCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*evalJsHost, *evalJsPort)
|
||||||
|
|
||||||
|
// Execute JavaScript
|
||||||
|
result, err := c.EvalJS(*evalJsTabID, *evalJsCode, *evalJsTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the result if there is one
|
||||||
|
if result != "" {
|
||||||
|
fmt.Println(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "switch-iframe":
|
||||||
|
switchIframeCmd.Parse(os.Args[2:])
|
||||||
|
if *switchIframeSelector == "" {
|
||||||
|
fmt.Println("Error: selector flag is required")
|
||||||
|
switchIframeCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*switchIframeHost, *switchIframePort)
|
||||||
|
|
||||||
|
// Switch to iframe
|
||||||
|
err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Switched to iframe context")
|
||||||
|
|
||||||
|
case "switch-main":
|
||||||
|
switchMainCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*switchMainHost, *switchMainPort)
|
||||||
|
|
||||||
|
// Switch back to main page
|
||||||
|
err := c.SwitchToMain(*switchMainTabID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Switched to main page context")
|
||||||
|
|
||||||
|
case "screenshot":
|
||||||
|
screenshotCmd.Parse(os.Args[2:])
|
||||||
|
if *screenshotOutput == "" {
|
||||||
|
fmt.Println("Error: output flag is required")
|
||||||
|
screenshotCmd.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*screenshotHost, *screenshotPort)
|
||||||
|
|
||||||
|
// Take screenshot
|
||||||
|
err := c.TakeScreenshot(*screenshotTabID, *screenshotOutput, *screenshotFullPage, *screenshotTimeout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Screenshot saved to: %s\n", *screenshotOutput)
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
statusCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*statusHost, *statusPort)
|
||||||
|
|
||||||
|
// Check daemon status
|
||||||
|
running, err := c.CheckStatus()
|
||||||
|
if err != nil {
|
||||||
|
// If we can't connect, the daemon is not running
|
||||||
|
fmt.Println("Daemon is not running")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if running {
|
||||||
|
fmt.Println("Daemon is running")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Daemon is not running")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "list-tabs":
|
||||||
|
listTabsCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
c := client.NewClient(*listTabsHost, *listTabsPort)
|
||||||
|
|
||||||
|
// List tabs
|
||||||
|
tabs, err := c.ListTabs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tabs by history index (most recent first)
|
||||||
|
sort.Slice(tabs, func(i, j int) bool {
|
||||||
|
return tabs[i].HistoryIndex > tabs[j].HistoryIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
// Print the tabs
|
||||||
|
if len(tabs) == 0 {
|
||||||
|
fmt.Println("No tabs open")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Open tabs (in order of recent use):")
|
||||||
|
for _, tab := range tabs {
|
||||||
|
currentMarker := " "
|
||||||
|
if tab.IsCurrent {
|
||||||
|
currentMarker = "*"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %s: %s\n", currentMarker, tab.ID, tab.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Println("Usage: cremote <command> [options]")
|
||||||
|
fmt.Println("Commands:")
|
||||||
|
fmt.Println(" open-tab Open a new tab and return its ID")
|
||||||
|
fmt.Println(" load-url Load a URL in a tab")
|
||||||
|
fmt.Println(" fill-form Fill a form field with a value")
|
||||||
|
fmt.Println(" upload-file Upload a file to a file input")
|
||||||
|
fmt.Println(" submit-form Submit a form")
|
||||||
|
fmt.Println(" get-source Get the source code of a page")
|
||||||
|
fmt.Println(" get-element Get the HTML of an element")
|
||||||
|
fmt.Println(" click-element Click on an element")
|
||||||
|
fmt.Println(" close-tab Close a tab")
|
||||||
|
fmt.Println(" wait-navigation Wait for a navigation event")
|
||||||
|
fmt.Println(" eval-js Execute JavaScript code in a tab")
|
||||||
|
fmt.Println(" switch-iframe Switch to iframe context for subsequent commands")
|
||||||
|
fmt.Println(" switch-main Switch back to main page context")
|
||||||
|
fmt.Println(" screenshot Take a screenshot of the current page")
|
||||||
|
fmt.Println(" list-tabs List all open tabs")
|
||||||
|
fmt.Println(" status Check if the daemon is running")
|
||||||
|
fmt.Println("\nRun 'cremote <command> -h' for more information on a command")
|
||||||
|
fmt.Println("\nBefore using this tool, make sure the daemon is running:")
|
||||||
|
fmt.Println(" cremotedaemon")
|
||||||
|
fmt.Println("\nThe daemon requires Chromium/Chrome to be running with remote debugging enabled:")
|
||||||
|
fmt.Println(" chromium --remote-debugging-port=9222 --user-data-dir=/tmp/chromium-debug")
|
||||||
|
fmt.Println("\nNote: Most commands can use the current tab if you don't specify a tab ID.")
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Checkbox Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Checkbox Test</h1>
|
||||||
|
<form id="testForm">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="checkbox1" name="checkbox1">
|
||||||
|
<label for="checkbox1">Checkbox 1</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="checkbox2" name="checkbox2">
|
||||||
|
<label for="checkbox2">Checkbox 2</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="radio1" name="radioGroup" value="option1">
|
||||||
|
<label for="radio1">Radio 1</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="radio2" name="radioGroup" value="option2">
|
||||||
|
<label for="radio2">Radio 2</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="textInput" name="textInput" placeholder="Text input">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" id="showValues" onclick="showValues()">Show Values</button>
|
||||||
|
</div>
|
||||||
|
<div id="result"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showValues() {
|
||||||
|
const checkbox1 = document.getElementById('checkbox1').checked;
|
||||||
|
const checkbox2 = document.getElementById('checkbox2').checked;
|
||||||
|
const radio1 = document.getElementById('radio1').checked;
|
||||||
|
const radio2 = document.getElementById('radio2').checked;
|
||||||
|
const textInput = document.getElementById('textInput').value;
|
||||||
|
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.innerHTML = `
|
||||||
|
<p>Checkbox 1: ${checkbox1}</p>
|
||||||
|
<p>Checkbox 2: ${checkbox2}</p>
|
||||||
|
<p>Radio 1: ${radio1}</p>
|
||||||
|
<p>Radio 2: ${radio2}</p>
|
||||||
|
<p>Text Input: ${textInput}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Checkbox Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Checkbox Test</h1>
|
||||||
|
<form id="testForm">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="checkbox1" name="checkbox1" onchange="showValues()">
|
||||||
|
<label for="checkbox1">Checkbox 1</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="checkbox2" name="checkbox2" onchange="showValues()">
|
||||||
|
<label for="checkbox2">Checkbox 2</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="radio1" name="radioGroup" value="option1" onchange="showValues()">
|
||||||
|
<label for="radio1">Radio 1</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="radio2" name="radioGroup" value="option2" onchange="showValues()">
|
||||||
|
<label for="radio2">Radio 2</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="textInput" name="textInput" placeholder="Text input" oninput="showValues()">
|
||||||
|
</div>
|
||||||
|
<div id="result">
|
||||||
|
<p>Checkbox 1: false</p>
|
||||||
|
<p>Checkbox 2: false</p>
|
||||||
|
<p>Radio 1: false</p>
|
||||||
|
<p>Radio 2: false</p>
|
||||||
|
<p>Text Input: </p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showValues() {
|
||||||
|
const checkbox1 = document.getElementById('checkbox1').checked;
|
||||||
|
const checkbox2 = document.getElementById('checkbox2').checked;
|
||||||
|
const radio1 = document.getElementById('radio1').checked;
|
||||||
|
const radio2 = document.getElementById('radio2').checked;
|
||||||
|
const textInput = document.getElementById('textInput').value;
|
||||||
|
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.innerHTML = `
|
||||||
|
<p>Checkbox 1: ${checkbox1}</p>
|
||||||
|
<p>Checkbox 2: ${checkbox2}</p>
|
||||||
|
<p>Radio 1: ${radio1}</p>
|
||||||
|
<p>Radio 2: ${radio2}</p>
|
||||||
|
<p>Text Input: ${textInput}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('Values updated:', { checkbox1, checkbox2, radio1, radio2, textInput });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Timeout Test</title>
|
||||||
|
<style>
|
||||||
|
.delayed-element {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Timeout Test</h1>
|
||||||
|
<div id="immediate">This element is immediately available</div>
|
||||||
|
<div id="delayed" class="delayed-element">This element appears after 3 seconds</div>
|
||||||
|
<button id="slow-button">Slow Button (3s delay)</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Show the delayed element after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('delayed').style.display = 'block';
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Add a click handler to the slow button that takes 3 seconds to complete
|
||||||
|
document.getElementById('slow-button').addEventListener('click', function() {
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.textContent = 'Processing...';
|
||||||
|
|
||||||
|
// Simulate a slow operation
|
||||||
|
setTimeout(() => {
|
||||||
|
result.textContent = 'Button click processed!';
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Timeout Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Timeout Test</h1>
|
||||||
|
<div id="immediate">This element is immediately available</div>
|
||||||
|
<div id="result"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Create the delayed element after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
const delayed = document.createElement('div');
|
||||||
|
delayed.id = 'delayed';
|
||||||
|
delayed.textContent = 'This element appears after 3 seconds';
|
||||||
|
document.body.appendChild(delayed);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Create the slow button
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.id = 'slow-button';
|
||||||
|
button.textContent = 'Slow Button (3s delay)';
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.textContent = 'Processing...';
|
||||||
|
|
||||||
|
// Simulate a slow operation
|
||||||
|
setTimeout(() => {
|
||||||
|
result.textContent = 'Button click processed!';
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
document.body.appendChild(button);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue