既存のChrome拡張のbackground pageをevent pageに移行する。

Right-Click to Calendar - Chrome ウェブストア
3.9.0をリリースしました。終了日時検出と24時以上の表記に対応させました。

内部的な変更としては、background pageからevent pageの利用に移行しました。雑に説明すると「拡張機能のためだけに裏でずっとプロセスを生きたままの状態にせず、必要なときにだけプロセスが起動して、用が済んだら終了する」という修正です。普段の使用中に目にする箇所ではないため地味なのですが、私の環境だと拡張1つにつき25,000kb-30,000kbくらいメモリ専有してしまうのでいい加減(やろうと思ってから6年経ってる)対応しました。たしかRight-Click to Calendar作って半年後ぐらいに追加された機能だった気がします。

background pageとevent pageの違いとか、移行時の注意は以下のサイトが本当によくまとまっているので私からは特に何も説明することはないです。公式ドキュメントとあわせてお世話になりました。

コンテキストメニューを設定する場合の注意として、chrome.contextMenus.createchrome.runtime.onInstalled.addListener内で実行しないと、イベントページが生成されるたびにcreateが呼ばれて無限にコンテキストメニューが増えていきます。あとコンテキストメニュー押下時のイベントはchrome.contextMenus.onClicked.addListenerで定義する必要があります。
このあたりサンプルコード見るとわかりやすいです。
chrome/common/extensions/docs/examples/api/contextMenus/event_page/sample.js - chromium/src.git - Git at Google

あと、Right-Click to Calendarは選択中のテキストを予定登録画面に送信する必要があり、今まではそのテキストをbackground pageに記憶させて、予定登録画面からgetBackgroundPageで取得していました。今回はbackground pageが永続化できないため、以下の方法を取りました。chrome.windows.createで任意のオブジェクト渡せればいいのに…。

  1. chrome.windows.createで生成するウインドウ(=予定登録画面)のURLパラメータに、コンテキストメニューの呼び出し元のtab.idを付与
  2. ウインドウ生成後、ウインドウ内で読み込んだスクリプトでURLパラメータ内のtab.idを取得
  3. tab.idに一致するcontent scriptに対して、ウインドウからsendMessageを実施
  4. messageを受け取ったcontent scriptがレスポンスとして選択中のテキストを送信する
  5. ウインドウがテキストを受信する

これだけいろいろやりましたが、Chrome Extensions Manifest V3でbackground pageを廃止してService Workerに移行する計画もあるみたいなので、すぐ移行できそうなアプリではない限り、今からのevent pageへの移行はおすすめできません。もう少し動向を見たほうが良いと思います。
参考:Latest topics > Chrome Extensions Manifest V3とFirefoxアドオンの死(の可能性) - outsider reflex

Right-Click to Calendar 3.8.1をリリースしました。

Right-Click to Calendar - Chrome ウェブストア

  • 正規表現で日付・時刻のいずれも抽出できなかった場合、選択したテキストがタイトルに設定されるようにしました。
  • 選択したテキストがイベント設定画面に送信できない場合がある不具合を修正しました。

不具合のほう、報告は受けたけど自分の環境で全く再現できずに放置してたのですが、やっと再現したので修正しました。事象発生時は、コンテキストメニュー押下時に本来は1回しか発火しないchrome.tabs.sendMessageがなぜか複数回発火するのですが、発火後のレスポンスにテキストを含んでる場合とそうでない場合があるようです。テキストを含んでない場合のレスポンスは破棄するように修正しました。なんでこんな挙動になるかは調査しないとわからないです。デグレってたら教えてください。

2019/06/03追記

本来は1回しか発火しないchrome.tabs.sendMessageがなぜか複数回発火する

"all_frames": trueを設定した場合、コンテキストメニューを呼び出したwindowだけではなく、ページ内に存在するiframe全員がイベントの発火をListenするのが原因でした。完全に仕様勘違いしてましたね…。(content_scriptが全iframeに挿入されるのは理解していたが、全員がイベント発火の通知を受け取るとは思ってなかった。)
Content Scripts - Google Chrome

合わせて、クロスオリジン対応していないページはgetSelection()するとエラーになるので、getSelection()が取得できない場合は素直にコンテキストメニューで選択中のテキストを設定するように修正しました。修正版はリリース済みです。

登場人物が全員プログラミング言語の紺青の拳あらすじ

この記事の目的

紺青の拳がめちゃくちゃに好きなので、コナンを見ない層に今年の映画がどんな話か知ってもらいたかった。
劇場版『名探偵コナン 紺青の拳(フィスト)』

あらすじ

優秀な高校生探偵のSwiftは、幼馴染で今は付き合ってるAnsibleと遊園地に遊びに行って、怪しげな企業(Google)の取引現場を目撃してしまう。
取引現場を見るのに夢中になっていたSwiftは、後ろから来たもうひとりの仲間(Go)に気づかず殴られてしまう。
怪しい毒薬を飲まされたSwiftが目を覚ますと、自慢の型推論ができなくなっており、Objective-Cになっていた。
Objective-CになってしまったSwiftはなんやかんやありながら元の姿に戻るために奮闘している。
そんなSwiftの好敵手の高校生怪盗Kotlinは、めっちゃすごい怪盗Javaおとうさんと、これまためっちゃすごい怪盗淑女Groovyおかあさんの子供である。
Javaが悪の企業(Oracle)に殺されてしまったため、Oracleの野望を打ち砕くため自身も怪盗として日夜活動を続け、不老不死を叶える命の石(CPU)を探している。
(ちなみに、Javaは実は生きてるがKotlinはそれを知らない。)

ある日夜道を歩いていたObjective-Cは、Kotolinにさらわれてしまい、MacOSからOpenShiftにつれてこられてしまう。
OpenShiftでは、実力者であるPerlがZ80を発掘したと話題になっていた。
競プロの大ファンであるPerlは、今度開催する競プロの大会の優勝者にZ80を渡すと公言しており、Kotolinはそれを狙っているという。
KotolinはあろうことかSwiftのふりをしてAnsibleと一緒にOpenShiftに来ていた。
「それなら勝手に自分でどうにかしろ、俺を巻き込む必要はない」「Ansibleにお前がKotlinだとばらす」と息巻くObjective-C。
しかし、Kotlinが言うことには、Objective-CがMacOSに戻るためにはKotlinが持っている移行スクリプトが必要になるという。
ここでKotlinがKasperskyに捕まってしまうと、Objective-CはMacOSに戻れなくなってしまう。「追々事情は話すから」と言うKotlinにしぶしぶ付き合うことになったObjective-C。

そんな折、一行は競プロの大会に出場するという、競プロ400戦無敗、アルゴリズムの貴公子C++と出会う。C++には、お金持ちのお嬢様であり、Ansibleの友達でもあるChef嬢という恋人がいる。
Ansible(とSwift)はChef嬢と一緒にC++を応援しに来ていたのだ。
そんなC++だが、彼のスポンサーだったCが何者かによって(コンパイラが)殺されてしまい、大会に出場できなくなってしまっていた。
ChefがC++のスポンサーになったことでC++は大会に出場できるようになったが、しょっぱなから殺言語事件が発生しており不穏な空気が流れる。
(ちなみにKotlinは一度Chefの家にクラッキングを仕掛けて、セキュリティとして動いていたC++にボコボコにされそうになった経験がある。)

Cの殺コンパイラ現場にはなんとKotlinのコードが残されており、Kotlinが容疑者になっているという。Kotolinは、そういうのを解決するのが得意な言語としてObjective-Cをさらったのだ。

ホテル(リポジトリ)に戻ろうとしていた彼らの前に、セキュリティソフトとして活動しているHaskellが現れ力を貸して欲しいという。
Haskellの話では、KotlinのクラッキングコードがKasperskyに届いており、Z80を盗むと予告しているという。
Z80は、Perlの依頼により、セキュリティスペシャリストでHaskellの師匠のScalaの屋敷の地下金庫に保管されており、さすがのKotlinも手が出せない。
ちなみに、Perlからの依頼でZ80を預かっているとはいえ、Scalaは過去の因縁からPerlのことがめちゃくちゃ嫌いであり、Perlが築いた10年もののソースコードをはてなを利用して破壊した上で、Scalaで構築し直そうと秘密裏に画策している。

Kotlin, C++, Objective-Cの三つ巴となったZ80争奪戦の結果はどうなる!?そして殺Cコンパイラの犯人は!?すべての黒幕は!?

書いてみて

書いてたときはめっちゃ楽しかったんだけど読み返してみたら微塵も意味がわからなくてびっくりした。当然ですが言語間の関係はフィクションです。
応援上映楽しかったのでもう一回いきたいです。

【メモ/未解決】Spring BootでMockMvcのレスポンスボディが空文字になる。

Spring Bootを始めたばかりなのですが、よくわからんことがあったのでメモ。解決していません。私がSpring BootのDIよくわかってないだけだと思う。

あらまし

ServiceをモックせずにControllerのユニットテスト(JUnit4)を実行したらMockMVCのレスポンスボディが空になってしまった。対象のSpring Bootのバージョンは1.5系で、テンプレートエンジンはThymeleafを使用。

テスト対象のController

@Controller
@RequestMapping("/")
public class DemoController {

  @Autowired
  DemoService service;
  
  @GetMapping("hello")
  public String demo (Model model) {
    model.addAttribute("message", service.getMessage());
    return "hello";
  }

}

Service

@Service
public class DemoService {
  public String getMessage() {
    return "message.";
  }
}

失敗するテスト

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoControllerTests {

  @Autowired
  private DemoController controller;

  private MockMvc mockMvc;

  @Before
  public void setup() {
     mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
  }

  @Test
  public void helloにアクセスする() throws Exception {
    mockMvc.perform(get("/hello"))
      .andExpect(status().isOk()) // ここは通る
      .andExpect(content().string(containsString("message"))); // contentが空文字になってしまって通らない
  } // 実際のアプリケーションで /hello にアクセスすると期待通りの挙動になる

}

ViewResolver?が動いてないっぽい?ControllerだけインジェクトしてもViewResolver?までインジェクトできないのかもしれない。
ServiceがMockBeanでいいならGetting Started · Testing the Web Layer通りに書けばいいが、今回はMockを使わずに通しの動作をテストしたい。

回避策として書いて、かつ期待通りには動作したテスト

とりあえずWebApplicationContextをAutowiredでインジェクトしてみたら動いた。

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoControllerTests {

  @Autowired
  private WebApplicationContext wac;

  private MockMvc mockMvc;

  @Before
  public void setup() {
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
  }

  @Test
  public void helloにアクセスする() throws Exception {
    mockMvc.perform(get("/hello"))
      .andExpect(status().isOk())
      .andExpect(content().string(containsString("message"))); // 期待通り通った
  }

}

※動かないパターンのレスポンスボディが空文字になる理由が理解できてないので、このコードはあくまで回避策。