Autosubmit token by paste and a bit more (#583)
* Use lib for typing tests instead copied types * add jwt utils * Add autosending * fix * tests * remove atob and fix tests
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="enzyme-adapter-preact-pure" />
|
||||
-723
@@ -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 <https://github.com/MarianPalkus>
|
||||
// Cap3 <http://www.cap3.de>
|
||||
// Ivo Stratev <https://github.com/NoHomey>
|
||||
// jwbay <https://github.com/jwbay>
|
||||
// huhuanming <https://github.com/huhuanming>
|
||||
// MartynasZilinskas <https://github.com/MartynasZilinskas>
|
||||
// Torgeir Hovden <https://github.com/thovden>
|
||||
// Martin Hochel <https://github.com/hotell>
|
||||
// Christian Rackerseder <https://github.com/screendriver>
|
||||
// Mateusz Sokoła <https://github.com/mateuszsokola>
|
||||
// Braiden Cutforth <https://github.com/braidencutforth>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 3.1
|
||||
|
||||
/// <reference types="cheerio" />
|
||||
import { Component } from 'preact';
|
||||
|
||||
export type HTMLAttributes = any;
|
||||
|
||||
class ReactElement<T = any> {}
|
||||
|
||||
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<Props> {
|
||||
new (props: Props, context?: any): Component<Props>;
|
||||
}
|
||||
|
||||
export type StatelessComponent<Props> = (props: Props, context?: any) => JSX.Element | null;
|
||||
|
||||
export type ComponentType<Props> = ComponentClass<Props> | StatelessComponent<Props>;
|
||||
|
||||
/**
|
||||
* 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<any> | ComponentClass<any> | EnzymePropSelector;
|
||||
|
||||
export type Intercepter<T> = (intercepter: T) => void;
|
||||
|
||||
export interface CommonWrapper<P = {}, S = {}, C = Component<P, S>> {
|
||||
/**
|
||||
* 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 extends Element = Element>(): 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>): 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<K extends keyof S>(key: K): S[K];
|
||||
state<T>(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<T>(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<K extends keyof P>(key: K): P[K];
|
||||
prop<T>(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<K extends keyof S>(state: Pick<S, K>, 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<K extends keyof P>(props: Pick<P, K>, 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<V>(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<R>(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<R>(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<P> | StatelessComponent<P>;
|
||||
|
||||
length: number;
|
||||
}
|
||||
|
||||
export type Parameters<T> = T extends (...args: infer A) => any ? A : never;
|
||||
|
||||
// tslint:disable-next-line no-empty-interface
|
||||
export interface ShallowWrapper<P = {}, S = {}, C = Component> extends CommonWrapper<P, S, C> {}
|
||||
export class ShallowWrapper<P = {}, S = {}, C = Component> {
|
||||
constructor(nodes: JSX.Element[] | JSX.Element, root?: ShallowWrapper<any, any>, options?: ShallowRendererProps);
|
||||
shallow(options?: ShallowRendererProps): ShallowWrapper<P, S>;
|
||||
unmount(): this;
|
||||
|
||||
/**
|
||||
* Find every node in the render tree that matches the provided selector.
|
||||
* @param selector The selector to match.
|
||||
*/
|
||||
find<P2>(statelessComponent: StatelessComponent<P2>): ShallowWrapper<P2, never>;
|
||||
find<P2>(component: ComponentType<P2>): ShallowWrapper<P2, any>;
|
||||
find(props: EnzymePropSelector): ShallowWrapper<any, any>;
|
||||
find(selector: string): ShallowWrapper<HTMLAttributes, any>;
|
||||
|
||||
/**
|
||||
* Removes nodes in the current wrapper that do not match the provided selector.
|
||||
* @param selector The selector to match.
|
||||
*/
|
||||
filter<P2>(statelessComponent: StatelessComponent<P2>): ShallowWrapper<P2, never>;
|
||||
filter<P2>(component: ComponentType<P2>): ShallowWrapper<P2, any>;
|
||||
filter(props: EnzymePropSelector | string): ShallowWrapper<P, S>;
|
||||
|
||||
/**
|
||||
* Finds every node in the render tree that returns true for the provided predicate function.
|
||||
*/
|
||||
findWhere(predicate: (wrapper: ShallowWrapper<any, any>) => boolean): ShallowWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* 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<P2>(statelessComponent: StatelessComponent<P2>): ShallowWrapper<P2, never>;
|
||||
children<P2>(component: ComponentType<P2>): ShallowWrapper<P2, any>;
|
||||
children(selector: string): ShallowWrapper<HTMLAttributes, any>;
|
||||
children(props?: EnzymePropSelector): ShallowWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* Returns a new wrapper with child at the specified index.
|
||||
*/
|
||||
childAt(index: number): ShallowWrapper<any, any>;
|
||||
childAt<P2, S2>(index: number): ShallowWrapper<P2, S2>;
|
||||
|
||||
/**
|
||||
* 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<C2 extends Component, P2 = C2['props'], S2 = C2['state']>(
|
||||
options?: ShallowRendererProps
|
||||
): ShallowWrapper<P2, S2, C2>;
|
||||
dive<P2, S2>(options?: ShallowRendererProps): ShallowWrapper<P2, S2>;
|
||||
dive<P2, S2, C2>(options?: ShallowRendererProps): ShallowWrapper<P2, S2, C2>;
|
||||
|
||||
/**
|
||||
* 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<HTMLAttributes>;
|
||||
|
||||
/**
|
||||
* 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<P2>(statelessComponent: StatelessComponent<P2>): ShallowWrapper<P2, never>;
|
||||
parents<P2>(component: ComponentType<P2>): ShallowWrapper<P2, any>;
|
||||
parents(selector: string): ShallowWrapper<HTMLAttributes, any>;
|
||||
parents(props?: EnzymePropSelector): ShallowWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* 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<P2>(statelessComponent: StatelessComponent<P2>): ShallowWrapper<P2, never>;
|
||||
closest<P2>(component: ComponentType<P2>): ShallowWrapper<P2, any>;
|
||||
closest(props: EnzymePropSelector): ShallowWrapper<any, any>;
|
||||
closest(selector: string): ShallowWrapper<HTMLAttributes, any>;
|
||||
|
||||
/**
|
||||
* Returns a wrapper with the direct parent of the node in the current wrapper.
|
||||
*/
|
||||
parent(): ShallowWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* Returns a wrapper of the node rendered by the provided render prop.
|
||||
*/
|
||||
renderProp<PropName extends keyof P>(
|
||||
prop: PropName
|
||||
): (...params: Parameters<P[PropName]>) => ShallowWrapper<any, never>;
|
||||
|
||||
/**
|
||||
* 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<P = {}, S = {}, C = Component> extends CommonWrapper<P, S, C> {}
|
||||
export class ReactWrapper<P = {}, S = {}, C = Component> {
|
||||
constructor(nodes: JSX.Element | JSX.Element[], root?: ReactWrapper<any, any>, 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<any, any>;
|
||||
ref<P2, S2>(refName: string): ReactWrapper<P2, S2>;
|
||||
|
||||
/**
|
||||
* 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<HTMLAttributes>;
|
||||
|
||||
/**
|
||||
* Find every node in the render tree that matches the provided selector.
|
||||
* @param selector The selector to match.
|
||||
*/
|
||||
find<P2>(statelessComponent: StatelessComponent<P2>): ReactWrapper<P2, never>;
|
||||
find<P2>(component: ComponentType<P2>): ReactWrapper<P2, any>;
|
||||
find(props: EnzymePropSelector): ReactWrapper<any, any>;
|
||||
find(selector: string): ReactWrapper<HTMLAttributes, any>;
|
||||
|
||||
/**
|
||||
* Finds every node in the render tree that returns true for the provided predicate function.
|
||||
*/
|
||||
findWhere(predicate: (wrapper: ReactWrapper<any, any>) => boolean): ReactWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* Removes nodes in the current wrapper that do not match the provided selector.
|
||||
* @param selector The selector to match.
|
||||
*/
|
||||
filter<P2>(statelessComponent: StatelessComponent<P2>): ReactWrapper<P2, never>;
|
||||
filter<P2>(component: ComponentType<P2>): ReactWrapper<P2, any>;
|
||||
filter(props: EnzymePropSelector | string): ReactWrapper<P, S>;
|
||||
|
||||
/**
|
||||
* 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<P2>(statelessComponent: StatelessComponent<P2>): ReactWrapper<P2, never>;
|
||||
children<P2>(component: ComponentType<P2>): ReactWrapper<P2, any>;
|
||||
children(selector: string): ReactWrapper<HTMLAttributes, any>;
|
||||
children(props?: EnzymePropSelector): ReactWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* Returns a new wrapper with child at the specified index.
|
||||
*/
|
||||
childAt(index: number): ReactWrapper<any, any>;
|
||||
childAt<P2, S2>(index: number): ReactWrapper<P2, S2>;
|
||||
|
||||
/**
|
||||
* 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<P2>(statelessComponent: StatelessComponent<P2>): ReactWrapper<P2, never>;
|
||||
parents<P2>(component: ComponentType<P2>): ReactWrapper<P2, any>;
|
||||
parents(selector: string): ReactWrapper<HTMLAttributes, any>;
|
||||
parents(props?: EnzymePropSelector): ReactWrapper<any, any>;
|
||||
|
||||
/**
|
||||
* 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<P2>(statelessComponent: StatelessComponent<P2>): ReactWrapper<P2, never>;
|
||||
closest<P2>(component: ComponentType<P2>): ReactWrapper<P2, any>;
|
||||
closest(props: EnzymePropSelector): ReactWrapper<any, any>;
|
||||
closest(selector: string): ReactWrapper<HTMLAttributes, any>;
|
||||
|
||||
/**
|
||||
* Returns a wrapper with the direct parent of the node in the current wrapper.
|
||||
*/
|
||||
parent(): ReactWrapper<any, any>;
|
||||
}
|
||||
|
||||
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<any>;
|
||||
/**
|
||||
* 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<C extends Component, P = C['props'], S = C['state']>(
|
||||
node: ReactElement<P>,
|
||||
options?: ShallowRendererProps
|
||||
): ShallowWrapper<P, S, C>;
|
||||
export function shallow<P>(node: ReactElement<P>, options?: ShallowRendererProps): ShallowWrapper<P, any>;
|
||||
export function shallow<P, S>(node: ReactElement<P>, options?: ShallowRendererProps): ShallowWrapper<P, S>;
|
||||
|
||||
/**
|
||||
* Mounts and renders a react component into the document and provides a testing wrapper around it.
|
||||
*/
|
||||
export function mount<C extends Component, P = C['props'], S = C['state']>(
|
||||
node: ReactElement<P>,
|
||||
options?: MountRendererProps
|
||||
): ReactWrapper<P, S, C>;
|
||||
export function mount<P>(node: ReactElement<P>, options?: MountRendererProps): ReactWrapper<P, any>;
|
||||
export function mount<P, S>(node: ReactElement<P>, options?: MountRendererProps): ReactWrapper<P, S>;
|
||||
|
||||
/**
|
||||
* Render react components to static HTML and analyze the resulting HTML structure.
|
||||
*/
|
||||
export function render<P, S>(node: ReactElement<P>, 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<ShallowRendererProps,"disableLifecycleMethods">` 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;
|
||||
}
|
||||
+15
-5
@@ -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;
|
||||
}
|
||||
|
||||
+66
-18
@@ -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<Props, State>(
|
||||
<EmailLoginForm
|
||||
sendEmailVerification={sendEmailVerification}
|
||||
@@ -19,32 +29,70 @@ describe('EmailLoginForm', () => {
|
||||
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<Props, State>(
|
||||
<EmailLoginForm
|
||||
sendEmailVerification={sendEmailVerification}
|
||||
onSignIn={onSignIn}
|
||||
onSuccess={onSuccess}
|
||||
theme="light"
|
||||
/>
|
||||
);
|
||||
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<HTMLTextAreaElement>().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<Props, State>(
|
||||
<EmailLoginForm
|
||||
sendEmailVerification={sendEmailVerification}
|
||||
onSignIn={onSignIn}
|
||||
onSuccess={onSuccess}
|
||||
theme="light"
|
||||
/>
|
||||
);
|
||||
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<HTMLTextAreaElement>().value = validToken;
|
||||
wrapper.find('textarea').simulate('input');
|
||||
|
||||
expect(wrapper.find('.auth-panel-email-login-form__error').text()).toBe('Token is expired');
|
||||
});
|
||||
});
|
||||
|
||||
+41
-38
@@ -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<Props, State> {
|
||||
usernameInputRef = createRef<HTMLInputElement>();
|
||||
tokenRef = createRef<TextareaAutosize>();
|
||||
|
||||
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<Props, State> {
|
||||
} 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<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
async goBack() {
|
||||
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) {}
|
||||
};
|
||||
|
||||
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<Props, State> {
|
||||
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<Props, State> {
|
||||
value={this.state.addressValue}
|
||||
onInput={this.onAddressChange}
|
||||
/>
|
||||
{this.state.error && <div className="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
<Button
|
||||
mix="auth-panel-email-login-form__submit"
|
||||
kind="primary"
|
||||
@@ -189,7 +193,6 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
>
|
||||
Send Verification
|
||||
</Button>
|
||||
{this.state.error && <div className="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -210,6 +213,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
spellcheck={false}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{this.state.error && <div className="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
<Button
|
||||
mix="auth-panel-email-login-form__submit"
|
||||
type="submit"
|
||||
@@ -220,7 +224,6 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
{this.state.error && <div className="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
+33
-2
@@ -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('<SubscribeByEmail', () => {
|
||||
jest.mock('@app/utils/jwt', () => ({
|
||||
isJwtExpired: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
describe('<SubscribeByEmail/>', () => {
|
||||
const createWrapper = (store: ReturnType<typeof mockStore> = mockStore(initialStore)) =>
|
||||
mount(
|
||||
<Provider store={store}>
|
||||
@@ -59,7 +64,7 @@ describe('<SubscribeByEmail', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('<SubscribeByEmailForm>', () => {
|
||||
describe('<SubscribeByEmailForm/>', () => {
|
||||
const createWrapper = (store: ReturnType<typeof mockStore> = mockStore(initialStore)) =>
|
||||
mount(
|
||||
<Provider store={store}>
|
||||
@@ -128,6 +133,32 @@ describe('<SubscribeByEmailForm>', () => {
|
||||
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);
|
||||
|
||||
+91
-62
@@ -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<typeof useRef>
|
||||
) => [
|
||||
<div className="comment-form__subscribe-by-email__title">Subscribe to replies</div>,
|
||||
<Input
|
||||
ref={emailAddressRef}
|
||||
mix="comment-form__subscribe-by-email__input"
|
||||
placeholder="Email"
|
||||
value={emailAddress}
|
||||
onInput={handleChangeEmail}
|
||||
disabled={loading}
|
||||
/>,
|
||||
];
|
||||
) => (
|
||||
<Fragment>
|
||||
<div className="comment-form__subscribe-by-email__title">Subscribe to replies</div>
|
||||
<Input
|
||||
ref={emailAddressRef}
|
||||
mix="comment-form__subscribe-by-email__input"
|
||||
placeholder="Email"
|
||||
value={emailAddress}
|
||||
onInput={handleChangeEmail}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const renderTokenPart = (
|
||||
loading: boolean,
|
||||
token: string,
|
||||
handleChangeToken: (e: Event) => void,
|
||||
setEmailStep: () => void
|
||||
) => [
|
||||
<Button kind="link" mix="auth-panel-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
Back
|
||||
</Button>,
|
||||
<TextareaAutosize
|
||||
className="comment-form__subscribe-by-email__token-input"
|
||||
placeholder="Token"
|
||||
autofocus
|
||||
onInput={handleChangeToken}
|
||||
disabled={loading}
|
||||
value={token}
|
||||
/>,
|
||||
];
|
||||
) => (
|
||||
<Fragment>
|
||||
<Button kind="link" mix="auth-panel-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
Back
|
||||
</Button>
|
||||
<TextareaAutosize
|
||||
className="comment-form__subscribe-by-email__token-input"
|
||||
placeholder="Token"
|
||||
autofocus
|
||||
onInput={handleChangeToken}
|
||||
disabled={loading}
|
||||
value={token}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
const theme = useTheme();
|
||||
@@ -87,62 +92,86 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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
|
||||
|
||||
@@ -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('<Comment />', () => {
|
||||
new Date(new Date(initTime).getTime() + 300 * 1000).getTime()
|
||||
);
|
||||
|
||||
component.setProps<Props & HTMLAttributes>({
|
||||
component.setProps({
|
||||
data: { ...props.data, time: changedTime },
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE1Nzk5ODY5ODIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTc5OTg1MTIyLCJoYW5kc2hha2UiOnsiaWQiOiJkZXZfdXNlcjo6YXNkQHgxMDEucHcifX0.SLXLOE0Z8HQb2JwAvLS9fdrghwf8ndpuEjDsZvVE9O4' as const;
|
||||
export const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE1Nzk5ODY5ODIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTc5OTg1MTIyLCJoYW5kc2hha2UiOnsiaWQiOiJkZXZfdXNlcjo6YXNkQHgxM' as const;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
Generated
+103
-8
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user