WIP: Add tests for http functions
This commit is contained in:
430
static/js/mithril/TestRunner.js
Normal file
430
static/js/mithril/TestRunner.js
Normal file
@@ -0,0 +1,430 @@
|
||||
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')
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user