diff --git a/.npmignore b/.npmignore index 553d36a..56496f8 100644 --- a/.npmignore +++ b/.npmignore @@ -9,4 +9,5 @@ docs/ .husky/ .github/ .vscode/ +.gitpod.yml .vercel/ \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 0fe4dcd..c2e3325 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,10 +3,8 @@ import { Config } from '@jest/types'; const options: Config.InitialOptions = { testEnvironment: 'jsdom', preset: 'ts-jest', - globals: { - 'ts-jest': { - tsconfig: 'test/tsconfig.json' - } + transform: { + '.+\\.spec\\.tsx?$': ['ts-jest', { tsconfig: 'test/tsconfig.json' }] } }; export default options; diff --git a/package.json b/package.json index 29faa94..4a1a9c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-cell", - "version": "3.0.0-rc.17", + "version": "3.0.0-rc.18", "description": "Web Components engine based on VDOM, JSX, MobX & TypeScript", "keywords": [ "web", @@ -51,7 +51,7 @@ "element-internals-polyfill": "^1.3.11", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "husky": "^9.1.2", + "husky": "^9.1.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jsdom": "^24.1.1", @@ -70,7 +70,7 @@ }, "scripts": { "prepare": "husky", - "test": "lint-staged", + "test": "lint-staged && jest", "clean": "rimraf .parcel-cache/ dist/ docs/", "preview": "npm run clean && cd preview/ && parcel --dist-dir=../docs/preview/ --open", "pack-preview": "rimraf .parcel-cache/ docs/preview/ && cd preview/ && parcel build --public-url=. --dist-dir=../docs/preview/", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30da9c5..02be882 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,8 @@ importers: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) husky: - specifier: ^9.1.2 - version: 9.1.2 + specifier: ^9.1.3 + version: 9.1.3 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@20.12.12)(typescript@5.5.4)) @@ -1830,8 +1830,8 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - husky@9.1.2: - resolution: {integrity: sha512-1/aDMXZdhr1VdJJTLt6e7BipM0Jd9qkpubPiIplon1WmCeOy3nnzsCMeBqS9AsL5ioonl8F8y/F2CLOmk19/Pw==} + husky@9.1.3: + resolution: {integrity: sha512-ET3TQmQgdIu0pt+jKkpo5oGyg/4MQZpG6xcam5J5JyNJV+CBT23OBpCF15bKHKycRyMH9k6ONy8g2HdGIsSkMQ==} engines: {node: '>=18'} hasBin: true @@ -5326,7 +5326,7 @@ snapshots: human-signals@5.0.0: {} - husky@9.1.2: {} + husky@9.1.3: {} iconv-lite@0.6.3: dependencies: diff --git a/source/WebCell.tsx b/source/WebCell.tsx index 21b0179..c64e671 100644 --- a/source/WebCell.tsx +++ b/source/WebCell.tsx @@ -56,11 +56,13 @@ export function component(meta: ComponentMeta) { { declare props: WebCellProps; - internals = this.attachInternals(); + internals = this.tagName.includes('-') + ? this.attachInternals() + : undefined; renderer = new DOMRenderer(); get root(): ParentNode { - return this.internals.shadowRoot || this; + return (this.internals || this).shadowRoot || this; } mounted = false; declare mountedCallback?: () => any; @@ -68,7 +70,7 @@ export function component(meta: ComponentMeta) { constructor() { super(); - if (meta.mode && !this.internals.shadowRoot) + if (meta.mode && !this.internals?.shadowRoot) this.attachShadow(meta as ShadowRootInit); } diff --git a/test/Async.spec.tsx b/test/Async.spec.tsx index 027e121..fc3af9d 100644 --- a/test/Async.spec.tsx +++ b/test/Async.spec.tsx @@ -1,28 +1,33 @@ import 'element-internals-polyfill'; import { sleep } from 'web-utility'; +import { configure } from 'mobx'; -import { WebCellProps } from '../source/utility/vDOM'; -import { createCell, render } from '../source/renderer'; +import { WebCellProps } from '../source/WebCell'; +import { DOMRenderer } from 'dom-renderer'; +import { FC } from '../source/decorator'; import { lazy } from '../source/Async'; +configure({ enforceActions: 'never' }); + describe('Async Box component', () => { + const renderer = new DOMRenderer(); + it('should render an Async Component', async () => { - const Async = lazy(async () => ({ - default: ({ - defaultSlot, - ...props - }: WebCellProps) => ( - {defaultSlot} - ) - })); - render(Test); - - expect(document.body.innerHTML).toBe(''); + const Sync: FC> = ({ + children, + ...props + }) => {children}; + + const Async = lazy(async () => ({ default: Sync })); + + renderer.render(Test); + + expect(document.body.innerHTML).toBe(''); await sleep(); - // expect(document.body.innerHTML).toBe( - // 'Test' - // ); + expect(document.body.innerHTML).toBe( + 'Test' + ); }); }); diff --git a/test/MobX.spec.tsx b/test/MobX.spec.tsx index df92ca2..7e323eb 100644 --- a/test/MobX.spec.tsx +++ b/test/MobX.spec.tsx @@ -1,23 +1,27 @@ import 'element-internals-polyfill'; + import { sleep } from 'web-utility'; -import { observable } from 'mobx'; +import { configure, observable } from 'mobx'; + +import { observer, reaction } from '../source/decorator'; +import { component } from '../source/WebCell'; +import { DOMRenderer } from 'dom-renderer'; -import { component, observer, reaction } from '../source/decorator'; -import { WebCell } from '../source/WebCell'; -import { createCell, render } from '../source/renderer'; +configure({ enforceActions: 'never' }); class Test { @observable - count = 0; + accessor count = 0; } describe('Observer decorator', () => { - const model = new Test(); + const model = new Test(), + renderer = new DOMRenderer(); it('should re-render Function Component', () => { const InlineTag = observer(() => {model.count}); - render(); + renderer.render(); expect(document.body.textContent.trim()).toBe('0'); @@ -27,22 +31,18 @@ describe('Observer decorator', () => { }); it('should re-render Class Component', () => { - @component({ - tagName: 'test-tag' - }) + @component({ tagName: 'test-tag' }) @observer - class TestTag extends WebCell() { + class TestTag extends HTMLElement { render() { return {model.count}; } } - - render(); + renderer.render(); expect(document.querySelector('test-tag i').textContent.trim()).toBe( '1' ); - model.count++; expect(document.querySelector('test-tag i').textContent.trim()).toBe( @@ -53,31 +53,29 @@ describe('Observer decorator', () => { it('should register a Reaction with MobX', async () => { const handler = jest.fn(); - @component({ - tagName: 'reaction-cell' - }) + @component({ tagName: 'reaction-cell' }) @observer - class ReactionCell extends WebCell() { + class ReactionCell extends HTMLElement { @observable - test = ''; + accessor test = ''; - @reaction((element: ReactionCell) => element.test) + @reaction(({ test }) => test) handleReaction(value: string) { handler(value); } } - render(); + renderer.render(); + + await sleep(); const tag = document.querySelector('reaction-cell'); tag.test = 'a'; await sleep(); - expect(handler).toBeCalledTimes(1); - expect(handler).toBeCalledWith('a'); + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith('a'); document.body.innerHTML = ''; - - expect(tag.disposers).toHaveLength(0); }); }); diff --git a/test/WebCell.spec.tsx b/test/WebCell.spec.tsx index b6e2c03..30ed5de 100644 --- a/test/WebCell.spec.tsx +++ b/test/WebCell.spec.tsx @@ -1,92 +1,93 @@ import 'element-internals-polyfill'; + import { sleep, stringifyCSS } from 'web-utility'; -import { observable } from 'mobx'; +import { configure, observable } from 'mobx'; + +import { observer, attribute } from '../source/decorator'; +import { component, on, WebCell, WebCellProps } from '../source/WebCell'; +import { DOMRenderer } from 'dom-renderer'; -import { component, observer, attribute, on } from '../source/decorator'; -import { WebCell } from '../source/WebCell'; -import { Fragment, render, createCell } from '../source/renderer'; -import type { WebCellProps } from '../source/utility/vDOM'; +configure({ enforceActions: 'never' }); describe('Base Class & Decorator', () => { + const renderer = new DOMRenderer(); + it('should define a Custom Element', () => { @component({ tagName: 'x-first', mode: 'open' }) - class XFirst extends WebCell() {} + class XFirst extends HTMLElement {} - render(); + renderer.render(); - expect(self.customElements.get('x-first')).toBe(XFirst); + expect(customElements.get('x-first')).toBe(XFirst); expect(document.body.lastElementChild.tagName).toBe('X-FIRST'); }); - it('should inject CSS into Shadow Root', () => { + it('should inject CSS into Shadow Root', async () => { @component({ tagName: 'x-second', mode: 'open' }) - class XSecond extends WebCell() { - private innerStyle = ( - - ); + class XSecond extends HTMLElement { + private innerStyle = stringifyCSS({ + h2: { color: 'red' } + }); render() { return ( <> - {this.innerStyle} +

); } } - render(); + renderer.render(); + + await sleep(); - const tag = document.body.lastElementChild as XSecond; + const { shadowRoot } = document.body.lastElementChild as XSecond; - expect(tag.toString()).toBe(`

`); }); it('should put .render() returned DOM into .children of a Custom Element', () => { - @component({ - tagName: 'x-third' - }) - class XThird extends WebCell() { + @component({ tagName: 'x-third' }) + class XThird extends HTMLElement { render() { return

; } } + renderer.render(); - render(); - - const tag = document.body.lastElementChild; + const { shadowRoot, innerHTML } = document.body.lastElementChild; - expect(tag.shadowRoot).toBeNull(); - expect(tag.innerHTML).toBe('

'); + expect(shadowRoot).toBeNull(); + expect(innerHTML).toBe('

'); }); it('should update Property & Attribute by watch() & attribute() decorators', async () => { - @component({ - tagName: 'x-fourth' - }) + interface XFourthProps extends WebCellProps { + name?: string; + } + interface XFourth extends WebCell {} + + @component({ tagName: 'x-fourth' }) @observer - class XFourth extends WebCell<{ name?: string } & WebCellProps>() { + class XFourth extends HTMLElement implements WebCell { @attribute @observable - name: string; + accessor name: string | undefined; render() { return

{this.name}

; } } - - render(); + renderer.render(); const tag = document.body.lastElementChild as XFourth; @@ -107,13 +108,16 @@ describe('Base Class & Decorator', () => { }); it('should delegate DOM Event by on() decorator', () => { - @component({ - tagName: 'x-firth' - }) + interface XFirthProps extends WebCellProps { + name?: string; + } + interface XFirth extends WebCell {} + + @component({ tagName: 'x-firth' }) @observer - class XFirth extends WebCell<{ name?: string } & WebCellProps>() { + class XFirth extends HTMLElement implements WebCell { @observable - name: string; + accessor name: string | undefined; @on('click', 'h2') handleClick( @@ -131,15 +135,13 @@ describe('Base Class & Decorator', () => { ); } } - - render(); + renderer.render(); const tag = document.body.lastElementChild as XFirth; tag.querySelector('a').dispatchEvent( new CustomEvent('click', { bubbles: true, detail: 1 }) ); - expect(tag.name).toBe('click,H2,1'); }); @@ -149,7 +151,7 @@ describe('Base Class & Decorator', () => { extends: 'blockquote', mode: 'open' }) - class XSixth extends WebCell(HTMLQuoteElement) { + class XSixth extends HTMLQuoteElement { render() { return ( <> @@ -159,7 +161,7 @@ describe('Base Class & Decorator', () => { ); } } - render(
test
); + renderer.render(
test
); const element = document.querySelector('blockquote'); @@ -167,6 +169,6 @@ describe('Base Class & Decorator', () => { expect(element).toBeInstanceOf(HTMLQuoteElement); expect(element.textContent).toBe('test'); - expect(element + '').toBe('💖'); + expect(element.shadowRoot.innerHTML).toBe('💖'); }); }); diff --git a/test/WebField.spec.tsx b/test/WebField.spec.tsx index 2ea7a0b..dcc3adc 100644 --- a/test/WebField.spec.tsx +++ b/test/WebField.spec.tsx @@ -1,21 +1,32 @@ import 'element-internals-polyfill'; import { sleep } from 'web-utility'; +import { configure } from 'mobx'; -import { render, createCell } from '../source/renderer'; -import { component, observer } from '../source/decorator'; -import { WebFieldProps, WebField } from '../source/WebField'; +import { DOMRenderer } from 'dom-renderer'; +import { component, WebCellProps } from '../source/WebCell'; +import { observer } from '../source/decorator'; +import { WebField, formField } from '../source/WebField'; + +configure({ enforceActions: 'never' }); describe('Field Class & Decorator', () => { - @component({ - tagName: 'test-input' - }) + const renderer = new DOMRenderer(); + + interface TestInputProps extends WebCellProps { + a?: number; + } + interface TestInput extends WebField {} + + @component({ tagName: 'test-input' }) + @formField @observer - class TestInput extends WebField<{ a?: number } & WebFieldProps>() {} + class TestInput extends HTMLElement implements WebField {} it('should define a Custom Field Element', () => { - render(); + renderer.render(); + + expect(customElements.get('test-input')).toBe(TestInput); - expect(self.customElements.get('test-input')).toBe(TestInput); expect(document.querySelector('test-input').tagName.toLowerCase()).toBe( 'test-input' ); diff --git a/test/tsconfig.json b/test/tsconfig.json index a419ce1..3726f48 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig.json", "compilerOptions": { "target": "ES6", - "module": "CommonJS" + "module": "CommonJS", + "types": ["@types/dom-view-transitions", "jest"] }, "include": ["./*", "../source/**/*"] } diff --git a/tsconfig.json b/tsconfig.json index 01938c5..4289e22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "jsx": "react-jsx", "jsxImportSource": "dom-renderer", "skipLibCheck": true, - "lib": ["ES2022", "DOM"] + "lib": ["ES2022", "DOM"], + "types": ["@types/dom-view-transitions"] }, "include": ["source/**/*"], "typedocOptions": {