GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix: stabilize desktop tests and asynchronous editor updates · phcode-dev/phoenix@e00a5a5

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • .codex
    • config.toml
  • serve-proxy.js
  • src-node
    • index.js
  • src
    • JSUtils
      • ScopeManager.js
    • editor
      • MediaViewer.js
    • extensions/default
      • DocCommentHints
        • integration-tests.js
      • HTMLCodeHints
        • integ-tests.js
      • QuickView
        • unittests.js
      • TypeScriptSupport
        • unittests.js
    • language
      • CodeInspection.js
    • node-loader.js
    • project
      • FileSyncManager.js
      • ProjectModel.js
  • test
    • UnitTestSuite.js
    • spec
      • CodeInspection-fix-integ-test.js
      • Document-integ-test.js
      • Extn-ESLint-integ-test.js
      • Extn-Git-integ-test.js
      • LiveDevelopmentMultiBrowser-test.js
      • ProjectModel-test.js
      • ScopeManager-integ-test.js
      • SpecRunnerUtils.js
      • md-editor-integ-test.js
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.codex/config.toml‎

Copy file name to clipboardExpand all lines: .codex/config.toml
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ import(pathToFileURL(path.join(root, entry)).href).catch((error) => {
2727
env_vars = ["PHOENIX_DESKTOP_PATH", "PHOENIX_MCP_WS_PORT"]
2828
# Live-preview connection and execution can take longer than the default 60s.
2929
tool_timeout_sec = 120
30+
31+
[mcp_servers.phoenix-builder.tools.get_phoenix_status]
32+
approval_mode = "approve"
Collapse file

‎serve-proxy.js‎

Copy file name to clipboardExpand all lines: serve-proxy.js
+20-2Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,26 @@ const server = http.createServer((req, res) => {
309309
}
310310
}
311311

312-
// Serve static files
313-
let filePath = path.join(config.root, parsedUrl.pathname);
312+
// Serve static files. url.parse leaves the pathname percent encoded, so a
313+
// directory or file whose name contains a space was looked up on disk as
314+
// "sub%20dir" and answered 404 even though it was right there. Decode before
315+
// touching the filesystem - the traversal check below still runs on the
316+
// result, which is what stops an encoded "%2e%2e" from buying anything.
317+
let decodedPathname;
318+
try {
319+
decodedPathname = decodeURIComponent(parsedUrl.pathname);
320+
} catch (e) {
321+
// A malformed escape is a bad request, not a missing file.
322+
res.writeHead(400, { 'Content-Type': 'text/plain' });
323+
res.end('Bad Request');
324+
return;
325+
}
326+
if (decodedPathname.indexOf('\0') !== -1) {
327+
res.writeHead(400, { 'Content-Type': 'text/plain' });
328+
res.end('Bad Request');
329+
return;
330+
}
331+
let filePath = path.join(config.root, decodedPathname);
314332

315333
// Security: prevent directory traversal
316334
const normalizedPath = path.normalize(filePath);
Collapse file

‎src-node/index.js‎

Copy file name to clipboardExpand all lines: src-node/index.js
+51-6Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,17 +169,62 @@ rl.on('close', () => {
169169
process.exit(1);
170170
});
171171

172+
// Opt-in watchdog for test processes; disabled by default. node-loader.js arms
173+
// it for Phoenix.isTestWindow, and SpecRunnerUtils refreshes the runner and its
174+
// test window. Normal editor sessions never send setIdleExit.
175+
//
176+
// Tauri owns this process and holds stdin open after the spawning page goes away.
177+
// A terminate command sent during beforeunload can be lost, leaving Node and its
178+
// LSP servers running. Test runs accept timeout-based cleanup for these orphans.
179+
// Keep this disabled in normal editor sessions: sleep, page suspension or long
180+
// pauses can stop heartbeats even though the page still exists. Expiry can then
181+
// trigger a Node crash dialog after wake. Earlier heartbeat/socket orphan checks
182+
// were removed for sleep-related crashes in a1e660cb5 and ba5800d93.
183+
let _idleExitMs = 0;
184+
let _idleExitTimer = null;
185+
186+
function _shutdown(reason) {
187+
lmdb.dumpDBToFileAndCloseDB()
188+
.catch(console.error)
189+
.finally(() => {
190+
console.log(reason);
191+
process.exit(0);
192+
});
193+
}
194+
195+
function _refreshIdleExit() {
196+
if (!_idleExitMs) {
197+
return;
198+
}
199+
if (_idleExitTimer) {
200+
clearTimeout(_idleExitTimer);
201+
}
202+
_idleExitTimer = setTimeout(() => {
203+
_shutdown(`No command for ${_idleExitMs}ms, the page that spawned us is gone.`);
204+
}, _idleExitMs);
205+
// never let this timer alone hold the process up
206+
if (_idleExitTimer.unref) {
207+
_idleExitTimer.unref();
208+
}
209+
}
210+
172211
function processCommand(line) {
173212
try{
174213
let jsonCmd = JSON.parse(line);
214+
// any command at all is proof the page is still there
215+
_refreshIdleExit();
175216
switch (jsonCmd.commandCode) {
217+
case "setIdleExit":
218+
_idleExitMs = Number(jsonCmd.commandData) || 0;
219+
if (!_idleExitMs && _idleExitTimer) {
220+
clearTimeout(_idleExitTimer);
221+
_idleExitTimer = null;
222+
}
223+
_refreshIdleExit();
224+
_sendResponse(_idleExitMs, jsonCmd.commandID);
225+
return;
176226
case "terminate":
177-
lmdb.dumpDBToFileAndCloseDB()
178-
.catch(console.error)
179-
.finally(()=>{
180-
console.log("Node terminated by phcode.");
181-
process.exit(0);
182-
});
227+
_shutdown("Node terminated by phcode.");
183228
return;
184229
case "ping": _sendResponse("pong", jsonCmd.commandID); return;
185230
case "setDebugMode":
Collapse file

‎src/JSUtils/ScopeManager.js‎

Copy file name to clipboardExpand all lines: src/JSUtils/ScopeManager.js
+26-10Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,22 @@ define(function (require, exports, module) {
110110

111111
/**
112112
* Init preferences from a file in the project root or builtin
113-
* defaults if no file is found;
113+
* defaults if no file is found. Ignore callbacks from a previous project.
114114
* @private
115115
* @param {string=} projectRootPath - new project root path. Only needed
116116
* for unit tests.
117117
*/
118118
function initPreferences(projectRootPath) {
119119

120-
// Reject the old preferences if they have not completed.
121-
if (deferredPreferences && deferredPreferences.state() === "pending") {
122-
deferredPreferences.reject();
120+
const previousRequest = deferredPreferences;
121+
const request = $.Deferred();
122+
deferredPreferences = request;
123+
// Cancelling the old request can release queued editor changes. Those
124+
// changes must already see the new project's preference request.
125+
if (previousRequest && previousRequest.state() === "pending") {
126+
previousRequest.reject();
123127
}
124-
125-
deferredPreferences = $.Deferred();
126-
var pr = ProjectManager.getProjectRoot();
128+
const pr = ProjectManager.getProjectRoot();
127129

128130
// Open preferences relative to the project root
129131
// Normally there is a project root, but for unit tests we need to
@@ -133,15 +135,22 @@ define(function (require, exports, module) {
133135
} else if (!projectRootPath) {
134136
console.log("initPreferences: projectRootPath has no value. Using Defaults.");
135137
preferences = new Preferences();
138+
request.resolve();
136139
return;
137140
}
138141

139142
var path = projectRootPath + Preferences.FILE_NAME;
140143

141144
preferences = new Preferences();
142145
FileSystem.resolve(path, function (err, file) {
146+
if (deferredPreferences !== request) {
147+
return;
148+
}
143149
if (!err) {
144150
FileUtils.readAsText(file).done(function (text) {
151+
if (deferredPreferences !== request) {
152+
return;
153+
}
145154
var configObj = null;
146155
try {
147156
configObj = JSON.parse(text);
@@ -154,13 +163,16 @@ define(function (require, exports, module) {
154163
}
155164
}
156165
preferences = new Preferences(configObj);
157-
deferredPreferences.resolve();
166+
request.resolve();
158167
}).fail(function (error) {
168+
if (deferredPreferences !== request) {
169+
return;
170+
}
159171
preferences = new Preferences();
160-
deferredPreferences.resolve();
172+
request.resolve();
161173
});
162174
} else {
163-
deferredPreferences.resolve();
175+
request.resolve();
164176
}
165177
});
166178
}
@@ -1299,6 +1311,10 @@ define(function (require, exports, module) {
12991311
});
13001312
});
13011313
});
1314+
}).fail(function () {
1315+
// A project switch cancelled these preferences. Release this
1316+
// initialization so the next editor change can initialize Tern.
1317+
addFilesDeferred.resolveWith(null);
13021318
});
13031319
}
13041320

Collapse file

‎src/editor/MediaViewer.js‎

Copy file name to clipboardExpand all lines: src/editor/MediaViewer.js
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,14 @@ define(function (require, exports, module) {
8383
*/
8484
function _mediaStreamURL(file) {
8585
const platformPath = Phoenix.fs.getTauriPlatformPath(file.fullPath);
86-
return window.PhNodeEngine.mediaURL +
86+
// The file name rides in the path, before the query that actually names the
87+
// file, purely so the URL ends in the right extension. WebKit picks its
88+
// media engine for WebM off the URL extension rather than the Content-Type,
89+
// so without this a .webm plays as "format not supported" on the Mac
90+
// desktop app while the very same bytes play from a blob. mp4 is content
91+
// sniffed and so never showed the problem. Node ignores this segment - it
92+
// matches the route by prefix and reads platformPath from the query.
93+
return window.PhNodeEngine.mediaURL + "/" + encodeURIComponent(file.name) +
8794
"?platformPath=" + encodeURIComponent(platformPath);
8895
}
8996

Collapse file

‎src/extensions/default/DocCommentHints/integration-tests.js‎

Copy file name to clipboardExpand all lines: src/extensions/default/DocCommentHints/integration-tests.js
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ define(function (require, exports, module) {
8181
await awaitsForDone(SpecRunnerUtils.openProjectFiles([tc.file]), "open " + tc.file);
8282
const editor = EditorManager.getActiveEditor();
8383
editor.setCursorPos(tc.line, tc.ch);
84-
CommandManager.execute(Commands.SHOW_CODE_HINTS);
84+
// Hand the editor to the command. Left to itself it asks for the
85+
// focused editor, and an editor only counts as focused once the
86+
// browser has dispatched focus to it - which it does not do while the
87+
// test window is behind another app. Then no session starts, no popup
88+
// opens, and every case here times out together.
89+
CommandManager.execute(Commands.SHOW_CODE_HINTS, editor);
8590

8691
// 1) the code-hints popup appears with our hint
8792
await awaitsFor(function () { return $docHint().length > 0; },
Collapse file

‎src/extensions/default/HTMLCodeHints/integ-tests.js‎

Copy file name to clipboardExpand all lines: src/extensions/default/HTMLCodeHints/integ-tests.js
+35-12Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,23 @@ define(function (require, exports, module) {
8888
const selected = ProjectManager.getSelectedItem();
8989
expect(selected.fullPath).toBe(testPath + "/jumpToDef.html");
9090

91-
let editor = EditorManager.getActiveEditor();
92-
editor.setCursorPos({ line: 5, ch: 6 });
91+
const hostEditor = EditorManager.getActiveEditor();
92+
hostEditor.setCursorPos({ line: 5, ch: 6 });
9393

9494
await awaitsForDone(CommandManager.execute(Commands.NAVIGATE_JUMPTO_DEFINITION),
9595
"jump to def on div");
9696

97-
editor = EditorManager.getFocusedInlineEditor();
98-
expect(editor.document.file.fullPath.endsWith("LiveDevelopment-MultiBrowser-test-files/simpleShared.css"))
99-
.toBeTrue();
97+
// Ask the host for its inline editors rather than for the focused one:
98+
// an inline editor only holds focus while the window does, so the old
99+
// check came back null whenever the test window was not the OS front
100+
// window, without the inline editor being any less open.
101+
let inlineEditor;
102+
await awaitsFor(function () {
103+
inlineEditor = EditorManager.getInlineEditors(hostEditor)[0];
104+
return !!inlineEditor;
105+
}, "inline editor to open on the definition");
106+
expect(inlineEditor.document.file.fullPath
107+
.endsWith("LiveDevelopment-MultiBrowser-test-files/simpleShared.css")).toBeTrue();
100108
await closeSession();
101109
});
102110

@@ -105,14 +113,22 @@ define(function (require, exports, module) {
105113
const selected = ProjectManager.getSelectedItem();
106114
expect(selected.fullPath).toBe(testPath + "/jumpToDef.html");
107115

108-
let editor = EditorManager.getActiveEditor();
109-
editor.setCursorPos({ line: 6, ch: 23 });
116+
const hostEditor = EditorManager.getActiveEditor();
117+
hostEditor.setCursorPos({ line: 6, ch: 23 });
110118

111119
await awaitsForDone(CommandManager.execute(Commands.NAVIGATE_JUMPTO_DEFINITION),
112120
"jump to def on div");
113121

114-
editor = EditorManager.getFocusedInlineEditor();
115-
expect(editor.document.file.fullPath.endsWith("LiveDevelopment-MultiBrowser-test-files/sub/test.css"))
122+
// Ask the host for its inline editors rather than for the focused one:
123+
// an inline editor only holds focus while the window does, so the old
124+
// check came back null whenever the test window was not the OS front
125+
// window, without the inline editor being any less open.
126+
let inlineEditor;
127+
await awaitsFor(function () {
128+
inlineEditor = EditorManager.getInlineEditors(hostEditor)[0];
129+
return !!inlineEditor;
130+
}, "inline editor to open on the definition");
131+
expect(inlineEditor.document.file.fullPath.endsWith("LiveDevelopment-MultiBrowser-test-files/sub/test.css"))
116132
.toBeTrue();
117133
await closeSession();
118134
});
@@ -202,9 +218,11 @@ define(function (require, exports, module) {
202218
let editor = EditorManager.getActiveEditor();
203219
editor.setCursorPos(cursor);
204220

205-
await awaitsForDone(CommandManager.execute(Commands.SHOW_CODE_HINTS),
206-
"show code hints");
207-
221+
// The index has to hold the classes before hints are asked for, not after.
222+
// Showing hints is a one-shot request: made while the index is still
223+
// catching up, the provider has no class hints to offer, no menu opens,
224+
// and nothing asks again once the index is ready - so this passed only
225+
// when the index happened to be warm, and timed out under a full run.
208226
await awaitsFor(async function () {
209227
for(let hint of expectedSomeHintsArray){
210228
const allSelectors = await CSSUtils.getAllCssSelectorsInProject();
@@ -215,6 +233,11 @@ define(function (require, exports, module) {
215233
return true;
216234
}, "CSSUtils project selectors to be updated");
217235

236+
// With the editor passed in, the session does not depend on it holding
237+
// focus, which it does not while the test window is in the background.
238+
await awaitsForDone(CommandManager.execute(Commands.SHOW_CODE_HINTS, editor),
239+
"show code hints");
240+
218241
await awaitsFor(function () {
219242
return $(".codehint-menu").is(":visible");
220243
}, "codehints to be shown");
Collapse file

‎src/extensions/default/QuickView/unittests.js‎

Copy file name to clipboardExpand all lines: src/extensions/default/QuickView/unittests.js
+11-1Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,17 @@ define(function (require, exports, module) {
197197
let quickViewSwatch = popoverInfo.content.find("#quick-view-color-swatch");
198198
expect(quickViewSwatch.attr("data-for-test")).toBe(color);
199199
quickViewSwatch.click();
200-
expect(EditorManager.getFocusedInlineWidget()._color).toBe(color);
200+
// Find the color editor among the host's inline widgets rather than as
201+
// the focused one: an inline widget only holds focus while the window
202+
// does, so the old check came back null when the test window was in
203+
// the background, with the color editor open all the same.
204+
let colorWidget;
205+
await awaitsFor(function () {
206+
colorWidget = EditorManager.getActiveEditor().getInlineWidgets()
207+
.find(function (widget) { return widget._color; });
208+
return !!colorWidget;
209+
}, "inline color editor to open");
210+
expect(colorWidget._color).toBe(color);
201211
});
202212

203213
describe("JavaScript file", function () {
Collapse file

‎src/extensions/default/TypeScriptSupport/unittests.js‎

Copy file name to clipboardExpand all lines: src/extensions/default/TypeScriptSupport/unittests.js
+21-12Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -431,19 +431,21 @@ define(function (require, exports, module) {
431431
ExtensionLoader.getRequireContextForExtension("JavaScriptCodeHints")(["main"], resolve, reject);
432432
});
433433
let hintText = "";
434-
await awaitsFor(function () {
434+
await awaitsFor(async function () {
435435
if (!jsCodeHints.jsHintProvider.hasHints(editor, null)) {
436436
return false; // Tern session/worker may still be starting up
437437
}
438-
const response = jsCodeHints.jsHintProvider.getHints(null);
439-
if (!response || typeof response.done !== "function") {
440-
return hintText.indexOf("push") !== -1; // sync response already captured below
438+
// The provider returns a Deferred for fresh hints and an object for
439+
// cached hints. Await either result before checking this request.
440+
let result;
441+
try {
442+
result = await jsCodeHints.jsHintProvider.getHints(null);
443+
} catch (err) {
444+
return false; // A project/session change can cancel a pending request.
441445
}
442-
response.done(function (result) {
443-
hintText = ((result && result.hints) || []).map(function (h) {
444-
return $(h).text();
445-
}).join("|");
446-
});
446+
hintText = ((result && result.hints) || []).map(function (h) {
447+
return $(h).text();
448+
}).join("|");
447449
return hintText.indexOf("push") !== -1;
448450
}, "Tern Array-member completions at arr. inside the <script>", 30000, 500);
449451
expect(hintText).toContain("push");
@@ -917,10 +919,17 @@ define(function (require, exports, module) {
917919
await awaitsFor(function () {
918920
return panelText().includes("never read") && panelText().includes("deprecated");
919921
}, "unused + deprecated hint rows in the problems panel", 30000);
920-
// the marked text carries the tag styles on top of the info squiggle
922+
// the marked text carries the tag styles on top of the info squiggle.
923+
// Read the marks off the editor rather than looking for their spans in
924+
// the page: CodeMirror only renders spans for lines it has painted, and
925+
// late in a full run it had not, so this timed out with the marks - and
926+
// the panel rows the waits above had already seen - all in place.
921927
await awaitsFor(function () {
922-
return $(".editor-text-fragment-unnecessary").length > 0 &&
923-
$(".editor-text-fragment-deprecated").length > 0;
928+
const classes = editor.findMarks({line: 0, ch: 0}, {line: editor.lineCount(), ch: 0})
929+
.map(function (mark) { return mark.className || ""; })
930+
.join(" ");
931+
return classes.includes("editor-text-fragment-unnecessary") &&
932+
classes.includes("editor-text-fragment-deprecated");
924933
}, "faded + strikethrough text marks in the editor", 30000);
925934
}, 90000);
926935

0 commit comments

Comments
0 (0)
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.