feat(ui): add automation settings and safe AI review presentation
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Alert, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
import { useOptionalI18n } from "../i18n/I18nProvider";
|
||||
import { useAccountPlan } from "../accountPlan";
|
||||
|
||||
type Props = {
|
||||
featureKey: string;
|
||||
@@ -15,6 +16,9 @@ function storageKey(featureKey: string) {
|
||||
|
||||
export default function ProFeatureNotice({ featureKey, title, children }: Props) {
|
||||
const i18n = useOptionalI18n();
|
||||
const { plan, canUseAi } = useAccountPlan();
|
||||
const aiUnavailable = plan === "pro" && !canUseAi;
|
||||
const displayTitle = aiUnavailable ? i18n?.t("aiUnavailableTitle") ?? "AI assistance is unavailable" : title;
|
||||
const [dismissed, setDismissed] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
@@ -40,15 +44,15 @@ export default function ProFeatureNotice({ featureKey, title, children }: Props)
|
||||
severity="info"
|
||||
action={(
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<Button href="/settings" size="small">{i18n?.t("proFeatureView") ?? "View Pro"}</Button>
|
||||
<IconButton size="small" aria-label={i18n?.t("proFeatureDismiss", { title }) ?? `Dismiss ${title}`} onClick={dismiss}>
|
||||
<Button href="/settings" size="small">{aiUnavailable ? i18n?.t("aiUnavailableSettings") ?? "Review AI settings" : i18n?.t("proFeatureView") ?? "View Pro"}</Button>
|
||||
<IconButton size="small" aria-label={i18n?.t("proFeatureDismiss", { title: displayTitle }) ?? `Dismiss ${displayTitle}`} onClick={dismiss}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
>
|
||||
<Typography component="span" sx={{ fontWeight: 800 }}>{title}</Typography>{" "}
|
||||
{children}
|
||||
<Typography component="span" sx={{ fontWeight: 800 }}>{displayTitle}</Typography>{" "}
|
||||
{aiUnavailable ? i18n?.t("aiUnavailableBody") ?? "You can continue editing manually. Review your AI settings and usage before trying again." : children}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
Tab,
|
||||
Tabs,
|
||||
Typography,
|
||||
@@ -81,6 +82,7 @@ export default function SettingsView({
|
||||
const rulesSession = useRef({ mounted: true, loading: false, saving: false });
|
||||
const notificationSession = useRef({ mounted: true, loading: false, saving: false, generation: 0 });
|
||||
const notificationBody = useRef<HTMLDivElement>(null);
|
||||
const aiControls = useRef<HTMLDivElement>(null);
|
||||
const notificationRetryFocus = useRef<HTMLElement | null>(null);
|
||||
const currentTranslation = useRef(t);
|
||||
const rulesDirty = Boolean(rulesDraft && savedRules && RULE_FIELDS.some(field => rulesDraft[field] !== String(savedRules[field])));
|
||||
@@ -223,6 +225,36 @@ export default function SettingsView({
|
||||
};
|
||||
const notificationReadStatus = (notificationError?.cause as { response?: { status?: number } } | undefined)?.response?.status;
|
||||
|
||||
const notificationCard = (
|
||||
<PageSection surface title={t("settingsNotificationsTitle")} description={t("settingsNotificationsBody")}>
|
||||
<Box ref={notificationBody} role="group" aria-label={t("settingsNotificationsTitle")} tabIndex={-1} aria-busy={loadingNotifications || savingNotifications} sx={{ minWidth: 0 }}>
|
||||
{!notificationPrefs ? <ViewStateNotice compact loading={loadingNotifications} title={t("settingsNotificationsLoadFailed")} error={notificationError ? {
|
||||
kind: notificationReadStatus === 401 || notificationReadStatus === 403 ? "unauthorized" : "error",
|
||||
message: getApiErrorMessage(notificationError.cause, t("settingsNotificationsLoadFailed")),
|
||||
retryable: notificationReadStatus !== 401 && notificationReadStatus !== 403,
|
||||
} : null} onRetry={retryNotifications} /> : <Box sx={{ display: "grid", gap: 1 }}>
|
||||
{notificationError?.action ? <Alert severity="error" sx={{ mb: 1.5, overflowWrap: "anywhere" }}>{getApiErrorMessage(notificationError.cause, t("settingsNotificationsSaveFailed"))}</Alert> : null}
|
||||
{notificationsDirty ? <Typography role="status" variant="body2" color="text.secondary">{t("assetsUnsavedChanges")}. {t("settingsNotificationsDraftScope")}</Typography> : null}
|
||||
<FormControlLabel
|
||||
control={<Switch checked={notificationPrefs.emailFollowUpRemindersEnabled} onChange={(e) => setNotificationPrefs({ emailFollowUpRemindersEnabled: e.target.checked })} />}
|
||||
label={t("settingsNotificationsFollowUpReminders")}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">{t("settingsNotificationsEmailBody")}</Typography>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||
<Button disabled={!notificationsDirty || savingNotifications} onClick={discardNotifications}>{t("assetsDiscardChanges")}</Button>
|
||||
<Button variant="contained" disabled={savingNotifications} onClick={() => void saveNotifications()}>{savingNotifications ? t("settingsSaving") : t("settingsNotificationsSave")}</Button>
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 1.5 }}>
|
||||
{t("settingsNotificationsDelivery")}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
|
||||
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
|
||||
</Box>
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<FormControl fullWidth sx={{ display: { xs: "flex", sm: "none" }, mb: 1 }}>
|
||||
@@ -234,6 +266,7 @@ export default function SettingsView({
|
||||
onChange={(event) => selectSection(Number(event.target.value))}
|
||||
>
|
||||
<MenuItem value={0}>{t("settingsTabGeneral")}</MenuItem>
|
||||
<MenuItem value={4}>{t("settingsTabAutomation")}</MenuItem>
|
||||
<MenuItem value={1}>{t("settingsTabFollowUps")}</MenuItem>
|
||||
<MenuItem value={2}>{t("settingsTabNotifications")}</MenuItem>
|
||||
<MenuItem value={3}>{t("settingsTabBackup")}</MenuItem>
|
||||
@@ -241,10 +274,11 @@ export default function SettingsView({
|
||||
</FormControl>
|
||||
|
||||
<Tabs aria-label={t("settings")} value={tab} onChange={(_, v) => selectSection(v)} variant="scrollable" scrollButtons="auto" sx={{ display: { xs: "none", sm: "flex" }, mb: 1 }}>
|
||||
<Tab id="settings-tab-0" aria-controls="settings-panel-0" label={t("settingsTabGeneral")} />
|
||||
<Tab id="settings-tab-1" aria-controls="settings-panel-1" label={t("settingsTabFollowUps")} />
|
||||
<Tab id="settings-tab-2" aria-controls="settings-panel-2" label={t("settingsTabNotifications")} />
|
||||
<Tab id="settings-tab-3" aria-controls="settings-panel-3" label={t("settingsTabBackup")} />
|
||||
<Tab value={0} id="settings-tab-0" aria-controls="settings-panel-0" label={t("settingsTabGeneral")} />
|
||||
<Tab value={4} id="settings-tab-4" aria-controls="settings-panel-4" label={t("settingsTabAutomation")} />
|
||||
<Tab value={1} id="settings-tab-1" aria-controls="settings-panel-1" label={t("settingsTabFollowUps")} />
|
||||
<Tab value={2} id="settings-tab-2" aria-controls="settings-panel-2" label={t("settingsTabNotifications")} />
|
||||
<Tab value={3} id="settings-tab-3" aria-controls="settings-panel-3" label={t("settingsTabBackup")} />
|
||||
</Tabs>
|
||||
|
||||
{rulesDirty ? <Typography role="status" variant="body2" sx={{ mt: 1, color: "text.secondary" }}>{t("assetsUnsavedChanges")}. {t("rulesDraftScope")}</Typography> : null}
|
||||
@@ -331,7 +365,7 @@ export default function SettingsView({
|
||||
</Box>
|
||||
</PageSection>
|
||||
|
||||
<AiPrivacySettingsCard />
|
||||
<Box ref={aiControls} id="settings-ai-controls" role="group" aria-label={t("settingsAiPrivacyTitle")} tabIndex={-1}><AiPrivacySettingsCard /></Box>
|
||||
<AiUsageCard />
|
||||
<QuickCaptureCard />
|
||||
<PageSection surface title={t("connectedAccounts")} description={t("settingsConnectedAccountsBody")}
|
||||
@@ -349,33 +383,22 @@ export default function SettingsView({
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tab} index={2} label={t("settingsTabNotifications")}>
|
||||
<PageSection surface title={t("settingsNotificationsTitle")} description={t("settingsNotificationsBody")}>
|
||||
<Box ref={notificationBody} role="group" aria-label={t("settingsNotificationsTitle")} tabIndex={-1} aria-busy={loadingNotifications || savingNotifications} sx={{ minWidth: 0 }}>
|
||||
{!notificationPrefs ? <ViewStateNotice compact loading={loadingNotifications} title={t("settingsNotificationsLoadFailed")} error={notificationError ? {
|
||||
kind: notificationReadStatus === 401 || notificationReadStatus === 403 ? "unauthorized" : "error",
|
||||
message: getApiErrorMessage(notificationError.cause, t("settingsNotificationsLoadFailed")),
|
||||
retryable: notificationReadStatus !== 401 && notificationReadStatus !== 403,
|
||||
} : null} onRetry={retryNotifications} /> : <Box sx={{ display: "grid", gap: 1 }}>
|
||||
{notificationError?.action ? <Alert severity="error" sx={{ mb: 1.5, overflowWrap: "anywhere" }}>{getApiErrorMessage(notificationError.cause, t("settingsNotificationsSaveFailed"))}</Alert> : null}
|
||||
{notificationsDirty ? <Typography role="status" variant="body2" color="text.secondary">{t("assetsUnsavedChanges")}. {t("settingsNotificationsDraftScope")}</Typography> : null}
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={notificationPrefs.emailFollowUpRemindersEnabled} onChange={(e) => setNotificationPrefs({ emailFollowUpRemindersEnabled: e.target.checked })} />}
|
||||
label={t("settingsNotificationsFollowUpReminders")}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">{t("settingsNotificationsEmailBody")}</Typography>
|
||||
{notificationCard}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tab} index={4} label={t("settingsTabAutomation")}>
|
||||
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<PageSection surface title={t("settingsTabAutomation")} description={t("settingsAutomationBody")}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>{t("settingsAutomationEmailSchedule")}</Typography>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||
<Button disabled={!notificationsDirty || savingNotifications} onClick={discardNotifications}>{t("assetsDiscardChanges")}</Button>
|
||||
<Button variant="contained" disabled={savingNotifications} onClick={() => void saveNotifications()}>{savingNotifications ? t("settingsSaving") : t("settingsNotificationsSave")}</Button>
|
||||
<Button onClick={() => { selectSection(0); requestAnimationFrame(() => aiControls.current?.focus()); }}>{t("settingsAutomationAi")}</Button>
|
||||
<Button onClick={() => selectSection(1)}>{t("settingsTabFollowUps")}</Button>
|
||||
<Button onClick={() => navigate("/discover")}>{t("settingsAutomationSearches")}</Button>
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 1.5 }}>
|
||||
{t("settingsNotificationsDelivery")}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
|
||||
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
|
||||
</Box>
|
||||
</PageSection>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>{t("settingsAutomationRulesScope")}</Typography>
|
||||
</PageSection>
|
||||
{notificationCard}
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tab} index={3} label={t("settingsTabBackup")}>
|
||||
|
||||
@@ -1107,6 +1107,16 @@ async function reviewDocumentSuggestion(result: string) {
|
||||
expect(request).not.toHaveProperty('targetId');
|
||||
}
|
||||
|
||||
test('changing whole-document output language discards the previous-language review without applying it', async () => {
|
||||
await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }]));
|
||||
expect(await screen.findByText('Review 1 proposed change(s)')).toBeInTheDocument();
|
||||
fireEvent.mouseDown(screen.getByLabelText('Output language'));
|
||||
fireEvent.click(screen.getByRole('option', { name: 'Norsk bokmål' }));
|
||||
expect(screen.queryByText('Review 1 proposed change(s)')).not.toBeInTheDocument();
|
||||
expect(mockedApi.post.mock.calls.filter(([url]) => url === '/cv/variants/3/ai/apply')).toHaveLength(0);
|
||||
expect(mockedApi.post.mock.calls.filter(([url]) => url === '/cv/ai/assist')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test.each(['Apply', 'Reject'])('[PRES-C11 PRES-C07] whole-document AI %s changes only an explicitly accepted CV override', async action => {
|
||||
await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }]));
|
||||
expect(await screen.findByText('Review 1 proposed change(s)')).toBeInTheDocument();
|
||||
|
||||
@@ -2,6 +2,15 @@ export type UiLanguage = "en" | "nb";
|
||||
|
||||
export const translations = {
|
||||
en: {
|
||||
aiUnavailableTitle: "AI assistance is unavailable",
|
||||
aiUnavailableSettings: "Review AI settings",
|
||||
aiUnavailableBody: "You can continue editing manually. Review your AI settings and usage before trying again.",
|
||||
settingsTabAutomation: "Automation",
|
||||
settingsAutomationBody: "Control email follow-up reminders here. Use the linked settings for AI assistance, waiting periods and individual saved-search alerts.",
|
||||
settingsAutomationEmailSchedule: "When reminder delivery is available, due or upcoming applications are checked about every six hours. Emails go only to your confirmed account address, at most once per application per day.",
|
||||
settingsAutomationAi: "AI privacy and usage",
|
||||
settingsAutomationSearches: "Saved-search alerts",
|
||||
settingsAutomationRulesScope: "Day thresholds influence reminders and optional automatic Ghosted status updates. They do not disable status processing. Saved-search alerts are controlled individually in Discover; AI privacy and usage remain in General.",
|
||||
documentsFilters: "Document filters",
|
||||
documentsJumpResults: "Jump to documents",
|
||||
documentsReturnFilters: "Return to document filters",
|
||||
@@ -3197,6 +3206,15 @@ export const translations = {
|
||||
coverAiReject: "Reject",
|
||||
},
|
||||
nb: {
|
||||
aiUnavailableTitle: "KI-hjelp er utilgjengelig",
|
||||
aiUnavailableSettings: "Se KI-innstillinger",
|
||||
aiUnavailableBody: "Du kan fortsette å redigere manuelt. Se gjennom KI-innstillingene og forbruket ditt før du prøver igjen.",
|
||||
settingsTabAutomation: "Automatisering",
|
||||
settingsAutomationBody: "Styr e-postpåminnelser om oppfølging her. Bruk de tilknyttede innstillingene for AI-hjelp, ventetider og varsler for hvert lagret søk.",
|
||||
settingsAutomationEmailSchedule: "Når påminnelseslevering er tilgjengelig, kontrolleres forfalte eller kommende søknader omtrent hver sjette time. E-post sendes bare til den bekreftede kontoadressen din, høyst én gang per søknad per dag.",
|
||||
settingsAutomationAi: "AI-personvern og forbruk",
|
||||
settingsAutomationSearches: "Varsler for lagrede søk",
|
||||
settingsAutomationRulesScope: "Daggrenser påvirker påminnelser og valgfrie automatiske statusoppdateringer til Ghostet. De deaktiverer ikke statusbehandlingen. Varsler for lagrede søk styres enkeltvis i Finn jobber; AI-personvern og forbruk ligger under Generelt.",
|
||||
documentsFilters: "Dokumentfiltre",
|
||||
documentsJumpResults: "Gå til dokumenter",
|
||||
documentsReturnFilters: "Tilbake til dokumentfiltre",
|
||||
|
||||
@@ -3,9 +3,31 @@ import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
import ProFeatureNotice from "./components/ProFeatureNotice";
|
||||
import { AccountPlanProvider } from "./accountPlan";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
|
||||
beforeEach(() => window.sessionStorage.clear());
|
||||
|
||||
test.each([
|
||||
["en", "AI assistance is unavailable", "Review AI settings"],
|
||||
["nb", "KI-hjelp er utilgjengelig", "Se KI-innstillinger"],
|
||||
])("keeps paid users on the manual path without an upgrade prompt (%s)", (language, title, action) => {
|
||||
window.localStorage.setItem("uiLanguage", language);
|
||||
render(
|
||||
<I18nProvider>
|
||||
<AccountPlanProvider value={{ plan: "pro", canUseAi: false, canUseProThemes: true }}>
|
||||
<ProFeatureNotice featureKey="cv-writing-ai" title="Upgrade prompt">Upgrade body</ProFeatureNotice>
|
||||
</AccountPlanProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
expect(screen.getByText(title)).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: action })).toHaveAttribute("href", "/settings");
|
||||
expect(screen.queryByText("Upgrade body")).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: new RegExp(title) }));
|
||||
expect(screen.queryByText(title)).not.toBeInTheDocument();
|
||||
window.localStorage.removeItem("uiLanguage");
|
||||
});
|
||||
|
||||
test("shows a respectful upgrade action and remembers dismissal for the session", () => {
|
||||
const { unmount } = render(
|
||||
<ProFeatureNotice featureKey="test-feature" title="Draft faster with Pro.">
|
||||
|
||||
@@ -319,12 +319,31 @@ test('[PRES-S03] Rules draft updates do not refetch an unrelated notification pr
|
||||
});
|
||||
|
||||
function preferenceCopy(key: TranslationKey) { return translations[document.documentElement.lang === 'nb-NO' ? 'nb' : 'en'][key]; }
|
||||
test.each(['en', 'nb'] as const)('%s Automation reuses the saved notification preference and explicit save', async language => {
|
||||
localStorage.setItem('uiLanguage', language); renderView();
|
||||
fireEvent.click(screen.getByRole('tab', { name: preferenceCopy('settingsTabAutomation') }));
|
||||
const panel = within(screen.getByRole('tabpanel', { name: preferenceCopy('settingsTabAutomation') }));
|
||||
const toggle = await panel.findByRole('switch', { name: preferenceCopy('settingsNotificationsFollowUpReminders') });
|
||||
expect(toggle).toBeChecked(); fireEvent.click(toggle); expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('tab', { name: preferenceCopy('settingsTabNotifications') }));
|
||||
expect(await notificationPanel().findByRole('switch', { name: preferenceCopy('settingsNotificationsFollowUpReminders') })).not.toBeChecked();
|
||||
fireEvent.click(notificationPanel().getByRole('button', { name: preferenceCopy('settingsNotificationsSave') }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/notification-settings', { emailFollowUpRemindersEnabled: false }));
|
||||
});
|
||||
|
||||
test('Automation AI entry moves focus into existing settings without an AI request', async () => {
|
||||
renderView(); fireEvent.click(screen.getByRole('tab', { name: preferenceCopy('settingsTabAutomation') }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: preferenceCopy('settingsAutomationAi') }));
|
||||
await waitFor(() => expect(document.getElementById('settings-ai-controls')).toHaveFocus());
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function notificationPanel() { return within(screen.getByRole('tabpanel', { name: preferenceCopy('settingsTabNotifications') })); }
|
||||
function notificationReads() { return mockedApi.get.mock.calls.filter(([url]) => url === '/notification-settings'); }
|
||||
function notificationWrites() { return mockedApi.put.mock.calls.filter(([url]) => url === '/notification-settings'); }
|
||||
async function openNotifications() {
|
||||
fireEvent.click(screen.getByRole('tab', { name: preferenceCopy('settingsTabNotifications') }));
|
||||
return notificationPanel().findByRole('checkbox', { name: preferenceCopy('settingsNotificationsFollowUpReminders') });
|
||||
return notificationPanel().findByRole('switch', { name: preferenceCopy('settingsNotificationsFollowUpReminders') });
|
||||
}
|
||||
async function preferenceLanguage(language: 'en' | 'nb') {
|
||||
await act(async () => { window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: language })); });
|
||||
@@ -345,7 +364,7 @@ test('[P7-09 preferences] initial notification failure offers a single GET Retry
|
||||
act(() => { retry.focus(); fireEvent.click(retry); fireEvent.click(retry); });
|
||||
expect(notificationReads()).toHaveLength(2); expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
await act(async () => pending.resolve({ data: { emailFollowUpRemindersEnabled: false } }));
|
||||
expect(await notificationPanel().findByRole('checkbox', { name: translations.en.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(await notificationPanel().findByRole('switch', { name: translations.en.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(notificationPanel().queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
|
||||
expect(notificationPanel().getByRole('group', { name: translations.en.settingsNotificationsTitle })).toHaveFocus();
|
||||
expect(notificationReads()).toHaveLength(2); expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
@@ -359,7 +378,7 @@ test('[P7-09 preferences] notification drafts survive internal sections and loca
|
||||
const retained = await openNotifications();
|
||||
expect(retained).not.toBeChecked(); expect(notificationReads()).toHaveLength(reads);
|
||||
expect(notificationPanel().getByRole('status')).toHaveTextContent(translations.nb.settingsNotificationsDraftScope);
|
||||
await preferenceLanguage('en'); expect(notificationPanel().getByRole('checkbox', { name: translations.en.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
await preferenceLanguage('en'); expect(notificationPanel().getByRole('switch', { name: translations.en.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(notificationReads()).toHaveLength(reads); expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -375,7 +394,7 @@ test('[P7-09 preferences] one held notification save acknowledges its snapshot a
|
||||
expect(await openNotifications()).toBeChecked();
|
||||
expect(notificationPanel().getByRole('status')).toHaveTextContent(translations.en.assetsUnsavedChanges);
|
||||
fireEvent.click(notificationPanel().getByRole('button', { name: translations.en.assetsDiscardChanges }));
|
||||
expect(notificationPanel().getByRole('checkbox', { name: translations.en.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(notificationPanel().getByRole('switch', { name: translations.en.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(notificationPanel().queryByText(translations.en.settingsNotificationsDraftScope)).not.toBeInTheDocument();
|
||||
expect(notificationReads()).toHaveLength(1); expect(notificationWrites()).toHaveLength(1);
|
||||
});
|
||||
@@ -387,7 +406,7 @@ test('[P7-09 preferences] failed notification save keeps its current-locale draf
|
||||
expect(await notificationPanel().findByRole('alert')).toHaveTextContent(translations.en.settingsNotificationsSaveFailed);
|
||||
const reads = notificationReads().length; await preferenceLanguage('nb');
|
||||
expect(notificationPanel().getByRole('alert')).toHaveTextContent(translations.nb.settingsNotificationsSaveFailed);
|
||||
expect(notificationPanel().getByRole('checkbox', { name: translations.nb.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(notificationPanel().getByRole('switch', { name: translations.nb.settingsNotificationsFollowUpReminders })).not.toBeChecked();
|
||||
expect(notificationPanel().queryByRole('button', { name: translations.nb.retry })).not.toBeInTheDocument();
|
||||
fireEvent.click(notificationPanel().getByRole('button', { name: translations.nb.settingsNotificationsSave }));
|
||||
await waitFor(() => expect(notificationPanel().queryByRole('alert')).not.toBeInTheDocument());
|
||||
@@ -403,7 +422,7 @@ test('[P7-09 preferences] discarding a notification draft leaves the independent
|
||||
renderView(); fireEvent.change(await openRules(), { target: { value: '5' } });
|
||||
fireEvent.click(await openNotifications());
|
||||
fireEvent.click(notificationPanel().getByRole('button', { name: translations.en.assetsDiscardChanges }));
|
||||
expect(notificationPanel().getByRole('checkbox', { name: translations.en.settingsNotificationsFollowUpReminders })).toBeChecked();
|
||||
expect(notificationPanel().getByRole('switch', { name: translations.en.settingsNotificationsFollowUpReminders })).toBeChecked();
|
||||
expect(notificationPanel().queryByText(translations.en.settingsNotificationsDraftScope)).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Follow-ups' }));
|
||||
expect(screen.getByLabelText('Applied: follow-up days')).toHaveValue(5);
|
||||
|
||||
@@ -892,7 +892,7 @@ function DocumentAiTab({ settings, outline, onGenerate, onAccept }: CvAiCallback
|
||||
setProposal(null);
|
||||
setBusy(null);
|
||||
return () => { requestId.current += 1; };
|
||||
}, [sourceKey, settings.language, canUseAi]);
|
||||
}, [sourceKey, settings.language, targetLanguage, canUseAi]);
|
||||
|
||||
const discard = () => {
|
||||
requestId.current += 1;
|
||||
|
||||
Reference in New Issue
Block a user