.NET Annotations Lambda Frameworkでarm64を指定する方法

.NET Annotations Lambda Frameworkは、ASP.NET MVC同様にAnnotationでRoutingが指定できるなどAWS Lambda固有のhandler廻りの処理を隠蔽できる便利なフレームワークですが、 aws.amazon.com まだPreview版ということもあってか、最新のv0.13であってもCPU Architechture("x86-64"または"arm64")を指定することができません。

Blueprint `Empty Function`で作成した場合のUpload画面(Architechture指定有り)

Blueprint `Annotations Lambda Framework`で作成した場合のUpload画面(Architechture指定無し)

一方でserverless.templateを直接いじることで指定は可能で、その指定はビルドし直しても消えません。

{
   "AWSTemplateFormatVersion": "2010-09-09",
    "Transform": "AWS::Serverless-2016-10-31",
    "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v0.13.0.0).",
+   "Globals": {
+     "Function": {
+       "Architectures": [ "arm64" ]
+     }
+   },
    "Resources": {

細かいことを言うとLambdaのArchitectureは関数単位に指定することができ、現在でもMemorySizeなどはAnnotation指定が可能(下記コードスニペット参照)となっていますが、Annotations Lambda Frameworkで作ったひとつのプロジェクト内で異なるArchitectureを使いたいということはまず無いでしょうから、上記の例のように"Globals"で書くのが楽かと。

        [LambdaFunction(MemorySize = 128)]   // ←このAnnotationでArchitectureが指定できればいいのに...
        [HttpApi(LambdaHttpMethod.Get, "/")] 
        public string Default()
        {
            var docs = @"Lambda Calculator Home:
You can make the following requests to invoke other Lambda functions perform calculator operations:
/add/{x}/{y}
/substract/{x}/{y}
/multiply/{x}/{y}
/divide/{x}/{y}
";
            return docs;
        }

dev_appserver.pyが"TypeError: environment can only contain strings"で動かないのをなんとかする

久々にGoogle AppEngine/PHPのプログラムを修正することになって、ローカルテストしようとdev_appserver.pyを起動したのですが、

ERROR:root:Failure to start PHP with: ['C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\php-5.5-Win32-VC11-x86\\php-cgi.exe', '-d', 'include_path=".;C:\\Users\\FOO\\source\\repos\\appengine-php55;C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\sdk"', '-c', 'C:\\Users\\FOO\\source\\repos\\appengine-php55', '-d', 'zend_extension="C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\php-5.5-Win32-VC11-x86\\php_xdebug.dll"', '-d', 'extension="php_gae_runtime_module.dll"', '-d', 'extension_dir="C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\php\\php-5.5-Win32-VC11-x86"']
Traceback (most recent call last):
  File "C:\Users\FOO\scoop\apps\gcloud\current\platform\google_appengine\google\appengine\tools\devappserver2\php\runtime\runtime.py", line 269, in __call__
    stdout=subprocess.PIPE)
INFO     2022-04-09 20:34:10,601 module.py:890] default: "GET /_ah/warmup HTTP/1.1" 500 734
  File "C:\Users\FOO\scoop\apps\gcloud\current\platform\google_appengine\google\appengine\tools\devappserver2\safe_subprocess.py", line 83, in start_process
    shell=shell)
  File "C:\Users\FOO\scoop\apps\gcloud\current\platform\bundledpython2\lib\subprocess.py", line 390, in __init__
    errread, errwrite)
    startupinfo)
TypeError: environment can only contain strings

で起動せず。gcloud componentは最新化したし、「パスに日本語(非ASCII文字)が含まれてませんか?」もNo。 仕方がないのでエラーが出ている runtime.py を解析するハメに。

  def __call__(self, environ, start_response):
    """Handles an HTTP request for the runtime using a PHP executable.

    Args:
      environ: An environ dict for the request as defined in PEP-333.
      start_response: A function with semantics defined in PEP-333.

    Returns:
      An iterable over strings containing the body of the HTTP response.
    """
    user_environ = self.make_php_cgi_environ(environ)

    if 'CONTENT_LENGTH' in environ:
      content = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    else:
      content = ''

    args = [six.ensure_str(arg) for arg in self.make_php_cgi_args()]

    # Handles interactive request.
    request_type = environ.pop(http_runtime_constants.REQUEST_TYPE_HEADER, None)
    if request_type == 'interactive':
      args.extend(['-d', 'html_errors="0"'])
      user_environ[http_runtime_constants.REQUEST_TYPE_HEADER] = request_type

    try:
      # stderr is not captured here so that it propagates to the parent process
      # and gets printed out to consle.
      p = safe_subprocess.start_process(
          args,
          input_string=content,
          env=user_environ,
          cwd=six.ensure_text(self.config.application_root),
          stdout=subprocess.PIPE)    # ← ここで例外発生
      stdout, _ = p.communicate()
    except Exception as e:
      logging.exception('Failure to start PHP with: %s', args)
      start_response('500 Internal Server Error',
                     [(http_runtime_constants.ERROR_CODE_HEADER, '1')])
      return ['Failure to start the PHP subprocess with %r:\n%s' % (args, e)]

というコードだったので、まずはuser_environ変数の中身をprint表示してみると

{u'TMP': u'C:\\Users\\FOO\\AppData\\Local\\Temp', u'REQUEST_ID_HASH': u'7CBB4402', u'REDIRECT_STATUS': u'1', u'SERVER_SOFTWARE': u'Development/2.0', u'REQUEST_METHOD': u'GET', u'PATH_INFO': u'/_ah/warmup', u'SERVER_PROTOCOL': u'HTTP/1.1', u'QUERY_STRING': u'', u'SYSTEMROOT': u'C:\\WINDOWS', u'DEFAULT_VERSION_HOSTNAME': u'localhost:8080', u'USER_IS_ADMIN': u'0', u'CONTENT_LENGTH': u'0', u'APPENGINE_RUNTIME': u'php', u'TZ': u'UTC', u'APPLICATION_ROOT': u'C:\\Users\\FOO\\source\\repos\\appengine-php55', u'SERVER_NAME': u'localhost', u'REMOTE_ADDR': u'0.1.0.3', u'INSTANCE_ID': u'a3f67df098d059011b5dafbdc3d41e4aacc2', u'REQUEST_LOG_ID': u'05e41c3d8cc239c03f0c8d4ccdeb797', u'AUTH_DOMAIN': u'gmail.com', u'SERVER_PORT': u'8080', u'CURRENT_MODULE_ID': u'default', u'CURRENT_VERSION_ID': u'None.918090198333333552', u'SCRIPT_FILENAME': u'C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\google\\appengine\\tools\\devappserver2\\php\\runtime\\setup.php', u'USER_ORGANIZATION': u'', u'HTTP_HOST': u'localhost:8080', u'HTTPS': u'off', u'USER_ID': u'', u'REAL_SCRIPT_FILENAME': u'C:\\Users\\FOO\\source\\repos\\appengine-php55\\403.php', u'USER_EMAIL': u'', u'REQUEST_URI': u'/_ah/warmup', u'STDERR_LOG_LEVEL': u'1', u'DATACENTER': u'us1', u'APPLICATION_ID': u'dev~None', u'TEMP': u'C:\\Users\\FOO\\AppData\\Local\\Temp', u'HTTP_X_APPENGINE_COUNTRY': u'ZZ', u'USER_NICKNAME': u'', u'REMOTE_API_PORT': u'52479', u'HTTP_CONTENT_LENGTH': u'0', u'REMOTE_API_HOST': u'localhost', u'REMOTE_REQUEST_ID': u'eRGVBeUVhC'}

という値。「やっぱりどこにも日本語なんか含まれてないじゃん」ということで行き詰りかけたのですが、よくよく見ると

「なんで全部の文字列の前に"u"が付いてるの?」

というわけで、こんなパッチを当ててみました。

--- gcloud/current/platform/google_appengine/google/appengine/tools/devappserver2/php/runtime/runtime.py.orig    Tue Jan  1 17:00:00 1980
+++ gcloud/current/platform/google_appengine/google/appengine/tools/devappserver2/php/runtime/runtime.py  Sat Apr  9 21:35:32 2022
@@ -259,6 +259,10 @@
       user_environ[http_runtime_constants.REQUEST_TYPE_HEADER] = request_type
 
     try:
+      # XXX: fix to convert all env keys and values to str.
+      # https://stackoverflow.com/a/38476472
+      user_environ = { str(key):str(value) for key,value in user_environ.items() }
+
       # stderr is not captured here so that it propagates to the parent process
       # and gets printed out to consle.
       p = safe_subprocess.start_process(

これを当てて再度user_environ変数を確認すると

{'TMP': 'C:\\Users\\FOO\\AppData\\Local\\Temp', 'REQUEST_ID_HASH': '7CBB4402', 'REDIRECT_STATUS': '1', 'SERVER_SOFTWARE': 'Development/2.0', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/_ah/warmup', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': '', 'SYSTEMROOT': 'C:\\WINDOWS', 'DEFAULT_VERSION_HOSTNAME': 'localhost:8080', 'USER_IS_ADMIN': '0', 'CONTENT_LENGTH': '0', 'APPENGINE_RUNTIME': 'php', 'TZ': 'UTC', 'APPLICATION_ROOT': 'C:\\Users\\FOO\\source\\repos\\appengine-php55', 'SERVER_NAME': 'localhost', 'REMOTE_ADDR': '0.1.0.3', 'INSTANCE_ID': 'a3f67df098d059011b5dafbdc3d41e4aacc2', 'REQUEST_LOG_ID': '05e41c3d8cc239c03f0c8d4ccdeb797', 'AUTH_DOMAIN': 'gmail.com', 'SERVER_PORT': '8080', 'CURRENT_MODULE_ID': 'default', 'CURRENT_VERSION_ID': 'None.918090198333333552', 'SCRIPT_FILENAME': 'C:\\Users\\FOO\\scoop\\apps\\gcloud\\current\\platform\\google_appengine\\google\\appengine\\tools\\devappserver2\\php\\runtime\\setup.php', 'USER_ORGANIZATION': '', 'HTTP_HOST': 'localhost:8080', 'HTTPS': 'off', 'USER_ID': '', 'REAL_SCRIPT_FILENAME': 'C:\\Users\\FOO\\source\\repos\\appengine-php55\\403.php', 'USER_EMAIL': '', 'REQUEST_URI': '/_ah/warmup', 'STDERR_LOG_LEVEL': '1', 'DATACENTER': 'us1', 'APPLICATION_ID': 'dev~None', 'TEMP': 'C:\\Users\\FOO\\AppData\\Local\\Temp', 'HTTP_X_APPENGINE_COUNTRY': 'ZZ', 'USER_NICKNAME': '', 'REMOTE_API_PORT': '52479', 'HTTP_CONTENT_LENGTH': '0', 'REMOTE_API_HOST': 'localhost', 'REMOTE_REQUEST_ID': 'eRGVBeUVhC'}

と無事に"u"が外れ、dev_appserver.pyも正常起動するようになりました。めでたし、めでたし。

本来のPHPアプリの修正より、こっちのほうがはるかに時間がかかった...

Vue.js Tokyo v-meetup #5参加レポート

2017/10/4に開催されたVue.js Tokyo v-meetup #5 - connpassに参加してきました。
まずはスタッフの皆様お疲れさまでした。最高の誉め言葉としての「滞りなく」を贈りたいと思います。

その競争率の高さゆえ参加機会に恵まれなかったv-meetupですが、今回「ブログ書く枠」がたまたま空いたので参加できることに。以下はそのレポートとなります。
なお、当日のツイートは@kazuponさんが既に昨日のVue.js Tokyo v-meetup #5のTweetまとめました!を作ってくれていますので、そちらも合わせて参照ください。

今回は発表の多くがNuxt.jsがらみでしたが、ほぼ満員(一般参加枠80名)の会場で「Nuxtで作っている人?」という問いかけに手を挙げた人は10名程度。
自分もNuxt.jsは

  • Next.jsのVue版
  • SPA/SSR案件で使うと便利

ぐらいの知識しかなかったのですが、一連の発表を聞いて「次もしSPA案件があったら使ってみよう」という気になりました。

現場枠1: Nuxt.jsで業務システムを作った話@wakame_isono_

https://speakerdeck.com/wakameisono/nuxt-dot-jsdeye-wu-sisutemuwozuo-tutahua

VB.NET開発者からフロントエンドエンジニアになった自らを「WEB初心者」と自称する@wakame_isono_さんが、Nuxt.jsでの業務システム(ECサイトのバックヤード)開発でのノウハウを発表。
「業務システムあるある」での、

  • 典型的な検索画面(上:検索条件入力、下:検索結果リスト表示)のMixinによる共通化
  • 多数の入力項目のあるForm画面のVuex対応

など、派手さはないものの実体験に即した発表内容でした。
業務システムの開発では画面単位に担当者を割り当てるという方法で開発をスケールすることが多く、いきおいコピペで済ませてしまうことになりがちですが、それにキチンと向き合って共通化していこうとする姿勢は好感が持てました。

現場枠2: Vuejsを使ったシステムを一年半間運用した話 & Vueのバックエンドとして Firebaseを使った話@jkang

前半:(資料未公開)
後半:https://speakerdeck.com/urverispecial/vuefalsebatukuendotosite-firebasewoshi-tutahua

株式会社ABEJAのお二人によるリレー形式での発表。
前半は昨年3月にVue.js v1からスタートし、途中でv2へのマイグレーションを経て現在に至るまでのシステム開発・運用の経験談
学習コストの低さからVue.jsを採用し、状態管理・イベントの複雑化が問題になったあとからVuexの導入という、まさに"Progressive Framework"を地で行くような話でした。
v2へのマイグレーションMigration Helperでの指摘はごくわずかで大きな設計変更は無し、そして「何もしなくてもパフォーマンスが上がった」とのこと。
「Vue.jsは開発の立ち上がりがスムースで、あとから参加するメンバーのキャッチアップも早い」は本当に御意。

後半はSPAのバックエンドにはFirebaseがいいよ、という話。
AWSとの対比でとかく「高性能だけどとっつきにくい」イメージのある(個人の感想ですw)GCPですが、Firebase(BaaS)は一度まな板に載せてみるのはアリだと思います。
「Cloud Functionはデバッグが出来ないのが欠点でしたが、つい先週できるようになりました」は知らなかったのでありがたいです。
残り時間の都合で「データはフラット化!」の詳細が聞けなかったのが残念。FirebaseはJSON DBなのでRDBでのそれとはまた違ったノウハウや勘所が求められるなぁと思っている今日この頃なので。

現場枠3: Nuxt.js本格導入で遠回りしないためのTips@devneko

https://speakerdeck.com/dotneet/nuxt-dot-jsben-ge-dao-ru-deyuan-hui-risinaitamefalsetips

記事を読ませる系サイトとユーザ登録有りのそこそこな規模のサイトの2案件で実際にNuxt.jsを使用している@devnekoさんが、Nuxt.js導入でハマらないためのノウハウを伝授。
「まずはドキュメントを読みましょう」という基礎的な話から「何も考えないとJavascriptが肥大化するのでサイズを小さくするには」まで実践感ありありの内容でした。
「Nuxt.jsは本当に簡単にSSRできる。解決できない致命的な問題は発生していない」とのことでしたが、そのためにはnuxt.config.jsの「研究」が大事なようです。
なお、本人曰く「黒魔術」=AsyncDataの困りごとを解決するヘルパー"nuxtend"はこちらで公開されています。

LT1: Nuxt.jsが掲げる"Universal Vue.js Applications"とは何者なのか@_ishkawa

https://speakerdeck.com/ishkawa/nuxt-dot-jsgajie-geru-universal-vue-dot-js-applications-tohahe-zhe-ka

「SWIFT実践入門」の著者でもある@_ishkawaさんによる発表。
自分を含めこれからNuxt.jsを始めようとする人がとっかかりにするにはベストの内容だと思いました。

  • Nuxt.jsは何をするのか?→レンダリングサーバで実行されるコードを自動生成する
  • レンダリングサーバで問題が起きたらどうする?→カスタマイズで対応
  • Nativeアプリ開発者がフロントエンドで一番つらいことは?→HTMLタグが分からない(笑)

LT2: NuxtなしでVue App作る時に乗り越えるべきnの壁(n=5)@vwxyutarooo

https://www.slideshare.net/vwxyutarooo/nuxt-vue-app-5

マンガZEROを作った@vwxyutaroooさんがNuxt.js無しでSSRサイトを作った「恨み辛み妬み」の発表。
vue-hackernews 2.0を参考に一生懸命頑張ったのですが、

みたいな感じだったそうで。
HackerNewsの404ページが「真っ白」というのは言われるまで気づきませんでしたが、これもNuxtなら簡単に対応できるとのこと。

mocha-chrome + chaiでJavascriptの単体テストコードを書く(3)

非同期処理のテストコードは以下の通りです。
Javascriptで"非同期"といえば"Ajax"でしょうから、以下のサンプルはlivedoorお天気Webサービス からAjaxJSONデータをGETしその妥当性を検証するテストコードを実装してみました。
livedoorお天気WebサービスJSONP未対応なのでJSON2JSONPサービスを併用)

<html><head>
    <meta charset="UTF-8">
    <link href="node_modules/mocha/mocha.css" rel="stylesheet" /> <!-- テスト結果表示用のCSS -->
    <script src="node_modules/mocha/mocha.js"></script> <!-- mochaの読込み -->
    <script src="node_modules/chai/chai.js"></script> <!-- chaiの読込み -->
</head><body>
    <div id="mocha"></div> <!-- テスト結果表示領域の確保 -->

    <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <!-- jQuery -->
    <script> /* テスト対象コード */
    function getWeather(city, callback) {
        $.getJSON(
            'http://json2jsonp.com/?url=http://weather.livedoor.com/forecast/webservice/json/v1%3Fcity='
            + city + '&callback=?',
            function (data) {
                console.log(data);
                callback(data);
            });
    };
    </script>
    <script> /* テストコード */
        // 初期化
        mocha.setup('bdd');
        expect = chai.expect;

        // ここにテストコードを書く
        describe('getWeather test', function() {
            this.timeout(5 * 1000);    // 非同期タイムアウトを5秒に設定
            
            describe('test1', function() {
                it('should return "東京" when getCity("130010", )', function(done) {    // done指定=非同期テスト
                    getWeather('130010', function(res) {
                        expect(res.location.city).to.be.equal('東京');
                        done();    //非同期完了待ち
                  });
                });
            });
            describe('test2 - わざとNG', function() {
                it('should return "秋田" when getCity("020010", )', function(done) {
                    getWeather('020010', function(res) {
                        expect(res.location.city).to.be.equal('秋田');
                        done();
                  });
                });
            });
        });

        // 単体テストの実行
        mocha.run();
    </script>
</body></html>

テストコードで前回と違うところは、

  • it()の第2引数であるコールバック関数に引数"done"が追加されている
  • テスト終了時にdone()を実行する

の2点です。

                it('should return "東京" when getCity("130010", )', function(done) {    // done指定=非同期テスト
                    getWeather('130010', function(res) {
                        expect(res.location.city).to.be.equal('東京');
                        done();    //非同期完了待ち
                });

この2点を追加するだけで非同期処理のテストを正常に行えるようになります。
逆にいうと、これを忘れるとテスト結果が正常に得られない(本当はNGなのにOKと判定されることがある)のに注意してください。

また、非同期処理のタイムアウト

            this.timeout(5 * 1000);    // 非同期タイムアウトを5秒に設定

という感じで指定します。なお、

  • 規定値は2秒
  • テスト毎に異なるタイムアウトを指定することも可能

です。詳細はTimeouts - Mochaを参照してください。

mocha-chrome + chaiでJavascriptの単体テストコードを書く(2)

前回のサンプルコードの解説です。

まず、mocha + chaiを使うのでそれをテスト用のHTMLに組込む処理が必要です。具体的には

  • mocha.css及びmocha.jsの組込み
  • chai.jsの組込み

が必要で、それらは普通のcss及びjs同様HTMLの<link href="…" rel="stylesheet">及び<script src="…">を使用します。
これらについては特に補足は不要ですね。サンプルではtest.htmlと同じフォルダにnode_modules/がある前提ですので必要に応じてパスの変更ぐらいかと思われます。

また、テスト対象コードは今回サンプルなのでHTML中に埋込みましたが、実務上は外部のJSファイルを読込むことになろうかと思います。
<script>でもrequireでも方法は問いませんのでテスト用のHTMLからグローバル関数としてアクセス可能になるようにしてください。

なお、HTMLのBODY中に記載している

    <div id="mocha"></div> <!-- テスト結果表示領域の確保 -->

の部分は(ヘッドレスでない普通の)ブラウザ用で、これを記載した場所にテスト結果が表示されます。
idは必ず"mocha"である必要があるのと、別途、初期化処理として"mocha.setup()"を実行する必要があります。
特に後者はmocha-chromeの公式サイトのサンプルには記載がありませんので注意してください。

テストコードは

    <script> /* テストコード */
        // 初期化
        mocha.setup('bdd');
        expect = chai.expect;

        // ここにテストコードを書く
...
        // 単体テストの実行
        mocha.run();
    </script>

の部分です。初期化と単体テストの実行の部分は「お約束」ということでこのまま記載してください。

ここまでがどんなテストの場合でも固定的に記載する内容で、いよいよテストコード本体の説明となります。

        // ここにテストコードを書く
        describe('add test', function() {
            it('should return 3 when arg1 is 1 and arg2 is 2', function() {
                expect(add(1,2)).to.be.equal(3);
            });
        });

テストコード本体の構文は大きく分けて以下の3つとなります。

  • describe()
  • it()
  • expect()

describe()はテストの目的を記載するものです。第1引数の文字列はテストを識別するための表示用で、テスト結果には影響しません。
またdescribe()はネストさせることができ、これはテストの分類に使うと便利です。
it()は1つのテスト範囲を示すものです。この第1引数の文字列も表示用で、テスト結果には影響しません。

テスト結果に影響するのはexpect()になります。

expect(実行結果).to.be.equal(期待値);

が基本形です。
このexpect()を提供しているのがchaiで、逆にいうとchai以外のassertionライブラリを使う場合はここの書き方が変わります。
また、".to.be.equal"「期待値と等しい」以外のテストを行いたい場合はchaiAPI Reference(BDD)
Expect / Should - Chai
を参照してください。例外が発生したかをチェックできる".throw"等も用意されています。

it()の中に書けるのはexpect()だけではありません。必要に応じて、

it('…', function() {
    前処理;
    expect(実行結果).to.be.equal(期待値);
    後処理;
});

という書き方もできます。

ここまでの説明で一般の関数に対する単体テストコードは書けるものと思いますが、非同期処理については少し補足が必要です。
次回(3)ではそれを説明します。

mocha-chrome + chaiでJavascriptの単体テストコードを書く(1)

ソフトウェアの品質アップには自動テスト可能(CI対応)な単体テストフレームワークを使うのが近道だと知っていても、

ということがあると思います。
そこでJavascriptのCI可能な単体テストコードをmocha-chrome + chaiで書く方法を解説します。

以前はこの用途としてmocha + chai + PhantomJSが使われていました。それぞれの役割は、

です。つまり、

  • mochaに対応した単体テストコードを
  • assertionとしてchaiを用いて記述し
  • それをPhantomJSでも実行可能なようにする

ことになります。

しかしChromeブラウザのヘッドレスモード対応を受け、PhantomJSが開発を停止
mocha-chromeはPhantomJSの代わりにChromeのヘッドレスモードを使うことで置き換えを図ったものです。
使い方はmocha-phantomjsと非常に似ていますが、まだ開発が始まったばかりということで公式サイトの解説はかゆいところに手が届いておらず十分とは言えません。
ということで、最初はmocha-chrome + chaiを用いて簡単な単体テストコードを書くところから説明します。

用意するもの

公式サイトでは明文化されていませんが、mocha-chromeはnode.js v7.6以上が必要です。
[mocha-chrome/packages.json]

  "engines": {
    "node": ">= 7.6.0"
  },

なおnode.jsのサポートポリシーでは奇数バージョンにはLTSが存在しないので、今ならv8の使用をお勧めします。

またmocha-chromeのインストールとは別にmocha単体とchaiのインストールも必要となります。
いずれもnpmで簡単にインストールできますが、以下のpackages.jsonを使えば"npm install"で一括インストールが可能です。
[packages.json]

{
  "name": "mocha-chrome-chai-test",
  "version": "1.0.0",
  "description": "test environment for mocha-chrome with chai",
  "dependencies": {},
  "devDependencies": {
    "chai": "^4.1.2",
    "mocha": "^3.5.0",
    "mocha-chrome": "^0.1.1"
  },
  "scripts": {
    "test": "mocha-chrome test.html --no-colors"
  }
}

では、いきなりサンプルコードです。解説は次回(2)にて行う予定です。
[test.html]

<!doctype>
<html><head>
    <meta charset="UTF-8">
    <link href="node_modules/mocha/mocha.css" rel="stylesheet" /> <!-- テスト結果表示用のCSS -->
    <script src="node_modules/mocha/mocha.js"></script> <!-- mochaの読込み -->
    <script src="node_modules/chai/chai.js"></script> <!-- chaiの読込み -->
</head><body>
    <div id="mocha"></div> <!-- テスト結果表示領域の確保 -->

    <script> /* テスト対象コード */
        function add(arg1, arg2) {
            return arg1 + arg2;
        }
    </script>
    <script> /* テストコード */
        // 初期化
        mocha.setup('bdd');
        expect = chai.expect;

        // ここにテストコードを書く
        describe('add test', function() {
            it('should return 3 when arg1 is 1 and arg2 is 2', function() {
                expect(add(1,2)).to.be.equal(3);
            });
        });

        // 単体テストの実行
        mocha.run();
    </script>
</body></html>

実行は先ほどのpackage.jsonを使っていれば"npm test"で行えます。

> mocha-chrome test.html --no-colors

  add test
    ✓ should return 3 when arg1 is 1 and arg2 is 2

  1 passing (22ms)

vue-touch-keyboardでカスタムキーボードを作る

前回の続き。
vue-touch-keyboardでカスタムキーボードを作るには、ソースコードlayouts.jsを見ればわかります。
特にシフトキーやCapsキー用のレイアウトが必要ない場合は_metaとdefaultを定義すればOKです。
defaultは表示通りの入力で良い場合は単にその文字、特殊キーは{}で括ったうえで_metaに定義を記載します。

  • key: 入力文字列
  • text: 表示文字列
  • classes: DOMのclass
  • width: DOMのwidth
  • func: 押下時に実行される関数名

funcは任意の関数名が使えるわけではなく、ソースのkeyboard.vueのpropsで定義されている以下のもののみです。
[onClickイベント発生時]

  • accept: 確定
  • cancel: キャンセル
  • next: フォーカス移動

[onChangeイベント発生時]

  • change: 変更

また、func: "backspace"は特殊で、

  • vue-touch-keyboard内部のbackspace関数の実行のみ:外部関数は呼び出せない
  • DOMのbackground-imageとして自動的に"✕"画像が表示

という動作をします。したがってbackspaceキーの表示を変えるにはtextを設定してもダメでCSSでスタイルを上書き定義する必要があります。

.vue-touch-keyboard .keyboard .key.backspace.nobackground {
  background-image: none;
  &:after {
    content: "BS";
  }
}

以上を踏まえて、一般的なテンキーを"tenkey"レイアウトとして再現してみたものがこれです。
比較のためビルトインの"numeric"レイアウトと並べてあります。

      // custom keyboard layouts
      layouts: {
        "tenkey": {
          _meta: {
            "backspace": { func: "backspace", classes: "control nobackground"},
            "accept": { func: "accept", text: "Enter", classes: "control featured"},
            "next": { func: "next", text: "Tab", classes: "control featured"}
          },

          default: [
            "{next} / * {backspace}",
            "1 2 3 -",
            "4 5 6 +",
            "7 8 9 =",
            "0 00 . {accept}"
          ]
        }
      }