From 0c41edaa55f102616e11985e6ac33b60cfd3dacd Mon Sep 17 00:00:00 2001 From: David Leal Date: Tue, 8 Jul 2025 23:13:30 -0400 Subject: [PATCH] Integrated with officescripts-unit-test-framewor --- .DS_Store | Bin 8196 -> 8196 bytes .gitignore | 3 +- CHANGELOG.md | 7 + README.md | 11 +- docs/.DS_Store | Bin 6148 -> 6148 bytes docs/git-basics.md | 152 ++++++++--- package.json | 2 +- test/main.ts | 341 ++++++++++++------------ test/unit-test-framework.ts | 501 ++++++++++++++++++++++++++++++------ 9 files changed, 735 insertions(+), 282 deletions(-) diff --git a/.DS_Store b/.DS_Store index ca563be3e7b6652369c3877b0fc6abc1cddc92b4..0800b25b413cfe52aa77c8b99910abc10f766d29 100644 GIT binary patch literal 8196 zcmeI1zi-n(6vy8s4owL{87r}v1&IMf#lRY>@DDTt8x2XSwjzl`et=+rRD!A-D-sNh zOb7`HQAP$31Nxz zUPG7DIL=3=m8|CqtU^8Yw^m!-XfVoTdWToQE8rFI3U~#)0{@BvShKlR3)X!T^}ScX zD{w3o;P*p_gKfjwxv~1yflhA$U<0^y!+D)gU_)yg*3OMZ1WlPzSW~L(7DJhG_(Pl5 zuy$^&DJNw&AIgrb><&fg(Q*Ayr;}wB+&S0JkZ@7)8sNL#c+Q)mC)Irm7$22r!w z=|>&-s+X@{`1tGer>kzfw=h217T*SjZIsedJT7h1j5^Nfqz;MvBw?&|C=n3Fy~(;g7A&# z3rd*0xC2vr!Q)nFplffXIJ^W1w*y4AFK$#KjrZQg&q&lusQe5E%gmKiG&Hk>HZ))l zGq_KiG?6sr&u4ysrZ88swVv-T56%2pke|+^KgpNe!J9Tv@SEfLJ;uBm(KMAbCnRqK z3;kZUSmHmseGp4~WC$s5ah622e?pXjI;O6igx2MtsqTHC*@#4Xyqh2;(Nw=|)Im@q zmLz~ic&0RsA$aN~P6wlO_Nk0Xkd1=`B&n6E+Lvf@()9thc?!*c5W;pXn*}ffZfCR_22GR{Q9rw;c@GJ16#fly8r+H delta 652 zcmZp1XmOa}XEU^hRb=42iL$+$3|{|pQaEDU-K=?s|+CAs-7E=f80NkB1l`qIJ3{K9^Edc6aV2C&{xmiGwi8*)jGJ$@^+{tEwa%?YFHc3h|PHqqs znVcucgWxL3DS))FFr)yTQOr=1la6E%quZ~^2Lz>98dAcn&(C3;Y$znj%z1a&WM3f(wvQV=7hh(aoGl~;V|EKkGjly)n0!D;meG0g zJs~OfUq{a>z5aV_@_!*|I9pNJgZV^N$z%s%naLf(JRINDmatz2h6h++2fDxuVP4ry zV1F|M-Cx9zjOv5rr;{&;h_P+W`VaEM8xaW@lS@=%vb!j6JmaS2VE>meq=Nj991*hG z_dt$=LTF=fVele@(f^bc9VjpJY_2Rb#8p#TU|fvn=(e1x%3 eb(?RA=`wCCn8&!8UE&*yJwz2Em5^uJY(@a_7t3$} diff --git a/.gitignore b/.gitignore index 04c01ba..8b4a82e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -dist/ \ No newline at end of file +dist/ +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a06a9b..e82a166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.1] – 2025-07-08 +### Changed +- Updated `test/unit-test-framework.ts` file with version 1.0.0 from [Unit Testing Framework](https://github.com/dlealv/officescripts-unit-test-framework) repository. +- Improved documentation of `git-basics.md` adding additional commands and common use cases. +- Minor corrections in `test/main.ts` file + ## [2.1.0] – 2025-06-26 ### Added diff --git a/README.md b/README.md index fcbd7e0..f8e1f3b 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,7 @@ This framework is designed to work seamlessly in both Node.js/TypeScript environ - **Tested Environments:** - Node.js/TypeScript (VSCode) - - Office Scripts (Excel on the web) + - Office Scripts (Excel) - **Usage in Office Scripts:** To use the framework in Office Scripts, just paste the `dist/logger.ts` file at the beginning of your script. If you want to run all tests, then you need to paste the source files into your script in the following order: @@ -349,7 +349,7 @@ This framework is designed to work seamlessly in both Node.js/TypeScript environ 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`. + - The code avoids unsupported keywords such as `any`, `require`, `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. - Office Scripts doesn't allow defining static properties that are functions. For example, `shortFormatterFun` must be defined outside the class. - Additional nuances and workarounds are documented in the source code comments. @@ -410,9 +410,10 @@ This ensures the logging framework is robust and reliable across both developmen ## Additional Information - For developer setup, testing, or CI details, see [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) -- For debug setup, see [VSCode Debugging.md](docs/VSCode%20Debugging.md) -- TypeDoc documentation: [TYPEDOC](docs/typedoc/index.html) -- Git basic documentation: [git-basics](docs/git-basics.md) +- For debug setup, see [docs/VSCode Debugging.md](docs/VSCode%20Debugging.md) +- TypeDoc documentation: [TYPEDOC](https://dlealv.github.io/officescripts-logging-framework/typedoc/) +- Git basic documentation: [docs/git-basics](docs/git-basics.md) +- Unit testing framework repository: [officescripts-unit-test-framework](https://github.com/dlealv/officescripts-unit-test-framework) from the same author. Used for testing current repository. Check repository's details for more information. ## License diff --git a/docs/.DS_Store b/docs/.DS_Store index be799be00eaa9d71a6252053d1af835351e2e893..f9b26f0c576b0604ce121e398fa747b9c9cde351 100644 GIT binary patch delta 59 zcmZoMXffEJ#>mJpS)EaynajC+vL~Y)TR^;;Ef@RbTt)>Lb26hM6Su+UgN$x$6C1cU Jvvd6A2LPK#4uAjv delta 59 zcmZoMXffEJ#>mJxS)EaynRE5L$)1dIYzLl5m0V_=oXe;HV@_sNWa65z`5>bk+r$R$ J&Fmb1`2i=V6Epw- diff --git a/docs/git-basics.md b/docs/git-basics.md index f66cd75..8745ab0 100644 --- a/docs/git-basics.md +++ b/docs/git-basics.md @@ -1,12 +1,20 @@ # Git Basic Operations Reference +A concise cheat sheet with essential commands and common workflows for daily Git usage. +**Tip:** Use `git help ` for detailed info about any command! + +--- + ## 1. Setup ```bash git config --global user.name "Your Name" git config --global user.email "you@example.com" +git config --global core.editor "code --wait" # Set VSCode as editor (optional) ``` +--- + ## 2. Creating a Repository ```bash @@ -14,81 +22,163 @@ 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 + +- **Staging**: Moves changes from your working directory to the staging area. +- **Committing**: Captures a snapshot of the currently staged changes. + ```bash git add # Stage a specific file -git add . # Stage all files +git add . # Stage all files (including new, modified, deleted) git commit -m "Message" # Commit staged changes -git commit --allow-empty -m "Message" # A commit with no changes +git commit --allow-empty -m "Message" # Commit with no changes (create marker) ``` +--- + ## 5. Viewing History ```bash -git log # View commit history -git log --oneline # Condensed log +git log # View commit history +git log --oneline # Condensed log (one commit per line) +git log -p # Show changes for a file over time +git diff # Show differences between two commits ``` +--- + ## 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 +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 +git branch -m # Rename 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 + +- **Pulling**: Fetches changes from a remote repository and integrates them into your local branch. +- **Pushing**: Uploads local commits 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 +git pull # Fetch and merge from remote (default remote/branch) +git pull origin main # Fetch and merge changes from 'main' on 'origin' +git push # Push changes to remote (current branch) +git push -u origin # Push new branch and set upstream tracking ``` +**Note**: `origin` is the default name of the remote repository on GitHub. + +--- ## 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!) +git checkout -- # Discard local changes in working directory (before staging) +git reset HEAD # Unstage a file (keep changes in working directory) +git revert # Create new commit to undo changes (safe, doesn't rewrite history) +git reset --hard # Danger! Discard ALL history and changes since ``` +--- + ## 9. Tags ```bash -git tag # List tags -git tag # Create tag -git push origin # Push tag to remote +git tag # List tags +git tag # Create tag at current commit +git tag -a -m "msg" # Annotated tag with message +git push origin # Push tag to remote +git push origin --tags # Push all tags 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 + +- 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 +git rebase # Rebase current branch onto base +git rebase -i # Interactive rebase for editing history ``` +--- + ## 11. Stashing -Allows temporarily save uncommitted changes + +- Temporarily save uncommitted changes. + +```bash +git stash # Stash unsaved changes +git stash pop # Apply and remove latest stash +git stash list # List all stashes +git stash apply stash@{n} # Apply specific stash (keeps it in stash list) +``` + +--- + +## 12. Common Multi-Step Workflows + +### A. Sync local repo with remote (bring latest changes) + +```bash +git checkout main +git pull origin main +``` + +### B. Create and work on a new feature branch + +```bash +git checkout main +git pull origin main # Make sure main is up to date +git checkout -b my-feature # Create and switch to new branch +# ...edit files... +git add . +git commit -m "Implement feature" +git push -u origin my-feature # Push branch and set upstream +``` + +### C. Merge a finished feature branch into main + +```bash +git checkout main +git pull origin main # Update local main +git merge my-feature +git push origin main # Push merged changes +``` + +### D. Update your feature branch with latest main + +```bash +git checkout my-feature +git pull origin main # Merge latest main into your branch +``` +*(Or use `git rebase origin/main` if rebasing is preferred)* + +### E. Delete a merged feature branch (locally and remotely) + ```bash -git stash # Stash unsaved changes -git stash pop # Apply stashed changes +git branch -d my-feature # Delete local branch +git push origin --delete my-feature # Delete remote branch ``` --- **Tip:** -Use `git help ` for detailed info about any command! +Use `git help ` for detailed info about any command! \ No newline at end of file diff --git a/package.json b/package.json index a68ab9b..4262661 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "officescripts-logging-framework", - "version": "2.1", + "version": "2.1.1", "description": "Lightweight, extensible logging framework for Office Scripts, inspired by libraries like Log4j. Enables structured logging via a singleton 'Logger', supporting multiple log levels ('Logger.LEVEL') and pluggable output targets through the 'Appender' interface", "main": "index.js", "directories": { diff --git a/test/main.ts b/test/main.ts index 2f3d12b..78019fe 100644 --- a/test/main.ts +++ b/test/main.ts @@ -117,7 +117,7 @@ class TestCase { AbstractAppender.clearLayout() // Clear the layout } - // Utility to escape regex special characters in variables + // Utility method to escape regex special characters in variables for testing purposes. public static escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } @@ -172,8 +172,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. + /** Helper method to send all possible log event during the testing process consider all possible ACTION value scenarios. + * 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 { @@ -259,7 +260,6 @@ class TestCase { } - /** * Helper method to simplify testing scenarios for all possible combinations of LEVEL,ACTION. Except for OFF level. */ @@ -567,7 +567,7 @@ class TestCase { () => { throw new ScriptError(expectedMsg) }, ScriptError, expectedMsg, - "scriptError(notcause)" + "scriptError-notcause" ) // Testing raising a ScriptError with cause @@ -578,33 +578,21 @@ class TestCase { () => { throw origin }, ScriptError, expectedMsg, - "scriptError(with cause)" + "scriptError-with cause" ) - function buildRegex(trigger: ScriptError): RegExp {// Building regex for toString - let NAME = trigger.cause ? trigger.cause.name : trigger.name - let MSG = trigger.cause ? trigger.cause.message : trigger.message - const regex = new RegExp( - `^${TestCase.escapeRegex(trigger.name)}: ${TestCase.escapeRegex(trigger.message)}\\n` + // Header - `Stack trace:\\n` + // Stack section - `${TestCase.escapeRegex(NAME)}: ${TestCase.escapeRegex(MSG)}\\n` + // type and message - `( +at .+\\n?)+$` // Variable stack trace lines - ) - return regex - } - let scriptError = new ScriptError("Script Error message") let scriptErrorwithCause = new ScriptError("Script Error message", cause) // Testing without cause let regex = buildRegex(scriptError) Assert.equals(regex.test(scriptError.toString()), true, - "scriptError(toString without cause)" + "scriptError-toString without cause" ) // Testing with cause regex = buildRegex(scriptErrorwithCause) Assert.equals(regex.test(scriptErrorwithCause.toString()), true, - "scriptError(toString with cause)" + "scriptError-toString with cause" ) // Testing rethrowCauseIfNeeded @@ -613,10 +601,10 @@ class TestCase { try { const err = new ScriptError(expectedMsg) err.rethrowCauseIfNeeded() - Assert.fail("Expected ScriptError to be thrown") + Assert.fail("scriptError(Expected ScriptError to be thrown)") } catch (e) { - isScriptError(e, "LogEvent(rethrowCauseIfNeeded)-No cause") - Assert.equals((e as ScriptError).message, expectedMsg, "LogEvent(rethrowCauseIfNeeded)-Top-level error") + isScriptError(e, "scriptError(rethrowCauseIfNeeded)-No cause") + Assert.equals((e as ScriptError).message, expectedMsg, "scriptError(rethrowCauseIfNeeded)-Top-level error") } // Cause is not a ScriptError, so it should be rethrown @@ -626,8 +614,8 @@ class TestCase { origin.rethrowCauseIfNeeded() //Assert.fail("Expected root cause Error to be thrown") } catch (e) { - isError(e, "LogEvent(rethrowCauseIfNeeded)-Root cause is Error") - isNotScriptError(e, "LogEvent(rethrowCauseIfNeeded)-Root cause is not a ScriptError") + isError(e, "scriptError(rethrowCauseIfNeeded)-Root cause is Error") + isNotScriptError(e, "scriptError(rethrowCauseIfNeeded)-Root cause is not a ScriptError") Assert.equals((e as Error).message, "Root cause") } @@ -639,14 +627,26 @@ class TestCase { top.rethrowCauseIfNeeded() Assert.fail("Expected root Error to be thrown") } catch (e) { - isError(e, "LogEvent(rethrowCauseIfNeeded)-Deepest cause is Error") - isNotScriptError(e, "LogEvent(rethrowCauseIfNeeded)-Deepest cause is not a ScriptError") + isError(e, "scriptError(rethrowCauseIfNeeded)-Deepest cause is Error") + isNotScriptError(e, "scriptError(rethrowCauseIfNeeded)-Deepest cause is not a ScriptError") Assert.equals((e as Error).message, "Root error") } TestCase.clear() // Clear all the instances // Inner functions + function buildRegex(trigger: ScriptError): RegExp {// Building regex for ScriptError.toString() method + let NAME = trigger.cause ? trigger.cause.name : trigger.name + let MSG = trigger.cause ? trigger.cause.message : trigger.message + const regex = new RegExp( + `^${TestCase.escapeRegex(trigger.name)}: ${TestCase.escapeRegex(trigger.message)}\\n` + // Header + `Stack trace:\\n` + // Stack section + `${TestCase.escapeRegex(NAME)}: ${TestCase.escapeRegex(MSG)}\\n` + // type and message + `( +at .+\\n?)+$` // Variable stack trace lines + ) + return regex + } + function isScriptError(value: unknown, message?: string): asserts value is ScriptError { if (!(value instanceof ScriptError)) { const prefix = message ? `${message}: ` : "" @@ -699,13 +699,13 @@ class TestCase { // Testing constructor: short layout layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // with short formatter - Assert.isNotNull(layout, "LayoutImpl(constructor-short layout is not null)") - Assert.equals((layout as LayoutImpl).getFormatter(), LayoutImpl.shortFormatterFun, "LayoutImpl(constructor-getFormatter() short formatter)") + Assert.isNotNull(layout, "LayoutImpl(constructor)-short layout is not null)") + 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.equals((layout as LayoutImpl).getFormatter(), LayoutImpl.defaultFormatterFun, "LayoutImpl(constructor-getFormatter() long formatter)") + Assert.isNotNull(layout, "LayoutImpl(constructor)-long layout is not null)") + 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. See LayoutImpl documentation for usage.` @@ -713,20 +713,20 @@ class TestCase { () => new LayoutImpl("Invalid formatter" as unknown as (event: LogEvent) => string), ScriptError, expectedStr, - "LayoutImpl(ScriptError)-constructor - invalid formatter" + "LayoutImpl(constructor)-invalid formatter" ) //Testing constructor: null formatter: null is valid, since it defaults to default formatter Assert.doesNotThrow(() => { new LayoutImpl(null as unknown as (event: LogEvent) => string) }, - "LayoutImpl(ScriptError)-constructor - null formatter") + "LayoutImpl(constructor)-null formatter") //Testing constructor: undefined formatter: undefined is valid, since it defaults to default formatter Assert.doesNotThrow(() => { new LayoutImpl(null as unknown as (event: LogEvent) => string) }, - "LayoutImpl(ScriptError)-constructor - undefined formatter") + "LayoutImpl(constructor)-undefined formatter") // Testing constructor: invalid formatter, return empty string errMsg = "[LayoutImpl.constructor]: Formatter function must return a non-empty string for a valid LogEvent. Got: empty string" @@ -734,7 +734,7 @@ class TestCase { () => new LayoutImpl(function alwaysEmpty(e) { return "" }), ScriptError, errMsg, - "LayoutImpl(constructor) - formatter returns empty string" + "LayoutImpl(constructor)-formatter returns empty string" ) // Testing constructor: invalid formatter return null @@ -743,7 +743,7 @@ class TestCase { () => new LayoutImpl(function alwaysNull(e) { return null as unknown as string }), ScriptError, errMsg, - "LayoutImpl(constructor) - formatter returns null" + "LayoutImpl(constructor)-formatter returns null" ) //Testing constructor: formatter with wrong arity (no arguments) @@ -752,7 +752,7 @@ class TestCase { () => new LayoutImpl(function zeroArgs() { return "ok" }), ScriptError, errMsg, - "LayoutImpl(constructor) - formatter with zero args" + "LayoutImpl(constructor)-formatter with zero args" ) //Testing constructor: formatter with wrong arity (2+ arguments) @@ -761,10 +761,9 @@ class TestCase { () => new LayoutImpl(function twoArgs(e:number, f:string) { return "ok" } as unknown as LayoutFormatter), ScriptError, errMsg, - "LayoutImpl(constructor) - formatter with two args" + "LayoutImpl(constructor)-formatter with two args" ) - // Testing format with short formatter layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // with short formatter expectedStr = `[${LOG_EVENT[expectedType]}] ${expectedMsg}` @@ -796,7 +795,7 @@ class TestCase { () => layout.format({ type: null, message: "msg", timestamp: new Date(), extraFields: {} } as unknown as LogEvent), ScriptError, errMsg, - "LayoutImpl(format) - invalid LogEvent (type=null)" + "LayoutImpl(format)-invalid LogEvent (type=null)" ) // Testing toString with short formatter @@ -881,7 +880,7 @@ class TestCase { 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 + // Testing the constructor 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.equals(Object.keys(actualEvent.extraFields).length, 0, "LogEventImpl(constructor with no extras)-extraFields is empty") @@ -899,7 +898,7 @@ class TestCase { // Testing constructor as undefined Assert.doesNotThrow( () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO, undefined as unknown as LogEventExtraFields, new Date()), - "LogEventImpl(ScriptError)-constructor - undefined extraFields" + "LogEventImpl(constructor)-undefined extraFields" ) // Testing properties of the LogEventImpl created @@ -916,7 +915,7 @@ class TestCase { () => new LogEventImpl(expectedMsg, -1 as LOG_EVENT), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - non valid LOG_EVENT" + "LogEventImpl(constructor)-non valid LOG_EVENT" ) // Testing constructor with null message @@ -926,7 +925,7 @@ class TestCase { () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - null message" + "LogEventImpl(constructor)-null message" ) // Testing constructor with undefined message @@ -936,7 +935,7 @@ class TestCase { () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - undefined message" + "LogEventImpl(constructor)-undefined message" ) // Testing constructor with an empty string @@ -946,15 +945,15 @@ class TestCase { () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - empty message" + "LogEventImpl(constructor)-empty message" ) // Testing constructor with white space only message Assert.throws( () => new LogEventImpl(" ", LOG_EVENT.INFO, {}, new Date()), ScriptError, - errMsg, - "LogEventImpl(constructor) - whitespace message throws" + errMsg, + "LogEventImpl(constructor)-whitespace message throws" ) // Testing Constructor with non valid date @@ -964,7 +963,7 @@ class TestCase { () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO, {}, null as unknown as Date), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - null timestamp" + "LogEventImpl(constructor)-null timestamp" ) // Testing constructor with wrong extraFields: null @@ -973,7 +972,7 @@ class TestCase { () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO, null as unknown as LogEventExtraFields, new Date()), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - null extraFields" + "LogEventImpl(constructor)-null extraFields" ) // Testing constructor with wrong extraFields: non valid function @@ -982,7 +981,7 @@ class TestCase { () => new LogEventImpl(expectedMsg, LOG_EVENT.INFO, "invalid" as unknown as LogEventExtraFields, new Date()), ScriptError, errMsg, - "LogEventImpl(ScriptError)-constructor - non valid extraFields" + "LogEventImpl(constructor)-non valid extraFields" ) // Testing constructor for extraFields with Deep object value: @@ -992,7 +991,7 @@ class TestCase { () => new LogEventImpl("msg", LOG_EVENT.INFO, { bad: { nested: true } } as unknown as LogEventExtraFields, new Date()), ScriptError, errMsg, - "LogEventImpl(constructor) - extraFields with deep object throws" + "LogEventImpl(constructor)-extraFields with deep object throws" ) // Testing constructor for extraFields with array value: @@ -1001,7 +1000,7 @@ class TestCase { () => new LogEventImpl("msg", LOG_EVENT.INFO, { bad: [1, 2, 3] } as unknown as LogEventExtraFields, new Date()), ScriptError, errMsg, - "LogEventImpl(constructor) - extraFields with array throws" + "LogEventImpl(constructor)-extraFields with array throws" ) // Testing constructor with extraFields with an undefined value @@ -1010,7 +1009,7 @@ class TestCase { () => new LogEventImpl("msg", LOG_EVENT.INFO, { bad: undefined }, new Date()), ScriptError, errMsg, - "LogEventImpl(constructor) - extraFields with undefined throws" + "LogEventImpl(constructor)-extraFields with undefined throws" ) // Testing toString @@ -1025,12 +1024,27 @@ class TestCase { actualStr = (eventWithExtras as LogEvent).toString() Assert.equals(actualStr, expectedStr, "LogEventImpl(toString with extras)") - // Testing eventToLabel, valid case - // It doesn't + // Testing eventToLabel + // Valid case expectedStr = `INFO` actualStr = LogEventImpl.eventTypeToLabel(actualType) Assert.equals(actualStr, expectedStr, "LogEventImpl(eventTypeToLabel)") - + // Invalid case: not defined in LOG_EVENT enum + errMsg = `[LogEventImpl.eventTypeToLabel]: LogEvent.type='-1' property is not defined in the LOG_EVENT enum.` + Assert.throws( + () => LogEventImpl.eventTypeToLabel(-1 as LOG_EVENT), + ScriptError, + errMsg, + "LogEventImpl(eventTypeToLabel)-invalid type(-1)" + ) + // Invalid case: null + errMsg = `[LogEventImpl.eventTypeToLabel]: LogEvent.type='null' property must be a number (LOG_EVENT enum value).` + Assert.throws( + () => LogEventImpl.eventTypeToLabel(null), + ScriptError, + errMsg, + "LogEventImpl(eventTypeToLabel)-invalid type(null)" + ) // Testing validateLogEvent (exception cases): null errMsg = "[LogEventImpl.validateLogEvent]: LogEvent.type='null' property must be a number (LOG_EVENT enum value)." @@ -1139,8 +1153,8 @@ 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, + actualEvent: LogEvent | null, appender: Appender, layout: Layout, + expectedMsg: string, msg: string, expectedType: LOG_EVENT, errMsg: string, extraFields: LogEventExtraFields // Test lazy initialization: We can't because we need and instance first @@ -1148,29 +1162,30 @@ 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.isInstanceOf(appender, ConsoleAppender, "ConsoleAppender(getInstance) is instance of ConsoleAppender") // Multiple calls to getInstance() returns the same object ConsoleAppender.clearInstance() const a1 = ConsoleAppender.getInstance() const a2 = ConsoleAppender.getInstance() - Assert.equals(a1, a2, "ConsoleAppender(getInstance) - singleton reference test") + Assert.equals(a1, a2, "ConsoleAppender(getInstance)-singleton reference test") // Testing static properties have default values (null) - Assert.isNull((appender as AbstractAppender).getLastLogEvent(), "ConsoleAppender(getInstance) has no last log event") + Assert.isNull((appender as AbstractAppender).getLastLogEvent(), "ConsoleAppender(getInstance)-has no last log event") // To test the initial configuration we would need toString, since the getters do lazy initialization expectedStr = `AbstractAppender: {layout=null, logEventFactory="null", lastLogEvent=null} ConsoleAppender: {}` actualStr = (appender as ConsoleAppender).toString() - Assert.equals(actualStr, expectedStr, "ConsoleAppender(getInstance) testing not initialized static properties from AbstractAppender via toString()") + Assert.equals(actualStr, expectedStr, "ConsoleAppender(getInstance)-testing not initialized static properties from AbstractAppender via toString()") // Checking static properties layout = AbstractAppender.getLayout() // lazy initialized - Assert.isNotNull(layout, "ConsoleAppender(getInstance) has a layout") + Assert.isNotNull(layout, "ConsoleAppender(getInstance)-has a layout") // Checking the default formatter via LayoutImpl.toString() expectedStr = `LayoutImpl: {formatter: [Function: "defaultLayoutFormatterFun"]}` actualStr = (layout as LayoutImpl).toString() - Assert.equals(actualStr, expectedStr, "ConsoleAppender(getInstance) has a default layout formatter") + Assert.equals(actualStr, expectedStr, "ConsoleAppender(getInstance)-has a default layout formatter") // Testing setting layout twice, second call should not change the layout AbstractAppender.clearLayout() @@ -1178,7 +1193,7 @@ class TestCase { const layout2 = new LayoutImpl(LayoutImpl.shortFormatterFun) AbstractAppender.setLayout(layout1) AbstractAppender.setLayout(layout2) // Should NOT replace layout1 - Assert.equals(AbstractAppender.getLayout(), layout1, "AbstractAppender(setLayout) - second setLayout call does not override") + Assert.equals(AbstractAppender.getLayout(), layout1, "AbstractAppender(setLayout)-second setLayout call does not override") AbstractAppender.clearLayout() // Reset the appender state AbstractAppender.setLayout(new LayoutImpl(LayoutImpl.defaultFormatterFun)) // Now set the second layout @@ -1189,7 +1204,7 @@ class TestCase { () => AbstractAppender.setLayout("not a layout" as unknown as Layout), ScriptError, errMsg, - "AbstractAppender(setLayout) - invalid layout" + "AbstractAppender(setLayout)-invalid layout" ) AbstractAppender.setLayout(new LayoutImpl()) // Reset the appender state to a valid layout @@ -1199,7 +1214,7 @@ class TestCase { const factory2 = function f2(message:string, type:LOG_EVENT) { return new LogEventImpl("B" + message, type) } AbstractAppender.setLogEventFactory(factory1) AbstractAppender.setLogEventFactory(factory2) // Should NOT replace factory1 - Assert.equals(AbstractAppender.getLogEventFactory(), factory1, "AbstractAppender(setLogEventFactory) - second call does not override") + Assert.equals(AbstractAppender.getLogEventFactory(), factory1, "AbstractAppender(setLogEventFactory)-second call does not override") AbstractAppender.clearLogEventFactory() // Reset the appender state AbstractAppender.setLogEventFactory(AbstractAppender.defaultLogEventFactoryFun) // Now set the second factory @@ -1210,7 +1225,7 @@ class TestCase { () => AbstractAppender.setLogEventFactory("not a function" as unknown as LogEventFactory), ScriptError, errMsg, - "AbstractAppender(setLogEventFactory) - invalid factory" + "AbstractAppender(setLogEventFactory)-invalid factory" ) AbstractAppender.setLogEventFactory(AbstractAppender.defaultLogEventFactoryFun) // Now set the second factory @@ -1219,7 +1234,7 @@ class TestCase { expectedType = LOG_EVENT.INFO expectedEvent = new LogEventImpl(expectedMsg, expectedType) Assert.doesNotThrow(() => appender!.log(expectedEvent), - "ConsoleAppender(log(LogEvent)) - valid case with lazy initialization" + "ConsoleAppender(log(LogEvent))-valid case with lazy initialization" ) actualEvent = appender!.getLastLogEvent() Assert.equals(actualEvent?.type, expectedEvent?.type, @@ -1233,7 +1248,7 @@ class TestCase { expectedType = LOG_EVENT.ERROR expectedEvent = new LogEventImpl(expectedMsg, expectedType, extraFields) Assert.doesNotThrow(() => appender!.log(expectedEvent), - "ConsoleAppender(log(LogEvent)) - valid case with lazy initialization and extra fields" + "ConsoleAppender(log(LogEvent))-valid case with lazy initialization and extra fields" ) actualEvent = appender!.getLastLogEvent() Assert.equals(actualEvent?.type, expectedEvent?.type, @@ -1252,7 +1267,7 @@ class TestCase { expectedType = LOG_EVENT.TRACE expectedEvent = new LogEventImpl(expectedMsg, expectedType) Assert.doesNotThrow(() => appender!.log(expectedMsg, expectedType), - "ConsoleAppender(log(string,LOG_EVENT)) - valid case with lazy initialization" + "ConsoleAppender(log(string,LOG_EVENT))-valid case with lazy initialization" ) actualEvent = appender!.getLastLogEvent() Assert.equals(actualEvent?.type, expectedEvent?.type, @@ -1266,7 +1281,7 @@ class TestCase { expectedType = LOG_EVENT.TRACE expectedEvent = new LogEventImpl(expectedMsg, expectedType, extraFields) Assert.doesNotThrow(() => appender!.log(expectedMsg, expectedType, extraFields), - "ConsoleAppender(log(string,LOG_EVENT)) - valid case with lazy initialization and extra fields" + "ConsoleAppender(log(string,LOG_EVENT))-valid case with lazy initialization and extra fields" ) actualEvent = appender!.getLastLogEvent() Assert.equals(actualEvent?.type, expectedEvent?.type, @@ -1286,13 +1301,13 @@ class TestCase { () => appender!.log(null as unknown as LogEvent), ScriptError, errMsg, - "ConsoleAppender(ScriptError)-log(LogEvent) - null" + "ConsoleAppender(log(LogEvent))-null" ) Assert.throws( () => appender!.log(undefined as unknown as LogEvent), ScriptError, errMsg, - "ConsoleAppender(ScriptError)-log(LogEvent) - undefined" + "ConsoleAppender(log(LogEvent))-undefined" ) // Testing log(string, LOG_EVENT): null/undefined case @@ -1326,7 +1341,7 @@ class TestCase { () => appender.log("Info event message", -1 as LOG_EVENT), ScriptError, expectedStr, - "ConsoleAppender(ScriptError)-log - non valid LOG_EVENT" + "ConsoleAppender(log)-non valid LOG_EVENT" ) // Testing throwing a ScriptError: Testing not instantiated singleton and calling toString method @@ -1336,14 +1351,14 @@ class TestCase { () => appender.toString(), ScriptError, expectedStr, - "ConsoleAppender(ScriptError)-toString-with singleton undefined" + "ConsoleAppender(toString)-with singleton undefined" ) // Testing NOT throwing a ScriptError: Testing not instantiated singleton and getting log event factory ConsoleAppender.clearInstance() Assert.doesNotThrow( () => ConsoleAppender.getLogEventFactory(), - "ConsoleAppender(ScriptError)-getLogEventFactory - no custom factory" + "ConsoleAppender(getLogEventFactory)-no custom factory" ) // Testing custom Log event factory with a valid factory @@ -1362,15 +1377,15 @@ class TestCase { appender.log(expectedMsg, expectedType) actualEvent = appender.getLastLogEvent() Assert.equals(actualEvent?.type, expectedEvent?.type, - "ConsoleAppender(getLastMsg not empty)-log(string,LOG_EVENT) with custom factory.type") + "ConsoleAppender(getLastLogEvent)-not empty-log(string,LOG_EVENT) with custom factory.type") Assert.equals(actualEvent?.message, expectedEvent?.message, - "ConsoleAppender(getLastMsg not empty)-log(string,LOG_EVENT) with custom factory.message") + "ConsoleAppender(getLastLogEvent)-not empty-log(string,LOG_EVENT) with custom factory.message") // Clear instance also clear the last log event appender.log("Some message", LOG_EVENT.INFO) ConsoleAppender.clearInstance() appender = ConsoleAppender.getInstance() - Assert.equals(appender.getLastLogEvent(), null, "ConsoleAppender(clearInstance) - lastLogEvent is null after clear") + Assert.equals(appender.getLastLogEvent(), null, "ConsoleAppender(clearInstance)-lastLogEvent is null after clear") AbstractAppender.clearLogEventFactory() @@ -1385,13 +1400,16 @@ class TestCase { let expectedStr: string, actualStr: string, msg: string, expectedEvent: LogEvent, actualEvent: LogEvent | null, expectedMsg: string, expectedType: LOG_EVENT, errMsg: string, appender: Appender, msgCellRng: ExcelScript.Range, extraFields: LogEventExtraFields, - activeSheet: ExcelScript.Worksheet, eventFonts: Record, - layout: Layout + activeSheet: ExcelScript.Worksheet, eventFonts: Record activeSheet = workbook.getActiveWorksheet() msgCellRng = activeSheet.getRange(msgCell) const address = msgCellRng.getAddress() appender = ExcelAppender.getInstance(msgCellRng) + Assert.isNotNull(appender, "ExcelAppender(getInstance) is not null") + Assert.isInstanceOf(appender, ExcelAppender, "ExcelAppender(getInstance) is instance of ExcelAppender") + + // Multiple calls to getInstance() returns the same object 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) @@ -1415,7 +1433,7 @@ class TestCase { expectedMsg = "Info event with extra fields testing ExcelAppender" expectedType = LOG_EVENT.INFO Assert.doesNotThrow(() => appender.log(expectedMsg, expectedType, extraFields), - "ExcelAppender(log(string,LOG_EVENT)) - valid case with extra fields" + "ExcelAppender(log(string,LOG_EVENT))-valid case with extra fields" ) TestCase.runTestAsync(() => { actualEvent = appender.getLastLogEvent() @@ -1438,7 +1456,7 @@ class TestCase { actualEvent = appender.getLastLogEvent() expectedEvent = new LogEventImpl(expectedMsg, expectedType, {}, actualEvent.timestamp) Assert.doesNotThrow(() => appender.log(expectedEvent), - "ExcelAppender(log(LogEvent)) - valid case" + "ExcelAppender(log(LogEvent))-valid case" ) actualEvent = appender.getLastLogEvent() actualStr = msgCellRng.getValue().toString() @@ -1459,7 +1477,7 @@ class TestCase { expectedType = LOG_EVENT.ERROR expectedEvent = new LogEventImpl(expectedMsg, expectedType, extraFields) Assert.doesNotThrow(() => appender.log(expectedEvent), - "ExcelAppender(log(LogEvent)) - valid case with extra fields" + "ExcelAppender(log(LogEvent))-valid case with extra fields" ) TestCase.runTestAsync(() => { actualEvent = appender.getLastLogEvent() @@ -1507,7 +1525,7 @@ class TestCase { () => appender.log("Info message", LOG_EVENT.INFO), ScriptError, errMsg, - "ExcelAppender(ScriptError)-log-singleton not defined" + "ExcelAppender(log)-singleton not defined" ) // Script Errors: Testing non valid input: getInstancce(null) errMsg = "[ExcelAppender.getInstance]: A valid ExcelScript.Range for input argument msgCellRng is required." @@ -1515,7 +1533,7 @@ class TestCase { () => ExcelAppender.getInstance(null), ScriptError, errMsg, - "ExcelAppender(ScriptError)-getInstance(Non valid msgCellRng-null)" + "ExcelAppender(getInstance)-Non valid msgCellRng-null" ) }) @@ -1524,7 +1542,7 @@ class TestCase { () => ExcelAppender.getInstance(undefined), ScriptError, errMsg, - "ExcelAppender(ScriptError))-getInstance - Non valid msgCellRng-undefined" + "ExcelAppender(getInstance)-Non valid msgCellRng-undefined" ) // Script Errors: Testing non valid input: log with non valid LOG_EVENT @@ -1534,7 +1552,7 @@ class TestCase { () => appender.log("Info event message", -1 as LOG_EVENT), ScriptError, errMsg, - "ExcelAppender(ScriptError)-Log non valid LOG_EVENT" + "ExcelAppender(log)-non valid LOG_EVENT" ) ExcelAppender.clearInstance() @@ -1547,7 +1565,7 @@ class TestCase { () => ExcelAppender.getInstance(mockArrRng as unknown as ExcelScript.Range), ScriptError, errMsg, - "ExcelAppender(ScriptError)-getInstance not a single cell" + "ExcelAppender(getInstance)-not a single cell" ) // Script Errors: Testing non-valid hexadecimal colors @@ -1558,7 +1576,7 @@ class TestCase { () => ExcelAppender.getInstance(msgCellRng, eventFonts), ScriptError, errMsg, - "ExcelAppender(ScriptError)-getInstance-Non valid font color for warning" + "ExcelAppender(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." eventFonts = { ...DEFAULT_FONTS, [LOG_EVENT.INFO]: "xxxxxx" } @@ -1566,7 +1584,7 @@ class TestCase { () => ExcelAppender.getInstance(msgCellRng, eventFonts), ScriptError, errMsg, - "ExcelAppender(ScriptError) - getInstance - Non valid font color for info" + "ExcelAppender(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." eventFonts = { ...DEFAULT_FONTS, [LOG_EVENT.TRACE]: "******" } @@ -1574,7 +1592,7 @@ class TestCase { () => ExcelAppender.getInstance(msgCellRng, eventFonts), ScriptError, errMsg, - "ExcelAppender(ScriptError)-getInstance-Non valid font color for trace" + "ExcelAppender(getInstance)-Non valid font color for trace" ) // Testing toString @@ -1598,7 +1616,7 @@ class TestCase { // Checking Initial situation logger = LoggerImpl.getInstance() - Assert.isNotNull(logger, "LoggerImpl(getInstance) is not null") + Assert.isNotNull(logger, "LoggerImpl(getInstance)-is not null") 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") @@ -1622,14 +1640,14 @@ class TestCase { // 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") + 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() @@ -1680,14 +1698,14 @@ class TestCase { logger.info(expectedMsg) // lazy initialization of the appender expectedNum = 1 actualNum = logger.getAppenders().length ?? 0 - Assert.equals(actualNum, expectedNum, "Logger(Lazy init)-appender size is one") - Assert.isNotNull(logger.getAppenders()[0], "Logger(Lazy init)-appender is not null") - 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") + Assert.equals(actualNum, expectedNum, "loggerImplLazyInit-appender size is one") + Assert.isNotNull(logger.getAppenders()[0], "loggerImplLazyInit-appender is not null") + Assert.equals(logger.getLevel(), LoggerImpl.LEVEL.INFO, "loggerImplLazyInit-level is INFO") + Assert.equals(logger.getAction(), LoggerImpl.ACTION.EXIT, "loggerImplLazyInit-action is EXIT(default") actualEvent = logger.getAppenders()[0].getLastLogEvent() // Safe to use getLastLogEvent here since it was tested in ConsoleAppender - Assert.isNotNull(actualEvent, "Logger(Lazy init)-getLastLogEvent is not null") - Assert.equals(actualEvent.type, expectedType, "Logger(Lazy init)-getLastLogEvent.type is INFO") - Assert.equals(actualEvent.message, expectedMsg, "Logger(Lazy init)-getLastLogEvent.message info message is correct") + Assert.isNotNull(actualEvent, "loggerImplLazyInit-getLastLogEvent is not null") + Assert.equals(actualEvent.type, expectedType, "loggerImplLazyInit-getLastLogEvent.type is INFO") + Assert.equals(actualEvent.message, expectedMsg, "loggerImplLazyInit-getLastLogEvent.message info message is correct") // Lazy initialization of the singleton with default parameters (WARN,EXIT) expectedMsg = "Error event testing lazyInit for LoggerImpl" @@ -1695,10 +1713,10 @@ class TestCase { expectedEvent = new LogEventImpl(expectedMsg, LOG_EVENT.ERROR) LoggerImpl.clearInstance() TestCase.runTestAsync(() => { - Assert.isNotNull(LoggerImpl.getInstance(), "Lazy init(logger != null)") - logger = LoggerImpl.getInstance() // Get alreazy lazy initialized singleton - Assert.equals(logger.getLevel(), LoggerImpl.LEVEL.WARN, "Logger(Lazy init)-level is WARN") - Assert.equals(logger.getAction(), LoggerImpl.ACTION.EXIT, "Logger(Lazy init)-action is EXIT") + Assert.isNotNull(LoggerImpl.getInstance(), "loggerImplLazyInit(logger != null)") + logger = LoggerImpl.getInstance() // Get already lazy initialized singleton + Assert.equals(logger.getLevel(), LoggerImpl.LEVEL.WARN, "loggerImplLazyInit-level is WARN") + Assert.equals(logger.getAction(), LoggerImpl.ACTION.EXIT, "loggerImplLazyInit-action is EXIT") }) // To check the ScriptError message, since it may include the timestamp, we would need to use a short layout @@ -1710,9 +1728,9 @@ class TestCase { "Logger(Lazy init)-error event with appender lazy initialized and expected to throw ScriptError" ) actualEvent = logger.getAppenders()[0].getLastLogEvent() // Safe to use getLastLogEvent here since it was tested in ConsoleAppender - Assert.isNotNull(actualEvent, "Logger(Lazy init)-getLastLogEvent is not null") - Assert.equals(actualEvent.type, expectedType, "Logger(Lazy init)-getLastLogEvent.type is ERROR") - Assert.equals(actualEvent.message, expectedMsg, "Logger(Lazy init)-getLastLogEvent.message error message is correct") + Assert.isNotNull(actualEvent, "loggerImplLazyInit(getLastLogEvent()[0])-is not null") + Assert.equals(actualEvent.type, expectedType, "loggerImplLazyInit(getLastLogEvent()[0]).type is ERROR") + Assert.equals(actualEvent.message, expectedMsg, "loggerImplLazyInit(getLastLogEvent()[0]).message error message is correct") TestCase.setDefaultLayout() // Testing ScriptError when no singleton is defined @@ -1816,23 +1834,23 @@ class TestCase { logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO, LoggerImpl.ACTION.CONTINUE) logger.addAppender(ConsoleAppender.getInstance()) // Testing counters on initial state - Assert.equals(logger.getErrCnt(), 0, "loggerCounters(getErrCnt=0)") - Assert.equals(logger.getWarnCnt(), 0, "loggerCounters(getWarnCnt=0)") - Assert.equals(logger.hasErrors(), false, "loggerCounters(hasErrors=false)") - Assert.equals(logger.hasWarnings(), false, "loggerCounters(hasWarnings=false)") - Assert.equals(logger.getCriticalEvents(), [], "loggerCounters(getMessages=[])") + Assert.equals(logger.getErrCnt(), 0, "loggerImplCounters(getErrCnt=0)") + Assert.equals(logger.getWarnCnt(), 0, "loggerImplCounters(getWarnCnt=0)") + Assert.equals(logger.hasErrors(), false, "loggerImplCounters(hasErrors=false)") + Assert.equals(logger.hasWarnings(), false, "loggerImplCounters(hasWarnings=false)") + Assert.equals(logger.getCriticalEvents(), [], "loggerImplCounters(getMessages=[])") // Sending events affecting the counter errMsg = "Error event for testing counters for LoggerImpl" logger.error(errMsg) expectedNum = 1 actualNum = logger.getErrCnt() - Assert.equals(actualNum, expectedNum, "loggerCounters(getErrCnt=1)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(getErrCnt=1)") actualEvent = logger.getCriticalEvents()[0] ?? null // Get the first event expectedEvent = new LogEventImpl(errMsg, LOG_EVENT.ERROR) - Assert.isNotNull(actualEvent, "LoggerCounters(getMessage()[0]) not null") - Assert.equals((actualEvent as LogEvent).type, expectedEvent.type, "LoggerCounters(getMessage()[0]).type") - Assert.equals((actualEvent as LogEvent).message, expectedEvent.message, "LoggerCounters(getMessage()[0]).message") + Assert.isNotNull(actualEvent, "loggerImplCounters(getMessage()[0]) not null") + Assert.equals((actualEvent as LogEvent).type, expectedEvent.type, "loggerImplCounters(getMessage()[0]).type") + Assert.equals((actualEvent as LogEvent).message, expectedEvent.message, "loggerImplCounters(getMessage()[0]).message") // Testing counter for warnings @@ -1840,36 +1858,36 @@ class TestCase { logger.warn(warnMsg) expectedNum = 1 actualNum = logger.getWarnCnt() - Assert.equals(actualNum, expectedNum, "loggerCounters(getWarnCnt=1)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(getWarnCnt=1)") // Testing messages let expectedArr = TestCase.simplifyLogEvents([new LogEventImpl(errMsg, LOG_EVENT.ERROR), new LogEventImpl(warnMsg, LOG_EVENT.WARN)]) let actualArr = TestCase.simplifyLogEvents(logger.getCriticalEvents()) - Assert.equals(actualArr, expectedArr, "loggerCounters(getMessages)") - Assert.equals(logger.hasMessages(), true, "loggerCounters(hasMessages)") + Assert.equals(actualArr, expectedArr, "loggerImplCounters(getMessages)") + Assert.equals(logger.hasMessages(), true, "loggerImplCounters(hasMessages)") // Testing other events, don't affect the counters let msg = "Info event doesn't count testing counters for LoggerImpl" logger.info(msg) actualNum = logger.getErrCnt() - Assert.equals(actualNum, expectedNum, "LoggerCounter(getErrCnt=1)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(getErrCnt=1)") actualNum = logger.getWarnCnt() - Assert.equals(actualNum, expectedNum, "LoggerCounter(getWarnCnt=1)") - Assert.equals(actualArr, expectedArr, "loggerCounters(getMessages-2nd time)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(getWarnCnt=1)") + Assert.equals(actualArr, expectedArr, "loggerImplCounters(getMessages-2nd time)") // Clearing counts logger.reset() expectedNum = 0 actualNum = logger.getErrCnt() - Assert.equals(actualNum, expectedNum, "LoggerCounter(errors cleared)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(errors cleared)") actualNum = logger.getWarnCnt() - Assert.equals(actualNum, expectedNum, "LoggerCounter(warnings cleared)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(warnings cleared)") expectedArr = [] actualArr = logger.getCriticalEvents() - Assert.equals(actualArr, expectedArr, "LoggerCounter(messages cleared)") + Assert.equals(actualArr, expectedArr, "loggerImplCounters(messages cleared)") // Checking appenders were not removed expectedNum = 1 actualNum = logger.getAppenders().length - Assert.equals(actualNum, expectedNum, "LoggerCounter(appenders not removed)") + Assert.equals(actualNum, expectedNum, "loggerImplCounters(appenders not removed)") TestCase.clear() } @@ -1879,6 +1897,7 @@ class TestCase { TestCase.setShortLayout() // Defining the variables to be used in the tests let expected: string, actual: string, layout: Layout, logger: Logger, extraFields: LogEventExtraFields + const TK="" // Placeholder for timestamp in the expected string //layout = new LayoutImpl(LayoutImpl.shortFormatterFun) // Short layout logger = LoggerImpl.getInstance(LoggerImpl.LEVEL.INFO, // Level of verbose @@ -1901,7 +1920,7 @@ class TestCase { `{msgCellRng(address)="C2", eventfonts={ERROR="9c0006",WARN="ed7d31",INFO="548235",TRACE="7f7f7f"}}]}` actual = logger.toString() TestCase.runTestAsync(() => { - Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger)") + Assert.equals(TestCase.removeTimestamp(actual,TK), TestCase.removeTimestamp(expected,TK), "loggerToString-valid case") }) // Testing toString with extra fields @@ -1915,19 +1934,15 @@ class TestCase { `lastLogEvent=LogEventImpl: {timestamp="2025-06-19 22:31:17,324", type="INFO", message="Info event testing logger.toString with extra fields", ` + `extraFields={"userId":123,"sessionId":"abc"}}} ConsoleAppender: {}]}` actual = logger.toString() - Assert.equals(normalizeTimestamps(actual), normalizeTimestamps(expected), "loggerToString(Logger with extra fields)") + Assert.equals(TestCase.removeTimestamp((actual), TK), TestCase.removeTimestamp(expected, TK), "loggerToString-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") + Assert.equals(actual, expected, "loggerToString-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 */ @@ -1982,7 +1997,7 @@ class TestCase { () => LoggerImpl.getInstance(-1, LoggerImpl.ACTION.CONTINUE), ScriptError, expectedMsg, - "loggerScriptError-Non valid LOG_LEVEL enum value" + "loggerScriptError(getInstance)-Non valid LOG_LEVEL enum value" ) // Testing when is invoked validateInstance method @@ -1994,56 +2009,56 @@ class TestCase { () => logger.getErrCnt(), ScriptError, expectedMsg, - "loggerScriptError(getErrCnt())" + "loggerScriptError(getErrCnt)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.getWarnCnt]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.getWarnCnt(), ScriptError, expectedMsg, - "loggerScriptError-(getWarnCnt())" + "loggerScriptError(getWarnCnt)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.getCriticalEvents]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.getCriticalEvents(), ScriptError, expectedMsg, - "loggerScriptError-(getMessages())" + "loggerScriptError(getMessages)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.getLevel]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.getLevel(), ScriptError, expectedMsg, - "loggerScriptError(getLevel())" + "loggerScriptError(getLevel)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.getAction]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.getAction(), ScriptError, expectedMsg, - "loggerScriptError(getAction())" + "loggerScriptError(getAction)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.hasErrors]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.hasErrors(), ScriptError, expectedMsg, - "loggerScriptError(hasErrors())" + "loggerScriptError(hasErrors)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.hasWarnings]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.hasWarnings(), ScriptError, expectedMsg, - "loggerScriptError(hasWarnings())" + "loggerScriptError(hasWarnings)-singleton instance undefined" ) expectedMsg = "[LoggerImpl.clear]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.reset(), ScriptError, expectedMsg, - "loggerScriptError(clear())" + "loggerScriptError(reset)-singleton instance undefined" ) // Testing adding/setting/removing appender with undefined/null singleton expectedMsg = "[LoggerImpl.getAppenders]: A singleton instance can't be undefined or null. Please invoke getInstance first." @@ -2052,21 +2067,21 @@ class TestCase { () => logger.getAppenders(), ScriptError, expectedMsg, - "loggerScriptError(getAppenders())-undefined singleton" + "loggerScriptError(getAppenders)-undefined singleton" ) expectedMsg = "[LoggerImpl.addAppender]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.addAppender(consoleAppender), ScriptError, expectedMsg, - "loggerScriptError(addAppender())-undefined singleton" + "loggerScriptError(addAppender)-undefined singleton" ) expectedMsg = "[LoggerImpl.removeAppender]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( () => logger.removeAppender(consoleAppender), ScriptError, expectedMsg, - "loggerScriptError(removeAppender())-undefined singleton" + "loggerScriptError(removeAppender)-undefined singleton" ) expectedMsg = "[LoggerImpl.setAppenders]: A singleton instance can't be undefined or null. Please invoke getInstance first." Assert.throws( @@ -2094,13 +2109,13 @@ class TestCase { () => logger.addAppender(null as unknown as Appender), ScriptError, expectedMsg, - "loggerScriptError(addAppenders()-null-valid-singleton)" + "loggerScriptError(addAppenders(null)-valid-singleton" ) Assert.throws( () => logger.addAppender(undefined as unknown as Appender), ScriptError, expectedMsg, - "loggerScriptError(addAppenders()-undefined-valid-singleton)" + "loggerScriptError(addAppenders(undefined))-valid-singleton" ) // Adding appenders via setAppenders expectedMsg = "[LoggerImpl.setAppenders]: Invalid input: the input argument 'appenders' must be a non-null array." @@ -2108,13 +2123,13 @@ class TestCase { () => logger.setAppenders(undefined as unknown as Appender[]), ScriptError, expectedMsg, - `Internal Error(setAppenders)-undefined-valid singleton` + `Internal Error(setAppenders(undefined)-valid singleton` ) Assert.throws( () => logger.setAppenders(null as unknown as Appender[]), ScriptError, expectedMsg, - "loggerScriptError(setAppenders)-null-valid-singleton" + "loggerScriptError(setAppenders(null)-valid-singleton" ) expectedMsg = "[LoggerImpl.setAppenders]: Input argument appenders array contains null or undefined entry." @@ -2122,20 +2137,20 @@ class TestCase { () => logger.setAppenders([consoleAppender, null as unknown as Appender]), ScriptError, expectedMsg, - "loggerScriptError-[consoleAppender,null]-valid singleton" + "loggerScriptError(setAppenders[consoleAppender,null])-valid singleton" ) Assert.throws( () => logger.setAppenders([consoleAppender, undefined as unknown as Appender]), ScriptError, expectedMsg, - "loggerScriptError-[consoleAppender,undefined]-valid singleton" + "loggerScriptError(setAppenders([consoleAppender,undefined])-valid singleton" ) expectedMsg = "[LoggerImpl.setAppenders]: Only one appender of type ConsoleAppender is allowed." Assert.throws( () => logger.setAppenders([consoleAppender, consoleAppender]), ScriptError, expectedMsg, - "loggerScriptError-[consoleAppender,consoleAppender]-valid singleton" + "loggerScriptError(setAppenders[consoleAppender,consoleAppender])-valid singleton" ) // Testing adding duplicate appender @@ -2147,7 +2162,7 @@ class TestCase { () => logger.addAppender(ConsoleAppender.getInstance()), ScriptError, expectedMsg, - "loggerScriptError-addaAppender duplicated" + "loggerScriptError-addAppender duplicated" ) LoggerImpl.clearInstance() LoggerImpl.getInstance() @@ -2158,7 +2173,7 @@ class TestCase { () => logger.setAppenders([excelAppender, excelAppender]), ScriptError, expectedMsg, - "loggerScriptError-setAppender - duplicated" + "loggerScriptError-setAppenders[excelAppender,excelAppender]-duplicated" ) TestCase.clear() diff --git a/test/unit-test-framework.ts b/test/unit-test-framework.ts index a4b151e..b0f9c1d 100644 --- a/test/unit-test-framework.ts +++ b/test/unit-test-framework.ts @@ -12,21 +12,18 @@ * * @remarks See the documentation for the Assert and TestRunner classes for assertion details and test execution control. * author David Leal - * date 2025-06-03 (creation date) - * version 2.1.0 + * date 2025-06-26 (creation date) + * version 1.0.0 */ // #region AssertionError /** * AssertionError is a custom error type used to indicate assertion failures in tests or validation utilities. - * * This error is intended to be thrown by assertion methods (such as those in a custom Assert class) when a condition * that should always be true is found to be false. Using a specific AssertionError type allows for more precise * error handling and clearer reporting in test environments, as assertion failures can be easily distinguished from * other kinds of runtime errors. - * - * Typical usage: - * + * @example * ```typescript * if (actual !== expected) { * throw new AssertionError(`Expected ${expected}, but got ${actual}`) @@ -51,10 +48,12 @@ class AssertionError extends Error { // #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 'AssertionError' is thrown. + * This class provides a set of static methods to perform common assertions + * such as checking equality, type, and exceptions, etc. */ class Assert { + + // #region throws /** * Asserts that the provided function throws an error. * Optionally checks the error type and message. @@ -64,13 +63,13 @@ class Assert { * @param expectedMessage - (Optional) Exact expected error message. * @param message - (Optional) Additional prefix for the error message if the assertion fails. * @throws AssertionError - If no error is thrown, or if the thrown error does not match the expected type or message. - * * @example * ```ts * Assert.throws(() => { * throw new TypeError("Invalid") * }, TypeError, "Invalid", "Should throw TypeError") * ``` + * @see Assert.doesNotThrow for the opposite assertion. */ public static throws( fn: () => void, @@ -78,59 +77,60 @@ class Assert { expectedMessage?: string, message: string = "" ): asserts fn is () => never { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" try { fn() } catch (e: unknown) { if (!(e instanceof Error)) { - throw new AssertionError(`${MSG}Thrown value is not an Error instance: (${Assert.safeStringify(e)})`) + throw new AssertionError(`${PREFIX}Thrown value is not an Error instance: (${Assert.safeStringify(e)})`) } if (expectedErrorType && !(e instanceof expectedErrorType)) { - throw new AssertionError(`${MSG}Expected error type ${expectedErrorType.name}, but got ${e.constructor.name}.`) + throw new AssertionError(`${PREFIX}Expected error type ${expectedErrorType.name}, but got ${e.constructor.name}.`) } if (expectedMessage && e.message !== expectedMessage) { - throw new AssertionError(`${MSG}Expected message "${expectedMessage}", but got "${e.message}".`) + throw new AssertionError(`${PREFIX}Expected message "${expectedMessage}", but got "${e.message}".`) } return // ✅ Test passed } - throw new AssertionError(`${MSG}Expected function to throw, but it did not.`) + throw new AssertionError(`${PREFIX}Expected function to throw, but it did not.`) } - - /** - * 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 (deep equality via JSON.stringify). - * - * If the values differ, a detailed error is thrown. - * For arrays, mismatches include index, value, and type. - * For arrays of objects, a shallow comparison using JSON.stringify is performed. - * - * @param actual - The actual value. - * @param expected - The expected value. - * @param message - (Optional) Prefix message included in the thrown error on failure. - * @throws AssertionError - If 'actual' and 'expected' are not equal. - * - * @example - * ```ts - * Assert.equals(2 + 2, 4, "Simple math") - * Assert.equals(["a", "b"], ["a", "b"], "Array match") - * Assert.equals([{x:1}], [{x:1}], "Object array match") // Passes - * Assert.equals([{x:1}], [{x:2}], "Object array mismatch") // Fails - * ``` - */ + // #endregion throws + + // #region equals + /** + * 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 (deep equality via JSON.stringify). + * If the values differ, a detailed error is thrown. + * For arrays, mismatches include index, value, and type. + * For arrays of objects, a shallow comparison using JSON.stringify is performed. + * If a value cannot be stringified (e.g., due to circular references), it is treated as "[unprintable value]" in error messages and object equality checks. + * @param actual - The actual value. + * @param expected - The expected value. + * @param message - (Optional) Prefix message included in the thrown error on failure. + * @throws AssertionError - If 'actual' and 'expected' are not equal. + * @example + * ```ts + * Assert.equals(2 + 2, 4, "Simple math") + * Assert.equals(["a", "b"], ["a", "b"], "Array match") + * Assert.equals([1, "2"], [1, 2], "Array doesn't match") // Fails + * Assert.equals([{x:1}], [{x:1}], "Object array match") // Passes + * Assert.equals([{x:1}], [{x:2}], "Object array mismatch") // Fails + * ``` + */ public static equals(actual: T, expected: T, message: string = ""): asserts actual is T { - const MSG = message ? `${message}: ` : ""; + const PREFIX = 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(`${PREFIX}Assertion failed: actual (${Assert.safeStringify(actual)}) !== expected (${Assert.safeStringify(expected)})`); } if (Array.isArray(actual) && Array.isArray(expected)) { - this.arraysEqual(actual, expected, MSG); + this.arraysEqual(actual, expected, message); return; } @@ -141,23 +141,36 @@ class Assert { actual !== null && expected !== null ) { - if (JSON.stringify(actual) !== JSON.stringify(expected)) { + let actualStr:string, expectedStr:string + try { + actualStr = JSON.stringify(actual) + } catch { + actualStr = "[unprintable value]" + } + try { + expectedStr = JSON.stringify(expected) + } catch { + expectedStr = "[unprintable value]" + } + if (actualStr !== expectedStr) { throw new AssertionError( - `${MSG}Assertion failed: actual (${Assert.safeStringify(actual)}) !== expected (${Assert.safeStringify(expected)})` - ); + `${PREFIX}Assertion failed: actual (${Assert.safeStringify(actual)}) !== expected (${Assert.safeStringify(expected)})` + ) } - return; + 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})` + `${PREFIX}Assertion failed: actual (${Assert.safeStringify(actual)} : ${actualType}) !== expected (${Assert.safeStringify(expected)} : ${expectedType})` ); } } + // #endregion equals + // #region isNull /** * Asserts that the given value is strictly `null`. * Provides a robust stringification of the value for error messages, @@ -165,16 +178,28 @@ class Assert { * @param value - The value to test. * @param message - Optional message to prefix in case of failure. * @throws AssertionError if the value is not exactly `null`. + * * @example + * ```ts + * Assert.isNull(null, "Value should be null") + * Assert.isNull(undefined, "Value should not be undefined") // Fails + * Assert.isNull(0, "Zero is not null") // Fails + * Assert.isNull(null) + * Assert.isNull(undefined) // Fails + * Assert.isNull(0) // Fails + * ``` + * @see Assert.isDefined for an alias that checks for defined values (not `null` or `undefined`). */ public static isNull(value: unknown, message: string = ""): asserts value is null { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" if (value !== null) { throw new AssertionError( - `${MSG}Expected value to be null, but got (${Assert.safeStringify(value)})` + `${PREFIX}Expected value to be null, but got (${Assert.safeStringify(value)})` ) } } + // #endregion isNull + // #region isNotNull /** * Asserts that the given value is not `null`. * Provides a robust stringification of the value for error messages, @@ -183,118 +208,399 @@ class Assert { * @param value - The value to test. * @param message - Optional message to prefix in case of failure. * @throws AssertionError if the value is `null`. + * @example + * ```ts + * Assert.isNotNull(42, "Value should not be null") + * Assert.isNotNull(null, "Value should be null") // Fails + * ``` + * @see Assert.isNull for the opposite assertion. + * @see Assert.isDefined for an alias that checks for defined values (not `null` or `undefined`). */ public static isNotNull(value: T, message: string = ""): asserts value is NonNullable { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" if (value === null) { throw new AssertionError( - `${MSG}Expected value not to be null, but got (${Assert.safeStringify(value)})` + `${PREFIX}Expected value not to be null, but got (${Assert.safeStringify(value)})` ) } } + // #endregion isNotNull + // #region fail /** * Fails the test by throwing an error with the provided message. - * + * This method is used to explicitly indicate that a test case has failed, + * regardless of any conditions or assertions. * @param message - (Optional) The failure message to display. * If not provided, a default "Assertion failed" message is used. + * @throws AssertionError - Always throws an AssertionError with the provided message. + * @example + * ```ts + * Assert.fail("This test should not pass") + * ``` */ static fail(message?: string) { throw new AssertionError(message || "Assertion failed") } + // #endregion fail + // #region 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 + * ```ts + * isType("hello", "string"); // passes + * isType(42, "number"); // passes + * isType({}, "string"); // throws + * isType([], "object", "Expected an object"); // passes + * isType(null, "object", "Expected an object"); // throws + * ``` + * @remarks This method checks the type using `typeof` and throws an AssertionError if the type does not match. + * It is useful for validating input types in functions or methods. + * The `type` parameter must be one of the following strings: "string", "number", "boolean", "object", "function", "undefined", "symbol", or "bigint". + * If the value is `null`, it will be considered an object, which is consistent with JavaScript's behavior. + * @see Assert.isNotType for the opposite assertion. */ - public static assertType( + public static isType( value: unknown, type: "string" | "number" | "boolean" | "object" | "function" | "undefined" | "symbol" | "bigint", - message?: string + message: string = "" ): void { + const PREFIX = message ? `${message}: ` : "" if (typeof value !== type) { throw new AssertionError( - message || - `Expected type '${type}', but got '${typeof value}': (${JSON.stringify(value)})` + `${PREFIX}Expected type '${type}', but got '${typeof value}': (${JSON.stringify(value)})` ); } } + // #endregion isType + // #region isNotType +/** + * Asserts that a value is NOT of the specified primitive type. + * @param value - The value to check. + * @param type - The unwanted type as a string ("string", "number", etc.) + * @param message - Optional error message. + * @throws AssertionError - If the type matches. + * @example + * ```ts + * isNotType("hello", "number"); // passes + * isNotType(42, "string"); // passes + * isNotType({}, "object"); // throws + * isNotType(null, "object", "Should not be object"); // throws (null is object in JS) + * ``` + * @remarks This method checks the type using `typeof` and throws an AssertionError if the type matches. + * The `type` parameter must be one of the following strings: "string", "number", "boolean", "object", "function", "undefined", "symbol", or "bigint". + * @see Assert.isType for the positive assertion. + */ +public static isNotType( + value: unknown, + type: "string" | "number" | "boolean" | "object" | "function" | "undefined" | "symbol" | "bigint", + message: string = "" +): void { + const PREFIX = message ? `${message}: ` : "" + if (typeof value === type) { + throw new AssertionError( + `${PREFIX}Did not expect type '${type}', but got '${typeof value}': (${JSON.stringify(value)})` + ) + } +} +// #endregion isNotType + // #region doesNotThrow /** * 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. - * * @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 * ```ts * Assert.doesNotThrow(() => { * const x = 1 + 1 * }, "Should not throw any error") * ``` + * @see Assert.throws for the opposite assertion. */ public static doesNotThrow(fn: () => void, message: string = ""): void { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" try { fn() } catch (e) { - throw new AssertionError(`${MSG}Expected function not to throw, but it threw: ${Assert.safeStringify(e)}`) + throw new AssertionError(`${PREFIX}Expected function not to throw, but it threw: ${Assert.safeStringify(e)}`) } } + // #endregion doesNotThrow + // #region isTrue /** * Asserts that the provided value is truthy. * Throws AssertionError if the value is not truthy. - * * @param value - The value to test for truthiness. * @param message - (Optional) Message to prefix in case of failure. * @throws AssertionError - If the value is not truthy. - * * @example * ```ts * Assert.isTrue(1 < 2, "Math sanity") * Assert.isTrue("non-empty string", "String should be truthy") * ``` + * @see Assert.isFalse for the opposite assertion. */ public static isTrue(value: unknown, message: string = ""): asserts value { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" if (!value) { - throw new AssertionError(`${MSG}Expected value to be truthy, but got (${Assert.safeStringify(value)})`) + throw new AssertionError(`${PREFIX}Expected value to be truthy, but got (${Assert.safeStringify(value)})`) } } + // #endregion isTrue + // #region isFalse /** * Asserts that the provided value is falsy. * Throws AssertionError if the value is not falsy. - * * @param value - The value to test for falsiness. * @param message - (Optional) Message to prefix in case of failure. * @throws AssertionError - If the value is not falsy. - * * @example + * ```ts + * Assert.isFalse(1 > 2, "Math sanity") + * Assert.isFalse(null, "Null should be falsy") + * Assert.isFalse(undefined, "Undefined should be falsy") + * Assert.isFalse(false, "Boolean false should be falsy") * Assert.isFalse(0, "Zero should be falsy") * Assert.isFalse("", "Empty string should be falsy") + * ``` + * @see Assert.isTrue for the opposite assertion. */ public static isFalse(value: unknown, message: string = ""): void { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" if (value) { - throw new AssertionError(`${MSG}Expected value to be falsy, but got (${Assert.safeStringify(value)})`) + throw new AssertionError(`${PREFIX}Expected value to be falsy, but got (${Assert.safeStringify(value)})`) + } + } + // #endregion isFalse + + // #region isUndefined + /** + * Asserts that the given value is strictly `undefined`. + * Throws AssertionError if the value is not exactly `undefined`. + * @param value - The value to check. + * @param message - (Optional) Message to prefix in case of failure. + * @throws AssertionError - If the value is not `undefined`. + * @example + * ```ts + * Assert.isUndefined(void 0) + * Assert.isUndefined(undefined) + * Assert.isUndefined(null, "Null is not undefined") // Fails + * ``` + * @see Assert.isNotUndefined for the opposite assertion. + * @see Assert.isDefined for an alias that checks for defined values (not `undefined`). + */ + public static isUndefined(value: unknown, message: string = ""): asserts value is undefined { + const PREFIX = message ? `${message}: ` : "" + if (value !== undefined) { + throw new AssertionError(`${PREFIX}Expected value to be undefined, but got (${Assert.safeStringify(value)})`) + } + } + // #endregion isUndefined + + // #region isNotUndefined + /** + * Asserts that the given value is not `undefined`. + * Narrows the type to exclude undefined. + * Throws AssertionError if the value is `undefined`. + * @param value - The value to check. + * @param message - (Optional) Message to prefix in case of failure. + * @throws AssertionError - If the value is `undefined`. + * @example + * ```ts + * Assert.isNotUndefined(42, "Value should not be undefined") + * Assert.isNotUndefined(null, "Null is allowed, but not undefined") + * Assert.isNotUndefined(42) + * Assert.isNotUndefined(null) + * ``` + * @see Assert.isUndefined for the opposite assertion. + * @see Assert.isDefined for an alias that checks for defined values (not `undefined`). + */ + public static isNotUndefined(value: T, message: string = ""): asserts value is Exclude { + const PREFIX = message ? `${message}: ` : "" + if (value === undefined) { + throw new AssertionError(`${PREFIX}Expected value not to be undefined, but got undefined`) + } + } + // #endregion isNotUndefined + + // #region isDefined + /** + * Asserts that the given value is defined (not `undefined`). + * Alias for isNotUndefined. + * @param value - The value to check. + * @param message - (Optional) Message to prefix in case of failure. + * @throws AssertionError - If the value is `undefined`. + * @example + * ```ts + * Assert.isDefined(42, "Value should be defined") + * Assert.isDefined(null, "Null is allowed, but not undefined") + * Assert.isDefined(42) + * Assert.isDefined(null) + * ``` + * @see Assert.isNotUndefined for the opposite assertion. + * @see Assert.isUndefined for an alias that checks for undefined values. + */ + public static isDefined(value: T, message: string = ""): asserts value is Exclude { + Assert.isNotUndefined(value, message) + } + // #endregion isDefined + + // #region notEquals + /** + * Asserts that two values are not equal (deep comparison). + * For arrays and objects, uses deep comparison (via JSON.stringify). + * Throws AssertionError if the values are equal. + * @param actual - The actual value. + * @param notExpected - The value that should NOT match. + * @param message - (Optional) Message to prefix in case of failure. + * @throws AssertionError - If values are equal. + * @example + * ````ts + * Assert.notEquals(1, 2, "Numbers should not be equal") + * Assert.notEquals([1, 2], [2, 1], "Arrays should not be equal") + * Assert.notEquals({ a: 1 }, { a: 2 }, "Objects should not be equal") + * Assert.notEquals(1, 2) + * Assert.notEquals([1,2], [2,1]) + * ``` + * @see Assert.equals for the opposite assertion. + */ + public static notEquals(actual: T, notExpected: T, message: string = ""): void { + const PREFIX = message ? `${message}: ` : "" + try { + Assert.equals(actual, notExpected, message) + } catch { + return // Passed: values are not equal + } + throw new AssertionError(`${PREFIX}Values should not be equal: (${Assert.safeStringify(actual)})`) + } + // #endregion notEquals + + // #region contains + /** + * Asserts that an array or string contains a specified value or substring. + * For arrays, uses indexOf for shallow equality. + * For strings, uses indexOf for substring check. + * Throws AssertionError if the value is not found. + * @param container - The array or string to search. + * @param value - The value (or substring) to search for. + * @param message - (Optional) Message to prefix in case of failure. + * @throws AssertionError - If the value is not found. + * @example + * ```ts + * Assert.contains([1, 2, 3], 2, "Array should contain 2") + * Assert.contains("hello world", "world", "String should contain 'world'") + * Assert.contains([1, 2, 3], 4) // Fails + * Assert.contains("hello world", "test") // Fails + * Assert.contains([1,2,3], 2) + * Assert.contains("hello world", "world") + * ``` + */ + public static contains(container: unknown[] | string, value: unknown, message: string = ""): void { + const PREFIX = message ? `${message}: ` : "" + if (typeof container === "string") { + if (typeof value !== "string" || container.indexOf(value) === -1) { + throw new AssertionError(`${PREFIX}String does not contain expected substring (${Assert.safeStringify(value)})`) + } + return + } + if (Array.isArray(container)) { + if (container.indexOf(value) === -1) { + throw new AssertionError(`${PREFIX}Array does not contain expected value (${Assert.safeStringify(value)})`) + } + return } + throw new AssertionError(`${PREFIX}Contains only works for arrays or strings`) } + // #endregion contains + // #region isInstanceOf + /** + * Asserts that the value is an instance of the specified constructor. + * Throws AssertionError if not. + * @param value - The value to check. + * @param ctor - The class/constructor function. + * @param message - Optional error message prefix. + * @throws AssertionError - If the value is not an instance of the constructor. + * @example + * ```ts + * class MyClass {} + * const instance = new MyClass() + * Assert.isInstanceOf(instance, MyClass, "Should be an instance of MyClass") + * Assert.isInstanceOf(instance, Object) // Passes, since all classes inherit from Object + * Assert.isInstanceOf(42, MyClass) // Fails + * ``` + * @see Assert.isNotInstanceOf for the opposite assertion. + */ + public static isInstanceOf( + value: unknown, + ctor: Function, + message: string = "" + ): void { + const PREFIX = message ? `${message}: ` : "" + if (typeof ctor !== "function") { + throw new AssertionError(`${PREFIX}Provided constructor is not a function or class.`) + } + if (value == null || (typeof value !== "object" && typeof value !== "function")) { + throw new AssertionError( + `${PREFIX}Expected instance of ${ctor.name}, but got (${Assert.safeStringify(value)})` + ) + } + if (!(value instanceof ctor)) { + throw new AssertionError( + `${PREFIX}Expected value to be instance of ${ctor.name}, but got (${Assert.safeStringify(value)})` + ) + } + } + // #endregion isInstanceOf + + // #region isNotInstanceOf + /** + * Asserts that the value is NOT an instance of the specified constructor. + * Throws AssertionError if it is. + * @param value - The value to check. + * @param ctor - The class/constructor function. + * @param message - Optional error message prefix. + * @throws AssertionError - If the value is an instance of the constructor. + * @example + * ```ts + * class MyClass {} + * const instance = new MyClass() + * Assert.isNotInstanceOf(instance, String, "Should not be an instance of String") + * Assert.isNotInstanceOf(instance, MyClass) // Fails + * Assert.isNotInstanceOf(42, MyClass) // Passes, since 42 is not an instance of MyClass + * ``` + * @see Assert.isInstanceOf for the opposite assertion. + */ + public static isNotInstanceOf( + value: unknown, + ctor: Function, + message: string = "" + ): void { + const PREFIX = message ? `${message}: ` : "" + if (typeof ctor !== "function") { + throw new AssertionError(`${PREFIX}Provided constructor is not a function or class.`) + } + if (value != null && (typeof value === "object" || typeof value === "function") && value instanceof ctor) { + throw new AssertionError( + `${PREFIX}Expected value NOT to be instance of ${ctor.name}, but got (${Assert.safeStringify(value)})` + ) + } + } + // #endregion isNotInstanceOf + + // #region arraysEqual /** * Asserts that two one-dimensional arrays are equal by type and value. * Supports arrays of primitives and arrays of objects (shallow comparison via JSON.stringify). @@ -306,10 +612,10 @@ class Assert { * @private */ private static arraysEqual(a: T[], b: T[], message: string = ""): boolean { - const MSG = message ? `${message}: ` : "" + const PREFIX = message ? `${message}: ` : "" if (a.length !== b.length) { - throw new AssertionError(`${MSG}Array length mismatch: actual (${a.length}) !== expected (${b.length})`) + throw new AssertionError(`${PREFIX}Array length mismatch: actual (${a.length}) !== expected (${b.length})`) } for (let i = 0; i < a.length; i++) { @@ -319,37 +625,54 @@ class Assert { const expectedType = typeof expectedValue if (actualType !== expectedType) { - throw new AssertionError(`${MSG}Array type mismatch at index ${i}: actual (${Assert.safeStringify(actualValue)} : ${actualType}) !== expected (${Assert.safeStringify(expectedValue)} : ${expectedType})`) + throw new AssertionError(`${PREFIX}Array type mismatch at index ${i}: actual (${Assert.safeStringify(actualValue)} : ${actualType}) !== expected (${Assert.safeStringify(expectedValue)} : ${expectedType})`) } if (actualType === "object" && expectedType === "object" && actualValue !== null && expectedValue !== null) { if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) { - throw new AssertionError(`${MSG}Array object value mismatch at index ${i}: actual (${Assert.safeStringify(actualValue)}) !== expected (${Assert.safeStringify(expectedValue)})`) + throw new AssertionError(`${PREFIX}Array object value mismatch at index ${i}: actual (${Assert.safeStringify(actualValue)}) !== expected (${Assert.safeStringify(expectedValue)})`) } continue } if (actualValue !== expectedValue) { - throw new AssertionError(`${MSG}Array value mismatch at index ${i}: actual (${Assert.safeStringify(actualValue)}) !== expected (${Assert.safeStringify(expectedValue)})`) + throw new AssertionError(`${PREFIX}Array value mismatch at index ${i}: actual (${Assert.safeStringify(actualValue)}) !== expected (${Assert.safeStringify(expectedValue)})`) } } return true // for consistency; return value is not used } + // #endregion arraysEqual - /** + // #region safeStringify + /** * Returns a safe string representation of any value, handling cases where * toString may throw or misbehave. Used internally by assertion methods. + * Tries JSON.stringify, then value.toString(), then Object.prototype.toString.call(value). + * If all fail, returns "[unprintable value]". * @param value - The value to stringify. - * @returns A string representation of the value, or a fallback if not possible. + * @returns A string representation of the value, or "[unprintable value]" if not possible. + * @private */ private static safeStringify(value: unknown): string { try { if (typeof value === "string") return `"${value}"` if (value && typeof value === "object") { + // Try JSON.stringify try { return JSON.stringify(value) } catch { - return value.toString?.() ?? Object.prototype.toString.call(value) + // Try value.toString() if it's a function + try { + if (typeof (value as { toString?: unknown }).toString === "function") { + return (value as { toString: () => string }).toString() + } + } catch {} + // Try Object.prototype.toString.call(value) + try { + return Object.prototype.toString.call(value) + } catch {} + // All else fails + return "[unprintable value]" } } return String(value) @@ -357,10 +680,13 @@ class Assert { return "[unprintable value]" } } + // #endregion safeStringify + } // #endregion Assert + // #region TestRunner /** * A utility class for managing and running test cases with controlled console output. @@ -404,6 +730,7 @@ class TestRunner { OFF: 0, HEADER: 1, SECTION: 2, + SUBSECTION: 3 } as const; // To facilitate the label associated to the verbosity value. @@ -448,7 +775,20 @@ class TestRunner { } } - /** See detailed JSDoc in class documentation */ + /** See detailed JSDoc in class documentation + * @param name - The name of the test case. + * @param fn - The function containing the test logic. It should contain assertions using `Assert` methods. + * @param indent - Indentation level for the title (default: `2`). The indentation level is indicated + * with the number of suffix '*'. + * @throws AssertionError - If an assertion fails within the test function. + * @example + * ```ts + * const runner = new TestRunner() + * runner.exec("My Test", () => { + * Assert.equals(1 + 1, 2) + * }) + * ``` + */ public exec(name: string, fn: () => void, indent: number = 2): void { this.title(`${TestRunner.START} ${name}`, indent); if (typeof fn !== "function") { @@ -484,4 +824,3 @@ if (typeof globalThis !== "undefined") { //#endregion unit-test-framework.ts -