@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.
- package/htmlTemplate.js +39 -44
- package/monkey.js +33 -26
- 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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
|
|
223
|
+
var ep = ENDPOINTS[key];
|
|
224
|
+
var main = document.getElementById('main-panel');
|
|
227
225
|
if (!ep) return;
|
|
228
226
|
|
|
229
|
-
|
|
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
|
-
|
|
280
|
-
|
|
274
|
+
var ep = ENDPOINTS[currentKey];
|
|
275
|
+
var path = ep.path;
|
|
281
276
|
|
|
282
277
|
if (ep.params && ep.params.length) {
|
|
283
|
-
for (
|
|
284
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
292
|
+
var body = undefined;
|
|
297
293
|
if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
var payload = {};
|
|
295
|
+
var hasData = false;
|
|
300
296
|
|
|
301
297
|
ep.fields.forEach(function(f) {
|
|
302
|
-
|
|
298
|
+
var el = document.getElementById('field-' + f.name);
|
|
303
299
|
if (!el) return;
|
|
304
|
-
|
|
305
|
-
if (v === '') return;
|
|
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
|
-
|
|
312
|
+
var t0 = Date.now();
|
|
317
313
|
|
|
318
314
|
try {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
367
|
+
var t = document.getElementById('toast');
|
|
373
368
|
t.textContent = msg;
|
|
374
369
|
t.classList.add('show');
|
|
375
|
-
setTimeout(()
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
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 …)
|
|
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.
|
|
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
|
-
//
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
if (
|
|
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
|
-
//
|
|
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
|
}
|