From 633a0d2d2dafe89319d898941bbcf0ecb9a16e56 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 26 Mar 2026 13:29:15 +0200 Subject: [PATCH] feat: better UI --- tests/wallets/index.html | 1182 ++++++++++++++++++-------------------- 1 file changed, 573 insertions(+), 609 deletions(-) diff --git a/tests/wallets/index.html b/tests/wallets/index.html index 2a5e76a1d..e929ae526 100644 --- a/tests/wallets/index.html +++ b/tests/wallets/index.html @@ -176,7 +176,7 @@ .workspace-grid { display: grid; gap: 18px; - grid-template-columns: 320px minmax(0, 1fr) 430px; + grid-template-columns: 320px minmax(0, 1fr); align-items: start; } @@ -248,6 +248,51 @@ word-break: break-word; } + .native-select-wrap { + position: relative; + } + + .native-select-wrap::after { + content: '▾'; + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--studio-muted); + pointer-events: none; + font-size: 0.95rem; + } + + .native-select-label { + display: block; + margin-bottom: 8px; + color: var(--studio-muted); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.12em; + font-weight: 700; + } + + .native-select { + width: 100%; + appearance: none; + -webkit-appearance: none; + border: 1px solid rgba(25, 54, 61, 0.14); + border-radius: 18px; + background: rgba(255, 255, 255, 0.92); + color: var(--studio-ink); + padding: 14px 42px 14px 14px; + font: inherit; + font-size: 0.95rem; + line-height: 1.35; + box-shadow: inset 0 1px 1px rgba(20, 30, 36, 0.04); + } + + .native-select:focus { + outline: 2px solid rgba(25, 54, 61, 0.18); + border-color: rgba(25, 54, 61, 0.22); + } + .pill-row { display: flex; gap: 8px; @@ -405,6 +450,14 @@ overflow: hidden; } + .test-expander > summary { + list-style: none; + } + + .test-expander > summary::-webkit-details-marker { + display: none; + } + .wallet-expanded-header { padding: 16px 18px; display: flex; @@ -414,6 +467,47 @@ background: rgba(25, 54, 61, 0.05); } + .test-summary { + cursor: pointer; + user-select: none; + } + + .test-summary:hover { + background: rgba(25, 54, 61, 0.08); + } + + .test-summary-copy { + display: flex; + gap: 12px; + min-width: 0; + align-items: flex-start; + } + + .test-summary-arrow { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 999px; + background: rgba(25, 54, 61, 0.08); + color: var(--studio-muted); + font-size: 1rem; + flex: 0 0 auto; + } + + .test-summary-arrow::before { + content: '▸'; + } + + .test-expander[open] .test-summary-arrow { + background: rgba(25, 54, 61, 0.14); + } + + .test-expander[open] .test-summary-arrow::before { + content: '▾'; + } + .wallet-expanded-title { margin: 0; font-size: 1rem; @@ -551,11 +645,6 @@ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace; } - .editor-card { - position: sticky; - top: 20px; - } - .editor-toolbar { display: flex; gap: 10px; @@ -573,7 +662,7 @@ .fixture-editor { width: 100%; - min-height: 72vh; + min-height: 68vh; resize: vertical; padding: 16px; border-radius: 20px; @@ -609,12 +698,6 @@ .workspace-grid { grid-template-columns: 300px minmax(0, 1fr); } - - .editor-card { - position: static; - top: auto; - grid-column: 1 / -1; - } } @media (max-width: 1080px) { @@ -811,271 +894,205 @@ size="sm" color="accent" outline - label="Reset Editor" - @click="resetEditor(currentDataset.key)" + label="Open JSON Editor" + @click="editorDialog = true" > - - - -
-
-

Function Explorer

-
- Select a wallet interface method, then inspect its raw test cases. -
-
-
- - - -
-
- -
-
- -
- No functions match the current search. -
-
-
- - - -
-
-

Raw Test Cases

-
- These are the entries under - functions.{{ currentFunctionName || '...' }}.tests. -
-
-
- -
-
- -
-
- -
- No raw tests match the current search for this function. -
-
-
- - - -
-
-

Funding Sources

-
- Wallet classes and settings configured in the current fixture file. -
-
-
- -
-
-
-
{{ wallet.name }}
-
- {{ wallet.wallet_class }} -
-
- - {{ Object.keys(wallet.settings || {}).length }} settings - - - Skipped by wallet config - -
-
-
-
-
-
- +
-

Function-Level Mock Templates

+

Funding Source

- These are the reusable mock definitions under - functions.{{ currentFunction.name }}.mocks. + Pick one wallet implementation, then inspect the settings and mocks that apply to it.
+
+ + +
+ +
+ +
+
+ + {{ currentFundingSource.wallet_class }} + + + funding_sources.{{ currentFundingSource.name }} + + + Skipped by wallet config + +
+ +
+ Settings to configure for the selected funding source. +
+ +
+
+
{{ setting.key }}
+
{{ prettyValue(setting.value) }}
+
+
+ +
+ No settings are defined for this funding source. +
+
+
+
+ + + +
+
+

Function

+
+ Choose the wallet interface method to inspect for the selected funding source. +
+
+
+ + +
+ +
+ +
{{ currentFunction.name }} + + {{ currentFundingSource.name }} + + + {{ currentFunctionBaseMockPath }} +
-
+
+
+

Function Mocks

+
+ Reusable mocks for this function and the selected funding source. +
+
+
+ +
-
+
-

{{ entry.fundingSource.name }}

-
- {{ entry.fundingSource.wallet_class }} -
+
{{ mock.name }}
+
{{ mockSummary(mock) }}
- - {{ entry.mockNames.length }} mocks + + {{ mock.response_type || mock.request_type || 'mock' }}
-
-
-
{{ mock.name }}
-
{{ mockSummary(mock) }}
-
-
- -
- No base mock templates for this wallet in this function. -
+
+ +
+ No function-level mocks are defined for + {{ currentFundingSource.name }} + in + {{ currentFunction.name }}. +
- +
-

Selected Raw Test

+

Tests For Selected Function

- Source path: - {{ selectedRawTestPath }} -
-
- - {{ selectedRawTest.description || `Test ${selectedRawTest.index + 1}` }} - -
- -
- - {{ coverage.fundingSource.name }} · {{ coverage.variantCount }} - -
- -
-
- -
-
- -
-
- -
-
-
-
- - - -
-
-

Generated Wallet Tests

-
- Expanded using the same wallet-test combination rules as - {{ currentDataset.testFile }}. + Each card shows the raw test case for the selected function, plus the mocks attached to the selected funding source.
-
-
-
-

{{ walletEntry.fundingSource.name }}

-
- {{ walletEntry.fundingSource.wallet_class }} + +
+ +
+

+ {{ testEntry.description || `Test ${testEntry.index + 1}` }} +

+
+ functions.{{ currentFunction.name }}.tests[{{ testEntry.index }}] +
@@ -1083,232 +1100,204 @@ - {{ walletEntry.status.label }} + {{ testEntry.status.label }} + + + {{ currentFundingSource.name }} - {{ walletEntry.variants.length }} variants + {{ testEntry.variantCount }} generated variants
-
+
-
- - Base path: {{ walletEntry.basePath }} - - - Test path: {{ walletEntry.testPath }} - +
+ {{ testEntry.note }}
-
- {{ walletEntry.note }} +
+
+ +
+
+ +
+
+ +
-
-
-
-
-

{{ variantDisplayLabel(variant) }}

-
- {{ buildTestId(variant) }} -
-
-
- - Skipped - - - {{ variant.expect_error ? 'expect_error' : 'expect' }} - -
+
+
+

Test Mocks

+
+ Mock entries under the selected funding source for this test.
+
+
-
- - {{ mock.name }} +
+
+
+
+
{{ mockSet.label }}
+
{{ mockSet.path }}
+
+ + {{ Object.keys(mockSet.value || {}).length }} mock names
-
-
-
-
-
{{ mock.name }}
-
{{ mockSummary(mock) }}
-
- - {{ mock.response_type || 'n/a' }} - -
- -
-
-
Base Mock Path
-
- {{ baseMockPath(currentFunctionName, walletEntry.fundingSource.name, mock.name) }} -
-
-
-
Test Mock Path
-
- {{ testMockPath(currentFunctionName, selectedRawTest.index, walletEntry.fundingSource.name, mock.name, mock._meta.testMockIndex) }} -
-
-
- - -
-
- -
- This variant does not generate any combined mocks. -
+
+ +
+ No wallet-specific mock entry is defined for + {{ currentFundingSource.name }} + in this test. +
-
+ +
+ +
+ No tests are defined for this function.
- - + + + +
+

Raw JSON Editor

+
+ Edit the current fixture file directly, then apply, save, or download the changes. +
+
+ + +
+ + + + +
+
+ + {{ editorDirty ? 'Unsaved editor changes' : 'Editor synced' }} + + + {{ currentDataset.parseError ? 'JSON error' : 'Valid JSON' }} + +
+ +
+ + + + + +
+
+ + + {{ currentDataset.parseError }} + + + +
+
+
+ { datasets[def.key] = createDatasetState(def) selectedFunctionByDataset[def.key] = '' - selectedTestIndexByDataset[def.key] = 0 + selectedFundingSourceByDataset[def.key] = '' }) return { datasetDefs: DATASET_DEFS, datasets, + editorDialog: false, selectedDatasetKey: 'rest', selectedFunctionByDataset, - selectedTestIndexByDataset, - search: '' + selectedFundingSourceByDataset } }, computed: { @@ -1750,6 +1735,41 @@ currentFundingSources() { return this.currentDataset.analysis?.fundingSources || [] }, + fundingSourceOptions() { + return this.currentFundingSources.map(fs => ({ + label: `${fs.name} · ${fs.wallet_class}`, + value: fs.name + })) + }, + currentFundingSourceName() { + return this.selectedFundingSourceByDataset[this.selectedDatasetKey] || '' + }, + currentFundingSource() { + return ( + this.currentFundingSources.find( + fs => fs.name === this.currentFundingSourceName + ) || null + ) + }, + currentFundingSourceSettings() { + if (!this.currentFundingSource?.settings) { + return [] + } + + return Object.entries(this.currentFundingSource.settings).map( + ([key, value]) => ({ + key, + value + }) + ) + }, + functionOptions() { + const functions = this.currentDataset.analysis?.functions || [] + return functions.map(fn => ({ + label: `${fn.name} · ${fn.tests.length} tests`, + value: fn.name + })) + }, currentFunctionName() { return this.selectedFunctionByDataset[this.selectedDatasetKey] || '' }, @@ -1760,68 +1780,6 @@ ) || null ) }, - currentTestIndex() { - return this.selectedTestIndexByDataset[this.selectedDatasetKey] || 0 - }, - selectedRawTest() { - if (!this.currentFunction) { - return null - } - - return ( - this.currentFunction.tests.find( - test => test.index === this.currentTestIndex - ) || this.currentFunction.tests[0] || null - ) - }, - filteredFunctions() { - const functions = this.currentDataset.analysis?.functions || [] - const query = this.search.toLowerCase().trim() - - if (!query) { - return functions - } - - return functions.filter(fn => { - const walletNames = fn.baseMockWallets - .map(entry => entry.fundingSource.name) - .join(' ') - const testDescriptions = fn.tests - .map(test => test.description) - .join(' ') - - return `${fn.name} ${walletNames} ${testDescriptions}` - .toLowerCase() - .includes(query) - }) - }, - filteredTests() { - if (!this.currentFunction) { - return [] - } - - const query = this.search.toLowerCase().trim() - - if (!query) { - return this.currentFunction.tests - } - - return this.currentFunction.tests.filter(test => { - const walletNames = test.walletCoverage - .map(entry => entry.fundingSource.name) - .join(' ') - return `${test.description} ${walletNames}` - .toLowerCase() - .includes(query) - }) - }, - selectedRawTestPath() { - if (!this.currentFunction || !this.selectedRawTest) { - return '' - } - - return `functions.${this.currentFunction.name}.tests[${this.selectedRawTest.index}]` - }, currentDatasetKindHelp() { if (this.selectedDatasetKey === 'rest') { return 'HTTP funding sources with request and response mocks consumed by pytest_httpserver.' @@ -1832,74 +1790,121 @@ editorDirty() { return this.currentDataset.rawText !== this.currentDataset.snapshotText }, - expandedWalletEntries() { - if (!this.currentFunction || !this.selectedRawTest) { + 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 '' + } + + return this.baseWalletPath( + this.currentFunction.name, + this.currentFundingSource.name + ) + }, + currentFunctionTestsForFundingSource() { + if (!this.currentFunction || !this.currentFundingSource) { return [] } - return this.currentFundingSources.map(fs => { - const variants = testsForFundingSource( - fs, - this.currentFunction.name, - this.currentFunction.raw, - this.selectedRawTest.raw, - this.selectedRawTest.index + return this.currentFunction.tests.map(test => { + const rawHasMocksBlock = Object.prototype.hasOwnProperty.call( + test.raw || {}, + 'mocks' ) - const hasMocksEntry = Boolean( - this.selectedRawTest.raw?.mocks && + const hasMockEntry = Boolean( + rawHasMocksBlock && Object.prototype.hasOwnProperty.call( - this.selectedRawTest.raw.mocks, - fs.name + test.raw.mocks || {}, + this.currentFundingSource.name ) ) - const basePath = this.baseWalletPath(this.currentFunction.name, fs.name) - const testPath = hasMocksEntry + const rawWalletMockValue = hasMockEntry + ? test.raw.mocks[this.currentFundingSource.name] + : undefined + const rawWalletPath = hasMockEntry ? this.testWalletPath( this.currentFunction.name, - this.selectedRawTest.index, - fs.name + test.index, + this.currentFundingSource.name ) : '' + const rawMockSets = rawWalletMockValue === undefined + ? [] + : (Array.isArray(rawWalletMockValue) + ? rawWalletMockValue + : [rawWalletMockValue] + ).map((mockSet, index) => ({ + label: Array.isArray(rawWalletMockValue) + ? `Mock set ${index + 1}` + : 'Mock set', + path: Array.isArray(rawWalletMockValue) + ? `${rawWalletPath}[${index}]` + : rawWalletPath, + value: deepClone(mockSet) + })) + const variants = testsForFundingSource( + this.currentFundingSource, + this.currentFunction.name, + this.currentFunction.raw, + test.raw, + test.index + ) - let note = '' let status = { label: 'Ready', color: 'positive' } + let note = '' - if (fs.skip) { + if (this.currentFundingSource.skip) { status = { - label: 'Skipped wallet', + label: 'Wallet skipped', color: 'warning' } - note = 'This funding source is marked with `skip: true` in the fixture file.' - } else if (!hasMocksEntry && this.selectedRawTest.raw?.mocks) { + note = 'The selected funding source is marked with `skip: true` in the fixture configuration.' + } else if (rawHasMocksBlock && !hasMockEntry) { status = { - label: 'Missing mock entry', + label: 'Missing wallet mocks', color: 'negative' } - note = 'This raw test omits this funding source under its `mocks` section, so the generated wallet test is skipped.' - } else if (!variants.length) { + note = 'This test has a `mocks` block, but it does not define one for the selected funding source.' + } else if (rawHasMocksBlock && hasMockEntry && !rawMockSets.length) { status = { - label: 'No variants', + label: 'Empty wallet mocks', color: 'grey-7' } - note = 'This wallet has a mock entry, but it expands to zero generated variants.' - } else if (variants.every(variant => variant.skip)) { + note = 'This test defines the selected funding source, but the mock set is empty.' + } else if (!rawHasMocksBlock) { status = { - label: 'All skipped', - color: 'warning' + label: 'No test-level mocks', + color: 'grey-7' } + note = 'This test case does not include a `mocks` section.' } return { - fundingSource: fs, + ...test, + rawWalletPath, + rawMockSets, variants, - hasMocksEntry, - basePath, - testPath, + variantCount: variants.length, + status, note, - status + expectedBlock: test.expect ?? test.expect_error, + expectedLabel: test.expect ? 'Expect' : 'Expect Error' } }) } @@ -1954,62 +1959,48 @@ return pieces.join(' · ') || 'No mock metadata' }, - cleanMockForDisplay(mock) { - const clone = deepClone(mock) - delete clone._meta - return clone - }, - coverageChipColor(coverage) { - if (coverage.fundingSource.skip) { - return 'warning' + prettyValue(value) { + if (typeof value === 'string') { + return value } - if (!coverage.hasMocksEntry) { - return 'negative' - } - if (!coverage.variantCount) { - return 'grey-7' - } - if (!coverage.readyCount) { - return 'warning' - } - return 'positive' - }, - variantDisplayLabel(variant) { - const raw = variant._meta?.rawDescription || variant.description || '' - const extra = variant._meta?.extraDescription - - if (extra) { - return `${raw} / ${extra}` + if ( + typeof value === 'number' || + typeof value === 'boolean' || + value === null + ) { + return String(value) } - return raw || variant.description || 'Generated variant' + return safeStringify(value) }, - buildTestId, baseWalletPath(functionName, fundingSourceName) { return `functions.${functionName}.mocks.${fundingSourceName}` }, testWalletPath(functionName, testIndex, fundingSourceName) { return `functions.${functionName}.tests[${testIndex}].mocks.${fundingSourceName}` }, - baseMockPath(functionName, fundingSourceName, mockName) { - return `functions.${functionName}.mocks.${fundingSourceName}.${mockName}` - }, - testMockPath( - functionName, - testIndex, - fundingSourceName, - mockName, - mockIndex - ) { - return `functions.${functionName}.tests[${testIndex}].mocks.${fundingSourceName}.${mockName}[${mockIndex}]` - }, ensureSelections(datasetKey) { const dataset = this.datasets[datasetKey] + const fundingSources = dataset.analysis?.fundingSources || [] const functions = dataset.analysis?.functions || [] + if (!fundingSources.length) { + this.selectedFundingSourceByDataset[datasetKey] = '' + } else { + const currentFundingSourceName = + this.selectedFundingSourceByDataset[datasetKey] + const selectedFundingSource = fundingSources.find( + fs => fs.name === currentFundingSourceName + ) + + if (!selectedFundingSource) { + this.selectedFundingSourceByDataset[datasetKey] = + fundingSources[0].name + } + } + if (!functions.length) { this.selectedFunctionByDataset[datasetKey] = '' - this.selectedTestIndexByDataset[datasetKey] = 0 return } @@ -2021,33 +2012,6 @@ if (!selectedFunction) { this.selectedFunctionByDataset[datasetKey] = functions[0].name } - - const nextFunction = functions.find( - fn => fn.name === this.selectedFunctionByDataset[datasetKey] - ) - const tests = nextFunction?.tests || [] - const currentIndex = this.selectedTestIndexByDataset[datasetKey] - - if (!tests.length) { - this.selectedTestIndexByDataset[datasetKey] = 0 - return - } - - const matchingTest = tests.find(test => test.index === currentIndex) - if (!matchingTest) { - this.selectedTestIndexByDataset[datasetKey] = tests[0].index - } - }, - selectFunction(functionName) { - this.selectedFunctionByDataset[this.selectedDatasetKey] = functionName - const fn = this.currentDataset.analysis?.functions.find( - item => item.name === functionName - ) - this.selectedTestIndexByDataset[this.selectedDatasetKey] = - fn?.tests[0]?.index || 0 - }, - selectTest(testIndex) { - this.selectedTestIndexByDataset[this.selectedDatasetKey] = testIndex }, commitDataset(datasetKey, parsedData, options = {}) { const dataset = this.datasets[datasetKey]