diff --git a/browser_tests/fixtures/UserSelectPage.ts b/browser_tests/fixtures/UserSelectPage.ts new file mode 100644 index 000000000..4b78076ca --- /dev/null +++ b/browser_tests/fixtures/UserSelectPage.ts @@ -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) + } +}) diff --git a/browser_tests/userSelectView.spec.ts b/browser_tests/userSelectView.spec.ts new file mode 100644 index 000000000..68d822b84 --- /dev/null +++ b/browser_tests/userSelectView.spec.ts @@ -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) + }) +}) diff --git a/index.html b/index.html index 5e6f66d54..7a321795c 100644 --- a/index.html +++ b/index.html @@ -9,33 +9,6 @@
- diff --git a/src/components/sidebar/SideToolbar.vue b/src/components/sidebar/SideToolbar.vue index 2b10435c2..1b0fd297d 100644 --- a/src/components/sidebar/SideToolbar.vue +++ b/src/components/sidebar/SideToolbar.vue @@ -12,6 +12,7 @@ @click="onTabClick(tab)" />
+
@@ -29,15 +30,18 @@ import SidebarIcon from './SidebarIcon.vue' import SidebarThemeToggleIcon from './SidebarThemeToggleIcon.vue' import SidebarSettingsToggleIcon from './SidebarSettingsToggleIcon.vue' +import SidebarLogoutIcon from './SidebarLogoutIcon.vue' import ExtensionSlot from '@/components/common/ExtensionSlot.vue' import { computed } from 'vue' import { useWorkspaceStore } from '@/stores/workspaceStore' import { useSettingStore } from '@/stores/settingStore' import type { SidebarTabExtension } from '@/types/extensionTypes' import { useKeybindingStore } from '@/stores/keybindingStore' +import { useUserStore } from '@/stores/userStore' const workspaceStore = useWorkspaceStore() const settingStore = useSettingStore() +const userStore = useUserStore() const teleportTarget = computed(() => settingStore.get('Comfy.Sidebar.Location') === 'left' diff --git a/src/components/sidebar/SidebarLogoutIcon.vue b/src/components/sidebar/SidebarLogoutIcon.vue new file mode 100644 index 000000000..64060870d --- /dev/null +++ b/src/components/sidebar/SidebarLogoutIcon.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/locales/en.ts b/src/locales/en.ts index c5cb9f928..3bf2b461a 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -3,6 +3,13 @@ export default { title: 'Welcome to ComfyUI', getStarted: 'Get Started' }, + userSelect: { + newUser: 'New user', + enterUsername: 'Enter a username', + existingUser: 'Existing user', + selectUser: 'Select a user', + next: 'Next' + }, notSupported: { title: 'Your device is not supported', message: 'Only following devices are supported:', @@ -147,6 +154,7 @@ export default { newFolder: 'New Folder', sideToolbar: { themeToggle: 'Toggle Theme', + logout: 'Logout', queue: 'Queue', nodeLibrary: 'Node Library', workflows: 'Workflows', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 38bdeb115..67000331f 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -148,6 +148,7 @@ export default { sideToolbar: { themeToggle: 'テーマの切り替え', queue: 'キュー', + logout: 'ログアウト', nodeLibrary: 'ノードライブラリ', workflows: 'ワークフロー', browseTemplates: 'サンプルテンプレートを表示', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index abc9ddcbc..1e8d2bdf9 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -60,6 +60,7 @@ export default { sideToolbar: { themeToggle: 'Переключить тему', queue: 'Очередь', + logout: 'Выйти', nodeLibrary: 'Библиотека узлов', workflows: 'Рабочие процессы', browseTemplates: 'Просмотреть примеры шаблонов', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index cae5199f1..9a87756f9 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -61,6 +61,7 @@ export default { newFolder: '新建文件夹', sideToolbar: { themeToggle: '主题切换', + logout: '登出', queue: '队列', nodeLibrary: '节点库', workflows: '工作流', diff --git a/src/router.ts b/src/router.ts index 86d4f117c..e1afb5a91 100644 --- a/src/router.ts +++ b/src/router.ts @@ -7,6 +7,7 @@ import { } from 'vue-router' import LayoutDefault from '@/views/layouts/LayoutDefault.vue' import { isElectron } from './utils/envUtil' +import { useUserStore } from './stores/userStore' const isFileProtocol = window.location.protocol === 'file:' const basePath = isElectron() ? '/' : window.location.pathname @@ -38,7 +39,21 @@ const router = createRouter({ { path: '', name: 'GraphView', - component: () => import('@/views/GraphView.vue') + component: () => import('@/views/GraphView.vue'), + beforeEnter: async (to, from, next) => { + const userStore = useUserStore() + await userStore.initialize() + if (userStore.needsLogin) { + next('/user-select') + } else { + next() + } + } + }, + { + path: 'user-select', + name: 'UserSelectView', + component: () => import('@/views/UserSelectView.vue') }, { path: 'server-start', diff --git a/src/scripts/api.ts b/src/scripts/api.ts index e86866cd8..a70b25356 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -42,6 +42,9 @@ export class ComfyApi extends EventTarget { * The current client id from websocket status updates. */ clientId?: string + /** + * The current user id. + */ user: string socket: WebSocket | null = null @@ -49,7 +52,6 @@ export class ComfyApi extends EventTarget { constructor() { super() - // api.user is set by ComfyApp.setup() this.user = '' this.api_host = location.host this.api_base = location.pathname.split('/').slice(0, -1).join('/') diff --git a/src/scripts/app.ts b/src/scripts/app.ts index a6f686c28..eab2f434e 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1711,67 +1711,6 @@ export class ComfyApp { ) } - async #setUser() { - const userConfig = await api.getUserConfig() - // Return in single user mode. - if (userConfig.users === undefined) { - return - } - - let user = localStorage['Comfy.userId'] - const users = userConfig.users ?? {} - if (!user || !users[user]) { - // Lift spinner / BlockUI for user selection. - if (this.vueAppReady) useWorkspaceStore().spinner = false - - // This will rarely be hit so move the loading to on demand - const { UserSelectionScreen } = await import('./ui/userSelection') - - this.ui.menuContainer.style.display = 'none' - const { userId, username } = await new UserSelectionScreen().show( - users, - user - ) - this.ui.menuContainer.style.display = '' - - user = userId - localStorage['Comfy.userName'] = username - localStorage['Comfy.userId'] = user - } - - api.user = user - - this.ui.settings.addSetting({ - id: 'Comfy.SwitchUser', - name: 'Switch User', - type: (name) => { - let currentUser = localStorage['Comfy.userName'] - if (currentUser) { - currentUser = ` (${currentUser})` - } - return $el('tr', [ - $el('td', [ - $el('label', { - textContent: name - }) - ]), - $el('td', [ - $el('button', { - textContent: name + (currentUser ?? ''), - onclick: () => { - delete localStorage['Comfy.userId'] - delete localStorage['Comfy.userName'] - window.location.reload() - } - }) - ]) - ]) - }, - // TODO: Is that the correct default value? - defaultValue: undefined - }) - } - /** * Set up the app on the page */ @@ -1779,7 +1718,6 @@ export class ComfyApp { this.canvasEl = canvasEl // Show menu container for GraphView. this.ui.menuContainer.style.display = 'block' - await this.#setUser() this.resizeCanvas() diff --git a/src/scripts/ui/userSelection.css b/src/scripts/ui/userSelection.css deleted file mode 100644 index 35c9d6614..000000000 --- a/src/scripts/ui/userSelection.css +++ /dev/null @@ -1,135 +0,0 @@ -.comfy-user-selection { - width: 100vw; - height: 100vh; - position: absolute; - top: 0; - left: 0; - z-index: 999; - display: flex; - align-items: center; - justify-content: center; - font-family: sans-serif; - background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color)); -} - -.comfy-user-selection-inner { - background: var(--comfy-menu-bg); - margin-top: -30vh; - padding: 20px 40px; - border-radius: 10px; - min-width: 365px; - position: relative; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); -} - -.comfy-user-selection-inner form { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -} - -.comfy-user-selection-inner h1 { - margin: 10px 0 30px 0; - font-weight: normal; -} - -.comfy-user-selection-inner label { - display: flex; - flex-direction: column; - width: 100%; -} - -.comfy-user-selection input, -.comfy-user-selection select { - background-color: var(--comfy-input-bg); - color: var(--input-text); - border: 0; - border-radius: 5px; - padding: 5px; - margin-top: 10px; -} - -.comfy-user-selection input::placeholder { - color: var(--descrip-text); - opacity: 1; -} - -.comfy-user-existing { - width: 100%; -} - -.no-users .comfy-user-existing { - display: none; -} - -.comfy-user-selection-inner .or-separator { - margin: 10px 0; - padding: 10px; - display: block; - text-align: center; - width: 100%; - color: var(--descrip-text); -} - -.comfy-user-selection-inner .or-separator { - overflow: hidden; - text-align: center; - margin-left: -10px; -} - -.comfy-user-selection-inner .or-separator::before, -.comfy-user-selection-inner .or-separator::after { - content: ""; - background-color: var(--border-color); - position: relative; - height: 1px; - vertical-align: middle; - display: inline-block; - width: calc(50% - 20px); - top: -1px; -} - -.comfy-user-selection-inner .or-separator::before { - right: 10px; - margin-left: -50%; -} - -.comfy-user-selection-inner .or-separator::after { - left: 10px; - margin-right: -50%; -} - -.comfy-user-selection-inner section { - width: 100%; - padding: 10px; - margin: -10px; - transition: background-color 0.2s; -} - -.comfy-user-selection-inner section.selected { - background: var(--border-color); - border-radius: 5px; -} - -.comfy-user-selection-inner footer { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 20px; -} - -.comfy-user-selection-inner .comfy-user-error { - color: var(--error-text); - margin-bottom: 10px; -} - -.comfy-user-button-next { - font-size: 16px; - padding: 6px 10px; - width: 100px; - display: flex; - gap: 5px; - align-items: center; - justify-content: center; -} \ No newline at end of file diff --git a/src/scripts/ui/userSelection.ts b/src/scripts/ui/userSelection.ts deleted file mode 100644 index 2c8154c35..000000000 --- a/src/scripts/ui/userSelection.ts +++ /dev/null @@ -1,146 +0,0 @@ -// @ts-strict-ignore -import { api } from '../api' -import { $el } from '../ui' -import { createSpinner } from './spinner' -import './userSelection.css' - -interface SelectedUser { - username: string - userId: string - created: boolean -} - -export class UserSelectionScreen { - async show(users, user): Promise { - const userSelection = document.getElementById('comfy-user-selection') - userSelection.style.display = '' - return new Promise((resolve) => { - const input = userSelection.getElementsByTagName('input')[0] - const select = userSelection.getElementsByTagName('select')[0] - const inputSection = input.closest('section') - const selectSection = select.closest('section') - const form = userSelection.getElementsByTagName('form')[0] - const error = userSelection.getElementsByClassName('comfy-user-error')[0] - const button = userSelection.getElementsByClassName( - 'comfy-user-button-next' - )[0] - - let inputActive = null - input.addEventListener('focus', () => { - inputSection.classList.add('selected') - selectSection.classList.remove('selected') - inputActive = true - }) - select.addEventListener('focus', () => { - inputSection.classList.remove('selected') - selectSection.classList.add('selected') - inputActive = false - select.style.color = '' - }) - select.addEventListener('blur', () => { - if (!select.value) { - select.style.color = 'var(--descrip-text)' - } - }) - - form.addEventListener('submit', async (e) => { - e.preventDefault() - if (inputActive == null) { - error.textContent = - 'Please enter a username or select an existing user.' - } else if (inputActive) { - const username = input.value.trim() - if (!username) { - error.textContent = 'Please enter a username.' - return - } - - // Create new user - // Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339) - // Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551) - input.disabled = - select.disabled = - // @ts-expect-error - input.readonly = - // @ts-expect-error - select.readonly = - true - const spinner = createSpinner() - button.prepend(spinner) - try { - const resp = await api.createUser(username) - if (resp.status >= 300) { - let message = - 'Error creating user: ' + resp.status + ' ' + resp.statusText - try { - const res = await resp.json() - if (res.error) { - message = res.error - } - } catch (error) {} - throw new Error(message) - } - - resolve({ username, userId: await resp.json(), created: true }) - } catch (err) { - spinner.remove() - error.textContent = - err.message ?? - err.statusText ?? - err ?? - 'An unknown error occurred.' - // Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339) - // Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551) - input.disabled = - select.disabled = - // @ts-expect-error - input.readonly = - // @ts-expect-error - select.readonly = - false - return - } - } else if (!select.value) { - error.textContent = 'Please select an existing user.' - return - } else { - resolve({ - username: users[select.value], - userId: select.value, - created: false - }) - } - }) - - if (user) { - const name = localStorage['Comfy.userName'] - if (name) { - input.value = name - } - } - if (input.value) { - // Focus the input, do this separately as sometimes browsers like to fill in the value - input.focus() - } - - const userIds = Object.keys(users ?? {}) - if (userIds.length) { - for (const u of userIds) { - $el('option', { textContent: users[u], value: u, parent: select }) - } - select.style.color = 'var(--descrip-text)' - - if (select.value) { - // Focus the select, do this separately as sometimes browsers like to fill in the value - select.focus() - } - } else { - userSelection.classList.add('no-users') - input.focus() - } - }).then((r: SelectedUser) => { - userSelection.remove() - return r - }) - } -} diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts new file mode 100644 index 000000000..7a6109dde --- /dev/null +++ b/src/stores/userStore.ts @@ -0,0 +1,109 @@ +import { api } from '@/scripts/api' +import { defineStore } from 'pinia' +import { computed, ref, watchEffect } from 'vue' +import type { User as UserConfig } from '@/types/apiTypes' + +export interface User { + userId: string + username: string +} + +export const useUserStore = defineStore('user', () => { + /** + * The user config. null if not loaded. + */ + const userConfig = ref(null) + /** + * The current user id. null if not logged in or in single user mode. + */ + const currentUserId = ref(null) + const isMultiUserServer = computed( + () => userConfig.value && 'users' in userConfig.value + ) + const needsLogin = computed( + () => !currentUserId.value && isMultiUserServer.value + ) + const users = computed(() => + Object.entries(userConfig.value?.users ?? {}).map(([userId, username]) => ({ + userId, + username + })) + ) + const currentUser = computed( + () => + users.value.find((user) => user.userId === currentUserId.value) ?? null + ) + const initialized = computed(() => userConfig.value !== null) + + /** + * Initialize the user store. + */ + async function initialize() { + userConfig.value = await api.getUserConfig() + currentUserId.value = localStorage['Comfy.userId'] + } + + /** + * Create a new user. + * + * @param username - The username. + * @returns The new user. + */ + async function createUser(username: string): Promise { + const resp = await api.createUser(username) + const data = await resp.json() + if (resp.status >= 300) { + throw new Error( + data.error ?? + 'Error creating user: ' + resp.status + ' ' + resp.statusText + ) + } + return { + userId: data, + username + } + } + + /** + * Login the current user. + * + * @param user - The user. + */ + async function login({ + userId, + username + }: { + userId: string + username: string + }) { + currentUserId.value = userId + localStorage['Comfy.userId'] = userId + localStorage['Comfy.userName'] = username + } + + watchEffect(() => { + if (isMultiUserServer.value && currentUserId.value) { + api.user = currentUserId.value + } + }) + + /** + * Logout the current user. + */ + async function logout() { + delete localStorage['Comfy.userId'] + delete localStorage['Comfy.userName'] + } + + return { + users, + currentUser, + isMultiUserServer, + needsLogin, + initialized, + initialize, + createUser, + login, + logout + } +}) diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index 5dbdb1c06..36f4072ce 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -431,8 +431,10 @@ export const zSystemStats = z.object({ }) const zUser = z.object({ storage: z.enum(['server']), - migrated: z.boolean(), - users: z.record(z.string(), z.unknown()) + // `migrated` is only available in single-user mode. + migrated: z.boolean().optional(), + // `users` is only available in multi-user server mode. + users: z.record(z.string(), z.string()).optional() }) const zUserData = z.array(z.array(z.string(), z.string())) const zUserDataFullInfo = z.object({ diff --git a/src/views/UserSelectView.vue b/src/views/UserSelectView.vue new file mode 100644 index 000000000..e446ca97c --- /dev/null +++ b/src/views/UserSelectView.vue @@ -0,0 +1,90 @@ + + + diff --git a/tests-ui/tests/slow/users.test.ts b/tests-ui/tests/slow/users.test.ts deleted file mode 100644 index 3ff06c06f..000000000 --- a/tests-ui/tests/slow/users.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -// @ts-strict-ignore -import { start } from '../../utils' -import lg from '../../utils/litegraph' - -describe('users', () => { - beforeEach(() => { - lg.setup(global) - }) - - afterEach(() => { - lg.teardown(global) - }) - - function expectNoUserScreen() { - // Ensure login isnt visible - const selection = document.querySelectorAll('#comfy-user-selection')?.[0] - expect(selection['style'].display).toBe('none') - const menu = document.querySelectorAll('.comfy-menu')?.[0] - expect(window.getComputedStyle(menu)?.display).not.toBe('none') - } - - describe('multi-user', () => { - async function mockAddStylesheet() { - const utils = await import('../../../src/scripts/utils') - utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve()) - } - - async function waitForUserScreenShow() { - // Wait for "show" to be called - const { UserSelectionScreen } = await import( - '../../../src/scripts/ui/userSelection' - ) - let resolve, reject - const fn = UserSelectionScreen.prototype.show - const p = new Promise((res, rej) => { - resolve = res - reject = rej - }) - jest - .spyOn(UserSelectionScreen.prototype, 'show') - .mockImplementation(async (...args) => { - const res = fn(...args) - await new Promise(process.nextTick) // wait for promises to resolve - resolve() - return res - }) - setTimeout( - () => reject('timeout waiting for UserSelectionScreen to be shown.'), - 500 - ) - await p - await new Promise(process.nextTick) // wait for promises to resolve - } - - async function testUserScreen(onShown, users?) { - if (!users) { - users = {} - } - const starting = start({ - resetEnv: true, - userConfig: { storage: 'server', users }, - preSetup: mockAddStylesheet - }) - - // Ensure no current user - expect(localStorage['Comfy.userId']).toBeFalsy() - expect(localStorage['Comfy.userName']).toBeFalsy() - - await waitForUserScreenShow() - - const selection = document.querySelectorAll('#comfy-user-selection')?.[0] - expect(selection).toBeTruthy() - - // Ensure login is visible - expect(window.getComputedStyle(selection)?.display).not.toBe('none') - // Ensure menu is hidden - const menu = document.querySelectorAll('.comfy-menu')?.[0] - expect(window.getComputedStyle(menu)?.display).toBe('none') - - const isCreate = await onShown(selection) - - // Submit form - selection.querySelectorAll('form')[0].submit() - await new Promise(process.nextTick) // wait for promises to resolve - - // Wait for start - const s = await starting - - // Ensure login is removed - expect(document.querySelectorAll('#comfy-user-selection')).toHaveLength(0) - expect(window.getComputedStyle(menu)?.display).not.toBe('none') - - // Ensure settings + templates are saved - const { api } = await import('../../../src/scripts/api') - expect(api.createUser).toHaveBeenCalledTimes(+isCreate) - expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate) - expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate) - if (isCreate) { - expect(api.storeUserData).toHaveBeenCalledWith( - 'comfy.templates.json', - null, - { stringify: false } - ) - } - return { users, selection, ...s } - } - - it('allows user creation if no users', async () => { - const { users } = await testUserScreen((selection) => { - // Ensure we have no users flag added - expect(selection.classList.contains('no-users')).toBeTruthy() - - // Enter a username - const input = selection.getElementsByTagName('input')[0] - input.focus() - input.value = 'Test User' - - return true - }) - - expect(users).toStrictEqual({ - 'Test User!': 'Test User' - }) - - expect(localStorage['Comfy.userId']).toBe('Test User!') - expect(localStorage['Comfy.userName']).toBe('Test User') - }) - it('allows user creation if no current user but other users', async () => { - const users = { - 'Test User 2!': 'Test User 2' - } - - await testUserScreen((selection) => { - expect(selection.classList.contains('no-users')).toBeFalsy() - - // Enter a username - const input = selection.getElementsByTagName('input')[0] - input.focus() - input.value = 'Test User 3' - return true - }, users) - - expect(users).toStrictEqual({ - 'Test User 2!': 'Test User 2', - 'Test User 3!': 'Test User 3' - }) - - expect(localStorage['Comfy.userId']).toBe('Test User 3!') - expect(localStorage['Comfy.userName']).toBe('Test User 3') - }) - it('allows user selection if no current user but other users', async () => { - const users = { - 'A!': 'A', - 'B!': 'B', - 'C!': 'C' - } - - await testUserScreen((selection) => { - expect(selection.classList.contains('no-users')).toBeFalsy() - - // Check user list - const select = selection.getElementsByTagName('select')[0] - const options = select.getElementsByTagName('option') - expect( - [...options] - .filter((o) => !o.disabled) - .reduce((p, n) => { - p[n.getAttribute('value')] = n.textContent - return p - }, {}) - ).toStrictEqual(users) - - // Select an option - select.focus() - select.value = options[2].value - - return false - }, users) - - expect(users).toStrictEqual(users) - - expect(localStorage['Comfy.userId']).toBe('B!') - expect(localStorage['Comfy.userName']).toBe('B') - }) - it('doesnt show user screen if current user', async () => { - const starting = start({ - resetEnv: true, - userConfig: { - storage: 'server', - users: { - 'User!': 'User' - } - }, - localStorage: { - 'Comfy.userId': 'User!', - 'Comfy.userName': 'User' - } - }) - await new Promise(process.nextTick) // wait for promises to resolve - - expectNoUserScreen() - - await starting - }) - it('allows user switching', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { - storage: 'server', - users: { - 'User!': 'User' - } - }, - localStorage: { - 'Comfy.userId': 'User!', - 'Comfy.userName': 'User' - } - }) - - // cant actually test switching user easily but can check the setting is present - expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeTruthy() - }) - }) - describe('single-user', () => { - it('doesnt show user creation if no default user', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: false, storage: 'server' } - }) - expectNoUserScreen() - - // It should store the settings - const { api } = await import('../../../src/scripts/api') - expect(api.storeSettings).toHaveBeenCalledTimes(1) - expect(api.storeUserData).toHaveBeenCalledTimes(1) - expect(api.storeUserData).toHaveBeenCalledWith( - 'comfy.templates.json', - null, - { stringify: false } - ) - }) - it('doesnt show user creation if default user', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: 'server' } - }) - expectNoUserScreen() - - // It should store the settings - const { api } = await import('../../../src/scripts/api') - expect(api.storeSettings).toHaveBeenCalledTimes(0) - expect(api.storeUserData).toHaveBeenCalledTimes(0) - }) - it('doesnt allow user switching', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: 'server' } - }) - expectNoUserScreen() - - expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeFalsy() - }) - }) - describe('browser-user', () => { - it('doesnt show user creation if no default user', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: false, storage: 'browser' } - }) - expectNoUserScreen() - - // It should store the settings - const { api } = await import('../../../src/scripts/api') - expect(api.storeSettings).toHaveBeenCalledTimes(0) - expect(api.storeUserData).toHaveBeenCalledTimes(0) - }) - it('doesnt show user creation if default user', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: 'server' } - }) - expectNoUserScreen() - - // It should store the settings - const { api } = await import('../../../src/scripts/api') - expect(api.storeSettings).toHaveBeenCalledTimes(0) - expect(api.storeUserData).toHaveBeenCalledTimes(0) - }) - it('doesnt allow user switching', async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: 'browser' } - }) - expectNoUserScreen() - - expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeFalsy() - }) - }) -})