Skip to content

Commit d9d368f

Browse files
committed
Merge branch 'codex/issue-127-webview-view-tests' into codex/issue-128-webview-script-tests
2 parents cafec6c + f8dc2c5 commit d9d368f

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import type { LJStateMachine } from '../../../types/fsm';
3+
import { renderStateMachineView } from './fsm';
4+
5+
const diagram = 'stateDiagram-v2\n[*] --> Closed\nClosed --> Open: open';
6+
7+
function machine(overrides: Partial<LJStateMachine> = {}): LJStateMachine {
8+
return {
9+
className: 'Connection', states: ['Closed', 'Open'],
10+
initialTransitions: [{ to: 'Closed' }],
11+
transitions: [{ from: 'Closed', to: 'Open', label: 'open' }],
12+
errorContext: null, ...overrides,
13+
};
14+
}
15+
16+
function button(id: string): HTMLButtonElement {
17+
return document.querySelector<HTMLButtonElement>(`#${id}`)!;
18+
}
19+
20+
afterEach(() => {
21+
vi.restoreAllMocks();
22+
document.body.innerHTML = '';
23+
});
24+
25+
describe('state machine view', () => {
26+
it('selects State Machine and shows an empty state when no machine is available', () => {
27+
renderStateMachineView(document.body, undefined, '', 'TB', false);
28+
29+
expect(document.querySelector('.nav-tab.selected')?.textContent).toBe('State Machine');
30+
expect(document.body.textContent).toContain('No state machine available for the current file');
31+
expect(document.querySelector('.diagram-container')).toBeNull();
32+
expect(document.querySelector('.diagram-controls')).toBeNull();
33+
});
34+
35+
it('renders the class, Mermaid source, and labeled diagram controls', () => {
36+
renderStateMachineView(document.body, machine(), diagram, 'TB', false);
37+
38+
expect(document.querySelector('.diagram-title')?.textContent).toBe('Connection');
39+
expect(document.querySelector('.mermaid')?.textContent).toBe(diagram);
40+
expect(Array.from(document.querySelectorAll('.diagram-controls button'), node => node.getAttribute('aria-label'))).toEqual([
41+
'Zoom In', 'Zoom Out', 'Reset Zoom', 'Toggle Orientation',
42+
'No additional conditions to expand', 'Copy Mermaid Source',
43+
]);
44+
for (const id of ['zoom-in-btn', 'zoom-out-btn', 'zoom-reset-btn', 'diagram-orientation-btn', 'copy-diagram-btn']) {
45+
expect(button(id).disabled).toBe(false);
46+
}
47+
expect(document.querySelector<HTMLElement>('.diagram-container')?.style.minHeight).toBe('');
48+
});
49+
50+
it('escapes class titles while retaining their text', () => {
51+
const className = '<img src=x onerror="alert(1)"> & Connection';
52+
renderStateMachineView(document.body, machine({ className }), diagram, 'TB', false);
53+
54+
const title = document.querySelector('.diagram-title')!;
55+
expect(title.textContent).toBe(className);
56+
expect(title.children).toHaveLength(0);
57+
});
58+
59+
it.each([
60+
['TB', 'arrow-down'], ['LR', 'arrow-right'],
61+
] as const)('shows the %s orientation icon', (orientation, icon) => {
62+
renderStateMachineView(document.body, machine(), diagram, orientation, false);
63+
64+
expect(button('diagram-orientation-btn').querySelector(`.codicon-${icon}`)).not.toBeNull();
65+
});
66+
67+
it('disables condition expansion when transitions have no additional conditions', () => {
68+
renderStateMachineView(document.body, machine({
69+
initialTransitions: [{ to: 'Closed', toCondition: null }],
70+
transitions: [{ from: 'Closed', to: 'Open', label: 'open', fromCondition: '', toCondition: null }],
71+
}), diagram, 'TB', false);
72+
73+
const toggle = button('diagram-conditions-btn');
74+
expect(toggle.disabled).toBe(true);
75+
expect(toggle.getAttribute('aria-label')).toBe('No additional conditions to expand');
76+
expect(toggle.querySelector('.icon-button-badge')).toBeNull();
77+
expect(document.querySelector('.diagram-condition-legend')).toBeNull();
78+
});
79+
80+
it.each([
81+
['initial transition postcondition', machine({ initialTransitions: [{ to: 'Closed', toCondition: 'size == 0' }] })],
82+
['method precondition', machine({ transitions: [{ from: 'Closed', to: 'Open', label: 'open', fromCondition: 'size > 0' }] })],
83+
['method postcondition', machine({ transitions: [{ from: 'Closed', to: 'Open', label: 'open', toCondition: 'size == 1' }] })],
84+
] as const)('enables condition expansion for an %s', (_, stateMachine) => {
85+
renderStateMachineView(document.body, stateMachine, diagram, 'TB', false);
86+
87+
expect(button('diagram-conditions-btn').disabled).toBe(false);
88+
expect(button('diagram-conditions-btn').getAttribute('aria-label')).toBe('Expand Conditions');
89+
});
90+
91+
it.each([false, true])('renders condition controls and legend with showConditions=%s', showConditions => {
92+
renderStateMachineView(document.body, machine({
93+
initialTransitions: [{ to: 'Closed', toCondition: 'size == 0' }],
94+
}), diagram, 'TB', showConditions);
95+
96+
const toggle = button('diagram-conditions-btn');
97+
const label = showConditions ? 'Collapse Conditions' : 'Expand Conditions';
98+
expect(toggle.title).toBe(label);
99+
expect(toggle.getAttribute('aria-label')).toBe(label);
100+
expect(toggle.getAttribute('aria-pressed')).toBe(String(showConditions));
101+
expect(toggle.classList.contains('active')).toBe(showConditions);
102+
expect(toggle.querySelector(showConditions ? '.codicon-collapse-all' : '.codicon-expand-all')).not.toBeNull();
103+
expect(toggle.querySelector('.icon-button-badge') !== null).toBe(!showConditions);
104+
const legend = document.querySelector('.diagram-condition-legend');
105+
if (showConditions) {
106+
expect(Array.from(legend!.querySelectorAll('span'), node => node.textContent)).toEqual(['Precondition', 'Postcondition']);
107+
} else {
108+
expect(legend).toBeNull();
109+
}
110+
});
111+
112+
it('preserves the previous diagram container height when rerendering', () => {
113+
renderStateMachineView(document.body, machine(), diagram, 'TB', false);
114+
const previousContainer = document.querySelector<HTMLElement>('.diagram-container')!;
115+
vi.spyOn(previousContainer, 'offsetHeight', 'get').mockReturnValue(240);
116+
117+
renderStateMachineView(document.body, machine({ className: 'UpdatedConnection' }), diagram, 'LR', false);
118+
119+
const container = document.querySelector<HTMLElement>('.diagram-container')!;
120+
expect(container).not.toBe(previousContainer);
121+
expect(container.style.minHeight).toBe('240px');
122+
expect(document.querySelector('.diagram-title')?.textContent).toBe('UpdatedConnection');
123+
});
124+
});

0 commit comments

Comments
 (0)