UNPKG

freshroute-server

Version:

Local development server for FreshRoute extension with API mocking and extended functionality

1,178 lines 59.5 kB
"use strict"; class ApiMockManager { constructor() { this.mockClient = null; this.mockRules = []; this.availableFiles = []; this.currentEditingRule = null; this.isInitialized = false; this.fileManagementListenersSetup = false; this.pendingFileOperations = /* @__PURE__ */ new Set(); this.isReconnecting = false; this.serverUrls = { httpUrl: "http://localhost:3001", wsUrl: "ws://localhost:3002" }; this.lastNotification = null; this.notificationTimeout = null; this.serverDownNotificationShown = false; this.lastNotificationTime = 0; this.init(); } async init() { if (this.isInitialized) return; try { await this.loadServerUrls(); this.mockClient = new MockServerClient(this.serverUrls.wsUrl); this.setupEventListeners(); this.setupMockClientHandlers(); await this.loadAvailableFiles(); this.isInitialized = true; console.log("✅ API Mock Manager initialized with URLs:", this.serverUrls); } catch (error) { console.error("❌ Failed to initialize API Mock Manager:", error); } } setupEventListeners() { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n; (_a = document.getElementById("closeApiMockModal")) == null ? void 0 : _a.addEventListener("click", () => { this.closeModal(); }); (_b = document.getElementById("cancelMockRuleBtn")) == null ? void 0 : _b.addEventListener("click", () => { this.closeModal(); }); (_c = document.getElementById("saveMockRuleBtn")) == null ? void 0 : _c.addEventListener("click", () => { this.saveMockRule(); }); (_d = document.getElementById("mockResponseType")) == null ? void 0 : _d.addEventListener("change", (e) => { this.handleResponseTypeChange(e.target.value); }); (_e = document.getElementById("mockMatchingType")) == null ? void 0 : _e.addEventListener("change", () => { this.updateMockMatchingTypeUI(); }); (_f = document.getElementById("addMockHeaderBtn")) == null ? void 0 : _f.addEventListener("click", () => { this.addHeaderField(); }); (_g = document.getElementById("apiMockModal")) == null ? void 0 : _g.addEventListener("click", (e) => { if (e.target.id === "apiMockModal") { this.closeModal(); } }); (_h = document.getElementById("toggleMockUrlTest")) == null ? void 0 : _h.addEventListener("click", () => { this.toggleUrlTestSection(); }); (_i = document.getElementById("testMockUrlBtn")) == null ? void 0 : _i.addEventListener("click", () => { this.testUrlPattern(); }); (_j = document.getElementById("mockUrlPattern")) == null ? void 0 : _j.addEventListener("input", () => { this.clearUrlTestResult(); }); (_k = document.getElementById("formatJsonBtn")) == null ? void 0 : _k.addEventListener("click", () => { this.formatJsonResponse(); }); (_l = document.getElementById("validateJsonBtn")) == null ? void 0 : _l.addEventListener("click", () => { this.validateJsonResponse(); }); (_m = document.getElementById("clearResponseBodyBtn")) == null ? void 0 : _m.addEventListener("click", () => { this.clearResponseBody(); }); (_n = document.getElementById("mockResponseBody")) == null ? void 0 : _n.addEventListener("input", () => { this.updateCharacterCount(); this.validateJsonInRealTime(); }); this.setupResponseBodyExamples(); } setupMockClientHandlers() { if (!this.mockClient) return; this.mockClient.on("connected", async () => { await this.updateConnectionStatus(true); console.log("🔌 FreshRoute server WebSocket connected to:", this.serverUrls.wsUrl); if (this.isReconnecting) { this.showNotification(`WebSocket connected to ${this.serverUrls.wsUrl}`, "success"); console.log("✅ Reconnection successful, error notifications re-enabled"); } }); this.mockClient.on("disconnected", async () => { await this.updateConnectionStatus(false); console.log("🔌 FreshRoute server disconnected"); }); this.mockClient.on("mockRulesUpdated", (rules) => { this.mockRules = rules; this.renderMockRules(); console.log(`📋 Mock rules updated: ${rules.length} rules`); }); this.mockClient.on("error", async (error) => { var _a; const errorMessage = (error == null ? void 0 : error.message) || "FreshRoute server connection error"; console.error("❌ FreshRoute server error:", errorMessage); if (this.isReconnecting) { console.log("🔄 Ignoring error during reconnection process"); return; } const hasMockRules = await this.checkForMockRulesInGroups(); const isMockModalOpen = ((_a = document.getElementById("apiMockModal")) == null ? void 0 : _a.style.display) === "block"; if (hasMockRules || isMockModalOpen) { this.showNotification(errorMessage, "error"); } }); this.mockClient.on("maxReconnectAttemptsReached", async () => { var _a; if (this.isReconnecting) { console.log("🔄 Ignoring max reconnect attempts during reconnection process"); return; } const hasMockRules = await this.checkForMockRulesInGroups(); const isMockModalOpen = ((_a = document.getElementById("apiMockModal")) == null ? void 0 : _a.style.display) === "block"; if (hasMockRules || isMockModalOpen) { this.showNotification("FreshRoute server unavailable. Please start the server.", "warning"); } }); } async loadAvailableFiles(showSuccessNotification = false) { try { const response = await fetch(`${this.serverUrls.httpUrl}/api/files`); if (response.ok) { this.availableFiles = await response.json(); this.updateFileDropdown(); if (showSuccessNotification) { this.showNotification(`Files refreshed successfully. Found ${this.availableFiles.length} files.`, "success"); } console.log(`📁 Loaded ${this.availableFiles.length} files from FreshRoute server`); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.warn("⚠️ Could not load available files:", error); if (showSuccessNotification) { this.showNotification("Failed to refresh files. Make sure the FreshRoute server is running.", "error"); } } } updateFileDropdown() { const select = document.getElementById("mockResponseFile"); if (!select) return; select.innerHTML = '<option value="">Select a file...</option>'; this.availableFiles.forEach((file) => { const option = document.createElement("option"); option.value = file.name; option.textContent = `${file.name} (${this.formatFileSize(file.size)})`; select.appendChild(option); }); } formatFileSize(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } async openModal(rule = null) { var _a; const modal = document.getElementById("apiMockModal"); if (!modal) return; this.currentEditingRule = rule; this.checkServerStatusForModal(); this.resetForm(); if (rule) { this.populateForm(rule); } modal.style.display = "block"; if (this.mockClient) { await this.updateStatusVisibility(this.mockClient.isConnected); } (_a = document.getElementById("mockRuleName")) == null ? void 0 : _a.focus(); } checkServerStatusForModal() { if (this.mockClient && !this.mockClient.isConnected) { this.showNotification("FreshRoute server is not running. Start the server to test your mock rules.", "warning"); } } async closeModal() { const modal = document.getElementById("apiMockModal"); if (modal) { modal.style.display = "none"; } this.currentEditingRule = null; if (this.mockClient) { await this.updateStatusVisibility(this.mockClient.isConnected); } } resetForm() { document.getElementById("mockMethod").value = "GET"; document.getElementById("mockMatchingType").value = "regex"; document.getElementById("mockUrlPattern").value = ""; document.getElementById("mockResponseType").value = "json"; document.getElementById("mockResponseBody").value = ""; document.getElementById("mockResponseFile").value = ""; document.getElementById("mockStatusCode").value = "200"; document.getElementById("mockDelay").value = "0"; document.getElementById("mockDescription").value = ""; const headersList = document.getElementById("mockHeadersList"); if (headersList) { headersList.innerHTML = ""; } document.getElementById("mockUrlTest").value = ""; document.getElementById("mockUrlResult").innerHTML = ""; const testSection = document.getElementById("mockUrlTestSection"); if (testSection) { testSection.style.display = "none"; const toggleBtn = document.getElementById("toggleMockUrlTest"); if (toggleBtn) { toggleBtn.querySelector(".toggle-text").textContent = "Show URL Test"; toggleBtn.querySelector(".toggle-icon").textContent = "▼"; } } this.updateCharacterCount(); this.showResponseBodyStatus("", ""); this.handleResponseTypeChange("json"); } populateForm(rule) { document.getElementById("mockMethod").value = rule.method || "GET"; document.getElementById("mockMatchingType").value = rule.matchingType || "regex"; document.getElementById("mockUrlPattern").value = rule.urlPattern || ""; document.getElementById("mockResponseType").value = rule.responseType || "json"; document.getElementById("mockStatusCode").value = rule.statusCode || 200; document.getElementById("mockDelay").value = rule.delay || 0; document.getElementById("mockDescription").value = rule.description || ""; if (rule.responseType === "file") { document.getElementById("mockResponseFile").value = rule.responseFile || ""; } else { const responseBody = typeof rule.responseBody === "object" ? JSON.stringify(rule.responseBody, null, 2) : rule.responseBody || ""; document.getElementById("mockResponseBody").value = responseBody; } if (rule.responseHeaders) { Object.entries(rule.responseHeaders).forEach(([key, value]) => { this.addHeaderField(key, value); }); } this.handleResponseTypeChange(rule.responseType || "json"); this.updateCharacterCount(); this.validateJsonInRealTime(); } handleResponseTypeChange(responseType) { const bodyGroup = document.getElementById("mockResponseBodyGroup"); const fileGroup = document.getElementById("mockResponseFileGroup"); const bodyTextarea = document.getElementById("mockResponseBody"); const bodyLabel = bodyGroup == null ? void 0 : bodyGroup.querySelector(".form-label"); const bodyHelp = bodyGroup == null ? void 0 : bodyGroup.querySelector(".small-text"); if (responseType === "file") { bodyGroup.style.display = "none"; fileGroup.style.display = "block"; } else { bodyGroup.style.display = "block"; fileGroup.style.display = "none"; if (bodyLabel && bodyTextarea && bodyHelp) { if (responseType === "json") { bodyLabel.textContent = "Response Body (JSON)"; bodyTextarea.placeholder = '{"message": "Hello World", "data": {"users": [{"id": 1, "name": "John Doe"}]}}'; bodyHelp.textContent = "JSON object to return as response. Leave empty for no response body. Use the format button to beautify JSON."; const formatBtn = document.getElementById("formatJsonBtn"); const validateBtn = document.getElementById("validateJsonBtn"); if (formatBtn) formatBtn.style.display = "inline-block"; if (validateBtn) validateBtn.style.display = "inline-block"; } else if (responseType === "text") { bodyLabel.textContent = "Response Body (Text)"; bodyTextarea.placeholder = "Plain text response content\nMultiple lines are supported\n\nExample:\nHello World!\nThis is a text response."; bodyHelp.textContent = "Plain text content to return as response. Leave empty for no response body. Supports multi-line text."; const formatBtn = document.getElementById("formatJsonBtn"); const validateBtn = document.getElementById("validateJsonBtn"); if (formatBtn) formatBtn.style.display = "none"; if (validateBtn) validateBtn.style.display = "none"; } } } this.updateCharacterCount(); } addHeaderField(key = "", value = "") { const headersList = document.getElementById("mockHeadersList"); if (!headersList) return; const headerDiv = document.createElement("div"); headerDiv.className = "header-field"; headerDiv.innerHTML = ` <div class="header-row"> <input type="text" class="form-input header-key" placeholder="Header name" value="${key}"> <input type="text" class="form-input header-value" placeholder="Header value" value="${value}"> <button type="button" class="btn-remove-header">×</button> </div> `; headerDiv.querySelector(".btn-remove-header").addEventListener("click", () => { headerDiv.remove(); }); headersList.appendChild(headerDiv); } toggleUrlTestSection() { const testSection = document.getElementById("mockUrlTestSection"); const toggleBtn = document.getElementById("toggleMockUrlTest"); if (!testSection || !toggleBtn) return; const isVisible = testSection.style.display !== "none"; if (isVisible) { testSection.style.display = "none"; toggleBtn.querySelector(".toggle-text").textContent = "Show URL Test"; toggleBtn.querySelector(".toggle-icon").textContent = "▼"; } else { testSection.style.display = "block"; toggleBtn.querySelector(".toggle-text").textContent = "Hide URL Test"; toggleBtn.querySelector(".toggle-icon").textContent = "▲"; } } updateMockMatchingTypeUI() { var _a; const matchingType = document.getElementById("mockMatchingType").value; const urlPatternLabel = document.querySelector('label[for="mockUrlPattern"]') || document.querySelector(".form-label"); const urlPatternInput = document.getElementById("mockUrlPattern"); const urlPatternHelp = (_a = urlPatternInput == null ? void 0 : urlPatternInput.parentElement) == null ? void 0 : _a.querySelector(".small-text"); this.clearUrlTestResult(); if (!urlPatternLabel || !urlPatternInput || !urlPatternHelp) return; switch (matchingType) { case "contains": urlPatternLabel.textContent = "URL Contains"; urlPatternInput.placeholder = "e.g., api.example.com"; urlPatternHelp.innerHTML = "Enter text that should be found anywhere in the URL. Use {{VARIABLE_NAME}} for environment variables."; break; case "equals": urlPatternLabel.textContent = "URL Equals"; urlPatternInput.placeholder = "e.g., https://api.example.com/v1/users"; urlPatternHelp.innerHTML = "Enter the exact URL that should be matched. Use {{VARIABLE_NAME}} for environment variables."; break; case "startsWith": urlPatternLabel.textContent = "URL Starts With"; urlPatternInput.placeholder = "e.g., https://api.example.com/"; urlPatternHelp.innerHTML = "Enter the beginning part of URLs that should be matched. Use {{VARIABLE_NAME}} for environment variables."; break; case "endsWith": urlPatternLabel.textContent = "URL Ends With"; urlPatternInput.placeholder = "e.g., /api/v1/data.json"; urlPatternHelp.innerHTML = "Enter the ending part of URLs that should be matched. Use {{VARIABLE_NAME}} for environment variables."; break; case "wildcard": urlPatternLabel.textContent = "Wildcard Pattern"; urlPatternInput.placeholder = "e.g., https://api.*.com/* or https://*/api/users"; urlPatternHelp.innerHTML = "Use asterisks (*) as wildcards to match any text. Use {{VARIABLE_NAME}} for environment variables."; break; case "regex": default: urlPatternLabel.textContent = "URL Pattern (Regex)"; urlPatternInput.placeholder = "e.g., https://api\\.example\\.com/v\\d+/.*"; urlPatternHelp.innerHTML = "Use regular expression patterns. Use {{VARIABLE_NAME}} for environment variables."; break; } } testUrlPattern() { const pattern = document.getElementById("mockUrlPattern").value.trim(); const testUrl = document.getElementById("mockUrlTest").value.trim(); const matchingType = document.getElementById("mockMatchingType").value; const resultElement = document.getElementById("mockUrlResult"); if (!pattern || !testUrl) { resultElement.innerHTML = '<div class="test-result error">⚠️ Please enter both URL pattern and test URL</div>'; return; } try { let isMatch = false; let matchDetails = ""; switch (matchingType) { case "contains": isMatch = testUrl.includes(pattern); matchDetails = isMatch ? `<strong>✅ Pattern found in URL!</strong><br><div class="match-info">Pattern "${this.escapeHtml(pattern)}" is contained in the test URL</div>` : `<strong>❌ Pattern not found</strong><br><div class="match-info">Pattern "${this.escapeHtml(pattern)}" is not contained in the test URL</div>`; break; case "equals": isMatch = testUrl === pattern; matchDetails = isMatch ? `<strong>✅ URL matches exactly!</strong><br><div class="match-info">The test URL matches the pattern exactly</div>` : `<strong>❌ URL does not match exactly</strong><br><div class="match-info">The test URL does not match the pattern exactly</div>`; break; case "startsWith": isMatch = testUrl.startsWith(pattern); matchDetails = isMatch ? `<strong>✅ URL starts with pattern!</strong><br><div class="match-info">The test URL starts with "${this.escapeHtml(pattern)}"</div>` : `<strong>❌ URL does not start with pattern</strong><br><div class="match-info">The test URL does not start with "${this.escapeHtml(pattern)}"</div>`; break; case "endsWith": isMatch = testUrl.endsWith(pattern); matchDetails = isMatch ? `<strong>✅ URL ends with pattern!</strong><br><div class="match-info">The test URL ends with "${this.escapeHtml(pattern)}"</div>` : `<strong>❌ URL does not end with pattern</strong><br><div class="match-info">The test URL does not end with "${this.escapeHtml(pattern)}"</div>`; break; case "wildcard": try { const wildcardRegex = this.convertWildcardToRegex(pattern); const regex = new RegExp(wildcardRegex); const match = testUrl.match(regex); if (match) { isMatch = true; matchDetails = ` <strong>✅ Wildcard pattern matches!</strong><br> <div class="match-info"> <strong>Full match:</strong> ${this.escapeHtml(match[0])}<br> ${match.length > 1 ? `<strong>Captured parts:</strong><br>${match.slice(1).map((group, i) => `*${i + 1}: ${this.escapeHtml(group || "")}`).join("<br>")}` : ""} </div> `; } else { matchDetails = `<strong>❌ Wildcard pattern does not match</strong><br><div class="match-info">Pattern "${this.escapeHtml(pattern)}" does not match the test URL</div>`; } } catch (error) { matchDetails = `<strong>🚫 Invalid wildcard pattern</strong><br><div class="error-info">${this.escapeHtml(error.message)}</div>`; } break; case "regex": default: try { const regex = new RegExp(pattern, "i"); const match = testUrl.match(regex); if (match) { isMatch = true; matchDetails = ` <strong>✅ Regex pattern matches!</strong><br> <div class="match-info"> <strong>Full match:</strong> ${this.escapeHtml(match[0])}<br> ${match.length > 1 ? `<strong>Capture groups:</strong><br>${match.slice(1).map((group, i) => `$${i + 1}: ${this.escapeHtml(group || "")}`).join("<br>")}` : ""} </div> `; } else { matchDetails = `<strong>❌ Regex pattern does not match</strong><br><div class="match-info">Pattern "${this.escapeHtml(pattern)}" does not match the test URL</div>`; } } catch (regexError) { matchDetails = `<strong>🚫 Invalid regex pattern</strong><br><div class="error-info">${this.escapeHtml(regexError.message)}</div>`; } break; } resultElement.innerHTML = ` <div class="test-result ${isMatch ? "success" : "no-match"}"> <div class="match-details">${matchDetails}</div> <div class="test-info"> <strong>Matching Type:</strong> ${this.getMatchingTypeDisplayName(matchingType)}<br> <strong>Pattern:</strong> ${this.escapeHtml(pattern)}<br> <strong>Test URL:</strong> ${this.escapeHtml(testUrl)} </div> </div> `; } catch (error) { resultElement.innerHTML = ` <div class="test-result error"> <strong>🚫 Error testing pattern</strong><br> <div class="error-info">${this.escapeHtml(error.message)}</div> </div> `; } } clearUrlTestResult() { const resultElement = document.getElementById("mockUrlResult"); if (resultElement) { resultElement.innerHTML = ""; } } escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } convertWildcardToRegex(wildcardPattern) { let regexPattern = wildcardPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); regexPattern = regexPattern.replace(/\*/g, "(.*?)"); if (!regexPattern.startsWith("^")) { regexPattern = "^" + regexPattern; } if (!regexPattern.endsWith("$")) { regexPattern = regexPattern + "$"; } return regexPattern; } getMatchingTypeDisplayName(matchingType) { switch (matchingType) { case "contains": return "Contains"; case "equals": return "Equals"; case "startsWith": return "Starts With"; case "endsWith": return "Ends With"; case "wildcard": return "Wildcard"; case "regex": return "Regex"; default: return "Unknown"; } } generateMockRuleName(method, urlPattern, statusCode) { try { let urlPart = urlPattern; const urlMatch = urlPattern.match(/https?:\/\/([^\/]+)(\/.*)?/); if (urlMatch) { const domain = urlMatch[1]; const path = urlMatch[2] || ""; const domainParts = domain.split("."); const simpleDomain = domainParts.length > 2 ? domainParts.slice(-2).join(".") : domain; const pathParts = path.split("/").filter((p) => p && !p.includes("*") && !p.includes(".*")); const simplePath = pathParts.length > 0 ? `/${pathParts[0]}` : ""; urlPart = simpleDomain + simplePath; } else { urlPart = urlPattern.replace(/[.*+?^${}()|[\]\\]/g, "").substring(0, 30); } const methodPart = method === "ANY" ? "API" : method; const statusPart = statusCode !== 200 ? ` (${statusCode})` : ""; return `${methodPart} ${urlPart}${statusPart}`; } catch (error) { return `${method} Mock Rule`; } } collectFormData() { const formData = { method: document.getElementById("mockMethod").value, matchingType: document.getElementById("mockMatchingType").value, urlPattern: document.getElementById("mockUrlPattern").value.trim(), responseType: document.getElementById("mockResponseType").value, statusCode: parseInt(document.getElementById("mockStatusCode").value) || 200, delay: parseInt(document.getElementById("mockDelay").value) || 0, description: document.getElementById("mockDescription").value.trim() }; if (formData.responseType === "file") { formData.responseFile = document.getElementById("mockResponseFile").value; } else { const responseBodyText = document.getElementById("mockResponseBody").value.trim(); if (formData.responseType === "json" && responseBodyText) { try { formData.responseBody = JSON.parse(responseBodyText); } catch (error) { throw new Error("Invalid JSON in response body"); } } else { formData.responseBody = responseBodyText; } } const headers = {}; const headerFields = document.querySelectorAll("#mockHeadersList .header-field"); headerFields.forEach((field) => { const key = field.querySelector(".header-key").value.trim(); const value = field.querySelector(".header-value").value.trim(); if (key && value) { headers[key] = value; } }); if (Object.keys(headers).length > 0) { formData.responseHeaders = headers; } return formData; } validateFormData(data) { const errors = []; if (!data.urlPattern) { errors.push("URL pattern is required"); } if (data.responseType === "file" && !data.responseFile) { errors.push("Response file is required when using file response type"); } if (data.statusCode < 100 || data.statusCode > 599) { errors.push("Status code must be between 100 and 599"); } if (data.delay < 0 || data.delay > 6e4) { errors.push("Delay must be between 0 and 60000 milliseconds"); } return errors; } async saveMockRule() { try { const formData = this.collectFormData(); const errors = this.validateFormData(formData); if (errors.length > 0) { this.showNotification(errors.join("\n"), "error"); return; } if (window.editingGroupIndex !== null && window.editingGroupIndex !== void 0) { await this.saveMockRuleToGroup(formData); } else { if (this.currentEditingRule) { this.mockClient.updateMockRule(this.currentEditingRule.id, formData); this.showNotification("Mock rule updated successfully", "success"); } else { this.mockClient.addMockRule(formData); this.showNotification("Mock rule created successfully", "success"); } } this.closeModal(); } catch (error) { console.error("❌ Error saving mock rule:", error); this.showNotification(error.message || "Failed to save mock rule", "error"); } } async saveMockRuleToGroup(formData) { var _a, _b; console.log("🔍 Starting saveMockRuleToGroup with formData:", formData); console.log("🔍 Current editing state:", { groupIndex: window.editingGroupIndex, ruleIndex: window.editingRuleIndex, ruleType: window.editingRuleType }); const ruleName = this.generateMockRuleName(formData.method, formData.urlPattern, formData.statusCode); console.log("🔍 Generated rule name:", ruleName); const rule = { id: ((_a = this.currentEditingRule) == null ? void 0 : _a.id) || Date.now().toString(), type: "mock_response", name: ruleName, enabled: ((_b = this.currentEditingRule) == null ? void 0 : _b.enabled) !== void 0 ? this.currentEditingRule.enabled : true, method: formData.method, matchingType: formData.matchingType, urlPattern: formData.urlPattern, responseType: formData.responseType, responseBody: formData.responseBody, responseFile: formData.responseFile, statusCode: formData.statusCode, delay: formData.delay, description: formData.description, responseHeaders: formData.responseHeaders }; console.log("🔍 Created rule object:", rule); const groupIndex = window.editingGroupIndex; const group = window.groups[groupIndex]; console.log("🔍 Target group (index", groupIndex, "):", group); console.log("🔍 Group rules before adding:", group.rules.length, group.rules); if (window.editingRuleIndex !== null && window.editingRuleIndex !== void 0) { console.log("🔍 Updating existing rule at index:", window.editingRuleIndex); group.rules[window.editingRuleIndex] = rule; this.showNotification("Mock rule updated successfully", "success"); } else { console.log("🔍 Adding new rule to group"); group.rules.push(rule); console.log("🔍 Group rules after adding:", group.rules.length, group.rules); this.showNotification(`Mock rule created successfully. Group now has ${group.rules.length} rules. Rule type: ${rule.type}`, "success"); } console.log("🔍 All groups before saving:", window.groups); console.log("🔍 Calling saveGroups..."); await window.saveGroups(); console.log("🔍 saveGroups completed, calling renderGroups..."); window.renderGroups(); console.log("🔍 renderGroups completed"); window.editingGroupIndex = null; window.editingRuleIndex = null; window.editingRuleType = null; console.log("🔍 Editing state reset"); } deleteMockRule(ruleId) { if (confirm("Are you sure you want to delete this mock rule?")) { this.mockClient.deleteMockRule(ruleId); this.showNotification("Mock rule deleted successfully", "success"); } } toggleMockRule(ruleId, enabled) { const rule = this.mockRules.find((r) => r.id === ruleId); if (rule) { this.mockClient.updateMockRule(ruleId, { enabled }); const status = enabled ? "enabled" : "disabled"; this.showNotification(`Mock rule ${status}`, "success"); } } renderMockRules() { if (window.renderApiMockRules) { window.renderApiMockRules(this.mockRules); } } async updateStatusVisibility(isConnected) { var _a; const statusElement = document.getElementById("mockServerStatus"); if (!statusElement) return; const hasMockRules = await this.checkForMockRulesInGroups(); const isMockModalOpen = ((_a = document.getElementById("apiMockModal")) == null ? void 0 : _a.style.display) === "block"; if (!isConnected && (hasMockRules || isMockModalOpen)) { statusElement.style.display = "inline-block"; if (hasMockRules && !isMockModalOpen) { this.showNotification("FreshRoute server is not running, but you have mock rules configured. Start the server to use them.", "warning"); } } else if (isConnected) { statusElement.style.display = "inline-block"; } else { statusElement.style.display = "none"; } } async checkForMockRulesInGroups() { try { console.log("🔍 Checking for mock rules in groups..."); if (typeof window !== "undefined" && window.groups && window.groups.length > 0) { console.log("📋 Using window.groups:", window.groups.length, "groups"); const hasMockRules = window.groups.some( (group) => group.rules && group.rules.some((rule) => rule.type === "mock_response") ); console.log("🔍 Found mock rules in window.groups:", hasMockRules); return hasMockRules; } console.log("📋 Loading groups from storage..."); const { groups } = await chrome.storage.local.get(["groups"]); if (groups && groups.length > 0) { console.log("📋 Loaded from storage:", groups.length, "groups"); const hasMockRules = groups.some( (group) => group.rules && group.rules.some((rule) => rule.type === "mock_response") ); console.log("🔍 Found mock rules in storage groups:", hasMockRules); return hasMockRules; } console.log("📋 No groups found"); return false; } catch (error) { console.error("❌ Error checking for mock rules:", error); return false; } } showNotification(message, type = "info") { const now = Date.now(); const notificationKey = `${type}:${message}`; if (this.lastNotification === notificationKey && now - this.lastNotificationTime < 3e4) { console.log("🔇 Suppressing duplicate notification:", message); return; } if (message.includes("FreshRoute server is not running") || message.includes("FreshRoute server unavailable") || message.includes("Mock server is not running") || message.includes("Mock server unavailable")) { if (this.serverDownNotificationShown) { console.log("🔇 Server down notification already shown, suppressing duplicate"); return; } this.serverDownNotificationShown = true; if (this.notificationTimeout) { clearTimeout(this.notificationTimeout); } this.notificationTimeout = setTimeout(() => { this.serverDownNotificationShown = false; console.log("🔔 Server down notification flag cleared"); }, 6e4); } if (type === "success" && (message.includes("connected") || message.includes("accessible"))) { this.serverDownNotificationShown = false; if (this.notificationTimeout) { clearTimeout(this.notificationTimeout); this.notificationTimeout = null; } } this.lastNotification = notificationKey; this.lastNotificationTime = now; if (window.showNotification) { window.showNotification(message, type); } else { console.log(`${type.toUpperCase()}: ${message}`); } } // Public methods for external use getConnectionStatus() { return this.mockClient ? this.mockClient.getConnectionStatus() : { isConnected: false }; } getMockRules() { return this.mockRules; } refreshFiles() { return this.loadAvailableFiles(true); } // Debug method to reset file management listeners (if needed) resetFileManagementListeners() { this.fileManagementListenersSetup = false; console.log("🔄 File management listeners reset"); } // Debug method to clear pending file operations (if needed) clearPendingFileOperations() { this.pendingFileOperations.clear(); console.log("🔄 Pending file operations cleared"); } // Debug method to reset file action listeners (if needed) resetFileActionListeners() { const fileList = document.getElementById("fileList"); if (fileList && fileList._fileActionListener) { fileList.removeEventListener("click", fileList._fileActionListener); fileList._fileActionListenerSetup = false; fileList._fileActionListener = null; console.log("🔄 File action listeners reset"); } } // Load server URLs from settings async loadServerUrls() { try { if (typeof getMockServerUrls === "function") { this.serverUrls = await getMockServerUrls(); } else { const { mockServerHttpUrl, mockServerWsUrl } = await chrome.storage.sync.get([ "mockServerHttpUrl", "mockServerWsUrl" ]); this.serverUrls = { httpUrl: mockServerHttpUrl || "http://localhost:3001", wsUrl: mockServerWsUrl || "ws://localhost:3002" }; } console.log("📡 Loaded server URLs from settings:", this.serverUrls); } catch (error) { console.error("❌ Error loading server URLs:", error); } } // Update server URLs and reinitialize connections async updateServerUrls(httpUrl, wsUrl) { try { this.serverUrls = { httpUrl, wsUrl }; this.isReconnecting = true; console.log("🔄 Starting server URL update, suppressing error notifications..."); if (this.mockClient) { this.mockClient.setIntentionalDisconnect(true); this.mockClient.disconnect(); await new Promise((resolve) => setTimeout(resolve, 200)); this.mockClient = new MockServerClient(wsUrl); this.setupMockClientHandlers(); } console.log("⏳ Waiting for WebSocket connection attempt..."); await new Promise((resolve) => setTimeout(resolve, 500)); await this.checkHttpServerStatus(); if (this.mockClient) { await this.updateConnectionStatus(this.mockClient.isConnected); } setTimeout(async () => { this.isReconnecting = false; console.log("✅ Server URL update complete, error notifications re-enabled"); await this.performFinalStatusCheck(); }, 3e3); console.log("🔄 Updated server URLs:", this.serverUrls); } catch (error) { console.error("❌ Error updating server URLs:", error); this.isReconnecting = false; } } // Check HTTP server status async checkHttpServerStatus() { try { console.log("🔍 Checking HTTP server status at:", this.serverUrls.httpUrl); const response = await fetch(`${this.serverUrls.httpUrl}/api/files`, { method: "GET", timeout: 5e3 // 5 second timeout }); if (response.ok) { await this.loadAvailableFiles(false); const statusElement = document.getElementById("mockServerStatus"); if (statusElement) { statusElement.className = "connection-status connected"; statusElement.textContent = "HTTP Server Connected"; statusElement.style.display = "inline-block"; } console.log("✅ HTTP server is accessible at:", this.serverUrls.httpUrl); this.serverDownNotificationShown = false; if (this.notificationTimeout) { clearTimeout(this.notificationTimeout); this.notificationTimeout = null; } const isUrlChangeReconnection = this.isReconnecting && this.serverUrls.httpUrl !== "http://localhost:3001"; if (!this.isReconnecting || isUrlChangeReconnection) { this.showNotification(`Successfully connected to FreshRoute server at ${this.serverUrls.httpUrl}`, "success"); } } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.log("⚠️ HTTP server not accessible:", error.message); const statusElement = document.getElementById("mockServerStatus"); if (statusElement) { statusElement.className = "connection-status disconnected"; statusElement.textContent = "HTTP Server Disconnected"; statusElement.style.display = "inline-block"; } if (this.isReconnecting) { console.log("🔄 Ignoring HTTP server check error during reconnection process"); return; } console.log("🔄 Retrying HTTP server check in 1 second..."); await new Promise((resolve) => setTimeout(resolve, 1e3)); try { const retryResponse = await fetch(`${this.serverUrls.httpUrl}/api/files`); if (retryResponse.ok) { console.log("✅ HTTP server accessible on retry"); const statusElement2 = document.getElementById("mockServerStatus"); if (statusElement2) { statusElement2.className = "connection-status connected"; statusElement2.textContent = "HTTP Server Connected"; statusElement2.style.display = "inline-block"; } this.serverDownNotificationShown = false; if (this.notificationTimeout) { clearTimeout(this.notificationTimeout); this.notificationTimeout = null; } return; } } catch (retryError) { console.log("⚠️ HTTP server still not accessible after retry"); } const hasMockRules = await this.checkForMockRulesInGroups(); if (hasMockRules) { this.showNotification(`FreshRoute server is not running at ${this.serverUrls.httpUrl}, but you have mock rules configured. Start the server to use them.`, "warning"); } } } // Method to be called when rules change to update status visibility async onRulesChanged() { if (this.mockClient) { await this.updateStatusVisibility(this.mockClient.isConnected); } } // Perform a final comprehensive status check after URL changes async performFinalStatusCheck() { console.log("🔍 Performing final status check after URL update..."); try { const httpResponse = await fetch(`${this.serverUrls.httpUrl}/api/files`); const httpAccessible = httpResponse.ok; const wsConnected = this.mockClient ? this.mockClient.isConnected : false; console.log("📊 Final status check results:", { httpAccessible, wsConnected, httpUrl: this.serverUrls.httpUrl, wsUrl: this.serverUrls.wsUrl }); const statusElement = document.getElementById("mockServerStatus"); if (statusElement) { if (httpAccessible && wsConnected) { statusElement.className = "connection-status connected"; statusElement.textContent = "Fully Connected"; } else if (httpAccessible) { statusElement.className = "connection-status connected"; statusElement.textContent = "HTTP Server Connected"; } else if (wsConnected) { statusElement.className = "connection-status connected"; statusElement.textContent = "WebSocket Connected"; } else { statusElement.className = "connection-status disconnected"; statusElement.textContent = "Disconnected"; } statusElement.style.display = "inline-block"; } await this.updateConnectionStatus(wsConnected); if (httpAccessible || wsConnected) { console.log("✅ Final status check: Server is accessible"); } else { console.log("⚠️ Final status check: Server is not accessible"); const hasMockRules = await this.checkForMockRulesInGroups(); if (hasMockRules) { this.showNotification(`Unable to connect to FreshRoute server at ${this.serverUrls.httpUrl}. Please verify the server is running.`, "warning"); } } } catch (error) { console.error("❌ Error in final status check:", error); } } // Response body editor enhancement methods setupResponseBodyExamples() { document.querySelectorAll(".btn-example-small").forEach((btn) => { btn.addEventListener("click", (e) => { const example = e.target.dataset.example; this.insertResponseExample(example); }); }); } insertResponseExample(exampleType) { const responseBody = document.getElementById("mockResponseBody"); if (!responseBody) return; let exampleData = ""; switch (exampleType) { case "success": exampleData = JSON.stringify({ "success": true, "message": "Operation completed successfully", "data": { "id": 123, "timestamp": (/* @__PURE__ */ new Date()).toISOString() } }, null, 2); break; case "error": exampleData = JSON.stringify({ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Invalid request parameters", "details": ["Field 'email' is required", "Field 'name' must be at least 3 characters"] } }, null, 2); break; case "list": exampleData = JSON.stringify({ "data": [ { "id": 1, "name": "Item 1", "status": "active" }, { "id": 2, "name": "Item 2", "status": "inactive" }, { "id": 3, "name": "Item 3", "status": "active" } ], "pagination": { "page": 1, "limit": 10, "total": 25, "hasMore": true } }, null, 2); break; case "user": exampleData = JSON.stringify({ "user": { "id": 12345, "username": "john_doe", "email": "john@example.com", "profile": { "firstName": "John", "lastName": "Doe", "avatar": "https://example.com/avatar.jpg" }, "preferences": { "theme": "dark", "notifications": true } } }, null, 2); break; case "empty": exampleData = ""; break; default: return; } responseBody.value = exampleData; this.updateCharacterCount(); this.validateJsonInRealTime(); } formatJsonResponse() { const responseBody = document.getElementById("mockResponseBody"); document.getElementById("responseBodyStatus"); if (!responseBody) return; const content = responseBody.value.trim(); if (!content) { this.showResponseBodyStatus("Nothing to format", "warning"); return; } try { const parsed = JSON.parse(content); const formatted = JSON.stringify(parsed, null, 2); responseBody.value = formatted; this.updateCharacterCount(); this.showResponseBodyStatus("JSON formatted successfully", "success"); } catch (error) { this.showResponseBodyStatus(`Invalid JSON: ${error.message}`, "error"); } } validateJsonResponse() { const responseBody = document.getElementById("mockResponseBody"); if (!responseBody) return; const content = responseBody.value.trim(); if (!content) { this.showResponseBodyStatus("No content to validate", "info"); return; } try { JSON.parse(content); this.showResponseBodyStatus("✓ Valid JSON", "success"); } catch (error) { this.showResponseBodyStatus(`✗ Invalid JSON: ${error.message}`, "error"); } } validateJsonInRealTime() { var _a; const responseType = (_a = document.getElementById("mockResponseType")) == null ? void 0 : _a.value; if (responseType !== "json") return; const responseBody = document.getElementById("mockResponseBody"); if (!responseBody) return; const content = responseBody.value.trim(); if (!content) { this.showResponseBodyStatus("", ""); return; } try { JSON.parse(content); this.showResponseBodyStatus("", ""); } catch (error) { this.showResponseBodyStatus(`Invalid JSON: ${error.message}`, "error"); } } clearResponseBody() { const responseBody = document.getElementById("mockResponseBody"); if (responseBody) { responseBody.value = ""; this.updateCharacterCount(); this.showResponseBodyStatus("Content cleared", "info"); } } updateCharacterCount() { const responseBody = document.getElementById("mockResponseBody"); const charCount = document.getElementById("responseBodyCharCount"); if (responseBody && charCount) { const count = responseBody.value.length; charCount.textContent = count.toLocaleString(); if (count > 1e4) { charCount.style.color = "#ff6b6b"; charCount.title = "Large response body - consider optimizing"; } else if (count > 5e3) { charCount.style.color = "#ffa500"; charCount.title = "Medium response body size"; } else { charCount.style.color = "#666"; charCount.title = "Character count"; } } } showResponseBodyStatus(message, type) { const statusElement = document.getElementById("responseBodyStatus"); if (!statusElement) return; statusElement.textContent = message; statusElement.className = `response-body-status ${type}`; if (type === "success" || type === "info") { setTimeout(() => { statusElement.textContent = ""; statusElement.className = "response-body-status"; }, 3e3); } } // File Management Modal functionality openFileManagementModal() { const modal = document.getElementById("fileManagementModal"); if (!modal) return; modal.style.display = "block"; if (!this.fileManagementListenersSetup) { this.setupFileManagementListeners(); this.fileManagementListenersSetup = true; } this.loadAndDisplayFiles(); } closeFileManagementModal() { const modal = document.getElementById("fileManagementModal"); if (modal) { modal.style.display = "none"; } const fileInput = document.getElementById("fileInput"); if (fileInput) { fileInput.value = ""; } const uploadProgress = document.getElementById("uploadProgress"); if (uploadProgress) { uploadProgress.style.display = "none"; } this.pendingFileOperations.clear(); } setupFileManagementListeners() { var _a, _b, _c, _d; (_a = document.getElementById("closeFileManagementModal")) == null ? void 0 : _a.addEventListener("click", () => { this.closeFileManagementModal(); }); (_b = document.getElementById("closeFileManagementBtn")) == null ? void 0 : _b.addEventListener("click", () => { this.closeFileManagementModal(); }); (_c = document.getElementById("fileManagementModal")) == null ? void 0 : _c.addEventListener("click", (e) => { if (e.target.id === "fileManagementModal") { this.closeFileManagementModal(); } }); const dropZone = document.getElementById("dropZone"); const fileInput = document.getElementById("fileInput"); if (dropZone) { dropZone.addEventListener("click", () => fileInput == null ? void 0 : fileInput.click()); dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); }); dropZone.addEventListener("dragleave", () => { dropZone.classList.remove("drag-over"); }); dropZone.addEventListener("drop", (e) => { e.preventDefault(); dropZone.classList.remove("drag-over"); this.handleFileUpload(e.dataTransfer.files); }); } if (fileInput) { fileInput.addEventListener("change", (e) => { this.handleFileUpload(e.target.files); }); } (_d = document.getElementById("refreshFileListBtn")) == null ? void 0 : _d.addEventListener("click", () => { this.loadAndDisplayFiles(); }); this.setupFileActionListeners(); } async loadAndDisplayFiles() { const fileList =