W dzisiejszym odcinku learning log’a: migracja mocha -> jest, nowe feature’y w jest v23, css-in-js - emotion, storybook vs styleguidist, mockowanie requestów w cypress.io oraz Object.create(null). Zapraszam.

Miniony tydzień upłynął mi pod znakiem productivity boost - taki przynajmniej feedback dostałem od zespołu po prezentacji zmian jakich dokonałem w projekcie. Dostałem do implementacji całkiem nowy moduł i przy okazji postanowiłem zakwestionować nasze projektowe status quo. Zaowocowało to poznaniem kilku nowych narzędzi oraz pierwszym poważnym pull-request’em do projektu open source jakim jest cypres.io ! :) Ale po kolei…

mocha -> jest migration

Już dawno chciałem to zrobić. W sumie zrobiłem to już miesiąc temu, ale przy pierwszej implementacji czas wykonania testów wzrósł dwukrotnie :( Nie spodobało się to reszcie zespołu, tym bardziej, że byliśmy wtedy jeszcze przed zmianą wersji node.js, która to ostatecznie przyspieszyła nam cały proces budowania. Do tematu postanowiłem wrócić po dokładnej analizie plusów i minusów, które przedstawiłem na forum zespołu...Okazało się, że dłuższy czas wykonania testów jest niczym przy benefitach jakie daje jest w porównaniu do mocha.js:

  • code coverage by default - bez żadnego czary mary z babelem
  • jest.mock - mockowanie importów !!
  • watch mode, który potrafi odpalić testy tylko dla zmienionych plików na podstawie git-stage
  • snapshot-testing
  • wbudowane machery i mocki (docelowo moglibyśmy pozbyć się bibliotek chai i sinon)
  • jsdom by default

Jak widzicie jest to nie tylko test-runner. Posiada wiele wbudowanych, niezbędnych funkcji, które w przypadku mocha.js, również są dostępne, ale trzeba je osobno instalować i konfigurować. Przy użyciu jest wszystko dostajemy z automatu.

Migracja odbyła się bezproblemowo. Pierwszym krokiem było dodanie pliku konfiguracyjnego jest.config.js.

const { defaults } = require("jest-config");

module.exports = {
    testEnvironment: "node",
    testPathIgnorePatterns: [...defaults.testPathIgnorePatterns, "/cypress/"],
    moduleNameMapper: {
        "\\.scss$": require.resolve("./test/mocks/style"),
        "^app\/(.*)$": "<rootDir>/src/main/javascript/app/$1",
        "^shared\/(.*)$": "<rootDir>/src/main/javascript/shared/$1"
    },
    setupTestFrameworkScriptFile: require.resolve("./test/jest-setup.js")
};

jest.config.js

Jako, że dotychczas testy odpalaliśmy w środowisku node, również w przypadku jest ustawiamy testEnvironment: "node". Przy testach korzystających z DOM używaliśmy jsdomify. Ustawienie środowiska na node uchroniło nas przed koniecznością usuwania z testów struktur typu:

beforeEach(() => jsdomify.create());
afterEach(() => jsdomify.destroy());

jsdomify

Gdybyśmy zostawili domyślną wartość jaką jest jsdom to dostalibyśmy konflikt. Docelowo i tak chcemy pozbyć się jsdomify na rzecz jsdom, który przychodzi w pakiecie z jest...ale to w następnych iteracjach ;)

Dalej do ignorowanych wzorców dodaliśmy testy z cypress.io, o których przeczytacie nieco niżej ;)

Kolejnym krokiem było ustawienie mapowania. W naszym webpacku mamy skonfigurowane aliasy do pewnych folderów:

resolve: {
        alias: {
            app: path.join(__dirname, "src/main/javascript/app"),
            components: path.join(__dirname, "src/main/javascript/components"),
            utils: path.join(__dirname, "src/main/javascript/shared/utils"),
            shared: path.join(__dirname, "src/main/javascript/shared"),
        },
        extensions: [".webpack.js", ".web.js", ".js", ".json", ".jsx"]
    },

webpack aliases

Te same aliasy musieliśmy zdefiniować dla jest’a.

moduleNameMapper: {
        "\\.scss$": require.resolve("./test/mocks/style"),
        "^app\/(.*)$": "<rootDir>/src/main/javascript/app/$1",
        "^shared\/(.*)$": "<rootDir>/src/main/javascript/shared/$1"
    },

moduleMapper in jest

Oprócz ustawienia aliasów w pierwszej linijce deklarujemy mapping na moduły scss. Uchroni to nas od błędów parsowania plików scss i css. Plik z naszym mockiem wygląda tak:

module.exports = {};

style mock

Ostatnim krokiem było ustawienie ścieżki do pliku, który inicjalizuje nasze testy.

import "babel-polyfill";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import chai from "chai";
import chaiEnzyme from "chai-enzyme";
import chaiMoment from "chai-moment";
import sinon from "sinon";
import sinonChai from "sinon-chai";
import sinonStubPromise from "sinon-stub-promise";
import chaiSubset from "chai-subset";
import "./chaiHelpers";

Enzyme.configure({ adapter: new Adapter() });

sinonStubPromise(sinon);
chai.use(sinonChai);
chai.use(chaiEnzyme());
chai.use(chaiMoment);
chai.use(chaiSubset);

global.context = describe;
global.before = beforeAll;
global.after = afterAll;

setup tests

Plik inicjalizujący nie różni się niczym od pliku, który używany był przy mocha.js. Ładujemy enzyme wraz z adapterem, chai do asercji i sinon do mocków. To co trzeba było dodać, to mapper na kilka funkcji hook, które nie występują, bądź nazwane są inaczej w jest, a są dostępne w mocha.js. I tak context zamieniliśmy na describe, before na beforeAll i after na aferAll.

global.context = describe;
global.before = beforeAll;
global.after = afterAll;

mocha -> jest hooks

To wszystko!. Od tej pory mogliśmy cieszyć się funkcjami jakie daje jest. I chociaż testy odpalają się ~2x dłużej (24s -> 48s) to cała reszta benefitów sprawia, że ten jeden minus nie robi tak wielkiej różnicy. Tym bardziej, że po zmianie na jest dostaliśmy masę ostrzeżeń w konsoli, które w następnych iteracjach będziemy sprawdzać i naprawiać. Dam Wam znać jak poszło w następnych learning logach ;)

jest 23 - new features

Tak się złożyło, że również w tym tygodniu wydana została nowa wersja jest otagowana numerkiem 23. Wprowadza ona wiele usprawnień i poprawek, o których możecie poczytać na oficjalnej stronie biblioteki. Tutaj wspomnę tylko o kilku:

jest-each

Libka została włączona do jest-core i pozwala na zdefiniowanie zbioru test-case’ów w formie tablicy bądź Spoke Data Tables. Dzięki temu nie musimy powtarzać naszych funkcji it dla każdego przypadku testowego.

const cases = [
    [0, 0 ,0],
    [1, 0, 1],
    [1, 1, 0],
    [2, 1, 1],
];

test.each(cases)("should return %s when adding %s to %s", (expected, a, b) => {
    expect(a + b).toBe(expected)
});

jest-each

watch-plugins

Zespół jest przepisał na nowo moduł watch. Dzięki temu można teraz dodawać pluginy. Jednym z nich jest jest-watch-typeahead, który wyświetla listę plików, które pasują do naszego reg-ex’a.

jest watch plugin
source: https://facebook.github.io/jest

blazing badge 🔥

Chyba najlepsza zmiana w całej paczce :) Pozwólcie, że zacytuję

Blazing 🔥: We've added a blazing badge to the README to indicate that Jest is blazing good.

Oprócz tego nowe matchery, interaktywne snapshoty i wiele innych zmian i dodatków o których przeczytacie na oficjalnej stronie biblioteki.

css-in-js - emotion

Jeżeli kiedykolwiek utrzymywałeś dużą aplikację internetową, to wiesz jaki problem stwarza pisanie skalowalnego css’a. Martwe reguły, nadpisane selectory, a jeśli używasz sassa to pewnie jeszcze zagnieżdżone głęboko selectory, których nie da się nadpisać. To wszystko w połączeniu z komponentowym, react’owym podejściem daje niezły misz-masz. Z pomocą przychodzi css-in-js. I nie chodzi wcale o pisanie styli inline ;) To nowe podejście do stylowania aplikacji. Całość polega na tym, że piszemy nasze reguły css w plikach js. Ale...ale jak to? Przecież to nie jest #poBożemu !! No dobra, a powiedzcie mi czy html w js to już jest #poBożemu ;) ? ...no właśnie...wszystko ewoluuje i ten trójpodział we frontendzie na html, js, css powoli zanika. React sprawił, że nasze markupy html’a piszemy jako jsx….a podejście css-in-js sprawia, że i style możemy pisać w plikach js. Co nam to daje ?

  • eliminację martwego kodu css,
  • łatwiejszy refactoring css,
  • eliminację konfliktów w globalnej przestrzeni nazw klas,
  • unit testy dla CSS’ów - WAT !? :),
  • współdzielenie kodu css - explicite import

Jeśli chcecie poczytać więcej odsyłam Was do kilku artykułów:

No dobra, postanowione - wprowadzam css-in-js do projektu! Pierwsze co, to trzeba wybrać bibliotekę….no właśnie...którą wybrać? Postawiłem na emotion. Dlaczego? Wydaje się być szybsza w porównaniu z chociażby bardzo znaną libką styled-components.

css in js benchmark
source: https://medium.com/@tkh44/emotion-ad1c45c6d28b

Dodatkowym bodźcem, który popchnął mnie do wykorzystania emotion to post mojego “kolegi” z zeszłego tygodnia ;) Kent’a C. Dodds’a - o tym jak zaprzestał rozwoju glamourus na rzecz wykorzystania emotion. Być może dałem się zmanipulować albo wybrałem najlepszą libkę do css-in-js ;) Myślę, że przekonamy się jak wyjdą pierwsze problemy :)

Instalacja emotion? Bułka z masłem

$ npm install --save emotion

Użycie emotion ? Pikuś! Możemy to zrobić na dwa sposoby, albo przy użyciu styled albo css. Różnica jest taka że css generuje nam nazwę klasy, którą dodajemy sami do naszego komponentu, a styled zwraca nam gotowy, ostylowany komponent.

import React from "react"
import { css } from 'emotion'

const className = css`
  color: hotpink;
`;

const Cmp = () => <div className={ className }>Hot pink</div>;

css

import React from "react"
import { styled } from 'emotion'

const StyledHotPink = styled("div")`
  color: hotpink;
`;

const Cmp = () => <StyledHotPink>Hot pink</StyledHotPink>;

styled

Super! To teraz zostaje nam tylko migracja starych komponentów, ostylowanych za pomocą scss’a na stylowanie za pomocą emotion. Od czego by tu zacząć? … Button.jsx wygląda wyzywająco :)

Na początku zobaczmy jak wyglądała implementacja po staremu:

@import "./common";
@import "./buttonConsts";

$change: 5%;

$default-border-color: #c2cfd8;
$default-text-color: #89929c;
$light-text-color: #fff;
$primary-background-color: #4ec3ce;
$primary-border-color: #3fafb9;

@mixin default-button-styles() {
  background-color: #fff;
  box-shadow: none;
  color: $default-text-color;
}

@mixin default-button-hover-styles() {
  border-color: darken($default-border-color, $change);
  color: darken($default-text-color, $change);
}

@mixin default-primary-button-styles() {
  background-color: $primary-background-color;
  color: $light-text-color;
}

@mixin default-primary-button-hover-styles() {
  color: $light-text-color;
  background-color: lighten($primary-background-color, $change);
  border-color: lighten($primary-border-color, $change);
  box-shadow: none;
}

.button {
  @include default-button-styles();

  display: inline-block;
  height: $button-height;
  line-height: $default-line-height;
  min-width: 66px;
  padding: 4px 11px 5px;
  border: 1px solid $default-border-color;
  border-radius: 4px;
  background-color: #fff;
  white-space: nowrap;
  font-family: 'Source Sans Pro', sans-serif;
  color: $default-text-color;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: color 100ms ease-out, border-color 100ms ease-out;
  user-select: none;

  + .button {
    margin-left: 5px;
  }

  &:active {
    border: 1px solid $default-border-color;
    box-shadow: inset 0 1px 0 0 #c5ccd8;
    background-color: darken(#fff, 6%);
    color: darken($default-text-color, $change);
  }

  &:hover {
    @include default-button-hover-styles();
  }

  &.transparent {
    background: transparent;
  }

  &.button--disabled {
    @include default-button-styles();

    &:hover,
    &:active {
      @include default-button-hover-styles();
    }
  }
}

.button--primary {
  @extend .button;
  @include default-primary-button-styles();

  border-color: $primary-border-color;

  &:hover {
    @include default-primary-button-hover-styles();
  }

  &:active {
    background-color: #55bac4;
    box-shadow: inset 0 1px 0 0 rgba(56, 121, 128, 0.34);
  }

  &.button--disabled {
    @include default-primary-button-styles();

    &:hover,
    &:active {
      @include default-primary-button-hover-styles();
    }
  }
}

.button--disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

plik sass

import React from "react";
import PropTypes from "prop-types";
import { noop } from "lodash";

import { cls } from "utils/reactUtils";

import "./Button.scss";

export const Button = ({ children, onClick, className, disabled, primary, showHelpCursor, ...props }) => (
    <div
        { ...props }
        onClick={ (e) => disabled || onClick(e) }
        className={ composeClassName({ className, disabled, primary, showHelpCursor }) }
    >
        { children }
    </div>
);

Button.propTypes = {
    children: PropTypes.node.isRequired,
    onClick: PropTypes.func,
    disabled: PropTypes.bool,
    primary: PropTypes.bool,
    showHelpCursor: PropTypes.bool
};

Button.defaultProps = {
    onClick: noop
};

export default Button;

function composeClassName({ className, disabled, primary, showHelpCursor }) {
    return cls(
        "button",
        className,
        disabled && "button--disabled",
        primary && "button--primary",
        showHelpCursor && "help-cursor"
    );
}

plik jsx

Jak widzicie spooooooooooooro kodu...co więcej w powyższej implementacji jest bug :) ... ale o nim za chwilę.

Teraz zobaczmy jak wygląda kod po migracji na css-in-js:

import React from "react";
import PropTypes from "prop-types";
import { noop } from "lodash";
import { cls } from "utils/reactUtils";

// Emotion + polished
import { css } from "react-emotion";
import { darken, lighten } from "polished";

const commonButtonStyle = css`
  display: inline-block;
  height: 30px;
  line-height: 18px;
  min-width: 66px;
  padding: 4px 11px 5px;
  border-radius: 4px;
  white-space: nowrap;
  font-weight: 600;
  text-align: center;
  transition: color 100ms ease-out, border-color 100ms ease-out;
  user-select: none;
  
  & + & {
    margin-left: 5px;
  }
`;

const dynamicStyle = ({ primary, danger, disabled, showHelpCursor } = {}) => {
    const change = 0.05;

    let bgColor = "#fff";
    let borderColor = "#c2cfd8";
    let fontColor = "#89929c";
    let activeShadowColor = "#c5ccd8";

    if (primary) {
        bgColor = "#4ec3ce";
        borderColor = "#3fafb9";
        fontColor = "#fff";
        activeShadowColor = "rgba(56, 121, 128, 0.34)";
    } else if (danger) {
        bgColor = "#f2314e";
        borderColor = "#d4223d";
        fontColor = "#fff";
        activeShadowColor = borderColor;
    }

    const hoverFunction = (primary || danger) ? lighten : darken;
    const hoverBgColor = (primary || danger) ? hoverFunction(change, bgColor) : bgColor;
    const activeBgColor = primary ? "#55bac4" : darken(danger ? change * 2 : 0.06, bgColor);

    return css`    
      background-color: ${bgColor};
      border-color: ${borderColor};
      color: ${fontColor};
      opacity: ${disabled ? 0.6 : 1};
      cursor: ${showHelpCursor ? "help" : disabled ? "default" : "pointer"};
      
      &:hover {
        background-color: ${hoverBgColor};
        border-color: ${hoverFunction(change, borderColor)};
        color: ${(primary || danger) ? fontColor : darken(change, fontColor)};
      }
      
      &:active {
        outline: none;
        border: 1px solid ${borderColor};
        box-shadow: ${disabled ? "none" : `inset 0 1px 0 0 ${activeShadowColor}`};
        background-color: ${disabled ? hoverBgColor : activeBgColor};
      }
      
      &:focus {
        outline: none;
      }
    `;
};

export const Button = ({ onClick, children, disabled, primary, danger, showHelpCursor, className, ...rest }) => (
    <button
        onClick={ (e) => disabled || onClick(e) }
        className={ cls(className, commonButtonStyle, dynamicStyle({ disabled, primary, danger, showHelpCursor })) }
        { ...rest }
    >
        { children }
    </button>
);

Button.propTypes = {
    children: PropTypes.node.isRequired,
    onClick: PropTypes.func,
    disabled: PropTypes.bool,
    primary: PropTypes.bool,
    danger: PropTypes.bool,
    showHelpCursor: PropTypes.bool
};

Button.defaultProps = {
    onClick: noop
};

export default Button;

Button po migracji

Zobaczcie, że takie funkcje jak darken i lighten, które dostępne są w sassie/compassie zastąpiłem przy pomocy biblioteki polished.js. Co więcej przykład użycia naszego Buttona, uprościł się i przy okazji naprawiłem bug’a związanego z przyciskiem danger ;)

Screen-Shot-2018-06-02-at-12.57.54

// New use case:
<Button>Standard</Button>
<Button primary>Primary</Button>
<Button danger>Danger</Button>
<Button primary disabled>Disabled Primary</Button>
...

// Old use case
<Button>Standard</Button>
<Button primary>Primary</Button>
<Button className="btn--danger">Danger</Button>
...

przypadki użycia

Widać niespójność w użyciu primary i danger. Poza tym danger w starej implementacji sprawia wrażenie "zepsutego" ;)

Niestety nie dało się całkowicie usunąć kodu button.scss :( Przyczyną tego stanu rzeczy jest wykorzystanie i nadpisanie klasy .button w innych miejscach projektu...

.some-class {
    .button.arrow-left {
        // nadpisane style
    }
}

nadpisanie stylu button

Jak widzicie jeszcze długa droga przed nami...ale staram się tym nie zrażać ;) Mam nadzieję, że css-in-js uporządkuje nam podejście do stylowania i pozwoli na łatwiejszy refactoring css’ów przy mniejszej liczbie wizualnych regresji ;)

Jeśli jesteście zainteresowani emotion.js dajcie znać w komentarzach - może urodzi się z tego osobny wpis na blogu ;)

Storybook vs Styleguidist

Bardzo często na naszym front-endowym kanale na firmowym slacku widzę pytania w stylu:

Elo...mamy w naszym kodzie komponent, który wygląda jak to (screenshot z ui-mock) ???

Po czym następuje faza przeszukiwania kodu i przypadków użycia komponentów. Pomyślałem, sobie czemu nie przygotować coś na kształt style-guide’a, w którym zamieszczę listę komponentów z przypadkami ich użycia? Bingo!

Pierwsze o czym pomyślałem to Storybook. Bibliotekę poznałem już w zeszłym roku, ale nie miałem okazji wykorzystania jej w żadnym projekcie. Ale żeby nie stawiać wszystkiego na jedną kartę postanowiłem zrobić research, dzięki któremu poznałem style-guidist. O różnicach między tymi dwoma bibliotekami możecie poczytać w artkule Storybook vs Styleguidist.

Koniec końców wygrał Storybook. Głównym powodem było to, że style-guidist wymagał większego nakładu przy konfiguracji. Myślałem też, że z automatu utworzy dokumentację do wylistowancyh komponentów. Niestety na twarz dostałem mnóstwo błędów. Jednym z nich było nadpisanie globalnego obiektu Date. Chwila chwila...co ? No tak… W naszym zbiorze komponentów mamy komponent Date.jsx. Formatuje on datę i takie tam. Okazało się, że style-guidist skanuje listę komponentów i dodaje je do globalnej przestrzeni nazw. Przez co window.Date został nadpisany naszym komponentem….i wszędzie, gdzie korzystaliśmy z Date.now dostawaliśmy błąd undefined is not a funciton. Postanowiłem skipnąć ten babol, żeby zobaczyć wygenerowaną dokumentację dla reszty komponentów. No i zawiodłem się. Jedyne co dostałem to lista komponentów z informacją w stylu: “musisz dodać plik nazwa-komponentu.md żeby zobaczyć jego przypadki użycia, bądź odpowiednio go okomentować”.

To mi wystarczyło, żeby wybrać Storybook'a. I powiem Wam, że to był dobry wybór. Teraz lista komponentów zawiera tylko te, do których zostały napisane przypadki użycia ‘stories’. Co więcej - dzięki Storybook możemy teraz tworzyć nowe komponenty w izolacji. Myślę, że bardzo pozytywnie wpłynie to na implementację chociażby Modali, do których trzeba się przeklikać po każdym refreshu strony (niestety nasz kod nie pozwala na łatwy HotModuleReloading :(....pracuję nad tym ;) ) … Storybook odświeży nam bezpośrednio stronę na której implementujemy nasz modal. Go go power-dev !! :D

Jak wygląda przykładowe użycie Storybooka?

import React from "react";
import { storiesOf } from "@storybook/react";

import Button, { LinkButton } from "./Button";

storiesOf("Button")
    .add("basic types", () => (
        <div>
            <Button>Standard</Button>
            <Button disabled>Standard Disabled</Button>
            <Button primary>Primary</Button>
            <Button primary disabled>Primary Disabled</Button>
            <Button danger>Danger</Button>
            <Button danger disabled>Danger Disabled</Button>
            <Button disabled showHelpCursor>With help cursor</Button>
        </div>
    ))
    .add("link button", () => (
        <LinkButton primary href="https://egnyte.com" target="blank">
            Link to egnyte.com
        </LinkButton>
    ));

Cypress.io - pierwsze starcie

Trzy tygodnie temu przy okazji pierwszego learning loga pisałem o cypress.io - bibliotece do testów E2E, która to szturmem podbija scenę front-endową. W tym tygodniu postanowiłem ją sprawdzić w praktyce. Napisanie całkiem nowego modułu było ku temu doskonałą okazją.

Instalacja oraz konfiguracja cypress’a odbyła się bez problemów. Na starcie dostajemy pokaźny zbiór przykładowych testów, w których możemy zobaczyć jak wykonuje się poszczególne operacje. Po szybkim przeglądzie przykładów usunąłem je i dodałem swój pierwszy plik z testem. Test polegał na prostych odwiedzinach urla i pobraniu zawartości elementu. Z uwagi na to, że moduł nad którym pracuję nie został jeszcze oficjalnie ogłoszony nie mogę Wam pokazać nagrania z testu ;) Musicie je sobie wyobrazić. Zestaw instrukcji wyglądał następująco:

context("My secret module", () => {

    it("test", () => {
        cy.visit("#/secret-link");
        cy.getByText("XYZ");
    });

});

simple cypress test

Pięknie - wszystko działa...na wersji developerskiej. Nasza wersja developerska implementuje swojego api-clienta, który zamiast wykonywać requesty do restowego API, zwraca Promis’y z generowanymi danymi - to tak w wielkim skrócie.

Żeby odpalić testy na wersji produkcyjnej bez “prawdziwego” servera pod spodem należy zamockować requesty do servera. Cyperss.io ma taki mechanizm wbudowany. Wystarczy użyć cy.server() oraz zdefiniować ścieżkę jaką chcemy zamockować cy.route(“api/call”, response).

context("My secret module", () => {

    it("test", () => {
        cy.server();
        cy.route("/api/secret-module/endpoint", [{
            name: "XYZ",
            period: "2",
            matched: 200
        }]);

        cy.visit("#/secret-link");
        cy.getByText("XYZ");
    });

});

cypress test with http mock

Prościzna...odpalamy...i ZONK!. Pomimo, że zamockowałem request do API to aplikacja nadal próbowała odpytywać server, którego nie było pod spodem...lipa...nie wiedziałem o co kaman. W końcu po krótkich poszukiwaniach odpowiedzi znalazłem pierwszy problem z cypress.io.

Problem polega na tym, że mockowanie działa tylko dla requestów wykonanych poprzez XHR. W naszej aplikacji używamy fetchAPI i niestety obecnie nie ma możliwości mockowania requestów, które wołane są za pomocą fetch’a. Ale ale...nic straconego :) developerzy cypress.io zadeklarowali, że będą pracować nad tą funkcjonalnością a do tego czasu zaproponowali workaround.

Obejście polega na dodaniu krótkiego snippeta do pliku, który inicjalizuje nasze testy:

// https://github.com/cypress-io/cypress/issues/95
Cypress.on("window:before:load", win => {
    win.fetch = null;
});

cypress init

Powyższy kod powoduje, że zamieniamy istniejącą w przeglądarce funkcję fetch na null. W takim wypadku nasz fetch-pollyfil, którego używamy dla zapewnienia kompatybilności ze starszymi przeglądarkami, zaimplementuje nam fake-fetcha, który pod spodem korzystać będzie z XHR’ów. Dzięki temu nasze mocki będą działać :) Jak się nie ma co się lubi to się lubi co się ma - przynajmniej do czasu ;)

Super! Nasze mocki działają, ale pojawił się drugi problem. Nasze style css-in-js nie są zachowywane w dom-snapshotach. Ilustruje to poniższy film.

Okazało się, że problem występuje dla większości bibliotek css-in-js a nie tylko dla emotion, który dodałem do projektu. Przyznam szczerze, że na początku się troszkę załamałem...ale po chwili potraktowałem to jako wyzwanie - kurde - może jest to okazja, żeby zanurkować w kod cypress.io i naprawić tego bug’a ?

Problem okazał się banalnie prosty. Otóż, mechanizm do robienia DOM-snapshotów w cypress zakładał, że style zapisane w tagu <style> znajdują się w jego “wnętrzu”. Tz. zdefiniowane są w ten sposób:

<style>
    .foo { color: red; }
</style>

style tag

Natomiast biblioteki css-in-js w wersjach produkcyjnych wykorzystują pewien trick dla zwiększenia wydajności stylowania. Zamiast wpisywać tekst to tagu style wykorzystują metodę insertRule. Dzięki temu style są zadeklarowane ale <style> tag pozostaje pusty.

Fix polegał na tym, że zamiast kopiować zawartość taga style, kopiowane zostają deklaracje css. Napisałem kod, dopisałem test i wystawiłem pull-requesta. W ciągu 30min dostałem review, wystawiłem poprawki a mój pull-request został wciągnięty do oficjalnego repo cypress.io !!! Jupikajej madafaka :D … Wersja 3.0.2 będzie zawierać mojego fixa ;)

Parafrazując klasyka: mały krok dla cypress.io wielki krok dla przemuh’a :D

Tak szczerze mówiąc to nie jest to pierwszy mój commit do projektu open source. Wcześniej jeszcze dodałem małą funkcjonalność do mozaik-ext-jenkins . Ale tym commitem do cypress.io jaram się o wiele bardziej ;)

Object.create(null)

Na deser mały TIP jak stworzyć obiekt, który nie jest powiązany z prototypem Object. Do tego celu wystarczy użyć Object.create(null).

let dict = Object.create(null);

// dict.__proto__ === "undefined"
// Objekt nie ma żadnych propertisów dopóki ich sami nie dodamy

let obj = Object.create({});

// obj.__proto__ === {}
// obj.hasOwnProperty === function

Object.prototype.someFunction = () => {};

// obj.someFunction === () => {};
// dict.someFunction === undefined

Szczerze mówiąc w okolicach środy myślałem, że nie będę miał o czym pisać w tym tygodniu. Okazało się jednak, że jest tego więcej niż myślałem. Co więcej - udało mi się naprawić buga w świetnej bibliotece jaką jest cypress.io...Teraz można się oddać weekendowym chilloutom ;) …

Pozdrawiam i do zobaczenia za tydzień !