@aimeloic/monkey-tester 3.0.3 → 3.0.5

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 (3) hide show
  1. package/htmlTemplate.js +39 -44
  2. package/monkey.js +33 -26
  3. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -183,15 +183,14 @@ function getHtmlTemplate(endpoints) {
183
183
  <div id="toast"></div>
184
184
 
185
185
  <script>
186
- const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
187
- let currentKey = null;
186
+ var ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
187
+ var currentKey = null;
188
188
 
189
189
  document.getElementById('base-url').value = window.location.origin;
190
190
 
191
- // ── Sidebar ────────────────────────────────────────────────────────────────
192
191
  function buildSidebar() {
193
- const sidebar = document.getElementById('sidebar-nav');
194
- const keys = Object.keys(ENDPOINTS);
192
+ var sidebar = document.getElementById('sidebar-nav');
193
+ var keys = Object.keys(ENDPOINTS);
195
194
 
196
195
  if (keys.length === 0) {
197
196
  sidebar.innerHTML += '<div style="padding:18px;color:var(--text-dim);font-size:12px">No endpoints discovered.</div>';
@@ -199,16 +198,16 @@ function getHtmlTemplate(endpoints) {
199
198
  return;
200
199
  }
201
200
 
202
- keys.forEach((key, i) => {
203
- const ep = ENDPOINTS[key];
204
- const item = document.createElement('div');
201
+ keys.forEach(function(key, i) {
202
+ var ep = ENDPOINTS[key];
203
+ var item = document.createElement('div');
205
204
  item.className = 'nav-item' + (i === 0 ? ' active' : '');
206
205
  item.setAttribute('data-key', key);
207
206
  item.innerHTML =
208
207
  '<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
209
208
  '<span class="nav-label">' + ep.path + '</span>';
210
- item.addEventListener('click', () => {
211
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
209
+ item.addEventListener('click', function() {
210
+ document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
212
211
  item.classList.add('active');
213
212
  clearResponse();
214
213
  renderPanel(key);
@@ -219,20 +218,18 @@ function getHtmlTemplate(endpoints) {
219
218
  renderPanel(keys[0]);
220
219
  }
221
220
 
222
- // ── Panel ──────────────────────────────────────────────────────────────────
223
221
  function renderPanel(key) {
224
222
  currentKey = key;
225
- const ep = ENDPOINTS[key];
226
- const main = document.getElementById('main-panel');
223
+ var ep = ENDPOINTS[key];
224
+ var main = document.getElementById('main-panel');
227
225
  if (!ep) return;
228
226
 
229
- let html =
227
+ var html =
230
228
  '<div class="endpoint-title">' + ep.title + '</div>' +
231
229
  '<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
232
230
  '<span>' + ep.path + '</span></div>' +
233
231
  '<div class="endpoint-desc">' + ep.desc + '</div>';
234
232
 
235
- // Path params
236
233
  if (ep.params && ep.params.length) {
237
234
  html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
238
235
  ep.params.forEach(function(p) {
@@ -245,7 +242,6 @@ function getHtmlTemplate(endpoints) {
245
242
  html += '</div>';
246
243
  }
247
244
 
248
- // Body fields
249
245
  if (ep.fields && ep.fields.length) {
250
246
  html += '<div class="form-section"><div class="form-section-title">Request Body</div>';
251
247
  ep.fields.forEach(function(f) {
@@ -274,35 +270,35 @@ function getHtmlTemplate(endpoints) {
274
270
  '<p>Make sure <code>app.use(endtesterExpress())</code> is added after your routes.</p></div>';
275
271
  }
276
272
 
277
- // ── Request ────────────────────────────────────────────────────────────────
278
273
  async function sendRequest() {
279
- const ep = ENDPOINTS[currentKey];
280
- let path = ep.path;
274
+ var ep = ENDPOINTS[currentKey];
275
+ var path = ep.path;
281
276
 
282
277
  if (ep.params && ep.params.length) {
283
- for (const p of ep.params) {
284
- const val = (document.getElementById('param-' + p.name) || {}).value || '';
278
+ for (var i = 0; i < ep.params.length; i++) {
279
+ var p = ep.params[i];
280
+ var val = (document.getElementById('param-' + p.name) || {}).value || '';
285
281
  if (!val.trim()) { showToast('⚠ Path param "' + p.label + '" is required'); return; }
286
282
  path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
287
283
  }
288
284
  }
289
285
 
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();
286
+ var baseUrl = document.getElementById('base-url').value.replace(/\/+$/, '');
287
+ var url = baseUrl + path;
288
+ var headers = { 'Content-Type': 'application/json' };
289
+ var jwt = document.getElementById('jwt-input').value.trim();
294
290
  if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
295
291
 
296
- let body = undefined;
292
+ var body = undefined;
297
293
  if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
298
- const payload = {};
299
- let hasData = false;
294
+ var payload = {};
295
+ var hasData = false;
300
296
 
301
297
  ep.fields.forEach(function(f) {
302
- const el = document.getElementById('field-' + f.name);
298
+ var el = document.getElementById('field-' + f.name);
303
299
  if (!el) return;
304
- let v = el.value.trim();
305
- if (v === '') return; // Avoid submitting empty strings for unfilled fields
300
+ var v = el.value.trim();
301
+ if (v === '') return;
306
302
 
307
303
  if (f.type === 'number') v = Number(v);
308
304
  payload[f.name] = v;
@@ -313,24 +309,23 @@ function getHtmlTemplate(endpoints) {
313
309
  }
314
310
 
315
311
  setResponse(null, 'loading');
316
- const t0 = Date.now();
312
+ var t0 = Date.now();
317
313
 
318
314
  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; }
315
+ var res = await fetch(url, { method: ep.method, headers: headers, body: body });
316
+ var ms = Date.now() - t0;
317
+ var text = await res.text();
318
+ var data;
319
+ try { data = JSON.parse(text); } catch(e) { data = text; }
324
320
  setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
325
321
  } catch (err) {
326
322
  setResponse({ error: err.message }, 'err', 'FAIL', 0);
327
323
  }
328
324
  }
329
325
 
330
- // ── Response ───────────────────────────────────────────────────────────────
331
326
  function setResponse(data, state, status, ms) {
332
- const badge = document.getElementById('status-badge');
333
- const body = document.getElementById('response-body');
327
+ var badge = document.getElementById('status-badge');
328
+ var body = document.getElementById('response-body');
334
329
 
335
330
  if (state === 'loading') {
336
331
  badge.className = 'status-badge status-idle';
@@ -343,14 +338,14 @@ function getHtmlTemplate(endpoints) {
343
338
  badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
344
339
  badge.textContent = status + ' · ' + ms + 'ms';
345
340
  body.className = 'response-body';
346
- const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
341
+ var str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
347
342
  body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
348
343
  }
349
344
 
350
345
  function clearResponse() {
351
346
  document.getElementById('status-badge').className = 'status-badge status-idle';
352
347
  document.getElementById('status-badge').textContent = '—';
353
- const body = document.getElementById('response-body');
348
+ var body = document.getElementById('response-body');
354
349
  body.className = 'response-body empty';
355
350
  body.textContent = 'Execute a request to see the response';
356
351
  }
@@ -369,10 +364,10 @@ function getHtmlTemplate(endpoints) {
369
364
  }
370
365
 
371
366
  function showToast(msg) {
372
- const t = document.getElementById('toast');
367
+ var t = document.getElementById('toast');
373
368
  t.textContent = msg;
374
369
  t.classList.add('show');
375
- setTimeout(() => t.classList.remove('show'), 2500);
370
+ setTimeout(function() { t.classList.remove('show'); }, 2500);
376
371
  }
377
372
 
378
373
  buildSidebar();
package/monkey.js CHANGED
@@ -85,20 +85,27 @@ function fallbackFields(path) {
85
85
 
86
86
  // ─── Extract router prefix safely from Express layer ─────────────────────────
87
87
  function extractRouterPrefix(layer) {
88
+ // Prefer explicit path if available
89
+ if (layer.path && typeof layer.path === 'string') {
90
+ return layer.path === '/' ? '' : layer.path;
91
+ }
92
+
88
93
  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;
94
+
95
+ // Convert the regexp back to a path prefix by looking at the regexp source
96
+ // Express generates regexps like: /^\/api\/v1\/?(?=\/|$)/i
97
+ const src = layer.regexp.source;
98
+
99
+ // Extract the literal path segment before any optional/lookahead parts
100
+ // Match from start: ^\/ then literal segments
101
+ const match = src.match(/^\^((?:\\\/[^\\(?[*+{}|$^]+)+)/);
102
+ if (!match) return '';
103
+
104
+ // Unescape the extracted path
105
+ const raw = match[1].replace(/\\\//g, '/');
106
+
107
+ // Remove trailing slash if present
108
+ return raw.replace(/\/$/, '') || '';
102
109
  }
103
110
 
104
111
  // ─── Walk the Express router stack recursively ────────────────────────────────
@@ -114,6 +121,7 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
114
121
 
115
122
  const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
116
123
 
124
+ // Skip the tester route itself
117
125
  if (fullPath.startsWith('/api/tester')) continue;
118
126
 
119
127
  const methods = Object.keys(layer.route.methods || {});
@@ -122,7 +130,7 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
122
130
  const httpMethod = method.toUpperCase();
123
131
  const key = `${httpMethod}::${fullPath}`;
124
132
 
125
- // ── Path params (:id, :slug …) safely parsed using matchAll ─────────
133
+ // ── Path params (:id, :slug …) ────────────────────────────────────
126
134
  const pathParams = [];
127
135
  const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
128
136
  const matches = [...fullPath.matchAll(paramRe)];
@@ -135,13 +143,14 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
135
143
  });
136
144
  }
137
145
 
138
- // ── Body fields ────────────────────────────────────────────────────
146
+ // ── Body fields ──────────────────────────────────────────────────
139
147
  let bodyFields = [];
140
148
  if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
141
149
  const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
142
150
  for (const handler of handlers) {
143
151
  bodyFields.push(...extractBodyFields(handler));
144
152
  }
153
+ // Deduplicate
145
154
  const seen = new Map();
146
155
  bodyFields = bodyFields.filter(f => {
147
156
  if (seen.has(f.name)) return false;
@@ -165,7 +174,7 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
165
174
  }
166
175
 
167
176
  // ── Nested router (app.use('/prefix', router)) ───────────────────────────
168
- else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
177
+ else if (layer.handle && typeof layer.handle === 'function' && layer.handle.stack) {
169
178
  const routerPrefix = extractRouterPrefix(layer);
170
179
  parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
171
180
  }
@@ -173,19 +182,19 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
173
182
  }
174
183
 
175
184
  // ─── Middleware ───────────────────────────────────────────────────────────────
176
- // ─── Middleware in monkey.js ──────────────────────────────────────────────────
177
185
  function endtesterExpress() {
178
186
  return function monkeyTesterMiddleware(req, res, next) {
179
- // Standard structural path cleaning
180
- const normalPath = req.path.replace(/\/$/, ''); // removes trailing slash
181
-
182
- if (normalPath !== '/api/tester') {
187
+ // Normalize path: strip trailing slash, handle both req.path and req.url
188
+ const rawPath = (req.path || req.url || '').split('?')[0].replace(/\/+$/, '');
189
+
190
+ if (rawPath !== '/api/tester') {
183
191
  return next();
184
192
  }
185
193
 
186
194
  const app = req.app;
187
-
188
- // CRITICAL FIX: Reset the detected endpoints on each call to prevent stale empty states
195
+
196
+ // Wait a tick to ensure all routes are registered before scanning
197
+ // (handles edge cases where middleware is mounted before some routes)
189
198
  const detectedEndpoints = {};
190
199
 
191
200
  const rootStack =
@@ -193,12 +202,10 @@ function endtesterExpress() {
193
202
  (app.router && app.router.stack) || // Express 5
194
203
  [];
195
204
 
196
- // Scan the live compiled routing stack
197
205
  parseStack(rootStack, detectedEndpoints);
198
206
 
199
- // Render page template
200
207
  const html = getHtmlTemplate(detectedEndpoints);
201
- res.setHeader('Content-Type', 'text/html');
208
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
202
209
  return res.send(html);
203
210
  };
204
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",