corrad-af-2024/components/api-platform/TestScriptsModal.vue

536 lines
19 KiB
Vue

<template>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-5xl mx-4 max-h-[90vh] flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<Icon name="ic:outline-quiz" size="24" class="text-primary-600" />
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Test Scripts</h3>
</div>
<rs-button
variant="text"
size="sm"
@click="$emit('close')"
class="p-2"
>
<Icon name="ic:outline-close" size="16" />
</rs-button>
</div>
<!-- Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Test Templates Sidebar -->
<div class="w-80 border-r border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900 overflow-y-auto">
<h4 class="font-medium text-gray-900 dark:text-white mb-4">Test Templates</h4>
<div class="space-y-2">
<button
v-for="template in testTemplates"
:key="template.id"
@click="insertTemplate(template)"
class="w-full text-left p-3 rounded-lg transition-colors border border-gray-200 dark:border-gray-700 hover:bg-white dark:hover:bg-gray-800"
>
<div class="font-medium text-gray-900 dark:text-white text-sm">{{ template.name }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ template.description }}</div>
</button>
</div>
<!-- Quick Snippets -->
<h5 class="font-medium text-gray-900 dark:text-white mt-6 mb-3">Quick Snippets</h5>
<div class="space-y-1">
<button
v-for="snippet in quickSnippets"
:key="snippet.id"
@click="insertSnippet(snippet.code)"
class="w-full text-left p-2 rounded text-xs bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
{{ snippet.name }}
</button>
</div>
</div>
<!-- Script Editor -->
<div class="flex-1 flex flex-col">
<!-- Toolbar -->
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<div class="flex items-center gap-3">
<rs-button variant="primary" size="sm" @click="runTests" :loading="isRunning">
<Icon name="ic:outline-play-arrow" size="16" class="mr-1" />
Run Tests
</rs-button>
<rs-button variant="secondary-outline" size="sm" @click="clearEditor">
<Icon name="ic:outline-clear" size="16" class="mr-1" />
Clear
</rs-button>
<rs-button variant="secondary-outline" size="sm" @click="formatCode">
<Icon name="ic:outline-auto-fix-high" size="16" class="mr-1" />
Format
</rs-button>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
pm.* APIs available
</div>
</div>
<!-- Code Editor -->
<div class="flex-1 relative">
<textarea
ref="codeEditor"
v-model="testScript"
class="w-full h-full p-4 font-mono text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white border-0 resize-none focus:outline-none"
placeholder="// Write your test scripts here
// Example:
pm.test('Status code is 200', function () {
pm.response.to.have.status(200);
});
pm.test('Response has expected properties', function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property('data');
});
pm.test('Response time is less than 200ms', function () {
pm.expect(pm.response.responseTime).to.be.below(200);
});"
spellcheck="false"
></textarea>
</div>
</div>
<!-- Results Panel -->
<div class="w-96 border-l border-gray-200 dark:border-gray-700 flex flex-col bg-gray-50 dark:bg-gray-900">
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<h4 class="font-medium text-gray-900 dark:text-white flex items-center gap-2">
<Icon name="ic:outline-assessment" size="16" />
Test Results
</h4>
</div>
<div class="flex-1 overflow-y-auto p-4">
<div v-if="testResults.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<Icon name="ic:outline-quiz" size="48" class="mx-auto mb-4 opacity-50" />
<p class="text-sm">No test results yet</p>
<p class="text-xs mt-1">Click "Run Tests" to execute your scripts</p>
</div>
<div v-else class="space-y-3">
<!-- Summary -->
<div class="p-3 rounded-lg border" :class="[
allTestsPassed ? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800' : 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
]">
<div class="font-medium" :class="[
allTestsPassed ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200'
]">
{{ passedTests }}/{{ testResults.length }} tests passed
</div>
<div class="text-xs mt-1" :class="[
allTestsPassed ? 'text-green-600 dark:text-green-300' : 'text-red-600 dark:text-red-300'
]">
Execution time: {{ executionTime }}ms
</div>
</div>
<!-- Individual Results -->
<div class="space-y-2">
<div
v-for="(result, index) in testResults"
:key="index"
class="p-3 rounded-lg border"
:class="[
result.passed
? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800'
: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
]"
>
<div class="flex items-start gap-2">
<Icon
:name="result.passed ? 'ic:outline-check-circle' : 'ic:outline-error'"
size="16"
class="flex-shrink-0 mt-0.5"
:class="[
result.passed ? 'text-green-500' : 'text-red-500'
]"
/>
<div class="flex-1 min-w-0">
<div class="font-medium text-sm" :class="[
result.passed
? 'text-green-800 dark:text-green-200'
: 'text-red-800 dark:text-red-200'
]">
{{ result.name }}
</div>
<div v-if="result.error" class="text-xs mt-1 text-red-600 dark:text-red-300 font-mono">
{{ result.error }}
</div>
</div>
</div>
</div>
</div>
<!-- Console Output -->
<div v-if="consoleOutput.length > 0" class="mt-4">
<h5 class="font-medium text-gray-900 dark:text-white mb-2">Console Output</h5>
<div class="bg-gray-900 text-green-400 p-3 rounded-lg font-mono text-xs space-y-1 max-h-32 overflow-y-auto">
<div v-for="(output, index) in consoleOutput" :key="index">
{{ output }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['close'])
const { response, showNotification } = useApiPlatform()
// Test script state
const testScript = ref('')
const testResults = ref([])
const consoleOutput = ref([])
const isRunning = ref(false)
const executionTime = ref(0)
// Editor reference
const codeEditor = ref(null)
// Test templates
const testTemplates = [
{
id: 'status-check',
name: 'Status Code Check',
description: 'Check response status code',
code: `pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});`
},
{
id: 'json-schema',
name: 'JSON Schema Validation',
description: 'Validate response structure',
code: `pm.test("Response has expected structure", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property('data');
pm.expect(jsonData).to.have.property('status');
});`
},
{
id: 'response-time',
name: 'Response Time Check',
description: 'Check response performance',
code: `pm.test("Response time is less than 200ms", function () {
pm.expect(pm.response.responseTime).to.be.below(200);
});`
},
{
id: 'header-check',
name: 'Header Validation',
description: 'Check response headers',
code: `pm.test("Content-Type header is present", function () {
pm.response.to.have.header("Content-Type");
});
pm.test("Content-Type is application/json", function () {
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});`
},
{
id: 'data-validation',
name: 'Data Validation',
description: 'Validate response data',
code: `pm.test("Response contains required fields", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property('id').that.is.a('number');
pm.expect(jsonData).to.have.property('name').that.is.a('string');
pm.expect(jsonData).to.have.property('email').that.matches(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/);
});`
}
]
// Quick snippets
const quickSnippets = [
{ id: 'test-block', name: 'pm.test()', code: 'pm.test("Test name", function () {\n \n});' },
{ id: 'status-200', name: 'Status 200', code: 'pm.response.to.have.status(200);' },
{ id: 'json-data', name: 'Get JSON', code: 'const jsonData = pm.response.json();' },
{ id: 'response-time', name: 'Response Time', code: 'pm.expect(pm.response.responseTime).to.be.below(200);' },
{ id: 'header-check', name: 'Header Check', code: 'pm.response.to.have.header("Content-Type");' },
{ id: 'console-log', name: 'Console Log', code: 'console.log("Debug message");' }
]
// Computed properties
const allTestsPassed = computed(() => testResults.value.length > 0 && testResults.value.every(test => test.passed))
const passedTests = computed(() => testResults.value.filter(test => test.passed).length)
// Insert template
const insertTemplate = (template) => {
const currentValue = testScript.value
const newValue = currentValue ? currentValue + '\n\n' + template.code : template.code
testScript.value = newValue
nextTick(() => {
codeEditor.value?.focus()
})
}
// Insert snippet
const insertSnippet = (code) => {
const textarea = codeEditor.value
if (!textarea) return
const start = textarea.selectionStart
const end = textarea.selectionEnd
const currentValue = testScript.value
const newValue = currentValue.substring(0, start) + code + currentValue.substring(end)
testScript.value = newValue
nextTick(() => {
textarea.focus()
textarea.setSelectionRange(start + code.length, start + code.length)
})
}
// Clear editor
const clearEditor = () => {
testScript.value = ''
testResults.value = []
consoleOutput.value = []
}
// Format code (basic formatting)
const formatCode = () => {
try {
// Basic formatting - add proper indentation
const lines = testScript.value.split('\n')
let indentLevel = 0
const formatted = []
for (const line of lines) {
const trimmed = line.trim()
if (trimmed.includes('}')) indentLevel--
formatted.push(' '.repeat(Math.max(0, indentLevel)) + trimmed)
if (trimmed.includes('{')) indentLevel++
}
testScript.value = formatted.join('\n')
showNotification('Code formatted', 'success', 2000)
} catch (error) {
showNotification('Failed to format code', 'error')
}
}
// Run test scripts
const runTests = async () => {
if (!testScript.value.trim()) {
showNotification('Please write some test scripts first', 'warning')
return
}
if (!response.value || !response.value.status) {
showNotification('No response data available to test', 'warning')
return
}
isRunning.value = true
testResults.value = []
consoleOutput.value = []
const startTime = Date.now()
try {
// Create pm object with response data
const pm = createPmObject()
// Create console object to capture output
const console = {
log: (...args) => {
consoleOutput.value.push(args.join(' '))
},
error: (...args) => {
consoleOutput.value.push('ERROR: ' + args.join(' '))
},
warn: (...args) => {
consoleOutput.value.push('WARN: ' + args.join(' '))
}
}
// Execute the test script in a controlled environment
const testFunction = new Function('pm', 'console', 'expect', testScript.value)
testFunction(pm, console, pm.expect)
if (testResults.value.length === 0) {
showNotification('No tests were executed. Make sure to use pm.test() functions.', 'warning')
} else {
const passed = testResults.value.filter(test => test.passed).length
const total = testResults.value.length
showNotification(`Test execution complete: ${passed}/${total} tests passed`, passed === total ? 'success' : 'warning')
}
} catch (error) {
console.error('Test execution error:', error)
testResults.value.push({
name: 'Script Execution Error',
passed: false,
error: error.message
})
showNotification(`Test execution failed: ${error.message}`, 'error')
} finally {
executionTime.value = Date.now() - startTime
isRunning.value = false
}
}
// Create pm object similar to Postman
const createPmObject = () => {
const pm = {
response: {
code: response.value.status,
status: response.value.statusText || '',
headers: new Map(Object.entries(response.value.headers || {})),
responseTime: response.value.time || 0,
responseSize: response.value.size || 0,
text: () => {
if (typeof response.value.data === 'string') return response.value.data
return JSON.stringify(response.value.data)
},
json: () => {
if (typeof response.value.data === 'object') return response.value.data
if (typeof response.value.data === 'string') {
try {
return JSON.parse(response.value.data)
} catch {
throw new Error('Response is not valid JSON')
}
}
throw new Error('Response is not JSON')
},
to: {
have: {
status: (expectedStatus) => {
if (response.value.status !== expectedStatus) {
throw new Error(`Expected status ${expectedStatus}, but got ${response.value.status}`)
}
},
header: (headerName) => {
if (!response.value.headers || !response.value.headers[headerName]) {
throw new Error(`Expected header '${headerName}' not found`)
}
}
}
}
},
test: (name, testFunction) => {
try {
testFunction()
testResults.value.push({
name,
passed: true,
error: null
})
} catch (error) {
testResults.value.push({
name,
passed: false,
error: error.message
})
}
},
expect: (value) => {
return {
to: {
equal: (expected) => {
if (value !== expected) {
throw new Error(`Expected ${expected}, but got ${value}`)
}
},
be: {
below: (threshold) => {
if (value >= threshold) {
throw new Error(`Expected ${value} to be below ${threshold}`)
}
},
above: (threshold) => {
if (value <= threshold) {
throw new Error(`Expected ${value} to be above ${threshold}`)
}
},
a: (type) => {
if (typeof value !== type) {
throw new Error(`Expected ${value} to be of type ${type}, but got ${typeof value}`)
}
return {
that: {
matches: (regex) => {
if (!regex.test(value)) {
throw new Error(`Expected ${value} to match ${regex}`)
}
}
}
}
}
},
have: {
property: (prop) => {
if (!value || typeof value !== 'object' || !(prop in value)) {
throw new Error(`Expected object to have property '${prop}'`)
}
return {
that: {
is: {
a: (type) => {
if (typeof value[prop] !== type) {
throw new Error(`Expected property '${prop}' to be of type ${type}`)
}
return {
that: {
matches: (regex) => {
if (!regex.test(value[prop])) {
throw new Error(`Expected property '${prop}' with value '${value[prop]}' to match ${regex}`)
}
}
}
}
}
}
}
}
},
length: (expectedLength) => {
if (!value || value.length !== expectedLength) {
throw new Error(`Expected length ${expectedLength}, but got ${value?.length || 0}`)
}
}
},
include: (substring) => {
if (!value || !value.toString().includes(substring)) {
throw new Error(`Expected '${value}' to include '${substring}'`)
}
}
}
}
}
}
// Add header helpers
pm.response.headers.get = (name) => {
return response.value.headers?.[name] || response.value.headers?.[name.toLowerCase()]
}
return pm
}
// Load saved scripts on mount
onMounted(() => {
const saved = localStorage.getItem('api-platform-test-scripts')
if (saved) {
testScript.value = saved
}
})
// Save scripts when they change
watch(testScript, (newScript) => {
localStorage.setItem('api-platform-test-scripts', newScript)
}, { debounce: 1000 })
</script>