fix: drag-resize working now
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -24,10 +24,18 @@ defmodule BDS.Desktop.Automation do
|
|||||||
GenServer.call(session, {:click, selector}, @request_timeout)
|
GenServer.call(session, {:click, selector}, @request_timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def drag(session, selector, delta_x) when is_binary(selector) and is_integer(delta_x) do
|
||||||
|
GenServer.call(session, {:drag, selector, delta_x}, @request_timeout)
|
||||||
|
end
|
||||||
|
|
||||||
def press(session, shortcut) when is_binary(shortcut) do
|
def press(session, shortcut) when is_binary(shortcut) do
|
||||||
GenServer.call(session, {:press, shortcut}, @request_timeout)
|
GenServer.call(session, {:press, shortcut}, @request_timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reload(session) do
|
||||||
|
GenServer.call(session, :reload, @request_timeout)
|
||||||
|
end
|
||||||
|
|
||||||
def capture_screenshot(session, destination) when is_binary(destination) do
|
def capture_screenshot(session, destination) when is_binary(destination) do
|
||||||
GenServer.call(session, {:capture_screenshot, destination}, @request_timeout)
|
GenServer.call(session, {:capture_screenshot, destination}, @request_timeout)
|
||||||
end
|
end
|
||||||
@@ -82,11 +90,23 @@ defmodule BDS.Desktop.Automation do
|
|||||||
{:reply, normalize_simple_reply(reply), state}
|
{:reply, normalize_simple_reply(reply), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_call({:drag, selector, delta_x}, _from, state) do
|
||||||
|
{reply, state} =
|
||||||
|
driver_request(state, %{"command" => "drag", "selector" => selector, "deltaX" => delta_x})
|
||||||
|
|
||||||
|
{:reply, normalize_simple_reply(reply), state}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_call({:press, shortcut}, _from, state) do
|
def handle_call({:press, shortcut}, _from, state) do
|
||||||
{reply, state} = driver_request(state, %{"command" => "press", "shortcut" => shortcut})
|
{reply, state} = driver_request(state, %{"command" => "press", "shortcut" => shortcut})
|
||||||
{:reply, normalize_simple_reply(reply), state}
|
{:reply, normalize_simple_reply(reply), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_call(:reload, _from, state) do
|
||||||
|
{reply, state} = driver_request(state, %{"command" => "reload"})
|
||||||
|
{:reply, normalize_simple_reply(reply), state}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_call({:capture_screenshot, destination}, _from, state) do
|
def handle_call({:capture_screenshot, destination}, _from, state) do
|
||||||
File.mkdir_p!(Path.dirname(destination))
|
File.mkdir_p!(Path.dirname(destination))
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
{:noreply, open_sidebar_item(socket, params, :pin)}
|
{:noreply, open_sidebar_item(socket, params, :pin)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("sync_layout", params, socket) do
|
||||||
|
{:noreply, reload_shell(socket, sync_layout(socket.assigns.workbench, params))}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("resize_panel", %{"target" => target, "width" => width}, socket) do
|
||||||
|
{:noreply, reload_shell(socket, resize_panel(socket.assigns.workbench, target, width))}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("shortcut", params, socket) do
|
def handle_event("shortcut", params, socket) do
|
||||||
if ignore_shortcut?(params) do
|
if ignore_shortcut?(params) do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
@@ -439,6 +447,44 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
Enum.find(tabs, &(&1.type == type and &1.id == id))
|
Enum.find(tabs, &(&1.type == type and &1.id == id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp sync_layout(workbench, params) do
|
||||||
|
workbench
|
||||||
|
|> maybe_set_sidebar_width(Map.get(params, "sidebar_width"))
|
||||||
|
|> maybe_set_assistant_width(Map.get(params, "assistant_sidebar_width"))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resize_panel(workbench, "sidebar", width) do
|
||||||
|
workbench
|
||||||
|
|> Workbench.set_sidebar_width(parse_width(width))
|
||||||
|
|> Map.put(:sidebar_visible, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resize_panel(workbench, "assistant", width) do
|
||||||
|
workbench
|
||||||
|
|> Workbench.set_assistant_sidebar_width(parse_width(width))
|
||||||
|
|> Map.put(:assistant_sidebar_visible, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resize_panel(workbench, _target, _width), do: workbench
|
||||||
|
|
||||||
|
defp maybe_set_sidebar_width(workbench, nil), do: workbench
|
||||||
|
defp maybe_set_sidebar_width(workbench, width), do: Workbench.set_sidebar_width(workbench, parse_width(width))
|
||||||
|
|
||||||
|
defp maybe_set_assistant_width(workbench, nil), do: workbench
|
||||||
|
|
||||||
|
defp maybe_set_assistant_width(workbench, width) do
|
||||||
|
Workbench.set_assistant_sidebar_width(workbench, parse_width(width))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_width(width) when is_integer(width), do: width
|
||||||
|
|
||||||
|
defp parse_width(width) when is_binary(width) do
|
||||||
|
case Integer.parse(width) do
|
||||||
|
{parsed, _rest} -> parsed
|
||||||
|
:error -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp ignore_shortcut?(params) do
|
defp ignore_shortcut?(params) do
|
||||||
Map.get(params, "alt", false) or
|
Map.get(params, "alt", false) or
|
||||||
Map.get(params, "contentEditable", false) or
|
Map.get(params, "contentEditable", false) or
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="app" id="bds-shell-app" phx-window-keydown="shortcut">
|
<div class="app" id="bds-shell-app" phx-hook="AppShell" phx-window-keydown="shortcut">
|
||||||
<div class="window-titlebar" data-region="title-bar">
|
<div class="window-titlebar" data-region="title-bar">
|
||||||
<div class="window-titlebar-menu-bar is-hidden">
|
<div class="window-titlebar-menu-bar is-hidden">
|
||||||
<button class="window-titlebar-menu-button" type="button">File</button>
|
<button class="window-titlebar-menu-button" type="button">File</button>
|
||||||
|
|||||||
109
priv/ui/live.js
109
priv/ui/live.js
@@ -3,7 +3,116 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
.querySelector("meta[name='csrf-token']")
|
.querySelector("meta[name='csrf-token']")
|
||||||
.getAttribute("content");
|
.getAttribute("content");
|
||||||
|
|
||||||
|
const SIDEBAR_STORAGE_KEY = "bds-panel-sidebar";
|
||||||
|
const ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar";
|
||||||
|
|
||||||
|
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
||||||
|
|
||||||
|
const readStoredSize = (key, fallback, min, max) => {
|
||||||
|
const raw = window.localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(raw, 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(parsed)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clamp(parsed, min, max);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shellWidth = (selector) => {
|
||||||
|
const shell = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (!shell) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = Number.parseInt(shell.style.width || "0", 10);
|
||||||
|
return Number.isNaN(width) ? Math.round(shell.getBoundingClientRect().width) : width;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setShellWidth = (selector, width) => {
|
||||||
|
const shell = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (shell) {
|
||||||
|
shell.style.width = `${width}px`;
|
||||||
|
shell.classList.remove("is-hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistWidth = (target, width) => {
|
||||||
|
const key = target === "assistant" ? ASSISTANT_STORAGE_KEY : SIDEBAR_STORAGE_KEY;
|
||||||
|
window.localStorage.setItem(key, String(width));
|
||||||
|
};
|
||||||
|
|
||||||
const Hooks = {
|
const Hooks = {
|
||||||
|
AppShell: {
|
||||||
|
mounted() {
|
||||||
|
this.syncStoredLayout();
|
||||||
|
|
||||||
|
this.handleMouseDown = (event) => {
|
||||||
|
const handle = event.target.closest("[data-role='resize-handle']");
|
||||||
|
|
||||||
|
if (!handle || !this.el.contains(handle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const target = handle.dataset.resize;
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startWidth =
|
||||||
|
target === "assistant"
|
||||||
|
? shellWidth("[data-testid='assistant-shell']")
|
||||||
|
: shellWidth("[data-testid='sidebar-shell']");
|
||||||
|
|
||||||
|
const min = target === "assistant" ? 280 : 200;
|
||||||
|
const max = target === "assistant" ? 640 : 500;
|
||||||
|
const invert = target === "assistant";
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
const delta = invert ? startX - moveEvent.clientX : moveEvent.clientX - startX;
|
||||||
|
const width = clamp(startWidth + delta, min, max);
|
||||||
|
const selector = target === "assistant" ? "[data-testid='assistant-shell']" : "[data-testid='sidebar-shell']";
|
||||||
|
|
||||||
|
setShellWidth(selector, width);
|
||||||
|
persistWidth(target, width);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (upEvent) => {
|
||||||
|
const delta = invert ? startX - upEvent.clientX : upEvent.clientX - startX;
|
||||||
|
const width = clamp(startWidth + delta, min, max);
|
||||||
|
|
||||||
|
persistWidth(target, width);
|
||||||
|
this.pushEvent("resize_panel", { target, width });
|
||||||
|
|
||||||
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.el.addEventListener("mousedown", this.handleMouseDown);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
this.el.removeEventListener("mousedown", this.handleMouseDown);
|
||||||
|
},
|
||||||
|
|
||||||
|
syncStoredLayout() {
|
||||||
|
this.pushEvent("sync_layout", {
|
||||||
|
sidebar_width: readStoredSize(SIDEBAR_STORAGE_KEY, shellWidth("[data-testid='sidebar-shell']"), 200, 500),
|
||||||
|
assistant_sidebar_width: readStoredSize(ASSISTANT_STORAGE_KEY, 360, 280, 640)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
SidebarInteractions: {
|
SidebarInteractions: {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.handleDblClick = (event) => {
|
this.handleDblClick = (event) => {
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ for await (const line of rl) {
|
|||||||
window_title: text("[data-testid='window-title']"),
|
window_title: text("[data-testid='window-title']"),
|
||||||
active_view: document.querySelector("[data-testid='activity-button'][data-active='true']")?.dataset.view ?? null,
|
active_view: document.querySelector("[data-testid='activity-button'][data-active='true']")?.dataset.view ?? null,
|
||||||
sidebar_visible: !hasClass("[data-testid='sidebar-shell']", "is-hidden"),
|
sidebar_visible: !hasClass("[data-testid='sidebar-shell']", "is-hidden"),
|
||||||
|
sidebar_width: document.querySelector("[data-testid='sidebar-shell']")?.getBoundingClientRect().width ?? 0,
|
||||||
assistant_visible: !hasClass("[data-testid='assistant-shell']", "is-hidden"),
|
assistant_visible: !hasClass("[data-testid='assistant-shell']", "is-hidden"),
|
||||||
|
assistant_width: document.querySelector("[data-testid='assistant-shell']")?.getBoundingClientRect().width ?? 0,
|
||||||
panel_visible: !hasClass(".panel-shell", "is-hidden"),
|
panel_visible: !hasClass(".panel-shell", "is-hidden"),
|
||||||
editor_title: text("[data-testid='editor-title']"),
|
editor_title: text("[data-testid='editor-title']"),
|
||||||
activity_labels: texts("[data-testid='activity-button']", (node) => node.getAttribute("aria-label")),
|
activity_labels: texts("[data-testid='activity-button']", (node) => node.getAttribute("aria-label")),
|
||||||
@@ -70,6 +72,33 @@ for await (const line of rl) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.command === "drag") {
|
||||||
|
const locator = page.locator(message.selector);
|
||||||
|
const box = await locator.boundingBox();
|
||||||
|
|
||||||
|
if (!box) {
|
||||||
|
throw new Error(`unable to drag missing element: ${message.selector}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startX = box.x + box.width / 2;
|
||||||
|
const startY = box.y + box.height / 2;
|
||||||
|
await page.mouse.move(startX, startY);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(startX + message.deltaX, startY, { steps: 10 });
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
console.log(JSON.stringify({ ref, status: "ok", result: "ok" }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.command === "reload") {
|
||||||
|
await page.reload({ waitUntil: "networkidle" });
|
||||||
|
await page.locator("#bds-shell-app").waitFor({ state: "visible" });
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
console.log(JSON.stringify({ ref, status: "ok", result: "ok" }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.command === "screenshot") {
|
if (message.command === "screenshot") {
|
||||||
await page.screenshot({ path: message.path, fullPage: false });
|
await page.screenshot({ path: message.path, fullPage: false });
|
||||||
console.log(JSON.stringify({ ref, status: "ok", result: message.path }));
|
console.log(JSON.stringify({ ref, status: "ok", result: message.path }));
|
||||||
|
|||||||
@@ -71,6 +71,34 @@ defmodule BDS.Desktop.AutomationTest do
|
|||||||
assert File.exists?(screenshot_path)
|
assert File.exists?(screenshot_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag timeout: 120_000
|
||||||
|
test "automation drags sidebar resize handle and restores width after reload" do
|
||||||
|
{:ok, session} = Automation.start_session()
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Automation.stop_session(session)
|
||||||
|
end)
|
||||||
|
|
||||||
|
snapshot = Automation.snapshot(session)
|
||||||
|
assert snapshot.sidebar_width >= 279
|
||||||
|
assert snapshot.sidebar_width <= 281
|
||||||
|
|
||||||
|
assert :ok = Automation.drag(session, "[data-resize='sidebar']", 90)
|
||||||
|
|
||||||
|
snapshot = Automation.snapshot(session)
|
||||||
|
assert snapshot.sidebar_width >= 360
|
||||||
|
assert snapshot.sidebar_width <= 380
|
||||||
|
|
||||||
|
resized_width = snapshot.sidebar_width
|
||||||
|
|
||||||
|
assert :ok = Automation.reload(session)
|
||||||
|
|
||||||
|
snapshot = Automation.snapshot(session)
|
||||||
|
assert snapshot.sidebar_visible == true
|
||||||
|
assert snapshot.sidebar_width >= resized_width - 2
|
||||||
|
assert snapshot.sidebar_width <= resized_width + 2
|
||||||
|
end
|
||||||
|
|
||||||
@tag timeout: 120_000
|
@tag timeout: 120_000
|
||||||
test "automation stop_session shuts down the app and browser child processes it started" do
|
test "automation stop_session shuts down the app and browser child processes it started" do
|
||||||
baseline = automation_process_counts()
|
baseline = automation_process_counts()
|
||||||
|
|||||||
@@ -169,4 +169,28 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert html =~ ~s(class="sidebar-shell is-hidden")
|
assert html =~ ~s(class="sidebar-shell is-hidden")
|
||||||
assert html =~ ~s(style="width: 0px;")
|
assert html =~ ~s(style="width: 0px;")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "layout hooks sync persisted widths and apply drag resizing" do
|
||||||
|
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
|
assert html =~ ~s(style="width: 280px;")
|
||||||
|
|
||||||
|
html = render_hook(view, "sync_layout", %{"sidebar_width" => 420, "assistant_sidebar_width" => 480})
|
||||||
|
|
||||||
|
assert html =~ ~s(data-testid="sidebar-shell")
|
||||||
|
assert html =~ ~s(style="width: 420px;")
|
||||||
|
|
||||||
|
html =
|
||||||
|
view
|
||||||
|
|> element("[data-testid='toggle-assistant']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert html =~ ~s(data-testid="assistant-shell")
|
||||||
|
assert html =~ ~s(style="width: 480px;")
|
||||||
|
|
||||||
|
html = render_hook(view, "resize_panel", %{"target" => "sidebar", "width" => 460})
|
||||||
|
|
||||||
|
assert html =~ ~s(data-testid="sidebar-shell")
|
||||||
|
assert html =~ ~s(style="width: 460px;")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user