Files
tibi-svelte-starter/video-tours/helpers.ts

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 }
)
}