diff --git a/sx/sx/home-stepper.sx b/sx/sx/home-stepper.sx index b61b6a56..89e275e2 100644 --- a/sx/sx/home-stepper.sx +++ b/sx/sx/home-stepper.sx @@ -8,7 +8,8 @@ (if (client?) (def-store "home-stepper" (fn () {:step-idx (signal 16)})) nil)) (step-idx (if store (get store "step-idx") (signal 16))) (dom-stack-sig (signal (list))) - (code-tokens (signal (list)))) + (code-tokens (signal (list))) + (initial-render (signal true))) (letrec ((split-tag (fn (expr result) (cond (not (list? expr)) (append! result {:expr expr :type "leaf"}) (empty? expr) nil (not (= (type-of (first expr)) "symbol")) (append! result {:expr expr :type "leaf"}) (is-html-tag? (symbol-name (first expr))) (let ((ctag (symbol-name (first expr))) (cargs (rest expr)) (cch (list)) (cat (list)) (spreads (list)) (ckw false)) (for-each (fn (a) (cond (= (type-of a) "keyword") (do (set! ckw true) (append! cat a)) ckw (do (set! ckw false) (append! cat a)) (and (list? a) (not (empty? a)) (= (type-of (first a)) "symbol") (starts-with? (symbol-name (first a)) "~")) (do (set! ckw false) (append! spreads a)) :else (do (set! ckw false) (append! cch a)))) cargs) (append! result {:spreads spreads :tag ctag :type "open" :attrs cat}) (for-each (fn (c) (split-tag c result)) cch) (append! result {:open-attrs cat :open-spreads spreads :tag ctag :type "close"})) :else (append! result {:expr expr :type "expr"})))) (build-code-tokens @@ -263,7 +264,36 @@ (let ((dom (render-to-dom expr (get-render-env nil) nil))) (when dom (dom-append container dom))))) - (set-stack (list container)))))) + (let + ((depth 0) (all (deref steps))) + (let + loop + ((i 0)) + (when + (< i target) + (let + ((stype (get (nth all i) "type"))) + (cond + (= stype "open") + (set! depth (+ depth 1)) + (= stype "close") + (set! depth (- depth 1)))) + (loop (+ i 1)))) + (let + ((stack (list container)) (node container)) + (let + walk + ((d 0)) + (when + (< d depth) + (let + ((child (host-get node "lastElementChild"))) + (when + child + (append! stack child) + (set! node child))) + (walk (+ d 1)))) + (set-stack stack))))))) (do-back (fn () @@ -304,7 +334,7 @@ (build-code-tokens (first parsed) tokens step-ref 0) (reset! code-tokens tokens))))) (let - ((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (rebuild-preview (deref step-idx)) (update-code-highlight) (run-post-render-hooks))))))) + ((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (if (deref initial-render) (do (reset! initial-render false) (set-stack (let ((p (get-preview))) (if p (list p) (list))))) (rebuild-preview (deref step-idx))) (update-code-highlight) (run-post-render-hooks))))))) (div (~tw :tokens "space-y-4 text-center") (div diff --git a/tests/playwright/stepper.spec.js b/tests/playwright/stepper.spec.js index 232dc639..b0c6f724 100644 --- a/tests/playwright/stepper.spec.js +++ b/tests/playwright/stepper.spec.js @@ -1,58 +1,62 @@ const { test, expect } = require('playwright/test'); const { loadPage, trackErrors } = require('./helpers'); -test('stepper: no raw SX component calls visible after hydration', async ({ page }) => { - const t = trackErrors(page); - await loadPage(page, ''); +test('stepper: lake never shows raw SX source during hydration', async ({ page }) => { + // Monitor the lake content during page load to catch flashes + const lakeStates = []; + await page.context().clearCookies(); - const stepper = page.locator('[data-sx-island="home/stepper"]'); - await expect(stepper).toBeVisible({ timeout: 10000 }); + // Set up mutation observer before navigation + await page.addInitScript(() => { + window.__lakeStates = []; + const observer = new MutationObserver(() => { + const lake = document.querySelector('[data-sx-lake="home-preview"]'); + if (lake) { + const text = lake.textContent; + if (text && text.length > 0) { + window.__lakeStates.push(text.slice(0, 100)); + } + } + }); + // Start observing once DOM is ready + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + } else { + document.addEventListener('DOMContentLoaded', () => { + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + }); + } + }); - const lake = stepper.locator('[data-sx-lake]'); - await expect(lake).toBeVisible({ timeout: 5000 }); - const lakeText = await lake.textContent(); - expect(lakeText).not.toContain('~cssx/tw'); - expect(lakeText).not.toContain('~tw'); - expect(lakeText).not.toContain(':tokens'); + await page.goto('http://localhost:8013/', { waitUntil: 'networkidle' }); + await page.waitForTimeout(3000); - expect(t.errors()).toEqual([]); + const states = await page.evaluate(() => window.__lakeStates); + + // No state should contain raw SX component calls + for (const state of states) { + expect(state).not.toContain('~tw'); + expect(state).not.toContain('~cssx'); + expect(state).not.toContain(':tokens'); + } }); -test('stepper: default view shows all four words', async ({ page }) => { - // Clear stepper cookie +test('stepper: default view shows all four words after hydration', async ({ page }) => { await page.context().clearCookies(); await loadPage(page, ''); const lake = page.locator('[data-sx-lake="home-preview"]'); await expect(lake).toBeVisible({ timeout: 10000 }); - // Wait for hydration - await page.waitForTimeout(2000); + await page.waitForTimeout(3000); const text = await lake.textContent(); - // All four words should be present after hydration expect(text).toContain('the'); expect(text).toContain('joy'); expect(text).toContain('of'); expect(text).toContain('sx'); }); -test('stepper: all spans inside h1 with correct structure', async ({ page }) => { - await page.context().clearCookies(); - await loadPage(page, ''); - await page.waitForTimeout(3000); - - const lake = page.locator('[data-sx-lake="home-preview"]'); - const h1 = lake.locator('h1'); - await expect(h1).toBeVisible({ timeout: 5000 }); - - // All colored spans should be inside the h1 - const spans = h1.locator('span'); - const count = await spans.count(); - expect(count).toBeGreaterThanOrEqual(4); -}); - -test('stepper: stepping forward renders styled text', async ({ page }) => { - // Start from step 0 +test('stepper: stepped spans have colored text', async ({ page }) => { await page.context().addCookies([{ name: 'sx-home-stepper', value: '0', @@ -64,18 +68,29 @@ test('stepper: stepping forward renders styled text', async ({ page }) => { const fwdBtn = page.locator('button:has-text("▶")'); await expect(fwdBtn).toBeVisible({ timeout: 5000 }); - // Step forward 10 times to get through "of" - for (let i = 0; i < 10; i++) { + // Step forward 16 times to complete the expression + for (let i = 0; i < 16; i++) { await fwdBtn.click(); - await page.waitForTimeout(300); + await page.waitForTimeout(200); } const lake = page.locator('[data-sx-lake="home-preview"]'); - const text = await lake.textContent(); - expect(text).toContain('of'); - // The "of" text should be in a styled span (with sx- or data-tw class) - const styledSpan = lake.locator('span[data-tw]').filter({ hasText: 'of' }); - const count = await styledSpan.count(); - expect(count).toBeGreaterThan(0); + // Check each span has a non-black computed color + const colors = await lake.evaluate(el => { + const spans = el.querySelectorAll('span'); + return Array.from(spans).map(s => ({ + text: s.textContent, + color: getComputedStyle(s).color, + hasClass: s.className.length > 0 + })); + }); + + expect(colors.length).toBeGreaterThanOrEqual(4); + for (const span of colors) { + // Each span should have a class applied + expect(span.hasClass).toBe(true); + // Color should not be default black (rgb(0, 0, 0)) + expect(span.color).not.toBe('rgb(0, 0, 0)'); + } });