From dca0f2721a6d100a3fd25448af4b10b3a62f3a1d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Jan 2026 06:30:28 -0500 Subject: [PATCH] Rearrange obsolete tests; test new v4 code --- src/Common/README.md | 4 +- src/Directory.Build.props | 16 +- src/Htmx/README.md | 12 +- src/Tests/Htmx.fs | 136 ++-- src/Tests/ViewEngine.fs | 1303 ++++++++++++++++++--------------- src/ViewEngine.Htmx/Htmx.fs | 43 +- src/ViewEngine.Htmx/README.md | 13 +- 7 files changed, 852 insertions(+), 675 deletions(-) diff --git a/src/Common/README.md b/src/Common/README.md index 9a2cd15..6bb8ed2 100644 --- a/src/Common/README.md +++ b/src/Common/README.md @@ -2,4 +2,6 @@ This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. It also contains htmx as a static web asset, allowing it to be loaded from your local (or published) project. -**htmx version: 2.0.8** +**htmx version: 4.0.0-alpha6** + +_**NOTE:** Pay special attention to breaking changes highlighted in the packages listed above._ \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 39a10a0..e5a671c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,16 +2,16 @@ net8.0;net9.0;net10.0 - 2.0.8 + 4.0.0 + alpha6a true - - Adds package-provided script (available via Giraffe.Htmx.Common); use app.MapStaticAssets() for publish support, use builder.UseStaticWebAssets() for non-development/non-published execution -- [View Engine] Deprecates Script.minified and Script.unminified (use Script.cdnMinified and Script.cdnUnminified instead) -- Updates script tags to pull htmx 2.0.8 (no header or attribute changes) -- Adds .NET 10 support + First htmx 4 alpha release of these libraries +- [Server] Marked removed headers as obsolete; added new HX-Source header +- [View Engine] Marked removed attributes as obsolete +- [View Engine] Added new attributes, modifiers, and support for new hx-partial tag +- Updated script tags to pull htmx 4.0.0-alpha6 -See full release notes, including more info about the package-provided script, at https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/releases/tag/v2.0.8 - -NOTE: As of 2.0.6, the CDN for htmx changed from unpkg.com to cdn.jsdelivr.net; sites with Content-Security-Policy headers will want to update their allowed script-src domains accordingly +See package READMEs; this is not an update-and-forget-it release danieljsummers Bit Badger Solutions diff --git a/src/Htmx/README.md b/src/Htmx/README.md index 07b002d..ea64b13 100644 --- a/src/Htmx/README.md +++ b/src/Htmx/README.md @@ -2,9 +2,11 @@ This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`. -**htmx version: 2.0.8** +**htmx version: 4.0.0-alpha6** -_Upgrading from v1.x: the [migration guide](https://htmx.org/migration-guide-htmx-1/) does not currently specify any request or response header changes. This means that there are no required code changes in moving from v1.* to v2.*._ +_Upgrading from v2.x: the [migration guide](https://four.htmx.org/migration-guide-htmx-4/) lists changes for v4. For this package, the `HX-Trigger` and `HX-Trigger-Name` headers are marked obsolete. They are replaced by `HX-Source`, which provides the triggering tag name and `id` attribute. The `HX-Prompt` header has also been marked as obsolete, as the `hx-prompt` attribute which generated its content has been removed._ + +_Obsolete elements will be removed in the first production v4 release._ ### Setup @@ -18,9 +20,9 @@ To obtain a request header, using the `IHeaderDictionary` extension properties: ```fsharp let myHandler : HttpHander = fun next ctx -> - match ctx.HxPrompt with - | Some prompt -> ... // do something with the text the user provided - | None -> ... // no text provided + match ctx.Target with + | Some elt -> ... // do something with id of the target element + | None -> ... // no target element provided ``` To set a response header: diff --git a/src/Tests/Htmx.fs b/src/Tests/Htmx.fs index bdcfecc..e6733a3 100644 --- a/src/Tests/Htmx.fs +++ b/src/Tests/Htmx.fs @@ -3,7 +3,6 @@ module Htmx open System open Expecto open Giraffe.Htmx -open Microsoft.AspNetCore.Html open Microsoft.AspNetCore.Http open NSubstitute @@ -74,21 +73,6 @@ let dictExtensions = ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header should have been false" } ] - testList "HxPrompt" [ - test "succeeds when the header is not present" { - let ctx = Substitute.For() - ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore - Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned" - } - test "succeeds when the header is present" { - let ctx = Substitute.For() - let dic = HeaderDictionary() - dic.Add("HX-Prompt", "of course") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present" - Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect" - } - ] testList "HxRequest" [ test "succeeds when the header is not present" { let ctx = Substitute.For() @@ -112,6 +96,44 @@ let dictExtensions = Expect.isFalse ctx.Request.Headers.HxRequest.Value "The header should have been false" } ] + testList "HxSource" [ + test "succeeds when the header is not present" { + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore + Expect.isNone ctx.Request.Headers.HxSource "There should not have been a header returned" + } + test "succeeds when the header is present and both parts exist" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Source", "button#theId") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + let hdr = ctx.Request.Headers.HxSource + Expect.isSome hdr "There should be a header present" + Expect.equal (fst hdr.Value) "button" "The source tag was incorrect" + Expect.isSome (snd hdr.Value) "There should be a source ID present" + Expect.equal (snd hdr.Value).Value "theId" "The source ID was incorrect" + } + test "succeeds when the header is present and ID is blank" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Source", "a#") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + let hdr = ctx.Request.Headers.HxSource + Expect.isSome hdr "There should be a header present" + Expect.equal (fst hdr.Value) "a" "The source tag was incorrect" + Expect.isNone (snd hdr.Value) "There should not be a source ID present" + } + test "succeeds when the header is present and ID is missing" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Source", "form") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + let hdr = ctx.Request.Headers.HxSource + Expect.isSome hdr "There should be a header present" + Expect.equal (fst hdr.Value) "form" "The source tag was incorrect" + Expect.isNone (snd hdr.Value) "There should not be a source ID present" + } + ] testList "HxTarget" [ test "succeeds when the header is not present" { let ctx = Substitute.For() @@ -127,36 +149,6 @@ let dictExtensions = Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect" } ] - testList "HxTrigger" [ - test "succeeds when the header is not present" { - let ctx = Substitute.For() - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned" - } - test "succeeds when the header is present" { - let ctx = Substitute.For() - let dic = HeaderDictionary() - dic.Add("HX-Trigger", "#trig") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present" - Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect" - } - ] - testList "HxTriggerName" [ - test "succeeds when the header is not present" { - let ctx = Substitute.For() - ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore - Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned" - } - test "HxTriggerName succeeds when the header is present" { - let ctx = Substitute.For() - let dic = HeaderDictionary() - dic.Add("HX-Trigger-Name", "click") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present" - Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect" - } - ] ] /// Tests for the HttpRequest extension properties @@ -366,5 +358,55 @@ let script = } ] +#nowarn 44 // Obsolete items still have tests +let dictExtensionsObs = + testList "IHeaderDictionaryExtensions (Obsolete)" [ + testList "HxPrompt" [ + test "succeeds when the header is not present" { + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore + Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned" + } + test "succeeds when the header is present" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Prompt", "of course") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present" + Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect" + } + ] + testList "HxTrigger" [ + test "succeeds when the header is not present" { + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned" + } + test "succeeds when the header is present" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Trigger", "#trig") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present" + Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect" + } + ] + testList "HxTriggerName" [ + test "succeeds when the header is not present" { + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore + Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned" + } + test "HxTriggerName succeeds when the header is present" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Trigger-Name", "click") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present" + Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect" + } + ] + ] + /// All tests for this module -let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script ] +let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script; dictExtensionsObs ] diff --git a/src/Tests/ViewEngine.fs b/src/Tests/ViewEngine.fs index 9f71f09..0467dba 100644 --- a/src/Tests/ViewEngine.fs +++ b/src/Tests/ViewEngine.fs @@ -4,6 +4,47 @@ open Expecto open Giraffe.ViewEngine open Giraffe.ViewEngine.Htmx +/// Tests for the HxConfig module +let hxConfig = + testList "HxConfig" [ + testList "Configure" [ + test "succeeds with an empty list" { + Expect.equal (HxConfig.Configure []) "{ }" "Configure with empty list incorrect" + } + test "succeeds with a non-empty list" { + Expect.equal + (HxConfig.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) """{ "a": "b", "c": "d" }""" + "Configure with a non-empty list incorrect" + } + test "succeeds with all known params configured" { + Expect.equal + (HxConfig.Configure + [ HxConfig.Timeout 1000; HxConfig.Credentials false; HxConfig.NoHeaders true ]) + """{ "timeout": 1000, "credentials": false, "noHeaders": true }""" + "Configure with all known params incorrect" + } + ] + test "Timeout succeeds" { + Expect.equal (HxConfig.Timeout 50) "\"timeout\": 50" "Timeout value incorrect" + } + testList "Credentials" [ + test "succeeds when set to true" { + Expect.equal (HxConfig.Credentials true) "\"credentials\": true" "Credentials value incorrect" + } + test "succeeds when set to false" { + Expect.equal (HxConfig.Credentials false) "\"credentials\": false" "Credentials value incorrect" + } + ] + testList "NoHeaders" [ + test "succeeds when set to true" { + Expect.equal (HxConfig.NoHeaders true) "\"noHeaders\": true" "NoHeaders value incorrect" + } + test "succeeds when set to false" { + Expect.equal (HxConfig.NoHeaders false) "\"noHeaders\": false" "NoHeaders value incorrect" + } + ] + ] + /// Tests for the HxEncoding module let hxEncoding = testList "HxEncoding" [ @@ -15,7 +56,7 @@ let hxEncoding = } ] -#nowarn 44 // Obsolete events still have tests +/// Tests for the HxEvent module let hxEvent = testList "HxEvent" [ testList "Abort" [ @@ -54,23 +95,6 @@ let hxEvent = Expect.equal (AfterInit.ToHxOnString()) "after:init" "AfterInit hx-on event name not correct" } ] - testList "AfterOnLoad" [ - test "ToString succeeds" { - Expect.equal (string AfterOnLoad) "afterOnLoad" "AfterOnLoad event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal (AfterOnLoad.ToHxOnString()) "after:init" "AfterOnLoad hx-on event name not correct" - } - ] - testList "AfterProcessNode" [ - test "ToString succeeds" { - Expect.equal (string AfterProcessNode) "afterProcessNode" "AfterProcessNode event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal - (AfterProcessNode.ToHxOnString()) "after:init" "AfterProcessNode hx-on event name not correct" - } - ] testList "AfterPushIntoHistory" [ test "ToString succeeds" { Expect.equal @@ -105,14 +129,6 @@ let hxEvent = Expect.equal (AfterRequest.ToHxOnString()) "after:request" "AfterRequest hx-on event name not correct" } ] - testList "AfterSettle" [ - test "ToString succeeds" { - Expect.equal (string AfterSettle) "afterSettle" "AfterSettle event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal (AfterSettle.ToHxOnString()) "after:swap" "AfterSettle hx-on event name not correct" - } - ] testList "AfterSseMessage" [ test "ToString succeeds" { Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct" @@ -148,29 +164,6 @@ let hxEvent = (BeforeCleanup.ToHxOnString()) "before:cleanup" "BeforeCleanup hx-on event name not correct" } ] - testList "BeforeCleanupElement" [ - test "ToString succeeds" { - Expect.equal - (string BeforeCleanupElement) "beforeCleanupElement" "BeforeCleanupElement event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal - (BeforeCleanupElement.ToHxOnString()) - "before:cleanup" - "BeforeCleanupElement hx-on event name not correct" - } - ] - testList "BeforeHistorySave" [ - test "ToString succeeds" { - Expect.equal (string BeforeHistorySave) "beforeHistorySave" "BeforeHistorySave event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal - (BeforeHistorySave.ToHxOnString()) - "before:history:update" - "BeforeHistorySave hx-on event name not correct" - } - ] testList "BeforeHistoryUpdate" [ test "ToString succeeds" { Expect.equal @@ -191,23 +184,6 @@ let hxEvent = Expect.equal (BeforeInit.ToHxOnString()) "before:init" "BeforeInit hx-on event name not correct" } ] - testList "BeforeOnLoad" [ - test "ToString succeeds" { - Expect.equal (string BeforeOnLoad) "beforeOnLoad" "BeforeOnLoad event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal (BeforeOnLoad.ToHxOnString()) "before:init" "BeforeOnLoad hx-on event name not correct" - } - ] - testList "BeforeProcessNode" [ - test "ToString succeeds" { - Expect.equal (string BeforeProcessNode) "beforeProcessNode" "BeforeProcessNode event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal - (BeforeProcessNode.ToHxOnString()) "before:init" "BeforeProcessNode hx-on event name not correct" - } - ] testList "BeforeRestoreHistory" [ test "ToString succeeds" { Expect.equal @@ -261,14 +237,6 @@ let hxEvent = (BeforeSseStream.ToHxOnString()) "before:sse:stream" "BeforeSseStream hx-on event name not correct" } ] - testList "BeforeSend" [ - test "ToString succeeds" { - Expect.equal (string BeforeSend) "beforeSend" "BeforeSend event name not correct" - } - test "ToHxOnString succeeds" { - Expect.equal (BeforeSend.ToHxOnString()) "before:request" "BeforeSend hx-on event name not correct" - } - ] testList "BeforeSwap" [ test "ToString succeeds" { Expect.equal (string BeforeSwap) "beforeSwap" "BeforeSwap event name not correct" @@ -311,6 +279,650 @@ let hxEvent = (FinallyRequest.ToHxOnString()) "finally:request" "FinallyRequest hx-on event name not correct" } ] + ] + +/// Tests for the HxHeaders module +let hxHeaders = + testList "HxHeaders" [ + testList "From" [ + test "succeeds with an empty list" { + Expect.equal (HxHeaders.From []) "{ }" "Empty headers not correct" + } + test "succeeds and escapes quotes" { + Expect.equal + (HxHeaders.From [ "test", "one two three"; "again", """four "five" six""" ]) + """{ "test": "one two three", "again": "four \"five\" six" }""" "Headers not correct" + } + ] + ] + +/// Tests for the HxSync module +let hxSync = + testList "HxSync" [ + test "Drop is correct" { + Expect.equal HxSync.Drop "drop" "Drop is incorrect" + } + test "Abort is correct" { + Expect.equal HxSync.Abort "abort" "Abort is incorrect" + } + test "Replace is correct" { + Expect.equal HxSync.Replace "replace" "Replace is incorrect" + } + test "Queue is correct" { + Expect.equal HxSync.Queue "queue" "Queue is incorrect" + } + test "QueueFirst is correct" { + Expect.equal HxSync.QueueFirst "queue first" "QueueFirst is incorrect" + } + test "QueueLast is correct" { + Expect.equal HxSync.QueueLast "queue last" "QueueLast is incorrect" + } + test "QueueAll is correct" { + Expect.equal HxSync.QueueAll "queue all" "QueueAll is incorrect" + } + ] + +/// Tests for the HxTrigger module +let hxTrigger = + testList "HxTrigger" [ + test "Click is correct" { + Expect.equal HxTrigger.Click "click" "Click is incorrect" + } + test "Load is correct" { + Expect.equal HxTrigger.Load "load" "Load is incorrect" + } + test "Revealed is correct" { + Expect.equal HxTrigger.Revealed "revealed" "Revealed is incorrect" + } + test "Every succeeds" { + Expect.equal (HxTrigger.Every "3s") "every 3s" "Every is incorrect" + } + testList "Filter" [ + test "Alt succeeds" { + Expect.equal (HxTrigger.Filter.Alt HxTrigger.Click) "click[altKey]" "Alt filter incorrect" + } + test "Ctrl succeeds" { + Expect.equal (HxTrigger.Filter.Ctrl HxTrigger.Click) "click[ctrlKey]" "Ctrl filter incorrect" + } + test "Shift succeeds" { + Expect.equal (HxTrigger.Filter.Shift HxTrigger.Click) "click[shiftKey]" "Shift filter incorrect" + } + test "CtrlAlt succeeds" { + Expect.equal + (HxTrigger.Filter.CtrlAlt HxTrigger.Click) "click[ctrlKey&&altKey]" "Ctrl+Alt filter incorrect" + } + test "CtrlShift succeeds" { + Expect.equal + (HxTrigger.Filter.CtrlShift HxTrigger.Click) "click[ctrlKey&&shiftKey]" + "Ctrl+Shift filter incorrect" + } + test "CtrlAltShift succeeds" { + Expect.equal + (HxTrigger.Filter.CtrlAltShift HxTrigger.Click) "click[ctrlKey&&altKey&&shiftKey]" + "Ctrl+Alt+Shift filter incorrect" + } + test "AltShift succeeds" { + Expect.equal + (HxTrigger.Filter.AltShift HxTrigger.Click) "click[altKey&&shiftKey]" "Alt+Shift filter incorrect" + } + ] + testList "Once" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Once "") "once" "Once modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Once "click") "click once" "Once modifier incorrect" + } + ] + testList "Changed" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Changed "") "changed" "Changed modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Changed "click") "click changed" "Changed modifier incorrect" + } + ] + testList "Delay" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Delay "1s" "") "delay:1s" "Delay modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Delay "2s" "click") "click delay:2s" "Delay modifier incorrect" + } + ] + testList "Throttle" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Throttle "4s" "") "throttle:4s" "Throttle modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Throttle "7s" "click") "click throttle:7s" "Throttle modifier incorrect" + } + ] + testList "From" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.From ".nav" "") "from:.nav" "From modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.From "#somewhere" "click") "click from:#somewhere" "From modifier incorrect" + } + ] + testList "FromDocument" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.FromDocument "") "from:document" "FromDocument modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.FromDocument "click") "click from:document" "FromDocument modifier incorrect" + } + ] + testList "FromWindow" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.FromWindow "") "from:window" "FromWindow modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.FromWindow "click") "click from:window" "FromWindow modifier incorrect" + } + ] + testList "FromClosest" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.FromClosest "div" "") "from:closest div" "FromClosest modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.FromClosest "p" "click") "click from:closest p" "FromClosest modifier incorrect" + } + ] + testList "FromFind" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.FromFind "li" "") "from:find li" "FromFind modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.FromFind ".spot" "click") "click from:find .spot" "FromFind modifier incorrect" + } + ] + testList "Target" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Target "main" "") "target:main" "Target modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Target "footer" "click") "click target:footer" "Target modifier incorrect" + } + ] + testList "Consume" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Consume "") "consume" "Consume modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Consume "click") "click consume" "Consume modifier incorrect" + } + ] + testList "Queue" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.Queue "abc" "") "queue:abc" "Queue modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect" + } + test "succeeds when no type of queueing is given" { + Expect.equal (HxTrigger.Queue "" "blur") "blur queue" "Queue modifier incorrect" + } + ] + testList "QueueFirst" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.QueueFirst "") "queue:first" "QueueFirst modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.QueueFirst "click") "click queue:first" "QueueFirst modifier incorrect" + } + ] + testList "QueueLast" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.QueueLast "") "queue:last" "QueueLast modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.QueueLast "click") "click queue:last" "QueueLast modifier incorrect" + } + ] + testList "QueueAll" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.QueueAll "") "queue:all" "QueueAll modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.QueueAll "click") "click queue:all" "QueueAll modifier incorrect" + } + ] + testList "QueueNone" [ + test "succeeds when it is the first modifier" { + Expect.equal (HxTrigger.QueueNone "") "queue:none" "QueueNone modifier incorrect" + } + test "succeeds when it is not the first modifier" { + Expect.equal (HxTrigger.QueueNone "click") "click queue:none" "QueueNone modifier incorrect" + } + ] + ] + +/// Tests for the HxVals module +let hxVals = + testList "HxVals" [ + testList "From" [ + test "succeeds with an empty list" { + Expect.equal (HxVals.From []) "{ }" "From with an empty list is incorrect" + } + test "succeeds and escapes quotes" { + Expect.equal + (HxVals.From [ "test", """a "b" c"""; "2", "d e f" ]) + """{ "test": "a \"b\" c", "2": "d e f" }""" "From value is incorrect" + } + ] + ] + +/// Pipe-able assertion for a rendered node +let shouldRender expected node = + Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect" + +/// Tests for the HtmxAttrs module +let attributes = + testList "Attributes" [ + + test "_hxAction succeeds" { + form [ _hxAction "/endpoint/1" ] [] |> shouldRender """
""" + } + test "_hxBoost succeeds" { + div [ _hxBoost ] [] |> shouldRender """
""" + } + test "_hxConfig succeeds" { + body [ _hxConfig [ ("test", "that") ] ] [] + |> shouldRender """""" + } + test "_hxConfirm succeeds" { + button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """""" + } + test "_hxDelete succeeds" { + span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """""" + } + test "_hxDisable succeeds" { + p [ _hxDisable "#that" ] [] |> shouldRender """

""" + } + test "_hxEncoding succeeds" { + form [ _hxEncoding "utf-7" ] [] |> shouldRender """
""" + } + test "_hxGet succeeds" { + article [ _hxGet "/the-text" ] [] |> shouldRender """
""" + } + test "_hxHeaders succeeds" { + figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] [] + |> shouldRender """
""" + } + test "_hxIgnore succeeds" { + span [ _hxIgnore ] [] |> shouldRender """""" + } + test "_hxInclude succeeds" { + a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """""" + } + test "_hxIndicator succeeds" { + aside [ _hxIndicator "#spinner" ] [] |> shouldRender """""" + } + test "_hxMethod succeeds" { + div [ _hxMethod System.Net.Http.HttpMethod.Options ] [] + |> shouldRender """
""" + } + test "_hxNoBoost succeeds" { + td [ _hxNoBoost ] [] |> shouldRender """""" + } + test "_hxOnEvent succeeds" { + a [ _hxOnEvent "click" "doThis()" ] [] |> shouldRender """""" + } + test "_hxOnHxEvent succeeds" { + strong [ _hxOnHxEvent BeforeSwap "changeStuff()" ] [] + |> shouldRender """""" + } + test "_hxPatch succeeds" { + div [ _hxPatch "/arrrgh" ] [] |> shouldRender """
""" + } + test "_hxPost succeeds" { + hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
""" + } + test "_hxPreserve succeeds" { + img [ _hxPreserve ] |> shouldRender """""" + } + test "_hxPushUrl succeeds" { + dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """
""" + } + test "_hxPut succeeds" { + s [ _hxPut "/take-this" ] [] |> shouldRender """""" + } + test "_hxReplaceUrl succeeds" { + p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """

""" + } + test "_hxSelect succeeds" { + nav [ _hxSelect "#navbar" ] [] |> shouldRender """""" + } + test "_hxSelectOob succeeds" { + section [ _hxSelectOob "#oob" ] [] |> shouldRender """
""" + } + test "_hxSwap succeeds" { + del [ _hxSwap "innerHTML" ] [] |> shouldRender """""" + } + test "_hxSwapWithTransition succeeds" { + del [ _hxSwapWithTransition "innerHTML" ] [] + |> shouldRender """""" + } + test "_hxSwapOob succeeds" { + li [ _hxSwapOob "true" ] [] |> shouldRender """
  • """ + } + test "_hxSync succeeds" { + nav [ _hxSync "closest form" HxSync.Abort ] [] + |> shouldRender """""" + } + test "_hxTarget succeeds" { + header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
    """ + } + test "_hxTrigger succeeds" { + figcaption [ _hxTrigger "load" ] [] |> shouldRender """
    """ + } + test "_hxValidate succeeds" { + form [ _hxValidate ] [] |> shouldRender """
    """ + } + test "_hxVals succeeds" { + dt [ _hxVals """{ "extra": "values" }""" ] [] + |> shouldRender """
    """ + } + test "_sseConnect succeeds" { + div [ _sseConnect "/gps/sse" ] [] |> shouldRender """
    """ + } + test "_sseSwap succeeds" { + ul [ _sseSwap "sseMessageName" ] [] |> shouldRender """
      """ + } + ] + +/// Tests for the HxModifiers module +let hxModifiers = + testList "HxModifiers" [ + testList "hxAppend" [ + test "succeeds with a value attribute" { + div [ hxAppend (_hxAction "/this/url") ] [] + |> shouldRender """
      """ + } + test "succeeds with a boolean attribute" { + form [ hxAppend _hxValidate ] [] + |> shouldRender """
      """ + } + ] + testList "hxInherited" [ + test "succeeds with a value attribute" { + span [ hxInherited (_hxGet "/some/link") ] [] + |> shouldRender """""" + } + test "succeeds with a boolean attribute" { + p [ hxInherited _hxValidate ] [] + |> shouldRender """

      """ + } + ] + test "succeeds when both are applied" { + div [ hxAppend (hxInherited (_hxPost "/there")) ] [] + |> shouldRender """
      """ + } + ] + +/// Tests for the HxTags module +let hxTags = + testList "HxTags" [ + test "hxPartial succeeds" { + hxPartial [ _class "testy" ] [ str "Howdy" ] + |> shouldRender """Howdy""" + } + ] + +open Giraffe.Htmx.Common + +/// Tests for the Script module +let script = + testList "Script" [ + test "local succeeds" { + let html = RenderView.AsString.htmlNode Script.local + Expect.equal + html + $"""""" + "Local script tag is incorrect" + } + test "cdnMinified succeeds" { + let html = RenderView.AsString.htmlNode Script.cdnMinified + Expect.equal + html + $"""""" + "CDN minified script tag is incorrect" + } + test "cdnUnminified succeeds" { + let html = RenderView.AsString.htmlNode Script.cdnUnminified + Expect.equal + html + $"""""" + "CDN unminified script tag is incorrect" + } + ] + +open System.Text + +/// Tests for the RenderFragment module +let renderFragment = + testList "RenderFragment" [ + + /// Validate that the two object references are the same object + let isSame obj1 obj2 message = + Expect.isTrue (obj.ReferenceEquals(obj1, obj2)) message + + testList "findIdNode" [ + test "fails with a Text node" { + Expect.isNone (RenderFragment.findIdNode "blue" (Text "")) "There should not have been a node found" + } + test "fails with a VoidElement without a matching ID" { + Expect.isNone + (RenderFragment.findIdNode "purple" (br [ _id "mauve" ])) "There should not have been a node found" + } + test "fails with a ParentNode with no children with a matching ID" { + Expect.isNone + (RenderFragment.findIdNode "green" (p [] [ str "howdy"; span [] [ str "huh" ] ])) + "There should not have been a node found" + } + test "succeeds with a VoidElement with a matching ID" { + let leNode = hr [ _id "groovy" ] + let foundNode = RenderFragment.findIdNode "groovy" leNode + Expect.isSome foundNode "There should have been a node found" + isSame leNode foundNode.Value "The node should have been the same object" + } + test "succeeds with a ParentNode with a child with a matching ID" { + let leNode = span [ _id "its-me" ] [ str "Mario" ] + let foundNode = + RenderFragment.findIdNode "its-me" (p [] [ str "test"; str "again"; leNode; str "un mas" ]) + Expect.isSome foundNode "There should have been a node found" + isSame leNode foundNode.Value "The node should have been the same object" + } + ] + + /// Generate a message if the requested ID node is not found + let nodeNotFound (nodeId : string) = + $"– ID {nodeId} not found –" + + testList "AsString" [ + testList "htmlFromNodes" [ + test "succeeds when an ID is matched" { + let html = + RenderFragment.AsString.htmlFromNodes "needle" + [ p [] [] + p [ _id "haystack" ] [ str "hay"; span [ _id "needle" ] [ str "ouch" ]; str "hay" ] + ] + Expect.equal html """ouch""" "HTML is incorrect" + } + test "fails when an ID is not matched" { + Expect.equal + (RenderFragment.AsString.htmlFromNodes "oops" []) (nodeNotFound "oops") "HTML is incorrect" + } + ] + testList "htmlFromNode" [ + test "succeeds when ID is matched at top level" { + let html = RenderFragment.AsString.htmlFromNode "wow" (p [ _id "wow" ] [ str "found it" ]) + Expect.equal html """

      found it

      """ "HTML is incorrect" + } + test "succeeds when ID is matched in child element" { + let html = + div [] [ p [] [ str "not it" ]; p [ _id "hey" ] [ str "ta-da" ]] + |> RenderFragment.AsString.htmlFromNode "hey" + Expect.equal html """

      ta-da

      """ "HTML is incorrect" + } + test "fails when an ID is not matched" { + Expect.equal + (RenderFragment.AsString.htmlFromNode "me" (hr [])) (nodeNotFound "me") "HTML is incorrect" + } + ] + ] + testList "AsBytes" [ + + /// Alias for UTF-8 encoding + let utf8 = Encoding.UTF8 + + testList "htmlFromNodes" [ + test "succeeds when an ID is matched" { + let bytes = + RenderFragment.AsBytes.htmlFromNodes "found" + [ p [] [] + p [ _id "not-it" ] [ str "nope"; span [ _id "found" ] [ str "boo" ]; str "nope" ] + ] + Expect.equal bytes (utf8.GetBytes """boo""") "HTML bytes are incorrect" + } + test "fails when an ID is not matched" { + Expect.equal + (RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes(nodeNotFound "whiff")) + "HTML bytes are incorrect" + } + ] + testList "htmlFromNode" [ + test "succeeds when ID is matched at top level" { + let bytes = RenderFragment.AsBytes.htmlFromNode "first" (p [ _id "first" ] [ str "!!!" ]) + Expect.equal bytes (utf8.GetBytes """

      !!!

      """) "HTML bytes are incorrect" + } + test "succeeds when ID is matched in child element" { + let bytes = + div [] [ p [] [ str "not me" ]; p [ _id "child" ] [ str "node" ]] + |> RenderFragment.AsBytes.htmlFromNode "child" + Expect.equal bytes (utf8.GetBytes """

      node

      """) "HTML bytes are incorrect" + } + test "fails when an ID is not matched" { + Expect.equal + (RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes(nodeNotFound "foo")) + "HTML bytes are incorrect" + } + ] + ] + testList "IntoStringBuilder" [ + testList "htmlFromNodes" [ + test "succeeds when an ID is matched" { + let sb = StringBuilder() + RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me" + [ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]] + Expect.equal (string sb) """;)""" "HTML is incorrect" + } + test "fails when an ID is not matched" { + let sb = StringBuilder() + RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" [] + Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect" + } + ] + testList "htmlFromNode" [ + test "succeeds when ID is matched at top level" { + let sb = StringBuilder() + RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ]) + Expect.equal (string sb) """

      pinnacle

      """ "HTML is incorrect" + } + test "succeeds when ID is matched in child element" { + let sb = StringBuilder() + div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]] + |> RenderFragment.IntoStringBuilder.htmlFromNode sb "it" + Expect.equal (string sb) """

      is here

      """ "HTML is incorrect" + } + test "fails when an ID is not matched" { + let sb = StringBuilder() + RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr []) + Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect" + } + ] + ] + ] + +#nowarn 44 // Obsolete events still have tests + +let hxEventObs = + testList "HxEvent (Obsolete)" [ + testList "AfterOnLoad" [ + test "ToString succeeds" { + Expect.equal (string AfterOnLoad) "afterOnLoad" "AfterOnLoad event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal (AfterOnLoad.ToHxOnString()) "after:init" "AfterOnLoad hx-on event name not correct" + } + ] + testList "AfterProcessNode" [ + test "ToString succeeds" { + Expect.equal (string AfterProcessNode) "afterProcessNode" "AfterProcessNode event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal + (AfterProcessNode.ToHxOnString()) "after:init" "AfterProcessNode hx-on event name not correct" + } + ] + testList "AfterSettle" [ + test "ToString succeeds" { + Expect.equal (string AfterSettle) "afterSettle" "AfterSettle event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal (AfterSettle.ToHxOnString()) "after:swap" "AfterSettle hx-on event name not correct" + } + ] + testList "BeforeCleanupElement" [ + test "ToString succeeds" { + Expect.equal + (string BeforeCleanupElement) "beforeCleanupElement" "BeforeCleanupElement event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal + (BeforeCleanupElement.ToHxOnString()) + "before:cleanup" + "BeforeCleanupElement hx-on event name not correct" + } + ] + testList "BeforeHistorySave" [ + test "ToString succeeds" { + Expect.equal (string BeforeHistorySave) "beforeHistorySave" "BeforeHistorySave event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal + (BeforeHistorySave.ToHxOnString()) + "before:history:update" + "BeforeHistorySave hx-on event name not correct" + } + ] + testList "BeforeOnLoad" [ + test "ToString succeeds" { + Expect.equal (string BeforeOnLoad) "beforeOnLoad" "BeforeOnLoad event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal (BeforeOnLoad.ToHxOnString()) "before:init" "BeforeOnLoad hx-on event name not correct" + } + ] + testList "BeforeProcessNode" [ + test "ToString succeeds" { + Expect.equal (string BeforeProcessNode) "beforeProcessNode" "BeforeProcessNode event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal + (BeforeProcessNode.ToHxOnString()) "before:init" "BeforeProcessNode hx-on event name not correct" + } + ] + testList "BeforeSend" [ + test "ToString succeeds" { + Expect.equal (string BeforeSend) "beforeSend" "BeforeSend event name not correct" + } + test "ToHxOnString succeeds" { + Expect.equal (BeforeSend.ToHxOnString()) "before:request" "BeforeSend hx-on event name not correct" + } + ] testList "HistoryCacheError" [ test "ToString succeeds" { Expect.equal (string HistoryCacheError) "historyCacheError" "HistoryCacheError event name not correct" @@ -560,25 +1172,9 @@ let hxEvent = } ] ] -#warn 44 // Obsolete should be flagged from here on - -/// Tests for the HxHeaders module -let hxHeaders = - testList "HxHeaders" [ - testList "From" [ - test "succeeds with an empty list" { - Expect.equal (HxHeaders.From []) "{ }" "Empty headers not correct" - } - test "succeeds and escapes quotes" { - Expect.equal - (HxHeaders.From [ "test", "one two three"; "again", """four "five" six""" ]) - """{ "test": "one two three", "again": "four \"five\" six" }""" "Headers not correct" - } - ] - ] /// Tests for the HxParams module -let hxParams = +let hxParamsObs = testList "HxParams" [ test "All is correct" { Expect.equal HxParams.All "*" "All is not correct" @@ -610,9 +1206,9 @@ let hxParams = ] ] -/// Tests for the HxRequest module -let hxRequest = - testList "HxRequest" [ +/// Tests for the HxConfig module +let hxRequestObs = + testList "HxRequest (Obsolete)" [ testList "Configure" [ test "succeeds with an empty list" { Expect.equal (HxRequest.Configure []) "{ }" "Configure with empty list incorrect" @@ -651,544 +1247,36 @@ let hxRequest = ] ] -/// Tests for the HxSync module -let hxSync = - testList "HxSync" [ - test "Drop is correct" { - Expect.equal HxSync.Drop "drop" "Drop is incorrect" - } - test "Abort is correct" { - Expect.equal HxSync.Abort "abort" "Abort is incorrect" - } - test "Replace is correct" { - Expect.equal HxSync.Replace "replace" "Replace is incorrect" - } - test "Queue is correct" { - Expect.equal HxSync.Queue "queue" "Queue is incorrect" - } - test "QueueFirst is correct" { - Expect.equal HxSync.QueueFirst "queue first" "QueueFirst is incorrect" - } - test "QueueLast is correct" { - Expect.equal HxSync.QueueLast "queue last" "QueueLast is incorrect" - } - test "QueueAll is correct" { - Expect.equal HxSync.QueueAll "queue all" "QueueAll is incorrect" - } - ] - -/// Tests for the HxTrigger module -let hxTrigger = - testList "HxTrigger" [ - test "Click is correct" { - Expect.equal HxTrigger.Click "click" "Click is incorrect" - } - test "Load is correct" { - Expect.equal HxTrigger.Load "load" "Load is incorrect" - } - test "Revealed is correct" { - Expect.equal HxTrigger.Revealed "revealed" "Revealed is incorrect" - } - test "Every succeeds" { - Expect.equal (HxTrigger.Every "3s") "every 3s" "Every is incorrect" - } - testList "Filter" [ - test "Alt succeeds" { - Expect.equal (HxTrigger.Filter.Alt HxTrigger.Click) "click[altKey]" "Alt filter incorrect" - } - test "Ctrl succeeds" { - Expect.equal (HxTrigger.Filter.Ctrl HxTrigger.Click) "click[ctrlKey]" "Ctrl filter incorrect" - } - test "Shift succeeds" { - Expect.equal (HxTrigger.Filter.Shift HxTrigger.Click) "click[shiftKey]" "Shift filter incorrect" - } - test "CtrlAlt succeeds" { - Expect.equal - (HxTrigger.Filter.CtrlAlt HxTrigger.Click) "click[ctrlKey&&altKey]" "Ctrl+Alt filter incorrect" - } - test "CtrlShift succeeds" { - Expect.equal - (HxTrigger.Filter.CtrlShift HxTrigger.Click) "click[ctrlKey&&shiftKey]" - "Ctrl+Shift filter incorrect" - } - test "CtrlAltShift succeeds" { - Expect.equal - (HxTrigger.Filter.CtrlAltShift HxTrigger.Click) "click[ctrlKey&&altKey&&shiftKey]" - "Ctrl+Alt+Shift filter incorrect" - } - test "AltShift succeeds" { - Expect.equal - (HxTrigger.Filter.AltShift HxTrigger.Click) "click[altKey&&shiftKey]" "Alt+Shift filter incorrect" - } - ] - testList "Once" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Once "") "once" "Once modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Once "click") "click once" "Once modifier incorrect" - } - ] - testList "Changed" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Changed "") "changed" "Changed modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Changed "click") "click changed" "Changed modifier incorrect" - } - ] - testList "Delay" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Delay "1s" "") "delay:1s" "Delay modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Delay "2s" "click") "click delay:2s" "Delay modifier incorrect" - } - ] - testList "Throttle" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Throttle "4s" "") "throttle:4s" "Throttle modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Throttle "7s" "click") "click throttle:7s" "Throttle modifier incorrect" - } - ] - testList "From" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.From ".nav" "") "from:.nav" "From modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.From "#somewhere" "click") "click from:#somewhere" "From modifier incorrect" - } - ] - testList "FromDocument" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.FromDocument "") "from:document" "FromDocument modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.FromDocument "click") "click from:document" "FromDocument modifier incorrect" - } - ] - testList "FromWindow" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.FromWindow "") "from:window" "FromWindow modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.FromWindow "click") "click from:window" "FromWindow modifier incorrect" - } - ] - testList "FromClosest" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.FromClosest "div" "") "from:closest div" "FromClosest modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.FromClosest "p" "click") "click from:closest p" "FromClosest modifier incorrect" - } - ] - testList "FromFind" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.FromFind "li" "") "from:find li" "FromFind modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.FromFind ".spot" "click") "click from:find .spot" "FromFind modifier incorrect" - } - ] - testList "Target" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Target "main" "") "target:main" "Target modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Target "footer" "click") "click target:footer" "Target modifier incorrect" - } - ] - testList "Consume" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Consume "") "consume" "Consume modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Consume "click") "click consume" "Consume modifier incorrect" - } - ] - testList "Queue" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.Queue "abc" "") "queue:abc" "Queue modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect" - } - test "succeeds when no type of queueing is given" { - Expect.equal (HxTrigger.Queue "" "blur") "blur queue" "Queue modifier incorrect" - } - ] - testList "QueueFirst" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.QueueFirst "") "queue:first" "QueueFirst modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.QueueFirst "click") "click queue:first" "QueueFirst modifier incorrect" - } - ] - testList "QueueLast" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.QueueLast "") "queue:last" "QueueLast modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.QueueLast "click") "click queue:last" "QueueLast modifier incorrect" - } - ] - testList "QueueAll" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.QueueAll "") "queue:all" "QueueAll modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.QueueAll "click") "click queue:all" "QueueAll modifier incorrect" - } - ] - testList "QueueNone" [ - test "succeeds when it is the first modifier" { - Expect.equal (HxTrigger.QueueNone "") "queue:none" "QueueNone modifier incorrect" - } - test "succeeds when it is not the first modifier" { - Expect.equal (HxTrigger.QueueNone "click") "click queue:none" "QueueNone modifier incorrect" - } - ] - ] - -/// Tests for the HxVals module -let hxVals = - testList "HxVals" [ - testList "From" [ - test "succeeds with an empty list" { - Expect.equal (HxVals.From []) "{ }" "From with an empty list is incorrect" - } - test "succeeds and escapes quotes" { - Expect.equal - (HxVals.From [ "test", """a "b" c"""; "2", "d e f" ]) - """{ "test": "a \"b\" c", "2": "d e f" }""" "From value is incorrect" - } - ] - ] - -/// Pipe-able assertion for a rendered node -let shouldRender expected node = - Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect" - -#nowarn 44 // Still have tests for obsolete attributes -/// Tests for the HtmxAttrs module -let attributes = - testList "Attributes" [ - - test "_hxAction succeeds" { - form [ _hxAction "/endpoint/1" ] [] |> shouldRender """
      """ - } - test "_hxBoost succeeds" { - div [ _hxBoost ] [] |> shouldRender """
      """ - } - test "_hxConfig succeeds" { - body [ _hxConfig [ ("test", "that") ] ] [] - |> shouldRender """""" - } - test "_hxConfirm succeeds" { - button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """""" - } - test "_hxDelete succeeds" { - span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """""" - } - test "_hxDisable succeeds" { - p [ _hxDisable "#that" ] [] |> shouldRender """

      """ - } +let attributesObs = + testList "Attributes (Obsolete)" [ test "_hxDisabledElt succeeds" { button [ _hxDisabledElt "this" ] [] |> shouldRender """""" } test "_hxDisinherit succeeds" { strong [ _hxDisinherit "*" ] [] |> shouldRender """""" } - test "_hxEncoding succeeds" { - form [ _hxEncoding "utf-7" ] [] |> shouldRender """
      """ - } test "_hxExt succeeds" { section [ _hxExt "extendme" ] [] |> shouldRender """
      """ } - test "_hxGet succeeds" { - article [ _hxGet "/the-text" ] [] |> shouldRender """
      """ - } - test "_hxHeaders succeeds" { - figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] [] - |> shouldRender """
      """ - } test "_hxHistory succeeds" { span [ _hxHistory false ] [] |> shouldRender """""" } test "_hxHistoryElt succeeds" { table [ _hxHistoryElt ] [] |> shouldRender """
      """ } - test "_hxIgnore succeeds" { - span [ _hxIgnore ] [] |> shouldRender """""" - } - test "_hxInclude succeeds" { - a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """""" - } - test "_hxIndicator succeeds" { - aside [ _hxIndicator "#spinner" ] [] |> shouldRender """""" - } - test "_hxMethod succeeds" { - div [ _hxMethod System.Net.Http.HttpMethod.Options ] [] - |> shouldRender """
      """ - } - test "_hxNoBoost succeeds" { - td [ _hxNoBoost ] [] |> shouldRender """""" - } - test "_hxOnEvent succeeds" { - a [ _hxOnEvent "click" "doThis()" ] [] |> shouldRender """""" - } - test "_hxOnHxEvent succeeds" { - strong [ _hxOnHxEvent BeforeSwap "changeStuff()" ] [] - |> shouldRender """""" - } test "_hxParams succeeds" { br [ _hxParams "[p1,p2]" ] |> shouldRender """
      """ } - test "_hxPatch succeeds" { - div [ _hxPatch "/arrrgh" ] [] |> shouldRender """
      """ - } - test "_hxPost succeeds" { - hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
      """ - } - test "_hxPreserve succeeds" { - img [ _hxPreserve ] |> shouldRender """""" - } test "_hxPrompt succeeds" { strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """""" } - test "_hxPushUrl succeeds" { - dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """
      """ - } - test "_hxPut succeeds" { - s [ _hxPut "/take-this" ] [] |> shouldRender """""" - } - test "_hxReplaceUrl succeeds" { - p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """

      """ - } test "_hxRequest succeeds" { u [ _hxRequest "noHeaders" ] [] |> shouldRender """""" } - test "_hxSelect succeeds" { - nav [ _hxSelect "#navbar" ] [] |> shouldRender """""" - } - test "_hxSelectOob succeeds" { - section [ _hxSelectOob "#oob" ] [] |> shouldRender """
      """ - } - test "_hxSwap succeeds" { - del [ _hxSwap "innerHTML" ] [] |> shouldRender """""" - } - test "_hxSwapWithTransition succeeds" { - del [ _hxSwapWithTransition "innerHTML" ] [] - |> shouldRender """""" - } - test "_hxSwapOob succeeds" { - li [ _hxSwapOob "true" ] [] |> shouldRender """
    • """ - } - test "_hxSync succeeds" { - nav [ _hxSync "closest form" HxSync.Abort ] [] - |> shouldRender """""" - } - test "_hxTarget succeeds" { - header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
      """ - } - test "_hxTrigger succeeds" { - figcaption [ _hxTrigger "load" ] [] |> shouldRender """
      """ - } - test "_hxValidate succeeds" { - form [ _hxValidate ] [] |> shouldRender """
      """ - } - test "_hxVals succeeds" { - dt [ _hxVals """{ "extra": "values" }""" ] [] - |> shouldRender """
      """ - } - test "_sseConnect succeeds" { - div [ _sseConnect "/gps/sse" ] [] |> shouldRender """
      """ - } - test "_sseSwap succeeds" { - ul [ _sseSwap "sseMessageName" ] [] |> shouldRender """
        """ - } - ] -#warn 44 // Obsolete should be flagged from here on - -open Giraffe.Htmx.Common - -/// Tests for the Script module -let script = - testList "Script" [ - test "local succeeds" { - let html = RenderView.AsString.htmlNode Script.local - Expect.equal - html - $"""""" - "Local script tag is incorrect" - } - test "cdnMinified succeeds" { - let html = RenderView.AsString.htmlNode Script.cdnMinified - Expect.equal - html - $"""""" - "CDN minified script tag is incorrect" - } - test "cdnUnminified succeeds" { - let html = RenderView.AsString.htmlNode Script.cdnUnminified - Expect.equal - html - $"""""" - "CDN unminified script tag is incorrect" - } ] -open System.Text - -/// Tests for the RenderFragment module -let renderFragment = - testList "RenderFragment" [ - - /// Validate that the two object references are the same object - let isSame obj1 obj2 message = - Expect.isTrue (obj.ReferenceEquals(obj1, obj2)) message - - testList "findIdNode" [ - test "fails with a Text node" { - Expect.isNone (RenderFragment.findIdNode "blue" (Text "")) "There should not have been a node found" - } - test "fails with a VoidElement without a matching ID" { - Expect.isNone - (RenderFragment.findIdNode "purple" (br [ _id "mauve" ])) "There should not have been a node found" - } - test "fails with a ParentNode with no children with a matching ID" { - Expect.isNone - (RenderFragment.findIdNode "green" (p [] [ str "howdy"; span [] [ str "huh" ] ])) - "There should not have been a node found" - } - test "succeeds with a VoidElement with a matching ID" { - let leNode = hr [ _id "groovy" ] - let foundNode = RenderFragment.findIdNode "groovy" leNode - Expect.isSome foundNode "There should have been a node found" - isSame leNode foundNode.Value "The node should have been the same object" - } - test "succeeds with a ParentNode with a child with a matching ID" { - let leNode = span [ _id "its-me" ] [ str "Mario" ] - let foundNode = - RenderFragment.findIdNode "its-me" (p [] [ str "test"; str "again"; leNode; str "un mas" ]) - Expect.isSome foundNode "There should have been a node found" - isSame leNode foundNode.Value "The node should have been the same object" - } - ] - - /// Generate a message if the requested ID node is not found - let nodeNotFound (nodeId : string) = - $"– ID {nodeId} not found –" - - testList "AsString" [ - testList "htmlFromNodes" [ - test "succeeds when an ID is matched" { - let html = - RenderFragment.AsString.htmlFromNodes "needle" - [ p [] [] - p [ _id "haystack" ] [ str "hay"; span [ _id "needle" ] [ str "ouch" ]; str "hay" ] - ] - Expect.equal html """ouch""" "HTML is incorrect" - } - test "fails when an ID is not matched" { - Expect.equal - (RenderFragment.AsString.htmlFromNodes "oops" []) (nodeNotFound "oops") "HTML is incorrect" - } - ] - testList "htmlFromNode" [ - test "succeeds when ID is matched at top level" { - let html = RenderFragment.AsString.htmlFromNode "wow" (p [ _id "wow" ] [ str "found it" ]) - Expect.equal html """

        found it

        """ "HTML is incorrect" - } - test "succeeds when ID is matched in child element" { - let html = - div [] [ p [] [ str "not it" ]; p [ _id "hey" ] [ str "ta-da" ]] - |> RenderFragment.AsString.htmlFromNode "hey" - Expect.equal html """

        ta-da

        """ "HTML is incorrect" - } - test "fails when an ID is not matched" { - Expect.equal - (RenderFragment.AsString.htmlFromNode "me" (hr [])) (nodeNotFound "me") "HTML is incorrect" - } - ] - ] - testList "AsBytes" [ - - /// Alias for UTF-8 encoding - let utf8 = Encoding.UTF8 - - testList "htmlFromNodes" [ - test "succeeds when an ID is matched" { - let bytes = - RenderFragment.AsBytes.htmlFromNodes "found" - [ p [] [] - p [ _id "not-it" ] [ str "nope"; span [ _id "found" ] [ str "boo" ]; str "nope" ] - ] - Expect.equal bytes (utf8.GetBytes """boo""") "HTML bytes are incorrect" - } - test "fails when an ID is not matched" { - Expect.equal - (RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes(nodeNotFound "whiff")) - "HTML bytes are incorrect" - } - ] - testList "htmlFromNode" [ - test "succeeds when ID is matched at top level" { - let bytes = RenderFragment.AsBytes.htmlFromNode "first" (p [ _id "first" ] [ str "!!!" ]) - Expect.equal bytes (utf8.GetBytes """

        !!!

        """) "HTML bytes are incorrect" - } - test "succeeds when ID is matched in child element" { - let bytes = - div [] [ p [] [ str "not me" ]; p [ _id "child" ] [ str "node" ]] - |> RenderFragment.AsBytes.htmlFromNode "child" - Expect.equal bytes (utf8.GetBytes """

        node

        """) "HTML bytes are incorrect" - } - test "fails when an ID is not matched" { - Expect.equal - (RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes(nodeNotFound "foo")) - "HTML bytes are incorrect" - } - ] - ] - testList "IntoStringBuilder" [ - testList "htmlFromNodes" [ - test "succeeds when an ID is matched" { - let sb = StringBuilder() - RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me" - [ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]] - Expect.equal (string sb) """;)""" "HTML is incorrect" - } - test "fails when an ID is not matched" { - let sb = StringBuilder() - RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" [] - Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect" - } - ] - testList "htmlFromNode" [ - test "succeeds when ID is matched at top level" { - let sb = StringBuilder() - RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ]) - Expect.equal (string sb) """

        pinnacle

        """ "HTML is incorrect" - } - test "succeeds when ID is matched in child element" { - let sb = StringBuilder() - div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]] - |> RenderFragment.IntoStringBuilder.htmlFromNode sb "it" - Expect.equal (string sb) """

        is here

        """ "HTML is incorrect" - } - test "fails when an ID is not matched" { - let sb = StringBuilder() - RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr []) - Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect" - } - ] - ] - ] +let obsolete = testList "Obsolete" [ hxEventObs; hxParamsObs; hxRequestObs; attributesObs ] /// All tests in this module let allTests = @@ -1196,12 +1284,13 @@ let allTests = hxEncoding hxEvent hxHeaders - hxParams - hxRequest hxSync hxTrigger hxVals attributes + hxModifiers + hxTags script renderFragment + obsolete ] diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index 77614dd..c4a6e03 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -1,7 +1,41 @@ /// Types and functions supporting htmx attributes in Giraffe View Engine module Giraffe.ViewEngine.Htmx -open System.Net.Http +/// Helpers to define hx-config attribute values +/// Documentation +[] +module HxConfig = + + open Giraffe.Htmx.Common + + /// Configure the request with various options + /// The options to configure + /// A string with the configured options + let Configure (opts: string list) = + opts + |> String.concat ", " + |> sprintf "{ %s }" + + /// Set a timeout (in milliseconds) + /// The milliseconds for the request timeout + /// A string with the configured request timeout + let Timeout (ms: int) = + $"\"timeout\": {ms}" + + /// Include or exclude credentials from the request + /// true if credentials should be sent, false if not + /// A string with the configured credential options + let Credentials send = + (toLowerBool >> sprintf "\"credentials\": %s") send + + /// Exclude or include headers from the request + /// + /// true if no headers should be sent; false if headers should be sent + /// + /// A string with the configured header options + let NoHeaders exclude = + (toLowerBool >> sprintf "\"noHeaders\": %s") exclude + /// Valid values for the hx-encoding attribute [] @@ -289,6 +323,7 @@ module HxHeaders = /// Values / helpers for the hx-params attribute /// Documentation [] +[] module HxParams = /// Include all parameters @@ -315,6 +350,7 @@ module HxParams = /// Helpers to define hx-request attribute values /// Documentation [] +[] module HxRequest = open Giraffe.Htmx.Common @@ -564,6 +600,7 @@ module HxVals = let From = Giraffe.Htmx.Common.toJson +open System.Net.Http open Giraffe.Htmx /// Attributes and flags for htmx @@ -913,10 +950,10 @@ module HxModifiers = | KeyValue (name, value) -> attr $"{name}:append" value | Boolean name -> flag $"{name}:append" - /// Explicity inherit the value for this attribute + /// Explicitly propagate inheritance for the value for this attribute /// The attribute whose value should be inherited /// The given attribute with :inherited applied - let hxInherit attrib = + let hxInherited attrib = match attrib with | KeyValue (name, value) -> attr $"{name}:inherited" value | Boolean name -> flag $"{name}:inherited" diff --git a/src/ViewEngine.Htmx/README.md b/src/ViewEngine.Htmx/README.md index 1ed050f..bea9e3b 100644 --- a/src/ViewEngine.Htmx/README.md +++ b/src/ViewEngine.Htmx/README.md @@ -2,9 +2,13 @@ This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine. -**htmx version: 2.0.8** +**htmx version: 4.0.0-alpha6** -_Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_ +_Upgrading from v2.x: see [the migration guide](https://four.htmx.org/migration-guide-htmx-4/) for changes, which are plentiful. htmx switches from `XMLHTTPRequest` to `fetch`, and many changes are related to the new event cycle._ + +_Inheritance is now explicit; to have an attribute's value inherited to its children, wrap the attribute in `hxInherited` (ex. `hxInherited (_hxTarget "#main")`). Values can be appended to inherited values as well using the `hxAppend` modifier._ + +_Several constructs have been marked obsolete in this release, and will be removed from the first production release of v4. With the exception of `_hxDisable`, though (which now functions as the deprecated `_hxDisabledElt` did), this should not introduce compile errors. Rather, this package will raise warnings for deprecated constructs, along with suggestions of what to use instead._ ### Setup @@ -21,10 +25,11 @@ let autoload = ``` Support modules include: +- `HxConfig` _(new in v4)_ - `HxEncoding` - `HxHeaders` -- `HxParams` -- `HxRequest` +- ~~`HxParams`~~ _(removed in v4)_ +- ~~`HxRequest`~~ _(renamed to `HxConfig`)_ - `HxSwap` (requires `open Giraffe.Htmx`) - `HxTrigger` - `HxVals`