Improve contextual help and download filter grouping
Open contextual help from wrapper hover and focus, close it only after pointer or focus exits the shared trigger-popover region, and make clicks open-only. Remove the standalone package title and present the existing download filters as the first compact neutral sidebar group without changing filter behavior. Extend the visual driver and manifest with hover, leave, focus, blur, internal-focus, and real headless Chromium regression coverage.
This commit is contained in:
@@ -43,13 +43,23 @@ export function ContextInfoButton({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ui-context-info">
|
||||
<div
|
||||
className="ui-context-info"
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}}
|
||||
onFocus={() => onOpenChange(true)}
|
||||
onMouseEnter={() => onOpenChange(true)}
|
||||
onMouseLeave={() => onOpenChange(false)}
|
||||
>
|
||||
<button
|
||||
aria-controls={regionId}
|
||||
aria-expanded={open}
|
||||
aria-label="Informationen"
|
||||
className="ui-context-info-trigger"
|
||||
onClick={() => onOpenChange(!open)}
|
||||
onClick={() => onOpenChange(true)}
|
||||
title="Informationen"
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -96,9 +96,8 @@ const filters: Array<{ id: DownloadSidebarFilter; label: string }> = [
|
||||
|
||||
export function DownloadsSidebar({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement {
|
||||
return (
|
||||
<aside className="downloads-sidebar" data-visual-region="downloads-sidebar">
|
||||
<div className="downloads-mode-title">Pakete</div>
|
||||
<SlidingSelection activeKey={model.filter} aria-label="Downloadfilter" as="nav" axis="vertical">
|
||||
<aside className="downloads-sidebar" data-visual-region="downloads-sidebar">
|
||||
<SlidingSelection activeKey={model.filter} aria-label="Downloadfilter" as="nav" axis="vertical" className="downloads-filter-group">
|
||||
{filters.map((filter) => <button aria-current={model.filter === filter.id ? "page" : undefined} className={model.filter === filter.id ? "is-active" : ""} data-sliding-selection-active={model.filter === filter.id} data-sliding-selection-item="true" key={filter.id} onClick={() => actions.onFilterChange(filter.id)} type="button"><span>{filter.label}</span><b>{model.counts[filter.id]}</b></button>)}
|
||||
</SlidingSelection>
|
||||
<label className="downloads-provider-filter"><span>Service</span><select aria-label="Service filtern" disabled={model.providerOptions.length <= 1} onChange={(event) => actions.onProviderFilterChange(event.target.value)} value={model.providerFilter}><option value="all">Alle Services</option>{model.providerOptions.map((provider) => <option key={provider.id} value={provider.id}>{provider.label}</option>)}</select></label>
|
||||
|
||||
@@ -39,18 +39,11 @@
|
||||
padding: 14px 12px 6px;
|
||||
}
|
||||
|
||||
.downloads-mode-title {
|
||||
display: flex;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
.downloads-filter-group {
|
||||
padding: 4px;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 6px;
|
||||
color: #0a0f1a;
|
||||
background: #90cdf4;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
background: var(--ui-input);
|
||||
}
|
||||
|
||||
.downloads-sidebar nav button,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { DownloadItem, DownloadStatus, PackageEntry } from "../src/shared/types";
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
normalizeDownloadServiceLabel
|
||||
} from "../src/renderer/download-format";
|
||||
import { getRollingMetricDirection } from "../src/renderer/ui/RollingMetricValue";
|
||||
import { SlidingSelection } from "../src/renderer/ui/SlidingSelection";
|
||||
|
||||
const now = new Date(2026, 7, 10, 12, 0, 0, 0).getTime();
|
||||
|
||||
@@ -1044,14 +1045,21 @@ describe("downloads view", () => {
|
||||
expect(multiple).not.toMatch(/<select[^>]*aria-label="Service filtern"[^>]*disabled=""/);
|
||||
});
|
||||
|
||||
it("shows only the package mode while the file mode remains hidden", () => {
|
||||
const html = renderToStaticMarkup(<DownloadsSidebar actions={createActions()} model={withRuntime(createInput())} />);
|
||||
it("starts with the six download filters in a compact neutral group without a package title", () => {
|
||||
const sidebar = DownloadsSidebar({ actions: createActions(), model: withRuntime(createInput()) });
|
||||
const children = Children.toArray(sidebar.props.children).filter(isValidElement);
|
||||
const filterGroup = children[0] as ReactElement<{ className?: string }>;
|
||||
const html = renderToStaticMarkup(sidebar);
|
||||
const css = fs.readFileSync(path.join(process.cwd(), "src/renderer/views/downloads/downloads.css"), "utf8");
|
||||
|
||||
expect(html).toContain("Pakete");
|
||||
expect(filterGroup.type).toBe(SlidingSelection);
|
||||
expect(filterGroup.props.className).toBe("downloads-filter-group");
|
||||
expect(html).not.toContain("downloads-mode-title");
|
||||
expect(html).not.toContain(">Pakete<");
|
||||
expect(html).not.toContain(">Dateien<");
|
||||
expect(html).not.toContain("downloads-mode-switch");
|
||||
expect(css).toMatch(/\.downloads-mode-title\s*\{[^}]*justify-content:\s*center;[^}]*color:\s*#0a0f1a;[^}]*background:\s*#90cdf4;[^}]*text-align:\s*center;/s);
|
||||
expect(css).toMatch(/\.downloads-filter-group\s*\{[^}]*padding:\s*4px;[^}]*border:\s*1px solid var\(--ui-border\);[^}]*border-radius:\s*6px;[^}]*background:\s*var\(--ui-input\);/s);
|
||||
expect(css).not.toContain(".downloads-mode-title");
|
||||
});
|
||||
|
||||
it("renders the five dense markers exactly once and the empty marker only for a true empty queue", () => {
|
||||
|
||||
@@ -155,6 +155,10 @@ describe("reference capture manifest", () => {
|
||||
"context-statistics",
|
||||
"info-closed",
|
||||
"info-open",
|
||||
"info-closed-after-leave",
|
||||
"info-open-focus",
|
||||
"info-closed-after-blur",
|
||||
"info-open-focus-inside",
|
||||
"info-absent"
|
||||
]));
|
||||
const collector = manifest.find((entry: { name: string }) => entry.name === "collector-dense");
|
||||
@@ -172,6 +176,48 @@ describe("reference capture manifest", () => {
|
||||
role: "button",
|
||||
name: "Update verfügbar"
|
||||
});
|
||||
const infoOpen = manifest.find((entry: { name: string }) => entry.name === "info-open");
|
||||
expect(infoOpen.interactions).toEqual([
|
||||
{ type: "hover", role: "button", name: "Informationen" },
|
||||
{ type: "wait-visible", role: "region", name: "Informationen zu Downloads" }
|
||||
]);
|
||||
const infoClosedAfterLeave = manifest.find((entry: { name: string }) => entry.name === "info-closed-after-leave");
|
||||
expect(infoClosedAfterLeave.interactions).toEqual([
|
||||
{ type: "hover", role: "button", name: "Informationen" },
|
||||
{ type: "wait-visible", role: "region", name: "Informationen zu Downloads" },
|
||||
{ type: "leave", role: "button", name: "Informationen" },
|
||||
{ type: "wait-absent", role: "region", name: "Informationen zu Downloads" }
|
||||
]);
|
||||
const infoOpenFocus = manifest.find((entry: { name: string }) => entry.name === "info-open-focus");
|
||||
expect(infoOpenFocus.interactions).toEqual([
|
||||
{ type: "focus", role: "button", name: "Informationen" },
|
||||
{ type: "wait-visible", role: "region", name: "Informationen zu Downloads" }
|
||||
]);
|
||||
const infoClosedAfterBlur = manifest.find((entry: { name: string }) => entry.name === "info-closed-after-blur");
|
||||
expect(infoClosedAfterBlur.interactions).toEqual([
|
||||
{ type: "focus", role: "button", name: "Informationen" },
|
||||
{ type: "wait-visible", role: "region", name: "Informationen zu Downloads" },
|
||||
{ type: "blur", role: "button", name: "Informationen" },
|
||||
{ type: "wait-absent", role: "region", name: "Informationen zu Downloads" }
|
||||
]);
|
||||
const infoOpenFocusInside = manifest.find((entry: { name: string }) => entry.name === "info-open-focus-inside");
|
||||
expect(infoOpenFocusInside.interactions).toEqual([
|
||||
{ type: "focus", role: "button", name: "Informationen" },
|
||||
{ type: "wait-visible", role: "region", name: "Informationen zu Downloads" },
|
||||
{ type: "focus", role: "button", name: "Kontextaktion" },
|
||||
{ type: "wait-visible", role: "region", name: "Informationen zu Downloads" }
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts explicit leave, focus and blur interactions", () => {
|
||||
expect(validateVisualCaptureManifest([{
|
||||
...validCapture,
|
||||
interactions: [
|
||||
{ type: "leave", role: "button", name: "Informationen" },
|
||||
{ type: "focus", role: "button", name: "Informationen" },
|
||||
{ type: "blur", role: "button", name: "Informationen" }
|
||||
]
|
||||
}])).toEqual([]);
|
||||
});
|
||||
|
||||
it("defines representative responsive captures before the final matrix", () => {
|
||||
|
||||
@@ -189,13 +189,72 @@
|
||||
"viewport": { "width": 2560, "height": 1369 },
|
||||
"activeView": "downloads",
|
||||
"interactions": [
|
||||
{ "type": "click", "role": "button", "name": "Informationen" }
|
||||
{ "type": "hover", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-visible", "role": "region", "name": "Informationen zu Downloads" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "visible", "role": "region", "name": "Informationen zu Downloads" },
|
||||
{ "type": "nonempty", "role": "region", "name": "Informationen zu Downloads" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info-closed-after-leave",
|
||||
"scenario": "dense",
|
||||
"viewport": { "width": 2560, "height": 1369 },
|
||||
"activeView": "downloads",
|
||||
"interactions": [
|
||||
{ "type": "hover", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-visible", "role": "region", "name": "Informationen zu Downloads" },
|
||||
{ "type": "leave", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-absent", "role": "region", "name": "Informationen zu Downloads" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "absent", "role": "region", "name": "Informationen zu Downloads" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info-open-focus",
|
||||
"scenario": "dense",
|
||||
"viewport": { "width": 2560, "height": 1369 },
|
||||
"activeView": "downloads",
|
||||
"interactions": [
|
||||
{ "type": "focus", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-visible", "role": "region", "name": "Informationen zu Downloads" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "visible", "role": "region", "name": "Informationen zu Downloads" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info-closed-after-blur",
|
||||
"scenario": "dense",
|
||||
"viewport": { "width": 2560, "height": 1369 },
|
||||
"activeView": "downloads",
|
||||
"interactions": [
|
||||
{ "type": "focus", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-visible", "role": "region", "name": "Informationen zu Downloads" },
|
||||
{ "type": "blur", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-absent", "role": "region", "name": "Informationen zu Downloads" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "absent", "role": "region", "name": "Informationen zu Downloads" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info-open-focus-inside",
|
||||
"scenario": "dense",
|
||||
"viewport": { "width": 2560, "height": 1369 },
|
||||
"activeView": "downloads",
|
||||
"interactions": [
|
||||
{ "type": "focus", "role": "button", "name": "Informationen" },
|
||||
{ "type": "wait-visible", "role": "region", "name": "Informationen zu Downloads" },
|
||||
{ "type": "focus", "role": "button", "name": "Kontextaktion" },
|
||||
{ "type": "wait-visible", "role": "region", "name": "Informationen zu Downloads" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "visible", "role": "region", "name": "Informationen zu Downloads" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info-absent",
|
||||
"scenario": "empty",
|
||||
|
||||
@@ -36,6 +36,16 @@ type ToolbarMeasurement = {
|
||||
schedule: DOMRect;
|
||||
};
|
||||
|
||||
type ContextInfoMeasurement = {
|
||||
afterHover: boolean;
|
||||
afterPopover: boolean;
|
||||
afterLeave: boolean;
|
||||
afterFocus: boolean;
|
||||
afterBlur: boolean;
|
||||
afterFirstClick: boolean;
|
||||
afterSecondClick: boolean;
|
||||
};
|
||||
|
||||
class CdpClient {
|
||||
private nextId = 0;
|
||||
private readonly pending = new Map<number, {
|
||||
@@ -246,6 +256,24 @@ describe("download disclosure in the headless visual harness", () => {
|
||||
throw new Error("Visual harness did not reach its ready state");
|
||||
}
|
||||
|
||||
async function loadDriverCapture(name: string): Promise<void> {
|
||||
if (!client) throw new Error("Chrome DevTools client is missing");
|
||||
await client.send("Page.navigate", {
|
||||
url: `http://127.0.0.1:${visualPort}/driver-test.html?capture=${encodeURIComponent(name)}&run=${Date.now()}`
|
||||
});
|
||||
const deadline = Date.now() + 20_000;
|
||||
while (Date.now() < deadline) {
|
||||
const state = await client.evaluate<{ error: string; ready: boolean }>(`({
|
||||
error: document.getElementById('root')?.textContent || '',
|
||||
ready: document.documentElement.dataset.visualReady === 'true'
|
||||
})`);
|
||||
if (state.ready) return;
|
||||
if (state.error.startsWith("Visual-Harness-Fehler:")) throw new Error(state.error);
|
||||
await delay(50);
|
||||
}
|
||||
throw new Error(`Visual driver capture did not reach its ready state: ${name}`);
|
||||
}
|
||||
|
||||
async function measureDisclosure(action: "einklappen" | "ausklappen"): Promise<DisclosureSample[]> {
|
||||
if (!client) throw new Error("Chrome DevTools client is missing");
|
||||
return client.evaluate<DisclosureSample[]>(`(async () => {
|
||||
@@ -348,4 +376,79 @@ describe("download disclosure in the headless visual harness", () => {
|
||||
expect(measurement.toggle.right).toBeLessThanOrEqual(measurement.toolbar.right + 1);
|
||||
expect(measurement.toolbar.right - measurement.toggle.right).toBeLessThanOrEqual(12);
|
||||
}, 30_000);
|
||||
|
||||
it("opens context info on hover and focus, stays open over the popover and closes outside", async () => {
|
||||
await loadDenseDownloads(1120);
|
||||
if (!client) throw new Error("Chrome DevTools client is missing");
|
||||
const trigger = await client.evaluate<DOMRect>(`(() => {
|
||||
const element = document.querySelector('.ui-context-info-trigger');
|
||||
if (!(element instanceof HTMLButtonElement)) throw new Error('Context info trigger is missing');
|
||||
return element.getBoundingClientRect().toJSON();
|
||||
})()`);
|
||||
await client.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseMoved",
|
||||
x: trigger.left + trigger.width / 2,
|
||||
y: trigger.top + trigger.height / 2
|
||||
});
|
||||
await delay(50);
|
||||
const afterHover = await client.evaluate<boolean>("Boolean(document.querySelector('.ui-context-info-region'))");
|
||||
const popover = await client.evaluate<DOMRect>(`(() => {
|
||||
const element = document.querySelector('.ui-context-info-region');
|
||||
if (!(element instanceof HTMLElement)) throw new Error('Context info region is missing after hover');
|
||||
return element.getBoundingClientRect().toJSON();
|
||||
})()`);
|
||||
await client.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseMoved",
|
||||
x: popover.left + popover.width / 2,
|
||||
y: popover.top + popover.height / 2
|
||||
});
|
||||
await delay(50);
|
||||
const afterPopover = await client.evaluate<boolean>("Boolean(document.querySelector('.ui-context-info-region'))");
|
||||
await client.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 1110, y: 10 });
|
||||
await delay(50);
|
||||
const afterLeave = await client.evaluate<boolean>("Boolean(document.querySelector('.ui-context-info-region'))");
|
||||
const focusStates = await client.evaluate<Omit<ContextInfoMeasurement, "afterHover" | "afterPopover" | "afterLeave">>(`(async () => {
|
||||
const wait = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
const trigger = document.querySelector('.ui-context-info-trigger');
|
||||
const outside = document.querySelector('.md-shell-navigation-item');
|
||||
if (!(trigger instanceof HTMLButtonElement) || !(outside instanceof HTMLButtonElement)) {
|
||||
throw new Error('Context info focus targets are missing');
|
||||
}
|
||||
trigger.focus();
|
||||
await wait();
|
||||
const afterFocus = Boolean(document.querySelector('.ui-context-info-region'));
|
||||
outside.focus();
|
||||
await wait();
|
||||
const afterBlur = Boolean(document.querySelector('.ui-context-info-region'));
|
||||
trigger.click();
|
||||
await wait();
|
||||
const afterFirstClick = Boolean(document.querySelector('.ui-context-info-region'));
|
||||
trigger.click();
|
||||
await wait();
|
||||
const afterSecondClick = Boolean(document.querySelector('.ui-context-info-region'));
|
||||
return { afterFocus, afterBlur, afterFirstClick, afterSecondClick };
|
||||
})()`);
|
||||
const measurement: ContextInfoMeasurement = { afterHover, afterPopover, afterLeave, ...focusStates };
|
||||
|
||||
expect(measurement).toEqual({
|
||||
afterHover: true,
|
||||
afterPopover: true,
|
||||
afterLeave: false,
|
||||
afterFocus: true,
|
||||
afterBlur: false,
|
||||
afterFirstClick: true,
|
||||
afterSecondClick: true
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
it("executes the context info hover, leave, focus and blur manifest captures", async () => {
|
||||
for (const name of ["info-open", "info-closed-after-leave", "info-open-focus", "info-closed-after-blur", "info-open-focus-inside"]) {
|
||||
await loadDriverCapture(name);
|
||||
if (name === "info-open-focus-inside") {
|
||||
if (!client) throw new Error("Chrome DevTools client is missing");
|
||||
const transitions = await client.evaluate<string>("document.querySelector('[data-context-info-transitions]')?.getAttribute('data-context-info-transitions') || ''");
|
||||
expect(transitions.split(",")).not.toContain("closed");
|
||||
}
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ContextInfoButton } from "../../src/renderer/ui/ContextInfoButton";
|
||||
import {
|
||||
loadVisualCapture,
|
||||
prepareVisualCapture,
|
||||
@@ -23,6 +24,7 @@ function DriverTestApp({ updateAvailable }: { updateAvailable: boolean }) {
|
||||
const [updateDialogOpen, setUpdateDialogOpen] = useState(updateAvailable);
|
||||
const [updateTooltipOpen, setUpdateTooltipOpen] = useState(false);
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const [infoTransitions, setInfoTransitions] = useState<boolean[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -48,8 +50,16 @@ function DriverTestApp({ updateAvailable }: { updateAvailable: boolean }) {
|
||||
<div role="row">Dokumentation Staffel 1</div>
|
||||
</div>
|
||||
<div data-visual-region="downloads-pagination">Seite 1 von 1</div>
|
||||
<button type="button" onClick={() => setInfoOpen((open) => !open)}>Informationen</button>
|
||||
{infoOpen && <section aria-label="Informationen zu Downloads">Drei Downloads sind sichtbar.</section>}
|
||||
<ContextInfoButton
|
||||
content={<><span>Drei Downloads sind sichtbar.</span><button type="button">Kontextaktion</button></>}
|
||||
contextName="Downloads"
|
||||
onOpenChange={(open) => {
|
||||
setInfoTransitions((transitions) => [...transitions, open]);
|
||||
setInfoOpen(open);
|
||||
}}
|
||||
open={infoOpen}
|
||||
/>
|
||||
<output data-context-info-transitions={infoTransitions.map((open) => open ? "open" : "closed").join(",")} />
|
||||
</>
|
||||
)}
|
||||
{activeView === "collector" && (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { VISUAL_SCENARIOS, type VisualScenario } from "./fixtures";
|
||||
export type MainViewId = "downloads" | "collector" | "settings" | "history" | "statistics";
|
||||
|
||||
export interface VisualInteraction {
|
||||
type: "click" | "hover" | "fill" | "press" | "wait-visible" | "wait-absent";
|
||||
type: "click" | "hover" | "leave" | "focus" | "blur" | "fill" | "press" | "wait-visible" | "wait-absent";
|
||||
role?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
@@ -33,7 +33,7 @@ export interface VisualCapture {
|
||||
}
|
||||
|
||||
const MAIN_VIEWS = ["downloads", "collector", "settings", "history", "statistics"] as const;
|
||||
const INTERACTION_TYPES = ["click", "hover", "fill", "press", "wait-visible", "wait-absent"] as const;
|
||||
const INTERACTION_TYPES = ["click", "hover", "leave", "focus", "blur", "fill", "press", "wait-visible", "wait-absent"] as const;
|
||||
const ASSERTION_TYPES = ["active-view", "visible", "absent", "nonempty", "minimum-row-count", "layer-above"] as const;
|
||||
const REGION_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
const VIEW_NAMES: Record<MainViewId, string> = {
|
||||
@@ -366,6 +366,19 @@ function hoverElement(targetDocument: Document, element: Element): void {
|
||||
dispatch(element, createEvent(targetDocument, "mouseenter", "mouse"));
|
||||
}
|
||||
|
||||
function leaveElement(targetDocument: Document, element: Element): void {
|
||||
dispatch(element, createEvent(targetDocument, "pointerout", "pointer"));
|
||||
dispatch(element, createEvent(targetDocument, "pointerleave", "pointer"));
|
||||
dispatch(element, createEvent(targetDocument, "mouseout", "mouse"));
|
||||
dispatch(element, createEvent(targetDocument, "mouseleave", "mouse"));
|
||||
}
|
||||
|
||||
function blurElement(element: Element): void {
|
||||
if ("blur" in element && typeof element.blur === "function") {
|
||||
element.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function setNativeValue(targetDocument: Document, element: Element, value: string): void {
|
||||
const view = targetDocument.defaultView;
|
||||
if (!view) {
|
||||
@@ -447,6 +460,12 @@ async function runInteraction(interaction: VisualInteraction, targetDocument: Do
|
||||
clickElement(targetDocument, element);
|
||||
} else if (interaction.type === "hover") {
|
||||
hoverElement(targetDocument, element);
|
||||
} else if (interaction.type === "leave") {
|
||||
leaveElement(targetDocument, element);
|
||||
} else if (interaction.type === "focus") {
|
||||
focusElement(element);
|
||||
} else if (interaction.type === "blur") {
|
||||
blurElement(element);
|
||||
} else if (interaction.type === "fill") {
|
||||
setNativeValue(targetDocument, element, interaction.value as string);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user