chore: implement tree w/o list (#33169)
This commit is contained in:
parent
6ea17a5d82
commit
2e8e7a66cd
|
@ -49,8 +49,8 @@ export async function toMatchAriaSnapshot(
|
||||||
|
|
||||||
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
||||||
const notFound = received === kNoElementsFoundError;
|
const notFound = received === kNoElementsFoundError;
|
||||||
const escapedExpected = escapePrivateUsePoints(expected);
|
const escapedExpected = unshift(escapePrivateUsePoints(expected));
|
||||||
const escapedReceived = escapePrivateUsePoints(received);
|
const escapedReceived = unshift(escapePrivateUsePoints(received));
|
||||||
const message = () => {
|
const message = () => {
|
||||||
if (pass) {
|
if (pass) {
|
||||||
if (notFound)
|
if (notFound)
|
||||||
|
@ -79,3 +79,17 @@ export async function toMatchAriaSnapshot(
|
||||||
function escapePrivateUsePoints(str: string) {
|
function escapePrivateUsePoints(str: string) {
|
||||||
return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unshift(snapshot: string): string {
|
||||||
|
const lines = snapshot.split('\n');
|
||||||
|
let whitespacePrefixLength = 100;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim())
|
||||||
|
continue;
|
||||||
|
const match = line.match(/^(\s*)/);
|
||||||
|
if (match && match[1].length < whitespacePrefixLength)
|
||||||
|
whitespacePrefixLength = match[1].length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,30 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
return { selectedItem };
|
return { selectedItem };
|
||||||
}, [itemMap, selectedAction]);
|
}, [itemMap, selectedAction]);
|
||||||
|
|
||||||
|
const isError = React.useCallback((item: ActionTreeItem) => {
|
||||||
|
return !!item.action?.error?.message;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onAccepted = React.useCallback((item: ActionTreeItem) => {
|
||||||
|
return setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime });
|
||||||
|
}, [setSelectedTime]);
|
||||||
|
|
||||||
|
const render = React.useCallback((item: ActionTreeItem) => {
|
||||||
|
return renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true });
|
||||||
|
}, [isLive, revealConsole, sdkLanguage]);
|
||||||
|
|
||||||
|
const isVisible = React.useCallback((item: ActionTreeItem) => {
|
||||||
|
return !selectedTime || !item.action || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum);
|
||||||
|
}, [selectedTime]);
|
||||||
|
|
||||||
|
const onSelectedAction = React.useCallback((item: ActionTreeItem) => {
|
||||||
|
onSelected?.(item.action!);
|
||||||
|
}, [onSelected]);
|
||||||
|
|
||||||
|
const onHighlightedAction = React.useCallback((item: ActionTreeItem | undefined) => {
|
||||||
|
onHighlighted?.(item?.action);
|
||||||
|
}, [onHighlighted]);
|
||||||
|
|
||||||
return <div className='vbox'>
|
return <div className='vbox'>
|
||||||
{selectedTime && <div className='action-list-show-all' onClick={() => setSelectedTime(undefined)}><span className='codicon codicon-triangle-left'></span>Show all</div>}
|
{selectedTime && <div className='action-list-show-all' onClick={() => setSelectedTime(undefined)}><span className='codicon codicon-triangle-left'></span>Show all</div>}
|
||||||
<ActionTreeView
|
<ActionTreeView
|
||||||
|
@ -67,12 +91,12 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
treeState={treeState}
|
treeState={treeState}
|
||||||
setTreeState={setTreeState}
|
setTreeState={setTreeState}
|
||||||
selectedItem={selectedItem}
|
selectedItem={selectedItem}
|
||||||
onSelected={item => onSelected?.(item.action!)}
|
onSelected={onSelectedAction}
|
||||||
onHighlighted={item => onHighlighted?.(item?.action)}
|
onHighlighted={onHighlightedAction}
|
||||||
onAccepted={item => setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime })}
|
onAccepted={onAccepted}
|
||||||
isError={item => !!item.action?.error?.message}
|
isError={isError}
|
||||||
isVisible={item => !selectedTime || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum)}
|
isVisible={isVisible}
|
||||||
render={item => renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true })}
|
render={render}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -161,7 +161,7 @@ export const TestListView: React.FC<{
|
||||||
render={treeItem => {
|
render={treeItem => {
|
||||||
return <div className='hbox ui-mode-tree-item'>
|
return <div className='hbox ui-mode-tree-item'>
|
||||||
<div className='ui-mode-tree-item-title'>
|
<div className='ui-mode-tree-item-title'>
|
||||||
<span title={treeItem.title}>{treeItem.title}</span>
|
<span>{treeItem.title}</span>
|
||||||
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
|
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
|
||||||
</div>
|
</div>
|
||||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-tree-item-time'>{msToString(treeItem.duration)}</div>}
|
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-tree-item-time'>{msToString(treeItem.duration)}</div>}
|
||||||
|
@ -179,6 +179,7 @@ export const TestListView: React.FC<{
|
||||||
</div>;
|
</div>;
|
||||||
}}
|
}}
|
||||||
icon={treeItem => testStatusIcon(treeItem.status)}
|
icon={treeItem => testStatusIcon(treeItem.status)}
|
||||||
|
title={treeItem => treeItem.title}
|
||||||
selectedItem={selectedTreeItem}
|
selectedItem={selectedTreeItem}
|
||||||
onAccepted={runTreeItem}
|
onAccepted={runTreeItem}
|
||||||
onSelected={treeItem => {
|
onSelected={treeItem => {
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
style={style}
|
style={style}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel || title}
|
||||||
>
|
>
|
||||||
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type TreeViewProps<T> = {
|
||||||
name: string,
|
name: string,
|
||||||
rootItem: T,
|
rootItem: T,
|
||||||
render: (item: T) => React.ReactNode,
|
render: (item: T) => React.ReactNode,
|
||||||
|
title?: (item: T) => string,
|
||||||
icon?: (item: T) => string | undefined,
|
icon?: (item: T) => string | undefined,
|
||||||
isError?: (item: T) => boolean,
|
isError?: (item: T) => boolean,
|
||||||
isVisible?: (item: T) => boolean,
|
isVisible?: (item: T) => boolean,
|
||||||
|
@ -52,6 +53,7 @@ export function TreeView<T extends TreeItem>({
|
||||||
name,
|
name,
|
||||||
rootItem,
|
rootItem,
|
||||||
render,
|
render,
|
||||||
|
title,
|
||||||
icon,
|
icon,
|
||||||
isError,
|
isError,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
@ -66,40 +68,12 @@ export function TreeView<T extends TreeItem>({
|
||||||
autoExpandDepth,
|
autoExpandDepth,
|
||||||
}: TreeViewProps<T>) {
|
}: TreeViewProps<T>) {
|
||||||
const treeItems = React.useMemo(() => {
|
const treeItems = React.useMemo(() => {
|
||||||
return flattenTree<T>(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0);
|
return indexTree<T>(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0, isVisible);
|
||||||
}, [rootItem, selectedItem, treeState, autoExpandDepth]);
|
}, [rootItem, selectedItem, treeState, autoExpandDepth, isVisible]);
|
||||||
|
|
||||||
// Filter visible items.
|
|
||||||
const visibleItems = React.useMemo(() => {
|
|
||||||
if (!isVisible)
|
|
||||||
return [...treeItems.keys()];
|
|
||||||
const cachedVisible = new Map<TreeItem, boolean>();
|
|
||||||
const visit = (item: TreeItem): boolean => {
|
|
||||||
const cachedResult = cachedVisible.get(item);
|
|
||||||
if (cachedResult !== undefined)
|
|
||||||
return cachedResult;
|
|
||||||
|
|
||||||
let hasVisibleChildren = item.children.some(child => visit(child));
|
|
||||||
for (const child of item.children) {
|
|
||||||
const result = visit(child);
|
|
||||||
hasVisibleChildren = hasVisibleChildren || result;
|
|
||||||
}
|
|
||||||
const result = isVisible(item as T) || hasVisibleChildren;
|
|
||||||
cachedVisible.set(item, result);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
for (const item of treeItems.keys())
|
|
||||||
visit(item);
|
|
||||||
const result: T[] = [];
|
|
||||||
for (const item of treeItems.keys()) {
|
|
||||||
if (isVisible(item))
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, [treeItems, isVisible]);
|
|
||||||
|
|
||||||
const itemListRef = React.useRef<HTMLDivElement>(null);
|
const itemListRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
||||||
|
const [isKeyboardNavigation, setIsKeyboardNavigation] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
onHighlighted?.(highlightedItem);
|
onHighlighted?.(highlightedItem);
|
||||||
|
@ -171,45 +145,55 @@ export function TreeView<T extends TreeItem>({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = selectedItem ? visibleItems.indexOf(selectedItem) : -1;
|
let newSelectedItem: T | undefined = selectedItem;
|
||||||
let newIndex = index;
|
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
if (index === -1)
|
if (selectedItem) {
|
||||||
newIndex = 0;
|
const itemData = treeItems.get(selectedItem)!;
|
||||||
else
|
newSelectedItem = itemData.next as T;
|
||||||
newIndex = Math.min(index + 1, visibleItems.length - 1);
|
} else if (treeItems.size) {
|
||||||
|
const itemList = [...treeItems.keys()];
|
||||||
|
newSelectedItem = itemList[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
if (index === -1)
|
if (selectedItem) {
|
||||||
newIndex = visibleItems.length - 1;
|
const itemData = treeItems.get(selectedItem)!;
|
||||||
else
|
newSelectedItem = itemData.prev as T;
|
||||||
newIndex = Math.max(index - 1, 0);
|
} else if (treeItems.size) {
|
||||||
|
const itemList = [...treeItems.keys()];
|
||||||
|
newSelectedItem = itemList[itemList.length - 1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = itemListRef.current?.children.item(newIndex);
|
// scrollIntoViewIfNeeded(element || undefined);
|
||||||
scrollIntoViewIfNeeded(element || undefined);
|
|
||||||
onHighlighted?.(undefined);
|
onHighlighted?.(undefined);
|
||||||
onSelected?.(visibleItems[newIndex]);
|
if (newSelectedItem) {
|
||||||
|
setIsKeyboardNavigation(true);
|
||||||
|
onSelected?.(newSelectedItem);
|
||||||
|
}
|
||||||
setHighlightedItem(undefined);
|
setHighlightedItem(undefined);
|
||||||
}}
|
}}
|
||||||
ref={itemListRef}
|
ref={itemListRef}
|
||||||
>
|
>
|
||||||
{noItemsMessage && visibleItems.length === 0 && <div className='tree-view-empty'>{noItemsMessage}</div>}
|
{noItemsMessage && treeItems.size === 0 && <div className='tree-view-empty'>{noItemsMessage}</div>}
|
||||||
{visibleItems.map(item => {
|
{rootItem.children.map(child => {
|
||||||
return <div key={item.id} role='treeitem' aria-selected={item === selectedItem}>
|
const itemData = treeItems.get(child as T);
|
||||||
<TreeItemHeader
|
return itemData && <TreeItemHeader
|
||||||
item={item}
|
key={child.id}
|
||||||
itemData={treeItems.get(item)!}
|
item={child}
|
||||||
selectedItem={selectedItem}
|
treeItems={treeItems}
|
||||||
onSelected={onSelected}
|
selectedItem={selectedItem}
|
||||||
onAccepted={onAccepted}
|
onSelected={onSelected}
|
||||||
isError={isError}
|
onAccepted={onAccepted}
|
||||||
toggleExpanded={toggleExpanded}
|
isError={isError}
|
||||||
highlightedItem={highlightedItem}
|
toggleExpanded={toggleExpanded}
|
||||||
setHighlightedItem={setHighlightedItem}
|
highlightedItem={highlightedItem}
|
||||||
render={render}
|
setHighlightedItem={setHighlightedItem}
|
||||||
icon={icon} />
|
render={render}
|
||||||
</div>;
|
icon={icon}
|
||||||
|
title={title}
|
||||||
|
isKeyboardNavigation={isKeyboardNavigation}
|
||||||
|
setIsKeyboardNavigation={setIsKeyboardNavigation} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -217,7 +201,7 @@ export function TreeView<T extends TreeItem>({
|
||||||
|
|
||||||
type TreeItemHeaderProps<T> = {
|
type TreeItemHeaderProps<T> = {
|
||||||
item: T,
|
item: T,
|
||||||
itemData: TreeItemData,
|
treeItems: Map<T, TreeItemData>,
|
||||||
selectedItem: T | undefined,
|
selectedItem: T | undefined,
|
||||||
onSelected?: (item: T) => void,
|
onSelected?: (item: T) => void,
|
||||||
toggleExpanded: (item: T) => void,
|
toggleExpanded: (item: T) => void,
|
||||||
|
@ -226,12 +210,15 @@ type TreeItemHeaderProps<T> = {
|
||||||
onAccepted?: (item: T) => void,
|
onAccepted?: (item: T) => void,
|
||||||
setHighlightedItem: (item: T | undefined) => void,
|
setHighlightedItem: (item: T | undefined) => void,
|
||||||
render: (item: T) => React.ReactNode,
|
render: (item: T) => React.ReactNode,
|
||||||
|
title?: (item: T) => string,
|
||||||
icon?: (item: T) => string | undefined,
|
icon?: (item: T) => string | undefined,
|
||||||
|
isKeyboardNavigation: boolean,
|
||||||
|
setIsKeyboardNavigation: (value: boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TreeItemHeader<T extends TreeItem>({
|
export function TreeItemHeader<T extends TreeItem>({
|
||||||
item,
|
item,
|
||||||
itemData,
|
treeItems,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
onSelected,
|
onSelected,
|
||||||
highlightedItem,
|
highlightedItem,
|
||||||
|
@ -240,68 +227,122 @@ export function TreeItemHeader<T extends TreeItem>({
|
||||||
onAccepted,
|
onAccepted,
|
||||||
toggleExpanded,
|
toggleExpanded,
|
||||||
render,
|
render,
|
||||||
icon }: TreeItemHeaderProps<T>) {
|
title,
|
||||||
|
icon,
|
||||||
|
isKeyboardNavigation,
|
||||||
|
setIsKeyboardNavigation }: TreeItemHeaderProps<T>) {
|
||||||
|
const itemRef = React.useRef(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (selectedItem === item && isKeyboardNavigation && itemRef.current) {
|
||||||
|
scrollIntoViewIfNeeded(itemRef.current);
|
||||||
|
setIsKeyboardNavigation(false);
|
||||||
|
}
|
||||||
|
}, [item, selectedItem, isKeyboardNavigation, setIsKeyboardNavigation]);
|
||||||
|
|
||||||
|
const itemData = treeItems.get(item)!;
|
||||||
const indentation = itemData.depth;
|
const indentation = itemData.depth;
|
||||||
const expanded = itemData.expanded;
|
const expanded = itemData.expanded;
|
||||||
let expandIcon = 'codicon-blank';
|
let expandIcon = 'codicon-blank';
|
||||||
if (typeof expanded === 'boolean')
|
if (typeof expanded === 'boolean')
|
||||||
expandIcon = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
|
expandIcon = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
|
||||||
const rendered = render(item);
|
const rendered = render(item);
|
||||||
|
const children = expanded && item.children.length ? item.children as T[] : [];
|
||||||
|
const titled = title?.(item);
|
||||||
|
|
||||||
return <div
|
return <div ref={itemRef} role='treeitem' aria-selected={item === selectedItem} aria-expanded={expanded} aria-label={titled} title={titled} className='vbox' style={{ flex: 'none' }}>
|
||||||
onDoubleClick={() => onAccepted?.(item)}
|
|
||||||
className={clsx(
|
|
||||||
'tree-view-entry',
|
|
||||||
selectedItem === item && 'selected',
|
|
||||||
highlightedItem === item && 'highlighted',
|
|
||||||
isError?.(item) && 'error')}
|
|
||||||
onClick={() => onSelected?.(item)}
|
|
||||||
onMouseEnter={() => setHighlightedItem(item)}
|
|
||||||
onMouseLeave={() => setHighlightedItem(undefined)}
|
|
||||||
>
|
|
||||||
{indentation ? new Array(indentation).fill(0).map((_, i) => <div key={'indent-' + i} className='tree-view-indent'></div>) : undefined}
|
|
||||||
<div
|
<div
|
||||||
className={'codicon ' + expandIcon}
|
onDoubleClick={() => onAccepted?.(item)}
|
||||||
style={{ minWidth: 16, marginRight: 4 }}
|
className={clsx(
|
||||||
onDoubleClick={e => {
|
'tree-view-entry',
|
||||||
e.preventDefault();
|
selectedItem === item && 'selected',
|
||||||
e.stopPropagation();
|
highlightedItem === item && 'highlighted',
|
||||||
}}
|
isError?.(item) && 'error')}
|
||||||
onClick={e => {
|
onClick={() => onSelected?.(item)}
|
||||||
e.stopPropagation();
|
onMouseEnter={() => setHighlightedItem(item)}
|
||||||
e.preventDefault();
|
onMouseLeave={() => setHighlightedItem(undefined)}
|
||||||
toggleExpanded(item);
|
>
|
||||||
}}
|
{indentation ? new Array(indentation).fill(0).map((_, i) => <div key={'indent-' + i} className='tree-view-indent'></div>) : undefined}
|
||||||
/>
|
<div
|
||||||
{icon && <div className={'codicon ' + (icon(item) || 'codicon-blank')} style={{ minWidth: 16, marginRight: 4 }}></div>}
|
aria-hidden='true'
|
||||||
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
className={'codicon ' + expandIcon}
|
||||||
|
style={{ minWidth: 16, marginRight: 4 }}
|
||||||
|
onDoubleClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleExpanded(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{icon && <div className={'codicon ' + (icon(item) || 'codicon-blank')} style={{ minWidth: 16, marginRight: 4 }} aria-hidden='true'></div>}
|
||||||
|
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
||||||
|
</div>
|
||||||
|
{!!children.length && <div aria-label='group'>
|
||||||
|
{children.map(child => {
|
||||||
|
const itemData = treeItems.get(child);
|
||||||
|
return itemData && <TreeItemHeader
|
||||||
|
key={child.id}
|
||||||
|
item={child}
|
||||||
|
treeItems={treeItems}
|
||||||
|
selectedItem={selectedItem}
|
||||||
|
onSelected={onSelected}
|
||||||
|
onAccepted={onAccepted}
|
||||||
|
isError={isError}
|
||||||
|
toggleExpanded={toggleExpanded}
|
||||||
|
highlightedItem={highlightedItem}
|
||||||
|
setHighlightedItem={setHighlightedItem}
|
||||||
|
render={render}
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
isKeyboardNavigation={isKeyboardNavigation}
|
||||||
|
setIsKeyboardNavigation={setIsKeyboardNavigation} />;
|
||||||
|
})}
|
||||||
|
</div>}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TreeItemData = {
|
type TreeItemData = {
|
||||||
depth: number,
|
depth: number;
|
||||||
expanded: boolean | undefined,
|
expanded: boolean | undefined;
|
||||||
parent: TreeItem | null,
|
parent: TreeItem | null;
|
||||||
|
next: TreeItem | null;
|
||||||
|
prev: TreeItem | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function flattenTree<T extends TreeItem>(
|
function indexTree<T extends TreeItem>(
|
||||||
rootItem: T,
|
rootItem: T,
|
||||||
selectedItem: T | undefined,
|
selectedItem: T | undefined,
|
||||||
expandedItems: Map<string, boolean | undefined>,
|
expandedItems: Map<string, boolean | undefined>,
|
||||||
autoExpandDepth: number): Map<T, TreeItemData> {
|
autoExpandDepth: number,
|
||||||
|
isVisible?: (item: T) => boolean): Map<T, TreeItemData> {
|
||||||
|
|
||||||
const result = new Map<T, TreeItemData>();
|
const result = new Map<T, TreeItemData>();
|
||||||
const temporaryExpanded = new Set<string>();
|
const temporaryExpanded = new Set<string>();
|
||||||
for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent)
|
for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent)
|
||||||
temporaryExpanded.add(item.id);
|
temporaryExpanded.add(item.id);
|
||||||
|
let lastItem: T | null = null;
|
||||||
|
|
||||||
const appendChildren = (parent: T, depth: number) => {
|
const appendChildren = (parent: T, depth: number) => {
|
||||||
|
if (isVisible && !isVisible(parent))
|
||||||
|
return;
|
||||||
for (const item of parent.children as T[]) {
|
for (const item of parent.children as T[]) {
|
||||||
const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id);
|
const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id);
|
||||||
const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false;
|
const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false;
|
||||||
const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined;
|
const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined;
|
||||||
result.set(item, { depth, expanded, parent: rootItem === parent ? null : parent });
|
const itemData: TreeItemData = {
|
||||||
|
depth,
|
||||||
|
expanded,
|
||||||
|
parent: rootItem === parent ? null : parent,
|
||||||
|
next: null,
|
||||||
|
prev: lastItem,
|
||||||
|
};
|
||||||
|
if (lastItem)
|
||||||
|
result.get(lastItem)!.next = item;
|
||||||
|
lastItem = item;
|
||||||
|
result.set(item, itemData);
|
||||||
if (expanded)
|
if (expanded)
|
||||||
appendChildren(item, depth + 1);
|
appendChildren(item, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,14 +181,12 @@ test('expected formatter', async ({ page }) => {
|
||||||
|
|
||||||
expect(stripAnsi(error.message)).toContain(`
|
expect(stripAnsi(error.message)).toContain(`
|
||||||
Locator: locator('body')
|
Locator: locator('body')
|
||||||
- Expected - 4
|
- Expected - 2
|
||||||
+ Received string + 3
|
+ Received string + 3
|
||||||
|
|
||||||
-
|
- - heading "todos"
|
||||||
+ - banner:
|
+ - banner:
|
||||||
- - heading "todos"
|
|
||||||
+ - heading "todos"
|
+ - heading "todos"
|
||||||
- - textbox "Wrong text"
|
- - textbox "Wrong text"
|
||||||
-
|
|
||||||
+ - textbox "What needs to be done?"`);
|
+ - textbox "What needs to be done?"`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -68,14 +68,15 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () =
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
const treeItems = treeElement.querySelectorAll('[role=treeitem]');
|
const treeItems = treeElement.querySelectorAll('[role=treeitem]');
|
||||||
for (const treeItem of treeItems) {
|
for (const treeItem of treeItems) {
|
||||||
const iconElements = treeItem.querySelectorAll('.codicon');
|
const treeItemHeader = treeItem.querySelector('.tree-view-entry');
|
||||||
|
const iconElements = treeItemHeader.querySelectorAll('.codicon');
|
||||||
const treeIcon = iconName(iconElements[0]);
|
const treeIcon = iconName(iconElements[0]);
|
||||||
const statusIcon = iconName(iconElements[1]);
|
const statusIcon = iconName(iconElements[1]);
|
||||||
const indent = treeItem.querySelectorAll('.tree-view-indent').length;
|
const indent = treeItemHeader.querySelectorAll('.tree-view-indent').length;
|
||||||
const watch = treeItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
|
const watch = treeItemHeader.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
|
||||||
const selected = treeItem.getAttribute('aria-selected') === 'true' ? ' <=' : '';
|
const selected = treeItem.getAttribute('aria-selected') === 'true' ? ' <=' : '';
|
||||||
const title = treeItem.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent;
|
const title = treeItemHeader.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent;
|
||||||
const timeElement = options.time ? treeItem.querySelector('.ui-mode-tree-item-time') : undefined;
|
const timeElement = options.time ? treeItemHeader.querySelector('.ui-mode-tree-item-time') : undefined;
|
||||||
const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : '';
|
const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : '';
|
||||||
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected);
|
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ test('should display annotations', async ({ runUITest }) => {
|
||||||
});
|
});
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'suite' }).locator('.codicon-chevron-right').click();
|
await page.getByRole('treeitem', { name: 'suite' }).locator('.codicon-chevron-right').click();
|
||||||
await page.getByText('annotation test').click();
|
await page.getByText('annotation test').click();
|
||||||
await page.getByText('Annotations', { exact: true }).click();
|
await page.getByText('Annotations', { exact: true }).click();
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ test('should run on hover', async ({ runUITest }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('passes').hover();
|
await page.getByText('passes').hover();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'passes' }).getByTitle('Run').click();
|
await page.getByRole('treeitem', { name: 'passes' }).getByRole('button', { name: 'Run' }).click();
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ◯ a.test.ts
|
▼ ◯ a.test.ts
|
||||||
|
@ -275,7 +275,7 @@ test('should run folder', async ({ runUITest }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('folder-b').hover();
|
await page.getByText('folder-b').hover();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click();
|
await page.getByRole('treeitem', { name: 'folder-b' }).getByRole('button', { name: 'Run' }).click();
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toContain(`
|
await expect.poll(dumpTestTree(page)).toContain(`
|
||||||
▼ ✅ folder-b <=
|
▼ ✅ folder-b <=
|
||||||
|
@ -421,8 +421,8 @@ test('should show proper total when using deps', async ({ runUITest }) => {
|
||||||
|
|
||||||
|
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
await page.getByLabel('setup').setChecked(true);
|
await page.getByRole('checkbox', { name: 'setup' }).setChecked(true);
|
||||||
await page.getByLabel('chromium').setChecked(true);
|
await page.getByRole('checkbox', { name: 'chromium' }).setChecked(true);
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toContain(`
|
await expect.poll(dumpTestTree(page)).toContain(`
|
||||||
▼ ◯ a.test.ts
|
▼ ◯ a.test.ts
|
||||||
|
|
|
@ -140,9 +140,9 @@ const testsWithSetup = {
|
||||||
test('should run setup and teardown projects (1)', async ({ runUITest }) => {
|
test('should run setup and teardown projects (1)', async ({ runUITest }) => {
|
||||||
const { page } = await runUITest(testsWithSetup);
|
const { page } = await runUITest(testsWithSetup);
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
await page.getByLabel('setup').setChecked(false);
|
await page.getByRole('checkbox', { name: 'setup' }).setChecked(false);
|
||||||
await page.getByLabel('teardown').setChecked(false);
|
await page.getByRole('checkbox', { name: 'teardown' }).setChecked(false);
|
||||||
await page.getByLabel('test').setChecked(false);
|
await page.getByRole('checkbox', { name: 'test' }).setChecked(false);
|
||||||
|
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
|
|
||||||
|
@ -164,9 +164,9 @@ test('should run setup and teardown projects (1)', async ({ runUITest }) => {
|
||||||
test('should run setup and teardown projects (2)', async ({ runUITest }) => {
|
test('should run setup and teardown projects (2)', async ({ runUITest }) => {
|
||||||
const { page } = await runUITest(testsWithSetup);
|
const { page } = await runUITest(testsWithSetup);
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
await page.getByLabel('setup').setChecked(false);
|
await page.getByRole('checkbox', { name: 'setup' }).setChecked(false);
|
||||||
await page.getByLabel('teardown').setChecked(true);
|
await page.getByRole('checkbox', { name: 'teardown' }).setChecked(true);
|
||||||
await page.getByLabel('test').setChecked(true);
|
await page.getByRole('checkbox', { name: 'test' }).setChecked(true);
|
||||||
|
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
|
|
||||||
|
@ -186,9 +186,9 @@ test('should run setup and teardown projects (2)', async ({ runUITest }) => {
|
||||||
test('should run setup and teardown projects (3)', async ({ runUITest }) => {
|
test('should run setup and teardown projects (3)', async ({ runUITest }) => {
|
||||||
const { page } = await runUITest(testsWithSetup);
|
const { page } = await runUITest(testsWithSetup);
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
await page.getByLabel('setup').setChecked(false);
|
await page.getByRole('checkbox', { name: 'setup' }).setChecked(false);
|
||||||
await page.getByLabel('teardown').setChecked(false);
|
await page.getByRole('checkbox', { name: 'teardown' }).setChecked(false);
|
||||||
await page.getByLabel('test').setChecked(true);
|
await page.getByRole('checkbox', { name: 'test' }).setChecked(true);
|
||||||
|
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
|
|
||||||
|
@ -206,12 +206,12 @@ test('should run setup and teardown projects (3)', async ({ runUITest }) => {
|
||||||
test('should run part of the setup only', async ({ runUITest }) => {
|
test('should run part of the setup only', async ({ runUITest }) => {
|
||||||
const { page } = await runUITest(testsWithSetup);
|
const { page } = await runUITest(testsWithSetup);
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
await page.getByLabel('setup').setChecked(true);
|
await page.getByRole('checkbox', { name: 'setup' }).setChecked(true);
|
||||||
await page.getByLabel('teardown').setChecked(true);
|
await page.getByRole('checkbox', { name: 'teardown' }).setChecked(true);
|
||||||
await page.getByLabel('test').setChecked(true);
|
await page.getByRole('checkbox', { name: 'test' }).setChecked(true);
|
||||||
|
|
||||||
await page.getByText('setup.ts').hover();
|
await page.getByText('setup.ts').hover();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'setup.ts' }).getByTitle('Run').click();
|
await page.getByRole('treeitem', { name: 'setup.ts' }).getByRole('button', { name: 'Run' }).click();
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ✅ setup.ts <=
|
▼ ✅ setup.ts <=
|
||||||
|
|
|
@ -215,7 +215,7 @@ test('should update test locations', async ({ runUITest, writeFiles }) => {
|
||||||
const messages: any[] = [];
|
const messages: any[] = [];
|
||||||
await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg));
|
await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg));
|
||||||
|
|
||||||
const passesItemLocator = page.getByRole('treeitem').filter({ hasText: 'passes' });
|
const passesItemLocator = page.getByRole('treeitem', { name: 'passes' });
|
||||||
await passesItemLocator.hover();
|
await passesItemLocator.hover();
|
||||||
await passesItemLocator.getByTitle('Show source').click();
|
await passesItemLocator.getByTitle('Show source').click();
|
||||||
await page.getByTitle('Open in VS Code').click();
|
await page.getByTitle('Open in VS Code').click();
|
||||||
|
|
|
@ -28,14 +28,14 @@ test('should watch files', async ({ runUITest, writeFiles }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('fails').click();
|
await page.getByText('fails').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'fails' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ◯ a.test.ts
|
▼ ◯ a.test.ts
|
||||||
◯ passes
|
◯ passes
|
||||||
◯ fails 👁 <=
|
◯ fails 👁 <=
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Run').click();
|
await page.getByRole('treeitem', { name: 'fails' }).getByRole('button', { name: 'Run' }).click();
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ❌ a.test.ts
|
▼ ❌ a.test.ts
|
||||||
|
@ -75,7 +75,7 @@ test('should watch e2e deps', async ({ runUITest, writeFiles }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('answer').click();
|
await page.getByText('answer').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'answer' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'answer' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ◯ a.test.ts
|
▼ ◯ a.test.ts
|
||||||
◯ answer 👁 <=
|
◯ answer 👁 <=
|
||||||
|
@ -102,13 +102,13 @@ test('should batch watch updates', async ({ runUITest, writeFiles }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('a.test.ts').click();
|
await page.getByText('a.test.ts').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'a.test.ts' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
await page.getByText('b.test.ts').click();
|
await page.getByText('b.test.ts').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'b.test.ts' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'b.test.ts' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
await page.getByText('c.test.ts').click();
|
await page.getByText('c.test.ts').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'c.test.ts' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'c.test.ts' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
await page.getByText('d.test.ts').click();
|
await page.getByText('d.test.ts').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'd.test.ts' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'd.test.ts' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ◯ a.test.ts 👁
|
▼ ◯ a.test.ts 👁
|
||||||
|
@ -229,7 +229,7 @@ test('should run added test in watched file', async ({ runUITest, writeFiles })
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('a.test.ts').click();
|
await page.getByText('a.test.ts').click();
|
||||||
await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
await page.getByRole('treeitem', { name: 'a.test.ts' }).getByRole('button', { name: 'Watch' }).click();
|
||||||
|
|
||||||
await expect.poll(dumpTestTree(page)).toBe(`
|
await expect.poll(dumpTestTree(page)).toBe(`
|
||||||
▼ ◯ a.test.ts 👁 <=
|
▼ ◯ a.test.ts 👁 <=
|
||||||
|
|
Loading…
Reference in New Issue