Liveview testing with hooks

Hi folks,

I am trying to test a LiveView that renders a page with a form. This form only contains 2 inputs: one hidden_input that gets populate by a JS hook and one file_input that is ignored by phoenix liveview and has the JS hook.

<%= hidden_input f, :file_base64, id: "hidden-input-base64", phx_update: "ignore" %>
<%= file_input f, :file, phx_hook: "ConvertToBase64", accept: ".csv",  multiple: false, required: true %>

Here is my JS hook:

const ConvertToBase64 = {
  mounted() {
    this.el.addEventListener("change", () => {
      if (this.el.files[0]) {
        const $fileName = document.querySelector(`#${FILE_NAME_ID}`);
        $fileName.textContent = this.el.files[0].name;

        toBase64(this.el.files[0]).then((base64) => {
          const $hiddenInput = document.querySelector(`#${HIDDEN_INPUT_ID}`);
          $hiddenInput.setAttribute("value", base64);
          $hiddenInput.focus(); // this is needed to register the new value with live view
        });
      }
    });
  },
};

export default ConvertToBase64;

So everything works well in dev and I am now trying to test my liveview. I wrote an integration test that opens the page, file the form (the hidden input) and render it but I get this error:

(ArgumentError) value for hidden "import[file_base64]" must be one of [""], got: "data/csv;base64,VGhvbWFz" .

For info here is my incomplete test:

    @base_import %{
      # TODO: remove this once liveview supports file upload
      file: "[object Object]",
      file_base64: "data/csv;base64,VGhvbWFz"
    }

    test "import transactions", %{conn: conn, platform: platform} do
      {:ok, index_live, _html} = live(conn, Routes.transaction_index_path(conn, :index))

      assert index_live |> element(@selectors[:import_transactions_button]) |> render_click() =~
               "Import transactions"

      assert_patch(index_live, Routes.transaction_index_path(conn, :import))

      {:ok, _, html} =
        index_live
        |> form(@selectors[:import_form], import: @base_import)
        |> render_submit()
        |> follow_redirect(conn, Routes.transaction_index_path(conn, :index))

      assert html =~ "Import complete"
    end

Any idea what might I do wrong?

Thanks in advance :slight_smile:

:wave:

Sorry for off-topic, but I’d just suggest using signed url uploads directly from the browser (if it’s possible for you) before liveview supports uploads (and maybe after as well). I found it to be easier to setup than these base64 workarounds.

1 Like

Thanks for the reply. Can you elaborate a bit more on that? I am not sure I fully understand. Is it to upload the file to S3 or something else from the client, and then send the signed url to the backend?

My upload is ephemeral, it is a CSV file that I need to read and import to my DB. Once done, I don’t care about the CSV file anymore.

I generate a presigend url with ExAws.S3.presigned_url/4, set it as a data attribute on the input element, and add a hook which uploads the file when it’s picked.

Is it to upload the file to S3 or something else from the client, and then send the signed url to the backend?

Almost, the backend generates the signed url, the client uploads.


In your case in might be overkill, but still it could be possible to upload the CSV from the client to S3 and have some background job process it later.

3 Likes

Seems like Phoenix.LiveViewTest doesn’t expect hidden inputs being filled: https://github.com/phoenixframework/phoenix_live_view/blob/master/test/phoenix_live_view/integrations/elements_test.exs#L481-L501

Also https://github.com/phoenixframework/phoenix_live_view/blob/master/lib/phoenix_live_view/test/client_proxy.ex#L839-L874

Yep, I saw this code on Github. I wonder what’s the recommended solution in that case. :thinking:

Maybe you can work around that by not using hidden_input helper (or <input type="hidden">) and style the file_base64 input as

position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;

Taken from https://tailwindcss.com/docs/screen-readers/#app.

But it would probably receive focus …

So I have done something similar to what you suggested. Instead of using a hidden_input I am using a text_input with a style of display: none;. It works.

1 Like