Repository: microsoft/playwright
File: packages/trace-viewer/src/ui/modelUtil.ts (TypeScript)

Code before:
  readonly visibleAttachments: Attachment[];
  readonly events: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[];
  readonly stdio: trace.StdioTraceEvent[];
  readonly errors: trace.ErrorTraceEvent[];
  readonly errorDescriptors: ErrorDescription[];
  readonly hasSource: boolean;
  readonly hasStepData: boolean;
  readonly sdkLanguage: Language | undefined;
  readonly testIdAttributeName: string | undefined;
  readonly sources: Map<string, SourceModel>;
  resources: ResourceSnapshot[];
  readonly actionCounters: Map<string, number>;
  readonly traceUrl: string;


  constructor(traceUrl: string, contexts: ContextEntry[]) {
    contexts.forEach(contextEntry => indexModel(contextEntry));
    const libraryContext = contexts.find(context => context.origin === 'library');

    this.traceUrl = traceUrl;
    this.browserName = libraryContext?.browserName || '';
    this.sdkLanguage = libraryContext?.sdkLanguage;
    this.channel = libraryContext?.channel;
    this.testIdAttributeName = libraryContext?.testIdAttributeName;
    this.platform = libraryContext?.platform || '';
    this.playwrightVersion = libraryContext?.playwrightVersion;
    this.title = libraryContext?.title || '';
    this.options = libraryContext?.options || {};
    // Next call updates all timestamps for all events in library contexts, so it must be done first.
    this.actions = mergeActionsAndUpdateTiming(contexts);
    this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
    this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE);
    this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
    this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
    this.events = ([] as (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]).concat(...contexts.map(c => c.events));
    this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio));
    this.errors = ([] as trace.ErrorTraceEvent[]).concat(...contexts.map(c => c.errors));
    this.hasSource = contexts.some(c => c.hasSource);
    this.hasStepData = contexts.some(context => context.origin === 'testRunner');
    this.resources = [...contexts.map(c => c.resources)].flat();
    this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUrl })) ?? []);
    this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_'));

    this.events.sort((a1, a2) => a1.time - a2.time);
    this.resources.sort((a1, a2) => a1._monotonicTime! - a2._monotonicTime!);
    this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions();
    this.sources = collectSources(this.actions, this.errorDescriptors);

    this.actionCounters = new Map();
    for (const action of this.actions) {
      action.group = action.group ?? getActionGroup({ type: action.class, method: action.method });

Review comment (suggestion):
I'd also report this in `TestTracing`, and take the version from any context where it's available, to accommodate more scenarios where library is not involved.

Code after:
  readonly visibleAttachments: Attachment[];
  readonly events: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[];
  readonly stdio: trace.StdioTraceEvent[];
  readonly errors: trace.ErrorTraceEvent[];
  readonly errorDescriptors: ErrorDescription[];
  readonly hasSource: boolean;
  readonly hasStepData: boolean;
  readonly sdkLanguage: Language | undefined;
  readonly testIdAttributeName: string | undefined;
  readonly sources: Map<string, SourceModel>;
  resources: ResourceSnapshot[];
  readonly actionCounters: Map<string, number>;
  readonly traceUrl: string;


  constructor(traceUrl: string, contexts: ContextEntry[]) {
    contexts.forEach(contextEntry => indexModel(contextEntry));
    const libraryContext = contexts.find(context => context.origin === 'library');

    this.traceUrl = traceUrl;
    this.browserName = libraryContext?.browserName || '';
    this.sdkLanguage = libraryContext?.sdkLanguage;
    this.channel = libraryContext?.channel;
    this.testIdAttributeName = libraryContext?.testIdAttributeName;
    this.platform = libraryContext?.platform || '';
    this.playwrightVersion = contexts.find(c => c.playwrightVersion)?.playwrightVersion;
    this.title = libraryContext?.title || '';
    this.options = libraryContext?.options || {};
    // Next call updates all timestamps for all events in library contexts, so it must be done first.
    this.actions = mergeActionsAndUpdateTiming(contexts);
    this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
    this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE);
    this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
    this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
    this.events = ([] as (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]).concat(...contexts.map(c => c.events));
    this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio));
    this.errors = ([] as trace.ErrorTraceEvent[]).concat(...contexts.map(c => c.errors));
    this.hasSource = contexts.some(c => c.hasSource);
    this.hasStepData = contexts.some(context => context.origin === 'testRunner');
    this.resources = [...contexts.map(c => c.resources)].flat();
    this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUrl })) ?? []);
    this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_'));

    this.events.sort((a1, a2) => a1.time - a2.time);
    this.resources.sort((a1, a2) => a1._monotonicTime! - a2._monotonicTime!);
    this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions();
    this.sources = collectSources(this.actions, this.errorDescriptors);

    this.actionCounters = new Map();
    for (const action of this.actions) {
      action.group = action.group ?? getActionGroup({ type: action.class, method: action.method });