El crítico-rebajado-a-mayor confirmado por el adversarial: 42 providers en la tubería `syncAttrs` servían HTML sin role, sin aria-*, sin data-state y sin type="button" (un Toggle dentro de un form era type="submit" — un clic pre-hidratación lo enviaba). Dos tuberías de attrs coexistían sin regla: el efecto cliente (`dom.apply`) y `renderProps()`. `partPropsForRegistration` resuelve ahora el contrato completo: statics (role, literales) + todo plan dinámico no-consumerWins, y los naming (consumerWins, A-85) al final como siempre. Consecuencias: - SSR lleva el contrato: los 42 providers lo heredan sin tocarlos (todos spreadean `.props`). - La reactividad es de render: readBindings lee fuentes rune-backed, el effect queda como re-aserción idempotente hasta C2c. - `renderProps()` pasa a devolver la misma bolsa — queda como alias hasta que sus ~70 call sites migren a `.props` (C2b) y se borre, sin shim. - El pin viejo («partProps sin attrs mutables») afirmaba el contrato contrario y se re-firma; nace ssr-contract.test.ts (server project, sin ventana) con harness de composición real — rojo antes del pliegue por los 5 attrs, verde después. Verificación: ssr-contract 6/6 · runtime.svelte.test 50/50 · soma-attr-audit verde · smoke 323/323 (hidratación limpia) · morfo:check y perm-check re-corridos (los rojos restantes son fase A adjudicada, no regresión de esto). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>alpha-0.1-background
parent
2ec0181079
commit
3ca138e913
@ -0,0 +1,54 @@
|
||||
// Deliberately NO environment directive: the server project is `node`, where
|
||||
// `window` does not exist — the same shape as SSR. (Never name the directive
|
||||
// literally in a comment here: vitest matches the token anywhere in the file
|
||||
// and would silently switch this suite to jsdom — the trap the eidos SSR
|
||||
// suite documents.)
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
|
||||
import Harness from './test/ssr-harness.svelte';
|
||||
|
||||
/**
|
||||
* P0 fase C (audit 2026-08-26): the morfo contract must reach the
|
||||
* server-rendered HTML. Before the render-bag unification, every part on the
|
||||
* `syncAttrs` pipeline server-rendered with no `role`, no `aria-*`, no
|
||||
* `data-state` and no `type="button"` — a Toggle inside a form defaulted to
|
||||
* `type="submit"` and a pre-hydration click submitted it.
|
||||
*
|
||||
* Two representatives pin the shared mechanism (`partPropsForRegistration`):
|
||||
* Toggle (literal attr + aria-boolean + data-state) and RadioGroup
|
||||
* (role on provider and items + aria-checked). The fix lives in the runtime,
|
||||
* so every provider inherits it; these assertions are the canary, not the
|
||||
* census.
|
||||
*/
|
||||
describe('morfo contract attrs reach server-rendered HTML', () => {
|
||||
it('the environment really has no window (guards the directive trap)', () => {
|
||||
expect(typeof window).toBe('undefined');
|
||||
});
|
||||
|
||||
const html = () => render(Harness, { props: {} }).body;
|
||||
|
||||
it('Toggle server-renders its literal type="button"', () => {
|
||||
expect(html()).toContain('type="button"');
|
||||
});
|
||||
|
||||
it('Toggle server-renders aria-pressed from its state', () => {
|
||||
expect(html()).toContain('aria-pressed="true"');
|
||||
});
|
||||
|
||||
it('Toggle server-renders data-state', () => {
|
||||
expect(html()).toMatch(/data-toggle[^>]*data-state="on"/);
|
||||
});
|
||||
|
||||
it('RadioGroup server-renders role="radiogroup"', () => {
|
||||
expect(html()).toContain('role="radiogroup"');
|
||||
});
|
||||
|
||||
it('RadioGroup items server-render role="radio" with aria-checked', () => {
|
||||
const body = html();
|
||||
expect(body).toContain('role="radio"');
|
||||
expect(body).toContain('aria-checked="true"');
|
||||
expect(body).toContain('aria-checked="false"');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
// Minimal real composition (no mocks — CLAUDE.md): the same boot the demo
|
||||
// layout performs, without events/prefs/eidos. On the server createActiveUix
|
||||
// degrades (no dom), which is exactly the environment under test.
|
||||
import { createActiveUix, setActiveUix } from '$active-uix';
|
||||
import { Soma } from '$soma/core/soma.svelte';
|
||||
import * as Toggle from '$soma/components/toggle';
|
||||
import * as RadioGroup from '$soma/components/radio-group';
|
||||
|
||||
const uix = createActiveUix({ langs: { schema: {}, defaultLocale: 'es' } });
|
||||
setActiveUix(uix);
|
||||
Soma.create();
|
||||
</script>
|
||||
|
||||
<Toggle.Provider pressed={true}>Bold</Toggle.Provider>
|
||||
|
||||
<RadioGroup.Provider value="a">
|
||||
<RadioGroup.Item value="a">A</RadioGroup.Item>
|
||||
<RadioGroup.Item value="b">B</RadioGroup.Item>
|
||||
</RadioGroup.Provider>
|
||||
Loading…
Reference in new issue