Skip to content

Commit 59f4e31

Browse files
CopilotDonJayamanne
andcommitted
Add comprehensive unit tests for IPyWidget renderer validation fix
Co-authored-by: DonJayamanne <[email protected]>
1 parent 61ea585 commit 59f4e31

File tree

3 files changed

+236
-45
lines changed

3 files changed

+236
-45
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { OutputItem } from 'vscode-notebook-renderer';
5+
6+
// Extracted function for testing - doesn't import styles.css
7+
export function convertVSCodeOutputToExecuteResultOrDisplayData(outputItem: OutputItem): any | undefined {
8+
try {
9+
// Try to parse as JSON first if the mime type suggests JSON content
10+
if (outputItem.mime.toLowerCase().includes('json')) {
11+
const data = outputItem.json();
12+
// Check if this looks like widget model data - it should have a model_id
13+
if (data && typeof data === 'object' && 'model_id' in data) {
14+
return data;
15+
}
16+
// If it's JSON but not widget data, return undefined to fallback
17+
return undefined;
18+
}
19+
20+
// For non-JSON content, try to parse text as JSON (for edge cases)
21+
const textData = outputItem.text();
22+
if (textData) {
23+
try {
24+
const parsed = JSON.parse(textData);
25+
// Only return if it looks like widget model data
26+
if (parsed && typeof parsed === 'object' && 'model_id' in parsed) {
27+
return parsed;
28+
}
29+
} catch {
30+
// Not valid JSON, this is regular text content - not widget data
31+
}
32+
}
33+
34+
// If we get here, this is not widget model data
35+
return undefined;
36+
} catch (error) {
37+
// If any parsing fails, this is not widget data
38+
return undefined;
39+
}
40+
}

src/webviews/webview-side/ipywidgets/renderer/index.ts

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,11 @@
22
// Licensed under the MIT License.
33

44
import './styles.css';
5-
import type * as nbformat from '@jupyterlab/nbformat';
65
import { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
76
import { createDeferred, Deferred } from '../../../../platform/common/utils/async';
87
import { NotebookMetadata } from '../../../../platform/common/utils';
98
import { logErrorMessage } from '../../react-common/logger';
10-
11-
function convertVSCodeOutputToExecuteResultOrDisplayData(outputItem: OutputItem):
12-
| (nbformat.IMimeBundle & {
13-
model_id: string;
14-
version_major: number;
15-
/**
16-
* This property is only used & added in tests.
17-
*/
18-
_vsc_test_cellIndex?: number;
19-
})
20-
| undefined {
21-
try {
22-
// Try to parse as JSON first if the mime type suggests JSON content
23-
if (outputItem.mime.toLowerCase().includes('json')) {
24-
const data = outputItem.json();
25-
// Check if this looks like widget model data - it should have a model_id
26-
if (data && typeof data === 'object' && 'model_id' in data) {
27-
return data;
28-
}
29-
// If it's JSON but not widget data, return undefined to fallback
30-
return undefined;
31-
}
32-
33-
// For non-JSON content, try to parse text as JSON (for edge cases)
34-
const textData = outputItem.text();
35-
if (textData) {
36-
try {
37-
const parsed = JSON.parse(textData);
38-
// Only return if it looks like widget model data
39-
if (parsed && typeof parsed === 'object' && 'model_id' in parsed) {
40-
return parsed;
41-
}
42-
} catch {
43-
// Not valid JSON, this is regular text content - not widget data
44-
}
45-
}
46-
47-
// If we get here, this is not widget model data
48-
return undefined;
49-
} catch (error) {
50-
// If any parsing fails, this is not widget data
51-
return undefined;
52-
}
53-
}
9+
import { convertVSCodeOutputToExecuteResultOrDisplayData } from './converter';
5410

5511
/**
5612
* Error to be throw to to notify VS Code that it should render the output with the next available mime type.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { assert } from 'chai';
5+
import { OutputItem } from 'vscode-notebook-renderer';
6+
7+
/* eslint-disable @typescript-eslint/no-explicit-any */
8+
9+
// Mock the OutputItem interface for testing
10+
class MockOutputItem implements OutputItem {
11+
constructor(
12+
public id: string,
13+
public mime: string,
14+
private _data: any,
15+
public metadata: Record<string, any> = {}
16+
) {}
17+
18+
json(): any {
19+
if (this.mime.toLowerCase().includes('json')) {
20+
return this._data;
21+
}
22+
throw new Error('Not JSON data');
23+
}
24+
25+
text(): string {
26+
return typeof this._data === 'string' ? this._data : JSON.stringify(this._data);
27+
}
28+
29+
blob(): Blob {
30+
throw new Error('Not implemented');
31+
}
32+
33+
data(): Uint8Array {
34+
throw new Error('Not implemented');
35+
}
36+
}
37+
38+
// Import the function we want to test
39+
import { convertVSCodeOutputToExecuteResultOrDisplayData } from './converter';
40+
41+
suite('IPyWidget Renderer - convertVSCodeOutputToExecuteResultOrDisplayData', () => {
42+
test('Should return widget data when JSON contains model_id', () => {
43+
const widgetData = {
44+
model_id: 'test-widget-123',
45+
version_major: 2,
46+
version_minor: 0,
47+
state: { value: 42 }
48+
};
49+
50+
const outputItem = new MockOutputItem('test-1', 'application/vnd.jupyter.widget-view+json', widgetData);
51+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
52+
53+
assert.deepEqual(result, widgetData);
54+
});
55+
56+
test('Should return undefined when JSON does not contain model_id', () => {
57+
const nonWidgetData = {
58+
content: '<div>Some HTML content</div>',
59+
metadata: {}
60+
};
61+
62+
const outputItem = new MockOutputItem('test-2', 'application/json', nonWidgetData);
63+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
64+
65+
assert.isUndefined(result);
66+
});
67+
68+
test('Should return widget data when text contains valid widget JSON', () => {
69+
const widgetData = {
70+
model_id: 'text-widget-456',
71+
version_major: 2,
72+
version_minor: 0,
73+
state: { text: 'Hello World' }
74+
};
75+
76+
const outputItem = new MockOutputItem('test-3', 'text/plain', JSON.stringify(widgetData));
77+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
78+
79+
assert.deepEqual(result, widgetData);
80+
});
81+
82+
test('Should return undefined when text contains non-widget JSON', () => {
83+
const nonWidgetData = {
84+
content: 'Some content',
85+
type: 'text'
86+
};
87+
88+
const outputItem = new MockOutputItem('test-4', 'text/plain', JSON.stringify(nonWidgetData));
89+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
90+
91+
assert.isUndefined(result);
92+
});
93+
94+
test('Should return undefined when text is not valid JSON', () => {
95+
const plainText = '<div>Some HTML content</div>';
96+
97+
const outputItem = new MockOutputItem('test-5', 'text/html', plainText);
98+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
99+
100+
assert.isUndefined(result);
101+
});
102+
103+
test('Should return undefined when text is regular plain text', () => {
104+
const plainText = 'This is just plain text content';
105+
106+
const outputItem = new MockOutputItem('test-6', 'text/plain', plainText);
107+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
108+
109+
assert.isUndefined(result);
110+
});
111+
112+
test('Should handle empty data gracefully', () => {
113+
const outputItem = new MockOutputItem('test-7', 'application/json', null);
114+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
115+
116+
assert.isUndefined(result);
117+
});
118+
119+
test('Should handle invalid JSON gracefully', () => {
120+
// Create a mock that throws an error on json() call
121+
const outputItem = new MockOutputItem('test-8', 'application/json', 'invalid-json');
122+
// Override json method to throw
123+
outputItem.json = () => {
124+
throw new Error('Invalid JSON');
125+
};
126+
127+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
128+
129+
assert.isUndefined(result);
130+
});
131+
132+
test('Should return widget data for append_display_data widget output', () => {
133+
// This tests the specific case from the issue - append_display_data creates
134+
// outputs that should be processed as widget data when they contain model_id
135+
const appendDisplayData = {
136+
model_id: 'output-widget-789',
137+
version_major: 2,
138+
version_minor: 0,
139+
state: {
140+
outputs: [
141+
{
142+
output_type: 'display_data',
143+
data: {
144+
'text/html': '<div style="color: green;">Content added via append_display_data</div>'
145+
}
146+
}
147+
]
148+
}
149+
};
150+
151+
const outputItem = new MockOutputItem('test-9', 'application/vnd.jupyter.widget-view+json', appendDisplayData);
152+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
153+
154+
assert.deepEqual(result, appendDisplayData);
155+
assert.isNotNull(result);
156+
assert.equal((result as any).model_id, 'output-widget-789');
157+
});
158+
159+
test('Should return undefined for regular HTML content from append_display_data', () => {
160+
// This tests the case where append_display_data creates regular HTML/text content
161+
// that should NOT be processed by the widget renderer
162+
const regularContent = '<div style="color: blue;">Regular HTML content</div>';
163+
164+
const outputItem = new MockOutputItem('test-10', 'text/html', regularContent);
165+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
166+
167+
assert.isUndefined(result);
168+
});
169+
170+
test('Should handle widget data with complex nested structures', () => {
171+
const complexWidgetData = {
172+
model_id: 'complex-widget-999',
173+
version_major: 2,
174+
version_minor: 0,
175+
state: {
176+
children: ['IPY_MODEL_child1', 'IPY_MODEL_child2'],
177+
layout: {
178+
width: '100%',
179+
height: '200px'
180+
},
181+
style: {
182+
background_color: '#ffffff'
183+
}
184+
}
185+
};
186+
187+
const outputItem = new MockOutputItem('test-11', 'application/vnd.jupyter.widget-view+json', complexWidgetData);
188+
const result = convertVSCodeOutputToExecuteResultOrDisplayData(outputItem);
189+
190+
assert.deepEqual(result, complexWidgetData);
191+
assert.isNotNull(result);
192+
assert.equal((result as any).model_id, 'complex-widget-999');
193+
assert.deepEqual((result as any).state.children, ['IPY_MODEL_child1', 'IPY_MODEL_child2']);
194+
});
195+
});

0 commit comments

Comments
 (0)