mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-26 17:33:44 +08:00
Improve keyboard navigation and focus (#19)
* Adjust result length * Add more keyboard navigation actions * Improve inserted/deleted cells focus and add some tests * Some refactoring * Run formatter
This commit is contained in:
parent
a8b5227dd6
commit
8acb483bcd
6 changed files with 285 additions and 40 deletions
|
@ -53,6 +53,16 @@ const Cell = {
|
||||||
markdown.setContent(newSource);
|
markdown.setContent(newSource);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New cells are initially focused, so check for such case.
|
||||||
|
|
||||||
|
if (isActive(this.props)) {
|
||||||
|
this.liveEditor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.isFocused) {
|
||||||
|
this.el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -60,15 +70,19 @@ const Cell = {
|
||||||
const prevProps = this.props;
|
const prevProps = this.props;
|
||||||
this.props = getProps(this);
|
this.props = getProps(this);
|
||||||
|
|
||||||
|
// Note: this.liveEditor is crated once we receive initial data
|
||||||
|
// so here we have to make sure it's defined.
|
||||||
|
|
||||||
if (!isActive(prevProps) && isActive(this.props)) {
|
if (!isActive(prevProps) && isActive(this.props)) {
|
||||||
this.liveEditor.focus();
|
this.liveEditor && this.liveEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isActive(prevProps) && !isActive(this.props)) {
|
if (isActive(prevProps) && !isActive(this.props)) {
|
||||||
this.liveEditor.blur();
|
this.liveEditor && this.liveEditor.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prevProps.isFocused && this.props.isFocused) {
|
if (!prevProps.isFocused && this.props.isFocused) {
|
||||||
|
// Note: it's important to trigger scrolling after focus, so it doesn't get interrupted.
|
||||||
this.el.scrollIntoView({ behavior: "smooth", block: "center" });
|
this.el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,21 +15,65 @@ const Session = {
|
||||||
|
|
||||||
// Keybindings
|
// Keybindings
|
||||||
this.handleDocumentKeydown = (event) => {
|
this.handleDocumentKeydown = (event) => {
|
||||||
if (event.shiftKey && event.key === "Enter" && !event.repeat) {
|
const key = event.key.toLowerCase();
|
||||||
if (this.props.focusedCellId !== null) {
|
const shift = event.shiftKey;
|
||||||
// If the editor is focused we don't want it to receive the input
|
const alt = event.altKey;
|
||||||
event.preventDefault();
|
const ctrl = event.ctrlKey;
|
||||||
this.pushEvent("toggle_cell_expanded", {});
|
|
||||||
|
if (event.repeat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shift && key === "enter") {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
|
if (this.props.focusedCellType === "elixir") {
|
||||||
|
this.pushEvent("queue_focused_cell_evaluation");
|
||||||
}
|
}
|
||||||
} else if (event.altKey && event.key === "j") {
|
|
||||||
event.preventDefault();
|
|
||||||
this.pushEvent("move_cell_focus", { offset: 1 });
|
this.pushEvent("move_cell_focus", { offset: 1 });
|
||||||
} else if (event.altKey && event.key === "k") {
|
} else if (alt && ctrl && key === "enter") {
|
||||||
event.preventDefault();
|
cancelEvent(event);
|
||||||
|
|
||||||
|
this.pushEvent("queue_child_cells_evaluation", {});
|
||||||
|
} else if (ctrl && key === "enter") {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
|
if (this.props.focusedCellType === "elixir") {
|
||||||
|
this.pushEvent("queue_focused_cell_evaluation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.focusedCellType === "markdown") {
|
||||||
|
this.pushEvent("toggle_cell_expanded");
|
||||||
|
}
|
||||||
|
} else if (alt && key === "j") {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
|
this.pushEvent("move_cell_focus", { offset: 1 });
|
||||||
|
} else if (alt && key === "k") {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
this.pushEvent("move_cell_focus", { offset: -1 });
|
this.pushEvent("move_cell_focus", { offset: -1 });
|
||||||
} else if (event.ctrlKey && event.key === "Enter") {
|
} else if (alt && key === "n") {
|
||||||
event.stopPropagation();
|
cancelEvent(event);
|
||||||
this.pushEvent("queue_cell_evaluation", {});
|
|
||||||
|
if (shift) {
|
||||||
|
this.pushEvent("insert_cell_above_focused", { type: "elixir" });
|
||||||
|
} else {
|
||||||
|
this.pushEvent("insert_cell_below_focused", { type: "elixir" });
|
||||||
|
}
|
||||||
|
} else if (alt && key === "m") {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
|
if (shift) {
|
||||||
|
this.pushEvent("insert_cell_above_focused", { type: "markdown" });
|
||||||
|
} else {
|
||||||
|
this.pushEvent("insert_cell_below_focused", { type: "markdown" });
|
||||||
|
}
|
||||||
|
} else if (alt && key === "w") {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
|
this.pushEvent("delete_focused_cell", {}); // TODO: focused:delete_cell ?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,7 +99,7 @@ const Session = {
|
||||||
destroyed() {
|
destroyed() {
|
||||||
document.removeEventListener("keydown", this.handleDocumentKeydown);
|
document.removeEventListener("keydown", this.handleDocumentKeydown);
|
||||||
document.removeEventListener("click", this.handleDocumentClick);
|
document.removeEventListener("click", this.handleDocumentClick);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProps(hook) {
|
function getProps(hook) {
|
||||||
|
@ -65,7 +109,15 @@ function getProps(hook) {
|
||||||
"data-focused-cell-id",
|
"data-focused-cell-id",
|
||||||
(value) => value || null
|
(value) => value || null
|
||||||
),
|
),
|
||||||
|
focusedCellType: getAttributeOrThrow(hook.el, "data-focused-cell-type"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelEvent(event) {
|
||||||
|
// Cancel any default browser behavior.
|
||||||
|
event.preventDefault();
|
||||||
|
// Stop event propagation (e.g. so it doesn't reach the editor).
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
export default Session;
|
export default Session;
|
||||||
|
|
|
@ -172,9 +172,9 @@ defmodule LiveBook.Notebook do
|
||||||
with {:ok, _, section} <- LiveBook.Notebook.fetch_cell_and_section(notebook, cell_id) do
|
with {:ok, _, section} <- LiveBook.Notebook.fetch_cell_and_section(notebook, cell_id) do
|
||||||
# A cell depends on all previous cells within the same section.
|
# A cell depends on all previous cells within the same section.
|
||||||
section.cells
|
section.cells
|
||||||
|> Enum.filter(&(&1.type == :elixir))
|
|
||||||
|> Enum.take_while(&(&1.id != cell_id))
|
|> Enum.take_while(&(&1.id != cell_id))
|
||||||
|> Enum.reverse()
|
|> Enum.reverse()
|
||||||
|
|> Enum.filter(&(&1.type == :elixir))
|
||||||
else
|
else
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
|
@ -190,9 +190,9 @@ defmodule LiveBook.Notebook do
|
||||||
with {:ok, _, section} <- LiveBook.Notebook.fetch_cell_and_section(notebook, cell_id) do
|
with {:ok, _, section} <- LiveBook.Notebook.fetch_cell_and_section(notebook, cell_id) do
|
||||||
# A cell affects all the cells below it within the same section.
|
# A cell affects all the cells below it within the same section.
|
||||||
section.cells
|
section.cells
|
||||||
|
|> Enum.drop_while(&(&1.id != cell_id))
|
||||||
|
|> Enum.drop(1)
|
||||||
|> Enum.filter(&(&1.type == :elixir))
|
|> Enum.filter(&(&1.type == :elixir))
|
||||||
|> Enum.reverse()
|
|
||||||
|> Enum.take_while(&(&1.id != cell_id))
|
|
||||||
else
|
else
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
|
|
|
@ -154,7 +154,7 @@ defmodule LiveBookWeb.Cell do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_output({:ok, value}) do
|
defp render_output({:ok, value}) do
|
||||||
inspected = Utils.inspect_as_html(value, pretty: true, width: 140)
|
inspected = Utils.inspect_as_html(value, pretty: true, width: 100)
|
||||||
|
|
||||||
assigns = %{inspected: inspected}
|
assigns = %{inspected: inspected}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
data: data,
|
data: data,
|
||||||
selected_section_id: first_section_id,
|
selected_section_id: first_section_id,
|
||||||
focused_cell_id: nil,
|
focused_cell_id: nil,
|
||||||
|
focused_cell_type: nil,
|
||||||
focused_cell_expanded: false
|
focused_cell_expanded: false
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -44,7 +45,8 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
<div class="flex flex-grow max-h-full"
|
<div class="flex flex-grow max-h-full"
|
||||||
id="session"
|
id="session"
|
||||||
phx-hook="Session"
|
phx-hook="Session"
|
||||||
data-focused-cell-id="<%= @focused_cell_id %>">
|
data-focused-cell-id="<%= @focused_cell_id %>"
|
||||||
|
data-focused-cell-type="<%= @focused_cell_type %>">
|
||||||
<div class="w-1/5 bg-gray-100 border-r-2 border-gray-200">
|
<div class="w-1/5 bg-gray-100 border-r-2 border-gray-200">
|
||||||
<h1 id="notebook-name"
|
<h1 id="notebook-name"
|
||||||
contenteditable
|
contenteditable
|
||||||
|
@ -135,12 +137,34 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("insert_cell_below_focused", %{"type" => type}, socket) do
|
||||||
|
type = String.to_atom(type)
|
||||||
|
insert_cell_next_to_focused(socket.assigns, type, idx_offset: 1)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("insert_cell_above_focused", %{"type" => type}, socket) do
|
||||||
|
type = String.to_atom(type)
|
||||||
|
insert_cell_next_to_focused(socket.assigns, type, idx_offset: 0)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
||||||
Session.delete_cell(socket.assigns.session_id, cell_id)
|
Session.delete_cell(socket.assigns.session_id, cell_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("delete_focused_cell", %{}, socket) do
|
||||||
|
if socket.assigns.focused_cell_id do
|
||||||
|
Session.delete_cell(socket.assigns.session_id, socket.assigns.focused_cell_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("set_notebook_name", %{"name" => name}, socket) do
|
def handle_event("set_notebook_name", %{"name" => name}, socket) do
|
||||||
name = normalize_name(name)
|
name = normalize_name(name)
|
||||||
Session.set_notebook_name(socket.assigns.session_id, name)
|
Session.set_notebook_name(socket.assigns.session_id, name)
|
||||||
|
@ -166,14 +190,24 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("focus_cell", %{"cell_id" => nil}, socket) do
|
||||||
|
{:noreply, focus_cell(socket, nil)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("focus_cell", %{"cell_id" => cell_id}, socket) do
|
def handle_event("focus_cell", %{"cell_id" => cell_id}, socket) do
|
||||||
{:noreply, assign(socket, focused_cell_id: cell_id, focused_cell_expanded: false)}
|
case Notebook.fetch_cell_and_section(socket.assigns.data.notebook, cell_id) do
|
||||||
|
{:ok, cell, _section} ->
|
||||||
|
{:noreply, focus_cell(socket, cell)}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("move_cell_focus", %{"offset" => offset}, socket) do
|
def handle_event("move_cell_focus", %{"offset" => offset}, socket) do
|
||||||
case new_focused_cell_from_offset(socket.assigns, offset) do
|
case new_focused_cell_from_offset(socket.assigns, offset) do
|
||||||
{:ok, cell} ->
|
{:ok, cell} ->
|
||||||
{:noreply, assign(socket, focused_cell_id: cell.id, focused_cell_expanded: false)}
|
{:noreply, focus_cell(socket, cell)}
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
@ -194,7 +228,7 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_cell_evaluation", %{}, socket) do
|
def handle_event("queue_focused_cell_evaluation", %{}, socket) do
|
||||||
if socket.assigns.focused_cell_id do
|
if socket.assigns.focused_cell_id do
|
||||||
Session.queue_cell_evaluation(socket.assigns.session_id, socket.assigns.focused_cell_id)
|
Session.queue_cell_evaluation(socket.assigns.session_id, socket.assigns.focused_cell_id)
|
||||||
end
|
end
|
||||||
|
@ -202,20 +236,78 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("queue_child_cells_evaluation", %{}, socket) do
|
||||||
|
if socket.assigns.focused_cell_id do
|
||||||
|
{:ok, cell, _section} =
|
||||||
|
Notebook.fetch_cell_and_section(
|
||||||
|
socket.assigns.data.notebook,
|
||||||
|
socket.assigns.focused_cell_id
|
||||||
|
)
|
||||||
|
|
||||||
|
cells = Notebook.child_cells(socket.assigns.data.notebook, cell.id)
|
||||||
|
|
||||||
|
for cell <- [cell | cells], cell.type == :elixir do
|
||||||
|
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:operation, operation}, socket) do
|
def handle_info({:operation, operation}, socket) do
|
||||||
case Session.Data.apply_operation(socket.assigns.data, operation) do
|
case Session.Data.apply_operation(socket.assigns.data, operation) do
|
||||||
{:ok, data, actions} ->
|
{:ok, data, actions} ->
|
||||||
new_socket = assign(socket, data: data)
|
new_socket =
|
||||||
{:noreply, handle_actions(new_socket, actions)}
|
socket
|
||||||
|
|> assign(data: data)
|
||||||
|
|> after_operation(socket, operation)
|
||||||
|
|> handle_actions(actions)
|
||||||
|
|
||||||
|
{:noreply, new_socket}
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_actions(state, actions) do
|
defp after_operation(socket, _prev_socket, {:insert_section, _index, section_id}) do
|
||||||
Enum.reduce(actions, state, &handle_action(&2, &1))
|
assign(socket, selected_section_id: section_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp after_operation(socket, _prev_socket, {:delete_section, _section_id}) do
|
||||||
|
assign(socket, selected_section_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp after_operation(socket, _prev_socket, {:insert_cell, _, _, _, cell_id}) do
|
||||||
|
{:ok, cell, _section} = Notebook.fetch_cell_and_section(socket.assigns.data.notebook, cell_id)
|
||||||
|
focus_cell(socket, cell, expanded: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp after_operation(socket, prev_socket, {:delete_cell, cell_id}) do
|
||||||
|
if cell_id == socket.assigns.focused_cell_id do
|
||||||
|
case Notebook.fetch_cell_sibling(prev_socket.assigns.data.notebook, cell_id, 1) do
|
||||||
|
{:ok, next_cell} ->
|
||||||
|
focus_cell(socket, next_cell)
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
case Notebook.fetch_cell_sibling(prev_socket.assigns.data.notebook, cell_id, -1) do
|
||||||
|
{:ok, previous_cell} ->
|
||||||
|
focus_cell(socket, previous_cell)
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
focus_cell(socket, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp after_operation(socket, _prev_socket, _operation), do: socket
|
||||||
|
|
||||||
|
defp handle_actions(socket, actions) do
|
||||||
|
Enum.reduce(actions, socket, &handle_action(&2, &1))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(socket, {:broadcast_delta, from, cell, delta}) do
|
defp handle_action(socket, {:broadcast_delta, from, cell, delta}) do
|
||||||
|
@ -238,6 +330,44 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp focus_cell(socket, cell, opts \\ [])
|
||||||
|
|
||||||
|
defp focus_cell(socket, nil = _cell, _opts) do
|
||||||
|
assign(socket, focused_cell_id: nil, focused_cell_type: nil, focused_cell_expanded: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp focus_cell(socket, cell, opts) do
|
||||||
|
expanded? = Keyword.get(opts, :expanded, false)
|
||||||
|
|
||||||
|
assign(socket,
|
||||||
|
focused_cell_id: cell.id,
|
||||||
|
focused_cell_type: cell.type,
|
||||||
|
focused_cell_expanded: expanded?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp insert_cell_next_to_focused(assigns, type, idx_offset: idx_offset) do
|
||||||
|
if assigns.focused_cell_id do
|
||||||
|
{:ok, cell, section} =
|
||||||
|
Notebook.fetch_cell_and_section(assigns.data.notebook, assigns.focused_cell_id)
|
||||||
|
|
||||||
|
index = Enum.find_index(section.cells, &(&1 == cell))
|
||||||
|
Session.insert_cell(assigns.session_id, section.id, index + idx_offset, type)
|
||||||
|
else
|
||||||
|
append_cell_to_section(assigns, type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp append_cell_to_section(assigns, type) do
|
||||||
|
if assigns.selected_section_id do
|
||||||
|
{:ok, section} = Notebook.fetch_section(assigns.data.notebook, assigns.selected_section_id)
|
||||||
|
|
||||||
|
end_index = length(section.cells)
|
||||||
|
|
||||||
|
Session.insert_cell(assigns.session_id, section.id, end_index, type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp new_focused_cell_from_offset(assigns, offset) do
|
defp new_focused_cell_from_offset(assigns, offset) do
|
||||||
cond do
|
cond do
|
||||||
assigns.focused_cell_id ->
|
assigns.focused_cell_id ->
|
||||||
|
|
|
@ -68,8 +68,8 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "UI-triggered updates" do
|
describe "UI events update the shared state" do
|
||||||
test "adding a new session updates the shared state", %{conn: conn, session_id: session_id} do
|
test "adding a new section", %{conn: conn, session_id: session_id} do
|
||||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||||
|
|
||||||
view
|
view
|
||||||
|
@ -79,7 +79,7 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
assert %{notebook: %{sections: [_section]}} = Session.get_data(session_id)
|
assert %{notebook: %{sections: [_section]}} = Session.get_data(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "adding a new cell updates the shared state", %{conn: conn, session_id: session_id} do
|
test "adding a new cell", %{conn: conn, session_id: session_id} do
|
||||||
Session.insert_section(session_id, 0)
|
Session.insert_section(session_id, 0)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||||
|
@ -92,10 +92,9 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
Session.get_data(session_id)
|
Session.get_data(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "queueing cell evaluation updates the shared state",
|
test "queueing cell evaluation", %{conn: conn, session_id: session_id} do
|
||||||
%{conn: conn, session_id: session_id} do
|
|
||||||
section_id = insert_section(session_id)
|
section_id = insert_section(session_id)
|
||||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||||
|
|
||||||
|
@ -107,24 +106,68 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
Session.get_data(session_id)
|
Session.get_data(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "queueing cell evaluation defaults to the focused cell if no cell id is given",
|
test "queueing focused cell evaluation", %{conn: conn, session_id: session_id} do
|
||||||
%{conn: conn, session_id: session_id} do
|
|
||||||
section_id = insert_section(session_id)
|
section_id = insert_section(session_id)
|
||||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||||
|
|
||||||
view
|
focus_cell(view, cell_id)
|
||||||
|> element("#session")
|
|
||||||
|> render_hook("focus_cell", %{"cell_id" => cell_id})
|
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("#session")
|
|> element("#session")
|
||||||
|> render_hook("queue_cell_evaluation", %{})
|
|> render_hook("queue_focused_cell_evaluation", %{})
|
||||||
|
|
||||||
assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} =
|
assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} =
|
||||||
Session.get_data(session_id)
|
Session.get_data(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "inserting a cell below the focused 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}")
|
||||||
|
|
||||||
|
focus_cell(view, cell_id)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#session")
|
||||||
|
|> render_hook("insert_cell_below_focused", %{"type" => "markdown"})
|
||||||
|
|
||||||
|
assert %{notebook: %{sections: [%{cells: [_first_cell, %{type: :markdown}]}]}} =
|
||||||
|
Session.get_data(session_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "inserting a cell above the focused 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}")
|
||||||
|
|
||||||
|
focus_cell(view, cell_id)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#session")
|
||||||
|
|> render_hook("insert_cell_above_focused", %{"type" => "markdown"})
|
||||||
|
|
||||||
|
assert %{notebook: %{sections: [%{cells: [%{type: :markdown}, _first_cell]}]}} =
|
||||||
|
Session.get_data(session_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deleting the focused 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}")
|
||||||
|
|
||||||
|
focus_cell(view, cell_id)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#session")
|
||||||
|
|> render_hook("delete_focused_cell", %{})
|
||||||
|
|
||||||
|
assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp wait_for_session_update(session_id) do
|
defp wait_for_session_update(session_id) do
|
||||||
|
@ -152,4 +195,10 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
|
|
||||||
cell.id
|
cell.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp focus_cell(view, cell_id) do
|
||||||
|
view
|
||||||
|
|> element("#session")
|
||||||
|
|> render_hook("focus_cell", %{"cell_id" => cell_id})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue