-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebview.test.js
More file actions
184 lines (162 loc) · 6.23 KB
/
Copy pathwebview.test.js
File metadata and controls
184 lines (162 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
const vscode = require('vscode');
const {
renderHtml,
buildCsp,
generateNonce,
handleMessage,
registerShowWebview,
createOrShow,
__reset: resetPanel,
} = require('../src/webview/panel');
const pkg = require('../package.json');
function createContext() {
return { subscriptions: [], extensionUri: 'file:///fake/ext' };
}
beforeEach(() => {
vscode.__reset();
resetPanel();
});
describe('generateNonce', () => {
test('produces distinct values across calls', () => {
const a = generateNonce();
const b = generateNonce();
expect(a).not.toBe(b);
// base64 of 16 bytes -> 24 chars including padding
expect(a).toMatch(/^[A-Za-z0-9+/=]+$/);
expect(a.length).toBeGreaterThanOrEqual(22);
});
});
describe('buildCsp', () => {
test('interpolates cspSource and nonce and locks default-src to none', () => {
const csp = buildCsp('vscode-webview://abc', 'N0NCE');
expect(csp).toContain("default-src 'none'");
expect(csp).toContain('style-src vscode-webview://abc');
expect(csp).toContain("script-src 'nonce-N0NCE'");
// Sanity: no wildcard that would defeat CSP.
expect(csp).not.toContain("script-src *");
expect(csp).not.toContain("'unsafe-eval'");
});
});
describe('renderHtml', () => {
test('embeds CSP meta, cspSource, and the provided nonce', () => {
const webview = { cspSource: 'vscode-webview://abc' };
const html = renderHtml(webview, 'NONCE-A');
expect(html).toContain('<meta http-equiv="Content-Security-Policy"');
expect(html).toContain("default-src 'none'");
expect(html).toContain('vscode-webview://abc');
expect(html).toContain('nonce-NONCE-A');
// Every script tag must carry the nonce — inline scripts without it are blocked.
// Case-insensitive regex: CodeQL js/bad-tag-filter flags case-sensitive HTML regexes
// because HTML tag names are case-insensitive in browsers.
const scriptOpenTags = html.match(/<script\b[^>]*>/gi) || [];
expect(scriptOpenTags.length).toBeGreaterThan(0);
for (const tag of scriptOpenTags) {
expect(tag).toContain('nonce="NONCE-A"');
}
});
test('uses a fresh nonce each call when driven by generateNonce', () => {
const webview = { cspSource: 'vscode-webview://x' };
const a = renderHtml(webview, generateNonce());
const b = renderHtml(webview, generateNonce());
expect(a).not.toBe(b);
});
});
describe('handleMessage', () => {
test('getWorkspace returns a workspace response with name + folders', () => {
const post = jest.fn();
const response = handleMessage(
{ type: 'getWorkspace' },
post,
{
name: 'demo.code-workspace',
workspaceFolders: [{ name: 'repo', uri: { fsPath: '/tmp/repo' } }],
}
);
expect(post).toHaveBeenCalledTimes(1);
expect(response).toEqual({
type: 'workspace',
data: {
name: 'demo.code-workspace',
folders: [{ name: 'repo', path: '/tmp/repo' }],
},
});
expect(post).toHaveBeenCalledWith(response);
});
test('handles missing workspaceFolders gracefully', () => {
const post = jest.fn();
const response = handleMessage({ type: 'getWorkspace' }, post, {});
expect(response).toEqual({ type: 'workspace', data: { name: null, folders: [] } });
});
test('ignores unknown message types without posting', () => {
const post = jest.fn();
const response = handleMessage({ type: 'unknown' }, post, {});
expect(response).toBeNull();
expect(post).not.toHaveBeenCalled();
});
test('ignores malformed messages', () => {
const post = jest.fn();
expect(handleMessage(null, post, {})).toBeNull();
expect(handleMessage('string', post, {})).toBeNull();
expect(post).not.toHaveBeenCalled();
});
});
describe('registerShowWebview + createOrShow', () => {
test('registers the command declared in package.json', () => {
const context = createContext();
const declared = pkg.contributes.commands.find(
(c) => c.command === 'my-extension.showWebview'
);
expect(declared).toBeDefined();
registerShowWebview(context);
expect(vscode.commands.registerCommand).toHaveBeenCalledWith(
'my-extension.showWebview',
expect.any(Function)
);
expect(context.subscriptions).toHaveLength(1);
});
test('executing the command creates a webview panel with locked-down options', async () => {
const context = createContext();
registerShowWebview(context);
await vscode.commands.executeCommand('my-extension.showWebview');
expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(1);
const [, , , options] = vscode.window.createWebviewPanel.mock.calls[0];
expect(options.enableScripts).toBe(true);
expect(Array.isArray(options.localResourceRoots)).toBe(true);
expect(options.localResourceRoots).toHaveLength(1);
});
test('reuses the existing panel instead of creating a second one', () => {
const context = createContext();
const first = createOrShow(context);
const second = createOrShow(context);
expect(second).toBe(first);
expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(1);
expect(first.reveal).toHaveBeenCalled();
});
test('disposing the panel clears the singleton so the next call recreates', () => {
const context = createContext();
const first = createOrShow(context);
first.dispose(); // fires onDidDispose listeners
const second = createOrShow(context);
expect(second).not.toBe(first);
expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(2);
});
test('webview messages flow through to postMessage', () => {
const context = createContext();
vscode.workspace.name = 'demo';
vscode.workspace.workspaceFolders = [{ name: 'r', uri: { fsPath: '/r' } }];
const panel = createOrShow(context);
panel.__fireMessage({ type: 'getWorkspace' });
expect(panel.webview.postMessage).toHaveBeenCalledWith({
type: 'workspace',
data: { name: 'demo', folders: [{ name: 'r', path: '/r' }] },
});
});
});
describe('package.json contract (webview command)', () => {
test('showWebview has an activationEvent', () => {
expect(pkg.contributes.commands.map((c) => c.command)).toContain(
'my-extension.showWebview'
);
expect(pkg.activationEvents).toContain('onCommand:my-extension.showWebview');
});
});