@veolab/discoverylab 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2OGZX6C4.js +588 -0
- package/dist/chunk-43U6UYV7.js +590 -0
- package/dist/chunk-4H2E3K2G.js +7638 -0
- package/dist/chunk-4KLG6DDE.js +334 -0
- package/dist/chunk-4NNTRJOI.js +7791 -0
- package/dist/chunk-5F76VWME.js +6397 -0
- package/dist/chunk-5NEFN42O.js +7791 -0
- package/dist/chunk-63MEQ6UH.js +7673 -0
- package/dist/chunk-C7QUR7XX.js +6397 -0
- package/dist/chunk-GGJJUCFK.js +7160 -0
- package/dist/chunk-GLHOY3NN.js +7805 -0
- package/dist/chunk-GSWHWEYC.js +1346 -0
- package/dist/chunk-HDKEQOF5.js +7788 -0
- package/dist/chunk-HZGSWVVS.js +7111 -0
- package/dist/chunk-I6YD3QFM.js +500 -0
- package/dist/chunk-KV7KDJ43.js +7639 -0
- package/dist/chunk-L4SA5F5W.js +6397 -0
- package/dist/chunk-MJS2YKNR.js +6397 -0
- package/dist/chunk-NDBW6ELQ.js +7638 -0
- package/dist/chunk-P4S7ZY6G.js +7638 -0
- package/dist/chunk-PMTGGZ7R.js +6397 -0
- package/dist/chunk-PYUCY3U6.js +1340 -0
- package/dist/chunk-RDZDSOAL.js +7750 -0
- package/dist/chunk-SLNJEF32.js +91 -0
- package/dist/chunk-SR67SRIT.js +1336 -0
- package/dist/chunk-TAODYZ52.js +1393 -0
- package/dist/chunk-TBG76CYG.js +6395 -0
- package/dist/chunk-TJ3H23LL.js +362 -0
- package/dist/chunk-XIBF5LBD.js +6395 -0
- package/dist/chunk-XUKWS2CE.js +7805 -0
- package/dist/cli.js +6 -6
- package/dist/db-ADBEBNH6.js +35 -0
- package/dist/index.d.ts +170 -1
- package/dist/index.html +1019 -84
- package/dist/index.js +9 -7
- package/dist/playwright-ATDC4NYW.js +38 -0
- package/dist/playwright-E6EUFIJG.js +38 -0
- package/dist/playwright-R7Y5HREH.js +39 -0
- package/dist/server-2VKO76UK.js +14 -0
- package/dist/server-3BK2VFU7.js +13 -0
- package/dist/server-6IPHVUYT.js +14 -0
- package/dist/server-73P7M3QB.js +14 -0
- package/dist/server-BPVRW5LJ.js +14 -0
- package/dist/server-IOOZK4NP.js +14 -0
- package/dist/server-NPZN3FWO.js +14 -0
- package/dist/server-O5FIAHSY.js +14 -0
- package/dist/server-P27BZXBL.js +14 -0
- package/dist/server-S6B5WUBT.js +14 -0
- package/dist/server-SRYNSGSP.js +14 -0
- package/dist/server-X3TLP6DX.js +14 -0
- package/dist/server-ZBPQ33V6.js +14 -0
- package/dist/setup-AQX4JQVR.js +17 -0
- package/dist/tools-2KPB37GK.js +178 -0
- package/dist/tools-3H6IOWXV.js +178 -0
- package/dist/tools-BUVCUCRL.js +178 -0
- package/dist/tools-HDNODRS6.js +178 -0
- package/dist/tools-L6PKKQPY.js +179 -0
- package/dist/tools-N5N2IO7V.js +178 -0
- package/dist/tools-TLCKABUW.js +178 -0
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -1824,12 +1824,22 @@
|
|
|
1824
1824
|
justify-content: center;
|
|
1825
1825
|
padding-top: 15vh;
|
|
1826
1826
|
z-index: 1000;
|
|
1827
|
+
overscroll-behavior: contain;
|
|
1827
1828
|
}
|
|
1828
1829
|
|
|
1829
1830
|
.modal-overlay.active {
|
|
1830
1831
|
display: flex;
|
|
1831
1832
|
}
|
|
1832
1833
|
|
|
1834
|
+
.modal-overlay.code-modal-overlay {
|
|
1835
|
+
align-items: center;
|
|
1836
|
+
justify-content: center;
|
|
1837
|
+
padding: clamp(10px, 3vh, 24px);
|
|
1838
|
+
padding-top: clamp(10px, 3vh, 24px);
|
|
1839
|
+
background: rgba(4, 6, 10, 0.78);
|
|
1840
|
+
backdrop-filter: blur(2px);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1833
1843
|
.modal {
|
|
1834
1844
|
background: var(--bg-surface);
|
|
1835
1845
|
border: 1px solid var(--border);
|
|
@@ -2014,10 +2024,12 @@
|
|
|
2014
2024
|
max-width: 900px;
|
|
2015
2025
|
width: min(92vw, 900px);
|
|
2016
2026
|
max-height: 82vh;
|
|
2027
|
+
height: min(86vh, 860px);
|
|
2017
2028
|
display: flex;
|
|
2018
2029
|
flex-direction: column;
|
|
2019
2030
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(0, 0, 0, 0.02)), var(--bg-surface);
|
|
2020
2031
|
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
|
2032
|
+
overscroll-behavior: contain;
|
|
2021
2033
|
}
|
|
2022
2034
|
|
|
2023
2035
|
.code-modal .modal-header {
|
|
@@ -2053,12 +2065,21 @@
|
|
|
2053
2065
|
}
|
|
2054
2066
|
|
|
2055
2067
|
.code-modal-body {
|
|
2056
|
-
padding:
|
|
2068
|
+
padding: 12px 16px 0;
|
|
2057
2069
|
flex: 1;
|
|
2058
2070
|
overflow: hidden;
|
|
2059
2071
|
display: flex;
|
|
2060
2072
|
flex-direction: column;
|
|
2061
|
-
gap:
|
|
2073
|
+
gap: 10px;
|
|
2074
|
+
min-height: 0;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
.code-modal-panel {
|
|
2078
|
+
display: flex;
|
|
2079
|
+
flex-direction: column;
|
|
2080
|
+
flex: 1;
|
|
2081
|
+
min-height: 0;
|
|
2082
|
+
min-width: 0;
|
|
2062
2083
|
}
|
|
2063
2084
|
|
|
2064
2085
|
.code-editor {
|
|
@@ -2081,17 +2102,20 @@
|
|
|
2081
2102
|
.code-editor-container {
|
|
2082
2103
|
display: flex;
|
|
2083
2104
|
flex: 1;
|
|
2084
|
-
background:
|
|
2105
|
+
background: #070b11;
|
|
2085
2106
|
border: 1px solid var(--border);
|
|
2086
2107
|
border-radius: 10px;
|
|
2087
2108
|
overflow: hidden;
|
|
2088
2109
|
min-height: 320px;
|
|
2110
|
+
height: 100%;
|
|
2111
|
+
min-width: 0;
|
|
2112
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02);
|
|
2089
2113
|
}
|
|
2090
2114
|
|
|
2091
2115
|
.code-line-numbers {
|
|
2092
|
-
background:
|
|
2116
|
+
background: #0b1017;
|
|
2093
2117
|
color: var(--text-muted);
|
|
2094
|
-
padding: 14px 12px 14px 14px;
|
|
2118
|
+
padding: 14px 12px calc(14px + 4.8em) 14px;
|
|
2095
2119
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
2096
2120
|
font-size: 12px;
|
|
2097
2121
|
line-height: 1.6;
|
|
@@ -2099,16 +2123,16 @@
|
|
|
2099
2123
|
user-select: none;
|
|
2100
2124
|
border-right: 1px solid var(--border);
|
|
2101
2125
|
min-width: 45px;
|
|
2102
|
-
overflow: hidden;
|
|
2126
|
+
overflow-y: hidden;
|
|
2103
2127
|
white-space: pre;
|
|
2104
2128
|
}
|
|
2105
2129
|
|
|
2106
2130
|
.code-editor-textarea {
|
|
2107
2131
|
flex: 1;
|
|
2108
|
-
background:
|
|
2132
|
+
background: #070b11;
|
|
2109
2133
|
color: var(--text-primary);
|
|
2110
2134
|
border: none;
|
|
2111
|
-
padding: 14px 16px;
|
|
2135
|
+
padding: 14px 16px calc(14px + 4.8em) 16px;
|
|
2112
2136
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
2113
2137
|
font-size: 12px;
|
|
2114
2138
|
line-height: 1.6;
|
|
@@ -2116,12 +2140,22 @@
|
|
|
2116
2140
|
tab-size: 2;
|
|
2117
2141
|
outline: none;
|
|
2118
2142
|
overflow-y: auto;
|
|
2143
|
+
height: 100%;
|
|
2144
|
+
min-height: 0;
|
|
2145
|
+
box-sizing: border-box;
|
|
2146
|
+
scroll-padding-bottom: 4.8em;
|
|
2147
|
+
overscroll-behavior: contain;
|
|
2148
|
+
-webkit-overflow-scrolling: touch;
|
|
2119
2149
|
}
|
|
2120
2150
|
|
|
2121
2151
|
.code-editor-textarea:focus {
|
|
2122
2152
|
outline: none;
|
|
2123
2153
|
}
|
|
2124
2154
|
|
|
2155
|
+
.code-editor-textarea::selection {
|
|
2156
|
+
background: rgba(96, 165, 250, 0.28);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2125
2159
|
/* YAML Syntax Highlighting (via overlay) */
|
|
2126
2160
|
.yaml-key { color: #7dd3fc; }
|
|
2127
2161
|
.yaml-value { color: #fde68a; }
|
|
@@ -2140,6 +2174,16 @@
|
|
|
2140
2174
|
max-height: 180px;
|
|
2141
2175
|
overflow-y: auto;
|
|
2142
2176
|
color: #8b949e;
|
|
2177
|
+
overscroll-behavior: contain;
|
|
2178
|
+
-webkit-overflow-scrolling: touch;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.code-vars-table-wrap {
|
|
2182
|
+
overflow: auto;
|
|
2183
|
+
border: 1px solid var(--border);
|
|
2184
|
+
border-radius: 8px;
|
|
2185
|
+
overscroll-behavior: contain;
|
|
2186
|
+
-webkit-overflow-scrolling: touch;
|
|
2143
2187
|
}
|
|
2144
2188
|
|
|
2145
2189
|
.code-modal-output .output-success { color: #4ade80; }
|
|
@@ -2179,6 +2223,7 @@
|
|
|
2179
2223
|
@media (max-width: 720px) {
|
|
2180
2224
|
.code-modal {
|
|
2181
2225
|
max-height: 88vh;
|
|
2226
|
+
height: min(92vh, 920px);
|
|
2182
2227
|
}
|
|
2183
2228
|
|
|
2184
2229
|
.code-modal-footer {
|
|
@@ -7049,7 +7094,8 @@
|
|
|
7049
7094
|
<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px;">
|
|
7050
7095
|
<a href="https://ollama.ai" target="_blank" style="color: var(--accent);">Install Ollama</a> - Used for local text/coding and optional visual fallback. Recommended starting points:
|
|
7051
7096
|
<a href="https://ollama.com/library/qwen2.5-coder" target="_blank" style="color: var(--accent);">qwen2.5-coder</a> (scripts/text) and
|
|
7052
|
-
<a href="https://ollama.com/library/qwen2.5vl" target="_blank" style="color: var(--accent);">qwen2.5vl</a>
|
|
7097
|
+
<a href="https://ollama.com/library/qwen2.5vl" target="_blank" style="color: var(--accent);">qwen2.5vl</a> or
|
|
7098
|
+
<a href="https://ollama.com/library/gemma3" target="_blank" style="color: var(--accent);">gemma3</a> (for example <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code>) for screenshots/vision. You must download and test on your machine.
|
|
7053
7099
|
</div>
|
|
7054
7100
|
</div>
|
|
7055
7101
|
<div class="setting-item">
|
|
@@ -8324,7 +8370,7 @@
|
|
|
8324
8370
|
</div>
|
|
8325
8371
|
<div style="font-size: 10px; color: var(--text-muted); line-height: 1.4;">
|
|
8326
8372
|
Recommended: <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5-coder:7b</code> for scripts/text and
|
|
8327
|
-
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> for screenshots/vision.
|
|
8373
|
+
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code> for screenshots/vision.
|
|
8328
8374
|
</div>
|
|
8329
8375
|
${selectedTextModelAvailable ? '' : `
|
|
8330
8376
|
<div style="padding: 8px; background: rgba(245, 158, 11, 0.08); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; color: var(--warning); font-size: 11px;">
|
|
@@ -8338,12 +8384,12 @@
|
|
|
8338
8384
|
`}
|
|
8339
8385
|
${selectedVisionModelAvailable && !visionModelLooksCapable ? `
|
|
8340
8386
|
<div style="padding: 8px; background: rgba(245, 158, 11, 0.08); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; color: var(--warning); font-size: 11px;">
|
|
8341
|
-
Vision model "${selectedVisionModel}" may not support images well. A multimodal model like <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> is recommended.
|
|
8387
|
+
Vision model "${selectedVisionModel}" may not support images well. A multimodal model like <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code> is recommended.
|
|
8342
8388
|
</div>
|
|
8343
8389
|
` : ''}
|
|
8344
8390
|
${preferOllamaVisionForActionDetection && visionCapableModels.length === 0 ? `
|
|
8345
8391
|
<div style="padding: 8px; background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(239, 68, 68, 0.18); border-radius: 6px; color: var(--error); font-size: 11px;">
|
|
8346
|
-
"Prefer Ollama Vision" is enabled, but no installed Ollama models look multimodal/vision-capable. Install <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> (or another vision model).
|
|
8392
|
+
"Prefer Ollama Vision" is enabled, but no installed Ollama models look multimodal/vision-capable. Install <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code> (or another vision model).
|
|
8347
8393
|
</div>
|
|
8348
8394
|
` : ''}
|
|
8349
8395
|
</div>
|
|
@@ -8363,7 +8409,7 @@
|
|
|
8363
8409
|
<div style="color: var(--text-muted);">
|
|
8364
8410
|
Install and test models (examples):
|
|
8365
8411
|
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull qwen2.5-coder:7b</code> and
|
|
8366
|
-
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull qwen2.5vl:7b</code>
|
|
8412
|
+
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull qwen2.5vl:7b</code> (or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull gemma3:4b</code>)
|
|
8367
8413
|
</div>
|
|
8368
8414
|
</div>
|
|
8369
8415
|
`;
|
|
@@ -14860,7 +14906,7 @@
|
|
|
14860
14906
|
|
|
14861
14907
|
// If we have the session locally, generate code directly
|
|
14862
14908
|
if (recorderSession.actions && recorderSession.actions.length > 0) {
|
|
14863
|
-
showCodeModal(generateSpecCode(recorderSession));
|
|
14909
|
+
showCodeModal(generateSpecCode(recorderSession), 'typescript', recorderSession.id, 'web');
|
|
14864
14910
|
return;
|
|
14865
14911
|
}
|
|
14866
14912
|
|
|
@@ -14879,7 +14925,7 @@
|
|
|
14879
14925
|
}
|
|
14880
14926
|
|
|
14881
14927
|
// Show code in a modal
|
|
14882
|
-
showCodeModal(data.specCode || generateSpecCode(data.recording));
|
|
14928
|
+
showCodeModal(data.specCode || generateSpecCode(data.recording), 'typescript', recorderSession.id, 'web');
|
|
14883
14929
|
|
|
14884
14930
|
} catch (error) {
|
|
14885
14931
|
showToast('Failed to load spec code', 'error');
|
|
@@ -16421,20 +16467,557 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16421
16467
|
}
|
|
16422
16468
|
}
|
|
16423
16469
|
|
|
16470
|
+
let codeModalState = null;
|
|
16471
|
+
let codeModalScrollLock = null;
|
|
16472
|
+
const CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY = 'discoverylab.codeModal.mobileRunDeviceId';
|
|
16473
|
+
|
|
16474
|
+
function lockPageScrollForCodeModal() {
|
|
16475
|
+
if (codeModalScrollLock) return;
|
|
16476
|
+
const body = document.body;
|
|
16477
|
+
const docEl = document.documentElement;
|
|
16478
|
+
const scrollX = window.scrollX || docEl.scrollLeft || 0;
|
|
16479
|
+
const scrollY = window.scrollY || docEl.scrollTop || 0;
|
|
16480
|
+
|
|
16481
|
+
codeModalScrollLock = {
|
|
16482
|
+
scrollX,
|
|
16483
|
+
scrollY,
|
|
16484
|
+
body: {
|
|
16485
|
+
position: body.style.position,
|
|
16486
|
+
top: body.style.top,
|
|
16487
|
+
left: body.style.left,
|
|
16488
|
+
right: body.style.right,
|
|
16489
|
+
width: body.style.width,
|
|
16490
|
+
overflow: body.style.overflow
|
|
16491
|
+
},
|
|
16492
|
+
htmlOverflow: docEl.style.overflow
|
|
16493
|
+
};
|
|
16494
|
+
|
|
16495
|
+
docEl.style.overflow = 'hidden';
|
|
16496
|
+
body.style.overflow = 'hidden';
|
|
16497
|
+
body.style.position = 'fixed';
|
|
16498
|
+
body.style.top = `-${scrollY}px`;
|
|
16499
|
+
body.style.left = `-${scrollX}px`;
|
|
16500
|
+
body.style.right = '0';
|
|
16501
|
+
body.style.width = '100%';
|
|
16502
|
+
}
|
|
16503
|
+
|
|
16504
|
+
function unlockPageScrollForCodeModal() {
|
|
16505
|
+
if (!codeModalScrollLock) return;
|
|
16506
|
+
const body = document.body;
|
|
16507
|
+
const docEl = document.documentElement;
|
|
16508
|
+
const lock = codeModalScrollLock;
|
|
16509
|
+
|
|
16510
|
+
docEl.style.overflow = lock.htmlOverflow || '';
|
|
16511
|
+
body.style.position = lock.body.position || '';
|
|
16512
|
+
body.style.top = lock.body.top || '';
|
|
16513
|
+
body.style.left = lock.body.left || '';
|
|
16514
|
+
body.style.right = lock.body.right || '';
|
|
16515
|
+
body.style.width = lock.body.width || '';
|
|
16516
|
+
body.style.overflow = lock.body.overflow || '';
|
|
16517
|
+
|
|
16518
|
+
window.scrollTo(lock.scrollX || 0, lock.scrollY || 0);
|
|
16519
|
+
codeModalScrollLock = null;
|
|
16520
|
+
}
|
|
16521
|
+
|
|
16522
|
+
function getPreferredCodeModalMobileDeviceId() {
|
|
16523
|
+
if (codeModalState?.mobileRunSelectedDeviceId) return codeModalState.mobileRunSelectedDeviceId;
|
|
16524
|
+
try {
|
|
16525
|
+
const fromStorage = localStorage.getItem(CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY);
|
|
16526
|
+
if (fromStorage) return fromStorage;
|
|
16527
|
+
} catch {}
|
|
16528
|
+
const deviceDropdown = document.getElementById('deviceDropdown');
|
|
16529
|
+
if (deviceDropdown && deviceDropdown.value) return deviceDropdown.value;
|
|
16530
|
+
return '';
|
|
16531
|
+
}
|
|
16532
|
+
|
|
16533
|
+
function formatCodeModalMobileDeviceOptionLabel(device) {
|
|
16534
|
+
const platform = String(device?.platform || '').toUpperCase() || 'MOBILE';
|
|
16535
|
+
const name = String(device?.name || device?.id || 'Unknown device');
|
|
16536
|
+
const status = String(device?.status || '').toLowerCase();
|
|
16537
|
+
return status ? `${platform} · ${name} (${status})` : `${platform} · ${name}`;
|
|
16538
|
+
}
|
|
16539
|
+
|
|
16540
|
+
function renderCodeModalMobileDevicePicker() {
|
|
16541
|
+
const selectEl = document.getElementById('codeModalMobileDeviceSelect');
|
|
16542
|
+
const statusEl = document.getElementById('codeModalMobileDeviceStatus');
|
|
16543
|
+
if (!selectEl || !statusEl || !codeModalState) return;
|
|
16544
|
+
|
|
16545
|
+
const devices = Array.isArray(codeModalState.mobileRunDevices) ? codeModalState.mobileRunDevices : [];
|
|
16546
|
+
const loading = codeModalState.mobileRunDevicesLoading === true;
|
|
16547
|
+
const selectedId = codeModalState.mobileRunSelectedDeviceId || '';
|
|
16548
|
+
|
|
16549
|
+
const options = ['<option value="">Auto-detect device (Maestro default)</option>'];
|
|
16550
|
+
for (const device of devices) {
|
|
16551
|
+
const id = String(device?.id || '').trim();
|
|
16552
|
+
if (!id) continue;
|
|
16553
|
+
const selectedAttr = selectedId === id ? ' selected' : '';
|
|
16554
|
+
options.push(`<option value="${escapeAttr(id)}"${selectedAttr}>${escapeHtml(formatCodeModalMobileDeviceOptionLabel(device))}</option>`);
|
|
16555
|
+
}
|
|
16556
|
+
selectEl.innerHTML = options.join('');
|
|
16557
|
+
|
|
16558
|
+
if (loading) {
|
|
16559
|
+
statusEl.textContent = 'Loading devices...';
|
|
16560
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
16561
|
+
} else if (devices.length === 0) {
|
|
16562
|
+
statusEl.textContent = 'No connected/booted devices found';
|
|
16563
|
+
statusEl.style.color = 'var(--warning)';
|
|
16564
|
+
} else if (selectedId) {
|
|
16565
|
+
const selected = devices.find(d => String(d.id || '') === selectedId);
|
|
16566
|
+
statusEl.textContent = selected ? `Selected: ${selected.name}` : 'Selected device unavailable (using auto)';
|
|
16567
|
+
statusEl.style.color = selected ? 'var(--success)' : 'var(--warning)';
|
|
16568
|
+
} else {
|
|
16569
|
+
statusEl.textContent = 'Using auto-detect unless a device is selected';
|
|
16570
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
16571
|
+
}
|
|
16572
|
+
}
|
|
16573
|
+
|
|
16574
|
+
async function loadCodeModalMobileRunDevices(forceRefresh = false) {
|
|
16575
|
+
if (!codeModalState || codeModalState.type !== 'mobile') return;
|
|
16576
|
+
codeModalState.mobileRunDevicesLoading = true;
|
|
16577
|
+
renderCodeModalMobileDevicePicker();
|
|
16578
|
+
try {
|
|
16579
|
+
const url = forceRefresh
|
|
16580
|
+
? '/api/testing/mobile/maestro-devices?refresh=1'
|
|
16581
|
+
: '/api/testing/mobile/maestro-devices';
|
|
16582
|
+
const response = await fetch(url);
|
|
16583
|
+
const data = await response.json();
|
|
16584
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load devices');
|
|
16585
|
+
const devices = Array.isArray(data.devices) ? data.devices : [];
|
|
16586
|
+
codeModalState.mobileRunDevices = devices;
|
|
16587
|
+
|
|
16588
|
+
const preferred = codeModalState.mobileRunSelectedDeviceId || getPreferredCodeModalMobileDeviceId();
|
|
16589
|
+
if (preferred && devices.some(d => String(d.id || '') === preferred)) {
|
|
16590
|
+
codeModalState.mobileRunSelectedDeviceId = preferred;
|
|
16591
|
+
} else {
|
|
16592
|
+
codeModalState.mobileRunSelectedDeviceId = '';
|
|
16593
|
+
}
|
|
16594
|
+
renderCodeModalMobileDevicePicker();
|
|
16595
|
+
} catch (error) {
|
|
16596
|
+
console.error('Failed to load Maestro devices for code modal:', error);
|
|
16597
|
+
codeModalState.mobileRunDevices = [];
|
|
16598
|
+
codeModalState.mobileRunSelectedDeviceId = '';
|
|
16599
|
+
renderCodeModalMobileDevicePicker();
|
|
16600
|
+
const statusEl = document.getElementById('codeModalMobileDeviceStatus');
|
|
16601
|
+
if (statusEl) {
|
|
16602
|
+
statusEl.textContent = `Failed to load devices: ${error.message || 'Unknown error'}`;
|
|
16603
|
+
statusEl.style.color = 'var(--error)';
|
|
16604
|
+
}
|
|
16605
|
+
} finally {
|
|
16606
|
+
if (codeModalState) codeModalState.mobileRunDevicesLoading = false;
|
|
16607
|
+
renderCodeModalMobileDevicePicker();
|
|
16608
|
+
}
|
|
16609
|
+
}
|
|
16610
|
+
|
|
16611
|
+
function onCodeModalMobileDeviceChanged(value) {
|
|
16612
|
+
if (!codeModalState) return;
|
|
16613
|
+
const normalized = String(value || '').trim();
|
|
16614
|
+
codeModalState.mobileRunSelectedDeviceId = normalized;
|
|
16615
|
+
try {
|
|
16616
|
+
if (normalized) localStorage.setItem(CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY, normalized);
|
|
16617
|
+
else localStorage.removeItem(CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY);
|
|
16618
|
+
} catch {}
|
|
16619
|
+
renderCodeModalMobileDevicePicker();
|
|
16620
|
+
}
|
|
16621
|
+
|
|
16622
|
+
function findScrollableAncestorInModal(target, modalRoot) {
|
|
16623
|
+
let el = target instanceof Element ? target : null;
|
|
16624
|
+
while (el && el !== modalRoot) {
|
|
16625
|
+
const style = window.getComputedStyle(el);
|
|
16626
|
+
const overflowY = style.overflowY || style.overflow;
|
|
16627
|
+
const overflowX = style.overflowX || style.overflow;
|
|
16628
|
+
const canScrollY = /(auto|scroll)/.test(overflowY) && el.scrollHeight > (el.clientHeight + 1);
|
|
16629
|
+
const canScrollX = /(auto|scroll)/.test(overflowX) && el.scrollWidth > (el.clientWidth + 1);
|
|
16630
|
+
if (canScrollY || canScrollX) return el;
|
|
16631
|
+
el = el.parentElement;
|
|
16632
|
+
}
|
|
16633
|
+
return null;
|
|
16634
|
+
}
|
|
16635
|
+
|
|
16636
|
+
function attachCodeModalScrollTrap(modalEl) {
|
|
16637
|
+
if (!modalEl) return;
|
|
16638
|
+
const wheelHandler = (e) => {
|
|
16639
|
+
if (!modalEl.contains(e.target)) return;
|
|
16640
|
+
const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
|
|
16641
|
+
const firstElementInPath = path.find((node) => node instanceof Element);
|
|
16642
|
+
const targetEl = (e.target instanceof Element ? e.target : null) || (firstElementInPath instanceof Element ? firstElementInPath : null);
|
|
16643
|
+
|
|
16644
|
+
// Only intercept wheel when cursor is on the line-number gutter.
|
|
16645
|
+
// Everything else should use native scrolling in Chrome (textarea/vars/output).
|
|
16646
|
+
if (targetEl?.closest('#codeLineNumbers')) {
|
|
16647
|
+
const textarea = modalEl.querySelector('#codeEditorTextarea');
|
|
16648
|
+
if (textarea) {
|
|
16649
|
+
textarea.scrollBy({
|
|
16650
|
+
top: e.deltaY || 0,
|
|
16651
|
+
left: e.deltaX || 0,
|
|
16652
|
+
behavior: 'auto'
|
|
16653
|
+
});
|
|
16654
|
+
}
|
|
16655
|
+
e.preventDefault();
|
|
16656
|
+
e.stopPropagation();
|
|
16657
|
+
return;
|
|
16658
|
+
}
|
|
16659
|
+
};
|
|
16660
|
+
|
|
16661
|
+
modalEl.addEventListener('wheel', wheelHandler, { passive: false, capture: true });
|
|
16662
|
+
modalEl._wheelTrapHandler = wheelHandler;
|
|
16663
|
+
}
|
|
16664
|
+
|
|
16665
|
+
function getCodeModalOwnerType(type) {
|
|
16666
|
+
return type === 'mobile' ? 'mobile-recording' : 'web-recording';
|
|
16667
|
+
}
|
|
16668
|
+
|
|
16669
|
+
function extractCodeModalPlaceholders(code) {
|
|
16670
|
+
const matches = new Set();
|
|
16671
|
+
const regex = /\$\{([A-Z][A-Z0-9_]*)\}/g;
|
|
16672
|
+
let match;
|
|
16673
|
+
const text = String(code || '');
|
|
16674
|
+
while ((match = regex.exec(text)) !== null) {
|
|
16675
|
+
if (match[1]) matches.add(match[1]);
|
|
16676
|
+
}
|
|
16677
|
+
return Array.from(matches).sort();
|
|
16678
|
+
}
|
|
16679
|
+
|
|
16680
|
+
function escapeAttr(value) {
|
|
16681
|
+
return String(value ?? '')
|
|
16682
|
+
.replace(/&/g, '&')
|
|
16683
|
+
.replace(/"/g, '"')
|
|
16684
|
+
.replace(/</g, '<')
|
|
16685
|
+
.replace(/>/g, '>');
|
|
16686
|
+
}
|
|
16687
|
+
|
|
16688
|
+
function parseEnvTestTextClient(rawText) {
|
|
16689
|
+
const rows = [];
|
|
16690
|
+
const lines = String(rawText || '').split(/\r?\n/);
|
|
16691
|
+
let pendingNotes = [];
|
|
16692
|
+
let pendingPlatform = 'both';
|
|
16693
|
+
for (const line of lines) {
|
|
16694
|
+
const trimmed = line.trim();
|
|
16695
|
+
if (!trimmed) {
|
|
16696
|
+
pendingNotes = [];
|
|
16697
|
+
pendingPlatform = 'both';
|
|
16698
|
+
continue;
|
|
16699
|
+
}
|
|
16700
|
+
if (trimmed.startsWith('#')) {
|
|
16701
|
+
const comment = trimmed.slice(1).trim();
|
|
16702
|
+
const m = comment.match(/^platform:\s*(mobile|web|both)$/i);
|
|
16703
|
+
if (m) {
|
|
16704
|
+
pendingPlatform = m[1].toLowerCase();
|
|
16705
|
+
} else if (comment) {
|
|
16706
|
+
pendingNotes.push(comment);
|
|
16707
|
+
}
|
|
16708
|
+
continue;
|
|
16709
|
+
}
|
|
16710
|
+
const eqIndex = line.indexOf('=');
|
|
16711
|
+
if (eqIndex <= 0) continue;
|
|
16712
|
+
const key = line.slice(0, eqIndex).trim().toUpperCase();
|
|
16713
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(key)) continue;
|
|
16714
|
+
rows.push({
|
|
16715
|
+
id: 'var_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
|
|
16716
|
+
key,
|
|
16717
|
+
value: line.slice(eqIndex + 1).replace(/\\n/g, '\n'),
|
|
16718
|
+
isSecret: /PASSWORD|SECRET|TOKEN|KEY|OTP|PIN/.test(key),
|
|
16719
|
+
platform: ['mobile', 'web', 'both'].includes(pendingPlatform) ? pendingPlatform : 'both',
|
|
16720
|
+
notes: pendingNotes.join(' ') || ''
|
|
16721
|
+
});
|
|
16722
|
+
pendingNotes = [];
|
|
16723
|
+
pendingPlatform = 'both';
|
|
16724
|
+
}
|
|
16725
|
+
return rows;
|
|
16726
|
+
}
|
|
16727
|
+
|
|
16728
|
+
function renderEnvTestTextClient(rows) {
|
|
16729
|
+
const lines = [];
|
|
16730
|
+
for (const row of (rows || [])) {
|
|
16731
|
+
const key = String(row.key || '').trim().toUpperCase();
|
|
16732
|
+
if (!key) continue;
|
|
16733
|
+
if (row.notes) lines.push(`# ${String(row.notes).trim()}`);
|
|
16734
|
+
if (row.platform && row.platform !== 'both') lines.push(`# platform: ${row.platform}`);
|
|
16735
|
+
lines.push(`${key}=${String(row.value || '').replace(/\n/g, '\\n')}`);
|
|
16736
|
+
lines.push('');
|
|
16737
|
+
}
|
|
16738
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
16739
|
+
}
|
|
16740
|
+
|
|
16741
|
+
function collectCodeModalVariablesFromTable() {
|
|
16742
|
+
const rows = Array.from(document.querySelectorAll('#codeModalVarsTableBody tr[data-var-row]'));
|
|
16743
|
+
return rows.map((tr) => ({
|
|
16744
|
+
id: tr.dataset.id || '',
|
|
16745
|
+
key: (tr.querySelector('[data-field="key"]')?.value || '').trim().toUpperCase(),
|
|
16746
|
+
value: tr.querySelector('[data-field="value"]')?.value || '',
|
|
16747
|
+
isSecret: tr.querySelector('[data-field="isSecret"]')?.checked === true,
|
|
16748
|
+
platform: tr.querySelector('[data-field="platform"]')?.value || 'both',
|
|
16749
|
+
notes: (tr.querySelector('[data-field="notes"]')?.value || '').trim(),
|
|
16750
|
+
})).filter((row) => row.key);
|
|
16751
|
+
}
|
|
16752
|
+
|
|
16753
|
+
function getCodeModalVariablesSignature(rows) {
|
|
16754
|
+
return JSON.stringify((rows || []).map((row) => ({
|
|
16755
|
+
key: String(row.key || '').trim().toUpperCase(),
|
|
16756
|
+
value: String(row.value || ''),
|
|
16757
|
+
isSecret: row.isSecret === true,
|
|
16758
|
+
platform: ['mobile', 'web', 'both'].includes(row.platform) ? row.platform : 'both',
|
|
16759
|
+
notes: String(row.notes || '').trim(),
|
|
16760
|
+
})));
|
|
16761
|
+
}
|
|
16762
|
+
|
|
16763
|
+
function syncCodeModalEnvFromTable() {
|
|
16764
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16765
|
+
if (!envTextarea) return;
|
|
16766
|
+
const rows = collectCodeModalVariablesFromTable();
|
|
16767
|
+
envTextarea.value = renderEnvTestTextClient(rows);
|
|
16768
|
+
if (codeModalState) codeModalState.variables = rows;
|
|
16769
|
+
updateCodeModalVariablesStatus();
|
|
16770
|
+
}
|
|
16771
|
+
|
|
16772
|
+
function updateCodeModalVariablesStatus() {
|
|
16773
|
+
if (!codeModalState) return;
|
|
16774
|
+
const codeText = document.getElementById('codeEditorTextarea')?.value || codeModalState.code || '';
|
|
16775
|
+
const placeholders = extractCodeModalPlaceholders(codeText);
|
|
16776
|
+
codeModalState.placeholders = placeholders;
|
|
16777
|
+
const rows = collectCodeModalVariablesFromTable();
|
|
16778
|
+
const activePlatform = codeModalState.type === 'mobile' ? 'mobile' : 'web';
|
|
16779
|
+
const scoped = rows.filter((row) => (row.platform || 'both') === 'both' || row.platform === activePlatform);
|
|
16780
|
+
const rowKeys = new Set(scoped.map((row) => String(row.key || '').trim().toUpperCase()).filter(Boolean));
|
|
16781
|
+
const missing = placeholders.filter((key) => !rowKeys.has(key));
|
|
16782
|
+
const unused = scoped.map((row) => row.key).filter((key) => key && !placeholders.includes(key));
|
|
16783
|
+
codeModalState.missingPlaceholders = missing;
|
|
16784
|
+
|
|
16785
|
+
const summaryEl = document.getElementById('codeModalVarSummary');
|
|
16786
|
+
if (summaryEl) {
|
|
16787
|
+
summaryEl.innerHTML = `
|
|
16788
|
+
<span>${placeholders.length} placeholder${placeholders.length === 1 ? '' : 's'} in script</span>
|
|
16789
|
+
<span style="color:${missing.length ? 'var(--warning)' : 'var(--success)'};">${missing.length} missing</span>
|
|
16790
|
+
<span>${unused.length} unused</span>
|
|
16791
|
+
`;
|
|
16792
|
+
}
|
|
16793
|
+
|
|
16794
|
+
const badgeEl = document.getElementById('codeModalVarsTabBadge');
|
|
16795
|
+
if (badgeEl) {
|
|
16796
|
+
badgeEl.textContent = missing.length > 0 ? `${missing.length} missing` : 'Ready';
|
|
16797
|
+
badgeEl.style.color = missing.length > 0 ? 'var(--warning)' : 'var(--success)';
|
|
16798
|
+
}
|
|
16799
|
+
|
|
16800
|
+
const missingEl = document.getElementById('codeModalMissingList');
|
|
16801
|
+
if (missingEl) {
|
|
16802
|
+
missingEl.textContent = missing.length > 0 ? `Missing: ${missing.join(', ')}` : 'All placeholders have values for this platform.';
|
|
16803
|
+
missingEl.style.color = missing.length > 0 ? 'var(--warning)' : 'var(--text-muted)';
|
|
16804
|
+
}
|
|
16805
|
+
}
|
|
16806
|
+
|
|
16807
|
+
function renderCodeModalVariablesTable() {
|
|
16808
|
+
if (!codeModalState) return;
|
|
16809
|
+
const body = document.getElementById('codeModalVarsTableBody');
|
|
16810
|
+
if (!body) return;
|
|
16811
|
+
const rows = Array.isArray(codeModalState.variables) ? codeModalState.variables : [];
|
|
16812
|
+
body.innerHTML = rows.length > 0 ? rows.map((row, index) => `
|
|
16813
|
+
<tr data-var-row data-id="${escapeAttr(row.id || '')}">
|
|
16814
|
+
<td><input data-field="key" class="setting-input" style="min-width: 120px;" value="${escapeAttr(row.key || '')}" placeholder="PASSWORD"></td>
|
|
16815
|
+
<td><input data-field="value" type="${row.isSecret ? 'password' : 'text'}" class="setting-input" style="min-width: 180px;" value="${escapeAttr(row.value || '')}" placeholder="value"></td>
|
|
16816
|
+
<td>
|
|
16817
|
+
<select data-field="platform" class="setting-input" style="min-width: 90px;">
|
|
16818
|
+
<option value="both" ${row.platform === 'both' ? 'selected' : ''}>both</option>
|
|
16819
|
+
<option value="mobile" ${row.platform === 'mobile' ? 'selected' : ''}>mobile</option>
|
|
16820
|
+
<option value="web" ${row.platform === 'web' ? 'selected' : ''}>web</option>
|
|
16821
|
+
</select>
|
|
16822
|
+
</td>
|
|
16823
|
+
<td style="text-align: center;">
|
|
16824
|
+
<input data-field="isSecret" type="checkbox" ${row.isSecret ? 'checked' : ''} onchange="toggleCodeModalVarSecret(${index}, this.checked)">
|
|
16825
|
+
</td>
|
|
16826
|
+
<td><input data-field="notes" class="setting-input" style="min-width: 160px;" value="${escapeAttr(row.notes || '')}" placeholder="optional note"></td>
|
|
16827
|
+
<td style="text-align: right;">
|
|
16828
|
+
<button class="btn btn-secondary" style="padding: 6px 8px;" onclick="removeCodeModalVariableRow(${index})">Remove</button>
|
|
16829
|
+
</td>
|
|
16830
|
+
</tr>
|
|
16831
|
+
`).join('') : `<tr><td colspan="6" style="color: var(--text-muted); padding: 12px; text-align: center;">No variables saved yet</td></tr>`;
|
|
16832
|
+
|
|
16833
|
+
body.querySelectorAll('input, select').forEach((el) => {
|
|
16834
|
+
el.addEventListener('input', () => syncCodeModalEnvFromTable());
|
|
16835
|
+
el.addEventListener('change', () => syncCodeModalEnvFromTable());
|
|
16836
|
+
});
|
|
16837
|
+
updateCodeModalVariablesStatus();
|
|
16838
|
+
}
|
|
16839
|
+
|
|
16840
|
+
function addCodeModalVariableRow(defaults = {}) {
|
|
16841
|
+
if (!codeModalState) return;
|
|
16842
|
+
codeModalState.variables = Array.isArray(codeModalState.variables) ? codeModalState.variables : [];
|
|
16843
|
+
codeModalState.variables.push({
|
|
16844
|
+
id: defaults.id || ('var_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7)),
|
|
16845
|
+
key: (defaults.key || '').toUpperCase(),
|
|
16846
|
+
value: defaults.value || '',
|
|
16847
|
+
isSecret: defaults.isSecret !== false,
|
|
16848
|
+
platform: defaults.platform || 'both',
|
|
16849
|
+
notes: defaults.notes || ''
|
|
16850
|
+
});
|
|
16851
|
+
renderCodeModalVariablesTable();
|
|
16852
|
+
syncCodeModalEnvFromTable();
|
|
16853
|
+
}
|
|
16854
|
+
|
|
16855
|
+
function removeCodeModalVariableRow(index) {
|
|
16856
|
+
if (!codeModalState || !Array.isArray(codeModalState.variables)) return;
|
|
16857
|
+
codeModalState.variables.splice(index, 1);
|
|
16858
|
+
renderCodeModalVariablesTable();
|
|
16859
|
+
syncCodeModalEnvFromTable();
|
|
16860
|
+
}
|
|
16861
|
+
|
|
16862
|
+
function toggleCodeModalVarSecret(index, checked) {
|
|
16863
|
+
if (!codeModalState || !Array.isArray(codeModalState.variables) || !codeModalState.variables[index]) return;
|
|
16864
|
+
codeModalState.variables[index].isSecret = !!checked;
|
|
16865
|
+
renderCodeModalVariablesTable();
|
|
16866
|
+
}
|
|
16867
|
+
|
|
16868
|
+
function addMissingCodeModalVariablesFromScript() {
|
|
16869
|
+
if (!codeModalState) return;
|
|
16870
|
+
const codeText = document.getElementById('codeEditorTextarea')?.value || '';
|
|
16871
|
+
const placeholders = extractCodeModalPlaceholders(codeText);
|
|
16872
|
+
const existing = new Set((codeModalState.variables || []).map((row) => String(row.key || '').toUpperCase()));
|
|
16873
|
+
let added = 0;
|
|
16874
|
+
placeholders.forEach((key) => {
|
|
16875
|
+
if (existing.has(key)) return;
|
|
16876
|
+
addCodeModalVariableRow({
|
|
16877
|
+
key,
|
|
16878
|
+
value: '',
|
|
16879
|
+
isSecret: /PASSWORD|SECRET|TOKEN|KEY|OTP|PIN/.test(key),
|
|
16880
|
+
platform: 'both',
|
|
16881
|
+
notes: ''
|
|
16882
|
+
});
|
|
16883
|
+
existing.add(key);
|
|
16884
|
+
added += 1;
|
|
16885
|
+
});
|
|
16886
|
+
if (added === 0) {
|
|
16887
|
+
showToast('No new placeholders to add', 'info');
|
|
16888
|
+
} else {
|
|
16889
|
+
showToast(`${added} variable${added === 1 ? '' : 's'} added from script`, 'success');
|
|
16890
|
+
}
|
|
16891
|
+
}
|
|
16892
|
+
|
|
16893
|
+
function applyEnvTestToCodeModalTable() {
|
|
16894
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16895
|
+
if (!envTextarea || !codeModalState) return;
|
|
16896
|
+
const parsed = parseEnvTestTextClient(envTextarea.value);
|
|
16897
|
+
const previousByKey = new Map((codeModalState.variables || []).map((row) => [String(row.key || '').toUpperCase(), row]));
|
|
16898
|
+
codeModalState.variables = parsed.map((row) => ({
|
|
16899
|
+
...row,
|
|
16900
|
+
isSecret: previousByKey.get(row.key)?.isSecret ?? row.isSecret,
|
|
16901
|
+
}));
|
|
16902
|
+
renderCodeModalVariablesTable();
|
|
16903
|
+
syncCodeModalEnvFromTable();
|
|
16904
|
+
showToast('Applied .env.test to variables table', 'success');
|
|
16905
|
+
}
|
|
16906
|
+
|
|
16907
|
+
async function loadCodeModalVariables() {
|
|
16908
|
+
if (!codeModalState?.recordingId || !codeModalState?.ownerType) return;
|
|
16909
|
+
try {
|
|
16910
|
+
const response = await fetch(`/api/test-variables/${codeModalState.ownerType}/${codeModalState.recordingId}`);
|
|
16911
|
+
const data = await response.json();
|
|
16912
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load variables');
|
|
16913
|
+
codeModalState.variables = Array.isArray(data.variables) ? data.variables : [];
|
|
16914
|
+
codeModalState.variablesLoaded = true;
|
|
16915
|
+
codeModalState.lastSavedVariablesSignature = getCodeModalVariablesSignature(codeModalState.variables);
|
|
16916
|
+
renderCodeModalVariablesTable();
|
|
16917
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16918
|
+
if (envTextarea) {
|
|
16919
|
+
envTextarea.value = typeof data.envTest === 'string' ? data.envTest : renderEnvTestTextClient(codeModalState.variables);
|
|
16920
|
+
}
|
|
16921
|
+
updateCodeModalVariablesStatus();
|
|
16922
|
+
return true;
|
|
16923
|
+
} catch (error) {
|
|
16924
|
+
console.error('Failed to load code modal variables:', error);
|
|
16925
|
+
const missingEl = document.getElementById('codeModalMissingList');
|
|
16926
|
+
if (missingEl) {
|
|
16927
|
+
missingEl.textContent = `Failed to load variables: ${error.message || 'Unknown error'}`;
|
|
16928
|
+
missingEl.style.color = 'var(--error)';
|
|
16929
|
+
}
|
|
16930
|
+
return false;
|
|
16931
|
+
}
|
|
16932
|
+
}
|
|
16933
|
+
|
|
16934
|
+
async function saveCodeModalVariables(options = {}) {
|
|
16935
|
+
if (!codeModalState?.recordingId || !codeModalState?.ownerType) return false;
|
|
16936
|
+
const { silent = false } = options;
|
|
16937
|
+
try {
|
|
16938
|
+
if (!codeModalState.variablesLoaded) {
|
|
16939
|
+
const loaded = await loadCodeModalVariables();
|
|
16940
|
+
if (!loaded) {
|
|
16941
|
+
throw new Error('Unable to load existing variables');
|
|
16942
|
+
}
|
|
16943
|
+
}
|
|
16944
|
+
const rows = collectCodeModalVariablesFromTable().map((row) => ({
|
|
16945
|
+
...row,
|
|
16946
|
+
key: String(row.key || '').trim().toUpperCase(),
|
|
16947
|
+
platform: ['mobile', 'web', 'both'].includes(row.platform) ? row.platform : 'both'
|
|
16948
|
+
})).filter((row) => row.key);
|
|
16949
|
+
const response = await fetch(`/api/test-variables/${codeModalState.ownerType}/${codeModalState.recordingId}`, {
|
|
16950
|
+
method: 'PUT',
|
|
16951
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16952
|
+
body: JSON.stringify({ variables: rows })
|
|
16953
|
+
});
|
|
16954
|
+
const data = await response.json();
|
|
16955
|
+
if (!response.ok) throw new Error(data.error || 'Failed to save variables');
|
|
16956
|
+
codeModalState.variables = Array.isArray(data.variables) ? data.variables : rows;
|
|
16957
|
+
codeModalState.lastSavedVariablesSignature = getCodeModalVariablesSignature(codeModalState.variables);
|
|
16958
|
+
renderCodeModalVariablesTable();
|
|
16959
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16960
|
+
if (envTextarea) envTextarea.value = typeof data.envTest === 'string' ? data.envTest : renderEnvTestTextClient(codeModalState.variables);
|
|
16961
|
+
if (!silent) showToast('Test variables saved', 'success');
|
|
16962
|
+
return true;
|
|
16963
|
+
} catch (error) {
|
|
16964
|
+
if (!silent) showToast(`Failed to save variables: ${error.message || 'Unknown error'}`, 'error');
|
|
16965
|
+
return false;
|
|
16966
|
+
}
|
|
16967
|
+
}
|
|
16968
|
+
|
|
16969
|
+
function switchCodeModalTab(tab) {
|
|
16970
|
+
const codePanel = document.getElementById('codeModalCodePanel');
|
|
16971
|
+
const varsPanel = document.getElementById('codeModalVarsPanel');
|
|
16972
|
+
const codeBtn = document.getElementById('codeModalTabCode');
|
|
16973
|
+
const varsBtn = document.getElementById('codeModalTabVars');
|
|
16974
|
+
if (codePanel) codePanel.style.display = tab === 'code' ? 'flex' : 'none';
|
|
16975
|
+
if (varsPanel) varsPanel.style.display = tab === 'vars' ? 'flex' : 'none';
|
|
16976
|
+
if (codeBtn) codeBtn.style.opacity = tab === 'code' ? '1' : '0.7';
|
|
16977
|
+
if (varsBtn) varsBtn.style.opacity = tab === 'vars' ? '1' : '0.7';
|
|
16978
|
+
if (codeModalState) codeModalState.activeTab = tab;
|
|
16979
|
+
if (tab === 'vars') {
|
|
16980
|
+
updateCodeModalVariablesStatus();
|
|
16981
|
+
}
|
|
16982
|
+
}
|
|
16983
|
+
|
|
16424
16984
|
// Generic code modal for both Web (TypeScript) and Mobile (YAML)
|
|
16425
16985
|
function showCodeModal(code, language, recordingId, type) {
|
|
16426
|
-
const
|
|
16986
|
+
const resolvedLanguage = language || 'typescript';
|
|
16987
|
+
const resolvedType = type || (resolvedLanguage === 'yaml' ? 'mobile' : 'web');
|
|
16988
|
+
const resolvedRecordingId = recordingId || (resolvedType === 'web' ? recorderSession?.id : currentProject?.id || lastMobileRecording?.projectId);
|
|
16989
|
+
const isYaml = resolvedLanguage === 'yaml';
|
|
16427
16990
|
const title = isYaml ? 'Maestro Flow (YAML)' : 'Playwright Test (TypeScript)';
|
|
16428
16991
|
const lineCount = (code || '').split('\n').length;
|
|
16429
16992
|
const lineNumbers = Array.from({length: Math.max(lineCount, 20)}, (_, i) => i + 1).join('\n');
|
|
16993
|
+
const hasVarsTab = !!resolvedRecordingId && (resolvedType === 'mobile' || resolvedType === 'web');
|
|
16994
|
+
|
|
16995
|
+
codeModalState = {
|
|
16996
|
+
code: code || '',
|
|
16997
|
+
language: resolvedLanguage,
|
|
16998
|
+
type: resolvedType,
|
|
16999
|
+
recordingId: resolvedRecordingId || null,
|
|
17000
|
+
ownerType: hasVarsTab ? getCodeModalOwnerType(resolvedType) : null,
|
|
17001
|
+
variables: [],
|
|
17002
|
+
variablesLoaded: false,
|
|
17003
|
+
lastSavedVariablesSignature: '',
|
|
17004
|
+
placeholders: extractCodeModalPlaceholders(code || ''),
|
|
17005
|
+
missingPlaceholders: [],
|
|
17006
|
+
activeTab: 'code',
|
|
17007
|
+
mobileRunDevices: [],
|
|
17008
|
+
mobileRunDevicesLoading: resolvedType === 'mobile',
|
|
17009
|
+
mobileRunSelectedDeviceId: resolvedType === 'mobile' ? getPreferredCodeModalMobileDeviceId() : '',
|
|
17010
|
+
lastSavedCode: code || ''
|
|
17011
|
+
};
|
|
16430
17012
|
|
|
16431
17013
|
const existing = document.getElementById('codeModal');
|
|
16432
17014
|
if (existing) {
|
|
16433
|
-
|
|
17015
|
+
closeCodeModal();
|
|
16434
17016
|
}
|
|
16435
17017
|
|
|
16436
17018
|
const modal = document.createElement('div');
|
|
16437
17019
|
modal.className = 'modal-overlay';
|
|
17020
|
+
modal.classList.add('code-modal-overlay');
|
|
16438
17021
|
modal.id = 'codeModal';
|
|
16439
17022
|
modal.innerHTML = `
|
|
16440
17023
|
<div class="modal code-modal">
|
|
@@ -16442,21 +17025,95 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16442
17025
|
<h3>${title}</h3>
|
|
16443
17026
|
<button class="modal-close" onclick="closeCodeModal()">×</button>
|
|
16444
17027
|
</div>
|
|
17028
|
+
<div style="display:flex; gap:8px; padding: 10px 16px 0 16px; border-bottom: 1px solid var(--border);">
|
|
17029
|
+
<button id="codeModalTabCode" class="btn btn-secondary" style="padding: 6px 10px; min-width: auto;" onclick="switchCodeModalTab('code')">Script</button>
|
|
17030
|
+
${hasVarsTab ? `
|
|
17031
|
+
<button id="codeModalTabVars" class="btn btn-secondary" style="padding: 6px 10px; min-width: auto; opacity: .7;" onclick="switchCodeModalTab('vars')">
|
|
17032
|
+
Test Variables
|
|
17033
|
+
<span id="codeModalVarsTabBadge" style="margin-left:6px; font-size: 10px; color: var(--text-muted);">...</span>
|
|
17034
|
+
</button>
|
|
17035
|
+
` : ''}
|
|
17036
|
+
</div>
|
|
17037
|
+
${resolvedType === 'mobile' ? `
|
|
17038
|
+
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; padding: 10px 16px; border-bottom: 1px solid var(--border); flex-wrap: wrap; background: rgba(255,255,255,0.01);">
|
|
17039
|
+
<div style="display:flex; align-items:center; gap:8px; flex-wrap: wrap; min-width: 0;">
|
|
17040
|
+
<label for="codeModalMobileDeviceSelect" style="font-size: 12px; color: var(--text-secondary);">Run on device</label>
|
|
17041
|
+
<select id="codeModalMobileDeviceSelect" class="setting-input" style="min-width: 280px; max-width: 100%; padding: 6px 10px; height: auto;" onchange="onCodeModalMobileDeviceChanged(this.value)">
|
|
17042
|
+
<option value="">Auto-detect device (Maestro default)</option>
|
|
17043
|
+
</select>
|
|
17044
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="loadCodeModalMobileRunDevices(true)">Refresh</button>
|
|
17045
|
+
</div>
|
|
17046
|
+
<div id="codeModalMobileDeviceStatus" style="font-size: 11px; color: var(--text-muted);">Loading devices...</div>
|
|
17047
|
+
</div>
|
|
17048
|
+
` : ''}
|
|
16445
17049
|
<div class="code-modal-body">
|
|
16446
|
-
<div class="code-
|
|
16447
|
-
<div
|
|
16448
|
-
|
|
17050
|
+
<div id="codeModalCodePanel" class="code-modal-panel">
|
|
17051
|
+
<div class="code-editor-container">
|
|
17052
|
+
<div id="codeLineNumbers" class="code-line-numbers">${lineNumbers}</div>
|
|
17053
|
+
<textarea id="codeEditorTextarea" class="code-editor-textarea" spellcheck="false">${escapeHtml(code)}</textarea>
|
|
17054
|
+
</div>
|
|
17055
|
+
<div id="codeModalOutput" class="code-modal-output" style="display: none;"></div>
|
|
16449
17056
|
</div>
|
|
16450
|
-
|
|
17057
|
+
${hasVarsTab ? `
|
|
17058
|
+
<div id="codeModalVarsPanel" class="code-modal-panel" style="display:none; padding: 12px 0 0 0; overflow:auto;">
|
|
17059
|
+
<div style="display:grid; gap:10px;">
|
|
17060
|
+
<div style="display:flex; justify-content: space-between; gap: 12px; align-items: center; flex-wrap: wrap;">
|
|
17061
|
+
<div id="codeModalVarSummary" style="font-size: 12px; color: var(--text-secondary); display:flex; gap:10px; flex-wrap:wrap;">
|
|
17062
|
+
<span>Loading variables...</span>
|
|
17063
|
+
</div>
|
|
17064
|
+
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
17065
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="addMissingCodeModalVariablesFromScript()">Add Missing From Script</button>
|
|
17066
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="addCodeModalVariableRow()">Add Variable</button>
|
|
17067
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="loadCodeModalVariables()">Reload</button>
|
|
17068
|
+
<button class="btn btn-primary" style="padding:6px 10px; min-width:auto;" onclick="saveCodeModalVariables()">Save Variables</button>
|
|
17069
|
+
</div>
|
|
17070
|
+
</div>
|
|
17071
|
+
<div id="codeModalMissingList" style="font-size:11px; color: var(--text-muted);">Checking placeholders...</div>
|
|
17072
|
+
<div class="code-vars-table-wrap">
|
|
17073
|
+
<table style="width:100%; border-collapse: collapse; min-width: 860px; background: var(--bg-secondary);">
|
|
17074
|
+
<thead>
|
|
17075
|
+
<tr style="text-align:left; font-size: 11px; color: var(--text-secondary); background: var(--bg-tertiary);">
|
|
17076
|
+
<th style="padding:8px;">Key</th>
|
|
17077
|
+
<th style="padding:8px;">Value</th>
|
|
17078
|
+
<th style="padding:8px;">Platform</th>
|
|
17079
|
+
<th style="padding:8px;">Secret</th>
|
|
17080
|
+
<th style="padding:8px;">Notes</th>
|
|
17081
|
+
<th style="padding:8px;"></th>
|
|
17082
|
+
</tr>
|
|
17083
|
+
</thead>
|
|
17084
|
+
<tbody id="codeModalVarsTableBody"></tbody>
|
|
17085
|
+
</table>
|
|
17086
|
+
</div>
|
|
17087
|
+
<div style="display:grid; gap:6px;">
|
|
17088
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
|
17089
|
+
<label style="font-size: 12px; color: var(--text-secondary);">.env.test (saved in DB as structured variables)</label>
|
|
17090
|
+
<div style="display:flex; gap:8px;">
|
|
17091
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="syncCodeModalEnvFromTable()">Regenerate</button>
|
|
17092
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="applyEnvTestToCodeModalTable()">Apply .env</button>
|
|
17093
|
+
</div>
|
|
17094
|
+
</div>
|
|
17095
|
+
<textarea id="codeModalEnvTextarea" class="code-editor-textarea" spellcheck="false" style="min-height: 140px; height: 140px;"></textarea>
|
|
17096
|
+
</div>
|
|
17097
|
+
</div>
|
|
17098
|
+
</div>
|
|
17099
|
+
` : ''}
|
|
16451
17100
|
</div>
|
|
16452
17101
|
<div class="code-modal-footer">
|
|
16453
|
-
${
|
|
16454
|
-
<button id="runCodeBtn" class="btn btn-run" onclick="runMaestroFromEditor('${
|
|
17102
|
+
${resolvedRecordingId ? `
|
|
17103
|
+
<button id="runCodeBtn" class="btn btn-run" onclick="${isYaml ? `runMaestroFromEditor('${resolvedRecordingId}')` : `runPlaywrightFromEditor('${resolvedRecordingId}')`}">
|
|
16455
17104
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
16456
17105
|
<polygon points="5 3 19 12 5 21 5 3"/>
|
|
16457
17106
|
</svg>
|
|
16458
|
-
Run Test
|
|
17107
|
+
${isYaml ? 'Run on Device' : 'Run Test'}
|
|
16459
17108
|
</button>
|
|
17109
|
+
${isYaml ? `
|
|
17110
|
+
<button id="stopCodeBtn" class="btn btn-secondary" onclick="stopMaestroRunFromEditor()" style="display:none; min-width: 120px; border-color: rgba(239,68,68,0.25); color: #fca5a5;">
|
|
17111
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
|
|
17112
|
+
<rect x="6" y="6" width="12" height="12" rx="1"/>
|
|
17113
|
+
</svg>
|
|
17114
|
+
Stop Run
|
|
17115
|
+
</button>
|
|
17116
|
+
` : ''}
|
|
16460
17117
|
` : ''}
|
|
16461
17118
|
<button class="btn btn-secondary" onclick="copyCodeToClipboard()">
|
|
16462
17119
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
@@ -16465,8 +17122,8 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16465
17122
|
</svg>
|
|
16466
17123
|
Copy
|
|
16467
17124
|
</button>
|
|
16468
|
-
${
|
|
16469
|
-
<button class="btn btn-primary" onclick="saveCodeChanges('${
|
|
17125
|
+
${resolvedRecordingId ? `
|
|
17126
|
+
<button class="btn btn-primary" onclick="saveCodeChanges('${resolvedRecordingId}', '${resolvedType}')">
|
|
16470
17127
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
16471
17128
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
|
16472
17129
|
<polyline points="17 21 17 13 7 13 7 21"/>
|
|
@@ -16479,6 +17136,8 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16479
17136
|
</div>
|
|
16480
17137
|
`;
|
|
16481
17138
|
document.body.appendChild(modal);
|
|
17139
|
+
lockPageScrollForCodeModal();
|
|
17140
|
+
attachCodeModalScrollTrap(modal);
|
|
16482
17141
|
requestAnimationFrame(() => modal.classList.add('active'));
|
|
16483
17142
|
|
|
16484
17143
|
// Sync line numbers with textarea scroll and content
|
|
@@ -16491,9 +17150,17 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16491
17150
|
lineNumbersEl.scrollTop = textarea.scrollTop;
|
|
16492
17151
|
});
|
|
16493
17152
|
|
|
17153
|
+
// If the pointer is over line numbers, scroll the editor instead of the page behind
|
|
17154
|
+
lineNumbersEl.addEventListener('wheel', (e) => {
|
|
17155
|
+
e.preventDefault();
|
|
17156
|
+
textarea.scrollTop += e.deltaY;
|
|
17157
|
+
textarea.scrollLeft += e.deltaX;
|
|
17158
|
+
}, { passive: false });
|
|
17159
|
+
|
|
16494
17160
|
// Update line numbers when content changes
|
|
16495
17161
|
textarea.addEventListener('input', () => {
|
|
16496
17162
|
updateLineNumbers();
|
|
17163
|
+
updateCodeModalVariablesStatus();
|
|
16497
17164
|
});
|
|
16498
17165
|
}
|
|
16499
17166
|
|
|
@@ -16507,6 +17174,14 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16507
17174
|
};
|
|
16508
17175
|
document.addEventListener('keydown', escHandler);
|
|
16509
17176
|
modal._escHandler = escHandler;
|
|
17177
|
+
|
|
17178
|
+
if (hasVarsTab) {
|
|
17179
|
+
loadCodeModalVariables();
|
|
17180
|
+
}
|
|
17181
|
+
if (resolvedType === 'mobile') {
|
|
17182
|
+
loadCodeModalMobileRunDevices();
|
|
17183
|
+
}
|
|
17184
|
+
switchCodeModalTab('code');
|
|
16510
17185
|
}
|
|
16511
17186
|
|
|
16512
17187
|
function updateLineNumbers() {
|
|
@@ -16519,15 +17194,277 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16519
17194
|
lineNumbersEl.textContent = Array.from({length: maxLines}, (_, i) => i + 1).join('\n');
|
|
16520
17195
|
}
|
|
16521
17196
|
|
|
17197
|
+
function setCodeModalRunButtonBusy(runBtn, label) {
|
|
17198
|
+
if (!runBtn) return;
|
|
17199
|
+
runBtn.disabled = true;
|
|
17200
|
+
runBtn.innerHTML = `
|
|
17201
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; animation: spin 1s linear infinite;">
|
|
17202
|
+
<circle cx="12" cy="12" r="10"/>
|
|
17203
|
+
<path d="M12 6v6l4 2"/>
|
|
17204
|
+
</svg>
|
|
17205
|
+
${escapeHtml(label || 'Running...')}
|
|
17206
|
+
`;
|
|
17207
|
+
}
|
|
17208
|
+
|
|
17209
|
+
function setCodeModalStopButtonState(stopBtn, options = {}) {
|
|
17210
|
+
if (!stopBtn) return;
|
|
17211
|
+
const {
|
|
17212
|
+
visible = false,
|
|
17213
|
+
disabled = false,
|
|
17214
|
+
label = 'Stop Run'
|
|
17215
|
+
} = options;
|
|
17216
|
+
stopBtn.style.display = visible ? '' : 'none';
|
|
17217
|
+
stopBtn.disabled = !!disabled;
|
|
17218
|
+
stopBtn.innerHTML = `
|
|
17219
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
|
|
17220
|
+
<rect x="6" y="6" width="12" height="12" rx="1"/>
|
|
17221
|
+
</svg>
|
|
17222
|
+
${escapeHtml(label)}
|
|
17223
|
+
`;
|
|
17224
|
+
}
|
|
17225
|
+
|
|
17226
|
+
function resetCodeModalRunButton(runBtn, type = 'mobile') {
|
|
17227
|
+
if (!runBtn) return;
|
|
17228
|
+
runBtn.disabled = false;
|
|
17229
|
+
runBtn.innerHTML = `
|
|
17230
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
17231
|
+
<polygon points="5 3 19 12 5 21 5 3"/>
|
|
17232
|
+
</svg>
|
|
17233
|
+
${type === 'mobile' ? 'Run on Device' : 'Run Test'}
|
|
17234
|
+
`;
|
|
17235
|
+
}
|
|
17236
|
+
|
|
17237
|
+
function appendCodeModalOutputLine(outputEl, htmlLine) {
|
|
17238
|
+
if (!outputEl) return;
|
|
17239
|
+
outputEl.innerHTML += '\n' + htmlLine;
|
|
17240
|
+
outputEl.scrollTop = outputEl.scrollHeight;
|
|
17241
|
+
}
|
|
17242
|
+
|
|
17243
|
+
async function pollMaestroRunStatus(runId, options = {}) {
|
|
17244
|
+
const {
|
|
17245
|
+
outputEl = null,
|
|
17246
|
+
runBtn = null,
|
|
17247
|
+
pollMs = 1200,
|
|
17248
|
+
timeoutMs = 360000
|
|
17249
|
+
} = options;
|
|
17250
|
+
const startedAt = Date.now();
|
|
17251
|
+
let runningAnnounced = false;
|
|
17252
|
+
|
|
17253
|
+
while ((Date.now() - startedAt) < timeoutMs) {
|
|
17254
|
+
const response = await fetch(`/api/testing/mobile/replays/${encodeURIComponent(runId)}`);
|
|
17255
|
+
const data = await response.json().catch(() => ({}));
|
|
17256
|
+
if (!response.ok) {
|
|
17257
|
+
throw new Error(data.error || 'Failed to read Maestro run status');
|
|
17258
|
+
}
|
|
17259
|
+
|
|
17260
|
+
const elapsedSec = Math.max(0, Math.round((Number(data.elapsedMs || 0) / 1000)));
|
|
17261
|
+
if (data.status === 'running') {
|
|
17262
|
+
if (!runningAnnounced && outputEl) {
|
|
17263
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-running">⏳ Maestro is running on device...</span>');
|
|
17264
|
+
runningAnnounced = true;
|
|
17265
|
+
}
|
|
17266
|
+
if (runBtn) {
|
|
17267
|
+
setCodeModalRunButtonBusy(runBtn, elapsedSec > 0 ? `Running... ${elapsedSec}s` : 'Running...');
|
|
17268
|
+
}
|
|
17269
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
17270
|
+
continue;
|
|
17271
|
+
}
|
|
17272
|
+
|
|
17273
|
+
return data;
|
|
17274
|
+
}
|
|
17275
|
+
|
|
17276
|
+
throw new Error('Timed out waiting for Maestro run to finish');
|
|
17277
|
+
}
|
|
17278
|
+
|
|
16522
17279
|
// Run Maestro test from editor
|
|
16523
17280
|
let codeModalRunning = false;
|
|
17281
|
+
async function stopMaestroRunFromEditor() {
|
|
17282
|
+
const stopBtn = document.getElementById('stopCodeBtn');
|
|
17283
|
+
const outputEl = document.getElementById('codeModalOutput');
|
|
17284
|
+
const runId = String(codeModalState?.activeMaestroRunId || '').trim();
|
|
17285
|
+
if (!runId) return;
|
|
17286
|
+
|
|
17287
|
+
try {
|
|
17288
|
+
setCodeModalStopButtonState(stopBtn, { visible: true, disabled: true, label: 'Stopping...' });
|
|
17289
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-running">⏹ Requesting stop...</span>');
|
|
17290
|
+
const response = await fetch(`/api/testing/mobile/replays/${encodeURIComponent(runId)}/stop`, {
|
|
17291
|
+
method: 'POST'
|
|
17292
|
+
});
|
|
17293
|
+
const data = await response.json().catch(() => ({}));
|
|
17294
|
+
if (!response.ok && response.status !== 409) {
|
|
17295
|
+
throw new Error(data.error || 'Failed to stop run');
|
|
17296
|
+
}
|
|
17297
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">⏳ Stop requested. Waiting for runner status...</span>');
|
|
17298
|
+
showToast('Stop requested', 'info');
|
|
17299
|
+
} catch (error) {
|
|
17300
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-error">✗ Stop failed: ' + escapeHtml(error.message || 'Unknown error') + '</span>');
|
|
17301
|
+
setCodeModalStopButtonState(stopBtn, { visible: true, disabled: false, label: 'Stop Run' });
|
|
17302
|
+
showToast('Failed to stop run', 'error');
|
|
17303
|
+
}
|
|
17304
|
+
}
|
|
17305
|
+
|
|
16524
17306
|
async function runMaestroFromEditor(recordingId) {
|
|
16525
17307
|
if (codeModalRunning) return;
|
|
16526
17308
|
|
|
16527
17309
|
const textarea = document.getElementById('codeEditorTextarea');
|
|
16528
17310
|
const outputEl = document.getElementById('codeModalOutput');
|
|
16529
17311
|
const runBtn = document.getElementById('runCodeBtn');
|
|
17312
|
+
const stopBtn = document.getElementById('stopCodeBtn');
|
|
17313
|
+
|
|
17314
|
+
if (!textarea || !outputEl) return;
|
|
17315
|
+
|
|
17316
|
+
codeModalRunning = true;
|
|
17317
|
+
if (codeModalState) codeModalState.activeMaestroRunId = null;
|
|
17318
|
+
if (runBtn) {
|
|
17319
|
+
setCodeModalRunButtonBusy(runBtn, 'Loading...');
|
|
17320
|
+
}
|
|
17321
|
+
setCodeModalStopButtonState(stopBtn, { visible: false });
|
|
17322
|
+
|
|
17323
|
+
// Show output area
|
|
17324
|
+
outputEl.style.display = 'block';
|
|
17325
|
+
outputEl.innerHTML = '<span class="output-running">▶ Saving and running Maestro test on device...</span>';
|
|
17326
|
+
|
|
17327
|
+
const selectedDeviceId = String(codeModalState?.mobileRunSelectedDeviceId || '').trim();
|
|
17328
|
+
const selectedDevice = Array.isArray(codeModalState?.mobileRunDevices)
|
|
17329
|
+
? codeModalState.mobileRunDevices.find((d) => String(d?.id || '').trim() === selectedDeviceId)
|
|
17330
|
+
: null;
|
|
17331
|
+
|
|
17332
|
+
try {
|
|
17333
|
+
const currentVars = collectCodeModalVariablesFromTable().map((row) => ({
|
|
17334
|
+
...row,
|
|
17335
|
+
key: String(row.key || '').trim().toUpperCase(),
|
|
17336
|
+
platform: ['mobile', 'web', 'both'].includes(row.platform) ? row.platform : 'both'
|
|
17337
|
+
})).filter((row) => row.key);
|
|
17338
|
+
const currentVarsSignature = getCodeModalVariablesSignature(currentVars);
|
|
17339
|
+
const shouldSaveVars = !codeModalState?.variablesLoaded
|
|
17340
|
+
|| currentVarsSignature !== String(codeModalState?.lastSavedVariablesSignature || '');
|
|
17341
|
+
if (shouldSaveVars) {
|
|
17342
|
+
const varsSaved = await saveCodeModalVariables({ silent: true });
|
|
17343
|
+
if (!varsSaved) throw new Error('Failed to save test variables');
|
|
17344
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-success">✓ Test variables saved</span>');
|
|
17345
|
+
} else {
|
|
17346
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">↺ Test variables unchanged (skipped save)</span>');
|
|
17347
|
+
}
|
|
17348
|
+
|
|
17349
|
+
// First, save the code
|
|
17350
|
+
const currentCode = textarea.value;
|
|
17351
|
+
const shouldSaveCode = currentCode !== String(codeModalState?.lastSavedCode ?? codeModalState?.code ?? '');
|
|
17352
|
+
if (shouldSaveCode) {
|
|
17353
|
+
const saveResponse = await fetch(`/api/testing/mobile/recordings/${recordingId}/flow`, {
|
|
17354
|
+
method: 'PUT',
|
|
17355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17356
|
+
body: JSON.stringify({ flowCode: currentCode })
|
|
17357
|
+
});
|
|
17358
|
+
|
|
17359
|
+
if (!saveResponse.ok) {
|
|
17360
|
+
throw new Error('Failed to save flow code');
|
|
17361
|
+
}
|
|
17362
|
+
if (codeModalState) {
|
|
17363
|
+
codeModalState.code = currentCode;
|
|
17364
|
+
codeModalState.lastSavedCode = currentCode;
|
|
17365
|
+
}
|
|
17366
|
+
outputEl.innerHTML += '\n<span class="output-success">✓ Flow saved</span>';
|
|
17367
|
+
} else {
|
|
17368
|
+
outputEl.innerHTML += '\n<span class="output-step">↺ Flow unchanged (skipped save)</span>';
|
|
17369
|
+
}
|
|
17370
|
+
outputEl.innerHTML += '\n<span class="output-running">▶ Starting Maestro test on device...</span>';
|
|
17371
|
+
|
|
17372
|
+
// Now run the test
|
|
17373
|
+
const runRequestBody = {};
|
|
17374
|
+
if (selectedDeviceId) {
|
|
17375
|
+
runRequestBody.deviceId = selectedDeviceId;
|
|
17376
|
+
if (selectedDevice?.platform === 'ios' || selectedDevice?.platform === 'android') {
|
|
17377
|
+
runRequestBody.devicePlatform = selectedDevice.platform;
|
|
17378
|
+
runRequestBody.deviceName = String(selectedDevice?.name || selectedDeviceId);
|
|
17379
|
+
runRequestBody.skipDeviceValidation = true;
|
|
17380
|
+
}
|
|
17381
|
+
}
|
|
17382
|
+
const runResponse = await fetch(`/api/testing/mobile/recordings/${recordingId}/replay`, {
|
|
17383
|
+
method: 'POST',
|
|
17384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17385
|
+
body: JSON.stringify(runRequestBody)
|
|
17386
|
+
});
|
|
17387
|
+
|
|
17388
|
+
const runData = await runResponse.json();
|
|
17389
|
+
|
|
17390
|
+
if (!runResponse.ok) {
|
|
17391
|
+
if (Array.isArray(runData?.missingKeys) && runData.missingKeys.length > 0) {
|
|
17392
|
+
outputEl.innerHTML += '\n<span class="output-error">✗ Missing variables: ' + runData.missingKeys.join(', ') + '</span>';
|
|
17393
|
+
switchCodeModalTab('vars');
|
|
17394
|
+
}
|
|
17395
|
+
throw new Error(runData.error || 'Failed to start test');
|
|
17396
|
+
}
|
|
17397
|
+
|
|
17398
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-success">✓ Maestro test started!</span>');
|
|
17399
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">📍 Flow: ' + escapeHtml(runData.flowPath || recordingId) + '</span>');
|
|
17400
|
+
if (runData?.deviceName || runData?.deviceId) {
|
|
17401
|
+
const deviceLabel = runData.deviceName || runData.deviceId;
|
|
17402
|
+
const platformLabel = runData.devicePlatform ? ` (${String(runData.devicePlatform).toUpperCase()})` : '';
|
|
17403
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">📱 Device: ' + escapeHtml(deviceLabel + platformLabel) + '</span>');
|
|
17404
|
+
if (runData?.deviceSelectionSource === 'trusted-client') {
|
|
17405
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">⚡ Fast start: reused selected device from modal</span>');
|
|
17406
|
+
}
|
|
17407
|
+
} else {
|
|
17408
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">📱 Device: Auto-detect (Maestro default)</span>');
|
|
17409
|
+
}
|
|
17410
|
+
if (Array.isArray(runData.usedKeys) && runData.usedKeys.length > 0) {
|
|
17411
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">🔐 Vars: ' + escapeHtml(runData.usedKeys.join(', ')) + '</span>');
|
|
17412
|
+
}
|
|
17413
|
+
appendCodeModalOutputLine(outputEl, '<span style="color: #fbbf24;">💡 Watch the selected device for test execution.</span>');
|
|
17414
|
+
|
|
17415
|
+
const runId = typeof runData?.runId === 'string' ? runData.runId : '';
|
|
17416
|
+
if (runId) {
|
|
17417
|
+
if (codeModalState) codeModalState.activeMaestroRunId = runId;
|
|
17418
|
+
setCodeModalRunButtonBusy(runBtn, 'Running...');
|
|
17419
|
+
setCodeModalStopButtonState(stopBtn, { visible: true, disabled: false, label: 'Stop Run' });
|
|
17420
|
+
const finalStatus = await pollMaestroRunStatus(runId, { outputEl, runBtn });
|
|
17421
|
+
const durationMs = Number(finalStatus?.durationMs);
|
|
17422
|
+
const durationLabel = Number.isFinite(durationMs) && durationMs > 0
|
|
17423
|
+
? ` (${Math.round(durationMs / 1000)}s)`
|
|
17424
|
+
: '';
|
|
17425
|
+
|
|
17426
|
+
if (finalStatus.status === 'completed') {
|
|
17427
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-success">✓ Maestro test completed' + escapeHtml(durationLabel) + '</span>');
|
|
17428
|
+
if (finalStatus.output) {
|
|
17429
|
+
const preview = String(finalStatus.output).slice(-2500);
|
|
17430
|
+
appendCodeModalOutputLine(outputEl, '<pre style="white-space: pre-wrap; margin: 8px 0 0 0; color: var(--text-secondary);">' + escapeHtml(preview) + '</pre>');
|
|
17431
|
+
}
|
|
17432
|
+
showToast('Maestro test completed', 'success');
|
|
17433
|
+
} else if (finalStatus.status === 'failed') {
|
|
17434
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-error">✗ Maestro test failed' + escapeHtml(durationLabel) + '</span>');
|
|
17435
|
+
if (finalStatus.error) {
|
|
17436
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-error">' + escapeHtml(String(finalStatus.error)) + '</span>');
|
|
17437
|
+
}
|
|
17438
|
+
if (finalStatus.output) {
|
|
17439
|
+
const preview = String(finalStatus.output).slice(-2500);
|
|
17440
|
+
appendCodeModalOutputLine(outputEl, '<pre style="white-space: pre-wrap; margin: 8px 0 0 0; color: var(--text-secondary);">' + escapeHtml(preview) + '</pre>');
|
|
17441
|
+
}
|
|
17442
|
+
showToast('Maestro test failed', 'error');
|
|
17443
|
+
} else if (finalStatus.status === 'canceled') {
|
|
17444
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">⏹ Maestro run canceled' + escapeHtml(durationLabel) + '</span>');
|
|
17445
|
+
showToast('Maestro run canceled', 'info');
|
|
17446
|
+
}
|
|
17447
|
+
} else {
|
|
17448
|
+
showToast('Maestro test started on device', 'success');
|
|
17449
|
+
}
|
|
16530
17450
|
|
|
17451
|
+
} catch (error) {
|
|
17452
|
+
outputEl.innerHTML += '\n<span class="output-error">✗ Error: ' + (error.message || 'Unknown error') + '</span>';
|
|
17453
|
+
showToast('Failed to run test', 'error');
|
|
17454
|
+
} finally {
|
|
17455
|
+
codeModalRunning = false;
|
|
17456
|
+
if (codeModalState) codeModalState.activeMaestroRunId = null;
|
|
17457
|
+
setCodeModalStopButtonState(stopBtn, { visible: false });
|
|
17458
|
+
resetCodeModalRunButton(runBtn, 'mobile');
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
17461
|
+
|
|
17462
|
+
async function runPlaywrightFromEditor(recordingId) {
|
|
17463
|
+
if (codeModalRunning) return;
|
|
17464
|
+
|
|
17465
|
+
const textarea = document.getElementById('codeEditorTextarea');
|
|
17466
|
+
const outputEl = document.getElementById('codeModalOutput');
|
|
17467
|
+
const runBtn = document.getElementById('runCodeBtn');
|
|
16531
17468
|
if (!textarea || !outputEl) return;
|
|
16532
17469
|
|
|
16533
17470
|
codeModalRunning = true;
|
|
@@ -16542,84 +17479,66 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16542
17479
|
`;
|
|
16543
17480
|
}
|
|
16544
17481
|
|
|
16545
|
-
// Show output area
|
|
16546
17482
|
outputEl.style.display = 'block';
|
|
16547
|
-
outputEl.innerHTML = '<span class="output-running">▶ Saving and running
|
|
16548
|
-
|
|
16549
|
-
// Detect platform from YAML appId
|
|
16550
|
-
const yamlContent = textarea.value;
|
|
16551
|
-
const appIdMatch = yamlContent.match(/appId:\s*([^\s\n]+)/);
|
|
16552
|
-
const appId = appIdMatch ? appIdMatch[1] : '';
|
|
16553
|
-
const platform = appId.includes('com.apple') || appId.startsWith('com.') && !appId.includes('android') ? 'ios' : 'android';
|
|
17483
|
+
outputEl.innerHTML = '<span class="output-running">▶ Saving Playwright script and running test...</span>';
|
|
16554
17484
|
|
|
16555
17485
|
try {
|
|
16556
|
-
|
|
16557
|
-
|
|
17486
|
+
const varsSaved = await saveCodeModalVariables({ silent: true });
|
|
17487
|
+
if (varsSaved === false) {
|
|
17488
|
+
throw new Error('Failed to save test variables');
|
|
17489
|
+
}
|
|
17490
|
+
|
|
17491
|
+
const saveResponse = await fetch(`/api/recorder/recordings/${recordingId}/spec`, {
|
|
16558
17492
|
method: 'PUT',
|
|
16559
17493
|
headers: { 'Content-Type': 'application/json' },
|
|
16560
|
-
body: JSON.stringify({
|
|
17494
|
+
body: JSON.stringify({ specCode: textarea.value })
|
|
16561
17495
|
});
|
|
16562
|
-
|
|
17496
|
+
const saveData = await saveResponse.json().catch(() => ({}));
|
|
16563
17497
|
if (!saveResponse.ok) {
|
|
16564
|
-
throw new Error('Failed to save
|
|
17498
|
+
throw new Error(saveData.error || 'Failed to save spec');
|
|
16565
17499
|
}
|
|
16566
17500
|
|
|
16567
|
-
outputEl.innerHTML += '\n<span class="output-success">✓
|
|
16568
|
-
outputEl.innerHTML += '\n<span class="output-running">▶ Starting auto-capture and Maestro test...</span>';
|
|
17501
|
+
outputEl.innerHTML += '\n<span class="output-success">✓ Spec saved</span>';
|
|
16569
17502
|
|
|
16570
|
-
|
|
16571
|
-
try {
|
|
16572
|
-
await fetch('/api/testing/mobile/auto-capture/start', {
|
|
16573
|
-
method: 'POST',
|
|
16574
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16575
|
-
body: JSON.stringify({ recordingId, platform })
|
|
16576
|
-
});
|
|
16577
|
-
outputEl.innerHTML += '\n<span class="output-success">✓ Auto-capture started</span>';
|
|
16578
|
-
} catch (captureErr) {
|
|
16579
|
-
console.log('Auto-capture not available:', captureErr);
|
|
16580
|
-
}
|
|
16581
|
-
|
|
16582
|
-
// Now run the test
|
|
16583
|
-
const runResponse = await fetch(`/api/testing/mobile/recordings/${recordingId}/replay`, {
|
|
17503
|
+
const runResponse = await fetch(`/api/recorder/recordings/${recordingId}/run`, {
|
|
16584
17504
|
method: 'POST',
|
|
16585
|
-
headers: { 'Content-Type': 'application/json' }
|
|
17505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17506
|
+
body: JSON.stringify({
|
|
17507
|
+
headless: false,
|
|
17508
|
+
browser: 'chromium',
|
|
17509
|
+
video: 'retain-on-failure',
|
|
17510
|
+
screenshot: 'only-on-failure',
|
|
17511
|
+
trace: 'retain-on-failure'
|
|
17512
|
+
})
|
|
16586
17513
|
});
|
|
16587
|
-
|
|
16588
17514
|
const runData = await runResponse.json();
|
|
16589
17515
|
|
|
16590
17516
|
if (!runResponse.ok) {
|
|
16591
|
-
|
|
17517
|
+
if (Array.isArray(runData?.missingKeys) && runData.missingKeys.length > 0) {
|
|
17518
|
+
outputEl.innerHTML += '\n<span class="output-error">✗ Missing variables: ' + runData.missingKeys.join(', ') + '</span>';
|
|
17519
|
+
switchCodeModalTab('vars');
|
|
17520
|
+
}
|
|
17521
|
+
throw new Error(runData.error || runData.result?.error || 'Failed to run Playwright test');
|
|
16592
17522
|
}
|
|
16593
17523
|
|
|
16594
|
-
|
|
16595
|
-
outputEl.innerHTML += '\n<span class="output-
|
|
16596
|
-
outputEl.innerHTML += '\n
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
body: JSON.stringify({ recordingId })
|
|
16608
|
-
});
|
|
16609
|
-
} catch {}
|
|
16610
|
-
}, 30000);
|
|
17524
|
+
const result = runData.result || {};
|
|
17525
|
+
outputEl.innerHTML += '\n<span class="output-success">✓ Playwright run completed</span>';
|
|
17526
|
+
outputEl.innerHTML += '\n<span class="output-step">✅ Passed: ' + (result.passed ?? 0) + ' | ❌ Failed: ' + (result.failed ?? 0) + ' | ⏭️ Skipped: ' + (result.skipped ?? 0) + '</span>';
|
|
17527
|
+
if (Array.isArray(runData.usedKeys) && runData.usedKeys.length > 0) {
|
|
17528
|
+
outputEl.innerHTML += '\n<span class="output-step">🔐 Vars: ' + runData.usedKeys.join(', ') + '</span>';
|
|
17529
|
+
}
|
|
17530
|
+
if (result.reportPath) {
|
|
17531
|
+
outputEl.innerHTML += '\n<span class="output-step">📄 Report: ' + result.reportPath + '</span>';
|
|
17532
|
+
}
|
|
17533
|
+
if (result.output) {
|
|
17534
|
+
const preview = String(result.output).slice(0, 2500);
|
|
17535
|
+
outputEl.innerHTML += '\n\n<pre style="white-space: pre-wrap; margin: 8px 0 0 0; color: var(--text-secondary);">' + escapeHtml(preview) + '</pre>';
|
|
17536
|
+
}
|
|
16611
17537
|
|
|
17538
|
+
showToast(result.success === false ? 'Playwright test failed' : 'Playwright test finished', result.success === false ? 'warning' : 'success');
|
|
16612
17539
|
} catch (error) {
|
|
16613
17540
|
outputEl.innerHTML += '\n<span class="output-error">✗ Error: ' + (error.message || 'Unknown error') + '</span>';
|
|
16614
|
-
showToast('Failed to run test', 'error');
|
|
16615
|
-
// Stop auto-capture on error
|
|
16616
|
-
try {
|
|
16617
|
-
await fetch('/api/testing/mobile/auto-capture/stop', {
|
|
16618
|
-
method: 'POST',
|
|
16619
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16620
|
-
body: JSON.stringify({ recordingId })
|
|
16621
|
-
});
|
|
16622
|
-
} catch {}
|
|
17541
|
+
showToast('Failed to run Playwright test', 'error');
|
|
16623
17542
|
} finally {
|
|
16624
17543
|
codeModalRunning = false;
|
|
16625
17544
|
if (runBtn) {
|
|
@@ -16640,8 +17559,13 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16640
17559
|
if (modal._escHandler) {
|
|
16641
17560
|
document.removeEventListener('keydown', modal._escHandler);
|
|
16642
17561
|
}
|
|
17562
|
+
if (modal._wheelTrapHandler) {
|
|
17563
|
+
modal.removeEventListener('wheel', modal._wheelTrapHandler, { capture: true });
|
|
17564
|
+
}
|
|
16643
17565
|
modal.remove();
|
|
16644
17566
|
}
|
|
17567
|
+
unlockPageScrollForCodeModal();
|
|
17568
|
+
codeModalState = null;
|
|
16645
17569
|
}
|
|
16646
17570
|
|
|
16647
17571
|
function copyCodeToClipboard() {
|
|
@@ -16659,6 +17583,11 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16659
17583
|
const code = textarea.value;
|
|
16660
17584
|
|
|
16661
17585
|
try {
|
|
17586
|
+
const varsSaved = await saveCodeModalVariables({ silent: true });
|
|
17587
|
+
if (varsSaved === false) {
|
|
17588
|
+
throw new Error('Failed to save test variables');
|
|
17589
|
+
}
|
|
17590
|
+
|
|
16662
17591
|
if (type === 'mobile') {
|
|
16663
17592
|
const response = await fetch(`/api/testing/mobile/recordings/${recordingId}/flow`, {
|
|
16664
17593
|
method: 'PUT',
|
|
@@ -16676,6 +17605,12 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16676
17605
|
if (!response.ok) throw new Error('Failed to save');
|
|
16677
17606
|
}
|
|
16678
17607
|
|
|
17608
|
+
if (codeModalState) {
|
|
17609
|
+
codeModalState.code = code;
|
|
17610
|
+
codeModalState.lastSavedCode = code;
|
|
17611
|
+
codeModalState.placeholders = extractCodeModalPlaceholders(code);
|
|
17612
|
+
}
|
|
17613
|
+
updateCodeModalVariablesStatus();
|
|
16679
17614
|
showToast('Code saved!', 'success');
|
|
16680
17615
|
} catch (error) {
|
|
16681
17616
|
showToast('Failed to save code', 'error');
|