feat: ui
This commit is contained in:
+298
-120
@@ -199,6 +199,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.picker-card--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.picker-card .q-card__section--vert {
|
||||
height: 100%;
|
||||
padding: 16px 18px;
|
||||
@@ -590,6 +594,20 @@
|
||||
background: rgba(25, 54, 61, 0.03);
|
||||
}
|
||||
|
||||
.test-mock-item-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.test-mock-item-json {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.test-mock-item-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--studio-muted);
|
||||
font-size: 0.74rem;
|
||||
@@ -888,18 +906,78 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.function-tabs {
|
||||
margin-bottom: 14px;
|
||||
border-radius: 18px;
|
||||
background: rgba(22, 32, 40, 0.04);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.function-tabs .q-tab {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
border-radius: 12px;
|
||||
color: var(--q-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.function-tabs .q-tab__content {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.function-tabs .q-tab--active {
|
||||
background: var(--q-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.function-tabs .q-tab--active,
|
||||
.function-tabs .q-tab--active .q-tab__label,
|
||||
.function-tabs .q-tab--active .text-weight-bold,
|
||||
.function-tabs .q-tab--active .text-caption {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.function-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.function-content-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.test-type-layout {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.test-type-tabs {
|
||||
margin-top: 8px;
|
||||
border-radius: 18px;
|
||||
background: rgba(22, 32, 40, 0.04);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.test-type-tabs .q-tab {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
min-height: 48px;
|
||||
border-radius: 12px;
|
||||
color: var(--q-primary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.test-type-tabs .q-tab__content {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.test-type-tabs .q-tab--active {
|
||||
@@ -914,9 +992,9 @@
|
||||
}
|
||||
|
||||
.test-type-panels {
|
||||
margin-top: 14px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.test-type-panel {
|
||||
@@ -1110,8 +1188,13 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.test-type-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mock-source-grid,
|
||||
.test-detail-grid,
|
||||
.test-mock-item-grid,
|
||||
.settings-row-grid,
|
||||
.settings-table-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1226,7 +1309,7 @@
|
||||
|
||||
<main class="workspace-grid">
|
||||
<section class="primary-row">
|
||||
<q-card flat class="studio-card picker-card">
|
||||
<q-card flat class="studio-card picker-card picker-card--full">
|
||||
<q-card-section>
|
||||
<div class="section-title">
|
||||
<div>
|
||||
@@ -1280,39 +1363,34 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card flat class="studio-card picker-card" v-if="currentDataset.analysis">
|
||||
</section>
|
||||
|
||||
<q-card flat class="studio-card" v-if="currentFundingSource && currentDataset.analysis">
|
||||
<q-card-section>
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>Function</h2>
|
||||
<div class="section-caption">
|
||||
Choose the wallet interface method to inspect for the selected funding source.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="native-select-label" for="function-select">
|
||||
Function
|
||||
</label>
|
||||
<div class="picker-action-row">
|
||||
<q-select
|
||||
id="function-select"
|
||||
<q-tabs
|
||||
v-if="functionOptions.length"
|
||||
v-model="selectedFunctionByDataset[selectedDatasetKey]"
|
||||
:options="functionOptions"
|
||||
emit-value
|
||||
map-options
|
||||
outlined
|
||||
dense
|
||||
options-dense
|
||||
behavior="menu"
|
||||
hide-dropdown-icon
|
||||
class="fixture-select picker-select"
|
||||
active-color="white"
|
||||
indicator-color="transparent"
|
||||
align="justify"
|
||||
class="function-tabs text-primary"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<span class="fixture-select-icon" aria-hidden="true">expand_more</span>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-tab
|
||||
v-for="fn in currentDataset.analysis.functions"
|
||||
:key="fn.name"
|
||||
:name="fn.name"
|
||||
>
|
||||
<div class="text-left">
|
||||
<div class="text-weight-bold">{{ fn.name }}</div>
|
||||
<div class="text-caption">{{ fn.tests.length }} tests</div>
|
||||
</div>
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
|
||||
<div v-if="functionOptions.length">
|
||||
<div class="function-content">
|
||||
<div class="function-content-header" v-if="currentFunction">
|
||||
<q-btn
|
||||
color="primary"
|
||||
outline
|
||||
@@ -1321,29 +1399,14 @@
|
||||
@click="functionMocksDialog = true"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
</section>
|
||||
|
||||
<q-card flat class="studio-card" v-if="currentFunction && currentFundingSource">
|
||||
<q-card-section>
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>Tests For Selected Function</h2>
|
||||
<div class="section-caption">
|
||||
Browse the raw test types for this function, then inspect the call params, expected result, and selected funding source mocks.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentFunctionTestTypeGroups.length" class="test-type-layout">
|
||||
<q-tabs
|
||||
v-if="currentFunctionTestTypeGroups.length"
|
||||
v-model="currentSelectedTestTypeKey"
|
||||
vertical
|
||||
dense
|
||||
active-color="white"
|
||||
indicator-color="transparent"
|
||||
align="justify"
|
||||
class="test-type-tabs text-primary"
|
||||
>
|
||||
<q-tab
|
||||
@@ -1518,9 +1581,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-mock-item-list" v-if="binding.itemTexts.length">
|
||||
<div class="test-mock-item-list" v-if="binding.items.length">
|
||||
<div
|
||||
v-for="(item, itemIndex) in binding.itemTexts"
|
||||
v-for="(item, itemIndex) in binding.items"
|
||||
:key="`${mockSet.path}-binding-${bindingIndex}-item-${itemIndex}`"
|
||||
class="test-mock-item-card"
|
||||
>
|
||||
@@ -1547,13 +1610,100 @@
|
||||
></q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="test-mock-item-grid">
|
||||
<div>
|
||||
<label
|
||||
class="field-label"
|
||||
:for="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
:id="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
v-model="item.descriptionText"
|
||||
class="native-input"
|
||||
type="text"
|
||||
placeholder="Optional label"
|
||||
spellcheck="false"
|
||||
@blur="persistTestMockSet(testEntry, mockSet)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="field-label"
|
||||
:for="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
>
|
||||
Request Type
|
||||
</label>
|
||||
<input
|
||||
:id="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
v-model="item.requestType"
|
||||
class="native-input"
|
||||
type="text"
|
||||
placeholder="json"
|
||||
spellcheck="false"
|
||||
@blur="persistTestMockSet(testEntry, mockSet)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="field-label"
|
||||
:for="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
>
|
||||
Request Body
|
||||
</label>
|
||||
<textarea
|
||||
:id="`test-mock-definition-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
v-model="item.text"
|
||||
class="native-textarea test-mock-editor"
|
||||
:id="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
v-model="item.requestBodyText"
|
||||
class="native-textarea test-mock-item-json"
|
||||
spellcheck="false"
|
||||
@blur="persistTestMockSet(testEntry, mockSet)"
|
||||
></textarea>
|
||||
<div v-if="item.requestBodyError" class="settings-error q-mt-sm">
|
||||
{{ item.requestBodyError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="field-label"
|
||||
:for="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
>
|
||||
Response Type
|
||||
</label>
|
||||
<input
|
||||
:id="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
v-model="item.responseType"
|
||||
class="native-input"
|
||||
type="text"
|
||||
placeholder="json"
|
||||
spellcheck="false"
|
||||
@blur="persistTestMockSet(testEntry, mockSet)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="test-mock-item-full">
|
||||
<label
|
||||
class="field-label"
|
||||
:for="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
>
|
||||
Response
|
||||
</label>
|
||||
<textarea
|
||||
:id="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
|
||||
v-model="item.responseText"
|
||||
class="native-textarea test-mock-item-json"
|
||||
spellcheck="false"
|
||||
@blur="persistTestMockSet(testEntry, mockSet)"
|
||||
></textarea>
|
||||
<div v-if="item.responseError" class="settings-error q-mt-sm">
|
||||
{{ item.responseError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.error" class="settings-error q-mt-sm">
|
||||
{{ item.error }}
|
||||
</div>
|
||||
@@ -1598,10 +1748,17 @@
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
|
||||
<div v-if="!currentFunctionTestTypeGroups.length" class="empty-state">
|
||||
<div v-else-if="currentFunction" class="empty-state">
|
||||
No tests are defined for this function.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
No functions are defined for this fixture file.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</main>
|
||||
@@ -2473,6 +2630,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function createTestMockItemDraftState(item = {}) {
|
||||
const source = isPlainObject(item) ? item : {}
|
||||
|
||||
return {
|
||||
descriptionText:
|
||||
typeof source.description === 'string' ? source.description : '',
|
||||
skip: typeof source.skip === 'boolean' ? source.skip : false,
|
||||
hasSkipField: typeof source.skip === 'boolean',
|
||||
requestType:
|
||||
typeof source.request_type === 'string' ? source.request_type : '',
|
||||
requestBodyText:
|
||||
source.request_body !== undefined
|
||||
? safeStringify(source.request_body)
|
||||
: '',
|
||||
requestBodyError: '',
|
||||
responseType:
|
||||
typeof source.response_type === 'string' ? source.response_type : '',
|
||||
responseText:
|
||||
source.response !== undefined ? safeStringify(source.response) : '',
|
||||
responseError: '',
|
||||
error: ''
|
||||
}
|
||||
}
|
||||
|
||||
function inferDatasetKeyFromData(data) {
|
||||
let rpcHint = false
|
||||
|
||||
@@ -2757,11 +2938,8 @@
|
||||
.filter(([key]) => key !== 'description')
|
||||
.map(([key, value]) => ({
|
||||
selectedMockName: key,
|
||||
itemTexts: (Array.isArray(value) ? value : [value]).map(
|
||||
item => ({
|
||||
text: safeStringify(item),
|
||||
error: ''
|
||||
})
|
||||
items: (Array.isArray(value) ? value : [value]).map(item =>
|
||||
createTestMockItemDraftState(item)
|
||||
)
|
||||
})),
|
||||
error: ''
|
||||
@@ -3003,10 +3181,7 @@
|
||||
|
||||
return {
|
||||
selectedMockName: mockName,
|
||||
itemTexts: normalizedItems.map(item => ({
|
||||
text: safeStringify(item),
|
||||
error: ''
|
||||
}))
|
||||
items: normalizedItems.map(item => createTestMockItemDraftState(item))
|
||||
}
|
||||
},
|
||||
testMockBindingOptions(selectedMockName = '') {
|
||||
@@ -3037,8 +3212,7 @@
|
||||
return availableName?.value || ''
|
||||
},
|
||||
testMockItemTitle(item, itemIndex) {
|
||||
const parsed = parseJsonText(item?.text || '')
|
||||
const description = parsed.ok ? parsed.data?.description : ''
|
||||
const description = item?.descriptionText || ''
|
||||
|
||||
if (typeof description === 'string' && description.trim() !== '') {
|
||||
return description.trim()
|
||||
@@ -3047,48 +3221,15 @@
|
||||
return `Test Mock Definition ${itemIndex + 1}`
|
||||
},
|
||||
testMockItemHasSkip(item) {
|
||||
const parsed = parseJsonText(item?.text || '')
|
||||
|
||||
return (
|
||||
parsed.ok &&
|
||||
isPlainObject(parsed.data) &&
|
||||
typeof parsed.data.skip === 'boolean'
|
||||
)
|
||||
return Boolean(item?.hasSkipField || item?.skip === true)
|
||||
},
|
||||
testMockItemSkip(item) {
|
||||
const parsed = parseJsonText(item?.text || '')
|
||||
|
||||
if (
|
||||
!parsed.ok ||
|
||||
!isPlainObject(parsed.data) ||
|
||||
typeof parsed.data.skip !== 'boolean'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return parsed.data.skip
|
||||
return Boolean(item?.skip)
|
||||
},
|
||||
setTestMockItemSkip(testEntry, mockSet, item, value) {
|
||||
const parsed = parseJsonText(item?.text || '')
|
||||
|
||||
if (
|
||||
!parsed.ok ||
|
||||
!isPlainObject(parsed.data) ||
|
||||
typeof parsed.data.skip !== 'boolean'
|
||||
) {
|
||||
item.error =
|
||||
'Could not update skip because this mock definition is not valid JSON with a boolean skip field.'
|
||||
this.notify(
|
||||
'Could not save mock skip flag.',
|
||||
'negative',
|
||||
item.error
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
item.error = ''
|
||||
parsed.data.skip = Boolean(value)
|
||||
item.text = safeStringify(parsed.data)
|
||||
item.hasSkipField = true
|
||||
item.skip = Boolean(value)
|
||||
this.persistTestMockSet(testEntry, mockSet)
|
||||
},
|
||||
addTestMockBinding(testEntry, mockSet) {
|
||||
@@ -3101,14 +3242,11 @@
|
||||
this.persistTestMockSet(testEntry, mockSet)
|
||||
},
|
||||
addTestMockBindingItem(testEntry, mockSet, binding) {
|
||||
binding.itemTexts.push({
|
||||
text: safeStringify({}),
|
||||
error: ''
|
||||
})
|
||||
binding.items.push(createTestMockItemDraftState({}))
|
||||
this.persistTestMockSet(testEntry, mockSet)
|
||||
},
|
||||
removeTestMockBindingItem(testEntry, mockSet, binding, itemIndex) {
|
||||
binding.itemTexts.splice(itemIndex, 1)
|
||||
binding.items.splice(itemIndex, 1)
|
||||
this.persistTestMockSet(testEntry, mockSet)
|
||||
},
|
||||
getSettingsDraftRows(datasetKey, fundingSourceName) {
|
||||
@@ -3374,8 +3512,10 @@
|
||||
|
||||
mockSet.error = ''
|
||||
;(mockSet.bindings || []).forEach(binding => {
|
||||
;(binding.itemTexts || []).forEach(item => {
|
||||
;(binding.items || []).forEach(item => {
|
||||
item.error = ''
|
||||
item.requestBodyError = ''
|
||||
item.responseError = ''
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3385,7 +3525,7 @@
|
||||
const mockName = (binding.selectedMockName || '').trim()
|
||||
|
||||
if (!mockName) {
|
||||
if ((binding.itemTexts || []).length) {
|
||||
if ((binding.items || []).length) {
|
||||
mockSet.error = 'Select a function mock before editing its overrides.'
|
||||
this.notify(
|
||||
'Could not save test mock definition.',
|
||||
@@ -3410,20 +3550,32 @@
|
||||
|
||||
const parsedItems = []
|
||||
|
||||
for (let itemIndex = 0; itemIndex < (binding.itemTexts || []).length; itemIndex += 1) {
|
||||
const item = binding.itemTexts[itemIndex]
|
||||
for (let itemIndex = 0; itemIndex < (binding.items || []).length; itemIndex += 1) {
|
||||
const item = binding.items[itemIndex]
|
||||
const nextItem = {}
|
||||
|
||||
if ((item.descriptionText || '').trim() !== '') {
|
||||
nextItem.description = item.descriptionText.trim()
|
||||
}
|
||||
|
||||
if (item.hasSkipField || item.skip === true) {
|
||||
nextItem.skip = Boolean(item.skip)
|
||||
}
|
||||
|
||||
if ((item.requestType || '').trim() !== '') {
|
||||
nextItem.request_type = item.requestType.trim()
|
||||
}
|
||||
|
||||
if ((item.requestBodyText || '').trim() !== '') {
|
||||
try {
|
||||
parsedItems.push(
|
||||
this.parseEditableTestJson(
|
||||
item.text,
|
||||
`${mockName} item ${itemIndex + 1}`
|
||||
)
|
||||
nextItem.request_body = this.parseEditableTestJson(
|
||||
item.requestBodyText,
|
||||
`${mockName} request_body ${itemIndex + 1}`
|
||||
)
|
||||
} catch (error) {
|
||||
item.error =
|
||||
item.requestBodyError =
|
||||
error instanceof Error ? error.message : String(error)
|
||||
mockSet.error = item.error
|
||||
mockSet.error = item.requestBodyError
|
||||
this.notify(
|
||||
'Could not save test mock definition.',
|
||||
'negative',
|
||||
@@ -3433,6 +3585,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
if ((item.responseType || '').trim() !== '') {
|
||||
nextItem.response_type = item.responseType.trim()
|
||||
}
|
||||
|
||||
if ((item.responseText || '').trim() !== '') {
|
||||
try {
|
||||
nextItem.response = this.parseEditableTestJson(
|
||||
item.responseText,
|
||||
`${mockName} response ${itemIndex + 1}`
|
||||
)
|
||||
} catch (error) {
|
||||
item.responseError =
|
||||
error instanceof Error ? error.message : String(error)
|
||||
mockSet.error = item.responseError
|
||||
this.notify(
|
||||
'Could not save test mock definition.',
|
||||
'negative',
|
||||
mockSet.error
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
parsedItems.push(nextItem)
|
||||
}
|
||||
|
||||
nextValue[mockName] = parsedItems
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user