-
{treeItem.title}
+ const prefixId = treeItem.id.replace(/[^\w\d-_]/g, '-');
+ const labelId = prefixId + '-label';
+ const timeId = prefixId + '-time';
+ return
+
+ {treeItem.title}
{treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null}
- {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
}
+ {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
}
runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}>
@@ -179,6 +182,7 @@ export const TestListView: React.FC<{
;
}}
icon={treeItem => testStatusIcon(treeItem.status)}
+ title={treeItem => treeItem.title}
selectedItem={selectedTreeItem}
onAccepted={runTreeItem}
onSelected={treeItem => {
diff --git a/packages/web/src/components/gridView.tsx b/packages/web/src/components/gridView.tsx
index 5d9b0a4c6c..10fc48c247 100644
--- a/packages/web/src/components/gridView.tsx
+++ b/packages/web/src/components/gridView.tsx
@@ -110,15 +110,12 @@ export function GridView
(model: GridViewProps) {
>;
}}
icon={model.icon}
- indent={model.indent}
isError={model.isError}
isWarning={model.isWarning}
isInfo={model.isInfo}
selectedItem={model.selectedItem}
onAccepted={model.onAccepted}
onSelected={model.onSelected}
- onLeftArrow={model.onLeftArrow}
- onRightArrow={model.onRightArrow}
onHighlighted={model.onHighlighted}
onIconClicked={model.onIconClicked}
noItemsMessage={model.noItemsMessage}
diff --git a/packages/web/src/components/listView.tsx b/packages/web/src/components/listView.tsx
index 4f2de5ae54..73f9b65b8f 100644
--- a/packages/web/src/components/listView.tsx
+++ b/packages/web/src/components/listView.tsx
@@ -16,7 +16,7 @@
import * as React from 'react';
import './listView.css';
-import { clsx } from '@web/uiUtils';
+import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
export type ListViewProps = {
name: string,
@@ -24,15 +24,12 @@ export type ListViewProps = {
id?: (item: T, index: number) => string,
render: (item: T, index: number) => React.ReactNode,
icon?: (item: T, index: number) => string | undefined,
- indent?: (item: T, index: number) => number | undefined,
isError?: (item: T, index: number) => boolean,
isWarning?: (item: T, index: number) => boolean,
isInfo?: (item: T, index: number) => boolean,
selectedItem?: T,
onAccepted?: (item: T, index: number) => void,
onSelected?: (item: T, index: number) => void,
- onLeftArrow?: (item: T, index: number) => void,
- onRightArrow?: (item: T, index: number) => void,
onHighlighted?: (item: T | undefined) => void,
onIconClicked?: (item: T, index: number) => void,
noItemsMessage?: string,
@@ -51,12 +48,9 @@ export function ListView({
isError,
isWarning,
isInfo,
- indent,
selectedItem,
onAccepted,
onSelected,
- onLeftArrow,
- onRightArrow,
onHighlighted,
onIconClicked,
noItemsMessage,
@@ -95,21 +89,12 @@ export function ListView({
onAccepted?.(selectedItem, items.indexOf(selectedItem));
return;
}
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
return;
event.stopPropagation();
event.preventDefault();
- if (selectedItem && event.key === 'ArrowLeft') {
- onLeftArrow?.(selectedItem, items.indexOf(selectedItem));
- return;
- }
- if (selectedItem && event.key === 'ArrowRight') {
- onRightArrow?.(selectedItem, items.indexOf(selectedItem));
- return;
- }
-
const index = selectedItem ? items.indexOf(selectedItem) : -1;
let newIndex = index;
if (event.key === 'ArrowDown') {
@@ -135,7 +120,6 @@ export function ListView({
>
{noItemsMessage && items.length === 0 && {noItemsMessage}
}
{items.map((item, index) => {
- const indentation = indent?.(item, index) || 0;
const rendered = render(item, index);
return ({
onMouseEnter={() => setHighlightedItem(item)}
onMouseLeave={() => setHighlightedItem(undefined)}
>
- {/* eslint-disable-next-line react/jsx-key */}
- {indentation ? new Array(indentation).fill(0).map(() =>
) : undefined}
{icon &&
({
;
}
-
-function scrollIntoViewIfNeeded(element: Element | undefined) {
- if (!element)
- return;
- if ((element as any)?.scrollIntoViewIfNeeded)
- (element as any).scrollIntoViewIfNeeded(false);
- else
- element?.scrollIntoView();
-}
diff --git a/packages/web/src/components/toolbarButton.tsx b/packages/web/src/components/toolbarButton.tsx
index 184642b395..2cdd85b9b7 100644
--- a/packages/web/src/components/toolbarButton.tsx
+++ b/packages/web/src/components/toolbarButton.tsx
@@ -52,7 +52,7 @@ export const ToolbarButton: React.FC
disabled={!!disabled}
style={style}
data-testid={testId}
- aria-label={ariaLabel}
+ aria-label={ariaLabel || title}
>
{icon && }
{children}
diff --git a/packages/web/src/components/treeView.css b/packages/web/src/components/treeView.css
new file mode 100644
index 0000000000..860d560fc9
--- /dev/null
+++ b/packages/web/src/components/treeView.css
@@ -0,0 +1,91 @@
+/*
+ Copyright (c) Microsoft Corporation.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+.tree-view-content {
+ display: flex;
+ flex-direction: column;
+ flex: auto;
+ position: relative;
+ user-select: none;
+ overflow: hidden auto;
+ outline: 1px solid transparent;
+}
+
+.tree-view-entry {
+ display: flex;
+ flex: none;
+ cursor: pointer;
+ align-items: center;
+ white-space: nowrap;
+ line-height: 28px;
+ padding-left: 5px;
+}
+
+.tree-view-content.not-selectable > .tree-view-entry {
+ cursor: inherit;
+}
+
+.tree-view-entry.highlighted:not(.selected) {
+ background-color: var(--vscode-list-inactiveSelectionBackground) !important;
+}
+
+.tree-view-entry.selected {
+ z-index: 10;
+}
+
+.tree-view-indent {
+ min-width: 16px;
+}
+
+.tree-view-content:focus .tree-view-entry.selected {
+ background-color: var(--vscode-list-activeSelectionBackground);
+ color: var(--vscode-list-activeSelectionForeground);
+ outline: 1px solid var(--vscode-focusBorder);
+}
+
+.tree-view-content .tree-view-entry.selected {
+ background-color: var(--vscode-list-inactiveSelectionBackground);
+}
+
+.tree-view-content:focus .tree-view-entry.selected * {
+ color: var(--vscode-list-activeSelectionForeground) !important;
+ background-color: transparent !important;
+}
+
+.tree-view-content:focus .tree-view-entry.selected .codicon {
+ color: var(--vscode-list-activeSelectionForeground) !important;
+}
+
+.tree-view-empty {
+ flex: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.tree-view-entry.error {
+ color: var(--vscode-list-errorForeground);
+ background-color: var(--vscode-inputValidation-errorBackground);
+}
+
+.tree-view-entry.warning {
+ color: var(--vscode-list-warningForeground);
+ background-color: var(--vscode-inputValidation-warningBackground);
+}
+
+.tree-view-entry.info {
+ background-color: var(--vscode-inputValidation-infoBackground);
+}
diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx
index 8341056779..cb7ab7150d 100644
--- a/packages/web/src/components/treeView.tsx
+++ b/packages/web/src/components/treeView.tsx
@@ -15,7 +15,8 @@
*/
import * as React from 'react';
-import { ListView } from './listView';
+import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
+import './treeView.css';
export type TreeItem = {
id: string,
@@ -31,6 +32,7 @@ export type TreeViewProps = {
name: string,
rootItem: T,
render: (item: T) => React.ReactNode,
+ title?: (item: T) => string,
icon?: (item: T) => string | undefined,
isError?: (item: T) => boolean,
isVisible?: (item: T) => boolean,
@@ -45,12 +47,13 @@ export type TreeViewProps = {
autoExpandDepth?: number,
};
-const TreeListView = ListView;
+const scrollPositions = new Map();
export function TreeView({
name,
rootItem,
render,
+ title,
icon,
isError,
isVisible,
@@ -65,113 +68,282 @@ export function TreeView({
autoExpandDepth,
}: TreeViewProps) {
const treeItems = React.useMemo(() => {
- return flattenTree(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0);
- }, [rootItem, selectedItem, treeState, autoExpandDepth]);
+ return indexTree(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0, isVisible);
+ }, [rootItem, selectedItem, treeState, autoExpandDepth, isVisible]);
- // Filter visible items.
- const visibleItems = React.useMemo(() => {
- if (!isVisible)
- return [...treeItems.keys()];
- const cachedVisible = new Map();
- const visit = (item: TreeItem): boolean => {
- const cachedResult = cachedVisible.get(item);
- if (cachedResult !== undefined)
- return cachedResult;
+ const itemListRef = React.useRef(null);
+ const [highlightedItem, setHighlightedItem] = React.useState();
+ const [isKeyboardNavigation, setIsKeyboardNavigation] = React.useState(false);
- 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;
+ React.useEffect(() => {
+ onHighlighted?.(highlightedItem);
+ }, [onHighlighted, highlightedItem]);
+
+ React.useEffect(() => {
+ const treeElem = itemListRef.current;
+ if (!treeElem)
+ return;
+ const saveScrollPosition = () => {
+ scrollPositions.set(name, treeElem.scrollTop);
};
- 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]);
+ treeElem.addEventListener('scroll', saveScrollPosition, { passive: true });
+ return () => treeElem.removeEventListener('scroll', saveScrollPosition);
+ }, [name]);
- return item.id}
- dataTestId={dataTestId || (name + '-tree')}
- render={item => {
- const rendered = render(item as T);
- return <>
- {icon &&
}
- {typeof rendered === 'string' ? {rendered}
: rendered}
- >;
- }}
- icon={item => {
- const expanded = treeItems.get(item as T)!.expanded;
- if (typeof expanded === 'boolean')
- return expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
- }}
- isError={item => isError?.(item as T) || false}
- indent={item => treeItems.get(item as T)!.depth}
- selectedItem={selectedItem}
- onAccepted={item => onAccepted?.(item as T)}
- onSelected={item => onSelected?.(item as T)}
- onHighlighted={item => onHighlighted?.(item as T)}
- onLeftArrow={item => {
- const { expanded, parent } = treeItems.get(item as T)!;
- if (expanded) {
- treeState.expandedItems.set(item.id, false);
- setTreeState({ ...treeState });
- } else if (parent) {
- onSelected?.(parent as T);
+ React.useEffect(() => {
+ if (itemListRef.current)
+ itemListRef.current.scrollTop = scrollPositions.get(name) || 0;
+ }, [name]);
+
+ const toggleExpanded = React.useCallback((item: T) => {
+ const { expanded } = treeItems.get(item)!;
+ if (expanded) {
+ // Move nested selection up.
+ for (let i: TreeItem | undefined = selectedItem; i; i = i.parent) {
+ if (i === item) {
+ onSelected?.(item as T);
+ break;
+ }
}
- }}
- onRightArrow={item => {
- if (item.children.length) {
- treeState.expandedItems.set(item.id, true);
- setTreeState({ ...treeState });
- }
- }}
- onIconClicked={item => {
- const { expanded } = treeItems.get(item as T)!;
- if (expanded) {
- // Move nested selection up.
- for (let i: TreeItem | undefined = selectedItem; i; i = i.parent) {
- if (i === item) {
- onSelected?.(item as T);
- break;
+ treeState.expandedItems.set(item.id, false);
+ } else {
+ treeState.expandedItems.set(item.id, true);
+ }
+ setTreeState({ ...treeState });
+ }, [treeItems, selectedItem, onSelected, treeState, setTreeState]);
+
+ return
+
{
+ if (selectedItem && event.key === 'Enter') {
+ onAccepted?.(selectedItem);
+ return;
+ }
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
+ return;
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ if (selectedItem && event.key === 'ArrowLeft') {
+ const { expanded, parent } = treeItems.get(selectedItem)!;
+ if (expanded) {
+ treeState.expandedItems.set(selectedItem.id, false);
+ setTreeState({ ...treeState });
+ } else if (parent) {
+ onSelected?.(parent as T);
+ }
+ return;
+ }
+ if (selectedItem && event.key === 'ArrowRight') {
+ if (selectedItem.children.length) {
+ treeState.expandedItems.set(selectedItem.id, true);
+ setTreeState({ ...treeState });
+ }
+ return;
+ }
+
+ let newSelectedItem: T | undefined = selectedItem;
+ if (event.key === 'ArrowDown') {
+ if (selectedItem) {
+ const itemData = treeItems.get(selectedItem)!;
+ newSelectedItem = itemData.next as T;
+ } else if (treeItems.size) {
+ const itemList = [...treeItems.keys()];
+ newSelectedItem = itemList[0];
}
}
- treeState.expandedItems.set(item.id, false);
- } else {
- treeState.expandedItems.set(item.id, true);
- }
- setTreeState({ ...treeState });
- }}
- noItemsMessage={noItemsMessage} />;
+ if (event.key === 'ArrowUp') {
+ if (selectedItem) {
+ const itemData = treeItems.get(selectedItem)!;
+ newSelectedItem = itemData.prev as T;
+ } else if (treeItems.size) {
+ const itemList = [...treeItems.keys()];
+ newSelectedItem = itemList[itemList.length - 1];
+ }
+ }
+
+ // scrollIntoViewIfNeeded(element || undefined);
+ onHighlighted?.(undefined);
+ if (newSelectedItem) {
+ setIsKeyboardNavigation(true);
+ onSelected?.(newSelectedItem);
+ }
+ setHighlightedItem(undefined);
+ }}
+ ref={itemListRef}
+ >
+ {noItemsMessage && treeItems.size === 0 &&
{noItemsMessage}
}
+ {rootItem.children.map(child => {
+ const itemData = treeItems.get(child as T);
+ return itemData &&
;
+ })}
+
+
;
+}
+
+type TreeItemHeaderProps = {
+ item: T,
+ treeItems: Map,
+ selectedItem: T | undefined,
+ onSelected?: (item: T) => void,
+ toggleExpanded: (item: T) => void,
+ highlightedItem: T | undefined,
+ isError?: (item: T) => boolean,
+ onAccepted?: (item: T) => void,
+ setHighlightedItem: (item: T | undefined) => void,
+ render: (item: T) => React.ReactNode,
+ title?: (item: T) => string,
+ icon?: (item: T) => string | undefined,
+ isKeyboardNavigation: boolean,
+ setIsKeyboardNavigation: (value: boolean) => void,
+};
+
+export function TreeItemHeader({
+ item,
+ treeItems,
+ selectedItem,
+ onSelected,
+ highlightedItem,
+ setHighlightedItem,
+ isError,
+ onAccepted,
+ toggleExpanded,
+ render,
+ title,
+ icon,
+ isKeyboardNavigation,
+ setIsKeyboardNavigation }: TreeItemHeaderProps) {
+ 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 expanded = itemData.expanded;
+ let expandIcon = 'codicon-blank';
+ if (typeof expanded === 'boolean')
+ expandIcon = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
+ const rendered = render(item);
+ const children = expanded && item.children.length ? item.children as T[] : [];
+ const titled = title?.(item);
+ const iconed = icon?.(item) || 'codicon-blank';
+
+ return
+
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) =>
) : undefined}
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onClick={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ toggleExpanded(item);
+ }}
+ />
+ {icon &&
}
+ {typeof rendered === 'string' ?
{rendered}
: rendered}
+
+ {!!children.length &&
+ {children.map(child => {
+ const itemData = treeItems.get(child);
+ return itemData && ;
+ })}
+
}
+
;
}
type TreeItemData = {
- depth: number,
- expanded: boolean | undefined,
- parent: TreeItem | null,
+ depth: number;
+ expanded: boolean | undefined;
+ parent: TreeItem | null;
+ next: TreeItem | null;
+ prev: TreeItem | null;
};
-function flattenTree
(rootItem: T, selectedItem: T | undefined, expandedItems: Map, autoExpandDepth: number): Map {
+function indexTree(
+ rootItem: T,
+ selectedItem: T | undefined,
+ expandedItems: Map,
+ autoExpandDepth: number,
+ isVisible?: (item: T) => boolean): Map {
+
const result = new Map();
const temporaryExpanded = new Set();
for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent)
temporaryExpanded.add(item.id);
+ let lastItem: T | null = null;
const appendChildren = (parent: T, depth: number) => {
+ if (isVisible && !isVisible(parent))
+ return;
for (const item of parent.children as T[]) {
const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id);
const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false;
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)
appendChildren(item, depth + 1);
}
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts
index 2697177c6f..ea71486014 100644
--- a/packages/web/src/uiUtils.ts
+++ b/packages/web/src/uiUtils.ts
@@ -208,5 +208,14 @@ export async function sha1(str: string): Promise {
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer))).map(b => b.toString(16).padStart(2, '0')).join('');
}
+export function scrollIntoViewIfNeeded(element: Element | undefined) {
+ if (!element)
+ return;
+ if ((element as any)?.scrollIntoViewIfNeeded)
+ (element as any).scrollIntoViewIfNeeded(false);
+ else
+ element?.scrollIntoView();
+}
+
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
diff --git a/tests/assets/codicon.css b/tests/assets/codicon.css
new file mode 100644
index 0000000000..41360ce21d
--- /dev/null
+++ b/tests/assets/codicon.css
@@ -0,0 +1,596 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+@font-face {
+ font-family: "codicon";
+ src: url("codicon.ttf") format("truetype");
+}
+
+.codicon {
+ font: normal normal normal 16px/1 codicon;
+ flex: none;
+ display: inline-block;
+ text-decoration: none;
+ text-rendering: auto;
+ text-align: center;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.codicon-add:before { content: '\ea60'; }
+.codicon-plus:before { content: '\ea60'; }
+.codicon-gist-new:before { content: '\ea60'; }
+.codicon-repo-create:before { content: '\ea60'; }
+.codicon-lightbulb:before { content: '\ea61'; }
+.codicon-light-bulb:before { content: '\ea61'; }
+.codicon-repo:before { content: '\ea62'; }
+.codicon-repo-delete:before { content: '\ea62'; }
+.codicon-gist-fork:before { content: '\ea63'; }
+.codicon-repo-forked:before { content: '\ea63'; }
+.codicon-git-pull-request:before { content: '\ea64'; }
+.codicon-git-pull-request-abandoned:before { content: '\ea64'; }
+.codicon-record-keys:before { content: '\ea65'; }
+.codicon-keyboard:before { content: '\ea65'; }
+.codicon-tag:before { content: '\ea66'; }
+.codicon-git-pull-request-label:before { content: '\ea66'; }
+.codicon-tag-add:before { content: '\ea66'; }
+.codicon-tag-remove:before { content: '\ea66'; }
+.codicon-person:before { content: '\ea67'; }
+.codicon-person-follow:before { content: '\ea67'; }
+.codicon-person-outline:before { content: '\ea67'; }
+.codicon-person-filled:before { content: '\ea67'; }
+.codicon-git-branch:before { content: '\ea68'; }
+.codicon-git-branch-create:before { content: '\ea68'; }
+.codicon-git-branch-delete:before { content: '\ea68'; }
+.codicon-source-control:before { content: '\ea68'; }
+.codicon-mirror:before { content: '\ea69'; }
+.codicon-mirror-public:before { content: '\ea69'; }
+.codicon-star:before { content: '\ea6a'; }
+.codicon-star-add:before { content: '\ea6a'; }
+.codicon-star-delete:before { content: '\ea6a'; }
+.codicon-star-empty:before { content: '\ea6a'; }
+.codicon-comment:before { content: '\ea6b'; }
+.codicon-comment-add:before { content: '\ea6b'; }
+.codicon-alert:before { content: '\ea6c'; }
+.codicon-warning:before { content: '\ea6c'; }
+.codicon-search:before { content: '\ea6d'; }
+.codicon-search-save:before { content: '\ea6d'; }
+.codicon-log-out:before { content: '\ea6e'; }
+.codicon-sign-out:before { content: '\ea6e'; }
+.codicon-log-in:before { content: '\ea6f'; }
+.codicon-sign-in:before { content: '\ea6f'; }
+.codicon-eye:before { content: '\ea70'; }
+.codicon-eye-unwatch:before { content: '\ea70'; }
+.codicon-eye-watch:before { content: '\ea70'; }
+.codicon-circle-filled:before { content: '\ea71'; }
+.codicon-primitive-dot:before { content: '\ea71'; }
+.codicon-close-dirty:before { content: '\ea71'; }
+.codicon-debug-breakpoint:before { content: '\ea71'; }
+.codicon-debug-breakpoint-disabled:before { content: '\ea71'; }
+.codicon-debug-hint:before { content: '\ea71'; }
+.codicon-terminal-decoration-success:before { content: '\ea71'; }
+.codicon-primitive-square:before { content: '\ea72'; }
+.codicon-edit:before { content: '\ea73'; }
+.codicon-pencil:before { content: '\ea73'; }
+.codicon-info:before { content: '\ea74'; }
+.codicon-issue-opened:before { content: '\ea74'; }
+.codicon-gist-private:before { content: '\ea75'; }
+.codicon-git-fork-private:before { content: '\ea75'; }
+.codicon-lock:before { content: '\ea75'; }
+.codicon-mirror-private:before { content: '\ea75'; }
+.codicon-close:before { content: '\ea76'; }
+.codicon-remove-close:before { content: '\ea76'; }
+.codicon-x:before { content: '\ea76'; }
+.codicon-repo-sync:before { content: '\ea77'; }
+.codicon-sync:before { content: '\ea77'; }
+.codicon-clone:before { content: '\ea78'; }
+.codicon-desktop-download:before { content: '\ea78'; }
+.codicon-beaker:before { content: '\ea79'; }
+.codicon-microscope:before { content: '\ea79'; }
+.codicon-vm:before { content: '\ea7a'; }
+.codicon-device-desktop:before { content: '\ea7a'; }
+.codicon-file:before { content: '\ea7b'; }
+.codicon-file-text:before { content: '\ea7b'; }
+.codicon-more:before { content: '\ea7c'; }
+.codicon-ellipsis:before { content: '\ea7c'; }
+.codicon-kebab-horizontal:before { content: '\ea7c'; }
+.codicon-mail-reply:before { content: '\ea7d'; }
+.codicon-reply:before { content: '\ea7d'; }
+.codicon-organization:before { content: '\ea7e'; }
+.codicon-organization-filled:before { content: '\ea7e'; }
+.codicon-organization-outline:before { content: '\ea7e'; }
+.codicon-new-file:before { content: '\ea7f'; }
+.codicon-file-add:before { content: '\ea7f'; }
+.codicon-new-folder:before { content: '\ea80'; }
+.codicon-file-directory-create:before { content: '\ea80'; }
+.codicon-trash:before { content: '\ea81'; }
+.codicon-trashcan:before { content: '\ea81'; }
+.codicon-history:before { content: '\ea82'; }
+.codicon-clock:before { content: '\ea82'; }
+.codicon-folder:before { content: '\ea83'; }
+.codicon-file-directory:before { content: '\ea83'; }
+.codicon-symbol-folder:before { content: '\ea83'; }
+.codicon-logo-github:before { content: '\ea84'; }
+.codicon-mark-github:before { content: '\ea84'; }
+.codicon-github:before { content: '\ea84'; }
+.codicon-terminal:before { content: '\ea85'; }
+.codicon-console:before { content: '\ea85'; }
+.codicon-repl:before { content: '\ea85'; }
+.codicon-zap:before { content: '\ea86'; }
+.codicon-symbol-event:before { content: '\ea86'; }
+.codicon-error:before { content: '\ea87'; }
+.codicon-stop:before { content: '\ea87'; }
+.codicon-variable:before { content: '\ea88'; }
+.codicon-symbol-variable:before { content: '\ea88'; }
+.codicon-array:before { content: '\ea8a'; }
+.codicon-symbol-array:before { content: '\ea8a'; }
+.codicon-symbol-module:before { content: '\ea8b'; }
+.codicon-symbol-package:before { content: '\ea8b'; }
+.codicon-symbol-namespace:before { content: '\ea8b'; }
+.codicon-symbol-object:before { content: '\ea8b'; }
+.codicon-symbol-method:before { content: '\ea8c'; }
+.codicon-symbol-function:before { content: '\ea8c'; }
+.codicon-symbol-constructor:before { content: '\ea8c'; }
+.codicon-symbol-boolean:before { content: '\ea8f'; }
+.codicon-symbol-null:before { content: '\ea8f'; }
+.codicon-symbol-numeric:before { content: '\ea90'; }
+.codicon-symbol-number:before { content: '\ea90'; }
+.codicon-symbol-structure:before { content: '\ea91'; }
+.codicon-symbol-struct:before { content: '\ea91'; }
+.codicon-symbol-parameter:before { content: '\ea92'; }
+.codicon-symbol-type-parameter:before { content: '\ea92'; }
+.codicon-symbol-key:before { content: '\ea93'; }
+.codicon-symbol-text:before { content: '\ea93'; }
+.codicon-symbol-reference:before { content: '\ea94'; }
+.codicon-go-to-file:before { content: '\ea94'; }
+.codicon-symbol-enum:before { content: '\ea95'; }
+.codicon-symbol-value:before { content: '\ea95'; }
+.codicon-symbol-ruler:before { content: '\ea96'; }
+.codicon-symbol-unit:before { content: '\ea96'; }
+.codicon-activate-breakpoints:before { content: '\ea97'; }
+.codicon-archive:before { content: '\ea98'; }
+.codicon-arrow-both:before { content: '\ea99'; }
+.codicon-arrow-down:before { content: '\ea9a'; }
+.codicon-arrow-left:before { content: '\ea9b'; }
+.codicon-arrow-right:before { content: '\ea9c'; }
+.codicon-arrow-small-down:before { content: '\ea9d'; }
+.codicon-arrow-small-left:before { content: '\ea9e'; }
+.codicon-arrow-small-right:before { content: '\ea9f'; }
+.codicon-arrow-small-up:before { content: '\eaa0'; }
+.codicon-arrow-up:before { content: '\eaa1'; }
+.codicon-bell:before { content: '\eaa2'; }
+.codicon-bold:before { content: '\eaa3'; }
+.codicon-book:before { content: '\eaa4'; }
+.codicon-bookmark:before { content: '\eaa5'; }
+.codicon-debug-breakpoint-conditional-unverified:before { content: '\eaa6'; }
+.codicon-debug-breakpoint-conditional:before { content: '\eaa7'; }
+.codicon-debug-breakpoint-conditional-disabled:before { content: '\eaa7'; }
+.codicon-debug-breakpoint-data-unverified:before { content: '\eaa8'; }
+.codicon-debug-breakpoint-data:before { content: '\eaa9'; }
+.codicon-debug-breakpoint-data-disabled:before { content: '\eaa9'; }
+.codicon-debug-breakpoint-log-unverified:before { content: '\eaaa'; }
+.codicon-debug-breakpoint-log:before { content: '\eaab'; }
+.codicon-debug-breakpoint-log-disabled:before { content: '\eaab'; }
+.codicon-briefcase:before { content: '\eaac'; }
+.codicon-broadcast:before { content: '\eaad'; }
+.codicon-browser:before { content: '\eaae'; }
+.codicon-bug:before { content: '\eaaf'; }
+.codicon-calendar:before { content: '\eab0'; }
+.codicon-case-sensitive:before { content: '\eab1'; }
+.codicon-check:before { content: '\eab2'; }
+.codicon-checklist:before { content: '\eab3'; }
+.codicon-chevron-down:before { content: '\eab4'; }
+.codicon-chevron-left:before { content: '\eab5'; }
+.codicon-chevron-right:before { content: '\eab6'; }
+.codicon-chevron-up:before { content: '\eab7'; }
+.codicon-chrome-close:before { content: '\eab8'; }
+.codicon-chrome-maximize:before { content: '\eab9'; }
+.codicon-chrome-minimize:before { content: '\eaba'; }
+.codicon-chrome-restore:before { content: '\eabb'; }
+.codicon-circle-outline:before { content: '\eabc'; }
+.codicon-circle:before { content: '\eabc'; }
+.codicon-debug-breakpoint-unverified:before { content: '\eabc'; }
+.codicon-terminal-decoration-incomplete:before { content: '\eabc'; }
+.codicon-circle-slash:before { content: '\eabd'; }
+.codicon-circuit-board:before { content: '\eabe'; }
+.codicon-clear-all:before { content: '\eabf'; }
+.codicon-clippy:before { content: '\eac0'; }
+.codicon-close-all:before { content: '\eac1'; }
+.codicon-cloud-download:before { content: '\eac2'; }
+.codicon-cloud-upload:before { content: '\eac3'; }
+.codicon-code:before { content: '\eac4'; }
+.codicon-collapse-all:before { content: '\eac5'; }
+.codicon-color-mode:before { content: '\eac6'; }
+.codicon-comment-discussion:before { content: '\eac7'; }
+.codicon-credit-card:before { content: '\eac9'; }
+.codicon-dash:before { content: '\eacc'; }
+.codicon-dashboard:before { content: '\eacd'; }
+.codicon-database:before { content: '\eace'; }
+.codicon-debug-continue:before { content: '\eacf'; }
+.codicon-debug-disconnect:before { content: '\ead0'; }
+.codicon-debug-pause:before { content: '\ead1'; }
+.codicon-debug-restart:before { content: '\ead2'; }
+.codicon-debug-start:before { content: '\ead3'; }
+.codicon-debug-step-into:before { content: '\ead4'; }
+.codicon-debug-step-out:before { content: '\ead5'; }
+.codicon-debug-step-over:before { content: '\ead6'; }
+.codicon-debug-stop:before { content: '\ead7'; }
+.codicon-debug:before { content: '\ead8'; }
+.codicon-device-camera-video:before { content: '\ead9'; }
+.codicon-device-camera:before { content: '\eada'; }
+.codicon-device-mobile:before { content: '\eadb'; }
+.codicon-diff-added:before { content: '\eadc'; }
+.codicon-diff-ignored:before { content: '\eadd'; }
+.codicon-diff-modified:before { content: '\eade'; }
+.codicon-diff-removed:before { content: '\eadf'; }
+.codicon-diff-renamed:before { content: '\eae0'; }
+.codicon-diff:before { content: '\eae1'; }
+.codicon-diff-sidebyside:before { content: '\eae1'; }
+.codicon-discard:before { content: '\eae2'; }
+.codicon-editor-layout:before { content: '\eae3'; }
+.codicon-empty-window:before { content: '\eae4'; }
+.codicon-exclude:before { content: '\eae5'; }
+.codicon-extensions:before { content: '\eae6'; }
+.codicon-eye-closed:before { content: '\eae7'; }
+.codicon-file-binary:before { content: '\eae8'; }
+.codicon-file-code:before { content: '\eae9'; }
+.codicon-file-media:before { content: '\eaea'; }
+.codicon-file-pdf:before { content: '\eaeb'; }
+.codicon-file-submodule:before { content: '\eaec'; }
+.codicon-file-symlink-directory:before { content: '\eaed'; }
+.codicon-file-symlink-file:before { content: '\eaee'; }
+.codicon-file-zip:before { content: '\eaef'; }
+.codicon-files:before { content: '\eaf0'; }
+.codicon-filter:before { content: '\eaf1'; }
+.codicon-flame:before { content: '\eaf2'; }
+.codicon-fold-down:before { content: '\eaf3'; }
+.codicon-fold-up:before { content: '\eaf4'; }
+.codicon-fold:before { content: '\eaf5'; }
+.codicon-folder-active:before { content: '\eaf6'; }
+.codicon-folder-opened:before { content: '\eaf7'; }
+.codicon-gear:before { content: '\eaf8'; }
+.codicon-gift:before { content: '\eaf9'; }
+.codicon-gist-secret:before { content: '\eafa'; }
+.codicon-gist:before { content: '\eafb'; }
+.codicon-git-commit:before { content: '\eafc'; }
+.codicon-git-compare:before { content: '\eafd'; }
+.codicon-compare-changes:before { content: '\eafd'; }
+.codicon-git-merge:before { content: '\eafe'; }
+.codicon-github-action:before { content: '\eaff'; }
+.codicon-github-alt:before { content: '\eb00'; }
+.codicon-globe:before { content: '\eb01'; }
+.codicon-grabber:before { content: '\eb02'; }
+.codicon-graph:before { content: '\eb03'; }
+.codicon-gripper:before { content: '\eb04'; }
+.codicon-heart:before { content: '\eb05'; }
+.codicon-home:before { content: '\eb06'; }
+.codicon-horizontal-rule:before { content: '\eb07'; }
+.codicon-hubot:before { content: '\eb08'; }
+.codicon-inbox:before { content: '\eb09'; }
+.codicon-issue-reopened:before { content: '\eb0b'; }
+.codicon-issues:before { content: '\eb0c'; }
+.codicon-italic:before { content: '\eb0d'; }
+.codicon-jersey:before { content: '\eb0e'; }
+.codicon-json:before { content: '\eb0f'; }
+.codicon-kebab-vertical:before { content: '\eb10'; }
+.codicon-key:before { content: '\eb11'; }
+.codicon-law:before { content: '\eb12'; }
+.codicon-lightbulb-autofix:before { content: '\eb13'; }
+.codicon-link-external:before { content: '\eb14'; }
+.codicon-link:before { content: '\eb15'; }
+.codicon-list-ordered:before { content: '\eb16'; }
+.codicon-list-unordered:before { content: '\eb17'; }
+.codicon-live-share:before { content: '\eb18'; }
+.codicon-loading:before { content: '\eb19'; }
+.codicon-location:before { content: '\eb1a'; }
+.codicon-mail-read:before { content: '\eb1b'; }
+.codicon-mail:before { content: '\eb1c'; }
+.codicon-markdown:before { content: '\eb1d'; }
+.codicon-megaphone:before { content: '\eb1e'; }
+.codicon-mention:before { content: '\eb1f'; }
+.codicon-milestone:before { content: '\eb20'; }
+.codicon-git-pull-request-milestone:before { content: '\eb20'; }
+.codicon-mortar-board:before { content: '\eb21'; }
+.codicon-move:before { content: '\eb22'; }
+.codicon-multiple-windows:before { content: '\eb23'; }
+.codicon-mute:before { content: '\eb24'; }
+.codicon-no-newline:before { content: '\eb25'; }
+.codicon-note:before { content: '\eb26'; }
+.codicon-octoface:before { content: '\eb27'; }
+.codicon-open-preview:before { content: '\eb28'; }
+.codicon-package:before { content: '\eb29'; }
+.codicon-paintcan:before { content: '\eb2a'; }
+.codicon-pin:before { content: '\eb2b'; }
+.codicon-play:before { content: '\eb2c'; }
+.codicon-run:before { content: '\eb2c'; }
+.codicon-plug:before { content: '\eb2d'; }
+.codicon-preserve-case:before { content: '\eb2e'; }
+.codicon-preview:before { content: '\eb2f'; }
+.codicon-project:before { content: '\eb30'; }
+.codicon-pulse:before { content: '\eb31'; }
+.codicon-question:before { content: '\eb32'; }
+.codicon-quote:before { content: '\eb33'; }
+.codicon-radio-tower:before { content: '\eb34'; }
+.codicon-reactions:before { content: '\eb35'; }
+.codicon-references:before { content: '\eb36'; }
+.codicon-refresh:before { content: '\eb37'; }
+.codicon-regex:before { content: '\eb38'; }
+.codicon-remote-explorer:before { content: '\eb39'; }
+.codicon-remote:before { content: '\eb3a'; }
+.codicon-remove:before { content: '\eb3b'; }
+.codicon-replace-all:before { content: '\eb3c'; }
+.codicon-replace:before { content: '\eb3d'; }
+.codicon-repo-clone:before { content: '\eb3e'; }
+.codicon-repo-force-push:before { content: '\eb3f'; }
+.codicon-repo-pull:before { content: '\eb40'; }
+.codicon-repo-push:before { content: '\eb41'; }
+.codicon-report:before { content: '\eb42'; }
+.codicon-request-changes:before { content: '\eb43'; }
+.codicon-rocket:before { content: '\eb44'; }
+.codicon-root-folder-opened:before { content: '\eb45'; }
+.codicon-root-folder:before { content: '\eb46'; }
+.codicon-rss:before { content: '\eb47'; }
+.codicon-ruby:before { content: '\eb48'; }
+.codicon-save-all:before { content: '\eb49'; }
+.codicon-save-as:before { content: '\eb4a'; }
+.codicon-save:before { content: '\eb4b'; }
+.codicon-screen-full:before { content: '\eb4c'; }
+.codicon-screen-normal:before { content: '\eb4d'; }
+.codicon-search-stop:before { content: '\eb4e'; }
+.codicon-server:before { content: '\eb50'; }
+.codicon-settings-gear:before { content: '\eb51'; }
+.codicon-settings:before { content: '\eb52'; }
+.codicon-shield:before { content: '\eb53'; }
+.codicon-smiley:before { content: '\eb54'; }
+.codicon-sort-precedence:before { content: '\eb55'; }
+.codicon-split-horizontal:before { content: '\eb56'; }
+.codicon-split-vertical:before { content: '\eb57'; }
+.codicon-squirrel:before { content: '\eb58'; }
+.codicon-star-full:before { content: '\eb59'; }
+.codicon-star-half:before { content: '\eb5a'; }
+.codicon-symbol-class:before { content: '\eb5b'; }
+.codicon-symbol-color:before { content: '\eb5c'; }
+.codicon-symbol-constant:before { content: '\eb5d'; }
+.codicon-symbol-enum-member:before { content: '\eb5e'; }
+.codicon-symbol-field:before { content: '\eb5f'; }
+.codicon-symbol-file:before { content: '\eb60'; }
+.codicon-symbol-interface:before { content: '\eb61'; }
+.codicon-symbol-keyword:before { content: '\eb62'; }
+.codicon-symbol-misc:before { content: '\eb63'; }
+.codicon-symbol-operator:before { content: '\eb64'; }
+.codicon-symbol-property:before { content: '\eb65'; }
+.codicon-wrench:before { content: '\eb65'; }
+.codicon-wrench-subaction:before { content: '\eb65'; }
+.codicon-symbol-snippet:before { content: '\eb66'; }
+.codicon-tasklist:before { content: '\eb67'; }
+.codicon-telescope:before { content: '\eb68'; }
+.codicon-text-size:before { content: '\eb69'; }
+.codicon-three-bars:before { content: '\eb6a'; }
+.codicon-thumbsdown:before { content: '\eb6b'; }
+.codicon-thumbsup:before { content: '\eb6c'; }
+.codicon-tools:before { content: '\eb6d'; }
+.codicon-triangle-down:before { content: '\eb6e'; }
+.codicon-triangle-left:before { content: '\eb6f'; }
+.codicon-triangle-right:before { content: '\eb70'; }
+.codicon-triangle-up:before { content: '\eb71'; }
+.codicon-twitter:before { content: '\eb72'; }
+.codicon-unfold:before { content: '\eb73'; }
+.codicon-unlock:before { content: '\eb74'; }
+.codicon-unmute:before { content: '\eb75'; }
+.codicon-unverified:before { content: '\eb76'; }
+.codicon-verified:before { content: '\eb77'; }
+.codicon-versions:before { content: '\eb78'; }
+.codicon-vm-active:before { content: '\eb79'; }
+.codicon-vm-outline:before { content: '\eb7a'; }
+.codicon-vm-running:before { content: '\eb7b'; }
+.codicon-watch:before { content: '\eb7c'; }
+.codicon-whitespace:before { content: '\eb7d'; }
+.codicon-whole-word:before { content: '\eb7e'; }
+.codicon-window:before { content: '\eb7f'; }
+.codicon-word-wrap:before { content: '\eb80'; }
+.codicon-zoom-in:before { content: '\eb81'; }
+.codicon-zoom-out:before { content: '\eb82'; }
+.codicon-list-filter:before { content: '\eb83'; }
+.codicon-list-flat:before { content: '\eb84'; }
+.codicon-list-selection:before { content: '\eb85'; }
+.codicon-selection:before { content: '\eb85'; }
+.codicon-list-tree:before { content: '\eb86'; }
+.codicon-debug-breakpoint-function-unverified:before { content: '\eb87'; }
+.codicon-debug-breakpoint-function:before { content: '\eb88'; }
+.codicon-debug-breakpoint-function-disabled:before { content: '\eb88'; }
+.codicon-debug-stackframe-active:before { content: '\eb89'; }
+.codicon-circle-small-filled:before { content: '\eb8a'; }
+.codicon-debug-stackframe-dot:before { content: '\eb8a'; }
+.codicon-terminal-decoration-mark:before { content: '\eb8a'; }
+.codicon-debug-stackframe:before { content: '\eb8b'; }
+.codicon-debug-stackframe-focused:before { content: '\eb8b'; }
+.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; }
+.codicon-symbol-string:before { content: '\eb8d'; }
+.codicon-debug-reverse-continue:before { content: '\eb8e'; }
+.codicon-debug-step-back:before { content: '\eb8f'; }
+.codicon-debug-restart-frame:before { content: '\eb90'; }
+.codicon-debug-alt:before { content: '\eb91'; }
+.codicon-call-incoming:before { content: '\eb92'; }
+.codicon-call-outgoing:before { content: '\eb93'; }
+.codicon-menu:before { content: '\eb94'; }
+.codicon-expand-all:before { content: '\eb95'; }
+.codicon-feedback:before { content: '\eb96'; }
+.codicon-git-pull-request-reviewer:before { content: '\eb96'; }
+.codicon-group-by-ref-type:before { content: '\eb97'; }
+.codicon-ungroup-by-ref-type:before { content: '\eb98'; }
+.codicon-account:before { content: '\eb99'; }
+.codicon-git-pull-request-assignee:before { content: '\eb99'; }
+.codicon-bell-dot:before { content: '\eb9a'; }
+.codicon-debug-console:before { content: '\eb9b'; }
+.codicon-library:before { content: '\eb9c'; }
+.codicon-output:before { content: '\eb9d'; }
+.codicon-run-all:before { content: '\eb9e'; }
+.codicon-sync-ignored:before { content: '\eb9f'; }
+.codicon-pinned:before { content: '\eba0'; }
+.codicon-github-inverted:before { content: '\eba1'; }
+.codicon-server-process:before { content: '\eba2'; }
+.codicon-server-environment:before { content: '\eba3'; }
+.codicon-pass:before { content: '\eba4'; }
+.codicon-issue-closed:before { content: '\eba4'; }
+.codicon-stop-circle:before { content: '\eba5'; }
+.codicon-play-circle:before { content: '\eba6'; }
+.codicon-record:before { content: '\eba7'; }
+.codicon-debug-alt-small:before { content: '\eba8'; }
+.codicon-vm-connect:before { content: '\eba9'; }
+.codicon-cloud:before { content: '\ebaa'; }
+.codicon-merge:before { content: '\ebab'; }
+.codicon-export:before { content: '\ebac'; }
+.codicon-graph-left:before { content: '\ebad'; }
+.codicon-magnet:before { content: '\ebae'; }
+.codicon-notebook:before { content: '\ebaf'; }
+.codicon-redo:before { content: '\ebb0'; }
+.codicon-check-all:before { content: '\ebb1'; }
+.codicon-pinned-dirty:before { content: '\ebb2'; }
+.codicon-pass-filled:before { content: '\ebb3'; }
+.codicon-circle-large-filled:before { content: '\ebb4'; }
+.codicon-circle-large:before { content: '\ebb5'; }
+.codicon-circle-large-outline:before { content: '\ebb5'; }
+.codicon-combine:before { content: '\ebb6'; }
+.codicon-gather:before { content: '\ebb6'; }
+.codicon-table:before { content: '\ebb7'; }
+.codicon-variable-group:before { content: '\ebb8'; }
+.codicon-type-hierarchy:before { content: '\ebb9'; }
+.codicon-type-hierarchy-sub:before { content: '\ebba'; }
+.codicon-type-hierarchy-super:before { content: '\ebbb'; }
+.codicon-git-pull-request-create:before { content: '\ebbc'; }
+.codicon-run-above:before { content: '\ebbd'; }
+.codicon-run-below:before { content: '\ebbe'; }
+.codicon-notebook-template:before { content: '\ebbf'; }
+.codicon-debug-rerun:before { content: '\ebc0'; }
+.codicon-workspace-trusted:before { content: '\ebc1'; }
+.codicon-workspace-untrusted:before { content: '\ebc2'; }
+.codicon-workspace-unknown:before { content: '\ebc3'; }
+.codicon-terminal-cmd:before { content: '\ebc4'; }
+.codicon-terminal-debian:before { content: '\ebc5'; }
+.codicon-terminal-linux:before { content: '\ebc6'; }
+.codicon-terminal-powershell:before { content: '\ebc7'; }
+.codicon-terminal-tmux:before { content: '\ebc8'; }
+.codicon-terminal-ubuntu:before { content: '\ebc9'; }
+.codicon-terminal-bash:before { content: '\ebca'; }
+.codicon-arrow-swap:before { content: '\ebcb'; }
+.codicon-copy:before { content: '\ebcc'; }
+.codicon-person-add:before { content: '\ebcd'; }
+.codicon-filter-filled:before { content: '\ebce'; }
+.codicon-wand:before { content: '\ebcf'; }
+.codicon-debug-line-by-line:before { content: '\ebd0'; }
+.codicon-inspect:before { content: '\ebd1'; }
+.codicon-layers:before { content: '\ebd2'; }
+.codicon-layers-dot:before { content: '\ebd3'; }
+.codicon-layers-active:before { content: '\ebd4'; }
+.codicon-compass:before { content: '\ebd5'; }
+.codicon-compass-dot:before { content: '\ebd6'; }
+.codicon-compass-active:before { content: '\ebd7'; }
+.codicon-azure:before { content: '\ebd8'; }
+.codicon-issue-draft:before { content: '\ebd9'; }
+.codicon-git-pull-request-closed:before { content: '\ebda'; }
+.codicon-git-pull-request-draft:before { content: '\ebdb'; }
+.codicon-debug-all:before { content: '\ebdc'; }
+.codicon-debug-coverage:before { content: '\ebdd'; }
+.codicon-run-errors:before { content: '\ebde'; }
+.codicon-folder-library:before { content: '\ebdf'; }
+.codicon-debug-continue-small:before { content: '\ebe0'; }
+.codicon-beaker-stop:before { content: '\ebe1'; }
+.codicon-graph-line:before { content: '\ebe2'; }
+.codicon-graph-scatter:before { content: '\ebe3'; }
+.codicon-pie-chart:before { content: '\ebe4'; }
+.codicon-bracket:before { content: '\eb0f'; }
+.codicon-bracket-dot:before { content: '\ebe5'; }
+.codicon-bracket-error:before { content: '\ebe6'; }
+.codicon-lock-small:before { content: '\ebe7'; }
+.codicon-azure-devops:before { content: '\ebe8'; }
+.codicon-verified-filled:before { content: '\ebe9'; }
+.codicon-newline:before { content: '\ebea'; }
+.codicon-layout:before { content: '\ebeb'; }
+.codicon-layout-activitybar-left:before { content: '\ebec'; }
+.codicon-layout-activitybar-right:before { content: '\ebed'; }
+.codicon-layout-panel-left:before { content: '\ebee'; }
+.codicon-layout-panel-center:before { content: '\ebef'; }
+.codicon-layout-panel-justify:before { content: '\ebf0'; }
+.codicon-layout-panel-right:before { content: '\ebf1'; }
+.codicon-layout-panel:before { content: '\ebf2'; }
+.codicon-layout-sidebar-left:before { content: '\ebf3'; }
+.codicon-layout-sidebar-right:before { content: '\ebf4'; }
+.codicon-layout-statusbar:before { content: '\ebf5'; }
+.codicon-layout-menubar:before { content: '\ebf6'; }
+.codicon-layout-centered:before { content: '\ebf7'; }
+.codicon-target:before { content: '\ebf8'; }
+.codicon-indent:before { content: '\ebf9'; }
+.codicon-record-small:before { content: '\ebfa'; }
+.codicon-error-small:before { content: '\ebfb'; }
+.codicon-terminal-decoration-error:before { content: '\ebfb'; }
+.codicon-arrow-circle-down:before { content: '\ebfc'; }
+.codicon-arrow-circle-left:before { content: '\ebfd'; }
+.codicon-arrow-circle-right:before { content: '\ebfe'; }
+.codicon-arrow-circle-up:before { content: '\ebff'; }
+.codicon-layout-sidebar-right-off:before { content: '\ec00'; }
+.codicon-layout-panel-off:before { content: '\ec01'; }
+.codicon-layout-sidebar-left-off:before { content: '\ec02'; }
+.codicon-blank:before { content: '\ec03'; }
+.codicon-heart-filled:before { content: '\ec04'; }
+.codicon-map:before { content: '\ec05'; }
+.codicon-map-horizontal:before { content: '\ec05'; }
+.codicon-fold-horizontal:before { content: '\ec05'; }
+.codicon-map-filled:before { content: '\ec06'; }
+.codicon-map-horizontal-filled:before { content: '\ec06'; }
+.codicon-fold-horizontal-filled:before { content: '\ec06'; }
+.codicon-circle-small:before { content: '\ec07'; }
+.codicon-bell-slash:before { content: '\ec08'; }
+.codicon-bell-slash-dot:before { content: '\ec09'; }
+.codicon-comment-unresolved:before { content: '\ec0a'; }
+.codicon-git-pull-request-go-to-changes:before { content: '\ec0b'; }
+.codicon-git-pull-request-new-changes:before { content: '\ec0c'; }
+.codicon-search-fuzzy:before { content: '\ec0d'; }
+.codicon-comment-draft:before { content: '\ec0e'; }
+.codicon-send:before { content: '\ec0f'; }
+.codicon-sparkle:before { content: '\ec10'; }
+.codicon-insert:before { content: '\ec11'; }
+.codicon-mic:before { content: '\ec12'; }
+.codicon-thumbsdown-filled:before { content: '\ec13'; }
+.codicon-thumbsup-filled:before { content: '\ec14'; }
+.codicon-coffee:before { content: '\ec15'; }
+.codicon-snake:before { content: '\ec16'; }
+.codicon-game:before { content: '\ec17'; }
+.codicon-vr:before { content: '\ec18'; }
+.codicon-chip:before { content: '\ec19'; }
+.codicon-piano:before { content: '\ec1a'; }
+.codicon-music:before { content: '\ec1b'; }
+.codicon-mic-filled:before { content: '\ec1c'; }
+.codicon-repo-fetch:before { content: '\ec1d'; }
+.codicon-copilot:before { content: '\ec1e'; }
+.codicon-lightbulb-sparkle:before { content: '\ec1f'; }
+.codicon-robot:before { content: '\ec20'; }
+.codicon-sparkle-filled:before { content: '\ec21'; }
+.codicon-diff-single:before { content: '\ec22'; }
+.codicon-diff-multiple:before { content: '\ec23'; }
+.codicon-surround-with:before { content: '\ec24'; }
+.codicon-share:before { content: '\ec25'; }
+.codicon-git-stash:before { content: '\ec26'; }
+.codicon-git-stash-apply:before { content: '\ec27'; }
+.codicon-git-stash-pop:before { content: '\ec28'; }
+.codicon-vscode:before { content: '\ec29'; }
+.codicon-vscode-insiders:before { content: '\ec2a'; }
+.codicon-code-oss:before { content: '\ec2b'; }
+.codicon-run-coverage:before { content: '\ec2c'; }
+.codicon-run-all-coverage:before { content: '\ec2d'; }
+.codicon-coverage:before { content: '\ec2e'; }
+.codicon-github-project:before { content: '\ec2f'; }
+.codicon-map-vertical:before { content: '\ec30'; }
+.codicon-fold-vertical:before { content: '\ec30'; }
+.codicon-map-vertical-filled:before { content: '\ec31'; }
+.codicon-fold-vertical-filled:before { content: '\ec31'; }
+.codicon-go-to-search:before { content: '\ec32'; }
+.codicon-percentage:before { content: '\ec33'; }
+.codicon-sort-percentage:before { content: '\ec33'; }
+.codicon-attach:before { content: '\ec34'; }
+.codicon-git-fetch:before { content: '\f101'; }
diff --git a/tests/assets/codicon.ttf b/tests/assets/codicon.ttf
new file mode 100644
index 0000000000..27ee4c68ca
Binary files /dev/null and b/tests/assets/codicon.ttf differ
diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts
index 0fe4a9a5c9..3eb3b11a15 100644
--- a/tests/config/traceViewerFixtures.ts
+++ b/tests/config/traceViewerFixtures.ts
@@ -62,13 +62,13 @@ class TraceViewerPage {
}
async actionIconsText(action: string) {
- const entry = await this.page.waitForSelector(`.list-view-entry:has-text("${action}")`);
+ const entry = await this.page.waitForSelector(`.tree-view-entry:has-text("${action}")`);
await entry.waitForSelector('.action-icon-value:visible');
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
}
async actionIcons(action: string) {
- return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
+ return await this.page.waitForSelector(`.tree-view-entry:has-text("${action}") .action-icons`);
}
@step
diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts
new file mode 100644
index 0000000000..f99f65fd6f
--- /dev/null
+++ b/tests/library/inspector/cli-codegen-aria.spec.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { test, expect } from './inspectorTest';
+
+test.describe(() => {
+ test.skip(({ mode }) => mode !== 'default');
+ test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
+
+ test('should generate aria snapshot', async ({ openRecorder }) => {
+ const { recorder } = await openRecorder();
+ await recorder.setContentAndWait(`Submit `);
+
+ await recorder.page.click('x-pw-tool-item.snapshot');
+ await recorder.page.hover('button');
+ await recorder.trustedClick();
+
+ await expect.poll(() =>
+ recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit"\`);`);
+ await expect.poll(() =>
+ recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\"")`);
+ await expect.poll(() =>
+ recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\"")`);
+ await expect.poll(() =>
+ recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\"");`);
+ await expect.poll(() =>
+ recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`);
+ });
+});
diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts
index 524fab1e57..d77ad86697 100644
--- a/tests/library/inspector/inspectorTest.ts
+++ b/tests/library/inspector/inspectorTest.ts
@@ -160,6 +160,15 @@ export class Recorder {
return this._sources;
}
+ async text(file: string): Promise {
+ const sources: Source[] = await this.recorderPage.evaluate(() => (window as any).playwrightSourcesEchoForTest || []);
+ for (const source of sources) {
+ if (codegenLangId2lang.get(source.id) === file)
+ return source.text;
+ }
+ return '';
+ }
+
async waitForHighlight(action: () => Promise): Promise {
await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove()));
await this.page.$$eval('x-pw-tooltip', els => els.forEach(e => e.remove()));
@@ -171,6 +180,13 @@ export class Recorder {
return this.page.locator('x-pw-tooltip').textContent();
}
+ async waitForHighlightNoTooltip(action: () => Promise): Promise {
+ await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove()));
+ await action();
+ await this.page.locator('x-pw-highlight').waitFor();
+ return '';
+ }
+
async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> {
let callback;
const listener = async msg => {
@@ -185,8 +201,8 @@ export class Recorder {
return new Promise(f => callback = f);
}
- async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }}): Promise {
- return this.waitForHighlight(async () => {
+ async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }, omitTooltip?: boolean }): Promise {
+ return (options?.omitTooltip ? this.waitForHighlightNoTooltip : this.waitForHighlight).call(this, async () => {
const box = await this.page.locator(selector).first().boundingBox();
const offset = options?.position || { x: box.width / 2, y: box.height / 2 };
await this.page.mouse.move(box.x + offset.x, box.y + offset.y);
diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts
index 405fffbe5b..647706956f 100644
--- a/tests/library/inspector/pause.spec.ts
+++ b/tests/library/inspector/pause.spec.ts
@@ -15,7 +15,7 @@
*/
import type { Page } from 'playwright-core';
-import { test as it, expect } from './inspectorTest';
+import { test as it, expect, Recorder } from './inspectorTest';
import { waitForTestLog } from '../../config/utils';
@@ -483,6 +483,7 @@ it.describe('pause', () => {
});
it('should record from debugger', async ({ page, recorderPageGetter }) => {
+ await page.setContent('');
const scriptPromise = (async () => {
await page.pause();
})();
@@ -490,7 +491,11 @@ it.describe('pause', () => {
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
await expect(recorderPage.locator('.source-line-paused')).toHaveText(/await page\.pause\(\)/);
await recorderPage.getByRole('button', { name: 'Record' }).click();
- await page.locator('body').click();
+
+ const recorder = new Recorder(page, recorderPage);
+ await recorder.hoverOverElement('body', { omitTooltip: true });
+ await recorder.trustedClick();
+
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue('javascript');
await expect(recorderPage.locator('.cm-wrapper')).toContainText(`await page.locator('body').click();`);
await recorderPage.getByRole('button', { name: 'Resume' }).click();
diff --git a/tests/library/sequence.spec.ts b/tests/library/sequence.spec.ts
new file mode 100644
index 0000000000..6004ce4da5
--- /dev/null
+++ b/tests/library/sequence.spec.ts
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import { test as it, expect } from '@playwright/test';
+import { findRepeatedSubsequences } from '../../packages/playwright-core/lib/utils/sequence';
+
+it('should return an empty array when the input is empty', () => {
+ const input = [];
+ const expectedOutput = [];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle a single-element array', () => {
+ const input = ['a'];
+ const expectedOutput = [{ sequence: ['a'], count: 1 }];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle an array with no repeats', () => {
+ const input = ['a', 'b', 'c'];
+ const expectedOutput = [
+ { sequence: ['a'], count: 1 },
+ { sequence: ['b'], count: 1 },
+ { sequence: ['c'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle contiguous repeats of single elements', () => {
+ const input = ['a', 'a', 'a', 'b', 'b', 'c'];
+ const expectedOutput = [
+ { sequence: ['a'], count: 3 },
+ { sequence: ['b'], count: 2 },
+ { sequence: ['c'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should detect longer repeating substrings', () => {
+ const input = ['a', 'b', 'a', 'b', 'a', 'b'];
+ const expectedOutput = [{ sequence: ['a', 'b'], count: 3 }];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle multiple repeating substrings', () => {
+ const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b'];
+ const expectedOutput = [
+ { sequence: ['a', 'a', 'b', 'b'], count: 2 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle complex cases with overlapping repeats', () => {
+ const input = ['a', 'a', 'a', 'a'];
+ const expectedOutput = [{ sequence: ['a'], count: 4 }];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle complex acceptance cases with multiple possible repeats', () => {
+ const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b', 'c', 'c', 'c', 'c'];
+ const expectedOutput = [
+ { sequence: ['a', 'a', 'b', 'b'], count: 2 },
+ { sequence: ['c'], count: 4 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle non-repeating sequences correctly', () => {
+ const input = ['a', 'b', 'c', 'd', 'e'];
+ const expectedOutput = [
+ { sequence: ['a'], count: 1 },
+ { sequence: ['b'], count: 1 },
+ { sequence: ['c'], count: 1 },
+ { sequence: ['d'], count: 1 },
+ { sequence: ['e'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle a case where the entire array is a repeating sequence', () => {
+ const input = ['x', 'y', 'x', 'y', 'x', 'y'];
+ const expectedOutput = [{ sequence: ['x', 'y'], count: 3 }];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should correctly identify the maximal repeating substring', () => {
+ const input = ['a', 'b', 'a', 'b', 'a', 'b', 'c', 'c', 'c', 'c'];
+ const expectedOutput = [
+ { sequence: ['a', 'b'], count: 3 },
+ { sequence: ['c'], count: 4 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle repeats with varying lengths', () => {
+ const input = ['a', 'a', 'b', 'b', 'b', 'b', 'a', 'a'];
+ const expectedOutput = [
+ { sequence: ['a'], count: 2 },
+ { sequence: ['b'], count: 4 },
+ { sequence: ['a'], count: 2 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should correctly handle a repeat count of one (k adjustment to zero)', () => {
+ const input = ['a', 'b', 'a', 'b', 'c'];
+ const expectedOutput = [
+ { sequence: ['a', 'b'], count: 2 },
+ { sequence: ['c'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should correctly handle repeats at the end of the array', () => {
+ const input = ['x', 'y', 'x', 'y', 'x', 'y', 'z'];
+ const expectedOutput = [
+ { sequence: ['x', 'y'], count: 3 },
+ { sequence: ['z'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should not overcount repeats when the last potential repeat is incomplete', () => {
+ const input = ['m', 'n', 'm', 'n', 'm'];
+ const expectedOutput = [
+ { sequence: ['m', 'n'], count: 2 },
+ { sequence: ['m'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
+
+it('should handle single repeats correctly when the substring length is greater than one', () => {
+ const input = ['a', 'b', 'c', 'a', 'b', 'd'];
+ const expectedOutput = [
+ { sequence: ['a'], count: 1 },
+ { sequence: ['b'], count: 1 },
+ { sequence: ['c'], count: 1 },
+ { sequence: ['a'], count: 1 },
+ { sequence: ['b'], count: 1 },
+ { sequence: ['d'], count: 1 },
+ ];
+ expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
+});
diff --git a/tests/page/expect-matcher-result.spec.ts b/tests/page/expect-matcher-result.spec.ts
index 8f8a83bc83..7767ecf5f6 100644
--- a/tests/page/expect-matcher-result.spec.ts
+++ b/tests/page/expect-matcher-result.spec.ts
@@ -24,12 +24,16 @@ test('toMatchText-based assertions should have matcher result', async ({ page })
{
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
e.matcherResult.message = stripAnsi(e.matcherResult.message);
+ e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff);
expect.soft(e.matcherResult).toEqual({
actual: 'Text content',
expected: /Text2/,
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
name: 'toHaveText',
pass: false,
+ locator: `locator('#node')`,
+ printedDiff: `Expected pattern: /Text2/
+Received string: \"Text content\"`,
log: expect.any(Array),
timeout: 1,
});
@@ -46,12 +50,17 @@ Call log`);
{
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
e.matcherResult.message = stripAnsi(e.matcherResult.message);
+ e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected);
+ e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived);
expect.soft(e.matcherResult).toEqual({
actual: 'Text content',
expected: /Text/,
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
name: 'toHaveText',
pass: true,
+ locator: `locator('#node')`,
+ printedExpected: 'Expected pattern: not /Text/',
+ printedReceived: `Received string: \"Text content\"`,
log: expect.any(Array),
timeout: 1,
});
@@ -79,6 +88,8 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page })
name: 'toBeVisible',
pass: false,
log: expect.any(Array),
+ printedExpected: 'Expected: visible',
+ printedReceived: 'Received: ',
timeout: 1,
});
@@ -101,6 +112,8 @@ Call log`);
name: 'toBeVisible',
pass: true,
log: expect.any(Array),
+ printedExpected: 'Expected: not visible',
+ printedReceived: 'Received: visible',
timeout: 1,
});
@@ -120,6 +133,7 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
{
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
e.matcherResult.message = stripAnsi(e.matcherResult.message);
+ e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff);
expect.soft(e.matcherResult).toEqual({
actual: 0,
expected: 1,
@@ -127,6 +141,8 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
name: 'toHaveCount',
pass: false,
log: expect.any(Array),
+ printedDiff: `Expected: 1
+Received: 0`,
timeout: 1,
});
@@ -141,6 +157,8 @@ Call log`);
{
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
e.matcherResult.message = stripAnsi(e.matcherResult.message);
+ e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected);
+ e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived);
expect.soft(e.matcherResult).toEqual({
actual: 1,
expected: 1,
@@ -148,6 +166,8 @@ Call log`);
name: 'toHaveCount',
pass: true,
log: expect.any(Array),
+ printedExpected: `Expected: not 1`,
+ printedReceived: `Received: 1`,
timeout: 1,
});
@@ -177,6 +197,8 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag
name: 'toBeChecked',
pass: false,
log: expect.any(Array),
+ printedExpected: 'Expected: checked',
+ printedReceived: 'Received: unchecked',
timeout: 1,
});
@@ -199,6 +221,8 @@ Call log`);
name: 'toBeChecked',
pass: true,
log: expect.any(Array),
+ printedExpected: 'Expected: not checked',
+ printedReceived: 'Received: checked',
timeout: 1,
});
@@ -221,6 +245,8 @@ Call log`);
name: 'toBeChecked',
pass: false,
log: expect.any(Array),
+ printedExpected: 'Expected: unchecked',
+ printedReceived: 'Received: checked',
timeout: 1,
});
@@ -243,6 +269,8 @@ Call log`);
name: 'toBeChecked',
pass: true,
log: expect.any(Array),
+ printedExpected: 'Expected: not unchecked',
+ printedReceived: 'Received: unchecked',
timeout: 1,
});
@@ -271,6 +299,8 @@ test('toHaveScreenshot should populate matcherResult', async ({ page, server, is
name: 'toHaveScreenshot',
pass: false,
log: expect.any(Array),
+ printedExpected: expect.stringContaining('screenshot-sanity-'),
+ printedReceived: expect.stringContaining('screenshot-sanity-actual'),
});
expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed:
diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts
new file mode 100644
index 0000000000..88306a1e43
--- /dev/null
+++ b/tests/page/page-aria-snapshot.spec.ts
@@ -0,0 +1,387 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Locator } from '@playwright/test';
+import { test as it, expect } from './pageTest';
+
+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');
+}
+
+async function checkAndMatchSnapshot(locator: Locator, snapshot: string) {
+ expect.soft(await locator.ariaSnapshot()).toBe(unshift(snapshot));
+ await expect.soft(locator).toMatchAriaSnapshot(snapshot);
+}
+
+it('should snapshot', async ({ page }) => {
+ await page.setContent(`title `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - heading "title" [level=1]
+ `);
+});
+
+it('should snapshot list', async ({ page }) => {
+ await page.setContent(`
+ title
+ title 2
+ `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - heading "title" [level=1]
+ - heading "title 2" [level=1]
+ `);
+});
+
+it('should snapshot list with accessible name', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - list "my list":
+ - listitem: one
+ - listitem: two
+ `);
+});
+
+it('should snapshot complex', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - list:
+ - listitem:
+ - link "link"
+ `);
+});
+
+it('should allow text nodes', async ({ page }) => {
+ await page.setContent(`
+ Microsoft
+ Open source projects and samples from Microsoft
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - heading "Microsoft" [level=1]
+ - text: Open source projects and samples from Microsoft
+ `);
+});
+
+it('should snapshot details visibility', async ({ page }) => {
+ await page.setContent(`
+
+ Summary
+ Details
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - group: Summary
+ `);
+});
+
+it('should snapshot integration', async ({ page }) => {
+ await page.setContent(`
+ Microsoft
+ Open source projects and samples from Microsoft
+
+
+
+
+ Verified
+
+
+
+
+ We've verified that the organization microsoft controls the domain:
+
+
+
+ opensource.microsoft.com
+
+
+
+
+
+
+
+
+
+ Sponsor
+
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - heading "Microsoft" [level=1]
+ - text: Open source projects and samples from Microsoft
+ - list:
+ - listitem:
+ - group: Verified
+ - listitem:
+ - link "Sponsor"
+ `);
+});
+
+it('should support multiline text', async ({ page }) => {
+ await page.setContent(`
+
+ Line 1
+ Line 2
+ Line 3
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - paragraph: Line 1 Line 2 Line 3
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - paragraph: |
+ Line 1
+ Line 2
+ Line 3
+ `);
+});
+
+it('should concatenate span text', async ({ page }) => {
+ await page.setContent(`
+ One Two Three
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - text: One Two Three
+ `);
+});
+
+it('should concatenate span text 2', async ({ page }) => {
+ await page.setContent(`
+ One Two Three
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - text: One Two Three
+ `);
+});
+
+it('should concatenate div text with spaces', async ({ page }) => {
+ await page.setContent(`
+ One
Two
Three
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - text: One Two Three
+ `);
+});
+
+it('should include pseudo in text', async ({ page }) => {
+ await page.setContent(`
+
+
+ hello
+ hello
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - link "worldhello hellobye"
+ `);
+});
+
+it('should not include hidden pseudo in text', async ({ page }) => {
+ await page.setContent(`
+
+
+ hello
+ hello
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - link "hello hello"
+ `);
+});
+
+it('should include new line for block pseudo', async ({ page }) => {
+ await page.setContent(`
+
+
+ hello
+ hello
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - link "world hello hello bye"
+ `);
+});
+
+it('should work with slots', async ({ page }) => {
+ // Text "foo" is assigned to the slot, should not be used twice.
+ await page.setContent(`
+ foo
+
+ `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - button "foo"
+ `);
+
+ // Text "foo" is assigned to the slot, should be used instead of slot content.
+ await page.setContent(`
+ foo
+
+ `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - button "foo"
+ `);
+
+ // Nothing is assigned to the slot, should use slot content.
+ await page.setContent(`
+
+
+ `);
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - button "pre"
+ `);
+});
+
+it('should snapshot inner text', async ({ page }) => {
+ await page.setContent(`
+
+
+
+ a.test.ts
+
+
+
+
+
+
+
+
+
+
+
+ snapshot
+
+
30ms
+
+
+
+
+
+
+
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - listitem:
+ - text: a.test.ts
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - listitem:
+ - text: snapshot 30ms
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ `);
+});
+
+it('should include pseudo codepoints', async ({ page, server }) => {
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+
+ hello
+ `);
+
+ await checkAndMatchSnapshot(page.locator('body'), `
+ - paragraph: \ueab2hello
+ `);
+});
diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts
index f32f224374..2c1d7a7eba 100644
--- a/tests/page/page-event-request.spec.ts
+++ b/tests/page/page-event-request.spec.ts
@@ -258,3 +258,18 @@ it('should finish 204 request', {
page.evaluate(async url => { await fetch(url); }, server.PREFIX + '/204').catch(() => {});
expect(await reqPromise).toBe('requestfinished');
});
+
+it(' resource should have type image', async ({ page }) => {
+ it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33148' });
+ const [request] = await Promise.all([
+ page.waitForEvent('request'),
+ page.setContent(`
+
+
+
+
+
+ `)
+ ]);
+ expect(request.resourceType()).toBe('image');
+});
\ No newline at end of file
diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts
index 826f8cc90e..1335be5a59 100644
--- a/tests/page/to-match-aria-snapshot.spec.ts
+++ b/tests/page/to-match-aria-snapshot.spec.ts
@@ -78,7 +78,7 @@ test('should match complex', async ({ page }) => {
test('should match regex', async ({ page }) => {
await page.setContent(`Issues 12 `);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- - heading /Issues \\d+/
+ - heading ${/Issues \d+/}
`);
});
@@ -94,7 +94,7 @@ test('should allow text nodes', async ({ page }) => {
`);
});
-test('details visibility', async ({ page, browserName }) => {
+test('details visibility', async ({ page }) => {
await page.setContent(`
Summary
@@ -107,7 +107,222 @@ test('details visibility', async ({ page, browserName }) => {
`);
});
-test('integration test', async ({ page, browserName }) => {
+test('checked attribute', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - checkbox
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - checkbox [checked]
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - checkbox [checked=true]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - checkbox [checked=false]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - checkbox [checked=mixed]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - checkbox [checked=5]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain(' attribute must be a boolean or "mixed"');
+ }
+});
+
+test('disabled attribute', async ({ page }) => {
+ await page.setContent(`
+ Click me
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [disabled]
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [disabled=true]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [disabled=false]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [disabled=invalid]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain(' attribute must be a boolean');
+ }
+});
+
+test('expanded attribute', async ({ page }) => {
+ await page.setContent(`
+ Toggle
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [expanded]
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [expanded=true]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [expanded=false]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [expanded=invalid]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain(' attribute must be a boolean');
+ }
+});
+
+test('level attribute', async ({ page }) => {
+ await page.setContent(`
+ Section Title
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - heading
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - heading [level=2]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - heading [level=3]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - heading [level=two]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain(' attribute must be a number');
+ }
+});
+
+test('pressed attribute', async ({ page }) => {
+ await page.setContent(`
+ Like
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [pressed]
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [pressed=true]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [pressed=false]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ // Test for 'mixed' state
+ await page.setContent(`
+ Like
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [pressed=mixed]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [pressed=true]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button [pressed=5]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain(' attribute must be a boolean or "mixed"');
+ }
+});
+
+test('selected attribute', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - row
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - row [selected]
+ `);
+
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - row [selected=true]
+ `);
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - row [selected=false]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain('Timed out 1000ms waiting for expect');
+ }
+
+ {
+ const e = await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - row [selected=invalid]
+ `, { timeout: 1000 }).catch(e => e);
+ expect(stripAnsi(e.message)).toContain(' attribute must be a boolean');
+ }
+});
+
+test('integration test', async ({ page }) => {
await page.setContent(`
Microsoft
Open source projects and samples from Microsoft
@@ -178,14 +393,15 @@ test('expected formatter', async ({ page }) => {
- heading "todos"
- textbox "Wrong text"
`, { timeout: 1 }).catch(e => e);
- expect(stripAnsi(error.message)).toContain(`- Expected - 3
+
+ expect(stripAnsi(error.message)).toContain(`
+Locator: locator('body')
+- Expected - 2
+ Received string + 3
--
-+ - :
-+ - banner:
- - heading "todos"
-- - textbox "Wrong text"
--
-+ - textbox "What needs to be done?"`);
+- - heading "todos"
+- - textbox "Wrong text"
++ - banner:
++ - heading "todos" [level=1]
++ - textbox "What needs to be done?"`);
});
diff --git a/tests/playwright-test/basic.spec.ts b/tests/playwright-test/basic.spec.ts
index 3b47603c25..ce6825c559 100644
--- a/tests/playwright-test/basic.spec.ts
+++ b/tests/playwright-test/basic.spec.ts
@@ -153,6 +153,10 @@ test('should respect focused tests', async ({ runInlineTest }) => {
});
});
+ test.fail.only('focused fail.only test', () => {
+ expect(1 + 1).toBe(3);
+ });
+
test.describe('non-focused describe', () => {
test('describe test', () => {
expect(1 + 1).toBe(3);
@@ -172,13 +176,46 @@ test('should respect focused tests', async ({ runInlineTest }) => {
test.only('test4', () => {
expect(1 + 1).toBe(2);
});
+ test.fail.only('test5', () => {
+ expect(1 + 1).toBe(3);
+ });
});
`
});
- expect(passed).toBe(5);
+ expect(passed).toBe(7);
expect(exitCode).toBe(0);
});
+test('should respect focused tests with test.fail', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'fail-only.spec.ts': `
+ import { test, expect } from '@playwright/test';
+
+ test('test1', () => {
+ console.log('test1 should not run');
+ expect(1 + 1).toBe(2);
+ });
+
+ test.fail.only('test2', () => {
+ console.log('test2 should run and fail');
+ expect(1 + 1).toBe(3);
+ });
+
+ test('test3', () => {
+ console.log('test3 should not run');
+ expect(1 + 1).toBe(2);
+ });
+ `,
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+ expect(result.failed).toBe(0);
+ expect(result.skipped).toBe(0);
+ expect(result.output).toContain('test2 should run and fail');
+ expect(result.output).not.toContain('test1 should not run');
+ expect(result.output).not.toContain('test3 should not run');
+});
+
test('skip should take priority over fail', async ({ runInlineTest }) => {
const { passed, skipped, failed } = await runInlineTest({
'test.spec.ts': `
@@ -550,3 +587,33 @@ test('should support describe.fixme', async ({ runInlineTest }) => {
expect(result.skipped).toBe(3);
expect(result.output).toContain('heytest4');
});
+
+test('should fail when test.fail.only passes unexpectedly', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'fail-only-pass.spec.ts': `
+ import { test, expect } from '@playwright/test';
+
+ test('test1', () => {
+ console.log('test1 should not run');
+ expect(1 + 1).toBe(2);
+ });
+
+ test.fail.only('test2', () => {
+ console.log('test2 should run and pass unexpectedly');
+ expect(1 + 1).toBe(2);
+ });
+
+ test('test3', () => {
+ console.log('test3 should not run');
+ expect(1 + 1).toBe(2);
+ });
+ `,
+ });
+ expect(result.exitCode).toBe(1);
+ expect(result.passed).toBe(0);
+ expect(result.failed).toBe(1);
+ expect(result.skipped).toBe(0);
+ expect(result.output).toContain('should run and pass unexpectedly');
+ expect(result.output).not.toContain('test1 should not run');
+ expect(result.output).not.toContain('test3 should not run');
+});
diff --git a/tests/playwright-test/global-setup.spec.ts b/tests/playwright-test/global-setup.spec.ts
index 3d28be82cd..f1bd7b7458 100644
--- a/tests/playwright-test/global-setup.spec.ts
+++ b/tests/playwright-test/global-setup.spec.ts
@@ -386,3 +386,43 @@ test('teardown after error', async ({ runInlineTest }) => {
'teardown 1',
]);
});
+
+test('globalSetup should support multiple', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'playwright.config.ts': `
+ module.exports = {
+ globalSetup: ['./globalSetup1.ts','./globalSetup2.ts','./globalSetup3.ts','./globalSetup4.ts'],
+ globalTeardown: ['./globalTeardown1.ts', './globalTeardown2.ts'],
+ };
+ `,
+ 'globalSetup1.ts': `module.exports = () => { console.log('%%globalSetup1'); return () => { console.log('%%globalSetup1Function'); throw new Error('kaboom'); } };`,
+ 'globalSetup2.ts': `module.exports = () => console.log('%%globalSetup2');`,
+ 'globalSetup3.ts': `module.exports = () => { console.log('%%globalSetup3'); return () => console.log('%%globalSetup3Function'); }`,
+ 'globalSetup4.ts': `module.exports = () => console.log('%%globalSetup4');`,
+ 'globalTeardown1.ts': `module.exports = () => console.log('%%globalTeardown1')`,
+ 'globalTeardown2.ts': `module.exports = () => { console.log('%%globalTeardown2'); throw new Error('kaboom'); }`,
+
+ 'a.test.js': `
+ import { test } from '@playwright/test';
+ test('a', () => console.log('%%test a'));
+ test('b', () => console.log('%%test b'));
+ `,
+ }, { reporter: 'line' });
+ expect(result.passed).toBe(2);
+
+ // behaviour: setups in order, teardowns in reverse order.
+ // setup-returned functions inherit their position, and take precedence over `globalTeardown` scripts.
+ expect(result.outputLines).toEqual([
+ 'globalSetup1',
+ 'globalSetup2',
+ 'globalSetup3',
+ 'globalSetup4',
+ 'test a',
+ 'test b',
+ 'globalSetup3Function',
+ 'globalTeardown2',
+ 'globalSetup1Function',
+ // 'globalTeardown1' is missing, because globalSetup1Function errored out.
+ ]);
+ expect(result.output).toContain('Error: kaboom');
+});
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 6a75602bf1..d9e604f994 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -43,7 +43,7 @@ const expect = baseExpect.configure({ timeout: process.env.CI ? 75000 : 25000 })
test.describe.configure({ mode: 'parallel' });
-for (const useIntermediateMergeReport of [false] as const) {
+for (const useIntermediateMergeReport of [true, false] as const) {
test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
test.use({ useIntermediateMergeReport });
@@ -612,7 +612,7 @@ for (const useIntermediateMergeReport of [false] as const) {
]);
});
`,
- }, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
+ }, { reporter: 'html,dot' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
@@ -727,6 +727,34 @@ for (const useIntermediateMergeReport of [false] as const) {
]);
});
+ test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => {
+ const result = await runInlineTest({
+ 'playwright.config.js': `
+ export default { testDir: './tests' };
+ `,
+ 'tests/a.test.ts': `
+ import { test, expect } from '@playwright/test';
+
+ test('example', async ({}) => {
+ await test.step('step title', async () => {
+ expect(1).toBe(1);
+ });
+ });
+ `,
+ }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+
+ await showReport();
+ await page.click('text=example');
+ await page.click('text=step title');
+ await page.click('text=expect.toBe');
+ await expect(page.getByTestId('test-snippet')).toContainText([
+ `await test.step('step title', async () => {`,
+ 'expect(1).toBe(1);',
+ ]);
+ });
+
test('should render annotations', async ({ runInlineTest, page, showReport }) => {
const result = await runInlineTest({
'playwright.config.js': `
diff --git a/tests/playwright-test/reporter-markdown.spec.ts b/tests/playwright-test/reporter-markdown.spec.ts
index d24f2561c1..076e28d66e 100644
--- a/tests/playwright-test/reporter-markdown.spec.ts
+++ b/tests/playwright-test/reporter-markdown.spec.ts
@@ -18,12 +18,14 @@ import fs from 'fs';
import path from 'path';
import { expect, test } from './playwright-test-fixtures';
+const markdownReporter = require.resolve('../../packages/playwright/lib/reporters/markdown');
+
test('simple report', async ({ runInlineTest }) => {
const files = {
'playwright.config.ts': `
module.exports = {
retries: 1,
- reporter: 'markdown',
+ reporter: ${JSON.stringify(markdownReporter)},
};
`,
'dir1/a.test.js': `
@@ -83,7 +85,7 @@ test('custom report file', async ({ runInlineTest }) => {
const files = {
'playwright.config.ts': `
module.exports = {
- reporter: [['markdown', { outputFile: 'my-report.md' }]],
+ reporter: [[${JSON.stringify(markdownReporter)}, { outputFile: 'my-report.md' }]],
};
`,
'a.test.js': `
@@ -107,7 +109,7 @@ test('report error without snippet', async ({ runInlineTest }) => {
'playwright.config.ts': `
module.exports = {
retries: 1,
- reporter: 'markdown',
+ reporter: ${JSON.stringify(markdownReporter)},
};
`,
'a.test.js': `
@@ -135,7 +137,7 @@ test('report with worker error', async ({ runInlineTest }) => {
'playwright.config.ts': `
module.exports = {
retries: 1,
- reporter: 'markdown',
+ reporter: ${JSON.stringify(markdownReporter)},
};
`,
'a.test.js': `
diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json
index 180f4d9b33..1ebdfb52cc 100644
--- a/tests/playwright-test/stable-test-runner/package-lock.json
+++ b/tests/playwright-test/stable-test-runner/package-lock.json
@@ -5,16 +5,15 @@
"packages": {
"": {
"dependencies": {
- "@playwright/test": "1.48.0-beta-1728384960000"
+ "@playwright/test": "1.49.0-alpha-2024-10-20"
}
},
"node_modules/@playwright/test": {
- "version": "1.48.0-beta-1728384960000",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0-beta-1728384960000.tgz",
- "integrity": "sha512-bqQorY7LKVldgwAsUbjULdwKEoUlZ8OOHRZmM/1XyGiGqJwzTGdr0x8Ss312BvKddAh+5pz8cbaPopw10Rp3Ng==",
- "license": "Apache-2.0",
+ "version": "1.49.0-alpha-2024-10-20",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz",
+ "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==",
"dependencies": {
- "playwright": "1.48.0-beta-1728384960000"
+ "playwright": "1.49.0-alpha-2024-10-20"
},
"bin": {
"playwright": "cli.js"
@@ -28,7 +27,6 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -38,12 +36,11 @@
}
},
"node_modules/playwright": {
- "version": "1.48.0-beta-1728384960000",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0-beta-1728384960000.tgz",
- "integrity": "sha512-5pIZTwoktOGYJL+YpF2RNhGzVUY6rA/ceQAT0lEQSZaL55MKUzraD2FAoZoBnz84cIIks2ZSlXt8j5mJ5xXt8g==",
- "license": "Apache-2.0",
+ "version": "1.49.0-alpha-2024-10-20",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz",
+ "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==",
"dependencies": {
- "playwright-core": "1.48.0-beta-1728384960000"
+ "playwright-core": "1.49.0-alpha-2024-10-20"
},
"bin": {
"playwright": "cli.js"
@@ -56,10 +53,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.48.0-beta-1728384960000",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0-beta-1728384960000.tgz",
- "integrity": "sha512-atIhpuvqvVEW5luPhwzhdcXsGdPvzOBLXAg3+MvOLY+6Q4JcTfXMTtTmltP+llUV+LAgj38foQz+6tKTzNMlWg==",
- "license": "Apache-2.0",
+ "version": "1.49.0-alpha-2024-10-20",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz",
+ "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==",
"bin": {
"playwright-core": "cli.js"
},
@@ -70,11 +66,11 @@
},
"dependencies": {
"@playwright/test": {
- "version": "1.48.0-beta-1728384960000",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0-beta-1728384960000.tgz",
- "integrity": "sha512-bqQorY7LKVldgwAsUbjULdwKEoUlZ8OOHRZmM/1XyGiGqJwzTGdr0x8Ss312BvKddAh+5pz8cbaPopw10Rp3Ng==",
+ "version": "1.49.0-alpha-2024-10-20",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz",
+ "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==",
"requires": {
- "playwright": "1.48.0-beta-1728384960000"
+ "playwright": "1.49.0-alpha-2024-10-20"
}
},
"fsevents": {
@@ -84,18 +80,18 @@
"optional": true
},
"playwright": {
- "version": "1.48.0-beta-1728384960000",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0-beta-1728384960000.tgz",
- "integrity": "sha512-5pIZTwoktOGYJL+YpF2RNhGzVUY6rA/ceQAT0lEQSZaL55MKUzraD2FAoZoBnz84cIIks2ZSlXt8j5mJ5xXt8g==",
+ "version": "1.49.0-alpha-2024-10-20",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz",
+ "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==",
"requires": {
"fsevents": "2.3.2",
- "playwright-core": "1.48.0-beta-1728384960000"
+ "playwright-core": "1.49.0-alpha-2024-10-20"
}
},
"playwright-core": {
- "version": "1.48.0-beta-1728384960000",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0-beta-1728384960000.tgz",
- "integrity": "sha512-atIhpuvqvVEW5luPhwzhdcXsGdPvzOBLXAg3+MvOLY+6Q4JcTfXMTtTmltP+llUV+LAgj38foQz+6tKTzNMlWg=="
+ "version": "1.49.0-alpha-2024-10-20",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz",
+ "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA=="
}
}
}
diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json
index 3e32d0bbb7..dbe21acd15 100644
--- a/tests/playwright-test/stable-test-runner/package.json
+++ b/tests/playwright-test/stable-test-runner/package.json
@@ -1,6 +1,6 @@
{
"private": true,
"dependencies": {
- "@playwright/test": "1.48.0-beta-1728384960000"
+ "@playwright/test": "1.49.0-alpha-2024-10-20"
}
}
diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts
index 0dd41bd0ab..f7fb9a6ae0 100644
--- a/tests/playwright-test/test-modifiers.spec.ts
+++ b/tests/playwright-test/test-modifiers.spec.ts
@@ -279,6 +279,33 @@ test.describe('test modifier annotations', () => {
expectTest('focused fixme by suite', 'skipped', 'skipped', ['fixme']);
});
+ test('should work with fail.only inside describe.only', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'a.test.ts': `
+ import { test, expect } from '@playwright/test';
+
+ test.describe.only("suite", () => {
+ test.skip('focused skip by suite', () => {});
+ test.fixme('focused fixme by suite', () => {});
+ test.fail.only('focused fail by suite', () => { expect(1).toBe(2); });
+ });
+
+ test.describe.skip('not focused', () => {
+ test('no marker', () => {});
+ });
+ `,
+ });
+ const expectTest = expectTestHelper(result);
+
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+ expect(result.failed).toBe(0);
+ expect(result.skipped).toBe(0);
+ expectTest('focused skip by suite', 'skipped', 'skipped', ['skip']);
+ expectTest('focused fixme by suite', 'skipped', 'skipped', ['fixme']);
+ expectTest('focused fail by suite', 'failed', 'expected', ['fail']);
+ });
+
test('should not multiple on retry', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
diff --git a/tests/playwright-test/types-2.spec.ts b/tests/playwright-test/types-2.spec.ts
index e61d4870ed..f794e06798 100644
--- a/tests/playwright-test/types-2.spec.ts
+++ b/tests/playwright-test/types-2.spec.ts
@@ -33,6 +33,7 @@ test('basics should work', async ({ runTSC }) => {
test.skip('my test', async () => {});
test.fixme('my test', async () => {});
test.fail('my test', async () => {});
+ test.fail.only('my test', async () => {});
});
test.describe(() => {
test('my test', () => {});
@@ -59,6 +60,7 @@ test('basics should work', async ({ runTSC }) => {
test.fixme('title', { tag: '@foo' }, () => {});
test.only('title', { tag: '@foo' }, () => {});
test.fail('title', { tag: '@foo' }, () => {});
+ test.fail.only('title', { tag: '@foo' }, () => {});
test.describe('title', { tag: '@foo' }, () => {});
test.describe('title', { annotation: { type: 'issue' } }, () => {});
// @ts-expect-error
diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts
index 2952761d60..7ae98bb662 100644
--- a/tests/playwright-test/ui-mode-fixtures.ts
+++ b/tests/playwright-test/ui-mode-fixtures.ts
@@ -66,16 +66,17 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () =
}
const result: string[] = [];
- const listItems = treeElement.querySelectorAll('[role=listitem]');
- for (const listItem of listItems) {
- const iconElements = listItem.querySelectorAll('.codicon');
+ const treeItems = treeElement.querySelectorAll('[role=treeitem]');
+ for (const treeItem of treeItems) {
+ const treeItemHeader = treeItem.querySelector('.tree-view-entry');
+ const iconElements = treeItemHeader.querySelectorAll('.codicon');
const treeIcon = iconName(iconElements[0]);
const statusIcon = iconName(iconElements[1]);
- const indent = listItem.querySelectorAll('.list-view-indent').length;
- const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' π' : '';
- const selected = listItem.classList.contains('selected') ? ' <=' : '';
- const title = listItem.querySelector('.ui-mode-list-item-title').childNodes[0].textContent;
- const timeElement = options.time ? listItem.querySelector('.ui-mode-list-item-time') : undefined;
+ const indent = treeItemHeader.querySelectorAll('.tree-view-indent').length;
+ const watch = treeItemHeader.querySelector('.toolbar-button.eye.toggled') ? ' π' : '';
+ const selected = treeItem.getAttribute('aria-selected') === 'true' ? ' <=' : '';
+ const title = treeItemHeader.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent;
+ const timeElement = options.time ? treeItemHeader.querySelector('.ui-mode-tree-item-time') : undefined;
const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : '';
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected);
}
diff --git a/tests/playwright-test/ui-mode-test-annotations.spec.ts b/tests/playwright-test/ui-mode-test-annotations.spec.ts
index 7a0dea8af1..eeff6a5aca 100644
--- a/tests/playwright-test/ui-mode-test-annotations.spec.ts
+++ b/tests/playwright-test/ui-mode-test-annotations.spec.ts
@@ -33,7 +33,7 @@ test('should display annotations', async ({ runUITest }) => {
});
await page.getByTitle('Run all').click();
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
- await page.getByRole('listitem').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('Annotations', { exact: true }).click();
diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts
index 5d70048473..dd59c334b2 100644
--- a/tests/playwright-test/ui-mode-test-filters.spec.ts
+++ b/tests/playwright-test/ui-mode-test-filters.spec.ts
@@ -64,7 +64,7 @@ test('should display native tags and filter by them on click', async ({ runUITes
test('pwt', { tag: '@smoke' }, () => {});
`,
});
- await page.locator('.ui-mode-list-item-title').getByText('smoke').click();
+ await page.locator('.ui-mode-tree-item-title').getByText('smoke').click();
await expect(page.getByPlaceholder('Filter')).toHaveValue('@smoke');
await expect.poll(dumpTestTree(page)).toBe(`
βΌ β― a.test.ts
diff --git a/tests/playwright-test/ui-mode-test-progress.spec.ts b/tests/playwright-test/ui-mode-test-progress.spec.ts
index f87eaa8fbc..f2f01a79ce 100644
--- a/tests/playwright-test/ui-mode-test-progress.spec.ts
+++ b/tests/playwright-test/ui-mode-test-progress.spec.ts
@@ -47,7 +47,7 @@ test('should update trace live', async ({ runUITest, server }) => {
await page.getByText('live test').dblclick();
// It should halt on loading one.html.
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -57,11 +57,11 @@ test('should update trace live', async ({ runUITest, server }) => {
]);
await expect(
- listItem.locator(':scope.selected'),
+ listItem.locator(':scope[aria-selected="true"]'),
'last action to be selected'
).toHaveText(/page.goto/);
await expect(
- listItem.locator(':scope.selected .codicon.codicon-loading'),
+ listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'),
'spinner'
).toBeVisible();
@@ -83,11 +83,11 @@ test('should update trace live', async ({ runUITest, server }) => {
/page.gotohttp:\/\/localhost:\d+\/two.html/
]);
await expect(
- listItem.locator(':scope.selected'),
+ listItem.locator(':scope[aria-selected="true"]'),
'last action to be selected'
).toHaveText(/page.goto/);
await expect(
- listItem.locator(':scope.selected .codicon.codicon-loading'),
+ listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'),
'spinner'
).toBeVisible();
@@ -132,7 +132,7 @@ test('should preserve action list selection upon live trace update', async ({ ru
await page.getByText('live test').dblclick();
// It should wait on the latch.
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -157,7 +157,7 @@ test('should preserve action list selection upon live trace update', async ({ ru
/page.setContent[\d.]+m?s/,
]);
await expect(
- listItem.locator(':scope.selected'),
+ listItem.locator(':scope[aria-selected="true"]'),
'selected action stays the same'
).toHaveText(/page.goto/);
});
@@ -193,7 +193,7 @@ test('should update tracing network live', async ({ runUITest, server }) => {
await page.getByText('live test').dblclick();
// It should wait on the latch.
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -233,7 +233,7 @@ test('should show trace w/ multiple contexts', async ({ runUITest, server, creat
await page.getByText('live test').dblclick();
// It should wait on the latch.
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -278,7 +278,7 @@ test('should show live trace for serial', async ({ runUITest, server, createLatc
await page.getByText('two', { exact: true }).click();
await page.getByTitle('Run all').click();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -318,7 +318,7 @@ test('should show live trace from hooks', async ({ runUITest, createLatch }) =>
`);
await page.getByText('test one').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts
index 5ead1889f0..3673faab45 100644
--- a/tests/playwright-test/ui-mode-test-run.spec.ts
+++ b/tests/playwright-test/ui-mode-test-run.spec.ts
@@ -61,6 +61,26 @@ test('should run visible', async ({ runUITest }) => {
β skipped
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-error] suite"
+ - treeitem "[icon-error] b.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/}
+ - treeitem "[icon-check] c.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem "[icon-circle-slash] skipped"
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
});
@@ -93,13 +113,24 @@ test('should run on hover', async ({ runUITest }) => {
});
await page.getByText('passes').hover();
- await page.getByRole('listitem').filter({ hasText: 'passes' }).getByTitle('Run').click();
+ await page.getByRole('treeitem', { name: 'passes' }).getByRole('button', { name: 'Run' }).click();
await expect.poll(dumpTestTree(page)).toBe(`
βΌ β― a.test.ts
β
passes <=
β― fails
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-circle-outline] fails"
+ `);
});
test('should run on double click', async ({ runUITest }) => {
@@ -118,6 +149,17 @@ test('should run on double click', async ({ runUITest }) => {
β
passes <=
β― fails
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-circle-outline] fails"
+ `);
});
test('should run on Enter', async ({ runUITest }) => {
@@ -137,6 +179,17 @@ test('should run on Enter', async ({ runUITest }) => {
β― passes
β fails <=
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem "[icon-circle-outline] passes"
+ - treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ `);
});
test('should run by project', async ({ runUITest }) => {
@@ -168,6 +221,26 @@ test('should run by project', async ({ runUITest }) => {
β skipped
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-error] suite"
+ - treeitem "[icon-error] b.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/}
+ - treeitem "[icon-check] c.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem "[icon-circle-slash] skipped"
+ `);
+
await page.getByText('Status:').click();
await page.getByLabel('bar').setChecked(true);
@@ -186,6 +259,29 @@ test('should run by project', async ({ runUITest }) => {
βΊ β― skipped
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-circle-outline\] passes/}
+ - treeitem ${/\[icon-error\] fails/}:
+ - group:
+ - treeitem ${/\[icon-error\] foo/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-circle-outline] bar"
+ - treeitem "[icon-error] suite"
+ - treeitem "[icon-error] b.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-circle-outline\] passes/}
+ - treeitem ${/\[icon-error\] fails/}
+ - treeitem "[icon-circle-outline] c.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-circle-outline\] passes/}
+ - treeitem ${/\[icon-circle-outline\] skipped/}
+ `);
+
await page.getByText('Status:').click();
await page.getByTestId('test-tree').getByText('passes').first().click();
@@ -199,6 +295,20 @@ test('should run by project', async ({ runUITest }) => {
βΊ β fails
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - group:
+ - treeitem ${/\[icon-check\] foo \d+ms/}
+ - treeitem ${/\[icon-circle-outline\] bar/}
+ - treeitem ${/\[icon-error\] fails \d+ms/}
+ `);
+
await expect(page.getByText('Projects: foo bar')).toBeVisible();
await page.getByTitle('Run all').click();
@@ -218,6 +328,32 @@ test('should run by project', async ({ runUITest }) => {
βΊ β
passes
βΊ β skipped
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/} [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] foo \d+ms/}
+ - treeitem ${/\[icon-check\] bar \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/} [expanded]:
+ - group:
+ - treeitem ${/\[icon-error\] foo \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem ${/\[icon-error\] bar \d+ms/}
+ - treeitem ${/\[icon-error\] suite/}
+ - treeitem "[icon-error] b.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes/}
+ - treeitem ${/\[icon-error\] fails/}
+ - treeitem "[icon-check] c.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes/}
+ - treeitem ${/\[icon-circle-slash\] skipped/}
+ `);
});
test('should stop', async ({ runUITest }) => {
@@ -244,6 +380,16 @@ test('should stop', async ({ runUITest }) => {
π¦ test 3
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-loading] a.test.ts" [expanded]:
+ - group:
+ - treeitem "[icon-circle-slash] test 0"
+ - treeitem ${/\[icon-check\] test 1 \d+ms/}
+ - treeitem ${/\[icon-loading\] test 2/}
+ - treeitem ${/\[icon-clock\] test 3/}
+ `);
+
await expect(page.getByTitle('Run all')).toBeDisabled();
await expect(page.getByTitle('Stop')).toBeEnabled();
@@ -256,6 +402,16 @@ test('should stop', async ({ runUITest }) => {
β― test 2
β― test 3
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
+ - group:
+ - treeitem "[icon-circle-slash] test 0"
+ - treeitem ${/\[icon-check\] test 1 \d+ms/}
+ - treeitem ${/\[icon-circle-outline\] test 2/}
+ - treeitem ${/\[icon-circle-outline\] test 3/}
+ `);
});
test('should run folder', async ({ runUITest }) => {
@@ -275,7 +431,7 @@ test('should run folder', async ({ runUITest }) => {
});
await page.getByText('folder-b').hover();
- await page.getByRole('listitem').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(`
βΌ β
folder-b <=
@@ -284,6 +440,17 @@ test('should run folder', async ({ runUITest }) => {
βΌ β― in-a.test.ts
β― passes
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] folder-b" [expanded] [selected]:
+ - group:
+ - treeitem "[icon-check] folder-c"
+ - treeitem "[icon-check] in-b.test.ts"
+ - treeitem "[icon-circle-outline] in-a.test.ts" [expanded]:
+ - group:
+ - treeitem "[icon-circle-outline] passes"
+ `);
});
test('should show time', async ({ runUITest }) => {
@@ -307,6 +474,26 @@ test('should show time', async ({ runUITest }) => {
β skipped
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-error] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-error] suite"
+ - treeitem "[icon-error] b.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem ${/\[icon-error\] fails \d+ms/}
+ - treeitem "[icon-check] c.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] passes \d+ms/}
+ - treeitem "[icon-circle-slash] skipped"
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
});
@@ -331,6 +518,13 @@ test('should show test.fail as passing', async ({ runUITest }) => {
β
should fail XXms
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] should fail \d+ms/}
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
});
@@ -360,6 +554,13 @@ test('should ignore repeatEach', async ({ runUITest }) => {
β
should pass
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] should pass \d+ms/}
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
});
@@ -387,6 +588,14 @@ test('should remove output folder before test run', async ({ runUITest }) => {
βΌ β
a.test.ts
β
should pass
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] should pass \d+ms/}
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
await page.getByTitle('Run all').click();
@@ -394,6 +603,14 @@ test('should remove output folder before test run', async ({ runUITest }) => {
βΌ β
a.test.ts
β
should pass
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] should pass \d+ms/}
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
});
@@ -421,8 +638,8 @@ test('should show proper total when using deps', async ({ runUITest }) => {
await page.getByText('Status:').click();
- await page.getByLabel('setup').setChecked(true);
- await page.getByLabel('chromium').setChecked(true);
+ await page.getByRole('checkbox', { name: 'setup' }).setChecked(true);
+ await page.getByRole('checkbox', { name: 'chromium' }).setChecked(true);
await expect.poll(dumpTestTree(page)).toContain(`
βΌ β― a.test.ts
@@ -434,6 +651,18 @@ test('should show proper total when using deps', async ({ runUITest }) => {
β
run @setup <=
β― run @chromium
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ - treeitem "[icon-circle-outline] run @chromium chromium"
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
await page.getByTitle('run @chromium').dblclick();
@@ -442,6 +671,18 @@ test('should show proper total when using deps', async ({ runUITest }) => {
β
run @setup
β
run @chromium <=
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] run @setup setup \d+ms/}
+ - treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]:
+ - button "Run"
+ - button "Show source"
+ - button "Watch"
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)');
});
@@ -501,6 +742,13 @@ test('should respect --tsconfig option', {
β
test
`);
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] test \d+ms/}
+ `);
+
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
});
@@ -522,4 +770,11 @@ test('should respect --ignore-snapshots option', {
βΌ β
a.test.ts
β
snapshot
`);
+
+ await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
+ - tree:
+ - treeitem "[icon-check] a.test.ts" [expanded]:
+ - group:
+ - treeitem ${/\[icon-check\] snapshot \d+ms/}
+ `);
});
diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts
index cd5503427d..65c6aa2533 100644
--- a/tests/playwright-test/ui-mode-test-setup.spec.ts
+++ b/tests/playwright-test/ui-mode-test-setup.spec.ts
@@ -140,9 +140,9 @@ const testsWithSetup = {
test('should run setup and teardown projects (1)', async ({ runUITest }) => {
const { page } = await runUITest(testsWithSetup);
await page.getByText('Status:').click();
- await page.getByLabel('setup').setChecked(false);
- await page.getByLabel('teardown').setChecked(false);
- await page.getByLabel('test').setChecked(false);
+ await page.getByRole('checkbox', { name: 'setup' }).setChecked(false);
+ await page.getByRole('checkbox', { name: 'teardown' }).setChecked(false);
+ await page.getByRole('checkbox', { name: 'test' }).setChecked(false);
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 }) => {
const { page } = await runUITest(testsWithSetup);
await page.getByText('Status:').click();
- await page.getByLabel('setup').setChecked(false);
- await page.getByLabel('teardown').setChecked(true);
- await page.getByLabel('test').setChecked(true);
+ await page.getByRole('checkbox', { name: 'setup' }).setChecked(false);
+ await page.getByRole('checkbox', { name: 'teardown' }).setChecked(true);
+ await page.getByRole('checkbox', { name: 'test' }).setChecked(true);
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 }) => {
const { page } = await runUITest(testsWithSetup);
await page.getByText('Status:').click();
- await page.getByLabel('setup').setChecked(false);
- await page.getByLabel('teardown').setChecked(false);
- await page.getByLabel('test').setChecked(true);
+ await page.getByRole('checkbox', { name: 'setup' }).setChecked(false);
+ await page.getByRole('checkbox', { name: 'teardown' }).setChecked(false);
+ await page.getByRole('checkbox', { name: 'test' }).setChecked(true);
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 }) => {
const { page } = await runUITest(testsWithSetup);
await page.getByText('Status:').click();
- await page.getByLabel('setup').setChecked(true);
- await page.getByLabel('teardown').setChecked(true);
- await page.getByLabel('test').setChecked(true);
+ await page.getByRole('checkbox', { name: 'setup' }).setChecked(true);
+ await page.getByRole('checkbox', { name: 'teardown' }).setChecked(true);
+ await page.getByRole('checkbox', { name: 'test' }).setChecked(true);
await page.getByText('setup.ts').hover();
- await page.getByRole('listitem').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(`
βΌ β
setup.ts <=
diff --git a/tests/playwright-test/ui-mode-test-update.spec.ts b/tests/playwright-test/ui-mode-test-update.spec.ts
index 61e2c89dc7..67d0626a5f 100644
--- a/tests/playwright-test/ui-mode-test-update.spec.ts
+++ b/tests/playwright-test/ui-mode-test-update.spec.ts
@@ -149,7 +149,7 @@ test('should not loose run information after execution if test wrote into testDi
await page.getByTitle('Run all').click();
await page.waitForTimeout(5_000);
await expect(page.getByText('Did not run')).toBeHidden();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -215,7 +215,7 @@ test('should update test locations', async ({ runUITest, writeFiles }) => {
const messages: any[] = [];
await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg));
- const passesItemLocator = page.getByRole('listitem').filter({ hasText: 'passes' });
+ const passesItemLocator = page.getByRole('treeitem', { name: 'passes' });
await passesItemLocator.hover();
await passesItemLocator.getByTitle('Show source').click();
await page.getByTitle('Open in VS Code').click();
diff --git a/tests/playwright-test/ui-mode-test-watch.spec.ts b/tests/playwright-test/ui-mode-test-watch.spec.ts
index bd04750a1f..9bbbdc0ec0 100644
--- a/tests/playwright-test/ui-mode-test-watch.spec.ts
+++ b/tests/playwright-test/ui-mode-test-watch.spec.ts
@@ -28,14 +28,14 @@ test('should watch files', async ({ runUITest, writeFiles }) => {
});
await page.getByText('fails').click();
- await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Watch').click();
+ await page.getByRole('treeitem', { name: 'fails' }).getByRole('button', { name: 'Watch' }).click();
await expect.poll(dumpTestTree(page)).toBe(`
βΌ β― a.test.ts
β― passes
β― fails π <=
`);
- await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Run').click();
+ await page.getByRole('treeitem', { name: 'fails' }).getByRole('button', { name: 'Run' }).click();
await expect.poll(dumpTestTree(page)).toBe(`
βΌ β a.test.ts
@@ -75,7 +75,7 @@ test('should watch e2e deps', async ({ runUITest, writeFiles }) => {
});
await page.getByText('answer').click();
- await page.getByRole('listitem').filter({ hasText: 'answer' }).getByTitle('Watch').click();
+ await page.getByRole('treeitem', { name: 'answer' }).getByRole('button', { name: 'Watch' }).click();
await expect.poll(dumpTestTree(page)).toBe(`
βΌ β― a.test.ts
β― answer π <=
@@ -102,13 +102,13 @@ test('should batch watch updates', async ({ runUITest, writeFiles }) => {
});
await page.getByText('a.test.ts').click();
- await page.getByRole('listitem').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.getByRole('listitem').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.getByRole('listitem').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.getByRole('listitem').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(`
βΌ β― 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.getByRole('listitem').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(`
βΌ β― a.test.ts π <=
diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts
index 9f0749893e..def44e9aeb 100644
--- a/tests/playwright-test/ui-mode-trace.spec.ts
+++ b/tests/playwright-test/ui-mode-trace.spec.ts
@@ -34,7 +34,7 @@ test('should merge trace events', async ({ runUITest }) => {
await page.getByText('trace test').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -61,7 +61,7 @@ test('should merge web assertion events', async ({ runUITest }, testInfo) => {
await page.getByText('trace test').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -86,7 +86,7 @@ test('should merge screenshot assertions', async ({ runUITest }, testInfo) => {
await page.getByText('trace test').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -134,7 +134,7 @@ test('should show snapshots for sync assertions', async ({ runUITest }) => {
await page.getByText('trace test').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
'action list'
@@ -214,7 +214,7 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => {
});
await page.getByText('pass').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
@@ -241,7 +241,7 @@ test('should not show caught errors in the errors tab', async ({ runUITest }, te
});
await page.getByText('pass').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
@@ -272,7 +272,7 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => {
});
await page.getByText('pass').dblclick();
- const listItem = page.getByTestId('actions-tree').getByRole('listitem');
+ const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
await expect(
listItem,
diff --git a/utils/docker/Dockerfile.focal b/utils/docker/Dockerfile.focal
deleted file mode 100644
index cd1d1d7c6e..0000000000
--- a/utils/docker/Dockerfile.focal
+++ /dev/null
@@ -1,51 +0,0 @@
-FROM ubuntu:focal
-
-ARG DEBIAN_FRONTEND=noninteractive
-ARG TZ=America/Los_Angeles
-ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-focal"
-
-ENV LANG=C.UTF-8
-ENV LC_ALL=C.UTF-8
-
-# === INSTALL Node.js ===
-
-RUN apt-get update && \
- # Install Node.js
- apt-get install -y curl wget gpg ca-certificates && \
- mkdir -p /etc/apt/keyrings && \
- curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
- echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \
- apt-get update && \
- apt-get install -y nodejs && \
- # Feature-parity with node.js base images.
- apt-get install -y --no-install-recommends git openssh-client && \
- npm install -g yarn && \
- # clean apt cache
- rm -rf /var/lib/apt/lists/* && \
- # Create the pwuser
- adduser pwuser
-
-# === BAKE BROWSERS INTO IMAGE ===
-
-ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
-
-# 1. Add tip-of-tree Playwright package to install its browsers.
-# The package should be built beforehand from tip-of-tree Playwright.
-COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz
-
-# 2. Bake in Playwright Agent.
-# Playwright Agent is used to bake in browsers and browser dependencies,
-# and run docker server later on.
-# Browsers will be downloaded in `/ms-playwright`.
-# Note: make sure to set 777 to the registry so that any user can access
-# registry.
-RUN mkdir /ms-playwright && \
- mkdir /ms-playwright-agent && \
- cd /ms-playwright-agent && npm init -y && \
- npm i /tmp/playwright-core.tar.gz && \
- npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \
- npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \
- rm /tmp/playwright-core.tar.gz && \
- rm -rf /ms-playwright-agent && \
- rm -rf ~/.npm/ && \
- chmod -R 777 /ms-playwright
diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble
index 7236acbbfc..29ca98c4e4 100644
--- a/utils/docker/Dockerfile.noble
+++ b/utils/docker/Dockerfile.noble
@@ -2,7 +2,7 @@ FROM ubuntu:noble
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=America/Los_Angeles
-ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-jammy"
+ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-noble"
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
diff --git a/utils/docker/build.sh b/utils/docker/build.sh
index 46e8d9e6b6..280727eac5 100755
--- a/utils/docker/build.sh
+++ b/utils/docker/build.sh
@@ -3,12 +3,12 @@ set -e
set +x
if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then
- echo "usage: $(basename $0) {--arm64,--amd64} {focal,jammy} playwright:localbuild-focal"
+ echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble} playwright:localbuild-noble"
echo
- echo "Build Playwright docker image and tag it as 'playwright:localbuild-focal'."
+ echo "Build Playwright docker image and tag it as 'playwright:localbuild-noble'."
echo "Once image is built, you can run it with"
echo ""
- echo " docker run --rm -it playwright:localbuild-focal /bin/bash"
+ echo " docker run --rm -it playwright:localbuild-noble /bin/bash"
echo ""
echo "NOTE: this requires on Playwright dependencies to be installed with 'npm install'"
echo " and Playwright itself being built with 'npm run build'"
diff --git a/utils/docker/publish_docker.sh b/utils/docker/publish_docker.sh
index a4892024a8..870da29a90 100755
--- a/utils/docker/publish_docker.sh
+++ b/utils/docker/publish_docker.sh
@@ -21,11 +21,6 @@ else
exit 1
fi
-# Ubuntu 20.04
-FOCAL_TAGS=(
- "v${PW_VERSION}-focal"
-)
-
# Ubuntu 22.04
JAMMY_TAGS=(
"v${PW_VERSION}-jammy"
@@ -69,14 +64,12 @@ install_oras_if_needed() {
publish_docker_images_with_arch_suffix() {
local FLAVOR="$1"
local TAGS=()
- if [[ "$FLAVOR" == "focal" ]]; then
- TAGS=("${FOCAL_TAGS[@]}")
- elif [[ "$FLAVOR" == "jammy" ]]; then
+ if [[ "$FLAVOR" == "jammy" ]]; then
TAGS=("${JAMMY_TAGS[@]}")
elif [[ "$FLAVOR" == "noble" ]]; then
TAGS=("${NOBLE_TAGS[@]}")
else
- echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'"
+ echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'"
exit 1
fi
local ARCH="$2"
@@ -97,14 +90,12 @@ publish_docker_images_with_arch_suffix() {
publish_docker_manifest () {
local FLAVOR="$1"
local TAGS=()
- if [[ "$FLAVOR" == "focal" ]]; then
- TAGS=("${FOCAL_TAGS[@]}")
- elif [[ "$FLAVOR" == "jammy" ]]; then
+ if [[ "$FLAVOR" == "jammy" ]]; then
TAGS=("${JAMMY_TAGS[@]}")
elif [[ "$FLAVOR" == "noble" ]]; then
TAGS=("${NOBLE_TAGS[@]}")
else
- echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'"
+ echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'"
exit 1
fi
@@ -123,11 +114,6 @@ publish_docker_manifest () {
done
}
-# Ubuntu 20.04
-publish_docker_images_with_arch_suffix focal amd64
-publish_docker_images_with_arch_suffix focal arm64
-publish_docker_manifest focal amd64 arm64
-
# Ubuntu 22.04
publish_docker_images_with_arch_suffix jammy amd64
publish_docker_images_with_arch_suffix jammy arm64
diff --git a/utils/generate_clip_paths.js b/utils/generate_clip_paths.js
index cbef6ece9d..83d26a905c 100644
--- a/utils/generate_clip_paths.js
+++ b/utils/generate_clip_paths.js
@@ -64,6 +64,7 @@ const iconNames = [
'check',
'close',
'pass',
+ 'gist',
];
(async () => {
diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js
index ae988ac32c..b3ad1c4f23 100644
--- a/utils/generate_types/index.js
+++ b/utils/generate_types/index.js
@@ -93,25 +93,8 @@ class TypesGenerator {
handledClasses.add(className);
return this.writeComment(docClass.comment, '') + '\n';
}, (className, methodName, overloadIndex) => {
- if (className === 'SuiteFunction' && methodName === '__call') {
- const cls = this.documentation.classes.get('Test');
- if (!cls)
- throw new Error(`Unknown class "Test"`);
- const method = cls.membersArray.find(m => m.alias === 'describe');
- if (!method)
- throw new Error(`Unknown method "Test.describe"`);
- return this.memberJSDOC(method, ' ').trimLeft();
- }
- if (className === 'TestFunction' && methodName === '__call') {
- const cls = this.documentation.classes.get('Test');
- if (!cls)
- throw new Error(`Unknown class "Test"`);
- const method = cls.membersArray.find(m => m.alias === '(call)');
- if (!method)
- throw new Error(`Unknown method "Test.(call)"`);
- return this.memberJSDOC(method, ' ').trimLeft();
- }
-
+ if (methodName === '__call')
+ methodName = '(call)';
const docClass = this.docClassForName(className);
let method;
if (docClass) {
@@ -591,8 +574,6 @@ class TypesGenerator {
'PlaywrightWorkerArgs.playwright',
'PlaywrightWorkerOptions.defaultBrowserType',
'Project',
- 'SuiteFunction',
- 'TestFunction',
]),
doNotExportClassNames: assertionClasses,
});
diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts
index be1fa7ee37..ff46ba0e5c 100644
--- a/utils/generate_types/overrides-test.d.ts
+++ b/utils/generate_types/overrides-test.d.ts
@@ -75,49 +75,83 @@ export type TestDetails = {
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
}
-interface SuiteFunction {
- (title: string, callback: () => void): void;
- (callback: () => void): void;
- (title: string, details: TestDetails, callback: () => void): void;
-}
+type TestBody = (args: TestArgs, testInfo: TestInfo) => Promise | void;
+type ConditionBody = (args: TestArgs) => boolean;
-interface TestFunction {
- (title: string, body: (args: TestArgs, testInfo: TestInfo) => Promise | void): void;
- (title: string, details: TestDetails, body: (args: TestArgs, testInfo: TestInfo) => Promise | void): void;
-}
+export interface TestType {
+ (title: string, body: TestBody): void;
+ (title: string, details: TestDetails, body: TestBody): void;
-export interface TestType extends TestFunction {
- only: TestFunction;
- describe: SuiteFunction & {
- only: SuiteFunction;
- skip: SuiteFunction;
- fixme: SuiteFunction;
- serial: SuiteFunction & {
- only: SuiteFunction;
+ only(title: string, body: TestBody): void;
+ only(title: string, details: TestDetails, body: TestBody): void;
+
+ describe: {
+ (title: string, callback: () => void): void;
+ (callback: () => void): void;
+ (title: string, details: TestDetails, callback: () => void): void;
+
+ only(title: string, callback: () => void): void;
+ only(callback: () => void): void;
+ only(title: string, details: TestDetails, callback: () => void): void;
+
+ skip(title: string, callback: () => void): void;
+ skip(callback: () => void): void;
+ skip(title: string, details: TestDetails, callback: () => void): void;
+
+ fixme(title: string, callback: () => void): void;
+ fixme(callback: () => void): void;
+ fixme(title: string, details: TestDetails, callback: () => void): void;
+
+ serial: {
+ (title: string, callback: () => void): void;
+ (callback: () => void): void;
+ (title: string, details: TestDetails, callback: () => void): void;
+
+ only(title: string, callback: () => void): void;
+ only(callback: () => void): void;
+ only(title: string, details: TestDetails, callback: () => void): void;
};
- parallel: SuiteFunction & {
- only: SuiteFunction;
+
+ parallel: {
+ (title: string, callback: () => void): void;
+ (callback: () => void): void;
+ (title: string, details: TestDetails, callback: () => void): void;
+
+ only(title: string, callback: () => void): void;
+ only(callback: () => void): void;
+ only(title: string, details: TestDetails, callback: () => void): void;
};
+
configure: (options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) => void;
};
- skip(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void;
- skip(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void;
+
+ skip(title: string, body: TestBody): void;
+ skip(title: string, details: TestDetails, body: TestBody): void;
skip(): void;
skip(condition: boolean, description?: string): void;
- skip(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
- fixme(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void;
- fixme(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void;
+ skip(callback: ConditionBody, description?: string): void;
+
+ fixme(title: string, body: TestBody): void;
+ fixme(title: string, details: TestDetails, body: TestBody): void;
fixme(): void;
fixme(condition: boolean, description?: string): void;
- fixme(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
- fail(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void;
- fail(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | void): void;
- fail(condition: boolean, description?: string): void;
- fail(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
- fail(): void;
+ fixme(callback: ConditionBody, description?: string): void;
+
+ fail: {
+ (title: string, body: TestBody): void;
+ (title: string, details: TestDetails, body: TestBody): void;
+ (condition: boolean, description?: string): void;
+ (callback: ConditionBody, description?: string): void;
+ (): void;
+
+ only(title: string, body: TestBody): void;
+ only(title: string, details: TestDetails, body: TestBody): void;
+ }
+
slow(): void;
slow(condition: boolean, description?: string): void;
- slow(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
+ slow(callback: ConditionBody, description?: string): void;
+
setTimeout(timeout: number): void;
beforeEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void;
beforeEach(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void;
diff --git a/utils/generate_types/parseOverrides.js b/utils/generate_types/parseOverrides.js
index ad80ea388f..bb96013842 100644
--- a/utils/generate_types/parseOverrides.js
+++ b/utils/generate_types/parseOverrides.js
@@ -101,9 +101,9 @@ async function parseOverrides(filePath, commentForClass, commentForMethod, extra
* @param {ts.Node} node
*/
function visitProperties(className, prefix, node) {
- // This function supports structs like "a: { b: string; c: number, (): void }"
- // and inserts comments for "a.b", "a.c", a.
- if (ts.isPropertySignature(node)) {
+ // This function supports structs like "a: { b: string; c: number, (): void, d(): void }"
+ // and inserts comments for "a.b", "a.c", "a", "a.d".
+ if (ts.isPropertySignature(node) || ts.isMethodSignature(node)) {
const name = checker.getSymbolAtLocation(node.name).getName();
const pos = node.getStart(file, false);
replacers.push({