diff --git a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue index f855f7967..b20fbf4e6 100644 --- a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue +++ b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue @@ -512,6 +512,11 @@ export default defineComponent({ }, values: { handler() { + // a prior test result no longer matches the edited config: + // revert "Save anyway" back to "Validate & save" + if (this.test.isError || this.test.isSuccess) { + this.test = initialTestState(); + } this.updateServiceValues(); }, deep: true, @@ -689,7 +694,6 @@ export default defineComponent({ this.testDevice, this.$refs["form"] as HTMLFormElement ); - console.log("test result", success); if (!success) { return; } diff --git a/tests/config-required-field.spec.ts b/tests/config-required-field.spec.ts new file mode 100644 index 000000000..3ed3801a7 --- /dev/null +++ b/tests/config-required-field.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "@playwright/test"; +import { start, stop, baseUrl } from "./evcc"; +import { expectModalVisible } from "./utils"; + +test.use({ baseURL: baseUrl() }); + +const templateFlags = [ + "--disable-auth", + "--template-type", + "meter", + "--template", + "tests/config-required-field.tpl.yaml", +]; + +test.beforeEach(async () => { + await start(undefined, undefined, templateFlags); +}); +test.afterEach(async () => { + await stop(); +}); + +test.describe("required field validation", async () => { + // regression test for evcc-io/evcc#29919: clearing a required field after a + // failed connection test must not allow saving via the "Save anyway" button + test("cannot save with empty required field after failed test", async ({ page }) => { + await page.goto("/#/config"); + + await page.getByRole("button", { name: "Add grid meter" }).click(); + const meterModal = page.getByTestId("meter-modal"); + await expectModalVisible(meterModal); + await meterModal.getByLabel("Manufacturer").selectOption("Required Field Demo"); + + // fill the required field so the connection test runs (and then fails) + await meterModal.getByLabel("Secret").fill("some-secret"); + await meterModal.getByRole("button", { name: "Validate & save" }).click(); + + // failed test exposes the "Save anyway" force-save button + const forceSave = meterModal.getByRole("button", { name: "Save anyway" }); + await expect(forceSave).toBeVisible(); + + // clearing the field invalidates the stale test result: the button must + // revert to "Validate & save" instead of staying in force-save mode + await meterModal.getByLabel("Secret").fill(""); + await expect(forceSave).not.toBeVisible(); + const validateSave = meterModal.getByRole("button", { name: "Validate & save" }); + await expect(validateSave).toBeVisible(); + + // attempting to save with the empty required field is blocked by validation + await validateSave.click(); + await expectModalVisible(meterModal); + await expect(meterModal.getByLabel("Secret")).toHaveJSProperty("validity.valid", false); + + // nothing was persisted + await expect(page.getByTestId("grid")).toHaveCount(0); + }); +}); diff --git a/tests/config-required-field.tpl.yaml b/tests/config-required-field.tpl.yaml new file mode 100644 index 000000000..0f3790e26 --- /dev/null +++ b/tests/config-required-field.tpl.yaml @@ -0,0 +1,20 @@ +template: required-field-demo +group: generic +products: + - description: + generic: Required Field Demo +params: + - name: usage + choice: ["grid"] + - name: secret + description: + generic: Secret + required: true + +# unreachable uri so the connection test always fails, exposing the +# "Save anyway" button (see tests/config-required-field.spec.ts) +render: | + type: custom + power: + source: http + uri: http://localhost:1/{{ .secret }}