mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 10:59:32 +01:00
fix(links): resolve ambiguous hard wraps through guarded lookup
This commit is contained in:
1 parent
5ce8213726
commit
97d53f6621
10 files changed
+250
-103
No files matched your search
@@ -241,6 +241,8 @@ function loadApp() {
|
||||
app.sessions = new Map([['s1', { workingDir: '/mnt/build/neon_getaway-mgr-hebel' }]]);
|
||||
app.openFilePreview = vi.fn();
|
||||
app.openLogViewerWindow = vi.fn();
|
||||
app._registerExternalPreview = vi.fn(async () => ({ attachmentId: 'att-fixture' }));
|
||||
app.showToast = vi.fn();
|
||||
app._isExternalPreviewPath = (p: string) => p.startsWith('~/');
|
||||
return app;
|
||||
}
|
||||
@@ -257,7 +259,30 @@ function linksOn(lines: string[], row: number, cols = 80, wrappedRows = new Set<
|
||||
}
|
||||
|
||||
describe('terminal link provider', () => {
|
||||
it('retains the filename space when the CLI word-wraps a parenthesised path', () => {
|
||||
it.each([
|
||||
[
|
||||
['Saved the screenshot (/home/me/proj/out/ghost1b_game', ' play.png).'],
|
||||
'/home/me/proj/out/ghost1b_gameplay.png',
|
||||
],
|
||||
[
|
||||
[' ⎿ Wrote (/mnt/build/neon/builds/captures/ghost1b_gamepl', ' ay.png) ok'],
|
||||
'/mnt/build/neon/builds/captures/ghost1b_gameplay.png',
|
||||
],
|
||||
[
|
||||
['Saved the screenshot (/home/me/proj/out/ghost1b_', ' very_long_game', ' play.png).'],
|
||||
'/home/me/proj/out/ghost1b_very_long_gameplay.png',
|
||||
],
|
||||
])('preserves a parenthesised filename without spaces across hard wraps: %s', async (rows, path) => {
|
||||
for (let row = 1; row <= rows.length; row++) {
|
||||
const { app, links } = linksOn(rows, row, 120);
|
||||
expect(links.map((link) => link.text)).toEqual([path]);
|
||||
await links[0].activate({}, links[0].text);
|
||||
expect(app.openFilePreview).toHaveBeenCalledWith(path, 's1');
|
||||
expect(app._registerExternalPreview.mock.calls).toEqual([[path, 's1']]);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses the spaced filename only when the unspaced hard-wrap target is missing', async () => {
|
||||
const path = '/mnt/user/Scratch/Kobo/Set-KoboCovers/Kobo eReader.conf';
|
||||
const rows = [
|
||||
'Found it: I prepared a corrected configuration (/mnt/user/Scratch/Kobo/Set-KoboCovers/Kobo',
|
||||
@@ -265,13 +290,40 @@ describe('terminal link provider', () => {
|
||||
];
|
||||
for (const row of [1, 2]) {
|
||||
const { app, links } = linksOn(rows, row, 120);
|
||||
expect(links.map((link) => link.text)).toEqual([path]);
|
||||
const unspaced = path.replace('Kobo eReader', 'KoboeReader');
|
||||
expect(links.map((link) => link.text)).toEqual([unspaced]);
|
||||
expect(links[0].range.end).toEqual({ x: ' eReader.conf'.length + 1, y: 2 });
|
||||
links[0].activate({}, links[0].text);
|
||||
app._registerExternalPreview.mockImplementation(async (candidate: string) =>
|
||||
candidate === path ? { attachmentId: 'att-fixture' } : { status: 404, error: 'File not found' }
|
||||
);
|
||||
await links[0].activate({}, links[0].text);
|
||||
expect(app.openLogViewerWindow).toHaveBeenCalledWith(path, 's1');
|
||||
expect(app._registerExternalPreview.mock.calls).toEqual([
|
||||
[unspaced, 's1'],
|
||||
[path, 's1'],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([403, 500, 503])('does not try another path after a guarded lookup returns HTTP %s', async (status) => {
|
||||
const { app, links } = linksOn(['Saved (/mnt/data/Kobo', ' eReader.conf).'], 1, 120);
|
||||
app._registerExternalPreview.mockResolvedValue({ status, error: 'Lookup refused' });
|
||||
await links[0].activate({}, links[0].text);
|
||||
expect(app._registerExternalPreview).toHaveBeenCalledTimes(1);
|
||||
expect(app.openFilePreview).not.toHaveBeenCalled();
|
||||
expect(app.openLogViewerWindow).not.toHaveBeenCalled();
|
||||
expect(app.showToast).toHaveBeenCalledWith('Lookup refused', 'error');
|
||||
});
|
||||
|
||||
it('keeps the session selected at click time while a path lookup is pending', async () => {
|
||||
const { app, links } = linksOn(['Saved (/mnt/data/Kobo', ' eReader.conf).'], 1, 120);
|
||||
const pending = links[0].activate({}, links[0].text);
|
||||
app.activeSessionId = 'another-session';
|
||||
await pending;
|
||||
expect(app._registerExternalPreview).toHaveBeenCalledWith('/mnt/data/KoboeReader.conf', 's1');
|
||||
expect(app.openLogViewerWindow).toHaveBeenCalledWith('/mnt/data/KoboeReader.conf', 's1');
|
||||
});
|
||||
|
||||
it('opens the complete parenthesised configuration path from either wrapped row', () => {
|
||||
const path = '/mnt/user/Scratch/Kobo/Set-KoboCovers/Kobo eReader.conf';
|
||||
const text = `Found it: I prepared a corrected configuration (${path}).`;
|
||||
|
||||
@@ -212,6 +212,27 @@ function loadApp(fetchImpl: (url: string, init?: { body?: string }) => unknown)
|
||||
return { app, elements, keyHandlers, fetch: context.fetch as ReturnType<typeof vi.fn> };
|
||||
}
|
||||
|
||||
describe('guarded attachment registration results', () => {
|
||||
it.each([404, 403, 500, 503])('preserves HTTP %s for the activation decision', async (status) => {
|
||||
const { app, fetch } = loadApp(async () => ({
|
||||
ok: false,
|
||||
status,
|
||||
json: async () => ({ error: 'Cannot read file' }),
|
||||
}));
|
||||
const result = await Object.getPrototypeOf(app)._registerExternalPreview.call(app, '/mnt/data/a.conf', 's1');
|
||||
expect(result).toEqual({ error: 'Cannot read file', status });
|
||||
expect(JSON.parse(fetch.mock.calls[0][1].body)).toEqual({ path: '/mnt/data/a.conf', notify: false });
|
||||
});
|
||||
|
||||
it('does not turn a connection failure into a missing file', async () => {
|
||||
const { app } = loadApp(async () => {
|
||||
throw new Error('Connection lost');
|
||||
});
|
||||
const result = await Object.getPrototypeOf(app)._registerExternalPreview.call(app, '/mnt/data/a.conf', 's1');
|
||||
expect(result).toEqual({ error: 'Connection lost' });
|
||||
});
|
||||
});
|
||||
|
||||
const LISTING = {
|
||||
folderId: 'fld_1',
|
||||
path: '/mnt/raid1/styleframes',
|
||||
|
||||
@@ -42,6 +42,10 @@ describe('spaced file paths', () => {
|
||||
expect(paths('Do not link (/mnt/data/../private files/settings.conf).')).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not take a comma-separated list inside parentheses as one spaced filename', () => {
|
||||
expect(paths('Updated (/home/me/proj, config.json)')).toEqual(['/home/me/proj', 'config.json']);
|
||||
});
|
||||
|
||||
it('links an own-line absolute path with spaces whole', () => {
|
||||
expect(paths(LONG)).toEqual([LONG]);
|
||||
expect(paths('/mnt/c/proj/My Big Docs/report.docx')).toEqual(['/mnt/c/proj/My Big Docs/report.docx']);
|
||||
|
||||
@@ -16,67 +16,100 @@ describe('wrapped configuration file links (Chromium)', () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it.each(['soft', 'hard'])('opens the complete file from both rows of a %s wrap', async (wrap) => {
|
||||
const page = await browser.newPage({ viewport: { width: 1920, height: 600 } });
|
||||
try {
|
||||
await page.setContent('<div id="terminal"></div>');
|
||||
await page.addStyleTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/css/xterm.css') });
|
||||
await page.addScriptTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/lib/xterm.js') });
|
||||
await page.addScriptTag({ content: 'window.CodemanApp = class CodemanApp {};' });
|
||||
for (const file of ['constants.js', 'terminal-ui.js']) {
|
||||
await page.addScriptTag({ content: readFileSync(resolve(publicDir, file), 'utf8') });
|
||||
}
|
||||
await page.evaluate(
|
||||
async ({ path, wrap }) => {
|
||||
const w = window as any;
|
||||
const first = `Found it: prepared a corrected configuration (${path.slice(0, -' eReader.conf'.length)}`;
|
||||
const terminal = new w.Terminal({ cols: first.length + (wrap === 'soft' ? 1 : 20), rows: 6 });
|
||||
terminal.open(document.getElementById('terminal'));
|
||||
w.opened = [];
|
||||
w.app = Object.assign(new w.CodemanApp(), {
|
||||
terminal,
|
||||
activeSessionId: 'fixture',
|
||||
sessions: new Map([['fixture', { workingDir: '/workspace' }]]),
|
||||
_isExternalPreviewPath: () => true,
|
||||
openFilePreview: (file: string) => w.opened.push(file),
|
||||
openLogViewerWindow: (file: string) => w.opened.push(`log:${file}`),
|
||||
openFolderGrid: (file: string) => w.opened.push(`folder:${file}`),
|
||||
});
|
||||
w.app.registerFilePathLinkProvider();
|
||||
const text = `${first}${wrap === 'soft' ? ' ' : '\r\n '}eReader.conf).`;
|
||||
await new Promise<void>((done) => terminal.write(text, done));
|
||||
await new Promise<void>((done) => requestAnimationFrame(() => requestAnimationFrame(() => done())));
|
||||
},
|
||||
{ path, wrap }
|
||||
);
|
||||
|
||||
for (const row of [0, 1]) {
|
||||
const point = await page.evaluate(
|
||||
async ({ path, row }) => {
|
||||
const app = (window as any).app;
|
||||
const terminal = app.terminal;
|
||||
const links = await new Promise<any[]>((done) => app._terminalLinkProvider.provideLinks(row + 1, done));
|
||||
const link = links.find((candidate) => candidate.text === path);
|
||||
if (!link) throw new Error(`The complete filename is not linked on row ${row + 1}`);
|
||||
const text = terminal.buffer.active.getLine(row).translateToString(true);
|
||||
const col = row === 0 ? link.range.start.x : text.indexOf('eReader') + 1;
|
||||
const box = terminal.element.querySelector('.xterm-screen').getBoundingClientRect();
|
||||
return {
|
||||
x: box.left + (col - 0.5) * (box.width / terminal.cols),
|
||||
y: box.top + (row + 0.5) * (box.height / terminal.rows),
|
||||
};
|
||||
},
|
||||
{ path, row }
|
||||
it.each([
|
||||
{ wrap: 'soft', originalExists: false },
|
||||
{ wrap: 'hard', originalExists: false },
|
||||
{ wrap: 'hard', originalExists: true },
|
||||
])(
|
||||
'opens the existing file from both rows: $wrap, original exists: $originalExists',
|
||||
async ({ wrap, originalExists }) => {
|
||||
const original = path.replace('Kobo eReader', 'KoboeReader');
|
||||
const expected = wrap === 'hard' && originalExists ? original : path;
|
||||
const lookups: string[] = [];
|
||||
const page = await browser.newPage({ viewport: { width: 1920, height: 600 } });
|
||||
try {
|
||||
await page.route('http://fixture.test/', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: '<div id="terminal"></div>',
|
||||
})
|
||||
);
|
||||
await page.mouse.move(point.x, point.y);
|
||||
await page.waitForFunction(() => (window as any).app._linkHovered === true);
|
||||
await page.mouse.click(point.x, point.y);
|
||||
await page.waitForFunction((count) => (window as any).opened.length === count, row + 1);
|
||||
expect(await page.evaluate(() => (window as any).opened)).toEqual(Array(row + 1).fill(path));
|
||||
await page.mouse.move(0, 0);
|
||||
await page.route('http://fixture.test/api/sessions/fixture/attachments', async (route) => {
|
||||
const body = route.request().postDataJSON();
|
||||
expect(body.notify).toBe(false);
|
||||
lookups.push(body.path);
|
||||
const exists = body.path === path || (originalExists && body.path === original);
|
||||
await route.fulfill({
|
||||
status: exists ? 200 : 404,
|
||||
json: exists
|
||||
? { success: true, data: { attachmentId: 'att-fixture' } }
|
||||
: { success: false, error: 'File not found' },
|
||||
});
|
||||
});
|
||||
await page.goto('http://fixture.test/');
|
||||
await page.addStyleTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/css/xterm.css') });
|
||||
await page.addScriptTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/lib/xterm.js') });
|
||||
await page.addScriptTag({ content: 'window.CodemanApp = class CodemanApp {};' });
|
||||
for (const file of ['constants.js', 'terminal-ui.js', 'panels-ui.js']) {
|
||||
await page.addScriptTag({ content: readFileSync(resolve(publicDir, file), 'utf8') });
|
||||
}
|
||||
await page.evaluate(
|
||||
async ({ path, wrap }) => {
|
||||
const w = window as any;
|
||||
const first = `Found it: prepared a corrected configuration (${path.slice(0, -' eReader.conf'.length)}`;
|
||||
const terminal = new w.Terminal({ cols: first.length + (wrap === 'soft' ? 1 : 20), rows: 6 });
|
||||
terminal.open(document.getElementById('terminal'));
|
||||
w.opened = [];
|
||||
w.app = Object.assign(new w.CodemanApp(), {
|
||||
terminal,
|
||||
activeSessionId: 'fixture',
|
||||
sessions: new Map([['fixture', { workingDir: '/workspace' }]]),
|
||||
_isExternalPreviewPath: () => true,
|
||||
openFilePreview: (file: string) => w.opened.push(file),
|
||||
openLogViewerWindow: (file: string) => w.opened.push(`log:${file}`),
|
||||
openFolderGrid: (file: string) => w.opened.push(`folder:${file}`),
|
||||
showToast: (message: string) => w.opened.push(`error:${message}`),
|
||||
});
|
||||
w.app.registerFilePathLinkProvider();
|
||||
const text = `${first}${wrap === 'soft' ? ' ' : '\r\n '}eReader.conf).`;
|
||||
await new Promise<void>((done) => terminal.write(text, done));
|
||||
await new Promise<void>((done) => requestAnimationFrame(() => requestAnimationFrame(() => done())));
|
||||
},
|
||||
{ path, wrap }
|
||||
);
|
||||
|
||||
expect(lookups).toEqual([]);
|
||||
for (const row of [0, 1]) {
|
||||
const point = await page.evaluate(
|
||||
async ({ path, row }) => {
|
||||
const app = (window as any).app;
|
||||
const terminal = app.terminal;
|
||||
const links = await new Promise<any[]>((done) => app._terminalLinkProvider.provideLinks(row + 1, done));
|
||||
const link = links.find((candidate) => candidate.text === path);
|
||||
if (!link) throw new Error(`The complete filename is not linked on row ${row + 1}`);
|
||||
const text = terminal.buffer.active.getLine(row).translateToString(true);
|
||||
const col = row === 0 ? link.range.start.x : text.indexOf('eReader') + 1;
|
||||
const box = terminal.element.querySelector('.xterm-screen').getBoundingClientRect();
|
||||
return {
|
||||
x: box.left + (col - 0.5) * (box.width / terminal.cols),
|
||||
y: box.top + (row + 0.5) * (box.height / terminal.rows),
|
||||
};
|
||||
},
|
||||
{ path: wrap === 'hard' ? original : path, row }
|
||||
);
|
||||
await page.mouse.move(point.x, point.y);
|
||||
await page.waitForFunction(() => (window as any).app._linkHovered === true);
|
||||
await page.mouse.click(point.x, point.y);
|
||||
await page.waitForFunction((count) => (window as any).opened.length === count, row + 1);
|
||||
expect(await page.evaluate(() => (window as any).opened)).toEqual(Array(row + 1).fill(expected));
|
||||
await page.mouse.move(0, 0);
|
||||
}
|
||||
expect(lookups).toEqual(
|
||||
wrap === 'soft' ? [] : originalExists ? [original, original] : [original, path, original, path]
|
||||
);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user