@capillarytech/cap-ui-utils 3.1.1 → 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.
Files changed (42) hide show
  1. package/e2e/constants/common.js +22 -0
  2. package/e2e/index.js +65 -0
  3. package/e2e/pages/common/base.page.js +13 -0
  4. package/e2e/pages/common/constant.js +5 -0
  5. package/e2e/pages/common/login.page.js +95 -0
  6. package/e2e/services/{lockService.ts → lockService.js} +63 -67
  7. package/e2e/utils/{antdVersionUtil.ts → antdVersionUtil.js} +24 -32
  8. package/e2e/utils/{automationBypassUtil.ts → automationBypassUtil.js} +11 -8
  9. package/e2e/utils/{debugModeUtil.ts → debugModeUtil.js} +9 -7
  10. package/e2e/utils/deletionRegistry.js +16 -0
  11. package/e2e/utils/{elementUtil.ts → elementUtil.js} +224 -296
  12. package/e2e/utils/expectUtil.js +40 -0
  13. package/e2e/utils/featureFlagUtil.js +25 -0
  14. package/e2e/utils/garudaDropdownUtil.js +34 -0
  15. package/e2e/utils/htmlEditorUtil.js +59 -0
  16. package/e2e/utils/logCollectorUtil.js +65 -0
  17. package/e2e/utils/{mockResponse.ts → mockResponse.js} +5 -2
  18. package/e2e/utils/{reportUploader.ts → reportUploader.js} +69 -66
  19. package/e2e/utils/{requestRecorderUtil.ts → requestRecorderUtil.js} +69 -97
  20. package/e2e/utils/{screenshotRecorderUtil.ts → screenshotRecorderUtil.js} +61 -82
  21. package/e2e/utils/setupMock.js +18 -0
  22. package/e2e/utils/unmatchedBracesUtil.js +87 -0
  23. package/e2e/utils/uploaders/fileServiceUploader.js +113 -0
  24. package/e2e/utils/uploaders/uploader.js +2 -0
  25. package/e2e/utils/virtualListUtil.js +74 -0
  26. package/package.json +1 -1
  27. package/e2e/constants/common.ts +0 -23
  28. package/e2e/index.ts +0 -30
  29. package/e2e/pages/common/base.page.ts +0 -11
  30. package/e2e/pages/common/constant.ts +0 -2
  31. package/e2e/pages/common/login.page.ts +0 -95
  32. package/e2e/utils/deletionRegistry.ts +0 -15
  33. package/e2e/utils/expectUtil.ts +0 -19
  34. package/e2e/utils/featureFlagUtil.ts +0 -30
  35. package/e2e/utils/garudaDropdownUtil.ts +0 -60
  36. package/e2e/utils/htmlEditorUtil.ts +0 -56
  37. package/e2e/utils/logCollectorUtil.ts +0 -59
  38. package/e2e/utils/setupMock.ts +0 -29
  39. package/e2e/utils/unmatchedBracesUtil.ts +0 -101
  40. package/e2e/utils/uploaders/fileServiceUploader.ts +0 -84
  41. package/e2e/utils/uploaders/uploader.ts +0 -20
  42. package/e2e/utils/virtualListUtil.ts +0 -115
@@ -1,101 +0,0 @@
1
- import elementUtil from "./elementUtil";
2
-
3
- /** Invalid snippet used to trigger unmatched-braces validation across Engage and Adiona flows. */
4
- export const UNMATCHED_BRACES_SNIPPET = "{{ my_standard_tag";
5
-
6
- /** Unsupported/unrecognised tag used to trigger the send-for-approval server-side validation error. */
7
- export const UNSUPPORTED_TAG = "{{my_invalid_standard_tag}}";
8
-
9
- /** Full footer / inline error copy for standard-tag validation (SMS, MPush, Web Push, etc.). */
10
- export const INVALID_LABEL_ERROR_TEXT = "Invalid label, please close all curly braces";
11
-
12
- /**
13
- * Footer error container for "Invalid label…" type errors (SMS, Journey SMS).
14
- * XPath: //div[contains(@class,'error-container')]//div[text()='<message>']
15
- */
16
- export function invalidLabelErrorFooterContainer(errorMessage: string) {
17
- return $(
18
- `//div[contains(@class,'error-container')]//div[text()='${errorMessage}']`
19
- );
20
- }
21
-
22
- /**
23
- * Inline error span for "Invalid label…" (Viber, Zalo, Journey Zalo).
24
- * XPath: //span[contains(@class,'error-message') and contains(.,'Invalid label')]
25
- */
26
- export function invalidLabelErrorSpan() {
27
- return $("//span[contains(@class,'error-message') and contains(.,'Invalid label')]");
28
- }
29
-
30
- /**
31
- * Validation error span below a component-with-label input (MPush Title / Message).
32
- * XPath: //div[contains(@class,'component-with-label-label') and normalize-space()='<label>']
33
- * /ancestor::div[contains(@class,'component-with-label')]//span[contains(@class,'error-message')]
34
- * Use for channels where the error appears inline under the input rather than in the footer.
35
- */
36
- export function componentWithLabelErrorSpan(label: string) {
37
- return $(
38
- `//div[contains(@class,'component-with-label-label') and normalize-space()='${label}']/ancestor::div[contains(@class,'component-with-label')]//span[contains(@class,'error-message')]`
39
- );
40
- }
41
-
42
- /**
43
- * CodeMirror email editor: click the last cm-line at a stable left-edge offset to acquire focus,
44
- * wait for the editor to be focused, then press End + Enter to position the cursor on a new last line.
45
- * Call this before typing a snippet into the editor.
46
- *
47
- * @param codeMirrorEditor The CodeMirror content-editable div (aria-placeholder contains "Write your HTML email code here").
48
- * @param codeMirrorLastLine The last `.cm-line` div inside that editor.
49
- */
50
- export async function focusEmailEditorAddNewLineAtEnd(
51
- codeMirrorEditor: WebdriverIO.Element,
52
- codeMirrorLastLine: WebdriverIO.Element
53
- ): Promise<void> {
54
- await codeMirrorEditor.waitForDisplayed({ timeout: 90000 });
55
- await codeMirrorLastLine.waitForDisplayed({ timeout: 10000 });
56
- const size = await codeMirrorLastLine.getSize();
57
- const xOffset = Math.round(2 - size.width / 2);
58
- await codeMirrorLastLine.click({ x: xOffset, y: 0 });
59
- await browser.waitUntil(
60
- async () => {
61
- const focused = await browser.execute(() => {
62
- const codeMirrorEditorEl = document.querySelector(
63
- 'div[aria-placeholder*="Write your HTML email code here"]'
64
- );
65
- if (!codeMirrorEditorEl) return false;
66
- const active = document.activeElement;
67
- const editorFocused = active === codeMirrorEditorEl || (active && codeMirrorEditorEl.contains(active));
68
- const cmEditor = codeMirrorEditorEl.closest(".cm-editor");
69
- const hasCmFocused =
70
- (cmEditor && cmEditor.classList.contains("cm-focused")) ||
71
- (active && active.classList.contains("cm-focused"));
72
- return !!(editorFocused || hasCmFocused);
73
- });
74
- return focused === true;
75
- },
76
- {
77
- timeout: 10000,
78
- timeoutMsg: "CodeMirror editor did not receive focus after clicking last line",
79
- interval: 100,
80
- }
81
- );
82
- await browser.keys(["End"]);
83
- await browser.keys(["Enter"]);
84
- }
85
-
86
- /**
87
- * Inject unmatched-braces snippet, try submit (if clickable), assert via callback, then restore field.
88
- * Not used for Email (CodeMirror debounce / waitUntil sequencing is bespoke).
89
- */
90
- export async function injectAndRestore(
91
- targetInput: WebdriverIO.Element,
92
- submitButton: WebdriverIO.Element,
93
- originalValue: string,
94
- assertFn: () => Promise<void>
95
- ): Promise<void> {
96
- await elementUtil.enterText(targetInput, UNMATCHED_BRACES_SNIPPET);
97
- await elementUtil.clickIfClickable(submitButton);
98
- await assertFn();
99
- await elementUtil.clearByBackspace(UNMATCHED_BRACES_SNIPPET, targetInput);
100
- await elementUtil.typeText(targetInput, originalValue);
101
- }
@@ -1,84 +0,0 @@
1
- import * as fs from 'fs';
2
- import axios from 'axios';
3
- import { Uploader } from './uploader';
4
-
5
- /**
6
- * Uploads a file to Capillary's internal File Service, mirroring the
7
- * `uploadData` flow in arya's arya-fileservice-sdk: a raw-binary POST to
8
- * POST {baseUrl}v2/{namespace}/?fileName={name}&fileNameEncoding=false
9
- * with the file bytes as the body. The service stores the object (in S3 under
10
- * the hood) and returns JSON containing file_path / secure_file_path.
11
- *
12
- * Config via env vars:
13
- * FILE_SERVICE_URL - base URL. Defaults to the in-cluster DNS
14
- * http://file-service.default/ which is ONLY
15
- * reachable from inside the Capillary cluster.
16
- * Set this to a reachable host when running
17
- * outside the cluster.
18
- * FILE_SERVICE_NAMESPACE - target namespace (must be configured on the
19
- * file-service side to allow the archive's
20
- * extension/mime-type and size). Defaults to
21
- * 'import', which is provisioned on the crm
22
- * clusters and permits .zip (it stores under a
23
- * generated UUID key, so use the returned path).
24
- * FILE_SERVICE_ORG_ID - optional org id (X-CAP-API-AUTH-ORG-ID header)
25
- * FILE_SERVICE_PUBLIC - "false" to upload as PRIVATE (default PUBLIC)
26
- *
27
- * Note: the arya SDK sends no auth header — the service relies on in-cluster
28
- * network isolation. There is no external/authenticated route here.
29
- */
30
- export class FileServiceUploader implements Uploader {
31
- readonly name = 'Capillary File Service';
32
- private baseUrl = process.env.FILE_SERVICE_URL || 'http://file-service.default/';
33
- // Defaults to the 'import' namespace: it is provisioned on the crm clusters
34
- // (unlike 'default', which does NOT exist server-side and 500s with
35
- // NoSuchNamespaceException) and permits .zip uploads. Note it stores under a
36
- // server-generated UUID key, so retrieve via the returned file_path, not the
37
- // original filename. Override per env with FILE_SERVICE_NAMESPACE.
38
- private namespace = process.env.FILE_SERVICE_NAMESPACE || 'import';
39
- private orgId = process.env.FILE_SERVICE_ORG_ID || '';
40
- private isPublic = process.env.FILE_SERVICE_PUBLIC !== 'false';
41
- // Hard network timeout so an unresponsive file-service can never hang the
42
- // run's onComplete hook (which would keep the pod alive forever).
43
- private timeoutMs = Number(process.env.FILE_SERVICE_TIMEOUT_MS) || 60000;
44
-
45
- isConfigured(): boolean {
46
- return Boolean(this.baseUrl && this.namespace);
47
- }
48
-
49
- missingConfigMessage(): string {
50
- const missing: string[] = [];
51
- if (!this.baseUrl) missing.push('FILE_SERVICE_URL');
52
- if (!this.namespace) missing.push('FILE_SERVICE_NAMESPACE');
53
- return missing.join(', ');
54
- }
55
-
56
- async upload(localFilePath: string, remoteName: string): Promise<string> {
57
- const base = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`;
58
- const url = `${base}v2/${this.namespace}/?fileName=${encodeURI(remoteName)}&fileNameEncoding=false`;
59
-
60
- const headers: Record<string, string> = { 'Content-Type': 'application/zip' };
61
- if (this.isPublic) headers['X-CAP-FS-ACL'] = 'PUBLIC';
62
- if (this.orgId) headers['X-CAP-API-AUTH-ORG-ID'] = String(this.orgId);
63
-
64
- const body = fs.readFileSync(localFilePath);
65
- const res = await axios.post(url, body, {
66
- headers,
67
- maxBodyLength: Infinity,
68
- maxContentLength: Infinity,
69
- timeout: this.timeoutMs,
70
- });
71
-
72
- // The file-service returns HTTP 200 even when the upload fails (e.g.
73
- // NoSuchNamespaceException), carrying the error in the JSON body. So a
74
- // 200 is NOT proof of success — only a real file path is. Treat a body
75
- // without one as a failure so the run doesn't silently report success.
76
- const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
77
- const location = data?.secure_file_path || data?.file_path || data?.file?.s3_token;
78
- if (!location) {
79
- const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data);
80
- throw new Error(`file-service returned no file path (namespace='${this.namespace}'): ${body}`);
81
- }
82
- return location;
83
- }
84
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Destination-agnostic uploader contract. Implement this for each backend
3
- * (Google Drive now, S3 later) so the orchestrator never changes.
4
- */
5
- export interface Uploader {
6
- /** Human-readable backend name, used in logs. */
7
- readonly name: string;
8
-
9
- /** True when all required credentials/config are present. */
10
- isConfigured(): boolean;
11
-
12
- /** Describes which config is missing (for a helpful log when not configured). */
13
- missingConfigMessage(): string;
14
-
15
- /**
16
- * Upload a local file and return a human-readable location
17
- * (URL or id) for logging.
18
- */
19
- upload(localFilePath: string, remoteName: string): Promise<string>;
20
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * Helpers for antd v6 virtualized lists (`rc-virtual-list`).
3
- *
4
- * The list renders only a window of DOM rows at a time. A row that exists in the data
5
- * model but is outside that window has no DOM node, so `waitForDisplayed` / `click`
6
- * through WDIO never sees it. These helpers drive the scroll container in the browser
7
- * until the row is rendered, then click the target directly.
8
- *
9
- * Works for both `ant-select-tree` (TreeSelect) and `ant-select-item-option` (Select).
10
- */
11
-
12
- interface ScrollToOptions {
13
- /** CSS selector of a container that scopes the search (popover root). If omitted, document. */
14
- containerSelector?: string;
15
- /** Click the checkbox inside the row instead of the row itself. */
16
- clickCheckbox?: boolean;
17
- /** Overall timeout in ms. */
18
- timeout?: number;
19
- /** Per-iteration pause in ms. */
20
- interval?: number;
21
- }
22
-
23
- /**
24
- * Scroll a virtualized antd list inside `containerSelector` until a row whose trimmed
25
- * textContent equals `label` is rendered, then click it (or its checkbox).
26
- *
27
- * Returns true on success, throws on timeout.
28
- */
29
- export async function scrollAndClickVirtualOption(
30
- label: string,
31
- opts: ScrollToOptions = {},
32
- ): Promise<void> {
33
- const {
34
- containerSelector,
35
- clickCheckbox = true,
36
- timeout = 15000,
37
- interval = 300,
38
- } = opts;
39
-
40
- await browser.waitUntil(
41
- async () => {
42
- return await browser.execute(
43
- (containerSel: string, targetLabel: string, useCheckbox: boolean) => {
44
- const root: ParentNode = containerSel
45
- ? document.querySelector(containerSel) || document
46
- : document;
47
-
48
- const ROW_SELECTOR =
49
- '[role="treeitem"], .ant-select-tree-treenode, .ant-select-item-option';
50
-
51
- const findRow = (): HTMLElement | null => {
52
- const rows = Array.from((root as ParentNode).querySelectorAll(ROW_SELECTOR));
53
- for (const r of rows) {
54
- if ((r.textContent || '').trim() === targetLabel) return r as HTMLElement;
55
- }
56
- return null;
57
- };
58
-
59
- // If already rendered, click and return.
60
- let row = findRow();
61
- if (row) {
62
- const checkbox = useCheckbox
63
- ? (row.querySelector(
64
- '.ant-select-tree-checkbox, .ant-tree-checkbox, .ant-select-item-option-state',
65
- ) as HTMLElement | null)
66
- : null;
67
- (checkbox || row).click();
68
- return true;
69
- }
70
-
71
- // Find the scroll container.
72
- const holder = (root as ParentNode).querySelector(
73
- '.rc-virtual-list-holder, .ant-select-tree-list-holder, .ant-tree-list-holder',
74
- ) as HTMLElement | null;
75
- if (!holder) return false;
76
-
77
- // rc-virtual-list uses the holder's scrollTop to drive the translateY of inner.
78
- // Advance by one viewport (clientHeight) each tick until we either find the row
79
- // or hit the end.
80
- const prev = holder.scrollTop;
81
- holder.scrollTop = Math.min(
82
- holder.scrollHeight,
83
- prev + Math.max(holder.clientHeight, 120),
84
- );
85
- // Dispatch scroll so the virtual list re-renders.
86
- holder.dispatchEvent(new Event('scroll', { bubbles: true }));
87
-
88
- row = findRow();
89
- if (row) {
90
- const checkbox = useCheckbox
91
- ? (row.querySelector(
92
- '.ant-select-tree-checkbox, .ant-tree-checkbox, .ant-select-item-option-state',
93
- ) as HTMLElement | null)
94
- : null;
95
- (checkbox || row).click();
96
- return true;
97
- }
98
-
99
- // Reached the bottom without finding it? Caller will timeout if so.
100
- return holder.scrollTop >= holder.scrollHeight - holder.clientHeight ? false : false;
101
- },
102
- containerSelector ?? '',
103
- label,
104
- clickCheckbox,
105
- );
106
- },
107
- {
108
- timeout,
109
- interval,
110
- timeoutMsg: `Virtualized option "${label}" not found within ${timeout}ms (container=${containerSelector ?? 'document'})`,
111
- },
112
- );
113
- }
114
-
115
- export default { scrollAndClickVirtualOption };