じゃこscript

フロントエンドとかデザインとかバックエンドとか浅く広く書いていきたいです

Vue-test-utilsではじめてのフロントエンドテストを書きました

背景

会社では主にディレクションやフロントエンドを担当することが多いのですが、恥ずかしながらフロントエンドのテストコードを書いたことがありません…
(そんな状態でフロントエンドエンジニアとして名乗るのもおこがましいですが…)。

現職でもエンジニアの数が少なく、これまでフロントエンドのテストコードを書く習慣がありませんでした。
そんな環境でプロダクトが大きくなるにつれ、改修に伴う動作チェックのコストが少しずつ肥大化していき、
つらみが増してきたのでテストを書いてみよう!と決意した次第です。

主に Vue.js を使う機会が多いため、Vueコンポーネントのテストコードを書いて最低限の品質を担保できるようになることを目標としたいと思います。
(欲を言えばフロントエンドのテストにおけるお作法とかも学びたい…!)

概要

  • Vue.js公式が提供している vue-test-utils について調べる
  • テストコードのサンプルを書いてみる
  • フロントエンドのテストコードにおいて、何を担保すべきかを調べる

vue-test-utilsとは

vue-test-utilsは Vue.js 向けの公式単体テストライブラリです。

https://vue-test-utils.vuejs.org/ja/ より

avoirazという非公式のテストライブラリを開発していた方が中心となって公式テストライブラリを作った、という背景があるようです。

テストを書いてみる

セットアップ vue-test-utils の使い方を体験したい場合は、基本設定としてデモリポジトリをクローンし、依存関係をインストールしてください。

Vue Test Utils | はじめるより

公式がサンプルを用意してくれてますね。
というわけで早速環境構築を進めていきたいと思います。

$ git clone https://github.com/vuejs/vue-test-utils-getting-started
$ cd vue-test-utils-getting-started
$ npm install

アプリケーションのコードは counter.js と、そのテストとなる test.js のみのシンプルな構造ですね。
インストールが完了したので、とりあえずテストを実行してみます。

$ npm test

> vue-test-utils-getting-started@1.0.0 test /Users/jaco/Development/vue-test-utils-getting-started
> jest

 PASS  ./test.js
  Counter
    ✓ renders the correct markup (4ms)
    ✓ has a button (7ms)
    ✓ button should increment the count (2ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.597s, estimated 1s
Ran all test suites.

テストが通っていることが確認できました。
では各ファイルの実装を見ていきましょう。

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}

ボタンを押すとカウンターがインクリメントされるだけのコンポーネントですね。

// Import the mount() method from the test utils
// and the component you want to test
import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
  // Now mount the component and you have the wrapper
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // it's also easy to check for the existence of elements
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })

  it('button should increment the count', () => {
    expect(wrapper.vm.count).toBe(0)
    const button = wrapper.find('button')
    button.trigger('click')
    expect(wrapper.vm.count).toBe(1)
  })
})

テストコードにコメントまでつけてくれている…!
気になるところをもうちょっと調べてみます。

const wrapper = mount(Counter)

mount メソッドによって、 import したコンポーネントのラッパオブジェクトを生成します。
mount以外にも shallowMount メソッドが存在し、
- mount: 子コンポーネントをスタブ化せずにマウントする - shallowMount: 子コンポーネントをスタブ化してマウントする といった違いがあるようです。

基本的にはこのラッパオブジェクトに定義されているメソッドを利用してテストコードを実装していくみたいですね。

console.log(wrapper) するとわかりますが、 wrapper.vm 経由でVueインスタンスにアクセスすることができます。
ラッパオブジェクトのプロパティ・メソッドについては公式ドキュメントにまとめられています。

console.log(wrapper)

// 以下はテスト結果の一部
VueWrapper {
  vnode: [Getter/Setter],
  element: [Getter/Setter],
  options: { attachedToDocument: false, sync: true },
  version: 2.4,
  vm:
    VueComponent {
      _uid: 0,
      ...

基本的な流れとして、
1. ラッパオブジェクトを生成する
2. テストケースに合わせてラッパオブジェクトのプロパティ・メソッドを利用し、テストコードを記述する
といった感じでしょうか。 この辺はテストコードを書いたり読んだりして学んでいきたいです。

フロントエンドのテストで何をテストすればいいの問題

実はこの辺りの疑問についても公式ドキュメントでサポートしてくれています。
Vue.jsの公式ドキュメントの充実具合は本当にすごいですね…

UI コンポーネントでは、コンポーネントの内部実装の詳細に集中しすぎて脆弱なテストが発生する可能性があるため、完全なラインベースのカバレッジを目指すことはお勧めしません。

代わりに、コンポーネントのパブリックインターフェイスを検証するテストを作成し、内部をブラックボックスとして扱うことをお勧めします。単一のテストケースでは、コンポーネントに提供された入力(ユーザーのやり取りやプロパティの変更)によって、期待される出力(結果の描画またはカスタムイベントの出力)が行われることが示されます。

たとえば、ボタンがクリックされるたびに表示カウンタを 1 ずつインクリメントする Counter コンポーネントの場合、そのテストケースはクリックをシミュレートし、描画された出力が 1 つ増加したのか検証します。カウンタは値をインクリメントし、入力と出力のみを扱います。

このアプローチの利点は、コンポーネントのパブリックインターフェイスが同じままである限り、コンポーネントの内部実装が時間の経過とともにどのように変化してもテストは合格になります。

このトピックは、Matt O'Connell による偉大なプレゼンテーションで詳細に説明されています。

Vue Test Utils | ガイドより

…恥ずかしながらぶっちゃけよくわかってないので、わたしなりに噛み砕いてみます(一度自分で検討をつけてみて、知見のある方に伺ってみようと思います)。

UI コンポーネントでは、コンポーネントの内部実装の詳細に集中しすぎて脆弱なテストが発生する可能性があるため、完全なラインベースのカバレッジを目指すことはお勧めしません。 代わりに、コンポーネントのパブリックインターフェイスを検証するテストを作成し、内部をブラックボックスとして扱うことをお勧めします。単一のテストケースでは、コンポーネントに提供された入力(ユーザーのやり取りやプロパティの変更)によって、期待される出力(結果の描画またはカスタムイベントの出力)が行われることが示されます。

内部のメソッドまでテストするのではなく、コンポーネントの振る舞いをテストしよう、ということですかね。

× コンポーネント内部の◯◯というメソッドが正しく動いていることをテストする
◯ 呼び出し側から見て、(内部で何が起こってるのかは知らないけど)コンポーネントが正しく動いていることをテストする

サンプルコードに置き換えると、

× incrementメソッドを実行するとcountプロパティが加算されることをテストする
◯ @clickを発火するとcountプロパティが加算されることをテストする

このようなイメージです。
@clickを発火するとspanの中身が加算されることをテストする でもいいのでは?と思ったのですが、

it('renders the correct markup', () => {
  expect(wrapper.html()).toContain('<span class="count">0</span>')
})

このテストで count が正しくviewに反映されていることが担保されているので、
よりシンプルに、 @click イベントによる挙動をテストしているのではないかと思います。

たとえば、ボタンがクリックされるたびに表示カウンタを 1 ずつインクリメントする Counter コンポーネントの場合、そのテストケースはクリックをシミュレートし、描画された出力が 1 つ増加したのか検証します。カウンタは値をインクリメントし、入力と出力のみを扱います。

このアプローチの利点は、コンポーネントのパブリックインターフェイスが同じままである限り、コンポーネントの内部実装が時間の経過とともにどのように変化してもテストは合格になります。

@clickというインターフェースが変わらない限り、incrementメソッドの名前や内部のロジックが変わったとしても、@clickを発火することで値がインクリメントされることはテストできるよね

ということでしょうか?
ここちょっと自信ないので要確認です…

また、技術書典で購入させていただいた、@mya-akeさんの現場で使えるVue.js tips集によると、

基本的にはホワイトボックステストというよりもブラックボックステストをする感覚がよいと思います。 具体的には次の3つあたりをテストすると効果が高そうです。

  1. 無事にマウントされること
  2. イベントが発火し期待した結果となること
  3. propsで受け取る値により期待した結果となること

とのことです。
propsコンポーネントのインターフェースの1つですし、ブラックボックステストとしての観点でテストしておいた方が良さそうですね。

※内容を記載することについては@mya-akeさんから承諾をいただくことができました。ありがとうございました!
※余談ですがこのtips集、とーーーーーーっても実用的でした!
(個人的に今年の技術書典で1番買って良かったと思っています( ◜◡◝ ))

所感

めちゃめちゃ長くなってしまいました…
あらためて、Vue.jsはドキュメント周りがしっかりしているな…と感じました。
vue-test-utilsJestKarma といったテストランナーを組み合わせることで真価を発揮するようなので、
いずれはこの辺りについても調べてみようと思います。

テストについては本当にド素人なので、ここ間違ってるよ〜・こうするといいかもねといったアドバイスをいただけるととても嬉しいです。

Dockのエフェクトをジニーからスケールに変更したらMacのフリーズ率が激減しました

背景

タイトル通りです。
1ヶ月ほど前に会社で新しいMBPを支給してもらったのですが、それからアプリケーションを最小化する際にPCがフリーズする現象が多発してしまい…
酷い時には1日に5~6回再起動することになり、原因を調べることになりました。

概要

PC新調前後それぞれの環境は以下の通りです。

新調前

MacBook Pro (Late2014) 15inch
OS Sierra

新調後

MacBook Pro (Late2017) 13inch
OS High Sierra

※メモリやCPUについては確かほぼ変わらなかった記憶があります。

調査

Mac Application minify freeze などググるものの、これといった解決策は見当たらず…
High Sierra が未だに動作が不安定という話は耳にしていたため、OSの問題かなぁと諦めようとしたその時、

後輩「どうでもいいですけど、ジニーエフェクトって無駄に挙動凝ってて好きじゃないんですよねー」
わたし「ほう…?」

ダメ元で Dockの最小化エフェクトをジニーからスケールに変更 したところ、これが当たりだったようです。
以後フリーズすることはなくなりました。

所感

確かにジニーエフェクトは開発側の「どう?ヌルっと動いてすごいやろ?」感がありますね!!
(いるかどうかはさておき)同じような現象に悩んでる方のお役に立てれば幸いです。

会社でフロントエンド研修課題をつくりました(その3. とりあえず実装してみる)

背景

こんな流れで研修課題を作ることになりました。

概要

  • 課題内容を考えてみる
  • 環境構築手順をまとめる
  • 実際にアプリを作ってみる ←いまここ

実際にアプリを作ってみる

前回で環境構築が完了したので、今回からはコードを書いていきましょう。
ゴールは Vue.js+Vuexを使ったTodoアプリ です。
とはいえ、いきなりコンポーネント化しつつVuexも使いつつ…というのもハードルが高いので、
少しずつ段階を踏んでいくようにしましょう。

  1. Vue.jsのみで1ファイルでTodoアプリを作る
  2. コンポーネント化を進める(ファイル分割)
  3. Vuexで状態管理する
  4. Vuelidateでリアルタイムバリデーションを実装する
  5. テストコードを書く

こんな感じですかね。

では、まず1ファイルでTodoアプリを作ってみましょう。

Vue.jsのみで1ファイルでTodoアプリを作る

というわけで出来上がったコードがこちらになります(3分クッキング感)。

App.vue

<template>
  <div class='todo'>
    <ul>
      <li v-for="item in items" :key="item.index">
        <label :class="{ checked: item.isChecked }">
          <input type="checkbox" v-model="item.isChecked"> {{ item.title }}
        </label>
      </li>
    </ul>
    <div>
      <input type="text"
        placeholder="タスクを入力してみよう"
        v-model="newItemTitle" />
    </div>
    <div>
      <button @click="addTodo(newItemTitle)">タスクを追加する</button>
      <button @click="deleteTodo(newItemTitle)">チェックをつけたタスクを削除する</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data: function() {
    return {
      items: [
        {
          title: '夕飯の材料を買いに行く',
          isChecked: false
        },
        {
          title: 'お風呂の掃除をする',
          isChecked: true
        },
        {
          title: 'ゴミ出しをする',
          isChecked: false
        }
      ],
      newItemTitle: '',
    }
  },
  methods: {
    addTodo(newTitle) {
      let item = {
        title: newTitle,
        isChecked: false
      }
      this.items.push(item)
    },
    deleteTodo() {
      this.items = this.items.filter(item => {
        return item.isChecked == false
      })
    }
  }
}
</script>

f:id:ia_isier:20180526011559p:plain

画面はこんな感じです。
見た目が非常にアレですが、とりあえずこのまま進めていきましょう。

コンポーネント化を進める(ファイル分割)

次の段階として、ボタンをコンポーネント化しましょう。
インターフェースとしては、テキストクリックした時のイベント 辺りを渡せれば十分ですかね。

App.vue

<template>
  <div class='todo'>
      // 略
    <div>
      <TodoButton @buttonClick="addTodo(newItemTitle)" :text="'タスクを追加する'"></TodoButton>
      <TodoButton @buttonClick="deleteTodo" :text="'チェックをつけたタスクを削除する'"></TodoButton>
    </div>
  </div>
</template>

<script>
import TodoButton from './components/Button'

export default {
  name: 'App',
  components: {
    TodoButton: TodoButton
  },
  // 略
}
</script>

components/Button.vue

<template>
  <button @click='onClick()'>{{ text }}</button>
</template>

<script>
export default {
  name: 'Button',
  props: {
    text: {
      type: String,
      default: 'default',
    },
  },
  methods: {
    onClick: function() {
      this.$emit('buttonClick')
    },
  },
}
</script>

コンポーネント化しただけなので、特に見た目は変わりません。
次は Vuexで状態管理する ですが、これは導入からやると長くなりそうなので今回はここまでにします。

所感

書いてて「良い研修課題ってなんだろう…?」と何度も頭を捻ってしまいました。
わかりやすさと楽しさとスキルの身につけやすさ、これらが同居するようなものを作れるといいのですが…

会社でフロントエンド研修課題をつくりました(その2. 環境構築)

背景

こんな流れで研修課題を作ることになりました。

概要

  • 課題内容を考えてみる
  • 環境構築手順をまとめる ←いまここ
  • 実際にアプリを作ってみる

環境構築手順をまとめる

研修課題なので、サクッと作れて何かトラブった時にサクッと壊せるのがベターですね。
というわけでvue-cliを使います。
こういう「とりあえずでいいからvue試してみたいねん」って時にめっちゃ役立ちます。
もしかしたら本番のプロダクトでも実運用できるのかな…?要調査ですね。

では早速使っていきましょう!

vue-cliのインストール

$ yarn global add @vue/cli
$ vue create vue-project
?  Your connection to the the default npm registry seems to be slow.

上記のコマンドを実行するだけでプロジェクトを作成してくれます。
途中いくつか質問を受けるので、適宜回答していきましょう。

注: ネット上でよく見る vue init ◯◯(テンプレート名) ◯◯(プロジェクト名) ですが、init コマンドは最新の3 系では古くなっているため、 create コマンドを使用するといいみたいです。

Use https://registry.npm.taobao.org for faster installation? (Y/n)

デフォルトで参照している npm package registry では接続が遅くなるとのこと。
こっちの方が速いぜ!と勧めてくれているので Yes と答えておきましょう。

? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
  Manually select features

babeleslint も用意されているデフォルトのプリセットで問題ありません。 default を選択。

? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM

パッケージマネージャーは yarn でいいですね。
諸々必要なパッケージのインストールが始まります。

✨  Creating project in /Users/jaco/Development/vue-cli.
🗃  Initializing git repository...
⚙  Installing CLI plugins. This might take a while...

yarn install v1.3.2
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 52.59s.

🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.3.2
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 2.97s.

⚓  Running completion hooks...

🎉  Successfully created project vue-project.
👉  Get started with the following commands:

 $ cd vue-project
 $ yarn serve

至れり尽くせりですね。
指示通りにコマンドを実行しましょう。
(どうでもいいですが、何故か「いたせりつくせり」だと思っていました…)

$ cd vue-project
$ yarn serve
 DONE  Compiled successfully in 3570ms


  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.179.5:8080/

  Note that the development build is not optimized.
  To create a production build, run yarn build.

内部で webpack-dev-server が立ち上がっているため、自動でブラウザ上で http://localhost:8080/ を開いてくれます。

f:id:ia_isier:20180526003345p:plain

これで環境構築は終了です。良い時代になりましたね。
次回からは実際にコードを書いていこうと思います。

会社でフロントエンド研修課題をつくりました(その1. 何をテーマにするか)

背景

現職のインターン生はほぼほぼバックエンドを担当しているのですが、
先日「フロントエンドも最低限開発できるようになりたい」という要望を受けました。

フロントエンドにおける研修課題のようなものはなかった(バックエンドはある)ため、いっちょやったるかーという次第です。
インターン生にヒアリングしてみたところ、

以上の需要があるようでした(コーディングも楽しいと思うんだけどなあ。。。)。

こういった研修課題は一種の集合知というか、作ってFBもらって改善して…というサイクルを回していくことで効率よく質を上げることができるので、
まずはお試しでプロトタイプのようなものを作ることを目標としたいと思います。

概要

おおまかにこんな流れで進めてみます。

  • 課題内容を考える ← いまここ
  • 環境構築手順をまとめる
  • 実際に課題を作ってみる

課題内容を考える

楽しく学べて、仕事に役立つスキルが身につくというのが自分の考える研修の理想像です。
要素を分解すると、

楽しく学べる→自分の関心がある要素が詰まっている
仕事に役立つ→実務で使えるノウハウが身につけることができる

こんな感じですかね。

わたしからインターン生に期待する(身につけてほしい・身につけると役に立つ)スキルとしては、

  • HTML/CSSで最低限のコーディングができる
    • とはいえゼロベースで新規機能のフロント実装…というのも厳しいので、「コーディング規約とスタイルガイドを参考にコーディングができる」と定義しておきます。
  • 要件を元にCSS, Javascriptを使って必要な機能を実装することができる

以上が挙げられます。
これに、

というインターン生からの要望を織り込んでみましょう。

あれ…Vue.js+Vuexを使ったTodoアプリでイケそうな気がしてきました。

現職ではAPIから取得した設定値に基づいてコンポーネントを配置するケースが多いため、
API制御周りも含めようと考えてました。
が、良いサンプルが思いつかなかったので、その辺は折を見て導入できればと。。。

というわけで、
Vue.js+Vuexを使ったTodoアプリ という、
いんたーねっつで検索かければいくらでも出てきそうな内容の研修課題となりましたが、とりあえずこの方向性で作ってみようと思います。
次回は環境構築編です。

localstorageに意図せずundefinedを突っ込んだらハマりました

localStorage を久しぶりに使う機会があって、ハマったのでメモしておきます。

背景

// hogeはobjectを想定しています
localStorage.setItem('item', JSON.stringify(hoge));

~~~~~~
let item = JSON.parse(localStorage.getItem('item'));
console.log(item); // object?

もちろんAPIから取得した object が表示されることを期待していたのですが、Uncaught Syntaxerror: Unexpected token u が発生。
調べてみると undefinedJSON.parse すると発生するようですね。
というわけで

// hogeはAPIからfetchしてきたobjectを想定しています
localStorage.setItem('item', JSON.stringify(hoge));

~~~~~~
let moge = localStorage.getItem('item');
if (typeof moge === undefined) return;
let item = JSON.parse(moge);
console.log(item); // object?

これなら大丈夫だろうと思いきや、ログには undefined が…
なんで undefined の判定入れてるのにすり抜けるんだろう?というかなんで JSON.parse() できてるの…

解決法

10分ほど悩みましたが、よく考えたら

localStorage.setItem('item', JSON.stringify(hoge));

ここで文字列として突っ込んでるんですよね。

console.log(typeof item); // String

ですよねー。
localStorageに String として突っ込んだ undefined はパースしても Stringとしての undefined だよ、という話でした。

まあAPIの返り値からちゃんとチェックしろという話なんですよね…これ…
localStorageの仕様を身体で覚えることができたということでとりあえずは良しとします…

【EC-CUBE3.x】ログイン後のリダイレクト周りを制御する

最近よくEC-CUBEを触るようになったのでメモ代わりに。
ほんとはRiotとRailsとSwift触りたいけど、これはこれで楽しい。

ケース

  • ログイン前にカートに商品を追加する
  • 追加した状態でログインする
  • ログイン後のリダイレクト先をカートに変更したい
  • (通常のログイン後のリダイレクト先はトップのままでいい)

実装手順

  • カートからログインページに遷移する際、クエリ文字列に rd=‘cart’ を指定する
  • クエリ文字列に rd=‘cart’ が含まれている場合、twig側で input[type=‘hidden’] を追加し、ログインと同時にPOSTする
  • ログイン成功後に DefaultAuthenticationSuccessHandler による処理を挟むようにする
  • DefaultAuthenticationSuccessHandleronAuthenticationSuccess メソッド内に条件分岐によるリダイレクト処理を挟む

流れとしてはこんな感じ。

ソースコード

カートからログインページに遷移する際、クエリ文字列に rd=‘cart’ を指定する

src/Eccube/Controller/ShoppingController.php

public function index(Application $app, Request $request) {
    // 略
    if (is_null($Customer)) {
        $params = array('rd' => 'cart');
        $url = $app['url_generator']->generate('shopping_login', $params); // クエリ文字列用の配列を渡せばOK
        return $app->redirect($url);

クエリ文字列に rd=‘cart’ が含まれている場合、twig側で input[type=‘hidden’] を追加し、ログインと同時にPOSTする

app/template/default/Shopping/login.twig

{% if rd == 'cart' %}
    <input type="hidden" name="rd" value="cart">
{% endif  %}

src/Eccube/Controller/ShoppingController.php

public function login(Application $app, Request $request) {
    // 略
    if ($request->query->get('rd') == 'cart') {
        $rd = 'cart';
    }

    return $app->render('Shopping/login.twig', array(
        'error' => $app['security.last_error']($request),
        'form' => $form->createView(),
        'rd' => $rd // twig内で参照できるように渡す
    ));
}

ログイン成功後に DefaultAuthenticationSuccessHandler による処理を挟むようにする + DefaultAuthenticationSuccessHandleronAuthenticationSuccess メソッド内に条件分岐によるリダイレクト処理を挟む

src/Eccube/Application.php

$this['security.authentication.success_handler.customer'] = $this->share(function ()  {
    $handler = new \Eccube\Security\DefaultAuthenticationSuccessHandler(
        $this,
        $this['security.http_utils'],
        $this['security.firewalls']['customer']['form']
    );
    $handler->setProviderKey('customer');

    return $handler;
});

↑を追記する

src/Eccube/Security/DefaultAuthenticationSuccessHandler.php

<?php

namespace Eccube\Security;

use Eccube\Application;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler as BaseDefaultAuthenticationSuccessHandler;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\HttpUtils;

class DefaultAuthenticationSuccessHandler extends BaseDefaultAuthenticationSuccessHandler
{
    /**
     * @var Application
     */
    private $app;

    public function __construct(Application $app, HttpUtils $httpUtils, array $options = array())
    {
        parent::__construct($httpUtils, $options);
        $this->app = $app;
    }

    public function onAuthenticationSuccess( Request $request, TokenInterface $token)
    {
        // rdがcartの場合、カートからのログインなのでカートにリダイレクトさせる
        if ($request->request->get('rd') == 'cart') {
          return $this->httpUtils->createRedirectResponse($request, '/cart');
        }
        return parent::onAuthenticationSuccess($request, $token);
    }
}

↑を新しく作る

参考

EC-CUBE3カスタマイズ - ユーザーがログイン成功/失敗したときにプログラムを実行する方法

おわりに

ちゃんとsymfonyとかsilexのドキュメント読むと色々よしなに対応してくれてるんだなーと実感した。

単純にログイン後のリダイレクト先を一律変更するだけなら、
/src/Eccube/Application.phpinitSecurity() 内で宣言している
$app[‘security.firewalls’] 辺りをいじればいけると思う。 (試してないので保証はしない)

手探り感ぱないのは否めないし、もっと良い書き方とかありそう。