108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
import type { Locator, Page } from "playwright";
|
|
import { humanDelay } from "@/lib/utils";
|
|
|
|
function normalizeDraftText(text: string): string {
|
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
}
|
|
|
|
function isTextInserted(actual: string, expected: string): boolean {
|
|
const a = normalizeDraftText(actual).trim();
|
|
const e = normalizeDraftText(expected).trim();
|
|
if (!e) return true;
|
|
if (a === e) return true;
|
|
if (a.length >= Math.min(e.length, 20)) return true;
|
|
return a.includes(e.slice(0, Math.min(24, e.length)));
|
|
}
|
|
|
|
async function clearEditor(page: Page, editor: Locator) {
|
|
await editor.scrollIntoViewIfNeeded().catch(() => undefined);
|
|
await editor.click();
|
|
await humanDelay(120, 220);
|
|
await page.keyboard.press("Meta+A");
|
|
await page.keyboard.press("Backspace");
|
|
await humanDelay(100, 180);
|
|
}
|
|
|
|
async function pasteTextIntoEditor(page: Page, editor: Locator, text: string): Promise<boolean> {
|
|
try {
|
|
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
} catch {
|
|
// ignore permission errors on some hosts
|
|
}
|
|
|
|
try {
|
|
await page.evaluate(async (value) => {
|
|
await navigator.clipboard.writeText(value);
|
|
}, text);
|
|
await editor.click();
|
|
await page.keyboard.press("Meta+v");
|
|
await humanDelay(250, 450);
|
|
return true;
|
|
} catch {
|
|
return page.evaluate((value) => {
|
|
const target =
|
|
document.querySelector('[data-threadtools-editor="true"]') ??
|
|
document.querySelector('[role="dialog"] [contenteditable="true"]') ??
|
|
document.querySelector('[contenteditable="true"][role="textbox"]') ??
|
|
document.querySelector('[contenteditable="true"]');
|
|
if (!(target instanceof HTMLElement)) return false;
|
|
|
|
target.focus();
|
|
const dataTransfer = new DataTransfer();
|
|
dataTransfer.setData("text/plain", value);
|
|
const pasted = target.dispatchEvent(
|
|
new ClipboardEvent("paste", {
|
|
clipboardData: dataTransfer,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
})
|
|
);
|
|
return pasted !== false;
|
|
}, text);
|
|
}
|
|
}
|
|
|
|
async function typeWithLineBreaks(page: Page, text: string) {
|
|
const normalized = normalizeDraftText(text);
|
|
const paragraphs = normalized.split(/\n\n+/);
|
|
|
|
for (let p = 0; p < paragraphs.length; p++) {
|
|
if (p > 0) {
|
|
await page.keyboard.press("Enter");
|
|
await humanDelay(40, 90);
|
|
}
|
|
|
|
const lines = paragraphs[p].split("\n");
|
|
for (let l = 0; l < lines.length; l++) {
|
|
if (l > 0) {
|
|
await page.keyboard.press("Shift+Enter");
|
|
await humanDelay(40, 90);
|
|
}
|
|
if (lines[l]) {
|
|
await page.keyboard.insertText(lines[l]);
|
|
await humanDelay(30, 70);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function typeIntoEditor(page: Page, editor: Locator, text: string) {
|
|
const normalized = normalizeDraftText(text);
|
|
await clearEditor(page, editor);
|
|
|
|
const pasted = await pasteTextIntoEditor(page, editor, normalized);
|
|
if (pasted) {
|
|
const value = await editor.innerText().catch(() => "");
|
|
if (isTextInserted(value, normalized)) return;
|
|
}
|
|
|
|
await clearEditor(page, editor);
|
|
await typeWithLineBreaks(page, normalized);
|
|
|
|
const value = await editor.innerText().catch(() => "");
|
|
if (!isTextInserted(value, normalized)) {
|
|
await clearEditor(page, editor);
|
|
await editor.click();
|
|
await page.keyboard.insertText(normalized);
|
|
}
|
|
} |