From 9fa5a9eee05d922b46805e38be84953e422f2db5 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 May 2026 06:30:29 -0700 Subject: [PATCH] chore: add react island strategy tests --- tests/specs/IslandClientStrategyReactSpec.cfc | 113 ++++++++++++ ...ec.cfc => IslandClientStrategyVueSpec.cfc} | 10 +- tests/specs/IslandLoadStrategyReactSpec.cfc | 162 ++++++++++++++++++ ...Spec.cfc => IslandLoadStrategyVueSpec.cfc} | 9 +- 4 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 tests/specs/IslandClientStrategyReactSpec.cfc rename tests/specs/{IslandClientStrategySpec.cfc => IslandClientStrategyVueSpec.cfc} (93%) create mode 100644 tests/specs/IslandLoadStrategyReactSpec.cfc rename tests/specs/{IslandLoadStrategySpec.cfc => IslandLoadStrategyVueSpec.cfc} (94%) diff --git a/tests/specs/IslandClientStrategyReactSpec.cfc b/tests/specs/IslandClientStrategyReactSpec.cfc new file mode 100644 index 0000000..07f2c75 --- /dev/null +++ b/tests/specs/IslandClientStrategyReactSpec.cfc @@ -0,0 +1,113 @@ +/** + * Spec: + * + * React parity of IslandClientStrategySpec. Island.cfm is framework-agnostic + * (it dispatches via the framework struct's .name / .clientEntry / .render / + * .ssrRender), so this spec proves the same client-strategy contract holds + * when a React-shaped struct is plugged in: no SSR call, no SSR HTML/CSS, + * empty mount div, boot script with the React client entry. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll(){ + application.coldspaConfig = { + "isDev": true, + "debug": false, + "vitePort": "5173", + "viteUrl": "http://localhost:5173", + "ssrUrl": "http://127.0.0.1:5174" + }; + } + + function run(){ + + describe( "cf_Island strategy=client (React)", function(){ + + beforeEach( function( currentSpec ){ + variables.ssrCallCount = 0; + + variables.mockReact = { + "name": "React", + "clientEntry": "./coldspa/vite/clients/react-client.js", + "ssrRender": function( componentGlobKey, props, slotHtml, namedSlots ){ + variables.ssrCallCount++; + return { "html": "

SHOULD NOT APPEAR

", "css": "", "error": "" }; + }, + "render": function( mountId, componentGlobKey, propsJson, resolvedClientEntry, optionsJson ){ + return { + "imports": "import { mount } from '" & arguments.resolvedClientEntry & "';", + "body": "mount('" & arguments.componentGlobKey & "', document.getElementById('" & arguments.mountId & "'), " & arguments.propsJson & ", " & arguments.optionsJson & ");" + }; + } + }; + } ); + + it( "skips SSR entirely (does not call framework.ssrRender)", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = { "msg": "hi" }, strategy = "client" ); + expect( ssrCallCount ).toBe( 0, "ssrRender must not run when strategy=client" ); + expect( html ).notToInclude( "SHOULD NOT APPEAR" ); + } ); + + it( "emits an empty mount div tagged for React (no SSR HTML inside)", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {}, strategy = "client" ); + expect( html ).toMatch( '
' ); + } ); + + it( "does not emit any SSR style or link tags", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {}, strategy = "client" ); + expect( html ).notToInclude( "data-coldspa-ssr" ); + } ); + + it( "does not emit a slot template when there are no slots", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {}, strategy = "client" ); + expect( html ).notToInclude( "' ); + expect( html ).toInclude( "import { mount } from 'http://localhost:5173/coldspa/vite/clients/react-client.js';" ); + expect( html ).toInclude( "mount('/Hello.jsx', document.getElementById(" ); + } ); + + it( "serializes mount options with strategy:'client' and hasSlot:false", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {}, strategy = "client" ); + expect( html ).toMatch( '"strategy"\s*:\s*"client"' ); + expect( html ).toMatch( '"hasSlot"\s*:\s*false' ); + } ); + + it( "serializes the props the caller passed", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = { "msg": "hello world" }, strategy = "client" ); + expect( html ).toInclude( '"msg":"hello world"' ); + } ); + + it( "does NOT wrap the boot in requestIdleCallback or IntersectionObserver", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {}, strategy = "client" ); + expect( html ).notToInclude( "requestIdleCallback" ); + expect( html ).notToInclude( "IntersectionObserver" ); + } ); + + } ); + + } + + private string function renderIsland( + required struct framework, + required string path, + struct props = {}, + string strategy = "client" + ){ + var out = ""; + savecontent variable="out" { + cf_Island( + framework = arguments.framework, + path = arguments.path, + props = arguments.props, + strategy = arguments.strategy + ); + } + return out; + } + +} diff --git a/tests/specs/IslandClientStrategySpec.cfc b/tests/specs/IslandClientStrategyVueSpec.cfc similarity index 93% rename from tests/specs/IslandClientStrategySpec.cfc rename to tests/specs/IslandClientStrategyVueSpec.cfc index f8e668b..177afff 100644 --- a/tests/specs/IslandClientStrategySpec.cfc +++ b/tests/specs/IslandClientStrategyVueSpec.cfc @@ -1,13 +1,13 @@ /** - * Spec: with strategy="client" + * Spec: * * The "client" strategy is the no-SSR contract: Island.cfm must NOT call the * framework's ssrRender(), must NOT emit SSR HTML / CSS / link tags, and must * still emit a boot script that the client uses to mount the component. * - * We use a mock framework struct so the spec is hermetic — no Vite, no SSR - * sidecar, no network. The shape (name, clientEntry, render, ssrRender) is - * the same one Vue.cfm / React.cfm produce. + * We use a mock framework struct shaped like the one Vue.cfm produces, so + * the spec is hermetic — no Vite, no SSR sidecar, no network. React parity + * for this contract lives in IslandClientStrategyReactSpec.cfc. */ component extends="testbox.system.BaseSpec" { @@ -25,7 +25,7 @@ component extends="testbox.system.BaseSpec" { function run(){ - describe( "cf_Island strategy=client", function(){ + describe( "cf_Island strategy=client (Vue)", function(){ beforeEach( function( currentSpec ){ // Tracks whether the framework's ssrRender() was invoked. diff --git a/tests/specs/IslandLoadStrategyReactSpec.cfc b/tests/specs/IslandLoadStrategyReactSpec.cfc new file mode 100644 index 0000000..5a2f35f --- /dev/null +++ b/tests/specs/IslandLoadStrategyReactSpec.cfc @@ -0,0 +1,162 @@ +/** + * Spec: + * + * React parity of IslandLoadStrategySpec. Same SSR + immediate-hydration + * contract, exercised through a React-shaped mock framework struct so we + * confirm Island.cfm doesn't favor Vue anywhere (mount div tagged "React", + * React client entry imported in the boot script, JSX-style component path + * threaded through to ssrRender). + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll(){ + application.coldspaConfig = { + "isDev": true, + "debug": false, + "vitePort": "5173", + "viteUrl": "http://localhost:5173", + "ssrUrl": "http://127.0.0.1:5174" + }; + } + + function run(){ + + describe( "cf_Island strategy=load (React)", function(){ + + beforeEach( function( currentSpec ){ + variables.ssrCalls = []; + + variables.mockReact = { + "name": "React", + "clientEntry": "./coldspa/vite/clients/react-client.js", + "ssrRender": function( componentGlobKey, props, slotHtml, namedSlots ){ + arrayAppend( variables.ssrCalls, { + "componentGlobKey": arguments.componentGlobKey, + "props": duplicate( arguments.props ), + "slotHtml": arguments.slotHtml, + "namedSlots": duplicate( arguments.namedSlots ) + } ); + return { + "html": "

SSR rendered: " & ( arguments.props.msg ?: "" ) & "

", + "css": ".coldspa-test{color:red}", + "error": "" + }; + }, + "render": function( mountId, componentGlobKey, propsJson, resolvedClientEntry, optionsJson ){ + return { + "imports": "import { mount } from '" & arguments.resolvedClientEntry & "';", + "body": "mount('" & arguments.componentGlobKey & "', document.getElementById('" & arguments.mountId & "'), " & arguments.propsJson & ", " & arguments.optionsJson & ");" + }; + } + }; + } ); + + it( "calls framework.ssrRender exactly once with the component path, props, and empty slots", function(){ + renderIsland( framework = mockReact, path = "./Hello.jsx", props = { "msg": "hi" } ); + expect( arrayLen( ssrCalls ) ).toBe( 1 ); + expect( ssrCalls[1].componentGlobKey ).toBe( "/Hello.jsx" ); + expect( ssrCalls[1].props.msg ).toBe( "hi" ); + expect( ssrCalls[1].slotHtml ).toBe( "" ); + expect( structIsEmpty( ssrCalls[1].namedSlots ) ).toBeTrue(); + } ); + + it( "injects the SSR HTML inside the React-tagged mount div", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = { "msg": "world" } ); + expect( html ).toMatch( '

SSR rendered: world

' ); + } ); + + it( "emits the SSR CSS in a tagged style element", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {} ); + expect( html ).toMatch( '' ); + } ); + + it( "emits an immediate boot script importing the React client entry (no idle / observer wrappers)", function(){ + var html = renderIsland( framework = mockReact, path = "./Hello.jsx", props = {} ); + expect( html ).toInclude( '