mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-27 07:47:43 +00:00
Compare commits
13 Commits
feat/image
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62979e3818 | ||
|
|
6e249f2e05 | ||
|
|
a1c46d7086 | ||
|
|
dd89b74ca5 | ||
|
|
e809d74192 | ||
|
|
47c9a027a7 | ||
|
|
6fbc5723bd | ||
|
|
db6e5062f2 | ||
|
|
6da5d26980 | ||
|
|
9b6b762a97 | ||
|
|
00c8c11288 | ||
|
|
668f7e48e7 | ||
|
|
3de387429a |
48
browser_tests/assets/widgets/painter_widget.json
Normal file
48
browser_tests/assets/widgets/painter_widget.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Painter",
|
||||
"pos": [50, 50],
|
||||
"size": [450, 550],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Painter"
|
||||
},
|
||||
"widgets_values": ["", 512, 512, "#000000"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -19,10 +19,12 @@ import { ContextMenu } from './components/ContextMenu'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import { BottomPanel } from './components/BottomPanel'
|
||||
import {
|
||||
AssetsSidebarTab,
|
||||
NodeLibrarySidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
import { AssetsHelper } from './helpers/AssetsHelper'
|
||||
import { CanvasHelper } from './helpers/CanvasHelper'
|
||||
import { PerformanceHelper } from './helpers/PerformanceHelper'
|
||||
import { QueueHelper } from './helpers/QueueHelper'
|
||||
@@ -55,6 +57,7 @@ class ComfyPropertiesPanel {
|
||||
}
|
||||
|
||||
class ComfyMenu {
|
||||
private _assetsTab: AssetsSidebarTab | null = null
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||
private _topbar: Topbar | null = null
|
||||
@@ -78,6 +81,11 @@ class ComfyMenu {
|
||||
return this._nodeLibraryTab
|
||||
}
|
||||
|
||||
get assetsTab() {
|
||||
this._assetsTab ??= new AssetsSidebarTab(this.page)
|
||||
return this._assetsTab
|
||||
}
|
||||
|
||||
get workflowsTab() {
|
||||
this._workflowsTab ??= new WorkflowsSidebarTab(this.page)
|
||||
return this._workflowsTab
|
||||
@@ -192,6 +200,7 @@ export class ComfyPage {
|
||||
public readonly command: CommandHelper
|
||||
public readonly bottomPanel: BottomPanel
|
||||
public readonly perf: PerformanceHelper
|
||||
public readonly assets: AssetsHelper
|
||||
public readonly queue: QueueHelper
|
||||
|
||||
/** Worker index to test user ID */
|
||||
@@ -238,6 +247,7 @@ export class ComfyPage {
|
||||
this.command = new CommandHelper(page)
|
||||
this.bottomPanel = new BottomPanel(page)
|
||||
this.perf = new PerformanceHelper(page)
|
||||
this.assets = new AssetsHelper(page)
|
||||
this.queue = new QueueHelper(page)
|
||||
}
|
||||
|
||||
|
||||
@@ -168,3 +168,32 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class AssetsSidebarTab extends SidebarTab {
|
||||
constructor(public override readonly page: Page) {
|
||||
super(page, 'assets')
|
||||
}
|
||||
|
||||
get generatedTab() {
|
||||
return this.page.getByRole('tab', { name: 'Generated' })
|
||||
}
|
||||
|
||||
get importedTab() {
|
||||
return this.page.getByRole('tab', { name: 'Imported' })
|
||||
}
|
||||
|
||||
get emptyStateMessage() {
|
||||
return this.page.getByText(
|
||||
'Upload files or generate content to see them here'
|
||||
)
|
||||
}
|
||||
|
||||
emptyStateTitle(title: string) {
|
||||
return this.page.getByText(title)
|
||||
}
|
||||
|
||||
override async open() {
|
||||
await super.open()
|
||||
await this.generatedTab.waitFor({ state: 'visible' })
|
||||
}
|
||||
}
|
||||
|
||||
147
browser_tests/fixtures/helpers/AssetsHelper.ts
Normal file
147
browser_tests/fixtures/helpers/AssetsHelper.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
import type { RawJobListItem } from '../../../src/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
const jobsListRoutePattern = /\/api\/jobs(?:\?.*)?$/
|
||||
const inputFilesRoutePattern = /\/internal\/files\/input(?:\?.*)?$/
|
||||
|
||||
function parseLimit(url: URL, total: number): number {
|
||||
const value = Number(url.searchParams.get('limit'))
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
return total
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function parseOffset(url: URL): number {
|
||||
const value = Number(url.searchParams.get('offset'))
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function getExecutionDuration(job: RawJobListItem): number {
|
||||
const start = job.execution_start_time ?? 0
|
||||
const end = job.execution_end_time ?? 0
|
||||
return end - start
|
||||
}
|
||||
|
||||
export class AssetsHelper {
|
||||
private jobsRouteHandler: ((route: Route) => Promise<void>) | null = null
|
||||
private inputFilesRouteHandler: ((route: Route) => Promise<void>) | null =
|
||||
null
|
||||
private generatedJobs: RawJobListItem[] = []
|
||||
private importedFiles: string[] = []
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async mockOutputHistory(jobs: RawJobListItem[]): Promise<void> {
|
||||
this.generatedJobs = [...jobs]
|
||||
|
||||
if (this.jobsRouteHandler) {
|
||||
return
|
||||
}
|
||||
|
||||
this.jobsRouteHandler = async (route: Route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const statuses = url.searchParams
|
||||
.get('status')
|
||||
?.split(',')
|
||||
.map((status) => status.trim())
|
||||
.filter(Boolean)
|
||||
const workflowId = url.searchParams.get('workflow_id')
|
||||
const sortBy = url.searchParams.get('sort_by')
|
||||
const sortOrder = url.searchParams.get('sort_order') === 'asc' ? 1 : -1
|
||||
|
||||
let filteredJobs = [...this.generatedJobs]
|
||||
|
||||
if (statuses?.length) {
|
||||
filteredJobs = filteredJobs.filter((job) =>
|
||||
statuses.includes(job.status)
|
||||
)
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
filteredJobs = filteredJobs.filter(
|
||||
(job) => job.workflow_id === workflowId
|
||||
)
|
||||
}
|
||||
|
||||
filteredJobs.sort((left, right) => {
|
||||
const leftValue =
|
||||
sortBy === 'execution_duration'
|
||||
? getExecutionDuration(left)
|
||||
: left.create_time
|
||||
const rightValue =
|
||||
sortBy === 'execution_duration'
|
||||
? getExecutionDuration(right)
|
||||
: right.create_time
|
||||
|
||||
return (leftValue - rightValue) * sortOrder
|
||||
})
|
||||
|
||||
const offset = parseOffset(url)
|
||||
const total = filteredJobs.length
|
||||
const limit = parseLimit(url, total)
|
||||
const visibleJobs = filteredJobs.slice(offset, offset + limit)
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
jobs: visibleJobs,
|
||||
pagination: {
|
||||
offset,
|
||||
limit,
|
||||
total,
|
||||
has_more: offset + visibleJobs.length < total
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await this.page.route(jobsListRoutePattern, this.jobsRouteHandler)
|
||||
}
|
||||
|
||||
async mockInputFiles(files: string[]): Promise<void> {
|
||||
this.importedFiles = [...files]
|
||||
|
||||
if (this.inputFilesRouteHandler) {
|
||||
return
|
||||
}
|
||||
|
||||
this.inputFilesRouteHandler = async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(this.importedFiles)
|
||||
})
|
||||
}
|
||||
|
||||
await this.page.route(inputFilesRoutePattern, this.inputFilesRouteHandler)
|
||||
}
|
||||
|
||||
async mockEmptyState(): Promise<void> {
|
||||
await this.mockOutputHistory([])
|
||||
await this.mockInputFiles([])
|
||||
}
|
||||
|
||||
async clearMocks(): Promise<void> {
|
||||
this.generatedJobs = []
|
||||
this.importedFiles = []
|
||||
|
||||
if (this.jobsRouteHandler) {
|
||||
await this.page.unroute(jobsListRoutePattern, this.jobsRouteHandler)
|
||||
this.jobsRouteHandler = null
|
||||
}
|
||||
|
||||
if (this.inputFilesRouteHandler) {
|
||||
await this.page.unroute(
|
||||
inputFilesRoutePattern,
|
||||
this.inputFilesRouteHandler
|
||||
)
|
||||
this.inputFilesRouteHandler = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,12 @@ export const TestIds = {
|
||||
main: 'graph-canvas',
|
||||
contextMenu: 'canvas-context-menu',
|
||||
toggleMinimapButton: 'toggle-minimap-button',
|
||||
toggleLinkVisibilityButton: 'toggle-link-visibility-button'
|
||||
toggleLinkVisibilityButton: 'toggle-link-visibility-button',
|
||||
zoomControlsButton: 'zoom-controls-button',
|
||||
zoomInAction: 'zoom-in-action',
|
||||
zoomOutAction: 'zoom-out-action',
|
||||
zoomToFitAction: 'zoom-to-fit-action',
|
||||
zoomPercentageInput: 'zoom-percentage-input'
|
||||
},
|
||||
dialogs: {
|
||||
settings: 'settings-dialog',
|
||||
|
||||
92
browser_tests/tests/painter.spec.ts
Normal file
92
browser_tests/tests/painter.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Painter', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test(
|
||||
'Renders canvas and controls',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
const painterWidget = node.locator('.widget-expands')
|
||||
await expect(painterWidget).toBeVisible()
|
||||
|
||||
await expect(painterWidget.locator('canvas')).toBeVisible()
|
||||
await expect(painterWidget.getByText('Brush')).toBeVisible()
|
||||
await expect(painterWidget.getByText('Eraser')).toBeVisible()
|
||||
await expect(painterWidget.getByText('Clear')).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.locator('input[type="color"]').first()
|
||||
).toBeVisible()
|
||||
|
||||
await expect(node).toHaveScreenshot('painter-default-state.png')
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Drawing a stroke changes the canvas',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const canvas = node.locator('.widget-expands canvas')
|
||||
await expect(canvas).toBeVisible()
|
||||
|
||||
const isEmptyBefore = await canvas.evaluate((el) => {
|
||||
const ctx = (el as HTMLCanvasElement).getContext('2d')
|
||||
if (!ctx) return true
|
||||
const data = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
(el as HTMLCanvasElement).width,
|
||||
(el as HTMLCanvasElement).height
|
||||
)
|
||||
return data.data.every((v, i) => (i % 4 === 3 ? v === 0 : true))
|
||||
})
|
||||
expect(isEmptyBefore).toBe(true)
|
||||
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not found')
|
||||
|
||||
await comfyPage.page.mouse.move(
|
||||
box.x + box.width * 0.3,
|
||||
box.y + box.height * 0.5
|
||||
)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(
|
||||
box.x + box.width * 0.7,
|
||||
box.y + box.height * 0.5,
|
||||
{ steps: 10 }
|
||||
)
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(async () => {
|
||||
const hasContent = await canvas.evaluate((el) => {
|
||||
const ctx = (el as HTMLCanvasElement).getContext('2d')
|
||||
if (!ctx) return false
|
||||
const data = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
(el as HTMLCanvasElement).width,
|
||||
(el as HTMLCanvasElement).height
|
||||
)
|
||||
for (let i = 3; i < data.data.length; i += 4) {
|
||||
if (data.data[i] > 0) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
expect(hasContent).toBe(true)
|
||||
}).toPass()
|
||||
|
||||
await expect(node).toHaveScreenshot('painter-after-stroke.png')
|
||||
}
|
||||
)
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
30
browser_tests/tests/sidebar/assets.spec.ts
Normal file
30
browser_tests/tests/sidebar/assets.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Assets sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.assets.mockEmptyState()
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.assets.clearMocks()
|
||||
})
|
||||
|
||||
test('Shows empty-state copy for generated and imported tabs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.emptyStateTitle('No generated files found')).toBeVisible()
|
||||
await expect(tab.emptyStateMessage).toBeVisible()
|
||||
|
||||
await tab.importedTab.click()
|
||||
|
||||
await expect(tab.emptyStateTitle('No imported files found')).toBeVisible()
|
||||
await expect(tab.emptyStateMessage).toBeVisible()
|
||||
})
|
||||
})
|
||||
138
browser_tests/tests/zoomControls.spec.ts
Normal file
138
browser_tests/tests/zoomControls.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { TestIds } from '../fixtures/selectors'
|
||||
|
||||
test.describe('Zoom Controls', { tag: '@canvas' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.page.waitForFunction(() => window.app && window.app.canvas)
|
||||
})
|
||||
|
||||
test('Default zoom is 100% and node has a size', async ({ comfyPage }) => {
|
||||
const nodeSize = await comfyPage.page.evaluate(
|
||||
() => window.app!.graph.nodes[0].size
|
||||
)
|
||||
expect(nodeSize[0]).toBeGreaterThan(0)
|
||||
expect(nodeSize[1]).toBeGreaterThan(0)
|
||||
|
||||
const zoomButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.zoomControlsButton
|
||||
)
|
||||
await expect(zoomButton).toContainText('100%')
|
||||
|
||||
const scale = await comfyPage.canvasOps.getScale()
|
||||
expect(scale).toBeCloseTo(1.0, 1)
|
||||
})
|
||||
|
||||
test('Zoom to fit reduces percentage', async ({ comfyPage }) => {
|
||||
const zoomButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.zoomControlsButton
|
||||
)
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const zoomToFit = comfyPage.page.getByTestId(TestIds.canvas.zoomToFitAction)
|
||||
await expect(zoomToFit).toBeVisible()
|
||||
await zoomToFit.click()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.canvasOps.getScale(), { timeout: 2000 })
|
||||
.toBeLessThan(1.0)
|
||||
|
||||
await expect(zoomButton).not.toContainText('100%')
|
||||
})
|
||||
|
||||
test('Zoom out reduces percentage', async ({ comfyPage }) => {
|
||||
const initialScale = await comfyPage.canvasOps.getScale()
|
||||
|
||||
const zoomButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.zoomControlsButton
|
||||
)
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const zoomOut = comfyPage.page.getByTestId(TestIds.canvas.zoomOutAction)
|
||||
await zoomOut.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newScale = await comfyPage.canvasOps.getScale()
|
||||
expect(newScale).toBeLessThan(initialScale)
|
||||
})
|
||||
|
||||
test('Zoom out clamps at 10% minimum', async ({ comfyPage }) => {
|
||||
const zoomButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.zoomControlsButton
|
||||
)
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const zoomOut = comfyPage.page.getByTestId(TestIds.canvas.zoomOutAction)
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await zoomOut.click()
|
||||
}
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.canvasOps.getScale(), { timeout: 2000 })
|
||||
.toBeCloseTo(0.1, 1)
|
||||
|
||||
await expect(zoomButton).toContainText('10%')
|
||||
})
|
||||
|
||||
test('Manual percentage entry allows zoom in and zoom out', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const zoomButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.zoomControlsButton
|
||||
)
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const input = comfyPage.page
|
||||
.getByTestId(TestIds.canvas.zoomPercentageInput)
|
||||
.locator('input')
|
||||
await input.focus()
|
||||
await comfyPage.page.keyboard.press('Control+a')
|
||||
await input.pressSequentially('100')
|
||||
await input.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.canvasOps.getScale(), { timeout: 2000 })
|
||||
.toBeCloseTo(1.0, 1)
|
||||
|
||||
const zoomIn = comfyPage.page.getByTestId(TestIds.canvas.zoomInAction)
|
||||
await zoomIn.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const scaleAfterZoomIn = await comfyPage.canvasOps.getScale()
|
||||
expect(scaleAfterZoomIn).toBeGreaterThan(1.0)
|
||||
|
||||
const zoomOut = comfyPage.page.getByTestId(TestIds.canvas.zoomOutAction)
|
||||
await zoomOut.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const scaleAfterZoomOut = await comfyPage.canvasOps.getScale()
|
||||
expect(scaleAfterZoomOut).toBeLessThan(scaleAfterZoomIn)
|
||||
})
|
||||
|
||||
test('Clicking zoom button toggles zoom controls visibility', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const zoomButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.zoomControlsButton
|
||||
)
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const zoomToFit = comfyPage.page.getByTestId(TestIds.canvas.zoomToFitAction)
|
||||
await expect(zoomToFit).toBeVisible()
|
||||
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(zoomToFit).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.43.6",
|
||||
"version": "1.43.7",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
@theme {
|
||||
--shadow-interface: var(--interface-panel-box-shadow);
|
||||
|
||||
--text-2xs: 0.625rem;
|
||||
--text-2xs--line-height: calc(1 / 0.625);
|
||||
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
|
||||
25
src/App.vue
25
src/App.vue
@@ -7,17 +7,15 @@
|
||||
<script setup lang="ts">
|
||||
import { captureException } from '@sentry/vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { isDesktop } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { parsePreloadError } from '@/utils/preloadErrorUtil'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -129,26 +127,5 @@ onMounted(() => {
|
||||
// Initialize conflict detection in background
|
||||
// This runs async and doesn't block UI setup
|
||||
void conflictDetection.initializeConflictDetection()
|
||||
|
||||
// Show cloud notification for macOS desktop users (one-time)
|
||||
if (isDesktop && electronAPI()?.getPlatform() === 'darwin') {
|
||||
const settingStore = useSettingStore()
|
||||
if (!settingStore.get('Comfy.Desktop.CloudNotificationShown')) {
|
||||
const dialogService = useDialogService()
|
||||
cloudNotificationTimer = setTimeout(async () => {
|
||||
try {
|
||||
await dialogService.showCloudNotification()
|
||||
} catch (e) {
|
||||
console.warn('[CloudNotification] Failed to show', e)
|
||||
}
|
||||
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let cloudNotificationTimer: ReturnType<typeof setTimeout> | undefined
|
||||
onUnmounted(() => {
|
||||
if (cloudNotificationTimer) clearTimeout(cloudNotificationTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -71,8 +71,8 @@ vi.mock('@/workbench/extensions/manager/composables/useManagerState', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
currentUser: null,
|
||||
loading: false
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
@@ -33,7 +33,7 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
function createWrapper(initialBatchCount = 1) {
|
||||
function renderComponent(initialBatchCount = 1) {
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
@@ -44,7 +44,9 @@ function createWrapper(initialBatchCount = 1) {
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mount(BatchCountEdit, {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(BatchCountEdit, {
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
directives: {
|
||||
@@ -55,44 +57,42 @@ function createWrapper(initialBatchCount = 1) {
|
||||
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
|
||||
return { wrapper, queueSettingsStore }
|
||||
return { user, queueSettingsStore }
|
||||
}
|
||||
|
||||
describe('BatchCountEdit', () => {
|
||||
it('doubles the current batch count when increment is clicked', async () => {
|
||||
const { wrapper, queueSettingsStore } = createWrapper(3)
|
||||
const { user, queueSettingsStore } = renderComponent(3)
|
||||
|
||||
await wrapper.get('button[aria-label="Increment"]').trigger('click')
|
||||
await user.click(screen.getByRole('button', { name: 'Increment' }))
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(6)
|
||||
})
|
||||
|
||||
it('halves the current batch count when decrement is clicked', async () => {
|
||||
const { wrapper, queueSettingsStore } = createWrapper(9)
|
||||
const { user, queueSettingsStore } = renderComponent(9)
|
||||
|
||||
await wrapper.get('button[aria-label="Decrement"]').trigger('click')
|
||||
await user.click(screen.getByRole('button', { name: 'Decrement' }))
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(4)
|
||||
})
|
||||
|
||||
it('clamps typed values to queue limits on blur', async () => {
|
||||
const { wrapper, queueSettingsStore } = createWrapper(2)
|
||||
const input = wrapper.get('input')
|
||||
const { user, queueSettingsStore } = renderComponent(2)
|
||||
const input = screen.getByRole('textbox', { name: 'Batch Count' })
|
||||
|
||||
await input.setValue('999')
|
||||
await input.trigger('blur')
|
||||
await nextTick()
|
||||
await user.clear(input)
|
||||
await user.type(input, '999')
|
||||
await user.tab()
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(maxBatchCount)
|
||||
expect((input.element as HTMLInputElement).value).toBe(
|
||||
String(maxBatchCount)
|
||||
)
|
||||
expect(input).toHaveValue(String(maxBatchCount))
|
||||
|
||||
await input.setValue('0')
|
||||
await input.trigger('blur')
|
||||
await nextTick()
|
||||
await user.clear(input)
|
||||
await user.type(input, '0')
|
||||
await user.tab()
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(1)
|
||||
expect((input.element as HTMLInputElement).value).toBe('1')
|
||||
expect(input).toHaveValue('1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
@@ -20,15 +20,15 @@ const configureSettings = (
|
||||
})
|
||||
}
|
||||
|
||||
const mountActionbar = (showRunProgressBar: boolean) => {
|
||||
const renderActionbar = (showRunProgressBar: boolean) => {
|
||||
const topMenuContainer = document.createElement('div')
|
||||
document.body.appendChild(topMenuContainer)
|
||||
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn })
|
||||
configureSettings(pinia, showRunProgressBar)
|
||||
|
||||
const wrapper = mount(ComfyActionbar, {
|
||||
attachTo: document.body,
|
||||
render(ComfyActionbar, {
|
||||
container: document.body.appendChild(document.createElement('div')),
|
||||
props: {
|
||||
topMenuContainer,
|
||||
queueOverlayExpanded: false
|
||||
@@ -57,10 +57,7 @@ const mountActionbar = (showRunProgressBar: boolean) => {
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
topMenuContainer
|
||||
}
|
||||
return { topMenuContainer }
|
||||
}
|
||||
|
||||
describe('ComfyActionbar', () => {
|
||||
@@ -70,31 +67,33 @@ describe('ComfyActionbar', () => {
|
||||
})
|
||||
|
||||
it('teleports inline progress when run progress bar is enabled', async () => {
|
||||
const { wrapper, topMenuContainer } = mountActionbar(true)
|
||||
const { topMenuContainer } = renderActionbar(true)
|
||||
|
||||
try {
|
||||
await nextTick()
|
||||
|
||||
/* eslint-disable testing-library/no-node-access -- Teleport target verification requires scoping to the container element */
|
||||
expect(
|
||||
topMenuContainer.querySelector('[data-testid="queue-inline-progress"]')
|
||||
).not.toBeNull()
|
||||
/* eslint-enable testing-library/no-node-access */
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
topMenuContainer.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not teleport inline progress when run progress bar is disabled', async () => {
|
||||
const { wrapper, topMenuContainer } = mountActionbar(false)
|
||||
const { topMenuContainer } = renderActionbar(false)
|
||||
|
||||
try {
|
||||
await nextTick()
|
||||
|
||||
/* eslint-disable testing-library/no-node-access -- Teleport target verification requires scoping to the container element */
|
||||
expect(
|
||||
topMenuContainer.querySelector('[data-testid="queue-inline-progress"]')
|
||||
).toBeNull()
|
||||
/* eslint-enable testing-library/no-node-access */
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
topMenuContainer.remove()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
|
||||
<Tag v-if="item.isBlueprint" value="Blueprint" severity="primary" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
<i v-if="isActive" class="pi pi-angle-down text-2xs"></i>
|
||||
</div>
|
||||
<Menu
|
||||
v-if="isActive || isRoot"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -68,80 +69,75 @@ describe('BuilderFooterToolbar', () => {
|
||||
mockState.settingView = false
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
return mount(BuilderFooterToolbar, {
|
||||
function renderComponent() {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(BuilderFooterToolbar, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: { Button: false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getButtons(wrapper: ReturnType<typeof mountComponent>) {
|
||||
const buttons = wrapper.findAll('button')
|
||||
return {
|
||||
exit: buttons[0],
|
||||
back: buttons[1],
|
||||
next: buttons[2]
|
||||
}
|
||||
return { user }
|
||||
}
|
||||
|
||||
it('disables back on the first step', () => {
|
||||
mockState.mode = 'builder:inputs'
|
||||
const { back } = getButtons(mountComponent())
|
||||
expect(back.attributes('disabled')).toBeDefined()
|
||||
renderComponent()
|
||||
expect(screen.getByRole('button', { name: /back/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('enables back on the second step', () => {
|
||||
mockState.mode = 'builder:arrange'
|
||||
const { back } = getButtons(mountComponent())
|
||||
expect(back.attributes('disabled')).toBeUndefined()
|
||||
renderComponent()
|
||||
expect(screen.getByRole('button', { name: /back/i })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('disables next on the setDefaultView step', () => {
|
||||
mockState.settingView = true
|
||||
const { next } = getButtons(mountComponent())
|
||||
expect(next.attributes('disabled')).toBeDefined()
|
||||
renderComponent()
|
||||
expect(screen.getByRole('button', { name: /next/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('disables next on arrange step when no outputs', () => {
|
||||
mockState.mode = 'builder:arrange'
|
||||
mockHasOutputs.value = false
|
||||
const { next } = getButtons(mountComponent())
|
||||
expect(next.attributes('disabled')).toBeDefined()
|
||||
renderComponent()
|
||||
expect(screen.getByRole('button', { name: /next/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('enables next on inputs step', () => {
|
||||
mockState.mode = 'builder:inputs'
|
||||
const { next } = getButtons(mountComponent())
|
||||
expect(next.attributes('disabled')).toBeUndefined()
|
||||
renderComponent()
|
||||
expect(screen.getByRole('button', { name: /next/i })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('calls setMode on back click', async () => {
|
||||
mockState.mode = 'builder:arrange'
|
||||
const { back } = getButtons(mountComponent())
|
||||
await back.trigger('click')
|
||||
const { user } = renderComponent()
|
||||
await user.click(screen.getByRole('button', { name: /back/i }))
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:outputs')
|
||||
})
|
||||
|
||||
it('calls setMode on next click from inputs step', async () => {
|
||||
mockState.mode = 'builder:inputs'
|
||||
const { next } = getButtons(mountComponent())
|
||||
await next.trigger('click')
|
||||
const { user } = renderComponent()
|
||||
await user.click(screen.getByRole('button', { name: /next/i }))
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:outputs')
|
||||
})
|
||||
|
||||
it('opens default view dialog on next click from arrange step', async () => {
|
||||
mockState.mode = 'builder:arrange'
|
||||
const { next } = getButtons(mountComponent())
|
||||
await next.trigger('click')
|
||||
const { user } = renderComponent()
|
||||
await user.click(screen.getByRole('button', { name: /next/i }))
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:arrange')
|
||||
expect(mockShowDialog).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('calls exitBuilder on exit button click', async () => {
|
||||
const { exit } = getButtons(mountComponent())
|
||||
await exit.trigger('click')
|
||||
const { user } = renderComponent()
|
||||
await user.click(screen.getByRole('button', { name: /exit app builder/i }))
|
||||
expect(mockExitBuilder).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import ColorCustomizationSelector from './ColorCustomizationSelector.vue'
|
||||
@@ -14,13 +15,17 @@ describe('ColorCustomizationSelector', () => {
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup PrimeVue
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
return mount(ColorCustomizationSelector, {
|
||||
function renderComponent(
|
||||
props: Record<string, unknown> = {},
|
||||
callbacks: { 'onUpdate:modelValue'?: (value: string | null) => void } = {}
|
||||
) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(ColorCustomizationSelector, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { SelectButton, ColorPicker }
|
||||
@@ -28,102 +33,123 @@ describe('ColorCustomizationSelector', () => {
|
||||
props: {
|
||||
modelValue: null,
|
||||
colorOptions,
|
||||
...props
|
||||
...props,
|
||||
...callbacks
|
||||
}
|
||||
})
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
/** PrimeVue SelectButton renders toggle buttons with aria-pressed */
|
||||
function getToggleButtons(container: Element) {
|
||||
return container.querySelectorAll<HTMLButtonElement>( // eslint-disable-line testing-library/no-node-access -- PrimeVue SelectButton renders toggle buttons without standard ARIA radiogroup roles
|
||||
'[data-pc-name="pctogglebutton"]'
|
||||
)
|
||||
}
|
||||
|
||||
it('renders predefined color options and custom option', () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
|
||||
expect(selectButton.props('options')).toHaveLength(colorOptions.length + 1)
|
||||
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
|
||||
const { container } = renderComponent()
|
||||
expect(getToggleButtons(container)).toHaveLength(colorOptions.length + 1)
|
||||
})
|
||||
|
||||
it('initializes with predefined color when provided', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '#0d6efd'
|
||||
})
|
||||
|
||||
const { container } = renderComponent({ modelValue: '#0d6efd' })
|
||||
await nextTick()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
expect(selectButton.props('modelValue')).toEqual({
|
||||
name: 'Blue',
|
||||
value: '#0d6efd'
|
||||
})
|
||||
|
||||
const buttons = getToggleButtons(container)
|
||||
expect(buttons[0]).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
it('initializes with custom color when non-predefined color provided', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '#123456'
|
||||
})
|
||||
|
||||
const { container } = renderComponent({ modelValue: '#123456' })
|
||||
await nextTick()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
const colorPicker = wrapper.findComponent(ColorPicker)
|
||||
|
||||
expect(selectButton.props('modelValue').name).toBe('_custom')
|
||||
expect(colorPicker.props('modelValue')).toBe('123456')
|
||||
const buttons = getToggleButtons(container)
|
||||
const customButton = buttons[buttons.length - 1]
|
||||
expect(customButton).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue ColorPicker uses readonly input preview with no ARIA role
|
||||
const colorPreview = container.querySelector(
|
||||
'.p-colorpicker-preview'
|
||||
) as HTMLInputElement | null
|
||||
expect(colorPreview).not.toBeNull()
|
||||
})
|
||||
|
||||
it('shows color picker when custom option is selected', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
const { container, user } = renderComponent()
|
||||
|
||||
// Select custom option
|
||||
await selectButton.setValue({ name: '_custom', value: '' })
|
||||
const buttons = getToggleButtons(container)
|
||||
await user.click(buttons[buttons.length - 1])
|
||||
|
||||
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
|
||||
expect(
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue ColorPicker internal DOM
|
||||
container.querySelector('[data-pc-name="colorpicker"]')
|
||||
).not.toBeNull()
|
||||
})
|
||||
|
||||
it('emits update when predefined color is selected', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
const onUpdate = vi.fn()
|
||||
const { container, user } = renderComponent(
|
||||
{},
|
||||
{ 'onUpdate:modelValue': onUpdate }
|
||||
)
|
||||
|
||||
await selectButton.setValue(colorOptions[0])
|
||||
const buttons = getToggleButtons(container)
|
||||
await user.click(buttons[0])
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
|
||||
expect(onUpdate).toHaveBeenCalledWith('#0d6efd')
|
||||
})
|
||||
|
||||
it('emits update when custom color is changed', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
const onUpdate = vi.fn()
|
||||
const { container, user } = renderComponent(
|
||||
{},
|
||||
{ 'onUpdate:modelValue': onUpdate }
|
||||
)
|
||||
|
||||
// Select custom option
|
||||
await selectButton.setValue({ name: '_custom', value: '' })
|
||||
// Custom is already selected by default (modelValue: null)
|
||||
// Select Blue first, then switch to custom so onUpdate fires for Blue
|
||||
const buttons = getToggleButtons(container)
|
||||
await user.click(buttons[0]) // Select Blue
|
||||
expect(onUpdate).toHaveBeenCalledWith('#0d6efd')
|
||||
|
||||
// Change custom color
|
||||
const colorPicker = wrapper.findComponent(ColorPicker)
|
||||
await colorPicker.setValue('ff0000')
|
||||
onUpdate.mockClear()
|
||||
await user.click(buttons[buttons.length - 1]) // Switch to custom
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
|
||||
// When switching to custom, the custom color value inherits from Blue ('0d6efd')
|
||||
// and the watcher on customColorValue emits the update
|
||||
expect(onUpdate).toHaveBeenCalledWith('#0d6efd')
|
||||
})
|
||||
|
||||
it('inherits color from previous selection when switching to custom', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
const onUpdate = vi.fn()
|
||||
const { container, user } = renderComponent(
|
||||
{},
|
||||
{ 'onUpdate:modelValue': onUpdate }
|
||||
)
|
||||
|
||||
// First select a predefined color
|
||||
await selectButton.setValue(colorOptions[0])
|
||||
const buttons = getToggleButtons(container)
|
||||
|
||||
// Then switch to custom
|
||||
await selectButton.setValue({ name: '_custom', value: '' })
|
||||
// First select Blue
|
||||
await user.click(buttons[0])
|
||||
expect(onUpdate).toHaveBeenCalledWith('#0d6efd')
|
||||
|
||||
const colorPicker = wrapper.findComponent(ColorPicker)
|
||||
expect(colorPicker.props('modelValue')).toBe('0d6efd')
|
||||
onUpdate.mockClear()
|
||||
|
||||
// Then switch to custom — inherits the Blue color
|
||||
await user.click(buttons[buttons.length - 1])
|
||||
|
||||
// The customColorValue watcher fires with the inherited Blue value
|
||||
expect(onUpdate).toHaveBeenCalledWith('#0d6efd')
|
||||
})
|
||||
|
||||
it('handles null modelValue correctly', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: null
|
||||
})
|
||||
|
||||
const { container } = renderComponent({ modelValue: null })
|
||||
await nextTick()
|
||||
const selectButton = wrapper.findComponent(SelectButton)
|
||||
expect(selectButton.props('modelValue')).toEqual({
|
||||
name: '_custom',
|
||||
value: ''
|
||||
})
|
||||
|
||||
const buttons = getToggleButtons(container)
|
||||
const customButton = buttons[buttons.length - 1]
|
||||
expect(customButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,140 +1,120 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { fireEvent, render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import EditableText from './EditableText.vue'
|
||||
|
||||
describe('EditableText', () => {
|
||||
beforeAll(() => {
|
||||
// Create a Vue app instance for PrimeVue
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
function renderComponent(
|
||||
props: { modelValue: string; isEditing?: boolean },
|
||||
callbacks: {
|
||||
onEdit?: (...args: unknown[]) => void
|
||||
onCancel?: (...args: unknown[]) => void
|
||||
} = {}
|
||||
) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const mountComponent = (props, options = {}) => {
|
||||
return mount(EditableText, {
|
||||
render(EditableText, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { InputText }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
props: {
|
||||
...props,
|
||||
...(callbacks.onEdit && { onEdit: callbacks.onEdit }),
|
||||
...(callbacks.onCancel && { onCancel: callbacks.onCancel })
|
||||
}
|
||||
})
|
||||
|
||||
return { user }
|
||||
}
|
||||
|
||||
it('renders span with modelValue when not editing', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Test Text',
|
||||
isEditing: false
|
||||
})
|
||||
expect(wrapper.find('span').text()).toBe('Test Text')
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(false)
|
||||
renderComponent({ modelValue: 'Test Text', isEditing: false })
|
||||
expect(screen.getByText('Test Text')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders input with modelValue when editing', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Test Text',
|
||||
isEditing: true
|
||||
})
|
||||
expect(wrapper.find('span').exists()).toBe(false)
|
||||
expect(wrapper.findComponent(InputText).props()['modelValue']).toBe(
|
||||
'Test Text'
|
||||
)
|
||||
renderComponent({ modelValue: 'Test Text', isEditing: true })
|
||||
expect(screen.queryByText('Test Text')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox')).toHaveValue('Test Text')
|
||||
})
|
||||
|
||||
it('emits edit event when input is submitted', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Test Text',
|
||||
isEditing: true
|
||||
})
|
||||
await wrapper.findComponent(InputText).setValue('New Text')
|
||||
await wrapper.findComponent(InputText).trigger('keydown.enter')
|
||||
// Blur event should have been triggered
|
||||
expect(wrapper.findComponent(InputText).element).not.toBe(
|
||||
document.activeElement
|
||||
const onEdit = vi.fn()
|
||||
const { user } = renderComponent(
|
||||
{ modelValue: 'Test Text', isEditing: true },
|
||||
{ onEdit }
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.clear(input)
|
||||
await user.type(input, 'New Text')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(onEdit).toHaveBeenCalledWith('New Text')
|
||||
})
|
||||
|
||||
it('finishes editing on blur', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Test Text',
|
||||
isEditing: true
|
||||
})
|
||||
await wrapper.findComponent(InputText).trigger('blur')
|
||||
expect(wrapper.emitted('edit')).toBeTruthy()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(wrapper.emitted('edit')[0]).toEqual(['Test Text'])
|
||||
const onEdit = vi.fn()
|
||||
renderComponent({ modelValue: 'Test Text', isEditing: true }, { onEdit })
|
||||
|
||||
await fireEvent.blur(screen.getByRole('textbox'))
|
||||
|
||||
expect(onEdit).toHaveBeenCalledWith('Test Text')
|
||||
})
|
||||
|
||||
it('cancels editing on escape key', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Original Text',
|
||||
isEditing: true
|
||||
})
|
||||
|
||||
// Change the input value
|
||||
await wrapper.findComponent(InputText).setValue('Modified Text')
|
||||
|
||||
// Press escape
|
||||
await wrapper.findComponent(InputText).trigger('keydown.escape')
|
||||
|
||||
// Should emit cancel event
|
||||
expect(wrapper.emitted('cancel')).toBeTruthy()
|
||||
|
||||
// Should NOT emit edit event
|
||||
expect(wrapper.emitted('edit')).toBeFalsy()
|
||||
|
||||
// Input value should be reset to original
|
||||
expect(wrapper.findComponent(InputText).props()['modelValue']).toBe(
|
||||
'Original Text'
|
||||
const onEdit = vi.fn()
|
||||
const onCancel = vi.fn()
|
||||
const { user } = renderComponent(
|
||||
{ modelValue: 'Original Text', isEditing: true },
|
||||
{ onEdit, onCancel }
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.clear(input)
|
||||
await user.type(input, 'Modified Text')
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
expect(onEdit).not.toHaveBeenCalled()
|
||||
expect(input).toHaveValue('Original Text')
|
||||
})
|
||||
|
||||
it('does not save changes when escape is pressed and blur occurs', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'Original Text',
|
||||
isEditing: true
|
||||
})
|
||||
it('does not save changes when escape is pressed', async () => {
|
||||
const onEdit = vi.fn()
|
||||
const onCancel = vi.fn()
|
||||
const { user } = renderComponent(
|
||||
{ modelValue: 'Original Text', isEditing: true },
|
||||
{ onEdit, onCancel }
|
||||
)
|
||||
|
||||
// Change the input value
|
||||
await wrapper.findComponent(InputText).setValue('Modified Text')
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.clear(input)
|
||||
await user.type(input, 'Modified Text')
|
||||
// Escape triggers cancelEditing → blur internally, so no separate blur needed
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
// Press escape (which triggers blur internally)
|
||||
await wrapper.findComponent(InputText).trigger('keydown.escape')
|
||||
|
||||
// Manually trigger blur to simulate the blur that happens after escape
|
||||
await wrapper.findComponent(InputText).trigger('blur')
|
||||
|
||||
// Should emit cancel but not edit
|
||||
expect(wrapper.emitted('cancel')).toBeTruthy()
|
||||
expect(wrapper.emitted('edit')).toBeFalsy()
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
expect(onEdit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves changes on enter but not on escape', async () => {
|
||||
// Test Enter key saves changes
|
||||
const enterWrapper = mountComponent({
|
||||
modelValue: 'Original Text',
|
||||
isEditing: true
|
||||
})
|
||||
await enterWrapper.findComponent(InputText).setValue('Saved Text')
|
||||
await enterWrapper.findComponent(InputText).trigger('keydown.enter')
|
||||
// Trigger blur that happens after enter
|
||||
await enterWrapper.findComponent(InputText).trigger('blur')
|
||||
expect(enterWrapper.emitted('edit')).toBeTruthy()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(enterWrapper.emitted('edit')[0]).toEqual(['Saved Text'])
|
||||
const onEditEnter = vi.fn()
|
||||
const { user: userEnter } = renderComponent(
|
||||
{ modelValue: 'Original Text', isEditing: true },
|
||||
{ onEdit: onEditEnter }
|
||||
)
|
||||
|
||||
// Test Escape key cancels changes with a fresh wrapper
|
||||
const escapeWrapper = mountComponent({
|
||||
modelValue: 'Original Text',
|
||||
isEditing: true
|
||||
})
|
||||
await escapeWrapper.findComponent(InputText).setValue('Cancelled Text')
|
||||
await escapeWrapper.findComponent(InputText).trigger('keydown.escape')
|
||||
expect(escapeWrapper.emitted('cancel')).toBeTruthy()
|
||||
expect(escapeWrapper.emitted('edit')).toBeFalsy()
|
||||
const enterInput = screen.getByRole('textbox')
|
||||
await userEnter.clear(enterInput)
|
||||
await userEnter.type(enterInput, 'Saved Text')
|
||||
await userEnter.keyboard('{Enter}')
|
||||
|
||||
expect(onEditEnter).toHaveBeenCalledWith('Saved Text')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { fireEvent, render, screen } from '@testing-library/vue'
|
||||
import Badge from 'primevue/badge'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
@@ -12,7 +12,6 @@ import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { InjectKeyHandleEditLabelFunction } from '@/types/treeExplorerTypes'
|
||||
|
||||
// Create a mock i18n instance
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
@@ -33,7 +32,6 @@ describe('TreeExplorerTreeNode', () => {
|
||||
const mockHandleEditLabel = vi.fn()
|
||||
|
||||
beforeAll(() => {
|
||||
// Create a Vue app instance for PrimeVuePrimeVue
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
vi.useFakeTimers()
|
||||
@@ -44,7 +42,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
})
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
render(TreeExplorerTreeNode, {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge },
|
||||
@@ -55,18 +53,16 @@ describe('TreeExplorerTreeNode', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('.tree-node').exists()).toBe(true)
|
||||
expect(wrapper.find('.tree-folder').exists()).toBe(true)
|
||||
expect(wrapper.find('.tree-leaf').exists()).toBe(false)
|
||||
expect(wrapper.findComponent(EditableText).props('modelValue')).toBe(
|
||||
'Test Node'
|
||||
)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(wrapper.findComponent(Badge).props()['value'].toString()).toBe('3')
|
||||
const treeNode = screen.getByTestId('tree-node-1')
|
||||
expect(treeNode).toBeInTheDocument()
|
||||
expect(treeNode).toHaveClass('tree-folder')
|
||||
expect(treeNode).not.toHaveClass('tree-leaf')
|
||||
expect(screen.getByText('Test Node')).toBeInTheDocument()
|
||||
expect(screen.getByText('3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('makes node label editable when renamingEditingNode matches', async () => {
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
it('makes node label editable when isEditingLabel is true', () => {
|
||||
render(TreeExplorerTreeNode, {
|
||||
props: {
|
||||
node: {
|
||||
...mockNode,
|
||||
@@ -82,14 +78,13 @@ describe('TreeExplorerTreeNode', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
expect(editableText.props('isEditing')).toBe(true)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('triggers handleEditLabel callback when editing is finished', async () => {
|
||||
const handleEditLabelMock = vi.fn()
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
render(TreeExplorerTreeNode, {
|
||||
props: {
|
||||
node: {
|
||||
...mockNode,
|
||||
@@ -103,8 +98,9 @@ describe('TreeExplorerTreeNode', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
// Trigger blur on the input to finish editing (fires the 'edit' event)
|
||||
await fireEvent.blur(screen.getByRole('textbox'))
|
||||
|
||||
expect(handleEditLabelMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,64 +1,66 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { fireEvent, render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import UrlInput from './UrlInput.vue'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
describe('UrlInput', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (
|
||||
function renderComponent(
|
||||
props: ComponentProps<typeof UrlInput> & {
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
},
|
||||
options = {}
|
||||
) => {
|
||||
return mount(UrlInput, {
|
||||
'onUpdate:modelValue'?: (value: string) => void
|
||||
}
|
||||
) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(UrlInput, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { IconField, InputIcon, InputText }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
props
|
||||
})
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
it('passes through additional attributes to input element', () => {
|
||||
const wrapper = mountComponent({
|
||||
renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
disabled: true
|
||||
})
|
||||
|
||||
expect(wrapper.find('input').attributes('disabled')).toBe('')
|
||||
expect(screen.getByRole('textbox')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('emits update:modelValue on blur', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const onUpdate = vi.fn()
|
||||
const { user } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL'
|
||||
placeholder: 'Enter URL',
|
||||
'onUpdate:modelValue': onUpdate
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('https://test.com/')
|
||||
await input.trigger('blur')
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.type(input, 'https://test.com/')
|
||||
expect(onUpdate).not.toHaveBeenCalled()
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
|
||||
'https://test.com/'
|
||||
])
|
||||
await user.tab()
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(onUpdate).toHaveBeenCalledWith('https://test.com/')
|
||||
})
|
||||
|
||||
it('renders spinner when validation is loading', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () =>
|
||||
@@ -67,43 +69,46 @@ describe('UrlInput', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-spinner').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-spinner class with no ARIA role
|
||||
expect(container.querySelector('.pi-spinner')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders check icon when validation is valid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-check class with no ARIA role
|
||||
expect(container.querySelector('.pi-check')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders cross icon when validation is invalid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(false)
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-times').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-times class with no ARIA role
|
||||
expect(container.querySelector('.pi-times')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('validates on mount', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container } = renderComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
@@ -111,12 +116,13 @@ describe('UrlInput', () => {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-check class with no ARIA role
|
||||
expect(container.querySelector('.pi-check')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('triggers validation when clicking the validation icon', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
const { container, user } = renderComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
@@ -129,7 +135,9 @@ describe('UrlInput', () => {
|
||||
await nextTick()
|
||||
|
||||
// Click the validation icon
|
||||
await wrapper.find('.pi-check').trigger('click')
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-check class with no ARIA role
|
||||
const icon = container.querySelector('.pi-check')!
|
||||
await user.click(icon)
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
@@ -138,7 +146,7 @@ describe('UrlInput', () => {
|
||||
|
||||
it('prevents multiple simultaneous validations', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender, user } = renderComponent({
|
||||
modelValue: '',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
@@ -148,14 +156,16 @@ describe('UrlInput', () => {
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// Trigger multiple validations in quick succession
|
||||
await wrapper.find('.pi-spinner').trigger('click')
|
||||
await wrapper.find('.pi-spinner').trigger('click')
|
||||
await wrapper.find('.pi-spinner').trigger('click')
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon
|
||||
const spinner = container.querySelector('.pi-spinner')!
|
||||
await user.click(spinner)
|
||||
await user.click(spinner)
|
||||
await user.click(spinner)
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
@@ -165,55 +175,49 @@ describe('UrlInput', () => {
|
||||
|
||||
describe('input cleaning functionality', () => {
|
||||
it('trims whitespace when user types', async () => {
|
||||
const wrapper = mountComponent({
|
||||
renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL'
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
// Test leading whitespace
|
||||
await input.setValue(' https://leading-space.com')
|
||||
await input.trigger('input')
|
||||
// The component strips whitespace on input via handleInput
|
||||
// We use fireEvent.input to simulate the input event handler directly
|
||||
await fireEvent.update(input, ' https://leading-space.com')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://leading-space.com')
|
||||
expect(input).toHaveValue('https://leading-space.com')
|
||||
|
||||
// Test trailing whitespace
|
||||
await input.setValue('https://trailing-space.com ')
|
||||
await input.trigger('input')
|
||||
await fireEvent.update(input, 'https://trailing-space.com ')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://trailing-space.com')
|
||||
expect(input).toHaveValue('https://trailing-space.com')
|
||||
|
||||
// Test both leading and trailing whitespace
|
||||
await input.setValue(' https://both-spaces.com ')
|
||||
await input.trigger('input')
|
||||
await fireEvent.update(input, ' https://both-spaces.com ')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://both-spaces.com')
|
||||
expect(input).toHaveValue('https://both-spaces.com')
|
||||
|
||||
// Test whitespace in the middle of the URL
|
||||
await input.setValue('https:// middle-space.com')
|
||||
await input.trigger('input')
|
||||
await fireEvent.update(input, 'https:// middle-space.com')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://middle-space.com')
|
||||
expect(input).toHaveValue('https://middle-space.com')
|
||||
})
|
||||
|
||||
it('trims whitespace when value set externally', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { rerender } = renderComponent({
|
||||
modelValue: ' https://initial-value.com ',
|
||||
placeholder: 'Enter URL'
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
// Check initial value is trimmed
|
||||
expect(input.element.value).toBe('https://initial-value.com')
|
||||
expect(input).toHaveValue('https://initial-value.com')
|
||||
|
||||
// Update props with whitespace
|
||||
await wrapper.setProps({ modelValue: ' https://updated-value.com ' })
|
||||
await rerender({ modelValue: ' https://updated-value.com ' })
|
||||
await nextTick()
|
||||
|
||||
// Check updated value is trimmed
|
||||
expect(input.element.value).toBe('https://updated-value.com')
|
||||
expect(input).toHaveValue('https://updated-value.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,8 +32,8 @@ const mockBalance = vi.hoisted(() => ({
|
||||
|
||||
const mockIsFetchingBalance = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
balance: mockBalance.value,
|
||||
isFetchingBalance: mockIsFetchingBalance.value
|
||||
}))
|
||||
|
||||
@@ -30,14 +30,14 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const { textClass, showCreditsOnly } = defineProps<{
|
||||
textClass?: string
|
||||
showCreditsOnly?: boolean
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import {
|
||||
configValueOrDefault,
|
||||
@@ -167,7 +167,7 @@ const { onSuccess } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
|
||||
@@ -156,7 +156,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { creditsToUsd, usdToCredits } from '@/base/credits/comfyCredits'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import FormattedNumberStepper from '@/components/ui/stepper/FormattedNumberStepper.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
@@ -171,7 +171,7 @@ const { isInsufficientCredits = false } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const dialogStore = useDialogStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
@@ -21,10 +21,10 @@ import { ref } from 'vue'
|
||||
|
||||
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { updatePasswordSchema } from '@/schemas/signInSchema'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const loading = ref(false)
|
||||
|
||||
const { onSuccess } = defineProps<{
|
||||
|
||||
@@ -116,12 +116,12 @@ import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
interface CreditHistoryItemData {
|
||||
@@ -133,8 +133,8 @@ interface CreditHistoryItemData {
|
||||
|
||||
const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
|
||||
@@ -18,8 +18,8 @@ import ApiKeyForm from './ApiKeyForm.vue'
|
||||
const mockStoreApiKey = vi.fn()
|
||||
const mockLoading = vi.fn(() => false)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
loading: mockLoading()
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -100,9 +100,9 @@ import {
|
||||
} from '@/platform/remoteConfig/remoteConfig'
|
||||
import { apiKeySchema } from '@/schemas/signInSchema'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const comfyPlatformBaseUrl = computed(() =>
|
||||
|
||||
@@ -35,15 +35,15 @@ vi.mock('firebase/auth', () => ({
|
||||
|
||||
// Mock the auth composables and stores
|
||||
const mockSendPasswordReset = vi.fn()
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
sendPasswordReset: mockSendPasswordReset
|
||||
}))
|
||||
}))
|
||||
|
||||
let mockLoading = false
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
get loading() {
|
||||
return mockLoading
|
||||
}
|
||||
|
||||
@@ -88,14 +88,14 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const authStore = useAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const toast = useToast()
|
||||
|
||||
@@ -127,6 +127,6 @@ const handleForgotPassword = async (
|
||||
document.getElementById(emailInputId)?.focus?.()
|
||||
return
|
||||
}
|
||||
await firebaseAuthActions.sendPasswordReset(email)
|
||||
await authActions.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -54,12 +54,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { signUpSchema } from '@/schemas/signInSchema'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
import PasswordFields from './PasswordFields.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between rounded-sm px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
|
||||
data-testid="zoom-in-action"
|
||||
@mousedown="startRepeat('Comfy.Canvas.ZoomIn')"
|
||||
@mouseup="stopRepeat"
|
||||
@mouseleave="stopRepeat"
|
||||
@@ -23,6 +24,7 @@
|
||||
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between rounded-sm px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
|
||||
data-testid="zoom-out-action"
|
||||
@mousedown="startRepeat('Comfy.Canvas.ZoomOut')"
|
||||
@mouseup="stopRepeat"
|
||||
@mouseleave="stopRepeat"
|
||||
@@ -35,6 +37,7 @@
|
||||
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between rounded-sm px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
|
||||
data-testid="zoom-to-fit-action"
|
||||
@click="executeCommand('Comfy.Canvas.FitView')"
|
||||
>
|
||||
<span class="font-medium">{{ $t('zoomControls.zoomToFit') }}</span>
|
||||
@@ -46,6 +49,7 @@
|
||||
<div
|
||||
ref="zoomInputContainer"
|
||||
class="zoomInputContainer flex items-center gap-1 rounded-sm bg-input-surface p-2"
|
||||
data-testid="zoom-percentage-input"
|
||||
>
|
||||
<InputNumber
|
||||
:default-value="canvasStore.appScalePercentage"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
@@ -29,9 +31,9 @@ describe('BypassButton', () => {
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
selectionToolbox: {
|
||||
bypassButton: {
|
||||
tooltip: 'Toggle bypass mode'
|
||||
commands: {
|
||||
Comfy_Canvas_ToggleSelectedNodes_Bypass: {
|
||||
label: 'Toggle bypass mode'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +48,10 @@ describe('BypassButton', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(BypassButton, {
|
||||
function renderComponent() {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(BypassButton, {
|
||||
global: {
|
||||
plugins: [i18n, PrimeVue],
|
||||
directives: { tooltip: Tooltip },
|
||||
@@ -56,28 +60,28 @@ describe('BypassButton', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { user }
|
||||
}
|
||||
|
||||
it('should render bypass button', () => {
|
||||
canvasStore.selectedItems = [getMockLGraphNode()]
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
renderComponent()
|
||||
expect(screen.getByTestId('bypass-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct test id', () => {
|
||||
canvasStore.selectedItems = [getMockLGraphNode()]
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('[data-testid="bypass-button"]')
|
||||
expect(button.exists()).toBe(true)
|
||||
renderComponent()
|
||||
expect(screen.getByTestId('bypass-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should execute bypass command when clicked', async () => {
|
||||
canvasStore.selectedItems = [getMockLGraphNode()]
|
||||
const executeSpy = vi.spyOn(commandStore, 'execute').mockResolvedValue()
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await wrapper.find('button').trigger('click')
|
||||
const { user } = renderComponent()
|
||||
await user.click(screen.getByTestId('bypass-button'))
|
||||
|
||||
expect(executeSpy).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.ToggleSelectedNodes.Bypass'
|
||||
@@ -90,21 +94,18 @@ describe('BypassButton', () => {
|
||||
})
|
||||
canvasStore.selectedItems = [bypassedNode]
|
||||
vi.spyOn(commandStore, 'execute').mockResolvedValue()
|
||||
const wrapper = mountComponent()
|
||||
const { user } = renderComponent()
|
||||
|
||||
// Click to trigger the reactivity update
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
await user.click(screen.getByTestId('bypass-button'))
|
||||
await nextTick()
|
||||
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(screen.getByTestId('bypass-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiple selected items', () => {
|
||||
vi.spyOn(commandStore, 'execute').mockResolvedValue()
|
||||
canvasStore.selectedItems = [getMockLGraphNode(), getMockLGraphNode()]
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
expect(button.exists()).toBe(true)
|
||||
renderComponent()
|
||||
expect(screen.getByTestId('bypass-button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<!-- Description -->
|
||||
<p
|
||||
v-if="nodeDef.description"
|
||||
class="m-0 text-[11px] leading-normal font-normal text-muted-foreground"
|
||||
class="m-0 text-2xs/normal font-normal text-muted-foreground"
|
||||
>
|
||||
{{ nodeDef.description }}
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="flex items-center gap-1 text-[11px] text-muted-foreground"
|
||||
class="flex items-center gap-1 text-2xs text-muted-foreground"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render, screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -28,54 +29,59 @@ describe('SidebarIcon', () => {
|
||||
selected: false
|
||||
}
|
||||
|
||||
const mountSidebarIcon = (props: Partial<SidebarIconProps>, options = {}) => {
|
||||
return mount(SidebarIcon, {
|
||||
function renderSidebarIcon(props: Partial<SidebarIconProps> = {}) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(SidebarIcon, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props: { ...exampleProps, ...props },
|
||||
...options
|
||||
props: { ...exampleProps, ...props }
|
||||
})
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
it('renders button element', () => {
|
||||
const wrapper = mountSidebarIcon({})
|
||||
expect(wrapper.find('button.side-bar-button').exists()).toBe(true)
|
||||
renderSidebarIcon()
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders icon', () => {
|
||||
const wrapper = mountSidebarIcon({})
|
||||
expect(wrapper.find('.side-bar-button-icon').exists()).toBe(true)
|
||||
const { container } = renderSidebarIcon()
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Icon escape hatch: iconify icons have no ARIA role
|
||||
expect(container.querySelector('.side-bar-button-icon')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('creates badge when iconBadge prop is set', () => {
|
||||
const badge = '2'
|
||||
const wrapper = mountSidebarIcon({ iconBadge: badge })
|
||||
const badgeEl = wrapper.find('.sidebar-icon-badge')
|
||||
expect(badgeEl.exists()).toBe(true)
|
||||
expect(badgeEl.text()).toEqual(badge)
|
||||
renderSidebarIcon({ iconBadge: badge })
|
||||
expect(screen.getByText(badge)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows tooltip on hover', async () => {
|
||||
const tooltipShowDelay = 300
|
||||
const tooltipText = 'Settings'
|
||||
const wrapper = mountSidebarIcon({ tooltip: tooltipText })
|
||||
const { user } = renderSidebarIcon({ tooltip: tooltipText })
|
||||
|
||||
const tooltipElBeforeHover = document.querySelector('[role="tooltip"]')
|
||||
expect(tooltipElBeforeHover).toBeNull()
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
|
||||
// Hover over the icon
|
||||
await wrapper.trigger('mouseenter')
|
||||
await new Promise((resolve) => setTimeout(resolve, tooltipShowDelay + 16))
|
||||
await user.hover(screen.getByRole('button'))
|
||||
|
||||
const tooltipElAfterHover = document.querySelector('[role="tooltip"]')
|
||||
expect(tooltipElAfterHover).not.toBeNull()
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('sets aria-label attribute when tooltip is provided', () => {
|
||||
const tooltipText = 'Settings'
|
||||
const wrapper = mountSidebarIcon({ tooltip: tooltipText })
|
||||
expect(wrapper.attributes('aria-label')).toEqual(tooltipText)
|
||||
renderSidebarIcon({ tooltip: tooltipText })
|
||||
expect(screen.getByRole('button')).toHaveAttribute(
|
||||
'aria-label',
|
||||
tooltipText
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
v-if="shouldShowBadge"
|
||||
:class="
|
||||
cn(
|
||||
'sidebar-icon-badge absolute min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] leading-[14px] font-medium text-base-foreground',
|
||||
'sidebar-icon-badge absolute min-w-[16px] rounded-full bg-primary-background py-0.25 text-2xs leading-[14px] font-medium text-base-foreground',
|
||||
badgeClass || '-top-1 -right-1'
|
||||
)
|
||||
"
|
||||
@@ -42,7 +42,7 @@
|
||||
</slot>
|
||||
<span
|
||||
v-if="label && !isSmall"
|
||||
class="side-bar-button-label text-center text-[10px]"
|
||||
class="side-bar-button-label text-center text-2xs"
|
||||
>{{ st(label, label) }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ const modelDef = props.modelDef
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin: 5px;
|
||||
font-size: 10px;
|
||||
font-size: var(--text-2xs);
|
||||
}
|
||||
.model_preview_prefix {
|
||||
font-weight: 700;
|
||||
|
||||
@@ -61,10 +61,10 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the useFirebaseAuthActions composable
|
||||
// Mock the useAuthActions composable
|
||||
const mockLogout = vi.fn()
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
fetchBalance: vi.fn().mockResolvedValue(undefined),
|
||||
logout: mockLogout
|
||||
}))
|
||||
@@ -77,7 +77,7 @@ vi.mock('@/services/dialogService', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the firebaseAuthStore with hoisted state for per-test manipulation
|
||||
// Mock the authStore with hoisted state for per-test manipulation
|
||||
const mockAuthStoreState = vi.hoisted(() => ({
|
||||
balance: {
|
||||
amount_micros: 100_000,
|
||||
@@ -91,8 +91,8 @@ const mockAuthStoreState = vi.hoisted(() => ({
|
||||
isFetchingBalance: false
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ Authorization: 'Bearer mock-token' }),
|
||||
|
||||
@@ -159,7 +159,7 @@ import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
@@ -168,7 +168,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
@@ -178,8 +178,8 @@ const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const authStore = useAuthStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { fireEvent, render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, watch } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
@@ -39,8 +40,8 @@ describe('SearchInput', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
function mountComponent(props = {}) {
|
||||
return mount(SearchInput, {
|
||||
function renderComponent(props = {}) {
|
||||
const result = render(SearchInput, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
@@ -63,140 +64,142 @@ describe('SearchInput', () => {
|
||||
...props
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
describe('debounced search', () => {
|
||||
it('should debounce search input by 300ms', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.find('input')
|
||||
const onSearch = vi.fn()
|
||||
renderComponent({ onSearch })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.setValue('test')
|
||||
await fireEvent.update(input, 'test')
|
||||
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
expect(onSearch).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(299)
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
expect(onSearch).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('search')).toEqual([['test']])
|
||||
expect(onSearch).toHaveBeenCalledWith('test')
|
||||
})
|
||||
|
||||
it('should reset debounce timer on each keystroke', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.find('input')
|
||||
const onSearch = vi.fn()
|
||||
renderComponent({ onSearch })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
await input.setValue('t')
|
||||
await fireEvent.update(input, 't')
|
||||
vi.advanceTimersByTime(200)
|
||||
await nextTick()
|
||||
|
||||
await input.setValue('te')
|
||||
await fireEvent.update(input, 'te')
|
||||
vi.advanceTimersByTime(200)
|
||||
await nextTick()
|
||||
|
||||
await input.setValue('tes')
|
||||
await fireEvent.update(input, 'tes')
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
expect(onSearch).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('search')).toBeTruthy()
|
||||
expect(wrapper.emitted('search')?.[0]).toEqual(['tes'])
|
||||
expect(onSearch).toHaveBeenCalled()
|
||||
expect(onSearch).toHaveBeenCalledWith('tes')
|
||||
})
|
||||
|
||||
it('should only emit final value after rapid typing', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.find('input')
|
||||
const onSearch = vi.fn()
|
||||
renderComponent({ onSearch })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
const searchTerms = ['s', 'se', 'sea', 'sear', 'searc', 'search']
|
||||
for (const term of searchTerms) {
|
||||
await input.setValue(term)
|
||||
await fireEvent.update(input, term)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
expect(onSearch).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('search')).toHaveLength(1)
|
||||
expect(wrapper.emitted('search')?.[0]).toEqual(['search'])
|
||||
expect(onSearch).toHaveBeenCalledTimes(1)
|
||||
expect(onSearch).toHaveBeenCalledWith('search')
|
||||
})
|
||||
})
|
||||
|
||||
describe('model sync', () => {
|
||||
it('should sync external model changes to internal state', async () => {
|
||||
const wrapper = mountComponent({ modelValue: 'initial' })
|
||||
const input = wrapper.find('input')
|
||||
const { rerender } = renderComponent({ modelValue: 'initial' })
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
expect(input.element.value).toBe('initial')
|
||||
expect(input).toHaveValue('initial')
|
||||
|
||||
await wrapper.setProps({ modelValue: 'external update' })
|
||||
await rerender({ modelValue: 'external update' })
|
||||
await nextTick()
|
||||
|
||||
expect(input.element.value).toBe('external update')
|
||||
expect(input).toHaveValue('external update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('placeholder', () => {
|
||||
it('should use custom placeholder when provided', () => {
|
||||
const wrapper = mountComponent({ placeholder: 'Custom search...' })
|
||||
const input = wrapper.find('input')
|
||||
|
||||
expect(input.attributes('placeholder')).toBe('Custom search...')
|
||||
renderComponent({ placeholder: 'Custom search...' })
|
||||
expect(
|
||||
screen.getByPlaceholderText('Custom search...')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use i18n placeholder when not provided', () => {
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.find('input')
|
||||
|
||||
expect(input.attributes('placeholder')).toBe('Search...')
|
||||
renderComponent()
|
||||
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('autofocus', () => {
|
||||
it('should pass autofocus prop to ComboboxInput', () => {
|
||||
const wrapper = mountComponent({ autofocus: true })
|
||||
const input = wrapper.find('input')
|
||||
expect(input.attributes('autofocus')).toBeDefined()
|
||||
renderComponent({ autofocus: true })
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveAttribute('autofocus')
|
||||
})
|
||||
|
||||
it('should not autofocus by default', () => {
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.find('input')
|
||||
expect(input.attributes('autofocus')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('focus method', () => {
|
||||
it('should expose focus method via ref', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.vm.focus).toBeDefined()
|
||||
renderComponent()
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).not.toHaveAttribute('autofocus')
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear button', () => {
|
||||
it('shows search icon when value is empty', () => {
|
||||
const wrapper = mountComponent({ modelValue: '' })
|
||||
expect(wrapper.find('button[aria-label="Clear"]').exists()).toBe(false)
|
||||
renderComponent({ modelValue: '' })
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Clear' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows clear button when value is not empty', () => {
|
||||
const wrapper = mountComponent({ modelValue: 'test' })
|
||||
expect(wrapper.find('button[aria-label="Clear"]').exists()).toBe(true)
|
||||
renderComponent({ modelValue: 'test' })
|
||||
expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clears value when clear button is clicked', async () => {
|
||||
const wrapper = mountComponent({ modelValue: 'test' })
|
||||
const clearButton = wrapper.find('button')
|
||||
await clearButton.trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([''])
|
||||
const onUpdate = vi.fn()
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
||||
renderComponent({
|
||||
modelValue: 'test',
|
||||
'onUpdate:modelValue': onUpdate
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: 'Clear' }))
|
||||
expect(onUpdate).toHaveBeenCalledWith('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { h, nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -16,29 +17,39 @@ const i18n = createI18n({
|
||||
})
|
||||
|
||||
describe('TagsInput', () => {
|
||||
function mountTagsInput(props = {}, slots = {}) {
|
||||
return mount(TagsInput, {
|
||||
function renderTagsInput(props = {}, slots = {}) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(TagsInput, {
|
||||
props: {
|
||||
modelValue: [],
|
||||
...props
|
||||
},
|
||||
slots
|
||||
})
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
it('renders slot content', () => {
|
||||
const wrapper = mountTagsInput({}, { default: '<span>Slot Content</span>' })
|
||||
renderTagsInput({}, { default: '<span>Slot Content</span>' })
|
||||
|
||||
expect(wrapper.text()).toContain('Slot Content')
|
||||
expect(screen.getByText('Slot Content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TagsInput with child components', () => {
|
||||
function mountFullTagsInput(tags: string[] = ['tag1', 'tag2']) {
|
||||
return mount(TagsInput, {
|
||||
function renderFullTagsInput(
|
||||
tags: string[] = ['tag1', 'tag2'],
|
||||
extraProps: Record<string, unknown> = {}
|
||||
) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(TagsInput, {
|
||||
global: { plugins: [i18n] },
|
||||
props: {
|
||||
modelValue: tags
|
||||
modelValue: tags,
|
||||
...extraProps
|
||||
},
|
||||
slots: {
|
||||
default: () => [
|
||||
@@ -52,55 +63,52 @@ describe('TagsInput with child components', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
it('renders tags structure and content', () => {
|
||||
const tags = ['tag1', 'tag2']
|
||||
const wrapper = mountFullTagsInput(tags)
|
||||
renderFullTagsInput(tags)
|
||||
|
||||
const items = wrapper.findAllComponents(TagsInputItem)
|
||||
const textElements = wrapper.findAllComponents(TagsInputItemText)
|
||||
const deleteButtons = wrapper.findAllComponents(TagsInputItemDelete)
|
||||
expect(screen.getByText('tag1')).toBeInTheDocument()
|
||||
expect(screen.getByText('tag2')).toBeInTheDocument()
|
||||
|
||||
expect(items).toHaveLength(tags.length)
|
||||
expect(textElements).toHaveLength(tags.length)
|
||||
const deleteButtons = tags.map((tag) =>
|
||||
screen.getByRole('button', { name: tag })
|
||||
)
|
||||
expect(deleteButtons).toHaveLength(tags.length)
|
||||
|
||||
textElements.forEach((el, i) => {
|
||||
expect(el.text()).toBe(tags[i])
|
||||
})
|
||||
|
||||
expect(wrapper.findComponent(TagsInputInput).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates model value when adding a tag', async () => {
|
||||
let currentTags = ['existing']
|
||||
const onUpdate = vi.fn()
|
||||
|
||||
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(TagsInput, {
|
||||
props: {
|
||||
modelValue: currentTags,
|
||||
'onUpdate:modelValue': (payload) => {
|
||||
currentTags = payload
|
||||
}
|
||||
modelValue: ['existing'],
|
||||
'onUpdate:modelValue': onUpdate
|
||||
},
|
||||
slots: {
|
||||
default: () => h(TagsInputInput, { placeholder: 'Add tag...' })
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.trigger('click')
|
||||
// Click the container to enter edit mode and show the input
|
||||
// eslint-disable-next-line testing-library/no-node-access -- TagsInput root element needs click to enter edit mode; no role/label available
|
||||
await user.click(container.firstElementChild!)
|
||||
await nextTick()
|
||||
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('newTag')
|
||||
await input.trigger('keydown', { key: 'Enter' })
|
||||
const input = screen.getByPlaceholderText('Add tag...')
|
||||
await user.type(input, 'newTag{Enter}')
|
||||
await nextTick()
|
||||
|
||||
expect(currentTags).toContain('newTag')
|
||||
expect(onUpdate).toHaveBeenCalledWith(['existing', 'newTag'])
|
||||
})
|
||||
|
||||
it('does not enter edit mode when disabled', async () => {
|
||||
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(TagsInput, {
|
||||
props: {
|
||||
modelValue: ['tag1'],
|
||||
disabled: true
|
||||
@@ -110,18 +118,21 @@ describe('TagsInput with child components', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
expect(screen.queryByPlaceholderText('Add tag...')).not.toBeInTheDocument()
|
||||
|
||||
await wrapper.trigger('click')
|
||||
// eslint-disable-next-line testing-library/no-node-access -- TagsInput root element needs click to test disabled behavior; no role/label available
|
||||
await user.click(container.firstElementChild!)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
expect(screen.queryByPlaceholderText('Add tag...')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('exits edit mode when clicking outside', async () => {
|
||||
const outsideElement = document.createElement('div')
|
||||
document.body.appendChild(outsideElement)
|
||||
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
|
||||
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(TagsInput, {
|
||||
props: {
|
||||
modelValue: ['tag1']
|
||||
},
|
||||
@@ -130,21 +141,21 @@ describe('TagsInput with child components', () => {
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.trigger('click')
|
||||
// eslint-disable-next-line testing-library/no-node-access -- TagsInput root element needs click; no role/label
|
||||
await user.click(container.firstElementChild!)
|
||||
await nextTick()
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
expect(screen.getByPlaceholderText('Add tag...')).toBeInTheDocument()
|
||||
|
||||
outsideElement.dispatchEvent(new PointerEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
expect(screen.queryByPlaceholderText('Add tag...')).not.toBeInTheDocument()
|
||||
|
||||
wrapper.unmount()
|
||||
outsideElement.remove()
|
||||
})
|
||||
|
||||
it('shows placeholder when modelValue is empty', async () => {
|
||||
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
|
||||
render(TagsInput, {
|
||||
props: {
|
||||
modelValue: []
|
||||
},
|
||||
@@ -156,8 +167,7 @@ describe('TagsInput with child components', () => {
|
||||
|
||||
await nextTick()
|
||||
|
||||
const input = wrapper.find('input')
|
||||
expect(input.exists()).toBe(true)
|
||||
expect(input.attributes('placeholder')).toBe('Add tag...')
|
||||
const input = screen.getByPlaceholderText('Add tag...')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type { BillingPortalTargetTier } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { BillingPortalTargetTier } from '@/stores/authStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
@@ -20,8 +20,8 @@ import { usdToMicros } from '@/utils/formatUtil'
|
||||
* All actions are wrapped with error handling.
|
||||
* @returns {Object} - Object containing all Firebase Auth actions
|
||||
*/
|
||||
export const useFirebaseAuthActions = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
export const useAuthActions = () => {
|
||||
const authStore = useAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
|
||||
@@ -3,11 +3,11 @@ import { computed, watch } from 'vue'
|
||||
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { AuthUserInfo } from '@/types/authTypes'
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
balance: { amount_micros: 5000000 },
|
||||
fetchBalance: vi.fn().mockResolvedValue({ amount_micros: 5000000 })
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
PreviewSubscribeResponse,
|
||||
SubscribeResponse
|
||||
} from '@/platform/workspace/api/workspaceApi'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
import type {
|
||||
BalanceInfo,
|
||||
@@ -33,7 +33,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
showSubscriptionDialog: legacyShowSubscriptionDialog
|
||||
} = useSubscription()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isInitialized = ref(false)
|
||||
const isLoading = ref(false)
|
||||
@@ -55,12 +55,12 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
renewalDate: formattedRenewalDate.value || null,
|
||||
endDate: formattedEndDate.value || null,
|
||||
isCancelled: isCancelled.value,
|
||||
hasFunds: (firebaseAuthStore.balance?.amount_micros ?? 0) > 0
|
||||
hasFunds: (authStore.balance?.amount_micros ?? 0) > 0
|
||||
}
|
||||
})
|
||||
|
||||
const balance = computed<BalanceInfo | null>(() => {
|
||||
const legacyBalance = firebaseAuthStore.balance
|
||||
const legacyBalance = authStore.balance
|
||||
if (!legacyBalance) return null
|
||||
|
||||
return {
|
||||
@@ -118,7 +118,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await firebaseAuthStore.fetchBalance()
|
||||
await authStore.fetchBalance()
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error ? err.message : 'Failed to fetch balance'
|
||||
|
||||
@@ -56,8 +56,8 @@ vi.mock('@/scripts/api', () => ({
|
||||
|
||||
vi.mock('@/platform/settings/settingStore')
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({}))
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuth', () => ({
|
||||
@@ -123,8 +123,8 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({}))
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
@@ -78,7 +78,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const toastStore = useToastStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
@@ -996,7 +996,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Sign Out',
|
||||
versionAdded: '1.18.1',
|
||||
function: async () => {
|
||||
await firebaseAuthActions.logout()
|
||||
await authActions.logout()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -67,7 +67,14 @@ Sentry.init({
|
||||
replaysOnErrorSampleRate: 0,
|
||||
// Only set these for non-cloud builds
|
||||
...(isCloud
|
||||
? {}
|
||||
? {
|
||||
integrations: [
|
||||
// Disable event target wrapping to reduce overhead on high-frequency
|
||||
// DOM events (pointermove, mousemove, wheel). Sentry still captures
|
||||
// errors via window.onerror and unhandledrejection.
|
||||
Sentry.browserApiErrorsIntegration({ eventTarget: false })
|
||||
]
|
||||
}
|
||||
: {
|
||||
integrations: [],
|
||||
autoSessionTracking: false,
|
||||
|
||||
@@ -141,6 +141,7 @@ import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
|
||||
import IconGroup from '@/components/button/IconGroup.vue'
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import {
|
||||
formatDuration,
|
||||
@@ -279,7 +280,8 @@ const formattedDuration = computed(() => {
|
||||
// Get metadata info based on file kind
|
||||
const metaInfo = computed(() => {
|
||||
if (!asset) return ''
|
||||
if (fileKind.value === 'image' && imageDimensions.value) {
|
||||
// TODO(assets): Re-enable once /assets API returns original image dimensions in metadata (#10590)
|
||||
if (fileKind.value === 'image' && imageDimensions.value && !isCloud) {
|
||||
return `${imageDimensions.value.width}x${imageDimensions.value.height}`
|
||||
}
|
||||
if (asset.size && ['video', 'audio', '3D'].includes(fileKind.value)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
/**
|
||||
* Session cookie management for cloud authentication.
|
||||
@@ -21,7 +21,7 @@ export const useSessionCookie = () => {
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
try {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
let authHeader: Record<string, string>
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import DesktopCloudNotificationController from './DesktopCloudNotificationController.vue'
|
||||
|
||||
const settingState = {
|
||||
shown: false
|
||||
}
|
||||
|
||||
const settingStore = {
|
||||
load: vi.fn<() => Promise<void>>(),
|
||||
get: vi.fn((key: string) =>
|
||||
key === 'Comfy.Desktop.CloudNotificationShown'
|
||||
? settingState.shown
|
||||
: undefined
|
||||
),
|
||||
set: vi.fn(async (_key: string, value: boolean) => {
|
||||
settingState.shown = value
|
||||
})
|
||||
}
|
||||
|
||||
const dialogService = {
|
||||
showCloudNotification: vi.fn<() => Promise<void>>()
|
||||
}
|
||||
|
||||
const electron = {
|
||||
getPlatform: vi.fn(() => 'darwin')
|
||||
}
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isDesktop: true
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => settingStore
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => dialogService
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/envUtil', () => ({
|
||||
electronAPI: () => electron
|
||||
}))
|
||||
|
||||
function createDeferred() {
|
||||
let resolve!: () => void
|
||||
const promise = new Promise<void>((res) => {
|
||||
resolve = res
|
||||
})
|
||||
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('DesktopCloudNotificationController', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
|
||||
settingState.shown = false
|
||||
electron.getPlatform.mockReturnValue('darwin')
|
||||
settingStore.load.mockResolvedValue(undefined)
|
||||
settingStore.set.mockImplementation(
|
||||
async (_key: string, value: boolean) => {
|
||||
settingState.shown = value
|
||||
}
|
||||
)
|
||||
dialogService.showCloudNotification.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('waits for settings to load before deciding whether to show the notification', async () => {
|
||||
const loadSettings = createDeferred()
|
||||
settingStore.load.mockImplementation(() => loadSettings.promise)
|
||||
|
||||
const wrapper = mount(DesktopCloudNotificationController)
|
||||
await nextTick()
|
||||
|
||||
settingState.shown = true
|
||||
loadSettings.resolve()
|
||||
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(dialogService.showCloudNotification).not.toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('does not schedule or show the notification after unmounting before settings load resolves', async () => {
|
||||
const loadSettings = createDeferred()
|
||||
settingStore.load.mockImplementation(() => loadSettings.promise)
|
||||
|
||||
const wrapper = mount(DesktopCloudNotificationController)
|
||||
await nextTick()
|
||||
|
||||
wrapper.unmount()
|
||||
loadSettings.resolve()
|
||||
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(settingStore.set).not.toHaveBeenCalled()
|
||||
expect(dialogService.showCloudNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('marks the notification as shown before awaiting dialog close', async () => {
|
||||
const dialogOpen = createDeferred()
|
||||
dialogService.showCloudNotification.mockImplementation(
|
||||
() => dialogOpen.promise
|
||||
)
|
||||
|
||||
const wrapper = mount(DesktopCloudNotificationController)
|
||||
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(settingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Desktop.CloudNotificationShown',
|
||||
true
|
||||
)
|
||||
expect(settingStore.set.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
dialogService.showCloudNotification.mock.invocationCallOrder[0]
|
||||
)
|
||||
|
||||
dialogOpen.resolve()
|
||||
await flushPromises()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
import { isDesktop } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
let isDisposed = false
|
||||
let cloudNotificationTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
async function scheduleCloudNotification() {
|
||||
if (!isDesktop || electronAPI()?.getPlatform() !== 'darwin') return
|
||||
|
||||
try {
|
||||
await settingStore.load()
|
||||
} catch (error) {
|
||||
console.warn('[CloudNotification] Failed to load settings', error)
|
||||
return
|
||||
}
|
||||
|
||||
if (isDisposed) return
|
||||
if (settingStore.get('Comfy.Desktop.CloudNotificationShown')) return
|
||||
|
||||
cloudNotificationTimer = setTimeout(async () => {
|
||||
if (isDisposed) return
|
||||
|
||||
try {
|
||||
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
|
||||
if (isDisposed) return
|
||||
await dialogService.showCloudNotification()
|
||||
} catch (error) {
|
||||
console.warn('[CloudNotification] Failed to show', error)
|
||||
await settingStore
|
||||
.set('Comfy.Desktop.CloudNotificationShown', false)
|
||||
.catch((resetError) => {
|
||||
console.warn(
|
||||
'[CloudNotification] Failed to reset shown state',
|
||||
resetError
|
||||
)
|
||||
})
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void scheduleCloudNotification()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isDisposed = true
|
||||
if (cloudNotificationTimer) clearTimeout(cloudNotificationTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -74,7 +74,7 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
|
||||
interface Props {
|
||||
errorMessage?: string
|
||||
@@ -83,7 +83,7 @@ interface Props {
|
||||
defineProps<Props>()
|
||||
|
||||
const router = useRouter()
|
||||
const { logout } = useFirebaseAuthActions()
|
||||
const { logout } = useAuthActions()
|
||||
const showTechnicalDetails = ref(false)
|
||||
|
||||
const handleRestart = async () => {
|
||||
|
||||
@@ -76,11 +76,11 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
|
||||
const email = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -110,7 +110,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import CloudSignInForm from '@/platform/cloud/onboarding/components/CloudSignInForm.vue'
|
||||
import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/useFreeTierOnboarding'
|
||||
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
|
||||
@@ -120,7 +120,7 @@ import type { SignInData } from '@/schemas/signInSchema'
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const isSecureContext = globalThis.isSecureContext
|
||||
const authError = ref('')
|
||||
const toastStore = useToastStore()
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</Button>
|
||||
<span
|
||||
v-if="isFreeTierEnabled"
|
||||
class="absolute -top-2.5 -right-2.5 rounded-full bg-yellow-400 px-2 py-0.5 text-[10px] font-bold whitespace-nowrap text-gray-900"
|
||||
class="absolute -top-2.5 -right-2.5 rounded-full bg-yellow-400 px-2 py-0.5 text-2xs font-bold whitespace-nowrap text-gray-900"
|
||||
>
|
||||
{{ t('auth.login.freeTierBadge') }}
|
||||
</span>
|
||||
@@ -133,7 +133,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import SignUpForm from '@/components/dialog/content/signin/SignUpForm.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/useFreeTierOnboarding'
|
||||
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
@@ -145,7 +145,7 @@ import { isInChina } from '@/utils/networkUtil'
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const isSecureContext = globalThis.isSecureContext
|
||||
const authError = ref('')
|
||||
const userIsInChina = ref(false)
|
||||
|
||||
@@ -25,8 +25,8 @@ const authActionMocks = vi.hoisted(() => ({
|
||||
accessBillingPortal: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => authActionMocks
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => authActionMocks
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useErrorHandling', () => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import { performSubscriptionCheckout } from '@/platform/cloud/subscription/utils/subscriptionCheckoutUtil'
|
||||
@@ -16,7 +16,7 @@ import type { BillingCycle } from '../subscription/utils/subscriptionTierRank'
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
||||
const { reportError, accessBillingPortal } = useAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isActiveSubscription, isInitialized, initialize } = useBillingContext()
|
||||
|
||||
@@ -89,9 +89,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -31,8 +31,8 @@ vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => ({
|
||||
accessBillingPortal: mockAccessBillingPortal,
|
||||
reportError: mockReportError
|
||||
})
|
||||
@@ -56,13 +56,13 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () =>
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
}),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
AuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<span>{{ option.label }}</span>
|
||||
<div
|
||||
v-if="option.value === 'yearly'"
|
||||
class="flex items-center rounded-full bg-primary-background px-1 py-0.5 text-[11px] font-bold text-white"
|
||||
class="flex items-center rounded-full bg-primary-background px-1 py-0.5 text-2xs font-bold text-white"
|
||||
>
|
||||
-20%
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
</span>
|
||||
<div
|
||||
v-if="tier.isPopular"
|
||||
class="flex h-5 items-center rounded-full bg-base-foreground px-1.5 text-[11px] font-bold tracking-tight text-base-background uppercase"
|
||||
class="flex h-5 items-center rounded-full bg-base-foreground px-1.5 text-2xs font-bold tracking-tight text-base-background uppercase"
|
||||
>
|
||||
{{ t('subscription.mostPopular') }}
|
||||
</div>
|
||||
@@ -262,7 +262,7 @@ import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import {
|
||||
@@ -279,7 +279,7 @@ import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscript
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
||||
@@ -365,8 +365,8 @@ const {
|
||||
isYearlySubscription
|
||||
} = useSubscription()
|
||||
const telemetry = useTelemetry()
|
||||
const { userId } = storeToRefs(useFirebaseAuthStore())
|
||||
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
|
||||
const { userId } = storeToRefs(useAuthStore())
|
||||
const { accessBillingPortal, reportError } = useAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -82,8 +82,8 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
authActions: vi.fn(() => ({
|
||||
accessBillingPortal: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -211,7 +211,7 @@ import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
@@ -227,7 +227,7 @@ import type { TierBenefit } from '@/platform/cloud/subscription/utils/tierBenefi
|
||||
import { getCommonTierBenefits } from '@/platform/cloud/subscription/utils/tierBenefits'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const { t, n } = useI18n()
|
||||
|
||||
const {
|
||||
|
||||
@@ -65,8 +65,8 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => mockTelemetry)
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
reportError: mockReportError,
|
||||
accessBillingPortal: mockAccessBillingPortal
|
||||
}))
|
||||
@@ -106,14 +106,14 @@ vi.mock('@/services/dialogService', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
get userId() {
|
||||
return mockUserId.value
|
||||
}
|
||||
})),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
AuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
// Mock fetch
|
||||
|
||||
@@ -2,7 +2,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
@@ -10,10 +10,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import { AuthStoreError, useAuthStore } from '@/stores/authStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { TIER_TO_KEY } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
@@ -37,11 +34,11 @@ function useSubscriptionInternal() {
|
||||
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
})
|
||||
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
||||
const { reportError, accessBillingPortal } = useAuthActions()
|
||||
const { showSubscriptionRequiredDialog } = useDialogService()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { getAuthHeader } = firebaseAuthStore
|
||||
const authStore = useAuthStore()
|
||||
const { getAuthHeader } = authStore
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -194,7 +191,7 @@ function useSubscriptionInternal() {
|
||||
async function fetchSubscriptionStatus(): Promise<CloudSubscriptionStatusResponse | null> {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
@@ -209,7 +206,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToFetchSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -248,9 +245,7 @@ function useSubscriptionInternal() {
|
||||
async (): Promise<CloudSubscriptionCheckoutResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
const checkoutAttribution = await getCheckoutAttributionForCloud()
|
||||
|
||||
@@ -268,7 +263,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToInitiateSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@ const mockFetchStatus = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
const mockExecute = vi.fn()
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => ({
|
||||
fetchBalance: mockFetchBalance
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -12,7 +12,7 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
*/
|
||||
export function useSubscriptionActions() {
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { fetchStatus } = useBillingContext()
|
||||
|
||||
@@ -36,14 +36,14 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => mockTelemetry)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() =>
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
})
|
||||
),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
AuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
|
||||
@@ -4,10 +4,7 @@ import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import { AuthStoreError, useAuthStore } from '@/stores/authStore'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { BillingCycle } from './subscriptionTierRank'
|
||||
@@ -51,13 +48,13 @@ export async function performSubscriptionCheckout(
|
||||
): Promise<void> {
|
||||
if (!isCloud) return
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { userId } = storeToRefs(firebaseAuthStore)
|
||||
const authStore = useAuthStore()
|
||||
const { userId } = storeToRefs(authStore)
|
||||
const telemetry = useTelemetry()
|
||||
const authHeader = await firebaseAuthStore.getAuthHeader()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle)
|
||||
@@ -97,7 +94,7 @@ export async function performSubscriptionCheckout(
|
||||
}
|
||||
}
|
||||
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToInitiateSubscription', {
|
||||
error: errorMessage
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserM
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import NavItem from '@/components/widget/nav/NavItem.vue'
|
||||
import NavTitle from '@/components/widget/nav/NavTitle.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import ColorPaletteMessage from '@/platform/settings/components/ColorPaletteMessage.vue'
|
||||
import SettingsPanel from '@/platform/settings/components/SettingsPanel.vue'
|
||||
import { useSettingSearch } from '@/platform/settings/composables/useSettingSearch'
|
||||
@@ -129,7 +129,7 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
|
||||
const navRef = ref<HTMLElement | null>(null)
|
||||
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
|
||||
|
||||
@@ -6,7 +6,7 @@ type MockApiKeyUser = {
|
||||
email?: string
|
||||
} | null
|
||||
|
||||
type MockFirebaseUser = {
|
||||
type MockAuthUser = {
|
||||
uid: string
|
||||
email?: string | null
|
||||
} | null
|
||||
@@ -14,19 +14,19 @@ type MockFirebaseUser = {
|
||||
const {
|
||||
mockCaptureCheckoutAttributionFromSearch,
|
||||
mockUseApiKeyAuthStore,
|
||||
mockUseFirebaseAuthStore,
|
||||
mockUseAuthStore,
|
||||
mockApiKeyAuthStore,
|
||||
mockFirebaseAuthStore
|
||||
mockAuthStore
|
||||
} = vi.hoisted(() => ({
|
||||
mockCaptureCheckoutAttributionFromSearch: vi.fn(),
|
||||
mockUseApiKeyAuthStore: vi.fn(),
|
||||
mockUseFirebaseAuthStore: vi.fn(),
|
||||
mockUseAuthStore: vi.fn(),
|
||||
mockApiKeyAuthStore: {
|
||||
isAuthenticated: false,
|
||||
currentUser: null as MockApiKeyUser
|
||||
},
|
||||
mockFirebaseAuthStore: {
|
||||
currentUser: null as MockFirebaseUser
|
||||
mockAuthStore: {
|
||||
currentUser: null as MockAuthUser
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -38,8 +38,8 @@ vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: mockUseApiKeyAuthStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: mockUseFirebaseAuthStore
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: mockUseAuthStore
|
||||
}))
|
||||
|
||||
import { ImpactTelemetryProvider } from './ImpactTelemetryProvider'
|
||||
@@ -64,14 +64,14 @@ describe('ImpactTelemetryProvider', () => {
|
||||
beforeEach(() => {
|
||||
mockCaptureCheckoutAttributionFromSearch.mockReset()
|
||||
mockUseApiKeyAuthStore.mockReset()
|
||||
mockUseFirebaseAuthStore.mockReset()
|
||||
mockUseAuthStore.mockReset()
|
||||
mockApiKeyAuthStore.isAuthenticated = false
|
||||
mockApiKeyAuthStore.currentUser = null
|
||||
mockFirebaseAuthStore.currentUser = null
|
||||
mockAuthStore.currentUser = null
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
mockUseApiKeyAuthStore.mockReturnValue(mockApiKeyAuthStore)
|
||||
mockUseFirebaseAuthStore.mockReturnValue(mockFirebaseAuthStore)
|
||||
mockUseAuthStore.mockReturnValue(mockAuthStore)
|
||||
|
||||
const queueFn: NonNullable<Window['ire']> = (...args: unknown[]) => {
|
||||
;(queueFn.a ??= []).push(args)
|
||||
@@ -93,7 +93,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
})
|
||||
|
||||
it('captures attribution and invokes identify with hashed email', async () => {
|
||||
mockFirebaseAuthStore.currentUser = {
|
||||
mockAuthStore.currentUser = {
|
||||
uid: 'user-123',
|
||||
email: ' User@Example.com '
|
||||
}
|
||||
@@ -153,7 +153,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
})
|
||||
|
||||
it('invokes identify on each page view even with identical identity payloads', async () => {
|
||||
mockFirebaseAuthStore.currentUser = {
|
||||
mockAuthStore.currentUser = {
|
||||
uid: 'user-123',
|
||||
email: 'user@example.com'
|
||||
}
|
||||
@@ -189,7 +189,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
id: 'api-key-user-123',
|
||||
email: 'apikey@example.com'
|
||||
}
|
||||
mockFirebaseAuthStore.currentUser = {
|
||||
mockAuthStore.currentUser = {
|
||||
uid: 'firebase-user-123',
|
||||
email: 'firebase@example.com'
|
||||
}
|
||||
@@ -228,7 +228,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
id: 'api-key-user-123',
|
||||
email: 'apikey@example.com'
|
||||
}
|
||||
mockFirebaseAuthStore.currentUser = null
|
||||
mockAuthStore.currentUser = null
|
||||
vi.stubGlobal('crypto', {
|
||||
subtle: {
|
||||
digest: vi.fn(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { captureCheckoutAttributionFromSearch } from '@/platform/telemetry/utils/checkoutAttribution'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
import type { PageViewMetadata, TelemetryProvider } from '../../types'
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
private initialized = false
|
||||
private stores: {
|
||||
apiKeyAuthStore: ReturnType<typeof useApiKeyAuthStore>
|
||||
firebaseAuthStore: ReturnType<typeof useFirebaseAuthStore>
|
||||
authStore: ReturnType<typeof useAuthStore>
|
||||
} | null = null
|
||||
|
||||
constructor() {
|
||||
@@ -109,12 +109,11 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (stores.firebaseAuthStore.currentUser) {
|
||||
if (stores.authStore.currentUser) {
|
||||
return {
|
||||
customerId:
|
||||
stores.firebaseAuthStore.currentUser.uid ?? EMPTY_CUSTOMER_VALUE,
|
||||
customerId: stores.authStore.currentUser.uid ?? EMPTY_CUSTOMER_VALUE,
|
||||
customerEmail:
|
||||
stores.firebaseAuthStore.currentUser.email ?? EMPTY_CUSTOMER_VALUE
|
||||
stores.authStore.currentUser.email ?? EMPTY_CUSTOMER_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +134,7 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
|
||||
private resolveAuthStores(): {
|
||||
apiKeyAuthStore: ReturnType<typeof useApiKeyAuthStore>
|
||||
firebaseAuthStore: ReturnType<typeof useFirebaseAuthStore>
|
||||
authStore: ReturnType<typeof useAuthStore>
|
||||
} | null {
|
||||
if (this.stores) {
|
||||
return this.stores
|
||||
@@ -144,7 +143,7 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
try {
|
||||
const stores = {
|
||||
apiKeyAuthStore: useApiKeyAuthStore(),
|
||||
firebaseAuthStore: useFirebaseAuthStore()
|
||||
authStore: useAuthStore()
|
||||
}
|
||||
this.stores = stores
|
||||
return stores
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export type WorkspaceType = 'personal' | 'team'
|
||||
export type WorkspaceRole = 'owner' | 'member'
|
||||
@@ -288,7 +288,7 @@ const workspaceApiClient = axios.create({
|
||||
})
|
||||
|
||||
async function getAuthHeaderOrThrow() {
|
||||
const authHeader = await useFirebaseAuthStore().getAuthHeader()
|
||||
const authHeader = await useAuthStore().getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new WorkspaceApiError(
|
||||
t('toastMessages.userNotAuthenticated'),
|
||||
@@ -300,7 +300,7 @@ async function getAuthHeaderOrThrow() {
|
||||
}
|
||||
|
||||
async function getFirebaseHeaderOrThrow() {
|
||||
const authHeader = await useFirebaseAuthStore().getFirebaseAuthHeader()
|
||||
const authHeader = await useAuthStore().getFirebaseAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new WorkspaceApiError(
|
||||
t('toastMessages.userNotAuthenticated'),
|
||||
|
||||
@@ -8,8 +8,8 @@ import WorkspaceAuthGate from './WorkspaceAuthGate.vue'
|
||||
const mockIsInitialized = ref(false)
|
||||
const mockCurrentUser = ref<object | null>(null)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
isInitialized: mockIsInitialized,
|
||||
currentUser: mockCurrentUser
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import { refreshRemoteConfig } from '@/platform/remoteConfig/refreshRemoteConfig'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const FIREBASE_INIT_TIMEOUT_MS = 16_000
|
||||
const CONFIG_REFRESH_TIMEOUT_MS = 10_000
|
||||
@@ -38,7 +38,7 @@ const subscriptionDialog = useSubscriptionDialog()
|
||||
async function initialize(): Promise<void> {
|
||||
if (!isCloud) return
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const { isInitialized, currentUser } = storeToRefs(authStore)
|
||||
|
||||
try {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<span>{{ option.label }}</span>
|
||||
<div
|
||||
v-if="option.value === 'yearly'"
|
||||
class="flex items-center rounded-full bg-primary-background px-1 py-0.5 text-[11px] font-bold text-white"
|
||||
class="flex items-center rounded-full bg-primary-background px-1 py-0.5 text-2xs font-bold text-white"
|
||||
>
|
||||
-20%
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
</span>
|
||||
<div
|
||||
v-if="tier.isPopular"
|
||||
class="flex h-5 items-center rounded-full bg-base-foreground px-1.5 text-[11px] font-bold tracking-tight text-base-background uppercase"
|
||||
class="flex h-5 items-center rounded-full bg-base-foreground px-1.5 text-2xs font-bold tracking-tight text-base-background uppercase"
|
||||
>
|
||||
{{ t('subscription.mostPopular') }}
|
||||
</div>
|
||||
|
||||
@@ -100,6 +100,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
|
||||
import type { PreviewSubscribeResponse } from '@/platform/workspace/api/workspaceApi'
|
||||
@@ -218,8 +219,8 @@ async function handleAddCreditCard() {
|
||||
if (!planSlug) return
|
||||
const response = await subscribe(
|
||||
planSlug,
|
||||
'https://www.comfy.org/payment/success',
|
||||
'https://www.comfy.org/payment/failed'
|
||||
`${getComfyPlatformBaseUrl()}/payment/success`,
|
||||
`${getComfyPlatformBaseUrl()}/payment/failed`
|
||||
)
|
||||
|
||||
if (!response) return
|
||||
@@ -272,8 +273,8 @@ async function handleConfirmTransition() {
|
||||
if (!planSlug) return
|
||||
const response = await subscribe(
|
||||
planSlug,
|
||||
'https://www.comfy.org/payment/success',
|
||||
'https://www.comfy.org/payment/failed'
|
||||
`${getComfyPlatformBaseUrl()}/payment/success`,
|
||||
`${getComfyPlatformBaseUrl()}/payment/failed`
|
||||
)
|
||||
|
||||
if (!response) return
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="resolveTierLabel(workspace)"
|
||||
class="rounded-full bg-base-foreground px-1 py-0.5 text-[10px] font-bold text-base-background uppercase"
|
||||
class="rounded-full bg-base-foreground px-1 py-0.5 text-2xs font-bold text-base-background uppercase"
|
||||
>
|
||||
{{ resolveTierLabel(workspace) }}
|
||||
</span>
|
||||
|
||||
@@ -300,6 +300,25 @@ describe('TeamWorkspacesDialogContent', () => {
|
||||
|
||||
expect(mockCreateWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resets loading state after createWorkspace fails', async () => {
|
||||
mockCreateWorkspace.mockRejectedValue(new Error('Limit reached'))
|
||||
const wrapper = mountComponent()
|
||||
|
||||
await typeAndCreate(wrapper, 'New Team')
|
||||
|
||||
expect(findCreateButton(wrapper).props('loading')).toBe(false)
|
||||
})
|
||||
|
||||
it('resets loading state after onConfirm fails', async () => {
|
||||
mockCreateWorkspace.mockResolvedValue({ id: 'new-ws' })
|
||||
const onConfirm = vi.fn().mockRejectedValue(new Error('Setup failed'))
|
||||
const wrapper = mountComponent({ onConfirm })
|
||||
|
||||
await typeAndCreate(wrapper, 'New Team')
|
||||
|
||||
expect(findCreateButton(wrapper).props('loading')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('close button', () => {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="tierLabels.get(workspace.id)"
|
||||
class="shrink-0 rounded-full bg-base-foreground px-1 py-0.5 text-[10px] font-bold text-base-background uppercase"
|
||||
class="shrink-0 rounded-full bg-base-foreground px-1 py-0.5 text-2xs font-bold text-base-background uppercase"
|
||||
>
|
||||
{{ tierLabels.get(workspace.id) }}
|
||||
</span>
|
||||
@@ -201,28 +201,30 @@ async function handleSwitch(workspaceId: string) {
|
||||
async function onCreate() {
|
||||
if (!isValidName.value || loading.value) return
|
||||
loading.value = true
|
||||
const name = workspaceName.value.trim()
|
||||
try {
|
||||
await workspaceStore.createWorkspace(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
const name = workspaceName.value.trim()
|
||||
try {
|
||||
await workspaceStore.createWorkspace(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await onConfirm?.(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('teamWorkspacesDialog.confirmCallbackFailed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
}
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
} finally {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
await onConfirm?.(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('teamWorkspacesDialog.confirmCallbackFailed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
}
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="uiConfig.showRoleBadge"
|
||||
class="rounded-full bg-base-foreground px-1 py-0.5 text-[10px] font-bold text-base-background uppercase"
|
||||
class="rounded-full bg-base-foreground px-1 py-0.5 text-2xs font-bold text-base-background uppercase"
|
||||
>
|
||||
{{ $t('workspaceSwitcher.roleOwner') }}
|
||||
</span>
|
||||
@@ -202,7 +202,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="uiConfig.showRoleBadge"
|
||||
class="rounded-full bg-base-foreground px-1 py-0.5 text-[10px] font-bold text-base-background uppercase"
|
||||
class="rounded-full bg-base-foreground px-1 py-0.5 text-2xs font-bold text-base-background uppercase"
|
||||
>
|
||||
{{ getRoleBadgeLabel(member.role) }}
|
||||
</span>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { WORKSPACE_STORAGE_KEYS } from '@/platform/workspace/workspaceConstants'
|
||||
|
||||
const mockGetIdToken = vi.fn()
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
getIdToken: mockGetIdToken
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
WORKSPACE_STORAGE_KEYS
|
||||
} from '@/platform/workspace/workspaceConstants'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { AuthHeader } from '@/types/authTypes'
|
||||
import type { WorkspaceWithRole } from '@/platform/workspace/workspaceTypes'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
@@ -181,8 +181,8 @@ export const useWorkspaceAuthStore = defineStore('workspaceAuth', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const firebaseToken = await firebaseAuthStore.getIdToken()
|
||||
const authStore = useAuthStore()
|
||||
const firebaseToken = await authStore.getIdToken()
|
||||
if (!firebaseToken) {
|
||||
throw new WorkspaceAuthError(
|
||||
t('workspaceAuth.errors.notAuthenticated'),
|
||||
|
||||
@@ -110,4 +110,19 @@ describe('FormDropdownMenu', () => {
|
||||
const virtualGrid = wrapper.findComponent({ name: 'VirtualGrid' })
|
||||
expect(virtualGrid.props('maxColumns')).toBe(1)
|
||||
})
|
||||
|
||||
it('has data-capture-wheel="true" on the root element', () => {
|
||||
const wrapper = mount(FormDropdownMenu, {
|
||||
props: defaultProps,
|
||||
global: {
|
||||
stubs: {
|
||||
FormDropdownMenuFilter: true,
|
||||
FormDropdownMenuActions: true,
|
||||
VirtualGrid: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.attributes('data-capture-wheel')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,6 +98,7 @@ const virtualItems = computed<VirtualDropdownItem[]>(() =>
|
||||
<template>
|
||||
<div
|
||||
class="flex h-[640px] w-103 flex-col rounded-lg bg-component-node-background pt-4 outline -outline-offset-1 outline-node-component-border"
|
||||
data-capture-wheel="true"
|
||||
>
|
||||
<FormDropdownMenuFilter
|
||||
v-if="filterOptions.length > 0"
|
||||
|
||||
@@ -103,6 +103,7 @@ function toggleBaseModelSelection(item: FilterOption) {
|
||||
<div class="text-secondary flex gap-2 px-4">
|
||||
<FormSearchInput
|
||||
v-model="searchQuery"
|
||||
autofocus
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
|
||||
@@ -38,9 +38,9 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', async () => {
|
||||
vi.mock('@/stores/authStore', async () => {
|
||||
return {
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: vi.fn(() => Promise.resolve(mockCloudAuth.authHeader))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IWidget, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const MAX_RETRIES = 5
|
||||
const TIMEOUT = 4096
|
||||
@@ -23,7 +23,7 @@ interface CacheEntry<T> {
|
||||
|
||||
async function getAuthHeaders() {
|
||||
if (isCloud) {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
return {
|
||||
...(authHeader && { headers: authHeader })
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
||||
|
||||
@@ -140,7 +140,7 @@ if (isCloud) {
|
||||
}
|
||||
// Global authentication guard
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Wait for Firebase auth to initialize
|
||||
// Timeout after 16 seconds
|
||||
|
||||
@@ -60,7 +60,7 @@ import type {
|
||||
JobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type { useAuthStore } from '@/stores/authStore'
|
||||
import type { AuthHeader } from '@/types/authTypes'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
@@ -332,7 +332,7 @@ export class ComfyApi extends EventTarget {
|
||||
/**
|
||||
* Cache Firebase auth store composable function.
|
||||
*/
|
||||
private authStoreComposable?: typeof useFirebaseAuthStore
|
||||
private authStoreComposable?: typeof useAuthStore
|
||||
|
||||
reportedUnknownMessageTypes = new Set<string>()
|
||||
|
||||
@@ -399,8 +399,8 @@ export class ComfyApi extends EventTarget {
|
||||
private async getAuthStore() {
|
||||
if (isCloud) {
|
||||
if (!this.authStoreComposable) {
|
||||
const module = await import('@/stores/firebaseAuthStore')
|
||||
this.authStoreComposable = module.useFirebaseAuthStore
|
||||
const module = await import('@/stores/authStore')
|
||||
this.authStoreComposable = module.useAuthStore
|
||||
}
|
||||
|
||||
return this.authStoreComposable()
|
||||
|
||||
@@ -67,7 +67,7 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
@@ -1594,7 +1594,7 @@ export class ComfyApp {
|
||||
executionErrorStore.clearAllErrors()
|
||||
|
||||
// Get auth token for backend nodes - uses workspace token if enabled, otherwise Firebase token
|
||||
const comfyOrgAuthToken = await useFirebaseAuthStore().getAuthToken()
|
||||
const comfyOrgAuthToken = await useAuthStore().getAuthToken()
|
||||
const comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
|
||||
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,7 @@ const mockAxiosInstance = vi.hoisted(() => ({
|
||||
get: vi.fn()
|
||||
}))
|
||||
|
||||
const mockFirebaseAuthStore = vi.hoisted(() => ({
|
||||
const mockAuthStore = vi.hoisted(() => ({
|
||||
getAuthHeader: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -27,8 +27,8 @@ vi.mock('axios', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => mockFirebaseAuthStore)
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => mockAuthStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
@@ -81,7 +81,7 @@ describe('useCustomerEventsService', () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Setup default mocks
|
||||
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeaders)
|
||||
mockAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeaders)
|
||||
mockI18n.d.mockImplementation((date, options) => {
|
||||
// Mock i18n date formatting
|
||||
if (options?.month === 'short') {
|
||||
@@ -118,7 +118,7 @@ describe('useCustomerEventsService', () => {
|
||||
limit: 10
|
||||
})
|
||||
|
||||
expect(mockFirebaseAuthStore.getAuthHeader).toHaveBeenCalled()
|
||||
expect(mockAuthStore.getAuthHeader).toHaveBeenCalled()
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/customers/events', {
|
||||
params: { page: 1, limit: 10 },
|
||||
headers: mockAuthHeaders
|
||||
@@ -141,7 +141,7 @@ describe('useCustomerEventsService', () => {
|
||||
})
|
||||
|
||||
it('should return null when auth headers are missing', async () => {
|
||||
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(null)
|
||||
mockAuthStore.getAuthHeader.mockResolvedValue(null)
|
||||
|
||||
const result = await service.getMyEvents()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ref, watch } from 'vue'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { d } from '@/i18n'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
@@ -180,7 +180,7 @@ export const useCustomerEventsService = () => {
|
||||
}
|
||||
|
||||
// Get auth headers
|
||||
const authHeaders = await useFirebaseAuthStore().getAuthHeader()
|
||||
const authHeaders = await useAuthStore().getAuthHeader()
|
||||
if (!authHeaders) {
|
||||
error.value = 'Authentication header is missing'
|
||||
return null
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { ApiKeyAuthHeader } from '@/types/authTypes'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
@@ -15,7 +15,7 @@ type ComfyApiUser =
|
||||
const STORAGE_KEY = 'comfy_api_key'
|
||||
|
||||
export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const apiKey = useLocalStorage<string | null>(STORAGE_KEY, null)
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
@@ -24,7 +24,7 @@ export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
|
||||
const isAuthenticated = computed(() => !!currentUser.value)
|
||||
|
||||
const initializeUserFromApiKey = async () => {
|
||||
const createCustomerResponse = await firebaseAuthStore
|
||||
const createCustomerResponse = await authStore
|
||||
.createCustomer()
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
|
||||
@@ -7,7 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as vuefire from 'vuefire'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
|
||||
// Hoisted mocks for dynamic imports
|
||||
@@ -122,8 +122,8 @@ vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useFirebaseAuthStore', () => {
|
||||
let store: ReturnType<typeof useFirebaseAuthStore>
|
||||
describe('useAuthStore', () => {
|
||||
let store: ReturnType<typeof useAuthStore>
|
||||
let authStateCallback: (user: User | null) => void
|
||||
let idTokenCallback: (user: User | null) => void
|
||||
|
||||
@@ -182,7 +182,7 @@ describe('useFirebaseAuthStore', () => {
|
||||
|
||||
// Initialize Pinia
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useFirebaseAuthStore()
|
||||
store = useAuthStore()
|
||||
|
||||
// Reset and set up getIdToken mock
|
||||
mockUser.getIdToken.mockReset()
|
||||
@@ -210,8 +210,8 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
const storeModule = await import('@/stores/firebaseAuthStore')
|
||||
store = storeModule.useFirebaseAuthStore()
|
||||
const storeModule = await import('@/stores/authStore')
|
||||
store = storeModule.useAuthStore()
|
||||
})
|
||||
|
||||
it("should not increment tokenRefreshTrigger on the user's first ID token event", () => {
|
||||
@@ -49,14 +49,14 @@ export type BillingPortalTargetTier = NonNullable<
|
||||
>['application/json']
|
||||
>['target_tier']
|
||||
|
||||
export class FirebaseAuthStoreError extends Error {
|
||||
export class AuthStoreError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FirebaseAuthStoreError'
|
||||
this.name = 'AuthStoreError'
|
||||
}
|
||||
}
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
// State
|
||||
@@ -241,9 +241,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
try {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/balance'), {
|
||||
@@ -259,7 +257,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToFetchBalance', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -279,7 +277,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const createCustomer = async (): Promise<CreateCustomerResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const createCustomerRes = await fetch(buildApiUrl('/customers'), {
|
||||
@@ -290,7 +288,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: createCustomerRes.statusText
|
||||
})
|
||||
@@ -300,7 +298,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const createCustomerResJson: CreateCustomerResponse =
|
||||
await createCustomerRes.json()
|
||||
if (!createCustomerResJson?.id) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: 'No customer ID returned'
|
||||
})
|
||||
@@ -431,7 +429,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
/** Update password for current user */
|
||||
const _updatePassword = async (newPassword: string): Promise<void> => {
|
||||
if (!currentUser.value) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
await updatePassword(currentUser.value, newPassword)
|
||||
}
|
||||
@@ -441,7 +439,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
@@ -461,7 +459,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToInitiateCreditPurchase', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -481,7 +479,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
): Promise<AccessBillingPortalResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/billing'), {
|
||||
@@ -497,7 +495,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -50,12 +50,12 @@ vi.mock('@/stores/userStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockIsFirebaseInitialized = ref(false)
|
||||
const mockIsFirebaseAuthenticated = ref(false)
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
isInitialized: mockIsFirebaseInitialized,
|
||||
isAuthenticated: mockIsFirebaseAuthenticated
|
||||
const mockIsAuthInitialized = ref(false)
|
||||
const mockIsAuthAuthenticated = ref(false)
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
isInitialized: mockIsAuthInitialized,
|
||||
isAuthenticated: mockIsAuthAuthenticated
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -67,8 +67,8 @@ vi.mock('@/platform/distribution/types', () => mockDistributionTypes)
|
||||
describe('bootstrapStore', () => {
|
||||
beforeEach(() => {
|
||||
mockIsSettingsReady.value = false
|
||||
mockIsFirebaseInitialized.value = false
|
||||
mockIsFirebaseAuthenticated.value = false
|
||||
mockIsAuthInitialized.value = false
|
||||
mockIsAuthAuthenticated.value = false
|
||||
mockNeedsLogin.value = false
|
||||
mockDistributionTypes.isCloud = false
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
@@ -107,14 +107,14 @@ describe('bootstrapStore', () => {
|
||||
expect(settingStore.isReady).toBe(false)
|
||||
|
||||
// Firebase initialized but user not yet authenticated
|
||||
mockIsFirebaseInitialized.value = true
|
||||
mockIsAuthInitialized.value = true
|
||||
await nextTick()
|
||||
|
||||
expect(store.isI18nReady).toBe(false)
|
||||
expect(settingStore.isReady).toBe(false)
|
||||
|
||||
// User authenticates (e.g. signs in on login page)
|
||||
mockIsFirebaseAuthenticated.value = true
|
||||
mockIsAuthAuthenticated.value = true
|
||||
await bootstrapPromise
|
||||
|
||||
await vi.waitFor(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user