@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,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.maxLoginAttempts = exports.common = exports.WAIT_120S = exports.WAIT_90S = exports.WAIT_60S = exports.WAIT_30S = exports.FAILURE = exports.SUCCESS = exports.INFO = exports.SG_CLUSTERS = void 0;
|
|
4
|
+
// Singapore clusters that require special handling
|
|
5
|
+
exports.SG_CLUSTERS = ['sgcrm', 'sgcrmasia'];
|
|
6
|
+
exports.INFO = "🟡";
|
|
7
|
+
exports.SUCCESS = "🟢";
|
|
8
|
+
exports.FAILURE = "🔴";
|
|
9
|
+
exports.WAIT_30S = 30000;
|
|
10
|
+
exports.WAIT_60S = 60000;
|
|
11
|
+
exports.WAIT_90S = 90000;
|
|
12
|
+
exports.WAIT_120S = 120000;
|
|
13
|
+
exports.common = {
|
|
14
|
+
"cluster": process.env.cluster,
|
|
15
|
+
"validatorUrl": "http://127.0.0.1:4000",
|
|
16
|
+
"maxLoginAttempts": 3,
|
|
17
|
+
"sgClusters": exports.SG_CLUSTERS,
|
|
18
|
+
"isLoyaltyMigration": process.env.module === 'loyalty_migration',
|
|
19
|
+
};
|
|
20
|
+
// login.page.ts (shared) imports this by name; re-exported here so the toolkit
|
|
21
|
+
// stays free of the data-heavy constants/constants aggregator (org/auth/endpoints).
|
|
22
|
+
exports.maxLoginAttempts = exports.common.maxLoginAttempts;
|
package/e2e/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.loginPage = exports.BasePage = exports.lockService = exports.setupMock = exports.screenshotRecorderUtil = exports.requestRecorderUtil = exports.reportUploader = exports.logCollectorUtil = exports.dropdownUtil = exports.deletionRegistry = exports.debugMode = exports.automationBypass = exports.expectUtil = exports.antdVersion = exports.elementUtil = void 0;
|
|
21
|
+
/**
|
|
22
|
+
* Shared WDIO E2E toolkit — the framework-agnostic, data-free helpers that were
|
|
23
|
+
* duplicated byte-for-byte across the product repos (wdio-ui-automation,
|
|
24
|
+
* garuda-ui, and every future migrated repo).
|
|
25
|
+
*
|
|
26
|
+
* Two ways to consume:
|
|
27
|
+
* import { elementUtil, loginPage } from '@capillarytech/cap-ui-utils/e2e';
|
|
28
|
+
* import elementUtil from '@capillarytech/cap-ui-utils/e2e/utils/elementUtil';
|
|
29
|
+
*
|
|
30
|
+
* NOT included yet (data-coupled — they read the per-repo `cons` org/auth/endpoint
|
|
31
|
+
* data via constants/constants): apiUtil, helperUtil, cookieUtil, navigationUtil,
|
|
32
|
+
* appNavigationUtil, requestRecorderService, home.page, testAndPreviewFlow.page.
|
|
33
|
+
* These need a small config-injection seam before they can move — see README.
|
|
34
|
+
*/
|
|
35
|
+
var elementUtil_1 = require("./utils/elementUtil");
|
|
36
|
+
Object.defineProperty(exports, "elementUtil", { enumerable: true, get: function () { return __importDefault(elementUtil_1).default; } });
|
|
37
|
+
var antdVersionUtil_1 = require("./utils/antdVersionUtil");
|
|
38
|
+
Object.defineProperty(exports, "antdVersion", { enumerable: true, get: function () { return __importDefault(antdVersionUtil_1).default; } });
|
|
39
|
+
var expectUtil_1 = require("./utils/expectUtil");
|
|
40
|
+
Object.defineProperty(exports, "expectUtil", { enumerable: true, get: function () { return __importDefault(expectUtil_1).default; } });
|
|
41
|
+
var automationBypassUtil_1 = require("./utils/automationBypassUtil");
|
|
42
|
+
Object.defineProperty(exports, "automationBypass", { enumerable: true, get: function () { return __importDefault(automationBypassUtil_1).default; } });
|
|
43
|
+
var debugModeUtil_1 = require("./utils/debugModeUtil");
|
|
44
|
+
Object.defineProperty(exports, "debugMode", { enumerable: true, get: function () { return __importDefault(debugModeUtil_1).default; } });
|
|
45
|
+
var deletionRegistry_1 = require("./utils/deletionRegistry");
|
|
46
|
+
Object.defineProperty(exports, "deletionRegistry", { enumerable: true, get: function () { return __importDefault(deletionRegistry_1).default; } });
|
|
47
|
+
var garudaDropdownUtil_1 = require("./utils/garudaDropdownUtil");
|
|
48
|
+
Object.defineProperty(exports, "dropdownUtil", { enumerable: true, get: function () { return __importDefault(garudaDropdownUtil_1).default; } });
|
|
49
|
+
var logCollectorUtil_1 = require("./utils/logCollectorUtil");
|
|
50
|
+
Object.defineProperty(exports, "logCollectorUtil", { enumerable: true, get: function () { return __importDefault(logCollectorUtil_1).default; } });
|
|
51
|
+
var reportUploader_1 = require("./utils/reportUploader");
|
|
52
|
+
Object.defineProperty(exports, "reportUploader", { enumerable: true, get: function () { return __importDefault(reportUploader_1).default; } });
|
|
53
|
+
var requestRecorderUtil_1 = require("./utils/requestRecorderUtil");
|
|
54
|
+
Object.defineProperty(exports, "requestRecorderUtil", { enumerable: true, get: function () { return __importDefault(requestRecorderUtil_1).default; } });
|
|
55
|
+
var screenshotRecorderUtil_1 = require("./utils/screenshotRecorderUtil");
|
|
56
|
+
Object.defineProperty(exports, "screenshotRecorderUtil", { enumerable: true, get: function () { return __importDefault(screenshotRecorderUtil_1).default; } });
|
|
57
|
+
var setupMock_1 = require("./utils/setupMock");
|
|
58
|
+
Object.defineProperty(exports, "setupMock", { enumerable: true, get: function () { return __importDefault(setupMock_1).default; } });
|
|
59
|
+
var lockService_1 = require("./services/lockService");
|
|
60
|
+
Object.defineProperty(exports, "lockService", { enumerable: true, get: function () { return __importDefault(lockService_1).default; } });
|
|
61
|
+
var base_page_1 = require("./pages/common/base.page");
|
|
62
|
+
Object.defineProperty(exports, "BasePage", { enumerable: true, get: function () { return __importDefault(base_page_1).default; } });
|
|
63
|
+
var login_page_1 = require("./pages/common/login.page");
|
|
64
|
+
Object.defineProperty(exports, "loginPage", { enumerable: true, get: function () { return __importDefault(login_page_1).default; } });
|
|
65
|
+
__exportStar(require("./constants/common"), exports);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class BasePage {
|
|
4
|
+
async launchApplication(url) {
|
|
5
|
+
console.log('Browser Maximize');
|
|
6
|
+
browser.maximizeWindow();
|
|
7
|
+
console.log('Triggered navigate to', url);
|
|
8
|
+
await browser.navigateTo(url);
|
|
9
|
+
console.log('Navigated to URL: ', url);
|
|
10
|
+
}
|
|
11
|
+
async quitApplication() { await browser.closeWindow(); }
|
|
12
|
+
}
|
|
13
|
+
exports.default = BasePage;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const base_page_1 = __importDefault(require("./base.page"));
|
|
7
|
+
const elementUtil_1 = __importDefault(require("../../utils/elementUtil"));
|
|
8
|
+
const common_1 = require("../../constants/common");
|
|
9
|
+
const wdio_commands_1 = __importDefault(require("@rpii/wdio-commands"));
|
|
10
|
+
class LoginPage extends base_page_1.default {
|
|
11
|
+
get isV2Login() { return process.env.V2_LOGIN === 'true'; }
|
|
12
|
+
get inputUserName() { return this.isV2Login ? $('#login-username') : $('#login_user'); }
|
|
13
|
+
get inputPassword() { return this.isV2Login ? $('#login-password') : $('#login_cred'); }
|
|
14
|
+
get buttonLogin() { return this.isV2Login ? $('//button[.="Sign in"]') : $('#c-login-btn'); }
|
|
15
|
+
get continueButton() { return $('//button[.="Continue"]'); }
|
|
16
|
+
// Post-login readiness marker. The MFE Navigation Host replaced the old
|
|
17
|
+
// Dashboard (which rendered ExplorePosts → "Explore features") with NewDashboard,
|
|
18
|
+
// so the sidebar shell testid is the reliable signal the home shell has mounted.
|
|
19
|
+
// Old text kept as a backward-compatible fallback for pre-MFE environments.
|
|
20
|
+
get exploreFeatures() { return $('//*[@data-testid="cap-navigation-spa-sidebar"] | //*[text()="Explore features"]'); }
|
|
21
|
+
async doLogin(url, username, password, loginTryCount = 0) {
|
|
22
|
+
try {
|
|
23
|
+
await browser.refresh();
|
|
24
|
+
await browser.pause(3000);
|
|
25
|
+
console.log('Browser launched');
|
|
26
|
+
await super.launchApplication(url);
|
|
27
|
+
if (loginTryCount > 0) {
|
|
28
|
+
let currentUrl = await browser.getUrl();
|
|
29
|
+
const isHome = currentUrl.includes("/home/ui");
|
|
30
|
+
if (isHome) {
|
|
31
|
+
try {
|
|
32
|
+
await (await this.exploreFeatures).waitForDisplayed({ timeout: 60000, interval: 5000 });
|
|
33
|
+
console.log('-----Logged In-----');
|
|
34
|
+
console.log('On retry login flow, /home/ui url and explore features text appeared after 60s probably, hence skipping entering username and password');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.log('proceeding with retry');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i <= 2; i++) {
|
|
43
|
+
try {
|
|
44
|
+
await (await this.inputUserName).waitForDisplayed({ timeout: 60000 });
|
|
45
|
+
await this.inputUserName.waitForEnabled({ timeout: 30000, timeoutMsg: "thrown from dologin", interval: 1000 });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.log('Failed to load login page- Refreshing browser');
|
|
50
|
+
await browser.refresh();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await elementUtil_1.default.enterText(await this.inputUserName, username);
|
|
54
|
+
if (this.isV2Login) {
|
|
55
|
+
await this.continueButton.waitForEnabled({ timeout: 10000 });
|
|
56
|
+
await elementUtil_1.default.elementClick(await this.continueButton);
|
|
57
|
+
await this.inputPassword.waitForDisplayed({ timeout: 30000 });
|
|
58
|
+
}
|
|
59
|
+
await elementUtil_1.default.enterText(await this.inputPassword, password);
|
|
60
|
+
await this.buttonLogin.waitForEnabled({ timeout: 10000 });
|
|
61
|
+
await elementUtil_1.default.elementClick(await this.buttonLogin);
|
|
62
|
+
await browser.waitUntil(async () => {
|
|
63
|
+
let currentUrl = await browser.getUrl();
|
|
64
|
+
return currentUrl.includes("/home/ui");
|
|
65
|
+
}, {
|
|
66
|
+
timeout: 60000,
|
|
67
|
+
timeoutMsg: 'expected url to include /home/ui',
|
|
68
|
+
interval: 1000,
|
|
69
|
+
});
|
|
70
|
+
console.log('-----Browsers current url contains home/ui after login-----');
|
|
71
|
+
await (await this.exploreFeatures).waitForDisplayed({ timeout: 60000, interval: 5000 });
|
|
72
|
+
console.log('-----Logged In-----');
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.log('Throwed error from dologin, attempt #: ', loginTryCount, ', Error: ', error);
|
|
76
|
+
wdio_commands_1.default.logScreenshot("Failed to get the user logged in: " + error);
|
|
77
|
+
if (loginTryCount >= (common_1.maxLoginAttempts - 1)) {
|
|
78
|
+
console.log('Since login retry count exceeds 2. Exiting now.');
|
|
79
|
+
throw new Error("Failed to get the user logged in: " + error);
|
|
80
|
+
}
|
|
81
|
+
console.log('Retrying Login.. ');
|
|
82
|
+
await this.doLogin(url, username, password, ++loginTryCount);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async redirectHome(url) {
|
|
86
|
+
try {
|
|
87
|
+
await super.launchApplication(url);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
wdio_commands_1.default.logScreenshot("Exception logged: " + error);
|
|
91
|
+
throw new Error(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.default = new LoginPage();
|
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const fs = __importStar(require("fs"));
|
|
27
|
+
const path = __importStar(require("path"));
|
|
4
28
|
// A publish section holds the lock for ~1-3 minutes; anything older than this
|
|
5
29
|
// can only be a leftover from a run that died without releasing.
|
|
6
30
|
const STALE_LOCK_TTL_MS = 15 * 60 * 1000;
|
|
7
|
-
|
|
8
31
|
class LockService {
|
|
9
|
-
private baseLockDir: string;
|
|
10
|
-
|
|
11
32
|
constructor() {
|
|
12
33
|
// Lock/signal files default to <this dir>/locks, but can be redirected via LOCK_DIR so that
|
|
13
34
|
// independent suite runs (e.g. staging v3 + nightly v6 in parallel) don't share signal files
|
|
@@ -19,16 +40,13 @@ class LockService {
|
|
|
19
40
|
fs.mkdirSync(this.baseLockDir, { recursive: true });
|
|
20
41
|
}
|
|
21
42
|
}
|
|
22
|
-
|
|
23
43
|
initialize() {
|
|
24
44
|
this.cleanStaleLocks();
|
|
25
45
|
console.log("Lock service initialized.");
|
|
26
46
|
}
|
|
27
|
-
|
|
28
|
-
private getLockFilePath(lockName: string): string {
|
|
47
|
+
getLockFilePath(lockName) {
|
|
29
48
|
return path.resolve(this.baseLockDir, `${lockName}.lock`);
|
|
30
49
|
}
|
|
31
|
-
|
|
32
50
|
/**
|
|
33
51
|
* A lock is stale when its owner can no longer release it: the owning worker
|
|
34
52
|
* process is dead (identifier format Test-<pid>-<timestamp>), or the file is
|
|
@@ -39,66 +57,62 @@ class LockService {
|
|
|
39
57
|
* other. Locks owned by a live process are never touched, so a concurrently
|
|
40
58
|
* running suite on this machine keeps its serialization.
|
|
41
59
|
*/
|
|
42
|
-
|
|
43
|
-
let owner
|
|
44
|
-
let mtimeMs
|
|
60
|
+
isLockStale(lockFilePath) {
|
|
61
|
+
let owner;
|
|
62
|
+
let mtimeMs;
|
|
45
63
|
try {
|
|
46
64
|
owner = fs.readFileSync(lockFilePath, 'utf8');
|
|
47
65
|
mtimeMs = fs.statSync(lockFilePath).mtimeMs;
|
|
48
|
-
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
49
68
|
return false; // vanished meanwhile — nothing to clean
|
|
50
69
|
}
|
|
51
|
-
|
|
70
|
+
// Probe the owner's liveness FIRST — a live owner must never lose its lock,
|
|
71
|
+
// regardless of age (slow cluster, retried publish, debug SETTLE_MS pauses).
|
|
72
|
+
// The TTL only applies when the owner is dead, unknown, or a reused pid.
|
|
52
73
|
const match = /^Test-(\d+)-\d+$/.exec(owner.trim());
|
|
53
|
-
if (!match)
|
|
74
|
+
if (!match)
|
|
75
|
+
return Date.now() - mtimeMs > STALE_LOCK_TTL_MS; // unknown format — TTL only
|
|
54
76
|
try {
|
|
55
77
|
process.kill(Number(match[1]), 0); // signal 0 = liveness probe, sends nothing
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
return false; // owner alive — never steal
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// ESRCH: no such process → stale. EPERM: exists but not ours → TTL fallback (pid reuse).
|
|
82
|
+
if (error.code === 'EPERM') {
|
|
83
|
+
return Date.now() - mtimeMs > STALE_LOCK_TTL_MS;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
removeIfStale(lockFilePath) {
|
|
89
|
+
if (!this.isLockStale(lockFilePath))
|
|
90
|
+
return false;
|
|
65
91
|
try {
|
|
66
92
|
fs.unlinkSync(lockFilePath);
|
|
67
93
|
console.log(`Removed stale lock '${path.basename(lockFilePath)}' (owner dead or TTL expired).`);
|
|
68
94
|
return true;
|
|
69
|
-
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
70
97
|
return false; // another worker removed it first — fine
|
|
71
98
|
}
|
|
72
99
|
}
|
|
73
|
-
|
|
74
100
|
/** Sweep all *.lock files in the lock dir, removing only stale ones. */
|
|
75
|
-
cleanStaleLocks()
|
|
76
|
-
let lockFiles
|
|
101
|
+
cleanStaleLocks() {
|
|
102
|
+
let lockFiles;
|
|
77
103
|
try {
|
|
78
104
|
lockFiles = fs.readdirSync(this.baseLockDir).filter(file => file.endsWith('.lock'));
|
|
79
|
-
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
80
107
|
return;
|
|
81
108
|
}
|
|
82
109
|
for (const file of lockFiles) {
|
|
83
110
|
this.removeIfStale(path.resolve(this.baseLockDir, file));
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
|
-
|
|
87
|
-
async acquireLock({
|
|
88
|
-
testIdentifier,
|
|
89
|
-
lockName,
|
|
90
|
-
maxRetries = 10,
|
|
91
|
-
retryInterval = 1000
|
|
92
|
-
}: {
|
|
93
|
-
testIdentifier: string;
|
|
94
|
-
lockName: string;
|
|
95
|
-
maxRetries?: number;
|
|
96
|
-
retryInterval?: number;
|
|
97
|
-
}): Promise<boolean> {
|
|
98
|
-
|
|
113
|
+
async acquireLock({ testIdentifier, lockName, maxRetries = 10, retryInterval = 1000 }) {
|
|
99
114
|
const lockFilePath = this.getLockFilePath(lockName);
|
|
100
|
-
|
|
101
|
-
for (let retries = 0; ; retries++) {
|
|
115
|
+
for (let retries = 0;; retries++) {
|
|
102
116
|
// Self-heal locks left by crashed workers, then try an atomic
|
|
103
117
|
// create-exclusive: 'wx' fails with EEXIST if the file exists, so two
|
|
104
118
|
// waiters can never both believe they own the lock.
|
|
@@ -107,17 +121,18 @@ class LockService {
|
|
|
107
121
|
fs.writeFileSync(lockFilePath, testIdentifier, { flag: 'wx' });
|
|
108
122
|
console.log(`Lock for '${lockName}' acquired by ${testIdentifier}.`);
|
|
109
123
|
return true;
|
|
110
|
-
}
|
|
111
|
-
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== 'EEXIST') {
|
|
112
127
|
console.error(`Failed to acquire lock for '${lockName}':`, error);
|
|
113
128
|
return false;
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
|
-
|
|
117
131
|
let currentLockOwner = 'unknown';
|
|
118
132
|
try {
|
|
119
133
|
currentLockOwner = fs.readFileSync(lockFilePath, 'utf8');
|
|
120
|
-
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
121
136
|
// released between attempts — next loop iteration will grab it
|
|
122
137
|
}
|
|
123
138
|
if (retries >= maxRetries) {
|
|
@@ -128,47 +143,40 @@ class LockService {
|
|
|
128
143
|
await browser.pause(retryInterval);
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
|
-
|
|
132
|
-
releaseLock(testIdentifier: string, lockName: string): boolean {
|
|
146
|
+
releaseLock(testIdentifier, lockName) {
|
|
133
147
|
const lockFilePath = this.getLockFilePath(lockName);
|
|
134
|
-
|
|
135
148
|
if (!fs.existsSync(lockFilePath)) {
|
|
136
149
|
console.log(`Lock for '${lockName}' is not acquired, nothing to release.`);
|
|
137
150
|
return false;
|
|
138
151
|
}
|
|
139
|
-
|
|
140
152
|
const currentLockOwner = fs.readFileSync(lockFilePath, 'utf8');
|
|
141
|
-
|
|
142
153
|
if (currentLockOwner !== testIdentifier) {
|
|
143
154
|
console.log(`Lock for '${lockName}' cannot be released. Current owner is ${currentLockOwner}, but ${testIdentifier} is trying to release it.`);
|
|
144
155
|
return false;
|
|
145
156
|
}
|
|
146
|
-
|
|
147
157
|
try {
|
|
148
158
|
fs.unlinkSync(lockFilePath);
|
|
149
159
|
console.log(`Lock for '${lockName}' released by ${testIdentifier}.`);
|
|
150
160
|
return true;
|
|
151
|
-
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
152
163
|
console.error(`Failed to release lock for '${lockName}':`, error);
|
|
153
164
|
return false;
|
|
154
165
|
}
|
|
155
166
|
}
|
|
156
|
-
|
|
157
|
-
isLocked(lockName: string): boolean {
|
|
167
|
+
isLocked(lockName) {
|
|
158
168
|
const lockFilePath = this.getLockFilePath(lockName);
|
|
159
169
|
return fs.existsSync(lockFilePath);
|
|
160
170
|
}
|
|
161
|
-
|
|
162
171
|
/**
|
|
163
172
|
* Writes a signal file so another worker can detect completion.
|
|
164
173
|
* The file persists until the next test run overwrites it.
|
|
165
174
|
*/
|
|
166
|
-
writeSignal(signalName
|
|
175
|
+
writeSignal(signalName, value = "ready") {
|
|
167
176
|
const filePath = path.resolve(this.baseLockDir, `${signalName}.signal`);
|
|
168
177
|
fs.writeFileSync(filePath, value);
|
|
169
178
|
console.log(`Signal '${signalName}' written (value: ${value})`);
|
|
170
179
|
}
|
|
171
|
-
|
|
172
180
|
/**
|
|
173
181
|
* Polls for a signal file written by another worker via writeSignal().
|
|
174
182
|
* Skips "pending" values — waits until a terminal value (ready/skipped/failed) is written.
|
|
@@ -178,15 +186,10 @@ class LockService {
|
|
|
178
186
|
* `ENOENT`) instead of a preceding `fs.existsSync` check, to avoid a TOCTOU
|
|
179
187
|
* race condition.
|
|
180
188
|
*/
|
|
181
|
-
async waitForSignal(
|
|
182
|
-
signalName: string,
|
|
183
|
-
timeoutMs: number = 300000,
|
|
184
|
-
intervalMs: number = 5000,
|
|
185
|
-
): Promise<string> {
|
|
189
|
+
async waitForSignal(signalName, timeoutMs = 300000, intervalMs = 5000) {
|
|
186
190
|
const filePath = path.resolve(this.baseLockDir, `${signalName}.signal`);
|
|
187
191
|
const deadline = Date.now() + timeoutMs;
|
|
188
192
|
let elapsed = 0;
|
|
189
|
-
|
|
190
193
|
while (Date.now() < deadline) {
|
|
191
194
|
if (fs.existsSync(filePath)) {
|
|
192
195
|
const value = fs.readFileSync(filePath, 'utf8');
|
|
@@ -195,16 +198,14 @@ class LockService {
|
|
|
195
198
|
return value;
|
|
196
199
|
}
|
|
197
200
|
console.log(`Signal '${signalName}' is pending — waiting for terminal value... (${elapsed}ms elapsed)`);
|
|
198
|
-
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
199
203
|
console.log(`Waiting for signal '${signalName}'... (${elapsed}ms elapsed, timeout: ${timeoutMs}ms)`);
|
|
200
204
|
}
|
|
201
205
|
await browser.pause(intervalMs);
|
|
202
206
|
elapsed += intervalMs;
|
|
203
207
|
}
|
|
204
|
-
|
|
205
208
|
throw new Error(`Signal '${signalName}' not received within ${timeoutMs}ms`);
|
|
206
209
|
}
|
|
207
|
-
|
|
208
210
|
}
|
|
209
|
-
|
|
210
|
-
export default new LockService();
|
|
211
|
+
exports.default = new LockService();
|
|
File without changes
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
3
|
/**
|
|
2
4
|
* antd-version detection for the antd v3/v5 -> v6 (new "unified" lib) migration.
|
|
3
5
|
*
|
|
@@ -13,25 +15,7 @@
|
|
|
13
15
|
* detect() below — change only that method if the marker ever moves.
|
|
14
16
|
*/
|
|
15
17
|
class AntdVersionUtil {
|
|
16
|
-
|
|
17
|
-
private _isV6: boolean | undefined;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Detect whether the app under test is the new antd-v6 build, and cache it for
|
|
21
|
-
* the rest of the worker. Apps do not use antd directly — they render through
|
|
22
|
-
* the internal `cap-ui-library` CSS bundle, of which exactly ONE version (old
|
|
23
|
-
* or new=v6) is loaded per page. So the loaded `cap-ui-library` stylesheet IS
|
|
24
|
-
* the app's version. The new (v6) lib ships an `ant-row-legacy` selector that
|
|
25
|
-
* the old lib never does, so a single same-origin CSS scan is an exact,
|
|
26
|
-
* render-independent discriminator. The bundle is served from the same pod
|
|
27
|
-
* (same origin), so `cssRules` is always readable (no CORS).
|
|
28
|
-
*/
|
|
29
|
-
// Stylesheet whose presence means the internal UI lib (old OR new) has loaded.
|
|
30
|
-
private static readonly LIB_HREF_MARKER = 'cap-ui-library';
|
|
31
|
-
// Selector shipped only by the new (v6) lib; absent from the old lib's CSS.
|
|
32
|
-
private static readonly V6_CSS_MARKER = 'ant-row-legacy';
|
|
33
|
-
|
|
34
|
-
async detect(): Promise<boolean> {
|
|
18
|
+
async detect() {
|
|
35
19
|
// Runtime detection is DISABLED. selector() now always emits a combined
|
|
36
20
|
// v6-first selector, so we no longer need to know the version. This stub
|
|
37
21
|
// stays callable (wdio.conf's navigateTo hook and a campaigns getter call
|
|
@@ -123,7 +107,6 @@ class AntdVersionUtil {
|
|
|
123
107
|
return this._isV6;
|
|
124
108
|
--------------------------------------------------------------------- */
|
|
125
109
|
}
|
|
126
|
-
|
|
127
110
|
/**
|
|
128
111
|
* Navigation hook (wired into a global `navigateTo` override in wdio.conf).
|
|
129
112
|
* The antd version is a property of the PAGE that is loaded, not of the worker
|
|
@@ -135,14 +118,13 @@ class AntdVersionUtil {
|
|
|
135
118
|
* switcher), so one of the detection markers is always present and the probe
|
|
136
119
|
* resolves quickly rather than waiting out the timeout.
|
|
137
120
|
*/
|
|
138
|
-
async onNavigate()
|
|
121
|
+
async onNavigate() {
|
|
139
122
|
// No-op while detection is disabled. selector() emits both variants, so
|
|
140
123
|
// there is nothing to re-detect on navigation. Restore the two lines below
|
|
141
124
|
// if runtime detection is re-enabled in detect().
|
|
142
125
|
// this.reset();
|
|
143
126
|
// await this.detect();
|
|
144
127
|
}
|
|
145
|
-
|
|
146
128
|
/**
|
|
147
129
|
* Version flag for the FEW call sites that branch on the version directly
|
|
148
130
|
* (different interaction model, not just a different locator) rather than
|
|
@@ -151,10 +133,9 @@ class AntdVersionUtil {
|
|
|
151
133
|
* those branches take the v6 path. If the env is reverted to legacy, flip
|
|
152
134
|
* this back (or restore detection in detect() and return this._isV6).
|
|
153
135
|
*/
|
|
154
|
-
get isV6()
|
|
136
|
+
get isV6() {
|
|
155
137
|
return true;
|
|
156
138
|
}
|
|
157
|
-
|
|
158
139
|
/**
|
|
159
140
|
* Build one combined selector that matches EITHER the v6 or the legacy
|
|
160
141
|
* variant, with the v6 variant placed FIRST. Runtime version detection is
|
|
@@ -171,20 +152,31 @@ class AntdVersionUtil {
|
|
|
171
152
|
* the `,` list. A page is either v6 or legacy, so only one side ever matches;
|
|
172
153
|
* ordering v6 first just expresses the preference.
|
|
173
154
|
*/
|
|
174
|
-
selector(legacy
|
|
175
|
-
const isXPath = (s
|
|
155
|
+
selector(legacy, v6) {
|
|
156
|
+
const isXPath = (s) => /^\s*(\/|\.\/|\.\.\/|\()/.test(s);
|
|
176
157
|
return isXPath(v6) ? `${v6} | ${legacy}` : `${v6}, ${legacy}`;
|
|
177
158
|
}
|
|
178
|
-
|
|
179
159
|
/** Resolve a legacy/v6 selector pair to a live element using the picked one. */
|
|
180
|
-
element(legacy
|
|
160
|
+
element(legacy, v6) {
|
|
181
161
|
return $(this.selector(legacy, v6));
|
|
182
162
|
}
|
|
183
|
-
|
|
184
163
|
/** Test hook: clear the cached detection (not used in normal runs). */
|
|
185
|
-
reset()
|
|
164
|
+
reset() {
|
|
186
165
|
this._isV6 = undefined;
|
|
187
166
|
}
|
|
188
167
|
}
|
|
189
|
-
|
|
190
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Detect whether the app under test is the new antd-v6 build, and cache it for
|
|
170
|
+
* the rest of the worker. Apps do not use antd directly — they render through
|
|
171
|
+
* the internal `cap-ui-library` CSS bundle, of which exactly ONE version (old
|
|
172
|
+
* or new=v6) is loaded per page. So the loaded `cap-ui-library` stylesheet IS
|
|
173
|
+
* the app's version. The new (v6) lib ships an `ant-row-legacy` selector that
|
|
174
|
+
* the old lib never does, so a single same-origin CSS scan is an exact,
|
|
175
|
+
* render-independent discriminator. The bundle is served from the same pod
|
|
176
|
+
* (same origin), so `cssRules` is always readable (no CORS).
|
|
177
|
+
*/
|
|
178
|
+
// Stylesheet whose presence means the internal UI lib (old OR new) has loaded.
|
|
179
|
+
AntdVersionUtil.LIB_HREF_MARKER = 'cap-ui-library';
|
|
180
|
+
// Selector shipped only by the new (v6) lib; absent from the old lib's CSS.
|
|
181
|
+
AntdVersionUtil.V6_CSS_MARKER = 'ant-row-legacy';
|
|
182
|
+
exports.default = new AntdVersionUtil();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
3
|
/**
|
|
2
4
|
* Env-driven kill-switch that lets a module's automation be turned off without a
|
|
3
5
|
* code change or a CI job edit. In prod the value is injected into the runner
|
|
@@ -20,9 +22,10 @@ class AutomationBypass {
|
|
|
20
22
|
* must never break a run, so anything unparseable is logged and treated as
|
|
21
23
|
* "nothing bypassed".
|
|
22
24
|
*/
|
|
23
|
-
|
|
25
|
+
getBypassedModules() {
|
|
24
26
|
const raw = (process.env.AUTOMATION_BYPASS_MODULES || '').trim();
|
|
25
|
-
if (!raw)
|
|
27
|
+
if (!raw)
|
|
28
|
+
return [];
|
|
26
29
|
try {
|
|
27
30
|
const parsed = JSON.parse(raw);
|
|
28
31
|
if (!Array.isArray(parsed)) {
|
|
@@ -30,17 +33,17 @@ class AutomationBypass {
|
|
|
30
33
|
return [];
|
|
31
34
|
}
|
|
32
35
|
return parsed.map((m) => String(m).trim()).filter(Boolean);
|
|
33
|
-
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
34
38
|
console.log(`[automation-bypass] Could not parse AUTOMATION_BYPASS_MODULES (${raw}): ${error}`);
|
|
35
39
|
return [];
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
|
-
|
|
39
42
|
/** True when the given module's automation should be bypassed. */
|
|
40
|
-
isBypassed(module
|
|
41
|
-
if (!module)
|
|
43
|
+
isBypassed(module = process.env.module) {
|
|
44
|
+
if (!module)
|
|
45
|
+
return false;
|
|
42
46
|
return this.getBypassedModules().includes(module);
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
export default new AutomationBypass();
|
|
49
|
+
exports.default = new AutomationBypass();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
3
|
/**
|
|
2
4
|
* Central gate for the FULL_DEBUG capture feature (request-payload + screenshot
|
|
3
5
|
* recording) and, indirectly, for whether the devtools automation protocol is
|
|
@@ -13,20 +15,20 @@
|
|
|
13
15
|
*/
|
|
14
16
|
class DebugMode {
|
|
15
17
|
/** Parse DEBUG_MODULES into a trimmed, non-empty module-name list. */
|
|
16
|
-
|
|
18
|
+
getDebugModules() {
|
|
17
19
|
return (process.env.DEBUG_MODULES || '')
|
|
18
20
|
.split(',')
|
|
19
21
|
.map((m) => m.trim())
|
|
20
22
|
.filter(Boolean);
|
|
21
23
|
}
|
|
22
|
-
|
|
23
24
|
/** True when request/screenshot capture should run for the current module. */
|
|
24
|
-
isCaptureEnabled()
|
|
25
|
-
if (process.env.FULL_DEBUG_MODE !== 'true')
|
|
25
|
+
isCaptureEnabled() {
|
|
26
|
+
if (process.env.FULL_DEBUG_MODE !== 'true')
|
|
27
|
+
return false;
|
|
26
28
|
const modules = this.getDebugModules();
|
|
27
|
-
if (modules.length === 0)
|
|
29
|
+
if (modules.length === 0)
|
|
30
|
+
return true;
|
|
28
31
|
return modules.includes(process.env.module || '');
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
export default new DebugMode();
|
|
34
|
+
exports.default = new DebugMode();
|