Navigation UI: make bottom nav standard (#29357)
This commit is contained in:
parent
d37121efd9
commit
b0126db57a
33 changed files with 125 additions and 1234 deletions
|
|
@ -94,7 +94,7 @@
|
||||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||||
|
|
||||||
--tab-bar-height: 3rem;
|
--tab-bar-height: 3rem;
|
||||||
--bottom-space: 1.5px;
|
--bottom-space: calc(var(--tab-bar-height) + 1.5rem);
|
||||||
--tab-bar-background: var(--evcc-background);
|
--tab-bar-background: var(--evcc-background);
|
||||||
--evcc-backdrop-blur: blur(20px);
|
--evcc-backdrop-blur: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
@ -830,6 +830,6 @@ input::-webkit-date-and-time-value {
|
||||||
transition: opacity var(--evcc-transition-fast) ease 0.2s;
|
transition: opacity var(--evcc-transition-fast) ease 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app--bottomtabs .sticky-bottom {
|
.sticky-bottom {
|
||||||
bottom: calc(var(--tab-bar-height) + 0.25rem);
|
bottom: calc(var(--tab-bar-height) + 0.25rem);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
<template>
|
|
||||||
<GenericModal
|
|
||||||
id="batterySettingsModal"
|
|
||||||
:title="$t('batterySettings.modalTitle')"
|
|
||||||
size="xl"
|
|
||||||
data-testid="battery-settings-modal"
|
|
||||||
@open="modalVisible"
|
|
||||||
@closed="modalInvisible"
|
|
||||||
>
|
|
||||||
<ul v-if="gridChargePossible || batteryGridChargeLimit !== null" class="nav nav-tabs mb-4">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link"
|
|
||||||
:class="{ active: usageTabActive }"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="showUsageTab"
|
|
||||||
>
|
|
||||||
{{ $t("batterySettings.usageTab") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link"
|
|
||||||
:class="{ active: gridTabActive }"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="showGridTab"
|
|
||||||
>
|
|
||||||
{{ $t("batterySettings.gridChargeTab") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<BatteryUsageSettings
|
|
||||||
v-show="usageTabActive"
|
|
||||||
:buffer-soc="bufferSoc"
|
|
||||||
:priority-soc="prioritySoc"
|
|
||||||
:buffer-start-soc="bufferStartSoc"
|
|
||||||
:battery-discharge-control="batteryDischargeControl"
|
|
||||||
:battery="battery"
|
|
||||||
/>
|
|
||||||
<SmartCostLimit
|
|
||||||
v-if="isModalVisible"
|
|
||||||
v-show="gridTabActive"
|
|
||||||
:current-limit="batteryGridChargeLimit"
|
|
||||||
:last-limit="lastSmartCostLimit"
|
|
||||||
:smart-cost-type="smartCostType"
|
|
||||||
:currency="currency"
|
|
||||||
:tariff="gridChargeTariff"
|
|
||||||
:possible="gridChargePossible"
|
|
||||||
/>
|
|
||||||
</GenericModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import SmartCostLimit from "../Tariff/SmartCostLimit.vue";
|
|
||||||
import BatteryUsageSettings from "./BatteryUsageSettings.vue";
|
|
||||||
import GenericModal from "../Helper/GenericModal.vue";
|
|
||||||
import settings from "@/settings";
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
import type { Battery, CURRENCY, Forecast } from "@/types/evcc";
|
|
||||||
import { SMART_COST_TYPE } from "@/types/evcc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "BatterySettingsModal",
|
|
||||||
components: { SmartCostLimit, BatteryUsageSettings, GenericModal },
|
|
||||||
props: {
|
|
||||||
bufferSoc: { type: Number, default: 100 },
|
|
||||||
prioritySoc: { type: Number, default: 0 },
|
|
||||||
bufferStartSoc: { type: Number, default: 0 },
|
|
||||||
batteryDischargeControl: Boolean,
|
|
||||||
battery: { type: Object as PropType<Battery> },
|
|
||||||
batteryGridChargeLimit: { type: Number, default: null },
|
|
||||||
smartCostAvailable: Boolean,
|
|
||||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
|
||||||
tariffGrid: Number,
|
|
||||||
currency: String as PropType<CURRENCY>,
|
|
||||||
forecast: Object as PropType<Forecast>,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isModalVisible: false,
|
|
||||||
gridTabActive: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
usageTabActive() {
|
|
||||||
return !this.gridTabActive;
|
|
||||||
},
|
|
||||||
batteryDevices() {
|
|
||||||
return this.battery?.devices ?? [];
|
|
||||||
},
|
|
||||||
controllable() {
|
|
||||||
return this.batteryDevices.some(({ controllable }) => controllable);
|
|
||||||
},
|
|
||||||
gridChargePossible() {
|
|
||||||
return this.controllable && this.isModalVisible && this.smartCostAvailable;
|
|
||||||
},
|
|
||||||
gridChargeTariff() {
|
|
||||||
if (this.smartCostType === SMART_COST_TYPE.CO2) {
|
|
||||||
return this.forecast?.co2;
|
|
||||||
}
|
|
||||||
return this.forecast?.grid;
|
|
||||||
},
|
|
||||||
lastSmartCostLimit() {
|
|
||||||
return settings.lastBatterySmartCostLimit;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
batteryGridChargeLimit() {
|
|
||||||
this.verifyTabs();
|
|
||||||
},
|
|
||||||
gridChargePossible() {
|
|
||||||
this.verifyTabs();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
showGridTab() {
|
|
||||||
this.gridTabActive = true;
|
|
||||||
},
|
|
||||||
showUsageTab() {
|
|
||||||
this.gridTabActive = false;
|
|
||||||
},
|
|
||||||
verifyTabs() {
|
|
||||||
if (
|
|
||||||
this.gridTabActive &&
|
|
||||||
!this.gridChargePossible &&
|
|
||||||
this.batteryGridChargeLimit === null
|
|
||||||
) {
|
|
||||||
this.gridTabActive = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modalVisible() {
|
|
||||||
this.isModalVisible = true;
|
|
||||||
},
|
|
||||||
modalInvisible() {
|
|
||||||
this.isModalVisible = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
:powerUnit="powerUnit"
|
:powerUnit="powerUnit"
|
||||||
:expanded="pvExpanded"
|
:expanded="pvExpanded"
|
||||||
data-testid="energyflow-entry-production"
|
data-testid="energyflow-entry-production"
|
||||||
@details-clicked="openForecastModal"
|
@details-clicked="openForecastView"
|
||||||
@toggle="togglePv"
|
@toggle="togglePv"
|
||||||
>
|
>
|
||||||
<template v-if="pv.length > 1" #expanded>
|
<template v-if="pv.length > 1" #expanded>
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
:expanded="batteryExpanded"
|
:expanded="batteryExpanded"
|
||||||
detailsClickable
|
detailsClickable
|
||||||
data-testid="energyflow-entry-batterydischarge"
|
data-testid="energyflow-entry-batterydischarge"
|
||||||
@details-clicked="openBatterySettingsModal"
|
@details-clicked="openBatteryView"
|
||||||
@toggle="toggleBattery"
|
@toggle="toggleBattery"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
|
|
@ -264,7 +264,7 @@
|
||||||
:expanded="batteryExpanded"
|
:expanded="batteryExpanded"
|
||||||
detailsClickable
|
detailsClickable
|
||||||
data-testid="energyflow-entry-batterycharge"
|
data-testid="energyflow-entry-batterycharge"
|
||||||
@details-clicked="openBatterySettingsModal"
|
@details-clicked="openBatteryView"
|
||||||
@toggle="toggleBattery"
|
@toggle="toggleBattery"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
|
|
@ -287,7 +287,7 @@
|
||||||
v-if="batteryGridChargeLimitSet"
|
v-if="batteryGridChargeLimitSet"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-reset d-flex justify-content-between text-start pe-4"
|
class="btn-reset d-flex justify-content-between text-start pe-4"
|
||||||
@click.stop="openBatterySettingsModal"
|
@click.stop="openBatteryView"
|
||||||
>
|
>
|
||||||
<span v-if="batteryGridChargeActive">
|
<span v-if="batteryGridChargeActive">
|
||||||
{{ $t("main.energyflow.batteryGridChargeActive") }}
|
{{ $t("main.energyflow.batteryGridChargeActive") }}
|
||||||
|
|
@ -339,7 +339,6 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@h2d2/shopicons/es/filled/square";
|
import "@h2d2/shopicons/es/filled/square";
|
||||||
import Modal from "bootstrap/js/dist/modal";
|
|
||||||
import Visualization from "./Visualization.vue";
|
import Visualization from "./Visualization.vue";
|
||||||
import Entry from "./Entry.vue";
|
import Entry from "./Entry.vue";
|
||||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||||
|
|
@ -651,25 +650,11 @@ export default defineComponent({
|
||||||
updateHeight() {
|
updateHeight() {
|
||||||
this.detailsCompleteHeight = this.$refs["detailsInner"]?.offsetHeight ?? 0;
|
this.detailsCompleteHeight = this.$refs["detailsInner"]?.offsetHeight ?? 0;
|
||||||
},
|
},
|
||||||
openBatterySettingsModal() {
|
openBatteryView() {
|
||||||
if (this.experimental) {
|
this.$router.push("/battery");
|
||||||
this.$router.push("/battery");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("batterySettingsModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
},
|
||||||
openForecastModal() {
|
openForecastView() {
|
||||||
if (this.experimental) {
|
this.$router.push("/forecast");
|
||||||
this.$router.push("/forecast");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("forecastModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
},
|
||||||
dischargePower(power: number) {
|
dischargePower(power: number) {
|
||||||
return Math.abs(Math.max(0, power));
|
return Math.abs(Math.max(0, power));
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import Footer from "./Footer.vue";
|
|
||||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Footer/Footer",
|
|
||||||
component: Footer,
|
|
||||||
argTypes: {
|
|
||||||
version: { control: "object" },
|
|
||||||
savings: { control: "object" },
|
|
||||||
},
|
|
||||||
} as Meta<typeof Footer>;
|
|
||||||
|
|
||||||
const Template: StoryFn<typeof Footer> = (args) => ({
|
|
||||||
components: { Footer },
|
|
||||||
setup() {
|
|
||||||
return { args };
|
|
||||||
},
|
|
||||||
template: '<Footer v-bind="args" />',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
|
||||||
Default.args = {
|
|
||||||
version: {
|
|
||||||
installed: "0.401",
|
|
||||||
available: "0.401",
|
|
||||||
},
|
|
||||||
savings: {
|
|
||||||
statistics: {
|
|
||||||
"30d": { solarPercentage: 50 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateNightly = Template.bind({});
|
|
||||||
UpdateNightly.args = {
|
|
||||||
version: {
|
|
||||||
installed: "0.400",
|
|
||||||
available: "0.400",
|
|
||||||
commit: "5ce7be4",
|
|
||||||
},
|
|
||||||
savings: {
|
|
||||||
statistics: {
|
|
||||||
"30d": { solarPercentage: 22 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateAvailable = Template.bind({});
|
|
||||||
UpdateAvailable.args = {
|
|
||||||
version: {
|
|
||||||
installed: "0.400",
|
|
||||||
available: "0.500",
|
|
||||||
},
|
|
||||||
savings: {
|
|
||||||
statistics: {
|
|
||||||
"30d": { solarPercentage: 100 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<footer class="footer" data-testid="footer">
|
|
||||||
<div class="container py-2">
|
|
||||||
<div class="d-flex justify-content-between align-items-center gap-2">
|
|
||||||
<Version v-bind="version" />
|
|
||||||
<Savings v-bind="savings" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import "@h2d2/shopicons/es/filled/testtube";
|
|
||||||
|
|
||||||
import Version from "./Version.vue";
|
|
||||||
import Savings from "../Savings/Savings.vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Footer",
|
|
||||||
components: { Version, Savings },
|
|
||||||
props: {
|
|
||||||
version: Object,
|
|
||||||
savings: Object,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
visible: visible,
|
visible: visible,
|
||||||
'alert-danger': showError,
|
'alert-danger': showError,
|
||||||
'alert-secondary': !showError,
|
'alert-secondary': !showError,
|
||||||
'alert--bottomtabs': experimental && !blocking,
|
'alert--bottomtabs': !blocking,
|
||||||
}"
|
}"
|
||||||
role="alert"
|
role="alert"
|
||||||
data-testid="bottom-banner"
|
data-testid="bottom-banner"
|
||||||
|
|
@ -81,7 +81,6 @@ export default defineComponent({
|
||||||
offline: Boolean,
|
offline: Boolean,
|
||||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
||||||
startupCompleted: Boolean,
|
startupCompleted: Boolean,
|
||||||
experimental: Boolean,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { dismissed: false };
|
return { dismissed: false };
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import Version from "./Version.vue";
|
|
||||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Footer/Version",
|
|
||||||
component: Version,
|
|
||||||
parameters: {
|
|
||||||
layout: "centered",
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
installed: { control: "text" },
|
|
||||||
available: { control: "text" },
|
|
||||||
commit: { control: "text" },
|
|
||||||
},
|
|
||||||
} as Meta<typeof Version>;
|
|
||||||
|
|
||||||
const Template: StoryFn<typeof Version> = (args) => ({
|
|
||||||
components: { Version },
|
|
||||||
setup() {
|
|
||||||
return { args };
|
|
||||||
},
|
|
||||||
template: '<Version v-bind="args" />',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Latest = Template.bind({});
|
|
||||||
Latest.args = {
|
|
||||||
installed: "0.303.1",
|
|
||||||
available: "0.303.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Nightly = Template.bind({});
|
|
||||||
Nightly.args = {
|
|
||||||
installed: "0.303.1",
|
|
||||||
available: "0.303.1",
|
|
||||||
commit: "5ce7be4",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateAvailable = Template.bind({});
|
|
||||||
UpdateAvailable.args = {
|
|
||||||
installed: "0.303.0",
|
|
||||||
available: "0.303.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NoReleaseNotes = Template.bind({});
|
|
||||||
NoReleaseNotes.args = {
|
|
||||||
installed: "0.303.0",
|
|
||||||
available: "0.303.1",
|
|
||||||
};
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
<template>
|
|
||||||
<a
|
|
||||||
v-if="commit"
|
|
||||||
:href="githubHashUrl"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="btn btn-link px-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end text-truncate"
|
|
||||||
>
|
|
||||||
<Logo class="logo me-2 flex-shrink-0" />
|
|
||||||
<span class="text-decoration-underline text-truncate">v{{ installed }}</span>
|
|
||||||
<shopicon-regular-moonstars
|
|
||||||
class="ms-2 text-gray-light flex-shrink-0"
|
|
||||||
></shopicon-regular-moonstars>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
v-else-if="newVersionAvailable"
|
|
||||||
class="btn btn-link px-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end text-truncate"
|
|
||||||
@click="openModal"
|
|
||||||
>
|
|
||||||
<shopicon-regular-gift class="me-2"></shopicon-regular-gift>
|
|
||||||
<span class="text-decoration-underline text-truncate">v{{ installed }}</span>
|
|
||||||
<span class="ms-2 d-none d-sm-block text-gray-medium text-decoration-underline">
|
|
||||||
{{ $t("footer.version.availableLong") }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
v-else
|
|
||||||
:href="releaseNotesUrl(installed)"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="btn btn-link evcc-default-text px-0 text-decoration-none text-nowrap d-flex align-items-end text-truncate"
|
|
||||||
>
|
|
||||||
<Logo class="logo me-2 flex-shrink-0" />
|
|
||||||
<span class="text-decoration-underline text-truncate">v{{ installed }}</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Modal from "bootstrap/js/dist/modal";
|
|
||||||
import "@h2d2/shopicons/es/regular/gift";
|
|
||||||
import "@h2d2/shopicons/es/regular/moonstars";
|
|
||||||
import Logo from "./Logo.vue";
|
|
||||||
import { isNewVersionAvailable } from "@/utils/version";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Version",
|
|
||||||
components: { Logo },
|
|
||||||
props: {
|
|
||||||
installed: String,
|
|
||||||
available: String,
|
|
||||||
commit: String,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
githubHashUrl() {
|
|
||||||
return `https://github.com/evcc-io/evcc/commit/${this.commit}`;
|
|
||||||
},
|
|
||||||
newVersionAvailable() {
|
|
||||||
return isNewVersionAvailable(this.installed, this.available);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
releaseNotesUrl(version?: string) {
|
|
||||||
return version == "0.0.0"
|
|
||||||
? `https://github.com/evcc-io/evcc/releases`
|
|
||||||
: `https://github.com/evcc-io/evcc/releases/tag/${version}`;
|
|
||||||
},
|
|
||||||
openModal() {
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("aboutModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.icon {
|
|
||||||
color: var(--evcc-dark-green);
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
height: 1.1rem;
|
|
||||||
margin: 0.3rem 0 0.2rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
<template>
|
|
||||||
<GenericModal
|
|
||||||
id="forecastModal"
|
|
||||||
:title="$t('forecast.modalTitle')"
|
|
||||||
size="xl"
|
|
||||||
data-testid="forecast-modal"
|
|
||||||
@open="modalVisible"
|
|
||||||
@closed="modalInvisible"
|
|
||||||
>
|
|
||||||
<div v-if="isModalVisible">
|
|
||||||
<div class="d-flex justify-content-between align-items-center gap-2">
|
|
||||||
<ForecastTypeSelect v-model="selectedType" :forecast="forecast" />
|
|
||||||
<ForecastActiveSlot :activeSlot="selectedSlot" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ForecastChart
|
|
||||||
class="my-3"
|
|
||||||
:grid="forecast.grid"
|
|
||||||
:solar="solar"
|
|
||||||
:co2="forecast.co2"
|
|
||||||
:currency="currency"
|
|
||||||
:selected="selectedType"
|
|
||||||
@selected="updateSlot"
|
|
||||||
/>
|
|
||||||
<ForecastDetails
|
|
||||||
:type="selectedType"
|
|
||||||
:grid="forecast.grid"
|
|
||||||
:solar="solar"
|
|
||||||
:co2="forecast.co2"
|
|
||||||
:currency="currency"
|
|
||||||
/>
|
|
||||||
<div v-if="showSolarAdjust" class="form-check form-switch mt-4">
|
|
||||||
<input
|
|
||||||
id="solarForecastAdjust"
|
|
||||||
:checked="solarAdjusted"
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
@change="changeAdjusted"
|
|
||||||
/>
|
|
||||||
<div class="form-check-label">
|
|
||||||
<label for="solarForecastAdjust"
|
|
||||||
><small>🧪 {{ solarAdjustText }}</small></label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GenericModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import "@h2d2/shopicons/es/regular/eco1";
|
|
||||||
import "@h2d2/shopicons/es/regular/sun";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
import type { PropType } from "vue";
|
|
||||||
import GenericModal from "../Helper/GenericModal.vue";
|
|
||||||
import Chart from "./Chart.vue";
|
|
||||||
import TypeSelect from "./TypeSelect.vue";
|
|
||||||
import Details from "./Details.vue";
|
|
||||||
import ActiveSlot from "./ActiveSlot.vue";
|
|
||||||
|
|
||||||
import formatter from "@/mixins/formatter";
|
|
||||||
import settings from "@/settings";
|
|
||||||
import type { CURRENCY, Forecast } from "@/types/evcc";
|
|
||||||
import { ForecastType, adjustedSolar } from "@/utils/forecast";
|
|
||||||
import type { ForecastSlot, TimeseriesEntry } from "./types";
|
|
||||||
export default defineComponent({
|
|
||||||
name: "ForecastModal",
|
|
||||||
components: {
|
|
||||||
GenericModal,
|
|
||||||
ForecastChart: Chart,
|
|
||||||
ForecastTypeSelect: TypeSelect,
|
|
||||||
ForecastDetails: Details,
|
|
||||||
ForecastActiveSlot: ActiveSlot,
|
|
||||||
},
|
|
||||||
mixins: [formatter],
|
|
||||||
props: {
|
|
||||||
forecast: { type: Object as PropType<Forecast>, default: () => ({}) },
|
|
||||||
currency: { type: String as PropType<CURRENCY> },
|
|
||||||
experimental: Boolean,
|
|
||||||
},
|
|
||||||
data(): {
|
|
||||||
isModalVisible: boolean;
|
|
||||||
selectedType: ForecastType;
|
|
||||||
selectedSlot: ForecastSlot | TimeseriesEntry | null;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
isModalVisible: false,
|
|
||||||
selectedType: ForecastType.Solar,
|
|
||||||
selectedSlot: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
solarAdjusted() {
|
|
||||||
return settings.solarAdjusted;
|
|
||||||
},
|
|
||||||
showSolarAdjust() {
|
|
||||||
// TODO fix https://github.com/evcc-io/evcc/issues/29165
|
|
||||||
return !!this.forecast.solar && this.experimental && false;
|
|
||||||
},
|
|
||||||
solar() {
|
|
||||||
if (this.showSolarAdjust && this.solarAdjusted) {
|
|
||||||
return adjustedSolar(this.forecast.solar);
|
|
||||||
}
|
|
||||||
return this.forecast.solar ? { ...this.forecast.solar, scale: undefined } : undefined;
|
|
||||||
},
|
|
||||||
solarAdjustText() {
|
|
||||||
let percent = "";
|
|
||||||
|
|
||||||
const scale = this.forecast.solar?.scale;
|
|
||||||
if (scale) {
|
|
||||||
const percentDiff = scale * 100 - 100;
|
|
||||||
percent = ` (${this.fmtPercentage(percentDiff, 0, true)})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$t("forecast.solarAdjust", { percent });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isModalVisible(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
this.updateSelectedType();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.updateSelectedType();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
modalVisible() {
|
|
||||||
this.isModalVisible = true;
|
|
||||||
},
|
|
||||||
modalInvisible() {
|
|
||||||
this.isModalVisible = false;
|
|
||||||
},
|
|
||||||
updateSlot(slot: ForecastSlot | TimeseriesEntry | null) {
|
|
||||||
this.selectedSlot = slot;
|
|
||||||
},
|
|
||||||
updateSelectedType() {
|
|
||||||
const availableTypes = {
|
|
||||||
[ForecastType.Solar]: !!this.forecast.solar,
|
|
||||||
[ForecastType.Price]: !!this.forecast.grid,
|
|
||||||
[ForecastType.Co2]: !!this.forecast.co2,
|
|
||||||
};
|
|
||||||
|
|
||||||
// selection has data, do nothing
|
|
||||||
if (availableTypes[this.selectedType]) return;
|
|
||||||
// fallback to first available type
|
|
||||||
this.selectedType =
|
|
||||||
Object.values(ForecastType).find((type) => availableTypes[type]) ||
|
|
||||||
Object.values(ForecastType)[0]!;
|
|
||||||
},
|
|
||||||
changeAdjusted() {
|
|
||||||
settings.solarAdjusted = !settings.solarAdjusted;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.value {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.extraValue {
|
|
||||||
color: var(--evcc-gray);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.label {
|
|
||||||
color: var(--evcc-gray);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -71,14 +71,12 @@
|
||||||
:selectedId="selectedLoadpointId"
|
:selectedId="selectedLoadpointId"
|
||||||
@id-changed="selectedLoadpointChanged"
|
@id-changed="selectedLoadpointChanged"
|
||||||
/>
|
/>
|
||||||
<Footer v-if="!experimental" v-bind="footer" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@h2d2/shopicons/es/regular/arrowup";
|
import "@h2d2/shopicons/es/regular/arrowup";
|
||||||
import Footer from "../Footer/Footer.vue";
|
|
||||||
import TopNavigationArea from "../Top/TopNavigationArea.vue";
|
import TopNavigationArea from "../Top/TopNavigationArea.vue";
|
||||||
import Energyflow from "../Energyflow/Energyflow.vue";
|
import Energyflow from "../Energyflow/Energyflow.vue";
|
||||||
import HemsWarning from "../HemsWarning.vue";
|
import HemsWarning from "../HemsWarning.vue";
|
||||||
|
|
@ -96,7 +94,6 @@ import type {
|
||||||
Notification,
|
Notification,
|
||||||
Circuit,
|
Circuit,
|
||||||
SMART_COST_TYPE,
|
SMART_COST_TYPE,
|
||||||
Sponsor,
|
|
||||||
FatalError,
|
FatalError,
|
||||||
EvOpt,
|
EvOpt,
|
||||||
BATTERY_MODE,
|
BATTERY_MODE,
|
||||||
|
|
@ -107,7 +104,6 @@ import type { Grid } from "./types";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Site",
|
name: "Site",
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
Loadpoints,
|
Loadpoints,
|
||||||
Energyflow,
|
Energyflow,
|
||||||
HemsWarning,
|
HemsWarning,
|
||||||
|
|
@ -143,7 +139,6 @@ export default defineComponent({
|
||||||
vehicles: Object,
|
vehicles: Object,
|
||||||
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
|
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
|
||||||
currency: { type: String as PropType<CURRENCY> },
|
currency: { type: String as PropType<CURRENCY> },
|
||||||
statistics: Object,
|
|
||||||
tariffFeedIn: Number,
|
tariffFeedIn: Number,
|
||||||
tariffGrid: Number,
|
tariffGrid: Number,
|
||||||
tariffCo2: Number,
|
tariffCo2: Number,
|
||||||
|
|
@ -152,20 +147,12 @@ export default defineComponent({
|
||||||
tariffPriceLoadpoints: Number,
|
tariffPriceLoadpoints: Number,
|
||||||
tariffCo2Loadpoints: Number,
|
tariffCo2Loadpoints: Number,
|
||||||
|
|
||||||
availableVersion: String,
|
|
||||||
releaseNotes: String,
|
|
||||||
hasUpdater: Boolean,
|
|
||||||
uploadMessage: String,
|
|
||||||
uploadProgress: Number,
|
|
||||||
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
|
|
||||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||||
smartCostAvailable: Boolean,
|
smartCostAvailable: Boolean,
|
||||||
smartFeedInPriorityAvailable: Boolean,
|
smartFeedInPriorityAvailable: Boolean,
|
||||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
||||||
forecast: Object as PropType<Forecast>,
|
forecast: Object as PropType<Forecast>,
|
||||||
circuits: Object as PropType<Record<string, Circuit>>,
|
circuits: Object as PropType<Record<string, Circuit>>,
|
||||||
telemetry: Boolean,
|
|
||||||
experimental: Boolean,
|
|
||||||
evopt: { type: Object as PropType<EvOpt> },
|
evopt: { type: Object as PropType<EvOpt> },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -198,24 +185,6 @@ export default defineComponent({
|
||||||
// work in progess
|
// work in progess
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
footer() {
|
|
||||||
return {
|
|
||||||
version: {
|
|
||||||
installed: window.evcc.version,
|
|
||||||
commit: window.evcc.commit,
|
|
||||||
available: this.availableVersion,
|
|
||||||
},
|
|
||||||
savings: {
|
|
||||||
sponsor: this.sponsor,
|
|
||||||
statistics: this.statistics,
|
|
||||||
co2Configured: this.tariffCo2 !== undefined,
|
|
||||||
currency: this.currency,
|
|
||||||
telemetry: this.telemetry,
|
|
||||||
forecast: this.forecast,
|
|
||||||
tariffGrid: this.tariffGrid,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hasFatalError() {
|
hasFatalError() {
|
||||||
return this.fatal.length > 0;
|
return this.fatal.length > 0;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,6 @@
|
||||||
data-testid="header"
|
data-testid="header"
|
||||||
>
|
>
|
||||||
<h1 class="mb-1 pt-1 d-flex text-nowrap text-truncate">
|
<h1 class="mb-1 pt-1 d-flex text-nowrap text-truncate">
|
||||||
<template v-if="!experimental">
|
|
||||||
<router-link class="evcc-default-text" to="/" data-testid="home-link">
|
|
||||||
<shopicon-regular-home size="s" class="icon"></shopicon-regular-home>
|
|
||||||
</router-link>
|
|
||||||
<div v-if="showConfig" class="d-flex">
|
|
||||||
<div size="s" class="mx-2 flex-grow-0 flex-shrink-0 fw-normal">/</div>
|
|
||||||
<router-link to="/config" class="evcc-default-text text-decoration-none">
|
|
||||||
<shopicon-regular-settings
|
|
||||||
size="s"
|
|
||||||
class="icon d-block d-sm-none"
|
|
||||||
></shopicon-regular-settings>
|
|
||||||
<span class="d-none d-sm-block">{{ $t("config.main.title") }}</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div size="s" class="mx-2 flex-grow-0 flex-shrink-0 fw-normal">/</div>
|
|
||||||
</template>
|
|
||||||
<span class="text-truncate">{{ title }}</span>
|
<span class="text-truncate">{{ title }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<TopNavigationArea ref="navigationArea" :notifications="notifications" />
|
<TopNavigationArea ref="navigationArea" :notifications="notifications" />
|
||||||
|
|
@ -27,10 +11,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@h2d2/shopicons/es/regular/home";
|
|
||||||
import "@h2d2/shopicons/es/regular/settings";
|
|
||||||
import TopNavigationArea from "./TopNavigationArea.vue";
|
import TopNavigationArea from "./TopNavigationArea.vue";
|
||||||
import store from "@/store";
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import type { Notification } from "@/types/evcc";
|
import type { Notification } from "@/types/evcc";
|
||||||
|
|
||||||
|
|
@ -40,15 +21,9 @@ export default defineComponent({
|
||||||
TopNavigationArea,
|
TopNavigationArea,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
showConfig: Boolean,
|
|
||||||
title: String,
|
title: String,
|
||||||
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
experimental(): boolean {
|
|
||||||
return store.state.experimental ?? false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
requestAuthProvider(providerId: string) {
|
requestAuthProvider(providerId: string) {
|
||||||
const navigationArea = this.$refs["navigationArea"] as
|
const navigationArea = this.$refs["navigationArea"] as
|
||||||
|
|
@ -59,12 +34,3 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.icon {
|
|
||||||
height: 22px;
|
|
||||||
width: 22px;
|
|
||||||
position: relative;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import Navigation from "./Navigation.vue";
|
|
||||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Top/Navigation",
|
|
||||||
component: Navigation,
|
|
||||||
parameters: {
|
|
||||||
layout: "centered",
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
authProviders: { control: "object" },
|
|
||||||
sponsor: { control: "object" },
|
|
||||||
},
|
|
||||||
} as Meta<typeof Navigation>;
|
|
||||||
|
|
||||||
const Template: StoryFn<typeof Navigation> = (args) => ({
|
|
||||||
components: { Navigation },
|
|
||||||
setup() {
|
|
||||||
return { args };
|
|
||||||
},
|
|
||||||
template: '<Navigation v-bind="args" />',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Standard = Template.bind({});
|
|
||||||
Standard.args = {};
|
|
||||||
|
|
||||||
export const OAuthStatus = Template.bind({});
|
|
||||||
OAuthStatus.args = {
|
|
||||||
authProviders: {
|
|
||||||
"Mercedes EQS": {
|
|
||||||
authenticated: true,
|
|
||||||
id: "mercedes-eqs-9oqwjdf9oqwjd",
|
|
||||||
},
|
|
||||||
"Nissan Leaf Pro": {
|
|
||||||
authenticated: true,
|
|
||||||
id: "nissan-leaf-pro-9oqwjdf9oqwjd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PendingOAuthStatus = Template.bind({});
|
|
||||||
PendingOAuthStatus.args = {
|
|
||||||
authProviders: {
|
|
||||||
"Mercedes EQS": {
|
|
||||||
authenticated: true,
|
|
||||||
id: "mercedes-eqs-9oqwjdf9oqwjd",
|
|
||||||
},
|
|
||||||
"Nissan Leaf Pro": {
|
|
||||||
authenticated: false,
|
|
||||||
id: "nissan-leaf-pro-9oqwjdf9oqwjd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TokenExpires = Template.bind({});
|
|
||||||
TokenExpires.args = {
|
|
||||||
sponsor: {
|
|
||||||
status: {
|
|
||||||
name: "Sponsor",
|
|
||||||
expiresAt: new Date().toISOString(),
|
|
||||||
expiresSoon: true,
|
|
||||||
},
|
|
||||||
yamlSource: "file",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,302 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
id="topNavigatonDropdown"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
class="btn btn-sm btn-outline-secondary position-relative border-0 menu-button"
|
|
||||||
data-testid="topnavigation-button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="showRootBadge"
|
|
||||||
class="position-absolute top-0 start-100 translate-middle p-2 rounded-circle"
|
|
||||||
:class="badgeClass"
|
|
||||||
>
|
|
||||||
<span class="visually-hidden">action required</span>
|
|
||||||
</span>
|
|
||||||
<shopicon-regular-menu></shopicon-regular-menu>
|
|
||||||
</button>
|
|
||||||
<ul
|
|
||||||
class="dropdown-menu dropdown-menu-end"
|
|
||||||
aria-labelledby="topNavigatonDropdown"
|
|
||||||
data-testid="topnavigation-dropdown"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
class="dropdown-item"
|
|
||||||
to="/sessions"
|
|
||||||
active-class="active"
|
|
||||||
data-testid="topnavigation-sessions"
|
|
||||||
>
|
|
||||||
{{ $t("header.sessions") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
data-testid="topnavigation-settings"
|
|
||||||
@click="openSettingsModal"
|
|
||||||
>
|
|
||||||
{{ $t("settings.title") }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="batteryModalAvailable">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
data-testid="topnavigation-battery"
|
|
||||||
@click="openBatterySettingsModal"
|
|
||||||
>
|
|
||||||
{{ $t("batterySettings.modalTitle") }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="forecastAvailable">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
data-testid="topnavigation-forecast"
|
|
||||||
@click="openForecastModal"
|
|
||||||
>
|
|
||||||
{{ $t("forecast.modalTitle") }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
class="dropdown-item"
|
|
||||||
to="/config"
|
|
||||||
active-class="active"
|
|
||||||
data-testid="topnavigation-config"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="showConfigBadge"
|
|
||||||
class="d-inline-block p-1 rounded-circle bg-warning rounded-circle"
|
|
||||||
:class="badgeClass"
|
|
||||||
></span>
|
|
||||||
{{ $t("config.main.title") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link class="dropdown-item" to="/log" active-class="active">
|
|
||||||
{{ $t("log.title") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li v-if="optimizeAvailable">
|
|
||||||
<router-link class="dropdown-item" to="/optimize" active-class="active">
|
|
||||||
Optimize 🧪
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li v-if="experimental">
|
|
||||||
<router-link class="dropdown-item" to="/history" active-class="active">
|
|
||||||
History 🧪
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
<template v-if="authorizationRequired">
|
|
||||||
<li>
|
|
||||||
<h6 class="dropdown-header">{{ $t("authProviders.authorizationRequired") }}</h6>
|
|
||||||
</li>
|
|
||||||
<li v-for="provider in providers" :key="provider.id">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
@click="handleAuthRequired(provider.id)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="d-inline-block p-1 rounded-circle border border-light"
|
|
||||||
:class="'bg-warning'"
|
|
||||||
></span>
|
|
||||||
{{ provider.title }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
</template>
|
|
||||||
<li>
|
|
||||||
<button type="button" class="dropdown-item" @click="openHelpModal">
|
|
||||||
<span>{{ $t("header.needHelp") }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item d-flex" href="https://evcc.io/" target="_blank">
|
|
||||||
<span>evcc.io</span>
|
|
||||||
<shopicon-regular-newtab
|
|
||||||
size="s"
|
|
||||||
class="ms-2 external"
|
|
||||||
></shopicon-regular-newtab>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li v-if="isApp">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
data-testid="topnavigation-app"
|
|
||||||
@click="openNativeSettings"
|
|
||||||
>
|
|
||||||
{{ $t("header.nativeSettings") }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="showLogout">
|
|
||||||
<button type="button" class="dropdown-item" @click="logout">
|
|
||||||
{{ $t("header.logout") }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Modal from "bootstrap/js/dist/modal";
|
|
||||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
|
||||||
import "@h2d2/shopicons/es/regular/gift";
|
|
||||||
import "@h2d2/shopicons/es/regular/moonstars";
|
|
||||||
import "@h2d2/shopicons/es/regular/menu";
|
|
||||||
import "@h2d2/shopicons/es/regular/newtab";
|
|
||||||
import collector from "@/mixins/collector";
|
|
||||||
import { logout, isLoggedIn, openLoginModal } from "../Auth/auth";
|
|
||||||
import { isApp, sendToApp } from "@/utils/native";
|
|
||||||
import { isUserConfigError } from "@/utils/fatal";
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
import type { FatalError, Sponsor, EvOpt, AuthProviders, Battery } from "@/types/evcc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "TopNavigation",
|
|
||||||
mixins: [collector],
|
|
||||||
props: {
|
|
||||||
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
|
|
||||||
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
|
|
||||||
forecast: Object,
|
|
||||||
battery: { type: Object as PropType<Battery> },
|
|
||||||
evopt: { type: Object as PropType<EvOpt>, required: false },
|
|
||||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
|
||||||
experimental: Boolean,
|
|
||||||
authDisabled: Boolean,
|
|
||||||
},
|
|
||||||
emits: ["auth-required"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isApp: isApp(),
|
|
||||||
dropdown: null as Dropdown | null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
batteryConfigured() {
|
|
||||||
return (this.battery?.devices?.length ?? 0) > 0;
|
|
||||||
},
|
|
||||||
providers() {
|
|
||||||
return Object.entries(this.authProviders)
|
|
||||||
.filter(([, provider]) => !provider.authenticated)
|
|
||||||
.map(([title, { authenticated, id }]) => ({
|
|
||||||
title,
|
|
||||||
authenticated,
|
|
||||||
id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
authorizationRequired() {
|
|
||||||
return this.providers.length > 0;
|
|
||||||
},
|
|
||||||
sponsorExpires(): boolean {
|
|
||||||
return !!this.sponsor?.status?.expiresSoon;
|
|
||||||
},
|
|
||||||
showConfigBadge() {
|
|
||||||
return this.sponsorExpires || isUserConfigError(this.fatal);
|
|
||||||
},
|
|
||||||
showRootBadge() {
|
|
||||||
return this.authorizationRequired || this.showConfigBadge;
|
|
||||||
},
|
|
||||||
badgeClass() {
|
|
||||||
if (this.fatal.length > 0) {
|
|
||||||
return "bg-danger";
|
|
||||||
}
|
|
||||||
return "bg-warning";
|
|
||||||
},
|
|
||||||
batteryModalAvailable() {
|
|
||||||
return this.batteryConfigured;
|
|
||||||
},
|
|
||||||
forecastAvailable() {
|
|
||||||
const { grid, solar, co2 } = this.forecast || {};
|
|
||||||
return grid || solar || co2;
|
|
||||||
},
|
|
||||||
optimizeAvailable() {
|
|
||||||
return !!this.evopt && this.experimental;
|
|
||||||
},
|
|
||||||
showLogout() {
|
|
||||||
return !this.authDisabled && isLoggedIn();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const $el = document.getElementById("topNavigatonDropdown");
|
|
||||||
if (!$el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.dropdown = new Dropdown(
|
|
||||||
document.getElementById("topNavigatonDropdown") as HTMLElement
|
|
||||||
);
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.dropdown?.dispose();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleAuthRequired(providerId: string) {
|
|
||||||
this.$emit("auth-required", providerId);
|
|
||||||
},
|
|
||||||
openSettingsModal() {
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("globalSettingsModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
|
||||||
openHelpModal() {
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("helpModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
|
||||||
openBatterySettingsModal() {
|
|
||||||
if (this.experimental) {
|
|
||||||
this.$router.push("/battery");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("batterySettingsModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
|
||||||
openForecastModal() {
|
|
||||||
if (this.experimental) {
|
|
||||||
this.$router.push("/forecast");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modal = Modal.getOrCreateInstance(
|
|
||||||
document.getElementById("forecastModal") as HTMLElement
|
|
||||||
);
|
|
||||||
modal.show();
|
|
||||||
},
|
|
||||||
openNativeSettings() {
|
|
||||||
sendToApp({ type: "settings" });
|
|
||||||
},
|
|
||||||
async login() {
|
|
||||||
openLoginModal();
|
|
||||||
},
|
|
||||||
async logout() {
|
|
||||||
await logout();
|
|
||||||
this.$router.push({ path: "/" });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.menu-button {
|
|
||||||
margin-right: -0.7rem;
|
|
||||||
}
|
|
||||||
.external {
|
|
||||||
width: 18px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
.dropdown-menu {
|
|
||||||
/* above sticky, below modal https://getbootstrap.com/docs/5.3/layout/z-index/ */
|
|
||||||
z-index: 1045 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -3,10 +3,9 @@
|
||||||
<Notifications
|
<Notifications
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
:loadpoints="loadpoints"
|
:loadpoints="loadpoints"
|
||||||
:class="experimental ? 'd-flex align-items-center' : ''"
|
class="d-flex align-items-center"
|
||||||
/>
|
/>
|
||||||
<TopNavigation v-if="!experimental" v-bind="topNavigation" @auth-required="openAuthModal" />
|
<Savings v-bind="savings" />
|
||||||
<Savings v-if="experimental" v-bind="savings" />
|
|
||||||
<AuthProviderModal :provider="authProvider" />
|
<AuthProviderModal :provider="authProvider" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -14,24 +13,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import Modal from "bootstrap/js/dist/modal";
|
import Modal from "bootstrap/js/dist/modal";
|
||||||
import Navigation from "./Navigation.vue";
|
|
||||||
import Notifications from "./Notifications.vue";
|
import Notifications from "./Notifications.vue";
|
||||||
import AuthProviderModal from "./AuthProviderModal.vue";
|
import AuthProviderModal from "./AuthProviderModal.vue";
|
||||||
import Savings from "../Savings/Savings.vue";
|
import Savings from "../Savings/Savings.vue";
|
||||||
import type { Provider } from "./types";
|
import type { Provider } from "./types";
|
||||||
import type { Notification } from "@/types/evcc";
|
import type { Notification } from "@/types/evcc";
|
||||||
import collector from "@/mixins/collector";
|
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "TopNavigationArea",
|
name: "TopNavigationArea",
|
||||||
components: {
|
components: {
|
||||||
TopNavigation: Navigation,
|
|
||||||
Notifications,
|
Notifications,
|
||||||
AuthProviderModal,
|
AuthProviderModal,
|
||||||
Savings,
|
Savings,
|
||||||
},
|
},
|
||||||
mixins: [collector],
|
|
||||||
props: {
|
props: {
|
||||||
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
||||||
},
|
},
|
||||||
|
|
@ -42,12 +37,6 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
experimental(): boolean {
|
|
||||||
return store.state?.experimental || false;
|
|
||||||
},
|
|
||||||
topNavigation(): any {
|
|
||||||
return this.collectProps(Navigation, store.state);
|
|
||||||
},
|
|
||||||
savings() {
|
savings() {
|
||||||
return {
|
return {
|
||||||
sponsor: store.state.sponsor,
|
sponsor: store.state.sponsor,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import {
|
||||||
} from "./components/Auth/auth";
|
} from "./components/Auth/auth";
|
||||||
import { initConfigModal } from "./configModal";
|
import { initConfigModal } from "./configModal";
|
||||||
import { hapticFeedback } from "./utils/haptic";
|
import { hapticFeedback } from "./utils/haptic";
|
||||||
import store from "./store";
|
|
||||||
import type { VueI18nInstance } from "vue-i18n";
|
import type { VueI18nInstance } from "vue-i18n";
|
||||||
|
|
||||||
function hideAllModals() {
|
function hideAllModals() {
|
||||||
|
|
@ -158,9 +157,7 @@ export default function setupRouter(i18n: VueI18nInstance) {
|
||||||
// Only hide modals when the actual route path changes, not query parameters
|
// Only hide modals when the actual route path changes, not query parameters
|
||||||
if (to.path !== from.path) {
|
if (to.path !== from.path) {
|
||||||
hideAllModals();
|
hideAllModals();
|
||||||
if (store.state.experimental) {
|
hapticFeedback();
|
||||||
hapticFeedback();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
initConfigModal(router);
|
initConfigModal(router);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app" :class="{ 'app--bottomtabs': state.experimental }">
|
<div class="app">
|
||||||
<router-view
|
<router-view
|
||||||
v-if="showRoutes"
|
v-if="showRoutes"
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
:offline="offline"
|
:offline="offline"
|
||||||
></router-view>
|
></router-view>
|
||||||
|
|
||||||
<BottomTabBar v-if="state.experimental" v-bind="bottomTabBarProps" />
|
<BottomTabBar v-bind="bottomTabBarProps" />
|
||||||
|
|
||||||
<GlobalSettingsModal v-bind="globalSettingsProps" />
|
<GlobalSettingsModal v-bind="globalSettingsProps" />
|
||||||
<BatterySettingsModal v-if="batteryModalAvailabe" v-bind="batterySettingsProps" />
|
|
||||||
<ForecastModal v-bind="forecastModalProps" />
|
|
||||||
<AboutModal v-bind="aboutModalProps" />
|
<AboutModal v-bind="aboutModalProps" />
|
||||||
<HelpModal />
|
<HelpModal />
|
||||||
<PasswordModal />
|
<PasswordModal />
|
||||||
|
|
@ -23,8 +21,6 @@
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
import BottomTabBar from "../components/BottomTabs/Bar.vue";
|
import BottomTabBar from "../components/BottomTabs/Bar.vue";
|
||||||
import GlobalSettingsModal from "../components/GlobalSettings/GlobalSettingsModal.vue";
|
import GlobalSettingsModal from "../components/GlobalSettings/GlobalSettingsModal.vue";
|
||||||
import BatterySettingsModal from "../components/Battery/BatterySettingsModal.vue";
|
|
||||||
import ForecastModal from "../components/Forecast/ForecastModal.vue";
|
|
||||||
import OfflineIndicator from "../components/Footer/OfflineIndicator.vue";
|
import OfflineIndicator from "../components/Footer/OfflineIndicator.vue";
|
||||||
import PasswordModal from "../components/Auth/PasswordModal.vue";
|
import PasswordModal from "../components/Auth/PasswordModal.vue";
|
||||||
import LoginModal from "../components/Auth/LoginModal.vue";
|
import LoginModal from "../components/Auth/LoginModal.vue";
|
||||||
|
|
@ -50,8 +46,6 @@ export default defineComponent({
|
||||||
BottomTabBar,
|
BottomTabBar,
|
||||||
GlobalSettingsModal,
|
GlobalSettingsModal,
|
||||||
HelpModal,
|
HelpModal,
|
||||||
BatterySettingsModal,
|
|
||||||
ForecastModal,
|
|
||||||
PasswordModal,
|
PasswordModal,
|
||||||
LoginModal,
|
LoginModal,
|
||||||
OfflineIndicator,
|
OfflineIndicator,
|
||||||
|
|
@ -76,9 +70,6 @@ export default defineComponent({
|
||||||
version() {
|
version() {
|
||||||
return store.state.version;
|
return store.state.version;
|
||||||
},
|
},
|
||||||
batteryModalAvailabe() {
|
|
||||||
return store.state.battery?.devices?.length;
|
|
||||||
},
|
|
||||||
showRoutes() {
|
showRoutes() {
|
||||||
return this.state.startupCompleted;
|
return this.state.startupCompleted;
|
||||||
},
|
},
|
||||||
|
|
@ -89,15 +80,9 @@ export default defineComponent({
|
||||||
globalSettingsProps() {
|
globalSettingsProps() {
|
||||||
return this.collectProps(GlobalSettingsModal, this.state);
|
return this.collectProps(GlobalSettingsModal, this.state);
|
||||||
},
|
},
|
||||||
batterySettingsProps() {
|
|
||||||
return this.collectProps(BatterySettingsModal, this.state);
|
|
||||||
},
|
|
||||||
offlineIndicatorProps() {
|
offlineIndicatorProps() {
|
||||||
return this.collectProps(OfflineIndicator, this.state);
|
return this.collectProps(OfflineIndicator, this.state);
|
||||||
},
|
},
|
||||||
forecastModalProps() {
|
|
||||||
return this.collectProps(ForecastModal, this.state);
|
|
||||||
},
|
|
||||||
loginModalProps() {
|
loginModalProps() {
|
||||||
return this.collectProps(LoginModal, this.state);
|
return this.collectProps(LoginModal, this.state);
|
||||||
},
|
},
|
||||||
|
|
@ -247,7 +232,4 @@ export default defineComponent({
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
.app--bottomtabs {
|
|
||||||
--bottom-space: calc(var(--tab-bar-height) + 1.5rem);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@
|
||||||
<hr class="my-5" />
|
<hr class="my-5" />
|
||||||
|
|
||||||
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
|
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
|
||||||
<div class="round-box p-4 d-flex gap-4 flex-wrap">
|
<div data-testid="config-system" class="round-box p-4 d-flex gap-4 flex-wrap">
|
||||||
<router-link to="/log" class="btn btn-outline-secondary">
|
<router-link to="/log" class="btn btn-outline-secondary">
|
||||||
{{ $t("config.system.logs") }}
|
{{ $t("config.system.logs") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import {
|
import { expectModalHidden, expectModalVisible, openMoreMenu } from "./utils";
|
||||||
expectModalHidden,
|
|
||||||
expectModalVisible,
|
|
||||||
openTopNavigation,
|
|
||||||
expectTopNavigationClosed,
|
|
||||||
} from "./utils";
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
const BASIC = "basics.evcc.yaml";
|
const BASIC = "basics.evcc.yaml";
|
||||||
|
|
@ -49,9 +44,8 @@ test("login", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
// go to config
|
// go to config
|
||||||
await openTopNavigation(page);
|
await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await page.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
|
|
||||||
// login modal
|
// login modal
|
||||||
const login = page.getByTestId("login-modal");
|
const login = page.getByTestId("login-modal");
|
||||||
|
|
@ -77,9 +71,8 @@ test("http iframe hint", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
// go to config
|
// go to config
|
||||||
await openTopNavigation(page);
|
await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await page.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
|
|
||||||
// login modal
|
// login modal
|
||||||
const login = page.getByTestId("login-modal");
|
const login = page.getByTestId("login-modal");
|
||||||
|
|
@ -129,18 +122,16 @@ test("update password", async ({ page }) => {
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
// logout
|
// logout
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByRole("button", { name: "Logout" }).click();
|
await menu.getByRole("button", { name: "Logout" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
|
|
||||||
// should be redirected to home page after logout
|
// should be redirected to home page after logout
|
||||||
await expect(page).toHaveURL("/#/");
|
await expect(page).toHaveURL("/#/");
|
||||||
|
|
||||||
// login modal
|
// login modal
|
||||||
await openTopNavigation(page);
|
const menu2 = await openMoreMenu(page);
|
||||||
await expect(page.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
await expect(menu2.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await menu2.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
const loginNew = page.getByTestId("login-modal");
|
const loginNew = page.getByTestId("login-modal");
|
||||||
await expectModalVisible(loginNew);
|
await expectModalVisible(loginNew);
|
||||||
await loginNew.getByLabel("Administrator Password").fill(newPassword);
|
await loginNew.getByLabel("Administrator Password").fill(newPassword);
|
||||||
|
|
@ -171,10 +162,9 @@ test("disable auth", async ({ page }) => {
|
||||||
await expectModalHidden(modal);
|
await expectModalHidden(modal);
|
||||||
|
|
||||||
// configuration page without login
|
// configuration page without login
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await expect(page.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
await expect(menu.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await menu.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||||
|
|
||||||
await stop();
|
await stop();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl, restart } from "./evcc";
|
import { start, stop, baseUrl, restart } from "./evcc";
|
||||||
import { openTopNavigation, expectModalVisible, expectModalHidden } from "./utils";
|
import { openMoreMenu, expectModalVisible, expectModalHidden } from "./utils";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ test.describe("reset", async () => {
|
||||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||||
|
|
||||||
// open backup & restore modal
|
// open backup & restore modal
|
||||||
await openTopNavigation(page);
|
await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await page.getByRole("link", { name: "Configuration" }).click();
|
||||||
await page.getByRole("button", { name: "Backup & Restore" }).click();
|
await page.getByRole("button", { name: "Backup & Restore" }).click();
|
||||||
const modal = page.getByTestId("backup-restore-modal");
|
const modal = page.getByTestId("backup-restore-modal");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { expectModalVisible, expectModalHidden, openTopNavigation } from "./utils";
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
|
|
@ -12,28 +11,24 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test.describe("battery settings co2", async () => {
|
test.describe("battery settings co2", async () => {
|
||||||
test("grid charging", async ({ page }) => {
|
test("grid charging", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/#/battery");
|
||||||
await openTopNavigation(page);
|
|
||||||
await page.getByTestId("topnavigation-battery").click();
|
|
||||||
const modal = page.getByTestId("battery-settings-modal");
|
|
||||||
await expectModalVisible(modal);
|
|
||||||
|
|
||||||
await modal.getByRole("link", { name: "Grid charging" }).click();
|
await page.getByLabel("Enable limit").check();
|
||||||
await modal.getByLabel("Enable limit").check();
|
await page.getByLabel("CO₂ limit").selectOption({ label: "≤ 150 g/kWh" });
|
||||||
await modal.getByLabel("CO₂ limit").selectOption({ label: "≤ 150 g/kWh" });
|
|
||||||
// CO2 demo tariff provides 72 hours, so active hours vary by test execution time
|
// CO2 demo tariff provides 72 hours, so active hours vary by test execution time
|
||||||
await expect(modal.getByTestId("active-hours").locator(".value")).toHaveText(/^\d+ hr/);
|
await expect(page.getByTestId("active-hours").locator(".value")).toHaveText(/^\d+ hr/);
|
||||||
await expect(modal).toContainText("20 g – 150 g");
|
await expect(page.locator("body")).toContainText("20 g – 150 g");
|
||||||
await page.getByRole("button", { name: "Close" }).click();
|
|
||||||
await expectModalHidden(modal);
|
// navigate back to main via bottom nav, open via energyflow deep-link
|
||||||
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
await page.getByTestId("energyflow").click();
|
await page.getByTestId("energyflow").click();
|
||||||
await page.getByRole("button", { name: "Grid charging: active (≤ 150 g)" }).click();
|
await page.getByRole("button", { name: "Grid charging: active (≤ 150 g)" }).click();
|
||||||
await expectModalVisible(modal);
|
await expect(page).toHaveURL(/#\/battery/);
|
||||||
await modal.getByLabel("CO₂ limit").selectOption({ label: "≤ 10 g/kWh" });
|
|
||||||
await expect(modal.getByTestId("active-hours")).toHaveText("Active time");
|
await page.getByLabel("CO₂ limit").selectOption({ label: "≤ 10 g/kWh" });
|
||||||
await modal.getByRole("button", { name: "Close" }).click();
|
await expect(page.getByTestId("active-hours")).toHaveText("Active time");
|
||||||
await expectModalHidden(modal);
|
|
||||||
await expect(modal).not.toBeVisible();
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
await expect(page.getByRole("button", { name: "Grid charging: when ≤ 10 g" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Grid charging: when ≤ 10 g" })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { expectModalVisible, expectModalHidden, openTopNavigation } from "./utils";
|
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
@ -13,56 +12,44 @@ test.afterEach(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("battery settings", async () => {
|
test.describe("battery settings", async () => {
|
||||||
test("open modal", async ({ page }) => {
|
test("battery view", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/#/battery");
|
||||||
await openTopNavigation(page);
|
|
||||||
await page.getByTestId("topnavigation-battery").click();
|
|
||||||
|
|
||||||
const modal = page.getByTestId("battery-settings-modal");
|
await expect(page.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
||||||
await expectModalVisible(modal);
|
await expect(page.getByRole("heading", { name: "Grid charging" })).toBeVisible();
|
||||||
await expect(modal.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
await expect(page.getByTestId("header")).toContainText("Home Battery");
|
||||||
await expect(modal.getByRole("link", { name: "Grid charging" })).toBeVisible();
|
await expect(page.locator("body")).toContainText("Battery level: 50%");
|
||||||
await expect(modal).toContainText("Battery level: 50%");
|
await expect(page.locator("body")).toContainText("10.0 kWh of 20.0 kWh");
|
||||||
await expect(modal).toContainText("10.0 kWh of 20.0 kWh");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("battery usage", async ({ page }) => {
|
test("battery usage", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/#/battery");
|
||||||
await openTopNavigation(page);
|
|
||||||
await page.getByTestId("topnavigation-battery").click();
|
|
||||||
|
|
||||||
const modal = page.getByTestId("battery-settings-modal");
|
await page.locator("#batterySettingsPriority").selectOption({ label: "50%" });
|
||||||
await expectModalVisible(modal);
|
await expect(page.locator("label[for=batterySettingsPriorityMiddle] span")).toHaveText("50%");
|
||||||
await modal.locator("#batterySettingsPriority").selectOption({ label: "50%" });
|
await expect(page.locator("label[for=batterySettingsPriorityBottom] span")).toHaveText("50%");
|
||||||
await expect(modal.locator("label[for=batterySettingsPriorityMiddle] span")).toHaveText("50%");
|
await page.locator("#batterySettingsBufferTop").selectOption({ label: "80%" });
|
||||||
await expect(modal.locator("label[for=batterySettingsPriorityBottom] span")).toHaveText("50%");
|
await page.locator("#batterySettingsBufferStart").selectOption({ label: "when above 90%." });
|
||||||
await modal.locator("#batterySettingsBufferTop").selectOption({ label: "80%" });
|
await expect(page.locator("label[for=batterySettingsBuffer] span")).toHaveText("80%");
|
||||||
await modal.locator("#batterySettingsBufferStart").selectOption({ label: "when above 90%." });
|
|
||||||
await expect(modal.locator("label[for=batterySettingsBuffer] span")).toHaveText("80%");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("grid charging", async ({ page }) => {
|
test("grid charging", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/#/battery");
|
||||||
await openTopNavigation(page);
|
|
||||||
await page.getByTestId("topnavigation-battery").click();
|
|
||||||
const modal = page.getByTestId("battery-settings-modal");
|
|
||||||
await expectModalVisible(modal);
|
|
||||||
|
|
||||||
await modal.getByRole("link", { name: "Grid charging" }).click();
|
await page.getByLabel("Enable limit").check();
|
||||||
await modal.getByLabel("Enable limit").check();
|
await page.getByLabel("Price limit").selectOption({ label: "≤ 50.0 ct/kWh" });
|
||||||
await modal.getByLabel("Price limit").selectOption({ label: "≤ 50.0 ct/kWh" });
|
await expect(page.getByTestId("active-hours")).toHaveText(["Active time", "96 hr"].join(""));
|
||||||
await expect(modal.getByTestId("active-hours")).toHaveText(["Active time", "96 hr"].join(""));
|
await expect(page.locator("body")).toContainText("5.0 ct – 50.0 ct");
|
||||||
await expect(modal).toContainText("5.0 ct – 50.0 ct");
|
|
||||||
await page.getByRole("button", { name: "Close" }).click();
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
await expectModalHidden(modal);
|
|
||||||
await page.getByTestId("energyflow").click();
|
await page.getByTestId("energyflow").click();
|
||||||
await page.getByRole("button", { name: "Grid charging: active (≤ 50.0 ct)" }).click();
|
await page.getByRole("button", { name: "Grid charging: active (≤ 50.0 ct)" }).click();
|
||||||
await expectModalVisible(modal);
|
await expect(page).toHaveURL(/#\/battery/);
|
||||||
await modal.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
|
|
||||||
await expect(modal.getByTestId("active-hours")).toHaveText("Active time");
|
await page.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
|
||||||
await modal.getByRole("button", { name: "Close" }).click();
|
await expect(page.getByTestId("active-hours")).toHaveText("Active time");
|
||||||
await expectModalHidden(modal);
|
|
||||||
await expect(modal).not.toBeVisible();
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("button", { name: "Grid charging: when ≤ -10.0 ct" })
|
page.getByRole("button", { name: "Grid charging: when ≤ -10.0 ct" })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
@ -79,14 +66,12 @@ test.describe("battery settings", async () => {
|
||||||
await expect(charge).toContainText("Battery charging");
|
await expect(charge).toContainText("Battery charging");
|
||||||
|
|
||||||
// enable discharge lock
|
// enable discharge lock
|
||||||
await openTopNavigation(page);
|
await page.goto("/#/battery");
|
||||||
await page.getByTestId("topnavigation-battery").click();
|
await page.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||||
const modal = page.getByTestId("battery-settings-modal");
|
await page.waitForLoadState("networkidle");
|
||||||
await expectModalVisible(modal);
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
await modal.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
|
||||||
await page.getByRole("button", { name: "Close" }).click();
|
|
||||||
await expectModalHidden(modal);
|
|
||||||
|
|
||||||
|
await page.getByTestId("energyflow").click();
|
||||||
await expect(discharge).toContainText("Battery (locked)");
|
await expect(discharge).toContainText("Battery (locked)");
|
||||||
await expect(charge).toContainText("Battery charging");
|
await expect(charge).toContainText("Battery charging");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
newLoadpoint,
|
newLoadpoint,
|
||||||
addDemoCharger,
|
addDemoCharger,
|
||||||
ChargerStatus,
|
ChargerStatus,
|
||||||
openTopNavigation,
|
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
|
@ -93,13 +92,10 @@ test.describe("boost", async () => {
|
||||||
await expectModalHidden(modal);
|
await expectModalHidden(modal);
|
||||||
|
|
||||||
// enable "Prevent discharge in fast mode"
|
// enable "Prevent discharge in fast mode"
|
||||||
await openTopNavigation(page);
|
await page.goto("/#/battery");
|
||||||
await page.getByTestId("topnavigation-battery").click();
|
await page.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||||
const batteryModal = page.getByTestId("battery-settings-modal");
|
await page.waitForLoadState("networkidle");
|
||||||
await expectModalVisible(batteryModal);
|
await page.goto("/");
|
||||||
await batteryModal.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
|
||||||
await page.getByRole("button", { name: "Close" }).click();
|
|
||||||
await expectModalHidden(batteryModal);
|
|
||||||
|
|
||||||
// LP2: switch to fast mode → triggers global battery hold
|
// LP2: switch to fast mode → triggers global battery hold
|
||||||
const lp2 = page.getByTestId("loadpoint").nth(1);
|
const lp2 = page.getByTestId("loadpoint").nth(1);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ test.describe("meter only", async () => {
|
||||||
await expect(page.getByTestId("welcome-banner")).not.toBeVisible();
|
await expect(page.getByTestId("welcome-banner")).not.toBeVisible();
|
||||||
await expect(page.getByTestId("grid").getByTestId("device-tag-power")).toContainText("1.0 kW");
|
await expect(page.getByTestId("grid").getByTestId("device-tag-power")).toContainText("1.0 kW");
|
||||||
|
|
||||||
await page.getByTestId("home-link").click();
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
|
|
||||||
await expect(page.locator("body")).not.toContainText("Hello aboard!");
|
await expect(page.locator("body")).not.toContainText("Hello aboard!");
|
||||||
await expect(page.getByTestId("energyflow")).toBeVisible();
|
await expect(page.getByTestId("energyflow")).toBeVisible();
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ test.describe("onboarding", async () => {
|
||||||
await expect(restartButton).not.toBeVisible();
|
await expect(restartButton).not.toBeVisible();
|
||||||
|
|
||||||
// navigate to main screen
|
// navigate to main screen
|
||||||
await page.getByTestId("home-link").click();
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
|
|
||||||
// verify configuration
|
// verify configuration
|
||||||
await page.getByTestId("visualization").click();
|
await page.getByTestId("visualization").click();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, restart, baseUrl } from "./evcc";
|
import { start, stop, restart, baseUrl } from "./evcc";
|
||||||
import {
|
import { expectModalHidden, expectModalVisible, openMoreMenu } from "./utils";
|
||||||
expectModalHidden,
|
|
||||||
expectModalVisible,
|
|
||||||
openTopNavigation,
|
|
||||||
expectTopNavigationClosed,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||||
const NETWORK_HOST = "somehostname.local";
|
const NETWORK_HOST = "somehostname.local";
|
||||||
|
|
@ -23,9 +18,8 @@ test.afterAll(async () => {
|
||||||
test.describe("basics", async () => {
|
test.describe("basics", async () => {
|
||||||
test("navigation to config", async ({ page }) => {
|
test("navigation to config", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await openTopNavigation(page);
|
await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await page.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -59,7 +53,7 @@ test.describe("general", async () => {
|
||||||
await expect(page.getByTestId("generalconfig-title")).toContainText("Ahoy World");
|
await expect(page.getByTestId("generalconfig-title")).toContainText("Ahoy World");
|
||||||
|
|
||||||
// check changed value on main ui
|
// check changed value on main ui
|
||||||
await page.getByTestId("home-link").click();
|
await page.getByRole("link", { name: "Charge" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Ahoy World" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Ahoy World" })).toBeVisible();
|
||||||
});
|
});
|
||||||
test("enable experimental", async ({ page }) => {
|
test("enable experimental", async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { expectModalHidden, openTopNavigation, expectTopNavigationClosed } from "./utils";
|
import { expectModalHidden, openMoreMenu } from "./utils";
|
||||||
import { ChildProcess } from "child_process";
|
import { ChildProcess } from "child_process";
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
@ -35,9 +35,8 @@ test.describe("demo mode", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("auth is locked", async ({ page }) => {
|
test("auth is locked", async ({ page }) => {
|
||||||
await openTopNavigation(page);
|
await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await page.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
const loginModal = page.getByTestId("login-modal");
|
const loginModal = page.getByTestId("login-modal");
|
||||||
await expect(loginModal).toBeVisible();
|
await expect(loginModal).toBeVisible();
|
||||||
await expect(loginModal).toContainText("Login is not supported in demo mode.");
|
await expect(loginModal).toContainText("Login is not supported in demo mode.");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { test, expect, type Page, type Locator } from "@playwright/test";
|
import { test, expect, type Page, type Locator } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { expectModalVisible, expectModalHidden, openTopNavigation, dragElement } from "./utils";
|
import { expectModalVisible, expectModalHidden, openMoreMenu, dragElement } from "./utils";
|
||||||
|
|
||||||
const CONFIG_LOADPOINT_SORT = "loadpoint-sort.evcc.yaml";
|
const CONFIG_LOADPOINT_SORT = "loadpoint-sort.evcc.yaml";
|
||||||
|
|
||||||
|
|
@ -20,8 +20,8 @@ test.beforeEach(async ({ page }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openModal(page: Page) {
|
async function openModal(page: Page) {
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByTestId("topnavigation-settings").click();
|
await menu.getByRole("button", { name: "User Interface" }).click();
|
||||||
const modal = page.getByTestId("global-settings-modal");
|
const modal = page.getByTestId("global-settings-modal");
|
||||||
await expectModalVisible(modal);
|
await expectModalVisible(modal);
|
||||||
return modal;
|
return modal;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { openTopNavigation, expectTopNavigationClosed } from "./utils";
|
import { openMoreMenu } from "./utils";
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
|
@ -14,17 +14,16 @@ test.afterAll(async () => {
|
||||||
test.describe("opening logs", async () => {
|
test.describe("opening logs", async () => {
|
||||||
test("via config", async ({ page }) => {
|
test("via config", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Configuration" }).click();
|
await menu.getByRole("link", { name: "Configuration" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||||
await page.getByRole("link", { name: "Logs" }).click();
|
await page.getByTestId("config-system").getByRole("link", { name: "Logs" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||||
});
|
});
|
||||||
test("via top navigation", async ({ page }) => {
|
test("via more menu", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByRole("link", { name: "Logs" }).click();
|
await menu.getByRole("link", { name: "Logs" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||||
});
|
});
|
||||||
test("via notifications", async ({ page }) => {
|
test("via notifications", async ({ page }) => {
|
||||||
|
|
@ -36,9 +35,8 @@ test.describe("opening logs", async () => {
|
||||||
});
|
});
|
||||||
test("via need help", async ({ page }) => {
|
test("via need help", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByRole("button", { name: "Need Help?" }).click();
|
await menu.getByRole("button", { name: "Need Help?" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
await page.getByRole("link", { name: "View logs" }).click();
|
await page.getByRole("link", { name: "View logs" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { startSimulator, stopSimulator, simulatorConfig } from "./simulator";
|
import { startSimulator, stopSimulator, simulatorConfig } from "./simulator";
|
||||||
import { openTopNavigation, expectTopNavigationClosed, closeTopNavigation } from "./utils";
|
import { openMoreMenu } from "./utils";
|
||||||
|
|
||||||
const BASICS_CONFIG = "basics.evcc.yaml";
|
const BASICS_CONFIG = "basics.evcc.yaml";
|
||||||
|
|
||||||
|
|
@ -29,10 +29,11 @@ test.describe("Basics", async () => {
|
||||||
await page.goto(route.path);
|
await page.goto(route.path);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
|
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
|
||||||
await openTopNavigation(page);
|
// no battery tab when battery is not configured
|
||||||
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
|
await expect(page.getByRole("link", { name: "Battery" })).not.toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Home Battery" })).not.toBeVisible();
|
const menu = await openMoreMenu(page);
|
||||||
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
|
await expect(menu.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||||
|
await expect(menu.getByRole("button", { name: "Need Help?" })).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -40,9 +41,8 @@ test.describe("Basics", async () => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByRole("button", { name: "Need help?" }).click();
|
await menu.getByRole("button", { name: "Need Help?" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Need help?" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Need help?" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
@ -50,9 +50,8 @@ test.describe("Basics", async () => {
|
||||||
test("User Interface", async ({ page }) => {
|
test("User Interface", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
|
||||||
await openTopNavigation(page);
|
const menu = await openMoreMenu(page);
|
||||||
await page.getByRole("button", { name: "User Interface" }).click();
|
await menu.getByRole("button", { name: "User Interface" }).click();
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "User Interface" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "User Interface" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
@ -76,20 +75,18 @@ test.describe("Advanced", async () => {
|
||||||
await page.goto(route.path);
|
await page.goto(route.path);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
|
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
|
||||||
await openTopNavigation(page);
|
// battery tab visible when battery is configured
|
||||||
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
|
await expect(page.getByRole("link", { name: "Battery" })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Home Battery" })).toBeVisible();
|
const menu = await openMoreMenu(page);
|
||||||
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
|
await expect(menu.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||||
await closeTopNavigation(page);
|
await expect(menu.getByRole("button", { name: "Need Help?" })).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Home Battery from top navigation", async ({ page }) => {
|
test("Home Battery from bottom nav", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page.getByRole("heading", { name: title })).toBeVisible();
|
await expect(page.getByRole("heading", { name: title })).toBeVisible();
|
||||||
await openTopNavigation(page);
|
await page.getByRole("link", { name: "Battery" }).click();
|
||||||
await page.getByRole("button", { name: "Home Battery" }).click();
|
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, restart, baseUrl } from "./evcc";
|
import { start, stop, restart, baseUrl } from "./evcc";
|
||||||
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
|
import { expectModalHidden, expectModalVisible } from "./utils";
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
|
@ -15,13 +15,6 @@ test.describe("bottom navigation", async () => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
const bottomNav = page.getByTestId("bottom-tab-bar");
|
const bottomNav = page.getByTestId("bottom-tab-bar");
|
||||||
|
|
||||||
// bottom nav not visible without experimental
|
|
||||||
await expect(bottomNav).not.toBeVisible();
|
|
||||||
|
|
||||||
// enable experimental feature
|
|
||||||
await enableExperimental(page);
|
|
||||||
await page.goto("/");
|
|
||||||
await expect(bottomNav).toBeVisible();
|
await expect(bottomNav).toBeVisible();
|
||||||
|
|
||||||
const tabCharge = bottomNav.getByRole("link", { name: "Charge" });
|
const tabCharge = bottomNav.getByRole("link", { name: "Charge" });
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { test, expect, devices, type Page } from "@playwright/test";
|
import { test, expect, devices, type Page } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import {
|
import { expectModalVisible, expectModalHidden } from "./utils";
|
||||||
expectModalVisible,
|
|
||||||
expectModalHidden,
|
|
||||||
openTopNavigation,
|
|
||||||
expectTopNavigationClosed,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
|
@ -38,12 +33,7 @@ async function selectVehicleFilter(page: Page, value: string) {
|
||||||
test.describe("basics", async () => {
|
test.describe("basics", async () => {
|
||||||
test("navigation to sessions", async ({ page }) => {
|
test("navigation to sessions", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await openTopNavigation(page);
|
await page.getByRole("link", { name: "Sessions" }).click();
|
||||||
await page
|
|
||||||
.getByTestId("topnavigation-dropdown")
|
|
||||||
.getByRole("link", { name: "Charging sessions" })
|
|
||||||
.click();
|
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||||
});
|
});
|
||||||
test("month without data", async ({ page }) => {
|
test("month without data", async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { start, stop, baseUrl } from "./evcc";
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
import { enableExperimental } from "./utils";
|
|
||||||
|
|
||||||
test.use({ baseURL: baseUrl() });
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
|
@ -108,7 +107,6 @@ test.describe("statistics values", async () => {
|
||||||
|
|
||||||
test.describe("header savings", async () => {
|
test.describe("header savings", async () => {
|
||||||
test("savings info in header and indicator persistence", async ({ page }) => {
|
test("savings info in header and indicator persistence", async ({ page }) => {
|
||||||
await enableExperimental(page);
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
// savings button visible in header
|
// savings button visible in header
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,10 @@
|
||||||
import { expect, type Page, type Locator } from "@playwright/test";
|
import { expect, type Page, type Locator } from "@playwright/test";
|
||||||
|
|
||||||
export async function enableExperimental(page: Page): Promise<void> {
|
export async function openMoreMenu(page: Page): Promise<Locator> {
|
||||||
await page.goto("/#/config");
|
const moreButton = page.getByTestId("tab-more");
|
||||||
|
await expect(moreButton).toBeVisible();
|
||||||
await page
|
await moreButton.click();
|
||||||
.getByTestId("generalconfig-experimental")
|
return moreButton;
|
||||||
.getByRole("button", { name: "edit" })
|
|
||||||
.click();
|
|
||||||
const modal = page.getByTestId("experimental-modal");
|
|
||||||
await expectModalVisible(modal);
|
|
||||||
await modal.getByLabel("Enable experimental features.").click();
|
|
||||||
await modal.getByRole("button", { name: "Close" }).click();
|
|
||||||
await expectModalHidden(modal);
|
|
||||||
await expect(page.locator(".modal-backdrop")).not.toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openTopNavigation(page: Page): Promise<void> {
|
|
||||||
await expect(page.getByTestId("topnavigation-button")).toBeVisible();
|
|
||||||
await page.getByTestId("topnavigation-button").click();
|
|
||||||
await expectTopNavigationOpened(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function closeTopNavigation(page: Page): Promise<void> {
|
|
||||||
await expectTopNavigationOpened(page);
|
|
||||||
await page.getByTestId("topnavigation-button").click();
|
|
||||||
await expectTopNavigationClosed(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectTopNavigationOpened(page: Page): Promise<void> {
|
|
||||||
await expect(page.getByTestId("topnavigation-dropdown")).toBeVisible();
|
|
||||||
await expect(page.getByTestId("topnavigation-button")).toHaveAttribute("aria-expanded", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectTopNavigationClosed(page: Page): Promise<void> {
|
|
||||||
await expect(page.getByTestId("topnavigation-button")).toHaveAttribute("aria-expanded", "false");
|
|
||||||
await expect(page.getByTestId("topnavigation-dropdown")).not.toBeVisible();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function expectModalVisible(modal: Locator): Promise<void> {
|
export async function expectModalVisible(modal: Locator): Promise<void> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue