mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
fix(file-viewer): COD-341 synchronize session lifecycle
This commit is contained in:
@@ -5633,6 +5633,7 @@ class CodemanApp {
|
|||||||
|
|
||||||
this._cleanupPreviousSession(sessionId);
|
this._cleanupPreviousSession(sessionId);
|
||||||
this.activeSessionId = sessionId;
|
this.activeSessionId = sessionId;
|
||||||
|
this._activateFileBrowserSession?.(sessionId);
|
||||||
// Repaint the partial-history banner for the tab being switched TO. The
|
// Repaint the partial-history banner for the tab being switched TO. The
|
||||||
// replay paths refresh it when their fetch lands; without this the previous
|
// replay paths refresh it when their fetch lands; without this the previous
|
||||||
// session's notice stays on screen until then (#258).
|
// session's notice stays on screen until then (#258).
|
||||||
|
|||||||
@@ -3002,6 +3002,81 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
return this._fileBrowserState;
|
return this._fileBrowserState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_activateFileBrowserSession(sessionId) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
const state = this._ensureFileBrowserState();
|
||||||
|
if (state.inFlight?.timer !== undefined && state.inFlight?.timer !== null) {
|
||||||
|
clearTimeout(state.inFlight.timer);
|
||||||
|
}
|
||||||
|
state.searchEpoch++;
|
||||||
|
state.treeEpoch++;
|
||||||
|
state.ownerSessionId = sessionId;
|
||||||
|
state.treeInFlight = null;
|
||||||
|
state.inFlight = null;
|
||||||
|
state.normalState = null;
|
||||||
|
state.matches = [];
|
||||||
|
state.deferredDirectoryTarget = null;
|
||||||
|
state.filter = '';
|
||||||
|
state.view = 'normal';
|
||||||
|
this.fileBrowserData = null;
|
||||||
|
this.fileBrowserFilter = '';
|
||||||
|
this.fileBrowserExpandedDirs?.clear?.();
|
||||||
|
this.fileBrowserAllExpanded = false;
|
||||||
|
|
||||||
|
const searchInput = this.$?.('fileBrowserSearch');
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
this._setFileBrowserExpandDisabled(false);
|
||||||
|
const expandBtn = this.$?.('fileBrowserExpandBtn');
|
||||||
|
if (expandBtn) expandBtn.innerHTML = '\u229E';
|
||||||
|
|
||||||
|
const panel = this.$?.('fileBrowserPanel');
|
||||||
|
const treeEl = this.$?.('fileBrowserTree');
|
||||||
|
const statusEl = this.$?.('fileBrowserStatus');
|
||||||
|
const visible = panel?.classList.contains('visible') === true;
|
||||||
|
if (treeEl) {
|
||||||
|
treeEl.innerHTML = visible
|
||||||
|
? `<div class="file-browser-loading">${escapeHtml('Loading files...')}</div>`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
if (statusEl) statusEl.textContent = visible ? 'Loading files...' : '';
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
const load = this.loadFileBrowser?.(sessionId);
|
||||||
|
load?.catch?.(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetFileBrowserForHide() {
|
||||||
|
const state = this._ensureFileBrowserState();
|
||||||
|
if (state.inFlight?.timer !== undefined && state.inFlight?.timer !== null) {
|
||||||
|
clearTimeout(state.inFlight.timer);
|
||||||
|
}
|
||||||
|
state.searchEpoch++;
|
||||||
|
state.treeEpoch++;
|
||||||
|
state.treeInFlight = null;
|
||||||
|
state.inFlight = null;
|
||||||
|
state.normalState = null;
|
||||||
|
state.matches = [];
|
||||||
|
state.deferredDirectoryTarget = null;
|
||||||
|
state.filter = '';
|
||||||
|
state.view = 'normal';
|
||||||
|
this.fileBrowserData = null;
|
||||||
|
this.fileBrowserFilter = '';
|
||||||
|
this.fileBrowserExpandedDirs?.clear?.();
|
||||||
|
this.fileBrowserAllExpanded = false;
|
||||||
|
|
||||||
|
const searchInput = this.$?.('fileBrowserSearch');
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
this._setFileBrowserExpandDisabled(false);
|
||||||
|
const expandBtn = this.$?.('fileBrowserExpandBtn');
|
||||||
|
if (expandBtn) expandBtn.innerHTML = '\u229E';
|
||||||
|
const treeEl = this.$?.('fileBrowserTree');
|
||||||
|
if (treeEl) treeEl.innerHTML = '';
|
||||||
|
const statusEl = this.$?.('fileBrowserStatus');
|
||||||
|
if (statusEl) statusEl.textContent = '';
|
||||||
|
},
|
||||||
|
|
||||||
_setFileBrowserExpandDisabled(disabled) {
|
_setFileBrowserExpandDisabled(disabled) {
|
||||||
const btn = this.$('fileBrowserExpandBtn');
|
const btn = this.$('fileBrowserExpandBtn');
|
||||||
if (btn) btn.disabled = disabled;
|
if (btn) btn.disabled = disabled;
|
||||||
@@ -3561,6 +3636,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
closeFileBrowserPanel() {
|
closeFileBrowserPanel() {
|
||||||
const panel = this.$('fileBrowserPanel');
|
const panel = this.$('fileBrowserPanel');
|
||||||
|
this._resetFileBrowserForHide();
|
||||||
if (panel) {
|
if (panel) {
|
||||||
panel.classList.remove('visible');
|
panel.classList.remove('visible');
|
||||||
// Reset position so it reopens at default location
|
// Reset position so it reopens at default location
|
||||||
|
|||||||
@@ -2832,7 +2832,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this.fileBrowserDragListeners._onFirstDrag = onFirstDrag;
|
this.fileBrowserDragListeners._onFirstDrag = onFirstDrag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (fileBrowserPanel.classList.contains('visible')) {
|
||||||
|
this._resetFileBrowserForHide?.();
|
||||||
fileBrowserPanel.classList.remove('visible');
|
fileBrowserPanel.classList.remove('visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import vm from 'node:vm';
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
||||||
|
const appJs = readFileSync(resolve(PUBLIC, 'app.js'), 'utf8');
|
||||||
const panelsJs = readFileSync(resolve(PUBLIC, 'panels-ui.js'), 'utf8');
|
const panelsJs = readFileSync(resolve(PUBLIC, 'panels-ui.js'), 'utf8');
|
||||||
|
const settingsJs = readFileSync(resolve(PUBLIC, 'settings-ui.js'), 'utf8');
|
||||||
|
|
||||||
type ClickHandler = () => void;
|
type ClickHandler = () => void;
|
||||||
|
|
||||||
@@ -34,9 +36,11 @@ interface FakeElement {
|
|||||||
textContent: string;
|
textContent: string;
|
||||||
value: string;
|
value: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
style: Record<string, string>;
|
||||||
attrs: Record<string, string>;
|
attrs: Record<string, string>;
|
||||||
classList: FakeClassList;
|
classList: FakeClassList;
|
||||||
setAttribute: (name: string, value: string) => void;
|
setAttribute: (name: string, value: string) => void;
|
||||||
|
querySelector: (selector: string) => FakeElement | null;
|
||||||
querySelectorAll: (selector: string) => FakeRow[];
|
querySelectorAll: (selector: string) => FakeRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +110,15 @@ function fakeElement(initialClasses: string[] = []): FakeElement {
|
|||||||
textContent: '',
|
textContent: '',
|
||||||
value: '',
|
value: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
style: {},
|
||||||
attrs,
|
attrs,
|
||||||
classList,
|
classList,
|
||||||
setAttribute(name, value) {
|
setAttribute(name, value) {
|
||||||
attrs[name] = value;
|
attrs[name] = value;
|
||||||
},
|
},
|
||||||
|
querySelector() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
querySelectorAll(selector) {
|
querySelectorAll(selector) {
|
||||||
return selector === '.file-tree-item' ? rows : [];
|
return selector === '.file-tree-item' ? rows : [];
|
||||||
},
|
},
|
||||||
@@ -153,7 +161,12 @@ function loadPanel(options: { sessionId?: string | null; showHidden?: boolean }
|
|||||||
console,
|
console,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
localStorage: { getItem: () => null, setItem: vi.fn() },
|
localStorage: { getItem: () => null, setItem: vi.fn() },
|
||||||
document: { getElementById: () => null, addEventListener: vi.fn(), querySelector: vi.fn() },
|
document: {
|
||||||
|
getElementById: (id: string) => elements[id] ?? null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
querySelector: vi.fn(),
|
||||||
|
},
|
||||||
window: { addEventListener: vi.fn() },
|
window: { addEventListener: vi.fn() },
|
||||||
setTimeout,
|
setTimeout,
|
||||||
clearTimeout,
|
clearTimeout,
|
||||||
@@ -163,6 +176,7 @@ function loadPanel(options: { sessionId?: string | null; showHidden?: boolean }
|
|||||||
return reply.promise;
|
return reply.promise;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
vm.runInContext(settingsJs, context, { filename: 'settings-ui.js' });
|
||||||
vm.runInContext(panelsJs, context, { filename: 'panels-ui.js' });
|
vm.runInContext(panelsJs, context, { filename: 'panels-ui.js' });
|
||||||
|
|
||||||
const app = new CodemanApp() as Record<string, any>;
|
const app = new CodemanApp() as Record<string, any>;
|
||||||
@@ -174,6 +188,9 @@ function loadPanel(options: { sessionId?: string | null; showHidden?: boolean }
|
|||||||
app.fileBrowserAllExpanded = false;
|
app.fileBrowserAllExpanded = false;
|
||||||
app.fileBrowserShowHidden = options.showHidden ?? false;
|
app.fileBrowserShowHidden = options.showHidden ?? false;
|
||||||
app.openFilePreview = vi.fn();
|
app.openFilePreview = vi.fn();
|
||||||
|
app.loadAppSettingsFromStorage = () => ({ showFileBrowser: false });
|
||||||
|
app.getDefaultSettings = () => ({});
|
||||||
|
app.saveAppSettingsToStorage = vi.fn();
|
||||||
|
|
||||||
return { app, elements, pending };
|
return { app, elements, pending };
|
||||||
}
|
}
|
||||||
@@ -856,4 +873,276 @@ describe('File Viewer server search', () => {
|
|||||||
expect(missingTree.pending).toHaveLength(0);
|
expect(missingTree.pending).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('session ownership lifecycle', () => {
|
||||||
|
it('activates the real selectSession owner before any terminal await can fail and retains the idle load', () => {
|
||||||
|
const selectStart = appJs.indexOf('async selectSession(sessionId, options = {})');
|
||||||
|
const activeAssignment = appJs.indexOf('this.activeSessionId = sessionId;', selectStart);
|
||||||
|
const activation = appJs.indexOf('this._activateFileBrowserSession?.(sessionId);', activeAssignment);
|
||||||
|
const firstTerminalAwait = appJs.indexOf('await ', activeAssignment);
|
||||||
|
const retainedIdleLoad = appJs.indexOf('this.loadFileBrowser(sessionId);', firstTerminalAwait);
|
||||||
|
|
||||||
|
expect(selectStart).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(appJs.slice(activeAssignment, activation + 53)).toMatch(
|
||||||
|
/this\.activeSessionId = sessionId;\s*this\._activateFileBrowserSession\?\.\(sessionId\);/
|
||||||
|
);
|
||||||
|
expect(activeAssignment).toBeLessThan(activation);
|
||||||
|
expect(activation).toBeLessThan(firstTerminalAwait);
|
||||||
|
expect(firstTerminalAwait).toBeLessThan(retainedIdleLoad);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('synchronously resets A state, owns B, and starts one visible B tree load', async () => {
|
||||||
|
const { app, elements, pending } = loadPanel();
|
||||||
|
app.fileBrowserData = successfulTree('old-A.ts').data;
|
||||||
|
app.renderFileBrowserTree('session/A');
|
||||||
|
app.filterFileBrowser('old query');
|
||||||
|
app.fileBrowserExpandedDirs.add('src');
|
||||||
|
app.fileBrowserAllExpanded = true;
|
||||||
|
app._fileBrowserState.deferredDirectoryTarget = { ownerSessionId: 'session/A', path: 'src' };
|
||||||
|
const searchEpoch = app._fileBrowserState.searchEpoch;
|
||||||
|
const treeEpoch = app._fileBrowserState.treeEpoch;
|
||||||
|
|
||||||
|
app.activeSessionId = 'session/B';
|
||||||
|
app._activateFileBrowserSession('session/B');
|
||||||
|
|
||||||
|
expect(app._fileBrowserState.ownerSessionId).toBe('session/B');
|
||||||
|
expect(app._fileBrowserState.searchEpoch).toBe(searchEpoch + 1);
|
||||||
|
expect(app._fileBrowserState.treeEpoch).toBe(treeEpoch + 1);
|
||||||
|
expect(app._fileBrowserState.inFlight).toBeNull();
|
||||||
|
expect(app._fileBrowserState.normalState.phase).toBe('loading');
|
||||||
|
expect(app._fileBrowserState.matches).toEqual([]);
|
||||||
|
expect(app._fileBrowserState.deferredDirectoryTarget).toBeNull();
|
||||||
|
expect(app._fileBrowserState.filter).toBe('');
|
||||||
|
expect(app._fileBrowserState.view).toBe('normal');
|
||||||
|
expect(app.fileBrowserData).toBeNull();
|
||||||
|
expect(app.fileBrowserFilter).toBe('');
|
||||||
|
expect(app.fileBrowserExpandedDirs.size).toBe(0);
|
||||||
|
expect(app.fileBrowserAllExpanded).toBe(false);
|
||||||
|
expect(elements.fileBrowserSearch.value).toBe('');
|
||||||
|
expect(elements.fileBrowserExpandBtn.disabled).toBe(false);
|
||||||
|
expect(elements.fileBrowserExpandBtn.innerHTML).toBe('\u229E');
|
||||||
|
expect(elements.fileBrowserTree.querySelectorAll('.file-tree-item')).toHaveLength(0);
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toContain('Loading files');
|
||||||
|
expect(pending.map(({ url }) => url)).toEqual(['/api/sessions/session%2FB/files?depth=5&showHidden=false']);
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(250);
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
const activatedLoad = app._fileBrowserState.treeInFlight.promise;
|
||||||
|
const retainedIdleLoad = app.loadFileBrowser('session/B');
|
||||||
|
expect(retainedIdleLoad).toBe(activatedLoad);
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
|
||||||
|
pending[0].reply.resolve(response(successfulTree('current-B.ts')));
|
||||||
|
await retainedIdleLoad;
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toContain('current-B.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not load while hidden and tolerates missing session or elements', () => {
|
||||||
|
const hidden = loadPanel();
|
||||||
|
hidden.elements.fileBrowserPanel.classList.remove('visible');
|
||||||
|
hidden.app.activeSessionId = 'session/B';
|
||||||
|
expect(() => hidden.app._activateFileBrowserSession('session/B')).not.toThrow();
|
||||||
|
expect(hidden.pending).toHaveLength(0);
|
||||||
|
expect(hidden.app._fileBrowserState.ownerSessionId).toBe('session/B');
|
||||||
|
expect(hidden.app._fileBrowserState.normalState).toBeNull();
|
||||||
|
expect(hidden.elements.fileBrowserTree.innerHTML).toBe('');
|
||||||
|
expect(hidden.elements.fileBrowserStatus.textContent).toBe('');
|
||||||
|
|
||||||
|
const missing = loadPanel();
|
||||||
|
delete missing.elements.fileBrowserTree;
|
||||||
|
delete missing.elements.fileBrowserStatus;
|
||||||
|
delete missing.elements.fileBrowserSearch;
|
||||||
|
delete missing.elements.fileBrowserExpandBtn;
|
||||||
|
expect(() => missing.app._activateFileBrowserSession(null)).not.toThrow();
|
||||||
|
expect(() => missing.app._activateFileBrowserSession('session/B')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('targets B when a query is entered before the activated B tree settles', async () => {
|
||||||
|
const { app, pending } = loadPanel();
|
||||||
|
app.activeSessionId = 'session/B';
|
||||||
|
app._activateFileBrowserSession('session/B');
|
||||||
|
app.filterFileBrowser('during load');
|
||||||
|
await vi.advanceTimersByTimeAsync(250);
|
||||||
|
|
||||||
|
expect(pending.map(({ url }) => url)).toEqual([
|
||||||
|
'/api/sessions/session%2FB/files?depth=5&showHidden=false',
|
||||||
|
'/api/sessions/session%2FB/files?depth=5&showHidden=false&q=during%20load',
|
||||||
|
]);
|
||||||
|
expect(pending.every(({ url }) => !url.includes('session%2FA'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts fresh A work on A to B to A and rejects the original A completion', async () => {
|
||||||
|
const { app, elements, pending } = loadPanel();
|
||||||
|
const firstA = app.loadFileBrowser('session/A');
|
||||||
|
app.activeSessionId = 'session/B';
|
||||||
|
app._activateFileBrowserSession('session/B');
|
||||||
|
const loadB = app._fileBrowserState.treeInFlight.promise;
|
||||||
|
app.activeSessionId = 'session/A';
|
||||||
|
app._activateFileBrowserSession('session/A');
|
||||||
|
const secondA = app._fileBrowserState.treeInFlight.promise;
|
||||||
|
|
||||||
|
expect(secondA).not.toBe(firstA);
|
||||||
|
expect(pending.map(({ url }) => url)).toEqual([
|
||||||
|
'/api/sessions/session%2FA/files?depth=5&showHidden=false',
|
||||||
|
'/api/sessions/session%2FB/files?depth=5&showHidden=false',
|
||||||
|
'/api/sessions/session%2FA/files?depth=5&showHidden=false',
|
||||||
|
]);
|
||||||
|
|
||||||
|
pending[0].reply.resolve(response(successfulTree('stale-A.ts')));
|
||||||
|
await firstA;
|
||||||
|
expect(app._fileBrowserState.treeInFlight.promise).toBe(secondA);
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).not.toContain('stale-A.ts');
|
||||||
|
pending[1].reply.reject(new Error('late B failure'));
|
||||||
|
await loadB;
|
||||||
|
expect(app._fileBrowserState.treeInFlight.promise).toBe(secondA);
|
||||||
|
pending[2].reply.resolve(response(successfulTree('fresh-A.ts')));
|
||||||
|
await secondA;
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toContain('fresh-A.ts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hide lifecycle', () => {
|
||||||
|
it('the real settings hide branch resets once and does not churn epochs while already hidden', () => {
|
||||||
|
const { app, elements } = loadPanel();
|
||||||
|
const state = app._ensureFileBrowserState();
|
||||||
|
const searchEpoch = state.searchEpoch;
|
||||||
|
const treeEpoch = state.treeEpoch;
|
||||||
|
|
||||||
|
app.applyMonitorVisibility();
|
||||||
|
|
||||||
|
expect(elements.fileBrowserPanel.classList.contains('visible')).toBe(false);
|
||||||
|
expect(state.searchEpoch).toBe(searchEpoch + 1);
|
||||||
|
expect(state.treeEpoch).toBe(treeEpoch + 1);
|
||||||
|
app.applyMonitorVisibility();
|
||||||
|
expect(state.searchEpoch).toBe(searchEpoch + 1);
|
||||||
|
expect(state.treeEpoch).toBe(treeEpoch + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels an unlaunched debounce and clears all hide-reset controls and navigation state', async () => {
|
||||||
|
const { app, elements, pending } = loadPanel();
|
||||||
|
app.filterFileBrowser('not launched');
|
||||||
|
app.fileBrowserExpandedDirs.add('src');
|
||||||
|
app.fileBrowserAllExpanded = true;
|
||||||
|
app._fileBrowserState.matches = [{ name: 'old.ts' }];
|
||||||
|
app._fileBrowserState.deferredDirectoryTarget = { ownerSessionId: 'session/A', path: 'src' };
|
||||||
|
elements.fileBrowserSearch.value = 'not launched';
|
||||||
|
elements.fileBrowserExpandBtn.disabled = true;
|
||||||
|
elements.fileBrowserExpandBtn.innerHTML = '\u229F';
|
||||||
|
|
||||||
|
app.closeFileBrowserPanel();
|
||||||
|
await vi.advanceTimersByTimeAsync(250);
|
||||||
|
|
||||||
|
expect(pending).toHaveLength(0);
|
||||||
|
expect(app._fileBrowserState.view).toBe('normal');
|
||||||
|
expect(app._fileBrowserState.filter).toBe('');
|
||||||
|
expect(app.fileBrowserFilter).toBe('');
|
||||||
|
expect(app.fileBrowserExpandedDirs.size).toBe(0);
|
||||||
|
expect(app.fileBrowserAllExpanded).toBe(false);
|
||||||
|
expect(app._fileBrowserState.matches).toEqual([]);
|
||||||
|
expect(app._fileBrowserState.deferredDirectoryTarget).toBeNull();
|
||||||
|
expect(elements.fileBrowserSearch.value).toBe('');
|
||||||
|
expect(elements.fileBrowserExpandBtn.disabled).toBe(false);
|
||||||
|
expect(elements.fileBrowserExpandBtn.innerHTML).toBe('\u229E');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['tree success', 'tree', false],
|
||||||
|
['tree failure', 'tree', true],
|
||||||
|
['search success', 'search', false],
|
||||||
|
['search failure', 'search', true],
|
||||||
|
])('explicit close blocks pending %s from updating state or UI', async (_name, kind, fail) => {
|
||||||
|
const { app, elements, pending } = loadPanel();
|
||||||
|
const operation = kind === 'tree' ? app.loadFileBrowser('session/A') : (await startSearch(app), undefined);
|
||||||
|
|
||||||
|
app.closeFileBrowserPanel();
|
||||||
|
expect(app._fileBrowserState.treeInFlight).toBeNull();
|
||||||
|
expect(app._fileBrowserState.inFlight).toBeNull();
|
||||||
|
expect(app._fileBrowserState.normalState).toBeNull();
|
||||||
|
expect(app._fileBrowserState.matches).toEqual([]);
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toBe('');
|
||||||
|
expect(elements.fileBrowserStatus.textContent).toBe('');
|
||||||
|
|
||||||
|
if (fail) pending[0].reply.reject(new Error(`late ${kind} failure`));
|
||||||
|
else
|
||||||
|
pending[0].reply.resolve(
|
||||||
|
response(
|
||||||
|
kind === 'tree'
|
||||||
|
? successfulTree('late.ts')
|
||||||
|
: successfulData({
|
||||||
|
matches: [{ name: 'late.ts', path: 'late.ts', type: 'file' }],
|
||||||
|
matchCount: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (operation) await operation;
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
expect(app.fileBrowserData).toBeNull();
|
||||||
|
expect(app._fileBrowserState.normalState).toBeNull();
|
||||||
|
expect(app._fileBrowserState.matches).toEqual([]);
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toBe('');
|
||||||
|
expect(elements.fileBrowserStatus.textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['tree success', 'tree', false],
|
||||||
|
['tree failure', 'tree', true],
|
||||||
|
['search success', 'search', false],
|
||||||
|
['search failure', 'search', true],
|
||||||
|
])('settings-driven hide blocks pending %s from updating state or UI', async (_name, kind, fail) => {
|
||||||
|
const { app, elements, pending } = loadPanel();
|
||||||
|
const operation = kind === 'tree' ? app.loadFileBrowser('session/A') : (await startSearch(app), undefined);
|
||||||
|
|
||||||
|
app.applyMonitorVisibility();
|
||||||
|
expect(elements.fileBrowserPanel.classList.contains('visible')).toBe(false);
|
||||||
|
if (fail) pending[0].reply.reject(new Error(`late ${kind} failure`));
|
||||||
|
else
|
||||||
|
pending[0].reply.resolve(
|
||||||
|
response(
|
||||||
|
kind === 'tree'
|
||||||
|
? successfulTree('late.ts')
|
||||||
|
: successfulData({
|
||||||
|
matches: [{ name: 'late.ts', path: 'late.ts', type: 'file' }],
|
||||||
|
matchCount: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (operation) await operation;
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
expect(app.fileBrowserData).toBeNull();
|
||||||
|
expect(app._fileBrowserState.normalState).toBeNull();
|
||||||
|
expect(app._fileBrowserState.matches).toEqual([]);
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toBe('');
|
||||||
|
expect(elements.fileBrowserStatus.textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['stale success', false],
|
||||||
|
['stale failure', true],
|
||||||
|
])('reopen after %s starts a replacement and stale settlement cannot clear it', async (_name, fail) => {
|
||||||
|
const { app, elements, pending } = loadPanel();
|
||||||
|
const stale = app.loadFileBrowser('session/A');
|
||||||
|
app.closeFileBrowserPanel();
|
||||||
|
elements.fileBrowserPanel.classList.add('visible');
|
||||||
|
const replacement = app.loadFileBrowser('session/A');
|
||||||
|
const replacementRecord = app._fileBrowserState.treeInFlight;
|
||||||
|
|
||||||
|
expect(replacement).not.toBe(stale);
|
||||||
|
expect(pending).toHaveLength(2);
|
||||||
|
if (fail) pending[0].reply.reject(new Error('stale failure'));
|
||||||
|
else pending[0].reply.resolve(response(successfulTree('stale.ts')));
|
||||||
|
await stale;
|
||||||
|
expect(app._fileBrowserState.treeInFlight).toBe(replacementRecord);
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toContain('Loading files');
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).not.toContain('stale.ts');
|
||||||
|
|
||||||
|
const reused = app.loadFileBrowser('session/A');
|
||||||
|
expect(reused).toBe(replacement);
|
||||||
|
expect(pending).toHaveLength(2);
|
||||||
|
pending[1].reply.resolve(response(successfulTree('replacement.ts')));
|
||||||
|
await replacement;
|
||||||
|
expect(elements.fileBrowserTree.innerHTML).toContain('replacement.ts');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user