@nan0web/ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.datasets/README.dataset.jsonl +12 -0
- package/.editorconfig +20 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +15 -0
- package/README.md +238 -0
- package/docs/uk/README.md +240 -0
- package/package.json +64 -0
- package/playground/User.js +52 -0
- package/playground/currency.exchange.js +48 -0
- package/playground/i18n/index.js +21 -0
- package/playground/i18n/uk.js +53 -0
- package/playground/language.form.js +25 -0
- package/playground/main.js +72 -0
- package/playground/registration.form.js +58 -0
- package/playground/topup.telephone.js +62 -0
- package/src/App/Command/Options.js +78 -0
- package/src/App/Command/index.js +9 -0
- package/src/App/Core/CoreApp.js +129 -0
- package/src/App/Core/UI.js +116 -0
- package/src/App/Core/Widget.js +67 -0
- package/src/App/Core/index.js +11 -0
- package/src/App/Scenario.js +45 -0
- package/src/App/User/Command/Message.js +44 -0
- package/src/App/User/Command/Options.js +48 -0
- package/src/App/User/Command/index.js +11 -0
- package/src/App/User/UserApp.js +73 -0
- package/src/App/User/UserApp.test.js +56 -0
- package/src/App/User/UserUI.js +20 -0
- package/src/App/User/UserUI.test.js +51 -0
- package/src/App/User/index.js +15 -0
- package/src/App/index.js +22 -0
- package/src/Component/Process/Input.js +70 -0
- package/src/Component/Process/Process.js +26 -0
- package/src/Component/Process/index.js +5 -0
- package/src/Component/Welcome/Input.js +50 -0
- package/src/Component/Welcome/Welcome.js +26 -0
- package/src/Component/Welcome/index.js +5 -0
- package/src/Component/index.js +9 -0
- package/src/Frame/Frame.js +591 -0
- package/src/Frame/Frame.test.js +429 -0
- package/src/Frame/Props.js +102 -0
- package/src/Locale.js +119 -0
- package/src/Model/User/User.js +56 -0
- package/src/Model/index.js +7 -0
- package/src/README.md.js +371 -0
- package/src/StdIn.js +111 -0
- package/src/StdOut.js +99 -0
- package/src/View/RenderOptions.js +48 -0
- package/src/View/View.js +289 -0
- package/src/View/View.test.js +77 -0
- package/src/core/Form/Form.js +289 -0
- package/src/core/Form/Form.test.js +116 -0
- package/src/core/Form/Input.js +116 -0
- package/src/core/Form/Input.test.js +58 -0
- package/src/core/Form/Message.js +86 -0
- package/src/core/Form/Message.test.js +54 -0
- package/src/core/Form/index.js +11 -0
- package/src/core/InputAdapter.js +41 -0
- package/src/core/InputAdapter.test.js +35 -0
- package/src/core/Message/InputMessage.js +119 -0
- package/src/core/Message/InputMessage.test.js +45 -0
- package/src/core/Message/Message.js +77 -0
- package/src/core/Message/Message.test.js +58 -0
- package/src/core/Message/OutputMessage.js +143 -0
- package/src/core/Message/OutputMessage.test.js +61 -0
- package/src/core/Message/index.js +7 -0
- package/src/core/OutputAdapter.js +50 -0
- package/src/core/OutputAdapter.test.js +35 -0
- package/src/core/Stream.js +71 -0
- package/src/core/Stream.test.js +78 -0
- package/src/core/StreamEntry.js +59 -0
- package/src/core/index.js +13 -0
- package/src/functions.js +38 -0
- package/src/index.js +34 -0
- package/src/index.test.js +14 -0
- package/src/models/SimpleUser.js +18 -0
- package/stories/App/AppView.js +15 -0
- package/stories/App/AppView.test.js +22 -0
- package/stories/App/RenderOptions.js +14 -0
- package/stories/nodejs/interface.test.js +27 -0
- package/system.md +187 -0
- package/system1.md +137 -0
- package/task.md +181 -0
- package/tsconfig.json +23 -0
- package/types/App/Command/Options.d.ts +46 -0
- package/types/App/Command/index.d.ts +8 -0
- package/types/App/Core/CoreApp.d.ts +70 -0
- package/types/App/Core/UI.d.ts +49 -0
- package/types/App/Core/Widget.d.ts +40 -0
- package/types/App/Core/index.d.ts +10 -0
- package/types/App/Scenario.d.ts +26 -0
- package/types/App/User/Command/Message.d.ts +30 -0
- package/types/App/User/Command/Options.d.ts +27 -0
- package/types/App/User/Command/index.d.ts +8 -0
- package/types/App/User/UserApp.d.ts +31 -0
- package/types/App/User/UserUI.d.ts +18 -0
- package/types/App/User/index.d.ts +12 -0
- package/types/App/index.d.ts +14 -0
- package/types/Component/Process/Input.d.ts +48 -0
- package/types/Component/Process/Process.d.ts +13 -0
- package/types/Component/Process/index.d.ts +4 -0
- package/types/Component/Welcome/Input.d.ts +34 -0
- package/types/Component/Welcome/Welcome.d.ts +13 -0
- package/types/Component/Welcome/index.d.ts +4 -0
- package/types/Component/index.d.ts +8 -0
- package/types/Frame/Frame.d.ts +186 -0
- package/types/Frame/Props.d.ts +77 -0
- package/types/Locale.d.ts +55 -0
- package/types/Model/User/User.d.ts +36 -0
- package/types/Model/index.d.ts +6 -0
- package/types/StdIn.d.ts +62 -0
- package/types/StdOut.d.ts +52 -0
- package/types/View/RenderOptions.d.ts +29 -0
- package/types/View/View.d.ts +115 -0
- package/types/core/Form/Form.d.ts +123 -0
- package/types/core/Form/Input.d.ts +69 -0
- package/types/core/Form/Message.d.ts +28 -0
- package/types/core/Form/index.d.ts +5 -0
- package/types/core/InputAdapter.d.ts +28 -0
- package/types/core/Message/InputMessage.d.ts +71 -0
- package/types/core/Message/Message.d.ts +50 -0
- package/types/core/Message/OutputMessage.d.ts +53 -0
- package/types/core/Message/index.d.ts +5 -0
- package/types/core/OutputAdapter.d.ts +33 -0
- package/types/core/Stream.d.ts +27 -0
- package/types/core/StreamEntry.d.ts +45 -0
- package/types/core/index.d.ts +9 -0
- package/types/functions.d.ts +3 -0
- package/types/index.d.ts +20 -0
- package/types/models/SimpleUser.d.ts +21 -0
- package/vitest.config.js +26 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import View from "../../View/View.js"
|
|
2
|
+
import CoreApp from "./CoreApp.js"
|
|
3
|
+
import Widget from "./Widget.js"
|
|
4
|
+
import { notEmpty } from "@nan0web/types"
|
|
5
|
+
import { CommandMessage } from "../Command/index.js"
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("../../View/View.js").ComponentFn} ComponentFn */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Abstract UI class to connect apps and widgets.
|
|
11
|
+
* Supports input/output data typed classes and views.
|
|
12
|
+
*/
|
|
13
|
+
class UI extends Widget {
|
|
14
|
+
/** @type {CoreApp} The app instance connected to this UI */
|
|
15
|
+
app
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new UI instance.
|
|
19
|
+
* @param {CoreApp} app - The app to connect to this UI
|
|
20
|
+
* @param {View} [view] - View instance for rendering (default: new View())
|
|
21
|
+
*/
|
|
22
|
+
constructor(app, view = new View()) {
|
|
23
|
+
super(view)
|
|
24
|
+
this.app = app
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert raw input to CommandMessage array.
|
|
29
|
+
* Must be implemented by subclasses.
|
|
30
|
+
* @param {any} rawInput - Raw input to convert
|
|
31
|
+
* @returns {CommandMessage[]} Array of command messages
|
|
32
|
+
* @throws {Error} Always thrown as this method must be implemented by subclasses
|
|
33
|
+
*/
|
|
34
|
+
convertInput(rawInput) {
|
|
35
|
+
throw new Error("convertInput must be implemented by subclass")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Process input, run commands on app, and output results.
|
|
40
|
+
* Supports progress callback.
|
|
41
|
+
* @emits {start} Emitted when processing begins
|
|
42
|
+
* @emits {data} Emitted for each command being processed
|
|
43
|
+
* @emits {end} Emitted when all commands have been processed
|
|
44
|
+
* @param {any} rawInput - Raw input to process
|
|
45
|
+
* @returns {Promise<any[]>} Results of command processing
|
|
46
|
+
*/
|
|
47
|
+
async process(rawInput) {
|
|
48
|
+
const commands = this.convertInput(rawInput).filter(notEmpty)
|
|
49
|
+
const results = []
|
|
50
|
+
let count = commands.length
|
|
51
|
+
|
|
52
|
+
const proc = this.view.get("UIProcess")
|
|
53
|
+
if (proc) {
|
|
54
|
+
this.show(proc)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (0 === count && this.app.selectCommand) {
|
|
58
|
+
const answer = await this.app.selectCommand(this)
|
|
59
|
+
if (answer) {
|
|
60
|
+
const [input] = this.convertInput(rawInput)
|
|
61
|
+
const cmd = new CommandMessage({
|
|
62
|
+
argv: [answer],
|
|
63
|
+
opts: input?.opts ?? {},
|
|
64
|
+
})
|
|
65
|
+
commands.push(cmd)
|
|
66
|
+
++count
|
|
67
|
+
} else {
|
|
68
|
+
this.emit("end", { commands, results })
|
|
69
|
+
return results
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.emit("start", { commands })
|
|
73
|
+
for (let i = 0; i < count; i++) {
|
|
74
|
+
const command = commands[i]
|
|
75
|
+
this.emit("data", { i, count, command })
|
|
76
|
+
const result = await this.app.processCommand(command, this)
|
|
77
|
+
results.push(result)
|
|
78
|
+
}
|
|
79
|
+
this.emit("end", { commands, results })
|
|
80
|
+
// this.output(results)
|
|
81
|
+
return results
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets up event handlers for UI process events.
|
|
86
|
+
* @param {ComponentFn} UIProcess - Process view component
|
|
87
|
+
*/
|
|
88
|
+
show(UIProcess) {
|
|
89
|
+
if (!UIProcess) return
|
|
90
|
+
const onStart = () => {
|
|
91
|
+
// this.view.render(UIProcess)
|
|
92
|
+
}
|
|
93
|
+
const onData = () => {
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
const onEnd = () => {
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.on("start", onStart)
|
|
101
|
+
this.on("data", onData)
|
|
102
|
+
this.on("end", onEnd)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Output results to the interface.
|
|
107
|
+
* @param {any[]} results - Results to output
|
|
108
|
+
*/
|
|
109
|
+
output(results) {
|
|
110
|
+
results.forEach(result => {
|
|
111
|
+
this.view.info(JSON.stringify(result))
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default UI
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import EventProcessor from "@nan0web/event/oop"
|
|
2
|
+
import View from "../../View/View.js"
|
|
3
|
+
import InputMessage from "../../core/Message/InputMessage.js"
|
|
4
|
+
import { StreamEntry } from "@nan0web/db"
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("./UI.js").ComponentFn} ComponentFn */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Abstract Widget class.
|
|
10
|
+
* Widget is a view with ability to input data in a specific format.
|
|
11
|
+
* Input and output data are typed classes.
|
|
12
|
+
*/
|
|
13
|
+
class Widget extends EventProcessor {
|
|
14
|
+
/** @type {View} The view associated with this widget */
|
|
15
|
+
view
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new Widget instance.
|
|
19
|
+
* @param {View} [view] - View instance (default: new View())
|
|
20
|
+
*/
|
|
21
|
+
constructor(view = new View()) {
|
|
22
|
+
super()
|
|
23
|
+
this.view = view
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ask user for input data of specific class.
|
|
28
|
+
* @param {InputMessage} input - instance of InputMessage or similar
|
|
29
|
+
* @returns {Promise<InputMessage | null>} instance of InputMessage or null
|
|
30
|
+
*/
|
|
31
|
+
async ask(input) {
|
|
32
|
+
return await this.view.ask(input)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {AsyncGenerator<StreamEntry>} stream
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
async read(stream) {
|
|
40
|
+
for await (const entry of stream) {
|
|
41
|
+
this.view.progress(true)(entry)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render output data using a view function.
|
|
47
|
+
* @param {Function|string} viewFnOrName - View function or registered view name
|
|
48
|
+
* @param {object} outputData - Typed output data instance
|
|
49
|
+
* @returns {any} Rendered output
|
|
50
|
+
* @throws {Error} If view component is not found when using string name
|
|
51
|
+
*/
|
|
52
|
+
render(viewFnOrName, outputData) {
|
|
53
|
+
/** @type {Function | ComponentFn | undefined} */
|
|
54
|
+
const viewFn = typeof viewFnOrName === "string"
|
|
55
|
+
? this.view.get(viewFnOrName)
|
|
56
|
+
: viewFnOrName
|
|
57
|
+
|
|
58
|
+
if (!viewFn) {
|
|
59
|
+
throw new Error([
|
|
60
|
+
"View component not found", ": ", viewFnOrName
|
|
61
|
+
].join(""))
|
|
62
|
+
}
|
|
63
|
+
return this.view.render(viewFn)(outputData)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default Widget
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import App from "./index.js"
|
|
2
|
+
import UI from "./Core/UI.js"
|
|
3
|
+
|
|
4
|
+
/** @typedef {import("./Core/CoreApp.js").default} CoreApp */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Abstract Scenario class to test app logic.
|
|
8
|
+
* Scenarios run input commands and verify output.
|
|
9
|
+
*/
|
|
10
|
+
export default class Scenario {
|
|
11
|
+
/** @type {CoreApp} The app to run scenarios against */
|
|
12
|
+
app
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new Scenario instance.
|
|
16
|
+
* @param {CoreApp} app - App instance to run scenarios against
|
|
17
|
+
* @param {UI} ui - User interface
|
|
18
|
+
* @throws {TypeError} If app is not an App.Core.App instance
|
|
19
|
+
*/
|
|
20
|
+
constructor(app, ui) {
|
|
21
|
+
if (!(app instanceof App.Core.App)) {
|
|
22
|
+
throw new TypeError("Scenario requires a App.Core.App instance")
|
|
23
|
+
}
|
|
24
|
+
this.app = app
|
|
25
|
+
this.ui = ui
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run scenario with input commands and expected output.
|
|
30
|
+
* @param {Array<any[]>} inputCommands - Array of command arrays
|
|
31
|
+
* @param {Array<any>} expectedOutputs - Expected outputs for each command
|
|
32
|
+
* @returns {Promise<boolean>} True if all outputs match expected
|
|
33
|
+
*/
|
|
34
|
+
async run(inputCommands, expectedOutputs) {
|
|
35
|
+
const commandMessages = inputCommands.map(arr => App.Command.Message.parse(arr))
|
|
36
|
+
const outputs = await this.app.processCommands(commandMessages, this.ui)
|
|
37
|
+
if (outputs.length !== expectedOutputs.length) return false
|
|
38
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
39
|
+
if (JSON.stringify(outputs[i]) !== JSON.stringify(expectedOutputs[i])) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CommandMessage } from "../../Command/index.js"
|
|
2
|
+
import UserAppCommandOptions from "./Options.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extends Command.Message to include user-specific command options.
|
|
6
|
+
*/
|
|
7
|
+
class UserAppCommandMessage extends CommandMessage {
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new UserAppCommandMessage instance.
|
|
10
|
+
* @param {object} props - Command message properties
|
|
11
|
+
* @param {string[]} [props.args=[]] - Command arguments
|
|
12
|
+
* @param {Partial<UserAppCommandOptions>} [props.opts={}] - User-specific options
|
|
13
|
+
*/
|
|
14
|
+
constructor(props = {}) {
|
|
15
|
+
super(props)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @returns {UserAppCommandOptions} */
|
|
19
|
+
get opts() {
|
|
20
|
+
return UserAppCommandOptions.from(super.opts)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Partial<UserAppCommandOptions>} value
|
|
25
|
+
*/
|
|
26
|
+
set opts(value) {
|
|
27
|
+
super.opts = UserAppCommandOptions.from(value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parses an array of strings into a UserAppCommandMessage.
|
|
32
|
+
* @param {string[] | string} value - Arguments to parse
|
|
33
|
+
* @returns {UserAppCommandMessage} Parsed command message
|
|
34
|
+
*/
|
|
35
|
+
static parse(value = []) {
|
|
36
|
+
if ("string" === typeof value) {
|
|
37
|
+
value = value.split(" ")
|
|
38
|
+
}
|
|
39
|
+
const result = super.parse(value)
|
|
40
|
+
return new this(result)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default UserAppCommandMessage
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import CommandOptions from "../../Command/Options.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extends CommandOptions to include user-specific options.
|
|
5
|
+
*/
|
|
6
|
+
class UserAppCommandOptions extends CommandOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Default option values including inherited ones.
|
|
9
|
+
* @type {object}
|
|
10
|
+
* @property {boolean} help - Whether help is requested
|
|
11
|
+
* @property {string} cwd - Current working directory
|
|
12
|
+
* @property {string} user - User name
|
|
13
|
+
*/
|
|
14
|
+
static DEFAULTS = {
|
|
15
|
+
...CommandOptions.DEFAULTS,
|
|
16
|
+
user: "",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @type {string} User name */
|
|
20
|
+
user
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new UserAppCommandOptions instance.
|
|
24
|
+
* @param {object} props - Options properties
|
|
25
|
+
* @param {boolean} [props.help=false] - Whether help is requested
|
|
26
|
+
* @param {string} [props.cwd=""] - Current working directory
|
|
27
|
+
* @param {string} [props.user=""] - User name
|
|
28
|
+
*/
|
|
29
|
+
constructor(props = {}) {
|
|
30
|
+
const {
|
|
31
|
+
user = UserAppCommandOptions.DEFAULTS.user,
|
|
32
|
+
} = props
|
|
33
|
+
super(props)
|
|
34
|
+
this.user = String(user)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a UserAppCommandOptions instance from the given props.
|
|
39
|
+
* @param {UserAppCommandOptions|object} props - The properties to create from
|
|
40
|
+
* @returns {UserAppCommandOptions} A UserAppCommandOptions instance
|
|
41
|
+
*/
|
|
42
|
+
static from(props) {
|
|
43
|
+
if (props instanceof UserAppCommandOptions) return props
|
|
44
|
+
return new this(props)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default UserAppCommandOptions
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Command from "../../Command/index.js"
|
|
2
|
+
import CommandMessage from "./Message.js"
|
|
3
|
+
import CommandOptions from "./Options.js"
|
|
4
|
+
|
|
5
|
+
export { CommandMessage, CommandOptions }
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
...Command,
|
|
9
|
+
Message: CommandMessage,
|
|
10
|
+
Options: CommandOptions,
|
|
11
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { notEmpty } from "@nan0web/types"
|
|
2
|
+
import CoreApp from "../Core/CoreApp.js"
|
|
3
|
+
import Command, { CommandMessage } from "./Command/index.js"
|
|
4
|
+
import User from "../../Model/User/User.js"
|
|
5
|
+
import UserUI from "./UserUI.js"
|
|
6
|
+
import InputMessage from "../../core/Message/InputMessage.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* UserApp requires user name and shows Welcome view.
|
|
10
|
+
* If user.name is provided in command input, ignores user input.
|
|
11
|
+
* User can change user data to see another Welcome view.
|
|
12
|
+
*/
|
|
13
|
+
class UserApp extends CoreApp {
|
|
14
|
+
/** @type {CommandMessage} Starting command parsed from argv */
|
|
15
|
+
startCommand
|
|
16
|
+
|
|
17
|
+
/** @type {object} App state */
|
|
18
|
+
state
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new UserApp instance.
|
|
22
|
+
* @param {object} props - UserApp properties
|
|
23
|
+
* @param {string} [props.name="UserApp"] - App name
|
|
24
|
+
* @param {object} [props.state={}] - Initial state object
|
|
25
|
+
* @param {string[]} [props.argv=[]] - Command line arguments to parse
|
|
26
|
+
*/
|
|
27
|
+
constructor(props = {}) {
|
|
28
|
+
super(props)
|
|
29
|
+
const {
|
|
30
|
+
argv = [],
|
|
31
|
+
state = {},
|
|
32
|
+
} = props
|
|
33
|
+
this.state = state
|
|
34
|
+
this.startCommand = CommandMessage.parse(argv)
|
|
35
|
+
this.registerCommand("setUser", this.setUser.bind(this))
|
|
36
|
+
this.registerCommand("welcome", this.welcome.bind(this))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set user data from params.
|
|
41
|
+
* @param {CommandMessage} cmd - Command message with user data
|
|
42
|
+
* @param {UserUI} ui - UI instance
|
|
43
|
+
* @returns {Promise<{ message: string }>} Welcome message
|
|
44
|
+
*/
|
|
45
|
+
async setUser(cmd, ui) {
|
|
46
|
+
this.state.user = User.from(cmd.opts.user)
|
|
47
|
+
const frame = await this.welcome(cmd, ui)
|
|
48
|
+
return {
|
|
49
|
+
message: String(frame)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Show welcome message for current user.
|
|
55
|
+
* @param {CommandMessage} cmd - Command message
|
|
56
|
+
* @param {UserUI} ui - UI instance
|
|
57
|
+
* @returns {Promise<string[][]>} Welcome view output
|
|
58
|
+
*/
|
|
59
|
+
async welcome(cmd, ui) {
|
|
60
|
+
if (cmd.opts.user) {
|
|
61
|
+
const user = User.from(cmd.opts.user)
|
|
62
|
+
return ui.render("Welcome", { user })
|
|
63
|
+
}
|
|
64
|
+
if (notEmpty(this.user)) {
|
|
65
|
+
return ui.render("Welcome", { user: this.user })
|
|
66
|
+
}
|
|
67
|
+
const answer = await ui.ask(InputMessage.from("What is your name?"))
|
|
68
|
+
this.user = User.from(answer?.value)
|
|
69
|
+
return ui.render("Welcome", { user: this.user })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default UserApp
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import UserApp from "./UserApp.js"
|
|
4
|
+
import View from "../../View/View.js"
|
|
5
|
+
import UserUI from "./UserUI.js"
|
|
6
|
+
import Command from "./Command/index.js"
|
|
7
|
+
import Frame from "../../Frame/Frame.js"
|
|
8
|
+
|
|
9
|
+
describe("UserApp", () => {
|
|
10
|
+
/** @type {UserApp} */
|
|
11
|
+
let app
|
|
12
|
+
/** @type {View} */
|
|
13
|
+
let view
|
|
14
|
+
/** @type {UserUI} */
|
|
15
|
+
let ui
|
|
16
|
+
|
|
17
|
+
it("should set user and welcome", async () => {
|
|
18
|
+
app = new UserApp()
|
|
19
|
+
view = new View()
|
|
20
|
+
view.register("Welcome", (input) => {
|
|
21
|
+
return ["Welcome " + input.user.name]
|
|
22
|
+
})
|
|
23
|
+
ui = new UserUI(app, view)
|
|
24
|
+
const cmd = Command.Message.parse("setUser --user Alice")
|
|
25
|
+
const result = await app.processCommand(cmd, ui)
|
|
26
|
+
assert.equal(String(result.message), Frame.CLEAR_LINE + "\r" + "Welcome Alice")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("should welcome with user", async () => {
|
|
30
|
+
app = new UserApp()
|
|
31
|
+
view = new View()
|
|
32
|
+
view.register("Welcome", (input) => {
|
|
33
|
+
return ["Welcome " + input.user.name]
|
|
34
|
+
})
|
|
35
|
+
ui = new UserUI(app, view)
|
|
36
|
+
const cmd = Command.Message.parse("welcome --user YaRa")
|
|
37
|
+
const result = await app.processCommand(cmd, ui)
|
|
38
|
+
assert.equal(String(result), Frame.CLEAR_LINE + "\r" + "Welcome YaRa")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should ask for a user name and welcome with user", async () => {
|
|
42
|
+
app = new UserApp()
|
|
43
|
+
view = new View()
|
|
44
|
+
view.register("Welcome", (input) => {
|
|
45
|
+
return ["Welcome " + input.user.name]
|
|
46
|
+
})
|
|
47
|
+
ui = new UserUI(app, view)
|
|
48
|
+
const cmd = Command.Message.parse("welcome")
|
|
49
|
+
|
|
50
|
+
// Mock the stdin to provide input immediately
|
|
51
|
+
view.stdin.write("Alice\n")
|
|
52
|
+
|
|
53
|
+
const result = await app.processCommand(cmd, ui)
|
|
54
|
+
assert.equal(String(result), Frame.CLEAR_LINE + "\r" + "Welcome Alice\n")
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import App from "../Core/index.js"
|
|
2
|
+
import { CommandMessage } from "./Command/index.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UserUI connects UserApp and View.
|
|
6
|
+
* It asks user for name if not provided in command input.
|
|
7
|
+
* Allows user to change user data to see another Welcome view.
|
|
8
|
+
*/
|
|
9
|
+
export default class UserUI extends App.UI {
|
|
10
|
+
/**
|
|
11
|
+
* Convert raw input to CommandMessage array.
|
|
12
|
+
* If user.name provided in rawInput, use it directly.
|
|
13
|
+
* Otherwise ask user for name.
|
|
14
|
+
* @param {any} rawInput - Raw input to convert
|
|
15
|
+
* @returns {CommandMessage[]} Array of command messages
|
|
16
|
+
*/
|
|
17
|
+
convertInput(rawInput) {
|
|
18
|
+
return [CommandMessage.parse(rawInput)]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import UserApp from "./UserApp.js"
|
|
4
|
+
import UserUI from "./UserUI.js"
|
|
5
|
+
import View from "../../View/View.js"
|
|
6
|
+
import InputMessage from "../../core/Message/InputMessage.js"
|
|
7
|
+
import Welcome from "../../Component/Welcome/index.js"
|
|
8
|
+
|
|
9
|
+
describe("UserUI", () => {
|
|
10
|
+
it("should convert input with user.name to commands", () => {
|
|
11
|
+
const app = new UserApp()
|
|
12
|
+
const view = new View()
|
|
13
|
+
const ui = new UserUI(app, view)
|
|
14
|
+
|
|
15
|
+
const commands = ui.convertInput("setUser --user Bob welcome")
|
|
16
|
+
assert.equal(commands.length, 1)
|
|
17
|
+
assert.equal(commands[0].args[0], "setUser")
|
|
18
|
+
assert.equal(commands[0].args[1], "welcome")
|
|
19
|
+
assert.equal(commands[0].opts.user, "Bob")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("should convert input without user.name to askUserName command", () => {
|
|
23
|
+
const app = new UserApp()
|
|
24
|
+
const view = new View()
|
|
25
|
+
const ui = new UserUI(app, view)
|
|
26
|
+
|
|
27
|
+
const commands = ui.convertInput("")
|
|
28
|
+
assert.equal(commands.length, 1)
|
|
29
|
+
assert.equal(commands[0].args.length, 0)
|
|
30
|
+
assert.equal(commands[0].opts.user, "")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("should process askUserName command interactively", async () => {
|
|
34
|
+
const app = new UserApp()
|
|
35
|
+
const view = new View()
|
|
36
|
+
const ui = new UserUI(app, view)
|
|
37
|
+
view.register("Welcome", Welcome)
|
|
38
|
+
|
|
39
|
+
// Mock view.ask to return a name
|
|
40
|
+
view.ask = (input) => Promise.resolve(new InputMessage("Charlie"))
|
|
41
|
+
// Mock output to collect outputs
|
|
42
|
+
const outputs = []
|
|
43
|
+
ui.output = (results) => outputs.push(...results)
|
|
44
|
+
|
|
45
|
+
const results = await ui.process(["welcome"])
|
|
46
|
+
assert.ok(view.ask)
|
|
47
|
+
assert.equal(results.length, 1)
|
|
48
|
+
assert.ok(results[0].value[0][0].includes("Welcome"))
|
|
49
|
+
assert.ok(view.stdout.stream[0].includes("Welcome Charlie!"))
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import UserApp from "./UserApp.js"
|
|
2
|
+
import UserUI from "./UserUI.js"
|
|
3
|
+
import UserCommand from "./Command/index.js"
|
|
4
|
+
import Command from "../Command/index.js"
|
|
5
|
+
|
|
6
|
+
export { UserApp, UserUI }
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
App: UserApp,
|
|
10
|
+
UI: UserUI,
|
|
11
|
+
Command: {
|
|
12
|
+
...Command,
|
|
13
|
+
...UserCommand,
|
|
14
|
+
},
|
|
15
|
+
}
|
package/src/App/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Command from "./Command/index.js"
|
|
2
|
+
import Scenario from "./Scenario.js"
|
|
3
|
+
import UI from "./Core/UI.js"
|
|
4
|
+
|
|
5
|
+
import Core from "./Core/index.js"
|
|
6
|
+
import User from "./User/index.js"
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
Core,
|
|
10
|
+
User,
|
|
11
|
+
Command,
|
|
12
|
+
Scenario,
|
|
13
|
+
UI,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
Core,
|
|
18
|
+
User,
|
|
19
|
+
Command,
|
|
20
|
+
Scenario,
|
|
21
|
+
UI,
|
|
22
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents input data for the Process component.
|
|
3
|
+
* Holds configuration for rendering a progress bar.
|
|
4
|
+
*/
|
|
5
|
+
class ProcessInput {
|
|
6
|
+
/** @type {string} Process name to display */
|
|
7
|
+
name
|
|
8
|
+
|
|
9
|
+
/** @type {number} Current progress index */
|
|
10
|
+
i
|
|
11
|
+
|
|
12
|
+
/** @type {number} Top limit for progress normalization */
|
|
13
|
+
top
|
|
14
|
+
|
|
15
|
+
/** @type {number} Width of the progress bar */
|
|
16
|
+
width
|
|
17
|
+
|
|
18
|
+
/** @type {string} Character to use for empty space */
|
|
19
|
+
space
|
|
20
|
+
|
|
21
|
+
/** @type {string} Character to use for filled progress */
|
|
22
|
+
char
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new ProcessInput instance.
|
|
26
|
+
* @param {object} props - Process input properties
|
|
27
|
+
* @param {string} [props.name="NaN•Coding"] - Process name
|
|
28
|
+
* @param {number} [props.i=0] - Current progress index
|
|
29
|
+
* @param {number} [props.top=9] - Top limit for progress normalization
|
|
30
|
+
* @param {number} [props.width=9] - Width of the progress bar
|
|
31
|
+
* @param {string} [props.space='•'] - Character for empty space
|
|
32
|
+
* @param {string} [props.char='*'] - Character for filled progress
|
|
33
|
+
*/
|
|
34
|
+
constructor(props = {}) {
|
|
35
|
+
const {
|
|
36
|
+
name = "NaN•Coding",
|
|
37
|
+
i = 0,
|
|
38
|
+
top = 9,
|
|
39
|
+
width = 9,
|
|
40
|
+
space = '•',
|
|
41
|
+
char = '*'
|
|
42
|
+
} = props
|
|
43
|
+
this.name = name
|
|
44
|
+
this.i = i
|
|
45
|
+
this.top = top
|
|
46
|
+
this.width = width
|
|
47
|
+
this.space = space
|
|
48
|
+
this.char = char
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts the input to a string representation.
|
|
53
|
+
* @returns {string} String representation of the ProcessInput
|
|
54
|
+
*/
|
|
55
|
+
toString() {
|
|
56
|
+
return `ProcessInput(name=${this.name}, i=${this.i}, top=${this.top}, width=${this.width}, space=${this.space}, char=${this.char})`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a ProcessInput instance from the given props.
|
|
61
|
+
* @param {ProcessInput|object} props - The properties to create from
|
|
62
|
+
* @returns {ProcessInput} A ProcessInput instance
|
|
63
|
+
*/
|
|
64
|
+
static from(props = {}) {
|
|
65
|
+
if (props instanceof ProcessInput) return props
|
|
66
|
+
return new ProcessInput(props)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default ProcessInput
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import ProcessInput from "./Input.js"
|
|
2
|
+
import View from "../../View/View.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders a progress bar based on input configuration.
|
|
6
|
+
* @this {View}
|
|
7
|
+
* @param {ProcessInput|object} props - Process component properties
|
|
8
|
+
* @returns {string[][]} Rendered progress bar as array of strings
|
|
9
|
+
*/
|
|
10
|
+
function Process(props = {}) {
|
|
11
|
+
const input = ProcessInput.from(props)
|
|
12
|
+
const valid = input.top || 1
|
|
13
|
+
const per = (input.i > valid ? valid : input.i) / valid
|
|
14
|
+
const done = per * input.width
|
|
15
|
+
const bar = input.char.repeat(done) + input.space.repeat(input.width - done)
|
|
16
|
+
// Provide empty options object to satisfy Locale.format signature
|
|
17
|
+
const format = this.locale.format(Number, {})
|
|
18
|
+
const num = format ? format(100 * per) : 100 * per
|
|
19
|
+
return [
|
|
20
|
+
[`I am ${input.name} ${bar} ${num}`]
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Process.Input = ProcessInput
|
|
25
|
+
|
|
26
|
+
export default Process
|