mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
fix(app): preserve browser interactions in retained workspace hosts
Initially disabled gesture hosts still registered pointer and wheel listeners, while hover cards used window positions inside a workspace-local sheet portal. Respect initial gesture enablement and position cards relative to their measured floating host. Keep developer-toast dismissal independent of pointer input and replacement toast visibility so modal, hover, and focus checks retain their input state.
This commit is contained in:
@@ -32,7 +32,9 @@ The UI worklet owns transient motion:
|
||||
|
||||
React publishes the active panel only when the canonical target and the UI-thread position agree at
|
||||
the final anchor. Retained content never observes gesture previews, progress, or an unsettled target.
|
||||
The gesture hosts stay mounted. Native worklets own overlay opacity and pointer events while
|
||||
The gesture hosts stay mounted. The Gesture Handler web patch keeps initially disabled hosts
|
||||
from capturing paragraph pointers; that capture confines browser selection to its starting paragraph.
|
||||
Native worklets own overlay opacity and pointer events while
|
||||
React owns settled panel activity and accessibility. Keep hidden native overlays laid out: collapsing
|
||||
their scroll ranges makes Android spring retained offsets back to zero during touch cancellation or
|
||||
release. Native panel hosts and their dependent draggable lists
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ Test suites in this repo are heavy. Running them in bulk freezes the machine, es
|
||||
- Playwright specs that exercise only the daemon import `daemonTest` from the shared fixtures so they do not create a browser context or page.
|
||||
- Helpers that create projects or workspaces own those records until cleanup. Their clients remove the daemon project on close, and an automatic fixture fails any test that still leaks a project record. Deleting only the temporary directory is not cleanup. Agent helpers pass the intended `workspaceId` through to agent creation; they never infer ownership from `cwd`.
|
||||
- Tests whose subject is daemon-global state, such as an empty history or daemon restart, start a dedicated host explicitly. Filenames and directories describe product behavior, never execution order or isolation mechanics.
|
||||
- Global setup accepts Metro as ready only when `/status` returns `packager-status:running`, then fetches the document's scripts so the cold bundle compilation finishes before Playwright's per-test timeout starts. A generic TCP listener is not sufficient readiness evidence. The browser suite uses direct local daemon connections and does not start a relay. Automated Metro launches set `EXPO_UNSTABLE_HEADLESS=1` so Expo does not download the native DevTools shell into temporary test homes; its read-only cache breaks teardown. Browser fixtures dismiss Expo's developer error toast through its close button while retaining console logs, so it cannot cover compact bottom actions.
|
||||
- Global setup accepts Metro as ready only when `/status` returns `packager-status:running`, then fetches the document's scripts so the cold bundle compilation finishes before Playwright's per-test timeout starts. A generic TCP listener is not sufficient readiness evidence. The browser suite uses direct local daemon connections and does not start a relay. Automated Metro launches set `EXPO_UNSTABLE_HEADLESS=1` so Expo does not download the native DevTools shell into temporary test homes; its read-only cache breaks teardown. Browser fixtures dispatch Expo's developer-toast close action while retaining console logs. A pointer click moves hover away from the tested control and stalls behind app modal backdrops.
|
||||
- The app Playwright harness boots on Windows as well as POSIX. Spawn Node entrypoints through `process.execPath`, not `npx` or `node_modules/.bin` shims: Node refuses to spawn `.cmd` or `.bat` without `shell: true`, and shell mode concatenates argv without escaping and sends kill signals to `cmd.exe` instead of the real child.
|
||||
- Teardown kills the process tree, because a Windows signal reaches only the direct child and leaves forked workers holding the listening port.
|
||||
- The `asdf`-backed local Elixir relay stays POSIX-only, so the `relay-deployment` Playwright project is unavailable on Windows.
|
||||
|
||||
@@ -5,10 +5,6 @@ import path from "node:path";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { connectNewWorkspaceDaemonClient } from "../support/helpers/new-workspace";
|
||||
import {
|
||||
expectMobileAgentSidebarVisible,
|
||||
openMobileAgentSidebar,
|
||||
} from "../support/helpers/sidebar";
|
||||
|
||||
const PLUGIN_ID = "plugin-host-ui-e2e";
|
||||
|
||||
@@ -91,12 +87,6 @@ async function useCompactLayout(page: Page): Promise<void> {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
}
|
||||
|
||||
async function reopenHostUiPluginFromCompactSidebar(page: Page): Promise<void> {
|
||||
await openMobileAgentSidebar(page);
|
||||
await expectMobileAgentSidebarVisible(page);
|
||||
await page.getByRole("button", { name: "Host UI", exact: true }).click();
|
||||
}
|
||||
|
||||
async function openPluginModal(page: Page): Promise<void> {
|
||||
await page.getByRole("button", { name: "Open plugin modal", exact: true }).click();
|
||||
}
|
||||
@@ -158,7 +148,6 @@ test("plugin modal adapts its presentation and preserves host contexts", async (
|
||||
|
||||
await test.step("compact layouts preserve host contexts inside a sheet", async () => {
|
||||
await useCompactLayout(page);
|
||||
await reopenHostUiPluginFromCompactSidebar(page);
|
||||
await openPluginModal(page);
|
||||
await expectCompactPluginSheet(page);
|
||||
await savePluginIssue(page);
|
||||
|
||||
@@ -2,10 +2,6 @@ import { copyPluginExample } from "../support/helpers/plugin-fixture";
|
||||
import { expect, test as base, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { connectNewWorkspaceDaemonClient } from "../support/helpers/new-workspace";
|
||||
import {
|
||||
openMobileAgentSidebar,
|
||||
expectMobileAgentSidebarVisible,
|
||||
} from "../support/helpers/sidebar";
|
||||
|
||||
async function openExample(page: Page, example: string) {
|
||||
await page.getByRole("button", { name: `Open ${example}`, exact: true }).click();
|
||||
@@ -122,9 +118,7 @@ async function openWideExamples(page: Page) {
|
||||
|
||||
async function openCompactExamples(page: Page) {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openMobileAgentSidebar(page);
|
||||
await expectMobileAgentSidebarVisible(page);
|
||||
await page.getByRole("button", { name: "Modal examples", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "Open Form", exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
async function expectDeniedCopyFeedback(page: Page) {
|
||||
|
||||
@@ -15,6 +15,7 @@ async function closeMenuSheet(page: Page): Promise<void> {
|
||||
|
||||
test("project and workspace kebabs open action sheets on compact layouts", async ({ page }) => {
|
||||
const seeded = await seedWorkspace({ repoPrefix: "sidebar-mobile-menu-sheet-" });
|
||||
const secondProject = await seedWorkspace({ repoPrefix: "sidebar-mobile-menu-filter-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
@@ -26,6 +27,7 @@ test("project and workspace kebabs open action sheets on compact layouts", async
|
||||
/^Grouping/,
|
||||
/^Title/,
|
||||
"Show",
|
||||
"Project",
|
||||
"Show background",
|
||||
]);
|
||||
await closeMenuSheet(page);
|
||||
@@ -53,6 +55,7 @@ test("project and workspace kebabs open action sheets on compact layouts", async
|
||||
});
|
||||
await expect(page.getByText("Workspace actions", { exact: true })).toBeVisible();
|
||||
} finally {
|
||||
await secondProject.cleanup();
|
||||
await seeded.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -624,7 +624,9 @@ test.describe("Half-screen desktop layout", () => {
|
||||
await expect(page.getByTestId("sidebar-settings")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps app navigation beside the Explorer pane", async ({ page }) => {
|
||||
test("yields app navigation to Explorer and restores it when Explorer closes", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-half-screen-explorer-" });
|
||||
|
||||
try {
|
||||
@@ -636,7 +638,7 @@ test.describe("Half-screen desktop layout", () => {
|
||||
const explorerToggle = page.getByTestId("workspace-explorer-toggle").first();
|
||||
await expect(page.getByTestId("workspace-tab-files").filter({ visible: true })).toBeVisible();
|
||||
await expect(explorerToggle).toHaveAccessibleName("Close Explorer sidebar");
|
||||
await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible();
|
||||
await expect(page.getByTestId("sidebar-global-new-workspace")).not.toBeVisible();
|
||||
await expect(page.getByTestId("workspace-explorer-sidebar")).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
|
||||
@@ -606,7 +606,10 @@ for (const compact of [false, true]) {
|
||||
if (compact) await openCompactSidebar(page);
|
||||
await openUsageFromIcon(page);
|
||||
await expectLoginErrors(page, ["Claude: Initial failure"]);
|
||||
await expectLoginUsageFreshness(page, "No successful update", compact);
|
||||
await test.step("developer errors do not interrupt freshness inspection", async () => {
|
||||
await page.evaluate(() => console.error("E2E: developer toast while Usage is open"));
|
||||
await expectLoginUsageFreshness(page, "No successful update", compact);
|
||||
});
|
||||
|
||||
await fixture.setReport({
|
||||
status: "available",
|
||||
|
||||
@@ -131,11 +131,15 @@ const test = daemonTest.extend<{
|
||||
});
|
||||
|
||||
// Expo's development LogBox toast overlaps bottom actions on compact screens.
|
||||
// Dismiss its UI through the close button; console errors still go into the test log.
|
||||
// Dispatch the developer UI's close action without moving the pointer or
|
||||
// clicking through an app modal. Console errors still go into the test log.
|
||||
const dismissDevToast = page
|
||||
.locator("#error-toast")
|
||||
.getByRole("button", { name: "", exact: true });
|
||||
await page.addLocatorHandler(dismissDevToast, () => dismissDevToast.click());
|
||||
await page.addLocatorHandler(dismissDevToast, () => dismissDevToast.dispatchEvent("click"), {
|
||||
// Another logged error can replace the toast before it disappears.
|
||||
noWaitAfter: true,
|
||||
});
|
||||
|
||||
const entries: string[] = [];
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function hoverContextWindowMeter(page: Page, name = METER_NAME): Pr
|
||||
await page.getByRole("img", { name, exact: true }).hover({ timeout: 30_000 });
|
||||
const card = contextWindowDetails(page);
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card).toBeInViewport();
|
||||
await expect(card).toBeInViewport({ ratio: 1 });
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,16 @@ import { Dimensions, View, type Role, type StyleProp, type ViewStyle } from "rea
|
||||
import { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { Portal } from "@gorhom/portal";
|
||||
import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
|
||||
import {
|
||||
OverlayLayerProvider,
|
||||
useOverlayLayer,
|
||||
useWebOverlayRegistration,
|
||||
} from "@/lib/overlay-root";
|
||||
import { FloatingSurface } from "@/components/ui/floating";
|
||||
import {
|
||||
measureFloatingPanelPortalHost,
|
||||
useFloatingPanelPortalHostName,
|
||||
} from "@/components/ui/floating-panel-portal";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
|
||||
import {
|
||||
@@ -326,28 +329,31 @@ function HoverCardSurface({
|
||||
triggerRef: RefObject<View | null>;
|
||||
contentRef: RefObject<View | null>;
|
||||
}): ReactElement {
|
||||
const bottomSheetInternal = useBottomSheetModalInternal(true);
|
||||
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
||||
const portalHostName = useFloatingPanelPortalHostName();
|
||||
const [anchor, setAnchor] = useState<{ triggerRect: Rect; hostRect: Rect } | null>(null);
|
||||
const [contentSize, setContentSize] = useState<Size | null>(null);
|
||||
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggerRef.current) return;
|
||||
let cancelled = false;
|
||||
void measureElement(triggerRef.current).then((rect) => {
|
||||
if (!cancelled) setTriggerRect(rect);
|
||||
void Promise.all([
|
||||
measureElement(triggerRef.current),
|
||||
measureFloatingPanelPortalHost(portalHostName),
|
||||
]).then(([triggerRect, hostRect]) => {
|
||||
if (!cancelled && hostRect) setAnchor({ triggerRect, hostRect });
|
||||
return undefined;
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [triggerRef]);
|
||||
}, [portalHostName, triggerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggerRect || !contentSize) return;
|
||||
if (!anchor || !contentSize) return;
|
||||
const { width, height } = Dimensions.get("window");
|
||||
const { x, y } = computePosition({
|
||||
triggerRect,
|
||||
triggerRect: anchor.triggerRect,
|
||||
contentSize,
|
||||
displayArea: { x: 0, y: 0, width, height },
|
||||
placement,
|
||||
@@ -355,8 +361,8 @@ function HoverCardSurface({
|
||||
alignment,
|
||||
offset,
|
||||
});
|
||||
setPosition({ x, y });
|
||||
}, [alignment, contentSize, offset, placement, triggerRect]);
|
||||
setPosition({ x: x - anchor.hostRect.x, y: y - anchor.hostRect.y });
|
||||
}, [alignment, anchor, contentSize, offset, placement]);
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(event: { nativeEvent: { layout: { width: number; height: number } } }) => {
|
||||
@@ -377,7 +383,7 @@ function HoverCardSurface({
|
||||
const surfaceStyle = useMemo(() => [styles.surface, style], [style]);
|
||||
|
||||
return (
|
||||
<Portal hostName={bottomSheetInternal?.hostName}>
|
||||
<Portal hostName={portalHostName}>
|
||||
<View pointerEvents="box-none" style={[styles.portalOverlay, { zIndex: layer }]}>
|
||||
<FloatingSurface
|
||||
ref={contentRef}
|
||||
|
||||
@@ -164,6 +164,21 @@ index 629c155..e1909ff 100644
|
||||
return shouldIntercept
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native-gesture-handler/lib/commonjs/web/handlers/GestureHandler.js b/node_modules/react-native-gesture-handler/lib/commonjs/web/handlers/GestureHandler.js
|
||||
index 96c5227..cbf1a88 100644
|
||||
--- a/node_modules/react-native-gesture-handler/lib/commonjs/web/handlers/GestureHandler.js
|
||||
+++ b/node_modules/react-native-gesture-handler/lib/commonjs/web/handlers/GestureHandler.js
|
||||
@@ -58,7 +58,9 @@ class GestureHandler {
|
||||
manager.setOnPointerMoveOver(this.onPointerMoveOver.bind(this));
|
||||
manager.setOnPointerMoveOut(this.onPointerMoveOut.bind(this));
|
||||
manager.setOnWheel(this.onWheel.bind(this));
|
||||
- manager.registerListeners();
|
||||
+ if (this.enabled) {
|
||||
+ manager.registerListeners();
|
||||
+ }
|
||||
}
|
||||
|
||||
//
|
||||
diff --git a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||
index 76eb808..7c40526 100644
|
||||
--- a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||
@@ -184,6 +199,21 @@ index 76eb808..7c40526 100644
|
||||
}
|
||||
this.markAsOutOfBounds(adaptedEvent.pointerId);
|
||||
this.trackedPointers.delete(adaptedEvent.pointerId);
|
||||
diff --git a/node_modules/react-native-gesture-handler/lib/module/web/handlers/GestureHandler.js b/node_modules/react-native-gesture-handler/lib/module/web/handlers/GestureHandler.js
|
||||
index 0791c66..2f41c77 100644
|
||||
--- a/node_modules/react-native-gesture-handler/lib/module/web/handlers/GestureHandler.js
|
||||
+++ b/node_modules/react-native-gesture-handler/lib/module/web/handlers/GestureHandler.js
|
||||
@@ -52,7 +52,9 @@ export default class GestureHandler {
|
||||
manager.setOnPointerMoveOver(this.onPointerMoveOver.bind(this));
|
||||
manager.setOnPointerMoveOut(this.onPointerMoveOut.bind(this));
|
||||
manager.setOnWheel(this.onWheel.bind(this));
|
||||
- manager.registerListeners();
|
||||
+ if (this.enabled) {
|
||||
+ manager.registerListeners();
|
||||
+ }
|
||||
}
|
||||
|
||||
//
|
||||
diff --git a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||
index 6ce3b8f..8fe28bc 100644
|
||||
--- a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||
@@ -204,6 +234,21 @@ index 6ce3b8f..8fe28bc 100644
|
||||
}
|
||||
this.markAsOutOfBounds(adaptedEvent.pointerId);
|
||||
this.trackedPointers.delete(adaptedEvent.pointerId);
|
||||
diff --git a/node_modules/react-native-gesture-handler/src/web/handlers/GestureHandler.ts b/node_modules/react-native-gesture-handler/src/web/handlers/GestureHandler.ts
|
||||
index ddb79a3..4e1274c 100644
|
||||
--- a/node_modules/react-native-gesture-handler/src/web/handlers/GestureHandler.ts
|
||||
+++ b/node_modules/react-native-gesture-handler/src/web/handlers/GestureHandler.ts
|
||||
@@ -79,7 +79,9 @@ export default abstract class GestureHandler implements IGestureHandler {
|
||||
manager.setOnPointerMoveOut(this.onPointerMoveOut.bind(this));
|
||||
manager.setOnWheel(this.onWheel.bind(this));
|
||||
|
||||
- manager.registerListeners();
|
||||
+ if (this.enabled) {
|
||||
+ manager.registerListeners();
|
||||
+ }
|
||||
}
|
||||
|
||||
//
|
||||
diff --git a/node_modules/react-native-gesture-handler/src/web/tools/PointerEventManager.ts b/node_modules/react-native-gesture-handler/src/web/tools/PointerEventManager.ts
|
||||
index 18d3627..769b0fe 100644
|
||||
--- a/node_modules/react-native-gesture-handler/src/web/tools/PointerEventManager.ts
|
||||
|
||||
Reference in New Issue
Block a user