mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(tv): render error in-line (#21586)
This commit is contained in:
parent
21950bc8ce
commit
e45a496850
12
package-lock.json
generated
12
package-lock.json
generated
@ -6203,14 +6203,12 @@
|
|||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
},
|
},
|
||||||
"packages/trace-viewer": {
|
"packages/trace-viewer": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0"
|
||||||
"dependencies": {
|
|
||||||
"ansi-to-html": "^0.7.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
"codemirror": "^5.65.9",
|
"codemirror": "^5.65.9",
|
||||||
"xterm": "^5.1.0",
|
"xterm": "^5.1.0",
|
||||||
"xterm-addon-fit": "^0.7.0"
|
"xterm-addon-fit": "^0.7.0"
|
||||||
@ -9704,10 +9702,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trace-viewer": {
|
"trace-viewer": {
|
||||||
"version": "file:packages/trace-viewer",
|
"version": "file:packages/trace-viewer"
|
||||||
"requires": {
|
|
||||||
"ansi-to-html": "^0.7.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"tree-kill": {
|
"tree-kill": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
@ -9886,6 +9881,7 @@
|
|||||||
"web": {
|
"web": {
|
||||||
"version": "file:packages/web",
|
"version": "file:packages/web",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
"codemirror": "^5.65.9",
|
"codemirror": "^5.65.9",
|
||||||
"xterm": "^5.1.0",
|
"xterm": "^5.1.0",
|
||||||
"xterm-addon-fit": "^0.7.0"
|
"xterm-addon-fit": "^0.7.0"
|
||||||
|
@ -56,14 +56,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.call-log-call.error {
|
.call-log-call.error {
|
||||||
background-color: #fff0f0;
|
background-color: var(--vscode-inputValidation-errorBackground);
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-log-call.error .call-log-call-header,
|
.call-log-call.error .call-log-call-header,
|
||||||
.call-log-message.error,
|
.call-log-message.error,
|
||||||
.call-log .codicon-error {
|
.call-log .codicon-error {
|
||||||
color: red;
|
color: var(--vscode-errorForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-log-details {
|
.call-log-details {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import type { CallLog, Mode, Source } from './recorderTypes';
|
import type { CallLog, Mode, Source } from './recorderTypes';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import { Source as SourceView } from '@web/components/source';
|
|
||||||
import { SplitView } from '@web/components/splitView';
|
import { SplitView } from '@web/components/splitView';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
@ -134,7 +133,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<SplitView sidebarSize={200} sidebarHidden={mode === 'recording'}>
|
<SplitView sidebarSize={200} sidebarHidden={mode === 'recording'}>
|
||||||
<SourceView text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine}></SourceView>
|
<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true}/>
|
||||||
<div className='vbox'>
|
<div className='vbox'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarButton icon='microscope' title='Pick locator' toggled={mode === 'inspecting'} onClick={() => {
|
<ToolbarButton icon='microscope' title='Pick locator' toggled={mode === 'inspecting'} onClick={() => {
|
||||||
@ -143,7 +142,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||||||
<CodeMirrorWrapper text={locator} language={source.language} readOnly={false} focusOnChange={true} wrapLines={true} onChange={text => {
|
<CodeMirrorWrapper text={locator} language={source.language} readOnly={false} focusOnChange={true} wrapLines={true} onChange={text => {
|
||||||
setLocator(text);
|
setLocator(text);
|
||||||
window.dispatch({ event: 'selectorUpdated', params: { selector: text, language: source.language } });
|
window.dispatch({ event: 'selectorUpdated', params: { selector: text, language: source.language } });
|
||||||
}}></CodeMirrorWrapper>
|
}} />
|
||||||
<ToolbarButton icon='files' title='Copy' onClick={() => {
|
<ToolbarButton icon='files' title='Copy' onClick={() => {
|
||||||
copy(locator);
|
copy(locator);
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
|
@ -7,8 +7,5 @@
|
|||||||
"build": "vite build && tsc",
|
"build": "vite build && tsc",
|
||||||
"build-sw": "vite --config vite.sw.config.ts build && tsc",
|
"build-sw": "vite --config vite.sw.config.ts build && tsc",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-to-html": "^0.7.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-icons:hover {
|
.action-icons:hover {
|
||||||
border-bottom: 1px solid white;
|
border-bottom: 1px solid var(--vscode-sideBarTitle-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-error {
|
.action-error {
|
||||||
color: red;
|
color: var(--vscode-errorForeground);
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.call-error .codicon {
|
.call-error .codicon {
|
||||||
color: red;
|
color: var(--vscode-errorForeground);
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
@ -22,7 +22,7 @@ import './callTab.css';
|
|||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
import { ErrorMessage } from './errorMessage';
|
import { ErrorMessage } from '@web/components/errorMessage';
|
||||||
|
|
||||||
export const CallTab: React.FunctionComponent<{
|
export const CallTab: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
|
@ -33,16 +33,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.console-line.error {
|
.console-line.error {
|
||||||
background: #fff0f0;
|
background: var(--vscode-inputValidation-errorBackground);
|
||||||
border-top-color: #ffd6d6;
|
border-top-color: var(--vscode-inputValidation-errorBorder);
|
||||||
border-bottom-color: #ffd6d6;
|
border-bottom-color: var(--vscode-inputValidation-errorBorder);
|
||||||
color: red;
|
color: var(--vscode-errorForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line.warning {
|
.console-line.warning {
|
||||||
background: #fffbe5;
|
background: var(--vscode-inputValidation-warningBackground);
|
||||||
border-top-color: #fff5c2;
|
border-top-color: var(--vscode-inputValidation-warningBorder);
|
||||||
border-bottom-color: #fff5c2;
|
border-bottom-color: var(--vscode-inputValidation-warningBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line .codicon {
|
.console-line .codicon {
|
||||||
|
@ -16,14 +16,14 @@
|
|||||||
|
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import { Source as SourceView } from '@web/components/source';
|
|
||||||
import { SplitView } from '@web/components/splitView';
|
import { SplitView } from '@web/components/splitView';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useAsyncMemo } from './helpers';
|
import { useAsyncMemo } from './helpers';
|
||||||
import './sourceTab.css';
|
import './sourceTab.css';
|
||||||
import { StackTraceView } from './stackTrace';
|
import { StackTraceView } from './stackTrace';
|
||||||
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
|
|
||||||
type StackInfo = string | {
|
type StackInfo = {
|
||||||
frames: StackFrame[];
|
frames: StackFrame[];
|
||||||
fileContent: Map<string, string>;
|
fileContent: Map<string, string>;
|
||||||
};
|
};
|
||||||
@ -33,17 +33,15 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
}> = ({ action }) => {
|
}> = ({ action }) => {
|
||||||
const [lastAction, setLastAction] = React.useState<ActionTraceEvent | undefined>();
|
const [lastAction, setLastAction] = React.useState<ActionTraceEvent | undefined>();
|
||||||
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
||||||
const [needReveal, setNeedReveal] = React.useState<boolean>(false);
|
|
||||||
|
|
||||||
if (lastAction !== action) {
|
if (lastAction !== action) {
|
||||||
setLastAction(action);
|
setLastAction(action);
|
||||||
setSelectedFrame(0);
|
setSelectedFrame(0);
|
||||||
setNeedReveal(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stackInfo = React.useMemo<StackInfo>(() => {
|
const stackInfo = React.useMemo<StackInfo>(() => {
|
||||||
if (!action)
|
if (!action)
|
||||||
return '';
|
return { frames: [], fileContent: new Map() };
|
||||||
const frames = action.stack || [];
|
const frames = action.stack || [];
|
||||||
return {
|
return {
|
||||||
frames,
|
frames,
|
||||||
@ -52,34 +50,20 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
}, [action]);
|
}, [action]);
|
||||||
|
|
||||||
const content = useAsyncMemo<string>(async () => {
|
const content = useAsyncMemo<string>(async () => {
|
||||||
let value: string;
|
const filePath = stackInfo.frames[selectedFrame]?.file;
|
||||||
if (typeof stackInfo === 'string') {
|
if (!filePath)
|
||||||
value = stackInfo;
|
return '';
|
||||||
} else {
|
if (!stackInfo.fileContent.has(filePath)) {
|
||||||
const filePath = stackInfo.frames[selectedFrame]?.file;
|
const sha1 = await calculateSha1(filePath);
|
||||||
if (!filePath)
|
stackInfo.fileContent.set(filePath, await fetch(`sha1/src@${sha1}.txt`).then(response => response.text()).catch(() => `<Unable to read "${filePath}">`));
|
||||||
return '';
|
|
||||||
if (!stackInfo.fileContent.has(filePath)) {
|
|
||||||
const sha1 = await calculateSha1(filePath);
|
|
||||||
stackInfo.fileContent.set(filePath, await fetch(`sha1/src@${sha1}.txt`).then(response => response.text()).catch(e => `<Unable to read "${filePath}">`));
|
|
||||||
}
|
|
||||||
value = stackInfo.fileContent.get(filePath)!;
|
|
||||||
}
|
}
|
||||||
return value;
|
return stackInfo.fileContent.get(filePath)!;
|
||||||
}, [stackInfo, selectedFrame], '');
|
}, [stackInfo, selectedFrame], '');
|
||||||
|
|
||||||
const targetLine = typeof stackInfo === 'string' ? 0 : stackInfo.frames[selectedFrame]?.line || 0;
|
const targetLine = stackInfo.frames[selectedFrame]?.line || 0;
|
||||||
|
const error = action?.error?.message;
|
||||||
const targetLineRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
if (needReveal && targetLineRef.current) {
|
|
||||||
targetLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
||||||
setNeedReveal(false);
|
|
||||||
}
|
|
||||||
}, [needReveal, targetLineRef]);
|
|
||||||
|
|
||||||
return <SplitView sidebarSize={200} orientation='horizontal'>
|
return <SplitView sidebarSize={200} orientation='horizontal'>
|
||||||
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
<CodeMirrorWrapper text={content} language='javascript' highlight={[{ line: targetLine, type: error ? 'error' : 'running', message: error }]} revealLine={targetLine} readOnly={true} lineNumbers={true}></CodeMirrorWrapper>
|
||||||
<StackTraceView action={action} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
<StackTraceView action={action} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
||||||
</SplitView>;
|
</SplitView>;
|
||||||
};
|
};
|
||||||
|
@ -49,9 +49,9 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = [
|
const tabs: TabbedPaneTabModel[] = [
|
||||||
{ id: 'call', title: 'Call', render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} /> },
|
{ id: 'call', title: 'Call', render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} /> },
|
||||||
|
{ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={activeAction} /> },
|
||||||
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
|
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
|
||||||
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
|
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
|
||||||
{ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={activeAction} /> },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (output)
|
if (output)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
"codemirror": "^5.65.9",
|
"codemirror": "^5.65.9",
|
||||||
"xterm": "^5.1.0",
|
"xterm": "^5.1.0",
|
||||||
"xterm-addon-fit": "^0.7.0"
|
"xterm-addon-fit": "^0.7.0"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
[expandable.spec.tsx]
|
[expandable.spec.tsx]
|
||||||
***
|
***
|
||||||
|
|
||||||
[source.spec.tsx]
|
[codeMirrorWrapper.spec.tsx]
|
||||||
***
|
***
|
||||||
|
|
||||||
[splitView.spec.tsx]
|
[splitView.spec.tsx]
|
||||||
|
@ -118,6 +118,7 @@ body.dark-mode .CodeMirror span.cm-type {
|
|||||||
|
|
||||||
.CodeMirror .CodeMirror-gutters {
|
.CodeMirror .CodeMirror-gutters {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
background: var(--vscode-editorGutter-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror .CodeMirror-gutterwrapper {
|
.CodeMirror .CodeMirror-gutterwrapper {
|
||||||
@ -138,3 +139,31 @@ body.dark-mode .CodeMirror span.cm-type {
|
|||||||
font-weight: var(--vscode-editor-font-weight) !important;
|
font-weight: var(--vscode-editor-font-weight) !important;
|
||||||
font-size: var(--vscode-editor-font-size) !important;
|
font-size: var(--vscode-editor-font-size) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror .source-line-running {
|
||||||
|
background-color: #b3dbff7f;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .source-line-paused {
|
||||||
|
background-color: #b3dbff7f;
|
||||||
|
outline: 1px solid #008aff;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .source-line-error {
|
||||||
|
/* Intentionally empty. */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .source-line-error-underline {
|
||||||
|
text-decoration: underline wavy var(--vscode-errorForeground);
|
||||||
|
position: relative;
|
||||||
|
top: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .source-line-error-widget {
|
||||||
|
background-color: var(--vscode-inputValidation-errorBackground);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin: 3px 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { expect, test } from '@playwright/experimental-ct-react';
|
import { expect, test } from '@playwright/experimental-ct-react';
|
||||||
import { Source } from './source';
|
import { CodeMirrorWrapper } from './codeMirrorWrapper';
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } });
|
test.use({ viewport: { width: 500, height: 500 } });
|
||||||
|
|
||||||
@ -70,31 +70,31 @@ class Program
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test('highlight JavaScript', async ({ mount }) => {
|
test('highlight JavaScript', async ({ mount }) => {
|
||||||
const component = await mount(<Source text={javascriptSnippet} language='javascript'></Source>);
|
const component = await mount(<CodeMirrorWrapper text={javascriptSnippet} language='javascript' />);
|
||||||
await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword');
|
await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('highlight Python', async ({ mount }) => {
|
test('highlight Python', async ({ mount }) => {
|
||||||
const component = await mount(<Source text={pythonSnippet} language='python'></Source>);
|
const component = await mount(<CodeMirrorWrapper text={pythonSnippet} language='python' />);
|
||||||
await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword');
|
await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('highlight Java', async ({ mount }) => {
|
test('highlight Java', async ({ mount }) => {
|
||||||
const component = await mount(<Source text={javaSnippet} language='java'></Source>);
|
const component = await mount(<CodeMirrorWrapper text={javaSnippet} language='java' />);
|
||||||
await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword');
|
await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('highlight C#', async ({ mount }) => {
|
test('highlight C#', async ({ mount }) => {
|
||||||
const component = await mount(<Source text={csharpSnippet} language='csharp'></Source>);
|
const component = await mount(<CodeMirrorWrapper text={csharpSnippet} language='csharp' />);
|
||||||
await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword');
|
await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('highlight lines', async ({ mount }) => {
|
test('highlight lines', async ({ mount }) => {
|
||||||
const component = await mount(<Source text={javascriptSnippet} language='javascript' highlight={[
|
const component = await mount(<CodeMirrorWrapper text={javascriptSnippet} language='javascript' highlight={[
|
||||||
{ line: 4, type: 'running' },
|
{ line: 4, type: 'running' },
|
||||||
{ line: 5, type: 'paused' },
|
{ line: 5, type: 'paused' },
|
||||||
{ line: 6, type: 'error' },
|
{ line: 6, type: 'error' },
|
||||||
]}></Source>);
|
]} />);
|
||||||
await expect(component.locator('.source-line-running')).toContainText('goto');
|
await expect(component.locator('.source-line-running')).toContainText('goto');
|
||||||
await expect(component.locator('.source-line-paused')).toContainText('title');
|
await expect(component.locator('.source-line-paused')).toContainText('title');
|
||||||
await expect(component.locator('.source-line-error')).toContainText('expect');
|
await expect(component.locator('.source-line-error')).toContainText('expect');
|
@ -14,13 +14,15 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './source.css';
|
import './codeMirrorWrapper.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { CodeMirror } from './codeMirrorModule';
|
import type { CodeMirror } from './codeMirrorModule';
|
||||||
|
import { ansi2htmlMarkup } from './errorMessage';
|
||||||
|
|
||||||
export type SourceHighlight = {
|
export type SourceHighlight = {
|
||||||
line: number;
|
line: number;
|
||||||
type: 'running' | 'paused' | 'error';
|
type: 'running' | 'paused' | 'error';
|
||||||
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
||||||
@ -28,7 +30,7 @@ export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
|||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string;
|
text: string;
|
||||||
language: Language;
|
language: Language;
|
||||||
readOnly: boolean;
|
readOnly?: boolean;
|
||||||
// 1-based
|
// 1-based
|
||||||
highlight?: SourceHighlight[];
|
highlight?: SourceHighlight[];
|
||||||
revealLine?: number;
|
revealLine?: number;
|
||||||
@ -51,7 +53,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const codemirrorElement = React.useRef<HTMLDivElement>(null);
|
const codemirrorElement = React.useRef<HTMLDivElement>(null);
|
||||||
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
|
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
|
||||||
const codemirrorRef = React.useRef<CodeMirror.Editor|null>(null);
|
const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight: SourceHighlight[], widgets: CodeMirror.LineWidget[] } | null>(null);
|
||||||
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
|
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -72,25 +74,25 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||||||
mode = 'text/x-csharp';
|
mode = 'text/x-csharp';
|
||||||
|
|
||||||
if (codemirrorRef.current
|
if (codemirrorRef.current
|
||||||
&& mode === codemirrorRef.current.getOption('mode')
|
&& mode === codemirrorRef.current.cm.getOption('mode')
|
||||||
&& readOnly === codemirrorRef.current.getOption('readOnly')
|
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
|
||||||
&& lineNumbers === codemirrorRef.current.getOption('lineNumbers')
|
&& lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers')
|
||||||
&& wrapLines === codemirrorRef.current.getOption('lineWrapping')) {
|
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')) {
|
||||||
// No need to re-create codemirror.
|
// No need to re-create codemirror.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either configuration is different or we don't have a codemirror yet.
|
// Either configuration is different or we don't have a codemirror yet.
|
||||||
codemirrorRef.current?.getWrapperElement().remove();
|
codemirrorRef.current?.cm?.getWrapperElement().remove();
|
||||||
|
|
||||||
const cm = CodeMirror(element, {
|
const cm = CodeMirror(element, {
|
||||||
value: '',
|
value: '',
|
||||||
mode,
|
mode,
|
||||||
readOnly,
|
readOnly: !!readOnly,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
lineWrapping: wrapLines,
|
lineWrapping: wrapLines,
|
||||||
});
|
});
|
||||||
codemirrorRef.current = cm;
|
codemirrorRef.current = { cm, highlight: [], widgets: [] };
|
||||||
setCodemirror(cm);
|
setCodemirror(cm);
|
||||||
return cm;
|
return cm;
|
||||||
})();
|
})();
|
||||||
@ -113,10 +115,37 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||||||
codemirror.focus();
|
codemirror.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < codemirror.lineCount(); ++i)
|
|
||||||
codemirror.removeLineClass(i, 'wrap');
|
// Line highlight.
|
||||||
|
for (const h of codemirrorRef.current!.highlight)
|
||||||
|
codemirror.removeLineClass(h.line - 1, 'wrap');
|
||||||
for (const h of highlight || [])
|
for (const h of highlight || [])
|
||||||
codemirror.addLineClass(h.line - 1, 'wrap', `source-line-${h.type}`);
|
codemirror.addLineClass(h.line - 1, 'wrap', `source-line-${h.type}`);
|
||||||
|
codemirrorRef.current!.highlight = highlight || [];
|
||||||
|
|
||||||
|
// Error widgets.
|
||||||
|
for (const w of codemirrorRef.current!.widgets)
|
||||||
|
codemirror.removeLineWidget(w);
|
||||||
|
const widgets: CodeMirror.LineWidget[] = [];
|
||||||
|
for (const h of highlight || []) {
|
||||||
|
if (h.type !== 'error')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const line = codemirrorRef.current?.cm.getLine(h.line - 1);
|
||||||
|
if (line) {
|
||||||
|
const underlineWidgetElement = document.createElement('div');
|
||||||
|
underlineWidgetElement.className = 'source-line-error-underline';
|
||||||
|
underlineWidgetElement.innerHTML = ' '.repeat(line.length || 1);
|
||||||
|
widgets.push(codemirror.addLineWidget(h.line, underlineWidgetElement, { above: true, coverGutter: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorWidgetElement = document.createElement('div');
|
||||||
|
errorWidgetElement.innerHTML = ansi2htmlMarkup(h.message || '');
|
||||||
|
errorWidgetElement.className = 'source-line-error-widget';
|
||||||
|
widgets.push(codemirror.addLineWidget(h.line, errorWidgetElement, { above: true, coverGutter: false }));
|
||||||
|
}
|
||||||
|
codemirrorRef.current!.widgets = widgets;
|
||||||
|
|
||||||
if (revealLine)
|
if (revealLine)
|
||||||
codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50);
|
codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50);
|
||||||
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
||||||
|
@ -21,17 +21,19 @@ import './errorMessage.css';
|
|||||||
export const ErrorMessage: React.FC<{
|
export const ErrorMessage: React.FC<{
|
||||||
error: string;
|
error: string;
|
||||||
}> = ({ error }) => {
|
}> = ({ error }) => {
|
||||||
const html = React.useMemo(() => {
|
const html = React.useMemo(() => ansi2htmlMarkup(error), [error]);
|
||||||
const config: any = {
|
|
||||||
bg: 'var(--vscode-panel-background)',
|
|
||||||
fg: 'var(--vscode-foreground)',
|
|
||||||
};
|
|
||||||
config.colors = ansiColors;
|
|
||||||
return new ansi2html(config).toHtml(escapeHTML(error));
|
|
||||||
}, [error]);
|
|
||||||
return <div className='error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
return <div className='error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function ansi2htmlMarkup(text: string) {
|
||||||
|
const config: any = {
|
||||||
|
bg: 'var(--vscode-panel-background)',
|
||||||
|
fg: 'var(--vscode-foreground)',
|
||||||
|
};
|
||||||
|
config.colors = ansiColors;
|
||||||
|
return new ansi2html(config).toHtml(escapeHTML(text));
|
||||||
|
}
|
||||||
|
|
||||||
const ansiColors = {
|
const ansiColors = {
|
||||||
0: '#000',
|
0: '#000',
|
||||||
1: '#C00',
|
1: '#C00',
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@import '../third_party/vscode/colors.css';
|
|
||||||
|
|
||||||
.source {
|
|
||||||
display: flex;
|
|
||||||
flex: auto;
|
|
||||||
flex-direction: column;
|
|
||||||
white-space: pre;
|
|
||||||
overflow: auto;
|
|
||||||
user-select: text;
|
|
||||||
font-family: var(--vscode-editor-font-family);
|
|
||||||
font-weight: var(--vscode-editor-font-weight);
|
|
||||||
line-height: 19px;
|
|
||||||
background: var(--vscode-editor-background);
|
|
||||||
color: var(--vscode-editor-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-line-running {
|
|
||||||
background-color: #b3dbff7f;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-line-paused {
|
|
||||||
background-color: #b3dbff7f;
|
|
||||||
outline: 1px solid #008aff;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-line-error {
|
|
||||||
background-color: #fff0f0;
|
|
||||||
outline: 1px solid #ff5656;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import './codeMirrorWrapper.css';
|
|
||||||
import type { Language } from './codeMirrorWrapper';
|
|
||||||
import { CodeMirrorWrapper } from './codeMirrorWrapper';
|
|
||||||
|
|
||||||
export type SourceHighlight = {
|
|
||||||
line: number;
|
|
||||||
type: 'running' | 'paused' | 'error';
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SourceProps {
|
|
||||||
text: string;
|
|
||||||
language: Language;
|
|
||||||
// 1-based
|
|
||||||
highlight?: SourceHighlight[];
|
|
||||||
revealLine?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Source: React.FC<SourceProps> = ({
|
|
||||||
text,
|
|
||||||
language,
|
|
||||||
highlight = [],
|
|
||||||
revealLine
|
|
||||||
}) => {
|
|
||||||
return <CodeMirrorWrapper text={text} language={language} readOnly={true} highlight={highlight} revealLine={revealLine} lineNumbers={true}></CodeMirrorWrapper>;
|
|
||||||
};
|
|
@ -294,7 +294,7 @@ it.describe('pause', () => {
|
|||||||
})().catch(e => e);
|
})().catch(e => e);
|
||||||
const recorderPage = await recorderPageGetter();
|
const recorderPage = await recorderPageGetter();
|
||||||
await recorderPage.click('[title="Resume (F8)"]');
|
await recorderPage.click('[title="Resume (F8)"]');
|
||||||
await recorderPage.waitForSelector('.source-line-error');
|
await recorderPage.waitForSelector('.source-line-error-underline');
|
||||||
expect(await sanitizeLog(recorderPage)).toEqual([
|
expect(await sanitizeLog(recorderPage)).toEqual([
|
||||||
'page.pause- XXms',
|
'page.pause- XXms',
|
||||||
'page.getByRole(\'button\').isChecked()- XXms',
|
'page.getByRole(\'button\').isChecked()- XXms',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user