mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
add/fix tests
This commit is contained in:
parent
6501a3b3c8
commit
72806b4403
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
|
||||
import { appendScrollableOutput, createOutputContent, scrollableClass } from './textHelper';
|
||||
import { createOutputContent, appendOutput, scrollableClass } from './textHelper';
|
||||
import { HtmlRenderingHook, IDisposable, IRichRenderContext, JavaScriptRenderingHook, OutputWithAppend, RenderOptions } from './rendererTypes';
|
||||
import { ttPolicy } from './htmlHelper';
|
||||
|
||||
|
@ -152,7 +152,7 @@ function renderError(
|
|||
outputInfo: OutputItem,
|
||||
outputElement: HTMLElement,
|
||||
ctx: IRichRenderContext,
|
||||
trustHTML: boolean
|
||||
trustHtml: boolean
|
||||
): IDisposable {
|
||||
const disposableStore = createDisposableStore();
|
||||
|
||||
|
@ -172,7 +172,7 @@ function renderError(
|
|||
outputElement.classList.add('traceback');
|
||||
|
||||
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
|
||||
const content = createOutputContent(outputInfo.id, err.stack ?? '', ctx.settings.lineLimit, outputScrolling, trustHTML);
|
||||
const content = createOutputContent(outputInfo.id, err.stack ?? '', { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml });
|
||||
const contentParent = document.createElement('div');
|
||||
contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap);
|
||||
disposableStore.push(ctx.onDidChangeSettings(e => {
|
||||
|
@ -271,9 +271,9 @@ function scrollingEnabled(output: OutputItem, options: RenderOptions) {
|
|||
// div.scrollable? tabindex="0" <-- contentParent
|
||||
// div output-item-id="{guid}" <-- content from outputItem parameter
|
||||
function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable {
|
||||
const appendedText = outputInfo.appendedText?.();
|
||||
const disposableStore = createDisposableStore();
|
||||
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
|
||||
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error };
|
||||
|
||||
outputElement.classList.add('output-stream');
|
||||
|
||||
|
@ -284,15 +284,9 @@ function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement,
|
|||
if (previousOutputParent) {
|
||||
const existingContent = previousOutputParent.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null;
|
||||
if (existingContent) {
|
||||
if (appendedText && outputScrolling) {
|
||||
appendScrollableOutput(existingContent, outputInfo.id, appendedText, outputInfo.text(), false);
|
||||
}
|
||||
else {
|
||||
const newContent = createContent(outputInfo, ctx, outputScrolling, error);
|
||||
existingContent.replaceWith(newContent);
|
||||
}
|
||||
appendOutput(outputInfo, existingContent, outputOptions);
|
||||
} else {
|
||||
const newContent = createContent(outputInfo, ctx, outputScrolling, error);
|
||||
const newContent = createOutputContent(outputInfo.id, outputInfo.text(), outputOptions);
|
||||
previousOutputParent.appendChild(newContent);
|
||||
}
|
||||
previousOutputParent.classList.toggle('scrollbar-visible', previousOutputParent.scrollHeight > previousOutputParent.clientHeight);
|
||||
|
@ -301,20 +295,9 @@ function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement,
|
|||
const existingContent = outputElement.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null;
|
||||
let contentParent = existingContent?.parentElement;
|
||||
if (existingContent && contentParent) {
|
||||
// appending output only in scrollable ouputs currently
|
||||
if (appendedText && outputScrolling) {
|
||||
appendScrollableOutput(existingContent, outputInfo.id, appendedText, outputInfo.text(), false);
|
||||
}
|
||||
else {
|
||||
const newContent = createContent(outputInfo, ctx, outputScrolling, error);
|
||||
existingContent.replaceWith(newContent);
|
||||
while (newContent.nextSibling) {
|
||||
// clear out any stale content if we had previously combined streaming outputs into this one
|
||||
newContent.nextSibling.remove();
|
||||
}
|
||||
}
|
||||
appendOutput(outputInfo, existingContent, outputOptions);
|
||||
} else {
|
||||
const newContent = createContent(outputInfo, ctx, outputScrolling, error);
|
||||
const newContent = createOutputContent(outputInfo.id, outputInfo.text(), outputOptions);
|
||||
contentParent = document.createElement('div');
|
||||
contentParent.appendChild(newContent);
|
||||
while (outputElement.firstChild) {
|
||||
|
@ -335,23 +318,13 @@ function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement,
|
|||
return disposableStore;
|
||||
}
|
||||
|
||||
function createContent(outputInfo: OutputWithAppend, ctx: IRichRenderContext, outputScrolling: boolean, error: boolean) {
|
||||
const text = outputInfo.text();
|
||||
const newContent = createOutputContent(outputInfo.id, text, ctx.settings.lineLimit, outputScrolling, false);
|
||||
newContent.setAttribute('output-item-id', outputInfo.id);
|
||||
if (error) {
|
||||
newContent.classList.add('error');
|
||||
}
|
||||
return newContent;
|
||||
}
|
||||
|
||||
function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRichRenderContext): IDisposable {
|
||||
const disposableStore = createDisposableStore();
|
||||
clearContainer(outputElement);
|
||||
|
||||
const text = outputInfo.text();
|
||||
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
|
||||
const content = createOutputContent(outputInfo.id, text, ctx.settings.lineLimit, outputScrolling, false);
|
||||
const content = createOutputContent(outputInfo.id, text, { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false });
|
||||
content.classList.add('output-plaintext');
|
||||
if (ctx.settings.outputWordWrap) {
|
||||
content.classList.add('word-wrap');
|
||||
|
|
|
@ -36,6 +36,13 @@ export interface RenderOptions {
|
|||
|
||||
export type IRichRenderContext = RendererContext<void> & { readonly settings: RenderOptions; readonly onDidChangeSettings: Event<RenderOptions> };
|
||||
|
||||
export type OutputElementOptions = {
|
||||
linesLimit: number;
|
||||
scrollable?: boolean;
|
||||
error?: boolean;
|
||||
trustHtml?: boolean;
|
||||
};
|
||||
|
||||
export interface OutputWithAppend extends OutputItem {
|
||||
appendedText?(): string | undefined;
|
||||
}
|
||||
|
|
|
@ -193,14 +193,14 @@ suite('Notebook builtin output renderer', () => {
|
|||
});
|
||||
|
||||
test('Append streaming output', async () => {
|
||||
const context = createContext({ outputWordWrap: false, outputScrolling: false });
|
||||
const context = createContext({ outputWordWrap: false, outputScrolling: true });
|
||||
const renderer = await activate(context);
|
||||
assert.ok(renderer, 'Renderer not created');
|
||||
|
||||
const outputElement = new OutputHtml().getFirstOuputElement();
|
||||
const outputItem = createOutputItem('content', stdoutMimeType, '123', 'ignoredAppend');
|
||||
await renderer!.renderOutputItem(outputItem, outputElement);
|
||||
const outputItem2 = createOutputItem('content\nappended', stdoutMimeType, '\nappended');
|
||||
const outputItem2 = createOutputItem('content\nappended', stdoutMimeType, '123', '\nappended');
|
||||
await renderer!.renderOutputItem(outputItem2, outputElement);
|
||||
|
||||
const inserted = outputElement.firstChild as HTMLElement;
|
||||
|
@ -210,39 +210,67 @@ suite('Notebook builtin output renderer', () => {
|
|||
assert.ok(inserted.innerHTML.indexOf('>content</') === inserted.innerHTML.lastIndexOf('>content</'), `Original content should not be duplicated: ${outputElement.innerHTML}`);
|
||||
});
|
||||
|
||||
test(`Appending multiple streaming outputs`, async () => {
|
||||
const context = createContext({ outputScrolling: true });
|
||||
const renderer = await activate(context);
|
||||
assert.ok(renderer, 'Renderer not created');
|
||||
|
||||
const outputHtml = new OutputHtml();
|
||||
const firstOutputElement = outputHtml.getFirstOuputElement();
|
||||
const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1');
|
||||
const outputItem2 = createOutputItem(JSON.stringify(error), errorMimeType, '2');
|
||||
const outputItem3 = createOutputItem('second stream content', stdoutMimeType, '3');
|
||||
await renderer!.renderOutputItem(outputItem1, firstOutputElement);
|
||||
const secondOutputElement = outputHtml.appendOutputElement();
|
||||
await renderer!.renderOutputItem(outputItem2, secondOutputElement);
|
||||
const thirdOutputElement = outputHtml.appendOutputElement();
|
||||
await renderer!.renderOutputItem(outputItem3, thirdOutputElement);
|
||||
|
||||
const appendedItem1 = createOutputItem('', stdoutMimeType, '1', ' appended1');
|
||||
await renderer!.renderOutputItem(appendedItem1, firstOutputElement);
|
||||
const appendedItem3 = createOutputItem('', stdoutMimeType, '3', ' appended3');
|
||||
await renderer!.renderOutputItem(appendedItem3, thirdOutputElement);
|
||||
|
||||
assert.ok(firstOutputElement.innerHTML.indexOf('>first stream content') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
assert.ok(firstOutputElement.innerHTML.indexOf('appended1') > -1, `Content was not appended to output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
assert.ok(secondOutputElement.innerHTML.indexOf('>NameError</') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
assert.ok(thirdOutputElement.innerHTML.indexOf('>second stream content') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
assert.ok(thirdOutputElement.innerHTML.indexOf('appended3') > -1, `Content was not appended to output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
});
|
||||
|
||||
test('Append large streaming outputs', async () => {
|
||||
const context = createContext({ outputWordWrap: false, outputScrolling: false });
|
||||
const context = createContext({ outputWordWrap: false, outputScrolling: true });
|
||||
const renderer = await activate(context);
|
||||
assert.ok(renderer, 'Renderer not created');
|
||||
|
||||
const outputElement = new OutputHtml().getFirstOuputElement();
|
||||
const lotsOfLines = new Array(4998).fill('line').join('\n') + 'endOfInitialContent';
|
||||
const lotsOfLines = new Array(4998).fill('line').join('\n');
|
||||
const firstOuput = lotsOfLines + 'expected1';
|
||||
const outputItem = createOutputItem(firstOuput, stdoutMimeType, '123');
|
||||
await renderer!.renderOutputItem(outputItem, outputElement);
|
||||
const appended = '\n' + lotsOfLines + 'expectedAppend';
|
||||
const outputItem2 = createOutputItem(firstOuput + appended, stdoutMimeType, appended);
|
||||
const outputItem2 = createOutputItem(firstOuput + appended, stdoutMimeType, '123', appended);
|
||||
await renderer!.renderOutputItem(outputItem2, outputElement);
|
||||
|
||||
const inserted = outputElement.firstChild as HTMLElement;
|
||||
assert.ok(inserted.innerHTML.indexOf('>expected1</') !== -1, `Last bit of previous content should still exist: ${outputElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('>expectedAppend</') !== -1, `Content was not appended to output element: ${outputElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('expected1') !== -1, `Last bit of previous content should still exist`);
|
||||
assert.ok(inserted.innerHTML.indexOf('expectedAppend') !== -1, `Content was not appended to output element`);
|
||||
});
|
||||
|
||||
test('Streaming outputs larger than the line limit are truncated', async () => {
|
||||
const context = createContext({ outputWordWrap: false, outputScrolling: false });
|
||||
const context = createContext({ outputWordWrap: false, outputScrolling: true });
|
||||
const renderer = await activate(context);
|
||||
assert.ok(renderer, 'Renderer not created');
|
||||
|
||||
const outputElement = new OutputHtml().getFirstOuputElement();
|
||||
const lotsOfLines = new Array(11000).fill('line').join('\n') + 'endOfInitialContent';
|
||||
const lotsOfLines = new Array(11000).fill('line').join('\n');
|
||||
const firstOuput = 'shouldBeTruncated' + lotsOfLines + 'expected1';
|
||||
const outputItem = createOutputItem(firstOuput, stdoutMimeType, '123');
|
||||
await renderer!.renderOutputItem(outputItem, outputElement);
|
||||
|
||||
const inserted = outputElement.firstChild as HTMLElement;
|
||||
assert.ok(inserted.innerHTML.indexOf('>endOfInitialContent</') !== -1, `Last bit of content should exist: ${outputElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('>shouldBeTruncated</') === -1, `Beginning content should be truncated: ${outputElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('expected1') !== -1, `Last bit of content should exist`);
|
||||
assert.ok(inserted.innerHTML.indexOf('shouldBeTruncated') === -1, `Beginning content should be truncated`);
|
||||
});
|
||||
|
||||
test(`Render with wordwrap and scrolling for error output`, async () => {
|
||||
|
@ -324,6 +352,29 @@ suite('Notebook builtin output renderer', () => {
|
|||
assert.ok(inserted.innerHTML.indexOf('>second stream content</') === -1, `Content was not replaced in output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
});
|
||||
|
||||
test(`Consolidated streaming outputs should append matching outputs correctly`, async () => {
|
||||
const context = createContext({ outputScrolling: true });
|
||||
const renderer = await activate(context);
|
||||
assert.ok(renderer, 'Renderer not created');
|
||||
|
||||
const outputHtml = new OutputHtml();
|
||||
const outputElement = outputHtml.getFirstOuputElement();
|
||||
const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1');
|
||||
const outputItem2 = createOutputItem('second stream content', stdoutMimeType, '2');
|
||||
await renderer!.renderOutputItem(outputItem1, outputElement);
|
||||
const secondOutput = outputHtml.appendOutputElement();
|
||||
await renderer!.renderOutputItem(outputItem2, secondOutput);
|
||||
const appendingOutput = createOutputItem('', stdoutMimeType, '2', ' appended');
|
||||
await renderer!.renderOutputItem(appendingOutput, secondOutput);
|
||||
|
||||
|
||||
const inserted = outputElement.firstChild as HTMLElement;
|
||||
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('>first stream content</') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('>second stream content') > -1, `Second content was not added to ouptut element: ${outputHtml.cellElement.innerHTML}`);
|
||||
assert.ok(inserted.innerHTML.indexOf('appended') > -1, `Content was not appended to ouptut element: ${outputHtml.cellElement.innerHTML}`);
|
||||
});
|
||||
|
||||
test(`Streaming outputs interleaved with other mime types will produce separate outputs`, async () => {
|
||||
const context = createContext({ outputScrolling: false });
|
||||
const renderer = await activate(context);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { handleANSIOutput } from './ansi';
|
||||
import { OutputElementOptions, OutputWithAppend } from './rendererTypes';
|
||||
export const scrollableClass = 'scrollable';
|
||||
|
||||
const softScrollableLineLimit = 5000;
|
||||
|
@ -102,37 +103,61 @@ function scrollableArrayOfString(id: string, buffer: string[], trustHtml: boolea
|
|||
return element;
|
||||
}
|
||||
|
||||
export function createOutputContent(id: string, outputText: string, linesLimit: number, scrollable: boolean, trustHtml: boolean): HTMLElement {
|
||||
|
||||
const buffer = outputText.split(/\r\n|\r|\n/g);
|
||||
|
||||
if (scrollable) {
|
||||
return scrollableArrayOfString(id, buffer, trustHtml);
|
||||
} else {
|
||||
return truncatedArrayOfString(id, buffer, linesLimit, trustHtml);
|
||||
}
|
||||
}
|
||||
|
||||
const outputLengths: Record<string, number> = {};
|
||||
|
||||
export function appendScrollableOutput(element: HTMLElement, id: string, appended: string, fullText: string, trustHtml: boolean) {
|
||||
function appendScrollableOutput(element: HTMLElement, id: string, appended: string, trustHtml: boolean) {
|
||||
if (!outputLengths[id]) {
|
||||
outputLengths[id] = 0;
|
||||
}
|
||||
|
||||
const buffer = appended.split(/\r\n|\r|\n/g);
|
||||
const appendedLength = buffer.length + outputLengths[id];
|
||||
// Allow the output to grow to the hard limit then replace it with the last softLimit number of lines if it grows too large
|
||||
if (buffer.length + outputLengths[id] > hardScrollableLineLimit) {
|
||||
const fullBuffer = fullText.split(/\r\n|\r|\n/g);
|
||||
outputLengths[id] = Math.min(fullBuffer.length, softScrollableLineLimit);
|
||||
const newElement = scrollableArrayOfString(id, fullBuffer.slice(-1 * softScrollableLineLimit), trustHtml);
|
||||
newElement.setAttribute('output-item-id', id);
|
||||
element.replaceWith(newElement);
|
||||
// Only append outputs up to the hard limit of lines, then replace it with the last softLimit number of lines
|
||||
if (appendedLength > hardScrollableLineLimit) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
element.appendChild(handleANSIOutput(buffer.join('\n'), trustHtml));
|
||||
outputLengths[id] = appendedLength;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function createOutputContent(id: string, outputText: string, options: OutputElementOptions): HTMLElement {
|
||||
const { linesLimit, error, scrollable, trustHtml } = options;
|
||||
const buffer = outputText.split(/\r\n|\r|\n/g);
|
||||
outputLengths[id] = outputLengths[id] = Math.min(buffer.length, softScrollableLineLimit);
|
||||
|
||||
let outputElement: HTMLElement;
|
||||
if (scrollable) {
|
||||
outputElement = scrollableArrayOfString(id, buffer, !!trustHtml);
|
||||
} else {
|
||||
outputElement = truncatedArrayOfString(id, buffer, linesLimit, !!trustHtml);
|
||||
}
|
||||
|
||||
outputElement.setAttribute('output-item-id', id);
|
||||
if (error) {
|
||||
outputElement.classList.add('error');
|
||||
}
|
||||
|
||||
return outputElement;
|
||||
}
|
||||
|
||||
export function appendOutput(outputInfo: OutputWithAppend, existingContent: HTMLElement, options: OutputElementOptions) {
|
||||
const appendedText = outputInfo.appendedText?.();
|
||||
// appending output only supported for scrollable ouputs currently
|
||||
if (appendedText && options.scrollable) {
|
||||
if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newContent = createOutputContent(outputInfo.id, outputInfo.text(), options);
|
||||
existingContent.replaceWith(newContent);
|
||||
while (newContent.nextSibling) {
|
||||
// clear out any stale content if we had previously combined streaming outputs into this one
|
||||
newContent.nextSibling.remove();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue