@aimeloic/monkey-tester 2.0.5 → 2.0.7

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.
Files changed (4) hide show
  1. package/htmlTemplate.js +249 -270
  2. package/index.js +3 -244
  3. package/monkey.js +198 -0
  4. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -1,13 +1,14 @@
1
- export function getHtmlTemplate(endpoints) {
1
+ 'use strict';
2
+
3
+ function getHtmlTemplate(endpoints) {
2
4
  const safeJsonString = Buffer.from(JSON.stringify(endpoints)).toString('base64');
3
5
 
4
- return `
5
- <!DOCTYPE html>
6
+ return `<!DOCTYPE html>
6
7
  <html lang="en">
7
8
  <head>
8
9
  <meta charset="UTF-8">
9
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
- <title>Endtester — API Environment</title>
11
+ <title>Monkey Tester — API Sandbox</title>
11
12
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
12
13
  <style>
13
14
  :root {
@@ -24,167 +25,143 @@ export function getHtmlTemplate(endpoints) {
24
25
  --blue: #5a86c0;
25
26
  --radius: 8px;
26
27
  }
27
-
28
+
28
29
  * { box-sizing: border-box; margin: 0; padding: 0; }
29
-
30
+
30
31
  body {
31
- background: var(--bg);
32
- color: var(--text);
33
- font-family: 'DM Sans', sans-serif;
34
- font-size: 14px;
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ font-family: 'DM Sans', sans-serif;
35
+ font-size: 14px;
35
36
  height: 100vh;
36
- overflow: hidden; /* Prevents whole-page scrolling */
37
+ overflow: hidden;
37
38
  background-image: radial-gradient(ellipse 80% 60% at 50% -20%, #3a2a0a22 0%, transparent 70%);
38
39
  }
39
-
40
+
40
41
  header {
41
- border-bottom: 1px solid var(--border);
42
- padding: 16px 32px;
43
- display: flex;
44
- align-items: center;
42
+ border-bottom: 1px solid var(--border);
43
+ padding: 16px 32px;
44
+ display: flex;
45
+ align-items: center;
45
46
  gap: 20px;
46
- background: #0e0c09ee;
47
- backdrop-filter: blur(8px);
47
+ background: #0e0c09ee;
48
+ backdrop-filter: blur(8px);
48
49
  height: 65px;
49
50
  }
50
-
51
+
51
52
  .logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); letter-spacing: 0.02em; }
52
53
  .logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; display: inline-block; margin-left: 8px; font-weight: 400; }
53
54
  .header-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
54
55
  .jwt-wrap, .base-url-wrap { display: flex; align-items: center; gap: 8px; }
55
56
  .jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
56
-
57
+
57
58
  #jwt-input, #base-url {
58
59
  background: var(--surface2); border: 1px solid var(--border); color: var(--text);
59
- font-family: 'DM Mono', monospace; font-size: 12px; padding: 6px 12px; border-radius: var(--radius); width: 220px; outline: none;
60
+ font-family: 'DM Mono', monospace; font-size: 12px; padding: 6px 12px;
61
+ border-radius: var(--radius); width: 220px; outline: none;
60
62
  }
61
-
62
- /* Fixed view height viewport matrix layout grid rules */
63
- .layout {
64
- display: grid;
65
- grid-template-columns: 280px 1fr 450px;
66
- height: calc(100vh - 65px);
63
+
64
+ .layout {
65
+ display: grid;
66
+ grid-template-columns: 280px 1fr 450px;
67
+ height: calc(100vh - 65px);
67
68
  overflow: hidden;
68
69
  }
69
-
70
- aside {
71
- border-right: 1px solid var(--border);
72
- overflow-y: auto;
73
- padding: 16px 0;
70
+
71
+ aside {
72
+ border-right: 1px solid var(--border);
73
+ overflow-y: auto;
74
+ padding: 16px 0;
74
75
  background: #0b0907;
75
76
  }
76
-
77
+
77
78
  .section-label { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text-dim); letter-spacing: 0.12em; text-transform: uppercase; padding: 12px 18px 6px; }
78
-
79
+
79
80
  .nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; cursor: pointer; border-left: 2px solid transparent; color: var(--text-dim); transition: all 0.2s; }
80
81
  .nav-item:hover { background: var(--surface); color: var(--text); }
81
82
  .nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
82
-
83
+
83
84
  .method-badge { font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 600; padding: 2px 6px; border-radius: 4px; min-width: 52px; text-align: center; text-transform: uppercase; }
84
- .GET { background: #1a3a22; color: #6ba05a; }
85
- .POST { background: #1a2e3a; color: #5a86c0; }
86
- .PUT, .PATCH { background: #3a2e10; color: #e8a838; }
85
+ .GET { background: #1a3a22; color: #6ba05a; }
86
+ .POST { background: #1a2e3a; color: #5a86c0; }
87
+ .PUT { background: #3a2e10; color: #e8a838; }
88
+ .PATCH { background: #3a2e10; color: #e8a838; }
87
89
  .DELETE { background: #3a1a14; color: #d45c3c; }
88
-
90
+
89
91
  .nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
90
-
91
- main {
92
- overflow-y: auto;
93
- padding: 32px;
92
+
93
+ main {
94
+ overflow-y: auto;
95
+ padding: 32px;
94
96
  background: #0e0c09;
95
97
  }
96
-
98
+
97
99
  .endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
98
- .endpoint-path { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
99
- .endpoint-desc { color: var(--text-dim); font-size: 13px; line-height: 1.6; margin-bottom: 24px; border-left: 2px solid var(--border); padding-left: 12px; }
100
-
100
+ .endpoint-path { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
101
+ .endpoint-desc { color: var(--text-dim); font-size: 13px; line-height: 1.6; margin-bottom: 24px; border-left: 2px solid var(--border); padding-left: 12px; }
102
+
101
103
  .form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
102
104
  .form-section-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
103
-
105
+
104
106
  .field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
105
107
  .field-row:last-child { margin-bottom: 0; }
106
108
  .field-label { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text-dim); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
107
-
108
- input[type=text], input[type=password], input[type=number], input[type=date], input[type=tel], input[type=url], select {
109
- background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 13px; padding: 8px 12px; border-radius: var(--radius); width: 100%; outline: none; transition: border-color 0.2s;
109
+
110
+ input[type=text], input[type=password], input[type=number], input[type=date],
111
+ input[type=tel], input[type=url], input[type=email], select {
112
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text);
113
+ font-family: 'DM Sans', sans-serif; font-size: 13px; padding: 8px 12px;
114
+ border-radius: var(--radius); width: 100%; outline: none; transition: border-color 0.2s;
110
115
  }
111
116
  input:focus { border-color: var(--accent); }
112
-
117
+
113
118
  .btn-row { margin-top: 24px; display: flex; gap: 12px; }
114
119
  .btn { background: var(--accent); color: #0e0c09; border: none; padding: 10px 24px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; }
115
120
  .btn:hover { background: #f0b850; }
116
121
  .btn-secondary { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
117
122
  .btn-secondary:hover { color: var(--text); background: var(--surface); }
118
-
119
- /* FIXED: Response block scroll logic layout rules */
120
- .response-panel {
121
- border-left: 1px solid var(--border);
122
- display: flex;
123
- flex-direction: column;
124
- overflow: hidden;
125
- background: #110e0a;
126
- }
123
+
124
+ .response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: #110e0a; }
127
125
  .response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
128
126
  .response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
129
127
  .status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
130
- .status-ok { background: #1a3a22; color: #6ba05a; }
131
- .status-err { background: #3a1a14; color: #d45c3c; }
128
+ .status-ok { background: #1a3a22; color: #6ba05a; }
129
+ .status-err { background: #3a1a14; color: #d45c3c; }
132
130
  .status-idle { background: var(--surface2); color: var(--text-dim); }
133
-
134
- /* FIXED: Body panel scrolls y natively, inner token element handles x scrolling */
135
- .response-body {
136
- flex: 1;
137
- overflow-y: auto;
138
- overflow-x: hidden;
139
- padding: 0;
140
- background: #0d0b08;
141
- }
142
- .response-body.empty {
143
- color: var(--text-dim);
144
- display: flex;
145
- align-items: center;
146
- justify-content: center;
147
- padding: 20px;
148
- text-align: center;
149
- font-size: 13px;
150
- }
151
-
152
- /* FIXED: Token code output container handles micro horizontal data flows elegantly */
153
- .json-render-block {
154
- display: block;
155
- padding: 20px;
156
- margin: 0;
157
- font-family: 'DM Mono', monospace;
158
- font-size: 12px;
159
- line-height: 1.5;
160
- white-space: pre;
161
- overflow-x: auto;
162
- word-break: normal;
163
- word-wrap: normal;
164
- }
165
-
166
- .json-key { color: #e8a838; }
167
- .json-str { color: #9ab878; }
131
+
132
+ .response-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0; background: #0d0b08; }
133
+ .response-body.empty { color: var(--text-dim); display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center; font-size: 13px; }
134
+
135
+ .json-render-block { display: block; padding: 20px; margin: 0; font-family: 'DM Mono', monospace; font-size: 12px; line-height: 1.5; white-space: pre; overflow-x: auto; word-break: normal; word-wrap: normal; }
136
+
137
+ .json-key { color: #e8a838; }
138
+ .json-str { color: #9ab878; }
168
139
  .json-num { color: #5a86c0; }
169
-
140
+ .json-bool { color: #c47a1e; }
141
+ .json-null { color: var(--text-dim); }
142
+
170
143
  #toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface2); border: 1px solid var(--border); padding: 10px 18px; border-radius: var(--radius); opacity: 0; transition: all .25s; z-index: 1000; font-family: 'DM Mono', monospace; font-size: 12px; color: var(--accent); }
171
144
  #toast.show { opacity: 1; }
145
+
146
+ .empty-state { text-align: center; padding: 60px 20px; color: var(--text-dim); }
147
+ .empty-state .monkey { font-size: 48px; margin-bottom: 16px; }
148
+ .empty-state h2 { color: var(--text); font-family: 'Playfair Display', serif; margin-bottom: 8px; }
172
149
  </style>
173
150
  </head>
174
151
  <body>
175
152
 
176
- <div id="endtester-data-vault" data-payload="${safeJsonString}" style="display: none;"></div>
153
+ <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
177
154
 
178
155
  <header>
179
- <div class="logo">Endtester <span>Application Runtime Sandbox</span></div>
156
+ <div class="logo">🐒 Monkey Tester <span>API Sandbox</span></div>
180
157
  <div class="header-right">
181
158
  <div class="base-url-wrap">
182
159
  <label>TARGET HOST</label>
183
160
  <input id="base-url" type="text" value="">
184
161
  </div>
185
162
  <div class="jwt-wrap">
186
- <label>BEARER AUTH</label>
187
- <input id="jwt-input" type="text" placeholder="Token value...">
163
+ <label>BEARER TOKEN</label>
164
+ <input id="jwt-input" type="text" placeholder="Paste token here...">
188
165
  </div>
189
166
  </div>
190
167
  </header>
@@ -196,210 +173,212 @@ export function getHtmlTemplate(endpoints) {
196
173
  <main id="main-panel"></main>
197
174
  <div class="response-panel">
198
175
  <div class="response-header">
199
- <span class="response-header-title">Response Output</span>
176
+ <span class="response-header-title">Response</span>
200
177
  <span id="status-badge" class="status-badge status-idle">—</span>
201
178
  </div>
202
- <div class="response-body empty" id="response-body">Execute a request row to generate feedback data</div>
179
+ <div class="response-body empty" id="response-body">Execute a request to see the response</div>
203
180
  </div>
204
181
  </div>
205
182
 
206
183
  <div id="toast"></div>
207
184
 
208
185
  <script>
209
- const rawPayload = document.getElementById('endtester-data-vault').getAttribute('data-payload');
210
- const ENDPOINTS = JSON.parse(atob(rawPayload));
186
+ const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
187
+ let currentKey = null;
211
188
 
212
- let currentEp = '';
189
+ document.getElementById('base-url').value = window.location.origin;
213
190
 
214
- document.getElementById('base-url').value = window.location.origin;
191
+ // ── Sidebar ────────────────────────────────────────────────────────────────
192
+ function buildSidebar() {
193
+ const sidebar = document.getElementById('sidebar-nav');
194
+ const keys = Object.keys(ENDPOINTS);
215
195
 
216
- function buildSidebar() {
217
- const sidebar = document.getElementById('sidebar-nav');
218
- const keys = Object.keys(ENDPOINTS);
196
+ if (keys.length === 0) {
197
+ sidebar.innerHTML += '<div style="padding:18px;color:var(--text-dim);font-size:12px">No endpoints discovered.</div>';
198
+ showEmptyState();
199
+ return;
200
+ }
219
201
 
220
- if (keys.length === 0) {
221
- sidebar.innerHTML += '<div style="padding:15px; color:var(--text-dim)">No active application endpoints discovered.</div>';
222
- return;
202
+ keys.forEach((key, i) => {
203
+ const ep = ENDPOINTS[key];
204
+ const item = document.createElement('div');
205
+ item.className = 'nav-item' + (i === 0 ? ' active' : '');
206
+ item.setAttribute('data-key', key);
207
+ item.innerHTML =
208
+ '<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
209
+ '<span class="nav-label">' + ep.path + '</span>';
210
+ item.addEventListener('click', () => {
211
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
212
+ item.classList.add('active');
213
+ clearResponse();
214
+ renderPanel(key);
215
+ });
216
+ sidebar.appendChild(item);
217
+ });
218
+
219
+ renderPanel(keys[0]);
223
220
  }
224
221
 
225
- keys.forEach((key, index) => {
222
+ // ── Panel ──────────────────────────────────────────────────────────────────
223
+ function renderPanel(key) {
224
+ currentKey = key;
226
225
  const ep = ENDPOINTS[key];
227
- const div = document.createElement('div');
228
- div.className = index === 0 ? 'nav-item active' : 'nav-item';
229
- div.setAttribute('data-ep', key);
230
- div.innerHTML = '<span class="method-badge ' + ep.method + '">' + ep.method + '</span><span class="nav-label">' + ep.path + '</span>';
231
- div.addEventListener('click', () => {
232
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
233
- div.classList.add('active');
234
- clearResponse();
235
- renderPanel(key);
236
- });
237
- sidebar.appendChild(div);
238
- });
226
+ const main = document.getElementById('main-panel');
227
+ if (!ep) return;
228
+
229
+ let html =
230
+ '<div class="endpoint-title">' + ep.title + '</div>' +
231
+ '<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
232
+ '<span>' + ep.path + '</span></div>' +
233
+ '<div class="endpoint-desc">' + ep.desc + '</div>';
234
+
235
+ // Path params
236
+ if (ep.params && ep.params.length) {
237
+ html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
238
+ ep.params.forEach(function(p) {
239
+ html +=
240
+ '<div class="field-row">' +
241
+ '<label class="field-label">' + p.label + '</label>' +
242
+ '<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" />' +
243
+ '</div>';
244
+ });
245
+ html += '</div>';
246
+ }
239
247
 
240
- if (keys.length > 0) renderPanel(keys[0]);
241
- }
248
+ // Body fields
249
+ if (ep.fields && ep.fields.length) {
250
+ html += '<div class="form-section"><div class="form-section-title">Request Body</div>';
251
+ ep.fields.forEach(function(f) {
252
+ html +=
253
+ '<div class="field-row">' +
254
+ '<label class="field-label">' + f.label + '</label>' +
255
+ '<input type="' + (f.type || 'text') + '" id="field-' + f.name + '" placeholder="' + (f.placeholder || '') + '" />' +
256
+ '</div>';
257
+ });
258
+ html += '</div>';
259
+ }
242
260
 
243
- function makeInputString(type, id, placeholder) {
244
- const pAttr = placeholder ? ' placeholder="' + placeholder + '"' : '';
245
- return '<input type="' + type + '" id="' + id + '"' + pAttr + ' />';
246
- }
261
+ html +=
262
+ '<div class="btn-row">' +
263
+ '<button class="btn" onclick="sendRequest()">&#9658; Send</button>' +
264
+ '<button class="btn btn-secondary" onclick="clearResponse()">Clear</button>' +
265
+ '</div>';
247
266
 
248
- function renderPanel(epKey) {
249
- currentEp = epKey;
250
- const ep = ENDPOINTS[epKey];
251
- const main = document.getElementById('main-panel');
252
- if (!ep) return;
253
-
254
- let html = \`
255
- <div class="endpoint-title">\${ep.title}</div>
256
- <div class="endpoint-path">
257
- <span class="method-badge \${ep.method}">\${ep.method}</span>
258
- <span>\${ep.path}</span>
259
- </div>
260
- <div class="endpoint-desc">\${ep.desc}</div>
261
- \`;
262
-
263
- if (ep.params && ep.params.length) {
264
- html += \`<div class="form-section"><div class="form-section-title">Path Parameters</div>\`;
265
- ep.params.forEach(function(p) {
266
- const inputHtml = makeInputString('text', 'param-' + p.name, p.placeholder);
267
- html += \`
268
- <div class="field-row">
269
- <label class="field-label">\${p.label}</label>
270
- \${inputHtml}
271
- </div>
272
- \`;
273
- });
274
- html += \`</div>\`;
267
+ main.innerHTML = html;
275
268
  }
276
269
 
277
- if (ep.fields && ep.fields.length) {
278
- html += \`<div class="form-section"><div class="form-section-title">HTTP JSON Request Payload Parameters</div>\`;
279
- ep.fields.forEach(function(f) {
280
- const inputHtml = makeInputString(f.type || 'text', 'field-' + f.name, f.placeholder || '');
281
- html += \`
282
- <div class="field-row">
283
- <label class="field-label">\${f.label}</label>
284
- \${inputHtml}
285
- </div>
286
- \`;
287
- });
288
- html += \`</div>\`;
270
+ function showEmptyState() {
271
+ document.getElementById('main-panel').innerHTML =
272
+ '<div class="empty-state"><div class="monkey">🐒</div>' +
273
+ '<h2>No endpoints found</h2>' +
274
+ '<p>Make sure <code>app.use(endtesterExpress())</code> is added after your routes.</p></div>';
289
275
  }
290
276
 
291
- html += \`
292
- <div class="btn-row">
293
- <button class="btn" onclick="sendRequest()">Execute Route</button>
294
- <button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>
295
- </div>
296
- \`;
277
+ // ── Request ────────────────────────────────────────────────────────────────
278
+ async function sendRequest() {
279
+ const ep = ENDPOINTS[currentKey];
280
+ let path = ep.path;
297
281
 
298
- main.innerHTML = html;
299
- }
282
+ if (ep.params && ep.params.length) {
283
+ for (const p of ep.params) {
284
+ const val = (document.getElementById('param-' + p.name) || {}).value || '';
285
+ if (!val.trim()) { showToast('⚠ Path param "' + p.label + '" is required'); return; }
286
+ path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
287
+ }
288
+ }
300
289
 
301
- async function sendRequest() {
302
- const ep = ENDPOINTS[currentEp];
303
- let path = ep.path;
290
+ const baseUrl = document.getElementById('base-url').value.replace(/\/+$/, '');
291
+ const url = baseUrl + path;
292
+ const headers = { 'Content-Type': 'application/json' };
293
+ const jwt = document.getElementById('jwt-input').value.trim();
294
+ if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
295
+
296
+ let body = undefined;
297
+ if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
298
+ const payload = {};
299
+ let hasData = false;
300
+
301
+ ep.fields.forEach(function(f) {
302
+ const el = document.getElementById('field-' + f.name);
303
+ if (!el) return;
304
+ let v = el.value.trim();
305
+ if (v === '') return; // Avoid submitting empty strings for unfilled fields
306
+
307
+ if (f.type === 'number') v = Number(v);
308
+ payload[f.name] = v;
309
+ hasData = true;
310
+ });
311
+
312
+ if (hasData) body = JSON.stringify(payload);
313
+ }
304
314
 
305
- if (ep.params) {
306
- for (const p of ep.params) {
307
- const val = document.getElementById('param-' + p.name)?.value.trim();
308
- if (!val) { showToast('Warning: Parameter ' + p.label + ' is required'); return; }
309
- path = path.replace(':' + p.name, encodeURIComponent(val));
315
+ setResponse(null, 'loading');
316
+ const t0 = Date.now();
317
+
318
+ try {
319
+ const res = await fetch(url, { method: ep.method, headers, body });
320
+ const ms = Date.now() - t0;
321
+ const text = await res.text();
322
+ let data;
323
+ try { data = JSON.parse(text); } catch { data = text; }
324
+ setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
325
+ } catch (err) {
326
+ setResponse({ error: err.message }, 'err', 'FAIL', 0);
310
327
  }
311
328
  }
312
329
 
313
- const baseUrl = document.getElementById('base-url').value.replace(/[/]+$/, '');
314
- const url = baseUrl + path;
315
- const headers = { 'Content-Type': 'application/json' };
316
-
317
- const jwt = document.getElementById('jwt-input').value.trim();
318
- if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
319
-
320
- let body = undefined;
321
- if (ep.fields && ep.fields.length && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
322
- const jsonPayload = {};
323
- for (const f of ep.fields) {
324
- const targetEl = document.getElementById('field-' + f.name);
325
- if (targetEl) {
326
- let entryVal = targetEl.value.trim();
327
- if (f.type === 'number' && entryVal !== '') {
328
- entryVal = Number(entryVal);
329
- }
330
- jsonPayload[f.name] = entryVal;
331
- }
330
+ // ── Response ───────────────────────────────────────────────────────────────
331
+ function setResponse(data, state, status, ms) {
332
+ const badge = document.getElementById('status-badge');
333
+ const body = document.getElementById('response-body');
334
+
335
+ if (state === 'loading') {
336
+ badge.className = 'status-badge status-idle';
337
+ badge.textContent = '…';
338
+ body.className = 'response-body empty';
339
+ body.textContent = 'Sending…';
340
+ return;
332
341
  }
333
- body = JSON.stringify(jsonPayload);
334
- }
335
342
 
336
- setResponse(null, 'loading');
337
- const start = Date.now();
338
-
339
- try {
340
- const res = await fetch(url, { method: ep.method, headers, body });
341
- const ms = Date.now() - start;
342
- const text = await res.text();
343
- let json;
344
- try { json = JSON.parse(text); } catch { json = text; }
345
- setResponse(json, res.ok ? 'ok' : 'err', res.status, ms);
346
- } catch (err) {
347
- setResponse({ error: err.message }, 'err', 'FAIL', 0);
343
+ badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
344
+ badge.textContent = status + ' · ' + ms + 'ms';
345
+ body.className = 'response-body';
346
+ const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
347
+ body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
348
348
  }
349
- }
350
-
351
- function setResponse(data, state, status, ms) {
352
- const badge = document.getElementById('status-badge');
353
- const body = document.getElementById('response-body');
354
349
 
355
- if (state === 'loading') {
356
- badge.className = 'status-badge status-idle';
357
- badge.textContent = '...';
350
+ function clearResponse() {
351
+ document.getElementById('status-badge').className = 'status-badge status-idle';
352
+ document.getElementById('status-badge').textContent = '';
353
+ const body = document.getElementById('response-body');
358
354
  body.className = 'response-body empty';
359
- body.innerHTML = 'Executing transmission...';
360
- return;
355
+ body.textContent = 'Execute a request to see the response';
361
356
  }
362
357
 
363
- badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
364
- badge.textContent = status + ' · ' + ms + 'ms';
365
- body.className = 'response-body';
366
-
367
- // FIXED: Nested rendering inside dedicated horizontal scrolling code tag block wrapper elements
368
- const outputString = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
369
- body.innerHTML = '<pre class="json-render-block">' + highlightJson(outputString) + '</pre>';
370
- }
371
-
372
- function clearResponse() {
373
- const badge = document.getElementById('status-badge');
374
- const body = document.getElementById('response-body');
375
- badge.className = 'status-badge status-idle';
376
- badge.textContent = '—';
377
- body.className = 'response-body empty';
378
- body.textContent = 'Execute a request row to generate feedback data';
379
- }
380
-
381
- function highlightJson(str) {
382
- return str
383
- .replace(/&/g, '&amp;').replace(/[<]/g, '&lt;').replace(/[>]/g, '&gt;')
384
- .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
385
- if (/^"/.test(match)) {
386
- if (/:$/.test(match)) return '<span class="json-key">' + match + '</span>';
387
- return '<span class="json-str">' + match + '</span>';
388
- }
389
- return '<span class="json-num">' + match + '</span>';
390
- });
391
- }
358
+ function highlight(str) {
359
+ return str
360
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
361
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
362
+ if (/^"/.test(m)) return /:$/.test(m)
363
+ ? '<span class="json-key">' + m + '</span>'
364
+ : '<span class="json-str">' + m + '</span>';
365
+ if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
366
+ if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
367
+ return '<span class="json-num">' + m + '</span>';
368
+ });
369
+ }
392
370
 
393
- function showToast(msg) {
394
- const t = document.getElementById('toast');
395
- t.textContent = msg;
396
- t.classList.add('show');
397
- setTimeout(() => t.classList.remove('show'), 2500);
398
- }
371
+ function showToast(msg) {
372
+ const t = document.getElementById('toast');
373
+ t.textContent = msg;
374
+ t.classList.add('show');
375
+ setTimeout(() => t.classList.remove('show'), 2500);
376
+ }
399
377
 
400
- buildSidebar();
378
+ buildSidebar();
401
379
  </script>
402
380
  </body>
403
- </html>
404
- `;
381
+ </html>`;
405
382
  }
383
+
384
+ module.exports = { getHtmlTemplate };
package/index.js CHANGED
@@ -1,246 +1,5 @@
1
- import { getHtmlTemplate } from './htmlTemplate.js';
1
+ 'use strict';
2
2
 
3
- export function endtesterExpress() {
4
- return (req, res, next) => {
5
- if (
6
- req.path !== '/api/tester' &&
7
- req.path !== '/api/tester/'
8
- ) {
9
- return next();
10
- }
3
+ const { endtesterExpress } = require('./monkey.js');
11
4
 
12
- const expressApp = req.app;
13
- const detectedEndpoints = {};
14
-
15
- // =========================
16
- // Detect Input Types
17
- // =========================
18
- function detectInputType(field) {
19
- const lower = field.toLowerCase();
20
-
21
- if (lower.includes('email')) return 'email';
22
- if (lower.includes('password')) return 'password';
23
- if (lower.includes('date')) return 'date';
24
-
25
- if (
26
- lower.includes('age') ||
27
- lower.includes('price') ||
28
- lower.includes('salary') ||
29
- lower.includes('stock') ||
30
- lower.includes('quantity') ||
31
- lower.includes('count') ||
32
- lower.includes('total') ||
33
- lower.includes('amount') ||
34
- lower.includes('id')
35
- ) {
36
- return 'number';
37
- }
38
-
39
- if (
40
- lower.includes('phone') ||
41
- lower.includes('tel')
42
- ) {
43
- return 'tel';
44
- }
45
-
46
- if (
47
- lower.includes('url') ||
48
- lower.includes('website')
49
- ) {
50
- return 'url';
51
- }
52
-
53
- return 'text';
54
- }
55
-
56
- // =========================
57
- // Context-Aware Static Fallbacks
58
- // =========================
59
- function getFallbackFieldsForPath(path) {
60
- const lowerPath = path.toLowerCase();
61
- let rawFields = [];
62
-
63
- if (lowerPath.includes('login') || lowerPath.includes('auth') || lowerPath.includes('signin')) {
64
- rawFields = ['email', 'password'];
65
- } else if (lowerPath.includes('user') || lowerPath.includes('register') || lowerPath.includes('signup')) {
66
- rawFields = ['username', 'email', 'password'];
67
- } else if (lowerPath.includes('product')) {
68
- rawFields = ['name', 'price', 'stock'];
69
- } else {
70
- return [];
71
- }
72
-
73
- return rawFields.map(field => ({
74
- name: field,
75
- label: field.charAt(0).toUpperCase() + field.slice(1),
76
- type: detectInputType(field),
77
- placeholder: `Enter ${field}`
78
- }));
79
- }
80
-
81
- // =========================
82
- // Extract req.body fields via Source Inspection
83
- // =========================
84
- function extractBodyFields(handler) {
85
- try {
86
- const source = handler.toString();
87
-
88
- // If it's a bound handler or lacks source reference code text
89
- if (!source || source.includes('[native code]')) return [];
90
-
91
- const regex = /(const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/gs;
92
- const matches = [...source.matchAll(regex)];
93
- const fields = [];
94
-
95
- matches.forEach((match) => {
96
- const cleanedVariablesBlock = match[2]
97
- .replace(/\/\/.*$/gm, '')
98
- .replace(/\/\*[\s\S]*?\*\//g, '')
99
- .replace(/[\r\n\t]/g, ' ');
100
-
101
- const variables = cleanedVariablesBlock
102
- .split(',')
103
- .map(v => v.trim())
104
- .filter(Boolean);
105
-
106
- variables.forEach((field) => {
107
- let realField = field;
108
-
109
- if (field.includes(':')) {
110
- realField = field.split(':')[0].trim();
111
- }
112
-
113
- if (realField.includes('=')) {
114
- realField = realField.split('=')[0].trim();
115
- }
116
-
117
- realField = realField.trim();
118
-
119
- const alreadyExists = fields.find(f => f.name === realField);
120
-
121
- if (!alreadyExists && realField) {
122
- fields.push({
123
- name: realField,
124
- label: realField.charAt(0).toUpperCase() + realField.slice(1),
125
- type: detectInputType(realField),
126
- placeholder: `Enter ${realField}`
127
- });
128
- }
129
- });
130
- });
131
-
132
- return fields;
133
- } catch (err) {
134
- console.error('Field extraction error:', err);
135
- return [];
136
- }
137
- }
138
-
139
- // =========================
140
- // Parse Express Stack
141
- // =========================
142
- function parseStack(stack, prefix = '') {
143
- if (!stack) return;
144
-
145
- stack.forEach((layer) => {
146
- // =========================
147
- // ROUTES
148
- // =========================
149
- if (layer.route) {
150
- const methods = Object.keys(layer.route.methods);
151
- const path = (prefix + layer.route.path).replace(/\/+/g, '/');
152
-
153
- if (path.includes('/api/tester')) {
154
- return;
155
- }
156
-
157
- methods.forEach((method) => {
158
- const httpMethod = method.toUpperCase();
159
- const key = `${httpMethod.toLowerCase()}-` + path.replace(/[^a-zA-Z0-9]/g, '-');
160
-
161
- const pathParams = layer.route.keys
162
- ? layer.route.keys.map((k) => ({
163
- name: k.name,
164
- label: k.name.toUpperCase(),
165
- placeholder: 'value'
166
- }))
167
- : [];
168
-
169
- // =========================
170
- // BODY FIELDS COMPILING
171
- // =========================
172
- let bodyFields = [];
173
-
174
- if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
175
- layer.route.stack.forEach((stackLayer) => {
176
- if (stackLayer.handle && typeof stackLayer.handle === 'function') {
177
- const extractedFields = extractBodyFields(stackLayer.handle);
178
- bodyFields.push(...extractedFields);
179
- }
180
- });
181
-
182
- // Deduplicate discovered elements
183
- bodyFields = bodyFields.filter(
184
- (field, index, self) => index === self.findIndex(f => f.name === field.name)
185
- );
186
-
187
- // CRITICAL FAILSAFE: If code reflection extracted nothing, apply smart path-based fallback fields
188
- if (bodyFields.length === 0) {
189
- bodyFields = getFallbackFieldsForPath(path);
190
- }
191
- }
192
-
193
- detectedEndpoints[key] = {
194
- method: httpMethod,
195
- path,
196
- title: `${httpMethod} ${path}`,
197
- desc: `Auto-discovered endpoint: ${path}`,
198
- params: pathParams,
199
- fields: bodyFields
200
- };
201
- });
202
- }
203
-
204
- // =========================
205
- // NESTED ROUTERS
206
- // =========================
207
- else if (
208
- layer.name === 'router' &&
209
- layer.handle &&
210
- layer.handle.stack
211
- ) {
212
- let routerPath = '';
213
-
214
- if (layer.regexp) {
215
- const match = layer.regexp
216
- .toString()
217
- .match(/^\/\^\\(.*?)\\\/\?/);
218
-
219
- if (match && match[1]) {
220
- routerPath = match[1].replace(/\\/g, '');
221
- }
222
- }
223
-
224
- parseStack(
225
- layer.handle.stack,
226
- prefix + '/' + routerPath
227
- );
228
- }
229
- });
230
- }
231
-
232
- // =========================
233
- // START PARSING
234
- // =========================
235
- if (expressApp._router && expressApp._router.stack) {
236
- parseStack(expressApp._router.stack);
237
- }
238
-
239
- // =========================
240
- // RENDER HTML
241
- // =========================
242
- const fullHtml = getHtmlTemplate(detectedEndpoints);
243
- res.setHeader('Content-Type', 'text/html');
244
- return res.send(fullHtml);
245
- };
246
- }
5
+ module.exports = { endtesterExpress };
package/monkey.js ADDED
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ const { getHtmlTemplate } = require('./htmlTemplate');
4
+
5
+ // ─── Field type inference ─────────────────────────────────────────────────────
6
+ function inferType(name) {
7
+ const n = name.toLowerCase();
8
+ if (n.includes('email')) return 'email';
9
+ if (n.includes('password') || n.includes('pass')) return 'password';
10
+ if (n.includes('date') || n.includes('birth')) return 'date';
11
+ if (n.includes('phone') || n.includes('tel')) return 'tel';
12
+ if (n.includes('url') || n.includes('website') || n.includes('link')) return 'url';
13
+ if (
14
+ n.includes('age') || n.includes('price') || n.includes('amount') ||
15
+ n.includes('count') || n.includes('qty') || n.includes('quantity') ||
16
+ n.includes('stock') || n.includes('salary') || n.includes('total') ||
17
+ (n === 'id') || n.endsWith('_id') || n.endsWith('Id')
18
+ ) return 'number';
19
+ return 'text';
20
+ }
21
+
22
+ function buildField(name) {
23
+ return {
24
+ name,
25
+ label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
26
+ type: inferType(name),
27
+ placeholder: `Enter ${name}`
28
+ };
29
+ }
30
+
31
+ // ─── Extract req.body fields from handler source ──────────────────────────────
32
+ function extractBodyFields(handler) {
33
+ try {
34
+ const source = handler.toString();
35
+ if (!source || source.includes('[native code]')) return [];
36
+
37
+ const seen = new Map();
38
+
39
+ // Pattern 1 — destructuring: const { email, password } = req.body
40
+ const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
41
+ let m;
42
+ while ((m = destructRe.exec(source)) !== null) {
43
+ m[1].split(',').forEach(part => {
44
+ const name = part.split(':')[0].split('=')[0].trim();
45
+ if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
46
+ seen.set(name, buildField(name));
47
+ }
48
+ });
49
+ }
50
+
51
+ // Pattern 2 — property access: req.body.email / req.body['email']
52
+ const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
53
+ while ((m = accessRe.exec(source)) !== null) {
54
+ const name = m[1] || m[2];
55
+ if (name && !seen.has(name)) seen.set(name, buildField(name));
56
+ }
57
+
58
+ return Array.from(seen.values());
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+
64
+ // ─── Path-based fallback fields ───────────────────────────────────────────────
65
+ function fallbackFields(path) {
66
+ const p = path.toLowerCase();
67
+
68
+ if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) {
69
+ return ['email', 'password'].map(buildField);
70
+ }
71
+ if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) {
72
+ return ['username', 'email', 'password'].map(buildField);
73
+ }
74
+ if (p.includes('user')) {
75
+ return ['username', 'email', 'password'].map(buildField);
76
+ }
77
+ if (p.includes('product')) {
78
+ return ['name', 'price', 'stock'].map(buildField);
79
+ }
80
+ if (p.includes('order')) {
81
+ return ['productId', 'quantity', 'address'].map(buildField);
82
+ }
83
+ return [];
84
+ }
85
+
86
+ // ─── Extract router prefix safely from Express layer ─────────────────────────
87
+ function extractRouterPrefix(layer) {
88
+ if (!layer.regexp) return '';
89
+ if (layer.path) return layer.path; // If explicit paths exist
90
+
91
+ let src = layer.regexp.source;
92
+
93
+ // Strip out boilerplate generated patterns from default express mounting structures
94
+ src = src
95
+ .replace('^\\/', '/')
96
+ .replace('\\/?(?=\\/|$)', '')
97
+ .replace('(?:\\/(?=$))?(?=\\/|$)', '')
98
+ .replace('\\/', '/');
99
+
100
+ const cleanPath = src.split('(?')[0].replace(/\\/g, '');
101
+ return cleanPath === '/' ? '' : cleanPath;
102
+ }
103
+
104
+ // ─── Walk the Express router stack recursively ────────────────────────────────
105
+ function parseStack(stack, detectedEndpoints, prefix = '') {
106
+ if (!Array.isArray(stack)) return;
107
+
108
+ for (const layer of stack) {
109
+ // ── Named route (app.get / app.post …) ──────────────────────────────────
110
+ if (layer.route) {
111
+ const rawPath = typeof layer.route.path === 'string'
112
+ ? layer.route.path
113
+ : (layer.route.path ? String(layer.route.path) : '');
114
+
115
+ const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
116
+
117
+ if (fullPath.startsWith('/api/tester')) continue;
118
+
119
+ const methods = Object.keys(layer.route.methods || {});
120
+
121
+ for (const method of methods) {
122
+ const httpMethod = method.toUpperCase();
123
+ const key = `${httpMethod}::${fullPath}`;
124
+
125
+ // ── Path params (:id, :slug …) safely parsed using matchAll ─────────
126
+ const pathParams = [];
127
+ const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
128
+ const matches = [...fullPath.matchAll(paramRe)];
129
+
130
+ for (const pm of matches) {
131
+ pathParams.push({
132
+ name: pm[1],
133
+ label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1),
134
+ placeholder: 'value'
135
+ });
136
+ }
137
+
138
+ // ── Body fields ────────────────────────────────────────────────────
139
+ let bodyFields = [];
140
+ if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
141
+ const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
142
+ for (const handler of handlers) {
143
+ bodyFields.push(...extractBodyFields(handler));
144
+ }
145
+ const seen = new Map();
146
+ bodyFields = bodyFields.filter(f => {
147
+ if (seen.has(f.name)) return false;
148
+ seen.set(f.name, true);
149
+ return true;
150
+ });
151
+ if (bodyFields.length === 0) {
152
+ bodyFields = fallbackFields(fullPath);
153
+ }
154
+ }
155
+
156
+ detectedEndpoints[key] = {
157
+ method: httpMethod,
158
+ path: fullPath,
159
+ title: `${httpMethod} ${fullPath}`,
160
+ desc: `Auto-discovered endpoint — ${fullPath}`,
161
+ params: pathParams,
162
+ fields: bodyFields,
163
+ };
164
+ }
165
+ }
166
+
167
+ // ── Nested router (app.use('/prefix', router)) ───────────────────────────
168
+ else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
169
+ const routerPrefix = extractRouterPrefix(layer);
170
+ parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
171
+ }
172
+ }
173
+ }
174
+
175
+ // ─── Middleware ───────────────────────────────────────────────────────────────
176
+ function endtesterExpress() {
177
+ return function monkeyTesterMiddleware(req, res, next) {
178
+ if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
179
+ return next();
180
+ }
181
+
182
+ const app = req.app;
183
+ const detectedEndpoints = {};
184
+
185
+ const rootStack =
186
+ (app._router && app._router.stack) || // Express 4
187
+ (app.router && app.router.stack) || // Express 5
188
+ [];
189
+
190
+ parseStack(rootStack, detectedEndpoints);
191
+
192
+ const html = getHtmlTemplate(detectedEndpoints);
193
+ res.setHeader('Content-Type', 'text/html');
194
+ return res.send(html);
195
+ };
196
+ }
197
+
198
+ module.exports = { endtesterExpress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",