diff --git a/.eslintrc.json b/.eslintrc.json
index 4b4a3db..316f28f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -20,7 +20,7 @@
"env": { "node": true, "jest": true }
},
{
- "files": ["generate-screenshots.js"],
+ "files": ["generate-screenshots.js", "scripts/**/*.js"],
"env": { "node": true }
}
]
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..bf13ee3
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,27 @@
+name: Validate extension
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+permissions:
+ contents: read
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+ - run: npm ci
+ - run: ./release.sh --no-bump
+ - run: npx playwright install --with-deps chromium
+ - run: npm run test:browser
+ - id: version
+ run: echo "value=$(node -p 'require("./manifest.json").version')" >> "$GITHUB_OUTPUT"
+ - uses: actions/upload-artifact@v4
+ with:
+ name: quick-api-client
+ path: dist/quick-api-client-v${{ steps.version.outputs.value }}.zip
+ if-no-files-found: error
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4892a21..e20bb91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## 1.1.5 - 2026-09-11
+
+- Prevent duplicate sends during permission prompts and ignore stale callbacks after cancellation.
+- Share request preparation across sending, preview and generated code, including GraphQL and environment values.
+- Preserve raw request bodies and duplicate query parameters. Validate protocols, headers and import data.
+- Bound request and response bodies to 5 MiB and skip syntax highlighting above 200,000 characters.
+- Report actual response bytes and preserve raw response text when copying or downloading.
+- Distinguish cancellations from timeouts and clean up request controllers and streams.
+- Fix generated PHP, Java and C# quoting, empty Java POST bodies, and non-JSON Fetch responses.
+- Preserve GraphQL variables during restoration and prevent settings reset from deleting favorites.
+- Retain active environment selection when renaming, clear it when deleting, and report failed saves correctly.
+- Include all popup modules in release archives and synchronize version metadata.
+- Replace copied test implementations with source-based checks and add regression and browser package tests.
+
# Changelog - Quick API Client
Welcome to the **Quick API Client** release log. Below is the list of changes, optimizations, and new features introduced for the Chrome Store and Edge Add-ons.
diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md
index db3a7e9..224db4a 100644
--- a/PRIVACY_POLICY.md
+++ b/PRIVACY_POLICY.md
@@ -1,74 +1,33 @@
# Privacy Policy for Quick API Client
-**Effective date:** 2025-01-01
+Effective date 2026-09-11
-This Privacy Policy describes how the browser extension **Quick API Client** (the "Extension") provided by **Hayk Jomardyan** ("we", "us", or "our") handles information when used in supported browsers, including Google Chrome and Microsoft Edge.
+Quick API Client is provided by Hayk Jomardyan. This policy describes the extension's implemented data handling.
-By installing or using the Extension, you acknowledge that you have read and understood this Privacy Policy.
+## API requests
-## 1. Scope
+When you send a request, the extension transmits its URL, query parameters, headers and body directly to the selected API server. That server receives the connection's IP address and any credentials or personal information you include. Redirects follow browser networking rules. The extension omits browser cookies from its fetch requests.
-This Privacy Policy applies solely to the Extension as distributed through:
-- The Chrome Web Store for Google Chrome and other Chromium-based browsers.
-- The Microsoft Edge Add-ons store for Microsoft Edge.
+The extension does not send request data to a developer-operated relay, analytics service or advertising service. It does not monitor general browsing activity.
-It does not apply to any websites, services, or applications that may be linked from the Extension but are operated by third parties under their own privacy policies.
+## Browser storage
-## 2. Data collection and processing
+The extension saves the most recent request and enabled request history in local extension storage. These records may include URLs, headers, credentials and bodies entered by the user.
-- The Extension does **not** collect, store, or process any personal data or personally identifiable information (PII).
-- The Extension does **not** collect or transmit your browsing history, URLs, search queries, IP address, device identifiers, cookies, or similar identifiers.
-- The Extension does **not** track your behavior across websites, and does **not** perform any profiling or analytics.
+Settings, favorites and environment variables use browser sync storage. The browser provider may synchronize these records through the signed-in browser account, according to its settings and policies. The extension does not encrypt these values itself. The developer does not retrieve them.
-All features of the Extension are designed to work without requiring any personal or sensitive user data, in line with data minimization expectations for Chrome and Edge extensions.
+Response bodies are displayed in the request page. They are not added to saved history. You can copy or download a response explicitly.
-## 3. Local processing and storage
+## Sharing
-- All processing performed by the Extension occurs locally within your browser.
-- Any settings, preferences, or cached data used by the Extension (if any) are stored only in your browser's local storage or synchronized storage associated with your browser account.
-- We do not have access to this locally stored information and do not retrieve it to our own servers.
+Request exports include the request URL, headers, query parameters, body and GraphQL settings. Base64 encoding is not encryption. Copying an export to the clipboard and sharing it can disclose credentials or other information contained in that request.
-## 4. No use of third-party services
+## Permissions and controls
-- The Extension does **not** use third-party analytics tools, advertising networks, or tracking pixels.
-- The Extension does **not** embed third-party SDKs or scripts for the purpose of collecting user data.
-- The Extension does **not** share any information with third parties, because no information is collected in the first place.
+The extension uses the storage permission and asks for optional access to API hosts. It does not require access to browsing history or all open tabs.
-## 5. Permissions
+You can disable or clear request history, edit or delete favorites and environments, revoke host access in the browser, or uninstall the extension. Disabling history prevents new history entries but does not remove the separate last-request draft. Resetting preferences preserves favorites. Browser synchronization and API-server retention are controlled by their respective providers.
-The Extension may request certain browser permissions that are technically required for its functionality (for example, access to specific sites, tabs, or storage).
-Requesting a permission does **not** mean that personal data is collected; permissions are only used to provide the core features of the Extension and not for data harvesting or tracking, in accordance with Chrome Web Store and Microsoft Edge Add-ons policies.
+## Policy updates and contact
-## 6. Children's privacy
-
-Because the Extension does not collect, store, or process any personal information, it does not knowingly collect information from children of any age.
-If this changes in a future version, this Privacy Policy will be updated to reflect the applicable requirements for children's data protection.
-
-## 7. User rights and controls
-
-Since the Extension does not collect or store personal data on our servers, there is no personal data for us to access, modify, export, or delete on your behalf.
-You may at any time:
-- Disable the Extension from your browser's extensions/add-ons page.
-- Uninstall the Extension, which will remove it and any associated local data stored by the browser.
-
-For Chrome and Edge, you can manage or remove extensions from the respective extensions management pages provided by the browser.
-
-## 8. International data transfers
-
-Because we do not collect or store personal data, there are no international transfers of personal data related to the Extension.
-If future versions begin to collect data, this section will be updated to describe any applicable transfers and safeguards.
-
-## 9. Changes to this Privacy Policy
-
-We may update this Privacy Policy from time to time, for example to reflect changes in:
-- The functionality or behavior of the Extension.
-- Applicable laws, regulations, or store policies for Chrome or Edge extensions.
-
-When changes are made, the "Effective date" at the top of this document will be updated. Material changes will be reflected in the latest version of the policy made available via the Chrome Web Store and Microsoft Edge Add-ons listing pages.
-
-## 10. Contact
-
-If you have any questions or concerns about this Privacy Policy or the Extension, you can contact us at:
-
-**Name:** Hayk Jomardyan
-**Website:** https://github.com/jomardyan/Quick-API-Client
+This file is updated when implemented data handling changes. For questions, contact Hayk Jomardyan through the [project repository](https://github.com/jomardyan/Quick-API-Client).
diff --git a/README.md b/README.md
index 1ef575c..37d32cf 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,49 @@
# Quick API Client
-A rapid API testing and debugging tool for developers.
+A Chrome and Edge extension for testing REST and GraphQL APIs.
## Features
-- Quick API testing
-- Request/response visualization
-- Multiple HTTP methods support
+- HTTP methods, headers, repeated query parameters and raw request bodies
+- GraphQL query and variables editor
+- Named environments, favorites and local request history
+- Cancellation, configurable timeouts and response timing
+- Request sharing and code generation for eight targets
+- JSON, XML, HTML and CSS parsing checks
-## ๐จโ๐ป Author
+## Install
-**Hayk Jomardyan**
+Download the latest versioned ZIP from `dist`, extract it, and use Load unpacked in your browser's extension management page with developer mode enabled. Grant access to the API host when sending a request. Use the open-in-tab control for requests that should remain visible while switching browser tabs.
-- ๐ Website: [lolino.pl](https://lolino.pl)
-- ๐ง Email: [hayk.jomardyan@outlook.com](mailto:hayk.jomardyan@outlook.com)
-- ๐ผ GitHub: [@jomardyan](https://github.com/jomardyan)
+## Development and verification
-## License
+Requires Node.js 22, npm, Bash, jq, zip and unzip.
-MIT License - See LICENSE file for details
+```sh
+npm ci
+npm run lint
+npm test -- --runInBand
+./release.sh --no-bump
+npx playwright install chromium
+npm run test:browser
+```
+
+The browser smoke test extracts the release ZIP and starts a local fixture server. Its temporary manifest pregrants only that fixture host. The distributed manifest continues to request optional permissions. The test exercises real extension messaging and HTTP requests. It requires an environment that permits Chromium processes and local sockets.
+
+The release script validates source before bumping versions. It updates the manifest, package and lockfile together and includes all runtime modules. GitHub Actions runs lint, regression tests, packaging and the Chromium smoke test, then provides the current ZIP as an artifact.
+
+## Operating limits
+
+Request and decoded response bodies are limited to 5 MiB. Larger responses fail explicitly. JSON responses above 200,000 characters display as plain text to keep the interface responsive. Copy and download retain the raw response text. Responses are text-oriented and do not provide lossless binary downloads.
+
+Timeouts range from 1 to 60 seconds. Chrome can terminate an extension service worker if response headers take over 30 seconds to arrive. The UI reports a disconnected background or recovers after the configured timeout plus five seconds. Failed or interrupted requests are never automatically retried because the server may already have processed them. See the [Chrome service worker lifecycle documentation](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle).
+
+Browser networking still controls restricted request headers, redirects and TLS validation. Cookies are omitted and the HTTP cache is bypassed. Closing the request page attempts to cancel its active request. Cancellation cannot reverse an action already performed by an API.
+
+Favorites, settings and environments use browser sync storage and remain subject to its quota. History and the last request use local extension storage. Request exports contain the entered headers and body. See [PRIVACY_POLICY.md](PRIVACY_POLICY.md) for details.
+
+## Author and license
+
+Hayk Jomardyan - [GitHub](https://github.com/jomardyan)
+
+The repository contains the Creative Commons Attribution-NoDerivatives 4.0 International license. See [LICENSE](LICENSE).
diff --git a/background.js b/background.js
index e927f3b..405dca1 100644
--- a/background.js
+++ b/background.js
@@ -1,97 +1,90 @@
const DEFAULT_TIMEOUT_MS = 15000;
-const DEBUG_LOGGING = false;
+const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
+const pendingRequests = new Map();
-// Track in-flight requests so cancellation can abort them
-const pendingRequests = new Map(); // requestId -> AbortController
+async function readResponse(res) {
+ if (!res.body) return { body: "", bodyBytes: 0 };
+ const reader = res.body.getReader();
+ const decoder = new TextDecoder();
+ const chunks = [];
+ let bodyBytes = 0;
+ try {
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ bodyBytes += value.byteLength;
+ if (bodyBytes > MAX_RESPONSE_BYTES) {
+ await reader.cancel();
+ throw new Error("Response exceeds the 5 MiB limit");
+ }
+ chunks.push(decoder.decode(value, { stream: true }));
+ }
+ chunks.push(decoder.decode());
+ return { body: chunks.join(""), bodyBytes };
+ } finally {
+ reader.releaseLock();
+ }
+}
-chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
- // Handle cancellation from popup
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ // Only extension pages may use the network bridge.
+ if (sender.id !== chrome.runtime.id || !sender.url?.startsWith(chrome.runtime.getURL(""))) return;
if (message?.type === "cancel-request") {
- const controller = pendingRequests.get(message.payload?.requestId);
- if (controller) {
- controller.abort();
- pendingRequests.delete(message.payload.requestId);
+ const request = pendingRequests.get(message.payload?.requestId);
+ if (request && request.owner === sender.url) {
+ request.cancelled = true;
+ request.controller.abort();
}
- return; // no async response needed
+ sendResponse({ ok: true });
+ return;
}
-
if (message?.type !== "api-request") return;
-
- const { url, method, headers, body, timeoutMs, requestId } = message.payload;
- const effectiveTimeout =
- typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
- const controller = new AbortController();
- const timeout = setTimeout(() => controller.abort(), effectiveTimeout);
-
- if (requestId) pendingRequests.set(requestId, controller);
-
+ const payload = message.payload;
+ let parsedUrl;
+ try {
+ if (!payload || typeof payload.requestId !== "string" || !payload.requestId || payload.requestId.length > 128) throw new Error("Invalid request ID");
+ if (pendingRequests.has(payload.requestId)) throw new Error("Request ID already in use");
+ if (pendingRequests.size >= 8) throw new Error("Too many active requests");
+ parsedUrl = new URL(payload.url);
+ if (!["http:", "https:"].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password) throw new Error("Only HTTP and HTTPS URLs without embedded credentials are supported");
+ if (!["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(payload.method)) throw new Error("Unsupported HTTP method");
+ if (payload.body != null && typeof payload.body !== "string") throw new Error("Invalid request body");
+ if (payload.body && new TextEncoder().encode(payload.body).length > MAX_RESPONSE_BYTES) throw new Error("Request exceeds the 5 MiB limit");
+ new Headers(payload.headers); // Validate before allocating request resources.
+ } catch (err) {
+ sendResponse({ ok: false, error: err.message });
+ return;
+ }
+ const { method, headers, body, requestId } = payload;
+ const timeoutMs = Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0
+ ? Math.min(60000, Math.max(1000, payload.timeoutMs)) : DEFAULT_TIMEOUT_MS;
+ const request = { controller: new AbortController(), cancelled: false, owner: sender.url };
+ pendingRequests.set(requestId, request);
+ const timeout = setTimeout(() => request.controller.abort(), timeoutMs);
const started = performance.now();
-
(async () => {
try {
- // Validate URL
- try {
- new URL(url);
- } catch (err) {
- sendResponse({
- ok: false,
- error: "Invalid URL format",
- });
- return;
- }
-
- const fetchOptions = {
- method,
- headers,
- signal: controller.signal,
- mode: 'cors',
- credentials: 'omit'
- };
-
- if (body && !['GET', 'HEAD'].includes(method)) {
- fetchOptions.body = body;
- }
-
- if (DEBUG_LOGGING) console.log("Fetching", url, fetchOptions);
- const res = await fetch(url, fetchOptions);
- const text = await res.text();
- const elapsed = Math.round(performance.now() - started);
- const headersEntries = Array.from(res.headers.entries());
-
- if (DEBUG_LOGGING) console.log("Fetch success", res.status);
+ const options = { method, headers, signal: request.controller.signal, credentials: "omit", cache: "no-store" };
+ if (!["GET", "HEAD"].includes(method) && body != null) options.body = body;
+ const res = await fetch(parsedUrl.href, options);
+ const result = await readResponse(res);
sendResponse({
- ok: true,
- status: res.status,
- statusText: res.statusText,
- url: res.url,
- type: res.type,
- elapsed,
- headers: headersEntries,
- body: text,
+ ok: true, status: res.status, statusText: res.statusText, url: res.url,
+ type: res.type, elapsed: Math.round(performance.now() - started),
+ headers: Array.from(res.headers.entries()), ...result,
});
} catch (err) {
- console.error('API request error:', err);
- const elapsed = Math.round(performance.now() - started);
sendResponse({
- ok: false,
- error:
- err.name === "AbortError"
- ? `Request timed out after ${Math.round(effectiveTimeout / 1000)}s`
- : err.name === "TypeError" && err.message.includes("fetch")
- ? "Network error: Unable to reach the server. Check your connection or the URL."
- : err.message || "Unknown error occurred",
- elapsed,
+ ok: false, cancelled: request.cancelled,
+ error: request.cancelled ? "Request cancelled" : request.controller.signal.aborted
+ ? `Request timed out after ${timeoutMs / 1000}s`
+ : err.message || "Network request failed",
+ elapsed: Math.round(performance.now() - started),
});
} finally {
clearTimeout(timeout);
- if (requestId) pendingRequests.delete(requestId);
+ if (pendingRequests.get(requestId) === request) pendingRequests.delete(requestId);
}
})();
-
- return true; // keep the message channel open for async response
+ return true;
});
-
-// Log extension startup
-if (DEBUG_LOGGING) {
- console.log("Quick API Client: Background service worker started");
-}
diff --git a/defaults.js b/defaults.js
index 1abcd0e..97807f0 100644
--- a/defaults.js
+++ b/defaults.js
@@ -16,9 +16,13 @@
function clampHistorySize(size) {
const num = Number(size);
if (!Number.isFinite(num)) return defaults.historySize;
- return Math.max(0, Math.min(50, num));
+ return Math.max(0, Math.min(50, Math.floor(num)));
}
+ window.clampTimeoutMs = (value) => {
+ const number = Number(value);
+ return Number.isFinite(number) && number > 0 ? Math.max(1000, Math.min(60000, Math.round(number))) : defaults.timeoutMs;
+ };
window.DEFAULT_OPTIONS = defaults;
window.clampHistorySize = clampHistorySize;
})();
diff --git a/dist/quick-api-client-v1.1.5.zip b/dist/quick-api-client-v1.1.5.zip
new file mode 100644
index 0000000..ea3372c
Binary files /dev/null and b/dist/quick-api-client-v1.1.5.zip differ
diff --git a/generate-screenshots.js b/generate-screenshots.js
index db926d2..b89f568 100644
--- a/generate-screenshots.js
+++ b/generate-screenshots.js
@@ -31,6 +31,8 @@ async function run() {
ok: true,
status: 200,
statusText: "OK",
+ type: "basic",
+ url: "https://api.example.com/users",
headers: [
["content-type", "application/json; charset=utf-8"],
["cache-control", "max-age=43200"],
diff --git a/manifest.json b/manifest.json
index 3a0edbd..9432ced 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Quick API Client",
- "version": "1.1.4",
+ "version": "1.1.5",
"author": "Hayk Jomardyan",
"homepage_url": "https://github.com/jomardyan/Quick-API-Client",
"description": "Test REST APIs from a popup: configure method, URL, headers, query params, body, view response, and copy cURL.",
diff --git a/options.js b/options.js
index 9f65d3b..bdd26e3 100644
--- a/options.js
+++ b/options.js
@@ -4,7 +4,7 @@ const clampHistorySize =
((size) => {
const num = Number(size);
if (!Number.isFinite(num)) return DEFAULT_OPTIONS.historySize;
- return Math.max(0, Math.min(50, num));
+ return Math.max(0, Math.min(50, Math.floor(num)));
});
const themeSelect = document.getElementById("themeSelect");
@@ -86,8 +86,8 @@ function parseKVText(text) {
if (Array.isArray(parsed)) {
return parsed
.map((item) => ({
- key: item.key?.trim?.() || "",
- value: item.value ?? "",
+ key: item?.key?.trim?.() || "",
+ value: String(item?.value ?? ""),
}))
.filter((kv) => kv.key);
}
@@ -127,7 +127,7 @@ function loadOptions() {
}
function saveOptions() {
- const timeoutMs = Math.max(1000, Math.min(60000, Number(timeoutSeconds.value) * 1000));
+ const timeoutMs = window.clampTimeoutMs(Number(timeoutSeconds.value) * 1000);
const size = clampHistorySize(historySize.value);
chrome.storage.sync.get("options", ({ options }) => {
const existing = options || {};
@@ -157,10 +157,22 @@ function saveOptions() {
}
function resetOptions() {
- chrome.storage.sync.set({ options: DEFAULT_OPTIONS }, () => {
- loadOptions();
- statusEl.textContent = "Reset to defaults.";
- setTimeout(() => (statusEl.textContent = ""), 1800);
+ chrome.storage.sync.get("options", ({ options }) => {
+ if (chrome.runtime.lastError) {
+ statusEl.textContent = "Reset failed - " + chrome.runtime.lastError.message;
+ return;
+ }
+ // Preferences reset must not delete saved requests.
+ const reset = { ...DEFAULT_OPTIONS, favorites: options?.favorites || [] };
+ chrome.storage.sync.set({ options: reset }, () => {
+ if (chrome.runtime.lastError) {
+ statusEl.textContent = "Reset failed - " + chrome.runtime.lastError.message;
+ return;
+ }
+ loadOptions();
+ statusEl.textContent = "Reset to defaults.";
+ setTimeout(() => (statusEl.textContent = ""), 1800);
+ });
});
}
@@ -179,25 +191,43 @@ function clearHistory() {
function loadEnvironments() {
chrome.storage.sync.get("environments", ({ environments: stored }) => {
- environments = stored || [];
+ environments = Array.isArray(stored) ? stored : [];
renderEnvSelect();
});
}
-function saveCurrentEnv() {
+function saveCurrentEnv(callback) {
if (selectedEnvIdx < 0 || selectedEnvIdx >= environments.length) return;
- environments[selectedEnvIdx].name = envNameInput.value.trim() || environments[selectedEnvIdx].name;
+ const name = envNameInput.value.trim() || environments[selectedEnvIdx].name;
+ if (environments.some((env, idx) => idx !== selectedEnvIdx && env.name === name)) {
+ statusEl.textContent = "An environment with this name already exists.";
+ return;
+ }
+ const previousName = environments[selectedEnvIdx].name;
+ environments[selectedEnvIdx].name = name;
environments[selectedEnvIdx].vars = parseKVText(envVarsInput.value);
- persistEnvironments();
+ persistEnvironments(callback, previousName, name);
}
-function persistEnvironments(callback) {
- chrome.storage.sync.set({ environments }, () => {
- if (chrome.runtime.lastError) {
- statusEl.textContent = "Env save failed: " + chrome.runtime.lastError.message;
- return;
+function persistEnvironments(callback, previousName, nextName) {
+ const snapshot = environments.map(env => ({ ...env, vars: env.vars.map(item => ({ ...item })) }));
+ [addEnvBtn, deleteEnvBtn, saveEnvBtn].forEach(button => { button.disabled = true; });
+ const fail = error => {
+ statusEl.textContent = "Env save failed - " + error.message;
+ loadEnvironments();
+ [addEnvBtn, deleteEnvBtn, saveEnvBtn].forEach(button => { button.disabled = false; });
+ };
+ chrome.storage.sync.get("options", ({ options }) => {
+ if (chrome.runtime.lastError) { fail(chrome.runtime.lastError); return; }
+ const update = { environments: snapshot };
+ if (previousName && options?.activeEnvironment === previousName) {
+ update.options = { ...options, activeEnvironment: nextName || "" };
}
- if (callback) callback();
+ chrome.storage.sync.set(update, () => {
+ if (chrome.runtime.lastError) { fail(chrome.runtime.lastError); return; }
+ [addEnvBtn, deleteEnvBtn, saveEnvBtn].forEach(button => { button.disabled = false; });
+ if (callback) callback();
+ });
});
}
@@ -237,7 +267,9 @@ function renderEnvEditor() {
}
addEnvBtn.addEventListener("click", () => {
- environments.push({ name: `Environment ${environments.length + 1}`, vars: [] });
+ let number = environments.length + 1;
+ while (environments.some(env => env.name === `Environment ${number}`)) number++;
+ environments.push({ name: `Environment ${number}`, vars: [] });
selectedEnvIdx = environments.length - 1;
persistEnvironments(() => {
renderEnvSelect();
@@ -251,15 +283,16 @@ deleteEnvBtn.addEventListener("click", () => {
showConfirm(`Delete environment "${name}"?`, () => {
environments.splice(selectedEnvIdx, 1);
selectedEnvIdx = Math.min(selectedEnvIdx, environments.length - 1);
- persistEnvironments(renderEnvSelect);
+ persistEnvironments(renderEnvSelect, name, "");
});
});
saveEnvBtn.addEventListener("click", () => {
- saveCurrentEnv();
- renderEnvSelect();
- statusEl.textContent = "Environment saved.";
- setTimeout(() => (statusEl.textContent = ""), 1800);
+ saveCurrentEnv(() => {
+ renderEnvSelect();
+ statusEl.textContent = "Environment saved.";
+ setTimeout(() => (statusEl.textContent = ""), 1800);
+ });
});
envNameSelect.addEventListener("change", () => {
diff --git a/package-lock.json b/package-lock.json
index 1d8a03b..cbfc3e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "quick-api-client",
- "version": "1.1.4",
+ "version": "1.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quick-api-client",
- "version": "1.1.4",
+ "version": "1.1.5",
"devDependencies": {
"@playwright/test": "^1.55.1",
"eslint": "^8.57.0",
diff --git a/package.json b/package.json
index 4ebb8f5..ed51ae7 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,13 @@
{
"name": "quick-api-client",
- "version": "1.1.4",
+ "version": "1.1.5",
"description": "Test REST/GraphQL APIs from a browser extension popup",
"private": true,
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
"test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch",
- "lint": "eslint ."
+ "lint": "eslint .",
+ "test:browser": "node scripts/browser-smoke.js"
},
"devDependencies": {
"@playwright/test": "^1.55.1",
@@ -24,6 +25,10 @@
"transform": {},
"testMatch": [
"**/tests/**/*.test.js"
+ ],
+ "coveragePathIgnorePatterns": [
+ "/node_modules/",
+ "/tests/"
]
}
}
diff --git a/popup.html b/popup.html
index 30a1f81..e5c1946 100644
--- a/popup.html
+++ b/popup.html
@@ -375,6 +375,7 @@