From f3fb675b1f50b336b331e4ce03f0278d07347852 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 30 Oct 2021 10:39:43 -0400 Subject: [PATCH 1/6] WIP on test projects --- .gitignore | 3 ++ src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj | 28 +++++++++++++++++++ src/Htmx.Tests/Program.fs | 1 + src/Htmx.Tests/Tests.fs | 8 ++++++ .../Giraffe.ViewEngine.Htmx.Tests.fsproj | 28 +++++++++++++++++++ src/ViewEngine.Htmx.Tests/Program.fs | 1 + src/ViewEngine.Htmx.Tests/Tests.fs | 8 ++++++ 7 files changed, 77 insertions(+) create mode 100644 src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj create mode 100644 src/Htmx.Tests/Program.fs create mode 100644 src/Htmx.Tests/Tests.fs create mode 100644 src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj create mode 100644 src/ViewEngine.Htmx.Tests/Program.fs create mode 100644 src/ViewEngine.Htmx.Tests/Tests.fs diff --git a/.gitignore b/.gitignore index f181d71..8462afe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ **/bin **/obj + +.fake +.ionide \ No newline at end of file diff --git a/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj b/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj new file mode 100644 index 0000000..768b74c --- /dev/null +++ b/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj @@ -0,0 +1,28 @@ + + + + net6.0 + + false + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/src/Htmx.Tests/Program.fs b/src/Htmx.Tests/Program.fs new file mode 100644 index 0000000..fdc31cd --- /dev/null +++ b/src/Htmx.Tests/Program.fs @@ -0,0 +1 @@ +module Program = let [] main _ = 0 diff --git a/src/Htmx.Tests/Tests.fs b/src/Htmx.Tests/Tests.fs new file mode 100644 index 0000000..b908e37 --- /dev/null +++ b/src/Htmx.Tests/Tests.fs @@ -0,0 +1,8 @@ +module Tests + +open System +open Xunit + +[] +let ``My test`` () = + Assert.True(true) diff --git a/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj b/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj new file mode 100644 index 0000000..768b74c --- /dev/null +++ b/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj @@ -0,0 +1,28 @@ + + + + net6.0 + + false + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/src/ViewEngine.Htmx.Tests/Program.fs b/src/ViewEngine.Htmx.Tests/Program.fs new file mode 100644 index 0000000..fdc31cd --- /dev/null +++ b/src/ViewEngine.Htmx.Tests/Program.fs @@ -0,0 +1 @@ +module Program = let [] main _ = 0 diff --git a/src/ViewEngine.Htmx.Tests/Tests.fs b/src/ViewEngine.Htmx.Tests/Tests.fs new file mode 100644 index 0000000..b908e37 --- /dev/null +++ b/src/ViewEngine.Htmx.Tests/Tests.fs @@ -0,0 +1,8 @@ +module Tests + +open System +open Xunit + +[] +let ``My test`` () = + Assert.True(true) -- 2.45.1 From f0aa1a41d7fb225f885a05fc197b8f4721b018c6 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 30 Oct 2021 13:02:10 -0400 Subject: [PATCH 2/6] Add tests for attribute rendering --- .gitignore | 4 +- .../Giraffe.ViewEngine.Htmx.Tests.fsproj | 5 + src/ViewEngine.Htmx.Tests/Tests.fs | 127 +++++++++++++++++- src/ViewEngine.Htmx/Htmx.fs | 14 +- 4 files changed, 138 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 8462afe..1592c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ **/obj .fake -.ionide \ No newline at end of file +.ionide +.idea +*.user diff --git a/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj b/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj index 768b74c..2d275f3 100644 --- a/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj +++ b/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj @@ -13,6 +13,7 @@ + @@ -25,4 +26,8 @@ + + + + diff --git a/src/ViewEngine.Htmx.Tests/Tests.fs b/src/ViewEngine.Htmx.Tests/Tests.fs index b908e37..f2f5ba5 100644 --- a/src/ViewEngine.Htmx.Tests/Tests.fs +++ b/src/ViewEngine.Htmx.Tests/Tests.fs @@ -1,8 +1,125 @@ -module Tests +module Giraffe.ViewEngine.Htmx.Tests -open System +open Giraffe.ViewEngine open Xunit -[] -let ``My test`` () = - Assert.True(true) +/// Tests for the HtmxAttrs module +module Attributes = + + /// Pipe-able assertion for a rendered node + let shouldRender expected node = Assert.Equal (expected, RenderView.AsString.htmlNode node) + + [] + let ``_hxBoost succeeds`` () = + div [ _hxBoost ] [] |> shouldRender """
""" + + [] + let ``_hxConfirm succeeds`` () = + button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """""" + + [] + let ``_hxDelete succeeds`` () = + span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """""" + + [] + let ``_hxDisable succeeds`` () = + p [ _hxDisable ] [] |> shouldRender """

""" + + [] + let ``_hxEncoding succeeds`` () = + form [ _hxEncoding "utf-7" ] [] |> shouldRender """
""" + + [] + let ``_hxExt succeeds`` () = + section [ _hxExt "extendme" ] [] |> shouldRender """
""" + + [] + let ``_hxGet succeeds`` () = + article [ _hxGet "/the-text" ] [] |> shouldRender """
""" + + [] + let ``_hxHeaders succeeds`` () = + figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] [] + |> shouldRender """
""" + + [] + let ``_hxHistoryElt succeeds`` () = + table [ _hxHistoryElt ] [] |> shouldRender """
""" + + [] + let ``_hxInclude succeeds`` () = + a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """""" + + [] + let ``_hxIndicator succeeds`` () = + aside [ _hxIndicator "#spinner" ] [] |> shouldRender """""" + + [] + let ``_hxNoBoost succeeds`` () = + td [ _hxNoBoost ] [] |> shouldRender """""" + + [] + let ``_hxParams succeeds`` () = + br [ _hxParams "[p1,p2]" ] |> shouldRender """
""" + + [] + let ``_hxPatch succeeds`` () = + div [ _hxPatch "/arrrgh" ] [] |> shouldRender """
""" + + [] + let ``_hxPost succeeds`` () = + hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
""" + + [] + let ``_hxPreserve succeeds`` () = + img [ _hxPreserve ] |> shouldRender """""" + + [] + let ``_hxPrompt succeeds`` () = + strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """""" + + [] + let ``_hxPushUrl succeeds`` () = + dl [ _hxPushUrl ] [] |> shouldRender """
""" + + [] + let ``_hxPut succeeds`` () = + s [ _hxPut "/take-this" ] [] |> shouldRender """""" + + [] + let ``_hxRequest succeeds`` () = + u [ _hxRequest "noHeaders" ] [] |> shouldRender """""" + + [] + let ``_hxSelect succeeds`` () = + nav [ _hxSelect "#navbar" ] [] |> shouldRender """""" + + [] + let ``_hxSse succeeds`` () = + footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """
""" + + [] + let ``_hxSwap succeeds`` () = + del [ _hxSwap "innerHTML" ] [] |> shouldRender """""" + + [] + let ``_hxSwapOob succeeds`` () = + li [ _hxSwapOob "true" ] [] |> shouldRender """
  • """ + + [] + let ``_hxTarget succeeds`` () = + header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
    """ + + [] + let ``_hxTrigger succeeds`` () = + figcaption [ _hxTrigger "load" ] [] |> shouldRender """
    """ + + [] + let ``_hxVals succeeds`` () = + dt [ _hxVals """{ "extra": "values" }""" ] [] + |> shouldRender """
    """ + + [] + let ``_hxWs succeeds`` () = + ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """
      """ + \ No newline at end of file diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index 5c8aa40..63b71de 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -50,8 +50,8 @@ module HxTrigger = match trigger.Contains "[" with | true -> let parts = trigger.Split ('[', ']') - sprintf "%s[%s&&%s]" parts.[0] parts.[1] filter - | false -> sprintf "%s[%s]" trigger filter + $"{parts.[0]}[{parts.[1]}&&{filter}]" + | false -> $"{trigger}[{filter}]" /// Trigger the event on a click let Click = "click" /// Trigger the event on page load @@ -81,9 +81,9 @@ module HxTrigger = /// Attributes and flags for HTMX [] module HtmxAttrs = - /// Progressively enhances anchors and forms to use AJAX requests + /// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false) let _hxBoost = attr "hx-boost" "true" - /// Shows a confim() dialog before issuing a request + /// Shows a confirm() dialog before issuing a request let _hxConfirm = attr "hx-confirm" /// Issues a DELETE to the specified URL let _hxDelete = attr "hx-delete" @@ -103,6 +103,8 @@ module HtmxAttrs = let _hxInclude = attr "hx-include" /// The element to put the htmx-request class on during the AJAX request let _hxIndicator = attr "hx-indicator" + /// Overrides a previous `hx-boost` + let _hxNoBoost = attr "hx-boost" "false" /// Filters the parameters that will be submitted with a request let _hxParams = attr "hx-params" /// Issues a PATCH to the specified URL @@ -123,10 +125,10 @@ module HtmxAttrs = let _hxSelect = attr "hx-select" /// Establishes and listens to Server Sent Event (SSE) sources for events let _hxSse = attr "hx-sse" - /// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target - let _hxSwapOob = attr "hx-swap-oob" /// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd') let _hxSwap = attr "hx-swap" + /// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target + let _hxSwapOob = attr "hx-swap-oob" /// Specifies the target element to be swapped let _hxTarget = attr "hx-target" /// Specifies the event that triggers the request -- 2.45.1 From 128b242ddba9e90a400fca93a97769aa6d53d0ff Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 30 Oct 2021 13:55:41 -0400 Subject: [PATCH 3/6] Finish tests for current view engine code --- src/ViewEngine.Htmx.Tests/Tests.fs | 120 +++++++++++++++++++++++++++++ src/ViewEngine.Htmx/Htmx.fs | 2 +- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/ViewEngine.Htmx.Tests/Tests.fs b/src/ViewEngine.Htmx.Tests/Tests.fs index f2f5ba5..129562e 100644 --- a/src/ViewEngine.Htmx.Tests/Tests.fs +++ b/src/ViewEngine.Htmx.Tests/Tests.fs @@ -3,6 +3,126 @@ module Giraffe.ViewEngine.Htmx.Tests open Giraffe.ViewEngine open Xunit +/// Tests for the HxEncoding module +module Encoding = + + [] + let ``Form is correct`` () = + Assert.Equal ("application/x-www-form-urlencoded", HxEncoding.Form) + + [] + let ``MultipartForm is correct`` () = + Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm) + + +/// Tests for the HxParams module +module Params = + + [] + let ``All is correct`` () = + Assert.Equal ("*", HxParams.All) + + [] + let ``None is correct`` () = + Assert.Equal ("none", HxParams.None) + + [] + let ``With succeeds with empty list`` () = + Assert.Equal ("", HxParams.With []) + + [] + let ``With succeeds with one list item`` () = + Assert.Equal ("boo", HxParams.With [ "boo" ]) + + [] + let ``With succeeds with multiple list items`` () = + Assert.Equal ("foo,bar,baz", HxParams.With [ "foo"; "bar"; "baz" ]) + + [] + let ``Except succeeds with empty list`` () = + Assert.Equal ("not ", HxParams.Except []) + + [] + let ``Except succeeds with one list item`` () = + Assert.Equal ("not that", HxParams.Except [ "that" ]) + + [] + let ``Except succeeds with multiple list items`` () = + Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ]) + + +/// Tests for the HxSwap module +module Swap = + + [] + let ``InnerHtml is correct`` () = + Assert.Equal ("innerHTML", HxSwap.InnerHtml) + + [] + let ``OuterHtml is correct`` () = + Assert.Equal ("outerHTML", HxSwap.OuterHtml) + + [] + let ``BeforeBegin is correct`` () = + Assert.Equal ("beforebegin", HxSwap.BeforeBegin) + + [] + let ``BeforeEnd is correct`` () = + Assert.Equal ("beforeend", HxSwap.BeforeEnd) + + [] + let ``AfterBegin is correct`` () = + Assert.Equal ("afterbegin", HxSwap.AfterBegin) + + [] + let ``AfterEnd is correct`` () = + Assert.Equal ("afterend", HxSwap.AfterEnd) + + [] + let ``None is correct`` () = + Assert.Equal ("none", HxSwap.None) + + +/// Tests for the HxTrigger module +module Trigger = + + [] + let ``Click is correct`` () = + Assert.Equal ("click", HxTrigger.Click) + + [] + let ``Load is correct`` () = + Assert.Equal ("load", HxTrigger.Load) + + [] + let ``Filter.Alt succeeds`` () = + Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click) + + [] + let ``Filter.Ctrl succeeds`` () = + Assert.Equal ("click[ctrlKey]", HxTrigger.Filter.Ctrl HxTrigger.Click) + + [] + let ``Filter.Shift succeeds`` () = + Assert.Equal ("click[shiftKey]", HxTrigger.Filter.Shift HxTrigger.Click) + + [] + let ``Filter.CtrlAlt succeeds`` () = + Assert.Equal ("click[ctrlKey&&altKey]", HxTrigger.Filter.CtrlAlt HxTrigger.Click) + + [] + let ``Filter.CtrlShift succeeds`` () = + Assert.Equal ("click[ctrlKey&&shiftKey]", HxTrigger.Filter.CtrlShift HxTrigger.Click) + + [] + let ``Filter.CtrlAltShift succeeds`` () = + Assert.Equal ("click[ctrlKey&&altKey&&shiftKey]", HxTrigger.Filter.CtrlAltShift HxTrigger.Click) + + [] + let ``Filter.AltShift succeeds`` () = + Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click) + + /// Tests for the HtmxAttrs module module Attributes = diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index 63b71de..c2ed2ee 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -18,7 +18,7 @@ module HxParams = /// Include no parameters let None = "none" /// Include the specified parameters - let With fields = fields |> List.reduce (fun acc it -> $"{acc},{it}") + let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") /// Exclude the specified parameters let Except fields = With fields |> sprintf "not %s" -- 2.45.1 From 9d97e1c7f2597e13be22139a57abc1a6abc98e7e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 30 Oct 2021 14:20:40 -0400 Subject: [PATCH 4/6] WIP on header dict extension tests --- src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj | 6 ++ src/Htmx.Tests/Tests.fs | 75 ++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj b/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj index 768b74c..a9d2f4e 100644 --- a/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj +++ b/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj @@ -13,7 +13,9 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,4 +27,8 @@ + + + + diff --git a/src/Htmx.Tests/Tests.fs b/src/Htmx.Tests/Tests.fs index b908e37..29c002e 100644 --- a/src/Htmx.Tests/Tests.fs +++ b/src/Htmx.Tests/Tests.fs @@ -1,8 +1,75 @@ -module Tests +module Giraffe.Htmx.Tests open System +open Giraffe.Htmx +open Microsoft.AspNetCore.Http +open NSubstitute open Xunit -[] -let ``My test`` () = - Assert.True(true) +/// Tests for the IHeaderDictionary extension properties +module IHeaderDictionaryExtensions = + + [] + let ``HxBoosted succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxBoosted |> Assert.True + + [] + let ``HxBoosted succeeds when the header is present and true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Boosted", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True + Option.get ctx.Request.Headers.HxBoosted |> Assert.True + + [] + let ``HxBoosted succeeds when the header is present and false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Boosted", "false") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True + Option.get ctx.Request.Headers.HxBoosted |> Assert.False + + [] + let ``HxCurrentUrl succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxCurrentUrl |> Assert.True + + [] + let ``HxCurrentUrl succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Current-URL", "http://localhost/test.htm") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxCurrentUrl |> Assert.True + Assert.Equal (Uri "http://localhost/test.htm", Option.get ctx.Request.Headers.HxCurrentUrl) + + [] + let ``HxHistoryRestoreRequest succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + + [] + let ``HxHistoryRestoreRequest succeeds when the header is present and true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-History-Restore-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + + [] + let ``HxHistoryRestoreRequest succeeds when the header is present and false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-History-Restore-Request", "false") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False + + -- 2.45.1 From 565a5cf830d306b658fe9011bc657a3e25e6ccda Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 30 Oct 2021 17:58:46 -0400 Subject: [PATCH 5/6] Finish test for server-side extensions --- src/Htmx.Tests/Tests.fs | 243 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/Htmx.Tests/Tests.fs b/src/Htmx.Tests/Tests.fs index 29c002e..556ab93 100644 --- a/src/Htmx.Tests/Tests.fs +++ b/src/Htmx.Tests/Tests.fs @@ -72,4 +72,247 @@ module IHeaderDictionaryExtensions = Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False + [] + let ``HxPrompt succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxPrompt |> Assert.True + + [] + let ``HxPrompt 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 + Option.isSome ctx.Request.Headers.HxPrompt |> Assert.True + Assert.Equal("of course", Option.get ctx.Request.Headers.HxPrompt) + + [] + let ``HxRequest succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxRequest |> Assert.True + + [] + let ``HxRequest succeeds when the header is present and true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxRequest |> Assert.True + Option.get ctx.Request.Headers.HxRequest |> Assert.True + + [] + let ``HxRequest succeeds when the header is present and false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "false") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxRequest |> Assert.True + Option.get ctx.Request.Headers.HxRequest |> Assert.False + + [] + let ``HxTarget succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxTarget |> Assert.True + + [] + let ``HxTarget succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Target", "#leItem") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxTarget |> Assert.True + Assert.Equal("#leItem", Option.get ctx.Request.Headers.HxTarget) + + [] + let ``HxTrigger succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxTrigger |> Assert.True + + [] + let ``HxTrigger succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Trigger", "#trig") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxTrigger |> Assert.True + Assert.Equal("#trig", Option.get ctx.Request.Headers.HxTrigger) + + [] + let ``HxTriggerName succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxTriggerName |> Assert.True + + [] + let ``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 + Option.isSome ctx.Request.Headers.HxTriggerName |> Assert.True + Assert.Equal("click", Option.get ctx.Request.Headers.HxTriggerName) + + +/// Tests for the HttpRequest extension properties +module HttpRequestExtensions = + + [] + let ``IsHtmx succeeds when request is not from htmx`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Assert.False ctx.Request.IsHtmx + + [] + let ``IsHtmx succeeds when request is from htmx`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Assert.True ctx.Request.IsHtmx + + [] + let ``IsHtmxRefresh succeeds when request is not from htmx`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Assert.False ctx.Request.IsHtmxRefresh + + [] + let ``IsHtmxRefresh succeeds when request is from htmx, but not a refresh`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Assert.False ctx.Request.IsHtmxRefresh + + [] + let ``IsHtmxRefresh succeeds when request is from htmx and is a refresh`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + dic.Add ("HX-History-Restore-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Assert.True ctx.Request.IsHtmxRefresh + + +/// Tests for the HttpHandler functions provided in the Handlers module +module HandlerTests = + + open System.Threading.Tasks + + /// Dummy "next" parameter to get the pipeline to execute/terminate + let next (ctx : HttpContext) = Task.FromResult (Some ctx) + + [] + let ``withHxPush succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxPush "/a-new-url" next ctx + Assert.True (dic.ContainsKey "HX-Push") + Assert.Equal ("/a-new-url", dic.["HX-Push"].[0]) + } + + [] + let ``withHxRedirect succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRedirect "/somewhere-else" next ctx + Assert.True (dic.ContainsKey "HX-Redirect") + Assert.Equal ("/somewhere-else", dic.["HX-Redirect"].[0]) + } + + [] + let ``withHxRefresh succeeds when set to true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRefresh true next ctx + Assert.True (dic.ContainsKey "HX-Refresh") + Assert.Equal ("true", dic.["HX-Refresh"].[0]) + } + + [] + let ``withHxRefresh succeeds when set to false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRefresh false next ctx + Assert.True (dic.ContainsKey "HX-Refresh") + Assert.Equal ("false", dic.["HX-Refresh"].[0]) + } + + [] + let ``withHxTrigger succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTrigger "doSomething" next ctx + Assert.True (dic.ContainsKey "HX-Trigger") + Assert.Equal ("doSomething", dic.["HX-Trigger"].[0]) + } + + [] + let ``withHxTriggerMany succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx + Assert.True (dic.ContainsKey "HX-Trigger") + Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic.["HX-Trigger"].[0]) + } + + [] + let ``withHxTriggerAfterSettle succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerAfterSettle "byTheWay" next ctx + Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") + Assert.Equal ("byTheWay", dic.["HX-Trigger-After-Settle"].[0]) + } + + [] + let ``withHxTriggerManyAfterSettle succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx + Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") + Assert.Equal ("""{ "oof": "ouch", "hmm": "uh" }""", dic.["HX-Trigger-After-Settle"].[0]) + } + + [] + let ``withHxTriggerAfterSwap succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerAfterSwap "justASec" next ctx + Assert.True (dic.ContainsKey "HX-Trigger-After-Swap") + Assert.Equal ("justASec", dic.["HX-Trigger-After-Swap"].[0]) + } + + [] + let ``withHxTriggerManyAfterSwap succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx + Assert.True (dic.ContainsKey "HX-Trigger-After-Swap") + Assert.Equal ("""{ "this": "1", "that": "2" }""", dic.["HX-Trigger-After-Swap"].[0]) + } -- 2.45.1 From 07836dad88e70bc6dc70f4728b1c32ec094bc355 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 30 Oct 2021 21:49:58 -0400 Subject: [PATCH 6/6] WIP on readme --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a09fb2a..1b38f67 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,66 @@ # Giraffe.Htmx -Extensions for Giraffe to support development with Htmx + +[Giraffe](https://giraffe.wiki) is a library that sits atop ASP.NET Core, and enables developers to create applications in a functional style (vs. the C# / object-oriented style of the base library). The Giraffe View Engine enables production of HTML views in a strongly-typed and fully-integrated-with-source fashion. + +[htmx](https://htmx.org) is a library that embraces the idea of HTML as a programming language, where any element can fire off a request, and portions of the page can be swapped out dynamically. It does all these with a tiny, dependency-free JavaScript library. + +htmx uses attributes and HTTP headers to attain its interactivity; the libraries here contain extensions to both Giraffe and Giraffe View Engine to enable strongly-typed development of htmx applications. + +## Installation + +`Giraffe.Htmx` provides extensions that facilitate using htmx on the server side, primarily reading and setting headers. `Giraffe.ViewEngine.Htmx` provides attributes and helpers to produce views that utilize htmx. Both can be installed from NuGet via standard methods. + +TODO: NuGet links + +## Server-Side (`Giraffe.Htmx`) + +In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://htmx.org/docs/#request_headers) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request. + +A server may want to respond to a request that originated from htmx differently than a regular request. One way htmx can provide the same feel as a Single Page Application (SPA) is by swapping out the `body` content (or an element within it) instead of reloading the entire page. In this case, the developer can provide a partial layout to be used for these responses, while returning the full page for regular requests. The `IsHtmx` property makes this easy... + +```fsharp + // "partial" and "full" are handlers that return the contents; + // "view" can be whatever your view engine needs for the body of the page + let result view : HttpHandler = + fun next ctx -> + match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with + | true -> partial view + | false -> full view +``` + +htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) to affect client-side behavior. For each of these, this library provides `HttpHandler`s that can be chained along with the response. As an example, if the server returns a redirect response (301, 302, 303, 307), the `XMLHttpRequest` handler on the client will follow the redirection before htmx can do anything with it. To redirect to a new page, you would return an OK (200) response with an `HX-Redirect` header set in the response. + +```fsharp + let theHandler : HttpHandler = + fun next ctx -> + // some interesting stuff + withHxRedirect "/the-new-url" >=> Successful.OK +``` + +Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well. + +## htmx with Giraffe View Engine (`Giraffe.ViewEngine.Htmx`) + +As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible. + +As an example, creating a `div` that loads data once the HTML is rendered: + +```fsharp + let autoload = + div [ _hxGet "/lazy-load-data"; _hxTrigger "load" ] [ str "Loading..." ] +``` + +_(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true. To disable it for an element, use `_hxNoBoost` instead.)_ + +Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these, there are modules with those values. For example, `HxTrigger.Load` could be used in the example above, to ensure that the known values are spelled correctly. `hx-trigger` can also take modifiers, such as an action that only responds to `Ctrl`+click. The `HxTrigger` module has a `Filter` submodule to assist with defining these actions. + +```fsharp + let shiftClick = + p [ _hxGet = "/something"; _hxTrigger (HxTrigger.Filter.Shift HxTrigger.Click) ] [ + str "hold down Shift and click me" + ] +``` + +## Feedback / Help + +The author hangs out in the #htmx-general channel of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/). -- 2.45.1