lupine.web
Version:
lupine.web is a extremely fast, small size and lightweight frontend framework, using React TSX syntax.
159 lines (133 loc) • 6.74 kB
text/typescript
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { renderComponentAsync } from './render-component';
import { domUniqueId } from './render-attribute';
describe('renderComponentAsync', () => {
beforeEach(() => {
mock.method(Date, 'now', () => 1775190809080);
domUniqueId(true); // reset global module state for each test
});
afterEach(() => {
mock.restoreAll();
});
// Helper to easily extract the rendered HTML string
const getHtml = (props: any) => props._html.join('');
describe('Basic HTML Elements Rendering', () => {
it('should render standard HTML tags without attributes', async () => {
const props = { _html: [] };
await renderComponentAsync('div', props);
assert.equal(getHtml(props), '<div></div>');
});
it('should render self-closing tags correctly', async () => {
const props = { _html: [], src: 'image.png', alt: 'logo' };
await renderComponentAsync('img', props);
assert.equal(getHtml(props), '<img src="image.png" alt="logo" />');
});
});
describe('Attributes Processing', () => {
it('should correctly render basic attributes', async () => {
const props = { _html: [], id: 'main', 'data-test': 'value', role: 'button' };
await renderComponentAsync('section', props);
assert.equal(getHtml(props), '<section id="main" data-test="value" role="button"></section>');
});
it('should convert object style property to inline CSS string', async () => {
const props = { _html: [], style: { backgroundColor: 'red', fontSize: '12px' } };
await renderComponentAsync('div', props);
assert.equal(getHtml(props), '<div style="background-color:red;font-size:12px;"></div>');
});
it('should process default boolean attributes (disabled, readonly, selected, checked)', async () => {
const props = { _html: [], disabled: true, readonly: 'true', checked: false, selected: 'selected' };
await renderComponentAsync('input', props);
// 'checked' is false, so it should be omitted entirely.
assert.equal(getHtml(props), '<input disabled="true" readonly="true" selected="selected" />');
});
it('should handle defaultChecked converting it to checked="true"', async () => {
const props = { _html: [], defaultChecked: true };
await renderComponentAsync('input', props);
assert.equal(getHtml(props), '<input checked="true" />');
});
});
describe('Children Content Rendering', () => {
it('should successfully render string and number children', async () => {
const props = { _html: [], children: ['Balance: ', 100] };
await renderComponentAsync('div', props);
assert.equal(getHtml(props), '<div>Balance: 100</div>');
});
it('should gracefully ignore false, null, and undefined in children array', async () => {
const props = { _html: [], children: [false, null, undefined, 'visible text'] };
await renderComponentAsync('span', props);
assert.equal(getHtml(props), '<span>visible text</span>');
});
it('should recursively render nested component children tree', async () => {
const nestedChild = { type: 'strong', props: { _html: [], children: ['bold content'] } };
const props = { _html: [], children: ['A ', nestedChild, ' end.'] };
await renderComponentAsync('p', props);
assert.equal(getHtml(props), '<p>A <strong>bold content</strong> end.</p>');
});
});
describe('DangerouslySetInnerHTML Logic', () => {
it('should render raw HTML correctly', async () => {
const props = { _html: [], dangerouslySetInnerHTML: '<span>raw injected string</span>' };
await renderComponentAsync('div', props);
assert.equal(getHtml(props), '<div><span>raw injected string</span></div>');
});
});
describe('Functional Components Resolution', () => {
it('should execute synchronous functional components', async () => {
const MockComponent = (p: any) => {
return {
type: 'h1',
props: { children: [p.title] }
};
};
const props = { _html: [], title: 'Lupine Test' };
await renderComponentAsync(MockComponent as any, props);
assert.equal(getHtml(props), '<h1>Lupine Test</h1>');
});
it('should await and execute asynchronous functional components', async () => {
const AsyncMockComponent = async (p: any) => {
// mock some async logic
await new Promise((r) => setTimeout(r, 5));
return {
type: 'span',
props: { children: ['Data Loaded'] }
};
};
const props = { _html: [] };
await renderComponentAsync(AsyncMockComponent as any, props);
assert.equal(getHtml(props), '<span>Data Loaded</span>');
});
});
describe('Fragment and Virtual Wrapper Handling', () => {
it('should omit wrappers when using string type Fragment', async () => {
const props = { _html: [], children: ['fragment inner text'] };
await renderComponentAsync('Fragment', props);
assert.equal(getHtml(props), 'fragment inner text');
});
it('should omit wrapper when using named Fragment function', async () => {
const FragmentFunc = () => { };
Object.defineProperty(FragmentFunc, 'name', { value: 'Fragment' });
const props = { _html: [], children: ['named function wrapper'] };
await renderComponentAsync(FragmentFunc as any, props);
assert.equal(getHtml(props), 'named function wrapper');
});
});
describe('CSS Mapping and Unique ID Implementations', () => {
it('should inject global and unique IDs into the class attribute matching ampersand symbol', async () => {
const props = { _html: [], class: 'base-cls &-item' };
// Passing defined uniqueClassName and referToCssId
await renderComponentAsync('div', props, 'u-id', 'g-id');
// Based on renderAttribute logic, it replaces "&" with both global and unique IDs
assert.equal(getHtml(props), '<div class="base-cls g-id-item u-id-item"></div>');
});
it('should automatically prepend unique generated ID to class list when local CSS is present', async () => {
// css must be an object, not a plain string, for processStyle to parse it correctly
const props = { _html: [], css: { '.&-item': { color: 'block' } } };
await renderComponentAsync('div', props);
const output = getHtml(props);
// Because Date.now() is mocked and baseTime is lazily initialized,
// the generated ID will always predictably be "l00"
assert.equal(output, '<div class="l00" l00><style id="sty-l00">.l00 .l00-item{color:block;}</style></div>');
});
});
});