Files
openobserve/web/src/components/alerts/AddAlert.spec.ts
T
Bhargav 50402cf46a fix: batch 3 package upgrade (#9755)
### **PR Type**
Enhancement


___

### **Description**
- Adjust Vitest config imports and coverage settings

- Refine coverage: add V8 provider and include patterns

- Bump Vitest, Vitest UI, and canvas mock versions

- Upgrade dotenv and remove unused type definitions


___

### Diagram Walkthrough


```mermaid
flowchart LR
  A["web/vitest.config.ts"] --> B["Import types corrected"]
  A --> C["Coverage provider & include updated"]
  D["web/package.json"] --> E["Bump Vitest & mocks versions"]
  D --> F["Upgrade dotenv & remove types"]
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>vitest.config.ts</strong><dd><code>Adjust Vitest config
imports and coverage</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

web/vitest.config.ts

<ul><li>Replace type import from <code>vitest/config</code> to
<code>vite</code><br> <li> Remove <code>VitestUserConfig</code> cast for
mergeConfig<br> <li> Add coverage provider <code>v8</code><br> <li>
Update coverage include pattern to
<code>src/**/*.{js,ts,vue}</code></ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9755/files#diff-c3fc010b682e68f2ed0b7b1485b8aa513443a66d500903b8dff1d65908d02aae">+4/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Upgrade devDependencies
versions</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>

web/package.json

<ul><li>Bump <code>@vitest/coverage-v8</code> and
<code>@vitest/ui</code> to 4.x<br> <li> Upgrade <code>vitest</code> and
<code>vitest-canvas-mock</code> versions<br> <li> Update
<code>dotenv</code> to <code>^17.2.3</code><br> <li> Remove unused
<code>@types/dompurify</code></ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9755/files#diff-b861012a5dd72b8a9f3281b7cf09f5a779c98569d040b1bbc1db50f1b15e7cce">+5/-6</a>&nbsp;
&nbsp; &nbsp; </td>

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

</details>

___
2025-12-25 19:18:59 +05:30

2065 lines
70 KiB
TypeScript

// Copyright 2023 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { flushPromises, mount } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { nextTick } from "vue";
import AddAlert from "@/components/alerts/AddAlert.vue";
import alertsService from "@/services/alerts";
import { Dialog, Notify } from "quasar";
import store from "@/test/unit/helpers/store";
import { installQuasar } from "@/test/unit/helpers";
import router from "@/test/unit/helpers/router";
import { generateWhereClause } from "@/utils/alerts/alertQueryBuilder";
import { detectConditionsVersion } from "@/utils/alerts/alertDataTransforms";
import PreviewAlert from "@/components/alerts/PreviewAlert.vue";
import i18n from "@/locales";
import cronParser from "cron-parser";
import { useLocalOrganization } from "@/utils/zincutils";
import searchService from "@/services/search";
installQuasar({
plugins: [Dialog, Notify],
});
vi.mock('@/composables/useStreams', () => {
return {
default: () => ({
getStream: vi.fn().mockResolvedValue({
schema: [
{ name: 'field1', type: 'string' },
{ name: 'field2', type: 'int' },
]
}),
getStreams: vi.fn().mockResolvedValue({ list: [] }),
}),
};
});
vi.mock('@/composables/useFunctions', () => {
return {
default: () => ({
getAllFunctions: vi.fn().mockResolvedValue({
functions: [
{ name: 'avg', description: 'Average' },
{ name: 'sum', description: 'Sum' },
]
}),
}),
};
});
vi.mock('@/services/search', () => ({
default: {
search: vi.fn()
}
}));
vi.mock('@/composables/useParser', () => {
return {
default: () => ({
sqlParser: async () => ({
astify: vi.fn((query) => {
const lowerQuery = query.toLowerCase();
if (lowerQuery.includes("select *")) {
return {
columns: [
{ expr: { column: "*" } }
]
};
}
if (lowerQuery.includes("valid_column")) {
return {
columns: [
{ expr: { column: "valid_column" } }
]
};
}
if (lowerQuery.includes("default")) {
throw new Error("Syntax error near 'default'");
}
return { columns: [] };
}),
sqlify: vi.fn(),
columnList: vi.fn(),
tableList: vi.fn(),
whiteListCheck: vi.fn(),
exprToSQL: vi.fn(),
parse: vi.fn(),
})
})
};
});
vi.mock('cron-parser', () => {
return {
default: {
parseExpression: vi.fn()
}
};
});
vi.mock('@/utils/zincutils', async () => {
const actual: any = await vi.importActual('@/utils/zincutils');
return {
...actual,
getUUID: vi.fn(() => 'mock-uuid'),
getTimezonesByOffset: vi.fn(() => Promise.resolve(['UTC'])),
} as any;
});
vi.mock('@/services/alerts', () => {
return {
default: {
create_by_alert_id: vi.fn(() =>
Promise.resolve({
data: {
code: 200,
message: "Alert saved",
id: "2yis4t0dJCU7Vs4D3sEYHDsvDKF",
name: "test_alert"
}
})
),
update_by_alert_id: vi.fn(() =>
Promise.resolve({
data: { success: true }
})
),
generate_sql: vi.fn(() =>
Promise.resolve({
data: {
sql: "SELECT * FROM test"
}
})
)
}
};
});
// Additional composable mocks to prevent inject() warnings
vi.mock("@/composables/useLogs", () => ({
default: vi.fn(() => ({
searchObj: { value: { loading: false, data: { queryResults: [], aggs: { histogram: [] } } } },
searchAggData: { value: { histogram: [], total: 0 } },
searchResultData: { value: { list: [] } },
getFunctions: vi.fn().mockResolvedValue([])
}))
}));
vi.mock("@/composables/useDashboard", () => ({
default: vi.fn(() => ({
dashboards: { value: [] },
loading: { value: false },
error: { value: null }
}))
}));
vi.mock("@/services/segment_analytics", () => ({
default: {
track: vi.fn()
}
}));
vi.mock("@/services/auth", () => ({
default: {
sign_in_user: vi.fn(),
sign_out: vi.fn(),
get_dex_config: vi.fn()
}
}));
vi.mock("@/services/organizations", () => ({
default: {
get_organization: vi.fn(),
list: vi.fn(),
add_members: vi.fn()
}
}));
vi.mock("@/services/billings", () => ({
default: {
get_billing_info: vi.fn(),
get_invoice_history: vi.fn()
}
}));
describe("AddAlert Component", () => {
let wrapper: any;
// Ensure async operations are complete
beforeEach(async () => {
const updateStreamsMock = vi.fn().mockResolvedValue({ success: true });
wrapper = mount(AddAlert, {
global: {
provide: {
store: store,
},
plugins: [i18n, router],
},
props: {
title: "Add Alert",
},
mocks: {
updateStreams: updateStreamsMock,
},
});
wrapper.vm.formData = {
name: "",
stream_type: "",
stream_name: "",
is_real_time: "false",
query_condition: {
conditions:
{
"or": [
{
"column": "",
"operator": ">",
"value": "",
"ignore_case": false
}
]
},
sql: "",
promql: "",
type: "custom",
aggregation: {
group_by: [""],
function: "avg",
having: {
column: "",
operator: ">=",
value: 1,
},
},
promql_condition: null,
vrl_function: null,
multi_time_range: [],
},
trigger_condition: {
period: 10,
operator: ">=",
frequency: 1,
cron: "",
threshold: 3,
silence: 10,
frequency_type: "minutes",
timezone: "UTC",
},
destinations: [],
context_attributes: {},
enabled: true,
description: "",
lastTriggeredAt: 0,
createdAt: "",
updatedAt: "",
owner: "",
lastEditedBy: "",
folder_id : "",
}
});
describe("Functions with input and output as expected", () => {
describe('general functions', () => {
it('filters streams via filterStreams', async () => {
// Setup indexOptions in the component
wrapper.vm.indexOptions = ['stream1', 'stream2', 'logstream', 'metrics'];
wrapper.vm.filteredStreams = [];
const mockUpdate = (cb: Function) => cb(); // immediately execute callback
// Call the filterStreams method
wrapper.vm.filterStreams('stream', mockUpdate);
await wrapper.vm.$nextTick(); // wait for reactivity to apply
// Verify the result
expect(wrapper.vm.filteredStreams).toEqual(['stream1', 'stream2', 'logstream']);
});
it('returns all streams when filter input is empty', async () => {
wrapper.vm.indexOptions = ['stream1', 'stream2', 'logstream', 'metrics'];
wrapper.vm.filteredStreams = [];
const mockUpdate = (cb: Function) => cb();
wrapper.vm.filterStreams('', mockUpdate);
await wrapper.vm.$nextTick();
expect(wrapper.vm.filteredStreams).toEqual(['stream1', 'stream2', 'logstream', 'metrics']);
});
it('updates stream fields via updateStreamFields', async () => {
await flushPromises();
// 🧪 2. Mock onInputUpdate
wrapper.vm.onInputUpdate = vi.fn();
// 🧪 4. Call the function
await wrapper.vm.updateStreamFields('testStream');
await wrapper.vm.$nextTick();
// 🧪 5. Expect originalStreamFields and filteredColumns to be updated
const expected = [
{ label: 'field1', value: 'field1', type: 'string' },
{ label: 'field2', value: 'field2', type: 'int' }
];
expect(wrapper.vm.originalStreamFields).toEqual(expected);
expect(wrapper.vm.filteredColumns).toEqual(expected);
});
});
describe('generateWhereClause function with all the possible combinations (V2 format)', () => {
let testStreamFieldsMap: any;
beforeEach(() => {
// Create streamFieldsMap for testing (don't use wrapper.vm.streamFieldsMap as it's computed)
testStreamFieldsMap = {
age: { label: 'age', value: 'age', type: 'Int64' },
city: { label: 'city', value: 'city', type: 'String' },
};
});
it('generates a simple where clause', () => {
// V2 format: filterType, logicalOperator, conditions array
const group = {
filterType: 'group',
logicalOperator: 'AND',
conditions: [
{ filterType: 'condition', column: 'age', operator: '>', value: 30, logicalOperator: 'AND' }
]
};
const result = generateWhereClause(group, testStreamFieldsMap);
// age is Int64 type, so no quotes around the value
expect(result).toBe("WHERE age > 30");
});
it('handles string value with quotes', () => {
const group = {
filterType: 'group',
logicalOperator: 'AND',
conditions: [
{ filterType: 'condition', column: 'city', operator: '=', value: 'delhi', logicalOperator: 'AND' }
]
};
const result = generateWhereClause(group, testStreamFieldsMap);
expect(result).toBe("WHERE city = 'delhi'");
});
it('handles contains operator without quotes', () => {
const group = {
filterType: 'group',
logicalOperator: 'AND',
conditions: [
{ filterType: 'condition', column: 'city', operator: 'contains', value: 'delhi', logicalOperator: 'AND' }
]
};
const result = generateWhereClause(group, testStreamFieldsMap);
expect(result).toBe("WHERE city LIKE '%delhi%'");
});
it('handles nested groups with AND/OR', () => {
// V2 format with nested group
// In V2, each item (condition or group) has logicalOperator field
// The operator connects the item to the PREVIOUS sibling (not used for first item)
// For nested groups, the group's logicalOperator field is used for BOTH:
// 1. Connecting this group to the previous sibling item
// 2. As the default operator for new items added within this group
//
// In the UI: when you add a nested group, it defaults to logicalOperator: 'OR'
// So if added to an AND parent as the 2nd item, it would show: "age > 30 OR (nested group)"
//
// For this test, we're modeling a case where:
// - Parent group has AND
// - First condition has AND (inherited from parent when created)
// - Nested group has AND to connect to previous condition
// - Inside nested group, conditions use OR
const group = {
filterType: 'group',
logicalOperator: 'AND', // Parent group uses AND
conditions: [
{
filterType: 'condition',
column: 'age',
operator: '>',
value: 30,
logicalOperator: 'AND' // Condition inherits parent's AND
},
{
filterType: 'group',
logicalOperator: 'AND', // This connects the group to previous condition with AND
groupId: 'nested-group-1',
conditions: [
// Conditions inside nested group use OR to join with each other
{ filterType: 'condition', column: 'city', operator: '=', value: 'delhi', logicalOperator: 'OR' },
{ filterType: 'condition', column: 'city', operator: '=', value: 'mumbai', logicalOperator: 'OR' }
]
}
]
};
const result = generateWhereClause(group, testStreamFieldsMap);
// Expected: age > 30 (first condition, Int64 no quotes) AND (nested group connected with AND) (city = delhi OR city = mumbai, String with quotes)
expect(result).toBe("WHERE age > 30 AND (city = 'delhi' OR city = 'mumbai')");
});
it('returns empty string if group is invalid', () => {
const result = generateWhereClause(null, testStreamFieldsMap);
expect(result).toBe("");
});
})
describe('generateSqlQuery function with all the possible combinations', () => {
beforeEach(() => {
wrapper.vm.originalStreamFields = [
{ value: 'geo_info_country', type: 'string', label: 'geo_info_country' }
];
// V2 format conditions
wrapper.vm.formData.query_condition.conditions = {
filterType: 'group',
logicalOperator: 'OR',
conditions: [
{
filterType: 'condition',
column: 'geo_info_country',
operator: '=',
value: 'india',
logicalOperator: 'OR',
},
],
};
wrapper.vm.formData.query_condition.aggregation.function = "";
wrapper.vm.formData.query_condition.aggregation.having.column = "";
wrapper.vm.formData.query_condition.aggregation.having.operator = ">=";
wrapper.vm.formData.query_condition.aggregation.having.value = 1;
wrapper.vm.formData.stream_name = "_rundata";
wrapper.vm.formData.stream_type = "logs";
wrapper.vm.generateWhereClause = vi.fn().mockReturnValue("WHERE geo_info_country = 'india'");
wrapper.vm.isAggregationEnabled = false;
});
it('generates basic count query when aggregation is disabled', async () => {
// Set aggregation disabled
wrapper.vm.isAggregationEnabled = false;
wrapper.vm.generateWhereClause = vi.fn().mockReturnValue("WHERE geo_info_country = 'india'");
const result = wrapper.vm.generateSqlQuery();
await flushPromises();
expect(result).toBe(
'SELECT histogram(_timestamp) AS zo_sql_key, COUNT(*) as zo_sql_val FROM "_rundata" WHERE geo_info_country = \'india\' GROUP BY zo_sql_key ORDER BY zo_sql_key ASC'
);
});
it('generates query with aggregation function and no groupBy', async () => {
wrapper.vm.isAggregationEnabled = true;
wrapper.vm.formData.query_condition.aggregation.function = 'avg';
wrapper.vm.formData.query_condition.aggregation.having.column = 'action_error_count';
wrapper.vm.formData.query_condition.aggregation.group_by = [];
const result = wrapper.vm.generateSqlQuery();
await flushPromises();
expect(result).toBe(
'SELECT histogram(_timestamp) AS zo_sql_key, avg(action_error_count) as zo_sql_val FROM "_rundata" WHERE geo_info_country = \'india\' GROUP BY zo_sql_key ORDER BY zo_sql_key ASC'
);
});
it('generates query with groupBy and aggregation function', async () => {
wrapper.vm.isAggregationEnabled = true;
wrapper.vm.formData.query_condition.aggregation.function = 'avg';
wrapper.vm.formData.query_condition.aggregation.having.column = 'action_error_count';
wrapper.vm.formData.query_condition.aggregation.group_by = ['geo_info_city'];
const result = wrapper.vm.generateSqlQuery();
await flushPromises();
expect(result).toBe(
'SELECT histogram(_timestamp) AS zo_sql_key, avg(action_error_count) as zo_sql_val , concat(geo_info_city) as x_axis_2 FROM "_rundata" WHERE geo_info_country = \'india\' GROUP BY zo_sql_key , x_axis_2 ORDER BY zo_sql_key ASC'
);
});
it('generates query with percentile aggregation function', async () => {
wrapper.vm.isAggregationEnabled = true;
wrapper.vm.formData.query_condition.aggregation.function = 'p95';
wrapper.vm.formData.query_condition.aggregation.having.column = 'latency';
wrapper.vm.formData.query_condition.aggregation.group_by = [];
const result = wrapper.vm.generateSqlQuery();
await flushPromises();
expect(result).toBe(
'SELECT histogram(_timestamp) AS zo_sql_key, approx_percentile_cont(latency, 0.95) as zo_sql_val FROM "_rundata" WHERE geo_info_country = \'india\' GROUP BY zo_sql_key ORDER BY zo_sql_key ASC'
);
});
afterEach(() => {
vi.restoreAllMocks();
});
});
describe('getParser', () => {
beforeEach(async () => {
wrapper.vm.sqlQueryErrorMsg = "";
// Initialize parser manually by importing and setting it
const useParserModule = await import("@/composables/useParser");
const { sqlParser } = useParserModule.default();
wrapper.vm.parser = await sqlParser();
});
it('returns false and sets error for query with *', async() => {
const result = wrapper.vm.getParser("SELECT * FROM INDEX_NAME");
await flushPromises();
expect(result).toBe(false);
expect(wrapper.vm.sqlQueryErrorMsg).toBe("Selecting all columns is not allowed");
});
it('returns true for valid query without *', () => {
const result = wrapper.vm.getParser("SELECT valid_column FROM my_table");
expect(result).toBe(true);
expect(wrapper.vm.sqlQueryErrorMsg).toBe("");
});
it('returns true when parser throws an error (e.g. reserved keyword)', () => {
const result = wrapper.vm.getParser("SELECT field FROM default");
expect(result).toBe(true);
expect(wrapper.vm.sqlQueryErrorMsg).toBe(""); // No change expected
});
});
describe('getAlertPayload', () => {
beforeEach(() => {
wrapper.vm.formData = {
uuid: 'abc-123',
is_real_time: 'true',
description: ' Sample Alert ',
query_condition: {
type: 'custom',
vrl_function: null,
sql: undefined,
aggregation: null,
conditions: [{ column: 'status', operator: '=', value: '200' }],
promql_condition: null
},
trigger_condition: {
threshold: '5',
period: '60',
frequency: '10',
silence: '15',
},
context_attributes: [
{ key: 'env', value: 'prod' },
{ key: 'team', value: 'infra' },
]
}
});
it('generates correct payload for a new alert in custom tab', () => {
const { formData, getSelectedTab, isAggregationEnabled, store, beingUpdated } = wrapper.vm;
const result = wrapper.vm.getAlertPayload();
expect(result.uuid).toBeUndefined();
expect(result.is_real_time).toBe(true);
expect(result.context_attributes).toEqual({
env: 'prod',
team: 'infra'
});
expect(result.query_condition.type).toBe('custom');
expect(result.trigger_condition.threshold).toBe(5);
expect(result.trigger_condition.period).toBe(60);
expect(result.trigger_condition.frequency).toBe(10);
expect(result.trigger_condition.silence).toBe(15);
expect(result.description).toBe('Sample Alert');
expect(result.query_condition.aggregation).toBeNull();
expect(result.query_condition.promql_condition).toBeNull();
expect(result.query_condition.sql).toBeUndefined();
expect(result.createdAt).toBeDefined();
expect(result.owner).toBe(wrapper.vm.store.state.userInfo.email);
expect(result.lastTriggeredAt).toBeGreaterThan(0);
expect(result.lastEditedBy).toBe(wrapper.vm.store.state.userInfo.email);
});
});
describe('validate input fields with all the possible combinations', () => {
it('passes validation for real-time alert', () => {
wrapper.vm.formData.is_real_time = true;
wrapper.vm.formData.trigger_condition.silence = 5;
const result = wrapper.vm.validateInputs(wrapper.vm.formData, false);
expect(result).toBe(true);
});
it('fails when silence is NaN', () => {
wrapper.vm.formData.trigger_condition.silence = "a";
const notifyMock = vi.fn();
wrapper.vm.q.notify = notifyMock;
const result = wrapper.vm.validateInputs(wrapper.vm.formData);
expect(result).toBe(false);
expect(wrapper.vm.q.notify).toHaveBeenCalledWith(expect.objectContaining({
message: 'Silence Notification should not be empty'
}));
});
it('fails when period is < 1 or NaN', () => {
wrapper.vm.formData.trigger_condition.period = 0;
wrapper.vm.formData.is_real_time = false;
const notifyMock = vi.fn();
wrapper.vm.q.notify = notifyMock;
const result = wrapper.vm.validateInputs(wrapper.vm.formData);
expect(result).toBe(false);
expect(wrapper.vm.q.notify).toHaveBeenCalledWith(expect.objectContaining({
message: 'Period should be greater than 0'
}));
});
it('fails when aggregation fields are incomplete', () => {
wrapper.vm.formData.is_real_time = false;
wrapper.vm.formData.trigger_condition.threshold = "a";
const notifyMock = vi.fn();
wrapper.vm.q.notify = notifyMock;
const result = wrapper.vm.validateInputs(wrapper.vm.formData);
expect(result).toBe(false);
expect(wrapper.vm.q.notify).toHaveBeenCalledWith(expect.objectContaining({
message: 'Threshold should not be empty'
}));
});
it('fails when invalid cron expression is passed', () => {
cronParser.parseExpression = vi.fn().mockImplementation(() => {
throw new Error('Invalid cron');
});
wrapper.vm.formData.is_real_time = false;
wrapper.vm.formData.query_condition.aggregation = null;
wrapper.vm.formData.trigger_condition.silence = '5';
wrapper.vm.formData.trigger_condition.period = '60';
wrapper.vm.formData.trigger_condition.threshold = '10';
wrapper.vm.formData.trigger_condition.frequency_type = 'cron';
wrapper.vm.formData.trigger_condition.operator = '>';
wrapper.vm.formData.trigger_condition.cron = 'invalid-cron';
const result = wrapper.vm.validateInputs(wrapper.vm.formData);
expect(result).toBe(false); // function returns false when cron is invalid
});
it('passes when valid cron expression is provided', () => {
cronParser.parseExpression = vi.fn().mockImplementation(() => {
return {
next: vi.fn(),
}
});
wrapper.vm.formData.is_real_time = false
wrapper.vm.formData.trigger_condition.silence = '5';
wrapper.vm.formData.trigger_condition.period = '60';
wrapper.vm.formData.trigger_condition.threshold = '10';
wrapper.vm.formData.trigger_condition.frequency_type = 'cron';
wrapper.vm.formData.trigger_condition.operator = '>';
wrapper.vm.formData.trigger_condition.cron = '* * * * *';
wrapper.vm.formData.trigger_condition.timezone = 'UTC';
wrapper.vm.formData.query_condition.aggregation = null;
const result = wrapper.vm.validateInputs(wrapper.vm.formData, false);
expect(result).toBe(true); // function returns true when cron is valid
});
});
describe('onSubmit', () => {
let notifyMock: any;
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = vi.fn();
wrapper.vm.q = {
notify: vi.fn().mockReturnValue(() => {})
};
wrapper.vm.navigateToErrorField = vi.fn();
wrapper.vm.validateFormAndNavigateToErrorField = vi.fn().mockResolvedValue(true);
wrapper.vm.validateSqlQueryPromise = vi.fn().mockResolvedValue(true);
});
it.skip('should submit alert successfully for creating new alert', async () => {
const createAlertSpy = vi.spyOn(alertsService, 'create_by_alert_id');
wrapper.vm.q = {
notify: vi.fn(() => vi.fn())
};
wrapper.vm.formData.name = "test_alert";
wrapper.vm.formData.stream_name = "_rundata";
wrapper.vm.formData.stream_type = "logs";
wrapper.vm.formData.query_condition.conditions = {
"or": [
{ "column": "status", "operator": "=", "value": "200" }
]
};
wrapper.vm.formData.query_condition.aggregation = null;
wrapper.vm.formData.query_condition.promql_condition = null;
wrapper.vm.formData.query_condition.sql = undefined;
wrapper.vm.formData.query_condition.vrl_function = null;
wrapper.vm.formData.query_condition.multi_time_range = [];
wrapper.vm.formData.query_condition.type = "custom";
wrapper.vm.formData.query_condition.vrl_function = null;
wrapper.vm.formData.query_condition.sql = undefined;
wrapper.vm.formData.query_condition.promql_condition = null;
wrapper.vm.formData.query_condition.aggregation = null;
wrapper.vm.formData.destinations = ['test_destinations'];
wrapper.vm.formData.context_attributes = ['test_context_attributes'];
wrapper.vm.formData.enabled = true;
wrapper.vm.formData.owner = 'test_owner';
wrapper.vm.formData.lastTriggeredAt = 0;
wrapper.vm.formData.lastEditedBy = '122122122';
wrapper.vm.formData.folder_id = 'test_folder_id';
await wrapper.vm.validateInputs(wrapper.vm.formData);
await wrapper.vm.getAlertPayload();
await wrapper.vm.onSubmit();
await flushPromises();
let mockResponse: any = {
data: {
"code": 200,
"message": "Alert saved",
"id": "2yis4t0dJCU7Vs4D3sEYHDsvDKF",
"name": "test_alert"
},
}
vi.mocked(alertsService.create_by_alert_id).mockResolvedValue(mockResponse);
await flushPromises();
//because we reset the data in the formData after the alert is saved
expect(wrapper.vm.formData.name).toBe('');
expect(wrapper.vm.formData.stream_name).toBe('');
expect(wrapper.vm.formData.stream_type).toBe('');
});
it.skip('should update alert successfully for updating existing alert', async () => {
wrapper.vm.q = {
notify: vi.fn(() => vi.fn())
};
wrapper.vm.formData.name = "test_alert_for_updating";
wrapper.vm.formData.id = "2yis4t0dJCU7Vs4D3sEYHDsvDKF";
wrapper.vm.formData.is_real_time = "false";
wrapper.vm.beingUpdated = true;
wrapper.vm.formData.stream_name = "_rundata";
wrapper.vm.formData.stream_type = "logs";
wrapper.vm.formData.query_condition.conditions = {
"or": [
{ "column": "status", "operator": "=", "value": "200" }
]
};
wrapper.vm.formData.query_condition.aggregation = null;
wrapper.vm.formData.query_condition.promql_condition = null;
wrapper.vm.formData.query_condition.sql = undefined;
wrapper.vm.formData.query_condition.vrl_function = null;
wrapper.vm.formData.query_condition.multi_time_range = [];
wrapper.vm.formData.query_condition.type = "custom";
wrapper.vm.formData.query_condition.vrl_function = null;
wrapper.vm.formData.query_condition.sql = undefined;
wrapper.vm.formData.query_condition.promql_condition = null;
wrapper.vm.formData.query_condition.aggregation = null;
wrapper.vm.formData.destinations = ['test_destinations'];
wrapper.vm.formData.context_attributes = ['test_context_attributes'];
wrapper.vm.formData.enabled = true;
wrapper.vm.formData.owner = 'test_owner';
wrapper.vm.formData.lastTriggeredAt = 0;
wrapper.vm.formData.lastEditedBy = '122122122';
wrapper.vm.formData.folder_id = 'test_folder_id';
await wrapper.vm.validateInputs(wrapper.vm.formData);
await wrapper.vm.getAlertPayload();
await wrapper.vm.onSubmit();
await flushPromises();
let mockResponse: any = {
data: {
"code": 200,
"message": "Alert updated",
"id": "2yis4t0dJCU7Vs4D3sEYHDsvDKF",
"name": "test_alert_for_updating"
},
}
vi.mocked(alertsService.update_by_alert_id).mockResolvedValue(mockResponse);
await flushPromises();
//because we reset the data in the formData after the alert is saved
expect(wrapper.vm.formData.name).toBe('');
expect(wrapper.vm.formData.stream_name).toBe('');
expect(wrapper.vm.formData.stream_type).toBe('');
});
it('should wait for validateSqlQueryPromise to resolve successfully', { timeout: 10000 }, async () => {
const dismissMock = vi.fn();
wrapper.vm.q = {
notify: vi.fn(() => dismissMock)
};
// Required for triggering that SQL block
wrapper.vm.formData.is_real_time = "false";
wrapper.vm.formData.query_condition.type = "sql";
wrapper.vm.formData.query_condition.sql = "SELECT * FROM test_table";
// Provide a fake resolve that resolves immediately
wrapper.vm.validateSqlQueryPromise = Promise.resolve();
// Return valid payload
wrapper.vm.getAlertPayload = vi.fn(() => ({
query_condition: {
type: "sql",
conditions: [],
},
}));
// Mock alertsService.create_by_alert_id
vi.mocked(alertsService.create_by_alert_id).mockResolvedValue({
data: {
code: 200,
message: "Success",
}
});
// Bypass validation
wrapper.vm.validateFormAndNavigateToErrorField = vi.fn().mockResolvedValue(true);
wrapper.vm.validateInputs = vi.fn().mockReturnValue(true);
wrapper.vm.transformFEToBE = vi.fn().mockReturnValue([]);
wrapper.vm.beingUpdated = false; // Setting to false to test creation path
await wrapper.vm.onSubmit();
await flushPromises();
expect(wrapper.vm.formData.name).toBe('');
expect(wrapper.vm.formData.stream_name).toBe('');
expect(wrapper.vm.formData.stream_type).toBe('');
});
it('should show error when validateSqlQueryPromise rejects', async () => {
const dismissMock = vi.fn();
const notifySpy = vi.fn(() => dismissMock);
wrapper.vm.q = {
notify: notifySpy
};
wrapper.vm.formData.is_real_time = "false";
wrapper.vm.formData.query_condition.type = "sql";
wrapper.vm.formData.query_condition.sql = "SELECT * FROM test_table";
wrapper.vm.formData.query_condition.cron = '* * * * *';
wrapper.vm.getAlertPayload = vi.fn(() => ({
query_condition: {
type: "sql",
conditions: [],
},
}));
// Mock validateSqlQueryPromise to reject with SELECT * error
wrapper.vm.validateSqlQueryPromise = Promise.reject({
message: "Selecting all Columns in SQL query is not allowed."
});
// Bypass validation
wrapper.vm.validateFormAndNavigateToErrorField = vi.fn().mockResolvedValue(true);
wrapper.vm.validateInputs = vi.fn().mockReturnValue(true);
wrapper.vm.transformFEToBE = vi.fn().mockReturnValue([]);
await wrapper.vm.onSubmit();
await flushPromises();
// Check that notify was called (exact message may vary based on error handling)
expect(notifySpy).toHaveBeenCalled();
});
});
describe('validateSqlQuery', () => {
beforeEach(() => {
wrapper.vm.q = { notify: vi.fn() };
wrapper.vm.getParser = vi.fn(() => true);
wrapper.vm.formData.query_condition.vrl_function = '';
wrapper.vm.formData.query_condition.type = 'sql';
wrapper.vm.formData.stream_name = '_rundata';
wrapper.vm.formData.stream_type = 'logs';
wrapper.vm.buildQueryPayload = vi.fn(() => ({
query: { start_time: 100000 },
aggs: {}
}));
wrapper.vm.store = {
state: {
selectedOrganization: { identifier: 'org-123' }
}
};
});
it('validates successfully when SQL is valid', async () => {
wrapper.vm.formData.query_condition.sql = 'SELECT * FROM "_rundata"';
searchService.search.mockResolvedValue({ data: {} });
await wrapper.vm.validateSqlQuery();
await flushPromises();
expect(wrapper.vm.sqlQueryErrorMsg).toBe('Selecting all columns is not allowed');
});
it('validates successfully when SQL is valid', async () => {
wrapper.vm.formData.query_condition.sql = 'SELECT job FROM "_rundata"';
searchService.search.mockResolvedValue({ data: {} });
await wrapper.vm.validateSqlQuery();
await flushPromises();
expect(wrapper.vm.sqlQueryErrorMsg).toBe('');
});
});
describe('retransformBEToFE', () => {
it('should return null if input is null or undefined', () => {
expect(wrapper.vm.retransformBEToFE(null)).toBeNull();
expect(wrapper.vm.retransformBEToFE(undefined)).toBeNull();
});
it('should return null if input has multiple keys', () => {
const invalid = { and: [], or: [] };
expect(wrapper.vm.retransformBEToFE(invalid)).toBeNull();
});
it('should transform a simple AND condition', () => {
const input = {
and: [
{ column: 'name', operator: '=', value: 'John', ignore_case: false }
]
};
const result = wrapper.vm.retransformBEToFE(input);
expect(result).toEqual({
groupId: 'mock-uuid',
label: 'and',
items: [
{
column: 'name',
operator: '=',
value: 'John',
ignore_case: false,
id: 'mock-uuid'
}
]
});
});
it('should transform nested condition groups', () => {
const input = {
or: [
{
and: [
{ column: 'age', operator: '>', value: 30, ignore_case: false }
]
},
{ column: 'status', operator: '=', value: 'active', ignore_case: true }
]
};
const result = wrapper.vm.retransformBEToFE(input);
expect(result).toEqual({
groupId: 'mock-uuid',
label: 'or',
items: [
{
groupId: 'mock-uuid',
label: 'and',
items: [
{
column: 'age',
operator: '>',
value: 30,
ignore_case: false,
id: 'mock-uuid'
}
]
},
{
column: 'status',
operator: '=',
value: 'active',
ignore_case: true,
id: 'mock-uuid'
}
]
});
});
});
});
describe('lifecycle-created and computed handlers', () => {
it('sets beingUpdated based on modelValue and normalizes is_real_time', async () => {
const wrapper = mount(AddAlert, {
global: {
provide: { store },
plugins: [i18n, router],
},
props: {
isUpdated: true,
modelValue: {
name: 'n',
stream_type: 'logs',
stream_name: 's',
is_real_time: false,
query_condition: { type: 'custom', conditions: { or: [] }, aggregation: null },
trigger_condition: { period: 1, operator: '>=', frequency: 1, cron: '', threshold: 1, silence: 1, frequency_type: 'minutes', timezone: 'UTC' },
destinations: [],
context_attributes: {},
enabled: true,
},
destinations: [{ name: 'email' }],
},
});
expect(wrapper.vm.beingUpdated).toBe(true);
expect(wrapper.vm.formData.is_real_time).toBe('false');
expect(wrapper.vm.getFormattedDestinations).toEqual(['email']);
});
});
describe('getFormattedCondition and generateWhereClause edge cases (V2 format)', () => {
let wrapper: any;
beforeEach(() => {
wrapper = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
wrapper.vm.originalStreamFields = [
{ value: 'n', type: 'Int64' },
{ value: 's', type: 'String' }
];
});
it('formats numeric types without quotes and string with quotes', () => {
const group = {
filterType: 'group',
logicalOperator: 'AND',
conditions: [
{ filterType: 'condition', column: 'n', operator: '>=', value: 10, logicalOperator: 'AND' },
{ filterType: 'condition', column: 's', operator: '=', value: 'x', logicalOperator: 'AND' }
]
};
expect(generateWhereClause(group, wrapper.vm.streamFieldsMap)).toBe("WHERE n >= 10 AND s = 'x'");
});
it('supports not_contains/NotContains variations', () => {
const group = {
filterType: 'group',
logicalOperator: 'OR',
conditions: [
{ filterType: 'condition', column: 's', operator: 'not_contains', value: 'bad', logicalOperator: 'OR' },
{ filterType: 'condition', column: 's', operator: 'NotContains', value: 'worse', logicalOperator: 'OR' }
]
};
expect(generateWhereClause(group, wrapper.vm.streamFieldsMap)).toBe("WHERE s NOT LIKE '%bad%' OR s NOT LIKE '%worse%'");
});
it('returns empty for invalid items', () => {
const group = {
filterType: 'group',
logicalOperator: 'AND',
conditions: [{ foo: 1 }]
} as any;
expect(generateWhereClause(group, wrapper.vm.streamFieldsMap)).toBe('');
});
});
describe('generateSqlQuery variations (V2 format)', () => {
let wrapper: any;
beforeEach(() => {
wrapper = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
wrapper.vm.formData.stream_name = '_rundata';
wrapper.vm.formData.stream_type = 'logs';
// V2 format conditions
wrapper.vm.formData.query_condition.conditions = {
filterType: 'group',
logicalOperator: 'AND',
conditions: [
{ filterType: 'condition', column: 'status', operator: '=', value: '200', logicalOperator: 'AND' }
]
};
wrapper.vm.generateWhereClause = vi.fn().mockReturnValue("WHERE status = '200'");
});
it('counts when aggregation invalid/missing', () => {
wrapper.vm.isAggregationEnabled = true;
wrapper.vm.formData.query_condition.aggregation = { group_by: [], function: '', having: { column: '', operator: '>=', value: 1 } };
const q = wrapper.vm.generateSqlQuery();
expect(q).toContain('COUNT(*) as zo_sql_val');
});
it('handles multiple group_by concat alias', () => {
wrapper.vm.isAggregationEnabled = true;
wrapper.vm.formData.query_condition.aggregation = { group_by: ['c1','c2'], function: 'sum', having: { column: 'latency', operator: '>=', value: 1 } };
const q = wrapper.vm.generateSqlQuery();
expect(q).toContain('GROUP BY zo_sql_key , x_axis_2');
});
it('handles percentile map p50', () => {
wrapper.vm.isAggregationEnabled = true;
wrapper.vm.formData.query_condition.aggregation = { group_by: [], function: 'p50', having: { column: 'latency', operator: '>=', value: 1 } };
const q = wrapper.vm.generateSqlQuery();
expect(q).toContain('approx_percentile_cont(latency, 0.5)');
});
});
describe('getParser reserved word and star', () => {
let wrapper: any;
beforeEach(async () => {
wrapper = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
await flushPromises();
});
it('rejects SELECT * detection', () => {
expect(wrapper.vm.getParser('SELECT * FROM t')).toBe(false);
expect(wrapper.vm.sqlQueryErrorMsg).toBe('Selecting all columns is not allowed');
});
it('ignores parser errors and returns true', () => {
expect(wrapper.vm.getParser('SELECT c FROM default')).toBe(true);
});
});
describe('getAlertPayload branches', () => {
let wrapper: any;
beforeEach(() => {
wrapper = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('sets tabs: promql clears sql and conditions', () => {
wrapper.vm.scheduledAlertRef = { tab: 'promql' };
wrapper.vm.formData.query_condition.promql = 'up';
const p = wrapper.vm.getAlertPayload();
expect(p.query_condition.sql).toBe('');
// Conditions may be initialized as V2 structure or empty array
expect(p.query_condition.conditions).toBeDefined();
});
it('sql tab keeps sql and nulls promql_condition', () => {
wrapper.vm.scheduledAlertRef = { tab: 'sql' };
wrapper.vm.formData.query_condition.sql = 'select x';
const p = wrapper.vm.getAlertPayload();
expect(p.query_condition.promql_condition).toBeNull();
});
it('custom tab nulls aggregation when disabled', () => {
wrapper.vm.scheduledAlertRef = { tab: 'custom' };
wrapper.vm.isAggregationEnabled = false;
const p = wrapper.vm.getAlertPayload();
expect(p.query_condition.aggregation).toBeNull();
});
});
describe('routeToCreateDestination builds URL', () => {
it('calls window.open with route href', () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
const spy = vi.spyOn(window, 'open').mockImplementation(() => null);
w.vm.routeToCreateDestination();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
describe('expand state and destinations updates', () => {
it('updateExpandState replaces object', () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
const v = { alertSetup: false, queryMode: false, advancedSetup: false, realTimeMode: false, thresholds: false, multiWindowSelection: true };
w.vm.updateExpandState(v);
expect(w.vm.expandState).toEqual(v);
});
it('updateDestinations writes list', () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
w.vm.updateDestinations(['a']);
expect(w.vm.formData.destinations).toEqual(['a']);
});
});
describe('transformFEToBE and retransformBEToFE additional', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('transformFEToBE returns {} for invalid', () => {
expect(w.vm.transformFEToBE(null)).toEqual({});
expect(w.vm.transformFEToBE({ label: 'x', items: [] })).toEqual({});
});
it('retransformBEToFE returns null for invalid keys', () => {
expect(w.vm.retransformBEToFE({} as any)).toBeNull();
});
});
describe('V2 Conditions Version Field Placement', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
w.vm.formData.name = "test_v2_alert";
w.vm.formData.stream_name = "_rundata";
w.vm.formData.stream_type = "logs";
w.vm.formData.is_real_time = false;
w.vm.scheduledAlertRef = { tab: 'custom' };
w.vm.isAggregationEnabled = false;
});
it('should wrap V2 conditions with version field INSIDE conditions object when saving', () => {
// Set up V2 format conditions
const v2Conditions = {
filterType: 'group',
logicalOperator: 'AND',
groupId: 'test-group',
conditions: [
{
id: 'test-id',
filterType: 'condition',
column: 'status',
operator: '=',
value: '200',
logicalOperator: 'AND',
}
]
};
w.vm.formData.query_condition.conditions = v2Conditions;
w.vm.formData.query_condition.aggregation = null;
w.vm.formData.query_condition.type = 'custom';
const payload = w.vm.getAlertPayload();
// Simulate the version wrapping logic from onSubmit()
const version = detectConditionsVersion(w.vm.formData.query_condition.conditions);
if (version === 2) {
payload.query_condition.conditions = {
version: 2,
conditions: w.vm.formData.query_condition.conditions,
};
}
// Backend expects: conditions: { version: 2, conditions: {...} }
expect(payload.query_condition.conditions).toHaveProperty('version');
expect(payload.query_condition.conditions.version).toBe(2);
expect(payload.query_condition.conditions).toHaveProperty('conditions');
expect(payload.query_condition.conditions.conditions.filterType).toBe('group');
expect(payload.query_condition.conditions.conditions.logicalOperator).toBe('AND');
});
it('should NOT have version field at query_condition level for V2', () => {
// Set up V2 format conditions
w.vm.formData.query_condition.conditions = {
filterType: 'group',
logicalOperator: 'OR',
groupId: 'test-group',
conditions: []
};
const payload = w.vm.getAlertPayload();
// Simulate the version wrapping logic from onSubmit()
const version = detectConditionsVersion(w.vm.formData.query_condition.conditions);
if (version === 2) {
payload.query_condition.conditions = {
version: 2,
conditions: w.vm.formData.query_condition.conditions,
};
}
// Version should NOT be at this level
expect(payload.query_condition).not.toHaveProperty('version');
// But should be inside conditions
expect(payload.query_condition.conditions).toHaveProperty('version');
});
it('should handle V1 conditions without version field', () => {
// Set up V1 format conditions (tree-based)
w.vm.formData.query_condition.conditions = {
or: [
{ column: 'status', operator: '=', value: '200', ignore_case: true }
]
};
const payload = w.vm.getAlertPayload();
// V1 format should not have version field at any level
expect(payload.query_condition).not.toHaveProperty('version');
expect(payload.query_condition.conditions).not.toHaveProperty('version');
// Should be transformed to V1 backend format
expect(payload.query_condition.conditions).toHaveProperty('or');
});
it('should correctly extract V2 conditions when loading from backend', async () => {
// Simulate backend response with V2 format
w.vm.formData.query_condition.conditions = {
version: 2,
conditions: {
filterType: 'group',
logicalOperator: 'AND',
groupId: 'backend-group',
conditions: [
{
id: 'backend-id',
filterType: 'condition',
column: 'level',
operator: '=',
value: 'error',
logicalOperator: 'AND',
}
]
}
};
// Call the data loading logic (simulating mounted hook behavior)
await w.vm.$nextTick();
// After loading, conditions should be extracted from nested structure
// The actual extraction happens in the component's data loading logic
// This test verifies the structure matches backend expectations
expect(w.vm.formData.query_condition.conditions.version).toBe(2);
expect(w.vm.formData.query_condition.conditions.conditions).toBeDefined();
expect(w.vm.formData.query_condition.conditions.conditions.filterType).toBe('group');
});
});
describe('validateFormAndNavigateToErrorField and navigateToErrorField', () => {
it('navigates on invalid and returns false', async () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
const fakeRef = { validate: vi.fn().mockResolvedValue(false), $el: { querySelector: vi.fn().mockReturnValue({ scrollIntoView: vi.fn() }) } } as any;
const spy = vi.spyOn(fakeRef.$el, 'querySelector');
const res = await w.vm.validateFormAndNavigateToErrorField(fakeRef);
expect(res).toBe(false);
expect(spy).toHaveBeenCalled();
});
it('returns true on valid', async () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
const fakeRef = { validate: vi.fn().mockResolvedValue(true), $el: { querySelector: vi.fn() } } as any;
const res = await w.vm.validateFormAndNavigateToErrorField(fakeRef);
expect(res).toBe(true);
});
});
describe('openJsonEditor, saveAlertJson, prepareAndSaveAlert', () => {
it('opens editor dialog', () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
w.vm.openJsonEditor();
expect(w.vm.showJsonEditorDialog).toBe(true);
});
it('saveAlertJson validates and sets validationErrors on failure', async () => {
const w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] }, props: { destinations: [{ name: 'email' }] } });
const bad = JSON.stringify({ name: '', stream_type: 'logs', stream_name: 's', is_real_time: false, query_condition: { type: 'sql', sql: '' }, trigger_condition: { period: 0, operator: '>=', frequency: 1, cron: '', threshold: 1, silence: 1, frequency_type: 'minutes', timezone: 'UTC' }, destinations: [], context_attributes: [], enabled: true });
await w.vm.saveAlertJson(bad);
expect(Array.isArray(w.vm.validationErrors)).toBe(true);
expect(w.vm.validationErrors.length).toBeGreaterThan(0);
});
});
describe('Wizard Step Navigation', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
w.vm.formData.name = "test_alert";
w.vm.formData.stream_name = "_rundata";
w.vm.formData.stream_type = "logs";
});
it('should initialize at step 1', () => {
expect(w.vm.wizardStep).toBe(1);
});
it('should initialize lastValidStep at 1', () => {
expect(w.vm.lastValidStep).toBe(1);
});
it('should have wizardStepper ref', () => {
expect(w.vm.wizardStepper).toBeDefined();
});
it('should navigate to step 2 via goToNextStep when step 1 is valid', async () => {
w.vm.wizardStep = 1;
// Mock validateStep instead of validateCurrentStep
w.vm.validateStep = vi.fn().mockResolvedValue(true);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(2);
});
it('should not navigate to step 2 if step 1 validation fails', async () => {
w.vm.wizardStep = 1;
// Mock step1Ref validate to return false
w.vm.step1Ref = {
validate: vi.fn().mockResolvedValue(false)
};
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(1);
});
it('should go back from step 2 to step 1', () => {
w.vm.wizardStep = 2;
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(1);
});
it('should go back from step 3 to step 2 for scheduled alerts', () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 3;
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(2);
});
it('should go back from step 4 to step 3 for scheduled alerts', () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 4;
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(3);
});
it('should go back from step 5 to step 4 for scheduled alerts', () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 5;
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(4);
});
it('should go back from step 6 to step 5 for scheduled alerts', () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 6;
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(5);
});
it('should skip steps 3 and 5 for real-time alerts when going back', () => {
w.vm.formData.is_real_time = 'true';
// From step 6 should go to 4
w.vm.wizardStep = 6;
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(4);
// From step 4 should go to 2
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(2);
// From step 2 should go to 1
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(1);
});
it('should not go back from step 1', () => {
w.vm.wizardStep = 1;
w.vm.goToPreviousStep();
// goToPreviousStep decrements by 1, so it goes to 0
// In practice, the UI should prevent navigation below step 1
expect(w.vm.wizardStep).toBe(0);
});
it('should identify step 6 as last step for scheduled alerts', () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 6;
expect(w.vm.isLastStep).toBe(true);
});
it('should identify step 6 as last step for real-time alerts', () => {
w.vm.formData.is_real_time = 'true';
w.vm.wizardStep = 6;
expect(w.vm.isLastStep).toBe(true);
});
it('should not identify step 5 as last step', () => {
w.vm.wizardStep = 5;
expect(w.vm.isLastStep).toBe(false);
});
it('should not identify step 1 as last step', () => {
w.vm.wizardStep = 1;
expect(w.vm.isLastStep).toBe(false);
});
it('should skip step 3 for real-time alerts when navigating forward', async () => {
w.vm.formData.is_real_time = 'true';
w.vm.wizardStep = 2;
w.vm.validateStep = vi.fn().mockResolvedValue(true);
await w.vm.goToNextStep();
await flushPromises();
// Should skip step 3 and go to step 4
expect(w.vm.wizardStep).toBe(4);
});
it('should skip step 5 for real-time alerts when navigating forward', async () => {
w.vm.formData.is_real_time = 'true';
w.vm.wizardStep = 4;
w.vm.validateStep = vi.fn().mockResolvedValue(true);
await w.vm.goToNextStep();
await flushPromises();
// Should skip step 5 and go to step 6
expect(w.vm.wizardStep).toBe(6);
});
it('should navigate through all steps for scheduled alerts', async () => {
w.vm.formData.is_real_time = 'false';
// Add a destination so step 4 validation passes
w.vm.formData.destinations = ['test-destination'];
// Mock all step refs to validate successfully
w.vm.step1Ref = { validate: vi.fn().mockResolvedValue(true) };
w.vm.step2Ref = { validate: vi.fn().mockResolvedValue(true) };
w.vm.step4Ref = { validate: vi.fn().mockResolvedValue(true) };
expect(w.vm.wizardStep).toBe(1);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(2);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(3);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(4);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(5);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(6);
});
});
describe('Wizard Step Validation', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should validate step 1 (Alert Setup)', async () => {
w.vm.step1Ref = {
validate: vi.fn().mockResolvedValue(true)
};
w.vm.wizardStep = 1;
const isValid = await w.vm.validateStep(1);
expect(w.vm.step1Ref.validate).toHaveBeenCalled();
expect(isValid).toBe(true);
});
it('should fail validation if step 1 ref has no validate method', async () => {
w.vm.step1Ref = {};
w.vm.wizardStep = 1;
const isValid = await w.vm.validateStep(1);
// When step ref has no validate method, validateStep returns true (continues)
expect(isValid).toBe(true);
});
it('should validate step 2 (Query Configuration)', async () => {
w.vm.step2Ref = {
validate: vi.fn().mockResolvedValue(true)
};
w.vm.wizardStep = 2;
const isValid = await w.vm.validateStep(2);
expect(w.vm.step2Ref.validate).toHaveBeenCalled();
expect(isValid).toBe(true);
});
it('should return false if step 2 validation fails', async () => {
w.vm.step2Ref = {
validate: vi.fn().mockResolvedValue(false)
};
w.vm.wizardStep = 2;
const isValid = await w.vm.validateStep(2);
expect(isValid).toBe(false);
});
it('should validate step 3 (Compare with Past) for scheduled alerts', async () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 3;
const isValid = await w.vm.validateStep(3);
// Step 3 has no validation, should return true
expect(isValid).toBe(true);
});
it('should validate step 4 (Alert Settings)', async () => {
w.vm.step4Ref = {
validate: vi.fn().mockResolvedValue(true)
};
w.vm.wizardStep = 4;
const isValid = await w.vm.validateStep(4);
expect(w.vm.step4Ref.validate).toHaveBeenCalled();
expect(isValid).toBe(true);
});
it('should return false if step 4 validation fails', async () => {
w.vm.step4Ref = {
validate: vi.fn().mockResolvedValue(false)
};
w.vm.wizardStep = 4;
const isValid = await w.vm.validateStep(4);
expect(isValid).toBe(false);
});
it('should validate step 5 (Deduplication) for scheduled alerts', async () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 5;
const isValid = await w.vm.validateStep(5);
// Step 5 has no validation, should return true
expect(isValid).toBe(true);
});
it('should validate step 6 (Advanced)', async () => {
w.vm.wizardStep = 6;
const isValid = await w.vm.validateStep(6);
// Step 6 has no validation, should return true
expect(isValid).toBe(true);
});
it('should update lastValidStep when validation succeeds', async () => {
w.vm.step1Ref = {
validate: vi.fn().mockResolvedValue(true)
};
w.vm.wizardStep = 1;
w.vm.lastValidStep = 1;
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.lastValidStep).toBeGreaterThanOrEqual(1);
});
it('should not update lastValidStep when validation fails', async () => {
w.vm.step1Ref = {
validate: vi.fn().mockResolvedValue(false)
};
w.vm.wizardStep = 1;
w.vm.lastValidStep = 1;
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.lastValidStep).toBe(1);
});
});
describe('Wizard Step State Management', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should track current wizard step', () => {
expect(w.vm.wizardStep).toBeDefined();
expect(typeof w.vm.wizardStep).toBe('number');
});
it('should update wizardStep reactively', async () => {
w.vm.wizardStep = 2;
await nextTick();
expect(w.vm.wizardStep).toBe(2);
w.vm.wizardStep = 3;
await nextTick();
expect(w.vm.wizardStep).toBe(3);
});
it('should provide currentStepCaption for step 1', () => {
w.vm.wizardStep = 1;
expect(w.vm.currentStepCaption).toBe('Set the stage for your alert');
});
it('should provide currentStepCaption for step 2', () => {
w.vm.wizardStep = 2;
expect(w.vm.currentStepCaption).toBe('What should trigger the alert');
});
it('should provide currentStepCaption for step 3', () => {
w.vm.wizardStep = 3;
expect(w.vm.currentStepCaption).toBe('Compare current results with data from another time period');
});
it('should provide currentStepCaption for step 4', () => {
w.vm.wizardStep = 4;
expect(w.vm.currentStepCaption).toBe('Set your alert rules and choose how you\'d like to be notified.');
});
it('should provide currentStepCaption for step 5', () => {
w.vm.wizardStep = 5;
expect(w.vm.currentStepCaption).toBe('Avoid sending the same alert multiple times by grouping similar alerts together.');
});
it('should provide currentStepCaption for step 6', () => {
w.vm.wizardStep = 6;
expect(w.vm.currentStepCaption).toBe('Context variables, description, and row template');
});
it('should return empty caption for invalid step', () => {
w.vm.wizardStep = 99;
expect(w.vm.currentStepCaption).toBe('');
});
it('should have step refs defined', () => {
expect(w.vm.step1Ref).toBeDefined();
expect(w.vm.step2Ref).toBeDefined();
expect(w.vm.step4Ref).toBeDefined();
});
});
describe('Wizard Flow for Real-Time vs Scheduled', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should show all 6 steps for scheduled alerts', () => {
w.vm.formData.is_real_time = 'false';
// Steps 1, 2, 3, 4, 5, 6 should all be accessible
expect(w.vm.wizardStep).toBeDefined();
});
it('should skip step 3 for real-time alerts', () => {
w.vm.formData.is_real_time = 'true';
// Step 3 (Compare with Past) should be skipped
// This is controlled by v-if in template
});
it('should skip step 5 for real-time alerts', () => {
w.vm.formData.is_real_time = 'true';
// Step 5 (Deduplication) should be skipped
// This is controlled by v-if in template
});
it('should navigate 1->2->4->6 for real-time alerts', async () => {
w.vm.formData.is_real_time = 'true';
// Add a destination so step 4 validation passes
w.vm.formData.destinations = ['test-destination'];
// Mock all step refs to validate successfully
w.vm.step1Ref = { validate: vi.fn().mockResolvedValue(true) };
w.vm.step2Ref = { validate: vi.fn().mockResolvedValue(true) };
w.vm.step4Ref = { validate: vi.fn().mockResolvedValue(true) };
expect(w.vm.wizardStep).toBe(1);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(2);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(4); // Skips 3
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(6); // Skips 5
});
it('should navigate 1->2->3->4->5->6 for scheduled alerts', async () => {
w.vm.formData.is_real_time = 'false';
// Add a destination so step 4 validation passes
w.vm.formData.destinations = ['test-destination'];
// Mock all step refs to validate successfully
w.vm.step1Ref = { validate: vi.fn().mockResolvedValue(true) };
w.vm.step2Ref = { validate: vi.fn().mockResolvedValue(true) };
w.vm.step4Ref = { validate: vi.fn().mockResolvedValue(true) };
expect(w.vm.wizardStep).toBe(1);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(2);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(3);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(4);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(5);
await w.vm.goToNextStep();
await flushPromises();
expect(w.vm.wizardStep).toBe(6);
});
it('should handle switching from scheduled to real-time', async () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 3; // On Compare with Past step
w.vm.formData.is_real_time = 'true';
await nextTick();
// Should still maintain current step, but step 3 won't be visible
expect(w.vm.wizardStep).toBe(3);
});
});
describe('Wizard Step Component Props', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should pass formData to AlertSetup', () => {
const alertSetup = w.findComponent({ name: 'AlertSetup' });
if (alertSetup.exists()) {
expect(alertSetup.props().formData).toBeDefined();
}
});
it('should pass formData to QueryConfig', () => {
const queryConfig = w.findComponent({ name: 'QueryConfig' });
if (queryConfig.exists()) {
expect(queryConfig.props().inputData).toBeDefined();
}
});
it('should pass formData to AlertSettings', () => {
const alertSettings = w.findComponent({ name: 'AlertSettings' });
if (alertSettings.exists()) {
expect(alertSettings.props().formData).toBeDefined();
}
});
it('should pass formData to CompareWithPast', async () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 3;
await nextTick();
const compareWithPast = w.findComponent({ name: 'CompareWithPast' });
if (compareWithPast.exists()) {
expect(compareWithPast.props().multiTimeRange).toBeDefined();
}
});
it('should pass formData to Deduplication', async () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 5;
await nextTick();
const deduplication = w.findComponent({ name: 'Deduplication' });
if (deduplication.exists()) {
expect(deduplication.props().deduplication).toBeDefined();
}
});
it('should pass formData to Advanced', async () => {
w.vm.wizardStep = 6;
await nextTick();
const advanced = w.findComponent({ name: 'Advanced' });
if (advanced.exists()) {
expect(advanced.props().contextAttributes).toBeDefined();
}
});
});
describe('Wizard Right Column Integration', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should pass formData to AlertWizardRightColumn', () => {
const rightColumn = w.findComponent({ name: 'AlertWizardRightColumn' });
if (rightColumn.exists()) {
expect(rightColumn.props().formData).toBeDefined();
}
});
it('should pass wizardStep to AlertWizardRightColumn', () => {
const rightColumn = w.findComponent({ name: 'AlertWizardRightColumn' });
if (rightColumn.exists()) {
expect(rightColumn.props().wizardStep).toBeDefined();
}
});
it('should pass previewQuery to AlertWizardRightColumn', () => {
const rightColumn = w.findComponent({ name: 'AlertWizardRightColumn' });
if (rightColumn.exists()) {
expect(rightColumn.props().previewQuery).toBeDefined();
}
});
it('should pass isUsingBackendSql to AlertWizardRightColumn', () => {
const rightColumn = w.findComponent({ name: 'AlertWizardRightColumn' });
if (rightColumn.exists()) {
expect(rightColumn.props().isUsingBackendSql).toBeDefined();
}
});
it('should have previewAlertRef defined', () => {
expect(w.vm.previewAlertRef).toBeDefined();
});
});
describe('Wizard Step Button States', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should disable Back button on step 1', () => {
w.vm.wizardStep = 1;
// Back button should be disabled at step 1
expect(w.vm.wizardStep === 1).toBe(true);
});
it('should enable Back button on step 2+', () => {
w.vm.wizardStep = 2;
expect(w.vm.wizardStep > 1).toBe(true);
});
it('should disable Continue button on last step', () => {
w.vm.wizardStep = 6;
expect(w.vm.isLastStep).toBe(true);
});
it('should enable Continue button on non-last steps', () => {
w.vm.wizardStep = 1;
expect(w.vm.isLastStep).toBe(false);
w.vm.wizardStep = 2;
expect(w.vm.isLastStep).toBe(false);
});
it('should disable Save button until last step', () => {
w.vm.wizardStep = 1;
expect(w.vm.isLastStep).toBe(false);
w.vm.wizardStep = 5;
expect(w.vm.isLastStep).toBe(false);
});
it('should enable Save button on last step', () => {
w.vm.wizardStep = 6;
expect(w.vm.isLastStep).toBe(true);
});
});
describe('Wizard Navigation Edge Cases', () => {
let w: any;
beforeEach(() => {
w = mount(AddAlert, { global: { provide: { store }, plugins: [i18n, router] } });
});
it('should handle navigation with missing step refs', async () => {
w.vm.step1Ref = null;
w.vm.wizardStep = 1;
await w.vm.goToNextStep();
await flushPromises();
// When step ref is null, validation returns true and navigation proceeds
expect(w.vm.wizardStep).toBe(2);
});
it('should handle validation errors gracefully', async () => {
w.vm.step1Ref = {
validate: vi.fn().mockRejectedValue(new Error('Validation error'))
};
w.vm.wizardStep = 1;
// When validation throws an error, it may propagate
try {
await w.vm.goToNextStep();
await flushPromises();
} catch (error) {
// Error is expected
}
// Wizard step should remain at 1 or handle error gracefully
expect(w.vm.wizardStep).toBeGreaterThanOrEqual(1);
});
it('should handle rapid navigation clicks', async () => {
w.vm.validateStep = vi.fn().mockResolvedValue(true);
w.vm.wizardStep = 1;
// Simulate rapid clicks
const promise1 = w.vm.goToNextStep();
const promise2 = w.vm.goToNextStep();
await Promise.all([promise1, promise2]);
await flushPromises();
// Should handle gracefully
expect(w.vm.wizardStep).toBeGreaterThanOrEqual(1);
});
it('should handle switching alert type mid-wizard', async () => {
w.vm.formData.is_real_time = 'false';
w.vm.wizardStep = 3; // On Compare with Past
// Switch to real-time
w.vm.formData.is_real_time = 'true';
await nextTick();
// Navigation should work correctly
w.vm.goToPreviousStep();
expect(w.vm.wizardStep).toBe(2);
});
});
});