From 730dac73ae68b2d96e3f15c08873b77d99e07323 Mon Sep 17 00:00:00 2001 From: xingzihai Date: Wed, 25 Mar 2026 17:24:08 +0000 Subject: [PATCH 1/3] docs: Improve CONTRIBUTING.md with comprehensive contributor guide - Add table of contents for easy navigation - Add project architecture overview with tech stack - Add quick start checklist for new contributors - Add finding issues section with categorized links - Add development setup quick guide - Add pull request process guidelines - Add community support channels - Improve overall structure and organization --- CONTRIBUTING.md | 266 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 251 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8252ed59e735..3ceb0ad85742 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,27 +1,263 @@ # Contributing to Appsmith Thank you for your interest in Appsmith and for taking the time to contribute to this project. 🙌 -Appsmith is a project by developers for developers and there are a lot of ways you can contribute. -If you don't know where to start contributing, ask us on our [Discord channel](https://discord.com/invite/rBTTVJp). -## Code of conduct +Appsmith is a project by developers for developers and there are a lot of ways you can contribute. If you don't know where to start contributing, ask us on our [Discord channel](https://discord.com/invite/rBTTVJp). -Read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing +## Table of Contents -## How can I contribute? +- [Code of Conduct](#code-of-conduct) +- [Project Architecture](#project-architecture) +- [Quick Start Checklist](#quick-start-checklist) +- [How Can I Contribute?](#how-can-i-contribute) +- [Development Setup](#development-setup) +- [Pull Request Process](#pull-request-process) +- [Community Support](#community-support) -There are many ways in which you can contribute to Appsmith. +--- + +## Code of Conduct + +Read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. We are committed to fostering an open, welcoming, and safe environment in the community. + +--- + +## Project Architecture + +Understanding the project structure will help you navigate the codebase more effectively: + +``` +appsmith/ +├── app/ +│ ├── client/ # Frontend (React + TypeScript) +│ │ ├── src/ # React components, widgets, utilities +│ │ ├── cypress/ # Cypress integration tests +│ │ └── packages/ # Shared packages including RTS (Real-Time Server) +│ └── server/ # Backend (Java + Spring + WebFlux) +│ ├── appsmith-server/ # Main server application +│ └── appsmith-plugins/ # Database/API connectors +├── deploy/ # Docker & Kubernetes deployment configs +├── contributions/ # Contribution guides and documentation +└── static/ # Static assets for documentation +``` + +### Tech Stack Overview + +| Component | Technologies | +|-----------|--------------| +| **Frontend** | React, TypeScript, Redux, Redux-Saga | +| **Backend** | Java 25, Spring, WebFlux, MongoDB, Redis | +| **Testing** | Jest (unit), Cypress (integration), JUnit (server) | +| **Deployment** | Docker, Kubernetes | + +--- + +## Quick Start Checklist + +New to contributing? Follow this checklist to get started: + +### Before You Start +- [ ] Read this CONTRIBUTING guide +- [ ] Read our [Code of Conduct](CODE_OF_CONDUCT.md) +- [ ] Join our [Discord community](https://discord.com/invite/rBTTVJp) for support + +### For Code Contributions +- [ ] Find an issue to work on (see [Finding Issues](#finding-issues)) +- [ ] Comment on the issue to get it assigned to you +- [ ] Fork and clone the repository +- [ ] Set up your development environment (see [Development Setup](#development-setup)) +- [ ] Create a feature branch from `release` +- [ ] Make your changes and write tests +- [ ] Submit a pull request + +### For Documentation Contributions +- [ ] Visit the [appsmith-docs repository](https://github.com/appsmithorg/appsmith-docs) +- [ ] Read the [Docs Contribution Guide](https://github.com/appsmithorg/appsmith-docs/blob/main/CONTRIBUTING.md) + +--- + +## How Can I Contribute? + +### 🐛 Report a Bug -#### 🐛 Report a bug Report all issues through GitHub Issues using the [Report a Bug](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Bug%2CNeeds+Triaging&template=--bug-report.yaml&title=%5BBug%5D%3A+) template. -To help resolve your issue as quickly as possible, read the template and provide all the requested information. -#### 🛠 File a feature request -We welcome all feature requests, whether it's to add new functionality to an existing extension or to offer an idea for a brand new extension. -File your feature request through GitHub Issues using the [Feature Request](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Enhancement&template=--feature-request.yaml&title=%5BFeature%5D%3A+) template. +**Tips for a good bug report:** +- Use a clear and descriptive title +- Include steps to reproduce the issue +- Describe the expected vs. actual behavior +- Include screenshots or screen recordings if helpful +- Mention your environment (OS, browser, Appsmith version) + +### 🛠 File a Feature Request + +We welcome all feature requests! File your request through GitHub Issues using the [Feature Request](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Enhancement&template=--feature-request.yaml&title=%5BFeature%5D%3A+) template. + +**Tips for a good feature request:** +- Describe the problem you're trying to solve +- Explain how your suggestion would help solve it +- Include examples or mockups if available + +### 📝 Improve the Documentation + +Help us keep our documentation up to date! You can: +- Suggest improvements using the [Documentation templates](https://github.com/appsmithorg/appsmith-docs/issues/new/choose) +- Contribute directly to our [Docs repository](https://github.com/appsmithorg/appsmith-docs) + +### ⚙️ Contribute Code + +#### Finding Issues + +Looking for issues to contribute to? Here are some great starting points: + +| Issue Type | Link | +|------------|------| +| Good First Issues | [Browse →](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) | +| Inviting Contributions | [Browse →](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Inviting+Contribution%22) | +| Help Wanted | [Browse →](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22) | + +> ⚠️ **Important:** Always get an issue assigned to you before starting work. Comment on the issue expressing your interest. Tag `@contributor-support` if needed. Working on issues without assignment may result in your contribution being rejected. + +#### Types of Code Contributions + +| Contribution Type | Guide | +|-------------------|-------| +| Frontend (React/TypeScript) | [Client Setup Guide](contributions/ClientSetup.md) | +| Backend (Java/Spring) | [Server Setup Guide](contributions/ServerSetup.md) | +| New Widget | [Widget Development Guide](contributions/AppsmithWidgetDevelopmentGuide.md) | +| New Plugin/Connector | [Plugin Contribution Guide](contributions/ServerCodeContributionsGuidelines/PluginCodeContributionsGuidelines.md) | +| Add Custom JS Library | [Custom JS Library Guide](contributions/CustomJsLibrary.md) | +| Write Tests | [Test Automation Guide](contributions/docs/TestAutomation.md) | + +--- + +## Development Setup + +### Prerequisites + +Before setting up the development environment, ensure you have: + +| Tool | Version | Notes | +|------|---------|-------| +| Docker | Latest | Required for containerized services | +| Node.js | 20.11.1 | Use nvm or fnm for version management | +| Java | OpenJDK 25 | Eclipse Temurin recommended | +| Maven | 3.9+ | Preferably 3.9.12 | +| Git | Latest | For version control | +| mkcert | Latest | For local HTTPS certificates | + +### Quick Setup + +#### Frontend Only (for UI contributions) + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/appsmith.git +cd appsmith + +# Set up local HTTPS certificates +cd app/client/docker && mkcert -install && mkcert "*.appsmith.com" && cd ../../.. + +# Add dev domain to hosts +echo "127.0.0.1 dev.appsmith.com" | sudo tee -a /etc/hosts + +# Copy environment file +cp .env.example .env + +# Install dependencies and start +cd app/client +yarn install +./start-https.sh https://release.app.appsmith.com # Use staging backend +yarn start +``` + +Your frontend will be running at https://dev.appsmith.com + +#### Full Stack Setup + +1. **Set up MongoDB and Redis** (using Docker): + ```bash + # MongoDB + docker run -d -p 127.0.0.1:27017:27017 --name appsmith-mongodb \ + -e MONGO_INITDB_DATABASE=appsmith mongo --replSet rs0 + + # Redis + docker run -d -p 127.0.0.1:6379:6379 --name appsmith-redis redis + ``` + +2. **Build and run the server**: + ```bash + cd app/server + mvn clean compile + cp envs/dev.env.example .env + # Edit .env to point to your local MongoDB and Redis + ./build.sh -Dmaven.test.skip + ./scripts/start-dev-server.sh + ``` + +For detailed setup instructions, see: +- [Client Setup Guide](contributions/ClientSetup.md) - Full frontend setup with troubleshooting +- [Server Setup Guide](contributions/ServerSetup.md) - Complete backend setup including IntelliJ configuration + +--- + +## Pull Request Process + +### Branch Naming + +Use descriptive branch names following these patterns: +- `fix/bug-description` - For bug fixes +- `feature/feature-name` - For new features +- `docs/description` - For documentation changes + +### Commit Messages + +Write clear, descriptive commit messages: +- Use the present tense ("Add feature" not "Added feature") +- Reference issue numbers when applicable +- Keep the first line under 72 characters + +### Before Submitting + +- [ ] Code compiles without errors +- [ ] Tests pass locally (Jest for frontend, JUnit for backend) +- [ ] New code includes appropriate tests +- [ ] Code follows the existing style conventions +- [ ] PR description clearly explains the changes + +### PR Guidelines + +1. **Create PR from your fork** to `appsmithorg/appsmith` `release` branch +2. **Link the issue** in your PR description (e.g., "Fixes #123") +3. **Tag the maintainer** you're collaborating with +4. **Wait for CI** to pass before requesting review +5. **Address review feedback** promptly and push new commits + +### What NOT to Do + +❌ Work on issues without getting them assigned first +❌ Create PRs without proper description +❌ Request review before CI passes +❌ Submit PRs without tests +❌ Skip reading the contribution guidelines + +--- + +## Community Support + +Need help? We're here for you! + +| Channel | Best For | +|---------|----------| +| [Discord](https://discord.com/invite/rBTTVJp) | Real-time help, discussions, community | +| [GitHub Discussions](https://github.com/appsmithorg/appsmith/discussions) | Questions, ideas, announcements | +| [GitHub Issues](https://github.com/appsmithorg/appsmith/issues) | Bug reports, feature requests | +| [support@appsmith.com](mailto:support@appsmith.com) | Private inquiries, security issues | + +--- + +## Recognition -#### 📝 Improve the documentation -In the process of shipping features quickly, we may forget to keep our docs up to date. You can help by suggesting improvements to our documentation using the [Documentation templates](https://github.com/appsmithorg/appsmith-docs/issues/new/choose) or dive right into our [Docs Contribution Guide](https://github.com/appsmithorg/appsmith-docs/blob/main/CONTRIBUTING.md)! +We ❤️ our contributors! All contributors are recognized in our [README](README.md#top-contributors). Your contributions help make Appsmith better for everyone. -#### ⚙️ Close a Bug / Feature issue -We welcome contributions that help make appsmith bug free & improve the experience of our users. You can also find issues tagged [Good First Issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22+bug). Check out our [Code Contribution Guide](contributions/CodeContributionsGuidelines.md) to begin. +Let's build great software together! 🚀 \ No newline at end of file From 4286112a304b44c8acea3e9ab9165324c06604e9 Mon Sep 17 00:00:00 2001 From: xingzihai <1315258019@qq.com> Date: Thu, 26 Mar 2026 03:35:48 +0000 Subject: [PATCH 2/3] test: add comprehensive unit tests for utility functions - Add TypeHelpers.test.ts: tests for getType() and isURL() functions - Add URLUtils.test.ts: tests for URL manipulation utilities - Add dayJsUtils.test.ts: tests for date/time formatting functions - Add treeUtils.test.ts: tests for tree traversal and manipulation - Add getIsSafeURL.test.ts: tests for URL safety validation - Extend formhelpers.test.ts: comprehensive tests for email/password validation These tests improve coverage for core utility functions used throughout the application, including type checking, URL validation, date formatting, and form validation helpers. --- app/client/src/utils/TypeHelpers.test.ts | 259 +++++++++++++ app/client/src/utils/URLUtils.test.ts | 312 ++++++++++++++++ app/client/src/utils/dayJsUtils.test.ts | 214 +++++++++++ app/client/src/utils/formhelpers.test.ts | 267 ++++++++++++-- app/client/src/utils/treeUtils.test.ts | 341 ++++++++++++++++++ .../src/utils/validation/getIsSafeURL.test.ts | 215 +++++++++++ 6 files changed, 1570 insertions(+), 38 deletions(-) create mode 100644 app/client/src/utils/TypeHelpers.test.ts create mode 100644 app/client/src/utils/URLUtils.test.ts create mode 100644 app/client/src/utils/dayJsUtils.test.ts create mode 100644 app/client/src/utils/treeUtils.test.ts create mode 100644 app/client/src/utils/validation/getIsSafeURL.test.ts diff --git a/app/client/src/utils/TypeHelpers.test.ts b/app/client/src/utils/TypeHelpers.test.ts new file mode 100644 index 000000000000..0835db1142c8 --- /dev/null +++ b/app/client/src/utils/TypeHelpers.test.ts @@ -0,0 +1,259 @@ +import { getType, Types, isURL } from "./TypeHelpers"; + +describe("TypeHelpers", () => { + describe("getType", () => { + describe("STRING type", () => { + it("should return STRING for string values", () => { + expect(getType("hello")).toBe(Types.STRING); + expect(getType("")).toBe(Types.STRING); + expect(getType(" ")).toBe(Types.STRING); + expect(getType("123")).toBe(Types.STRING); + expect(getType("true")).toBe(Types.STRING); + }); + }); + + describe("NUMBER type", () => { + it("should return NUMBER for numeric values", () => { + expect(getType(0)).toBe(Types.NUMBER); + expect(getType(123)).toBe(Types.NUMBER); + expect(getType(-123)).toBe(Types.NUMBER); + expect(getType(3.14)).toBe(Types.NUMBER); + expect(getType(-3.14)).toBe(Types.NUMBER); + expect(getType(Infinity)).toBe(Types.NUMBER); + expect(getType(-Infinity)).toBe(Types.NUMBER); + }); + + it("should return NUMBER for NaN", () => { + expect(getType(NaN)).toBe(Types.NUMBER); + }); + }); + + describe("BOOLEAN type", () => { + it("should return BOOLEAN for boolean values", () => { + expect(getType(true)).toBe(Types.BOOLEAN); + expect(getType(false)).toBe(Types.BOOLEAN); + }); + }); + + describe("ARRAY type", () => { + it("should return ARRAY for array values", () => { + expect(getType([])).toBe(Types.ARRAY); + expect(getType([1, 2, 3])).toBe(Types.ARRAY); + expect(getType(["a", "b", "c"])).toBe(Types.ARRAY); + expect(getType([1, "a", true])).toBe(Types.ARRAY); + expect(getType([null, undefined])).toBe(Types.ARRAY); + }); + }); + + describe("FUNCTION type", () => { + it("should return FUNCTION for function values", () => { + expect(getType(() => {})).toBe(Types.FUNCTION); + expect(getType(function () {})).toBe(Types.FUNCTION); + expect(getType(async () => {})).toBe(Types.FUNCTION); + expect(getType(function* () {})).toBe(Types.FUNCTION); + }); + }); + + describe("OBJECT type", () => { + it("should return OBJECT for plain objects", () => { + expect(getType({})).toBe(Types.OBJECT); + expect(getType({ key: "value" })).toBe(Types.OBJECT); + expect(getType({ a: 1, b: 2 })).toBe(Types.OBJECT); + }); + + it("should return OBJECT for Date objects", () => { + expect(getType(new Date())).toBe(Types.OBJECT); + }); + + it("should return OBJECT for RegExp objects", () => { + expect(getType(/test/)).toBe(Types.OBJECT); + expect(getType(new RegExp("test"))).toBe(Types.OBJECT); + }); + + it("should return OBJECT for Map and Set", () => { + expect(getType(new Map())).toBe(Types.OBJECT); + expect(getType(new Set())).toBe(Types.OBJECT); + }); + + it("should return OBJECT for Error objects", () => { + expect(getType(new Error())).toBe(Types.OBJECT); + expect(getType(new TypeError())).toBe(Types.OBJECT); + }); + }); + + describe("UNDEFINED type", () => { + it("should return UNDEFINED for undefined values", () => { + expect(getType(undefined)).toBe(Types.UNDEFINED); + expect(getType(void 0)).toBe(Types.UNDEFINED); + }); + }); + + describe("NULL type", () => { + it("should return NULL for null values", () => { + expect(getType(null)).toBe(Types.NULL); + }); + }); + + describe("UNKNOWN type", () => { + it("should return UNKNOWN for symbols", () => { + // Symbols are not considered objects by lodash's isObject + expect(getType(Symbol("test"))).toBe(Types.UNKNOWN); + }); + + it("should return UNKNOWN for values that don't match other types", () => { + // Most primitive and object values have specific types + // UNKNOWN is a fallback for edge cases like Symbols + expect(getType(Symbol())).toBe(Types.UNKNOWN); + }); + }); + + describe("Type priority", () => { + it("should correctly identify arrays over objects", () => { + // Arrays are also objects, but getType should return ARRAY + expect(getType([1, 2, 3])).toBe(Types.ARRAY); + expect(getType([])).toBe(Types.ARRAY); + }); + + it("should correctly distinguish between string numbers and actual numbers", () => { + expect(getType("123")).toBe(Types.STRING); + expect(getType(123)).toBe(Types.NUMBER); + }); + }); + }); + + describe("isURL", () => { + describe("Valid URLs", () => { + it("should return true for valid HTTP URLs with proper domain", () => { + expect(isURL("http://example.com")).toBe(true); + expect(isURL("http://www.example.com")).toBe(true); + expect(isURL("http://example.com/path")).toBe(true); + expect(isURL("http://example.com/path/to/page")).toBe(true); + expect(isURL("http://example.com/path?query=value")).toBe(true); + expect(isURL("http://example.com/path?query=value&other=123")).toBe(true); + expect(isURL("http://example.com#anchor")).toBe(true); + }); + + it("should return true for valid HTTPS URLs with proper domain", () => { + expect(isURL("https://example.com")).toBe(true); + expect(isURL("https://www.example.com")).toBe(true); + expect(isURL("https://example.com/path")).toBe(true); + expect(isURL("https://example.com/path?query=value")).toBe(true); + }); + + it("should return true for blob: URLs", () => { + expect(isURL("blob:http://example.com")).toBe(true); + expect(isURL("blob:https://example.com/path")).toBe(true); + }); + + it("should return true for URLs with ports", () => { + expect(isURL("http://example.com:8080")).toBe(true); + expect(isURL("https://example.com:443")).toBe(true); + // Note: localhost without TLD won't match the domain pattern + }); + + it("should return true for URLs with IP addresses", () => { + expect(isURL("http://192.168.1.1")).toBe(true); + expect(isURL("https://10.0.0.1/path")).toBe(true); + expect(isURL("http://127.0.0.1:8080")).toBe(true); + }); + + it("should return true for URLs with subdomains", () => { + expect(isURL("http://sub.example.com")).toBe(true); + expect(isURL("https://api.example.com/v1")).toBe(true); + expect(isURL("http://www.sub.example.com")).toBe(true); + }); + + it("should return true for URLs with fragments", () => { + expect(isURL("http://example.com#section")).toBe(true); + expect(isURL("http://example.com/path#section")).toBe(true); + expect(isURL("http://example.com/path?query=value#section")).toBe(true); + }); + + it("should return true for URLs with query strings", () => { + expect(isURL("http://example.com?a=1")).toBe(true); + expect(isURL("http://example.com?a=1&b=2")).toBe(true); + expect(isURL("http://example.com/path?a=1&b=2&c=3")).toBe(true); + }); + + it("should return true for URLs with trailing slash", () => { + expect(isURL("http://example.com/")).toBe(true); + expect(isURL("http://example.com/path/")).toBe(true); + }); + }); + + describe("Invalid URLs", () => { + it("should return false for non-URL strings", () => { + expect(isURL("not a url")).toBe(false); + expect(isURL("example")).toBe(false); + }); + + it("should return false for empty strings", () => { + expect(isURL("")).toBe(false); + expect(isURL(" ")).toBe(false); + }); + + it("should return false for URLs without protocol", () => { + // Note: The regex actually allows URLs without protocol (protocol is optional) + // The regex pattern allows domains like "example.com" as valid + expect(isURL("example.com")).toBe(true); // Domain without protocol is accepted + expect(isURL("www.example.com")).toBe(true); // Domain without protocol is accepted + }); + + it("should return false for domains without TLD", () => { + // The regex requires a domain with TLD (e.g., .com) + // localhost doesn't have a TLD so it won't match + expect(isURL("http://localhost")).toBe(false); + expect(isURL("http://localhost:3000")).toBe(false); + }); + + it("should return false for FTP and other non-HTTP protocols", () => { + expect(isURL("ftp://example.com")).toBe(false); + expect(isURL("file://example.com")).toBe(false); + expect(isURL("mailto:test@example.com")).toBe(false); + }); + + it("should return false for malformed URLs", () => { + expect(isURL("http://")).toBe(false); + expect(isURL("http://.com")).toBe(false); + }); + + it("should return false for URLs with invalid characters in domain", () => { + expect(isURL("http://example.com with space")).toBe(false); + }); + }); + + describe("Edge cases", () => { + it("should handle case sensitivity", () => { + // The regex uses 'i' flag for case insensitivity + expect(isURL("HTTP://EXAMPLE.COM")).toBe(true); + expect(isURL("HTTPS://Example.Com")).toBe(true); + }); + + it("should handle complex paths", () => { + expect(isURL("http://example.com/path/to/resource/file.html")).toBe(true); + expect(isURL("http://example.com/path-with-dashes")).toBe(true); + expect(isURL("http://example.com/path_with_underscores")).toBe(true); + }); + + it("should handle special characters in path and query", () => { + expect(isURL("http://example.com/path?name=John%20Doe")).toBe(true); + expect(isURL("http://example.com/search?q=hello+world")).toBe(true); + }); + }); + }); + + describe("Types enum", () => { + it("should have all expected type values", () => { + expect(Types.URL).toBe("URL"); + expect(Types.STRING).toBe("STRING"); + expect(Types.NUMBER).toBe("NUMBER"); + expect(Types.BOOLEAN).toBe("BOOLEAN"); + expect(Types.OBJECT).toBe("OBJECT"); + expect(Types.ARRAY).toBe("ARRAY"); + expect(Types.FUNCTION).toBe("FUNCTION"); + expect(Types.UNDEFINED).toBe("UNDEFINED"); + expect(Types.NULL).toBe("NULL"); + expect(Types.UNKNOWN).toBe("UNKNOWN"); + }); + }); +}); \ No newline at end of file diff --git a/app/client/src/utils/URLUtils.test.ts b/app/client/src/utils/URLUtils.test.ts new file mode 100644 index 000000000000..8721594b4ae8 --- /dev/null +++ b/app/client/src/utils/URLUtils.test.ts @@ -0,0 +1,312 @@ +import { + getQueryParams, + convertObjectToQueryParams, + isValidURL, + matchesURLPattern, + sanitizeString, +} from "./URLUtils"; + +describe("URLUtils", () => { + describe("getQueryParams", () => { + let originalLocation: Location; + + beforeEach(() => { + originalLocation = window.location; + delete (window as any).location; + }); + + afterEach(() => { + window.location = originalLocation; + }); + + it("should return an empty object when there are no query params", () => { + (window as any).location = { search: "" }; + + const result = getQueryParams(); + + expect(result).toEqual({}); + }); + + it("should return query params as an object", () => { + (window as any).location = { search: "?key1=value1&key2=value2" }; + + const result = getQueryParams(); + + expect(result).toEqual({ + key1: "value1", + key2: "value2", + }); + }); + + it("should handle a single query param", () => { + (window as any).location = { search: "?key=value" }; + + const result = getQueryParams(); + + expect(result).toEqual({ key: "value" }); + }); + + it("should handle empty values", () => { + (window as any).location = { search: "?key1=&key2=value2" }; + + const result = getQueryParams(); + + expect(result).toEqual({ + key1: "", + key2: "value2", + }); + }); + + it("should handle encoded values", () => { + (window as any).location = { search: "?name=John%20Doe&email=test%40example.com" }; + + const result = getQueryParams(); + + expect(result).toEqual({ + name: "John Doe", + email: "test@example.com", + }); + }); + + it("should handle special characters in values", () => { + (window as any).location = { search: "?key=value%26with%3Dspecial%20chars" }; + + const result = getQueryParams(); + + expect(result).toEqual({ + key: "value&with=special chars", + }); + }); + }); + + describe("convertObjectToQueryParams", () => { + it("should convert a simple object to query params", () => { + const result = convertObjectToQueryParams({ key1: "value1", key2: "value2" }); + + expect(result).toBe("?key1=value1&key2=value2"); + }); + + it("should return an empty string for null input", () => { + const result = convertObjectToQueryParams(null); + + expect(result).toBe(""); + }); + + it("should return an empty string for undefined input", () => { + const result = convertObjectToQueryParams(undefined); + + expect(result).toBe(""); + }); + + it("should handle an empty object", () => { + const result = convertObjectToQueryParams({}); + + expect(result).toBe("?"); + }); + + it("should encode special characters in keys and values", () => { + const result = convertObjectToQueryParams({ + "key with spaces": "value with spaces", + "key&special": "value&special", + }); + + expect(result).toContain("key%20with%20spaces=value%20with%20spaces"); + expect(result).toContain("key%26special=value%26special"); + }); + + it("should handle numeric values", () => { + const result = convertObjectToQueryParams({ count: 42, price: 19.99 }); + + expect(result).toBe("?count=42&price=19.99"); + }); + + it("should handle boolean values", () => { + const result = convertObjectToQueryParams({ active: true, disabled: false }); + + expect(result).toBe("?active=true&disabled=false"); + }); + + it("should handle empty string values", () => { + const result = convertObjectToQueryParams({ key: "" }); + + expect(result).toBe("?key="); + }); + + it("should handle array values by converting to string", () => { + const result = convertObjectToQueryParams({ items: "a,b,c" }); + + expect(result).toBe("?items=a%2Cb%2Cc"); + }); + }); + + describe("isValidURL", () => { + it("should return true for valid HTTP URLs", () => { + expect(isValidURL("http://example.com")).toBe(true); + expect(isValidURL("http://www.example.com")).toBe(true); + expect(isValidURL("http://example.com/path")).toBe(true); + expect(isValidURL("http://example.com/path?query=value")).toBe(true); + }); + + it("should return true for valid HTTPS URLs", () => { + expect(isValidURL("https://example.com")).toBe(true); + expect(isValidURL("https://www.example.com")).toBe(true); + expect(isValidURL("https://example.com/path")).toBe(true); + expect(isValidURL("https://example.com/path?query=value")).toBe(true); + }); + + it("should return true for valid FTP URLs", () => { + expect(isValidURL("ftp://ftp.example.com")).toBe(true); + }); + + it("should return true for valid mailto URLs", () => { + expect(isValidURL("mailto:test@example.com")).toBe(true); + }); + + it("should return true for valid tel URLs", () => { + expect(isValidURL("tel:+1234567890")).toBe(true); + }); + + it("should return true for localhost URLs", () => { + expect(isValidURL("http://localhost")).toBe(true); + expect(isValidURL("http://localhost:3000")).toBe(true); + }); + + it("should return true for URLs with ports", () => { + expect(isValidURL("http://example.com:8080")).toBe(true); + expect(isValidURL("https://example.com:443")).toBe(true); + }); + + it("should return false for invalid URLs", () => { + expect(isValidURL("not a url")).toBe(false); + expect(isValidURL("example.com")).toBe(false); + expect(isValidURL("")).toBe(false); + expect(isValidURL("http://")).toBe(false); + }); + + it("should return false for URLs without protocol", () => { + expect(isValidURL("www.example.com")).toBe(false); + }); + + it("should return true for URLs with fragments", () => { + expect(isValidURL("http://example.com#section")).toBe(true); + expect(isValidURL("http://example.com/path#section")).toBe(true); + }); + + it("should return true for URLs with complex paths", () => { + expect(isValidURL("http://example.com/path/to/resource")).toBe(true); + expect(isValidURL("http://example.com/path/to/resource/file.html")).toBe(true); + }); + }); + + describe("matchesURLPattern", () => { + it("should match standard HTTP URLs", () => { + expect(matchesURLPattern("http://example.com")).toBe(true); + }); + + it("should match standard HTTPS URLs", () => { + expect(matchesURLPattern("https://example.com")).toBe(true); + }); + + it("should match URLs with www prefix", () => { + expect(matchesURLPattern("www.example.com")).toBe(true); + }); + + it("should match URLs with paths", () => { + expect(matchesURLPattern("http://example.com/path/to/page")).toBe(true); + }); + + it("should match URLs with query strings", () => { + expect(matchesURLPattern("http://example.com?key=value")).toBe(true); + }); + + it("should match URLs with fragments", () => { + expect(matchesURLPattern("http://example.com#section")).toBe(true); + }); + + it("should match localhost URLs with trailing slash", () => { + // Note: The regex has localhost(?=\/) which requires localhost to be followed by / + expect(matchesURLPattern("localhost/")).toBe(true); + expect(matchesURLPattern("localhost/path")).toBe(true); + }); + + it("should not match localhost URLs without trailing slash", () => { + // localhost without / doesn't match the pattern + expect(matchesURLPattern("localhost")).toBe(false); + expect(matchesURLPattern("http://localhost:3000")).toBe(false); + }); + + it("should match IP addresses", () => { + expect(matchesURLPattern("192.168.1.1")).toBe(true); + expect(matchesURLPattern("http://192.168.1.1:8080")).toBe(true); + }); + + it("should match URLs with ports", () => { + expect(matchesURLPattern("http://example.com:8080")).toBe(true); + }); + + it("should match URLs with subdomains", () => { + expect(matchesURLPattern("http://sub.example.com")).toBe(true); + expect(matchesURLPattern("http://api.v2.example.com")).toBe(true); + }); + + it("should match FTP URLs", () => { + expect(matchesURLPattern("ftp://ftp.example.com")).toBe(true); + }); + + it("should match mailto URLs", () => { + expect(matchesURLPattern("mailto:test@example.com")).toBe(true); + }); + + it("should not match tel URLs", () => { + // The regex doesn't properly match tel: URLs + expect(matchesURLPattern("tel:+1234567890")).toBe(false); + expect(matchesURLPattern("tel://+1234567890")).toBe(false); + }); + + it("should not match invalid strings", () => { + // Note: The regex is quite permissive, so many strings might match + // This test documents the behavior rather than asserting failure + expect(matchesURLPattern("")).toBe(false); + }); + }); + + describe("sanitizeString", () => { + it("should convert uppercase letters to lowercase", () => { + expect(sanitizeString("HELLO")).toBe("hello"); + expect(sanitizeString("Hello World")).toBe("hello_world"); + }); + + it("should replace special characters with underscores", () => { + expect(sanitizeString("hello-world")).toBe("hello_world"); + expect(sanitizeString("hello@world")).toBe("hello_world"); + expect(sanitizeString("hello world")).toBe("hello_world"); + }); + + it("should keep alphanumeric characters", () => { + expect(sanitizeString("abc123")).toBe("abc123"); + expect(sanitizeString("ABC123")).toBe("abc123"); + }); + + it("should handle multiple special characters in a row", () => { + expect(sanitizeString("hello--world")).toBe("hello__world"); + expect(sanitizeString("hello@@world")).toBe("hello__world"); + }); + + it("should handle strings starting or ending with special characters", () => { + expect(sanitizeString("-hello-")).toBe("_hello_"); + expect(sanitizeString("@hello@")).toBe("_hello_"); + }); + + it("should handle empty string", () => { + expect(sanitizeString("")).toBe(""); + }); + + it("should handle strings with only special characters", () => { + expect(sanitizeString("@#$%")).toBe("____"); + }); + + it("should handle strings with mixed content", () => { + expect(sanitizeString("My-App_v2.0")).toBe("my_app_v2_0"); + }); + }); +}); \ No newline at end of file diff --git a/app/client/src/utils/dayJsUtils.test.ts b/app/client/src/utils/dayJsUtils.test.ts new file mode 100644 index 000000000000..ece9dc362de7 --- /dev/null +++ b/app/client/src/utils/dayJsUtils.test.ts @@ -0,0 +1,214 @@ +import { getHumanizedTime, getReadableDateInFormat, dayjs } from "./dayJsUtils"; + +describe("dayJsUtils", () => { + describe("getHumanizedTime", () => { + it("should return 'a few seconds' for small time values", () => { + expect(getHumanizedTime(1000)).toBe("a few seconds"); + expect(getHumanizedTime(5000)).toBe("a few seconds"); + expect(getHumanizedTime(10000)).toBe("a few seconds"); + }); + + it("should return correct humanized time for minutes", () => { + expect(getHumanizedTime(60000)).toBe("a minute"); + expect(getHumanizedTime(120000)).toBe("2 minutes"); + expect(getHumanizedTime(300000)).toBe("5 minutes"); + expect(getHumanizedTime(1800000)).toBe("30 minutes"); + }); + + it("should return correct humanized time for hours", () => { + expect(getHumanizedTime(3600000)).toBe("an hour"); + expect(getHumanizedTime(7200000)).toBe("2 hours"); + expect(getHumanizedTime(14400000)).toBe("4 hours"); + expect(getHumanizedTime(28800000)).toBe("8 hours"); + }); + + it("should return correct humanized time for days", () => { + expect(getHumanizedTime(86400000)).toBe("a day"); + expect(getHumanizedTime(172800000)).toBe("2 days"); + expect(getHumanizedTime(604800000)).toBe("7 days"); + }); + + it("should return correct humanized time for months", () => { + expect(getHumanizedTime(2592000000)).toBe("a month"); + expect(getHumanizedTime(5184000000)).toBe("2 months"); + }); + + it("should return correct humanized time for years", () => { + expect(getHumanizedTime(31536000000)).toBe("a year"); + expect(getHumanizedTime(63072000000)).toBe("2 years"); + }); + + it("should handle zero milliseconds", () => { + expect(getHumanizedTime(0)).toBe("a few seconds"); + }); + + it("should handle edge cases around time boundaries", () => { + // Just under a minute (dayjs may have different thresholds) + const underMinute = getHumanizedTime(45000); + expect(["a few seconds", "a minute"]).toContain(underMinute); + // Just at a minute + expect(getHumanizedTime(60000)).toBe("a minute"); + // Just under an hour (dayjs duration humanize may round up) + const underHour = getHumanizedTime(3500000); + expect(["58 minutes", "59 minutes", "an hour"]).toContain(underHour); + // Just at an hour + expect(getHumanizedTime(3600000)).toBe("an hour"); + }); + }); + + describe("getReadableDateInFormat", () => { + it("should format date in default format", () => { + const date = new Date("2024-03-15T12:00:00Z"); + expect(getReadableDateInFormat(date, "YYYY-MM-DD")).toBe("2024-03-15"); + }); + + it("should format date with time", () => { + const date = new Date("2024-03-15T14:30:00Z"); + expect(getReadableDateInFormat(date, "YYYY-MM-DD HH:mm")).toBe("2024-03-15 14:30"); + }); + + it("should format date in various formats", () => { + const date = new Date("2024-03-15T12:00:00Z"); + + expect(getReadableDateInFormat(date, "DD/MM/YYYY")).toBe("15/03/2024"); + expect(getReadableDateInFormat(date, "MM-DD-YYYY")).toBe("03-15-2024"); + expect(getReadableDateInFormat(date, "MMMM Do, YYYY")).toBe("March 15th, 2024"); + }); + + it("should handle month names", () => { + const date = new Date("2024-06-15T12:00:00Z"); + expect(getReadableDateInFormat(date, "MMMM YYYY")).toBe("June 2024"); + }); + + it("should handle day names", () => { + const date = new Date("2024-03-15T12:00:00Z"); + expect(getReadableDateInFormat(date, "dddd")).toBe("Friday"); + }); + + it("should format with seconds", () => { + const date = new Date("2024-03-15T14:30:45Z"); + expect(getReadableDateInFormat(date, "HH:mm:ss")).toBe("14:30:45"); + }); + + it("should handle 12-hour format", () => { + const date = new Date("2024-03-15T14:30:00Z"); + expect(getReadableDateInFormat(date, "hh:mm A")).toBe("02:30 PM"); + }); + + it("should handle empty format string", () => { + const date = new Date("2024-03-15T12:00:00Z"); + // dayjs returns ISO format when format string is empty + const result = getReadableDateInFormat(date, ""); + expect(result).toContain("2024"); + }); + + it("should handle current date", () => { + const now = new Date(); + const formatted = getReadableDateInFormat(now, "YYYY-MM-DD"); + // Should be a valid date string + expect(formatted).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); + + it("should handle advanced format features", () => { + const date = new Date("2024-03-15T14:30:45Z"); + + // Quarter + expect(getReadableDateInFormat(date, "Q")).toBe("1"); + + // Note: week of year requires weekOfYear plugin which may not be loaded + // Testing day of year might require additional plugins + expect(typeof getReadableDateInFormat(date, "DD")).toBe("string"); + }); + }); + + describe("dayjs", () => { + it("should be a valid dayjs instance", () => { + const now = dayjs(); + expect(now.isValid()).toBe(true); + }); + + it("should parse date strings", () => { + const date = dayjs("2024-03-15"); + expect(date.isValid()).toBe(true); + expect(date.year()).toBe(2024); + expect(date.month()).toBe(2); // March is month 2 (0-indexed) + expect(date.date()).toBe(15); + }); + + it("should have duration plugin available", () => { + const duration = dayjs.duration(3600000, "milliseconds"); + expect(duration.asMinutes()).toBe(60); + expect(duration.asHours()).toBe(1); + }); + + it("should have relativeTime plugin available", () => { + const now = dayjs(); + const past = now.subtract(1, "hour"); + expect(past.fromNow()).toContain("hour"); + }); + + it("should have advancedFormat plugin available", () => { + const date = dayjs("2024-03-15"); + // advancedFormat allows formatting like 'Do' for ordinal dates + expect(date.format("Do")).toBe("15th"); + }); + + it("should support chaining operations", () => { + const date = dayjs("2024-03-15") + .add(1, "day") + .subtract(1, "month"); + + expect(date.format("YYYY-MM-DD")).toBe("2024-02-16"); + }); + + it("should handle invalid dates", () => { + const invalidDate = dayjs("not a date"); + expect(invalidDate.isValid()).toBe(false); + }); + + it("should compare dates correctly", () => { + const date1 = dayjs("2024-03-15"); + const date2 = dayjs("2024-03-16"); + + expect(date1.isBefore(date2)).toBe(true); + expect(date2.isAfter(date1)).toBe(true); + expect(date1.isSame(date1)).toBe(true); + }); + + it("should handle date manipulation", () => { + const date = dayjs("2024-03-15"); + + expect(date.add(7, "day").format("YYYY-MM-DD")).toBe("2024-03-22"); + expect(date.subtract(1, "month").format("YYYY-MM-DD")).toBe("2024-02-15"); + expect(date.startOf("month").format("YYYY-MM-DD")).toBe("2024-03-01"); + expect(date.endOf("month").format("YYYY-MM-DD")).toBe("2024-03-31"); + }); + }); + + describe("Integration tests", () => { + it("should work together for humanized time from dates", () => { + const now = dayjs(); + const pastDate = now.subtract(2, "hour"); + const diffMs = now.diff(pastDate); + + expect(getHumanizedTime(diffMs)).toBe("2 hours"); + }); + + it("should format relative times correctly", () => { + const now = dayjs(); + const oneHourAgo = now.subtract(1, "hour"); + + expect(oneHourAgo.fromNow()).toBe("an hour ago"); + }); + + it("should handle formatting and duration together", () => { + const startTime = dayjs("2024-03-15T10:00:00"); + const endTime = dayjs("2024-03-15T14:30:00"); + const duration = dayjs.duration(endTime.diff(startTime)); + + expect(getReadableDateInFormat(startTime.toDate(), "HH:mm")).toBe("10:00"); + expect(getReadableDateInFormat(endTime.toDate(), "HH:mm")).toBe("14:30"); + expect(duration.asHours()).toBe(4.5); + }); + }); +}); \ No newline at end of file diff --git a/app/client/src/utils/formhelpers.test.ts b/app/client/src/utils/formhelpers.test.ts index 8caa517ed3d1..468c3ff43cf4 100644 --- a/app/client/src/utils/formhelpers.test.ts +++ b/app/client/src/utils/formhelpers.test.ts @@ -1,43 +1,234 @@ -import { isEmail } from "./formhelpers"; - -describe("isEmail test", () => { - it("Check whether the valid emails are recognized as valid", () => { - const validEmails = [ - "appsmith@yahoo.com", - "appsmith-100@yahoo.com", - "appsmith.100@yahoo.com", - "appsmith111@appsmith.com", - "appsmith-100@appsmith.net", - "appsmith.100@appsmith.com.au", - "appsmith@1.com", - "appsmith@gmail.com.com", - "appsmith+100@gmail.com", - "appsmith-100@yahoo-test.com", - ]; - - validEmails.forEach((validEmail) => { - expect(isEmail(validEmail)).toBeTruthy(); +import { + isEmail, + isEmptyString, + isStrongPassword, + noSpaces, + isRelevantEmail, + PASSWORD_MIN_LENGTH, + PASSWORD_MAX_LENGTH, +} from "./formhelpers"; + +describe("formhelpers", () => { + describe("PASSWORD constants", () => { + it("should have correct minimum password length", () => { + expect(PASSWORD_MIN_LENGTH).toBe(8); + }); + + it("should have correct maximum password length", () => { + expect(PASSWORD_MAX_LENGTH).toBe(48); + }); + }); + + describe("isEmptyString", () => { + it("should return true for empty string", () => { + expect(isEmptyString("")).toBe(true); + }); + + it("should return true for string with only whitespace", () => { + expect(isEmptyString(" ")).toBe(true); + expect(isEmptyString("\t")).toBe(true); + expect(isEmptyString("\n")).toBe(true); + expect(isEmptyString(" \n\t ")).toBe(true); + }); + + it("should return true for null or undefined", () => { + expect(isEmptyString(null as any)).toBe(true); + expect(isEmptyString(undefined as any)).toBe(true); + }); + + it("should return false for strings with content", () => { + expect(isEmptyString("hello")).toBe(false); + expect(isEmptyString(" hello ")).toBe(false); + expect(isEmptyString("a")).toBe(false); + expect(isEmptyString("123")).toBe(false); + }); + + it("should return false for strings with mixed content", () => { + expect(isEmptyString(" hello world ")).toBe(false); + expect(isEmptyString("test@example.com")).toBe(false); + }); + }); + + describe("isStrongPassword", () => { + it("should return true for passwords with valid length", () => { + expect(isStrongPassword("12345678")).toBe(true); // exactly 8 chars + expect(isStrongPassword("123456789")).toBe(true); // 9 chars + expect(isStrongPassword("a".repeat(48))).toBe(true); // exactly 48 chars + }); + + it("should return false for passwords that are too short", () => { + expect(isStrongPassword("")).toBe(false); + expect(isStrongPassword("1234567")).toBe(false); // 7 chars + expect(isStrongPassword("a")).toBe(false); + expect(isStrongPassword("123456")).toBe(false); + }); + + it("should return false for passwords that are too long", () => { + expect(isStrongPassword("a".repeat(49))).toBe(false); // 49 chars + expect(isStrongPassword("a".repeat(100))).toBe(false); + }); + + it("should handle passwords with leading/trailing whitespace", () => { + // The function uses trim(), so whitespace is removed + expect(isStrongPassword(" 12345678 ")).toBe(true); + expect(isStrongPassword(" 1234567 ")).toBe(false); + }); + + it("should return true for edge case lengths", () => { + expect(isStrongPassword("12345678")).toBe(true); // min length + expect(isStrongPassword("a".repeat(48))).toBe(true); // max length + }); + + it("should return true for complex passwords", () => { + expect(isStrongPassword("P@ssw0rd!")).toBe(true); + expect(isStrongPassword("MySecurePassword123!@#")).toBe(true); + }); + }); + + describe("noSpaces", () => { + it("should return true for empty string", () => { + expect(noSpaces("")).toBe(true); + }); + + it("should return true for null or undefined", () => { + expect(noSpaces(null as any)).toBe(true); + expect(noSpaces(undefined as any)).toBe(true); + }); + + it("should return true for string with only whitespace", () => { + expect(noSpaces(" ")).toBe(true); + expect(noSpaces("\t\n")).toBe(true); + }); + + it("should return false for strings with content", () => { + expect(noSpaces("hello")).toBe(false); + expect(noSpaces("a")).toBe(false); + expect(noSpaces(" a ")).toBe(false); + }); + + it("should return false for strings with any non-whitespace character", () => { + expect(noSpaces("test")).toBe(false); + expect(noSpaces("123")).toBe(false); + expect(noSpaces("test example")).toBe(false); + }); + }); + + describe("isEmail", () => { + it("Check whether the valid emails are recognized as valid", () => { + const validEmails = [ + "appsmith@yahoo.com", + "appsmith-100@yahoo.com", + "appsmith.100@yahoo.com", + "appsmith111@appsmith.com", + "appsmith-100@appsmith.net", + "appsmith.100@appsmith.com.au", + "appsmith@1.com", + "appsmith@gmail.com.com", + "appsmith+100@gmail.com", + "appsmith-100@yahoo-test.com", + "test@example.com", + "user.name@example.co.uk", + "user+tag@example.org", + ]; + + validEmails.forEach((validEmail) => { + expect(isEmail(validEmail)).toBeTruthy(); + }); + }); + + it("Check whether the invalid emails are recognized as invalid", () => { + const invalidEmails = [ + "appsmith", + "appsmith@.com.my", + "appsmith123@gmail.a", + "appsmith123@.com", + "appsmith123@.com.com", + ".appsmith@appsmith.com", + "appsmith()*@gmail.com", + "appsmith@%*.com", + "appsmith..2002@gmail.com", + "appsmith.@gmail.com", + "appsmith@appsmith@gmail.com", + "appsmith@gmail.com.1a", + "", + "plainaddress", + "@missingdomain.com", + "missing@.com", + ]; + + invalidEmails.forEach((invalidEmail) => { + expect(isEmail(invalidEmail)).toBeFalsy(); + }); + }); + + it("should handle edge cases", () => { + expect(isEmail("")).toBeFalsy(); + expect(isEmail(" ")).toBeFalsy(); + // Note: "a@b.c" doesn't pass because TLD must be at least 2 characters + expect(isEmail("a@b.cd")).toBeTruthy(); // minimal valid email with 2-char TLD + }); + }); + + describe("isRelevantEmail", () => { + it("should return false for general email domains", () => { + expect(isRelevantEmail("user@gmail.com")).toBe(false); + expect(isRelevantEmail("user@yahoo.com")).toBe(false); + expect(isRelevantEmail("user@outlook.com")).toBe(false); + expect(isRelevantEmail("user@hotmail.com")).toBe(false); + expect(isRelevantEmail("user@aol.com")).toBe(false); + expect(isRelevantEmail("user@icloud.com")).toBe(false); + expect(isRelevantEmail("user@protonmail.com")).toBe(false); + expect(isRelevantEmail("user@zoho.com")).toBe(false); + expect(isRelevantEmail("user@yandex.com")).toBe(false); + expect(isRelevantEmail("user@appsmith.com")).toBe(false); + }); + + it("should return true for custom/business email domains", () => { + expect(isRelevantEmail("user@company.com")).toBe(true); + expect(isRelevantEmail("user@mybusiness.org")).toBe(true); + expect(isRelevantEmail("user@startup.io")).toBe(true); + expect(isRelevantEmail("user@enterprise.net")).toBe(true); + expect(isRelevantEmail("user@example.co")).toBe(true); + }); + + it("should handle case insensitivity", () => { + expect(isRelevantEmail("user@GMAIL.COM")).toBe(false); + expect(isRelevantEmail("user@YAHOO.COM")).toBe(false); + expect(isRelevantEmail("user@COMPANY.COM")).toBe(true); + }); + + it("should return false for invalid email format (missing domain)", () => { + expect(isRelevantEmail("invalid-email")).toBe(false); + expect(isRelevantEmail("no-at-sign")).toBe(false); + expect(isRelevantEmail("")).toBe(false); + }); + + it("should return false for email without domain part", () => { + expect(isRelevantEmail("user@")).toBe(false); + // Note: "@domain.com" split by "@" returns ["", "domain.com"], so domain is "domain.com" + // which is not in GENERAL_DOMAINS, so it returns true + expect(isRelevantEmail("@domain.com")).toBe(true); + }); + + it("should handle subdomains correctly", () => { + // Subdomains of general domains should still be detected + expect(isRelevantEmail("user@mail.gmail.com")).toBe(true); // gmail.com is not the direct domain + expect(isRelevantEmail("user@sub.company.com")).toBe(true); + }); + + it("should handle emails with multiple @ symbols", () => { + // Note: split("@")[1] gets the second part, which is "company" for "user@company@gmail.com" + // "company" is not in GENERAL_DOMAINS, so it returns true (might be a bug in the function) + expect(isRelevantEmail("user@company@gmail.com")).toBe(true); }); }); - it("Check whether the invalid emails are recognized as invalid", () => { - const invalidEmails = [ - "appsmith", - "appsmith@.com.my", - "appsmith123@gmail.a", - "appsmith123@.com", - "appsmith123@.com.com", - ".appsmith@appsmith.com", - "appsmith()*@gmail.com", - "appsmith@%*.com", - "appsmith..2002@gmail.com", - "appsmith.@gmail.com", - "appsmith@appsmith@gmail.com", - "appsmith@gmail.com.1a", - ]; - - invalidEmails.forEach((invalidEmail) => { - expect(isEmail(invalidEmail)).toBeFalsy(); + describe("hashPassword", () => { + it("should return the password as-is", () => { + const { hashPassword } = require("./formhelpers"); + expect(hashPassword("mypassword")).toBe("mypassword"); + expect(hashPassword("")).toBe(""); + expect(hashPassword("complexPassword123!@#")).toBe("complexPassword123!@#"); }); }); -}); +}); \ No newline at end of file diff --git a/app/client/src/utils/treeUtils.test.ts b/app/client/src/utils/treeUtils.test.ts new file mode 100644 index 000000000000..e97186ac0ee8 --- /dev/null +++ b/app/client/src/utils/treeUtils.test.ts @@ -0,0 +1,341 @@ +import { traverseTree, mapTree, sortObjectWithArray } from "./treeUtils"; + +describe("treeUtils", () => { + describe("traverseTree", () => { + it("should call callback for a single node tree", () => { + const tree = { name: "root" }; + const callback = jest.fn(); + + traverseTree(tree, callback); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(tree); + }); + + it("should traverse a tree with children", () => { + const tree = { + name: "root", + children: [{ name: "child1" }, { name: "child2" }], + }; + const callback = jest.fn(); + + traverseTree(tree, callback); + + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenNthCalledWith(1, tree); + expect(callback).toHaveBeenNthCalledWith(2, { name: "child1" }); + expect(callback).toHaveBeenNthCalledWith(3, { name: "child2" }); + }); + + it("should traverse a deeply nested tree", () => { + const tree = { + name: "root", + children: [ + { + name: "child1", + children: [{ name: "grandchild1" }, { name: "grandchild2" }], + }, + { name: "child2" }, + ], + }; + const callback = jest.fn(); + + traverseTree(tree, callback); + + expect(callback).toHaveBeenCalledTimes(5); + expect(callback).toHaveBeenNthCalledWith(1, { name: "root", children: expect.any(Array) }); + expect(callback).toHaveBeenNthCalledWith(2, { name: "child1", children: expect.any(Array) }); + expect(callback).toHaveBeenNthCalledWith(3, { name: "grandchild1" }); + expect(callback).toHaveBeenNthCalledWith(4, { name: "grandchild2" }); + expect(callback).toHaveBeenNthCalledWith(5, { name: "child2" }); + }); + + it("should handle empty children array", () => { + const tree = { + name: "root", + children: [], + }; + const callback = jest.fn(); + + traverseTree(tree, callback); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should handle tree without children property", () => { + const tree = { name: "root" }; + const callback = jest.fn(); + + traverseTree(tree, callback); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should allow callback to access node properties", () => { + const tree = { + name: "root", + value: 10, + children: [{ name: "child", value: 20 }], + }; + const names: string[] = []; + + traverseTree(tree, (node) => { + names.push(node.name); + }); + + expect(names).toEqual(["root", "child"]); + }); + + it("should handle complex tree structures", () => { + const tree = { + id: 1, + data: { label: "Root" }, + children: [ + { + id: 2, + data: { label: "Child 1" }, + children: [{ id: 3, data: { label: "Grandchild" } }], + }, + { + id: 4, + data: { label: "Child 2" }, + }, + ], + }; + const ids: number[] = []; + + traverseTree(tree, (node) => { + ids.push(node.id); + }); + + expect(ids).toEqual([1, 2, 3, 4]); + }); + }); + + describe("mapTree", () => { + it("should map a single node tree", () => { + const tree = { name: "root", value: 1 }; + const callback = (node: any) => ({ ...node, mapped: true }); + + const result = mapTree(tree, callback); + + expect(result).toEqual({ name: "root", value: 1, mapped: true }); + }); + + it("should map a tree with children", () => { + const tree = { + name: "root", + children: [{ name: "child1" }, { name: "child2" }], + }; + const callback = (node: any) => ({ ...node, visited: true }); + + const result = mapTree(tree, callback); + + expect(result).toEqual({ + name: "root", + visited: true, + children: [ + { name: "child1", visited: true }, + { name: "child2", visited: true }, + ], + }); + }); + + it("should map a deeply nested tree", () => { + const tree = { + name: "root", + children: [ + { + name: "child1", + children: [{ name: "grandchild" }], + }, + ], + }; + const callback = (node: any) => ({ + ...node, + level: node.name.split("").length, + }); + + const result = mapTree(tree, callback); + + expect(result).toEqual({ + name: "root", + level: 4, + children: [ + { + name: "child1", + level: 6, + children: [{ name: "grandchild", level: 10 }], + }, + ], + }); + }); + + it("should not modify the original tree", () => { + const tree = { + name: "root", + children: [{ name: "child" }], + }; + const originalTree = JSON.stringify(tree); + + mapTree(tree, (node: any) => ({ ...node, modified: true })); + + expect(JSON.stringify(tree)).toBe(originalTree); + }); + + it("should handle empty children array", () => { + const tree = { + name: "root", + children: [], + }; + const callback = (node: any) => ({ ...node, processed: true }); + + const result = mapTree(tree, callback); + + expect(result).toEqual({ + name: "root", + processed: true, + children: [], + }); + }); + + it("should handle tree without children property", () => { + const tree = { name: "root", value: 42 }; + const callback = (node: any) => ({ ...node, doubled: node.value * 2 }); + + const result = mapTree(tree, callback); + + expect(result).toEqual({ name: "root", value: 42, doubled: 84 }); + }); + + it("should handle complex transformations", () => { + const tree = { + id: 1, + children: [ + { id: 2, children: [{ id: 4 }] }, + { id: 3 }, + ], + }; + const callback = (node: any) => ({ nodeId: node.id }); + + const result = mapTree(tree, callback); + + expect(result).toEqual({ + nodeId: 1, + children: [ + { nodeId: 2, children: [{ nodeId: 4 }] }, + { nodeId: 3 }, + ], + }); + }); + }); + + describe("sortObjectWithArray", () => { + it("should sort array values for each key", () => { + const data = { + key1: ["c", "a", "b"], + key2: ["z", "x", "y"], + }; + + const result = sortObjectWithArray(data); + + expect(result.key1).toEqual(["a", "b", "c"]); + expect(result.key2).toEqual(["x", "y", "z"]); + }); + + it("should handle already sorted arrays", () => { + const data = { + key: ["a", "b", "c"], + }; + + const result = sortObjectWithArray(data); + + expect(result.key).toEqual(["a", "b", "c"]); + }); + + it("should handle empty arrays", () => { + const data = { + key: [], + }; + + const result = sortObjectWithArray(data); + + expect(result.key).toEqual([]); + }); + + it("should handle multiple keys", () => { + const data = { + letters: ["b", "a", "c"], + numbers: ["3", "1", "2"], + mixed: ["z", "a", "m"], + }; + + const result = sortObjectWithArray(data); + + expect(result.letters).toEqual(["a", "b", "c"]); + expect(result.numbers).toEqual(["1", "2", "3"]); + expect(result.mixed).toEqual(["a", "m", "z"]); + }); + + it("should handle single element arrays", () => { + const data = { + key: ["only"], + }; + + const result = sortObjectWithArray(data); + + expect(result.key).toEqual(["only"]); + }); + + it("should handle numeric string sorting (lexicographic)", () => { + const data = { + numbers: ["10", "2", "1", "20"], + }; + + const result = sortObjectWithArray(data); + + // String sorting is lexicographic, not numeric + expect(result.numbers).toEqual(["1", "10", "2", "20"]); + }); + + it("should modify the original object", () => { + const data = { + key: ["c", "a", "b"], + }; + + sortObjectWithArray(data); + + // The function modifies the original object + expect(data.key).toEqual(["a", "b", "c"]); + }); + + it("should return the same object reference", () => { + const data = { + key: ["b", "a"], + }; + + const result = sortObjectWithArray(data); + + expect(result).toBe(data); + }); + + it("should handle empty object", () => { + const data = {}; + + const result = sortObjectWithArray(data); + + expect(result).toEqual({}); + }); + + it("should handle case-sensitive sorting", () => { + const data = { + mixed: ["B", "a", "C", "b"], + }; + + const result = sortObjectWithArray(data); + + // Default string sort is case-sensitive (uppercase comes before lowercase) + expect(result.mixed).toEqual(["B", "C", "a", "b"]); + }); + }); +}); \ No newline at end of file diff --git a/app/client/src/utils/validation/getIsSafeURL.test.ts b/app/client/src/utils/validation/getIsSafeURL.test.ts new file mode 100644 index 000000000000..e5c53fef6730 --- /dev/null +++ b/app/client/src/utils/validation/getIsSafeURL.test.ts @@ -0,0 +1,215 @@ +import getIsSafeURL from "./getIsSafeURL"; + +// Note: getIsSafeURL returns a truthy match array for matching URLs, not a boolean true +// For non-matching URLs, it returns false (for non-strings) or null/falsy (for non-matching strings) + +describe("getIsSafeURL", () => { + describe("Safe URLs", () => { + describe("HTTP/HTTPS URLs", () => { + it("should return truthy for valid HTTP URLs", () => { + expect(getIsSafeURL("http://example.com")).toBeTruthy(); + expect(getIsSafeURL("http://example.com/path")).toBeTruthy(); + expect(getIsSafeURL("http://example.com/path?query=value")).toBeTruthy(); + }); + + it("should return truthy for valid HTTPS URLs", () => { + expect(getIsSafeURL("https://example.com")).toBeTruthy(); + expect(getIsSafeURL("https://example.com/path")).toBeTruthy(); + expect(getIsSafeURL("https://example.com/path?query=value#hash")).toBeTruthy(); + }); + }); + + describe("Mailto URLs", () => { + it("should return truthy for mailto URLs", () => { + expect(getIsSafeURL("mailto:test@example.com")).toBeTruthy(); + expect(getIsSafeURL("mailto:user@domain.org")).toBeTruthy(); + }); + }); + + describe("FTP URLs", () => { + it("should return truthy for FTP URLs", () => { + expect(getIsSafeURL("ftp://ftp.example.com")).toBeTruthy(); + expect(getIsSafeURL("ftp://files.example.com/file.txt")).toBeTruthy(); + }); + }); + + describe("Tel URLs", () => { + it("should return truthy for tel URLs", () => { + expect(getIsSafeURL("tel:+1234567890")).toBeTruthy(); + expect(getIsSafeURL("tel:1234567890")).toBeTruthy(); + }); + }); + + describe("File URLs", () => { + it("should return truthy for file URLs", () => { + expect(getIsSafeURL("file:///path/to/file")).toBeTruthy(); + expect(getIsSafeURL("file://localhost/path")).toBeTruthy(); + }); + }); + + describe("SMS URLs", () => { + it("should return truthy for sms URLs", () => { + expect(getIsSafeURL("sms:+1234567890")).toBeTruthy(); + expect(getIsSafeURL("sms:1234567890?body=Hello")).toBeTruthy(); + }); + }); + }); + + describe("Data URLs", () => { + describe("Image data URLs", () => { + it("should return truthy for valid image data URLs", () => { + expect(getIsSafeURL("data:image/png;base64,iVBORw0KGgo=")).toBeTruthy(); + expect(getIsSafeURL("data:image/jpeg;base64,/9j/4AAQSkZJ=")).toBeTruthy(); + expect(getIsSafeURL("data:image/gif;base64,R0lGODlh=")).toBeTruthy(); + expect(getIsSafeURL("data:image/bmp;base64,Qk0=")).toBeTruthy(); + expect(getIsSafeURL("data:image/tiff;base64,SUkqAA==")).toBeTruthy(); + expect(getIsSafeURL("data:image/webp;base64,UklGRj==")).toBeTruthy(); + }); + + it("should return truthy for image data URLs with different base64 patterns", () => { + expect(getIsSafeURL("data:image/png;base64,abc123+/==")).toBeTruthy(); + expect(getIsSafeURL("data:image/jpg;base64,xyz789=")).toBeTruthy(); + }); + }); + + describe("Video data URLs", () => { + it("should return truthy for valid video data URLs", () => { + expect(getIsSafeURL("data:video/mp4;base64,AAAAIGZ0eXBpc29t")).toBeTruthy(); + expect(getIsSafeURL("data:video/mpeg;base64,AAAAGGZ0eXBpc29t")).toBeTruthy(); + expect(getIsSafeURL("data:video/ogg;base64,AAAAGGZ0eXBpc29t")).toBeTruthy(); + expect(getIsSafeURL("data:video/webm;base64,AAAAGGZ0eXBpc29t")).toBeTruthy(); + }); + }); + + describe("Audio data URLs", () => { + it("should return truthy for valid audio data URLs", () => { + expect(getIsSafeURL("data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy(); + expect(getIsSafeURL("data:audio/oga;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy(); + expect(getIsSafeURL("data:audio/ogg;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy(); + expect(getIsSafeURL("data:audio/opus;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy(); + }); + }); + + describe("Invalid data URLs", () => { + it("should return falsy for data URLs with unsupported MIME types", () => { + expect(getIsSafeURL("data:text/html;base64,PGh0bWw+")).toBeFalsy(); + expect(getIsSafeURL("data:text/javascript;base64,YWxlcnQoMSk=")).toBeFalsy(); + expect(getIsSafeURL("data:application/pdf;base64,JVBERi0xLjQ=")).toBeFalsy(); + expect(getIsSafeURL("data:application/json;base64,eyAiYWJjIjogMTIzIH0=")).toBeFalsy(); + }); + + it("should return falsy for data URLs without base64 encoding", () => { + expect(getIsSafeURL("data:text/plain,Hello World")).toBeFalsy(); + expect(getIsSafeURL("data:text/html,")).toBeFalsy(); + }); + }); + }); + + describe("Unsafe URLs", () => { + describe("JavaScript URLs", () => { + it("should return falsy for javascript: URLs", () => { + expect(getIsSafeURL("javascript:alert(1)")).toBeFalsy(); + expect(getIsSafeURL("javascript:void(0)")).toBeFalsy(); + expect(getIsSafeURL("javascript:window.location='http://evil.com'")).toBeFalsy(); + }); + }); + + describe("Data URLs with malicious content", () => { + it("should return falsy for data URLs that could execute scripts", () => { + expect(getIsSafeURL("data:text/html,")).toBeFalsy(); + expect(getIsSafeURL("data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==")).toBeFalsy(); + }); + }); + + describe("Other unsafe protocols", () => { + it("should return falsy for vbscript: URLs", () => { + expect(getIsSafeURL("vbscript:msgbox(1)")).toBeFalsy(); + }); + + it("should return falsy for data URLs with HTML", () => { + expect(getIsSafeURL("data:text/html;charset=utf-8,")).toBeFalsy(); + }); + }); + }); + + describe("Relative URLs and paths", () => { + it("should return truthy for relative paths", () => { + expect(getIsSafeURL("/path/to/resource")).toBeTruthy(); + expect(getIsSafeURL("path/to/resource")).toBeTruthy(); + expect(getIsSafeURL("./relative/path")).toBeTruthy(); + expect(getIsSafeURL("../parent/path")).toBeTruthy(); + }); + + it("should return truthy for paths with query strings", () => { + expect(getIsSafeURL("/path?query=value")).toBeTruthy(); + expect(getIsSafeURL("path?a=1&b=2")).toBeTruthy(); + }); + + it("should return truthy for paths with fragments", () => { + expect(getIsSafeURL("/path#section")).toBeTruthy(); + expect(getIsSafeURL("#anchor")).toBeTruthy(); + }); + }); + + describe("Edge cases", () => { + it("should return falsy for non-string input", () => { + expect(getIsSafeURL(null as any)).toBeFalsy(); + expect(getIsSafeURL(undefined as any)).toBeFalsy(); + expect(getIsSafeURL(123 as any)).toBeFalsy(); + expect(getIsSafeURL({} as any)).toBeFalsy(); + }); + + it("should return truthy for empty string", () => { + // Note: The regex pattern matches empty string because [^&:/?#]* can match zero characters + // followed by $ which is end of string + expect(getIsSafeURL("")).toBeTruthy(); + }); + + it("should handle URLs with special characters in path", () => { + expect(getIsSafeURL("http://example.com/path%20with%20spaces")).toBeTruthy(); + expect(getIsSafeURL("http://example.com/path?query=value&other=123")).toBeTruthy(); + }); + + it("should handle case insensitivity for MIME types", () => { + expect(getIsSafeURL("data:IMAGE/png;base64,iVBORw0KGgo=")).toBeTruthy(); + expect(getIsSafeURL("data:Image/Png;base64,iVBORw0KGgo=")).toBeTruthy(); + }); + }); + + describe("URL safety validation", () => { + it("should prevent XSS via javascript: protocol", () => { + const xssPayloads = [ + "javascript:alert(document.cookie)", + "javascript:window.location='http://evil.com'", + "javascript:void(document.body.innerHTML='')", + ]; + + xssPayloads.forEach((payload) => { + expect(getIsSafeURL(payload)).toBeFalsy(); + }); + }); + + it("should prevent XSS via data: URLs with HTML", () => { + const htmlPayloads = [ + "data:text/html,", + "data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==", + "data:text/html,", + ]; + + htmlPayloads.forEach((payload) => { + expect(getIsSafeURL(payload)).toBeFalsy(); + }); + }); + + it("should allow safe image data URLs", () => { + const safeImageUrls = [ + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBEQAhEDEQAAL+fgA//9k=", + ]; + + safeImageUrls.forEach((url) => { + expect(getIsSafeURL(url)).toBeTruthy(); + }); + }); + }); +}); \ No newline at end of file From f2d6d085aafb0f77a2344aee28e640da66e784a7 Mon Sep 17 00:00:00 2001 From: xingzihai <1315258019@qq.com> Date: Thu, 26 Mar 2026 03:55:31 +0000 Subject: [PATCH 3/3] fix: address CodeRabbit review comments - Add locale pinning in dayJsUtils.test.ts to ensure deterministic English assertions - Use local Date constructors instead of UTC strings for timezone-independent tests - Fix test description in TypeHelpers.test.ts to match actual behavior --- app/client/src/utils/TypeHelpers.test.ts | 2 +- app/client/src/utils/dayJsUtils.test.ts | 29 ++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/client/src/utils/TypeHelpers.test.ts b/app/client/src/utils/TypeHelpers.test.ts index 0835db1142c8..f9040c3a4e8d 100644 --- a/app/client/src/utils/TypeHelpers.test.ts +++ b/app/client/src/utils/TypeHelpers.test.ts @@ -192,7 +192,7 @@ describe("TypeHelpers", () => { expect(isURL(" ")).toBe(false); }); - it("should return false for URLs without protocol", () => { + it("should accept domain URLs without protocol", () => { // Note: The regex actually allows URLs without protocol (protocol is optional) // The regex pattern allows domains like "example.com" as valid expect(isURL("example.com")).toBe(true); // Domain without protocol is accepted diff --git a/app/client/src/utils/dayJsUtils.test.ts b/app/client/src/utils/dayJsUtils.test.ts index ece9dc362de7..fc689d298ca4 100644 --- a/app/client/src/utils/dayJsUtils.test.ts +++ b/app/client/src/utils/dayJsUtils.test.ts @@ -1,6 +1,17 @@ import { getHumanizedTime, getReadableDateInFormat, dayjs } from "./dayJsUtils"; describe("dayJsUtils", () => { + let previousLocale: string; + + beforeAll(() => { + previousLocale = dayjs.locale(); + dayjs.locale("en"); + }); + + afterAll(() => { + dayjs.locale(previousLocale); + }); + describe("getHumanizedTime", () => { it("should return 'a few seconds' for small time values", () => { expect(getHumanizedTime(1000)).toBe("a few seconds"); @@ -58,17 +69,17 @@ describe("dayJsUtils", () => { describe("getReadableDateInFormat", () => { it("should format date in default format", () => { - const date = new Date("2024-03-15T12:00:00Z"); + const date = new Date(2024, 2, 15, 12, 0, 0); // Local time, timezone-independent expect(getReadableDateInFormat(date, "YYYY-MM-DD")).toBe("2024-03-15"); }); it("should format date with time", () => { - const date = new Date("2024-03-15T14:30:00Z"); + const date = new Date(2024, 2, 15, 14, 30, 0); // Local time, timezone-independent expect(getReadableDateInFormat(date, "YYYY-MM-DD HH:mm")).toBe("2024-03-15 14:30"); }); it("should format date in various formats", () => { - const date = new Date("2024-03-15T12:00:00Z"); + const date = new Date(2024, 2, 15, 12, 0, 0); // Local time, timezone-independent expect(getReadableDateInFormat(date, "DD/MM/YYYY")).toBe("15/03/2024"); expect(getReadableDateInFormat(date, "MM-DD-YYYY")).toBe("03-15-2024"); @@ -76,27 +87,27 @@ describe("dayJsUtils", () => { }); it("should handle month names", () => { - const date = new Date("2024-06-15T12:00:00Z"); + const date = new Date(2024, 5, 15, 12, 0, 0); // June 15, 2024 (month is 0-indexed) expect(getReadableDateInFormat(date, "MMMM YYYY")).toBe("June 2024"); }); it("should handle day names", () => { - const date = new Date("2024-03-15T12:00:00Z"); + const date = new Date(2024, 2, 15, 12, 0, 0); // March 15, 2024 is a Friday expect(getReadableDateInFormat(date, "dddd")).toBe("Friday"); }); it("should format with seconds", () => { - const date = new Date("2024-03-15T14:30:45Z"); + const date = new Date(2024, 2, 15, 14, 30, 45); // Local time, timezone-independent expect(getReadableDateInFormat(date, "HH:mm:ss")).toBe("14:30:45"); }); it("should handle 12-hour format", () => { - const date = new Date("2024-03-15T14:30:00Z"); + const date = new Date(2024, 2, 15, 14, 30, 0); // 2:30 PM in local time expect(getReadableDateInFormat(date, "hh:mm A")).toBe("02:30 PM"); }); it("should handle empty format string", () => { - const date = new Date("2024-03-15T12:00:00Z"); + const date = new Date(2024, 2, 15, 12, 0, 0); // dayjs returns ISO format when format string is empty const result = getReadableDateInFormat(date, ""); expect(result).toContain("2024"); @@ -110,7 +121,7 @@ describe("dayJsUtils", () => { }); it("should handle advanced format features", () => { - const date = new Date("2024-03-15T14:30:45Z"); + const date = new Date(2024, 2, 15, 14, 30, 45); // March 15, 2024 is Q1 // Quarter expect(getReadableDateInFormat(date, "Q")).toBe("1");