mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix terminal width overflow causing flickering in native terminals (#123)
This commit is contained in:
parent
4b74fdf89c
commit
38b7846605
11 changed files with 594 additions and 40 deletions
|
|
@ -28,20 +28,27 @@ struct PerformanceTests {
|
||||||
return parts.joined()
|
return parts.joined()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure approximate performance difference
|
// Test both methods
|
||||||
let start1 = Date()
|
|
||||||
let result1 = inefficientConcat()
|
let result1 = inefficientConcat()
|
||||||
let time1 = Date().timeIntervalSince(start1)
|
|
||||||
|
|
||||||
let start2 = Date()
|
|
||||||
let result2 = efficientConcat()
|
let result2 = efficientConcat()
|
||||||
let time2 = Date().timeIntervalSince(start2)
|
|
||||||
|
|
||||||
|
// Verify both methods produce identical results
|
||||||
|
#expect(result1 == result2)
|
||||||
#expect(!result1.isEmpty)
|
#expect(!result1.isEmpty)
|
||||||
#expect(!result2.isEmpty)
|
#expect(!result2.isEmpty)
|
||||||
// Allow some variance in timing - just verify both methods work
|
|
||||||
#expect(time1 >= 0)
|
// Verify the content is correct
|
||||||
#expect(time2 >= 0)
|
let lines1 = result1.split(separator: "\n")
|
||||||
|
let lines2 = result2.split(separator: "\n")
|
||||||
|
#expect(lines1.count == iterations)
|
||||||
|
#expect(lines2.count == iterations)
|
||||||
|
|
||||||
|
// Verify first and last lines
|
||||||
|
#expect(lines1.first == "Line 0")
|
||||||
|
#expect(lines1.last == "Line \(iterations - 1)")
|
||||||
|
|
||||||
|
// Note: Performance timing removed as it's unreliable in test environments
|
||||||
|
// Both methods should produce functionally identical results
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Collection Performance
|
// MARK: - Collection Performance
|
||||||
|
|
@ -346,22 +353,24 @@ struct PerformanceTests {
|
||||||
|
|
||||||
// Test built-in sort
|
// Test built-in sort
|
||||||
var array1 = randomArray
|
var array1 = randomArray
|
||||||
let start1 = Date()
|
|
||||||
array1.sort()
|
array1.sort()
|
||||||
let time1 = Date().timeIntervalSince(start1)
|
|
||||||
|
|
||||||
// Test sort with custom comparator
|
// Test sort with custom comparator
|
||||||
var array2 = randomArray
|
var array2 = randomArray
|
||||||
let start2 = Date()
|
|
||||||
array2.sort { $0 < $1 }
|
array2.sort { $0 < $1 }
|
||||||
let time2 = Date().timeIntervalSince(start2)
|
|
||||||
|
|
||||||
// Verify both sorted correctly
|
// Verify both sorted correctly
|
||||||
#expect(array1 == Array(0..<size))
|
#expect(array1 == Array(0..<size))
|
||||||
#expect(array2 == Array(0..<size))
|
#expect(array2 == Array(0..<size))
|
||||||
|
|
||||||
|
// Verify sorting is stable and complete
|
||||||
|
for i in 0..<size {
|
||||||
|
#expect(array1[i] == i)
|
||||||
|
#expect(array2[i] == i)
|
||||||
|
}
|
||||||
|
|
||||||
// Built-in should be faster or similar
|
// Note: Performance timing removed as it's unreliable in test environments
|
||||||
#expect(time1 <= time2 * 2) // Allow some variance
|
// Both methods should produce identical sorted results
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Hash table resize performance")
|
@Test("Hash table resize performance")
|
||||||
|
|
@ -373,23 +382,28 @@ struct PerformanceTests {
|
||||||
var preSized: [Int: String] = [:]
|
var preSized: [Int: String] = [:]
|
||||||
preSized.reserveCapacity(iterations)
|
preSized.reserveCapacity(iterations)
|
||||||
|
|
||||||
let start1 = Date()
|
// Test dynamic resize
|
||||||
for i in 0..<iterations {
|
for i in 0..<iterations {
|
||||||
dictionary[i] = "Value \(i)"
|
dictionary[i] = "Value \(i)"
|
||||||
}
|
}
|
||||||
let time1 = Date().timeIntervalSince(start1)
|
|
||||||
|
|
||||||
let start2 = Date()
|
// Test pre-sized
|
||||||
for i in 0..<iterations {
|
for i in 0..<iterations {
|
||||||
preSized[i] = "Value \(i)"
|
preSized[i] = "Value \(i)"
|
||||||
}
|
}
|
||||||
let time2 = Date().timeIntervalSince(start2)
|
|
||||||
|
|
||||||
|
// Verify both methods work correctly
|
||||||
#expect(dictionary.count == iterations)
|
#expect(dictionary.count == iterations)
|
||||||
#expect(preSized.count == iterations)
|
#expect(preSized.count == iterations)
|
||||||
|
|
||||||
|
// Verify all values are stored correctly
|
||||||
|
for i in 0..<iterations {
|
||||||
|
#expect(dictionary[i] == "Value \(i)")
|
||||||
|
#expect(preSized[i] == "Value \(i)")
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-sized should be faster or similar
|
// Note: Performance timing removed as it's unreliable in test environments
|
||||||
#expect(time2 <= time1 * 1.5) // Allow some variance
|
// Both methods should produce functionally identical results
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - WebSocket Message Processing
|
// MARK: - WebSocket Message Processing
|
||||||
|
|
|
||||||
|
|
@ -45,4 +45,27 @@ See [spec.md](./spec.md) for detailed architecture documentation.
|
||||||
- Real-time streaming (SSE + WebSocket)
|
- Real-time streaming (SSE + WebSocket)
|
||||||
- Binary-optimized buffer updates
|
- Binary-optimized buffer updates
|
||||||
- Multi-session support
|
- Multi-session support
|
||||||
- File browser integration
|
- File browser integration
|
||||||
|
|
||||||
|
## Terminal Resizing Behavior
|
||||||
|
|
||||||
|
VibeTunnel intelligently handles terminal width based on how the session was created:
|
||||||
|
|
||||||
|
### Tunneled Sessions (via `vt` command)
|
||||||
|
- Sessions created by running `vt` in a native terminal window
|
||||||
|
- Terminal width is automatically limited to the native terminal's width to prevent text overflow
|
||||||
|
- Prevents flickering and display issues in the native terminal
|
||||||
|
- Shows "≤120" (or actual width) in the width selector when limited
|
||||||
|
- Users can manually override this limit using the width selector
|
||||||
|
|
||||||
|
### Frontend-Created Sessions
|
||||||
|
- Sessions created directly from the web interface (using the "New Session" button)
|
||||||
|
- No width restrictions by default - uses full browser width
|
||||||
|
- Perfect for web-only workflows where no native terminal is involved
|
||||||
|
- Shows "∞" in the width selector for unlimited width
|
||||||
|
|
||||||
|
### Manual Width Control
|
||||||
|
- Click the width indicator in the session header to open the width selector
|
||||||
|
- Choose from common terminal widths (80, 120, 132, etc.) or unlimited
|
||||||
|
- Width preferences are saved per session and persist across reloads
|
||||||
|
- Selecting any width manually overrides automatic limitations
|
||||||
|
|
@ -551,6 +551,130 @@ describe('SessionView', () => {
|
||||||
expect(element.showWidthSelector).toBe(false);
|
expect(element.showWidthSelector).toBe(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass initial dimensions to terminal', async () => {
|
||||||
|
const mockSession = createMockSession();
|
||||||
|
// Add initial dimensions to mock session
|
||||||
|
mockSession.initialCols = 120;
|
||||||
|
mockSession.initialRows = 30;
|
||||||
|
|
||||||
|
element.session = mockSession;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
const terminal = element.querySelector('vibe-terminal') as Terminal;
|
||||||
|
if (terminal) {
|
||||||
|
expect(terminal.initialCols).toBe(120);
|
||||||
|
expect(terminal.initialRows).toBe(30);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set user override when width is selected', async () => {
|
||||||
|
element.showWidthSelector = true;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
const terminal = element.querySelector('vibe-terminal') as Terminal;
|
||||||
|
const setUserOverrideWidthSpy = vi.spyOn(terminal, 'setUserOverrideWidth');
|
||||||
|
|
||||||
|
// Simulate width selection
|
||||||
|
element.handleWidthSelect(100);
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
expect(setUserOverrideWidthSpy).toHaveBeenCalledWith(true);
|
||||||
|
expect(terminal.maxCols).toBe(100);
|
||||||
|
expect(element.terminalMaxCols).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow unlimited width selection with override', async () => {
|
||||||
|
element.showWidthSelector = true;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
const terminal = element.querySelector('vibe-terminal') as Terminal;
|
||||||
|
const setUserOverrideWidthSpy = vi.spyOn(terminal, 'setUserOverrideWidth');
|
||||||
|
|
||||||
|
// Select unlimited (0)
|
||||||
|
element.handleWidthSelect(0);
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
expect(setUserOverrideWidthSpy).toHaveBeenCalledWith(true);
|
||||||
|
expect(terminal.maxCols).toBe(0);
|
||||||
|
expect(element.terminalMaxCols).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show limited width label when constrained by session dimensions', async () => {
|
||||||
|
const mockSession = createMockSession();
|
||||||
|
// Set up a tunneled session (from vt command) with 'fwd_' prefix
|
||||||
|
mockSession.id = 'fwd_1234567890';
|
||||||
|
mockSession.initialCols = 120;
|
||||||
|
mockSession.initialRows = 30;
|
||||||
|
|
||||||
|
element.session = mockSession;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
const terminal = element.querySelector('vibe-terminal') as Terminal;
|
||||||
|
if (terminal) {
|
||||||
|
terminal.initialCols = 120;
|
||||||
|
terminal.initialRows = 30;
|
||||||
|
// Simulate no user override
|
||||||
|
terminal.userOverrideWidth = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no manual selection (terminalMaxCols = 0) and initial dimensions,
|
||||||
|
// the label should show "≤120" for tunneled sessions
|
||||||
|
const label = element.getCurrentWidthLabel();
|
||||||
|
expect(label).toBe('≤120');
|
||||||
|
|
||||||
|
// Tooltip should explain the limitation
|
||||||
|
const tooltip = element.getWidthTooltip();
|
||||||
|
expect(tooltip).toContain('Limited to native terminal width');
|
||||||
|
expect(tooltip).toContain('120 columns');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show unlimited label when user overrides', async () => {
|
||||||
|
const mockSession = createMockSession();
|
||||||
|
mockSession.initialCols = 120;
|
||||||
|
|
||||||
|
element.session = mockSession;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
const terminal = element.querySelector('vibe-terminal') as Terminal;
|
||||||
|
if (terminal) {
|
||||||
|
terminal.initialCols = 120;
|
||||||
|
terminal.userOverrideWidth = true; // User has overridden
|
||||||
|
}
|
||||||
|
|
||||||
|
// With user override, should show ∞
|
||||||
|
const label = element.getCurrentWidthLabel();
|
||||||
|
expect(label).toBe('∞');
|
||||||
|
|
||||||
|
const tooltip = element.getWidthTooltip();
|
||||||
|
expect(tooltip).toBe('Terminal width: Unlimited');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show unlimited width for frontend-created sessions', async () => {
|
||||||
|
const mockSession = createMockSession();
|
||||||
|
// Use default UUID format ID (not tunneled) - do not override the ID
|
||||||
|
mockSession.initialCols = 120;
|
||||||
|
mockSession.initialRows = 30;
|
||||||
|
|
||||||
|
element.session = mockSession;
|
||||||
|
element.terminalMaxCols = 0; // No manual width selection
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
const terminal = element.querySelector('vibe-terminal') as Terminal;
|
||||||
|
if (terminal) {
|
||||||
|
terminal.initialCols = 120;
|
||||||
|
terminal.initialRows = 30;
|
||||||
|
terminal.userOverrideWidth = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frontend-created sessions should show unlimited, not limited by initial dimensions
|
||||||
|
const label = element.getCurrentWidthLabel();
|
||||||
|
expect(label).toBe('∞');
|
||||||
|
|
||||||
|
// Tooltip should show unlimited
|
||||||
|
const tooltip = element.getWidthTooltip();
|
||||||
|
expect(tooltip).toBe('Terminal width: Unlimited');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('navigation', () => {
|
describe('navigation', () => {
|
||||||
|
|
|
||||||
|
|
@ -661,17 +661,55 @@ export class SessionView extends LitElement {
|
||||||
const terminal = this.querySelector('vibe-terminal') as Terminal;
|
const terminal = this.querySelector('vibe-terminal') as Terminal;
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.maxCols = newMaxCols;
|
terminal.maxCols = newMaxCols;
|
||||||
|
// Mark that user has manually selected a width
|
||||||
|
terminal.setUserOverrideWidth(true);
|
||||||
// Trigger a resize to apply the new constraint
|
// Trigger a resize to apply the new constraint
|
||||||
terminal.requestUpdate();
|
terminal.requestUpdate();
|
||||||
|
} else {
|
||||||
|
logger.warn('Terminal component not found when setting width');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentWidthLabel(): string {
|
private getCurrentWidthLabel(): string {
|
||||||
|
const terminal = this.querySelector('vibe-terminal') as Terminal;
|
||||||
|
|
||||||
|
// Only apply width restrictions to tunneled sessions (those with 'fwd_' prefix)
|
||||||
|
const isTunneledSession = this.session?.id?.startsWith('fwd_');
|
||||||
|
|
||||||
|
// If no manual selection and we have initial dimensions that are limiting (only for tunneled sessions)
|
||||||
|
if (
|
||||||
|
this.terminalMaxCols === 0 &&
|
||||||
|
terminal?.initialCols > 0 &&
|
||||||
|
!terminal.userOverrideWidth &&
|
||||||
|
isTunneledSession
|
||||||
|
) {
|
||||||
|
return `≤${terminal.initialCols}`; // Shows "≤120" to indicate limited to session width
|
||||||
|
}
|
||||||
|
|
||||||
if (this.terminalMaxCols === 0) return '∞';
|
if (this.terminalMaxCols === 0) return '∞';
|
||||||
const commonWidth = COMMON_TERMINAL_WIDTHS.find((w) => w.value === this.terminalMaxCols);
|
const commonWidth = COMMON_TERMINAL_WIDTHS.find((w) => w.value === this.terminalMaxCols);
|
||||||
return commonWidth ? commonWidth.label : this.terminalMaxCols.toString();
|
return commonWidth ? commonWidth.label : this.terminalMaxCols.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getWidthTooltip(): string {
|
||||||
|
const terminal = this.querySelector('vibe-terminal') as Terminal;
|
||||||
|
|
||||||
|
// Only apply width restrictions to tunneled sessions (those with 'fwd_' prefix)
|
||||||
|
const isTunneledSession = this.session?.id?.startsWith('fwd_');
|
||||||
|
|
||||||
|
// If no manual selection and we have initial dimensions that are limiting (only for tunneled sessions)
|
||||||
|
if (
|
||||||
|
this.terminalMaxCols === 0 &&
|
||||||
|
terminal?.initialCols > 0 &&
|
||||||
|
!terminal.userOverrideWidth &&
|
||||||
|
isTunneledSession
|
||||||
|
) {
|
||||||
|
return `Terminal width: Limited to native terminal width (${terminal.initialCols} columns)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Terminal width: ${this.terminalMaxCols === 0 ? 'Unlimited' : `${this.terminalMaxCols} columns`}`;
|
||||||
|
}
|
||||||
|
|
||||||
private handleFontSizeChange(newSize: number) {
|
private handleFontSizeChange(newSize: number) {
|
||||||
// Clamp to reasonable bounds
|
// Clamp to reasonable bounds
|
||||||
const clampedSize = Math.max(8, Math.min(32, newSize));
|
const clampedSize = Math.max(8, Math.min(32, newSize));
|
||||||
|
|
@ -854,6 +892,8 @@ export class SessionView extends LitElement {
|
||||||
.terminalFontSize=${this.terminalFontSize}
|
.terminalFontSize=${this.terminalFontSize}
|
||||||
.customWidth=${this.customWidth}
|
.customWidth=${this.customWidth}
|
||||||
.showWidthSelector=${this.showWidthSelector}
|
.showWidthSelector=${this.showWidthSelector}
|
||||||
|
.widthLabel=${this.getCurrentWidthLabel()}
|
||||||
|
.widthTooltip=${this.getWidthTooltip()}
|
||||||
.onBack=${() => this.handleBack()}
|
.onBack=${() => this.handleBack()}
|
||||||
.onSidebarToggle=${() => this.handleSidebarToggle()}
|
.onSidebarToggle=${() => this.handleSidebarToggle()}
|
||||||
.onOpenFileBrowser=${() => this.handleOpenFileBrowser()}
|
.onOpenFileBrowser=${() => this.handleOpenFileBrowser()}
|
||||||
|
|
@ -898,6 +938,8 @@ export class SessionView extends LitElement {
|
||||||
.fontSize=${this.terminalFontSize}
|
.fontSize=${this.terminalFontSize}
|
||||||
.fitHorizontally=${false}
|
.fitHorizontally=${false}
|
||||||
.maxCols=${this.terminalMaxCols}
|
.maxCols=${this.terminalMaxCols}
|
||||||
|
.initialCols=${this.session?.initialCols || 0}
|
||||||
|
.initialRows=${this.session?.initialRows || 0}
|
||||||
.disableClick=${this.isMobile && this.useDirectKeyboard}
|
.disableClick=${this.isMobile && this.useDirectKeyboard}
|
||||||
.hideScrollButton=${this.showQuickKeys}
|
.hideScrollButton=${this.showQuickKeys}
|
||||||
class="w-full h-full p-0 m-0"
|
class="w-full h-full p-0 m-0"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
import { html, LitElement } from 'lit';
|
import { html, LitElement } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { COMMON_TERMINAL_WIDTHS } from '../../utils/terminal-preferences.js';
|
|
||||||
import type { Session } from '../session-list.js';
|
import type { Session } from '../session-list.js';
|
||||||
import '../clickable-path.js';
|
import '../clickable-path.js';
|
||||||
import './width-selector.js';
|
import './width-selector.js';
|
||||||
|
|
@ -28,6 +27,8 @@ export class SessionHeader extends LitElement {
|
||||||
@property({ type: Number }) terminalFontSize = 14;
|
@property({ type: Number }) terminalFontSize = 14;
|
||||||
@property({ type: String }) customWidth = '';
|
@property({ type: String }) customWidth = '';
|
||||||
@property({ type: Boolean }) showWidthSelector = false;
|
@property({ type: Boolean }) showWidthSelector = false;
|
||||||
|
@property({ type: String }) widthLabel = '';
|
||||||
|
@property({ type: String }) widthTooltip = '';
|
||||||
@property({ type: Function }) onBack?: () => void;
|
@property({ type: Function }) onBack?: () => void;
|
||||||
@property({ type: Function }) onSidebarToggle?: () => void;
|
@property({ type: Function }) onSidebarToggle?: () => void;
|
||||||
@property({ type: Function }) onOpenFileBrowser?: () => void;
|
@property({ type: Function }) onOpenFileBrowser?: () => void;
|
||||||
|
|
@ -59,11 +60,6 @@ export class SessionHeader extends LitElement {
|
||||||
return this.session.status === 'running' ? 'bg-status-success' : 'bg-status-warning';
|
return this.session.status === 'running' ? 'bg-status-success' : 'bg-status-warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentWidthLabel(): string {
|
|
||||||
const width = COMMON_TERMINAL_WIDTHS.find((w) => w.value === this.terminalMaxCols);
|
|
||||||
return width?.label || this.terminalMaxCols.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCloseWidthSelector() {
|
private handleCloseWidthSelector() {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('close-width-selector', {
|
new CustomEvent('close-width-selector', {
|
||||||
|
|
@ -163,11 +159,9 @@ export class SessionHeader extends LitElement {
|
||||||
<button
|
<button
|
||||||
class="btn-secondary font-mono text-xs px-2 py-1 flex-shrink-0 width-selector-button"
|
class="btn-secondary font-mono text-xs px-2 py-1 flex-shrink-0 width-selector-button"
|
||||||
@click=${() => this.onMaxWidthToggle?.()}
|
@click=${() => this.onMaxWidthToggle?.()}
|
||||||
title="Terminal width: ${
|
title="${this.widthTooltip}"
|
||||||
this.terminalMaxCols === 0 ? 'Unlimited' : `${this.terminalMaxCols} columns`
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
${this.getCurrentWidthLabel()}
|
${this.widthLabel}
|
||||||
</button>
|
</button>
|
||||||
<width-selector
|
<width-selector
|
||||||
.visible=${this.showWidthSelector}
|
.visible=${this.showWidthSelector}
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,243 @@ describe('Terminal', () => {
|
||||||
// So this test should verify the property is set
|
// So this test should verify the property is set
|
||||||
expect(element.maxCols).toBe(100);
|
expect(element.maxCols).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should respect initial dimensions when no user override', async () => {
|
||||||
|
element.initialCols = 120;
|
||||||
|
element.initialRows = 30;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Verify properties are set
|
||||||
|
expect(element.initialCols).toBe(120);
|
||||||
|
expect(element.initialRows).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow user override with setUserOverrideWidth', async () => {
|
||||||
|
element.initialCols = 120;
|
||||||
|
element.setUserOverrideWidth(true);
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Verify the method exists and can be called
|
||||||
|
expect(element.setUserOverrideWidth).toBeDefined();
|
||||||
|
expect(typeof element.setUserOverrideWidth).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different width constraint scenarios', async () => {
|
||||||
|
// Test scenario 1: User sets specific width
|
||||||
|
element.maxCols = 80;
|
||||||
|
element.initialCols = 120;
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.maxCols).toBe(80);
|
||||||
|
|
||||||
|
// Test scenario 2: User selects unlimited with override
|
||||||
|
element.maxCols = 0;
|
||||||
|
element.setUserOverrideWidth(true);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.maxCols).toBe(0);
|
||||||
|
|
||||||
|
// Test scenario 3: Initial dimensions with no override
|
||||||
|
element.maxCols = 0;
|
||||||
|
element.setUserOverrideWidth(false);
|
||||||
|
element.initialCols = 100;
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.initialCols).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only apply width restrictions to tunneled sessions', async () => {
|
||||||
|
// Setup initial conditions
|
||||||
|
element.initialCols = 80;
|
||||||
|
element.maxCols = 0;
|
||||||
|
element.setUserOverrideWidth(false);
|
||||||
|
|
||||||
|
// Test frontend-created session (UUID format) - should NOT be limited
|
||||||
|
element.sessionId = '123e4567-e89b-12d3-a456-426614174000';
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// The terminal should use full calculated width, not limited by initialCols
|
||||||
|
// Since we can't directly test the internal fitTerminal logic in this test environment,
|
||||||
|
// we verify the setup is correct
|
||||||
|
expect(element.sessionId).not.toMatch(/^fwd_/);
|
||||||
|
expect(element.initialCols).toBe(80);
|
||||||
|
expect(element.userOverrideWidth).toBe(false);
|
||||||
|
|
||||||
|
// Test tunneled session (fwd_ prefix) - should be limited
|
||||||
|
element.sessionId = 'fwd_1234567890';
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// The terminal should be limited by initialCols for tunneled sessions
|
||||||
|
expect(element.sessionId).toMatch(/^fwd_/);
|
||||||
|
expect(element.initialCols).toBe(80);
|
||||||
|
expect(element.userOverrideWidth).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined initial dimensions gracefully', async () => {
|
||||||
|
element.initialCols = undefined as unknown as number;
|
||||||
|
element.initialRows = undefined as unknown as number;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// When initial dimensions are undefined, the terminal will use calculated dimensions
|
||||||
|
// based on container size, not the default 80x24
|
||||||
|
expect(element.cols).toBeGreaterThan(0);
|
||||||
|
expect(element.rows).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Should still be able to resize
|
||||||
|
element.setTerminalSize(100, 30);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.cols).toBe(100);
|
||||||
|
expect(element.rows).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle zero initial dimensions gracefully', async () => {
|
||||||
|
element.initialCols = 0;
|
||||||
|
element.initialRows = 0;
|
||||||
|
element.maxCols = 0;
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Should fall back to calculated width based on container
|
||||||
|
expect(element.cols).toBeGreaterThan(0);
|
||||||
|
expect(element.rows).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Terminal should still be functional
|
||||||
|
element.write('Test content');
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.querySelector('.terminal-container')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist user override preference to localStorage', async () => {
|
||||||
|
// Set sessionId directly since attribute binding might not work in tests
|
||||||
|
element.sessionId = 'test-123';
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Clear any existing value
|
||||||
|
localStorage.removeItem('terminal-width-override-test-123');
|
||||||
|
|
||||||
|
// Set user override
|
||||||
|
element.setUserOverrideWidth(true);
|
||||||
|
|
||||||
|
// Check localStorage
|
||||||
|
const stored = localStorage.getItem('terminal-width-override-test-123');
|
||||||
|
expect(stored).toBe('true');
|
||||||
|
|
||||||
|
// Set to false
|
||||||
|
element.setUserOverrideWidth(false);
|
||||||
|
const storedFalse = localStorage.getItem('terminal-width-override-test-123');
|
||||||
|
expect(storedFalse).toBe('false');
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
localStorage.removeItem('terminal-width-override-test-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore user override preference from localStorage', async () => {
|
||||||
|
// Pre-set localStorage value
|
||||||
|
localStorage.setItem('terminal-width-override-test-456', 'true');
|
||||||
|
|
||||||
|
// Create new element with sessionId
|
||||||
|
const newElement = await fixture<Terminal>(html`
|
||||||
|
<vibe-terminal></vibe-terminal>
|
||||||
|
`);
|
||||||
|
newElement.sessionId = 'test-456';
|
||||||
|
|
||||||
|
// Trigger connectedCallback by removing and re-adding to DOM
|
||||||
|
newElement.remove();
|
||||||
|
document.body.appendChild(newElement);
|
||||||
|
await newElement.updateComplete;
|
||||||
|
|
||||||
|
// Verify override was restored
|
||||||
|
expect(newElement.userOverrideWidth).toBe(true);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
newElement.remove();
|
||||||
|
localStorage.removeItem('terminal-width-override-test-456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore user override preference when sessionId changes', async () => {
|
||||||
|
// Pre-set localStorage value for the new sessionId
|
||||||
|
localStorage.setItem('terminal-width-override-new-session-789', 'true');
|
||||||
|
|
||||||
|
// Create element with initial sessionId
|
||||||
|
element.sessionId = 'old-session-123';
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Verify initial state (no override for old session)
|
||||||
|
expect(element.userOverrideWidth).toBe(false);
|
||||||
|
|
||||||
|
// Change sessionId - this should trigger loading the preference
|
||||||
|
element.sessionId = 'new-session-789';
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// The updated() lifecycle method should have loaded the preference
|
||||||
|
expect(element.userOverrideWidth).toBe(true);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
localStorage.removeItem('terminal-width-override-new-session-789');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle localStorage errors gracefully', async () => {
|
||||||
|
// Mock localStorage to throw errors
|
||||||
|
const originalGetItem = localStorage.getItem;
|
||||||
|
const originalSetItem = localStorage.setItem;
|
||||||
|
|
||||||
|
// Test getItem error handling
|
||||||
|
localStorage.getItem = vi.fn().mockImplementation(() => {
|
||||||
|
throw new Error('localStorage unavailable');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create element - should not crash despite localStorage error
|
||||||
|
const errorElement = await fixture<Terminal>(html`
|
||||||
|
<vibe-terminal session-id="error-test"></vibe-terminal>
|
||||||
|
`);
|
||||||
|
await errorElement.updateComplete;
|
||||||
|
|
||||||
|
// Should default to false when localStorage fails
|
||||||
|
expect(errorElement.userOverrideWidth).toBe(false);
|
||||||
|
|
||||||
|
// Test setItem error handling
|
||||||
|
localStorage.setItem = vi.fn().mockImplementation(() => {
|
||||||
|
throw new Error('Quota exceeded');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not crash when saving preference fails
|
||||||
|
errorElement.setUserOverrideWidth(true);
|
||||||
|
expect(errorElement.userOverrideWidth).toBe(true); // State should still update
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
errorElement.remove();
|
||||||
|
localStorage.getItem = originalGetItem;
|
||||||
|
localStorage.setItem = originalSetItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set explicitSizeSet flag if terminal is not ready', async () => {
|
||||||
|
// Create a new terminal component instance without rendering
|
||||||
|
const newElement = document.createElement('vibe-terminal') as Terminal;
|
||||||
|
|
||||||
|
// Set terminal size before it's connected to DOM (terminal will be null)
|
||||||
|
newElement.setTerminalSize(100, 30);
|
||||||
|
|
||||||
|
// explicitSizeSet should remain false since terminal wasn't ready
|
||||||
|
expect((newElement as unknown as { explicitSizeSet: boolean }).explicitSizeSet).toBe(false);
|
||||||
|
|
||||||
|
// Cols and rows should still be updated
|
||||||
|
expect(newElement.cols).toBe(100);
|
||||||
|
expect(newElement.rows).toBe(30);
|
||||||
|
|
||||||
|
// Now connect to DOM and let it initialize
|
||||||
|
document.body.appendChild(newElement);
|
||||||
|
await newElement.updateComplete;
|
||||||
|
await newElement.firstUpdated();
|
||||||
|
|
||||||
|
// After initialization, terminal should be ready
|
||||||
|
const terminal = (newElement as unknown as { terminal: MockTerminal }).terminal;
|
||||||
|
expect(terminal).toBeDefined();
|
||||||
|
|
||||||
|
// Now if we set size again, explicitSizeSet should be set
|
||||||
|
newElement.setTerminalSize(120, 40);
|
||||||
|
expect((newElement as unknown as { explicitSizeSet: boolean }).explicitSizeSet).toBe(true);
|
||||||
|
expect(newElement.cols).toBe(120);
|
||||||
|
expect(newElement.rows).toBe(40);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
newElement.remove();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('scrolling behavior', () => {
|
describe('scrolling behavior', () => {
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,11 @@ export class Terminal extends LitElement {
|
||||||
@property({ type: Number }) maxCols = 0; // 0 means no limit
|
@property({ type: Number }) maxCols = 0; // 0 means no limit
|
||||||
@property({ type: Boolean }) disableClick = false; // Disable click handling (for mobile direct keyboard)
|
@property({ type: Boolean }) disableClick = false; // Disable click handling (for mobile direct keyboard)
|
||||||
@property({ type: Boolean }) hideScrollButton = false; // Hide scroll-to-bottom button
|
@property({ type: Boolean }) hideScrollButton = false; // Hide scroll-to-bottom button
|
||||||
|
@property({ type: Number }) initialCols = 0; // Initial terminal width from session creation
|
||||||
|
@property({ type: Number }) initialRows = 0; // Initial terminal height from session creation
|
||||||
|
|
||||||
private originalFontSize: number = 14;
|
private originalFontSize: number = 14;
|
||||||
|
userOverrideWidth = false; // Track if user manually selected a width (public for session-view access)
|
||||||
|
|
||||||
@state() private terminal: XtermTerminal | null = null;
|
@state() private terminal: XtermTerminal | null = null;
|
||||||
private _viewportY = 0; // Current scroll position in pixels
|
private _viewportY = 0; // Current scroll position in pixels
|
||||||
|
|
@ -61,6 +64,7 @@ export class Terminal extends LitElement {
|
||||||
|
|
||||||
private container: HTMLElement | null = null;
|
private container: HTMLElement | null = null;
|
||||||
private resizeTimeout: NodeJS.Timeout | null = null;
|
private resizeTimeout: NodeJS.Timeout | null = null;
|
||||||
|
private explicitSizeSet = false; // Flag to prevent auto-resize when size is explicitly set
|
||||||
|
|
||||||
// Virtual scrolling optimization
|
// Virtual scrolling optimization
|
||||||
private renderPending = false;
|
private renderPending = false;
|
||||||
|
|
@ -106,13 +110,45 @@ export class Terminal extends LitElement {
|
||||||
|
|
||||||
// Check for debug mode
|
// Check for debug mode
|
||||||
this.debugMode = new URLSearchParams(window.location.search).has('debug');
|
this.debugMode = new URLSearchParams(window.location.search).has('debug');
|
||||||
|
|
||||||
|
// Restore user override preference if we have a sessionId
|
||||||
|
if (this.sessionId) {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(`terminal-width-override-${this.sessionId}`);
|
||||||
|
if (stored !== null) {
|
||||||
|
this.userOverrideWidth = stored === 'true';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// localStorage might be unavailable (e.g., private browsing mode)
|
||||||
|
logger.warn('Failed to load terminal width preference from localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProperties: PropertyValues) {
|
updated(changedProperties: PropertyValues) {
|
||||||
|
// Load user width override preference when sessionId changes
|
||||||
|
if (changedProperties.has('sessionId') && this.sessionId) {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(`terminal-width-override-${this.sessionId}`);
|
||||||
|
if (stored !== null) {
|
||||||
|
this.userOverrideWidth = stored === 'true';
|
||||||
|
// Apply the loaded preference immediately
|
||||||
|
if (this.container) {
|
||||||
|
this.fitTerminal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// localStorage might be unavailable (e.g., private browsing mode)
|
||||||
|
logger.warn('Failed to load terminal width preference from localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (changedProperties.has('cols') || changedProperties.has('rows')) {
|
if (changedProperties.has('cols') || changedProperties.has('rows')) {
|
||||||
if (this.terminal) {
|
if (this.terminal && !this.explicitSizeSet) {
|
||||||
this.reinitializeTerminal();
|
this.reinitializeTerminal();
|
||||||
}
|
}
|
||||||
|
// Reset the flag after processing
|
||||||
|
this.explicitSizeSet = false;
|
||||||
}
|
}
|
||||||
if (changedProperties.has('fontSize')) {
|
if (changedProperties.has('fontSize')) {
|
||||||
// Store original font size when it changes (but not during horizontal fitting)
|
// Store original font size when it changes (but not during horizontal fitting)
|
||||||
|
|
@ -144,6 +180,24 @@ export class Terminal extends LitElement {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to set user override when width is manually selected
|
||||||
|
setUserOverrideWidth(override: boolean) {
|
||||||
|
this.userOverrideWidth = override;
|
||||||
|
// Persist the preference
|
||||||
|
if (this.sessionId) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(`terminal-width-override-${this.sessionId}`, String(override));
|
||||||
|
} catch (error) {
|
||||||
|
// localStorage might be unavailable or quota exceeded
|
||||||
|
logger.warn('Failed to save terminal width preference to localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trigger a resize to apply the new setting
|
||||||
|
if (this.container) {
|
||||||
|
this.fitTerminal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private cleanup() {
|
private cleanup() {
|
||||||
// Stop momentum animation
|
// Stop momentum animation
|
||||||
if (this.momentumAnimation) {
|
if (this.momentumAnimation) {
|
||||||
|
|
@ -346,9 +400,31 @@ export class Terminal extends LitElement {
|
||||||
|
|
||||||
// Ensure charWidth is valid before division
|
// Ensure charWidth is valid before division
|
||||||
const safeCharWidth = Number.isFinite(charWidth) && charWidth > 0 ? charWidth : 8; // Default char width
|
const safeCharWidth = Number.isFinite(charWidth) && charWidth > 0 ? charWidth : 8; // Default char width
|
||||||
const calculatedCols = Math.max(20, Math.floor(containerWidth / safeCharWidth)) - 1; // This -1 should not be needed, but it is...
|
// Subtract 1 to prevent horizontal scrollbar due to rounding/border issues
|
||||||
// Apply maxCols constraint if set (0 means no limit)
|
const calculatedCols = Math.max(20, Math.floor(containerWidth / safeCharWidth)) - 1;
|
||||||
this.cols = this.maxCols > 0 ? Math.min(calculatedCols, this.maxCols) : calculatedCols;
|
|
||||||
|
// Apply constraints in order of priority:
|
||||||
|
// 1. If user has manually selected a specific width (maxCols > 0), use that as the limit
|
||||||
|
// 2. If user has explicitly selected "unlimited" (maxCols = 0 with userOverrideWidth), use full width
|
||||||
|
// 3. For tunneled sessions (fwd_*), if we have initial dimensions and no user override, limit to initial width
|
||||||
|
// 4. Otherwise, use calculated width (unlimited)
|
||||||
|
|
||||||
|
// Check if this is a tunneled session (from vt command)
|
||||||
|
const isTunneledSession = this.sessionId.startsWith('fwd_');
|
||||||
|
|
||||||
|
if (this.maxCols > 0) {
|
||||||
|
// User has manually selected a specific width limit
|
||||||
|
this.cols = Math.min(calculatedCols, this.maxCols);
|
||||||
|
} else if (this.userOverrideWidth) {
|
||||||
|
// User has explicitly selected "unlimited" - use full width
|
||||||
|
this.cols = calculatedCols;
|
||||||
|
} else if (this.initialCols > 0 && isTunneledSession) {
|
||||||
|
// Only apply initial width restriction for tunneled sessions
|
||||||
|
this.cols = Math.min(calculatedCols, this.initialCols);
|
||||||
|
} else {
|
||||||
|
// No constraints - use full width (for frontend-created sessions or sessions without initial dimensions)
|
||||||
|
this.cols = calculatedCols;
|
||||||
|
}
|
||||||
this.rows = Math.max(6, Math.floor(containerHeight / lineHeight));
|
this.rows = Math.max(6, Math.floor(containerHeight / lineHeight));
|
||||||
this.actualRows = this.rows;
|
this.actualRows = this.rows;
|
||||||
|
|
||||||
|
|
@ -962,13 +1038,22 @@ export class Terminal extends LitElement {
|
||||||
this.cols = cols;
|
this.cols = cols;
|
||||||
this.rows = rows;
|
this.rows = rows;
|
||||||
|
|
||||||
if (!this.terminal) return;
|
if (!this.terminal) {
|
||||||
|
// Don't set explicitSizeSet if terminal isn't ready
|
||||||
|
// This allows reinitializeTerminal to run later when terminal is available
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set flag to prevent auto-resize in updated() lifecycle
|
||||||
|
// Only set this AFTER confirming terminal exists
|
||||||
|
this.explicitSizeSet = true;
|
||||||
|
|
||||||
this.queueRenderOperation(() => {
|
this.queueRenderOperation(() => {
|
||||||
if (!this.terminal) return;
|
if (!this.terminal) return;
|
||||||
|
|
||||||
this.terminal.resize(cols, rows);
|
this.terminal.resize(cols, rows);
|
||||||
this.fitTerminal();
|
// Don't call fitTerminal here - when explicitly setting size,
|
||||||
|
// we shouldn't recalculate based on container dimensions
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,8 @@ export class PtyManager extends EventEmitter {
|
||||||
workingDir: workingDir,
|
workingDir: workingDir,
|
||||||
status: 'starting',
|
status: 'starting',
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
|
initialCols: cols,
|
||||||
|
initialRows: rows,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save initial session info
|
// Save initial session info
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,9 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
||||||
|
|
||||||
// Create new session (local or on remote)
|
// Create new session (local or on remote)
|
||||||
router.post('/sessions', async (req, res) => {
|
router.post('/sessions', async (req, res) => {
|
||||||
const { command, workingDir, name, remoteId, spawn_terminal } = req.body;
|
const { command, workingDir, name, remoteId, spawn_terminal, cols, rows } = req.body;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`creating new session: command=${JSON.stringify(command)}, remoteId=${remoteId || 'local'}`
|
`creating new session: command=${JSON.stringify(command)}, remoteId=${remoteId || 'local'}, cols=${cols}, rows=${rows}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!command || !Array.isArray(command) || command.length === 0) {
|
if (!command || !Array.isArray(command) || command.length === 0) {
|
||||||
|
|
@ -159,6 +159,8 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
||||||
workingDir,
|
workingDir,
|
||||||
name,
|
name,
|
||||||
spawn_terminal,
|
spawn_terminal,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
// Don't forward remoteId to avoid recursion
|
// Don't forward remoteId to avoid recursion
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(10000), // 10 second timeout
|
signal: AbortSignal.timeout(10000), // 10 second timeout
|
||||||
|
|
@ -246,6 +248,8 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
||||||
const result = await ptyManager.createSession(command, {
|
const result = await ptyManager.createSession(command, {
|
||||||
name: sessionName,
|
name: sessionName,
|
||||||
workingDir: cwd,
|
workingDir: cwd,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sessionId, sessionInfo } = result;
|
const { sessionId, sessionInfo } = result;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export interface SessionInfo {
|
||||||
exitCode?: number;
|
exitCode?: number;
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
|
initialCols?: number;
|
||||||
|
initialRows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,33 @@ describe('Sessions API Tests', () => {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
expect(result).toHaveProperty('sessionId');
|
expect(result).toHaveProperty('sessionId');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create session with initial dimensions', async () => {
|
||||||
|
const response = await fetch(`http://localhost:${server?.port}/api/sessions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
command: ['echo', 'dimension test'],
|
||||||
|
workingDir: server?.testDir,
|
||||||
|
cols: 120,
|
||||||
|
rows: 30,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
const result = await response.json();
|
||||||
|
expect(result).toHaveProperty('sessionId');
|
||||||
|
|
||||||
|
// Verify session was created with initial dimensions
|
||||||
|
const sessionResponse = await fetch(
|
||||||
|
`http://localhost:${server?.port}/api/sessions/${result.sessionId}`
|
||||||
|
);
|
||||||
|
const session = await sessionResponse.json();
|
||||||
|
expect(session.initialCols).toBe(120);
|
||||||
|
expect(session.initialRows).toBe(30);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Session lifecycle', () => {
|
describe('Session lifecycle', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue