@capillarytech/cap-ui-utils 3.1.0 → 3.1.2
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.
- package/e2e/constants/common.js +22 -0
- package/e2e/index.js +65 -0
- package/e2e/pages/common/base.page.js +13 -0
- package/e2e/pages/common/constant.js +5 -0
- package/e2e/pages/common/login.page.js +95 -0
- package/e2e/services/{lockService.ts → lockService.js} +71 -70
- package/e2e/services/locks/.gitkeep +0 -0
- package/e2e/utils/{antdVersionUtil.ts → antdVersionUtil.js} +24 -32
- package/e2e/utils/{automationBypassUtil.ts → automationBypassUtil.js} +11 -8
- package/e2e/utils/{debugModeUtil.ts → debugModeUtil.js} +9 -7
- package/e2e/utils/deletionRegistry.js +16 -0
- package/e2e/utils/{elementUtil.ts → elementUtil.js} +226 -298
- package/e2e/utils/expectUtil.js +40 -0
- package/e2e/utils/featureFlagUtil.js +25 -0
- package/e2e/utils/garudaDropdownUtil.js +34 -0
- package/e2e/utils/htmlEditorUtil.js +59 -0
- package/e2e/utils/logCollectorUtil.js +65 -0
- package/e2e/utils/{mockResponse.ts → mockResponse.js} +5 -2
- package/e2e/utils/{reportUploader.ts → reportUploader.js} +69 -66
- package/e2e/utils/{requestRecorderUtil.ts → requestRecorderUtil.js} +69 -97
- package/e2e/utils/{screenshotRecorderUtil.ts → screenshotRecorderUtil.js} +61 -82
- package/e2e/utils/setupMock.js +18 -0
- package/e2e/utils/unmatchedBracesUtil.js +87 -0
- package/e2e/utils/uploaders/fileServiceUploader.js +113 -0
- package/e2e/utils/uploaders/uploader.js +2 -0
- package/e2e/utils/virtualListUtil.js +74 -0
- package/package.json +1 -1
- package/e2e/constants/common.ts +0 -23
- package/e2e/index.ts +0 -30
- package/e2e/pages/common/base.page.ts +0 -11
- package/e2e/pages/common/constant.ts +0 -2
- package/e2e/pages/common/login.page.ts +0 -95
- package/e2e/services/locks/beeTemplateReady.signal +0 -1
- package/e2e/utils/deletionRegistry.ts +0 -15
- package/e2e/utils/expectUtil.ts +0 -19
- package/e2e/utils/featureFlagUtil.ts +0 -30
- package/e2e/utils/garudaDropdownUtil.ts +0 -60
- package/e2e/utils/htmlEditorUtil.ts +0 -56
- package/e2e/utils/logCollectorUtil.ts +0 -52
- package/e2e/utils/setupMock.ts +0 -29
- package/e2e/utils/unmatchedBracesUtil.ts +0 -101
- package/e2e/utils/uploaders/fileServiceUploader.ts +0 -84
- package/e2e/utils/uploaders/uploader.ts +0 -20
- package/e2e/utils/virtualListUtil.ts +0 -115
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.FileServiceUploader = void 0;
|
|
30
|
+
const fs = __importStar(require("fs"));
|
|
31
|
+
const axios_1 = __importDefault(require("axios"));
|
|
32
|
+
/**
|
|
33
|
+
* Uploads a file to Capillary's internal File Service, mirroring the
|
|
34
|
+
* `uploadData` flow in arya's arya-fileservice-sdk: a raw-binary POST to
|
|
35
|
+
* POST {baseUrl}v2/{namespace}/?fileName={name}&fileNameEncoding=false
|
|
36
|
+
* with the file bytes as the body. The service stores the object (in S3 under
|
|
37
|
+
* the hood) and returns JSON containing file_path / secure_file_path.
|
|
38
|
+
*
|
|
39
|
+
* Config via env vars:
|
|
40
|
+
* FILE_SERVICE_URL - base URL. Defaults to the in-cluster DNS
|
|
41
|
+
* http://file-service.default/ which is ONLY
|
|
42
|
+
* reachable from inside the Capillary cluster.
|
|
43
|
+
* Set this to a reachable host when running
|
|
44
|
+
* outside the cluster.
|
|
45
|
+
* FILE_SERVICE_NAMESPACE - target namespace (must be configured on the
|
|
46
|
+
* file-service side to allow the archive's
|
|
47
|
+
* extension/mime-type and size). Defaults to
|
|
48
|
+
* 'import', which is provisioned on the crm
|
|
49
|
+
* clusters and permits .zip (it stores under a
|
|
50
|
+
* generated UUID key, so use the returned path).
|
|
51
|
+
* FILE_SERVICE_ORG_ID - optional org id (X-CAP-API-AUTH-ORG-ID header)
|
|
52
|
+
* FILE_SERVICE_PUBLIC - "false" to upload as PRIVATE (default PUBLIC)
|
|
53
|
+
*
|
|
54
|
+
* Note: the arya SDK sends no auth header — the service relies on in-cluster
|
|
55
|
+
* network isolation. There is no external/authenticated route here.
|
|
56
|
+
*/
|
|
57
|
+
class FileServiceUploader {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.name = 'Capillary File Service';
|
|
60
|
+
this.baseUrl = process.env.FILE_SERVICE_URL || 'http://file-service.default/';
|
|
61
|
+
// Defaults to the 'import' namespace: it is provisioned on the crm clusters
|
|
62
|
+
// (unlike 'default', which does NOT exist server-side and 500s with
|
|
63
|
+
// NoSuchNamespaceException) and permits .zip uploads. Note it stores under a
|
|
64
|
+
// server-generated UUID key, so retrieve via the returned file_path, not the
|
|
65
|
+
// original filename. Override per env with FILE_SERVICE_NAMESPACE.
|
|
66
|
+
this.namespace = process.env.FILE_SERVICE_NAMESPACE || 'import';
|
|
67
|
+
this.orgId = process.env.FILE_SERVICE_ORG_ID || '';
|
|
68
|
+
this.isPublic = process.env.FILE_SERVICE_PUBLIC !== 'false';
|
|
69
|
+
// Hard network timeout so an unresponsive file-service can never hang the
|
|
70
|
+
// run's onComplete hook (which would keep the pod alive forever).
|
|
71
|
+
this.timeoutMs = Number(process.env.FILE_SERVICE_TIMEOUT_MS) || 60000;
|
|
72
|
+
}
|
|
73
|
+
isConfigured() {
|
|
74
|
+
return Boolean(this.baseUrl && this.namespace);
|
|
75
|
+
}
|
|
76
|
+
missingConfigMessage() {
|
|
77
|
+
const missing = [];
|
|
78
|
+
if (!this.baseUrl)
|
|
79
|
+
missing.push('FILE_SERVICE_URL');
|
|
80
|
+
if (!this.namespace)
|
|
81
|
+
missing.push('FILE_SERVICE_NAMESPACE');
|
|
82
|
+
return missing.join(', ');
|
|
83
|
+
}
|
|
84
|
+
async upload(localFilePath, remoteName) {
|
|
85
|
+
var _a;
|
|
86
|
+
const base = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`;
|
|
87
|
+
const url = `${base}v2/${this.namespace}/?fileName=${encodeURI(remoteName)}&fileNameEncoding=false`;
|
|
88
|
+
const headers = { 'Content-Type': 'application/zip' };
|
|
89
|
+
if (this.isPublic)
|
|
90
|
+
headers['X-CAP-FS-ACL'] = 'PUBLIC';
|
|
91
|
+
if (this.orgId)
|
|
92
|
+
headers['X-CAP-API-AUTH-ORG-ID'] = String(this.orgId);
|
|
93
|
+
const body = fs.readFileSync(localFilePath);
|
|
94
|
+
const res = await axios_1.default.post(url, body, {
|
|
95
|
+
headers,
|
|
96
|
+
maxBodyLength: Infinity,
|
|
97
|
+
maxContentLength: Infinity,
|
|
98
|
+
timeout: this.timeoutMs,
|
|
99
|
+
});
|
|
100
|
+
// The file-service returns HTTP 200 even when the upload fails (e.g.
|
|
101
|
+
// NoSuchNamespaceException), carrying the error in the JSON body. So a
|
|
102
|
+
// 200 is NOT proof of success — only a real file path is. Treat a body
|
|
103
|
+
// without one as a failure so the run doesn't silently report success.
|
|
104
|
+
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
|
105
|
+
const location = (data === null || data === void 0 ? void 0 : data.secure_file_path) || (data === null || data === void 0 ? void 0 : data.file_path) || ((_a = data === null || data === void 0 ? void 0 : data.file) === null || _a === void 0 ? void 0 : _a.s3_token);
|
|
106
|
+
if (!location) {
|
|
107
|
+
const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data);
|
|
108
|
+
throw new Error(`file-service returned no file path (namespace='${this.namespace}'): ${body}`);
|
|
109
|
+
}
|
|
110
|
+
return location;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.FileServiceUploader = FileServiceUploader;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers for antd v6 virtualized lists (`rc-virtual-list`).
|
|
4
|
+
*
|
|
5
|
+
* The list renders only a window of DOM rows at a time. A row that exists in the data
|
|
6
|
+
* model but is outside that window has no DOM node, so `waitForDisplayed` / `click`
|
|
7
|
+
* through WDIO never sees it. These helpers drive the scroll container in the browser
|
|
8
|
+
* until the row is rendered, then click the target directly.
|
|
9
|
+
*
|
|
10
|
+
* Works for both `ant-select-tree` (TreeSelect) and `ant-select-item-option` (Select).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.scrollAndClickVirtualOption = void 0;
|
|
14
|
+
/**
|
|
15
|
+
* Scroll a virtualized antd list inside `containerSelector` until a row whose trimmed
|
|
16
|
+
* textContent equals `label` is rendered, then click it (or its checkbox).
|
|
17
|
+
*
|
|
18
|
+
* Returns true on success, throws on timeout.
|
|
19
|
+
*/
|
|
20
|
+
async function scrollAndClickVirtualOption(label, opts = {}) {
|
|
21
|
+
const { containerSelector, clickCheckbox = true, timeout = 15000, interval = 300, } = opts;
|
|
22
|
+
await browser.waitUntil(async () => {
|
|
23
|
+
return await browser.execute((containerSel, targetLabel, useCheckbox) => {
|
|
24
|
+
const root = containerSel
|
|
25
|
+
? document.querySelector(containerSel) || document
|
|
26
|
+
: document;
|
|
27
|
+
const ROW_SELECTOR = '[role="treeitem"], .ant-select-tree-treenode, .ant-select-item-option';
|
|
28
|
+
const findRow = () => {
|
|
29
|
+
const rows = Array.from(root.querySelectorAll(ROW_SELECTOR));
|
|
30
|
+
for (const r of rows) {
|
|
31
|
+
if ((r.textContent || '').trim() === targetLabel)
|
|
32
|
+
return r;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
// If already rendered, click and return.
|
|
37
|
+
let row = findRow();
|
|
38
|
+
if (row) {
|
|
39
|
+
const checkbox = useCheckbox
|
|
40
|
+
? row.querySelector('.ant-select-tree-checkbox, .ant-tree-checkbox, .ant-select-item-option-state')
|
|
41
|
+
: null;
|
|
42
|
+
(checkbox || row).click();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Find the scroll container.
|
|
46
|
+
const holder = root.querySelector('.rc-virtual-list-holder, .ant-select-tree-list-holder, .ant-tree-list-holder');
|
|
47
|
+
if (!holder)
|
|
48
|
+
return false;
|
|
49
|
+
// rc-virtual-list uses the holder's scrollTop to drive the translateY of inner.
|
|
50
|
+
// Advance by one viewport (clientHeight) each tick until we either find the row
|
|
51
|
+
// or hit the end.
|
|
52
|
+
const prev = holder.scrollTop;
|
|
53
|
+
holder.scrollTop = Math.min(holder.scrollHeight, prev + Math.max(holder.clientHeight, 120));
|
|
54
|
+
// Dispatch scroll so the virtual list re-renders.
|
|
55
|
+
holder.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
56
|
+
row = findRow();
|
|
57
|
+
if (row) {
|
|
58
|
+
const checkbox = useCheckbox
|
|
59
|
+
? row.querySelector('.ant-select-tree-checkbox, .ant-tree-checkbox, .ant-select-item-option-state')
|
|
60
|
+
: null;
|
|
61
|
+
(checkbox || row).click();
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// Reached the bottom without finding it? Caller will timeout if so.
|
|
65
|
+
return holder.scrollTop >= holder.scrollHeight - holder.clientHeight ? false : false;
|
|
66
|
+
}, containerSelector !== null && containerSelector !== void 0 ? containerSelector : '', label, clickCheckbox);
|
|
67
|
+
}, {
|
|
68
|
+
timeout,
|
|
69
|
+
interval,
|
|
70
|
+
timeoutMsg: `Virtualized option "${label}" not found within ${timeout}ms (container=${containerSelector !== null && containerSelector !== void 0 ? containerSelector : 'document'})`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
exports.scrollAndClickVirtualOption = scrollAndClickVirtualOption;
|
|
74
|
+
exports.default = { scrollAndClickVirtualOption };
|
package/package.json
CHANGED
package/e2e/constants/common.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Singapore clusters that require special handling
|
|
2
|
-
export const SG_CLUSTERS = ['sgcrm', 'sgcrmasia'];
|
|
3
|
-
|
|
4
|
-
export const INFO = "🟡";
|
|
5
|
-
export const SUCCESS = "🟢";
|
|
6
|
-
export const FAILURE = "🔴";
|
|
7
|
-
|
|
8
|
-
export const WAIT_30S = 30000;
|
|
9
|
-
export const WAIT_60S = 60000;
|
|
10
|
-
export const WAIT_90S = 90000;
|
|
11
|
-
export const WAIT_120S = 120000;
|
|
12
|
-
|
|
13
|
-
export const common = {
|
|
14
|
-
"cluster" : process.env.cluster,
|
|
15
|
-
"validatorUrl" : "http://127.0.0.1:4000",
|
|
16
|
-
"maxLoginAttempts": 3,
|
|
17
|
-
"sgClusters": SG_CLUSTERS,
|
|
18
|
-
"isLoyaltyMigration": process.env.module === 'loyalty_migration',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// login.page.ts (shared) imports this by name; re-exported here so the toolkit
|
|
22
|
-
// stays free of the data-heavy constants/constants aggregator (org/auth/endpoints).
|
|
23
|
-
export const maxLoginAttempts = common.maxLoginAttempts;
|
package/e2e/index.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared WDIO E2E toolkit — the framework-agnostic, data-free helpers that were
|
|
3
|
-
* duplicated byte-for-byte across the product repos (wdio-ui-automation,
|
|
4
|
-
* garuda-ui, and every future migrated repo).
|
|
5
|
-
*
|
|
6
|
-
* Two ways to consume:
|
|
7
|
-
* import { elementUtil, loginPage } from '@capillarytech/cap-ui-utils/e2e';
|
|
8
|
-
* import elementUtil from '@capillarytech/cap-ui-utils/e2e/utils/elementUtil';
|
|
9
|
-
*
|
|
10
|
-
* NOT included yet (data-coupled — they read the per-repo `cons` org/auth/endpoint
|
|
11
|
-
* data via constants/constants): apiUtil, helperUtil, cookieUtil, navigationUtil,
|
|
12
|
-
* appNavigationUtil, requestRecorderService, home.page, testAndPreviewFlow.page.
|
|
13
|
-
* These need a small config-injection seam before they can move — see README.
|
|
14
|
-
*/
|
|
15
|
-
export { default as elementUtil } from './utils/elementUtil';
|
|
16
|
-
export { default as antdVersion } from './utils/antdVersionUtil';
|
|
17
|
-
export { default as expectUtil } from './utils/expectUtil';
|
|
18
|
-
export { default as automationBypass } from './utils/automationBypassUtil';
|
|
19
|
-
export { default as debugMode } from './utils/debugModeUtil';
|
|
20
|
-
export { default as deletionRegistry } from './utils/deletionRegistry';
|
|
21
|
-
export { default as dropdownUtil } from './utils/garudaDropdownUtil';
|
|
22
|
-
export { default as logCollectorUtil } from './utils/logCollectorUtil';
|
|
23
|
-
export { default as reportUploader } from './utils/reportUploader';
|
|
24
|
-
export { default as requestRecorderUtil } from './utils/requestRecorderUtil';
|
|
25
|
-
export { default as screenshotRecorderUtil } from './utils/screenshotRecorderUtil';
|
|
26
|
-
export { default as setupMock } from './utils/setupMock';
|
|
27
|
-
export { default as lockService } from './services/lockService';
|
|
28
|
-
export { default as BasePage } from './pages/common/base.page';
|
|
29
|
-
export { default as loginPage } from './pages/common/login.page';
|
|
30
|
-
export * from './constants/common';
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export default class BasePage{
|
|
2
|
-
async launchApplication(url){
|
|
3
|
-
console.log('Browser Maximize')
|
|
4
|
-
browser.maximizeWindow();
|
|
5
|
-
console.log('Triggered navigate to', url)
|
|
6
|
-
await browser.navigateTo(url)
|
|
7
|
-
console.log('Navigated to URL: ', url);
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
async quitApplication(){await browser.closeWindow()}
|
|
11
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import basePage from "./base.page"
|
|
2
|
-
import elementUtil from "../../utils/elementUtil"
|
|
3
|
-
import { maxLoginAttempts } from '../../constants/common';
|
|
4
|
-
import commands from "@rpii/wdio-commands"
|
|
5
|
-
|
|
6
|
-
class LoginPage extends basePage{
|
|
7
|
-
|
|
8
|
-
get isV2Login(){return process.env.V2_LOGIN === 'true'}
|
|
9
|
-
get inputUserName(){return this.isV2Login ? $('#login-username') : $('#login_user')}
|
|
10
|
-
get inputPassword(){return this.isV2Login ? $('#login-password') : $('#login_cred')}
|
|
11
|
-
get buttonLogin(){return this.isV2Login ? $('//button[.="Sign in"]') : $('#c-login-btn')}
|
|
12
|
-
get continueButton(){return $('//button[.="Continue"]')}
|
|
13
|
-
// Post-login readiness marker. The MFE Navigation Host replaced the old
|
|
14
|
-
// Dashboard (which rendered ExplorePosts → "Explore features") with NewDashboard,
|
|
15
|
-
// so the sidebar shell testid is the reliable signal the home shell has mounted.
|
|
16
|
-
// Old text kept as a backward-compatible fallback for pre-MFE environments.
|
|
17
|
-
get exploreFeatures(){return $('//*[@data-testid="cap-navigation-spa-sidebar"] | //*[text()="Explore features"]')}
|
|
18
|
-
|
|
19
|
-
async doLogin(url, username, password, loginTryCount=0){
|
|
20
|
-
try {
|
|
21
|
-
await browser.refresh();
|
|
22
|
-
await browser.pause(3000);
|
|
23
|
-
console.log('Browser launched')
|
|
24
|
-
await super.launchApplication(url)
|
|
25
|
-
if (loginTryCount > 0) {
|
|
26
|
-
let currentUrl = await browser.getUrl();
|
|
27
|
-
const isHome = currentUrl.includes("/home/ui");
|
|
28
|
-
if (isHome) {
|
|
29
|
-
try {
|
|
30
|
-
await (await this.exploreFeatures).waitForDisplayed({ timeout: 60000, interval: 5000 });
|
|
31
|
-
console.log('-----Logged In-----');
|
|
32
|
-
console.log('On retry login flow, /home/ui url and explore features text appeared after 60s probably, hence skipping entering username and password');
|
|
33
|
-
return;
|
|
34
|
-
} catch(e) {
|
|
35
|
-
console.log('proceeding with retry');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
for(let i=0; i<=2; i++){
|
|
40
|
-
try{
|
|
41
|
-
await (await this.inputUserName).waitForDisplayed({ timeout: 60000 });
|
|
42
|
-
await this.inputUserName.waitForEnabled({ timeout: 30000, timeoutMsg:"thrown from dologin", interval:1000 })
|
|
43
|
-
break;
|
|
44
|
-
}catch(error){
|
|
45
|
-
console.log('Failed to load login page- Refreshing browser')
|
|
46
|
-
await browser.refresh();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
await elementUtil.enterText( await this.inputUserName, username)
|
|
50
|
-
if (this.isV2Login) {
|
|
51
|
-
await this.continueButton.waitForEnabled({ timeout: 10000 })
|
|
52
|
-
await elementUtil.elementClick( await this.continueButton)
|
|
53
|
-
await this.inputPassword.waitForDisplayed({ timeout: 30000 })
|
|
54
|
-
}
|
|
55
|
-
await elementUtil.enterText( await this.inputPassword, password)
|
|
56
|
-
await this.buttonLogin.waitForEnabled({ timeout: 10000 })
|
|
57
|
-
await elementUtil.elementClick( await this.buttonLogin)
|
|
58
|
-
await browser.waitUntil(
|
|
59
|
-
async () => {
|
|
60
|
-
let currentUrl = await browser.getUrl();
|
|
61
|
-
return currentUrl.includes("/home/ui")
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
timeout: 60000,
|
|
65
|
-
timeoutMsg: 'expected url to include /home/ui',
|
|
66
|
-
interval: 1000,
|
|
67
|
-
}
|
|
68
|
-
);
|
|
69
|
-
console.log('-----Browsers current url contains home/ui after login-----')
|
|
70
|
-
await (await this.exploreFeatures).waitForDisplayed({ timeout: 60000, interval: 5000 });
|
|
71
|
-
console.log('-----Logged In-----')
|
|
72
|
-
}
|
|
73
|
-
catch (error){
|
|
74
|
-
console.log('Throwed error from dologin, attempt #: ', loginTryCount, ', Error: ', error);
|
|
75
|
-
commands.logScreenshot("Failed to get the user logged in: "+error)
|
|
76
|
-
if (loginTryCount >= (maxLoginAttempts - 1)) {
|
|
77
|
-
console.log('Since login retry count exceeds 2. Exiting now.');
|
|
78
|
-
throw new Error("Failed to get the user logged in: "+error);
|
|
79
|
-
}
|
|
80
|
-
console.log('Retrying Login.. ');
|
|
81
|
-
await this.doLogin(url, username, password, ++loginTryCount);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async redirectHome(url){
|
|
86
|
-
try{
|
|
87
|
-
await super.launchApplication(url)
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
commands.logScreenshot("Exception logged: "+error)
|
|
91
|
-
throw new Error(error)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
export default new LoginPage()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ready
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
class DeletionRegistry {
|
|
2
|
-
private deleted = new Set<string>();
|
|
3
|
-
|
|
4
|
-
has(name: string): boolean {
|
|
5
|
-
return this.deleted.has(name);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
register(name: string): void {
|
|
9
|
-
this.deleted.add(name);
|
|
10
|
-
console.log(`DeletionRegistry: registered "${name}"`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const deletionRegistry = new DeletionRegistry();
|
|
15
|
-
export default deletionRegistry;
|
package/e2e/utils/expectUtil.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import * as assert from 'assert'
|
|
3
|
-
|
|
4
|
-
class AssertionUtil {
|
|
5
|
-
|
|
6
|
-
async assertValue(element:WebdriverIO.Element, textToBeValidated:string){
|
|
7
|
-
assert.strictEqual(await element.getText(),textToBeValidated);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async assertElementExistence(element:WebdriverIO.Element){
|
|
11
|
-
try {
|
|
12
|
-
assert.strictEqual(await element.isDisplayed(), true);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
throw new Error(error)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default new AssertionUtil()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads feature flags from window.capAuth.accessibleFeatures — the same source
|
|
3
|
-
* that Auth.hasFeatureAccess() uses in the app (cap-ui-utils/auth/hasFeatureAccess.js).
|
|
4
|
-
*
|
|
5
|
-
* Call loadFeatureFlags() once after the app has loaded (e.g. after openAudiencePage),
|
|
6
|
-
* then use hasFeatureAccess() anywhere in the test run.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
let _accessibleFeatures: string[] = [];
|
|
10
|
-
|
|
11
|
-
async function loadFeatureFlags(): Promise<void> {
|
|
12
|
-
// window.capAuth.accessibleFeatures is populated async after the app boots.
|
|
13
|
-
// Wait until it's array-shaped (up to 30s) before caching — an empty array
|
|
14
|
-
// is a valid "no features enabled" state, not a sign it's still loading.
|
|
15
|
-
await browser.waitUntil(
|
|
16
|
-
async () => {
|
|
17
|
-
const features = await browser.execute(() => (window as any).capAuth?.accessibleFeatures);
|
|
18
|
-
return Array.isArray(features);
|
|
19
|
-
},
|
|
20
|
-
{ timeout: 30000, interval: 1000, timeoutMsg: '[featureFlags] window.capAuth.accessibleFeatures never populated' }
|
|
21
|
-
);
|
|
22
|
-
_accessibleFeatures = await browser.execute(() => (window as any).capAuth?.accessibleFeatures || []);
|
|
23
|
-
console.log(`[featureFlags] loaded ${_accessibleFeatures.length} features`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function hasFeatureAccess(feature: string): boolean {
|
|
27
|
-
return _accessibleFeatures.includes(feature);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default { loadFeatureFlags, hasFeatureAccess };
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
class GarudaDropdownUtil {
|
|
2
|
-
treeOptionByText(
|
|
3
|
-
text: string,
|
|
4
|
-
index: number | 'last' = 1,
|
|
5
|
-
altText?: string
|
|
6
|
-
): WebdriverIO.Element {
|
|
7
|
-
const textMatch = altText
|
|
8
|
-
? `normalize-space()="${text}" or normalize-space()="${altText}"`
|
|
9
|
-
: `normalize-space()="${text}"`;
|
|
10
|
-
const position = index === 'last' ? 'last()' : index;
|
|
11
|
-
return $(
|
|
12
|
-
`(//span[contains(@class,"ant-select-tree-node-content-wrapper")][.//div[${textMatch}]])[${position}]`
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
unifiedOptionByText(text: string): WebdriverIO.Element {
|
|
17
|
-
return $(
|
|
18
|
-
`//div[contains(@class,"cap-unified-select-popup")]//div[.//div[text()="${text}"] and @role="treeitem"]`
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
listOptionByLabel(label: string, index: number = 1): WebdriverIO.Element {
|
|
23
|
-
return $(
|
|
24
|
-
`(//ul[@role="listbox"]//li[@role="option" and @label="${label}"])[${index}]`
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
selectOptionByText(text: string, index: number = 1): WebdriverIO.Element {
|
|
29
|
-
return $(
|
|
30
|
-
`(//div[contains(@class,"ant-select-item-option")][.//div[contains(@class,"ant-select-item-option-content")][normalize-space()="${text}"]])[${index}]`
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
treeOption(index: number | 'last' = 1): WebdriverIO.Element {
|
|
35
|
-
const position = index === 'last' ? 'last()' : index;
|
|
36
|
-
return $(
|
|
37
|
-
`(//span[contains(@class,"ant-select-tree-node-content-wrapper")][normalize-space(.)])[${position}]`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
listOptionByLabelInPopup(ariaControlId: string, label: string): WebdriverIO.Element {
|
|
42
|
-
return $(
|
|
43
|
-
`//div[contains(@id,"${ariaControlId}")]//ul//li[@role="option" and @label="${label}"]`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
menuItemByText(text: string, index: number = 1): WebdriverIO.Element {
|
|
48
|
-
return $(
|
|
49
|
-
`(//li[contains(@class,"ant-dropdown-menu-item")][.//span[normalize-space()="${text}"]])[${index}]`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
unifiedSelectConfirmButton(): WebdriverIO.Element {
|
|
54
|
-
return $(
|
|
55
|
-
`//button[contains(@class,"cap-unified-select-confirm-button")]`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export default new GarudaDropdownUtil();
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helpers for the antd v6 contenteditable HTML editor.
|
|
3
|
-
* After bulk HTML injection, the caret remains outside the `<body>` tag, causing label
|
|
4
|
-
* insertions at the wrong position. `placeCaretInsideBody` locates the `<body>` position
|
|
5
|
-
* across text nodes and updates the Selection range correctly.
|
|
6
|
-
*
|
|
7
|
-
* Caller passes the WDIO editor element (defined as a page selector), so this util
|
|
8
|
-
* does not own selector strings.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export async function placeCaretInsideBody(
|
|
12
|
-
editor: WebdriverIO.Element,
|
|
13
|
-
): Promise<boolean> {
|
|
14
|
-
return browser.execute((node: HTMLElement) => {
|
|
15
|
-
if (!node) return false;
|
|
16
|
-
|
|
17
|
-
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
18
|
-
const nodes: Text[] = [];
|
|
19
|
-
const starts: number[] = [];
|
|
20
|
-
let combined = '';
|
|
21
|
-
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
22
|
-
const t = n as Text;
|
|
23
|
-
starts.push(combined.length);
|
|
24
|
-
combined += t.nodeValue || '';
|
|
25
|
-
nodes.push(t);
|
|
26
|
-
}
|
|
27
|
-
if (!nodes.length) return false;
|
|
28
|
-
|
|
29
|
-
const open = combined.match(/<\s*body\b[^>]*>/i);
|
|
30
|
-
if (!open || open.index === undefined) return false;
|
|
31
|
-
|
|
32
|
-
let idx = open.index + open[0].length;
|
|
33
|
-
while (idx < combined.length && /\s/.test(combined.charAt(idx))) idx++;
|
|
34
|
-
|
|
35
|
-
let nodeIdx = 0;
|
|
36
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
37
|
-
const end = starts[i] + (nodes[i].nodeValue || '').length;
|
|
38
|
-
if (idx <= end) { nodeIdx = i; break; }
|
|
39
|
-
nodeIdx = i;
|
|
40
|
-
}
|
|
41
|
-
const target = nodes[nodeIdx];
|
|
42
|
-
const offset = Math.max(0, Math.min(idx - starts[nodeIdx], (target.nodeValue || '').length));
|
|
43
|
-
|
|
44
|
-
node.focus();
|
|
45
|
-
const range = document.createRange();
|
|
46
|
-
range.setStart(target, offset);
|
|
47
|
-
range.collapse(true);
|
|
48
|
-
const selection = window.getSelection();
|
|
49
|
-
if (!selection) return false;
|
|
50
|
-
selection.removeAllRanges();
|
|
51
|
-
selection.addRange(range);
|
|
52
|
-
return true;
|
|
53
|
-
}, editor as any);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default { placeCaretInsideBody };
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import supertest from 'supertest'
|
|
2
|
-
import commands from "@rpii/wdio-commands"
|
|
3
|
-
|
|
4
|
-
class LogCollectorUtil {
|
|
5
|
-
|
|
6
|
-
callCount=0;
|
|
7
|
-
|
|
8
|
-
async collectLogs(runId, test, error, duration, passed, skipped = false){
|
|
9
|
-
this.callCount += 1;
|
|
10
|
-
let validationMessage
|
|
11
|
-
let status
|
|
12
|
-
|
|
13
|
-
if(passed){
|
|
14
|
-
status='passed'
|
|
15
|
-
validationMessage = ''
|
|
16
|
-
} else if(skipped){
|
|
17
|
-
status='skipped'
|
|
18
|
-
validationMessage = ''
|
|
19
|
-
} else {
|
|
20
|
-
commands.logScreenshot("Exception logged: "+error)
|
|
21
|
-
status='failed'
|
|
22
|
-
if(error===undefined){
|
|
23
|
-
validationMessage='cannot capture error'
|
|
24
|
-
} else {
|
|
25
|
-
validationMessage=error.toString().replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// this.recordBrowserLogs()
|
|
29
|
-
const baseUrl = 'http://127.0.0.1:4000'
|
|
30
|
-
const request = supertest(baseUrl)
|
|
31
|
-
let exeTime = duration / 1000
|
|
32
|
-
let message ={[runId]: {[test.parent]: {[test.title]: {"status": status,
|
|
33
|
-
"time": exeTime.toString(), "validataionMessage": validationMessage,
|
|
34
|
-
"total": this.callCount}}}}
|
|
35
|
-
const messagePayload ={"result" : message}
|
|
36
|
-
const response = await request.
|
|
37
|
-
post('/logger')
|
|
38
|
-
.send(messagePayload)
|
|
39
|
-
console.log('API Response',response.body);
|
|
40
|
-
console.log('Message', message)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async recordBrowserLogs(){
|
|
44
|
-
const logTypes = Promise.resolve(browser.getLogs('browser')).then(function(data){return data})
|
|
45
|
-
logTypes.then(data=>(JSON.stringify(data.filter(function getObj(logs) {
|
|
46
|
-
if(logs['level']==='SEVERE' || logs['level']==='ERROR'){
|
|
47
|
-
commands.logMessage((logs['level']+": "+JSON.stringify(logs)))}
|
|
48
|
-
})))
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export default new LogCollectorUtil()
|
package/e2e/utils/setupMock.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { beamerApiResp } from "./mockResponse";
|
|
2
|
-
|
|
3
|
-
class setupMock {
|
|
4
|
-
async mockBeamerAPIForPopup() {
|
|
5
|
-
const beamerReq1 = await browser.mock(
|
|
6
|
-
"https://backend.getbeamer.com/initialize" + "**",
|
|
7
|
-
{
|
|
8
|
-
method: "get",
|
|
9
|
-
}
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
beamerReq1.respond(beamerApiResp);
|
|
13
|
-
|
|
14
|
-
const beamerReq2 = await browser.mock(
|
|
15
|
-
"https://backend.getbeamer.com/numberFeatures" + "**",
|
|
16
|
-
{
|
|
17
|
-
method: "get",
|
|
18
|
-
}
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
beamerReq2.respond(
|
|
22
|
-
[{}],
|
|
23
|
-
{
|
|
24
|
-
statusCode: 404,
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export default new setupMock();
|