Sessions: filter, monthly sums, paging (#8278)
This commit is contained in:
parent
10ba256787
commit
e600d8dc71
16 changed files with 742 additions and 226 deletions
2
.github/workflows/default.yml
vendored
2
.github/workflows/default.yml
vendored
|
|
@ -143,7 +143,7 @@ jobs:
|
|||
integration:
|
||||
name: Integration
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -54,6 +54,8 @@ make
|
|||
./evcc
|
||||
```
|
||||
|
||||
### UI development
|
||||
|
||||
For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above).
|
||||
|
||||
```sh
|
||||
|
|
@ -61,6 +63,15 @@ npm install
|
|||
npm run start
|
||||
```
|
||||
|
||||
### Integration tests
|
||||
|
||||
We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first.
|
||||
|
||||
```sh
|
||||
make ui build
|
||||
npm run playwright
|
||||
```
|
||||
|
||||
### Code formatting
|
||||
|
||||
We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Veture](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
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="showBadge"
|
||||
|
|
@ -15,7 +16,11 @@
|
|||
</span>
|
||||
<shopicon-regular-menu></shopicon-regular-menu>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="topNavigatonDropdown">
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-end"
|
||||
aria-labelledby="topNavigatonDropdown"
|
||||
data-testid="topnavigation-dropdown"
|
||||
>
|
||||
<li>
|
||||
<router-link class="dropdown-item" to="/sessions">
|
||||
{{ $t("header.sessions") }}
|
||||
|
|
|
|||
|
|
@ -170,6 +170,11 @@ export default {
|
|||
year: "numeric",
|
||||
}).format(date);
|
||||
},
|
||||
fmtMonth: function (date, short) {
|
||||
return new Intl.DateTimeFormat(this.$i18n.locale, {
|
||||
month: short ? "short" : "long",
|
||||
}).format(date);
|
||||
},
|
||||
fmtDayMonthYear: function (date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n.locale, {
|
||||
day: "numeric",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,19 @@ export default function setupRouter(i18n) {
|
|||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: "/", component: Main, props: true },
|
||||
{ path: "/sessions", component: ChargingSessions, props: true },
|
||||
{
|
||||
path: "/sessions",
|
||||
component: ChargingSessions,
|
||||
props: (route) => {
|
||||
const { month, year, loadpoint, vehicle } = route.query;
|
||||
return {
|
||||
month: month ? parseInt(month, 10) : undefined,
|
||||
year: year ? parseInt(year, 10) : undefined,
|
||||
loadpointFilter: loadpoint,
|
||||
vehicleFilter: vehicle,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
router.beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -12,126 +12,215 @@
|
|||
|
||||
<div class="row">
|
||||
<main class="col-12">
|
||||
<div class="d-flex justify-content-start mb-4">
|
||||
<a
|
||||
class="btn btn-outline-secondary text-nowrap my-2"
|
||||
:href="csvHrefLink()"
|
||||
download="sessions.csv"
|
||||
<div class="d-flex align-items-baseline justify-content-between my-5">
|
||||
<router-link
|
||||
class="d-flex text-decoration-none align-items-center"
|
||||
:class="{ 'pe-none': !hasPrev, 'text-muted': !hasPrev }"
|
||||
:to="{ query: { ...$route.query, ...prevYearMonth } }"
|
||||
>
|
||||
{{ $t("sessions.downloadCsv") }}
|
||||
</a>
|
||||
<shopicon-regular-angledoubleleftsmall
|
||||
size="s"
|
||||
class="me-1"
|
||||
></shopicon-regular-angledoubleleftsmall>
|
||||
<span class="d-none d-sm-block">{{ prevMonthName }}</span>
|
||||
<span class="d-block d-sm-none">{{ prevMonthNameShort }}</span>
|
||||
</router-link>
|
||||
<h2 class="text-center">{{ headline }}</h2>
|
||||
<router-link
|
||||
class="d-flex text-decoration-none align-items-center"
|
||||
:class="{ 'pe-none': !hasNext, 'text-muted': !hasNext }"
|
||||
:to="{ query: { ...$route.query, ...nextYearMonth } }"
|
||||
>
|
||||
<span class="d-none d-sm-block">{{ nextMonthName }}</span>
|
||||
<span class="d-block d-sm-none">{{ nextMonthNameShort }}</span>
|
||||
<shopicon-regular-angledoublerightsmall
|
||||
size="s"
|
||||
class="ms-1"
|
||||
></shopicon-regular-angledoublerightsmall>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-for="group in sessionsByMonthAndLoadpoint" :key="group.month">
|
||||
<div class="d-flex align-items-center my-5">
|
||||
<h2 class="me-4 mb-0">
|
||||
{{ formatGroupHeadline(group.month) }}
|
||||
</h2>
|
||||
<a
|
||||
class="btn btn-xs btn-outline-secondary text-nowrap"
|
||||
:href="csvHrefLink(group.month)"
|
||||
download="sessions.csv"
|
||||
>
|
||||
CSV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-for="loadpoint in group.loadpoints" :key="loadpoint.name">
|
||||
<div class="d-flex align-items-baseline mb-3">
|
||||
<h3 class="me-4 mb-0">
|
||||
{{ loadpoint.name }}
|
||||
</h3>
|
||||
<div class="large">{{ fmtKWh(loadpoint.total) }}</div>
|
||||
</div>
|
||||
|
||||
<ul class="breakdown text-gray d-sm-flex flex-sm-wrap ps-0 mb-2">
|
||||
<li
|
||||
v-for="(vehicle, id) in groupedKWh('vehicle', loadpoint.sessions)"
|
||||
:key="id"
|
||||
class="breakdown-item"
|
||||
>
|
||||
{{ vehicle.name }}: {{ fmtKWh(vehicle.energy) }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="table-responsive my-3">
|
||||
<table class="table text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="ps-0">{{ $t("sessions.date") }}</th>
|
||||
<th scope="col">{{ $t("sessions.vehicle") }}</th>
|
||||
<th scope="col" class="text-end">
|
||||
{{ $t("sessions.energy") }}
|
||||
</th>
|
||||
<th scope="col" class="text-end">
|
||||
{{ $t("sessions.solar") }}
|
||||
</th>
|
||||
<th scope="col" class="text-end">
|
||||
{{ $t("sessions.price") }}
|
||||
</th>
|
||||
<th scope="col" class="text-end">
|
||||
{{ $t("sessions.avgPrice") }}
|
||||
</th>
|
||||
<th scope="col" class="text-end pe-0">
|
||||
{{ $t("sessions.co2") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(session, id) in loadpoint.sessions"
|
||||
:key="id"
|
||||
role="button"
|
||||
@click="showDetails(session.id)"
|
||||
<div v-if="currentSessions.length === 0" data-testid="sessions-nodata" class="my-5">
|
||||
<p>{{ $t("sessions.noData") }}</p>
|
||||
</div>
|
||||
<div v-else class="table-responsive my-3">
|
||||
<table class="table text-nowrap">
|
||||
<thead>
|
||||
<tr data-testid="sessions-head">
|
||||
<th scope="col" class="align-top ps-0">
|
||||
{{ $t("sessions.date") }}
|
||||
</th>
|
||||
<th scope="col" class="align-top">
|
||||
{{ $t("sessions.loadpoint") }}
|
||||
<label class="position-relative d-block">
|
||||
<select
|
||||
:value="loadpointFilter"
|
||||
class="custom-select"
|
||||
@change="changeLoadpointFilter"
|
||||
>
|
||||
<option
|
||||
v-for="{
|
||||
name,
|
||||
value,
|
||||
count,
|
||||
} in loadpointFilterOptions"
|
||||
:key="value"
|
||||
:value="value"
|
||||
:disabled="count === 0"
|
||||
>
|
||||
{{ name }} ({{ count }})
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
class="fw-normal text-decoration-underline text-nowrap text-muted pe-none"
|
||||
>
|
||||
{{ loadpointFilter || $t("sessions.filter.filter") }}
|
||||
</span>
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col" class="align-top">
|
||||
{{ $t("sessions.vehicle") }}
|
||||
<label class="position-relative d-block">
|
||||
<select
|
||||
:value="vehicleFilter"
|
||||
class="custom-select"
|
||||
@change="changeVehicleFilter"
|
||||
>
|
||||
<option
|
||||
v-for="{
|
||||
name,
|
||||
value,
|
||||
count,
|
||||
} in vehicleFilterOptions"
|
||||
:key="value"
|
||||
:value="value"
|
||||
:disabled="count === 0"
|
||||
>
|
||||
{{ name }} ({{ count }})
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
class="fw-normal text-decoration-underline text-nowrap text-muted pe-none"
|
||||
>
|
||||
{{ vehicleFilter || $t("sessions.filter.filter") }}
|
||||
</span>
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col" class="align-top text-end">
|
||||
{{ $t("sessions.energy") }}
|
||||
<div class="text-muted fw-normal">
|
||||
{{ fmtKWh(chargedEnergy * 1e3, chargedEnergy >= 1) }}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
v-if="hasSolarPercentage"
|
||||
scope="col"
|
||||
class="align-top text-end"
|
||||
>
|
||||
{{ $t("sessions.solar") }}
|
||||
<div
|
||||
v-if="solarPercentage != null"
|
||||
class="text-muted fw-normal"
|
||||
>
|
||||
<td class="ps-0">
|
||||
{{ fmtFullDateTime(new Date(session.created), true) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ session.vehicle }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{{
|
||||
fmtKWh(
|
||||
session.chargedEnergy * 1e3,
|
||||
session.chargedEnergy >= 1
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span v-if="session.solarPercentage != null">
|
||||
{{ fmtNumber(session.solarPercentage, 1) }}%
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span v-if="session.price != null">
|
||||
{{ fmtMoney(session.price, currency) }}
|
||||
{{ fmtCurrencySymbol(currency) }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span v-if="session.pricePerKWh != null">
|
||||
{{ fmtPricePerKWh(session.pricePerKWh, currency) }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td class="text-end pe-0">
|
||||
<span v-if="session.co2PerKWh != null">
|
||||
{{ fmtCo2Medium(session.co2PerKWh) }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ fmtNumber(solarPercentage, 1) }}%
|
||||
</div>
|
||||
</th>
|
||||
<th v-if="hasPrice" scope="col" class="align-top text-end">
|
||||
{{ $t("sessions.price") }}
|
||||
<div v-if="price != null" class="text-muted fw-normal">
|
||||
{{ fmtMoney(price, currency) }}
|
||||
{{ fmtCurrencySymbol(currency) }}
|
||||
</div>
|
||||
</th>
|
||||
<th v-if="hasPrice" scope="col" class="align-top text-end">
|
||||
{{ $t("sessions.avgPrice") }}
|
||||
<div v-if="pricePerKWh != null" class="text-muted fw-normal">
|
||||
{{ fmtPricePerKWh(pricePerKWh, currency) }}
|
||||
</div>
|
||||
</th>
|
||||
<th v-if="hasCo2" scope="col" class="align-top text-end pe-0">
|
||||
{{ $t("sessions.co2") }}
|
||||
<div v-if="co2PerKWh != null" class="text-muted fw-normal">
|
||||
{{ fmtCo2Medium(co2PerKWh) }}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(session, id) in filteredSessions"
|
||||
:key="id"
|
||||
role="button"
|
||||
data-testid="sessions-entry"
|
||||
@click="showDetails(session.id)"
|
||||
>
|
||||
<td class="ps-0">
|
||||
{{ fmtFullDateTime(new Date(session.created), true) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ session.loadpoint }}
|
||||
</td>
|
||||
<td>
|
||||
{{ session.vehicle }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{{
|
||||
fmtKWh(
|
||||
session.chargedEnergy * 1e3,
|
||||
session.chargedEnergy >= 1
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td v-if="hasSolarPercentage" class="text-end">
|
||||
<span v-if="session.solarPercentage != null">
|
||||
{{ fmtNumber(session.solarPercentage, 1) }}%
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td v-if="hasPrice" class="text-end">
|
||||
<span v-if="session.price != null">
|
||||
{{ fmtMoney(session.price, currency) }}
|
||||
{{ fmtCurrencySymbol(currency) }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td v-if="hasPrice" class="text-end">
|
||||
<span v-if="session.pricePerKWh != null">
|
||||
{{ fmtPricePerKWh(session.pricePerKWh, currency) }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
<td v-if="hasCo2" class="text-end pe-0">
|
||||
<span v-if="session.co2PerKWh != null">
|
||||
{{ fmtCo2Medium(session.co2PerKWh) }}
|
||||
</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex mb-5 my-4">
|
||||
<a
|
||||
v-if="currentSessions.length"
|
||||
class="btn btn-outline-secondary text-nowrap me-3"
|
||||
:href="csvLink"
|
||||
download
|
||||
>
|
||||
{{ $t("sessions.csvMonth", { month: headline }) }}
|
||||
</a>
|
||||
<a
|
||||
v-if="sessions.length"
|
||||
class="btn btn-outline-secondary text-nowrap"
|
||||
:href="csvTotalLink"
|
||||
download
|
||||
>
|
||||
{{ $t("sessions.csvTotal") }}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<ChargingSessionModal
|
||||
:session="selectedSession"
|
||||
:vehicles="vehicles"
|
||||
:vehicles="vehiclesObjects"
|
||||
@session-changed="loadSessions"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -142,6 +231,8 @@
|
|||
import Modal from "bootstrap/js/dist/modal";
|
||||
import TopNavigation from "../components/TopNavigation.vue";
|
||||
import "@h2d2/shopicons/es/bold/arrowback";
|
||||
import "@h2d2/shopicons/es/regular/angledoubleleftsmall";
|
||||
import "@h2d2/shopicons/es/regular/angledoublerightsmall";
|
||||
import formatter from "../mixins/formatter";
|
||||
import api from "../api";
|
||||
import store from "../store";
|
||||
|
|
@ -153,31 +244,100 @@ export default {
|
|||
mixins: [formatter],
|
||||
props: {
|
||||
notifications: Array,
|
||||
month: { type: Number, default: () => new Date().getMonth() + 1 },
|
||||
year: { type: Number, default: () => new Date().getFullYear() },
|
||||
loadpointFilter: { type: String, default: "" },
|
||||
vehicleFilter: { type: String, default: "" },
|
||||
},
|
||||
data() {
|
||||
return { sessions: [], selectedSessionId: undefined };
|
||||
return {
|
||||
sessions: [],
|
||||
selectedSessionId: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sessionsByMonthAndLoadpoint() {
|
||||
currentSessions() {
|
||||
const sessionsWithDefaults = this.sessions.map((session) => {
|
||||
const loadpoint = session.loadpoint || this.$t("main.loadpoint.fallbackName");
|
||||
const vehicle = session.vehicle || this.$t("main.vehicle.unknown");
|
||||
return { ...session, loadpoint, vehicle };
|
||||
});
|
||||
|
||||
const sessionsByMonth = this.groupByMonth(sessionsWithDefaults);
|
||||
|
||||
return Object.entries(sessionsByMonth).map(([month, sessions]) => {
|
||||
const loadpoints = Object.entries(this.groupByLoadpoint(sessions)).map(
|
||||
([loadpoint, sessionsByLoadpoint]) => {
|
||||
const total = this.totalKWh(sessionsByLoadpoint);
|
||||
return { name: loadpoint, total, sessions: sessionsByLoadpoint };
|
||||
}
|
||||
);
|
||||
return { month, loadpoints };
|
||||
return sessionsWithDefaults.filter((session) => {
|
||||
const date = new Date(session.created);
|
||||
return date.getFullYear() === this.year && date.getMonth() + 1 === this.month;
|
||||
});
|
||||
},
|
||||
filteredSessions() {
|
||||
return this.currentSessions.filter(this.filterByLoadpoint).filter(this.filterByVehicle);
|
||||
},
|
||||
vehicleFilterOptions() {
|
||||
const options = [
|
||||
{
|
||||
name: this.$t("sessions.filter.allVehicles"),
|
||||
value: "",
|
||||
count: this.filterCountForVehicle(),
|
||||
},
|
||||
];
|
||||
this.vehicles.forEach((name) => {
|
||||
const count = this.filterCountForVehicle(name);
|
||||
options.push({ name, value: name, count });
|
||||
});
|
||||
return options;
|
||||
},
|
||||
loadpointFilterOptions() {
|
||||
const options = [
|
||||
{
|
||||
name: this.$t("sessions.filter.allLoadpoints"),
|
||||
value: "",
|
||||
count: this.filterCountForLoadpoint(),
|
||||
},
|
||||
];
|
||||
this.loadpoints.forEach((name) => {
|
||||
const count = this.filterCountForLoadpoint(name);
|
||||
options.push({ name, value: name, count });
|
||||
});
|
||||
return options;
|
||||
},
|
||||
chargedEnergy() {
|
||||
return this.filteredSessions.reduce((total, s) => total + s.chargedEnergy, 0);
|
||||
},
|
||||
price() {
|
||||
return this.filteredSessions.reduce((total, s) => total + s.price, 0);
|
||||
},
|
||||
hasPrice() {
|
||||
return this.filteredSessions.find((s) => s.price != null) != null;
|
||||
},
|
||||
hasSolarPercentage() {
|
||||
return this.filteredSessions.find((s) => s.solarPercentage != null) != null;
|
||||
},
|
||||
hasCo2() {
|
||||
return this.filteredSessions.find((s) => s.co2PerKWh != null) != null;
|
||||
},
|
||||
pricePerKWh() {
|
||||
return this.price / this.chargedEnergy;
|
||||
},
|
||||
co2PerKWh() {
|
||||
const emittedCo2 = this.filteredSessions.reduce(
|
||||
(total, s) => total + s.chargedEnergy * s.co2PerKWh,
|
||||
0
|
||||
);
|
||||
return emittedCo2 / this.chargedEnergy;
|
||||
},
|
||||
solarPercentage() {
|
||||
const chargedSolarEnergy = this.filteredSessions.reduce(
|
||||
(total, s) => total + s.chargedEnergy * (s.solarPercentage / 100),
|
||||
0
|
||||
);
|
||||
return (100 / this.chargedEnergy) * chargedSolarEnergy;
|
||||
},
|
||||
loadpoints() {
|
||||
return [...new Set(this.currentSessions.map((s) => s.loadpoint))];
|
||||
},
|
||||
vehicles() {
|
||||
return [...new Set(this.currentSessions.map((s) => s.vehicle))];
|
||||
},
|
||||
vehiclesObjects() {
|
||||
return (
|
||||
store.state.vehicles?.map((v, index) => {
|
||||
return { id: index, title: v };
|
||||
|
|
@ -190,70 +350,108 @@ export default {
|
|||
currency() {
|
||||
return store.state.currency;
|
||||
},
|
||||
headline() {
|
||||
const date = new Date();
|
||||
date.setMonth(this.month - 1);
|
||||
date.setFullYear(this.year);
|
||||
return this.fmtMonthYear(date);
|
||||
},
|
||||
csvLink() {
|
||||
return this.csvHrefLink(this.year, this.month);
|
||||
},
|
||||
csvTotalLink() {
|
||||
return this.csvHrefLink();
|
||||
},
|
||||
prevDate() {
|
||||
const date = new Date();
|
||||
date.setFullYear(this.year);
|
||||
date.setMonth(this.month - 2);
|
||||
return date;
|
||||
},
|
||||
prevYearMonth() {
|
||||
return { year: this.prevDate.getFullYear(), month: this.prevDate.getMonth() + 1 };
|
||||
},
|
||||
prevMonthName() {
|
||||
return this.fmtMonth(this.prevDate);
|
||||
},
|
||||
prevMonthNameShort() {
|
||||
return this.fmtMonth(this.prevDate, true);
|
||||
},
|
||||
nextDate() {
|
||||
const date = new Date();
|
||||
date.setFullYear(this.year);
|
||||
date.setMonth(this.month);
|
||||
return date;
|
||||
},
|
||||
nextYearMonth() {
|
||||
return { year: this.nextDate.getFullYear(), month: this.nextDate.getMonth() + 1 };
|
||||
},
|
||||
nextMonthName() {
|
||||
return this.fmtMonth(this.nextDate);
|
||||
},
|
||||
nextMonthNameShort() {
|
||||
return this.fmtMonth(this.nextDate, true);
|
||||
},
|
||||
hasNext() {
|
||||
const now = new Date();
|
||||
return this.year < now.getFullYear() || this.month < now.getMonth() + 1;
|
||||
},
|
||||
hasPrev() {
|
||||
const length = this.sessions.length;
|
||||
if (length === 0) {
|
||||
return false;
|
||||
}
|
||||
const first = new Date(this.sessions[length - 1].created);
|
||||
return this.year > first.getFullYear() || this.month > first.getMonth() + 1;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadSessions();
|
||||
},
|
||||
methods: {
|
||||
filterByLoadpoint(session) {
|
||||
return !this.loadpointFilter || session.loadpoint === this.loadpointFilter;
|
||||
},
|
||||
filterByVehicle(session) {
|
||||
return !this.vehicleFilter || session.vehicle === this.vehicleFilter;
|
||||
},
|
||||
filterCountForVehicle(vehicle) {
|
||||
return this.currentSessions
|
||||
.filter(this.filterByLoadpoint)
|
||||
.filter((s) => !vehicle || s.vehicle === vehicle).length;
|
||||
},
|
||||
filterCountForLoadpoint(loadpoint) {
|
||||
return this.currentSessions
|
||||
.filter(this.filterByVehicle)
|
||||
.filter((s) => !loadpoint || s.loadpoint === loadpoint).length;
|
||||
},
|
||||
changeLoadpointFilter(event) {
|
||||
const loadpoint = event.target.value || undefined;
|
||||
this.$router.push({ query: { ...this.$route.query, loadpoint } });
|
||||
},
|
||||
changeVehicleFilter(event) {
|
||||
const vehicle = event.target.value || undefined;
|
||||
this.$router.push({ query: { ...this.$route.query, vehicle } });
|
||||
},
|
||||
async loadSessions() {
|
||||
const response = await api.get("sessions");
|
||||
this.sessions = response.data?.result;
|
||||
},
|
||||
groupByMonth(sessions) {
|
||||
return sessions.reduce((groups, session) => {
|
||||
const date = new Date(session.created);
|
||||
const month = `${date.getFullYear()}.${date.getMonth() + 1}`;
|
||||
if (!groups[month]) groups[month] = [];
|
||||
groups[month].push(session);
|
||||
return groups;
|
||||
}, {});
|
||||
},
|
||||
groupByLoadpoint(sessions) {
|
||||
return sessions.reduce((groups, session) => {
|
||||
const loadpoint = session.loadpoint;
|
||||
if (!groups[loadpoint]) groups[loadpoint] = [];
|
||||
groups[loadpoint].push(session);
|
||||
return groups;
|
||||
}, {});
|
||||
},
|
||||
totalKWh(sessions) {
|
||||
return sessions.reduce((total, session) => total + session.chargedEnergy, 0) * 1e3;
|
||||
},
|
||||
groupedKWh(by, sessions) {
|
||||
const grouped = sessions.reduce((groups, session) => {
|
||||
const name = session[by];
|
||||
if (!groups[name]) groups[name] = 0;
|
||||
groups[name] += session.chargedEnergy * 1e3;
|
||||
return groups;
|
||||
}, {});
|
||||
const list = Object.entries(grouped).map(([name, energy]) => {
|
||||
return { name, energy };
|
||||
});
|
||||
return list.length >= 2 ? list : [];
|
||||
},
|
||||
formatGroupHeadline(group) {
|
||||
const date = new Date();
|
||||
const [year, month] = group.split(".");
|
||||
date.setMonth(month - 1);
|
||||
date.setFullYear(year);
|
||||
return this.fmtMonthYear(date);
|
||||
},
|
||||
showDetails(sessionId) {
|
||||
this.selectedSessionId = sessionId;
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("sessionDetailsModal"));
|
||||
modal.show();
|
||||
},
|
||||
csvHrefLink(groupKey) {
|
||||
var url = `./api/sessions?format=csv&lang=${this.$i18n.locale}`;
|
||||
if (groupKey) {
|
||||
const [year, month] = groupKey.split(".");
|
||||
url += `&year=${year}`;
|
||||
|
||||
if (month) {
|
||||
url += `&month=${month}`;
|
||||
}
|
||||
csvHrefLink(year, month) {
|
||||
const params = new URLSearchParams({
|
||||
format: "csv",
|
||||
lang: this.$i18n.locale,
|
||||
});
|
||||
if (year && month) {
|
||||
params.append("year", year);
|
||||
params.append("month", month);
|
||||
}
|
||||
return url;
|
||||
return `./api/sessions?${params.toString()}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -265,27 +463,13 @@ export default {
|
|||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
.breakdown {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.breakdown-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* breakpoint sm */
|
||||
@media (min-width: 576px) {
|
||||
.breakdown-item:after {
|
||||
content: ", ";
|
||||
white-space: wrap;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.breakdown-item:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.breakdown:empty {
|
||||
display: none;
|
||||
.custom-select {
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
-webkit-appearance: menulist-button;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -285,10 +285,13 @@ title = "Ladevorgang"
|
|||
[sessions]
|
||||
avgPrice = "Ø Preis"
|
||||
co2 = "Ø CO₂"
|
||||
csvMonth = "Download {month} CSV"
|
||||
csvTotal = "Download total CSV"
|
||||
date = "Anfang"
|
||||
downloadCsv = "Als CSV herunterladen"
|
||||
energy = "Geladen"
|
||||
loadpoint = "Ladepunkt"
|
||||
noData = "Noch keine Ladevorgänge in diesem Monat."
|
||||
price = "Σ Preis"
|
||||
reallyDelete = "Wollen Sie diesen Ladevorgang wirklich löschen?"
|
||||
solar = "Sonne"
|
||||
|
|
@ -306,6 +309,11 @@ meterstop = "Endzählerstand (kWh)"
|
|||
odometer = "Kilometerstand (km)"
|
||||
vehicle = "Fahrzeug"
|
||||
|
||||
[sessions.filter]
|
||||
allLoadpoints = "Alle Ladepunkte"
|
||||
allVehicles = "Alle Fahrzeuge"
|
||||
filter = "filtern"
|
||||
|
||||
[settings]
|
||||
title = "Einstellungen"
|
||||
|
||||
|
|
|
|||
|
|
@ -285,10 +285,13 @@ title = "Charging Session"
|
|||
[sessions]
|
||||
avgPrice = "Ø Price"
|
||||
co2 = "Ø CO₂"
|
||||
csvMonth = "Download {month} CSV"
|
||||
csvTotal = "Download total CSV"
|
||||
date = "Finished"
|
||||
downloadCsv = "Download as CSV"
|
||||
energy = "Charged"
|
||||
loadpoint = "Charging point"
|
||||
noData = "No charging sessions this month."
|
||||
price = "Σ Price"
|
||||
reallyDelete = "Do you really want to delete this session?"
|
||||
solar = "Solar"
|
||||
|
|
@ -306,6 +309,11 @@ meterstop = "Meter stop (kWh)"
|
|||
odometer = "Mileage (km)"
|
||||
vehicle = "Vehicle"
|
||||
|
||||
[sessions.filter]
|
||||
allLoadpoints = "all charging points"
|
||||
allVehicles = "all vehicles"
|
||||
filter = "filter"
|
||||
|
||||
[settings]
|
||||
title = "Settings"
|
||||
|
||||
|
|
|
|||
81
package-lock.json
generated
81
package-lock.json
generated
|
|
@ -43,7 +43,8 @@
|
|||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-meta": "^3.0.0-alpha.2",
|
||||
"vue-router": "^4.0.12"
|
||||
"vue-router": "^4.0.12",
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
|
|
@ -2082,6 +2083,19 @@
|
|||
"resolved": "https://registry.npmjs.org/@h2d2/shopicons/-/shopicons-1.8.1.tgz",
|
||||
"integrity": "sha512-vnL8GncQJp+Pqv0yFpIcGvRNzaAXeZDT28tM427dy/e1IShafVNnB9fi9b4sbqlKwsg875vQSq7zUgm36oS9TA=="
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
|
||||
},
|
||||
"node_modules/@hapi/topo": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
||||
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@histoire/app": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@histoire/app/-/app-0.16.1.tgz",
|
||||
|
|
@ -2401,6 +2415,24 @@
|
|||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
|
||||
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/formula": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
|
||||
},
|
||||
"node_modules/@sideway/pinpoint": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||
},
|
||||
"node_modules/@toml-tools/lexer": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@toml-tools/lexer/-/lexer-0.3.1.tgz",
|
||||
|
|
@ -5372,6 +5404,18 @@
|
|||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.9.2",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz",
|
||||
"integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0",
|
||||
"@hapi/topo": "^5.0.0",
|
||||
"@sideway/address": "^4.1.3",
|
||||
"@sideway/formula": "^3.0.1",
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-beautify": {
|
||||
"version": "1.14.6",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.6.tgz",
|
||||
|
|
@ -6596,6 +6640,14 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
|
|
@ -7615,6 +7667,33 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",
|
||||
"integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.7",
|
||||
"rxjs": "^7.8.0"
|
||||
},
|
||||
"bin": {
|
||||
"wait-on": "bin/wait-on"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on/node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@
|
|||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-meta": "^3.0.0-alpha.2",
|
||||
"vue-router": "^4.0.12"
|
||||
"vue-router": "^4.0.12",
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.0.0",
|
||||
|
|
|
|||
|
|
@ -5,22 +5,13 @@ const { defineConfig, devices } = require("@playwright/test");
|
|||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
workers: 1, // run testfiles serially to avoid port and database conflicts
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:7070",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
projects: [
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
const { test, expect } = require("@playwright/test");
|
||||
const { execEvcc } = require("./utils");
|
||||
const { start, stop } = require("./evcc");
|
||||
|
||||
let server;
|
||||
|
||||
test.beforeAll(() => {
|
||||
server = execEvcc("basics.evcc.yaml");
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml");
|
||||
});
|
||||
|
||||
test.afterAll(() => {
|
||||
server.kill();
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
|
|
|||
40
tests/evcc.js
Normal file
40
tests/evcc.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import fs from "fs";
|
||||
import waitOn from "wait-on";
|
||||
import axios from "axios";
|
||||
import { exec, execSync } from "child_process";
|
||||
import playwrightConfig from "../playwright.config";
|
||||
|
||||
const BASE_URL = playwrightConfig.use.baseURL;
|
||||
|
||||
const DB_PATH = "./evcc.db";
|
||||
const BINARY = "./evcc";
|
||||
|
||||
export async function start(config, database) {
|
||||
clean();
|
||||
if (database) {
|
||||
console.log("loading database", { database });
|
||||
execSync(`sqlite3 ${DB_PATH} < tests/${database}`);
|
||||
}
|
||||
console.log("starting evcc", { config });
|
||||
const instance = exec(`EVCC_DATABASE_DSN=${DB_PATH} ${BINARY} --config tests/${config}`);
|
||||
instance.stdout.pipe(process.stdout);
|
||||
instance.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
throw new Error("evcc terminated", code);
|
||||
}
|
||||
});
|
||||
await waitOn({ resources: [BASE_URL] });
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
console.log("shutting down evcc");
|
||||
await axios.post(BASE_URL + "/api/shutdown");
|
||||
clean();
|
||||
}
|
||||
|
||||
function clean() {
|
||||
if (fs.existsSync(DB_PATH)) {
|
||||
console.log("delete database", DB_PATH);
|
||||
fs.unlinkSync(DB_PATH);
|
||||
}
|
||||
}
|
||||
159
tests/sessions.spec.js
Normal file
159
tests/sessions.spec.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
const { test, expect } = require("@playwright/test");
|
||||
const { start, stop } = require("./evcc");
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", "sessions.sql");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("basics", async () => {
|
||||
test("navigation to sessions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const topNavigationButton = await page.getByTestId("topnavigation-button");
|
||||
await topNavigationButton.click();
|
||||
await page
|
||||
.getByTestId("topnavigation-dropdown")
|
||||
.getByRole("link", { name: "Charging sessions" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||
});
|
||||
test("month without data", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
|
||||
});
|
||||
test("month with data", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByRole("heading", { name: "MAY 2023" })).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
||||
await expect(page.getByRole("table")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
|
||||
await expect(page.getByText("Charged 20.0 kWh")).toBeVisible();
|
||||
await expect(page.getByText("Solar 67.3%")).toBeVisible();
|
||||
await expect(page.getByText("Σ Price 5.50 €")).toBeVisible();
|
||||
await expect(page.getByText("Ø Price 27.5 ct/kWh")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("paging", async () => {
|
||||
test("prev/next links", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "April" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "June" })).toBeVisible();
|
||||
});
|
||||
test("next month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByRole("link", { name: "June" }).click();
|
||||
await expect(page.getByRole("heading", { name: "JUNE 2023" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "May" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "July" })).toBeVisible();
|
||||
});
|
||||
test("prev month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await page.getByRole("link", { name: "May" }).click();
|
||||
await expect(page.getByRole("heading", { name: "MAY 2023" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("filter", async () => {
|
||||
test("by vehicle", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await page.getByLabel("all vehicles").selectOption("blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
|
||||
await page.getByLabel("all vehicles").selectOption("weißes Model 3 (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
|
||||
|
||||
await page.getByLabel("all vehicles").selectOption("all vehicles (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by loadpoint", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await page.getByLabel("all charging points").selectOption("Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
|
||||
|
||||
await page.getByLabel("all charging points").selectOption("Garage (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
|
||||
|
||||
await page.getByLabel("all charging points").selectOption("all charging points (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await page.getByLabel("all charging points").selectOption("Carport (3)");
|
||||
await page.getByLabel("all vehicles").selectOption("weißes Model 3 (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
|
||||
|
||||
await page.getByLabel("all vehicles").selectOption("blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint disabled options", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await page.getByLabel("all vehicles").selectOption("blauer e-Golf (2)");
|
||||
const option = page.locator("option[value=Garage]");
|
||||
await expect(option).toHaveAttribute("disabled", "");
|
||||
await expect(option).toHaveText("Garage (0)");
|
||||
});
|
||||
|
||||
test("keep filter when paging", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await page.getByLabel("all charging points").selectOption("Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
await page.getByRole("link", { name: "June" }).click();
|
||||
await page.getByRole("link", { name: "May" }).click();
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("csv export", async () => {
|
||||
test("total export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en"
|
||||
);
|
||||
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en"
|
||||
);
|
||||
});
|
||||
test("monthly export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023&month=5"
|
||||
);
|
||||
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByRole("link", { name: "Download June 2023 CSV" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
23
tests/sessions.sql
Normal file
23
tests/sessions.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
DROP TABLE IF EXISTS `sessions`;
|
||||
CREATE TABLE `sessions` (
|
||||
`id` integer,
|
||||
`created` datetime,
|
||||
`finished` datetime,
|
||||
`loadpoint` text,
|
||||
`identifier` text,
|
||||
`vehicle` text,
|
||||
`meter_start_kwh` real,
|
||||
`meter_end_kwh` real,
|
||||
`charged_kwh` real,
|
||||
`odometer` real,
|
||||
`solar_percentage` real,
|
||||
`price` real,
|
||||
`price_per_kwh` real,
|
||||
`co2_per_kwh` real,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
INSERT INTO `sessions` VALUES (1,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,NULL);
|
||||
INSERT INTO `sessions` VALUES (2,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,2.5,NULL,88.21,0.75,0.3,NULL);
|
||||
INSERT INTO `sessions` VALUES (3,'2023-05-03 16:00:00.0+02:00','2023-05-03 20:00:00.0+02:00','Carport',NULL,'weißes Model 3',NULL,NULL,2.5,NULL,50,0.25,0.1,NULL);
|
||||
INSERT INTO `sessions` VALUES (4,'2023-05-04 22:00:00.0+02:00','2023-05-05 06:00:00.0+02:00','Garage',NULL,'weißes Model 3',NULL,NULL,5,NULL,0,2.5,0.5,NULL);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { exec } from "child_process";
|
||||
|
||||
export function execEvcc(config) {
|
||||
const server = exec(`./evcc --config tests/${config}`);
|
||||
server.stdout.pipe(process.stdout);
|
||||
return server;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue