freshroute-server
Version:
Local development server for FreshRoute extension with API mocking and extended functionality
1,178 lines • 59.5 kB
JavaScript
"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 =