М. 7 Ajax Часть 2 - Evanto/qna GitHub Wiki
Сегодня углубимся в использование Ajax в Rails, работу Capybara и то, зачем нам нужен гем database_cleaner.
Как помним, Capybara предоставляет нам язык DSL с функциями, позволяющими взаимодействовать со страницей. Но как именно Capybara взаимодействует со страницей? Она использует драйверы (некие процессы, позволяющие рендерить страницы и эмулировать действия юзера, выполняя действия с этой страницей). Есть несколько таких драйверов: одни поддерживают тестирование js, другие - нет. По умолчанию используется драйвер Rask::Test, он не поддерживает js.
Rask::Test - довольно простой драйвер, все взаимодействие в нем происходит на уровне рендеринга строк в памяти - если происходит клик на какую-то страницу, то это поиск текста на странице через разбор страницы через регулярные выражения. Преимущество этого драйвера - довольно быстро выполняются acceptance-спеки, главный недостаток - нет поддержки js.
Другой драйвер, Selenuim, по умолчанию используется для js, т.е. когда мы помечаем 1 или несколько сценариев как js: true
, Capybara автоматически начинает использовать Selenium. И есть другие драйверы - например, Capybara Webkit, который можно подключить отдельно (он не идет по умолчанию). CW поддерживает js и у него есть преимущества перед Selenium. Есть еще драйвер Phantom.js, он в последнее время набирает популярность, но мы его сегодня не будем разбирать. Мы сегодня разберем Webkit.
Selenium запускает для тестов лёгкий веб-сервер на каком-либо порту (т.е. поднимает наше приложение в отдельном процессе, как это делает rails s на 3000-м порту). В качестве клиента (браузера) он использует Firefox. Почему Firefox? Это один из первых браузеров, реализовавших поддержку API WebDriver (стандарт W3C). API WebDriver служит для удаленного управления браузером (клики на ссылки, переходы по страницам и т.д. - все действия, которые мы эмулируем в браузере для тестирования). Минусы Selenium:
- Необходим Firefox
- Тесты долго запускаются и выполняются
- Запуск Firefox требует много оперативной памяти. Поначалу это может не напрягать, но если у вас не очень мощная машина, а число тестов растет, вы это быстро заметите (или если у вас станет довольно много тестов js). Кроме того, у Selenium есть такая особенность: после прохождения набора js-тестов Firefox завершается, и если после этого идёт серия не-js тестов, а потом снова js-тесты, то Firefox запускается заново. Обычно именно так и бывает - тесты идут вперемежку, и Firefox то открывается, то закрывается. Иногда даже открывается несколько вкладок в Firefox. Отсюда низкая скорость работы и нагрузка на оперативку.
Но есть выход - альтернативы селениуму (другие движки для тестирования) - например, Capybara Webkit. CW работает примерно так же, как Selenium - тоже запускает тесты в отдельном процессе, но в качестве браузера использует Webkit, причем его облегчённую версию без пользовательского интерфейса (мы не видим, как запускается браузер и как юзер взаимодействует со страницей). Webkit запускается в фоне и запускается только движок рендеринга. Запускается быстро (экономит время на инициализацию), и некоторое время не закрывается после окончания js-теста, чтобы если снова пойдут js-тесты, их подхватил этот же открытый инстанс браузера. В итоге CW выполняет тесты гораздо быстрее, чем Selenium. И в случае с CW нам не надо ставить браузер в систему - он сам его ставит. Но есть минус: требует наличия в системе библиотеки Qt (её нужно поставить). Сегодня мы перейдем на CW и посмотрим, насколько быстрее станут запускаться наши тест.
Есть ещё одна проблема при тестировании acceptance-тестами js: если мы используем тест-драйверы, отличные от Rack::Test, данные не очищаются между тестами. Т.е. после запуска теста мы сталкиваемся с тем, что у нас остались в бд данные прошлого теста, либо, если мы не переключили опцию true/false в конфиге RSpec-а (который на прошлом занятии мы добавляли в rails_heper.rb), данные очищаются, но они не видны в браузере при запуске теста.
Чтобы понять, почему так происходит, разберемся, как работает тестовая база RSpec. По умолчанию, все тесты оборачиваются в транзакции базы данных. Как устроена транзакция бд? Транзакция имеет входную точку (начало транзакции) и точку выхода. Точка выхода может либо применить транзакцию (записать данные в бд), либо отменить транзакцию (тогда данные не сохранятся в бд). Между этими 2-мя точками и происходит весь код теста (поэтому и говорят, что тест обернут в транзакции). После выполнения кода теста должна быть выполнена либо команда сохранения данных в бд (commit), либо команда отмены всех изменений (rollback). Сами транзакции работают в памяти и создают копию таблиц в базе - внутри транзакции код может видеть другие данные из бд, что-то там менять и сохранять, но в реальности эти данные не попадут в базу, пока не будет выполнен commit. То есть внутри транзакции база находится в “грязном” состоянии, когда происходят какие-то процессы с бд, но пока не применны ни команда сохранения, и команда роллбэка. Из других процессов это состояние не видно, оно существует только внутри транзакции.
Все тесты RSpec обернуты в такие транзакции, и после тестов транзакция отменяется. Для чего это делается? Чтобы после тестов автоматически очищалась бд, ибо один из принципов тестирования - тесты должны быть изолированы друг от друга - ни один тест (и его выполнение или невыполнение) не должен влиять на другие. Каждый тест должен начинаться с чистой бд. Что и происходит при условии обернутости теста в отменяющуюся в конце транзакцию. Второе преимущество - обернутости - скорость, внутри транзакции все происходит очень быстро (практически мгновенно), тк. при тесте не используются реальные записи в таблицу. Именно поэтому юнит-тесты у нас такие быстрые.
Однако, при тестировании js у нас возникает проблема. Транзакции в том варианте, который я описал выше, работают только в рамках одного процесса (точнее, в рамках одного подключения к бд). В рамках нескольких процессов это уже не работает.
Selenium и WC создают 2 процесса (сервер и клиент) и 2 подключения к бд (клиент = браузер, 2 подключения к бд - одно для выполнения теста при помощи RSpec с оборачиванием в транзакции, второе сервер Selenium или WC браузер). Это 2 разных отдельных процесса. Получается, что данные в нашем тесте недоступны для клиента. Транзакции, в которые у нас обернуты тесты и данные, которые создаются для теста в этих транзакциях - это 2 разных процесса. Таким образом получается, что бд не очищается, либо очищается некорректно. И какое тут решение?
Решений тут 2: одно тупое, второе рабочее:
- Очищать бд вручную после каждой транзакции (пересоздавать бд или delete)
- Юзать гем
database_cleaner
Ручная очистка, само собой, не вариант. Вариант - юзать database_cleaner
, который мы подключили на прошлом занятии.
database_cleaner
- ORM-независимое решение для очистки бд, работает не только с ActiveRecord, а может и чистить бд для js-тестов. DC умеет очищать бд несколькими способами, которые называются стратегиями. У DC есть следующие стратегии:
- Transaction (оборачивание каждого теста в транзакцию, как в RSpec с rollback в конце)
- Truncation (sql-выражение
TRUNCATE
устанавливает размер текущей таблицы, если поставить truncate 0, то все данные из таблиц исчезнут), truncate быстрее чем delete. - Deletion (sql-выражение
DELETE
)
При truncate
и delete
данные будут реально создаваться в бд (будут доступны, пока мы их не удалим) и потом удаляться.
Теперь откроем rails_helper.rb
и, глядя в него, продолжим разговор о DC.
Вспомним, что мы делали с DC на прошлом занятии. Сначала мы выключили (переключили на false) опцию оборачивания тестов в транзакции (эта опция отвечает за то, чтобы каждый тест оборачивался в транзакции):
config.use_transactional_fixtures = false
Сделали мы так для того, чтобы передать управление оборачиваемостью тестов гему DC, и чтобы в случае запуска js-спек у нас данные были доступны в обоих процессах - и в процессе запуска теста, и там, где он выполняется (браузер).
Во-вторых, мы подключили и настроили гем DC в файле rails_helper.rb
(с помощью несколько методов before и after - таких же, как мы используем в наших спеках):
config.order = “random”
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
Когда мы пишем before и сразу блок - это то же самое, что before(:each)
. Мы можем передать в before дополнительные параметры, которые будут управлять тем, когда будет выполнен код внутри before. Если мы пишем before(:each)
или просто before
, то код в before
выполняется перед каждым тестом. А before(:suite)
выполняется перед запуском всего файла спек, с самого верхнего describe
. Строкой before(:suite)
мы очищаем все данные бд (все её таблицы) с помощью метода truncation (ставя все таблицы в 0 байт):
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
Дальше перед каждым типом теста применяем ту или иную стратегию очистки (по умолчанию это transaction
):
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
Для js-тестов (если у нас js: true
) ставим truncation
:
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
Также перед каждым тестом запускаем DC методом start и после каждого теста запускаем clean
:
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
В момент start DC начинает следить за изменением бд - тем, какие таблицы у нас были затронуты.
Clean после каждого теста делает очистку в соответствии с той стратегией, которую мы назначили этому типу теста выше.
Вынос конфигов acceptance-тестов в отдельный хепер acceptance_helper.rb
.
Все это хорошо, но есть нюанс. Сейчас у нас все эти настройки DC закинуты в файл rails_helper.rb
. Но разные тесты (юнит-тесты, acceptance-тесты, тесты моделей, тесты контроллеров) часто требуют разных конфигураций, подключения разных модулей. По сути, код конфига DC нам нужен только для acceptance-тестов. Чтобы у нас все это работало в юнит-спеках, нам достаточно тут заменить false
на true
:
config.use_transactional_fixtures = false
Acceptance-спеки стоят особняком от остальных тестов. Их и запускают реже, и конфигурация, как правило, несколько отличается от юнит-тестовой. Если кидать все конфиги в rails_helper.rb
, то файл разрастется, станет неаккуратным, с ним станет неудобно работать. Поэтому рекомендую выносить конфиг для acceptance-теста в отдельный хелпер - acceptance_helper.rb
, а в rails_helper.rb
хранить, кроме общей конфигурации, только ту, что используется для юнит-тестов.
Давайте это сделаем - вынесем конфиг acceptance-тестов в отдельный хелпер. Создадим хелпер acceptance_helper.rb в папке acceptance. Этот файл будет брать всё, что у нас есть в rails_helper
и немного менять конфигурацию. Пишем в нём сверху require ‘rails_helper’ - это значит, что у нас сюда подключится весь файл rails_hepler.rb
. Ниже расширяем конфигурацию RSpec:
Так как у нас сейчас в rails_helper уже есть код конфига под acceptance-тесты, мы его копируем в acceptance_helper (конфиг DC и строку настройки оборачивания транзакций). А в rails_helper вернём оборачивания обратно на true. Перенесем заодно и acceptance macros:
Чего мы добились вынесением конфигов для acceptance-тестов в отдельный хелпер? станут чуть быстрее стартовать юнит-тесты, потому что не будет лишней инициализации В них будут по умолчанию использоваться транзакции для очистки бд, что чуть быстрее стратегии transaction, но через DC, без лишних запросов к бд.
Стало более компактно и удобно. Вся конфигурация, относящаяся к acceptance-тестам, теперь в отдельном файле, в который удобно добавлять относящиеся к ней вещи. При этом всё, что осталось в rails_helper - все модули, которые там лежат, мы можем использовать и в acceptance-тестах, и acceptance_helper.
Теперь нам нужно во всех файлах acceptance-тестов (все файлы папки acceptance) заменить require ‘rails_helper’ на require_relative 'acceptance_helper':
Если после этого запустим rspec spec/acceptance - все тесты зеленые, а время запуск уменьшилось (записать время запуска тестов до этих изменений и сравнить, насколько уменьшилось).
Теперь поменяем драйвер Selenium на Webkit. Для этого в Gemfile в группу test, development поставим гем capybara_webkit:
И удалим гем selenium-webdriver.
Делаем bundle install и в acceptance-helper добавляем опцию webkit для js-драйвера капибары:
Cнова запустим rspec spec/acceptance. Они зеленые. Обратим внимание, что сейчас у нас не будет запускаться браузер, и время выполнения сократилось до 3.05 сек (это не настоящее время, настоящее можно засечь в unix-системах при помощи утилиты time). Если запустим второй раз, выполнятется еще быстрее: 2:45. Это потому, что Webkit не выгружается из памяти сразу после тестов. Если вы запускаете довольно много js-тестов, вам просто необходим этот драйвер capybara_webkit, иначе все будет работать очень медленно.
На этом всё с теоретической и подготовительной частью. Перейдем к коду. У нас осталось 2 момента, непокрытых в предыдущем занятии: Вывод ошибок в Ajax У нас сейчас сделано так, что если мы пишем ответ, то он просто добавляется к списку ответов, и если возникает ошибка (например, отправляем пустой ответ), она никак не проявляется. Можем это проверить - если при пустом ответе жмем create, ничего не происходит. Копнем глубже, что тут происходит: откроем web-инспектор -- netrwork -- XHR и повторим. Когда мы жмем create, у нас уходит запрос POST на создание нашего ответа:
У нас приходит ответ, который в соответствии с содержанием файла views/answers/create.js.erb рендерит список ответов для вопроса:
Но так как ответ был пустой, валидация не прошла, он был не создан, он и не рендерится. Логически всё работает правильно, но хотелось бы сообщить юзеру, что ответ не может быть пуст. Для этого давайте напишем ещё один сценарий (js-сценарий ) в spec/acceptance/create_answers_spec.rb - “юзер пытается создать невалидный ответ”:
Заставим этот тест пройти. Для этого в create.js.erb нам нужно определить, есть ли у только что созданного ответа ошибки, и если да, вывести сообщения. Сделать это довольно просто: так как это файл .erb, мы можем в нём использовать любые erb-конструкции, чем и воспользуемся. Сделаем проверку на наличие ошибок в созданном объекте ответа. Если объект имеет ошибки, делаем какие-то действия, в остальных случаях выполняем действия, которые у нас уже были записаны в этом файле:
Почему мы делаем эту проверку через if, а не через unless? Чтобы проверить 2 варианта: в первой ветке вариант когда у нас все хорошо и ошибок нет, во второй - обработку ошибок. Дело в том, что категорически не рекомендуется использовать unless c else. При unless, у нас должна была бы быть только 1 ветка выполнения кода, без else. Unless (если не) и так подразумевает лишь 1 ветку, а если бы у него было еще и else, это бы все очень запутало. Поэтому, если ваше условие подразумевает 2 варианта, успешный и неуспешный (с наличием else), используйте if.
Итак, если ответ содержит ошибки, то что мы должны сделать? Мы должны эти сообщения об ошибках перебрать и добавить на страницу. Перебрать мы их можем через метод full_messages, используемый по умолчанию. Он подходит, если у нас нелокализованная версия. С локалями может быть чуть сложнее. По умолчанию, full_messages берет название поля и сообщение об ошибке, склеивает их и получает сообщение. В русском языке так не всегда бывает удобно построить предложение, поэтому, если у вас есть русская локаль, full_messages вам не подойдет, вам нужно будет просто перебирать errors и брать оттуда сообщения об ошибках. А мы сейчас сделаем через full_messages. Итак, мы должны перебрать все сообщения об ошибках и вывести их на экран. Будем использовать errors.full_messages, то есть массив сообщений об ошибке, и перебирать его с помощью .each do. Так как мы используем erb, не забываем ставить закрывающие теги <% end %> (в слиме и хамле не используются end-ы).
Тут мы уже можем получить месседж, но надо решить, где на странице мы хотим его вывести. Выведем над полем ответа. Для этого используем метод before: в качестве элемента возьмем тело ответа (answer_body), и с помощью before добавим html-код - текст нашего сообщения об ошибке:
Запустим тест снова. Он не проходит, но непонятно, почему. Посмотрим, что в браузере. Тут у нас 500-ая ошибка:
Если посмотрим в лог консоли, там тоже эта ошибка:
И чуть ниже - подробности:
Ошибка в первой же строчке - не определен метод errors:
Судя по этой ошибке, у нас не инициализирован @answers, в нем ничего не лежит. Почему ошибка пишет, что не инициализирован errors а не answers? Раз у нас проблема с переменными, давайте вернемся в answers_controller. Действительно, тут в методе create мы просто ищем question и создаем ответ, но не создаем переменную @answer:
Давайте её добавим:
Посмотрим руками в браузере, заработали ли у нас ошибки при отправке невалидного ответа. Видим - заработало. Но в тестах у нас отличается текст сообщения. Поменяем сообщение в тесте на то, которое выдается в реальности в spec/acceptance/create_answers_spec.rb. Снова запускаем тест.
Видим, что теперь он проходит. Итак, в create.js.erb мы успешно проверили наличие ошибок и вывели их случае наличия:
Соответственно, в коде контроллера мы эти проверки уже не делаем. Слегка причешем вьюху questions/show.html.slim, а то сообщение некрасиво выводится сбоку от формы. Было:
Под test-area выведем блок answer-errors - пустой див для вывода ошибок. Стало:
И поменяем в create.js.erb answer_body на паршл ‘answer-errors’, а before на html (мы внутрь этого блока answer-errors добавляет html):
Проверим в браузере. Теперь у нас все нормально:
Теперь у нас проходит тест:
И rspec spec тоже проходит (за 3.18 сек). Делаем коммит “answer errors” (но первый коммит стоило сделать после рефакторинга хелперов).
Редактирование ответов - update.js.erb Теперь у нас нормально добавляются вопросы, но юзеру надо дать возможность редактировать свои ответы. Создаем acceptance-спеку edit_answer_spec.rb. Добавляем хелпер через require_relative:
“С целью исправить ошибку как автор ответа я хочу отредактировать мой ответ”:
Что нам понадобится? Возьмем часть данных из create_answer_spec.rb. Нам понадобится пользователь и вопрос:
И добавим ответ для редактирования, в качестве связанного вопроса передадим созданный строкой выше вопрос:
Сценарии: 1) неаутентифицированный юзер пытается редактить не свой ответ 2) свой ответ 3) . Вот у нас есть 3 сценария:
Начиная писать спеку, всегда сначала подумайте, какие вообще существуют сценарии, сколько их, и запишите все сценарии, которые будут (как на скрине выше) без реализации.
Тест 1 - неаутентифицированный юзер пытается редактить вопрос. Проверяем, что на странице нет ссылки на редактирование. Запустим спеку. Желтым в ответе отмечены неактивированные спеки. Первая спека прошла. Спека 2 - автор пытается отредактировать ответ. Проверяем, что на странице есть ссылка редакирования. Добавим эту ссылку, чтобы тест проходил. Эта ссылка должна быть у каждого ответа. Так как список ответов у нас выводится в паршле _answers, туда и добавим:
0:40 Чтобы у нас все верно работало, сначала вернемся к тесту и добавим within, чтобы ссылка появлялась именно внутри блока с классом .answers. Обратите внимание, мы делаем только то, что сработает и ничего лишнего. Только потом, по мере увеличения тестов, мы увеличиваем функционал. Запускаем тест, вторая спека не прошла:
Это потому, что во вьюхе вы вывели просто ссылку, а надо - ссылку для залогиненных юзеров. Добавляем проверку:
Можно так, а можно через current_user_present - это хелпер devise, который можно использовать во вьюхах и контроллерах. Таким образом, мы теперь будем выводить ссылку редактирования только залогиненным юзерам. 42:30 Теперь у нас проходит уже 2 теста.
Теперь у нас есть ссылка Edit, но она пока ничего не делает. Следующим шагом было бы логично написать сценарий для собственно процесса редактирования. Но мы пока не знаем, как это сделать, перейдем к сценарию “аут. юзер пытается редактить вопрос другого юзера". Тут можно добавить еще сценарий для проверки, что юзер видит ссылку Edit только если он автор. Оставим это как домашнее задание, а пока облагородим наши спеки.
Вынесем сценарии для аут. Юзера в отдельный describe и в нем вынесем наверх before, в который вынесем логин юзера и посещение страницы:
Попробуем сделать сценарий редактирования (с аяксом). Написали базовый вариант:
Можно добавить еще несколько сценариев: 1) что не только что виден новый отредактированный ответ, но и что не виден старый 2) убедиться, что новый отредактированный ответ находится не в поле редактирования answer, и что мы больше не видим эту форму. Добавим эти ожидания:
Последний экспект проверяет, что у нас там нет формы. Запустим тест, падает потому что мы еще и не писали функционал редактирования. Спека падает на строке 34, тк у нас нет поля Answer:
Добавим форму для редактирования вопроса в questions/_answers.html.slim. У нас вложенные ресурсы, поэтому используем [@question, answer] - они берутся из списка выше. По сути, для каждого вопроса мы создаем форму. Форма будет обновляться через Ajax, поэтому сразу пишем remote: true.
У нас в цикле выведутся все формы, при нажатии на кнопки которых пока ничего не происходит:
Запустим спеку, чтобы увидеть, что у нас теперь не проходит - жалуется, что нашел 2 поля Answer:
Добавим в тест within, чтобы отбить одно от другого и выбрать нужное поле из двух:
Снова запускаем тест и смотрим, что не проходит:
Ошибка missing template. Это потому, что тест у нас работает не через js - добавляем js: true:
Снова запускаем - теперь не проходит, потому что старый текст у нас виден:
За него отвечает эта строка:
Откроем в браузере, посмотрим что будет, если отредактировать ответ и отправить:
Рельсы выдали 404 ошибку:
Смотрим в логи - там ActionNotFound:
Естественно, ведь у нас в контроллере нет метода update, потому что мы его не писали. Прежде чем добавить его в контроллер, пишем спеку в answers_controller_spec.rb.
Что должен делать этот метод? Найти объект answer и обновить его атрибуты. Скопируем его из спеки контроллера questions и адапритуем под answers, также вынесем наверх спеки создание вопроса и наверх метода - создание ответа, связанного с вопросом (чтобы это сработало, нам надо, чтобы question уже был создан, поэтому верху файла создаем вопрос через let! с воскл. знаком):
0:50 Запускаем - падает, метода update же нету. Добавляем в контроллер answers метод update. Он загружает по id ответ и обновляет объект (ищем и обновляем исходя из параметров):
Создаем файл answers/update.js.erb. В базовом случае он должен делать почти то же, что и create, но чуть иначе. Скопируем код из create и посмотрим, что изменить. Логика та же самая, только тут нам не нужно присваивать answer_body пустое значение, удалим эту строку:
Запускаем, тут ошибка, возникла потому что в спеке контроллера надо было передать id ответа, правим:
И тут еще забыл поменять на answer, правим:
После этого все спеки контроллера проходят.
В процессе разработки мы сначала проверяем вручную, работает ли функционал, и по завершении написания тестов и функционала запускаем acceptance-спеки. Запускаем acceptance - видим, что там еще одна ошибка, но непонятно, почему. Смотрим в браузере. Дело в том, что когда мы рендерим update.js.erb, мы рендерим паршл _answers, а он начинается с @question.answers, который не определен (выдает ошибку undefined method answers for nil:NilClass).. Это значит, что переменная questions пустая, а значит, мы не установили ее в контроллере.
И действительно, в контроллере она не установлены:
Исправим это, но сначала напишем спеку ‘assigns to question ’, которая поможет избежать эту ошибку, в answers_controller_spec чтобы убедиться, что мы установили все нужные переменные. Проверяем, что у нас в @question есть question:
Установим @question в контроллере answers_controller.rb - он равен связанному с ответом вопросу:
Теперь тесты answers_controller_spec.rb у нас проходят.
Еще раз проверим в браузере - меняем ответ и он изменяется, все получается:
Запускаем acceptance спеки, там не проходит edit_answer_spec.rb. Правим: тут нам надо перенести в блок .answers все действия и проверки - чтобы все выполнялось только там:
Теперь тест сыпется на строке - у нас все еще видно поле textarea:
Действительно, после изменения ответа наша форма никуда не исчезает и остается на странице. Нам нужно спрятать эту форму и показывать ее через js. То есть форма должна появляться при клике на ссылку edit. Для начала скроем форму в assets,
Написав для этого такое css-правило, чтобы форма скрылась:
Проверим в браузере - теперь форма не видна:
Мы хотим, чтобы форма появлялась при клике на ссылку edit. Мы это можем сделать в папке джаваскриптов, в файле javascripts/answers.js.coffee:
По умолчанию у нас в этих файлах используется coffeescript, но можно использовать и синтаксис обычного js.
- это аналог записи document ready. Итак, нам нужно показать форму по клику на ссылку, и форма должна быть показана именно к тому вопросу, у которого была нажата ссылка. Для этого нам надо взять нужный селектор и вызвать метод show, а для кнопки edit вызвать hide. Нам надо понять, как связать ссылку с конкретным вопросом. Обычно в html-5 это делается с помощью дельта-атрибутов.
В паршле _answers дополним строку : Ссылка эта не будет никуда вести, мы должны присвоить ей класс или id, чтобы можно было повесить на неё обработчик события - добавим класс и укажем, для какого конкретно ответа эта ссылка будет с помощью атрибута data с хэшем:
Проверим в браузере:
У ссылки появился класс и id вопроса - мы уже можем по ней определить id нашего ответа. Теперь нам нужно добавить такой же id для формы, чтобы знать, какую форму показывать к этому ответу. Можно сделать это через нахождение ближайшей формы от нажатой ссылки. Но надежнее сделать через атрибуты. Добавим к форме атрибут html с хэшем:
Посмотрим в браузере, как изменилась верстка:
У формы появились скрытые id:
Теперь у нас есть все, что нужно, чтобы написать наш answer.js.coffee. Повесим на все наши ссылки Edit (edit-answer-link) обработчик события на click:
Создаем функцию, в которой мы должны скрыть сам элемент (ссылку edit?) и форму . Для этого надо сначала получить id ответа, чтобы потом составить правильный id для формы. This тут - это наша ссылка edit, по которой мы кликнули. id ответа получаем через функцию data. Итого:
Теперь мы можем составить селектор для формы. Динамически создаем селектор:
И показываем форму с помощью show:
#
означает id
, те. edit-answer
- это id
формы (html-элемента). Тут edit-answer-
конкатинируется с answer_id
.
Запретим стандартное поведение ссылки:
Итого:
Проверим в браузере, что получилось. Нажимаем edit - у нас что-то показывается, но это не то, что мы хотим. Дело в том, что когда мы пишем на слиме data (в паршле _answers) и указываем id
В файле coffee мы должны это писать id не так же, а в нотации js - кэмелкейсом, т.е. так: (удаляем пробелы, слово 1 с маленькой буквы, второе с большой). Итого:
Проверяем еще раз в браузере. Редактируем ответ, сохраняем, он у нас отредактировался. Но если мы нажимаем edit повторно, второй раз форма не возникает, зато появляется лишняя форма в конце списка ответов. Это потому, что в паршле _answers мы выводим и пустые, и не пустые ответы, и у нас тут есть форма для пустого ответа:
Чтобы этого избежать, выведем данные только для тех ответов, которые были сохранены в бд, с помощью метода persisted? (сохраненный):
Проверим в браузере. Лишней формы answer уже нет, редактирование работает, но все еще не работает повторное редактирование. Это потому, что заново рендерится наш паршл и из-за этого этот код в coffe не срабатывает:
Чтобы это заработало, надо весь написанный нами в answers.js.coffee код перенести в answers/update.js.erb. Поскольку код написан на кофескрипте, надо него немного изменить на js - поменяем вызов функции и расставим точки с запятыми. Итого, в update.js.erb у нас:
Проверяем в браузере - вот теперь все работает, в том числе повторный edit. Запускаем тест acceptance/edit_answer_spec.rb Там ошибка, потому что не создается ответ - нам надо его создать с воскл. знаком: После этого спека полностью прошла.
Но у нас не хватает спек: надо проверить, что ссылка edit показывается только автору, и что только автор может редактировать этот вопрос - достаточно, чтобы неавтор не видел ссылки редактирования. Это домашнее задание. Проверку надо сделать как со стороны вьюхи (в acceptance-спеке) - что автор видит ссылки edit только у своих вопросов , так и со стороны контроллера проверить, что только автор может редактировать ответ (в controller спеке). И проверить, что неавтор, пытающийся редактировать не свой ответ, не может это сделать.