mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
fix: updated browser check action buttons (#13482)
Editing a browser check offered Cancel / Continue / Update check on the Journey step, where neither save-bearing label matched its behaviour: Continue advanced to Configure without saving, and Update check saved but deliberately stayed on the page. There was no obvious save-and-leave action. Edit mode now shows Cancel / Save & Continue / Save & Exit on the Journey step, and the Configure step's primary button becomes Save & Exit. Create mode is unchanged — a new check has no name, URL or locations yet, so saving from the Journey step would always fail validation.
This commit is contained in:
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Wählen Sie aus, was Sie überwachen möchten",
|
||||
"modalTitle": "Eine Prüfung erstellen",
|
||||
"pageTitle": "Neue {label}-Prüfung",
|
||||
"saveAndContinue": "Speichern & fortfahren",
|
||||
"saveAndExit": "Speichern & beenden",
|
||||
"saveCheck": "Prüfung speichern",
|
||||
"saveFailed": "Prüfung konnte nicht gespeichert werden.",
|
||||
"saved": "Prüfung erfolgreich gespeichert.",
|
||||
|
||||
@@ -9660,6 +9660,8 @@
|
||||
"modalSubtitle": "Choose what you want to monitor",
|
||||
"modalTitle": "Create a Check",
|
||||
"pageTitle": "New {label} Check",
|
||||
"saveAndContinue": "Save & Continue",
|
||||
"saveAndExit": "Save & Exit",
|
||||
"saveCheck": "Save check",
|
||||
"saveFailed": "Failed to save check.",
|
||||
"saved": "Check saved successfully.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Elija lo que desea monitorear",
|
||||
"modalTitle": "Crear una verificación",
|
||||
"pageTitle": "Nueva verificación {label}",
|
||||
"saveAndContinue": "Guardar y continuar",
|
||||
"saveAndExit": "Guardar y salir",
|
||||
"saveCheck": "Guardar verificación",
|
||||
"saveFailed": "Error al guardar la verificación.",
|
||||
"saved": "Verificación guardada exitosamente.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Choisissez ce que vous voulez surveiller",
|
||||
"modalTitle": "Créer une vérification",
|
||||
"pageTitle": "Nouvelle vérification {label}",
|
||||
"saveAndContinue": "Enregistrer et continuer",
|
||||
"saveAndExit": "Enregistrer et quitter",
|
||||
"saveCheck": "Enregistrer la vérification",
|
||||
"saveFailed": "Échec de l'enregistrement de la vérification.",
|
||||
"saved": "Vérification enregistrée avec succès.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Scegli cosa monitorare",
|
||||
"modalTitle": "Crea un check",
|
||||
"pageTitle": "Nuovo controllo {label}",
|
||||
"saveAndContinue": "Salva e continua",
|
||||
"saveAndExit": "Salva ed esci",
|
||||
"saveCheck": "Salva controllo",
|
||||
"saveFailed": "Salvataggio del controllo fallito.",
|
||||
"saved": "Controllo salvato con successo.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "監視するものを選択",
|
||||
"modalTitle": "チェックを作成",
|
||||
"pageTitle": "新規 {label} チェック",
|
||||
"saveAndContinue": "保存して続行",
|
||||
"saveAndExit": "保存して終了",
|
||||
"saveCheck": "チェックを保存",
|
||||
"saveFailed": "チェックの保存に失敗しました。",
|
||||
"saved": "チェックが正常に保存されました。",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "모니터링할 항목 선택",
|
||||
"modalTitle": "검사 생성",
|
||||
"pageTitle": "새 {label} 검사",
|
||||
"saveAndContinue": "저장 후 계속",
|
||||
"saveAndExit": "저장 후 종료",
|
||||
"saveCheck": "검사 저장",
|
||||
"saveFailed": "검사 저장에 실패했습니다.",
|
||||
"saved": "검사가 성공적으로 저장되었습니다.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Kies wat u wilt monitoren",
|
||||
"modalTitle": "Maak een Check",
|
||||
"pageTitle": "Nieuwe {label}-controle",
|
||||
"saveAndContinue": "Opslaan en doorgaan",
|
||||
"saveAndExit": "Opslaan en afsluiten",
|
||||
"saveCheck": "Controle opslaan",
|
||||
"saveFailed": "Kan controle niet opslaan.",
|
||||
"saved": "Controle succesvol opgeslagen.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Wybierz, co chcesz monitorować",
|
||||
"modalTitle": "Utwórz sprawdzenie",
|
||||
"pageTitle": "Nowe sprawdzenie {label}",
|
||||
"saveAndContinue": "Zapisz i kontynuuj",
|
||||
"saveAndExit": "Zapisz i zakończ",
|
||||
"saveCheck": "Zapisz sprawdzenie",
|
||||
"saveFailed": "Nie udało się zapisać sprawdzenia.",
|
||||
"saved": "Sprawdzenie zapisano pomyślnie.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Escolha o que deseja monitorar",
|
||||
"modalTitle": "Criar uma verificação",
|
||||
"pageTitle": "Nova verificação {label}",
|
||||
"saveAndContinue": "Salvar e continuar",
|
||||
"saveAndExit": "Salvar e sair",
|
||||
"saveCheck": "Salvar verificação",
|
||||
"saveFailed": "Falha ao salvar a verificação.",
|
||||
"saved": "Verificação salva com sucesso.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Выберите, что вы хотите мониторить",
|
||||
"modalTitle": "Создать проверку",
|
||||
"pageTitle": "Новая проверка {label}",
|
||||
"saveAndContinue": "Сохранить и продолжить",
|
||||
"saveAndExit": "Сохранить и выйти",
|
||||
"saveCheck": "Сохранить проверку",
|
||||
"saveFailed": "Не удалось сохранить проверку.",
|
||||
"saved": "Проверка успешно сохранена.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "İzlemek istediğiniz şeyi seçin",
|
||||
"modalTitle": "Bir Kontrol Oluşturun",
|
||||
"pageTitle": "Yeni {label} Kontrol",
|
||||
"saveAndContinue": "Kaydet ve devam et",
|
||||
"saveAndExit": "Kaydet ve çık",
|
||||
"saveCheck": "Kontrolü kaydet",
|
||||
"saveFailed": "Kontrol kaydedilemedi.",
|
||||
"saved": "Kontrol başarıyla kaydedildi.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "Chọn những gì bạn muốn giám sát",
|
||||
"modalTitle": "Tạo kiểm tra",
|
||||
"pageTitle": "Kiểm tra {label} mới",
|
||||
"saveAndContinue": "Lưu và tiếp tục",
|
||||
"saveAndExit": "Lưu và thoát",
|
||||
"saveCheck": "Lưu kiểm tra",
|
||||
"saveFailed": "Lưu kiểm tra thất bại.",
|
||||
"saved": "Đã lưu kiểm tra thành công.",
|
||||
|
||||
@@ -9592,6 +9592,8 @@
|
||||
"modalSubtitle": "选择您要监控的内容",
|
||||
"modalTitle": "创建检查",
|
||||
"pageTitle": "新建 {label} 检查",
|
||||
"saveAndContinue": "保存并继续",
|
||||
"saveAndExit": "保存并退出",
|
||||
"saveCheck": "保存检查",
|
||||
"saveFailed": "保存检查失败。",
|
||||
"saved": "检查已成功保存。",
|
||||
|
||||
@@ -9587,6 +9587,8 @@
|
||||
"modalSubtitle": "選擇您要監控的內容",
|
||||
"modalTitle": "建立檢查",
|
||||
"pageTitle": "新增 {label} 檢查",
|
||||
"saveAndContinue": "儲存並繼續",
|
||||
"saveAndExit": "儲存並離開",
|
||||
"saveCheck": "儲存檢查",
|
||||
"saveFailed": "儲存檢查失敗。",
|
||||
"saved": "檢查儲存成功。",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
// Render tests for CreateBrowserTest.vue — browser test creation/editing page.
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { nextTick } from "vue";
|
||||
import { mount, VueWrapper, flushPromises } from "@vue/test-utils";
|
||||
import i18n from "@/locales";
|
||||
import store from "@/test/unit/helpers/store";
|
||||
@@ -127,8 +128,9 @@ const baseStubs = {
|
||||
props: ["title", "subtitle", "back"],
|
||||
},
|
||||
OButton: {
|
||||
// Slot names mirror the real OButton (icon-left / default / icon-right).
|
||||
template:
|
||||
'<button :data-test="$attrs[\'data-test\']" :disabled="disabled"><slot name="prefix" /><slot /></button>',
|
||||
'<button :data-test="$attrs[\'data-test\']" :disabled="disabled"><slot name="icon-left" /><slot /><slot name="icon-right" /></button>',
|
||||
props: ["variant", "size", "disabled", "loading", "class", "iconLeft"],
|
||||
inheritAttrs: true,
|
||||
},
|
||||
@@ -235,6 +237,39 @@ function mountPage(props: Record<string, unknown> = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Mount in edit mode with a loaded check that passes the save schema. */
|
||||
async function mountValidEdit() {
|
||||
mockServiceGet.mockResolvedValue({
|
||||
data: {
|
||||
name: "Test Check",
|
||||
url: "https://example.com",
|
||||
folder: "folder-1",
|
||||
journey: [],
|
||||
},
|
||||
});
|
||||
const w = mountPage({ editId: "check-123" });
|
||||
await flushPromises();
|
||||
return w;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount in create mode and walk the gate → Journey → Configure flow so the
|
||||
* step 2 footer (with the primary save button) is rendered.
|
||||
*/
|
||||
async function mountCreateAtConfigure(name = "Brand New Check") {
|
||||
const w = mountPage();
|
||||
await flushPromises();
|
||||
|
||||
await w.find('[data-test="synthetics-create-url-input"]').setValue("https://example.com");
|
||||
await w.find('[data-test="synthetics-create-name-input"]').setValue(name);
|
||||
await w.find('[data-test="synthetics-create-build-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
await w.find('[data-test="synthetics-create-continue-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
return w;
|
||||
}
|
||||
|
||||
describe("CreateBrowserTest", () => {
|
||||
let wrapper: VueWrapper;
|
||||
|
||||
@@ -325,7 +360,7 @@ describe("CreateBrowserTest", () => {
|
||||
expect(mockServiceGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render 'Update Check' button in Journey step footer when editId is set", async () => {
|
||||
it("should render 'Save & Continue' and 'Save & Exit' in the Journey footer when editId is set", async () => {
|
||||
mockServiceGet.mockResolvedValue({
|
||||
data: { name: "Test Check", url: "https://example.com", journey: [] },
|
||||
});
|
||||
@@ -333,12 +368,13 @@ describe("CreateBrowserTest", () => {
|
||||
wrapper = mountPage({ editId: "check-123" });
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-from-journey-btn"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-continue-btn"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-exit-btn"]').exists()).toBe(true);
|
||||
// The plain (non-saving) Continue button is create-mode only
|
||||
expect(wrapper.find('[data-test="synthetics-create-continue-btn"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should NOT render 'Update Check' button when editId is not set, even in editor phase", async () => {
|
||||
it("should render only Cancel + Continue in the Journey footer when editId is not set", async () => {
|
||||
wrapper = mountPage();
|
||||
await flushPromises();
|
||||
|
||||
@@ -351,13 +387,223 @@ describe("CreateBrowserTest", () => {
|
||||
await flushPromises();
|
||||
|
||||
// We are now in the editor phase on step 1 — verify footer buttons
|
||||
// Continue and Cancel should be present
|
||||
expect(wrapper.find('[data-test="synthetics-create-continue-btn"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-test="synthetics-create-cancel-btn"]').exists()).toBe(true);
|
||||
// Update Check button should NOT appear (no editId)
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-from-journey-btn"]').exists()).toBe(
|
||||
// Neither save action appears without an editId — there is nothing to update yet
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-continue-btn"]').exists()).toBe(
|
||||
false,
|
||||
);
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-exit-btn"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit mode — Journey footer save actions", () => {
|
||||
it("'Save & Exit' should update the check and navigate back to the list", async () => {
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockServiceUpdate).toHaveBeenCalledWith(
|
||||
"default",
|
||||
"check-123",
|
||||
expect.anything(),
|
||||
"folder-1",
|
||||
);
|
||||
expect(mockRouterPush).toHaveBeenCalledWith({
|
||||
name: "synthetics",
|
||||
query: { folder: "folder-1" },
|
||||
});
|
||||
});
|
||||
|
||||
it("'Save & Continue' should update the check and advance to Configure without navigating", async () => {
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-continue-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
// Step 2 footer is now rendered — Go Back only exists on the Configure step
|
||||
expect(wrapper.find('[data-test="synthetics-create-back-to-journey-btn"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not navigate when the update request fails", async () => {
|
||||
mockServiceUpdate.mockRejectedValue({ response: { status: 500, data: {} } });
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ variant: "error" }));
|
||||
});
|
||||
|
||||
it("should not navigate when validation fails", async () => {
|
||||
mockServiceGet.mockResolvedValue({
|
||||
data: { name: "", url: "https://example.com", journey: [] },
|
||||
});
|
||||
wrapper = mountPage({ editId: "check-123" });
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceUpdate).not.toHaveBeenCalled();
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit mode — check deleted while editing (404)", () => {
|
||||
const notFound = { response: { status: 404, data: {} } };
|
||||
|
||||
it("'Save & Exit' should navigate to the list once, without a folder query", async () => {
|
||||
mockServiceUpdate.mockRejectedValue(notFound);
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
// persist() already navigated for the 404 case — onSaveAndExit must bail
|
||||
// out instead of pushing a second, folder-scoped route on top of it.
|
||||
expect(mockRouterPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouterPush).toHaveBeenCalledWith({ name: "synthetics" });
|
||||
});
|
||||
|
||||
it("'Save & Exit' should warn (not error) when the check no longer exists", async () => {
|
||||
mockServiceUpdate.mockRejectedValue(notFound);
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ variant: "warning" }));
|
||||
expect(mockToast).not.toHaveBeenCalledWith(expect.objectContaining({ variant: "error" }));
|
||||
});
|
||||
|
||||
it("'Save & Continue' should not advance to Configure", async () => {
|
||||
mockServiceUpdate.mockRejectedValue(notFound);
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-continue-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
// Still on the Journey step: Go Back only exists on Configure.
|
||||
expect(wrapper.find('[data-test="synthetics-create-back-to-journey-btn"]').exists()).toBe(
|
||||
false,
|
||||
);
|
||||
expect(mockRouterPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouterPush).toHaveBeenCalledWith({ name: "synthetics" });
|
||||
});
|
||||
|
||||
it("should clear the saving state after the 404 early return", async () => {
|
||||
mockServiceUpdate.mockRejectedValue(notFound);
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(
|
||||
wrapper.findComponent('[data-test="synthetics-create-save-exit-btn"]').props("loading"),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saving state", () => {
|
||||
it("should mark the save buttons as loading while the request is in flight", async () => {
|
||||
let resolveUpdate: (value: unknown) => void = () => {};
|
||||
mockServiceUpdate.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveUpdate = resolve;
|
||||
}),
|
||||
);
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
const saveExitBtn = () =>
|
||||
wrapper.findComponent('[data-test="synthetics-create-save-exit-btn"]');
|
||||
expect(saveExitBtn().props("loading")).toBe(false);
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-exit-btn"]').trigger("click");
|
||||
await nextTick();
|
||||
expect(saveExitBtn().props("loading")).toBe(true);
|
||||
|
||||
resolveUpdate({});
|
||||
await flushPromises();
|
||||
expect(saveExitBtn().props("loading")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Configure footer save action", () => {
|
||||
it("should update and navigate back to the list in edit mode", async () => {
|
||||
wrapper = await mountValidEdit();
|
||||
|
||||
// The only way to reach Configure in edit mode is Save & Continue.
|
||||
await wrapper.find('[data-test="synthetics-create-save-continue-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
mockServiceUpdate.mockClear();
|
||||
mockRouterPush.mockClear();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouterPush).toHaveBeenCalledWith({
|
||||
name: "synthetics",
|
||||
query: { folder: "folder-1" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (not update) and navigate back to the list in create mode", async () => {
|
||||
wrapper = await mountCreateAtConfigure();
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceUpdate).not.toHaveBeenCalled();
|
||||
expect(mockServiceCreate).toHaveBeenCalledTimes(1);
|
||||
expect(mockServiceCreate).toHaveBeenCalledWith(
|
||||
"default",
|
||||
expect.objectContaining({ name: "Brand New Check" }),
|
||||
"default",
|
||||
);
|
||||
expect(mockRouterPush).toHaveBeenCalledWith({
|
||||
name: "synthetics",
|
||||
query: { folder: "default" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should not create or navigate when the check has no name", async () => {
|
||||
wrapper = await mountCreateAtConfigure("");
|
||||
|
||||
await wrapper.find('[data-test="synthetics-create-save-btn"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockServiceCreate).not.toHaveBeenCalled();
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ variant: "error" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("create mode — Continue to Configure", () => {
|
||||
it("should advance to Configure without persisting anything", async () => {
|
||||
wrapper = await mountCreateAtConfigure();
|
||||
|
||||
// Configure-only footer buttons are rendered…
|
||||
expect(wrapper.find('[data-test="synthetics-create-back-to-journey-btn"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-btn"]').exists()).toBe(true);
|
||||
// …and the Journey-step Continue button is gone.
|
||||
expect(wrapper.find('[data-test="synthetics-create-continue-btn"]').exists()).toBe(false);
|
||||
// Continue is pure navigation — nothing is written until the user saves.
|
||||
expect(mockServiceCreate).not.toHaveBeenCalled();
|
||||
expect(mockServiceUpdate).not.toHaveBeenCalled();
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -373,7 +619,7 @@ describe("CreateBrowserTest", () => {
|
||||
wrapper = mountPage({ editId: "check-123" });
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper.find('[data-test="synthetics-create-save-from-journey-btn"]');
|
||||
const saveBtn = wrapper.find('[data-test="synthetics-create-save-exit-btn"]');
|
||||
expect(saveBtn.exists()).toBe(true);
|
||||
|
||||
await saveBtn.trigger("click");
|
||||
@@ -394,7 +640,7 @@ describe("CreateBrowserTest", () => {
|
||||
wrapper = mountPage({ editId: "check-123" });
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper.find('[data-test="synthetics-create-save-from-journey-btn"]');
|
||||
const saveBtn = wrapper.find('[data-test="synthetics-create-save-exit-btn"]');
|
||||
await saveBtn.trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@@ -406,7 +406,13 @@ function beforeUnloadHandler(e: BeforeUnloadEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
async function saveCheck() {
|
||||
/**
|
||||
* Validates and persists the check. Owns validation, the API call and all
|
||||
* toasts — but deliberately NOT navigation, so the footer buttons can decide
|
||||
* where to go afterwards (stay on Configure vs. return to the checks list).
|
||||
* Returns true only when the check was actually written.
|
||||
*/
|
||||
async function persist(): Promise<boolean> {
|
||||
// ── Pre-save validation ───────────────────────────────────────────
|
||||
validationErrors.value = {};
|
||||
const toValidate = {
|
||||
@@ -435,7 +441,7 @@ async function saveCheck() {
|
||||
variant: "error",
|
||||
message: t("synthetics.validation.fixHighlightedFields"),
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
isSaving.value = true;
|
||||
@@ -451,32 +457,28 @@ async function saveCheck() {
|
||||
await syntheticsService.update(org, props.editId, apiPayload.value, check.value.folder);
|
||||
dismiss();
|
||||
toast({ variant: "success", message: t("synthetics.newCheck.updated") });
|
||||
isDirty.value = false;
|
||||
if (currentStep.value !== 1) {
|
||||
router.push({ name: "synthetics", query: { folder: check.value.folder } });
|
||||
}
|
||||
} else {
|
||||
const res = await syntheticsService.create(org, apiPayload.value, check.value.folder);
|
||||
const savedId = res.data?.id ?? crypto.randomUUID();
|
||||
await syntheticsService.create(org, apiPayload.value, check.value.folder);
|
||||
dismiss();
|
||||
toast({ variant: "success", message: t("synthetics.newCheck.saved") });
|
||||
isDirty.value = false;
|
||||
router.push({ name: "synthetics", query: { folder: check.value.folder } });
|
||||
}
|
||||
isDirty.value = false;
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
dismiss();
|
||||
if (err?.response?.status === 404) {
|
||||
// Already navigated away — the caller must not push on top of this.
|
||||
forceLeave = true;
|
||||
router.push({ name: "synthetics" });
|
||||
toast({ variant: "warning", message: t("synthetics.newCheck.notFoundInOrg") });
|
||||
isSaving.value = false;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
toast({
|
||||
variant: "error",
|
||||
message: err?.response?.data?.message || t("synthetics.newCheck.saveFailed"),
|
||||
});
|
||||
console.error("[synthetics] save failed", err);
|
||||
return false;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
@@ -500,6 +502,19 @@ function onContinueToConfigure() {
|
||||
currentStep.value = 2;
|
||||
}
|
||||
|
||||
/** Edit mode, Journey step: persist, then move on to Configure. */
|
||||
async function onSaveAndContinue() {
|
||||
if (!(await persist())) return;
|
||||
journeyStepDone.value = true;
|
||||
currentStep.value = 2;
|
||||
}
|
||||
|
||||
/** Persist, then return to the checks list. */
|
||||
async function onSaveAndExit() {
|
||||
if (!(await persist())) return;
|
||||
router.push({ name: "synthetics", query: { folder: check.value.folder } });
|
||||
}
|
||||
|
||||
// ── Replay — uses the composable's phase-based state machine ────────────────
|
||||
/** Local unwraps so the template can read these without `.value`. */
|
||||
const replayPhase = computed(() => recorder.replayPhase.value);
|
||||
@@ -621,7 +636,7 @@ function onClearResults() {
|
||||
data-test="synthetics-create-record-btn"
|
||||
@click="onRecordClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<template #icon-left>
|
||||
<OIcon name="smart-display" size="sm" />
|
||||
</template>
|
||||
{{ t("synthetics.journey.recordJourney") }}
|
||||
@@ -632,7 +647,7 @@ function onClearResults() {
|
||||
data-test="synthetics-create-build-btn"
|
||||
@click="buildManually"
|
||||
>
|
||||
<template #prefix>
|
||||
<template #icon-left>
|
||||
<OIcon name="edit" size="sm" />
|
||||
</template>
|
||||
{{ t("synthetics.createBrowserTest.buildManually") }}
|
||||
@@ -857,7 +872,7 @@ function onClearResults() {
|
||||
<div
|
||||
class="border-border-default bg-surface-base flex shrink-0 items-center gap-2 border-t px-3 py-2.5"
|
||||
>
|
||||
<!-- Journey step: Cancel | Selection actions (left) | Replay status + Continue (right) -->
|
||||
<!-- Journey step: Selection actions (left) | Cancel + save/continue actions (right) -->
|
||||
<template v-if="currentStep === 1">
|
||||
<!-- Selection actions — moved from BrowserJourney, kept on the left -->
|
||||
<template v-if="journeySelectionState.count > 0 && !journeySelectionState.isRecording">
|
||||
@@ -884,26 +899,36 @@ function onClearResults() {
|
||||
>
|
||||
{{ t("common.cancel") }}
|
||||
</OButton>
|
||||
<!-- Create mode: nothing to save yet — Configure holds the required fields -->
|
||||
<OButton
|
||||
v-if="!props.editId"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-test="synthetics-create-continue-btn"
|
||||
@click="onContinueToConfigure"
|
||||
>
|
||||
{{ t("synthetics.createBrowserTest.continue") }}
|
||||
<template #suffix><OIcon name="chevron-right" size="sm" /></template>
|
||||
</OButton>
|
||||
<OButton
|
||||
v-if="props.editId"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:loading="isSaving"
|
||||
data-test="synthetics-create-save-from-journey-btn"
|
||||
@click="saveCheck"
|
||||
>
|
||||
{{ t("synthetics.newCheck.updateCheck") }}
|
||||
<template #suffix><OIcon name="save" size="sm" /></template>
|
||||
</OButton>
|
||||
<template v-else>
|
||||
<OButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:loading="isSaving"
|
||||
data-test="synthetics-create-save-continue-btn"
|
||||
@click="onSaveAndContinue"
|
||||
>
|
||||
{{ t("synthetics.newCheck.saveAndContinue") }}
|
||||
</OButton>
|
||||
<OButton
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:loading="isSaving"
|
||||
data-test="synthetics-create-save-exit-btn"
|
||||
@click="onSaveAndExit"
|
||||
>
|
||||
{{ t("synthetics.newCheck.saveAndExit") }}
|
||||
</OButton>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Configure step: Cancel | Back + Save -->
|
||||
@@ -923,7 +948,6 @@ function onClearResults() {
|
||||
data-test="synthetics-create-back-to-journey-btn"
|
||||
@click="currentStep = 1"
|
||||
>
|
||||
<template #prefix><OIcon name="chevron-left" size="sm" /></template>
|
||||
{{ t("common.goBack") }}
|
||||
</OButton>
|
||||
<OButton
|
||||
@@ -931,14 +955,9 @@ function onClearResults() {
|
||||
size="sm"
|
||||
:loading="isSaving"
|
||||
data-test="synthetics-create-save-btn"
|
||||
@click="saveCheck"
|
||||
@click="onSaveAndExit"
|
||||
>
|
||||
{{
|
||||
props.editId
|
||||
? t("synthetics.newCheck.updateCheck")
|
||||
: t("synthetics.newCheck.saveCheck")
|
||||
}}
|
||||
<template #suffix><OIcon name="save" size="sm" /></template>
|
||||
{{ t("synthetics.newCheck.saveAndExit") }}
|
||||
</OButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -93,14 +93,18 @@ const baseStubs = {
|
||||
template: '<div data-test="synthetics-header"><slot name="title" /><slot /></div>',
|
||||
props: ["title", "subtitle", "back"],
|
||||
},
|
||||
// Mirrors the real OButton slot contract (OButton.vue): only `icon-left`,
|
||||
// default and `icon-right` are rendered. Any other slot name (e.g. `suffix`)
|
||||
// is silently dropped — which is exactly the bug this stub must surface.
|
||||
OButton: {
|
||||
template:
|
||||
'<button :data-test="$attrs[\'data-test\']" :disabled="disabled"><slot name="prefix" /><slot name="suffix" /><slot /></button>',
|
||||
'<button :data-test="$attrs[\'data-test\']" :disabled="disabled"><slot name="icon-left" /><slot /><slot name="icon-right" /></button>',
|
||||
props: ["variant", "size", "disabled", "loading", "class", "iconLeft"],
|
||||
inheritAttrs: true,
|
||||
},
|
||||
// Renders an identifiable marker per icon name so slot placement is assertable.
|
||||
OIcon: {
|
||||
template: "<span />",
|
||||
template: '<span :data-test="`icon-${name}`" />',
|
||||
props: ["name", "size", "class"],
|
||||
},
|
||||
ODialog: {
|
||||
@@ -185,6 +189,17 @@ describe("CreateProtocolCheck", () => {
|
||||
expect(wrapper.find('[data-test="synthetics-create-save-btn"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
// The footer actions are deliberately text-only — no leading or trailing
|
||||
// icons. The stub renders OButton's real slots, so a stray icon would show.
|
||||
it("should render the footer save button without an icon", async () => {
|
||||
wrapper = mountPage("http");
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper.find('[data-test="synthetics-create-save-btn"]');
|
||||
expect(saveBtn.exists()).toBe(true);
|
||||
expect(saveBtn.find('[data-test^="icon-"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should render the Beta badge in the page title", async () => {
|
||||
wrapper = mountPage("http");
|
||||
await flushPromises();
|
||||
|
||||
@@ -331,8 +331,7 @@ async function saveCheck() {
|
||||
data-test="synthetics-create-save-btn"
|
||||
@click="saveCheck"
|
||||
>
|
||||
{{ editId ? t("synthetics.newCheck.updateCheck") : t("synthetics.newCheck.saveCheck") }}
|
||||
<template #suffix><OIcon name="save" size="sm" /></template>
|
||||
{{ t("synthetics.newCheck.saveAndExit") }}
|
||||
</OButton>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user