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 { ProgressController } from '../../progress';
import type { Page } from '../../page'; 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> { 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) { for (const traceUrl of traceUrls) {
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) { if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -89,8 +89,6 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
await syncLocalStorageWithSettings(page, 'traceviewer'); await syncLocalStorageWithSettings(page, 'traceviewer');
const params = traceUrls.map(t => `trace=${t}`); const params = traceUrls.map(t => `trace=${t}`);
if (watchMode)
params.push('watchMode=true');
if (isUnderTest()) { if (isUnderTest()) {
params.push('isUnderTest=true'); params.push('isUnderTest=true');
page.on('close', () => context.close(serverSideCallMetadata()).catch(() => {})); 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('&') : ''; 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; return page;
} }

View file

@ -281,12 +281,18 @@ class HtmlBuilder {
if (this._hasTraces) { if (this._hasTraces) {
const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'traceViewer'); const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'webpack', 'traceViewer');
const traceViewerTargetFolder = path.join(this._reportFolder, 'trace'); 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)) { for (const file of fs.readdirSync(traceViewerFolder)) {
if (file.endsWith('.map')) if (file.endsWith('.map') || file.includes('watch') || file.includes('assets'))
continue; continue;
await copyFileAndMakeWritable(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file)); 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. // Inline report data.

View file

@ -65,7 +65,7 @@ class UIMode {
} }
async showUI() { async showUI() {
this._page = await showTraceViewer([], 'chromium', { watchMode: true }); this._page = await showTraceViewer([], 'chromium', { app: 'watch.html' });
const exitPromise = new ManualPromise(); const exitPromise = new ManualPromise();
this._page.on('close', () => exitPromise.resolve()); this._page.on('close', () => exitPromise.resolve());
this._page.exposeBinding('sendMessage', false, async (source, data) => { 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 '@web/third_party/vscode/codicon.css';
import React from 'react'; import React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { WatchModeView } from './ui/watchMode';
import { WorkbenchLoader } from './ui/workbench'; import { WorkbenchLoader } from './ui/workbench';
export const RootView: React.FC<{}> = ({
}) => {
if (window.location.href.includes('watchMode=true'))
return <WatchModeView />;
else
return <WorkbenchLoader/>;
};
(async () => { (async () => {
applyTheme(); applyTheme();
if (window.location.protocol !== 'file:') { if (window.location.protocol !== 'file:') {
@ -46,5 +37,5 @@ export const RootView: React.FC<{}> = ({
setInterval(function() { fetch('ping'); }, 10000); 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: { rollupOptions: {
input: { input: {
index: path.resolve(__dirname, 'index.html'), index: path.resolve(__dirname, 'index.html'),
watch: path.resolve(__dirname, 'watch.html'),
popout: path.resolve(__dirname, 'popout.html'), popout: path.resolve(__dirname, 'popout.html'),
}, },
output: { 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 './source.css';
import * as React from 'react'; import * as React from 'react';
import CodeMirror from 'codemirror'; import type { CodeMirror } from './codeMirrorModule';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike';
import 'codemirror/lib/codemirror.css';
export type SourceHighlight = { export type SourceHighlight = {
line: number; line: number;
@ -54,42 +50,53 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
onChange, onChange,
}) => { }) => {
const codemirrorElement = React.createRef<HTMLDivElement>(); const codemirrorElement = React.createRef<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>(); const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
React.useEffect(() => { React.useEffect(() => {
let mode; (async () => {
if (language === 'javascript') // Always load the module first.
mode = 'javascript'; const CodeMirror = await modulePromise;
if (language === 'python')
mode = 'python';
if (language === 'java')
mode = 'text/x-java';
if (language === 'csharp')
mode = 'text/x-csharp';
if (codemirror && codemirror.getOption('mode') === mode && codemirror.isReadOnly() === readOnly) const element = codemirrorElement.current;
return; if (!element)
return;
if (!codemirrorElement.current) let mode = 'javascript';
return; if (language === 'python')
if (codemirror) mode = 'python';
codemirror.getWrapperElement().remove(); if (language === 'java')
mode = 'text/x-java';
if (language === 'csharp')
mode = 'text/x-csharp';
const cm = CodeMirror(codemirrorElement.current, { if (codemirror
value: '', && mode === codemirror.getOption('mode')
mode, && readOnly === codemirror.getOption('readOnly')
readOnly, && lineNumbers === codemirror.getOption('lineNumbers')
lineNumbers, && wrapLines === codemirror.getOption('lineWrapping')) {
lineWrapping: wrapLines, updateEditor(codemirror, text, highlight, revealLine, focusOnChange);
}); return;
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) // Either configuration is different or we don't have a codemirror yet.
updateEditor(codemirror, text, highlight, revealLine, focusOnChange); 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>; 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 * as React from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import './xtermWrapper.css'; import './xtermWrapper.css';
import type { Terminal } from 'xterm';
import type { XTermModule } from './xtermModule';
export type XTermDataSource = { export type XTermDataSource = {
pending: (string | Uint8Array)[]; pending: (string | Uint8Array)[];
@ -30,29 +29,37 @@ export const XTermWrapper: React.FC<{ source: XTermDataSource }> = ({
source source
}) => { }) => {
const xtermElement = React.createRef<HTMLDivElement>(); const xtermElement = React.createRef<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<XTermModule>>(import('./xTermModule').then(m => m.default));
const [terminal, setTerminal] = React.useState<Terminal>(); const [terminal, setTerminal] = React.useState<Terminal>();
React.useEffect(() => { React.useEffect(() => {
if (terminal) (async () => {
return; // Always load the module first.
if (!xtermElement.current) const { Terminal, FitAddon } = await modulePromise;
return; const element = xtermElement.current;
const newTerminal = new Terminal({ convertEol: true }); if (!element)
const fitAddon = new FitAddon(); return;
newTerminal.loadAddon(fitAddon);
for (const p of source.pending) if (terminal)
newTerminal.write(p); return;
source.write = (data => {
newTerminal.write(data); const newTerminal = new Terminal({ convertEol: true });
}); const fitAddon = new FitAddon();
newTerminal.open(xtermElement.current); newTerminal.loadAddon(fitAddon);
setTerminal(newTerminal); for (const p of source.pending)
fitAddon.fit(); newTerminal.write(p);
const resizeObserver = new ResizeObserver(() => { source.write = (data => {
source.resize(newTerminal.cols, newTerminal.rows); newTerminal.write(data);
});
newTerminal.open(element);
fitAddon.fit(); fitAddon.fit();
}); setTerminal(newTerminal);
resizeObserver.observe(xtermElement.current); const resizeObserver = new ResizeObserver(() => {
}, [terminal, xtermElement, source]); 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}> return <div className='xterm-wrapper' style={{ flex: 'auto' }} ref={xtermElement}>
</div>; </div>;
}; };