UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

172 lines (164 loc) • 7.5 kB
import ts from 'typescript'; import { describe, expect, it } from 'vitest'; /** * @param {string} source * @param {boolean} automatic */ function create_service(source, automatic) { const root = process.cwd(); const file_name = `${root}/packages/ripple/jsx-types-test.tsx`; const options = { strict: true, target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, jsx: ts.JsxEmit.Preserve, ...(automatic ? { jsxImportSource: 'ripple' } : {}), skipLibCheck: true, types: [], paths: { '#public': [`${root}/packages/ripple/types/index.d.ts`], ripple: [`${root}/packages/ripple/types/index.d.ts`], 'ripple/jsx-runtime': [`${root}/packages/ripple/src/jsx-runtime.d.ts`], }, }; const service = ts.createLanguageService({ getCompilationSettings: () => options, getScriptFileNames: () => [file_name], getScriptVersion: () => '0', getScriptSnapshot: (name) => { const text = name === file_name ? source : ts.sys.readFile(name); return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text); }, getCurrentDirectory: () => root, getDefaultLibFileName: ts.getDefaultLibFilePath, realpath: ts.sys.realpath, directoryExists: ts.sys.directoryExists, getDirectories: ts.sys.getDirectories, fileExists: (name) => name === file_name || ts.sys.fileExists(name), readFile: (name) => (name === file_name ? source : ts.sys.readFile(name)), }); return { service, file_name }; } describe('Ripple JSX types', () => { it.each([true, false])('checks CSS property values (automatic runtime: %s)', (automatic) => { const source = ` import type { CSSProperties, Ripple } from 'ripple'; import type { CSSProperties as RuntimeCSSProperties } from 'ripple/jsx-runtime'; const styles = { width: '24rem', height: '50%', margin: 0, padding: '1em', maxWidth: 'calc(100% - 2rem)', minWidth: 'var(--min-width)', opacity: 0.5, lineHeight: 1.5, zIndex: 2, flexGrow: 1, fontSizeAdjust: 0.5, shapeImageThreshold: 0.5, mathDepth: 2, scale: 1.5, 'font-size': '1rem', 'line-height': 1.5, 'border-width': 0, WebkitLineClamp: 2, '-webkit-line-clamp': 2, transitionDuration: '200ms', '--space': '1rem', '--scale': 2, } satisfies CSSProperties; const namespaced: Ripple.CSSProperties = styles; const runtime_styles: RuntimeCSSProperties = styles; const html = <div style={namespaced} />; const svg = <svg style={styles}><circle style={{ strokeWidth: 2, 'fill-opacity': 0.5 }} /></svg>; const zero = <div style={{ width: 0 }} />; const css_text = <div style="width: 400px; opacity: 0.5" />; const absent = <div style={null} />; const omitted = <svg style={undefined} />; const conditional = <div style={{ width: Math.random() ? '400px' : undefined, '--scale': undefined }} />; const nullable = { width: null, 'font-size': null, opacity: null, WebkitLineClamp: null, '-webkit-line-clamp': null, '--scale': null, } satisfies CSSProperties; const nullable_html = <div style={nullable} />; const nullable_svg = <svg style={nullable} />; // @ts-expect-error Lengths require explicit units unless the value is zero. const bad_styles = { width: 400 } satisfies CSSProperties; // @ts-expect-error Lengths require explicit units unless the value is zero. const bad_width = <div style={{ width: 400 }} />; // @ts-expect-error Kebab-case lengths use the same rules. const bad_font_size = <div style={{ 'font-size': 16 }} />; // @ts-expect-error SVG style objects also require units for these lengths. const bad_svg = <svg style={{ width: 400 }} />; // @ts-expect-error Vendor-prefixed lengths require units too. const bad_vendor_length = <div style={{ WebkitBorderRadius: 4 }} />; // @ts-expect-error Durations require time units, even for zero. const bad_duration = <div style={{ transitionDuration: 0 }} />; // @ts-expect-error Unknown property names do not bypass CSS checking. const bad_property = <div style={{ widht: '400px' }} />; // @ts-expect-error Custom properties accept strings and numbers, not objects. const bad_custom = <div style={{ '--theme': {} }} />; // @ts-expect-error CSS fallback arrays are not supported by the runtime. const bad_array = <div style={{ display: ['flex', 'block'] }} />; `; const { service, file_name } = create_service(source, automatic); try { expect( service .getSemanticDiagnostics(file_name) .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')), ).toEqual([]); } finally { service.dispose(); } }); it.each([true, false])('qualifies HTML tag hovers (automatic runtime: %s)', (automatic) => { const source = ` import 'ripple/jsx-runtime'; const paragraph = <p />; const input = <input />; `; const { service, file_name } = create_service(source, automatic); try { expect(service.getSemanticDiagnostics(file_name)).toEqual([]); for (const [tag, attributes, element] of [ ['p', 'HTMLAttributes', 'HTMLParagraphElement'], ['input', 'InputHTMLAttributes', 'HTMLInputElement'], ]) { const info = service.getQuickInfoAtPosition(file_name, source.indexOf(`<${tag} `) + 1); expect(ts.displayPartsToString(info?.displayParts)).toBe( `(property) Ripple.JSX.IntrinsicElements.${tag}: Ripple.DetailedHTMLProps<Ripple.${attributes}<${element}>, ${element}>`, ); } } finally { service.dispose(); } }); it('preserves JSX imports, global types, native events, and DOM refs', () => { const source = ` import type { ClassValue, JSX as RuntimeJSX, Ripple as RuntimeRipple } from 'ripple/jsx-runtime'; import type { JSX as PublicJSX, Ripple } from 'ripple'; import { createRefKey, type RefKey } from 'ripple'; const ref_key: RefKey = createRefKey(); const props: Ripple.DetailedHTMLProps<Ripple.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> = { value: 'hello', ref: (node) => { node.select(); }, [ref_key]: (node) => { node.select(); }, onInput(event) { const input: HTMLInputElement = event.currentTarget; input.select(); }, onClick: { handleEvent(event) { event.currentTarget.select(); } }, }; const global_props: JSX.IntrinsicElements['input'] = props; const runtime_props: RuntimeJSX.IntrinsicElements['input'] = global_props; const public_props: PublicJSX.IntrinsicElements['input'] = runtime_props; const runtime_namespace_props: RuntimeRipple.InputHTMLAttributes<HTMLInputElement> = public_props; const classes: ClassValue = ['one', { two: true }]; const input = <input {...public_props} class={classes} />; const svg = <svg><circle cx={10} ref={(node) => { const circle: SVGCircleElement = node; }} /></svg>; // @ts-expect-error Input values do not accept objects. const bad_value = <input value={{}} />; // @ts-expect-error Refs remain specific to the element. const bad_ref = <p ref={(node: HTMLInputElement) => { node.select(); }} />; // @ts-expect-error Event currentTarget remains specific to the element. const bad_event = <p onClick={(event) => { event.currentTarget.select(); }} />; `; const { service, file_name } = create_service(source, true); try { const diagnostics = service.getSemanticDiagnostics(file_name); expect( diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), ), ).toEqual([]); } finally { service.dispose(); } }); });