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

Code before:

import type { TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
import { msToString } from './utils';
import { Chip } from './chip';
import { Link, LinkBadge, SearchParamsContext, testResultHref, TraceLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image } from './icons';
import { clsx } from '@web/uiUtils';
import { ProjectAndTagLabelsView } from './labels';

export const TestFileView: React.FC<React.PropsWithChildren<{
  file: TestFileSummary;
  projectNames: string[];
  isFileExpanded?: (fileId: string) => boolean;
  setFileExpanded?: (fileId: string, expanded: boolean) => void;
  footer?: React.JSX.Element | string;
}>> = ({ file, projectNames, isFileExpanded, setFileExpanded, footer }) => {
  const searchParams = React.useContext(SearchParamsContext);
  const linkedSearchParams = new URLSearchParams();
  if (searchParams.has('q'))
    linkedSearchParams.set('q', searchParams.get('q')!);
  if (searchParams.has('speedboard'))
    linkedSearchParams.set('speedboard', '');
  const filterParam = linkedSearchParams.size ? '&' + linkedSearchParams : '';
  return <Chip
    expanded={isFileExpanded ? isFileExpanded(file.fileId) : undefined}
    noInsets={true}
    setExpanded={setFileExpanded ? (expanded => setFileExpanded(file.fileId, expanded)) : undefined}
    header={<span className='chip-header-allow-selection'>
      {file.fileName}
    </span>}
    footer={footer}
  >
    {file.tests.map(test =>
      <div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome)}>
        <div className='hbox' style={{ alignItems: 'flex-start' }}>
          <div className='hbox'>
            <span className='test-file-test-status-icon'>
              {statusIcon(test.outcome)}
            </span>
            <span>
              <Link href={testResultHref({ test }) + filterParam} title={[...test.path, test.title].join(' › ')}>
                <span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
              </Link>
              <ProjectAndTagLabelsView style={{ marginLeft: '6px' }} projectNames={projectNames} activeProjectName={test.projectName} otherLabels={test.tags} />
            </span>
          </div>
          <span data-testid='test-duration' style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
        </div>

Review comment (refactor):
Should we make `testResultHref()` function inherit `speedboard` param?

Overall, it seems like a good strategy to always inherit all the params, and only add/remove the ones we care about. For example, add `testId` when going to a test, add/remove `speedboard` when toggling between modes. I single helper function for this would be nice.

Code after:

import type { TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
import { msToString } from './utils';
import { Chip } from './chip';
import { Link, LinkBadge, SearchParamsContext, testResultHref, TraceLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image } from './icons';
import { clsx } from '@web/uiUtils';
import { ProjectAndTagLabelsView } from './labels';

export const TestFileView: React.FC<React.PropsWithChildren<{
  file: TestFileSummary;
  projectNames: string[];
  isFileExpanded?: (fileId: string) => boolean;
  setFileExpanded?: (fileId: string, expanded: boolean) => void;
  footer?: React.JSX.Element | string;
}>> = ({ file, projectNames, isFileExpanded, setFileExpanded, footer }) => {
  const searchParams = React.useContext(SearchParamsContext);
  return <Chip
    expanded={isFileExpanded ? isFileExpanded(file.fileId) : undefined}
    noInsets={true}
    setExpanded={setFileExpanded ? (expanded => setFileExpanded(file.fileId, expanded)) : undefined}
    header={<span className='chip-header-allow-selection'>
      {file.fileName}
    </span>}
    footer={footer}
  >
    {file.tests.map(test =>
      <div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome)}>
        <div className='hbox' style={{ alignItems: 'flex-start' }}>
          <div className='hbox'>
            <span className='test-file-test-status-icon'>
              {statusIcon(test.outcome)}
            </span>
            <span>
              <Link href={testResultHref({ test }, searchParams)} title={[...test.path, test.title].join(' › ')}>
                <span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
              </Link>
              <ProjectAndTagLabelsView style={{ marginLeft: '6px' }} projectNames={projectNames} activeProjectName={test.projectName} otherLabels={test.tags} />
            </span>
          </div>
          <span data-testid='test-duration' style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
        </div>
        <div className='test-file-details-row'>
          <div className='test-file-details-row-items'>
            <Link href={testResultHref({ test }, searchParams)} title={[...test.path, test.title].join(' › ')} className='test-file-path-link'>
              <span className='test-file-path'>{test.location.file}:{test.location.line}</span>
            </Link>
            <ImageDiffBadge test={test} />