Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
name: Deploy
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- '**/*.md'
release:
types: [published]
jobs:
deploy:
name: Deploy
Expand Down
5 changes: 5 additions & 0 deletions sln/src/FSharp.ViewEngine/Datastar.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ namespace FSharp.ViewEngine

type Datastar =
// Core attributes
static member inline _dataAttr (v: string) = { Name = "data-attr"; Value = ValueSome v }
static member inline _dataAttr (name: string, v: string) = { Name = $"data-attr:{name}"; Value = ValueSome v }
static member inline _dataBind (name: string) = { Name = $"data-bind:{name}"; Value = ValueNone }
static member inline _dataBind (name: string, v: string) = { Name = $"data-bind:{name}"; Value = ValueSome v }
static member inline _dataClass (v: string) = { Name = "data-class"; Value = ValueSome v }
static member inline _dataClass (name: string, v: string) = { Name = $"data-class:{name}"; Value = ValueSome v }
static member inline _dataComputed (v: string) = { Name = "data-computed"; Value = ValueSome v }
static member inline _dataComputed (name: string, v: string) = { Name = $"data-computed:{name}"; Value = ValueSome v }
static member inline _dataEffect (v: string) = { Name = "data-effect"; Value = ValueSome v }
static member inline _dataIgnore = { Name = "data-ignore"; Value = ValueNone }
Expand All @@ -23,7 +26,9 @@ type Datastar =
static member inline _dataRef (name: string) = { Name = $"data-ref:{name}"; Value = ValueNone }
static member inline _dataRef (name: string, v: string) = { Name = $"data-ref:{name}"; Value = ValueSome v }
static member inline _dataShow (v: string) = { Name = "data-show"; Value = ValueSome v }
static member inline _dataSignals (v: string) = { Name = "data-signals"; Value = ValueSome v }
static member inline _dataSignals (name: string, v: string) = { Name = $"data-signals:{name}"; Value = ValueSome v }
static member inline _dataStyle (v: string) = { Name = "data-style"; Value = ValueSome v }
static member inline _dataStyle (prop: string, v: string) = { Name = $"data-style:{prop}"; Value = ValueSome v }
static member inline _dataText (v: string) = { Name = "data-text"; Value = ValueSome v }

Expand Down
68 changes: 68 additions & 0 deletions sln/src/Tests/AlpineTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module AlpineTests

open FSharp.ViewEngine
open System.Text.RegularExpressions
open Expecto
open type Html
open type Alpine

[<Tests>]
let tests =
testList "Alpine Tests" [
test "Alpine attributes render correctly" {
let actual =
div {
_xData "{ open: false }"
_xInit "console.log('init')"
_xShow "open"
_xBind ("class", "open ? 'active' : ''")
_xOn ("click", "$dispatch('close')")
_xText "$message"
_xRef "container"
_xIf "show"
_xFor "item in items"
_xModel "name"
_xModel ("name", ".lazy")
_xModelable "value"
_xId "['dropdown']"
_xEffect "$watch('open', val => console.log(val))"
_xTransition ()
_xTransition ("fade", ":enter")
_xTrap "open"
_xTrap ("open", ".noscroll")
_xCloak
_xAnchor "#trigger"
_xAnchor ("#trigger", ".bottom")
_xTeleport "#modals"
_by "x.id"
_x ("mask", "99/99/9999")
_x "collapse"
"Content"
} |> Render.toString
Expect.stringContains actual "x-data=\"{ open: false }\"" "x-data"
Expect.stringContains actual "x-init=\"console.log('init')\"" "x-init"
Expect.stringContains actual "x-show=\"open\"" "x-show"
Expect.stringContains actual "x-bind:class=\"open ? 'active' : ''\"" "x-bind"
Expect.stringContains actual "x-on:click=\"$dispatch('close')\"" "x-on with handler"
Expect.stringContains actual "x-text=\"$message\"" "x-text"
Expect.stringContains actual "x-ref=\"container\"" "x-ref"
Expect.stringContains actual "x-if=\"show\"" "x-if"
Expect.stringContains actual "x-for=\"item in items\"" "x-for"
Expect.stringContains actual "x-model=\"name\"" "x-model"
Expect.stringContains actual "x-model.lazy=\"name\"" "x-model with modifier"
Expect.stringContains actual "x-modelable=\"value\"" "x-modelable"
Expect.stringContains actual "x-id=\"['dropdown']\"" "x-id"
Expect.stringContains actual "x-effect=\"$watch('open', val => console.log(val))\"" "x-effect"
Expect.isTrue (Regex.IsMatch(actual, @"x-transition(?!=)(?!:)")) "x-transition bare"
Expect.stringContains actual "x-transition:enter=\"fade\"" "x-transition with modifier and value"
Expect.stringContains actual "x-trap=\"open\"" "x-trap"
Expect.stringContains actual "x-trap.noscroll=\"open\"" "x-trap with modifier"
Expect.stringContains actual "x-cloak" "x-cloak"
Expect.stringContains actual "x-anchor=\"#trigger\"" "x-anchor"
Expect.stringContains actual "x-anchor.bottom=\"#trigger\"" "x-anchor with modifier"
Expect.stringContains actual "x-teleport=\"#modals\"" "x-teleport"
Expect.stringContains actual "by=\"x.id\"" "by"
Expect.stringContains actual "x-mask=\"99/99/9999\"" "x generic with value"
Expect.isTrue (Regex.IsMatch(actual, @"x-collapse(?!=)")) "x generic no value"
}
]
151 changes: 104 additions & 47 deletions sln/src/Tests/Tests.fs → sln/src/Tests/CoreTests.fs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
module Tests
module CoreTests

open FSharp.ViewEngine
open System.Text
open System.Web
open System.Text.Json
open System.Text.RegularExpressions
open Expecto
open type Html
open type Htmx
open type Alpine
open type Datastar
open type Svg
open type Tailwind

Expand Down Expand Up @@ -140,54 +138,114 @@ let expectedHtml = """

[<Tests>]
let tests =
testList "ViewEngine Tests" [
testList "Core Tests" [
test "ViewEngine should render html document" {
let actual = ViewEngineApi.buildDocument() |> Render.toHtmlDocString
Expect.equal (String.clean actual) (String.clean expectedHtml) "Rendered HTML should match expected"
}

test "Datastar attributes should render with data- prefix" {
let actual =
div {
_dataSignals ("count", "0")
_dataOn ("click", "$count++")
_dataShow "$count > 0"
_dataText "$count"
_dataBind "name"
_dataEffect "console.log($count)"
_dataClass ("active", "$isActive")
_dataAttr ("disabled", "$count === 0")
_dataComputed ("double", "$count * 2")
_dataInit "console.log('init')"
_dataIgnore
_dataIgnoreMorph
_dataStyle ("color", "red")
_dataRef "myInput"
_dataIndicator "loading"
_dataAnimate "fadeIn"
_dataPersist ()
_dataPersist "count"
_dataScrollIntoView
"Content"
} |> Render.toString
Expect.stringContains actual "data-signals:count=\"0\"" "data-signals"
Expect.stringContains actual "data-on:click=\"$count++\"" "data-on"
Expect.stringContains actual "data-show=\"$count > 0\"" "data-show"
Expect.stringContains actual "data-text=\"$count\"" "data-text"
Expect.stringContains actual "data-bind:name" "data-bind"
Expect.stringContains actual "data-effect=\"console.log($count)\"" "data-effect"
Expect.stringContains actual "data-class:active=\"$isActive\"" "data-class"
Expect.stringContains actual "data-attr:disabled=\"$count === 0\"" "data-attr"
Expect.stringContains actual "data-computed:double=\"$count * 2\"" "data-computed"
Expect.stringContains actual "data-init=\"console.log('init')\"" "data-init"
Expect.stringContains actual "data-ignore-morph" "data-ignore-morph"
Expect.stringContains actual "data-ignore" "data-ignore"
Expect.stringContains actual "data-style:color=\"red\"" "data-style"
Expect.stringContains actual "data-ref:myInput" "data-ref"
Expect.stringContains actual "data-indicator:loading" "data-indicator"
Expect.stringContains actual "data-animate=\"fadeIn\"" "data-animate"
Expect.stringContains actual "data-persist data-persist:count" "data-persist"
Expect.stringContains actual "data-scroll-into-view" "data-scroll-into-view"
test "Void elements render without closing tag" {
let actual = br |> Render.toString
Expect.equal actual "<br>" "br"
let actual2 = hr |> Render.toString
Expect.equal actual2 "<hr>" "hr"
let actual3 = (img { _src "/logo.png"; _alt "logo" }) |> Render.toString
Expect.equal actual3 "<img src=\"/logo.png\" alt=\"logo\">" "img with attrs"
}

test "Regular element with no children renders open and close tags" {
let actual = div {} |> Render.toString
Expect.equal actual "<div></div>" "empty div"
}

test "Regular element with text child and no attrs" {
let actual = span { "hello" } |> Render.toString
Expect.equal actual "<span>hello</span>" "span with text"
}

test "raw bypasses encoding, text encodes" {
let rawResult = raw "<b>hi</b>" |> Render.toString
Expect.equal rawResult "<b>hi</b>" "raw passes through"
let textResult = div { text "<b>hi</b>" } |> Render.toString
Expect.equal textResult "<div>&lt;b&gt;hi&lt;/b&gt;</div>" "text encodes"
}

test "Html.empty (NoopElement) renders nothing" {
let actual = div { empty } |> Render.toString
Expect.equal actual "<div></div>" "empty renders nothing"
}

test "EmptyAttr is silently dropped (boolean false)" {
let actual = input { _hidden false; _disabled false; _required false } |> Render.toString
Expect.equal actual "<input>" "no attrs when all false"
}

test "Boolean attributes render when true, omit when false" {
let actual = input { _hidden true; _disabled true; _required true; _checked true } |> Render.toString
Expect.stringContains actual "hidden" "hidden present"
Expect.stringContains actual "disabled" "disabled present"
Expect.stringContains actual "required" "required present"
Expect.stringContains actual "checked" "checked present"
let actual2 = input { _hidden false; _disabled false } |> Render.toString
Expect.isFalse (actual2.Contains("hidden")) "hidden absent"
Expect.isFalse (actual2.Contains("disabled")) "disabled absent"
}

test "_class with string seq joins with spaces" {
let actual = div { _class [ "a"; "b"; "c" ] } |> Render.toString
Expect.equal actual "<div class=\"a b c\"></div>" "class list joined"
}

test "_data custom data attribute" {
let actual = div { _data ("foo", "bar"); _data "baz" } |> Render.toString
Expect.stringContains actual "data-foo=\"bar\"" "data with value"
Expect.stringContains actual "data-baz" "data without value"
}

test "Custom element builders el and elVoid" {
let actual = (Html.el "my-component") { _id "c1"; "content" } |> Render.toString
Expect.equal actual "<my-component id=\"c1\">content</my-component>" "custom regular element"
let actual2 = (Html.elVoid "my-void") { _id "v1" } |> Render.toString
Expect.equal actual2 "<my-void id=\"v1\">" "custom void element"
}

test "title element renders correctly" {
let actual = title "My Page" |> Render.toString
Expect.equal actual "<title>My Page</title>" "title"
}

test "For iteration in builder" {
let items = [ "a"; "b"; "c" ]
let actual = ul { for item in items do li { item } } |> Render.toString
Expect.equal actual "<ul><li>a</li><li>b</li><li>c</li></ul>" "for iteration"
}

test "3+ attributes exercises attrRest branch" {
let actual = div { _id "x"; _class "y"; _style "z"; _title "w" } |> Render.toString
Expect.equal actual "<div id=\"x\" class=\"y\" style=\"z\" title=\"w\"></div>" "4 attrs"
}

test "3+ children exercises childRest branch" {
let actual = div { span { "a" }; span { "b" }; span { "c" } } |> Render.toString
Expect.equal actual "<div><span>a</span><span>b</span><span>c</span></div>" "3 children"
}

test "3+ attrs on void element exercises attrRest branch" {
let actual = input { _id "x"; _class "y"; _name "z"; _type "text" } |> Render.toString
Expect.equal actual "<input id=\"x\" class=\"y\" name=\"z\" type=\"text\">" "4 attrs on void"
}

test "Render.toHtmlDocString prepends DOCTYPE" {
let actual = html { body { "hi" } } |> Render.toHtmlDocString
Expect.isTrue (actual.StartsWith("<!DOCTYPE html>")) "starts with doctype"
Expect.stringContains actual "<html><body>hi</body></html>" "contains html"
}

test "StringBuilderPool reuse works across multiple renders" {
let r1 = div { "first" } |> Render.toString
let r2 = div { "second" } |> Render.toString
Expect.equal r1 "<div>first</div>" "first render"
Expect.equal r2 "<div>second</div>" "second render (reused pool)"
}

test "HtmlEncode should match HttpUtility.HtmlEncode" {
Expand All @@ -214,5 +272,4 @@ let tests =

Expect.equal actual expected $"HtmlEncode should match HttpUtility for input: {input}"
}

]
Loading
Loading