From a0368437288bc8874c0d408200bb6c6256592f07 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 12 Jul 2026 14:43:31 -0700 Subject: [PATCH] fix(builder): deterministic ids for NumberCounter + form-relay marker Two components of the det-id bug class were missed in the earlier migration: NumberCounter's wrapper/counter ids and count-up script, and the shared form-relay-wiring fid (used by ContactForm/FormContainer). Both used Math.random() for exported HTML ids, breaking caching/diffing across exports. Migrate both to scopeId(nodeId, fallbackSeed, prefix), threading nodeId through NumberCounter.toHtml and relayFormWiring (via ContactForm.toHtml and FormContainer.toHtml, both now passing nodeId as their 3rd arg). --- .../forms/ContactForm.toHtml.test.ts | 28 +++++++++-- craft/src/components/forms/ContactForm.tsx | 4 +- .../forms/FormContainer.toHtml.test.ts | 20 ++++++-- craft/src/components/forms/FormContainer.tsx | 4 +- .../sections/NumberCounter.toHtml.test.ts | 49 +++++++++++++++++++ .../src/components/sections/NumberCounter.tsx | 12 +++-- craft/src/utils/form-relay-wiring.test.ts | 38 ++++++++++++++ craft/src/utils/form-relay-wiring.ts | 9 +++- 8 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 craft/src/components/sections/NumberCounter.toHtml.test.ts create mode 100644 craft/src/utils/form-relay-wiring.test.ts diff --git a/craft/src/components/forms/ContactForm.toHtml.test.ts b/craft/src/components/forms/ContactForm.toHtml.test.ts index 8f0f7be..4f50fec 100644 --- a/craft/src/components/forms/ContactForm.toHtml.test.ts +++ b/craft/src/components/forms/ContactForm.toHtml.test.ts @@ -6,12 +6,12 @@ const toHtml = (ContactForm as any).toHtml; describe('ContactForm.toHtml relay wiring', () => { test('with recipientEmail: emits marker, placeholder action, honeypot', () => { const { html } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, ''); - expect(html).toMatch(//); - expect(html).toMatch(/action="__WHP_FORM_ACTION__F[0-9a-z]+__"/); + expect(html).toMatch(//); + expect(html).toMatch(/action="__WHP_FORM_ACTION__F_[0-9a-z]+__"/); expect(html).toContain('method="POST"'); expect(html).toContain('name="_gotcha"'); // marker id and action id match - const mid = html.match(/id="(F[0-9a-z]+)"/)![1]; + const mid = html.match(/id="(F_[0-9a-z]+)"/)![1]; expect(html).toContain(`__WHP_FORM_ACTION__${mid}__`); }); @@ -64,6 +64,28 @@ describe('ContactForm.toHtml successMessage', () => { }); }); +describe('ContactForm.toHtml relay marker deterministic + unique via node id (no Math.random)', () => { + test('same node id -> identical marker+placeholder ids across two calls', () => { + const { html: html1 } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, '', 'node-cf1'); + const { html: html2 } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, '', 'node-cf1'); + expect(html1).toBe(html2); + }); + + test('marker id always equals the placeholder id it pairs with', () => { + const { html } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, '', 'node-cf1'); + const mid = html.match(//); - expect(html).toMatch(/action="__WHP_FORM_ACTION__F[0-9a-z]+__"/); + expect(html).toMatch(//); + expect(html).toMatch(/action="__WHP_FORM_ACTION__F_[0-9a-z]+__"/); expect(html).toContain('method="POST"'); // relay forces POST even though method=GET expect(html).toContain('name="_gotcha"'); // honeypot precedes the form's children expect(html.indexOf('_gotcha')).toBeLessThan(html.indexOf('name="email"')); // marker id === action id - const mid = html.match(/id="(F[0-9a-z]+)"/)![1]; + const mid = html.match(/id="(F_[0-9a-z]+)"/)![1]; expect(html).toContain(`__WHP_FORM_ACTION__${mid}__`); }); @@ -24,4 +24,18 @@ describe('FormContainer.toHtml relay wiring', () => { expect(html).toContain('action="/legacy"'); expect(html).toContain(''); }); + + test('same node id -> identical marker+placeholder ids across two calls', () => { + const { html: html1 } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx' }, '', 'node-fc1'); + const { html: html2 } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx' }, '', 'node-fc1'); + expect(html1).toBe(html2); + }); + + test('two different node ids -> different fids', () => { + const { html: html1 } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx' }, '', 'node-fc1'); + const { html: html2 } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx' }, '', 'node-fc2'); + const mid1 = html1.match(/`,