diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 79e288f..fc7dcb2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,10 +1,9 @@ name: CI on: - push: - branches: [main] pull_request: - branches: [main] + branches: + - main jobs: build-and-test: @@ -26,4 +25,4 @@ jobs: run: npm run build - name: Run tests (local Node.js + mocks) - run: npm test \ No newline at end of file + run: npm test \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..314dd3c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.preferences.tsconfigPath": "tsconfig.test.json" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f52f61..2a06a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [2.1.0] – 2025-06-26 -## [Unreleased] +### Added +- **`ExcelAppender` color map support**: The `getInstance` method now accepts a color map, allowing users to specify custom font colors for different log levels. The default color map is exposed as a public property, enabling users to reference or modify it for their own configurations. +- **Additional documentation**: Added `git-basics.md`, providing concise, project-specific instructions and best practices for using Git effectively within this repository. + +### Changed +- Ensured cross-platform compatibility: Adjustments were made to guarantee the framework works in both Node.js/TypeScript and Office Scripts environments. To pass all tests, `setTimeout` was used in some cases to handle the asynchronous nature of Office Scripts. +- **VSCode configuration improvements**: The project now uses a dedicated `types` folder to include all `*.d.ts` files, such as Office Scripts declarations and global variable definitions. This streamlines type management and ensures accurate IntelliSense and type checking within VSCode. --- ## [2.0.0] – 2025-06-19 ### Added -- **AssertionError**: Introduced a new error class for clearer assertion failure reporting in the unit test framework (`unit-test-framework.ts`). +- **`AssertionError**`:** Introduced a new error class for clearer assertion failure reporting in the unit test framework (`unit-test-framework.ts`). - **Assert enhancements**: Added new convenience methods to the `Assert` class, making test writing more robust and expressive (`unit-test-framework.ts`). - **New interfaces and types**: Introduced `Logger`, `Layout`, and `LogEvent` interfaces, as well as the `LogEventFactory` type, to enhance extensibility and type safety (`logger.ts`). - **New classes**: Added `LoggerImpl`, `Utility`, `LayoutImpl`, `LogEventImpl` (`logger.ts`), and `AssertionError` (`unit-test-framework.ts`) to provide a more modular, extensible, and testable architecture. diff --git a/README.md b/README.md index c23e067..08e4427 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,13 @@ Logger.addAppender(ConsoleAppender.getInstance()) // Add console appender ## Basic Usage Examples ```typescript -Logger.info("Script started") // [INFO] Script started. -Logger.warn("This might be a problem") // [WARN] This might be a problem. -Logger.error("A fatal error occurred") // [ERROR] A fatal error occurred -Logger.trace("Step-by-step details") // [TRACE] Step-by-step details +Logger.info("Script started") // [2025-06-25 23:41:10,585] [INFO] Script started. +Logger.warn("This might be a problem") // [2025-06-25 23:41:10,585] [WARN] This might be a problem. +Logger.error("A fatal error occurred") // [2025-06-25 23:41:10,585] [ERROR] A fatal error occurred +Logger.trace("Step-by-step details") // [2025-06-25 23:41:10,585] [TRACE] Step-by-step details ``` -> Output as shown above uses the short layout. With the default layout, a timestamp and brackets are included. +> Output as shown above uses the default layout. With the short layout, a timestamp and brackets are excluded. ### Logging to Excel Cell @@ -57,8 +57,8 @@ function main(workbook: ExcelScript.Workbook) { Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) Logger.addAppender(ExcelAppender.getInstance(cell)) - Logger.info("Log written to Excel!") // Output in cell B1: [INFO] Log written to Excel! (green text) - Logger.trace("Trace event in cell") // Output in cell B1: [TRACE] Trace event in cell (gray text) + Logger.info("Log written to Excel!") // Output in cell B1: [2025-06-25 23:41:10,586] [INFO] Log written to Excel! (green text) + Logger.trace("Trace event in cell") // Output in cell B1: [2025-06-25 23:41:10,586] [TRACE] Trace event in cell (gray text) } ``` @@ -222,6 +222,7 @@ You can pass an object with arbitrary key-value pairs as the `extrafields` argum #### Example: Adding custom fields to a log entry +Following examples assumed a short layout configuration. ```typescript Logger.info("Processing started", { step: "init", user: "alice@example.com" }) ```` @@ -297,10 +298,10 @@ function main(workbook: ExcelScript.Workbook) { Logger.addAppender(ExcelAppender.getInstance(logCell)) // Logging (with short layout, output shown as comments): - Logger.info("Script started.") // [INFO] Script started. + Logger.info("Script started.") // [INFO] Script started. Logger.trace("This is a trace message.") // [TRACE] This is a trace message. - Logger.warn("This is a warning.") // [WARN] This is a warning. - Logger.error("This is an error!") // [ERROR] This is an error! (if ACTION.EXIT, aborts script) + Logger.warn("This is a warning.") // [WARN] This is a warning. + Logger.error("This is an error!") // [ERROR] This is an error! (if ACTION.EXIT, aborts script) // ExcelAppender outputs in cell C2: // [INFO] Script started. (green text) @@ -323,6 +324,28 @@ This framework is designed so that the log message layout and log event factory - Passing these configurations via `getInstance` would require adding extra rarely-used parameters to already overloaded constructors, making the API harder for most users. - If your scenario truly requires dynamic, runtime reconfiguration, you can fork the codebase or adapt it for your specific needs, but for most Office Scripts, stability is preferred. +## Cross-Platform Compatibility + +This framework is designed to work seamlessly in both Node.js/TypeScript environments (such as VSCode) and directly within Office Scripts. + +- **Tested Environments:** + - Node.js/TypeScript (VSCode) + - Office Scripts (Excel on the web) + +- **Usage in Office Scripts:** + To use the framework in Office Scripts, paste the source files into your script in the following order: + 1. `test/unit-test-framework.ts` + 2. `src/logger.ts` + 3. `test/main.ts` + The order matters because Office Scripts requires that all objects and functions are declared before they are used. + +- **Office Scripts Compatibility Adjustments:** + - The code avoids unsupported keywords such as `any`, `export`, and `import`. + - Office Scripts does not allow calling `ExcelScript` API methods on Office objects inside class constructors; the code is structured to comply with this limitation. + - Additional nuances and workarounds are documented in the source code comments. + +This ensures the logging framework is robust and reliable across both development and production Office Scripts scenarios. + --- ## Troubleshooting & FAQ @@ -348,6 +371,26 @@ This framework is designed so that the log message layout and log event factory - **Why can't I send a different message to different appenders?** By design, all channels (appenders) receive the same log event message for consistency. +- **Why am I getting unexpected results when running some tests in Office Scripts compared to Node.js/TypeScript?** + This can happen because Office Scripts executes code asynchronously, which means some operations (like logging or cell updates) may not complete in strict sequence. As a result, test outcomes may differ from those in Node.js/TypeScript, which runs synchronously and flushes operations immediately. + + **Workaround:** + To ensure reliable test results in Office Scripts, introduce a delay or use asynchronous test helpers to wait for operations to complete before making assertions. In `test/main.ts`, the `TestCase.runTestAsync` method is used for this purpose. For example: + + ```typescript + let actualEvent = appender.getLastLogEvent() + TestCase.runTestAsync(() => { + Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null") + Assert.equals(actualEvent!.type, expectedType, "ExcelAppender(getLastLogEvent).type") + Assert.equals(actualEvent!.message, expectedMsg, "ExcelAppender(getLastLogEvent).message") + // Now checking the Excel cell value (formatted via layout) + let expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) + Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(string,LOG_EVENT))") + }) + ``` + + By wrapping assertions in `runTestAsync`, you allow asynchronous operations to finish, making your tests more reliable across both environments. + --- ## Additional Information diff --git a/docs/git-basics.md b/docs/git-basics.md new file mode 100644 index 0000000..f66cd75 --- /dev/null +++ b/docs/git-basics.md @@ -0,0 +1,94 @@ +# Git Basic Operations Reference + +## 1. Setup + +```bash +git config --global user.name "Your Name" +git config --global user.email "you@example.com" +``` + +## 2. Creating a Repository + +```bash +git init # Initialize new repo in current directory +git clone # Clone an existing repository +``` + +## 3. Checking Status + +```bash +git status # Show status of changes +``` + +## 4. Staging and Committing +- **Stagging**: Moves changes from your working directory to the staging area, preparing them for the next commit +- **Committing**: Captures a snapshot of the currently staged changes and records them in the local repository +```bash +git add # Stage a specific file +git add . # Stage all files +git commit -m "Message" # Commit staged changes +git commit --allow-empty -m "Message" # A commit with no changes +``` + +## 5. Viewing History + +```bash +git log # View commit history +git log --oneline # Condensed log +``` + +## 6. Working with Branches + +```bash +git branch # List branches +git branch # Create new branch +git checkout # Switch to branch +git checkout -b # Create and switch to branch +git merge # Merge branch into current +git branch -d # Delete a branch +``` + +## 7. Pulling and Pushing +- **Pulling**: Fetches changes from a remote repository and integrates them into your local branch +- **Pushing**: Upload local repository content to a remote repository +```bash +git pull # Fetch and merge from remote +git push # Push changes to remote +git push -u origin # Push new branch and track +``` + +## 8. Undoing Changes + +```bash +git checkout -- # Discard changes in working directory +git reset HEAD # Unstage a file +git revert # Create a new commit to undo changes +git reset --hard # Reset history and working directory (danger!) +``` + +## 9. Tags + +```bash +git tag # List tags +git tag # Create tag +git push origin # Push tag to remote +``` + +## 10. Rebasing (Advanced) +Modify the commit history of a branch by moving or combining a sequence of commits to a new base commit +```bash +git rebase # Rebase current branch onto base +git rebase -i # Interactive rebase for editing history +``` + +## 11. Stashing +Allows temporarily save uncommitted changes +```bash +git stash # Stash unsaved changes +git stash pop # Apply stashed changes +``` + +--- + +**Tip:** +Use `git help ` for detailed info about any command! diff --git a/docs/typedoc/assets/hierarchy.js b/docs/typedoc/assets/hierarchy.js index 1a401a5..8cb7ff5 100644 --- a/docs/typedoc/assets/hierarchy.js +++ b/docs/typedoc/assets/hierarchy.js @@ -1 +1 @@ -window.hierarchyData = "eJyV0t9uwiAUBvB3Ode4lYP0D3dm8WLJ3sB40bWozSg0gMsW47sv6HTVutDe9IJ+nB/txwGsMd6BWDFOsCCIZJ6tCVi5UbLyjdEOxAEQw1OXrQQBb2a7/JTaA4GPRtcgkKcE9laBgEZ7aTdlJd3zJfa0860CApUqnQMB3tWzsG92zYaXu0bVVmoQK0rz9ZEAFn2y/Db7KHgKTeZ4GjjGe9yi66SupY2Al9hUEpOUIE8IFizQ8+z2526j8Dk0lWUZDRyl+YMyX9tOXVWK+UU9De+1GXL/wueFYPB00F5UuKbi8zHpz1+8O2/Lyg9qe6DcZyPWbW9/nSFPegd4MdoZJcf4d9ERn1qwnrT8qqQa49wE4wrL6OASjrgSv6nY/OPxBwNybqE=" \ No newline at end of file +window.hierarchyData = "eJyVkk1uwyAQRu8ya9I6Y/wTdlGVRaXeIMrCtUliFYMFpGoV+e4VTu3acSrsDQt4zGPm4wpaKWuA7cOI4IYgEpocCGh+FDy3pZIG2BUQ3SqzigODN3XafXJpgcBHKQtgGMUELloAg1Jaro9Zzs1zhz2dbSWAQC4yY4CBNcXK3Vv1rDs8l6LQXALbr2lwaAjgZqjMvtXFK2yhxbokdbowGui2dc1lwbVH2GFLlRgEBCklmLZqmoyHe/KKb9BSbRi3g13T4EGYr1Uteusa087aFh+k6bh/xbcN50jSSXpeQ0/562Mw7GH7bqzOcjuJ7YHlnvW4xrn9ZYaUDh7woqRRgs/x36EzWk2Ho9x95VzM8YxAvyWMg8knnPElfilf/ab5AfvnbqQ=" \ No newline at end of file diff --git a/docs/typedoc/assets/search.js b/docs/typedoc/assets/search.js index f01c942..0537a06 100644 --- a/docs/typedoc/assets/search.js +++ b/docs/typedoc/assets/search.js @@ -1 +1 @@ -window.searchData = "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"; \ No newline at end of file +window.searchData = "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"; \ No newline at end of file diff --git a/docs/typedoc/classes/AbstractAppender.html b/docs/typedoc/classes/AbstractAppender.html index 9ae2cbf..4633faf 100644 --- a/docs/typedoc/classes/AbstractAppender.html +++ b/docs/typedoc/classes/AbstractAppender.html @@ -7,7 +7,8 @@ to supply custom event creation logic if needed.

Appenders such as 'ConsoleAppender' and 'ExcelAppender' should extend this class to inherit consistent logging behavior and event creation via the LogEventFactory.

-

Hierarchy (View Summary)

Implements

Index

Constructors

Hierarchy (View Summary)

Implements

Index

Constructors

  • Constructs a new AbstractAppender instance. Nothing is initialized, because the class only has static properties that are lazy initialized or set by the user.

    -

    Returns AbstractAppender

Methods

  • To ensure when invoking clearInstance in a sub-class it also clear the last log event

    -

    Returns void

Properties

defaultLogEventFactoryFun: LogEventFactory

Methods

  • To ensure when invoking clearInstance in a sub-class it also clear the last log event

    +

    Returns void

  • Send the event to the appropriate destination. The event is stored based on the

    +

Returns void

  • Send the event to the appropriate destination. The event is stored based on the

    Parameters

    • event: LogEvent

      The log event to be sent.

    • Optionalcontext: string

      (Optional) A string to provide additional context in case of an error.

    Returns void

    ScriptError if - The event is not a valid LogEvent.

    -
  • Send the event to the appropriate destination.

    Parameters

    • event: LogEvent

      The log event to be sent.

    Returns void

    ScriptError if - The event is not a valid LogEvent.

    Subclasses must call setLastLogEvent(event) after successfully sending the event, otherwise getLastLogEvent() will not reflect the most recent log event.

    -
  • Returns a string representation of the appender. +

  • Returns a string representation of the appender. It includes the information from the base class plus the information of the current class, so far this class doesn't have additional properties to show.

    Returns string

    A string representation of the appender.

    -
  • Sets to null the static layout, useful for running different scenarios.

    -

    Returns void

  • Sets to null the log event factory, useful for running different scenarios.

    -

    Returns void

  • Sets to null the static layout, useful for running different scenarios.

    +

    Returns void

  • Sets to null the log event factory, useful for running different scenarios.

    +

    Returns void

  • Returns Layout

    The layout associated to all events. Used to format the log event before sending it to the appenders. If the layout was not set, it returns a default layout (lazy initialization). The layout is shared by all events and all appenders, so it is static.

    Static, shared by all log events. Singleton.

    -
  • Gets the log event factory function used to create LogEvent instances. If it was not set before, it returns the default factory function.

    +
  • Gets the log event factory function used to create LogEvent instances. If it was not set before, it returns the default factory function.

    Returns LogEventFactory

    The log event factory function.

    -
  • Sets the layout associated to all events, the layout is assigned only if it was not set before.

    +
  • Sets the layout associated to all events, the layout is assigned only if it was not set before.

    Parameters

    • layout: Layout

      The layout to set.

    Returns void

    ScriptError if the layout is not a valid Layout implementation.

    -
  • Sets the log event factory function used to create LogEvent instances if it was not set before.

    +
  • Sets the log event factory function used to create LogEvent instances if it was not set before.

    Parameters

    • logEventFactory: LogEventFactory

      A factory function to create LogEvent instances. Must have the signature (message: string, eventType: LOG_EVENT) => LogEvent. If not provided, a default factory function is used.

      @@ -62,4 +63,4 @@
      // Example: Custom LogEvent to be used to specify the environment where the log event was created.
      let prodLogEventFactory: LogEventFactory
      = function prodLogEventFactoryFun(message: string, eventType: LOG_EVENT) {
      return new LogEventImpl("PROD-" + message, eventType) // add environment prefix
      }
      AbstractAppender.setLogEventFactory(prodLogEventFactory) // Now all appenders will use ProdLogEvent
      -
+
diff --git a/docs/typedoc/classes/ConsoleAppender.html b/docs/typedoc/classes/ConsoleAppender.html index 0d0f60c..2b63a00 100644 --- a/docs/typedoc/classes/ConsoleAppender.html +++ b/docs/typedoc/classes/ConsoleAppender.html @@ -10,7 +10,8 @@ // Add console appender to the Logger Logger.addAppender(ConsoleAppender.getInstance()) -

Hierarchy (View Summary)

Implements

Index

Methods

Hierarchy (View Summary)

Implements

Index

Methods

Properties

defaultLogEventFactoryFun: LogEventFactory

Methods

  • Internal method to send the event to the console.

    +

Returns void

  • Internal method to send the event to the console.

    Parameters

    • event: LogEvent

      The log event to output.

    • Optionalcontext: string

      (Optional) A string to provide additional context in case of an error.

    Returns void

    ScriptError if The event is not a valid LogEvent. The instance is not available (not instantiated).

    -
  • Send the event to the appropriate destination.

    Parameters

    • event: LogEvent

      The log event to be sent.

    Returns void

    ScriptError if - The event is not a valid LogEvent.

    Subclasses must call setLastLogEvent(event) after successfully sending the event, otherwise getLastLogEvent() will not reflect the most recent log event.

    -
  • Sets to null the singleton instance, useful for running different scenarios. It also sets to null the parent property _lastLogEvent, so the last log event is cleared.

    Returns void

    Mainly intended for testing purposes. The state of the singleton will be lost. This method only exist in src folder it wont be deployed in dist folder (production).

    There is no way to empty the last message sent after the instance was created unless
    you reset it.
    appender:ConsoleAppender = ConsoleAppender.getInstance()
    appender = ConsoleAppender.log("info event", LOG_EVENT.INFO)
    appender.getLastLogEvent().message // Output: info event"
    ConsoleAppender.clearInstance() // clear the singleton
    appender = ConsoleAppender.getInstance() // restart the singleton
    appender.getLastLogEvent().message // Output: ""
    -
  • Returns Layout

    The layout associated to all events. Used to format the log event before sending it to the appenders. +

  • Returns Layout

    The layout associated to all events. Used to format the log event before sending it to the appenders. If the layout was not set, it returns a default layout (lazy initialization). The layout is shared by all events and all appenders, so it is static.

    Static, shared by all log events. Singleton.

    -
  • Sets the layout associated to all events, the layout is assigned only if it was not set before.

    Parameters

    • layout: Layout

      The layout to set.

    Returns void

    ScriptError if the layout is not a valid Layout implementation.

    -
  • Sets the log event factory function used to create LogEvent instances if it was not set before.

    Parameters

    • logEventFactory: LogEventFactory

      A factory function to create LogEvent instances. Must have the signature (message: string, eventType: LOG_EVENT) => LogEvent. If not provided, a default factory function is used.

      @@ -73,4 +74,4 @@
      // Example: Custom LogEvent to be used to specify the environment where the log event was created.
      let prodLogEventFactory: LogEventFactory
      = function prodLogEventFactoryFun(message: string, eventType: LOG_EVENT) {
      return new LogEventImpl("PROD-" + message, eventType) // add environment prefix
      }
      AbstractAppender.setLogEventFactory(prodLogEventFactory) // Now all appenders will use ProdLogEvent
      -
+
diff --git a/docs/typedoc/classes/ExcelAppender.html b/docs/typedoc/classes/ExcelAppender.html index e9599a6..4d3f399 100644 --- a/docs/typedoc/classes/ExcelAppender.html +++ b/docs/typedoc/classes/ExcelAppender.html @@ -11,8 +11,12 @@
const range = workbook.getWorksheet("Log").getRange("A1")
Logger.addAppender(ExcelAppender.getInstance(range)) // Add appender to the Logger
-

Hierarchy (View Summary)

Implements

Index

Methods

Hierarchy (View Summary)

Implements

Index

Methods

Properties

DEFAULT_EVENT_FONTS: Readonly<
    { "1": "9c0006"; "2": "ed7d31"; "3": "548235"; "4": "7f7f7f" },
> = ...

Default colors for log events, used if no custom colors are provided. +These colors are defined as hex strings (without the # prefix). +The colors can be customized by passing a map of LOG_EVENT types to hex color strings +when calling getInstance(). Default colors are:

+
    +
  • ERROR: "9c0006" (red)
  • +
  • WARN: "ed7d31" (orange)
  • +
  • INFO: "548235" (green)
  • +
  • TRACE: "7f7f7f" (gray)
  • +
+
defaultLogEventFactoryFun: LogEventFactory

Methods

  • Returns the map of event types to font colors used by this appender.

    +

    Returns Record<LOG_EVENT, string>

    A defensive copy of the event fonts map.

    +

    The keys are LOG_EVENT enum values, and the values are hex color strings.

    +
  • Returns the Excel range where log messages are written.

    +

    Returns any

    The ExcelScript.Range object representing the message cell range.

    +

    This is the cell where log messages will be displayed.

    +
  • Sets the value of the cell, with the event message, using the font defined for the event type, +

Returns void

  • Sets the value of the cell, with the event message, using the font defined for the event type, if not font was defined it doesn't change the font of the cell.

    Parameters

    • event: LogEvent

      a value from enum LOG_EVENT.

    • Optionalcontext: string

    Returns void

    ScriptError in case event is not a valid LOG_EVENT enum value.

    -
  • Send the event to the appropriate destination.

    Parameters

    • event: LogEvent

      The log event to be sent.

    Returns void

    ScriptError if - The event is not a valid LogEvent.

    -

    Subclasses must call setLastLogEvent(event) after successfully sending the event, +

    Subclasses must call setLastLogEvent(event) after successfully sending the event, otherwise getLastLogEvent() will not reflect the most recent log event.

    -
  • Sets to null the singleton instance, useful for running different scenarios. It also sets to null the parent property _lastLogEvent, so the last log event is cleared.

    -

    Returns void

    Mainly intended for testing purposes. The state of the singleton will be lost. +

    Returns void

    Mainly intended for testing purposes. The state of the singleton will be lost. This method only exist in src folder, it wont be deployed in dist folder (production).

    const activeSheet = workbook.getActiveWorksheet() // workbook is input argument of main
    const msgCellRng = activeSheet.getRange("C2")
    appender = ExcelAppender.getInstance(msgCellRng) // with default log event colors
    appender.info("info event") // Output: In Excel in cell C2 with green color shows: "info event"
    // Now we want to test how getInstance() can throw a ScriptError,
    // but we can't because the instance was already created and it is a singleton we need clearInstance
    appender.clearInstance()
    appender = ExcelAppender.getInstance(null) // throws a ScriptError
    -
  • Returns the singleton ExcelAppender instance, creating it if it doesn't exist. +

  • Returns the singleton ExcelAppender instance, creating it if it doesn't exist. On first call, requires a valid single cell Excel range to display log messages and optional color customizations for different log events (LOG_EVENT). Subsequent calls ignore parameters and return the existing instance.

    Parameters

    • msgCellRng: any

      Excel range where log messages will be written. Must be a single cell and not null or undefined.

      -
    • errFont: string = ...

      Hex color code for error messages (default: "9c0006" red).

      -
    • warnFont: string = ...

      Hex color code for warnings (default: "ed7d31" orange).

      -
    • infoFont: string = ...

      Hex color code for info messages (default: "548235" green).

      -
    • traceFont: string = ...

      Hex color code for trace messages (default: "7f7f7f" gray).

      +
    • eventFonts: Record<LOG_EVENT, string> = ExcelAppender.DEFAULT_EVENT_FONTS

      Optional. A map of LOG_EVENT types to hex color codes for the font colors. +If not provided, defaults to the predefined colors in DEFAULT_EVENT_FONTS. +The user can provide just the colors they want to customize, +the rest will use the default colors.

    Returns ExcelAppender

    The singleton ExcelAppender instance.

    ScriptError if msgCellRng was not defined or if the range covers multiple cells or if it is not a valid Excel range. @@ -69,16 +89,17 @@

    const range = workbook.getWorksheet("Log").getRange("A1")
    const excelAppender = ExcelAppender.getInstance(range)
    ExcelAppender.getInstance(range, "ff0000") // ignored if called again
    -
  • Returns Layout

    The layout associated to all events. Used to format the log event before sending it to the appenders. +

    DEFAULT_EVENT_FONTS

    +
  • Returns Layout

    The layout associated to all events. Used to format the log event before sending it to the appenders. If the layout was not set, it returns a default layout (lazy initialization). The layout is shared by all events and all appenders, so it is static.

    -

    Static, shared by all log events. Singleton.

    -
  • Gets the log event factory function used to create LogEvent instances. If it was not set before, it returns the default factory function.

    +

    Static, shared by all log events. Singleton.

    +
  • Sets the layout associated to all events, the layout is assigned only if it was not set before.

    Parameters

    • layout: Layout

      The layout to set.

    Returns void

    ScriptError if the layout is not a valid Layout implementation.

    -
  • Sets the log event factory function used to create LogEvent instances if it was not set before.

    Parameters

    • logEventFactory: LogEventFactory

      A factory function to create LogEvent instances. Must have the signature (message: string, eventType: LOG_EVENT) => LogEvent. If not provided, a default factory function is used.

      @@ -86,4 +107,4 @@
      // Example: Custom LogEvent to be used to specify the environment where the log event was created.
      let prodLogEventFactory: LogEventFactory
      = function prodLogEventFactoryFun(message: string, eventType: LOG_EVENT) {
      return new LogEventImpl("PROD-" + message, eventType) // add environment prefix
      }
      AbstractAppender.setLogEventFactory(prodLogEventFactory) // Now all appenders will use ProdLogEvent
      -
+
diff --git a/docs/typedoc/classes/LayoutImpl.html b/docs/typedoc/classes/LayoutImpl.html index 1903770..f53d80a 100644 --- a/docs/typedoc/classes/LayoutImpl.html +++ b/docs/typedoc/classes/LayoutImpl.html @@ -5,12 +5,13 @@
  • All events are validated to conform to the LogEvent interface before formatting.
  • Throws ScriptError if the event does not conform to the expected LogEvent interface.
  • -

    Implements

    Index

    Constructors

    Implements

    Index

    Constructors

    • Constructs a new LayoutImpl.

      Parameters

      • Optionalformatter: LayoutFormatter

        Optional. A function that formats a LogEvent as a string. @@ -26,34 +27,35 @@

        // Using the default formatter:
        const layout = new LayoutImpl()
        // Using a custom formatter for JSON output:
        const jsonLayout = new LayoutImpl(event => JSON.stringify(event))
        // Using a formatter for XML output:
        const xmlLayout = new LayoutImpl(event =>
        `<log><type>${event.type}</type><message>${event.message}</message></log>`
        )
        // Using a shorter format [type] [message]:
        const shortLayout = new LayoutImpl(e => `[${LOG_EVENT[e.type]}] ${e.message}`)
        // Using a custom formatter with a named function, so in toString() shows the name of the formatter.
        let shortLayoutFun: Layout = new LayoutImpl(
        function shortLayoutFun(e:LogEvent):string{return `[${LOG_EVENT[e.type]}] ${e.message}`})
        -

    Properties

    defaultFormatterFun: (event: LogEvent) => string = ...

    Default formatter function. Created as a named function. Formats a log event as [timestamp] [type] message. -The timestamp is formatted as YYYY-MM-DD HH:mm:ss,SSS. -If extraFields are present in the event, they will be appended as a JSON object (surrounded by braces) to the output. -Example: [2025-06-19 15:06:41,123] [ERROR] Something bad happened {"user":"dlealv","id":42} -Defined as a named function to ensure toString() returns the function name.

    -
    shortFormatterFun: (event: LogEvent) => string = ...

    Convenience public constant to help users to define a short format for log events. -Formats a log event as a short string as follows '[type] message'. -If extraFields are present in the event, they will be appended as a JSON object (surrounded by braces) to the output. -Example: [ERROR] Something bad happened {"user":"dlealv","id":42} -Defined as a named function to ensure toString() returns the function name.

    -

    Methods

    Properties

    defaultFormatterFun: LayoutFormatter

    Convninient static property to define a long formatter used as default.

    +

    LayoutImpl.defaultFormatterFun

    +
    shortFormatterFun: LayoutFormatter

    Convninience static property to define a short formatter.

    +

    LayoutImpl.shortFormatterFun

    +

    Methods

    • Formats the given log event as a string.

      Parameters

      Returns string

      A string representation of the log event.

      ScriptError if the event does not conform to the LogEvent interface.

      -
    • Returns string

      A string representation of the layout. +

    • Returns string

      A string representation of the layout. If the formatter is a function, it returns the name of the function.

      -
    • Asserts that the provided object implements the Layout interface. +

    • Validates that the provided value is a valid formatter function +for use in LayoutImpl (_formatter property). The formatter must be +a function accepting a single LogEvent argument and must return a non-empty, non-null string.

      +

      Parameters

      • formatter: LayoutFormatter

        The candidate formatter function to validate

        +
      • Optionalcontext: string

        (Optional) Additional context for error messages

        +

      Returns void

      ScriptError if formatter is missing, not a function, doesn't have arity 1, +or returns null/empty string for a sample event.

      +
    • Asserts that the provided object implements the Layout interface. Checks for the public 'format' method (should be a function taking one argument). Also validates the internal '_formatter' property if present, by calling validateFormatter. Used by appenders to validate layout objects at runtime.

      -

      Parameters

      • layout: unknown

        The object to validate as a Layout implementation

        +

        Parameters

        • layout: Layout

          The object to validate as a Layout implementation

        • Optionalcontext: string

          (Optional) Additional context for error messages

          -

        Returns asserts layout is Layout

        ScriptError if:

        +

      Returns void

      ScriptError if:

      • layout is null or undefined
      • format is not a function or doesn't have arity 1
      • _formatter is present and is missing, not a function, or doesn't have arity 1
      -
    +
    diff --git a/docs/typedoc/classes/LogEventImpl.html b/docs/typedoc/classes/LogEventImpl.html index b0713d4..8929e01 100644 --- a/docs/typedoc/classes/LogEventImpl.html +++ b/docs/typedoc/classes/LogEventImpl.html @@ -1,7 +1,7 @@ LogEventImpl | officescripts-logging-framework
    officescripts-logging-framework
      Preparing search index...

      Class LogEventImpl

      Implements the LogEvent interface, providing a concrete representation of a log event. It includes properties for the event type, message, and timestamp, along with methods to manage the layout used for formatting log events before sending them to appenders.

      -

      Implements

      Index

      Constructors

      Implements

      Index

      Constructors

      Accessors

      extraFields message timestamp @@ -16,16 +16,16 @@
    • OptionalextraFields: LogEventExtraFields

      (Optional) Additional fields for the log event, can include strings, numbers, dates, or functions.

    • timestamp: Date = ...

      (Optional) The timestamp of the event, defaults to current time.

    • Returns LogEventImpl

      ScriptError if validation fails.

      -

      Accessors

      Accessors

      • get extraFields(): Readonly<LogEventExtraFields>

        Gets the extra fields of the log event.

        Returns Readonly<LogEventExtraFields>

        Returns a shallow copy of custom fields for this event. These are immutable (Object.freeze), but if you allow object values in the future, document that deep mutation is not prevented.

        -

      Methods

      • Returns string

        A string representation of the log event in stardard toString format

        -

      Methods

      • Returns string

        A string representation of the log event in stardard toString format

        +
      • Returns a standardized label for the given log event.

        Parameters

        • type: LOG_EVENT

          The event type from 'LOG_EVENT' enum.

        Returns string

        A string label, e.g., '[INFO]', '[ERROR]'.

        -
      • Validates if the input object conforms to the LogEvent interface (for any implementation).

        +
      • Validates if the input object conforms to the LogEvent interface (for any implementation).

        Parameters

        • event: unknown
        • Optionalcontext: string

        Returns void

        ScriptError if event is invalid.

        -
      +
      diff --git a/docs/typedoc/classes/LoggerImpl.html b/docs/typedoc/classes/LoggerImpl.html index e98f0d0..9e39c3f 100644 --- a/docs/typedoc/classes/LoggerImpl.html +++ b/docs/typedoc/classes/LoggerImpl.html @@ -25,7 +25,7 @@
      // Minimal logger usage; ConsoleAppender is auto-added if none specified
      LoggerImpl.getInstance().info("This message will appear in the console by default.");
      -

      Implements

      Index

      Properties

      Implements

      Index

      Properties

      Methods

      Properties

      ACTION: Readonly<{ CONTINUE: 0; EXIT: 1 }> = ...
      LEVEL: Readonly<{ OFF: number } & typeof LOG_EVENT> = ...

      Methods

      • Adds an appender to the list of appenders.

        +

      Properties

      ACTION: Readonly<{ CONTINUE: 0; EXIT: 1 }> = ...
      LEVEL: Readonly<{ OFF: number } & typeof LOG_EVENT> = ...

      Methods

      • Adds an appender to the list of appenders.

        Parameters

        Returns void

        ScriptError If the singleton was not instantiated, if the input argument is null or undefined, or if it breaks the class uniqueness of the appenders. All appenders must be from a different implementation of the Appender class.

        setAppenders

        -
      • Sends an error log message (with optional structured extra fields) to all appenders if the level allows it. The level has to be greater than or equal to Logger.LEVEL.ERROR to send this event to the appenders. After the message is sent, it updates the error counter.

        Parameters

        • msg: string

          The error message to log.

          @@ -66,46 +66,46 @@

        Returns void

        If no singleton was defined, it does lazy initialization with default configuration. If no appender was defined, it does lazy initialization to ConsoleAppender.

        ScriptError Only if level is greater than Logger.LEVEL.OFF and the action is Logger.ACTION.EXIT.

        -
      • Serializes the current state of the logger to a plain object, useful for +

      • Serializes the current state of the logger to a plain object, useful for capturing logs and metrics for post-run analysis. For testing/debugging: Compare expected vs actual logger state. For persisting logs into Excel, JSON, or another external system.

        Returns {
            action: string;
            criticalEvents: LogEvent[];
            errorCount: number;
            level: string;
            warningCount: number;
        }

        A structure with key information about the logger, such as: level, action, errorCount, warningCount, criticalEvents.

        ScriptError If the singleton was not instantiated.

        -
      • Returns 0 | 1

        The action to take in case of errors or warning log events.

        ScriptError If the singleton was not instantiated.

        -
      • Returns number

        Total number of error message events sent to the appenders.

        ScriptError If the singleton was not instantiated.

        -
      • Returns the level of verbosity allowed in the Logger. The levels are incremental, i.e. +

      • Returns the level of verbosity allowed in the Logger. The levels are incremental, i.e. it includes all previous levels. For example: Logger.WARN includes warnings and errors since Logger.ERROR is lower.

        Returns number

        The current log level.

        ScriptError If the singleton was not instantiated.

        -
      • Returns number

        Total number of warning events sent to the appenders.

        ScriptError If the singleton was not instantiated.

        -
      • Returns boolean

        true if an error log event was sent to the appenders, otherwise false.

        +
      • Returns boolean

        true if an error log event was sent to the appenders, otherwise false.

        ScriptError If the singleton was not instantiated.

        -
      • Returns boolean

        true if some error or warning event has been sent by the appenders, otherwise false.

        +
      • Returns boolean

        true if some error or warning event has been sent by the appenders, otherwise false.

        ScriptError If the singleton was not instantiated.

        -
      • Returns boolean

        true if a warning log event was sent to the appenders, otherwise false.

        +
      • Returns boolean

        true if a warning log event was sent to the appenders, otherwise false.

        ScriptError If the singleton was not instantiated.

        -
      • Sends an info log message (with optional structured extra fields) to all appenders if the level allows it. The level has to be greater than or equal to Logger.LEVEL.INFO to send this event to the appenders.

        Parameters

        • msg: string

          The informational message to log.

        • OptionalextraFields: LogEventExtraFields

          Optional structured data to attach to the log event (e.g., context info, tags).

        Returns void

        If no singleton was defined, it does lazy initialization with default configuration. If no appender was defined, it does lazy initialization to ConsoleAppender.

        -
      • If the list of appenders is not empty, removes the appender from the list.

        Parameters

        • appender: Appender

          The appender to remove.

        Returns void

        ScriptError If the singleton was not instantiated.

        -
      • Resets the Logger history, i.e., state (errors, warnings, message summary). It doesn't reset the appenders.

        +
      • Resets the Logger history, i.e., state (errors, warnings, message summary). It doesn't reset the appenders.

        Returns void

        ScriptError If the singleton was not instantiated.

        -
      • Sets the array of appenders with the input argument appenders.

        Parameters

        • appenders: Appender[]

          Array with all appenders to set.

        Returns void

        ScriptError If the singleton was not instantiated, if appenders is null or undefined, or contains @@ -113,17 +113,17 @@ or if the appenders to add are not unique by appender class. See JSDoc from addAppender.

        addAppender

        -
      • Short version fo the toString() which exludes the appenders details

        Returns string

        Similar to toString, but showing the list of appenders name only.

        -
      • Override toString method.

        Returns string

        ScriptError If the singleton was not instantiated.

        -
      • Sends a trace log message (with optional structured extra fields) to all appenders if the level allows it. The level has to be greater than or equal to Logger.LEVEL.TRACE to send this event to the appenders.

        Parameters

        • msg: string

          The trace message to log.

        • OptionalextraFields: LogEventExtraFields

          Optional structured data to attach to the log event (e.g., context info, tags).

        Returns void

        If no singleton was defined, it does lazy initialization with default configuration. If no appender was defined, it does lazy initialization to ConsoleAppender.

        -
      • Sends a warning log message (with optional structured extra fields) to all appenders if the level allows it. The level has to be greater than or equal to Logger.LEVEL.WARN to send this event to the appenders. After the message is sent, it updates the warning counter.

        Parameters

        • msg: string

          The warning message to log.

          @@ -131,7 +131,7 @@

        Returns void

        If no singleton was defined, it does lazy initialization with default configuration. If no appender was defined, it does lazy initialization to ConsoleAppender.

        ScriptError Only if level is greater than Logger.LEVEL.ERROR and the action is Logger.ACTION.EXIT.

        -
      • Sets the singleton instance to null, useful for running different scenarios.

        +
      • Sets the singleton instance to null, useful for running different scenarios.

        Returns void

        Mainly intended for testing purposes. The state of the singleton will be lost. This method only exist in src folder, it wont be deployed in dist folder (production). It doesn't set the appenders to null, so the appenders are not cleared.

        @@ -139,10 +139,10 @@
        // Testing how the logger works with default configuration, and then changing the configuration.
        // Since the class doesn't define setter methods to change the configuration, you can use
        // clearInstance to reset the singleton and instantiate it with different configuration.
        // Testing default configuration
        Logger.getInstance(); // LEVEL: WARN, ACTION: EXIT
        logger.error("error event"); // Output: "error event" and ScriptError
        // Now we want to test with the following configuration: Logger.LEVEL:WARN, Logger.ACTION:CONTINUE
        Logger.clearInstance(); // Clear the singleton
        Logger.getInstance(LEVEL.WARN, ACTION.CONTINUE);
        Logger.error("error event"); // Output: "error event" (no ScriptError was thrown)
        -
      • Returns the label for a log action value. +

      • Returns the label for a log action value. If no parameter is provided, uses the current logger instance's action. Returns "UNKNOWN" if the value is not found or logger is not initialized.

        -

        Parameters

        • Optionalaction: 0 | 1

        Returns string

      • Returns the singleton Logger instance, creating it if it doesn't exist. +

        Parameters

        • Optionalaction: 0 | 1

        Returns string

      • Returns the singleton Logger instance, creating it if it doesn't exist. If the Logger is created during this call, the provided 'level' and 'action' parameters initialize the log level and error-handling behavior.

        Parameters

        • level: number = LoggerImpl.DEFAULT_LEVEL

          Initial log level (default: Logger.LEVEL.WARN). Controls verbosity. @@ -163,7 +163,8 @@

          AbstractAppender.constructor for more information on how to use the logEventFactory.

          -
      • Returns the label for a log level value. -If no parameter is provided, uses the current logger instance's level. -Returns "UNKNOWN" if the value is not found or logger is not initialized.

        -

        Parameters

        • Optionallevel: number

        Returns string

      +
      • Returns the label for the given log level.

        +

        Parameters

        • Optionallevel: number

        Returns string

        The label for the log level. +If level is undefined, returns the label for the current logger instance's level. +If neither is set, returns "UNKNOWN".

        +
      diff --git a/docs/typedoc/classes/ScriptError.html b/docs/typedoc/classes/ScriptError.html index dc93806..464129c 100644 --- a/docs/typedoc/classes/ScriptError.html +++ b/docs/typedoc/classes/ScriptError.html @@ -7,7 +7,7 @@
      const original = new Error("Missing field")
      throw new ScriptError("Validation failed", original)
      -

      Hierarchy

      • Error
        • ScriptError
      Index

      Constructors

      Hierarchy

      • Error
        • ScriptError
      Index

      Constructors

      Properties

      cause? message name @@ -21,26 +21,26 @@

      Parameters

      • message: string

        A description of the error.

      • Optionalcause: Error

        (Optional) The original error that caused this one. If provided the exception message will have a refernece to the cause

        -

      Returns ScriptError

      Properties

      cause?: Error

      (Optional) The original error that caused this one. +

      Returns ScriptError

      Properties

      cause?: Error

      (Optional) The original error that caused this one. If provided the exception message will have a refernece to the cause

      -
      message: string
      name: string
      stack?: string
      stackTraceLimit: number

      The Error.stackTraceLimit property specifies the number of stack frames +

      message: string
      name: string
      stack?: string
      stackTraceLimit: number

      The Error.stackTraceLimit property specifies the number of stack frames collected by a stack trace (whether generated by new Error().stack or Error.captureStackTrace(obj)).

      The default value is 10 but may be set to any valid JavaScript number. Changes will affect any stack trace captured after the value has been changed.

      If set to a non-number value, or set to a negative number, stack traces will not capture any frames.

      -

      Methods

      Methods

      • Utility method to rethrow the deepest original cause if present, otherwise rethrows this 'ScriptError' itself. Useful for deferring a controlled exception and then surfacing the root cause explicitly.

        -

        Returns never

      • Override toString() method.

        +

        Returns never

      • Override toString() method.

        Returns string

        The name and the message on the first line, then on the second line the Stack trace section name, i.e. 'Stack trace:'. Starting on the third line the stack trace information. If a cause was provided the stack trace will refer to the cause otherwise to the original exception.

        -
      • Creates a .stack property on targetObject, which when accessed returns +

      • Creates a .stack property on targetObject, which when accessed returns a string representing the location in the code at which Error.captureStackTrace() was called.

        const myObject = {};
        Error.captureStackTrace(myObject);
        myObject.stack; // Similar to `new Error().stack` @@ -56,5 +56,5 @@
        function a() {
        b();
        }

        function b() {
        c();
        }

        function c() {
        // Create an error without stack trace to avoid calculating the stack trace twice.
        const { stackTraceLimit } = Error;
        Error.stackTraceLimit = 0;
        const error = new Error();
        Error.stackTraceLimit = stackTraceLimit;

        // Capture the stack trace above function b
        Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
        throw error;
        }

        a();
        -

        Parameters

        • targetObject: object
        • OptionalconstructorOpt: Function

        Returns void

      +

      Parameters

      • targetObject: object
      • OptionalconstructorOpt: Function

      Returns void

      diff --git a/docs/typedoc/classes/Utility.html b/docs/typedoc/classes/Utility.html index 9a258a5..b546ecf 100644 --- a/docs/typedoc/classes/Utility.html +++ b/docs/typedoc/classes/Utility.html @@ -1,12 +1,12 @@ Utility | officescripts-logging-framework
      officescripts-logging-framework
        Preparing search index...

        Class Utility

        Utility class providing static helper methods for logging operations.

        -
        Index

        Constructors

        Index

        Constructors

        Methods

        • Helpder to format the local date as a string. Ouptut in standard format: YYYY-MM-DD HH:mm:ss,SSS

          -

          Parameters

          • date: Date

          Returns string

        • Helper method to check for an empty array.

          -

          Type Parameters

          • T

          Parameters

          • arr: T[]

          Returns boolean

        • Validates a log event factory is a function.

          +

          Parameters

          • date: Date

          Returns string

        • Helper method to check for an empty array.

          +

          Type Parameters

          • T

          Parameters

          • arr: T[]

          Returns boolean

        • Validates a log event factory is a function.

          Parameters

          • factory: unknown

            The factory function to validate.

          • OptionalfunName: string

            Used to identify the function name in the error message.

          • Optionalcontext: string

          Returns void

          ScriptError if the log event factory is not a function.

          -
        +
        diff --git a/docs/typedoc/enums/LOG_EVENT.html b/docs/typedoc/enums/LOG_EVENT.html index 58d75db..87bbb9f 100644 --- a/docs/typedoc/enums/LOG_EVENT.html +++ b/docs/typedoc/enums/LOG_EVENT.html @@ -12,8 +12,8 @@ Logger.OFF, therefore the verbosity respects the same order of the LOG_EVENT.

        It was defined as an independent entity since it is used by appenders and by the Logger.

        -
        Index

        Enumeration Members

        Index

        Enumeration Members

        Enumeration Members

        ERROR: 1
        INFO: 3
        TRACE: 4
        WARN: 2
        +

        Enumeration Members

        ERROR: 1
        INFO: 3
        TRACE: 4
        WARN: 2
        diff --git a/docs/typedoc/hierarchy.html b/docs/typedoc/hierarchy.html index 3c3662b..45c5569 100644 --- a/docs/typedoc/hierarchy.html +++ b/docs/typedoc/hierarchy.html @@ -1 +1 @@ -officescripts-logging-framework
        officescripts-logging-framework
          Preparing search index...
          +officescripts-logging-framework
          officescripts-logging-framework
            Preparing search index...
            diff --git a/docs/typedoc/interfaces/Appender.html b/docs/typedoc/interfaces/Appender.html index 92debd0..e2ad186 100644 --- a/docs/typedoc/interfaces/Appender.html +++ b/docs/typedoc/interfaces/Appender.html @@ -15,20 +15,20 @@
          • Provides to log methods sending a LogEvent object or a message with an event type and optional extra fields. This allows flexibility in how log events are created and sent.
          • -
            interface Appender {
                getLastLogEvent(): LogEvent;
                log(event: LogEvent): void;
                log(msg: string, type: LOG_EVENT, extraFields?: object): void;
                toString(): string;
            }

            Implemented by

            Index
            interface Appender {
                getLastLogEvent(): LogEvent;
                log(event: LogEvent): void;
                log(msg: string, type: LOG_EVENT, extraFields?: object): void;
                toString(): string;
            }

            Implemented by

            Index

            Methods

            • Returns the last LogEvent delivered to the appender, or null if none sent yet.

              Returns LogEvent

              The last LogEvent object delivered, or null.

              ScriptError if the appender instance is unavailable.

              -
            • Sends a structured log event to the appender.

              Parameters

              • event: LogEvent

                The log event object to deliver.

              Returns void

              ScriptError if the event is invalid or cannot be delivered.

              -
            • Sends a log message to the appender, specifying the event type and optional structured extra fields.

              +
            • Sends a log message to the appender, specifying the event type and optional structured extra fields.

              Parameters

              • msg: string

                The message to log.

              • type: LOG_EVENT

                The type of log event (from LOG_EVENT enum).

              • OptionalextraFields: object

                Optional structured data (object) to attach to the log event (e.g., context info, tags).

                -

              Returns void

            • Returns a string summary of the appender's state, typically including its type and last event.

              +

            Returns void

            • Returns a string summary of the appender's state, typically including its type and last event.

              Returns string

              A string describing the appender and its most recent activity.

              ScriptError if the appender instance is unavailable.

              -
            +
            diff --git a/docs/typedoc/interfaces/Layout.html b/docs/typedoc/interfaces/Layout.html index b5e3e31..447cd8e 100644 --- a/docs/typedoc/interfaces/Layout.html +++ b/docs/typedoc/interfaces/Layout.html @@ -8,11 +8,11 @@
          • Typical implementations may provide static/shared instances for consistency.
          • Layouts are intended for core message structure, not for display/presentation logic.
          • -
            interface Layout {
                format(event: LogEvent): string;
                toString(): string;
            }

            Implemented by

            Index

            Methods

            interface Layout {
                format(event: LogEvent): string;
                toString(): string;
            }

            Implemented by

            Index

            Methods

            • Formats the given log event into its core string representation.

              Parameters

              • event: LogEvent

                The log event to format (must be a valid, immutable LogEvent).

              Returns string

              The formatted string representing the event's core content.

              -
            • Returns a string describing the layout, ideally including the formatter function name or configuration. +

            • Returns a string describing the layout, ideally including the formatter function name or configuration. Used for diagnostics or debugging.

              -

              Returns string

            +

            Returns string

            diff --git a/docs/typedoc/interfaces/LogEvent.html b/docs/typedoc/interfaces/LogEvent.html index fcd1908..731c58b 100644 --- a/docs/typedoc/interfaces/LogEvent.html +++ b/docs/typedoc/interfaces/LogEvent.html @@ -13,7 +13,7 @@
          • Framework code may validate any object passed as a LogEvent to ensure all invariants hold. If you implement this interface directly, you are responsible for upholding these invariants.
          • -
            interface LogEvent {
                extraFields: LogEventExtraFields;
                message: string;
                timestamp: Date;
                type: LOG_EVENT;
                toString(): string;
            }

            Implemented by

            Index

            Properties

            interface LogEvent {
                extraFields: LogEventExtraFields;
                message: string;
                timestamp: Date;
                type: LOG_EVENT;
                toString(): string;
            }

            Implemented by

            Index

            Properties

            extraFields message timestamp type @@ -21,12 +21,12 @@

            Properties

            extraFields: LogEventExtraFields

            Additional metadata for the log event, for extension and contextual purposes. This field is immutable and must be a plain object. Intended for extensibility—avoid storing sensitive or large data here.

            -
            message: string

            The log message to be sent. +

            message: string

            The log message to be sent. This field is immutable. It must not be null, undefined, or an empty string.

            -
            timestamp: Date

            The timestamp when the event was created. +

            timestamp: Date

            The timestamp when the event was created. This field is immutable and must be a valid Date instance.

            -
            type: LOG_EVENT

            The event type from the LOG_EVENT enum. +

            type: LOG_EVENT

            The event type from the LOG_EVENT enum. This field is immutable and must be set at construction.

            -

            Methods

            • Returns a string representation of the log event in a human-readable, single-line format, +

            Methods

            • Returns a string representation of the log event in a human-readable, single-line format, including all relevant fields.

              -

              Returns string

            +

            Returns string

            diff --git a/docs/typedoc/interfaces/Logger.html b/docs/typedoc/interfaces/Logger.html index 2fd9657..ac00d2e 100644 --- a/docs/typedoc/interfaces/Logger.html +++ b/docs/typedoc/interfaces/Logger.html @@ -2,7 +2,7 @@ Provides methods for logging messages, querying log state, managing appenders, and exporting logger state. Implementations should ensure they should not maintain global mutable state outside the singleton and efficient log event handling.

            -
            interface Logger {
                addAppender(appender: Appender): void;
                error(msg: string, extraFields?: object): void;
                exportState(): {
                    action: string;
                    criticalEvents: LogEvent[];
                    errorCount: number;
                    level: string;
                    warningCount: number;
                };
                getAction(): number;
                getAppenders(): Appender[];
                getCriticalEvents(): LogEvent[];
                getErrCnt(): number;
                getLevel(): number;
                getWarnCnt(): number;
                hasErrors(): boolean;
                hasMessages(): boolean;
                hasWarnings(): boolean;
                info(msg: string, extraFields?: object): void;
                removeAppender(appender: Appender): void;
                reset(): void;
                setAppenders(appenders: Appender[]): void;
                toString(): string;
                trace(msg: string, extraFields?: object): void;
                warn(msg: string, extraFields?: object): void;
            }

            Implemented by

            Index

            Methods

            interface Logger {
                addAppender(appender: Appender): void;
                error(msg: string, extraFields?: object): void;
                exportState(): {
                    action: string;
                    criticalEvents: LogEvent[];
                    errorCount: number;
                    level: string;
                    warningCount: number;
                };
                getAction(): number;
                getAppenders(): Appender[];
                getCriticalEvents(): LogEvent[];
                getErrCnt(): number;
                getLevel(): number;
                getWarnCnt(): number;
                hasErrors(): boolean;
                hasMessages(): boolean;
                hasWarnings(): boolean;
                info(msg: string, extraFields?: object): void;
                removeAppender(appender: Appender): void;
                reset(): void;
                setAppenders(appenders: Appender[]): void;
                toString(): string;
                trace(msg: string, extraFields?: object): void;
                warn(msg: string, extraFields?: object): void;
            }

            Implemented by

            Index

            Methods

            addAppender error exportState getAction @@ -29,69 +29,69 @@ - The appender is already registered in the logger.

            Logger.setAppenders() for setting multiple appenders at once and for more details on the validation of the appenders.

            -
            • Sends an error log event with the provided message and optional extraFields to all appenders.

              +
            • Sends an error log event with the provided message and optional extraFields to all appenders.

              Parameters

              • msg: string

                The error message to log.

              • OptionalextraFields: object

                Optional structured data (object) to attach to the log event. May include metadata, context, etc.

              Returns void

              ScriptError if - The singleton instance is not available (not instantiated) - The logger is configured to exit on critical events.

              -
            • Exports the current state of the logger, including level, action, error/warning counts, and critical events.

              +
            • Exports the current state of the logger, including level, action, error/warning counts, and critical events.

              Returns {
                  action: string;
                  criticalEvents: LogEvent[];
                  errorCount: number;
                  level: string;
                  warningCount: number;
              }

              An object containing the logger's state.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Gets the current action setting for error/warning events.

              +
            • Gets the current action setting for error/warning events.

              Returns number

              The action value (e.g., CONTINUE or EXIT).

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Gets the array of appenders currently registered with the logger.

              Returns Appender[]

              An array of Appender instances.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Gets an array of all error and warning log events sent.

              Returns LogEvent[]

              An array of LogEvent objects representing error and warning events.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Gets the total number of error log events sent.

              +
            • Gets the total number of error log events sent.

              Returns number

              The count of error events.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Gets the current log level setting.

              Returns number

              The log level value (e.g., OFF, ERROR, WARN, INFO, TRACE). It refers to the level of verbosity to show during the logging process.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Gets the total number of warning log events sent.

              +
            • Gets the total number of warning log events sent.

              Returns number

              The count of warning events.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Checks if any error log events have been sent.

              +
            • Checks if any error log events have been sent.

              Returns boolean

              True if at least one error event has been sent; otherwise, false.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Checks if any error or warning log events have been sent.

              +
            • Checks if any error or warning log events have been sent.

              Returns boolean

              True if at least one error or warning event has been sent; otherwise, false.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Checks if any warning log events have been sent.

              +
            • Checks if any warning log events have been sent.

              Returns boolean

              True if at least one warning event has been sent; otherwise, false.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Sends an informational log event with the provided message and optional extraFields to all appenders.

              +
            • Sends an informational log event with the provided message and optional extraFields to all appenders.

              Parameters

              • msg: string

                The informational message to log.

              • OptionalextraFields: object

                Optional structured data (object) to attach to the log event.

              Returns void

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Removes an appender from the logger, if the resulting array of appenders appender is not empty.

              +
            • Removes an appender from the logger, if the resulting array of appenders appender is not empty.

              Parameters

              • appender: Appender

                The Appender instance to remove.

              Returns void

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Clears the logger's history of error and warning events and resets counters.

              +
            • Clears the logger's history of error and warning events and resets counters.

              Returns void

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Sets the array of appenders for the logger.

              Parameters

              • appenders: Appender[]

                The array of Appender instances to set.

              Returns void

              ScriptError if - The singleton instance is not available (not instantiated). - The resulting array doesn't contain unique implementations of Appender. - appender is null or undefined or has null or undefined elements

              -
            • Returns a string representation of the logger's state, including level, action, and message counts.

              +
            • Returns a string representation of the logger's state, including level, action, and message counts.

              Returns string

              A string describing the logger's current state.

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Sends a trace log event with the provided message and optional extraFields to all appenders.

              +
            • Sends a trace log event with the provided message and optional extraFields to all appenders.

              Parameters

              • msg: string

                The trace message to log.

              • OptionalextraFields: object

                Optional structured data (object) to attach to the log event.

              Returns void

              ScriptError if the singleton instance is not available (not instantiated).

              -
            • Sends a warning log event with the provided message and optional extraFields to all appenders.

              +
            • Sends a warning log event with the provided message and optional extraFields to all appenders.

              Parameters

              • msg: string

                The warning message to log.

              • OptionalextraFields: object

                Optional structured data (object) to attach to the log event.

              Returns void

              ScriptError if - The singleton instance is not available (not instantiated) - The logger is configured to exit on critical events.

              -
            +
            diff --git a/docs/typedoc/types/LayoutFormatter.html b/docs/typedoc/types/LayoutFormatter.html index b0e7c92..1fd4a5a 100644 --- a/docs/typedoc/types/LayoutFormatter.html +++ b/docs/typedoc/types/LayoutFormatter.html @@ -1 +1 @@ -LayoutFormatter | officescripts-logging-framework
            officescripts-logging-framework
              Preparing search index...

              Type Alias LayoutFormatter

              LayoutFormatter: (event: LogEvent) => string

              Type declaration

              +LayoutFormatter | officescripts-logging-framework
              officescripts-logging-framework
                Preparing search index...

                Type Alias LayoutFormatter

                LayoutFormatter: (event: LogEvent) => string

                Type declaration

                diff --git a/docs/typedoc/types/LogEventExtraFields.html b/docs/typedoc/types/LogEventExtraFields.html index 259f031..2c90836 100644 --- a/docs/typedoc/types/LogEventExtraFields.html +++ b/docs/typedoc/types/LogEventExtraFields.html @@ -1 +1 @@ -LogEventExtraFields | officescripts-logging-framework
                officescripts-logging-framework
                  Preparing search index...

                  Type Alias LogEventExtraFields

                  LogEventExtraFields: { [key: string]: string | number | Date | (() => string) }

                  Type declaration

                  • [key: string]: string | number | Date | (() => string)
                  +LogEventExtraFields | officescripts-logging-framework
                  officescripts-logging-framework
                    Preparing search index...

                    Type Alias LogEventExtraFields

                    LogEventExtraFields: { [key: string]: string | number | Date | (() => string) }

                    Type declaration

                    • [key: string]: string | number | Date | (() => string)
                    diff --git a/docs/typedoc/types/LogEventFactory.html b/docs/typedoc/types/LogEventFactory.html index 584fa09..d499b34 100644 --- a/docs/typedoc/types/LogEventFactory.html +++ b/docs/typedoc/types/LogEventFactory.html @@ -2,4 +2,4 @@

                    Type declaration

                    +
                    diff --git a/mocks/excelscript.mock.ts b/mocks/excelscript.mock.ts index 5d76aca..5956278 100644 --- a/mocks/excelscript.mock.ts +++ b/mocks/excelscript.mock.ts @@ -1,3 +1,4 @@ + // excelscript.mock.ts // // - This file provides a minimal mock implementation of the ExcelScript namespace for local testing of Office Scripts. @@ -89,7 +90,6 @@ export namespace ExcelScript { } // Make ExcelScript available globally for Node/ts-node test environments -if (typeof globalThis !== "undefined" && typeof ExcelScript !== "undefined") { - // @ts-ignore - globalThis.ExcelScript = ExcelScript; -} \ No newline at end of file +// Attach the mock ExcelScript namespace and environment flag to globalThis for compatibility with test environments (Node, ts-node, etc.) +(globalThis as any).ExcelScript = ExcelScript; +(globalThis as any).RunSyncTest = true; // If true, no need to force a delay for tests diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 47e35cd..b8959c9 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,6 +1,6 @@ { "name": "officescripts-logging-framework", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { @@ -235,9 +235,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", - "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "peer": true, diff --git a/node_modules/@types/node/README.md b/node_modules/@types/node/README.md index cdc792d..268fd23 100644 --- a/node_modules/@types/node/README.md +++ b/node_modules/@types/node/README.md @@ -8,7 +8,7 @@ This package contains type definitions for node (https://nodejs.org/). Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node. ### Additional Details - * Last updated: Wed, 11 Jun 2025 20:36:02 GMT + * Last updated: Mon, 16 Jun 2025 11:02:21 GMT * Dependencies: [undici-types](https://npmjs.com/package/undici-types) # Credits diff --git a/node_modules/@types/node/assert.d.ts b/node_modules/@types/node/assert.d.ts index 666682a..b79fc21 100644 --- a/node_modules/@types/node/assert.d.ts +++ b/node_modules/@types/node/assert.d.ts @@ -79,7 +79,9 @@ declare module "assert" { * @return A function that wraps `fn`. */ calls(exact?: number): () => void; - calls any>(fn?: Func, exact?: number): Func; + calls(fn: undefined, exact?: number): () => void; + calls any>(fn: Func, exact?: number): Func; + calls any>(fn?: Func, exact?: number): Func | (() => void); /** * Example: * diff --git a/node_modules/@types/node/fs.d.ts b/node_modules/@types/node/fs.d.ts index 9dde50d..4a3fa5a 100644 --- a/node_modules/@types/node/fs.d.ts +++ b/node_modules/@types/node/fs.d.ts @@ -2650,7 +2650,7 @@ declare module "fs" { buffer: TBuffer, offset: number, length: number, - position: number | null, + position: ReadPosition | null, ): Promise<{ bytesRead: number; buffer: TBuffer; diff --git a/node_modules/@types/node/fs/promises.d.ts b/node_modules/@types/node/fs/promises.d.ts index 6e26d85..cfc1691 100644 --- a/node_modules/@types/node/fs/promises.d.ts +++ b/node_modules/@types/node/fs/promises.d.ts @@ -29,6 +29,7 @@ declare module "fs/promises" { OpenDirOptions, OpenMode, PathLike, + ReadPosition, ReadStream, ReadVResult, RmDirOptions, @@ -69,7 +70,7 @@ declare module "fs/promises" { * @default `buffer.byteLength` */ length?: number | null; - position?: number | null; + position?: ReadPosition | null; } interface CreateReadStreamOptions extends Abortable { encoding?: BufferEncoding | null | undefined; @@ -229,7 +230,7 @@ declare module "fs/promises" { buffer: T, offset?: number | null, length?: number | null, - position?: number | null, + position?: ReadPosition | null, ): Promise>; read( buffer: T, diff --git a/node_modules/@types/node/package.json b/node_modules/@types/node/package.json index 4123aec..36b0d51 100644 --- a/node_modules/@types/node/package.json +++ b/node_modules/@types/node/package.json @@ -1,6 +1,6 @@ { "name": "@types/node", - "version": "24.0.1", + "version": "24.0.3", "description": "TypeScript definitions for node", "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node", "license": "MIT", @@ -230,6 +230,6 @@ "undici-types": "~7.8.0" }, "peerDependencies": {}, - "typesPublisherContentHash": "081849e52c12a334c50381c2152b738916516dc101987420b20c3a1e81c27c7c", + "typesPublisherContentHash": "50346638d9eb4e5a9d98e4739a4c05900502bd5d0a2347843c47ea6fc70a2638", "typeScriptVersion": "5.1" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 283d68b..0c1b5d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "officescripts-logging-framework", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "officescripts-logging-framework", - "version": "1.0.0", - "license": "GPL-3.0-or-later", + "version": "2.0.0", + "license": "MIT", "dependencies": { "acorn": "^8.14.1", "acorn-walk": "^8.3.4", @@ -260,9 +260,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", - "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 8a1c90f..a68ab9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "officescripts-logging-framework", - "version": "2.0.0", + "version": "2.1", "description": "Lightweight, extensible logging framework for Office Scripts, inspired by libraries like Log4j. Enables structured logging via a singleton 'Logger', supporting multiple log levels ('Logger.LEVEL') and pluggable output targets through the 'Appender' interface", "main": "index.js", "directories": { @@ -30,7 +30,7 @@ "setup": "npm install && npx tsc --init", "build": "npm run copy:ts && npm run strip:testonly", "copy:ts": "mkdir -p dist && rsync -av --include='*/' --include='*.ts' --exclude='*' src/ dist/", - "strip:testonly": "find dist/ -name \"*.ts\" -exec sed -i '' '/#TEST-ONLY-START/,/#TEST-ONLY-END/d' {} +", + "strip:testonly": "node scripts/strip-testonly.js", "test": "npx ts-node --project tsconfig.test.json wrappers/main-wrapper.ts", "debug": "npx ts-node --project tsconfig.test.json wrappers/main-wrapper.ts --debug", "doc:ts:install": "npm install --save-dev typedoc", diff --git a/scripts/strip-testonly.js b/scripts/strip-testonly.js new file mode 100644 index 0000000..60d8bfc --- /dev/null +++ b/scripts/strip-testonly.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const path = require('path'); + +function stripTestOnlyBlocks(file) { + let content = fs.readFileSync(file, 'utf8'); + // Remove blocks between #TEST-ONLY-START and #TEST-ONLY-END, inclusive + content = content.replace(/#TEST-ONLY-START[\s\S]*?#TEST-ONLY-END/g, ''); + fs.writeFileSync(file, content, 'utf8'); +} + +function walk(dir) { + fs.readdirSync(dir).forEach(file => { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + walk(fullPath); + } else if (fullPath.endsWith('.ts')) { + stripTestOnlyBlocks(fullPath); + } + }); +} + +walk(path.resolve(__dirname, '../dist')); \ No newline at end of file diff --git a/src/logger.ts b/src/logger.ts index fb6a58a..1579e92 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,3 +1,5 @@ + +// #region logger.ts // =============================================== // Lightweight logging Framework for Office Script // =============================================== @@ -32,11 +34,10 @@ * ``` * @remarks Designed and tested for Office Scripts runtime. Extendable with custom appenders via the 'Appender' interface. * @author David Leal - * version 2.0.0 + * version 2.1.0 * creation date: 2024-10-01 */ - // Enum DEFINITIONS // -------------------- @@ -72,14 +73,17 @@ enum LOG_EVENT { * @returns A LogEvent object. */ type LogEventFactory = (message: string, eventType: LOG_EVENT, extraFields?: LogEventExtraFields) => LogEvent -type LogEventExtraFields = { - [key: string]: string | number | Date | (() => string) +type LogEventExtraFields = { + [key: string]: string | number | Date | (() => string) } type LayoutFormatter = (event: LogEvent) => string + // #endregion enum and types + // INTERFACES // -------------------- + // #region INTERFACES /** @@ -104,32 +108,32 @@ interface LogEvent { * The event type from the LOG_EVENT enum. * This field is immutable and must be set at construction. */ - readonly type: LOG_EVENT; + readonly type: LOG_EVENT /** * The log message to be sent. * This field is immutable. It must not be null, undefined, or an empty string. */ - readonly message: string; + readonly message: string /** * The timestamp when the event was created. * This field is immutable and must be a valid Date instance. */ - readonly timestamp: Date; + readonly timestamp: Date /** * Additional metadata for the log event, for extension and contextual purposes. * This field is immutable and must be a plain object. * Intended for extensibility—avoid storing sensitive or large data here. */ - readonly extraFields: LogEventExtraFields; + readonly extraFields: LogEventExtraFields /** * Returns a string representation of the log event in a human-readable, single-line format, * including all relevant fields. */ - toString(): string; + toString(): string } /** @@ -370,11 +374,74 @@ interface Logger { toString(): string } + // #endregion INTERFACES // CLASSES // -------------------- + +// #region ScriptError +/** + * A custom error class for domain-specific or script-level exceptions. + * Designed to provide clarity and structure when handling expected or controlled + * failures in scripts (e.g., logging or validation errors). It supports error chaining + * through an optional 'cause' parameter, preserving the original stack trace. + * Prefer using 'ScriptError' for intentional business logic errors to distinguish them + * from unexpected system-level failures. + * @example + * ```ts + * const original = new Error("Missing field") + * throw new ScriptError("Validation failed", original) + * ``` + */ +class ScriptError extends Error { + /** + * Constructs a new 'ScriptError'. + * @param message A description of the error. + * @param cause (Optional) The original error that caused this one. + * If provided the exception message will have a refernece to the cause + */ + constructor(message: string, public cause?: Error) { + super(message) + this.name = new.target.name // Dinamically take the name of the class + if (cause?.message) + this.message += ` (caused by '${cause.constructor.name}' with message '${cause.message}')` + } + + /** + * Utility method to rethrow the deepest original cause if present, + * otherwise rethrows this 'ScriptError' itself. + * Useful for deferring a controlled exception and then + * surfacing the root cause explicitly. + */ + public rethrowCauseIfNeeded(): never { + if (this.cause instanceof ScriptError && typeof this.cause.rethrowCauseIfNeeded === "function") { + // Recursively rethrow the root cause if nested ScriptError + this.cause.rethrowCauseIfNeeded() + } else if (this.cause) { + // Rethrow the immediate cause if not a ScriptError + throw this.cause + } + // No cause, throw self + throw this + } + + /** Override toString() method. + * @returns The name and the message on the first line, then + * on the second line the Stack trace section name, i.e. 'Stack trace:'. + * Starting on the third line the stack trace information. + * If a cause was provided the stack trace will refer to the cause + * otherwise to the original exception. + */ + public toString(): string { + const stack = this.cause?.stack ? this.cause.stack : this.stack + return `${this.constructor.name}: ${this.message}\nStack trace:\n${stack}` + } +} +// #endregion ScriptError + + /** * Utility class providing static helper methods for logging operations. */ @@ -382,20 +449,20 @@ class Utility { /**Helpder to format the local date as a string. Ouptut in standard format: YYYY-MM-DD HH:mm:ss,SSS */ public static date2Str(date: Date): string { - // Defensive: handle null, undefined, or non-Date input - if (!(date instanceof Date) || isNaN(date.getTime())) { - const PREFIX = `[${Utility.name}.date2Str]: ` - return `${PREFIX}Invalid Date` - } - const pad = (n: number, width = 2) => n.toString().padStart(width, '0') - return `${date.getFullYear()}-${pad(date.getMonth() + 1) - }-${pad(date.getDate()) - } ${pad(date.getHours()) - }:${pad(date.getMinutes()) - }:${pad(date.getSeconds()) - },${pad(date.getMilliseconds(), 3) - }`; -} + // Defensive: handle null, undefined, or non-Date input + if (!(date instanceof Date) || isNaN(date.getTime())) { + const PREFIX = `[${Utility.name}.date2Str]: ` + return `${PREFIX}Invalid Date` + } + const pad = (n: number, width = 2) => n.toString().padStart(width, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1) + }-${pad(date.getDate()) + } ${pad(date.getHours()) + }:${pad(date.getMinutes()) + }:${pad(date.getSeconds()) + },${pad(date.getMilliseconds(), 3) + }`; + } /** Helper method to check for an empty array. */ public static isEmptyArray(arr: T[]): boolean { @@ -410,18 +477,19 @@ class Utility { * @throws ScriptError if the log event factory is not a function. */ public static validateLogEventFactory( - factory: unknown, // or Function, or your specific function type - funName?: string, - context?: string -): void { - const PREFIX = context ? `[${context}]: ` : ''; - if (typeof factory !== 'function') { - throw new ScriptError(`${PREFIX}Invalid ${funName || ""}: Not a function`); + factory: unknown, // or Function, or your specific function type + funName?: string, + context?: string + ): void { + const PREFIX = context ? `[${context}]: ` : ''; + if (typeof factory !== 'function') { + throw new ScriptError(`${PREFIX}Invalid ${funName || ""}: Not a function`); + } } -} } + // #region LogEventImpl /** * Implements the LogEvent interface, providing a concrete representation of a log event. @@ -448,7 +516,7 @@ class LogEventImpl implements LogEvent { * @throws ScriptError if validation fails. */ constructor(message: string, type: LOG_EVENT, extraFields?: LogEventExtraFields, timestamp: Date = new Date(), - ) { + ) { LogEventImpl.validateLogEventAttrs({ type: type, message, timestamp }, extraFields, "LogEventImpl.constructor") this._type = type this._message = message @@ -522,7 +590,7 @@ class LogEventImpl implements LogEvent { public toString(): string { const sDATE = Utility.date2Str(this._timestamp) //Local date as string // Get the string representation of the type, don't use LogEventImpl.eventTypeToLabel(this.type) to avoid unnecesary validation - const sTYPE = LOG_EVENT[this.type] + const sTYPE = LOG_EVENT[this.type] const BASE = `${this.constructor.name}: {timestamp="${sDATE}", type="${sTYPE}", message="${this._message}"` const HAS_EXTRA = Object.keys(this._extraFields).length > 0 const EXTRA = HAS_EXTRA ? `, extraFields=${JSON.stringify(this.extraFields)}` : '' @@ -540,61 +608,62 @@ class LogEventImpl implements LogEvent { return `${LOG_EVENT[type]}` } -/** - * Validates the raw attributes for a log event, including extraFields if provided. - * @param attrs An object containing the core attributes: type, message, timestamp. - * @param extraFields Optional object containing additional fields to validate. - * @param context Optional string for error context prefixing. - * @throws ScriptError if any of the attributes are not valid. - */ -private static validateLogEventAttrs( - attrs: { type: unknown, message: unknown, timestamp: unknown }, - extraFields?: unknown, - context?: string -): void { - const PREFIX = context ? `[${context}]: ` : `[${LogEventImpl.name}.validateLogEventAttrs]: `; - - // Validate type - if (typeof attrs.type !== 'number') { - throw new ScriptError(`${PREFIX}LogEvent.type='${attrs.type}' property must be a number (LOG_EVENT enum value).`); - } - if (!Object.values(LOG_EVENT).includes(attrs.type as LOG_EVENT)) { - throw new ScriptError(`${PREFIX}LogEvent.type='${attrs.type}' property is not defined in the LOG_EVENT enum.`); - } - - // Validate message - if (typeof attrs.message !== 'string') { - throw new ScriptError(`${PREFIX}LogEvent.message='${attrs.message}' property must be a string.`); - } - if (attrs.message.trim().length === 0) { - throw new ScriptError(`${PREFIX}LogEvent.message cannot be empty.`); - } - - // Validate timestamp - if (!(attrs.timestamp instanceof Date)) { - throw new ScriptError(`${PREFIX}LogEvent.timestamp='${attrs.timestamp}' property must be a Date.`); - } - - // Validate extraFields if provided - if (extraFields !== undefined) { - if (typeof extraFields !== "object" || extraFields === null || Array.isArray(extraFields)) { - throw new ScriptError(`${PREFIX}extraFields must be a plain object.`); + /** + * Validates the raw attributes for a log event, including extraFields if provided. + * @param attrs An object containing the core attributes: type, message, timestamp. + * @param extraFields Optional object containing additional fields to validate. + * @param context Optional string for error context prefixing. + * @throws ScriptError if any of the attributes are not valid. + */ + private static validateLogEventAttrs( + attrs: { type: unknown, message: unknown, timestamp: unknown }, + extraFields?: unknown, + context?: string + ): void { + const PREFIX = context ? `[${context}]: ` : `[${LogEventImpl.name}.validateLogEventAttrs]: `; + + // Validate type + if (typeof attrs.type !== 'number') { + throw new ScriptError(`${PREFIX}LogEvent.type='${attrs.type}' property must be a number (LOG_EVENT enum value).`); + } + if (!Object.values(LOG_EVENT).includes(attrs.type as LOG_EVENT)) { + throw new ScriptError(`${PREFIX}LogEvent.type='${attrs.type}' property is not defined in the LOG_EVENT enum.`); } - for (const [k, v] of Object.entries(extraFields)) { - if (v === undefined) { - throw new ScriptError(`${PREFIX}extraFields[${k}] must not be undefined.`); + + // Validate message + if (typeof attrs.message !== 'string') { + throw new ScriptError(`${PREFIX}LogEvent.message='${attrs.message}' property must be a string.`); + } + if (attrs.message.trim().length === 0) { + throw new ScriptError(`${PREFIX}LogEvent.message cannot be empty.`); + } + + // Validate timestamp + if (!(attrs.timestamp instanceof Date)) { + throw new ScriptError(`${PREFIX}LogEvent.timestamp='${attrs.timestamp}' property must be a Date.`); + } + + // Validate extraFields if provided + if (extraFields !== undefined) { + if (typeof extraFields !== "object" || extraFields === null || Array.isArray(extraFields)) { + throw new ScriptError(`${PREFIX}extraFields must be a plain object.`); } - if (typeof v !== "string" && typeof v !== "number" && - !(v instanceof Date) && typeof v !== "function") { - throw new ScriptError(`${PREFIX}extraFields[${k}] has invalid type: ${typeof v}. Must be string, number, Date, or function.`); + for (const [k, v] of Object.entries(extraFields)) { + if (v === undefined) { + throw new ScriptError(`${PREFIX}extraFields[${k}] must not be undefined.`); + } + if (typeof v !== "string" && typeof v !== "number" && + !(v instanceof Date) && typeof v !== "function") { + throw new ScriptError(`${PREFIX}extraFields[${k}] has invalid type: ${typeof v}. Must be string, number, Date, or function.`); + } } } } -} } // #endregion LogEventImpl + // #region LayoutImpl /** * Default implementation of the 'Layout' interface. @@ -607,38 +676,16 @@ private static validateLogEventAttrs( */ class LayoutImpl implements Layout { - /** - * Convenience public constant to help users to define a short format for log events. - * Formats a log event as a short string as follows '[type] message'. - * If extraFields are present in the event, they will be appended as a JSON object (surrounded by braces) to the output. - * Example: [ERROR] Something bad happened {"user":"dlealv","id":42} - * Defined as a named function to ensure toString() returns the function name. - */ -public static readonly shortFormatterFun = Object.freeze(function shortLayoutFormatterFun(event: LogEvent): string { - const sType = LOG_EVENT[event.type] - let extraFieldsStr = "" - if (event.extraFields && Object.keys(event.extraFields).length > 0) { - extraFieldsStr = ` ${JSON.stringify(event.extraFields)}` // JSON.stringify includes the braces - } - return `[${sType}] ${event.message}${extraFieldsStr}` -}) - /** - * Default formatter function. Created as a named function. Formats a log event as [timestamp] [type] message. - * The timestamp is formatted as YYYY-MM-DD HH:mm:ss,SSS. - * If extraFields are present in the event, they will be appended as a JSON object (surrounded by braces) to the output. - * Example: [2025-06-19 15:06:41,123] [ERROR] Something bad happened {"user":"dlealv","id":42} - * Defined as a named function to ensure toString() returns the function name. - */ -public static readonly defaultFormatterFun = Object.freeze(function defaultLayoutFormatterFun(event: LogEvent): string { - const sDATE = Utility.date2Str(event.timestamp) - const sType = LOG_EVENT[event.type] - let extraFieldsStr = "" - if (event.extraFields && Object.keys(event.extraFields).length > 0) { - extraFieldsStr = ` ${JSON.stringify(event.extraFields)}` // JSON.stringify includes the braces - } - return `[${sDATE}] [${sType}] ${event.message}${extraFieldsStr}` -}) + /**Convninience static property to define a short formatter. + * @see LayoutImpl.shortFormatterFun + */ + public static shortFormatterFun: LayoutFormatter + + /**Convninient static property to define a long formatter used as default. + * @see LayoutImpl.defaultFormatterFun + */ + public static defaultFormatterFun: LayoutFormatter /** * Function used to convert a LogEvent into a string. @@ -723,137 +770,101 @@ public static readonly defaultFormatterFun = Object.freeze(function defaultLayou * - format is not a function or doesn't have arity 1 * - _formatter is present and is missing, not a function, or doesn't have arity 1 */ - public static validateLayout(layout: unknown, context?: string): asserts layout is Layout { - const PREFIX = context ? `[${context}]: ` : `[LayoutImpl.validateLayout]: ` - const FORMAT_ERROR_MSG = - "Invalid Layout: The 'format' method must be a function accepting a single LogEvent argument. " + - 'Example: event => "[type] " + event.message. See LayoutImpl documentation for usage.' - + static validateLayout(layout: Layout, context?: string) { + const PREFIX = context ? `[${context}]: ` : "[LayoutImpl.validateLayout]: " if (!layout || typeof layout !== "object") { - throw new ScriptError(`${PREFIX}Invalid Layout: layout object is null or undefined`) + throw new ScriptError(PREFIX + "Invalid Layout: layout object is null or undefined") } - const maybeLayout = layout as Record - // Validate public 'format' method exists and has correct signature - if (typeof maybeLayout.format !== "function" || (maybeLayout.format as Function).length !== 1) { + if (typeof layout.format !== "function" || layout.format.length !== 1) { throw new ScriptError( - `${PREFIX}${FORMAT_ERROR_MSG} Got: type="${typeof maybeLayout.format}", arity=${maybeLayout.format && typeof maybeLayout.format === "function" ? (maybeLayout.format as Function).length : "N/A"}` - ) + `{PREFIX} Invalid Layout: The 'format' method must be a function accepting a single LogEvent argument. ` + + `See LayoutImpl documentation for usage.` + ); } - // If _formatter property exists, validate it using the helper - if ("_formatter" in maybeLayout) { - LayoutImpl.validateFormatter(maybeLayout._formatter, context) + if (layout instanceof LayoutImpl) { + LayoutImpl.validateFormatter(layout._formatter, context) } } /** - * Validates that the provided value is a valid formatter function - * for use in LayoutImpl (_formatter property). The formatter must be - * a function accepting a single LogEvent argument and must return a non-empty, non-null string. - * - * @param formatter - The candidate formatter function to validate - * @param context - (Optional) Additional context for error messages - * @throws ScriptError if formatter is missing, not a function, doesn't have arity 1, - * or returns null/empty string for a sample event. - */ - private static validateFormatter(formatter: unknown, context?: string) { - const PREFIX = context ? `[${context}]: ` : `[${LayoutImpl.name}.validateFormatter]: ` - const FORMATTER_ERROR_MSG = - "Invalid Layout: The internal '_formatter' property must be a function accepting a single LogEvent argument. " + - 'Example: event => "[type] " + event.message. This is typically set in the LayoutImpl constructor. ' + - 'See LayoutImpl documentation for usage.' - - // 1. Type & arity check - if (typeof formatter !== "function" || (formatter as Function).length !== 1) { + * Validates that the provided value is a valid formatter function + * for use in LayoutImpl (_formatter property). The formatter must be + * a function accepting a single LogEvent argument and must return a non-empty, non-null string. + * + * @param formatter - The candidate formatter function to validate + * @param context - (Optional) Additional context for error messages + * @throws ScriptError if formatter is missing, not a function, doesn't have arity 1, + * or returns null/empty string for a sample event. + */ + static validateFormatter(formatter: LayoutFormatter, context?: string) { + const PREFIX = context ? `[${context}]: ` : "[LayoutImpl.validateFormatter]: "; + if (typeof formatter !== "function" || formatter.length !== 1) { throw new ScriptError( - `${PREFIX}${FORMATTER_ERROR_MSG} Got: type="${typeof formatter}", arity=${formatter && typeof formatter === "function" ? (formatter as Function).length : "N/A"}` - ) + PREFIX + + "Invalid Layout: The internal '_formatter' property must be a function accepting a single LogEvent argument. See LayoutImpl documentation for usage." + ); } - - // 2. Output check: ensure the function returns a non-null, non-empty string for a valid LogEvent - try { - const mockEvent: LogEvent = { - timestamp: new Date(), - type: (typeof LOG_EVENT === "object" ? Object.values(LOG_EVENT)[0] : 0) as LOG_EVENT, - message: "__validateFormatterTest__", - // Add extraFields if your LogEvent type supports it - extraFields: { - user: "test-user", - session: "mock-session" - } - } - const result = (formatter as LayoutFormatter)(mockEvent) - if (typeof result !== "string" || result === "" || result == null) { - throw new ScriptError( - `${PREFIX}Formatter function must return a non-empty string for a valid LogEvent. Got: ${result === "" ? "empty string" : String(result)}` - ) - } - } catch (e) { - throw new ScriptError(`${PREFIX}Formatter function threw an error when formatting a valid LogEvent. Error: ${e instanceof Error ? e.message : String(e)}`) + // Try calling with a mock event + const mockEvent: LogEvent = { + type: LOG_EVENT.INFO, + message: "test", + timestamp: new Date(), + extraFields: {}, + }; + const result = formatter(mockEvent); + if ( + typeof result !== "string" || + result === "" || + result == null + ) { + throw new ScriptError( + PREFIX + + "Formatter function must return a non-empty string for a valid LogEvent. Got: " + + (result === "" ? "empty string" : String(result)) + ); } } } -// #endregion LayoutImpl -// #region ScriptError +// Assign the static formatters outside the class /** - * A custom error class for domain-specific or script-level exceptions. - * Designed to provide clarity and structure when handling expected or controlled - * failures in scripts (e.g., logging or validation errors). It supports error chaining - * through an optional 'cause' parameter, preserving the original stack trace. - * Prefer using 'ScriptError' for intentional business logic errors to distinguish them - * from unexpected system-level failures. - * @example - * ```ts - * const original = new Error("Missing field") - * throw new ScriptError("Validation failed", original) - * ``` + * Convenience public constant to help users to define a short format for log events. + * Formats a log event as a short string as follows '[type] message'. + * If extraFields are present in the event, they will be appended as a JSON object (surrounded by braces) to the output. + * Example: [ERROR] Something bad happened {"user":"dlealv","id":42} + * Defined as a named function to ensure toString() returns the function name. */ -class ScriptError extends Error { - /** - * Constructs a new 'ScriptError'. - * @param message A description of the error. - * @param cause (Optional) The original error that caused this one. - * If provided the exception message will have a refernece to the cause - */ - constructor(message: string, public cause?: Error) { - super(message) - this.name = new.target.name // Dinamically take the name of the class - if (cause?.message) - this.message += ` (caused by '${cause.constructor.name}' with message '${cause.message}')` - } - /** - * Utility method to rethrow the deepest original cause if present, - * otherwise rethrows this 'ScriptError' itself. - * Useful for deferring a controlled exception and then - * surfacing the root cause explicitly. - */ - public rethrowCauseIfNeeded(): never { - if (this.cause instanceof ScriptError && typeof this.cause.rethrowCauseIfNeeded === "function") { - // Recursively rethrow the root cause if nested ScriptError - this.cause.rethrowCauseIfNeeded() - } else if (this.cause) { - // Rethrow the immediate cause if not a ScriptError - throw this.cause - } - // No cause, throw self - throw this +LayoutImpl.shortFormatterFun = Object.freeze(function shortLayoutFormatterFun(event: LogEvent): string { + const sType = LOG_EVENT[event.type] + let extraFieldsStr = "" + if (event.extraFields && Object.keys(event.extraFields).length > 0) { + extraFieldsStr = ` ${JSON.stringify(event.extraFields)}` // JSON.stringify includes the braces } + return `[${sType}] ${event.message}${extraFieldsStr}` +}) - /** Override toString() method. - * @returns The name and the message on the first line, then - * on the second line the Stack trace section name, i.e. 'Stack trace:'. - * Starting on the third line the stack trace information. - * If a cause was provided the stack trace will refer to the cause - * otherwise to the original exception. - */ - public toString(): string { - const stack = this.cause?.stack ? this.cause.stack : this.stack - return `${this.constructor.name}: ${this.message}\nStack trace:\n${stack}` +/** + * Default formatter function. Created as a named function. Formats a log event as [timestamp] [type] message. + * The timestamp is formatted as YYYY-MM-DD HH:mm:ss,SSS. + * If extraFields are present in the event, they will be appended as a JSON object (surrounded by braces) to the output. + * Example: [2025-06-19 15:06:41,123] [ERROR] Something bad happened {"user":"dlealv","id":42} + * Defined as a named function to ensure toString() returns the function name. + */ + +LayoutImpl.defaultFormatterFun = Object.freeze(function defaultLayoutFormatterFun(event: LogEvent): string { + const sDATE = Utility.date2Str(event.timestamp) + const sType = LOG_EVENT[event.type] + let extraFieldsStr = "" + if (event.extraFields && Object.keys(event.extraFields).length > 0) { + extraFieldsStr = ` ${JSON.stringify(event.extraFields)}` // JSON.stringify includes the braces } -} -// #endregion ScriptError + return `[${sDATE}] [${sType}] ${event.message}${extraFieldsStr}` +}) + +// #endregion LayoutImpl + // #region AbstractAppender /** @@ -871,23 +882,18 @@ class ScriptError extends Error { */ abstract class AbstractAppender implements Appender { // Default factory function to create LogEvent instances, if was not set before. - private static readonly _defaultLogEventFactoryFun: LogEventFactory = Object.freeze( - function defaultLogEventFactoryFun(message: string, eventType: LOG_EVENT, extraFields?: LogEventExtraFields) { - return new LogEventImpl(message, eventType, extraFields) - } - ) + public static defaultLogEventFactoryFun: LogEventFactory // Static layout shared by all events - private static _layout: Layout | null = null + private static _layout: Layout | null = null // Static log event factory function used to create LogEvent instances. - - private static _logEventFactory: LogEventFactory | null = null + private static _logEventFactory: LogEventFactory | null = null private _lastLogEvent: LogEvent | null = null // The last event sent by the appender /** * Constructs a new AbstractAppender instance. Nothing is initialized, because the class only has static properties * that are lazy initialized or set by the user. */ - protected constructor() {} + protected constructor() { } /** * @returns The layout associated to all events. Used to format the log event before sending it to the appenders. @@ -918,7 +924,7 @@ abstract class AbstractAppender implements Appender { */ public static setLogEventFactory(logEventFactory: LogEventFactory): void { if (!AbstractAppender._logEventFactory) { - AbstractAppender.validateLogEventFactory(logEventFactory, "logEventFactory", "AbstractAppender.setLogEventFactory") + AbstractAppender.validateLogEventFactory(logEventFactory, "logEventFactory", "AbstractAppender.setLogEventFactory") AbstractAppender._logEventFactory = logEventFactory } } @@ -928,7 +934,7 @@ abstract class AbstractAppender implements Appender { */ public static getLogEventFactory(): LogEventFactory { if (!AbstractAppender._logEventFactory) { - AbstractAppender._logEventFactory = AbstractAppender._defaultLogEventFactoryFun // Default factory if not set + AbstractAppender._logEventFactory = AbstractAppender.defaultLogEventFactoryFun // Default factory if not set } return AbstractAppender._logEventFactory } @@ -953,6 +959,7 @@ abstract class AbstractAppender implements Appender { * @param arg3 - extraFields, only used if arg1 is a string. * @override */ + public log(arg1: LogEvent | string, arg2?: LOG_EVENT, arg3?: LogEventExtraFields): void { const CONTEXT = `AbstractAppender.log` const PREFIX = `[${CONTEXT}]: ` @@ -999,7 +1006,8 @@ abstract class AbstractAppender implements Appender { */ public toString(): string { const NAME = "AbstractAppender" // since it can be extended, we use the class name as literal - const LAYOUT_STR = AbstractAppender._layout ? AbstractAppender._layout.toString() : "null" + const LAYOUT = AbstractAppender._layout + const LAYOUT_STR = LAYOUT ? LAYOUT.toString() : "null" const FACTORY_STR = AbstractAppender._logEventFactory ? AbstractAppender._logEventFactory.name || "anonymous" : "null" const LAST_LOG_EVENT_STR = this._lastLogEvent ? this._lastLogEvent.toString() : "null" return `${NAME}: {layout=${LAYOUT_STR}, logEventFactory="${FACTORY_STR}", lastLogEvent=${LAST_LOG_EVENT_STR}}` @@ -1055,8 +1063,17 @@ abstract class AbstractAppender implements Appender { } } + +// Functions outside of the class: +AbstractAppender.defaultLogEventFactoryFun = Object.freeze( + function defaultLogEventFactoryFun(message: string, eventType: LOG_EVENT, extraFields?: LogEventExtraFields) { + return new LogEventImpl(message, eventType, extraFields) + } +) + // #endregion AbstractAppender + // #region ConsoleAppender /** * Singleton appender that logs messages to the Office Script console. It is used as default appender, @@ -1140,13 +1157,14 @@ class ConsoleAppender extends AbstractAppender implements Appender { // format the output using the layout that gets lazy initialized if it was not set before const MSG = AbstractAppender.getLayout().format(event) console.log(MSG) + } - /** @internal - * Common safeguard method, where calling initIfNeeded doesn't make sense. - * @param context - (Optional) A string to provide additional context in case of an error. - * @throws ScriptError In case the singleton was not initialized. - */ + /** @internal + * Common safeguard method, where calling initIfNeeded doesn't make sense. + * @param context - (Optional) A string to provide additional context in case of an error. + * @throws ScriptError In case the singleton was not initialized. + */ private static validateInstance(context?: string): void { if (!ConsoleAppender._instance) { const PREFIX = context ? `[${context}]: ` : `[${ConsoleAppender.name}.validateInstance]: ` @@ -1156,8 +1174,10 @@ class ConsoleAppender extends AbstractAppender implements Appender { } } + // #endregion ConsoleAppender + // #region ExcelAppender /** * Singleton appender that logs messages to a specified Excel cell. @@ -1174,55 +1194,69 @@ class ConsoleAppender extends AbstractAppender implements Appender { * ``` */ class ExcelAppender extends AbstractAppender implements Appender { - private static readonly _DEFAULT_COLOR_MAP = Object.freeze({ + /** + * Default colors for log events, used if no custom colors are provided. + * These colors are defined as hex strings (without the # prefix). + * The colors can be customized by passing a map of LOG_EVENT types to hex color strings + * when calling getInstance(). Default colors are: + * - ERROR: "9c0006" (red) + * - WARN: "ed7d31" (orange) + * - INFO: "548235" (green) + * - TRACE: "7f7f7f" (gray) + */ + public static readonly DEFAULT_EVENT_FONTS = Object.freeze({ [LOG_EVENT.ERROR]: "9c0006", // RED [LOG_EVENT.WARN]: "ed7d31", // ORANGE [LOG_EVENT.INFO]: "548235", // GREEN [LOG_EVENT.TRACE]: "7f7f7f" // GRAY } as const); - // Static map to associate LOG_EVENT types with font input argument from getInstance() - private static readonly FONT_LABEL_MAP = Object.freeze({ - [LOG_EVENT.ERROR]: "errFont", - [LOG_EVENT.WARN]: "warnFont", - [LOG_EVENT.INFO]: "infoFont", - [LOG_EVENT.TRACE]: "traceFont", - } as const); - /** - * Instance-level color map for current appender configuration. - * Maps LOG_EVENT types to hex color strings. + * Instance-level font map for current appender configuration. + * Maps LOG_EVENT types to hex font strings. */ - private readonly colorMap: Record; + private readonly _eventFonts: Record - /* Regular expression to validate hexadecimal colors */ - private static readonly HEX_REGEX = Object.freeze(/^#?[0-9A-Fa-f]{6}$/); + /* Regular expression to validate hexadecimal fonts */ + private static readonly HEX_REGEX = Object.freeze(/^#?[0-9A-Fa-f]{6}$/) private static _instance: ExcelAppender | null = null; // Instance of the singleton pattern - private readonly _msgCellRng: ExcelScript.Range; + private readonly _msgCellRng: ExcelScript.Range + /* Required for Office Script limitation, only use getAddress, the first time _msgCellRng is assigned, then + use this property. Calling this._msgCellRng.getAddress() fails in toString(). The workaround is to create + this artificial property. */ + private _msgCellRngAddress: string - // Private constructor to prevent user invocation - private constructor( - msgCellRng: ExcelScript.Range, - errFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.ERROR], - warnFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.WARN], - infoFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.INFO], - traceFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.TRACE] + /** + * Private constructor to prevent user invocation. + * @remarks Office Script limitation. Cannot call ExcelScript API methods on Office objects inside a class constructor, instead + * we do such API calls in the getInstance() method. + */ + private constructor(msgCellRng: ExcelScript.Range, eventFonts: Record = ExcelAppender.DEFAULT_EVENT_FONTS ) { - super(); - this._msgCellRng = msgCellRng; - this._msgCellRng.getFormat().setVerticalAlignment(ExcelScript.VerticalAlignment.center); - this.clearCellIfNotEmpty(); // Clear the cell if it has a value - this.colorMap = { - [LOG_EVENT.ERROR]: errFont, - [LOG_EVENT.WARN]: warnFont, - [LOG_EVENT.INFO]: infoFont, - [LOG_EVENT.TRACE]: traceFont - }; - // Set the default layout if not set - if (!AbstractAppender.getLayout()) { - AbstractAppender.setLayout(new LayoutImpl()); // Default layout if not set - } + super() + this._msgCellRng = msgCellRng + this.clearCellIfNotEmpty() // it can't be called in the construtor due to Office Script limitations + this._eventFonts = eventFonts + } + + // Setters and getters for the private properties + /** + * Returns the map of event types to font colors used by this appender. + * @returns A defensive copy of the event fonts map. + * @remarks The keys are LOG_EVENT enum values, and the values are hex color strings. + */ + public getEventFonts(): Record { + return { ...this._eventFonts }; // Defensive copy + } + + /** + * Returns the Excel range where log messages are written. + * @returns The ExcelScript.Range object representing the message cell range. + * @remarks This is the cell where log messages will be displayed. + */ + public getMsgCellRng(): ExcelScript.Range { + return { ...this._msgCellRng } } /** @@ -1232,10 +1266,10 @@ class ExcelAppender extends AbstractAppender implements Appender { * and return the existing instance. * @param msgCellRng - Excel range where log messages will be written. Must be a single cell and * not null or undefined. - * @param errFont - Hex color code for error messages (default: "9c0006" red). - * @param warnFont - Hex color code for warnings (default: "ed7d31" orange). - * @param infoFont - Hex color code for info messages (default: "548235" green). - * @param traceFont - Hex color code for trace messages (default: "7f7f7f" gray). + * @param eventFonts - Optional. A map of LOG_EVENT types to hex color codes for the font colors. + * If not provided, defaults to the predefined colors in DEFAULT_EVENT_FONTS. + * The user can provide just the colors they want to customize, + * the rest will use the default colors. * @returns The singleton ExcelAppender instance. * @throws ScriptError if msgCellRng was not defined or if the range covers multiple cells * or if it is not a valid Excel range. @@ -1246,24 +1280,20 @@ class ExcelAppender extends AbstractAppender implements Appender { * const excelAppender = ExcelAppender.getInstance(range) * ExcelAppender.getInstance(range, "ff0000") // ignored if called again * ``` + * @see DEFAULT_EVENT_FONTS */ public static getInstance( - msgCellRng: ExcelScript.Range, - errFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.ERROR], - warnFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.WARN], - infoFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.INFO], - traceFont: string = ExcelAppender._DEFAULT_COLOR_MAP[LOG_EVENT.TRACE] + msgCellRng: ExcelScript.Range, eventFonts: Record = ExcelAppender.DEFAULT_EVENT_FONTS ): ExcelAppender { const PREFIX = `[${ExcelAppender.name}.getInstance]: ` - ExcelAppender.validateLogEventMappings() // Validate the static color map if (!ExcelAppender._instance) { if (!msgCellRng || !msgCellRng.setValue) { const MSG = `${PREFIX}A valid ExcelScript.Range for input argument msgCellRng is required.`; - throw new ScriptError(MSG); + throw new ScriptError(MSG) } if (msgCellRng.getCellCount() != 1) { const MSG = `${PREFIX}Input argument msgCellRng must represent a single Excel cell.`; - throw new ScriptError(MSG); + throw new ScriptError(MSG) } // Enhanced Excel Range check in getInstance: if (!msgCellRng || typeof msgCellRng.setValue !== "function" || @@ -1274,14 +1304,24 @@ class ExcelAppender extends AbstractAppender implements Appender { throw new ScriptError(MSG) } // Checking valid hexadecimal color - const CONTEXT = `${ExcelAppender.name}.getInstance` - ExcelAppender.validateColor(errFont, "error", CONTEXT); - ExcelAppender.validateColor(warnFont, "warning", CONTEXT); - ExcelAppender.validateColor(infoFont, "info", CONTEXT); - ExcelAppender.validateColor(traceFont, "trace", CONTEXT); - ExcelAppender._instance = new ExcelAppender(msgCellRng, errFont, warnFont, infoFont, traceFont); + ExcelAppender.validateLogEventMappings() // Validate all LOG_EVENT mappings for fonts + const CONTEXT = `${ExcelAppender.name}.getInstance`; + // Merge defaults with user-provided values (user takes precedence) + const fonts: Record = { + ...ExcelAppender.DEFAULT_EVENT_FONTS, + ...(eventFonts ?? {}) + }; + for (const [event, font] of Object.entries(fonts)) { + const label = LOG_EVENT[Number(event)]; + ExcelAppender.validateFont(font, label, CONTEXT) + } + ExcelAppender._instance = new ExcelAppender(msgCellRng, fonts) + // Invoking Office Script API method, can't called in the `constructor` due to Office Script limitations + ExcelAppender._instance.clearCellIfNotEmpty("") + ExcelAppender._instance._msgCellRngAddress = msgCellRng.getAddress() // Store the address of the range for later use + ExcelAppender._instance._msgCellRng.getFormat().setVerticalAlignment(ExcelScript.VerticalAlignment.center) } - return ExcelAppender._instance; + return ExcelAppender._instance } // #TEST-ONLY-START @@ -1315,16 +1355,15 @@ class ExcelAppender extends AbstractAppender implements Appender { * @throws ScriptError, if the singleton was not instantiated. */ public toString(): string { - ExcelAppender.validateInstance("ExcelAppender.toString") // Validate the instance; + ExcelAppender.validateInstance("ExcelAppender.toString") const NAME = this.constructor.name - const ADDRESS = this._msgCellRng.getAddress() - // Present the color map in the output as "event colors" - const EVENT_COLORS = Object.entries(this.colorMap).map( + // Use enum reverse mapping for label + const EVENT_COLORS = Object.entries(this._eventFonts).map( ([key, value]) => - `${ExcelAppender.FONT_LABEL_MAP[Number(key) as LOG_EVENT]}="${value}"` + `${LOG_EVENT[Number(key)]}="${value}"` ).join(","); - const output = `${super.toString()} ${NAME}: {msgCellRng(address)="${ADDRESS}", ` - + `event fonts(map)={${EVENT_COLORS}}}`; + const output = `${super.toString()} ${NAME}: {msgCellRng(address)="${this._msgCellRngAddress}", ` + + `eventfonts={${EVENT_COLORS}}}` return output } @@ -1333,12 +1372,13 @@ class ExcelAppender extends AbstractAppender implements Appender { * if not font was defined it doesn't change the font of the cell. * @param event a value from enum LOG_EVENT. * @throws ScriptError in case event is not a valid LOG_EVENT enum value. + * @override */ protected sendEvent(event: LogEvent, context?: string): void { const CTX = context ? context : `${this.constructor.name}.sendEvent` ExcelAppender.validateInstance(CTX) LogEventImpl.validateLogEvent(event, CTX) // Validate the event - const FONT = this.colorMap[event.type] ?? null + const FONT = this._eventFonts[event.type] ?? null // If no color defined for event type, use default font color (do not throw) if (FONT) { this._msgCellRng.getFormat().getFont().setColor(FONT) @@ -1352,11 +1392,11 @@ class ExcelAppender extends AbstractAppender implements Appender { // Common safeguard method private static validateInstance(context?: string): void { - const PREFIX = context ? `[${context}]: ` : `[${ExcelAppender.name}]: `; + const PREFIX = context ? `[${context}]: ` : `[${ExcelAppender.name}]: ` // If the instance is not defined, throw an error if (!ExcelAppender._instance) { const MSG = `${PREFIX}A singleton instance can't be undefined or null. Please invoke getInstance first`; - throw new ScriptError(MSG); + throw new ScriptError(MSG) } } @@ -1370,7 +1410,7 @@ class ExcelAppender extends AbstractAppender implements Appender { * @remarks The color must be in 'RRGGBB' or '#RRGGBB' format. * If the color is not valid, it throws a ScriptError with a message indicating the issue. */ - private static validateColor(color: string, name: string, context?: string): void { + private static validateFont(color: string, name: string, context?: string): void { const PREFIX = context ? `[${context}]: ` : `[${ExcelAppender.name}.assertColor]: ` if (typeof color !== "string" || !color) { const MSG = `${PREFIX}The input value '${color}' for '${name}' event is missing or not a string. Please provide a 6-digit hexadecimal color as 'RRGGBB' or '#RRGGBB'.` @@ -1382,8 +1422,21 @@ class ExcelAppender extends AbstractAppender implements Appender { throw new ScriptError(MSG) } } + + /** Validates that all log events are properly mapped to colors and fonts. */ + private static validateLogEventMappings(): void { + const logEventValues = Object.values(LOG_EVENT).filter(v => typeof v === "number") as LOG_EVENT[] + const missingColor = logEventValues.filter(ev => !(ev in ExcelAppender.DEFAULT_EVENT_FONTS)) + if (missingColor.length > 0) { + throw new ScriptError( + `[ExcelAppender]: LOG_EVENT enum is not fully mapped in DEFAULT_EVENT_FONTS. Missing: color=${missingColor.map(ev => LOG_EVENT[ev]).join(", ")}` + ); + } + } + /** * Clears the message cell only if it is not empty. + * @remarks Defined before constructor to ensure Script Office compatibility, since it is used in the constructor. */ private clearCellIfNotEmpty(nextValue?: string): void { const value = this._msgCellRng.getValue() @@ -1392,21 +1445,10 @@ class ExcelAppender extends AbstractAppender implements Appender { } } - /** Validates that all log events are properly mapped to colors and fonts. */ - private static validateLogEventMappings(): void { - const logEventValues = Object.values(LOG_EVENT).filter(v => typeof v === "number") as LOG_EVENT[] - const missingColor = logEventValues.filter(ev => !(ev in ExcelAppender._DEFAULT_COLOR_MAP)) - const missingFont = logEventValues.filter(ev => !(ev in ExcelAppender.FONT_LABEL_MAP)) - if (missingColor.length > 0 || missingFont.length > 0) { - throw new ScriptError( - `[ExcelAppender]: LOG_EVENT enum is not fully mapped in _DEFAULT_COLOR_MAP or FONT_LABEL_MAP. Missing: color=${missingColor}, font=${missingFont}` - ) - } -} - } // #endregion ExcelAppender + // #region LoggerImpl /** * Singleton class that manages application logging through appenders. @@ -1448,8 +1490,8 @@ class LoggerImpl implements Logger { // Equivalent labels from LEVEL private static readonly LEVEL_LABELS = Object.entries(LoggerImpl.LEVEL).reduce((acc, [key, value]) => { - acc[value] = key; - return acc; + acc[value] = key + return acc }, {} as Record) // Equivalent labels from ACTION @@ -1463,13 +1505,14 @@ class LoggerImpl implements Logger { private static readonly DEFAULT_LEVEL = LoggerImpl.LEVEL.WARN private static readonly DEFAULT_ACTION = LoggerImpl.ACTION.EXIT - private readonly _level: typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL] = LoggerImpl.DEFAULT_LEVEL; - private readonly _action: typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION] = LoggerImpl.DEFAULT_ACTION; + private readonly _level: typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL] = LoggerImpl.DEFAULT_LEVEL + private readonly _action: typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION] = LoggerImpl.DEFAULT_ACTION private _criticalEvents: LogEvent[] = []; // Collects all ERROR and WARN events only private _errCnt = 0; // Counts the number of error events found private _warnCnt = 0; // Counts the number of warning events found private _appenders: Appender[] = []; // List of appenders + // Private constructor to prevent user instantiation private constructor( level: typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL] = LoggerImpl.DEFAULT_LEVEL, action: typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION] = LoggerImpl.DEFAULT_ACTION, @@ -1482,37 +1525,42 @@ class LoggerImpl implements Logger { /** * @returns An array with error and warning event messages only. * @throws ScriptError If the singleton was not instantiated. + * @override */ + public getCriticalEvents(): LogEvent[] { - LoggerImpl.validateInstance("LoggerImpl.getCriticalEvents") // Validate the instance - return LoggerImpl._instance._criticalEvents + LoggerImpl.validateInstance("LoggerImpl.getCriticalEvents") + return this._criticalEvents } /** * @returns Total number of error message events sent to the appenders. * @throws ScriptError If the singleton was not instantiated. + * @override */ public getErrCnt(): number { LoggerImpl.validateInstance("LoggerImpl.getErrCnt") // Validate the instance - return LoggerImpl._instance._errCnt + return this._errCnt } - /** + /** * @returns Total number of warning events sent to the appenders. * @throws ScriptError If the singleton was not instantiated. + * @override */ public getWarnCnt(): number { LoggerImpl.validateInstance("LoggerImpl.getWarnCnt") // Validate the instance - return LoggerImpl._instance._warnCnt + return this._warnCnt } /** * @returns The action to take in case of errors or warning log events. * @throws ScriptError If the singleton was not instantiated. + * @override */ public getAction(): typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION] { LoggerImpl.validateInstance("LoggerImpl.getAction") // Validate the instance - return LoggerImpl._instance._action + return this._action } /** @@ -1521,19 +1569,21 @@ class LoggerImpl implements Logger { * Logger.ERROR is lower. * @returns The current log level. * @throws ScriptError If the singleton was not instantiated. + * @override */ public getLevel(): typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL] { LoggerImpl.validateInstance("LoggerImpl.getLevel") // Validate the instance - return LoggerImpl._instance._level + return this._level } /** * @returns Array with appenders subscribed to the Logger. * @throws ScriptError If the singleton was not instantiated. + * @override */ public getAppenders(): Appender[] { LoggerImpl.validateInstance("LoggerImpl.getAppenders") // Validate the instance - return LoggerImpl._instance._appenders + return this._appenders } // Setters @@ -1545,13 +1595,14 @@ class LoggerImpl implements Logger { * null or undefined entries, * or if the appenders to add are not unique * by appender class. See JSDoc from addAppender. + * @override * @see addAppender */ public setAppenders(appenders: Appender[]) { const CONTEXT = `${LoggerImpl.name}.setAppenders` LoggerImpl.validateInstance(CONTEXT) // Validate the instance - LoggerImpl.assertUniqueAppenderTypes(appenders, CONTEXT) // Validate uniqueness - LoggerImpl._instance._appenders = appenders + LoggerImpl.assertUniqueAppenderTypes(appenders, CONTEXT) + this._appenders = appenders } /** @@ -1561,6 +1612,7 @@ class LoggerImpl implements Logger { * if the input argument is null or undefined, * or if it breaks the class uniqueness of the appenders. * All appenders must be from a different implementation of the Appender class. + * @override * @see setAppenders */ public addAppender(appender: Appender): void { @@ -1572,7 +1624,7 @@ class LoggerImpl implements Logger { } const newAppenders = [...LoggerImpl._instance._appenders, appender] LoggerImpl.assertUniqueAppenderTypes(newAppenders, "LoggerImpl.addAppender") // Validate uniqueness - LoggerImpl._instance._appenders.push(appender) + this._appenders.push(appender) } /** @@ -1624,11 +1676,11 @@ class LoggerImpl implements Logger { public removeAppender(appender: Appender): void { const CONTEXT = `${LoggerImpl.name}.removeAppender` LoggerImpl.validateInstance(CONTEXT) // Validate the instance - const appenders = LoggerImpl._instance._appenders + const appenders = this._appenders if (!Utility.isEmptyArray(appenders)) { - const index = LoggerImpl._instance._appenders.indexOf(appender) + const index = this._appenders.indexOf(appender) if (index > -1) { - LoggerImpl._instance._appenders.splice(index, 1) // Remove one element at index + this._appenders.splice(index, 1) // Remove one element at index } } } @@ -1644,50 +1696,50 @@ class LoggerImpl implements Logger { * If no appender was defined, it does lazy initialization to ConsoleAppender. * @throws ScriptError Only if level is greater than Logger.LEVEL.OFF and the action is Logger.ACTION.EXIT. */ -public error(msg: string, extraFields?: LogEventExtraFields): void { - this.log(msg, LOG_EVENT.ERROR, extraFields) -} + public error(msg: string, extraFields?: LogEventExtraFields): void { + this.log(msg, LOG_EVENT.ERROR, extraFields) + } -/** - * Sends a warning log message (with optional structured extra fields) to all appenders if the level allows it. - * The level has to be greater than or equal to Logger.LEVEL.WARN to send this event to the appenders. - * After the message is sent, it updates the warning counter. - * @param msg - The warning message to log. - * @param extraFields - Optional structured data to attach to the log event (e.g., context info, tags). - * @remarks - * If no singleton was defined, it does lazy initialization with default configuration. - * If no appender was defined, it does lazy initialization to ConsoleAppender. - * @throws ScriptError Only if level is greater than Logger.LEVEL.ERROR and the action is Logger.ACTION.EXIT. - */ -public warn(msg: string, extraFields?: LogEventExtraFields): void { - this.log(msg, LOG_EVENT.WARN, extraFields) -} + /** + * Sends a warning log message (with optional structured extra fields) to all appenders if the level allows it. + * The level has to be greater than or equal to Logger.LEVEL.WARN to send this event to the appenders. + * After the message is sent, it updates the warning counter. + * @param msg - The warning message to log. + * @param extraFields - Optional structured data to attach to the log event (e.g., context info, tags). + * @remarks + * If no singleton was defined, it does lazy initialization with default configuration. + * If no appender was defined, it does lazy initialization to ConsoleAppender. + * @throws ScriptError Only if level is greater than Logger.LEVEL.ERROR and the action is Logger.ACTION.EXIT. + */ + public warn(msg: string, extraFields?: LogEventExtraFields): void { + this.log(msg, LOG_EVENT.WARN, extraFields) + } -/** - * Sends an info log message (with optional structured extra fields) to all appenders if the level allows it. - * The level has to be greater than or equal to Logger.LEVEL.INFO to send this event to the appenders. - * @param msg - The informational message to log. - * @param extraFields - Optional structured data to attach to the log event (e.g., context info, tags). - * @remarks - * If no singleton was defined, it does lazy initialization with default configuration. - * If no appender was defined, it does lazy initialization to ConsoleAppender. - */ -public info(msg: string, extraFields?: LogEventExtraFields): void { - this.log(msg, LOG_EVENT.INFO, extraFields) -} + /** + * Sends an info log message (with optional structured extra fields) to all appenders if the level allows it. + * The level has to be greater than or equal to Logger.LEVEL.INFO to send this event to the appenders. + * @param msg - The informational message to log. + * @param extraFields - Optional structured data to attach to the log event (e.g., context info, tags). + * @remarks + * If no singleton was defined, it does lazy initialization with default configuration. + * If no appender was defined, it does lazy initialization to ConsoleAppender. + */ + public info(msg: string, extraFields?: LogEventExtraFields): void { + this.log(msg, LOG_EVENT.INFO, extraFields) + } -/** - * Sends a trace log message (with optional structured extra fields) to all appenders if the level allows it. - * The level has to be greater than or equal to Logger.LEVEL.TRACE to send this event to the appenders. - * @param msg - The trace message to log. - * @param extraFields - Optional structured data to attach to the log event (e.g., context info, tags). - * @remarks - * If no singleton was defined, it does lazy initialization with default configuration. - * If no appender was defined, it does lazy initialization to ConsoleAppender. - */ -public trace(msg: string, extraFields?: LogEventExtraFields): void { - this.log(msg, LOG_EVENT.TRACE, extraFields) -} + /** + * Sends a trace log message (with optional structured extra fields) to all appenders if the level allows it. + * The level has to be greater than or equal to Logger.LEVEL.TRACE to send this event to the appenders. + * @param msg - The trace message to log. + * @param extraFields - Optional structured data to attach to the log event (e.g., context info, tags). + * @remarks + * If no singleton was defined, it does lazy initialization with default configuration. + * If no appender was defined, it does lazy initialization to ConsoleAppender. + */ + public trace(msg: string, extraFields?: LogEventExtraFields): void { + this.log(msg, LOG_EVENT.TRACE, extraFields) + } /** * @returns true if an error log event was sent to the appenders, otherwise false. @@ -1695,8 +1747,8 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { */ public hasErrors(): boolean { const CONTEXT = `${LoggerImpl.name}.hasErrors` - LoggerImpl.validateInstance(CONTEXT) // Validate the instance - return LoggerImpl._instance._errCnt > 0 + LoggerImpl.validateInstance(CONTEXT) + return this._errCnt > 0 } /** @@ -1705,8 +1757,8 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { */ public hasWarnings(): boolean { const CONTEXT = `${LoggerImpl.name}.hasWarnings` - LoggerImpl.validateInstance(CONTEXT) // Validate the instance - return LoggerImpl._instance._warnCnt > 0 + LoggerImpl.validateInstance(CONTEXT) + return this._warnCnt > 0 } /** @@ -1715,8 +1767,8 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { */ public hasMessages(): boolean { const CONTEXT = `${LoggerImpl.name}.hasMessages` - LoggerImpl.validateInstance(CONTEXT) // Validate the instance - return LoggerImpl._instance._criticalEvents.length > 0 + LoggerImpl.validateInstance(CONTEXT) + return this._criticalEvents.length > 0 } /** @@ -1725,10 +1777,10 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { */ public reset(): void { const CONTEXT = `${LoggerImpl.name}.clear` - LoggerImpl.validateInstance(CONTEXT) // Validate the instance - LoggerImpl._instance._criticalEvents = [] - LoggerImpl._instance._errCnt = 0 - LoggerImpl._instance._warnCnt = 0 + LoggerImpl.validateInstance(CONTEXT) + this._criticalEvents = [] + this._errCnt = 0 + this._warnCnt = 0 } /** @@ -1749,15 +1801,15 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { } { const CONTEXT = `${LoggerImpl.name}.exportState` LoggerImpl.validateInstance(CONTEXT); // Validate the instance - const levelKey = Object.keys(LoggerImpl.LEVEL).find(k => LoggerImpl.LEVEL[k as keyof typeof LoggerImpl.LEVEL] === LoggerImpl._instance._level); - const actionKey = Object.keys(LoggerImpl.ACTION).find(k => LoggerImpl.ACTION[k as keyof typeof LoggerImpl.ACTION] === LoggerImpl._instance._action); + const levelKey = Object.keys(LoggerImpl.LEVEL).find(k => LoggerImpl.LEVEL[k as keyof typeof LoggerImpl.LEVEL] === this._level); + const actionKey = Object.keys(LoggerImpl.ACTION).find(k => LoggerImpl.ACTION[k as keyof typeof LoggerImpl.ACTION] === this._action); return { level: levelKey ?? "UNKNOWN", action: actionKey ?? "UNKNOWN", - errorCount: LoggerImpl._instance._errCnt, - warningCount: LoggerImpl._instance._warnCnt, - criticalEvents: [...LoggerImpl._instance._criticalEvents] + errorCount: this._errCnt, + warningCount: this._warnCnt, + criticalEvents: [...this._criticalEvents] } } @@ -1767,44 +1819,48 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { * Returns "UNKNOWN" if the value is not found or logger is not initialized. */ public static getActionLabel(action?: typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION]): string { - const val = action !== undefined ? action : LoggerImpl._instance?._action; - if (val === undefined) return "UNKNOWN"; + const val = action !== undefined ? action : LoggerImpl._instance?._action + const UNKNOWN = "UNKNOWN" + if (val === undefined) return UNKNOWN const label = Object.keys(LoggerImpl.ACTION).find( key => LoggerImpl.ACTION[key as keyof typeof LoggerImpl.ACTION] === val ); - return label ?? "UNKNOWN"; + return label ?? UNKNOWN } /** - * Returns the label for a log level value. - * If no parameter is provided, uses the current logger instance's level. - * Returns "UNKNOWN" if the value is not found or logger is not initialized. - */ + * Returns the label for the given log level. + * @returns The label for the log level. + * If `level` is undefined, returns the label for the current logger instance's level. + * If neither is set, returns "UNKNOWN". + */ public static getLevelLabel(level?: typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL]): string { - const val = level !== undefined ? level : LoggerImpl._instance?._level; - if (val === undefined) return "UNKNOWN"; + const val = level !== undefined ? level : LoggerImpl._instance?._level + const UNKNOWN = "UNKNOWN" + if (val === undefined) return UNKNOWN const label = Object.keys(LoggerImpl.LEVEL).find( key => LoggerImpl.LEVEL[key as keyof typeof LoggerImpl.LEVEL] === val ); - return label ?? "UNKNOWN"; + return label ?? UNKNOWN } /** * Override toString method. * @throws ScriptError If the singleton was not instantiated. + * @override */ public toString(): string { const CONTEXT = `${LoggerImpl.name}.toString` - LoggerImpl.validateInstance(CONTEXT); // Validate the instance + LoggerImpl.validateInstance(CONTEXT) // Validate the instance const NAME = this.constructor.name const levelTk = Object.keys(LoggerImpl.LEVEL).find(key => LoggerImpl.LEVEL[key as keyof typeof LoggerImpl.LEVEL] === this._level) const actionTk = Object.keys(LoggerImpl.ACTION).find(key => LoggerImpl.ACTION[key as keyof typeof LoggerImpl.ACTION] === this._action) const appendersString = Array.isArray(this._appenders) - ? `[${this._appenders.map(a => a.toString()).join(", ")}]` - : "[]" - const scalarInfo = `level: "${levelTk}", action: "${actionTk}", errCnt: ${LoggerImpl._instance._errCnt}, warnCnt: ${LoggerImpl._instance._warnCnt}` + ? `[${this._appenders.map(a => a.toString()).join(", ")}]` + : "[]" + const scalarInfo = `level: "${levelTk}", action: "${actionTk}", errCnt: ${this._errCnt}, warnCnt: ${this._warnCnt}` return `${NAME}: {${scalarInfo}, appenders: ${appendersString}}` } @@ -1819,15 +1875,14 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { LoggerImpl.LEVEL[key as keyof typeof LoggerImpl.LEVEL] === this._level) const actionTk = Object.keys(LoggerImpl.ACTION).find(key => LoggerImpl.ACTION[key as keyof typeof LoggerImpl.ACTION] === this._action) - const scalarInfo = `level: "${levelTk}", action: "${actionTk}", errCnt: ${LoggerImpl._instance._errCnt}, warnCnt: ${LoggerImpl._instance._warnCnt}` + const scalarInfo = `level: "${levelTk}", action: "${actionTk}", errCnt: ${this._errCnt}, warnCnt: ${this._warnCnt}` const appendersString = Array.isArray(this._appenders) - ? `[${this._appenders.map(a => a.constructor.name).join(", ")}]` - : "[]" + ? `[${this._appenders.map(a => a.constructor.name).join(", ")}]` + : "[]" return `${NAME}: {${scalarInfo}, appenders: ${appendersString}}` - } - // #TEST-ONLY-START + // #TEST-ONLY-START /** * Sets the singleton instance to null, useful for running different scenarios. * @remarks Mainly intended for testing purposes. The state of the singleton will be lost. @@ -1849,7 +1904,7 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { * ``` */ public static clearInstance(): void { - LoggerImpl._instance = null; // Force full re-init + LoggerImpl._instance = null // Force full re-init } // #TEST-ONLY-END @@ -1877,42 +1932,42 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void { * @throws ScriptError In case the action defined for the logger is Logger.ACTION.EXIT and the event type * is LOG_EVENT.ERROR or LOG_EVENT.WARN. */ -private log(msg: string, type: LOG_EVENT, extraFields?: LogEventExtraFields): void { - LoggerImpl.initIfNeeded("LoggerImpl.log") // lazy initialization of the singleton with default parameters - const SEND_EVENTS = (LoggerImpl._instance._level !== LoggerImpl.LEVEL.OFF) - && (LoggerImpl._instance._level >= type) // Only send events if the level allows it - if (SEND_EVENTS) { - if (Utility.isEmptyArray(this.getAppenders())) { - this.addAppender(ConsoleAppender.getInstance()) // lazy initialization at least the basic appender - } - for (const appender of LoggerImpl._instance._appenders) { // sends to all appenders - appender.log(msg, type, extraFields) // Pass extraFields through to the appender - } - if (type <= LOG_EVENT.WARN) { // Only collects errors or warnings event messages - // Updating the counter - if (type === LOG_EVENT.ERROR) ++LoggerImpl._instance._errCnt - if (type === LOG_EVENT.WARN) ++LoggerImpl._instance._warnCnt - // Updating the message. Assumes first appender is representative (message for all appenders are the same) - const appender = LoggerImpl._instance._appenders[0] - const lastEvent = appender.getLastLogEvent() - if (!lastEvent) {// internal error - throw new Error("[LoggerImpl.log] Appender did not return a LogEvent for getLastLogEvent()") + private log(msg: string, type: LOG_EVENT, extraFields?: LogEventExtraFields): void { + LoggerImpl.initIfNeeded("LoggerImpl.log") // lazy initialization of the singleton with default parameters + const SEND_EVENTS = (this._level !== LoggerImpl.LEVEL.OFF) + && (this._level >= type) // Only send events if the level allows it + if (SEND_EVENTS) { + if (Utility.isEmptyArray(this._appenders)) { + this.addAppender(ConsoleAppender.getInstance()) // lazy initialization at least the basic appender + } + for (const appender of this._appenders) { // sends to all appenders + appender.log(msg, type, extraFields) // Pass extraFields through to the appender } - LoggerImpl._instance._criticalEvents.push(lastEvent) - if (LoggerImpl._instance._action === LoggerImpl.ACTION.EXIT) { - const LAST_MSG = AbstractAppender.getLayout().format(lastEvent) - throw new ScriptError(LAST_MSG) + if (type <= LOG_EVENT.WARN) { // Only collects errors or warnings event messages + // Updating the counter + if (type === LOG_EVENT.ERROR) ++this._errCnt + if (type === LOG_EVENT.WARN) ++this._warnCnt + // Updating the message. Assumes first appender is representative (message for all appenders are the same) + const appender = this._appenders[0] + const lastEvent = appender.getLastLogEvent() + if (!lastEvent) {// internal error + throw new Error("[LoggerImpl.log] Appender did not return a LogEvent for getLastLogEvent()") + } + this._criticalEvents.push(lastEvent) + if (this._action === LoggerImpl.ACTION.EXIT) { + const LAST_MSG = AbstractAppender.getLayout().format(lastEvent) + throw new ScriptError(LAST_MSG) + } } } } -} /* Enforces instantiation lazily. If the user didn't invoke getInstance(), provides a logger * with default configuration. It also sends a trace event indicating the lazy initialization */ private static initIfNeeded(context?: string): void { const PREFIX = context ? `[${context}]: ` : `[LoggerImpl.initIfNeeded]: ` if (!LoggerImpl._instance) { - LoggerImpl._instance = LoggerImpl.getInstance(); + LoggerImpl._instance = LoggerImpl.getInstance() const LEVEL_LABEL = `Logger.LEVEL.${LoggerImpl.getLevelLabel()}` const ACTION_LABEL = `Logger.ACTION.${LoggerImpl.getActionLabel()}` const MSG = `${PREFIX}Logger instantiated via Lazy initialization with default parameters (level='${LEVEL_LABEL}', action='${ACTION_LABEL}')` @@ -2007,6 +2062,7 @@ private log(msg: string, type: LOG_EVENT, extraFields?: LogEventExtraFields): vo // =================================================== // Export to globalThis for Office Scripts compatibility in Node/ts-node + if (typeof globalThis !== "undefined") { if (typeof LOG_EVENT !== "undefined") { // @ts-ignore @@ -2047,4 +2103,7 @@ if (typeof globalThis !== "undefined") { globalThis.Utility = Utility; } -} \ No newline at end of file +} + +// #endregion logger.ts + diff --git a/test/main.ts b/test/main.ts index 1d9ccf6..2f3d12b 100644 --- a/test/main.ts +++ b/test/main.ts @@ -1,8 +1,24 @@ + +// #region main.ts // ---------------------------------------- +// This script tests the Logging framework implemented in TypeScript for ExcelScript environment. +// It uses the TestRunner class to execute test cases defined in the TestCase class. +// The tests cover various functionalities of the logging framework, including error handling, +// log event creation, and appender functionality. +// +// The script is designed to run in an Office Scripts/ExcelScript environment, but can also be adapted +// for Node.js or TypeScript environments by removing or modifying certain parts of the code. // Testing the Logging framework // ---------------------------------------- -//Main function of the Script +/** + * Entry point for the test suite. + * @remarks + * Some tests are adapted for the asynchronous behavior of Office Scripts, requiring a delay to ensure cell values are set before assertions. + * The helper `TestCase.runTestAsync(fn)` is used to handle this, detecting the environment via the global variable `RunSyncTest`. + * If the variable is set to true, no delay is applied, allowing tests to run synchronously, otherwise a delay is applied to ensure proper execution + * under Office Scripts. In Node.js/TypeScript environments, this is not required. + */ function main(workbook: ExcelScript.Workbook, ) { // Parameters and constants definitions @@ -11,11 +27,11 @@ function main(workbook: ExcelScript.Workbook, //const VERBOSITY = TestRunner.VERBOSITY.OFF // uncomment the scenario of your preference const VERBOSITY = TestRunner.VERBOSITY.HEADER //const VERBOSITY = TestRunner.VERBOSITY.SECTION - const START_TEST = "START TEST" + const START_TEST = "START TEST" // Used in the title of the test run const END_TEST = "END TEST" const SHOW_TRACE = false - let run: TestRunner = new TestRunner(VERBOSITY) // Controles the test execution process + let run: TestRunner = new TestRunner(VERBOSITY) // Controles the test execution process specifying the verbosity level let success = false // Control variable to send the last message in finally // MAIN EXECUTION @@ -27,25 +43,24 @@ function main(workbook: ExcelScript.Workbook, run.title(`${START_TEST} with verbosity '${VERBOSITY_LEVEL}'`, 1) const INDENT: number = 2 // Use the same indentation level for all test cases - /*All functions need to be invoked using arrow function (=>). Test cases organized by topics. They don't have any dependency, so they can be executed in any order.*/ run.exec("Test Case ScriptError", () => TestCase.utility(), INDENT) run.exec("Test Case ScriptError", () => TestCase.scriptError(), INDENT) - run.exec("Test Case LayoutImpl", () => TestCase.layoutImpl(), INDENT) run.exec("Test Case LogEventImpl", () => TestCase.logEventImpl(), INDENT) + run.exec("Test Case LayoutImpl", () => TestCase.layoutImpl(), INDENT) run.exec("Test Case ConsoleAppender", () => TestCase.consoleAppender(), INDENT) run.exec("Test Case ExcelAppender", () => TestCase.excelAppender(workbook, MSG_CELL), INDENT) - run.exec("Test Case LoggerImpl: LoggerImpl", () => TestCase.loggerImpl(workbook, MSG_CELL), INDENT) - run.exec("Test Case LoggerImpl: Lazy Init", () => TestCase.loggerImplLazyInit(), INDENT) - run.exec("Test Case LoggerImpl: Reset Singleton", () => TestCase.loggerImplResetSingleton(workbook, MSG_CELL), INDENT) + // LoggerImpl tests run.exec("Test Case LoggerImpl: Counters", () => TestCase.loggerImplCounters(), INDENT) run.exec("Test Case LoggerImpl: Export State", () => TestCase.loggerImplExportState(), INDENT) - run.exec("Test Case LoggerImpl: Internal Errors", () => TestCase.loggerImplScriptError(workbook, MSG_CELL), INDENT) + run.exec("Test Case LoggerImpl: Reset Singleton", () => TestCase.loggerImplResetSingleton(workbook, MSG_CELL), INDENT) run.exec("Test Case LoggerImpl: toString", () => TestCase.loggerImplToString(workbook, MSG_CELL), INDENT) - + run.exec("Test Case LoggerImpl: Lazy Init", () => TestCase.loggerImplLazyInit(), INDENT) + run.exec("Test Case LoggerImpl: LoggerImpl", () => TestCase.loggerImpl(workbook, MSG_CELL), INDENT) + run.exec("Test Case LoggerImpl: Internal Errors", () => TestCase.loggerImplScriptError(workbook, MSG_CELL), INDENT) success = true } catch (e) { // TypeScript strict mode: 'e' is of type 'unknown', so we must check its type before property access @@ -104,13 +119,15 @@ class TestCase { // Utility to escape regex special characters in variables public static escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } - /** Removes the timestamp from a string. This is used to compare strings */ - public static removeTimestamp(str: string): string { // Remove timestamp from a string - let timestampRegex = /^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}\] / - return str.replace(timestampRegex, '') + /** Removes the timestamp from a string. This is used to compare strings in toString output or to remove + * the timestamp from the Excel cell value. + */ + public static removeTimestamp(str: string, token:string=''): string { // Remove timestamp from a string + let regex = /^(?:\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\])|"(?:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})"/g + return str.replace(regex, token) } public static setShortLayout(): void { // Clears and Sets the short layout for the appenders @@ -125,15 +142,30 @@ class TestCase { AbstractAppender.setLayout(layout) // Set the layout for all appenders } + /** Helper method to run a test asynchronously only under Office Scripts/ExcelScript environment. + * Control via global variable ExcelScriptIsMock. If 'true' it will run the test with no + * delay, otherwise it will run the test with a delay of 0 milliseconds. + */ + public static runTestAsync(fn: () => void) { + // Detect Office Scripts/ExcelScript by presence of ExcelScript global + if ( + typeof globalThis['ExcelScript'] !== 'undefined' && + // @ts-ignore + globalThis['RunSyncTest'] !== true + ) { + setTimeout(fn, 0) + } else { + fn() + } + } + /** * Returns a new array with only the 'type' and 'message' properties * from each LogEvent in the input array. * @param logEvents Array of LogEvent objects * @returns Array of objects containing only type and message */ - public static simplifyLogEvents( - logEvents: LogEvent[] - ): { type: LOG_EVENT; message: string }[] { + public static simplifyLogEvents(logEvents: LogEvent[]): { type: LOG_EVENT; message: string }[] { return logEvents.map(event => ({ type: event.type, message: event.message @@ -141,10 +173,10 @@ class TestCase { } // Helper method to send all possible log event during the testing process consider all possible ACTION value scenrios. - // It assumes the logger is already initialized - public static sendLog(msg: string, type: LOG_EVENT, extraFields: LogEventExtraFields, + // It assumes the logger is already initialized. Used in loggerImplLevels. + public static sendLog(msg: string, type: LOG_EVENT, extraFields: LogEventExtraFields, action: typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION], context: string = "TestCase.sendLog"): void { - + // Defining variables let typeStr: string, actionStr: string, errMsg: string, logger: Logger, CONTEXT: string @@ -155,90 +187,91 @@ class TestCase { CONTEXT = `-[type,action]=[${typeStr},${actionStr}]-${context}` let extraFieldsStr = extraFields ? ` ${JSON.stringify(extraFields)}` : "" // If extraFields are present, append them to the message errMsg = `[${typeStr}] ${msg}${extraFieldsStr}` - - Assert.isNotNull(logger, `getInstance()-is not null${CONTEXT}`) // Logger instance should not be null - if (action === LoggerImpl.ACTION.CONTINUE) { // No ScriptError is thrown, since the action is CONTINUE - if (type === LOG_EVENT.ERROR) { - Assert.doesNotThrow( - () => extraFields ? logger.error(msg, extraFields) : logger.error(msg), - `error())-do not throw ScriptError${CONTEXT}` - ) - } else if (type === LOG_EVENT.WARN) { - Assert.doesNotThrow( - () => extraFields ? logger.warn(msg, extraFields) : logger.warn(msg), - `warn()-do not throw ScriptError${CONTEXT}` - ) - } else if (type === LOG_EVENT.INFO) { - Assert.doesNotThrow( - () => extraFields ? logger.info(msg, extraFields) : logger.info(msg), - `info()-do not throw ScriptError${CONTEXT}` - ) - } else if (type === LOG_EVENT.TRACE) { - Assert.doesNotThrow( - () => extraFields ? logger.trace(msg, extraFields) : logger.trace(msg), - `trace()-do not throw ScriptError${CONTEXT}` - ) - } else {// Testing scenario not covered - throw new AssertionError(`Invalid type: ${typeStr}`) - } - } else if (action === LoggerImpl.ACTION.EXIT) { // For error will throw ScriptError, and for warning, depending on the level - if (type === LOG_EVENT.ERROR) { - TestCase.setShortLayout() - Assert.throws( - () => extraFields ? logger.error(msg, extraFields) : logger.error(msg), - ScriptError, - errMsg, - `error())-throws ScriptError${CONTEXT}` - ) - TestCase.setDefaultLayout() - } else if (type === LOG_EVENT.WARN) { - if (level >= LoggerImpl.LEVEL.WARN) { // If the level is greater than or equal to WARN onl + TestCase.runTestAsync(() => { + Assert.isNotNull(logger, `getInstance()-is not null${CONTEXT}`) // Logger instance should not be null + if (action === LoggerImpl.ACTION.CONTINUE) { // No ScriptError is thrown, since the action is CONTINUE + if (type === LOG_EVENT.ERROR) { + Assert.doesNotThrow( + () => extraFields ? logger.error(msg, extraFields) : logger.error(msg), + `error())-do not throw ScriptError${CONTEXT}` + ) + } else if (type === LOG_EVENT.WARN) { + Assert.doesNotThrow( + () => extraFields ? logger.warn(msg, extraFields) : logger.warn(msg), + `warn()-do not throw ScriptError${CONTEXT}` + ) + } else if (type === LOG_EVENT.INFO) { + Assert.doesNotThrow( + () => extraFields ? logger.info(msg, extraFields) : logger.info(msg), + `info()-do not throw ScriptError${CONTEXT}` + ) + } else if (type === LOG_EVENT.TRACE) { + Assert.doesNotThrow( + () => extraFields ? logger.trace(msg, extraFields) : logger.trace(msg), + `trace()-do not throw ScriptError${CONTEXT}` + ) + } else {// Testing scenario not covered + throw new AssertionError(`Invalid type: ${typeStr}`) + } + } else if (action === LoggerImpl.ACTION.EXIT) { // For error will throw ScriptError, and for warning, depending on the level + if (type === LOG_EVENT.ERROR) { TestCase.setShortLayout() Assert.throws( - () => extraFields ? logger.warn(msg, extraFields) : logger.warn(msg), + () => extraFields ? logger.error(msg, extraFields) : logger.error(msg), ScriptError, errMsg, - `warn()-throws ScriptError${CONTEXT}` + `error())-throws ScriptError${CONTEXT}` ) TestCase.setDefaultLayout() - } else { // If the level is ERROR then it is not expected to throw ScriptError + } else if (type === LOG_EVENT.WARN) { + if (level >= LoggerImpl.LEVEL.WARN) { // If the level is greater than or equal to WARN onl + TestCase.setShortLayout() + Assert.throws( + () => extraFields ? logger.warn(msg, extraFields) : logger.warn(msg), + ScriptError, + errMsg, + `warn()-throws ScriptError${CONTEXT}` + ) + TestCase.setDefaultLayout() + } else { // If the level is ERROR then it is not expected to throw ScriptError + Assert.doesNotThrow( + () => extraFields ? logger.warn(msg, extraFields) : logger.warn(msg), + `warn()-do not throw ScriptError${CONTEXT}` + ) + } + } else if (type === LOG_EVENT.INFO) { Assert.doesNotThrow( - () => extraFields ? logger.warn(msg, extraFields) : logger.warn(msg), - `warn()-do not throw ScriptError${CONTEXT}` + () => extraFields ? logger.info(msg, extraFields) : logger.info(msg), + `info()-throws ScriptError${CONTEXT}` + ) + } else if (type === LOG_EVENT.TRACE) { + Assert.doesNotThrow( + () => extraFields ? logger.trace(msg, extraFields) : logger.trace(msg), + `trace()-throws ScriptError${CONTEXT}` ) + } else { + throw new AssertionError(`Invalid type: ${typeStr}`) } - } else if (type === LOG_EVENT.INFO) { - Assert.doesNotThrow( - () => extraFields ? logger.info(msg, extraFields) : logger.info(msg), - `info()-throws ScriptError${CONTEXT}` - ) - } else if (type === LOG_EVENT.TRACE) { - Assert.doesNotThrow( - () => extraFields ? logger.trace(msg, extraFields) : logger.trace(msg), - `trace()-throws ScriptError${CONTEXT}` - ) } else { - throw new AssertionError(`Invalid type: ${typeStr}`) + throw new AssertionError(`Invalid action: ${actionStr}`) } - } else { - throw new AssertionError(`Invalid action: ${actionStr}`) - } + }) } + /** - * Helper to simplify testing scenarios for all possible combinations of LEVEL,ACTION. Except for OFF level. + * Helper method to simplify testing scenarios for all possible combinations of LEVEL,ACTION. Except for OFF level. */ private static loggerImplLevels(includeExtraFields: boolean, // If true, it will send extra fields to the log events level: typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL], action: typeof LoggerImpl.ACTION[keyof typeof LoggerImpl.ACTION], - workbook: ExcelScript.Workbook, msgCell: string, context: string = "loggerImplLevels" - ): void { + workbook: ExcelScript.Workbook, msgCell: string, context: string = "loggerImplLevels"): void { // Defining variables let logger: Logger, appender: Appender, msgCellRng: ExcelScript.Range, activeSheet: ExcelScript.Worksheet, expectedNum: number, actualNum: number, - levelStr: string, actionStr: string, SUFFIX, extraFields: LogEventExtraFields + levelStr: string, actionStr: string, SUFFIX: string, extraFields: LogEventExtraFields // Logger: level, action TestCase.clear() @@ -256,8 +289,6 @@ class TestCase { msgCellRng = activeSheet.getRange(msgCell) appender = ExcelAppender.getInstance(msgCellRng) Assert.isNotNull(appender, `ExcelAppender(getInstance) is not null${SUFFIX}`) - Assert.instanceOf(appender, ExcelAppender, `ExcelAppender(getInstance) is ExcelAppender${SUFFIX}`) - Assert.instanceOf(AbstractAppender.getLayout(), LayoutImpl, `ExcelAppender(getInstance)-default layout${SUFFIX}`) // Adding appender to the logger logger!.addAppender(appender) Assert.equals(logger!.getAppenders().length, 1, `getAppenders().length-size=1${SUFFIX}`) @@ -280,7 +311,7 @@ class TestCase { } // level is not OFF, so we can continue with the tests - if(includeExtraFields) { + if (includeExtraFields) { extraFields = { userId: 123, sessionId: "abc" } } else { extraFields = undefined @@ -288,37 +319,46 @@ class TestCase { repeatCheckPerLevel(level, `repeatCheckPerLevel-extraFields=${includeExtraFields}`) // Inner functions - function repeatCheckingCriticalEvents(msg: string, type: LOG_EVENT, context: string = "repeatCheckingCriticalEvents"): void { + function repeatCheckingCriticalEvents(msg: string, type: LOG_EVENT, context: string = "repeatCheckingCriticalEvents"): void { let CONTEXT = `-[level,action]=[${levelStr},${actionStr}]-${context}` // Checking the number of critical events sent // Checking the critical event sent Assert.isNotNull(logger.getCriticalEvents(), `getCriticalEvents()-critial events are not null${CONTEXT}`) let lastIdx = logger.getCriticalEvents().length - 1 - Assert.isTrue(lastIdx >= 0, `LoggerImpl(getCriticalEvents)-critical events array is not empty${CONTEXT}`) let actualEvent = logger.getCriticalEvents()[lastIdx] - Assert.isNotNull(actualEvent, `getCriticalEvents()-last log event is not null${CONTEXT}`) - Assert.equals(actualEvent!.type, type, `getCriticalEvents()-last log event type is correct${CONTEXT}`) - Assert.equals(actualEvent!.message, msg, `getCriticalEvents()-last log event message is correct${CONTEXT}`) + TestCase.runTestAsync(() => { + Assert.isTrue(lastIdx >= 0, `LoggerImpl(getCriticalEvents)-critical events array is not empty${CONTEXT}`) + Assert.isNotNull(actualEvent, `getCriticalEvents()-last log event is not null${CONTEXT}`) + Assert.equals(actualEvent!.type, type, `getCriticalEvents()-last log event type is correct${CONTEXT}`) + Assert.equals(actualEvent!.message, msg, `getCriticalEvents()-last log event message is correct${CONTEXT}`) + }) } function repeatCheckingAbstractAppender(expectedMsg: string, expectedType: LOG_EVENT, context: string = "repeatCheckingAbstractAppender"): void { let CONTEXT = `-[level,action]=[${levelStr},${actionStr}]-${context}` // Checking the last log event sent via AbstractAppender - Assert.isNotNull(appender.getLastLogEvent(), `getLastLogEvent()-is not null${CONTEXT}`) - let actualEvent = appender.getLastLogEvent() - Assert.isNotNull(actualEvent, `getLastLogEvent()-last log event is not null${CONTEXT}`) - Assert.equals(actualEvent!.type, expectedType, `getLastLogEvent()-last log event type is ir correct${CONTEXT}`) - Assert.equals(actualEvent!.message, expectedMsg, `getLastLogEvent()-last log event message is correct${CONTEXT}`) + TestCase.runTestAsync(() => { + Assert.isNotNull(appender.getLastLogEvent(), `getLastLogEvent()-is not null${CONTEXT}`) + let actualEvent = appender.getLastLogEvent() + Assert.isNotNull(actualEvent, `getLastLogEvent()-last log event is not null${CONTEXT}`) + Assert.equals(actualEvent!.type, expectedType, `getLastLogEvent()-last log event type is ir correct${CONTEXT}`) + Assert.equals(actualEvent!.message, expectedMsg, `getLastLogEvent()-last log event message is correct${CONTEXT}`) + }) } function repeatCheckingExcelCellContent(expectedMsg: string, expectedType: LOG_EVENT, context: string = "repeatCheckingExcelCellContent"): void { // Checking the content of the excel cell let CONTEXT = `-[level,action]=[${levelStr},${actionStr}]-${context}` + const TK = "" // Token to be used to remove the timestamp from the message Assert.isNotNull(msgCellRng, `ExcelAppender(getInstance)-msgCellRng is not null${CONTEXT}`) - let actualMsg = TestCase.removeTimestamp(msgCellRng.getValue()) // under default configuration the output has stimestamp + let value = msgCellRng.getValue().toString() + let actualMsg = TestCase.removeTimestamp(value, TK) // under default configuration the output has stimestamp + const PREFIX = actualMsg == value ? "" : `${TK} ` // Adjust it in case timestamp was removed let extraFieldsStr = extraFields ? ` ${JSON.stringify(extraFields)}` : "" // If extraFields are present, append them to the message - expectedMsg = `[${LOG_EVENT[expectedType]}] ${expectedMsg}${extraFieldsStr}` - Assert.equals(actualMsg, expectedMsg, `ExcelAppender(msgCellRng.getValue)-excel cell content is correct${CONTEXT}`) + expectedMsg = `${PREFIX}[${LOG_EVENT[expectedType]}] ${expectedMsg}${extraFieldsStr}` + TestCase.runTestAsync(() => { + Assert.equals(actualMsg, expectedMsg, `ExcelAppender(msgCellRng.getValue)-excel cell content is correct${CONTEXT}`) + }) } function repeatCheckPerLevel(level: typeof LoggerImpl.LEVEL[keyof typeof LoggerImpl.LEVEL], context: string = "repeatCheckPerLevel"): void { @@ -335,9 +375,11 @@ class TestCase { TestCase.sendLog(lastMsg, lastType, extraFields, action, CONTEXT) // Depending on action, could throw ScriptError or not expectedNum = 1 // error log event is always a critical event actualNum = logger.getCriticalEvents().length - Assert.equals(actualNum, expectedNum, `getCriticalEvents()${SUFFIX}`) - Assert.equals(logger.hasErrors(), true, `hasErrors() is true${SUFFIX}`) - Assert.equals(logger.hasWarnings(), false, `hasWarnings() is false${SUFFIX}`) + TestCase.runTestAsync(() => { + Assert.equals(actualNum, expectedNum, `getCriticalEvents(length=1)-from error event is correct${SUFFIX}`) + Assert.equals(logger.hasErrors(), true, `hasErrors() is true${SUFFIX}`) + Assert.equals(logger.hasWarnings(), false, `hasWarnings() is false${SUFFIX}`) + }) repeatCheckingCriticalEvents(lastMsg, lastType) repeatCheckingAbstractAppender(lastMsg, lastType, CONTEXT) repeatCheckingExcelCellContent(lastMsg, lastType, CONTEXT) @@ -350,12 +392,17 @@ class TestCase { TestCase.sendLog(expectedMsg, expectedType, extraFields, action, CONTEXT) expectedNum = level > LoggerImpl.LEVEL.ERROR ? 2 : 1 // If level is ERROR, only the error log event was sent actualNum = logger.getCriticalEvents().length - Assert.equals(actualNum, expectedNum, `getCriticalEvents() is correct${SUFFIX}`) + TestCase.runTestAsync(() => { + Assert.equals(actualNum, expectedNum, `getCriticalEvents(length) from warning event is correct${SUFFIX}`) + }) addCriticalEvent = level >= LoggerImpl.LEVEL.WARN ? true : false // If level is WARN, warning log event was sent addEvent = level >= LoggerImpl.LEVEL.WARN ? true : false // If level is WARN or lower, warning log event was sent - Assert.isTrue(logger.hasErrors(), `hasErrors() is true${SUFFIX}`) - Assert.equals(logger.hasWarnings(), addEvent, `hasWarnings() is correct${SUFFIX}`) + TestCase.runTestAsync(() => { + Assert.isTrue(logger.hasErrors(), `hasErrors() is true${SUFFIX}`) + Assert.equals(logger.hasWarnings(), addEvent, `hasWarnings() is correct${SUFFIX}`) + }) + if (addCriticalEvent) { lastCriticalMsg = expectedMsg lastCriticalType = expectedType @@ -373,9 +420,11 @@ class TestCase { expectedType = LOG_EVENT.INFO TestCase.sendLog(expectedMsg, expectedType, extraFields, action, CONTEXT) actualNum = logger.getCriticalEvents().length - Assert.equals(actualNum, expectedNum, `getCriticalEvents()-is correct${SUFFIX}`) - Assert.equals(logger.hasErrors(), true, `hasErrors() is true${SUFFIX}`) - Assert.equals(logger.hasWarnings(), addCriticalEvent, `hasWarnings() is correct${SUFFIX}`) + TestCase.runTestAsync(() => { + Assert.equals(actualNum, expectedNum, `getCriticalEvents(length)-from info log is correct${SUFFIX}`) + Assert.equals(logger.hasErrors(), true, `hasErrors() is true${SUFFIX}`) + Assert.equals(logger.hasWarnings(), addCriticalEvent, `hasWarnings() is correct${SUFFIX}`) + }) addEvent = level >= LoggerImpl.LEVEL.INFO ? true : false // If level is INFO or lower, info log event was sent if (addEvent) { lastMsg = expectedMsg @@ -390,9 +439,11 @@ class TestCase { expectedType = LOG_EVENT.TRACE TestCase.sendLog(expectedMsg, expectedType, extraFields, action, CONTEXT) actualNum = logger.getCriticalEvents().length - Assert.equals(actualNum, expectedNum, `getCriticalEvents()-is correct${SUFFIX}`) - Assert.equals(logger.hasErrors(), true, `hasErrors() is true${SUFFIX}`) - Assert.equals(logger.hasWarnings(), addCriticalEvent, `hasWarnings() is correct${SUFFIX}`) + TestCase.runTestAsync(() => { + Assert.equals(actualNum, expectedNum, `getCriticalEvents(length) from trace event is correct${SUFFIX}`) + Assert.equals(logger.hasErrors(), true, `hasErrors() is true${SUFFIX}`) + Assert.equals(logger.hasWarnings(), addCriticalEvent, `hasWarnings() is correct${SUFFIX}`) + }) addEvent = level >= LoggerImpl.LEVEL.TRACE ? true : false // If level is TRACE or lower, trace log event was sent if (addEvent) { lastMsg = expectedMsg @@ -436,7 +487,7 @@ class TestCase { // Testing validateLogEventFactory const validFactory: LogEventFactory = (message: string, eventType: LOG_EVENT) => { - return new LogEventImpl(message, eventType); + return new LogEventImpl(message, eventType) } Assert.doesNotThrow( () => Utility.validateLogEventFactory(validFactory), @@ -530,18 +581,13 @@ class TestCase { "scriptError(with cause)" ) - // Testing toString - function escapeRegex(str: string): string {// Scaping metacharacters - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - } - function buildRegex(trigger: ScriptError): RegExp {// Building regex for toString let NAME = trigger.cause ? trigger.cause.name : trigger.name let MSG = trigger.cause ? trigger.cause.message : trigger.message const regex = new RegExp( - `^${escapeRegex(trigger.name)}: ${escapeRegex(trigger.message)}\\n` + // Header + `^${TestCase.escapeRegex(trigger.name)}: ${TestCase.escapeRegex(trigger.message)}\\n` + // Header `Stack trace:\\n` + // Stack section - `${escapeRegex(NAME)}: ${escapeRegex(MSG)}\\n` + // type and message + `${TestCase.escapeRegex(NAME)}: ${TestCase.escapeRegex(MSG)}\\n` + // type and message `( +at .+\\n?)+$` // Variable stack trace lines ) return regex @@ -565,69 +611,104 @@ class TestCase { // Testing rethrowCauseIfNeeded without cause expectedMsg = "Script Error message" try { - const err = new ScriptError(expectedMsg); - err.rethrowCauseIfNeeded(); - Assert.fail("Expected ScriptError to be thrown"); + const err = new ScriptError(expectedMsg) + err.rethrowCauseIfNeeded() + Assert.fail("Expected ScriptError to be thrown") } catch (e) { - Assert.instanceOf(e, ScriptError); + isScriptError(e, "LogEvent(rethrowCauseIfNeeded)-No cause") Assert.equals((e as ScriptError).message, expectedMsg, "LogEvent(rethrowCauseIfNeeded)-Top-level error") } // Cause is not a ScriptError, so it should be rethrown try { - cause = new Error("Root cause"); - origin = new ScriptError("Wrapper error", cause); - origin.rethrowCauseIfNeeded(); - Assert.fail("Expected root cause Error to be thrown"); + cause = new Error("Root cause") + origin = new ScriptError("Wrapper error", cause) + origin.rethrowCauseIfNeeded() + //Assert.fail("Expected root cause Error to be thrown") } catch (e) { - Assert.instanceOf(e, Error); - Assert.notInstanceOf(e, ScriptError); - Assert.equals((e as Error).message, "Root cause"); + isError(e, "LogEvent(rethrowCauseIfNeeded)-Root cause is Error") + isNotScriptError(e, "LogEvent(rethrowCauseIfNeeded)-Root cause is not a ScriptError") + Assert.equals((e as Error).message, "Root cause") } // Deepest cause is a ScriptError, so it should be rethrown try { - const root = new Error("Root error"); - const nested = new ScriptError("Nested script error", root); - const top = new ScriptError("Top script error", nested); - top.rethrowCauseIfNeeded(); - Assert.fail("Expected root Error to be thrown"); + const root = new Error("Root error") + const nested = new ScriptError("Nested script error", root) + const top = new ScriptError("Top script error", nested) + top.rethrowCauseIfNeeded() + Assert.fail("Expected root Error to be thrown") } catch (e) { - Assert.instanceOf(e, Error); - Assert.notInstanceOf(e, ScriptError); - Assert.equals((e as Error).message, "Root error"); + isError(e, "LogEvent(rethrowCauseIfNeeded)-Deepest cause is Error") + isNotScriptError(e, "LogEvent(rethrowCauseIfNeeded)-Deepest cause is not a ScriptError") + Assert.equals((e as Error).message, "Root error") } - TestCase.clear() // Clear all the instances + + // Inner functions + function isScriptError(value: unknown, message?: string): asserts value is ScriptError { + if (!(value instanceof ScriptError)) { + const prefix = message ? `${message}: ` : "" + throw new AssertionError( + `${prefix}Expected instance of ScriptError, but got ${(value as object)?.constructor?.name || typeof value}` + ) + } + } + + function isNotScriptError(value: unknown, message?: string): void { + if (value instanceof ScriptError) { + const prefix = message ? `${message}: ` : "" + throw new AssertionError( + `${prefix}Expected value NOT to be instance of ScriptError, but got ${(value as object)?.constructor?.name || typeof value}` + ) + } + } + + function isError(value: unknown, message?: string): asserts value is Error { + if (!(value instanceof Error)) { + const prefix = message ? `${message}: ` : "" + throw new AssertionError( + `${prefix}Expected instance of Error, but got ${(value as object)?.constructor?.name || typeof value}` + ) + } + } + + function isNotError(value: unknown, message?: string): void { + if (value instanceof Error) { + const prefix = message ? `${message}: ` : "" + throw new AssertionError( + `${prefix}Expected value NOT to be instance of Error, but got ${(value as object)?.constructor?.name || typeof value}` + ) + } + } + } public static layoutImpl(): void { // Unit tests for LayoutImpl class TestCase.clear() // Deffining the variables to be used in the tests - let layout: Layout, event: LogEvent, actualStr: string, expectedStr: string, expectedMsg, expectedType: LOG_EVENT, eventWithExtras: LogEvent + let layout: Layout, event: LogEvent, actualStr: string, expectedStr: string, expectedMsg: string, expectedType: LOG_EVENT, eventWithExtras: LogEvent, + errMsg: string expectedMsg = "Test message" expectedType = LOG_EVENT.INFO event = new LogEventImpl(expectedMsg, expectedType) - + const TK= "" // Token to be used to replace the timestamp from the message + // Testing constructor: short layout layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // with short formatter Assert.isNotNull(layout, "LayoutImpl(constructor-short layout is not null)") - Assert.isType(layout, LayoutImpl, "LayoutImpl(constructor-is LayoutImpl)") Assert.equals((layout as LayoutImpl).getFormatter(), LayoutImpl.shortFormatterFun, "LayoutImpl(constructor-getFormatter() short formatter)") // Testing constructor: long layout layout = new LayoutImpl() // Default formatter with timestamp Assert.isNotNull(layout, "LayoutImpl(constructor-long layout is not null)") - Assert.isType(layout, LayoutImpl, "LayoutImpl(constructor-is LayoutImpl)") Assert.equals((layout as LayoutImpl).getFormatter(), LayoutImpl.defaultFormatterFun, "LayoutImpl(constructor-getFormatter() long formatter)") - // Testing constructor: invalid formatter, since the input argument was provided, it doesn't use the default formatter - expectedStr = `[LayoutImpl.constructor]: Invalid Layout: The internal '_formatter' property must be a function accepting a single LogEvent argument. ` + - `Example: event => "[type] " + event.message. This is typically set in the LayoutImpl constructor. See LayoutImpl documentation for usage. ` + - `Got: type="string", arity=N/A` + expectedStr = `[LayoutImpl.constructor]: Invalid Layout: The internal '_formatter' ` + + `property must be a function accepting a single LogEvent argument. See LayoutImpl documentation for usage.` Assert.throws( () => new LayoutImpl("Invalid formatter" as unknown as (event: LogEvent) => string), ScriptError, @@ -647,6 +728,43 @@ class TestCase { }, "LayoutImpl(ScriptError)-constructor - undefined formatter") + // Testing constructor: invalid formatter, return empty string + errMsg = "[LayoutImpl.constructor]: Formatter function must return a non-empty string for a valid LogEvent. Got: empty string" + Assert.throws( + () => new LayoutImpl(function alwaysEmpty(e) { return "" }), + ScriptError, + errMsg, + "LayoutImpl(constructor) - formatter returns empty string" + ) + + // Testing constructor: invalid formatter return null + errMsg = "[LayoutImpl.constructor]: Formatter function must return a non-empty string for a valid LogEvent. Got: null" + Assert.throws( + () => new LayoutImpl(function alwaysNull(e) { return null as unknown as string }), + ScriptError, + errMsg, + "LayoutImpl(constructor) - formatter returns null" + ) + + //Testing constructor: formatter with wrong arity (no arguments) + errMsg = `[LayoutImpl.constructor]: Invalid Layout: The internal '_formatter' property must be a function accepting a single LogEvent argument. See LayoutImpl documentation for usage.` + Assert.throws( + () => new LayoutImpl(function zeroArgs() { return "ok" }), + ScriptError, + errMsg, + "LayoutImpl(constructor) - formatter with zero args" + ) + + //Testing constructor: formatter with wrong arity (2+ arguments) + errMsg = `[LayoutImpl.constructor]: Invalid Layout: The internal '_formatter' property must be a function accepting a single LogEvent argument. See LayoutImpl documentation for usage.` + Assert.throws( + () => new LayoutImpl(function twoArgs(e:number, f:string) { return "ok" } as unknown as LayoutFormatter), + ScriptError, + errMsg, + "LayoutImpl(constructor) - formatter with two args" + ) + + // Testing format with short formatter layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // with short formatter expectedStr = `[${LOG_EVENT[expectedType]}] ${expectedMsg}` @@ -660,17 +778,27 @@ class TestCase { Assert.equals(actualStr, expectedStr, "LayoutImpl(format-short formatter with extras)") // Testing format with long formatter - expectedStr = `[${LOG_EVENT[expectedType]}] ${expectedMsg}` + expectedStr = `${TK} [${LOG_EVENT[expectedType]}] ${expectedMsg}` layout = new LayoutImpl() // Default formatter with timestamp - actualStr = TestCase.removeTimestamp(layout.format(event)) + actualStr = TestCase.removeTimestamp(layout.format(event), TK) Assert.equals(actualStr, expectedStr, "LayoutImpl(format-long formatter)") // Testing format with long formatter and with extra fields eventWithExtras = new LogEventImpl(expectedMsg, expectedType, { userId: 123, sessionId: "abc" }) - expectedStr = `[${LOG_EVENT[expectedType]}] ${expectedMsg} {"userId":123,"sessionId":"abc"}` - actualStr = TestCase.removeTimestamp(layout.format(eventWithExtras)) + expectedStr = `${TK} [${LOG_EVENT[expectedType]}] ${expectedMsg} {"userId":123,"sessionId":"abc"}` + actualStr = TestCase.removeTimestamp(layout.format(eventWithExtras), TK) Assert.equals(actualStr, expectedStr, "LayoutImpl(format-long formatter with extras)") + // Testing format with invalid LogEvent + layout = new LayoutImpl() // or pick any valid formatter + errMsg = "[LayoutImpl.format]: LogEvent.type='null' property must be a number (LOG_EVENT enum value)." + Assert.throws( + () => layout.format({ type: null, message: "msg", timestamp: new Date(), extraFields: {} } as unknown as LogEvent), + ScriptError, + errMsg, + "LayoutImpl(format) - invalid LogEvent (type=null)" + ) + // Testing toString with short formatter layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // with short formatter expectedStr = `LayoutImpl: {formatter: [Function: "shortLayoutFormatterFun"]}` @@ -683,6 +811,10 @@ class TestCase { actualStr = layout.toString() Assert.equals(actualStr, expectedStr, "LayoutImpl(toString-long formatter)") + // Testing toString with a custom formatter + layout = new LayoutImpl(function customNamedFormatter(e: LogEvent) { return "X" }) + Assert.equals(layout.toString(), `LayoutImpl: {formatter: [Function: "customNamedFormatter"]}`, "LayoutImpl(toString) - custom named formatter") + // Testing validateLayout: invalid formatter: null expectedStr = `[LayoutImpl.constructor]: Invalid Layout: layout object is null or undefined` Assert.throws( @@ -702,9 +834,8 @@ class TestCase { ) // Testing validateLayout: invalid formatter: string is not a function - expectedStr = `[LayoutImpl.constructor]: Invalid Layout: The internal '_formatter' property must be a function accepting a single LogEvent argument. ` + - `Example: event => "[type] " + event.message. This is typically set in the LayoutImpl constructor. See LayoutImpl documentation for usage. ` + - `Got: type="string", arity=N/A` + expectedStr = `[LayoutImpl.constructor]: Invalid Layout: The internal '_formatter' ` + + `property must be a function accepting a single LogEvent argument. See LayoutImpl documentation for usage.` const customInvalidFormatter = "Invalid formatter" as unknown as (event: LogEvent) => string Assert.throws( () => LayoutImpl.validateLayout(new LayoutImpl(customInvalidFormatter), "TestCase.layoutImpl"), @@ -728,47 +859,42 @@ class TestCase { TestCase.clear() // Defining the variables to be used in the tests - let actualEvent: LogEvent, expectedMsg: string, actualMsg: string, expectedStr, actualStr: string, - expectedType: LOG_EVENT, actualType: LOG_EVENT, actualtimeStamp: Date, errMsg + let actualEvent: LogEvent, expectedMsg: string, actualMsg: string, expectedStr: string, actualStr: string, + expectedType: LOG_EVENT, actualType: LOG_EVENT, actualtimeStamp: Date, errMsg: string, + expectedTimestamp: Date - let eventExtras: LogEventExtraFields = {userId: 123, sessionId: "abc"} + let eventExtras: LogEventExtraFields = { userId: 123, sessionId: "abc" } // Testing constructor - expectedMsg = "Test message" + expectedMsg = "Test message testing logEventImpl" expectedType = LOG_EVENT.INFO + expectedTimestamp = new Date() actualEvent = new LogEventImpl(expectedMsg, expectedType) Assert.isNotNull(actualEvent, "LogEventImpl(constructor-is not null)") - Assert.isType(actualEvent, LogEventImpl, "LogEventImpl(constructor-is LogEventImpl)") // Testing constructor with extra fields - let eventWithExtras = new LogEventImpl(expectedMsg, expectedType,eventExtras) + let eventWithExtras = new LogEventImpl(expectedMsg, expectedType, eventExtras, expectedTimestamp) Assert.isNotNull(eventWithExtras, "LogEventImpl(constructor with extras)-is not null") - Assert.isType(eventWithExtras, LogEventImpl, "LogEventImpl(constructor with extras)-is LogEventImpl") Assert.equals(eventWithExtras.message, expectedMsg, "LogEventImpl(constructor with extras)-message is correct") Assert.equals(eventWithExtras.type, expectedType, "LogEventImpl(constructor with extras)-type is correct") Assert.isNotNull(eventWithExtras.timestamp, "LogEventImpl(constructor with extras)-timestamp is not null") - Assert.isType(eventWithExtras.timestamp, Date, "LogEventImpl(constructor with extras)-timestamp is Date") Assert.equals(eventWithExtras.extraFields.userId, eventExtras.userId, "LogEventImpl(constructor with extras)-userId is correct") Assert.equals(eventWithExtras.extraFields.sessionId, eventExtras.sessionId, "LogEventImpl(constructor with extras)-sessionId is correct") // Testing the constructorw with no extra field and checking the value of the property actualEvent = new LogEventImpl(expectedMsg, expectedType) Assert.isNotNull(actualEvent.extraFields, "LogEventImpl(constructor with no extras)-extraFields is not null") - Assert.isType(actualEvent.extraFields, Object, "LogEventImpl(constructor with no extras)-extraFields is Object") - Assert.equals(Object.keys(actualEvent.extraFields).length, 0, "LogEventImpl(constructor with no extras)-extraFields is empty") - + Assert.equals(Object.keys(actualEvent.extraFields).length, 0, "LogEventImpl(constructor with no extras)-extraFields is empty") + // Testing extraFields with a field that is a function eventExtras = { userId: 123, sessionId: "abc", logTime: () => new Date().toISOString() } - eventWithExtras = new LogEventImpl(expectedMsg, expectedType, eventExtras, new Date()) + eventWithExtras = new LogEventImpl(expectedMsg, expectedType, eventExtras, expectedTimestamp) Assert.isNotNull(eventWithExtras, "LogEventImpl(constructor with function extra)-is not null") - Assert.isType(eventWithExtras, LogEventImpl, "LogEventImpl(constructor with function extra)-is LogEventImpl") Assert.equals(eventWithExtras.message, expectedMsg, "LogEventImpl(constructor with function extra)-message is correct") Assert.equals(eventWithExtras.type, expectedType, "LogEventImpl(constructor with function extra)-type is correct") Assert.isNotNull(eventWithExtras.timestamp, "LogEventImpl(constructor with function extra)-timestamp is not null") - Assert.isType(eventWithExtras.timestamp, Date, "LogEventImpl(constructor with function extra)-timestamp is Date") Assert.equals(eventWithExtras.extraFields.userId, eventExtras.userId, "LogEventImpl(constructor with function extra)-userId is correct") Assert.equals(eventWithExtras.extraFields.sessionId, eventExtras.sessionId, "LogEventImpl(constructor with function extra)-sessionId is correct") - Assert.isType(eventWithExtras.extraFields.logTime, Function, "LogEventImpl(constructor with function extra)-logTime is Function") // Testing constructor as undefined Assert.doesNotThrow( @@ -781,7 +907,6 @@ class TestCase { actualType = (actualEvent as LogEvent).type actualtimeStamp = (actualEvent as LogEvent).timestamp Assert.isNotNull(actualtimeStamp, "LogEventImpl(get timestamp) is not null") - Assert.isType(actualtimeStamp, Date, "LogEventImpl(get timestamp) is Date") Assert.equals(actualType, expectedType, "LogEventImpl(get type())") Assert.equals(actualMsg, expectedMsg, "LogEventImpl(get message())") @@ -824,8 +949,15 @@ class TestCase { "LogEventImpl(ScriptError)-constructor - empty message" ) + // Testing constructor with white space only message + Assert.throws( + () => new LogEventImpl(" ", LOG_EVENT.INFO, {}, new Date()), + ScriptError, + errMsg, + "LogEventImpl(constructor) - whitespace message throws" + ) + // Testing Constructor with non valid date - errMsg = "[LogEventImpl.constructor]: LogEvent.timestamp='null' property must be a Date." expectedMsg = "Test message" Assert.throws( @@ -853,14 +985,43 @@ class TestCase { "LogEventImpl(ScriptError)-constructor - non valid extraFields" ) + // Testing constructor for extraFields with Deep object value: + // Deep object value + errMsg = "[LogEventImpl.constructor]: extraFields[bad] has invalid type: object. Must be string, number, Date, or function." + Assert.throws( + () => new LogEventImpl("msg", LOG_EVENT.INFO, { bad: { nested: true } } as unknown as LogEventExtraFields, new Date()), + ScriptError, + errMsg, + "LogEventImpl(constructor) - extraFields with deep object throws" + ) + + // Testing constructor for extraFields with array value: + errMsg = "[LogEventImpl.constructor]: extraFields[bad] has invalid type: object. Must be string, number, Date, or function." + Assert.throws( + () => new LogEventImpl("msg", LOG_EVENT.INFO, { bad: [1, 2, 3] } as unknown as LogEventExtraFields, new Date()), + ScriptError, + errMsg, + "LogEventImpl(constructor) - extraFields with array throws" + ) + + // Testing constructor with extraFields with an undefined value + errMsg = "[LogEventImpl.constructor]: extraFields[bad] must not be undefined." + Assert.throws( + () => new LogEventImpl("msg", LOG_EVENT.INFO, { bad: undefined }, new Date()), + ScriptError, + errMsg, + "LogEventImpl(constructor) - extraFields with undefined throws" + ) + // Testing toString - let regex: RegExp = new RegExp(`^LogEventImpl: {timestamp="\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}", type="${LOG_EVENT[actualType]}", message="${actualMsg}"}$`) + let TK = "" + let regex: RegExp = new RegExp(`^LogEventImpl: {timestamp=${TK}, type="${LOG_EVENT[actualType]}", message="${actualMsg}"}$`) expectedStr = `[${actualType}] ${expectedMsg}` - actualStr = (actualEvent as LogEvent).toString() + actualStr = TestCase.removeTimestamp((actualEvent as LogEvent).toString(), TK) // Remove timestamp for comparison Assert.equals(regex.test(actualStr), true, "LogEventImpl(toString())") // Testing toString with extra fields - expectedStr = `LogEventImpl: {timestamp="${Utility.date2Str(actualtimeStamp)}", type="${LOG_EVENT[actualType]}", message="${actualMsg}", extraFields=${JSON.stringify(eventExtras)}}` + expectedStr = `LogEventImpl: {timestamp="${Utility.date2Str(expectedTimestamp)}", type="${LOG_EVENT[actualType]}", message="${actualMsg}", extraFields=${JSON.stringify(eventExtras)}}` actualStr = (eventWithExtras as LogEvent).toString() Assert.equals(actualStr, expectedStr, "LogEventImpl(toString with extras)") @@ -951,14 +1112,21 @@ class TestCase { errMsg, "LogEventImpl(validateLogEvent)-invalid case with extra fields-null" ) - + // Testing validateLogEvent with extra fields (undefined valid case) // undefined is valid, since it defaults to an empty object Assert.doesNotThrow( () => LogEventImpl.validateLogEvent({ type: actualType, message: actualMsg, timestamp: actualtimeStamp, extraFields: undefined }), "LogEventImpl(validateLogEvent)-valid case with extra fields-undefined" ) - + + // ExtraFields: Reserved keys should not be included + eventExtras = { type: "SHOULD NOT INCLUDE", message: "NO", timestamp: 123, toString: () => "no" } + actualEvent = new LogEventImpl("msg", LOG_EVENT.INFO, eventExtras, new Date()) + Assert.equals(Object.keys(actualEvent.extraFields).length, 0, "LogEventImpl: Reserves keys: all are reserved in extraFields") + eventExtras = { name: "Valid", message: "NO", timestamp: 123, toString: () => "no" } + actualEvent = new LogEventImpl("msg", LOG_EVENT.INFO, eventExtras, new Date()) + Assert.equals(Object.keys(actualEvent.extraFields).length, 1, "LogEventImpl: Reserves keys: not all are reserved in extraFields") TestCase.clear() } @@ -972,7 +1140,7 @@ class TestCase { // Defining the variables to be used in the tests let expectedStr: string, actualStr: string, expectedEvent: LogEvent, actualEvent: LogEvent | null, appender: Appender, layout: Layout, expectedNull: LogEvent | null, - actualMsg: string, expectedMsg: string, msg: string, expectedType:LOG_EVENT, actualType: LOG_EVENT, errMsg: string, + actualMsg: string, expectedMsg: string, msg: string, expectedType: LOG_EVENT, actualType: LOG_EVENT, errMsg: string, extraFields: LogEventExtraFields // Test lazy initialization: We can't because we need and instance first @@ -980,7 +1148,12 @@ class TestCase { // Initial situation (testing information in AbstractAppender common to all appenders, no need to test it in each appender) appender = ConsoleAppender.getInstance() Assert.isNotNull(appender, "ConsoleAppender(getInstance) is not null") - Assert.instanceOf(appender, ConsoleAppender, "ConsoleAppender(getInstance) is ConsoleAppender") + + // Multiple calls to getInstance() returns the same object + ConsoleAppender.clearInstance() + const a1 = ConsoleAppender.getInstance() + const a2 = ConsoleAppender.getInstance() + Assert.equals(a1, a2, "ConsoleAppender(getInstance) - singleton reference test") // Testing static properties have default values (null) Assert.isNull((appender as AbstractAppender).getLastLogEvent(), "ConsoleAppender(getInstance) has no last log event") @@ -993,15 +1166,56 @@ class TestCase { // Checking static properties layout = AbstractAppender.getLayout() // lazy initialized Assert.isNotNull(layout, "ConsoleAppender(getInstance) has a layout") - Assert.isType(layout, LayoutImpl, "ConsoleAppender(getInstance) has a LayoutImpl layout") // Checking the default formatter via LayoutImpl.toString() expectedStr = `LayoutImpl: {formatter: [Function: "defaultLayoutFormatterFun"]}` actualStr = (layout as LayoutImpl).toString() Assert.equals(actualStr, expectedStr, "ConsoleAppender(getInstance) has a default layout formatter") + // Testing setting layout twice, second call should not change the layout + AbstractAppender.clearLayout() + const layout1 = new LayoutImpl() + const layout2 = new LayoutImpl(LayoutImpl.shortFormatterFun) + AbstractAppender.setLayout(layout1) + AbstractAppender.setLayout(layout2) // Should NOT replace layout1 + Assert.equals(AbstractAppender.getLayout(), layout1, "AbstractAppender(setLayout) - second setLayout call does not override") + AbstractAppender.clearLayout() // Reset the appender state + AbstractAppender.setLayout(new LayoutImpl(LayoutImpl.defaultFormatterFun)) // Now set the second layout + + // setLayout: Invalid layout + AbstractAppender.clearLayout() + errMsg = `[AbstractAppender.setLayout]: Invalid Layout: layout object is null or undefined` + Assert.throws( + () => AbstractAppender.setLayout("not a layout" as unknown as Layout), + ScriptError, + errMsg, + "AbstractAppender(setLayout) - invalid layout" + ) + AbstractAppender.setLayout(new LayoutImpl()) // Reset the appender state to a valid layout + + // Setting log event factory twice (should not override the first) + AbstractAppender.clearLogEventFactory() + const factory1 = function f1(message:string, type:LOG_EVENT) { return new LogEventImpl("A" + message, type) } + const factory2 = function f2(message:string, type:LOG_EVENT) { return new LogEventImpl("B" + message, type) } + AbstractAppender.setLogEventFactory(factory1) + AbstractAppender.setLogEventFactory(factory2) // Should NOT replace factory1 + Assert.equals(AbstractAppender.getLogEventFactory(), factory1, "AbstractAppender(setLogEventFactory) - second call does not override") + AbstractAppender.clearLogEventFactory() // Reset the appender state + AbstractAppender.setLogEventFactory(AbstractAppender.defaultLogEventFactoryFun) // Now set the second factory + + // Pass a non-function to setLogEventFactory and expect a ScriptError. + AbstractAppender.clearLogEventFactory() + errMsg = `[AbstractAppender.setLogEventFactory]: Invalid logEventFactory: Not a function` + Assert.throws( + () => AbstractAppender.setLogEventFactory("not a function" as unknown as LogEventFactory), + ScriptError, + errMsg, + "AbstractAppender(setLogEventFactory) - invalid factory" + ) + AbstractAppender.setLogEventFactory(AbstractAppender.defaultLogEventFactoryFun) // Now set the second factory + // Testing log(LogEvent): valid case - expectedMsg = "Info Message with lazy initialization" + expectedMsg = "Info Message with lazy initialization testing ConsoleAppender" expectedType = LOG_EVENT.INFO expectedEvent = new LogEventImpl(expectedMsg, expectedType) Assert.doesNotThrow(() => appender!.log(expectedEvent), @@ -1015,7 +1229,7 @@ class TestCase { // Testing log(LogEvent): valid case with extra fields extraFields = { userId: 123, sessionId: "abc" } - expectedMsg = "Error Message with lazy initialization and extra fields" + expectedMsg = "Error Message with lazy initialization and extra fields testing ConsoleAppender" expectedType = LOG_EVENT.ERROR expectedEvent = new LogEventImpl(expectedMsg, expectedType, extraFields) Assert.doesNotThrow(() => appender!.log(expectedEvent), @@ -1034,7 +1248,7 @@ class TestCase { ) // Testing log(string, LOG_EVENT) - expectedMsg = "Trace Message with lazy initialization" + expectedMsg = "Trace Message with lazy initialization testing ConsoleAppender" expectedType = LOG_EVENT.TRACE expectedEvent = new LogEventImpl(expectedMsg, expectedType) Assert.doesNotThrow(() => appender!.log(expectedMsg, expectedType), @@ -1048,7 +1262,7 @@ class TestCase { // Testing log(string, LOG_EVENT) with extra fields extraFields = { userId: 456, sessionId: "xyz" } - expectedMsg = "Trace Message with lazy initialization and extra fields" + expectedMsg = "Trace Message with lazy initialization and extra fields testing ConsoleAppender" expectedType = LOG_EVENT.TRACE expectedEvent = new LogEventImpl(expectedMsg, expectedType, extraFields) Assert.doesNotThrow(() => appender!.log(expectedMsg, expectedType, extraFields), @@ -1092,7 +1306,7 @@ class TestCase { // Testing toString, default logEventFactory ConsoleAppender.clearInstance() - expectedMsg = "error message in appendersToString" + expectedMsg = "Error message in appenders.toString testing ConsoleAppender" expectedType = LOG_EVENT.ERROR appender = ConsoleAppender.getInstance() actualEvent = new LogEventImpl(expectedMsg, expectedType) @@ -1142,7 +1356,7 @@ class TestCase { appender = ConsoleAppender.getInstance() AbstractAppender.clearLogEventFactory() AbstractAppender.setLogEventFactory(prodLogEventFactoryFun) - expectedMsg = "Custom LogEvent factory message" + expectedMsg = "Custom LogEvent factory message testing ConsoleAppender" expectedType = LOG_EVENT.INFO expectedEvent = new LogEventImpl(ENV + expectedMsg, expectedType) appender.log(expectedMsg, expectedType) @@ -1152,7 +1366,14 @@ class TestCase { Assert.equals(actualEvent?.message, expectedEvent?.message, "ConsoleAppender(getLastMsg not empty)-log(string,LOG_EVENT) with custom factory.message") + // Clear instance also clear the last log event + appender.log("Some message", LOG_EVENT.INFO) + ConsoleAppender.clearInstance() + appender = ConsoleAppender.getInstance() + Assert.equals(appender.getLastLogEvent(), null, "ConsoleAppender(clearInstance) - lastLogEvent is null after clear") + AbstractAppender.clearLogEventFactory() + TestCase.clear() } @@ -1162,53 +1383,59 @@ class TestCase { // Defining the variables to be used in the tests let expectedStr: string, actualStr: string, msg: string, expectedEvent: LogEvent, - actualEvent: LogEvent | null, expectedMsg:string, expectedType: LOG_EVENT, - errMsg: string, + actualEvent: LogEvent | null, expectedMsg: string, expectedType: LOG_EVENT, errMsg: string, appender: Appender, msgCellRng: ExcelScript.Range, extraFields: LogEventExtraFields, - activeSheet: ExcelScript.Worksheet + activeSheet: ExcelScript.Worksheet, eventFonts: Record, + layout: Layout activeSheet = workbook.getActiveWorksheet() msgCellRng = activeSheet.getRange(msgCell) const address = msgCellRng.getAddress() appender = ExcelAppender.getInstance(msgCellRng) + const DEFAULT_FONTS = { ...ExcelAppender.DEFAULT_EVENT_FONTS } // Note: Testing log calls (may be redundant because log is from AbstractAppender, but we need to test the ExcelAppender specific behavior) // Testing sending log(message, LOG_EVENT) - expectedMsg = "Info event in ExcelConsole" + expectedMsg = "Info event testing ExcelAppender" expectedType = LOG_EVENT.INFO Assert.doesNotThrow(() => appender.log(expectedMsg, expectedType), "ExcelAppender(log(string,LOG_EVENT)) - valid case") actualStr = msgCellRng.getValue().toString() - actualEvent = appender.getLastLogEvent() // Safe to use getLastLogEvent here since it was tested in ConsoleAppender - Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null") - Assert.equals(actualEvent!.type, expectedType, "ExcelAppender(getLastLogEvent).type") - Assert.equals(actualEvent!.message, expectedMsg, "ExcelAppender(getLastLogEvent).message") - // Now checking the excel cell value (formatted via format method) - expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) - Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(string,LOG_EVENT))") + actualEvent = appender.getLastLogEvent() + TestCase.runTestAsync(() => { + Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null") + Assert.equals(actualEvent!.type, expectedType, "ExcelAppender(getLastLogEvent).type") + Assert.equals(actualEvent!.message, expectedMsg, "ExcelAppender(getLastLogEvent).message") + // Now checking the excel cell value (formatted via format method) + expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) + Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(string,LOG_EVENT))") + }) // Testing sending log(message, LOG_EVENT) with extra fields extraFields = { userId: 123, sessionId: "abc" } - expectedMsg = "Info event with extra fields in ExcelConsole" + expectedMsg = "Info event with extra fields testing ExcelAppender" expectedType = LOG_EVENT.INFO Assert.doesNotThrow(() => appender.log(expectedMsg, expectedType, extraFields), "ExcelAppender(log(string,LOG_EVENT)) - valid case with extra fields" ) - actualEvent = appender.getLastLogEvent() - Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null") - Assert.equals(actualEvent!.type, expectedType, "ExcelAppender(getLastLogEvent).type with extra fields") - Assert.equals(actualEvent!.message, expectedMsg, "ExcelAppender(getLastLogEvent).message with extra fields") - Assert.equals(actualEvent!.extraFields.userId, extraFields.userId, - "ExcelAppender(getLastLogEvent).extraFields.userId with extra fields" - ) - Assert.equals(actualEvent!.extraFields.sessionId, extraFields.sessionId, - "ExcelAppender(getLastLogEvent).extraFields.sessionId with extra fields" - ) - // Now checking the excel cell value (formatted via format method) - actualStr = msgCellRng.getValue().toString() - expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) - Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(message,LOG_EVENT)-with extra fields)") + TestCase.runTestAsync(() => { + actualEvent = appender.getLastLogEvent() + Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null") + Assert.equals(actualEvent!.type, expectedType, "ExcelAppender(getLastLogEvent).type with extra fields") + Assert.equals(actualEvent!.message, expectedMsg, "ExcelAppender(getLastLogEvent).message with extra fields") + Assert.equals(actualEvent!.extraFields.userId, extraFields.userId, + "ExcelAppender(getLastLogEvent).extraFields.userId with extra fields" + ) + Assert.equals(actualEvent!.extraFields.sessionId, extraFields.sessionId, + "ExcelAppender(getLastLogEvent).extraFields.sessionId with extra fields" + ) + // Now checking the excel cell value (formatted via format method) + actualStr = msgCellRng.getValue().toString() + expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) + Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(message,LOG_EVENT)-with extra fields)") + }) // Testing log(LogEvent) + actualEvent = appender.getLastLogEvent() expectedEvent = new LogEventImpl(expectedMsg, expectedType, {}, actualEvent.timestamp) Assert.doesNotThrow(() => appender.log(expectedEvent), "ExcelAppender(log(LogEvent)) - valid case" @@ -1228,43 +1455,69 @@ class TestCase { "ExcelAppender(getLastEvent).message from log(LogEvent)") // Testing log(LogEvent) with extra fields - expectedMsg = "Error event with extra fields in ExcelConsole" + expectedMsg = "Error event with extra fields testing ExcelAppender" expectedType = LOG_EVENT.ERROR expectedEvent = new LogEventImpl(expectedMsg, expectedType, extraFields) Assert.doesNotThrow(() => appender.log(expectedEvent), "ExcelAppender(log(LogEvent)) - valid case with extra fields" ) - actualEvent = appender.getLastLogEvent() - Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null from log(LogEvent) with extra fields") - Assert.equals(actualEvent!.type, expectedEvent.type, - "ExcelAppender(getLastLogEvent).type from log(LogEvent) with extra fields") - Assert.equals(actualEvent!.message, expectedEvent.message, - "ExcelAppender(getLastLogEvent).message from log(LogEvent) with extra fields") - Assert.equals(actualEvent!.extraFields.userId, extraFields.userId, - "ExcelAppender(getLastLogEvent).extraFields.userId from log(LogEvent) with extra fields" + TestCase.runTestAsync(() => { + actualEvent = appender.getLastLogEvent() + Assert.isNotNull(actualEvent, "ExcelAppender(getLastLogEvent) not null from log(LogEvent) with extra fields") + Assert.equals(actualEvent!.type, expectedEvent.type, + "ExcelAppender(getLastLogEvent).type from log(LogEvent) with extra fields") + Assert.equals(actualEvent!.message, expectedEvent.message, + "ExcelAppender(getLastLogEvent).message from log(LogEvent) with extra fields") + Assert.equals(actualEvent!.extraFields.userId, extraFields.userId, + "ExcelAppender(getLastLogEvent).extraFields.userId from log(LogEvent) with extra fields" + ) + // Now checking the excel cell value (formatted via format method) + actualStr = msgCellRng.getValue().toString() + expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) + Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(LogEvent)-with extra fields)") + }) + + // Custom log event valid colors + ExcelAppender.clearInstance() + Assert.doesNotThrow( + () => ExcelAppender.getInstance(msgCellRng, null as unknown as Record), + "ExcelAppender(getInstance)-using null log event fonts" + ) + Assert.equals( + (appender as ExcelAppender).getEventFonts(), + DEFAULT_FONTS, + "ExcelAppender(getEventFonts)-using null log event fonts defaults to DEFAULT_EVENT_FONTS" + ) + + // Changing two colors and the rest will be the defalt ones + eventFonts = { ...DEFAULT_FONTS, [LOG_EVENT.WARN]: "#ed7d31", [LOG_EVENT.INFO]: "#548235" } + ExcelAppender.clearInstance() + appender = ExcelAppender.getInstance(msgCellRng, eventFonts) + Assert.equals( + (appender as ExcelAppender).getEventFonts(), + eventFonts, + "ExcelAppender(getEventFonts)-using custom log event fonts" ) - // Now checking the excel cell value (formatted via format method) - actualStr = msgCellRng.getValue().toString() - expectedStr = AbstractAppender.getLayout().format(actualEvent as LogEvent) - Assert.equals(actualStr, expectedStr, "ExcelAppender(cell value via log(LogEvent)-with extra fields)") // Script Errors ExcelAppender.clearInstance() // singleton is undefined - errMsg = "[AbstractAppender.log]: A singleton instance can't be undefined or null. Please invoke getInstance first" - Assert.throws( - () => appender.log("Info message", LOG_EVENT.INFO), - ScriptError, - errMsg, - "ExcelAppender(ScriptError)-log-singleton not defined" - ) - // Script Errors: Testing non valid input: getInstancce(null) - errMsg = "[ExcelAppender.getInstance]: A valid ExcelScript.Range for input argument msgCellRng is required." - Assert.throws( - () => ExcelAppender.getInstance(null), - ScriptError, - errMsg, - "ExcelAppender(ScriptError)-getInstance(Non valid msgCellRng-null)" - ) + TestCase.runTestAsync(() => { + errMsg = "[AbstractAppender.log]: A singleton instance can't be undefined or null. Please invoke getInstance first" + Assert.throws( + () => appender.log("Info message", LOG_EVENT.INFO), + ScriptError, + errMsg, + "ExcelAppender(ScriptError)-log-singleton not defined" + ) + // Script Errors: Testing non valid input: getInstancce(null) + errMsg = "[ExcelAppender.getInstance]: A valid ExcelScript.Range for input argument msgCellRng is required." + Assert.throws( + () => ExcelAppender.getInstance(null), + ScriptError, + errMsg, + "ExcelAppender(ScriptError)-getInstance(Non valid msgCellRng-null)" + ) + }) // Script Errors: Testing non valid input: getInstancce(undefined) Assert.throws( @@ -1299,30 +1552,26 @@ class TestCase { // Script Errors: Testing non-valid hexadecimal colors ExcelAppender.clearInstance() - errMsg = "[ExcelAppender.getInstance]: The input value 'null' for 'error' event is missing or not a string. Please provide a 6-digit hexadecimal color as 'RRGGBB' or '#RRGGBB'." - Assert.throws( - () => ExcelAppender.getInstance(msgCellRng, null as unknown as string), // don't use undefined, it is valid - ScriptError, - errMsg, - "ExcelAppender(ScriptError)-getInstance-red color undefined" - ) - errMsg = "[ExcelAppender.getInstance]: The input value '' for 'warning' event is missing or not a string. Please provide a 6-digit hexadecimal color as 'RRGGBB' or '#RRGGBB'." + errMsg = "[ExcelAppender.getInstance]: The input value '' for 'WARN' event is missing or not a string. Please provide a 6-digit hexadecimal color as 'RRGGBB' or '#RRGGBB'." + eventFonts = { ...DEFAULT_FONTS, [LOG_EVENT.WARN]: "" } Assert.throws( - () => ExcelAppender.getInstance(msgCellRng, "000000", ""), + () => ExcelAppender.getInstance(msgCellRng, eventFonts), ScriptError, errMsg, "ExcelAppender(ScriptError)-getInstance-Non valid font color for warning" ) - errMsg = "[ExcelAppender.getInstance]: The input value 'xxxxxx' for 'info' event is not a valid 6-digit hexadecimal color. Please use 'RRGGBB' or '#RRGGBB' format." + errMsg = "[ExcelAppender.getInstance]: The input value 'xxxxxx' for 'INFO' event is not a valid 6-digit hexadecimal color. Please use 'RRGGBB' or '#RRGGBB' format." + eventFonts = { ...DEFAULT_FONTS, [LOG_EVENT.INFO]: "xxxxxx" } Assert.throws( - () => ExcelAppender.getInstance(msgCellRng, "000000", "000000", "xxxxxx"), + () => ExcelAppender.getInstance(msgCellRng, eventFonts), ScriptError, errMsg, "ExcelAppender(ScriptError) - getInstance - Non valid font color for info" ) - errMsg = "[ExcelAppender.getInstance]: The input value '******' for 'trace' event is not a valid 6-digit hexadecimal color. Please use 'RRGGBB' or '#RRGGBB' format." + errMsg = "[ExcelAppender.getInstance]: The input value '******' for 'TRACE' event is not a valid 6-digit hexadecimal color. Please use 'RRGGBB' or '#RRGGBB' format." + eventFonts = { ...DEFAULT_FONTS, [LOG_EVENT.TRACE]: "******" } Assert.throws( - () => ExcelAppender.getInstance(msgCellRng, "000000", "000000", "000000", "******"), + () => ExcelAppender.getInstance(msgCellRng, eventFonts), ScriptError, errMsg, "ExcelAppender(ScriptError)-getInstance-Non valid font color for trace" @@ -1335,7 +1584,7 @@ class TestCase { appender.log(msg, LOG_EVENT.TRACE) expectedStr = `${AbstractAppender.prototype.toString.call(appender)}` // address in the expected string is dynamic and works cross` platform (Office Script and TypeScript) - expectedStr += ` ExcelAppender: {msgCellRng(address)="${address}", event fonts(map)={errFont="9c0006",warnFont="ed7d31",infoFont="548235",traceFont="7f7f7f"}}` + expectedStr += ` ExcelAppender: {msgCellRng(address)="${address}", eventfonts={ERROR="9c0006",WARN="ed7d31",INFO="548235",TRACE="7f7f7f"}}` actualStr = (appender as ExcelAppender).toString() Assert.equals(actualStr, expectedStr, "ExcelAppender(toString)") @@ -1345,17 +1594,43 @@ class TestCase { public static loggerImpl(workbook: ExcelScript.Workbook, msgCell: string): void { // Unit tests for LoggerImpl class TestCase.clear() // Defining variables - let logger: Logger + let logger: Logger, actualStr: string, expectedStr: string, appender: Appender // Checking Initial situation logger = LoggerImpl.getInstance() Assert.isNotNull(logger, "LoggerImpl(getInstance) is not null") - Assert.instanceOf(logger, LoggerImpl, "LoggerImpl(getInstance) is LoggerImpl") - Assert.instanceOf(logger, LoggerImpl, "LoggerImpl(getInstance) is LoggerImpl") Assert.equals(logger!.getLevel(), LoggerImpl.LEVEL.WARN, "LoggerImpl(getInstance)-default level is WARN") Assert.equals(logger!.getAction(), LoggerImpl.ACTION.EXIT, "LoggerImpl(getInstance)-default action is EXIT") Assert.isNotNull(logger!.getAppenders(), "LoggerImpl(getInstance)-default appenders is not null") Assert.equals(logger!.getAppenders().length, 0, "LoggerImpl(getInstance)-default appenders length is 0") + + // Testing getting label for LEVEL and ACTION + expectedStr = "OFF" + actualStr = LoggerImpl.getLevelLabel(LoggerImpl.LEVEL.OFF) + Assert.equals(actualStr, expectedStr, "LoggerImpl(getLevelLabel)-OFF label is correct") + expectedStr = "WARN" // Default level + actualStr = LoggerImpl.getLevelLabel(undefined) // non valid level + Assert.equals(actualStr, expectedStr, "LoggerImpl(getLevelLabel)-non valid level label is WARN") + + // Testing getting action label + expectedStr = "CONTINUE" + actualStr = LoggerImpl.getActionLabel(LoggerImpl.ACTION.CONTINUE) + Assert.equals(actualStr, expectedStr, "LoggerImpl(getActionLabel)-CONTINUE label is correct") + expectedStr = "EXIT" // Default action + actualStr = LoggerImpl.getActionLabel(undefined) // non valid action + Assert.equals(actualStr, expectedStr, "LoggerImpl(getActionLabel)-non valid action label is UNKNOWN") + + // Testing adding/removing appenders + appender = ConsoleAppender.getInstance() + Assert.doesNotThrow(() => logger.addAppender(appender), "LoggerImpl(addAppender) - valid case") + Assert.equals(logger.getAppenders().length, 1, "LoggerImpl(addAppender) - appender added") + Assert.isTrue(logger.getAppenders().includes(appender), "LoggerImpl(addAppender) - appender is in the list") + Assert.doesNotThrow(() => logger.removeAppender(appender), "LoggerImpl(removeAppender) - valid case") + Assert.equals(logger.getAppenders().length, 0, "LoggerImpl(removeAppender) - appender removed") + Assert.isFalse(logger.getAppenders().includes(appender), "LoggerImpl(removeAppender) - appender is not in the list") + Assert.doesNotThrow(() => logger.removeAppender(appender), "LoggerImpl(removeAppender) - empty list valid case") + Assert.equals(logger.getAppenders().length, 0, "LoggerImpl(removeAppender) - empty list valid case") + TestCase.clear() // Testing scenario based on different combinations of LEVEL and ACTION @@ -1387,22 +1662,7 @@ class TestCase { TestCase.loggerImplLevels(true, LoggerImpl.LEVEL.INFO, LoggerImpl.ACTION.EXIT, workbook, msgCell) TestCase.loggerImplLevels(true, LoggerImpl.LEVEL.TRACE, LoggerImpl.ACTION.EXIT, workbook, msgCell) - // Testing loggerImpl.getInstance() with custom appender - console.log("ANTES") - const state = logger.exportState(); - state.criticalEvents.forEach(event => { - // event.extraFields will include your custom data if present - console.log(event.extraFields); - }); - LoggerImpl.clearInstance(); // Clear the singleton instance - LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO).info("Info Log event with extra fields", {user:"admin", sessionId:"123"}) - let event = new LogEventImpl("Showing toString", LOG_EVENT.INFO, {user:"admin", sessionId:"123"}); - console.log(`event(extra fields)=${event}`) - event = new LogEventImpl("Showing toString", LOG_EVENT.INFO) - console.log(`event=${event}`) - console.log("DESPUES") - - TestCase.clear(); + TestCase.clear() } public static loggerImplLazyInit() { // Unit Tests on Lazy Initialization for Logger class (instance and appender) @@ -1413,16 +1673,15 @@ class TestCase { expectedNum: number, actualNum: number, actualEvent: LogEvent | null // Testing lazy initialization of the appender - expectedMsg = "Info event, in lazyInit" + expectedMsg = "Info event, testing lazyInit for LoggerImpl" expectedType = LOG_EVENT.INFO // No appender was defined logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO) // initialized the singleton logger.info(expectedMsg) // lazy initialization of the appender expectedNum = 1 actualNum = logger.getAppenders().length ?? 0 - Assert.equals(actualNum, expectedNum, "Logger(Lazy init)-appender") + Assert.equals(actualNum, expectedNum, "Logger(Lazy init)-appender size is one") Assert.isNotNull(logger.getAppenders()[0], "Logger(Lazy init)-appender is not null") - Assert.instanceOf(logger.getAppenders()[0], ConsoleAppender, "Logger(Lazy init)-appender is ConsoleAppender") Assert.equals(logger.getLevel(), LoggerImpl.LEVEL.INFO, "Logger(Lazy init)-level is INFO") Assert.equals(logger.getAction(), LoggerImpl.ACTION.EXIT, "Logger(Lazy init)-action is EXIT(default") actualEvent = logger.getAppenders()[0].getLastLogEvent() // Safe to use getLastLogEvent here since it was tested in ConsoleAppender @@ -1431,13 +1690,16 @@ class TestCase { Assert.equals(actualEvent.message, expectedMsg, "Logger(Lazy init)-getLastLogEvent.message info message is correct") // Lazy initialization of the singleton with default parameters (WARN,EXIT) - expectedMsg = "Error event, in lazyInit" + expectedMsg = "Error event testing lazyInit for LoggerImpl" expectedType = LOG_EVENT.ERROR expectedEvent = new LogEventImpl(expectedMsg, LOG_EVENT.ERROR) LoggerImpl.clearInstance() - Assert.isNotNull(LoggerImpl.getInstance(), "Lazy init(logger != null)") - Assert.equals(logger.getLevel(), LoggerImpl.LEVEL.WARN, "Logger(Lazy init)-level is WARN") - Assert.equals(logger.getAction(), LoggerImpl.ACTION.EXIT, "Logger(Lazy init)-action is EXIT") + TestCase.runTestAsync(() => { + Assert.isNotNull(LoggerImpl.getInstance(), "Lazy init(logger != null)") + logger = LoggerImpl.getInstance() // Get alreazy lazy initialized singleton + Assert.equals(logger.getLevel(), LoggerImpl.LEVEL.WARN, "Logger(Lazy init)-level is WARN") + Assert.equals(logger.getAction(), LoggerImpl.ACTION.EXIT, "Logger(Lazy init)-action is EXIT") + }) // To check the ScriptError message, since it may include the timestamp, we would need to use a short layout TestCase.setShortLayout() @@ -1471,7 +1733,7 @@ class TestCase { /**Unit tests for Logger class checking the behaviour after the singleton was reset */ - public static loggerImplResetSingleton(workbook: ExcelScript.Workbook, msgCell: string):void { + public static loggerImplResetSingleton(workbook: ExcelScript.Workbook, msgCell: string): void { TestCase.clear() // Defining the variables to be used in the tests @@ -1547,7 +1809,7 @@ class TestCase { // Defining the variables to be used in the tests let logger: Logger, layout: Layout, actualNum: number, expectedNum: number, actualEvent: LogEvent | null, expectedEvent: LogEvent, errMsg: string, warnMsg: string - + // Initializing the logger with a short layout layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // Short layout @@ -1561,7 +1823,7 @@ class TestCase { Assert.equals(logger.getCriticalEvents(), [], "loggerCounters(getMessages=[])") // Sending events affecting the counter - errMsg = "Error event in counters" + errMsg = "Error event for testing counters for LoggerImpl" logger.error(errMsg) expectedNum = 1 actualNum = logger.getErrCnt() @@ -1574,7 +1836,7 @@ class TestCase { // Testing counter for warnings - warnMsg = "Warning event in counters" + warnMsg = "Warning event for testing counters for LoggerImpl" logger.warn(warnMsg) expectedNum = 1 actualNum = logger.getWarnCnt() @@ -1586,7 +1848,7 @@ class TestCase { Assert.equals(actualArr, expectedArr, "loggerCounters(getMessages)") Assert.equals(logger.hasMessages(), true, "loggerCounters(hasMessages)") // Testing other events, don't affect the counters - let msg = "Info event doesn't count for counters" + let msg = "Info event doesn't count testing counters for LoggerImpl" logger.info(msg) actualNum = logger.getErrCnt() Assert.equals(actualNum, expectedNum, "LoggerCounter(getErrCnt=1)") @@ -1612,11 +1874,11 @@ class TestCase { } /**Unit Tests for Logger class on toString method */ - public static loggerImplToString(workbook: ExcelScript.Workbook, msgCell: string):void { + public static loggerImplToString(workbook: ExcelScript.Workbook, msgCell: string): void { TestCase.clear() TestCase.setShortLayout() // Defining the variables to be used in the tests - let expected: string, actual: string, layout: Layout, logger: Logger + let expected: string, actual: string, layout: Layout, logger: Logger, extraFields: LogEventExtraFields //layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // Short layout logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO, // Level of verbose @@ -1626,24 +1888,46 @@ class TestCase { logger.addAppender(ExcelAppender.getInstance(workbook.getActiveWorksheet().getRange(msgCell))) // Testing toString method - const MSGS = ["Error event in loggerToString", "Warning event in loggerToString"] + const MSGS = ["Error event testing for loggerImpl.toString", + "Warning event for loggerImpl.toString"] logger.error(MSGS[0]) // lazy initialization of the appender logger.warn(MSGS[1]) - expected = `LoggerImpl: {level: "INFO", action: "CONTINUE", errCnt: 1, warnCnt: 1, appenders: `+ - `[AbstractAppender: {layout=LayoutImpl: {formatter: [Function: "shortLayoutFormatterFun"]}, `+ - `logEventFactory="defaultLogEventFactoryFun", lastLogEvent=LogEventImpl: {timestamp="2025-06-18 01:02:39,720", `+ - `type="WARN", message="Warning event in loggerToString"}} ConsoleAppender: {}, AbstractAppender: {layout=LayoutImpl: `+ - `{formatter: [Function: "shortLayoutFormatterFun"]}, logEventFactory="defaultLogEventFactoryFun", lastLogEvent=LogEventImpl: `+ - `{timestamp="2025-06-18 01:02:39,720", type="WARN", message="Warning event in loggerToString"}} ExcelAppender: `+ - `{msgCellRng(address)="C2", event fonts(map)={errFont="9c0006",warnFont="ed7d31",infoFont="548235",traceFont="7f7f7f"}}]}` + expected = `LoggerImpl: {level: "INFO", action: "CONTINUE", errCnt: 1, warnCnt: 1, appenders: ` + + `[AbstractAppender: {layout=LayoutImpl: {formatter: [Function: "shortLayoutFormatterFun"]}, ` + + `logEventFactory="defaultLogEventFactoryFun", lastLogEvent=LogEventImpl: {timestamp="2025-06-18 01:02:39,720", ` + + `type="WARN", message="Warning event for loggerImpl.toString"}} ConsoleAppender: {}, AbstractAppender: {layout=LayoutImpl: ` + + `{formatter: [Function: "shortLayoutFormatterFun"]}, logEventFactory="defaultLogEventFactoryFun", lastLogEvent=LogEventImpl: ` + + `{timestamp="2025-06-18 01:02:39,720", type="WARN", message="Warning event for loggerImpl.toString"}} ExcelAppender: ` + + `{msgCellRng(address)="C2", eventfonts={ERROR="9c0006",WARN="ed7d31",INFO="548235",TRACE="7f7f7f"}}]}` + actual = logger.toString() + TestCase.runTestAsync(() => { + Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger)") + }) + + // Testing toString with extra fields + extraFields = { userId: 123, sessionId: "abc" } + TestCase.clear() + logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO) + logger.info("Info event testing logger.toString with extra fields", extraFields) + //console.log(`logger=${logger.toString()}`) + expected = `LoggerImpl: {level: "INFO", action: "EXIT", errCnt: 0, warnCnt: 0, appenders: [AbstractAppender: {layout=LayoutImpl: ` + + `{formatter: [Function: "defaultLayoutFormatterFun"]}, logEventFactory="defaultLogEventFactoryFun", ` + + `lastLogEvent=LogEventImpl: {timestamp="2025-06-19 22:31:17,324", type="INFO", message="Info event testing logger.toString with extra fields", ` + + `extraFields={"userId":123,"sessionId":"abc"}}} ConsoleAppender: {}]}` actual = logger.toString() - Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger)") + Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger with extra fields)") + + // Testing shortToString method + expected = `LoggerImpl: {level: "INFO", action: "EXIT", errCnt: 0, warnCnt: 0, appenders: [ConsoleAppender]}` + actual = (logger as LoggerImpl).toShortString() + Assert.equals(actual, expected, "loggerToString(LoggerImpl) short version") TestCase.clear() + // Helper function to normalize timestamps in the expected and actual strings function normalizeTimestamps(str: string): string { return str.replace(/timestamp="[^"]*"/g, 'timestamp=""') -} + } } /**Unit Tests for Logger class for method exportState */ @@ -1660,9 +1944,10 @@ class TestCase { layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // Short layout logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.TRACE, LoggerImpl.ACTION.CONTINUE) - msgs = ["warning event in exportState", "error event in exportState"] - logger.trace("trace event in exportState") - logger.info("info event in exportState") + msgs = ["warning event for testing exportState for LoggerImpl", + "error event for testing exportState for LoggerImpl"] + logger.trace("trace event for testing exportState for LoggerImpl") + logger.info("info event for testing exportState for LoggerImpl") logger.warn(msgs[0]) logger.error(msgs[1]) state = logger.exportState() @@ -1884,9 +2169,10 @@ class TestCase { // ---------------------------------------- // End Testing the Logging framework // ---------------------------------------- - // Make main available globally for Node/ts-node test environments if (typeof globalThis !== "undefined" && typeof main !== "undefined") { // @ts-ignore - globalThis.main = main; + globalThis.main = main } +// #endregion main.ts + diff --git a/test/unit-test-framework.ts b/test/unit-test-framework.ts index a631c8b..a4b151e 100644 --- a/test/unit-test-framework.ts +++ b/test/unit-test-framework.ts @@ -1,3 +1,6 @@ + +// #region unit-test-framework.ts + // ==================================================== // Lightweight unit testing framework for Office Script // ==================================================== @@ -10,9 +13,10 @@ * @remarks See the documentation for the Assert and TestRunner classes for assertion details and test execution control. * author David Leal * date 2025-06-03 (creation date) - * version 2.0.0 + * version 2.1.0 */ +// #region AssertionError /** * AssertionError is a custom error type used to indicate assertion failures in tests or validation utilities. * @@ -42,11 +46,13 @@ class AssertionError extends Error { this.name = "AssertionError" } } +// #endregion AssertionError +// #region Assert /** * Utility class for writing unit-test-style assertions. * Provides static methods to assert value equality and exception throwing. - * If an assertion fails, an informative 'Error' is thrown. + * If an assertion fails, an informative 'AssertionError' is thrown. */ class Assert { /** @@ -97,7 +103,7 @@ class Assert { /** * Asserts that two values are equal by type and value. * Supports comparison of primitive types, one-dimensional arrays of primitives, - * and one-dimensional arrays of objects (shallow comparison via JSON.stringify). + * and one-dimensional arrays of objects (deep equality via JSON.stringify). * * If the values differ, a detailed error is thrown. * For arrays, mismatches include index, value, and type. @@ -117,21 +123,38 @@ class Assert { * ``` */ public static equals(actual: T, expected: T, message: string = ""): asserts actual is T { - const MSG = message ? `${message}: ` : "" + const MSG = message ? `${message}: ` : ""; if ((actual == null || expected == null) && actual !== expected) { - throw new AssertionError(`${MSG}Assertion failed: actual (${Assert.safeStringify(actual)}) !== expected (${Assert.safeStringify(expected)})`) + throw new AssertionError(`${MSG}Assertion failed: actual (${Assert.safeStringify(actual)}) !== expected (${Assert.safeStringify(expected)})`); } if (Array.isArray(actual) && Array.isArray(expected)) { - this.arraysEqual(actual, expected, MSG) - return + this.arraysEqual(actual, expected, MSG); + return; + } + + // Add this block for plain objects + if ( + typeof actual === "object" && + typeof expected === "object" && + actual !== null && + expected !== null + ) { + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new AssertionError( + `${MSG}Assertion failed: actual (${Assert.safeStringify(actual)}) !== expected (${Assert.safeStringify(expected)})` + ); + } + return; } if (actual !== expected) { - const actualType = typeof actual - const expectedType = typeof expected - throw new AssertionError(`${MSG}Assertion failed: actual (${Assert.safeStringify(actual)} : ${actualType}) !== expected (${Assert.safeStringify(expected)} : ${expectedType})`) + const actualType = typeof actual; + const expectedType = typeof expected; + throw new AssertionError( + `${MSG}Assertion failed: actual (${Assert.safeStringify(actual)} : ${actualType}) !== expected (${Assert.safeStringify(expected)} : ${expectedType})` + ); } } @@ -156,6 +179,7 @@ class Assert { * Asserts that the given value is not `null`. * Provides a robust stringification of the value for error messages, * guarding against unsafe or error-throwing `toString()` implementations. + * Narrows the type of 'value' to NonNullable if assertion passes. * @param value - The value to test. * @param message - Optional message to prefix in case of failure. * @throws AssertionError if the value is `null`. @@ -169,38 +193,6 @@ class Assert { } } - /** - * Asserts that the provided object is an instance of the specified class or constructor. - * Throws an error if the assertion fails. - * - * @param obj - The object to check. - * @param cls - The constructor function (class) to check against. - * @param message - (Optional) Custom error message to display if the assertion fails. - */ - static instanceOf(obj: any, cls: Function, message?: string) { - if (!(obj instanceof cls)) { - throw new AssertionError( - message || `Expected object to be instance of ${cls.name}, got ${obj?.constructor?.name}` - ) - } - } - - /** - * Asserts that the provided object is NOT an instance of the specified class or constructor. - * Throws an error if the assertion fails. - * - * @param obj - The object to check. - * @param cls - The constructor function (class) to check against. - * @param message - (Optional) Custom error message to display if the assertion fails. - */ - static notInstanceOf(obj: any, cls: Function, message?: string) { - if (obj instanceof cls) { - throw new AssertionError( - message || `Expected object NOT to be instance of ${cls.name}, but got ${obj.constructor.name}` - ) - } - } - /** * Fails the test by throwing an error with the provided message. * @@ -212,37 +204,32 @@ class Assert { } /** - * Asserts that a value is of the expected primitive type (e.g. "string", "number") - * or instance of a class/constructor (e.g. Date, Array, custom class). - * @param value - The value to check. - * @param typeOrConstructor - The expected type as a string (e.g. "string", "object") or a constructor function. - * @param message - Optional custom error message. - */ - static isType( + * Asserts that a value is of the specified primitive type. + * + * @param value - The value to check. + * @param type - The expected type as a string ("string", "number", etc.) + * @param message - Optional error message. + * @throws AssertionError - If the type does not match. + * + * @example + * assertType("hello", "string"); // passes + * assertType(42, "number"); // passes + * assertType({}, "string"); // throws + */ + public static assertType( value: unknown, - typeOrConstructor: string | (new (...args: any[]) => any), + type: "string" | "number" | "boolean" | "object" | "function" | "undefined" | "symbol" | "bigint", message?: string ): void { - if (typeof typeOrConstructor === "string") { - if (typeof value !== typeOrConstructor) { - throw new AssertionError( - message || - `Expected type '${typeOrConstructor}', but got '${typeof value}': (${Assert.safeStringify(value)})` - ) - } - } else if (typeof typeOrConstructor === "function") { - if (!(value instanceof typeOrConstructor)) { - const ctorName = typeOrConstructor.name || "unknown" - throw new AssertionError( - message || - `Expected value to be instance of '${ctorName}', but got '${value?.constructor?.name ?? typeof value}': (${Assert.safeStringify(value)})` - ) - } - } else { - throw new AssertionError("Invalid typeOrConstructor argument.") + if (typeof value !== type) { + throw new AssertionError( + message || + `Expected type '${type}', but got '${typeof value}': (${JSON.stringify(value)})` + ); } } + /** * Asserts that the provided function does NOT throw an error. * If an error is thrown, an AssertionError is thrown with the provided message or details of the error. @@ -250,7 +237,6 @@ class Assert { * @param fn - A function that is expected to NOT throw. * Must be passed as a function reference, e.g. '() => codeThatShouldNotThrow()'. * @param message - (Optional) Prefix for the error message if the assertion fails. - * * @throws AssertionError - If the function throws any error. * * @example @@ -316,7 +302,7 @@ class Assert { * @param a - Actual array. * @param b - Expected array. * @param message - (Optional) Prefix message for errors. - * @throws Error - If arrays differ in length, type, or value at any index. + * @throws AssertionError - If arrays differ in length, type, or value at any index. * @private */ private static arraysEqual(a: T[], b: T[], message: string = ""): boolean { @@ -373,6 +359,9 @@ class Assert { } } +// #endregion Assert + +// #region TestRunner /** * A utility class for managing and running test cases with controlled console output. * TestRunner' supports configurable verbosity levels and allows structured logging @@ -470,6 +459,8 @@ class TestRunner { } } +// #endregion TestRunner + // =========================================================== // End of Lightweight unit testing framework for Office Script // =========================================================== @@ -488,4 +479,9 @@ if (typeof globalThis !== "undefined") { // @ts-ignore globalThis.AssertionError = AssertionError } -} \ No newline at end of file +} + +//#endregion unit-test-framework.ts + + + diff --git a/tsconfig.json b/tsconfig.json index 2c64d6b..d28a3ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,12 @@ // - All strict type checks except strictNullChecks are enabled for code quality. // - Side comments explain individual settings inline. // - This config includes all code folders for full IntelliSense in VS Code. For a production-only build, use a separate config if needed. +// +// - "typeRoots" specifies directories where TypeScript should look for type definition files (*.d.ts). +// * "./types" is included to provide Office Scripts and custom global type definitions for this project. +// * "./node_modules/@types" is included for compatibility with community and built-in type packages (e.g., for Node.js or testing frameworks). +// * Only .d.ts files in these folders will be included for global types, so make sure all necessary type definitions are placed accordingly. +// { "compilerOptions": { "emitDeclarationOnly": true, // Only output .d.ts files, not .js @@ -21,17 +27,21 @@ "target": "es2019", // Set JS target (irrelevant here, but required) "strict": true, // Enable all strict type-checking options "strictNullChecks": false, // Disable strict null/undefined checks (see note above) - "esModuleInterop": true // Enable compatibility for ES modules/CommonJS imports + "esModuleInterop": true, // Enable compatibility for ES modules/CommonJS imports + "typeRoots": [ + "./types", // Look for custom and Office Scripts type definitions here + "./node_modules/@types" // Standard location for community-provided type definitions + ] }, "include": [ - "src/**/*", // Main source code - "test/**/*", // Unit and integration tests - "mocks/**/*", // Mocks for unit testing - "wrappers/**/*", // Wrappers for dependency injection/testing - "office-scripts.d.ts" // Office Scripts type definitions + "src/**/*", + "test/**/*", + "mocks/**/*", + "wrappers/**/*", + "types/**/*.d.ts" ], "exclude": [ - "node_modules", - "dist" + "dist", + "node_modules" ] } \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json index c397491..70834d0 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,3 +1,9 @@ +// This configuration extends the base tsconfig.json for local testing. +// - "extends" imports all settings from tsconfig.json, but any property listed here overrides the base. +// - The "include" array here replaces (does NOT merge with) the one in the base config. +// * This allows you to include test, mocks, and wrappers folders for test runs without affecting production builds. +// * Also includes "types/**/*.d.ts" to ensure all custom and Office Scripts type definitions are available during tests. +// - "compilerOptions" may override or add to those in the base config. { "extends": "./tsconfig.json", "compilerOptions": { @@ -8,6 +14,7 @@ "src/**/*", "test/**/*", "mocks/**/*", - "wrappers/**/*" + "wrappers/**/*", + "types/**/*.d.ts" ] } \ No newline at end of file diff --git a/types/excel-script-globals.d.ts b/types/excel-script-globals.d.ts new file mode 100644 index 0000000..167499d --- /dev/null +++ b/types/excel-script-globals.d.ts @@ -0,0 +1,18 @@ +// Augment global scope for test/mock detection and runtime ExcelScript environment flag. +// +// - This file declares a global variable for use in detecting the execution environment in code and tests. +// - DO NOT declare `var ExcelScript` here, as that would cause a duplicate identifier error with the ExcelScript namespace. +// - In your test setup (not in a .d.ts), you can assign to globalThis.ExcelScript as needed. +// - Use ExcelScriptIsMock to distinguish between Office Scripts and test/runtime environments. + +export {}; // Required for proper global augmentation + +declare global { + /** + * True if running in a mocked/test environment. Used for environment detection. + * Assign in your test setup: globalThis.RunSyncTest = true to force synchronous execution, i.e. with + * no delay between script execution and test assertions. + */ + var RunSyncTest: boolean | undefined; + +} \ No newline at end of file diff --git a/office-scripts.d.ts b/types/office-scripts/index.d.ts similarity index 98% rename from office-scripts.d.ts rename to types/office-scripts/index.d.ts index 0514a38..278fbc7 100644 --- a/office-scripts.d.ts +++ b/types/office-scripts/index.d.ts @@ -1,4 +1,3 @@ -// office-scripts.d.ts // // - Declares a minimal subset of the Office Scripts ExcelScript namespace for use in local development and testing. // - This file provides enough typing to write and test scripts that interact with Excel ranges, worksheets, and workbooks. diff --git a/wrappers/main-wrapper.ts b/wrappers/main-wrapper.ts index 4ef7960..727449e 100644 --- a/wrappers/main-wrapper.ts +++ b/wrappers/main-wrapper.ts @@ -1,4 +1,4 @@ -/// + // main-wrapper.ts // // - This file acts as the entry point for running all tests in a local or CI environment.