-
Notifications
You must be signed in to change notification settings - Fork 0
/
TestResultDetailsTabPreviewer.tsx
347 lines (278 loc) · 12.3 KB
/
TestResultDetailsTabPreviewer.tsx
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
import * as React from "react";
import * as SDK from "azure-devops-extension-sdk";
import "./TestResultDetailsTabPreviewer.scss";
import { ArrayItemProvider } from "azure-devops-ui/Utilities/Provider";
import { Card } from "azure-devops-ui/Card";
import { Page } from "azure-devops-ui/Page";
import { ScrollableList, IListItemDetails, ListSelection, ListItem } from "azure-devops-ui/List";
import { Spinner, SpinnerSize } from "azure-devops-ui/Spinner";
import { ZeroData } from "azure-devops-ui/ZeroData";
import { IProjectPageService, CommonServiceIds, IProjectInfo } from "azure-devops-extension-api/Common";
import { TestAttachment, ResultDetails } from "azure-devops-extension-api/Test/Test";
import { showRootComponent } from "../../Common";
import { AzureDevOpsUtilities } from "./AzureDevOpsUtilities";
import { TestResultsRestClient } from "azure-devops-extension-api/TestResults";
/**
* The interface containg the information of the currently selected information
* item.
*/
interface ISelectedInformation {
/**
* The title being displayed of the selected resource from the information.
*/
readonly title: string;
/**
* The URL to the resource, it is embedded in a iframe.
*/
readonly url: string;
/**
* The MIME type of the resource, an empty string applies all restrictions,
* null applies no restrictions.
*/
readonly sandbox: string | null;
}
/**
* The state of the run with its corresponding data.
*/
interface ITestResultDetailsTabPreviewerComponentState {
/**
* The attachments of the run, if there were multiple attempts of the run,
* the attachments of the attempt are fetched.
*/
readonly attachments: ArrayItemProvider<TestAttachment>;
/**
* The information that are currently selected by the user.
*/
readonly selected?: ISelectedInformation;
/**
* Whether the component has finished loading its data.
*/
readonly loaded: boolean;
/**
* Whether an attachment is currently being loaded, happens when one is
* clicked by the user, prevents them from loading 2 attachments at the
* same time.
*/
readonly lock: boolean;
}
/**
* The tab containing the attachment previews.
*/
export class TestResultDetailsTabPreviewerComponent extends React.Component<{}, ITestResultDetailsTabPreviewerComponentState> {
/**
* The current attachment selection from the list overview.
*/
private attachmentSelection = new ListSelection(true);
/**
* A map that maps the file extension type to a MIME type.
*/
private TYPE_MAPPINGS: { [type: string]: string } = {
"htm": "text/html",
"html": "text/html",
"pdf": "application/pdf",
"mp4": "video/mp4",
"wmv": "video/x-ms-wmv",
"gif": "image/gif",
"ico": "image/vnd.microsoft.icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"json": "application/json",
"png": "image/png",
"svg": "image/svg+xml",
};
/**
* A map that contains the MIME types and their sandbox restrictions.
*/
private TYPE_SANDBOX: { [type: string]: string | null } = {
// required for videos to display and allow their playback
"video/mp4": "allow-same-origin",
"video/x-ms-wmv": "allow-same-origin",
// there are no restrictions that prevents PDFs from being blocked
"application/pdf": null,
};
/**
* Initializes the component and sets a default state.
*/
constructor(props: {}) {
super(props);
this.state = {
attachments: new ArrayItemProvider([]),
selected: undefined,
loaded: false,
lock: false,
};
}
/**
* Setups the component once it gets mounted in the UI.
*/
public componentDidMount() {
this.setup();
}
/**
* Render the attachment preview tab.
*/
public render(): JSX.Element {
// the initial loading screen
if (!this.state.loaded) {
return (
<Page className="test-result-details-tab-previewer-tab flex-grow flex-row">
<div className="loading" data-testid="loading">
<Spinner className="loading" size={SpinnerSize.large} />
</div>
</Page>
);
}
// whenever no attachments are available
if (!this.state.attachments || this.state.attachments.length === 0) {
return (
<Page className="test-result-details-tab-previewer-tab flex-grow flex-column justify-center">
<ZeroData
primaryText="No attachments"
secondaryText={
<span>
No available attachments to preview.
</span>
}
imagePath=""
imageAltText=""
/>
</Page>
);
}
// the attachments listing and preview itself
return (
<Page className="test-result-details-tab-previewer-tab flex-grow flex-row">
<div className={"attachments flex-row " + (this.state.lock ? "attachments-disable" : "")}>
<ScrollableList
itemProvider={this.state.attachments}
renderRow={this.renderAttachmentRow}
selection={this.attachmentSelection}
onSelect={(_, item) => this.onAttachmentClick(item.data.fileName, item.data.id)}
maxWidth="250px" />
</div>
<div className="attachment flex-column">
{!this.state.lock ?
this.state.selected ?
<Card className="bolt-card-white" titleProps={{ text: this.state.selected.title, ariaLevel: 3 }}>
{this.state.selected.sandbox === null // only disable the sandbox if specifically configured
? <iframe className="iframe" data-testid="iframe" frameBorder="false" src={this.state.selected.url}></iframe>
: <iframe className="iframe" data-testid="iframe" frameBorder="false" src={this.state.selected.url} sandbox={this.state.selected.sandbox}></iframe>
}
</Card>
:
<ZeroData
primaryText="No attachment selected"
secondaryText={
<span>
Please select an attachment to preview.
</span>
}
imagePath=""
imageAltText="" />
:
<div className="loading" data-testid="loading-attachment">
<Spinner className="loading" size={SpinnerSize.large} />
</div>
}
</div>
</Page>
);
}
/**
* Renders the row where the attachments are being listed.
*/
private renderAttachmentRow(index: number,
item: TestAttachment,
details: IListItemDetails<TestAttachment>,
key?: string): JSX.Element {
return (
<ListItem key={key || "list-item-" + index} index={index} details={details}>
<div className="attachment-item flex-column h-scroll-hidden">
<span className="text-ellipses">{item.fileName}</span>
<span className="fontSizeMS font-size-ms text-ellipses secondary-text">{item.comment || "No description available"}</span>
</div>
</ListItem>
);
}
/**
* Gets the run sub result id for the provided configuration, returns
* null if invalid.
*/
private async getSubResultId(configuration: { [key: string]: any }, project: IProjectInfo, testResultClient: TestResultsRestClient): Promise<number | null> {
// for test run sections that aren't actually run results
if (!configuration.resultId) {
this.setState({ loaded: true, attachments: new ArrayItemProvider([]) });
return null;
}
const results = await testResultClient.getTestResultById(project.name, configuration.runId, configuration.resultId, ResultDetails.SubResults);
// in this case a test run has been selected directly
if (configuration.subResultId != 0) return configuration.subResultId;
// this ensures that when a run exists with sub results that the
// attachments of the last attempt is shown
const subResultId = results.subResults && results.subResults.length > 0
? Array.from(results.subResults).reverse()[0].id
: configuration.subResultId;
return subResultId;
}
/**
* Load the attachment data of the currently selected test result.
*/
private async setup(): Promise<void> {
await SDK.init();
const configuration = SDK.getConfiguration();
const projectService = await SDK.getService<IProjectPageService>(CommonServiceIds.ProjectPageService);
const project = await projectService.getProject();
if (!project) {
console.warn("[TestResultDetailsTabPreviewer] invalid project");
return;
}
const testResultClient = AzureDevOpsUtilities.getTestResultRestClient();
const subResultId = await this.getSubResultId(configuration, project, testResultClient);
if (subResultId == null) {
this.setState({ loaded: true, attachments: new ArrayItemProvider([]) });
return;
}
const attachments = await testResultClient.getTestSubResultAttachments(project.name, configuration.runId, configuration.resultId, subResultId)
const provider = new ArrayItemProvider(attachments);
this.setState({ loaded: true, attachments: provider });
}
/**
* Handles the selection of an attachment from the provided attachment list.
*/
private async onAttachmentClick(title: string, attachmentId: number): Promise<void> {
// lock the UI, prevents the user from clicking on another attachment
// during loading
this.setState({ lock: true });
const configuration = SDK.getConfiguration();
const projectService = await SDK.getService<IProjectPageService>(CommonServiceIds.ProjectPageService);
const project = await projectService.getProject();
if (!project) {
console.warn("[TestResultDetailsTabPreviewer] invalid project");
return;
}
const testResultClient = AzureDevOpsUtilities.getTestResultRestClient();
const subResultId = await this.getSubResultId(configuration, project, testResultClient);
if (subResultId == null) return;
// if the attempt ID is not `-1` then the attempt attachments are
// fetched from the run itself
const content = await testResultClient.getTestSubResultAttachmentContent(project.name, configuration.runId, configuration.resultId, attachmentId, subResultId);
const blob = new Blob([content]);
// the title is the file name it can be different types, such as a PDF
// PNG, WMV or a text
const split = title.split(".");
const type = split[split.length - 1];
const mime = this.TYPE_MAPPINGS[type] || "text/plain";
const file = new Blob([blob], { type: mime });
// get the sandbox restrictions for the given MIME type, by default an
// empty string means everything is restricted, if null is specified no
// restrictions are applied
const whitelist = this.TYPE_SANDBOX[mime];
const sandbox = (whitelist || whitelist === null) ? whitelist : "";
// create the local URL to display
const url = URL.createObjectURL(file);
const selected: ISelectedInformation = { title, url, sandbox };
this.setState({ selected: selected, lock: false });
}
}
export default TestResultDetailsTabPreviewerComponent;
showRootComponent(<TestResultDetailsTabPreviewerComponent />);