diff --git a/tests-ui/tests/users.test.js b/tests-ui/tests/users.test.js
new file mode 100644
index 000000000..e42bbe3c1
--- /dev/null
+++ b/tests-ui/tests/users.test.js
@@ -0,0 +1,223 @@
+// @ts-check
+///
+const { start } = require("../utils");
+const lg = require("../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 testUserScreen(onShown, users) {
+ if (!users) {
+ users = {};
+ }
+ const starting = start({
+ resetEnv: true,
+ users,
+ });
+
+ // Ensure no current user
+ expect(localStorage["Comfy.userId"]).toBeFalsy();
+ expect(localStorage["Comfy.userName"]).toBeFalsy();
+
+ await new Promise(process.nextTick); // wait for promises to resolve
+
+ 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 } = require("../../web/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 });
+ expect(s.app.isNewUserSession).toBeTruthy();
+ } else {
+ expect(s.app.isNewUserSession).toBeFalsy();
+ }
+
+ 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,
+ 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,
+ 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,
+ users: false,
+ });
+ expectNoUserScreen();
+
+ // It should store the settings
+ const { api } = require("../../web/scripts/api");
+ expect(api.storeSettings).toHaveBeenCalledTimes(1);
+ expect(api.storeUserData).toHaveBeenCalledTimes(1);
+ expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
+ expect(app.isNewUserSession).toBeTruthy();
+ });
+ it("doesnt show user creation if default user", async () => {
+ const { app } = await start({
+ resetEnv: true,
+ users: true,
+ });
+ expectNoUserScreen();
+
+ // It should store the settings
+ const { api } = require("../../web/scripts/api");
+ expect(api.storeSettings).toHaveBeenCalledTimes(0);
+ expect(api.storeUserData).toHaveBeenCalledTimes(0);
+ expect(app.isNewUserSession).toBeFalsy();
+ });
+ it("doesnt allow user switching", async () => {
+ const { app } = await start({
+ resetEnv: true,
+ });
+ expectNoUserScreen();
+
+ expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
+ });
+ });
+});
diff --git a/tests-ui/utils/index.js b/tests-ui/utils/index.js
index 3a018f566..349e5b675 100644
--- a/tests-ui/utils/index.js
+++ b/tests-ui/utils/index.js
@@ -1,10 +1,18 @@
const { mockApi } = require("./setup");
const { Ez } = require("./ezgraph");
const lg = require("./litegraph");
+const fs = require("fs");
+const path = require("path");
+
+const html = fs.readFileSync(path.resolve(__dirname, "../../web/index.html"))
/**
*
- * @param { Parameters[0] & { resetEnv?: boolean, preSetup?(app): Promise } } config
+ * @param { Parameters[0] & {
+ * resetEnv?: boolean,
+ * preSetup?(app): Promise,
+ * localStorage?: Record
+ * } } config
* @returns
*/
export async function start(config = {}) {
@@ -12,8 +20,14 @@ export async function start(config = {}) {
jest.resetModules();
jest.resetAllMocks();
lg.setup(global);
+ localStorage.clear();
+ sessionStorage.clear();
}
+ Object.assign(localStorage, config.localStorage ?? {});
+ document.body.innerHTML = html;
+
+ mockUtils();
mockApi(config);
const { app } = require("../../web/scripts/app");
config.preSetup?.(app);
@@ -21,6 +35,12 @@ export async function start(config = {}) {
return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
}
+function mockUtils() {
+ jest.mock("../../web/scripts/utils", () => ({
+ addStylesheet: () => Promise.resolve()
+ }));
+}
+
/**
* @param { ReturnType["graph"] } graph
* @param { (hasReloaded: boolean) => (Promise | void) } cb
diff --git a/tests-ui/utils/setup.js b/tests-ui/utils/setup.js
index 54aff954b..87d0fe026 100644
--- a/tests-ui/utils/setup.js
+++ b/tests-ui/utils/setup.js
@@ -18,16 +18,21 @@ function* walkSync(dir) {
*/
/**
- * @param {
- * {
+ * @param {{
* mockExtensions?: string[],
* mockNodeDefs?: Record,
* users?: boolean | Record
* settings?: Record
* userData?: Record
- * } } config
+ * }} config
*/
-export function mockApi({ mockExtensions, mockNodeDefs, users, settings, userData } = {}) {
+export function mockApi(config = {}) {
+ let { mockExtensions, mockNodeDefs, users, settings, userData } = {
+ users: true,
+ settings: {},
+ userData: {},
+ ...config,
+ };
if (!mockExtensions) {
mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core")))
.filter((x) => x.endsWith(".js"))
@@ -36,16 +41,7 @@ export function mockApi({ mockExtensions, mockNodeDefs, users, settings, userDat
if (!mockNodeDefs) {
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json")));
}
- if(!users) {
- users = true;
- }
- if(!settings) {
- settings = {};
- }
- if(!userData) {
- userData = {};
- }
-
+
const events = new EventTarget();
const mockApi = {
addEventListener: events.addEventListener.bind(events),
@@ -56,15 +52,26 @@ export function mockApi({ mockExtensions, mockNodeDefs, users, settings, userDat
getNodeDefs: jest.fn(() => mockNodeDefs),
init: jest.fn(),
apiURL: jest.fn((x) => "../../web/" + x),
+ createUser: jest.fn((username) => {
+ if(username in users) {
+ return { status: 400, json: () => "Duplicate" }
+ }
+ users[username + "!"] = username;
+ return { status: 200, json: () => username + "!" }
+ }),
getUsers: jest.fn(() => users),
- getSettings: jest.fn(() => settings ?? {}),
- getUserData: jest.fn(f => {
- if(f in userData) {
+ getSettings: jest.fn(() => settings),
+ storeSettings: jest.fn((v) => Object.assign(settings, v)),
+ getUserData: jest.fn((f) => {
+ if (f in userData) {
return { status: 200, json: () => userData[f] };
} else {
- return { status: 404 }
+ return { status: 404 };
}
- })
+ }),
+ storeUserData: jest.fn((file, data) => {
+ userData[file] = data;
+ }),
};
jest.mock("../../web/scripts/api", () => ({
get api() {
diff --git a/web/scripts/api.js b/web/scripts/api.js
index 250e936b2..aeafdc90f 100644
--- a/web/scripts/api.js
+++ b/web/scripts/api.js
@@ -324,13 +324,28 @@ class ComfyApi extends EventTarget {
}
/**
- * Gets all setting values for the current user
- * @returns { Promise } A dictionary of id -> value
+ * Gets a list of users or true if single user mode and default user is created, or false if single user mode and default user is not created.
+ * @returns { Promise | boolean } If multi-user, a dictionary of id -> value, else whether the default user is created
*/
async getUsers() {
return (await this.fetchApi("/users")).json();
}
+ /**
+ * Creates a new user
+ * @param { string } username
+ * @returns The fetch response
+ */
+ createUser(username) {
+ return this.fetchApi("/users", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ username }),
+ });
+ }
+
/**
* Gets all setting values for the current user
* @returns { Promise } A dictionary of id -> value
diff --git a/web/scripts/ui/userSelection.js b/web/scripts/ui/userSelection.js
index 9145905b9..f9f1ca807 100644
--- a/web/scripts/ui/userSelection.js
+++ b/web/scripts/ui/userSelection.js
@@ -9,7 +9,7 @@ export class UserSelectionScreen {
await addStylesheet(import.meta.url);
const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = "";
- return new Promise(async (r) => {
+ return new Promise((resolve) => {
const input = userSelection.getElementsByTagName("input")[0];
const select = userSelection.getElementsByTagName("select")[0];
const inputSection = input.closest("section");
@@ -40,7 +40,6 @@ export class UserSelectionScreen {
e.preventDefault();
if (inputActive == null) {
error.textContent = "Please enter a username or select an existing user.";
- return;
} else if (inputActive) {
const username = input.value.trim();
if (!username) {
@@ -53,13 +52,7 @@ export class UserSelectionScreen {
const spinner = createSpinner();
button.prepend(spinner);
try {
- const resp = await api.fetchApi("/users", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ username }),
- });
+ const resp = await api.createUser(username);
if (resp.status >= 300) {
let message = "Error creating user: " + resp.status + " " + resp.statusText;
try {
@@ -72,7 +65,7 @@ export class UserSelectionScreen {
throw new Error(message);
}
- r({ username, userId: await resp.json(), created: true });
+ resolve({ username, userId: await resp.json(), created: true });
} catch (err) {
spinner.remove();
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
@@ -83,7 +76,7 @@ export class UserSelectionScreen {
error.textContent = "Please select an existing user.";
return;
} else {
- r({ username: users[select.value], userId: select.value, created: false });
+ resolve({ username: users[select.value], userId: select.value, created: false });
}
});
@@ -106,7 +99,7 @@ export class UserSelectionScreen {
select.style.color = "var(--descrip-text)";
if (select.value) {
- // Focus the input, do this separately as sometimes browsers like to fill in the value
+ // Focus the select, do this separately as sometimes browsers like to fill in the value
select.focus();
}
} else {