This commit is contained in:
Vlad Stan
2026-03-27 11:43:31 +02:00
parent 0003574d21
commit 77f112cd5e
+298 -120
View File
@@ -199,6 +199,10 @@
height: 100%; height: 100%;
} }
.picker-card--full {
grid-column: 1 / -1;
}
.picker-card .q-card__section--vert { .picker-card .q-card__section--vert {
height: 100%; height: 100%;
padding: 16px 18px; padding: 16px 18px;
@@ -590,6 +594,20 @@
background: rgba(25, 54, 61, 0.03); 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 { .detail-label {
color: var(--studio-muted); color: var(--studio-muted);
font-size: 0.74rem; font-size: 0.74rem;
@@ -888,18 +906,78 @@
gap: 12px; 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 { .test-type-tabs {
margin-top: 8px;
border-radius: 18px; border-radius: 18px;
background: rgba(22, 32, 40, 0.04); background: rgba(22, 32, 40, 0.04);
padding: 6px; padding: 6px;
} }
.test-type-tabs .q-tab { .test-type-tabs .q-tab {
flex: 1 1 0; justify-content: flex-start;
min-width: 0; min-height: 48px;
border-radius: 12px; border-radius: 12px;
color: var(--q-primary); 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 { .test-type-tabs .q-tab--active {
@@ -914,9 +992,9 @@
} }
.test-type-panels { .test-type-panels {
margin-top: 14px;
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
min-width: 0;
} }
.test-type-panel { .test-type-panel {
@@ -1110,8 +1188,13 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.test-type-layout {
grid-template-columns: 1fr;
}
.mock-source-grid, .mock-source-grid,
.test-detail-grid, .test-detail-grid,
.test-mock-item-grid,
.settings-row-grid, .settings-row-grid,
.settings-table-grid { .settings-table-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -1226,7 +1309,7 @@
<main class="workspace-grid"> <main class="workspace-grid">
<section class="primary-row"> <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> <q-card-section>
<div class="section-title"> <div class="section-title">
<div> <div>
@@ -1280,39 +1363,34 @@
</q-card-section> </q-card-section>
</q-card> </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> <q-card-section>
<div class="section-title"> <q-tabs
<div> v-if="functionOptions.length"
<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"
v-model="selectedFunctionByDataset[selectedDatasetKey]" v-model="selectedFunctionByDataset[selectedDatasetKey]"
:options="functionOptions"
emit-value
map-options
outlined
dense dense
options-dense active-color="white"
behavior="menu" indicator-color="transparent"
hide-dropdown-icon align="justify"
class="fixture-select picker-select" class="function-tabs text-primary"
> >
<template v-slot:append> <q-tab
<span class="fixture-select-icon" aria-hidden="true">expand_more</span> v-for="fn in currentDataset.analysis.functions"
</template> :key="fn.name"
</q-select> :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 <q-btn
color="primary" color="primary"
outline outline
@@ -1321,29 +1399,14 @@
@click="functionMocksDialog = true" @click="functionMocksDialog = true"
></q-btn> ></q-btn>
</div> </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 <q-tabs
v-if="currentFunctionTestTypeGroups.length"
v-model="currentSelectedTestTypeKey" v-model="currentSelectedTestTypeKey"
vertical
dense dense
active-color="white" active-color="white"
indicator-color="transparent" indicator-color="transparent"
align="justify"
class="test-type-tabs text-primary" class="test-type-tabs text-primary"
> >
<q-tab <q-tab
@@ -1518,9 +1581,9 @@
</div> </div>
</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 <div
v-for="(item, itemIndex) in binding.itemTexts" v-for="(item, itemIndex) in binding.items"
:key="`${mockSet.path}-binding-${bindingIndex}-item-${itemIndex}`" :key="`${mockSet.path}-binding-${bindingIndex}-item-${itemIndex}`"
class="test-mock-item-card" class="test-mock-item-card"
> >
@@ -1547,13 +1610,100 @@
></q-checkbox> ></q-checkbox>
</div> </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 <textarea
:id="`test-mock-definition-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`" :id="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
v-model="item.text" v-model="item.requestBodyText"
class="native-textarea test-mock-editor" class="native-textarea test-mock-item-json"
spellcheck="false" spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)" @blur="persistTestMockSet(testEntry, mockSet)"
></textarea> ></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"> <div v-if="item.error" class="settings-error q-mt-sm">
{{ item.error }} {{ item.error }}
</div> </div>
@@ -1598,10 +1748,17 @@
</div> </div>
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </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. No tests are defined for this function.
</div> </div>
</div>
</div>
<div v-else class="empty-state">
No functions are defined for this fixture file.
</div>
</q-card-section> </q-card-section>
</q-card> </q-card>
</main> </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) { function inferDatasetKeyFromData(data) {
let rpcHint = false let rpcHint = false
@@ -2757,11 +2938,8 @@
.filter(([key]) => key !== 'description') .filter(([key]) => key !== 'description')
.map(([key, value]) => ({ .map(([key, value]) => ({
selectedMockName: key, selectedMockName: key,
itemTexts: (Array.isArray(value) ? value : [value]).map( items: (Array.isArray(value) ? value : [value]).map(item =>
item => ({ createTestMockItemDraftState(item)
text: safeStringify(item),
error: ''
})
) )
})), })),
error: '' error: ''
@@ -3003,10 +3181,7 @@
return { return {
selectedMockName: mockName, selectedMockName: mockName,
itemTexts: normalizedItems.map(item => ({ items: normalizedItems.map(item => createTestMockItemDraftState(item))
text: safeStringify(item),
error: ''
}))
} }
}, },
testMockBindingOptions(selectedMockName = '') { testMockBindingOptions(selectedMockName = '') {
@@ -3037,8 +3212,7 @@
return availableName?.value || '' return availableName?.value || ''
}, },
testMockItemTitle(item, itemIndex) { testMockItemTitle(item, itemIndex) {
const parsed = parseJsonText(item?.text || '') const description = item?.descriptionText || ''
const description = parsed.ok ? parsed.data?.description : ''
if (typeof description === 'string' && description.trim() !== '') { if (typeof description === 'string' && description.trim() !== '') {
return description.trim() return description.trim()
@@ -3047,48 +3221,15 @@
return `Test Mock Definition ${itemIndex + 1}` return `Test Mock Definition ${itemIndex + 1}`
}, },
testMockItemHasSkip(item) { testMockItemHasSkip(item) {
const parsed = parseJsonText(item?.text || '') return Boolean(item?.hasSkipField || item?.skip === true)
return (
parsed.ok &&
isPlainObject(parsed.data) &&
typeof parsed.data.skip === 'boolean'
)
}, },
testMockItemSkip(item) { testMockItemSkip(item) {
const parsed = parseJsonText(item?.text || '') return Boolean(item?.skip)
if (
!parsed.ok ||
!isPlainObject(parsed.data) ||
typeof parsed.data.skip !== 'boolean'
) {
return false
}
return parsed.data.skip
}, },
setTestMockItemSkip(testEntry, mockSet, item, value) { 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 = '' item.error = ''
parsed.data.skip = Boolean(value) item.hasSkipField = true
item.text = safeStringify(parsed.data) item.skip = Boolean(value)
this.persistTestMockSet(testEntry, mockSet) this.persistTestMockSet(testEntry, mockSet)
}, },
addTestMockBinding(testEntry, mockSet) { addTestMockBinding(testEntry, mockSet) {
@@ -3101,14 +3242,11 @@
this.persistTestMockSet(testEntry, mockSet) this.persistTestMockSet(testEntry, mockSet)
}, },
addTestMockBindingItem(testEntry, mockSet, binding) { addTestMockBindingItem(testEntry, mockSet, binding) {
binding.itemTexts.push({ binding.items.push(createTestMockItemDraftState({}))
text: safeStringify({}),
error: ''
})
this.persistTestMockSet(testEntry, mockSet) this.persistTestMockSet(testEntry, mockSet)
}, },
removeTestMockBindingItem(testEntry, mockSet, binding, itemIndex) { removeTestMockBindingItem(testEntry, mockSet, binding, itemIndex) {
binding.itemTexts.splice(itemIndex, 1) binding.items.splice(itemIndex, 1)
this.persistTestMockSet(testEntry, mockSet) this.persistTestMockSet(testEntry, mockSet)
}, },
getSettingsDraftRows(datasetKey, fundingSourceName) { getSettingsDraftRows(datasetKey, fundingSourceName) {
@@ -3374,8 +3512,10 @@
mockSet.error = '' mockSet.error = ''
;(mockSet.bindings || []).forEach(binding => { ;(mockSet.bindings || []).forEach(binding => {
;(binding.itemTexts || []).forEach(item => { ;(binding.items || []).forEach(item => {
item.error = '' item.error = ''
item.requestBodyError = ''
item.responseError = ''
}) })
}) })
@@ -3385,7 +3525,7 @@
const mockName = (binding.selectedMockName || '').trim() const mockName = (binding.selectedMockName || '').trim()
if (!mockName) { if (!mockName) {
if ((binding.itemTexts || []).length) { if ((binding.items || []).length) {
mockSet.error = 'Select a function mock before editing its overrides.' mockSet.error = 'Select a function mock before editing its overrides.'
this.notify( this.notify(
'Could not save test mock definition.', 'Could not save test mock definition.',
@@ -3410,20 +3550,32 @@
const parsedItems = [] const parsedItems = []
for (let itemIndex = 0; itemIndex < (binding.itemTexts || []).length; itemIndex += 1) { for (let itemIndex = 0; itemIndex < (binding.items || []).length; itemIndex += 1) {
const item = binding.itemTexts[itemIndex] 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 { try {
parsedItems.push( nextItem.request_body = this.parseEditableTestJson(
this.parseEditableTestJson( item.requestBodyText,
item.text, `${mockName} request_body ${itemIndex + 1}`
`${mockName} item ${itemIndex + 1}`
)
) )
} catch (error) { } catch (error) {
item.error = item.requestBodyError =
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
mockSet.error = item.error mockSet.error = item.requestBodyError
this.notify( this.notify(
'Could not save test mock definition.', 'Could not save test mock definition.',
'negative', '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 nextValue[mockName] = parsedItems
} }