test: search partition and pipeline dynamic name tests (#7172)

### **User description**
Added test cases to verify:

1. Search Partition:
   - Verify search partition and search API calls for histogram query

2. Pipeline Dynamic Stream Names:
   - Verify pipeline with dynamic destination name using {dynamicname}
- Verify pipeline with dynamic destination name using
test_{dynamicname}_test with underscores
- Verify pipeline with dynamic destination name using
test{dynamicname}test directly


___

### **PR Type**
Tests, Other


___

### **Description**
Add Playwright E2E tests for dynamic pipeline names
Add Playwright E2E test for search partition histogram query
Update CI workflow to include new tests


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>pipeline-dynamic.spec.js</strong><dd><code>Add pipeline
dynamic stream name tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

tests/ui-testing/playwright-tests/pipeline-dynamic.spec.js

<li>Introduce E2E tests for dynamic pipeline stream names<br> <li> Add
helper functions: login, ingestion, navigation<br> <li> Cover three
dynamic naming scenarios<br> <li> Verify pipeline creation and deletion


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/7172/files#diff-a661fdc01b51aa8d44953a9b3452cc4407b0e3770309d6541355d251a58c1ace">+175/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>searchparition.spec.js</strong><dd><code>Add search
partition histogram tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

tests/ui-testing/playwright-tests/searchparition.spec.js

<li>Add E2E test for search partition histogram query<br> <li> Implement
helpers: login, ingestion, applyQueryButton<br> <li> Validate
_search_partition API response fields<br> <li> Verify matching search
calls per partition


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/7172/files#diff-d6855518e0bc277da8b3696ac75727cc1fb40b96012b270f12b85f17f5a47434">+151/-0</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>playwright.yml</strong><dd><code>Include new tests in
CI workflow</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.github/workflows/playwright.yml

- Include new test files in CI workflow job


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/7172/files#diff-7afcd2d8f7b49bda74843f209eefb7b2da45f7e7803bf2e4bd636699b76aa2d3">+3/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
This commit is contained in:
Neha P
2025-06-16 21:37:00 +05:30
committed by GitHub
parent 3f5a9d59b6
commit 36189a421d
5 changed files with 495 additions and 13 deletions
+4 -2
View File
@@ -142,8 +142,10 @@ jobs:
"usersOrg.spec.js",
"websocket.spec.js",
"alertsNew.spec.js",
# "remotepipeline.spec.js",
"pipeline-dynamic.spec.js",
"searchpartition.spec.js"
# "remotepipeline.spec.js",
]
steps:
+139 -8
View File
@@ -2,6 +2,7 @@
import { expect } from '@playwright/test';
const { chromium } = require('playwright');
import { dateTimeButtonLocator, relative30SecondsButtonLocator, absoluteTabLocator, Past30SecondsValue } from '../pages/CommonLocator.js';
import logData from '../cypress/fixtures/log.json';
export class LogsPage {
constructor(page) {
@@ -13,6 +14,8 @@ export class LogsPage {
this.logsMenuItem = page.locator('[data-test="menu-link-\\/logs-item"]');
this.indexDropDown = page.locator('[data-test="logs-search-index-list"]').getByText('arrow_drop_down');
this.streamToggle = page.locator('[data-test="log-search-index-list-stream-toggle-default"] div');
this.exploreButton = page.getByRole('button', { name: 'Explore' });
this.timestampColumnMenu = page.locator('[data-test="log-table-column-1-_timestamp"] [data-test="table-row-expand-menu"]');
this.filterMessage = page.locator('div:has-text("info Adjust filter parameters and click \'Run query\'")');
@@ -47,6 +50,15 @@ export class LogsPage {
this.profileButton = page.locator('[data-test="header-my-account-profile-icon"]');
this.signOutButton = page.getByText('Sign Out');
// Search Partition locators
this.searchPartitionButton = page.locator('[data-test="logs-search-partition-btn"]');
this.partitionDropdown = page.locator('[data-test="logs-search-partition-dropdown"]');
this.partitionOption = (option) => page.getByRole('option', { name: option });
this.partitionValueInput = page.locator('[data-test="logs-search-partition-value-input"]');
this.partitionApplyButton = page.locator('[data-test="logs-search-partition-apply-btn"]');
this.partitionClearButton = page.locator('[data-test="logs-search-partition-clear-btn"]');
this.partitionActiveIndicator = page.locator('[data-test="logs-search-partition-active"]');
}
async selectOrganization() {
@@ -205,15 +217,12 @@ export class LogsPage {
}
async applyQuery() {
const search = this.page.waitForResponse("**/api/default/_search**");
const search = this.page.waitForResponse(logData.applyQuery);
await this.page.waitForTimeout(3000);
await this.page.locator(this.queryButton).click({ force: true });
try {
const response = await search;
await expect.poll(async () => response.status()).toBe(200);
} catch (error) {
throw new Error(`Failed to get response: ${error.message}`);
}
await this.page.locator("[data-test='logs-search-bar-refresh-btn']").click({
force: true,
});
await expect.poll(async () => (await search).status()).toBe(200);
}
async applyQueryButton(expectedUrl) {
@@ -586,4 +595,126 @@ async executeQueryWithErrorHandling() {
await this.page.locator('[data-test="logs-search-result-detail-undefined"]').click();
}
// New methods for histogram query test
async executeHistogramQuery(query) {
await this.page.locator('[data-test="logs-search-bar-query-editor"] > .monaco-editor').click();
await this.page.keyboard.type(query);
await this.page.waitForTimeout(2000);
}
async toggleHistogramAndExecute() {
await this.page.locator('[data-test="logs-search-bar-show-histogram-toggle-btn"] div').nth(2).click();
await this.page.waitForTimeout(1000);
await this.page.locator('[data-test="logs-search-bar-refresh-btn"]').click();
}
async verifyHistogramState() {
const isHistogramOff = await this.page.locator('[data-test="logs-search-bar-show-histogram-toggle-btn"]')
.evaluate(el => el.getAttribute('aria-checked') === 'false');
expect(isHistogramOff).toBeTruthy();
}
async verifySearchPartitionResponse() {
const searchPartitionPromise = this.page.waitForResponse(response =>
response.url().includes('/api/default/_search_partition') &&
response.request().method() === 'POST'
);
const searchPartitionResponse = await searchPartitionPromise;
const searchPartitionData = await searchPartitionResponse.json();
expect(searchPartitionData).toHaveProperty('partitions');
expect(searchPartitionData).toHaveProperty('histogram_interval');
expect(searchPartitionData).toHaveProperty('order_by', 'asc');
return searchPartitionData;
}
async captureSearchCalls() {
const searchCalls = [];
this.page.on('response', async response => {
if (response.url().includes('/api/default/_search') &&
response.request().method() === 'POST') {
const requestData = await response.request().postDataJSON();
searchCalls.push({
start_time: requestData.query.start_time,
end_time: requestData.query.end_time,
sql: requestData.query.sql
});
}
});
await this.page.waitForTimeout(2000);
return searchCalls;
}
async verifyStreamingModeResponse() {
const searchPromise = this.page.waitForResponse(response =>
response.url().includes('/api/default/_search') &&
response.request().method() === 'POST'
);
const searchResponse = await searchPromise;
expect(searchResponse.status()).toBe(200);
const searchData = await searchResponse.json();
expect(searchData).toBeDefined();
expect(searchData.hits).toBeDefined();
}
async clickExplore() {
try {
// Wait for the explore button to be visible and clickable
await this.exploreButton.first().waitFor({ state: 'visible', timeout: 10000 });
await this.exploreButton.first().waitFor({ state: 'attached', timeout: 10000 });
// Click the button and wait for any network requests to complete
await Promise.all([
this.page.waitForLoadState('networkidle'),
this.exploreButton.first().click()
]);
// Wait for the results to load
await this.page.waitForTimeout(3000);
} catch (error) {
console.error('Error in clickExplore:', error);
throw error;
}
}
async openTimestampMenu() {
try {
// Wait for the table to be visible first
await this.page.waitForSelector('[data-test="logs-search-result-logs-table"]', { state: 'visible', timeout: 10000 });
// Wait for the timestamp column to be visible
await this.page.waitForSelector('[data-test="log-table-column-1-_timestamp"]', { state: 'visible', timeout: 10000 });
// Wait for the menu button to be visible and clickable
await this.timestampColumnMenu.waitFor({ state: 'visible', timeout: 10000 });
// Ensure the menu button is in viewport
await this.timestampColumnMenu.scrollIntoViewIfNeeded();
// Click the menu and wait for any network requests to complete
await Promise.all([
this.page.waitForLoadState('networkidle'),
this.timestampColumnMenu.click({ force: true })
]);
// Wait for the menu to open
await this.page.waitForTimeout(1000);
} catch (error) {
console.error('Error in openTimestampMenu:', error);
// Try alternative approach if first attempt fails
try {
await this.page.waitForTimeout(2000); // Give more time for the table to load
await this.timestampColumnMenu.click({ force: true });
await this.page.waitForTimeout(1000);
} catch (retryError) {
console.error('Error in openTimestampMenu retry:', retryError);
throw retryError;
}
}
}
}
+140 -3
View File
@@ -84,9 +84,18 @@ class PipelinePage {
this.headerKeyInput = page.locator('[data-test="add-destination-header--key-input"]');
this.headerValueInput = page.locator('[data-test="add-destination-header-Authorization-value-input"]');
this.submitButton = page.locator('[data-test="add-destination-submit-btn"]');
this.streamsMenuItem = page.locator('[data-test="menu-link-\\/streams-item"]');
this.refreshStatsButton = page.locator('[data-test="log-stream-refresh-stats-btn"]');
this.searchStreamInput = page.getByPlaceholder('Search Stream');
this.exploreButton = page.getByRole('button', { name: 'Explore' });
this.timestampColumnMenu = page.locator('[data-test="log-table-column-1-_timestamp"] [data-test="table-row-expand-menu"]');
this.streamIcon = page.getByRole("img", { name: "Stream", exact: true });
this.outputStreamIcon = page.getByRole("img", { name: "Output Stream" });
this.containsOption = page.getByText("Contains", { exact: true });
this.valueInput = page.getByPlaceholder("Value");
this.kubernetesContainerNameOption = page.getByRole("option", { name: "kubernetes_container_name" });
this.conditionText = page.getByRole("button", { name: "kubernetes_container_name" });
this.pipelineSavedMessage = page.getByText('Pipeline saved successfully');
}
async openPipelineMenu() {
@@ -403,6 +412,134 @@ async createRemoteDestination(randomNodeName, AuthorizationToken) {
await this.submitButton.click();
}
async navigateToStreams() {
await this.streamsMenuItem.click();
await this.page.waitForTimeout(1000);
}
async refreshStreamStats() {
await this.refreshStatsButton.click();
await this.page.waitForTimeout(1000);
}
async searchStream(streamName) {
await this.searchStreamInput.click();
await this.searchStreamInput.fill(streamName);
await this.page.waitForTimeout(1000);
}
async clickExplore() {
await this.exploreButton.first().click();
await this.page.waitForTimeout(3000);
}
async openTimestampMenu() {
await this.timestampColumnMenu.click();
}
async navigateToPipeline() {
await this.page.locator('[data-test="menu-link-\\/pipeline-item"]').click();
}
async setupContainerNameCondition() {
await this.editButton.hover();
await this.streamIcon.click();
await this.columnInput.click();
await this.columnInput.fill("container_name");
await this.page.waitForTimeout(1000);
await this.kubernetesContainerNameOption.click();
await this.page.locator("div:nth-child(2) > div:nth-child(2) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native").click();
await this.containsOption.click();
await this.valueInput.click();
await this.valueInput.fill("ziox");
await this.saveCondition();
await this.page.waitForTimeout(2000);
await this.conditionText.hover();
}
async setupDestinationStream(dynamicDestinationName) {
await this.outputStreamIcon.click();
await this.streamNameInput.click();
await this.page.waitForTimeout(1000);
await this.streamNameInput.fill(dynamicDestinationName);
await this.page.waitForTimeout(1000);
await this.clickInputNodeStreamSave();
}
async waitForPipelineSaved() {
await this.pipelineSavedMessage.waitFor({ state: "visible" });
}
async exploreStreamAndNavigateToPipeline(streamName) {
// Ingestion for the specific stream
const orgId = process.env["ORGNAME"];
const headers = {
"Authorization": `Basic ${Buffer.from(
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
).toString('base64')}`,
"Content-Type": "application/json",
};
const url = `${process.env.INGESTION_URL}/api/${orgId}/${streamName}/_json`;
await this.page.evaluate(async ({ url, headers, logsdata }) => {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(logsdata)
});
return await response.json();
}, {
url: url,
headers: headers,
logsdata: require('../../test-data/logs_data.json')
});
await this.page.waitForTimeout(2000);
// Navigate and explore
await this.navigateToStreams();
await this.refreshStreamStats();
await this.searchStream(streamName);
await this.clickExplore();
await this.openTimestampMenu();
await this.navigateToPipeline();
}
async setupPipelineWithSourceStream(sourceStream) {
await this.openPipelineMenu();
await this.page.waitForTimeout(1000);
await this.addPipeline();
await this.selectStream();
await this.dragStreamToTarget(this.streamButton);
await this.selectLogs();
await this.enterStreamName(sourceStream);
await this.page.waitForTimeout(2000);
await this.page.getByRole("option", { name: sourceStream, exact: true }).click();
await this.saveInputNodeStream();
await this.page.waitForTimeout(2000);
await this.page.locator("button").filter({ hasText: "delete" }).nth(1).click();
await this.page.locator('[data-test="confirm-button"]').click();
}
async createAndVerifyPipeline(expectedStreamName, sourceStream) {
const pipelineName = `pipeline-${Math.random().toString(36).substring(7)}`;
await this.enterPipelineName(pipelineName);
await this.savePipeline();
await this.waitForPipelineSaved();
// Verify the dynamic destination stream exists
await this.exploreStreamAndNavigateToPipeline(expectedStreamName);
// Verify pipeline creation and cleanup
await this.searchPipeline(pipelineName);
await this.page.waitForTimeout(1000);
const deletePipelineButton = this.page.locator(
`[data-test="pipeline-list-${pipelineName}-delete-pipeline"]`
);
await deletePipelineButton.waitFor({ state: "visible" });
await deletePipelineButton.click();
await this.confirmDeletePipeline();
await this.verifyPipelineDeleted();
}
}
export default PipelinePage;
@@ -0,0 +1,107 @@
import { test, expect } from '@playwright/test';
import PipelinePage from '../pages/pipelinePage.js';
import logsdata from '../../test-data/logs_data.json';
import { getHeaders, getIngestionUrl, sendRequest } from '../utils/apiUtils.js';
test.describe('Pipeline Dynamic Stream Names', () => {
let page;
let pipelinePage;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
pipelinePage = new PipelinePage(page);
// Login - happens only once before all tests
await page.goto(process.env["ZO_BASE_URL"]);
if (await page.getByText('Login as internal user').isVisible()) {
await page.getByText('Login as internal user').click();
}
await page.waitForTimeout(1000);
await page
.locator('[data-cy="login-user-id"]')
.fill(process.env["ZO_ROOT_USER_EMAIL"]);
await page
.locator('[data-cy="login-password"]')
.fill(process.env["ZO_ROOT_USER_PASSWORD"]);
await page.locator('[data-cy="login-sign-in"]').click();
// Wait for login to complete and page to be ready
await page.waitForTimeout(4000);
});
test.beforeEach(async () => {
// Ensure page is still open
if (page.isClosed()) {
throw new Error('Page was closed unexpectedly');
}
// Ingestion - happens before each test
const orgId = process.env["ORGNAME"];
const streamNames = ["e2e_automate", "e2e_automate1", "e2e_automate2", "e2e_automate3"];
const headers = getHeaders();
for(const streamName of streamNames) {
const url = getIngestionUrl(orgId, streamName);
const response = await sendRequest(page, url, logsdata, headers);
console.log(response);
}
await page.waitForTimeout(2000);
});
test.afterEach(async () => {
// Add a small wait after each test to ensure operations are complete
await page.waitForTimeout(1000);
});
test.afterAll(async () => {
// Add a wait before closing to ensure all operations are complete
await page.waitForTimeout(2000);
if (!page.isClosed()) {
await page.close();
}
});
test('Verify pipeline with dynamic destination name using kubernetes_container_name', async () => {
// Navigate to stream and pipeline
await pipelinePage.exploreStreamAndNavigateToPipeline('e2e_automate1');
await page.waitForLoadState('networkidle');
// Setup source stream
await pipelinePage.setupPipelineWithSourceStream('e2e_automate1');
await page.waitForLoadState('networkidle');
// Setup container name condition
await pipelinePage.setupContainerNameCondition();
await page.waitForLoadState('networkidle');
// Setup destination stream
await pipelinePage.setupDestinationStream('dynamic_ziox_dynamic');
await page.waitForLoadState('networkidle');
// Create and verify pipeline
await pipelinePage.createAndVerifyPipeline('dynamic_ziox_dynamic', 'e2e_automate1');
});
test('Verify pipeline with dynamic destination name using kubernetes_container_name with underscores', async () => {
await pipelinePage.exploreStreamAndNavigateToPipeline('e2e_automate2');
await page.waitForLoadState('networkidle');
await pipelinePage.setupPipelineWithSourceStream('e2e_automate2');
await page.waitForLoadState('networkidle');
await pipelinePage.setupContainerNameCondition();
await page.waitForLoadState('networkidle');
await pipelinePage.setupDestinationStream('dynamic_ziox_dynamic');
await page.waitForLoadState('networkidle');
await pipelinePage.createAndVerifyPipeline('dynamic_ziox_dynamic', 'e2e_automate2');
});
test('Verify pipeline with dynamic destination name using kubernetes_container_name directly', async () => {
await pipelinePage.exploreStreamAndNavigateToPipeline('e2e_automate3');
await page.waitForLoadState('networkidle');
await pipelinePage.setupPipelineWithSourceStream('e2e_automate3');
await page.waitForLoadState('networkidle');
await pipelinePage.setupContainerNameCondition();
await page.waitForLoadState('networkidle');
await pipelinePage.setupDestinationStream('ziox');
await page.waitForLoadState('networkidle');
await pipelinePage.createAndVerifyPipeline('ziox', 'e2e_automate3');
});
});
@@ -0,0 +1,105 @@
import { test, expect } from './baseFixtures.js';
import logData from "../cypress/fixtures/log.json";
import logsdata from "../../test-data/logs_data.json";
import { LogsPage } from '../pages/logsPage.js';
test.describe.configure({ mode: "parallel" });
// Helper functions
async function setupTest(page) {
const logsPage = new LogsPage(page);
await page.goto(process.env["ZO_BASE_URL"]);
// Login
if (await page.getByText('Login as internal user').isVisible()) {
await page.getByText('Login as internal user').click();
}
await page.waitForTimeout(1000);
await page.locator('[data-cy="login-user-id"]').fill(process.env["ZO_ROOT_USER_EMAIL"]);
await page.locator('[data-cy="login-password"]').fill(process.env["ZO_ROOT_USER_PASSWORD"]);
await page.locator('[data-cy="login-sign-in"]').click();
await page.waitForTimeout(4000);
return logsPage;
}
async function ingestTestData(page) {
const orgId = process.env["ORGNAME"];
const streamName = "e2e_automate";
const basicAuthCredentials = Buffer.from(
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
).toString('base64');
const headers = {
"Authorization": `Basic ${basicAuthCredentials}`,
"Content-Type": "application/json",
};
await page.evaluate(async ({ url, headers, orgId, streamName, logsdata }) => {
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
method: 'POST',
headers: headers,
body: JSON.stringify(logsdata)
});
return await fetchResponse.json();
}, {
url: process.env.INGESTION_URL,
headers: headers,
orgId: orgId,
streamName: streamName,
logsdata: logsdata
});
}
async function applyQuery(page) {
const search = page.waitForResponse(logData.applyQuery);
await page.waitForTimeout(3000);
await page.locator("[data-test='logs-search-bar-refresh-btn']").click({ force: true });
await expect.poll(async () => (await search).status()).toBe(200);
}
test.describe("Search Partition Tests", () => {
let logsPage;
test.beforeEach(async ({ page }) => {
logsPage = await setupTest(page);
await ingestTestData(page);
await page.waitForTimeout(2000);
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
await logsPage.selectStreamAndStreamTypeForLogs("e2e_automate");
await applyQuery(page);
});
test("should verify search partition and search API calls for histogram query", async ({ page }) => {
const isStreamingEnabled = process.env["ZO_STREAMING_ENABLED"] === "true";
const histogramQuery = `SELECT histogram(_timestamp) as "x_axis_1", count(distinct(kubernetes_container_name)) as "y_axis_1" FROM "e2e_automate" GROUP BY x_axis_1 ORDER BY x_axis_1 ASC`;
// Setup and execute query
await logsPage.executeHistogramQuery(histogramQuery);
await logsPage.toggleHistogramAndExecute();
if (!isStreamingEnabled) {
// Verify search partition response
const searchPartitionData = await logsPage.verifySearchPartitionResponse();
const searchCalls = await logsPage.captureSearchCalls();
expect(searchCalls.length).toBe(searchPartitionData.partitions.length);
// Verify each partition has a matching search call
for (const partition of searchPartitionData.partitions) {
const matchingCall = searchCalls.find(call =>
call.start_time === partition[0] &&
call.end_time === partition[1]
);
expect(matchingCall).toBeTruthy();
expect(matchingCall.sql).toContain('SELECT histogram(_timestamp');
}
} else {
await logsPage.verifyStreamingModeResponse();
}
// Verify histogram state
await logsPage.verifyHistogramState();
});
});