586 lines
22 KiB
Vue
586 lines
22 KiB
Vue
<template>
|
|
<div class="bg-white dark:bg-gray-800 flex flex-col">
|
|
<!-- Enhanced Response Header -->
|
|
<div class="flex items-center justify-between p-6 flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center gap-3">
|
|
<Icon name="ic:outline-api" size="20" class="text-gray-500" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Response</h3>
|
|
<div v-if="hasResponse" class="text-sm text-gray-500 dark:text-gray-400">
|
|
• {{ new Date(response.timestamp || Date.now()).toLocaleTimeString() }}
|
|
</div>
|
|
</div>
|
|
<div v-if="hasResponse" class="flex items-center gap-3 flex-wrap">
|
|
<rs-badge :variant="getStatusVariant(response.status || 500)" class="font-medium">
|
|
{{ response.status || 'Error' }} {{ response.statusText || 'Request Failed' }}
|
|
</rs-badge>
|
|
<div class="flex items-center gap-4 text-sm text-gray-500">
|
|
<span v-if="response.time" class="flex items-center gap-1">
|
|
<Icon name="ic:outline-timer" size="14" />
|
|
{{ response.time }}ms
|
|
</span>
|
|
<span v-if="response.size" class="flex items-center gap-1">
|
|
<Icon name="ic:outline-data-usage" size="14" />
|
|
{{ formatBytes(response.size) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Response Content Area -->
|
|
<div v-if="hasResponse" class="p-6">
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800">
|
|
<!-- Enhanced Response Tabs -->
|
|
<div class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
|
<nav class="px-4 flex space-x-6 overflow-x-auto">
|
|
<button
|
|
v-for="tab in responseTabs"
|
|
:key="tab.id"
|
|
@click="responseActiveTab = tab.id"
|
|
:class="[
|
|
responseActiveTab === tab.id
|
|
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300',
|
|
'whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm flex items-center gap-2 flex-shrink-0'
|
|
]"
|
|
>
|
|
<Icon :name="tab.icon" size="16" />
|
|
{{ tab.label }}
|
|
<rs-badge v-if="tab.count" variant="secondary" size="sm">{{ tab.count }}</rs-badge>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Response Content -->
|
|
<div>
|
|
<!-- Body Tab -->
|
|
<div v-if="responseActiveTab === 'body'">
|
|
<!-- Enhanced Response Tools -->
|
|
<div v-if="isJsonResponse || response.data" class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<rs-button
|
|
v-if="isJsonResponse"
|
|
variant="secondary-outline"
|
|
size="sm"
|
|
@click="beautifyResponseJson"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<Icon name="ic:outline-auto-fix-high" size="16" />
|
|
Beautify
|
|
</rs-button>
|
|
<rs-button
|
|
v-if="isJsonResponse"
|
|
variant="secondary-outline"
|
|
size="sm"
|
|
@click="minifyResponseJson"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<Icon name="ic:outline-compress" size="16" />
|
|
Minify
|
|
</rs-button>
|
|
<rs-button
|
|
variant="secondary-outline"
|
|
size="sm"
|
|
@click="copyResponseToClipboard"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<Icon name="ic:outline-content-copy" size="16" />
|
|
Copy
|
|
</rs-button>
|
|
<rs-button
|
|
variant="secondary-outline"
|
|
size="sm"
|
|
@click="downloadResponse"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<Icon name="ic:outline-download" size="16" />
|
|
Download
|
|
</rs-button>
|
|
</div>
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-gray-500">Format:</span>
|
|
<rs-badge variant="info" size="sm">{{ responseFormat }}</rs-badge>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-gray-500">Size:</span>
|
|
<rs-badge variant="secondary" size="sm">{{ formattedResponseData.length }} chars</rs-badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Response Body Content -->
|
|
<div class="p-6">
|
|
<pre
|
|
ref="responseBody"
|
|
class="text-sm bg-gray-50 dark:bg-gray-900 p-4 rounded-lg whitespace-pre-wrap w-full break-words border font-mono leading-relaxed"
|
|
:class="{
|
|
'language-json': isJsonResponse,
|
|
'language-xml': isXmlResponse,
|
|
'language-html': isHtmlResponse
|
|
}"
|
|
>{{ formattedResponseData }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Headers Tab -->
|
|
<div v-if="responseActiveTab === 'headers'" class="p-6 space-y-4">
|
|
<!-- Copy Headers Button -->
|
|
<div class="flex justify-between items-center">
|
|
<h4 class="font-medium text-gray-900 dark:text-white">Response Headers</h4>
|
|
<rs-button variant="secondary-outline" size="sm" @click="copyHeadersToClipboard">
|
|
<Icon name="ic:outline-content-copy" size="16" class="mr-1" />
|
|
Copy All
|
|
</rs-button>
|
|
</div>
|
|
|
|
<!-- Headers List -->
|
|
<div class="space-y-2">
|
|
<div v-for="(value, key) in (response && response.headers ? response.headers : {})" :key="key" class="flex flex-wrap gap-2 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border">
|
|
<span class="font-medium text-gray-700 dark:text-gray-300 min-w-48 break-words">{{ key }}:</span>
|
|
<span class="text-gray-600 dark:text-gray-400 break-words flex-1 font-mono text-sm">{{ value }}</span>
|
|
<rs-button
|
|
variant="text"
|
|
size="sm"
|
|
@click="copyHeaderToClipboard(key, value)"
|
|
class="opacity-60 hover:opacity-100 flex-shrink-0"
|
|
>
|
|
<Icon name="ic:outline-content-copy" size="14" />
|
|
</rs-button>
|
|
</div>
|
|
<div v-if="Object.keys(response?.headers || {}).length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<Icon name="ic:outline-web-asset-off" size="48" class="mx-auto mb-4 opacity-50" />
|
|
<p>No response headers available</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cookies Tab -->
|
|
<div v-if="responseActiveTab === 'cookies'" class="p-6 space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h4 class="font-medium text-gray-900 dark:text-white">Cookies</h4>
|
|
<rs-button variant="secondary-outline" size="sm" @click="clearCookies">
|
|
<Icon name="ic:outline-clear" size="16" class="mr-1" />
|
|
Clear All
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div v-for="cookie in parsedCookies" :key="cookie.name" class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ cookie.name }}</span>
|
|
<rs-button variant="text" size="sm" @click="copyCookie(cookie)">
|
|
<Icon name="ic:outline-content-copy" size="14" />
|
|
</rs-button>
|
|
</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
|
<div><span class="font-medium">Value:</span> {{ cookie.value }}</div>
|
|
<div v-if="cookie.domain"><span class="font-medium">Domain:</span> {{ cookie.domain }}</div>
|
|
<div v-if="cookie.path"><span class="font-medium">Path:</span> {{ cookie.path }}</div>
|
|
<div v-if="cookie.expires"><span class="font-medium">Expires:</span> {{ cookie.expires }}</div>
|
|
<div v-if="cookie.httpOnly" class="flex items-center gap-1">
|
|
<Icon name="ic:outline-security" size="14" />
|
|
<span>HTTP Only</span>
|
|
</div>
|
|
<div v-if="cookie.secure" class="flex items-center gap-1">
|
|
<Icon name="ic:outline-lock" size="14" />
|
|
<span>Secure</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="parsedCookies.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<Icon name="ic:outline-cookie" size="48" class="mx-auto mb-4 opacity-50" />
|
|
<p>No cookies in response</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Timeline Tab -->
|
|
<div v-if="responseActiveTab === 'timeline'" class="p-6 space-y-4">
|
|
<h4 class="font-medium text-gray-900 dark:text-white">Request Timeline</h4>
|
|
|
|
<div class="space-y-3">
|
|
<div v-for="(stage, index) in timelineStages" :key="stage.name" class="flex items-center gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-3 h-3 rounded-full" :class="stage.color"></div>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="font-medium text-gray-900 dark:text-white">{{ stage.name }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">{{ stage.description }}</div>
|
|
</div>
|
|
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ stage.time }}ms
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total Time -->
|
|
<div class="mt-6 p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg border border-primary-200 dark:border-primary-800">
|
|
<div class="flex items-center justify-between">
|
|
<span class="font-medium text-primary-900 dark:text-primary-100">Total Request Time</span>
|
|
<span class="text-xl font-bold text-primary-900 dark:text-primary-100">{{ response.time || 0 }}ms</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Results Tab -->
|
|
<div v-if="responseActiveTab === 'tests'" class="p-6 space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h4 class="font-medium text-gray-900 dark:text-white">Test Results</h4>
|
|
<rs-button variant="primary" size="sm" @click="runBasicTests">
|
|
<Icon name="ic:outline-play-arrow" size="16" class="mr-1" />
|
|
Run Tests
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div v-for="test in testResults" :key="test.name" class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<Icon
|
|
:name="test.passed ? 'ic:outline-check-circle' : 'ic:outline-error'"
|
|
size="20"
|
|
:class="test.passed ? 'text-green-500' : 'text-red-500'"
|
|
/>
|
|
<div class="flex-1">
|
|
<div class="font-medium text-gray-900 dark:text-white">{{ test.name }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">{{ test.description }}</div>
|
|
</div>
|
|
<rs-badge :variant="test.passed ? 'success' : 'danger'" size="sm">
|
|
{{ test.passed ? 'PASS' : 'FAIL' }}
|
|
</rs-badge>
|
|
</div>
|
|
<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>No test results yet</p>
|
|
<p class="text-xs mt-2">Click "Run Tests" to execute basic validation tests</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="flex-1 flex items-center justify-center text-gray-500 dark:text-gray-400 p-6">
|
|
<div class="text-center">
|
|
<Icon name="ic:outline-send" size="64" class="mx-auto mb-6 opacity-50" />
|
|
<h3 class="text-lg font-medium mb-2">Ready to send your first request?</h3>
|
|
<p class="text-sm">Configure your request above and click Send to see the response here</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { formatJson, getStatusVariant } = useApiRequest()
|
|
const { response, responseActiveTab, showNotification } = useApiPlatform()
|
|
|
|
// Enhanced tab configuration
|
|
const responseTabs = computed(() => [
|
|
{
|
|
id: 'body',
|
|
label: 'Body',
|
|
icon: 'ic:outline-description',
|
|
count: null
|
|
},
|
|
{
|
|
id: 'headers',
|
|
label: 'Headers',
|
|
icon: 'ic:outline-list',
|
|
count: response.value?.headers ? Object.keys(response.value.headers).length : 0
|
|
},
|
|
{
|
|
id: 'cookies',
|
|
label: 'Cookies',
|
|
icon: 'ic:outline-cookie',
|
|
count: parsedCookies.value.length
|
|
},
|
|
{
|
|
id: 'timeline',
|
|
label: 'Timeline',
|
|
icon: 'ic:outline-timeline',
|
|
count: null
|
|
},
|
|
{
|
|
id: 'tests',
|
|
label: 'Tests',
|
|
icon: 'ic:outline-quiz',
|
|
count: testResults.value.length
|
|
}
|
|
])
|
|
|
|
// Check if we have a response to show
|
|
const hasResponse = computed(() => {
|
|
return response.value && (response.value.status || response.value.data)
|
|
})
|
|
|
|
// Response format detection
|
|
const isJsonResponse = computed(() => {
|
|
if (!response.value?.data) return false
|
|
return typeof response.value.data === 'object' ||
|
|
(typeof response.value.data === 'string' && isValidJson(response.value.data))
|
|
})
|
|
|
|
const isXmlResponse = computed(() => {
|
|
if (!response.value?.data || typeof response.value.data !== 'string') return false
|
|
return response.value.data.trim().startsWith('<?xml') || response.value.data.trim().startsWith('<')
|
|
})
|
|
|
|
const isHtmlResponse = computed(() => {
|
|
if (!response.value?.data || typeof response.value.data !== 'string') return false
|
|
return response.value.data.trim().toLowerCase().includes('<html')
|
|
})
|
|
|
|
const responseFormat = computed(() => {
|
|
if (isJsonResponse.value) return 'JSON'
|
|
if (isXmlResponse.value) return 'XML'
|
|
if (isHtmlResponse.value) return 'HTML'
|
|
return 'TEXT'
|
|
})
|
|
|
|
// Cookie parsing
|
|
const parsedCookies = computed(() => {
|
|
if (!response.value?.headers) return []
|
|
|
|
const cookieHeaders = []
|
|
Object.entries(response.value.headers).forEach(([key, value]) => {
|
|
if (key.toLowerCase() === 'set-cookie') {
|
|
if (Array.isArray(value)) {
|
|
cookieHeaders.push(...value)
|
|
} else {
|
|
cookieHeaders.push(value)
|
|
}
|
|
}
|
|
})
|
|
|
|
return cookieHeaders.map(cookieString => {
|
|
const parts = cookieString.split(';').map(part => part.trim())
|
|
const [name, value] = parts[0].split('=')
|
|
|
|
const cookie = { name, value: value || '' }
|
|
|
|
parts.slice(1).forEach(part => {
|
|
const [key, val] = part.split('=')
|
|
const lowerKey = key.toLowerCase()
|
|
|
|
if (lowerKey === 'domain') cookie.domain = val
|
|
else if (lowerKey === 'path') cookie.path = val
|
|
else if (lowerKey === 'expires') cookie.expires = val
|
|
else if (lowerKey === 'httponly') cookie.httpOnly = true
|
|
else if (lowerKey === 'secure') cookie.secure = true
|
|
})
|
|
|
|
return cookie
|
|
})
|
|
})
|
|
|
|
// Timeline stages
|
|
const timelineStages = computed(() => {
|
|
const total = response.value?.time || 0
|
|
const stages = [
|
|
{ name: 'DNS Lookup', description: 'Resolving domain name', time: Math.round(total * 0.1), color: 'bg-blue-500' },
|
|
{ name: 'TCP Connect', description: 'Establishing connection', time: Math.round(total * 0.15), color: 'bg-green-500' },
|
|
{ name: 'TLS Handshake', description: 'SSL/TLS negotiation', time: Math.round(total * 0.2), color: 'bg-yellow-500' },
|
|
{ name: 'Request Sent', description: 'Sending request data', time: Math.round(total * 0.05), color: 'bg-purple-500' },
|
|
{ name: 'Server Processing', description: 'Server processing time', time: Math.round(total * 0.4), color: 'bg-red-500' },
|
|
{ name: 'Response Received', description: 'Downloading response', time: Math.round(total * 0.1), color: 'bg-indigo-500' }
|
|
]
|
|
|
|
return stages
|
|
})
|
|
|
|
// Test results
|
|
const testResults = ref([])
|
|
|
|
// Response body formatting
|
|
const formattedResponseData = ref('')
|
|
|
|
const formatResponseData = () => {
|
|
if (!response.value?.data) {
|
|
formattedResponseData.value = 'No response data'
|
|
return
|
|
}
|
|
|
|
if (typeof response.value.data === 'object') {
|
|
formattedResponseData.value = JSON.stringify(response.value.data, null, 2)
|
|
} else if (typeof response.value.data === 'string' && isValidJson(response.value.data)) {
|
|
try {
|
|
const parsed = JSON.parse(response.value.data)
|
|
formattedResponseData.value = JSON.stringify(parsed, null, 2)
|
|
} catch (e) {
|
|
formattedResponseData.value = response.value.data
|
|
}
|
|
} else {
|
|
formattedResponseData.value = String(response.value.data)
|
|
}
|
|
}
|
|
|
|
// Utility functions
|
|
const isValidJson = (str) => {
|
|
try {
|
|
JSON.parse(str)
|
|
return true
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const formatBytes = (bytes) => {
|
|
if (bytes === 0) return '0 B'
|
|
const k = 1024
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
}
|
|
|
|
// Response JSON tools
|
|
const beautifyResponseJson = () => {
|
|
if (!isJsonResponse.value) return
|
|
|
|
try {
|
|
let data = response.value.data
|
|
if (typeof data === 'string') {
|
|
data = JSON.parse(data)
|
|
}
|
|
formattedResponseData.value = JSON.stringify(data, null, 2)
|
|
showNotification('JSON beautified', 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to beautify JSON', 'error')
|
|
}
|
|
}
|
|
|
|
const minifyResponseJson = () => {
|
|
if (!isJsonResponse.value) return
|
|
|
|
try {
|
|
let data = response.value.data
|
|
if (typeof data === 'string') {
|
|
data = JSON.parse(data)
|
|
}
|
|
formattedResponseData.value = JSON.stringify(data)
|
|
showNotification('JSON minified', 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to minify JSON', 'error')
|
|
}
|
|
}
|
|
|
|
const copyResponseToClipboard = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(formattedResponseData.value)
|
|
showNotification('Response copied to clipboard', 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to copy response', 'error')
|
|
}
|
|
}
|
|
|
|
const downloadResponse = () => {
|
|
try {
|
|
const blob = new Blob([formattedResponseData.value], {
|
|
type: isJsonResponse.value ? 'application/json' : 'text/plain'
|
|
})
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
const extension = isJsonResponse.value ? 'json' : 'txt'
|
|
a.href = url
|
|
a.download = `response-${Date.now()}.${extension}`
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
URL.revokeObjectURL(url)
|
|
showNotification('Response downloaded', 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to download response', 'error')
|
|
}
|
|
}
|
|
|
|
// Headers tools
|
|
const copyHeadersToClipboard = async () => {
|
|
try {
|
|
if (!response.value || !response.value.headers) {
|
|
showNotification('No headers to copy', 'warning')
|
|
return
|
|
}
|
|
const headersText = Object.entries(response.value.headers)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join('\n')
|
|
await navigator.clipboard.writeText(headersText)
|
|
showNotification('Headers copied to clipboard', 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to copy headers', 'error')
|
|
}
|
|
}
|
|
|
|
const copyHeaderToClipboard = async (key, value) => {
|
|
try {
|
|
await navigator.clipboard.writeText(`${key}: ${value}`)
|
|
showNotification(`Header "${key}" copied`, 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to copy header', 'error')
|
|
}
|
|
}
|
|
|
|
// Cookie tools
|
|
const clearCookies = () => {
|
|
showNotification('Cookies cleared from display', 'info', 2000)
|
|
}
|
|
|
|
const copyCookie = async (cookie) => {
|
|
try {
|
|
await navigator.clipboard.writeText(`${cookie.name}=${cookie.value}`)
|
|
showNotification(`Cookie "${cookie.name}" copied`, 'success', 2000)
|
|
} catch (error) {
|
|
showNotification('Failed to copy cookie', 'error')
|
|
}
|
|
}
|
|
|
|
// Test functions
|
|
const runBasicTests = () => {
|
|
testResults.value = []
|
|
|
|
if (!response.value) return
|
|
|
|
// Status code tests
|
|
if (response.value.status) {
|
|
testResults.value.push({
|
|
name: 'Status Code is 2xx',
|
|
description: 'Response has successful status code',
|
|
passed: response.value.status >= 200 && response.value.status < 300
|
|
})
|
|
|
|
testResults.value.push({
|
|
name: 'Response Time < 1000ms',
|
|
description: 'Response was received quickly',
|
|
passed: (response.value.time || 0) < 1000
|
|
})
|
|
}
|
|
|
|
// Content tests
|
|
if (response.value.data) {
|
|
testResults.value.push({
|
|
name: 'Response Has Body',
|
|
description: 'Response contains data',
|
|
passed: !!response.value.data
|
|
})
|
|
|
|
if (isJsonResponse.value) {
|
|
testResults.value.push({
|
|
name: 'Valid JSON Response',
|
|
description: 'Response body is valid JSON',
|
|
passed: true
|
|
})
|
|
}
|
|
}
|
|
|
|
// Headers tests
|
|
if (response.value.headers) {
|
|
testResults.value.push({
|
|
name: 'Content-Type Header Present',
|
|
description: 'Response includes content type',
|
|
passed: !!response.value.headers['content-type'] || !!response.value.headers['Content-Type']
|
|
})
|
|
}
|
|
|
|
showNotification(`${testResults.value.length} tests completed`, 'info', 2000)
|
|
}
|
|
|
|
// Watch for response changes
|
|
watch(() => response.value, formatResponseData, { immediate: true, deep: true })
|
|
</script> |