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 { 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<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
*/
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<Ez["graph"]>["graph"] } graph
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb

View File

@ -18,16 +18,21 @@ function* walkSync(dir) {
*/
/**
* @param {
* {
* @param {{
* mockExtensions?: string[],
* mockNodeDefs?: Record<string, ComfyObjectInfo>,
* users?: boolean | Record<string, string>
* settings?: Record<string, string>
* 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) {
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() {

View File

@ -324,13 +324,28 @@ class ComfyApi extends EventTarget {
}
/**
* Gets all setting values for the current user
* @returns { Promise<string, unknown> } 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<string, unknown> | 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<string, unknown> } A dictionary of id -> value

View File

@ -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 {