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);
|
||||
|
||||
--tab-bar-height: 3rem;
|
||||
--bottom-space: 1.5px;
|
||||
--bottom-space: calc(var(--tab-bar-height) + 1.5rem);
|
||||
--tab-bar-background: var(--evcc-background);
|
||||
--evcc-backdrop-blur: blur(20px);
|
||||
}
|
||||
|
|
@ -830,6 +830,6 @@ input::-webkit-date-and-time-value {
|
|||
transition: opacity var(--evcc-transition-fast) ease 0.2s;
|
||||
}
|
||||
|
||||
.app--bottomtabs .sticky-bottom {
|
||||
.sticky-bottom {
|
||||
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"
|
||||
:expanded="pvExpanded"
|
||||
data-testid="energyflow-entry-production"
|
||||
@details-clicked="openForecastModal"
|
||||
@details-clicked="openForecastView"
|
||||
@toggle="togglePv"
|
||||
>
|
||||
<template v-if="pv.length > 1" #expanded>
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
:expanded="batteryExpanded"
|
||||
detailsClickable
|
||||
data-testid="energyflow-entry-batterydischarge"
|
||||
@details-clicked="openBatterySettingsModal"
|
||||
@details-clicked="openBatteryView"
|
||||
@toggle="toggleBattery"
|
||||
>
|
||||
<template
|
||||
|
|
@ -264,7 +264,7 @@
|
|||
:expanded="batteryExpanded"
|
||||
detailsClickable
|
||||
data-testid="energyflow-entry-batterycharge"
|
||||
@details-clicked="openBatterySettingsModal"
|
||||
@details-clicked="openBatteryView"
|
||||
@toggle="toggleBattery"
|
||||
>
|
||||
<template
|
||||
|
|
@ -287,7 +287,7 @@
|
|||
v-if="batteryGridChargeLimitSet"
|
||||
type="button"
|
||||
class="btn-reset d-flex justify-content-between text-start pe-4"
|
||||
@click.stop="openBatterySettingsModal"
|
||||
@click.stop="openBatteryView"
|
||||
>
|
||||
<span v-if="batteryGridChargeActive">
|
||||
{{ $t("main.energyflow.batteryGridChargeActive") }}
|
||||
|
|
@ -339,7 +339,6 @@
|
|||
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/filled/square";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import Visualization from "./Visualization.vue";
|
||||
import Entry from "./Entry.vue";
|
||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||
|
|
@ -651,25 +650,11 @@ export default defineComponent({
|
|||
updateHeight() {
|
||||
this.detailsCompleteHeight = this.$refs["detailsInner"]?.offsetHeight ?? 0;
|
||||
},
|
||||
openBatterySettingsModal() {
|
||||
if (this.experimental) {
|
||||
this.$router.push("/battery");
|
||||
return;
|
||||
}
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("batterySettingsModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
openBatteryView() {
|
||||
this.$router.push("/battery");
|
||||
},
|
||||
openForecastModal() {
|
||||
if (this.experimental) {
|
||||
this.$router.push("/forecast");
|
||||
return;
|
||||
}
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("forecastModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
openForecastView() {
|
||||
this.$router.push("/forecast");
|
||||
},
|
||||
dischargePower(power: number) {
|
||||
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,
|
||||
'alert-danger': showError,
|
||||
'alert-secondary': !showError,
|
||||
'alert--bottomtabs': experimental && !blocking,
|
||||
'alert--bottomtabs': !blocking,
|
||||
}"
|
||||
role="alert"
|
||||
data-testid="bottom-banner"
|
||||
|
|
@ -81,7 +81,6 @@ export default defineComponent({
|
|||
offline: Boolean,
|
||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
||||
startupCompleted: Boolean,
|
||||
experimental: Boolean,
|
||||
},
|
||||
data() {
|
||||
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"
|
||||
@id-changed="selectedLoadpointChanged"
|
||||
/>
|
||||
<Footer v-if="!experimental" v-bind="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/arrowup";
|
||||
import Footer from "../Footer/Footer.vue";
|
||||
import TopNavigationArea from "../Top/TopNavigationArea.vue";
|
||||
import Energyflow from "../Energyflow/Energyflow.vue";
|
||||
import HemsWarning from "../HemsWarning.vue";
|
||||
|
|
@ -96,7 +94,6 @@ import type {
|
|||
Notification,
|
||||
Circuit,
|
||||
SMART_COST_TYPE,
|
||||
Sponsor,
|
||||
FatalError,
|
||||
EvOpt,
|
||||
BATTERY_MODE,
|
||||
|
|
@ -107,7 +104,6 @@ import type { Grid } from "./types";
|
|||
export default defineComponent({
|
||||
name: "Site",
|
||||
components: {
|
||||
Footer,
|
||||
Loadpoints,
|
||||
Energyflow,
|
||||
HemsWarning,
|
||||
|
|
@ -143,7 +139,6 @@ export default defineComponent({
|
|||
vehicles: Object,
|
||||
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
|
||||
currency: { type: String as PropType<CURRENCY> },
|
||||
statistics: Object,
|
||||
tariffFeedIn: Number,
|
||||
tariffGrid: Number,
|
||||
tariffCo2: Number,
|
||||
|
|
@ -152,20 +147,12 @@ export default defineComponent({
|
|||
tariffPriceLoadpoints: 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>,
|
||||
smartCostAvailable: Boolean,
|
||||
smartFeedInPriorityAvailable: Boolean,
|
||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
||||
forecast: Object as PropType<Forecast>,
|
||||
circuits: Object as PropType<Record<string, Circuit>>,
|
||||
telemetry: Boolean,
|
||||
experimental: Boolean,
|
||||
evopt: { type: Object as PropType<EvOpt> },
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -198,24 +185,6 @@ export default defineComponent({
|
|||
// work in progess
|
||||
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() {
|
||||
return this.fatal.length > 0;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,22 +4,6 @@
|
|||
data-testid="header"
|
||||
>
|
||||
<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>
|
||||
</h1>
|
||||
<TopNavigationArea ref="navigationArea" :notifications="notifications" />
|
||||
|
|
@ -27,10 +11,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/home";
|
||||
import "@h2d2/shopicons/es/regular/settings";
|
||||
import TopNavigationArea from "./TopNavigationArea.vue";
|
||||
import store from "@/store";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { Notification } from "@/types/evcc";
|
||||
|
||||
|
|
@ -40,15 +21,9 @@ export default defineComponent({
|
|||
TopNavigationArea,
|
||||
},
|
||||
props: {
|
||||
showConfig: Boolean,
|
||||
title: String,
|
||||
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
||||
},
|
||||
computed: {
|
||||
experimental(): boolean {
|
||||
return store.state.experimental ?? false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
requestAuthProvider(providerId: string) {
|
||||
const navigationArea = this.$refs["navigationArea"] as
|
||||
|
|
@ -59,12 +34,3 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</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"
|
||||
: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-if="experimental" v-bind="savings" />
|
||||
<Savings v-bind="savings" />
|
||||
<AuthProviderModal :provider="authProvider" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -14,24 +13,20 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import Navigation from "./Navigation.vue";
|
||||
import Notifications from "./Notifications.vue";
|
||||
import AuthProviderModal from "./AuthProviderModal.vue";
|
||||
import Savings from "../Savings/Savings.vue";
|
||||
import type { Provider } from "./types";
|
||||
import type { Notification } from "@/types/evcc";
|
||||
import collector from "@/mixins/collector";
|
||||
import store from "@/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TopNavigationArea",
|
||||
components: {
|
||||
TopNavigation: Navigation,
|
||||
Notifications,
|
||||
AuthProviderModal,
|
||||
Savings,
|
||||
},
|
||||
mixins: [collector],
|
||||
props: {
|
||||
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
||||
},
|
||||
|
|
@ -42,12 +37,6 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
experimental(): boolean {
|
||||
return store.state?.experimental || false;
|
||||
},
|
||||
topNavigation(): any {
|
||||
return this.collectProps(Navigation, store.state);
|
||||
},
|
||||
savings() {
|
||||
return {
|
||||
sponsor: store.state.sponsor,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from "./components/Auth/auth";
|
||||
import { initConfigModal } from "./configModal";
|
||||
import { hapticFeedback } from "./utils/haptic";
|
||||
import store from "./store";
|
||||
import type { VueI18nInstance } from "vue-i18n";
|
||||
|
||||
function hideAllModals() {
|
||||
|
|
@ -158,9 +157,7 @@ export default function setupRouter(i18n: VueI18nInstance) {
|
|||
// Only hide modals when the actual route path changes, not query parameters
|
||||
if (to.path !== from.path) {
|
||||
hideAllModals();
|
||||
if (store.state.experimental) {
|
||||
hapticFeedback();
|
||||
}
|
||||
hapticFeedback();
|
||||
}
|
||||
});
|
||||
initConfigModal(router);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
<template>
|
||||
<div class="app" :class="{ 'app--bottomtabs': state.experimental }">
|
||||
<div class="app">
|
||||
<router-view
|
||||
v-if="showRoutes"
|
||||
:notifications="notifications"
|
||||
:offline="offline"
|
||||
></router-view>
|
||||
|
||||
<BottomTabBar v-if="state.experimental" v-bind="bottomTabBarProps" />
|
||||
<BottomTabBar v-bind="bottomTabBarProps" />
|
||||
|
||||
<GlobalSettingsModal v-bind="globalSettingsProps" />
|
||||
<BatterySettingsModal v-if="batteryModalAvailabe" v-bind="batterySettingsProps" />
|
||||
<ForecastModal v-bind="forecastModalProps" />
|
||||
<AboutModal v-bind="aboutModalProps" />
|
||||
<HelpModal />
|
||||
<PasswordModal />
|
||||
|
|
@ -23,8 +21,6 @@
|
|||
import store from "../store";
|
||||
import BottomTabBar from "../components/BottomTabs/Bar.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 PasswordModal from "../components/Auth/PasswordModal.vue";
|
||||
import LoginModal from "../components/Auth/LoginModal.vue";
|
||||
|
|
@ -50,8 +46,6 @@ export default defineComponent({
|
|||
BottomTabBar,
|
||||
GlobalSettingsModal,
|
||||
HelpModal,
|
||||
BatterySettingsModal,
|
||||
ForecastModal,
|
||||
PasswordModal,
|
||||
LoginModal,
|
||||
OfflineIndicator,
|
||||
|
|
@ -76,9 +70,6 @@ export default defineComponent({
|
|||
version() {
|
||||
return store.state.version;
|
||||
},
|
||||
batteryModalAvailabe() {
|
||||
return store.state.battery?.devices?.length;
|
||||
},
|
||||
showRoutes() {
|
||||
return this.state.startupCompleted;
|
||||
},
|
||||
|
|
@ -89,15 +80,9 @@ export default defineComponent({
|
|||
globalSettingsProps() {
|
||||
return this.collectProps(GlobalSettingsModal, this.state);
|
||||
},
|
||||
batterySettingsProps() {
|
||||
return this.collectProps(BatterySettingsModal, this.state);
|
||||
},
|
||||
offlineIndicatorProps() {
|
||||
return this.collectProps(OfflineIndicator, this.state);
|
||||
},
|
||||
forecastModalProps() {
|
||||
return this.collectProps(ForecastModal, this.state);
|
||||
},
|
||||
loginModalProps() {
|
||||
return this.collectProps(LoginModal, this.state);
|
||||
},
|
||||
|
|
@ -247,7 +232,4 @@ export default defineComponent({
|
|||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
.app--bottomtabs {
|
||||
--bottom-space: calc(var(--tab-bar-height) + 1.5rem);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@
|
|||
<hr class="my-5" />
|
||||
|
||||
<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">
|
||||
{{ $t("config.system.logs") }}
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import {
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
openTopNavigation,
|
||||
expectTopNavigationClosed,
|
||||
} from "./utils";
|
||||
import { expectModalHidden, expectModalVisible, openMoreMenu } from "./utils";
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const BASIC = "basics.evcc.yaml";
|
||||
|
|
@ -49,9 +44,8 @@ test("login", async ({ page }) => {
|
|||
await page.goto("/");
|
||||
|
||||
// go to config
|
||||
await openTopNavigation(page);
|
||||
await openMoreMenu(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
|
||||
// login modal
|
||||
const login = page.getByTestId("login-modal");
|
||||
|
|
@ -77,9 +71,8 @@ test("http iframe hint", async ({ page }) => {
|
|||
await page.goto("/");
|
||||
|
||||
// go to config
|
||||
await openTopNavigation(page);
|
||||
await openMoreMenu(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
|
||||
// login modal
|
||||
const login = page.getByTestId("login-modal");
|
||||
|
|
@ -129,18 +122,16 @@ test("update password", async ({ page }) => {
|
|||
).not.toBeVisible();
|
||||
|
||||
// logout
|
||||
await openTopNavigation(page);
|
||||
await page.getByRole("button", { name: "Logout" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("button", { name: "Logout" }).click();
|
||||
|
||||
// should be redirected to home page after logout
|
||||
await expect(page).toHaveURL("/#/");
|
||||
|
||||
// login modal
|
||||
await openTopNavigation(page);
|
||||
await expect(page.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu2 = await openMoreMenu(page);
|
||||
await expect(menu2.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
||||
await menu2.getByRole("link", { name: "Configuration" }).click();
|
||||
const loginNew = page.getByTestId("login-modal");
|
||||
await expectModalVisible(loginNew);
|
||||
await loginNew.getByLabel("Administrator Password").fill(newPassword);
|
||||
|
|
@ -171,10 +162,9 @@ test("disable auth", async ({ page }) => {
|
|||
await expectModalHidden(modal);
|
||||
|
||||
// configuration page without login
|
||||
await openTopNavigation(page);
|
||||
await expect(page.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu = await openMoreMenu(page);
|
||||
await expect(menu.getByRole("button", { name: "Logout" })).not.toBeVisible();
|
||||
await menu.getByRole("link", { name: "Configuration" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl, restart } from "./evcc";
|
||||
import { openTopNavigation, expectModalVisible, expectModalHidden } from "./utils";
|
||||
import { openMoreMenu, expectModalVisible, expectModalHidden } from "./utils";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ test.describe("reset", async () => {
|
|||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
// open backup & restore modal
|
||||
await openTopNavigation(page);
|
||||
await openMoreMenu(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await page.getByRole("button", { name: "Backup & Restore" }).click();
|
||||
const modal = page.getByTestId("backup-restore-modal");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { expectModalVisible, expectModalHidden, openTopNavigation } from "./utils";
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
|
|
@ -12,28 +11,24 @@ test.afterAll(async () => {
|
|||
|
||||
test.describe("battery settings co2", async () => {
|
||||
test("grid charging", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
const modal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(modal);
|
||||
await page.goto("/#/battery");
|
||||
|
||||
await modal.getByRole("link", { name: "Grid charging" }).click();
|
||||
await modal.getByLabel("Enable limit").check();
|
||||
await modal.getByLabel("CO₂ limit").selectOption({ label: "≤ 150 g/kWh" });
|
||||
await page.getByLabel("Enable limit").check();
|
||||
await page.getByLabel("CO₂ limit").selectOption({ label: "≤ 150 g/kWh" });
|
||||
// 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(modal).toContainText("20 g – 150 g");
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expect(page.getByTestId("active-hours").locator(".value")).toHaveText(/^\d+ hr/);
|
||||
await expect(page.locator("body")).toContainText("20 g – 150 g");
|
||||
|
||||
// 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.getByRole("button", { name: "Grid charging: active (≤ 150 g)" }).click();
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("CO₂ limit").selectOption({ label: "≤ 10 g/kWh" });
|
||||
await expect(modal.getByTestId("active-hours")).toHaveText("Active time");
|
||||
await modal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/#\/battery/);
|
||||
|
||||
await page.getByLabel("CO₂ limit").selectOption({ label: "≤ 10 g/kWh" });
|
||||
await expect(page.getByTestId("active-hours")).toHaveText("Active time");
|
||||
|
||||
await page.getByRole("link", { name: "Charge" }).click();
|
||||
await expect(page.getByRole("button", { name: "Grid charging: when ≤ 10 g" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { expectModalVisible, expectModalHidden, openTopNavigation } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
|
@ -13,56 +12,44 @@ test.afterEach(async () => {
|
|||
});
|
||||
|
||||
test.describe("battery settings", async () => {
|
||||
test("open modal", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
test("battery view", async ({ page }) => {
|
||||
await page.goto("/#/battery");
|
||||
|
||||
const modal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(modal);
|
||||
await expect(modal.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
||||
await expect(modal.getByRole("link", { name: "Grid charging" })).toBeVisible();
|
||||
await expect(modal).toContainText("Battery level: 50%");
|
||||
await expect(modal).toContainText("10.0 kWh of 20.0 kWh");
|
||||
await expect(page.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Grid charging" })).toBeVisible();
|
||||
await expect(page.getByTestId("header")).toContainText("Home Battery");
|
||||
await expect(page.locator("body")).toContainText("Battery level: 50%");
|
||||
await expect(page.locator("body")).toContainText("10.0 kWh of 20.0 kWh");
|
||||
});
|
||||
|
||||
test("battery usage", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
await page.goto("/#/battery");
|
||||
|
||||
const modal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.locator("#batterySettingsPriority").selectOption({ label: "50%" });
|
||||
await expect(modal.locator("label[for=batterySettingsPriorityMiddle] span")).toHaveText("50%");
|
||||
await expect(modal.locator("label[for=batterySettingsPriorityBottom] span")).toHaveText("50%");
|
||||
await modal.locator("#batterySettingsBufferTop").selectOption({ label: "80%" });
|
||||
await modal.locator("#batterySettingsBufferStart").selectOption({ label: "when above 90%." });
|
||||
await expect(modal.locator("label[for=batterySettingsBuffer] span")).toHaveText("80%");
|
||||
await page.locator("#batterySettingsPriority").selectOption({ label: "50%" });
|
||||
await expect(page.locator("label[for=batterySettingsPriorityMiddle] span")).toHaveText("50%");
|
||||
await expect(page.locator("label[for=batterySettingsPriorityBottom] span")).toHaveText("50%");
|
||||
await page.locator("#batterySettingsBufferTop").selectOption({ label: "80%" });
|
||||
await page.locator("#batterySettingsBufferStart").selectOption({ label: "when above 90%." });
|
||||
await expect(page.locator("label[for=batterySettingsBuffer] span")).toHaveText("80%");
|
||||
});
|
||||
|
||||
test("grid charging", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
const modal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(modal);
|
||||
await page.goto("/#/battery");
|
||||
|
||||
await modal.getByRole("link", { name: "Grid charging" }).click();
|
||||
await modal.getByLabel("Enable limit").check();
|
||||
await modal.getByLabel("Price limit").selectOption({ label: "≤ 50.0 ct/kWh" });
|
||||
await expect(modal.getByTestId("active-hours")).toHaveText(["Active time", "96 hr"].join(""));
|
||||
await expect(modal).toContainText("5.0 ct – 50.0 ct");
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await page.getByLabel("Enable limit").check();
|
||||
await page.getByLabel("Price limit").selectOption({ label: "≤ 50.0 ct/kWh" });
|
||||
await expect(page.getByTestId("active-hours")).toHaveText(["Active time", "96 hr"].join(""));
|
||||
await expect(page.locator("body")).toContainText("5.0 ct – 50.0 ct");
|
||||
|
||||
await page.getByRole("link", { name: "Charge" }).click();
|
||||
await page.getByTestId("energyflow").click();
|
||||
await page.getByRole("button", { name: "Grid charging: active (≤ 50.0 ct)" }).click();
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
|
||||
await expect(modal.getByTestId("active-hours")).toHaveText("Active time");
|
||||
await modal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/#\/battery/);
|
||||
|
||||
await page.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
|
||||
await expect(page.getByTestId("active-hours")).toHaveText("Active time");
|
||||
|
||||
await page.getByRole("link", { name: "Charge" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Grid charging: when ≤ -10.0 ct" })
|
||||
).toBeVisible();
|
||||
|
|
@ -79,14 +66,12 @@ test.describe("battery settings", async () => {
|
|||
await expect(charge).toContainText("Battery charging");
|
||||
|
||||
// enable discharge lock
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
const modal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await page.goto("/#/battery");
|
||||
await page.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.getByRole("link", { name: "Charge" }).click();
|
||||
|
||||
await page.getByTestId("energyflow").click();
|
||||
await expect(discharge).toContainText("Battery (locked)");
|
||||
await expect(charge).toContainText("Battery charging");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
newLoadpoint,
|
||||
addDemoCharger,
|
||||
ChargerStatus,
|
||||
openTopNavigation,
|
||||
} from "./utils";
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
|
|
@ -93,13 +92,10 @@ test.describe("boost", async () => {
|
|||
await expectModalHidden(modal);
|
||||
|
||||
// enable "Prevent discharge in fast mode"
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
const batteryModal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(batteryModal);
|
||||
await batteryModal.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(batteryModal);
|
||||
await page.goto("/#/battery");
|
||||
await page.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto("/");
|
||||
|
||||
// LP2: switch to fast mode → triggers global battery hold
|
||||
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("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.getByTestId("energyflow")).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ test.describe("onboarding", async () => {
|
|||
await expect(restartButton).not.toBeVisible();
|
||||
|
||||
// navigate to main screen
|
||||
await page.getByTestId("home-link").click();
|
||||
await page.getByRole("link", { name: "Charge" }).click();
|
||||
|
||||
// verify configuration
|
||||
await page.getByTestId("visualization").click();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import {
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
openTopNavigation,
|
||||
expectTopNavigationClosed,
|
||||
} from "./utils";
|
||||
import { expectModalHidden, expectModalVisible, openMoreMenu } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
const NETWORK_HOST = "somehostname.local";
|
||||
|
|
@ -23,9 +18,8 @@ test.afterAll(async () => {
|
|||
test.describe("basics", async () => {
|
||||
test("navigation to config", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await openMoreMenu(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
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");
|
||||
|
||||
// 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();
|
||||
});
|
||||
test("enable experimental", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { expectModalHidden, openTopNavigation, expectTopNavigationClosed } from "./utils";
|
||||
import { expectModalHidden, openMoreMenu } from "./utils";
|
||||
import { ChildProcess } from "child_process";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
|
@ -35,9 +35,8 @@ test.describe("demo mode", async () => {
|
|||
});
|
||||
|
||||
test("auth is locked", async ({ page }) => {
|
||||
await openTopNavigation(page);
|
||||
await openMoreMenu(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const loginModal = page.getByTestId("login-modal");
|
||||
await expect(loginModal).toBeVisible();
|
||||
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 { 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";
|
||||
|
||||
|
|
@ -20,8 +20,8 @@ test.beforeEach(async ({ page }) => {
|
|||
});
|
||||
|
||||
async function openModal(page: Page) {
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-settings").click();
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("button", { name: "User Interface" }).click();
|
||||
const modal = page.getByTestId("global-settings-modal");
|
||||
await expectModalVisible(modal);
|
||||
return modal;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { openTopNavigation, expectTopNavigationClosed } from "./utils";
|
||||
import { openMoreMenu } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
|
|
@ -14,17 +14,16 @@ test.afterAll(async () => {
|
|||
test.describe("opening logs", async () => {
|
||||
test("via config", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
await page.getByRole("link", { name: "Logs" }).click();
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("link", { name: "Configuration" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||
await page.getByTestId("config-system").getByRole("link", { name: "Logs" }).click();
|
||||
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 openTopNavigation(page);
|
||||
await page.getByRole("link", { name: "Logs" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("link", { name: "Logs" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||
});
|
||||
test("via notifications", async ({ page }) => {
|
||||
|
|
@ -36,9 +35,8 @@ test.describe("opening logs", async () => {
|
|||
});
|
||||
test("via need help", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page.getByRole("button", { name: "Need Help?" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("button", { name: "Need Help?" }).click();
|
||||
await page.getByRole("link", { name: "View logs" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorConfig } from "./simulator";
|
||||
import { openTopNavigation, expectTopNavigationClosed, closeTopNavigation } from "./utils";
|
||||
import { openMoreMenu } from "./utils";
|
||||
|
||||
const BASICS_CONFIG = "basics.evcc.yaml";
|
||||
|
||||
|
|
@ -29,10 +29,11 @@ test.describe("Basics", async () => {
|
|||
await page.goto(route.path);
|
||||
|
||||
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
|
||||
await openTopNavigation(page);
|
||||
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Home Battery" })).not.toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
|
||||
// no battery tab when battery is not configured
|
||||
await expect(page.getByRole("link", { name: "Battery" })).not.toBeVisible();
|
||||
const menu = await openMoreMenu(page);
|
||||
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 expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
|
||||
await openTopNavigation(page);
|
||||
await page.getByRole("button", { name: "Need help?" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("button", { name: "Need Help?" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Need help?" })).toBeVisible();
|
||||
});
|
||||
|
|
@ -50,9 +50,8 @@ test.describe("Basics", async () => {
|
|||
test("User Interface", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
|
||||
await openTopNavigation(page);
|
||||
await page.getByRole("button", { name: "User Interface" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
const menu = await openMoreMenu(page);
|
||||
await menu.getByRole("button", { name: "User Interface" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "User Interface" })).toBeVisible();
|
||||
});
|
||||
|
|
@ -76,20 +75,18 @@ test.describe("Advanced", async () => {
|
|||
await page.goto(route.path);
|
||||
|
||||
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
|
||||
await openTopNavigation(page);
|
||||
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Home Battery" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
|
||||
await closeTopNavigation(page);
|
||||
// battery tab visible when battery is configured
|
||||
await expect(page.getByRole("link", { name: "Battery" })).toBeVisible();
|
||||
const menu = await openMoreMenu(page);
|
||||
await expect(menu.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||
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 expect(page.getByRole("heading", { name: title })).toBeVisible();
|
||||
await openTopNavigation(page);
|
||||
await page.getByRole("button", { name: "Home Battery" }).click();
|
||||
await expectTopNavigationClosed(page);
|
||||
await page.getByRole("link", { name: "Battery" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Home Battery" })).toBeVisible();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
|
||||
import { expectModalHidden, expectModalVisible } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
|
|
@ -15,13 +15,6 @@ test.describe("bottom navigation", async () => {
|
|||
await page.goto("/");
|
||||
|
||||
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();
|
||||
|
||||
const tabCharge = bottomNav.getByRole("link", { name: "Charge" });
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
import { test, expect, devices, type Page } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import {
|
||||
expectModalVisible,
|
||||
expectModalHidden,
|
||||
openTopNavigation,
|
||||
expectTopNavigationClosed,
|
||||
} from "./utils";
|
||||
import { expectModalVisible, expectModalHidden } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
|
|
@ -38,12 +33,7 @@ async function selectVehicleFilter(page: Page, value: string) {
|
|||
test.describe("basics", async () => {
|
||||
test("navigation to sessions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openTopNavigation(page);
|
||||
await page
|
||||
.getByTestId("topnavigation-dropdown")
|
||||
.getByRole("link", { name: "Charging sessions" })
|
||||
.click();
|
||||
await expectTopNavigationClosed(page);
|
||||
await page.getByRole("link", { name: "Sessions" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||
});
|
||||
test("month without data", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
|
|
@ -108,7 +107,6 @@ test.describe("statistics values", async () => {
|
|||
|
||||
test.describe("header savings", async () => {
|
||||
test("savings info in header and indicator persistence", async ({ page }) => {
|
||||
await enableExperimental(page);
|
||||
await page.goto("/");
|
||||
|
||||
// savings button visible in header
|
||||
|
|
|
|||
|
|
@ -1,40 +1,10 @@
|
|||
import { expect, type Page, type Locator } from "@playwright/test";
|
||||
|
||||
export async function enableExperimental(page: Page): Promise<void> {
|
||||
await page.goto("/#/config");
|
||||
|
||||
await page
|
||||
.getByTestId("generalconfig-experimental")
|
||||
.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 openMoreMenu(page: Page): Promise<Locator> {
|
||||
const moreButton = page.getByTestId("tab-more");
|
||||
await expect(moreButton).toBeVisible();
|
||||
await moreButton.click();
|
||||
return moreButton;
|
||||
}
|
||||
|
||||
export async function expectModalVisible(modal: Locator): Promise<void> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue