@aimeloic/monkey-tester 2.0.5 → 2.0.6

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 +243 -270
  2. package/index.js +3 -244
  3. package/monkey.js +204 -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,206 @@ 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
+ ep.fields.forEach(function(f) {
300
+ const el = document.getElementById('field-' + f.name);
301
+ if (!el) return;
302
+ let v = el.value.trim();
303
+ if (f.type === 'number' && v !== '') v = Number(v);
304
+ payload[f.name] = v;
305
+ });
306
+ body = JSON.stringify(payload);
307
+ }
304
308
 
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));
309
+ setResponse(null, 'loading');
310
+ const t0 = Date.now();
311
+
312
+ try {
313
+ const res = await fetch(url, { method: ep.method, headers, body });
314
+ const ms = Date.now() - t0;
315
+ const text = await res.text();
316
+ let data;
317
+ try { data = JSON.parse(text); } catch { data = text; }
318
+ setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
319
+ } catch (err) {
320
+ setResponse({ error: err.message }, 'err', 'FAIL', 0);
310
321
  }
311
322
  }
312
323
 
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
- }
324
+ // ── Response ───────────────────────────────────────────────────────────────
325
+ function setResponse(data, state, status, ms) {
326
+ const badge = document.getElementById('status-badge');
327
+ const body = document.getElementById('response-body');
328
+
329
+ if (state === 'loading') {
330
+ badge.className = 'status-badge status-idle';
331
+ badge.textContent = '…';
332
+ body.className = 'response-body empty';
333
+ body.textContent = 'Sending…';
334
+ return;
332
335
  }
333
- body = JSON.stringify(jsonPayload);
334
- }
335
336
 
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);
337
+ badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
338
+ badge.textContent = status + ' · ' + ms + 'ms';
339
+ body.className = 'response-body';
340
+ const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
341
+ body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
348
342
  }
349
- }
350
-
351
- function setResponse(data, state, status, ms) {
352
- const badge = document.getElementById('status-badge');
353
- const body = document.getElementById('response-body');
354
343
 
355
- if (state === 'loading') {
356
- badge.className = 'status-badge status-idle';
357
- badge.textContent = '...';
344
+ function clearResponse() {
345
+ document.getElementById('status-badge').className = 'status-badge status-idle';
346
+ document.getElementById('status-badge').textContent = '';
347
+ const body = document.getElementById('response-body');
358
348
  body.className = 'response-body empty';
359
- body.innerHTML = 'Executing transmission...';
360
- return;
349
+ body.textContent = 'Execute a request to see the response';
361
350
  }
362
351
 
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
- }
352
+ function highlight(str) {
353
+ return str
354
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
355
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
356
+ if (/^"/.test(m)) return /:$/.test(m)
357
+ ? '<span class="json-key">' + m + '</span>'
358
+ : '<span class="json-str">' + m + '</span>';
359
+ if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
360
+ if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
361
+ return '<span class="json-num">' + m + '</span>';
362
+ });
363
+ }
392
364
 
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
- }
365
+ function showToast(msg) {
366
+ const t = document.getElementById('toast');
367
+ t.textContent = msg;
368
+ t.classList.add('show');
369
+ setTimeout(() => t.classList.remove('show'), 2500);
370
+ }
399
371
 
400
- buildSidebar();
372
+ buildSidebar();
401
373
  </script>
402
374
  </body>
403
- </html>
404
- `;
375
+ </html>`;
405
376
  }
377
+
378
+ 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,204 @@
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
+ // handle rename (email: userEmail) and default (name = '')
45
+ const name = part.split(':')[0].split('=')[0].trim();
46
+ if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
47
+ seen.set(name, buildField(name));
48
+ }
49
+ });
50
+ }
51
+
52
+ // Pattern 2 — property access: req.body.email / req.body['email']
53
+ const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
54
+ while ((m = accessRe.exec(source)) !== null) {
55
+ const name = m[1] || m[2];
56
+ if (name && !seen.has(name)) seen.set(name, buildField(name));
57
+ }
58
+
59
+ return Array.from(seen.values());
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ // ─── Path-based fallback fields ───────────────────────────────────────────────
66
+ function fallbackFields(path) {
67
+ const p = path.toLowerCase();
68
+
69
+ if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) {
70
+ return ['email', 'password'].map(buildField);
71
+ }
72
+ if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) {
73
+ return ['username', 'email', 'password'].map(buildField);
74
+ }
75
+ if (p.includes('user')) {
76
+ return ['username', 'email', 'password'].map(buildField);
77
+ }
78
+ if (p.includes('product')) {
79
+ return ['name', 'price', 'stock'].map(buildField);
80
+ }
81
+ if (p.includes('order')) {
82
+ return ['productId', 'quantity', 'address'].map(buildField);
83
+ }
84
+ return [];
85
+ }
86
+
87
+ // ─── Extract router prefix from Express layer regexp ─────────────────────────
88
+ function extractRouterPrefix(layer) {
89
+ if (!layer.regexp) return '';
90
+ const src = layer.regexp.source;
91
+
92
+ // Express serialises router paths as: ^\/<prefix>(?:\/(?=$))?(?=\/|$)
93
+ const patterns = [
94
+ /^\^\\\/([^\\?$]+)/, // common format
95
+ /^\^\\\/([a-zA-Z0-9_/-]+)/, // simple paths
96
+ ];
97
+ for (const re of patterns) {
98
+ const m = re.exec(src);
99
+ if (m && m[1]) {
100
+ return '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '');
101
+ }
102
+ }
103
+ // Fallback: check .regexp property stored by Express (Express 5 style)
104
+ if (layer.regexp && layer.regexp.fast_slash) return '';
105
+ return '';
106
+ }
107
+
108
+ // ─── Walk the Express router stack recursively ────────────────────────────────
109
+ function parseStack(stack, detectedEndpoints, prefix = '') {
110
+ if (!Array.isArray(stack)) return;
111
+
112
+ for (const layer of stack) {
113
+ // ── Named route (app.get / app.post …) ──────────────────────────────────
114
+ if (layer.route) {
115
+ const rawPath = typeof layer.route.path === 'string'
116
+ ? layer.route.path
117
+ : (layer.route.path ? String(layer.route.path) : '');
118
+
119
+ const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
120
+
121
+ // skip the tester's own route
122
+ if (fullPath.startsWith('/api/tester')) continue;
123
+
124
+ const methods = Object.keys(layer.route.methods || {});
125
+
126
+ for (const method of methods) {
127
+ const httpMethod = method.toUpperCase();
128
+ const key = `${httpMethod}::${fullPath}`;
129
+
130
+ // ── Path params (:id, :slug …) ─────────────────────────────────────
131
+ const pathParams = [];
132
+ const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
133
+ let pm;
134
+ while ((pm = paramRe.exec(fullPath)) !== null) {
135
+ pathParams.push({
136
+ name: pm[1],
137
+ label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1),
138
+ placeholder: 'value'
139
+ });
140
+ }
141
+
142
+ // ── Body fields ────────────────────────────────────────────────────
143
+ let bodyFields = [];
144
+ if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
145
+ const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
146
+ for (const handler of handlers) {
147
+ bodyFields.push(...extractBodyFields(handler));
148
+ }
149
+ // deduplicate by name
150
+ const seen = new Map();
151
+ bodyFields = bodyFields.filter(f => {
152
+ if (seen.has(f.name)) return false;
153
+ seen.set(f.name, true);
154
+ return true;
155
+ });
156
+ // fallback if extraction produced nothing
157
+ if (bodyFields.length === 0) {
158
+ bodyFields = fallbackFields(fullPath);
159
+ }
160
+ }
161
+
162
+ detectedEndpoints[key] = {
163
+ method: httpMethod,
164
+ path: fullPath,
165
+ title: `${httpMethod} ${fullPath}`,
166
+ desc: `Auto-discovered endpoint — ${fullPath}`,
167
+ params: pathParams,
168
+ fields: bodyFields,
169
+ };
170
+ }
171
+ }
172
+
173
+ // ── Nested router (app.use('/prefix', router)) ───────────────────────────
174
+ else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
175
+ const routerPrefix = extractRouterPrefix(layer);
176
+ parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
177
+ }
178
+ }
179
+ }
180
+
181
+ // ─── Middleware ───────────────────────────────────────────────────────────────
182
+ function endtesterExpress() {
183
+ return function monkeyTesterMiddleware(req, res, next) {
184
+ if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
185
+ return next();
186
+ }
187
+
188
+ const app = req.app;
189
+ const detectedEndpoints = {};
190
+
191
+ const rootStack =
192
+ (app._router && app._router.stack) || // Express 4
193
+ (app.router && app.router.stack) || // Express 5 preview
194
+ [];
195
+
196
+ parseStack(rootStack, detectedEndpoints);
197
+
198
+ const html = getHtmlTemplate(detectedEndpoints);
199
+ res.setHeader('Content-Type', 'text/html');
200
+ return res.send(html);
201
+ };
202
+ }
203
+
204
+ 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.6",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",