mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
## What Repairs the shared setup + ingestion path that was failing **every** module of the alpha1 cloud E2E suite (`playwright_alpha1.yml`) after the UX revamp, **and stabilizes the first full test shard — Functions — to green** (validated in CI, run link below). No changes to the test matrix. Per-module fixes for the remaining shards land in separate follow-up PRs. ## Root cause — shared setup (blocked all 13 modules) 1. **Automation login user routes through Okta.** After the revamp, `@openobserve.ai` logins on alpha redirect to Okta SSO and 404 — automation can't complete login. The local-auth `e2etest@o2-qa.com` user still uses the Dex password flow. 2. **Org-switch selectors changed** (`global-setup-alpha1.js`). The org dropdown now opens via a dedicated trigger button; the old inline `arrow_drop_down` text is an SVG icon, the search box's `data-test` moved to the `OSearchInput` wrapper `<div>`, and rows carry a `data-test-org-identifier`. 3. **Ingestion helper rejected https.** `ingestionPage.js` hardcoded an `http.Agent` for node-fetch — throws `Protocol "https:" not supported` against alpha's https ingestion URL (killed ~27 tests/run across modules). ## Root cause — Functions shard 4. **Tests created enrichment tables in the wrong org.** `navigateToBase` passes `?org_identifier=` on the root URL; the redirect to `/web/` **drops the query param**, so the app fell back to the user's default org and enrichment tables were created there. Fixed by deep-navigating to the enrichment page with the org param and asserting the active org stuck (same pattern js-transform tests already use). 5. **Backend bug (reported separately): enrichment URL-jobs fail for table names starting with `schema`.** 100% reproducible, org/session/timing-independent: the job's save step fails with `Failed to save batch 1: not an ingester` (`schema_probe1` ❌ / `view_probe1` ✅, same request otherwise). Test tables renamed `schema_view_*` → `view_schema_*` and `schema_mismatch_*` → `mismatch_schema_*`; cleanup patterns cover both prefixes. The product bug itself still needs a backend fix — it affects any user naming a table `schema…`. Plus hardening while there: URL-job completion is now decided from the backend status API instead of inferring from row-button counts (which misread a failed job as success), the job's `error_message` is logged on failure, and the create form verifies field values + Save enabled + create-API response before proceeding. ## Validation - **CI (this PR's branch): [run #168](https://github.com/openobserve/o2-enterprise/actions/runs/29315765982) — Cloud ✅ (3m31s) + Functions ✅ (13m14s), `playwright_summary` ✅** - Local (alpha, CI conditions): previously-failing schema-view + schema-mismatch tests 2/2 green; earlier full-shard run 24/25 with the same single failure now fixed. ## Notes - `ALPHA1_USER_*` secrets: CI already uses a working non-Okta user (Cloud job green). - Follow-ups: backend `schema`-prefix bug (separate report), per-module PRs for remaining shards. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
509 lines
21 KiB
JavaScript
509 lines
21 KiB
JavaScript
const { expect } = require('@playwright/test');
|
|
|
|
class FunctionsPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// ==================== Locators (All in constructor) ====================
|
|
|
|
// Main page elements
|
|
this.addFunctionButton = this.page.locator('[data-test="function-list-add-function-btn"]');
|
|
// OInput wrapper for the list-page search field
|
|
this.searchInputWrapper = this.page.locator('[data-test="functions-list-search-input"]');
|
|
// OInput's auto-derived inner native input
|
|
this.searchInputField = this.page.locator('[data-test="functions-list-search-input-field"]');
|
|
// Bulk delete (footer) action — shown when rows are selected
|
|
this.bulkDeleteButton = this.page.locator('[data-test="function-list-delete-functions-btn"]');
|
|
// Per-row action buttons (resolved relative to a row locator)
|
|
this.rowEditButtonSelector = '[data-test="function-list-edit-function-btn"]';
|
|
this.rowDeleteButtonSelector = '[data-test="function-list-delete-function-btn"]';
|
|
this.rowEditButton = this.page.locator(this.rowEditButtonSelector);
|
|
this.rowDeleteButton = this.page.locator(this.rowDeleteButtonSelector);
|
|
|
|
// Function type selection (Reka-UI radios — wrapper divs carry the data-test).
|
|
// The inner RadioGroupItem renders data-state="checked|unchecked" — we read it
|
|
// via page.evaluate scoped from the wrapper's data-test locator (no element-tag
|
|
// or class selectors per AGENT_RULES §2).
|
|
this.vrlRadio = this.page.locator('[data-test="function-transform-type-vrl-radio"]');
|
|
this.jsRadio = this.page.locator('[data-test="function-transform-type-js-radio"]');
|
|
|
|
// Function form elements
|
|
this.functionNameInputWrapper = this.page.locator('[data-test="add-function-name-input"]');
|
|
this.functionNameInputField = this.page.locator('[data-test="add-function-name-input-field"]');
|
|
this.functionEditor = this.page.locator('[data-test="logs-vrl-function-editor"]');
|
|
// Monaco mounts inside `data-test="query-editor"` (child of the function editor wrapper)
|
|
this.functionEditorQueryDiv = this.page.locator('[data-test="logs-vrl-function-editor"] [data-test="query-editor"]');
|
|
this.saveButton = this.page.locator('[data-test="add-function-save-btn"]');
|
|
this.cancelButton = this.page.locator('[data-test="add-function-cancel-btn"]');
|
|
this.testButton = this.page.locator('[data-test="add-function-test-btn"]');
|
|
this.backButton = this.page.locator('[data-test="add-function-back-btn"]');
|
|
|
|
// Test function elements
|
|
this.testSection = this.page.locator('[data-test="test-function-section"]');
|
|
this.testEventsEditor = this.page.locator('[data-test="vrl-function-test-events-editor"]');
|
|
this.testEventsEditorQueryDiv = this.page.locator('[data-test="vrl-function-test-events-editor"] [data-test="query-editor"]');
|
|
this.testOutputEditor = this.page.locator('[data-test="vrl-function-test-events-output-editor"]');
|
|
|
|
// Confirm dialog (delete confirmation) — primary/secondary buttons
|
|
this.confirmDialog = this.page.locator('[data-test="confirm-dialog"]');
|
|
this.confirmDialogPrimaryBtn = this.page.locator('[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]');
|
|
this.confirmDialogSecondaryBtn = this.page.locator('[data-test="confirm-dialog"] [data-test="o-dialog-secondary-btn"]');
|
|
}
|
|
|
|
// ==================== Locator factories (runtime-dynamic) ====================
|
|
|
|
/**
|
|
* Locator for the unique per-row function-name cell. FunctionList.vue
|
|
* `#cell-name` slot renders one span per row tagged with
|
|
* `data-test="function-list-name-cell-<name>"` (guaranteed unique per row).
|
|
*/
|
|
getFunctionNameCell(name) {
|
|
return this.page.locator(`[data-test="function-list-name-cell-${name}"]`);
|
|
}
|
|
|
|
/**
|
|
* Get a row locator for a function by name. Walks up from the unique
|
|
* name-cell to the OTable row using the `o2-table-row-*` data-test prefix.
|
|
* @param {string} name
|
|
*/
|
|
getRowByName(name) {
|
|
return this.getFunctionNameCell(name).locator(
|
|
'xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]'
|
|
);
|
|
}
|
|
|
|
// ==================== Navigation Methods ====================
|
|
|
|
async navigate(org = null) {
|
|
const targetOrg = org || process.env.ORGID;
|
|
await this.page.goto(`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${targetOrg}`);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
// ==================== Action Methods ====================
|
|
|
|
/**
|
|
* Click Add Function button with auto-recovery
|
|
* @param {string} [org] - Optional org to navigate to if button isn't visible (use '_meta' for JS functions)
|
|
*/
|
|
async clickAddFunctionButton(org = null) {
|
|
// Wait for page to stabilize (in case we just closed a dialog)
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// Quick check if button is visible - if not, navigate to ensure we're on list page
|
|
const isVisible = await this.addFunctionButton.isVisible({ timeout: 3000 }).catch(() => false);
|
|
if (!isVisible) {
|
|
// Navigate to functions page (uses provided org or current URL's org)
|
|
const targetOrg = org || this.getCurrentOrgFromUrl() || process.env.ORGID;
|
|
await this.page.goto(`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${targetOrg}`);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
await expect(this.addFunctionButton).toBeVisible({ timeout: 10000 });
|
|
await this.addFunctionButton.click();
|
|
// Wait for the function dialog to actually open
|
|
await expect(this.functionNameInputField).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Extract org identifier from current URL
|
|
*/
|
|
getCurrentOrgFromUrl() {
|
|
const url = this.page.url();
|
|
const match = url.match(/org_identifier=([^&]+)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
async selectJavaScriptType() {
|
|
// ORadio forwards the consumer's data-test onto the inner RadioGroupItem
|
|
// (the ARIA-focusable radio button) — `this.jsRadio` IS the clickable item.
|
|
await expect(this.jsRadio).toBeVisible();
|
|
await this.jsRadio.click();
|
|
// Wait for CodeQueryEditor to remount Monaco for the new language
|
|
// (editor-id is re-keyed on currentLanguage in QueryEditor.vue)
|
|
await this.page.waitForTimeout(1500);
|
|
}
|
|
|
|
async selectVRLType() {
|
|
await expect(this.vrlRadio).toBeVisible();
|
|
await this.vrlRadio.click();
|
|
await this.page.waitForTimeout(1500);
|
|
}
|
|
|
|
async fillFunctionName(name) {
|
|
// OInput convention: fill the auto-derived `-field` (native input), not the wrapper
|
|
await expect(this.functionNameInputField).toBeVisible({ timeout: 15000 });
|
|
await this.functionNameInputField.fill(name);
|
|
}
|
|
|
|
async enterFunctionCode(code) {
|
|
// Drive the Monaco editor value via window.monaco APIs per AGENT_RULES §5.
|
|
// Find the editor whose DOM node is inside the function-editor wrapper, then setValue.
|
|
// On headless Linux, clicking the wrapper div doesn't transfer focus to Monaco's
|
|
// internal textarea, so keyboard.type() sends keystrokes to nothing.
|
|
await this.functionEditorQueryDiv.waitFor({ state: 'visible', timeout: 10000 });
|
|
// CodeQueryEditor remounts the Monaco editor when language changes (transType
|
|
// toggle re-keys editor-id), so we poll until a fresh, non-disposed editor
|
|
// inside the wrapper accepts the value and the model reflects it.
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
await this.functionEditor.evaluate((el, text) => {
|
|
const editors = window.monaco?.editor?.getEditors?.() ?? [];
|
|
let target = null;
|
|
for (const ed of editors) {
|
|
const node = ed.getDomNode?.();
|
|
if (node && el.contains(node)) target = ed;
|
|
}
|
|
if (!target) return null;
|
|
target.setValue(text);
|
|
target.focus();
|
|
return target.getValue();
|
|
}, code),
|
|
{ timeout: 10000, intervals: [200, 500, 1000] },
|
|
)
|
|
.toBe(code);
|
|
// Wait longer than the Monaco editor's 500ms debounce so formData syncs via v-model
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
async clickSaveButton() {
|
|
await this.saveButton.click();
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
async clickCancelButton() {
|
|
await this.cancelButton.click();
|
|
await this.page.waitForTimeout(1500); // Wait for dialog to fully close
|
|
}
|
|
|
|
async clickTestButton() {
|
|
// force: true needed — button can be obscured by overlapping editor chrome
|
|
await this.testButton.click({ force: true });
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
async searchFunction(name) {
|
|
// OInput convention: fill the auto-derived `-field` (native input), not the wrapper
|
|
await expect(this.searchInputWrapper).toBeVisible();
|
|
await this.searchInputField.fill(name);
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
async clickFunctionByName(name) {
|
|
// Walk from the name-cell up to the OTable row's data-test prefix,
|
|
// then click the per-row edit button using its stable data-test.
|
|
const functionRow = this.getRowByName(name);
|
|
const editButton = functionRow.locator(this.rowEditButtonSelector);
|
|
// After a cross-org (_meta) navigation the functions list can take a while
|
|
// to stream in — wait for the row's edit button before clicking so a slow
|
|
// list render doesn't fail the click.
|
|
await editButton.waitFor({ state: 'visible', timeout: 30000 });
|
|
await editButton.click();
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
async deleteFirstFunction() {
|
|
// Use the per-row delete button (visible in each table row)
|
|
const deleteBtn = this.rowDeleteButton.first();
|
|
if (await deleteBtn.isVisible({ timeout: 2000 })) {
|
|
await deleteBtn.click();
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// Confirm deletion via ODialog primary button (scoped under ConfirmDialog)
|
|
if (await this.confirmDialogPrimaryBtn.isVisible({ timeout: 2000 })) {
|
|
await this.confirmDialogPrimaryBtn.click();
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== Test Function Methods ====================
|
|
|
|
async enterTestEvent(eventJson) {
|
|
// Drive the Monaco editor value via window.monaco APIs per AGENT_RULES §5.
|
|
// Find the editor whose DOM node is inside the test-events-editor wrapper, then setValue.
|
|
// Using setValue() ensures the v-model sync fires even on headless Linux where
|
|
// focus + keyboard.type would otherwise be unreliable.
|
|
await this.testEventsEditorQueryDiv.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.testEventsEditor.evaluate((el, text) => {
|
|
const editors = window.monaco?.editor?.getEditors?.() ?? [];
|
|
for (const ed of editors) {
|
|
const node = ed.getDomNode?.();
|
|
if (node && el.contains(node)) {
|
|
ed.setValue(text);
|
|
ed.focus();
|
|
return;
|
|
}
|
|
}
|
|
}, eventJson);
|
|
// Wait for Monaco editor v-model debounce to sync
|
|
await this.page.waitForTimeout(600);
|
|
}
|
|
|
|
async clickRunTestButton() {
|
|
// The toolbar "Test Function" button (`add-function-test-btn`) is the only
|
|
// run-test trigger — it invokes the TestFunction component's testFunction()
|
|
// via ref and POSTs to /api/{org}/functions/test, populating the output editor.
|
|
if (await this.testButton.isVisible({ timeout: 2000 })) {
|
|
// The spec calls clickTestButton() (which already fires a test request with the
|
|
// pre-existing default events) BEFORE entering the custom test event and then
|
|
// calls clickRunTestButton() to fire a SECOND request. The output editor still
|
|
// holds the first request's payload, so we can't just poll on length > 10 —
|
|
// we'd resolve on the stale value before the second response lands and overwrites
|
|
// it (e.g. with "Error in testing function" on a backend reject). Tie the click
|
|
// to a fresh `/functions/test` response via Promise.all + page.waitForResponse so
|
|
// we read the model only after THIS click's request resolves on the client.
|
|
const testApiResponse = this.page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes('/functions/test') && response.request().method() === 'POST',
|
|
{ timeout: 15000 },
|
|
);
|
|
// force: true needed — button can be obscured by overlapping editor chrome
|
|
await Promise.all([testApiResponse, this.testButton.click({ force: true })]);
|
|
// After the network call returns, TestFunction.vue runs the success/error
|
|
// handler synchronously (then() or catch()) and sets outputEvents.value;
|
|
// Monaco's v-model debounce (500 ms) then flushes that into the model.
|
|
// Poll the model for the post-debounce value (>10 chars distinguishes real
|
|
// content / error text from an empty editor).
|
|
await expect
|
|
.poll(async () => (await this.getTestOutput()).length, {
|
|
timeout: 15000,
|
|
intervals: [200, 500, 1000],
|
|
})
|
|
.toBeGreaterThan(10);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async getTestOutput() {
|
|
// Read the Monaco model bound to the output editor wrapper directly per §5.
|
|
// Falls back to .textContent only if Monaco isn't attached yet.
|
|
return await this.testOutputEditor.evaluate((el) => {
|
|
try {
|
|
const editors = window.monaco?.editor?.getEditors?.() ?? [];
|
|
for (const ed of editors) {
|
|
const node = ed.getDomNode?.();
|
|
if (node && el.contains(node)) {
|
|
return ed.getValue();
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
// Fallback when Monaco not initialized yet
|
|
return el.textContent || '';
|
|
});
|
|
}
|
|
|
|
// ==================== Assertion Methods ====================
|
|
|
|
async expectAddButtonVisible() {
|
|
await expect(this.addFunctionButton).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectVrlRadioVisible() {
|
|
await expect(this.vrlRadio).toBeVisible();
|
|
}
|
|
|
|
async expectJsRadioVisible() {
|
|
await expect(this.jsRadio).toBeVisible();
|
|
}
|
|
|
|
async expectJsRadioSelected() {
|
|
// ORadio forwards the consumer's data-test onto the inner RadioGroupItem,
|
|
// which Reka-UI exposes via `data-state="checked|unchecked"`.
|
|
await this.jsRadio.waitFor({ state: 'visible' });
|
|
await expect(this.jsRadio).toHaveAttribute('data-state', 'checked');
|
|
}
|
|
|
|
async expectVrlRadioSelected() {
|
|
await this.vrlRadio.waitFor({ state: 'visible' });
|
|
await expect(this.vrlRadio).toHaveAttribute('data-state', 'checked');
|
|
}
|
|
|
|
async expectTestOutputContains(text) {
|
|
const outputText = await this.getTestOutput();
|
|
expect(outputText).toContain(text);
|
|
}
|
|
|
|
async expectFunctionInList(functionName) {
|
|
const functionRow = this.getRowByName(functionName);
|
|
await expect(functionRow).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async isCancelButtonVisible() {
|
|
return await this.cancelButton.isVisible({ timeout: 1000 }).catch(() => false);
|
|
}
|
|
|
|
async getEditorContent() {
|
|
// Drive Monaco editor value via window.monaco — do NOT scrape .view-lines
|
|
return await this.functionEditor.evaluate((el) => {
|
|
try {
|
|
const editors = window.monaco?.editor?.getEditors?.() ?? [];
|
|
// Find the editor whose DOM node is inside this container
|
|
for (const ed of editors) {
|
|
const node = ed.getDomNode?.();
|
|
if (node && el.contains(node)) {
|
|
return ed.getValue();
|
|
}
|
|
}
|
|
return '';
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
});
|
|
}
|
|
|
|
async isJsRadioVisible() {
|
|
return await this.jsRadio.isVisible().catch(() => false);
|
|
}
|
|
|
|
async expectJsRadioHidden() {
|
|
await expect(this.jsRadio).not.toBeVisible();
|
|
}
|
|
|
|
// ==================== Complex Workflows ====================
|
|
|
|
/**
|
|
* Complete workflow to create a JS function
|
|
* NOTE: JS functions only work in _meta org, so this defaults to _meta for recovery
|
|
* @param {string} functionName - Name of the function
|
|
* @param {string} jsCode - JavaScript code
|
|
*/
|
|
async createJavaScriptFunction(functionName, jsCode) {
|
|
await this.clickAddFunctionButton('_meta'); // JS functions require _meta org
|
|
await this.fillFunctionName(functionName);
|
|
await this.selectJavaScriptType();
|
|
await this.enterFunctionCode(jsCode);
|
|
await this.clickSaveButton();
|
|
}
|
|
|
|
/**
|
|
* Complete workflow to create a VRL function
|
|
* @param {string} functionName - Name of the function
|
|
* @param {string} vrlCode - VRL code
|
|
* @param {string} [org] - Optional org identifier for recovery navigation
|
|
*/
|
|
async createVRLFunction(functionName, vrlCode, org = null) {
|
|
await this.clickAddFunctionButton(org);
|
|
await this.fillFunctionName(functionName);
|
|
await this.selectVRLType();
|
|
await this.enterFunctionCode(vrlCode);
|
|
await this.clickSaveButton();
|
|
}
|
|
|
|
/**
|
|
* Delete a function by name
|
|
* @param {string} functionName - Name of the function to delete
|
|
* @param {string} [org] - Optional org identifier (defaults to ORGID, use '_meta' for JS functions)
|
|
*/
|
|
async deleteFunctionByName(functionName, org = null) {
|
|
// Make sure we're on the functions list page (not in edit mode)
|
|
const isOnListPage = await this.searchInputWrapper.isVisible({ timeout: 2000 }).catch(() => false);
|
|
|
|
if (!isOnListPage) {
|
|
// Navigate back to functions list with correct org
|
|
const targetOrg = org || process.env.ORGID;
|
|
await this.page.goto(`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${targetOrg}`);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
await this.searchFunction(functionName);
|
|
await this.deleteFirstFunction();
|
|
}
|
|
|
|
/**
|
|
* Test a function with input data and return output.
|
|
* The toolbar "Test Function" button directly triggers the API call using
|
|
* the current function code and events editor content. Default test events
|
|
* are loaded by the TestFunction component on mount.
|
|
* @param {string} [testEventJson] - Optional custom test event JSON (entered before triggering test)
|
|
* @returns {Promise<string>} Test output
|
|
*/
|
|
async testFunctionExecution(testEventJson = null) {
|
|
// The TestFunction component is always visible (rendered in splitter v-slot:after).
|
|
// If custom events provided, enter them before triggering the test.
|
|
if (testEventJson) {
|
|
await this.enterTestEvent(testEventJson);
|
|
}
|
|
|
|
// The toolbar "Test Function" button calls testFunction() on the TestFunction
|
|
// component, which POSTs to /api/{org}/functions/test and populates the output editor.
|
|
await this.clickTestButton();
|
|
|
|
// Poll the Monaco output model directly (>10 chars distinguishes real
|
|
// content from an empty/initial editor); avoids DOM .view-line scraping.
|
|
await expect
|
|
.poll(async () => (await this.getTestOutput()).length, {
|
|
timeout: 15000,
|
|
intervals: [200, 500, 1000],
|
|
})
|
|
.toBeGreaterThan(10);
|
|
|
|
return await this.getTestOutput();
|
|
}
|
|
|
|
/**
|
|
* Navigate to function, edit it, and verify type
|
|
* @param {string} functionName - Name of the function
|
|
* @returns {Promise<'js'|'vrl'|null>} Function type
|
|
*/
|
|
async openFunctionAndCheckType(functionName) {
|
|
await this.searchFunction(functionName);
|
|
await this.clickFunctionByName(functionName);
|
|
|
|
// Wait for the editor to load after clicking the function
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Wait for radio buttons to be visible
|
|
await this.jsRadio.waitFor({ state: 'visible', timeout: 10000 });
|
|
|
|
// ORadio forwards the consumer's data-test onto the inner RadioGroupItem,
|
|
// which Reka-UI exposes via `data-state="checked|unchecked"`.
|
|
const jsState = await this.jsRadio.getAttribute('data-state');
|
|
const vrlState = await this.vrlRadio.getAttribute('data-state');
|
|
|
|
if (jsState === 'checked') return 'js';
|
|
if (vrlState === 'checked') return 'vrl';
|
|
return null;
|
|
}
|
|
|
|
// ==================== Cleanup Methods ====================
|
|
|
|
/**
|
|
* Delete all functions matching a pattern
|
|
* @param {string} pattern - Pattern to match (e.g., 'test_js_')
|
|
*/
|
|
async deleteAllFunctionsMatching(pattern) {
|
|
await this.searchFunction(pattern);
|
|
await this.page.waitForTimeout(1000);
|
|
|
|
// Delete all visible functions matching pattern with max attempts safety
|
|
const maxAttempts = 50;
|
|
let attempts = 0;
|
|
|
|
while (attempts < maxAttempts) {
|
|
const deleteBtn = this.bulkDeleteButton.first();
|
|
if (await deleteBtn.isVisible({ timeout: 2000 })) {
|
|
await deleteBtn.click();
|
|
await this.page.waitForTimeout(500);
|
|
|
|
if (await this.confirmDialogPrimaryBtn.isVisible({ timeout: 2000 })) {
|
|
await this.confirmDialogPrimaryBtn.click();
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
attempts++;
|
|
} else {
|
|
break; // No more functions to delete
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = FunctionsPage;
|