Repository: microsoft/playwright
File: packages/html-reporter/src/reportView.tsx (TypeScript)

Code before:
  See the License for the specific language governing permissions and
  limitations under the License.
*/

import type { FilteredStats, TestCase, TestCaseSummary, TestFile, TestFileSummary } from './types';
import * as React from 'react';
import './colors.css';
import './common.css';
import { Filter, filterWithQuery } from './filter';
import { HeaderView, GlobalFilterView } from './headerView';
import { navigate, Route, SearchParamsContext, testResultHref } from './links';
import type { LoadedReport } from './loadedReport';
import './reportView.css';
import { TestCaseView } from './testCaseView';
import { TestFilesHeader, TestFilesView } from './testFilesView';
import './theme.css';
import { useSetting } from '@web/uiUtils';
import { Speedboard } from './speedboard';

declare global {
  interface Window {
    playwrightReportBase64?: string;
  }
}

type TestModelSummary = {
  files: TestFileSummary[];
  tests: TestCaseSummary[];
};

export const ReportView: React.FC<{
  report: LoadedReport | undefined,
}> = ({ report }) => {
  const searchParams = React.useContext(SearchParamsContext);
  const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
  const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
  const [metadataVisible, setMetadataVisible] = React.useState(false);
  const speedboard = searchParams.has('speedboard');
  const [mergeFiles] = useSetting('mergeFiles', false);
  const testId = searchParams.get('testId');
  const q = searchParams.get('q')?.toString() || '';
  const filterParam = q ? '&q=' + q : '';
  const reportTitle = report?.json()?.options.title;

  const testIdToFileIdMap = React.useMemo(() => {
    const map = new Map<string, string>();
    for (const file of report?.json().files || []) {
      for (const test of file.tests)
        map.set(test.testId, file.fileId);
    }
    return map;

Review comment (suggestion):
`useCallback` also preserves identity, and allows keeping definition closer to usage

Code after:
  See the License for the specific language governing permissions and
  limitations under the License.
*/

import type { FilteredStats, TestCase, TestCaseSummary, TestFile, TestFileSummary } from './types';
import * as React from 'react';
import './colors.css';
import './common.css';
import { Filter, filterWithQuery } from './filter';
import { HeaderView, GlobalFilterView } from './headerView';
import { navigate, Route, SearchParamsContext, testResultHref } from './links';
import type { LoadedReport } from './loadedReport';
import './reportView.css';
import { TestCaseView } from './testCaseView';
import { TestFilesHeader, TestFilesView } from './testFilesView';
import './theme.css';
import { useSetting } from '@web/uiUtils';
import { Speedboard } from './speedboard';

declare global {
  interface Window {
    playwrightReportBase64?: string;
  }
}

// These are extracted to preserve the function identity between renders to avoid re-triggering effects.
const testFilesRoutePredicate = (params: URLSearchParams) => !params.has('testId') && !params.has('speedboard');
const testCaseRoutePredicate = (params: URLSearchParams) => params.has('testId');
const speedboardRoutePredicate = (params: URLSearchParams) => params.has('speedboard') && !params.has('testId');

type TestModelSummary = {
  files: TestFileSummary[];
  tests: TestCaseSummary[];
};

export const ReportView: React.FC<{
  report: LoadedReport | undefined,
}> = ({ report }) => {
  const searchParams = React.useContext(SearchParamsContext);
  const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
  const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
  const [metadataVisible, setMetadataVisible] = React.useState(false);
  const speedboard = searchParams.has('speedboard');
  const [mergeFiles] = useSetting('mergeFiles', false);
  const testId = searchParams.get('testId');
  const q = searchParams.get('q')?.toString() || '';
  const filterParam = q ? '&q=' + q : '';
  const reportTitle = report?.json()?.options.title;

  const testIdToFileIdMap = React.useMemo(() => {
    const map = new Map<string, string>();