diff --git a/src/extensionsIntegrated/CustomSnippets/codeHintIntegration.js b/src/extensionsIntegrated/CustomSnippets/codeHintIntegration.js
index 9d4d1ee5f..2159fe8d4 100644
--- a/src/extensionsIntegrated/CustomSnippets/codeHintIntegration.js
+++ b/src/extensionsIntegrated/CustomSnippets/codeHintIntegration.js
@@ -116,7 +116,7 @@ define(function (require, exports, module) {
);
if (matchedSnippet) {
// Get current editor from EditorManager since it's not passed
- const editor = EditorManager.getFocusedEditor();
+ const editor = EditorManager.getActiveEditor();
if (editor) {
// to track the usage metrics
@@ -154,4 +154,5 @@ define(function (require, exports, module) {
}
exports.init = init;
+ exports._CustomSnippetsHandler = CustomSnippetsHandler; // exposed for integration testing
});
diff --git a/src/extensionsIntegrated/CustomSnippets/main.js b/src/extensionsIntegrated/CustomSnippets/main.js
index 5e2b1b3d0..75a78c35d 100644
--- a/src/extensionsIntegrated/CustomSnippets/main.js
+++ b/src/extensionsIntegrated/CustomSnippets/main.js
@@ -267,7 +267,7 @@ define(function (require, exports, module) {
CodeHintIntegration.init();
// load snippets from file storage
- SnippetsState.loadSnippetsFromState()
+ const _snippetsLoadedPromise = SnippetsState.loadSnippetsFromState()
.then(function () {
// track boot-time snippet count (only if user has snippets)
const snippetCount = Global.SnippetHintsList.length;
@@ -281,5 +281,15 @@ define(function (require, exports, module) {
});
SnippetCursorManager.registerHandlers();
+
+ // Expose modules for integration testing
+ if (brackets.test) {
+ brackets.test.CustomSnippetsGlobal = Global;
+ brackets.test.CustomSnippetsHelper = Helper;
+ brackets.test.CustomSnippetsCursorManager = SnippetCursorManager;
+ brackets.test.CustomSnippetsCodeHintHandler = CodeHintIntegration._CustomSnippetsHandler;
+ brackets.test.CustomSnippetsDriver = Driver;
+ brackets.test._customSnippetsLoadedPromise = _snippetsLoadedPromise;
+ }
});
});
diff --git a/src/extensionsIntegrated/CustomSnippets/snippetCursorManager.js b/src/extensionsIntegrated/CustomSnippets/snippetCursorManager.js
index b99455d5f..a7ce3de81 100644
--- a/src/extensionsIntegrated/CustomSnippets/snippetCursorManager.js
+++ b/src/extensionsIntegrated/CustomSnippets/snippetCursorManager.js
@@ -529,4 +529,6 @@ define(function (require, exports, module) {
exports.handleCursorActivity = handleCursorActivity;
exports.endSnippetSession = endSnippetSession;
exports.registerHandlers = registerHandlers;
+ exports.navigateToNextTabStop = navigateToNextTabStop; // exposed for integration testing
+ exports.navigateToPreviousTabStop = navigateToPreviousTabStop; // exposed for integration testing
});
diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js
index da2d38eed..c84fbec18 100644
--- a/test/UnitTestSuite.js
+++ b/test/UnitTestSuite.js
@@ -132,6 +132,8 @@ define(function (require, exports, module) {
require("spec/Extn-CSSColorPreview-integ-test");
require("spec/Extn-CollapseFolders-integ-test");
require("spec/Extn-Tabbar-integ-test");
+ require("spec/Extn-CustomSnippets-test");
+ require("spec/Extn-CustomSnippets-integ-test");
// extension integration tests
require("spec/Extn-CSSCodeHints-integ-test");
require("spec/Extn-HTMLCodeHints-Lint-integ-test");
diff --git a/test/spec/CustomSnippets-test-files/test.html b/test/spec/CustomSnippets-test-files/test.html
new file mode 100644
index 000000000..4d2268652
--- /dev/null
+++ b/test/spec/CustomSnippets-test-files/test.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/test/spec/CustomSnippets-test-files/test.js b/test/spec/CustomSnippets-test-files/test.js
new file mode 100644
index 000000000..a1fa2f6b7
--- /dev/null
+++ b/test/spec/CustomSnippets-test-files/test.js
@@ -0,0 +1 @@
+// test file for custom snippets integration tests
diff --git a/test/spec/CustomSnippets-test-files/test.py b/test/spec/CustomSnippets-test-files/test.py
new file mode 100644
index 000000000..5731e64d3
--- /dev/null
+++ b/test/spec/CustomSnippets-test-files/test.py
@@ -0,0 +1 @@
+# test file for custom snippets integration tests
diff --git a/test/spec/CustomSnippets-test-files/test.ts b/test/spec/CustomSnippets-test-files/test.ts
new file mode 100644
index 000000000..a1fa2f6b7
--- /dev/null
+++ b/test/spec/CustomSnippets-test-files/test.ts
@@ -0,0 +1 @@
+// test file for custom snippets integration tests
diff --git a/test/spec/Extn-CustomSnippets-integ-test.js b/test/spec/Extn-CustomSnippets-integ-test.js
new file mode 100644
index 000000000..f7be579a0
--- /dev/null
+++ b/test/spec/Extn-CustomSnippets-integ-test.js
@@ -0,0 +1,1036 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
+ *
+ */
+
+/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsForDone, awaitsFor */
+
+define(function (require, exports, module) {
+
+ const SpecRunnerUtils = require("spec/SpecRunnerUtils");
+
+ describe("integration:Custom Snippets Code Hints", function () {
+
+ const testPath = SpecRunnerUtils.getTestPath("/spec/CustomSnippets-test-files");
+
+ let testWindow,
+ brackets,
+ $,
+ EditorManager,
+ CommandManager,
+ Commands,
+ FileViewController,
+ CustomSnippetsGlobal,
+ CustomSnippetsHelper,
+ CustomSnippetsCursorManager,
+ CustomSnippetsHandler;
+
+ // Test snippets to inject for each test
+ const TEST_SNIPPETS = [
+ {
+ abbreviation: "clg",
+ description: "Console log",
+ templateText: "console.log(${1});${0}",
+ fileExtension: ".js, .ts"
+ },
+ {
+ abbreviation: "clgall",
+ description: "Console log for all files",
+ templateText: "console.log(${1});${0}",
+ fileExtension: "all"
+ },
+ {
+ abbreviation: "fnn",
+ description: "Arrow function",
+ templateText: "const ${1} = (${2}) => {\n ${3}\n};${0}",
+ fileExtension: ".js, .ts"
+ },
+ {
+ abbreviation: "pydef",
+ description: "Python function def",
+ templateText: "def ${1}(${2}):\n ${3}",
+ fileExtension: ".py"
+ },
+ {
+ abbreviation: "divbox",
+ description: "HTML div box",
+ templateText: "\n ${2}\n
${0}",
+ fileExtension: ".html"
+ },
+ {
+ abbreviation: "notabs",
+ description: "Snippet without tab stops",
+ templateText: "no tabs here",
+ fileExtension: "all"
+ },
+ {
+ abbreviation: "clgdup",
+ description: "Another clg variant",
+ templateText: "console.log('debug:', ${1});${0}",
+ fileExtension: ".js"
+ }
+ ];
+
+ let savedSnippetsList = [];
+
+ beforeAll(async function () {
+ testWindow = await SpecRunnerUtils.createTestWindowAndRun();
+ brackets = testWindow.brackets;
+ $ = testWindow.$;
+ EditorManager = brackets.test.EditorManager;
+ CommandManager = brackets.test.CommandManager;
+ Commands = brackets.test.Commands;
+ FileViewController = brackets.test.FileViewController;
+
+ // Wait for Custom Snippets extension to be loaded
+ await awaitsFor(function () {
+ return brackets.test.CustomSnippetsGlobal !== undefined;
+ }, "Custom Snippets to be loaded", 10000);
+
+ CustomSnippetsGlobal = brackets.test.CustomSnippetsGlobal;
+ CustomSnippetsHelper = brackets.test.CustomSnippetsHelper;
+ CustomSnippetsCursorManager = brackets.test.CustomSnippetsCursorManager;
+ CustomSnippetsHandler = brackets.test.CustomSnippetsCodeHintHandler;
+
+ // Wait for snippets to finish loading from storage
+ await brackets.test._customSnippetsLoadedPromise;
+
+ await SpecRunnerUtils.loadProjectInTestWindow(testPath);
+ }, 60000);
+
+ afterAll(async function () {
+ testWindow = null;
+ brackets = null;
+ $ = null;
+ EditorManager = null;
+ CommandManager = null;
+ Commands = null;
+ FileViewController = null;
+ CustomSnippetsGlobal = null;
+ CustomSnippetsHelper = null;
+ CustomSnippetsCursorManager = null;
+ CustomSnippetsHandler = null;
+ await SpecRunnerUtils.closeTestWindow();
+ }, 30000);
+
+ function setupTestSnippets() {
+ savedSnippetsList = CustomSnippetsGlobal.SnippetHintsList.slice();
+ CustomSnippetsGlobal.SnippetHintsList.length = 0;
+ TEST_SNIPPETS.forEach(function (snippet) {
+ CustomSnippetsGlobal.SnippetHintsList.push(Object.assign({}, snippet));
+ });
+ CustomSnippetsHelper.rebuildOptimizedStructures();
+ }
+
+ function restoreSnippets() {
+ CustomSnippetsGlobal.SnippetHintsList.length = 0;
+ savedSnippetsList.forEach(function (s) {
+ CustomSnippetsGlobal.SnippetHintsList.push(s);
+ });
+ CustomSnippetsHelper.rebuildOptimizedStructures();
+ savedSnippetsList = [];
+ }
+
+ async function openFile(fileName) {
+ await awaitsForDone(
+ FileViewController.openAndSelectDocument(
+ testPath + "/" + fileName,
+ FileViewController.PROJECT_MANAGER
+ ),
+ "open file: " + fileName
+ );
+ }
+
+ async function closeAllFiles() {
+ await awaitsForDone(
+ CommandManager.execute(Commands.FILE_CLOSE_ALL, { _forceClose: true }),
+ "closing all files"
+ );
+ }
+
+ /**
+ * Open a file and clear its content for a clean test
+ */
+ async function openCleanFile(fileName) {
+ await openFile(fileName);
+ const editor = EditorManager.getActiveEditor();
+ editor.document.setText("");
+ editor.setCursorPos({line: 0, ch: 0});
+ return editor;
+ }
+
+ /**
+ * Type text at the current cursor position
+ */
+ function typeAtCursor(editor, text) {
+ const pos = editor.getCursorPos();
+ editor.document.replaceRange(text, pos);
+ }
+
+ // ================================================================
+ // Test Suite: Hint Availability (hasHints)
+ // ================================================================
+ describe("Hint Availability", function () {
+
+ beforeEach(function () {
+ setupTestSnippets();
+ });
+
+ afterEach(async function () {
+ if (CustomSnippetsCursorManager.isInSnippetSession()) {
+ CustomSnippetsCursorManager.endSnippetSession();
+ }
+ restoreSnippets();
+ await closeAllFiles();
+ });
+
+ it("should have hints when exact snippet abbreviation is typed in JS file", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ expect(CustomSnippetsHandler.hasHints(editor, "g")).toBeTrue();
+ });
+
+ it("should NOT have hints when implicitChar is null (explicit invocation)", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ expect(CustomSnippetsHandler.hasHints(editor, null)).toBeFalse();
+ });
+
+ it("should NOT have hints for non-matching text", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "xyzabc");
+
+ expect(CustomSnippetsHandler.hasHints(editor, "c")).toBeFalse();
+ });
+
+ it("should NOT have hints when word before cursor is empty", async function () {
+ const editor = await openCleanFile("test.js");
+
+ expect(CustomSnippetsHandler.hasHints(editor, " ")).toBeFalse();
+ });
+
+ it("should NOT have hints for partial match only (no exact abbreviation match)", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "cl");
+
+ expect(CustomSnippetsHandler.hasHints(editor, "l")).toBeFalse();
+ });
+
+ it("should have hints for case-insensitive abbreviation matching", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "CLG");
+
+ expect(CustomSnippetsHandler.hasHints(editor, "G")).toBeTrue();
+ });
+ });
+
+ // ================================================================
+ // Test Suite: Hint Results (getHints)
+ // ================================================================
+ describe("Hint Results", function () {
+
+ beforeEach(function () {
+ setupTestSnippets();
+ });
+
+ afterEach(async function () {
+ if (CustomSnippetsCursorManager.isInSnippetSession()) {
+ CustomSnippetsCursorManager.endSnippetSession();
+ }
+ restoreSnippets();
+ await closeAllFiles();
+ });
+
+ it("should return hint objects with data-isCustomSnippet attribute", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ expect(result).toBeTruthy();
+ expect(result.hints).toBeTruthy();
+ expect(result.hints.length).toBeGreaterThan(0);
+
+ const firstHint = result.hints[0];
+ expect(firstHint.attr("data-isCustomSnippet")).toBe("true");
+ expect(firstHint.attr("data-val")).toBe("clg");
+ });
+
+ it("should return hints with custom-snippets-hint CSS class", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ expect(result.hints[0].hasClass("custom-snippets-hint")).toBeTrue();
+ });
+
+ it("should return exact match as first hint when partial matches also exist", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ expect(result).toBeTruthy();
+ // "clg" matches: clg (exact), clgall (prefix), clgdup (prefix)
+ expect(result.hints.length).toBeGreaterThan(1);
+ expect(result.hints[0].attr("data-val")).toBe("clg");
+
+ const otherVals = result.hints.slice(1).map(function (h) {
+ return h.attr("data-val");
+ });
+ expect(otherVals).toContain("clgall");
+ expect(otherVals).toContain("clgdup");
+ });
+
+ it("should show snippet indicator label in hint items", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ const snippetLabel = result.hints[0].find(".custom-snippet-code-hint");
+ expect(snippetLabel.length).toBe(1);
+ });
+
+ it("should show description in hint when description is provided", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ const descElem = result.hints[0].find(".snippet-description");
+ expect(descElem.length).toBe(1);
+ expect(descElem.text()).toBe("Console log");
+ });
+
+ it("should return selectInitial true in hint response", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ expect(result.selectInitial).toBeTrue();
+ });
+
+ it("should return null when no exact abbreviation match exists", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "xyz");
+
+ const result = CustomSnippetsHandler.getHints(editor, "z");
+ expect(result).toBeNull();
+ });
+
+ it("should highlight matching characters in hint text", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ const matchedSpans = result.hints[0].find(".matched-hint");
+ expect(matchedSpans.length).toBeGreaterThan(0);
+ });
+ });
+
+ // ================================================================
+ // Test Suite: Language Filtering
+ // ================================================================
+ describe("Language Filtering", function () {
+
+ beforeEach(function () {
+ setupTestSnippets();
+ });
+
+ afterEach(async function () {
+ if (CustomSnippetsCursorManager.isInSnippetSession()) {
+ CustomSnippetsCursorManager.endSnippetSession();
+ }
+ restoreSnippets();
+ await closeAllFiles();
+ });
+
+ it("should show JS-scoped snippet in JS file", async function () {
+ const editor = await openCleanFile("test.js");
+ typeAtCursor(editor, "clg");
+
+ expect(CustomSnippetsHandler.hasHints(editor, "g")).toBeTrue();
+
+ const result = CustomSnippetsHandler.getHints(editor, "g");
+ const hintValues = result.hints.map(function (h) { return h.attr("data-val"); });
+ expect(hintValues).toContain("clg");
+ });
+
+ it("should NOT show JS-scoped snippet in HTML file outside script tag", async function () {
+ await openFile("test.html");
+ const editor = EditorManager.getActiveEditor();
+ // Line 3 is an empty line inside but outside