코딩하는 고릴라

playwright ui 모드 분석 - 2. 선택 실행 기능 추가 본문

오픈소스

playwright ui 모드 분석 - 2. 선택 실행 기능 추가

코릴라입니다 2025. 7. 7. 23:15

분석 과정은 배제하고 결과 위주로 기술하겠습니다.

 

🦍 컴포넌트 구조

선택 실행 기능 추가를 위해 수정할 컴포넌트의 계층 구조는 다음과 같습니다.

UIModeView > TestListView > TreeView > TreeItemHeader

uiModeView 컴포넌트uiModeTestListView, treeView 컴포넌트
1. UIModeView 컴포넌트 2. TestListView, treeView 컴포넌트 3. TreeItemHeader

🐖 상태관리

최상위 컴포넌트인 uiModeView에서 [1. 선택된 테스트 케이스를 관리할 상태]와, [2. 테스트 케이스 선택을 위한 핸들러], 그리고 [3. 선택된 테스트 케이스를 실행시킬 함수] 를 작성하고,  상태와 핸들러를 TreeItemHeader 컴포넌트까지 내려줬습니다.

 

1. 선택된 테스트 케이스를 관리할 상태

// uiModeView.tsx

const [selectedItems, setSelectedItems] = React.useState<Set<TreeItem>>(new Set());

 

실행을 위해 선택된 테스트 케이스를 TreeItem 타입의 객체를 저장하고 있는 Set의 형태로 관리합니다.

TreeItem은 테스트 실행을 위해 필요한 TestId와 테스트 케이스 간 위계 관계(폴더, 파일, 파일 내 개별 테스트 케이스)와 같은 정보를 가지고 있습니다.

 

2. 테스트 케이스 선택을 위한 핸들러

// uiModeView.tsx

const handleMultiSelect = React.useCallback((treeItem: TreeItem) => {
  const currentSelectedItems = new Set(selectedItems.values());
  const action = selectedItems.has(treeItem)
    ? (treeItem: TreeItem) => { currentSelectedItems.delete(treeItem) }
    : (treeItem: TreeItem) => { currentSelectedItems.add(treeItem) }

  const visit = (treeItem: TreeItem) => {
    action(treeItem);
    for (const child of treeItem.children) {
      visit(child); 
    }
  }
  visit(treeItem);
  setSelectedItems(currentSelectedItems);
}, [selectedItems, setSelectedItems])

 

treeView 컴포넌트에서 개별 테스트 케이스 클릭 시 동작하는 핸들러입니다. 

선택되지 않은 항목이라면, 재귀적으로 탐색해 하위에 존재하는 모든 항목들을 선택합니다.

선택된 항목이라면, 재귀적으로 탐색해 하위에 존재하는 모든 항목들의 선택을 해제합니다. 

 

3. 선택된 테스트 케이스를 실행시킬 함수

const runSelectedTests = React.useCallback(() => {
  const testIds = new Set<string>();
  const visited = new Set<TreeItem>();

  const visit = (treeItem: TreeItem) => {      
    visited.add(treeItem);

    if (treeItem.kind === 'case' && treeItem.test) {
      testIds.add(treeItem.test.id);
    } else {
      for (const child of treeItem.children) {
        if (!visited.has(child)) {
          visit(child);
        }
      }
    }
  }

  for (const item of selectedItems) {
    if (visited.has(item)) continue;
    visit(item);
  }

  runTests('bounce-if-busy', testIds);
  setSelectedItems(new Set());
}, [selectedItems, setSelectedItems])

 

 

테스트 케이스 실행을 위해서는 개별 테스트 케이스가 가진 id를 필요로 합니다.

선택된 테스트 항목을 TreeItem 형태로 관리했으니, 실행하는 시점에 하위에 존재하는 모든 테스트 케이스 id를 모아 runTests 함수의 인자로 넘겨줍니다.

 

4. runTests 함수

const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set<string>) => {
  // ...
  commandQueue.current = commandQueue.current.then(async () => {
    // ...
    setSelectedItems(new Set()); // 테스트 실행시, 기존에 선택되어있던 테스트 항목의 선택을 해제
    // ...
  });
}, [projectFilters, isRunningTest, testModel, testServerConnection, singleWorker, showBrowser, updateSnapshots]);

 

기존에 구현되어 있던 runTests 함수 내부에 선택된 테스트 항목을 초기화하는 코드를 작성합니다.

 

runTests를 제외하고 위에서 구현한 2가지 내용(상태, 핸들러) 을 treeView.tsx 파일의 TreeItemHeader 까지 내려줍니다.

// treeView.tsx

export function TreeItemHeader<T extends TreeItem>({
// ...
onMultiSelect,
selectedItems }: TreeItemHeaderProps<T>) {
const groupId = React.useId();
const itemRef = React.useRef(null);

// ...

return <div ref={itemRef} role='treeitem' aria-selected={item === selectedItem} aria-expanded={expanded} aria-controls={groupId} title={titled} className='vbox' style={{ flex: 'none' }}>
  <div
    onDoubleClick={() => onAccepted?.(item)}
    className={clsx(
        'tree-view-entry',
        selectedItem === item && 'selected',
        highlightedItem === item && 'highlighted',
        selectedItems?.has(item) && 'checked', // 선택된 경우, 'checked' 클래스를 추가
        isError?.(item) && 'error')}
    onClick={() => {
        onSelected?.(item)
        onMultiSelect?.(item) // 위에서 구현한 핸들러
      }
    }
    onMouseEnter={() => setHighlightedItem(item)}
    onMouseLeave={() => setHighlightedItem(undefined)}
  >
    // ...
}

 

TreeItemHeader 컴포넌트에서는 선택 상태에 따른 class 변화와 위에서 구현한 핸들러를 통해 테스트 항목 선택, 선택 해제 기능을 가지게끔 코드를 수정했습니다.

 

5. 선택된 테스트 실행

// uiModeView.tsx

export const UIModeView: React.FC<{}> = ({
}) => {

  // ...
  
  return <div className='vbox ui-mode'>
    // ...
          <ToolbarButton icon='play' title='Run all — F5' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
          
          // 선택된 테스트 항목을 실행시킬 수 있도록 아래 툴바버튼을 추가
          <ToolbarButton icon='play-circle' title='Run selective' onClick={runSelectedTests} disabled={ isRunningTest || isLoading || selectedItems.size === 0 }></ToolbarButton>
          
          // ...
};

 

6. CSS 추가

/* treeView.css */

.tree-view-entry.checked {
  background-color: var(--vscode-list-inactiveSelectionBackground);
}

 

선택된 항목의 경우, 'checked' 클래스를 추가하게끔 구현했으므로, checked 클래스에 대해 배경색을 변화시켜 선택됨을 식별할 수 있는 CSS를 추가하였습니다.

 

 

🐕 결과

 

상단 툴바의 전체 실행, 실행 중단 버튼 사이에 새로운 실행 버튼이 추가됐으며,

테스트 항목 선택시 음영이 적용되며, 새로 생긴 실행 버튼을 클릭해 선택된 테스트 케이스만을 실행시킬 수 있음을 볼 수 있습니다.

 

🐩 고찰

개발에 있어서 항상 소비자의 입장에서만 개발을 진행했습니다. 짜임새있게 만들어진 프레임워크, 라이브러리를 활용하며 주어진 요구사항을 만족하는데만 급급했었으나, 때로는 기존의 라이브러리에서 지원하지 않는 기능들을 커스터마이징하며 더 가치있고, 효율적으로 서비스를 활용하는 방법을 배울 수 있었습니다.

반응형

'오픈소스' 카테고리의 다른 글

playwright ui 모드 분석 - 1. 개요  (0) 2025.05.14