@@ -1524,7 +1526,7 @@
color="negative"
outline
label="Remove"
- @click="removeTestMockBindingItem(testEntry, mockSet, binding, itemIndex)"
+ @click="removeTestMockBindingItem(testEntry, mockSet, activeTestMockBinding(mockSet), itemIndex)"
>
@@ -1543,12 +1545,12 @@
@@ -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}`