@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.
Files changed (131) hide show
  1. package/.datasets/README.dataset.jsonl +12 -0
  2. package/.editorconfig +20 -0
  3. package/CONTRIBUTING.md +42 -0
  4. package/LICENSE +15 -0
  5. package/README.md +238 -0
  6. package/docs/uk/README.md +240 -0
  7. package/package.json +64 -0
  8. package/playground/User.js +52 -0
  9. package/playground/currency.exchange.js +48 -0
  10. package/playground/i18n/index.js +21 -0
  11. package/playground/i18n/uk.js +53 -0
  12. package/playground/language.form.js +25 -0
  13. package/playground/main.js +72 -0
  14. package/playground/registration.form.js +58 -0
  15. package/playground/topup.telephone.js +62 -0
  16. package/src/App/Command/Options.js +78 -0
  17. package/src/App/Command/index.js +9 -0
  18. package/src/App/Core/CoreApp.js +129 -0
  19. package/src/App/Core/UI.js +116 -0
  20. package/src/App/Core/Widget.js +67 -0
  21. package/src/App/Core/index.js +11 -0
  22. package/src/App/Scenario.js +45 -0
  23. package/src/App/User/Command/Message.js +44 -0
  24. package/src/App/User/Command/Options.js +48 -0
  25. package/src/App/User/Command/index.js +11 -0
  26. package/src/App/User/UserApp.js +73 -0
  27. package/src/App/User/UserApp.test.js +56 -0
  28. package/src/App/User/UserUI.js +20 -0
  29. package/src/App/User/UserUI.test.js +51 -0
  30. package/src/App/User/index.js +15 -0
  31. package/src/App/index.js +22 -0
  32. package/src/Component/Process/Input.js +70 -0
  33. package/src/Component/Process/Process.js +26 -0
  34. package/src/Component/Process/index.js +5 -0
  35. package/src/Component/Welcome/Input.js +50 -0
  36. package/src/Component/Welcome/Welcome.js +26 -0
  37. package/src/Component/Welcome/index.js +5 -0
  38. package/src/Component/index.js +9 -0
  39. package/src/Frame/Frame.js +591 -0
  40. package/src/Frame/Frame.test.js +429 -0
  41. package/src/Frame/Props.js +102 -0
  42. package/src/Locale.js +119 -0
  43. package/src/Model/User/User.js +56 -0
  44. package/src/Model/index.js +7 -0
  45. package/src/README.md.js +371 -0
  46. package/src/StdIn.js +111 -0
  47. package/src/StdOut.js +99 -0
  48. package/src/View/RenderOptions.js +48 -0
  49. package/src/View/View.js +289 -0
  50. package/src/View/View.test.js +77 -0
  51. package/src/core/Form/Form.js +289 -0
  52. package/src/core/Form/Form.test.js +116 -0
  53. package/src/core/Form/Input.js +116 -0
  54. package/src/core/Form/Input.test.js +58 -0
  55. package/src/core/Form/Message.js +86 -0
  56. package/src/core/Form/Message.test.js +54 -0
  57. package/src/core/Form/index.js +11 -0
  58. package/src/core/InputAdapter.js +41 -0
  59. package/src/core/InputAdapter.test.js +35 -0
  60. package/src/core/Message/InputMessage.js +119 -0
  61. package/src/core/Message/InputMessage.test.js +45 -0
  62. package/src/core/Message/Message.js +77 -0
  63. package/src/core/Message/Message.test.js +58 -0
  64. package/src/core/Message/OutputMessage.js +143 -0
  65. package/src/core/Message/OutputMessage.test.js +61 -0
  66. package/src/core/Message/index.js +7 -0
  67. package/src/core/OutputAdapter.js +50 -0
  68. package/src/core/OutputAdapter.test.js +35 -0
  69. package/src/core/Stream.js +71 -0
  70. package/src/core/Stream.test.js +78 -0
  71. package/src/core/StreamEntry.js +59 -0
  72. package/src/core/index.js +13 -0
  73. package/src/functions.js +38 -0
  74. package/src/index.js +34 -0
  75. package/src/index.test.js +14 -0
  76. package/src/models/SimpleUser.js +18 -0
  77. package/stories/App/AppView.js +15 -0
  78. package/stories/App/AppView.test.js +22 -0
  79. package/stories/App/RenderOptions.js +14 -0
  80. package/stories/nodejs/interface.test.js +27 -0
  81. package/system.md +187 -0
  82. package/system1.md +137 -0
  83. package/task.md +181 -0
  84. package/tsconfig.json +23 -0
  85. package/types/App/Command/Options.d.ts +46 -0
  86. package/types/App/Command/index.d.ts +8 -0
  87. package/types/App/Core/CoreApp.d.ts +70 -0
  88. package/types/App/Core/UI.d.ts +49 -0
  89. package/types/App/Core/Widget.d.ts +40 -0
  90. package/types/App/Core/index.d.ts +10 -0
  91. package/types/App/Scenario.d.ts +26 -0
  92. package/types/App/User/Command/Message.d.ts +30 -0
  93. package/types/App/User/Command/Options.d.ts +27 -0
  94. package/types/App/User/Command/index.d.ts +8 -0
  95. package/types/App/User/UserApp.d.ts +31 -0
  96. package/types/App/User/UserUI.d.ts +18 -0
  97. package/types/App/User/index.d.ts +12 -0
  98. package/types/App/index.d.ts +14 -0
  99. package/types/Component/Process/Input.d.ts +48 -0
  100. package/types/Component/Process/Process.d.ts +13 -0
  101. package/types/Component/Process/index.d.ts +4 -0
  102. package/types/Component/Welcome/Input.d.ts +34 -0
  103. package/types/Component/Welcome/Welcome.d.ts +13 -0
  104. package/types/Component/Welcome/index.d.ts +4 -0
  105. package/types/Component/index.d.ts +8 -0
  106. package/types/Frame/Frame.d.ts +186 -0
  107. package/types/Frame/Props.d.ts +77 -0
  108. package/types/Locale.d.ts +55 -0
  109. package/types/Model/User/User.d.ts +36 -0
  110. package/types/Model/index.d.ts +6 -0
  111. package/types/StdIn.d.ts +62 -0
  112. package/types/StdOut.d.ts +52 -0
  113. package/types/View/RenderOptions.d.ts +29 -0
  114. package/types/View/View.d.ts +115 -0
  115. package/types/core/Form/Form.d.ts +123 -0
  116. package/types/core/Form/Input.d.ts +69 -0
  117. package/types/core/Form/Message.d.ts +28 -0
  118. package/types/core/Form/index.d.ts +5 -0
  119. package/types/core/InputAdapter.d.ts +28 -0
  120. package/types/core/Message/InputMessage.d.ts +71 -0
  121. package/types/core/Message/Message.d.ts +50 -0
  122. package/types/core/Message/OutputMessage.d.ts +53 -0
  123. package/types/core/Message/index.d.ts +5 -0
  124. package/types/core/OutputAdapter.d.ts +33 -0
  125. package/types/core/Stream.d.ts +27 -0
  126. package/types/core/StreamEntry.d.ts +45 -0
  127. package/types/core/index.d.ts +9 -0
  128. package/types/functions.d.ts +3 -0
  129. package/types/index.d.ts +20 -0
  130. package/types/models/SimpleUser.d.ts +21 -0
  131. 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,11 @@
1
+ import CoreApp from "./CoreApp.js"
2
+ import UI from "./UI.js"
3
+ import Widget from "./Widget.js"
4
+
5
+ export { CoreApp, UI }
6
+
7
+ export default {
8
+ App: CoreApp,
9
+ UI,
10
+ Widget,
11
+ }
@@ -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
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ import Process from "./Process.js"
2
+ import ProcessInput from "./Input.js"
3
+
4
+ export default Process
5
+ export { ProcessInput }