diff --git a/.eslintrc.json b/.eslintrc.json index 88eb01d..c70dc5f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,9 +8,10 @@ }, "overrides": [ { - "files": "**/*.test.ts", + "files": ["**/*.test.ts", "test/test-helpers.ts"], "rules": { - "max-len": 0 + "max-len": 0, + "@typescript-eslint/no-non-null-assertion": 0 } }, { diff --git a/src/StripeBase.ts b/src/StripeBase.ts index 2caf509..06a3098 100644 --- a/src/StripeBase.ts +++ b/src/StripeBase.ts @@ -1,4 +1,4 @@ -import type { TemplateResult, PropertyValues } from 'lit'; +import type { TemplateResult, PropertyValues, ComplexAttributeConverter } from 'lit'; import type * as Stripe from '@stripe/stripe-js'; import { LitElement, html } from 'lit'; import { property } from 'lit/decorators.js'; @@ -22,7 +22,6 @@ export const enum SlotName { } export type PaymentRepresentation = 'payment-method'|'source'|'token' - export type StripePaymentResponse = | Stripe.PaymentIntentResult | Stripe.PaymentMethodResult @@ -30,8 +29,8 @@ export type StripePaymentResponse = | Stripe.TokenResult | Stripe.SourceResult -type AmbiguousError = - Error|Stripe.StripeError|StripeElementsError; +type StripeElementType = Stripe.StripeCardElement | Stripe.StripePaymentRequestButtonElement; +type AmbiguousError = Error|Stripe.StripeError|StripeElementsError; declare global { interface Node { @@ -48,12 +47,12 @@ class StripeElementsError extends Error { } } -function isStripeElementsError(error: AmbiguousError): error is StripeElementsError { +function isStripeElementsError(error: AmbiguousError | null): error is StripeElementsError { return !!error && error instanceof StripeElementsError; } -const errorConverter = { - toAttribute: (error: AmbiguousError): string => +const errorConverter: ComplexAttributeConverter = { + toAttribute: (error: AmbiguousError) => !error ? null : isStripeElementsError(error) ? error.originalMessage : error.message || '', @@ -71,29 +70,29 @@ const errorConverter = { * @csspart 'stripe' - container for the stripe element */ export class StripeBase extends LitElement { - static is: 'stripe-elements'|'stripe-payment-request' + static is: 'stripe-elements'|'stripe-payment-request'; /* PAYMENT CONFIGURATION */ /** * billing_details object sent to create the payment representation. (optional) */ - billingDetails: Stripe.PaymentMethod.BillingDetails; + billingDetails?: Stripe.CreatePaymentMethodData['billing_details']; /** * Data passed to stripe.createPaymentMethod. (optional) */ - paymentMethodData: Stripe.CreatePaymentMethodData; + paymentMethodData?: Stripe.CreatePaymentMethodData; /** * Data passed to stripe.createSource. (optional) */ - sourceData: Stripe.CreateSourceData; + sourceData?: Stripe.CreateSourceData; /** * Data passed to stripe.createToken. (optional) */ - tokenData: Stripe.CreateTokenCardData; + tokenData?: Stripe.CreateTokenCardData; /* SETTINGS */ @@ -114,39 +113,32 @@ export class StripeBase extends LitElement { * stripeElements.submit(); * ``` */ - @property({ type: String }) - action: string; + @property({ type: String }) action?: string; /** * The `client_secret` part of a Stripe `PaymentIntent` */ - @property({ type: String, attribute: 'client-secret' }) - clientSecret: string; + @property({ type: String, attribute: 'client-secret' }) clientSecret?: string; /** * Type of payment representation to generate. */ - @property({ type: String }) - generate: PaymentRepresentation = 'source'; + @property({ type: String }) generate: PaymentRepresentation = 'source'; /** * Stripe Publishable Key. EG. `pk_test_XXXXXXXXXXXXXXXXXXXXXXXX` */ @notify - @property({ type: String, attribute: 'publishable-key', reflect: true }) - publishableKey: string; + @property({ type: String, attribute: 'publishable-key', reflect: true }) publishableKey?: string; /** Whether to display the error message */ - @property({ type: Boolean, attribute: 'show-error', reflect: true }) - showError = false; + @property({ type: Boolean, attribute: 'show-error', reflect: true }) showError = false; /** Stripe account to use (connect) */ - @property({ type: String, attribute: 'stripe-account' }) - stripeAccount: string; + @property({ type: String, attribute: 'stripe-account' }) stripeAccount?: string; /** Stripe locale to use */ - @property({ type: String, attribute: 'locale' }) - locale: StripeElementLocale = 'auto'; + @property({ type: String, attribute: 'locale' }) locale: StripeElementLocale = 'auto'; /* READ-ONLY FIELDS */ @@ -158,7 +150,7 @@ export class StripeBase extends LitElement { @readonly @notify @property({ type: Object, attribute: 'payment-method' }) - readonly paymentMethod: Stripe.PaymentMethod = null; + readonly paymentMethod: Stripe.PaymentMethod | null = null; /** * Stripe Source @@ -166,7 +158,7 @@ export class StripeBase extends LitElement { @readonly @notify @property({ type: Object }) - readonly source: Stripe.Source = null; + readonly source: Stripe.Source | null = null; /** * Stripe Token @@ -174,21 +166,21 @@ export class StripeBase extends LitElement { @readonly @notify @property({ type: Object }) - readonly token: Stripe.Token = null; + readonly token: Stripe.Token | null = null; /** * Stripe element instance */ @readonly @property({ type: Object }) - readonly element: Stripe.StripeCardElement|Stripe.StripePaymentRequestButtonElement = null; + readonly element: StripeElementType | null = null; /** * Stripe Elements instance */ @readonly @property({ type: Object }) - readonly elements: Stripe.StripeElements = null; + readonly elements: Stripe.StripeElements | null = null; /** * Stripe or validation error @@ -196,7 +188,7 @@ export class StripeBase extends LitElement { @readonly @notify @property({ type: Object, reflect: true, converter: errorConverter }) - readonly error: null|AmbiguousError = null; + readonly error: AmbiguousError | null = null; /** * If the element is focused. @@ -219,7 +211,7 @@ export class StripeBase extends LitElement { */ @readonly @property({ type: Object }) - readonly stripe: Stripe.Stripe = null; + readonly stripe: Stripe.Stripe | null = null; /** * Stripe appearance theme @@ -273,7 +265,7 @@ export class StripeBase extends LitElement { super.updated?.(changed); if (changed.has('error')) this.errorChanged(); if (changed.has('publishableKey')) this.init(); - [...changed.keys()].forEach(this.representationChanged); + [...changed.keys()].forEach(k => this.representationChanged(k)); } /** @inheritdoc */ @@ -337,7 +329,7 @@ export class StripeBase extends LitElement { /** * Fires an Error Event */ - private fireError(error: AmbiguousError): void { + private fireError(error: AmbiguousError | null): void { this.dispatchEvent(new ErrorEvent('error', { error })); } @@ -374,7 +366,7 @@ export class StripeBase extends LitElement { private async init(): Promise { await this.unmount(); await this.initStripe(); - await this.initElement(); + await this.initElement!(); this.initElementListeners(); this.breadcrumb.init(); this.mount(); @@ -404,7 +396,8 @@ export class StripeBase extends LitElement { try { const options = { stripeAccount, locale }; const stripe = - (window.Stripe) ? window.Stripe(publishableKey, options) : await loadStripe(publishableKey, options); + (window.Stripe) ? window.Stripe(publishableKey, options) + : await loadStripe(publishableKey, options); const elements = stripe?.elements(); readonly.set(this, { elements, error: null, stripe }); } catch (e) { @@ -445,10 +438,6 @@ export class StripeBase extends LitElement { await this.updateComplete; } - /** - * @param {StripeFocusEvent} event - * @private - */ @bound private async onFocus(): Promise { readonly.set(this, { focused: true }); await this.updateComplete; @@ -475,7 +464,7 @@ export class StripeBase extends LitElement { const body = JSON.stringify({ token, source, paymentMethod }); const headers = { 'Content-Type': 'application/json' }; const method = 'POST'; - return fetch(this.action, { body, headers, method }) + return fetch(this.action!, { body, headers, method }) .then(throwBadResponse) .then(onSuccess) .catch(onError); @@ -484,14 +473,14 @@ export class StripeBase extends LitElement { /** * Updates the state and fires events when the token, source, or payment method is updated. */ - @bound private representationChanged(name: string): void { - if (!isRepresentation(name)) + private representationChanged(name: PropertyKey): void { + if (!isRepresentation(name as string)) return; - const value = this[name]; + const value = this[name as keyof this]; /* istanbul ignore if */ if (!value) return; - this.fire(`${dash(name)}`, value); + this.fire(`${dash(name as string)}`, value); if (this.action) this.postRepresentation(); } diff --git a/src/breadcrumb-controller.ts b/src/breadcrumb-controller.ts index 8e9ac17..e3e6f51 100644 --- a/src/breadcrumb-controller.ts +++ b/src/breadcrumb-controller.ts @@ -31,19 +31,19 @@ export class BreadcrumbController implements ReactiveController { /** * Mount point element. This element must be connected to the document. */ - public get mount(): Element { return document.getElementById(this.mountId); } + public get mount(): Element|null { return document.getElementById(this.mountId); } constructor( private host: ReactiveControllerHost & Element, private options?: BreadcrumbControllerOptions ) { this.host.addController(this); - this.resetMountId(); + this.mountId = this.resetMountId(); this.slotName = this.options?.slotName ?? `breadcrumb-${getRandom()}`; } hostUpdated(): void { - if (!this.initialized && this.options.autoInitialize !== false) + if (!this.initialized && this.options?.autoInitialize !== false) this.init(); } @@ -52,8 +52,8 @@ export class BreadcrumbController implements ReactiveController { } private resetMountId() { - const prefix = this.options.mountPrefix ?? this.host.localName; - this.mountId = `${prefix}-mount-point-${getRandom()}`; + const prefix = this.options?.mountPrefix ?? this.host.localName; + return `${prefix}-mount-point-${getRandom()}`; } private createMountPoint(): HTMLElement { @@ -93,7 +93,7 @@ export class BreadcrumbController implements ReactiveController { } if (this.mount) this.mount.remove(); - this.resetMountId(); + this.mountId = this.resetMountId(); } /** @@ -111,16 +111,16 @@ export class BreadcrumbController implements ReactiveController { // Prepare the shallowest breadcrumb slot at document level const hosts = [...shadowHosts]; const root = hosts.pop(); - if (!root.querySelector(`[slot="${slotName}"]`)) { + if (!root!.querySelector(`[slot="${slotName}"]`)) { const div = document.createElement('div'); div.slot = slotName; - root.appendChild(div); + root!.appendChild(div); } - const container = root.querySelector(`[slot="${slotName}"]`); + const container = root!.querySelector(`[slot="${slotName}"]`); // Render the form to the document, so that the slotted content can mount - this.appendTemplate(container); + this.appendTemplate(container!); // Append breadcrumb slots to each shadowroot in turn, // from the document down to the instance. diff --git a/src/lib/functions.ts b/src/lib/functions.ts index a31870c..b5c6beb 100644 --- a/src/lib/functions.ts +++ b/src/lib/functions.ts @@ -1 +1 @@ -export const unary = f => x => f(x); +export const unary = (f: (...args:any[]) => unknown) => (x: unknown) => f(x); diff --git a/src/lib/notify.ts b/src/lib/notify.ts index d14b750..416f95d 100644 --- a/src/lib/notify.ts +++ b/src/lib/notify.ts @@ -21,14 +21,15 @@ class NotifyController implements ReactiveController { constructor(private host: HTMLElement & ReactiveControllerHost) { if (NotifyController.instances.has(host)) - return NotifyController.instances.get(host); + return NotifyController.instances.get(host) as this; host.addController(this); NotifyController.instances.set(host, this); } hostUpdated() { + // eslint-disable-next-line easy-loops/easy-loops for (const [key, oldValue] of this.cache) { - const newValue = this.host[key]; + const newValue = this.host[key as keyof typeof this.host]; const { attribute } = (this.host.constructor as typeof ReactiveElement) .getPropertyOptions(key) ?? {}; const attr = typeof attribute === 'string' ? attribute : null; @@ -42,6 +43,6 @@ class NotifyController implements ReactiveController { export function notify(proto: T, key: string) { (proto.constructor as typeof ReactiveElement).addInitializer(x => { const controller = new NotifyController(x); - controller.cache.set(key, x[key]); + controller.cache.set(key, x[key as keyof typeof x]); }); } diff --git a/src/lib/object.ts b/src/lib/object.ts index f6b0c84..5a9a763 100644 --- a/src/lib/object.ts +++ b/src/lib/object.ts @@ -1 +1 @@ -export const objectOf = key => x => ({ [key]: x }); +export const objectOf = (key: keyof T) => (x: T[keyof T]) => ({ [key]: x }); diff --git a/src/lib/predicates.ts b/src/lib/predicates.ts index aedbcf5..f9b0f5d 100644 --- a/src/lib/predicates.ts +++ b/src/lib/predicates.ts @@ -1,5 +1,5 @@ -export const elem = xs => x => xs.includes(x); +export const elem = (xs: readonly T[]) => (x: T) => xs.includes(x); -export const not = p => x => !p(x); +export const not = (p: (x: T) => boolean) => (x: T) => !p(x); export const isRepresentation = elem(['paymentMethod', 'source', 'token']); diff --git a/src/lib/read-only.ts b/src/lib/read-only.ts index e071738..e9bd030 100644 --- a/src/lib/read-only.ts +++ b/src/lib/read-only.ts @@ -17,7 +17,7 @@ class ReadOnlyController implements ReactiveController { constructor(private host: ReactiveControllerHost) { if (ReadOnlyController.instances.has(host)) - return ReadOnlyController.instances.get(host); + return ReadOnlyController.instances.get(host) as this; host.addController(this); ReadOnlyController.instances.set(host, this); } diff --git a/src/lib/strings.ts b/src/lib/strings.ts index 7ac3506..4e61cd2 100644 --- a/src/lib/strings.ts +++ b/src/lib/strings.ts @@ -2,7 +2,7 @@ import eagerDash from '@lavadrop/kebab-case'; import eagerCamel from '@lavadrop/camel-case'; import { memoize } from '@pacote/memoize'; -const identity = x => x; +const identity = (x: T): T => x; /** camelCase a string */ export const camel = memoize(identity, eagerCamel); diff --git a/src/lib/stripe-method-decorator.ts b/src/lib/stripe-method-decorator.ts index aceda59..a92f0ad 100644 --- a/src/lib/stripe-method-decorator.ts +++ b/src/lib/stripe-method-decorator.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/ban-types, no-invalid-this, @typescript-eslint/no-explicit-any */ -function wrap(f) { +function wrap(f: { + (f: { + call?: any; name?: any; + }): (this: any, ...args: unknown[]) => Promise; (arg0: any): any; +}) { return (_target: Object, _property: string, descriptor: TypedPropertyDescriptor) => { const original = descriptor.value; descriptor.value = f(original); @@ -8,9 +12,9 @@ function wrap(f) { }; } -export const stripeMethod = wrap(function(f) { +export const stripeMethod = wrap(function(f: { call?: any; name?: any; }) { const { name } = f; - return async function(...args: unknown[]) { + return async function(this: any, ...args: unknown[]) { if (!this.stripe) throw new Error(`<${this.constructor.is}>: Stripe must be initialized before calling ${name}.`); return f.call(this, ...args) .then(this.handleResponse); diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts index 2da39f0..fdf36f0 100644 --- a/src/lib/stripe.ts +++ b/src/lib/stripe.ts @@ -1,4 +1,4 @@ -export function throwResponseError(response) { +export function throwResponseError(response: T) { if (response.error) throw response.error; else return response; } diff --git a/src/stripe-elements.ts b/src/stripe-elements.ts index 5859bba..dbd6662 100644 --- a/src/stripe-elements.ts +++ b/src/stripe-elements.ts @@ -19,6 +19,10 @@ interface StripeStyleInit { invalid?: Stripe.StripeElementStyle; } +type IconStyle = Stripe.StripeCardElementOptions['iconStyle']; +type CardBrand = Stripe.StripeCardElementChangeEvent['brand']; +type StripeFormValues = Stripe.StripeCardElementOptions['value']; + const ALLOWED_STYLES = [ 'color', 'fontFamily', @@ -183,28 +187,24 @@ export class StripeElements extends StripeBase { /** * Whether to hide icons in the Stripe form. */ - @property({ type: Boolean, attribute: 'hide-icon' }) - hideIcon = false; + @property({ type: Boolean, attribute: 'hide-icon' }) hideIcon = false; /** * Whether or not to hide the postal code field. * Useful when you gather shipping info elsewhere. */ - @property({ type: Boolean, attribute: 'hide-postal-code' }) - hidePostalCode = false; + @property({ type: Boolean, attribute: 'hide-postal-code' }) hidePostalCode = false; /** * Stripe icon style. */ - @property({ type: String, attribute: 'icon-style' }) - iconStyle: Stripe.StripeCardElementOptions['iconStyle'] = 'default'; + @property({ type: String, attribute: 'icon-style' }) iconStyle: IconStyle = 'default'; /** * Prefilled values for form. * @example { postalCode: '90210' } */ - @property({ type: Object }) - value: Stripe.StripeCardElementOptions['value'] = {}; + @property({ type: Object }) value: StripeFormValues = {}; /* READ ONLY PROPERTIES */ @@ -214,7 +214,7 @@ export class StripeElements extends StripeBase { @notify @readonly @property({ type: String }) - readonly brand: Stripe.StripeCardElementChangeEvent['brand'] = null; + readonly brand: CardBrand | null = null; /** * Whether the form is complete. @@ -248,16 +248,16 @@ export class StripeElements extends StripeBase { @stripeMethod public async createPaymentMethod( paymentMethodData: Stripe.CreatePaymentMethodData = this.getPaymentMethodData() ): Promise { - return this.stripe.createPaymentMethod(paymentMethodData); + return this.stripe!.createPaymentMethod(paymentMethodData); } /** * Submit payment information to generate a source */ @stripeMethod public async createSource( - sourceData: Stripe.CreateSourceData = this.sourceData + sourceData: Stripe.CreateSourceData = this.sourceData! ): Promise { - return this.stripe.createSource(this.element, sourceData); + return this.stripe!.createSource(this.element, sourceData); } /** @@ -266,7 +266,7 @@ export class StripeElements extends StripeBase { @stripeMethod public async createToken( tokenData = this.tokenData ): Promise { - return this.stripe.createToken(this.element, tokenData); + return this.stripe!.createToken(this.element, tokenData); } /** @@ -339,7 +339,8 @@ export class StripeElements extends StripeBase { * Returns a Stripe-friendly style object computed from CSS custom properties */ private getStripeElementsStyles(): Stripe.StripeElementStyle { - const getStyle = (prop: string): string => this.getCSSCustomPropertyValue(prop) || undefined; + const getStyle = (prop: string): string|undefined => + this.getCSSCustomPropertyValue(prop) || undefined; const STATES = ['base', 'complete', 'empty', 'invalid']; const subReducer = (state: string) => (acc: StripeStyleInit, sub: string) => { @@ -387,7 +388,7 @@ export class StripeElements extends StripeBase { } private createElement(options: Stripe.StripeCardElementOptions) { - const element = this.elements.create('card', options); + const element = this.elements!.create('card', options); return element; } diff --git a/src/stripe-payment-request.ts b/src/stripe-payment-request.ts index 6d5eb28..0fc59ef 100644 --- a/src/stripe-payment-request.ts +++ b/src/stripe-payment-request.ts @@ -24,16 +24,16 @@ interface StripeShippingOption extends HTMLElement { dataset: { id: string; label: string; - detail?: string; + detail: string; amount: string; }; } -type StripePaymentRequestButtonType = - Stripe.StripePaymentRequestButtonElementOptions['style']['paymentRequestButton']['type']; - -type StripePaymentRequestButtonTheme = - Stripe.StripePaymentRequestButtonElementOptions['style']['paymentRequestButton']['theme']; +type PRBStyle = NonNullable; +type PRBStyleProps = PRBStyle['paymentRequestButton']; +type StripePaymentRequestButtonType = NonNullable; +type StripePaymentRequestButtonTheme = NonNullable; +type StripeCurrency = Stripe.PaymentRequestOptions['currency']; type StripePaymentRequestEvent = | Stripe.PaymentRequestPaymentMethodEvent @@ -165,8 +165,7 @@ export class StripePaymentRequest extends StripeBase { /** * The amount in the currency's subunit (e.g. cents, yen, etc.) */ - @property({ type: Number, reflect: true }) - amount: number; + @property({ type: Number, reflect: true }) amount?: number; /** * Whether or not the device can make the payment request. @@ -175,23 +174,21 @@ export class StripePaymentRequest extends StripeBase { @notify @readonly @property({ type: Boolean, attribute: 'can-make-payment', reflect: true }) - readonly canMakePayment: Stripe.CanMakePaymentResult = null; + readonly canMakePayment: Stripe.CanMakePaymentResult | null = null; /** * The two-letter country code of your Stripe account * @example CA */ - @property({ type: String }) - country: CountryCode; + @property({ type: String }) country?: CountryCode; /** * Three character currency code * @example usd */ - @property({ type: String }) - currency: Stripe.PaymentRequestOptions['currency']; + @property({ type: String }) currency?: StripeCurrency; - #displayItems: Stripe.PaymentRequestItem[]; + #displayItems?: Stripe.PaymentRequestItem[]; /** * An array of PaymentRequestItem objects. These objects are shown as line items in the browser’s payment interface. Note that the sum of the line item amounts does not need to add up to the total amount above. @@ -214,8 +211,7 @@ export class StripePaymentRequest extends StripeBase { /** * A name that the browser shows the customer in the payment interface. */ - @property({ type: String, reflect: true }) - label: string; + @property({ type: String, reflect: true }) label?: string; /** * Stripe PaymentIntent @@ -223,49 +219,44 @@ export class StripePaymentRequest extends StripeBase { @notify @readonly @property({ type: Object, attribute: 'payment-intent' }) - readonly paymentIntent: Stripe.PaymentIntent = null; + readonly paymentIntent: Stripe.PaymentIntent | null = null; /** * Stripe PaymentRequest */ @readonly @property({ type: Object, attribute: 'payment-request' }) - readonly paymentRequest: Stripe.PaymentRequest = null; + readonly paymentRequest: Stripe.PaymentRequest | null = null; /** * If you might change the payment amount later (for example, after you have calcluated shipping costs), set this to true. Note that browsers treat this as a hint for how to display things, and not necessarily as something that will prevent submission. */ - @property({ type: Boolean, reflect: true }) - pending = false; + @property({ type: Boolean, reflect: true }) pending = false; /** * See the requestPayerName option. */ - @property({ type: Boolean, attribute: 'request-payer-email' }) - requestPayerEmail: boolean; + @property({ type: Boolean, attribute: 'request-payer-email' }) requestPayerEmail?: boolean; /** * By default, the browser‘s payment interface only asks the customer for actual payment information. A customer name can be collected by setting this option to true. This collected name will appears in the PaymentResponse object. * * We highly recommend you collect at least one of name, email, or phone as this also results in collection of billing address for Apple Pay. The billing address can be used to perform address verification and block fraudulent payments. For all other payment methods, the billing address is automatically collected when available. */ - @property({ type: Boolean, attribute: 'request-payer-name' }) - requestPayerName: boolean; + @property({ type: Boolean, attribute: 'request-payer-name' }) requestPayerName?: boolean; /** * See the requestPayerName option. */ - @property({ type: Boolean, attribute: 'request-payer-phone' }) - requestPayerPhone: boolean; + @property({ type: Boolean, attribute: 'request-payer-phone' }) requestPayerPhone?: boolean; /** * Collect shipping address by setting this option to true. The address appears in the PaymentResponse. * You must also supply a valid [ShippingOptions] to the shippingOptions property. This can be up front at the time stripe.paymentRequest is called, or in response to a shippingaddresschange event using the updateWith callback. */ - @property({ type: Boolean, attribute: 'request-shipping' }) - requestShipping: boolean; + @property({ type: Boolean, attribute: 'request-shipping' }) requestShipping?: boolean; - #shippingOptions: Stripe.PaymentRequestShippingOption[] + #shippingOptions?: Stripe.PaymentRequestShippingOption[]; /** * An array of PaymentRequestShippingOption objects. The first shipping option listed appears in the browser payment interface as the default option. @@ -282,10 +273,10 @@ export class StripePaymentRequest extends StripeBase { } @property({ type: String, attribute: 'button-type' }) - buttonType: StripePaymentRequestButtonType = 'default'; + buttonType: StripePaymentRequestButtonType = 'default'; @property({ type: String, attribute: 'button-theme' }) - buttonTheme: StripePaymentRequestButtonTheme = 'dark'; + buttonTheme: StripePaymentRequestButtonTheme = 'dark'; /* PUBLIC API */ @@ -323,8 +314,8 @@ export class StripePaymentRequest extends StripeBase { } = this; const total = { label, amount }; return { - country, - currency, + country: country!, + currency: currency!, displayItems, requestPayerEmail, requestPayerName, @@ -361,12 +352,15 @@ export class StripePaymentRequest extends StripeBase { * Creates Stripe Payment Request Element. */ private async initPaymentRequestButton(): Promise { - const { buttonTheme: theme, buttonType: type, canMakePayment, paymentRequest } = this; + const { buttonTheme: theme, buttonType: type, canMakePayment } = this; if (!canMakePayment || !this.elements) return; const propertyName = '--stripe-payment-request-button-height'; const height = this.getCSSCustomPropertyValue(propertyName) || '40px'; const style = { paymentRequestButton: { height, theme, type } }; - const element = this.elements.create('paymentRequestButton', { paymentRequest, style }); + const element = this.elements.create('paymentRequestButton', { + paymentRequest: this.paymentRequest!, + style, + }); readonly.set(this, { element }); await this.updateComplete; } @@ -378,13 +372,14 @@ export class StripePaymentRequest extends StripeBase { if (!this.canMakePayment) return; // @ts-expect-error: the types are incomplete in @types/stripe.js this.paymentRequest.on('click', this.updatePaymentRequest); - this.paymentRequest.on('cancel', this.onCancel); - this.paymentRequest.on('shippingaddresschange', this.onShippingaddresschange); - this.paymentRequest.on('shippingoptionchange', this.onShippingoptionchange); + this.paymentRequest!.on('cancel', this.onCancel); + this.paymentRequest!.on('shippingaddresschange', this.onShippingaddresschange); + this.paymentRequest!.on('shippingoptionchange', this.onShippingoptionchange); switch (this.generate) { - case 'payment-method': this.paymentRequest.on('paymentmethod', this.onPaymentResponse); break; - case 'source': this.paymentRequest.on('source', this.onPaymentResponse); break; - case 'token': this.paymentRequest.on('token', this.onPaymentResponse); break; + case 'payment-method': + this.paymentRequest!.on('paymentmethod', this.onPaymentResponse); break; + case 'source': this.paymentRequest!.on('source', this.onPaymentResponse); break; + case 'token': this.paymentRequest!.on('token', this.onPaymentResponse); break; } } @@ -417,7 +412,7 @@ export class StripePaymentRequest extends StripeBase { paymentResponse.complete(status); this.fire(status, paymentResponse); return confirmationError ? { error: confirmationError } : paymentResponse; - } + }; /** * Handle a paymentResponse from Stripe @@ -446,8 +441,8 @@ export class StripePaymentRequest extends StripeBase { @bound private async confirmPaymentIntent( paymentResponse: Stripe.PaymentRequestPaymentMethodEvent ): Promise { - const confirmCardData = { payment_method: this.paymentMethod.id }; - const { error = null, paymentIntent = null } = + const confirmCardData = { payment_method: this.paymentMethod!.id }; + const response = await this.confirmCardPayment(confirmCardData, { handleActions: false }) .then(({ error: confirmationError }) => this.complete(paymentResponse, confirmationError)) // throws if first confirm errors .then(throwResponseError) @@ -455,6 +450,10 @@ export class StripePaymentRequest extends StripeBase { .then(throwResponseError) .catch(error => ({ error })); // catch error from first confirm + const { error = null } = response; + + const paymentIntent = (response as Stripe.PaymentIntentResult).paymentIntent ?? null; + readonly.set(this, { error, paymentIntent }); await this.updateComplete; } @@ -466,7 +465,7 @@ export class StripePaymentRequest extends StripeBase { data?: Stripe.ConfirmCardPaymentData, options?: Stripe.ConfirmCardPaymentOptions ): Promise { - return this.stripe.confirmCardPayment(this.clientSecret, data, options); + return this.stripe!.confirmCardPayment(this.clientSecret!, data, options); } @bound private onShippingaddresschange( diff --git a/test/fetch.test.ts b/test/fetch.test.ts index 3864226..96c7167 100644 --- a/test/fetch.test.ts +++ b/test/fetch.test.ts @@ -10,7 +10,7 @@ describe('throwBadResponse', function() { await throwBadResponse(response); expect.fail('resolved response'); } catch (err) { - expect(err.message).to.equal(statusText); + expect((err as Error).message).to.equal(statusText); } }); diff --git a/test/mock-stripe/index.ts b/test/mock-stripe/index.ts index 64d6fcb..e692c49 100644 --- a/test/mock-stripe/index.ts +++ b/test/mock-stripe/index.ts @@ -3,7 +3,9 @@ import luhn from 'luhn-js'; import creditCardType from 'credit-card-type'; import { spy } from 'sinon'; -const assign = (target: object) => ([k, v]: [string, unknown]): unknown => target[k] = v; +const assign = (target: T) => + ([k, v]: [string, T[keyof T]]): unknown => + target[k as keyof T] = v; export enum Keys { STRIPE_ACCOUNT = 'acct_XXXXXXXXXXX', @@ -40,7 +42,7 @@ export const INCOMPLETE_CARD_ERROR = Object.freeze({ export const CARD_DECLINED_ERROR = Object.freeze({ type: 'card_error', code: 'card_declined', - decline_code: 'generic_decline', // eslint-disable-line @typescript-eslint/camelcase + decline_code: 'generic_decline', message: 'The card has been declined.', }); @@ -50,12 +52,12 @@ const CARD_ERRORS = { '4000000000000002': CARD_DECLINED_ERROR, }; -const userAgentCreditCards = []; +const userAgentCreditCards: object[] = []; class SynthEventTarget extends EventTarget { - listeners = []; + listeners: [string, EventListenerOrEventListenerObject][] = []; - error: Error; + error?: Error; synthEvent(type: string, params: any): void { const error = this.error ?? params?.error; @@ -83,29 +85,26 @@ class PaymentRequest extends SynthEventTarget { this.update(options); } - async canMakePayment(): Promise<{ applePay: boolean }> { + async canMakePayment(): Promise<{ applePay: boolean }|null> { return userAgentCreditCards.length ? { applePay: true } : null; } - update(options): void { Object.entries(options).forEach(assign(this)); } + update(options: object): void { Object.entries(options).forEach(assign(this)); } } class Element extends SynthEventTarget { - type: string - - constructor(type, options) { + constructor(public type: string, options: object) { super(); - this.type = type; Object.entries(options).forEach(assign(this)); } - setState(props): void { + setState(props: object): void { Object.entries(props).forEach(assign(this)); } // Stripe Card APIs - mount(node): void { + mount(node: HTMLElement): void { render(html``, node); this.dispatchEvent(new CustomEvent('ready')); } @@ -127,49 +126,43 @@ class Element extends SynthEventTarget { unmount(): void { null; } - update(options): void { this.setState(options); } + update(options: object): void { this.setState(options); } } class CardElement extends Element { - cardNumber: string; - - complete: boolean; - - empty: boolean; + cardNumber?: string; - type: string; + complete?: boolean; - options: any; + empty?: boolean; - brand: string; + brand?: string; // @ts-expect-error: whatever get error(): Error { const { cardNumber, complete, empty } = this; - const cardError = CARD_ERRORS[cardNumber?.toString()]; + const cardError = CARD_ERRORS[cardNumber?.toString() as keyof typeof CARD_ERRORS]; const stateError = (!complete || empty) ? INCOMPLETE_CARD_ERROR : undefined; - return cardError || stateError; + return ( cardError || stateError ) as unknown as Error; } - constructor(type, options) { + constructor(public type: string, public options: object) { super(type, options); - this.type = type; - this.options = options; } - setState({ cardNumber, mm, yy, cvc, zip }): void { + setState({ cardNumber, mm, yy, cvc, zip }: Record): void { super.setState({ cardNumber, mm, yy, cvc, zip }); - [{ type: this.brand }] = creditCardType(this.cardNumber); + [{ type: this.brand }] = creditCardType(this.cardNumber!); this.complete = - luhn.isValid(this.cardNumber) && mm && yy && cvc !== undefined; + !!(luhn.isValid(this.cardNumber!) && mm && yy && cvc !== undefined); this.empty = - !cardNumber && cardNumber !== 0 && - !mm && mm !== 0 && - !yy && yy !== 0 && - !cvc && cvc !== 0; + !cardNumber && cardNumber.toString() !== '0' && + !mm && mm.toString() !== '0' && + !yy && yy.toString() !== '0' && + !cvc && cvc.toString() !== '0'; const { brand, complete, empty } = this; @@ -181,17 +174,20 @@ class PaymentRequestButtonElement extends Element { } class Elements { - locale: string; + locale?: string; - fonts: any; + fonts?: any; - constructor({ locale, fonts }) { + constructor({ locale, fonts }: Partial>) { this.locale = locale; this.fonts = fonts; return this; } - create(type: string, { style = undefined } = {}): CardElement|PaymentRequestButtonElement { + create( + type: 'card'|'paymentRequestButton', + { style = undefined } = {} + ): CardElement|PaymentRequestButtonElement { switch (type) { case 'card': return new CardElement(type, { style }); case 'paymentRequestButton': return new PaymentRequestButtonElement(type, { style }); @@ -204,7 +200,7 @@ export class Stripe { opts: any; - keyError: Error; + keyError?: Error; constructor(key: Keys, opts: any) { this.key = key; @@ -238,20 +234,20 @@ export class Stripe { return { error, paymentIntent }; } - async createPaymentMethod(paymentMethodData) { + async createPaymentMethod(paymentMethodData: { card: { error?: Error | undefined; }; }) { const { error = this.keyError } = paymentMethodData.card; const paymentMethod = error ? undefined : SUCCESSFUL_PAYMENT_METHOD; const response = { error, paymentMethod }; return response; } - async createSource({ error = this.keyError }, cardData) { + async createSource({ error = this.keyError }: any, cardData: any) { const source = error ? undefined : SUCCESSFUL_SOURCE; const response = { error, source }; return response; } - async createToken({ error = this.keyError } = {}, cardData) { + async createToken({ error = this.keyError } = {}, cardData: any) { const token = error ? undefined : SUCCESSFUL_TOKEN; const response = { error, token }; return response; @@ -264,7 +260,7 @@ export class Stripe { } } -export function addUserAgentCreditCard(card) { +export function addUserAgentCreditCard(card: object) { userAgentCreditCards.push(card); } diff --git a/test/stripe-elements.test.ts b/test/stripe-elements.test.ts index 40a424b..89387d0 100644 --- a/test/stripe-elements.test.ts +++ b/test/stripe-elements.test.ts @@ -20,6 +20,7 @@ import { import { elem, not } from '../src/lib/predicates'; import { StripeBase } from '../src/StripeBase'; +import { StripePaymentRequest } from '../src'; const DEFAULT_PROPS = Object.freeze({ ...Helpers.BASE_DEFAULT_PROPS, @@ -94,11 +95,10 @@ describe('', function() { let tertiaryHost: Helpers.TertiaryHost; let stripeMountId: string; afterEach(function() { - nestedElement = undefined; - primaryHost = undefined; - secondaryHost = undefined; - tertiaryHost = undefined; - stripeMountId = undefined; + // @ts-expect-error: intended: reset test state + nestedElement = undefined; primaryHost = undefined; secondaryHost = undefined; + // @ts-expect-error: intended: reset test state + tertiaryHost = undefined; stripeMountId = undefined; }); describe('when nested one shadow-root deep', function() { beforeEach(Helpers.mockStripe); @@ -111,7 +111,7 @@ describe('', function() { it('leaves one breadcrumb on its way up to the document', function() { const slot = nestedElement.querySelector('slot'); - const [slottedChild] = slot.assignedNodes(); + const [slottedChild] = slot!.assignedNodes(); expect(slottedChild).to.contain(nestedElement.stripeMount); }); @@ -141,8 +141,8 @@ describe('', function() { it('forwards stripe mount deeply through slots', function() { const [slottedChild] = - primaryHost.shadowRoot.querySelector('slot') - .assignedNodes() + (primaryHost.shadowRoot.querySelector('slot')! + .assignedNodes() as HTMLSlotElement[]) .flatMap(Helpers.assignedNodes); expect(slottedChild).to.contain(nestedElement.stripeMount); }); @@ -178,9 +178,9 @@ describe('', function() { it('forwards stripe mount deeply through slots', function() { const [slottedChild] = - primaryHost.shadowRoot.querySelector('slot') - .assignedNodes() - .flatMap(Helpers.assignedNodes) + ((primaryHost.shadowRoot.querySelector('slot')! + .assignedNodes() as HTMLSlotElement[]) + .flatMap(Helpers.assignedNodes) as HTMLSlotElement[]) .flatMap(Helpers.assignedNodes); expect(slottedChild).to.contain(nestedElement.stripeMount); }); @@ -241,7 +241,7 @@ describe('', function() { await (element as StripeElements).createPaymentMethod(); expect.fail('Resolved source promise without Stripe.js'); } catch (err) { - expect(err.message).to.equal(`<${(element.constructor as typeof StripeBase).is}>: ${Helpers.NO_STRIPE_CREATE_PAYMENT_METHOD_ERROR}`); + expect((err as Error).message).to.equal(`<${(element.constructor as typeof StripeBase).is}>: ${Helpers.NO_STRIPE_CREATE_PAYMENT_METHOD_ERROR}`); } }); @@ -250,7 +250,7 @@ describe('', function() { await (element as StripeElements).createToken(); expect.fail('Resolved token promise without Stripe.js'); } catch (err) { - expect(err.message).to.equal(`<${element.tagName.toLowerCase()}>: ${Helpers.NO_STRIPE_CREATE_TOKEN_ERROR}`); + expect((err as Error).message).to.equal(`<${element.tagName.toLowerCase()}>: ${Helpers.NO_STRIPE_CREATE_TOKEN_ERROR}`); } }); @@ -259,7 +259,7 @@ describe('', function() { await (element as StripeElements).createSource(); expect.fail('Resolved source promise without Stripe.js'); } catch (err) { - expect(err.message).to.equal(`<${(element.constructor as typeof StripeBase).is}>: ${Helpers.NO_STRIPE_CREATE_SOURCE_ERROR}`); + expect((err as Error)).to.equal(`<${(element.constructor as typeof StripeBase).is}>: ${Helpers.NO_STRIPE_CREATE_SOURCE_ERROR}`); } }); @@ -268,7 +268,7 @@ describe('', function() { await (element as StripeElements).submit(); expect.fail('Resolved submit promise without Stripe.js'); } catch (err) { - expect(err.message).to.equal(`<${(element.constructor as typeof StripeBase).is}>: ${Helpers.NO_STRIPE_CREATE_SOURCE_ERROR}`); + expect((err as Error)).to.equal(`<${(element.constructor as typeof StripeBase).is}>: ${Helpers.NO_STRIPE_CREATE_SOURCE_ERROR}`); } }); }); @@ -287,9 +287,8 @@ describe('', function() { describe('and a valid card', function() { beforeEach(Helpers.synthStripeFormValues({ cardNumber: '4242424242424242', mm: '01', yy: '40', cvc: '000' })); it('passes CSS custom property values to stripe', function() { - const allValues = Object.values( - // @ts-expect-error: stripe made this private? - element.element.style + const allValues = Object.values( + (element.element as Stripe.StripeElement & { style: Record }).style as {} ).flatMap(Object.values); const noEmpties = allValues.filter(x => !(typeof x === 'object' && Object.values(x).every(x => x === undefined))); expect(noEmpties).to.deep.equal(Array.from(noEmpties, () => 'blue')); @@ -323,7 +322,7 @@ describe('', function() { }); it('uses a new id', function() { - expect(element.stripeMount.id).to.not.equal(Helpers.initialStripeMountId); + expect(element.stripeMount!.id).to.not.equal(Helpers.initialStripeMountId); }); }); }); @@ -338,7 +337,7 @@ describe('', function() { beforeEach(Helpers.createPaymentMethod); it('unsets the `paymentMethod` property', Helpers.assertProps({ paymentMethod: null })); it('sets the `error` property', function() { - expect(element.error.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); + expect(element.error!.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); }); }); @@ -346,7 +345,7 @@ describe('', function() { beforeEach(Helpers.createSource); it('unsets the `source` property', Helpers.assertProps({ source: null })); it('sets the `error` property', function() { - expect(element.error.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); + expect(element.error!.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); }); }); @@ -354,14 +353,14 @@ describe('', function() { beforeEach(Helpers.createToken); it('unsets the `token` property', Helpers.assertProps({ token: null })); it('sets the `error` property', function() { - expect(element.error.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); + expect(element.error!.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); }); }); describe('calling submit()', function() { it('sets the `error` property', function() { return (element as StripeElements).submit().then(x => expect.fail(x.toString()), function() { - expect(element.error.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); + expect(element.error!.message, 'error').to.equal(Keys.SHOULD_ERROR_KEY); expect(element.source, 'source').to.be.null; }); }); @@ -394,13 +393,13 @@ describe('', function() { }); describe('removing the element', function() { - let removed; + let removed: StripeElements | StripePaymentRequest | undefined; beforeEach(function() { removed = element; element.remove(); }); afterEach(function() { removed = undefined; }); it('unmounts the card', function() { expect(removed).to.be.an.instanceof(HTMLElement); - expect(removed.isConnected).to.be.false; - expect(removed.stripeMount).to.not.be.ok; + expect(removed!.isConnected).to.be.false; + expect(removed!.stripeMount).to.not.be.ok; expect(document.querySelector('[slot="stripe-elements-slot"]')).to.not.be.ok; }); }); @@ -410,7 +409,7 @@ describe('', function() { beforeEach(Helpers.blur); afterEach(Helpers.restoreStripeElementBlur); it('calls StripeElement#blur', function() { - expect(element.element.blur).to.have.been.called; + expect(element.element!.blur).to.have.been.called; }); }); @@ -419,7 +418,7 @@ describe('', function() { beforeEach(Helpers.focus); afterEach(Helpers.restoreStripeElementFocus); it('calls StripeElement#focus', function() { - expect(element.element.focus).to.have.been.called; + expect(element.element!.focus).to.have.been.called; }); }); @@ -438,7 +437,7 @@ describe('', function() { }); describe('when publishable key is changed', function() { - let initialStripeMountId; + let initialStripeMountId: string | undefined; beforeEach(function() { initialStripeMountId = element.stripeMountId; }); beforeEach(Helpers.listenFor('ready')); beforeEach(Helpers.setProps({ publishableKey: 'foo' })); @@ -538,7 +537,7 @@ describe('', function() { afterEach(Helpers.restoreCardClear); it('unsets the `error` property', Helpers.assertProps({ error: null })); it('clears the card', function() { - expect(element.element.clear).to.have.been.called; + expect(element.element!.clear).to.have.been.called; }); }); }); @@ -667,7 +666,7 @@ describe('', function() { }); describe('calling createSource()', function() { - it('resolves with the source', function() { + it('resolves with the source', function(this: Mocha.Context) { return (element as StripeElements).createSource() .then(result => expect(result.source).to.equal(SUCCESS_RESPONSES.source)); }); @@ -696,7 +695,7 @@ describe('', function() { }); describe('calling createToken()', function() { - it('resolves with the token', function() { + it('resolves with the token', function(this: Mocha.Context) { return (element as StripeElements).createToken() .then(result => expect(result.token).to.equal(SUCCESS_RESPONSES.token)); }); @@ -725,7 +724,7 @@ describe('', function() { }); describe('and generate unset', function() { - it('calling submit() resolves with the source', function() { + it('calling submit() resolves with the source', function(this: Mocha.Context) { return (element as StripeElements).submit() .then(result => expect((result as Stripe.SourceResult).source).to.equal(SUCCESS_RESPONSES.source)); }); @@ -756,7 +755,7 @@ describe('', function() { describe('and generate set to `source`', function() { beforeEach(Helpers.setProps({ generate: 'source' })); describe('calling submit()', function() { - it('resolves with the source', function() { + it('resolves with the source', function(this: Mocha.Context) { return (element as StripeElements).submit() .then(result => expect((result as Stripe.SourceResult).source).to.equal(SUCCESS_RESPONSES.source)); }); @@ -786,7 +785,7 @@ describe('', function() { describe('and generate set to `token`', function() { beforeEach(Helpers.setProps({ generate: 'token' })); describe('calling submit()', function() { - it('resolves with the token', function() { + it('resolves with the token', function(this: Mocha.Context) { return (element as StripeElements).submit() .then(result => expect((result as Stripe.TokenResult).token).to.equal(SUCCESS_RESPONSES.token)); }); @@ -860,7 +859,7 @@ describe('', function() { describe('calling submit()', function() { it('rejects', function() { return (element as StripeElements).submit().then(() => expect.fail('Response received'), function(err) { - expect(err.message).to.equal(': cannot generate something-silly'); + expect((err as Error).message).to.equal(': cannot generate something-silly'); }); }); @@ -869,8 +868,8 @@ describe('', function() { expect(Helpers.fetchStub).to.not.have.been.called; }); - describe('subsequently', function() { - beforeEach(() => Helpers.submit().catch(Helpers.noop)); + describe('subsequently', function(this: Mocha.Suite) { + beforeEach(function(this: Mocha.Context) { return Helpers.submit.call(this).catch(Helpers.noop) }); it('sets the `error` property', Helpers.assertElementErrorMessage('cannot generate something-silly')); diff --git a/test/stripe-payment-request.test.ts b/test/stripe-payment-request.test.ts index 0b264a5..589c254 100644 --- a/test/stripe-payment-request.test.ts +++ b/test/stripe-payment-request.test.ts @@ -1,4 +1,5 @@ /* istanbul ignore file */ +import type { StripeElements } from '../src'; import '../src/stripe-payment-request'; import { expect, fixture, nextFrame, aTimeout } from '@open-wc/testing'; @@ -20,7 +21,7 @@ import { import { elem, not } from '../src/lib/predicates'; import { isStripeShippingOption, StripePaymentRequest } from '../src/stripe-payment-request'; -import {StripeElements} from "../src"; +import { StripeBase } from '../src/StripeBase'; const DEFAULT_PROPS = Object.freeze({ ...Helpers.BASE_DEFAULT_PROPS, @@ -146,11 +147,11 @@ describe('', function() { }); describe('with Native Shadow DOM support', function shadowDOM() { - let nestedElement; - let primaryHost; - let secondaryHost; - let tertiaryHost; - let stripeMountId; + let nestedElement: StripeBase|null; + let primaryHost: Helpers.PrimaryHost; + let secondaryHost: Helpers.SecondaryHost; + let tertiaryHost: Helpers.TertiaryHost; + let stripeMountId: string; describe('when nested one shadow-root deep', function() { beforeEach(Helpers.mockStripe); @@ -162,12 +163,12 @@ describe('', function() { }); it('leaves one breadcrumb on its way up to the document', async function breadcrumbs() { - const [slottedChild] = nestedElement.querySelector('slot').assignedNodes(); - expect(slottedChild).to.contain(nestedElement.stripeMount); + const [slottedChild] = nestedElement!.querySelector('slot')!.assignedNodes(); + expect(slottedChild).to.contain(nestedElement!.stripeMount); }); it('slots mount point in to its light DOM', function() { - const { tagName } = nestedElement; + const { tagName } = nestedElement!; expect(primaryHost).lightDom.to.equal(Helpers.expectedLightDOM({ stripeMountId, tagName })); }); @@ -191,14 +192,14 @@ describe('', function() { it('forwards stripe mount deeply through slots', async function breadcrumbs() { const [slottedChild] = - primaryHost.shadowRoot.querySelector('slot') - .assignedNodes() + (primaryHost.shadowRoot.querySelector('slot')! + .assignedNodes() as HTMLSlotElement[]) .flatMap(Helpers.assignedNodes); - expect(slottedChild).to.contain(nestedElement.stripeMount); + expect(slottedChild).to.contain(nestedElement!.stripeMount); }); it('slots mount point in to the light DOM of the secondary shadow host', function() { - const { tagName } = nestedElement; + const { tagName } = nestedElement!; expect(secondaryHost).lightDom.to.equal(Helpers.expectedLightDOM({ stripeMountId, tagName })); }); @@ -228,15 +229,15 @@ describe('', function() { it('forwards stripe mount deeply through slots', async function breadcrumbs() { const [slottedChild] = - primaryHost.shadowRoot.querySelector('slot') - .assignedNodes() - .flatMap(Helpers.assignedNodes) + ((primaryHost.shadowRoot.querySelector('slot')! + .assignedNodes() as HTMLSlotElement[]) + .flatMap(Helpers.assignedNodes) as HTMLSlotElement[]) .flatMap(Helpers.assignedNodes); - expect(slottedChild).to.contain(nestedElement.stripeMount); + expect(slottedChild).to.contain(nestedElement!.stripeMount); }); it('slots mount point in to the light DOM of the tertiary shadow host', function() { - const { tagName } = nestedElement; + const { tagName } = nestedElement!; expect(tertiaryHost).lightDom.to.equal(Helpers.expectedLightDOM({ stripeMountId, tagName })); }); @@ -316,7 +317,7 @@ describe('', function() { beforeEach(Helpers.setProps({ publishableKey: Keys.PUBLISHABLE_KEY })); beforeEach(nextFrame); it('initializes stripe with requestShipping option', function() { - expect(element.stripe.paymentRequest).to.have.been.calledWithMatch({ requestShipping: true }); + expect(element.stripe!.paymentRequest).to.have.been.calledWithMatch({ requestShipping: true }); }); }); }); @@ -334,7 +335,7 @@ describe('', function() { beforeEach(Helpers.blur); afterEach(Helpers.restoreStripeElementBlur); it('calls StripeElement#blur', function() { - expect(element.element.blur).to.have.been.called; + expect(element.element!.blur).to.have.been.called; }); }); @@ -343,7 +344,7 @@ describe('', function() { beforeEach(Helpers.focus); afterEach(Helpers.restoreStripeElementFocus); it('calls StripeElement#focus', function() { - expect(element.element.focus).to.have.been.called; + expect(element.element!.focus).to.have.been.called; }); }); @@ -388,7 +389,7 @@ describe('', function() { }); it('uses a new id', function() { - expect(element.stripeMount.id).to.not.equal(Helpers.initialStripeMountId); + expect(element.stripeMount!.id).to.not.equal(Helpers.initialStripeMountId); }); }); }); @@ -468,7 +469,7 @@ describe('', function() { }); describe('when publishable key is changed', function publishableKeyReset() { - let initialStripeMountId; + let initialStripeMountId: string | undefined; beforeEach(function() { initialStripeMountId = element.stripeMountId; }); beforeEach(Helpers.setProps({ publishableKey: 'foo' })); beforeEach(nextFrame); diff --git a/test/test-helpers.ts b/test/test-helpers.ts index 105efc5..dc2bf27 100644 --- a/test/test-helpers.ts +++ b/test/test-helpers.ts @@ -13,7 +13,7 @@ import { nextFrame, oneEvent, } from '@open-wc/testing'; -import { SinonSpy, spy, stub } from 'sinon'; +import { SinonSpy, SinonStub, spy, stub } from 'sinon'; import { Stripe, @@ -25,7 +25,7 @@ import { dash } from '../src/lib/strings'; import { StripeBase } from '../src/StripeBase'; import { ifDefined } from 'lit/directives/if-defined.js'; import { readonly } from '../src/lib/read-only'; -import { StripeCardElement, StripeConstructor, StripeConstructorOptions, StripePaymentRequestButtonElement } from '@stripe/stripe-js'; +import { StripeConstructor, StripeConstructorOptions } from '@stripe/stripe-js'; declare global { interface Node { @@ -53,12 +53,15 @@ declare global { const getTemplate = ( tagName: ReturnType, - { publishableKey = undefined, stripeAccount = undefined } = {} + { publishableKey = '', stripeAccount = '' } = {} ) => - html`<${tagName} publishable-key="${ifDefined(publishableKey)}" stripe-account="${ifDefined(stripeAccount)}">`; + html`<${tagName} + publishable-key="${ifDefined(publishableKey || undefined)}" + stripe-account="${ifDefined(stripeAccount || undefined)}">`; class Host extends LitElement { - @property({ type: String }) tag: string; + declare shadowRoot: ShadowRoot; + @property({ type: String }) tag?: string; } @customElement('primary-host') @@ -66,13 +69,13 @@ export class PrimaryHost extends Host { static is = 'primary-host'; get nestedElement(): StripeBase { - return this.shadowRoot.querySelector(this.tag); + return this.shadowRoot.querySelector(this.tag!) as StripeBase; } render(): TemplateResult { return html`

Other Primary Host Content

- ${getTemplate(unsafeStatic(this.tag), { publishableKey: Keys.PUBLISHABLE_KEY })} + ${getTemplate(unsafeStatic(this.tag!), { publishableKey: Keys.PUBLISHABLE_KEY! })} `; } } @@ -80,7 +83,7 @@ export class PrimaryHost extends Host { @customElement('secondary-host') export class SecondaryHost extends Host { get primaryHost(): PrimaryHost { - return this.shadowRoot.querySelector(PrimaryHost.is); + return this.shadowRoot.querySelector(PrimaryHost.is) as PrimaryHost; } render() { @@ -91,7 +94,7 @@ export class SecondaryHost extends Host { @customElement('tertiary-host') export class TertiaryHost extends Host { get secondaryHost(): SecondaryHost { - return this.shadowRoot.querySelector('secondary-host'); + return this.shadowRoot.querySelector('secondary-host') as SecondaryHost; } render() { @@ -192,9 +195,8 @@ export let initialStripe: typeof Stripe; export const events = new Map(); export function resetTestState(): void { - element = undefined; - initialStripeMountId = undefined; - initialStripe = undefined; + // @ts-expect-error: intended: resetting the test state; + element = undefined; initialStripeMountId = undefined; initialStripe = undefined; events.clear(); document.getElementById('stripe-elements-custom-css-properties')?.remove(); document.getElementById('stripe-payment-request-custom-css-properties')?.remove(); @@ -226,7 +228,7 @@ export const mountLightDOM = ({ stripeMountId, tagName = element.tagName.toLowerCase() }: Opts): string => `
`; -export const expectedLightDOM = ({ stripeMountId, tagName }): string => +export const expectedLightDOM = ({ stripeMountId, tagName }: Record): string => `
${mountLightDOM({ stripeMountId, tagName })}
`; /* MOCKS, STUBS, AND SPIES */ @@ -267,23 +269,23 @@ export function restoreStripe(): void { } export function spyCardClear(): void { - if (element instanceof StripeElements && element?.element?.clear) spy(element.element, 'clear'); + if (element instanceof StripeElements) spy(element.element, 'clear'); } export function spyStripeElementBlur(): void { - spy(element.element, 'blur'); + spy(element.element!, 'blur'); } export function restoreStripeElementBlur(): void { - (element.element.blur as SinonSpy)?.restore?.(); + (element.element!.blur as SinonSpy)?.restore?.(); } export function spyStripeElementFocus(): void { - spy(element.element, 'focus'); + spy(element.element!, 'focus'); } export function restoreStripeElementFocus(): void { - (element.element.focus as SinonSpy)?.restore?.(); + (element.element!.focus as SinonSpy)?.restore?.(); } export function restoreCardClear(): void { @@ -315,8 +317,8 @@ export function setupWithTemplate(template: TemplateResult|string) { }; } -export async function setupNoProps(): Promise { - const [describeTitle] = this.test.titlePath(); +export async function setupNoProps(this: Mocha.Context): Promise { + const [describeTitle] = this.test!.titlePath(); const tagName = unsafeStatic(describeTitle.replace(/<(.*)>/, '$1')); element = await fixture(getTemplate(tagName)); } @@ -326,8 +328,8 @@ export async function updateComplete(): Promise { } export function setupWithPublishableKey(publishableKey: string) { - return async function setup(): Promise { - const [describeTitle] = this.test.titlePath(); + return async function setup(this: Mocha.Context): Promise { + const [describeTitle] = this.test!.titlePath(); const tagName = unsafeStatic(describeTitle.replace(/<(.*)>/, '$1')); element = await fixture(getTemplate(tagName, { publishableKey })); await element.updateComplete; @@ -340,8 +342,8 @@ export function setupWithPublishableKeyAndStripeAccount( publishableKey: string, stripeAccount: string ) { - return async function setup(): Promise { - const [describeTitle] = this.test.titlePath(); + return async function setup(this: Mocha.Context): Promise { + const [describeTitle] = this.test!.titlePath(); const tagName = unsafeStatic(describeTitle.replace(/<(.*)>/, '$1')); element = await fixture(getTemplate(tagName, { publishableKey, stripeAccount })); await element.updateComplete; @@ -360,7 +362,7 @@ export function appendAllBlueStyleTag(): void { } export function removeAllBlueStyleTag(): void { - document.getElementById('all-blue-styles').remove(); + document.getElementById('all-blue-styles')!.remove(); } export function appendHeightStyleTag(): void { @@ -368,16 +370,16 @@ export function appendHeightStyleTag(): void { } export function removeHeightStyleTag(): void { - document.getElementById('height-styles').remove(); + document.getElementById('height-styles')!.remove(); } -export function listenFor(eventType) { +export function listenFor(eventType: string) { return async function(): Promise { events.set(eventType, oneEvent(element, eventType)); }; } -export function awaitEvent(eventType) { +export function awaitEvent(eventType: string) { return async function(): Promise { await events.get(eventType); }; @@ -391,7 +393,7 @@ export function sleep(ms: number) { /* ASSERTIONS */ -export function assertCalled(stub) { +export function assertCalled(stub: SinonStub) { return function(): void { expect(stub).to.have.been.called; }; @@ -404,20 +406,23 @@ export function assertFired(eventType: string) { }; } -export function assertEventDetail(eventType, expected) { +export function assertEventDetail(eventType: string, expected: unknown) { return async function(): Promise { const { detail } = await events.get(eventType); expect(detail, `${eventType} detail`).to.deep.equal(expected); }; } -export function assertProps(props, { deep = false } = {}) { +export function assertProps(props: T, { deep = false } = {}) { return async function(): Promise { await element.updateComplete; - Object.entries(props).forEach(([name, value]) => { - if (deep) expect(element[name]).to.deep.equal(value); - else expect(element[name]).to.equal(value); - }); + // eslint-disable-next-line easy-loops/easy-loops + for (const [name, value] of Object.entries(props)) { + if (deep) + expect(element[name as keyof typeof element]).to.deep.equal(value); + else + expect(element[name as keyof typeof element]).to.equal(value); + } }; } @@ -431,23 +436,23 @@ export function assertPropsOk(props: any[], { not = false } = {}) { return async function(): Promise { await element.updateComplete; props.forEach(prop => - not ? expect(element[prop]).to.not.be.ok - : expect(element[prop]).to.be.ok + not ? expect(element[prop as keyof typeof element]).to.not.be.ok + : expect(element[prop as keyof typeof element]).to.be.ok ); }; } -export function testDefaultPropEntry([name, value]): Mocha.Test { +export function testDefaultPropEntry([name, value]: [string, unknown]): Mocha.Test { return it(name, async function() { - expect(element[name], name).to.eql(value); + expect(element[name as keyof typeof element], name).to.eql(value); }); } export function testReadOnlyProp(name: string): void { it(name, function() { - const init = element[name]; - element[name] = Math.random(); - expect(element[name], name).to.equal(init); + const init = element[name as keyof typeof element]; + (element as any)[name] = Math.random(); + expect(element[name as keyof typeof element], name).to.equal(init); }); } @@ -455,7 +460,7 @@ export function testWritableNotifyingProp(name: string): void { it(name, async function() { const synth = `${Math.random()}`; const eventName = `${dash(name)}-changed`; - setTimeout(function setProp() { element[name] = synth; }); + setTimeout(function setProp() { (element as any)[name] = synth; }); const { detail: { value } } = await oneEvent(element, eventName); expect(value, name).to.eql(synth); }); @@ -473,7 +478,7 @@ export function testReadonlyNotifyingProp(name: string): void { export function assertElementErrorMessage(message: string) { return function(): void { - expect(element.error.message).to.equal(`<${element.tagName.toLowerCase()}>: ${message}`); + expect(element.error!.message).to.equal(`<${element.tagName.toLowerCase()}>: ${message}`); }; } @@ -495,7 +500,7 @@ export async function focusStripeElement(): Promise { (element.element as any).synthEvent('focus'); } -export async function submit(): Promise { +export async function submit(this: Mocha.Context): Promise { if (element instanceof StripeElements) { const submitPromise = element.submit(); // don't await result if we need to set up a listener @@ -509,7 +514,7 @@ export async function reset(): Promise { await element.updateComplete; } -async function swallowCallError(p: Promise) { +async function swallowCallError(this: Mocha.Context, p: Promise) { // swallow the errors, we're not testing that right now. p.catch(() => void 0); // don't await result if we need to set up a listener @@ -519,19 +524,19 @@ async function swallowCallError(p: Promise) { return p; } -export async function createPaymentMethod(): Promise { +export async function createPaymentMethod(this: Mocha.Context): Promise { if (!(element instanceof StripeElements)) throw new Error(`TEST HELPERS: can't create a payment method on ${element.constructor.name}`); await swallowCallError.call(this, element.createPaymentMethod()); } -export async function createSource(): Promise { +export async function createSource(this: Mocha.Context): Promise { if (!(element instanceof StripeElements)) throw new Error(`TEST HELPERS: can't create a source on ${element.constructor.name}`); await swallowCallError.call(this, element.createSource()); } -export async function createToken(): Promise { +export async function createToken(this: Mocha.Context): Promise { if (!(element instanceof StripeElements)) throw new Error(`TEST HELPERS: can't create a token on ${element.constructor.name}`); await swallowCallError.call(this, element.createToken()); @@ -543,29 +548,29 @@ export async function validate(): Promise { await element.updateComplete; } -export function setProps(props) { +export function setProps(props: T) { return async function doSetProps(): Promise { Object.entries(props).forEach(([name, value]) => { - element[name] = value; + (element as any)[name] = value; }); await element.updateComplete; }; } -export function synthCardEvent(...args) { +export function synthCardEvent(...args: unknown[]) { return function(): void { (element.element as any).synthEvent(...args); }; } -export function synthPaymentRequestEvent(...args) { +export function synthPaymentRequestEvent(...args: unknown[]) { return function(): void { if (element instanceof StripePaymentRequest) (element.paymentRequest as any).synthEvent(...args); }; } -export function synthStripeFormValues(inputs) { +export function synthStripeFormValues(inputs: T) { return async function(): Promise { if (element instanceof StripeElements) { (element?.element as any)?.setState(inputs); diff --git a/tsconfig.json b/tsconfig.json index aa50acc..966bcad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "exclude": [ "node_modules" ], "compilerOptions": { "declaration": true, + "strict": true, "emitDeclarationOnly": true, "outDir": ".", "emitDecoratorMetadata": true,