mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
### **User description** - https://github.com/openobserve/openobserve/issues/6505 - https://github.com/openobserve/openobserve/issues/9196 Design at: https://github.com/openobserve/designs/blob/main/dashboards/variable-scoping/variable-scoping-design.md ___ ### **PR Type** Enhancement, Tests ___ ### **Description** - Introduce `useVariablesManager` for scoped variables - Manages global, tab, panel scopes with lazy loading - Builds and validates dependency graph, detects cycles - Commits live changes on refresh, syncs with URL - Extend `variablesDependencyUtils.ts` with scoped graph - Export `buildScopedDependencyGraph`, `detectCyclesInScopedGraph` - Make `extractVariableNames` optional filter - Update PanelContainer tests - Mock services, test refresh/delete/move/duplicate logic - Cover view-only, theme, error, emission scenarios - Enhance VariablesValueSelector tests - Wait for async init, test multi-select, search, streaming handlers - Add manager mode, add-button, edge-case scenarios ___ ### Diagram Walkthrough ```mermaid flowchart LR A["Variable Config"] --> B["expandVariablesForScopes"] B --> C["buildScopedDependencyGraph"] C --> D["initialize state (global/tabs/panels)"] D --> E["lazy load on visibility & dependency"] E --> F["commitAll on Refresh"] F --> G["Panels consume committed state"] ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>useVariablesManager.ts</strong><dd><code>Add variables manager composable for scoped loading</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-95f6bdeb96f8454bdff53bb60cafe6851b0cda450591813dd4dfb2038c4f3d50">+1124/-0</a></td> </tr> <tr> <td><strong>variablesDependencyUtils.ts</strong><dd><code>Implement scoped dependency graph & cycle detection</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-58a77094b6971705fb63bde853a79e17b4e2e85240694c4017aada1d01cf1fb4">+280/-4</a> </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>3 files</summary><table> <tr> <td><strong>PanelContainer.spec.ts</strong><dd><code>Extend PanelContainer tests for panel actions</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-d74ba5293a041ca029d5e9b8fec08d187e51a099451ab0372747d3ec175559a9">+925/-10</a></td> </tr> <tr> <td><strong>VariablesValueSelector.spec.ts</strong><dd><code>Enhance VariablesValueSelector async & streaming tests</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-9f234d5a15777d62cf8cd6cc9f0d68fbc0419a87ef58fbdb11de8c46ed6d17cb">+716/-26</a></td> </tr> <tr> <td><strong>usePanelDataLoader.spec.ts</strong><dd><code>Ensure variable readiness in panel data loader tests</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-f8306bde2ca2b3902674ef12091f2867c7e8b13d4d59d39d4d4152704326c09a">+53/-3</a> </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>68 files</summary><table> <tr> <td><strong>playwright.yml</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-7afcd2d8f7b49bda74843f209eefb7b2da45f7e7803bf2e4bd636699b76aa2d3">+15/-2</a> </td> </tr> <tr> <td><strong>mod.rs</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-cbf3fe971525ba58892400391dfd7dac7e43d6049d6b4f5a9840ad104ebc5e08">+6/-0</a> </td> </tr> <tr> <td><strong>dashboard-panel-actions.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-d29713fdcc11cbc93bda29da5261da389226cd718e4292a1c2cd96629f573a80">+2/-2</a> </td> </tr> <tr> <td><strong>dashboard-settings.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-1a07f3f14dacb06180bc02437804d175886aeacda98f5b0ef507a274e5ddd485">+51/-11</a> </td> </tr> <tr> <td><strong>dashboard-variables-scoped.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-86abb9f90fabd982dedfdd9a21a052a47b6984edc7bd29a51ee104c8c473aec4">+682/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-67d38d39bb67f4a0cbfa473ee81ad50b1fffebc40c635417e58905f3c865a6e2">+1/-1</a> </td> </tr> <tr> <td><strong>page-manager.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-565eab8b3c767ae50c9d05e591621d14f66b81af84d6b5ea6efe136c1c3933df">+2/-0</a> </td> </tr> <tr> <td><strong>custom-charts.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-e67e40336061645d2e29d009538f198c4afb5d674a63ed0bff66530caa3eae95">[link]</a> </td> </tr> <tr> <td><strong>dashbaord-tableJson</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-fe87bf86d1d91df383be2c8c118169e2a18b976ddb4bbff5fb2cc70c76381662">[link]</a> </td> </tr> <tr> <td><strong>dashboard-chartJson.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-187b68c3a07b9fe52b36379098172a4a8445f46a2da054af62c8c2deb372b590">[link]</a> </td> </tr> <tr> <td><strong>dashboard-filter.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-0b36e0f7bb97e53ba43466ae6666f04e2b3015f5760243493a058ad3df6fbef9">[link]</a> </td> </tr> <tr> <td><strong>dashboard-folder.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-808be84f3e76f1a69eb26e1acfc7c0bbc504445dc2dcf83475d474d0cad47ff2">[link]</a> </td> </tr> <tr> <td><strong>dashboard-general-setting.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-60a678bb3767da174e18e3453190d5b526687094a1c34144c59b5b3797edde56">[link]</a> </td> </tr> <tr> <td><strong>dashboard-geoMap.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-37446976600456902ad3287acddef8dd8cb0e6819606a423b6711145d08cded0">[link]</a> </td> </tr> <tr> <td><strong>dashboard-html-chart.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-5b258fd2098659bd4445a432b9aaca79b1e1295bc45f20e82408498cf2b4696a">[link]</a> </td> </tr> <tr> <td><strong>dashboard-import.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-19a69660b4842f0e2872aaefcb2a539ed25761b42daa804cfb8426d5a7cda567">[link]</a> </td> </tr> <tr> <td><strong>dashboard-maps.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-18dc153e8132b3c446cdbf5bac7140ecb7629029d021f006d5d76a0e966eacfe">[link]</a> </td> </tr> <tr> <td><strong>dashboard-multi-y-axis.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-2c0ab4d43c3e915a4e00f2a729234e7c1c975a7a8bc9483fff2ba0a4660f0f83">[link]</a> </td> </tr> <tr> <td><strong>dashboard-streaming.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-7181101483e2bb5a3d086f87546287e47debe10a4b4343707c3feaf95bbb255d">[link]</a> </td> </tr> <tr> <td><strong>dashboard-tabs-setting.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-188e3e1e0adfb6431a70ca6f26e6a5db58be330d3de15b9c559e390cbf601147">[link]</a> </td> </tr> <tr> <td><strong>dashboard-transpose.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-6b7619b9de4f7daa67bd35028c25f13ad3620635538a7203f1095b35ddf013d1">[link]</a> </td> </tr> <tr> <td><strong>dashboard-variables-creation-scopes.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-878775b83972fb1f04f1d4df7bc0bbec364fbcbacda6fbd081423673e4ea4b05">+958/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-dependency.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-ad5a7685fbf4477a81657949b7dd4aa4ada6e66a4576d0c61bacb388095d0b52">+590/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-global.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-f3750bde6fb0d471ec65d81b9d5a5a5b15d79ca900782959a0ff856a5156f5be">+600/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-null-handling.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-e42ab503059a687ceb4fa2edeed0888a47591e0a61680c10448eaf68b4e6bbd8">+133/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-panel-level.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-a4f3068a8d69478dafa0bc1bfb7f51cf98a6980ecc62ae78ba46a617f8e227fd">+712/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-refresh.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-8e82d40d2eda995a2915569d16456a9fd7d605017ed5868a28ee409e53646027">+605/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-setting.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-1c6dd312b59d03b6cef1e15784451e5a5ffb3ed4682dec017e9834de8094fbdd">+3/-3</a> </td> </tr> <tr> <td><strong>dashboard-variables-tab-level.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-b1f20469fa2e12dc68ede8ae381f60e2717489947ca693dd8a519fc03f195d48">+684/-0</a> </td> </tr> <tr> <td><strong>dashboard-variables-url-sync.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-709abba02dfd42b1be6d3838b162b544a2b97ba204dd179c37d462f0921ff14b">+599/-0</a> </td> </tr> <tr> <td><strong>dashboard.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-1403f96be575d5ae4215674b2b1c53c2503ddb1b35a3f4e0aefcf391b1cea1be">[link]</a> </td> </tr> <tr> <td><strong>dashboard2.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-657bb65c00477679ef10c9f79582468cf4f632589469e2c1da7da13979d71f8e">[link]</a> </td> </tr> <tr> <td><strong>dashboardtype.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-1827c80e7bf807fa4be90c240ee9546392558c98d8785f3a39a889adced4241a">[link]</a> </td> </tr> <tr> <td><strong>maxquery.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-68ad92cdbe3b024f7146090b46fb3fca0d91e5864afe490a12f807ddefa29726">[link]</a> </td> </tr> <tr> <td><strong>dashCreation.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-0f865c438e06691b660a9d4a2448c7c94e6f5172c6a0615eb2a905fa276db575">[link]</a> </td> </tr> <tr> <td><strong>dashIngestion.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-f324562b6f8143a8f780b29edc21e7cf969ff3ccc043e4f73aa4c32e48f7d06b">[link]</a> </td> </tr> <tr> <td><strong>dashLogin.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-ba1d380a34ddae6c351de0f325d26ed3e0715c33c88d9e2f39177b696f3b66ef">[link]</a> </td> </tr> <tr> <td><strong>visualization-vrl.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-935ba0de56260b2b735a350ada52a9d07cff34b79f0b137a384ed47cf80075d6">[link]</a> </td> </tr> <tr> <td><strong>visualize.spec.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-28874b05126e014a74c04029ea47a8797f936827a0b7196bd543b91f7389d0c1">[link]</a> </td> </tr> <tr> <td><strong>variable-helpers.js</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-2e2771d2582ab60dfcbaeef480226887677e58b881edbb244e0ffa4396ffbb5c">+427/-0</a> </td> </tr> <tr> <td><strong>PanelContainer.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-414737cba6064461f7026e0b016afcb9620bd5bb29c4708fc170dee789dbcd4d">+35/-7</a> </td> </tr> <tr> <td><strong>PanelSchemaRenderer.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-e5376e7e77bf3aaf6590c2522d8c2dbffcc95a8006ae0d6dc0040a37825367d6">+11/-2</a> </td> </tr> <tr> <td><strong>VariablesValueSelector.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-5c6e3bcb87cb64f25bf8157a541604b7a4904c1282acf662cab5d76a5be6bd56">+844/-193</a></td> </tr> <tr> <td><strong>CustomHTMLEditor.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-66cb3b43f79787a9b8d99b8c1d0f2f89aacd82055ff9ddbd4a4e5c96ec6b61ca">+10/-0</a> </td> </tr> <tr> <td><strong>CustomMarkdownEditor.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-6f1e425e1dc7a287070fbad1713679c729dc7ee61fa3b31781ed6b285ae3d279">+10/-0</a> </td> </tr> <tr> <td><strong>DrilldownPopUp.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-9fdb8c9d3953179dbd311c7e4d2250683af9b51e7706122a505c5c6a708c2a55">+45/-7</a> </td> </tr> <tr> <td><strong>HTMLRenderer.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-128fd699e9ae1552077c27185db37871b57e0002558f89377ec64b4e29e1d7f7">+6/-5</a> </td> </tr> <tr> <td><strong>HTMLRenderer.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-9184ad98d56a2466a3cd9917dc2a15238023df9eafb2ca6d1b7488877ff47194">+13/-1</a> </td> </tr> <tr> <td><strong>MarkdownRenderer.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-63eed3fbb6882be90c70e833f9f64e9e8c6511e4c40162aa5c1a7662fc79469d">+6/-5</a> </td> </tr> <tr> <td><strong>MarkdownRenderer.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-8a4f13edce033462affe8585dd8f128ab333a52a0a59e097ce7db949e042b56b">+13/-1</a> </td> </tr> <tr> <td><strong>AddSettingVariable.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-e622f54c1f7629ee67d29ed2fc87008d07eb9246767a05435cc42c3c17ce459a">+560/-80</a></td> </tr> <tr> <td><strong>VariableCustomValueSelector.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-ecfbb27a97242a9fd854f63f83f7af07bb4ddaaf1f465542ad7f74ec6b1f327e">+20/-1</a> </td> </tr> <tr> <td><strong>VariableQueryValueSelector.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-7e096d825438fc75baab2ef87cb4a04ae7ac1206e5021b71ce42cedd0984e064">+11/-1</a> </td> </tr> <tr> <td><strong>VariableQueryValueSelector.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-2cca760c14e835bd1845485fb5517a60f71a6dc23626a6381c8559b82476f5f4">+39/-10</a> </td> </tr> <tr> <td><strong>VariableSettings.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-3d55b5c7a758c836df096cb320a0ac3ba4586bc025d9e69c180e6a0b668ed940">+132/-9</a> </td> </tr> <tr> <td><strong>ViewPanel.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-e7b26fbbfe7ae57f50377c0f002155df17109ea54708b0f134dd2005356b2930">+82/-3</a> </td> </tr> <tr> <td><strong>usePanelDataLoader.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-5a42e9493a7bcd722b88e61fd38883c4f61001bc9cafa399f65f8eafda3aace9">+61/-11</a> </td> </tr> <tr> <td><strong>useVariablesManager.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-a8440d3f1bc2c5d8a96a4ce8bc5fe8f5a14872fdab4ad5037674f55f07d6d2ae">+633/-0</a> </td> </tr> <tr> <td><strong>commons.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-17cb74f8142984fc46bbfecdeaacccc118fd5d4ab7f040a55695caaa309d104f">+40/-0</a> </td> </tr> <tr> <td><strong>variablesScopeUtils.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-51e0200d0cd001418ab2708878120830d849b912a3b16a7da37dea1432a12407">+33/-0</a> </td> </tr> <tr> <td><strong>variablesUtils.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-769aea99ac476a78b01cb3bb4221b0a0f0d5fa99f3dbc8c77022088ed8024f1b">+3/-2</a> </td> </tr> <tr> <td><strong>variablesUtils.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-64086381f002e7ab774bf802bda51a31c14a0f2ff890b02df2037962345f78ae">+176/-40</a></td> </tr> <tr> <td><strong>RenderDashboardCharts.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-313f7c69102161242c893c863c50f1f1cd1fc19b68b5c99bf840b4b5c72f0f2e">+357/-61</a></td> </tr> <tr> <td><strong>ViewDashboard.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-052badb6758aff09ceaf1645869b9ba96125e6a0ea21abb01a8ee0a40d6467f3">+187/-62</a></td> </tr> <tr> <td><strong>AddPanel.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-5565bef51b35f1e9c8167eab0a22dc61c1d697dca45fe31f46cc8e5e9020ca74">+8/-8</a> </td> </tr> <tr> <td><strong>AddPanel.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-c9632db1ad95af95aba10161bcbc7c5608318919f228637bdafae3f3a31267e8">+609/-54</a></td> </tr> <tr> <td><strong>DashboardFiltersOption.spec.ts</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-d3e73ec3d32f9068b22b3697ba8a23e3100285b7c92cc86f7d7e88477bb29bba">+26/-12</a> </td> </tr> <tr> <td><strong>DashboardFiltersOption.vue</strong></td> <td><a href="https://github.com/openobserve/openobserve/pull/9693/files#diff-1309de92cc43f54536fef3d92db8c11cc666ef63bc40d4d9ca3fdaed85a94d92">+36/-2</a> </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___ --------- Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech> Co-authored-by: ktx-riya <riya.kukadiya@kiara.tech>
428 lines
14 KiB
JavaScript
428 lines
14 KiB
JavaScript
/**
|
|
* Variable Testing Helper Utilities
|
|
* Provides common methods for variable API monitoring, assertions, and state validation
|
|
*/
|
|
|
|
const testLogger = require('./test-logger.js');
|
|
|
|
/**
|
|
* Monitor variable values API calls and track completion
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Object} options - Configuration options
|
|
* @param {number} options.expectedCount - Expected number of API calls (default: 1)
|
|
* @param {number} options.timeout - Timeout in milliseconds (default: 15000)
|
|
* @returns {Promise<Object>} - {success, actualCount, calls, timedOut}
|
|
*/
|
|
export async function monitorVariableAPICalls(page, options = {}) {
|
|
const { expectedCount = 1, timeout = 15000 } = options;
|
|
const startTime = Date.now();
|
|
const apiCalls = [];
|
|
|
|
return new Promise((resolve) => {
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
|
|
if (url.includes('_values_stream') || url.includes('/values')) {
|
|
try {
|
|
const body = await response.text();
|
|
const status = response.status();
|
|
|
|
const callInfo = {
|
|
url,
|
|
status,
|
|
timestamp: Date.now(),
|
|
duration: Date.now() - startTime,
|
|
completed: body.includes('[[DONE]]') || body.includes('"type":"end"'),
|
|
hasData: body.length > 0,
|
|
};
|
|
|
|
apiCalls.push(callInfo);
|
|
|
|
testLogger.info(`Variable API call ${apiCalls.length}/${expectedCount}: ${status} - ${callInfo.completed ? 'Completed' : 'Incomplete'}`);
|
|
|
|
if (callInfo.completed && apiCalls.length >= expectedCount) {
|
|
page.off('response', responseHandler);
|
|
resolve({
|
|
success: true,
|
|
actualCount: apiCalls.length,
|
|
calls: apiCalls,
|
|
timedOut: false,
|
|
totalDuration: Date.now() - startTime
|
|
});
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug(`Error reading response body: ${error.message}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
setTimeout(() => {
|
|
page.off('response', responseHandler);
|
|
testLogger.warn(`Variable API monitoring timed out: ${apiCalls.length}/${expectedCount} calls completed`);
|
|
resolve({
|
|
success: apiCalls.length >= expectedCount,
|
|
actualCount: apiCalls.length,
|
|
calls: apiCalls,
|
|
timedOut: true,
|
|
totalDuration: timeout
|
|
});
|
|
}, timeout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for variable to load by checking its state
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} variableName - Variable name
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Promise<Object>} - {success, state, error}
|
|
*/
|
|
export async function waitForVariableToLoad(page, variableName, options = {}) {
|
|
const { timeout = 10000 } = options;
|
|
|
|
try {
|
|
// Wait for variable to be visible
|
|
await page.locator(`[data-test="dashboard-variable-${variableName}"]`).waitFor({
|
|
state: 'visible',
|
|
timeout
|
|
});
|
|
|
|
// Check if variable has error state
|
|
const hasError = await page.locator(`[data-test="dashboard-variable-${variableName}-error"]`).isVisible().catch(() => false);
|
|
|
|
if (hasError) {
|
|
return { success: false, state: 'error', error: 'Variable shows error state' };
|
|
}
|
|
|
|
// Check if variable is still loading
|
|
const isLoading = await page.locator(`[data-test="dashboard-variable-${variableName}-loading"]`).isVisible().catch(() => false);
|
|
|
|
if (isLoading) {
|
|
// Wait for loading to finish
|
|
await page.locator(`[data-test="dashboard-variable-${variableName}-loading"]`).waitFor({
|
|
state: 'hidden',
|
|
timeout
|
|
});
|
|
}
|
|
|
|
return { success: true, state: 'loaded', error: null };
|
|
} catch (error) {
|
|
return { success: false, state: 'timeout', error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify variable API was called when clicking dropdown
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Function} action - Action to trigger API call (e.g., clicking dropdown)
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function verifyVariableAPITriggered(page, action, options = {}) {
|
|
const { timeout = 10000 } = options;
|
|
let apiCalled = false;
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
if (url.includes('_values_stream') || url.includes('/values')) {
|
|
apiCalled = true;
|
|
testLogger.info(`Variable API triggered: ${url}`);
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
try {
|
|
await action();
|
|
await page.waitForTimeout(timeout);
|
|
} finally {
|
|
page.off('response', responseHandler);
|
|
}
|
|
|
|
return apiCalled;
|
|
}
|
|
|
|
/**
|
|
* Check if refresh button indicates changes are pending
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} level - 'global' or panel ID
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function hasRefreshIndicator(page, level = 'global') {
|
|
try {
|
|
if (level === 'global') {
|
|
const refreshBtn = page.locator('[data-test="dashboard-global-refresh-btn"]');
|
|
const hasIndicator = await refreshBtn.getAttribute('data-needs-refresh');
|
|
return hasIndicator === 'true';
|
|
} else {
|
|
const panelRefreshBtn = page.locator(`[data-test="dashboard-panel-${level}-refresh-btn"]`);
|
|
const hasIndicator = await panelRefreshBtn.getAttribute('data-needs-refresh');
|
|
return hasIndicator === 'true';
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug(`Error checking refresh indicator: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify panel shows refresh needed indicator
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function panelNeedsRefresh(page, panelId) {
|
|
try {
|
|
const warningIcon = page.locator(`[data-test="dashboard-panel-${panelId}-refresh-warning"]`);
|
|
return await warningIcon.isVisible({ timeout: 3000 });
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Track panel reload by monitoring query API calls
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @param {Function} action - Action that triggers panel reload
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
* @returns {Promise<Object>} - {reloaded, queryCount}
|
|
*/
|
|
export async function trackPanelReload(page, panelId, action, timeout = 10000) {
|
|
const queryCalls = [];
|
|
const startTime = Date.now();
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
if (url.includes('_search') || url.includes('_multi_search')) {
|
|
queryCalls.push({
|
|
url,
|
|
timestamp: Date.now(),
|
|
panelId: extractPanelIdFromURL(url)
|
|
});
|
|
testLogger.info(`Panel query detected: ${queryCalls.length} calls`);
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
try {
|
|
await action();
|
|
await page.waitForTimeout(timeout);
|
|
} finally {
|
|
page.off('response', responseHandler);
|
|
}
|
|
|
|
return {
|
|
reloaded: queryCalls.length > 0,
|
|
queryCount: queryCalls.length,
|
|
calls: queryCalls,
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract panel ID from URL (helper function)
|
|
*/
|
|
function extractPanelIdFromURL(url) {
|
|
const match = url.match(/panelId=([^&]+)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Verify variable value persists across tab switches
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} variableName - Variable name
|
|
* @param {string} tabId - Tab ID
|
|
* @param {string} expectedValue - Expected value
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function verifyVariableValuePersists(page, variableName, tabId, expectedValue) {
|
|
try {
|
|
// Switch to the tab
|
|
await page.locator(`[data-test="dashboard-tab-${tabId}"]`).click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Get variable value
|
|
const variableInput = page.locator(`[data-test="dashboard-variable-${variableName}"] input`);
|
|
const actualValue = await variableInput.inputValue();
|
|
|
|
testLogger.info(`Variable ${variableName} in tab ${tabId}: expected="${expectedValue}", actual="${actualValue}"`);
|
|
|
|
return actualValue === expectedValue;
|
|
} catch (error) {
|
|
testLogger.error(`Error verifying variable value: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify variable loading sequence with dependencies
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Array} variableNames - Array of variable names in expected load order
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
* @returns {Promise<Object>} - {success, loadOrder, errors}
|
|
*/
|
|
export async function verifyVariableLoadSequence(page, variableNames, timeout = 20000) {
|
|
const loadOrder = [];
|
|
const errors = [];
|
|
const startTime = Date.now();
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
|
|
if (url.includes('_values_stream') || url.includes('/values')) {
|
|
try {
|
|
const body = await response.text();
|
|
if (body.includes('[[DONE]]') || body.includes('"type":"end"')) {
|
|
// Extract variable name from URL if possible
|
|
const variableName = extractVariableNameFromURL(url);
|
|
if (variableName && variableNames.includes(variableName)) {
|
|
loadOrder.push({
|
|
name: variableName,
|
|
timestamp: Date.now(),
|
|
order: loadOrder.length + 1,
|
|
duration: Date.now() - startTime
|
|
});
|
|
testLogger.info(`Variable ${variableName} loaded (${loadOrder.length}/${variableNames.length})`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
errors.push({ error: error.message, timestamp: Date.now() });
|
|
}
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
try {
|
|
await page.waitForTimeout(timeout);
|
|
} finally {
|
|
page.off('response', responseHandler);
|
|
}
|
|
|
|
return {
|
|
success: loadOrder.length === variableNames.length,
|
|
loadOrder,
|
|
errors,
|
|
totalDuration: Date.now() - startTime
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract variable name from URL (helper function)
|
|
*/
|
|
function extractVariableNameFromURL(url) {
|
|
const match = url.match(/variable=([^&]+)/);
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
/**
|
|
* Verify URL contains correct variable parameters
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Object} expectedVariables - Expected variables {name: value, ...}
|
|
* @param {string} scope - 'global', 'tab', or 'panel'
|
|
* @param {string} scopeId - Tab or panel ID (if applicable)
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function verifyVariablesInURL(page, expectedVariables, scope = 'global', scopeId = null) {
|
|
const currentURL = page.url();
|
|
|
|
for (const [name, value] of Object.entries(expectedVariables)) {
|
|
let expectedParam;
|
|
|
|
if (scope === 'global') {
|
|
expectedParam = `v-${name}=${encodeURIComponent(value)}`;
|
|
} else if (scope === 'tab') {
|
|
expectedParam = `v-${name}.t.${scopeId}=${encodeURIComponent(value)}`;
|
|
} else if (scope === 'panel') {
|
|
expectedParam = `v-${name}.p.${scopeId}=${encodeURIComponent(value)}`;
|
|
}
|
|
|
|
if (!currentURL.includes(expectedParam)) {
|
|
testLogger.error(`URL missing expected parameter: ${expectedParam}`);
|
|
testLogger.debug(`Current URL: ${currentURL}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Wait for all panels to finish loading
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {number} panelCount - Expected number of panels
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
export async function waitForAllPanelsToLoad(page, panelCount, timeout = 30000) {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await page.waitForFunction(
|
|
(count) => {
|
|
const loadedPanels = document.querySelectorAll('[data-test*="dashboard-panel-"][data-state="loaded"]');
|
|
return loadedPanels.length >= count;
|
|
},
|
|
panelCount,
|
|
{ timeout, polling: 500 }
|
|
);
|
|
|
|
testLogger.info(`All ${panelCount} panels loaded in ${Date.now() - startTime}ms`);
|
|
return true;
|
|
} catch (error) {
|
|
testLogger.error(`Panels failed to load within ${timeout}ms`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert variable API loading states
|
|
* @param {Object} monitorResult - Result from monitorVariableAPICalls
|
|
* @param {Object} expectations - {success: true, minCalls: 1, maxDuration: 10000}
|
|
* @throws {Error} if expectations are not met
|
|
*/
|
|
export function assertVariableAPILoading(monitorResult, expectations = {}) {
|
|
const {
|
|
success = true,
|
|
minCalls = 1,
|
|
maxDuration = 15000,
|
|
shouldTimeout = false
|
|
} = expectations;
|
|
|
|
if (expectations.success !== undefined && monitorResult.success !== success) {
|
|
throw new Error(`Expected success=${success}, got ${monitorResult.success}`);
|
|
}
|
|
|
|
if (monitorResult.actualCount < minCalls) {
|
|
throw new Error(`Expected at least ${minCalls} API calls, got ${monitorResult.actualCount}`);
|
|
}
|
|
|
|
if (monitorResult.totalDuration > maxDuration) {
|
|
throw new Error(`Loading took ${monitorResult.totalDuration}ms, expected < ${maxDuration}ms`);
|
|
}
|
|
|
|
if (shouldTimeout !== monitorResult.timedOut) {
|
|
throw new Error(`Expected timedOut=${shouldTimeout}, got ${monitorResult.timedOut}`);
|
|
}
|
|
|
|
testLogger.info(`Variable API assertion passed: ${monitorResult.actualCount} calls in ${monitorResult.totalDuration}ms`);
|
|
}
|
|
|
|
module.exports = {
|
|
monitorVariableAPICalls,
|
|
waitForVariableToLoad,
|
|
verifyVariableAPITriggered,
|
|
hasRefreshIndicator,
|
|
panelNeedsRefresh,
|
|
trackPanelReload,
|
|
verifyVariableValuePersists,
|
|
verifyVariableLoadSequence,
|
|
verifyVariablesInURL,
|
|
waitForAllPanelsToLoad,
|
|
assertVariableAPILoading
|
|
};
|