diff --git a/assets/js/components/Issue/template.test.ts b/assets/js/components/Issue/template.test.ts index 4181d12c6..0dbe5aa38 100644 --- a/assets/js/components/Issue/template.test.ts +++ b/assets/js/components/Issue/template.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vite-plus/test"; -import { generateGitHubContent } from "./template"; +import { + generateGitHubContent, + generateMailtoUrl, + generateDebugFile, + MAX_MAIL_TITLE_LENGTH, + MAX_MAIL_DESCRIPTION_LENGTH, +} from "./template"; import type { IssueData, Sections } from "./types"; describe("Issue Utils", () => { @@ -123,4 +129,46 @@ Linux/amd64, MST -07:00`); expect(result.body).not.toContain("## Logs"); }); }); + + describe("generateMailtoUrl", () => { + it("generates plaintext mailto url without steps or diagnostics", () => { + const url = generateMailtoUrl("support@example.com", mockIssueData); + + expect(url).toContain("mailto:support@example.com?subject=Test%20Issue&body="); + const body = decodeURIComponent(url.split("&body=")[1]); + expect(body).toBe(`This is a test description + +Version: v1.0.0 + +System: Linux/amd64, MST -07:00`); + }); + + it("stays below the windows mailto limit at maximum input length", () => { + // umlaut-dense german text, each umlaut costs 6 chars percent-encoded + const text = "Fehlermeldung: Ladepunkt überprüfen, Zählerstände größer als üblich. "; + const fill = (length: number) => + text.repeat(Math.ceil(length / text.length)).slice(0, length); + + const url = generateMailtoUrl("support@installer-example.com", { + ...mockIssueData, + title: fill(MAX_MAIL_TITLE_LENGTH), + description: fill(MAX_MAIL_DESCRIPTION_LENGTH), + }); + + expect(url.length).toBeLessThan(2000); + }); + }); + + describe("generateDebugFile", () => { + it("contains version, system and enabled sections", () => { + const content = generateDebugFile(mockIssueData, mockSections); + + expect(content).toContain("# evcc debug information"); + expect(content).toContain("Version: v1.0.0"); + expect(content).toContain("System: Linux/amd64, MST -07:00"); + expect(content).toContain("## Configuration (YAML)"); + expect(content).toContain("## Logs"); + expect(content).not.toContain("## System State"); + }); + }); }); diff --git a/assets/js/components/Issue/template.ts b/assets/js/components/Issue/template.ts index 1620a1663..9f189efa7 100644 --- a/assets/js/components/Issue/template.ts +++ b/assets/js/components/Issue/template.ts @@ -3,6 +3,10 @@ import type { IssueData, Sections, GitHubContent, Template, HelpType } from "./t // Constants const PLACEHOLDER = "⚠️ RETURN TO EVCC TAB → COPY STEP 2 → PASTE HERE"; const MAX_BODY_LENGTH = 8000; +// windows shell silently truncates mailto urls at ~2000 chars; input limits keep +// the percent-encoded url below that even for umlaut-heavy text +export const MAX_MAIL_TITLE_LENGTH = 100; +export const MAX_MAIL_DESCRIPTION_LENGTH = 600; function toString(sections: Template): string { return sections @@ -68,6 +72,26 @@ function generateAdditional(sections: Sections): string { return toString(result); } +// Generates mailto url with plaintext body; diagnostics travel as file attachment instead +export function generateMailtoUrl(email: string, issue: IssueData): string { + const body = toString([ + issue.description, + `Version: ${issue.version}`, + `System: ${issue.system}, ${issue.timezone}`, + ]); + + return `mailto:${email}?subject=${encodeURIComponent(issue.title)}&body=${encodeURIComponent(body)}`; +} + +// Generates text file content with selected diagnostics for manual mail attachment +export function generateDebugFile(issue: IssueData, sections: Sections): string { + return toString([ + "# evcc debug information", + [`Version: ${issue.version}`, `System: ${issue.system}, ${issue.timezone}`], + generateAdditional(sections), + ]); +} + // Generates GitHub URL for issues or discussions export function generateGitHubUrl(type: HelpType, title: string, body: string): string { const baseUrl = diff --git a/assets/js/views/Issue.vue b/assets/js/views/Issue.vue index 25abd8abd..a144dfb43 100644 --- a/assets/js/views/Issue.vue +++ b/assets/js/views/Issue.vue @@ -8,7 +8,7 @@ -
+
{{ $t("issue.helpType.title") }}
@@ -60,14 +60,14 @@
-

+

🇬🇧 Please write your issue in English so everyone can participate.

-
+
@@ -89,12 +90,18 @@ id="issueDescription" v-model="issue.description" class="form-control" - rows="6" + :rows="emailMode ? 12 : 6" placeholder="Describe what you expected to happen and what actually happened..." + :maxlength="descriptionMaxLength" required > +
+ + {{ issue.description.length }} / {{ descriptionMaxLength }} + +
-
+
@@ -149,10 +156,16 @@
* required
+
+ +
-
+
+
{{ $t("issue.additional.title") }}

@@ -289,11 +302,24 @@

+ +
+

{{ $t("issue.downloadHint") }}

+
+ +
+
-
+
@@ -304,6 +330,7 @@ { await expect(email).toHaveAttribute("href", "mailto:support@example.com"); }); + test("issue page uses email flow", async ({ page }) => { + await page.goto("/#/issue"); + + // github-only elements gone + await expect(page.getByRole("radio")).toHaveCount(0); + await expect(page.getByLabel("Steps to Reproduce")).toHaveCount(0); + await expect(page.getByText("Please write your issue in English")).toHaveCount(0); + + await expect(page.getByRole("button", { name: "Send request by email" })).toBeVisible(); + await expect( + page.getByText("Attach this file to your email if more information is needed.") + ).toBeVisible(); + + const downloadButton = page.getByRole("button", { name: "Download debug information" }); + await expect(downloadButton).toBeVisible(); + const downloadPromise = page.waitForEvent("download"); + await downloadButton.click(); + const download = await downloadPromise; + expect(download.suggestedFilename()).toMatch(/^evcc-debug-v.+\.txt$/); + }); + test("help modal shows contact button instead of github discussions", async ({ page }) => { await page.goto("/"); const menu = await openMoreMenu(page);