銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

mocha を使った npm のユニットテストをブラウザで動かす設定

f:id:takamints:20181110123153j:plain
photo credit: wuestenigel White Cup with Coffee Grains via photopin (license)

mochaとchaiを使ったnpmのユニットテストをブラウザで動かすための設定をご紹介。

mochaにはブラウザで動作させる機能が備わっていますが、テストスクリプト以外にHTMLも用意しなくてはならないので少し億劫なんですよね。

実際には、ほぼ定型のHTMLなのですが、結構情報が少なくて「これで決まり!」みたいなのがない感じ。 コンソールとブラウザでスクリプトを共有したい場合に、問題が出て途方に暮れたこともありました。

ところが最近、ゆる~く試行錯誤を繰り返した結果、自分なりのテンプレートみたいなのが出来上がりつつあるので、ここに書いておきますよっと。

これとは逆に、WEBブラウザ向けESM(ESモジュール)のmochaのスクリプトをNode.jsでテストしたい場合の設定は、以下のページに書いています。

takamints.hatenablog.jp

テスト駆動開発
テスト駆動開発
posted with amazlet at 18.11.10
オーム社 (2017-11-13)
売り上げランキング: 7,425
目次

ブラウザでmochaのテストを行う為のHTML

以下はユニットテストブラウザーで動かすために必要なHTMLファイルです。

test/web-test.html

<!DOCTYPE html>
<html>
    <head>
        <title>npm run web-test</title>
        <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
    </head>
<body>
<h1>npm run web-test</h1>

<div id="mocha"></div>

<!-- mocha の読み込みとセットアップ -->
<script src="../node_modules/mocha/mocha.js"></script>
<script>
    mocha.setup('bdd');
    mocha.setup('tdd');
</script>

<!-- テストスクリプトを読み込む -->
<script src="./web-test.js"></script>

<!-- テストの実行 -->
<script>
    localStorage.debug = "*"; //npm debug向けのデバッグログの設定。
    mocha.run();
</script>

</body>
</html>

mochaの公式ページに掲載されているHTMLファイル(→RUNNING MOCHA IN THE BROWSER - mocha)をもとにしており、npm の開発環境にインストール(npm install --save-dev mocha chai )されているモジュールを利用するように変更しています。

元ファイルでは mocha と chai の外部スクリプトCDNから読み込んでいますが、mocha は node_modules 以下のファイルを読み込むようにして、chai はJS側で取り込みます(理由は次項で)。

debug モジュールのすゝめ

必須ではありませんが、上の例では見やすいログを出力する debug モジュールの設定をしています。 ブラウザではF12ツールのコンソールに色がついて見やすくなります。 WebではlocalStorage.debugに、出力したいロガーの名前をカンマ区切りで指定しておきますが、上の例では "*" としていますから、すべてのログが表示されます(mochaもdebugを利用しています)。

ブラウザで動作するmochaのテストスクリプト

chai は テストスクリプトで require する。

前述のように、元ファイルでは chai をSCRIPTタグで読み込んでいますが、自分的にはコンソールから実行するものと同じように、テストスクリプトで取り込むようにしています。 こうしておくことで、コンソールで実行するテストスクリプトのうち Node.js特有の機能を使っていない物をブラウザでも実行できるようになるからです。

しかし、これによってテストスクリプトをバンドラーでコンパイルしなくてはなりません。 これについては後述しますが、設定なしの Parcel を使えば簡単です。

ユーザー操作を待つテストの書き方

テストの実行時に、ボタンのクリックなどの操作を待つ必要がある場合は、その操作で解決する非同期関数を用意し、テストの中から呼び出しますようにします。 この場合、mochaのテストのタイムアウトを無効にしておく必要があります。

以下の例は、ボタンがクリックされてからテストが実行されます。

例)ユーザー操作を待つテストスクリプト

"use strict";
const assert = require("chai").assert;

/**
 * ボタンを作って押されたら解決するPromiseを返す。
 * Promiseは30秒後にリジェクトされる。
 * @returns {Promise} Promiseを返す。
 */
const buttonClicked = (() => (new Promise( (resolve, reject) => {
    const button = document.createElement("BUTTON");
    button.innerHTML = "Click to go";
    button.addEventListener("click", () => resolve() );
    document.body.appendChild(button);
    setTimeout(() => {
        document.body.removeChild(button);
        reject(new Error("The user operation timeout"));
    }, 30000);
})));

//テストターゲット
const foo = () => "bar";

//ユニットテスト
describe("foo", () => {
    it("should be bar", async () => {//非同期関数とします
        await buttonClicked();         //ボタンが押されるまで待つ関数

        assert.equal( foo(), "bar" );

    }).timeout(0);  //タイムアウト無効
});
  • 待ち時間が30秒を越えるとエラーを投入するのでテストは失敗します。
  • クリックするためのボタンを動的に追加していますが、あらかじめHTMLに書いておいても構いません。

npmのscriptsを設定する

テストに限らず、何らかのコマンドの実行は npm の scripts を使うと便利です。 node_modules 以下にあるモジュールのコマンドはパスの指定をせずに使えますから。

ここでは一例として、npm run web-test でブラウザでのテストを実行するよう設定し、 npm test または npm run test で通常のコンソールでのテストを実行するようにしています。 この辺はお好みに応じて変更してください。

package.json(一部):

"scripts": {
    "test": "mocha",
    "web-test": "parcel test/web-test.html --open",
    ・・・
    },

Parcel のすゝめ

ブラウザでのテストは、Parcel だけで 「バンドル」 → 「Webサーバー起動」 →「 ページを開く」 までを一気にやっています。 Parcelめっちゃ便利です。ブラウザーでのテストのためだけに使っても良いかも?って思っています。 BrowserifyやWeb-Packでは、いろいろ細かな設定が必要になるはずです。

ページを開いたときの実行時エラー 「regeneratorRuntime is not defined」を解消する

Parcelを使うとBabelも使うことになりますが、そのままだとブラウザでの実行時に以下のようなエラーが出ることがあります。

Uncaught ReferenceError: regeneratorRuntime is not defined
    at Suite.<anonymous> (test-script.js:38)
    at Object.create (mocha.js:720)
    at context.describe.context.context (mocha.js:532)
    at Suite.<anonymous> (test-script.js:7)
    at Object.create (mocha.js:720)
    at context.describe.context.context (mocha.js:532)
    at Suite.<anonymous> (test-script.js:6)
    at Object.create (mocha.js:720)
    at context.describe.context.context (mocha.js:532)
    at Object.parcelRequire.transworker.js.chai (test-script.js:5)

このエラーはBabelで変換対象となるような比較的新しいコードがあるときに発生するように思います。

解消するには、以下のような babel-polyfill だけをインポートするスクリプトを用意して、HTMLのSCRIPTで、テストスクリプトよりも前に読み込みます。ポリフィルはWebページから一度だけ参照すべきなので、このような対処になります。

test/babel-polyfill.js

require("babel-polyfill");

test/web-test.html

<!-- ポリフィルを読み込む -->
<script src="./babel-polyfill.js"></script>

<!-- テストスクリプトを読み込む -->
<script src="./web-test.js"></script>

コンソールでブラウザ向けテストを除外する

細かなことですが、コンソールでもテストを行っている場合は、 ブラウザ特有の機能を使っているテストスクリプトを除外してやる必要があります。

これは、mocha.opt というファイルで行います。 以下の例では、testディレクトリ内のファイル名が'web-'で始まるファイルを除外しています。

test/mocha.opt

test/!(web-)*.js

リンク

あなたが正しくウェブページのカーソル形状を変更するには

f:id:takamints:20181101110252j:plain
photo credit: Maria Eklind Frankie & Benjys bookstore and theatre Reggiano via photopin (license)

なにやら暑苦しいタイトルで失礼します。

ここにはウェブページのマウスカーソルの形状を変更するにはどうすればってことを書いてます。

「何を今さら」って感じですけど、先日、目からウロコが3つほど落ちましたので書いておきます。

小ネタといえば小ネタです。

目次

ページ全体のカーソル形状を変更する

ページ全体でマウスカーソルの形状を「待ち状態」にしたいことがありますね。 勝手に手が動くぐらいの勢いで以下のようにしていましたし、ググってみてもこのように説明されてるページが多いです。

でも、これ厳密には不完全なんですよ。

document.body.style.cursor = "wait";

何がダメかって言うと、WEBページのコンテンツが短い場合です。

コンテンツが少ないと、画面の下のほうにカーソル形状が変化しない領域が残ってしまうのです。

BODY要素(=document.body)はブラウザ画面の全体に広がっているとは限らないということです。 スタイルシートがどうなってるかにもよりますけどね。

で、これを回避するのは実に簡単。BODYじゃなくてHTMLのスタイルを変更します。

document.body.parentElement.style.cursor = "wait";

document.bodyの親要素はHTMLです。document.documentElement もHTML要素を指すそうですが、ブラウザ間の実装に違いがあるかもしれないので注意が必要。

以下のボタンを押すとHTMLのカーソル形状を変化させます。

コントロールは親要素のカーソル形状を継承しない

下のフォームのボタンを押すとBODYのカーソル形状をWaitカーソルに変えます(5秒後に元に戻ります)が、 各コントロールの上へカーソルを持って行っても形状は変化していません。 ChromeFirefoxではLABELのカーソル形状も変化しません(Edgeでは変化しました)。





既定の形状に戻すのはdefaultではない

一時的に変化させたカーソル形状を元に戻す場合、default に設定しなおしていましたが、これでは全てが矢印になっていました。

ほとんどの要素の既定の形状は矢印ですが、そうでないのもありまして。 例えばテキストボックス(<input type="text"/>)は縦線みたいなのが既定の形状。

const foo = getElementById("foo");
foo.style.cursor = "wait";
(長めの処理)
foo.style.cursor = "default"; //← 全部矢印になっちゃいます

といっても、すべての要素の既定のカーソル形状を覚えておいて、それぞれ個別に元に戻すなんてナンセンス。

じゃ、どうするか。defaultじゃなくautoにすればよいのです。

const foo = getElementById("foo");
foo.style.cursor = "wait";
(長めの処理)
foo.style.cursor = "auto"; // ← 要素ごとの既定のカーソルに戻ります。

ちなみに、最近のブラウザでは initial でも元に戻っているようです。 これはHTML5で規定されているのかもしれませんが、IE11では効きませんから、やっぱり auto でよいみたいです。

default / initial / auto でそれぞれ意味が違うのでしょうから、ややこしいですね。

EdgeがIMGタグのSVGをストレッチしてくれないことがある

f:id:takamints:20181014160024p:plain

HTMLのIMGタグでSVG画像を表示するとき、EdgeではSVG画像がIMGタグのサイズに拡大・縮小されず、SVGで指定されたサイズでそのまま表示されることがあります。

他のブラウザ(ChromeFirefoxSafari)では大丈夫でした。 また他の画像形式ならばEdgeでも問題ありません。

どうやらEdgeでは、SVGのサイズ指定方法によって表示が変わるようです。これが不具合なのかどうなのかはわかりません。

このようにEdgeはちょっと邪魔くさいことが多く、ブラウザシェアも6%程度(2018年9月現在の日本国内シェア)のようなので、できれば無視したいのですが、Windows 10の標準バンドル品なので、無視しがたいのが悩み所・・・。

※ 本記事で確認しているEdgeのバージョンは42。レンダリングエンジンEdgeHTMLのバージョンは17。


目次

SVG関連の記事:

takamints.hatenablog.jp

takamints.hatenablog.jp

発生する現象

下の画像は、9×9のクロスハッチで、SVGのサイズは1215×1215pxのSVG画像です。 Edge以外では正しく表示されているはず。

gray-9x9-crosshatch-1215x1215.svg:

<img width="270px"
  src="https://takamin.github.io/images/gray-9x9-crosshatch-1215x1215.svg"/>

Edgeでは下のように表示されてしまいます。 IMGタグの領域内にオリジナルのスケールで表示されています。

原因

SVGファイル内での画像サイズの指定がstyle属性で行われていると、(Edgeだけで)この現象が発生するようです。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   style="width:1215px; height:1215px;"
   viewBox="0 0 321.46874 321.46876"
   version="1.1"
   .
   .
   .

対策

SVGファイルのSVG要素のサイズ指定をstyleではなくwidthと height で行えばEdgeでも正しく表示してくれます。

※ widthとheight属性でサイズ指定していても、styleでサイズが指定されていれば、スケーリングされないようです。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   width="1215px"
   height="1215px"
   viewBox="0 0 71.437498 71.437502"
   version="1.1"
   .
   .
   .

以下は属性だけでサイズ指定したSVGです。Edgeでも正しく表示されているはずですよ。

<img width="270px"
  src="https://takamin.github.io/images/gray-9x9-crosshatch-1215x1215-edge.svg"/>

他の対策

他の対策もありますので以下に書いておきますね。

style属性を書き換える

SVGのstyle属性でサイズを指定していても、表示したいサイズに変更すれば問題ありません。 しかしこれだと、大きなサイズの画像を縮小してサムネイル的に表示したいときなどに、ファイルを分ける必要があり、SVGを使っている利点が出ません。

XHRでSVGを取り出して表示。

外部サイトのSVGファイルなどで編集できない場合などは、別途XHRでSVGを取り出して内容を編集してから表示する必要があるかもしれません。 この場合は、IMGタグではなくインラインSVGSVGタグ)で表示する手もあります。

何が正しいのかよくわからん

この挙動が、HTMLの仕様として正しいのかそうでもないのかは読み取れませんでした。 しかし、SVGの仕様では、style属性の中でwidthやheightを指定できるという記述は見つけられませんでした。

つまり、本来widthやheightはstyleで指定するものではないけれど、Edgeはそれを(良かれと思って?)解釈してくれて、上記のような挙動になっているのかもしれません。

リンク

developer.mozilla.org

developer.mozilla.org

developer.mozilla.org

ウェブアプリからGoogle Driveのファイルを扱う 'gdrive-fs'

f:id:takamints:20171130164857j:plain
photo credit: suzyhazelwood DSC04299-02 via photopin (license)

ウェブアプリから Google Drive のファイルを操作するモジュール(npm)を公開しましたので、ご紹介。

これからWebをはじめる人のHTML&CSS、JavaScriptのきほんのきほん
たにぐちまこと
マイナビ出版 (2017-03-27)
売り上げランキング: 13,953

npmはこちら。

www.npmjs.com

目次

関連記事:

takamints.hatenablog.jp

takamints.hatenablog.jp

機能概要

サインインしたユーザーのファイルを読み書き

このモジュールを利用するウェブアプリで、ユーザーがGoogleアカウントでサインインすると、ウェブアプリからユーザー自身のGoogle Driveのファイルへアクセスできます。

これらのファイルは、ユーザーが特別に共有設定などをしない限り、他人には見ることができません(ただしアプリはDrive内の全内容を把握できることには注意が必要)。

ファイル名によるファイルの操作

ローカルのファイルシステムと同じように、ファイル名とフォルダ名によってファイルを扱うAPIを提供しています。

Google Drive API は、ファイルIDによってファイルにアクセスしますが、パスでアクセスできるほうが直感的ですよね。

APIは、おおまかに Node.jsの fs モジュールのメソッド名に従っています。 ただし、多くが非同期関数として動作しますが、callback は受け付けず、Promiseを返します(ほとんどが async 関数です)。

UI作成支援機能

こちらはまだα版。動作確認用のサンプル実装程度です。

そのうちまじめに作って公開するつもりなのでちょっと待ってネ。

インストールと使い方

npmで公開しているので、npmでinstallしてください。

$ npm install --save gdrive-fs

バンドラーを使われているなら、CommonJS的にインポートしてください。

const Gdfs = require("gdrive-fs");
(async()=>{
    await Gdfs.loadApi(<clientId>, <apiKey>);
    .
    .
    .
})();

バンドラーを使っていない場合、HTMLのScriptタグで 本モジュールの/build/gdrive-fs.min.jsを読み込んでください。 この場合、グローバル変数として、Gdfsが利用できます。

<!DOCTYPE html>
<html>
<head>...</head>
<body>
.
.
.
<script src="<path-to-module>/build/gdrive-fs.min.js">
(async()=>{
    await Gdfs.loadApi(<clientId>, <apiKey>);
    .
    .
    .
})();
</script>
</body>
</html>

動作条件

APIキー」と「クライアント ID」

ウェブアプリ開発者は、Google Developer Console でプロジェクトを作成し、ウェブアプリケーション向けの認証情報として、「APIキー」と「OAuth 2.0のクライアント ID」を作成し、適切に設定しておく必要があります。

多くの場合、APIキーはHTTPリファラーによる制限をかけ、クライアントIDのタイプは「ウェブアプリケーション」としておきます。

APIリファレンス

本モジュールが提供しているクラス、APIの完全な説明は、以下のリンクからどうぞ。 つたない英文で書いています。

以下に要約した内容を転載します。最新情報は上記URLを参照してください。

Gdfsクラス

このクラスは Google Drive API v3 へのインターフェースであり、インスタンスはカレントディレクトリを管理して、ファイルやフォルダを操作するメソッドを提供します。

このインスタンスを作成する前に、これを利用するアプリケーションの Client Idと Api Keyにより、クラスメソッド loadApi で、このAPI群が読み込まれていなければなりません。 これら2つのキーは、Google Developer Consoleのプロジェクトで作成されていなくてはなりません。

また、ファイルを操作するにはユーザーがGoogleアカウントでサインインしておく必要があります。

このインスタンスのカレントディレクトリは、コンストラクタでルートフォルダに初期化されます。 これはchdirメソッドで変更できます。 カレントディレクトリ変更時には、oncwdupdate コールバックが呼ばれます。カレントディレクトリを知るには、cwd メソッドが利用できます。

クラスメソッド

  • async loadApi(clientId, apiKey) - Google Drive API (v3)を読み込む。
  • isSignedIn() - Google Driveにサインインしているかどうかの確認。
  • async signIn() - Google Driveにサインインする。
  • async signOut() - Google Driveからサインアウトする。

コンストラク

  • Gdfs() - コンストラク

インスタンスメソッド

  • async chdir(directory:string) - カレントディレクトリの移動。
  • cwd() - カレントディレクトリを返す。
  • async isDirectory(path:string) - パスがディレクトリかどうかを返す。
  • async mkdir(path:string) - ディレクトリの作成。
  • async readdir(path:string, options:object) - ディレクトリ内のファイルのリストを読み出す。
  • async readFile(path:string) - ファイル内容の読み取り。
  • async rmdir(path:string) - ディレクトリ削除。
  • async stat(path:string) - ファイルのメタ情報を取得する。
  • async unlink(path:string) - ファイルの削除(ゴミ箱には入らない)
  • async writeFile(path:string, mimeType:string, data:any) - ファイルへの書き込み。または新規作成。

制限事項・注意事項

名前だけではファイルを特定できない問題は無視

Google Driveでは、一つのフォルダ内に同じ名前のファイルが複数存在できます。 なので、本当は名前だけではファイルを一意に特定できません。 でも、そういう使い方自体が混乱のもとですから、本モジュールではバッサリ無視して最初に見つけたほうを採用しています(多分)。

削除してもゴミ箱には入りません

Gdfs.unlink(path) メソッド で削除した場合、ごみ箱には入りませんので、重々気を付けてくださいね。

セキュリティ警告とアプリケーションの申請について

未申請のアプリケーションに、ユーザーがサインインしようとすると「安全でないアプリケーションである」との警告が表示されます。 Googleにアプリケーションを申請して承認されていれば、これは表示されなくなるはずです。 アプリケーションを正式に公開する場合は申請しておいたほうが良いでしょう。

アプリケーションの申請はそれぞれのエンドポイントで行う必要があるため、本モジュールで行うものではありません。

ライセンス

本モジュールはMITライセンスにおいて公開しています。

免責

本モジュールは、これを利用するアプリケーションの安全性を保証せず、その動作について一切の責任を負いません。 本モジュールを利用するアプリケーションのすべての動作は、アプリケーションの製造者の責任において試験され、ユーザーに提供されるべきです。 悪意のあるアプリケーションや不具合によって被害を受けないためには、信頼できる人以外が作ったアプリケーションには接続するべきではありません。

SVGでツールチップを表示する

f:id:takamints:20181014113409p:plain

WEBページのインラインSVGツールチップを表示させる方法は、HTMLとは違うので、以下に両方の方法を書いています。

ツールチップは、画面の要素にマウスポインタを乗せてしばらくすると、説明文などが表示される小さなボックスのことですね。

ホームメイド 製菓用チョコチップ160g
共立食品
売り上げランキング: 114,017

HTMLの場合

HTMLでは「title属性(Attribute)」です。 title属性に表示したい内容を設定しておけば、マウスポインタを乗せてしばらくすると、ツールチップが表示されます。

以下のHTMLで描いたボックスにマウスを乗せると・・・

HTMLでは「title属性(Attribute)」に設定します。

ソース:

<span style="border-radius:5px; background-color: #080; color:white; font-weight:bold; padding: 1em 2em;"
class="tooltip-container"
title="これはTITLE属性の内容です。">
HTMLでは「title属性(Attribute)」に設定します。
</span>

SVGの場合

しかし、SVGにはtitle属性がありません。 「じゃどうするか?」って調べてみると「SVGでは『title要素』を使いましょう」とのことでした。

以下のインラインSVGで描いた図形にマウスを乗せるとSVGツールチップが表示されます。

これはSVGのタイトル要素。 SVGではTITLE要素に設定しますの・・・

ソース:

<svg viewport="0 0 400 100" width="100%">
  <g>
    <title>これはSVGのタイトル要素。</title>
    <rect x="0" y="0" width="400" height="100" fill="#080" rx="5" ry="5"/>
    <rect x="30" y="20" width="50" height="50" fill="#008" rx="5" ry="5"/>
    <rect x="70" y="45" width="30" height="30" fill="#088" rx="5" ry="5"/>
    <rect x="20" y="50" width="40" height="40" fill="#800" rx="5" ry="5"/>
    <circle cx="340" cy="75" r="20" fill="#880"/>
    <circle cx="310" cy="40" r="40" fill="#808"/>
    <circle cx="350" cy="32" r="30" fill="#088"/>
    <text x="25" y="80" width="400" fill="white" font-weight="bold"
        >SVGではTITLE要素に設定しますの・・・</text>
  </g>
</svg>

JavaScriptではツールチップの内容は、title要素のtextContentに設定します。