W ciągu ostatnich dwóch tygodni miałem okazję zaznajomić się z biblioteką React. Jest to narzędzie do tworzenia UI dla aplikacji webowych za pomocą JavaScriptu. Ja będę używał akurat TypeScriptu, żeby moje programy były bardziej poprawne. W tym artykule chcę opisać podstawy Reacta i jego testowania.
Jak działa React? Biblioteka utrzymuje w pamięci wirtualne drzewo DOM, które składa się z wirtualnych komponentów. Każdy komponent jest renderowany do postaci tagów HTML. Komponent dostaje parametry i może też utrzymywać swój stan. React śledzi zmiany stanu i odświeża odpowiednie elementy strony.
Żeby utworzyć aplikację React polecam użyć polecenia
npx create-react-app app-name --typescript
Dostaniemy szablon aplikacji i będziemy mogli używać menadżera pakietów yarn. Można albo uruchamiać npx yarn jeśli mamy zawsze internet, albo zainstalować go sobie lokalnie npm i -g yarn.
Żeby było wygodniej, do tworzenia aplikacji w Reactcie używa się rozszerzenia JSX, które zamienia tagi bliskie tym w HTML na wywołania metod JavasScript. Ponieważ tag jest wywołaniem metody, to nie możemy zwrócić kilku tagów na najwyższym poziomie zagnieżdżenia - zawsze jest jeden korzeń i potem może być już wiele dzieci - kolejnych parametrów tej głównej metody. Jeśli nie chcesz opakowywać elementów w <div> to możesz je zapakować w <React.Fragment>, który po prostu wyrenderuje swoje dzieci, samemu znikając.
Dobra, przejdźmy do jakiegoś przykładu:
import React from "react";
interface HelloProps { name: string; }
export function Hello(props: HelloProps) {
    return (
        <div>
            <h1>This is a sample component</h1>
            <p>Hello {props.name}!</p>
        </div>
    );
}
Jest to prosty komponent funkcyjny który przyjmuje parametry w swoim argumencie i renderuje powitalną wiadomość. Jeśli w JSX użyjemy klamer {} to w środku umieszczamy wyrażenie, które zostanie obliczone i jego wynik umieszczony na stronie.
Żeby użyć tego komponentu, należy przekazać mu parametr
<Hello name="World" />
Parametry typu string można podać bezpośrednio tak jak tutaj, a wszelkie inne muszą być opatrzone w klamry:
<Button size={4} style={ { display: "block" } }>Search</Button>
Jeśli chcemy w komponencie funkcyjnym użyć stanu, to możemy użyć funkcji
const [state, setState] = React.useState(initialValue);
Dostajemy stały obiekt state i funkcję do jego modyfikacji
setState( oldState => newState )
Drugim typem komponentów są komponenty klasowe. Te będą dziedziczyć po klasie React.Component.
interface P { initial: int; }
interface S { counter: int; }
export class Counter extends React.Component<P, S> {
    constructor(props: P) {
        super(props);
        this.state = { counter: props.initial };
    }
    public inc() {
        this.setState(s => ({...s, counter: s.counter + 1 }));
    }
    public render() {
        return (
            <button onClick={this.inc.bind(this)}>
                {this.state.counter}
            </button>
        );
    }
}
Przy większych komponentach klasy są wygodniejsze.
Testowanie
Powiedzmy, że mamy jakiś abstrakcyjny interfejs
interface IDataProvider {
    getData(): Promise<string[]>;
}
I mamy komponent
interface P { dataProvider: IDataProvider; }
export class DataList extends React.Component<P, string[]> {
    public async componentDidMount() {
        const data = await this.props.dataProvider.getData();
        this.setState(data);
    }
    public render() {
        return (
            <React.Fragment>
                {
                    this.state.map((s, i) => (
                        <p key={i}>{s}</p>
                    ))
                }
            </React.Fragment>
        );
    }
}
Ta pierwsza metoda to jest taki trochę onLoad dla komponentów. Podczas renderowania zauważmy, że w jeśli tworzymy tablicę tagów, to każdy musi dostać unikatowy parametr key, taki jest wymóg Reacta.
No i chcemy teraz przetestować ten komponent. Utworzymy sobie plik testowy DataList.test.tsx
import Enzyme, {mount, shallow} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
import { IDataProvider } from "./IDataProvider.ts";
import { DataList } from "./DataList.tsx";
Enzyme.configure({adapter: new Adapter() });
const data = ["Hello"];
function mockDataProvider() {
    const Mock = jest.fn<IDataProvider, []>(() => ({
        getData: jest.fn(() => data)
    }));
    return Mock;
}
describe("DataList component", () => {
    it("when mounted gets data", () => {
        const mockDP = mockDataProvider()();
        const list = shallow<DataList>(
            <DataList dataProvider={mockDP} />
        );
        setTimeout(() => {
            const mockGet = mockDP.getData as jest.Mock<Promise<string[]>, []>;
            expect(mockGet).toHaveBeenCalled();
            expect(list.state()).toEqual(data);
            done();
        });
    })
})
Dobra, trochę tu jest do odpakowania. Więc korzystam z biblioteki Jest do testów i biblioteki Enzyme do pracy z komponentami Reacta. Enzyme potrzebuje adaptera do wersji Reacta, której używam.
Tworzymy mock naszego interfejsu - obiekt z funkcją, którą możemy odpytać o to czy była wywołana, ile razy i z jakimi argumentami.
Ponieważ funkcja zwraca Promise to musimy wywołać część testu za chwilę, po tym jak asynchroniczna kontynuacja montowania zostanie wykonana. Na koniec wołamy done(), żeby powiadomić środowisko o zakończeniu testu.
Enzyme daje nam dwie funkcje: shallow i mount. Pierwsza tworzy komponent ale nie tworzy jego dzieci. Druga tworzy komponent wraz z dziećmi i pozwala wyciągać dzieci przez .find(ComponentName) i wywoływać na nich metody lub eventy.
No i to tyle w tym temacie. Miłego programowania!
