ripple
Version:
Ripple is an elegant TypeScript UI framework
145 lines (131 loc) • 4.22 kB
JavaScript
import { describe, it, expect } from 'vitest';
import { flushSync } from 'ripple';
import { render } from 'ripple/server';
import { hydrateComponent, container, stripHydrationMarkers } from '../setup-hydration.js';
import * as ServerComponents from './compiled/server/fragment-cursor.js';
import * as ClientComponents from './compiled/client/fragment-cursor.js';
// Component roots, mostly fragments, whose last node the hydration cursor
// must end on. Each is hydrated on its own and as `Wrap<Name>`: a child
// followed by siblings that hydrate wrongly if the cursor is off by a node.
const shapes = [
'TrailingNavigatedElements',
'TrailingStaticNavigatedElements',
'NavigatedThenStatic',
'LeadingNavigatedThenStatic',
'TrailingNestedNavigated',
'NestedNavigatedThenStatic',
'TrackedTextThenStatic',
'StaticThenTrackedText',
'StaticNestedThenStatic',
'AllStatic',
'TrailingDynamicChild',
'DynamicChildThenStatic',
'IfThenStatic',
'StaticThenIf',
'CompThenStatic',
'StaticThenComp',
'SiblingComps',
'UntrackedTextThenStatic',
'NestedFragmentThenStatic',
'TrailingNestedFragment',
'ForThenStatic',
'SwitchThenStatic',
'TryThenStatic',
'StyleThenStatic',
'CollectionThenStatic',
'InlineElementThenStatic',
'IfOnly',
'IfThenOne',
'SingleRootWithIf',
'IfTwoThenStatic',
'StaticCallThenStatic',
'StaticThenStyleThenStatic',
];
// Single-root shapes whose children end on a slot the cursor must step past.
const rootShapes = [
'ExprThenSiblingInDiv',
'IfTwoInDiv',
'ForTwoNodeItems',
'CompThenStaticInDiv',
'SiblingCompsInDiv',
'IfSwapThenStaticInDiv',
'ForThenStaticInDiv',
'CompThenStatementThenStaticInDiv',
];
/**
* Server renders and hydrates a component, asserting the hydrated DOM matches
* the server markup (no duplicated or dropped nodes).
* @param {string} name
*/
async function hydrateShape(name) {
const { body } = await render(ServerComponents[name]);
await hydrateComponent(ServerComponents[name], ClientComponents[name]);
expect(container.innerHTML).toBeHtml(stripHydrationMarkers(body));
}
/**
* Clicks the shape's own `.inc` button (when it has one) and checks the
* hydrated `.count` text updates in place.
*/
function clickInner() {
const inc = container.querySelector('.inc');
const count = container.querySelector('.count');
if (inc && count) {
expect(count.textContent).toBe('0');
inc.click();
flushSync();
expect(count.textContent).toBe('1');
}
}
describe('hydration > fragment cursor', () => {
describe('fragment as the root', () => {
for (const name of shapes) {
it(`hydrates ${name}`, async () => {
await hydrateShape(name);
clickInner();
});
}
});
describe('slot followed by siblings inside an element', () => {
for (const name of rootShapes) {
it(`hydrates ${name}`, async () => {
await hydrateShape(name);
clickInner();
const after = container.querySelector('.after');
const inc = container.querySelector('.outer-inc');
if (after && inc) {
expect(after.textContent).toBe('0');
inc.click();
flushSync();
expect(after.textContent).toBe('1');
}
});
}
});
it('swaps an @if branch before its static element sibling after hydration', async () => {
await hydrateShape('IfSwapThenStaticInDiv');
const outer = container.querySelector('.outer');
expect(outer?.querySelector('.even')).not.toBeNull();
container.querySelector('.outer-inc')?.click();
flushSync();
expect(outer?.querySelector('.even')).toBeNull();
expect(outer?.children[0]?.className).toBe('odd');
expect(outer?.children[1]?.className).toBe('after');
container.querySelector('.outer-inc')?.click();
flushSync();
expect(outer?.children[0]?.className).toBe('even');
expect(outer?.children[1]?.textContent).toBe('2');
});
describe('fragment as a child with trailing siblings', () => {
for (const name of shapes) {
it(`hydrates ${name} followed by siblings`, async () => {
await hydrateShape(`Wrap${name}`);
const after = container.querySelector('.after');
expect(after?.textContent).toBe('0');
container.querySelector('.outer-inc')?.click();
flushSync();
expect(after?.textContent).toBe('1');
clickInner();
});
}
});
});