UNPKG

@monitoro/herd

Version:

Automate your browser, build AI web tools and MCP servers with Monitoro Herd

212 lines (211 loc) 9.65 kB
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" }; } }