diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-06 15:35:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-06 15:35:24 +0200 |
| commit | 99b02bf8c389a793df5d5986db05eed7e459f7b1 (patch) | |
| tree | bc4e36cfcd3c9ef9b067beed2eb5b68a75a45aa2 /internal/flamegraph/livehtml_interaction_test.go | |
| parent | 4ff17c30120d657b966f8a55188ba167dc875e64 (diff) | |
refactor: remove web flamegrapher and keep TUI-only
Diffstat (limited to 'internal/flamegraph/livehtml_interaction_test.go')
| -rw-r--r-- | internal/flamegraph/livehtml_interaction_test.go | 615 |
1 files changed, 0 insertions, 615 deletions
diff --git a/internal/flamegraph/livehtml_interaction_test.go b/internal/flamegraph/livehtml_interaction_test.go deleted file mode 100644 index 4c947f5..0000000 --- a/internal/flamegraph/livehtml_interaction_test.go +++ /dev/null @@ -1,615 +0,0 @@ -package flamegraph - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "strings" - "testing" -) - -type zoomSearchStateResult struct { - BeforePath string `json:"beforePath"` - AfterPath string `json:"afterPath"` - DeepPathStable bool `json:"deepPathStable"` - SearchPersisted bool `json:"searchPersisted"` - ZoomedBranchStable bool `json:"zoomedBranchStable"` - NonZoomedHidden bool `json:"nonZoomedHidden"` - NewChildVisible bool `json:"newChildVisible"` - PauseUnpauseKeeps bool `json:"pauseUnpauseKeeps"` -} - -type pauseKeyboardResult struct { - PausedBySpace bool `json:"pausedBySpace"` - NoUpdateWhilePaused bool `json:"noUpdateWhilePaused"` - ZoomSearchWhilePaused bool `json:"zoomSearchWhilePaused"` - UnpauseRendersLatest bool `json:"unpauseRendersLatest"` - RapidToggleStable bool `json:"rapidToggleStable"` - SlashSearchWorks bool `json:"slashSearchWorks"` - EscapeResets bool `json:"escapeResets"` - ButtonMatchesKeyboard bool `json:"buttonMatchesKeyboard"` - TypingIgnoresShortcuts bool `json:"typingIgnoresShortcuts"` -} - -type resetBaselineResult struct { - HotkeyPrevented bool `json:"hotkeyPrevented"` - ShiftHotkeyIgnored bool `json:"shiftHotkeyIgnored"` - HotkeyResetApplied bool `json:"hotkeyResetApplied"` - ButtonResetApplied bool `json:"buttonResetApplied"` - ResetCallsValid bool `json:"resetCallsValid"` -} - -type orderToggleResult struct { - OrderButtonUpdated bool `json:"orderButtonUpdated"` - OrderCallValid bool `json:"orderCallValid"` - OrderSnapshotShown bool `json:"orderSnapshotShown"` -} - -func TestLiveHTMLJSZoomSearchStatePreservedAcrossUpdates(t *testing.T) { - if _, err := exec.LookPath("node"); err != nil { - t.Skip("node not available") - } - - snippet := ` -const fg = liveFlamegraphState; - -const frameA = makeFrame("A", "A", 1, 0, 700); -const frameAChild = makeFrame("Achild", "A\u001fAchild", 2, 0, 400); -const frameB = makeFrame("B", "B", 1, 700, 500); -fg.frames = [frameA, frameAChild, frameB]; -fg.rootWidth = 1200; - -fgZoom(frameA); -const beforePath = fg.zoomRange.path; -prompt = function(){ return "A"; }; -fgSearch(); - -const frameA2 = makeFrame("A", "A", 1, 0, 800); -const frameAChild2 = makeFrame("Achild", "A\u001fAchild", 2, 0, 500); -const frameAnew2 = makeFrame("Anew", "A\u001fAnew", 2, 500, 300); -const frameB2 = makeFrame("B", "B", 1, 800, 400); -fg.frames = [frameA2, frameAChild2, frameAnew2, frameB2]; -fg.rootWidth = 1200; -fgApplyZoom(); -prompt = function(_msg, prev){ return prev || "A"; }; -fgSearch(); - -const afterPath = fg.zoomRange.path; -const searchPersisted = frameA2.querySelector("rect").getAttribute("fill") === fg.matchColor; -const nonZoomedHidden = frameB2.style.display === "none"; -const newChildVisible = frameAnew2.style.display !== "none"; -const zoomedBranchStable = frameA2.style.display !== "none" && frameAChild2.style.display !== "none"; - -const deep1 = makeFrame("A2", "A\u001fA1\u001fA2", 3, 100, 200); -fg.frames = [deep1]; -fg.rootWidth = 1200; -fgZoom(deep1); -const deepPath = fg.zoomRange.path; - -const deep2 = makeFrame("A2", "A\u001fA1\u001fA2", 3, 120, 240); -fg.frames = [deep2]; -fgApplyZoom(); -const deep3 = makeFrame("A2", "A\u001fA1\u001fA2", 3, 140, 260); -fg.frames = [deep3]; -fgApplyZoom(); -const deepPathStable = fg.zoomRange.path === deepPath && deep3.style.display !== "none"; - -fg.pendingData = "{\"n\":\"\",\"v\":0,\"t\":0}"; -fgTogglePause(); -fgTogglePause(); -const pauseUnpauseKeeps = fg.zoomRange.path === deepPath; - -console.log(JSON.stringify({ - beforePath, - afterPath, - deepPathStable, - searchPersisted, - zoomedBranchStable, - nonZoomedHidden, - newChildVisible, - pauseUnpauseKeeps -})); -` - - out := runLiveHTMLNodeSnippet(t, snippet) - var got zoomSearchStateResult - if err := json.Unmarshal([]byte(out), &got); err != nil { - t.Fatalf("decode node result: %v\nraw:\n%s", err, out) - } - - if got.BeforePath != "A" || got.AfterPath != "A" { - t.Fatalf("zoom path changed unexpectedly: before=%q after=%q", got.BeforePath, got.AfterPath) - } - if !got.SearchPersisted { - t.Fatalf("expected search highlight to persist across update") - } - if !got.ZoomedBranchStable { - t.Fatalf("expected zoomed branch to remain visible across update") - } - if !got.NonZoomedHidden { - t.Fatalf("expected non-zoomed branch to be hidden while zoomed") - } - if !got.NewChildVisible { - t.Fatalf("expected newly added child in zoomed branch to remain visible") - } - if !got.DeepPathStable { - t.Fatalf("expected deep zoom path to remain stable across multiple updates") - } - if !got.PauseUnpauseKeeps { - t.Fatalf("expected pause/unpause to preserve zoom state") - } -} - -func TestLiveHTMLJSPauseResumeAndKeyboard(t *testing.T) { - if _, err := exec.LookPath("node"); err != nil { - t.Skip("node not available") - } - - snippet := ` -const fg = liveFlamegraphState; -const keydown = __docListeners["keydown"]; - -function keyEvent(key, code, target) { - let prevented = false; - keydown({ - key: key, - code: code, - target: target || { tagName: "BODY", isContentEditable: false }, - preventDefault: function(){ prevented = true; } - }); - return prevented; -} - -let promptCalls = 0; -prompt = function(_msg, prev) { - promptCalls++; - return prev || "needle"; -}; - -const pausePayload = "{\"n\":\"\",\"v\":0,\"t\":10,\"c\":[{\"n\":\"latest\",\"v\":10,\"t\":10}]}"; -const beforeHTML = fg.svg.innerHTML; -const pausedBySpacePrevented = keyEvent(" ", "Space"); -const pausedBySpace = pausedBySpacePrevented && fg.paused && fg.pauseBtn.textContent === "Resume" && fg.status.textContent.indexOf("PAUSED") === 0; - -fg.eventSource.onmessage({ data: pausePayload }); -const noUpdateWhilePaused = fg.pendingData === pausePayload && fg.svg.innerHTML === beforeHTML; - -const pausedFrame = makeFrame("needle", "needle", 1, 0, 1200); -fg.frames = [pausedFrame]; -fg.rootWidth = 1200; -fgZoom(pausedFrame); -prompt = function(_msg, prev) { - promptCalls++; - return prev || "needle"; -}; -fgSearch(); -const zoomSearchWhilePaused = fg.zoomRange && fg.zoomRange.path === "needle" && - pausedFrame.querySelector("rect").getAttribute("fill") === fg.matchColor; - -const resumedBySpacePrevented = keyEvent(" ", "Space"); -const unpauseRendersLatest = resumedBySpacePrevented && !fg.paused && fg.pendingData === null && - fg.pauseBtn.textContent === "Pause" && fg.svg.innerHTML.indexOf('data-name="latest"') >= 0; - -let rapidToggleStable = true; -for (let i = 0; i < 20; i++) { - try { - fgTogglePause(); - } catch (err) { - rapidToggleStable = false; - } -} -if (fg.paused) { - fgTogglePause(); -} -rapidToggleStable = rapidToggleStable && !fg.paused && fg.pauseBtn.textContent === "Pause"; - -promptCalls = 0; -prompt = function() { - promptCalls++; - return "slash"; -}; -const slashPrevented = keyEvent("/", "Slash"); -const slashSearchWorks = slashPrevented && promptCalls === 1 && fg.searchQuery === "slash"; - -const escFrame = makeFrame("slash", "slash", 1, 0, 1200); -fg.frames = [escFrame]; -fg.rootWidth = 1200; -fgZoom(escFrame); -fgSearch(); -const escapePrevented = keyEvent("Escape", "Escape"); -const escapeResets = escapePrevented && fg.zoomRange === null && - escFrame.querySelector("rect").getAttribute("fill") === escFrame.dataset.baseFill; - -let buttonPromptCalls = 0; -prompt = function() { - buttonPromptCalls++; - return "button"; -}; -document.getElementById("btn-pause").listeners.click(); -const pauseViaButton = fg.paused && fg.pauseBtn.textContent === "Resume"; -document.getElementById("btn-pause").listeners.click(); -const resumeViaButton = !fg.paused && fg.pauseBtn.textContent === "Pause"; -document.getElementById("btn-search").listeners.click(); -const searchViaButton = buttonPromptCalls === 1 && fg.searchQuery === "button"; - -const btnFrame = makeFrame("button", "button", 1, 0, 1200); -fg.frames = [btnFrame]; -fg.rootWidth = 1200; -fgZoom(btnFrame); -document.getElementById("btn-reset-search").listeners.click(); -document.getElementById("btn-reset-zoom").listeners.click(); -const resetViaButton = fg.zoomRange === null && - btnFrame.querySelector("rect").getAttribute("fill") === btnFrame.dataset.baseFill; -const buttonMatchesKeyboard = pauseViaButton && resumeViaButton && searchViaButton && resetViaButton; - -const typingTarget = { tagName: "INPUT", isContentEditable: false }; -fg.searchQuery = "typed"; -fg.zoomRange = { path: "typed", x: 0, w: 1200, depth: 1 }; -promptCalls = 0; -const typingSpacePrevented = keyEvent(" ", "Space", typingTarget); -const typingSlashPrevented = keyEvent("/", "Slash", typingTarget); -const typingEscapePrevented = keyEvent("Escape", "Escape", typingTarget); -const typingIgnoresShortcuts = !typingSpacePrevented && !typingSlashPrevented && !typingEscapePrevented && - !fg.paused && promptCalls === 0 && fg.zoomRange !== null && fg.searchQuery === "typed"; - -console.log(JSON.stringify({ - pausedBySpace, - noUpdateWhilePaused, - zoomSearchWhilePaused, - unpauseRendersLatest, - rapidToggleStable, - slashSearchWorks, - escapeResets, - buttonMatchesKeyboard, - typingIgnoresShortcuts -})); -` - - out := runLiveHTMLNodeSnippet(t, snippet) - var got pauseKeyboardResult - if err := json.Unmarshal([]byte(out), &got); err != nil { - t.Fatalf("decode node result: %v\nraw:\n%s", err, out) - } - - if !got.PausedBySpace { - t.Fatalf("expected Space shortcut to pause and update status/button state") - } - if !got.NoUpdateWhilePaused { - t.Fatalf("expected stream updates to queue while paused without rerendering") - } - if !got.ZoomSearchWhilePaused { - t.Fatalf("expected zoom and search to work while paused") - } - if !got.UnpauseRendersLatest { - t.Fatalf("expected unpause to render latest queued update immediately") - } - if !got.RapidToggleStable { - t.Fatalf("expected rapid pause/unpause toggles to remain stable") - } - if !got.SlashSearchWorks { - t.Fatalf("expected '/' shortcut to open search flow") - } - if !got.EscapeResets { - t.Fatalf("expected Escape shortcut to reset zoom/search highlighting") - } - if !got.ButtonMatchesKeyboard { - t.Fatalf("expected button actions to match keyboard behavior") - } - if !got.TypingIgnoresShortcuts { - t.Fatalf("expected keyboard shortcuts to be ignored while typing in an input") - } -} - -func TestLiveHTMLJSResetBaselineHotkeyAndButton(t *testing.T) { - if _, err := exec.LookPath("node"); err != nil { - t.Skip("node not available") - } - - snippet := ` -const fg = liveFlamegraphState; -const keydown = __docListeners["keydown"]; - -function keyEvent(key, code, target) { - let prevented = false; - keydown({ - key: key, - code: code, - target: target || { tagName: "BODY", isContentEditable: false }, - preventDefault: function(){ prevented = true; } - }); - return prevented; -} - -const frame = makeFrame("needle", "needle", 1, 0, 1200); -fg.frames = [frame]; -fg.rootWidth = 1200; -fgZoom(frame); -prompt = function(){ return "needle"; }; -fgSearch(); - -const resetPayload = "{\"n\":\"\",\"v\":0,\"t\":0}"; -const resetCalls = []; -fetch = function(url, opts) { - resetCalls.push({ - url: url, - method: (opts && opts.method) || "GET" - }); - return Promise.resolve({ - ok: true, - text: function() { return Promise.resolve(resetPayload); } - }); -}; - -const hotkeyPrevented = keyEvent("r", "KeyR"); -const shiftHotkeyPrevented = keyEvent("R", "KeyR"); -const shiftHotkeyIgnored = !shiftHotkeyPrevented && resetCalls.length === 1; - -setTimeout(function() { - const hotkeyResetApplied = fg.zoomRange === null && fg.searchQuery === "" && fg.frames.length === 0; - - const frame2 = makeFrame("again", "again", 1, 0, 1200); - fg.frames = [frame2]; - fg.rootWidth = 1200; - fgZoom(frame2); - fg.searchQuery = "again"; - document.getElementById("btn-reset-baseline").listeners.click(); - - setTimeout(function() { - const buttonResetApplied = fg.zoomRange === null && fg.searchQuery === "" && fg.frames.length === 0; - const resetCallsValid = resetCalls.length === 2 && - resetCalls[0].url === "/reset" && resetCalls[0].method === "POST" && - resetCalls[1].url === "/reset" && resetCalls[1].method === "POST"; - - console.log(JSON.stringify({ - hotkeyPrevented, - shiftHotkeyIgnored, - hotkeyResetApplied, - buttonResetApplied, - resetCallsValid - })); - }, 0); -}, 0); -` - - out := runLiveHTMLNodeSnippet(t, snippet) - var got resetBaselineResult - if err := json.Unmarshal([]byte(out), &got); err != nil { - t.Fatalf("decode node result: %v\nraw:\n%s", err, out) - } - - if !got.HotkeyPrevented { - t.Fatalf("expected reset hotkey to prevent default browser handling") - } - if !got.ShiftHotkeyIgnored { - t.Fatalf("expected uppercase 'R' to be ignored for baseline reset") - } - if !got.HotkeyResetApplied { - t.Fatalf("expected 'r' hotkey to reset baseline and clear UI state") - } - if !got.ButtonResetApplied { - t.Fatalf("expected Reset Baseline button to clear UI state") - } - if !got.ResetCallsValid { - t.Fatalf("expected reset interactions to POST /reset") - } -} - -func TestLiveHTMLJSOrderToggle(t *testing.T) { - if _, err := exec.LookPath("node"); err != nil { - t.Skip("node not available") - } - - snippet := ` -const fg = liveFlamegraphState; -const orderCalls = []; -fetch = function(url, opts) { - orderCalls.push({ - url: url, - method: (opts && opts.method) || "GET", - body: (opts && opts.body) || "" - }); - return Promise.resolve({ - ok: true, - json: function() { - return Promise.resolve({ - fields: ["path", "tracepoint", "comm"], - snapshot: { - n: "", - v: 0, - t: 1, - c: [{ n: "/tmp", v: 1, t: 1 }] - } - }); - } - }); -}; - -document.getElementById("btn-toggle-order").listeners.click(); - -setTimeout(function() { - const orderButtonUpdated = document.getElementById("btn-toggle-order").textContent.indexOf("path > tracepoint > comm") >= 0; - const orderSnapshotShown = fg.svg.innerHTML.indexOf('data-name="/tmp"') >= 0; - const req = orderCalls[0] || {}; - let bodyFields = []; - try { - bodyFields = JSON.parse(req.body || "{}").fields || []; - } catch (err) { - bodyFields = []; - } - const orderCallValid = orderCalls.length === 1 && - req.url === "/order" && - req.method === "POST" && - JSON.stringify(bodyFields) === JSON.stringify(["path", "tracepoint", "comm"]); - - console.log(JSON.stringify({ - orderButtonUpdated, - orderCallValid, - orderSnapshotShown - })); -}, 0); -` - - out := runLiveHTMLNodeSnippet(t, snippet) - var got orderToggleResult - if err := json.Unmarshal([]byte(out), &got); err != nil { - t.Fatalf("decode node result: %v\nraw:\n%s", err, out) - } - - if !got.OrderButtonUpdated { - t.Fatalf("expected toggle button label to update to next order") - } - if !got.OrderCallValid { - t.Fatalf("expected toggle to POST /order with next preset fields") - } - if !got.OrderSnapshotShown { - t.Fatalf("expected returned order snapshot to render immediately") - } -} - -func runLiveHTMLNodeSnippet(t *testing.T, snippet string) string { - t.Helper() - - script := extractLiveHTMLScript(t) - harness := fmt.Sprintf(` -const vm = require("vm"); -const liveScript = %q; - -function makeElement(id) { - return { - id, - textContent: "", - innerHTML: "", - style: {}, - dataset: {}, - attrs: {}, - classList: { toggle: function(){}, add: function(){}, remove: function(){} }, - listeners: {}, - addEventListener: function(event, cb) { this.listeners[event] = cb; }, - getBoundingClientRect: function() { return { height: id === "controls" ? 56 : 0 }; }, - setAttribute: function(k, v) { this.attrs[k] = String(v); }, - getAttribute: function(k) { return this.attrs[k] || ""; }, - querySelectorAll: function() { return []; }, - querySelector: function() { return null; } - }; -} - -function makeRect(fill) { - return { - attrs: { fill: fill || "" }, - dataset: {}, - style: {}, - setAttribute: function(k, v) { this.attrs[k] = String(v); }, - getAttribute: function(k) { return this.attrs[k] || ""; } - }; -} - -function makeText(name) { - return { - textContent: name || "", - dataset: { full: name || "", hidden: "0", ox: "0" }, - style: {}, - setAttribute: function(k, v) { - this[k] = String(v); - }, - getAttribute: function(k) { - return this[k] || ""; - } - }; -} - -function makeFrame(name, path, depth, x, w) { - const rect = makeRect("rgb(1,2,3)"); - rect.dataset.ox = String(x); - rect.dataset.ow = String(w); - rect.setAttribute("x", String(x)); - rect.setAttribute("width", String(w)); - - const text = makeText(name); - text.dataset.ox = String(x + 3); - text.setAttribute("x", String(x + 3)); - - const title = { textContent: name + " title" }; - return { - dataset: { - name: name, - path: path, - depth: String(depth), - x: String(x), - w: String(w), - ox: String(x), - ow: String(w), - baseFill: "rgb(1,2,3)" - }, - style: {}, - listeners: {}, - addEventListener: function(event, cb) { this.listeners[event] = cb; }, - querySelector: function(selector) { - if (selector === "rect") return rect; - if (selector === "text") return text; - if (selector === "title") return title; - return null; - }, - querySelectorAll: function() { return []; }, - }; -} - -const elements = {}; -["controls", "flamegraph", "status", "btn-pause", "btn-search", "btn-reset-search", "btn-undo-zoom", "btn-reset-zoom", "btn-reset-baseline", "btn-toggle-order"].forEach((id) => { - elements[id] = makeElement(id); -}); -elements["body"] = makeElement("body"); - -const docListeners = {}; -global.document = { - body: elements["body"], - getElementById: function(id) { - if (!elements[id]) elements[id] = makeElement(id); - return elements[id]; - }, - addEventListener: function(event, cb) { docListeners[event] = cb; }, -}; -global.window = global; -global.prompt = function(){ return ""; }; -global.fetch = function() { - return Promise.resolve({ - ok: true, - json: function() { return Promise.resolve({ fields: ["comm", "tracepoint", "path"], snapshot: { n: "", v: 0, t: 0 } }); }, - text: function() { return Promise.resolve("{\"n\":\"\",\"v\":0,\"t\":0}"); } - }); -}; -global.requestAnimationFrame = function(cb){ cb(); }; -global.EventSource = function() { - this.onmessage = null; - this.onerror = null; -}; -window.addEventListener = function(){}; - -vm.runInThisContext(liveScript); - -global.makeFrame = makeFrame; -global.__docListeners = docListeners; - -%s -`, script, snippet) - - tmp, err := os.CreateTemp("", "livehtml-node-snippet-*.cjs") - if err != nil { - t.Fatalf("create temp script: %v", err) - } - defer os.Remove(tmp.Name()) - - if _, err := tmp.WriteString(harness); err != nil { - _ = tmp.Close() - t.Fatalf("write temp script: %v", err) - } - if err := tmp.Close(); err != nil { - t.Fatalf("close temp script: %v", err) - } - - out, err := exec.Command("node", tmp.Name()).CombinedOutput() - if err != nil { - t.Fatalf("node snippet failed: %v\n%s", err, string(out)) - } - return strings.TrimSpace(string(out)) -} |
