Skip to main content
Glama
push-based

Angular Toolkit MCP

by push-based
utils.spec.ts19.1 kB
import { extractClassNamesFromNgClassAST, ngClassContainsClass, ngClassesIncludeClassName, } from './utils'; import type { ASTWithSource, TmplAstElement, } from '@angular/compiler' with { 'resolution-mode': 'import' }; let parseTemplate: typeof import('@angular/compiler').parseTemplate; beforeAll(async () => { parseTemplate = (await import('@angular/compiler')).parseTemplate; }); function parseNgClassExpression(expression: string): ASTWithSource { const template = `<div [ngClass]="${expression}"></div>`; const result = parseTemplate(template, 'test.html'); const element = result.nodes[0] as TmplAstElement; const ngClassInput = element.inputs.find((input) => input.name === 'ngClass'); return ngClassInput?.value as ASTWithSource; } describe('extractClassNamesFromNgClassAST', () => { it('should extract classes from array literals', () => { const expression = "['btn', 'btn-primary', someVariable]"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'btn', 'btn-primary', 'other', ]); expect(result).toEqual(['btn', 'btn-primary']); }); it('should extract classes from object literals', () => { const expression = "{ 'btn': true, 'btn-primary': isActive, 'other-class': false }"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'btn', 'btn-primary', 'other-class', ]); expect(result).toEqual(['btn', 'btn-primary', 'other-class']); }); it('should extract classes from string literals', () => { const expression = "'btn btn-primary active'"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'btn', 'btn-primary', 'active', ]); expect(result).toEqual(['btn', 'btn-primary', 'active']); }); it('should extract classes from ternary expressions without false positives from conditions', () => { const expression = "option?.logo?.toLowerCase() === 'card' ? 'card-special' : 'card-normal'"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'card-special', 'card-normal', ]); expect(result).toEqual(['card-special', 'card-normal']); }); it('should not extract classes from binary comparison expressions', () => { const expression = "someValue === 'card' && otherValue !== 'btn'"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'btn', ]); expect(result).toEqual([]); }); it('should handle complex nested expressions', () => { const expression = "[option?.logo?.toLowerCase() === 'card' ? 'card-active' : '', 'base-class']"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'card-active', 'base-class', ]); expect(result).toEqual(['card-active', 'base-class']); }); it('should handle object with conditional values', () => { const expression = "{ 'card': option?.logo?.toLowerCase() === 'card', 'btn': isButton }"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'btn', ]); expect(result).toEqual(['card', 'btn']); }); it('should handle spaced class names in object keys', () => { const expression = "{ 'card special': true, 'btn primary': false }"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'special', 'btn', 'primary', ]); expect(result).toEqual(['card', 'special', 'btn', 'primary']); }); it('should return empty array for non-matching classes', () => { const expression = "['other', 'different']"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'btn', ]); expect(result).toEqual([]); }); it('should remove duplicates', () => { const expression = "['card', 'card', 'btn']"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'btn', ]); expect(result).toEqual(['card', 'btn']); }); }); describe('ngClassContainsClass', () => { it('should return true when class is found in AST', () => { const expression = "['btn', 'btn-primary']"; const astWithSource = parseNgClassExpression(expression); expect(ngClassContainsClass(astWithSource, 'btn')).toBe(true); expect(ngClassContainsClass(astWithSource, 'btn-primary')).toBe(true); }); it('should return false when class is not found in AST', () => { const expression = "['other', 'different']"; const astWithSource = parseNgClassExpression(expression); expect(ngClassContainsClass(astWithSource, 'btn')).toBe(false); }); it('should return false for classes in comparisons', () => { const expression = "option?.logo?.toLowerCase() === 'card' ? 'active' : 'inactive'"; const astWithSource = parseNgClassExpression(expression); // Should not find 'card' since it's in a comparison, not a class assignment expect(ngClassContainsClass(astWithSource, 'card')).toBe(false); expect(ngClassContainsClass(astWithSource, 'active')).toBe(true); }); }); describe('AST-based ngClass parsing integration tests', () => { it('should handle the original problem case correctly', () => { const expression = "[option?.logo?.toLowerCase(), option?.logo?.toLowerCase() == 'card' ? mergedCardLogoClass : '']"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, ['card']); // Should NOT find 'card' from the comparison expect(result).toEqual([]); }); it('should find card when actually used as a class', () => { const expression = "['card', 'other-class']"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, ['card']); expect(result).toEqual(['card']); }); it('should handle complex real-world scenarios', () => { const expression = "{ 'card': isCard, 'btn': isButton, 'active': option?.logo?.toLowerCase() === 'card' }"; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'btn', ]); expect(result).toEqual(['card', 'btn']); }); }); describe('AST-based class binding parsing', () => { function parseClassExpression(expression: string): ASTWithSource { const template = `<div [class]="${expression}"></div>`; const result = parseTemplate(template, 'test.html'); const element = result.nodes[0] as TmplAstElement; const classInput = element.inputs.find((input) => input.name === 'class'); return classInput?.value as ASTWithSource; } it('should work the same for [class] bindings with false positive avoidance', () => { const expression = "option?.logo?.toLowerCase() == 'card' ? 'card-special' : 'other'"; const astWithSource = parseClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'card-special', ]); expect(result).toEqual(['card-special']); }); it('should handle simple string literals in [class] bindings', () => { const expression = "'card btn-primary'"; const astWithSource = parseClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'btn-primary', 'other', ]); expect(result).toEqual(['card', 'btn-primary']); }); it('should handle ternary expressions in [class] bindings', () => { const expression = "isActive ? 'card active' : 'card inactive'"; const astWithSource = parseClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'active', 'inactive', ]); expect(result).toEqual(['card', 'active', 'inactive']); }); }); describe('AST-based interpolated class parsing', () => { function parseInterpolatedClassExpression(classValue: string): ASTWithSource { const template = `<div class="${classValue}"></div>`; const result = parseTemplate(template, 'test.html'); const element = result.nodes[0] as TmplAstElement; const classInput = element.inputs.find((input) => input.name === 'class'); return classInput?.value as ASTWithSource; } it('should extract classes from interpolated class attributes', () => { const classValue = 'count count-{{ size() }} other-class'; const astWithSource = parseInterpolatedClassExpression(classValue); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'count', 'other-class', 'different', ]); expect(result).toEqual(['count', 'other-class']); }); it('should handle interpolation with multiple dynamic parts', () => { const classValue = 'prefix {{ type }} middle {{ state }} suffix'; const astWithSource = parseInterpolatedClassExpression(classValue); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'prefix', 'middle', 'suffix', ]); expect(result).toEqual(['prefix', 'middle', 'suffix']); }); it('should handle interpolation with only static classes', () => { const classValue = 'btn btn-primary active'; const astWithSource = parseInterpolatedClassExpression(classValue); // Static class attributes without interpolation don't create class inputs // They are handled as static attributes, not dynamic bindings expect(astWithSource).toBeUndefined(); }); it('should avoid false positives from dynamic expressions in interpolation', () => { const classValue = "base-class {{ condition ? 'card' : 'other' }}"; const astWithSource = parseInterpolatedClassExpression(classValue); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'base-class', 'card', 'other', ]); expect(result).toEqual(['base-class']); }); it('should handle the exact failing test case', () => { const classValue = 'count count-{{ size() }} other-class'; const astWithSource = parseInterpolatedClassExpression(classValue); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'count', ]); expect(result).toEqual(['count']); }); }); describe('Real-world ngClass patterns from monorepo', () => { it('should handle complex object-style ngClass with multiple conditions', () => { // Based on promo filter popup component const expression = `{ 'disabled': selectedFilterIndexes?.totalCount === 0, 'pill-with-badge-v2': isOfferPageEnhanced, 'ml-2': true, 'promo-filter-apply': isOfferPageEnhanced }`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'disabled', 'pill-with-badge-v2', 'ml-2', 'promo-filter-apply', 'btn-primary', ]); expect(result).toEqual([ 'disabled', 'pill-with-badge-v2', 'ml-2', 'promo-filter-apply', ]); }); it('should handle dark mode conditional classes', () => { const expression = `{ 'bg-body-10': !darkModeService.isEnabled, 'dark-mode-background': darkModeService.isEnabled }`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'bg-body-10', 'dark-mode-background', 'other-class', ]); expect(result).toEqual(['bg-body-10', 'dark-mode-background']); }); it('should handle simple boolean-based ngClass', () => { const expression = `{ 'offers-navigation-enable-v1': subNavigationV1Enabled }`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'offers-navigation-enable-v1', 'other-class', ]); expect(result).toEqual(['offers-navigation-enable-v1']); }); it('should handle responsive class conditions', () => { const expression = `{ 'mobile-card-middle': isMobile, 'btn-sm': isSmallScreen, 'form-width-75': !isMobile }`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'mobile-card-middle', 'btn-sm', 'form-width-75', ]); expect(result).toEqual(['mobile-card-middle', 'btn-sm', 'form-width-75']); }); it('should handle property-based class binding', () => { const expression = `filterByProduct?.messages?.producticonclass`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'icon-class', 'ui-icon', ]); expect(result).toEqual([]); }); it('should avoid false positives in complex comparison expressions', () => { const expression = `{ 'disabled': filtersByOffer?.count === 0, 'badge-counter': selectedFilterIndexes?.totalCount > 0, 'active': selectedFilters && selectedFilters.length > 0 }`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'disabled', 'badge-counter', 'active', 'count', 'selectedFilters', ]); expect(result).toEqual(['disabled', 'badge-counter', 'active']); }); it('should handle mixed static and conditional classes', () => { const expression = `[ 'navbar', 'navbar-expand-sm', condition ? 'sub-nav-wrapper' : '', menuContainer?.class ]`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'navbar', 'navbar-expand-sm', 'sub-nav-wrapper', 'other-class', ]); expect(result).toEqual(['navbar', 'navbar-expand-sm', 'sub-nav-wrapper']); }); it('should handle the exact problematic pattern from the original issue', () => { // The exact pattern that was causing false positives const expression = `[ option?.logo?.toLowerCase(), option?.logo?.toLowerCase() == 'card' ? mergedCardLogoClass : '', 'base-class' ]`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'card', 'base-class', 'other', ]); expect(result).toEqual(['base-class']); }); it('should handle nested object with spaced class names', () => { // Based on filter popup patterns const expression = `{ 'filter-product-icon ui-icon': true, 'ui-icon-size-lg': iconSize === 'large', 'status-pills transparent': hasTransparentStyle }`; const astWithSource = parseNgClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'filter-product-icon', 'ui-icon', 'ui-icon-size-lg', 'status-pills', 'transparent', 'large', ]); expect(result).toEqual([ 'filter-product-icon', 'ui-icon', 'ui-icon-size-lg', 'status-pills', 'transparent', ]); }); }); describe('Real-world [class] binding patterns', () => { // Helper function to extract AST from [class] expression function parseClassExpression(expression: string): ASTWithSource { const template = `<div [class]="${expression}"></div>`; const result = parseTemplate(template, 'test.html'); const element = result.nodes[0] as TmplAstElement; const classInput = element.inputs.find((input) => input.name === 'class'); return classInput?.value as ASTWithSource; } it('should handle conditional class strings', () => { const expression = `isActive ? 'nav-item active' : 'nav-item'`; const astWithSource = parseClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'nav-item', 'active', 'inactive', ]); expect(result).toEqual(['nav-item', 'active']); }); it('should handle complex ternary with comparison false positive potential', () => { const expression = `userType === 'admin' ? 'admin-panel active' : userType === 'user' ? 'user-panel' : 'guest-panel'`; const astWithSource = parseClassExpression(expression); const result = extractClassNamesFromNgClassAST(astWithSource.ast, [ 'admin', 'admin-panel', 'active', 'user', 'user-panel', 'guest-panel', ]); expect(result).toEqual([ 'admin-panel', 'active', 'user-panel', 'guest-panel', ]); }); }); describe('Backward compatibility - ngClassesIncludeClassName', () => { it('should work with object expressions', () => { const expression = `{ 'btn': true, 'btn-primary': isActive }`; expect(ngClassesIncludeClassName(expression, 'btn')).toBe(true); expect(ngClassesIncludeClassName(expression, 'btn-primary')).toBe(true); expect(ngClassesIncludeClassName(expression, 'btn-secondary')).toBe(false); }); it('should work with array expressions', () => { const expression = `['btn', 'btn-primary', condition ? 'active' : '']`; expect(ngClassesIncludeClassName(expression, 'btn')).toBe(true); expect(ngClassesIncludeClassName(expression, 'btn-primary')).toBe(true); expect(ngClassesIncludeClassName(expression, 'active')).toBe(true); expect(ngClassesIncludeClassName(expression, 'inactive')).toBe(false); }); it('should work with string expressions', () => { const expression = `'btn btn-primary active'`; expect(ngClassesIncludeClassName(expression, 'btn')).toBe(true); expect(ngClassesIncludeClassName(expression, 'btn-primary')).toBe(true); expect(ngClassesIncludeClassName(expression, 'active')).toBe(true); expect(ngClassesIncludeClassName(expression, 'inactive')).toBe(false); }); it('should handle basic string matching', () => { const expression = `{ 'btn': option?.logo?.toLowerCase() === 'card', 'active': true }`; expect(ngClassesIncludeClassName(expression, 'btn')).toBe(true); expect(ngClassesIncludeClassName(expression, 'active')).toBe(true); }); it('should handle missing classes', () => { const expression = `{ 'other': true }`; expect(ngClassesIncludeClassName(expression, 'btn')).toBe(false); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/push-based/angular-toolkit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server