- Copy only (right-click -> Copy): write to clipboard without auto-pasting or hiding the window - Color swatches: hex/rgb/hsl entries show a preview chip - SQLite WAL + synchronous=NORMAL so the UI can read while the clipboard poller writes - Memoize list rows (React.memo) so selection/focus changes don't re-render every row's preview - Add docs/GUIDE.md (living install + usage doc), docs/SHARING.md, docs/TESTING.md, docs/LAUNCH-AT-LOGIN.md, docs/PRODUCT.md, CHANGELOG.md, ROADMAP.md - Add homelab Gitea Actions CI (.gitea/workflows/ci.yml) + gitleaks allowlist - Bump version to 0.2.0
103 lines
2.7 KiB
JavaScript
103 lines
2.7 KiB
JavaScript
/**
|
|
* Headed visual walkthrough — opens Chromium (headless: false) so you can watch.
|
|
*
|
|
* npm run test:visual
|
|
*/
|
|
import { chromium } from "playwright";
|
|
import { spawn } from "node:child_process";
|
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
|
|
const PORT = 1420;
|
|
const BASE = `http://127.0.0.1:${PORT}/visual.html`;
|
|
|
|
async function portOpen(port) {
|
|
try {
|
|
const res = await fetch(`http://127.0.0.1:${port}/visual.html`);
|
|
return res.status < 500;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function ensureVite() {
|
|
if (await portOpen(PORT)) return null;
|
|
const child = spawn("npm", ["run", "dev", "--", "--host", "127.0.0.1"], {
|
|
cwd: process.cwd(),
|
|
stdio: "ignore",
|
|
detached: true,
|
|
});
|
|
for (let i = 0; i < 40; i++) {
|
|
await sleep(250);
|
|
if (await portOpen(PORT)) return child;
|
|
}
|
|
throw new Error("Vite did not start on :1420");
|
|
}
|
|
|
|
async function main() {
|
|
const vite = await ensureVite();
|
|
console.log("Opening headed Chromium — watch the window…\n");
|
|
|
|
const browser = await chromium.launch({
|
|
headless: false,
|
|
slowMo: 400,
|
|
});
|
|
const page = await browser.newPage({ viewport: { width: 480, height: 640 } });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
|
|
// 1) Show banner
|
|
await page.waitForSelector('[data-testid="banner"]');
|
|
await sleep(800);
|
|
|
|
// 2) Type in search
|
|
const input = page.getByPlaceholder("Search…");
|
|
await input.click();
|
|
await input.fill("Meeting");
|
|
await sleep(900);
|
|
await input.fill("");
|
|
await sleep(500);
|
|
|
|
// 3) Hover / focus list rows
|
|
await page.getByText("Meeting notes").hover();
|
|
await sleep(600);
|
|
|
|
// 4) Open context menu + Paste plain
|
|
await page.getByTestId("open-menu").click();
|
|
await sleep(700);
|
|
await page.getByText("Paste plain").click();
|
|
await sleep(1000);
|
|
|
|
// 5) Click redacted password row (pastes full secret into banner)
|
|
await page.getByText("(password)").click({ modifiers: [] });
|
|
await sleep(1200);
|
|
|
|
// 6) Alt-click card row for plain
|
|
await page.getByText("(card)").click({ modifiers: ["Alt"] });
|
|
await sleep(1200);
|
|
|
|
// 7) Show transform menu → Pretty JSON not relevant; show uppercase on meeting
|
|
await page.getByText("Meeting notes").click({ button: "right" });
|
|
await sleep(600);
|
|
await page.getByText("UPPERCASE").click();
|
|
await sleep(1500);
|
|
|
|
const banner = await page.getByTestId("banner").innerText();
|
|
console.log("Final banner:", banner);
|
|
console.log("\nLeaving the window open for 5s so you can look…");
|
|
await sleep(5000);
|
|
|
|
await browser.close();
|
|
if (vite) {
|
|
try {
|
|
process.kill(-vite.pid);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
console.log("Visual walkthrough finished.");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|