mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-31 19:06:14 +00:00
fix(studio): Renders dropdown options take a mouse click again (#4890)
* fix(studio): Renders dropdown options take a mouse click again * test(studio): name a Select whose list never opens and keep both Studio layout checks running
This commit is contained in:
@@ -958,7 +958,7 @@ jobs:
|
||||
node packages/studio/tests/e2e/perf-ratchet.mjs check \
|
||||
packages/studio/tests/e2e/perf-ceilings.json studio-open /tmp/studio-open-counts.json \
|
||||
/tmp/base-perf-ceilings.json
|
||||
- name: Measure the fade handle tabs on a narrow clip
|
||||
- name: Measure the fade handle tabs and click every Renders select
|
||||
if: success() || failure()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -973,8 +973,13 @@ jobs:
|
||||
echo "FAIL: studio dev server did not start on port 5316"
|
||||
exit 1
|
||||
fi
|
||||
# Both checks run even when the first fails.
|
||||
rc=0
|
||||
STUDIO_URL="http://localhost:5316/#project/fade-handles" \
|
||||
node packages/studio/tests/e2e/fade-handles.mjs
|
||||
node packages/studio/tests/e2e/fade-handles.mjs || rc=1
|
||||
STUDIO_URL="http://localhost:5316/#project/fade-handles?tab=renders" \
|
||||
node packages/studio/tests/e2e/render-selects.mjs || rc=1
|
||||
exit "$rc"
|
||||
- name: Measure the caret of a text edited in place
|
||||
if: success() || failure()
|
||||
run: |
|
||||
|
||||
@@ -52,8 +52,8 @@ export const popupSurface = cn(
|
||||
"data-[preview-state=open]:opacity-100 data-[preview-state=open]:scale-100",
|
||||
);
|
||||
|
||||
/** The popup's own layer: menus sit above panel chrome and below a modal. */
|
||||
const POPUP_LAYER = "z-200";
|
||||
/** Every floating list, menu, popover and tooltip sits on this layer, above panel chrome and modals. */
|
||||
export const POPUP_LAYER = "z-200";
|
||||
|
||||
const menuPopup = cn(popupSurface, "min-w-36 p-1 shadow-menu");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Popover as BasePopover } from "@base-ui/react/popover";
|
||||
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
||||
import { cn } from "./cn";
|
||||
import { popupSurface, type PopupPreviewState } from "./Menu";
|
||||
import { POPUP_LAYER, popupSurface, type PopupPreviewState } from "./Menu";
|
||||
|
||||
type PortalContainer = ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
|
||||
|
||||
@@ -70,7 +70,7 @@ export function Popover({
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={VIEWPORT_MARGIN}
|
||||
arrowPadding={ARROW_CORNER_CLEARANCE}
|
||||
className="z-200"
|
||||
className={POPUP_LAYER}
|
||||
>
|
||||
<BasePopover.Popup
|
||||
aria-label={ariaLabel}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Select as BaseSelect } from "@base-ui/react/select";
|
||||
import { cn } from "./cn";
|
||||
import { fieldBase } from "./Input";
|
||||
import { floatingMotion } from "./Menu";
|
||||
import { floatingMotion, POPUP_LAYER } from "./Menu";
|
||||
import type { PreviewState } from "./Button";
|
||||
|
||||
export interface SelectOption {
|
||||
@@ -77,7 +77,7 @@ export function Select({
|
||||
</BaseSelect.Trigger>
|
||||
|
||||
<BaseSelect.Portal>
|
||||
<BaseSelect.Positioner sideOffset={4} alignItemWithTrigger={false}>
|
||||
<BaseSelect.Positioner sideOffset={4} alignItemWithTrigger={false} className={POPUP_LAYER}>
|
||||
<BaseSelect.Popup
|
||||
className={cn(
|
||||
"min-w-[var(--anchor-width)] rounded-md border border-border bg-surface py-1 shadow-menu",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
|
||||
import { cloneElement, useId, useRef, useState, type ReactElement } from "react";
|
||||
import { cn } from "./cn";
|
||||
import { floatingMotion } from "./Menu";
|
||||
import { floatingMotion, POPUP_LAYER } from "./Menu";
|
||||
|
||||
interface TooltipProps {
|
||||
label: string;
|
||||
@@ -39,7 +39,7 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
||||
side={side}
|
||||
sideOffset={SIDE_OFFSET}
|
||||
collisionPadding={VIEWPORT_MARGIN}
|
||||
className="z-200"
|
||||
className={POPUP_LAYER}
|
||||
>
|
||||
<BaseTooltip.Popup
|
||||
id={tooltipId}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
// Each Renders Select must take a real mouse click on its last option: the open list has to sit above
|
||||
// the sibling fields and the Export button. Hit-testing only happens in real layout, so this runs in Chrome.
|
||||
import { launchStudioChrome } from "./chrome-executable.mjs";
|
||||
|
||||
const STUDIO_URL = process.env.STUDIO_URL;
|
||||
const LABELS = ["Format", "Resolution", "Frame rate", "Quality"];
|
||||
const SHOT_DIR = process.env.RENDER_SELECTS_SHOT_DIR;
|
||||
|
||||
if (!STUDIO_URL) {
|
||||
console.error("STUDIO_URL is required and must point at a Studio project with ?tab=renders");
|
||||
process.exit(2);
|
||||
}
|
||||
const trigger = (label) => `[role="combobox"][aria-label="${label}"]`;
|
||||
const { browser } = await launchStudioChrome();
|
||||
const failures = [];
|
||||
const evidence = [];
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 900 });
|
||||
await page.goto(STUDIO_URL, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(trigger("Format"), { visible: true, timeout: 60_000 });
|
||||
// Format's last option is WebM, which keeps Quality on screen for the last pass.
|
||||
for (const label of LABELS) {
|
||||
if (!(await page.$(trigger(label)))) {
|
||||
failures.push(`${label}: no Select on the Renders panel`);
|
||||
continue;
|
||||
}
|
||||
// A click before Studio settles is dropped, so press until the list says it is open.
|
||||
const open = `${trigger(label)}[aria-expanded="true"]`;
|
||||
for (let tries = 0; tries < 10 && !(await page.$(open)); tries += 1) {
|
||||
await page.click(trigger(label));
|
||||
await page.waitForSelector(open, { timeout: 1_000 }).catch(() => null);
|
||||
}
|
||||
if (!(await page.$(open))) {
|
||||
failures.push(`${label}: the list never opened`);
|
||||
continue;
|
||||
}
|
||||
await page.waitForFunction(() =>
|
||||
[...document.querySelectorAll('[role="option"]')].some((el) => el.checkVisibility()),
|
||||
);
|
||||
// Let the open motion finish so the hit test sees the resting layout.
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
// A closing list can linger in the DOM, so only options that are drawn count.
|
||||
const { text: want, ...point } = await page.evaluate(() => {
|
||||
const drawn = [...document.querySelectorAll('[role="option"]')].filter(
|
||||
(el) => el.checkVisibility() && !el.hasAttribute("data-disabled"),
|
||||
);
|
||||
const el = drawn.at(-1);
|
||||
const r = el.getBoundingClientRect();
|
||||
return { text: el.textContent.trim(), x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
||||
});
|
||||
const hit = await page.evaluate(({ x, y }) => {
|
||||
const top = document.elementFromPoint(x, y);
|
||||
return {
|
||||
onOption: Boolean(top?.closest('[role="option"]')),
|
||||
top: top?.outerHTML.slice(0, 120),
|
||||
};
|
||||
}, point);
|
||||
if (SHOT_DIR) await page.screenshot({ path: `${SHOT_DIR}/${label.replace(" ", "-")}.png` });
|
||||
if (!hit.onOption) {
|
||||
// Clicking through would press whatever covers the option, Export included.
|
||||
failures.push(`${label}: "${want}" is covered by ${hit.top}`);
|
||||
await page.keyboard.press("Escape");
|
||||
continue;
|
||||
}
|
||||
await page.mouse.click(point.x, point.y);
|
||||
await page.waitForSelector(`${trigger(label)}[aria-expanded="false"]`, { timeout: 5_000 });
|
||||
const got = await page.$eval(trigger(label), (el) => el.textContent.trim());
|
||||
evidence.push({ label, want, got });
|
||||
if (got !== want) failures.push(`${label}: clicked "${want}", the field shows "${got}"`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
console.log(JSON.stringify({ evidence, failures }, null, 2));
|
||||
if (failures.length > 0) process.exit(1);
|
||||
Reference in New Issue
Block a user