Turn the hardcoded straight-line sweep into a strategy system behind three
seams so new patterns are easy to add and, for the first time, testable
without nut.js or a real screen:
- src/device.ts: injectable Device seam over nut.js (autoDelayMs lives
here now); the only module that touches the native lib.
- src/strategies.ts: pure per-pattern path generators + registry + lenient
name resolution. Ships line, diagonal, jitter, walk,
arc, figureEight.
- src/executor.ts: single executePath driver owning bounds policy
(abort/clamp/reflect), pacing, interrupt detection, and
restore-on-clean.
keeper.ts's simulateActivity now selects a strategy and delegates to the
executor; the default `line` pattern is byte-for-byte the previous behavior.
New config surface, layered CLI > file > default with strict validation:
- -p/--pattern <name> movement strategy (names matched case/-/_-insensitive)
- -s/--step-size <px> pixels per step; stepCount is now a step *count*
Robustness for the new edge-seeking patterns: interrupt detection compares
against the last commanded (rounded) point with a 2px tolerance, and
clamp/reflect stay a couple pixels off the screen edge, so sub-pixel cursor
placement on scaled/multi-monitor displays isn't misread as user activity.
jitter's radius scales with sweep length so it moves at the default stepSize.
Tests: new suites for strategies, the executor (all bounds policies,
rounding, interrupt, tolerance, pacing), and the keeper loop; config and
configFile suites extended for pattern/stepSize. editor.test.ts moved to
tests/ for consistency. 64 pass.
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
/**
|
|
* editor.test.ts
|
|
* --------------
|
|
* Unit tests for the `--edit` helper. The spawn path is not exercised
|
|
* (would actually launch $EDITOR); instead we test:
|
|
* - the pure argv-construction helper, and
|
|
* - the two refusal paths ($EDITOR unset, file missing).
|
|
*
|
|
* Run via `bun test`.
|
|
*/
|
|
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { editConfig, editorCommand } from "../src/editor.ts";
|
|
import { CliError } from "../src/errors.ts";
|
|
|
|
describe("editorCommand", () => {
|
|
test("builds 'sh -c <editor> \"$@\"' argv with -- placeholder and path", () => {
|
|
const argv = editorCommand("vim", "/tmp/x.json");
|
|
expect(argv).toEqual(["sh", "-c", 'vim "$@"', "--", "/tmp/x.json"]);
|
|
});
|
|
|
|
test("interpolates the editor verbatim so shell word-splits multi-word values", () => {
|
|
const argv = editorCommand("code --wait", "/path with space.json");
|
|
expect(argv).toEqual([
|
|
"sh",
|
|
"-c",
|
|
'code --wait "$@"',
|
|
"--",
|
|
"/path with space.json",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("editConfig", () => {
|
|
let TMP: string;
|
|
let savedEditor: string | undefined;
|
|
|
|
beforeAll(() => {
|
|
TMP = mkdtempSync(join(tmpdir(), "move-edit-test-"));
|
|
});
|
|
|
|
afterAll(() => {
|
|
rmSync(TMP, { recursive: true, force: true });
|
|
});
|
|
|
|
beforeEach(() => {
|
|
savedEditor = process.env.EDITOR;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (savedEditor === undefined) delete process.env.EDITOR;
|
|
else process.env.EDITOR = savedEditor;
|
|
});
|
|
|
|
test("throws CliError when $EDITOR is unset", () => {
|
|
delete process.env.EDITOR;
|
|
expect(() => editConfig(join(TMP, "any.json"))).toThrow(CliError);
|
|
});
|
|
|
|
test("throws CliError when $EDITOR is empty", () => {
|
|
process.env.EDITOR = "";
|
|
expect(() => editConfig(join(TMP, "any.json"))).toThrow(CliError);
|
|
});
|
|
|
|
test("throws CliError when the config file does not exist", () => {
|
|
// Use a benign editor command that we never actually reach (the
|
|
// existence check fires first).
|
|
process.env.EDITOR = "true";
|
|
const missing = join(TMP, "no-such-file.json");
|
|
expect(() => editConfig(missing)).toThrow(/no config file at/);
|
|
});
|
|
|
|
test("error message names the missing path", () => {
|
|
process.env.EDITOR = "true";
|
|
const missing = join(TMP, "missing.json");
|
|
expect(() => editConfig(missing)).toThrow(new RegExp(missing.replace(/[.]/g, "\\.")));
|
|
});
|
|
|
|
test("error message mentions 'reinstall' as a recovery hint", () => {
|
|
process.env.EDITOR = "true";
|
|
expect(() => editConfig(join(TMP, "x.json"))).toThrow(/reinstall/);
|
|
});
|
|
|
|
test("$EDITOR unset error explicitly mentions setting it", () => {
|
|
delete process.env.EDITOR;
|
|
expect(() => editConfig(join(TMP, "x.json"))).toThrow(/export EDITOR/);
|
|
});
|
|
|
|
// Success path: $EDITOR set, file exists. The editor IS spawned and we
|
|
// then call process.exit() — which kills the test process. So we don't
|
|
// exercise this code path in unit tests; the manual smoke test in
|
|
// dev-setup verifies end-to-end behavior instead.
|
|
test("placeholder: success path is verified via manual `EDITOR=true move -e` run", () => {
|
|
// Intentionally empty assertion. See comment above.
|
|
expect(true).toBe(true);
|
|
// Ensure the fixture path is referenced so this test isn't seen
|
|
// as truly empty if the fixture system ever needs assertion.
|
|
writeFileSync(join(TMP, "exists.json"), "{}");
|
|
});
|
|
});
|