const TestRunner = { oninit(vnode) { this.function_id = vnode.attrs.function_id; this.function_name = vnode.attrs.function_name; this.tests = []; this.testResults = {}; this.loading = true; this.runningTests = false; this.showCreateModal = false; this.selectedTestForEdit = null; // New test form data this.newTest = { name: '', description: '', request_method: 'POST', request_headers: '{}', request_body: '{}', expected_status: 'SUCCESS', expected_output: '{}', assertions: [] }; // Load tests this.loadTests(); }, async loadTests() { this.loading = true; try { const response = await m.request({ method: 'GET', url: `/tests/http/tests/${this.function_id}` }); this.tests = response.tests || []; } catch (err) { Alert.show('Error loading tests: ' + err.message, 'error'); } finally { this.loading = false; m.redraw(); } }, async createTest() { try { const response = await m.request({ method: 'POST', url: `/tests/http/tests/${this.function_id}`, body: { name: this.newTest.name, description: this.newTest.description, request_method: this.newTest.request_method, request_headers: JSON.parse(this.newTest.request_headers), request_body: JSON.parse(this.newTest.request_body), expected_status: this.newTest.expected_status, expected_output: this.newTest.expected_output ? JSON.parse(this.newTest.expected_output) : null, assertions: this.newTest.assertions } }); Alert.show('Test created successfully', 'success'); this.showCreateModal = false; this.resetNewTestForm(); await this.loadTests(); } catch (err) { Alert.show('Error creating test: ' + err.message, 'error'); } }, async deleteTest(testId) { if (!confirm('Are you sure you want to delete this test?')) return; try { await m.request({ method: 'DELETE', url: `/tests/http/tests/test/${testId}` }); Alert.show('Test deleted', 'success'); await this.loadTests(); } catch (err) { Alert.show('Error deleting test: ' + err.message, 'error'); } }, async runSingleTest(testId) { try { this.runningTests = true; m.redraw(); const result = await m.request({ method: 'POST', url: `/tests/http/tests/run/${testId}` }); this.testResults[testId] = result; m.redraw(); } catch (err) { Alert.show('Error running test: ' + err.message, 'error'); } finally { this.runningTests = false; m.redraw(); } }, async runAllTests() { try { this.runningTests = true; this.testResults = {}; m.redraw(); const response = await m.request({ method: 'POST', url: `/tests/http/tests/run-all/${this.function_id}` }); // Map results by test_id response.results.forEach(result => { this.testResults[result.test_id] = result; }); const summary = response.summary; Alert.show(`Tests completed: ${summary.passed}/${summary.total} passed`, summary.passed === summary.total ? 'success' : 'error'); } catch (err) { Alert.show('Error running tests: ' + err.message, 'error'); } finally { this.runningTests = false; m.redraw(); } }, resetNewTestForm() { this.newTest = { name: '', description: '', request_method: 'POST', request_headers: '{}', request_body: '{}', expected_status: 'SUCCESS', expected_output: '{}', assertions: [] }; }, addAssertion() { this.newTest.assertions.push({ type: 'field_exists', path: '', description: '' }); }, removeAssertion(index) { this.newTest.assertions.splice(index, 1); }, view(vnode) { const passedCount = Object.values(this.testResults).filter(r => r.passed).length; const totalTests = this.tests.length; const hasResults = Object.keys(this.testResults).length > 0; return m('div.p-6', [ // Header m('div.flex.justify-between.items-center.mb-6', [ m('div', [ m('h2.text-2xl.font-bold.text-gray-900.dark:text-white', `Tests for ${this.function_name}`), hasResults && m('p.text-sm.text-gray-600.dark:text-gray-400.mt-1', `${passedCount}/${Object.keys(this.testResults).length} tests passed` ) ]), m('div.flex.gap-2', [ m('button.px-4.py-2.bg-blue-500.text-white.rounded-lg.hover:bg-blue-600.disabled:opacity-50', { onclick: () => this.showCreateModal = true }, '+ New Test'), m('button.px-4.py-2.bg-green-500.text-white.rounded-lg.hover:bg-green-600.disabled:opacity-50', { onclick: () => this.runAllTests(), disabled: this.runningTests || this.tests.length === 0 }, this.runningTests ? 'Running...' : '▶ Run All Tests') ]) ]), // Tests List this.loading ? m('div.text-center.py-12', m('div.animate-spin.h-8.w-8.border-4.border-blue-500.border-t-transparent.rounded-full.mx-auto')) : this.tests.length === 0 ? m('div.text-center.py-12', [ m('p.text-gray-600.dark:text-gray-400', 'No tests yet'), m('p.text-sm.text-gray-500.dark:text-gray-500.mt-2', 'Create your first test to ensure your function works as expected') ]) : m('div.space-y-3', this.tests.map(test => { const result = this.testResults[test.id]; const isPassed = result?.passed === true; const isFailed = result?.passed === false; return m('div.bg-white.dark:bg-gray-800.border.rounded-lg.p-4', { class: isPassed ? 'border-green-500' : isFailed ? 'border-red-500' : 'border-gray-200 dark:border-gray-700' }, [ m('div.flex.justify-between.items-start', [ m('div.flex-1', [ m('div.flex.items-center.gap-2', [ result && m('span.text-2xl', isPassed ? '✓' : '✗'), m('h3.text-lg.font-semibold.text-gray-900.dark:text-white', test.name), m('span.text-xs.px-2.py-1.rounded.bg-gray-100.dark:bg-gray-700', test.request_method) ]), test.description && m('p.text-sm.text-gray-600.dark:text-gray-400.mt-1', test.description), // Test Results result && m('div.mt-3.space-y-2', [ // Show assertion results if they exist result.assertions && result.assertions.length > 0 ? [ m('div.text-sm.font-medium.mb-2', `Assertions: ${result.assertions.filter(a => a.passed).length}/${result.assertions.length} passed`), result.assertions.map(assertion => m('div.text-xs.p-2.rounded.border', { class: assertion.passed ? 'bg-green-50.dark:bg-green-900/20.border-green-200.dark:border-green-800' : 'bg-red-50.dark:bg-red-900/20.border-red-200.dark:border-red-800' }, [ m('div.flex.items-center.gap-2', [ m('span', assertion.passed ? '✓' : '✗'), m('span.font-medium', assertion.description || `${assertion.type}: ${assertion.path}`) ]), m('p.text-gray-600.dark:text-gray-400.mt-1', assertion.message) ]) ) ] : [ // Fallback to old output comparison isFailed && m('div.bg-red-50.dark:bg-red-900/20.border.border-red-200.dark:border-red-800.rounded.p-3', [ m('p.text-sm.font-medium.text-red-800.dark:text-red-300', 'Test Failed'), result.error && m('p.text-xs.text-red-600.dark:text-red-400.mt-1', `Error: ${result.error}`), result.actual_output !== result.expected_output && m('div.mt-2', [ m('div.grid.grid-cols-2.gap-2.text-xs', [ m('div', [ m('p.font-medium.text-gray-700.dark:text-gray-300', 'Expected:'), m('pre.bg-white.dark:bg-gray-900.p-2.rounded.mt-1.overflow-auto', JSON.stringify(result.expected_output, null, 2)) ]), m('div', [ m('p.font-medium.text-gray-700.dark:text-gray-300', 'Actual:'), m('pre.bg-white.dark:bg-gray-900.p-2.rounded.mt-1.overflow-auto', JSON.stringify(result.actual_output, null, 2)) ]) ]) ]) ]), isPassed && m('div.bg-green-50.dark:bg-green-900/20.border.border-green-200.dark:border-green-800.rounded.p-3', [ m('p.text-sm.font-medium.text-green-800.dark:text-green-300', '✓ Test Passed') ]) ] ]) ]), m('div.flex.gap-2', [ m('button.px-3.py-1.text-sm.bg-blue-500.text-white.rounded.hover:bg-blue-600', { onclick: () => this.runSingleTest(test.id), disabled: this.runningTests }, '▶ Run'), m('button.px-3.py-1.text-sm.bg-red-500.text-white.rounded.hover:bg-red-600', { onclick: () => this.deleteTest(test.id) }, 'Delete') ]) ]) ]); })), // Create Test Modal this.showCreateModal && m('div.fixed.inset-0.bg-black.bg-opacity-50.flex.items-center.justify-center.z-50', { onclick: () => this.showCreateModal = false }, [ m('div.bg-white.dark:bg-gray-800.rounded-lg.p-6.max-w-2xl.w-full.overflow-y-auto', { onclick: (e) => e.stopPropagation(), style: 'max-height: 90vh;' }, [ m('h3.text-xl.font-bold.mb-4.text-gray-900.dark:text-white', 'Create New Test'), m('form', { onsubmit: (e) => { e.preventDefault(); this.createTest(); } }, [ m('div.space-y-4', [ m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Test Name'), m('input.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.name, oninput: (e) => this.newTest.name = e.target.value, required: true, placeholder: 'Valid user creation' }) ]), m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Description'), m('textarea.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.description, oninput: (e) => this.newTest.description = e.target.value, rows: 2 }) ]), m('div.grid.grid-cols-2.gap-4', [ m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Method'), m('select.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.request_method, onchange: (e) => this.newTest.request_method = e.target.value }, [ m('option', {value: 'GET'}, 'GET'), m('option', {value: 'POST'}, 'POST'), m('option', {value: 'PUT'}, 'PUT'), m('option', {value: 'DELETE'}, 'DELETE') ]) ]), m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Expected Status'), m('select.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.expected_status, onchange: (e) => this.newTest.expected_status = e.target.value }, [ m('option', {value: 'SUCCESS'}, 'SUCCESS'), m('option', {value: 'ERROR'}, 'ERROR') ]) ]) ]), m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Request Headers (JSON)'), m('textarea.w-full.p-2.border.rounded.font-mono.text-sm.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.request_headers, oninput: (e) => this.newTest.request_headers = e.target.value, rows: 3 }) ]), m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Request Body (JSON)'), m('textarea.w-full.p-2.border.rounded.font-mono.text-sm.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.request_body, oninput: (e) => this.newTest.request_body = e.target.value, rows: 4 }) ]), m('div', [ m('label.block.text-sm.font-medium.mb-1', 'Expected Output (JSON)'), m('textarea.w-full.p-2.border.rounded.font-mono.text-sm.dark:bg-gray-700.dark:border-gray-600', { value: this.newTest.expected_output, oninput: (e) => this.newTest.expected_output = e.target.value, rows: 4 }) ]), // Assertions Section m('div.border-t.pt-4.mt-4', [ m('div.flex.justify-between.items-center.mb-3', [ m('label.block.text-sm.font-medium', 'Assertions (Advanced)'), m('button.text-sm.px-3.py-1.bg-green-500.text-white.rounded.hover:bg-green-600', { type: 'button', onclick: () => this.addAssertion() }, '+ Add Assertion') ]), this.newTest.assertions.length === 0 ? m('p.text-sm.text-gray-500.dark:text-gray-400', 'No assertions yet. Add assertions to validate specific fields in the response.') : m('div.space-y-3', this.newTest.assertions.map((assertion, idx) => m('div.p-3.border.rounded.dark:border-gray-600.bg-gray-50.dark:bg-gray-900', [ m('div.flex.justify-between.items-start.mb-2', [ m('span.text-xs.font-medium.text-gray-700.dark:text-gray-300', `Assertion ${idx + 1}`), m('button.text-red-500.hover:text-red-700.text-xs', { type: 'button', onclick: () => this.removeAssertion(idx) }, '✕ Remove') ]), m('div.grid.grid-cols-2.gap-2', [ m('div', [ m('label.block.text-xs.mb-1', 'Type'), m('select.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: assertion.type, onchange: (e) => assertion.type = e.target.value }, [ m('option', {value: 'field_exists'}, 'Field Exists'), m('option', {value: 'field_equals'}, 'Field Equals'), m('option', {value: 'field_type'}, 'Field Type'), m('option', {value: 'field_contains'}, 'Field Contains') ]) ]), m('div', [ m('label.block.text-xs.mb-1', 'Path'), m('input.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: assertion.path, oninput: (e) => assertion.path = e.target.value, placeholder: 'e.g., user.email' }) ]) ]), // Conditional fields based on assertion type (assertion.type === 'field_equals' || assertion.type === 'field_type' || assertion.type === 'field_contains') && m('div.mt-2', [ m('label.block.text-xs.mb-1', assertion.type === 'field_equals' ? 'Expected Value' : assertion.type === 'field_type' ? 'Expected Type' : 'Substring'), m('input.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: assertion.type === 'field_equals' ? assertion.value : assertion.type === 'field_type' ? assertion.expectedType : assertion.substring, oninput: (e) => { if (assertion.type === 'field_equals') assertion.value = e.target.value; else if (assertion.type === 'field_type') assertion.expectedType = e.target.value; else assertion.substring = e.target.value; }, placeholder: assertion.type === 'field_type' ? 'str, int, bool, etc.' : '' }) ]), m('div.mt-2', [ m('label.block.text-xs.mb-1', 'Description (Optional)'), m('input.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', { value: assertion.description, oninput: (e) => assertion.description = e.target.value, placeholder: 'Describe this check' }) ]) ]) )) ]) ]), m('div.flex.justify-end.gap-2.mt-6', [ m('button.px-4.py-2.border.rounded.hover:bg-gray-100.dark:hover:bg-gray-700', { type: 'button', onclick: () => this.showCreateModal = false }, 'Cancel'), m('button.px-4.py-2.bg-blue-500.text-white.rounded.hover:bg-blue-600', { type: 'submit' }, 'Create Test') ]) ]) ]) ]) ]); } };