From f962fffb39a64a9a0095e8bb5d0081d4a9ee8761 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 20:54:40 +0530 Subject: [PATCH 01/11] chore: increase video test timeouts and fail fast on decode errors --- test/spec/MainViewFactory-integ-test.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/spec/MainViewFactory-integ-test.js b/test/spec/MainViewFactory-integ-test.js index 7b0a682f4b..9db2559c21 100644 --- a/test/spec/MainViewFactory-integ-test.js +++ b/test/spec/MainViewFactory-integ-test.js @@ -116,14 +116,19 @@ define(function (require, exports, module) { promise = MainViewManager._open(MainViewManager.ACTIVE_PANE, getFileObject("/videos/small.mp4")); await awaitsForDone(promise, "MainViewManager.doOpen"); const videoEl = _$(".media-view video.media-preview")[0]; + await awaitsFor(function () { + if (_$(".media-view .media-error").is(":visible")) { + throw new Error("video failed to load/decode: " + + _$(".media-view .media-error").text()); + } return videoEl.readyState >= 1; // HAVE_METADATA - }, "video metadata to load", 10000); + }, "video metadata to load", 30000); expect(videoEl.videoWidth).toEqual(320); expect(videoEl.videoHeight).toEqual(240); await awaitsFor(function () { return _$(".media-view .media-data").text().length > 0; - }, "video header data to render", 10000); + }, "video header data to render", 30000); const dataText = _$(".media-view .media-data").text(); expect(dataText).toContain("320"); expect(dataText).toContain("240"); From b5672d992239623f2fc9344a8999c1c5fc9a82cf Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 20:54:49 +0530 Subject: [PATCH 02/11] chore: retry jump to definition in browser to fix flaky ci test --- test/spec/EditorCommandHandlers-integ-test.js | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/test/spec/EditorCommandHandlers-integ-test.js b/test/spec/EditorCommandHandlers-integ-test.js index 7a0489db0b..c9ca50ac6f 100644 --- a/test/spec/EditorCommandHandlers-integ-test.js +++ b/test/spec/EditorCommandHandlers-integ-test.js @@ -217,6 +217,24 @@ define(function (require, exports, module) { "Open into working set"); myEditor = EditorManager.getCurrentFullEditor(); + // one jump attempt capped at 3s, so a hung request just counts + // as a failed attempt and the caller can retry + function attemptJumpToDefinition() { + return new Promise(function (resolve) { + let settled = false; + function settle(val) { + if (!settled) { + settled = true; + resolve(val); + } + } + CommandManager.execute(Commands.NAVIGATE_JUMPTO_DEFINITION) + .done(function () { settle(true); }) + .fail(function () { settle(false); }); + setTimeout(function () { settle(false); }, 3000); + }); + } + if (window.Phoenix.isNativeApp) { // Desktop has the LSP server (vtsls), which provides jump-to-definition for // JavaScript at a higher priority than the built-in Tern provider. vtsls returns @@ -230,23 +248,11 @@ define(function (require, exports, module) { // awaitsFor aborts outright on a pollFn exception, so the promise is absorbed. await awaitsFor(async function () { myEditor.setCursorPos({line: 5, ch: 8}); - var landed = await new Promise(function (resolve) { - var settled = false; - function settle(val) { - if (!settled) { - settled = true; - resolve(val); - } - } - CommandManager.execute(Commands.NAVIGATE_JUMPTO_DEFINITION) - .done(function () { settle(true); }) - .fail(function () { settle(false); }); - setTimeout(function () { settle(false); }, 3000); - }); + const landed = await attemptJumpToDefinition(); if (!landed) { return false; } - var sel = myEditor.getSelection(); + const sel = myEditor.getSelection(); return sel.start.line === 0 && sel.start.ch === 0 && sel.end.line === 0 && sel.end.ch === 0; }, "LSP jump-to-definition to land on the testMe declaration", 15000, 500); @@ -256,9 +262,18 @@ define(function (require, exports, module) { end: {line: 0, ch: 0} })); } else { - myEditor.setCursorPos({line: 5, ch: 8}); - await awaitsForDone(CommandManager.execute(Commands.NAVIGATE_JUMPTO_DEFINITION), - "Jump To Definition"); + // Tern can stall on a slow CI runner while it warms up, so + // retry with capped attempts, same as the LSP path above + await awaitsFor(async function () { + myEditor.setCursorPos({line: 5, ch: 8}); + const landed = await attemptJumpToDefinition(); + if (!landed) { + return false; + } + const sel = myEditor.getSelection(); + return sel.start.line === 0 && sel.start.ch === 9 && + sel.end.line === 0 && sel.end.ch === 15; + }, "Tern jump-to-definition to select the testMe identifier", 30000, 500); selection = myEditor.getSelection(); expect(fixSel(selection)).toEql(fixSel({ start: {line: 0, ch: 9}, From 336cac09bbacf8e036f2ed237e7d8660d86ae391 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:30 +0530 Subject: [PATCH 03/11] chore: use webm in video metadata test as playwright chromium lacks h264 --- test/spec/MainViewFactory-integ-test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/spec/MainViewFactory-integ-test.js b/test/spec/MainViewFactory-integ-test.js index 9db2559c21..76ba26d366 100644 --- a/test/spec/MainViewFactory-integ-test.js +++ b/test/spec/MainViewFactory-integ-test.js @@ -113,7 +113,8 @@ define(function (require, exports, module) { expect(_$(".media-view video.media-preview").length).toEqual(1); }); it("should load video metadata and show dimensions, duration and size", async function () { - promise = MainViewManager._open(MainViewManager.ACTIVE_PANE, getFileObject("/videos/small.mp4")); + // webm, not mp4: Playwright's Chromium has no H.264 codec + promise = MainViewManager._open(MainViewManager.ACTIVE_PANE, getFileObject("/videos/small.webm")); await awaitsForDone(promise, "MainViewManager.doOpen"); const videoEl = _$(".media-view video.media-preview")[0]; @@ -132,7 +133,7 @@ define(function (require, exports, module) { const dataText = _$(".media-view .media-data").text(); expect(dataText).toContain("320"); expect(dataText).toContain("240"); - expect(_$(".media-view .media-path").text()).toContain("small.mp4"); + expect(_$(".media-view .media-path").text()).toContain("small.webm"); }); it("should open a webm video", async function () { promise = MainViewManager._open(MainViewManager.ACTIVE_PANE, getFileObject("/videos/small.webm")); From 95a6bf84fdee8bd9c473101649f0f0889badd20a Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:30 +0530 Subject: [PATCH 04/11] chore: keep encoding test fixtures byte exact on windows checkouts --- .gitattributes | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index eba1110b57..f1b23ccf15 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Auto detect text files and perform LF normalization -* text=auto \ No newline at end of file +* text=auto + +# must stay byte-exact; autocrlf on Windows CI breaks the file-encoding tests +test/spec/encoding-test-files/* -text From bcb41b5a305d468827ab1d1d185f36ea4d5c49e0 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:30 +0530 Subject: [PATCH 05/11] chore: retry eslint fix all in test when the stale fix dialog appears --- test/spec/Extn-ESLint-integ-test.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/spec/Extn-ESLint-integ-test.js b/test/spec/Extn-ESLint-integ-test.js index 44ff01065a..ef09d0ef00 100644 --- a/test/spec/Extn-ESLint-integ-test.js +++ b/test/spec/Extn-ESLint-integ-test.js @@ -452,13 +452,27 @@ define(function (require, exports, module) { it("should be able to fix all errors", async function () { await _openAndVerifyInitial(); - const editor = EditorManager.getCurrentFullEditor(); + let editor = EditorManager.getCurrentFullEditor(); editor.setCursorPos(0, 0); // resent any saved selections from previous run // click on fix : Expected indentation of 4 spaces but found 9. ESLint (indent) + // a late doc reload can stale the fixes and pop the "Failed to Apply Fix" dialog + // on slow CI; dismiss it, re-lint and retry so it doesn't leak into later suites $($("#problems-panel").find(".problems-fix-all-btn")).click(); - await awaitsFor(()=>{ + await awaitsFor(async ()=>{ + if($(".error-dialog.instance").length){ + testWindow.brackets.test.Dialogs.cancelModalDialogIfOpen( + testWindow.brackets.test.DefaultDialogs.DIALOG_ID_ERROR); + await _triggerLint(); + await awaitsFor(()=>{ + return $("#problems-panel").find(".ph-fix-problem").length === 2; + }, "fix buttons to reappear after re-lint", 15000); + editor = EditorManager.getCurrentFullEditor(); + editor.setCursorPos(0, 0); + $($("#problems-panel").find(".problems-fix-all-btn")).click(); + return false; + } return $("#problems-panel").find(".ph-fix-problem").length === 0; - }, "no problems should remain as all is now fixed", 15000); + }, "no problems should remain as all is now fixed", 30000); // fixing multiple should place the cursor on first fix expect(editor.hasSelection()).toBeFalse(); @@ -472,7 +486,7 @@ define(function (require, exports, module) { await awaitsFor(()=>{ return $("#problems-panel").find(".ph-fix-problem").length === 2; }, "2 problem should be there", 15000); - }, 30000); + }, 60000); }); }); }); From 6fe5e9d6b8463ff919ecfd01bbd5b1714b1c8e33 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:30 +0530 Subject: [PATCH 06/11] chore: wait for recent projects filter to apply before keyboard delete in test --- test/spec/Extn-RecentProjects-integ-test.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/spec/Extn-RecentProjects-integ-test.js b/test/spec/Extn-RecentProjects-integ-test.js index 9ae0de9d1f..1c84f07088 100644 --- a/test/spec/Extn-RecentProjects-integ-test.js +++ b/test/spec/Extn-RecentProjects-integ-test.js @@ -119,12 +119,17 @@ define(function (require, exports, module) { let text = $dropDown.text(); expect(text.includes(jsUtilsTestFolderProjectName)).toBeTrue(); typeInEncodingPopup("JSUtils"); + // if the filter hasn't applied yet, DELETE would remove the wrong entry + await awaitsFor(function () { + return $dropDown.find(".recent-folder-link:visible").length === 1; + }, "dropdown to filter down to the JSUtils project", 5000); SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_DOWN, "keydown", $dropDown[0]); SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_DELETE, "keydown", $dropDown[0]); - text = $dropDown.text(); - expect(text.includes(jsUtilsTestFolderProjectName)).toBeFalse(); + await awaitsFor(function () { + return !$dropDown.text().includes(jsUtilsTestFolderProjectName); + }, "JSUtils project to be removed from the recent projects list", 5000); }); }); }); From b34e7a9aeeeb8cca1ae5ca7bb712b071c1df7744 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:40 +0530 Subject: [PATCH 07/11] chore: increase markdown editor test timeouts for slow ci runners --- test/spec/md-editor-edit-integ-test.js | 4 +- test/spec/md-editor-edit-more-integ-test.js | 80 ++++++++++----------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/test/spec/md-editor-edit-integ-test.js b/test/spec/md-editor-edit-integ-test.js index 3531ecbea3..0da08f00f2 100644 --- a/test/spec/md-editor-edit-integ-test.js +++ b/test/spec/md-editor-edit-integ-test.js @@ -577,11 +577,11 @@ define(function (require, exports, module) { await awaitsFor(() => { const blocks = mdDoc.querySelectorAll("#viewer-content pre code"); return blocks.length > 0 && blocks[0].textContent.includes("world"); - }, "viewer code block to reflect CM edit"); + }, "viewer code block to reflect CM edit", 10000); await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }), "force close"); - }, 10000); + }, 30000); it("should changing code block language in CM update viewer syntax class", async function () { await _openMdFile("code-block-test.md"); diff --git a/test/spec/md-editor-edit-more-integ-test.js b/test/spec/md-editor-edit-more-integ-test.js index 2c8ba74ed8..92d2d4ca28 100644 --- a/test/spec/md-editor-edit-more-integ-test.js +++ b/test/spec/md-editor-edit-more-integ-test.js @@ -255,7 +255,7 @@ define(function (require, exports, module) { } else { _pressKeyInSearch("Escape"); } - await awaitsFor(() => !_isSearchOpen(), "search bar to close"); + await awaitsFor(() => !_isSearchOpen(), "search bar to close", 10000); } } @@ -265,48 +265,48 @@ define(function (require, exports, module) { beforeAll(async function () { await enterModeFn(); - }, 10000); + }, 30000); it("should Ctrl+F open search bar", async function () { expect(_isSearchOpen()).toBeFalse(); _openSearchWithCtrlF(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); await _closeSearch(); - }, 10000); + }, 30000); it("should typing in search highlight matches", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("Document"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to be highlighted"); + "matches to be highlighted", 10000); expect(_getActiveMatch()).not.toBeNull(); await _closeSearch(); - }, 10000); + }, 30000); it("should match count show N/total format", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("Document"); await awaitsFor(() => { const count = _getSearchCount(); return count && /^\d+\/\d+$/.test(count.textContent); - }, "match count to show N/total format"); + }, "match count to show N/total format", 10000); expect(_getSearchCount().textContent).toMatch(/^1\/\d+$/); await _closeSearch(); - }, 10000); + }, 30000); it("should Enter navigate to next match", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("Document"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to appear"); + "matches to appear", 10000); const firstActive = _getActiveMatch(); expect(firstActive).not.toBeNull(); @@ -315,41 +315,41 @@ define(function (require, exports, module) { await awaitsFor(() => { const active = _getActiveMatch(); return active && active !== firstActive; - }, "active match to change on Enter"); + }, "active match to change on Enter", 10000); await _closeSearch(); - }, 10000); + }, 30000); it("should Shift+Enter navigate to previous match", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("Document"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to appear"); + "matches to appear", 10000); _pressKeyInSearch("Enter"); await awaitsFor(() => { const count = _getSearchCount(); return count && count.textContent.startsWith("2/"); - }, "to be on match 2"); + }, "to be on match 2", 10000); _pressKeyInSearch("Enter", { shiftKey: true }); await awaitsFor(() => { const count = _getSearchCount(); return count && count.textContent.startsWith("1/"); - }, "to navigate back to match 1"); + }, "to navigate back to match 1", 10000); await _closeSearch(); - }, 10000); + }, 30000); it("should navigation wrap around", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("Document"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to appear"); + "matches to appear", 10000); const totalMatches = _getHighlightedMatches().length; for (let i = 0; i < totalMatches - 1; i++) { @@ -358,27 +358,27 @@ define(function (require, exports, module) { await awaitsFor(() => { const count = _getSearchCount(); return count && count.textContent.startsWith(totalMatches + "/"); - }, "to be on last match"); + }, "to be on last match", 10000); _pressKeyInSearch("Enter"); await awaitsFor(() => { const count = _getSearchCount(); return count && count.textContent.startsWith("1/"); - }, "to wrap to first match"); + }, "to wrap to first match", 10000); await _closeSearch(); - }, 10000); + }, 30000); it("should Escape close search and restore focus", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("test"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to appear"); + "matches to appear", 10000); _pressKeyInSearch("Escape"); - await awaitsFor(() => !_isSearchOpen(), "search bar to close"); + await awaitsFor(() => !_isSearchOpen(), "search bar to close", 10000); // Focus should leave the search input const mdDoc = _getMdIFrameDoc(); @@ -386,47 +386,47 @@ define(function (require, exports, module) { await awaitsFor(() => { return mdDoc.activeElement !== searchInput; }, "focus to leave search input"); - }, 10000); + }, 30000); it("should closing search clear all highlights", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("Document"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to appear"); + "matches to appear", 10000); await _closeSearch(); expect(_getHighlightedMatches().length).toBe(0); - }, 10000); + }, 30000); it("should close button close search", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("test"); await awaitsFor(() => _getHighlightedMatches().length > 0, - "matches to appear"); + "matches to appear", 10000); _getMdIFrameDoc().getElementById("search-close").click(); - await awaitsFor(() => !_isSearchOpen(), "search bar to close via × button"); + await awaitsFor(() => !_isSearchOpen(), "search bar to close via × button", 10000); expect(_getHighlightedMatches().length).toBe(0); - }, 10000); + }, 30000); it("should search start from 1 character", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); _typeInSearch("D"); await awaitsFor(() => _getHighlightedMatches().length > 0, "matches to appear for single character"); await _closeSearch(); - }, 10000); + }, 30000); it("should Escape in search NOT forward to Phoenix", async function () { _openSearch(); - await awaitsFor(() => _isSearchOpen(), "search bar to open"); + await awaitsFor(() => _isSearchOpen(), "search bar to open", 10000); let escapeSent = false; const handler = function (event) { @@ -438,11 +438,11 @@ define(function (require, exports, module) { testWindow.addEventListener("message", handler); _pressKeyInSearch("Escape"); - await awaitsFor(() => !_isSearchOpen(), "search bar to close"); + await awaitsFor(() => !_isSearchOpen(), "search bar to close", 10000); testWindow.removeEventListener("message", handler); expect(escapeSent).toBeFalse(); - }, 10000); + }, 30000); }); } From 2891800593687cd5177642541a4438a368f37f48 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:40 +0530 Subject: [PATCH 08/11] chore: make emmet lorem test tolerant of random punctuation --- src/extensions/default/HTMLCodeHints/unittests.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/extensions/default/HTMLCodeHints/unittests.js b/src/extensions/default/HTMLCodeHints/unittests.js index 73f4afbb06..8275e4c123 100644 --- a/src/extensions/default/HTMLCodeHints/unittests.js +++ b/src/extensions/default/HTMLCodeHints/unittests.js @@ -852,12 +852,14 @@ define(function (require, exports, module) { HTMLCodeHints.emmetHintProvider.insertHint(hints[0]); - let text = testDocument.getText().toLowerCase(); - text = text.split(" ")[0]; - // add an extra space at the end to make sure that content after that is also present - // we cannot check with exact text because it generates randomly - text += ' '; - expect(text).toBe("lorem "); + const fullText = testDocument.getText().toLowerCase(); + expect(fullText.length).toBeGreaterThan("lorem".length); + let text = fullText.split(" ")[0]; + // generated text is random and may punctuate the first word ("lorem, ipsum") + while (text.length && (text[text.length - 1] < "a" || text[text.length - 1] > "z")) { + text = text.slice(0, -1); + } + expect(text).toBe("lorem"); }); it("should show emmet hint when lorem is typed along with some number and expand it", function () { From 213e6033c5d3010636daa56134d06478756b35f5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:40 +0530 Subject: [PATCH 09/11] chore: retry first live preview connection in tests if it gets stuck --- test/spec/LiveDevelopmentMultiBrowser-test.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/spec/LiveDevelopmentMultiBrowser-test.js b/test/spec/LiveDevelopmentMultiBrowser-test.js index 897f979465..7d7688cc54 100644 --- a/test/spec/LiveDevelopmentMultiBrowser-test.js +++ b/test/spec/LiveDevelopmentMultiBrowser-test.js @@ -198,7 +198,14 @@ define(function (require, exports, module) { async function waitsForLiveDevelopmentToOpen() { LiveDevMultiBrowser.open(); - await waitsForLiveDevelopmentFileSwitch(); + try { + await waitsForLiveDevelopmentFileSwitch(); + } catch (e) { + // first connection can wedge while the preview server warms up on slow CI + LiveDevMultiBrowser.close(); + LiveDevMultiBrowser.open(); + await waitsForLiveDevelopmentFileSwitch(); + } } it("should establish a browser connection for an opened html file", async function () { @@ -210,7 +217,7 @@ define(function (require, exports, module) { expect(LiveDevMultiBrowser.status).toBe(LiveDevMultiBrowser.STATUS_ACTIVE); await endPreviewSession(); - }, 30000); + }, 60000); it("should establish a browser connection for an opened html file that has no 'head' tag", async function () { //open a file @@ -220,7 +227,7 @@ define(function (require, exports, module) { expect(LiveDevMultiBrowser.status).toBe(LiveDevMultiBrowser.STATUS_ACTIVE); await endPreviewSession(); - }, 30000); + }, 60000); it("should send all external stylesheets as related docs on start-up", async function () { let liveDoc; From c4e6b5ecab795b87edc1f4456c9b66cd2d99d75d Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 18 Jul 2026 23:28:40 +0530 Subject: [PATCH 10/11] chore: always close dirty files in typescript lsp tests to stop cascades --- .../default/TypeScriptSupport/unittests.js | 170 +++++++++--------- 1 file changed, 89 insertions(+), 81 deletions(-) diff --git a/src/extensions/default/TypeScriptSupport/unittests.js b/src/extensions/default/TypeScriptSupport/unittests.js index 59e2f73184..26a0a7e4c8 100644 --- a/src/extensions/default/TypeScriptSupport/unittests.js +++ b/src/extensions/default/TypeScriptSupport/unittests.js @@ -167,73 +167,79 @@ define(function (require, exports, module) { it("keeps the server in sync across many incremental edits", async function () { await _openInProject("ts/", "incremental.ts"); - const editor = EditorManager.getCurrentFullEditor(); - const doc = editor.document; - await awaitsFor(inspectionClean, "incremental.ts to start clean", 30000); - - // 1) Many single-character inserts - each a separate change record - appending a valid - // statement. Fed only these incremental edits, the server must still parse it as clean. - const insert = "\nlet d: number = 4;"; - for (let i = 0; i < insert.length; i++) { - const line = editor.lineCount() - 1; - doc.replaceRange(insert[i], { line: line, ch: editor.getLine(line).length }); - } - await awaitsFor(inspectionClean, "still clean after many single-char inserts", 30000); - - // 2) A whole-line replacement (non-empty range) that introduces a type error on line 0. - doc.replaceRange('let a: number = "oops";', - { line: 0, ch: 0 }, { line: 0, ch: editor.getLine(0).length }); - await awaitsFor(function () { - return panelText().includes("not assignable"); - }, "type error after a mid-document line replacement", 30000); - - // 3) Fix it with another replacement -> the error must clear. - doc.replaceRange("let a: number = 0;", - { line: 0, ch: 0 }, { line: 0, ch: editor.getLine(0).length }); - await awaitsFor(inspectionClean, "error clears after the fix", 30000); - - // 4) A multi-line deletion (non-empty range, empty text): drop line 1 entirely. Still valid. - doc.replaceRange("", { line: 1, ch: 0 }, { line: 2, ch: 0 }); - await awaitsFor(inspectionClean, "still clean after a deletion", 30000); + try { + const editor = EditorManager.getCurrentFullEditor(); + const doc = editor.document; + await awaitsFor(inspectionClean, "incremental.ts to start clean", 30000); + + // 1) Many single-character inserts - each a separate change record - appending a valid + // statement. Fed only these incremental edits, the server must still parse it as clean. + const insert = "\nlet d: number = 4;"; + for (let i = 0; i < insert.length; i++) { + const line = editor.lineCount() - 1; + doc.replaceRange(insert[i], { line: line, ch: editor.getLine(line).length }); + } + await awaitsFor(inspectionClean, "still clean after many single-char inserts", 30000); - // Discard the in-memory edits so the fixture is pristine for re-runs / other tests. - await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }), - "close incremental.ts"); + // 2) A whole-line replacement (non-empty range) that introduces a type error on line 0. + doc.replaceRange('let a: number = "oops";', + { line: 0, ch: 0 }, { line: 0, ch: editor.getLine(0).length }); + await awaitsFor(function () { + return panelText().includes("not assignable"); + }, "type error after a mid-document line replacement", 30000); + + // 3) Fix it with another replacement -> the error must clear. + doc.replaceRange("let a: number = 0;", + { line: 0, ch: 0 }, { line: 0, ch: editor.getLine(0).length }); + await awaitsFor(inspectionClean, "error clears after the fix", 30000); + + // 4) A multi-line deletion (non-empty range, empty text): drop line 1 entirely. Still valid. + doc.replaceRange("", { line: 1, ch: 0 }, { line: 2, ch: 0 }); + await awaitsFor(inspectionClean, "still clean after a deletion", 30000); + } finally { + // must run even on failure - a dirty file left open hangs every later + // project switch on the save prompt, cascading into unrelated suites + await jsPromise(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true })) + .catch(function (e) { console.error("Failed closing incremental.ts", e); }); + } }, 90000); it("keeps the server in sync for a batched multi-cursor edit", async function () { await _openInProject("ts/", "incremental.ts"); - const editor = EditorManager.getCurrentFullEditor(); - await awaitsFor(inspectionClean, "incremental.ts to start clean", 30000); - - // Two cursors on the SAME line, edited in one operation - the order-sensitive case, since - // the first replacement shifts the columns of the second. CodeMirror applies and reports - // the batch so that replaying the records in array order reproduces the result; this - // confirms our 1:1 change-record -> LSP-range map honours that ordering. - // "let a: number = 0;" -> "let abc: number = \"hello\";" - editor.setSelections([ - { start: { line: 0, ch: 4 }, end: { line: 0, ch: 5 } }, // the identifier `a` - { start: { line: 0, ch: 16 }, end: { line: 0, ch: 17 } } // the literal `0` - ]); - editor._codeMirror.replaceSelections(["abc", '"hello"']); - // Editor side is correct by construction; the assertion that matters is the server agreeing - // - it can only report the error if it received the same text. - expect(editor.getLine(0)).toBe('let abc: number = "hello";'); - await awaitsFor(function () { - return panelText().includes("not assignable"); - }, "type error after the multi-cursor edit", 30000); - - // Revert both cursors in one operation -> the error must clear (sync holds the other way). - editor.setSelections([ - { start: { line: 0, ch: 4 }, end: { line: 0, ch: 7 } }, // `abc` - { start: { line: 0, ch: 18 }, end: { line: 0, ch: 25 } } // `"hello"` - ]); - editor._codeMirror.replaceSelections(["a", "0"]); - expect(editor.getLine(0)).toBe("let a: number = 0;"); - await awaitsFor(inspectionClean, "error clears after reverting the multi-cursor edit", 30000); - - await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }), - "close incremental.ts"); + try { + const editor = EditorManager.getCurrentFullEditor(); + await awaitsFor(inspectionClean, "incremental.ts to start clean", 30000); + + // Two cursors on the SAME line, edited in one operation - the order-sensitive case, since + // the first replacement shifts the columns of the second. CodeMirror applies and reports + // the batch so that replaying the records in array order reproduces the result; this + // confirms our 1:1 change-record -> LSP-range map honours that ordering. + // "let a: number = 0;" -> "let abc: number = \"hello\";" + editor.setSelections([ + { start: { line: 0, ch: 4 }, end: { line: 0, ch: 5 } }, // the identifier `a` + { start: { line: 0, ch: 16 }, end: { line: 0, ch: 17 } } // the literal `0` + ]); + editor._codeMirror.replaceSelections(["abc", '"hello"']); + // Editor side is correct by construction; the assertion that matters is the server agreeing + // - it can only report the error if it received the same text. + expect(editor.getLine(0)).toBe('let abc: number = "hello";'); + await awaitsFor(function () { + return panelText().includes("not assignable"); + }, "type error after the multi-cursor edit", 30000); + + // Revert both cursors in one operation -> the error must clear (sync holds the other way). + editor.setSelections([ + { start: { line: 0, ch: 4 }, end: { line: 0, ch: 7 } }, // `abc` + { start: { line: 0, ch: 18 }, end: { line: 0, ch: 25 } } // `"hello"` + ]); + editor._codeMirror.replaceSelections(["a", "0"]); + expect(editor.getLine(0)).toBe("let a: number = 0;"); + await awaitsFor(inspectionClean, "error clears after reverting the multi-cursor edit", 30000); + } finally { + // see the incremental-edits test above + await jsPromise(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true })) + .catch(function (e) { console.error("Failed closing incremental.ts", e); }); + } }, 90000); // ----- embedded JavaScript in HTML