@theia/playwright 1.23.0-next.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+
19
+ import { TheiaApp } from './theia-app';
20
+ import { TheiaEditor } from './theia-editor';
21
+ import { normalizeId } from './util';
22
+
23
+ export class TheiaTextEditor extends TheiaEditor {
24
+
25
+ constructor(filePath: string, app: TheiaApp) {
26
+ super({
27
+ tabSelector: normalizeId(`#shell-tab-code-editor-opener:file://${app.workspace.escapedPath}/${filePath}:1`),
28
+ viewSelector: normalizeId(`#code-editor-opener:file://${app.workspace.escapedPath}/${filePath}:1`) + '.theia-editor'
29
+ }, app);
30
+ }
31
+
32
+ async numberOfLines(): Promise<number | undefined> {
33
+ await this.activate();
34
+ const viewElement = await this.viewElement();
35
+ const lineElements = await viewElement?.$$('.view-lines .view-line');
36
+ return lineElements?.length;
37
+ }
38
+
39
+ async textContentOfLineByLineNumber(lineNumber: number): Promise<string | undefined> {
40
+ const lineElement = await this.lineByLineNumber(lineNumber);
41
+ const content = await lineElement?.textContent();
42
+ return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
43
+ }
44
+
45
+ async replaceLineWithLineNumber(text: string, lineNumber: number): Promise<void> {
46
+ await this.selectLineWithLineNumber(lineNumber);
47
+ await this.typeTextAndHitEnter(text);
48
+ }
49
+
50
+ protected async typeTextAndHitEnter(text: string): Promise<void> {
51
+ await this.page.keyboard.type(text);
52
+ await this.page.keyboard.press('Enter');
53
+ }
54
+
55
+ async selectLineWithLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
56
+ await this.activate();
57
+ const lineElement = await this.lineByLineNumber(lineNumber);
58
+ await this.selectLine(lineElement);
59
+ return lineElement;
60
+ }
61
+
62
+ async placeCursorInLineWithLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
63
+ await this.activate();
64
+ const lineElement = await this.lineByLineNumber(lineNumber);
65
+ await this.placeCursorInLine(lineElement);
66
+ return lineElement;
67
+ }
68
+
69
+ async deleteLineByLineNumber(lineNumber: number): Promise<void> {
70
+ await this.selectLineWithLineNumber(lineNumber);
71
+ await this.page.keyboard.press('Backspace');
72
+ }
73
+
74
+ protected async lineByLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
75
+ await this.activate();
76
+ const viewElement = await this.viewElement();
77
+ const lines = await viewElement?.$$('.view-lines .view-line');
78
+ if (!lines) {
79
+ throw new Error(`Couldn't retrieve lines of text editor ${this.tabSelector}`);
80
+ }
81
+
82
+ const linesWithXCoordinates = [];
83
+ for (const lineElement of lines) {
84
+ const box = await lineElement.boundingBox();
85
+ linesWithXCoordinates.push({ x: box ? box.x : Number.MAX_VALUE, lineElement });
86
+ }
87
+ linesWithXCoordinates.sort((a, b) => a.x.toString().localeCompare(b.x.toString()));
88
+ return linesWithXCoordinates[lineNumber - 1].lineElement;
89
+ }
90
+
91
+ async textContentOfLineContainingText(text: string): Promise<string | undefined> {
92
+ await this.activate();
93
+ const lineElement = await this.lineContainingText(text);
94
+ const content = await lineElement?.textContent();
95
+ return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
96
+ }
97
+
98
+ async replaceLineContainingText(newText: string, oldText: string): Promise<void> {
99
+ await this.selectLineContainingText(oldText);
100
+ await this.typeTextAndHitEnter(newText);
101
+ }
102
+
103
+ async selectLineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
104
+ await this.activate();
105
+ const lineElement = await this.lineContainingText(text);
106
+ await this.selectLine(lineElement);
107
+ return lineElement;
108
+ }
109
+
110
+ async placeCursorInLineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
111
+ await this.activate();
112
+ const lineElement = await this.lineContainingText(text);
113
+ await this.placeCursorInLine(lineElement);
114
+ return lineElement;
115
+ }
116
+
117
+ async deleteLineContainingText(text: string): Promise<void> {
118
+ await this.selectLineContainingText(text);
119
+ await this.page.keyboard.press('Backspace');
120
+ }
121
+
122
+ async addTextToNewLineAfterLineContainingText(textContainedByExistingLine: string, newText: string): Promise<void> {
123
+ const existingLine = await this.lineContainingText(textContainedByExistingLine);
124
+ await this.placeCursorInLine(existingLine);
125
+ await this.page.keyboard.press('End');
126
+ await this.page.keyboard.press('Enter');
127
+ await this.page.keyboard.type(newText);
128
+ }
129
+
130
+ async addTextToNewLineAfterLineByLineNumber(lineNumber: number, newText: string): Promise<void> {
131
+ const existingLine = await this.lineByLineNumber(lineNumber);
132
+ await this.placeCursorInLine(existingLine);
133
+ await this.page.keyboard.press('End');
134
+ await this.page.keyboard.press('Enter');
135
+ await this.page.keyboard.type(newText);
136
+ }
137
+
138
+ protected async lineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
139
+ const viewElement = await this.viewElement();
140
+ return viewElement?.waitForSelector(`.view-lines .view-line:has-text("${text}")`);
141
+ }
142
+
143
+ protected async selectLine(lineElement: ElementHandle<SVGElement | HTMLElement> | undefined): Promise<void> {
144
+ await lineElement?.click({ clickCount: 3 });
145
+ }
146
+
147
+ protected async placeCursorInLine(lineElement: ElementHandle<SVGElement | HTMLElement> | undefined): Promise<void> {
148
+ await lineElement?.click();
149
+ }
150
+
151
+ protected replaceEditorSymbolsWithSpace(content: string): string | Promise<string | undefined> {
152
+ // [ ] &nbsp; => \u00a0
153
+ // [·] &middot; => \u00b7
154
+ return content.replace(/[\u00a0\u00b7]/g, ' ');
155
+ }
156
+
157
+ protected async selectedSuggestion(): Promise<ElementHandle<SVGElement | HTMLElement>> {
158
+ return this.page.waitForSelector(this.viewSelector + ' .monaco-list-row.show-file-icons.focused');
159
+ }
160
+
161
+ async getSelectedSuggestionText(): Promise<string> {
162
+ const suggestion = await this.selectedSuggestion();
163
+ const text = await suggestion.textContent();
164
+ if (text === null) { throw new Error('Text content could not be found'); }
165
+ return text;
166
+ }
167
+
168
+ }
@@ -0,0 +1,25 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { TheiaStatusIndicator } from './theia-status-indicator';
18
+
19
+ const TOGGLE_BOTTOM_ICON = 'codicon-window';
20
+
21
+ export class TheiaToggleBottomIndicator extends TheiaStatusIndicator {
22
+ async isVisible(): Promise<boolean> {
23
+ return super.isVisible(TOGGLE_BOTTOM_ICON);
24
+ }
25
+ }
@@ -0,0 +1,60 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+
19
+ import { TheiaApp } from './theia-app';
20
+ import { TheiaContextMenu } from './theia-context-menu';
21
+ import { TheiaMenu } from './theia-menu';
22
+
23
+ export class TheiaTreeNode {
24
+
25
+ labelElementCssClass = '.theia-TreeNodeSegmentGrow';
26
+ expansionToggleCssClass = '.theia-ExpansionToggle';
27
+ collapsedCssClass = '.theia-mod-collapsed';
28
+
29
+ constructor(protected elementHandle: ElementHandle<SVGElement | HTMLElement>, protected app: TheiaApp) { }
30
+
31
+ async label(): Promise<string | null> {
32
+ const labelNode = await this.elementHandle.$(this.labelElementCssClass);
33
+ if (!labelNode) {
34
+ throw new Error('Cannot read label of ' + this.elementHandle);
35
+ }
36
+ return labelNode.textContent();
37
+ }
38
+
39
+ async isCollapsed(): Promise<boolean> {
40
+ return !! await this.elementHandle.$(this.collapsedCssClass);
41
+ }
42
+
43
+ async isExpandable(): Promise<boolean> {
44
+ return !! await this.elementHandle.$(this.expansionToggleCssClass);
45
+ }
46
+
47
+ async expand(): Promise<void> {
48
+ if (! await this.isCollapsed()) {
49
+ return;
50
+ }
51
+ const expansionToggle = await this.elementHandle.waitForSelector(this.expansionToggleCssClass);
52
+ await expansionToggle.click();
53
+ await this.elementHandle.waitForSelector(`${this.expansionToggleCssClass}:not(${this.collapsedCssClass})`);
54
+ }
55
+
56
+ async openContextMenu(): Promise<TheiaMenu> {
57
+ return TheiaContextMenu.open(this.app, () => this.elementHandle.waitForSelector(this.labelElementCssClass));
58
+ }
59
+
60
+ }
@@ -0,0 +1,177 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+
19
+ import { TheiaApp } from './theia-app';
20
+ import { TheiaContextMenu } from './theia-context-menu';
21
+ import { TheiaMenu } from './theia-menu';
22
+ import { TheiaPageObject } from './theia-page-object';
23
+ import { containsClass, isElementVisible, textContent } from './util';
24
+
25
+ export interface TheiaViewData {
26
+ tabSelector: string;
27
+ viewSelector: string;
28
+ viewName?: string;
29
+ }
30
+
31
+ export class TheiaView extends TheiaPageObject {
32
+
33
+ constructor(protected readonly data: TheiaViewData, app: TheiaApp) {
34
+ super(app);
35
+ }
36
+
37
+ get tabSelector(): string {
38
+ return this.data.tabSelector;
39
+ }
40
+
41
+ get viewSelector(): string {
42
+ return this.data.viewSelector;
43
+ }
44
+
45
+ get name(): string | undefined {
46
+ return this.data.viewName;
47
+ }
48
+
49
+ async open(): Promise<TheiaView> {
50
+ if (!this.data.viewName) {
51
+ throw new Error('View name must be specified to open via command palette');
52
+ }
53
+ await this.app.quickCommandPalette.trigger('View: Open View...', this.data.viewName);
54
+ await this.waitForVisible();
55
+ return this;
56
+ }
57
+
58
+ async focus(): Promise<void> {
59
+ await this.activate();
60
+ const view = await this.viewElement();
61
+ await view?.click();
62
+ }
63
+
64
+ async activate(): Promise<void> {
65
+ await this.page.waitForSelector(this.tabSelector, { state: 'visible' });
66
+ if (!await this.isActive()) {
67
+ const tab = await this.tabElement();
68
+ await tab?.click();
69
+ }
70
+ return this.waitForVisible();
71
+ }
72
+
73
+ async waitForVisible(): Promise<void> {
74
+ await this.page.waitForSelector(this.viewSelector, { state: 'visible' });
75
+ }
76
+
77
+ async isTabVisible(): Promise<boolean> {
78
+ return isElementVisible(this.tabElement());
79
+ }
80
+
81
+ async isDisplayed(): Promise<boolean> {
82
+ return isElementVisible(this.viewElement());
83
+ }
84
+
85
+ async isActive(): Promise<boolean> {
86
+ return await this.isTabVisible() && containsClass(this.tabElement(), 'p-mod-current');
87
+ }
88
+
89
+ async isClosable(): Promise<boolean> {
90
+ return await this.isTabVisible() && containsClass(this.tabElement(), 'p-mod-closable');
91
+ }
92
+
93
+ async close(waitForClosed = true): Promise<void> {
94
+ if (!(await this.isTabVisible())) {
95
+ return;
96
+ }
97
+ if (!(await this.isClosable())) {
98
+ throw Error(`View ${this.tabSelector} is not closable`);
99
+ }
100
+ const tab = await this.tabElement();
101
+ const side = await this.side();
102
+ if (side === 'main' || side === 'bottom') {
103
+ const closeIcon = await tab?.waitForSelector('div.p-TabBar-tabCloseIcon');
104
+ await closeIcon?.click();
105
+ } else {
106
+ const menu = await this.openContextMenuOnTab();
107
+ const closeItem = await menu.menuItemByName('Close');
108
+ await closeItem?.click();
109
+ }
110
+ if (waitForClosed) {
111
+ await this.waitUntilClosed();
112
+ }
113
+ }
114
+
115
+ protected async waitUntilClosed(): Promise<void> {
116
+ await this.page.waitForSelector(this.tabSelector, { state: 'detached' });
117
+ }
118
+
119
+ async title(): Promise<string | undefined> {
120
+ if ((await this.isInSidePanel()) && !(await this.isActive())) {
121
+ // we can only determine the label of a side-panel view, if it is active
122
+ await this.activate();
123
+ }
124
+ switch (await this.side()) {
125
+ case 'left':
126
+ return textContent(this.page.waitForSelector('div.theia-left-side-panel > div.theia-sidepanel-title'));
127
+ case 'right':
128
+ return textContent(this.page.waitForSelector('div.theia-right-side-panel > div.theia-sidepanel-title'));
129
+ }
130
+ const tab = await this.tabElement();
131
+ if (tab) {
132
+ return textContent(tab.waitForSelector('div.theia-tab-icon-label > div.p-TabBar-tabLabel'));
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ async isInSidePanel(): Promise<boolean> {
138
+ return (await this.side() === 'left') || (await this.side() === 'right');
139
+ }
140
+
141
+ async side(): Promise<'left' | 'right' | 'bottom' | 'main'> {
142
+ if (!await this.isTabVisible()) {
143
+ throw Error(`Unable to determine side of invisible view tab '${this.tabSelector}'`);
144
+ }
145
+ const tab = await this.tabElement();
146
+ let appAreaElement = tab?.$('xpath=../..');
147
+ if (await containsClass(appAreaElement, 'theia-app-left')) {
148
+ return 'left';
149
+ }
150
+ if (await containsClass(appAreaElement, 'theia-app-right')) {
151
+ return 'right';
152
+ }
153
+
154
+ appAreaElement = (await appAreaElement)?.$('xpath=../..');
155
+ if (await containsClass(appAreaElement, 'theia-app-bottom')) {
156
+ return 'bottom';
157
+ }
158
+ if (await containsClass(appAreaElement, 'theia-app-main')) {
159
+ return 'main';
160
+ }
161
+ throw Error(`Unable to determine side of view tab '${this.tabSelector}'`);
162
+ }
163
+
164
+ async openContextMenuOnTab(): Promise<TheiaMenu> {
165
+ await this.activate();
166
+ return TheiaContextMenu.open(this.app, () => this.page.waitForSelector(this.tabSelector));
167
+ }
168
+
169
+ protected viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
170
+ return this.page.$(this.viewSelector);
171
+ }
172
+
173
+ protected tabElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
174
+ return this.page.$(this.tabSelector);
175
+ }
176
+
177
+ }
@@ -0,0 +1,65 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import * as fs from 'fs-extra';
18
+ import { tmpdir } from 'os';
19
+ import { sep } from 'path';
20
+ import * as path from 'path';
21
+
22
+ export class TheiaWorkspace {
23
+
24
+ protected workspacePath: string;
25
+
26
+ /**
27
+ * Creates a Theia workspace location with the specified path to files that shall be copied to this workspace.
28
+ * The `pathOfFilesToInitialize` must be relative to cwd of the node process.
29
+ *
30
+ * @param {string[]} pathOfFilesToInitialize Path to files or folders that shall be copied to the workspace
31
+ */
32
+ constructor(protected pathOfFilesToInitialize?: string[]) {
33
+ this.workspacePath = fs.mkdtempSync(`${tmpdir}${sep}cloud-ws-`);
34
+ }
35
+
36
+ /** Performs the file system operations preparing the workspace location synchronously. */
37
+ initialize(): void {
38
+ if (this.pathOfFilesToInitialize) {
39
+ for (const initPath of this.pathOfFilesToInitialize) {
40
+ const absoluteInitPath = path.resolve(process.cwd(), initPath);
41
+ if (!fs.pathExistsSync(absoluteInitPath)) {
42
+ throw Error('Workspace does not exist at ' + absoluteInitPath);
43
+ }
44
+ fs.copySync(absoluteInitPath, this.workspacePath);
45
+ }
46
+ }
47
+ }
48
+
49
+ get path(): string {
50
+ return this.workspacePath;
51
+ }
52
+
53
+ get urlEncodedPath(): string {
54
+ return this.path.replace(/[\\]/g, '/');
55
+ }
56
+
57
+ get escapedPath(): string {
58
+ return this.path.replace(/:/g, '%3A').replace(/[\\]/g, '%5C');
59
+ }
60
+
61
+ clear(): void {
62
+ fs.emptyDirSync(this.workspacePath);
63
+ }
64
+
65
+ }
package/src/util.ts ADDED
@@ -0,0 +1,72 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+
19
+ export const USER_KEY_TYPING_DELAY = 80;
20
+
21
+ export function normalizeId(nodeId: string): string {
22
+ // Special characters (i.e. in our case '.',':','/','%', and '\\') in CSS IDs have to be escaped
23
+ return nodeId.replace(/[.:,%/\\]/g, matchedChar => '\\' + matchedChar);
24
+ }
25
+
26
+ export async function toTextContentArray(items: ElementHandle<SVGElement | HTMLElement>[]): Promise<string[]> {
27
+ const contents = items.map(item => item.textContent());
28
+ const resolvedContents = await Promise.all(contents);
29
+ return resolvedContents.filter(text => text !== undefined) as string[];
30
+ }
31
+
32
+ export function isDefined(content: string | undefined): content is string {
33
+ return content !== undefined;
34
+ }
35
+
36
+ export function isNotNull(content: string | null): content is string {
37
+ return content !== null;
38
+ }
39
+
40
+ export async function textContent(elementPromise: Promise<ElementHandle<SVGElement | HTMLElement> | null>): Promise<string | undefined> {
41
+ const element = await elementPromise;
42
+ if (!element) {
43
+ return undefined;
44
+ }
45
+ const content = await element.textContent();
46
+ return content ? content : undefined;
47
+ }
48
+
49
+ export async function containsClass(elementPromise: Promise<ElementHandle<SVGElement | HTMLElement> | null> | undefined, cssClass: string): Promise<boolean> {
50
+ return elementContainsClass(await elementPromise, cssClass);
51
+ }
52
+
53
+ export async function elementContainsClass(element: ElementHandle<SVGElement | HTMLElement> | null | undefined, cssClass: string): Promise<boolean> {
54
+ if (element) {
55
+ const classValue = await element.getAttribute('class');
56
+ if (classValue) {
57
+ return classValue?.split(' ').includes(cssClass);
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+
63
+ export async function isElementVisible(elementPromise: Promise<ElementHandle<SVGElement | HTMLElement> | null>): Promise<boolean> {
64
+ const element = await elementPromise;
65
+ return element ? element.isVisible() : false;
66
+ }
67
+
68
+ export async function elementId(element: ElementHandle<SVGElement | HTMLElement>): Promise<string> {
69
+ const id = await element.getAttribute('id');
70
+ if (id === null) { throw new Error('Could not get ID of ' + element); }
71
+ return id;
72
+ }