Compare commits
9 Commits
cloud/1.37
...
austin/und
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
177ad4da68 | ||
|
|
97a78f4a35 | ||
|
|
4b2e4c59af | ||
|
|
6cbb83a1e2 | ||
|
|
14866ac11b | ||
|
|
6f3855f5f1 | ||
|
|
0f1e54530c | ||
|
|
8d622e6519 | ||
|
|
3021ea9afe |
11
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -144,9 +144,10 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Setup pnpm/node to run playwright merge-reports (no browsers needed)
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Download blob reports
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -158,10 +159,10 @@ jobs:
|
||||
- name: Merge into HTML Report
|
||||
run: |
|
||||
# Generate HTML report
|
||||
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
|
||||
pnpm dlx @playwright/test merge-reports --reporter=html ./all-blob-reports
|
||||
# Generate JSON report separately with explicit output path
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
|
||||
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -159,8 +159,8 @@ export class VueNodeHelpers {
|
||||
getInputNumberControls(widget: Locator) {
|
||||
return {
|
||||
input: widget.locator('input'),
|
||||
incrementButton: widget.locator('button').first(),
|
||||
decrementButton: widget.locator('button').nth(1)
|
||||
decrementButton: widget.getByTestId('decrement'),
|
||||
incrementButton: widget.getByTestId('increment')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 105 KiB |
@@ -194,7 +194,10 @@ test.describe('Image widget', () => {
|
||||
const comboEntry = comfyPage.page.getByRole('menuitem', {
|
||||
name: 'image32x32.webp'
|
||||
})
|
||||
await comboEntry.click({ noWaitAfter: true })
|
||||
await comboEntry.click()
|
||||
|
||||
// Stabilization for the image swap
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Expect the image preview to change automatically
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 46 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.37.10",
|
||||
"version": "1.38.0",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -4041,6 +4041,11 @@ export class LGraphNode
|
||||
*/
|
||||
#arrangeWidgets(widgetStartY: number): void {
|
||||
if (!this.widgets || !this.widgets.length) return
|
||||
const realWidgets = this.widgets.filter((w) => w)
|
||||
if (realWidgets.length != this.widgets.length) {
|
||||
console.error('Removing undefined widgets from', this)
|
||||
this.widgets.splice(0, this.widgets.length, ...realWidgets)
|
||||
}
|
||||
|
||||
const bodyHeight = this.bodyHeight
|
||||
const startY =
|
||||
|
||||
@@ -8,3 +8,18 @@ import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
export function getWidgetStep(options: IWidgetOptions<unknown>): number {
|
||||
return options.step2 || (options.step || 10) * 0.1
|
||||
}
|
||||
|
||||
export function evaluateInput(input: string): number | undefined {
|
||||
// Check if v is a valid equation or a number
|
||||
if (/^[\d\s.()*+/-]+$/.test(input)) {
|
||||
// Solve the equation if possible
|
||||
try {
|
||||
input = eval(input)
|
||||
} catch {
|
||||
// Ignore eval errors
|
||||
}
|
||||
}
|
||||
const newValue = Number(input)
|
||||
if (isNaN(newValue)) return undefined
|
||||
return newValue
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IChartWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class ChartWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'Chart: Vue-only'
|
||||
const text = `Chart: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IColorWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class ColorWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'Color: Vue-only'
|
||||
const text = `Color: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IFileUploadWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class FileUploadWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'Fileupload: Vue-only'
|
||||
const text = `Fileupload: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IGalleriaWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class GalleriaWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'Galleria: Vue-only'
|
||||
const text = `Galleria: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IImageCompareWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class ImageCompareWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'ImageCompare: Vue-only'
|
||||
const text = `ImageCompare: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IMarkdownWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class MarkdownWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'Markdown: Vue-only'
|
||||
const text = `Markdown: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { IMultiSelectWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class MultiSelectWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'MultiSelect: Vue-only'
|
||||
const text = `MultiSelect: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { getWidgetStep } from '@/lib/litegraph/src/utils/widget'
|
||||
import { evaluateInput, getWidgetStep } from '@/lib/litegraph/src/utils/widget'
|
||||
|
||||
import { BaseSteppedWidget } from './BaseSteppedWidget'
|
||||
import type { WidgetEventOptions } from './BaseWidget'
|
||||
@@ -68,19 +68,8 @@ export class NumberWidget
|
||||
'Value',
|
||||
this.value,
|
||||
(v: string) => {
|
||||
// Check if v is a valid equation or a number
|
||||
if (/^[\d\s()*+/-]+|\d+\.\d+$/.test(v)) {
|
||||
// Solve the equation if possible
|
||||
try {
|
||||
v = eval(v)
|
||||
} catch {
|
||||
// Ignore eval errors
|
||||
}
|
||||
}
|
||||
const newValue = Number(v)
|
||||
if (!isNaN(newValue)) {
|
||||
this.setValue(newValue, { e, node, canvas })
|
||||
}
|
||||
const parsed = evaluateInput(v)
|
||||
if (parsed !== undefined) this.setValue(parsed, { e, node, canvas })
|
||||
},
|
||||
e
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ISelectButtonWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class SelectButtonWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'SelectButton: Vue-only'
|
||||
const text = `SelectButton: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ITextareaWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class TextareaWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'Textarea: Vue-only'
|
||||
const text = `Textarea: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ITreeSelectWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
@@ -29,7 +31,7 @@ export class TreeSelectWidget
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = 'TreeSelect: Vue-only'
|
||||
const text = `TreeSelect: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "تحديث تعريفات العقد"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "إعادة تسمية سير العمل"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "حفظ سير العمل"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "ابحث عنه في قسم {type} من مكتبة النماذج.",
|
||||
"finish": "إنهاء",
|
||||
"genericLinkPlaceholder": "الصق الرابط هنا",
|
||||
"importAnother": "استيراد آخر",
|
||||
"jobId": "معرّف المهمة",
|
||||
"loadingModels": "جارٍ تحميل {type}...",
|
||||
"maxFileSize": "الحد الأقصى لحجم الملف: {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "إعادة",
|
||||
"Refresh Node Definitions": "تحديث تعريفات العقد",
|
||||
"Reinstall": "إعادة التثبيت",
|
||||
"Rename": "إعادة التسمية",
|
||||
"Reset View": "إعادة تعيين العرض",
|
||||
"Resize Selected Nodes": "تغيير حجم العقد المحددة",
|
||||
"Restart": "إعادة التشغيل",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Refresh Node Definitions"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "Rename Workflow"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "Save Workflow"
|
||||
},
|
||||
|
||||
@@ -1176,6 +1176,7 @@
|
||||
"Queue Selected Output Nodes": "Queue Selected Output Nodes",
|
||||
"Redo": "Redo",
|
||||
"Refresh Node Definitions": "Refresh Node Definitions",
|
||||
"Rename": "Rename",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Show Settings Dialog": "Show Settings Dialog",
|
||||
@@ -2082,6 +2083,7 @@
|
||||
"Set Group Nodes to Always": "Set Group Nodes to Always"
|
||||
},
|
||||
"widgets": {
|
||||
"node2only": "Node 2.0 only",
|
||||
"selectModel": "Select model",
|
||||
"uploadSelect": {
|
||||
"placeholder": "Select...",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Actualizar Definiciones de Nodo"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "Renombrar flujo de trabajo"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "Guardar Flujo de Trabajo"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "Encuéntralo en la sección {type} de la biblioteca de modelos.",
|
||||
"finish": "Finalizar",
|
||||
"genericLinkPlaceholder": "Pega el enlace aquí",
|
||||
"importAnother": "Importar otro",
|
||||
"jobId": "ID de tarea",
|
||||
"loadingModels": "Cargando {type}...",
|
||||
"maxFileSize": "Tamaño máximo de archivo: {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "Rehacer",
|
||||
"Refresh Node Definitions": "Actualizar definiciones de nodo",
|
||||
"Reinstall": "Reinstalar",
|
||||
"Rename": "Renombrar",
|
||||
"Reset View": "Restablecer vista",
|
||||
"Resize Selected Nodes": "Redimensionar Nodos Seleccionados",
|
||||
"Restart": "Reiniciar",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "بهروزرسانی تعاریف نود"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "تغییر نام Workflow"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "ذخیره ورکفلو"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "در بخش {type} کتابخانه مدلها پیدا کنید.",
|
||||
"finish": "پایان",
|
||||
"genericLinkPlaceholder": "لینک را اینجا وارد کنید",
|
||||
"importAnother": "وارد کردن مورد دیگر",
|
||||
"jobId": "شناسه کار: {jobId}",
|
||||
"loadingModels": "در حال بارگذاری {type}...",
|
||||
"maxFileSize": "حداکثر اندازه فایل: {size}",
|
||||
@@ -237,22 +238,6 @@
|
||||
"title": "ایجاد حساب کاربری"
|
||||
}
|
||||
},
|
||||
"authTimeout": {
|
||||
"causes": [
|
||||
"فایروال یا پراکسی سازمانی که سرویسهای احراز هویت را مسدود میکند",
|
||||
"محدودیتهای VPN یا شبکه",
|
||||
"افزونههای مرورگر که در درخواستها اختلال ایجاد میکنند",
|
||||
"محدودیتهای منطقهای شبکه",
|
||||
"امتحان مرورگر یا شبکه دیگر"
|
||||
],
|
||||
"helpText": "نیاز به کمک دارید؟ تماس با",
|
||||
"message": "در اتصال به ComfyUI Cloud با مشکل مواجه شدهایم. ممکن است به دلیل کندی اتصال یا مشکل موقت سرویس باشد.",
|
||||
"restart": "خروج و تلاش مجدد",
|
||||
"supportLink": "پشتیبانی",
|
||||
"technicalDetails": "جزئیات فنی",
|
||||
"title": "اتصال بیش از حد طول کشید",
|
||||
"troubleshooting": "دلایل رایج:"
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "پاکسازی workflow",
|
||||
"deleteBlueprint": "حذف blueprint",
|
||||
@@ -261,7 +246,6 @@
|
||||
"enterNewName": "نام جدید را وارد کنید",
|
||||
"missingNodesWarning": "workflow شامل نودهای پشتیبانینشده است (با رنگ قرمز مشخص شدهاند)."
|
||||
},
|
||||
"checkingStatus": "در حال بررسی وضعیت حساب شما...",
|
||||
"clipboard": {
|
||||
"errorMessage": "کپی به کلیپبورد ناموفق بود",
|
||||
"errorNotSupported": "Clipboard API در مرورگر شما پشتیبانی نمیشود",
|
||||
@@ -279,6 +263,49 @@
|
||||
"cloudForgotPassword_sendResetLink": "ارسال لینک بازنشانی",
|
||||
"cloudForgotPassword_title": "فراموشی رمز عبور",
|
||||
"cloudOnboarding": {
|
||||
"authTimeout": {
|
||||
"causes": [
|
||||
"فایروال یا پراکسی سازمانی که سرویسهای احراز هویت را مسدود میکند",
|
||||
"محدودیتهای VPN یا شبکه",
|
||||
"افزونههای مرورگر که در درخواستها اختلال ایجاد میکنند",
|
||||
"محدودیتهای منطقهای شبکه",
|
||||
"استفاده از مرورگر یا شبکه دیگر را امتحان کنید"
|
||||
],
|
||||
"helpText": "نیاز به راهنمایی دارید؟ تماس با",
|
||||
"message": "در اتصال به ComfyUI Cloud با مشکل مواجه شدیم. این مشکل ممکن است به دلیل کندی اتصال یا اختلال موقت سرویس باشد.",
|
||||
"restart": "خروج و تلاش مجدد",
|
||||
"supportLink": "پشتیبانی",
|
||||
"technicalDetails": "جزئیات فنی",
|
||||
"title": "اتصال بیش از حد طول کشید",
|
||||
"troubleshooting": "دلایل رایج:"
|
||||
},
|
||||
"checkingStatus": "در حال بررسی وضعیت حساب شما...",
|
||||
"forgotPassword": {
|
||||
"backToLogin": "بازگشت به ورود",
|
||||
"didntReceiveEmail": "ایمیلی دریافت نکردید؟ با ما تماس بگیرید:",
|
||||
"emailLabel": "ایمیل",
|
||||
"emailPlaceholder": "ایمیل خود را وارد کنید",
|
||||
"emailRequired": "وارد کردن ایمیل الزامی است",
|
||||
"instructions": "آدرس ایمیل خود را وارد کنید تا لینک بازنشانی رمز عبور برای شما ارسال شود.",
|
||||
"passwordResetError": "ارسال ایمیل بازنشانی رمز عبور ناموفق بود. لطفاً دوباره تلاش کنید.",
|
||||
"passwordResetSent": "ایمیل بازنشانی رمز عبور ارسال شد",
|
||||
"sendResetLink": "ارسال لینک بازنشانی",
|
||||
"title": "فراموشی رمز عبور"
|
||||
},
|
||||
"privateBeta": {
|
||||
"desc": "برای پیوستن به لیست انتظار وارد شوید. زمانی که نوبت شما شد به شما اطلاع خواهیم داد. قبلاً اطلاع داده شدهاید؟ وارد شوید و از Cloud استفاده کنید.",
|
||||
"title": "کلود در حال حاضر در نسخه بتای خصوصی است"
|
||||
},
|
||||
"retry": "تلاش دوباره",
|
||||
"retrying": "در حال تلاش مجدد...",
|
||||
"start": {
|
||||
"desc": "بدون نیاز به تنظیمات اولیه. روی هر دستگاهی کار میکند.",
|
||||
"download": "دانلود ComfyUI",
|
||||
"explain": "چندین خروجی را به طور همزمان تولید کنید. گردشکارها را به راحتی به اشتراک بگذارید.",
|
||||
"learnAboutButton": "درباره Cloud بیشتر بدانید",
|
||||
"title": "در چند ثانیه شروع به خلق کنید",
|
||||
"wantToRun": "میخواهید ComfyUI را به صورت محلی اجرا کنید؟"
|
||||
},
|
||||
"survey": {
|
||||
"options": {
|
||||
"familiarity": {
|
||||
@@ -616,18 +643,6 @@
|
||||
"noStackTrace": "هیچ stacktraceی موجود نیست",
|
||||
"promptExecutionError": "اجرای prompt با شکست مواجه شد"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"backToLogin": "بازگشت به ورود",
|
||||
"didntReceiveEmail": "ایمیلی دریافت نکردید؟ با ما تماس بگیرید:",
|
||||
"emailLabel": "ایمیل",
|
||||
"emailPlaceholder": "ایمیل خود را وارد کنید",
|
||||
"emailRequired": "وارد کردن ایمیل الزامی است",
|
||||
"instructions": "آدرس ایمیل خود را وارد کنید تا لینک بازنشانی رمز عبور برای شما ارسال شود.",
|
||||
"passwordResetError": "ارسال ایمیل بازنشانی رمز عبور ناموفق بود. لطفاً دوباره تلاش کنید.",
|
||||
"passwordResetSent": "ایمیل بازنشانی رمز عبور ارسال شد",
|
||||
"sendResetLink": "ارسال لینک بازنشانی",
|
||||
"title": "فراموشی رمز عبور"
|
||||
},
|
||||
"g": {
|
||||
"1x": "۱x",
|
||||
"2x": "۲x",
|
||||
@@ -1506,6 +1521,7 @@
|
||||
"Redo": "انجام مجدد",
|
||||
"Refresh Node Definitions": "بهروزرسانی تعاریف Node",
|
||||
"Reinstall": "نصب مجدد",
|
||||
"Rename": "تغییر نام",
|
||||
"Reset View": "بازنشانی نما",
|
||||
"Resize Selected Nodes": "تغییر اندازه Nodeهای انتخابشده",
|
||||
"Restart": "راهاندازی مجدد",
|
||||
@@ -1694,10 +1710,6 @@
|
||||
},
|
||||
"title": "دستگاه شما پشتیبانی نمیشود"
|
||||
},
|
||||
"privateBeta": {
|
||||
"desc": "برای پیوستن به لیست انتظار وارد شوید. زمانی که نوبت شما شد به شما اطلاع خواهیم داد. قبلاً اطلاعرسانی شدهاید؟ وارد شوید و از Cloud استفاده کنید.",
|
||||
"title": "Cloud در حال حاضر در نسخه بتای خصوصی است"
|
||||
},
|
||||
"progressToast": {
|
||||
"allDownloadsCompleted": "همه دانلودها تکمیل شدند",
|
||||
"downloadingModel": "در حال دانلود مدل...",
|
||||
@@ -1773,8 +1785,6 @@
|
||||
"update": "بهروزرسانی",
|
||||
"whatsNew": "مشاهده تغییرات جدید"
|
||||
},
|
||||
"retry": "تلاش دوباره",
|
||||
"retrying": "در حال تلاش مجدد...",
|
||||
"rightSidePanel": {
|
||||
"bypass": "عبور",
|
||||
"color": "رنگ نود",
|
||||
@@ -2165,14 +2175,6 @@
|
||||
},
|
||||
"workflows": "Workflowها"
|
||||
},
|
||||
"start": {
|
||||
"desc": "بدون نیاز به تنظیمات. روی هر دستگاهی کار میکند.",
|
||||
"download": "دانلود ComfyUI",
|
||||
"explain": "چندین خروجی را همزمان تولید کنید. workflowها را به راحتی به اشتراک بگذارید.",
|
||||
"learnAboutButton": "درباره Cloud بیشتر بدانید",
|
||||
"title": "در چند ثانیه شروع به خلق کنید",
|
||||
"wantToRun": "مایلید ComfyUI را به صورت محلی اجرا کنید؟"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "نام زیرگراف",
|
||||
"confirmDelete": "این عمل باعث حذف دائمی بلوپرینت از کتابخانه شما میشود",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Actualiser les définitions de nœud"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "Renommer le workflow"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "Enregistrer le flux de travail"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "Trouvez-le dans la section {type} de la bibliothèque de modèles.",
|
||||
"finish": "Terminer",
|
||||
"genericLinkPlaceholder": "Collez le lien ici",
|
||||
"importAnother": "Importer un autre",
|
||||
"jobId": "ID de tâche",
|
||||
"loadingModels": "Chargement de {type}...",
|
||||
"maxFileSize": "Taille maximale du fichier : {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "Refaire",
|
||||
"Refresh Node Definitions": "Actualiser les définitions de nœud",
|
||||
"Reinstall": "Réinstaller",
|
||||
"Rename": "Renommer",
|
||||
"Reset View": "Réinitialiser la vue",
|
||||
"Resize Selected Nodes": "Redimensionner les nœuds sélectionnés",
|
||||
"Restart": "Redémarrer",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "ノード定義を更新"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "ワークフローの名前を変更"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "ワークフローを保存する"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "モデルライブラリの{type}セクションで見つけることができます。",
|
||||
"finish": "完了",
|
||||
"genericLinkPlaceholder": "ここにリンクを貼り付けてください",
|
||||
"importAnother": "別のファイルをインポート",
|
||||
"jobId": "ジョブID",
|
||||
"loadingModels": "{type}を読み込み中...",
|
||||
"maxFileSize": "最大ファイルサイズ:{size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "やり直す",
|
||||
"Refresh Node Definitions": "ノード定義を更新",
|
||||
"Reinstall": "再インストール",
|
||||
"Rename": "名前を変更",
|
||||
"Reset View": "ビューをリセット",
|
||||
"Resize Selected Nodes": "選択したノードのサイズ変更",
|
||||
"Restart": "再起動",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "노드 정의 새로 고침"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "워크플로우 이름 변경"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "워크플로 저장"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "모델 라이브러리의 {type} 섹션에서 찾을 수 있습니다.",
|
||||
"finish": "완료",
|
||||
"genericLinkPlaceholder": "여기에 링크를 붙여넣으세요",
|
||||
"importAnother": "다른 항목 가져오기",
|
||||
"jobId": "작업 ID",
|
||||
"loadingModels": "{type} 불러오는 중...",
|
||||
"maxFileSize": "최대 파일 크기: {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "다시 실행",
|
||||
"Refresh Node Definitions": "노드 정의 새로 고침",
|
||||
"Reinstall": "재설치",
|
||||
"Rename": "이름 바꾸기",
|
||||
"Reset View": "보기 초기화",
|
||||
"Resize Selected Nodes": "선택된 노드 크기 조정",
|
||||
"Restart": "재시작",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Atualizar Definições de Nós"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "Renomear fluxo de trabalho"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "Salvar Fluxo de Trabalho"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "Encontre na seção {type} da biblioteca de modelos.",
|
||||
"finish": "Concluir",
|
||||
"genericLinkPlaceholder": "Cole o link aqui",
|
||||
"importAnother": "Importar outro",
|
||||
"jobId": "ID do trabalho",
|
||||
"loadingModels": "Carregando {type}...",
|
||||
"maxFileSize": "Tamanho máximo do arquivo: {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "Refazer",
|
||||
"Refresh Node Definitions": "Atualizar definições de nós",
|
||||
"Reinstall": "Reinstalar",
|
||||
"Rename": "Renomear",
|
||||
"Reset View": "Redefinir visualização",
|
||||
"Resize Selected Nodes": "Redimensionar nós selecionados",
|
||||
"Restart": "Reiniciar",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Обновить определения нод"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "Переименовать рабочий процесс"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "Сохранить рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "Найдите это в разделе {type} библиотеки моделей.",
|
||||
"finish": "Готово",
|
||||
"genericLinkPlaceholder": "Вставьте ссылку сюда",
|
||||
"importAnother": "Импортировать другой",
|
||||
"jobId": "ID задачи",
|
||||
"loadingModels": "Загрузка {type}...",
|
||||
"maxFileSize": "Максимальный размер файла: {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "Повторить",
|
||||
"Refresh Node Definitions": "Обновить определения нод",
|
||||
"Reinstall": "Переустановить",
|
||||
"Rename": "Переименовать",
|
||||
"Reset View": "Сбросить вид",
|
||||
"Resize Selected Nodes": "Изменить размер выбранных узлов",
|
||||
"Restart": "Перезапустить",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Düğüm Tanımlarını Yenile"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "Çalışma Akışını Yeniden Adlandır"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "İş Akışını Kaydet"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "Bunu modeller kütüphanesinin {type} bölümünde bulabilirsiniz.",
|
||||
"finish": "Bitir",
|
||||
"genericLinkPlaceholder": "Bağlantıyı buraya yapıştırın",
|
||||
"importAnother": "Başka Birini İçe Aktar",
|
||||
"jobId": "İş ID",
|
||||
"loadingModels": "{type} yükleniyor...",
|
||||
"maxFileSize": "Maksimum dosya boyutu: {size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "Yinele",
|
||||
"Refresh Node Definitions": "Düğüm Tanımlarını Yenile",
|
||||
"Reinstall": "Yeniden Yükle",
|
||||
"Rename": "Yeniden Adlandır",
|
||||
"Reset View": "Görünümü Sıfırla",
|
||||
"Resize Selected Nodes": "Seçili Düğümleri Yeniden Boyutlandır",
|
||||
"Restart": "Yeniden Başlat",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "重新整理節點定義"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "重新命名工作流程"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "儲存工作流程"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "可在模型庫的 {type} 區段找到。",
|
||||
"finish": "完成",
|
||||
"genericLinkPlaceholder": "請在此貼上連結",
|
||||
"importAnother": "匯入其他",
|
||||
"jobId": "工作 ID",
|
||||
"loadingModels": "正在載入 {type}...",
|
||||
"maxFileSize": "最大檔案大小:{size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "重做",
|
||||
"Refresh Node Definitions": "重新整理節點定義",
|
||||
"Reinstall": "重新安裝",
|
||||
"Rename": "重新命名",
|
||||
"Reset View": "重設視圖",
|
||||
"Resize Selected Nodes": "調整選取節點大小",
|
||||
"Restart": "重新啟動",
|
||||
|
||||
@@ -254,6 +254,9 @@
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "刷新节点定义"
|
||||
},
|
||||
"Comfy_RenameWorkflow": {
|
||||
"label": "重命名工作流"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "保存工作流"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"findInLibrary": "模型在模型库的{type}区里",
|
||||
"finish": "完成",
|
||||
"genericLinkPlaceholder": "粘贴链接到这",
|
||||
"importAnother": "导入其他",
|
||||
"jobId": "任务ID",
|
||||
"loadingModels": "正在加载{type}...",
|
||||
"maxFileSize": "最大文件大小:{size}",
|
||||
@@ -1520,6 +1521,7 @@
|
||||
"Redo": "重做",
|
||||
"Refresh Node Definitions": "刷新节点定义",
|
||||
"Reinstall": "重装",
|
||||
"Rename": "重命名",
|
||||
"Reset View": "重置视图",
|
||||
"Resize Selected Nodes": "调整选定节点的大小",
|
||||
"Restart": "重启",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
'gap-2 select-none group',
|
||||
selected
|
||||
? 'ring-3 ring-inset ring-modal-card-border-highlighted'
|
||||
: 'hover:bg-modal-card-background-hovered'
|
||||
: 'hover:bg-modal-card-background-hovered/20'
|
||||
)
|
||||
"
|
||||
:data-selected="selected"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetInputNumberInput from './WidgetInputNumberInput.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en'
|
||||
})
|
||||
|
||||
function createMockWidget(
|
||||
value: number = 0,
|
||||
type: 'int' | 'float' = 'int',
|
||||
@@ -24,10 +28,7 @@ function createMockWidget(
|
||||
|
||||
function mountComponent(widget: SimplifiedWidget<number>, modelValue: number) {
|
||||
return mount(WidgetInputNumberInput, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { InputNumber }
|
||||
},
|
||||
global: { plugins: [i18n] },
|
||||
props: {
|
||||
widget,
|
||||
modelValue
|
||||
@@ -36,7 +37,7 @@ function mountComponent(widget: SimplifiedWidget<number>, modelValue: number) {
|
||||
}
|
||||
|
||||
function getNumberInput(wrapper: ReturnType<typeof mount>) {
|
||||
const input = wrapper.get<HTMLInputElement>('input[inputmode="numeric"]')
|
||||
const input = wrapper.get<HTMLInputElement>('input[inputmode="decimal"]')
|
||||
return input.element
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ describe('WidgetInputNumberInput Value Binding', () => {
|
||||
const widget = createMockWidget(10, 'int')
|
||||
const wrapper = mountComponent(widget, 10)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
const inputNumber = wrapper
|
||||
await inputNumber.vm.$emit('update:modelValue', 20)
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
@@ -78,75 +79,6 @@ describe('WidgetInputNumberInput Value Binding', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('WidgetInputNumberInput Component Rendering', () => {
|
||||
it('renders InputNumber component with show-buttons', () => {
|
||||
const widget = createMockWidget(5, 'int')
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.exists()).toBe(true)
|
||||
expect(inputNumber.props('showButtons')).toBe(true)
|
||||
})
|
||||
|
||||
it('sets button layout to horizontal', () => {
|
||||
const widget = createMockWidget(5, 'int')
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('buttonLayout')).toBe('horizontal')
|
||||
})
|
||||
|
||||
it('sets size to small', () => {
|
||||
const widget = createMockWidget(5, 'int')
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('size')).toBe('small')
|
||||
})
|
||||
})
|
||||
|
||||
describe('WidgetInputNumberInput Step Value', () => {
|
||||
it('defaults to 0 for unrestricted stepping', () => {
|
||||
const widget = createMockWidget(5, 'int')
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('step')).toBe(0)
|
||||
})
|
||||
|
||||
it('uses step2 value when provided', () => {
|
||||
const widget = createMockWidget(5, 'int', { step2: 0.5 })
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('step')).toBe(0.5)
|
||||
})
|
||||
|
||||
it('calculates step from precision for precision 0', () => {
|
||||
const widget = createMockWidget(5, 'int', { precision: 0 })
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('step')).toBe(1)
|
||||
})
|
||||
|
||||
it('calculates step from precision for precision 1', () => {
|
||||
const widget = createMockWidget(5, 'float', { precision: 1 })
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('step')).toBe(0.1)
|
||||
})
|
||||
|
||||
it('calculates step from precision for precision 2', () => {
|
||||
const widget = createMockWidget(5, 'float', { precision: 2 })
|
||||
const wrapper = mountComponent(widget, 5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('step')).toBe(0.01)
|
||||
})
|
||||
})
|
||||
|
||||
describe('WidgetInputNumberInput Grouping Behavior', () => {
|
||||
it('displays numbers without commas by default for int widgets', () => {
|
||||
const widget = createMockWidget(1000, 'int')
|
||||
@@ -202,24 +134,21 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => {
|
||||
const widget = createMockWidget(1000, 'int')
|
||||
const wrapper = mountComponent(widget, 1000)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(true)
|
||||
expect(wrapper.findAll('button').length).toBe(2)
|
||||
})
|
||||
|
||||
it('shows buttons for values at safe integer limit', () => {
|
||||
const widget = createMockWidget(SAFE_INTEGER_MAX, 'int')
|
||||
const wrapper = mountComponent(widget, SAFE_INTEGER_MAX)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(true)
|
||||
expect(wrapper.findAll('button').length).toBe(2)
|
||||
})
|
||||
|
||||
it('hides buttons for unsafe large integer values', () => {
|
||||
const widget = createMockWidget(UNSAFE_LARGE_INTEGER, 'int')
|
||||
const wrapper = mountComponent(widget, UNSAFE_LARGE_INTEGER)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(false)
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
|
||||
it('hides buttons for unsafe negative integer values', () => {
|
||||
@@ -227,8 +156,7 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => {
|
||||
const widget = createMockWidget(unsafeNegative, 'int')
|
||||
const wrapper = mountComponent(widget, unsafeNegative)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(false)
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
|
||||
it('shows tooltip for disabled buttons due to precision limits', (context) => {
|
||||
@@ -250,43 +178,19 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => {
|
||||
expect(tooltipDiv.attributes('v-tooltip')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles edge case of zero value', () => {
|
||||
const widget = createMockWidget(0, 'int')
|
||||
const wrapper = mountComponent(widget, 0)
|
||||
it('handles floating point values correctly', () => {
|
||||
const widget = createMockWidget(1000.5, 'float')
|
||||
const wrapper = mountComponent(widget, 1000.5)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(true)
|
||||
expect(wrapper.findAll('button').length).toBe(2)
|
||||
})
|
||||
|
||||
it('correctly identifies safe vs unsafe integers using Number.isSafeInteger', () => {
|
||||
// Test the JavaScript behavior our component relies on
|
||||
expect(Number.isSafeInteger(SAFE_INTEGER_MAX)).toBe(true)
|
||||
expect(Number.isSafeInteger(SAFE_INTEGER_MAX + 1)).toBe(false)
|
||||
expect(Number.isSafeInteger(UNSAFE_LARGE_INTEGER)).toBe(false)
|
||||
expect(Number.isSafeInteger(-SAFE_INTEGER_MAX)).toBe(true)
|
||||
expect(Number.isSafeInteger(-SAFE_INTEGER_MAX - 1)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles floating point values correctly', (context) => {
|
||||
context.skip('needs diagnosis')
|
||||
|
||||
const safeFloat = 1000.5
|
||||
const widget = createMockWidget(safeFloat, 'float')
|
||||
const wrapper = mountComponent(widget, safeFloat)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(true)
|
||||
})
|
||||
|
||||
it('hides buttons for unsafe floating point values', (context) => {
|
||||
context.skip('needs diagnosis')
|
||||
|
||||
it('hides buttons for unsafe floating point values', () => {
|
||||
const unsafeFloat = UNSAFE_LARGE_INTEGER + 0.5
|
||||
const widget = createMockWidget(unsafeFloat, 'float')
|
||||
const wrapper = mountComponent(widget, unsafeFloat)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(false)
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -295,18 +199,14 @@ describe('WidgetInputNumberInput Edge Cases for Precision Handling', () => {
|
||||
const widget = createMockWidget(0, 'int')
|
||||
// Mount with undefined as modelValue
|
||||
const wrapper = mount(WidgetInputNumberInput, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { InputNumber }
|
||||
},
|
||||
global: { plugins: [i18n] },
|
||||
props: {
|
||||
widget,
|
||||
modelValue: undefined as any
|
||||
}
|
||||
})
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(true) // Should default to safe behavior
|
||||
expect(wrapper.findAll('button').length).toBe(2)
|
||||
})
|
||||
|
||||
it('handles NaN values gracefully', (context) => {
|
||||
@@ -314,24 +214,20 @@ describe('WidgetInputNumberInput Edge Cases for Precision Handling', () => {
|
||||
const widget = createMockWidget(NaN, 'int')
|
||||
const wrapper = mountComponent(widget, NaN)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
// NaN is not a safe integer, so buttons should be hidden
|
||||
expect(inputNumber.props('showButtons')).toBe(false)
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
|
||||
it('handles Infinity values', () => {
|
||||
const widget = createMockWidget(Infinity, 'int')
|
||||
const wrapper = mountComponent(widget, Infinity)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(false)
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
|
||||
it('handles negative Infinity values', () => {
|
||||
const widget = createMockWidget(-Infinity, 'int')
|
||||
const wrapper = mountComponent(widget, -Infinity)
|
||||
|
||||
const inputNumber = wrapper.findComponent(InputNumber)
|
||||
expect(inputNumber.props('showButtons')).toBe(false)
|
||||
expect(wrapper.findAll('button').length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
@@ -12,12 +14,68 @@ import {
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const { n } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
}>()
|
||||
|
||||
const widgetContainer = useTemplateRef<HTMLDivElement>('widgetContainer')
|
||||
const inputField = useTemplateRef<HTMLInputElement>('inputField')
|
||||
const textEdit = ref(false)
|
||||
onClickOutside(widgetContainer, () => {
|
||||
if (textEdit.value) {
|
||||
textEdit.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const decimalSeparator = computed(() => n(1.1).replace(/\p{Number}/gu, ''))
|
||||
const groupSeparator = computed(() => n(11111).replace(/\p{Number}/gu, ''))
|
||||
function unformatValue(value: string) {
|
||||
return value
|
||||
.replaceAll(groupSeparator.value, '')
|
||||
.replaceAll(decimalSeparator.value, '.')
|
||||
}
|
||||
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
const unformattedValue = dragValue.value ?? modelValue.value
|
||||
if (!isFinite(unformattedValue)) return `${unformattedValue}`
|
||||
|
||||
return n(unformattedValue, {
|
||||
useGrouping: useGrouping.value,
|
||||
minimumFractionDigits: precision.value,
|
||||
maximumFractionDigits: precision.value
|
||||
})
|
||||
})
|
||||
|
||||
function updateValue(e: UIEvent) {
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLInputElement)) return
|
||||
const parsed = evaluateInput(unformatValue(target.value))
|
||||
if (parsed !== undefined)
|
||||
modelValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, parsed)
|
||||
)
|
||||
else target.value = formattedValue.value
|
||||
|
||||
textEdit.value = false
|
||||
}
|
||||
|
||||
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
|
||||
const canDecrement = computed(
|
||||
() =>
|
||||
modelValue.value > filteredProps.value.min &&
|
||||
!props.widget.options?.disabled
|
||||
)
|
||||
const canIncrement = computed(
|
||||
() =>
|
||||
modelValue.value < filteredProps.value.max &&
|
||||
!props.widget.options?.disabled
|
||||
)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
@@ -69,6 +127,48 @@ const buttonsDisabled = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
function updateValueBy(delta: number) {
|
||||
modelValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, modelValue.value + delta)
|
||||
)
|
||||
}
|
||||
|
||||
const dragValue = ref<number>()
|
||||
const dragDelta = ref(0)
|
||||
function handleMouseDown(e: PointerEvent) {
|
||||
if (props.widget.options?.disabled) return
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
target.setPointerCapture(e.pointerId)
|
||||
dragValue.value = modelValue.value
|
||||
dragDelta.value = 0
|
||||
}
|
||||
function handleMouseMove(e: PointerEvent) {
|
||||
if (dragValue.value === undefined) return
|
||||
dragDelta.value += e.movementX
|
||||
const unclippedValue =
|
||||
dragValue.value + ((dragDelta.value / 10) | 0) * stepValue.value
|
||||
dragDelta.value %= 10
|
||||
dragValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, unclippedValue)
|
||||
)
|
||||
}
|
||||
function handleMouseUp() {
|
||||
const newValue = dragValue.value
|
||||
if (newValue === undefined) return
|
||||
modelValue.value = newValue
|
||||
dragValue.value = undefined
|
||||
|
||||
if (dragDelta.value === 0) {
|
||||
textEdit.value = true
|
||||
inputField.value?.focus()
|
||||
inputField.value?.setSelectionRange(0, -1)
|
||||
}
|
||||
dragDelta.value = 0
|
||||
}
|
||||
|
||||
const buttonTooltip = computed(() => {
|
||||
if (buttonsDisabled.value) {
|
||||
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
|
||||
@@ -79,54 +179,80 @@ const buttonTooltip = computed(() => {
|
||||
|
||||
<template>
|
||||
<WidgetLayoutField :widget>
|
||||
<InputNumber
|
||||
v-model="modelValue"
|
||||
<div
|
||||
ref="widgetContainer"
|
||||
v-tooltip="buttonTooltip"
|
||||
v-bind="filteredProps"
|
||||
fluid
|
||||
button-layout="horizontal"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
:step="stepValue"
|
||||
:min-fraction-digits="precision"
|
||||
:max-fraction-digits="precision"
|
||||
:use-grouping="useGrouping"
|
||||
:class="cn(WidgetInputBaseClass, 'grow text-xs')"
|
||||
:aria-label="widget.name"
|
||||
:show-buttons="!buttonsDisabled"
|
||||
:pt="{
|
||||
root: {
|
||||
class: cn(
|
||||
'[&>input]:bg-transparent [&>input]:border-0',
|
||||
'[&>input]:truncate [&>input]:min-w-[4ch]',
|
||||
$slots.default && '[&>input]:pr-7'
|
||||
)
|
||||
},
|
||||
decrementButton: {
|
||||
class: 'w-8 border-0'
|
||||
},
|
||||
incrementButton: {
|
||||
class: 'w-8 border-0'
|
||||
}
|
||||
}"
|
||||
:class="cn(WidgetInputBaseClass, 'grow text-xs flex h-7')"
|
||||
>
|
||||
<template #incrementicon>
|
||||
<span class="pi pi-plus text-sm" />
|
||||
</template>
|
||||
<template #decrementicon>
|
||||
<span class="pi pi-minus text-sm" />
|
||||
</template>
|
||||
</InputNumber>
|
||||
<div class="absolute top-5 right-8 h-4 w-7 -translate-y-4/5 flex">
|
||||
<button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="decrement"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-minus', !canDecrement && 'opacity-60')
|
||||
"
|
||||
:disabled="!canDecrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue -= stepValue"
|
||||
/>
|
||||
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
|
||||
<input
|
||||
ref="inputField"
|
||||
:aria-valuenow="dragValue ?? modelValue"
|
||||
:aria-valuemin="filteredProps.min"
|
||||
:aria-valuemax="filteredProps.max"
|
||||
:class="
|
||||
cn(
|
||||
'bg-transparent border-0 focus:outline-0 p-1 truncate text-sm absolute inset-0'
|
||||
)
|
||||
"
|
||||
inputmode="decimal"
|
||||
:value="formattedValue"
|
||||
role="spinbutton"
|
||||
tabindex="0"
|
||||
:disabled="widget.options?.disabled"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
@blur="updateValue"
|
||||
@keyup.enter="updateValue"
|
||||
@keydown.up.prevent="updateValueBy(stepValue)"
|
||||
@keydown.down.prevent="updateValueBy(-stepValue)"
|
||||
@keydown.page-up.prevent="updateValueBy(10 * stepValue)"
|
||||
@keydown.page-down.prevent="updateValueBy(-10 * stepValue)"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-0 z-10 cursor-ew-resize',
|
||||
textEdit && 'hidden pointer-events-none'
|
||||
)
|
||||
"
|
||||
@pointerdown="handleMouseDown"
|
||||
@pointermove="handleMouseMove"
|
||||
@pointerup="handleMouseUp"
|
||||
@pointercancel="
|
||||
() => {
|
||||
dragValue = undefined
|
||||
dragDelta = 0
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
<button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="increment"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-plus', !canIncrement && 'opacity-60')
|
||||
"
|
||||
:disabled="!canIncrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue += stepValue"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-inputnumber-input) {
|
||||
height: 1.625rem;
|
||||
margin: 1px 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@ const togglePopover = (event: Event) => {
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
|
||||
@pointerdown.stop.prevent="togglePopover"
|
||||
@click.stop.prevent="togglePopover"
|
||||
>
|
||||
<i :class="`${controlButtonIcon} text-blue-100 text-xs size-3.5`" />
|
||||
</Button>
|
||||
|
||||