Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c7ad82c
Try having controling playwright with http
Zer0- Dec 31, 2025
9bf0ce7
miso-test passes with playwrite.ts being an http server
Zer0- Dec 27, 2025
24fec69
Stub in test executable for integration test server
Zer0- Dec 27, 2025
e808c29
Integration testing - get test server to build
Zer0- Jan 1, 2026
d3bdfe5
WiP to generate arbitrary html
Zer0- Jan 5, 2026
7118975
WiP - hydration test, fix HtmlGen
Zer0- Jan 5, 2026
3e96631
Wip, Add a more appropriate arbitrary html generator
Zer0- Jan 5, 2026
c2abaa0
WiP - tweaking the generator
Zer0- Jan 13, 2026
323b244
HtmlGen - fix attribute generation
Zer0- Jan 13, 2026
7987e4a
WiP - can't generate the same html clientside
Zer0- Jan 22, 2026
e6387f7
Rewrite HtmlGen2
Zer0- Jan 24, 2026
342324d
HtmlGen3 - have a function to choose the next tag based on previous tag
Zer0- Jan 26, 2026
f074946
Tweak HtmlGen3 to emit valid html
Zer0- Jan 27, 2026
10a7034
miso-tests, end to end test for hydration passing
Zer0- Jan 29, 2026
51b3e51
playwright-js script now includes integration-tests-server test
Zer0- Jan 29, 2026
dcd3c09
miso-tests - get playwright-wasm working
Zer0- Jan 31, 2026
a5e9df7
default.nix - fix playwright-wasm script
Zer0- Jan 31, 2026
4fbbf2a
Remove unused experiment modules
Zer0- Jan 31, 2026
fb2533e
miso-tests: TestApp now performs a click and checks if its handled
Zer0- Jan 31, 2026
c13a291
WiP: miso-tests - test bindings
Zer0- Feb 5, 2026
03edf88
WiP: TestBindings - maybe we don't need the outer app
Zer0- Feb 6, 2026
9aeecce
WiP: TestBindings
Zer0- Feb 6, 2026
9a9bc3d
WiP: TestBindings, server might be OK
Zer0- Feb 6, 2026
4b5fba3
Miso-tests - TestBindingsApp
Zer0- Feb 6, 2026
d809951
Merge remote-tracking branch 'origin/master' into miso-testing
Zer0- Feb 6, 2026
c7a2343
Fix some compilation issues
Zer0- Feb 11, 2026
512f6fa
miso-tests - us miso dsl instead of jsaddle
Zer0- Feb 11, 2026
7088003
miso-tests: use CPP to use aeson serverside and Miso.JSON clientside
Zer0- Feb 11, 2026
1ade748
Add `Miso.JSON.encode` for SSR.
dmjio Feb 11, 2026
bdf909b
Merge remote-tracking branch 'origin/encode' into miso-tests-json
Zer0- Feb 11, 2026
6468d7a
Use Miso.JSON server-side
Zer0- Feb 11, 2026
e195858
Encode strings correctly
dmjio Feb 11, 2026
07944a9
Disallow `Generic`-deriving for sum types.
dmjio Feb 11, 2026
0c63343
Merge remote-tracking branch 'origin/encode' into miso-tests-json
Zer0- Feb 11, 2026
a57a392
Merge remote-tracking branch 'origin/master' into miso-tests-json
Zer0- Feb 11, 2026
a1600e9
Merge remote-tracking branch 'origin/master' into miso-tests-json
Zer0- Feb 13, 2026
0dbe702
Merge remote-tracking branch 'origin/master' into miso-tests-json
Zer0- Feb 14, 2026
54ed368
Manually define To/FromJSON instances for Sum types
Zer0- Feb 14, 2026
ff270b8
Fix component initialization bug in initialDraw
Zer0- Feb 16, 2026
d87f101
Minor cleanup to TestBindingsApp
Zer0- Feb 16, 2026
1919f70
miso-tests: Temporary work-around to get wasm to load properly
Zer0- Feb 16, 2026
3c73522
Revert "Fix component initialization bug in initialDraw"
Zer0- Feb 21, 2026
bfc0e32
Revert "miso-tests: Temporary work-around to get wasm to load properly"
Zer0- Feb 21, 2026
dcd6f46
Merge remote-tracking branch 'origin/master' into miso-testing
Zer0- Feb 21, 2026
c25a4b6
Add FromJSON instances to HtmlGen
Zer0- Feb 22, 2026
417768e
Add flag no-implicit-js to miso build
Zer0- Feb 22, 2026
656e407
Runtime.hs - fix transitive bindings
Zer0- Feb 23, 2026
c3e1eb7
quick hack for <--> bindings to not recurse propagateParent
Zer0- Feb 23, 2026
e7b2e03
Recursively calling propagateParent wasn't a good idea
Zer0- Feb 23, 2026
361ff39
Use single value instead of Set in Runtime.hs
Zer0- Feb 24, 2026
defcf33
remove trace statements
Zer0- Feb 24, 2026
22c7948
Revert "Add flag no-implicit-js to miso build"
Zer0- Feb 24, 2026
5f106ff
Merge remote-tracking branch 'origin/master' into merge-bindings-debug3
Zer0- Feb 24, 2026
fc9a17a
Get rid of logging/tracing introduced in previous commit
Zer0- Feb 24, 2026
87e5f7c
Rename Synch -> Sync in Runtime.hs
Zer0- Feb 23, 2026
97ecb9d
fix html attribute bool value
Zer0- Feb 24, 2026
e8b3285
import miso.js in init_integration_wasm_client.js
Zer0- Feb 24, 2026
9869b4b
Fix merge artifact
Zer0- Feb 24, 2026
5a525c2
add missing curly brace
Zer0- Mar 12, 2026
67fde47
Commit WiP test changes
Zer0- Mar 12, 2026
079a335
Merge remote-tracking branch 'origin/master' into miso-testing
Zer0- Mar 13, 2026
6b869f3
Escape JSON strings
Zer0- Mar 13, 2026
7fa45ce
Property tests (hydration, bindings) passing for ghcjs and wasm
Zer0- Mar 13, 2026
6d59df9
playwright-js/wasm scripts - see if this makes CI tests pass reliably
Zer0- Mar 13, 2026
a32d8a4
toMisoString instance for Double
Zer0- Apr 7, 2026
f9bca22
Drop trailing zeros when encoding `Float` / `Double` as JSON.
dmjio Apr 7, 2026
12a1146
Revert "toMisoString instance for Double"
Zer0- Apr 7, 2026
18e6ec2
remove redundant CPP block
Zer0- Apr 7, 2026
efc1ba8
Merge remote-tracking branch 'origin/encode-double' into miso-testing…
Zer0- Apr 7, 2026
07fb966
remove unused var
Zer0- Apr 7, 2026
57127c7
JSON Lexer - support unicode surrogate pairs
Zer0- Apr 7, 2026
39bbdc9
Revert "JSON Lexer - support unicode surrogate pairs"
Zer0- Apr 9, 2026
3fd7153
Merge remote-tracking branch 'origin/master' into miso-testing
Zer0- Apr 9, 2026
2fe377a
playwright-wasm test: use instantiate instead of instantiateStreaming
Zer0- Apr 11, 2026
ba74b68
Cleanup integration-tests
Zer0- Apr 11, 2026
32dfad5
playwright.ts driver - wait for page load instead of networkidle
Zer0- Apr 11, 2026
eda6040
playwright.ts driver - add retry
Zer0- Apr 11, 2026
be88cba
playwright.ts - wait for domcontentloaded
Zer0- Apr 11, 2026
425bcb0
playwright.ts - try not waiting around for domcontentloaded and rely …
Zer0- Apr 11, 2026
096e9a2
Components being mounted/remounted didn't sync bindings
Zer0- Apr 14, 2026
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
51 changes: 41 additions & 10 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
with (import ./nix { inherit overlays; });

with pkgs.haskell.lib;
{
rec {
inherit pkgs legacyPkgs;

# hackage release
Expand All @@ -24,7 +24,7 @@ with pkgs.haskell.lib;
# miso x86
miso-ghc = legacyPkgs.haskell.packages.ghc865.miso;
miso-ghc-9122 = pkgs.haskell.packages.ghc9122.miso;
miso-tests-ghc = pkgs.haskell.packages.ghc9122.miso;
miso-tests-ghc = pkgs.haskell.packages.ghc9122.miso-tests;

# sample app legacy build
inherit (legacyPkgs.haskell.packages.ghc865)
Expand Down Expand Up @@ -79,28 +79,59 @@ with pkgs.haskell.lib;

playwright-js = pkgs.writeScriptBin "playwright" ''
#!${pkgs.stdenv.shell}
set -e
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
export PATH="${pkgs.lib.makeBinPath [ pkgs.http-server pkgs.bun ]}:$PATH"
bun install playwright@1.53
http-server ${pkgs.pkgsCross.ghcjs.haskell.packages.ghc9122.miso-tests}/bin/component-tests.jsexe &
cd tests
bun run ../ts/playwright.ts
http-server -p 8061 ${miso-tests}/bin/component-tests.jsexe &
HTTP_SERVER_PID=$!
echo $HTTP_SERVER_PID
PW_API_PORT=8060
PORT=$PW_API_PORT bun run ./ts/playwright.ts &
PLAYWRIGHT_SERVER_PID=$!
echo $PLAYWRIGHT_SERVER_PID
until curl -sf "http://localhost:$PW_API_PORT/ready"; do sleep 0.1; done
echo "Playwright server ready, asking it to start test."
curl --fail "http://localhost:$PW_API_PORT/test?port=8061&wait=true"
kill $HTTP_SERVER_PID
PORT=8062 \
PLAYWRIGHT_PORT=$PW_API_PORT \
STATIC_DIR="${miso-tests}/bin/integration-tests-client.jsexe/" \
BACKEND=GHCJS \
${miso-tests-ghc}/bin/integration-tests-server
exit_code=$?
pkill http-server
kill $PLAYWRIGHT_SERVER_PID
exit "$exit_code"
'';

playwright-wasm = pkgs.writeScriptBin "playwright" ''
#!${pkgs.stdenv.shell}
set -e
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
export PATH="${pkgs.lib.makeBinPath [ pkgs.http-server pkgs.bun ]}:$PATH"
bun install playwright@1.53
cd tests
pushd tests
nix develop .#wasm --command bash -c 'make'
http-server ./public &
bun run ../ts/playwright.ts
http-server -p 8061 ./public &
HTTP_SERVER_PID=$!
echo $HTTP_SERVER_PID
PW_API_PORT=8060
echo pwd:
pwd
PORT=$PW_API_PORT bun run ../ts/playwright.ts &
PLAYWRIGHT_SERVER_PID=$!
echo $PLAYWRIGHT_SERVER_PID
until curl -sf "http://localhost:$PW_API_PORT/ready"; do sleep 0.1; done
echo "Playwright server ready, asking it to start test."
curl --fail "http://localhost:$PW_API_PORT/test?port=8061&wait=true"
kill $HTTP_SERVER_PID
PORT=8062 \
PLAYWRIGHT_PORT=$PW_API_PORT \
STATIC_DIR=./public \
BACKEND=WASM \
${miso-tests-ghc}/bin/integration-tests-server
exit_code=$?
pkill http-server
kill $PLAYWRIGHT_SERVER_PID
exit "$exit_code"
'';

Expand Down
14 changes: 11 additions & 3 deletions nix/haskell/packages/ghc/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ let
in
with pkgs.haskell.lib;
self: super:
{
rec {
/* miso */
miso = self.callCabal2nixWithOptions "miso" source.miso "-ftemplate-haskell" {};
miso-tests = self.callCabal2nix "miso-tests" source.miso-tests {};
miso = self.callCabal2nixWithOptions "miso" source.miso "-ftemplate-haskell -fssr" {};

/* miso utils */
miso-from-html = self.callCabal2nix "miso-from-html" source.miso-from-html {};
servant-miso-html = self.callCabal2nix "servant-miso-html" source.servant-miso-html {};
servant-miso-router = self.callCabal2nix "servant-miso-router" source.servant-miso-router {
servant-miso-html = servant-miso-html;
};

miso-tests = self.callCabal2nix "miso-tests" source.miso-tests {
servant-miso-router = servant-miso-router;
servant-miso-html = servant-miso-html;
};

/* examples */
sample-app = self.callCabal2nix "app" source.sample-app {};
Expand Down
14 changes: 14 additions & 0 deletions nix/source.nix
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ in
sha256 = "0s6kzqxbshsnqbqfj7rblqkrr5mzkjxknb6k8m8z4h10mcv1zh7j";
};

servant-miso-html = fetchFromGitHub {
owner = "haskell-miso";
repo = "servant-miso-html";
rev = "00781d1920795b67e0476b67ed6840c388f29810";
sha256 = "sha256-dYPlwSbQ+QXvMeS5tonBVnT9zQGADtohmD/ZAiY/cXA=";
};

servant-miso-router = fetchFromGitHub {
owner = "haskell-miso";
repo = "servant-miso-router";
rev = "0c828e0ba30ee7a446ce8999288b32b7f6425dd1";
sha256 = "sha256-2Vkheb2iNDFWNAToO+r8rMY3OAA6LlUtgxiCWRm0wAY=";
};

ghcjs-base = fetchFromGitHub {
owner = "ghcjs";
repo = "ghcjs-base";
Expand Down
2 changes: 1 addition & 1 deletion src/Miso/Html/Render.hs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ renderBuilder (VNode ns tag attrs children) = mconcat
, mconcat [ " " <> intercalate " " (renderAttrs <$> attrs)
| not (Prelude.null attrs)
]
, if tag `elem` selfClosing then "/>" else ">"
, ">"
, mconcat
[ mconcat
[ foldMap renderBuilder (collapseSiblingTextNodes children)
Expand Down
2 changes: 1 addition & 1 deletion src/Miso/JSON.hs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import Data.Char
import qualified Data.Map.Strict as M
import Data.Map.Strict (Map)
import Data.Int
import GHC.Natural (naturalToInteger, naturalFromInteger)
import GHC.Natural (naturalToInteger, naturalFromInteger )
import GHC.TypeLits
import Data.Kind
#ifndef VANILLA
Expand Down
99 changes: 64 additions & 35 deletions src/Miso/Runtime.hs
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,10 @@ initialize events _componentParentId hydrate isRoot comp@Component {..} getCompo
(Hydrate, Just m) -> m
(Draw, _) -> do
IM.lookup _componentId <$> readIORef components >>= \case
Nothing ->
pure model
Just cs ->
-- hot reload scenario, let it flow
pure (cs ^. componentModel)
_ -> pure model
Nothing -> applyParentBindings _componentParentId model bindings
Just cs -> pure (cs ^. componentModel)
_ -> applyParentBindings _componentParentId model bindings

_componentScripts <- (++) <$> renderScripts scripts <*> renderStyles styles
_componentDOMRef <- getComponentMountPoint
_componentIsDirty <- pure False
Expand Down Expand Up @@ -328,12 +326,30 @@ propagate
-> IntMap (ComponentState p m a)
-> (IntMap (ComponentState p m a), ComponentIds)
propagate vcompId vcomps =
let dfsState = execState synch (dfs vcomps vcompId)
let dfsState = execState sync (dfs vcomps vcompId)
in (_state dfsState, _visited dfsState)
-----------------------------------------------------------------------------
-- | Create an empty DFS state
dfs :: IntMap (ComponentState p m a) -> ComponentId -> DFS p m a
dfs cs vcompId = DFS cs mempty (pure vcompId)
dfs cs vcompId = DFS cs mempty (pure vcompId) vcompId
-----------------------------------------------------------------------------
-- | Applies ParentToChild & Bidirectional bindings from the parent's current model
-- to the child's initial model. Safe to call during mount.
applyParentBindings
:: ComponentId
-> model
-> [Binding parent model]
-> IO model
applyParentBindings pId mdl bindings = do
mParent <- IM.lookup pId <$> readIORef components
pure $ case mParent of
Nothing -> mdl
Just parentState ->
foldr (applyBinding parentState) mdl bindings
where
applyBinding parentState (ParentToChild from into) acc = into (from (parentState ^. componentModel)) acc
applyBinding parentState (Bidirectional from _ _ into) acc = into (from (parentState ^. componentModel)) acc
applyBinding _ _ acc = acc
-----------------------------------------------------------------------------
type ComponentIds = IntSet
-----------------------------------------------------------------------------
Expand All @@ -345,9 +361,11 @@ data DFS p m a
-- ^ visited set
, _stack :: [ComponentId]
-- ^ neighbors queue
, _triggeredComponent :: ComponentId
-- ^ start of the traverse
}
-----------------------------------------------------------------------------
type Synch p m a x = State (DFS p m a) x
type Sync p m a x = State (DFS p m a) x
-----------------------------------------------------------------------------
visited :: Lens (DFS p m a) (ComponentIds)
visited = lens _visited $ \r x -> r { _visited = x }
Expand All @@ -358,40 +376,50 @@ state = lens _state $ \r x -> r { _state = x }
stack :: Lens (DFS p m a) [ComponentId]
stack = lens _stack $ \r x -> r { _stack = x }
-----------------------------------------------------------------------------
synch :: Synch p m a ()
synch = mapM_ go =<< pop
triggeredComponent :: Lens (DFS p m a) ComponentId
triggeredComponent = lens _triggeredComponent $ \r x -> r { _triggeredComponent = x }
-----------------------------------------------------------------------------
sync :: Sync p m a ()
sync = mapM_ go =<< pop
where
go :: ComponentState p m a -> Synch p m a ()
go :: ComponentState p m a -> Sync p m a ()
go cs = do
seen <- IS.member (cs ^. componentId) <$> use visited
visited_ <- use visited
let seen = IS.member (cs ^. componentId) visited_
when (not seen) $ do
propagateParent cs (cs ^. parentId)
let parentSeen = IS.member (cs ^. parentId) visited_
when (not parentSeen) $
propagateParent cs (cs ^. parentId)
propagateChildren cs (cs ^. children)
markVisited (cs ^. componentId)
synch
sync
-----------------------------------------------------------------------------
propagateChildren
:: forall p m a
. ComponentState p m a
-> ComponentIds
-> Synch p m a ()
-> Sync p m a ()
propagateChildren currentState childComponents = do
forM_ (IS.toList childComponents) $ \childId -> do
childState <- unsafeCoerce (IM.! childId) <$> use state
updatedChild <- unsafeCoerce <$>
foldM process childState (childState ^. componentBindings)
let isChildDirty =
(_componentModelDirty childState)
(_componentModel childState)
(_componentModel updatedChild)
when isChildDirty $ do
state.at childId ?= updatedChild { _componentIsDirty = True }
visit childId
triggeredComponent_ <- use triggeredComponent

when (childId /= triggeredComponent_) $ do
childState <- unsafeCoerce (IM.! childId) <$> use state
updatedChild <- unsafeCoerce <$>
foldM process childState (childState ^. componentBindings)
let isChildDirty =
(_componentModelDirty childState)
(_componentModel childState)
(_componentModel updatedChild)
when isChildDirty $ do
state.at childId ?= updatedChild { _componentIsDirty = True }
visit childId

where
process
:: ComponentState m child a
-> Binding m child
-> Synch p m a (ComponentState m child a)
-> Sync p m a (ComponentState m child a)
process childState = \case
ParentToChild getCurrentField setChildField -> do
let currentChildModel = childState ^. componentModel
Expand All @@ -410,10 +438,11 @@ propagateParent
:: forall p m a
. ComponentState p m a
-> ComponentId
-> Synch p m a ()
-> Sync p m a ()
propagateParent currentState parentId_ =
IM.lookup parentId_ <$> use state >>= mapM_ \case
parentState -> do
IM.lookup parentId_ <$> use state >>= \case
Nothing -> pure ()
Just parentState -> do
updatedParent <- unsafeCoerce <$>
foldM process (unsafeCoerce parentState) (currentState ^. componentBindings)
let isParentDirty =
Expand All @@ -427,7 +456,7 @@ propagateParent currentState parentId_ =
process
:: ComponentState x p a
-> Binding p m
-> Synch p m a (ComponentState x p a)
-> Sync p m a (ComponentState x p a)
process parentState = \case
ChildToParent setParentField getCurrentField -> do
let currentParentModel = parentState ^. componentModel
Expand All @@ -442,13 +471,13 @@ propagateParent currentState parentId_ =
_ ->
pure parentState
-----------------------------------------------------------------------------
markVisited :: ComponentId -> Synch p m a ()
markVisited :: ComponentId -> Sync p m a ()
markVisited vcompId = visited.at vcompId ?= ()
-----------------------------------------------------------------------------
visit :: ComponentId -> Synch p m a ()
visit :: ComponentId -> Sync p m a ()
visit vcompId = stack %= (vcompId:)
-----------------------------------------------------------------------------
pop :: Synch p m a (Maybe (ComponentState p m a))
pop :: Sync p m a (Maybe (ComponentState p m a))
pop = use stack >>= \case
[] ->
pure Nothing
Expand Down Expand Up @@ -1095,7 +1124,7 @@ setAttrs vnode_@(Object jval) attrs snk logLevel events =
-----------------------------------------------------------------------------
-- | Registers components in the global state
registerComponent :: MonadIO m => ComponentState parent model action -> m ()
registerComponent componentState = liftIO $
registerComponent componentState = liftIO $ do
atomicModifyIORef' components $ \cs ->
(IM.insert (_componentId componentState) componentState cs, ())
-----------------------------------------------------------------------------
Expand Down
4 changes: 0 additions & 4 deletions src/Miso/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,7 @@ data Component parent model action
= Component
{ model :: model
-- ^ Initial model
#ifdef SSR
, hydrateModel :: Maybe (IO model)
#else
, hydrateModel :: Maybe (IO model)
#endif
-- ^ Optional 'IO' to load component 'model' state, such as reading data from page.
-- The resulting 'model' is only used during initial hydration, not on remounts.
, update :: action -> Effect parent model action
Expand Down
5 changes: 0 additions & 5 deletions tests/CHANGELOG.md

This file was deleted.

15 changes: 11 additions & 4 deletions tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ update:
wasm32-wasi-cabal update

build:
wasm32-wasi-cabal build component-tests
wasm32-wasi-cabal build --allow-newer
rm -rf public
cp -r static public
$(eval my_wasm=$(shell wasm32-wasi-cabal list-bin component-tests | tail -n 1))
$(shell wasm32-wasi-ghc --print-libdir)/post-link.mjs --input $(my_wasm) --output public/ghc_wasm_jsffi.js
cp -v $(my_wasm) public/
bash -c '\
libdir=$$(wasm32-wasi-ghc --print-libdir); \
my_wasm=$$(wasm32-wasi-cabal list-bin component-tests --allow-newer); \
$$libdir/post-link.mjs -i "$$my_wasm" -o public/ghc_wasm_jsffi.js; \
cp -v "$$my_wasm" public/; \
more_wasm=$$(wasm32-wasi-cabal list-bin integration-tests-client --allow-newer); \
echo "Processing integration-tests-client: $$more_wasm"; \
$$libdir/post-link.mjs -i "$$more_wasm" -o public/wasm.js; \
cp -v "$$more_wasm" public/integration-client.wasm \
'

optim:
wasm-opt -all -O2 public/component-tests.wasm -o public/component-tests.wasm
Expand Down
Loading
Loading