feat(cli): bring in trace viewer (#4920)

This commit is contained in:
Dmitry Gozman 2021-01-07 16:15:34 -08:00 коммит произвёл GitHub
Родитель 54c06a1b45
Коммит 2e05feac25
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
43 изменённых файлов: 3583 добавлений и 4 удалений

Просмотреть файл

@ -5,6 +5,9 @@ module.exports = {
ecmaVersion: 9,
sourceType: 'module',
},
extends: [
'plugin:react-hooks/recommended'
],
/**
* ESLint rules

807
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -58,22 +58,32 @@
"@types/progress": "^2.0.3",
"@types/proper-lockfile": "^4.1.1",
"@types/proxy-from-env": "^1.0.1",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/resize-observer-browser": "^0.1.4",
"@types/rimraf": "^3.0.0",
"@types/ws": "7.2.6",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"chokidar": "^3.5.0",
"css-loader": "^4.3.0",
"colors": "^1.4.0",
"commonmark": "^0.29.1",
"cross-env": "^7.0.2",
"electron": "^9.2.1",
"eslint": "^7.7.0",
"eslint-plugin-notice": "^0.9.10",
"eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "^6.1.0",
"folio": "=0.3.16",
"formidable": "^1.2.2",
"html-webpack-plugin": "^4.4.1",
"ncp": "^2.0.0",
"node-stream-zip": "^1.11.3",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"socksv5": "0.0.6",
"style-loader": "^1.2.1",
"ts-loader": "^8.0.3",
"typescript": "^4.0.2",
"webpack": "^4.44.2",

Просмотреть файл

@ -30,6 +30,7 @@ import { PythonLanguageGenerator } from './codegen/languages/python';
import { CSharpLanguageGenerator } from './codegen/languages/csharp';
import { RecorderController } from './codegen/recorderController';
import { runServer, printApiJson, installBrowsers } from './driver';
import { showTraceViewer } from './traceViewer/traceViewer';
import type { Browser, BrowserContext, Page, BrowserType, BrowserContextOptions, LaunchOptions } from '../..';
import * as playwright from '../..';
@ -136,6 +137,22 @@ program
});
});
if (process.env.PWTRACE) {
program
.command('show-trace <trace>')
.description('Show trace viewer')
.option('--resources <dir>', 'Directory with the shared trace artifacts')
.action(function(trace, command) {
showTraceViewer(command.resources, trace);
}).on('--help', function() {
console.log('');
console.log('Examples:');
console.log('');
console.log(' $ show-trace --resources=resources trace/file.trace');
console.log(' $ show-trace trace/directory');
});
}
if (process.argv[2] === 'run-driver')
runServer();
else if (process.argv[2] === 'print-api-json')

Просмотреть файл

@ -20,6 +20,7 @@ const InlineSource = require('../../server/injected/webpack-inline-source-plugin
module.exports = {
entry: path.join(__dirname, 'recorder.ts'),
devtool: 'source-map',
mode: 'development',
module: {
rules: [
{

Просмотреть файл

@ -0,0 +1,136 @@
/**
* 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 fs from 'fs';
import * as path from 'path';
import * as playwright from '../../..';
import * as util from 'util';
import { SnapshotRouter } from './snapshotRouter';
import { actionById, ActionEntry, ContextEntry, TraceModel } from './traceModel';
import type { PageSnapshot } from '../../trace/traceTypes';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
export class ScreenshotGenerator {
private _traceStorageDir: string;
private _browserPromise: Promise<playwright.Browser> | undefined;
private _traceModel: TraceModel;
private _rendering = new Map<ActionEntry, Promise<Buffer | undefined>>();
constructor(traceStorageDir: string, traceModel: TraceModel) {
this._traceStorageDir = traceStorageDir;
this._traceModel = traceModel;
}
async generateScreenshot(actionId: string): Promise<Buffer | undefined> {
const { context, action } = actionById(this._traceModel, actionId);
if (!action.action.snapshot)
return;
const imageFileName = path.join(this._traceStorageDir, action.action.snapshot.sha1 + '-thumbnail.png');
let body: Buffer | undefined;
try {
body = await fsReadFileAsync(imageFileName);
} catch (e) {
if (!this._rendering.has(action)) {
this._rendering.set(action, this._render(context, action, imageFileName).then(body => {
this._rendering.delete(action);
return body;
}));
}
body = await this._rendering.get(action)!;
}
return body;
}
private _browser() {
if (!this._browserPromise)
this._browserPromise = playwright.chromium.launch();
return this._browserPromise;
}
private async _render(contextEntry: ContextEntry, actionEntry: ActionEntry, imageFileName: string): Promise<Buffer | undefined> {
const { action } = actionEntry;
const browser = await this._browser();
const page = await browser.newPage({
viewport: contextEntry.created.viewportSize,
deviceScaleFactor: contextEntry.created.deviceScaleFactor
});
try {
const snapshotPath = path.join(this._traceStorageDir, action.snapshot!.sha1);
let snapshot;
try {
snapshot = await fsReadFileAsync(snapshotPath, 'utf8');
} catch (e) {
console.log(`Unable to read snapshot at ${snapshotPath}`); // eslint-disable-line no-console
return;
}
const snapshotObject = JSON.parse(snapshot) as PageSnapshot;
const snapshotRouter = new SnapshotRouter(this._traceStorageDir);
snapshotRouter.selectSnapshot(snapshotObject, contextEntry);
page.route('**/*', route => snapshotRouter.route(route));
const url = snapshotObject.frames[0].url;
console.log('Generating screenshot for ' + action.action, snapshotObject.frames[0].url); // eslint-disable-line no-console
await page.goto(url);
let clip: any = undefined;
const element = await page.$(action.selector || '*[__playwright_target__]');
if (element) {
await element.evaluate(e => {
e.style.backgroundColor = '#ff69b460';
});
clip = await element.boundingBox() || undefined;
if (clip) {
const thumbnailSize = {
width: 400,
height: 200
};
const insets = {
width: 60,
height: 30
};
clip.width = Math.min(thumbnailSize.width, clip.width);
clip.height = Math.min(thumbnailSize.height, clip.height);
if (clip.width < thumbnailSize.width) {
clip.x -= (thumbnailSize.width - clip.width) / 2;
clip.x = Math.max(0, clip.x);
clip.width = thumbnailSize.width;
} else {
clip.x = Math.max(0, clip.x - insets.width);
}
if (clip.height < thumbnailSize.height) {
clip.y -= (thumbnailSize.height - clip.height) / 2;
clip.y = Math.max(0, clip.y);
clip.height = thumbnailSize.height;
} else {
clip.y = Math.max(0, clip.y - insets.height);
}
}
}
const imageData = await page.screenshot({ clip });
await fsWriteFileAsync(imageFileName, imageData);
return imageData;
} catch (e) {
console.log(e); // eslint-disable-line no-console
} finally {
await page.close();
}
}
}

Просмотреть файл

@ -0,0 +1,122 @@
/**
* 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 fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import type { Route } from '../../..';
import type { FrameSnapshot, NetworkResourceTraceEvent, PageSnapshot } from '../../trace/traceTypes';
import { ContextEntry } from './traceModel';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
export class SnapshotRouter {
private _contextEntry: ContextEntry | undefined;
private _unknownUrls = new Set<string>();
private _traceStorageDir: string;
private _frameBySrc = new Map<string, FrameSnapshot>();
constructor(traceStorageDir: string) {
this._traceStorageDir = traceStorageDir;
}
selectSnapshot(snapshot: PageSnapshot, contextEntry: ContextEntry) {
this._frameBySrc.clear();
this._contextEntry = contextEntry;
for (const frameSnapshot of snapshot.frames)
this._frameBySrc.set(frameSnapshot.url, frameSnapshot);
}
async route(route: Route) {
const url = route.request().url();
if (this._frameBySrc.has(url)) {
const frameSnapshot = this._frameBySrc.get(url)!;
route.fulfill({
contentType: 'text/html',
body: Buffer.from(frameSnapshot.html),
});
return;
}
const frameSrc = route.request().frame().url();
const frameSnapshot = this._frameBySrc.get(frameSrc);
if (!frameSnapshot)
return this._routeUnknown(route);
// Find a matching resource from the same context, preferrably from the same frame.
// Note: resources are stored without hash, but page may reference them with hash.
let resource: NetworkResourceTraceEvent | null = null;
const resourcesWithUrl = this._contextEntry!.resourcesByUrl.get(removeHash(url)) || [];
for (const resourceEvent of resourcesWithUrl) {
if (resource && resourceEvent.frameId !== frameSnapshot.frameId)
continue;
resource = resourceEvent;
if (resourceEvent.frameId === frameSnapshot.frameId)
break;
}
if (!resource)
return this._routeUnknown(route);
// This particular frame might have a resource content override, for example when
// stylesheet is modified using CSSOM.
const resourceOverride = frameSnapshot.resourceOverrides.find(o => o.url === url);
const overrideSha1 = resourceOverride ? resourceOverride.sha1 : undefined;
const resourceData = await this._readResource(resource, overrideSha1);
if (!resourceData)
return this._routeUnknown(route);
const headers: { [key: string]: string } = {};
for (const { name, value } of resourceData.headers)
headers[name] = value;
headers['Access-Control-Allow-Origin'] = '*';
route.fulfill({
contentType: resourceData.contentType,
body: resourceData.body,
headers,
});
}
private _routeUnknown(route: Route) {
const url = route.request().url();
if (!this._unknownUrls.has(url)) {
console.log(`Request to unknown url: ${url}`); /* eslint-disable-line no-console */
this._unknownUrls.add(url);
}
route.abort();
}
private async _readResource(event: NetworkResourceTraceEvent, overrideSha1: string | undefined) {
try {
const body = await fsReadFileAsync(path.join(this._traceStorageDir, overrideSha1 || event.sha1));
return {
contentType: event.contentType,
body,
headers: event.responseHeaders,
};
} catch (e) {
return undefined;
}
}
}
function removeHash(url: string) {
try {
const u = new URL(url);
u.hash = '';
return u.toString();
} catch (e) {
return url;
}
}

Просмотреть файл

@ -0,0 +1,154 @@
/**
* 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 trace from '../../trace/traceTypes';
export type TraceModel = {
contexts: ContextEntry[];
}
export type ContextEntry = {
name: string;
filePath: string;
startTime: number;
endTime: number;
created: trace.ContextCreatedTraceEvent;
destroyed: trace.ContextDestroyedTraceEvent;
pages: PageEntry[];
resourcesByUrl: Map<string, trace.NetworkResourceTraceEvent[]>;
}
export type VideoEntry = {
video: trace.PageVideoTraceEvent;
videoId: string;
};
export type PageEntry = {
created: trace.PageCreatedTraceEvent;
destroyed: trace.PageDestroyedTraceEvent;
video?: VideoEntry;
actions: ActionEntry[];
resources: trace.NetworkResourceTraceEvent[];
}
export type ActionEntry = {
actionId: string;
action: trace.ActionTraceEvent;
resources: trace.NetworkResourceTraceEvent[];
};
export type VideoMetaInfo = {
frames: number;
width: number;
height: number;
fps: number;
startTime: number;
endTime: number;
};
export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) {
const contextEntries = new Map<string, ContextEntry>();
const pageEntries = new Map<string, PageEntry>();
for (const event of events) {
switch (event.type) {
case 'context-created': {
contextEntries.set(event.contextId, {
filePath,
name: filePath.substring(filePath.lastIndexOf('/') + 1),
startTime: Number.MAX_VALUE,
endTime: Number.MIN_VALUE,
created: event,
destroyed: undefined as any,
pages: [],
resourcesByUrl: new Map(),
});
break;
}
case 'context-destroyed': {
contextEntries.get(event.contextId)!.destroyed = event;
break;
}
case 'page-created': {
const pageEntry: PageEntry = {
created: event,
destroyed: undefined as any,
actions: [],
resources: [],
};
pageEntries.set(event.pageId, pageEntry);
contextEntries.get(event.contextId)!.pages.push(pageEntry);
break;
}
case 'page-destroyed': {
pageEntries.get(event.pageId)!.destroyed = event;
break;
}
case 'page-video': {
const pageEntry = pageEntries.get(event.pageId)!;
pageEntry.video = { video: event, videoId: event.contextId + '/' + event.pageId };
break;
}
case 'action': {
const pageEntry = pageEntries.get(event.pageId!)!;
const action: ActionEntry = {
actionId: event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length,
action: event,
resources: pageEntry.resources,
};
pageEntry.resources = [];
pageEntry.actions.push(action);
break;
}
case 'resource': {
const contextEntry = contextEntries.get(event.contextId)!;
const pageEntry = pageEntries.get(event.pageId!)!;
const action = pageEntry.actions[pageEntry.actions.length - 1];
if (action)
action.resources.push(event);
else
pageEntry.resources.push(event);
let responseEvents = contextEntry.resourcesByUrl.get(event.url);
if (!responseEvents) {
responseEvents = [];
contextEntry.resourcesByUrl.set(event.url, responseEvents);
}
responseEvents.push(event);
break;
}
}
const contextEntry = contextEntries.get(event.contextId)!;
contextEntry.startTime = Math.min(contextEntry.startTime, (event as any).timestamp);
contextEntry.endTime = Math.max(contextEntry.endTime, (event as any).timestamp);
}
traceModel.contexts.push(...contextEntries.values());
}
export function actionById(traceModel: TraceModel, actionId: string): { context: ContextEntry, page: PageEntry, action: ActionEntry } {
const [contextId, pageId, actionIndex] = actionId.split('/');
const context = traceModel.contexts.find(entry => entry.created.contextId === contextId)!;
const page = context.pages.find(entry => entry.created.pageId === pageId)!;
const action = page.actions[+actionIndex];
return { context, page, action };
}
export function videoById(traceModel: TraceModel, videoId: string): { context: ContextEntry, page: PageEntry } {
const [contextId, pageId] = videoId.split('/');
const context = traceModel.contexts.find(entry => entry.created.contextId === contextId)!;
const page = context.pages.find(entry => entry.created.pageId === pageId)!;
return { context, page };
}

Просмотреть файл

@ -0,0 +1,181 @@
/**
* 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 fs from 'fs';
import * as path from 'path';
import * as playwright from '../../..';
import * as util from 'util';
import { ScreenshotGenerator } from './screenshotGenerator';
import { SnapshotRouter } from './snapshotRouter';
import { readTraceFile, TraceModel } from './traceModel';
import type { ActionTraceEvent, PageSnapshot, TraceEvent } from '../../trace/traceTypes';
import { VideoTileGenerator } from './videoTileGenerator';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
class TraceViewer {
private _traceStorageDir: string;
private _traceModel: TraceModel;
private _snapshotRouter: SnapshotRouter;
private _screenshotGenerator: ScreenshotGenerator;
private _videoTileGenerator: VideoTileGenerator;
constructor(traceStorageDir: string) {
this._traceStorageDir = traceStorageDir;
this._snapshotRouter = new SnapshotRouter(traceStorageDir);
this._traceModel = {
contexts: [],
};
this._screenshotGenerator = new ScreenshotGenerator(traceStorageDir, this._traceModel);
this._videoTileGenerator = new VideoTileGenerator(this._traceModel);
}
async load(filePath: string) {
const traceContent = await fsReadFileAsync(filePath, 'utf8');
const events = traceContent.split('\n').map(line => line.trim()).filter(line => !!line).map(line => JSON.parse(line)) as TraceEvent[];
readTraceFile(events, this._traceModel, filePath);
}
async show() {
const browser = await playwright.chromium.launch({ headless: false });
const uiPage = await browser.newPage({ viewport: null });
uiPage.on('close', () => process.exit(0));
await uiPage.exposeBinding('readFile', async (_, path: string) => {
return fs.readFileSync(path).toString();
});
await uiPage.exposeBinding('renderSnapshot', async (_, action: ActionTraceEvent) => {
try {
if (!action.snapshot) {
const snapshotFrame = uiPage.frames()[1];
await snapshotFrame.goto('data:text/html,No snapshot available');
return;
}
const snapshot = await fsReadFileAsync(path.join(this._traceStorageDir, action.snapshot!.sha1), 'utf8');
const snapshotObject = JSON.parse(snapshot) as PageSnapshot;
const contextEntry = this._traceModel.contexts.find(entry => entry.created.contextId === action.contextId)!;
this._snapshotRouter.selectSnapshot(snapshotObject, contextEntry);
// TODO: fix Playwright bug where frame.name is lost (empty).
const snapshotFrame = uiPage.frames()[1];
try {
await snapshotFrame.goto(snapshotObject.frames[0].url);
} catch (e) {
if (!e.message.includes('frame was detached'))
console.error(e);
return;
}
const element = await snapshotFrame.$(action.selector || '*[__playwright_target__]');
if (element) {
await element.evaluate(e => {
e.style.backgroundColor = '#ff69b460';
});
}
} catch (e) {
console.log(e); // eslint-disable-line no-console
}
});
await uiPage.exposeBinding('getTraceModel', () => this._traceModel);
await uiPage.exposeBinding('getVideoMetaInfo', async (_, videoId: string) => {
return this._videoTileGenerator.render(videoId);
});
await uiPage.route('**/*', (route, request) => {
if (request.frame().parentFrame()) {
this._snapshotRouter.route(route);
return;
}
const url = new URL(request.url());
try {
if (request.url().includes('action-preview')) {
const fullPath = url.pathname.substring('/action-preview/'.length);
const actionId = fullPath.substring(0, fullPath.indexOf('.png'));
this._screenshotGenerator.generateScreenshot(actionId).then(body => {
if (body)
route.fulfill({ contentType: 'image/png', body });
else
route.fulfill({ status: 404 });
});
return;
}
let filePath: string;
if (request.url().includes('video-tile')) {
const fullPath = url.pathname.substring('/video-tile/'.length);
filePath = this._videoTileGenerator.tilePath(fullPath);
} else {
filePath = path.join(__dirname, 'web', url.pathname.substring(1));
}
const body = fs.readFileSync(filePath);
route.fulfill({
contentType: extensionToMime[path.extname(url.pathname).substring(1)] || 'text/plain',
body,
});
} catch (e) {
console.log(e); // eslint-disable-line no-console
route.fulfill({
status: 404
});
}
});
await uiPage.goto('http://trace-viewer/index.html');
}
}
export async function showTraceViewer(traceStorageDir: string | undefined, tracePath: string) {
if (!fs.existsSync(tracePath))
throw new Error(`${tracePath} does not exist`);
let files: string[];
if (fs.statSync(tracePath).isFile()) {
files = [tracePath];
if (!traceStorageDir)
traceStorageDir = path.dirname(tracePath);
} else {
files = collectFiles(tracePath);
if (!traceStorageDir)
traceStorageDir = tracePath;
}
const traceViewer = new TraceViewer(traceStorageDir);
for (const filePath of files)
await traceViewer.load(filePath);
await traceViewer.show();
}
function collectFiles(dir: string): string[] {
const files = [];
for (const name of fs.readdirSync(dir)) {
const fullName = path.join(dir, name);
if (fs.lstatSync(fullName).isDirectory())
files.push(...collectFiles(fullName));
else if (fullName.endsWith('.trace'))
files.push(fullName);
}
return files;
}
const extensionToMime: { [key: string]: string } = {
'css': 'text/css',
'html': 'text/html',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'application/javascript',
'png': 'image/png',
'ttf': 'font/ttf',
'svg': 'image/svg+xml',
'webp': 'image/webp',
'woff': 'font/woff',
'woff2': 'font/woff2',
};

Просмотреть файл

@ -0,0 +1,87 @@
/**
* 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 { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { TraceModel, videoById, VideoMetaInfo } from './traceModel';
import type { PageVideoTraceEvent } from '../../trace/traceTypes';
import { ffmpegExecutable } from '../../utils/binaryPaths';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
export class VideoTileGenerator {
private _traceModel: TraceModel;
constructor(traceModel: TraceModel) {
this._traceModel = traceModel;
}
tilePath(urlPath: string) {
const index = urlPath.lastIndexOf('/');
const tile = urlPath.substring(index + 1);
const videoId = urlPath.substring(0, index);
const { context, page } = videoById(this._traceModel, videoId);
const videoFilePath = path.join(path.dirname(context.filePath), page.video!.video.fileName);
return videoFilePath + '-' + tile;
}
async render(videoId: string): Promise<VideoMetaInfo | undefined> {
const { context, page } = videoById(this._traceModel, videoId);
const video = page.video!.video;
const videoFilePath = path.join(path.dirname(context.filePath), video.fileName);
const metaInfoFilePath = videoFilePath + '-metainfo.txt';
try {
const metaInfo = await fsReadFileAsync(metaInfoFilePath, 'utf8');
return metaInfo ? JSON.parse(metaInfo) : undefined;
} catch (e) {
}
const ffmpeg = ffmpegExecutable()!;
console.log('Generating frames for ' + videoFilePath); // eslint-disable-line no-console
// Force output frame rate to 25 fps as otherwise it would produce one image per timebase unit
// which is currently 1 / (25 * 1000).
const result = spawnSync(ffmpeg, ['-i', videoFilePath, '-r', '25', `${videoFilePath}-%03d.png`]);
const metaInfo = parseMetaInfo(result.stderr.toString(), video);
await fsWriteFileAsync(metaInfoFilePath, metaInfo ? JSON.stringify(metaInfo) : '');
return metaInfo;
}
}
function parseMetaInfo(text: string, video: PageVideoTraceEvent): VideoMetaInfo | undefined {
const lines = text.split('\n');
let framesLine = lines.find(l => l.startsWith('frame='));
if (!framesLine)
return;
framesLine = framesLine.substring(framesLine.lastIndexOf('frame='));
const framesMatch = framesLine.match(/frame=\s+(\d+)/);
const outputLineIndex = lines.findIndex(l => l.trim().startsWith('Output #0'));
const streamLine = lines.slice(outputLineIndex).find(l => l.trim().startsWith('Stream #0:0'))!;
const fpsMatch = streamLine.match(/, (\d+) fps,/);
const resolutionMatch = streamLine.match(/, (\d+)x(\d+)\D/);
const durationMatch = lines.find(l => l.trim().startsWith('Duration'))!.match(/Duration: (\d+):(\d\d):(\d\d.\d\d)/);
const duration = (((parseInt(durationMatch![1], 10) * 60) + parseInt(durationMatch![2], 10)) * 60 + parseFloat(durationMatch![3])) * 1000;
return {
frames: parseInt(framesMatch![1], 10),
width: parseInt(resolutionMatch![1], 10),
height: parseInt(resolutionMatch![2], 10),
fps: parseInt(fpsMatch![1], 10),
startTime: (video as any).timestamp,
endTime: (video as any).timestamp + duration
};
}

Просмотреть файл

@ -0,0 +1,122 @@
/*
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.
*/
:root {
--light-background: #f3f2f1;
--background: #edebe9;
--active-background: #333333;
--color: #252423;
--red: #F44336;
--green: #4CAF50;
--purple: #9C27B0;
--yellow: #FFC107;
--blue: #2196F3;
--orange: #d24726;
--black: #1E1E1E;
--gray: #888888;
--separator: #80808059;
--focus-ring: #0E639CCC;
--inactive-focus-ring: #80808059;
--layout-gap: 10px;
--selection: #074771;
--control-background: #3C3C3C;
--settings: #E7E7E7;
--sidebar-width: 250px;
--light-pink: #ff69b460;
--box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
}
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
display: flex;
background: var(--background);
overscroll-behavior-x: none;
}
* {
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
*[hidden] {
display: none !important;
}
.codicon {
color: #C5C5C5;
}
svg {
fill: currentColor;
}
body {
background-color: var(--background);
color: var(--color);
font-size: 14px;
font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
}
#root {
width: 100%;
height: 100%;
display: flex;
}
.platform-windows {
--monospace-font: Consolas, Inconsolata, "Courier New", monospace;
}
.platform-linux {
--monospace-font:"Droid Sans Mono", Inconsolata, "Courier New", monospace, "Droid Sans Fallback";
}
.platform-mac {
--monospace-font: "SF Mono",Monaco,Menlo,Inconsolata,"Courier New",monospace;
}
.vbox {
display: flex;
flex-direction: column;
flex: auto;
position: relative;
}
.hbox {
display: flex;
flex: auto;
position: relative;
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-thumb {
border: 1px solid #ccc;
background-color: var(--light-background);
}
::-webkit-scrollbar-corner {
background-color: var(--background);
}

Просмотреть файл

@ -0,0 +1,25 @@
/*
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.
*/
export type Size = {
width: number;
height: number;
};
export type Boundaries = {
minimum: number;
maximum: number;
};

Просмотреть файл

@ -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.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playwright Trace Viewer</title>
</head>
<body>
<div id=root></div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,57 @@
1;/**
* 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 { TraceModel, VideoMetaInfo } from '../traceModel';
import './common.css';
import './third_party/vscode/codicon.css';
import { Workbench } from './ui/workbench';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { ActionTraceEvent } from '../../../trace/traceTypes';
declare global {
interface Window {
getTraceModel(): Promise<TraceModel>;
getVideoMetaInfo(videoId: string): Promise<VideoMetaInfo | undefined>;
readFile(filePath: string): Promise<string>;
renderSnapshot(action: ActionTraceEvent): void;
}
}
function platformName(): string {
if (window.navigator.userAgent.includes('Linux'))
return 'platform-linux';
if (window.navigator.userAgent.includes('Windows'))
return 'platform-windows';
if (window.navigator.userAgent.includes('Mac'))
return 'platform-mac';
return 'platform-generic';
}
(async () => {
document!.defaultView!.addEventListener('focus', (event: any) => {
if (event.target.document.nodeType === Node.DOCUMENT_NODE)
document.body.classList.remove('inactive');
}, false);
document!.defaultView!.addEventListener('blur', event => {
document.body.classList.add('inactive');
}, false);
document.documentElement.classList.add(platformName());
const traceModel = await window.getTraceModel();
ReactDOM.render(<Workbench traceModel={traceModel} />, document.querySelector('#root'));
})();

21
src/cli/traceViewer/web/third_party/vscode/LICENSE.txt поставляемый Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

440
src/cli/traceViewer/web/third_party/vscode/codicon.css поставляемый Normal file
Просмотреть файл

@ -0,0 +1,440 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
@font-face {
font-family: "codicon";
src: url("codicon.ttf") format("truetype");
}
.codicon {
font: normal normal normal 16px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.codicon-add:before { content: '\ea60'; }
.codicon-plus:before { content: '\ea60'; }
.codicon-gist-new:before { content: '\ea60'; }
.codicon-repo-create:before { content: '\ea60'; }
.codicon-lightbulb:before { content: '\ea61'; }
.codicon-light-bulb:before { content: '\ea61'; }
.codicon-repo:before { content: '\ea62'; }
.codicon-repo-delete:before { content: '\ea62'; }
.codicon-gist-fork:before { content: '\ea63'; }
.codicon-repo-forked:before { content: '\ea63'; }
.codicon-git-pull-request:before { content: '\ea64'; }
.codicon-git-pull-request-abandoned:before { content: '\ea64'; }
.codicon-record-keys:before { content: '\ea65'; }
.codicon-keyboard:before { content: '\ea65'; }
.codicon-tag:before { content: '\ea66'; }
.codicon-tag-add:before { content: '\ea66'; }
.codicon-tag-remove:before { content: '\ea66'; }
.codicon-person:before { content: '\ea67'; }
.codicon-person-add:before { content: '\ea67'; }
.codicon-person-follow:before { content: '\ea67'; }
.codicon-person-outline:before { content: '\ea67'; }
.codicon-person-filled:before { content: '\ea67'; }
.codicon-git-branch:before { content: '\ea68'; }
.codicon-git-branch-create:before { content: '\ea68'; }
.codicon-git-branch-delete:before { content: '\ea68'; }
.codicon-source-control:before { content: '\ea68'; }
.codicon-mirror:before { content: '\ea69'; }
.codicon-mirror-public:before { content: '\ea69'; }
.codicon-star:before { content: '\ea6a'; }
.codicon-star-add:before { content: '\ea6a'; }
.codicon-star-delete:before { content: '\ea6a'; }
.codicon-star-empty:before { content: '\ea6a'; }
.codicon-comment:before { content: '\ea6b'; }
.codicon-comment-add:before { content: '\ea6b'; }
.codicon-alert:before { content: '\ea6c'; }
.codicon-warning:before { content: '\ea6c'; }
.codicon-search:before { content: '\ea6d'; }
.codicon-search-save:before { content: '\ea6d'; }
.codicon-log-out:before { content: '\ea6e'; }
.codicon-sign-out:before { content: '\ea6e'; }
.codicon-log-in:before { content: '\ea6f'; }
.codicon-sign-in:before { content: '\ea6f'; }
.codicon-eye:before { content: '\ea70'; }
.codicon-eye-unwatch:before { content: '\ea70'; }
.codicon-eye-watch:before { content: '\ea70'; }
.codicon-circle-filled:before { content: '\ea71'; }
.codicon-primitive-dot:before { content: '\ea71'; }
.codicon-close-dirty:before { content: '\ea71'; }
.codicon-debug-breakpoint:before { content: '\ea71'; }
.codicon-debug-breakpoint-disabled:before { content: '\ea71'; }
.codicon-debug-hint:before { content: '\ea71'; }
.codicon-primitive-square:before { content: '\ea72'; }
.codicon-edit:before { content: '\ea73'; }
.codicon-pencil:before { content: '\ea73'; }
.codicon-info:before { content: '\ea74'; }
.codicon-issue-opened:before { content: '\ea74'; }
.codicon-gist-private:before { content: '\ea75'; }
.codicon-git-fork-private:before { content: '\ea75'; }
.codicon-lock:before { content: '\ea75'; }
.codicon-mirror-private:before { content: '\ea75'; }
.codicon-close:before { content: '\ea76'; }
.codicon-remove-close:before { content: '\ea76'; }
.codicon-x:before { content: '\ea76'; }
.codicon-repo-sync:before { content: '\ea77'; }
.codicon-sync:before { content: '\ea77'; }
.codicon-clone:before { content: '\ea78'; }
.codicon-desktop-download:before { content: '\ea78'; }
.codicon-beaker:before { content: '\ea79'; }
.codicon-microscope:before { content: '\ea79'; }
.codicon-vm:before { content: '\ea7a'; }
.codicon-device-desktop:before { content: '\ea7a'; }
.codicon-file:before { content: '\ea7b'; }
.codicon-file-text:before { content: '\ea7b'; }
.codicon-more:before { content: '\ea7c'; }
.codicon-ellipsis:before { content: '\ea7c'; }
.codicon-kebab-horizontal:before { content: '\ea7c'; }
.codicon-mail-reply:before { content: '\ea7d'; }
.codicon-reply:before { content: '\ea7d'; }
.codicon-organization:before { content: '\ea7e'; }
.codicon-organization-filled:before { content: '\ea7e'; }
.codicon-organization-outline:before { content: '\ea7e'; }
.codicon-new-file:before { content: '\ea7f'; }
.codicon-file-add:before { content: '\ea7f'; }
.codicon-new-folder:before { content: '\ea80'; }
.codicon-file-directory-create:before { content: '\ea80'; }
.codicon-trash:before { content: '\ea81'; }
.codicon-trashcan:before { content: '\ea81'; }
.codicon-history:before { content: '\ea82'; }
.codicon-clock:before { content: '\ea82'; }
.codicon-folder:before { content: '\ea83'; }
.codicon-file-directory:before { content: '\ea83'; }
.codicon-symbol-folder:before { content: '\ea83'; }
.codicon-logo-github:before { content: '\ea84'; }
.codicon-mark-github:before { content: '\ea84'; }
.codicon-github:before { content: '\ea84'; }
.codicon-terminal:before { content: '\ea85'; }
.codicon-console:before { content: '\ea85'; }
.codicon-repl:before { content: '\ea85'; }
.codicon-zap:before { content: '\ea86'; }
.codicon-symbol-event:before { content: '\ea86'; }
.codicon-error:before { content: '\ea87'; }
.codicon-stop:before { content: '\ea87'; }
.codicon-variable:before { content: '\ea88'; }
.codicon-symbol-variable:before { content: '\ea88'; }
.codicon-array:before { content: '\ea8a'; }
.codicon-symbol-array:before { content: '\ea8a'; }
.codicon-symbol-module:before { content: '\ea8b'; }
.codicon-symbol-package:before { content: '\ea8b'; }
.codicon-symbol-namespace:before { content: '\ea8b'; }
.codicon-symbol-object:before { content: '\ea8b'; }
.codicon-symbol-method:before { content: '\ea8c'; }
.codicon-symbol-function:before { content: '\ea8c'; }
.codicon-symbol-constructor:before { content: '\ea8c'; }
.codicon-symbol-boolean:before { content: '\ea8f'; }
.codicon-symbol-null:before { content: '\ea8f'; }
.codicon-symbol-numeric:before { content: '\ea90'; }
.codicon-symbol-number:before { content: '\ea90'; }
.codicon-symbol-structure:before { content: '\ea91'; }
.codicon-symbol-struct:before { content: '\ea91'; }
.codicon-symbol-parameter:before { content: '\ea92'; }
.codicon-symbol-type-parameter:before { content: '\ea92'; }
.codicon-symbol-key:before { content: '\ea93'; }
.codicon-symbol-text:before { content: '\ea93'; }
.codicon-symbol-reference:before { content: '\ea94'; }
.codicon-go-to-file:before { content: '\ea94'; }
.codicon-symbol-enum:before { content: '\ea95'; }
.codicon-symbol-value:before { content: '\ea95'; }
.codicon-symbol-ruler:before { content: '\ea96'; }
.codicon-symbol-unit:before { content: '\ea96'; }
.codicon-activate-breakpoints:before { content: '\ea97'; }
.codicon-archive:before { content: '\ea98'; }
.codicon-arrow-both:before { content: '\ea99'; }
.codicon-arrow-down:before { content: '\ea9a'; }
.codicon-arrow-left:before { content: '\ea9b'; }
.codicon-arrow-right:before { content: '\ea9c'; }
.codicon-arrow-small-down:before { content: '\ea9d'; }
.codicon-arrow-small-left:before { content: '\ea9e'; }
.codicon-arrow-small-right:before { content: '\ea9f'; }
.codicon-arrow-small-up:before { content: '\eaa0'; }
.codicon-arrow-up:before { content: '\eaa1'; }
.codicon-bell:before { content: '\eaa2'; }
.codicon-bold:before { content: '\eaa3'; }
.codicon-book:before { content: '\eaa4'; }
.codicon-bookmark:before { content: '\eaa5'; }
.codicon-debug-breakpoint-conditional-unverified:before { content: '\eaa6'; }
.codicon-debug-breakpoint-conditional:before { content: '\eaa7'; }
.codicon-debug-breakpoint-conditional-disabled:before { content: '\eaa7'; }
.codicon-debug-breakpoint-data-unverified:before { content: '\eaa8'; }
.codicon-debug-breakpoint-data:before { content: '\eaa9'; }
.codicon-debug-breakpoint-data-disabled:before { content: '\eaa9'; }
.codicon-debug-breakpoint-log-unverified:before { content: '\eaaa'; }
.codicon-debug-breakpoint-log:before { content: '\eaab'; }
.codicon-debug-breakpoint-log-disabled:before { content: '\eaab'; }
.codicon-briefcase:before { content: '\eaac'; }
.codicon-broadcast:before { content: '\eaad'; }
.codicon-browser:before { content: '\eaae'; }
.codicon-bug:before { content: '\eaaf'; }
.codicon-calendar:before { content: '\eab0'; }
.codicon-case-sensitive:before { content: '\eab1'; }
.codicon-check:before { content: '\eab2'; }
.codicon-checklist:before { content: '\eab3'; }
.codicon-chevron-down:before { content: '\eab4'; }
.codicon-chevron-left:before { content: '\eab5'; }
.codicon-chevron-right:before { content: '\eab6'; }
.codicon-chevron-up:before { content: '\eab7'; }
.codicon-chrome-close:before { content: '\eab8'; }
.codicon-chrome-maximize:before { content: '\eab9'; }
.codicon-chrome-minimize:before { content: '\eaba'; }
.codicon-chrome-restore:before { content: '\eabb'; }
.codicon-circle-outline:before { content: '\eabc'; }
.codicon-debug-breakpoint-unverified:before { content: '\eabc'; }
.codicon-circle-slash:before { content: '\eabd'; }
.codicon-circuit-board:before { content: '\eabe'; }
.codicon-clear-all:before { content: '\eabf'; }
.codicon-clippy:before { content: '\eac0'; }
.codicon-close-all:before { content: '\eac1'; }
.codicon-cloud-download:before { content: '\eac2'; }
.codicon-cloud-upload:before { content: '\eac3'; }
.codicon-code:before { content: '\eac4'; }
.codicon-collapse-all:before { content: '\eac5'; }
.codicon-color-mode:before { content: '\eac6'; }
.codicon-comment-discussion:before { content: '\eac7'; }
.codicon-compare-changes:before { content: '\eafd'; }
.codicon-credit-card:before { content: '\eac9'; }
.codicon-dash:before { content: '\eacc'; }
.codicon-dashboard:before { content: '\eacd'; }
.codicon-database:before { content: '\eace'; }
.codicon-debug-continue:before { content: '\eacf'; }
.codicon-debug-disconnect:before { content: '\ead0'; }
.codicon-debug-pause:before { content: '\ead1'; }
.codicon-debug-restart:before { content: '\ead2'; }
.codicon-debug-start:before { content: '\ead3'; }
.codicon-debug-step-into:before { content: '\ead4'; }
.codicon-debug-step-out:before { content: '\ead5'; }
.codicon-debug-step-over:before { content: '\ead6'; }
.codicon-debug-stop:before { content: '\ead7'; }
.codicon-debug:before { content: '\ead8'; }
.codicon-device-camera-video:before { content: '\ead9'; }
.codicon-device-camera:before { content: '\eada'; }
.codicon-device-mobile:before { content: '\eadb'; }
.codicon-diff-added:before { content: '\eadc'; }
.codicon-diff-ignored:before { content: '\eadd'; }
.codicon-diff-modified:before { content: '\eade'; }
.codicon-diff-removed:before { content: '\eadf'; }
.codicon-diff-renamed:before { content: '\eae0'; }
.codicon-diff:before { content: '\eae1'; }
.codicon-discard:before { content: '\eae2'; }
.codicon-editor-layout:before { content: '\eae3'; }
.codicon-empty-window:before { content: '\eae4'; }
.codicon-exclude:before { content: '\eae5'; }
.codicon-extensions:before { content: '\eae6'; }
.codicon-eye-closed:before { content: '\eae7'; }
.codicon-file-binary:before { content: '\eae8'; }
.codicon-file-code:before { content: '\eae9'; }
.codicon-file-media:before { content: '\eaea'; }
.codicon-file-pdf:before { content: '\eaeb'; }
.codicon-file-submodule:before { content: '\eaec'; }
.codicon-file-symlink-directory:before { content: '\eaed'; }
.codicon-file-symlink-file:before { content: '\eaee'; }
.codicon-file-zip:before { content: '\eaef'; }
.codicon-files:before { content: '\eaf0'; }
.codicon-filter:before { content: '\eaf1'; }
.codicon-flame:before { content: '\eaf2'; }
.codicon-fold-down:before { content: '\eaf3'; }
.codicon-fold-up:before { content: '\eaf4'; }
.codicon-fold:before { content: '\eaf5'; }
.codicon-folder-active:before { content: '\eaf6'; }
.codicon-folder-opened:before { content: '\eaf7'; }
.codicon-gear:before { content: '\eaf8'; }
.codicon-gift:before { content: '\eaf9'; }
.codicon-gist-secret:before { content: '\eafa'; }
.codicon-gist:before { content: '\eafb'; }
.codicon-git-commit:before { content: '\eafc'; }
.codicon-git-compare:before { content: '\eafd'; }
.codicon-git-merge:before { content: '\eafe'; }
.codicon-github-action:before { content: '\eaff'; }
.codicon-github-alt:before { content: '\eb00'; }
.codicon-globe:before { content: '\eb01'; }
.codicon-grabber:before { content: '\eb02'; }
.codicon-graph:before { content: '\eb03'; }
.codicon-gripper:before { content: '\eb04'; }
.codicon-heart:before { content: '\eb05'; }
.codicon-home:before { content: '\eb06'; }
.codicon-horizontal-rule:before { content: '\eb07'; }
.codicon-hubot:before { content: '\eb08'; }
.codicon-inbox:before { content: '\eb09'; }
.codicon-issue-closed:before { content: '\eb0a'; }
.codicon-issue-reopened:before { content: '\eb0b'; }
.codicon-issues:before { content: '\eb0c'; }
.codicon-italic:before { content: '\eb0d'; }
.codicon-jersey:before { content: '\eb0e'; }
.codicon-json:before { content: '\eb0f'; }
.codicon-kebab-vertical:before { content: '\eb10'; }
.codicon-key:before { content: '\eb11'; }
.codicon-law:before { content: '\eb12'; }
.codicon-lightbulb-autofix:before { content: '\eb13'; }
.codicon-link-external:before { content: '\eb14'; }
.codicon-link:before { content: '\eb15'; }
.codicon-list-ordered:before { content: '\eb16'; }
.codicon-list-unordered:before { content: '\eb17'; }
.codicon-live-share:before { content: '\eb18'; }
.codicon-loading:before { content: '\eb19'; }
.codicon-location:before { content: '\eb1a'; }
.codicon-mail-read:before { content: '\eb1b'; }
.codicon-mail:before { content: '\eb1c'; }
.codicon-markdown:before { content: '\eb1d'; }
.codicon-megaphone:before { content: '\eb1e'; }
.codicon-mention:before { content: '\eb1f'; }
.codicon-milestone:before { content: '\eb20'; }
.codicon-mortar-board:before { content: '\eb21'; }
.codicon-move:before { content: '\eb22'; }
.codicon-multiple-windows:before { content: '\eb23'; }
.codicon-mute:before { content: '\eb24'; }
.codicon-no-newline:before { content: '\eb25'; }
.codicon-note:before { content: '\eb26'; }
.codicon-octoface:before { content: '\eb27'; }
.codicon-open-preview:before { content: '\eb28'; }
.codicon-package:before { content: '\eb29'; }
.codicon-paintcan:before { content: '\eb2a'; }
.codicon-pin:before { content: '\eb2b'; }
.codicon-play:before { content: '\eb2c'; }
.codicon-run:before { content: '\eb2c'; }
.codicon-plug:before { content: '\eb2d'; }
.codicon-preserve-case:before { content: '\eb2e'; }
.codicon-preview:before { content: '\eb2f'; }
.codicon-project:before { content: '\eb30'; }
.codicon-pulse:before { content: '\eb31'; }
.codicon-question:before { content: '\eb32'; }
.codicon-quote:before { content: '\eb33'; }
.codicon-radio-tower:before { content: '\eb34'; }
.codicon-reactions:before { content: '\eb35'; }
.codicon-references:before { content: '\eb36'; }
.codicon-refresh:before { content: '\eb37'; }
.codicon-regex:before { content: '\eb38'; }
.codicon-remote-explorer:before { content: '\eb39'; }
.codicon-remote:before { content: '\eb3a'; }
.codicon-remove:before { content: '\eb3b'; }
.codicon-replace-all:before { content: '\eb3c'; }
.codicon-replace:before { content: '\eb3d'; }
.codicon-repo-clone:before { content: '\eb3e'; }
.codicon-repo-force-push:before { content: '\eb3f'; }
.codicon-repo-pull:before { content: '\eb40'; }
.codicon-repo-push:before { content: '\eb41'; }
.codicon-report:before { content: '\eb42'; }
.codicon-request-changes:before { content: '\eb43'; }
.codicon-rocket:before { content: '\eb44'; }
.codicon-root-folder-opened:before { content: '\eb45'; }
.codicon-root-folder:before { content: '\eb46'; }
.codicon-rss:before { content: '\eb47'; }
.codicon-ruby:before { content: '\eb48'; }
.codicon-save-all:before { content: '\eb49'; }
.codicon-save-as:before { content: '\eb4a'; }
.codicon-save:before { content: '\eb4b'; }
.codicon-screen-full:before { content: '\eb4c'; }
.codicon-screen-normal:before { content: '\eb4d'; }
.codicon-search-stop:before { content: '\eb4e'; }
.codicon-server:before { content: '\eb50'; }
.codicon-settings-gear:before { content: '\eb51'; }
.codicon-settings:before { content: '\eb52'; }
.codicon-shield:before { content: '\eb53'; }
.codicon-smiley:before { content: '\eb54'; }
.codicon-sort-precedence:before { content: '\eb55'; }
.codicon-split-horizontal:before { content: '\eb56'; }
.codicon-split-vertical:before { content: '\eb57'; }
.codicon-squirrel:before { content: '\eb58'; }
.codicon-star-full:before { content: '\eb59'; }
.codicon-star-half:before { content: '\eb5a'; }
.codicon-symbol-class:before { content: '\eb5b'; }
.codicon-symbol-color:before { content: '\eb5c'; }
.codicon-symbol-constant:before { content: '\eb5d'; }
.codicon-symbol-enum-member:before { content: '\eb5e'; }
.codicon-symbol-field:before { content: '\eb5f'; }
.codicon-symbol-file:before { content: '\eb60'; }
.codicon-symbol-interface:before { content: '\eb61'; }
.codicon-symbol-keyword:before { content: '\eb62'; }
.codicon-symbol-misc:before { content: '\eb63'; }
.codicon-symbol-operator:before { content: '\eb64'; }
.codicon-symbol-property:before { content: '\eb65'; }
.codicon-wrench:before { content: '\eb65'; }
.codicon-wrench-subaction:before { content: '\eb65'; }
.codicon-symbol-snippet:before { content: '\eb66'; }
.codicon-tasklist:before { content: '\eb67'; }
.codicon-telescope:before { content: '\eb68'; }
.codicon-text-size:before { content: '\eb69'; }
.codicon-three-bars:before { content: '\eb6a'; }
.codicon-thumbsdown:before { content: '\eb6b'; }
.codicon-thumbsup:before { content: '\eb6c'; }
.codicon-tools:before { content: '\eb6d'; }
.codicon-triangle-down:before { content: '\eb6e'; }
.codicon-triangle-left:before { content: '\eb6f'; }
.codicon-triangle-right:before { content: '\eb70'; }
.codicon-triangle-up:before { content: '\eb71'; }
.codicon-twitter:before { content: '\eb72'; }
.codicon-unfold:before { content: '\eb73'; }
.codicon-unlock:before { content: '\eb74'; }
.codicon-unmute:before { content: '\eb75'; }
.codicon-unverified:before { content: '\eb76'; }
.codicon-verified:before { content: '\eb77'; }
.codicon-versions:before { content: '\eb78'; }
.codicon-vm-active:before { content: '\eb79'; }
.codicon-vm-outline:before { content: '\eb7a'; }
.codicon-vm-running:before { content: '\eb7b'; }
.codicon-watch:before { content: '\eb7c'; }
.codicon-whitespace:before { content: '\eb7d'; }
.codicon-whole-word:before { content: '\eb7e'; }
.codicon-window:before { content: '\eb7f'; }
.codicon-word-wrap:before { content: '\eb80'; }
.codicon-zoom-in:before { content: '\eb81'; }
.codicon-zoom-out:before { content: '\eb82'; }
.codicon-list-filter:before { content: '\eb83'; }
.codicon-list-flat:before { content: '\eb84'; }
.codicon-list-selection:before { content: '\eb85'; }
.codicon-selection:before { content: '\eb85'; }
.codicon-list-tree:before { content: '\eb86'; }
.codicon-debug-breakpoint-function-unverified:before { content: '\eb87'; }
.codicon-debug-breakpoint-function:before { content: '\eb88'; }
.codicon-debug-breakpoint-function-disabled:before { content: '\eb88'; }
.codicon-debug-stackframe-active:before { content: '\eb89'; }
.codicon-debug-stackframe-dot:before { content: '\eb8a'; }
.codicon-debug-stackframe:before { content: '\eb8b'; }
.codicon-debug-stackframe-focused:before { content: '\eb8b'; }
.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; }
.codicon-symbol-string:before { content: '\eb8d'; }
.codicon-debug-reverse-continue:before { content: '\eb8e'; }
.codicon-debug-step-back:before { content: '\eb8f'; }
.codicon-debug-restart-frame:before { content: '\eb90'; }
.codicon-call-incoming:before { content: '\eb92'; }
.codicon-call-outgoing:before { content: '\eb93'; }
.codicon-menu:before { content: '\eb94'; }
.codicon-expand-all:before { content: '\eb95'; }
.codicon-feedback:before { content: '\eb96'; }
.codicon-group-by-ref-type:before { content: '\eb97'; }
.codicon-ungroup-by-ref-type:before { content: '\eb98'; }
.codicon-account:before { content: '\eb99'; }
.codicon-bell-dot:before { content: '\eb9a'; }
.codicon-debug-console:before { content: '\eb9b'; }
.codicon-library:before { content: '\eb9c'; }
.codicon-output:before { content: '\eb9d'; }
.codicon-run-all:before { content: '\eb9e'; }
.codicon-sync-ignored:before { content: '\eb9f'; }
.codicon-pinned:before { content: '\eba0'; }
.codicon-github-inverted:before { content: '\eba1'; }
.codicon-debug-alt:before { content: '\eb91'; }
.codicon-server-process:before { content: '\eba2'; }
.codicon-server-environment:before { content: '\eba3'; }
.codicon-pass:before { content: '\eba4'; }
.codicon-stop-circle:before { content: '\eba5'; }
.codicon-play-circle:before { content: '\eba6'; }
.codicon-record:before { content: '\eba7'; }
.codicon-debug-alt-small:before { content: '\eba8'; }
.codicon-vm-connect:before { content: '\eba9'; }
.codicon-cloud:before { content: '\ebaa'; }
.codicon-merge:before { content: '\ebab'; }
.codicon-export:before { content: '\ebac'; }
.codicon-graph-left:before { content: '\ebad'; }
.codicon-magnet:before { content: '\ebae'; }

Двоичные данные
src/cli/traceViewer/web/third_party/vscode/codicon.ttf поставляемый Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,92 @@
/*
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.
*/
.action-list {
width: var(--sidebar-width);
display: flex;
flex-direction: column;
flex: none;
overflow: auto;
position: relative;
contain: size;
padding: 0 var(--layout-gap);
}
.action-entry {
position: relative;
display: flex;
flex-direction: column;
flex: none;
overflow: hidden;
border: 3px solid var(--background);
margin-top: var(--layout-gap);
user-select: none;
padding: 0 5px 5px 5px;
cursor: pointer;
outline: none;
}
.action-entry:hover {
border-color: var(--inactive-focus-ring);
}
.action-entry.selected {
border-color: var(--inactive-focus-ring);
}
.action-entry.selected:focus {
border-color: var(--orange);
}
.action-title {
display: inline;
white-space: nowrap;
font-weight: 600;
}
.action-header {
display: block;
align-items: center;
margin: 5px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-selector {
display: inline;
padding-left: 5px;
}
.action-url {
display: inline;
padding-left: 5px;
}
.action-thumbnail {
flex: none;
display: flex;
align-items: center;
justify-content: center;
width: 200px;
height: 100px;
box-shadow: var(--box-shadow);
}
.action-thumbnail img {
max-width: 200px;
max-height: 100px;
}

Просмотреть файл

@ -0,0 +1,42 @@
/*
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 { ActionEntry } from '../../traceModel';
import './actionList.css';
import * as React from 'react';
export const ActionList: React.FunctionComponent<{
actions: ActionEntry[],
selectedAction?: ActionEntry,
onSelected: (action: ActionEntry) => void,
}> = ({ actions, selectedAction, onSelected }) => {
return <div className='action-list'>{actions.map(actionEntry => {
const { action, actionId } = actionEntry;
return <div
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
key={actionId}
onClick={() => onSelected(actionEntry)}>
<div className='action-header'>
<div className='action-title'>{action.action}</div>
{action.selector && <div className='action-selector' title={action.selector}>{action.selector}</div>}
{action.action === 'goto' && action.value && <div className='action-url' title={action.value}>{action.value}</div>}
</div>
<div className='action-thumbnail'>
{action.snapshot ? <img src={`action-preview/${actionId}.png`} /> : 'No snapshot available'}
</div>
</div>;
})}</div>;
};

Просмотреть файл

@ -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.
*/
.context-selector {
min-width: 38px;
border: 1px solid transparent;
border-radius: 0;
padding: 0 13px 0 5px;
position: relative;
height: 22px;
align-self: center;
margin-right: 20px;
background: rgba(222, 222, 222, 0.3);
color: white;
border-radius: 3px;
outline: none !important;
}

Просмотреть файл

@ -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 * as React from 'react';
import { ContextEntry } from '../../traceModel';
import './contextSelector.css';
export const ContextSelector: React.FunctionComponent<{
contexts: ContextEntry[],
context: ContextEntry,
onChange: (contextEntry: ContextEntry) => void,
}> = ({ contexts, context, onChange }) => {
return (
<select
className='context-selector'
style={{
visibility: contexts.length <= 1 ? 'hidden' : 'visible',
}}
value={context.created.contextId}
onChange={e => {
const newIndex = e.target.selectedIndex;
onChange(contexts[newIndex]);
}}
>
{contexts.map(entry => <option value={entry.created.contextId} key={entry.created.contextId}>{entry.name}</option>)}
</select>
);
};

Просмотреть файл

@ -0,0 +1,45 @@
/*
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.
*/
.film-strip {
flex: none;
display: flex;
flex-direction: column;
position: relative;
}
.film-strip-lane {
flex: none;
display: flex;
}
.film-strip-frame {
flex: none;
pointer-events: none;
box-shadow: var(--box-shadow);
}
.film-strip-hover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 10px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 10px 0px;
z-index: 10;
}

Просмотреть файл

@ -0,0 +1,136 @@
/*
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 { ContextEntry, VideoEntry, VideoMetaInfo } from '../../traceModel';
import './filmStrip.css';
import { Boundaries } from '../geometry';
import * as React from 'react';
import { useAsyncMemo, useMeasure } from './helpers';
function imageURL(videoId: string, index: number) {
const imageURLpadding = '0'.repeat(3 - String(index + 1).length);
return `video-tile/${videoId}/${imageURLpadding}${index + 1}.png`;
}
export const FilmStrip: React.FunctionComponent<{
context: ContextEntry,
boundaries: Boundaries,
previewX?: number,
}> = ({ context, boundaries, previewX }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();
const videos = React.useMemo(() => {
const videos: VideoEntry[] = [];
for (const page of context.pages) {
if (page.video)
videos.push(page.video);
}
return videos;
}, [context]);
const metaInfos = useAsyncMemo<Map<VideoEntry, VideoMetaInfo | undefined>>(async () => {
const infos = new Map<VideoEntry, VideoMetaInfo | undefined>();
for (const video of videos)
infos.set(video, await window.getVideoMetaInfo(video.videoId));
return infos;
}, [videos], new Map(), new Map());
// TODO: pick file from the Y position.
const previewVideo = videos[0];
const previewMetaInfo = metaInfos.get(previewVideo);
let previewIndex = 0;
if ((previewX !== undefined) && previewMetaInfo) {
const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewX / measure.width;
previewIndex = (previewTime - previewMetaInfo.startTime) / (previewMetaInfo.endTime - previewMetaInfo.startTime) * previewMetaInfo.frames | 0;
}
const previewImage = useAsyncMemo<HTMLImageElement | undefined>(async () => {
if (!previewMetaInfo || previewIndex < 0 || previewIndex >= previewMetaInfo.frames)
return;
const idealWidth = previewMetaInfo.width / 2;
const idealHeight = previewMetaInfo.height / 2;
const ratio = Math.min(1, (measure.width - 20) / idealWidth);
const image = new Image((idealWidth * ratio) | 0, (idealHeight * ratio) | 0);
image.src = imageURL(previewVideo.videoId, previewIndex);
await new Promise(f => image.onload = f);
return image;
}, [previewMetaInfo, previewIndex, measure.width, previewVideo], undefined);
return <div className='film-strip' ref={ref}>{
videos.map(video => <FilmStripLane
boundaries={boundaries}
video={video}
metaInfo={metaInfos.get(video)}
width={measure.width}
key={video.videoId}
/>)
}
{(previewX !== undefined) && previewMetaInfo && previewImage &&
<div className='film-strip-hover' style={{
width: previewImage.width + 'px',
height: previewImage.height + 'px',
top: measure.bottom + 5 + 'px',
left: Math.min(previewX, measure.width - previewImage.width - 10) + 'px',
}}>
<img src={previewImage.src} width={previewImage.width} height={previewImage.height} />
</div>
}
</div>;
};
const FilmStripLane: React.FunctionComponent<{
boundaries: Boundaries,
video: VideoEntry,
metaInfo: VideoMetaInfo | undefined,
width: number,
}> = ({ boundaries, video, metaInfo, width }) => {
const frameHeight = 45;
const frameMargin = 2.5;
if (!metaInfo)
return <div className='film-strip-lane' style={{ height: (frameHeight + 2 * frameMargin) + 'px' }}></div>;
const frameWidth = frameHeight / metaInfo.height * metaInfo.width | 0;
const boundariesSize = boundaries.maximum - boundaries.minimum;
const gapLeft = (metaInfo.startTime - boundaries.minimum) / boundariesSize * width;
const gapRight = (boundaries.maximum - metaInfo.endTime) / boundariesSize * width;
const effectiveWidth = (metaInfo.endTime - metaInfo.startTime) / boundariesSize * width;
const frameCount = effectiveWidth / (frameWidth + 2 * frameMargin) | 0;
const frameStep = metaInfo.frames / frameCount;
const frameGap = frameCount <= 1 ? 0 : (effectiveWidth - (frameWidth + 2 * frameMargin) * frameCount) / (frameCount - 1);
const frames: JSX.Element[] = [];
for (let i = 0; i < metaInfo.frames; i += frameStep) {
let index = i | 0;
// Always show last frame.
if (Math.floor(i + frameStep) >= metaInfo.frames)
index = metaInfo.frames - 1;
frames.push(<div className='film-strip-frame' key={i} style={{
width: frameWidth + 'px',
height: frameHeight + 'px',
backgroundImage: `url(${imageURL(video.videoId, index)})`,
backgroundSize: `${frameWidth}px ${frameHeight}px`,
margin: frameMargin + 'px',
marginRight: (frameMargin + frameGap) + 'px',
}} />);
}
return <div className='film-strip-lane' style={{
marginLeft: gapLeft + 'px',
marginRight: gapRight + 'px',
}}>{frames}</div>;
};

Просмотреть файл

@ -0,0 +1,73 @@
/*
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';
// Recalculates the value when dependencies change.
export function useAsyncMemo<T>(fn: () => Promise<T>, deps: React.DependencyList, initialValue: T, resetValue?: T) {
const [value, setValue] = React.useState<T>(initialValue);
React.useEffect(() => {
let canceled = false;
if (resetValue !== undefined)
setValue(resetValue);
fn().then(value => {
if (!canceled)
setValue(value);
});
return () => {
canceled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return value;
}
// Tracks the element size and returns it's contentRect (always has x=0, y=0).
export function useMeasure<T extends Element>() {
const ref = React.useRef<T | null>(null);
const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10));
React.useLayoutEffect(() => {
const target = ref.current;
if (!target)
return;
const resizeObserver = new ResizeObserver(entries => {
const entry = entries[entries.length - 1];
if (entry && entry.contentRect)
setMeasure(entry.contentRect);
});
resizeObserver.observe(target);
return () => resizeObserver.unobserve(target);
}, [ref]);
return [measure, ref] as const;
}
export const Expandable: React.FunctionComponent<{
title: JSX.Element,
body: JSX.Element,
style?: React.CSSProperties,
}> = ({ title, body, style }) => {
const [expanded, setExpanded] = React.useState(true);
return <div style={{ ...style, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', whiteSpace: 'nowrap' }}>
<div
className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')}
style={{ cursor: 'pointer', color: 'var(--color)', marginRight: '4px'}}
onClick={() => setExpanded(!expanded)} />
{title}
</div>
{ expanded && <div style={{ display: 'flex', flex: 'auto', margin: '5px 0 5px 20px' }}>{body}</div> }
</div>;
};

Просмотреть файл

@ -0,0 +1,68 @@
/*
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.
*/
.network-tab {
display: flex;
flex-direction: column;
flex: auto;
overflow: auto;
}
.network-tab:focus {
outline: none;
}
.network-request {
box-shadow: var(--box-shadow);
white-space: nowrap;
display: flex;
align-items: center;
padding: 0 10px;
margin-bottom: 10px;
background: #fdfcfc;
width: 100%;
border: 3px solid transparent;
flex: none;
outline: none;
}
.network-request-title {
height: 36px;
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
}
.network-request-details {
font-family: var(--monospace-font);
white-space: pre;
overflow: hidden;
}
.network-request-title > div {
overflow: hidden;
text-overflow: ellipsis;
}
.network-request.selected,
.network-request:hover {
border-color: var(--inactive-focus-ring);
}
.network-request.selected:focus {
border-color: var(--orange);
}

Просмотреть файл

@ -0,0 +1,39 @@
/**
* 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 { ActionEntry } from '../../traceModel';
import './networkTab.css';
import * as React from 'react';
import { Expandable } from './helpers';
export const NetworkTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined,
}> = ({ actionEntry }) => {
const [selected, setSelected] = React.useState(0);
return <div className='network-tab'>{
(actionEntry ? actionEntry.resources : []).map((resource, index) => {
return <div key={index}
className={'network-request ' + (index === selected ? 'selected' : '')}
onClick={() => setSelected(index)}>
<Expandable style={{ width: '100%' }} title={
<div className='network-request-title'><div>resource.url</div></div>
} body={
<div className='network-request-details'>{resource.responseHeaders.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
}/>
</div>;
})
}</div>;
};

Просмотреть файл

@ -0,0 +1,86 @@
/*
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.
*/
.properties-tabbed-pane {
display: flex;
flex: auto;
overflow: hidden;
}
.properties-tab-content {
display: flex;
flex: auto;
overflow: hidden;
}
.properties-tab-strip {
flex: auto;
display: flex;
flex-direction: row;
align-items: center;
height: 34px;
}
.properties-tab-strip:focus {
outline: none;
}
.properties-tab-element {
padding: 2px 6px 0 6px;
margin-right: 4px;
cursor: pointer;
display: flex;
flex: none;
align-items: center;
justify-content: center;
user-select: none;
border-bottom: 3px solid transparent;
width: 80px;
outline: none;
}
.properties-tab-label {
max-width: 250px;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.properties-tab-element.selected {
border-bottom-color: var(--color);
}
.properties-tab-element:hover {
font-weight: 600;
}
.snapshot-wrapper {
flex: auto;
margin: 1px;
}
.snapshot-container {
display: block;
background: white;
outline: 1px solid #aaa;
}
iframe#snapshot {
width: 100%;
height: 100%;
border: none;
}

Просмотреть файл

@ -0,0 +1,88 @@
/**
* 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 { ActionEntry } from '../../traceModel';
import { Size } from '../geometry';
import { NetworkTab } from './networkTab';
import { SourceTab } from './sourceTab';
import './propertiesTabbedPane.css';
import * as React from 'react';
import { useMeasure } from './helpers';
export const PropertiesTabbedPane: React.FunctionComponent<{
actionEntry: ActionEntry | undefined,
snapshotSize: Size,
}> = ({ actionEntry, snapshotSize }) => {
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network'>('snapshot');
return <div className='properties-tabbed-pane'>
<div className='vbox'>
<div className='hbox' style={{ flex: 'none' }}>
<div className='properties-tab-strip'>
<div className={'properties-tab-element ' + (selected === 'snapshot' ? 'selected' : '')}
onClick={() => setSelected('snapshot')}>
<div className='properties-tab-label'>Snapshot</div>
</div>
<div className={'properties-tab-element ' + (selected === 'source' ? 'selected' : '')}
onClick={() => setSelected('source')}>
<div className='properties-tab-label'>Source</div>
</div>
<div className={'properties-tab-element ' + (selected === 'network' ? 'selected' : '')}
onClick={() => setSelected('network')}>
<div className='properties-tab-label'>Network</div>
</div>
</div>
</div>
<div className='properties-tab-content' style={{ display: selected === 'snapshot' ? 'flex' : 'none' }}>
<SnapshotTab actionEntry={actionEntry} snapshotSize={snapshotSize} />
</div>
<div className='properties-tab-content' style={{ display: selected === 'source' ? 'flex' : 'none' }}>
<SourceTab actionEntry={actionEntry} />
</div>
<div className='properties-tab-content' style={{ display: selected === 'network' ? 'flex' : 'none' }}>
<NetworkTab actionEntry={actionEntry} />
</div>
</div>
</div>;
};
const SnapshotTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined,
snapshotSize: Size,
}> = ({ actionEntry, snapshotSize }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();
const iframeRef = React.createRef<HTMLIFrameElement>();
React.useEffect(() => {
if (iframeRef.current && !actionEntry)
iframeRef.current.src = 'about:blank';
}, [actionEntry, iframeRef]);
React.useEffect(() => {
if (actionEntry)
window.renderSnapshot(actionEntry.action);
}, [actionEntry]);
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
return <div ref={ref} className='snapshot-wrapper'>
<div className='snapshot-container' style={{
width: snapshotSize.width + 'px',
height: snapshotSize.height + 'px',
transform: `translate(${-snapshotSize.width * (1 - scale) / 2}px, ${-snapshotSize.height * (1 - scale) / 2}px) scale(${scale})`,
}}>
<iframe ref={iframeRef} id='snapshot' name='snapshot'></iframe>
</div>
</div>;
};

Просмотреть файл

@ -0,0 +1,43 @@
/*
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.
*/
.source-tab {
flex: auto;
position: relative;
overflow: auto;
background: #fdfcfc;
font-family: var(--monospace-font);
white-space: nowrap;
}
.source-line-number {
width: 80px;
border-right: 1px solid var(--separator);
display: inline-block;
margin-right: 3px;
text-align: end;
padding-right: 4px;
color: var(--gray);
}
.source-code {
white-space: pre;
display: inline-block;
}
.source-line-highlight {
background-color: #ff69b460;
}

Просмотреть файл

@ -0,0 +1,75 @@
/**
* 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 { ActionEntry } from '../../traceModel';
import * as React from 'react';
import { useAsyncMemo } from './helpers';
import './sourceTab.css';
import '../../../../third_party/highlightjs/highlightjs/tomorrow.css';
import * as highlightjs from '../../../../third_party/highlightjs/highlightjs';
export const SourceTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined,
}> = ({ actionEntry }) => {
const location = React.useMemo<{ fileName?: string, lineNumber?: number, value?: string }>(() => {
if (!actionEntry)
return { value: '' };
const { action } = actionEntry;
const frames = action.stack!.split('\n').slice(1);
const frame = frames.filter(frame => !frame.includes('playwright/lib/') && !frame.includes('playwright/src/'))[0];
if (!frame)
return { value: action.stack! };
const match = frame.match(/at [^(]+\(([^:]+):(\d+):\d+\)/) || frame.match(/at ([^:^(]+):(\d+):\d+/);
if (!match)
return { value: action.stack! };
const fileName = match[1];
const lineNumber = parseInt(match[2], 10);
return { fileName, lineNumber };
}, [actionEntry]);
const content = useAsyncMemo<string[]>(async () => {
const value = location.fileName ? await window.readFile(location.fileName) : location.value;
const result = [];
let continuation: any;
for (const line of (value || '').split('\n')) {
const highlighted = highlightjs.highlight('javascript', line, true, continuation);
continuation = highlighted.top;
result.push(highlighted.value);
}
return result;
}, [location.fileName, location.value], []);
const targetLineRef = React.createRef<HTMLDivElement>();
React.useLayoutEffect(() => {
if (targetLineRef.current)
targetLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' });
}, [content, location.lineNumber, targetLineRef]);
return <div className='source-tab'>{
content.map((markup, index) => {
const isTargetLine = (index + 1) === location.lineNumber;
return <div
key={index}
className={isTargetLine ? 'source-line-highlight' : ''}
ref={isTargetLine ? targetLineRef : null}
>
<div className='source-line-number'>{index + 1}</div>
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
</div>;
})
}
</div>;
};

Просмотреть файл

@ -0,0 +1,110 @@
/*
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.
*/
.timeline-view {
flex: none;
flex-basis: 60px;
position: relative;
display: flex;
flex-direction: column;
background: white;
padding: 20px 0 5px;
cursor: text;
}
.timeline-divider {
position: absolute;
width: 1px;
top: 0;
bottom: 0;
background-color: rgb(0 0 0 / 10%);
}
.timeline-label {
position: absolute;
top: 4px;
right: 3px;
font-size: 80%;
white-space: nowrap;
pointer-events: none;
}
.timeline-lane {
pointer-events: none;
overflow: hidden;
flex: none;
flex-basis: 20px;
position: relative;
}
.timeline-grid {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.timeline-lane.timeline-action-labels {
margin-top: 10px;
}
.timeline-lane.timeline-actions {
margin-bottom: 10px;
}
.timeline-action {
position: absolute;
top: 0;
bottom: 0;
background-color: red;
border-radius: 3px;
}
.timeline-action.click {
background-color: var(--green);
}
.timeline-action.fill,
.timeline-action.press {
background-color: var(--orange);
}
.timeline-action.goto {
background-color: var(--blue);
}
.timeline-action-label {
position: absolute;
top: 0;
bottom: 0;
margin-left: 2px;
background-color: #fffffff0;
}
.timeline-time-bar {
display: none;
position: absolute;
top: 0;
bottom: 0;
width: 3px;
background-color: black;
pointer-events: none;
}
.timeline-time-bar.timeline-time-bar-hover {
background-color: var(--light-pink);
}

Просмотреть файл

@ -0,0 +1,156 @@
/*
* Copyright 2017 Google Inc. All rights reserved.
* Modifications 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 { ContextEntry } from '../../traceModel';
import './timeline.css';
import { FilmStrip } from './filmStrip';
import { Boundaries } from '../geometry';
import * as React from 'react';
import { useMeasure } from './helpers';
export const Timeline: React.FunctionComponent<{
context: ContextEntry,
boundaries: Boundaries,
}> = ({ context, boundaries }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();
const [previewX, setPreviewX] = React.useState<number | undefined>();
const offsets = React.useMemo(() => {
return calculateDividerOffsets(measure.width, boundaries);
}, [measure.width, boundaries]);
const actionEntries = React.useMemo(() => {
const actions = [];
for (const page of context.pages)
actions.push(...page.actions);
return actions;
}, [context]);
const actionTimes = React.useMemo(() => {
return actionEntries.map(entry => {
return {
action: entry.action,
actionId: entry.actionId,
left: timeToPercent(measure.width, boundaries, entry.action.startTime!),
right: timeToPercent(measure.width, boundaries, entry.action.endTime!),
};
});
}, [actionEntries, boundaries, measure.width]);
const onMouseMove = (event: React.MouseEvent) => {
if (ref.current)
setPreviewX(event.clientX - ref.current.getBoundingClientRect().left);
};
const onMouseLeave = () => {
setPreviewX(undefined);
};
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave}>
<div className='timeline-grid'>{
offsets.map((offset, index) => {
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
<div className='timeline-label'>{msToString(offset.time - boundaries.minimum)}</div>
</div>;
})
}</div>
<div className='timeline-lane timeline-action-labels'>{
actionTimes.map(({ action, actionId, left }) => {
return <div key={actionId}
className={'timeline-action-label ' + action.action}
style={{ left: left + '%' }}
>
{action.action}
</div>;
})
}</div>
<div className='timeline-lane timeline-actions'>{
actionTimes.map(({ action, actionId, left, right }) => {
return <div key={actionId}
className={'timeline-action ' + action.action}
style={{
left: left + '%',
width: (right - left) + '%',
}}
></div>;
})
}</div>
<FilmStrip context={context} boundaries={boundaries} previewX={previewX} />
<div className='timeline-time-bar timeline-time-bar-hover' style={{
display: (previewX !== undefined) ? 'block' : 'none',
left: (previewX || 0) + 'px',
}}></div>
</div>;
};
function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { percent: number, time: number }[] {
const minimumGap = 64;
let dividerCount = clientWidth / minimumGap;
const boundarySpan = boundaries.maximum - boundaries.minimum;
const pixelsPerMillisecond = clientWidth / boundarySpan;
let sectionTime = boundarySpan / dividerCount;
const logSectionTime = Math.ceil(Math.log(sectionTime) / Math.LN10);
sectionTime = Math.pow(10, logSectionTime);
if (sectionTime * pixelsPerMillisecond >= 5 * minimumGap)
sectionTime = sectionTime / 5;
if (sectionTime * pixelsPerMillisecond >= 2 * minimumGap)
sectionTime = sectionTime / 2;
const firstDividerTime = boundaries.minimum;
let lastDividerTime = boundaries.maximum;
lastDividerTime += minimumGap / pixelsPerMillisecond;
dividerCount = Math.ceil((lastDividerTime - firstDividerTime) / sectionTime);
if (!sectionTime)
dividerCount = 0;
const offsets = [];
for (let i = 0; i < dividerCount; ++i) {
const time = firstDividerTime + sectionTime * i;
offsets.push({ percent: timeToPercent(clientWidth, boundaries, time), time });
}
return offsets;
}
function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number): number {
const position = (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
return 100 * position / clientWidth;
}
function msToString(ms: number): string {
if (!isFinite(ms))
return '-';
if (ms === 0)
return '0';
if (ms < 1000)
return ms.toFixed(0) + 'ms';
const seconds = ms / 1000;
if (seconds < 60)
return seconds.toFixed(1) + 's';
const minutes = seconds / 60;
if (minutes < 60)
return minutes.toFixed(1) + 's';
const hours = minutes / 60;
if (hours < 24)
return hours.toFixed(1) + 'h';
const days = hours / 24;
return days.toFixed(1) + 'h';
}

Просмотреть файл

@ -0,0 +1,59 @@
/*
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.
*/
.workbench {
contain: size;
}
.workbench .header {
display: flex;
background-color: #000;
flex: none;
flex-basis: 48px;
line-height: 48px;
font-size: 16px;
color: white;
}
.workbench tab-content {
padding: 25px;
contain: size;
}
.workbench tab-strip {
margin-left: calc(-1*var(--sidebar-width));
padding-left: var(--sidebar-width);
box-shadow: var(--box-shadow);
}
.workbench .logo {
font-size: 20px;
margin-left: 16px;
}
.workbench .product {
font-weight: 600;
margin-left: 16px;
}
.workbench .spacer {
flex: auto;
}
tab-strip {
background-color: var(--light-background);
}

Просмотреть файл

@ -0,0 +1,67 @@
/*
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 { ActionEntry, TraceModel } from '../../traceModel';
import { ActionList } from './actionList';
import { PropertiesTabbedPane } from './propertiesTabbedPane';
import { Timeline } from './timeline';
import './workbench.css';
import * as React from 'react';
import { ContextSelector } from './contextSelector';
export const Workbench: React.FunctionComponent<{
traceModel: TraceModel,
}> = ({ traceModel }) => {
const [context, setContext] = React.useState(traceModel.contexts[0]);
const [action, setAction] = React.useState<ActionEntry | undefined>();
const actions = React.useMemo(() => {
const actions: ActionEntry[] = [];
for (const page of context.pages)
actions.push(...page.actions);
return actions;
}, [context]);
const snapshotSize = context.created.viewportSize!;
return <div className='vbox workbench'>
<div className='hbox header'>
<div className='logo'>🎭</div>
<div className='product'>Playwright</div>
<div className='spacer'></div>
<ContextSelector
contexts={traceModel.contexts}
context={context}
onChange={context => {
setContext(context);
setAction(undefined);
}}
/>
</div>
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none' }}>
<Timeline
context={context}
boundaries={{ minimum: context.startTime, maximum: context.endTime }}
/>
</div>
<div className='hbox'>
<div style={{ display: 'flex', flex: 'none' }}>
<ActionList actions={actions} selectedAction={action} onSelected={action => setAction(action)} />
</div>
<PropertiesTabbedPane actionEntry={action} snapshotSize={snapshotSize} />
</div>
</div>;
};

Просмотреть файл

@ -0,0 +1,40 @@
const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
app: path.join(__dirname, 'index.tsx'),
},
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx']
},
output: {
globalObject: 'self',
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../../../../lib/cli/traceViewer/web')
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.ttf$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebPackPlugin({
title: 'Playwright Trace Viewer',
template: path.join(__dirname, 'index.html'),
})
]
};

Просмотреть файл

@ -20,6 +20,7 @@ const InlineSource = require('../../server/injected/webpack-inline-source-plugin
module.exports = {
entry: path.join(__dirname, 'consoleApi.ts'),
devtool: 'source-map',
mode: 'development',
module: {
rules: [
{

Просмотреть файл

@ -20,6 +20,7 @@ const InlineSource = require('./webpack-inline-source-plugin.js');
module.exports = {
entry: path.join(__dirname, 'injectedScript.ts'),
devtool: 'source-map',
mode: 'development',
module: {
rules: [
{

Просмотреть файл

@ -20,6 +20,7 @@ const InlineSource = require('./webpack-inline-source-plugin.js');
module.exports = {
entry: path.join(__dirname, 'utilityScript.ts'),
devtool: 'source-map',
mode: 'development',
module: {
rules: [
{

72
src/third_party/highlightjs/highlightjs/tomorrow.css поставляемый Normal file
Просмотреть файл

@ -0,0 +1,72 @@
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
background: white;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

2
src/third_party/highlightjs/roll.sh поставляемый
Просмотреть файл

@ -6,6 +6,7 @@ set +x
# https://github.com/highlightjs/highlight.js/releases
RELEASE_REVISION="af20048d5c601d6e30016d8171317bfdf8a6c242"
LANGUAGES="javascript python csharp"
STYLES="tomorrow.css"
trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
@ -27,5 +28,6 @@ mkdir -p ./highlightjs
cp -R output/highlight.js/build/lib/* highlightjs/
cp output/highlight.js/build/LICENSE highlightjs/
cp output/highlight.js/build/types/index.d.ts highlightjs/
cp output/highlight.js/build/styles/${STYLES} highlightjs/
echo $'\n'"export = hljs;"$'\n' >> highlightjs/index.d.ts
rm -rf ./output

Просмотреть файл

@ -8,9 +8,10 @@
"outDir": "./lib",
"strict": true,
"allowJs": true,
"declaration": false
"declaration": false,
"jsx": "react"
},
"compileOnSave": true,
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["node_modules", "src/.eslintrc.js"]
"exclude": ["node_modules", "src/.eslintrc.js", "src/cli/traceViewer/web/**"]
}

Просмотреть файл

@ -67,11 +67,12 @@ const webPackFiles = [
'src/server/injected/utilityScript.webpack.config.js',
'src/debug/injected/consoleApi.webpack.config.js',
'src/cli/injected/recorder.webpack.config.js',
'src/cli/traceViewer/web/web.webpack.config.js',
];
for (const file of webPackFiles) {
steps.push({
command: 'npx',
args: ['webpack', '--config', filePath(file), '--mode', 'development', ...(watchMode ? ['--watch', '--silent'] : [])],
args: ['webpack', '--config', filePath(file), ...(watchMode ? ['--watch', '--silent', '--mode', 'development'] : [])],
shell: true,
});
}

Просмотреть файл

@ -150,7 +150,7 @@ DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/ser
DEPS['src/service.ts'] = ['src/remote/'];
// CLI should only use client-side features.
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/'];
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
checkDeps().catch(e => {
console.error(e && e.stack ? e.stack : e);