2026-06-24 20:31:05 +05:00

343 lines
12 KiB
TypeScript

import {ElementHandle, Page, expect} from '@playwright/test';
import dotenv from 'dotenv';
import moment from 'moment';
import path from 'path';
import {
ActionPanelQA,
ControlQA,
DialogCreateWorkbookEntryQa,
DlNavigationQA,
EntryDialogQA,
SelectQa,
} from '../../src/shared';
import {ActionPanelEntryContextMenuQa} from '../../src/shared/constants/qa/action-panel';
export * from './helpers';
export const ROOT_ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
dotenv.config({path: ROOT_ENV_PATH});
type SlctMode = 'exact' | 'contains' | 'startsWith' | 'endsWith';
type SlctOptions = {
mode?: SlctMode;
text?: string;
};
const dataQaSelectorByMode: Record<SlctMode, (nodeAnchorKey: string) => string> = {
exact: (nodeAnchorKey) => `[data-qa="${nodeAnchorKey}"]`,
contains: (nodeAnchorKey) => `[data-qa*="${nodeAnchorKey}"]`,
startsWith: (nodeAnchorKey) => `[data-qa^="${nodeAnchorKey}"]`,
endsWith: (nodeAnchorKey) => `[data-qa$="${nodeAnchorKey}"]`,
};
/**
* Builds a Playwright selector for elements with the `data-qa` attribute.
*
* Supports:
* - `slct('node-key')` -> exact `data-qa` match
* - `slct('node-key', 'bar')` -> exact `data-qa` + text match (backward-compatible API)
* - `slct('node-key', {mode: 'contains' | 'startsWith' | 'endsWith' | 'exact', text?: 'bar'})`
*
* @example
* slct('node-key') // [data-qa="node-key"]
* slct('node-key', 'bar') // [data-qa="node-key"] >> text="bar"
* slct('node', {mode: 'contains'}) // [data-qa*="node"]
*
* @param nodeAnchorKey Node key used to build the `data-qa` selector.
* @param textOrOptions Optional text filter or selector options.
* @returns Playwright selector string.
*/
export const slct = (nodeAnchorKey: string, textOrOptions?: string | SlctOptions) => {
const options = typeof textOrOptions === 'string' ? {text: textOrOptions} : textOrOptions;
const mode = options?.mode ?? 'exact';
const selector = dataQaSelectorByMode[mode](nodeAnchorKey);
if (options?.text) {
return `${selector} >> text="${options.text}"`;
} else {
return selector;
}
};
// A method to simplify writing playwright selectors (see https://playwright.dev/docs/api/class-selectors /)
// <div className="node-key">
// in the test, using the slct helper, it is enough to write page.click(cssSlct('node-key'))
// If the second argument is passed, the selection takes place not only by the selector, but also by the text content
// nodes. Example:
// <div className="node-key">foo</div>
// <div className="node-key">bar</div>
// <div className="node-key">baz</div>
// To click on a node with the text "bar", write page.click(cssSlct('node-key', 'bar'))
export const cssSlct = (nodeAnchorKey: string, text?: string) => {
if (text) {
return `.data-qa-${nodeAnchorKey} >> text="${text}"`;
} else {
return `.data-qa-${nodeAnchorKey}`;
}
};
// Delete the entity on the page where the test is being performed. You need to delete the entities created by the tests so that they do not block navigation.
export async function deleteEntity(page: Page, url?: string) {
const moreButton = page.locator(slct(ActionPanelQA.MoreBtn));
await expect(moreButton).toBeVisible();
await moreButton.click();
const menuItemRemove = page
.locator(slct(ActionPanelEntryContextMenuQa.Menu))
.locator(slct(ActionPanelEntryContextMenuQa.Remove));
await expect(menuItemRemove).toBeVisible();
await menuItemRemove.click();
const applyButton = page.locator(slct(EntryDialogQA.Apply));
await expect(applyButton).toBeVisible();
if (url) {
await Promise.all([page.waitForURL(() => page.url().includes(url)), applyButton.click()]);
} else {
await Promise.all([page.waitForNavigation(), applyButton.click()]);
}
}
export async function copyEntity(page: Page, entryName: string) {
await page.waitForSelector(slct(ActionPanelQA.MoreBtn));
await page.click(slct(ActionPanelQA.MoreBtn));
await page.waitForSelector(
`${slct(ActionPanelEntryContextMenuQa.Menu)} ${slct(ActionPanelEntryContextMenuQa.Copy)}`,
);
await page.click(
`${slct(ActionPanelEntryContextMenuQa.Menu)} ${slct(ActionPanelEntryContextMenuQa.Copy)}`,
);
await page.waitForSelector(slct(EntryDialogQA.Apply));
await page.fill(`${slct(EntryDialogQA.Content)} .g-text-input__control_type_input`, entryName);
await Promise.all([page.waitForNavigation(), page.click(slct(EntryDialogQA.Apply))]);
}
// Log in after redirecting the passport page, the second argument = true if with the rights of a superuser
export async function makeLogIn(page: Page, asSuperuser?: boolean) {
await page.waitForSelector('input[name=login]');
await page.type('input[name=login]', process.env.E2E_USER_LOGIN as string);
await page.waitForSelector('input[name=passwd]');
await page.type('input[name=passwd]', process.env.E2E_USER_PASSWORD as string);
await Promise.all([page.waitForNavigation(), page.click('button[type=submit]')]);
if (asSuperuser) {
// installing a super-user mod
await setSuperUserMode(page);
}
}
// Fill in the input with the name of the entity being created in the EntryDialog (the dialog that appears when saving entities) and click the "Create" button
export async function entryDialogFillAndSave(
page: Page,
entryName: string,
useUserFolder?: boolean,
) {
const entryDialogInput = page
.locator(slct(DialogCreateWorkbookEntryQa.Input))
.or(page.locator(slct(EntryDialogQA.Content)).locator(slct(EntryDialogQA.PathSelect)));
await entryDialogInput.locator('input').fill(entryName);
// copy entity to user folder (not current)
const entryDialogSelect = page.locator(slct(EntryDialogQA.FolderSelect));
const isSelectVisible = await entryDialogSelect.isVisible();
if (useUserFolder && isSelectVisible) {
await entryDialogSelect.click();
await page.waitForSelector(slct(DlNavigationQA.NavigationMinimal));
await page.locator(slct(DlNavigationQA.MinimalDoneBtn)).click();
}
const button = page
.locator(slct(EntryDialogQA.Apply))
.or(page.locator(slct(DialogCreateWorkbookEntryQa.ApplyButton)));
await button.click();
}
function getFullUrl(url: string) {
const formattedUrl = url.startsWith('/') ? url : `/${url}`;
return new URL(`${process.env.E2E_DOMAIN}${formattedUrl}`);
}
export const getStringFullUrl = (url: string) => {
return getFullUrl(url).toString();
};
// Go to url by passing only pathname
export async function goto(
page: Page,
url: string,
options: {isRetry: boolean} = {
isRetry: false,
},
) {
const fullUrl = getFullUrl(url);
await page.goto(fullUrl.toString());
const hostname = await page.evaluate(() => window.location.hostname);
const CONTAINER_HOSTNAME = process.env.CONTAINER_HOSTNAME;
if (typeof CONTAINER_HOSTNAME !== 'undefined' && hostname === CONTAINER_HOSTNAME) {
if (options.isRetry) {
throw new Error(`Datalens is unavailable: ${fullUrl.toString()}`);
}
await page.waitForTimeout(5 * 1000);
await goto(page, url, {isRetry: true});
}
}
// Get the full address by passing pathname, hostname will connect depending on the value of the environment variable
// toLowerCase is needed because E2E_DOMAIN can have capital letters if they are in the branch name
export function getAddress(url: string) {
return `${process.env.E2E_DOMAIN}${url}`.toLowerCase();
}
export function getUniqueTimestamp() {
const randomSuffix = Math.floor(Math.random() * 100);
return `${moment(moment.now()).format('DD.MM.YYYY HH:mm:ss.SS')} ${randomSuffix}`;
}
export async function clickSelectOption(page: Page, key: string, optionText: string) {
await page.click(slct(key));
await page.click(`css=[data-qa=${key}-items] * >> text=${optionText}`);
}
export async function clickGSelectOption({
page,
key,
...args
}: {page: Page; key: string; optionText: string} | {page: Page; key: string; optionQa: string}) {
await page.click(slct(key));
if ('optionText' in args) {
await page.click(`css=[data-qa=${SelectQa.LIST}] >> text=${args.optionText}`);
} else {
await page.click(slct(args.optionQa));
}
}
export async function clickDropDownOption(page: Page, optionQa: string) {
await page.click(slct(optionQa));
}
export async function setCookie(page: Page, name: string, value: string, days: number) {
await page.evaluate(
({name, value, days}: {name: string; value: string; days: number}) => {
let expires = '';
if (days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = '; expires=' + date.toUTCString();
}
document.cookie = name + '=' + (value || '') + expires + '; path=/';
},
{
name,
value,
days,
},
);
}
// Get a link to the control node from Chartkit by its title
export async function getControlByTitle(
page: Page,
controlTitle: string,
): Promise<ElementHandle<HTMLElement>> {
let controlTitleElement = await page
.locator(slct(ControlQA.controlLabel))
.filter({hasText: controlTitle})
.elementHandle();
let control = await controlTitleElement?.getProperty('parentNode');
if (!control) {
controlTitleElement = await page.waitForSelector(
slct(ControlQA.chartkitControl, controlTitle),
);
control = await controlTitleElement.getProperty('parentNode');
}
return control as ElementHandle<HTMLElement>;
}
export async function getParentByQARole(
element: ElementHandle<SVGElement | HTMLElement> | null,
qaRole: string,
): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
while (element) {
element = await element.$('xpath=..');
if (element) {
const attribute = await element.getAttribute('data-qa');
if (attribute === qaRole) {
return element;
}
}
}
return element;
}
export async function setSuperUserMode(page: Page) {
await setCookie(page, 'dl_superuser_switch_mode', 'enable', 0);
}
export const waitForCondition = async <T>(callback: () => Promise<T>) => {
const attempts = 30;
const attemptTimeout = 1000;
for (let i = 0; i < attempts; i++) {
const result = await callback();
if (result) {
return;
}
await new Promise((resolve) => setTimeout(resolve, attemptTimeout));
}
throw new Error(`waitForCondition timeout, failedFunction is: ${callback.toString()}`);
};
export const generateQueryString = (queryMap: Record<string, string>) => {
const searchParams = new URLSearchParams();
Object.entries(queryMap).forEach(([key, value]) => {
searchParams.set(key, value);
});
return searchParams.toString();
};
export const openTestPage = async (page: Page, url: string, queryMap?: Record<string, string>) => {
const {pathname, searchParams} = getFullUrl(url);
const query = generateQueryString({...Object.fromEntries(searchParams), ...queryMap});
const fullUrl = query ? `${pathname}?${query}` : url;
await goto(page, fullUrl, {isRetry: false});
};
export const fillDatePicker = async (args: {page: Page; selector: string; value?: string}) => {
const {page, selector, value = ''} = args;
const datePicker = page.locator(selector).first();
const valueParts = value.split(/\.| |:/).filter(Boolean);
await datePicker.focus();
// date parts should be entered one by one,
// otherwise playwright cannot fill date field in properly way
for (const part of valueParts) {
await datePicker.pressSequentially(part);
}
};