user tests

This commit is contained in:
pythongosssss 2023-12-03 15:59:09 +00:00
parent cb91a6eecd
commit 325008a57d
5 changed files with 292 additions and 34 deletions

View File

@ -0,0 +1,223 @@
// @ts-check
/// <reference path="../node_modules/@types/jest/index.d.ts" />
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();
});
});
});

View File

@ -1,10 +1,18 @@
const { mockApi } = require("./setup"); const { mockApi } = require("./setup");
const { Ez } = require("./ezgraph"); const { Ez } = require("./ezgraph");
const lg = require("./litegraph"); const lg = require("./litegraph");
const fs = require("fs");
const path = require("path");
const html = fs.readFileSync(path.resolve(__dirname, "../../web/index.html"))
/** /**
* *
* @param { Parameters<mockApi>[0] & { resetEnv?: boolean, preSetup?(app): Promise<void> } } config * @param { Parameters<typeof mockApi>[0] & {
* resetEnv?: boolean,
* preSetup?(app): Promise<void>,
* localStorage?: Record<string, string>
* } } config
* @returns * @returns
*/ */
export async function start(config = {}) { export async function start(config = {}) {
@ -12,8 +20,14 @@ export async function start(config = {}) {
jest.resetModules(); jest.resetModules();
jest.resetAllMocks(); jest.resetAllMocks();
lg.setup(global); lg.setup(global);
localStorage.clear();
sessionStorage.clear();
} }
Object.assign(localStorage, config.localStorage ?? {});
document.body.innerHTML = html;
mockUtils();
mockApi(config); mockApi(config);
const { app } = require("../../web/scripts/app"); const { app } = require("../../web/scripts/app");
config.preSetup?.(app); config.preSetup?.(app);
@ -21,6 +35,12 @@ export async function start(config = {}) {
return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app }; return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
} }
function mockUtils() {
jest.mock("../../web/scripts/utils", () => ({
addStylesheet: () => Promise.resolve()
}));
}
/** /**
* @param { ReturnType<Ez["graph"]>["graph"] } graph * @param { ReturnType<Ez["graph"]>["graph"] } graph
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb * @param { (hasReloaded: boolean) => (Promise<void> | void) } cb

View File

@ -18,16 +18,21 @@ function* walkSync(dir) {
*/ */
/** /**
* @param { * @param {{
* {
* mockExtensions?: string[], * mockExtensions?: string[],
* mockNodeDefs?: Record<string, ComfyObjectInfo>, * mockNodeDefs?: Record<string, ComfyObjectInfo>,
* users?: boolean | Record<string, string> * users?: boolean | Record<string, string>
* settings?: Record<string, string> * settings?: Record<string, string>
* userData?: Record<string, any> * userData?: Record<string, any>
* } } 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) { if (!mockExtensions) {
mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core"))) mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core")))
.filter((x) => x.endsWith(".js")) .filter((x) => x.endsWith(".js"))
@ -36,15 +41,6 @@ export function mockApi({ mockExtensions, mockNodeDefs, users, settings, userDat
if (!mockNodeDefs) { if (!mockNodeDefs) {
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json"))); 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 events = new EventTarget();
const mockApi = { const mockApi = {
@ -56,15 +52,26 @@ export function mockApi({ mockExtensions, mockNodeDefs, users, settings, userDat
getNodeDefs: jest.fn(() => mockNodeDefs), getNodeDefs: jest.fn(() => mockNodeDefs),
init: jest.fn(), init: jest.fn(),
apiURL: jest.fn((x) => "../../web/" + x), 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), getUsers: jest.fn(() => users),
getSettings: jest.fn(() => settings ?? {}), getSettings: jest.fn(() => settings),
getUserData: jest.fn(f => { storeSettings: jest.fn((v) => Object.assign(settings, v)),
if(f in userData) { getUserData: jest.fn((f) => {
if (f in userData) {
return { status: 200, json: () => userData[f] }; return { status: 200, json: () => userData[f] };
} else { } else {
return { status: 404 } return { status: 404 };
} }
}) }),
storeUserData: jest.fn((file, data) => {
userData[file] = data;
}),
}; };
jest.mock("../../web/scripts/api", () => ({ jest.mock("../../web/scripts/api", () => ({
get api() { get api() {

View File

@ -324,13 +324,28 @@ class ComfyApi extends EventTarget {
} }
/** /**
* Gets all setting values for the current user * 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<string, unknown> } A dictionary of id -> value * @returns { Promise<string, unknown> | boolean } If multi-user, a dictionary of id -> value, else whether the default user is created
*/ */
async getUsers() { async getUsers() {
return (await this.fetchApi("/users")).json(); 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 * Gets all setting values for the current user
* @returns { Promise<string, unknown> } A dictionary of id -> value * @returns { Promise<string, unknown> } A dictionary of id -> value

View File

@ -9,7 +9,7 @@ export class UserSelectionScreen {
await addStylesheet(import.meta.url); await addStylesheet(import.meta.url);
const userSelection = document.getElementById("comfy-user-selection"); const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = ""; userSelection.style.display = "";
return new Promise(async (r) => { return new Promise((resolve) => {
const input = userSelection.getElementsByTagName("input")[0]; const input = userSelection.getElementsByTagName("input")[0];
const select = userSelection.getElementsByTagName("select")[0]; const select = userSelection.getElementsByTagName("select")[0];
const inputSection = input.closest("section"); const inputSection = input.closest("section");
@ -40,7 +40,6 @@ export class UserSelectionScreen {
e.preventDefault(); e.preventDefault();
if (inputActive == null) { if (inputActive == null) {
error.textContent = "Please enter a username or select an existing user."; error.textContent = "Please enter a username or select an existing user.";
return;
} else if (inputActive) { } else if (inputActive) {
const username = input.value.trim(); const username = input.value.trim();
if (!username) { if (!username) {
@ -53,13 +52,7 @@ export class UserSelectionScreen {
const spinner = createSpinner(); const spinner = createSpinner();
button.prepend(spinner); button.prepend(spinner);
try { try {
const resp = await api.fetchApi("/users", { const resp = await api.createUser(username);
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
});
if (resp.status >= 300) { if (resp.status >= 300) {
let message = "Error creating user: " + resp.status + " " + resp.statusText; let message = "Error creating user: " + resp.status + " " + resp.statusText;
try { try {
@ -72,7 +65,7 @@ export class UserSelectionScreen {
throw new Error(message); throw new Error(message);
} }
r({ username, userId: await resp.json(), created: true }); resolve({ username, userId: await resp.json(), created: true });
} catch (err) { } catch (err) {
spinner.remove(); spinner.remove();
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred."; 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."; error.textContent = "Please select an existing user.";
return; return;
} else { } 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)"; select.style.color = "var(--descrip-text)";
if (select.value) { 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(); select.focus();
} }
} else { } else {