diff --git a/frontend/app/@types/enzyme.d.ts b/frontend/app/@types/enzyme.d.ts new file mode 100644 index 0000000000..bb5f4397d0 --- /dev/null +++ b/frontend/app/@types/enzyme.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/app/@types/enzyme/enzyme.d.ts b/frontend/app/@types/enzyme/enzyme.d.ts deleted file mode 100644 index 97a4e35520..0000000000 --- a/frontend/app/@types/enzyme/enzyme.d.ts +++ /dev/null @@ -1,723 +0,0 @@ -/** - * THIS IS PATCHED VERSION OF @types/enzyme THAT ADJUSTED FOR PREACT - * ALL CREDIT GOES TO MAINTAINERS OF @types/enzyme - */ - -/* eslint-disable */ - -declare module 'enzyme' { - // Type definitions for Enzyme 3.10 - // Project: https://github.com/airbnb/enzyme - // Definitions by: Marian Palkus - // Cap3 - // Ivo Stratev - // jwbay - // huhuanming - // MartynasZilinskas - // Torgeir Hovden - // Martin Hochel - // Christian Rackerseder - // Mateusz SokoĊ‚a - // Braiden Cutforth - // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - // TypeScript Version: 3.1 - - /// - import { Component } from 'preact'; - - export type HTMLAttributes = any; - - class ReactElement {} - - export class ElementClass {} - - /* These are purposefully stripped down versions of React.ComponentClass and React.StatelessComponent. - * The optional static properties on them break overload ordering for wrapper methods if they're not - * all specified in the implementation. TS chooses the EnzymePropSelector overload and loses the generics - */ - export interface ComponentClass { - new (props: Props, context?: any): Component; - } - - export type StatelessComponent = (props: Props, context?: any) => JSX.Element | null; - - export type ComponentType = ComponentClass | StatelessComponent; - - /** - * Many methods in Enzyme's API accept a selector as an argument. Selectors in Enzyme can fall into one of the - * following three categories: - * - * 1. A Valid CSS Selector - * 2. A React Component Constructor - * 3. A React Component's displayName - * 4. A React Stateless component - * 5. A React component property map - */ - export interface EnzymePropSelector { - [key: string]: any; - } - export type EnzymeSelector = string | StatelessComponent | ComponentClass | EnzymePropSelector; - - export type Intercepter = (intercepter: T) => void; - - export interface CommonWrapper

> { - /** - * Returns a new wrapper with only the nodes of the current wrapper that, when passed into the provided predicate function, return true. - */ - filterWhere(predicate: (wrapper: this) => boolean): this; - - /** - * Returns whether or not the current wrapper has a node anywhere in it's render tree that looks like the one passed in. - */ - contains(node: ReactElement | ReactElement[] | string): boolean; - - /** - * Returns whether or not a given react element exists in the shallow render tree. - */ - containsMatchingElement(node: ReactElement | ReactElement[]): boolean; - - /** - * Returns whether or not all the given react elements exists in the shallow render tree - */ - containsAllMatchingElements(nodes: ReactElement[] | ReactElement[][]): boolean; - - /** - * Returns whether or not one of the given react elements exists in the shallow render tree. - */ - containsAnyMatchingElements(nodes: ReactElement[] | ReactElement[][]): boolean; - - /** - * Returns whether or not the current render tree is equal to the given node, based on the expected value. - */ - equals(node: ReactElement): boolean; - - /** - * Returns whether or not a given react element matches the shallow render tree. - */ - matchesElement(node: ReactElement): boolean; - - /** - * Returns whether or not the current node has a className prop including the passed in class name. - */ - hasClass(className: string | RegExp): boolean; - - /** - * Invokes a function prop. - * @param invokePropName The function prop to call. - * @param ...args The argments to the invokePropName function - * @returns The value of the function. - */ - invoke< - K extends NonNullable<{ [K in keyof P]: P[K] extends ((...arg: any[]) => void) | undefined ? K : never }[keyof P]> - >( - invokePropName: K - ): P[K]; - - /** - * Returns whether or not the current node matches a provided selector. - */ - is(selector: EnzymeSelector): boolean; - - /** - * Returns whether or not the current node is empty. - * @deprecated Use .exists() instead. - */ - isEmpty(): boolean; - - /** - * Returns whether or not the current node exists. - */ - exists(selector?: EnzymeSelector): boolean; - - /** - * Returns a new wrapper with only the nodes of the current wrapper that don't match the provided selector. - * This method is effectively the negation or inverse of filter. - */ - not(selector: EnzymeSelector): this; - - /** - * Returns a string of the rendered text of the current render tree. This function should be looked at with - * skepticism if being used to test what the actual HTML output of the component will be. If that is what you - * would like to test, use enzyme's render function instead. - * - * Note: can only be called on a wrapper of a single node. - */ - text(): string; - - /** - * Returns a string of the rendered HTML markup of the current render tree. - * - * Note: can only be called on a wrapper of a single node. - */ - html(): string; - - /** - * Returns the node at a given index of the current wrapper. - */ - get(index: number): ReactElement; - - /** - * Returns the wrapper's underlying node. - */ - getNode(): ReactElement; - - /** - * Returns the wrapper's underlying nodes. - */ - getNodes(): ReactElement[]; - - /** - * Returns the wrapper's underlying node. - */ - getElement(): ReactElement; - - /** - * Returns the wrapper's underlying node. - */ - getElements(): ReactElement[]; - - /** - * Returns the outer most DOMComponent of the current wrapper. - */ - getDOMNode(): T; - - /** - * Returns a wrapper around the node at a given index of the current wrapper. - */ - at(index: number): this; - - /** - * Reduce the set of matched nodes to the first in the set. - */ - first(): this; - - /** - * Reduce the set of matched nodes to the last in the set. - */ - last(): this; - - /** - * Returns a new wrapper with a subset of the nodes of the original wrapper, according to the rules of `Array#slice`. - */ - slice(begin?: number, end?: number): this; - - /** - * Taps into the wrapper method chain. Helpful for debugging. - */ - tap(intercepter: Intercepter): this; - - /** - * Returns the state hash for the root node of the wrapper. Optionally pass in a prop name and it will return just that value. - */ - state(): S; - state(key: K): S[K]; - state(key: string): T; - - /** - * Returns the context hash for the root node of the wrapper. Optionally pass in a prop name and it will return just that value. - */ - context(): any; - context(key: string): T; - - /** - * Returns the props hash for the current node of the wrapper. - * - * NOTE: can only be called on a wrapper of a single node. - */ - props(): P; - - /** - * Returns the prop value for the node of the current wrapper with the provided key. - * - * NOTE: can only be called on a wrapper of a single node. - */ - prop(key: K): P[K]; - prop(key: string): T; - - /** - * Returns the key value for the node of the current wrapper. - * NOTE: can only be called on a wrapper of a single node. - */ - key(): string; - - /** - * Simulate events. - * Returns itself. - * @param args? - */ - simulate(event: string, ...args: any[]): this; - - /** - * Used to simulate throwing a rendering error. Pass an error to throw. - * Returns itself. - * @param error - */ - simulateError(error: any): this; - - /** - * A method to invoke setState() on the root component instance similar to how you might in the definition of - * the component, and re-renders. This method is useful for testing your component in hard to achieve states, - * however should be used sparingly. If possible, you should utilize your component's external API in order to - * get it into whatever state you want to test, in order to be as accurate of a test as possible. This is not - * always practical, however. - * Returns itself. - * - * NOTE: can only be called on a wrapper instance that is also the root instance. - */ - setState(state: Pick, callback?: () => void): this; - - /** - * A method that sets the props of the root component, and re-renders. Useful for when you are wanting to test - * how the component behaves over time with changing props. Calling this, for instance, will call the - * componentWillReceiveProps lifecycle method. - * - * Similar to setState, this method accepts a props object and will merge it in with the already existing props. - * Returns itself. - * - * NOTE: can only be called on a wrapper instance that is also the root instance. - */ - setProps(props: Pick, callback?: () => void): this; - - /** - * A method that sets the context of the root component, and re-renders. Useful for when you are wanting to - * test how the component behaves over time with changing contexts. - * Returns itself. - * - * NOTE: can only be called on a wrapper instance that is also the root instance. - */ - setContext(context: any): this; - - /** - * Gets the instance of the component being rendered as the root node passed into shallow(). - * - * NOTE: can only be called on a wrapper instance that is also the root instance. - */ - instance(): C; - - /** - * Forces a re-render. Useful to run before checking the render output if something external may be updating - * the state of the component somewhere. - * Returns itself. - * - * NOTE: can only be called on a wrapper instance that is also the root instance. - */ - update(): this; - - /** - * Returns an html-like string of the wrapper for debugging purposes. Useful to print out to the console when - * tests are not passing when you expect them to. - */ - debug(): string; - - /** - * Returns the name of the current node of the wrapper. - */ - name(): string; - - /** - * Iterates through each node of the current wrapper and executes the provided function with a wrapper around - * the corresponding node passed in as the first argument. - * - * Returns itself. - * @param fn A callback to be run for every node in the collection. Should expect a ShallowWrapper as the first - * argument, and will be run with a context of the original instance. - */ - forEach(fn: (wrapper: this, index: number) => any): this; - - /** - * Maps the current array of nodes to another array. Each node is passed in as a ShallowWrapper to the map - * function. - * Returns an array of the returned values from the mapping function.. - * @param fn A mapping function to be run for every node in the collection, the results of which will be mapped - * to the returned array. Should expect a ShallowWrapper as the first argument, and will be run - * with a context of the original instance. - */ - map(fn: (wrapper: this, index: number) => V): V[]; - - /** - * Applies the provided reducing function to every node in the wrapper to reduce to a single value. Each node - * is passed in as a ShallowWrapper, and is processed from left to right. - */ - reduce(fn: (prevVal: R, wrapper: this, index: number) => R, initialValue?: R): R; - - /** - * Applies the provided reducing function to every node in the wrapper to reduce to a single value. - * Each node is passed in as a ShallowWrapper, and is processed from right to left. - */ - reduceRight(fn: (prevVal: R, wrapper: this, index: number) => R, initialValue?: R): R; - - /** - * Returns whether or not any of the nodes in the wrapper match the provided selector. - */ - some(selector: EnzymeSelector): boolean; - - /** - * Returns whether or not any of the nodes in the wrapper pass the provided predicate function. - */ - someWhere(fn: (wrapper: this) => boolean): boolean; - - /** - * Returns whether or not all of the nodes in the wrapper match the provided selector. - */ - every(selector: EnzymeSelector): boolean; - - /** - * Returns whether or not all of the nodes in the wrapper pass the provided predicate function. - */ - everyWhere(fn: (wrapper: this) => boolean): boolean; - - /** - * Returns true if renderer returned null - */ - isEmptyRender(): boolean; - - /** - * Renders the component to static markup and returns a Cheerio wrapper around the result. - */ - render(): Cheerio; - - /** - * Returns the type of the current node of this wrapper. If it's a composite component, this will be the - * component constructor. If it's native DOM node, it will be a string of the tag name. - * - * Note: can only be called on a wrapper of a single node. - */ - type(): string | ComponentClass

| StatelessComponent

; - - length: number; - } - - export type Parameters = T extends (...args: infer A) => any ? A : never; - - // tslint:disable-next-line no-empty-interface - export interface ShallowWrapper

extends CommonWrapper {} - export class ShallowWrapper

{ - constructor(nodes: JSX.Element[] | JSX.Element, root?: ShallowWrapper, options?: ShallowRendererProps); - shallow(options?: ShallowRendererProps): ShallowWrapper; - unmount(): this; - - /** - * Find every node in the render tree that matches the provided selector. - * @param selector The selector to match. - */ - find(statelessComponent: StatelessComponent): ShallowWrapper; - find(component: ComponentType): ShallowWrapper; - find(props: EnzymePropSelector): ShallowWrapper; - find(selector: string): ShallowWrapper; - - /** - * Removes nodes in the current wrapper that do not match the provided selector. - * @param selector The selector to match. - */ - filter(statelessComponent: StatelessComponent): ShallowWrapper; - filter(component: ComponentType): ShallowWrapper; - filter(props: EnzymePropSelector | string): ShallowWrapper; - - /** - * Finds every node in the render tree that returns true for the provided predicate function. - */ - findWhere(predicate: (wrapper: ShallowWrapper) => boolean): ShallowWrapper; - - /** - * Returns a new wrapper with all of the children of the node(s) in the current wrapper. Optionally, a selector - * can be provided and it will filter the children by this selector. - */ - children(statelessComponent: StatelessComponent): ShallowWrapper; - children(component: ComponentType): ShallowWrapper; - children(selector: string): ShallowWrapper; - children(props?: EnzymePropSelector): ShallowWrapper; - - /** - * Returns a new wrapper with child at the specified index. - */ - childAt(index: number): ShallowWrapper; - childAt(index: number): ShallowWrapper; - - /** - * Shallow render the one non-DOM child of the current wrapper, and return a wrapper around the result. - * NOTE: can only be called on wrapper of a single non-DOM component element node. - */ - dive( - options?: ShallowRendererProps - ): ShallowWrapper; - dive(options?: ShallowRendererProps): ShallowWrapper; - dive(options?: ShallowRendererProps): ShallowWrapper; - - /** - * Strips out all the not host-nodes from the list of nodes - * - * This method is useful if you want to check for the presence of host nodes - * (actually rendered HTML elements) ignoring the React nodes. - */ - hostNodes(): ShallowWrapper; - - /** - * Returns a wrapper around all of the parents/ancestors of the wrapper. Does not include the node in the - * current wrapper. Optionally, a selector can be provided and it will filter the parents by this selector. - * - * Note: can only be called on a wrapper of a single node. - */ - parents(statelessComponent: StatelessComponent): ShallowWrapper; - parents(component: ComponentType): ShallowWrapper; - parents(selector: string): ShallowWrapper; - parents(props?: EnzymePropSelector): ShallowWrapper; - - /** - * Returns a wrapper of the first element that matches the selector by traversing up through the current node's - * ancestors in the tree, starting with itself. - * - * Note: can only be called on a wrapper of a single node. - */ - closest(statelessComponent: StatelessComponent): ShallowWrapper; - closest(component: ComponentType): ShallowWrapper; - closest(props: EnzymePropSelector): ShallowWrapper; - closest(selector: string): ShallowWrapper; - - /** - * Returns a wrapper with the direct parent of the node in the current wrapper. - */ - parent(): ShallowWrapper; - - /** - * Returns a wrapper of the node rendered by the provided render prop. - */ - renderProp( - prop: PropName - ): (...params: Parameters) => ShallowWrapper; - - /** - * If a wrappingComponent was passed in options, - * this methods returns a ShallowWrapper around the rendered wrappingComponent. - * This ShallowWrapper can be used to update the wrappingComponent's props and state - */ - getWrappingComponent: () => ShallowWrapper; - } - - // tslint:disable-next-line no-empty-interface - export interface ReactWrapper

extends CommonWrapper {} - export class ReactWrapper

{ - constructor(nodes: JSX.Element | JSX.Element[], root?: ReactWrapper, options?: MountRendererProps); - - unmount(): this; - mount(): this; - - /** - * Returns a wrapper of the node that matches the provided reference name. - * - * NOTE: can only be called on a wrapper instance that is also the root instance. - */ - ref(refName: string): ReactWrapper; - ref(refName: string): ReactWrapper; - - /** - * Detaches the react tree from the DOM. Runs ReactDOM.unmountComponentAtNode() under the hood. - * - * This method will most commonly be used as a "cleanup" method if you decide to use the attachTo option in mount(node, options). - * - * The method is intentionally not "fluent" (in that it doesn't return this) because you should not be doing anything with this wrapper after this method is called. - * - * Using the attachTo is not generally recommended unless it is absolutely necessary to test something. - * It is your responsibility to clean up after yourself at the end of the test if you do decide to use it, though. - */ - detach(): void; - - /** - * Strips out all the not host-nodes from the list of nodes - * - * This method is useful if you want to check for the presence of host nodes - * (actually rendered HTML elements) ignoring the React nodes. - */ - hostNodes(): ReactWrapper; - - /** - * Find every node in the render tree that matches the provided selector. - * @param selector The selector to match. - */ - find(statelessComponent: StatelessComponent): ReactWrapper; - find(component: ComponentType): ReactWrapper; - find(props: EnzymePropSelector): ReactWrapper; - find(selector: string): ReactWrapper; - - /** - * Finds every node in the render tree that returns true for the provided predicate function. - */ - findWhere(predicate: (wrapper: ReactWrapper) => boolean): ReactWrapper; - - /** - * Removes nodes in the current wrapper that do not match the provided selector. - * @param selector The selector to match. - */ - filter(statelessComponent: StatelessComponent): ReactWrapper; - filter(component: ComponentType): ReactWrapper; - filter(props: EnzymePropSelector | string): ReactWrapper; - - /** - * Returns a new wrapper with all of the children of the node(s) in the current wrapper. Optionally, a selector - * can be provided and it will filter the children by this selector. - */ - children(statelessComponent: StatelessComponent): ReactWrapper; - children(component: ComponentType): ReactWrapper; - children(selector: string): ReactWrapper; - children(props?: EnzymePropSelector): ReactWrapper; - - /** - * Returns a new wrapper with child at the specified index. - */ - childAt(index: number): ReactWrapper; - childAt(index: number): ReactWrapper; - - /** - * Returns a wrapper around all of the parents/ancestors of the wrapper. Does not include the node in the - * current wrapper. Optionally, a selector can be provided and it will filter the parents by this selector. - * - * Note: can only be called on a wrapper of a single node. - */ - parents(statelessComponent: StatelessComponent): ReactWrapper; - parents(component: ComponentType): ReactWrapper; - parents(selector: string): ReactWrapper; - parents(props?: EnzymePropSelector): ReactWrapper; - - /** - * Returns a wrapper of the first element that matches the selector by traversing up through the current node's - * ancestors in the tree, starting with itself. - * - * Note: can only be called on a wrapper of a single node. - */ - closest(statelessComponent: StatelessComponent): ReactWrapper; - closest(component: ComponentType): ReactWrapper; - closest(props: EnzymePropSelector): ReactWrapper; - closest(selector: string): ReactWrapper; - - /** - * Returns a wrapper with the direct parent of the node in the current wrapper. - */ - parent(): ReactWrapper; - } - - export interface Lifecycles { - componentDidUpdate?: { - onSetState: boolean; - prevContext: boolean; - }; - getDerivedStateFromProps?: { hasShouldComponentUpdateBug: boolean } | boolean; - getChildContext?: { - calledByRenderer: boolean; - [key: string]: any; - }; - setState?: any; - // TODO Maybe some life cycle are missing - [lifecycleName: string]: any; - } - - export interface ShallowRendererProps { - // See https://github.com/airbnb/enzyme/blob/enzyme@3.10.0/docs/api/shallow.md#arguments - /** - * If set to true, componentDidMount is not called on the component, and componentDidUpdate is not called after - * setProps and setContext. Default to false. - */ - disableLifecycleMethods?: boolean; - /** - * Enable experimental support for full react lifecycle methods - */ - lifecycleExperimental?: boolean; - /** - * Context to be passed into the component - */ - context?: any; - /** - * The legacy enableComponentDidUpdateOnSetState option should be matched by - * `lifecycles: { componentDidUpdate: { onSetState: true } }`, for compatibility - */ - enableComponentDidUpdateOnSetState?: boolean; - /** - * the legacy supportPrevContextArgumentOfComponentDidUpdate option should be matched by - * `lifecycles: { componentDidUpdate: { prevContext: true } }`, for compatibility - */ - supportPrevContextArgumentOfComponentDidUpdate?: boolean; - lifecycles?: Lifecycles; - /** - * A component that will render as a parent of the node. - * It can be used to provide context to the node, among other things. - * See https://airbnb.io/enzyme/docs/api/ShallowWrapper/getWrappingComponent.html - * Note: wrappingComponent must render its children. - */ - wrappingComponent?: ComponentType; - /** - * Initial props to pass to the wrappingComponent if it is specified. - */ - wrappingComponentProps?: any; - /** - * If set to true, when rendering Suspense enzyme will replace all the lazy components in children - * with fallback element prop. Otherwise it won't handle fallback of lazy component. - * Default to true. Note: not supported in React < 16.6. - */ - suspenseFallback?: boolean; - adapter?: EnzymeAdapter; - /* TODO what are these doing??? */ - attachTo?: any; - hydrateIn?: any; - PROVIDER_VALUES?: any; - } - - export interface MountRendererProps { - /** - * Context to be passed into the component - */ - context?: {}; - /** - * DOM Element to attach the component to - */ - attachTo?: HTMLElement | null; - /** - * Merged contextTypes for all children of the wrapper - */ - childContextTypes?: {}; - } - - /** - * Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that - * your tests aren't indirectly asserting on behavior of child components. - */ - export function shallow( - node: ReactElement

, - options?: ShallowRendererProps - ): ShallowWrapper; - export function shallow

(node: ReactElement

, options?: ShallowRendererProps): ShallowWrapper; - export function shallow(node: ReactElement

, options?: ShallowRendererProps): ShallowWrapper; - - /** - * Mounts and renders a react component into the document and provides a testing wrapper around it. - */ - export function mount( - node: ReactElement

, - options?: MountRendererProps - ): ReactWrapper; - export function mount

(node: ReactElement

, options?: MountRendererProps): ReactWrapper; - export function mount(node: ReactElement

, options?: MountRendererProps): ReactWrapper; - - /** - * Render react components to static HTML and analyze the resulting HTML structure. - */ - export function render(node: ReactElement

, options?: any): Cheerio; - - // See https://github.com/airbnb/enzyme/blob/v3.10.0/packages/enzyme/src/EnzymeAdapter.js - export class EnzymeAdapter { - wrapWithWrappingComponent?: (node: ReactElement, options?: ShallowRendererProps) => any; - } - - /** - * Configure enzyme to use the correct adapter for the react version - * This is enabling the Enzyme configuration with adapters in TS - */ - export function configure(options: { - adapter: EnzymeAdapter; - // See https://github.com/airbnb/enzyme/blob/enzyme@3.10.0/docs/guides/migration-from-2-to-3.md#lifecycle-methods - // Actually, `{adapter:} & Pick` is more precise. However, - // in that case jsdoc won't be shown - /** - * If set to true, componentDidMount is not called on the component, and componentDidUpdate is not called after - * setProps and setContext. Default to false. - */ - disableLifecycleMethods?: boolean; - }): void; -} diff --git a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss index 2a80648b9f..5b6e14dcb9 100644 --- a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss +++ b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss @@ -8,7 +8,7 @@ .auth-panel-email-login-form__input, .auth-panel-email-login-form__token-input { width: 12rem; - margin: 0.15rem; + margin: 2px; } .auth-panel-email-login-form__token-input { @@ -28,7 +28,7 @@ } .auth-panel-email-login-form__submit { - margin: 0.3rem 0.15rem 0.15rem; + margin: 0.3rem 2px 2px; } .auth-panel-email-login-form__back-button { @@ -44,8 +44,18 @@ } .auth-panel-email-login-form__error { - color: #9a0000; - text-align: center; - margin-top: 1em; + margin: 4px 2px; + padding: 6px 8px; font-weight: normal; + line-height: 1.2; +} + +.auth-panel-email-login-form_theme_dark .auth-panel-email-login-form__error { + background: #672323; + color: #f98989; +} + +.auth-panel-email-login-form_theme_light .auth-panel-email-login-form__error { + background: #ffd7d7; + color: #9a0000; } diff --git a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.test.tsx b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.test.tsx index a8db5efe26..7cebf6d28b 100644 --- a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.test.tsx +++ b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.test.tsx @@ -4,13 +4,23 @@ import { mount } from 'enzyme'; import { EmailLoginForm, Props, State } from './auth-panel__email-login-form'; import { User } from '@app/common/types'; import { sleep } from '@app/utils/sleep'; +import { validToken } from '@app/testUtils/mocks/jwt'; + +jest.mock('@app/utils/jwt', () => ({ + isJwtExpired: jest + .fn() + .mockImplementationOnce(() => false) + .mockImplementationOnce(() => true), +})); describe('EmailLoginForm', () => { + const testUser = ({} as any) as User; + const onSuccess = jest.fn(async () => {}); + const onSignIn = jest.fn(async () => testUser); + it('works', async () => { - const testUser = ({} as any) as User; const sendEmailVerification = jest.fn(async () => {}); - const onSignIn = jest.fn(async () => testUser); - const onSuccess = jest.fn(async () => {}); + const el = mount( { theme="light" /> ); + await new Promise(resolve => - el.setState( - { - usernameValue: 'someone', - addressValue: 'someone@example.com', - } as State, - resolve - ) + el.setState({ usernameValue: 'someone', addressValue: 'someone@example.com' } as State, resolve) ); + el.find('form').simulate('submit'); await sleep(100); expect(sendEmailVerification).toBeCalledWith('someone', 'someone@example.com'); expect(el.state().verificationSent).toBe(true); - await new Promise(resolve => - el.setState( - { - tokenValue: 'abcd', - } as State, - resolve - ) - ); + await new Promise(resolve => el.setState({ tokenValue: 'abcd' } as State, resolve)); el.find('form').simulate('submit'); await sleep(100); expect(onSignIn).toBeCalledWith('abcd'); expect(onSuccess).toBeCalledWith(testUser); }); + + it('should send form by pasting token', async () => { + const sendEmailVerification = jest.fn(async () => {}); + const onSignIn = jest.fn(async () => testUser); + + const wrapper = mount( + + ); + await new Promise(resolve => + wrapper.setState({ usernameValue: 'someone', addressValue: 'someone@example.com' } as State, resolve) + ); + wrapper.find('form').simulate('submit'); + await sleep(100); + wrapper.update(); + + wrapper.find('textarea').getDOMNode().value = validToken; + wrapper.find('textarea').simulate('input'); + + expect(onSignIn).toBeCalledWith(validToken); + }); + + it('should show error "Token is expired" on paste', async () => { + const sendEmailVerification = jest.fn(async () => {}); + const onSignIn = jest.fn(async () => testUser); + + const wrapper = mount( + + ); + await new Promise(resolve => + wrapper.setState({ usernameValue: 'someone', addressValue: 'someone@example.com' } as State, resolve) + ); + wrapper.find('form').simulate('submit'); + await sleep(100); + wrapper.update(); + wrapper.find('textarea').getDOMNode().value = validToken; + wrapper.find('textarea').simulate('input'); + + expect(wrapper.find('.auth-panel-email-login-form__error').text()).toBe('Token is expired'); + }); }); diff --git a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx index 2a7f1a17c6..598c6c934a 100644 --- a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx +++ b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx @@ -11,6 +11,7 @@ import { sleep } from '@app/utils/sleep'; import TextareaAutosize from '@app/components/comment-form/textarea-autosize'; import { Input } from '@app/components/input'; import { Button } from '@app/components/button'; +import { isJwtExpired } from '@app/utils/jwt'; const mapStateToProps = () => ({ sendEmailVerification: sendEmailVerificationRequest, @@ -41,37 +42,25 @@ export class EmailLoginForm extends Component { usernameInputRef = createRef(); tokenRef = createRef(); - constructor(props: Props) { - super(props); + state = { + usernameValue: '', + addressValue: '', + tokenValue: '', + verificationSent: false, + loading: false, + error: null, + }; - this.state = { - usernameValue: '', - addressValue: '', - tokenValue: '', - verificationSent: false, - loading: false, - error: null, - }; - - this.focus = this.focus.bind(this); - this.onVerificationSubmit = this.onVerificationSubmit.bind(this); - this.onSubmit = this.onSubmit.bind(this); - this.onUsernameChange = this.onUsernameChange.bind(this); - this.onAddressChange = this.onAddressChange.bind(this); - this.onTokenChange = this.onTokenChange.bind(this); - this.goBack = this.goBack.bind(this); - } - - async focus() { + focus = async () => { await sleep(100); if (this.usernameInputRef.current) { this.usernameInputRef.current.focus(); return; } this.tokenRef.current && this.tokenRef.current.textareaRef && this.tokenRef.current.textareaRef.select(); - } + }; - async onVerificationSubmit(e: Event) { + onVerificationSubmit = async (e: Event) => { e.preventDefault(); this.setState({ loading: true, error: null }); try { @@ -85,13 +74,12 @@ export class EmailLoginForm extends Component { } finally { this.setState({ loading: false }); } - } + }; - async onSubmit(e: Event) { - e.preventDefault(); + async sendForm(token: string = this.state.tokenValue) { try { this.setState({ loading: true }); - const user = await this.props.onSignIn(this.state.tokenValue); + const user = await this.props.onSignIn(token); if (!user) { this.setState({ error: 'No user was found' }); return; @@ -105,19 +93,34 @@ export class EmailLoginForm extends Component { } } - onUsernameChange(e: Event) { + onSubmit = async (e: Event) => { + e.preventDefault(); + this.sendForm(); + }; + + onUsernameChange = (e: Event) => { this.setState({ error: null, usernameValue: (e.target as HTMLInputElement).value }); - } + }; - onAddressChange(e: Event) { + onAddressChange = (e: Event) => { this.setState({ error: null, addressValue: (e.target as HTMLInputElement).value }); - } + }; - onTokenChange(e: Event) { - this.setState({ error: null, tokenValue: (e.target as HTMLInputElement).value }); - } + onTokenChange = (e: Event) => { + const { value } = e.target as HTMLInputElement; + + this.setState({ error: null, tokenValue: value }); + + try { + if (value.length > 0 && isJwtExpired(value)) { + this.setState({ error: 'Token is expired' }); + return; + } + this.sendForm(value); + } catch (e) {} + }; - async goBack() { + goBack = async () => { // Wait for finding back button in DOM by dropbox // It prevents dropdown from closing, because if dropdown doesn't find clicked element it closes await sleep(0); @@ -134,7 +137,7 @@ export class EmailLoginForm extends Component { if (this.usernameInputRef.current) { this.usernameInputRef.current.focus(); } - } + }; getForm1InvalidReason(): string | null { if (this.state.loading) return 'Loading...'; @@ -179,6 +182,7 @@ export class EmailLoginForm extends Component { value={this.state.addressValue} onInput={this.onAddressChange} /> + {this.state.error &&

{this.state.error}
} - {this.state.error &&
{this.state.error}
} ); @@ -210,6 +213,7 @@ export class EmailLoginForm extends Component { spellcheck={false} autocomplete="off" /> + {this.state.error &&
{this.state.error}
} - {this.state.error &&
{this.state.error}
} ); } diff --git a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx index 028c7390b3..78a760dd5d 100644 --- a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx +++ b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx @@ -8,6 +8,7 @@ import createMockStore from 'redux-mock-store'; import '@app/testUtils/mockApi'; import { user, anonymousUser } from '@app/testUtils/mocks/user'; +import { validToken } from '@app/testUtils/mocks/jwt'; import * as api from '@app/common/api'; import { sleep } from '@app/utils/sleep'; @@ -32,7 +33,11 @@ const makeInputEvent = (value: string) => ({ }, }); -describe(' { +jest.mock('@app/utils/jwt', () => ({ + isJwtExpired: jest.fn(() => false), +})); + +describe('', () => { const createWrapper = (store: ReturnType = mockStore(initialStore)) => mount( @@ -59,7 +64,7 @@ describe(' { }); }); -describe('', () => { +describe('', () => { const createWrapper = (store: ReturnType = mockStore(initialStore)) => mount( @@ -128,6 +133,32 @@ describe('', () => { expect(wrapper.find(Button).prop('children')).toEqual('Unsubscribe'); }); + it('should send form by paste valid token', async () => { + const wrapper = createWrapper(); + const onInputEmail = wrapper.find(Input).prop('onInput'); + const form = wrapper.find('form'); + + expect(onInputEmail).toBeFunction(); + + act(() => onInputEmail(makeInputEvent('some@email.com'))); + + form.simulate('submit'); + + await sleep(0); + wrapper.update(); + + const textarea = wrapper.find(TextareaAutosize); + const onInputToken = textarea.prop('onInput') as (e: any) => void; + + act(() => onInputToken(makeInputEvent(validToken))); + + await sleep(0); + wrapper.update(); + + expect(wrapper.text()).toStartWith('You have been subscribed on updates by email'); + expect(wrapper.find(Button).prop('children')).toEqual('Unsubscribe'); + }); + it('should pass throw unsubscribe process', async () => { const store = mockStore({ ...initialStore, user: { email_subscription: true } }); const wrapper = createWrapper(store); diff --git a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx index 97476190e6..fca3961675 100644 --- a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx +++ b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx @@ -1,5 +1,5 @@ /** @jsx createElement */ -import { createElement, FunctionComponent } from 'preact'; +import { createElement, FunctionComponent, Fragment } from 'preact'; import { useState, useCallback, useEffect, useRef } from 'preact/hooks'; import { useSelector, useDispatch } from 'react-redux'; import b from 'bem-react-helper'; @@ -22,6 +22,7 @@ import { Dropdown } from '@app/components/dropdown'; import { Preloader } from '@app/components/preloader'; import TextareaAutosize from '@app/components/comment-form/textarea-autosize'; import { isUserAnonymous } from '@app/utils/isUserAnonymous'; +import { isJwtExpired } from '@app/utils/jwt'; const emailRegex = /[^@]+@[^.]+\..+/; @@ -39,36 +40,40 @@ const renderEmailPart = ( emailAddress: string, handleChangeEmail: (e: Event) => void, emailAddressRef: ReturnType -) => [ -
Subscribe to replies
, - , -]; +) => ( + +
Subscribe to replies
+ +
+); const renderTokenPart = ( loading: boolean, token: string, handleChangeToken: (e: Event) => void, setEmailStep: () => void -) => [ - , - , -]; +) => ( + + + + +); export const SubscribeByEmailForm: FunctionComponent = () => { const theme = useTheme(); @@ -87,62 +92,86 @@ export const SubscribeByEmailForm: FunctionComponent = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const sendForm = useCallback( + async (currentToken: string = token) => { + setLoading(true); + setError(null); + + try { + switch (step) { + case Step.Email: + await emailVerificationForSubscribe(emailAddress); + setToken(''); + setStep(Step.Token); + break; + case Step.Token: + await emailConfirmationForSubscribe(currentToken); + dispatch(setUserSubscribed(true)); + previousStep.current = Step.Token; + setStep(Step.Subscribed); + break; + default: + break; + } + } catch (e) { + setError(extractErrorMessageFromResponse(e)); + } finally { + setLoading(false); + } + }, + [setLoading, setError, setStep, step, emailAddress, token] + ); + const handleChangeEmail = useCallback((e: Event) => { - const value = (e.target as HTMLInputElement).value; + const { value } = e.target as HTMLInputElement; e.preventDefault(); setError(null); setEmailAddress(value); }, []); - const handleChangeToken = useCallback((e: Event) => { - const value = (e.target as HTMLInputElement).value; + const handleChangeToken = useCallback( + (e: Event) => { + const { value } = e.target as HTMLInputElement; - e.preventDefault(); - setError(null); - setToken(value); - }, []); + e.preventDefault(); + setError(null); - const handleSubmit = async (e: Event) => { - e.preventDefault(); - setLoading(true); - setError(null); + try { + if (value.length > 0 && isJwtExpired(value)) { + setError('Token is expired'); + } else { + sendForm(value); + } + } catch (e) {} - try { - switch (step) { - case Step.Email: - await emailVerificationForSubscribe(emailAddress); - setStep(Step.Token); - break; - case Step.Token: - await emailConfirmationForSubscribe(token); - dispatch(setUserSubscribed(true)); - previousStep.current = Step.Token; - setStep(Step.Subscribed); - break; - default: - break; - } - } catch (e) { - setError(extractErrorMessageFromResponse(e)); - } finally { - setLoading(false); - } - }; + setToken(value); + }, + [sendForm, setError, setToken] + ); + + const handleSubmit = useCallback( + async (e: Event) => { + e.preventDefault(); + sendForm(); + }, + [sendForm] + ); const isValidEmailAddress = emailRegex.test(emailAddress); + const setEmailStep = useCallback(async () => { + await sleep(0); + setError(null); + setStep(Step.Email); + }, [setStep]); + useEffect(() => { if (emailAddressRef.current) { emailAddressRef.current.focus(); } }, []); - const setEmailStep = useCallback(async () => { - await sleep(0); - setStep(Step.Email); - }, [setStep]); - /** * It needs for dropdown closing by click on button * More info below diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index 00d683e733..f8cfcc284c 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -1,6 +1,6 @@ /** @jsx createElement */ import { createElement } from 'preact'; -import { mount, shallow, HTMLAttributes } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { Props, Comment } from './comment'; import { User, Comment as CommentType, PostInfo } from '@app/common/types'; import { sleep } from '@app/utils/sleep'; @@ -277,7 +277,7 @@ describe('', () => { new Date(new Date(initTime).getTime() + 300 * 1000).getTime() ); - component.setProps({ + component.setProps({ data: { ...props.data, time: changedTime }, }); diff --git a/frontend/app/testUtils/mocks/jwt.ts b/frontend/app/testUtils/mocks/jwt.ts new file mode 100644 index 0000000000..2587559a92 --- /dev/null +++ b/frontend/app/testUtils/mocks/jwt.ts @@ -0,0 +1,2 @@ +export const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE1Nzk5ODY5ODIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTc5OTg1MTIyLCJoYW5kc2hha2UiOnsiaWQiOiJkZXZfdXNlcjo6YXNkQHgxMDEucHcifX0.SLXLOE0Z8HQb2JwAvLS9fdrghwf8ndpuEjDsZvVE9O4' as const; +export const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE1Nzk5ODY5ODIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTc5OTg1MTIyLCJoYW5kc2hha2UiOnsiaWQiOiJkZXZfdXNlcjo6YXNkQHgxM' as const; diff --git a/frontend/app/utils/jwt.test.ts b/frontend/app/utils/jwt.test.ts new file mode 100644 index 0000000000..df7321c5fb --- /dev/null +++ b/frontend/app/utils/jwt.test.ts @@ -0,0 +1,46 @@ +import { validToken, invalidToken } from '@app/testUtils/mocks/jwt'; +import { parseJwt, isJwtExpired } from './jwt'; + +describe('JWT', () => { + describe('parseJWT', () => { + it('should parse token', () => { + expect(parseJwt(validToken)).toEqual({ + aud: 'remark', + exp: 1579986982, + handshake: { + id: 'dev_user::asd@x101.pw', + }, + iss: 'remark42', + nbf: 1579985122, + }); + }); + + it('should throw error', () => { + expect.assertions(1); + try { + parseJwt(invalidToken); + } catch (e) { + expect(e.message).toBe('The string to be decoded contains invalid characters.'); + } + }); + }); + + describe('isJwtExpired', () => { + const now = jest + .fn() + .mockImplementationOnce(() => 1579986981 * 1000) + .mockImplementationOnce(() => 1579986982 * 1000) + .mockImplementationOnce(() => 1579986983 * 1000); + + Object.defineProperty(window, 'Data', { value: { now } }); + + it('should be not expired', () => { + expect(isJwtExpired(validToken)).toBe(true); + expect(isJwtExpired(validToken)).toBe(true); + }); + + it('should be expired', () => { + expect(isJwtExpired(validToken)).toBe(true); + }); + }); +}); diff --git a/frontend/app/utils/jwt.ts b/frontend/app/utils/jwt.ts new file mode 100644 index 0000000000..f5a4b3967a --- /dev/null +++ b/frontend/app/utils/jwt.ts @@ -0,0 +1,18 @@ +export const parseJwt = (token: string) => { + const [, base64Url] = token.split('.'); + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + + return JSON.parse(jsonPayload); +}; + +export const isJwtExpired = (token: string) => { + const { exp } = parseJwt(token); + + return exp * 1000 < Date.now(); +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7534f3a9a8..ee8a14116f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1782,6 +1782,16 @@ "integrity": "sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ==", "dev": true }, + "@types/enzyme": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.4.tgz", + "integrity": "sha512-P5XpxcIt9KK8QUH4al4ttfJfIHg6xmN9ZjyUzRSzAsmDYwRXLI05ng/flZOPXrEXmp8ZYiN8/tEXYK5KSOQk3w==", + "dev": true, + "requires": { + "@types/cheerio": "*", + "@types/react": "*" + } + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -2526,14 +2536,99 @@ } }, "array.prototype.flatmap": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.1.tgz", - "integrity": "sha512-i18e2APdsiezkcqDyZor78Pbfjfds3S94dG6dgIV2ZASJaUf1N0dz2tGdrmwrmlZuNUgxH+wz6Z0zYVH2c5xzQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz", + "integrity": "sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + } } }, "asn1": { @@ -4873,9 +4968,9 @@ } }, "enzyme-adapter-preact-pure": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-preact-pure/-/enzyme-adapter-preact-pure-2.1.0.tgz", - "integrity": "sha512-C1kcVqqA9GuSIFTUdX3aXjLoTrr0Z6aUn6DZqFOBhjGtHXOXrCycwsdPaF0x+/y1286/LaZxQvi8iBaMay4/Ew==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-preact-pure/-/enzyme-adapter-preact-pure-2.2.0.tgz", + "integrity": "sha512-wb483yuBIk5CV+E9ardATYQxVywwWRo+Z3IaIECGTOLjWQYiv9NMSJGp0qXPKGp/zj9qS9yP1/qHEE4k9sEeYA==", "dev": true, "requires": { "array.prototype.flatmap": "^1.2.1", diff --git a/frontend/package.json b/frontend/package.json index 0d97884f55..4180b75548 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "@babel/preset-react": "^7.6.3", "@types/cheerio": "^0.22.13", "@types/core-js": "^2.5.2", + "@types/enzyme": "^3.10.4", "@types/fetch-mock": "^7.3.1", "@types/jest": "^24.0.20", "@types/lodash": "^4.14.144", @@ -53,7 +54,7 @@ "document-register-element": "^1.14.3", "dotenv": "^8.2.0", "enzyme": "^3.10.0", - "enzyme-adapter-preact-pure": "^2.1.0", + "enzyme-adapter-preact-pure": "^2.2.0", "es-check": "^5.1.0", "eslint": "^6.6.0", "eslint-config-prettier": "^6.5.0",