Skip to content

Commit

Permalink
test: Improve tooling and add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
maxmilton committed Jul 15, 2024
1 parent 8d0625f commit 08dfc0f
Show file tree
Hide file tree
Showing 11 changed files with 2,119 additions and 233 deletions.
2 changes: 1 addition & 1 deletion test/TestComponent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { compile } from '../src/macro' assert { type: 'macro' };
import { compile } from '../src/macro' with { type: 'macro' };
import { collect, h } from '../src/runtime';

type TestComponent = HTMLDivElement;
Expand Down
126 changes: 126 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,103 @@ declare global {
}
/* eslint-enable */

/**
* Get the total number of parameters of a function including optional
* parameters with default values.
*
* @remarks Native functions will only return the number of required parameters;
* optional parameters cannot be determined.
*
* @returns The number of parameters, including optional parameters.
*/
export function parameters(func: unknown): number {
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}

const str = func.toString();
const len = str.length;
const start = str.indexOf('(');
let index = start;
let count = 1;
let nested = 0;
let char: string;

// FIXME: Handle nested string template literals.
const string = (quote: '"' | "'" | '`') => {
while (index++ < len) {
char = str[index];

if (char === quote) {
break;
}
// skip escaped characters
if (char === '\\') {
index++;
}
}
};

while (index++ < len) {
char = str[index];

if (!nested) {
if (char === ')') {
break;
}
if (char === ',') {
count++;
continue; // eslint-disable-line no-continue
}
}

switch (char) {
case '"':
case "'":
case '`':
string(char);
break;
case '(':
case '[':
case '{':
nested++;
break;
case ')':
case ']':
case '}':
nested--;
break;
default:
break;
}
}

if (index >= len || nested !== 0) {
throw new Error('Invalid function signature');
}

// handle no parameters
if (str.slice(start + 1, index).trim().length === 0) {
if (str.indexOf('[native code]', index) >= 0) {
count = func.length;
// eslint-disable-next-line no-console
console.warn('Optional parameters cannot be determined for native functions');
} else {
count = 0;
}
}

return count;
}

declare module 'bun:test' {
interface Matchers {
/** Asserts that a value is a plain `object`. */
toBePlainObject(): void;
/** Asserts that a value is a `class`. */
toBeClass(): void;
/** Asserts that a function has a specific number of parameters. */
toHaveParameters(required: number, optional: number): void;
}
}

Expand All @@ -26,8 +119,41 @@ expect.extend({
message: () => `expected ${String(received)} to be a plain object`,
};
},

toBeClass(received: unknown) {
return typeof received === 'function' &&
/^class\s/.test(Function.prototype.toString.call(received))
? { pass: true }
: {
pass: false,
message: () => `expected ${String(received)} to be a class`,
};
},

toHaveParameters(received: unknown, required: number, optional: number) {
if (typeof received !== 'function') {
return {
pass: false,
message: () => `expected ${String(received)} to be a function`,
};
}

const actualRequired = received.length;
const actualOptional = parameters(received) - actualRequired;

return actualRequired === required && actualOptional === optional
? { pass: true }
: {
pass: false,
message: () =>
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`expected ${received.name} to have ${required}/${optional} required/optional parameters, but it has ${actualRequired}/${actualOptional}`,
};
},
});

export const originalConsoleCtor = global.console.Console;

const originalConsole = global.console;
const noop = () => {};

Expand Down
191 changes: 114 additions & 77 deletions test/unit/browser-compile.test.ts → test/unit/browser-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,101 +5,138 @@ import { collect, h, html } from '../../src/browser/runtime';
import { cleanup, render } from './utils';

describe('h', () => {
afterEach(cleanup);

test('renders basic template', () => {
expect.assertions(1);
const view = h(`
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`);
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li><li>C</li></ul>');
test('is a function', () => {
expect.assertions(2);
expect(h).toBeFunction();
expect(h).not.toBeClass();
});

test('renders basic template with messy whitespace', () => {
test('expects 1 parameters', () => {
expect.assertions(1);
const view = h(`
<ul>
<li \f\n\r\t\v\u0020\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF >A</li>
<li
>
B</li>
<li>C
</li>
</ul>
`);
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li><li>C</li></ul>');
expect(h).toHaveParameters(1, 0);
});

test('renders SVG template', () => {
expect.assertions(2);
const view = h(`
<svg>
<circle cx=10 cy='10' r="10" />
</svg>
`);
const rendered = render(view);
expect(view).toBeInstanceOf(window.SVGSVGElement);
expect(rendered.container.innerHTML).toBe(
'<svg><circle cx="10" cy="10" r="10"></circle></svg>',
);
describe('render', () => {
afterEach(cleanup);

test('renders basic template', () => {
expect.assertions(1);
const view = h(`
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`);
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li><li>C</li></ul>');
});

test('renders basic template with messy whitespace', () => {
expect.assertions(1);
const view = h(`
<ul>
<li \f\n\r\t\v\u0020\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF >A</li>
<li
>
B</li>
<li>C
</li>
</ul>
`);
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li><li>C</li></ul>');
});

test('renders SVG template', () => {
expect.assertions(2);
const view = h(`
<svg>
<circle cx=10 cy='10' r="10" />
</svg>
`);
const rendered = render(view);
expect(view).toBeInstanceOf(window.SVGSVGElement);
expect(rendered.container.innerHTML).toBe(
'<svg><circle cx="10" cy="10" r="10"></circle></svg>',
);
});

test('returns root element', () => {
expect.assertions(3);
const view = h(`
<ul id=root>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`);
const rendered = render(view);
expect(view).toBeInstanceOf(window.HTMLUListElement);
expect(view.id).toBe('root');
expect(rendered.container.firstChild).toBe(view);
});

test('removes refs in template from output DOM', () => {
expect.assertions(1);
const view = h(`
<ul @list>
<li @item-one>A</li>
<li @item-two>B</li>
</ul>
`);
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li></ul>');
});

// NOTE: This is not supported by the current implementation of the h()
// function because it would be too slow.
test.skip('does not minify in whitespace-sensitive blocks', () => {});
});
});

test('returns root element', () => {
expect.assertions(3);
const view = h(`
<ul id=root>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`);
const rendered = render(view);
expect(view).toBeInstanceOf(window.HTMLUListElement);
expect(view.id).toBe('root');
expect(rendered.container.firstChild).toBe(view);
describe('html', () => {
test('is a function', () => {
expect.assertions(2);
expect(html).toBeFunction();
expect(html).not.toBeClass();
});

test('removes refs in template from output DOM', () => {
test('expects 2 parameters (1 optional)', () => {
expect.assertions(1);
const view = h(`
<ul @list>
<li @item-one>A</li>
<li @item-two>B</li>
</ul>
`);
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li></ul>');
expect(html).toHaveParameters(1, 1);
});

// NOTE: This is not supported by the current implementation of the h()
// function because it would be too slow.
test.skip('does not minify in whitespace-sensitive blocks', () => {});
describe('render', () => {
afterEach(cleanup);

test('renders basic template', () => {
expect.assertions(1);
const view = html`
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`;
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li><li>C</li></ul>');
});
});
});

describe('html', () => {
afterEach(cleanup);
describe('collect', () => {
test('is a function', () => {
expect.assertions(2);
expect(collect).toBeFunction();
expect(collect).not.toBeClass();
});

test('renders basic template', () => {
test('expects 2 parameters', () => {
expect.assertions(1);
const view = html`
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`;
const rendered = render(view);
expect(rendered.container.innerHTML).toBe('<ul><li>A</li><li>B</li><li>C</li></ul>');
expect(collect).toHaveParameters(2, 0);
});
});

describe('collect', () => {
test('collects all refs', () => {
expect.assertions(39);
const view = h(`
Expand Down
Loading

0 comments on commit 08dfc0f

Please sign in to comment.