Use dom-on for event handlers; add CI config and stepper Playwright test
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
- web/orchestration.sx, web/signals.sx: dom-listen → dom-on (trampoline wrapper that resolves TCO thunks from Lambda event handlers) - .gitea/: CI workflow and Dockerfile for automated test runs - tests/playwright/stepper.spec.js: stepper widget smoke test - Remove stale artdag .pyc file Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
.git
|
.git
|
||||||
.gitea
|
.gitea/workflows
|
||||||
.env
|
.env
|
||||||
_snapshot
|
_snapshot
|
||||||
docs
|
docs
|
||||||
|
|||||||
84
.gitea/Dockerfile.test
Normal file
84
.gitea/Dockerfile.test
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# CI test image — Python 3 + Node.js + OCaml 5.2 + dune.
|
||||||
|
#
|
||||||
|
# Build chain:
|
||||||
|
# 1. Compile OCaml from checked-in sx_ref.ml — produces sx_server.exe
|
||||||
|
# 2. Bootstrap JS (sx-browser.js) — OcamlSync transpiler → JS
|
||||||
|
# 3. Re-bootstrap OCaml (sx_ref.ml) — OcamlSync transpiler → OCaml
|
||||||
|
# 4. Recompile OCaml with fresh sx_ref.ml — final native binary
|
||||||
|
#
|
||||||
|
# Test suites (run at CMD):
|
||||||
|
# - JS standard + full tests — Node
|
||||||
|
# - OCaml spec tests — native binary
|
||||||
|
# - OCaml bridge integration tests — Python + OCaml subprocess
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker build -f .gitea/Dockerfile.test -t sx-test .
|
||||||
|
# docker run --rm sx-test
|
||||||
|
|
||||||
|
FROM ocaml/opam:debian-12-ocaml-5.2
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 ca-certificates curl xz-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
# Node.js — direct binary (avoids the massive Debian nodejs dep tree)
|
||||||
|
RUN NODE_VERSION=22.22.1 \
|
||||||
|
&& ARCH=$(dpkg --print-architecture | sed 's/amd64/x64/;s/arm64/arm64/;s/armhf/armv7l/') \
|
||||||
|
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
|
||||||
|
| tar -xJ --strip-components=1 -C /usr/local
|
||||||
|
USER opam
|
||||||
|
|
||||||
|
# Install dune into the opam switch
|
||||||
|
RUN opam install dune -y
|
||||||
|
|
||||||
|
# Bake the opam switch PATH into the image so dune/ocamlfind work in RUN
|
||||||
|
ENV PATH="/home/opam/.opam/5.2/bin:${PATH}"
|
||||||
|
|
||||||
|
WORKDIR /home/opam/project
|
||||||
|
|
||||||
|
# Copy OCaml sources first (changes less often → better caching)
|
||||||
|
COPY --chown=opam:opam hosts/ocaml/dune-project ./hosts/ocaml/
|
||||||
|
COPY --chown=opam:opam hosts/ocaml/lib/ ./hosts/ocaml/lib/
|
||||||
|
COPY --chown=opam:opam hosts/ocaml/bin/ ./hosts/ocaml/bin/
|
||||||
|
|
||||||
|
# Copy spec, web, shared (needed by bootstrappers + tests)
|
||||||
|
COPY --chown=opam:opam spec/ ./spec/
|
||||||
|
COPY --chown=opam:opam web/ ./web/
|
||||||
|
COPY --chown=opam:opam shared/sx/ ./shared/sx/
|
||||||
|
COPY --chown=opam:opam shared/__init__.py ./shared/__init__.py
|
||||||
|
|
||||||
|
# Copy JS host (bootstrapper + test runner)
|
||||||
|
COPY --chown=opam:opam hosts/javascript/ ./hosts/javascript/
|
||||||
|
|
||||||
|
# Copy OCaml host (bootstrapper + transpiler)
|
||||||
|
COPY --chown=opam:opam hosts/ocaml/bootstrap.py ./hosts/ocaml/bootstrap.py
|
||||||
|
COPY --chown=opam:opam hosts/ocaml/transpiler.sx ./hosts/ocaml/transpiler.sx
|
||||||
|
|
||||||
|
# Create output directory for JS builds
|
||||||
|
RUN mkdir -p shared/static/scripts
|
||||||
|
|
||||||
|
# Step 1: Compile OCaml from checked-in sx_ref.ml
|
||||||
|
# → produces sx_server.exe (needed by both JS and OCaml bootstrappers)
|
||||||
|
RUN cd hosts/ocaml && dune build
|
||||||
|
|
||||||
|
# Step 2: Bootstrap JS (uses sx_server.exe via OcamlSync)
|
||||||
|
RUN python3 hosts/javascript/cli.py \
|
||||||
|
--output shared/static/scripts/sx-browser.js \
|
||||||
|
&& python3 hosts/javascript/cli.py \
|
||||||
|
--extensions continuations --spec-modules types \
|
||||||
|
--output shared/static/scripts/sx-full-test.js
|
||||||
|
|
||||||
|
# Step 3: Re-bootstrap OCaml (transpile current spec → fresh sx_ref.ml)
|
||||||
|
RUN python3 hosts/ocaml/bootstrap.py \
|
||||||
|
--output hosts/ocaml/lib/sx_ref.ml
|
||||||
|
|
||||||
|
# Step 4: Recompile OCaml with freshly bootstrapped sx_ref.ml
|
||||||
|
RUN cd hosts/ocaml && dune build
|
||||||
|
|
||||||
|
# Default: run all tests
|
||||||
|
COPY --chown=opam:opam .gitea/run-ci-tests.sh ./run-ci-tests.sh
|
||||||
|
RUN chmod +x run-ci-tests.sh
|
||||||
|
|
||||||
|
CMD ["./run-ci-tests.sh"]
|
||||||
115
.gitea/run-ci-tests.sh
Executable file
115
.gitea/run-ci-tests.sh
Executable file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ===========================================================================
|
||||||
|
# run-ci-tests.sh — CI test runner for SX language suite.
|
||||||
|
#
|
||||||
|
# Runs JS + OCaml tests. No Python evaluator (eliminated).
|
||||||
|
# Exit non-zero if any suite fails.
|
||||||
|
# ===========================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FAILURES=()
|
||||||
|
PASSES=()
|
||||||
|
|
||||||
|
run_suite() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo " $name"
|
||||||
|
echo "============================================================"
|
||||||
|
if "$@"; then
|
||||||
|
PASSES+=("$name")
|
||||||
|
else
|
||||||
|
FAILURES+=("$name")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 1. JS standard tests
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
run_suite "JS standard (spec tests)" \
|
||||||
|
node hosts/javascript/run_tests.js
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 2. JS full tests (continuations + types + VM)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
run_suite "JS full (spec + continuations + types + VM)" \
|
||||||
|
node hosts/javascript/run_tests.js --full
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 3. OCaml spec tests
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
run_suite "OCaml (spec tests)" \
|
||||||
|
hosts/ocaml/_build/default/bin/run_tests.exe
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 4. OCaml bridge integration (custom special forms, web-forms.sx)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
run_suite "OCaml bridge — custom special forms + web-forms" \
|
||||||
|
python3 -c "
|
||||||
|
from shared.sx.ocaml_sync import OcamlSync
|
||||||
|
bridge = OcamlSync()
|
||||||
|
for f in ['spec/parser.sx', 'spec/render.sx', 'web/adapter-html.sx', 'web/adapter-sx.sx', 'web/web-forms.sx', 'spec/freeze.sx']:
|
||||||
|
bridge.load(f)
|
||||||
|
ok = 0; fail = 0
|
||||||
|
def check(name, expr, expected=None):
|
||||||
|
global ok, fail
|
||||||
|
try:
|
||||||
|
r = bridge.eval(expr)
|
||||||
|
if expected is not None and r != expected:
|
||||||
|
print(f' FAIL: {name}: expected {expected!r}, got {r!r}'); fail += 1
|
||||||
|
else:
|
||||||
|
print(f' PASS: {name}'); ok += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f' FAIL: {name}: {e}'); fail += 1
|
||||||
|
|
||||||
|
for form in ['defhandler', 'defquery', 'defaction', 'defpage', 'defrelation', 'defstyle', 'deftype', 'defeffect']:
|
||||||
|
check(f'{form} registered', f'(has-key? *custom-special-forms* \"{form}\")', 'true')
|
||||||
|
|
||||||
|
check('deftype via eval', '(deftype test-t number)', 'nil')
|
||||||
|
check('defeffect via eval', '(defeffect test-e)', 'nil')
|
||||||
|
check('defstyle via eval', '(defstyle my-s \"bold\")', 'bold')
|
||||||
|
check('defhandler via eval', '(has-key? (defhandler test-h (&key x) x) \"__type\")', 'true')
|
||||||
|
|
||||||
|
check('definition-form-extensions populated', '(> (len *definition-form-extensions*) 0)', 'true')
|
||||||
|
check('RENDER_HTML_FORMS has defstyle', '(contains? RENDER_HTML_FORMS \"defstyle\")', 'true')
|
||||||
|
|
||||||
|
bridge2 = OcamlSync()
|
||||||
|
bridge2.eval('(register-special-form! \"shadow-test\" (fn (args env) 42))')
|
||||||
|
bridge2.load('spec/evaluator.sx')
|
||||||
|
check('custom form survives evaluator.sx load',
|
||||||
|
bridge2.eval('(has-key? *custom-special-forms* \"shadow-test\")'), 'true')
|
||||||
|
bridge2.eval('(register-special-form! \"post-load\" (fn (args env) 99))')
|
||||||
|
check('custom form callable after evaluator.sx load',
|
||||||
|
bridge2.eval('(post-load 1)'), '99')
|
||||||
|
|
||||||
|
print(f'\nResults: {ok} passed, {fail} failed')
|
||||||
|
import sys; sys.exit(1 if fail > 0 else 0)
|
||||||
|
"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo " CI TEST SUMMARY"
|
||||||
|
echo "============================================================"
|
||||||
|
for p in "${PASSES[@]}"; do
|
||||||
|
echo " PASS: $p"
|
||||||
|
done
|
||||||
|
for f in "${FAILURES[@]}"; do
|
||||||
|
echo " FAIL: $f"
|
||||||
|
done
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
if [ ${#FAILURES[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo " ${#FAILURES[@]} suite(s) FAILED"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " All ${#PASSES[@]} suites passed."
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Deploy
|
name: Test, Build, and Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -10,7 +10,7 @@ env:
|
|||||||
BUILD_DIR: /root/rose-ash-ci
|
BUILD_DIR: /root/rose-ash-ci
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
test-build-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -29,12 +29,11 @@ jobs:
|
|||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
|
||||||
- name: Build and deploy changed apps
|
- name: Sync CI build directory
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
run: |
|
run: |
|
||||||
ssh "root@$DEPLOY_HOST" "
|
ssh "root@$DEPLOY_HOST" "
|
||||||
# --- Build in isolated CI directory (never touch dev working tree) ---
|
|
||||||
BUILD=${{ env.BUILD_DIR }}
|
BUILD=${{ env.BUILD_DIR }}
|
||||||
ORIGIN=\$(git -C ${{ env.APP_DIR }} remote get-url origin)
|
ORIGIN=\$(git -C ${{ env.APP_DIR }} remote get-url origin)
|
||||||
if [ ! -d \"\$BUILD/.git\" ]; then
|
if [ ! -d \"\$BUILD/.git\" ]; then
|
||||||
@@ -43,6 +42,31 @@ jobs:
|
|||||||
cd \"\$BUILD\"
|
cd \"\$BUILD\"
|
||||||
git fetch origin
|
git fetch origin
|
||||||
git reset --hard origin/${{ github.ref_name }}
|
git reset --hard origin/${{ github.ref_name }}
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Test SX language suite
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
run: |
|
||||||
|
ssh "root@$DEPLOY_HOST" "
|
||||||
|
cd ${{ env.BUILD_DIR }}
|
||||||
|
|
||||||
|
echo '=== Building SX test image ==='
|
||||||
|
docker build \
|
||||||
|
-f .gitea/Dockerfile.test \
|
||||||
|
-t sx-test:${{ github.sha }} \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo '=== Running SX tests ==='
|
||||||
|
docker run --rm sx-test:${{ github.sha }}
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Build and deploy changed apps
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
run: |
|
||||||
|
ssh "root@$DEPLOY_HOST" "
|
||||||
|
cd ${{ env.BUILD_DIR }}
|
||||||
|
|
||||||
# Detect changes using push event SHAs (not local checkout state)
|
# Detect changes using push event SHAs (not local checkout state)
|
||||||
BEFORE='${{ github.event.before }}'
|
BEFORE='${{ github.event.before }}'
|
||||||
|
|||||||
Binary file not shown.
27
tests/playwright/stepper.spec.js
Normal file
27
tests/playwright/stepper.spec.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const { test, expect } = require('playwright/test');
|
||||||
|
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
|
||||||
|
|
||||||
|
test('home page stepper: no raw SX component calls visible', async ({ page }) => {
|
||||||
|
await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
const stepper = page.locator('[data-sx-island="home/stepper"]');
|
||||||
|
await expect(stepper).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const text = await stepper.textContent();
|
||||||
|
// Should NOT show raw component calls
|
||||||
|
expect(text).not.toContain('~cssx/tw');
|
||||||
|
expect(text).not.toContain(':tokens');
|
||||||
|
|
||||||
|
// Should show rendered content (colored text)
|
||||||
|
expect(text.length).toBeGreaterThan(10);
|
||||||
|
|
||||||
|
// Stepper navigation should work
|
||||||
|
const buttons = stepper.locator('button');
|
||||||
|
await expect(buttons).toHaveCount(2);
|
||||||
|
const textBefore = await stepper.textContent();
|
||||||
|
await buttons.last().click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const textAfter = await stepper.textContent();
|
||||||
|
expect(textAfter).not.toBe(textBefore);
|
||||||
|
});
|
||||||
@@ -1107,7 +1107,7 @@
|
|||||||
(mark-processed! el (str "on:" event-name))
|
(mark-processed! el (str "on:" event-name))
|
||||||
;; Parse body as SX, bind handler that evaluates it
|
;; Parse body as SX, bind handler that evaluates it
|
||||||
(let ((exprs (sx-parse body)))
|
(let ((exprs (sx-parse body)))
|
||||||
(dom-listen el event-name
|
(dom-on el event-name
|
||||||
(fn (e)
|
(fn (e)
|
||||||
(let ((handler-env (env-extend (dict))))
|
(let ((handler-env (env-extend (dict))))
|
||||||
(env-bind! handler-env "event" e)
|
(env-bind! handler-env "event" e)
|
||||||
@@ -1211,7 +1211,7 @@
|
|||||||
(mark-processed! el "emit")
|
(mark-processed! el "emit")
|
||||||
(let ((event-name (dom-get-attr el "data-sx-emit")))
|
(let ((event-name (dom-get-attr el "data-sx-emit")))
|
||||||
(when event-name
|
(when event-name
|
||||||
(dom-listen el "click"
|
(dom-on el "click"
|
||||||
(fn (e)
|
(fn (e)
|
||||||
(let ((detail-json (dom-get-attr el "data-sx-emit-detail"))
|
(let ((detail-json (dom-get-attr el "data-sx-emit-detail"))
|
||||||
(detail (if detail-json (json-parse detail-json) (dict))))
|
(detail (if detail-json (json-parse detail-json) (dict))))
|
||||||
|
|||||||
@@ -425,7 +425,7 @@
|
|||||||
|
|
||||||
(define on-event :effects [io]
|
(define on-event :effects [io]
|
||||||
(fn (el (event-name :as string) (handler :as lambda))
|
(fn (el (event-name :as string) (handler :as lambda))
|
||||||
(dom-listen el event-name handler)))
|
(dom-on el event-name handler)))
|
||||||
|
|
||||||
;; Convenience: create an effect that listens for a DOM event on an
|
;; Convenience: create an effect that listens for a DOM event on an
|
||||||
;; element and writes the event detail (or a transformed value) into
|
;; element and writes the event detail (or a transformed value) into
|
||||||
@@ -436,7 +436,7 @@
|
|||||||
(define bridge-event :effects [mutation io]
|
(define bridge-event :effects [mutation io]
|
||||||
(fn (el (event-name :as string) (target-signal :as signal) transform-fn)
|
(fn (el (event-name :as string) (target-signal :as signal) transform-fn)
|
||||||
(effect (fn ()
|
(effect (fn ()
|
||||||
(let ((remove (dom-listen el event-name
|
(let ((remove (dom-on el event-name
|
||||||
(fn (e)
|
(fn (e)
|
||||||
(let ((detail (event-detail e))
|
(let ((detail (event-detail e))
|
||||||
(new-val (if transform-fn
|
(new-val (if transform-fn
|
||||||
|
|||||||
Reference in New Issue
Block a user