mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(terminal): keep unrelated rows out of wrapped links
This commit is contained in:
1 parent
9f62da9ac3
commit
40a9528be1
8 files changed
+77
-16
No files matched your search
@@ -17,6 +17,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const publicFile = (name: string) => readFileSync(join(__dirname, '..', 'src', 'web', 'public', name), 'utf-8');
|
||||
|
||||
@@ -99,8 +100,9 @@ describe('terminal link-provider regexes (shipped source)', () => {
|
||||
// Excluding `&` truncated every real query string: a WordPress edit link
|
||||
// resolved to `?post=1479` and opened the wrong page, and Claude Code's OAuth
|
||||
// login URL (many `&` params) was not clickable at all.
|
||||
const url = shippedPattern('urlPattern');
|
||||
const strip = (u: string) => u.replace(/[.,;:!?)&]+$/, '');
|
||||
const context = vm.createContext({ console, window: undefined });
|
||||
vm.runInContext(`${CONSTANTS_SOURCE}\nglobalThis.findUrls = findTerminalUrls;`, context);
|
||||
const findUrls = context.findUrls as (text: string) => Array<{ url: string }>;
|
||||
const cases: Array<[string, string]> = [
|
||||
[
|
||||
'updated in place: https://bio-hacking.blog/wp-admin/post.php?post=1479&action=edit',
|
||||
@@ -115,10 +117,7 @@ describe('terminal link-provider regexes (shipped source)', () => {
|
||||
['trailing https://x.com/a?b=1& next', 'https://x.com/a?b=1'],
|
||||
];
|
||||
for (const [line, want] of cases) {
|
||||
url.lastIndex = 0;
|
||||
const m = url.exec(line);
|
||||
expect(m, line).not.toBeNull();
|
||||
expect(strip(m![0]), line).toBe(want);
|
||||
expect(findUrls(line).map((match) => match.url), line).toEqual([want]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@ const TABLE: Row[] = [
|
||||
{ text: ROW2 },
|
||||
{ text: '' },
|
||||
];
|
||||
const LISTINGS = [
|
||||
['/tmp/x', '/tmp/x/dir', '/tmp/x/dir/a.png'],
|
||||
[' ⎿ /home/u/proj/src', ' /home/u/proj/src/a.ts'],
|
||||
['4.0K /tmp/foo/bar/', '8.0K /tmp/foo/baz.png'],
|
||||
['/home/u/proj/docs:', 'guide.md notes.md'],
|
||||
['Created /tmp/out', 'report.md written next to it'],
|
||||
];
|
||||
|
||||
const BEFORE = '/tmp/demo-check/docs/review/BLDMESH-243/before-overview.png';
|
||||
const AFTER = '/tmp/demo-check/docs/review/BLDMESH-243/after-overview.png';
|
||||
|
||||
@@ -70,6 +78,11 @@ describe('terminalCellWrappedPaths', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(LISTINGS.map((lines) => [lines]))('does not join independent listing rows: %j', (lines) => {
|
||||
const rows = lines.map((text) => ({ text }));
|
||||
for (let row = 0; row < rows.length; row++) expect(find(fakeBuffer(rows, COLS), row)).toEqual([]);
|
||||
});
|
||||
|
||||
it('places the segments on the path cells only', () => {
|
||||
const hits = find(fakeBuffer(TABLE, COLS), 1);
|
||||
const before = hits.find((h) => h.path === BEFORE)!;
|
||||
@@ -188,6 +201,20 @@ describe('terminal link provider with cell-wrapped paths', () => {
|
||||
return { provide, app };
|
||||
}
|
||||
|
||||
it.each(LISTINGS.map((lines) => [lines]))('preserves ordinary listing links: %j', (lines) => {
|
||||
const { provide } = harness(lines.map((text) => ({ text })));
|
||||
const expected = [
|
||||
['/tmp/x', '/tmp/x/dir', '/tmp/x/dir/a.png'],
|
||||
['/home/u/proj/src', '/home/u/proj/src/a.ts'],
|
||||
['/tmp/foo/bar/', '/tmp/foo/baz.png'],
|
||||
['/home/u/proj/docs', 'guide.md'],
|
||||
['/tmp/out', 'report.md'],
|
||||
][LISTINGS.indexOf(lines)];
|
||||
for (let row = 0; row < lines.length; row++) {
|
||||
expect(provide(row + 1).map((link) => link.text)).toContain(expected[row]);
|
||||
}
|
||||
});
|
||||
|
||||
it('makes both screenshot paths clickable from either row and opens the whole path', () => {
|
||||
const { provide, app } = harness(TABLE);
|
||||
for (const line of [2, 3]) {
|
||||
|
||||
@@ -64,6 +64,12 @@ describe('terminalLogicalLine', () => {
|
||||
const ROW1 = ' ackage/thing';
|
||||
const HARD = [{ text: ROW0 }, { text: ROW1 }];
|
||||
|
||||
it('keeps full-width dividers out of link and touch-selection text', () => {
|
||||
const rows = buffer([{ text: '─'.repeat(COLS) }, { text: 'https://example.com/docs' }]);
|
||||
expect(terminalLogicalLine(rows, 0, COLS)).toMatchObject({ startRow: 0, endRow: 0, text: '─'.repeat(COLS) });
|
||||
expect(terminalLogicalLine(rows, 1, COLS)).toMatchObject({ startRow: 1, endRow: 1, text: 'https://example.com/docs' });
|
||||
});
|
||||
|
||||
it('stitches a hard wrap and drops the indent the continuation carries', () => {
|
||||
const line = terminalLogicalLine(buffer(HARD), 0, COLS)!;
|
||||
|
||||
|
||||
@@ -168,6 +168,16 @@ describe('terminalUrlContinuationExtent (wrap joining)', () => {
|
||||
expect(terminalUrlContinuationExtent(buffer, 4, COLS, 12)).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['1. https://example.com/one/', '2. https://example.com/two/'],
|
||||
['Docs: https://docs.example.com/guide/', '1. Install the package'],
|
||||
['Fetching https://registry.example.com/pkg/', 'v1.2.3 is the latest'],
|
||||
['GET http://localhost:3000/api/', '200 OK in 12ms'],
|
||||
])('keeps list, version and status rows separate after %s', (first, next) => {
|
||||
const buffer = fakeBuffer([{ text: first }, { text: next }], 100);
|
||||
for (const row of [0, 1]) expect(terminalUrlContinuationExtent(buffer, row, 100, 12)).toBeNull();
|
||||
});
|
||||
|
||||
it('does not glue a following prose line onto a URL that ends a line', () => {
|
||||
const buffer = fakeBuffer(
|
||||
[{ text: 'Download from https://example.com/files/' }, { text: 'then run the installer' }],
|
||||
@@ -204,11 +214,12 @@ describe('terminalUrlContinuationExtent (wrap joining)', () => {
|
||||
describe('native OSC 8 terminal URLs', () => {
|
||||
it('opens complete HTTP links through the terminal opener and rejects unsafe schemes', () => {
|
||||
const open = vi.fn();
|
||||
const confirm = vi.fn(() => true);
|
||||
const context = vm.createContext({
|
||||
console,
|
||||
CodemanApp: class CodemanApp {},
|
||||
document: { addEventListener: vi.fn() },
|
||||
window: { addEventListener: vi.fn(), removeEventListener: vi.fn() },
|
||||
window: { addEventListener: vi.fn(), removeEventListener: vi.fn(), confirm },
|
||||
});
|
||||
vm.runInContext(`${CONSTANTS}\n${TERMINAL_UI}\nglobalThis.__proto = CodemanApp.prototype;`, context);
|
||||
const proto = (
|
||||
@@ -220,10 +231,16 @@ describe('native OSC 8 terminal URLs', () => {
|
||||
const url = 'https://docs.example.com/Doc.aspx?file=Q3%20Plan&action=default&mobileredirect=true';
|
||||
handler.activate(null, url);
|
||||
expect(open).toHaveBeenCalledExactlyOnceWith(url);
|
||||
expect(confirm).toHaveBeenCalledExactlyOnceWith(`Open this link?\n\n${url}`);
|
||||
confirm.mockReturnValue(false);
|
||||
handler.activate(null, 'http://localhost:3000/private');
|
||||
expect(confirm).toHaveBeenLastCalledWith('Open this link?\n\nhttp://localhost:3000/private');
|
||||
expect(open).toHaveBeenCalledTimes(1);
|
||||
for (const unsafe of ['javascript:alert(1)', 'data:text/html,hello', 'file:///etc/passwd']) {
|
||||
handler.activate(null, unsafe);
|
||||
}
|
||||
expect(open).toHaveBeenCalledTimes(1);
|
||||
expect(confirm).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user