host: relate/unrelate keep both lists in sync (add to current list, never blank the picker)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Two reported bugs on the edit page's relation editor:
1. relating a candidate didn't add it to the current-relations list (the AJAX
relate just deleted the candidate row; the relation only showed after a reload);
2. removing a relation could blank the relate picker.
Fix (lib/host/blog.sx): both the candidate's relate form and a current relation's
remove form now target #rel-editor-<kind> with sx-swap=outerHTML, and the
relate/unrelate handlers return the re-rendered editor for that kind (current list +
a fresh picker). So one swap keeps BOTH lists in sync: the related post moves into
the current list and out of the (re-loaded) candidate pool; removing moves it back.
Gated on the SX-Target header, so a plain boosted form / no-JS POST (the is-a-tag
toggle) still redirects + re-renders #content.
Engine fix (web/orchestration.sx): handle-html-response's non-select branch called
post-swap on the OLD target, which an outerHTML swap has already REPLACED — so the
swapped-in content's triggers (here the re-rendered picker's "load") never bound and
the picker stayed empty. post-swap the swap result (the new node), mirroring the
sx-select branch. Recompiled orchestration.sxbc for the content-addressed client.
Tests:
- web/tests/test-relate-picker.sx: relating re-syncs the editor (post in current
list + picker re-loads); removing does likewise — both fail without the engine fix.
- lib/host/tests/blog.sx: relate/unrelate return the re-rendered editor fragment
(200, #rel-editor + picker), forms wire to #rel-editor-KIND/outerHTML, plain
boosted POST still 303.
- relate-picker.spec.js: the full in-page flow (relate adds to list, remove keeps
the picker, no reload) + persistence.
Verified: host conformance 277/277, web engine suite 8/8, run-picker-check 3/3,
run-spa-check 3/3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,28 +50,53 @@ async function login(page) {
|
||||
}
|
||||
|
||||
test.describe('relate picker (browser-only)', () => {
|
||||
test('the remove button on a current relation actually unrelates it', async ({ page }) => {
|
||||
test('relating a candidate adds it to the current list AND removing keeps the picker', async ({ page }) => {
|
||||
// The whole in-page flow the user reported broken — no reloads. Relating a
|
||||
// candidate re-renders the editor: the post moves into the current-relations
|
||||
// list and the picker re-loads its candidates (it is NOT blanked). Removing it
|
||||
// re-renders the editor back: the post leaves the current list and the picker
|
||||
// still offers candidates.
|
||||
test.setTimeout(75000);
|
||||
await loginTo(page, `/${HOST}/edit`);
|
||||
await waitReady(page);
|
||||
// relate Item 13 via the picker, then reload so it shows in the current list
|
||||
await page.evaluate(() => { window.__noReload = true; });
|
||||
// relate Item 13 from the picker
|
||||
await page.fill(RELF, 'Item 13');
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(1);
|
||||
await page.locator(`${RELROWS} button`).first().click();
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(0);
|
||||
const relLink = page.locator('a[href="/picker-item-13/"]');
|
||||
// ISSUE 1: it now appears in the CURRENT relations list (added, not just removed)
|
||||
await expect(relLink).toHaveCount(1, { timeout: 12000 });
|
||||
// and the re-rendered picker still offers candidates (not blanked)
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThan(0);
|
||||
// now remove it via its current-list remove button
|
||||
await page.locator('li:has(a[href="/picker-item-13/"]) button').click();
|
||||
await expect(relLink).toHaveCount(0, { timeout: 12000 }); // left the current list
|
||||
// ISSUE 2: removing must NOT clear "the list of posts to relate"
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThan(0);
|
||||
expect(await page.evaluate(() => window.__noReload)).toBe(true); // all in-page, no reload
|
||||
// and the relation truly persisted gone (reload shows it not present)
|
||||
await page.reload();
|
||||
await waitReady(page);
|
||||
const relLink = page.locator('a[href="/picker-item-13/"]');
|
||||
await expect(relLink).toHaveCount(1); // current relation present
|
||||
// the picker is populated (empty filter -> first page of candidates)
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThan(0);
|
||||
// click its remove button
|
||||
await page.locator('li:has(a[href="/picker-item-13/"]) button').click();
|
||||
await expect(relLink).toHaveCount(0, { timeout: 12000 }); // relation removed
|
||||
// REGRESSION: removing a relation must NOT clear "the list of posts to relate".
|
||||
// (The old plain-boosted remove form redirected -> re-rendered #content and the
|
||||
// picker came back empty; the AJAX in-place remove leaves the picker untouched.)
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThan(0);
|
||||
await expect(page.locator('a[href="/picker-item-13/"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('relating a candidate persists the relation', async ({ page }) => {
|
||||
test.setTimeout(75000);
|
||||
await loginTo(page, `/${HOST}/edit`);
|
||||
await waitReady(page);
|
||||
await page.fill(RELF, 'Item 07');
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(1);
|
||||
await page.locator(`${RELROWS} button`).first().click();
|
||||
await expect(page.locator('a[href="/picker-item-07/"]')).toHaveCount(1, { timeout: 12000 });
|
||||
// persisted across a reload
|
||||
await page.reload();
|
||||
await waitReady(page);
|
||||
await expect(page.locator('a[href="/picker-item-07/"]')).toHaveCount(1);
|
||||
// and visible on the public post page
|
||||
await page.goto(`/${HOST}/`);
|
||||
await expect(page.getByRole('heading', { name: 'Related posts' })).toBeVisible();
|
||||
await expect(page.locator('body')).toContainText('Picker Item 07');
|
||||
});
|
||||
|
||||
test('picker populates after a boosted SPA nav to the edit page', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user