Compare commits

...

6 Commits

Author SHA1 Message Date
Alexander Brown
a8aa1ec5b0 Merge branch 'main' into glary/fix-logout-unsaved-changes-modal 2026-04-10 01:08:06 -07:00
Alexander Brown
17314a6892 Merge branch 'main' into glary/fix-logout-unsaved-changes-modal 2026-04-08 12:45:20 -07:00
Glary-Bot
f98af98733 test: assert no success toast on cancelled logout 2026-04-07 22:53:22 +00:00
Glary-Bot
e22cc1cb95 fix: use exact match for Save button in E2E tests
The 'Don't Save' button rename caused getByRole('button', { name: 'Save' })
to match both 'Save' and 'Don't Save' (Playwright uses substring matching).
Add exact: true to disambiguate.
2026-04-07 22:46:02 +00:00
Glary-Bot
56536ce52b test: add save-before-logout ordering assertion and failed-save scenario 2026-04-07 21:47:47 +00:00
Glary-Bot
b2becca33d fix: handle Save/Don't Save options in logout unsaved changes dialog
- Fix logout to properly handle 3-state dirtyClose response (save/don't save/cancel)
- Save all modified workflows before logout when user clicks Save
- Rename 'No' button to 'Don't Save' in dirtyClose dialog for clarity
- Update logout dialog message to match new options
2026-04-07 21:31:48 +00:00
7 changed files with 183 additions and 6 deletions

2
.gitignore vendored
View File

@@ -99,4 +99,4 @@ vitest.config.*.timestamp*
# Weekly docs check output
/output.txt
.amp
.amp.glary/

View File

@@ -20,7 +20,7 @@ export class ConfirmDialog {
this.overwrite = this.root.getByRole('button', { name: 'Overwrite' })
this.reject = this.root.getByRole('button', { name: 'Cancel' })
this.confirm = this.root.getByRole('button', { name: 'Confirm' })
this.save = this.root.getByRole('button', { name: 'Save' })
this.save = this.root.getByRole('button', { name: 'Save', exact: true })
}
async click(locator: KeysOfType<ConfirmDialog, Locator>) {

View File

@@ -450,7 +450,10 @@ test.describe('Workflow Persistence', () => {
})
// Click "Save" in the dirty close dialog
const saveButton = comfyPage.page.getByRole('button', { name: 'Save' })
const saveButton = comfyPage.page.getByRole('button', {
name: 'Save',
exact: true
})
await saveButton.waitFor({ state: 'visible' })
await saveButton.click()
await comfyPage.workflow.waitForWorkflowIdle()

View File

@@ -86,7 +86,7 @@
<template v-else-if="type === 'dirtyClose'">
<Button variant="secondary" @click="onDeny">
<i class="pi pi-times" />
{{ $t('g.no') }}
{{ $t('g.dontSave') }}
</Button>
<Button @click="onConfirm">
<i class="pi pi-save" />

View File

@@ -0,0 +1,163 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useAuthActions } from '@/composables/auth/useAuthActions'
const {
mockConfirm,
mockAuthLogout,
mockSaveWorkflow,
mockModifiedWorkflows,
mockToastAdd
} = vi.hoisted(() => ({
mockConfirm: vi.fn(),
mockAuthLogout: vi.fn(),
mockSaveWorkflow: vi.fn(),
mockModifiedWorkflows: { value: [] as Array<{ path: string }> },
mockToastAdd: vi.fn()
}))
vi.mock('@/services/dialogService', () => ({
useDialogService: () => ({
confirm: mockConfirm,
showSignInDialog: vi.fn()
})
}))
vi.mock('@/stores/authStore', () => ({
useAuthStore: () => ({
logout: mockAuthLogout
})
}))
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
useWorkflowStore: () => ({
get modifiedWorkflows() {
return mockModifiedWorkflows.value
}
})
}))
vi.mock('@/platform/workflow/core/services/workflowService', () => ({
useWorkflowService: () => ({
saveWorkflow: mockSaveWorkflow
})
}))
vi.mock('@/platform/updates/common/toastStore', () => ({
useToastStore: () => ({
add: mockToastAdd
})
}))
vi.mock('@/platform/distribution/types', () => ({
isCloud: false
}))
vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => null
}))
vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({
isActiveSubscription: { value: false }
})
}))
describe('useAuthActions', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createTestingPinia())
mockModifiedWorkflows.value = []
mockAuthLogout.mockResolvedValue(undefined)
mockSaveWorkflow.mockResolvedValue(undefined)
})
describe('logout', () => {
it('should log out directly when no modified workflows exist', async () => {
const { logout } = useAuthActions()
await logout()
expect(mockConfirm).not.toHaveBeenCalled()
expect(mockAuthLogout).toHaveBeenCalledOnce()
})
it('should show unsaved changes dialog when modified workflows exist', async () => {
mockModifiedWorkflows.value = [{ path: 'workflow1.json' }]
mockConfirm.mockResolvedValue(false)
const { logout } = useAuthActions()
await logout()
expect(mockConfirm).toHaveBeenCalledWith(
expect.objectContaining({ type: 'dirtyClose' })
)
})
it('should cancel logout when user closes the dialog', async () => {
mockModifiedWorkflows.value = [{ path: 'workflow1.json' }]
mockConfirm.mockResolvedValue(null)
const { logout } = useAuthActions()
await logout()
expect(mockAuthLogout).not.toHaveBeenCalled()
expect(mockSaveWorkflow).not.toHaveBeenCalled()
expect(mockToastAdd).not.toHaveBeenCalled()
})
it('should save all modified workflows then log out when user clicks Save', async () => {
const workflows = [{ path: 'workflow1.json' }, { path: 'workflow2.json' }]
mockModifiedWorkflows.value = workflows
mockConfirm.mockResolvedValue(true)
const { logout } = useAuthActions()
await logout()
expect(mockSaveWorkflow).toHaveBeenCalledTimes(2)
expect(mockSaveWorkflow).toHaveBeenCalledWith(workflows[0])
expect(mockSaveWorkflow).toHaveBeenCalledWith(workflows[1])
expect(mockAuthLogout).toHaveBeenCalledOnce()
const lastSaveOrder = Math.max(
...mockSaveWorkflow.mock.invocationCallOrder
)
const logoutOrder = mockAuthLogout.mock.invocationCallOrder[0]
expect(lastSaveOrder).toBeLessThan(logoutOrder)
})
it('should not log out when saving a workflow fails', async () => {
mockModifiedWorkflows.value = [{ path: 'workflow1.json' }]
mockConfirm.mockResolvedValue(true)
mockSaveWorkflow.mockRejectedValueOnce(new Error('Save failed'))
const { logout } = useAuthActions()
await logout()
expect(mockSaveWorkflow).toHaveBeenCalledOnce()
expect(mockAuthLogout).not.toHaveBeenCalled()
})
it("should log out without saving when user clicks Don't Save", async () => {
mockModifiedWorkflows.value = [{ path: 'workflow1.json' }]
mockConfirm.mockResolvedValue(false)
const { logout } = useAuthActions()
await logout()
expect(mockSaveWorkflow).not.toHaveBeenCalled()
expect(mockAuthLogout).toHaveBeenCalledOnce()
})
it('should show success toast after logout', async () => {
const { logout } = useAuthActions()
await logout()
expect(mockToastAdd).toHaveBeenCalledWith(
expect.objectContaining({ severity: 'success' })
)
})
})
})

View File

@@ -9,6 +9,7 @@ import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useDialogService } from '@/services/dialogService'
import { useAuthStore } from '@/stores/authStore'
@@ -60,7 +61,16 @@ export const useAuthActions = () => {
message: t('auth.signOut.unsavedChangesMessage'),
type: 'dirtyClose'
})
if (!confirmed) return
if (confirmed === null) return
if (confirmed === true) {
const workflowService = useWorkflowService()
const toSave = [...workflowStore.modifiedWorkflows]
for (const workflow of toSave) {
await workflowService.saveWorkflow(workflow)
}
}
}
await authStore.logout()

View File

@@ -129,6 +129,7 @@
"saveAnyway": "Save Anyway",
"saving": "Saving",
"no": "No",
"dontSave": "Don't Save",
"cancel": "Cancel",
"close": "Close",
"closeDialog": "Close dialog",
@@ -2173,7 +2174,7 @@
"success": "Signed out successfully",
"successDetail": "You have been signed out of your account.",
"unsavedChangesTitle": "Unsaved Changes",
"unsavedChangesMessage": "You have unsaved changes that will be lost when you sign out. Do you want to continue?"
"unsavedChangesMessage": "You have unsaved changes that will be lost when you sign out. Would you like to save before signing out?"
},
"passwordUpdate": {
"success": "Password Updated",