Jestでwindowオブジェクトをモックする方法

この記事では以下の Window.js と CheckHostName.js を使用します。

export const getHostName = () => {
  return window.location.hostname;
};
import * as Window from "./Window";

export const isTwitter = () => {
  return window.location.hostname === "twitter.com";
};

export const isTwitterCallWrapperFunction = () => {
  return Window.getHostName() === "twitter.com";
};

Windowモジュールをモックする

windowオブジェクトのプロパティの値を返す関数を作ってモックする。

import { isTwitterCallWrapperFunction } from "./CheckHostname";
import * as Window from "./Window";

describe("Windowモジュールをモックする", () => {
  it("ホスト名が「twitter.com」のとき、isTwitterCallWrapperFunction が true を返す", () => {
    const expectedHostName = "twitter.com";
    Window.getHostName = jest.fn().mockReturnValue(expectedHostName);
    expect(isTwitterCallWrapperFunction()).toBe(true);
  });

  it("ホスト名が「twitter.com」以外のとき、isTwitterCallWrapperFunction が falseを返す", () => {
    const expectedHostName = "google.com";
    Window.getHostName = jest.fn().mockReturnValue(expectedHostName);
    expect(isTwitterCallWrapperFunction()).toBe(false);
  });
});

windowオブジェクトのプロパティを上書きする

defineProperty でモック関数を返すようにする。

import { isTwitter } from "./CheckHostname";

describe("windowオブジェクトのプロパティを上書きする", () => {
  const tmpLocation = window.location;

  afterAll(() => {
    Object.defineProperty(window, "location", {
      get: () => {
        return tmpLocation;
      },
    });
  });

  it("ホスト名が「twitter.com」のとき、isTwitter が true を返す", () => {
    const expectedHostName = "twitter.com";
    Object.defineProperty(window, "location", {
      get: jest.fn().mockReturnValue({ hostname: expectedHostName }),
    });
    expect(isTwitter()).toBe(true);
  });

  it("ホスト名が「twitter.com」以外のとき、isTwitter が falseを返す", () => {
    const expectedHostName = "google.com";
    Object.defineProperty(window, "location", {
      get: jest.fn().mockReturnValue({ hostname: expectedHostName }),
    });
    expect(isTwitter()).toBe(false);
  });
});

windowオブジェクトをspyする

Jest の spyOn を使う。

import { isTwitter } from "./CheckHostname";

describe("windowオブジェクトをspyする", () => {
  it("ホスト名が「twitter.com」のとき、isTwitter が true を返す", () => {
    const expectedHostName = "twitter.com";
    const originalWindow = { ...window };
    const windowSpy = jest.spyOn(global, "window", "get");
    windowSpy.mockImplementation(() => ({
      ...originalWindow,
      location: {
        ...originalWindow.location,
        hostname: expectedHostName,
      },
    }));
    expect(isTwitter()).toBe(true);
  });

  it("ホスト名が「twitter.com」以外のとき、isTwitter が falseを返す", () => {
    const expectedHostName = "google.com";
    const originalWindow = { ...window };
    const windowSpy = jest.spyOn(global, "window", "get");
    windowSpy.mockImplementation(() => ({
      ...originalWindow,
      location: {
        ...originalWindow.location,
        hostname: expectedHostName,
      },
    }));
    expect(isTwitter()).toBe(false);
  });
});

VSCodeで特定のディレクトリ以下の未使用import文をすべて削除する [Javascript/TypeScript]

ファイルの保存時に organize imports をしていれば未使用の import文 が commit されることはほぼないと言っても過言ではありません。

例: VSCode でファイル保存時に未使用の import文 を削除 & 自動ソートする設定。 settings.json に以下を記述する。

  "editor.codeActionsOnSave": {
    "source.organizeImports": true
  }

eslint で unused-var を有効にしている場合も lint チェックで気付いて commit 前に修正することができます。しかし、目視チェックしてるプロジェクトだと容易に差分に紛れ込みます。あとから eslint を入れて未使用の import文 を洗い出したようなプロジェクトだと、対象となるファイルの数が多すぎて1つ1つ確認&修正していくのはかなり面倒くさいです。

そういうときに特定ディレクトリ以下のすべての Javascript/TypeScript ファイルの未使用の import文 をまとめて削除できたら便利ですよね。ついでにソートもしてほしいですよね。今回はそのやり方について紹介します。

拡張機能を使う方法

  1. Folder source actions をインストールする
  2. ディレクトリを右クリック → "Organize Imports in Folder" を選択する

ファイル数が多い場合はファイルの自動保存を有効にした状態で実行した方がいいです。変更があったファイルをあとからすべて上書き保存しようとすると VSCode がフリーズしました。

ちなみに IntelliJ IDEA には同様の操作で import文 の自動削除とソートをやってくれる Optimize Imports という機能が標準で入ってます。

コマンドラインツールを使う方法

保存時に organize import すると Prettier みたいなフォーマット系の拡張と衝突してうまく動かないみたいな話を聞くので、できれば git hook で commit するときに適用されるようにしたいです。

organize-imports-cli を使うとコマンドラインから実行することができます。

github.com

個人的にはコマンドラインツールを使ったやり方の方がオススメです。

異なるDOMノードにレンダリングする2つの React Component 間で状態を共有する

結論

Portalを使う ja.reactjs.org

解説

既存のサイトを部分的に React に置き換えている場合、それぞれ異なるDOMノードにレンダリングするコンポーネント間で状態を共有したい場面に遭遇することがあります。

例えば、次のようなヘッダー、サイドバー、コンテンツを表示するサイトを想像してみてください。

f:id:dev-moyashi:20200531155557p:plain

<html>
  <body>
    <div id="root"></div>
    <header class="header"></header>
    <aside class="sidebar"></aside>
    <main class="content"></main>
  </body>
</html

このサイトのヘッダーとサイドバー部分を React で置き換えているとします。

ヘッダーにはサイドバーの表示/非表示を切り替えるボタンを設置し、クリックするたびにサイドバーが開閉します。

このとき、別々のDOMノードにコンポーネントレンダリングしようとすると以下のようになります。

ReactDOM.render(<Header />, document.querySelector(".header"));
ReactDOM.render(<Sidebar />, document.querySelector(".sidebar"));

ただし、ReactDom.render() で別々のDOMノードにレンダリングすると、Header と Sidebar コンポーネント間でサイドバーの開閉状態を共有することができません。

そこで使用するのが React の Portal です。

Portalを使えば、Virtual DOM 上では通常の親-子コンポーネントと同じように props を渡すことができて、実DOM上では親コンポーネント外の箇所に子コンポーネントレンダリングすることができます。

使い方は ReactDOM.createPortal の第1引数にコンポーネント、第2引数にDOMノードを指定するだけです。

const Index = () => {
  const [isOpenSidebar, setOpenSidebar] = useState(false);

  const headerPortal = ReactDOM.createPortal(
    <Header setOpenSidebar={setOpenSiebar} />,
    document.querySelector(".header")
  );

  const sidebarPortal = ReactDOM.createPortal(
    <Sidebar isOpenSidebar={isOpenSidebar} />,
    document.querySelector(".sidebar")
  );

  return (
    <>
      {headerPortal}
      {sidebarPortal}
    </>
  );
};

ReactDOM.render(<Index />, document.getElementById("root")); // 任意のDOMノード

参考

Chrome拡張でiframe内のDOMの変化を監視する

Chrome拡張を作っているとiframe内の DOM の変化を監視したい場面に遭遇することがあると思います。監視する方法はとても簡単です。

manifest.json に "all_frames": true を追記して、すべてのフレームにスクリプトを挿入するように設定します。そして、iframe内の要素を MutationObserver で監視します。

検証環境

検証用のページ

検証用のページ

iframe_content.html

<html>
  <body>
    ...
    <h1>Content of an iframe</h1>
    <button id="addDivButton">Add div element</button>
    ...
  </body>
  <script>
    const addDivButton = document.getElementById("addDivButton");
    addDivButton.addEventListener("click", () =>
      document.body.appendChild(document.createElement("div"))
    );
  </script>
</html>

content.html

<html>
  <body>
    ...
    <h1>Content</h1>
    <iframe src="iframe_content.htmlのurl"></iframe>
    ...
  </body>
</html>

Chrome拡張

content_script.js

console.log('load content script');
const observer = new MutationObserver(() => console.log("Added div element"));
observer.observe(document.body, { childList: true, subtree: true });

manifest.json

{
    "name": "ChromeExtension",
    "version": "1.0.0",
    "manifest_version": 2,
    "permissions": ["<all_urls>"],
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "js": ["js/content_script.js"],
            "all_frames": true
        }
    ]
}

all_framesを有効にする

manifest.json に "all_frames": true を設定します。

all_framesを有効にすることで、iframeを含むすべてのフレームでスクリプトが実行されるようになります。

デフォルトは false です。

Optional. Defaults to false, meaning that only the top frame is matched. If specified true, it will inject into all frames, even if the frame is not the topmost frame in the tab. Each frame is checked independently for URL requirements, it will not inject into child frames if the URL requirements are not met.

Chrome Extensions content scripts - Chrome Developers

Chrome拡張をブラウザにインストールして検証用のページをリロードするとload content scriptが2回表示されました。 コンテンツとiframe内のコンテンツ両方から実行されていることを確認するために、ログにdocument.bodyを出力してみます。

以下、コンソールに出力された要素をマウスホバーした結果です。コンテンツのbody要素と、iframe内のコンテンツのbody要素をそれぞれ取得できていることが分かります。

iframe内のDOMの変化を監視してみる

MutationObserver を使ってDOMの変更を監視します。

developer.mozilla.org

通常、iframe の外で iframe内の DOM の変更を監視するためには、iframe内の要素(iframeObject.contentDocument)を取得して、MutationObserver 引数に指定する必要があります。

all_frames を true に設定することで、スクリプトが iframe の外と内でそれぞれ実行されるようになるので、iframeの外で IFrame Object から body 要素を取得しなくてもDOMの監視ができるようになります。

body要素にdiv要素を追加するボタンを押してみるとDOMの変更を監視できていることがわかります。

余談

all_frames の存在を知る前は以下のようなコードを書いてました。

type Callback = (addedEl: HTMLElement) => void;

export const addedElementObserver = (targetEl: HTMLElement, callbackList: Callback[]) => {
    const observer = new MutationObserver((records: MutationRecord[]) => {
        for (const record of records) {
            for (const node of Array.from(record.addedNodes)) {
                const el = node as HTMLElement;
                if (!el.getElementsByTagName) {
                    continue;
                }
                for (const callback of callbackList) {
                    callback(el);
                }
                const iframeCollection = el.getElementsByTagName('iframe');
                for (const iframeEl of Array.from(iframeCollection)) {
                    iframeEl.contentWindow.onload = () => {
                        const iframeBody = iframeEl.contentDocument.body;
                        for (const callback of callbackList) {
                            callback(iframeBody);
                        }
                        addedElementObserver(iframeBody, callbackList);
                    };
                }
            }
        }
    });
    observer.observe(targetEl, {
        childList: true,
        subtree: true,
    });
};

addedElementObserver(document.body, [(addedEl: HTMLElement) => console.log(addedEl)]);

監視対象のDOMに iframe が追加されたら、その iframe の読み込みが完了するのを待って、読み込みが完了したら iframe内の body要素を取得して監視対象にするコードです。

どちらを使ってもいいと思いますが all_frames の設定を有効にする方がコード量が減るのでおすすめです。

OpenCVで1つのウインドウに複数の画像を表示する

表示させたい画像の画素の配列を結合して、imshow の引数に渡す。

import cv2
import numpy

if __name__ == "__main__":
    catImg1 = cv2.imread("./cat1.jpg")
    catImg2 = cv2.imread("./cat2.jpg")
    mergeImg = numpy.hstack((catImg1, catImg2))
    cv2.imshow("sample", mergeImg)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


hstack

f:id:dev-moyashi:20191230173158p:plain

vstack

f:id:dev-moyashi:20191230173727p:plain:w400:left

参考

Javaのジェネリクスの型安全について自分なりにまとめてみた

Effective Java 第3版を読んでみたところ、自分がジェネリックスの型安全について理解できていないことがわかったのでまとめてみました。


はじめに

Animal クラスを継承した Cat クラスと Human クラスについて考える。

public class Animal { }
public class Cat extends Animal { }
public class Human extends Animal { }

f:id:dev-moyashi:20191227233419p:plain:w200

記事の中では主に上記のクラスを用いて解説を行う。


Javaの型変換

ジェネリクスの型安全について考えていると頭が混乱するので、ひとまず頭を整理するため Java の型変換について復習する。

アップキャスト

アップキャストとはスーパークラス(親クラス)からサブクラス(子クラス)へ変換のことである。

子クラスは親クラスを継承しているため、親クラスのフィールドや関数を全て持っている。そのため、親クラスに、子クラスのインスタンスを代入することができる。

Cat cat = new Cat();
Animal animal = cat;

ダウンキャスト

ダウンキャストとはサブクラスからスーパークラスへの変換のことである。

親クラスは子クラスが持つ独自の関数やフィールドを持っていないため、親クラスのインスタンスを子クラスに代入しようとするとコンパイルエラーになる。

Animal animal = new Animal();
Cat cat = (Cat) animal; // java.lang.ClassCastException: class Animal cannot be cast to class Cat

ダウンキャストを行うときは親クラスの実体が子クラスでなければならない。

つまり、以下のように親クラスを子クラスでインスタンス化する必要がある

Animal animal = new Cat();
Cat cat = (Cat) animal;


ジェネリクス

List<Animal><Animal>の部分。

List には様々なオブジェクトを入れることができるが、ジェネリクスを使うと代入できる型を限定することができる。

コンパイル時に型を検査してエラーを出してくれるようになり、コードを見たときに何の型を扱っているのか明確になるのでプログラマが幸せになれる。

以下のように引数の型を T とすることで、インスタンス生成時に型を指定することができる。

public class Generic<T> {
    private T value;

    public void setValue(T val) {
        value = val;
    }

    public T getValue() {
        return value;
    }
}

Generic generic = new Generic<Integer>(); // インスタンス作成時に型を指定


非境界ワイルドカード

要素の型が何であろうと構わないような場合は 非境界ワイルドカード型 を使う。

非境界ワイルドカード型は型パラメータにクエスチョン記号を付ける。例:List<?>

非境界ワイルドカード型は型安全である。原型のコレクションにはどのような要素も挿入できるが、非境界ワイルドカード型のコレクションには、(null以外の) 要素が挿入できない。また、非境界ワイルドカード型の要素はどのような型にでもなりえるため、取得した要素型はもっとも汎用的な型である Object となる。

Generic<?> generic = new Generic<Integer>();
generic.setValue(null); // nullのみ代入できる
Object value = generic.getValue(); // どのような型が返ってくるか保証できないので Object が返る


ジェネリクス型の継承関係

型パラメータ Animal を持つ List<Animal> にインスタンスを格納してみる。

Cat も Human も Animal を継承しているため、List<Animal> にインスタンスを代入することができる。

List<Animal> animals = new ArrayList<Animal>();
animals.add(new Animal());
animals.add(new Cat());
animals.add(new Human());


次に List<Animal> と List<Cat> について考える。 直感的には List に List<Cat> のインスタンスを代入できそうだが、これはコンパイルエラーとなる。

List<Animal> animals = new ArrayList<Cat>(); // error: incompatible types: ArrayList<Cat> cannot be converted to List<Animal>

同様に以下もコンパイルエラーとなる。

List<Cat> cats = new ArrayList<Animal>(); // error: incompatible types: ArrayList<Animal> cannot be converted to List<Cat>

コンパイルエラーとなる理由は、Java では型パラメータに指定したクラスに継承関係があったとしても別の型として扱われるからである。

これを不変(invariant)であると言う。つまり、ジェネリクスは不変である。

また、ジェネリクスが不変ではないと仮定する。

List<Animal> animals = new ArrayList<Cat>();が成り立つ場合を共変(covariant)

List<Cat> cats = new ArrayList<Animal>();が成り立つ場合を反変(contravariant)という。


不変性(invariant)

List<Animal> と List<Cat> に関係性がないとき、ジェネリクスは不変である。 ジェネリクスに不変性しか持たせられない場合、ジェネリクスを引数とする汎用的なメソッドを定義したい場合に困ることになる。

public void tmpAllList(List<Animal> animals) {
    ....
}

List<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
tmpAllList(cats); // List<Animal>しか受け付けないのでコンパイルエラー

様々なジェネリクスを引数にとる汎用的なメソッドを定義したいとき、それぞれのジェネリクスを引数とする同じ名前のメソッドを量産することになる


共変性(covariant)

ジェネリクスが共変であると仮定する

List<Cat> が List<Animal> のサブタイプであるとき、ジェネリクスは共変である。

ジェネリクスは不変だが、共変だったと仮定すると以下のように扱うことができる。

List<Animal> animals = new ArrayList<Cat>();

この定義が有効である場合について、まず、List<Animal>から要素を取得する場合について考えてみる。

List<Animal>の要素の返り値はAnimalCatHumanのいずれかのインスタンスになる。これらは Animal型 にキャストすることが可能なため問題はない。

List<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
List<Animal> animals1 = cats;

List<Human> humans = new ArrayList<Human>();
humans.add(new Human());
List<Animal> animals2 = human;

Animal animal1 = animals1.get(0) // Catのインスタンスが取り出せる
Animal animal2 = animals2.get(0) // Humanのインスタンスが取り出せる

次に、List<Animal>に要素を追加する場合について考えてみる。

List<Animal>に追加できる要素のインスタンスはAnimalCatHumanである。しかし、List<Animal>の実体が List<Cat>である場合、Cat のリストに Human のインスタンスが存在することになるので Java の型安全を破棄することになる。

List<Cat> cats = new ArrayList<Cat>();
List<Animal> animals = cats;
animals.add(new Human()); // ランタイムエラー Catのリストに Human が入ることになる

このような問題があるため、Javaのジェネリクス型は共変になっていない。

共変は要素を取り出すときには問題はないが、要素を追加するときに問題がある。


ジェネリクスに共変性を持たせる

上限境界ワイルドカード型 を使うとジェネリクスに共変性を持たせることができるようになる。例: List<? extends Animal>

共変は互換の無いインスタンスを格納できてしまうという問題があったが、上限境界ワイルドカード型の場合は要素を追加しようとするとコンパイルエラーとなる。これにより Java の型安全が守られる。

List<? extends Animal> animals = new ArrayList<Animal>();
animals.add(new Animal()); // コンパイルエラー
animals.add(new Cat()); // コンパイルエラー
animals.add(new Human()); // コンパイルエラー
animals.add(null); // nullのみ代入可


配列は共変

Java の配列は共変であるため、Object の配列に Long の配列を代入してもコンパイルが通る。

Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // ArrayStoreException


反変性(contravariant)

ジェネリクスが反変であると仮定する

List<Cat> が List<Animal> のスーパータイプであるとき、ジェネリクスは反変である。

ジェネリクスは不変だが、反変だったと仮定すると以下のように扱うことができる。

List<Cat> cats = new ArrayList<Animal>();

この定義が有効である場合、List<Cat>の実体はList<Animal>であるため、要素としてAnimalCatのインスタンスを追加できる。

cats.add(new Cat()); // 実体がAnimalのリストに Cat を代入しても問題はない

次にList<Animal>の要素を取得する場合について考えてみる。

List<Animal>に追加できる要素のインスタンスはAnimalCatHumanである。そのため、List<Animal>Humanのインスタンスを入れることができる。List<Cat>のサブクラスはList<Animal>(反変)であるため、List<Animal>List<Cat>にキャストできる。しかし、要素としてはHumanのインスタンスを持つのでエラーとなる。

List<Animal> animals = new ArrayList<Animal>();
animals.add(new Human());
List<Cat> cats = animals;
Cat cat = animals.get(0); // ランタイムエラー Humanのインスタンスが返る

このような問題があるため、Javaのジェネリクス型は反変になっていない。

反変は値を追加するときには問題はないが、値を取得するときに問題がある。


ジェネリクスに反変性を持たせる

下限境界ワイルドカード型 を使うとジェネリクスに反変性を持たせることができるようになる。例: List<? super Animal>

反変は互換の無いインスタンスを取得できてしまうという問題があったが、上限境界ワイルドカード型の場合は要素を取得しようとするとコンパイルエラーとなる。これにより Java の型安全が守られる。

List<? super Animal> animals = new ArrayList<Animal>();
animals.add(new Object());
animals.add(new Animal());
animals.add(new Cat());
animals.add(new Human());

Object object = animals.get(0); // OK
Animal animal = animals.get(1); // コンパイルエラー
Cat cat = animals.get(2); // コンパイルエラー
Human human = animals.get(3); // コンパイルエラー


ジェネリクスの不変・共変・反変のまとめ

  • 不変(invariant): List<Animal> と List<Cat> には関係性がない
  • 共変(covariant): List<Cat> は List<Animal> のサブタイプ
    • 要素を追加するときに型安全を破棄してしまう。
    • ジェネリクスに共変性を持たせるときは 上限境界ワイルドカード型(List<? extends T>) を使う。
  • 反変(contravariant): List<Cat> は List<Animal> のスーパータイプ
    • 要素を取得するときに互換性のない型が取得できてしまう。
    • ジェネリクスに反変性を持たせるときは 下限境界ワイルドカード型(List<? super T>) を使う。


参考

Windows版のRでパッケージのインストールに失敗するときの対処法

Rはパッケージのインストールをネットワーク経由で行います。

メニューの「パッケージ」→「パッケージのインストール」もしくはinstall.packagesを端末に打ち込んで、CRAN(Comprehensive R Archive Network)から各種パッケージをダウンロードします。

このとき、windows版のRで以下のようなエラーメッセージが表示されてパッケージがインストールできないことがあります。

> utils:::menuInstallPkgs()
 --- このセッションで使うために、CRAN のミラーサイトを選んでください --- 
 警告:   リポジトリー https://cran.ism.ac.jp/src/contrib に対する索引にアクセスできません :
   URL 'https://cran.ism.ac.jp/src/contrib/PACKAGES' を開けません 
 install.packages(NULL, .libPaths()[1L], dependencies = NA, type = type) でエラー: 
   如何なるパッケージも指定されませんでした

原因の特定

Rを起動して、以下のコードを実行してください。

source("http://noucobi.com/Rsource/tameshi.txt")

Rがインターネットに接続できているときは以下の文字列が表示されます。

この場合、パッケージが置いてあるミラーサイトがダウンしている可能性があります。

Congratulations!
You've successfully accessed to this message through the internet.

大学や会社のネットワークのように外部にプロキシサーバー経由で接続している環境では以下のようなエラーが表示されます。

この場合はRのプロキシ設定を変更します。

以下にエラー file(filename, "r", encoding = encoding) : コネクションを開くことができません 
追加情報:  警告メッセージ: In file(filename, "r", encoding = encoding) : 'noucobi.com' をポート 80 でコネクトできません 

ミラーサイトの変更

optionsのreposにミラーサイトのurlを指定します。

ミラーサイトを選択するウインドウでJapanを選んだ場合は東京の統計数学研究所になります。

ミラーサイト一覧はこちら

例: options(repos="https://ftp.yz.yamagata-u.ac.jp/pub/cran/")

プロキシ設定

Rguiのショートカットを右クリックしてプロパティを開き、リンク先の末尾に--internet2オプションを付けてあげるとウィンドウズのプロキシ設定を使用して外部のネットワークと接続できるようになります。

例: "C:\Program Files\R\R-<Rのバージョン>\bin\x64\Rgui.exe" --internet2

認証プロキシが必要な環境ではRで下記のコマンドを入力します。

Sys.setenv("http_proxy"="http://<ProxyサーバのIP or ホスト名>:<Port番号>")

これでパッケージインストールの際にプロキシユーザーとパスワードを入力するダイアログが出現するようになります。

パッケージを保存するためのディレクトリが作成できないときはこちらを参考にしてください。

dev-moyashi.hatenablog.com