chore: split code mirror and xterm modules (#21415)

This commit is contained in:
Pavel Feldman 2023-03-06 10:40:45 -08:00 committed by GitHub
parent 99e736afc8
commit b6ff3bad98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 237 additions and 75 deletions

View File

@ -26,10 +26,10 @@ import { createPlaywright } from '../../playwright';
import { ProgressController } from '../../progress';
import type { Page } from '../../page';
type Options = { headless?: boolean, host?: string, port?: number, watchMode?: boolean };
type Options = { app?: string, headless?: boolean, host?: string, port?: number };
export async function showTraceViewer(traceUrls: string[], browserName: string, options?: Options): Promise<Page> {
const { headless = false, host, port, watchMode } = options || {};
const { headless = false, host, port, app } = options || {};
for (const traceUrl of traceUrls) {
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
// eslint-disable-next-line no-console
@ -89,8 +89,6 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
await syncLocalStorageWithSettings(page, 'traceviewer');
const params = traceUrls.map(t => `trace=${t}`);
if (watchMode)
params.push('watchMode=true');
if (isUnderTest()) {
params.push('isUnderTest=true');
page.on('close', () => context.close(serverSideCallMetadata()).catch(() => {}));
@ -99,6 +97,6 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
}
const searchQuery = params.length ? '?' + params.join('&') : '';
await page.mainFrame().goto(serverSideCallMetadata(), urlPrefix + `/trace/index.html${searchQuery}`);
await page.mainFrame().goto(serverSideCallMetadata(), urlPrefix + `/trace/${app || 'index.html'}${searchQuery}`);
return page;
}

View File

@ -281,12 +281,18 @@ class HtmlBuilder {
if (this._hasTraces) {
const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'traceViewer');
const traceViewerTargetFolder = path.join(this._reportFolder, 'trace');
fs.mkdirSync(traceViewerTargetFolder, { recursive: true });
const traceViewerAssetsTargetFolder = path.join(traceViewerTargetFolder, 'assets');
fs.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
for (const file of fs.readdirSync(traceViewerFolder)) {
if (file.endsWith('.map'))
if (file.endsWith('.map') || file.includes('watch') || file.includes('assets'))
continue;
await copyFileAndMakeWritable(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file));
}
for (const file of fs.readdirSync(path.join(traceViewerFolder, 'assets'))) {
if (file.endsWith('.map') || file.includes('xTermModule'))
continue;
await copyFileAndMakeWritable(path.join(traceViewerFolder, 'assets', file), path.join(traceViewerAssetsTargetFolder, file));
}
}
// Inline report data.

View File

@ -65,7 +65,7 @@ class UIMode {
}
async showUI() {
this._page = await showTraceViewer([], 'chromium', { watchMode: true });
this._page = await showTraceViewer([], 'chromium', { app: 'watch.html' });
const exitPromise = new ManualPromise();
this._page.on('close', () => exitPromise.resolve());
this._page.exposeBinding('sendMessage', false, async (source, data) => {

View File

@ -0,0 +1,30 @@
{
"theme_color": "#000",
"background_color": "#fff",
"display": "browser",
"start_url": "watch.html",
"name": "Playwright Test",
"short_name": "Trace Viewer",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -19,17 +19,8 @@ import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { WatchModeView } from './ui/watchMode';
import { WorkbenchLoader } from './ui/workbench';
export const RootView: React.FC<{}> = ({
}) => {
if (window.location.href.includes('watchMode=true'))
return <WatchModeView />;
else
return <WorkbenchLoader/>;
};
(async () => {
applyTheme();
if (window.location.protocol !== 'file:') {
@ -46,5 +37,5 @@ export const RootView: React.FC<{}> = ({
setInterval(function() { fetch('ping'); }, 10000);
}
ReactDOM.render(<RootView></RootView>, document.querySelector('#root'));
ReactDOM.render(<WorkbenchLoader/>, document.querySelector('#root'));
})();

View File

@ -0,0 +1,41 @@
/**
* 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 React from 'react';
import '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom';
import { WatchModeView } from './ui/watchMode';
(async () => {
applyTheme();
if (window.location.protocol !== 'file:') {
if (window.location.href.includes('isUnderTest=true'))
await new Promise(f => setTimeout(f, 1000));
navigator.serviceWorker.register('sw.bundle.js');
if (!navigator.serviceWorker.controller) {
await new Promise<void>(f => {
navigator.serviceWorker.oncontrollerchange = () => f();
});
}
// Keep SW running.
setInterval(function() { fetch('ping'); }, 10000);
}
ReactDOM.render(<WatchModeView></WatchModeView>, document.querySelector('#root'));
})();

View File

@ -43,6 +43,7 @@ export default defineConfig({
rollupOptions: {
input: {
index: path.resolve(__dirname, 'index.html'),
watch: path.resolve(__dirname, 'watch.html'),
popout: path.resolve(__dirname, 'popout.html'),
},
output: {

View File

@ -0,0 +1,30 @@
<!--
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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" sizes="32x32" href="/icon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icon-16x16.png">
<link rel="manifest" href="/watch.webmanifest">
<title>Playwright Test</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/watch.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
/*
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 codemirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike';
export type CodeMirror = typeof codemirror;
export default codemirror;

View File

@ -16,11 +16,7 @@
import './source.css';
import * as React from 'react';
import CodeMirror from 'codemirror';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike';
import 'codemirror/lib/codemirror.css';
import type { CodeMirror } from './codeMirrorModule';
export type SourceHighlight = {
line: number;
@ -54,42 +50,53 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
onChange,
}) => {
const codemirrorElement = React.createRef<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
React.useEffect(() => {
let mode;
if (language === 'javascript')
mode = 'javascript';
if (language === 'python')
mode = 'python';
if (language === 'java')
mode = 'text/x-java';
if (language === 'csharp')
mode = 'text/x-csharp';
(async () => {
// Always load the module first.
const CodeMirror = await modulePromise;
if (codemirror && codemirror.getOption('mode') === mode && codemirror.isReadOnly() === readOnly)
return;
const element = codemirrorElement.current;
if (!element)
return;
if (!codemirrorElement.current)
return;
if (codemirror)
codemirror.getWrapperElement().remove();
let mode = 'javascript';
if (language === 'python')
mode = 'python';
if (language === 'java')
mode = 'text/x-java';
if (language === 'csharp')
mode = 'text/x-csharp';
const cm = CodeMirror(codemirrorElement.current, {
value: '',
mode,
readOnly,
lineNumbers,
lineWrapping: wrapLines,
});
if (onChange)
cm.on('change', () => onChange(cm.getValue()));
setCodemirror(cm);
updateEditor(cm, text, highlight, revealLine, focusOnChange);
}, [codemirror, codemirrorElement, text, language, highlight, revealLine, focusOnChange, lineNumbers, wrapLines, readOnly, onChange]);
if (codemirror
&& mode === codemirror.getOption('mode')
&& readOnly === codemirror.getOption('readOnly')
&& lineNumbers === codemirror.getOption('lineNumbers')
&& wrapLines === codemirror.getOption('lineWrapping')) {
updateEditor(codemirror, text, highlight, revealLine, focusOnChange);
return;
}
if (codemirror)
updateEditor(codemirror, text, highlight, revealLine, focusOnChange);
// Either configuration is different or we don't have a codemirror yet.
if (codemirror)
codemirror.getWrapperElement().remove();
const cm = CodeMirror(element, {
value: '',
mode,
readOnly,
lineNumbers,
lineWrapping: wrapLines,
});
setCodemirror(cm);
if (onChange)
cm.on('change', () => onChange(cm.getValue()));
updateEditor(cm, text, highlight, revealLine, focusOnChange);
return cm;
})();
}, [modulePromise, codemirror, codemirrorElement, text, language, highlight, revealLine, focusOnChange, lineNumbers, wrapLines, readOnly, onChange]);
return <div className='cm-wrapper' ref={codemirrorElement}></div>;
};

View File

@ -0,0 +1,27 @@
/*
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 'xterm/css/xterm.css';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
export type XTermModule = {
Terminal: typeof Terminal;
FitAddon: typeof FitAddon;
};
export default { Terminal, FitAddon };

View File

@ -15,10 +15,9 @@
*/
import * as React from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import './xtermWrapper.css';
import type { Terminal } from 'xterm';
import type { XTermModule } from './xtermModule';
export type XTermDataSource = {
pending: (string | Uint8Array)[];
@ -30,29 +29,37 @@ export const XTermWrapper: React.FC<{ source: XTermDataSource }> = ({
source
}) => {
const xtermElement = React.createRef<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<XTermModule>>(import('./xTermModule').then(m => m.default));
const [terminal, setTerminal] = React.useState<Terminal>();
React.useEffect(() => {
if (terminal)
return;
if (!xtermElement.current)
return;
const newTerminal = new Terminal({ convertEol: true });
const fitAddon = new FitAddon();
newTerminal.loadAddon(fitAddon);
for (const p of source.pending)
newTerminal.write(p);
source.write = (data => {
newTerminal.write(data);
});
newTerminal.open(xtermElement.current);
setTerminal(newTerminal);
fitAddon.fit();
const resizeObserver = new ResizeObserver(() => {
source.resize(newTerminal.cols, newTerminal.rows);
(async () => {
// Always load the module first.
const { Terminal, FitAddon } = await modulePromise;
const element = xtermElement.current;
if (!element)
return;
if (terminal)
return;
const newTerminal = new Terminal({ convertEol: true });
const fitAddon = new FitAddon();
newTerminal.loadAddon(fitAddon);
for (const p of source.pending)
newTerminal.write(p);
source.write = (data => {
newTerminal.write(data);
});
newTerminal.open(element);
fitAddon.fit();
});
resizeObserver.observe(xtermElement.current);
}, [terminal, xtermElement, source]);
setTerminal(newTerminal);
const resizeObserver = new ResizeObserver(() => {
source.resize(newTerminal.cols, newTerminal.rows);
fitAddon.fit();
});
resizeObserver.observe(element);
})();
}, [modulePromise, terminal, xtermElement, source]);
return <div className='xterm-wrapper' style={{ flex: 'auto' }} ref={xtermElement}>
</div>;
};