Charging plan: make strategy settings discoverable (#32452)
This commit is contained in:
parent
0df2610dcf
commit
9d0bc5a865
8 changed files with 232 additions and 109 deletions
|
|
@ -847,7 +847,7 @@ input::-webkit-date-and-time-value {
|
|||
.collapsible-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows var(--evcc-transition-fast) ease 0.2s;
|
||||
transition: grid-template-rows var(--evcc-transition-fast) ease var(--evcc-transition-fast);
|
||||
}
|
||||
|
||||
.collapsible-wrapper.open {
|
||||
|
|
@ -858,12 +858,19 @@ input::-webkit-date-and-time-value {
|
|||
.collapsible-content {
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
/* keep hidden content non-interactive and out of the a11y tree */
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity var(--evcc-transition-fast) ease,
|
||||
visibility var(--evcc-transition-fast) ease;
|
||||
}
|
||||
|
||||
.collapsible-wrapper.open .collapsible-content {
|
||||
opacity: 1;
|
||||
transition: opacity var(--evcc-transition-fast) ease 0.2s;
|
||||
visibility: visible;
|
||||
transition:
|
||||
opacity var(--evcc-transition-fast) ease var(--evcc-transition-fast),
|
||||
visibility var(--evcc-transition-fast) ease var(--evcc-transition-fast);
|
||||
}
|
||||
|
||||
.sticky-bottom {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mb-5 mb-lg-4" data-testid="plan-entry">
|
||||
<div class="mb-4" data-testid="plan-entry">
|
||||
<h5
|
||||
v-if="multiplePlans"
|
||||
class="d-flex gap-3 align-items-baseline d-lg-none mb-4 fw-normal evcc-gray"
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
<span class="text-uppercase fs-6">
|
||||
{{ `${$t("main.chargingPlan.planNumber", { number: "#1" })}` }}
|
||||
</span>
|
||||
<small>
|
||||
{{ $t("main.chargingPlan.once") }}
|
||||
</small>
|
||||
</h5>
|
||||
|
||||
<div class="row d-none d-lg-flex mb-2">
|
||||
|
|
|
|||
|
|
@ -1,58 +1,86 @@
|
|||
<template>
|
||||
<div class="collapsible-wrapper" :class="{ open: show }">
|
||||
<div class="collapsible-content pb-3">
|
||||
<div v-if="disabled" class="row mb-4">
|
||||
<div class="small text-muted">
|
||||
<strong class="text-primary">{{ $t("general.note") }}</strong>
|
||||
{{ $t("main.chargingPlan.strategyDisabledDescription") }}
|
||||
</div>
|
||||
<div data-testid="plan-strategy" class="mb-5 mb-lg-4">
|
||||
<div :class="{ 'mb-2': open }">
|
||||
<div v-if="disabled">
|
||||
<span class="fw-bold evcc-gray">{{ $t("main.chargingPlan.strategy.label") }}:</span>
|
||||
{{ " " }}
|
||||
<span class="small">{{ summary }}</span>
|
||||
</div>
|
||||
<div v-else class="row">
|
||||
<div class="col-12 col-sm-6 col-lg-3 offset-lg-3 mb-3">
|
||||
<div class="row">
|
||||
<label :for="formId('continuous')" class="col-form-label col-5 col-sm-12">
|
||||
{{ $t("main.chargingPlan.optimization.label") }}
|
||||
</label>
|
||||
<div class="col-7 col-sm-12">
|
||||
<select
|
||||
:id="formId('continuous')"
|
||||
v-model="localContinuous"
|
||||
class="form-select"
|
||||
@change="updateStrategy"
|
||||
>
|
||||
<option :value="false">
|
||||
{{ $t("main.chargingPlan.optimization.cheapest") }}
|
||||
</option>
|
||||
<option :value="true">
|
||||
{{ $t("main.chargingPlan.optimization.continuous") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-link btn-sm text-gray p-0 border-0 d-flex align-items-center text-start"
|
||||
:class="{ 'text-primary': open }"
|
||||
@click="open = !open"
|
||||
>
|
||||
<span class="fw-bold">
|
||||
{{ $t("main.chargingPlan.strategy.label") }}<span v-if="!open">:</span>
|
||||
</span>
|
||||
<span v-if="!open" class="ms-1">{{ summary }}</span>
|
||||
<DropdownIcon class="icon flex-shrink-0" :class="{ iconUp: open }" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapsible-wrapper" :class="{ open }">
|
||||
<div class="collapsible-content ring-space">
|
||||
<div v-if="disabled" class="row mb-4">
|
||||
<div class="small text-muted">
|
||||
<strong class="text-primary">{{ $t("general.note") }}</strong>
|
||||
{{ $t("main.chargingPlan.strategyDisabledDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3 mb-3">
|
||||
<div class="row">
|
||||
<label :for="formId('precondition')" class="col-form-label col-5 col-sm-12">
|
||||
{{ $t("main.chargingPlan.precondition.label") }}
|
||||
</label>
|
||||
<div class="col-7 col-sm-12">
|
||||
<select
|
||||
:id="formId('precondition')"
|
||||
v-model="localPrecondition"
|
||||
class="form-select"
|
||||
@change="updateStrategy"
|
||||
>
|
||||
<option :value="0">
|
||||
{{ $t("main.chargingPlan.precondition.optionNo") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="opt in preconditionOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
<div v-else class="row">
|
||||
<div class="col-12 col-lg-6 mb-3">
|
||||
<div class="row">
|
||||
<label :for="formId('continuous')" class="col-form-label col-5 col-lg-12">
|
||||
{{ $t("main.chargingPlan.optimization.label") }}
|
||||
</label>
|
||||
<div class="col-7 col-lg-12">
|
||||
<select
|
||||
:id="formId('continuous')"
|
||||
v-model="localContinuous"
|
||||
class="form-select"
|
||||
@change="updateStrategy"
|
||||
>
|
||||
{{ opt.name }}
|
||||
</option>
|
||||
</select>
|
||||
<option :value="false">
|
||||
{{ $t(`main.chargingPlan.optimization.${cheapestKey}`) }}
|
||||
</option>
|
||||
<option :value="true">
|
||||
{{ $t("main.chargingPlan.optimization.continuous") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">
|
||||
{{ optimizationDescription }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 mb-3">
|
||||
<div class="row">
|
||||
<label :for="formId('precondition')" class="col-form-label col-5 col-lg-12">
|
||||
{{ $t("main.chargingPlan.precondition.label") }}
|
||||
</label>
|
||||
<div class="col-7 col-lg-12">
|
||||
<select
|
||||
:id="formId('precondition')"
|
||||
v-model="localPrecondition"
|
||||
class="form-select"
|
||||
@change="updateStrategy"
|
||||
>
|
||||
<option :value="0">
|
||||
{{ $t("main.chargingPlan.precondition.optionNo") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="opt in preconditionOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">
|
||||
{{ $t("main.chargingPlan.precondition.description") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -62,28 +90,63 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import type { PlanStrategy } from "@/types/evcc";
|
||||
import { SMART_COST_TYPE, type PlanStrategy } from "@/types/evcc";
|
||||
import DropdownIcon from "../MaterialIcon/Dropdown.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChargingPlanStrategy",
|
||||
components: { DropdownIcon },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
id: [String, Number],
|
||||
show: Boolean,
|
||||
precondition: { type: Number, default: 0 },
|
||||
continuous: { type: Boolean, default: false },
|
||||
disabled: Boolean,
|
||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||
},
|
||||
emits: ["update"],
|
||||
data() {
|
||||
return {
|
||||
open: false,
|
||||
localPrecondition: this.precondition,
|
||||
localContinuous: this.continuous,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isCo2(): boolean {
|
||||
return this.smartCostType === SMART_COST_TYPE.CO2;
|
||||
},
|
||||
cheapestKey(): string {
|
||||
return this.isCo2 ? "cleanest" : "cheapest";
|
||||
},
|
||||
summary(): string {
|
||||
if (this.disabled) {
|
||||
return this.$t("main.chargingPlan.strategyDisabledDescription");
|
||||
}
|
||||
const parts = [
|
||||
this.$t(
|
||||
`main.chargingPlan.optimization.${this.continuous ? "continuous" : this.cheapestKey}`
|
||||
),
|
||||
];
|
||||
if (this.precondition) {
|
||||
parts.push(
|
||||
this.$t("main.chargingPlan.precondition.summary", {
|
||||
precondition: this.fmtDurationLong(this.precondition),
|
||||
})
|
||||
);
|
||||
}
|
||||
return parts.join(", ");
|
||||
},
|
||||
optimizationDescription(): string {
|
||||
const variant = this.localContinuous
|
||||
? this.isCo2
|
||||
? "continuousCo2"
|
||||
: "continuousPrice"
|
||||
: this.cheapestKey;
|
||||
return this.$t(`main.chargingPlan.optimization.${variant}Description`);
|
||||
},
|
||||
preconditionOptions() {
|
||||
const HOUR = 60 * 60;
|
||||
const QUARTER_HOUR = 0.25 * HOUR;
|
||||
|
|
@ -142,3 +205,18 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* overflow-hidden would clip the select focus rings */
|
||||
.ring-space {
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
}
|
||||
.icon {
|
||||
transform: rotate(0deg);
|
||||
transition: transform var(--evcc-transition-medium) ease;
|
||||
}
|
||||
.iconUp {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<ChargingPlanRepeatingSettings
|
||||
:showHeader="index === 0"
|
||||
:number="index + 2"
|
||||
class="mb-5 mb-lg-4"
|
||||
class="mb-4"
|
||||
:formIdPrefix="formIdPrefix"
|
||||
v-bind="plan"
|
||||
:rangePerSoc="rangePerSoc"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@
|
|||
@updated="updateRepeatingPlans"
|
||||
/>
|
||||
</div>
|
||||
<ChargingPlanStrategy
|
||||
:id="id"
|
||||
:precondition="effectivePlanStrategy?.precondition"
|
||||
:continuous="effectivePlanStrategy?.continuous"
|
||||
:disabled="strategyDisabled"
|
||||
:smart-cost-type="smartCostType"
|
||||
@update="updatePlanStrategy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
|
@ -40,29 +48,11 @@
|
|||
<span v-if="!multiplePlans">
|
||||
{{ $t(`main.targetCharge.${noActivePlan ? "preview" : "currentPlan"}`) }}
|
||||
</span>
|
||||
<span v-else-if="noActivePlan">{{ $t("main.targetCharge.preview") }} #1</span>
|
||||
<span v-else-if="noActivePlan">{{ $t("main.targetCharge.preview") }}: #1</span>
|
||||
<span v-else-if="alreadyReached">{{ $t("main.targetCharge.goalReached") }}</span>
|
||||
<span v-else>{{ nextPlanTitle }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
:class="strategyOpen ? 'btn-secondary' : 'evcc-gray'"
|
||||
:aria-label="$t('main.chargingPlan.strategySettings')"
|
||||
tabindex="0"
|
||||
@click="strategyOpen = !strategyOpen"
|
||||
>
|
||||
<shopicon-regular-adjust size="s"></shopicon-regular-adjust>
|
||||
</button>
|
||||
</div>
|
||||
</h5>
|
||||
<ChargingPlanStrategy
|
||||
:id="id"
|
||||
:precondition="effectivePlanStrategy?.precondition"
|
||||
:continuous="effectivePlanStrategy?.continuous"
|
||||
:disabled="strategyDisabled"
|
||||
:show="strategyOpen"
|
||||
@update="updatePlanStrategy"
|
||||
/>
|
||||
<ChargingPlanPreview v-bind="chargingPlanPreviewProps" />
|
||||
<ChargingPlanWarnings v-bind="chargingPlanWarningsProps" />
|
||||
</div>
|
||||
|
|
@ -88,6 +78,7 @@ import type {
|
|||
PlanStrategy,
|
||||
PlanWrapper,
|
||||
RepeatingPlan,
|
||||
SMART_COST_TYPE,
|
||||
StaticEnergyPlan,
|
||||
StaticPlan,
|
||||
StaticSocPlan,
|
||||
|
|
@ -117,7 +108,7 @@ export default defineComponent({
|
|||
socBasedPlanning: Boolean,
|
||||
socPerKwh: Number,
|
||||
rangePerSoc: Number,
|
||||
smartCostType: String,
|
||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
mode: String,
|
||||
capacity: Number,
|
||||
|
|
@ -138,7 +129,6 @@ export default defineComponent({
|
|||
plan: {} as PlanWrapper,
|
||||
activeTab: "time",
|
||||
nextPlanId: 0,
|
||||
strategyOpen: false,
|
||||
updatePlanPreviewDebounced: null as any as () => void,
|
||||
updateActivePlanDebounced: null as any as () => void,
|
||||
};
|
||||
|
|
@ -170,7 +160,7 @@ export default defineComponent({
|
|||
return this.plan.duration === 0;
|
||||
},
|
||||
nextPlanTitle(): string {
|
||||
return `${this.$t("main.targetCharge.nextPlan")} #${this.nextPlanId}`;
|
||||
return `${this.$t("main.targetCharge.nextPlan")}: #${this.nextPlanId}`;
|
||||
},
|
||||
strategyDisabled(): boolean {
|
||||
// options only make sense if there are variable prices
|
||||
|
|
@ -327,7 +317,7 @@ export default defineComponent({
|
|||
h5 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
top: -33px;
|
||||
top: -26px;
|
||||
margin-bottom: -0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
justify-content: center;
|
||||
|
|
|
|||
15
i18n/de.json
15
i18n/de.json
|
|
@ -1280,24 +1280,33 @@
|
|||
"goal": "Ladeziel",
|
||||
"modalTitle": "Ladeplanung",
|
||||
"none": "keiner",
|
||||
"once": "einmalig",
|
||||
"optimization": {
|
||||
"cheapest": "günstigst",
|
||||
"cheapestDescription": "Wählt die günstigsten Zeitfenster. Kann zu mehreren Starts und Stopps führen.",
|
||||
"cleanest": "sauberst",
|
||||
"cleanestDescription": "Wählt die saubersten Zeitfenster. Kann zu mehreren Starts und Stopps führen.",
|
||||
"continuous": "kontinuierlich",
|
||||
"continuousCo2Description": "Wählt die sauberste unterbrechungsfreie Folge von Zeitfenstern.",
|
||||
"continuousPriceDescription": "Wählt die günstigste unterbrechungsfreie Folge von Zeitfenstern.",
|
||||
"label": "Optimierung"
|
||||
},
|
||||
"planNumber": "Plan {number}",
|
||||
"precondition": {
|
||||
"description": "Lade {duration} vor Abfahrt zur Batterie-Vorkonditionierung.",
|
||||
"description": "Verlagert einen Teil des Ladens direkt vor die Abfahrt: warme Batterie, weniger Zeit mit hohem Ladestand.",
|
||||
"label": "Spätes Laden",
|
||||
"optionAll": "alles",
|
||||
"optionNo": "nein"
|
||||
"optionNo": "nein",
|
||||
"summary": "{precondition} vorher"
|
||||
},
|
||||
"remove": "Entfernen",
|
||||
"repeating": "wiederholend",
|
||||
"repeatingPlans": "Wiederholende Pläne",
|
||||
"selectAll": "Alle wählen",
|
||||
"strategy": {
|
||||
"label": "Strategie"
|
||||
},
|
||||
"strategyDisabledDescription": "Das Laden startet so spät wie möglich und wird rechtzeitig zur Abfahrt abgeschlossen. Mit dynamischen Netzpreisen oder einem CO₂-Tarif stehen hier weitere Optionen zur Verfügung.",
|
||||
"strategySettings": "Strategie-Einstellungen",
|
||||
"time": "Zeit",
|
||||
"title": "Plan",
|
||||
"unsavedChanges": "Ungespeicherte Änderungen vorhanden. Jetzt anwenden?",
|
||||
|
|
|
|||
15
i18n/en.json
15
i18n/en.json
|
|
@ -1280,24 +1280,33 @@
|
|||
"goal": "Charging goal",
|
||||
"modalTitle": "Charging Plan",
|
||||
"none": "none",
|
||||
"once": "once",
|
||||
"optimization": {
|
||||
"cheapest": "cheapest",
|
||||
"cheapestDescription": "Selects the cheapest slots. May result in multiple starts and stops.",
|
||||
"cleanest": "cleanest",
|
||||
"cleanestDescription": "Selects the cleanest slots. May result in multiple starts and stops.",
|
||||
"continuous": "continuous",
|
||||
"continuousCo2Description": "Selects the cleanest uninterrupted series of slots.",
|
||||
"continuousPriceDescription": "Selects the cheapest uninterrupted series of slots.",
|
||||
"label": "Optimization"
|
||||
},
|
||||
"planNumber": "Plan {number}",
|
||||
"precondition": {
|
||||
"description": "Charge {duration} before departure for battery preconditioning.",
|
||||
"description": "Moves part of the charging right before departure: warm battery, less time at high charge level.",
|
||||
"label": "Late Charging",
|
||||
"optionAll": "everything",
|
||||
"optionNo": "no"
|
||||
"optionNo": "no",
|
||||
"summary": "{precondition} before"
|
||||
},
|
||||
"remove": "Remove",
|
||||
"repeating": "repeating",
|
||||
"repeatingPlans": "Repeating plans",
|
||||
"selectAll": "Select all",
|
||||
"strategy": {
|
||||
"label": "Strategy"
|
||||
},
|
||||
"strategyDisabledDescription": "Charging starts as late as possible to finish just in time for departure. With dynamic grid prices or CO₂ tariff, more options are available here.",
|
||||
"strategySettings": "Strategy settings",
|
||||
"time": "Time",
|
||||
"title": "Plan",
|
||||
"unsavedChanges": "There are unsaved changes. Apply now?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const mobile = devices["iPhone 12 Mini"].viewport;
|
|||
const CONFIG = "plan.evcc.yaml";
|
||||
const CONFIG_NO_TARIFF = "basics.evcc.yaml";
|
||||
const CONFIG_FIXED_TARIFF = "plan-fixed-tariff.evcc.yaml";
|
||||
const CONFIG_CO2 = "battery-settings-co2.evcc.yaml";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await start(CONFIG);
|
||||
|
|
@ -79,11 +80,11 @@ test.describe("basic functionality", async () => {
|
|||
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("static-plan-time").fill("09:30");
|
||||
await page.getByTestId("static-plan-soc").selectOption("80%");
|
||||
await page.getByRole("button", { name: "Strategy settings" }).click();
|
||||
await page.getByTestId("plan-strategy").getByRole("button", { name: "cheapest" }).click();
|
||||
await expect(page.getByLabel("Late Charging")).toBeVisible();
|
||||
await page.getByLabel("Late Charging").selectOption("1 hour");
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await page.getByRole("button", { name: "Close", exact: true }).click();
|
||||
|
||||
const tomorrowShort = getWeekday(1, "short");
|
||||
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
|
||||
|
|
@ -487,23 +488,23 @@ test.describe("repeating", async () => {
|
|||
|
||||
// with multiple plans, preview shows first plan
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan #1");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan: #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// activate #1 - should show next plan #1
|
||||
await modal.getByTestId("static-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// deactivate #1, activate #2 - should show next plan #2
|
||||
await modal.getByTestId("static-plan-active").click();
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("11:11");
|
||||
|
||||
// back to preview if no active plan
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan #1");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan: #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:00");
|
||||
});
|
||||
|
||||
|
|
@ -550,7 +551,7 @@ test.describe("repeating", async () => {
|
|||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// specific weekday and time
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("Thu 09:22");
|
||||
});
|
||||
|
||||
|
|
@ -599,12 +600,12 @@ test.describe("repeating", async () => {
|
|||
await plan3.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// check next plans
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #3");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #3");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:10");
|
||||
|
||||
// disable plan #3
|
||||
await plan3.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:20");
|
||||
|
||||
// change plan #2 to yesterday
|
||||
|
|
@ -613,13 +614,13 @@ test.describe("repeating", async () => {
|
|||
await days2.getByRole("checkbox", { name: yesterday }).click(); // check
|
||||
await days2.click(); // close
|
||||
// no changes yet
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:20");
|
||||
|
||||
// apply
|
||||
await plan2.getByTestId("repeating-plan-apply").click();
|
||||
await expect(plan2.getByTestId("repeating-plan-apply")).not.toBeVisible();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:30");
|
||||
|
||||
// set lower targets than vehicle soc (50%)
|
||||
|
|
@ -654,13 +655,14 @@ test.describe("repeating", async () => {
|
|||
await plan.getByRole("checkbox", { name: "Select all" }).click(); // uncheck all
|
||||
await plan.getByRole("checkbox", { name: tomorrow }).check();
|
||||
await plan.getByTestId("repeating-plan-time").fill("09:20");
|
||||
await modal.getByRole("button", { name: "Strategy settings" }).click();
|
||||
await plan.getByTestId("repeating-plan-weekdays").click(); // close dropdown
|
||||
await modal.getByTestId("plan-strategy").getByRole("button", { name: "cheapest" }).click();
|
||||
await expect(modal.getByLabel("Late Charging")).toBeVisible();
|
||||
await modal.getByLabel("Late Charging").selectOption("2 hours");
|
||||
await plan.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:20");
|
||||
await modal.getByRole("button", { name: "Close" }).click();
|
||||
await modal.getByRole("button", { name: "Close", exact: true }).click();
|
||||
await expect(
|
||||
lp1.getByTestId("charging-plan").getByRole("button", { name: `${tomorrowShort} 09:20` })
|
||||
).toBeVisible();
|
||||
|
|
@ -679,7 +681,7 @@ test.describe("repeating", async () => {
|
|||
.getByRole("button", { name: `${tomorrowShort} 09:20` })
|
||||
.click();
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan: #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:20");
|
||||
await expect(modal.getByLabel("Late Charging")).toHaveValue("7200");
|
||||
});
|
||||
|
|
@ -695,12 +697,37 @@ test.describe("plan strategy", async () => {
|
|||
const modal = page.getByTestId("charging-plan-modal");
|
||||
await expect(modal.getByTestId("static-plan-active")).toBeVisible();
|
||||
|
||||
// Strategy toggle should be visible but expand to show informational note only
|
||||
await expect(modal.getByRole("button", { name: "Strategy settings" })).toBeVisible();
|
||||
await modal.getByRole("button", { name: "Strategy settings" }).click();
|
||||
// Strategy line shows informational note only, not clickable
|
||||
await expect(modal.getByTestId("plan-strategy")).toContainText("just in time for departure");
|
||||
await expect(modal.getByTestId("plan-strategy").getByRole("button")).toHaveCount(0);
|
||||
await expect(modal.getByLabel("Optimization")).not.toBeVisible();
|
||||
await expect(modal.getByLabel("Late Charging")).not.toBeVisible();
|
||||
await expect(modal).toContainText("just in time for departure");
|
||||
});
|
||||
|
||||
test("co2 tariff: cleanest instead of cheapest", async ({ page }) => {
|
||||
await restart(CONFIG_CO2);
|
||||
await page.goto("/");
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = page.getByTestId("charging-plan-modal");
|
||||
|
||||
await expect(modal.getByTestId("plan-strategy")).toContainText("cleanest");
|
||||
|
||||
// collapsed selects must not be interactive
|
||||
await expect(modal.getByLabel("Optimization")).not.toBeVisible();
|
||||
await expect(modal.getByLabel("Late Charging")).not.toBeVisible();
|
||||
|
||||
await modal.getByTestId("plan-strategy").getByRole("button", { name: "cleanest" }).click();
|
||||
|
||||
const optimization = modal.getByLabel("Optimization");
|
||||
await expect(optimization).toBeVisible();
|
||||
await expect(optimization.getByRole("option", { name: "cleanest" })).toHaveCount(1);
|
||||
await expect(modal.getByTestId("plan-strategy")).toContainText("cleanest slots");
|
||||
|
||||
await optimization.selectOption("continuous");
|
||||
await expect(modal.getByTestId("plan-strategy")).toContainText(
|
||||
"cleanest uninterrupted series"
|
||||
);
|
||||
});
|
||||
|
||||
test("visible and functional on mobile", async ({ page }) => {
|
||||
|
|
@ -719,11 +746,11 @@ test.describe("plan strategy", async () => {
|
|||
|
||||
const modal = page.getByTestId("charging-plan-modal");
|
||||
|
||||
// Strategy toggle should be visible on mobile
|
||||
await expect(modal.getByRole("button", { name: "Strategy settings" })).toBeVisible();
|
||||
// Strategy summary should be visible on mobile
|
||||
await expect(modal.getByTestId("plan-strategy")).toContainText("cheapest");
|
||||
|
||||
// Open strategy panel
|
||||
await modal.getByRole("button", { name: "Strategy settings" }).click();
|
||||
await modal.getByTestId("plan-strategy").getByRole("button", { name: "cheapest" }).click();
|
||||
|
||||
// Strategy controls should be visible and functional
|
||||
await expect(modal.getByLabel("Optimization")).toBeVisible();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue