mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-26 09:22:00 +08:00
Integrate evaluation into UI (#17)
* Render evaluation outputs and result * Fix auto-scrolling to not be interrupted by editor focus * Add cell output tests * Run formatter * Show cell status * Apply review suggestions * Change EEx strings to Live EEx
This commit is contained in:
parent
936d0af5fb
commit
77b60c8110
12 changed files with 378 additions and 87 deletions
|
@ -121,3 +121,17 @@ iframe[hidden] {
|
|||
.bg-editor {
|
||||
background-color: #282c34;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.25rem;
|
||||
@apply bg-gray-400;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar-track {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
|
|
@ -67,6 +67,10 @@ const Cell = {
|
|||
if (isActive(prevProps) && !isActive(this.props)) {
|
||||
this.liveEditor.blur();
|
||||
}
|
||||
|
||||
if (!prevProps.isFocused && this.props.isFocused) {
|
||||
this.el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ const Session = {
|
|||
this.props = getProps(this);
|
||||
|
||||
// Keybindings
|
||||
document.addEventListener("keydown", (event) => {
|
||||
this.handleDocumentKeydown = (event) => {
|
||||
if (event.shiftKey && event.key === "Enter" && !event.repeat) {
|
||||
if (this.props.focusedCellId !== null) {
|
||||
// If the editor is focused we don't want it to receive the input
|
||||
|
@ -27,33 +27,35 @@ const Session = {
|
|||
} else if (event.altKey && event.key === "k") {
|
||||
event.preventDefault();
|
||||
this.pushEvent("move_cell_focus", { offset: -1 });
|
||||
} else if (event.ctrlKey && event.key === "Enter") {
|
||||
event.stopPropagation();
|
||||
this.pushEvent("queue_cell_evaluation", {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", this.handleDocumentKeydown, true);
|
||||
|
||||
// Focus/unfocus a cell when the user clicks somewhere
|
||||
document.addEventListener("click", (event) => {
|
||||
this.handleDocumentClick = (event) => {
|
||||
// Find the parent with cell id info, if there is one
|
||||
const cell = event.target.closest("[data-cell-id]");
|
||||
const cellId = cell ? cell.dataset.cellId : null;
|
||||
if (cellId !== this.props.focusedCellId) {
|
||||
this.pushEvent("focus_cell", { cell_id: cellId });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener("click", this.handleDocumentClick);
|
||||
},
|
||||
|
||||
updated() {
|
||||
const prevProps = this.props;
|
||||
this.props = getProps(this);
|
||||
|
||||
// When a new cell gets focus, center it nicely on the page
|
||||
if (
|
||||
this.props.focusedCellId &&
|
||||
this.props.focusedCellId !== prevProps.focusedCellId
|
||||
) {
|
||||
const cell = this.el.querySelector(`#cell-${this.props.focusedCellId}`);
|
||||
cell.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
document.removeEventListener("keydown", this.handleDocumentKeydown);
|
||||
document.removeEventListener("click", this.handleDocumentClick);
|
||||
}
|
||||
};
|
||||
|
||||
function getProps(hook) {
|
||||
|
|
|
@ -18,7 +18,6 @@ defmodule LiveBook.Notebook.Cell do
|
|||
id: id(),
|
||||
type: type(),
|
||||
source: String.t(),
|
||||
# TODO: expand on this
|
||||
outputs: list(),
|
||||
metadata: %{atom() => term()}
|
||||
}
|
||||
|
|
|
@ -326,22 +326,35 @@ defmodule LiveBook.Session.Data do
|
|||
|> set_cell_info!(cell.id, evaluation_status: :ready)
|
||||
end
|
||||
|
||||
defp add_cell_evaluation_stdout({data, _} = data_actions, _cell, _string) do
|
||||
defp add_cell_evaluation_stdout({data, _} = data_actions, cell, string) do
|
||||
data_actions
|
||||
|> set!(
|
||||
# TODO: add stdout to cell outputs
|
||||
notebook: data.notebook
|
||||
notebook:
|
||||
Notebook.update_cell(data.notebook, cell.id, fn cell ->
|
||||
%{cell | outputs: add_output(cell.outputs, string)}
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
defp add_cell_evaluation_response({data, _} = data_actions, _cell, _response) do
|
||||
defp add_cell_evaluation_response({data, _} = data_actions, cell, response) do
|
||||
data_actions
|
||||
|> set!(
|
||||
# TODO: add result to outputs
|
||||
notebook: data.notebook
|
||||
notebook:
|
||||
Notebook.update_cell(data.notebook, cell.id, fn cell ->
|
||||
%{cell | outputs: add_output(cell.outputs, response)}
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
defp add_output([], output), do: [output]
|
||||
|
||||
defp add_output([head | tail], output) when is_binary(head) and is_binary(output) do
|
||||
# Merge consecutive string outputs
|
||||
[head <> output | tail]
|
||||
end
|
||||
|
||||
defp add_output(outputs, output), do: [output | outputs]
|
||||
|
||||
defp finish_cell_evaluation(data_actions, cell, section) do
|
||||
data_actions
|
||||
|> set_cell_info!(cell.id,
|
||||
|
|
|
@ -17,14 +17,16 @@ defmodule LiveBookWeb.Cell do
|
|||
|
||||
def render_cell_content(%{cell: %{type: :markdown}} = assigns) do
|
||||
~L"""
|
||||
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
|
||||
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
||||
<%= Icons.svg(:trash, class: "h-6") %>
|
||||
</button>
|
||||
</div>
|
||||
<%= if @focused do %>
|
||||
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
|
||||
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
||||
<%= Icons.svg(:trash, class: "h-6") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="<%= if @expanded, do: "mb-4", else: "hidden" %>">
|
||||
<%= render_editor(@cell) %>
|
||||
<%= render_editor(@cell, @cell_info) %>
|
||||
</div>
|
||||
|
||||
<div class="markdown" data-markdown-container id="markdown-container-<%= @cell.id %>" phx-update="ignore">
|
||||
|
@ -35,26 +37,44 @@ defmodule LiveBookWeb.Cell do
|
|||
|
||||
def render_cell_content(%{cell: %{type: :elixir}} = assigns) do
|
||||
~L"""
|
||||
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
|
||||
<button class="text-gray-500 hover:text-current">
|
||||
<%= Icons.svg(:play, class: "h-6") %>
|
||||
</button>
|
||||
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
||||
<%= Icons.svg(:trash, class: "h-6") %>
|
||||
</button>
|
||||
</div>
|
||||
<%= if @focused do %>
|
||||
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
|
||||
<button phx-click="queue_cell_evaluation" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
||||
<%= Icons.svg(:play, class: "h-6") %>
|
||||
</button>
|
||||
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
||||
<%= Icons.svg(:trash, class: "h-6") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render_editor(@cell) %>
|
||||
<%= render_editor(@cell, @cell_info, show_status: true) %>
|
||||
|
||||
<%= if @cell.outputs != [] do %>
|
||||
<div class="mt-2">
|
||||
<%= render_outputs(@cell.outputs) %>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_editor(cell) do
|
||||
~E"""
|
||||
<div class="py-3 rounded-md overflow-hidden bg-editor"
|
||||
data-editor-container
|
||||
id="editor-container-<%= cell.id %>"
|
||||
phx-update="ignore">
|
||||
<%= render_editor_content_placeholder(cell.source) %>
|
||||
defp render_editor(cell, cell_info, opts \\ []) do
|
||||
show_status = Keyword.get(opts, :show_status, false)
|
||||
assigns = %{cell: cell, cell_info: cell_info, show_status: show_status}
|
||||
|
||||
~L"""
|
||||
<div class="py-3 rounded-md overflow-hidden bg-editor relative">
|
||||
<div id="editor-container-<%= @cell.id %>"
|
||||
data-editor-container
|
||||
phx-update="ignore">
|
||||
<%= render_editor_content_placeholder(@cell.source) %>
|
||||
</div>
|
||||
|
||||
<%= if @show_status do %>
|
||||
<div class="absolute bottom-2 right-2 z-50">
|
||||
<%= render_cell_status(@cell_info) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
@ -64,13 +84,17 @@ defmodule LiveBookWeb.Cell do
|
|||
# or and editors are mounted, so show neat placeholders immediately.
|
||||
|
||||
defp render_markdown_content_placeholder("" = _content) do
|
||||
~E"""
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="h-4"></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_markdown_content_placeholder(_content) do
|
||||
~E"""
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="max-w-2xl w-full animate-pulse">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
|
@ -82,13 +106,17 @@ defmodule LiveBookWeb.Cell do
|
|||
end
|
||||
|
||||
defp render_editor_content_placeholder("" = _content) do
|
||||
~E"""
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="h-4"></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_editor_content_placeholder(_content) do
|
||||
~E"""
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="px-8 max-w-2xl w-full animate-pulse">
|
||||
<div class="flex-1 space-y-4 py-1">
|
||||
<div class="h-4 bg-gray-500 rounded w-3/4"></div>
|
||||
|
@ -98,4 +126,98 @@ defmodule LiveBookWeb.Cell do
|
|||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_outputs(outputs) do
|
||||
assigns = %{outputs: outputs}
|
||||
|
||||
~L"""
|
||||
<div class="flex flex-col rounded-md border border-gray-200 divide-y divide-gray-200 text-sm">
|
||||
<%= for output <- Enum.reverse(@outputs) do %>
|
||||
<div class="p-4">
|
||||
<div class="max-h-80 overflow-auto tiny-scrollbar">
|
||||
<%= render_output(output) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output(output) when is_binary(output) do
|
||||
assigns = %{output: output}
|
||||
|
||||
~L"""
|
||||
<div class="whitespace-pre text-gray-500"><%= @output %></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output({:ok, value}) do
|
||||
inspected = inspect(value, pretty: true, width: 140)
|
||||
assigns = %{inspected: inspected}
|
||||
|
||||
~L"""
|
||||
<div class="whitespace-pre text-gray-500"><%= @inspected %></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_output({:error, kind, error, stacktrace}) do
|
||||
formatted = Exception.format(kind, error, stacktrace)
|
||||
assigns = %{formatted: formatted}
|
||||
|
||||
~L"""
|
||||
<div class="whitespace-pre text-red-600"><%= @formatted %></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(%{evaluation_status: :evaluating}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-xs text-gray-400">Evaluating</div>
|
||||
<span class="flex relative h-3 w-3">
|
||||
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-blue-300 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-blue-400"></span>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(%{evaluation_status: :queued}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-xs text-gray-400">Queued</div>
|
||||
<span class="flex relative h-3 w-3">
|
||||
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-gray-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-gray-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(%{validity_status: :evaluated}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-xs text-gray-400">Evaluated</div>
|
||||
<div class="h-3 w-3 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(%{validity_status: :stale}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-xs text-gray-400">Stale</div>
|
||||
<div class="h-3 w-3 rounded-full bg-yellow-200"></div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(_), do: nil
|
||||
end
|
||||
|
|
|
@ -1,44 +1,55 @@
|
|||
defmodule LiveBookWeb.Icons do
|
||||
import Phoenix.HTML
|
||||
import Phoenix.HTML.Tag
|
||||
import Phoenix.LiveView.Helpers
|
||||
|
||||
@doc """
|
||||
Returns icon svg tag.
|
||||
"""
|
||||
@spec svg(atom(), keyword()) :: Phoenix.HTML.safe()
|
||||
def svg(name, attrs \\ [])
|
||||
|
||||
def svg(:chevron_right, attrs) do
|
||||
~e"""
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
||||
|
||||
~L"""
|
||||
<%= tag(:svg, @attrs) %>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
"""
|
||||
|> heroicon_svg(attrs)
|
||||
end
|
||||
|
||||
def svg(:play, attrs) do
|
||||
~e"""
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
||||
|
||||
~L"""
|
||||
<%= tag(:svg, @attrs) %>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
"""
|
||||
|> heroicon_svg(attrs)
|
||||
end
|
||||
|
||||
def svg(:plus, attrs) do
|
||||
~e"""
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
||||
|
||||
~L"""
|
||||
<%= tag(:svg, @attrs) %>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
"""
|
||||
|> heroicon_svg(attrs)
|
||||
end
|
||||
|
||||
def svg(:trash, attrs) do
|
||||
~e"""
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
||||
|
||||
~L"""
|
||||
<%= tag(:svg, @attrs) %>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
"""
|
||||
|> heroicon_svg(attrs)
|
||||
end
|
||||
|
||||
# https://heroicons.com
|
||||
defp heroicon_svg(svg_content, attrs) do
|
||||
defp heroicon_svg_attrs(attrs) do
|
||||
heroicon_svg_attrs = [
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
fill: "none",
|
||||
|
@ -46,6 +57,6 @@ defmodule LiveBookWeb.Icons do
|
|||
stroke: "currentColor"
|
||||
]
|
||||
|
||||
content_tag(:svg, svg_content, Keyword.merge(attrs, heroicon_svg_attrs))
|
||||
Keyword.merge(attrs, heroicon_svg_attrs)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,9 @@ defmodule LiveBookWeb.InsertCellActions do
|
|||
end
|
||||
|
||||
defp line() do
|
||||
~e"""
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="border-t-2 border-dashed border-gray-200 flex-grow"></div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -25,15 +25,18 @@ defmodule LiveBookWeb.Section do
|
|||
<div class="container py-4">
|
||||
<div class="flex flex-col space-y-2 pb-80">
|
||||
<%= live_component @socket, LiveBookWeb.InsertCellActions,
|
||||
id: "#{@section.id}:0",
|
||||
section_id: @section.id,
|
||||
index: 0 %>
|
||||
<%= for {cell, index} <- Enum.with_index(@section.cells) do %>
|
||||
<%= live_component @socket, LiveBookWeb.Cell,
|
||||
id: cell.id,
|
||||
cell: cell,
|
||||
cell_info: @cell_infos[cell.id],
|
||||
focused: @selected and cell.id == @focused_cell_id,
|
||||
expanded: @selected and cell.id == @focused_cell_id and @focused_cell_expanded %>
|
||||
<%= live_component @socket, LiveBookWeb.InsertCellActions,
|
||||
id: "#{@section.id}:#{index + 1}",
|
||||
section_id: @section.id,
|
||||
index: index + 1 %>
|
||||
<% end %>
|
||||
|
|
|
@ -75,6 +75,7 @@ defmodule LiveBookWeb.SessionLive do
|
|||
<div class="max-w-screen-lg w-full mx-auto">
|
||||
<%= for section <- @data.notebook.sections do %>
|
||||
<%= live_component @socket, LiveBookWeb.Section,
|
||||
id: section.id,
|
||||
section: section,
|
||||
selected: section.id == @selected_section_id,
|
||||
cell_infos: @data.cell_infos,
|
||||
|
@ -187,6 +188,20 @@ defmodule LiveBookWeb.SessionLive do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, cell_id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("queue_cell_evaluation", %{}, socket) do
|
||||
if socket.assigns.focused_cell_id do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, socket.assigns.focused_cell_id)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:operation, operation}, socket) do
|
||||
case Session.Data.apply_operation(socket.assigns.data, operation) do
|
||||
|
|
|
@ -252,14 +252,73 @@ defmodule LiveBook.Session.DataTest do
|
|||
end
|
||||
|
||||
describe "apply_operation/2 given :add_cell_evaluation_stdout" do
|
||||
test "update the cell output" do
|
||||
# TODO assert against output being updated once we do so
|
||||
test "updates the cell outputs" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, 0, "s1"},
|
||||
{:insert_cell, "s1", 0, :elixir, "c1"},
|
||||
{:queue_cell_evaluation, "c1"}
|
||||
])
|
||||
|
||||
operation = {:add_cell_evaluation_stdout, "c1", "Hello!"}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
notebook: %{
|
||||
sections: [
|
||||
%{
|
||||
cells: [%{outputs: ["Hello!"]}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}, []} = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "merges consecutive stdout results" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, 0, "s1"},
|
||||
{:insert_cell, "s1", 0, :elixir, "c1"},
|
||||
{:queue_cell_evaluation, "c1"},
|
||||
{:add_cell_evaluation_stdout, "c1", "Hello"}
|
||||
])
|
||||
|
||||
operation = {:add_cell_evaluation_stdout, "c1", " amigo!"}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
notebook: %{
|
||||
sections: [
|
||||
%{
|
||||
cells: [%{outputs: ["Hello amigo!"]}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}, []} = Data.apply_operation(data, operation)
|
||||
end
|
||||
end
|
||||
|
||||
describe "apply_operation/2 given :add_cell_evaluation_response" do
|
||||
test "update the cell output" do
|
||||
# TODO assert against output being updated once we do so
|
||||
test "updates the cell outputs" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, 0, "s1"},
|
||||
{:insert_cell, "s1", 0, :elixir, "c1"},
|
||||
{:queue_cell_evaluation, "c1"}
|
||||
])
|
||||
|
||||
operation = {:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
notebook: %{
|
||||
sections: [
|
||||
%{
|
||||
cells: [%{outputs: [{:ok, [1, 2, 3]}]}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}, []} = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "marks the cell as evaluated" do
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule LiveBookWeb.SessionLiveTest do
|
|||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias LiveBook.{SessionSupervisor, Session}
|
||||
alias LiveBook.{SessionSupervisor, Session, Delta}
|
||||
|
||||
setup do
|
||||
{:ok, session_id} = SessionSupervisor.create_session()
|
||||
|
@ -29,48 +29,42 @@ defmodule LiveBookWeb.SessionLiveTest do
|
|||
test "renders a newly inserted section", %{conn: conn, session_id: session_id} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
|
||||
Session.insert_section(session_id, 0)
|
||||
%{notebook: %{sections: [section]}} = Session.get_data(session_id)
|
||||
section_id = insert_section(session_id)
|
||||
|
||||
assert render(view) =~ section.id
|
||||
assert render(view) =~ section_id
|
||||
end
|
||||
|
||||
test "renders an updated section name", %{conn: conn, session_id: session_id} do
|
||||
Session.insert_section(session_id, 0)
|
||||
%{notebook: %{sections: [section]}} = Session.get_data(session_id)
|
||||
section_id = insert_section(session_id)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
|
||||
Session.set_section_name(session_id, section.id, "My section")
|
||||
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
|
||||
Session.insert_section(session_id, 0)
|
||||
%{notebook: %{sections: [section]}} = Session.get_data(session_id)
|
||||
section_id = insert_section(session_id)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
|
||||
Session.insert_cell(session_id, section.id, 0, :markdown)
|
||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id)
|
||||
cell_id = insert_cell(session_id, section_id, :markdown)
|
||||
|
||||
assert render(view) =~ cell.id
|
||||
assert render(view) =~ cell_id
|
||||
end
|
||||
|
||||
test "un-renders a deleted cell", %{conn: conn, session_id: session_id} do
|
||||
Session.insert_section(session_id, 0)
|
||||
%{notebook: %{sections: [section]}} = Session.get_data(session_id)
|
||||
Session.insert_cell(session_id, section.id, 0, :markdown)
|
||||
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id)
|
||||
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)
|
||||
Session.delete_cell(session_id, cell_id)
|
||||
wait_for_session_update(session_id)
|
||||
|
||||
refute render(view) =~ cell.id
|
||||
refute render(view) =~ cell_id
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -97,6 +91,40 @@ defmodule LiveBookWeb.SessionLiveTest do
|
|||
assert %{notebook: %{sections: [%{cells: [%{type: :markdown}]}]}} =
|
||||
Session.get_data(session_id)
|
||||
end
|
||||
|
||||
test "queueing cell evaluation updates the shared state", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
||||
|
||||
{: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 "queueing cell evaluation defaults to the focused cell if no cell id is given",
|
||||
%{conn: conn, session_id: session_id} do
|
||||
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("focus_cell", %{"cell_id" => cell_id})
|
||||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("queue_cell_evaluation", %{})
|
||||
|
||||
assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} =
|
||||
Session.get_data(session_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_for_session_update(session_id) do
|
||||
|
@ -105,4 +133,23 @@ defmodule LiveBookWeb.SessionLiveTest do
|
|||
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, self(), cell.id, delta, 1)
|
||||
|
||||
cell.id
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue