mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 06:51:24 +02:00
fix: popup exit and capture
This commit is contained in:
1 parent
d93afc2bd5
commit
f493c9ef7c
5 files changed
+78
-38
No files matched your search
+52
-17
@@ -611,6 +611,21 @@ test("profile table filters, range selection, view settings, and keyboard edits
|
||||
[],
|
||||
);
|
||||
const row = (index) => `tr[data-table-row="${profiles[index].id}"]`;
|
||||
// A new profile is scrolled into view, so the last of the 32 leaves the
|
||||
// table at its end. Scroll back to the first row.
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(
|
||||
`
|
||||
const root = document.querySelector('[data-table-id="profiles"]');
|
||||
if (root?.querySelector('table')?.getAttribute('aria-rowcount') !== "33") return false;
|
||||
root.querySelector('.scroll-fade').scrollTop = 0;
|
||||
return Boolean(root.querySelector(arguments[0]));
|
||||
`,
|
||||
[row(0)],
|
||||
),
|
||||
{ description: "the first profile row in view" },
|
||||
);
|
||||
await app.clickSelector(`${row(0)} [role="checkbox"]`);
|
||||
await tableShiftClick(app, `${row(3)} [role="checkbox"]`);
|
||||
await app.waitFor(
|
||||
@@ -1126,11 +1141,28 @@ test("management tables share search, filters, selection, and saved views", asyn
|
||||
async () => (await tableRows(app, "proxies")).length === 1,
|
||||
{ description: "proxy search" },
|
||||
);
|
||||
assert.equal(
|
||||
await app.execute(
|
||||
`return Boolean(document.querySelector('[data-slot="table-action-bar"]'));`,
|
||||
),
|
||||
false,
|
||||
// A search keeps the selection, and the bar counts the rows it hides.
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(
|
||||
`return document.querySelector('[data-slot="table-action-bar"] [data-slot="table-selection-count"] .sr-only')?.textContent === arguments[0];`,
|
||||
[
|
||||
en.dataTableActionBar.selectedHidden
|
||||
.replace("{{selected}}", "3")
|
||||
.replace("{{hidden}}", "2"),
|
||||
],
|
||||
),
|
||||
{ description: "selection kept through the search" },
|
||||
);
|
||||
await app.clickSelector(
|
||||
`[data-slot="table-action-bar"] [aria-label="${en.dataTableActionBar.clearSelection}"]`,
|
||||
);
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(
|
||||
`return !document.querySelector('[data-slot="table-action-bar"]');`,
|
||||
),
|
||||
{ timeoutMs: 2000, description: "selection cleared" },
|
||||
);
|
||||
await app.fillSelector(
|
||||
`[data-table-id="proxies"] input[aria-label="${en.tables.search}"]`,
|
||||
@@ -1847,6 +1879,7 @@ test("MCP clients on this computer come first, and toolsets go to the connected
|
||||
assert.deepEqual((await calls("set_mcp_toolsets"))[0].toolsets, [
|
||||
"organize",
|
||||
"network",
|
||||
"traffic",
|
||||
"fingerprint",
|
||||
"extensions",
|
||||
"sync",
|
||||
@@ -5602,7 +5635,7 @@ test("tips open from the rail, walk the catalog, and deep-link into the feature"
|
||||
);
|
||||
|
||||
// The action lands inside the feature: the DNS tip opens settings on
|
||||
// its DNS section, and the dialog is gone by then.
|
||||
// its DNS section, and the dialog leaves after its short exit.
|
||||
await app.clickSelector(
|
||||
'[data-slot="tips-list-item"][data-tip-id="dnsBlocklist"]',
|
||||
);
|
||||
@@ -5614,12 +5647,12 @@ test("tips open from the rail, walk the catalog, and deep-link into the feature"
|
||||
),
|
||||
{ description: "the DNS settings section focused" },
|
||||
);
|
||||
assert.equal(
|
||||
await app.execute(
|
||||
`return Boolean(document.querySelector(arguments[0]));`,
|
||||
[TIPS_DIALOG],
|
||||
),
|
||||
false,
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(`return !document.querySelector(arguments[0]);`, [
|
||||
TIPS_DIALOG,
|
||||
]),
|
||||
{ timeoutMs: 2000, description: "the tips dialog to leave" },
|
||||
);
|
||||
|
||||
// The chord opens the catalog too, and the switch turns the automatic
|
||||
@@ -6041,11 +6074,13 @@ test("a freshly paid account is welcomed once and walked to its plan tips", asyn
|
||||
),
|
||||
{ description: "the Cookie Bot tip" },
|
||||
);
|
||||
assert.equal(
|
||||
await app.execute(
|
||||
`return Boolean(document.querySelector('[data-slot="paid-welcome"]'));`,
|
||||
),
|
||||
false,
|
||||
// The welcome leaves after its short exit, under the catalog.
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(
|
||||
`return !document.querySelector('[data-slot="paid-welcome"]');`,
|
||||
),
|
||||
{ timeoutMs: 2000, description: "the paid welcome to leave" },
|
||||
);
|
||||
const listed = await app.execute(
|
||||
`return [...document.querySelectorAll('[data-slot="tips-list-item"]')].map((node) => node.dataset.tipId);`,
|
||||
|
||||
@@ -797,6 +797,8 @@ async fn pump(
|
||||
ready: tokio::sync::oneshot::Sender<Result<(), String>>,
|
||||
) {
|
||||
let mut ready = Some(ready);
|
||||
// A tab opened while the capture runs waits until its Network domain is
|
||||
// on, so its first request is not missed; `attached` lets it go.
|
||||
let attach = connection
|
||||
.send_to(
|
||||
None,
|
||||
@@ -804,7 +806,7 @@ async fn pump(
|
||||
"Target.setAutoAttach",
|
||||
json!({
|
||||
"autoAttach": true,
|
||||
"waitForDebuggerOnStart": false,
|
||||
"waitForDebuggerOnStart": true,
|
||||
"flatten": true,
|
||||
"filter": [{ "type": "page" }],
|
||||
}),
|
||||
|
||||
@@ -16,9 +16,13 @@ function assignRef<T>(ref: React.Ref<T> | undefined, value: T | null) {
|
||||
* Motion for a Radix popup's content element (`.popup-motion` in
|
||||
* globals.css): the `data-motion` flag, and a ref that ends a stalled exit.
|
||||
*
|
||||
* Radix keeps a closed popup mounted until its exit animation sends
|
||||
* `animationend`. A paused animation clock (an occluded webview) never sends
|
||||
* it, so a timer sends it instead, and the popup always leaves.
|
||||
* Radix keeps a closed popup mounted until an `animationend` names the
|
||||
* animation it runs now. A paused animation clock (an occluded webview) never
|
||||
* sends it, and a popup whose motion turns off mid-exit (a key press makes
|
||||
* the user a keyboard user) has no animation left to end. A popup left
|
||||
* mounted comes back with its old state (a search typed into it) when it
|
||||
* opens again. So the ref sends that event itself: at once when no exit can
|
||||
* play, else after the exit's length.
|
||||
*/
|
||||
export function usePopupMotion<T extends HTMLElement>(ref?: React.Ref<T>) {
|
||||
const motionAllowed = useMotionAllowed();
|
||||
@@ -27,26 +31,23 @@ export function usePopupMotion<T extends HTMLElement>(ref?: React.Ref<T>) {
|
||||
assignRef(ref, node);
|
||||
if (!node) return;
|
||||
let timer = 0;
|
||||
const endExit = () => {
|
||||
if (!node.isConnected || node.dataset.state !== "closed") return;
|
||||
node.dispatchEvent(
|
||||
new AnimationEvent("animationend", {
|
||||
animationName: getComputedStyle(node).animationName,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const observer = new MutationObserver(() => {
|
||||
window.clearTimeout(timer);
|
||||
if (node.dataset.state !== "closed") return;
|
||||
timer = window.setTimeout(() => {
|
||||
if (
|
||||
node.isConnected &&
|
||||
node.dataset.state === "closed" &&
|
||||
getComputedStyle(node).animationName === "popup-exit"
|
||||
) {
|
||||
node.dispatchEvent(
|
||||
new AnimationEvent("animationend", {
|
||||
animationName: "popup-exit",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, POPUP_EXIT_FALLBACK_MS);
|
||||
if (document.hidden || node.dataset.motion !== "on") endExit();
|
||||
else timer = window.setTimeout(endExit, POPUP_EXIT_FALLBACK_MS);
|
||||
});
|
||||
observer.observe(node, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-state"],
|
||||
attributeFilter: ["data-state", "data-motion"],
|
||||
});
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
|
||||
@@ -65,7 +65,9 @@ function SelectContent({
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
// Grows in; never animates out. A Select unmounts the moment it closes.
|
||||
// Grows in; never animates out. `animate-none` on close cancels the
|
||||
// `popup-exit` of `.popup-motion`, so Radix unmounts the content at once
|
||||
// (`hidden` cannot: the popper sets an inline `display: flex`).
|
||||
const motionAllowed = useMotionAllowed();
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
@@ -73,7 +75,7 @@ function SelectContent({
|
||||
data-slot="select-content"
|
||||
data-motion={motionAllowed ? "on" : undefined}
|
||||
className={cn(
|
||||
"popup-motion relative z-50000 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-sm data-[state=closed]:pointer-events-none data-[state=closed]:hidden",
|
||||
"popup-motion relative z-50000 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-sm data-[state=closed]:pointer-events-none data-[state=closed]:hidden data-[state=closed]:animate-none",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
Menus, selects and popovers only scale: they are fully opaque from the first
|
||||
frame, so a paused animation clock still shows them readable. A tooltip
|
||||
also fades from half opacity. The exit holds a closed popup for 100ms;
|
||||
`usePopupExitFallback` ends it on a timer if the clock is paused. */
|
||||
`usePopupMotion` ends it if the clock is paused. A Select has no exit. */
|
||||
@keyframes popup-enter {
|
||||
from {
|
||||
scale: 0.97;
|
||||
|
||||
Reference in new issue
Block a user