/** * 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 '../../server/trace/traceTypes'; export * as trace from '../../server/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: { [key: string]: { resourceId: string, frameId: string }[] }; overridenUrls: { [key: string]: boolean }; } export type VideoEntry = { video: trace.PageVideoTraceEvent; videoId: string; }; export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent; export type PageEntry = { created: trace.PageCreatedTraceEvent; destroyed: trace.PageDestroyedTraceEvent; video?: VideoEntry; actions: ActionEntry[]; interestingEvents: InterestingPageEvent[]; resources: trace.NetworkResourceTraceEvent[]; snapshotsByFrameId: { [key: string]: trace.FrameSnapshotTraceEvent[] }; } export type ActionEntry = { actionId: string; action: trace.ActionTraceEvent; thumbnailUrl: string; resources: trace.NetworkResourceTraceEvent[]; }; export type VideoMetaInfo = { frames: number; width: number; height: number; fps: number; startTime: number; endTime: number; }; const kInterestingActions = ['click', 'dblclick', 'hover', 'check', 'uncheck', 'tap', 'fill', 'press', 'type', 'selectOption', 'setInputFiles', 'goto', 'setContent', 'goBack', 'goForward', 'reload']; export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) { const contextEntries = new Map(); const pageEntries = new Map(); for (const event of events) { switch (event.type) { case 'context-created': { contextEntries.set(event.contextId, { filePath, name: event.debugName || filePath.substring(filePath.lastIndexOf('/') + 1), startTime: Number.MAX_VALUE, endTime: Number.MIN_VALUE, created: event, destroyed: undefined as any, pages: [], resourcesByUrl: {}, overridenUrls: {} }); 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: [], interestingEvents: [], snapshotsByFrameId: {}, }; 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': { if (!kInterestingActions.includes(event.method)) break; const pageEntry = pageEntries.get(event.pageId!)!; const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length; const action: ActionEntry = { actionId, action: event, thumbnailUrl: `/action-preview/${actionId}.png`, resources: pageEntry.resources, }; pageEntry.resources = []; pageEntry.actions.push(action); break; } case 'resource': { 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); break; } case 'dialog-opened': case 'dialog-closed': case 'navigation': case 'load': { const pageEntry = pageEntries.get(event.pageId)!; pageEntry.interestingEvents.push(event); break; } case 'snapshot': { const pageEntry = pageEntries.get(event.pageId!)!; if (!(event.frameId in pageEntry.snapshotsByFrameId)) pageEntry.snapshotsByFrameId[event.frameId] = []; pageEntry.snapshotsByFrameId[event.frameId]!.push(event); break; } } const contextEntry = contextEntries.get(event.contextId)!; contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp); contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp); } traceModel.contexts.push(...contextEntries.values()); preprocessModel(traceModel); } function preprocessModel(traceModel: TraceModel) { for (const contextEntry of traceModel.contexts) { const appendResource = (event: trace.NetworkResourceTraceEvent) => { let responseEvents = contextEntry.resourcesByUrl[event.url]; if (!responseEvents) { responseEvents = []; contextEntry.resourcesByUrl[event.url] = responseEvents; } responseEvents.push({ frameId: event.frameId, resourceId: event.resourceId }); }; for (const pageEntry of contextEntry.pages) { for (const action of pageEntry.actions) action.resources.forEach(appendResource); pageEntry.resources.forEach(appendResource); for (const snapshots of Object.values(pageEntry.snapshotsByFrameId)) { for (let i = 0; i < snapshots.length; ++i) { const snapshot = snapshots[i]; for (const override of snapshot.snapshot.resourceOverrides) { if (override.ref) { const refOverride = snapshots[i - override.ref]?.snapshot.resourceOverrides.find(o => o.url === override.url); override.sha1 = refOverride?.sha1; delete override.ref; } contextEntry.overridenUrls[override.url] = true; } } } } } } 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 }; }