Compare commits
146 Commits
node-templ
...
v1.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c35d29f31c | ||
|
|
e50d7c5eef | ||
|
|
7c2cce40de | ||
|
|
2017b9016b | ||
|
|
0bf30e7621 | ||
|
|
7f5b685c9f | ||
|
|
ec824579d6 | ||
|
|
9e565154a9 | ||
|
|
541335bb31 | ||
|
|
814c4b8ef0 | ||
|
|
df3fff5dbb | ||
|
|
b0085114d7 | ||
|
|
7f9c70386f | ||
|
|
578870d345 | ||
|
|
05fab91bda | ||
|
|
5191e11650 | ||
|
|
92079a653e | ||
|
|
a7d14eb815 | ||
|
|
9aea6eae70 | ||
|
|
88a42172c5 | ||
|
|
e79013dcfe | ||
|
|
08f3370828 | ||
|
|
c4d3c672ad | ||
|
|
39eaa2e850 | ||
|
|
2d022e4e49 | ||
|
|
1ac6d6529f | ||
|
|
86fec820ac | ||
|
|
030d5845db | ||
|
|
dd1c878fdf | ||
|
|
3942603a38 | ||
|
|
244578db96 | ||
|
|
6b6edfde9f | ||
|
|
c54b675a48 | ||
|
|
b7008dfc5c | ||
|
|
d0ad4af51c | ||
|
|
4a182014e1 | ||
|
|
46cd522384 | ||
|
|
c977667a15 | ||
|
|
d531bc34c4 | ||
|
|
adfbec2744 | ||
|
|
23521559bf | ||
|
|
51f57aba17 | ||
|
|
97bab053df | ||
|
|
c1c5573e7f | ||
|
|
16d2a95760 | ||
|
|
f97b673481 | ||
|
|
c8d5a6f154 | ||
|
|
3708afaf21 | ||
|
|
43c23e526c | ||
|
|
a80eb84df1 | ||
|
|
f89898b3d0 | ||
|
|
af21142602 | ||
|
|
4b91860227 | ||
|
|
e53bafbca6 | ||
|
|
e01c8f06c7 | ||
|
|
c61ed4da37 | ||
|
|
4a4d6d070a | ||
|
|
4bedd873a1 | ||
|
|
f8bd910e63 | ||
|
|
1160231b62 | ||
|
|
a51e27bedf | ||
|
|
abed0656af | ||
|
|
5febda16c7 | ||
|
|
069dc67c30 | ||
|
|
7623810166 | ||
|
|
21fa88461f | ||
|
|
27b0493306 | ||
|
|
ad2c1a0d3e | ||
|
|
f51866d988 | ||
|
|
46627bb44b | ||
|
|
68cadbda9f | ||
|
|
0f2260065a | ||
|
|
4007cc13c2 | ||
|
|
3920210c5c | ||
|
|
4e22bffae2 | ||
|
|
462a131557 | ||
|
|
ec01a04786 | ||
|
|
4c48241e19 | ||
|
|
886c40a69a | ||
|
|
479d1b28c7 | ||
|
|
c41b57128a | ||
|
|
5d178a407d | ||
|
|
73b7606f6e | ||
|
|
94f5031f0d | ||
|
|
c857e7d98c | ||
|
|
d5b8a555d9 | ||
|
|
f34d50da3d | ||
|
|
4f3693e322 | ||
|
|
431ad7d27f | ||
|
|
0c97b09a5a | ||
|
|
bdb9f0d845 | ||
|
|
77b85acdd5 | ||
|
|
8906f5c26e | ||
|
|
81194cc7fe | ||
|
|
f4b972fab5 | ||
|
|
3aa1c03566 | ||
|
|
600b7f93e5 | ||
|
|
2a7df57404 | ||
|
|
6352cd86ee | ||
|
|
0058691579 | ||
|
|
1531bb6d9f | ||
|
|
40245aacf9 | ||
|
|
6e49685f58 | ||
|
|
946823ce6c | ||
|
|
c05f1465db | ||
|
|
88164bdac5 | ||
|
|
fc9e347055 | ||
|
|
6fbf1248f4 | ||
|
|
56848724cd | ||
|
|
26c3eeb942 | ||
|
|
a8f869337e | ||
|
|
7e245ba1cf | ||
|
|
2a93f873b4 | ||
|
|
f8e7058e19 | ||
|
|
8d4e740baa | ||
|
|
3273ee938b | ||
|
|
94f1bc3b38 | ||
|
|
d5ce140eb6 | ||
|
|
b5f0c4bf73 | ||
|
|
545a990365 | ||
|
|
71e4a42cfe | ||
|
|
16b0ebf75a | ||
|
|
eaeb17bdc7 | ||
|
|
239b464957 | ||
|
|
00b6d989ec | ||
|
|
c5f05b1855 | ||
|
|
6fefcaad7b | ||
|
|
22fdfd7f0b | ||
|
|
6842eb05de | ||
|
|
37e7994d55 | ||
|
|
399893bbb2 | ||
|
|
227db065f3 | ||
|
|
b4352bcd8d | ||
|
|
39bab9d9e2 | ||
|
|
c71644f02f | ||
|
|
6aad7ee8b6 | ||
|
|
2b96d831fc | ||
|
|
dde0291add | ||
|
|
8af016ffc1 | ||
|
|
82b4547d7d | ||
|
|
791a25637f | ||
|
|
b922aa5c7c | ||
|
|
cbaebbc9c2 | ||
|
|
86b2e1aa6c | ||
|
|
61c5f05126 | ||
|
|
dde9c3dad5 |
@@ -6,6 +6,11 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
|
||||
# Note: localhost:8188 does not work.
|
||||
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
|
||||
|
||||
# Allow dev server access from remote IP addresses.
|
||||
# If true, the vite dev server will listen on all addresses, including LAN
|
||||
# and public addresses.
|
||||
VITE_REMOTE_DEV=false
|
||||
|
||||
# The target ComfyUI checkout directory to deploy the frontend code to.
|
||||
# The dist directory will be copied to {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev
|
||||
# Add `--front-end-root {DEPLOY_COMFYUI_DIR}/custom_web_versions/main/dev`
|
||||
|
||||
18
.github/workflows/release.yaml
vendored
@@ -41,3 +41,21 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
make_latest: "true"
|
||||
publish_types:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Release')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm run build:types
|
||||
- name: Publish package
|
||||
run: npm publish --access public
|
||||
working-directory: ./dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
2
.github/workflows/test-ui.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
run: |
|
||||
npm run test:generate
|
||||
npm run test:generate:examples
|
||||
npm test -- --verbose
|
||||
npm run test:jest:fast -- --verbose
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
playwright-tests-chromium:
|
||||
|
||||
51
README.md
@@ -414,6 +414,7 @@ We will support custom icons later.
|
||||
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
|
||||
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
|
||||
- [zod](https://zod.dev/) for schema validation
|
||||
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
@@ -431,6 +432,8 @@ core extensions will be loaded.
|
||||
|
||||
#### Access dev server on touch devices
|
||||
|
||||
Enable remote access to the dev server by setting `VITE_REMOTE_DEV` in `.env` to `true`.
|
||||
|
||||
After you start the dev server, you should see following logs:
|
||||
|
||||
```
|
||||
@@ -478,6 +481,54 @@ This repo is using litegraph package hosted on <https://github.com/Comfy-Org/lit
|
||||
|
||||
This will replace the litegraph package in this repo with the local litegraph repo.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
- en
|
||||
- zh
|
||||
- ru
|
||||
- ja
|
||||
|
||||
### How to Add a New Language
|
||||
|
||||
We welcome the addition of new languages. You can add a new language by following these steps:
|
||||
|
||||
#### 1. Create a Locale File
|
||||
|
||||
Navigate to the `src/locales` directory.
|
||||
Duplicate the existing `src/locales/en.ts` file and rename it to your target language code (e.g., `src/locales/ja.ts` for Japanese).
|
||||
|
||||
#### 2. Provide Translations
|
||||
|
||||
Translate the contents into your target language.
|
||||
If there are items that are not applicable, you can delete those items. In that case, the default language set in `en (src/locales/en.ts)` will be loaded.
|
||||
|
||||
#### 3. Update i18n Configuration
|
||||
|
||||
Import the new locale file in the `src/i18n.ts` file.
|
||||
|
||||
#### 4. Enable Selection of the New Language
|
||||
|
||||
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
options: ['en', 'zh', 'ru', 'ja'],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
```
|
||||
|
||||
#### 5. Test the Translations
|
||||
|
||||
Start the development server, switch to the new language, and verify the translations.
|
||||
You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
|
||||
|
||||
## Deploy
|
||||
|
||||
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
|
||||
|
||||
90
browser_tests/assets/oversized_group.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
37,
|
||||
98
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Group",
|
||||
"bounding": [
|
||||
23,
|
||||
23,
|
||||
900,
|
||||
825
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -11,7 +11,7 @@ test.describe('Browser tab title', () => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].extensionManager.workflow.activeWorkflow.filename
|
||||
})
|
||||
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
})
|
||||
|
||||
// Failing on CI
|
||||
|
||||
@@ -3,9 +3,6 @@ import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './fixtures/ComfyPage'
|
||||
import type { useWorkspaceStore } from '../src/stores/workspaceStore'
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
@@ -26,65 +23,41 @@ test.describe('Change Tracker', () => {
|
||||
})
|
||||
|
||||
test('Can undo multiple operations', async ({ comfyPage }) => {
|
||||
function isModified() {
|
||||
return comfyPage.page.evaluate(async () => {
|
||||
return !!(window['app'].extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow?.isModified
|
||||
})
|
||||
}
|
||||
|
||||
function getUndoQueueSize() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.undoQueue.length
|
||||
})
|
||||
}
|
||||
|
||||
function getRedoQueueSize() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.redoQueue.length
|
||||
})
|
||||
}
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
// Save, confirm no errors & workflow modified flag removed
|
||||
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await isModified()).toBe(false)
|
||||
|
||||
// TODO(huchenlei): Investigate why saving the workflow is causing the
|
||||
// undo queue to be triggered.
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await expect(node).toBeCollapsed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(3)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(2)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(1)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await isModified()).toBe(false)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(2)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,6 +71,7 @@ test.describe('Change Tracker', () => {
|
||||
|
||||
// Make changes outside set
|
||||
// Bypass + collapse node
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeCollapsed()
|
||||
@@ -111,6 +85,10 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).not.toBeBypassed()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
|
||||
// Prevent clicks registering a double-click
|
||||
await comfyPage.clickEmptySpace()
|
||||
await node.click('title')
|
||||
|
||||
// Run again, but within a change transaction
|
||||
await beforeChange(comfyPage)
|
||||
|
||||
@@ -152,6 +130,7 @@ test.describe('Change Tracker', () => {
|
||||
const multipleChanges = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
// Call other actions that uses begin/endChange
|
||||
await node.click('title')
|
||||
await collapse()
|
||||
await bypassAndPin()
|
||||
await afterChange(comfyPage)
|
||||
@@ -169,4 +148,20 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).toBePinned()
|
||||
await expect(node).toBeCollapsed()
|
||||
})
|
||||
|
||||
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].graph.extra.foo = 'bar'
|
||||
})
|
||||
// Click empty space to trigger a change detection.
|
||||
await comfyPage.clickEmptySpace()
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
})
|
||||
|
||||
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.pan({ x: 10, y: 10 })
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 151 KiB |
@@ -24,14 +24,6 @@ test.describe('Copy Paste', () => {
|
||||
|
||||
test('Can copy and paste widget value', async ({ comfyPage }) => {
|
||||
// Copy width value (512) from empty latent node to KSampler's seed.
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 718,
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC(null)
|
||||
// KSampler's seed
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
@@ -39,6 +31,14 @@ test.describe('Copy Paste', () => {
|
||||
y: 281
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlC(null)
|
||||
// Empty latent node's width
|
||||
await comfyPage.canvas.click({
|
||||
position: {
|
||||
x: 718,
|
||||
y: 643
|
||||
}
|
||||
})
|
||||
await comfyPage.ctrlV(null)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
@@ -17,8 +17,11 @@ import {
|
||||
import { Topbar } from './components/Topbar'
|
||||
import { NodeReference } from './utils/litegraphUtils'
|
||||
import type { Position, Size } from './types'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
class ComfyMenu {
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly themeToggleButton: Locator
|
||||
@@ -204,13 +207,15 @@ export class ComfyPage {
|
||||
}
|
||||
}
|
||||
|
||||
async setup() {
|
||||
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
await this.goto()
|
||||
await this.page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, this.id)
|
||||
if (clearStorage) {
|
||||
await this.page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, this.id)
|
||||
}
|
||||
await this.goto()
|
||||
|
||||
// Unify font for consistent screenshots.
|
||||
@@ -314,9 +319,9 @@ export class ComfyPage {
|
||||
}, settingId)
|
||||
}
|
||||
|
||||
async reload() {
|
||||
async reload({ clearStorage = true }: { clearStorage?: boolean } = {}) {
|
||||
await this.page.reload({ timeout: 15000 })
|
||||
await this.setup()
|
||||
await this.setup({ clearStorage })
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@@ -524,9 +529,6 @@ export class ComfyPage {
|
||||
safeSpot = safeSpot || { x: 10, y: 10 }
|
||||
await this.page.mouse.move(safeSpot.x, safeSpot.y)
|
||||
await this.page.mouse.down()
|
||||
// TEMPORARY HACK: Multiple pans open the search menu, so cheat and keep it closed.
|
||||
// TODO: Fix that (double-click at not-the-same-coordinations should not open the menu)
|
||||
await this.page.keyboard.press('Escape')
|
||||
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
@@ -555,6 +557,11 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickContextMenuItem(name: string): Promise<void> {
|
||||
await this.page.getByRole('menuitem', { name }).click()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async doubleClickCanvas() {
|
||||
await this.page.mouse.dblclick(10, 10, { delay: 5 })
|
||||
await this.nextFrame()
|
||||
@@ -733,6 +740,19 @@ export class ComfyPage {
|
||||
)
|
||||
}
|
||||
|
||||
async confirmDialog(prompt: string, text: string = 'Yes') {
|
||||
const modal = this.page.locator(
|
||||
`.comfy-modal-content:has-text("${prompt}")`
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
await modal
|
||||
.locator('.comfyui-button', {
|
||||
hasText: text
|
||||
})
|
||||
.click()
|
||||
await expect(modal).toBeHidden()
|
||||
}
|
||||
|
||||
async convertAllNodesToGroupNode(groupNodeName: string) {
|
||||
this.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
@@ -771,6 +791,26 @@ export class ComfyPage {
|
||||
async moveMouseToEmptyArea() {
|
||||
await this.page.mouse.move(10, 10)
|
||||
}
|
||||
async getUndoQueueSize() {
|
||||
return this.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.undoQueue.length
|
||||
})
|
||||
}
|
||||
async getRedoQueueSize() {
|
||||
return this.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.redoQueue.length
|
||||
})
|
||||
}
|
||||
async isCurrentWorkflowModified() {
|
||||
return this.page.evaluate(() => {
|
||||
return (window['app'].extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow?.isModified
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
|
||||
41
browser_tests/fixtures/UserSelectPage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Page } from 'playwright'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
public readonly url: string,
|
||||
public readonly page: Page
|
||||
) {}
|
||||
|
||||
get selectionUrl() {
|
||||
return this.url + '/user-select'
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this.page.locator('#comfy-user-selection')
|
||||
}
|
||||
|
||||
get newUserInput() {
|
||||
return this.container.locator('#new-user-input')
|
||||
}
|
||||
|
||||
get existingUserSelect() {
|
||||
return this.container.locator('#existing-user-select')
|
||||
}
|
||||
|
||||
get nextButton() {
|
||||
return this.container.getByText('Next')
|
||||
}
|
||||
}
|
||||
|
||||
export const userSelectPageFixture = base.extend<{
|
||||
userSelectPage: UserSelectPage
|
||||
}>({
|
||||
userSelectPage: async ({ page }, use) => {
|
||||
const userSelectPage = new UserSelectPage(
|
||||
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
|
||||
page
|
||||
)
|
||||
await use(userSelectPage)
|
||||
}
|
||||
})
|
||||
@@ -43,7 +43,7 @@ export class ComfyNodeSearchBox {
|
||||
}
|
||||
|
||||
get filterButton() {
|
||||
return this.page.locator('.comfy-vue-node-search-container ._filter-button')
|
||||
return this.page.locator('.comfy-vue-node-search-container .filter-button')
|
||||
}
|
||||
|
||||
async fillAndSelectFirstNode(
|
||||
|
||||
@@ -103,6 +103,12 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveWorkflowName() {
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async getTopLevelSavedWorkflowNames() {
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-browse .node-label')
|
||||
|
||||
@@ -77,8 +77,13 @@ test.describe('Group Node', () => {
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
test('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
// The 500ms fixed delay on the search results is causing flakiness
|
||||
// Potential solution: add a spinner state when the search is in progress,
|
||||
// and observe that state from the test. Blocker: the PrimeVue AutoComplete
|
||||
// does not have a v-model on the query, so we cannot observe the raw
|
||||
// query update, and thus cannot set the spinning state between the raw query
|
||||
// update and the debounced search update.
|
||||
test.skip('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
@@ -320,6 +320,15 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
|
||||
})
|
||||
|
||||
test('Can fit group to contents', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
|
||||
})
|
||||
|
||||
// Somehow this test fails on GitHub Actions. It works locally.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
|
||||
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
|
||||
@@ -537,6 +546,34 @@ test.describe('Load workflow', () => {
|
||||
await comfyPage.loadWorkflow('string_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
|
||||
})
|
||||
|
||||
test('Restore workflow on reload (switch workflow)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
await comfyPage.reload({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
||||
})
|
||||
|
||||
test('Restore workflow on reload (modify workflow)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('collapse')
|
||||
// Wait 300ms between 2 clicks so that it is not treated as a double click
|
||||
// by litegraph.
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
await comfyPage.reload({ clearStorage: false })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -350,23 +350,6 @@ test.describe('Menu', () => {
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
})
|
||||
|
||||
test('Can migrate legacy bookmarks', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [
|
||||
'foo/',
|
||||
'foo/KSampler (Advanced)',
|
||||
'UNKNOWN',
|
||||
'KSampler'
|
||||
])
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
|
||||
await comfyPage.reload()
|
||||
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
|
||||
[]
|
||||
)
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
|
||||
).toEqual(['foo/', 'foo/KSamplerAdvanced', 'KSampler'])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows sidebar', () => {
|
||||
@@ -379,7 +362,9 @@ test.describe('Menu', () => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
@@ -392,7 +377,7 @@ test.describe('Menu', () => {
|
||||
await tab.newBlankWorkflowButton.click()
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'Unsaved Workflow (2).json'
|
||||
'*Unsaved Workflow (2).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -450,6 +435,58 @@ test.describe('Menu', () => {
|
||||
).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
})
|
||||
|
||||
test('Can save temporary workflow with unmodified name', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow')
|
||||
// Should not trigger the overwrite dialog
|
||||
expect(
|
||||
await comfyPage.page.locator('.comfy-modal-content:visible').count()
|
||||
).toBe(0)
|
||||
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
})
|
||||
|
||||
test('Can overwrite other workflows with save as', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const topbar = comfyPage.menu.topbar
|
||||
await topbar.saveWorkflow('workflow1.json')
|
||||
await topbar.saveWorkflowAs('workflow2.json')
|
||||
await comfyPage.nextFrame()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow1.json', 'workflow2.json'])
|
||||
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
|
||||
'workflow2.json'
|
||||
)
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow2.json', 'workflow1.json'])
|
||||
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
|
||||
'workflow1.json'
|
||||
)
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -475,12 +512,12 @@ test.describe('Menu', () => {
|
||||
`tempWorkflow-${test.info().title}`
|
||||
)
|
||||
const closeButton = comfyPage.page.locator(
|
||||
'.comfyui-workflows-open .p-button-icon.pi-times'
|
||||
'.comfyui-workflows-open .close-workflow-button'
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['Unsaved Workflow.json'])
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ test.describe('Canvas Right Click Menu', () => {
|
||||
await dialog.accept('GroupNode2CLIP')
|
||||
})
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.page.getByText('Convert to Group Node').click()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-group-node.png'
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
42
browser_tests/userSelectView.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { userSelectPageFixture as test } from './fixtures/UserSelectPage'
|
||||
|
||||
/**
|
||||
* Expects ComfyUI backend to be launched with `--multi-user` flag.
|
||||
*/
|
||||
test.describe('User Select View', () => {
|
||||
test.beforeEach(async ({ userSelectPage, page }) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
})
|
||||
|
||||
test('Redirects to user select view if no user is logged in', async ({
|
||||
userSelectPage,
|
||||
page
|
||||
}) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await expect(userSelectPage.container).toBeVisible()
|
||||
expect(page.url()).toBe(userSelectPage.selectionUrl)
|
||||
})
|
||||
|
||||
test('Can create new user', async ({ userSelectPage, page }) => {
|
||||
const randomUser = `test-user-${Math.random().toString(36).substring(2, 7)}`
|
||||
await page.goto(userSelectPage.url)
|
||||
await expect(page).toHaveURL(userSelectPage.selectionUrl)
|
||||
await userSelectPage.newUserInput.fill(randomUser)
|
||||
await userSelectPage.nextButton.click()
|
||||
await expect(page).toHaveURL(userSelectPage.url)
|
||||
})
|
||||
|
||||
test('Can choose existing user', async ({ userSelectPage, page }) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await expect(page).toHaveURL(userSelectPage.selectionUrl)
|
||||
await userSelectPage.existingUserSelect.click()
|
||||
await page.locator('.p-select-list .p-select-option').first().click()
|
||||
await userSelectPage.nextButton.click()
|
||||
await expect(page).toHaveURL(userSelectPage.url)
|
||||
})
|
||||
})
|
||||
27
index.html
@@ -9,33 +9,6 @@
|
||||
</head>
|
||||
<body class="litegraph grid">
|
||||
<div id="vue-app"></div>
|
||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
||||
<main class="comfy-user-selection-inner">
|
||||
<h1>ComfyUI</h1>
|
||||
<form>
|
||||
<section>
|
||||
<label>New user:
|
||||
<input placeholder="Enter a username" />
|
||||
</label>
|
||||
</section>
|
||||
<div class="comfy-user-existing">
|
||||
<span class="or-separator">OR</span>
|
||||
<section>
|
||||
<label>
|
||||
Existing user:
|
||||
<select>
|
||||
<option hidden disabled selected value> Select a user </option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="comfy-user-error"> </span>
|
||||
<button class="comfy-btn comfy-user-button-next">Next</button>
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||