From c047dfe4cf37164afa3f311bd7b7be2a2dafe831 Mon Sep 17 00:00:00 2001 From: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:51:38 +0530 Subject: [PATCH] fix: updated browser check action buttons (#13482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/src/locales/languages/de-DE.json | 2 + web/src/locales/languages/en-US.json | 2 + web/src/locales/languages/es-ES.json | 2 + web/src/locales/languages/fr-FR.json | 2 + web/src/locales/languages/it-IT.json | 2 + web/src/locales/languages/ja-JP.json | 2 + web/src/locales/languages/ko-KR.json | 2 + web/src/locales/languages/nl-NL.json | 2 + web/src/locales/languages/pl-PL.json | 2 + web/src/locales/languages/pt-PT.json | 2 + web/src/locales/languages/ru-RU.json | 2 + web/src/locales/languages/tr-TR.json | 2 + web/src/locales/languages/vi-VN.json | 2 + web/src/locales/languages/zh-CN.json | 2 + web/src/locales/languages/zh-TW.json | 2 + .../synthetics/CreateBrowserTest.spec.ts | 268 +++++++++++++++++- .../views/synthetics/CreateBrowserTest.vue | 89 +++--- .../synthetics/CreateProtocolCheck.spec.ts | 19 +- .../views/synthetics/CreateProtocolCheck.vue | 3 +- 19 files changed, 359 insertions(+), 50 deletions(-) diff --git a/web/src/locales/languages/de-DE.json b/web/src/locales/languages/de-DE.json index 7400ff16c6..8b935e5837 100644 --- a/web/src/locales/languages/de-DE.json +++ b/web/src/locales/languages/de-DE.json @@ -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.", diff --git a/web/src/locales/languages/en-US.json b/web/src/locales/languages/en-US.json index aaf9eb85ef..4b28a645e6 100644 --- a/web/src/locales/languages/en-US.json +++ b/web/src/locales/languages/en-US.json @@ -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.", diff --git a/web/src/locales/languages/es-ES.json b/web/src/locales/languages/es-ES.json index ebe9d9e7df..c20c006606 100644 --- a/web/src/locales/languages/es-ES.json +++ b/web/src/locales/languages/es-ES.json @@ -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.", diff --git a/web/src/locales/languages/fr-FR.json b/web/src/locales/languages/fr-FR.json index fbe3315333..f4b84cbaca 100644 --- a/web/src/locales/languages/fr-FR.json +++ b/web/src/locales/languages/fr-FR.json @@ -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.", diff --git a/web/src/locales/languages/it-IT.json b/web/src/locales/languages/it-IT.json index 9c103299cb..bedf4db6af 100644 --- a/web/src/locales/languages/it-IT.json +++ b/web/src/locales/languages/it-IT.json @@ -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.", diff --git a/web/src/locales/languages/ja-JP.json b/web/src/locales/languages/ja-JP.json index 811c166f27..6da3965346 100644 --- a/web/src/locales/languages/ja-JP.json +++ b/web/src/locales/languages/ja-JP.json @@ -9592,6 +9592,8 @@ "modalSubtitle": "監視するものを選択", "modalTitle": "チェックを作成", "pageTitle": "新規 {label} チェック", + "saveAndContinue": "保存して続行", + "saveAndExit": "保存して終了", "saveCheck": "チェックを保存", "saveFailed": "チェックの保存に失敗しました。", "saved": "チェックが正常に保存されました。", diff --git a/web/src/locales/languages/ko-KR.json b/web/src/locales/languages/ko-KR.json index 54f2ad72a0..19f3551840 100644 --- a/web/src/locales/languages/ko-KR.json +++ b/web/src/locales/languages/ko-KR.json @@ -9592,6 +9592,8 @@ "modalSubtitle": "모니터링할 항목 선택", "modalTitle": "검사 생성", "pageTitle": "새 {label} 검사", + "saveAndContinue": "저장 후 계속", + "saveAndExit": "저장 후 종료", "saveCheck": "검사 저장", "saveFailed": "검사 저장에 실패했습니다.", "saved": "검사가 성공적으로 저장되었습니다.", diff --git a/web/src/locales/languages/nl-NL.json b/web/src/locales/languages/nl-NL.json index 4d7eb39cb7..6369e242ed 100644 --- a/web/src/locales/languages/nl-NL.json +++ b/web/src/locales/languages/nl-NL.json @@ -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.", diff --git a/web/src/locales/languages/pl-PL.json b/web/src/locales/languages/pl-PL.json index 2ee6468ad5..72af18cb63 100644 --- a/web/src/locales/languages/pl-PL.json +++ b/web/src/locales/languages/pl-PL.json @@ -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.", diff --git a/web/src/locales/languages/pt-PT.json b/web/src/locales/languages/pt-PT.json index f9774e3028..1c191ecf87 100644 --- a/web/src/locales/languages/pt-PT.json +++ b/web/src/locales/languages/pt-PT.json @@ -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.", diff --git a/web/src/locales/languages/ru-RU.json b/web/src/locales/languages/ru-RU.json index 0a1fc191b1..a05161c37c 100644 --- a/web/src/locales/languages/ru-RU.json +++ b/web/src/locales/languages/ru-RU.json @@ -9592,6 +9592,8 @@ "modalSubtitle": "Выберите, что вы хотите мониторить", "modalTitle": "Создать проверку", "pageTitle": "Новая проверка {label}", + "saveAndContinue": "Сохранить и продолжить", + "saveAndExit": "Сохранить и выйти", "saveCheck": "Сохранить проверку", "saveFailed": "Не удалось сохранить проверку.", "saved": "Проверка успешно сохранена.", diff --git a/web/src/locales/languages/tr-TR.json b/web/src/locales/languages/tr-TR.json index 61a93b8f21..0a127a7c78 100644 --- a/web/src/locales/languages/tr-TR.json +++ b/web/src/locales/languages/tr-TR.json @@ -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.", diff --git a/web/src/locales/languages/vi-VN.json b/web/src/locales/languages/vi-VN.json index dfd57e0f50..cf6e705431 100644 --- a/web/src/locales/languages/vi-VN.json +++ b/web/src/locales/languages/vi-VN.json @@ -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.", diff --git a/web/src/locales/languages/zh-CN.json b/web/src/locales/languages/zh-CN.json index a5695c5796..bd8d3d0254 100644 --- a/web/src/locales/languages/zh-CN.json +++ b/web/src/locales/languages/zh-CN.json @@ -9592,6 +9592,8 @@ "modalSubtitle": "选择您要监控的内容", "modalTitle": "创建检查", "pageTitle": "新建 {label} 检查", + "saveAndContinue": "保存并继续", + "saveAndExit": "保存并退出", "saveCheck": "保存检查", "saveFailed": "保存检查失败。", "saved": "检查已成功保存。", diff --git a/web/src/locales/languages/zh-TW.json b/web/src/locales/languages/zh-TW.json index 9e3c1c0bd4..959e498b28 100644 --- a/web/src/locales/languages/zh-TW.json +++ b/web/src/locales/languages/zh-TW.json @@ -9587,6 +9587,8 @@ "modalSubtitle": "選擇您要監控的內容", "modalTitle": "建立檢查", "pageTitle": "新增 {label} 檢查", + "saveAndContinue": "儲存並繼續", + "saveAndExit": "儲存並離開", "saveCheck": "儲存檢查", "saveFailed": "儲存檢查失敗。", "saved": "檢查儲存成功。", diff --git a/web/src/views/synthetics/CreateBrowserTest.spec.ts b/web/src/views/synthetics/CreateBrowserTest.spec.ts index 71b8dcd3c3..113572ccf5 100644 --- a/web/src/views/synthetics/CreateBrowserTest.spec.ts +++ b/web/src/views/synthetics/CreateBrowserTest.spec.ts @@ -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: - '', + '', props: ["variant", "size", "disabled", "loading", "class", "iconLeft"], inheritAttrs: true, }, @@ -235,6 +237,39 @@ function mountPage(props: Record = {}) { }); } +/** 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(); diff --git a/web/src/views/synthetics/CreateBrowserTest.vue b/web/src/views/synthetics/CreateBrowserTest.vue index 1f090e681a..61f73d28ff 100644 --- a/web/src/views/synthetics/CreateBrowserTest.vue +++ b/web/src/views/synthetics/CreateBrowserTest.vue @@ -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 { // ── 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" > -