6397 lines
219 KiB
HTML
6397 lines
219 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;
|
|
}
|
|
|
|
.material-icons,
|
|
.q-icon,
|
|
.q-notification__icon {
|
|
font-family: 'Material Icons' !important;
|
|
font-feature-settings: 'liga';
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
letter-spacing: normal;
|
|
line-height: 1;
|
|
text-transform: none;
|
|
white-space: nowrap;
|
|
word-wrap: normal;
|
|
direction: ltr;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
[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;
|
|
}
|
|
|
|
.stats-row {
|
|
margin-top: 18px;
|
|
display: grid;
|
|
gap: 14px;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
|
|
.stat-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
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;
|
|
}
|
|
|
|
.stat-value-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.stat-card-action {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.workspace-grid {
|
|
display: grid;
|
|
gap: 18px;
|
|
}
|
|
|
|
.primary-row {
|
|
display: grid;
|
|
gap: 18px;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
align-items: stretch;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.picker-card {
|
|
height: 100%;
|
|
}
|
|
|
|
.picker-card--full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.picker-card .q-card__section--vert {
|
|
height: 100%;
|
|
padding: 16px 18px;
|
|
}
|
|
|
|
.picker-card .section-title {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.picker-card .section-caption {
|
|
font-size: 0.84rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.native-select-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.native-select-wrap::after {
|
|
content: 'expand_more';
|
|
position: absolute;
|
|
right: 14px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-family: 'Material Icons';
|
|
font-feature-settings: 'liga';
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
color: var(--studio-muted);
|
|
pointer-events: none;
|
|
font-size: 1.2rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.native-select-label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
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: 10px;
|
|
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);
|
|
}
|
|
|
|
.fixture-select .q-field__control {
|
|
min-height: 46px;
|
|
border-radius: 10px;
|
|
background: rgba(255, 255, 255, 0.92);
|
|
}
|
|
|
|
.fixture-select.q-field--outlined .q-field__control::before {
|
|
border-color: rgba(25, 54, 61, 0.14);
|
|
}
|
|
|
|
.fixture-select.q-field--outlined .q-field__control:hover::before {
|
|
border-color: rgba(25, 54, 61, 0.22);
|
|
}
|
|
|
|
.fixture-select .q-field__native,
|
|
.fixture-select .q-field__input,
|
|
.fixture-select .q-field__marginal {
|
|
color: var(--studio-ink);
|
|
}
|
|
|
|
.fixture-select .q-field__append {
|
|
color: var(--studio-muted);
|
|
}
|
|
|
|
.fixture-select-icon {
|
|
font-family: 'Material Icons';
|
|
font-feature-settings: 'liga';
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
font-size: 1.2rem;
|
|
line-height: 1;
|
|
color: var(--studio-muted);
|
|
user-select: none;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.picker-action-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.picker-select {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.editor-dialog-card {
|
|
width: calc(100vw - 32px);
|
|
max-width: calc(100vw - 32px);
|
|
border-radius: 24px;
|
|
}
|
|
|
|
.dialog-header {
|
|
position: relative;
|
|
padding-right: 96px;
|
|
}
|
|
|
|
.dialog-close-btn {
|
|
position: absolute;
|
|
top: 16px;
|
|
right: 16px;
|
|
}
|
|
|
|
.native-input,
|
|
.native-textarea {
|
|
width: 100%;
|
|
border: 1px solid rgba(25, 54, 61, 0.14);
|
|
border-radius: 10px;
|
|
background: rgba(255, 255, 255, 0.92);
|
|
color: var(--studio-ink);
|
|
padding: 14px;
|
|
font: inherit;
|
|
font-size: 0.95rem;
|
|
line-height: 1.35;
|
|
box-shadow: inset 0 1px 1px rgba(20, 30, 36, 0.04);
|
|
}
|
|
|
|
.native-input {
|
|
min-height: 46px;
|
|
padding: 10px 14px;
|
|
}
|
|
|
|
.native-textarea {
|
|
min-height: 92px;
|
|
padding: 14px;
|
|
resize: vertical;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
font-size: 0.84rem;
|
|
}
|
|
|
|
.native-input:focus,
|
|
.native-textarea:focus {
|
|
outline: 2px solid rgba(25, 54, 61, 0.18);
|
|
border-color: rgba(25, 54, 61, 0.22);
|
|
}
|
|
|
|
.field-label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.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(2, 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);
|
|
}
|
|
|
|
.test-json-editor {
|
|
min-height: 220px;
|
|
}
|
|
|
|
.test-mock-editor {
|
|
min-height: 240px;
|
|
}
|
|
|
|
.test-mock-binding {
|
|
margin-top: 12px;
|
|
padding: 12px;
|
|
border-radius: 16px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
.test-mock-binding:first-of-type {
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.test-mock-binding-header {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.test-mock-binding-body {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.test-mock-item-list {
|
|
margin-top: 12px;
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.test-mock-item-card {
|
|
padding: 12px;
|
|
border-radius: 14px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(25, 54, 61, 0.03);
|
|
}
|
|
|
|
.test-mock-item-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.test-mock-item-top-row {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
align-items: end;
|
|
}
|
|
|
|
.test-mock-item-action-row {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
align-items: end;
|
|
}
|
|
|
|
.test-mock-item-pair-row {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
align-items: end;
|
|
}
|
|
|
|
.test-mock-item-pair-row--top {
|
|
align-items: start;
|
|
}
|
|
|
|
.test-mock-item-skip {
|
|
display: flex;
|
|
align-items: center;
|
|
min-height: 46px;
|
|
}
|
|
|
|
.test-mock-item-columns {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
align-items: start;
|
|
}
|
|
|
|
.test-mock-item-column {
|
|
display: grid;
|
|
gap: 12px;
|
|
align-content: start;
|
|
}
|
|
|
|
.test-mock-item-column + .test-mock-item-column {
|
|
padding-left: 16px;
|
|
border-left: 1px solid rgba(25, 54, 61, 0.12);
|
|
}
|
|
|
|
.test-mock-item-json {
|
|
min-height: 160px;
|
|
}
|
|
|
|
.request-detail-tabs {
|
|
margin-top: 12px;
|
|
border-radius: 8px;
|
|
background: rgba(22, 32, 40, 0.04);
|
|
padding: 4px;
|
|
}
|
|
|
|
.request-detail-tabs .q-tab {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
min-height: 42px;
|
|
border-radius: 4px !important;
|
|
color: var(--q-primary);
|
|
text-align: center;
|
|
}
|
|
|
|
.request-detail-tabs .q-tab__content {
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.request-detail-tabs .q-tab--active {
|
|
background: var(--q-primary);
|
|
color: white;
|
|
}
|
|
|
|
.request-detail-tabs .q-tab--active,
|
|
.request-detail-tabs .q-tab--active .q-tab__label {
|
|
color: white !important;
|
|
}
|
|
|
|
.request-detail-panels {
|
|
margin-top: 10px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, 0.7);
|
|
box-shadow: none;
|
|
}
|
|
|
|
.request-detail-panel {
|
|
padding: 12px;
|
|
}
|
|
|
|
.test-mock-item-full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.settings-editor {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.settings-table {
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
border-radius: 20px;
|
|
background: rgba(255, 255, 255, 0.78);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.settings-table-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns:
|
|
minmax(180px, 1fr) 140px minmax(220px, 1.3fr)
|
|
auto;
|
|
align-items: center;
|
|
}
|
|
|
|
.settings-table-header {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(25, 54, 61, 0.05);
|
|
}
|
|
|
|
.settings-table-heading {
|
|
color: var(--studio-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.settings-table-row {
|
|
padding: 14px;
|
|
border-bottom: 1px solid rgba(25, 54, 61, 0.08);
|
|
}
|
|
|
|
.settings-table-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.settings-table-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.settings-table-note {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.settings-row {
|
|
padding: 14px;
|
|
border-radius: 18px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(255, 255, 255, 0.78);
|
|
}
|
|
|
|
.settings-row-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns:
|
|
minmax(180px, 1fr) 140px minmax(220px, 1.3fr)
|
|
auto;
|
|
align-items: end;
|
|
}
|
|
|
|
.settings-row-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.function-mocks-table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.function-mocks-table {
|
|
min-width: 1320px;
|
|
border: 1px solid rgba(25, 54, 61, 0.08);
|
|
border-radius: 20px;
|
|
background: rgba(255, 255, 255, 0.78);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.function-mocks-table-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.function-mocks-table-grid--rest {
|
|
grid-template-columns:
|
|
minmax(150px, 1fr)
|
|
minmax(240px, 1.3fr)
|
|
minmax(220px, 1.2fr)
|
|
minmax(180px, 1fr);
|
|
}
|
|
|
|
.function-mocks-table-grid--rpc {
|
|
grid-template-columns:
|
|
minmax(150px, 1fr)
|
|
minmax(280px, 1.35fr)
|
|
minmax(320px, 1.65fr);
|
|
}
|
|
|
|
.function-mocks-header {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid rgba(25, 54, 61, 0.08);
|
|
background: rgba(25, 54, 61, 0.05);
|
|
}
|
|
|
|
.function-mocks-heading {
|
|
color: var(--studio-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.function-mocks-row {
|
|
padding: 14px;
|
|
border-bottom: 1px solid rgba(25, 54, 61, 0.08);
|
|
}
|
|
|
|
.function-mocks-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.function-mock-primary-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.function-mock-cell-stack {
|
|
display: grid;
|
|
gap: 12px;
|
|
align-content: start;
|
|
}
|
|
|
|
.function-mock-subfield {
|
|
display: grid;
|
|
gap: 6px;
|
|
align-content: start;
|
|
}
|
|
|
|
.function-mock-cell-stack--rpc {
|
|
grid-auto-rows: min-content;
|
|
align-self: start;
|
|
}
|
|
|
|
.function-mock-json-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.function-mock-json-cell .native-textarea {
|
|
flex: 1 1 auto;
|
|
min-height: 248px;
|
|
}
|
|
|
|
.function-mocks-note {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.settings-hint {
|
|
color: var(--studio-muted);
|
|
font-size: 0.82rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.settings-error {
|
|
color: var(--q-negative);
|
|
font-size: 0.82rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.test-expander > summary {
|
|
list-style: none;
|
|
}
|
|
|
|
.test-expander > summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.wallet-expanded-header {
|
|
padding: 16px 18px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
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: center;
|
|
}
|
|
|
|
.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;
|
|
line-height: 1;
|
|
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;
|
|
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;
|
|
}
|
|
|
|
.function-tabs {
|
|
margin-bottom: 14px;
|
|
border-radius: 8px;
|
|
background: rgba(22, 32, 40, 0.04);
|
|
padding: 4px;
|
|
}
|
|
|
|
.function-tabs .q-tab {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
justify-content: center;
|
|
min-height: 48px;
|
|
border-radius: 4px !important;
|
|
color: var(--q-primary);
|
|
text-align: center;
|
|
}
|
|
|
|
.function-tabs .q-tab__content {
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.function-tabs .q-tab--active {
|
|
background: var(--q-primary);
|
|
color: white;
|
|
}
|
|
|
|
.function-tabs .q-tab--active,
|
|
.function-tabs .q-tab--active .q-tab__label,
|
|
.function-tabs .q-tab--active .text-weight-bold,
|
|
.function-tabs .q-tab--active .text-caption {
|
|
color: white !important;
|
|
}
|
|
|
|
.function-content {
|
|
min-width: 0;
|
|
}
|
|
|
|
.test-type-layout {
|
|
display: grid;
|
|
gap: 18px;
|
|
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
|
align-items: start;
|
|
}
|
|
|
|
.test-type-tabs {
|
|
border-radius: 8px;
|
|
background: rgba(22, 32, 40, 0.04);
|
|
padding: 4px;
|
|
}
|
|
|
|
.test-type-tabs .q-tab {
|
|
justify-content: flex-start;
|
|
min-height: 48px;
|
|
border-radius: 4px !important;
|
|
color: var(--q-primary);
|
|
text-align: left;
|
|
}
|
|
|
|
.test-type-tabs .q-tab__content {
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.test-type-tabs .q-tab--active {
|
|
background: var(--q-primary);
|
|
color: white;
|
|
}
|
|
|
|
.test-type-tabs .q-tab--active,
|
|
.test-type-tabs .q-tab--active .q-tab__label,
|
|
.test-type-tabs .q-tab--active .text-weight-bold {
|
|
color: white !important;
|
|
}
|
|
|
|
.test-type-panels {
|
|
background: transparent;
|
|
box-shadow: none;
|
|
min-width: 0;
|
|
}
|
|
|
|
.test-type-panel {
|
|
padding: 0;
|
|
}
|
|
|
|
.test-case-stack {
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
|
|
.test-case-section {
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
|
|
.test-case-section + .test-case-section {
|
|
padding-top: 18px;
|
|
border-top: 1px solid rgba(25, 54, 61, 0.08);
|
|
}
|
|
|
|
.test-case-header {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.test-case-title {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
line-height: 1.25;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.test-case-subtitle {
|
|
margin-top: 4px;
|
|
color: var(--studio-muted);
|
|
font-size: 0.84rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.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-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: 68vh;
|
|
resize: vertical;
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
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: 1080px) {
|
|
.hero-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.hero-actions {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.stats-row {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.primary-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.test-type-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.mock-source-grid,
|
|
.test-mock-item-action-row,
|
|
.test-mock-item-pair-row,
|
|
.test-detail-grid,
|
|
.test-mock-item-top-row,
|
|
.test-mock-item-grid,
|
|
.test-mock-item-columns,
|
|
.settings-row-grid,
|
|
.settings-table-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.test-mock-item-column + .test-mock-item-column {
|
|
padding-left: 0;
|
|
border-left: 0;
|
|
padding-top: 12px;
|
|
border-top: 1px solid rgba(25, 54, 61, 0.12);
|
|
}
|
|
|
|
.picker-action-row,
|
|
.test-mock-binding-header {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.settings-table-header {
|
|
display: none;
|
|
}
|
|
|
|
.settings-row-actions,
|
|
.settings-table-actions {
|
|
justify-content: flex-start;
|
|
}
|
|
}
|
|
|
|
@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">Test matrix for wallet funding sources</h1>
|
|
<p><code>{{ currentLoadedFileName }}</code></p>
|
|
</div>
|
|
|
|
<div class="hero-actions">
|
|
<q-btn
|
|
color="primary"
|
|
unelevated
|
|
label="Load File"
|
|
@click="openFilePicker"
|
|
></q-btn>
|
|
<q-btn
|
|
color="secondary"
|
|
outline
|
|
label="Reload Current"
|
|
@click="loadRelative(currentDataset.key)"
|
|
></q-btn>
|
|
<q-btn
|
|
color="accent"
|
|
outline
|
|
label="Save"
|
|
@click="saveDataset(currentDataset.key)"
|
|
></q-btn>
|
|
<q-btn
|
|
color="accent"
|
|
outline
|
|
label="Open JSON Editor"
|
|
@click="editorDialog = true"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-row" v-if="currentDataset.analysis">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Funding Sources</div>
|
|
<div class="stat-value-row">
|
|
<div class="stat-value">
|
|
{{ currentDataset.analysis.summary.fundingSources }}
|
|
</div>
|
|
<q-btn
|
|
color="dark"
|
|
unelevated
|
|
label="Settings"
|
|
class="stat-card-action"
|
|
@click="fundingSourceSettingsDialog = true"
|
|
></q-btn>
|
|
</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>
|
|
|
|
<q-banner
|
|
v-if="currentDataset.loadError"
|
|
dense
|
|
rounded
|
|
inline-actions
|
|
class="bg-orange-1 text-orange-10 q-mt-md"
|
|
>
|
|
{{ currentDataset.loadError }}
|
|
</q-banner>
|
|
</header>
|
|
|
|
<main class="workspace-grid">
|
|
<q-card
|
|
flat
|
|
class="studio-card"
|
|
v-if="currentTestFundingSource && currentDataset.analysis"
|
|
>
|
|
<q-card-section>
|
|
<q-tabs
|
|
v-if="functionOptions.length"
|
|
v-model="selectedFunctionByDataset[selectedDatasetKey]"
|
|
dense
|
|
active-color="white"
|
|
indicator-color="transparent"
|
|
align="justify"
|
|
class="function-tabs text-primary"
|
|
>
|
|
<q-tab
|
|
v-for="fn in currentDataset.analysis.functions"
|
|
:key="fn.name"
|
|
:name="fn.name"
|
|
>
|
|
<div class="text-left">
|
|
<div class="text-weight-bold">{{ fn.name }}</div>
|
|
<div class="text-caption">{{ fn.tests.length }} tests</div>
|
|
</div>
|
|
</q-tab>
|
|
</q-tabs>
|
|
|
|
<div v-if="functionOptions.length">
|
|
<div class="function-content">
|
|
<div
|
|
v-if="currentFunctionTestTypeGroups.length"
|
|
class="test-type-layout"
|
|
>
|
|
<q-tabs
|
|
v-model="currentSelectedTestTypeKey"
|
|
vertical
|
|
dense
|
|
active-color="white"
|
|
indicator-color="transparent"
|
|
class="test-type-tabs text-primary"
|
|
>
|
|
<q-tab
|
|
v-for="group in currentFunctionTestTypeGroups"
|
|
:key="group.key"
|
|
:name="group.key"
|
|
>
|
|
<div class="text-weight-bold">{{ group.label }}</div>
|
|
</q-tab>
|
|
</q-tabs>
|
|
|
|
<q-tab-panels
|
|
v-if="currentSelectedTestTypeGroup"
|
|
v-model="currentSelectedTestTypeKey"
|
|
class="test-type-panels"
|
|
>
|
|
<q-tab-panel
|
|
v-for="group in currentFunctionTestTypeGroups"
|
|
:key="group.key"
|
|
:name="group.key"
|
|
class="test-type-panel"
|
|
>
|
|
<div class="test-case-stack">
|
|
<div
|
|
v-for="(testEntry, entryIndex) in group.entries"
|
|
:key="`${group.key}-${testEntry.index}-${entryIndex}`"
|
|
class="test-case-section"
|
|
>
|
|
<div
|
|
class="test-case-header"
|
|
v-if="group.entries.length > 1"
|
|
>
|
|
<div>
|
|
<h3 class="test-case-title">
|
|
{{ `${group.label} ${entryIndex + 1}` }}
|
|
</h3>
|
|
<div class="test-case-subtitle">
|
|
Raw test {{ testEntry.index + 1 }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="testEntry.note" class="empty-state">
|
|
{{ testEntry.note }}
|
|
</div>
|
|
|
|
<div class="test-detail-grid">
|
|
<div class="detail-panel">
|
|
<label
|
|
class="field-label"
|
|
:for="`test-call-params-${group.key}-${testEntry.index}-${entryIndex}`"
|
|
>
|
|
Call Params
|
|
</label>
|
|
<textarea
|
|
:id="`test-call-params-${group.key}-${testEntry.index}-${entryIndex}`"
|
|
v-model="testEntry.callParamsText"
|
|
class="native-textarea test-json-editor"
|
|
spellcheck="false"
|
|
@input="persistTestCallParams(testEntry, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
v-if="testEntry.callParamsError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ testEntry.callParamsError }}
|
|
</div>
|
|
</div>
|
|
<div class="detail-panel">
|
|
<label
|
|
class="field-label"
|
|
:for="`test-expected-${group.key}-${testEntry.index}-${entryIndex}`"
|
|
>
|
|
{{ testEntry.expectedLabel }}
|
|
</label>
|
|
<textarea
|
|
:id="`test-expected-${group.key}-${testEntry.index}-${entryIndex}`"
|
|
v-model="testEntry.expectedText"
|
|
class="native-textarea test-json-editor"
|
|
spellcheck="false"
|
|
@input="persistTestExpected(testEntry, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
v-if="testEntry.expectedError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ testEntry.expectedError }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="q-mt-lg">
|
|
<label
|
|
class="field-label"
|
|
:for="`test-funding-source-${group.key}-${testEntry.index}-${entryIndex}`"
|
|
>
|
|
Funding Source
|
|
</label>
|
|
<div class="picker-action-row">
|
|
<q-select
|
|
:id="`test-funding-source-${group.key}-${testEntry.index}-${entryIndex}`"
|
|
v-model="selectedTestFundingSourceByDataset[selectedDatasetKey]"
|
|
:options="fundingSourceOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select picker-select"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
|
|
<q-btn
|
|
color="primary"
|
|
outline
|
|
:label="`Funding Source Mocks (${currentFunctionMockRows.length})`"
|
|
:disable="!currentFunction || !currentTestFundingSource"
|
|
@click="functionMocksDialog = true"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="mock-list q-mt-lg"
|
|
v-if="testEntry.rawMockSets.length"
|
|
>
|
|
<div
|
|
v-for="(mockSet, mockSetIndex) in testEntry.rawMockSets"
|
|
:key="mockSet.path"
|
|
>
|
|
<div
|
|
v-if="mockSet.metaFields.description !== undefined"
|
|
class="q-mb-md"
|
|
>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-description-${group.key}-${testEntry.index}-${mockSetIndex}`"
|
|
>
|
|
Description
|
|
</label>
|
|
<input
|
|
:id="`test-mock-description-${group.key}-${testEntry.index}-${mockSetIndex}`"
|
|
v-model="mockSet.metaFields.description"
|
|
class="native-input"
|
|
type="text"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
class="test-mock-binding"
|
|
:key="`${mockSet.path}-binding-${activeTestMockBindingName(mockSet)}`"
|
|
>
|
|
<div class="test-mock-binding-header">
|
|
<div class="test-mock-binding-body">
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-binding-${group.key}-${testEntry.index}-${mockSetIndex}`"
|
|
>
|
|
Function Mock
|
|
</label>
|
|
<q-select
|
|
:id="`test-mock-binding-${group.key}-${testEntry.index}-${mockSetIndex}`"
|
|
:model-value="activeTestMockBindingName(mockSet)"
|
|
:options="testMockBindingOptions(mockSet)"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleTestMockBindingSelection(testEntry, mockSet, $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="test-mock-item-list"
|
|
v-if="activeTestMockBinding(mockSet) && activeTestMockBinding(mockSet).items.length"
|
|
>
|
|
<div
|
|
v-for="(item, itemIndex) in activeTestMockBinding(mockSet).items"
|
|
:key="`${mockSet.path}-binding-${activeTestMockBindingName(mockSet)}-item-${itemIndex}`"
|
|
class="test-mock-item-card"
|
|
>
|
|
<template
|
|
v-if="activeTestMockBindingMode(mockSet) === 'rest'"
|
|
>
|
|
<div class="test-mock-item-top-row">
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Description
|
|
</label>
|
|
<input
|
|
:id="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.descriptionText"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="Optional label"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
/>
|
|
</div>
|
|
|
|
<div class="test-mock-item-skip">
|
|
<q-checkbox
|
|
:model-value="testMockItemSkip(item)"
|
|
dense
|
|
color="warning"
|
|
keep-color
|
|
label="Skip this mock definition"
|
|
@update:model-value="setTestMockItemSkip(testEntry, mockSet, item, $event)"
|
|
></q-checkbox>
|
|
</div>
|
|
|
|
<q-btn
|
|
color="negative"
|
|
outline
|
|
label="Remove"
|
|
@click="removeTestMockBindingItem(testEntry, mockSet, activeTestMockBinding(mockSet), itemIndex)"
|
|
></q-btn>
|
|
</div>
|
|
|
|
<div
|
|
class="test-mock-item-columns q-mt-md"
|
|
>
|
|
<div class="test-mock-item-column">
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Request Type
|
|
</label>
|
|
<q-select
|
|
:id="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
:model-value="item.requestType"
|
|
:options="restTestMockRequestTypeOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleTestMockSelectChange(testEntry, mockSet, item, 'requestType', $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Request Body
|
|
</label>
|
|
<textarea
|
|
:id="`test-mock-item-request-body-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.requestBodyText"
|
|
class="native-textarea test-mock-item-json"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
v-if="item.requestBodyError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ item.requestBodyError }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-mock-item-column">
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Response Type
|
|
</label>
|
|
<q-select
|
|
:id="`test-mock-item-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
:model-value="item.responseType"
|
|
:options="restTestMockResponseTypeOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleTestMockSelectChange(testEntry, mockSet, item, 'responseType', $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Response
|
|
</label>
|
|
<textarea
|
|
:id="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.responseText"
|
|
class="native-textarea test-mock-item-json"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
v-if="item.responseError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ item.responseError }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="test-mock-item-action-row">
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Description
|
|
</label>
|
|
<input
|
|
:id="`test-mock-item-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.descriptionText"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="Optional label"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
/>
|
|
<div class="settings-hint q-mt-xs">
|
|
Test label for this RPC mock
|
|
variant.
|
|
</div>
|
|
</div>
|
|
|
|
<q-btn
|
|
color="negative"
|
|
outline
|
|
label="Remove"
|
|
@click="removeTestMockBindingItem(testEntry, mockSet, activeTestMockBinding(mockSet), itemIndex)"
|
|
></q-btn>
|
|
</div>
|
|
|
|
<div
|
|
class="test-mock-item-pair-row test-mock-item-pair-row--top"
|
|
>
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-rpc-call-name-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
RPC Call
|
|
</label>
|
|
<input
|
|
:id="`test-mock-item-rpc-call-name-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.rpcCallName"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="some_rpc_call"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
/>
|
|
<div class="settings-hint q-mt-xs">
|
|
Mocked RPC method name used by the
|
|
wallet code.
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-rpc-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
RPC Call Description
|
|
</label>
|
|
<input
|
|
:id="`test-mock-item-rpc-description-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.rpcDescriptionText"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="Optional label"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
/>
|
|
<div class="settings-hint q-mt-xs">
|
|
Optional label for the nested RPC
|
|
call.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="test-mock-item-pair-row test-mock-item-pair-row--top"
|
|
>
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Request Type
|
|
</label>
|
|
<q-select
|
|
:id="`test-mock-item-request-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
:model-value="item.requestType"
|
|
:options="rpcTestMockRequestTypeOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleTestMockSelectChange(testEntry, mockSet, item, 'requestType', $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
<div class="settings-hint q-mt-xs">
|
|
How the mocked RPC callable is
|
|
invoked.
|
|
</div>
|
|
|
|
<q-tabs
|
|
:model-value="rpcRequestDetailTab(testEntry, mockSet, itemIndex)"
|
|
align="justify"
|
|
dense
|
|
indicator-color="transparent"
|
|
no-caps
|
|
class="request-detail-tabs text-primary"
|
|
@update:model-value="setRpcRequestDetailTab(testEntry, mockSet, itemIndex, $event)"
|
|
>
|
|
<q-tab
|
|
name="class"
|
|
label="Request Class"
|
|
></q-tab>
|
|
<q-tab
|
|
name="args"
|
|
label="Request Args"
|
|
></q-tab>
|
|
<q-tab
|
|
name="kwargs"
|
|
label="Request Kwargs"
|
|
></q-tab>
|
|
</q-tabs>
|
|
|
|
<q-tab-panels
|
|
:model-value="rpcRequestDetailTab(testEntry, mockSet, itemIndex)"
|
|
class="request-detail-panels"
|
|
>
|
|
<q-tab-panel
|
|
name="class"
|
|
class="request-detail-panel"
|
|
>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-request-class-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Request Class
|
|
</label>
|
|
<input
|
|
:id="`test-mock-item-request-class-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.requestClassName"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="optional.import.path.ClassName"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
/>
|
|
<div
|
|
class="settings-hint q-mt-xs"
|
|
>
|
|
Optional request class used by
|
|
the RPC test harness.
|
|
</div>
|
|
</q-tab-panel>
|
|
|
|
<q-tab-panel
|
|
name="args"
|
|
class="request-detail-panel"
|
|
>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-request-args-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Request Args
|
|
</label>
|
|
<textarea
|
|
:id="`test-mock-item-request-args-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.requestArgsText"
|
|
class="native-textarea test-mock-item-json"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
class="settings-hint q-mt-xs"
|
|
>
|
|
JSON array of positional
|
|
arguments passed into the RPC
|
|
call.
|
|
</div>
|
|
<div
|
|
v-if="item.requestArgsError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ item.requestArgsError }}
|
|
</div>
|
|
</q-tab-panel>
|
|
|
|
<q-tab-panel
|
|
name="kwargs"
|
|
class="request-detail-panel"
|
|
>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-request-kwargs-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Request Kwargs
|
|
</label>
|
|
<textarea
|
|
:id="`test-mock-item-request-kwargs-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.requestKwargsText"
|
|
class="native-textarea test-mock-item-json"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
class="settings-hint q-mt-xs"
|
|
>
|
|
JSON object of keyword
|
|
arguments passed into the RPC
|
|
call.
|
|
</div>
|
|
<div
|
|
v-if="item.requestKwargsError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ item.requestKwargsError }}
|
|
</div>
|
|
</q-tab-panel>
|
|
</q-tab-panels>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-rpc-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Response Type
|
|
</label>
|
|
<q-select
|
|
:id="`test-mock-item-rpc-response-type-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
:model-value="item.rpcResponseType"
|
|
:options="rpcTestMockResponseTypeOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleTestMockSelectChange(testEntry, mockSet, item, 'rpcResponseType', $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
<div class="settings-hint q-mt-xs">
|
|
Return mode expected by the test
|
|
harness.
|
|
</div>
|
|
<div class="q-mt-md">
|
|
<label
|
|
class="field-label"
|
|
:for="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
>
|
|
Response Payload
|
|
</label>
|
|
<textarea
|
|
:id="`test-mock-item-response-${group.key}-${testEntry.index}-${mockSetIndex}-${activeTestMockBindingName(mockSet)}-${itemIndex}`"
|
|
v-model="item.responseText"
|
|
class="native-textarea test-mock-item-json"
|
|
spellcheck="false"
|
|
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
|
|
></textarea>
|
|
<div
|
|
class="settings-hint q-mt-xs"
|
|
>
|
|
Business data returned by this
|
|
mocked RPC call.
|
|
</div>
|
|
<div
|
|
v-if="item.responseError"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ item.responseError }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div
|
|
v-if="item.error"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ item.error }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state q-mt-md">
|
|
No objects are defined in this mock array
|
|
yet.
|
|
</div>
|
|
|
|
<div class="pill-row q-mt-md">
|
|
<q-btn
|
|
color="primary"
|
|
outline
|
|
label="Add Function Mock"
|
|
:disable="!activeTestMockBinding(mockSet)"
|
|
@click="addTestMockBindingItem(testEntry, mockSet, activeTestMockBinding(mockSet))"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="mockSet.error"
|
|
class="settings-error q-mt-sm"
|
|
>
|
|
{{ mockSet.error }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state q-mt-lg">
|
|
No wallet-specific mock entry is defined for
|
|
<code>{{ currentTestFundingSource.name }}</code>
|
|
in this test.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-tab-panel>
|
|
</q-tab-panels>
|
|
</div>
|
|
|
|
<div v-else-if="currentFunction" class="empty-state">
|
|
No tests are defined for this function.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
No functions are defined for this fixture file.
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</main>
|
|
</div>
|
|
|
|
<q-dialog v-model="fundingSourceSettingsDialog" full-width position="top">
|
|
<q-card class="editor-dialog-card">
|
|
<q-card-section class="q-pa-lg dialog-header">
|
|
<div>
|
|
<h2 class="q-my-none">Funding Source Settings</h2>
|
|
<div class="section-caption">
|
|
Edit the settings for the selected funding source. Changes
|
|
update the active fixture JSON immediately.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pill-row q-mt-md">
|
|
<q-chip
|
|
v-if="currentFundingSource"
|
|
dense
|
|
square
|
|
color="primary"
|
|
text-color="white"
|
|
>
|
|
{{ currentFundingSourceSettingRows.length }} rows
|
|
</q-chip>
|
|
</div>
|
|
|
|
<q-btn
|
|
flat
|
|
color="primary"
|
|
label="Close"
|
|
class="dialog-close-btn"
|
|
v-close-popup
|
|
></q-btn>
|
|
</q-card-section>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-card-section class="q-pa-lg">
|
|
<div v-if="fundingSourceOptions.length">
|
|
<label
|
|
class="native-select-label"
|
|
for="dialog-funding-source-select"
|
|
>
|
|
Funding source
|
|
</label>
|
|
<q-select
|
|
id="dialog-funding-source-select"
|
|
v-model="selectedFundingSourceByDataset[selectedDatasetKey]"
|
|
:options="fundingSourceOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
>
|
|
<template v-slot:append>
|
|
<span class="fixture-select-icon" aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
|
|
<div v-if="currentFundingSource" class="q-mt-md q-mb-lg">
|
|
<q-checkbox
|
|
v-model="currentFundingSourceSkip"
|
|
dense
|
|
color="warning"
|
|
keep-color
|
|
label="Skip this funding source"
|
|
></q-checkbox>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="currentFundingSource">
|
|
<div
|
|
class="settings-editor"
|
|
v-if="currentFundingSourceSettingRows.length"
|
|
>
|
|
<div class="settings-table">
|
|
<div class="settings-table-header settings-table-grid">
|
|
<div class="settings-table-heading">Key</div>
|
|
<div class="settings-table-heading">Type</div>
|
|
<div class="settings-table-heading">Value</div>
|
|
<div class="settings-table-heading">Actions</div>
|
|
</div>
|
|
|
|
<div
|
|
v-for="(row, rowIndex) in currentFundingSourceSettingRows"
|
|
:key="row.id"
|
|
class="settings-table-row"
|
|
>
|
|
<div class="settings-table-grid">
|
|
<div>
|
|
<input
|
|
:id="`setting-key-${row.id}`"
|
|
v-model="row.key"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="setting_key"
|
|
spellcheck="false"
|
|
@input="persistFundingSourceSettings({ silent: true })"
|
|
/>
|
|
</div>
|
|
|
|
<div class="native-select-wrap">
|
|
<select
|
|
:id="`setting-type-${row.id}`"
|
|
v-model="row.type"
|
|
class="native-select"
|
|
@change="handleFundingSourceSettingTypeChange(row)"
|
|
>
|
|
<option
|
|
v-for="option in settingTypeOptions"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<div
|
|
v-if="row.type === 'boolean'"
|
|
class="native-select-wrap"
|
|
>
|
|
<select
|
|
:id="`setting-value-${row.id}`"
|
|
v-model="row.valueText"
|
|
class="native-select"
|
|
@change="persistFundingSourceSettings()"
|
|
>
|
|
<option value="true">true</option>
|
|
<option value="false">false</option>
|
|
</select>
|
|
</div>
|
|
<textarea
|
|
v-else-if="row.type === 'json'"
|
|
:id="`setting-value-${row.id}`"
|
|
v-model="row.valueText"
|
|
class="native-textarea"
|
|
spellcheck="false"
|
|
@input="persistFundingSourceSettings({ silent: true })"
|
|
></textarea>
|
|
<input
|
|
v-else
|
|
:id="`setting-value-${row.id}`"
|
|
v-model="row.valueText"
|
|
class="native-input"
|
|
type="text"
|
|
spellcheck="false"
|
|
@input="persistFundingSourceSettings({ silent: true })"
|
|
/>
|
|
</div>
|
|
|
|
<div class="settings-table-actions">
|
|
<q-btn
|
|
color="negative"
|
|
outline
|
|
label="Remove"
|
|
@click="removeFundingSourceSettingRow(rowIndex)"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="row.error"
|
|
class="settings-error settings-table-note"
|
|
>
|
|
{{ row.error }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
No settings are defined yet for this funding source.
|
|
</div>
|
|
|
|
<div class="pill-row q-mt-md">
|
|
<q-btn
|
|
color="primary"
|
|
outline
|
|
label="Add Setting"
|
|
@click="addFundingSourceSettingRow()"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
No funding sources are defined for this fixture file.
|
|
</div>
|
|
</q-card-section>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-card-actions align="right" class="q-pa-lg">
|
|
<q-btn
|
|
color="primary"
|
|
unelevated
|
|
label="Done"
|
|
v-close-popup
|
|
></q-btn>
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="functionMocksDialog" full-width position="top">
|
|
<q-card class="editor-dialog-card">
|
|
<q-card-section class="q-pa-lg dialog-header">
|
|
<div>
|
|
<h2 class="q-my-none">Function Mocks</h2>
|
|
<div class="section-caption">
|
|
Reusable mocks for the selected function and funding source.
|
|
</div>
|
|
<div v-if="currentFunctionBaseMockPath" class="section-caption">
|
|
Changes update
|
|
<code>{{ currentFunctionBaseMockPath }}</code>
|
|
immediately.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pill-row q-mt-md">
|
|
<q-chip
|
|
v-if="currentFunction && currentTestFundingSource"
|
|
dense
|
|
square
|
|
color="primary"
|
|
text-color="white"
|
|
>
|
|
{{ currentFunctionMockRows.length }} mocks
|
|
</q-chip>
|
|
</div>
|
|
|
|
<q-btn
|
|
flat
|
|
color="primary"
|
|
label="Close"
|
|
class="dialog-close-btn"
|
|
v-close-popup
|
|
></q-btn>
|
|
</q-card-section>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-card-section class="q-pa-lg">
|
|
<div v-if="currentFunction && currentTestFundingSource">
|
|
<div
|
|
class="settings-editor"
|
|
v-if="currentFunctionMockRows.length"
|
|
>
|
|
<div class="function-mocks-table-wrap">
|
|
<div class="function-mocks-table">
|
|
<div
|
|
class="function-mocks-header function-mocks-table-grid"
|
|
:class="`function-mocks-table-grid--${currentFunctionMockEditorMode}`"
|
|
>
|
|
<div class="function-mocks-heading">Mock Name</div>
|
|
<div class="function-mocks-heading">Method</div>
|
|
<template v-if="currentFunctionMockEditorMode === 'rest'">
|
|
<div class="function-mocks-heading">Headers JSON</div>
|
|
<div class="function-mocks-heading">
|
|
Query Params JSON
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="function-mocks-heading">Response</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div
|
|
v-for="(mockRow, rowIndex) in currentFunctionMockRows"
|
|
:key="mockRow.id"
|
|
class="function-mocks-row"
|
|
>
|
|
<div
|
|
class="function-mocks-table-grid"
|
|
:class="`function-mocks-table-grid--${functionMockRowMode(mockRow)}`"
|
|
>
|
|
<div>
|
|
<input
|
|
:id="`function-mock-name-${mockRow.id}`"
|
|
v-model="mockRow.name"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="mock_name"
|
|
spellcheck="false"
|
|
@input="persistFunctionMocks({ silent: true })"
|
|
/>
|
|
<div class="settings-hint q-mt-sm">
|
|
Mock Name: Stable mock identifier used inside this
|
|
function's fixture map.
|
|
</div>
|
|
<div class="function-mock-primary-actions">
|
|
<q-btn
|
|
color="negative"
|
|
outline
|
|
label="Remove Mock"
|
|
@click="removeFunctionMockRow(rowIndex)"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="function-mock-cell-stack"
|
|
:class="{ 'function-mock-cell-stack--rpc': mockRow.requestType === 'function' }"
|
|
>
|
|
<div class="function-mock-subfield">
|
|
<q-select
|
|
:id="`function-mock-request-type-${mockRow.id}`"
|
|
:model-value="mockRow.requestType"
|
|
:options="functionMockRequestTypeOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleFunctionMockRequestTypeChange(mockRow, $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
<div class="settings-hint">
|
|
Request Type: Choose <code>REST</code> for HTTP
|
|
mocks or <code>RPC</code> for Python patch mocks.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="function-mock-subfield">
|
|
<input
|
|
:id="`function-mock-method-${mockRow.id}`"
|
|
v-model="mockRow.method"
|
|
class="native-input"
|
|
type="text"
|
|
:placeholder="mockRow.requestType === 'function' ? 'package.module.symbol' : 'GET'"
|
|
spellcheck="false"
|
|
@input="persistFunctionMocks({ silent: true })"
|
|
/>
|
|
<div class="settings-hint">
|
|
{{ functionMockMethodHint(mockRow) }}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="mockRow.requestType !== 'function'"
|
|
class="function-mock-subfield"
|
|
>
|
|
<input
|
|
:id="`function-mock-uri-${mockRow.id}`"
|
|
v-model="mockRow.uri"
|
|
class="native-input"
|
|
type="text"
|
|
placeholder="/path"
|
|
spellcheck="false"
|
|
@input="persistFunctionMocks({ silent: true })"
|
|
/>
|
|
<div class="settings-hint">
|
|
Path: Request path or URI matcher for REST mocks.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template
|
|
v-if="functionMockRowMode(mockRow) === 'rest'"
|
|
>
|
|
<div class="function-mock-json-cell">
|
|
<textarea
|
|
:id="`function-mock-headers-${mockRow.id}`"
|
|
v-model="mockRow.headersText"
|
|
class="native-textarea"
|
|
placeholder='{"Authorization":"Bearer ..."}'
|
|
spellcheck="false"
|
|
@input="persistFunctionMocks({ silent: true })"
|
|
></textarea>
|
|
<div class="settings-hint q-mt-sm">
|
|
Headers JSON: JSON object of request headers that
|
|
must match.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="function-mock-json-cell">
|
|
<textarea
|
|
:id="`function-mock-query-${mockRow.id}`"
|
|
v-model="mockRow.queryParamsText"
|
|
class="native-textarea"
|
|
placeholder='{"foo":"bar"}'
|
|
spellcheck="false"
|
|
@input="persistFunctionMocks({ silent: true })"
|
|
></textarea>
|
|
<div class="settings-hint q-mt-sm">
|
|
Query Params JSON: JSON object of expected
|
|
query-string parameters.
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="function-mock-cell-stack">
|
|
<div class="function-mock-subfield">
|
|
<q-select
|
|
:id="`function-mock-response-type-${mockRow.id}`"
|
|
:model-value="mockRow.responseType"
|
|
:options="functionMockRpcResponseTypeOptions"
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
dense
|
|
options-dense
|
|
behavior="menu"
|
|
hide-dropdown-icon
|
|
class="fixture-select"
|
|
@update:model-value="handleFunctionMockResponseTypeChange(mockRow, $event)"
|
|
>
|
|
<template v-slot:append>
|
|
<span
|
|
class="fixture-select-icon"
|
|
aria-hidden="true"
|
|
>expand_more</span
|
|
>
|
|
</template>
|
|
</q-select>
|
|
<div class="settings-hint">
|
|
Response Type: Optional RPC return mode such as
|
|
<code>data</code>, <code>function</code>,
|
|
<code>async-function</code>, <code>json</code>,
|
|
or <code>exception</code>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="function-mock-subfield">
|
|
<textarea
|
|
:id="`function-mock-response-${mockRow.id}`"
|
|
v-model="mockRow.responseText"
|
|
class="native-textarea"
|
|
placeholder='{"method_name":{"request_type":"function","response_type":"json","response":{}}}'
|
|
spellcheck="false"
|
|
@input="persistFunctionMocks({ silent: true })"
|
|
></textarea>
|
|
<div class="settings-hint">
|
|
Response: RPC mock object returned by the
|
|
patched function.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div
|
|
v-if="mockRow.error"
|
|
class="settings-error function-mocks-note"
|
|
>
|
|
{{ mockRow.error }}
|
|
</div>
|
|
<div v-else class="settings-hint function-mocks-note">
|
|
Leave unused fields blank. JSON columns must contain
|
|
valid JSON objects or values.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
No function-level mocks are defined yet for
|
|
<code>{{ currentTestFundingSource.name }}</code>
|
|
in
|
|
<code>{{ currentFunction.name }}</code>.
|
|
</div>
|
|
|
|
<div class="pill-row q-mt-md">
|
|
<q-btn
|
|
color="primary"
|
|
outline
|
|
label="Add Mock"
|
|
@click="addFunctionMockRow()"
|
|
></q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
Select a funding source and function first.
|
|
</div>
|
|
</q-card-section>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-card-actions align="right" class="q-pa-lg">
|
|
<q-btn
|
|
color="primary"
|
|
unelevated
|
|
label="Done"
|
|
v-close-popup
|
|
></q-btn>
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="editorDialog" maximized>
|
|
<q-card>
|
|
<q-card-section class="row items-start justify-between q-pa-lg">
|
|
<div>
|
|
<h2 class="q-my-none">Raw JSON Editor</h2>
|
|
<div class="section-caption">
|
|
Edit the current fixture file directly, then apply, save, or
|
|
download the changes.
|
|
</div>
|
|
</div>
|
|
|
|
<q-btn flat color="primary" label="Close" v-close-popup></q-btn>
|
|
</q-card-section>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-card-section class="q-pa-lg">
|
|
<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="Reset"
|
|
@click="resetEditor(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"
|
|
@input="syncEditorFromRawText(selectedDatasetKey, { silent: true })"
|
|
></textarea>
|
|
</q-card-section>
|
|
|
|
<q-separator></q-separator>
|
|
|
|
<q-card-actions align="right" class="q-pa-lg">
|
|
<q-btn
|
|
color="primary"
|
|
unelevated
|
|
label="Done"
|
|
v-close-popup
|
|
></q-btn>
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<input
|
|
ref="loadFileInput"
|
|
class="hidden-input"
|
|
type="file"
|
|
accept=".json,application/json"
|
|
@change="handleSingleFileSelection($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 STORAGE_PREFIX = 'lnbits-wallet-fixture-studio'
|
|
|
|
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 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
|
|
}
|
|
}
|
|
|
|
function createTestMockItemDraftState(item = {}, mode = 'rest') {
|
|
const source = isPlainObject(item) ? item : {}
|
|
|
|
if (mode === 'rpc') {
|
|
const responseMap = isPlainObject(source.response)
|
|
? source.response
|
|
: {}
|
|
const [rpcCallName, rpcCallValue] = Object.entries(
|
|
responseMap
|
|
)[0] || ['', {}]
|
|
const rpcCall = isPlainObject(rpcCallValue) ? rpcCallValue : {}
|
|
const requestData = isPlainObject(rpcCall.request_data)
|
|
? rpcCall.request_data
|
|
: {}
|
|
const requestDataExtras = Object.fromEntries(
|
|
Object.entries(requestData).filter(
|
|
([key]) => !['args', 'kwargs', 'klass'].includes(key)
|
|
)
|
|
)
|
|
|
|
return {
|
|
descriptionText:
|
|
typeof source.description === 'string' ? source.description : '',
|
|
skip: false,
|
|
hasSkipField: false,
|
|
requestType:
|
|
typeof rpcCall.request_type === 'string'
|
|
? rpcCall.request_type
|
|
: 'function',
|
|
requestArgsText: Object.prototype.hasOwnProperty.call(
|
|
requestData,
|
|
'args'
|
|
)
|
|
? safeStringify(requestData.args)
|
|
: '',
|
|
requestArgsError: '',
|
|
requestKwargsText: Object.prototype.hasOwnProperty.call(
|
|
requestData,
|
|
'kwargs'
|
|
)
|
|
? safeStringify(requestData.kwargs)
|
|
: '',
|
|
requestKwargsError: '',
|
|
requestClassName:
|
|
typeof requestData.klass === 'string' ? requestData.klass : '',
|
|
requestDataExtras,
|
|
responseType:
|
|
typeof source.response_type === 'string'
|
|
? source.response_type
|
|
: 'data',
|
|
responseText:
|
|
rpcCall.response !== undefined
|
|
? safeStringify(rpcCall.response)
|
|
: '',
|
|
responseError: '',
|
|
rpcCallName,
|
|
rpcDescriptionText:
|
|
typeof rpcCall.description === 'string'
|
|
? rpcCall.description
|
|
: '',
|
|
rpcResponseType:
|
|
typeof rpcCall.response_type === 'string'
|
|
? rpcCall.response_type
|
|
: 'data',
|
|
error: ''
|
|
}
|
|
}
|
|
|
|
return {
|
|
descriptionText:
|
|
typeof source.description === 'string' ? source.description : '',
|
|
skip: typeof source.skip === 'boolean' ? source.skip : false,
|
|
hasSkipField: typeof source.skip === 'boolean',
|
|
requestType:
|
|
typeof source.request_type === 'string' ? source.request_type : '',
|
|
requestBodyText:
|
|
source.request_body !== undefined
|
|
? safeStringify(source.request_body)
|
|
: '',
|
|
requestBodyError: '',
|
|
responseType:
|
|
typeof source.response_type === 'string'
|
|
? source.response_type
|
|
: '',
|
|
responseText:
|
|
source.response !== undefined ? safeStringify(source.response) : '',
|
|
responseError: '',
|
|
error: ''
|
|
}
|
|
}
|
|
|
|
function inferDatasetKeyFromData(data) {
|
|
let rpcHint = false
|
|
|
|
for (const fn of Object.values(data?.functions || {})) {
|
|
for (const walletMocks of Object.values(fn?.mocks || {})) {
|
|
for (const mockDefinition of Object.values(walletMocks || {})) {
|
|
if (!isPlainObject(mockDefinition)) {
|
|
continue
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(mockDefinition, 'uri')) {
|
|
return 'rest'
|
|
}
|
|
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(
|
|
mockDefinition,
|
|
'request_type'
|
|
) ||
|
|
Object.prototype.hasOwnProperty.call(
|
|
mockDefinition,
|
|
'response_type'
|
|
)
|
|
) {
|
|
rpcHint = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return rpcHint ? 'rpc' : ''
|
|
}
|
|
|
|
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({
|
|
components: {
|
|
JsonPanel
|
|
},
|
|
data() {
|
|
const datasets = {}
|
|
const functionMockDrafts = {}
|
|
const selectedFunctionByDataset = {}
|
|
const selectedFundingSourceByDataset = {}
|
|
const selectedRpcRequestDetailTabByScope = {}
|
|
const selectedTestMockBindingByScope = {}
|
|
const selectedTestFundingSourceByDataset = {}
|
|
const settingsDrafts = {}
|
|
|
|
DATASET_DEFS.forEach(def => {
|
|
datasets[def.key] = createDatasetState(def)
|
|
selectedFunctionByDataset[def.key] = ''
|
|
selectedFundingSourceByDataset[def.key] = ''
|
|
selectedTestFundingSourceByDataset[def.key] = ''
|
|
})
|
|
|
|
return {
|
|
datasets,
|
|
editorDialog: false,
|
|
fundingSourceSettingsDialog: false,
|
|
lastFilePickerHandle: null,
|
|
functionMockDraftRowId: 0,
|
|
functionMocksDialog: false,
|
|
functionMockDrafts,
|
|
settingsDraftRowId: 0,
|
|
settingsDrafts,
|
|
selectedDatasetKey: 'rest',
|
|
selectedRpcRequestDetailTabByScope,
|
|
selectedTestMockBindingByScope,
|
|
selectedTestFundingSourceByDataset,
|
|
selectedTestTypeByScope: {},
|
|
selectedFunctionByDataset,
|
|
selectedFundingSourceByDataset
|
|
}
|
|
},
|
|
computed: {
|
|
currentDataset() {
|
|
return this.datasets[this.selectedDatasetKey]
|
|
},
|
|
currentLoadedFileName() {
|
|
return (
|
|
this.currentDataset.fileHandle?.name ||
|
|
this.currentDataset.fileName
|
|
)
|
|
},
|
|
currentFundingSources() {
|
|
return this.currentDataset.analysis?.fundingSources || []
|
|
},
|
|
fundingSourceOptions() {
|
|
return this.currentFundingSources.map(fs => ({
|
|
label: `${fs.name} · ${fs.wallet_class}`,
|
|
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'}
|
|
]
|
|
},
|
|
functionMockRequestTypeOptions() {
|
|
return [
|
|
{label: 'REST', value: ''},
|
|
{label: 'RPC', value: 'function'}
|
|
]
|
|
},
|
|
functionMockRpcResponseTypeOptions() {
|
|
return [
|
|
{label: 'data', value: 'data'},
|
|
{label: 'function', value: 'function'},
|
|
{label: 'async-function', value: 'async-function'},
|
|
{label: 'json', value: 'json'},
|
|
{label: 'exception', value: 'exception'}
|
|
]
|
|
},
|
|
rpcTestMockRequestTypeOptions() {
|
|
return [
|
|
{label: 'function', value: 'function'},
|
|
{label: 'async-function', value: 'async-function'}
|
|
]
|
|
},
|
|
rpcTestMockResponseTypeOptions() {
|
|
return [
|
|
{label: 'data', value: 'data'},
|
|
{label: 'json', value: 'json'},
|
|
{label: 'exception', value: 'exception'},
|
|
{label: 'function', value: 'function'},
|
|
{label: '__aiter__', value: '__aiter__'}
|
|
]
|
|
},
|
|
restTestMockRequestTypeOptions() {
|
|
return [
|
|
{label: 'data', value: 'data'},
|
|
{label: 'json', value: 'json'}
|
|
]
|
|
},
|
|
restTestMockResponseTypeOptions() {
|
|
return [
|
|
{label: 'json', value: 'json'},
|
|
{label: 'data', value: 'data'},
|
|
{label: 'response', value: 'response'},
|
|
{label: 'stream', value: 'stream'}
|
|
]
|
|
},
|
|
currentFundingSourceName() {
|
|
return (
|
|
this.selectedFundingSourceByDataset[this.selectedDatasetKey] || ''
|
|
)
|
|
},
|
|
currentFundingSource() {
|
|
return (
|
|
this.currentFundingSources.find(
|
|
fs => fs.name === this.currentFundingSourceName
|
|
) || null
|
|
)
|
|
},
|
|
currentTestFundingSourceName() {
|
|
return (
|
|
this.selectedTestFundingSourceByDataset[
|
|
this.selectedDatasetKey
|
|
] || ''
|
|
)
|
|
},
|
|
currentTestFundingSource() {
|
|
return (
|
|
this.currentFundingSources.find(
|
|
fs => fs.name === this.currentTestFundingSourceName
|
|
) || null
|
|
)
|
|
},
|
|
currentFundingSourceSkip: {
|
|
get() {
|
|
return Boolean(this.currentFundingSource?.skip)
|
|
},
|
|
set(value) {
|
|
if (!this.currentFundingSourceName) {
|
|
return
|
|
}
|
|
|
|
this.applyDatasetMutation(
|
|
this.selectedDatasetKey,
|
|
data => {
|
|
const fundingSource =
|
|
data.funding_sources?.[this.currentFundingSourceName]
|
|
|
|
if (!fundingSource) {
|
|
return
|
|
}
|
|
|
|
fundingSource.skip = Boolean(value)
|
|
},
|
|
{
|
|
preserveSettingsDrafts: true,
|
|
preserveFunctionMockDrafts: true
|
|
}
|
|
)
|
|
}
|
|
},
|
|
currentFundingSourceSettingRows() {
|
|
if (!this.currentFundingSourceName) {
|
|
return []
|
|
}
|
|
|
|
return this.getSettingsDraftRows(
|
|
this.selectedDatasetKey,
|
|
this.currentFundingSourceName
|
|
)
|
|
},
|
|
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] || ''
|
|
},
|
|
currentFunction() {
|
|
return (
|
|
this.currentDataset.analysis?.functions.find(
|
|
fn => fn.name === this.currentFunctionName
|
|
) || null
|
|
)
|
|
},
|
|
editorDirty() {
|
|
return (
|
|
this.currentDataset.rawText !== this.currentDataset.snapshotText
|
|
)
|
|
},
|
|
currentFunctionBaseMockPath() {
|
|
if (!this.currentFunction || !this.currentTestFundingSource) {
|
|
return ''
|
|
}
|
|
|
|
return this.baseWalletPath(
|
|
this.currentFunction.name,
|
|
this.currentTestFundingSource.name
|
|
)
|
|
},
|
|
currentFunctionMockRows() {
|
|
if (
|
|
!this.currentFunctionName ||
|
|
!this.currentTestFundingSourceName
|
|
) {
|
|
return []
|
|
}
|
|
|
|
return this.getFunctionMockDraftRows(
|
|
this.selectedDatasetKey,
|
|
this.currentFunctionName,
|
|
this.currentTestFundingSourceName
|
|
)
|
|
},
|
|
currentFunctionMockEditorMode() {
|
|
if (this.currentFunctionMockRows.length) {
|
|
return this.functionMockRowMode(this.currentFunctionMockRows[0])
|
|
}
|
|
|
|
return this.selectedDatasetKey === 'rpc' ? 'rpc' : 'rest'
|
|
},
|
|
currentFunctionMockNameOptions() {
|
|
const seen = new Set()
|
|
|
|
return this.currentFunctionMockRows
|
|
.map(row => row.name.trim())
|
|
.filter(name => {
|
|
if (!name || seen.has(name)) {
|
|
return false
|
|
}
|
|
|
|
seen.add(name)
|
|
return true
|
|
})
|
|
.map(name => ({
|
|
label: name,
|
|
value: name
|
|
}))
|
|
},
|
|
currentFunctionTestsForFundingSource() {
|
|
if (!this.currentFunction || !this.currentTestFundingSource) {
|
|
return []
|
|
}
|
|
|
|
return this.currentFunction.tests.map(test => {
|
|
const rawHasMocksBlock = Object.prototype.hasOwnProperty.call(
|
|
test.raw || {},
|
|
'mocks'
|
|
)
|
|
const hasMockEntry = Boolean(
|
|
rawHasMocksBlock &&
|
|
Object.prototype.hasOwnProperty.call(
|
|
test.raw.mocks || {},
|
|
this.currentTestFundingSource.name
|
|
)
|
|
)
|
|
const rawWalletMockValue = hasMockEntry
|
|
? test.raw.mocks[this.currentTestFundingSource.name]
|
|
: undefined
|
|
const hasExpectField = Object.prototype.hasOwnProperty.call(
|
|
test.raw || {},
|
|
'expect'
|
|
)
|
|
const expectedField = hasExpectField
|
|
? 'expect'
|
|
: Object.prototype.hasOwnProperty.call(
|
|
test.raw || {},
|
|
'expect_error'
|
|
)
|
|
? 'expect_error'
|
|
: 'expect'
|
|
const rawWalletPath = hasMockEntry
|
|
? this.testWalletPath(
|
|
this.currentFunction.name,
|
|
test.index,
|
|
this.currentTestFundingSource.name
|
|
)
|
|
: ''
|
|
const walletMockValueIsArray = Array.isArray(rawWalletMockValue)
|
|
const rawMockSets =
|
|
rawWalletMockValue === undefined
|
|
? []
|
|
: (walletMockValueIsArray
|
|
? rawWalletMockValue
|
|
: [rawWalletMockValue]
|
|
).map((mockSet, index) => ({
|
|
label: Array.isArray(rawWalletMockValue)
|
|
? `Mock set ${index + 1}`
|
|
: 'Mock set',
|
|
setIndex: index,
|
|
walletMockValueIsArray,
|
|
path: Array.isArray(rawWalletMockValue)
|
|
? `${rawWalletPath}[${index}]`
|
|
: rawWalletPath,
|
|
value: deepClone(mockSet),
|
|
metaFields: Object.fromEntries(
|
|
Object.entries(
|
|
isPlainObject(mockSet) ? mockSet : {}
|
|
).filter(([key]) => key === 'description')
|
|
),
|
|
bindings: Object.entries(
|
|
isPlainObject(mockSet) ? mockSet : {}
|
|
)
|
|
.filter(([key]) => key !== 'description')
|
|
.map(([key, value]) => ({
|
|
selectedMockName: key,
|
|
items: (Array.isArray(value) ? value : [value]).map(
|
|
item =>
|
|
createTestMockItemDraftState(
|
|
item,
|
|
this.functionMockModeByName(key)
|
|
)
|
|
)
|
|
})),
|
|
error: ''
|
|
}))
|
|
const variants = testsForFundingSource(
|
|
this.currentTestFundingSource,
|
|
this.currentFunction.name,
|
|
this.currentFunction.raw,
|
|
test.raw,
|
|
test.index
|
|
)
|
|
|
|
let status = {
|
|
label: 'Ready',
|
|
color: 'positive'
|
|
}
|
|
let note = ''
|
|
|
|
if (this.currentTestFundingSource.skip) {
|
|
status = {
|
|
label: 'Wallet skipped',
|
|
color: 'warning'
|
|
}
|
|
note =
|
|
'The selected funding source is marked with `skip: true` in the fixture configuration.'
|
|
} else if (rawHasMocksBlock && !hasMockEntry) {
|
|
status = {
|
|
label: 'Missing wallet mocks',
|
|
color: 'negative'
|
|
}
|
|
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: 'Empty wallet mocks',
|
|
color: 'grey-7'
|
|
}
|
|
note =
|
|
'This test defines the selected funding source, but the mock set is empty.'
|
|
} else if (!rawHasMocksBlock) {
|
|
status = {
|
|
label: 'No test-level mocks',
|
|
color: 'grey-7'
|
|
}
|
|
note = 'This test case does not include a `mocks` section.'
|
|
}
|
|
|
|
return {
|
|
...test,
|
|
rawWalletPath,
|
|
rawMockSets,
|
|
variants,
|
|
variantCount: variants.length,
|
|
status,
|
|
note,
|
|
callParamsText: safeStringify(test.call_params || {}),
|
|
callParamsError: '',
|
|
expectedField,
|
|
expectedBlock:
|
|
expectedField === 'expect'
|
|
? test.expect === undefined
|
|
? null
|
|
: test.expect
|
|
: test.expect_error === undefined
|
|
? null
|
|
: test.expect_error,
|
|
expectedText: safeStringify(
|
|
expectedField === 'expect'
|
|
? test.expect === undefined
|
|
? null
|
|
: test.expect
|
|
: test.expect_error === undefined
|
|
? null
|
|
: test.expect_error
|
|
),
|
|
expectedError: '',
|
|
expectedLabel:
|
|
expectedField === 'expect' ? 'Expect' : 'Expect Error'
|
|
}
|
|
})
|
|
},
|
|
currentTestTypeScopeKey() {
|
|
if (
|
|
!this.currentFunctionName ||
|
|
!this.currentTestFundingSourceName
|
|
) {
|
|
return ''
|
|
}
|
|
|
|
return [
|
|
this.selectedDatasetKey,
|
|
this.currentFunctionName,
|
|
this.currentTestFundingSourceName
|
|
].join(':')
|
|
},
|
|
currentFunctionTestTypeGroups() {
|
|
const groups = []
|
|
const groupMap = new Map()
|
|
|
|
this.currentFunctionTestsForFundingSource.forEach(testEntry => {
|
|
const label =
|
|
(
|
|
testEntry.description || `Test ${testEntry.index + 1}`
|
|
).trim() || `Test ${testEntry.index + 1}`
|
|
|
|
if (!groupMap.has(label)) {
|
|
const group = {
|
|
key: label,
|
|
label,
|
|
entries: [],
|
|
totalVariants: 0
|
|
}
|
|
groupMap.set(label, group)
|
|
groups.push(group)
|
|
}
|
|
|
|
const group = groupMap.get(label)
|
|
group.entries.push(testEntry)
|
|
group.totalVariants += testEntry.variantCount
|
|
})
|
|
|
|
return groups
|
|
},
|
|
currentSelectedTestTypeKey: {
|
|
get() {
|
|
const scopeKey = this.currentTestTypeScopeKey
|
|
if (!scopeKey) {
|
|
return ''
|
|
}
|
|
|
|
const selectedKey = this.selectedTestTypeByScope[scopeKey]
|
|
if (
|
|
selectedKey &&
|
|
this.currentFunctionTestTypeGroups.some(
|
|
group => group.key === selectedKey
|
|
)
|
|
) {
|
|
return selectedKey
|
|
}
|
|
|
|
return this.currentFunctionTestTypeGroups[0]?.key || ''
|
|
},
|
|
set(value) {
|
|
if (!this.currentTestTypeScopeKey) {
|
|
return
|
|
}
|
|
|
|
this.selectedTestTypeByScope[this.currentTestTypeScopeKey] = value
|
|
}
|
|
},
|
|
currentSelectedTestTypeGroup() {
|
|
return (
|
|
this.currentFunctionTestTypeGroups.find(
|
|
group => group.key === this.currentSelectedTestTypeKey
|
|
) || null
|
|
)
|
|
}
|
|
},
|
|
watch: {
|
|
selectedDatasetKey() {
|
|
this.ensureSelections(this.selectedDatasetKey)
|
|
this.ensureSettingsDraftForCurrent()
|
|
this.ensureFunctionMockDraftForCurrent()
|
|
this.persistUiState()
|
|
},
|
|
currentFundingSourceName() {
|
|
this.ensureSettingsDraftForCurrent()
|
|
this.persistUiState()
|
|
},
|
|
currentTestFundingSourceName() {
|
|
this.ensureFunctionMockDraftForCurrent()
|
|
this.persistUiState()
|
|
},
|
|
currentFunctionName() {
|
|
this.ensureFunctionMockDraftForCurrent()
|
|
this.persistUiState()
|
|
},
|
|
currentSelectedTestTypeKey() {
|
|
this.persistUiState()
|
|
}
|
|
},
|
|
mounted() {
|
|
this.restoreUiState()
|
|
|
|
let restoredAny = this.restoreActiveDatasetFromStorage()
|
|
if (!restoredAny) {
|
|
for (const def of DATASET_DEFS) {
|
|
restoredAny =
|
|
this.restoreDatasetFromStorage(def.key) || restoredAny
|
|
}
|
|
}
|
|
|
|
if (!this.datasets[this.selectedDatasetKey].analysis) {
|
|
this.loadRelative(this.selectedDatasetKey)
|
|
} else if (!restoredAny) {
|
|
this.loadRelative(this.selectedDatasetKey)
|
|
}
|
|
|
|
window.addEventListener('keydown', this.handleGlobalKeydown)
|
|
},
|
|
beforeUnmount() {
|
|
window.removeEventListener('keydown', this.handleGlobalKeydown)
|
|
},
|
|
methods: {
|
|
storageKey(datasetKey) {
|
|
return `${STORAGE_PREFIX}:dataset:${datasetKey}`
|
|
},
|
|
activeDatasetStorageKey() {
|
|
return `${STORAGE_PREFIX}:active-dataset`
|
|
},
|
|
uiStateStorageKey() {
|
|
return `${STORAGE_PREFIX}:ui-state`
|
|
},
|
|
canUseLocalStorage() {
|
|
try {
|
|
return (
|
|
typeof window !== 'undefined' && Boolean(window.localStorage)
|
|
)
|
|
} catch (error) {
|
|
return false
|
|
}
|
|
},
|
|
clearPersistedFixtureState() {
|
|
if (!this.canUseLocalStorage()) {
|
|
return
|
|
}
|
|
|
|
DATASET_DEFS.forEach(def => {
|
|
window.localStorage.removeItem(this.storageKey(def.key))
|
|
})
|
|
window.localStorage.removeItem(this.activeDatasetStorageKey())
|
|
window.localStorage.removeItem(this.uiStateStorageKey())
|
|
},
|
|
resetInMemoryFixtureState(nextDatasetKey = this.selectedDatasetKey) {
|
|
DATASET_DEFS.forEach(def => {
|
|
this.datasets[def.key] = createDatasetState(def)
|
|
this.selectedFunctionByDataset[def.key] = ''
|
|
this.selectedFundingSourceByDataset[def.key] = ''
|
|
this.selectedTestFundingSourceByDataset[def.key] = ''
|
|
})
|
|
|
|
this.functionMockDrafts = {}
|
|
this.settingsDrafts = {}
|
|
this.selectedRpcRequestDetailTabByScope = {}
|
|
this.selectedTestMockBindingByScope = {}
|
|
this.selectedTestTypeByScope = {}
|
|
this.functionMockDraftRowId = 0
|
|
this.settingsDraftRowId = 0
|
|
this.lastFilePickerHandle = null
|
|
this.selectedDatasetKey = nextDatasetKey
|
|
},
|
|
persistDatasetToStorage(datasetKey) {
|
|
if (!this.canUseLocalStorage()) {
|
|
return
|
|
}
|
|
|
|
const dataset = this.datasets[datasetKey]
|
|
|
|
if (!dataset?.rawText) {
|
|
window.localStorage.removeItem(this.storageKey(datasetKey))
|
|
if (datasetKey === this.selectedDatasetKey) {
|
|
window.localStorage.removeItem(this.activeDatasetStorageKey())
|
|
}
|
|
return
|
|
}
|
|
|
|
const payload = JSON.stringify({
|
|
rawText: dataset.rawText,
|
|
snapshotText: dataset.snapshotText,
|
|
sourceLabel: dataset.sourceLabel || ''
|
|
})
|
|
|
|
window.localStorage.setItem(this.storageKey(datasetKey), payload)
|
|
|
|
if (datasetKey === this.selectedDatasetKey) {
|
|
window.localStorage.setItem(
|
|
this.activeDatasetStorageKey(),
|
|
JSON.stringify({
|
|
datasetKey,
|
|
rawText: dataset.rawText,
|
|
snapshotText: dataset.snapshotText,
|
|
sourceLabel: dataset.sourceLabel || ''
|
|
})
|
|
)
|
|
}
|
|
},
|
|
restoreActiveDatasetFromStorage() {
|
|
if (!this.canUseLocalStorage()) {
|
|
return false
|
|
}
|
|
|
|
const storedValue = window.localStorage.getItem(
|
|
this.activeDatasetStorageKey()
|
|
)
|
|
if (!storedValue) {
|
|
return false
|
|
}
|
|
|
|
const parsedStoredValue = parseJsonText(storedValue)
|
|
if (
|
|
!parsedStoredValue.ok ||
|
|
!isPlainObject(parsedStoredValue.data)
|
|
) {
|
|
window.localStorage.removeItem(this.activeDatasetStorageKey())
|
|
return false
|
|
}
|
|
|
|
const {datasetKey} = parsedStoredValue.data
|
|
|
|
if (
|
|
typeof datasetKey !== 'string' ||
|
|
!DATASET_DEFS.some(def => def.key === datasetKey)
|
|
) {
|
|
window.localStorage.removeItem(this.activeDatasetStorageKey())
|
|
return false
|
|
}
|
|
|
|
const rawText = String(parsedStoredValue.data.rawText || '').trim()
|
|
const snapshotText =
|
|
typeof parsedStoredValue.data.snapshotText === 'string'
|
|
? parsedStoredValue.data.snapshotText
|
|
: rawText
|
|
|
|
if (!rawText) {
|
|
window.localStorage.removeItem(this.activeDatasetStorageKey())
|
|
return false
|
|
}
|
|
|
|
const parsedDataset = parseJsonText(rawText)
|
|
if (!parsedDataset.ok) {
|
|
window.localStorage.removeItem(this.activeDatasetStorageKey())
|
|
return false
|
|
}
|
|
|
|
this.selectedDatasetKey = datasetKey
|
|
this.commitDataset(datasetKey, parsedDataset.data, {
|
|
rawText,
|
|
snapshotText,
|
|
sourceLabel:
|
|
parsedStoredValue.data.sourceLabel ||
|
|
`Local storage: ${this.datasets[datasetKey].fileName}`,
|
|
fileHandle: null,
|
|
loadError: ''
|
|
})
|
|
|
|
return true
|
|
},
|
|
restoreDatasetFromStorage(datasetKey) {
|
|
if (!this.canUseLocalStorage()) {
|
|
return false
|
|
}
|
|
|
|
const storedValue = window.localStorage.getItem(
|
|
this.storageKey(datasetKey)
|
|
)
|
|
if (!storedValue) {
|
|
return false
|
|
}
|
|
|
|
const parsedStoredValue = parseJsonText(storedValue)
|
|
if (
|
|
!parsedStoredValue.ok ||
|
|
!isPlainObject(parsedStoredValue.data)
|
|
) {
|
|
window.localStorage.removeItem(this.storageKey(datasetKey))
|
|
return false
|
|
}
|
|
|
|
const rawText = String(parsedStoredValue.data.rawText || '').trim()
|
|
const snapshotText =
|
|
typeof parsedStoredValue.data.snapshotText === 'string'
|
|
? parsedStoredValue.data.snapshotText
|
|
: rawText
|
|
|
|
if (!rawText) {
|
|
window.localStorage.removeItem(this.storageKey(datasetKey))
|
|
return false
|
|
}
|
|
|
|
const parsedDataset = parseJsonText(rawText)
|
|
if (!parsedDataset.ok) {
|
|
window.localStorage.removeItem(this.storageKey(datasetKey))
|
|
return false
|
|
}
|
|
|
|
this.commitDataset(datasetKey, parsedDataset.data, {
|
|
rawText,
|
|
snapshotText,
|
|
sourceLabel:
|
|
parsedStoredValue.data.sourceLabel ||
|
|
`Local storage: ${this.datasets[datasetKey].fileName}`,
|
|
fileHandle: null,
|
|
loadError: ''
|
|
})
|
|
|
|
return true
|
|
},
|
|
persistUiState() {
|
|
if (!this.canUseLocalStorage()) {
|
|
return
|
|
}
|
|
|
|
window.localStorage.setItem(
|
|
this.uiStateStorageKey(),
|
|
JSON.stringify({
|
|
selectedDatasetKey: this.selectedDatasetKey,
|
|
selectedFunctionByDataset: this.selectedFunctionByDataset,
|
|
selectedFundingSourceByDataset:
|
|
this.selectedFundingSourceByDataset,
|
|
selectedRpcRequestDetailTabByScope:
|
|
this.selectedRpcRequestDetailTabByScope,
|
|
selectedTestMockBindingByScope:
|
|
this.selectedTestMockBindingByScope,
|
|
selectedTestFundingSourceByDataset:
|
|
this.selectedTestFundingSourceByDataset,
|
|
selectedTestTypeByScope: this.selectedTestTypeByScope
|
|
})
|
|
)
|
|
},
|
|
restoreUiState() {
|
|
if (!this.canUseLocalStorage()) {
|
|
return
|
|
}
|
|
|
|
const storedValue = window.localStorage.getItem(
|
|
this.uiStateStorageKey()
|
|
)
|
|
if (!storedValue) {
|
|
return
|
|
}
|
|
|
|
const parsedStoredValue = parseJsonText(storedValue)
|
|
if (
|
|
!parsedStoredValue.ok ||
|
|
!isPlainObject(parsedStoredValue.data)
|
|
) {
|
|
window.localStorage.removeItem(this.uiStateStorageKey())
|
|
return
|
|
}
|
|
|
|
const state = parsedStoredValue.data
|
|
|
|
if (
|
|
typeof state.selectedDatasetKey === 'string' &&
|
|
DATASET_DEFS.some(def => def.key === state.selectedDatasetKey)
|
|
) {
|
|
this.selectedDatasetKey = state.selectedDatasetKey
|
|
}
|
|
|
|
if (isPlainObject(state.selectedFunctionByDataset)) {
|
|
Object.assign(
|
|
this.selectedFunctionByDataset,
|
|
state.selectedFunctionByDataset
|
|
)
|
|
}
|
|
|
|
if (isPlainObject(state.selectedFundingSourceByDataset)) {
|
|
Object.assign(
|
|
this.selectedFundingSourceByDataset,
|
|
state.selectedFundingSourceByDataset
|
|
)
|
|
}
|
|
|
|
if (isPlainObject(state.selectedRpcRequestDetailTabByScope)) {
|
|
this.selectedRpcRequestDetailTabByScope = {
|
|
...state.selectedRpcRequestDetailTabByScope
|
|
}
|
|
}
|
|
|
|
if (isPlainObject(state.selectedTestMockBindingByScope)) {
|
|
this.selectedTestMockBindingByScope = {
|
|
...state.selectedTestMockBindingByScope
|
|
}
|
|
}
|
|
|
|
if (isPlainObject(state.selectedTestFundingSourceByDataset)) {
|
|
Object.assign(
|
|
this.selectedTestFundingSourceByDataset,
|
|
state.selectedTestFundingSourceByDataset
|
|
)
|
|
}
|
|
|
|
if (isPlainObject(state.selectedTestTypeByScope)) {
|
|
this.selectedTestTypeByScope = {
|
|
...state.selectedTestTypeByScope
|
|
}
|
|
}
|
|
},
|
|
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)
|
|
}
|
|
},
|
|
prettyValue(value) {
|
|
if (typeof value === 'string') {
|
|
return value
|
|
}
|
|
if (
|
|
typeof value === 'number' ||
|
|
typeof value === 'boolean' ||
|
|
value === null
|
|
) {
|
|
return String(value)
|
|
}
|
|
|
|
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: ''
|
|
}
|
|
},
|
|
nextGeneratedSettingKey() {
|
|
const existingKeys = new Set(
|
|
this.currentFundingSourceSettingRows
|
|
.map(row => (row.key || '').trim())
|
|
.filter(Boolean)
|
|
)
|
|
|
|
let index = 1
|
|
let candidate = `new_setting_${index}`
|
|
|
|
while (existingKeys.has(candidate)) {
|
|
index += 1
|
|
candidate = `new_setting_${index}`
|
|
}
|
|
|
|
return candidate
|
|
},
|
|
createFunctionMockDraftRow(name = '', value = {}) {
|
|
const mock = isPlainObject(value) ? value : {}
|
|
|
|
return {
|
|
id: ++this.functionMockDraftRowId,
|
|
originalName: name,
|
|
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: ''
|
|
}
|
|
},
|
|
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 (datasetKey === 'rpc') {
|
|
const template = {}
|
|
const sampleValue = isPlainObject(sample) ? sample : {}
|
|
const functionMockResponse = isPlainObject(
|
|
functionMockValue?.response
|
|
)
|
|
? functionMockValue.response
|
|
: {}
|
|
const sampleResponse = isPlainObject(sampleValue.response)
|
|
? sampleValue.response
|
|
: {}
|
|
const [sampleCallName, sampleCallValue] = Object.entries(
|
|
sampleResponse
|
|
)[0] ||
|
|
Object.entries(functionMockResponse)[0] || ['some_rpc_call', {}]
|
|
const innerSample = isPlainObject(sampleCallValue)
|
|
? sampleCallValue
|
|
: {}
|
|
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(sampleValue, 'description')
|
|
) {
|
|
template.description = ''
|
|
}
|
|
|
|
template.response_type =
|
|
typeof sampleValue.response_type === 'string' &&
|
|
sampleValue.response_type.trim()
|
|
? sampleValue.response_type
|
|
: 'data'
|
|
|
|
template.response = {
|
|
[sampleCallName || 'some_rpc_call']: {
|
|
...(Object.prototype.hasOwnProperty.call(
|
|
innerSample,
|
|
'description'
|
|
)
|
|
? {description: ''}
|
|
: {}),
|
|
request_type:
|
|
typeof innerSample.request_type === 'string' &&
|
|
innerSample.request_type.trim()
|
|
? innerSample.request_type
|
|
: 'function',
|
|
...(Object.prototype.hasOwnProperty.call(
|
|
innerSample,
|
|
'request_data'
|
|
)
|
|
? {request_data: {}}
|
|
: {}),
|
|
response_type:
|
|
typeof innerSample.response_type === 'string' &&
|
|
innerSample.response_type.trim()
|
|
? innerSample.response_type
|
|
: 'data',
|
|
response: Array.isArray(innerSample.response)
|
|
? []
|
|
: innerSample.response === null
|
|
? null
|
|
: {}
|
|
}
|
|
}
|
|
|
|
return template
|
|
}
|
|
|
|
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
|
|
: items === undefined
|
|
? []
|
|
: [items]
|
|
const mode = this.functionMockModeByName(mockName)
|
|
|
|
return {
|
|
selectedMockName: mockName,
|
|
items: normalizedItems.map(item =>
|
|
createTestMockItemDraftState(item, mode)
|
|
)
|
|
}
|
|
},
|
|
testMockBindingScopeKey(mockSet) {
|
|
if (!mockSet?.path) {
|
|
return ''
|
|
}
|
|
|
|
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 => ({
|
|
label: name,
|
|
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
|
|
)
|
|
},
|
|
functionMockRowMode(row) {
|
|
return row?.requestType === 'function' ? 'rpc' : 'rest'
|
|
},
|
|
functionMockModeByName(mockName) {
|
|
const normalizedName = (mockName || '').trim()
|
|
if (!normalizedName) {
|
|
return this.selectedDatasetKey === 'rpc' ? 'rpc' : 'rest'
|
|
}
|
|
|
|
const draftRow = this.currentFunctionMockRows.find(
|
|
row => (row.name || '').trim() === normalizedName
|
|
)
|
|
if (draftRow) {
|
|
return this.functionMockRowMode(draftRow)
|
|
}
|
|
|
|
const mockValue =
|
|
this.datasets[this.selectedDatasetKey].data?.functions?.[
|
|
this.currentFunctionName
|
|
]?.mocks?.[this.currentTestFundingSourceName]?.[normalizedName] ||
|
|
{}
|
|
|
|
return mockValue?.request_type === 'function' ? 'rpc' : 'rest'
|
|
},
|
|
activeTestMockBindingMode(mockSet) {
|
|
return this.functionMockModeByName(
|
|
this.activeTestMockBindingName(mockSet)
|
|
)
|
|
},
|
|
nextAvailableTestMockName(mockSet) {
|
|
const usedNames = new Set(
|
|
(mockSet.bindings || [])
|
|
.map(binding => (binding.selectedMockName || '').trim())
|
|
.filter(Boolean)
|
|
)
|
|
|
|
const availableName = this.currentFunctionMockNameOptions.find(
|
|
option => !usedNames.has(option.value)
|
|
)
|
|
|
|
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
|
|
)
|
|
]
|
|
},
|
|
testMockItemHasSkip(item) {
|
|
return Boolean(item?.hasSkipField || item?.skip === true)
|
|
},
|
|
testMockItemSkip(item) {
|
|
return Boolean(item?.skip)
|
|
},
|
|
setTestMockItemSkip(testEntry, mockSet, item, value) {
|
|
item.error = ''
|
|
item.hasSkipField = true
|
|
item.skip = Boolean(value)
|
|
this.persistTestMockSet(testEntry, mockSet)
|
|
},
|
|
handleTestMockBindingSelection(testEntry, mockSet, value) {
|
|
const nextName = typeof value === 'string' ? value : ''
|
|
const scopeKey = this.testMockBindingScopeKey(mockSet)
|
|
if (!nextName || !scopeKey) {
|
|
return
|
|
}
|
|
|
|
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(
|
|
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) {
|
|
const functionMockValue =
|
|
this.datasets[this.selectedDatasetKey].data?.functions?.[
|
|
this.currentFunctionName
|
|
]?.mocks?.[this.currentTestFundingSourceName]?.[
|
|
binding.selectedMockName
|
|
] || {}
|
|
const mode = this.functionMockModeByName(binding.selectedMockName)
|
|
const template = this.createTestMockItemTemplate(
|
|
this.selectedDatasetKey,
|
|
this.currentFunctionName,
|
|
this.currentTestFundingSourceName,
|
|
functionMockValue
|
|
)
|
|
|
|
binding.items.push(createTestMockItemDraftState(template, mode))
|
|
this.persistTestMockSet(testEntry, mockSet)
|
|
},
|
|
removeTestMockBindingItem(testEntry, mockSet, binding, itemIndex) {
|
|
binding.items.splice(itemIndex, 1)
|
|
this.persistTestMockSet(testEntry, mockSet)
|
|
},
|
|
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.currentTestFundingSourceName
|
|
) {
|
|
return
|
|
}
|
|
|
|
this.ensureFunctionMockDraft(
|
|
this.selectedDatasetKey,
|
|
this.currentFunctionName,
|
|
this.currentTestFundingSourceName,
|
|
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 mode = this.functionMockRowMode(row)
|
|
|
|
const name = row.name.trim()
|
|
if (!name) {
|
|
throw new Error('Mock name cannot be empty.')
|
|
}
|
|
|
|
if (row.method.trim() !== '') {
|
|
mock.method = row.method
|
|
}
|
|
|
|
if (mode === 'rest') {
|
|
if (row.uri.trim() !== '') {
|
|
mock.uri = row.uri
|
|
}
|
|
|
|
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
|
|
}
|
|
} else {
|
|
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
|
|
},
|
|
functionMockMethodHint(row) {
|
|
if (row?.requestType === 'function') {
|
|
return 'Method: Fully qualified Python function or constructor path used by this RPC mock.'
|
|
}
|
|
|
|
return 'Method: HTTP verb used by this REST mock, such as GET or POST.'
|
|
},
|
|
handleFunctionMockRequestTypeChange(row, value) {
|
|
row.requestType = value === 'function' ? 'function' : ''
|
|
|
|
if (row.requestType === 'function') {
|
|
row.uri = ''
|
|
row.headersText = ''
|
|
row.queryParamsText = ''
|
|
} else {
|
|
row.responseType = ''
|
|
row.responseText = ''
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.persistFunctionMocks({silent: true})
|
|
})
|
|
},
|
|
handleFunctionMockResponseTypeChange(row, value) {
|
|
row.responseType = typeof value === 'string' ? value : ''
|
|
this.$nextTick(() => {
|
|
this.persistFunctionMocks({silent: true})
|
|
})
|
|
},
|
|
handleTestMockSelectChange(testEntry, mockSet, item, field, value) {
|
|
item[field] = typeof value === 'string' ? value : ''
|
|
this.$nextTick(() => {
|
|
this.persistTestMockSet(testEntry, mockSet, {silent: true})
|
|
})
|
|
},
|
|
rpcRequestDetailScopeKey(testEntry, mockSet, itemIndex) {
|
|
if (
|
|
!this.currentFunctionName ||
|
|
!this.currentTestFundingSourceName
|
|
) {
|
|
return ''
|
|
}
|
|
|
|
return [
|
|
this.selectedDatasetKey,
|
|
this.currentFunctionName,
|
|
this.currentTestFundingSourceName,
|
|
testEntry.index,
|
|
mockSet.setIndex,
|
|
this.activeTestMockBindingName(mockSet),
|
|
itemIndex
|
|
].join(':')
|
|
},
|
|
rpcRequestDetailTab(testEntry, mockSet, itemIndex) {
|
|
const scopeKey = this.rpcRequestDetailScopeKey(
|
|
testEntry,
|
|
mockSet,
|
|
itemIndex
|
|
)
|
|
const currentValue =
|
|
this.selectedRpcRequestDetailTabByScope[scopeKey]
|
|
|
|
return ['class', 'args', 'kwargs'].includes(currentValue)
|
|
? currentValue
|
|
: 'class'
|
|
},
|
|
setRpcRequestDetailTab(testEntry, mockSet, itemIndex, value) {
|
|
const scopeKey = this.rpcRequestDetailScopeKey(
|
|
testEntry,
|
|
mockSet,
|
|
itemIndex
|
|
)
|
|
if (!scopeKey) {
|
|
return
|
|
}
|
|
|
|
this.selectedRpcRequestDetailTabByScope[scopeKey] = [
|
|
'class',
|
|
'args',
|
|
'kwargs'
|
|
].includes(value)
|
|
? value
|
|
: 'class'
|
|
this.persistUiState()
|
|
},
|
|
parseEditableTestJson(text, label) {
|
|
const parsed = parseJsonText(text)
|
|
|
|
if (!parsed.ok) {
|
|
throw new Error(`${label}: ${parsed.error}`)
|
|
}
|
|
|
|
return parsed.data
|
|
},
|
|
persistTestCallParams(testEntry, options = {}) {
|
|
if (!this.currentFunctionName) {
|
|
return
|
|
}
|
|
|
|
testEntry.callParamsError = ''
|
|
|
|
let nextValue
|
|
try {
|
|
nextValue = this.parseEditableTestJson(
|
|
testEntry.callParamsText,
|
|
'Call Params'
|
|
)
|
|
} catch (error) {
|
|
testEntry.callParamsError =
|
|
error instanceof Error ? error.message : String(error)
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save call params.',
|
|
'negative',
|
|
testEntry.callParamsError
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
this.applyDatasetMutation(
|
|
this.selectedDatasetKey,
|
|
data => {
|
|
data.functions[this.currentFunctionName].tests[
|
|
testEntry.index
|
|
].call_params = nextValue
|
|
},
|
|
{
|
|
preserveSettingsDrafts: true,
|
|
preserveFunctionMockDrafts: true
|
|
}
|
|
)
|
|
},
|
|
persistTestExpected(testEntry, options = {}) {
|
|
if (!this.currentFunctionName) {
|
|
return
|
|
}
|
|
|
|
testEntry.expectedError = ''
|
|
|
|
let nextValue
|
|
try {
|
|
nextValue = this.parseEditableTestJson(
|
|
testEntry.expectedText,
|
|
testEntry.expectedLabel
|
|
)
|
|
} catch (error) {
|
|
testEntry.expectedError =
|
|
error instanceof Error ? error.message : String(error)
|
|
if (!options.silent) {
|
|
this.notify(
|
|
`Could not save ${testEntry.expectedLabel.toLowerCase()}.`,
|
|
'negative',
|
|
testEntry.expectedError
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
this.applyDatasetMutation(
|
|
this.selectedDatasetKey,
|
|
data => {
|
|
const rawTest =
|
|
data.functions[this.currentFunctionName].tests[
|
|
testEntry.index
|
|
]
|
|
rawTest[testEntry.expectedField || 'expect'] = nextValue
|
|
},
|
|
{
|
|
preserveSettingsDrafts: true,
|
|
preserveFunctionMockDrafts: true
|
|
}
|
|
)
|
|
},
|
|
persistTestMockSet(testEntry, mockSet, options = {}) {
|
|
if (
|
|
!this.currentFunctionName ||
|
|
!this.currentTestFundingSourceName
|
|
) {
|
|
return
|
|
}
|
|
|
|
mockSet.error = ''
|
|
;(mockSet.bindings || []).forEach(binding => {
|
|
;(binding.items || []).forEach(item => {
|
|
item.error = ''
|
|
item.requestBodyError = ''
|
|
item.requestArgsError = ''
|
|
item.requestKwargsError = ''
|
|
item.responseError = ''
|
|
})
|
|
})
|
|
|
|
const nextValue = deepClone(mockSet.metaFields || {})
|
|
|
|
for (const binding of mockSet.bindings || []) {
|
|
const mockName = (binding.selectedMockName || '').trim()
|
|
const bindingMode = this.functionMockModeByName(mockName)
|
|
|
|
if (!mockName) {
|
|
if ((binding.items || []).length) {
|
|
mockSet.error =
|
|
'Select a function mock before editing its overrides.'
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(nextValue, mockName)) {
|
|
mockSet.error = `Duplicate function mock selected: ${mockName}`
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
const parsedItems = []
|
|
|
|
for (
|
|
let itemIndex = 0;
|
|
itemIndex < (binding.items || []).length;
|
|
itemIndex += 1
|
|
) {
|
|
const item = binding.items[itemIndex]
|
|
const nextItem = {}
|
|
|
|
if (
|
|
bindingMode === 'rest' &&
|
|
(item.descriptionText || '').trim() !== ''
|
|
) {
|
|
nextItem.description = item.descriptionText.trim()
|
|
}
|
|
|
|
if (
|
|
bindingMode === 'rest' &&
|
|
(item.hasSkipField || item.skip === true)
|
|
) {
|
|
nextItem.skip = Boolean(item.skip)
|
|
}
|
|
|
|
if (
|
|
bindingMode === 'rest' &&
|
|
(item.requestType || '').trim() !== ''
|
|
) {
|
|
nextItem.request_type = item.requestType.trim()
|
|
}
|
|
|
|
if (
|
|
bindingMode === 'rest' &&
|
|
(item.requestBodyText || '').trim() !== ''
|
|
) {
|
|
try {
|
|
nextItem.request_body = this.parseEditableTestJson(
|
|
item.requestBodyText,
|
|
`${mockName} request_body ${itemIndex + 1}`
|
|
)
|
|
} catch (error) {
|
|
item.requestBodyError =
|
|
error instanceof Error ? error.message : String(error)
|
|
mockSet.error = item.requestBodyError
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if (
|
|
bindingMode === 'rest' &&
|
|
(item.responseType || '').trim() !== ''
|
|
) {
|
|
nextItem.response_type = item.responseType.trim()
|
|
}
|
|
|
|
if (
|
|
bindingMode === 'rest' &&
|
|
(item.responseText || '').trim() !== ''
|
|
) {
|
|
try {
|
|
nextItem.response = this.parseEditableTestJson(
|
|
item.responseText,
|
|
`${mockName} response ${itemIndex + 1}`
|
|
)
|
|
} catch (error) {
|
|
item.responseError =
|
|
error instanceof Error ? error.message : String(error)
|
|
mockSet.error = item.responseError
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if (bindingMode !== 'rest') {
|
|
if ((item.descriptionText || '').trim() !== '') {
|
|
nextItem.description = item.descriptionText.trim()
|
|
}
|
|
|
|
if ((item.responseType || '').trim() !== '') {
|
|
nextItem.response_type = item.responseType.trim()
|
|
}
|
|
|
|
const rpcCallName = (item.rpcCallName || '').trim()
|
|
const rpcCallHasValues = Boolean(
|
|
rpcCallName ||
|
|
(item.rpcDescriptionText || '').trim() ||
|
|
(item.requestType || '').trim() ||
|
|
(item.requestArgsText || '').trim() ||
|
|
(item.requestKwargsText || '').trim() ||
|
|
(item.requestClassName || '').trim() ||
|
|
(item.rpcResponseType || '').trim() ||
|
|
(item.responseText || '').trim()
|
|
)
|
|
|
|
if (!rpcCallName && rpcCallHasValues) {
|
|
item.error =
|
|
'Enter the RPC call name before editing its mock data.'
|
|
mockSet.error = item.error
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (rpcCallName) {
|
|
const rpcCall = {}
|
|
|
|
if ((item.rpcDescriptionText || '').trim() !== '') {
|
|
rpcCall.description = item.rpcDescriptionText.trim()
|
|
}
|
|
|
|
if ((item.requestType || '').trim() !== '') {
|
|
rpcCall.request_type = item.requestType.trim()
|
|
}
|
|
|
|
const requestData = isPlainObject(item.requestDataExtras)
|
|
? deepClone(item.requestDataExtras)
|
|
: {}
|
|
|
|
if ((item.requestArgsText || '').trim() !== '') {
|
|
try {
|
|
const parsedArgs = this.parseEditableTestJson(
|
|
item.requestArgsText,
|
|
`${mockName} request_data.args ${itemIndex + 1}`
|
|
)
|
|
|
|
if (!Array.isArray(parsedArgs)) {
|
|
throw new Error(
|
|
`${mockName} request_data.args ${
|
|
itemIndex + 1
|
|
}: expected a JSON array`
|
|
)
|
|
}
|
|
|
|
requestData.args = parsedArgs
|
|
} catch (error) {
|
|
item.requestArgsError =
|
|
error instanceof Error ? error.message : String(error)
|
|
mockSet.error = item.requestArgsError
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if ((item.requestKwargsText || '').trim() !== '') {
|
|
try {
|
|
const parsedKwargs = this.parseEditableTestJson(
|
|
item.requestKwargsText,
|
|
`${mockName} request_data.kwargs ${itemIndex + 1}`
|
|
)
|
|
|
|
if (!isPlainObject(parsedKwargs)) {
|
|
throw new Error(
|
|
`${mockName} request_data.kwargs ${
|
|
itemIndex + 1
|
|
}: expected a JSON object`
|
|
)
|
|
}
|
|
|
|
requestData.kwargs = parsedKwargs
|
|
} catch (error) {
|
|
item.requestKwargsError =
|
|
error instanceof Error ? error.message : String(error)
|
|
mockSet.error = item.requestKwargsError
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if ((item.requestClassName || '').trim() !== '') {
|
|
requestData.klass = item.requestClassName.trim()
|
|
}
|
|
|
|
if (Object.keys(requestData).length > 0) {
|
|
rpcCall.request_data = requestData
|
|
}
|
|
|
|
if ((item.rpcResponseType || '').trim() !== '') {
|
|
rpcCall.response_type = item.rpcResponseType.trim()
|
|
}
|
|
|
|
if ((item.responseText || '').trim() !== '') {
|
|
try {
|
|
rpcCall.response = this.parseEditableTestJson(
|
|
item.responseText,
|
|
`${mockName} response ${itemIndex + 1}`
|
|
)
|
|
} catch (error) {
|
|
item.responseError =
|
|
error instanceof Error ? error.message : String(error)
|
|
mockSet.error = item.responseError
|
|
if (!options.silent) {
|
|
this.notify(
|
|
'Could not save test mock definition.',
|
|
'negative',
|
|
mockSet.error
|
|
)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
nextItem.response = {
|
|
[rpcCallName]: rpcCall
|
|
}
|
|
}
|
|
}
|
|
|
|
parsedItems.push(nextItem)
|
|
}
|
|
|
|
nextValue[mockName] = parsedItems
|
|
}
|
|
|
|
this.applyDatasetMutation(
|
|
this.selectedDatasetKey,
|
|
data => {
|
|
const rawTest =
|
|
data.functions[this.currentFunctionName].tests[
|
|
testEntry.index
|
|
]
|
|
|
|
if (!rawTest.mocks) {
|
|
rawTest.mocks = {}
|
|
}
|
|
|
|
const walletMockValue =
|
|
rawTest.mocks[this.currentTestFundingSourceName]
|
|
|
|
if (
|
|
Array.isArray(walletMockValue) ||
|
|
mockSet.walletMockValueIsArray
|
|
) {
|
|
if (
|
|
!Array.isArray(
|
|
rawTest.mocks[this.currentTestFundingSourceName]
|
|
)
|
|
) {
|
|
rawTest.mocks[this.currentTestFundingSourceName] = []
|
|
}
|
|
|
|
rawTest.mocks[this.currentTestFundingSourceName][
|
|
mockSet.setIndex
|
|
] = nextValue
|
|
} else {
|
|
rawTest.mocks[this.currentTestFundingSourceName] = nextValue
|
|
}
|
|
},
|
|
{
|
|
preserveSettingsDrafts: true,
|
|
preserveFunctionMockDrafts: true
|
|
}
|
|
)
|
|
},
|
|
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(this.nextGeneratedSettingKey(), '')
|
|
)
|
|
this.persistFundingSourceSettings({silent: true})
|
|
},
|
|
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(options = {}) {
|
|
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)
|
|
if (!options.silent) {
|
|
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.currentTestFundingSourceName
|
|
) {
|
|
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(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
|
|
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
|
|
) {
|
|
return
|
|
}
|
|
|
|
const rows = this.currentFunctionMockRows
|
|
const nextMocks = {}
|
|
const renamePairs = []
|
|
|
|
for (const row of rows) {
|
|
row.error = ''
|
|
}
|
|
|
|
for (const row of rows) {
|
|
const name = row.name.trim()
|
|
const originalName = (row.originalName || '').trim()
|
|
|
|
if (!name && !originalName) {
|
|
continue
|
|
}
|
|
|
|
if (!name) {
|
|
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 {
|
|
nextMocks[name] = this.parseFunctionMockRowBody(row)
|
|
} catch (error) {
|
|
row.error =
|
|
error instanceof Error ? error.message : String(error)
|
|
if (!options.silent) {
|
|
this.notify(
|
|
`Could not save mock "${name || 'new mock'}".`,
|
|
'negative',
|
|
row.error
|
|
)
|
|
}
|
|
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,
|
|
preserveSettingsDrafts: true
|
|
}
|
|
)
|
|
|
|
rows.forEach(row => {
|
|
row.originalName = row.name.trim()
|
|
})
|
|
},
|
|
baseWalletPath(functionName, fundingSourceName) {
|
|
return `functions.${functionName}.mocks.${fundingSourceName}`
|
|
},
|
|
testWalletPath(functionName, testIndex, fundingSourceName) {
|
|
return `functions.${functionName}.tests[${testIndex}].mocks.${fundingSourceName}`
|
|
},
|
|
ensureSelections(datasetKey) {
|
|
const dataset = this.datasets[datasetKey]
|
|
const fundingSources = dataset.analysis?.fundingSources || []
|
|
const functions = dataset.analysis?.functions || []
|
|
|
|
if (!fundingSources.length) {
|
|
this.selectedFundingSourceByDataset[datasetKey] = ''
|
|
this.selectedTestFundingSourceByDataset[datasetKey] = ''
|
|
} else {
|
|
const currentFundingSourceName =
|
|
this.selectedFundingSourceByDataset[datasetKey]
|
|
const selectedFundingSource = fundingSources.find(
|
|
fs => fs.name === currentFundingSourceName
|
|
)
|
|
|
|
if (!selectedFundingSource) {
|
|
this.selectedFundingSourceByDataset[datasetKey] =
|
|
fundingSources[0].name
|
|
}
|
|
|
|
const currentTestFundingSourceName =
|
|
this.selectedTestFundingSourceByDataset[datasetKey]
|
|
const selectedTestFundingSource = fundingSources.find(
|
|
fs => fs.name === currentTestFundingSourceName
|
|
)
|
|
|
|
if (!selectedTestFundingSource) {
|
|
this.selectedTestFundingSourceByDataset[datasetKey] =
|
|
fundingSources[0].name
|
|
}
|
|
}
|
|
|
|
if (!functions.length) {
|
|
this.selectedFunctionByDataset[datasetKey] = ''
|
|
return
|
|
}
|
|
|
|
const currentFunctionName =
|
|
this.selectedFunctionByDataset[datasetKey]
|
|
const selectedFunction = functions.find(
|
|
fn => fn.name === currentFunctionName
|
|
)
|
|
|
|
if (!selectedFunction) {
|
|
this.selectedFunctionByDataset[datasetKey] = functions[0].name
|
|
}
|
|
},
|
|
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
|
|
}
|
|
|
|
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.selectedTestFundingSourceByDataset[datasetKey],
|
|
options.preserveFunctionMockDrafts !== true
|
|
)
|
|
}
|
|
}
|
|
|
|
this.persistDatasetToStorage(datasetKey)
|
|
this.persistUiState()
|
|
},
|
|
syncEditorFromRawText(datasetKey, options = {}) {
|
|
const dataset = this.datasets[datasetKey]
|
|
const parsed = parseJsonText(dataset.rawText)
|
|
|
|
if (!parsed.ok) {
|
|
dataset.parseError = `Invalid JSON in ${dataset.fileName}: ${parsed.error}`
|
|
if (!options.silent) {
|
|
this.notify('Could not apply the editor contents.', 'negative')
|
|
}
|
|
return false
|
|
}
|
|
|
|
this.commitDataset(datasetKey, parsed.data, {
|
|
rawText: dataset.rawText,
|
|
snapshotText: dataset.snapshotText,
|
|
fileHandle: dataset.fileHandle,
|
|
sourceLabel: dataset.sourceLabel,
|
|
loadError: dataset.loadError
|
|
})
|
|
|
|
return true
|
|
},
|
|
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]
|
|
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]
|
|
|
|
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 File” to pick it manually.'
|
|
|
|
if (!dataset.analysis) {
|
|
dataset.parseError = ''
|
|
}
|
|
|
|
console.error(error)
|
|
}
|
|
},
|
|
inferDatasetKey(fileName, data = null) {
|
|
const normalizedName = String(fileName || '').toLowerCase()
|
|
const exactMatch = DATASET_DEFS.find(
|
|
def => def.fileName.toLowerCase() === normalizedName
|
|
)
|
|
|
|
if (exactMatch) {
|
|
return exactMatch.key
|
|
}
|
|
|
|
if (
|
|
normalizedName.includes('fixtures_rest') ||
|
|
normalizedName.includes('_rest')
|
|
) {
|
|
return 'rest'
|
|
}
|
|
|
|
if (
|
|
normalizedName.includes('fixtures_rpc') ||
|
|
normalizedName.includes('_rpc')
|
|
) {
|
|
return 'rpc'
|
|
}
|
|
|
|
if (data) {
|
|
return inferDatasetKeyFromData(data)
|
|
}
|
|
|
|
return ''
|
|
},
|
|
openFilePickerOptions() {
|
|
const options = {
|
|
id: 'lnbits-wallet-fixture-studio',
|
|
multiple: false,
|
|
types: [
|
|
{
|
|
description: 'JSON fixture file',
|
|
accept: {
|
|
'application/json': ['.json']
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
const startIn =
|
|
this.currentDataset.fileHandle || this.lastFilePickerHandle
|
|
|
|
if (startIn) {
|
|
options.startIn = startIn
|
|
}
|
|
|
|
return options
|
|
},
|
|
async openFilePicker() {
|
|
if (window.showOpenFilePicker) {
|
|
try {
|
|
const [handle] = await window.showOpenFilePicker(
|
|
this.openFilePickerOptions()
|
|
)
|
|
|
|
if (!handle) {
|
|
return
|
|
}
|
|
|
|
const file = await handle.getFile()
|
|
await this.loadChosenDatasetFile(file, {
|
|
fileHandle: handle,
|
|
sourceLabel: `File picker: ${file.name}`
|
|
})
|
|
return
|
|
} catch (error) {
|
|
if (error && error.name !== 'AbortError') {
|
|
console.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.$refs.loadFileInput.value = ''
|
|
this.$refs.loadFileInput.click()
|
|
},
|
|
async handleSingleFileSelection(event) {
|
|
const file = event.target.files?.[0]
|
|
if (!file) {
|
|
return
|
|
}
|
|
|
|
await this.loadChosenDatasetFile(file, {
|
|
sourceLabel: `Manual file load: ${file.name}`
|
|
})
|
|
},
|
|
async loadChosenDatasetFile(file, options = {}) {
|
|
const text = await file.text()
|
|
const parsed = parseJsonText(text)
|
|
const datasetKey = this.inferDatasetKey(
|
|
file.name,
|
|
parsed.ok ? parsed.data : null
|
|
)
|
|
|
|
if (!datasetKey) {
|
|
this.notify(
|
|
'Could not determine whether that fixture file is REST or RPC.',
|
|
'negative',
|
|
'Use fixtures_rest.json or fixtures_rpc.json.'
|
|
)
|
|
return
|
|
}
|
|
|
|
if (!parsed.ok) {
|
|
this.datasets[datasetKey].parseError =
|
|
`Invalid JSON in ${file.name}: ${parsed.error}`
|
|
this.notify(`Could not read ${file.name}.`, 'negative')
|
|
return
|
|
}
|
|
|
|
this.clearPersistedFixtureState()
|
|
this.resetInMemoryFixtureState(datasetKey)
|
|
|
|
const formatted = JSON.stringify(parsed.data, null, 2)
|
|
this.commitDataset(datasetKey, parsed.data, {
|
|
rawText: formatted,
|
|
snapshotText: formatted,
|
|
fileHandle: options.fileHandle || null,
|
|
sourceLabel: options.sourceLabel || `Loaded from ${file.name}`,
|
|
loadError: ''
|
|
})
|
|
|
|
if (options.fileHandle) {
|
|
this.lastFilePickerHandle = options.fileHandle
|
|
}
|
|
|
|
this.notify(`Loaded ${file.name}.`, 'positive')
|
|
},
|
|
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 savePickerOptions = {
|
|
id: 'lnbits-wallet-fixture-studio',
|
|
suggestedName: dataset.fileName,
|
|
types: [
|
|
{
|
|
description: 'JSON fixture file',
|
|
accept: {
|
|
'application/json': ['.json']
|
|
}
|
|
}
|
|
]
|
|
}
|
|
const saveStartIn =
|
|
dataset.fileHandle || this.lastFilePickerHandle
|
|
|
|
if (saveStartIn) {
|
|
savePickerOptions.startIn = saveStartIn
|
|
}
|
|
|
|
const handle =
|
|
await window.showSaveFilePicker(savePickerOptions)
|
|
|
|
const writable = await handle.createWritable()
|
|
await writable.write(contents)
|
|
await writable.close()
|
|
|
|
dataset.fileHandle = handle
|
|
this.lastFilePickerHandle = 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>
|