From 99d50d754da290e023c7493a8ccfcab3f1f06244 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 25 Dec 2019 00:17:02 +0100 Subject: [PATCH] Replaced js-logger. Improved testability with mocks, improved test configuration and code improvements. --- jest.config.js | 5 +- package.json | 3 +- src/__mocks__/monaco-editor.js | 89 +++++++ src/__mocks__/webworkers.js | 6 + src/application/index.test.ts | 49 ++++ src/application/index.ts | 132 +++++++++++ src/core/HttpClient.ts | 4 +- src/core/Logger.ts | 154 ++++++++++++ src/core/Persister.ts | 4 +- .../compression/prs/decompress.ts | 4 +- src/core/data_formats/parsing/ninja/njcm.ts | 4 +- .../data_formats/parsing/ninja/texture.ts | 4 +- src/core/data_formats/parsing/ninja/xj.ts | 4 +- src/core/data_formats/parsing/prc.ts | 4 +- src/core/data_formats/parsing/quest/bin.ts | 4 +- src/core/data_formats/parsing/quest/dat.ts | 4 +- src/core/data_formats/parsing/quest/index.ts | 4 +- src/core/data_formats/parsing/quest/qst.ts | 8 +- src/core/data_formats/parsing/rlc.ts | 4 +- src/core/gui/FileButton.css | 2 + src/core/gui/FileButton.ts | 1 - src/core/gui/Input.ts | 9 +- src/core/gui/LazyWidget.ts | 4 +- src/core/gui/TabContainer.ts | 3 +- src/core/gui/Table.ts | 4 +- src/core/gui/Widget.ts | 4 +- src/core/gui/dom.ts | 51 +++- src/core/observable/Disposer.ts | 8 +- src/core/observable/SimpleEmitter.ts | 4 +- .../property/AbstractMinimalProperty.ts | 4 +- .../property/list/AbstractListProperty.ts | 4 +- src/core/stores/DisposableServerMap.ts | 27 +++ src/core/stores/GuiStore.ts | 46 ++-- src/core/stores/ItemTypeStore.ts | 10 +- src/core/stores/Store.ts | 18 ++ src/core/undo/UndoStack.ts | 4 +- .../gui/MethodsForEpisodeView.ts | 4 +- .../gui/OptimizationResultView.ts | 5 +- src/hunt_optimizer/gui/WantedItemsView.ts | 4 +- src/hunt_optimizer/index.ts | 39 ++-- .../persistence/HuntMethodPersister.ts | 2 +- src/hunt_optimizer/stores/HuntMethodStore.ts | 25 +- .../stores/HuntOptimizerStore.ts | 31 ++- src/hunt_optimizer/stores/ItemDropStore.ts | 13 +- src/index.ts | 9 +- src/initialize.test.ts | 46 ---- src/initialize.ts | 121 ---------- src/quest_editor/QuestRunner.ts | 20 +- .../QuestEditorToolBarController.test.ts | 8 +- .../QuestEditorToolBarController.ts | 4 +- .../controllers/QuestInfoController.test.ts | 3 - src/quest_editor/gui/LogView.css | 2 +- src/quest_editor/gui/LogView.ts | 26 +-- .../gui/QuestEditorToolBar.test.ts | 3 - src/quest_editor/gui/QuestEditorView.ts | 4 +- src/quest_editor/gui/QuestInfoView.test.ts | 3 - .../QuestEditorToolBar.test.ts.snap | 181 ++++++++++++++ .../__snapshots__/QuestInfoView.test.ts.snap | 218 +++++++++++++++++ src/quest_editor/index.ts | 91 +++++--- src/quest_editor/loading/AreaAssetLoader.ts | 9 +- src/quest_editor/loading/EntityAssetLoader.ts | 220 ++++++++++-------- src/quest_editor/loading/LoadingCache.ts | 4 + src/quest_editor/model/QuestModel.ts | 4 +- .../rendering/EntityImageRenderer.ts | 26 ++- .../rendering/QuestModelManager.ts | 19 +- src/quest_editor/scripting/assembly.ts | 4 +- src/quest_editor/scripting/assembly_worker.ts | 5 - .../data_flow_analysis/register_value.ts | 4 +- .../data_flow_analysis/stack_value.ts | 4 +- src/quest_editor/scripting/disassembly.ts | 4 +- .../scripting/vm/VirtualMachine.ts | 4 +- src/quest_editor/scripting/vm/io.ts | 4 +- src/quest_editor/stores/AreaStore.ts | 5 +- src/quest_editor/stores/AsmEditorStore.ts | 21 +- src/quest_editor/stores/LogStore.ts | 137 ++++------- src/quest_editor/stores/QuestEditorStore.ts | 16 +- src/quest_editor/stores/model_conversion.ts | 4 +- src/viewer/index.ts | 39 +++- src/viewer/rendering/TextureRenderer.ts | 4 +- src/viewer/stores/Model3DStore.ts | 17 +- src/viewer/stores/TextureStore.ts | 7 +- test/src/core/FileSystemHttpClient.ts | 5 +- test/src/setup.js | 10 +- test/src/utils.ts | 10 + tsconfig-scripts.json | 39 ++-- tsconfig.json | 3 +- webpack.common.js | 15 +- webpack.dev.js | 26 +-- webpack.prod.js | 7 +- yarn.lock | 5 - 90 files changed, 1498 insertions(+), 738 deletions(-) create mode 100644 src/__mocks__/monaco-editor.js create mode 100644 src/__mocks__/webworkers.js create mode 100644 src/application/index.test.ts create mode 100644 src/application/index.ts create mode 100644 src/core/Logger.ts create mode 100644 src/core/stores/DisposableServerMap.ts create mode 100644 src/core/stores/Store.ts delete mode 100644 src/initialize.test.ts delete mode 100644 src/initialize.ts create mode 100644 src/quest_editor/gui/__snapshots__/QuestEditorToolBar.test.ts.snap create mode 100644 src/quest_editor/gui/__snapshots__/QuestInfoView.test.ts.snap diff --git a/jest.config.js b/jest.config.js index 62cdd3b5..2c040c69 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,11 @@ module.exports = { preset: "ts-jest", - testEnvironment: "node", moduleDirectories: ["node_modules"], setupFiles: ["./test/src/setup.js"], + roots: ["./src", "./test"], moduleNameMapper: { "\\.(css|gif|jpg|png|svg|ttf)$": "/src/__mocks__/static_files.js", - "monaco-editor": "/node_modules/monaco-editor/dev/vs/editor/editor.main.js", + "^monaco-editor$": "/node_modules/monaco-editor/esm/vs/editor/editor.main.js", + "^worker-loader!": "/src/__mocks__/webworkers.js", }, }; diff --git a/package.json b/package.json index 9892ad60..d6a9a9a0 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,13 @@ "camera-controls": "^1.16.2", "golden-layout": "^1.5.9", "javascript-lp-solver": "0.4.17", - "js-logger": "^1.6.0", "lodash": "^4.17.15", "luxon": "^1.21.3", "monaco-editor": "^0.19.0", "three": "^0.111.0" }, "scripts": { - "start": "webpack-dev-server --port 1623 --config webpack.dev.js", + "start": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.prod.js", "test": "jest", "update_generic_data": "ts-node --project=tsconfig-scripts.json assets_generation/update_generic_data.ts", diff --git a/src/__mocks__/monaco-editor.js b/src/__mocks__/monaco-editor.js new file mode 100644 index 00000000..be6ece7c --- /dev/null +++ b/src/__mocks__/monaco-editor.js @@ -0,0 +1,89 @@ +class Editor { + addCommand() {} + getAction() {} + addAction() { + return { dispose() {} }; + } + trigger() {} + updateOptions() {} + setModel() {} + setPosition() {} + getLineDecorations() {} + deltaDecorations() {} + revealLineInCenterIfOutsideViewport() {} + revealPositionInCenterIfOutsideViewport() {} + onDidFocusEditorWidget() { + return { dispose() {} }; + } + onMouseDown() { + return { dispose() {} }; + } + onMouseUp() { + return { dispose() {} }; + } + focus() {} + layout() {} + onDidChangeCursorPosition() { + return { dispose() {} }; + } + dispose() {} +} + +exports.editor = { + defineTheme() {}, + createModel() {}, + create() { + return new Editor(); + }, +}; + +exports.languages = { + CompletionItemKind: { + Method: 0, + Function: 1, + Constructor: 2, + Field: 3, + Variable: 4, + Class: 5, + Struct: 6, + Interface: 7, + Module: 8, + Property: 9, + Event: 10, + Operator: 11, + Unit: 12, + Value: 13, + Constant: 14, + Enum: 15, + EnumMember: 16, + Keyword: 17, + Text: 18, + Color: 19, + File: 20, + Reference: 21, + Customcolor: 22, + Folder: 23, + TypeParameter: 24, + Snippet: 25, + }, + register() {}, + setMonarchTokensProvider() {}, + registerCompletionItemProvider() {}, + registerSignatureHelpProvider() {}, + setLanguageConfiguration() {}, + registerDefinitionProvider() {}, +}; + +exports.KeyMod = { + CtrlCmd: 0, + Shift: 1, + Alt: 2, + WinCtrl: 3, +}; + +exports.KeyCode = { + LeftArrow: 15, + UpArrow: 16, + RightArrow: 17, + DownArrow: 18, +}; diff --git a/src/__mocks__/webworkers.js b/src/__mocks__/webworkers.js new file mode 100644 index 00000000..6614e494 --- /dev/null +++ b/src/__mocks__/webworkers.js @@ -0,0 +1,6 @@ +class Worker { + onmessage() {} + postMessage() {} +} + +module.exports = Worker; diff --git a/src/application/index.test.ts b/src/application/index.test.ts new file mode 100644 index 00000000..d39d077a --- /dev/null +++ b/src/application/index.test.ts @@ -0,0 +1,49 @@ +import { initialize_application } from "./index"; +import { DisposableThreeRenderer } from "../core/rendering/Renderer"; +import { LogManager, LogHandler, LogLevel } from "../core/Logger"; +import { FileSystemHttpClient } from "../../test/src/core/FileSystemHttpClient"; +import { timeout } from "../../test/src/utils"; + +for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) { + const with_path = path == undefined ? "without specific path" : `with path ${path}`; + + test(`Initialization and shutdown ${with_path} should succeed without throwing errors or logging with level Warn or above.`, async () => { + const logged_errors: string[] = []; + + const handler: LogHandler = ({ level, message }) => { + if (level >= LogLevel.Warn) { + logged_errors.push(message); + } + }; + + return LogManager.with_default_handler(handler, async () => { + if (path != undefined) { + window.location.hash = path; + } + + const app = initialize_application( + new FileSystemHttpClient(), + () => new StubRenderer(), + ); + + expect(app).toBeDefined(); + expect(logged_errors).toEqual([]); + + await timeout(2000); + + app.dispose(); + + expect(logged_errors).toEqual([]); + }); + }); +} + +class StubRenderer implements DisposableThreeRenderer { + domElement: HTMLCanvasElement = document.createElement("canvas"); + + dispose(): void {} // eslint-disable-line + + render(): void {} // eslint-disable-line + + setSize(): void {} // eslint-disable-line +} diff --git a/src/application/index.ts b/src/application/index.ts new file mode 100644 index 00000000..a4804fb9 --- /dev/null +++ b/src/application/index.ts @@ -0,0 +1,132 @@ +import { HttpClient } from "../core/HttpClient"; +import { Disposable } from "../core/observable/Disposable"; +import { GuiStore, GuiTool } from "../core/stores/GuiStore"; +import { create_item_type_stores } from "../core/stores/ItemTypeStore"; +import { create_item_drop_stores } from "../hunt_optimizer/stores/ItemDropStore"; +import { ApplicationView } from "./gui/ApplicationView"; +import { throttle } from "lodash"; +import { DisposableThreeRenderer } from "../core/rendering/Renderer"; +import { Disposer } from "../core/observable/Disposer"; +import { disposable_custom_listener, disposable_listener } from "../core/gui/dom"; + +export function initialize_application( + http_client: HttpClient, + create_three_renderer: () => DisposableThreeRenderer, +): Disposable { + const disposer = new Disposer(); + + // Disable native undo/redo. + disposer.add(disposable_custom_listener(document, "beforeinput", before_input)); + // Work-around for FireFox: + disposer.add(disposable_listener(document, "keydown", keydown)); + + // Disable native drag-and-drop to avoid users dragging in unsupported file formats and leaving + // the application unexpectedly. + disposer.add_all( + disposable_listener(document, "dragenter", dragenter), + disposable_listener(document, "dragover", dragover), + disposable_listener(document, "drop", drop), + ); + + // Initialize core stores shared by several submodules. + const gui_store = disposer.add(new GuiStore()); + const item_type_stores = disposer.add(create_item_type_stores(http_client, gui_store)); + const item_drop_stores = disposer.add( + create_item_drop_stores(http_client, gui_store, item_type_stores), + ); + + // Initialize application view. + const application_view = disposer.add( + new ApplicationView(gui_store, [ + [ + GuiTool.Viewer, + async () => { + const { initialize_viewer } = await import("../viewer"); + const viewer = disposer.add( + initialize_viewer(http_client, gui_store, create_three_renderer), + ); + + return viewer.view; + }, + ], + [ + GuiTool.QuestEditor, + async () => { + const { initialize_quest_editor } = await import("../quest_editor"); + const quest_editor = disposer.add( + initialize_quest_editor(http_client, gui_store, create_three_renderer), + ); + + return quest_editor.view; + }, + ], + [ + GuiTool.HuntOptimizer, + async () => { + const { initialize_hunt_optimizer } = await import("../hunt_optimizer"); + const hunt_optimizer = disposer.add( + initialize_hunt_optimizer( + http_client, + gui_store, + item_type_stores, + item_drop_stores, + ), + ); + + return hunt_optimizer.view; + }, + ], + ]), + ); + + // Resize the view on window resize. + const resize = throttle( + () => { + application_view.resize(window.innerWidth, window.innerHeight); + }, + 100, + { leading: true, trailing: true }, + ); + + resize(); + document.body.append(application_view.element); + disposer.add(disposable_listener(window, "resize", resize)); + + return { + dispose(): void { + disposer.dispose(); + }, + }; +} + +function before_input(e: Event): void { + const ie = e as any; + + if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") { + e.preventDefault(); + } +} + +function keydown(e: Event): void { + const kbe = e as KeyboardEvent; + + if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") { + kbe.preventDefault(); + } +} + +function dragenter(e: DragEvent): void { + e.preventDefault(); + + if (e.dataTransfer) { + e.dataTransfer.dropEffect = "none"; + } +} + +function dragover(e: DragEvent): void { + dragenter(e); +} + +function drop(e: DragEvent): void { + dragenter(e); +} diff --git a/src/core/HttpClient.ts b/src/core/HttpClient.ts index 73b571d4..febf560b 100644 --- a/src/core/HttpClient.ts +++ b/src/core/HttpClient.ts @@ -33,12 +33,12 @@ export class StubHttpClient implements HttpClient { get(url: string): HttpResponse { return { json(): Promise { - throw new Error(`Dummy client's json method invoked for get request to "${url}".`); + throw new Error(`Stub client's json method invoked for get request to "${url}".`); }, array_buffer(): Promise { throw new Error( - `Dummy client's array_buffer method invoked for get request to "${url}".`, + `Stub client's array_buffer method invoked for get request to "${url}".`, ); }, }; diff --git a/src/core/Logger.ts b/src/core/Logger.ts new file mode 100644 index 00000000..5e63c158 --- /dev/null +++ b/src/core/Logger.ts @@ -0,0 +1,154 @@ +import { enum_values } from "./enums"; +import { assert } from "./util"; + +// Log level names in order of importance. +export enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +export const LogLevels = enum_values(LogLevel); + +export function log_level_from_string(str: string): LogLevel { + const level = (LogLevel as any)[str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()]; + assert(level != undefined, () => `"${str}" is not a valid log level.`); + return level; +} + +export type LogEntry = { + readonly time: Date; + readonly message: string; + readonly level: LogLevel; + readonly logger: Logger; + readonly cause?: any; +}; + +export type LogHandler = (entry: LogEntry, logger_name: string) => void; + +function default_log_handler({ time, message, level, logger, cause }: LogEntry): void { + const str = `${time_to_string(time)} [${LogLevel[level]}] ${logger.name} - ${message}`; + + /* eslint-disable no-console */ + let method: (...args: any[]) => void; + + switch (level) { + case LogLevel.Trace: + method = console.trace; + break; + case LogLevel.Debug: + method = console.debug; + break; + case LogLevel.Info: + method = console.info; + break; + case LogLevel.Warn: + method = console.warn; + break; + case LogLevel.Error: + method = console.error; + break; + default: + method = console.log; + } + + if (cause == undefined) { + method.call(console, str); + } else { + method.call(console, str, cause); + } + /* eslint-enable no-console */ +} + +export function time_to_string(time: Date): string { + const hours = time_part_to_string(time.getHours(), 2); + const minutes = time_part_to_string(time.getMinutes(), 2); + const seconds = time_part_to_string(time.getSeconds(), 2); + const millis = time_part_to_string(time.getMilliseconds(), 3); + return `${hours}:${minutes}:${seconds}.${millis}`; +} + +function time_part_to_string(value: number, n: number): string { + return value.toString().padStart(n, "0"); +} + +export class Logger { + private _level?: LogLevel; + + get level(): LogLevel { + return this._level ?? LogManager.default_level; + } + + set level(level: LogLevel) { + this._level = level; + } + + private _handler?: LogHandler; + + get handler(): LogHandler { + return this._handler ?? LogManager.default_handler; + } + + set handler(handler: LogHandler) { + this._handler = handler; + } + + constructor(readonly name: string) {} + + trace = (message: string, cause?: any): void => { + this.handle(LogLevel.Trace, message, cause); + }; + + debug = (message: string, cause?: any): void => { + this.handle(LogLevel.Debug, message, cause); + }; + + info = (message: string, cause?: any): void => { + this.handle(LogLevel.Info, message, cause); + }; + + warn = (message: string, cause?: any): void => { + this.handle(LogLevel.Warn, message, cause); + }; + + error = (message: string, cause?: any): void => { + this.handle(LogLevel.Error, message, cause); + }; + + private handle(level: LogLevel, message: string, cause?: any): void { + if (level >= this.level) { + this.handler({ time: new Date(), message, level, logger: this, cause }, this.name); + } + } +} + +export class LogManager { + private static readonly loggers = new Map(); + + static default_level: LogLevel = log_level_from_string(process.env["LOG_LEVEL"] ?? "Info"); + static default_handler: LogHandler = default_log_handler; + + static get(name: string): Logger { + let logger = this.loggers.get(name); + + if (!logger) { + logger = new Logger(name); + this.loggers.set(name, logger); + } + + return logger; + } + + static with_default_handler(handler: LogHandler, f: () => T): T { + const orig_handler = this.default_handler; + + try { + this.default_handler = handler; + return f(); + } finally { + this.default_handler = orig_handler; + } + } +} diff --git a/src/core/Persister.ts b/src/core/Persister.ts index 12b7f167..fe0648cc 100644 --- a/src/core/Persister.ts +++ b/src/core/Persister.ts @@ -1,7 +1,7 @@ -import Logger from "js-logger"; import { Server } from "./model"; +import { LogManager } from "./Logger"; -const logger = Logger.get("core/persistence/Persister"); +const logger = LogManager.get("core/persistence/Persister"); export abstract class Persister { protected persist_for_server(server: Server, key: string, data: any): void { diff --git a/src/core/data_formats/compression/prs/decompress.ts b/src/core/data_formats/compression/prs/decompress.ts index 567fa1e9..cbae57b9 100644 --- a/src/core/data_formats/compression/prs/decompress.ts +++ b/src/core/data_formats/compression/prs/decompress.ts @@ -1,10 +1,10 @@ -import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; import { WritableCursor } from "../../cursor/WritableCursor"; import { ResizableBuffer } from "../../ResizableBuffer"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/compression/prs/decompress"); +const logger = LogManager.get("core/data_formats/compression/prs/decompress"); export function prs_decompress(cursor: Cursor): Cursor { const ctx = new Context(cursor); diff --git a/src/core/data_formats/parsing/ninja/njcm.ts b/src/core/data_formats/parsing/ninja/njcm.ts index b03d2f3e..3dea38f0 100644 --- a/src/core/data_formats/parsing/ninja/njcm.ts +++ b/src/core/data_formats/parsing/ninja/njcm.ts @@ -1,8 +1,8 @@ -import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; import { Vec2, Vec3 } from "../../vector"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/ninja/njcm"); +const logger = LogManager.get("core/data_formats/parsing/ninja/njcm"); // TODO: // - colors diff --git a/src/core/data_formats/parsing/ninja/texture.ts b/src/core/data_formats/parsing/ninja/texture.ts index 40694e75..0bd03f6e 100644 --- a/src/core/data_formats/parsing/ninja/texture.ts +++ b/src/core/data_formats/parsing/ninja/texture.ts @@ -1,8 +1,8 @@ -import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; import { parse_iff } from "../iff"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/ninja/texture"); +const logger = LogManager.get("core/data_formats/parsing/ninja/texture"); export type Xvm = { textures: XvmTexture[]; diff --git a/src/core/data_formats/parsing/ninja/xj.ts b/src/core/data_formats/parsing/ninja/xj.ts index 4609dc44..bfcb8490 100644 --- a/src/core/data_formats/parsing/ninja/xj.ts +++ b/src/core/data_formats/parsing/ninja/xj.ts @@ -1,8 +1,8 @@ -import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; import { Vec2, Vec3 } from "../../vector"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/ninja/xj"); +const logger = LogManager.get("core/data_formats/parsing/ninja/xj"); // TODO: // - vertex colors diff --git a/src/core/data_formats/parsing/prc.ts b/src/core/data_formats/parsing/prc.ts index 8b96c265..b0a92a1d 100644 --- a/src/core/data_formats/parsing/prc.ts +++ b/src/core/data_formats/parsing/prc.ts @@ -1,9 +1,9 @@ -import Logger from "js-logger"; import { prs_decompress } from "../compression/prs/decompress"; import { Cursor } from "../cursor/Cursor"; import { prc_decrypt } from "../encryption/prc"; +import { LogManager } from "../../Logger"; -const logger = Logger.get("core/data_formats/parsing/prc"); +const logger = LogManager.get("core/data_formats/parsing/prc"); /** * Decrypts and decompresses a .prc file. diff --git a/src/core/data_formats/parsing/quest/bin.ts b/src/core/data_formats/parsing/quest/bin.ts index 22c3868a..bd55947e 100644 --- a/src/core/data_formats/parsing/quest/bin.ts +++ b/src/core/data_formats/parsing/quest/bin.ts @@ -1,4 +1,3 @@ -import Logger from "js-logger"; import { Endianness } from "../../Endianness"; import { ControlFlowGraph } from "../../../../quest_editor/scripting/data_flow_analysis/ControlFlowGraph"; import { register_value } from "../../../../quest_editor/scripting/data_flow_analysis/register_value"; @@ -27,8 +26,9 @@ import { Cursor } from "../../cursor/Cursor"; import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; import { WritableCursor } from "../../cursor/WritableCursor"; import { ResizableBuffer } from "../../ResizableBuffer"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/quest/bin"); +const logger = LogManager.get("core/data_formats/parsing/quest/bin"); export type BinFile = { readonly quest_id: number; diff --git a/src/core/data_formats/parsing/quest/dat.ts b/src/core/data_formats/parsing/quest/dat.ts index 37ac195e..4156381e 100644 --- a/src/core/data_formats/parsing/quest/dat.ts +++ b/src/core/data_formats/parsing/quest/dat.ts @@ -1,4 +1,3 @@ -import Logger from "js-logger"; import { groupBy } from "lodash"; import { Endianness } from "../../Endianness"; import { Cursor } from "../../cursor/Cursor"; @@ -7,8 +6,9 @@ import { ResizableBuffer } from "../../ResizableBuffer"; import { Vec3 } from "../../vector"; import { WritableCursor } from "../../cursor/WritableCursor"; import { assert } from "../../../util"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/quest/dat"); +const logger = LogManager.get("core/data_formats/parsing/quest/dat"); const OBJECT_SIZE = 68; const NPC_SIZE = 72; diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index 6e2b014a..a806503d 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -1,4 +1,3 @@ -import Logger from "js-logger"; import { Instruction, InstructionSegment, @@ -20,8 +19,9 @@ import { object_data, ObjectType, pso_id_to_object_type } from "./object_types"; import { parse_qst, QstContainedFile, write_qst } from "./qst"; import { npc_data, NpcType } from "./npc_types"; import { reinterpret_f32_as_i32, reinterpret_i32_as_f32 } from "../../../primitive_conversion"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/quest"); +const logger = LogManager.get("core/data_formats/parsing/quest"); export type Quest = { readonly id: number; diff --git a/src/core/data_formats/parsing/quest/qst.ts b/src/core/data_formats/parsing/quest/qst.ts index 3c468149..4d851167 100644 --- a/src/core/data_formats/parsing/quest/qst.ts +++ b/src/core/data_formats/parsing/quest/qst.ts @@ -1,4 +1,3 @@ -import Logger from "js-logger"; import { Endianness } from "../../Endianness"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { Cursor } from "../../cursor/Cursor"; @@ -6,8 +5,9 @@ import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; import { WritableCursor } from "../../cursor/WritableCursor"; import { ResizableBuffer } from "../../ResizableBuffer"; import { basename } from "../../../util"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/data_formats/parsing/quest/qst"); +const logger = LogManager.get("core/data_formats/parsing/quest/qst"); export type QstContainedFile = { id?: number; @@ -114,8 +114,8 @@ type QstHeader = { function parse_headers(cursor: Cursor): QstHeader[] { const headers: QstHeader[] = []; - let prev_quest_id: number | undefined; - let prev_file_name: string | undefined; + let prev_quest_id: number | undefined = undefined; + let prev_file_name: string | undefined = undefined; for (let i = 0; i < 4; ++i) { cursor.seek(4); diff --git a/src/core/data_formats/parsing/rlc.ts b/src/core/data_formats/parsing/rlc.ts index 05cf8538..bb5b3a91 100644 --- a/src/core/data_formats/parsing/rlc.ts +++ b/src/core/data_formats/parsing/rlc.ts @@ -1,9 +1,9 @@ -import Logger from "js-logger"; import { Endianness } from "../Endianness"; import { Cursor } from "../cursor/Cursor"; import { parse_prc } from "./prc"; +import { LogManager } from "../../Logger"; -const logger = Logger.get("core/data_formats/parsing/rlc"); +const logger = LogManager.get("core/data_formats/parsing/rlc"); const MARKER = "RelChunkVer0.20"; /** diff --git a/src/core/gui/FileButton.css b/src/core/gui/FileButton.css index 85601698..f8f72632 100644 --- a/src/core/gui/FileButton.css +++ b/src/core/gui/FileButton.css @@ -1,3 +1,5 @@ +@import "./Button.css"; + .core_FileButton_input { overflow: hidden; clip: rect(0, 0, 0, 0); diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index 4b0c74e8..68ab1fbb 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -1,6 +1,5 @@ import { create_element, el, icon, Icon } from "./dom"; import "./FileButton.css"; -import "./Button.css"; import { property } from "../observable"; import { Property } from "../observable/property/Property"; import { Control, ControlOptions } from "./Control"; diff --git a/src/core/gui/Input.ts b/src/core/gui/Input.ts index 098c3dfd..96505701 100644 --- a/src/core/gui/Input.ts +++ b/src/core/gui/Input.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-dupe-class-members */ import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; import { create_element, el } from "./dom"; import { WritableProperty } from "../observable/property/WritableProperty"; @@ -57,13 +56,7 @@ export abstract class Input extends LabelledControl { protected abstract set_value(value: T): void; - protected set_attr(attr: InputAttrsOfType, value?: T | Property): void; - protected set_attr( - attr: InputAttrsOfType, - value: T | Property | undefined, - convert: (value: T) => U, - ): void; - protected set_attr( + protected set_attr( attr: InputAttrsOfType, value?: T | Property, convert?: (value: T) => U, diff --git a/src/core/gui/LazyWidget.ts b/src/core/gui/LazyWidget.ts index 174b3add..84e307d0 100644 --- a/src/core/gui/LazyWidget.ts +++ b/src/core/gui/LazyWidget.ts @@ -22,7 +22,9 @@ export class LazyWidget extends ResizableWidget { this.initialized = true; this.create_view().then(view => { - if (!this.disposed) { + if (this.disposed) { + view.dispose(); + } else { this.view = this.disposable(view); this.view.resize(this.width, this.height); this.element.append(view.element); diff --git a/src/core/gui/TabContainer.ts b/src/core/gui/TabContainer.ts index 342014a9..fe27a240 100644 --- a/src/core/gui/TabContainer.ts +++ b/src/core/gui/TabContainer.ts @@ -39,7 +39,7 @@ export class TabContainer extends ResizableWidget { }); this.bar_element.append(tab_element); - const lazy_view = new LazyWidget(tab.create_view); + const lazy_view = this.disposable(new LazyWidget(tab.create_view)); this.tabs.push({ ...tab, @@ -48,7 +48,6 @@ export class TabContainer extends ResizableWidget { }); this.panes_element.append(lazy_view.element); - this.disposable(lazy_view); } if (this.tabs.length) { diff --git a/src/core/gui/Table.ts b/src/core/gui/Table.ts index 4005522a..5026e2e4 100644 --- a/src/core/gui/Table.ts +++ b/src/core/gui/Table.ts @@ -4,9 +4,9 @@ import { ListProperty } from "../observable/property/list/ListProperty"; import { Disposer } from "../observable/Disposer"; import "./Table.css"; import { Disposable } from "../observable/Disposable"; -import Logger = require("js-logger"); +import { LogManager } from "../Logger"; -const logger = Logger.get("core/gui/Table"); +const logger = LogManager.get("core/gui/Table"); export type Column = { key?: string; diff --git a/src/core/gui/Widget.ts b/src/core/gui/Widget.ts index 5352d27a..ab1a1562 100644 --- a/src/core/gui/Widget.ts +++ b/src/core/gui/Widget.ts @@ -5,9 +5,9 @@ import { bind_hidden } from "./dom"; import { WritableProperty } from "../observable/property/WritableProperty"; import { WidgetProperty } from "../observable/property/WidgetProperty"; import { Property } from "../observable/property/Property"; -import Logger from "js-logger"; +import { LogManager } from "../Logger"; -const logger = Logger.get("core/gui/Widget"); +const logger = LogManager.get("core/gui/Widget"); export type WidgetOptions = { id?: string; diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index 9779221e..b32ab56b 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -234,17 +234,60 @@ export function section_id_icon(section_id: SectionId, options?: { size?: number return element; } +export function disposable_listener( + target: GlobalEventHandlers, + type: K, + listener: (this: GlobalEventHandlers, ev: GlobalEventHandlersEventMap[K]) => any, + options?: AddEventListenerOptions, +): Disposable; +export function disposable_listener( + target: WindowEventHandlers, + type: K, + listener: (this: WindowEventHandlers, ev: WindowEventHandlersEventMap[K]) => any, + options?: AddEventListenerOptions, +): Disposable; +export function disposable_listener( + target: DocumentAndElementEventHandlers, + type: K, + listener: ( + this: DocumentAndElementEventHandlers, + ev: DocumentAndElementEventHandlersEventMap[K], + ) => any, + options?: AddEventListenerOptions, +): Disposable; export function disposable_listener( - element: EventTarget, - event: string, + target: + | GlobalEventHandlers + | DocumentAndElementEventHandlers + | WindowEventHandlers + | EventTarget, + type: string, listener: EventListenerOrEventListenerObject, options?: AddEventListenerOptions, ): Disposable { - element.addEventListener(event, listener, options); + target.addEventListener(type, listener, options); return { dispose(): void { - element.removeEventListener(event, listener); + target.removeEventListener(type, listener); + }, + }; +} + +/** + * More lax definition of {@link disposable_listener} for custom and experimental event types. + */ +export function disposable_custom_listener( + target: EventTarget, + type: string, + listener: EventListenerOrEventListenerObject, + options?: AddEventListenerOptions, +): Disposable { + target.addEventListener(type, listener, options); + + return { + dispose(): void { + target.removeEventListener(type, listener); }, }; } diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts index e5577d1a..4ddba732 100644 --- a/src/core/observable/Disposer.ts +++ b/src/core/observable/Disposer.ts @@ -1,7 +1,7 @@ import { Disposable } from "./Disposable"; -import Logger = require("js-logger"); +import { LogManager } from "../Logger"; -const logger = Logger.get("core/observable/Disposer"); +const logger = LogManager.get("core/observable/Disposer"); /** * Container for disposables. @@ -29,7 +29,9 @@ export class Disposer implements Disposable { * Add a single disposable and return the given disposable. */ add(disposable: T): T { - if (!this._disposed) { + if (this.disposed) { + disposable.dispose(); + } else { this.disposables.push(disposable); } diff --git a/src/core/observable/SimpleEmitter.ts b/src/core/observable/SimpleEmitter.ts index df854f53..532a53f7 100644 --- a/src/core/observable/SimpleEmitter.ts +++ b/src/core/observable/SimpleEmitter.ts @@ -1,9 +1,9 @@ import { Disposable } from "./Disposable"; -import Logger from "js-logger"; import { Emitter } from "./Emitter"; import { ChangeEvent } from "./Observable"; +import { LogManager } from "../Logger"; -const logger = Logger.get("core/observable/SimpleEmitter"); +const logger = LogManager.get("core/observable/SimpleEmitter"); export class SimpleEmitter implements Emitter { protected readonly observers: ((event: ChangeEvent) => void)[] = []; diff --git a/src/core/observable/property/AbstractMinimalProperty.ts b/src/core/observable/property/AbstractMinimalProperty.ts index a876a1cb..23451247 100644 --- a/src/core/observable/property/AbstractMinimalProperty.ts +++ b/src/core/observable/property/AbstractMinimalProperty.ts @@ -1,8 +1,8 @@ import { Disposable } from "../Disposable"; -import Logger from "js-logger"; import { Property, PropertyChangeEvent } from "./Property"; +import { LogManager } from "../../Logger"; -const logger = Logger.get("core/observable/property/AbstractMinimalProperty"); +const logger = LogManager.get("core/observable/property/AbstractMinimalProperty"); // This class exists purely because otherwise the resulting cyclic dependency graph would trip up commonjs. // The dependency graph is still cyclic but for some reason it's not a problem this way. diff --git a/src/core/observable/property/list/AbstractListProperty.ts b/src/core/observable/property/list/AbstractListProperty.ts index f78a0fbe..6f44b2cc 100644 --- a/src/core/observable/property/list/AbstractListProperty.ts +++ b/src/core/observable/property/list/AbstractListProperty.ts @@ -3,9 +3,9 @@ import { AbstractProperty } from "../AbstractProperty"; import { Disposable } from "../../Disposable"; import { Observable } from "../../Observable"; import { Property } from "../Property"; -import Logger from "js-logger"; +import { LogManager } from "../../../Logger"; -const logger = Logger.get("core/observable/property/list/AbstractListProperty"); +const logger = LogManager.get("core/observable/property/list/AbstractListProperty"); class LengthProperty extends AbstractProperty { private length = 0; diff --git a/src/core/stores/DisposableServerMap.ts b/src/core/stores/DisposableServerMap.ts new file mode 100644 index 00000000..8cf15805 --- /dev/null +++ b/src/core/stores/DisposableServerMap.ts @@ -0,0 +1,27 @@ +import { Server } from "../model"; +import { GuiStore } from "./GuiStore"; +import { ServerMap } from "./ServerMap"; +import { Disposable } from "../observable/Disposable"; +import { Disposer } from "../observable/Disposer"; + +export class DisposableServerMap extends ServerMap implements Disposable { + private readonly disposer = new Disposer(); + + constructor(gui_store: GuiStore, get_value: (server: Server) => Promise) { + super(gui_store, async server => { + const value = await get_value(server); + + if (this.disposer.disposed) { + value.dispose(); + } else { + this.disposer.add(value); + } + + return value; + }); + } + + dispose(): void { + this.disposer.dispose(); + } +} diff --git a/src/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts index 2d46726d..18fb4636 100644 --- a/src/core/stores/GuiStore.ts +++ b/src/core/stores/GuiStore.ts @@ -3,6 +3,8 @@ import { Disposable } from "../observable/Disposable"; import { property } from "../observable"; import { Property } from "../observable/property/Property"; import { Server } from "../model"; +import { Store } from "./Store"; +import { disposable_listener } from "../gui/dom"; export enum GuiTool { Viewer, @@ -17,24 +19,17 @@ const GUI_TOOL_TO_STRING = new Map([ ]); const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k])); -export class GuiStore implements Disposable { - readonly tool: WritableProperty = property(GuiTool.Viewer); - readonly server: Property; - +export class GuiStore extends Store { private readonly _server: WritableProperty = property(Server.Ephinea); - private readonly hash_disposer = this.tool.observe(({ value: tool }) => { - let hash = `#/${gui_tool_to_string(tool)}`; - - if (this.features.size) { - hash += "?features=" + [...this.features].join(","); - } - - window.location.hash = hash; - }); private readonly global_keydown_handlers = new Map void>(); private readonly features: Set = new Set(); + readonly tool: WritableProperty = property(GuiTool.Viewer); + readonly server: Property = this._server; + constructor() { + super(); + const url = window.location.hash.slice(2); const [tool_str, params_str] = url.split("?"); @@ -51,18 +46,21 @@ export class GuiStore implements Disposable { } } + this.disposables( + this.tool.observe(({ value: tool }) => { + let hash = `#/${gui_tool_to_string(tool)}`; + + if (this.features.size) { + hash += "?features=" + [...this.features].join(","); + } + + window.location.hash = hash; + }), + + disposable_listener(window, "keydown", this.dispatch_global_keydown), + ); + this.tool.val = string_to_gui_tool(tool_str) || GuiTool.Viewer; - - this.server = this._server; - - window.addEventListener("keydown", this.dispatch_global_keydown); - } - - dispose(): void { - this.hash_disposer.dispose(); - this.global_keydown_handlers.clear(); - - window.removeEventListener("keydown", this.dispatch_global_keydown); } on_global_keydown( diff --git a/src/core/stores/ItemTypeStore.ts b/src/core/stores/ItemTypeStore.ts index 4a826f93..027225fc 100644 --- a/src/core/stores/ItemTypeStore.ts +++ b/src/core/stores/ItemTypeStore.ts @@ -6,23 +6,25 @@ import { UnitItemType, WeaponItemType, } from "../model/items"; -import { ServerMap } from "./ServerMap"; import { Server } from "../model"; import { ItemTypeDto } from "../dto/ItemTypeDto"; import { GuiStore } from "./GuiStore"; import { HttpClient } from "../HttpClient"; +import { DisposableServerMap } from "./DisposableServerMap"; +import { Store } from "./Store"; export function create_item_type_stores( http_client: HttpClient, gui_store: GuiStore, -): ServerMap { - return new ServerMap(gui_store, create_loader(http_client)); +): DisposableServerMap { + return new DisposableServerMap(gui_store, create_loader(http_client)); } -export class ItemTypeStore { +export class ItemTypeStore extends Store { readonly item_types: ItemType[]; constructor(item_types: ItemType[], private readonly id_to_item_type: ItemType[]) { + super(); this.item_types = item_types; } diff --git a/src/core/stores/Store.ts b/src/core/stores/Store.ts new file mode 100644 index 00000000..bea9eff8 --- /dev/null +++ b/src/core/stores/Store.ts @@ -0,0 +1,18 @@ +import { Disposable } from "../observable/Disposable"; +import { Disposer } from "../observable/Disposer"; + +export abstract class Store implements Disposable { + private readonly disposer = new Disposer(); + + dispose(): void { + this.disposer.dispose(); + } + + protected disposable(disposable: T): T { + return this.disposer.add(disposable); + } + + protected disposables(...disposables: Disposable[]): void { + this.disposer.add_all(...disposables); + } +} diff --git a/src/core/undo/UndoStack.ts b/src/core/undo/UndoStack.ts index 268876a8..f08019f2 100644 --- a/src/core/undo/UndoStack.ts +++ b/src/core/undo/UndoStack.ts @@ -3,9 +3,9 @@ import { WritableListProperty } from "../observable/property/list/WritableListPr import { Action } from "./Action"; import { list_property, map, property } from "../observable"; import { undo_manager } from "./UndoManager"; -import Logger = require("js-logger"); +import { LogManager } from "../Logger"; -const logger = Logger.get("core/undo/UndoStack"); +const logger = LogManager.get("core/undo/UndoStack"); /** * Full-fledged linear undo/redo implementation. diff --git a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts index 013f087a..22d5ad96 100644 --- a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts +++ b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts @@ -15,9 +15,9 @@ import { SortDirection, Table } from "../../core/gui/Table"; import { list_property } from "../../core/observable"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntMethodStore } from "../stores/HuntMethodStore"; -import Logger from "js-logger"; +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("hunt_optimizer/gui/MethodsForEpisodeView"); +const logger = LogManager.get("hunt_optimizer/gui/MethodsForEpisodeView"); export class MethodsForEpisodeView extends ResizableWidget { readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" }); diff --git a/src/hunt_optimizer/gui/OptimizationResultView.ts b/src/hunt_optimizer/gui/OptimizationResultView.ts index d2db1bb6..6dbfe25c 100644 --- a/src/hunt_optimizer/gui/OptimizationResultView.ts +++ b/src/hunt_optimizer/gui/OptimizationResultView.ts @@ -10,9 +10,9 @@ import "./OptimizationResultView.css"; import { Duration } from "luxon"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; -import Logger from "js-logger"; +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("hunt_optimizer/gui/OptimizationResultView"); +const logger = LogManager.get("hunt_optimizer/gui/OptimizationResultView"); export class OptimizationResultView extends Widget { readonly element = el.div( @@ -31,6 +31,7 @@ export class OptimizationResultView extends Widget { async ({ value }) => { try { const hunt_optimizer_store = await value; + if (this.disposed) return; if (this.results_observer) { this.results_observer.dispose(); diff --git a/src/hunt_optimizer/gui/WantedItemsView.ts b/src/hunt_optimizer/gui/WantedItemsView.ts index d6794ded..650417f3 100644 --- a/src/hunt_optimizer/gui/WantedItemsView.ts +++ b/src/hunt_optimizer/gui/WantedItemsView.ts @@ -11,9 +11,9 @@ import { ItemType } from "../../core/model/items"; import { Disposable } from "../../core/observable/Disposable"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; -import Logger from "js-logger"; +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("hunt_optimizer/gui/WantedItemsView"); +const logger = LogManager.get("hunt_optimizer/gui/WantedItemsView"); export class WantedItemsView extends Widget { readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" }); diff --git a/src/hunt_optimizer/index.ts b/src/hunt_optimizer/index.ts index c8b2a593..2f35260e 100644 --- a/src/hunt_optimizer/index.ts +++ b/src/hunt_optimizer/index.ts @@ -1,32 +1,43 @@ import { HuntOptimizerView } from "./gui/HuntOptimizerView"; import { ServerMap } from "../core/stores/ServerMap"; -import { HuntMethodStore, create_hunt_method_stores } from "./stores/HuntMethodStore"; +import { create_hunt_method_stores, HuntMethodStore } from "./stores/HuntMethodStore"; import { GuiStore } from "../core/stores/GuiStore"; -import { HuntOptimizerStore, create_hunt_optimizer_stores } from "./stores/HuntOptimizerStore"; +import { create_hunt_optimizer_stores, HuntOptimizerStore } from "./stores/HuntOptimizerStore"; import { ItemTypeStore } from "../core/stores/ItemTypeStore"; import { HuntMethodPersister } from "./persistence/HuntMethodPersister"; import { HuntOptimizerPersister } from "./persistence/HuntOptimizerPersister"; import { ItemDropStore } from "./stores/ItemDropStore"; import { HttpClient } from "../core/HttpClient"; +import { Disposable } from "../core/observable/Disposable"; +import { Disposer } from "../core/observable/Disposer"; export function initialize_hunt_optimizer( http_client: HttpClient, gui_store: GuiStore, item_type_stores: ServerMap, item_drop_stores: ServerMap, -): HuntOptimizerView { - const hunt_method_stores: ServerMap = create_hunt_method_stores( - http_client, - gui_store, - new HuntMethodPersister(), +): { view: HuntOptimizerView } & Disposable { + const disposer = new Disposer(); + + const hunt_method_stores: ServerMap = disposer.add( + create_hunt_method_stores(http_client, gui_store, new HuntMethodPersister()), ); - const hunt_optimizer_stores: ServerMap = create_hunt_optimizer_stores( - gui_store, - new HuntOptimizerPersister(item_type_stores), - item_type_stores, - item_drop_stores, - hunt_method_stores, + const hunt_optimizer_stores: ServerMap = disposer.add( + create_hunt_optimizer_stores( + gui_store, + new HuntOptimizerPersister(item_type_stores), + item_type_stores, + item_drop_stores, + hunt_method_stores, + ), ); - return new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores); + const view = disposer.add(new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores)); + + return { + view, + dispose(): void { + disposer.dispose(); + }, + }; } diff --git a/src/hunt_optimizer/persistence/HuntMethodPersister.ts b/src/hunt_optimizer/persistence/HuntMethodPersister.ts index 9decec51..58890b00 100644 --- a/src/hunt_optimizer/persistence/HuntMethodPersister.ts +++ b/src/hunt_optimizer/persistence/HuntMethodPersister.ts @@ -1,7 +1,7 @@ -import { Persister } from "../../core/Persister"; import { Server } from "../../core/model"; import { HuntMethodModel } from "../model/HuntMethodModel"; import { Duration } from "luxon"; +import { Persister } from "../../core/Persister"; const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes"; diff --git a/src/hunt_optimizer/stores/HuntMethodStore.ts b/src/hunt_optimizer/stores/HuntMethodStore.ts index ea318339..638b6ebb 100644 --- a/src/hunt_optimizer/stores/HuntMethodStore.ts +++ b/src/hunt_optimizer/stores/HuntMethodStore.ts @@ -1,4 +1,3 @@ -import Logger from "js-logger"; import { Server } from "../../core/model"; import { QuestDto } from "../dto/QuestDto"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; @@ -8,13 +7,13 @@ import { HuntMethodPersister } from "../persistence/HuntMethodPersister"; import { Duration } from "luxon"; import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { list_property } from "../../core/observable"; -import { Disposable } from "../../core/observable/Disposable"; -import { Disposer } from "../../core/observable/Disposer"; import { GuiStore } from "../../core/stores/GuiStore"; -import { ServerMap } from "../../core/stores/ServerMap"; import { HttpClient } from "../../core/HttpClient"; +import { Store } from "../../core/stores/Store"; +import { DisposableServerMap } from "../../core/stores/DisposableServerMap"; +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("hunt_optimizer/stores/HuntMethodStore"); +const logger = LogManager.get("hunt_optimizer/stores/HuntMethodStore"); const DEFAULT_DURATION = Duration.fromObject({ minutes: 30 }); const DEFAULT_GOVERNMENT_TEST_DURATION = Duration.fromObject({ minutes: 45 }); @@ -24,32 +23,28 @@ export function create_hunt_method_stores( http_client: HttpClient, gui_store: GuiStore, hunt_method_persister: HuntMethodPersister, -): ServerMap { - return new ServerMap(gui_store, create_loader(http_client, hunt_method_persister)); +): DisposableServerMap { + return new DisposableServerMap(gui_store, create_loader(http_client, hunt_method_persister)); } -export class HuntMethodStore implements Disposable { +export class HuntMethodStore extends Store { readonly methods: ListProperty; - private readonly disposer = new Disposer(); - constructor( hunt_method_persister: HuntMethodPersister, server: Server, methods: HuntMethodModel[], ) { + super(); + this.methods = list_property(method => [method.user_time], ...methods); - this.disposer.add( + this.disposables( this.methods.observe_list(() => hunt_method_persister.persist_method_user_times(this.methods.val, server), ), ); } - - dispose(): void { - this.disposer.dispose(); - } } function create_loader( diff --git a/src/hunt_optimizer/stores/HuntOptimizerStore.ts b/src/hunt_optimizer/stores/HuntOptimizerStore.ts index b5308356..1fd2b8a2 100644 --- a/src/hunt_optimizer/stores/HuntOptimizerStore.ts +++ b/src/hunt_optimizer/stores/HuntOptimizerStore.ts @@ -19,11 +19,11 @@ import { WritableListProperty } from "../../core/observable/property/list/Writab import { HuntMethodStore } from "./HuntMethodStore"; import { ItemDropStore } from "./ItemDropStore"; import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; -import { Disposable } from "../../core/observable/Disposable"; -import { Disposer } from "../../core/observable/Disposer"; import { ServerMap } from "../../core/stores/ServerMap"; import { GuiStore } from "../../core/stores/GuiStore"; import { HuntOptimizerPersister } from "../persistence/HuntOptimizerPersister"; +import { DisposableServerMap } from "../../core/stores/DisposableServerMap"; +import { Store } from "../../core/stores/Store"; export function create_hunt_optimizer_stores( gui_store: GuiStore, @@ -31,8 +31,8 @@ export function create_hunt_optimizer_stores( item_type_stores: ServerMap, item_drop_stores: ServerMap, hunt_method_stores: ServerMap, -): ServerMap { - return new ServerMap( +): DisposableServerMap { + return new DisposableServerMap( gui_store, create_loader( hunt_optimizer_persister, @@ -50,16 +50,15 @@ export function create_hunt_optimizer_stores( // TODO: Show expected value or probability per item per method. // Can be useful when deciding which item to hunt first. // TODO: boxes. -export class HuntOptimizerStore implements Disposable { - readonly huntable_item_types: ItemType[]; - // TODO: wanted items per server. - readonly wanted_items: ListProperty; - readonly result: Property; - +export class HuntOptimizerStore extends Store { private readonly _wanted_items: WritableListProperty< WantedItemModel > = list_property(wanted_item => [wanted_item.amount]); - private readonly disposer = new Disposer(); + + readonly huntable_item_types: ItemType[]; + // TODO: wanted items per server. + readonly wanted_items: ListProperty = this._wanted_items; + readonly result: Property; constructor( private readonly hunt_optimizer_persister: HuntOptimizerPersister, @@ -68,21 +67,17 @@ export class HuntOptimizerStore implements Disposable { private readonly item_drop_store: ItemDropStore, hunt_method_store: HuntMethodStore, ) { + super(); + this.huntable_item_types = item_type_store.item_types.filter( item_type => item_drop_store.enemy_drops.get_drops_for_item_type(item_type.id).length, ); - this.wanted_items = this._wanted_items; - this.result = map(this.optimize, this.wanted_items, hunt_method_store.methods); this.initialize_persistence(); } - dispose(): void { - this.disposer.dispose(); - } - add_wanted_item(item_type: ItemType): void { if (!this._wanted_items.val.find(wanted => wanted.item_type === item_type)) { this._wanted_items.push(new WantedItemModel(item_type, 1)); @@ -336,7 +331,7 @@ export class HuntOptimizerStore implements Disposable { private initialize_persistence = async (): Promise => { this._wanted_items.val = await this.hunt_optimizer_persister.load_wanted_items(this.server); - this.disposer.add( + this.disposable( this._wanted_items.observe(({ value }) => { this.hunt_optimizer_persister.persist_wanted_items(this.server, value); }), diff --git a/src/hunt_optimizer/stores/ItemDropStore.ts b/src/hunt_optimizer/stores/ItemDropStore.ts index cb389508..816c45b9 100644 --- a/src/hunt_optimizer/stores/ItemDropStore.ts +++ b/src/hunt_optimizer/stores/ItemDropStore.ts @@ -1,27 +1,30 @@ import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../core/model"; import { ServerMap } from "../../core/stores/ServerMap"; -import Logger from "js-logger"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { EnemyDrop } from "../model/ItemDrop"; import { EnemyDropDto } from "../dto/drops"; import { GuiStore } from "../../core/stores/GuiStore"; import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { HttpClient } from "../../core/HttpClient"; +import { DisposableServerMap } from "../../core/stores/DisposableServerMap"; +import { Store } from "../../core/stores/Store"; +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("stores/ItemDropStore"); +const logger = LogManager.get("stores/ItemDropStore"); export function create_item_drop_stores( http_client: HttpClient, gui_store: GuiStore, item_type_stores: ServerMap, -): ServerMap { - return new ServerMap(gui_store, create_loader(http_client, item_type_stores)); +): DisposableServerMap { + return new DisposableServerMap(gui_store, create_loader(http_client, item_type_stores)); } -export class ItemDropStore { +export class ItemDropStore extends Store { readonly enemy_drops: EnemyDropTable; constructor(enemy_drops: EnemyDropTable) { + super(); this.enemy_drops = enemy_drops; } } diff --git a/src/index.ts b/src/index.ts index b8841e0e..e9903315 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,17 @@ import "./core/gui/index.css"; -import Logger from "js-logger"; import "@fortawesome/fontawesome-free/js/fontawesome"; import "@fortawesome/fontawesome-free/js/solid"; import "@fortawesome/fontawesome-free/js/regular"; import "@fortawesome/fontawesome-free/js/brands"; -import { initialize } from "./initialize"; +import { initialize_application } from "./application"; import { FetchClient } from "./core/HttpClient"; import { WebGLRenderer } from "three"; import { DisposableThreeRenderer } from "./core/rendering/Renderer"; -Logger.useDefaults({ - defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] ?? "INFO"], -}); - function create_three_renderer(): DisposableThreeRenderer { const renderer = new WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(window.devicePixelRatio); return renderer; } -initialize(new FetchClient(), create_three_renderer); +initialize_application(new FetchClient(), create_three_renderer); diff --git a/src/initialize.test.ts b/src/initialize.test.ts deleted file mode 100644 index 7dc2025b..00000000 --- a/src/initialize.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import Logger from "js-logger"; -import { IContext } from "js-logger/src/types"; -import { initialize } from "./initialize"; -import { StubHttpClient } from "./core/HttpClient"; -import { DisposableThreeRenderer } from "./core/rendering/Renderer"; - -for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) { - const with_path = path == undefined ? "without specific path" : `with path ${path}`; - - test(`Initialization and shutdown ${with_path} should succeed without throwing errors or logging with level WARN or above.`, () => { - const logged_errors: string[] = []; - - Logger.setHandler((messages: any[], context: IContext) => { - if (context.level.value >= Logger.WARN.value) { - logged_errors.push(Array.prototype.join.call(messages, " ")); - } - }); - - if (path != undefined) { - window.location.hash = path; - } - - const app = initialize(new StubHttpClient(), () => new StubRenderer()); - - expect(app).toBeDefined(); - expect(logged_errors).toEqual([]); - - app.dispose(); - - expect(logged_errors).toEqual([]); - }); -} - -class StubRenderer implements DisposableThreeRenderer { - domElement: HTMLCanvasElement = document.createElement("canvas"); - - dispose(): void {} // eslint-disable-line - - render(): void {} // eslint-disable-line - - setSize(): void {} // eslint-disable-line -} diff --git a/src/initialize.ts b/src/initialize.ts deleted file mode 100644 index 1f8bdee9..00000000 --- a/src/initialize.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { HttpClient } from "./core/HttpClient"; -import { Disposable } from "./core/observable/Disposable"; -import { GuiStore, GuiTool } from "./core/stores/GuiStore"; -import { create_item_type_stores } from "./core/stores/ItemTypeStore"; -import { create_item_drop_stores } from "./hunt_optimizer/stores/ItemDropStore"; -import { ApplicationView } from "./application/gui/ApplicationView"; -import { throttle } from "lodash"; -import { DisposableThreeRenderer } from "./core/rendering/Renderer"; - -export function initialize( - http_client: HttpClient, - create_three_renderer: () => DisposableThreeRenderer, -): Disposable { - // Disable native undo/redo. - document.addEventListener("beforeinput", before_input); - // Work-around for FireFox: - document.addEventListener("keydown", keydown); - - // Disable native drag-and-drop. - document.addEventListener("dragenter", dragenter); - document.addEventListener("dragover", dragover); - document.addEventListener("drop", drop); - - // Initialize core stores shared by several submodules. - const gui_store = new GuiStore(); - const item_type_stores = create_item_type_stores(http_client, gui_store); - const item_drop_stores = create_item_drop_stores(http_client, gui_store, item_type_stores); - - // Initialize application view. - const application_view = new ApplicationView(gui_store, [ - [ - GuiTool.Viewer, - async () => { - return (await import("./viewer/index")).initialize_viewer( - http_client, - gui_store, - create_three_renderer, - ); - }, - ], - [ - GuiTool.QuestEditor, - async () => { - return (await import("./quest_editor/index")).initialize_quest_editor( - http_client, - gui_store, - create_three_renderer, - ); - }, - ], - [ - GuiTool.HuntOptimizer, - async () => { - return (await import("./hunt_optimizer/index")).initialize_hunt_optimizer( - http_client, - gui_store, - item_type_stores, - item_drop_stores, - ); - }, - ], - ]); - - // Resize the view on window resize. - const resize = throttle( - () => { - application_view.resize(window.innerWidth, window.innerHeight); - }, - 100, - { leading: true, trailing: true }, - ); - - resize(); - document.body.append(application_view.element); - window.addEventListener("resize", resize); - - // Dispose view and global event listeners when necessary. - return { - dispose(): void { - window.removeEventListener("beforeinput", before_input); - window.removeEventListener("keydown", keydown); - window.removeEventListener("resize", resize); - window.removeEventListener("dragenter", dragenter); - window.removeEventListener("dragover", dragover); - window.removeEventListener("drop", drop); - application_view.dispose(); - }, - }; -} - -function before_input(e: Event): void { - const ie = e as any; - - if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") { - e.preventDefault(); - } -} - -function keydown(e: Event): void { - const kbe = e as KeyboardEvent; - - if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") { - kbe.preventDefault(); - } -} - -function dragenter(e: DragEvent): void { - e.preventDefault(); - - if (e.dataTransfer) { - e.dataTransfer.dropEffect = "none"; - } -} - -function dragover(e: DragEvent): void { - dragenter(e); -} - -function drop(e: DragEvent): void { - dragenter(e); -} diff --git a/src/quest_editor/QuestRunner.ts b/src/quest_editor/QuestRunner.ts index d47e7cd4..55e380d9 100644 --- a/src/quest_editor/QuestRunner.ts +++ b/src/quest_editor/QuestRunner.ts @@ -54,7 +54,7 @@ export type GameState = Readonly; * delegates to {@link Debugger}. */ export class QuestRunner { - private quest_logger = log_store.get_logger("quest_editor/QuestRunner"); + private logger = log_store.get_logger("quest_editor/QuestRunner"); private animation_frame?: number; private startup = true; private readonly _state: WritableProperty = property( @@ -102,7 +102,7 @@ export class QuestRunner { this.stop(); // Runner state. - this.quest_logger.info("Starting debugger."); + this.logger.info("Starting debugger."); this.startup = true; this.initial_area_id = 0; this.npcs.splice(0, this.npcs.length, ...quest.npcs.val); @@ -148,7 +148,7 @@ export class QuestRunner { return; } - this.quest_logger.info("Stopping debugger."); + this.logger.info("Stopping debugger."); if (this.animation_frame != undefined) { cancelAnimationFrame(this.animation_frame); @@ -292,15 +292,15 @@ export class QuestRunner { }, window_msg: (msg: string): void => { - this.quest_logger.info(`window_msg "${msg}"`); + this.logger.info(`window_msg "${msg}"`); }, message: (msg: string): void => { - this.quest_logger.info(`message "${msg}"`); + this.logger.info(`message "${msg}"`); }, add_msg: (msg: string): void => { - this.quest_logger.info(`add_msg "${msg}"`); + this.logger.info(`add_msg "${msg}"`); }, winend: (): void => { @@ -317,15 +317,15 @@ export class QuestRunner { }, list: (list_items: string[]): void => { - this.quest_logger.info(`list "[${list_items}]"`); + this.logger.info(`list "[${list_items}]"`); }, warning: (msg: string, inst_ptr?: InstructionPointer): void => { - this.quest_logger.warning(message_with_inst_ptr(msg, inst_ptr)); + this.logger.warn(message_with_inst_ptr(msg, inst_ptr)); }, error: (err: Error, inst_ptr?: InstructionPointer): void => { - this.quest_logger.error(message_with_inst_ptr(err.message, inst_ptr)); + this.logger.error(message_with_inst_ptr(err.message, inst_ptr)); }, }; }; @@ -339,7 +339,7 @@ export class QuestRunner { const label = this._game_state.floor_handlers.get(area_id); if (label == undefined) { - this.quest_logger.debug(`No floor handler registered for floor ${area_id}.`); + this.logger.debug(`No floor handler registered for floor ${area_id}.`); } else { this.vm.start_thread(label); this.schedule_frame(); diff --git a/src/quest_editor/controllers/QuestEditorToolBarController.test.ts b/src/quest_editor/controllers/QuestEditorToolBarController.test.ts index dbb7589e..98fa1d98 100644 --- a/src/quest_editor/controllers/QuestEditorToolBarController.test.ts +++ b/src/quest_editor/controllers/QuestEditorToolBarController.test.ts @@ -1,11 +1,9 @@ -/** - * @jest-environment jsdom - */ import { GuiStore } from "../../core/stores/GuiStore"; import { create_area_store } from "../../../test/src/quest_editor/stores/store_creation"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { QuestEditorToolBarController } from "./QuestEditorToolBarController"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { next_animation_frame } from "../../../test/src/utils"; test("Some widgets should only be enabled when a quest is loaded.", async () => { const gui_store = new GuiStore(); @@ -59,7 +57,3 @@ test("Debugging controls should be enabled and disabled at the right times.", as ctrl.stop(); } }); - -function next_animation_frame(): Promise { - return new Promise(resolve => requestAnimationFrame(() => resolve())); -} diff --git a/src/quest_editor/controllers/QuestEditorToolBarController.ts b/src/quest_editor/controllers/QuestEditorToolBarController.ts index c5b4b67d..001bfd0b 100644 --- a/src/quest_editor/controllers/QuestEditorToolBarController.ts +++ b/src/quest_editor/controllers/QuestEditorToolBarController.ts @@ -13,10 +13,10 @@ import { parse_quest, write_quest_qst } from "../../core/data_formats/parsing/qu import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; import { Endianness } from "../../core/data_formats/Endianness"; import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion"; -import Logger from "js-logger"; import { create_element } from "../../core/gui/dom"; +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("quest_editor/controllers/QuestEditorToolBarController"); +const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController"); export type AreaAndLabel = { readonly area: AreaModel; readonly label: string }; diff --git a/src/quest_editor/controllers/QuestInfoController.test.ts b/src/quest_editor/controllers/QuestInfoController.test.ts index 019f1317..3af09ab1 100644 --- a/src/quest_editor/controllers/QuestInfoController.test.ts +++ b/src/quest_editor/controllers/QuestInfoController.test.ts @@ -1,6 +1,3 @@ -/** - * @jest-environment jsdom - */ import { create_area_store, create_quest_editor_store, diff --git a/src/quest_editor/gui/LogView.css b/src/quest_editor/gui/LogView.css index 11721714..50c7e9bc 100644 --- a/src/quest_editor/gui/LogView.css +++ b/src/quest_editor/gui/LogView.css @@ -39,6 +39,6 @@ color: hsl(0, 80%, 50%); } -.quest_editor_LogView .quest_editor_LogView_Warning_message .quest_editor_LogView_message_level { +.quest_editor_LogView .quest_editor_LogView_Warn_message .quest_editor_LogView_message_level { color: hsl(30, 80%, 50%); } diff --git a/src/quest_editor/gui/LogView.ts b/src/quest_editor/gui/LogView.ts index 6b481dce..97b38e7e 100644 --- a/src/quest_editor/gui/LogView.ts +++ b/src/quest_editor/gui/LogView.ts @@ -2,8 +2,9 @@ import { bind_children_to, el } from "../../core/gui/dom"; import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ToolBar } from "../../core/gui/ToolBar"; import "./LogView.css"; -import { log_store, LogLevel, LogLevels, LogMessage } from "../stores/LogStore"; +import { log_store } from "../stores/LogStore"; import { Select } from "../../core/gui/Select"; +import { LogEntry, LogLevel, LogLevels, time_to_string } from "../../core/Logger"; const AUTOSCROLL_TRESHOLD = 5; @@ -44,18 +45,15 @@ export class LogView extends ResizableWidget { this.list_container.addEventListener("scroll", this.scrolled); this.disposables( - bind_children_to( - this.list_element, - log_store.log_messages, - this.create_message_element, - { after: this.scroll_to_bottom }, - ), + bind_children_to(this.list_element, log_store.log, this.create_message_element, { + after: this.scroll_to_bottom, + }), this.level_filter.selected.observe( - ({ value }) => value != undefined && log_store.set_log_level(value), + ({ value }) => value != undefined && log_store.set_level(value), ), - log_store.log_level.observe( + log_store.level.observe( ({ value }) => { this.level_filter.selected.val = value; }, @@ -88,25 +86,25 @@ export class LogView extends ResizableWidget { } }; - private create_message_element = (msg: LogMessage): HTMLElement => { + private create_message_element = ({ time, level, message }: LogEntry): HTMLElement => { return el.div( { class: [ "quest_editor_LogView_message", - "quest_editor_LogView_" + LogLevel[msg.level] + "_message", + "quest_editor_LogView_" + LogLevel[level] + "_message", ].join(" "), }, el.div({ class: "quest_editor_LogView_message_timestamp", - text: msg.time.toTimeString().slice(0, 8), + text: time_to_string(time), }), el.div({ class: "quest_editor_LogView_message_level", - text: "[" + LogLevel[msg.level] + "]", + text: "[" + LogLevel[level] + "]", }), el.div({ class: "quest_editor_LogView_message_contents", - text: msg.message, + text: message, }), ); }; diff --git a/src/quest_editor/gui/QuestEditorToolBar.test.ts b/src/quest_editor/gui/QuestEditorToolBar.test.ts index 8dbaafb4..4629e4ce 100644 --- a/src/quest_editor/gui/QuestEditorToolBar.test.ts +++ b/src/quest_editor/gui/QuestEditorToolBar.test.ts @@ -1,6 +1,3 @@ -/** - * @jest-environment jsdom - */ import { QuestEditorToolBarController } from "../controllers/QuestEditorToolBarController"; import { QuestEditorToolBar } from "./QuestEditorToolBar"; import { GuiStore } from "../../core/stores/GuiStore"; diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index ff8b5382..f19df0c5 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -18,9 +18,9 @@ import { LogView } from "./LogView"; import { QuestRunnerRendererView } from "./QuestRunnerRendererView"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister"; -import Logger = require("js-logger"); +import { LogManager } from "../../core/Logger"; -const logger = Logger.get("quest_editor/gui/QuestEditorView"); +const logger = LogManager.get("quest_editor/gui/QuestEditorView"); const DEFAULT_LAYOUT_CONFIG = { settings: { diff --git a/src/quest_editor/gui/QuestInfoView.test.ts b/src/quest_editor/gui/QuestInfoView.test.ts index 147ad6e0..fa6cdf5e 100644 --- a/src/quest_editor/gui/QuestInfoView.test.ts +++ b/src/quest_editor/gui/QuestInfoView.test.ts @@ -1,6 +1,3 @@ -/** - * @jest-environment jsdom - */ import { QuestInfoController } from "../controllers/QuestInfoController"; import { undo_manager } from "../../core/undo/UndoManager"; import { QuestInfoView } from "./QuestInfoView"; diff --git a/src/quest_editor/gui/__snapshots__/QuestEditorToolBar.test.ts.snap b/src/quest_editor/gui/__snapshots__/QuestEditorToolBar.test.ts.snap new file mode 100644 index 00000000..f1c491db --- /dev/null +++ b/src/quest_editor/gui/__snapshots__/QuestEditorToolBar.test.ts.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Renders correctly. 1`] = ` +
+
+ + +
+ + + + +
+ + +
+`; diff --git a/src/quest_editor/gui/__snapshots__/QuestInfoView.test.ts.snap b/src/quest_editor/gui/__snapshots__/QuestInfoView.test.ts.snap new file mode 100644 index 00000000..1221f89b --- /dev/null +++ b/src/quest_editor/gui/__snapshots__/QuestInfoView.test.ts.snap @@ -0,0 +1,218 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Renders correctly with a current quest.: should render property inputs 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ Episode: + + I +
+ ID: + + + + +
+ Name: + + + + +
+ Short description: +
+
+