@monitoro/herd
Version:
Automate your browser, build AI web tools and MCP servers with Monitoro Herd
212 lines (211 loc) • 9.65 kB
JavaScript
import { loadTrail } from "./loadTrail.js";
export async function testTrailAction(herdClient, action, params, resources) {
const [device] = await herdClient.listDevices();
try {
return await action.test(device, params, resources);
}
catch (error) {
console.error(error);
return { status: "error", message: error.message, result: null };
}
}
function validateSelectorResult(result, selector) {
// check if the selector is valid
if (!selector) {
return { status: "error", message: "No selector provided" };
}
if (!result) {
return { status: "error", message: "No result found" };
}
if (typeof selector === "string") {
const isValid = typeof result === "string" || typeof result === "number" || typeof result === "boolean" || Array.isArray(result);
return { status: isValid ? "success" : "error", message: isValid ? undefined : "Invalid result" };
}
for (const key of Object.keys(selector).filter(key => key !== "_$r" && key !== "_$")) {
if (!result[key]) {
return { status: "error", message: `Missing key: ${key}` };
}
if (typeof selector[key] === "object" && selector[key]._$r) {
if (!Array.isArray(result[key])) {
return { status: "error", message: `Expected array for key: ${key}` };
}
if (!result[key].length) {
return { status: "error", message: `Expected more than 1 list items for key: ${key}` };
}
for (const item of result[key]) {
const validation = validateSelectorResult(item, selector[key]);
if (validation.status === "error") {
return validation;
}
}
}
if (typeof selector[key] === "object" && selector[key]._$) {
if (typeof result[key] !== "string") {
return { status: "error", message: `Expected primitive value (string, number, boolean) for key: ${key}` };
}
}
}
return { status: "success" };
}
export async function testTrailSelector(herdClient, example) {
const [device] = await herdClient.listDevices();
let page;
try {
page = await device.newPage();
await page.goto(example.url);
const result = await page.extract(example.selector);
const validation = validateSelectorResult(result, example.selector);
if (validation.status === "error") {
return { result, status: "error", message: validation.message };
}
return { status: "success", result };
}
catch (error) {
console.error(error);
return { status: "error", message: error.message, result: null };
}
finally {
if (page) {
await page.close();
}
}
}
export async function execute(herdClient, path, { actionName, selectorId }) {
// Load the trail action class and run it
// Will later need resolving
const { actions, resources } = await loadTrail(path);
// Helper function to test a single action
async function testAction(name) {
const action = actions[name];
console.log('\n' + '='.repeat(80));
console.log(`\x1b[1m🧪 RUNNING TEST: \x1b[36m${action.manifest.name}\x1b[0m\x1b[1m with ${action.manifest.examples.length} examples\x1b[0m`);
console.log('='.repeat(80) + '\n');
let result;
const startTime = Date.now();
for (const example of action.manifest.examples) {
try {
result = await testTrailAction(herdClient, action, example, resources);
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
if (result && result.status === "success") {
console.log(`\x1b[32m✅ TEST PASSED\x1b[0m (${duration}s)`);
console.log('\x1b[1mExample:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(example, null, 2) + '\x1b[0m');
console.log('\x1b[1mResults:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(result.result, null, 2) + '\x1b[0m');
}
else if (result && result.status === "warning") {
console.log(`\x1b[33m⚠️ TEST PASSED WITH WARNING\x1b[0m (${duration}s)`);
if (result?.message) {
console.log(`\x1b[1mMessage:\x1b[0m \x1b[31m${result.message}\x1b[0m`);
}
console.log('\x1b[1mExample:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(example, null, 2) + '\x1b[0m');
console.log('\x1b[1mResults:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(result.result, null, 2) + '\x1b[0m');
}
else {
console.log(`\x1b[31m❌ TEST FAILED\x1b[0m (${duration}s)`);
console.log(`\x1b[1mStatus:\x1b[0m \x1b[33m${result?.status || 'unknown'}\x1b[0m`);
console.log('\x1b[1mExample:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(example, null, 2) + '\x1b[0m');
if (result?.message) {
console.log(`\x1b[1mMessage:\x1b[0m \x1b[31m${result.message}\x1b[0m`);
}
if (result?.result) {
console.log('\x1b[1mPartial Results:\x1b[0m');
console.log('\x1b[33m' + JSON.stringify(result.result, null, 2) + '\x1b[0m');
}
}
}
catch (error) {
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`\x1b[31m❌ TEST ERROR\x1b[0m (${duration}s)`);
console.log('\x1b[1mExample:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(example, null, 2) + '\x1b[0m');
console.log('\x1b[1mError Details:\x1b[0m');
console.log(`\x1b[31m${error.stack || error}\x1b[0m`);
}
}
console.log('\n' + '='.repeat(80) + '\n');
return result;
}
// Helper function to test a single selector
async function testSelector(id) {
const selector = resources.selector(id);
const examples = resources.urlExamplesForSelector(id)
.map(url => ({ url, selector }));
console.log('\n' + '='.repeat(80));
console.log(`\x1b[1m🧪 RUNNING SELECTOR TEST: \x1b[36m${id}\x1b[0m\x1b[1m with ${examples.length} examples\x1b[0m`);
console.log('='.repeat(80) + '\n');
let success = true;
for (const example of examples) {
const result = await testTrailSelector(herdClient, example);
if (result && result.status === "success") {
console.log(`\x1b[32m✅ TEST PASSED\x1b[0m`);
}
else if (result && result.status === "warning") {
console.log(`\x1b[33m⚠️ TEST PASSED WITH WARNING\x1b[0m`);
}
else {
console.log(`\x1b[31m❌ TEST FAILED\x1b[0m`);
success = false;
}
if (result?.message) {
console.log(`\x1b[1mMessage:\x1b[0m \x1b[31m${result.message}\x1b[0m`);
}
console.log('\x1b[1mExample:\x1b[0m');
console.log('\x1b[36m' + JSON.stringify(example, null, 2) + '\x1b[0m');
console.log('\x1b[1mResults:\x1b[0m');
console.log('\x1b[33m' + JSON.stringify(result.result, null, 2) + '\x1b[0m');
}
return { status: success ? "success" : "error" };
}
// Test a specific action if provided
if (actionName) {
return await testAction(actionName);
}
// Test a specific selector if provided
else if (selectorId) {
return await testSelector(selectorId);
}
// Test all selectors and actions if neither is provided
else {
console.log('\n' + '='.repeat(80));
console.log(`\x1b[1m🧪 RUNNING ALL TESTS\x1b[0m`);
console.log('='.repeat(80) + '\n');
// First test all selectors
console.log('\n' + '='.repeat(80));
console.log(`\x1b[1m🧪 TESTING ALL SELECTORS\x1b[0m`);
console.log('='.repeat(80) + '\n');
const selectorIds = resources.selectors.map(selector => selector.id);
let allSelectorsSuccess = true;
for (const id of selectorIds) {
const result = await testSelector(id);
if (result && result.status !== "success") {
allSelectorsSuccess = false;
}
}
// Then test all actions
console.log('\n' + '='.repeat(80));
console.log(`\x1b[1m🧪 TESTING ALL ACTIONS\x1b[0m`);
console.log('='.repeat(80) + '\n');
const actionNames = Object.keys(actions);
let allActionsSuccess = true;
for (const name of actionNames) {
const result = await testAction(name);
if (result && result.status !== "success") {
allActionsSuccess = false;
}
}
console.log('\n' + '='.repeat(80));
console.log(`\x1b[1m🧪 ALL TESTS COMPLETED\x1b[0m`);
if (allSelectorsSuccess && allActionsSuccess) {
console.log(`\x1b[32m✅ ALL TESTS PASSED\x1b[0m`);
}
else {
console.log(`\x1b[31m❌ SOME TESTS FAILED\x1b[0m`);
}
console.log('='.repeat(80) + '\n');
return { status: (allSelectorsSuccess && allActionsSuccess) ? "success" : "error" };
}
}