feat: better ui

This commit is contained in:
Vlad Stan
2026-03-27 13:54:44 +02:00
parent 9f9057cca6
commit a2e5d1507c
+571 -47
View File
@@ -1477,22 +1477,21 @@
</div>
<div
v-for="(binding, bindingIndex) in mockSet.bindings"
:key="`${mockSet.path}-binding-${bindingIndex}`"
class="test-mock-binding"
:key="`${mockSet.path}-binding-${activeTestMockBindingName(mockSet)}`"
>
<div class="test-mock-binding-header">
<div class="test-mock-binding-body">
<label
class="field-label"
:for="`test-mock-binding-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}`"
:for="`test-mock-binding-${group.key}-${testEntry.index}-${mockSetIndex}`"
>
Function Mock
</label>
<q-select
:id="`test-mock-binding-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}`"
:model-value="binding.selectedMockName"
:options="testMockBindingOptions(binding.selectedMockName)"
:id="`test-mock-binding-${group.key}-${testEntry.index}-${mockSetIndex}`"
:model-value="activeTestMockBindingName(mockSet)"
:options="testMockBindingOptions(mockSet)"
emit-value
map-options
outlined
@@ -1501,7 +1500,7 @@
behavior="menu"
hide-dropdown-icon
class="fixture-select"
@update:model-value="handleTestMockBindingSelection(testEntry, mockSet, binding, $event)"
@update:model-value="handleTestMockBindingSelection(testEntry, mockSet, $event)"
>
<template v-slot:append>
<span class="fixture-select-icon" aria-hidden="true">expand_more</span>
@@ -1510,10 +1509,13 @@
</div>
</div>
<div class="test-mock-item-list" v-if="binding.items.length">
<div
class="test-mock-item-list"
v-if="activeTestMockBinding(mockSet) && activeTestMockBinding(mockSet).items.length"
>
<div
v-for="(item, itemIndex) in binding.items"
:key="`${mockSet.path}-binding-${bindingIndex}-item-${itemIndex}`"
v-for="(item, itemIndex) in activeTestMockBinding(mockSet).items"
:key="`${mockSet.path}-binding-${activeTestMockBindingName(mockSet)}-item-${itemIndex}`"
class="test-mock-item-card"
>
<div class="section-title">
@@ -1524,7 +1526,7 @@
color="negative"
outline
label="Remove"
@click="removeTestMockBindingItem(testEntry, mockSet, binding, itemIndex)"
@click="removeTestMockBindingItem(testEntry, mockSet, activeTestMockBinding(mockSet), itemIndex)"
></q-btn>
</div>
@@ -1543,12 +1545,12 @@
<div>
<label
class="field-label"
:for="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:for="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
>
Description
</label>
<input
:id="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:id="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
v-model="item.descriptionText"
class="native-input"
type="text"
@@ -1561,12 +1563,12 @@
<div>
<label
class="field-label"
:for="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:for="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
>
Request Type
</label>
<input
:id="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:id="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
v-model="item.requestType"
class="native-input"
type="text"
@@ -1579,12 +1581,12 @@
<div>
<label
class="field-label"
:for="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:for="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
>
Request Body
</label>
<textarea
:id="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:id="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
v-model="item.requestBodyText"
class="native-textarea test-mock-item-json"
spellcheck="false"
@@ -1598,12 +1600,12 @@
<div>
<label
class="field-label"
:for="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:for="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
>
Response Type
</label>
<input
:id="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:id="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
v-model="item.responseType"
class="native-input"
type="text"
@@ -1616,12 +1618,12 @@
<div class="test-mock-item-full">
<label
class="field-label"
:for="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:for="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
>
Response
</label>
<textarea
:id="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${bindingIndex}-${itemIndex}`"
:id="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
v-model="item.responseText"
class="native-textarea test-mock-item-json"
spellcheck="false"
@@ -1648,7 +1650,8 @@
color="primary"
outline
label="Add Object"
@click="addTestMockBindingItem(testEntry, mockSet, binding)"
:disable="!activeTestMockBinding(mockSet)"
@click="addTestMockBindingItem(testEntry, mockSet, activeTestMockBinding(mockSet))"
></q-btn>
</div>
</div>
@@ -2688,6 +2691,7 @@
const functionMockDrafts = {}
const selectedFunctionByDataset = {}
const selectedFundingSourceByDataset = {}
const selectedTestMockBindingByScope = {}
const selectedTestFundingSourceByDataset = {}
const settingsDrafts = {}
@@ -2709,6 +2713,7 @@
settingsDraftRowId: 0,
settingsDrafts,
selectedDatasetKey: 'rest',
selectedTestMockBindingByScope,
selectedTestFundingSourceByDataset,
selectedTestTypeByScope: {},
selectedFunctionByDataset,
@@ -3269,6 +3274,7 @@
selectedDatasetKey: this.selectedDatasetKey,
selectedFunctionByDataset: this.selectedFunctionByDataset,
selectedFundingSourceByDataset: this.selectedFundingSourceByDataset,
selectedTestMockBindingByScope: this.selectedTestMockBindingByScope,
selectedTestFundingSourceByDataset:
this.selectedTestFundingSourceByDataset,
selectedTestTypeByScope: this.selectedTestTypeByScope
@@ -3311,6 +3317,12 @@
)
}
if (isPlainObject(state.selectedTestMockBindingByScope)) {
this.selectedTestMockBindingByScope = {
...state.selectedTestMockBindingByScope
}
}
if (isPlainObject(state.selectedTestFundingSourceByDataset)) {
Object.assign(
this.selectedTestFundingSourceByDataset,
@@ -3375,6 +3387,7 @@
return {
id: ++this.functionMockDraftRowId,
originalName: name,
name,
uri: mock.uri ?? '',
method: mock.method ?? '',
@@ -3391,6 +3404,282 @@
error: ''
}
},
nextGeneratedFunctionMockName() {
const existingNames = new Set(
this.currentFunctionMockRows
.map(row => (row.name || '').trim())
.filter(Boolean)
)
let index = 1
let candidate = `new_mock_${index}`
while (existingNames.has(candidate)) {
index += 1
candidate = `new_mock_${index}`
}
return candidate
},
findSampleFunctionMockValue(datasetKey, functionName, fundingSourceName) {
const mocks =
this.datasets[datasetKey].data?.functions?.[functionName]?.mocks?.[
fundingSourceName
] || {}
return (
Object.values(mocks).find(value => isPlainObject(value)) || null
)
},
findSampleTestMockItem(datasetKey, functionName, fundingSourceName) {
const tests =
this.datasets[datasetKey].data?.functions?.[functionName]?.tests || []
for (const test of tests) {
const fundingSourceMocks = test?.mocks?.[fundingSourceName]
if (!fundingSourceMocks) {
continue
}
const mockSets = Array.isArray(fundingSourceMocks)
? fundingSourceMocks
: [fundingSourceMocks]
for (const mockSet of mockSets) {
if (!isPlainObject(mockSet)) {
continue
}
for (const [mockName, items] of Object.entries(mockSet)) {
if (mockName === 'description') {
continue
}
const firstItem = Array.isArray(items) ? items[0] : items
if (isPlainObject(firstItem)) {
return firstItem
}
}
}
}
return null
},
createFunctionMockValueTemplate(datasetKey, functionName, fundingSourceName) {
const sample = this.findSampleFunctionMockValue(
datasetKey,
functionName,
fundingSourceName
)
if (isPlainObject(sample)) {
const template = {}
const sampleMethod =
typeof sample.method === 'string' ? sample.method : ''
const looksLikeHttpVerb = /^[A-Z]+$/.test(sampleMethod)
if (Object.prototype.hasOwnProperty.call(sample, 'uri')) {
template.uri = ''
}
if (Object.prototype.hasOwnProperty.call(sample, 'headers')) {
template.headers = {}
}
if (Object.prototype.hasOwnProperty.call(sample, 'query_params')) {
template.query_params = {}
}
if (Object.prototype.hasOwnProperty.call(sample, 'method')) {
template.method = looksLikeHttpVerb ? sampleMethod : ''
}
if (Object.prototype.hasOwnProperty.call(sample, 'request_type')) {
template.request_type =
typeof sample.request_type === 'string' && sample.request_type.trim()
? sample.request_type
: 'function'
}
if (Object.prototype.hasOwnProperty.call(sample, 'response_type')) {
template.response_type =
typeof sample.response_type === 'string' &&
sample.response_type.trim()
? sample.response_type
: 'data'
}
if (Object.prototype.hasOwnProperty.call(sample, 'response')) {
template.response = {}
}
return template
}
if (datasetKey === 'rest') {
return {
uri: '',
headers: {},
method: 'GET'
}
}
return {
method: '',
request_type: 'function',
response_type: 'data',
response: {}
}
},
createTestMockItemTemplate(
datasetKey,
functionName,
fundingSourceName,
functionMockValue = {}
) {
const sample = this.findSampleTestMockItem(
datasetKey,
functionName,
fundingSourceName
)
if (isPlainObject(sample)) {
const template = {}
if (Object.prototype.hasOwnProperty.call(sample, 'request_type')) {
template.request_type =
typeof sample.request_type === 'string' ? sample.request_type : ''
}
if (Object.prototype.hasOwnProperty.call(sample, 'request_body')) {
template.request_body = {}
}
if (Object.prototype.hasOwnProperty.call(sample, 'response_type')) {
template.response_type =
typeof sample.response_type === 'string'
? sample.response_type
: 'json'
}
template.response = {}
return template
}
if (datasetKey === 'rest') {
return {
response_type: 'json',
response: {}
}
}
if (
isPlainObject(functionMockValue) &&
Object.prototype.hasOwnProperty.call(functionMockValue, 'response_type')
) {
return {
response: {}
}
}
return {
response_type: 'data',
response: {}
}
},
withFundingSourceMockSets(data, functionName, fundingSourceName, visitor) {
const tests = data?.functions?.[functionName]?.tests || []
tests.forEach((test, testIndex) => {
if (
!test.mocks ||
!Object.prototype.hasOwnProperty.call(test.mocks, fundingSourceName)
) {
return
}
const fundingSourceMocks = test.mocks[fundingSourceName]
if (Array.isArray(fundingSourceMocks)) {
fundingSourceMocks.forEach((mockSet, mockSetIndex) => {
if (isPlainObject(mockSet)) {
visitor(mockSet, test, testIndex, mockSetIndex)
}
})
return
}
if (isPlainObject(fundingSourceMocks)) {
visitor(fundingSourceMocks, test, testIndex, 0)
}
})
},
addFunctionMockToTestSets(
data,
functionName,
fundingSourceName,
mockName,
functionMockValue
) {
const defaultItem = this.createTestMockItemTemplate(
this.selectedDatasetKey,
functionName,
fundingSourceName,
functionMockValue
)
this.withFundingSourceMockSets(
data,
functionName,
fundingSourceName,
mockSet => {
if (!Object.prototype.hasOwnProperty.call(mockSet, mockName)) {
mockSet[mockName] = [deepClone(defaultItem)]
}
}
)
},
renameFunctionMockInTestSets(
data,
functionName,
fundingSourceName,
previousName,
nextName
) {
if (!previousName || !nextName || previousName === nextName) {
return
}
this.withFundingSourceMockSets(
data,
functionName,
fundingSourceName,
mockSet => {
if (!Object.prototype.hasOwnProperty.call(mockSet, previousName)) {
return
}
if (!Object.prototype.hasOwnProperty.call(mockSet, nextName)) {
mockSet[nextName] = mockSet[previousName]
}
delete mockSet[previousName]
}
)
},
removeFunctionMockFromTestSets(
data,
functionName,
fundingSourceName,
mockName
) {
if (!mockName) {
return
}
this.withFundingSourceMockSets(
data,
functionName,
fundingSourceName,
mockSet => {
if (Object.prototype.hasOwnProperty.call(mockSet, mockName)) {
delete mockSet[mockName]
}
}
)
},
createTestMockBindingDraft(mockName = '', items = []) {
const normalizedItems = Array.isArray(items)
? items
@@ -3403,13 +3692,31 @@
items: normalizedItems.map(item => createTestMockItemDraftState(item))
}
},
testMockBindingOptions(selectedMockName = '') {
const names = this.currentFunctionMockNameOptions.map(
option => option.value
)
testMockBindingScopeKey(mockSet) {
if (!mockSet?.path) {
return ''
}
if (selectedMockName && !names.includes(selectedMockName)) {
names.unshift(selectedMockName)
return [
this.selectedDatasetKey,
this.currentFunctionName,
this.currentTestFundingSourceName,
mockSet.path
].join(':')
},
testMockBindingOptions(mockSet) {
const names = this.currentFunctionMockNameOptions.map(option => option.value)
const activeName = this.activeTestMockBindingName(mockSet)
;(mockSet?.bindings || []).forEach(binding => {
const name = (binding.selectedMockName || '').trim()
if (name && !names.includes(name)) {
names.push(name)
}
})
if (activeName && !names.includes(activeName)) {
names.unshift(activeName)
}
return names.map(name => ({
@@ -3417,6 +3724,29 @@
value: name
}))
},
activeTestMockBindingName(mockSet) {
const scopeKey = this.testMockBindingScopeKey(mockSet)
const selectedName = scopeKey
? this.selectedTestMockBindingByScope[scopeKey]
: ''
const bindingNames = (mockSet?.bindings || [])
.map(binding => (binding.selectedMockName || '').trim())
.filter(Boolean)
if (selectedName && bindingNames.includes(selectedName)) {
return selectedName
}
return bindingNames[0] || ''
},
activeTestMockBinding(mockSet) {
const activeName = this.activeTestMockBindingName(mockSet)
return (
(mockSet?.bindings || []).find(
binding => (binding.selectedMockName || '').trim() === activeName
) || null
)
},
nextAvailableTestMockName(mockSet) {
const usedNames = new Set(
(mockSet.bindings || [])
@@ -3430,6 +3760,21 @@
return availableName?.value || ''
},
createDefaultTestMockBindingItems(mockName) {
const functionMockValue =
this.datasets[this.selectedDatasetKey].data?.functions?.[
this.currentFunctionName
]?.mocks?.[this.currentTestFundingSourceName]?.[mockName] || {}
return [
this.createTestMockItemTemplate(
this.selectedDatasetKey,
this.currentFunctionName,
this.currentTestFundingSourceName,
functionMockValue
)
]
},
testMockItemTitle(item, itemIndex) {
const description = item?.descriptionText || ''
@@ -3451,29 +3796,53 @@
item.skip = Boolean(value)
this.persistTestMockSet(testEntry, mockSet)
},
handleTestMockBindingSelection(testEntry, mockSet, binding, value) {
handleTestMockBindingSelection(testEntry, mockSet, value) {
const nextName = typeof value === 'string' ? value : ''
const previousName = (binding.selectedMockName || '').trim()
binding.selectedMockName = nextName
if (!nextName) {
binding.items = []
} else if (nextName !== previousName || !Array.isArray(binding.items)) {
binding.items = [createTestMockItemDraftState({})]
} else if (!binding.items.length) {
binding.items.push(createTestMockItemDraftState({}))
const scopeKey = this.testMockBindingScopeKey(mockSet)
if (!nextName || !scopeKey) {
return
}
this.persistTestMockSet(testEntry, mockSet)
let binding = (mockSet.bindings || []).find(
candidate => (candidate.selectedMockName || '').trim() === nextName
)
if (!binding) {
binding = this.createTestMockBindingDraft(
nextName,
this.createDefaultTestMockBindingItems(nextName)
)
mockSet.bindings.push(binding)
this.selectedTestMockBindingByScope[scopeKey] = nextName
this.persistUiState()
this.persistTestMockSet(testEntry, mockSet)
return
}
if (!Array.isArray(binding.items)) {
binding.items = []
}
this.selectedTestMockBindingByScope[scopeKey] = nextName
this.persistUiState()
},
addTestMockBinding(testEntry, mockSet) {
const nextName = this.nextAvailableTestMockName(mockSet)
if (!nextName) {
return
}
mockSet.bindings.push(
this.createTestMockBindingDraft(
this.nextAvailableTestMockName(mockSet),
[{}]
nextName,
this.createDefaultTestMockBindingItems(nextName)
)
)
const scopeKey = this.testMockBindingScopeKey(mockSet)
if (scopeKey) {
this.selectedTestMockBindingByScope[scopeKey] = nextName
this.persistUiState()
}
this.persistTestMockSet(testEntry, mockSet)
},
addTestMockBindingItem(testEntry, mockSet, binding) {
@@ -3998,17 +4367,89 @@
return
}
const newMockName = this.nextGeneratedFunctionMockName()
const nextMockValue = this.createFunctionMockValueTemplate(
this.selectedDatasetKey,
this.currentFunctionName,
this.currentTestFundingSourceName
)
const rows = this.ensureFunctionMockDraft(
this.selectedDatasetKey,
this.currentFunctionName,
this.currentTestFundingSourceName
)
rows.push(this.createFunctionMockDraftRow('', {}))
rows.push(this.createFunctionMockDraftRow(newMockName, nextMockValue))
this.applyDatasetMutation(
this.selectedDatasetKey,
data => {
if (!data.functions[this.currentFunctionName].mocks) {
data.functions[this.currentFunctionName].mocks = {}
}
if (
!data.functions[this.currentFunctionName].mocks[
this.currentTestFundingSourceName
]
) {
data.functions[this.currentFunctionName].mocks[
this.currentTestFundingSourceName
] = {}
}
data.functions[this.currentFunctionName].mocks[
this.currentTestFundingSourceName
][newMockName] = deepClone(nextMockValue)
this.addFunctionMockToTestSets(
data,
this.currentFunctionName,
this.currentTestFundingSourceName,
newMockName,
nextMockValue
)
},
{
preserveFunctionMockDrafts: true,
preserveSettingsDrafts: true
}
)
},
removeFunctionMockRow(rowIndex) {
const rows = this.currentFunctionMockRows
rows.splice(rowIndex, 1)
this.persistFunctionMocks()
const [removedRow] = rows.splice(rowIndex, 1)
this.applyDatasetMutation(
this.selectedDatasetKey,
data => {
const functionMocks =
data.functions?.[this.currentFunctionName]?.mocks?.[
this.currentTestFundingSourceName
]
if (
functionMocks &&
removedRow?.originalName &&
Object.prototype.hasOwnProperty.call(
functionMocks,
removedRow.originalName
)
) {
delete functionMocks[removedRow.originalName]
}
this.removeFunctionMockFromTestSets(
data,
this.currentFunctionName,
this.currentTestFundingSourceName,
removedRow?.originalName || removedRow?.name || ''
)
},
{
preserveFunctionMockDrafts: true,
preserveSettingsDrafts: true
}
)
},
persistFunctionMocks(options = {}) {
if (!this.currentFunctionName || !this.currentTestFundingSourceName) {
@@ -4017,6 +4458,7 @@
const rows = this.currentFunctionMockRows
const nextMocks = {}
const renamePairs = []
for (const row of rows) {
row.error = ''
@@ -4024,9 +4466,34 @@
for (const row of rows) {
const name = row.name.trim()
const originalName = (row.originalName || '').trim()
if (!name && !originalName) {
continue
}
if (!name) {
continue
row.error = 'Mock name cannot be empty.'
if (!options.silent) {
this.notify(
`Could not save mock "${originalName || 'new mock'}".`,
'negative',
row.error
)
}
return
}
if (Object.prototype.hasOwnProperty.call(nextMocks, name)) {
row.error = `Duplicate mock name: ${name}`
if (!options.silent) {
this.notify(
`Could not save mock "${name}".`,
'negative',
row.error
)
}
return
}
try {
@@ -4043,22 +4510,79 @@
}
return
}
if (originalName && originalName !== name) {
renamePairs.push({
from: originalName,
to: name
})
}
}
this.applyDatasetMutation(
this.selectedDatasetKey,
data => {
const currentMocks =
data.functions?.[this.currentFunctionName]?.mocks?.[
this.currentTestFundingSourceName
] || {}
const currentNames = Object.keys(currentMocks)
const nextNames = Object.keys(nextMocks)
const renameFrom = new Set(renamePairs.map(item => item.from))
const renameTo = new Set(renamePairs.map(item => item.to))
const removedNames = currentNames.filter(
name => !nextNames.includes(name) && !renameFrom.has(name)
)
const addedNames = nextNames.filter(
name => !currentNames.includes(name) && !renameTo.has(name)
)
if (!data.functions[this.currentFunctionName].mocks) {
data.functions[this.currentFunctionName].mocks = {}
}
renamePairs.forEach(pair => {
this.renameFunctionMockInTestSets(
data,
this.currentFunctionName,
this.currentTestFundingSourceName,
pair.from,
pair.to
)
})
removedNames.forEach(name => {
this.removeFunctionMockFromTestSets(
data,
this.currentFunctionName,
this.currentTestFundingSourceName,
name
)
})
addedNames.forEach(name => {
this.addFunctionMockToTestSets(
data,
this.currentFunctionName,
this.currentTestFundingSourceName,
name,
nextMocks[name]
)
})
data.functions[this.currentFunctionName].mocks[
this.currentTestFundingSourceName
] = nextMocks
},
{
preserveFunctionMockDrafts: true
preserveFunctionMockDrafts: true,
preserveSettingsDrafts: true
}
)
rows.forEach(row => {
row.originalName = row.name.trim()
})
},
baseWalletPath(functionName, fundingSourceName) {
return `functions.${functionName}.mocks.${fundingSourceName}`