Turbo confirm

One of the coolest additions to rails and turbo is the turbo modals. Just configure the following:
lang-js
// app/javascript/application.js
Turbo.setConfirmMethod((message, element) => {
  return new Promise((resolve) => {
    const modal = new bootstrap.Modal(document.getElementById("turbo-confirm"))
    const messageElement = document.getElementById("turbo-confirm-message")

    messageElement.textContent = message
    modal.show()

    const modalElement = modal._element

    const handleConfirm = (event) => {
      if (event.target.closest(".modal-footer button")) {
        resolve(event.target.value === "confirm")
      }
    }

    const handleHide = () => {
      modalElement.removeEventListener("click", handleConfirm)
      modalElement.removeEventListener("hidden.bs.modal", handleHide)
      resolve(false)
    }

    modalElement.addEventListener("click", handleConfirm)
    modalElement.addEventListener("hidden.bs.modal", handleHide)
  })
})
Then slap a bootstrap modal into `application.html.erb`
lang-erb
<!-- app/views/layouts/application.html.erb -->
<div class="modal fade" id="turbo-confirm" tabindex="-1" aria-labelledby="turbo-confirm-label" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="turbo-confirm-label">Confirmation required</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p id="turbo-confirm-message"></p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" value="cancel">Cancel</button>
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal" value="confirm">Confirm</button>
      </div>
    </div>
  </div>
</div>
Every time we use data-turbo-submits-with on a form or a button_to, it will use this as a confirmation modal. That's great, but what if we wanted to be able to use `window.confirm` the same way? 

It is possible, and we can do almost the same thing:
lang-js
window.showConfirmModal = function showConfirmModal(message) {
  return new Promise((resolve) => {
    const modal = new bootstrap.Modal(document.getElementById("turbo-confirm"))
    const messageElement = document.getElementById("turbo-confirm-message")

    messageElement.textContent = message
    modal.show()

    const modalElement = modal._element

    const handleConfirm = (event) => {
      if (event.target.closest(".modal-footer button")) {
        resolve(event.target.value === "confirm")
      }
    }

    const handleHide = () => {
      modalElement.removeEventListener("click", handleConfirm)
      modalElement.removeEventListener("hidden.bs.modal", handleHide)
      resolve(false)
    }

    modalElement.addEventListener("click", handleConfirm)
    modalElement.addEventListener("hidden.bs.modal", handleHide)
  })
}

;["turbo:load", "turbo:render"].forEach((event) => {
  document.addEventListener(event, () => {
    document.addEventListener(
      "confirm",
      (event) => {
        const element = event.target.closest("[data-confirm]")
        if (element) {
          event.preventDefault()
          event.stopPropagation()

          showConfirmModal(element.dataset.confirm).then((confirmed) => {
            if (confirmed) {
              element.dataset.confirm = ""
              element.dispatchEvent(
                new Event("click", { bubbles: true, cancelable: true }),
              )
            }
          })
        }
      },
      true,
    )
  })
})
This is great but we will run into problems with capybara if we are testing with accept_confirm. The problem is that the browser won't pop up at the top level so capybara might not find the modal depending on what your current scope is. Take the following for example:

lang-ruby
sell_order_tab("Trade")

within "#sell_order_trade_tab" do
  # Click the toggle button
  find(".show_car_info_toggle").click

  # Check for the specific content
  within ".show_car_info .show_car" do
    expect(page).to have_content("MYR777")
  end

  accept_confirm("If you delete the trade, remember to check the calculations.") do
    click_on "Delete trade"
  end
end
This will now fail with Capybara::ModalNotFound.

The solution is to create some helper methods to help us match the modal at the document level:

lang-ruby
def accept_custom_confirm(message = nil)
  yield

  handle_confirm(message:, button: "Confirm")
end

def dismiss_custom_confirm(message = nil)
  yield

  handle_confirm(message:, button: "Cancel")
end

def handle_confirm(message: nil, button: "Confirm")
  modal = page.document.find_by_id("turbo-confirm", visible: true)
  expect(modal).to have_content(message) if message

  modal.click_on(button)
end
All that is required now is to change from accept_confirm to accept_custom_confirm.
accept_custom_confirm("If you delete the trade, remember to check the calculations.") do
  click_on "Delete trade"
end