fix: improves table formatting in the playground and adds Jest tests (#8743)
* feat(tests): add Jest configuration and setup for testing environment - Introduced Jest configuration file to set up testing environment with TypeScript support and JSDOM. - Added setupTests.ts for global test configurations, including mocks for ResizeObserver and IntersectionObserver. - Updated package.json and package-lock.json to include Jest and related dependencies. - Implemented utility functions for processing markdown content, including handling tables and <think> tags. - Added comprehensive tests for markdown utility functions to ensure proper functionality. * refactor(makefile): separate frontend commands into a dedicated Makefile - Removed frontend-related targets from the main Makefile and created a new Makefile.frontend to manage frontend-specific commands. - Updated the main Makefile to include a reference to the new frontend Makefile and added a help message for frontend commands. - This restructuring improves organization and clarity for managing backend and frontend build processes.
This commit is contained in:
parent
0034c0b8cc
commit
ff00eb582f
10 changed files with 5004 additions and 73 deletions
47
Makefile
47
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: all init format_backend format_frontend format lint build build_frontend install_frontend run_frontend run_backend dev help tests coverage clean_python_cache clean_npm_cache clean_all
|
||||
.PHONY: all init format_backend format lint build run_backend dev help tests coverage clean_python_cache clean_npm_cache clean_all
|
||||
|
||||
# Configurations
|
||||
VERSION=$(shell grep "^version" pyproject.toml | sed 's/.*\"\(.*\)\"$$/\1/')
|
||||
|
|
@ -45,6 +45,7 @@ help: ## show this help message
|
|||
awk -F ':.*##' '{printf "\033[36mmake %s\033[0m: %s\n", $$1, $$2}' | \
|
||||
column -c2 -t -s :
|
||||
@echo '----'
|
||||
@echo 'For frontend commands, run: make help_frontend'
|
||||
|
||||
######################
|
||||
# INSTALL PROJECT
|
||||
|
|
@ -58,22 +59,7 @@ install_backend: ## install the backend dependencies
|
|||
@echo 'Installing backend dependencies'
|
||||
@uv sync --frozen --extra "postgresql" $(EXTRA_ARGS)
|
||||
|
||||
install_frontend: ## install the frontend dependencies
|
||||
@echo 'Installing frontend dependencies'
|
||||
@cd src/frontend && npm install > /dev/null 2>&1
|
||||
|
||||
build_frontend: ## build the frontend static files
|
||||
@echo '==== Starting frontend build ===='
|
||||
@echo 'Current directory: $$(pwd)'
|
||||
@echo 'Checking if src/frontend exists...'
|
||||
@ls -la src/frontend || true
|
||||
@echo 'Building frontend static files...'
|
||||
@cd src/frontend && CI='' npm run build 2>&1 || { echo "\nBuild failed! Error output above ☝️"; exit 1; }
|
||||
@echo 'Clearing destination directory...'
|
||||
$(call CLEAR_DIRS,src/backend/base/langflow/frontend)
|
||||
@echo 'Copying build files...'
|
||||
@cp -r src/frontend/build/. src/backend/base/langflow/frontend
|
||||
@echo '==== Frontend build complete ===='
|
||||
|
||||
init: check_tools ## initialize the project
|
||||
@make install_backend
|
||||
|
|
@ -189,9 +175,6 @@ format_backend: ## backend code formatters
|
|||
@uv run ruff check . --fix
|
||||
@uv run ruff format . --config pyproject.toml
|
||||
|
||||
format_frontend: ## frontend code formatters
|
||||
@cd src/frontend && npm run format
|
||||
|
||||
format: format_backend format_frontend ## run code formatters
|
||||
|
||||
unsafe_fix:
|
||||
|
|
@ -200,22 +183,7 @@ unsafe_fix:
|
|||
lint: install_backend ## run linters
|
||||
@uv run mypy --namespace-packages -p "langflow"
|
||||
|
||||
install_frontendci:
|
||||
@cd src/frontend && npm ci > /dev/null 2>&1
|
||||
|
||||
install_frontendc:
|
||||
@cd src/frontend && $(call CLEAR_DIRS,node_modules) && rm -f package-lock.json && npm install > /dev/null 2>&1
|
||||
|
||||
run_frontend: ## run the frontend
|
||||
@-kill -9 `lsof -t -i:3000`
|
||||
@cd src/frontend && npm start $(if $(FRONTEND_START_FLAGS),-- $(FRONTEND_START_FLAGS))
|
||||
|
||||
tests_frontend: ## run frontend tests
|
||||
ifeq ($(UI), true)
|
||||
@cd src/frontend && npx playwright test --ui --project=chromium
|
||||
else
|
||||
@cd src/frontend && npx playwright test --project=chromium
|
||||
endif
|
||||
|
||||
run_cli: install_frontend install_backend build_frontend ## run the CLI
|
||||
@echo 'Running the CLI'
|
||||
|
|
@ -250,11 +218,7 @@ setup_devcontainer: ## set up the development container
|
|||
setup_env: ## set up the environment
|
||||
@sh ./scripts/setup/setup_env.sh
|
||||
|
||||
frontend: install_frontend ## run the frontend in development mode
|
||||
make run_frontend
|
||||
|
||||
frontendc: install_frontendc
|
||||
make run_frontend
|
||||
|
||||
|
||||
backend: setup_env install_backend ## run the backend in development mode
|
||||
|
|
@ -551,3 +515,10 @@ locust: ## run locust load tests (options: locust_users=10 locust_spawn_rate=1 l
|
|||
--host $(locust_host) \
|
||||
-f $$(basename "$(locust_file)"); \
|
||||
fi
|
||||
|
||||
######################
|
||||
# INCLUDE FRONTEND MAKEFILE
|
||||
######################
|
||||
|
||||
# Include frontend-specific Makefile
|
||||
include Makefile.frontend
|
||||
|
|
|
|||
206
Makefile.frontend
Normal file
206
Makefile.frontend
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# Frontend-specific Makefile for Langflow
|
||||
# This file contains all frontend-related targets
|
||||
|
||||
# Variables
|
||||
FRONTEND_DIR = src/frontend
|
||||
NPM = npm
|
||||
|
||||
.PHONY: install_frontend install_frontendci install_frontendc frontend_deps_check build_frontend run_frontend frontend frontendc format_frontend tests_frontend test_frontend test_frontend_watch test_frontend_coverage test_frontend_verbose test_frontend_ci test_frontend_clean test_frontend_file test_frontend_pattern test_frontend_snapshots test_frontend_config test_frontend_bail test_frontend_silent test_frontend_coverage_open help_frontend
|
||||
|
||||
######################
|
||||
# FRONTEND DEPENDENCIES
|
||||
######################
|
||||
|
||||
install_frontend: ## install the frontend dependencies
|
||||
@echo 'Installing frontend dependencies'
|
||||
@cd $(FRONTEND_DIR) && npm install > /dev/null 2>&1
|
||||
|
||||
install_frontendci:
|
||||
@cd $(FRONTEND_DIR) && npm ci > /dev/null 2>&1
|
||||
|
||||
install_frontendc:
|
||||
@cd $(FRONTEND_DIR) && $(call CLEAR_DIRS,node_modules) && rm -f package-lock.json && npm install > /dev/null 2>&1
|
||||
|
||||
# Check if frontend dependencies are installed
|
||||
frontend_deps_check:
|
||||
@if [ ! -d "$(FRONTEND_DIR)/node_modules" ]; then \
|
||||
echo "Frontend dependencies not found. Installing..."; \
|
||||
$(MAKE) install_frontend; \
|
||||
fi
|
||||
|
||||
######################
|
||||
# FRONTEND BUILD
|
||||
######################
|
||||
|
||||
build_frontend: ## build the frontend static files
|
||||
@echo '==== Starting frontend build ===='
|
||||
@echo 'Current directory: $$(pwd)'
|
||||
@echo 'Checking if $(FRONTEND_DIR) exists...'
|
||||
@ls -la $(FRONTEND_DIR) || true
|
||||
@echo 'Building frontend static files...'
|
||||
@cd $(FRONTEND_DIR) && CI='' npm run build 2>&1 || { echo "\nBuild failed! Error output above ☝️"; exit 1; }
|
||||
@echo 'Clearing destination directory...'
|
||||
$(call CLEAR_DIRS,src/backend/base/langflow/frontend)
|
||||
@echo 'Copying build files...'
|
||||
@cp -r $(FRONTEND_DIR)/build/. src/backend/base/langflow/frontend
|
||||
@echo '==== Frontend build complete ===='
|
||||
|
||||
######################
|
||||
# FRONTEND DEVELOPMENT
|
||||
######################
|
||||
|
||||
run_frontend: ## run the frontend
|
||||
@-kill -9 `lsof -t -i:3000`
|
||||
@cd $(FRONTEND_DIR) && npm start $(if $(FRONTEND_START_FLAGS),-- $(FRONTEND_START_FLAGS))
|
||||
|
||||
frontend: install_frontend ## run the frontend in development mode
|
||||
make run_frontend
|
||||
|
||||
frontendc: install_frontendc
|
||||
make run_frontend
|
||||
|
||||
######################
|
||||
# FRONTEND CODE QUALITY
|
||||
######################
|
||||
|
||||
format_frontend: ## frontend code formatters
|
||||
@cd $(FRONTEND_DIR) && npm run format
|
||||
|
||||
######################
|
||||
# FRONTEND E2E TESTS (PLAYWRIGHT)
|
||||
######################
|
||||
|
||||
tests_frontend: ## run frontend tests
|
||||
ifeq ($(UI), true)
|
||||
@cd $(FRONTEND_DIR) && npx playwright test --ui --project=chromium
|
||||
else
|
||||
@cd $(FRONTEND_DIR) && npx playwright test --project=chromium
|
||||
endif
|
||||
|
||||
######################
|
||||
# FRONTEND UNIT TESTS (JEST)
|
||||
######################
|
||||
|
||||
# Run all frontend Jest unit tests
|
||||
test_frontend: frontend_deps_check ## run all frontend Jest unit tests
|
||||
@echo "Running all frontend Jest unit tests..."
|
||||
@cd $(FRONTEND_DIR) && $(NPM) test
|
||||
|
||||
# Run frontend tests in watch mode
|
||||
test_frontend_watch: frontend_deps_check ## run frontend tests in watch mode
|
||||
@echo "Running frontend tests in watch mode..."
|
||||
@cd $(FRONTEND_DIR) && $(NPM) run test:watch
|
||||
|
||||
# Run frontend tests with coverage report
|
||||
test_frontend_coverage: frontend_deps_check ## run frontend tests with coverage report
|
||||
@echo "Running frontend tests with coverage report..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --coverage
|
||||
|
||||
# Run frontend tests with verbose output
|
||||
test_frontend_verbose: frontend_deps_check ## run frontend tests with verbose output
|
||||
@echo "Running frontend tests with verbose output..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --verbose
|
||||
|
||||
# Run frontend tests in CI mode (no watch, with coverage)
|
||||
test_frontend_ci: frontend_deps_check ## run frontend tests in CI mode
|
||||
@echo "Running frontend tests in CI mode..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --ci --coverage --watchAll=false
|
||||
|
||||
# Clean test cache and run tests
|
||||
test_frontend_clean: frontend_deps_check ## clean test cache and run tests
|
||||
@echo "Cleaning Jest cache and running tests..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --clearCache && npx jest
|
||||
|
||||
# Run tests for a specific file
|
||||
test_frontend_file: frontend_deps_check ## run tests for a specific file (usage: make test_frontend_file path/to/test.ts)
|
||||
$(eval file := $(word 2,$(MAKECMDGOALS)))
|
||||
@if [ -z "$(file)" ]; then \
|
||||
echo "Usage: make test_frontend_file path/to/test.ts"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Running tests for file: $(file)"
|
||||
@cd $(FRONTEND_DIR) && npx jest $(file)
|
||||
|
||||
# Prevent make from treating the file argument as another target
|
||||
%:
|
||||
@:
|
||||
|
||||
# Run tests matching a pattern
|
||||
test_frontend_pattern: frontend_deps_check ## run tests matching a pattern (usage: make test_frontend_pattern pattern)
|
||||
$(eval pattern := $(word 2,$(MAKECMDGOALS)))
|
||||
@if [ -z "$(pattern)" ]; then \
|
||||
echo "Usage: make test_frontend_pattern pattern"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Running tests matching pattern: $(pattern)"
|
||||
@cd $(FRONTEND_DIR) && npx jest --testNamePattern="$(pattern)"
|
||||
|
||||
# Update test snapshots
|
||||
test_frontend_snapshots: frontend_deps_check ## update Jest snapshots
|
||||
@echo "Updating Jest snapshots..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --updateSnapshot
|
||||
|
||||
# Show test configuration
|
||||
test_frontend_config: ## show Jest configuration
|
||||
@echo "Jest configuration:"
|
||||
@cd $(FRONTEND_DIR) && npx jest --showConfig
|
||||
|
||||
# Run Jest tests with bail (stop on first failure)
|
||||
test_frontend_bail: frontend_deps_check ## run tests with bail (stop on first failure)
|
||||
@echo "Running Jest tests with bail (stop on first failure)..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --bail
|
||||
|
||||
# Run Jest tests silently (minimal output)
|
||||
test_frontend_silent: frontend_deps_check ## run tests silently (minimal output)
|
||||
@echo "Running Jest tests silently..."
|
||||
@cd $(FRONTEND_DIR) && npx jest --silent
|
||||
|
||||
# Run Jest tests and open coverage report in browser
|
||||
test_frontend_coverage_open: test_frontend_coverage ## run tests with coverage and open report in browser
|
||||
@echo "Opening coverage report in browser..."
|
||||
@if command -v open >/dev/null 2>&1; then \
|
||||
open $(FRONTEND_DIR)/coverage/lcov-report/index.html; \
|
||||
elif command -v xdg-open >/dev/null 2>&1; then \
|
||||
xdg-open $(FRONTEND_DIR)/coverage/lcov-report/index.html; \
|
||||
else \
|
||||
echo "Coverage report generated at: $(FRONTEND_DIR)/coverage/lcov-report/index.html"; \
|
||||
fi
|
||||
|
||||
######################
|
||||
# FRONTEND HELP
|
||||
######################
|
||||
|
||||
help_frontend: ## show frontend help
|
||||
@echo "Frontend Commands:"
|
||||
@echo ""
|
||||
@echo "Dependencies:"
|
||||
@echo " install_frontend - Install frontend dependencies"
|
||||
@echo " install_frontendci - Install frontend dependencies with npm ci"
|
||||
@echo " install_frontendc - Clean install frontend dependencies"
|
||||
@echo ""
|
||||
@echo "Build & Development:"
|
||||
@echo " build_frontend - Build frontend static files"
|
||||
@echo " run_frontend - Run the frontend development server"
|
||||
@echo " frontend - Install dependencies and run frontend in dev mode"
|
||||
@echo " frontendc - Clean install dependencies and run frontend"
|
||||
@echo ""
|
||||
@echo "Code Quality:"
|
||||
@echo " format_frontend - Format frontend code"
|
||||
@echo ""
|
||||
@echo "Testing:"
|
||||
@echo " tests_frontend - Run frontend Playwright e2e tests"
|
||||
@echo " test_frontend - Run frontend Jest unit tests"
|
||||
@echo " test_frontend_watch - Run unit tests in watch mode"
|
||||
@echo " test_frontend_coverage - Run unit tests with coverage"
|
||||
@echo " test_frontend_coverage_open - Run coverage and open report"
|
||||
@echo " test_frontend_verbose - Run unit tests with verbose output"
|
||||
@echo " test_frontend_ci - Run unit tests in CI mode"
|
||||
@echo " test_frontend_clean - Clean cache and run unit tests"
|
||||
@echo " test_frontend_bail - Run unit tests with bail"
|
||||
@echo " test_frontend_silent - Run unit tests silently"
|
||||
@echo ""
|
||||
@echo "Targeted Testing:"
|
||||
@echo " test_frontend_file path - Run tests for specific file"
|
||||
@echo " test_frontend_pattern pattern - Run tests matching pattern"
|
||||
@echo " test_frontend_snapshots - Update Jest snapshots"
|
||||
@echo " test_frontend_config - Show Jest configuration"
|
||||
20
src/frontend/jest.config.js
Normal file
20
src/frontend/jest.config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
injectGlobals: true,
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||
testMatch: [
|
||||
"<rootDir>/src/**/__tests__/**/*.{ts,tsx}",
|
||||
"<rootDir>/src/**/*.{test,spec}.{ts,tsx}",
|
||||
],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest",
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
|
||||
// Ignore node_modules except for packages that need transformation
|
||||
transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$|@testing-library))"],
|
||||
};
|
||||
4455
src/frontend/package-lock.json
generated
4455
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -93,6 +93,8 @@
|
|||
"dev:docker": "vite --host 0.0.0.0",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
|
||||
"check-format": "npx prettier --check \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
|
||||
|
|
@ -118,6 +120,7 @@
|
|||
},
|
||||
"proxy": "http://localhost:7860",
|
||||
"devDependencies": {
|
||||
"@jest/types": "^30.0.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@swc/cli": "^0.5.2",
|
||||
"@swc/core": "^1.6.1",
|
||||
|
|
@ -135,6 +138,8 @@
|
|||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.5.0",
|
||||
"jest": "^30.0.3",
|
||||
"jest-environment-jsdom": "^30.0.2",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
|
|
@ -142,8 +147,9 @@
|
|||
"simple-git-hooks": "^2.11.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss-dotted-background": "^1.1.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.4.5",
|
||||
"ua-parser-js": "^1.0.38",
|
||||
"vite": "^5.4.19"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { ProfileIcon } from "@/components/core/appHeaderComponent/components/ProfileIcon";
|
||||
import { ContentBlockDisplay } from "@/components/core/chatComponents/ContentBlockDisplay";
|
||||
import { useUpdateMessage } from "@/controllers/API/queries/messages";
|
||||
import { CustomProfileIcon } from "@/customization/components/custom-profile-icon";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { EMPTY_OUTPUT_SEND_MESSAGE } from "@/constants/constants";
|
||||
import { preprocessChatMessage } from "@/utils/markdownUtils";
|
||||
import { cn } from "@/utils/utils";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeMathjax from "rehype-mathjax";
|
||||
|
|
@ -14,14 +15,6 @@ type MarkdownFieldProps = {
|
|||
isAudioMessage?: boolean;
|
||||
};
|
||||
|
||||
// Function to replace <think> tags with a placeholder before markdown processing
|
||||
const preprocessChatMessage = (text: string): string => {
|
||||
// Replace <think> tags with `<span class="think-tag">think:</span>`
|
||||
return text
|
||||
.replace(/<think>/g, "`<think>`")
|
||||
.replace(/<\/think>/g, "`</think>`");
|
||||
};
|
||||
|
||||
export const MarkdownField = ({
|
||||
chat,
|
||||
isEmpty,
|
||||
|
|
@ -29,7 +22,7 @@ export const MarkdownField = ({
|
|||
editedFlag,
|
||||
isAudioMessage,
|
||||
}: MarkdownFieldProps) => {
|
||||
// Process the chat message to handle <think> tags
|
||||
// Process the chat message to handle <think> tags and clean up tables
|
||||
const processedChatMessage = preprocessChatMessage(chatMessage);
|
||||
|
||||
return (
|
||||
|
|
|
|||
62
src/frontend/src/setupTests.ts
Normal file
62
src/frontend/src/setupTests.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Jest setup file for testing environment
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
// Mock ResizeObserver if not available in test environment
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock IntersectionObserver if not available in test environment
|
||||
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock window.matchMedia for components that use it
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Suppress console.error and console.warn in tests unless explicitly needed
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
beforeAll(() => {
|
||||
console.error = (...args) => {
|
||||
if (
|
||||
typeof args[0] === "string" &&
|
||||
args[0].includes("Warning: ReactDOM.render is deprecated")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalError.call(console, ...args);
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
if (
|
||||
typeof args[0] === "string" &&
|
||||
args[0].includes("componentWillReceiveProps has been renamed")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalWarn.call(console, ...args);
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
215
src/frontend/src/utils/__tests__/markdownUtils.test.ts
Normal file
215
src/frontend/src/utils/__tests__/markdownUtils.test.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import {
|
||||
cleanupTableEmptyCells,
|
||||
isMarkdownTable,
|
||||
preprocessChatMessage,
|
||||
} from "../markdownUtils";
|
||||
|
||||
describe("markdownUtils", () => {
|
||||
describe("isMarkdownTable", () => {
|
||||
it("should return true for valid markdown table", () => {
|
||||
const table = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |`;
|
||||
expect(isMarkdownTable(table)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for table with alignment", () => {
|
||||
const table = `| Left | Center | Right |
|
||||
|:-----|:------:|------:|
|
||||
| L1 | C1 | R1 |`;
|
||||
expect(isMarkdownTable(table)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-table content", () => {
|
||||
expect(isMarkdownTable("Just some text")).toBe(false);
|
||||
expect(isMarkdownTable("# Header\nSome content")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for empty or null input", () => {
|
||||
expect(isMarkdownTable("")).toBe(false);
|
||||
expect(isMarkdownTable(" ")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for table without separator", () => {
|
||||
const invalidTable = `| Header 1 | Header 2 |
|
||||
| Cell 1 | Cell 2 |`;
|
||||
expect(isMarkdownTable(invalidTable)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for table with extra whitespace", () => {
|
||||
const table = ` | Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 | `;
|
||||
expect(isMarkdownTable(table)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanupTableEmptyCells", () => {
|
||||
it("should remove completely empty rows", () => {
|
||||
const table = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
| | |
|
||||
| Cell 3 | Cell 4 |`;
|
||||
|
||||
const expected = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
| Cell 3 | Cell 4 |`;
|
||||
|
||||
expect(cleanupTableEmptyCells(table)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should keep rows with at least one non-empty cell", () => {
|
||||
const table = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | |
|
||||
| | Cell 2 |`;
|
||||
|
||||
const expected = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | |
|
||||
| | Cell 2 |`;
|
||||
|
||||
expect(cleanupTableEmptyCells(table)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should preserve separator rows", () => {
|
||||
const table = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| | |`;
|
||||
|
||||
const expected = `| Header 1 | Header 2 |
|
||||
|----------|----------|`;
|
||||
|
||||
expect(cleanupTableEmptyCells(table)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle mixed content with tables", () => {
|
||||
const content = `Some text before
|
||||
|
||||
| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
| | |
|
||||
|
||||
Some text after`;
|
||||
|
||||
const expected = `Some text before
|
||||
|
||||
| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
|
||||
Some text after`;
|
||||
|
||||
expect(cleanupTableEmptyCells(content)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle non-table content unchanged", () => {
|
||||
const content = `# Header
|
||||
This is just regular text.
|
||||
No tables here.`;
|
||||
|
||||
expect(cleanupTableEmptyCells(content)).toBe(content);
|
||||
});
|
||||
|
||||
it("should handle empty input", () => {
|
||||
expect(cleanupTableEmptyCells("")).toBe("");
|
||||
expect(cleanupTableEmptyCells(" \n \n ")).toBe(" \n \n ");
|
||||
});
|
||||
|
||||
it("should handle table with alignment separators", () => {
|
||||
const table = `| Left | Center | Right |
|
||||
|:-----|:------:|------:|
|
||||
| L1 | C1 | R1 |
|
||||
| | | |`;
|
||||
|
||||
const expected = `| Left | Center | Right |
|
||||
|:-----|:------:|------:|
|
||||
| L1 | C1 | R1 |`;
|
||||
|
||||
expect(cleanupTableEmptyCells(table)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("preprocessChatMessage", () => {
|
||||
it("should replace <think> tags with backticks", () => {
|
||||
const message = "Before <think>thinking</think> after";
|
||||
const expected = "Before `<think>`thinking`</think>` after";
|
||||
expect(preprocessChatMessage(message)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle multiple <think> tags", () => {
|
||||
const message = "<think>first</think> and <think>second</think>";
|
||||
const expected = "`<think>`first`</think>` and `<think>`second`</think>`";
|
||||
expect(preprocessChatMessage(message)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should clean up tables when present", () => {
|
||||
const message = `<think>analyzing</think>
|
||||
|
||||
| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
| | |`;
|
||||
|
||||
const result = preprocessChatMessage(message);
|
||||
|
||||
// Should replace think tags
|
||||
expect(result).toContain("`<think>`analyzing`</think>`");
|
||||
|
||||
// Should remove empty table row
|
||||
expect(result).not.toContain("| | |");
|
||||
|
||||
// Should keep good content
|
||||
expect(result).toContain("| Cell 1 | Cell 2 |");
|
||||
});
|
||||
|
||||
it("should handle messages without tables", () => {
|
||||
const message = "<think>pondering</think> Just some regular text";
|
||||
const expected = "`<think>`pondering`</think>` Just some regular text";
|
||||
expect(preprocessChatMessage(message)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle messages without think tags", () => {
|
||||
const message = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
| | |`;
|
||||
|
||||
const result = preprocessChatMessage(message);
|
||||
expect(result).not.toContain("| | |");
|
||||
expect(result).toContain("| Cell 1 | Cell 2 |");
|
||||
});
|
||||
|
||||
it("should handle empty messages", () => {
|
||||
expect(preprocessChatMessage("")).toBe("");
|
||||
expect(preprocessChatMessage(" ")).toBe(" ");
|
||||
});
|
||||
|
||||
it("should handle complex nested scenarios", () => {
|
||||
const message = `<think>Let me create a table</think>
|
||||
|
||||
| Name | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| John | Active | Good |
|
||||
| | | |
|
||||
| Jane | Active | |
|
||||
|
||||
<think>Done</think>`;
|
||||
|
||||
const result = preprocessChatMessage(message);
|
||||
|
||||
// Think tags should be replaced
|
||||
expect(result).toContain("`<think>`Let me create a table`</think>`");
|
||||
expect(result).toContain("`<think>`Done`</think>`");
|
||||
|
||||
// Empty row should be removed
|
||||
expect(result).not.toContain("| | | |");
|
||||
|
||||
// Partial row should be kept
|
||||
expect(result).toContain("| Jane | Active | |");
|
||||
});
|
||||
});
|
||||
});
|
||||
52
src/frontend/src/utils/markdownUtils.ts
Normal file
52
src/frontend/src/utils/markdownUtils.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Utility functions for processing markdown content, particularly tables
|
||||
*/
|
||||
|
||||
/**
|
||||
* Detects if the given text contains a markdown table
|
||||
*/
|
||||
export const isMarkdownTable = (text: string): boolean => {
|
||||
if (!text?.trim()) return false;
|
||||
|
||||
// Single regex to detect markdown table with header separator
|
||||
return /\|.*\|.*\n\s*\|[\s\-:]+\|/m.test(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes completely empty rows from markdown tables
|
||||
*/
|
||||
export const cleanupTableEmptyCells = (text: string): string => {
|
||||
return text
|
||||
.split("\n")
|
||||
.filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Keep non-table lines
|
||||
if (!trimmed.includes("|")) return true;
|
||||
|
||||
// Keep separator rows (contain only |, -, :, spaces)
|
||||
if (/^\|[\s\-:]+\|$/.test(trimmed)) return true;
|
||||
|
||||
// For data rows, check if any cell has content
|
||||
const cells = trimmed.split("|").slice(1, -1); // Remove delimiter cells
|
||||
return cells.some((cell) => cell.trim() !== "");
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Preprocesses chat messages by handling <think> tags and cleaning up tables
|
||||
*/
|
||||
export const preprocessChatMessage = (text: string): string => {
|
||||
// Handle <think> tags
|
||||
let processed = text
|
||||
.replace(/<think>/g, "`<think>`")
|
||||
.replace(/<\/think>/g, "`</think>`");
|
||||
|
||||
// Clean up tables if present
|
||||
if (isMarkdownTable(processed)) {
|
||||
processed = cleanupTableEmptyCells(processed);
|
||||
}
|
||||
|
||||
return processed;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue