mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-28 18:50:48 +08:00
e755ff8122
* Force menu items into a single line * Add shortcut for saving the notebook * Make the disk icon always show file dialog * Split runtime and file settings into separate modals * Add ctrl+s to the shortcuts list * Add togglable menu to the session page * Make sure newly saved file appears in the file selector * Fix path seletor force reloading * Remove notebook generated in tests * Add test for file list refresh after save
300 lines
9.4 KiB
Elixir
300 lines
9.4 KiB
Elixir
defmodule LivebookWeb.SessionLiveTest do
|
|
use LivebookWeb.ConnCase
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Livebook.{SessionSupervisor, Session, Delta, Runtime}
|
|
|
|
setup do
|
|
{:ok, session_id} = SessionSupervisor.create_session()
|
|
%{session_id: session_id}
|
|
end
|
|
|
|
test "disconnected and connected render", %{conn: conn, session_id: session_id} do
|
|
{:ok, view, disconnected_html} = live(conn, "/sessions/#{session_id}")
|
|
assert disconnected_html =~ "Untitled notebook"
|
|
assert render(view) =~ "Untitled notebook"
|
|
end
|
|
|
|
describe "asynchronous updates" do
|
|
test "renders an updated notebook name", %{conn: conn, session_id: session_id} do
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
Session.set_notebook_name(session_id, "My notebook")
|
|
wait_for_session_update(session_id)
|
|
|
|
assert render(view) =~ "My notebook"
|
|
end
|
|
|
|
test "renders a newly inserted section", %{conn: conn, session_id: session_id} do
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
section_id = insert_section(session_id)
|
|
|
|
assert render(view) =~ section_id
|
|
end
|
|
|
|
test "renders an updated section name", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
Session.set_section_name(session_id, section_id, "My section")
|
|
wait_for_session_update(session_id)
|
|
|
|
assert render(view) =~ "My section"
|
|
end
|
|
|
|
test "renders a newly inserted cell", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
cell_id = insert_cell(session_id, section_id, :markdown)
|
|
|
|
assert render(view) =~ cell_id
|
|
end
|
|
|
|
test "un-renders a deleted cell", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :markdown)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
Session.delete_cell(session_id, cell_id)
|
|
wait_for_session_update(session_id)
|
|
|
|
refute render(view) =~ cell_id
|
|
end
|
|
end
|
|
|
|
describe "UI events update the shared state" do
|
|
test "adding a new section", %{conn: conn, session_id: session_id} do
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("button", "New section")
|
|
|> render_click()
|
|
|
|
assert %{notebook: %{sections: [_section]}} = Session.get_data(session_id)
|
|
end
|
|
|
|
test "adding a new cell", %{conn: conn, session_id: session_id} do
|
|
Session.insert_section(session_id, 0)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("button", "+ Markdown")
|
|
|> render_click()
|
|
|
|
assert %{notebook: %{sections: [%{cells: [%{type: :markdown}]}]}} =
|
|
Session.get_data(session_id)
|
|
end
|
|
|
|
test "queueing cell evaluation", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
|
|
|
|
assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} =
|
|
Session.get_data(session_id)
|
|
end
|
|
|
|
test "cancelling cell evaluation", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(2000)")
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("cancel_cell_evaluation", %{"cell_id" => cell_id})
|
|
|
|
assert %{cell_infos: %{^cell_id => %{evaluation_status: :ready}}} =
|
|
Session.get_data(session_id)
|
|
end
|
|
|
|
test "inserting a cell below the given cell", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("insert_cell_below", %{"cell_id" => cell_id, "type" => "markdown"})
|
|
|
|
assert %{notebook: %{sections: [%{cells: [_first_cell, %{type: :markdown}]}]}} =
|
|
Session.get_data(session_id)
|
|
end
|
|
|
|
test "inserting a cell above the given cell", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("insert_cell_above", %{"cell_id" => cell_id, "type" => "markdown"})
|
|
|
|
assert %{notebook: %{sections: [%{cells: [%{type: :markdown}, _first_cell]}]}} =
|
|
Session.get_data(session_id)
|
|
end
|
|
|
|
test "deleting the given cell", %{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("delete_cell", %{"cell_id" => cell_id})
|
|
|
|
assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session_id)
|
|
end
|
|
end
|
|
|
|
describe "runtime settings" do
|
|
test "connecting to elixir standalone updates connect button to reconnect",
|
|
%{conn: conn, session_id: session_id} do
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/runtime")
|
|
|
|
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}")
|
|
|
|
[elixir_standalone_view] = live_children(view)
|
|
|
|
elixir_standalone_view
|
|
|> element("button", "Connect")
|
|
|> render_click()
|
|
|
|
assert_receive {:operation, {:set_runtime, _pid, %Runtime.ElixirStandalone{} = runtime}}
|
|
|
|
page = render(view)
|
|
assert page =~ Atom.to_string(runtime.node)
|
|
assert page =~ "Reconnect"
|
|
assert page =~ "Disconnect"
|
|
end
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
describe "persistence settings" do
|
|
test "saving to file shows the newly created file",
|
|
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/file")
|
|
|
|
path = Path.join(tmp_dir, "notebook.livemd")
|
|
|
|
view
|
|
|> element("button", "Save to file")
|
|
|> render_click()
|
|
|
|
view
|
|
|> element("form")
|
|
|> render_change(%{path: path})
|
|
|
|
view
|
|
|> element(~s{button[phx-click="save"]}, "Save")
|
|
|> render_click()
|
|
|
|
assert view
|
|
|> element("button", "notebook.livemd")
|
|
|> has_element?()
|
|
end
|
|
end
|
|
|
|
describe "completion" do
|
|
test "replies with nil completion reference when no runtime is started",
|
|
%{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("completion_request", %{"cell_id" => cell_id, "hint" => "System.ver"})
|
|
|
|
assert_reply view, %{"completion_ref" => nil}
|
|
end
|
|
|
|
test "replies with completion reference and then sends asynchronous response",
|
|
%{conn: conn, session_id: session_id} do
|
|
section_id = insert_section(session_id)
|
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
|
|
|
{:ok, runtime} = LivebookTest.Runtime.SingleEvaluator.init()
|
|
Session.connect_runtime(session_id, runtime)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
view
|
|
|> element("#session")
|
|
|> render_hook("completion_request", %{"cell_id" => cell_id, "hint" => "System.ver"})
|
|
|
|
assert_reply view, %{"completion_ref" => ref}
|
|
assert ref != nil
|
|
|
|
assert_push_event(view, "completion_response", %{
|
|
"completion_ref" => ^ref,
|
|
"items" => [%{label: "version/0"}]
|
|
})
|
|
end
|
|
end
|
|
|
|
test "forking the session", %{conn: conn, session_id: session_id} do
|
|
Session.set_notebook_name(session_id, "My notebook")
|
|
wait_for_session_update(session_id)
|
|
|
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
view
|
|
|> element("button", "Fork")
|
|
|> render_click()
|
|
|
|
assert to =~ "/sessions/"
|
|
|
|
{:ok, view, _} = live(conn, to)
|
|
assert render(view) =~ "My notebook - fork"
|
|
end
|
|
|
|
# Helpers
|
|
|
|
defp wait_for_session_update(session_id) do
|
|
# This call is synchronous, so it gives the session time
|
|
# for handling the previously sent change messages.
|
|
Session.get_data(session_id)
|
|
:ok
|
|
end
|
|
|
|
# Utils for sending session requests, waiting for the change to be applied
|
|
# and retrieving new ids if applicable.
|
|
|
|
defp insert_section(session_id) do
|
|
Session.insert_section(session_id, 0)
|
|
%{notebook: %{sections: [section]}} = Session.get_data(session_id)
|
|
section.id
|
|
end
|
|
|
|
defp insert_cell(session_id, section_id, type, content \\ "") do
|
|
Session.insert_cell(session_id, section_id, 0, type)
|
|
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id)
|
|
|
|
delta = Delta.new(insert: content)
|
|
Session.apply_cell_delta(session_id, cell.id, delta, 1)
|
|
|
|
cell.id
|
|
end
|
|
end
|