2021-03-04 05:56:28 +08:00
|
|
|
defmodule LivebookWeb.HomeLiveTest do
|
2022-01-29 23:39:41 +08:00
|
|
|
use LivebookWeb.ConnCase, async: true
|
2021-01-08 04:16:54 +08:00
|
|
|
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
alias Livebook.{Sessions, Session}
|
2021-02-21 23:54:44 +08:00
|
|
|
|
2021-01-08 04:16:54 +08:00
|
|
|
test "disconnected and connected render", %{conn: conn} do
|
2021-01-08 21:14:26 +08:00
|
|
|
{:ok, view, disconnected_html} = live(conn, "/")
|
2021-03-26 02:04:49 +08:00
|
|
|
assert disconnected_html =~ "Running sessions"
|
|
|
|
assert render(view) =~ "Running sessions"
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
test "redirects to session upon creation", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
view
|
2022-04-12 02:34:31 +08:00
|
|
|
|> element(~s/[role="navigation"] button/, "New notebook")
|
2021-02-21 23:54:44 +08:00
|
|
|
|> render_click()
|
|
|
|
|
|
|
|
assert to =~ "/sessions/"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(to)
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
describe "file selection" do
|
|
|
|
test "updates the list of files as the input changes", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
path = Path.expand("../../../lib", __DIR__) <> "/"
|
|
|
|
|
2021-08-14 03:17:43 +08:00
|
|
|
view
|
2022-01-29 04:45:04 +08:00
|
|
|
|> element(~s{form[phx-change="set_path"]})
|
2021-08-14 03:17:43 +08:00
|
|
|
|> render_change(%{path: path})
|
|
|
|
|
|
|
|
# Render the view separately to make sure it received the :set_file event
|
|
|
|
render(view) =~ "livebook_web"
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
test "allows importing when a notebook file is selected", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
path = test_notebook_path("basic")
|
|
|
|
|
|
|
|
view
|
2022-01-29 04:45:04 +08:00
|
|
|
|> element(~s{form[phx-change="set_path"]})
|
2021-08-14 03:17:43 +08:00
|
|
|
|> render_change(%{path: Path.dirname(path) <> "/"})
|
|
|
|
|
|
|
|
view
|
|
|
|
|> element("button", "basic.livemd")
|
|
|
|
|> render_click()
|
2021-02-21 23:54:44 +08:00
|
|
|
|
|
|
|
assert assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
view
|
2021-03-20 21:10:15 +08:00
|
|
|
|> element(~s{button[phx-click="fork"]}, "Fork")
|
2021-02-21 23:54:44 +08:00
|
|
|
|> render_click()
|
|
|
|
|
|
|
|
assert to =~ "/sessions/"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(to)
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
2021-08-14 03:17:43 +08:00
|
|
|
@tag :tmp_dir
|
|
|
|
test "disables import when a directory is selected", %{conn: conn, tmp_dir: tmp_dir} do
|
2021-02-21 23:54:44 +08:00
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
view
|
2022-01-29 04:45:04 +08:00
|
|
|
|> element(~s{form[phx-change="set_path"]})
|
2021-08-14 03:17:43 +08:00
|
|
|
|> render_change(%{path: tmp_dir <> "/"})
|
2021-02-21 23:54:44 +08:00
|
|
|
|
|
|
|
assert view
|
2021-03-20 21:10:15 +08:00
|
|
|
|> element(~s{button[phx-click="fork"][disabled]}, "Fork")
|
2021-02-21 23:54:44 +08:00
|
|
|
|> has_element?()
|
|
|
|
end
|
|
|
|
|
|
|
|
test "disables import when a nonexistent file is selected", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
path = File.cwd!() |> Path.join("nonexistent.livemd")
|
|
|
|
|
|
|
|
view
|
2022-01-29 04:45:04 +08:00
|
|
|
|> element(~s{form[phx-change="set_path"]})
|
2021-02-21 23:54:44 +08:00
|
|
|
|> render_change(%{path: path})
|
|
|
|
|
|
|
|
assert view
|
2021-03-20 21:10:15 +08:00
|
|
|
|> element(~s{button[phx-click="fork"][disabled]}, "Fork")
|
2021-02-21 23:54:44 +08:00
|
|
|
|> has_element?()
|
|
|
|
end
|
2021-04-14 22:38:54 +08:00
|
|
|
|
2021-04-14 22:47:50 +08:00
|
|
|
@tag :tmp_dir
|
|
|
|
test "disables open when a write-protected notebook is selected",
|
|
|
|
%{conn: conn, tmp_dir: tmp_dir} do
|
2021-04-14 22:38:54 +08:00
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
2021-04-14 22:47:50 +08:00
|
|
|
path = Path.join(tmp_dir, "write_protected.livemd")
|
|
|
|
File.touch!(path)
|
|
|
|
File.chmod!(path, 0o444)
|
2021-04-14 22:38:54 +08:00
|
|
|
|
|
|
|
view
|
2022-01-29 04:45:04 +08:00
|
|
|
|> element(~s{form[phx-change="set_path"]})
|
2021-08-14 03:17:43 +08:00
|
|
|
|> render_change(%{path: tmp_dir <> "/"})
|
|
|
|
|
|
|
|
view
|
|
|
|
|> element("button", "write_protected.livemd")
|
|
|
|
|> render_click()
|
2021-04-14 22:38:54 +08:00
|
|
|
|
|
|
|
assert view
|
|
|
|
|> element(~s{button[phx-click="open"][disabled]}, "Open")
|
|
|
|
|> has_element?()
|
|
|
|
|
|
|
|
assert view
|
2021-11-02 01:20:56 +08:00
|
|
|
|> element(~s{[data-tooltip="This file is write-protected, please fork instead"]})
|
2021-04-14 22:38:54 +08:00
|
|
|
|> has_element?()
|
|
|
|
end
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
describe "sessions list" do
|
|
|
|
test "lists running sessions", %{conn: conn} do
|
2021-09-05 01:16:01 +08:00
|
|
|
{:ok, session1} = Sessions.create_session()
|
|
|
|
{:ok, session2} = Sessions.create_session()
|
2021-02-21 23:54:44 +08:00
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
assert render(view) =~ session1.id
|
|
|
|
assert render(view) =~ session2.id
|
2022-02-10 22:36:59 +08:00
|
|
|
|
|
|
|
Session.close([session1.pid, session2.pid])
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
test "updates UI whenever a session is added or deleted", %{conn: conn} do
|
2022-04-19 17:29:38 +08:00
|
|
|
Sessions.subscribe()
|
2021-09-05 01:16:01 +08:00
|
|
|
|
2021-02-21 23:54:44 +08:00
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
{:ok, %{id: id} = session} = Sessions.create_session()
|
|
|
|
assert_receive {:session_created, %{id: ^id}}
|
2021-02-21 23:54:44 +08:00
|
|
|
assert render(view) =~ id
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
Session.close(session.pid)
|
|
|
|
assert_receive {:session_closed, %{id: ^id}}
|
2021-02-21 23:54:44 +08:00
|
|
|
refute render(view) =~ id
|
|
|
|
end
|
2021-03-26 02:04:49 +08:00
|
|
|
|
2022-02-04 18:36:09 +08:00
|
|
|
test "allows download the source of an existing session", %{conn: conn} do
|
|
|
|
{:ok, session} = Sessions.create_session()
|
|
|
|
Session.set_notebook_name(session.pid, "My notebook")
|
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
{:error, {:redirect, %{to: to}}} =
|
|
|
|
view
|
|
|
|
|> element(~s{[data-test-session-id="#{session.id}"] a}, "Download source")
|
|
|
|
|> render_click
|
|
|
|
|
|
|
|
assert to ==
|
|
|
|
Routes.session_path(conn, :download_source, session.id, "livemd",
|
|
|
|
include_outputs: false
|
|
|
|
)
|
2022-02-10 22:36:59 +08:00
|
|
|
|
|
|
|
Session.close(session.pid)
|
2022-02-04 18:36:09 +08:00
|
|
|
end
|
|
|
|
|
2021-03-26 02:04:49 +08:00
|
|
|
test "allows forking existing session", %{conn: conn} do
|
2021-09-05 01:16:01 +08:00
|
|
|
{:ok, session} = Sessions.create_session()
|
|
|
|
Session.set_notebook_name(session.pid, "My notebook")
|
2021-03-26 02:04:49 +08:00
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
view
|
2021-09-05 01:16:01 +08:00
|
|
|
|> element(~s{[data-test-session-id="#{session.id}"] button}, "Fork")
|
2021-03-26 02:04:49 +08:00
|
|
|
|> render_click()
|
|
|
|
|
|
|
|
assert to =~ "/sessions/"
|
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, to)
|
|
|
|
assert render(view) =~ "My notebook - fork"
|
2022-02-10 22:36:59 +08:00
|
|
|
|
2022-04-12 02:34:31 +08:00
|
|
|
close_session_by_path(to)
|
2022-02-10 22:36:59 +08:00
|
|
|
Session.close(session.pid)
|
2021-03-26 02:04:49 +08:00
|
|
|
end
|
|
|
|
|
2021-04-13 05:24:26 +08:00
|
|
|
test "allows closing session after confirmation", %{conn: conn} do
|
2021-09-05 01:16:01 +08:00
|
|
|
{:ok, session} = Sessions.create_session()
|
2021-03-26 02:04:49 +08:00
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
assert render(view) =~ session.id
|
2021-03-26 02:04:49 +08:00
|
|
|
|
|
|
|
view
|
2021-09-05 01:16:01 +08:00
|
|
|
|> element(~s{[data-test-session-id="#{session.id}"] a}, "Close")
|
2021-03-26 02:04:49 +08:00
|
|
|
|> render_click()
|
|
|
|
|
|
|
|
view
|
2022-01-29 04:45:04 +08:00
|
|
|
|> element(~s{button[role=button]}, "Close session")
|
2021-03-26 02:04:49 +08:00
|
|
|
|> render_click()
|
|
|
|
|
2021-09-05 01:16:01 +08:00
|
|
|
refute render(view) =~ session.id
|
2021-03-26 02:04:49 +08:00
|
|
|
end
|
2022-01-29 04:45:04 +08:00
|
|
|
|
|
|
|
test "close all selected sessions using bulk action", %{conn: conn} do
|
|
|
|
{:ok, session1} = Sessions.create_session()
|
|
|
|
{:ok, session2} = Sessions.create_session()
|
|
|
|
{:ok, session3} = Sessions.create_session()
|
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
assert render(view) =~ session1.id
|
|
|
|
assert render(view) =~ session2.id
|
|
|
|
assert render(view) =~ session3.id
|
|
|
|
|
|
|
|
view
|
|
|
|
|> form("#bulk-action-form", %{
|
|
|
|
"action" => "close_all",
|
|
|
|
"session_ids" => [session1.id, session2.id, session3.id]
|
|
|
|
})
|
|
|
|
|> render_submit()
|
|
|
|
|
|
|
|
assert render(view) =~ "Are you sure you want to close 3 sessions?"
|
|
|
|
|
|
|
|
view
|
|
|
|
|> element(~s{button[role="button"]}, "Close sessions")
|
|
|
|
|> render_click()
|
|
|
|
|
|
|
|
refute render(view) =~ session1.id
|
|
|
|
refute render(view) =~ session2.id
|
|
|
|
refute render(view) =~ session3.id
|
2022-02-10 22:36:59 +08:00
|
|
|
|
|
|
|
Session.close([session1.pid, session2.pid, session3.pid])
|
2022-01-29 04:45:04 +08:00
|
|
|
end
|
2021-02-21 23:54:44 +08:00
|
|
|
end
|
|
|
|
|
2021-03-31 03:42:02 +08:00
|
|
|
test "link to introductory notebook correctly creates a new session", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/")
|
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
view
|
2022-04-04 18:19:11 +08:00
|
|
|
|> element(~s{[data-el-explore-section] a}, "Welcome to Livebook")
|
2021-03-31 03:42:02 +08:00
|
|
|
|> render_click()
|
2021-06-03 03:51:43 +08:00
|
|
|
|> follow_redirect(conn)
|
2021-03-31 03:42:02 +08:00
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, to)
|
2021-04-01 18:56:19 +08:00
|
|
|
assert render(view) =~ "Welcome to Livebook"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(to)
|
2021-03-31 03:42:02 +08:00
|
|
|
end
|
|
|
|
|
2021-04-23 23:40:13 +08:00
|
|
|
describe "notebook import" do
|
|
|
|
test "allows importing notebook directly from content", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/home/import/content")
|
|
|
|
|
|
|
|
notebook_content = """
|
|
|
|
# My notebook
|
|
|
|
"""
|
|
|
|
|
|
|
|
view
|
|
|
|
|> element("form", "Import")
|
|
|
|
|> render_submit(%{data: %{content: notebook_content}})
|
|
|
|
|
2022-04-12 02:34:31 +08:00
|
|
|
{path, _flash} = assert_redirect(view, 5000)
|
2021-04-23 23:40:13 +08:00
|
|
|
|
2022-03-01 05:27:39 +08:00
|
|
|
{:ok, view, _} = live(conn, path)
|
2021-04-23 23:40:13 +08:00
|
|
|
assert render(view) =~ "My notebook"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(path)
|
2021-04-23 23:40:13 +08:00
|
|
|
end
|
2021-10-22 05:21:54 +08:00
|
|
|
|
|
|
|
test "should show info flash with information about the imported notebook", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/home/import/content")
|
|
|
|
|
|
|
|
notebook_content = """
|
|
|
|
# My notebook
|
|
|
|
"""
|
|
|
|
|
|
|
|
view
|
|
|
|
|> element("form", "Import")
|
|
|
|
|> render_submit(%{data: %{content: notebook_content}})
|
|
|
|
|
2022-04-12 02:34:31 +08:00
|
|
|
{path, flash} = assert_redirect(view, 5000)
|
2021-10-22 05:21:54 +08:00
|
|
|
|
|
|
|
assert flash["info"] =~
|
|
|
|
"You have imported a notebook, no code has been executed so far. You should read and evaluate code as needed."
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(path)
|
2021-10-22 05:21:54 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
test "should show warning flash when the imported notebook have errors", %{conn: conn} do
|
|
|
|
{:ok, view, _} = live(conn, "/home/import/content")
|
|
|
|
|
|
|
|
# Notebook with 3 headers
|
|
|
|
notebook_content = """
|
|
|
|
# My notebook
|
|
|
|
# My notebook
|
|
|
|
# My notebook
|
|
|
|
"""
|
|
|
|
|
|
|
|
view
|
|
|
|
|> element("form", "Import")
|
|
|
|
|> render_submit(%{data: %{content: notebook_content}})
|
|
|
|
|
2022-04-12 02:34:31 +08:00
|
|
|
{path, flash} = assert_redirect(view, 5000)
|
2021-10-22 05:21:54 +08:00
|
|
|
|
|
|
|
assert flash["warning"] =~
|
|
|
|
"We found problems while importing the file and tried to autofix them:\n- Downgrading all headings, because 3 instances of heading 1 were found"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(path)
|
2021-10-22 05:21:54 +08:00
|
|
|
end
|
2021-04-23 23:40:13 +08:00
|
|
|
end
|
|
|
|
|
2021-10-16 18:23:08 +08:00
|
|
|
describe "public import endpoint" do
|
|
|
|
test "imports notebook from the given url and redirects to the new session", %{conn: conn} do
|
|
|
|
bypass = Bypass.open()
|
|
|
|
|
|
|
|
Bypass.expect_once(bypass, "GET", "/notebook", fn conn ->
|
|
|
|
conn
|
|
|
|
|> Plug.Conn.put_resp_content_type("text/plain")
|
|
|
|
|> Plug.Conn.resp(200, "# My notebook")
|
|
|
|
end)
|
|
|
|
|
|
|
|
notebook_url = "http://localhost:#{bypass.port}/notebook"
|
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
live(conn, "/import?url=#{URI.encode_www_form(notebook_url)}")
|
2021-11-12 22:49:22 +08:00
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, to)
|
|
|
|
assert render(view) =~ "My notebook"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(to)
|
2021-11-12 22:49:22 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
@tag :tmp_dir
|
|
|
|
test "imports notebook from local file URL", %{conn: conn, tmp_dir: tmp_dir} do
|
|
|
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
|
|
|
File.write!(notebook_path, "# My notebook")
|
|
|
|
notebook_url = "file://" <> notebook_path
|
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
live(conn, "/import?url=#{URI.encode_www_form(notebook_url)}")
|
2021-10-16 18:23:08 +08:00
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, to)
|
|
|
|
assert render(view) =~ "My notebook"
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
close_session_by_path(to)
|
2021-10-16 18:23:08 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
test "redirects to the import form on error", %{conn: conn} do
|
|
|
|
bypass = Bypass.open()
|
|
|
|
|
|
|
|
Bypass.expect(bypass, "GET", "/notebook", fn conn ->
|
|
|
|
Plug.Conn.resp(conn, 500, "Error")
|
|
|
|
end)
|
|
|
|
|
|
|
|
notebook_url = "http://localhost:#{bypass.port}/notebook"
|
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{to: to}}} =
|
|
|
|
live(conn, "/import?url=#{URI.encode_www_form(notebook_url)}")
|
|
|
|
|
|
|
|
assert to == "/home/import/url?url=#{URI.encode_www_form(notebook_url)}"
|
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, to)
|
|
|
|
assert render(view) =~ notebook_url
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-02-04 03:26:18 +08:00
|
|
|
describe "public open endpoint" do
|
|
|
|
test "checkouts the directory when a directory is passed", %{conn: conn} do
|
|
|
|
directory_path = Path.join(File.cwd!(), "test")
|
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, "/?path=#{directory_path}")
|
|
|
|
|
|
|
|
assert render(view) =~ directory_path
|
|
|
|
end
|
|
|
|
|
|
|
|
@tag :tmp_dir
|
|
|
|
test "opens a file when livebook file is passed", %{conn: conn, tmp_dir: tmp_dir} do
|
|
|
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
|
|
|
|
2022-04-12 02:34:31 +08:00
|
|
|
:ok = File.write(notebook_path, "# My notebook")
|
2022-02-04 03:26:18 +08:00
|
|
|
|
|
|
|
assert {:error, {:live_redirect, %{flash: %{}, to: to}}} =
|
|
|
|
live(conn, "/open?path=#{notebook_path}")
|
|
|
|
|
|
|
|
{:ok, view, _} = live(conn, to)
|
2022-04-12 02:34:31 +08:00
|
|
|
assert render(view) =~ "My notebook"
|
|
|
|
|
|
|
|
close_session_by_path(to)
|
2022-02-04 03:26:18 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-02-21 23:54:44 +08:00
|
|
|
# Helpers
|
|
|
|
|
|
|
|
defp test_notebook_path(name) do
|
|
|
|
path =
|
|
|
|
["../../support/notebooks", name <> ".livemd"]
|
|
|
|
|> Path.join()
|
|
|
|
|> Path.expand(__DIR__)
|
|
|
|
|
|
|
|
unless File.exists?(path) do
|
|
|
|
raise "Cannot find test notebook with the name: #{name}"
|
|
|
|
end
|
|
|
|
|
|
|
|
path
|
2021-01-08 04:16:54 +08:00
|
|
|
end
|
2022-04-12 02:34:31 +08:00
|
|
|
|
|
|
|
defp close_session_by_path("/sessions/" <> session_id) do
|
|
|
|
{:ok, session} = Sessions.fetch_session(session_id)
|
|
|
|
Session.close(session.pid)
|
|
|
|
end
|
2021-01-08 04:16:54 +08:00
|
|
|
end
|