chore: e2e stability vehicle settings, fatal (#22848)

This commit is contained in:
Michael Geers 2025-08-08 15:26:45 +02:00 • committed by GitHub
parent c4407c290d
commit 4f3beb150b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 76 additions and 96 deletions

View file

@ -208,6 +208,7 @@ This file provides guidance to AI coding agents when working with code in this r
- Use `expectModalVisible()` and `expectModalHidden()` helpers
- Test configuration persistence across application restarts
- Standard structure: import `{ start, stop, baseUrl }` from `./evcc`, use `test.afterEach(stop)`
- Never use fixed timeouts, use existance of elements or wait for network idle
## Device Integration & Configuration

View file

@ -30,80 +30,61 @@
</LabelAndValue>
<Teleport to="body">
<div
<GenericModal
:id="modalId"
ref="modal"
class="modal fade text-dark modal-xl"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
aria-hidden="true"
:title="modalTitle"
size="xl"
data-testid="charging-plan-modal"
@open="modalVisible"
@closed="modalInvisible"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("main.chargingPlan.modalTitle")
}}<span v-if="socBasedPlanning && vehicle"
>: {{ vehicle.title }}</span
>
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body pt-2">
<ul class="nav nav-tabs">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: departureTabActive }"
href="#"
@click.prevent="showDeatureTab"
>
{{ $t("main.chargingPlan.departureTab") }}
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: arrivalTabActive }"
href="#"
@click.prevent="showArrivalTab"
>
{{ $t("main.chargingPlan.arrivalTab") }}
</a>
</li>
</ul>
<div v-if="isModalVisible">
<ChargingPlansSettings
v-if="departureTabActive"
v-bind="chargingPlansSettingsProps"
@static-plan-updated="updateStaticPlan"
@static-plan-removed="removeStaticPlan"
@repeating-plans-updated="updateRepeatingPlans"
/>
<ChargingPlanArrival
v-if="arrivalTabActive"
v-bind="chargingPlanArrival"
@minsoc-updated="setMinSoc"
@limitsoc-updated="setLimitSoc"
/>
</div>
</div>
<div class="pt-2">
<ul class="nav nav-tabs">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: departureTabActive }"
href="#"
@click.prevent="showDeatureTab"
>
{{ $t("main.chargingPlan.departureTab") }}
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: arrivalTabActive }"
href="#"
@click.prevent="showArrivalTab"
>
{{ $t("main.chargingPlan.arrivalTab") }}
</a>
</li>
</ul>
<div v-if="isModalVisible">
<ChargingPlansSettings
v-if="departureTabActive"
v-bind="chargingPlansSettingsProps"
@static-plan-updated="updateStaticPlan"
@static-plan-removed="removeStaticPlan"
@repeating-plans-updated="updateRepeatingPlans"
/>
<ChargingPlanArrival
v-if="arrivalTabActive"
v-bind="chargingPlanArrival"
@minsoc-updated="setMinSoc"
@limitsoc-updated="setLimitSoc"
/>
</div>
</div>
</div>
</GenericModal>
</Teleport>
</div>
</template>
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import GenericModal from "../Helper/GenericModal.vue";
import LabelAndValue from "../Helper/LabelAndValue.vue";
import PlansSettings from "./PlansSettings.vue";
import Arrival from "./Arrival.vue";
@ -121,6 +102,7 @@ const ONE_MINUTE = 60 * 1000;
export default defineComponent({
name: "ChargingPlan",
components: {
GenericModal,
LabelAndValue,
ChargingPlansSettings: PlansSettings,
ChargingPlanArrival: Arrival,
@ -154,7 +136,6 @@ export default defineComponent({
},
data() {
return {
modal: null as Modal | null,
isModalVisible: false,
activeTab: "departure",
targetTimeLabel: "",
@ -210,6 +191,13 @@ export default defineComponent({
modalId(): string {
return `chargingPlanModal_${this.id}`;
},
modalTitle(): string {
const baseTitle = this.$t("main.chargingPlan.modalTitle");
if (this.socBasedPlanning && this.vehicle) {
return `${baseTitle}: ${this.vehicle.title}`;
}
return baseTitle;
},
departureTabActive(): boolean {
return this.activeTab === "departure";
},
@ -251,38 +239,15 @@ export default defineComponent({
},
},
mounted(): void {
const ref = this.$refs["modal"];
if (ref) {
this.modal = Modal.getOrCreateInstance(ref);
ref.addEventListener("show.bs.modal", this.modalVisible);
ref.addEventListener("hidden.bs.modal", this.modalInvisible);
ref.addEventListener("hide.bs.modal", this.checkUnsavedOnClose);
}
this.interval = setInterval(this.updateTargetTimeLabel, ONE_MINUTE);
this.updateTargetTimeLabel();
},
unmounted(): void {
const ref = this.$refs["modal"];
if (ref) {
ref.removeEventListener("show.bs.modal", this.modalVisible);
ref.removeEventListener("hidden.bs.modal", this.modalInvisible);
ref.removeEventListener("hide.bs.modal", this.checkUnsavedOnClose);
}
if (this.interval) {
clearInterval(this.interval);
}
},
methods: {
checkUnsavedOnClose(): void {
const applyButton = this.$refs["modal"]?.querySelector<HTMLElement>(
"[data-testid=plan-apply]"
);
if (applyButton) {
if (confirm(this.$t("main.chargingPlan.unsavedChanges"))) {
applyButton.click();
}
}
},
modalVisible(): void {
this.isModalVisible = true;
},
@ -291,7 +256,8 @@ export default defineComponent({
},
openModal(): void {
this.showDeatureTab();
this.modal?.show();
const modalRef = this.$refs["modal"] as any;
modalRef?.open();
},
openPlanModal(arrivalTab = false) {
if (arrivalTab) {
@ -299,7 +265,8 @@ export default defineComponent({
} else {
this.showDeatureTab();
}
this.modal?.show();
const modalRef = this.$refs["modal"] as any;
modalRef?.open();
},
updateTargetTimeLabel(): void {
if (!this.effectivePlanTime) return;

View file

@ -81,6 +81,7 @@ test.describe("fatal config handling", async () => {
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await expect(page.getByTestId("loadpoint")).toContainText("Test Carport");
await page.waitForLoadState("networkidle");
// break meter
await stopSimulator();
@ -107,6 +108,7 @@ test.describe("fatal config handling", async () => {
await expectModalHidden(meterModal);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await page.waitForLoadState("networkidle");
// restart and verify
await restart();

View file

@ -599,6 +599,7 @@ test.describe("repeating", async () => {
await plan.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("09:20");
await page.waitForLoadState("networkidle");
await restart(CONFIG);
await page.goto("/");

View file

@ -1,5 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden } from "./utils";
import {
startSimulator,
stopSimulator,
@ -36,11 +37,16 @@ test.describe("minSoc", async () => {
await page.goto("/");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
const modal = page.getByTestId("charging-plan-modal");
await expectModalVisible(modal);
await modal.getByRole("link", { name: "Arrival" }).click();
await expect(page.getByText("charged to x in solar mode")).toBeVisible();
await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%");
await expect(page.getByText("charged to 20% in solar mode")).toBeVisible();
await expect(modal).toContainText("charged to x in solar mode");
await modal.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%");
await expect(modal).toContainText("charged to 20% in solar mode");
await modal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(modal);
await page.waitForLoadState("networkidle");
await restart(simulatorConfig());
await page.reload();
@ -75,10 +81,13 @@ test.describe("limitSoc", async () => {
await page.goto("/");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
const modal = page.getByTestId("charging-plan-modal");
await expectModalVisible(modal);
await modal.getByRole("link", { name: "Arrival" }).click();
await page.getByRole("combobox", { name: "Default limit" }).selectOption("80%");
await page.getByRole("button", { name: "Close" }).click();
await modal.getByRole("combobox", { name: "Default limit" }).selectOption("80%");
await modal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(modal);
await expect(page.getByTestId("limit-soc-value")).toContainText("80%");
await page.waitForLoadState("networkidle");