From c16ef73090ff45d599993a2584e7c77f0e63e99a Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 11 Feb 2026 10:43:53 +0100 Subject: [PATCH] fix: flaky plan e2e test (#27349) --- .../ChargingPlans/PlansSettings.vue | 42 +++++++++---------- assets/js/utils/debounceLeading.ts | 26 ++++++++++++ 2 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 assets/js/utils/debounceLeading.ts diff --git a/assets/js/components/ChargingPlans/PlansSettings.vue b/assets/js/components/ChargingPlans/PlansSettings.vue index 03b055508..bd93d3e8e 100644 --- a/assets/js/components/ChargingPlans/PlansSettings.vue +++ b/assets/js/components/ChargingPlans/PlansSettings.vue @@ -80,8 +80,9 @@ import collector from "@/mixins/collector"; import api from "@/api"; import deepEqual from "@/utils/deepEqual"; import convertRates from "@/utils/convertRates"; +import { debounceLeading } from "@/utils/debounceLeading"; import { defineComponent, type PropType } from "vue"; -import type { Vehicle, Timeout, CURRENCY, Forecast } from "@/types/evcc"; +import type { Vehicle, CURRENCY, Forecast } from "@/types/evcc"; import type { StaticPlan, RepeatingPlan, @@ -135,9 +136,10 @@ export default defineComponent({ staticPlanPreview: {} as StaticPlan, plan: {} as PlanWrapper, activeTab: "time", - debounceTimer: null as Timeout, nextPlanId: 0, strategyOpen: false, + updatePlanPreviewDebounced: null as any as () => void, + updateActivePlanDebounced: null as any as () => void, }; }, computed: { @@ -182,8 +184,10 @@ export default defineComponent({ }, effectivePlanStrategy: { deep: true, - handler() { - this.updatePlanDebounced(); + handler(vNew: PlanStrategy, vOld: PlanStrategy) { + if (!deepEqual(vNew, vOld)) { + this.updatePlanDebounced(); + } }, }, staticPlan: { @@ -204,14 +208,22 @@ export default defineComponent({ }, }, mounted(): void { + this.updatePlanPreviewDebounced = debounceLeading( + async () => await this.updatePreviewPlan(), + 300 + ); + this.updateActivePlanDebounced = debounceLeading( + async () => await this.updateActivePlan(), + 300 + ); this.updatePlanDebounced(); }, methods: { - async updatePlanDebounced() { + updatePlanDebounced(): void { if (this.noActivePlan) { - await this.updatePlanPreviewDebounced(); + this.updatePlanPreviewDebounced(); } else { - await this.updateActivePlanDebounced(); + this.updateActivePlanDebounced(); } }, async updateActivePlan(): Promise { @@ -285,22 +297,6 @@ export default defineComponent({ console.error(e); } }, - async updatePlanPreviewDebounced(): Promise { - if (!this.debounceTimer) { - await this.updatePreviewPlan(); - return; - } - clearTimeout(this.debounceTimer); - this.debounceTimer = setTimeout(async () => await this.updatePreviewPlan(), 1000); - }, - async updateActivePlanDebounced(): Promise { - if (!this.debounceTimer) { - await this.updateActivePlan(); - return; - } - clearTimeout(this.debounceTimer); - this.debounceTimer = setTimeout(async () => await this.updateActivePlan(), 1000); - }, removeStaticPlan(): void { this.$emit("static-plan-removed"); }, diff --git a/assets/js/utils/debounceLeading.ts b/assets/js/utils/debounceLeading.ts new file mode 100644 index 000000000..5316667a1 --- /dev/null +++ b/assets/js/utils/debounceLeading.ts @@ -0,0 +1,26 @@ +import type { Timeout } from "@/types/evcc"; + +/** + * Creates a debounced version of `fn` that calls at the leading edge. + * The debounced function does not return the result of `fn`, even if `fn` is async. + */ +export function debounceLeading any>( + fn: T, + delay: number +): (...args: Parameters) => void { + let timer: Timeout; + return (...args: Parameters) => { + if (!timer) { + fn(...args); + timer = setTimeout(() => { + timer = null; + }, delay); + return; + } + clearTimeout(timer); + timer = setTimeout(() => { + timer = null; + fn(...args); + }, delay); + }; +}