536 lines
19 KiB
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> |