+
+
+
+
+ Function Mocks
+
+
+
-
-
-
Function Mocks
+
+
+ {{ currentFunctionMockRows.length }} mocks
+
+
+
+
+
Reusable mocks for this function and the selected funding source.
-
-
-
-
-
-
-
+
+
+
+
-
- No function-level mocks are defined for
- {{ currentFundingSource.name }}
- in
- {{ currentFunction.name }}.
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ mockRow.error }}
+
+
+ Leave unused fields blank. JSON columns must contain valid JSON objects or values.
+
+
+
+
+
+
+
+ No function-level mocks are defined yet for
+ {{ currentFundingSource.name }}
+ in
+ {{ currentFunction.name }}.
+
+
+
+
+
+
+
@@ -1135,13 +1563,6 @@
max-height="220px"
>
-
-
-
@@ -1702,6 +2123,38 @@
}
}
+ function settingTypeFromValue(value) {
+ if (value === null) {
+ return 'null'
+ }
+ if (Array.isArray(value) || isPlainObject(value)) {
+ return 'json'
+ }
+ if (typeof value === 'boolean') {
+ return 'boolean'
+ }
+ if (typeof value === 'number') {
+ return 'number'
+ }
+ return 'string'
+ }
+
+ function settingValueToText(value, type = settingTypeFromValue(value)) {
+ if (type === 'null') {
+ return 'null'
+ }
+ if (type === 'boolean') {
+ return value === true ? 'true' : 'false'
+ }
+ if (type === 'number') {
+ return value === undefined || value === null ? '' : String(value)
+ }
+ if (type === 'json') {
+ return safeStringify(value)
+ }
+ return value === undefined || value === null ? '' : String(value)
+ }
+
const { createApp } = Vue
createApp({
@@ -1710,8 +2163,10 @@
},
data() {
const datasets = {}
+ const functionMockDrafts = {}
const selectedFunctionByDataset = {}
const selectedFundingSourceByDataset = {}
+ const settingsDrafts = {}
DATASET_DEFS.forEach(def => {
datasets[def.key] = createDatasetState(def)
@@ -1723,6 +2178,10 @@
datasetDefs: DATASET_DEFS,
datasets,
editorDialog: false,
+ functionMockDraftRowId: 0,
+ functionMockDrafts,
+ settingsDraftRowId: 0,
+ settingsDrafts,
selectedDatasetKey: 'rest',
selectedFunctionByDataset,
selectedFundingSourceByDataset
@@ -1741,6 +2200,15 @@
value: fs.name
}))
},
+ settingTypeOptions() {
+ return [
+ { label: 'String', value: 'string' },
+ { label: 'Boolean', value: 'boolean' },
+ { label: 'Number', value: 'number' },
+ { label: 'Null', value: 'null' },
+ { label: 'JSON', value: 'json' }
+ ]
+ },
currentFundingSourceName() {
return this.selectedFundingSourceByDataset[this.selectedDatasetKey] || ''
},
@@ -1751,16 +2219,14 @@
) || null
)
},
- currentFundingSourceSettings() {
- if (!this.currentFundingSource?.settings) {
+ currentFundingSourceSettingRows() {
+ if (!this.currentFundingSourceName) {
return []
}
- return Object.entries(this.currentFundingSource.settings).map(
- ([key, value]) => ({
- key,
- value
- })
+ return this.getSettingsDraftRows(
+ this.selectedDatasetKey,
+ this.currentFundingSourceName
)
},
functionOptions() {
@@ -1790,20 +2256,6 @@
editorDirty() {
return this.currentDataset.rawText !== this.currentDataset.snapshotText
},
- currentFunctionBaseMockEntry() {
- if (!this.currentFunction || !this.currentFundingSource) {
- return null
- }
-
- return (
- this.currentFunction.baseMockWallets.find(
- entry => entry.fundingSource.name === this.currentFundingSource.name
- ) || null
- )
- },
- currentFunctionBaseMockEntries() {
- return this.currentFunctionBaseMockEntry?.mockEntries || []
- },
currentFunctionBaseMockPath() {
if (!this.currentFunction || !this.currentFundingSource) {
return ''
@@ -1814,6 +2266,17 @@
this.currentFundingSource.name
)
},
+ currentFunctionMockRows() {
+ if (!this.currentFunctionName || !this.currentFundingSourceName) {
+ return []
+ }
+
+ return this.getFunctionMockDraftRows(
+ this.selectedDatasetKey,
+ this.currentFunctionName,
+ this.currentFundingSourceName
+ )
+ },
currentFunctionTestsForFundingSource() {
if (!this.currentFunction || !this.currentFundingSource) {
return []
@@ -1912,6 +2375,15 @@
watch: {
selectedDatasetKey() {
this.ensureSelections(this.selectedDatasetKey)
+ this.ensureSettingsDraftForCurrent()
+ this.ensureFunctionMockDraftForCurrent()
+ },
+ currentFundingSourceName() {
+ this.ensureSettingsDraftForCurrent()
+ this.ensureFunctionMockDraftForCurrent()
+ },
+ currentFunctionName() {
+ this.ensureFunctionMockDraftForCurrent()
}
},
mounted() {
@@ -1938,27 +2410,6 @@
this.saveDataset(this.currentDataset.key)
}
},
- mockSummary(mock) {
- const pieces = []
-
- if (mock.method && mock.uri) {
- pieces.push(`${mock.method} ${mock.uri}`)
- } else if (mock.method) {
- pieces.push(mock.method)
- } else if (mock.uri) {
- pieces.push(mock.uri)
- }
-
- if (mock.request_type) {
- pieces.push(`request=${mock.request_type}`)
- }
-
- if (mock.response_type) {
- pieces.push(`response=${mock.response_type}`)
- }
-
- return pieces.join(' ยท ') || 'No mock metadata'
- },
prettyValue(value) {
if (typeof value === 'string') {
return value
@@ -1973,6 +2424,386 @@
return safeStringify(value)
},
+ makeSettingsDraftKey(datasetKey, fundingSourceName) {
+ return `${datasetKey}:${fundingSourceName}`
+ },
+ makeFunctionMockDraftKey(datasetKey, functionName, fundingSourceName) {
+ return `${datasetKey}:${functionName}:${fundingSourceName}`
+ },
+ createSettingsDraftRow(key = '', value = '') {
+ const type = settingTypeFromValue(value)
+
+ return {
+ id: ++this.settingsDraftRowId,
+ key,
+ type,
+ valueText: settingValueToText(value, type),
+ error: ''
+ }
+ },
+ createFunctionMockDraftRow(name = '', value = {}) {
+ const mock = isPlainObject(value) ? value : {}
+
+ return {
+ id: ++this.functionMockDraftRowId,
+ name,
+ uri: mock.uri ?? '',
+ method: mock.method ?? '',
+ headersText:
+ mock.headers !== undefined ? safeStringify(mock.headers) : '',
+ queryParamsText:
+ mock.query_params !== undefined
+ ? safeStringify(mock.query_params)
+ : '',
+ requestType: mock.request_type ?? '',
+ responseType: mock.response_type ?? '',
+ responseText:
+ mock.response !== undefined ? safeStringify(mock.response) : '',
+ error: ''
+ }
+ },
+ getSettingsDraftRows(datasetKey, fundingSourceName) {
+ const draftKey = this.makeSettingsDraftKey(datasetKey, fundingSourceName)
+ return this.settingsDrafts[draftKey] || []
+ },
+ getFunctionMockDraftRows(datasetKey, functionName, fundingSourceName) {
+ const draftKey = this.makeFunctionMockDraftKey(
+ datasetKey,
+ functionName,
+ fundingSourceName
+ )
+ return this.functionMockDrafts[draftKey] || []
+ },
+ ensureSettingsDraft(datasetKey, fundingSourceName, force = false) {
+ if (!datasetKey || !fundingSourceName) {
+ return []
+ }
+
+ const draftKey = this.makeSettingsDraftKey(datasetKey, fundingSourceName)
+
+ if (!force && this.settingsDrafts[draftKey]) {
+ return this.settingsDrafts[draftKey]
+ }
+
+ const settings =
+ this.datasets[datasetKey].data?.funding_sources?.[fundingSourceName]
+ ?.settings || {}
+
+ this.settingsDrafts[draftKey] = Object.entries(settings).map(
+ ([key, value]) => this.createSettingsDraftRow(key, value)
+ )
+
+ return this.settingsDrafts[draftKey]
+ },
+ ensureFunctionMockDraft(
+ datasetKey,
+ functionName,
+ fundingSourceName,
+ force = false
+ ) {
+ if (!datasetKey || !functionName || !fundingSourceName) {
+ return []
+ }
+
+ const draftKey = this.makeFunctionMockDraftKey(
+ datasetKey,
+ functionName,
+ fundingSourceName
+ )
+
+ if (!force && this.functionMockDrafts[draftKey]) {
+ return this.functionMockDrafts[draftKey]
+ }
+
+ const mocks =
+ this.datasets[datasetKey].data?.functions?.[functionName]?.mocks?.[
+ fundingSourceName
+ ] || {}
+
+ this.functionMockDrafts[draftKey] = Object.entries(mocks).map(
+ ([name, value]) => this.createFunctionMockDraftRow(name, value)
+ )
+
+ return this.functionMockDrafts[draftKey]
+ },
+ ensureSettingsDraftForCurrent(force = false) {
+ if (!this.currentFundingSourceName) {
+ return
+ }
+
+ this.ensureSettingsDraft(
+ this.selectedDatasetKey,
+ this.currentFundingSourceName,
+ force
+ )
+ },
+ ensureFunctionMockDraftForCurrent(force = false) {
+ if (!this.currentFunctionName || !this.currentFundingSourceName) {
+ return
+ }
+
+ this.ensureFunctionMockDraft(
+ this.selectedDatasetKey,
+ this.currentFunctionName,
+ this.currentFundingSourceName,
+ force
+ )
+ },
+ clearSettingsDrafts(datasetKey) {
+ Object.keys(this.settingsDrafts).forEach(key => {
+ if (key.startsWith(`${datasetKey}:`)) {
+ delete this.settingsDrafts[key]
+ }
+ })
+ },
+ clearFunctionMockDrafts(datasetKey) {
+ Object.keys(this.functionMockDrafts).forEach(key => {
+ if (key.startsWith(`${datasetKey}:`)) {
+ delete this.functionMockDrafts[key]
+ }
+ })
+ },
+ parseSettingRowValue(row) {
+ if (row.type === 'boolean') {
+ return row.valueText === 'true'
+ }
+
+ if (row.type === 'number') {
+ if (row.valueText === '') {
+ throw new Error('Number values cannot be empty.')
+ }
+
+ const value = Number(row.valueText)
+ if (Number.isNaN(value)) {
+ throw new Error('Enter a valid number.')
+ }
+
+ return value
+ }
+
+ if (row.type === 'null') {
+ return null
+ }
+
+ if (row.type === 'json') {
+ if (row.valueText.trim() === '') {
+ throw new Error('Enter valid JSON for this value.')
+ }
+
+ return JSON.parse(row.valueText)
+ }
+
+ return row.valueText
+ },
+ parseFunctionMockRowBody(row) {
+ const mock = {}
+
+ const name = row.name.trim()
+ if (!name) {
+ throw new Error('Mock name cannot be empty.')
+ }
+
+ if (row.uri.trim() !== '') {
+ mock.uri = row.uri
+ }
+
+ if (row.method.trim() !== '') {
+ mock.method = row.method
+ }
+
+ if (row.headersText.trim() !== '') {
+ const parsedHeaders = JSON.parse(row.headersText)
+ if (!isPlainObject(parsedHeaders)) {
+ throw new Error('Headers must be a JSON object.')
+ }
+ mock.headers = parsedHeaders
+ }
+
+ if (row.queryParamsText.trim() !== '') {
+ const parsedQueryParams = JSON.parse(row.queryParamsText)
+ if (!isPlainObject(parsedQueryParams)) {
+ throw new Error('Query params must be a JSON object.')
+ }
+ mock.query_params = parsedQueryParams
+ }
+
+ if (row.requestType.trim() !== '') {
+ mock.request_type = row.requestType
+ }
+
+ if (row.responseType.trim() !== '') {
+ mock.response_type = row.responseType
+ }
+
+ if (row.responseText.trim() !== '') {
+ mock.response = JSON.parse(row.responseText)
+ }
+
+ return mock
+ },
+ applyDatasetMutation(datasetKey, mutator, options = {}) {
+ const dataset = this.datasets[datasetKey]
+ if (!dataset.data) {
+ return
+ }
+
+ const nextData = deepClone(dataset.data)
+ mutator(nextData)
+
+ const rawText = JSON.stringify(nextData, null, 2)
+ this.commitDataset(datasetKey, nextData, {
+ rawText,
+ snapshotText: dataset.snapshotText,
+ sourceLabel: dataset.sourceLabel,
+ fileHandle: dataset.fileHandle,
+ loadError: dataset.loadError,
+ preserveSettingsDrafts: options.preserveSettingsDrafts === true,
+ preserveFunctionMockDrafts:
+ options.preserveFunctionMockDrafts === true
+ })
+ },
+ addFundingSourceSettingRow() {
+ if (!this.currentFundingSourceName) {
+ return
+ }
+
+ const rows = this.ensureSettingsDraft(
+ this.selectedDatasetKey,
+ this.currentFundingSourceName
+ )
+ rows.push(this.createSettingsDraftRow('', ''))
+ },
+ handleFundingSourceSettingTypeChange(row) {
+ if (row.type === 'boolean') {
+ row.valueText =
+ row.valueText === 'true' || row.valueText === 'false'
+ ? row.valueText
+ : 'false'
+ } else if (row.type === 'null') {
+ row.valueText = 'null'
+ } else if (row.type === 'number') {
+ row.valueText = row.valueText.trim() === '' ? '0' : row.valueText
+ } else if (row.type === 'json') {
+ row.valueText =
+ row.valueText.trim() === '' ? '{}' : row.valueText
+ }
+
+ this.persistFundingSourceSettings()
+ },
+ removeFundingSourceSettingRow(rowIndex) {
+ const rows = this.currentFundingSourceSettingRows
+ rows.splice(rowIndex, 1)
+ this.persistFundingSourceSettings()
+ },
+ persistFundingSourceSettings() {
+ if (!this.currentFundingSourceName) {
+ return
+ }
+
+ const rows = this.currentFundingSourceSettingRows
+ const nextSettings = {}
+
+ for (const row of rows) {
+ row.error = ''
+ }
+
+ for (const row of rows) {
+ const key = row.key.trim()
+
+ if (!key) {
+ continue
+ }
+
+ try {
+ nextSettings[key] = this.parseSettingRowValue(row)
+ } catch (error) {
+ row.error =
+ error instanceof Error ? error.message : String(error)
+ this.notify(
+ `Could not save setting "${key || 'new setting'}".`,
+ 'negative',
+ row.error
+ )
+ return
+ }
+ }
+
+ this.applyDatasetMutation(
+ this.selectedDatasetKey,
+ data => {
+ data.funding_sources[this.currentFundingSourceName].settings =
+ nextSettings
+ },
+ {
+ preserveSettingsDrafts: true
+ }
+ )
+ },
+ addFunctionMockRow() {
+ if (!this.currentFunctionName || !this.currentFundingSourceName) {
+ return
+ }
+
+ const rows = this.ensureFunctionMockDraft(
+ this.selectedDatasetKey,
+ this.currentFunctionName,
+ this.currentFundingSourceName
+ )
+ rows.push(this.createFunctionMockDraftRow('', {}))
+ },
+ removeFunctionMockRow(rowIndex) {
+ const rows = this.currentFunctionMockRows
+ rows.splice(rowIndex, 1)
+ this.persistFunctionMocks()
+ },
+ persistFunctionMocks() {
+ if (!this.currentFunctionName || !this.currentFundingSourceName) {
+ return
+ }
+
+ const rows = this.currentFunctionMockRows
+ const nextMocks = {}
+
+ for (const row of rows) {
+ row.error = ''
+ }
+
+ for (const row of rows) {
+ const name = row.name.trim()
+
+ if (!name) {
+ continue
+ }
+
+ try {
+ nextMocks[name] = this.parseFunctionMockRowBody(row)
+ } catch (error) {
+ row.error =
+ error instanceof Error ? error.message : String(error)
+ this.notify(
+ `Could not save mock "${name || 'new mock'}".`,
+ 'negative',
+ row.error
+ )
+ return
+ }
+ }
+
+ this.applyDatasetMutation(
+ this.selectedDatasetKey,
+ data => {
+ if (!data.functions[this.currentFunctionName].mocks) {
+ data.functions[this.currentFunctionName].mocks = {}
+ }
+ data.functions[this.currentFunctionName].mocks[
+ this.currentFundingSourceName
+ ] = nextMocks
+ },
+ {
+ preserveFunctionMockDrafts: true
+ }
+ )
+ },
baseWalletPath(functionName, fundingSourceName) {
return `functions.${functionName}.mocks.${fundingSourceName}`
},
@@ -2040,7 +2871,33 @@
dataset.loadError = options.loadError
}
+ if (options.preserveSettingsDrafts !== true) {
+ this.clearSettingsDrafts(datasetKey)
+ }
+ if (options.preserveFunctionMockDrafts !== true) {
+ this.clearFunctionMockDrafts(datasetKey)
+ }
+
this.ensureSelections(datasetKey)
+
+ if (
+ datasetKey === this.selectedDatasetKey &&
+ this.selectedFundingSourceByDataset[datasetKey]
+ ) {
+ this.ensureSettingsDraft(
+ datasetKey,
+ this.selectedFundingSourceByDataset[datasetKey],
+ options.preserveSettingsDrafts !== true
+ )
+ if (this.selectedFunctionByDataset[datasetKey]) {
+ this.ensureFunctionMockDraft(
+ datasetKey,
+ this.selectedFunctionByDataset[datasetKey],
+ this.selectedFundingSourceByDataset[datasetKey],
+ options.preserveFunctionMockDrafts !== true
+ )
+ }
+ }
},
validateEditor(datasetKey) {
const dataset = this.datasets[datasetKey]
@@ -2087,9 +2944,22 @@
},
resetEditor(datasetKey) {
const dataset = this.datasets[datasetKey]
- dataset.rawText = dataset.snapshotText
- dataset.parseError = ''
- this.notify(`Reset editor for ${dataset.fileName}.`, 'info')
+ const parsed = parseJsonText(dataset.snapshotText)
+
+ if (!parsed.ok) {
+ dataset.parseError = `Invalid JSON in ${dataset.fileName}: ${parsed.error}`
+ this.notify(`Could not reset ${dataset.fileName}.`, 'negative')
+ return
+ }
+
+ this.commitDataset(datasetKey, parsed.data, {
+ rawText: dataset.snapshotText,
+ snapshotText: dataset.snapshotText,
+ fileHandle: dataset.fileHandle,
+ sourceLabel: dataset.sourceLabel,
+ loadError: dataset.loadError
+ })
+ this.notify(`Reset ${dataset.fileName} to the last saved snapshot.`, 'info')
},
async loadRelative(datasetKey) {
const dataset = this.datasets[datasetKey]