Add notification feedback settings
This commit is contained in:
@@ -50,9 +50,18 @@ const (
|
|||||||
const (
|
const (
|
||||||
maxAttachmentBytes = 10 << 20
|
maxAttachmentBytes = 10 << 20
|
||||||
statusBannerDuration = 2600 * time.Millisecond
|
statusBannerDuration = 2600 * time.Millisecond
|
||||||
|
statusBannerLong = 6 * time.Second
|
||||||
autofillStatusTTL = 12 * time.Second
|
autofillStatusTTL = 12 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type autofillNoticeMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
autofillNoticeAll autofillNoticeMode = "all"
|
||||||
|
autofillNoticeApprovals autofillNoticeMode = "approvals"
|
||||||
|
autofillNoticeSuppressed autofillNoticeMode = "suppressed"
|
||||||
|
)
|
||||||
|
|
||||||
type bannerKind string
|
type bannerKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -138,10 +147,12 @@ type recentRemoteRecord struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type uiPreferences struct {
|
type uiPreferences struct {
|
||||||
GroupControlsHidden bool `json:"groupControlsHidden"`
|
GroupControlsHidden bool `json:"groupControlsHidden"`
|
||||||
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
|
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
|
||||||
HistoryHidden bool `json:"historyHidden"`
|
HistoryHidden bool `json:"historyHidden"`
|
||||||
DenseLayout bool `json:"denseLayout"`
|
DenseLayout bool `json:"denseLayout"`
|
||||||
|
StatusBannerMillis int `json:"statusBannerMillis,omitempty"`
|
||||||
|
AutofillNoticeMode string `json:"autofillNoticeMode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type entriesSectionState struct {
|
type entriesSectionState struct {
|
||||||
@@ -265,6 +276,12 @@ type ui struct {
|
|||||||
toggleHistory widget.Clickable
|
toggleHistory widget.Clickable
|
||||||
togglePasswordInline widget.Clickable
|
togglePasswordInline widget.Clickable
|
||||||
toggleSyncPassword widget.Clickable
|
toggleSyncPassword widget.Clickable
|
||||||
|
setStatusBannerShort widget.Clickable
|
||||||
|
setStatusBannerStandard widget.Clickable
|
||||||
|
setStatusBannerLong widget.Clickable
|
||||||
|
showAllAutofillNotices widget.Clickable
|
||||||
|
showApprovalAutofillOnly widget.Clickable
|
||||||
|
hideAutofillNotices widget.Clickable
|
||||||
showEntries widget.Clickable
|
showEntries widget.Clickable
|
||||||
showTemplates widget.Clickable
|
showTemplates widget.Clickable
|
||||||
showRecycle widget.Clickable
|
showRecycle widget.Clickable
|
||||||
@@ -359,6 +376,8 @@ type ui struct {
|
|||||||
lifecycleAdvancedHidden bool
|
lifecycleAdvancedHidden bool
|
||||||
historyHidden bool
|
historyHidden bool
|
||||||
denseLayout bool
|
denseLayout bool
|
||||||
|
statusBannerTTL time.Duration
|
||||||
|
autofillNoticePreference autofillNoticeMode
|
||||||
recentVaults []string
|
recentVaults []string
|
||||||
recentRemotes []recentRemoteRecord
|
recentRemotes []recentRemoteRecord
|
||||||
recentVaultGroups map[string][]string
|
recentVaultGroups map[string][]string
|
||||||
@@ -482,25 +501,27 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
lifecycleList: widget.List{
|
lifecycleList: widget.List{
|
||||||
List: layout.List{Axis: layout.Vertical},
|
List: layout.List{Axis: layout.Vertical},
|
||||||
},
|
},
|
||||||
state: appstate.State{},
|
state: appstate.State{},
|
||||||
selectedHistoryIndex: -1,
|
selectedHistoryIndex: -1,
|
||||||
selectedAuditIndex: -1,
|
selectedAuditIndex: -1,
|
||||||
lifecycleMode: "local",
|
lifecycleMode: "local",
|
||||||
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
||||||
recentVaultsPath: paths.RecentVaultsPath,
|
recentVaultsPath: paths.RecentVaultsPath,
|
||||||
uiPreferencesPath: paths.UIPreferencesPath,
|
uiPreferencesPath: paths.UIPreferencesPath,
|
||||||
recentRemotesPath: paths.RecentRemotesPath,
|
recentRemotesPath: paths.RecentRemotesPath,
|
||||||
autofillCachePath: paths.AutofillCachePath,
|
autofillCachePath: paths.AutofillCachePath,
|
||||||
recentVaultGroups: map[string][]string{},
|
recentVaultGroups: map[string][]string{},
|
||||||
recentVaultUsedAt: map[string]time.Time{},
|
recentVaultUsedAt: map[string]time.Time{},
|
||||||
lifecycleAdvancedHidden: true,
|
lifecycleAdvancedHidden: true,
|
||||||
historyHidden: true,
|
historyHidden: true,
|
||||||
now: time.Now,
|
statusBannerTTL: statusBannerDuration,
|
||||||
syncSourceMode: syncSourceLocal,
|
now: time.Now,
|
||||||
syncDirection: syncDirectionPull,
|
syncSourceMode: syncSourceLocal,
|
||||||
apiPolicyGroupScope: true,
|
syncDirection: syncDirectionPull,
|
||||||
backgroundResults: make(chan backgroundActionResult, 8),
|
apiPolicyGroupScope: true,
|
||||||
requestMasterPassFocus: true,
|
autofillNoticePreference: autofillNoticeAll,
|
||||||
|
backgroundResults: make(chan backgroundActionResult, 8),
|
||||||
|
requestMasterPassFocus: true,
|
||||||
}
|
}
|
||||||
u.apiPolicyAllow.Value = true
|
u.apiPolicyAllow.Value = true
|
||||||
u.apiPolicyGroupScopeW.Value = true
|
u.apiPolicyGroupScopeW.Value = true
|
||||||
@@ -1299,6 +1320,8 @@ func (u *ui) loadUIPreferences() {
|
|||||||
u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden
|
u.lifecycleAdvancedHidden = prefs.LifecycleAdvancedHidden
|
||||||
u.historyHidden = prefs.HistoryHidden
|
u.historyHidden = prefs.HistoryHidden
|
||||||
u.denseLayout = prefs.DenseLayout
|
u.denseLayout = prefs.DenseLayout
|
||||||
|
u.statusBannerTTL = normalizedStatusBannerTTL(prefs.StatusBannerMillis)
|
||||||
|
u.autofillNoticePreference = normalizedAutofillNoticeMode(prefs.AutofillNoticeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) saveUIPreferences() {
|
func (u *ui) saveUIPreferences() {
|
||||||
@@ -1313,6 +1336,8 @@ func (u *ui) saveUIPreferences() {
|
|||||||
LifecycleAdvancedHidden: u.lifecycleAdvancedHidden,
|
LifecycleAdvancedHidden: u.lifecycleAdvancedHidden,
|
||||||
HistoryHidden: u.historyHidden,
|
HistoryHidden: u.historyHidden,
|
||||||
DenseLayout: u.denseLayout,
|
DenseLayout: u.denseLayout,
|
||||||
|
StatusBannerMillis: int(u.statusBannerTTL / time.Millisecond),
|
||||||
|
AutofillNoticeMode: string(u.autofillNoticePreference),
|
||||||
}, "", " ")
|
}, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -1334,6 +1359,38 @@ func (u *ui) applySettingsFormToPreferences() {
|
|||||||
u.denseLayout = u.settingsDenseLayout.Value
|
u.denseLayout = u.settingsDenseLayout.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizedStatusBannerTTL(valueMillis int) time.Duration {
|
||||||
|
switch {
|
||||||
|
case valueMillis <= 0:
|
||||||
|
return statusBannerDuration
|
||||||
|
case time.Duration(valueMillis)*time.Millisecond > statusBannerLong:
|
||||||
|
return statusBannerLong
|
||||||
|
default:
|
||||||
|
return time.Duration(valueMillis) * time.Millisecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedAutofillNoticeMode(value string) autofillNoticeMode {
|
||||||
|
switch autofillNoticeMode(strings.TrimSpace(value)) {
|
||||||
|
case autofillNoticeApprovals:
|
||||||
|
return autofillNoticeApprovals
|
||||||
|
case autofillNoticeSuppressed:
|
||||||
|
return autofillNoticeSuppressed
|
||||||
|
default:
|
||||||
|
return autofillNoticeAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) setStatusBannerTTL(value time.Duration) {
|
||||||
|
u.statusBannerTTL = normalizedStatusBannerTTL(int(value / time.Millisecond))
|
||||||
|
u.saveUIPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) setAutofillNoticePreference(value autofillNoticeMode) {
|
||||||
|
u.autofillNoticePreference = normalizedAutofillNoticeMode(string(value))
|
||||||
|
u.saveUIPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) {
|
func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) {
|
||||||
baseURL = strings.TrimSpace(baseURL)
|
baseURL = strings.TrimSpace(baseURL)
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
@@ -1705,7 +1762,7 @@ func (u *ui) armDeleteCurrentGroupAction() {
|
|||||||
u.deleteGroupPath = append([]string(nil), u.currentPath...)
|
u.deleteGroupPath = append([]string(nil), u.currentPath...)
|
||||||
u.state.ErrorMessage = ""
|
u.state.ErrorMessage = ""
|
||||||
u.state.StatusMessage = fmt.Sprintf("Confirm deleting empty group %q.", strings.Join(u.displayPath(), " / "))
|
u.state.StatusMessage = fmt.Sprintf("Confirm deleting empty group %q.", strings.Join(u.displayPath(), " / "))
|
||||||
u.statusExpiresAt = u.now().Add(statusBannerDuration)
|
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) runAction(label string, action func() error) {
|
func (u *ui) runAction(label string, action func() error) {
|
||||||
@@ -1732,7 +1789,7 @@ func (u *ui) runAction(label string, action func() error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.state.StatusMessage = label + " complete"
|
u.state.StatusMessage = label + " complete"
|
||||||
u.statusExpiresAt = u.now().Add(statusBannerDuration)
|
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) runBackgroundAction(label string, prepare func() (func() error, error)) {
|
func (u *ui) runBackgroundAction(label string, prepare func() (func() error, error)) {
|
||||||
@@ -1791,7 +1848,7 @@ func (u *ui) applyBackgroundResult(result backgroundActionResult) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.state.StatusMessage = result.label + " complete"
|
u.state.StatusMessage = result.label + " complete"
|
||||||
u.statusExpiresAt = u.now().Add(statusBannerDuration)
|
u.statusExpiresAt = u.now().Add(u.statusBannerTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) cancelLifecycleBusyState() {
|
func (u *ui) cancelLifecycleBusyState() {
|
||||||
@@ -1933,6 +1990,9 @@ func (u *ui) statusToastSurface() uiBanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) autofillStatusSurface() uiAutofillStatus {
|
func (u *ui) autofillStatusSurface() uiAutofillStatus {
|
||||||
|
if u.autofillNoticePreference == autofillNoticeSuppressed {
|
||||||
|
return uiAutofillStatus{}
|
||||||
|
}
|
||||||
if request, ok := u.pendingAutofillApproval(); ok {
|
if request, ok := u.pendingAutofillApproval(); ok {
|
||||||
detail := approvalResourceText(request)
|
detail := approvalResourceText(request)
|
||||||
if strings.TrimSpace(detail) == "" {
|
if strings.TrimSpace(detail) == "" {
|
||||||
@@ -1948,6 +2008,9 @@ func (u *ui) autofillStatusSurface() uiAutofillStatus {
|
|||||||
if u.auditLog == nil {
|
if u.auditLog == nil {
|
||||||
return uiAutofillStatus{}
|
return uiAutofillStatus{}
|
||||||
}
|
}
|
||||||
|
if u.autofillNoticePreference == autofillNoticeApprovals {
|
||||||
|
return uiAutofillStatus{}
|
||||||
|
}
|
||||||
for _, event := range u.auditLog.Events() {
|
for _, event := range u.auditLog.Events() {
|
||||||
if status, ok := autofillStatusFromAuditEvent(event, u.now()); ok {
|
if status, ok := autofillStatusFromAuditEvent(event, u.now()); ok {
|
||||||
return status
|
return status
|
||||||
@@ -2467,6 +2530,24 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
u.loadSettingsFormFromPreferences()
|
u.loadSettingsFormFromPreferences()
|
||||||
u.securityDialogOpen = true
|
u.securityDialogOpen = true
|
||||||
}
|
}
|
||||||
|
for u.setStatusBannerShort.Clicked(gtx) {
|
||||||
|
u.setStatusBannerTTL(2 * time.Second)
|
||||||
|
}
|
||||||
|
for u.setStatusBannerStandard.Clicked(gtx) {
|
||||||
|
u.setStatusBannerTTL(statusBannerDuration)
|
||||||
|
}
|
||||||
|
for u.setStatusBannerLong.Clicked(gtx) {
|
||||||
|
u.setStatusBannerTTL(statusBannerLong)
|
||||||
|
}
|
||||||
|
for u.showAllAutofillNotices.Clicked(gtx) {
|
||||||
|
u.setAutofillNoticePreference(autofillNoticeAll)
|
||||||
|
}
|
||||||
|
for u.showApprovalAutofillOnly.Clicked(gtx) {
|
||||||
|
u.setAutofillNoticePreference(autofillNoticeApprovals)
|
||||||
|
}
|
||||||
|
for u.hideAutofillNotices.Clicked(gtx) {
|
||||||
|
u.setAutofillNoticePreference(autofillNoticeSuppressed)
|
||||||
|
}
|
||||||
for u.closeAdvancedSync.Clicked(gtx) {
|
for u.closeAdvancedSync.Clicked(gtx) {
|
||||||
u.syncDialogOpen = false
|
u.syncDialogOpen = false
|
||||||
u.showSyncPassword = false
|
u.showSyncPassword = false
|
||||||
@@ -2970,7 +3051,7 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior and which KDBX security settings it should use for new or future saves.")
|
lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior, tune noncritical feedback, and set the KDBX security defaults used for new or future saves.")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
@@ -3014,10 +3095,68 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
|
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(syncDialogSectionLabel(u.theme, "Feedback")),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(14), "Success and reminder banners")
|
||||||
|
lbl.Color = accentColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "Choose how long noncritical status banners stay visible.")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return tonedButton(gtx, u.theme, &u.closeSecuritySettings, "Cancel")
|
return syncChoiceButton(gtx, u.theme, &u.setStatusBannerShort, "Short", u.statusBannerTTL == 2*time.Second)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.setStatusBannerStandard, "Standard", u.statusBannerTTL == statusBannerDuration)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.setStatusBannerLong, "Long", u.statusBannerTTL == statusBannerLong)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(14), "Autofill notices")
|
||||||
|
lbl.Color = accentColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "Keep recent autofill results visible, reduce them to approval-only, or hide them.")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showAllAutofillNotices, "All", u.autofillNoticePreference == autofillNoticeAll)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showApprovalAutofillOnly, "Approval Only", u.autofillNoticePreference == autofillNoticeApprovals)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.hideAutofillNotices, "Hidden", u.autofillNoticePreference == autofillNoticeSuppressed)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.closeSecuritySettings, "Close")
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
|||||||
@@ -2914,6 +2914,27 @@ func TestUIStatusToastExpiresAfterTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIStatusToastExpiresAfterConfiguredTimeout(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
now := time.Date(2026, time.March, 29, 12, 0, 0, 0, time.UTC)
|
||||||
|
u := newUIWithModel("desktop", vault.Model{})
|
||||||
|
u.now = func() time.Time { return now }
|
||||||
|
u.statusBannerTTL = statusBannerLong
|
||||||
|
u.state.StatusMessage = "save complete"
|
||||||
|
u.statusExpiresAt = now.Add(u.statusBannerTTL)
|
||||||
|
|
||||||
|
now = now.Add(statusBannerDuration + time.Second)
|
||||||
|
if got := u.statusToastSurface(); got.Kind != bannerStatus {
|
||||||
|
t.Fatalf("statusToastSurface() before configured expiry = %#v, want visible status toast", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
now = now.Add(statusBannerLong)
|
||||||
|
if got := u.statusToastSurface(); got.Kind != bannerNone {
|
||||||
|
t.Fatalf("statusToastSurface() after configured expiry = %#v, want no toast", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIAutofillStatusSurfaceUsesPendingApproval(t *testing.T) {
|
func TestUIAutofillStatusSurfaceUsesPendingApproval(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -2945,6 +2966,41 @@ func TestUIAutofillStatusSurfaceUsesPendingApproval(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIAutofillStatusSurfaceRespectsNoticePreference(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
now := time.Date(2026, time.March, 29, 12, 0, 0, 0, time.UTC)
|
||||||
|
u := newUIWithModel("desktop", vault.Model{})
|
||||||
|
u.now = func() time.Time { return now }
|
||||||
|
u.auditLog = &apiaudit.Log{}
|
||||||
|
u.auditLog.Record(apiaudit.Event{
|
||||||
|
Type: apiaudit.EventAutofillFound,
|
||||||
|
TokenName: "Browser Extension",
|
||||||
|
ClientName: "Firefox",
|
||||||
|
Operation: apitokens.OperationCopyPassword,
|
||||||
|
At: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
u.autofillNoticePreference = autofillNoticeApprovals
|
||||||
|
if got := u.autofillStatusSurface(); got.Kind != autofillStatusNone {
|
||||||
|
t.Fatalf("autofillStatusSurface() with approvals-only preference = %#v, want no recent notice", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.autofillNoticePreference = autofillNoticeSuppressed
|
||||||
|
u.state.Approvals = &mainStubApprovalManager{
|
||||||
|
pending: []apiapproval.Request{{
|
||||||
|
ID: "approval-1",
|
||||||
|
TokenName: "Browser Extension",
|
||||||
|
ClientName: "Firefox",
|
||||||
|
Operation: apitokens.OperationCopyPassword,
|
||||||
|
Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "entry-1"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
if got := u.autofillStatusSurface(); got.Kind != autofillStatusNone {
|
||||||
|
t.Fatalf("autofillStatusSurface() with suppressed preference = %#v, want no notice", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIAutofillStatusSurfaceUsesAuditEventsForFoundAmbiguousAndBlocked(t *testing.T) {
|
func TestUIAutofillStatusSurfaceUsesAuditEventsForFoundAmbiguousAndBlocked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -3502,6 +3558,31 @@ func TestUIEntryRowMetricsUseDenseLayout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUINotificationPreferencesPersist(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||||
|
|
||||||
|
first := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
first.uiPreferencesPath = configPath
|
||||||
|
first.statusBannerTTL = statusBannerLong
|
||||||
|
first.autofillNoticePreference = autofillNoticeApprovals
|
||||||
|
first.saveUIPreferences()
|
||||||
|
|
||||||
|
second := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
second.uiPreferencesPath = configPath
|
||||||
|
second.statusBannerTTL = statusBannerDuration
|
||||||
|
second.autofillNoticePreference = autofillNoticeAll
|
||||||
|
second.loadUIPreferences()
|
||||||
|
|
||||||
|
if got := second.statusBannerTTL; got != statusBannerLong {
|
||||||
|
t.Fatalf("statusBannerTTL after reload = %v, want %v", got, statusBannerLong)
|
||||||
|
}
|
||||||
|
if got := second.autofillNoticePreference; got != autofillNoticeApprovals {
|
||||||
|
t.Fatalf("autofillNoticePreference after reload = %q, want %q", got, autofillNoticeApprovals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
|
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user