|
7 | 7 |
|
8 | 8 | define(function (require, exports, module) { |
9 | 9 | const SpecRunnerUtils = require("spec/SpecRunnerUtils"), |
10 | | - FileSystemError = require("filesystem/FileSystemError"); |
| 10 | + FileSystemError = require("filesystem/FileSystemError"), |
| 11 | + MessageIds = JSON.parse(require("text!JSUtils/MessageIds.json")); |
11 | 12 |
|
12 | 13 | describe("integration:JavaScript hint initialization", function () { |
13 | | - let testWindow, FileSystem, ScopeManager, EditorManager, Session, testFolder; |
| 14 | + let testWindow, FileSystem, ScopeManager, EditorManager, Session, IndexingWorker, testFolder; |
14 | 15 |
|
15 | 16 | beforeAll(async function () { |
16 | 17 | testWindow = await SpecRunnerUtils.createTestWindowAndRun(); |
17 | 18 | FileSystem = testWindow.brackets.test.FileSystem; |
18 | 19 | EditorManager = testWindow.brackets.test.EditorManager; |
19 | 20 | ScopeManager = testWindow.brackets.getModule("JSUtils/ScopeManager"); |
20 | 21 | Session = testWindow.brackets.getModule("JSUtils/Session"); |
| 22 | + IndexingWorker = testWindow.brackets.getModule("worker/IndexingWorker"); |
21 | 23 | }, 30000); |
22 | 24 |
|
23 | 25 | beforeEach(async function () { |
@@ -95,5 +97,150 @@ define(function (require, exports, module) { |
95 | 97 | reads.restore(); |
96 | 98 | } |
97 | 99 | }); |
| 100 | + |
| 101 | + it("should finish initialization when a background directory disappears", async function () { |
| 102 | + await SpecRunnerUtils.createTextFileAsync(testFolder + "/hints.js", "const arr = [1, 2, 3];\narr."); |
| 103 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["hints.js"]), "open JavaScript fixture"); |
| 104 | + const editor = EditorManager.getCurrentFullEditor(); |
| 105 | + editor.setCursorPos(1, 4); |
| 106 | + const session = new Session(editor); |
| 107 | + await awaitsForDone(ScopeManager.requestHints(session, editor.document), "initialize Tern"); |
| 108 | + |
| 109 | + const directoryPath = editor.document.file.parentPath; |
| 110 | + const resolve = FileSystem.resolve; |
| 111 | + let directoryReads = 0, interruptedLookup; |
| 112 | + const lookup = spyOn(FileSystem, "resolve").and.callFake(function (filePath, callback) { |
| 113 | + if (filePath === directoryPath && ++directoryReads === 2) { |
| 114 | + // The first lookup initializes Tern. The second starts its background scan, |
| 115 | + // after the directory could have been deleted during a project switch. |
| 116 | + interruptedLookup = callback; |
| 117 | + callback(FileSystemError.NOT_FOUND); |
| 118 | + return; |
| 119 | + } |
| 120 | + return resolve.apply(this, arguments); |
| 121 | + }); |
| 122 | + const ready = ScopeManager._maybeReset(session, editor.document, true); |
| 123 | + try { |
| 124 | + await awaitsFor(() => !!interruptedLookup, "background directory lookup"); |
| 125 | + await awaitsFor(() => ready.state() !== "pending", "initialization after a missing directory", 5000); |
| 126 | + await awaitsForDone(ready, "initialization after a missing directory"); |
| 127 | + const hints = ScopeManager.requestHints(session, editor.document); |
| 128 | + await awaitsForDone(hints, "hints after a missing directory"); |
| 129 | + expect(session.ternHints.some(hint => hint.value === "push")).toBeTrue(); |
| 130 | + } finally { |
| 131 | + lookup.and.callThrough(); |
| 132 | + if (interruptedLookup && ready.state() === "pending") { |
| 133 | + // Release the injected failure even if an assertion fails, so subsequent |
| 134 | + // specs are not left behind the same unfinished initialization. |
| 135 | + resolve.call(FileSystem, directoryPath, interruptedLookup); |
| 136 | + await awaitsForDone(ready, "finish test cleanup"); |
| 137 | + } |
| 138 | + } |
| 139 | + }); |
| 140 | + |
| 141 | + it("should initialize HTML without parsing markup as JavaScript", async function () { |
| 142 | + const worker = spyOn(IndexingWorker, "execPeer").and.callThrough(); |
| 143 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["embedded.html"]), "open embedded script"); |
| 144 | + const editor = EditorManager.getCurrentFullEditor(); |
| 145 | + editor.setCursorPos(6, 4); |
| 146 | + const session = new Session(editor); |
| 147 | + await awaitsForDone(ScopeManager.requestHints(session, editor.document), "embedded script hints"); |
| 148 | + |
| 149 | + const reads = worker.calls.allArgs().filter(args => args[0] === "invokeTernCommand" && |
| 150 | + args[1].type === MessageIds.TERN_GET_FILE_MSG && args[1].file.endsWith("/embedded.html")); |
| 151 | + expect(reads.length).toBeGreaterThan(0); |
| 152 | + reads.forEach(args => expect(args[1].text).not.toContain("<html>")); |
| 153 | + expect(session.ternHints.some(hint => hint.value === "push")).toBeTrue(); |
| 154 | + }); |
| 155 | + |
| 156 | + it("should serialize editor changes queued behind initialization", async function () { |
| 157 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["embedded.html"]), "open embedded script"); |
| 158 | + const editor = EditorManager.getCurrentFullEditor(); |
| 159 | + editor.setCursorPos(6, 4); |
| 160 | + const session = new Session(editor); |
| 161 | + await awaitsForDone(ScopeManager.requestHints(session, editor.document), "initialize Tern"); |
| 162 | + |
| 163 | + const documents = []; |
| 164 | + for (const name of ["queued-a.html", "queued-b.html", "queued-c.html"]) { |
| 165 | + const path = testFolder + "/" + name; |
| 166 | + await SpecRunnerUtils.createTextFileAsync(path, editor.document.getText()); |
| 167 | + documents.push(await testWindow.brackets.test.DocumentManager.getDocumentForPath(path)); |
| 168 | + } |
| 169 | + |
| 170 | + const directoryPath = editor.document.file.parentPath; |
| 171 | + const resolve = FileSystem.resolve; |
| 172 | + const callbacks = []; |
| 173 | + const lookup = spyOn(FileSystem, "resolve").and.callFake(function (filePath, callback) { |
| 174 | + if (filePath === directoryPath) { |
| 175 | + callbacks.push(callback); |
| 176 | + return; |
| 177 | + } |
| 178 | + return resolve.apply(this, arguments); |
| 179 | + }); |
| 180 | + try { |
| 181 | + documents.forEach(document => ScopeManager.handleEditorChange(session, document, null)); |
| 182 | + expect(callbacks.length).toBe(1); |
| 183 | + resolve.call(FileSystem, directoryPath, callbacks.shift()); |
| 184 | + await awaitsFor(() => callbacks.length > 0, "next queued editor initialization"); |
| 185 | + // Waking several queued changes must start only the next initialization. |
| 186 | + // Concurrent initializations replace each other's worker-ready handler. |
| 187 | + expect(callbacks.length).toBe(1); |
| 188 | + } finally { |
| 189 | + // Fail held lookups before restoring them, including any cleanup queues. |
| 190 | + while (callbacks.length) { |
| 191 | + callbacks.shift()(FileSystemError.NOT_FOUND); |
| 192 | + } |
| 193 | + lookup.and.callThrough(); |
| 194 | + } |
| 195 | + |
| 196 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["queued-c.html"]), "open last queued editor"); |
| 197 | + const finalEditor = EditorManager.getCurrentFullEditor(); |
| 198 | + finalEditor.setCursorPos(6, 4); |
| 199 | + const finalSession = new Session(finalEditor); |
| 200 | + await awaitsForDone(ScopeManager.requestHints(finalSession, finalEditor.document), "hints after queued editors"); |
| 201 | + expect(finalSession.ternHints.some(hint => hint.value === "push")).toBeTrue(); |
| 202 | + }); |
| 203 | + |
| 204 | + it("should exclude markup when updating a dirty HTML document before switching editors", async function () { |
| 205 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["embedded.html"]), "open embedded script"); |
| 206 | + const previousEditor = EditorManager.getCurrentFullEditor(); |
| 207 | + previousEditor.setCursorPos(6, 4); |
| 208 | + await awaitsForDone(ScopeManager.requestHints(new Session(previousEditor), previousEditor.document), |
| 209 | + "initialize embedded script"); |
| 210 | + await SpecRunnerUtils.createTextFileAsync(testFolder + "/other.html", previousEditor.document.getText()); |
| 211 | + |
| 212 | + const worker = spyOn(IndexingWorker, "execPeer").and.callThrough(); |
| 213 | + previousEditor.document.setText(previousEditor.document.getText() + "\n<!-- unsaved edit -->"); |
| 214 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["other.html"]), "switch embedded script"); |
| 215 | + const editor = EditorManager.getCurrentFullEditor(); |
| 216 | + editor.setCursorPos(6, 4); |
| 217 | + const session = new Session(editor); |
| 218 | + await awaitsForDone(ScopeManager.requestHints(session, editor.document), "hints after dirty HTML update"); |
| 219 | + |
| 220 | + const updates = worker.calls.allArgs().filter(args => args[0] === "invokeTernCommand" && |
| 221 | + args[1].type === MessageIds.TERN_UPDATE_FILE_MSG && |
| 222 | + args[1].path === previousEditor.document.file.fullPath); |
| 223 | + expect(updates.length).toBeGreaterThan(0); |
| 224 | + updates.forEach(args => expect(args[1].text).not.toContain("<html>")); |
| 225 | + expect(session.ternHints.some(hint => hint.value === "push")).toBeTrue(); |
| 226 | + }); |
| 227 | + |
| 228 | + it("should analyze embedded JavaScript without sending PHP host code to Tern", async function () { |
| 229 | + const html = await SpecRunnerUtils.readTextFileAsync(testFolder + "/embedded.html"); |
| 230 | + await SpecRunnerUtils.createTextFileAsync(testFolder + "/embedded.php", '<?php echo "header"; ?>\n' + html); |
| 231 | + const worker = spyOn(IndexingWorker, "execPeer").and.callThrough(); |
| 232 | + await awaitsForDone(SpecRunnerUtils.openProjectFiles(["embedded.php"]), "open PHP embedded script"); |
| 233 | + const editor = EditorManager.getCurrentFullEditor(); |
| 234 | + editor.setCursorPos(7, 4); |
| 235 | + const session = new Session(editor); |
| 236 | + await awaitsForDone(ScopeManager.requestHints(session, editor.document), "PHP embedded script hints"); |
| 237 | + |
| 238 | + const reads = worker.calls.allArgs().filter(args => args[0] === "invokeTernCommand" && |
| 239 | + args[1].type === MessageIds.TERN_GET_FILE_MSG && args[1].file.endsWith("/embedded.php")); |
| 240 | + expect(reads.length).toBeGreaterThan(0); |
| 241 | + reads.forEach(args => expect(args[1].text).not.toContain("<?php")); |
| 242 | + expect(session.ternHints.some(hint => hint.value === "push")).toBeTrue(); |
| 243 | + }); |
| 244 | + |
98 | 245 | }); |
99 | 246 | }); |
0 commit comments