fix: issue #20 cmd-J panel tab strip visibility and output panel parity
This commit is contained in:
@@ -129,6 +129,31 @@ defmodule BDS.Desktop.AutomationTest do
|
||||
assert automation_process_counts() == baseline
|
||||
end
|
||||
|
||||
@tag timeout: 120_000
|
||||
test "cmd-J panel tab strip is visible and switches between tasks and output (issue #20)" do
|
||||
{:ok, session} = Automation.start_session()
|
||||
|
||||
on_exit(fn ->
|
||||
Automation.stop_session(session)
|
||||
end)
|
||||
|
||||
assert :ok = Automation.press(session, "Meta+J")
|
||||
|
||||
snapshot = await(session, & &1.panel_visible)
|
||||
assert snapshot.panel_visible == true
|
||||
|
||||
# The tab strip must offer both tabs and be tall enough to actually see
|
||||
# and hit them — issue #20 was the strip collapsing to an unusable sliver.
|
||||
assert "Tasks" in snapshot.panel_tab_labels
|
||||
assert "Output" in snapshot.panel_tab_labels
|
||||
assert snapshot.panel_tabs_height >= 20
|
||||
|
||||
assert :ok = Automation.click(session, "button[phx-value-tab='output']")
|
||||
|
||||
snapshot = await(session, &(&1.panel_active_tab == "Output"))
|
||||
assert snapshot.panel_active_tab == "Output"
|
||||
end
|
||||
|
||||
@tag timeout: 120_000
|
||||
test "automation dispatches native menu actions into the liveview shell" do
|
||||
{:ok, session} = Automation.start_session()
|
||||
|
||||
@@ -907,6 +907,120 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
assert html =~ ~s(data-tab-id="#{created_post.id}")
|
||||
end
|
||||
|
||||
test "tasks panel shows a cancel button for running tasks and cancels them" do
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
{:ok, task} =
|
||||
BDS.Tasks.submit_task("Long Running Task", fn _report ->
|
||||
receive do
|
||||
:finish -> :ok
|
||||
after
|
||||
30_000 -> :ok
|
||||
end
|
||||
end)
|
||||
|
||||
send(view.pid, :refresh_task_status)
|
||||
|
||||
html = render_click(view, "select_panel_tab", %{"tab" => "tasks"})
|
||||
|
||||
assert html =~ "Long Running Task"
|
||||
assert html =~ ~s(data-testid="cancel-task-button")
|
||||
|
||||
view
|
||||
|> element(~s{[data-testid="cancel-task-button"]})
|
||||
|> render_click()
|
||||
|
||||
assert Enum.find(BDS.Tasks.list_tasks(), &(&1.id == task.id)).status == :cancelled
|
||||
end
|
||||
|
||||
test "tasks panel groups tasks by group_id and toggles the group collapsed" do
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
blocking = fn _report ->
|
||||
receive do
|
||||
:finish -> :ok
|
||||
after
|
||||
30_000 -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, _t1} = BDS.Tasks.submit_task("First Grouped", blocking, %{group_id: "grp", group_name: "Batch"})
|
||||
{:ok, _t2} = BDS.Tasks.submit_task("Second Grouped", blocking, %{group_id: "grp", group_name: "Batch"})
|
||||
|
||||
send(view.pid, :refresh_task_status)
|
||||
|
||||
html = render_click(view, "select_panel_tab", %{"tab" => "tasks"})
|
||||
|
||||
# One group row for both tasks, expanded by default (task names also
|
||||
# appear in the status bar, hence the <strong> row scoping)
|
||||
assert html =~ "Batch (2)"
|
||||
assert html =~ "<strong>First Grouped</strong>"
|
||||
assert html =~ "<strong>Second Grouped</strong>"
|
||||
|
||||
html = render_click(view, "toggle_task_group", %{"group" => "grp"})
|
||||
|
||||
assert html =~ "Batch (2)"
|
||||
refute html =~ "<strong>First Grouped</strong>"
|
||||
refute html =~ "<strong>Second Grouped</strong>"
|
||||
|
||||
html = render_click(view, "toggle_task_group", %{"group" => "grp"})
|
||||
|
||||
assert html =~ "<strong>First Grouped</strong>"
|
||||
assert html =~ "<strong>Second Grouped</strong>"
|
||||
end
|
||||
|
||||
test "output panel copy button pushes the full log text to the clipboard" do
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
send(view.pid, {:editor_output, "First", "line one", nil, "info"})
|
||||
send(view.pid, {:editor_output, "Second", "line two", nil, "error"})
|
||||
|
||||
html = render_click(view, "select_panel_tab", %{"tab" => "output"})
|
||||
|
||||
assert html =~ ~s(data-testid="copy-output-button")
|
||||
|
||||
view
|
||||
|> element(~s{[data-testid="copy-output-button"]})
|
||||
|> render_click()
|
||||
|
||||
# Chronological order (oldest first), entries joined by a blank line.
|
||||
assert_push_event(view, "clipboard", %{text: "line one\n\nline two"})
|
||||
end
|
||||
|
||||
test "blogmark transform output lands in the output panel and auto-opens it", %{
|
||||
project: project
|
||||
} do
|
||||
{:ok, _script} =
|
||||
Scripts.create_and_publish_script(%{
|
||||
project_id: project.id,
|
||||
title: "Logging Transform",
|
||||
kind: :transform,
|
||||
entrypoint: "main",
|
||||
content:
|
||||
"function main(data, ctx) bds.app.log('transform ran') print('still ran') return data end"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
url =
|
||||
"bds2://new-post?title=" <>
|
||||
URI.encode_www_form("Logged Blogmark") <>
|
||||
"&url=" <> URI.encode_www_form("https://example.com/logged")
|
||||
|
||||
send(view.pid, {:blogmark_deep_link, url})
|
||||
_html = render(view)
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
messages = Enum.map(assigns.output_entries, & &1.message)
|
||||
|
||||
assert "transform ran" in messages
|
||||
assert "still ran" in messages
|
||||
|
||||
# Like the old app: noteworthy script output opens the panel on Output.
|
||||
assert assigns.workbench.panel.visible == true
|
||||
assert assigns.workbench.panel.active_tab == :output
|
||||
end
|
||||
|
||||
test "blogmark deep link with a project_id switches to that project and imports there",
|
||||
%{project: active} do
|
||||
{:ok, other} =
|
||||
@@ -6052,6 +6166,38 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
assert reloaded.version == 1
|
||||
end
|
||||
|
||||
test "script editor run streams print and bds.app.log output to the output panel", %{
|
||||
project: project
|
||||
} do
|
||||
{:ok, script} =
|
||||
Scripts.create_script(%{
|
||||
project_id: project.id,
|
||||
title: "Output Test",
|
||||
kind: :utility,
|
||||
content:
|
||||
"function main() print('from-print') bds.app.log('from-log') return 'done' end"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
render_click(view, "pin_sidebar_item", %{
|
||||
"route" => "scripts",
|
||||
"id" => script.id,
|
||||
"title" => script.title,
|
||||
"subtitle" => "draft"
|
||||
})
|
||||
|
||||
view
|
||||
|> element(".scripts-run-button")
|
||||
|> render_click()
|
||||
|
||||
entries = :sys.get_state(view.pid).socket.assigns.output_entries
|
||||
messages = Enum.map(entries, & &1.message)
|
||||
|
||||
assert "from-print" in messages
|
||||
assert "from-log" in messages
|
||||
end
|
||||
|
||||
test "script editor check syntax validates lua content without saving", %{project: project} do
|
||||
{:ok, script} =
|
||||
Scripts.create_script(%{
|
||||
|
||||
@@ -34,6 +34,57 @@ defmodule BDS.Scripting.LuaTest do
|
||||
assert_receive {:progress, %{"phase" => "write", "current" => 2, "total" => 2}}
|
||||
end
|
||||
|
||||
test "streams bds.app.log output through the on_output callback" do
|
||||
parent = self()
|
||||
|
||||
source = """
|
||||
function main()
|
||||
bds.app.log('fetching')
|
||||
bds.app.log('done', 42)
|
||||
return 'ok'
|
||||
end
|
||||
"""
|
||||
|
||||
callback = fn text -> send(parent, {:script_output, text}) end
|
||||
|
||||
assert {:ok, "ok"} =
|
||||
BDS.Scripting.execute_project_script("project-id", source, "main", [],
|
||||
on_output: callback
|
||||
)
|
||||
|
||||
assert_receive {:script_output, "fetching"}
|
||||
assert_receive {:script_output, "done 42"}
|
||||
end
|
||||
|
||||
test "routes print calls through the on_output callback" do
|
||||
parent = self()
|
||||
|
||||
source = """
|
||||
function main()
|
||||
print('hello', 'world')
|
||||
return 'ok'
|
||||
end
|
||||
"""
|
||||
|
||||
callback = fn text -> send(parent, {:script_output, text}) end
|
||||
|
||||
assert {:ok, "ok"} = BDS.Scripting.execute(source, "main", [], on_output: callback)
|
||||
|
||||
assert_receive {:script_output, "hello world"}
|
||||
end
|
||||
|
||||
test "log and print fall back to a silent sink without an on_output callback" do
|
||||
source = """
|
||||
function main()
|
||||
bds.app.log('note')
|
||||
print('hello')
|
||||
return 'ok'
|
||||
end
|
||||
"""
|
||||
|
||||
assert {:ok, "ok"} = BDS.Scripting.execute_project_script("project-id", source, "main", [])
|
||||
end
|
||||
|
||||
test "sandbox blocks os.execute" do
|
||||
source = "function main() os.execute('echo hacked') end"
|
||||
|
||||
|
||||
78
test/bds/ui/task_grouping_test.exs
Normal file
78
test/bds/ui/task_grouping_test.exs
Normal file
@@ -0,0 +1,78 @@
|
||||
defmodule BDS.UI.TaskGroupingTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias BDS.UI.TaskGrouping
|
||||
|
||||
defp task(id, status, opts \\ %{}) do
|
||||
%{
|
||||
id: id,
|
||||
name: Map.get(opts, :name, "Task #{id}"),
|
||||
status: status,
|
||||
progress: Map.get(opts, :progress),
|
||||
message: nil,
|
||||
group_id: Map.get(opts, :group_id),
|
||||
group_name: Map.get(opts, :group_name)
|
||||
}
|
||||
end
|
||||
|
||||
describe "build_task_entries/1" do
|
||||
test "tasks without a group stay single entries in order" do
|
||||
entries = TaskGrouping.build_task_entries([task("a", :running), task("b", :completed)])
|
||||
|
||||
assert [{:single, %{id: "a"}}, {:single, %{id: "b"}}] = entries
|
||||
end
|
||||
|
||||
test "tasks with a group_id are grouped by first-seen order" do
|
||||
entries =
|
||||
TaskGrouping.build_task_entries([
|
||||
task("a", :running, %{group_id: "g1", group_name: "Import"}),
|
||||
task("b", :running),
|
||||
task("c", :pending, %{group_id: "g1", group_name: "Import"}),
|
||||
task("d", :completed, %{group_id: "g2", group_name: "Upload"})
|
||||
])
|
||||
|
||||
assert [
|
||||
{:group, "g1", "Import", [%{id: "a"}, %{id: "c"}]},
|
||||
{:single, %{id: "b"}},
|
||||
{:group, "g2", "Upload", [%{id: "d"}]}
|
||||
] = entries
|
||||
end
|
||||
|
||||
test "group label falls back to the group_id when no group_name is set" do
|
||||
assert [{:group, "g1", "g1", [_]}] =
|
||||
TaskGrouping.build_task_entries([task("a", :running, %{group_id: "g1"})])
|
||||
end
|
||||
end
|
||||
|
||||
describe "summarize_task_group/1" do
|
||||
test "counts statuses and averages progress contributions" do
|
||||
summary =
|
||||
TaskGrouping.summarize_task_group([
|
||||
task("a", :running, %{progress: 0.5}),
|
||||
task("b", :pending),
|
||||
task("c", :completed)
|
||||
])
|
||||
|
||||
assert summary.total == 3
|
||||
assert summary.running == 1
|
||||
assert summary.pending == 1
|
||||
assert summary.completed == 1
|
||||
assert summary.failed == 0
|
||||
assert summary.cancelled == 0
|
||||
# contributions: 0.5 (running) + 0 (pending) + 1.0 (completed)
|
||||
assert_in_delta summary.progress, 0.5, 0.001
|
||||
end
|
||||
|
||||
test "finished failures and cancellations count as full progress" do
|
||||
summary = TaskGrouping.summarize_task_group([task("a", :failed), task("b", :cancelled)])
|
||||
|
||||
assert summary.progress == 1.0
|
||||
assert summary.failed == 1
|
||||
assert summary.cancelled == 1
|
||||
end
|
||||
|
||||
test "empty group summarizes to zero" do
|
||||
assert %{total: 0, progress: 0.0} = TaskGrouping.summarize_task_group([])
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user