mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
781 lines
28 KiB
TypeScript
781 lines
28 KiB
TypeScript
// Copyright 2026 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 { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
|
import { defineComponent, h, KeepAlive, nextTick, reactive, ref } from "vue";
|
|
import { mount, flushPromises, type VueWrapper } from "@vue/test-utils";
|
|
import Index from "@/plugins/traces/Index.vue";
|
|
import i18n from "@/locales";
|
|
import store from "@/test/unit/helpers/store";
|
|
import router from "@/test/unit/helpers/router";
|
|
|
|
// The default 1 s waitFor flakes when a coverage-instrumented shard mounts the whole page.
|
|
vi.setConfig({ testTimeout: 15_000 });
|
|
const waitFor = (assertion: () => unknown) => vi.waitFor(assertion, { timeout: 10_000 });
|
|
|
|
const node = document.createElement("div");
|
|
node.setAttribute("id", "app");
|
|
document.body.appendChild(node);
|
|
|
|
const streamList = {
|
|
list: [
|
|
{
|
|
name: "default",
|
|
stream_type: "traces",
|
|
stats: { doc_time_min: 1, doc_time_max: 2, doc_num: 1 },
|
|
schema: [{ name: "trace_id", type: "Utf8" }],
|
|
settings: { partition_keys: {}, full_text_search_keys: [], max_query_range: 0 },
|
|
},
|
|
],
|
|
};
|
|
|
|
const freshSearchObj = () => ({
|
|
organizationIdentifier: "default",
|
|
loading: false,
|
|
loadingStream: false,
|
|
searchApplied: false,
|
|
runQuery: false,
|
|
config: { splitterModel: 20, lastSplitterPosition: 20, splitterLimit: [0, 40] },
|
|
meta: {
|
|
showFields: true,
|
|
showQuery: true,
|
|
showHistogram: true,
|
|
sqlMode: false,
|
|
searchMode: "traces",
|
|
resultGrid: {
|
|
rowsPerPage: 25,
|
|
sortBy: "start_time",
|
|
sortOrder: "desc",
|
|
showPagination: false,
|
|
navigation: {
|
|
currentRowIndex: 0,
|
|
selectionActive: false,
|
|
pendingPageSelection: null,
|
|
lastOpenedId: null,
|
|
},
|
|
},
|
|
liveMode: false,
|
|
serviceColors: {},
|
|
metricsRangeFilters: new Map(),
|
|
},
|
|
data: {
|
|
query: "",
|
|
editorValue: "",
|
|
errorMsg: "",
|
|
errorCode: 0,
|
|
errorDetail: "",
|
|
stream: {
|
|
streamLists: [] as any[],
|
|
selectedStream: { label: "", value: "" },
|
|
selectedStreamFields: [] as any[],
|
|
selectedFields: [] as string[],
|
|
functions: [],
|
|
},
|
|
streamResults: { list: [] as any[] },
|
|
queryResults: { hits: [] } as any,
|
|
sortedQueryResults: [],
|
|
histogram: { layout: {}, data: [] },
|
|
resultGrid: { currentPage: 0, columns: [], pageRequest: null, pageLoad: null },
|
|
datetime: {
|
|
startTime: 1700000000000000,
|
|
endTime: 1700000900000000,
|
|
relativeTimePeriod: "15m",
|
|
type: "relative",
|
|
},
|
|
queryPayload: null as any,
|
|
},
|
|
});
|
|
|
|
const searchObj = reactive(freshSearchObj());
|
|
|
|
const {
|
|
mockGetStreams,
|
|
mockGetStream,
|
|
mockFetch,
|
|
mockCancel,
|
|
mockLoadTracesParser,
|
|
mockTakeLanding,
|
|
mockRecordRecent,
|
|
mockSyncUrlViewState,
|
|
mockStopUrlSync,
|
|
} = vi.hoisted(() => ({
|
|
mockGetStreams: vi.fn(),
|
|
mockGetStream: vi.fn(),
|
|
mockFetch: vi.fn(),
|
|
mockCancel: vi.fn(),
|
|
mockLoadTracesParser: vi.fn(),
|
|
mockTakeLanding: vi.fn(),
|
|
mockRecordRecent: vi.fn(),
|
|
mockSyncUrlViewState: vi.fn(),
|
|
mockStopUrlSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/aws-exports", () => ({ default: { isCloud: "false", isEnterprise: "true" } }));
|
|
|
|
vi.mock("@/composables/useTraces", () => ({
|
|
default: () => ({
|
|
searchObj,
|
|
resetSearchObj: vi.fn(),
|
|
getUrlQueryParams: vi.fn(() => ({})),
|
|
applyUrlListState: vi.fn(() => ({ currentPage: null })),
|
|
syncUrlViewState: mockSyncUrlViewState,
|
|
patchUrlViewState: vi.fn(),
|
|
copyTracesUrl: vi.fn(),
|
|
formatTracesMetaData: vi.fn((hits) => hits),
|
|
setServiceColors: vi.fn(),
|
|
loadLocalLogFilterField: vi.fn(),
|
|
updatedLocalLogFilterField: vi.fn(),
|
|
loadTracesParser: mockLoadTracesParser,
|
|
tracesParser: { value: { astify: vi.fn(() => ({ where: null })) } },
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/utils/streamPersist", () => ({
|
|
saveTracesStream: vi.fn(),
|
|
restoreTracesStream: vi.fn(() => ""),
|
|
}));
|
|
|
|
vi.mock("@/composables/useStreams", () => ({
|
|
default: () => ({
|
|
getStreams: mockGetStreams,
|
|
getStream: mockGetStream,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/composables/useNotifications", () => ({
|
|
default: () => ({ showErrorNotification: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock("@/composables/useStreamingSearch", () => ({
|
|
default: () => ({
|
|
fetchQueryDataWithHttpStream: mockFetch,
|
|
cancelStreamQueryBasedOnRequestId: mockCancel,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/services/product_analytics", () => ({ default: { track: vi.fn() } }));
|
|
|
|
vi.mock("@/utils/queryHistory/routes", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("@/utils/queryHistory/routes")>()),
|
|
takeRecentLanding: mockTakeLanding,
|
|
}));
|
|
|
|
vi.mock("@/composables/queryHistory/useRecentSearches", () => ({
|
|
useRecentSearches: () => ({ record: mockRecordRecent }),
|
|
}));
|
|
|
|
const stubs = {
|
|
"search-bar": true,
|
|
"index-list": true,
|
|
"search-result": { template: "<div />", methods: { getDashboardData: () => {} } },
|
|
"service-graph": true,
|
|
"services-catalog": true,
|
|
SanitizedHtmlRenderer: true,
|
|
TracesNoDataState: true,
|
|
FirstDataPanel: { template: '<div data-test="first-data-panel"><slot layout="full" /></div>' },
|
|
};
|
|
const globalOptions = { plugins: [i18n, router], provide: { store }, stubs };
|
|
|
|
const routeSpy = vi.spyOn(router, "currentRoute", "get");
|
|
const setRouteQuery = (query: Record<string, string>) =>
|
|
routeSpy.mockReturnValue({ value: { query, name: "traces", path: "/traces" } } as any);
|
|
|
|
const callsOfType = (kind: "main" | "count") =>
|
|
mockFetch.mock.calls.filter(([req]) => {
|
|
// The count query is the only one that asks for a single row.
|
|
const isCount = req.queryReq?.query?.size === 1;
|
|
return kind === "count" ? isCount : !isCount;
|
|
});
|
|
const lastCall = (kind: "main" | "count") => callsOfType(kind).at(-1)!;
|
|
|
|
const keptAliveParents: VueWrapper<any>[] = [];
|
|
const mountKeptAlive = () => {
|
|
const show = ref(true);
|
|
const Parent = defineComponent({
|
|
setup: () => () =>
|
|
h(KeepAlive, null, {
|
|
default: () => (show.value ? h(Index) : h("div", { key: "elsewhere" })),
|
|
}),
|
|
});
|
|
const parent = mount(Parent, { attachTo: node, global: globalOptions });
|
|
keptAliveParents.push(parent);
|
|
return { show };
|
|
};
|
|
|
|
describe("Traces page load states", () => {
|
|
let wrapper: VueWrapper<any> | undefined;
|
|
|
|
beforeEach(() => {
|
|
Object.assign(searchObj, freshSearchObj());
|
|
mockGetStreams.mockResolvedValue(streamList);
|
|
mockGetStream.mockImplementation((name: string) =>
|
|
Promise.resolve(streamList.list.find((s) => s.name === name)),
|
|
);
|
|
mockLoadTracesParser.mockResolvedValue(undefined);
|
|
mockSyncUrlViewState.mockReturnValue(mockStopUrlSync);
|
|
mockTakeLanding.mockReset().mockReturnValue(null);
|
|
vi.spyOn(router, "push").mockResolvedValue(undefined as any);
|
|
vi.spyOn(router, "replace").mockResolvedValue(undefined as any);
|
|
setRouteQuery({});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
wrapper?.unmount();
|
|
wrapper = undefined;
|
|
keptAliveParents.splice(0).forEach((parent) => parent.unmount());
|
|
await flushPromises();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("registers as AppTraces, the name MainLayout's keep-alive list matches", () => {
|
|
expect((Index as any).name).toBe("AppTraces");
|
|
});
|
|
|
|
describe("no false onboarding on a cold load", () => {
|
|
it("shows the loading state, not the no-streams onboarding, while the parser loads", async () => {
|
|
mockLoadTracesParser.mockReturnValue(new Promise(() => {}));
|
|
wrapper = mount(Index, { attachTo: node, global: globalOptions });
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-test="traces-search-loading"]').exists()).toBe(true);
|
|
expect(wrapper.find('[data-test="traces-no-streams-in-org-text"]').exists()).toBe(false);
|
|
expect(mockGetStreams).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows the onboarding once the stream list settles with no streams", async () => {
|
|
mockGetStreams.mockResolvedValue({ list: [] });
|
|
wrapper = mount(Index, { attachTo: node, global: globalOptions });
|
|
await waitFor(() => expect(searchObj.loadingStream).toBe(false));
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-test="traces-no-streams-in-org-text"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("still loads the stream list when the parser fails to load", async () => {
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
mockLoadTracesParser.mockRejectedValue(new Error("chunk"));
|
|
wrapper = mount(Index, { attachTo: node, global: globalOptions });
|
|
await waitFor(() => expect(searchObj.loadingStream).toBe(false));
|
|
|
|
expect(mockGetStreams).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("span/trace count while the count query runs", () => {
|
|
const mountSearching = async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
wrapper = mount(Index, { attachTo: node, global: globalOptions });
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
};
|
|
const countHits = (n: number) => ({
|
|
content: { results: { hits: [{ trace_count: n, span_count: n, error_count: 1 }] } },
|
|
});
|
|
|
|
it("keeps the count pending instead of 0 until the count query answers", async () => {
|
|
await mountSearching();
|
|
expect(searchObj.data.queryResults.total).toBeNull();
|
|
expect(searchObj.data.queryResults.countPending).toBe(true);
|
|
|
|
lastCall("main")[1].complete(null);
|
|
expect(searchObj.data.queryResults.countPending).toBe(true);
|
|
|
|
lastCall("count")[1].data(null, countHits(42));
|
|
lastCall("count")[1].complete(null);
|
|
expect(searchObj.data.queryResults.total).toBe(42);
|
|
expect(searchObj.data.queryResults.countPending).toBe(false);
|
|
});
|
|
|
|
it("settles the count when the count query fails", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
await mountSearching();
|
|
lastCall("main")[1].complete(null);
|
|
lastCall("count")[1].error(null, new Error("boom"));
|
|
|
|
expect(searchObj.data.queryResults.countPending).toBe(false);
|
|
expect(searchObj.data.queryResults.total).toBeNull();
|
|
});
|
|
|
|
it("does not let a superseded search's count land on the new results", async () => {
|
|
await mountSearching();
|
|
lastCall("main")[1].complete(null);
|
|
const staleCount = lastCall("count")[1];
|
|
|
|
await wrapper!.vm.getQueryData();
|
|
staleCount.data(null, countHits(999));
|
|
staleCount.complete(null);
|
|
|
|
expect(searchObj.data.queryResults.total).toBeNull();
|
|
expect(searchObj.data.queryResults.countPending).toBe(true);
|
|
});
|
|
|
|
it("clears the pending count when the user cancels the search", async () => {
|
|
await mountSearching();
|
|
wrapper!.vm.cancelSearch();
|
|
|
|
expect(searchObj.data.queryResults.countPending).toBe(false);
|
|
});
|
|
|
|
describe("error chip and pager across a re-run", () => {
|
|
const settleFirstRun = async () => {
|
|
await mountSearching();
|
|
lastCall("main")[1].complete(null);
|
|
lastCall("count")[1].data(null, countHits(42));
|
|
lastCall("count")[1].complete(null);
|
|
await wrapper!.vm.getQueryData();
|
|
};
|
|
|
|
it("shows no error chip or pager before the first count lands", async () => {
|
|
await mountSearching();
|
|
expect(searchObj.data.queryResults.errorCount).toBeUndefined();
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
|
|
it("keeps the last error count and the pager while the re-run counts", async () => {
|
|
await settleFirstRun();
|
|
expect(searchObj.data.queryResults.countPending).toBe(true);
|
|
expect(searchObj.data.queryResults.errorCount).toBe(1);
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(true);
|
|
|
|
lastCall("main")[1].complete(null);
|
|
lastCall("count")[1].data(null, {
|
|
content: { results: { hits: [{ trace_count: 0, span_count: 0, error_count: 0 }] } },
|
|
});
|
|
lastCall("count")[1].complete(null);
|
|
expect(searchObj.data.queryResults.errorCount).toBe(0);
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
|
|
it("drops the kept error count and pager when the re-run's count fails", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
await settleFirstRun();
|
|
lastCall("main")[1].complete(null);
|
|
lastCall("count")[1].error(null, new Error("boom"));
|
|
|
|
expect(searchObj.data.queryResults.errorCount).toBeUndefined();
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
|
|
it("drops the kept error count and pager when the re-run is cancelled", async () => {
|
|
await settleFirstRun();
|
|
wrapper!.vm.cancelSearch();
|
|
|
|
expect(searchObj.data.queryResults.errorCount).toBeUndefined();
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
|
|
const settleOnce = async () => {
|
|
await mountSearching();
|
|
lastCall("main")[1].complete(null);
|
|
lastCall("count")[1].data(null, countHits(42));
|
|
lastCall("count")[1].complete(null);
|
|
};
|
|
|
|
it("drops the kept error count and pager when the re-run fails to start", async () => {
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
await settleOnce();
|
|
mockFetch.mockImplementationOnce(() => {
|
|
throw new Error("boom");
|
|
});
|
|
await wrapper!.vm.getQueryData();
|
|
|
|
expect(searchObj.data.queryResults.countPending).toBe(false);
|
|
expect(searchObj.data.queryResults.errorCount).toBeUndefined();
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
|
|
it("keeps the shown results when the re-run's query cannot be built", async () => {
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
await settleOnce();
|
|
searchObj.data.editorValue = null as any;
|
|
await wrapper!.vm.getQueryData();
|
|
|
|
expect(searchObj.data.errorMsg).toBe("");
|
|
expect(searchObj.data.queryResults.countPending).toBe(false);
|
|
expect(searchObj.data.queryResults.total).toBe(42);
|
|
expect(searchObj.loading).toBe(false);
|
|
});
|
|
|
|
it("drops the error count and pager when the stream changes", async () => {
|
|
await settleOnce();
|
|
searchObj.data.stream.selectedStream = { label: "other", value: "other" };
|
|
await wrapper!.vm.getQueryData();
|
|
|
|
expect(searchObj.data.queryResults.errorCount).toBeUndefined();
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
|
|
it("drops the error count and pager when the view switches between traces and spans", async () => {
|
|
await settleOnce();
|
|
wrapper!.vm.onSearchModeChange(searchObj.meta.searchMode === "spans" ? "traces" : "spans");
|
|
|
|
expect(searchObj.data.queryResults.countPending).toBe(true);
|
|
expect(searchObj.data.queryResults.errorCount).toBeUndefined();
|
|
expect(searchObj.meta.resultGrid.showPagination).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("kept alive behind a trace detail page", () => {
|
|
const settleSearch = async () => {
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
lastCall("main")[1].complete(null);
|
|
lastCall("count")[1].complete(null);
|
|
};
|
|
const leaveAndReturn = async (show: { value: boolean }, query?: Record<string, string>) => {
|
|
show.value = false;
|
|
await flushPromises();
|
|
if (query) setRouteQuery(query);
|
|
show.value = true;
|
|
await flushPromises();
|
|
};
|
|
|
|
it("loads once on the first mount", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
mountKeptAlive();
|
|
await settleSearch();
|
|
await flushPromises();
|
|
|
|
expect(mockGetStreams).toHaveBeenCalledTimes(1);
|
|
expect(callsOfType("main")).toHaveLength(1);
|
|
});
|
|
|
|
it("re-runs nothing when Back returns to the same URL", async () => {
|
|
setRouteQuery({ stream: "default", period: "15m" });
|
|
const { show } = mountKeptAlive();
|
|
await settleSearch();
|
|
mockGetStreams.mockClear();
|
|
mockFetch.mockClear();
|
|
|
|
await leaveAndReturn(show);
|
|
|
|
expect(mockGetStreams).not.toHaveBeenCalled();
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("writes no URL state while it sits in the cache and resumes on return", async () => {
|
|
setRouteQuery({ stream: "default", period: "15m" });
|
|
const { show } = mountKeptAlive();
|
|
await settleSearch();
|
|
expect(mockSyncUrlViewState).toHaveBeenCalledTimes(1);
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
expect(mockStopUrlSync).toHaveBeenCalledTimes(1);
|
|
|
|
show.value = true;
|
|
await flushPromises();
|
|
expect(mockSyncUrlViewState).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it.each([
|
|
["a relative range", { stream: "default", period: "15m" }],
|
|
[
|
|
"an absolute range",
|
|
{ stream: "default", from: "1700000000000000", to: "1700000900000000" },
|
|
],
|
|
])("Back never re-runs completed results, whatever their age, for %s", async (_, query) => {
|
|
setRouteQuery(query);
|
|
const { show } = mountKeptAlive();
|
|
await settleSearch();
|
|
searchObj.data.resultGrid.currentPage = 2;
|
|
mockFetch.mockClear();
|
|
|
|
await leaveAndReturn(show);
|
|
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
expect(searchObj.data.resultGrid.currentPage).toBe(2);
|
|
});
|
|
|
|
it("refreshes when it returns to a different URL", async () => {
|
|
setRouteQuery({ stream: "default", period: "15m" });
|
|
const { show } = mountKeptAlive();
|
|
await settleSearch();
|
|
mockGetStreams.mockClear();
|
|
mockFetch.mockClear();
|
|
|
|
await leaveAndReturn(show, { stream: "default", period: "1h" });
|
|
|
|
expect(mockGetStreams).toHaveBeenCalledTimes(1);
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
});
|
|
|
|
it("cancels a search in flight when it leaves and runs it again on return", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
const inFlightId = lastCall("main")[0].traceId;
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
expect(mockCancel).toHaveBeenCalledWith(expect.objectContaining({ trace_id: inFlightId }));
|
|
expect(searchObj.loading).toBe(false);
|
|
|
|
mockFetch.mockClear();
|
|
show.value = true;
|
|
await flushPromises();
|
|
expect(callsOfType("main")).toHaveLength(1);
|
|
});
|
|
|
|
it("re-runs a search it left mid-run on the page the user left", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
searchObj.data.resultGrid.currentPage = 2;
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
mockFetch.mockClear();
|
|
show.value = true;
|
|
await flushPromises();
|
|
|
|
expect(callsOfType("main")).toHaveLength(1);
|
|
expect(searchObj.data.resultGrid.currentPage).toBe(2);
|
|
expect(lastCall("main")[0].queryReq.query.from).toBe(
|
|
2 * searchObj.meta.resultGrid.rowsPerPage,
|
|
);
|
|
});
|
|
|
|
it("re-fetches only the count when it left while the count was running", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
lastCall("main")[1].complete(null);
|
|
mockFetch.mockClear();
|
|
|
|
await leaveAndReturn(show);
|
|
|
|
expect(callsOfType("main")).toHaveLength(0);
|
|
expect(callsOfType("count")).toHaveLength(1);
|
|
});
|
|
|
|
it("drops the pending count when it returns to a URL that runs nothing", async () => {
|
|
try {
|
|
setRouteQuery({ stream: "default" });
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
lastCall("main")[1].complete(null);
|
|
mockFetch.mockClear();
|
|
store.state.zoConfig.auto_query_enabled = true;
|
|
localStorage.setItem("oo_toggle_auto_run", "false");
|
|
|
|
await leaveAndReturn(show, { period: "1h" });
|
|
|
|
await waitFor(() => expect(searchObj.data.queryResults.countPending).toBe(false));
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
} finally {
|
|
delete store.state.zoConfig.auto_query_enabled;
|
|
localStorage.removeItem("oo_toggle_auto_run");
|
|
}
|
|
});
|
|
|
|
const deferred = <T>() => {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((r) => (resolve = r));
|
|
return { promise, resolve };
|
|
};
|
|
|
|
it("starts nothing when it leaves while the parser loads, and finishes loading on return", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const parser = deferred<void>();
|
|
mockLoadTracesParser.mockReturnValueOnce(parser.promise);
|
|
const { show } = mountKeptAlive();
|
|
await flushPromises();
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
parser.resolve();
|
|
await flushPromises();
|
|
|
|
expect(mockGetStreams).not.toHaveBeenCalled();
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
|
|
show.value = true;
|
|
await flushPromises();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
expect(mockGetStreams).toHaveBeenCalledTimes(1);
|
|
expect(searchObj.data.stream.selectedStream.value).toBe("default");
|
|
});
|
|
|
|
it("applies no stream list and starts no search when it leaves while the stream list loads", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const streams = deferred<typeof streamList>();
|
|
mockGetStreams.mockReturnValueOnce(streams.promise);
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(mockGetStreams).toHaveBeenCalledTimes(1));
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
streams.resolve(streamList);
|
|
await flushPromises();
|
|
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
expect(searchObj.data.stream.streamLists).toEqual([]);
|
|
expect(searchObj.data.streamResults.list).toEqual([]);
|
|
|
|
show.value = true;
|
|
await flushPromises();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
expect(mockGetStreams).toHaveBeenCalledTimes(2);
|
|
expect(searchObj.data.stream.selectedStream.value).toBe("default");
|
|
});
|
|
|
|
it("runs a recent-search restore on return when it left while the stream list loaded", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
// The router hands a landing out once, so only the first load can claim it.
|
|
mockTakeLanding.mockReturnValueOnce({ run: true });
|
|
const streams = deferred<typeof streamList>();
|
|
mockGetStreams.mockReturnValueOnce(streams.promise);
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(mockGetStreams).toHaveBeenCalledTimes(1));
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
streams.resolve(streamList);
|
|
await flushPromises();
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
|
|
show.value = true;
|
|
await flushPromises();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
lastCall("main")[1].complete(null);
|
|
await flushPromises();
|
|
|
|
// Only the restore's run is recorded; the default auto-run on a landing is not.
|
|
expect(mockRecordRecent).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("starts no search from a URL tab change while it sits in the cache", async () => {
|
|
const route = ref({ query: { stream: "default" } as Record<string, string>, name: "traces" });
|
|
routeSpy.mockReturnValue(route as any);
|
|
const { show } = mountKeptAlive();
|
|
await settleSearch();
|
|
await flushPromises();
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
const shownMode = searchObj.meta.searchMode;
|
|
const otherMode = shownMode === "spans" ? "traces" : "spans";
|
|
route.value = {
|
|
query: { stream: "incoming", tab: otherMode, reload: "true" },
|
|
name: "traces",
|
|
};
|
|
await flushPromises();
|
|
// A mode switch clears the results and re-runs the search, for a URL this page is not showing yet.
|
|
expect(searchObj.meta.searchMode).toBe(shownMode);
|
|
expect(callsOfType("main")).toHaveLength(1);
|
|
});
|
|
|
|
it("runs a newer restore over one it left mid-load", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
mockTakeLanding.mockReturnValueOnce({ run: false });
|
|
const streams = deferred<typeof streamList>();
|
|
mockGetStreams.mockReturnValueOnce(streams.promise);
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(mockGetStreams).toHaveBeenCalledTimes(1));
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
streams.resolve(streamList);
|
|
await flushPromises();
|
|
mockTakeLanding.mockReturnValueOnce({ run: true });
|
|
show.value = true;
|
|
await flushPromises();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
lastCall("main")[1].complete(null);
|
|
await flushPromises();
|
|
|
|
expect(mockRecordRecent).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("writes no field or schema state when it leaves while the stream schema loads", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const schema = deferred<(typeof streamList.list)[number]>();
|
|
mockGetStream.mockReturnValueOnce(schema.promise);
|
|
const { show } = mountKeptAlive();
|
|
await waitFor(() => expect(mockGetStream).toHaveBeenCalledTimes(1));
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
schema.resolve({
|
|
...streamList.list[0],
|
|
settings: { ...streamList.list[0].settings, max_query_range: 6 },
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(searchObj.data.stream.selectedStreamFields).toEqual([]);
|
|
expect((searchObj.data.datetime as any).queryRangeRestrictionInHour).toBeUndefined();
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
|
|
show.value = true;
|
|
await flushPromises();
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
expect(searchObj.data.stream.selectedStreamFields.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("starts no search when it leaves during a filter handoff's stream load", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const { show } = mountKeptAlive();
|
|
await settleSearch();
|
|
mockFetch.mockClear();
|
|
const streams = deferred<typeof streamList>();
|
|
mockGetStreams.mockReturnValueOnce(streams.promise);
|
|
|
|
show.value = false;
|
|
await flushPromises();
|
|
setRouteQuery({ stream: "default", filter: "service_name='api'" });
|
|
show.value = true;
|
|
await waitFor(() => expect(mockGetStreams).toHaveBeenCalledTimes(2));
|
|
show.value = false;
|
|
await flushPromises();
|
|
streams.resolve(streamList);
|
|
await flushPromises();
|
|
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("starts nothing when it unmounts while the parser loads", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
const parser = deferred<void>();
|
|
mockLoadTracesParser.mockReturnValueOnce(parser.promise);
|
|
wrapper = mount(Index, { attachTo: node, global: globalOptions });
|
|
await flushPromises();
|
|
|
|
wrapper.unmount();
|
|
wrapper = undefined;
|
|
parser.resolve();
|
|
await flushPromises();
|
|
|
|
expect(mockGetStreams).not.toHaveBeenCalled();
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("drops a late batch from a search cancelled by leaving the page", async () => {
|
|
setRouteQuery({ stream: "default" });
|
|
wrapper = mount(Index, { attachTo: node, global: globalOptions });
|
|
await waitFor(() => expect(callsOfType("main")).toHaveLength(1));
|
|
const lateBatch = lastCall("main")[1].data;
|
|
|
|
wrapper.unmount();
|
|
wrapper = undefined;
|
|
lateBatch(null, {
|
|
type: "search_response_hits",
|
|
content: { results: { hits: [{ trace_id: "other-org-trace" }] } },
|
|
});
|
|
|
|
expect(searchObj.data.queryResults.hits).toEqual([]);
|
|
});
|
|
});
|
|
});
|