host: block-editor card-type <select> options are direct children (populate on boosted nav)

The add-block dropdown wrapped its <option>s in a <span> — (select :name "ctype" (span
(option…)…)) — to splice a dynamic list. A <select> only renders <option>/<optgroup> direct
children, so the dropdown was empty. A full-page load hid it (the browser's HTML parser hoists
mis-nested options out of the select), but on a BOOSTED nav the DOM is built programmatically
(no parser error-recovery), so the span stayed and the dropdown was empty. The card types are
a fixed set — inline the options directly as <select> children.

TEST-FIRST: 4th boost-nav.spec.js case (LOGGED IN: boosted nav to edit → assert
select[name=ctype] > option count is 5, incl card-heading). RED before (0 direct-child
options — span-wrapped), GREEN after. All 4 boost-nav tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:06:57 +00:00
parent 7bec86289c
commit 01e0b5db41
2 changed files with 27 additions and 4 deletions

View File

@@ -1374,16 +1374,23 @@
(define host/blog--block-editor (define host/blog--block-editor
(fn (slug) (fn (slug)
(let ((refs (host/blog-body-refs slug))) (let ((refs (host/blog-body-refs slug)))
(let ((rows (map (fn (c) (host/blog--block-row slug c)) refs)) (let ((rows (map (fn (c) (host/blog--block-row slug c)) refs)))
(type-opts (map (fn (ct) (quasiquote (option :value (unquote ct) (unquote ct))))
(list "card-heading" "card-text" "card-quote" "card-code" "card-callout"))))
(quasiquote (quasiquote
(div :id "block-editor" :style "margin-top:1.5em;border-top:1px solid #ccc;padding-top:1em" (div :id "block-editor" :style "margin-top:1.5em;border-top:1px solid #ccc;padding-top:1em"
(h3 :style "font-size:1em;margin:0 0 0.3em" "Blocks") (h3 :style "font-size:1em;margin:0 0 0.3em" "Blocks")
(unquote (if (> (len refs) 0) (cons (quote ul) rows) (quote (p :style "color:#999" "No blocks yet.")))) (unquote (if (> (len refs) 0) (cons (quote ul) rows) (quote (p :style "color:#999" "No blocks yet."))))
(form :method "post" :action (unquote (str "/" slug "/blocks/add")) (form :method "post" :action (unquote (str "/" slug "/blocks/add"))
:sx-post (unquote (str "/" slug "/blocks/add")) :sx-target "#block-editor" :sx-swap "outerHTML" :sx-post (unquote (str "/" slug "/blocks/add")) :sx-target "#block-editor" :sx-swap "outerHTML"
(select :name "ctype" (unquote (cons (quote span) type-opts))) ;; options MUST be DIRECT children of <select> — a wrapper (e.g. a span to splice
;; a dynamic list) leaves the dropdown empty when the DOM is built programmatically
;; on a boosted swap (a full-page HTML parse would hoist them out, masking it). The
;; card types are a fixed set, so inline them.
(select :name "ctype"
(option :value "card-heading" "heading")
(option :value "card-text" "text")
(option :value "card-quote" "quote")
(option :value "card-code" "code")
(option :value "card-callout" "callout"))
" " (input :name "text" :placeholder "text…" :style "width:50%") " " (input :name "text" :placeholder "text…" :style "width:50%")
" " (button :type "submit" "+ add block")))))))) " " (button :type "submit" "+ add block"))))))))

View File

@@ -88,4 +88,20 @@ test.describe('boosted navigation (browser-only)', () => {
await expect(page.locator('body')).toContainText('Posts', { timeout: 12000 }); await expect(page.locator('body')).toContainText('Posts', { timeout: 12000 });
expect(new URL(page.url()).pathname).toBe('/'); expect(new URL(page.url()).pathname).toBe('/');
}); });
test('LOGGED IN: the block-editor card-type dropdown populates after a boosted nav to edit', async ({ page }) => {
test.setTimeout(90000);
await login(page);
await page.goto(BASE + '/');
await waitReady(page);
await page.locator('a[href="/compose-demo/"]').first().click();
await expect(page.locator('body')).toContainText('composition object', { timeout: 15000 });
await page.locator('a[href="/compose-demo/edit"]').last().click();
await expect(page.locator('body')).toContainText('Edit:', { timeout: 15000 });
// the ctype <select> must have selectable <option> DIRECT children. A <span> wrapper
// leaves the dropdown empty when the DOM is built programmatically on a boosted swap
// (the HTML parser would hoist them out on a full load, hiding the bug there).
await expect(page.locator('#block-editor select[name="ctype"] > option')).toHaveCount(5, { timeout: 10000 });
await expect(page.locator('#block-editor select[name="ctype"] > option[value="card-heading"]')).toHaveCount(1);
});
}); });