From 07a44501b7e0ea77c446f0e99f15e7b1278d8f7b Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 00:09:59 -0400
Subject: [PATCH 01/13] chore: [main-review] minor improvements and review
updates, mainly in test/main.ts
---
src/logger.ts | 28 ++++++++------
test/main.ts | 76 ++++++++++++++++++++++++++-----------
test/unit-test-framework.ts | 6 +++
3 files changed, 76 insertions(+), 34 deletions(-)
diff --git a/src/logger.ts b/src/logger.ts
index fb6a58a..27fa19f 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -1470,6 +1470,7 @@ class LoggerImpl implements Logger {
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,
@@ -1767,26 +1768,29 @@ 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
}
/**
@@ -1849,7 +1853,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
@@ -1912,7 +1916,7 @@ private log(msg: string, type: LOG_EVENT, extraFields?: LogEventExtraFields): vo
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}')`
diff --git a/test/main.ts b/test/main.ts
index 1d9ccf6..3e84d74 100644
--- a/test/main.ts
+++ b/test/main.ts
@@ -11,11 +11,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
@@ -141,7 +141,7 @@ 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
+ // 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 {
@@ -227,7 +227,7 @@ class TestCase {
}
/**
- * 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],
@@ -1345,7 +1345,7 @@ 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()
@@ -1356,6 +1356,34 @@ class TestCase {
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,21 +1415,6 @@ 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();
}
@@ -1616,7 +1629,7 @@ class TestCase {
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
@@ -1639,11 +1652,30 @@ class TestCase {
actual = logger.toString()
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 in loggerToString 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 in loggerToString with extra fields", `+
+ `extraFields={"userId":123,"sessionId":"abc"}}} ConsoleAppender: {}]}`
+ actual = logger.toString()
+ 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 */
diff --git a/test/unit-test-framework.ts b/test/unit-test-framework.ts
index a631c8b..01c0e6e 100644
--- a/test/unit-test-framework.ts
+++ b/test/unit-test-framework.ts
@@ -43,6 +43,7 @@ class AssertionError extends Error {
}
}
+// #region Assert
/**
* Utility class for writing unit-test-style assertions.
* Provides static methods to assert value equality and exception throwing.
@@ -373,6 +374,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 +474,8 @@ class TestRunner {
}
}
+// #endregion TestRunner
+
// ===========================================================
// End of Lightweight unit testing framework for Office Script
// ===========================================================
From db29f75872a2621db68ac5b12289000d8c3fc845 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 00:48:10 -0400
Subject: [PATCH 02/13] chore: sync with Node 20 environment
---
docs/git-basics.md | 93 +++++++++++++++++++++++
node_modules/.package-lock.json | 8 +-
node_modules/@types/node/README.md | 2 +-
node_modules/@types/node/assert.d.ts | 4 +-
node_modules/@types/node/fs.d.ts | 2 +-
node_modules/@types/node/fs/promises.d.ts | 5 +-
node_modules/@types/node/package.json | 4 +-
package-lock.json | 12 +--
8 files changed, 113 insertions(+), 17 deletions(-)
create mode 100644 docs/git-basics.md
diff --git a/docs/git-basics.md b/docs/git-basics.md
new file mode 100644
index 0000000..8a6af25
--- /dev/null
+++ b/docs/git-basics.md
@@ -0,0 +1,93 @@
+# 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
+```
+
+## 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/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,
From 90b2aed662f92192ee58e870ea0af48196a6c1e3 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 01:12:51 -0400
Subject: [PATCH 03/13] fix: use cross-platform script to strip test-only
blocks
---
package.json | 2 +-
scripts/strip-testonly.js | 22 ++++++++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
create mode 100644 scripts/strip-testonly.js
diff --git a/package.json b/package.json
index 8a1c90f..d7e897b 100644
--- a/package.json
+++ b/package.json
@@ -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
From defa7972309c8758025c108b504d0138f8d18877 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 01:44:53 -0400
Subject: [PATCH 04/13] fix: update workflow configuration
---
.github/workflows/ci.yaml | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 79e288f..571437d 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:
From 23d17bd7097b1ee4a636ddb9aea091298bb57eba Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 01:50:55 -0400
Subject: [PATCH 05/13] chore: retrigger CI
From cac02be68122caa4e461508c87efc6f0cfbc005b Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 13:04:57 -0400
Subject: [PATCH 06/13] chore: retrigger CI
From af8bb9578f7e81cbe96bdf70c342d6d9a0bcfc57 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 13:07:12 -0400
Subject: [PATCH 07/13] chore: retrigger CI
From 8c9290f4510255e89895bc2ddd37f38f77bb9cc4 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 19:15:37 -0400
Subject: [PATCH 08/13] chore: retrigger CI
From 9fefa62af66ee9695eb23aabb0850db3d67fd37f Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 19:23:09 -0400
Subject: [PATCH 09/13] chore: retrigger CI after branch protection fix
From d371b3376a46a6968676beccfd4aafe8eb297c98 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 19:25:25 -0400
Subject: [PATCH 10/13] fix: update workflow configuration
---
.github/workflows/ci.yaml | 24 +-----------------------
1 file changed, 1 insertion(+), 23 deletions(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 571437d..4daa289 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -3,26 +3,4 @@ name: CI
on:
pull_request:
branches:
- - main
-
-jobs:
- build-and-test:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build TypeScript (production)
- run: npm run build
-
- - name: Run tests (local Node.js + mocks)
- run: npm test
\ No newline at end of file
+ - main
\ No newline at end of file
From 8ef9770dc33fe48e582167248877760b48caccc3 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Fri, 20 Jun 2025 19:37:44 -0400
Subject: [PATCH 11/13] fix: update workflow configuration
---
.github/workflows/ci.yaml | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 4daa289..fc7dcb2 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -3,4 +3,26 @@ name: CI
on:
pull_request:
branches:
- - main
\ No newline at end of file
+ - main
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build TypeScript (production)
+ run: npm run build
+
+ - name: Run tests (local Node.js + mocks)
+ run: npm test
\ No newline at end of file
From 6a4fa0b8ef33178ad91fd8b82c10cb34d6464071 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Sat, 21 Jun 2025 01:24:39 -0400
Subject: [PATCH 12/13] Fixed Office Script errors, still some exeuction
errors, push to share
---
CHANGELOG.md | 4 +-
docs/git-basics.md | 7 +-
src/logger.ts | 612 +++++++++++++++++++-----------------
test/main.ts | 205 +++++++-----
test/unit-test-framework.ts | 129 ++++----
5 files changed, 504 insertions(+), 453 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f52f61..fad50f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,13 +7,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [Unreleased]
+### Changes
+- **`ExcelAppender`**: Now getInstance instead of getting as input the differen font colors, the user can enter a map of colors. The default color map is now public, so the user can use it as a reference to set, chagne any of the colors.
---
## [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/docs/git-basics.md b/docs/git-basics.md
index 8a6af25..f66cd75 100644
--- a/docs/git-basics.md
+++ b/docs/git-basics.md
@@ -24,9 +24,10 @@ git status # Show status of changes
- **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 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
diff --git a/src/logger.ts b/src/logger.ts
index 27fa19f..9f8118c 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -1,3 +1,5 @@
+
+// #region logger.ts
// ===============================================
// Lightweight logging Framework for Office Script
// ===============================================
@@ -72,8 +74,8 @@ 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
@@ -375,6 +377,68 @@ interface Logger {
// 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 +446,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,15 +474,15 @@ 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`);
+ }
}
-}
}
@@ -448,7 +512,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 +586,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,57 +604,57 @@ 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
@@ -614,14 +678,14 @@ class LayoutImpl implements Layout {
* 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}`
-})
+ 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.
@@ -630,15 +694,15 @@ public static readonly shortFormatterFun = Object.freeze(function shortLayoutFor
* 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}`
-})
+ 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}`
+ })
/**
* Function used to convert a LogEvent into a string.
@@ -795,65 +859,6 @@ public static readonly defaultFormatterFun = Object.freeze(function defaultLayou
}
// #endregion LayoutImpl
-// #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
// #region AbstractAppender
/**
@@ -877,17 +882,17 @@ abstract class AbstractAppender implements Appender {
}
)
// 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 +923,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
}
}
@@ -1142,11 +1147,11 @@ class ConsoleAppender extends AbstractAppender implements Appender {
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]: `
@@ -1174,57 +1179,68 @@ 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 */
+ /* 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 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(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
- };
+ this.clearCellIfNotEmpty()
+ this._eventFonts = eventFonts
// Set the default layout if not set
if (!AbstractAppender.getLayout()) {
AbstractAppender.setLayout(new LayoutImpl()); // Default layout if not set
}
}
+ // 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}
+ }
+
/**
* 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
@@ -1232,10 +1248,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,16 +1262,12 @@ 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.`;
@@ -1274,14 +1286,20 @@ 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)
}
- return ExcelAppender._instance;
+ return ExcelAppender._instance
}
// #TEST-ONLY-START
@@ -1315,17 +1333,17 @@ class ExcelAppender extends AbstractAppender implements Appender {
* @throws ScriptError, if the singleton was not instantiated.
*/
public toString(): string {
- ExcelAppender.validateInstance("ExcelAppender.toString") // Validate the instance;
- 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(
+ ExcelAppender.validateInstance("ExcelAppender.toString");
+ const NAME = this.constructor.name;
+ const ADDRESS = this._msgCellRng.getAddress();
+ // 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}}}`;
- return output
+ + `eventfonts={${EVENT_COLORS}}}`;
+ return output;
}
/**
@@ -1338,7 +1356,7 @@ class ExcelAppender extends AbstractAppender implements Appender {
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)
@@ -1370,7 +1388,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'.`
@@ -1394,15 +1412,14 @@ 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}`
- )
+ 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(", ")}`
+ );
+ }
}
-}
}
// #endregion ExcelAppender
@@ -1645,50 +1662,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.
@@ -1777,12 +1794,12 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void {
return label ?? UNKNOWN
}
- /**
- * 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".
- */
+ /**
+ * 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
const UNKNOWN = "UNKNOWN"
@@ -1806,8 +1823,8 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void {
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(", ")}]`
- : "[]"
+ ? `[${this._appenders.map(a => a.toString()).join(", ")}]`
+ : "[]"
const scalarInfo = `level: "${levelTk}", action: "${actionTk}", errCnt: ${LoggerImpl._instance._errCnt}, warnCnt: ${LoggerImpl._instance._warnCnt}`
return `${NAME}: {${scalarInfo}, appenders: ${appendersString}}`
}
@@ -1825,13 +1842,13 @@ public trace(msg: string, extraFields?: LogEventExtraFields): void {
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 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.
@@ -1881,35 +1898,35 @@ 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 = (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
}
- LoggerImpl._instance._criticalEvents.push(lastEvent)
- if (LoggerImpl._instance._action === LoggerImpl.ACTION.EXIT) {
- const LAST_MSG = AbstractAppender.getLayout().format(lastEvent)
- throw new ScriptError(LAST_MSG)
+ 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()")
+ }
+ LoggerImpl._instance._criticalEvents.push(lastEvent)
+ if (LoggerImpl._instance._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 */
@@ -2051,4 +2068,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 3e84d74..521c8cc 100644
--- a/test/main.ts
+++ b/test/main.ts
@@ -1,3 +1,6 @@
+
+
+// #region main.ts
// ----------------------------------------
// Testing the Logging framework
// ----------------------------------------
@@ -142,9 +145,9 @@ 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. Used in loggerImplLevels.
- public static sendLog(msg: string, type: LOG_EVENT, extraFields: LogEventExtraFields,
+ 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
@@ -238,7 +241,7 @@ class TestCase {
// 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 +259,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 +281,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,7 +289,7 @@ 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
@@ -315,7 +316,7 @@ class TestCase {
// Checking the content of the excel cell
let CONTEXT = `-[level,action]=[${levelStr},${actionStr}]-${context}`
Assert.isNotNull(msgCellRng, `ExcelAppender(getInstance)-msgCellRng is not null${CONTEXT}`)
- let actualMsg = TestCase.removeTimestamp(msgCellRng.getValue()) // under default configuration the output has stimestamp
+ let actualMsg = TestCase.removeTimestamp(msgCellRng.getValue().toString()) // under default configuration the output has stimestamp
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}`)
@@ -565,11 +566,11 @@ 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")
}
@@ -578,10 +579,10 @@ class TestCase {
cause = new Error("Root cause");
origin = new ScriptError("Wrapper error", cause);
origin.rethrowCauseIfNeeded();
- Assert.fail("Expected root cause Error to be thrown");
+ //Assert.fail("Expected root cause Error to be thrown");
} catch (e) {
- Assert.instanceOf(e, Error);
- Assert.notInstanceOf(e, ScriptError);
+ 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");
}
@@ -593,37 +594,71 @@ class TestCase {
top.rethrowCauseIfNeeded();
Assert.fail("Expected root Error to be thrown");
} catch (e) {
- Assert.instanceOf(e, Error);
- Assert.notInstanceOf(e, ScriptError);
+ 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
expectedMsg = "Test message"
expectedType = LOG_EVENT.INFO
event = new LogEventImpl(expectedMsg, expectedType)
-
+
// 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. ` +
@@ -728,47 +763,40 @@ 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
- let eventExtras: LogEventExtraFields = {userId: 123, sessionId: "abc"}
+ let eventExtras: LogEventExtraFields = { userId: 123, sessionId: "abc" }
// Testing constructor
expectedMsg = "Test message"
expectedType = LOG_EVENT.INFO
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)
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())
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 +809,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())")
@@ -825,7 +852,7 @@ class TestCase {
)
// Testing Constructor with non valid date
-
+
errMsg = "[LogEventImpl.constructor]: LogEvent.timestamp='null' property must be a Date."
expectedMsg = "Test message"
Assert.throws(
@@ -951,14 +978,14 @@ 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"
)
-
+
TestCase.clear()
}
@@ -972,7 +999,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 +1007,6 @@ 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")
// Testing static properties have default values (null)
Assert.isNull((appender as AbstractAppender).getLastLogEvent(), "ConsoleAppender(getInstance) has no last log event")
@@ -993,7 +1019,6 @@ 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"]}`
@@ -1162,15 +1187,15 @@ 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
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)
@@ -1182,7 +1207,7 @@ class TestCase {
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)
+ // 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))")
@@ -1203,7 +1228,7 @@ class TestCase {
Assert.equals(actualEvent!.extraFields.sessionId, extraFields.sessionId,
"ExcelAppender(getLastLogEvent).extraFields.sessionId with extra fields"
)
- // Now checking the excel cell value (formatted via format method)
+ // 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)")
@@ -1243,11 +1268,33 @@ class TestCase {
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)
+ // 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"
+ )
+
// Script Errors
ExcelAppender.clearInstance() // singleton is undefined
errMsg = "[AbstractAppender.log]: A singleton instance can't be undefined or null. Please invoke getInstance first"
@@ -1299,30 +1346,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 +1378,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)")
@@ -1350,8 +1393,6 @@ class TestCase {
// 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")
@@ -1435,7 +1476,6 @@ class TestCase {
actualNum = logger.getAppenders().length ?? 0
Assert.equals(actualNum, expectedNum, "Logger(Lazy init)-appender")
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
@@ -1484,7 +1524,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
@@ -1560,7 +1600,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
@@ -1625,7 +1665,7 @@ 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
@@ -1642,13 +1682,13 @@ class TestCase {
const MSGS = ["Error event in loggerToString", "Warning event in loggerToString"]
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 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", eventfonts={ERROR="9c0006",WARN="ed7d31",INFO="548235",TRACE="7f7f7f"}}]}`
actual = logger.toString()
Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger)")
@@ -1658,10 +1698,10 @@ class TestCase {
logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO)
logger.info("Info event in loggerToString 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 in loggerToString with extra fields", `+
- `extraFields={"userId":123,"sessionId":"abc"}}} ConsoleAppender: {}]}`
+ 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 in loggerToString with extra fields", ` +
+ `extraFields={"userId":123,"sessionId":"abc"}}} ConsoleAppender: {}]}`
actual = logger.toString()
Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger with extra fields)")
@@ -1675,7 +1715,7 @@ class TestCase {
// 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 */
@@ -1916,9 +1956,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;
}
+// #endregion main.ts
+
diff --git a/test/unit-test-framework.ts b/test/unit-test-framework.ts
index 01c0e6e..097a5ee 100644
--- a/test/unit-test-framework.ts
+++ b/test/unit-test-framework.ts
@@ -1,3 +1,5 @@
+
+// #region unit-test-framework.ts
// ====================================================
// Lightweight unit testing framework for Office Script
// ====================================================
@@ -13,6 +15,7 @@
* version 2.0.0
*/
+// #region AssertionError
/**
* AssertionError is a custom error type used to indicate assertion failures in tests or validation utilities.
*
@@ -42,12 +45,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 {
/**
@@ -98,7 +102,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.
@@ -118,21 +122,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})`
+ );
}
}
@@ -157,6 +178,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`.
@@ -170,38 +192,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.
*
@@ -213,37 +203,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.
@@ -251,7 +236,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
@@ -317,7 +301,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 {
@@ -374,9 +358,9 @@ class Assert {
}
}
- // #endregion Assert
+// #endregion Assert
- // #region TestRunner
+// #region TestRunner
/**
* A utility class for managing and running test cases with controlled console output.
* TestRunner' supports configurable verbosity levels and allows structured logging
@@ -494,4 +478,7 @@ if (typeof globalThis !== "undefined") {
// @ts-ignore
globalThis.AssertionError = AssertionError
}
-}
\ No newline at end of file
+}
+
+//#endregion unit-test-framework.ts
+
From a5d591cf4c005319b7af87f41154661456a9ece3 Mon Sep 17 00:00:00 2001
From: David Leal
Date: Thu, 26 Jun 2025 00:51:12 -0400
Subject: [PATCH 13/13] Code tested in Office Script too, improved
documentation
---
.vscode/settings.json | 3 +
CHANGELOG.md | 11 +-
README.md | 63 +-
docs/typedoc/assets/hierarchy.js | 2 +-
docs/typedoc/assets/search.js | 2 +-
docs/typedoc/classes/AbstractAppender.html | 29 +-
docs/typedoc/classes/ConsoleAppender.html | 33 +-
docs/typedoc/classes/ExcelAppender.html | 67 +-
docs/typedoc/classes/LayoutImpl.html | 38 +-
docs/typedoc/classes/LogEventImpl.html | 18 +-
docs/typedoc/classes/LoggerImpl.html | 57 +-
docs/typedoc/classes/ScriptError.html | 16 +-
docs/typedoc/classes/Utility.html | 8 +-
docs/typedoc/enums/LOG_EVENT.html | 4 +-
docs/typedoc/hierarchy.html | 2 +-
docs/typedoc/interfaces/Appender.html | 10 +-
docs/typedoc/interfaces/Layout.html | 6 +-
docs/typedoc/interfaces/LogEvent.html | 12 +-
docs/typedoc/interfaces/Logger.html | 40 +-
docs/typedoc/types/LayoutFormatter.html | 2 +-
docs/typedoc/types/LogEventExtraFields.html | 2 +-
docs/typedoc/types/LogEventFactory.html | 2 +-
mocks/excelscript.mock.ts | 8 +-
package.json | 2 +-
src/logger.ts | 409 ++++++-----
test/main.ts | 675 ++++++++++++------
test/unit-test-framework.ts | 5 +-
tsconfig.json | 26 +-
tsconfig.test.json | 9 +-
types/excel-script-globals.d.ts | 18 +
.../office-scripts/index.d.ts | 1 -
wrappers/main-wrapper.ts | 2 +-
32 files changed, 972 insertions(+), 610 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 types/excel-script-globals.d.ts
rename office-scripts.d.ts => types/office-scripts/index.d.ts (98%)
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 fad50f3..2a06a9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,15 @@ 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]
-### Changes
-- **`ExcelAppender`**: Now getInstance instead of getting as input the differen font colors, the user can enter a map of colors. The default color map is now public, so the user can use it as a reference to set, chagne any of the colors.
+### 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.
---
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/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.
Constructs a new AbstractAppender instance. Nothing is initialized, because the class only has static properties
that are lazy initialized or set by the user.
ScriptError if
- The event is not a valid LogEvent.
Remarks
Subclasses must call setLastLogEvent(event) after successfully sending the event,
otherwise getLastLogEvent() will not reflect the most recent log event.
-
toString
toString():string
Returns a string representation of the appender.
+
toString
toString():string
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.
-
StaticclearLayout
clearLayout():void
Sets to null the static layout, useful for running different scenarios.
-
Returns void
StaticclearLogEventFactory
clearLogEventFactory():void
Sets to null the log event factory, useful for running different scenarios.
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.
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
// Example: Custom LogEvent to be used to specify the environment where the log event was created. letprodLogEventFactory: LogEventFactory = functionprodLogEventFactoryFun(message: string, eventType: LOG_EVENT) { returnnewLogEventImpl("PROD-" + message, eventType) // add environment prefix } AbstractAppender.setLogEventFactory(prodLogEventFactory) // Now all appenders will use ProdLogEvent
Constructs a new AbstractAppender instance. Nothing is initialized, because the class only has static properties that are lazy initialized or set by the user.
-