Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ coverage:
threshold: 5%
patch:
default:
target: auto
target: 80%
informational: false

ignore:
- "**/*.test.ts"
Expand Down
144 changes: 136 additions & 8 deletions src/__tests__/unit/routes/settings.advanced.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "../../../test-utils";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConnectionState } from "@/lib/store";

Expand Down Expand Up @@ -101,6 +102,7 @@ describe("Settings Advanced Route", () => {
// Default mock implementations
mockSettings.mockResolvedValue({
debugLogging: false,
errorReporting: false,
audioScanFeedback: true,
readersAutoDetect: false,
});
Expand Down Expand Up @@ -138,6 +140,16 @@ describe("Settings Advanced Route", () => {
});
});

it("should render error reporting toggle", async () => {
renderComponent();

await waitFor(() => {
expect(
screen.getByText("settings.advanced.errorReporting"),
).toBeInTheDocument();
});
});

it("should render debug logging toggle", async () => {
renderComponent();

Expand Down Expand Up @@ -194,19 +206,22 @@ describe("Settings Advanced Route", () => {

describe("settings updates", () => {
it("should call settingsUpdate when debug logging is toggled", async () => {
mockSettings.mockResolvedValue({ debugLogging: false });
mockSettings.mockResolvedValue({
debugLogging: false,
errorReporting: false,
});

renderComponent();

// Wait for loading to complete (checkboxes to appear)
await waitFor(() => {
const checkboxes = screen.getAllByRole("checkbox");
expect(checkboxes.length).toBeGreaterThanOrEqual(2);
expect(checkboxes.length).toBeGreaterThanOrEqual(3);
});

// Get the first checkbox (debug logging toggle)
// Get the second checkbox (debug logging toggle - after error reporting)
const checkboxes = screen.getAllByRole("checkbox");
const debugLoggingCheckbox = checkboxes[0]!;
const debugLoggingCheckbox = checkboxes[1]!;
fireEvent.click(debugLoggingCheckbox);

await waitFor(() => {
Expand All @@ -225,21 +240,134 @@ describe("Settings Advanced Route", () => {

renderComponent();

// Wait for loading to complete (both checkboxes to appear)
// Wait for loading to complete (all checkboxes to appear)
await waitFor(() => {
const checkboxes = screen.getAllByRole("checkbox");
expect(checkboxes.length).toBeGreaterThanOrEqual(2);
expect(checkboxes.length).toBeGreaterThanOrEqual(3);
});

// Get the second checkbox (show filenames toggle)
// Get the third checkbox (show filenames toggle - after error reporting and debug logging)
const checkboxes = screen.getAllByRole("checkbox");
const showFilenamesCheckbox = checkboxes[1]!;
const showFilenamesCheckbox = checkboxes[2]!;
fireEvent.click(showFilenamesCheckbox);

expect(mockSetShowFilenames).toHaveBeenCalledWith(true);
});
});

describe("error reporting", () => {
it("should show confirmation modal when enabling error reporting", async () => {
const user = userEvent.setup();
mockSettings.mockResolvedValue({
debugLogging: false,
errorReporting: false,
});

renderComponent();

const toggle = await screen.findByRole("checkbox", {
name: /settings.advanced.errorReporting/i,
});
await user.click(toggle);

// Modal should appear
await waitFor(() => {
expect(
screen.getAllByText("settings.advanced.errorReportingConfirmTitle")
.length,
).toBeGreaterThan(0);
expect(
screen.getByText("settings.advanced.errorReportingConfirmText"),
).toBeInTheDocument();
});
});

it("should enable error reporting when confirmed", async () => {
const user = userEvent.setup();
mockSettings.mockResolvedValue({
debugLogging: false,
errorReporting: false,
});

renderComponent();

const toggle = await screen.findByRole("checkbox", {
name: /settings.advanced.errorReporting/i,
});
await user.click(toggle);

// Wait for modal and click confirm
const confirmButton = await screen.findByText("yes");
await user.click(confirmButton);

await waitFor(() => {
expect(mockSettingsUpdate).toHaveBeenCalledWith({
errorReporting: true,
});
});
});

it("should not enable error reporting when cancelled", async () => {
const user = userEvent.setup();
mockSettings.mockResolvedValue({
debugLogging: false,
errorReporting: false,
});

renderComponent();

const toggle = await screen.findByRole("checkbox", {
name: /settings.advanced.errorReporting/i,
});
await user.click(toggle);

// Wait for modal and click cancel
const cancelButton = await screen.findByText("nav.cancel");
await user.click(cancelButton);

// No update should be called when cancelled
expect(mockSettingsUpdate).not.toHaveBeenCalled();
});

it("should disable error reporting without confirmation", async () => {
const user = userEvent.setup();
mockSettings.mockResolvedValue({
debugLogging: false,
errorReporting: true,
});

renderComponent();

const toggle = await screen.findByRole("checkbox", {
name: /settings.advanced.errorReporting/i,
});
await user.click(toggle);

// Should directly call update without showing modal
await waitFor(() => {
expect(mockSettingsUpdate).toHaveBeenCalledWith({
errorReporting: false,
});
});
});

it("should disable error reporting toggle when disconnected", async () => {
mockUseStatusStore.mockImplementation((selector) =>
selector({
...defaultStoreState,
connected: false,
}),
);

renderComponent();

const toggle = await screen.findByRole("checkbox", {
name: /settings.advanced.errorReporting/i,
});
expect(toggle).toBeDisabled();
});
});

describe("connection state", () => {
it("should disable debug logging toggle when disconnected", async () => {
mockUseStatusStore.mockImplementation((selector) =>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export interface HistoryResponse {
export interface SettingsResponse {
runZapScript: boolean;
debugLogging: boolean;
errorReporting: boolean;
audioScanFeedback: boolean;
readersAutoDetect: boolean;
readersScanMode: "tap" | "hold" | "insert";
Expand All @@ -170,6 +171,7 @@ export interface SettingsResponse {

export interface UpdateSettingsRequest {
debugLogging?: boolean;
errorReporting?: boolean;
audioScanFeedback?: boolean;
readersAutoDetect?: boolean;
readersScanMode?: "tap" | "hold" | "insert";
Expand Down
58 changes: 58 additions & 0 deletions src/routes/settings.advanced.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from "react";
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
Expand All @@ -15,6 +16,8 @@ import { BackIcon, NextIcon } from "@/lib/images";
import { HeaderButton } from "@/components/wui/HeaderButton";
import { RestorePuchasesButton } from "@/components/ProPurchase";
import { usePageHeadingFocus } from "@/hooks/usePageHeadingFocus";
import { SlideModal } from "@/components/SlideModal";
import { Button } from "@/components/wui/Button";

export const Route = createFileRoute("/settings/advanced")({
component: AdvancedSettings,
Expand All @@ -28,6 +31,8 @@ function AdvancedSettings() {
const showFilenames = usePreferencesStore((s) => s.showFilenames);
const setShowFilenames = usePreferencesStore((s) => s.setShowFilenames);

const [showErrorReportingModal, setShowErrorReportingModal] = useState(false);

// Determine if we're in a loading state (connecting or fetching data)
const isConnecting =
connectionState === ConnectionState.CONNECTING ||
Expand All @@ -44,6 +49,19 @@ function AdvancedSettings() {
onSuccess: () => refetch(),
});

const handleErrorReportingToggle = (value: boolean) => {
if (value) {
setShowErrorReportingModal(true);
} else {
update.mutate({ errorReporting: false });
}
};

const confirmEnableErrorReporting = () => {
update.mutate({ errorReporting: true });
setShowErrorReportingModal(false);
};

const router = useRouter();
const goBack = () => router.history.back();
const swipeHandlers = useSmartSwipe({
Expand Down Expand Up @@ -71,6 +89,22 @@ function AdvancedSettings() {
}
>
<div className="flex flex-col gap-5">
<ToggleSwitch
label={
<span className="flex items-center">
{t("settings.advanced.errorReporting")}
<SettingHelp
title={t("settings.advanced.errorReporting")}
description={t("settings.advanced.errorReportingHelp")}
/>
</span>
}
value={data?.errorReporting ?? false}
setValue={handleErrorReportingToggle}
disabled={!connected}
loading={isLoading}
/>

<ToggleSwitch
label={
<span className="flex items-center">
Expand Down Expand Up @@ -122,6 +156,30 @@ function AdvancedSettings() {

{Capacitor.isNativePlatform() && <RestorePuchasesButton />}
</div>

<SlideModal
isOpen={showErrorReportingModal}
close={() => setShowErrorReportingModal(false)}
title={t("settings.advanced.errorReportingConfirmTitle")}
>
<div className="flex flex-col gap-4 p-4">
<p className="text-center">
{t("settings.advanced.errorReportingConfirmText")}
</p>
<div className="flex flex-row justify-center gap-4">
<Button
label={t("nav.cancel")}
variant="outline"
onClick={() => setShowErrorReportingModal(false)}
/>
<Button
label={t("yes")}
intent="primary"
onClick={confirmEnableErrorReporting}
/>
</div>
</div>
</SlideModal>
</PageFrame>
);
}
4 changes: 4 additions & 0 deletions src/translations/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@
},
"advanced": {
"title": "Advanced",
"errorReporting": "Error reporting",
"errorReportingHelp": "Enable anonymous error reporting to help identify and fix bugs faster. Reports are anonymized and sent via Sentry. No personal data is collected.",
"errorReportingConfirmTitle": "Enable error reporting?",
"errorReportingConfirmText": "Error reports help us fix bugs faster. Reports are anonymized and sent via Sentry. No personal data is collected.",
"debugLogging": "Debug logging",
"debugLoggingHelp": "Enables more verbose log files on the connected device. Can be useful for troubleshooting issues.",
"showFilenames": "Show filenames for media",
Expand Down