diff --git a/.gitignore b/.gitignore index 9d37538..732d377 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ _site lib/ .jekyll-cache .lighthouseci +coverage diff --git a/package.json b/package.json index c76177a..b8342ca 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,11 @@ "lib" ], "scripts": { - "build": "tsc --build", + "build": "tsc --build tsconfig.build.json", "build:docs": "cd docs && JEKYLL_ENV=production bundle exec jekyll build", - "clean": "tsc --build --clean", + "clean": "tsc --build --clean tsconfig.build.json", "lint": "eslint . --ignore-path .gitignore", + "postlint": "tsc", "prepack": "npm run build", "presize": "npm run build", "size": "size-limit", diff --git a/src/mark.ts b/src/mark.ts index 045476b..b1caf53 100644 --- a/src/mark.ts +++ b/src/mark.ts @@ -1,9 +1,11 @@ type PropertyType = 'field' | 'getter' | 'setter' | 'method' -type PropertyDecorator = (proto: object, key: PropertyKey) => void +interface PropertyDecorator { + (proto: object, key: PropertyKey, descriptor?: PropertyDescriptor): void + readonly static: unique symbol +} type GetMarks = (instance: object) => Set export function createMark(validate: (key: PropertyKey, type: PropertyType) => void): [PropertyDecorator, GetMarks] { const marks = new WeakMap>() - const sym = Symbol() function get(proto: object): Set { if (!marks.has(proto)) { const parent = Object.getPrototypeOf(proto) @@ -22,13 +24,15 @@ export function createMark(validate: (key: PropertyKey, type: PropertyType) => v validate(key, type) get(proto).add(key) } - marker.static = sym + marker.static = Symbol() return [ - marker, + marker as PropertyDecorator, (instance: object): Set => { const proto = Object.getPrototypeOf(instance) - for (const key of proto.constructor[sym] || []) marker(proto, key, Object.getOwnPropertyDescriptor(proto, key)) + for (const key of proto.constructor[marker.static] || []) { + marker(proto, key, Object.getOwnPropertyDescriptor(proto, key)) + } return new Set(get(proto)) } ] diff --git a/test/ability.ts b/test/ability.ts index 12c2fb9..222ec12 100644 --- a/test/ability.ts +++ b/test/ability.ts @@ -1,9 +1,10 @@ import {expect, fixture, html} from '@open-wc/testing' import {restore, fake} from 'sinon' +import type {CustomElement} from '../src/custom-element.js' import {createAbility, attachShadowCallback, attachInternalsCallback} from '../src/ability.js' describe('ability', () => { - let calls = [] + let calls: string[] = [] const fakeable = createAbility( Class => class extends Class { @@ -22,9 +23,9 @@ describe('ability', () => { calls.push('fakeable adoptedCallback') super.adoptedCallback?.() } - attributeChangedCallback(...args) { + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { calls.push('fakeable attributeChangedCallback') - super.attributeChangedCallback?.(...args) + super.attributeChangedCallback?.(name, oldValue, newValue) } } ) @@ -46,9 +47,9 @@ describe('ability', () => { calls.push('otherfakeable adoptedCallback') super.adoptedCallback?.() } - attributeChangedCallback(...args) { + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { calls.push('otherfakeable attributeChangedCallback') - super.attributeChangedCallback?.(...args) + super.attributeChangedCallback?.(name, oldValue, newValue) } } ) @@ -85,9 +86,9 @@ describe('ability', () => { it('can be called multiple times, but only applies once', async () => { const MultipleFakeable = fakeable(fakeable(fakeable(fakeable(fakeable(Element))))) customElements.define('multiple-fakeable', MultipleFakeable) - const instance = await fixture(html``) + const instance: CustomElement = await fixture(html``) expect(calls).to.eql(['fakeable connectedCallback']) - instance.connectedCallback() + instance.connectedCallback!() expect(calls).to.eql(['fakeable connectedCallback', 'fakeable connectedCallback']) }) @@ -95,22 +96,22 @@ describe('ability', () => { const CoreTest = otherfakeable(fakeable(Element)) customElements.define('core-test', CoreTest) - let instance + let instance: CustomElement & typeof CoreTest beforeEach(async () => { instance = await fixture(html``) }) it('applies keys from delegate onto subclass upon instantiation', () => { expect(instance).to.have.property('foo') - expect(instance.foo()).to.equal('foo!') + expect((instance as unknown as Record void>).foo()).to.equal('foo!') expect(instance).to.have.property('bar') - expect(instance.bar()).to.equal('bar!') + expect((instance as unknown as Record void>).bar()).to.equal('bar!') }) for (const method of ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback']) { it(`delegates to other ${method}s before class ${method}`, () => { calls = [] - instance[method]() + ;(instance as unknown as Record void>)[method]() expect(calls).to.eql([`otherfakeable ${method}`, `fakeable ${method}`]) }) } @@ -118,8 +119,8 @@ describe('ability', () => { describe('ability extension behaviour', () => { describe('attachShadowCallback', () => { - let attachShadowFake - let shadow + let attachShadowFake: (shadow: ShadowRoot) => void + let shadow: ShadowRoot | null beforeEach(() => { shadow = null attachShadowFake = fake() @@ -128,8 +129,8 @@ describe('ability', () => { const declarable = createAbility( Class => class extends Class { - [attachShadowCallback](...args) { - super[attachShadowCallback](...args) + [attachShadowCallback](...args: [ShadowRoot]) { + super[attachShadowCallback]!(...args) return attachShadowFake.apply(this, args) } } @@ -211,8 +212,8 @@ describe('ability', () => { }) describe('attachInternalsCallback', () => { - let attachInternalsFake - let internals + let attachInternalsFake: (internals: ElementInternals) => void + let internals: ElementInternals | null beforeEach(() => { internals = null attachInternalsFake = fake() @@ -221,8 +222,8 @@ describe('ability', () => { const internable = createAbility( Class => class extends Class { - [attachInternalsCallback](...args) { - super[attachInternalsCallback](...args) + [attachInternalsCallback](...args: [ElementInternals]) { + super[attachInternalsCallback]!(...args) return attachInternalsFake.apply(this, args) } } @@ -261,7 +262,7 @@ describe('ability', () => { }) it('errors if userland calls attachInternals more than once', async () => { - const instance = await fixture(html``) + const instance = await fixture(html``) internals = instance.attachInternals() expect(internals).to.exist.and.be.instanceof(ElementInternals) expect(attachInternalsFake).to.be.calledOnce.calledOn(instance).and.calledWithExactly(internals) diff --git a/test/attr.ts b/test/attr.ts index 1b262c2..211bbda 100644 --- a/test/attr.ts +++ b/test/attr.ts @@ -3,79 +3,79 @@ import {controller} from '../src/controller.js' import {attr} from '../src/attr.js' describe('Attr', () => { - @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars - class InitializeAttrTest extends HTMLElement { - @attr fooBar = 'hello' - fooBaz = 1 + { + @controller + class InitializeAttrTest extends HTMLElement { + @attr fooBar = 'hello' + fooBaz = 1 - getCount = 0 - setCount = 0 - #bing = 'world' - get bingBaz() { - this.getCount += 1 - return this.#bing - } - @attr set bingBaz(value: string) { - this.setCount += 1 - this.#bing = value + getCount = 0 + setCount = 0 + #bing = 'world' + get bingBaz() { + this.getCount += 1 + return this.#bing + } + @attr set bingBaz(value: string) { + this.setCount += 1 + this.#bing = value + } } + + let instance: InitializeAttrTest + beforeEach(async () => { + instance = await fixture(html``) + }) + + it('does not error during creation', () => { + document.createElement('initialize-attr-test') + }) + + it('does not alter field values from their initial value', () => { + expect(instance).to.have.property('fooBar', 'hello') + expect(instance).to.have.property('fooBaz', 1) + expect(instance).to.have.property('bingBaz', 'world') + }) + + it('reflects the initial value as an attribute, if not present', () => { + expect(instance).to.have.attribute('data-foo-bar', 'hello') + expect(instance).to.not.have.attribute('data-foo-baz') + expect(instance).to.have.attribute('data-bing-baz', 'world') + }) + + it('prioritises the value in the attribute over the property', async () => { + instance = await fixture(html``) + expect(instance).to.have.property('fooBar', 'goodbye') + expect(instance).to.have.attribute('data-foo-bar', 'goodbye') + expect(instance).to.have.property('bingBaz', 'universe') + expect(instance).to.have.attribute('data-bing-baz', 'universe') + }) + + it('changes the property when the attribute changes', async () => { + instance.setAttribute('data-foo-bar', 'goodbye') + await Promise.resolve() + expect(instance).to.have.property('fooBar', 'goodbye') + instance.setAttribute('data-bing-baz', 'universe') + await Promise.resolve() + expect(instance).to.have.property('bingBaz', 'universe') + }) + + it('changes the attribute when the property changes', () => { + instance.fooBar = 'goodbye' + expect(instance).to.have.attribute('data-foo-bar', 'goodbye') + instance.bingBaz = 'universe' + expect(instance).to.have.attribute('data-bing-baz', 'universe') + }) } - let instance - beforeEach(async () => { - instance = await fixture(html``) - }) - - it('does not error during creation', () => { - document.createElement('initialize-attr-test') - }) - - it('does not alter field values from their initial value', () => { - expect(instance).to.have.property('fooBar', 'hello') - expect(instance).to.have.property('fooBaz', 1) - expect(instance).to.have.property('bingBaz', 'world') - }) - - it('reflects the initial value as an attribute, if not present', () => { - expect(instance).to.have.attribute('data-foo-bar', 'hello') - expect(instance).to.not.have.attribute('data-foo-baz') - expect(instance).to.have.attribute('data-bing-baz', 'world') - }) - - it('prioritises the value in the attribute over the property', async () => { - instance = await fixture(html``) - expect(instance).to.have.property('fooBar', 'goodbye') - expect(instance).to.have.attribute('data-foo-bar', 'goodbye') - expect(instance).to.have.property('bingBaz', 'universe') - expect(instance).to.have.attribute('data-bing-baz', 'universe') - }) - - it('changes the property when the attribute changes', async () => { - instance.setAttribute('data-foo-bar', 'goodbye') - await Promise.resolve() - expect(instance).to.have.property('fooBar', 'goodbye') - instance.setAttribute('data-bing-baz', 'universe') - await Promise.resolve() - expect(instance).to.have.property('bingBaz', 'universe') - }) - - it('changes the attribute when the property changes', () => { - instance.fooBar = 'goodbye' - expect(instance).to.have.attribute('data-foo-bar', 'goodbye') - instance.bingBaz = 'universe' - expect(instance).to.have.attribute('data-bing-baz', 'universe') - }) - describe('types', () => { it('infers boolean types from property and uses has/toggleAttribute', async () => { @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars class BooleanAttrTest extends HTMLElement { @attr fooBar = false } - instance = await fixture(html``) + const instance = await fixture(html``) expect(instance).to.have.property('fooBar', false) expect(instance).to.not.have.attribute('data-foo-bar') @@ -104,7 +104,6 @@ describe('Attr', () => { it('avoids infinite loops', async () => { @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars class LoopAttrTest extends HTMLElement { count = 0 @attr @@ -115,7 +114,8 @@ describe('Attr', () => { this.count += 1 } } - instance = await fixture(html``) + + const instance = await fixture(html``) expect(instance).to.have.property('fooBar') instance.fooBar = 1 @@ -127,13 +127,13 @@ describe('Attr', () => { describe('naming', () => { @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars class NamingAttrTest extends HTMLElement { @attr fooBarBazBing = 'a' @attr URLBar = 'b' @attr ClipX = 'c' } + let instance: NamingAttrTest beforeEach(async () => { instance = await fixture(html``) }) diff --git a/test/auto-shadow-root.ts b/test/auto-shadow-root.ts index 776f11c..221c3da 100644 --- a/test/auto-shadow-root.ts +++ b/test/auto-shadow-root.ts @@ -3,9 +3,12 @@ import {replace, fake} from 'sinon' import {autoShadowRoot} from '../src/auto-shadow-root.js' describe('autoShadowRoot', () => { - window.customElements.define('shadowroot-test-element', class extends HTMLElement {}) + class ShadowRootTestElement extends HTMLElement { + declare shadowRoot: ShadowRoot + } + window.customElements.define('shadowroot-test-element', ShadowRootTestElement) - let instance + let instance: ShadowRootTestElement beforeEach(async () => { instance = await fixture(html``) }) @@ -58,7 +61,7 @@ describe('autoShadowRoot', () => { instance = await fixture(html` `) - let shadowRoot = null + let shadowRoot: ShadowRoot | null = null replace( instance, 'attachShadow', diff --git a/test/bind.ts b/test/bind.ts index 217515b..b329061 100644 --- a/test/bind.ts +++ b/test/bind.ts @@ -3,18 +3,16 @@ import {replace, fake} from 'sinon' import {bind, listenForBind} from '../src/bind.js' describe('bind', () => { - window.customElements.define( - 'bind-test-element', - class extends HTMLElement { - foo = fake() - bar = fake() - handleEvent = fake() - } - ) + class BindTestElement extends HTMLElement { + foo = fake() + bar = fake() + handleEvent = fake() + } + window.customElements.define('bind-test-element', BindTestElement) - let instance + let instance: BindTestElement beforeEach(async () => { - instance = await fixture(html``) + instance = await fixture(html``) }) it('binds events on elements based on their data-action attribute', () => { @@ -47,7 +45,7 @@ describe('bind', () => { it('does not bind elements whose closest selector is not this controller', () => { const el = document.createElement('div') - el.getAttribute('data-action', 'click:bind-test-element#foo') + el.setAttribute('data-action', 'click:bind-test-element#foo') const container = document.createElement('div') container.append(instance, el) bind(instance) @@ -157,7 +155,7 @@ describe('bind', () => { const el2 = document.createElement('div') el1.setAttribute('data-action', 'click:bind-test-element#foo') el2.setAttribute('data-action', 'submit:bind-test-element#foo') - instance.shadowRoot.append(el1, el2) + instance.shadowRoot!.append(el1, el2) bind(instance) expect(instance.foo).to.have.callCount(0) el1.click() @@ -173,8 +171,8 @@ describe('bind', () => { el1.setAttribute('data-action', 'click:bind-test-element#foo') el2.setAttribute('data-action', 'submit:bind-test-element#foo') bind(instance) - instance.shadowRoot.append(el1) - instance.shadowRoot.append(el2) + instance.shadowRoot!.append(el1) + instance.shadowRoot!.append(el2) // We need to wait for one microtask after injecting the HTML into to // controller so that the actions have been bound to the controller. await Promise.resolve() @@ -267,7 +265,7 @@ describe('bind', () => { // We need to wait for one microtask after injecting the HTML into to // controller so that the actions have been bound to the controller. await Promise.resolve() - instance.querySelector('button').click() + instance.querySelector('button')!.click() expect(instance.foo).to.have.callCount(1) }) @@ -277,7 +275,7 @@ describe('bind', () => { `) bind(instance) expect(instance.foo).to.have.callCount(0) - const el = instance.querySelector('div') + const el = instance.querySelector('div')! el.click() expect(instance.foo).to.have.callCount(1) el.setAttribute('data-action', 'click:other-element#foo') @@ -292,7 +290,7 @@ describe('bind', () => { bind(instance) listenForBind(instance.ownerDocument) await Promise.resolve() - const button = instance.querySelector('button') + const button = instance.querySelector('button')! button.click() expect(instance.foo).to.have.callCount(0) button.setAttribute('data-action', 'click:bind-test-element#foo') diff --git a/test/controller.ts b/test/controller.ts index 232fede..cb6abe5 100644 --- a/test/controller.ts +++ b/test/controller.ts @@ -25,7 +25,6 @@ describe('controller', () => { it('binds controllers before custom connectedCallback behaviour', async () => { @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars class ControllerBindOrderElement extends HTMLElement { foo = fake() } @@ -36,7 +35,7 @@ describe('controller', () => { this.dispatchEvent(new CustomEvent('loaded')) } } - instance = await fixture(html` + instance = await fixture(html` @@ -46,36 +45,34 @@ describe('controller', () => { it('binds shadowRoots after connectedCallback behaviour', async () => { @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars class ControllerBindShadowElement extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}) const button = document.createElement('button') button.setAttribute('data-action', 'click:controller-bind-shadow#foo') - this.shadowRoot.appendChild(button) + this.shadowRoot!.appendChild(button) } foo() { return 'foo' } } - instance = await fixture(html``) + instance = await fixture(html``) replace(instance, 'foo', fake(instance.foo)) - instance.shadowRoot.querySelector('button').click() + instance.shadowRoot!.querySelector('button')!.click() expect(instance.foo).to.have.callCount(1) }) it('binds auto shadowRoots', async () => { @controller - // eslint-disable-next-line @typescript-eslint/no-unused-vars class ControllerBindAutoShadowElement extends HTMLElement { foo() { return 'foo' } } - instance = await fixture(html` + instance = await fixture(html`