Rails Diary

プログラミングの学習記録です。

RSpec作成の流れ

RSpecの作り方の流れをまとめておきたい。
※ 過去のメモと重複箇所多

テストコードはプロダクトコードに比べると絶対的な正解というのがあまりなく、色々な書き方が存在するので、解答例と寸分狂わずみたいなことを目指さなくて良い。

RSpec

スペックファイル作成

bundle exec rails g rspec:system AdminArticlePreviews

ドライバ設定, supportファイルの読み込み設定

① スペックファイルごとにドライバの初期設定が記載されているが、共通項目として別のsupportディレクトリ配下にドライバの設定ファイルを作成する
# spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selnium, using: :headless_chrome, screen_size: [1920, 1080]
  end
end
  • ドライバとはテストにおける動作システム。テストの実行環境のこと。デフォルトで設定されているRack::Testは高速だが、JSをテストすることができないので、JSが使えるseleniumドライバを設定している
  • 互換性の問題でヘッドレスブラウザ(テストで使うGUIを持たないブラウザ)にはChromeを採用した方が良いらしい。Chromeを使うためにはwebdriversジェムを追加する。そうすると、依存関係にあるselenium-webdriverも一緒にインストールしてくれるので明示的に書かなくていいとのこと。(詳しくはよくわからない)
② 各スペックがsupport配下のファイル設定を読み込むよう、rails_helper.rbに記載
# 元々コメントアウトになっているものを外せばOK
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

FactoryBotでテストデータを作成

① FactoryBotのファイルを作成
$ bundle exec rails g factory_bot:model user
② テストデータを作成する
FactoryBot.define do
  factory :user do
    sequence(:name, "general_1" } 
    password { "password" }
    password_confirmation { "password" }
    role { :general }

    trait :admin do
      sequence(:name, "admin_1")
      role { :admin }
    end
  end
end
  • sequenceで連番データの作成ができる
  • ユニークなテストデータを作りたい時に使う
  • sequenceはブロックを渡さずに第二引数を渡すとRubyの.nextメソッドが呼び出され、上記のような書き方でも連番が作れる。sequence(:email) { |n| "test#{n}@example.com" }のように連続させたい数字が真ん中に挟まっている場合、.nextが上手く反映されないので大人しくブロックを渡す
  • 管理者権限はtraitを用いて付与
③ FactoryBotの記載省略を設定ファイルに追加

通常、FactoryBot.create(:user)のような記載でダミーデータを作成するが、設定ファイルに下記の記載をすることで先頭のFactoryBotを省略できる

# spec/rails_helper.rb
confin.include FactoryBot::Syntax::Methods

user = create(:user)ないし、let(:user) { create(:user) }みたいな形で作成できる。

ログイン処理を共通項目として切り分ける

ログイン後のテストにおいて、一々ログイン処理をテストに書き込むのはDRYさに欠ける。このため、ログイン処理をモジュールに切り分ける。

① support配下にログイン処理を作成

# spec/support/login_macros.rb
module LoginMacros
  def login_as(user)
    visit admin_login_identifier_path
    click_link 'Login'
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password'
    click_button 'Login'
  end
end

ちなみにmacrosとは

パソコンで、複雑な操作の手順をあらかじめ登録しておき、必要なときに簡単に実行させる機能。マクロ機能。 macro(マクロ)の意味 - goo国語辞書

② 共通項目なので、こちらもrails_helper.rbに読み込みを記載
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods

  # 明示的にLoginMacrosの読み込みを記載
  config.include LoginMacros
end

実際にテストを作成

前述した通り、テストコードに絶対的な正解はないんだそう。ただ現状、自分の乏しい経験値では的確なコードを書くのはかなり難しい。たくさん書いて慣れていくしかないと思う。

require 'rails_helper'

RSpec.describe "AdminArticlesPreviews", type: :system do
  let(:user) { create(:user, :admin)}
  describe 'ログイン後' do
    describe '画像のコンテンツブロック挿入後' do
      context '画像のファイルをアップロードしていない場合' do
        it '記事のプレビューページを閲覧できる' do
          login_as(user)
          click_link '記事'
          click_link '新規作成'
          fill_in 'タイトル', with: 'test'
          click_button '登録する'
          click_link 'ブロックを追加する'
          click_link '画像'
          click_link 'プレビュー'
          switch_to_window(windows.second)
          expect(page).to have_content('test')
        end
      end
    end
  end
end

こんな感じで書いた。 テストを実行すると、

https://i.gyazo.com/8614491183ea222c5a20e3124df429cb.png

エラー発生

https://i.gyazo.com/119b410ef8f2863ce58d095b5f7caff6.png

https://i.gyazo.com/5ea029496fe685193fbbafeea55eb5c1.png

結論、テスト環境にもシードデータを追加することで解決。。

bundle exec rails db:seed_fu RAILS_ENV=test

seed-fuは、既に存在しているが変更したいレコードだけ更新したり、ファイル単位で実行できたり、簡単に書けるようなシンタックスシュガーがあったりど便利です。 railsで初期データを入れる(seed-fuの使い方) - Qiita

rails db:seedを何も考えずに使うと実行する度に同じデータが登録されてしまう。

もしくは、spec/spec_helper.rbに下記を記載

RSpec.configure do |config|
  config.before :suite do
    SeedFu.seed
  end
end

テスト実行の度にシードデータを入れている。

着眼点として

  • current_siteがnilである
  • そもそもSiteのデータ登録が必要?

という思考には至らなかった。もし思い至ってもシードデータをテスト環境に追加しようとは思えなかった。

click_buttonとclick_linkについて

RSpec Capybara href の無い a タグにハマる - かもメモ

自分は検証で調べてbtnならclick_button、見た目がボタンではなくただのリンクならclick_linkみたいな使い方をして来たけれど、aタグはリンクでないとマッチしないらしい。

https://i.gyazo.com/832461d576b7a9b4ea9aec54891c82e9.png

https://i.gyazo.com/46730b7fb7883fea893ad310217994aa.png

見た目がボタンならボタンというわけではないらしい。

参考にしたサイト

【Rails】はじめてのSystemSpec(RSpec) - Qiita

RSpec Capybara href の無い a タグにハマる - かもメモ

railsで初期データを入れる(seed-fuの使い方) - Qiita

RSpecメモ(3)

letを使う

let(:project) { create(:project) }
let!(:task) { create(:task, project_id: project.id) }

FactoryBotのファイルにprojectとのアソシエーションを記載すれば、テストデータ作成の際にproject_idを記載しなくても良いものと思っていたけれど、上手く行かなかったのでproject_idも記載
※ 遅延するletとitの前に作成してくれるlet!の使い所に注意

別ダブを開く時のテスト

projectの詳細画面に遷移し、task一覧へのリンクをクリックすると別タブが開かれる仕様。別タブを開く記載switch_to_window(window.second)がないためテストが失敗していた。

RSpecのテストで別タブをテストする方法 - study-outputの日記

visit project_path(project)
click_link 'View Todos'

switch_to_window(window.second) # 追加

expect(page).to have_content task.title
(省略)

click_linkやclick_buttonの後に記載、最後に開いたタブに移動する場合はwindow.last、次に開いたタブに移動する場合はwindow.secondを記述する。

日付表示

expect(find('.task_list')).to have_content(Time.current.strftime('%Y-%m-%d')) # 削除
expect(find('.task_list')).to have_content(Time.current.strftime('%-m/%d %-H:%M'))  #編集

時刻をformat文字列に従って文字列に変換した結果を返します。 strftimeメソッド

実際のビュー画面とdeadlineの表示がことなっていたので変更

しかしこの書き方だと、Viewファイルの仕様が変わった時にこのままのテストで対応できなくなってしまう模様。

RSpecの日付表示を、viewで使っているメソッドに合わせて修正 - Kuni-Blog、こちらのサイトを同じ状態だった。

viewファイルで使っているヘルパーメソッドを用いてテストも書き換えるのが望ましい。

<!-- app/views/tasks/index.html.erb -->
<td><%= short_time(task.deadline) if task.deadline? %></td>
expect(find('.task_list')).to have_content(short_time(Time.current))

↑のような形で記述する

さらにこのままだとshort_timeを使ったことでNo methodエラーが出てしまうので、rails_helper.rbにApplicationHelperの読み込みを記載する。

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include ApplicationHelper
end
この記載により、RSpecでもヘルパーを用いることができる。

RSpecメモ(2) Capybara

qiita.com

Capybara

  • Webアプリ用のE2E(end to end…始めから終わりまでテストすること)テストフレームワーク
  • 複数あるドライバ(動作システム)の中から使いたいものを選択できる
    →Capybaraのテスト実行環境(Headlessブラウザ)を選択

※ ヘッドレスブラウザとはGUIを持たないブラウザ。人が操作する目的のものではない。
GUI…マウスや指などで操作できる画面を持たないブラウザのこと。テストの実行環境。 https://wa3.i-3-i.info/word1371.html

Capybaraを使うことで、Webアプリのブラウザでの自動実行、JavaScriptの動作確認、Headlessブラウザ(GUIのないブラウザ)を操作できる

RSpecファイル作成、使用するドライバ設定

❶ テストファイル作成

bundle exec rails g rspec:system User, UserSessions, Task

❷ 作成したスペックファイルにテストを記載

require 'rails_helper'

RSpec.describe "Users", type: :system do
# デフォルトではRack::Testというドライバが設定されている
  before do
    driven_by(:rack_test)
  end

  pending "add some scenarios (or delete) #{__FILE__}"

Rack::Test…デフォルトのドライバ。高速だがJSを使えない。 Rackとは

Seleniumドライバ

JavaScriptもサポートしているseleniumドライバを使う場合、デフォルトでFirefoxを使ってテストする様に設定されているが、互換性の問題でChromeを使うように設定しておいた方がいいとのこと。Chromeを使うためにはwebdriversジェムを追加する。webdriversはライブラリの依存関係上、selenium-webdriverも一緒にインストールしてくれるので、明示的に書かなくて済むそうな。

他にも、Webkitドライバ(Seleniumより高速)、Poltergeistなどがある

ドライバの設定

通常テストで複数スペックを作成するので、各ファイルに自動的に記載されているドライバの設定は削除し、自作したsupportディレクトリにまとめておく

# spec/support/capybara.rb supportディレクトリを自分で作成
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :headless_chrome
  end
end

こっちの書き方でもOKだった

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by(:selenium_chrome_headless)
  end
end

末尾の_headlessを外すことで実際のブラウザでの動作が見られる

rails_helper.rbなどにsupport配下のファイルを読み込む設定を記載

これにより上で作成したsupport配下の設定ファイルが読み込まれる

# spec/rails_helper.rbの下記のコメントアウトを外す
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

capybaraの構文

describeやit等の起点はRSpecのもの、vistやfill_in '', with: ''というのは、Capybara独自の起点

.rspec

下記のようにルートディレクトリに.rspecファイルを作成し、下記の様に記述することでテスト結果を見やすくすることができる。

--format documentation
--color

RSpecで特定のテストを実行する方法

# ファイル名と行数指定してテストが実行できる
$ rspec spec/models/user_spec.rb:12

もしくは
spec_helper.rbに以下の記述を追加し、focus: trueを使える様にする

RSpec.configure do |config|
  config.filter_run_when_matching :focus
end

テストファイルのdescribe, context, itの内、実行したいテストブロックの先頭にfをつけてfdescribe, fcontext, fitのようにすることで特定のテストのみ実行ができる。

fの消し忘れ対策として

Rubocopでclass: RuboCop::Cop::RSpec::Focusを設定しておくと消し忘れた場合に検知できる。 pre-commitやCIを使って自動でRubocopを実行する様にしておくと便利とのこと。

参考にしたサイト

【Rspec】Capybaraについて | プログラミングマガジン

【Rspec】Capybaraの構文まとめ | プログラミングマガジン

Selenium + Capybara + RSpecで自動テストしてみる : 準備編

RSpecで特定のテストを実行する方法

Rackとは何か - Qiita

Capybaraチートシート - Qiita

RSpecメモ(1)

RSpecを書く際のポイントメモ

■ エラーチェック

task = user.tasks.new(title: “”, content: “aaa”, status: :todo)
expect(task.valid?).to be(false)
expect(task.errors).not_to be_empty
expect(task.errors[:status]).to eq [“can’t be blank”] 
  • errorが空出ないことの確認だけではなく、エラー文に想定する文言が含まれているかもテストする

■ タイトルの重複チェック

it ‘is invalid with a duplicate title’ do
  user = User.create(email: “user@example.com”, password: “password”)
  task1 = user.tasks.create(title: “aaa”, content: “aaa”, status: :todo)
  task2→task_with_duplicated_title = user.tasks.build(title: “aaa”, content: “aaa”, status: :todo)
  expect(task2.valid?).to be(false)
  expect(task2.errors[:title]).to eq [“has already been taken”]
end
  • 一つ目のタスクを適当に作成し、二つ目を一つ目と同じtask.titleにすることでtitleの重複状態を作る
  • 一つ目をcreateで作成し、二つ目をbuildで未saveにしているのは二つ目がtitleの重複によってsaveできないことを検証するテストだから
  • 基準(?)となる一つ目のtaskオブジェクトは他で使わないので変数に入れる必要はない
  • タスクの変数をtaskやtask1と書くだけだとイメージしづらいので、task_without_titleやtask_with_duplicated_titleの様に具体的な名前にすると良い

■ be_validやbe_invaidマッチャを使う

expect(task.valid?).to be(false)
# 書き換え
expect(task).to be_valid
expect(task.invalid?).to be(true)

expect(task).to be_invalid

# 良いか悪いかはさておき(分からない)、こっちでも通った
expect(task).not_to be_valid

FactoryBotについて

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
end
# spec/factories/tasks.rb
FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    content { "content" }
    status { :todo }
    deadline { 1.week.from_now }
    association :user
  end
end
  • contentとstatusは一意である必要がないので、そのまま打ち込む
  • 今現在から一週間の間でランダムに値を作ってくれているらしい
    https://i.gyazo.com/bb59c984c46158a9574a26ac25b34aa7.png

  • associationでuserとの関連を記載することで、データ作成時にuserを記載する必要がなくなる(後述)。

上記のsequenceの書き方について

連番データを作成することができる。ユニークな値を作りたい場合に用いる。

sequence(:title) { |n| "title_#{n}" }

の書き方の方がどういう動きか見えやすい気がするが…

sequence(:title, "title_1")

は何が起きているのか分からない。
二つ目の書き方では何が起こっているのか。

こちらのサイトによると、「ブロックを渡さずに第二引数を渡すと、.next(Rubyメソッド)が呼ばれるようになっている」ので

https://i.gyazo.com/7b3ea94000ab4c5a5eb8998b201d985c.png

このように異なる連番のtitleを作り出せる(ちなみにテストをカタカナで書くと上手くいかない)

emailのsequenceは変えたい番号が文字列の途中に挟まっているので、nextメソッドで上手く変更を入れることができない。ブロックを使う。

FactoryBotに記載したデータを使う

user = User.create(email: "test@example.com", password: "password")
task = user.tasks.build(title: "test1", content: "test1")
expect(task).to be_valid

みたいな形でexample(テストの1単位)を作ってきたが、FactoryBotを追加することで、記載が楽になる。


確かにごちゃごちゃしていない

user = FactoryBot.create(:user)
task = FactoryBot.build(:task, user: user)


さらに、先ほどspec/factories/tasks.rbにassociation :userというuserとの関連を記載したが、アレによってuserの記載が省略できる

task = FactoryBot.build(:task)

一層シンプルに!
さらに…

rails_helper.rb内のconfigブロック内に以下を記載

# spec/rails_helper.rb
config.include FactoryBot::Syntax::Methods

これにより、データ作成の先頭にあったFactoryBotをも省略できる

task = build(:task)

もはや原型がない。DRYすぎて返って分かりづらい気もするけど、テスト数が増えてくればこの簡素な感じが役に立つのかなと思う。や

所感

こうやって順を追って、「こういう理由だからこの記載を省略できる」と分かりやすいのかもしれない。最初から洗練された簡素なコードを見てしまうと、情報量の少なさにどうしたら良いのか分からなくなった。

書き方に全く馴染みがなかったのもあるけれど、いきなりFactoryBotでテスト用のダミーデータ作成、RSpecのテスト構文を色々試してみるだと混乱するだけなので、まずはFactoryBotを使わず、慣れないマッチャを使わず、RSpecのDRY回避をせず、少しずつ試してみれば良かったなと思います。

よく分からないままlet(:user) { emai: "", password: "password"}みたいなことをしていたので、与えられた条件の中で上手く工夫することが苦手だなぁと・・・。 あとは本来アプリ上の不具合を予防するためにテストを書くはずなのに、そのテスト自体でエラーが出ちゃう状態に苦笑いでした笑

参考にしたサイト

RSpec

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita

後々参考にしたいサイト

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

FactoryBot

FactoryBot(FactoryGirl)チートシート - Qiita

Factorybotを使ったテストデータの作成方法 - Qiita

Sequence

Rspec,FactoryBotのsequence - Qiita

FactoryBot (旧FactoryGirl) の sequence と .next - Qiita

JSとDOM

DOM(Document Object Model)

ドキュメントを物として扱うモデル。 JSからHTMLにアクセスする仕組みのこと。この仕組みによって文書構造、スタイル、内容を変更することができる。DOMを操作して画面をちょこちょこ変えるのがJSの役割(ただし、ひと昔前の内容らしい)

https://www.w3schools.com/js/pic_htmltree.gif
DOMのツリー構造 JavaScript HTML DOM

DOMは、文書をノードとオブジェクトで表現する。ノードとは、上記画像における一つ一つの要素のことをそう呼ぶ。HTMLにおけるエレメントやタグのこと。

ウェブページは文書です。この文書はブラウザーのウィンドウに表示されるかHTMLソースとして表示することが可能です。しかし両方の場合においてもそれは同じ文書です。ドキュメントオブジェクトモデル(DOM)は、その同じ文書を表現、保存する方法です。DOMはウェブページの完全なオブジェクト指向の表現で、JavaScriptのようなスクリプト言語から変更できます。

ID名からノードを取得し、操作する

<button id="button-delete">削除ボタン</button>
<button id="button-update">更新ボタン</button>
コンソールに以下を打ち込む

document.getElementById("id名")

ID名から要素を取得するメソッド

https://i.gyazo.com/c31ea2c554e2578879034df97f7622ea.png

button-deleteというIDが振られた要素が取得されている。
単なる文字列ではなくこれがDOMであり、オブジェクト。

https://i.gyazo.com/0483e3e6edbf8597e322ef7c7249ac9e.png

https://i.gyazo.com/e6bc4e4cd025568099d938f46585aab6.png

削除ボタンだけ赤になった。

上記のような方法で取得したオブジェクトを介してメソッドを実行することができる。

https://i.gyazo.com/4697dced0afe428243edd730e353fd9b.png

document.getElementById('ID名')で取得した要素を定数に代入し、定義した定数に対してinnerTextメソッドを用いるとタグに囲まれたテキストが表示された。

https://i.gyazo.com/ed38480e9a0266e1df1a901d32ece6d3.png

上記のように新たに文字列を代入するとボタンの文字が変更された。

https://i.gyazo.com/2563cc55ec90814661bf83f381845156.png

このようにコンソールにJSを打ち込むことでDOM(ドキュメントオブジェクトモデル)の取得ができる。実際はイベントドリブンと呼ばれる「何かをしたら何かが起きる」という処理を追加することで動かす。

  • あるボタンをクリックしたらポップアップを出す
  • マウスポインターがある文字の上に乗ったらその文字を色付ける
  • あるテキストエリアの内容が変更されたらそれらの文字をプレビュー表示する

イベントは下記のように設定する

DOMに対してaddEventListenerを設定することでイベントを仕掛けられる

// 一番上の行は「ブラウザがDOMの解析をし終わったら〜」というような一種のイベント設定処理
// この記述がないと、DOMの解析がし終わる前に、削除ボタンのDOMを取得しようとするなど上手く動かないことがある
document.addEventListener('DOMContentLoaded', () => {
  console.log("DOMContentLoaded")

  // 削除ボタンのDOMを取得
  const buttonDelete = document.getElementById("button-delete")
  // 更新ボタンのDOMを取得
  const buttonUpdate = document.getElementById("button-update")
  
  // 削除ボタンにクリックイベントを仕掛ける
  buttonDelete.addEventListener('click', () => {
    // クリックされた時に動く処理
    alert("削除!")
  })

  // 更新ボタンにクリックイベントを仕掛ける
  buttonUpdate.addEventListener('click’, () => {
    // クリックされた時に動く処理
  })
});

https://i.gyazo.com/70b742cfd76215f7ec8d189e4be853bb.png

↑クリックイベントを仕掛けた後、実際にボタンをクリックすると、こんな感じで画面上部にポップアップが出現する。

演習の考え方まとめ

(なぜか画像が陰った・・・)

https://i.gyazo.com/d113b002035105bba0b6df03e89834ce.png

↑アラートボタンをクリックするとテキストエリア内に記載した文字がポップアップとして出現する

やってみたこと(失敗)

<p>テキストエリアの内容をアラートで出す</p>
<textarea id="textarea"></textarea>
<button id="button">アラート</button>
document.addEventListener('DOMContentLoaded', ()=>{
  console.log("DOMContentLoaded")
  
  const buttonAlert = document.getElementById('button')
  const textarea = document.getElementById('textarea').innerText

  buttonAlert.addEventListener('click', ()=>{
    alert(textarea)
  })
});

ボタンが反応しなかった・・・
原因はよく分からないけど、パソコン再起動で解決した

検証でコードを確認した

https://i.gyazo.com/2c48c5b42c1260669e1aa74252af56f6.png

innerTextメソッドを使ってしまったので、タグに囲まれた内容を表示しようとしているのが間違いだったのかも?

検証の内容を参考に下記のように書き換える(成功)

参考というか検証のカンニングです。

<textarea id="textarea"></textarea>
<button id="button">アラート</button> 
document.addEventListener('DOMContentLoaded', () => {
    console.log("DOMContentLoaded")
    const button = document.getElementById("button")
    button.addEventListener('click', () => {
      const text = document.getElementById('textarea')
      alert(text.value)
    })
});

https://i.gyazo.com/576803e82e7fe487c1e6a03b9a2b08a1.png

https://i.gyazo.com/9d99317c38474417384dbdad212c1ec7.png

できた!

考え方

  • テキストエリア内の内容を特定したいのでidを指定
  • ボタンも特定したいのでidを指定

  • アラートを押した時に何かしたい

  • まずアラートのDOMを取ってくる
    const buttonAlert = document.getElementById('button')

↑これに対してイベントを設定したい

const text = document.getElementById('textarea');
buttonAlert.addEventListener('click', ()=>{
  const value = text.value // 一々定数に入れなくてもOK
  alert(value)
});

最初からalert(text.value)でいい。

演習2

全く分からなかったので、検証で調べた。

https://i.gyazo.com/b695bfc5467caa4a498ca5a373097222.png

<label for="amount">数値</label>
<input type="number" name="amount" id="js-amount">
<button id="js-add-button">増やす</button>
<button id="js-minus-button">減らす</button>

<p>結果</p>
<p id="js-result">0</p>

まず、何はともあれDOMを取得しておく

const jsAddButton = document.getElementById('js-add-button')
const jsMinusButton = document.getElementById('js-minus-button')
const jsAmount = document.getElementById('js-amount')
const jsResult = document.getElementById('js-result')

jsAddButton.addEventListener('click', ()=>{
  // 試しにここでalert('jsAmount.value')など入れてみてちゃんと動くか、またどう表示されるのか確認する
  // 少しずつ少しずつ実装しよう◎
  // jsResultのinnerTextに対してjsAmountの値を入れている
  jsResult.innerText = jsAmount.value
})

この時点で、inputに入力した内容を結果に表示させることには成功

https://i.gyazo.com/a769ee62b6a2b218a10c2c77e1061572.mp4

const jsAddButton = document.getElementById('js-add-button')
const jsMinusButton = document.getElementById('js-minus-button')
const jsAmount = document.getElementById('js-amount')
const jsResult = document.getElementById('js-result')

// 現在値を取得。最初は0で設定する
var currentValue = 0

jsAddButton.addEventListener('click', ()=>{
  jsResult.innerText = currentValue + jsAmount.value
})

↑のようにしてみると・・・

https://i.gyazo.com/ceb18d08737aeb5c198d7a1523aee632.mp4

上手くいかない。
jsAmount.valueはあくまでも文字列として取得した値なので、文字列のまま足されてしまい、思う結果にならない。このため、文字列を数値に変換する作業が必要。

parseIntを使う

currentValue = currentValue + parseInt(jsAmount.value)
jsResult.innerText = currentValue

https://i.gyazo.com/cf6fd88f196ab22f0d32b17735e48600.mp4

できた!感動😳

減らすボタンは増やすボタンをコピペして、AddをMinusに、+をーに変更すればOK

https://i.gyazo.com/4460af2e2b31fe9aad50b447207e5b0d.mp4

演習3

何も分からない笑

https://i.gyazo.com/ce9f8167da9d585e5e6b4f5da5041584.png

<input type="text" placeholder="GitHubアカウント名" id="github-name">
<button onclick="fetchRepositories()">リポジトリ取得</button>
<ul id="repository-lists">
  <!-- 例 -->
  <!-- <li><a href="url************">repo-name</a></li> -->
</ul>

https://i.gyazo.com/37254cced380bb39bc2e549e9a7667de.png

const fetchRepositories = async () => {
  const githubName = document.querySelector("#github-name").value
  try}

感想

一気にやろうとすると死ぬので、まず一つ決めてどう記載すれば実現できるのか、細かく分けて考える。分からないことだらけなので、タスクを細分化するのがまず難しいです。慣れるしかなさそう。とはいえ、仕組みは解説動画を見て腑に落ちました👏

Procとは

Procとは?

  • Procとはブロックを持ち運びに便利なオブジェクトにしたものです。
  • ProcはクラスなのでProc.newでオブジェクトを作る事が出来ます。
  • Proc.newによって作成されたProcオブジェクトはcallで呼び出すことが出来ます。 https://qiita.com/k-penguin-sato/items/7f98335ef631ea5ce7ad

ブロックはdo ~ end{...}の塊のこと。これ自体はオブジェクトではないらしい。オブジェクト指向Rubyにおいてオブジェクト以外のものは存在できないらしい。
(正直ここには未だにピンときていない)

調べれば調べるほどよく理解できずに迷宮に迷い込んでいたけれど、ブロックは確かに持ち運びづらそう。ブロックをprocでオブジェクト化して持ち運びやすくしよう!みたいな説明が一番分かりやすかった。

Procの使用例

# ブロックをProcオブジェクト化して変数に代入している
sample_proc_1 = proc { |n| n * n }
sample_proc_2 = Proc.new { |n| n ** 3 } 

# Procオブジェクトはcallメソッドで呼び出せる
sample_proc_1.call(2) #=> 4
sample_proc_2.call(2) #=> 8

持ち運びしやすい!!!

メソッドから呼び出すとは?🙄

def sample_proc(&my_proc)
# (&引数名)のようにメソッドの引数を宣言する
# ② 呼び出し元で指定されたブロックがProcオブジェクトに変換されてる

  puts my_proc.call(2)
# ③ 渡されたブロック引数をcallメソッドで呼び出すことができる
end

sample_proc { |n| n * 2 }
# ① sample_procメソッドを呼び出し時にブロック{ |n| n * 2 }を引数として渡している

なるほどeachメソッドやmapメソッドもこんなふうに定義されているのかなと思った。

def each(&引数名)
  # 何らかの処理
end

配列.each { ブロック }

実際の内部構造を見てみたかったけれど、調べても出てこなかったので合っているのかは分かりません。

感想

ransackのカスタム述語に出てきたprocが何か分からずに調べましたが、かなり難しく感じました。Rubyのオブジェクトについて勉強する時、実際の使用例が見えづらいのが理解に苦しむ要因だなと思います。

yieldやIambdaや何やは一気に考えるとどんどん迷い込みそうだったので省いて考えました。procProc.new以外にもProcオブジェクトを作る方法はあるとだけ覚えておきます。。

あと今はぼんやりと理解できたようなできていないような、数日後にはまた訳分からなくなってそうな程度の理解度です。難しい🥲

参考にしたサイト

【Ruby】eachとmapの違い - Qiita

RubyのProcとは?ブロック・Proc・lamdaの違いをマスター | ポテパンスタイル

【ブロック?yield?】RubyのProcオブジェクトをしっかり理解する | shin>>media

【Ruby】ブロック・Proc・lambda を理解する - Qiita

権限のプルダウン(メモ書き)

権限のプルダウンを作るために調べたことまとめ

ransack軽く復習

ransackは検索機能を実装できるgem

コントローラ

# ビューファイルから送られてくるパラメータqをもとにテーブルからデータを検索する
# whereメソッドのransack版なイメージ
@q = User.ransack(params[:q])

# ransackメソッドで取得したデータをresultメソッドを用いてActiveRecord_Relationのオブジェクトに変換する
@results = @q.result

ビュー

<!-- ransackで定義されているsearch_form_forメソッドを用いる -->
<%= search_form_for @q, url: ○○_path do |f| %>
  <%= f.label :name_cont, 'ユーザー名'%>
  <%= f.search_field :name_cont %>
<!-- 文字入力であればtext_field、数値入力であればnumber_fieldでも可 -->

  <%= f.submit '検索'%>
<% end %>

_contメソッド

ransackで用意されているメソッド
検索したワードが含まれているレコードを取得するためのメソッド。

name_contと、_の前に指定したカラムに対してあいまい検索(検索したワードが含まれたデータが取得)される

_eq

完全に一致する検索結果を表示したい場合に用いる。

name_eqのように記述

詳細に検索をかける

<%= search_form_for @q, url: ○○_path do |f| %>
  <%= f.label :name_cont, 'ユーザー名'%>
  <%= f.search_field :name_cont %>
<!-- 年齢を検索するフォームも追加 -->
  <%= f.label :age_eq, '年齢' %>
  <%= f.number_field :age_eq %>
  <%= f.submit '検索'%>
<% end %>

セレクトボックスを作成する

f.seletcでセレクトボックスを作ることができる

<%= form_with model: @user, url: ○○_path, do |f| %>
 <%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>

どう使うのか、
見慣れないものがたくさんついている。。。

まず、_18nって何?

【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記

enumI18n対応させるために用いるenum_helpジェムをインストールすることで使える

enum
class User < ActiveRecord::Base
  enum role: { general: 0, admin: 1 }
end
# roleカラムに入っている数値によって値が定数名が表示される
# roleが0の場合
user.role
=> "general"

この文字列を日本語で表示させたい時に用いるのがコレ!

gem 'enum_help'

翻訳ファイルに追加

ja:
  enum:
    user:
      role:
        general: '一般'
        admin: '権限'

呼び出す際、末尾に_18nを付ける

# roleカラムの値が0の場合
user.role_i18n
=> "一般"
invert

enum_helpによって使えるヘルパー

User.roles_i18n.invert
=> {"一般"=>"general", "管理者"=>"admin"}

以上を踏まえてセレクトボックスについて

<%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>
<!-- <%= f.select :カラム名, セレクトボックス表示に使うデータ配列orハッシュ, {オプション},{HTMLオプション}%> -->

セレクトボックスの表示に使うデータ配列orハッシュ

% rails c
irb(main):009:0> User.roles
=> {"general"=>0, "admin"=>1}
irb(main):010:0> User.roles_i18n
=> {"general"=>"一般", "admin"=>"権限"}
irb(main):011:0> User.roles_i18n.invert
=> {"一般"=>"general", "権限"=>"admin"}
irb(main):012:0> User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}
=> [["一般", 0], ["権限", 1]]

↑二次元配列というやつが出来上がった。

セレクトボックスの表示に使うデータはこの二次元配列を使う。必ず配列かハッシュを指定する。

オプション

セレクトボックスのオプション

{ include_blank: true, selected: 0 }
  • include_blank: 先頭を空の選択肢にする
  • selected: デフォルトで選択しておきたい値を設定

include_blank: ここに文字を入れると先頭の選択肢名が入る https://i.gyazo.com/de33f577dd6d24f59976464920db3311.mp4

HTMLオプション

selectタグにid, classオプションを指定したい時に使う

{ class: 'form-control mr-1' }

※ 必ず通常のオプションを設定しない場合でも空の波括弧{},をオプション部分に設定すること
※ これがないと、HTMLオプションが無視される

localizeメソッド (i18n主要メソッド)

日時や時刻を設定した言語のフォーマットに変換する https://pikawaka.com/rails/i18n

rails-18nのgemをインストール済みなので、所定の翻訳を参照してくれる

irb(main):001:0> Date.today
=> Sat, 12 Mar 2022
irb(main):002:0> Time.now
=> 2022-03-12 20:44:53 +0900
irb(main):003:0> I18n.l(Date.today)
=> "2022/03/12"
irb(main):004:0> I18n.l(Time.now)
=> "2022年03月12日(土) 20時45分24秒 +0900"

default以外のフォーマットを指定する場合はI18n.l()の第二引数にformat: :○○と指定する。

ja:
  activerecord:
    errors:
      messages:
       # 中略
  date:
       # 中略
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"

https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml

irb(main):005:0> I18n.l(Date.today, format: :long)
=> "2022年03月12日(土)"
irb(main):006:0> I18n.l(Date.today, format: :short)
=> "03/12"

irb(main):007:0> l(Date.today, format: :short)
Traceback (most recent call last):
        1: from (irb):7
NoMethodError (undefined method `l' for main:Object)

※ コンソールでは先頭のi18nの記載が必要だが、コントローラやビューファイルでは省略可◎

<%= @board.created_at %>

https://i.gyazo.com/5e3adf75542e3d7f46efb5a79a5c3f1e.png
直感的に分かりづらい表示

<%= l @board.created_at, format: :long %>

https://i.gyazo.com/92495cac717b322b3f2ba07f9cd0c0fa.png
見やすい!!

参考にしたサイト

セレクトボックス

【Rails】完全理解 formでセレクトボックスをつくるselectの使い方 | WEB屋のメモ帳

ransack

【Rails】 ransackを使って検索機能がついたアプリを作ろう! | Pikawaka

enumi18n対応

【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記

localizeメソッドについて

【Rails】 I18n入門書~日本語化対応の手順と応用的な使い方 | Pikawaka