From f66195ce18636247ae4c89961b1d6ac7a3b2bf45 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 1 Apr 2026 19:41:32 +0000 Subject: [PATCH] Fix delete-row handler: nested slug extraction in OCaml dispatch The slug extractor after (api. scanned for the first ) but for nested URLs like (api.(delete.1)) it got "(delete.1" instead of "delete". Now handles nested parens: extracts handler name and injects path params into query string. Also strengthened the Playwright test to accept confirm dialogs and assert strict row count decrease. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 25 ++++++++++++++++++-- tests/playwright/hypermedia-handlers.spec.js | 22 ++++++++++------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 552f3f0a..1b8f7492 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -2658,8 +2658,29 @@ let http_mode port = if i + 5 > String.length s then "" else if String.sub s i 5 = "(api." then let start = i + 5 in - let end_ = try String.index_from s start ')' with Not_found -> String.length s in - String.sub s start (end_ - start) + if start < String.length s && s.[start] = '(' then + (* Nested: (api.(delete.1)) — extract "delete" as slug, + inject path param into query string *) + let inner = start + 1 in + let end_ = + let rec scan j = + if j >= String.length s then j + else match s.[j] with '.' | ')' -> j | _ -> scan (j + 1) + in scan inner in + let handler_name = String.sub s inner (end_ - inner) in + (* Extract path param value if present (after the dot) *) + if end_ < String.length s && s.[end_] = '.' then begin + let val_start = end_ + 1 in + let val_end = try String.index_from s val_start ')' with Not_found -> String.length s in + let param_val = String.sub s val_start (val_end - val_start) in + (* Append to query string so request-arg can find it *) + let sep = if !_req_query = "" then "" else "&" in + _req_query := !_req_query ^ sep ^ "item-id=" ^ param_val + end; + handler_name + else + let end_ = try String.index_from s start ')' with Not_found -> String.length s in + String.sub s start (end_ - start) else find_api s (i + 1) in find_api path 0 in let handler_key = "handler:ex-" ^ slug in diff --git a/tests/playwright/hypermedia-handlers.spec.js b/tests/playwright/hypermedia-handlers.spec.js index d49413ad..1d01800b 100644 --- a/tests/playwright/hypermedia-handlers.spec.js +++ b/tests/playwright/hypermedia-handlers.spec.js @@ -191,14 +191,20 @@ test.describe('Mutation handlers', () => { test('delete-row: clicking delete removes row', async ({ page }) => { await loadPage(page, '(geography.(hypermedia.(example.delete-row)))'); - const rowsBefore = await page.locator('table tbody tr').count(); - const deleteBtn = page.locator('button:has-text("Delete")').first(); - if (await deleteBtn.count() > 0 && rowsBefore > 0) { - await deleteBtn.click(); - await page.waitForTimeout(2000); - const rowsAfter = await page.locator('table tbody tr').count(); - // Row count should decrease or stay same (if swap replaces) - expect(rowsAfter).toBeLessThanOrEqual(rowsBefore); + // Auto-accept confirm dialogs (sx-confirm triggers window.confirm) + page.on('dialog', dialog => dialog.accept()); + const rowsBefore = await page.locator('tbody#delete-rows tr').count(); + expect(rowsBefore).toBeGreaterThan(0); + const firstRowId = await page.locator('tbody#delete-rows tr').first().getAttribute('id'); + const deleteBtn = page.locator('tbody#delete-rows tr:first-child button').first(); + await deleteBtn.click(); + await page.waitForTimeout(2000); + const rowsAfter = await page.locator('tbody#delete-rows tr').count(); + // Row count must strictly decrease + expect(rowsAfter).toBe(rowsBefore - 1); + // The specific row must be gone + if (firstRowId) { + await expect(page.locator(`#${firstRowId}`)).toHaveCount(0); } });