2408 lines
75 KiB
HTML
2408 lines
75 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1, maximum-scale=1"
|
|
/>
|
|
<title>Wallet Fixture Studio</title>
|
|
<link rel="stylesheet" href="../../lnbits/static/vendor/quasar.css" />
|
|
<style>
|
|
@font-face {
|
|
font-family: 'Material Icons';
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
src: url('../../lnbits/static/fonts/material-icons-v50.woff2')
|
|
format('woff2');
|
|
}
|
|
|
|
:root {
|
|
--q-primary: #19363d;
|
|
--q-secondary: #b86a28;
|
|
--q-accent: #287675;
|
|
--q-positive: #2c7a54;
|
|
--q-negative: #b6402c;
|
|
--q-warning: #c48a12;
|
|
--q-info: #4c6fa5;
|
|
--studio-bg: #f5efe7;
|
|
--studio-surface: rgba(255, 252, 247, 0.88);
|
|
--studio-surface-strong: rgba(255, 255, 255, 0.96);
|
|
--studio-border: rgba(25, 54, 61, 0.12);
|
|
--studio-border-strong: rgba(25, 54, 61, 0.18);
|
|
--studio-ink: #162028;
|
|
--studio-muted: #5d6a71;
|
|
--studio-shadow: 0 18px 54px rgba(26, 40, 46, 0.08);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
[v-cloak] {
|
|
display: none;
|
|
}
|
|
|
|
html,
|
|
body,
|
|
#q-app {
|
|
min-height: 100%;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
color: var(--studio-ink);
|
|
font-family: 'Avenir Next', 'Segoe UI', 'Helvetica Neue', sans-serif;
|
|
background:
|
|
radial-gradient(
|
|
circle at top left,
|
|
rgba(184, 106, 40, 0.16),
|
|
transparent 28%
|
|
),
|
|
radial-gradient(
|
|
circle at top right,
|
|
rgba(40, 118, 117, 0.13),
|
|
transparent 24%
|
|
),
|
|
linear-gradient(180deg, #fbf7f1 0%, var(--studio-bg) 100%);
|
|
}
|
|
|
|
a {
|
|
color: inherit;
|
|
}
|
|
|
|
.app-shell {
|
|
max-width: 1680px;
|
|
margin: 0 auto;
|
|
padding: 24px;
|
|
}
|
|
|
|
.topbar {
|
|
margin-bottom: 18px;
|
|
padding: 22px;
|
|
border: 1px solid var(--studio-border);
|
|
border-radius: 28px;
|
|
background:
|
|
linear-gradient(
|
|
135deg,
|
|
rgba(255, 255, 255, 0.98) 0%,
|
|
rgba(248, 242, 233, 0.92) 48%,
|
|
rgba(239, 247, 247, 0.9) 100%
|
|
);
|
|
box-shadow: var(--studio-shadow);
|
|
}
|
|
|
|
.hero-row {
|
|
display: flex;
|
|
gap: 18px;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.hero-copy {
|
|
min-width: 0;
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.hero-eyebrow {
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
font-size: 0.74rem;
|
|
font-weight: 700;
|
|
color: var(--studio-muted);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.hero-title {
|
|
margin: 0;
|
|
font-size: clamp(1.7rem, 2.2vw, 2.75rem);
|
|
line-height: 1.05;
|
|
font-weight: 800;
|
|
letter-spacing: -0.035em;
|
|
}
|
|
|
|
.hero-copy p {
|
|
margin: 10px 0 0;
|
|
max-width: 900px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.98rem;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.hero-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.dataset-tabs {
|
|
margin-top: 20px;
|
|
border-radius: 18px;
|
|
background: rgba(22, 32, 40, 0.04);
|
|
padding: 6px;
|
|
}
|
|
|
|
.stats-row {
|
|
margin-top: 18px;
|
|
display: grid;
|
|
gap: 14px;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
|
|
.stat-card {
|
|
padding: 16px 18px;
|
|
border-radius: 22px;
|
|
border: 1px solid var(--studio-border);
|
|
background: var(--studio-surface);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.stat-label {
|
|
margin-bottom: 4px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.8rem;
|
|
line-height: 1;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.workspace-grid {
|
|
display: grid;
|
|
gap: 18px;
|
|
grid-template-columns: 320px minmax(0, 1fr) 430px;
|
|
align-items: start;
|
|
}
|
|
|
|
.stack {
|
|
display: grid;
|
|
gap: 18px;
|
|
}
|
|
|
|
.studio-card {
|
|
border-radius: 26px;
|
|
border: 1px solid var(--studio-border);
|
|
background: var(--studio-surface);
|
|
box-shadow: var(--studio-shadow);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.studio-card .q-card__section--vert {
|
|
padding: 18px 20px;
|
|
}
|
|
|
|
.section-title {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.section-title h2,
|
|
.section-title h3 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
line-height: 1.2;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.section-caption {
|
|
margin-top: 4px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.88rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.summary-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.summary-item {
|
|
padding: 12px 14px;
|
|
border-radius: 18px;
|
|
background: rgba(25, 54, 61, 0.04);
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
}
|
|
|
|
.summary-item-label {
|
|
color: var(--studio-muted);
|
|
font-size: 0.74rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.summary-item-value {
|
|
margin-top: 4px;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
font-size: 0.86rem;
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.pill-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.function-list,
|
|
.test-list,
|
|
.wallet-list {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.function-item,
|
|
.test-item,
|
|
.wallet-item {
|
|
border-radius: 18px;
|
|
border: 1px solid transparent;
|
|
background: rgba(255, 255, 255, 0.62);
|
|
transition:
|
|
transform 0.18s ease,
|
|
border-color 0.18s ease,
|
|
box-shadow 0.18s ease;
|
|
}
|
|
|
|
.function-item:hover,
|
|
.test-item:hover,
|
|
.wallet-item:hover {
|
|
transform: translateY(-1px);
|
|
border-color: var(--studio-border-strong);
|
|
box-shadow: 0 8px 26px rgba(26, 40, 46, 0.06);
|
|
}
|
|
|
|
.function-item.active,
|
|
.test-item.active {
|
|
border-color: rgba(25, 54, 61, 0.25);
|
|
background: rgba(25, 54, 61, 0.08);
|
|
}
|
|
|
|
.list-button {
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
text-align: left;
|
|
padding: 14px 16px;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
}
|
|
|
|
.list-button-title {
|
|
font-weight: 800;
|
|
font-size: 0.96rem;
|
|
}
|
|
|
|
.list-button-subtitle {
|
|
margin-top: 6px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.84rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.function-matrix {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
|
}
|
|
|
|
.matrix-card {
|
|
border-radius: 20px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(255, 255, 255, 0.7);
|
|
padding: 14px;
|
|
}
|
|
|
|
.matrix-card-header {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.matrix-card-title {
|
|
margin: 0;
|
|
font-size: 0.96rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.matrix-card-meta {
|
|
color: var(--studio-muted);
|
|
font-size: 0.82rem;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.matrix-list {
|
|
margin-top: 12px;
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.matrix-list-row {
|
|
padding: 10px 12px;
|
|
border-radius: 16px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(25, 54, 61, 0.04);
|
|
}
|
|
|
|
.matrix-list-title {
|
|
font-weight: 700;
|
|
font-size: 0.86rem;
|
|
}
|
|
|
|
.matrix-list-caption {
|
|
margin-top: 4px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.test-detail-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
}
|
|
|
|
.detail-panel {
|
|
padding: 14px;
|
|
border-radius: 18px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(255, 255, 255, 0.72);
|
|
}
|
|
|
|
.detail-label {
|
|
color: var(--studio-muted);
|
|
font-size: 0.74rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.11em;
|
|
font-weight: 700;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.wallet-expanded-list {
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
|
|
.wallet-expanded-card {
|
|
border-radius: 22px;
|
|
border: 1px solid rgba(25, 54, 61, 0.1);
|
|
background:
|
|
linear-gradient(
|
|
180deg,
|
|
rgba(255, 255, 255, 0.88) 0%,
|
|
rgba(252, 248, 242, 0.78) 100%
|
|
);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.wallet-expanded-header {
|
|
padding: 16px 18px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
background: rgba(25, 54, 61, 0.05);
|
|
}
|
|
|
|
.wallet-expanded-title {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.wallet-expanded-meta {
|
|
margin-top: 4px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.86rem;
|
|
}
|
|
|
|
.wallet-expanded-body {
|
|
padding: 16px 18px 18px;
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
|
|
.variant-card {
|
|
padding: 14px;
|
|
border-radius: 18px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(255, 255, 255, 0.76);
|
|
}
|
|
|
|
.variant-header {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.variant-title {
|
|
margin: 0;
|
|
font-size: 0.94rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.variant-subtitle {
|
|
margin-top: 5px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.82rem;
|
|
line-height: 1.45;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.mock-list {
|
|
margin-top: 12px;
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.mock-card {
|
|
border-radius: 16px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(25, 54, 61, 0.03);
|
|
padding: 12px;
|
|
}
|
|
|
|
.mock-card-header {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.mock-card-title {
|
|
font-size: 0.88rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.mock-card-subtitle {
|
|
margin-top: 4px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.8rem;
|
|
line-height: 1.45;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.mock-source-grid {
|
|
margin-top: 10px;
|
|
display: grid;
|
|
gap: 10px;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.source-chip {
|
|
padding: 10px;
|
|
border-radius: 14px;
|
|
background: rgba(255, 255, 255, 0.76);
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
}
|
|
|
|
.source-chip-label {
|
|
color: var(--studio-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.source-chip-value {
|
|
margin-top: 5px;
|
|
font-size: 0.79rem;
|
|
line-height: 1.45;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.json-panel {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
|
|
.json-label {
|
|
color: var(--studio-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.json-pre {
|
|
margin: 0;
|
|
padding: 12px;
|
|
border-radius: 16px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: #fffdf8;
|
|
color: #1c2931;
|
|
font-size: 0.8rem;
|
|
line-height: 1.55;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
}
|
|
|
|
.editor-card {
|
|
position: sticky;
|
|
top: 20px;
|
|
}
|
|
|
|
.editor-toolbar {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.editor-status {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.fixture-editor {
|
|
width: 100%;
|
|
min-height: 72vh;
|
|
resize: vertical;
|
|
padding: 16px;
|
|
border-radius: 20px;
|
|
border: 1px solid rgba(25, 54, 61, 0.12);
|
|
background: #fffdf9;
|
|
color: #172127;
|
|
font-size: 0.84rem;
|
|
line-height: 1.55;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
box-shadow: inset 0 1px 2px rgba(18, 24, 28, 0.04);
|
|
}
|
|
|
|
.fixture-editor:focus {
|
|
outline: 2px solid rgba(25, 54, 61, 0.22);
|
|
border-color: rgba(25, 54, 61, 0.2);
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 18px;
|
|
border-radius: 18px;
|
|
border: 1px dashed rgba(25, 54, 61, 0.18);
|
|
background: rgba(255, 255, 255, 0.5);
|
|
color: var(--studio-muted);
|
|
font-size: 0.92rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.hidden-input {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 1320px) {
|
|
.workspace-grid {
|
|
grid-template-columns: 300px minmax(0, 1fr);
|
|
}
|
|
|
|
.editor-card {
|
|
position: static;
|
|
top: auto;
|
|
grid-column: 1 / -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1080px) {
|
|
.hero-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.hero-actions {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.stats-row {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.workspace-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.test-detail-grid,
|
|
.mock-source-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.app-shell {
|
|
padding: 14px;
|
|
}
|
|
|
|
.topbar,
|
|
.studio-card .q-card__section--vert {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
}
|
|
|
|
.stats-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="q-app" v-cloak>
|
|
<div class="app-shell">
|
|
<header class="topbar">
|
|
<div class="hero-row">
|
|
<div class="hero-copy">
|
|
<div class="hero-eyebrow">Wallet Fixture Studio</div>
|
|
<h1 class="hero-title">Fixture editor and test matrix for wallet funding sources</h1>
|
|
<p>
|
|
Browse and edit
|
|
<code>fixtures_rest.json</code>
|
|
and
|
|
<code>fixtures_rpc.json</code>
|
|
with the same expansion logic the wallet tests use. The explorer shows which
|
|
funding source, test case, and merged mocks produce each generated wallet test.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="hero-actions">
|
|
<q-btn
|
|
color="primary"
|
|
unelevated
|
|
label="Try Relative Load"
|
|
@click="loadAllRelative"
|
|
></q-btn>
|
|
<q-btn
|
|
color="secondary"
|
|
outline
|
|
label="Pick Fixtures Folder"
|
|
@click="pickFixturesDirectory"
|
|
></q-btn>
|
|
<q-btn
|
|
color="accent"
|
|
outline
|
|
label="Load Both Files"
|
|
@click="openBulkInput"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<q-tabs
|
|
v-model="selectedDatasetKey"
|
|
dense
|
|
active-color="white"
|
|
indicator-color="transparent"
|
|
align="left"
|
|
class="dataset-tabs text-primary"
|
|
>
|
|
<q-tab
|
|
v-for="datasetDef in datasetDefs"
|
|
:key="datasetDef.key"
|
|
:name="datasetDef.key"
|
|
>
|
|
<div class="text-left">
|
|
<div class="text-weight-bold">{{ datasetDef.label }}</div>
|
|
<div class="text-caption">
|
|
{{ datasets[datasetDef.key].analysis ? datasets[datasetDef.key].analysis.summary.expandedTests : 0 }}
|
|
generated wallet tests
|
|
</div>
|
|
</div>
|
|
</q-tab>
|
|
</q-tabs>
|
|
|
|
<div class="stats-row" v-if="currentDataset.analysis">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Funding Sources</div>
|
|
<div class="stat-value">{{ currentDataset.analysis.summary.fundingSources }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Functions</div>
|
|
<div class="stat-value">{{ currentDataset.analysis.summary.functions }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Raw Test Cases</div>
|
|
<div class="stat-value">{{ currentDataset.analysis.summary.rawTests }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Expanded Wallet Tests</div>
|
|
<div class="stat-value">{{ currentDataset.analysis.summary.expandedTests }}</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="workspace-grid">
|
|
<section class="stack">
|
|
<q-card flat class="studio-card">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Current Dataset</h2>
|
|
<div class="section-caption">
|
|
{{ currentDatasetKindHelp }}
|
|
</div>
|
|
</div>
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="currentDataset.analysis ? 'positive' : 'grey-6'"
|
|
text-color="white"
|
|
>
|
|
{{ currentDataset.analysis ? 'Loaded' : 'Waiting' }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="summary-grid">
|
|
<div class="summary-item">
|
|
<div class="summary-item-label">Fixture File</div>
|
|
<div class="summary-item-value">{{ currentDataset.fileName }}</div>
|
|
</div>
|
|
<div class="summary-item">
|
|
<div class="summary-item-label">Fixture Path</div>
|
|
<div class="summary-item-value">{{ currentDataset.relativePath }}</div>
|
|
</div>
|
|
<div class="summary-item">
|
|
<div class="summary-item-label">Pytest File</div>
|
|
<div class="summary-item-value">{{ currentDataset.testFile }}</div>
|
|
</div>
|
|
<div class="summary-item">
|
|
<div class="summary-item-label">Loaded From</div>
|
|
<div class="summary-item-value">
|
|
{{ currentDataset.sourceLabel || 'Not loaded yet' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<q-banner
|
|
v-if="currentDataset.loadError"
|
|
dense
|
|
rounded
|
|
inline-actions
|
|
class="bg-orange-1 text-orange-10 q-mt-md"
|
|
>
|
|
{{ currentDataset.loadError }}
|
|
</q-banner>
|
|
|
|
<div class="pill-row q-mt-md">
|
|
<q-btn
|
|
size="sm"
|
|
color="primary"
|
|
outline
|
|
label="Open Current File"
|
|
@click="openCurrentFile"
|
|
></q-btn>
|
|
<q-btn
|
|
size="sm"
|
|
color="secondary"
|
|
outline
|
|
label="Reload Relative"
|
|
@click="loadRelative(currentDataset.key)"
|
|
></q-btn>
|
|
<q-btn
|
|
size="sm"
|
|
color="accent"
|
|
outline
|
|
label="Reset Editor"
|
|
@click="resetEditor(currentDataset.key)"
|
|
></q-btn>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card flat class="studio-card">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Function Explorer</h2>
|
|
<div class="section-caption">
|
|
Select a wallet interface method, then inspect its raw test cases.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<q-input
|
|
v-model.trim="search"
|
|
dense
|
|
outlined
|
|
clearable
|
|
placeholder="Search functions, tests, or funding sources"
|
|
></q-input>
|
|
|
|
<div class="function-list q-mt-md">
|
|
<div
|
|
v-for="fn in filteredFunctions"
|
|
:key="fn.name"
|
|
class="function-item"
|
|
:class="{ active: fn.name === currentFunctionName }"
|
|
>
|
|
<button class="list-button" @click="selectFunction(fn.name)">
|
|
<div class="list-button-title">{{ fn.name }}</div>
|
|
<div class="list-button-subtitle">
|
|
{{ fn.tests.length }} raw tests · {{ fn.expandedCount }} generated wallet tests
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!filteredFunctions.length" class="empty-state q-mt-md">
|
|
No functions match the current search.
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card flat class="studio-card">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Raw Test Cases</h2>
|
|
<div class="section-caption">
|
|
These are the entries under
|
|
<code>functions.{{ currentFunctionName || '...' }}.tests</code>.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-list">
|
|
<div
|
|
v-for="test in filteredTests"
|
|
:key="`${currentFunctionName}-${test.index}`"
|
|
class="test-item"
|
|
:class="{ active: test.index === currentTestIndex }"
|
|
>
|
|
<button class="list-button" @click="selectTest(test.index)">
|
|
<div class="list-button-title">
|
|
{{ test.description || `Test ${test.index + 1}` }}
|
|
</div>
|
|
<div class="list-button-subtitle">
|
|
{{ test.readyWalletCount }} wallets with generated variants ·
|
|
{{ test.generatedVariants }} generated tests
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!filteredTests.length" class="empty-state q-mt-md">
|
|
No raw tests match the current search for this function.
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card flat class="studio-card">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Funding Sources</h2>
|
|
<div class="section-caption">
|
|
Wallet classes and settings configured in the current fixture file.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wallet-list">
|
|
<div
|
|
v-for="wallet in currentFundingSources"
|
|
:key="wallet.name"
|
|
class="wallet-item"
|
|
>
|
|
<div class="list-button">
|
|
<div class="list-button-title">{{ wallet.name }}</div>
|
|
<div class="list-button-subtitle">
|
|
{{ wallet.wallet_class }}
|
|
</div>
|
|
<div class="pill-row q-mt-sm">
|
|
<q-chip dense size="sm" square color="primary" text-color="white">
|
|
{{ Object.keys(wallet.settings || {}).length }} settings
|
|
</q-chip>
|
|
<q-chip
|
|
v-if="wallet.skip"
|
|
dense
|
|
size="sm"
|
|
square
|
|
color="warning"
|
|
text-color="white"
|
|
>
|
|
Skipped by wallet config
|
|
</q-chip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</section>
|
|
|
|
<section class="stack">
|
|
<q-card flat class="studio-card" v-if="currentFunction">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Function-Level Mock Templates</h2>
|
|
<div class="section-caption">
|
|
These are the reusable mock definitions under
|
|
<code>functions.{{ currentFunction.name }}.mocks</code>.
|
|
</div>
|
|
</div>
|
|
<q-chip dense square color="primary" text-color="white">
|
|
{{ currentFunction.name }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="function-matrix">
|
|
<div
|
|
v-for="entry in currentFunction.baseMockWallets"
|
|
:key="`${currentFunction.name}-${entry.fundingSource.name}`"
|
|
class="matrix-card"
|
|
>
|
|
<div class="matrix-card-header">
|
|
<div>
|
|
<h3 class="matrix-card-title">{{ entry.fundingSource.name }}</h3>
|
|
<div class="matrix-card-meta">
|
|
{{ entry.fundingSource.wallet_class }}
|
|
</div>
|
|
</div>
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="entry.mockNames.length ? 'primary' : 'grey-6'"
|
|
text-color="white"
|
|
>
|
|
{{ entry.mockNames.length }} mocks
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="matrix-list" v-if="entry.mockEntries.length">
|
|
<div
|
|
v-for="mock in entry.mockEntries"
|
|
:key="`${entry.fundingSource.name}-${mock.name}`"
|
|
class="matrix-list-row"
|
|
>
|
|
<div class="matrix-list-title">{{ mock.name }}</div>
|
|
<div class="matrix-list-caption">{{ mockSummary(mock) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state q-mt-md">
|
|
No base mock templates for this wallet in this function.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card flat class="studio-card" v-if="selectedRawTest">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Selected Raw Test</h2>
|
|
<div class="section-caption">
|
|
Source path:
|
|
<code>{{ selectedRawTestPath }}</code>
|
|
</div>
|
|
</div>
|
|
<q-chip dense square color="secondary" text-color="white">
|
|
{{ selectedRawTest.description || `Test ${selectedRawTest.index + 1}` }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="pill-row q-mb-md">
|
|
<q-chip
|
|
v-for="coverage in selectedRawTest.walletCoverage"
|
|
:key="`${selectedRawTest.index}-${coverage.fundingSource.name}`"
|
|
dense
|
|
square
|
|
:color="coverageChipColor(coverage)"
|
|
text-color="white"
|
|
>
|
|
{{ coverage.fundingSource.name }} · {{ coverage.variantCount }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="test-detail-grid">
|
|
<div class="detail-panel">
|
|
<json-panel
|
|
label="Call Params"
|
|
:value="selectedRawTest.call_params || {}"
|
|
max-height="220px"
|
|
></json-panel>
|
|
</div>
|
|
<div class="detail-panel">
|
|
<json-panel
|
|
label="Expect"
|
|
:value="selectedRawTest.expect"
|
|
max-height="220px"
|
|
></json-panel>
|
|
</div>
|
|
<div class="detail-panel">
|
|
<json-panel
|
|
label="Expect Error"
|
|
:value="selectedRawTest.expect_error"
|
|
empty-label="No expect_error block"
|
|
max-height="220px"
|
|
></json-panel>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card flat class="studio-card" v-if="selectedRawTest">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Generated Wallet Tests</h2>
|
|
<div class="section-caption">
|
|
Expanded using the same wallet-test combination rules as
|
|
<code>{{ currentDataset.testFile }}</code>.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wallet-expanded-list">
|
|
<div
|
|
v-for="walletEntry in expandedWalletEntries"
|
|
:key="`${currentFunctionName}-${selectedRawTest.index}-${walletEntry.fundingSource.name}`"
|
|
class="wallet-expanded-card"
|
|
>
|
|
<div class="wallet-expanded-header">
|
|
<div>
|
|
<h3 class="wallet-expanded-title">{{ walletEntry.fundingSource.name }}</h3>
|
|
<div class="wallet-expanded-meta">
|
|
{{ walletEntry.fundingSource.wallet_class }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pill-row">
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="walletEntry.status.color"
|
|
text-color="white"
|
|
>
|
|
{{ walletEntry.status.label }}
|
|
</q-chip>
|
|
<q-chip dense square color="primary" text-color="white">
|
|
{{ walletEntry.variants.length }} variants
|
|
</q-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wallet-expanded-body">
|
|
<div class="pill-row">
|
|
<q-chip dense square color="grey-8" text-color="white">
|
|
Base path: {{ walletEntry.basePath }}
|
|
</q-chip>
|
|
<q-chip
|
|
v-if="walletEntry.testPath"
|
|
dense
|
|
square
|
|
color="grey-8"
|
|
text-color="white"
|
|
>
|
|
Test path: {{ walletEntry.testPath }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div
|
|
v-if="walletEntry.note"
|
|
class="empty-state"
|
|
>
|
|
{{ walletEntry.note }}
|
|
</div>
|
|
|
|
<div v-if="walletEntry.variants.length">
|
|
<div
|
|
v-for="variant in walletEntry.variants"
|
|
:key="variant._meta.variantKey"
|
|
class="variant-card"
|
|
>
|
|
<div class="variant-header">
|
|
<div>
|
|
<h4 class="variant-title">{{ variantDisplayLabel(variant) }}</h4>
|
|
<div class="variant-subtitle">
|
|
{{ buildTestId(variant) }}
|
|
</div>
|
|
</div>
|
|
<div class="pill-row">
|
|
<q-chip
|
|
v-if="variant.skip"
|
|
dense
|
|
square
|
|
color="warning"
|
|
text-color="white"
|
|
>
|
|
Skipped
|
|
</q-chip>
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="variant.expect_error ? 'negative' : 'positive'"
|
|
text-color="white"
|
|
>
|
|
{{ variant.expect_error ? 'expect_error' : 'expect' }}
|
|
</q-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pill-row q-mt-sm">
|
|
<q-chip
|
|
v-for="mock in variant.mocks"
|
|
:key="`${variant._meta.variantKey}-${mock.name}`"
|
|
dense
|
|
square
|
|
color="secondary"
|
|
text-color="white"
|
|
>
|
|
{{ mock.name }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="mock-list" v-if="variant.mocks.length">
|
|
<div
|
|
v-for="mock in variant.mocks"
|
|
:key="`${variant._meta.variantKey}-mock-${mock.name}`"
|
|
class="mock-card"
|
|
>
|
|
<div class="mock-card-header">
|
|
<div>
|
|
<div class="mock-card-title">{{ mock.name }}</div>
|
|
<div class="mock-card-subtitle">{{ mockSummary(mock) }}</div>
|
|
</div>
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="mock.skip ? 'warning' : 'primary'"
|
|
text-color="white"
|
|
>
|
|
{{ mock.response_type || 'n/a' }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="mock-source-grid">
|
|
<div class="source-chip">
|
|
<div class="source-chip-label">Base Mock Path</div>
|
|
<div class="source-chip-value">
|
|
{{ baseMockPath(currentFunctionName, walletEntry.fundingSource.name, mock.name) }}
|
|
</div>
|
|
</div>
|
|
<div class="source-chip" v-if="mock._meta">
|
|
<div class="source-chip-label">Test Mock Path</div>
|
|
<div class="source-chip-value">
|
|
{{ testMockPath(currentFunctionName, selectedRawTest.index, walletEntry.fundingSource.name, mock.name, mock._meta.testMockIndex) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<json-panel
|
|
class="q-mt-md"
|
|
label="Merged Mock"
|
|
:value="cleanMockForDisplay(mock)"
|
|
max-height="360px"
|
|
></json-panel>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state q-mt-md">
|
|
This variant does not generate any combined mocks.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</section>
|
|
|
|
<aside class="stack">
|
|
<q-card flat class="studio-card editor-card">
|
|
<q-card-section>
|
|
<div class="section-title">
|
|
<div>
|
|
<h2>Raw JSON Editor</h2>
|
|
<div class="section-caption">
|
|
Edit the current fixture file directly, then apply or save the changes.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="editor-toolbar">
|
|
<div class="editor-status">
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="editorDirty ? 'warning' : 'positive'"
|
|
text-color="white"
|
|
>
|
|
{{ editorDirty ? 'Unsaved editor changes' : 'Editor synced' }}
|
|
</q-chip>
|
|
<q-chip
|
|
dense
|
|
square
|
|
:color="currentDataset.parseError ? 'negative' : 'primary'"
|
|
text-color="white"
|
|
>
|
|
{{ currentDataset.parseError ? 'JSON error' : 'Valid JSON' }}
|
|
</q-chip>
|
|
</div>
|
|
|
|
<div class="pill-row">
|
|
<q-btn
|
|
size="sm"
|
|
color="primary"
|
|
unelevated
|
|
label="Apply"
|
|
@click="applyEditor(currentDataset.key)"
|
|
></q-btn>
|
|
<q-btn
|
|
size="sm"
|
|
color="secondary"
|
|
outline
|
|
label="Format"
|
|
@click="formatEditor(currentDataset.key)"
|
|
></q-btn>
|
|
<q-btn
|
|
size="sm"
|
|
color="accent"
|
|
outline
|
|
label="Save"
|
|
@click="saveDataset(currentDataset.key)"
|
|
></q-btn>
|
|
<q-btn
|
|
size="sm"
|
|
color="grey-8"
|
|
outline
|
|
label="Download"
|
|
@click="downloadDataset(currentDataset.key)"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<q-banner
|
|
v-if="currentDataset.parseError"
|
|
dense
|
|
rounded
|
|
inline-actions
|
|
class="bg-red-1 text-red-10 q-mb-md"
|
|
>
|
|
{{ currentDataset.parseError }}
|
|
</q-banner>
|
|
|
|
<textarea
|
|
v-model="datasets[selectedDatasetKey].rawText"
|
|
class="fixture-editor"
|
|
spellcheck="false"
|
|
autocapitalize="off"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
></textarea>
|
|
</q-card-section>
|
|
</q-card>
|
|
</aside>
|
|
</main>
|
|
</div>
|
|
|
|
<input
|
|
ref="bulkInput"
|
|
class="hidden-input"
|
|
type="file"
|
|
accept=".json,application/json"
|
|
multiple
|
|
@change="handleBulkFileSelection"
|
|
/>
|
|
<input
|
|
ref="restInput"
|
|
class="hidden-input"
|
|
type="file"
|
|
accept=".json,application/json"
|
|
@change="handleSingleFileSelection('rest', $event)"
|
|
/>
|
|
<input
|
|
ref="rpcInput"
|
|
class="hidden-input"
|
|
type="file"
|
|
accept=".json,application/json"
|
|
@change="handleSingleFileSelection('rpc', $event)"
|
|
/>
|
|
</div>
|
|
|
|
<script src="../../lnbits/static/vendor/vue.global.prod.js"></script>
|
|
<script src="../../lnbits/static/vendor/quasar.umd.prod.js"></script>
|
|
<script>
|
|
const DATASET_DEFS = [
|
|
{
|
|
key: 'rest',
|
|
label: 'REST Fixtures',
|
|
relativePath: './fixtures/json/fixtures_rest.json',
|
|
fileName: 'fixtures_rest.json',
|
|
testFile: 'tests/wallets/test_rest_wallets.py'
|
|
},
|
|
{
|
|
key: 'rpc',
|
|
label: 'RPC Fixtures',
|
|
relativePath: './fixtures/json/fixtures_rpc.json',
|
|
fileName: 'fixtures_rpc.json',
|
|
testFile: 'tests/wallets/test_rpc_wallets.py'
|
|
}
|
|
]
|
|
|
|
const JsonPanel = {
|
|
name: 'JsonPanel',
|
|
props: {
|
|
label: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
value: {
|
|
type: null,
|
|
default: undefined
|
|
},
|
|
emptyLabel: {
|
|
type: String,
|
|
default: 'No data'
|
|
},
|
|
maxHeight: {
|
|
type: String,
|
|
default: '260px'
|
|
}
|
|
},
|
|
computed: {
|
|
formatted() {
|
|
if (this.value === undefined) {
|
|
return this.emptyLabel
|
|
}
|
|
return safeStringify(this.value)
|
|
}
|
|
},
|
|
template: `
|
|
<div class="json-panel">
|
|
<div v-if="label" class="json-label">{{ label }}</div>
|
|
<pre class="json-pre" :style="{ maxHeight }">{{ formatted }}</pre>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return Object.prototype.toString.call(value) === '[object Object]'
|
|
}
|
|
|
|
function deepClone(value) {
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
return JSON.parse(JSON.stringify(value))
|
|
}
|
|
|
|
function safeStringify(value) {
|
|
if (value === undefined) {
|
|
return 'undefined'
|
|
}
|
|
return JSON.stringify(value, null, 2)
|
|
}
|
|
|
|
function parseJsonText(text) {
|
|
try {
|
|
return {
|
|
ok: true,
|
|
data: JSON.parse(text)
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildTestId(test) {
|
|
return `${test.funding_source.name}.${test.function}(${test.description})`
|
|
}
|
|
|
|
function combineMocks(mockName, fsMock, testMock, testMockIndex) {
|
|
const merged = {
|
|
...deepClone(fsMock),
|
|
...deepClone(testMock),
|
|
name: mockName
|
|
}
|
|
|
|
if (isPlainObject(merged.response) && isPlainObject(fsMock?.response)) {
|
|
merged.response = {
|
|
...deepClone(merged.response),
|
|
...deepClone(fsMock.response)
|
|
}
|
|
}
|
|
|
|
merged._meta = {
|
|
testMockIndex
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
function buildMockObjects(mockNames, fsMocks, testMocks) {
|
|
const mocks = []
|
|
|
|
for (const mockName of mockNames) {
|
|
if (!Object.prototype.hasOwnProperty.call(testMocks || {}, mockName)) {
|
|
continue
|
|
}
|
|
|
|
const values = Array.isArray(testMocks[mockName])
|
|
? testMocks[mockName]
|
|
: [testMocks[mockName]]
|
|
|
|
values.forEach((testMock, testMockIndex) => {
|
|
const mock = {
|
|
fsMock: fsMocks[mockName],
|
|
testMock,
|
|
testMockIndex
|
|
}
|
|
|
|
if (mockNames.length === 1) {
|
|
mocks.push({ [mockName]: mock })
|
|
} else {
|
|
const subMocks = buildMockObjects(
|
|
mockNames.slice(1),
|
|
fsMocks,
|
|
testMocks
|
|
)
|
|
|
|
subMocks.forEach(subMock => {
|
|
mocks.push({
|
|
[mockName]: mock,
|
|
...subMock
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
return mocks
|
|
}
|
|
|
|
return mocks
|
|
}
|
|
|
|
function testsFromMock(baseTest, mockObject) {
|
|
const mocks = Object.keys(mockObject).map(mockName => {
|
|
const value = mockObject[mockName]
|
|
return combineMocks(
|
|
mockName,
|
|
value.fsMock,
|
|
value.testMock,
|
|
value.testMockIndex
|
|
)
|
|
})
|
|
|
|
const extraDescription = mocks
|
|
.filter(mock => mock.description)
|
|
.map(mock => mock.description)
|
|
.join(';')
|
|
|
|
const description = `${baseTest.description}:${extraDescription}`
|
|
const skip = Boolean(baseTest.skip || mocks.some(mock => mock.skip))
|
|
|
|
return {
|
|
...deepClone(baseTest),
|
|
description,
|
|
skip,
|
|
mocks,
|
|
_meta: {
|
|
...deepClone(baseTest._meta),
|
|
extraDescription,
|
|
variantKey: `${baseTest.funding_source.name}-${baseTest.function}-${baseTest._meta.testIndex}-${description}`
|
|
}
|
|
}
|
|
}
|
|
|
|
function testsFromFsMocks(baseTest, fn, test, fsName) {
|
|
const fsMocks = fn?.mocks?.[fsName] || {}
|
|
const testMocks = test?.mocks?.[fsName]
|
|
const allTestMocks = Array.isArray(testMocks) ? testMocks : [testMocks]
|
|
|
|
let mockSets = []
|
|
|
|
allTestMocks.forEach(testMockSet => {
|
|
if (!testMockSet) {
|
|
return
|
|
}
|
|
|
|
mockSets = mockSets.concat(
|
|
buildMockObjects(Object.keys(fsMocks), fsMocks, testMockSet)
|
|
)
|
|
})
|
|
|
|
return mockSets.map(mockSet => testsFromMock(baseTest, mockSet))
|
|
}
|
|
|
|
function testsForFundingSource(fs, fnName, fn, test, testIndex) {
|
|
const baseTest = {
|
|
skip: Boolean(fs.skip),
|
|
function: fnName,
|
|
description: test.description || '',
|
|
funding_source: deepClone(fs),
|
|
call_params: deepClone(test.call_params || {}),
|
|
expect: deepClone(test.expect),
|
|
expect_error: deepClone(test.expect_error),
|
|
mocks: [],
|
|
_meta: {
|
|
testIndex,
|
|
rawDescription: test.description || ''
|
|
}
|
|
}
|
|
|
|
if (test && Object.prototype.hasOwnProperty.call(test, 'mocks')) {
|
|
if (!Object.prototype.hasOwnProperty.call(test.mocks || {}, fs.name)) {
|
|
return [
|
|
{
|
|
...baseTest,
|
|
skip: true,
|
|
_meta: {
|
|
...baseTest._meta,
|
|
variantKey: `${fs.name}-${fnName}-${testIndex}-missing`,
|
|
skipReason: 'This raw test has no mock entry for this funding source.'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
return testsFromFsMocks(baseTest, fn, test, fs.name)
|
|
}
|
|
|
|
return [
|
|
{
|
|
...baseTest,
|
|
_meta: {
|
|
...baseTest._meta,
|
|
variantKey: `${fs.name}-${fnName}-${testIndex}-plain`
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
function walletFixturesFromJson(data) {
|
|
const fundingSources = Object.entries(data?.funding_sources || {}).map(
|
|
([name, config]) => ({
|
|
name,
|
|
...deepClone(config)
|
|
})
|
|
)
|
|
|
|
const tests = []
|
|
|
|
Object.entries(data?.functions || {}).forEach(([fnName, fn]) => {
|
|
;(fn.tests || []).forEach((test, testIndex) => {
|
|
fundingSources.forEach(fs => {
|
|
tests.push(...testsForFundingSource(fs, fnName, fn, test, testIndex))
|
|
})
|
|
})
|
|
})
|
|
|
|
return tests
|
|
}
|
|
|
|
function createCoverageEntry(fs, fnName, fn, test, testIndex) {
|
|
const hasMocksEntry = Boolean(
|
|
test?.mocks && Object.prototype.hasOwnProperty.call(test.mocks, fs.name)
|
|
)
|
|
const variants = testsForFundingSource(fs, fnName, fn, test, testIndex)
|
|
const testMockNames = hasMocksEntry
|
|
? Object.keys(test.mocks?.[fs.name] || {})
|
|
: []
|
|
|
|
return {
|
|
fundingSource: fs,
|
|
hasMocksEntry,
|
|
variantCount: variants.length,
|
|
readyCount: variants.filter(variant => !variant.skip).length,
|
|
skippedCount: variants.filter(variant => variant.skip).length,
|
|
testMockNames
|
|
}
|
|
}
|
|
|
|
function analyzeFixtureData(data) {
|
|
const fundingSources = Object.entries(data?.funding_sources || {}).map(
|
|
([name, config]) => ({
|
|
name,
|
|
...deepClone(config)
|
|
})
|
|
)
|
|
|
|
const functions = Object.entries(data?.functions || {}).map(
|
|
([fnName, fn]) => {
|
|
const tests = (fn.tests || []).map((test, testIndex) => {
|
|
const walletCoverage = fundingSources.map(fs =>
|
|
createCoverageEntry(fs, fnName, fn, test, testIndex)
|
|
)
|
|
|
|
return {
|
|
index: testIndex,
|
|
description: test.description || '',
|
|
call_params: deepClone(test.call_params || {}),
|
|
expect: deepClone(test.expect),
|
|
expect_error: deepClone(test.expect_error),
|
|
raw: deepClone(test),
|
|
walletCoverage,
|
|
generatedVariants: walletCoverage.reduce(
|
|
(sum, item) => sum + item.variantCount,
|
|
0
|
|
),
|
|
readyWalletCount: walletCoverage.filter(
|
|
item => item.readyCount > 0
|
|
).length
|
|
}
|
|
})
|
|
|
|
const baseMockWallets = fundingSources.map(fs => {
|
|
const mocks = deepClone(fn?.mocks?.[fs.name] || {})
|
|
const mockEntries = Object.entries(mocks).map(([name, mock]) => ({
|
|
name,
|
|
...mock
|
|
}))
|
|
|
|
return {
|
|
fundingSource: fs,
|
|
mocks,
|
|
mockEntries,
|
|
mockNames: mockEntries.map(item => item.name)
|
|
}
|
|
})
|
|
|
|
return {
|
|
name: fnName,
|
|
raw: deepClone(fn),
|
|
tests,
|
|
baseMockWallets,
|
|
expandedCount: tests.reduce(
|
|
(sum, item) => sum + item.generatedVariants,
|
|
0
|
|
)
|
|
}
|
|
}
|
|
)
|
|
|
|
const expandedTests = walletFixturesFromJson(data)
|
|
|
|
return {
|
|
fundingSources,
|
|
functions,
|
|
expandedTests,
|
|
summary: {
|
|
fundingSources: fundingSources.length,
|
|
functions: functions.length,
|
|
rawTests: functions.reduce((sum, fn) => sum + fn.tests.length, 0),
|
|
expandedTests: expandedTests.length,
|
|
skippedExpandedTests: expandedTests.filter(test => test.skip).length
|
|
}
|
|
}
|
|
}
|
|
|
|
function createDatasetState(def) {
|
|
return {
|
|
...def,
|
|
rawText: '',
|
|
snapshotText: '',
|
|
sourceLabel: '',
|
|
parseError: '',
|
|
loadError: '',
|
|
fileHandle: null,
|
|
data: null,
|
|
analysis: null
|
|
}
|
|
}
|
|
|
|
const { createApp } = Vue
|
|
|
|
createApp({
|
|
components: {
|
|
JsonPanel
|
|
},
|
|
data() {
|
|
const datasets = {}
|
|
const selectedFunctionByDataset = {}
|
|
const selectedTestIndexByDataset = {}
|
|
|
|
DATASET_DEFS.forEach(def => {
|
|
datasets[def.key] = createDatasetState(def)
|
|
selectedFunctionByDataset[def.key] = ''
|
|
selectedTestIndexByDataset[def.key] = 0
|
|
})
|
|
|
|
return {
|
|
datasetDefs: DATASET_DEFS,
|
|
datasets,
|
|
selectedDatasetKey: 'rest',
|
|
selectedFunctionByDataset,
|
|
selectedTestIndexByDataset,
|
|
search: ''
|
|
}
|
|
},
|
|
computed: {
|
|
currentDataset() {
|
|
return this.datasets[this.selectedDatasetKey]
|
|
},
|
|
currentFundingSources() {
|
|
return this.currentDataset.analysis?.fundingSources || []
|
|
},
|
|
currentFunctionName() {
|
|
return this.selectedFunctionByDataset[this.selectedDatasetKey] || ''
|
|
},
|
|
currentFunction() {
|
|
return (
|
|
this.currentDataset.analysis?.functions.find(
|
|
fn => fn.name === this.currentFunctionName
|
|
) || 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.'
|
|
}
|
|
|
|
return 'RPC funding sources with patched Python methods, nested return data, and spy assertions.'
|
|
},
|
|
editorDirty() {
|
|
return this.currentDataset.rawText !== this.currentDataset.snapshotText
|
|
},
|
|
expandedWalletEntries() {
|
|
if (!this.currentFunction || !this.selectedRawTest) {
|
|
return []
|
|
}
|
|
|
|
return this.currentFundingSources.map(fs => {
|
|
const variants = testsForFundingSource(
|
|
fs,
|
|
this.currentFunction.name,
|
|
this.currentFunction.raw,
|
|
this.selectedRawTest.raw,
|
|
this.selectedRawTest.index
|
|
)
|
|
const hasMocksEntry = Boolean(
|
|
this.selectedRawTest.raw?.mocks &&
|
|
Object.prototype.hasOwnProperty.call(
|
|
this.selectedRawTest.raw.mocks,
|
|
fs.name
|
|
)
|
|
)
|
|
const basePath = this.baseWalletPath(this.currentFunction.name, fs.name)
|
|
const testPath = hasMocksEntry
|
|
? this.testWalletPath(
|
|
this.currentFunction.name,
|
|
this.selectedRawTest.index,
|
|
fs.name
|
|
)
|
|
: ''
|
|
|
|
let note = ''
|
|
let status = {
|
|
label: 'Ready',
|
|
color: 'positive'
|
|
}
|
|
|
|
if (fs.skip) {
|
|
status = {
|
|
label: 'Skipped wallet',
|
|
color: 'warning'
|
|
}
|
|
note = 'This funding source is marked with `skip: true` in the fixture file.'
|
|
} else if (!hasMocksEntry && this.selectedRawTest.raw?.mocks) {
|
|
status = {
|
|
label: 'Missing mock entry',
|
|
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) {
|
|
status = {
|
|
label: 'No variants',
|
|
color: 'grey-7'
|
|
}
|
|
note = 'This wallet has a mock entry, but it expands to zero generated variants.'
|
|
} else if (variants.every(variant => variant.skip)) {
|
|
status = {
|
|
label: 'All skipped',
|
|
color: 'warning'
|
|
}
|
|
}
|
|
|
|
return {
|
|
fundingSource: fs,
|
|
variants,
|
|
hasMocksEntry,
|
|
basePath,
|
|
testPath,
|
|
note,
|
|
status
|
|
}
|
|
})
|
|
}
|
|
},
|
|
watch: {
|
|
selectedDatasetKey() {
|
|
this.ensureSelections(this.selectedDatasetKey)
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadAllRelative()
|
|
|
|
window.addEventListener('keydown', this.handleGlobalKeydown)
|
|
},
|
|
beforeUnmount() {
|
|
window.removeEventListener('keydown', this.handleGlobalKeydown)
|
|
},
|
|
methods: {
|
|
notify(message, type = 'info', caption = '') {
|
|
Quasar.Notify.create({
|
|
message,
|
|
caption: caption || void 0,
|
|
type,
|
|
timeout: 2500,
|
|
progress: true
|
|
})
|
|
},
|
|
handleGlobalKeydown(event) {
|
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
|
event.preventDefault()
|
|
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'
|
|
},
|
|
cleanMockForDisplay(mock) {
|
|
const clone = deepClone(mock)
|
|
delete clone._meta
|
|
return clone
|
|
},
|
|
coverageChipColor(coverage) {
|
|
if (coverage.fundingSource.skip) {
|
|
return 'warning'
|
|
}
|
|
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}`
|
|
}
|
|
|
|
return raw || variant.description || 'Generated variant'
|
|
},
|
|
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 functions = dataset.analysis?.functions || []
|
|
|
|
if (!functions.length) {
|
|
this.selectedFunctionByDataset[datasetKey] = ''
|
|
this.selectedTestIndexByDataset[datasetKey] = 0
|
|
return
|
|
}
|
|
|
|
const currentFunctionName = this.selectedFunctionByDataset[datasetKey]
|
|
const selectedFunction = functions.find(
|
|
fn => fn.name === currentFunctionName
|
|
)
|
|
|
|
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]
|
|
const rawText =
|
|
options.rawText !== undefined
|
|
? options.rawText
|
|
: JSON.stringify(parsedData, null, 2)
|
|
const snapshotText =
|
|
options.snapshotText !== undefined ? options.snapshotText : rawText
|
|
|
|
dataset.data = parsedData
|
|
dataset.analysis = analyzeFixtureData(parsedData)
|
|
dataset.rawText = rawText
|
|
dataset.snapshotText = snapshotText
|
|
dataset.parseError = ''
|
|
|
|
if (options.fileHandle !== undefined) {
|
|
dataset.fileHandle = options.fileHandle
|
|
}
|
|
|
|
if (options.sourceLabel !== undefined) {
|
|
dataset.sourceLabel = options.sourceLabel
|
|
}
|
|
|
|
if (options.loadError !== undefined) {
|
|
dataset.loadError = options.loadError
|
|
}
|
|
|
|
this.ensureSelections(datasetKey)
|
|
},
|
|
validateEditor(datasetKey) {
|
|
const dataset = this.datasets[datasetKey]
|
|
const parsed = parseJsonText(dataset.rawText)
|
|
|
|
if (!parsed.ok) {
|
|
dataset.parseError = `Invalid JSON in ${dataset.fileName}: ${parsed.error}`
|
|
return null
|
|
}
|
|
|
|
dataset.parseError = ''
|
|
return parsed.data
|
|
},
|
|
applyEditor(datasetKey) {
|
|
const parsedData = this.validateEditor(datasetKey)
|
|
if (!parsedData) {
|
|
this.notify('Could not apply the editor contents.', 'negative')
|
|
return
|
|
}
|
|
|
|
const dataset = this.datasets[datasetKey]
|
|
this.commitDataset(datasetKey, parsedData, {
|
|
rawText: dataset.rawText,
|
|
snapshotText: dataset.rawText,
|
|
sourceLabel: dataset.sourceLabel
|
|
})
|
|
this.notify(`Applied ${dataset.fileName}.`, 'positive')
|
|
},
|
|
formatEditor(datasetKey) {
|
|
const parsedData = this.validateEditor(datasetKey)
|
|
if (!parsedData) {
|
|
this.notify('Could not format invalid JSON.', 'negative')
|
|
return
|
|
}
|
|
|
|
const formatted = JSON.stringify(parsedData, null, 2)
|
|
const dataset = this.datasets[datasetKey]
|
|
this.commitDataset(datasetKey, parsedData, {
|
|
rawText: formatted,
|
|
snapshotText: formatted,
|
|
sourceLabel: dataset.sourceLabel
|
|
})
|
|
this.notify(`Formatted ${dataset.fileName}.`, 'positive')
|
|
},
|
|
resetEditor(datasetKey) {
|
|
const dataset = this.datasets[datasetKey]
|
|
dataset.rawText = dataset.snapshotText
|
|
dataset.parseError = ''
|
|
this.notify(`Reset editor for ${dataset.fileName}.`, 'info')
|
|
},
|
|
async loadRelative(datasetKey) {
|
|
const dataset = this.datasets[datasetKey]
|
|
|
|
try {
|
|
const response = await fetch(dataset.relativePath, {
|
|
cache: 'no-store'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`)
|
|
}
|
|
|
|
const text = await response.text()
|
|
const parsed = parseJsonText(text)
|
|
|
|
if (!parsed.ok) {
|
|
throw new Error(parsed.error)
|
|
}
|
|
|
|
const formatted = JSON.stringify(parsed.data, null, 2)
|
|
this.commitDataset(datasetKey, parsed.data, {
|
|
rawText: formatted,
|
|
snapshotText: formatted,
|
|
sourceLabel: `Relative load: ${dataset.relativePath}`,
|
|
fileHandle: null,
|
|
loadError: ''
|
|
})
|
|
} catch (error) {
|
|
dataset.loadError =
|
|
`Automatic relative loading failed for ${dataset.fileName}. ` +
|
|
`If you opened this page with file://, use “Load Both Files” or “Pick Fixtures Folder”.`
|
|
|
|
if (!dataset.analysis) {
|
|
dataset.parseError = ''
|
|
}
|
|
|
|
console.error(error)
|
|
}
|
|
},
|
|
async loadAllRelative() {
|
|
await Promise.all(this.datasetDefs.map(def => this.loadRelative(def.key)))
|
|
},
|
|
openBulkInput() {
|
|
this.$refs.bulkInput.value = ''
|
|
this.$refs.bulkInput.click()
|
|
},
|
|
async openCurrentFile() {
|
|
const datasetKey = this.currentDataset.key
|
|
|
|
if (window.showOpenFilePicker) {
|
|
try {
|
|
const [handle] = await window.showOpenFilePicker({
|
|
multiple: false,
|
|
types: [
|
|
{
|
|
description: 'JSON fixture file',
|
|
accept: {
|
|
'application/json': ['.json']
|
|
}
|
|
}
|
|
]
|
|
})
|
|
|
|
if (!handle) {
|
|
return
|
|
}
|
|
|
|
const file = await handle.getFile()
|
|
await this.loadDatasetFromFile(datasetKey, file, {
|
|
fileHandle: handle,
|
|
sourceLabel: `File picker: ${file.name}`
|
|
})
|
|
return
|
|
} catch (error) {
|
|
if (error && error.name !== 'AbortError') {
|
|
console.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
const refName = `${datasetKey}Input`
|
|
this.$refs[refName].value = ''
|
|
this.$refs[refName].click()
|
|
},
|
|
async handleBulkFileSelection(event) {
|
|
const files = Array.from(event.target.files || [])
|
|
const byName = Object.fromEntries(files.map(file => [file.name, file]))
|
|
|
|
for (const def of this.datasetDefs) {
|
|
if (byName[def.fileName]) {
|
|
await this.loadDatasetFromFile(def.key, byName[def.fileName], {
|
|
sourceLabel: `Manual file load: ${byName[def.fileName].name}`
|
|
})
|
|
}
|
|
}
|
|
|
|
const missing = this.datasetDefs
|
|
.filter(def => !byName[def.fileName])
|
|
.map(def => def.fileName)
|
|
|
|
if (missing.length) {
|
|
this.notify(
|
|
`Missing file selection for ${missing.join(', ')}.`,
|
|
'warning'
|
|
)
|
|
}
|
|
},
|
|
async handleSingleFileSelection(datasetKey, event) {
|
|
const file = event.target.files?.[0]
|
|
if (!file) {
|
|
return
|
|
}
|
|
|
|
await this.loadDatasetFromFile(datasetKey, file, {
|
|
sourceLabel: `Manual file load: ${file.name}`
|
|
})
|
|
},
|
|
async loadDatasetFromFile(datasetKey, file, options = {}) {
|
|
const text = await file.text()
|
|
const parsed = parseJsonText(text)
|
|
|
|
if (!parsed.ok) {
|
|
this.datasets[datasetKey].parseError = `Invalid JSON in ${file.name}: ${parsed.error}`
|
|
this.notify(`Could not read ${file.name}.`, 'negative')
|
|
return
|
|
}
|
|
|
|
const formatted = JSON.stringify(parsed.data, null, 2)
|
|
this.commitDataset(datasetKey, parsed.data, {
|
|
rawText: formatted,
|
|
snapshotText: formatted,
|
|
fileHandle: options.fileHandle,
|
|
sourceLabel: options.sourceLabel || `Loaded from ${file.name}`,
|
|
loadError: ''
|
|
})
|
|
this.notify(`Loaded ${file.name}.`, 'positive')
|
|
},
|
|
async pickFixturesDirectory() {
|
|
if (!window.showDirectoryPicker) {
|
|
this.notify(
|
|
'Directory picker is not available in this browser. Use “Load Both Files” instead.',
|
|
'warning'
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
const directoryHandle = await window.showDirectoryPicker({
|
|
mode: 'readwrite'
|
|
})
|
|
|
|
for (const def of this.datasetDefs) {
|
|
try {
|
|
const fileHandle = await directoryHandle.getFileHandle(def.fileName)
|
|
const file = await fileHandle.getFile()
|
|
await this.loadDatasetFromFile(def.key, file, {
|
|
fileHandle,
|
|
sourceLabel: `Directory handle: ${def.fileName}`
|
|
})
|
|
} catch (error) {
|
|
console.error(error)
|
|
this.datasets[def.key].loadError =
|
|
`Could not find ${def.fileName} in the selected folder. Choose tests/wallets/fixtures/json.`
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error && error.name !== 'AbortError') {
|
|
console.error(error)
|
|
this.notify('Could not open the selected folder.', 'negative')
|
|
}
|
|
}
|
|
},
|
|
async saveDataset(datasetKey) {
|
|
const parsedData = this.validateEditor(datasetKey)
|
|
if (!parsedData) {
|
|
this.notify('Fix the JSON error before saving.', 'negative')
|
|
return
|
|
}
|
|
|
|
const dataset = this.datasets[datasetKey]
|
|
const contents = dataset.rawText
|
|
|
|
this.commitDataset(datasetKey, parsedData, {
|
|
rawText: contents,
|
|
snapshotText: contents,
|
|
fileHandle: dataset.fileHandle,
|
|
sourceLabel: dataset.sourceLabel
|
|
})
|
|
|
|
if (dataset.fileHandle) {
|
|
try {
|
|
const writable = await dataset.fileHandle.createWritable()
|
|
await writable.write(contents)
|
|
await writable.close()
|
|
this.notify(`Saved ${dataset.fileName} in place.`, 'positive')
|
|
return
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
if (window.showSaveFilePicker) {
|
|
try {
|
|
const handle = await window.showSaveFilePicker({
|
|
suggestedName: dataset.fileName,
|
|
types: [
|
|
{
|
|
description: 'JSON fixture file',
|
|
accept: {
|
|
'application/json': ['.json']
|
|
}
|
|
}
|
|
]
|
|
})
|
|
|
|
const writable = await handle.createWritable()
|
|
await writable.write(contents)
|
|
await writable.close()
|
|
|
|
dataset.fileHandle = handle
|
|
dataset.sourceLabel = `Save picker: ${dataset.fileName}`
|
|
this.notify(`Saved ${dataset.fileName}.`, 'positive')
|
|
return
|
|
} catch (error) {
|
|
if (error && error.name === 'AbortError') {
|
|
return
|
|
}
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
this.downloadDataset(datasetKey)
|
|
},
|
|
downloadDataset(datasetKey) {
|
|
const parsedData = this.validateEditor(datasetKey)
|
|
if (!parsedData) {
|
|
this.notify('Fix the JSON error before downloading.', 'negative')
|
|
return
|
|
}
|
|
|
|
const dataset = this.datasets[datasetKey]
|
|
const contents = dataset.rawText
|
|
|
|
this.commitDataset(datasetKey, parsedData, {
|
|
rawText: contents,
|
|
snapshotText: contents,
|
|
fileHandle: dataset.fileHandle,
|
|
sourceLabel: dataset.sourceLabel
|
|
})
|
|
|
|
const status = Quasar.exportFile(
|
|
dataset.fileName,
|
|
contents,
|
|
'application/json'
|
|
)
|
|
|
|
if (status === true) {
|
|
this.notify(`Downloaded ${dataset.fileName}.`, 'positive')
|
|
} else {
|
|
this.notify(
|
|
`Browser blocked the download for ${dataset.fileName}.`,
|
|
'negative'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.use(Quasar, {
|
|
config: {
|
|
notify: {
|
|
position: 'top-right'
|
|
}
|
|
}
|
|
})
|
|
.mount('#q-app')
|
|
</script>
|
|
</body>
|
|
</html>
|