test(techdemo): add full product e2e coverage
req: examples/001 req: dx/008 req: form/002 req: page_swap/002 req: push/003
This commit is contained in:
Generated
+368
@@ -84,6 +84,18 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -115,6 +127,61 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3"
|
||||
dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser-macros"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa-short"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ego-tree"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
@@ -157,6 +224,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
|
||||
dependencies = [
|
||||
"mac",
|
||||
"new_debug_unreachable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@@ -202,6 +279,24 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
@@ -226,6 +321,18 @@ dependencies = [
|
||||
"tree-sitter-hemplate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
"match_token",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -328,12 +435,52 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_token"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
@@ -380,18 +527,99 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
@@ -410,6 +638,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -428,6 +662,30 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@@ -469,6 +727,46 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "scraper"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"ego-tree",
|
||||
"getopts",
|
||||
"html5ever",
|
||||
"precomputed-hash",
|
||||
"selectors",
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cssparser",
|
||||
"derive_more",
|
||||
"fxhash",
|
||||
"log",
|
||||
"new_debug_unreachable",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"precomputed-hash",
|
||||
"servo_arc",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -536,12 +834,27 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "servo_arc"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -614,6 +927,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures-util",
|
||||
"scraper",
|
||||
"slhx",
|
||||
"slhx-axum",
|
||||
"slhx-build",
|
||||
@@ -663,12 +977,43 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"phf_shared",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -686,6 +1031,17 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
|
||||
dependencies = [
|
||||
"futf",
|
||||
"mac",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
@@ -813,6 +1169,18 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
||||
@@ -15,6 +15,7 @@ slhx-axum = { path = "../../slhx-axum" }
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
scraper = "0.23"
|
||||
slhx-test = { path = "../../slhx-test" }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -6,7 +6,7 @@ Run:
|
||||
|
||||
Open <http://127.0.0.1:3002>.
|
||||
|
||||
This demo is tailored to showcase slhx strengths:
|
||||
This is a polished Linear-style product demo for planning typed work across lanes. It is tailored to showcase slhx strengths:
|
||||
|
||||
- modern SSR-first UI
|
||||
- generated resource modules from `.heml`
|
||||
@@ -17,3 +17,8 @@ This demo is tailored to showcase slhx strengths:
|
||||
- page-enhancer navigation with native link fallback
|
||||
- SSE server push into a generated slot
|
||||
- no user-authored browser JavaScript
|
||||
|
||||
Verification:
|
||||
|
||||
cargo test -p slhx-techdemo --test e2e
|
||||
mutest -p slhx-techdemo -f examples/techdemo/src/main.rs -F 'registry' --test-package slhx-techdemo -j 2 --timeout 90
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
use scraper::{Html, Selector};
|
||||
use slhx::{Effect, EffectBatch, Payload, EFFECT_BATCH_ABI_VERSION};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const ADDR: &str = "127.0.0.1:3002";
|
||||
|
||||
struct Server {
|
||||
child: Child,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn start() -> Self {
|
||||
let bin = env!("CARGO_BIN_EXE_slhx-techdemo");
|
||||
let child = Command::new(bin)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("start slhx-techdemo");
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while Instant::now() < deadline {
|
||||
if TcpStream::connect(ADDR).is_ok() {
|
||||
return Self { child };
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
panic!("slhx-techdemo did not listen on {ADDR}");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn product_is_e2e_working_over_http() {
|
||||
// req: examples/001 req: dx/008 req: form/002 req: page_swap/002 req: push/003
|
||||
let _server = Server::start();
|
||||
|
||||
let home = get("/");
|
||||
assert_eq!(home.status, 200);
|
||||
assert!(home.header("content-type").contains("text/html"));
|
||||
let document = Html::parse_document(home.text());
|
||||
assert_text(&document, "Rust owns the interaction graph");
|
||||
assert_text(&document, "Compile checked handles");
|
||||
assert_text(&document, "Stream typed presence");
|
||||
assert_selector_count_at_least(&document, "[data-slhx-root=techdemo]", 1);
|
||||
assert_selector_count_at_least(&document, "[data-hid]", 8);
|
||||
assert_selector_count_at_least(&document, "[data-sid]", 7);
|
||||
assert_selector_count_at_least(&document, ".work-card", 3);
|
||||
assert!(home.text().contains("data-slhx-sse=\"/events\""));
|
||||
|
||||
let runtime = get("/slhx.js");
|
||||
assert_eq!(runtime.status, 200);
|
||||
assert!(runtime.header("content-type").contains("javascript"));
|
||||
assert!(runtime.text().contains("const HID = \"data-hid\""));
|
||||
|
||||
let architecture = request("GET", "/architecture", &[("X-SLHX-Partial", "1")], "");
|
||||
assert_eq!(architecture.status, 200);
|
||||
assert!(architecture.header("x-slhx-partial").contains("true"));
|
||||
let architecture_doc = Html::parse_document(architecture.text());
|
||||
assert_text(&architecture_doc, "Page swap");
|
||||
assert_text(&architecture_doc, "slhx-build");
|
||||
|
||||
let launch = post("/", "__h=539242093&title=Design+hero+moment&lane=product&impact=9");
|
||||
assert_effect_response(&launch);
|
||||
let launch_batch = launch.batch();
|
||||
assert_payload_contains(&launch_batch, "Design hero moment");
|
||||
assert_card(&launch_batch, "Design hero moment", "Product", "Draft", "width:99%");
|
||||
assert_payload_contains(&launch_batch, "Launch accepted");
|
||||
assert_payload_contains(&launch_batch, "Launched card #4");
|
||||
assert!(launch_batch.ops.len() >= 5, "launch should update multiple generated targets");
|
||||
|
||||
let default_impact = post("/", "__h=539242093&title=Default+impact&lane=compiler");
|
||||
assert_effect_response(&default_impact);
|
||||
let default_impact_batch = default_impact.batch();
|
||||
assert_payload_contains(&default_impact_batch, "Default impact");
|
||||
assert_card(&default_impact_batch, "Default impact", "Compiler", "Draft", "width:55%");
|
||||
|
||||
let low_impact = post("/", "__h=539242093&title=Low+impact&lane=runtime&impact=0");
|
||||
assert_effect_response(&low_impact);
|
||||
let low_impact_batch = low_impact.batch();
|
||||
assert_card(&low_impact_batch, "Low impact", "Runtime", "Draft", "width:11%");
|
||||
|
||||
let high_impact = post("/", "__h=539242093&title=High+impact&lane=runtime&impact=99");
|
||||
assert_effect_response(&high_impact);
|
||||
let high_impact_batch = high_impact.batch();
|
||||
assert_card(&high_impact_batch, "High impact", "Runtime", "Draft", "width:99%");
|
||||
|
||||
let missing_title = post("/", "__h=539242093&lane=runtime&impact=8");
|
||||
assert_effect_response(&missing_title);
|
||||
let missing_title_batch = missing_title.batch();
|
||||
assert_payload_contains(&missing_title_batch, "Launch accepted");
|
||||
assert_payload_not_contains(&missing_title_batch, "data-key=\"8\"");
|
||||
assert_payload_not_contains(&missing_title_batch, "MUTATED");
|
||||
|
||||
let inspect = post("/", "__h=3112123592&work_id=4");
|
||||
assert_effect_response(&inspect);
|
||||
let inspect_batch = inspect.batch();
|
||||
assert_payload_contains(&inspect_batch, "Design hero moment · lane=Product");
|
||||
assert_payload_contains(&inspect_batch, "Inspector focused");
|
||||
|
||||
let advance = post("/", "__h=4085043678&work_id=4");
|
||||
assert_effect_response(&advance);
|
||||
let advance_batch = advance.batch();
|
||||
assert_payload_contains(&advance_batch, "Pipeline advanced");
|
||||
assert_payload_contains(&advance_batch, "<span class=\"pill\">Active</span>");
|
||||
|
||||
let advance_default = post("/", "__h=4085043678&work_id=5");
|
||||
assert_effect_response(&advance_default);
|
||||
let advance_default_batch = advance_default.batch();
|
||||
assert_payload_contains(&advance_default_batch, "Default impact");
|
||||
assert_card(&advance_default_batch, "Default impact", "Compiler", "Active", "width:55%");
|
||||
|
||||
let ship_default = post("/", "__h=4085043678&work_id=5");
|
||||
assert_effect_response(&ship_default);
|
||||
let ship_default_batch = ship_default.batch();
|
||||
assert_card(&ship_default_batch, "Default impact", "Product", "Shipped", "width:55%");
|
||||
|
||||
let simulated_push = post("/", "__h=2942980687");
|
||||
assert_effect_response(&simulated_push);
|
||||
let push_batch = simulated_push.batch();
|
||||
assert_payload_contains(&push_batch, "SSE tick");
|
||||
assert_payload_contains(&push_batch, "Push simulated · no client app code");
|
||||
assert_payload_contains(&push_batch, "Simulated push event produced the same EffectBatch shape");
|
||||
|
||||
let delete_missing = post("/", "__h=165211725&work_id=999");
|
||||
assert_effect_response(&delete_missing);
|
||||
let delete_missing_batch = delete_missing.batch();
|
||||
assert_payload_not_contains(&delete_missing_batch, "Deleted card #999");
|
||||
|
||||
let delete = post("/", "__h=165211725&work_id=4");
|
||||
assert_effect_response(&delete);
|
||||
let delete_batch = delete.batch();
|
||||
assert_payload_contains(&delete_batch, "Card removed");
|
||||
assert_payload_contains(&delete_batch, "Deleted card #4");
|
||||
assert_payload_not_contains(&delete_batch, "data-key=\"4\"");
|
||||
assert_payload_contains(&delete_batch, "Default impact");
|
||||
|
||||
let reset = post("/", "__h=4166593243");
|
||||
assert_effect_response(&reset);
|
||||
let reset_batch = reset.batch();
|
||||
assert_payload_contains(&reset_batch, "Demo reset from Rust state");
|
||||
assert_payload_contains(&reset_batch, "Compile checked handles");
|
||||
|
||||
let sse = get("/events?once=1");
|
||||
assert_eq!(sse.status, 200);
|
||||
assert!(sse.header("content-type").contains("text/event-stream"));
|
||||
assert!(sse.text().contains("event: slhx"));
|
||||
assert!(sse.text().contains("data: "));
|
||||
|
||||
let unknown = post("/", "__h=999999");
|
||||
assert_eq!(unknown.status, 404);
|
||||
assert!(unknown.text().contains("unknown slhx handle id 999999"));
|
||||
}
|
||||
|
||||
fn assert_effect_response(response: &Response) {
|
||||
assert_eq!(response.status, 200);
|
||||
assert!(response.header("content-type").contains("application/slhx"));
|
||||
assert_eq!(response.header("x-slhx-fingerprint"), "9481407110904740166");
|
||||
let batch = response.batch();
|
||||
assert_eq!(batch.abi_version, EFFECT_BATCH_ABI_VERSION);
|
||||
assert!(!batch.ops.is_empty());
|
||||
}
|
||||
|
||||
fn assert_payload_contains(batch: &EffectBatch, needle: &str) {
|
||||
assert!(
|
||||
batch.ops.iter().any(|op| match op {
|
||||
Effect::Put { payload, .. } | Effect::Insert { payload, .. } | Effect::Prepend { payload, .. } => payload_value(payload).contains(needle),
|
||||
Effect::Emit { payload, .. } => payload.contains(needle),
|
||||
Effect::Navigate { url, .. } => url.contains(needle),
|
||||
Effect::Remove { .. } | Effect::Move { .. } | Effect::Focus { .. } => false,
|
||||
}),
|
||||
"missing payload {needle:?} in {batch:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_card(batch: &EffectBatch, title: &str, lane: &str, stage: &str, impact_style: &str) {
|
||||
let board = board_html(batch);
|
||||
let document = Html::parse_fragment(&board);
|
||||
let lane_selector = Selector::parse(".lane").unwrap();
|
||||
let card_selector = Selector::parse(".work-card").unwrap();
|
||||
let strong_selector = Selector::parse("strong").unwrap();
|
||||
let stage_selector = Selector::parse(".pill").unwrap();
|
||||
let impact_selector = Selector::parse(".impact i").unwrap();
|
||||
|
||||
for lane_node in document.select(&lane_selector) {
|
||||
let lane_text = lane_node.text().collect::<Vec<_>>().join(" ");
|
||||
if !lane_text.contains(lane) {
|
||||
continue;
|
||||
}
|
||||
for card in lane_node.select(&card_selector) {
|
||||
let card_title = card
|
||||
.select(&strong_selector)
|
||||
.next()
|
||||
.map(|node| node.text().collect::<String>())
|
||||
.unwrap_or_default();
|
||||
if card_title != title {
|
||||
continue;
|
||||
}
|
||||
let card_stage = card
|
||||
.select(&stage_selector)
|
||||
.next()
|
||||
.map(|node| node.text().collect::<String>())
|
||||
.unwrap_or_default();
|
||||
let style = card
|
||||
.select(&impact_selector)
|
||||
.next()
|
||||
.and_then(|node| node.value().attr("style"))
|
||||
.unwrap_or("");
|
||||
assert_eq!(card_stage, stage);
|
||||
assert!(style.contains(impact_style), "style {style:?} missing {impact_style:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("missing card title={title:?} lane={lane:?} in {board}");
|
||||
}
|
||||
|
||||
fn board_html(batch: &EffectBatch) -> String {
|
||||
batch
|
||||
.ops
|
||||
.iter()
|
||||
.find_map(|op| match op {
|
||||
Effect::Put { payload: Payload::Html(value), .. } if value.contains("class=\"lanes\"") => Some(value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("board html payload")
|
||||
}
|
||||
|
||||
fn assert_payload_not_contains(batch: &EffectBatch, needle: &str) {
|
||||
assert!(
|
||||
batch.ops.iter().all(|op| match op {
|
||||
Effect::Put { payload, .. } | Effect::Insert { payload, .. } | Effect::Prepend { payload, .. } => !payload_value(payload).contains(needle),
|
||||
Effect::Emit { payload, .. } => !payload.contains(needle),
|
||||
Effect::Navigate { url, .. } => !url.contains(needle),
|
||||
Effect::Remove { .. } | Effect::Move { .. } | Effect::Focus { .. } => true,
|
||||
}),
|
||||
"unexpected payload {needle:?} in {batch:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn payload_value(payload: &Payload) -> &str {
|
||||
match payload {
|
||||
Payload::Text(value) | Payload::Html(value) => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_text(document: &Html, text: &str) {
|
||||
let body = document.root_element().text().collect::<Vec<_>>().join(" ");
|
||||
assert!(body.contains(text), "missing text {text:?} in {body:?}");
|
||||
}
|
||||
|
||||
fn assert_selector_count_at_least(document: &Html, selector: &str, expected: usize) {
|
||||
let selector = Selector::parse(selector).unwrap();
|
||||
let count = document.select(&selector).count();
|
||||
assert!(count >= expected, "selector count {count} < {expected}");
|
||||
}
|
||||
|
||||
fn get(path: &str) -> Response {
|
||||
request("GET", path, &[], "")
|
||||
}
|
||||
|
||||
fn post(path: &str, body: &str) -> Response {
|
||||
request(
|
||||
"POST",
|
||||
path,
|
||||
&[("Content-Type", "application/x-www-form-urlencoded")],
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
fn request(method: &str, path: &str, headers: &[(&str, &str)], body: &str) -> Response {
|
||||
let mut stream = TcpStream::connect(ADDR).expect("connect to server");
|
||||
stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
write!(
|
||||
stream,
|
||||
"{method} {path} HTTP/1.1\r\nHost: {ADDR}\r\nConnection: close\r\nContent-Length: {}\r\n",
|
||||
body.len()
|
||||
)
|
||||
.unwrap();
|
||||
for (name, value) in headers {
|
||||
write!(stream, "{name}: {value}\r\n").unwrap();
|
||||
}
|
||||
write!(stream, "\r\n{body}").unwrap();
|
||||
|
||||
let mut raw = Vec::new();
|
||||
stream.read_to_end(&mut raw).unwrap();
|
||||
Response::parse(raw)
|
||||
}
|
||||
|
||||
struct Response {
|
||||
status: u16,
|
||||
headers: Vec<(String, String)>,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
fn parse(raw: Vec<u8>) -> Self {
|
||||
let split = raw
|
||||
.windows(4)
|
||||
.position(|window| window == b"\r\n\r\n")
|
||||
.expect("response header terminator");
|
||||
let head = String::from_utf8(raw[..split].to_vec()).unwrap();
|
||||
let body = raw[split + 4..].to_vec();
|
||||
let mut lines = head.lines();
|
||||
let status = lines
|
||||
.next()
|
||||
.and_then(|line| line.split_whitespace().nth(1))
|
||||
.and_then(|status| status.parse().ok())
|
||||
.expect("status code");
|
||||
let headers = lines
|
||||
.filter_map(|line| line.split_once(':'))
|
||||
.map(|(name, value)| (name.trim().to_ascii_lowercase(), value.trim().to_string()))
|
||||
.collect();
|
||||
Self { status, headers, body }
|
||||
}
|
||||
|
||||
fn header(&self, name: &str) -> &str {
|
||||
self.headers
|
||||
.iter()
|
||||
.find(|(key, _)| key == &name.to_ascii_lowercase())
|
||||
.map(|(_, value)| value.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
fn text(&self) -> &str {
|
||||
std::str::from_utf8(&self.body).unwrap()
|
||||
}
|
||||
|
||||
fn batch(&self) -> EffectBatch {
|
||||
EffectBatch::from_wire(&self.body).unwrap()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user