142 lines
5.5 KiB
TypeScript
142 lines
5.5 KiB
TypeScript
import type { Page } from "@playwright/test"
|
|
|
|
/**
|
|
* Injects a visible CSS cursor overlay that follows mouse movements.
|
|
* Features a glowing blue dot with an expanding ripple ring on click.
|
|
*
|
|
* Must be called BEFORE the first page.goto() (uses addInitScript).
|
|
*/
|
|
export async function injectVisibleCursor(page: Page, options?: { color?: string; size?: number }): Promise<void> {
|
|
const color = options?.color ?? "59, 130, 246" // blue-500
|
|
const size = options?.size ?? 24
|
|
|
|
await page.addInitScript(
|
|
({ color, size }) => {
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Avoid duplicates on soft-navigations
|
|
if (document.getElementById("pw-cursor")) return
|
|
|
|
// ── Cursor dot ──
|
|
const cursor = document.createElement("div")
|
|
cursor.id = "pw-cursor"
|
|
Object.assign(cursor.style, {
|
|
width: `${size}px`,
|
|
height: `${size}px`,
|
|
borderRadius: "50%",
|
|
background: `radial-gradient(circle, rgba(${color}, 0.9) 0%, rgba(${color}, 0.4) 60%, transparent 100%)`,
|
|
boxShadow: `0 0 ${size / 2}px rgba(${color}, 0.5), 0 0 ${size}px rgba(${color}, 0.2)`,
|
|
position: "fixed",
|
|
zIndex: "999999",
|
|
pointerEvents: "none",
|
|
transition: "transform 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94), box-shadow 0.12s ease",
|
|
transform: "translate(-50%, -50%)",
|
|
top: "-100px",
|
|
left: "-100px",
|
|
})
|
|
document.body.appendChild(cursor)
|
|
|
|
// ── Click ripple animation ──
|
|
const style = document.createElement("style")
|
|
style.textContent = `
|
|
@keyframes pw-ripple {
|
|
0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0.8; }
|
|
100% { transform: translate(-50%, -50%) scale(2.5); opacity: 0; }
|
|
}
|
|
.pw-ripple {
|
|
position: fixed;
|
|
width: ${size * 2.5}px;
|
|
height: ${size * 2.5}px;
|
|
border-radius: 50%;
|
|
border: 2.5px solid rgba(${color}, 0.7);
|
|
pointer-events: none;
|
|
z-index: 999998;
|
|
animation: pw-ripple 0.5s cubic-bezier(0, 0, 0.2, 1) forwards;
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|
|
|
|
let cx = -100,
|
|
cy = -100
|
|
|
|
document.addEventListener("mousemove", (e) => {
|
|
cx = e.clientX
|
|
cy = e.clientY
|
|
cursor.style.left = cx + "px"
|
|
cursor.style.top = cy + "px"
|
|
})
|
|
|
|
document.addEventListener("mousedown", () => {
|
|
// Shrink cursor + intensify glow
|
|
cursor.style.transform = "translate(-50%, -50%) scale(0.5)"
|
|
cursor.style.boxShadow = `0 0 ${size}px rgba(${color}, 0.8), 0 0 ${size * 1.5}px rgba(${color}, 0.4)`
|
|
|
|
// Spawn expanding ripple ring
|
|
const ripple = document.createElement("div")
|
|
ripple.className = "pw-ripple"
|
|
ripple.style.left = cx + "px"
|
|
ripple.style.top = cy + "px"
|
|
document.body.appendChild(ripple)
|
|
ripple.addEventListener("animationend", () => ripple.remove())
|
|
})
|
|
|
|
document.addEventListener("mouseup", () => {
|
|
cursor.style.transform = "translate(-50%, -50%) scale(1)"
|
|
cursor.style.boxShadow = `0 0 ${size / 2}px rgba(${color}, 0.5), 0 0 ${size}px rgba(${color}, 0.2)`
|
|
})
|
|
})
|
|
},
|
|
{ color, size }
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Moves the visible cursor smoothly to a target element before clicking.
|
|
* This ensures mousemove events fire so the injected cursor follows along.
|
|
*/
|
|
export async function moveThenClick(page: Page, locator: ReturnType<Page["locator"]>): Promise<void> {
|
|
const box = await locator.boundingBox()
|
|
if (box) {
|
|
const x = box.x + box.width / 2
|
|
const y = box.y + box.height / 2
|
|
// Smooth move in steps
|
|
await page.mouse.move(x, y, { steps: 15 })
|
|
await page.waitForTimeout(150)
|
|
}
|
|
await locator.click()
|
|
}
|
|
|
|
/**
|
|
* Moves the visible cursor to a target element and types text character by character.
|
|
* Simulates realistic typing for demo purposes.
|
|
*/
|
|
export async function moveThenType(
|
|
page: Page,
|
|
locator: ReturnType<Page["locator"]>,
|
|
text: string,
|
|
options?: { delay?: number }
|
|
): Promise<void> {
|
|
const box = await locator.boundingBox()
|
|
if (box) {
|
|
const x = box.x + box.width / 2
|
|
const y = box.y + box.height / 2
|
|
await page.mouse.move(x, y, { steps: 15 })
|
|
await page.waitForTimeout(150)
|
|
}
|
|
await locator.click()
|
|
await page.waitForTimeout(200)
|
|
await page.keyboard.type(text, { delay: options?.delay ?? 80 })
|
|
}
|
|
|
|
/**
|
|
* Scrolls the page smoothly to a given position.
|
|
*/
|
|
export async function smoothScroll(page: Page, y: number, duration = 600): Promise<void> {
|
|
await page.evaluate(
|
|
({ y, duration }) => {
|
|
window.scrollTo({ top: y, behavior: "smooth" })
|
|
return new Promise((r) => setTimeout(r, duration))
|
|
},
|
|
{ y, duration }
|
|
)
|
|
}
|