Files
glimpse/test/smoke.js
Evan Reichard 6adb5111de refactor!: replace snapshot with reader fallback, collapse commands
Remove the snapshot command and enhance reader to try Firefox Reader
View first, falling back to raw Turndown conversion of document.body
when Reader View fails or is skipped via --no-reader.

- reader always returns markdown by default (--format=json for structured)
- JSON output includes method: 'reader' | 'raw' to signal extraction path
- --no-reader skips Reader View (stays on loaded page, preserving JS mutations)
- Add @ts-nocheck to test/smoke.js and exclude test/ from tsconfig
- Update all tests from snapshot to reader with --no-reader for data URIs
- Update AGENTS.md and help text

BREAKING CHANGE: snapshot subcommand removed; use reader instead.
2026-05-02 20:05:27 -04:00

389 lines
11 KiB
JavaScript
Executable File

#!/usr/bin/env node
// @ts-nocheck
import {
mkdtempSync,
rmSync,
existsSync,
mkdirSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import assert from "node:assert/strict";
const cliPath = new URL("../src/index.ts", import.meta.url).pathname;
const tempDir = mkdtempSync(join(tmpdir(), "glimpse-smoke-"));
const filters = process.argv.slice(2).filter((arg) => arg !== "--list");
const shouldList = process.argv.includes("--list");
const tests = [];
function test(name, tags, fn) {
tests.push({ name, tags, fn });
}
function dataHtml(html) {
return `data:text/html,${html}`;
}
function runCli(args, options = {}) {
return spawnSync(process.execPath, ["--import", "tsx", cliPath, ...args], {
encoding: "utf-8",
env: options.env ?? process.env,
timeout: 30000,
});
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error(`Failed to parse JSON: ${err.message}\n${text}`);
}
}
function expectSuccess(args, options = {}) {
const result = runCli(args, options);
assert.equal(result.status, 0, result.stderr || result.stdout);
return parseJson(result.stdout);
}
function expectFailure(args, options = {}) {
const result = runCli(args, options);
assert.notEqual(result.status, 0, result.stdout || result.stderr);
return parseJson(result.stderr);
}
function matchesFilters(entry) {
if (filters.length === 0) {
return true;
}
return filters.some((filter) => {
const normalized = filter.toLowerCase();
return (
entry.name.toLowerCase().includes(normalized) ||
entry.tags.includes(normalized)
);
});
}
test("no args prints help", ["help", "cli"], () => {
const result = runCli([]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Usage: glimpse <command> <url> \[options\]/);
assert.match(result.stdout, /reader <url>/);
assert.equal(result.stderr, "");
});
test("help flag prints help", ["help", "cli"], () => {
const result = runCli(["--help"]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Usage: glimpse <command> <url> \[options\]/);
assert.match(result.stdout, /--wait-js=<code>/);
assert.equal(result.stderr, "");
});
test("reader extracts page content as markdown", ["reader"], () => {
const result = runCli([
"reader",
dataHtml(
'<title>Hello</title><h1>Main</h1><p>Some text</p><a href="https://example.com">Link</a>',
),
"--no-reader",
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
const output = result.stdout.trim();
assert.match(output, /# Main/);
assert.match(output, /Some text/);
assert.match(output, /\[Link\]\(https:\/\/example\.com\/??\)/);
});
test("reader returns json format with method field", ["reader"], () => {
const output = expectSuccess([
"reader",
dataHtml("<title>Hello</title><h1>Main</h1><p>World</p>"),
"--no-reader",
"--format=json",
]);
assert.equal(output.title, "Hello");
assert.equal(output.method, "raw");
assert.equal(typeof output.markdown, "string");
assert.match(output.markdown, /# Main/);
assert.match(output.text, /Main/);
});
test(
"reader runs top-level javascript before extraction",
["reader", "js"],
() => {
const result = runCli([
"reader",
dataHtml("<title>Hello</title><h1>Old</h1>"),
"--no-reader",
"--js=document.querySelector('h1').textContent = 'New'",
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /# New/);
},
);
test("exec returns javascript result", ["exec", "js"], () => {
const result = runCli([
"exec",
dataHtml("<title>Hello</title>"),
"--js=return document.title",
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(result.stdout.trim(), "Hello");
});
test(
"screenshot returns standard success envelope and writes file",
["screenshot"],
() => {
const outputPath = join(tempDir, "page.png");
const output = expectSuccess([
"screenshot",
dataHtml("<title>Hello</title>"),
`--output=${outputPath}`,
]);
assert.equal(output.ok, true);
assert.equal(output.result.path, outputPath);
assert.equal(typeof output.elapsedMs, "number");
assert.equal(existsSync(outputPath), true);
},
);
test("search validates kagi token in provider", ["search", "errors"], () => {
const env = { ...process.env };
delete env.KAGI_TOKEN;
const result = runCli(["search", "--config=/nonexistent/path", "example query"], { env });
const output = parseJson(result.stderr);
assert.notEqual(result.status, 0, result.stdout || result.stderr);
assert.equal(output.ok, false);
assert.equal(output.error.code, "KAGI_TOKEN_REQUIRED");
assert.match(output.error.message, /Kagi search requires/);
assert.match(output.error.message, /config token/);
assert.equal(typeof output.elapsedMs, "number");
});
test(
"invalid config returns structured error before browser startup",
["config", "errors"],
() => {
const configPath = join(tempDir, "bad-config.json");
writeFileSync(configPath, "not json");
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
`--config=${configPath}`,
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "CONFIG_READ_FAILED");
assert.match(output.error.message, /Failed to read config file/);
},
);
test(
"invalid config schema returns structured error",
["config", "errors"],
() => {
const configPath = join(tempDir, "bad-schema.json");
writeFileSync(configPath, JSON.stringify({ search: { provider: 42 } }));
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
`--config=${configPath}`,
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "INVALID_CONFIG");
assert.match(output.error.message, /search\.provider must be a string/);
},
);
test("empty home config is accepted", ["config"], () => {
const configHome = join(tempDir, "config-home");
const configDir = join(configHome, "glimpse");
mkdirSync(configDir, { recursive: true });
writeFileSync(join(configDir, "config.json"), "{}");
const result = runCli(
["reader", dataHtml("<title>Hello</title><h1>Main</h1>"), "--no-reader"],
{ env: { ...process.env, XDG_CONFIG_HOME: configHome } },
);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /# Main/);
});
test("unknown command returns structured error", ["errors", "cli"], () => {
const output = expectFailure(["nope", dataHtml("<title>Hello</title>"), "--no-reader"]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "UNKNOWN_COMMAND");
assert.match(output.error.message, /Unknown command: nope/);
assert.equal(typeof output.elapsedMs, "number");
});
test(
"invalid timeout returns invalid option before browser startup",
["errors", "timeout"],
() => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
"--timeout=abc",
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "INVALID_OPTION");
assert.match(output.error.message, /--timeout must be a positive integer/);
assert.equal(typeof output.elapsedMs, "number");
},
);
test("invalid wait-until returns invalid option", ["errors", "wait"], () => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
"--wait-until=loaded",
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "INVALID_OPTION");
assert.match(output.error.message, /Unsupported --wait-until value: loaded/);
});
test("wait-js succeeds when condition is true", ["wait"], () => {
const result = runCli([
"reader",
dataHtml("<title>Hello</title><h1>Main</h1>"),
"--no-reader",
'--wait-js=return document.title === "Hello"',
]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /# Main/);
});
test("wait-js timeout returns wait timeout", ["wait", "errors"], () => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
"--wait-js=return false",
"--timeout=1",
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "WAIT_TIMEOUT");
assert.match(output.error.message, /waiting for --wait-js/);
assert.equal(typeof output.elapsedMs, "number");
assert.match(output.url, /^data:text\/html,/);
});
test(
"wait-js exception returns script failed",
["wait", "errors", "js"],
() => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
'--wait-js=throw new Error("boom")',
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "SCRIPT_FAILED");
assert.match(output.error.message, /--wait-js failed/);
assert.match(output.error.message, /boom/);
},
);
test(
"top-level javascript exception returns script failed",
["errors", "js"],
() => {
const output = expectFailure([
"reader",
dataHtml("<title>Hello</title>"),
"--no-reader",
'--js=throw new Error("boom")',
]);
assert.equal(output.ok, false);
assert.equal(output.error.code, "SCRIPT_FAILED");
assert.match(output.error.message, /Prelude script failed/);
assert.match(output.error.message, /boom/);
},
);
function listTests() {
const tags = [...new Set(tests.flatMap((entry) => entry.tags))].sort();
console.log(`Tags: ${tags.join(", ")}`);
for (const entry of tests) {
console.log(`${entry.name} [${entry.tags.join(", ")}]`);
}
}
async function main() {
if (shouldList) {
listTests();
return;
}
const selectedTests = tests.filter(matchesFilters);
if (selectedTests.length === 0) {
console.error(`No tests matched: ${filters.join(", ")}`);
process.exit(1);
}
let failed = 0;
// Run Tests
for (const { name, fn } of selectedTests) {
try {
await fn();
console.log(`ok - ${name}`);
} catch (err) {
failed += 1;
console.error(`not ok - ${name}`);
console.error(err.stack || err.message);
}
}
// Clean Temporary Files
rmSync(tempDir, { recursive: true, force: true });
if (failed > 0) {
console.error(`${failed} test(s) failed`);
process.exit(1);
}
console.log(`${selectedTests.length} test(s) passed`);
}
main().catch((err) => {
rmSync(tempDir, { recursive: true, force: true });
console.error(err.stack || err.message);
process.exit(1);
});