Go で hello-world の減量
Go言語ってビルドサイズが大きいですよね。
バイナリサイズを削減する ldflags
や upx
について、どの程度の減量が見込めるのか確認してみます。
ついでに標準ライブラリへの理解を兼ねて hello-world の書き方もいくつかのパターンで比べてみます。
% go version go version go1.15.5 darwin/amd64
fmt
一般的な hello-world の書き方です。
func main() { fmt.Print("Hello, world!") }
いつも使っている表現ですが fmt
パッケージで標準出力するのにちょっと違和感を持ってました。
os
func main() { os.Stdout.WriteString("Hello, world!") }
fmt
に比べて、こちらの方が標準出力している感がありますね。
でも fmt
の方が色んな型を渡せて便利なので fmt
を使っちゃいますね。
syscall
func main() { syscall.Write(syscall.Stdout, []byte("Hello, world!")) }
使ったことはありませんでしたが、意外とシンプルに書けました。
syscall.Stdout
は uintptr
型の変数で os.File
型の os.Stdout
とは別物です。
バイナリサイズ比較
ビルドしてサイズを比べてみます。
- ldflags を指定して
go build -ldflags="-s -w"
するパターン - ビルドしたバイナリに
upx -9
を適用したパターン - その両方を適用したパターン
を比較したところ以下のようになりました。
default | ldflags | upx | ldflags+upx | |
---|---|---|---|---|
fmt |
2.0M | 1.6M | 1.1M | 620K |
os |
1.5M | 1.1M | 840K | 448K |
syscall |
1.2M | 915K | 708K | 376K |
syscall
が一番小さくなりましたが、それでも何も工夫しなければ1MBを超えています。
御膳立て部分のコードが大きそうですね。
ldflags と upx はそれぞれバイナリサイズを半分程度まで削減できるみたいです。 両方使うと元の大きさの 1/3 程度の小ささになりました。 まだ何をやっているツールか把握していませんが、upx すごいですね。
本当は各ライブラリの表現を部分的に引っ張ってきてより低級な表現に展開する、みたいなことをしたかったのですが、internal
なパッケージをユーザ側で利用することができず諦めました。
おまけ
ディレクトリを跨いだファイルサイズの比較に tree -h が便利
tree -h
でファイルサイズを表示しつつツリー表示してくれます。
--du
オプションをつけると du
的にフォルダの合計サイズも表示してくれます。
% tree --du -h . ├── [ 303] build.sh ├── [5.3M] fmt │ ├── [2.0M] hello-world │ ├── [1.6M] hello-world.ldflags │ ├── [620K] hello-world.ldflags.upx │ ├── [1.1M] hello-world.upx │ └── [ 176] main.go ├── [3.8M] os │ ├── [1.5M] hello-world │ ├── [1.1M] hello-world.ldflags │ ├── [448K] hello-world.ldflags.upx │ ├── [840K] hello-world.upx │ └── [ 83] main.go └── [3.2M] syscall ├── [1.2M] hello-world ├── [915K] hello-world.ldflags ├── [376K] hello-world.ldflags.upx ├── [708K] hello-world.upx └── [ 104] main.go 12M used in 3 directories, 16 files
続き: Karabiner-Elements と Swift で background のアプリに特定のキーストロークを送る
ターミナルやエディタでコーディングしつつ、参考書を表示しているKindleのページ送りをしたかったのが動機です。 表題で言っている background のアプリとは最前面(foremost)ではないウィンドウで動作しているアプリを指しています。
上の記事で試したのは、指定したアプリ向けにキーストロークを送る AppleScript を Karabiner-Elements で特定のキーにより実行する、というものでした。しかし AppleScript の限界なのか background のアプリにキーストロークを送ることはできず、実際には一瞬ターゲットのアプリを foremost に持ってきてキーを送り foremost を元のアプリに戻すという処理しか書けませんでした。
Swift を使えばターゲットのアプリを activate することなく特定のアプリにキーストロークを送れるようだったので、使用感向上のため実装してみました。
Swift スクリプトの用意
アプリにキーストロークを送る send-key-to-app.swift
を用意します。
AppleScript よりこちらの方が好きです。
import Foundation import AppKit let args = CommandLine.arguments if args.count != 3 { print("\(args[0]) [APP_NAME] [KEY_CODE]") print("APP_NAME: should be an executable file name(not a path)") print("KEY_CODE: a number such as 123(LEFT), 124(RIGHT), ...") exit(1) } let appName = args[1] let keyCode = CGKeyCode(args[2]) ?? 0 let src = CGEventSource(stateID: CGEventSourceStateID.hidSystemState) func sendKeyStroke(pid: pid_t, keyCode: CGKeyCode) { let keyUp = CGEvent(keyboardEventSource: src, virtualKey: keyCode, keyDown: true) keyUp?.postToPid(pid) let keyDown = CGEvent(keyboardEventSource: src, virtualKey: keyCode, keyDown: false) keyDown?.postToPid(pid) } func getPidByName(executableFileName: String) -> Optional<pid_t> { let apps = NSWorkspace.shared.runningApplications for a in apps { print(a.bundleIdentifier) if a.executableURL?.lastPathComponent == executableFileName { return a.processIdentifier } } return nil } if let pid = getPidByName(executableFileName: appName) { sendKeyStroke(pid: pid, keyCode: keyCode) } else { print("executable '\(appName)' not found in user processes") }
始めて Swift を書きましたが、Optional
型と if let
のコードが気持ちいいですね。
値を wrap した Optional の扱い方はちょっと Rust っぽいです。
Karabiner-Elements に設定する前に以下のように動作確認をしておきます。
swift send-key-to-app.swift Kindle 124
これで background で動いている Kindle のページが進めば成功です。 AppleScript のものと比べるとウィンドウ切り替えのチカチカ感が減ってとても快適です。
Karabiner-Elements の設定
冒頭で紹介した以前の記事と shell_command
以外は同じです。
適当に Command + 1
でページ送り、Command + 2
でページバックするようにしています。
{ "profiles": [ { "complex_modifications": { "rules": [ { "description": "Kindleのページ移動", "manipulators": [ { "from": { "key_code": "1", "modifiers": { "mandatory": ["command"], "optional": ["any"] } }, "to": [{"shell_command": "swift /path/to/send-key-to-app.swift Kindle 123"}], "type": "basic" }, { "from": { "key_code": "2", "modifiers": { "mandatory": ["command"], "optional": ["any"] } }, "to": [{"shell_command": "swift /path/to/send-key-to-app.swift Kindle 124"}], "type": "basic" } ] },
更新したら「Restart Karabiner-Elements」をすれば早速反映されます。
この使い方でもいいのですが、現状ページ送りごとに毎回 swift インタプリターが実行されているので、気になる場合はビルドすると良さそうですね。
余裕があったので Swift をビルドする
swift package build --type executable
でコマンド用のプロジェクトが簡単に作成できたので作っておきました。
https://github.com/asa-taka/send-key-to-app
心なしかページ遷移を繰り返してもiMacのファンが鳴らなくなった気がします。
Karabiner-Elements と AppleScript で background のアプリに特定のキーストロークを送る
ターミナルやエディタでコーディングしつつ、参考書を表示しているKindleのページ送りをしたかったのが動機です。 表題で言っている background のアプリとは最前面(foremost)ではないウィンドウで動作しているアプリを指しています。
指定したアプリ向けにキーストロークを送る AppleScript を Karabiner-Elements で特定のキーにより実行することで実現します。 「background のアプリに」と言いつつ実際にやっていることは、一瞬 foremost に持ってきてキーを送って元のアプリに戻す、という処理です。
AppleScript の用意
アプリにキーストロークを送る send-key.applescript
を用意します。
AppleScript を書くのは初めてですが自然言語志向すぎて胸焼けしそうですね。
on run argv if (count of argv) < 2 then log "send-key [TARGET_APP] [KEY_CODE]" log "KEY_CODEs: 123(LEFT), 124(RIGHT), ..." error number -1721 end if set targetApp to (item 1 of argv as text) set keyCode to (item 2 of argv as number) set currentApp to (path to frontmost application as text) activate application targetApp tell application "System Events" to key code keyCode activate application currentApp end run
シンプルに書きたい人は変数を埋め込んで以下のようにしてもOKです。
set currentApp to (path to frontmost application as text) activate application "Kindle" tell application "System Events" to key code 123 activate application currentApp
事前に以下のように動作確認をしておきます。osascript
は AppleScript をシェルから起動するためのものらしいです。
osascript send-key.applescript Kindle 124
これで Kindle のページが進んで元のアプリが frontmost になっていれば成功です。
background のアプリに直接キーストロークを送る仕組みが見つからなかったので、 一度ターゲットのアプリを activate してからキーストロークを送り元のアプリを再び activate しています。 なので一瞬ウィンドウがチカチカします。ホームメイド感がありますね。
Karabiner-Elements の設定
Karabiner-Elements に前述のスクリプトを起動するキーの設定をします。
デフォルトだと ~/.config/karabiner
みたいです。
以下のような感じで profiles[].complex_modifications.rules
に設定を追加します。
全体は長いので、今回関係のないフィールドは省略しています。
設定の肝は from
のキーに対して shell_command
で任意のコマンドを実行可能な点です。
これができるだけでも Karabiner の用途はかなり広がりますね。
ここでは適当に Command + 1
でページ送り、Command + 2
でページバックするようにしています。
{ "profiles": [ { "complex_modifications": { "rules": [ { "description": "Kindleのページ移動", "manipulators": [ { "from": { "key_code": "1", "modifiers": { "mandatory": ["command"], "optional": ["any"] } }, "to": [{"shell_command": "osascript /path/to/send-key.applescript Kindle 123"}], "type": "basic" }, { "from": { "key_code": "2", "modifiers": { "mandatory": ["command"], "optional": ["any"] } }, "to": [{"shell_command": "osascript /path/to/send-key.applescript Kindle 124"}], "type": "basic" } ] },
設定ファイルの構造については Karabiner-Element のドキュメント をご覧ください。
更新したら「Restart Karabiner-Elements」をすれば早速反映されます。
サボったところ
Karabiner-Element の設定をいい感じにしたらいけるかもしれませんが、調べるのが面倒で放置している箇所です。
快適にコードを書きたいというのが当初の目的なので、時間はコーディングに割くべきと、多少の不具合には目をつぶっています(正当化)。
Go の struct と interface で Embedding
Effective Go の Embedding の内容を試してみます。
Go では embedding を利用して継承のようなことができますが、struct と interface の違いが今ひとつ理解できていなかったため、実際にコードを書いてコンパイラに怒られながら、どういう違いがあるのか試していきたいと思います。
- 確認方法
- interface を interface に埋め込む
- struct を struct に埋め込む
- struct を interface に埋め込む(不可)
- interface を struct に埋め込む
確認方法
こんな感じで embedding を利用して BaseMethod
と SuperMethod
を実装するパターンを色々見ていきます。
type baseInterface interface { BaseMethod() string } type superInterface interface { baseInterface SuperMethod() string }
実装の確認用兼、interface を満たしているかの確認用の関数を用意します。
func printSuper(name string, v superInterface) { fmt.Println(name+".BaseMethod ->", v.BaseMethod()) fmt.Println(name+".SuperMethod ->", v.SuperMethod()) }
interface を interface に埋め込む
type baseInterface interface { BaseMethod() string } // 一応 baseInterface の方の実装も作っておく type implOfBaseInterface struct{} func (implOfBaseInterface) BaseMethod() string { return "implOfBaseInterface.BaseMethod" } type superInterface interface { baseInterface SuperMethod() string } type implOfSuperInterface struct{} // interface を interface に埋め込んだ場合 BaseMethod の定義は必要 func (i implOfSuperInterface) BaseMethod() string { return "implOfSuperInterface.BaseMethod" } func (implOfSuperInterface) SuperMethod() string { return "implOfSuperInterface.SuperMethod" }
impleOfSuperInterface
側でも BaseMethod
の実装が必要になります。
実装を参照する先も無いので当たり前と言えば当たり前ですね。
func main() { printSuper("implOfSuperInterface", implOfSuperInterface{}) }
してみると
implOfSuperInterface.BaseMethod -> implOfSuperInterface.BaseMethod implOfSuperInterface.SuperMethod -> implOfSuperInterface.SuperMethod
これも impleOfSuperInterface
に実装したものがそのまま呼ばれて、当たり前の結果ですね。
このパターンの埋め込みをするメリットとしては、interface 側のメソッドの定義を省略できることくらいでしょうか。
struct を struct に埋め込む
type baseStruct struct{} func (b baseStruct) BaseMethod() string { return "baseStruct.BaseMethod" } type superStruct struct { baseStruct } // この場合 BaseMethod は省略できるが実装してもいい // func (s superStruct) BaseMethod() string { // // 明示的に baseStruct.BaseMethod を呼ぶこともできるし(あまり意味なさそう) // return s.baseStruct.BaseMethod() // // 他の処理を定義してもいい // return "superStruct.BaseMethod" // } func (s superStruct) SuperMethod() string { return "superStruct.SuperMethod" }
struct を struct に埋め込んだ場合は superStruct.BaseMethod
の実装を省略できるみたいです。
func main() { printSuper("superStruct", superStruct{}) }
してみると
superStruct.BaseMethod -> baseStruct.BaseMethod superStruct.SuperMethod -> superStruct.SuperMethod
superStruct.BaseMethod
で baseStruct.BaseMethod
が呼ばれていますね。
struct を interface に埋め込む(不可)
type structEmbeddedInterface interface { baseStruct SuperMethod() } type implOfStructEmbeddedInterface struct{} func (implOfStructEmbeddedInterface) SuperMethod() { fmt.Println("structEmbeddedInterface.SuperMethod") }
これは不可能なパターンで、コンパイル時に interface contains embedded non-interface baseStruct
と怒られます。
interface を struct に埋め込む
type interfaceEmbeddedStruct struct { baseInterface } func (interfaceEmbeddedStruct) SuperMethod() string { return "interfaceEmbeddedStruct.SuperMethod" }
この場合は interfaceEmbeddedStruct.BaseMethod
の実装をしなくてもコンパイルは通ります。しかし、ランタイムで panic: runtime error: invalid memory address or nil pointer dereference
というエラーになります。一番タチが悪いですね。
このパターンの活用事例は悩ましいですが、考察されているページがあったので時間のある時に読みたいと思います。 https://horizoon.jp/post/2019/03/16/go_embedded_interface/
今回触って改めて、このGo言語における embedding は手を動かさないと理解できないなと思いました。単純にパターンが多いので網羅しづらいのと、網羅されても今度は長大になるため読み解く時間がないので、手元で試した方が速いなと…
macOS のキーボード入力が重複するので Wacom 系のソフトウェアをアンインストールした
Mac を使いながら数年間悩まされ続けてきたキーボード入力系の不具合がある。
症状としては上の記事と同じで「application 」と打つと「appapplication」のように入力される。物理的なチャタリングとは異なりワンテンポ遅れてまとまったキーストロークが繰り返されるという、どうにもソフトウェア領域らしい不具合が発生し続けていた。記事で紹介されていた各種リセットを行ってみて、改善したかに思えた瞬間もあったが程なくして再発する。ただ記事を見つけて、同じ問題を抱えている人がいるのだと少し安心できた。
そんなこともあり日本語の記事も他に見つからず、英語で調べるの面倒だなと思いながら、たまにイライラする程度だしと数年間放置し続けていたのだが、いざ調べてみるとあっさり解決方法が見つかった。
- homebrew - Duplicate keyboard input after upgrading to macOS Catalina - Ask Different
- https://discussions.apple.com/thread/250733138?answerId=251408993022
Wacom 系のソフトウェアが原因らしい。アンインストールすれば症状が改善されたとの報告がされている。
Kudos to rakdavid. 本当にありがとう。
私もタブレットユーザなので、いくつかインストールしていた Wacom 系のドライバやついでにユーティリティを全てアンインストールしたところ、今のところ本当に症状が解消されている。どうやって気づいたんだこんなもの。もう iMac 買い換えるかとも思い始めていた。そして同様に Wacom をインストールして問題が再発し絶望する未来もありえた。ここに私は正しく、救われたのだった。
早く調べておけばよかった。過去数年間の矢印戻りバックスペース連打行末戻りのルーチンと、いつ重複が発生するかという不信感は確実に私の処理能力に対するオーバーヘッドになっていた。数十行のコードを完成させるまでに10回は発生していたように思う。そして変数名が異なりメソッド名が異なりコンパイラに怒られたりランタイムエラーになったりするのだ。それで数十秒のロスだ。トライアンドエラーを繰り返すコーディング作業の中でこのロスは大きすぎるし邪魔すぎる。
人間が1日に行える判断の回数には上限があるという。この不具合が奪っていったものについて、私はもうあまり考えたくはない。
Material UI の AppBar と Drawer を自分好みに組み合わせる
半年ほどかけてようやく Material UI にも慣れてきました。AppBar と Drawer の組み合わせは昨今のアプリでは定番となっていますが、レスポンシブに Drawer の表示を切り替えようとすると、AppBar との色使いの兼ね合いで迷います。
- モバイルなどの狭いスクリーン
- Drawer は AppBar のボタンをクリックした時に表示
- デスクトップなどの広いスクリーン
- Drawer は常にページ横などに表示
というのが割とよくあるパターンですが、この時両方にテーマカラーなどの濃い色をつけてしまうと、広いスクリーンの場合に上と横の2辺に濃い色が来てしまいます。
個人的な好みですが、この 2辺以上の濃い色のバーに囲まれたコンテンツ からは窮屈さを感じてしまい、あまり好きではありません。
今回作成するのはスクリーン幅によって AppBar の色が変わるようにして Drawer と組み合わせたものです。
ついでに公式に useScrollTrigger
のデモがあったので、それも利用してなんかいい感じに表示されてくれる AppBar をカスタムしていきます。
動作するコードは github に置いてあります。
ドロワー用のダークテーマを用意する
ドロワー用のテーマをサクッと定義しておきます。今回は単純なコンテンツなので部分的に palette
を定義するにと止めています。手抜きです。
import { createMuiTheme } from '@material-ui/core' import { cyan } from '@material-ui/core/colors' import { darken, lighten } from '@material-ui/core/styles/colorManipulator' export default createMuiTheme({ palette: { primary: { light: lighten(cyan[700], 0.5), main: darken(cyan[700], 0.3), dark: darken(cyan[700], 0.5), contrastText: '#fff', }, background: { default: darken(cyan[700], 0.5), paper: darken(cyan[700], 0.5), }, type: 'dark', } })
重要なのは type: 'dark'
です。これが指定されていると文字色が白くなったりするなど、各コンポーネントがダークテーマを前提とした色使いになります。
今回はこのドロワー用のダークテーマと、コンテンツ領域用の Material UI デフォルトのテーマを利用します。ルートの App
コンポーネントはこんな感じです。MyAppBar
が今回作成するメインのコンポーネントです。
const theme = createMuiTheme() const App: React.FC = () => { return ( <div className="App"> <CssBaseline /> <MuiThemeProvider theme={theme}> <MyAppBar /> </MuiThemeProvider> </div> ) }
MyAppBar
実際に組んだ AppBar が以下のようなものです。
MyContent
と MyDrawerContent
は適当な内容を定義しているだけなので適当に読み飛ばしてください。
HideOnScroll
は useScrollTrigger
を利用した例として 公式のサンプル として書かれているものです。ありがたいですね。同じ公式のサンプルからスクロール時に AppBar
に box-shadow
を適用する効果も付け加えてみました。
import React from 'react' import { Drawer, AppBar, Toolbar, IconButton, Typography, makeStyles, useMediaQuery, useScrollTrigger, Theme, } from '@material-ui/core' import { ThemeProvider } from '@material-ui/styles' import * as Icons from '@material-ui/icons' import drawerTheme from '../themes/drawerTheme' import { useBoolean } from '../utils' import MyContents from './MyContents' import MyDrawerContent from './MyDrawerContent' import HideOnScroll from './HideOnScroll' const useStyles = makeStyles( ({ palette, spacing: sp, breakpoints: bp, mixins }) => ({ root: { backgroundColor: palette.background.default, minHeight: '100vh', }, appBar: { [bp.up('md')]: { width: `calc(100vw - 240px)`, }, transition: 'background .2s, color .2s', }, drawerPaper: { width: 240, }, menuButton: { marginRight: sp(2), }, contentWrapper: { [bp.up('md')]: { paddingLeft: 240, }, }, appBarSpacer: mixins.toolbar, content: { padding: sp(3), }, }), ) export default function MyAppBar() { const cls = useStyles() const [mobileOpen, openDrawer, closeDrawer] = useBoolean() // useState の boolean 版 // `useMediaQuery` でモバイルかどうか判定 (*˘꒳˘*) らくちん const isMobile = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')) // おまけ: `useScrollTrigger` でスクロール時に `AppBar` に `box-shadow` を適用する // おしゃれ (*˘꒳˘*) const elevationTrigger = useScrollTrigger({ disableHysteresis: true, threshold: 0, }) // ドロワーの中身を適当に const drawerContent = <MyDrawerContent sets={3} length={12} /> return ( <div className={cls.root}> <ThemeProvider theme={drawerTheme}> <nav> {isMobile ? ( <Drawer open={mobileOpen} color="primary" onClose={closeDrawer} classes={{ paper: cls.drawerPaper }} variant="temporary" ModalProps={{ keepMounted: true }} children={drawerContent} /> ) : ( <Drawer open color="primary" classes={{ paper: cls.drawerPaper }} variant="permanent" children={drawerContent} /> )} </nav> </ThemeProvider> <ThemeProvider theme={originalTheme => (isMobile ? drawerTheme : originalTheme)} > <HideOnScroll> <AppBar className={cls.appBar} color={isMobile ? 'primary' : 'default'} elevation={elevationTrigger ? 4 : 0} > <Toolbar> {isMobile && ( <IconButton className={cls.menuButton} edge="start" color="inherit" onClick={openDrawer} > <Icons.Menu /> </IconButton> )} <Typography variant="h6">AppBar</Typography> </Toolbar> </AppBar> </HideOnScroll> </ThemeProvider> <div className={cls.contentWrapper}> <div className={cls.appBarSpacer} /> <div className={cls.content}> <MyContents length={12} /> </div> </div> </div> ) }
今回の肝としては isMobile
で theme
をスイッチしている部分です。
<ThemeProvider theme={originalTheme => (isMobile ? drawerTheme : originalTheme)}>
といっても特に特別なことをやっているわけではないですね。 こんな感じかな...で実装して実際に動いてよく動くもんだな、と感心したというそんな感じの実験的コードでした。
余談ですが地味に 100vh
がとても便利でした。今までスクリーンの高さを覆う要素を定義するには height: 100%
を親子間でリレーしていかなければならなかったので、入れ込んだ要素でそれを行うのが面倒だったのですが、いいプロパティが増えたものですね。create-react-app
の初期状態のコードを見ていて気づきました。いいコードを入れてくれているものですね。ありがたいです。
Material-UI と Downshift のスナップショットテストで属性値の差分が出るのを解消する
最近 React のテストを書き始めて、Jest の toMatchSnapshot
便利だなと感じながら使っています。
テスト結果をいちいちテストコード内に頑張って組み立てる必要がなくて楽ですし、
出力結果の差分ってコードの変更の結果としてもとても理解しやすいんですよね。
ロジックを追うより出力を見た方が何をしてるか一目瞭然な場合も多いです。
さてこのスナップショットテスト、便利なんですが、時々変な差分が出たり、一つのテストの失敗が別のテストにも波及して見えることがあったりします。class
とか aria-xxxx
とかその辺の属性値が微妙にブレたりするんですよね。JavaScript のライブラリによくあるパターンだと思います。
原因としては、モジュール内にIDを裁判するためのカウンタ変数など、モジュール内 State とも言えるような変数が存在して、テストが一箇所失敗するとカウンタがずれて後続のテストのスナップショットに波及する、と言った感じだと思います。
Issue として見かける問題なので、著名なライブラリであれば、モジュール内変数を操作するためのテスト用ユーティリティが提供されている場合があります。されていない場合はちょっと辛いですね。諦めて差分を甘受するかIssue/PRを送るかの二択になりそうです。
今回は Material UI と Downshift で出くわした属性値のブレと、そのブレを解消した記録です。動作するコードは github に置いておきました。
TL;DL
コードが長くなるので先に要所だけ抜き出しておきます。
Material UI の makeStyles の採番をなくす
クラス名の生成関数をインデックス番号が振られないように新しく定義して Provide してやります。
import { StylesProvider } from '@material-ui/styles' import { GenerateId } from 'jss' const generateTestClassName: GenerateId = (rule, sheet) => { return `${sheet!.options.classNamePrefix}-${rule.key}` } const provideTestStyles = (el: React.ReactElement) => ( <StylesProvider generateClassName={generateTestClassName} children={el} /> ) // `createElement` 時に以下のように使ったり provideTestStyles( <> <MySelectInput items={items} itemToString={itemToString} /> <MyOtherComponent /> </> )
テスト用と割り切って !.
を使っています。
テスト用の Provider をメソッドにするかコンポーネントにするかはお好みでどうぞ。
Downshift の採番をテストごとにリセットする
こちらはテストごとに採番をリセットする方法をとります。
import { resetIdCounter } from 'downshift' beforeEach(resetIdCounter)
以上です。
参考
- https://github.com/downshift-js/downshift#resetidcounter
- SSR 用って書いてあるので、あんまりテストで使ったりはしないのかもしれません...
- https://github.com/mui-org/material-ui/issues/9492#issuecomment-368205258
- そのまま使わせてもらいました :)
順を追って
採番される対象
今回スナップショットで差分が出る場所は以下の通りです。
- Material UI:
makeStyles
で生成したクラス名MySelectInput-root-23
の23
- Downshift:
aria-labelledby="downshift-0-label"
の0
テスト対象のコンポーネント
テスト対象となるコンポーネントです。
よくあるリストから選択するタイプの Material-UI + Downshift な入力コンポーネント MySelectInput
です。
import React from 'react' import Downshift from 'downshift' import { Paper, MenuItem, TextField, makeStyles } from '@material-ui/core' interface Props<Item> { items: Item[] itemToString: (item: Item | null) => string onChange?: (item: Item | null) => void } const useStyles = makeStyles( { root: {}, menu: {}, }, { name: 'MySelectInput' }, ) export default function MySelectInput<Item>(props: Props<Item>) { const { items, ...downshiftProps } = props const classes = useStyles() return ( <Downshift {...downshiftProps}> {({ isOpen, getInputProps, getMenuProps, getItemProps, itemToString, inputValue, highlightedIndex, }) => { const itemsToUse = inputValue ? items.filter(e => itemToString(e).includes(inputValue)) : items return ( <div className={classes.root}> <TextField {...getInputProps()} fullWidth /> {isOpen && ( <div className={classes.menu} {...getMenuProps()}> <Paper open={isOpen} {...getMenuProps()}> {itemsToUse.map((item, index) => ( <MenuItem key={index} children={itemToString(item)} selected={index === highlightedIndex} {...getItemProps({ item, index })} /> ))} </Paper> </div> )} </div> ) }} </Downshift> ) }
そして makeStyles
の採番をずらすために、もう一つコンポーネントが必要だったので、
適当に MyOtherComponent
を定義します。
import React from 'react' import { makeStyles } from '@material-ui/core' const useStyles = makeStyles( { root: {} }, { name: 'MyOtherComponent' }, ) export default function MyOtherComponent() { const classes = useStyles({}) return <div className={classes.root} /> }
テスト
採番に対して何も対策していない元々のテストコードです。 ReactDOM をそのまま利用した素朴なテストです。
import React from 'react' import ReactDOM from 'react-dom' import MySelectInput from './MySelectInput' import MyOtherComponent from './MyOtherComponent' // Common Props // ------------ interface SimpleItem { id: number name: string } const items: SimpleItem[] = [ { id: 1, name: 'item-1' }, { id: 2, name: 'item-2' }, ] const itemToString = (item: SimpleItem | null) => (item ? item.name : '') // Test Container // -------------- let container: HTMLDivElement beforeEach(() => { document.body.innerHTML = '' container = document.createElement('div') document.body.appendChild(container) }) // Tests // ----- describe('MySelectInput with MyOtherComponent', () => { it('renders without crush', async () => { // throw new Error('Something occurred!') ReactDOM.render( <> <MySelectInput items={items} itemToString={itemToString} /> <MyOtherComponent /> </> , container) expect(container).toMatchSnapshot() }) it('renders without crush in reverse order', async () => { ReactDOM.render( <> <MyOtherComponent /> <MySelectInput items={items} itemToString={itemToString} /> </> , container) expect(container).toMatchSnapshot() }) })
ポイントとしては MySelectInput
と MyOtherComponent
の順序を逆にして二回テストを行っているところです。普段はこんなテストはしませんが、採番ズレを再現するために手軽な方法をとっています。
一度このテストを実行してスナップショットを保存してから、
上記のコードの throw new Error('Something occurred!')
のコメントアウトを外すと初めのテストがレンダリング前に失敗になり、二番目のテストのスナップショットに
- aria-labelledby="downshift-1-label" - class="MySelectInput-root-1" + aria-labelledby="downshift-0-label" + class="MySelectInput-root-2"
のような差分がちらほらと見られるはずです。
コンポーネントの初回の評価順が逆になったことによる class
のインデックス番号のズレと、Downshift
の評価回数が減ったことによる aria
の属性値のズレが発生しています。
冒頭に書いた変更を行えばこのズレが解消されるはずです。最後に修正後のテストコード全体を書いておきます。
import React from 'react' import ReactDOM from 'react-dom' import { resetIdCounter } from 'downshift' import { StylesProvider } from '@material-ui/styles' import { GenerateId } from 'jss' import MySelectInput from './MySelectInput' import MyOtherComponent from './MyOtherComponent' // Common Props // ------------ interface SimpleItem { id: number name: string } const items: SimpleItem[] = [ { id: 1, name: 'item-1' }, { id: 2, name: 'item-2' }, ] const itemToString = (item: SimpleItem | null) => (item ? item.name : '') // Test Container // -------------- let container: HTMLDivElement beforeEach(() => { document.body.innerHTML = '' container = document.createElement('div') document.body.appendChild(container) // Reset Downshift module-internal state resetIdCounter() }) // Test Styles // ----------- // Generate class name without rule index const generateTestClassName: GenerateId = (rule, sheet) => { return `${sheet!.options.classNamePrefix}-${rule.key}` } const provideTestStyles = (el: React.ReactElement) => ( <StylesProvider generateClassName={generateTestClassName} children={el} /> ) // Tests // ----- describe('MySelectInput with MyOtherComponent', () => { it('renders without crush', async () => { // throw new Error('Something occurred!') ReactDOM.render(provideTestStyles( <> <MySelectInput items={items} itemToString={itemToString} /> <MyOtherComponent /> </>, ), container) expect(container).toMatchSnapshot() }) it('renders without crush in reverse order', async () => { ReactDOM.render(provideTestStyles( <> <MyOtherComponent /> <MySelectInput items={items} itemToString={itemToString} /> </>, ), container) expect(container).toMatchSnapshot() }) })
こういう風にテストコードはは太っていくんですね。