Chapter 23

4-4. 学習ログ一覧機能作成

Masuyama
Masuyama
2022.10.15に更新

学習ログ一覧機能作成

今回は、投稿した学習ログの一覧ページ作成していきましょう。

実装

ルーティング

前回追加した recources :postsindex アクションも追加してあげましょう。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root 'home#top'

  resources :posts, only: [:new, :create, :show, :index] # 追加
end

これで、/posts というパスへアクセスすると学習ログの一覧ページのアクションへ飛ぶようになります。
ページを表示するためには index アクションが必要となるので Controller にアクションと、対応する View を追加していきましょう。

Controller (#index)

PostsController に index アクションを用意してあげます。
外部から呼び出すアクション (メソッド )なので、private の上に書くことに注意してください。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:show]

  ...

  def index # 追加
    @posts = Post.limit(10).order(created_at: :desc)
  end

  private
...

また、TechLog では投稿の閲覧についてはログインを必要としないため、
show アクションと同じくauthenticate_user!から index アクションも対象外とします。

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:show, :index] # 修正
...

次は View を用意しましょう。

View

index アクションの view ファイルは用意されていないので、ファイルを作成してあげましょう。

$ touch app/views/posts/index.html.erb

中身は次のように編集してください。

app/views/posts/index.html.erb

<div class="space-y-6 w-3/4 max-w-lg">
  <label class="block text-xl font-bold text-gray-700">学習ログ一覧</label>
  <div class="items-center justify-center">
    <% @posts.each do |post| %>
      <div tabindex="0" aria-label="card 1" class="focus:outline-none mb-7 bg-white p-6 shadow rounded">
        <div class="flex items-center border-b border-gray-200 pb-6">
          <div class="flex items-start justify-between w-full">
            <div class="pl-3">
              <p class="focus:outline-none text-lg font-medium leading-5 text-gray-800"><%= link_to post.title, post_path(post) %></p>
              <p class="focus:outline-none text-sm leading-normal pt-2 text-gray-500">by <%= post.user.nickname %></p>
            </div>
          </div>
        </div>
        <div class="px-2">
          <p class="focus:outline-none text-sm leading-5 py-4 text-gray-600"><%= post.content %></p>
        </div>
      </div>
    <% end %>
  </div>
</div>

ナビゲーションバーにリンク追加

最後に、ナビゲーションバーに投稿一覧ページへのリンクを追加してあげます。
投稿一覧はログイン状態に関係なく閲覧できるため、ログイン状態を判定する if 文の外に書いてあげましょう。

app/views/shared/_navbar.html.erb

...
      <ul class="flex flex-col mt-4 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium">
        <%# ここから追加 %>
        <li>
          <%= link_to "ログ一覧", posts_path, class: "block py-2 pr-4 pl-3 text-gray-400 hover:text-white border-b border-gray-700 hover:bg-gray-700 md:hover:bg-transparent md:border-0 md:hover:text-blue-white md:p-0" %>
        </li>
        <%# 追加ここまで %>
        <% if current_user %>
        ...

Controller (#new)

最後に、投稿した後のリダイレクト先を変えましょう。
現在は一時的にルートページへリダイレクトしていましたが、投稿一覧ページへリダイレクトするように変更します。

app/controllers/posts_controller.rb

  def create
    @post = Post.new(post_params)
    @post.user_id = current_user.id # ログインユーザのIDを代入
    if @post.save
      flash[:notice] = '投稿しました'
      redirect_to posts_path # 修正

System Spec もこれに合わせて修正しておきます。

spec/system/posts_spec.rb

...
  describe 'ログ投稿機能の検証' do
  ...
    context 'ログインしている場合' do
      before do
        sign_in @user
        visit '/posts/new'
      end
      it 'ログインページへリダイレクトしない' do
        expect(current_path).not_to eq('/users/sign_in')
      end
        context 'パラメータが正常な場合' do
        it 'Postを作成できる' do
          expect { subject }.to change(Post, :count).by(1)
          expect(current_path).to eq('/posts') # 修正

動作確認

では、実際に画面で確認していきましょう。

ログイン時

開発用サーバを起動し、投稿画面から何らかの投稿をします。
その後、http://localhost:3000/postsへアクセスすると投稿の一覧画面が開きます。

一覧ページへアクセスすると、このような見た目になっています。

<img width="542" alt="スクリーンショット 2022-08-06 10 05 09" src="https://user-images.githubusercontent.com/68495563/183227578-1062527b-0d2e-4728-9d99-5a1365cc6151.png">

ナビゲーションバーにもログ一覧のリンクが追加されていることを確認しておきます。

<img width="290" alt="image" src="https://user-images.githubusercontent.com/68495563/183227667-5465081d-f65b-4f7b-b60b-e689f5869bdd.png">

テスト

さて、実装は完了したので上記の動作を保証するテストを書いていきましょう。

Request Spec

まずは、新しく作った一覧ページへのアクセスを確認する Request Spec を追加していきます。

spec/requests/posts_spec.rb

  ...
  describe 'GET /posts' do
    context 'ログインしていない場合' do
      it 'HTTPステータス200を返す' do
        get '/posts'
        expect(response).to have_http_status '200'
      end
    end
    context 'ログインしている場合' do
      it 'HTTPステータス200を返す' do
        sign_in @user
        get '/posts'
        expect(response).to have_http_status '200'
      end
    end
  end
  ...

Request Spec の全文は以下のようになります。

<details>
<summary>spec/requests/posts_spec.rb</summary>

require 'rails_helper'

RSpec.describe 'Posts', type: :request do
  before do
    @user = create(:user) # 各テストで使用できるユーザーを作成
    @post = create(:post) # 閲覧用の Post を作成
  end

  describe 'GET /posts/new' do
    context 'ログインしていない場合' do
      it 'HTTPステータス302を返す' do
        get '/posts/new'
        expect(response).to have_http_status(302)
      end

      it 'ログインページにリダイレクトされる' do
        get '/posts/new'
        expect(response).to redirect_to '/users/sign_in'
      end
    end

    context 'ログインしている場合' do
      before { sign_in @user }
      it 'HTTPステータス200を返す' do
        get '/posts/new'
        expect(response).to have_http_status(200)
      end

      it 'ログインページにリダイレクトされない' do
        get '/posts/new'
        expect(response).not_to redirect_to '/users/sign_in'
      end
    end
  end

  describe 'GET /posts/:id' do
    context 'ログインしていない場合' do
      it 'HTTPステータス200を返す' do
        get "/posts/#{@post.id}"
        expect(response).to have_http_status '200'
      end
    end
    context 'ログインしている場合' do
      it 'HTTPステータス200を返す' do
        sign_in @user
        get "/posts/#{@post.id}"
        expect(response).to have_http_status '200'
      end
    end
  end

  describe 'GET /posts' do
    context 'ログインしていない場合' do
      it 'HTTPステータス200を返す' do
        get '/posts'
        expect(response).to have_http_status '200'
      end
    end
    context 'ログインしている場合' do
      it 'HTTPステータス200を返す' do
        sign_in @user
        get '/posts'
        expect(response).to have_http_status '200'
      end
    end
  end
end

</details>

これで Request Spec は完了です。
次は System Spec を作成してあげましょう。

System Spec

Factory Bot を用いて事前に Post を2つ作成してから、一覧ページの表示内容を確認するテストにします。
System Spec に以下のテストを追記します。

spec/system/posts_spec.rb

require 'rails_helper'

describe 'Post', type: :system do
  before do
    driven_by :selenium_chrome_headless # ヘッドレスモードで実行
    @user = create(:user) # ログイン用ユーザー作成
    @post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
    @post2 = create(:post, title: 'RSpec学習完了 2', content: 'System Specを作成した 2', user_id: @user.id) # 追加
  end
  ...

  ####### ここから追加 #######
  describe 'ログ一覧機能の検証' do
    before { visit '/posts' }

    it '1件目のPostの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了')
      expect(page).to have_content('System Specを作成した')
      expect(page).to have_content(@user.nickname)
    end

    it '2件目のPostの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了 2')
      expect(page).to have_content('System Specを作成した 2')
      expect(page).to have_content(@user.nickname)
    end

    it '投稿タイトルをクリックすると詳細ページへ遷移する' do
      click_link 'RSpec学習完了'
      expect(current_path).to eq("/posts/#{@post.id}")
    end
  end
end

この時点での System Spec の全文は以下の通りです。

<details>
<summary>spec/system/posts_spec.rb</summary>

require 'rails_helper'

describe 'Post', type: :system do
  before do
    driven_by :selenium_chrome_headless # ヘッドレスモードで実行
    @user = create(:user) # ログイン用ユーザー作成
    @post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
    @post2 = create(:post, title: 'RSpec学習完了 2', content: 'System Specを作成した 2', user_id: @user.id)
  end

  # 投稿フォーム
  let(:title) { 'テストタイトル' }
  let(:content) { 'テスト本文' }

  describe 'ログ投稿機能の検証' do
    # ログ投稿を行う一連の操作を subject にまとめる
    subject do
      fill_in 'post_title', with: title
      fill_in 'post_content', with: content
      click_button 'ログを記録'
    end

    context 'ログインしていない場合' do
      before { visit '/posts/new' }
      it 'ログインページへリダイレクトする' do
        expect(current_path).to eq('/users/sign_in')
        expect(page).to have_content('ログインしてください。')
      end
    end

    context 'ログインしている場合' do
      before do
        sign_in @user
        visit '/posts/new'
      end
      it 'ログインページへリダイレクトしない' do
        expect(current_path).not_to eq('/users/sign_in')
      end

      context 'パラメータが正常な場合' do
        it 'Postを作成できる' do
          expect { subject }.to change(Post, :count).by(1)
          expect(current_path).to eq('/posts')
          expect(page).to have_content('投稿しました')
        end
      end

      context 'パラメータが異常な場合' do
        let(:title) { nil }
        it 'Postを作成できない' do
          expect { subject }.not_to change(Post, :count)
          expect(page).to have_content('投稿に失敗しました')
        end
        it '入力していた内容は維持される' do
          subject
          expect(page).to have_field('post_content', with: content)
        end
      end
    end
  end

  describe 'ログ詳細機能の検証' do
    before { visit "/posts/#{@post.id}" }

    it 'Postの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了')
      expect(page).to have_content('System Specを作成した')
      expect(page).to have_content(@user.nickname)
    end
  end

  describe 'ログ一覧機能の検証' do
    before { visit '/posts' }

    it '1件目のPostの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了')
      expect(page).to have_content('System Specを作成した')
      expect(page).to have_content(@user.nickname)
    end

    it '2件目のPostの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了 2')
      expect(page).to have_content('System Specを作成した 2')
      expect(page).to have_content(@user.nickname)
    end

    it '投稿タイトルをクリックすると詳細ページへ遷移する' do
      click_link 'RSpec学習完了'
      expect(current_path).to eq("/posts/#{@post.id}")
    end
  end
end

</details>

テスト実行

最後に、すべてのテストに通ることを確認しておきます。

$ bin/rspec
...
Finished in 12.21 seconds (files took 0.41113 seconds to load)
58 examples, 0 failures

変更をコミット

ここまでの変更をコミットしておきます。

$ git add .
$ git commit -m "学習ログ一覧機能を作成" 
$ git push