From 58d6a6de07e336a4480cf2ceafe8a9793104b22a Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 28 Mar 2026 21:02:46 +0000 Subject: [PATCH] sx-http: fix AJAX fragment extraction + proper back button test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AJAX fragment: extract #main-panel with matching close tag (depth tracking) instead of taking everything to end of file. Prevents shell closing tags from breaking the DOM swap. Back button test: verifies content actually changes — checks for "Geography" and "Rendering Pipeline" after going back, not just that body has >100 chars. Tests forward nav content change too. 7/7 navigation tests pass including back button content verification. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 30 ++++++++++++++++++- tests/playwright/navigation.spec.js | 45 +++++++++++++---------------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index aa0386ce..0edc30a5 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -2117,7 +2117,35 @@ let http_mode port = with _ -> None in match panel_start with | Some start -> - let fragment = String.sub html start (String.length html - start) in + (* Find the tag name (section, div, etc.) *) + let tag_end = ref (start + 1) in + while !tag_end < String.length html && html.[!tag_end] <> ' ' && html.[!tag_end] <> '>' do + tag_end := !tag_end + 1 + done; + let tag = String.sub html (start + 1) (!tag_end - start - 1) in + (* Find matching close tag by counting depth *) + let open_tag = "<" ^ tag in + let close_tag = "" in + let depth = ref 1 in + let pos = ref (!tag_end) in + let found_end = ref (String.length html) in + while !depth > 0 && !pos < String.length html - String.length close_tag do + if String.sub html !pos (String.length close_tag) = close_tag then begin + depth := !depth - 1; + if !depth = 0 then + found_end := !pos + String.length close_tag + else + pos := !pos + 1 + end else if !pos + String.length open_tag < String.length html + && String.sub html !pos (String.length open_tag) = open_tag + && (html.[!pos + String.length open_tag] = ' ' + || html.[!pos + String.length open_tag] = '>') then begin + depth := !depth + 1; + pos := !pos + 1 + end else + pos := !pos + 1 + done; + let fragment = String.sub html start (!found_end - start) in http_response ~content_type:"text/html; charset=utf-8" fragment | None -> http_response html end else begin diff --git a/tests/playwright/navigation.spec.js b/tests/playwright/navigation.spec.js index b8c1c299..2d1d6583 100644 --- a/tests/playwright/navigation.spec.js +++ b/tests/playwright/navigation.spec.js @@ -93,35 +93,30 @@ test.describe('Client-side Navigation', () => { await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); - // Note the initial URL and content - const initialUrl = page.url(); - const initialText = await page.locator('h1, h2').first().textContent(); + // Get initial content + const initialContent = await page.locator('#main-panel').textContent(); + expect(initialContent).toContain('Geography'); - // Navigate forward - const cekLink = page.locator('a:has-text("CEK Machine")'); - if (await cekLink.count() > 0) { - await cekLink.first().click(); - await page.waitForTimeout(3000); + // Navigate forward to Hypermedia + await page.click('a[href*="geography.(hypermedia"]:not([href*="example"])'); + await page.waitForTimeout(3000); - // Should be on a different page - const navUrl = page.url(); - expect(navUrl).not.toBe(initialUrl); + // Verify we navigated — content should be different + const navContent = await page.locator('#main-panel').textContent(); + expect(navContent).toContain('Hypermedia'); + expect(page.url()).toContain('hypermedia'); - // Go back - await page.goBack(); - await page.waitForTimeout(3000); + // Go back + await page.goBack(); + await page.waitForTimeout(3000); - // URL should return to original - expect(page.url()).toContain('geography'); - - // Content should show Geography page content (not blank, not broken) - const bodyText = await page.textContent('body'); - expect(bodyText.length).toBeGreaterThan(100); - - // Should not show raw SX - const mainContent = await page.locator('#main-panel, main').first().textContent(); - expect(mainContent).not.toContain('(~shared:layout'); - } + // Content should return to Geography — not stay on Hypermedia + const backContent = await page.locator('#main-panel').textContent(); + expect(backContent).toContain('Geography'); + expect(backContent).toContain('Rendering Pipeline'); + // Should NOT still show Hypermedia content + expect(page.url()).toContain('geography'); + expect(page.url()).not.toContain('hypermedia'); }); test('back button preserves layout (no side-by-side)', async ({ page }) => {