This commit is contained in:
Michał Sieciechowicz 2026-01-18 21:29:07 +01:00
parent c3dda73d3a
commit ba47312f55
13 changed files with 898 additions and 11 deletions

169
PIPE_IMPLEMENTATION_FIX.md Normal file
View File

@ -0,0 +1,169 @@
# Naprawa implementacji Pipe - Problem z operatorem ||
## Problem
Po dodaniu obsługi pipes do frameworka Quarc, komponent devices w `/web/IoT/Ant` przestał renderować zawartość.
### Przyczyna
Implementacja `transformPipeExpression` w `template-transformer.ts` błędnie traktowała operator logiczny `||` jako separator pipe `|`.
Wyrażenie:
```typescript
{{ device.name || 'Unnamed' }}
```
Było transformowane na:
```typescript
this._pipes?.['']?.transform(this._pipes?.['Unnamed']?.transform(device.name))
```
Zamiast pozostać jako:
```typescript
device.name || 'Unnamed'
```
## Rozwiązanie
Dodano metodę `splitByPipe()` która poprawnie rozróżnia:
- Pojedynczy `|` - separator pipe
- Podwójny `||` - operator logiczny OR
- Podwójny `&&` - operator logiczny AND
### Zmieniony plik
**`/web/quarc/cli/processors/template/template-transformer.ts`**
```typescript
private transformPipeExpression(expression: string): string {
const parts = this.splitByPipe(expression);
if (parts.length === 1) {
return expression;
}
let result = parts[0].trim();
for (let i = 1; i < parts.length; i++) {
const pipePart = parts[i].trim();
const colonIndex = pipePart.indexOf(':');
if (colonIndex === -1) {
const pipeName = pipePart.trim();
result = `this._pipes?.['${pipeName}']?.transform(${result})`;
} else {
const pipeName = pipePart.substring(0, colonIndex).trim();
const argsStr = pipePart.substring(colonIndex + 1).trim();
const args = argsStr.split(':').map(arg => arg.trim());
const argsJoined = args.join(', ');
result = `this._pipes?.['${pipeName}']?.transform(${result}, ${argsJoined})`;
}
}
return result;
}
private splitByPipe(expression: string): string[] {
const parts: string[] = [];
let current = '';
let i = 0;
while (i < expression.length) {
const char = expression[i];
if (char === '|') {
if (i + 1 < expression.length && expression[i + 1] === '|') {
// To jest || (operator logiczny), nie pipe
current += '||';
i += 2;
} else {
// To jest | (separator pipe)
parts.push(current);
current = '';
i++;
}
} else {
current += char;
i++;
}
}
if (current) {
parts.push(current);
}
return parts.length > 0 ? parts : [expression];
}
```
## Testy
Utworzono testy w `/web/quarc/tests/unit/`:
1. **test-for-transformation.ts** - Weryfikuje transformację `@for` do `*ngFor`
2. **test-interpolation-transformation.ts** - Weryfikuje transformację interpolacji `{{ }}`
3. **test-pipe-with-logical-operators.ts** - Weryfikuje rozróżnienie pipe `|` od operatorów `||` i `&&`
### Wyniki testów
```
@FOR TRANSFORMATION TEST PASSED (6/6)
✅ INTERPOLATION TRANSFORMATION TEST PASSED (8/8)
✅ PIPE VS LOGICAL OPERATORS TEST PASSED (7/7)
```
## Przykłady działania
### Operator || (poprawnie zachowany)
```typescript
// Input
{{ device.name || 'Unnamed' }}
// Output
<span [inner-text]="device.name || 'Unnamed'"></span>
```
### Prawdziwy pipe (poprawnie transformowany)
```typescript
// Input
{{ value | uppercase }}
// Output
<span [inner-text]="this._pipes?.['uppercase']?.transform(value)"></span>
```
### Kombinacja || i pipe
```typescript
// Input
{{ (value || 'default') | uppercase }}
// Output
<span [inner-text]="this._pipes?.['uppercase']?.transform((value || 'default'))"></span>
```
### Łańcuch pipes
```typescript
// Input
{{ value | lowercase | slice:0:5 }}
// Output
<span [inner-text]="this._pipes?.['slice']?.transform(this._pipes?.['lowercase']?.transform(value), 0, 5)"></span>
```
## Weryfikacja
Build aplikacji IoT/Ant przechodzi pomyślnie:
```bash
cd /web/IoT/Ant/assets/resources/quarc
npm run build
# ✅ Build completed | Environment: production
```
## Wpływ na istniejący kod
Naprawa jest **wstecznie kompatybilna** - nie zmienia zachowania dla:
- Prostych interpolacji bez operatorów logicznych
- Istniejących pipes
- Transformacji `@for` i `@if`
Naprawia tylko błędną interpretację operatorów logicznych jako separatorów pipe.

View File

@ -40,18 +40,24 @@ export class DirectiveCollectorProcessor extends BaseProcessor {
} }
const importsContent = importsMatch[1]; const importsContent = importsMatch[1];
const importNames = this.parseImportNames(importsContent); const { directives, pipes } = this.categorizeImports(importsContent, source);
if (importNames.length === 0) { let insert = '';
continue;
if (directives.length > 0) {
insert += `\n static _quarcDirectives = [${directives.join(', ')}];`;
} }
const directivesProperty = `\n static _quarcDirectives = [${importNames.join(', ')}];`; if (pipes.length > 0) {
insert += `\n static _quarcPipes = [${pipes.join(', ')}];`;
}
replacements.push({ if (insert) {
position: scopeIdEnd, replacements.push({
insert: directivesProperty, position: scopeIdEnd,
}); insert,
});
}
} }
for (let i = replacements.length - 1; i >= 0; i--) { for (let i = replacements.length - 1; i >= 0; i--) {
@ -63,6 +69,36 @@ export class DirectiveCollectorProcessor extends BaseProcessor {
return modified ? this.changed(source) : this.noChange(source); return modified ? this.changed(source) : this.noChange(source);
} }
private categorizeImports(importsContent: string, source: string): { directives: string[]; pipes: string[] } {
const importNames = this.parseImportNames(importsContent);
const directives: string[] = [];
const pipes: string[] = [];
for (const name of importNames) {
if (this.isPipe(name, source)) {
pipes.push(name);
} else {
directives.push(name);
}
}
return { directives, pipes };
}
private isPipe(className: string, source: string): boolean {
const classPattern = new RegExp(`class\\s+${className}\\s*(?:extends|implements|\\{)`);
const classMatch = source.match(classPattern);
if (!classMatch) {
return false;
}
const beforeClass = source.substring(0, classMatch.index!);
const pipeDecoratorPattern = new RegExp(`static\\s+_quarcPipe\\s*=.*?${className}`, 's');
return pipeDecoratorPattern.test(source);
}
private parseImportNames(importsContent: string): string[] { private parseImportNames(importsContent: string): string[] {
return importsContent return importsContent
.split(',') .split(',')

View File

@ -48,7 +48,8 @@ export class TemplateTransformer {
parts.push(`'${literal}'`); parts.push(`'${literal}'`);
} }
} }
parts.push(`(${match[1].trim()})`); const transformedExpr = this.transformPipeExpression(match[1].trim());
parts.push(`(${transformedExpr})`);
lastIndex = exprRegex.lastIndex; lastIndex = exprRegex.lastIndex;
} }
@ -80,10 +81,71 @@ export class TemplateTransformer {
private transformContentInterpolation(content: string): string { private transformContentInterpolation(content: string): string {
return content.replace( return content.replace(
/\{\{\s*([^}]+?)\s*\}\}/g, /\{\{\s*([^}]+?)\s*\}\}/g,
(_, expr) => `<span [innerText]="${expr.trim()}"></span>`, (_, expr) => {
const transformedExpr = this.transformPipeExpression(expr.trim());
return `<span [innerText]="${transformedExpr}"></span>`;
},
); );
} }
private transformPipeExpression(expression: string): string {
const parts = this.splitByPipe(expression);
if (parts.length === 1) {
return expression;
}
let result = parts[0].trim();
for (let i = 1; i < parts.length; i++) {
const pipePart = parts[i].trim();
const colonIndex = pipePart.indexOf(':');
if (colonIndex === -1) {
const pipeName = pipePart.trim();
result = `this._pipes?.['${pipeName}']?.transform(${result})`;
} else {
const pipeName = pipePart.substring(0, colonIndex).trim();
const argsStr = pipePart.substring(colonIndex + 1).trim();
const args = argsStr.split(':').map(arg => arg.trim());
const argsJoined = args.join(', ');
result = `this._pipes?.['${pipeName}']?.transform(${result}, ${argsJoined})`;
}
}
return result;
}
private splitByPipe(expression: string): string[] {
const parts: string[] = [];
let current = '';
let i = 0;
while (i < expression.length) {
const char = expression[i];
if (char === '|') {
if (i + 1 < expression.length && expression[i + 1] === '|') {
current += '||';
i += 2;
} else {
parts.push(current);
current = '';
i++;
}
} else {
current += char;
i++;
}
}
if (current) {
parts.push(current);
}
return parts.length > 0 ? parts : [expression];
}
transformControlFlowIf(content: string): string { transformControlFlowIf(content: string): string {
return this.controlFlowTransformer.transform(content); return this.controlFlowTransformer.transform(content);
} }

View File

@ -10,6 +10,14 @@ export interface PipeOptions {
pure?: boolean; pure?: boolean;
} }
/**
* Interfejs dla pipe transformacji.
* Każdy pipe musi implementować metodę transform.
*/
export interface PipeTransform {
transform(value: any, ...args: any[]): any;
}
/** /**
* Dekorator pipe. * Dekorator pipe.
* *

View File

@ -11,11 +11,12 @@ export { WebComponent } from "./module/web-component";
export { WebComponentFactory } from "./module/web-component-factory"; export { WebComponentFactory } from "./module/web-component-factory";
export { DirectiveRegistry } from "./module/directive-registry"; export { DirectiveRegistry } from "./module/directive-registry";
export { DirectiveRunner, DirectiveInstance } from "./module/directive-runner"; export { DirectiveRunner, DirectiveInstance } from "./module/directive-runner";
export { PipeRegistry } from "./module/pipe-registry";
// Decorators // Decorators
export { Component, ComponentOptions } from "./angular/component"; export { Component, ComponentOptions } from "./angular/component";
export { Directive, DirectiveOptions, IDirective } from "./angular/directive"; export { Directive, DirectiveOptions, IDirective } from "./angular/directive";
export { Pipe, PipeOptions } from "./angular/pipe"; export { Pipe, PipeOptions, PipeTransform } from "./angular/pipe";
export { Injectable, InjectableOptions } from "./angular/injectable"; export { Injectable, InjectableOptions } from "./angular/injectable";
export { Input, input, createInput, createRequiredInput } from "./angular/input"; export { Input, input, createInput, createRequiredInput } from "./angular/input";
export type { InputSignal, InputOptions } from "./angular/input"; export type { InputSignal, InputOptions } from "./angular/input";

View File

@ -0,0 +1,46 @@
import { Type } from './type';
export interface PipeMetadata {
name: string;
pure: boolean;
}
export class PipeRegistry {
private static instance: PipeRegistry;
private pipes = new Map<string, Type<any>>();
private pipeMetadata = new Map<Type<any>, PipeMetadata>();
private constructor() {}
static get(): PipeRegistry {
if (!PipeRegistry.instance) {
PipeRegistry.instance = new PipeRegistry();
}
return PipeRegistry.instance;
}
register(pipeType: Type<any>): void {
const metadata = (pipeType as any)._quarcPipe?.[0];
if (!metadata) {
return;
}
const pipeName = metadata.name;
const pure = metadata.pure !== false;
this.pipes.set(pipeName, pipeType);
this.pipeMetadata.set(pipeType, { name: pipeName, pure });
}
getPipe(name: string): Type<any> | undefined {
return this.pipes.get(name);
}
getPipeMetadata(pipeType: Type<any>): PipeMetadata | undefined {
return this.pipeMetadata.get(pipeType);
}
getAllPipes(): Map<string, Type<any>> {
return new Map(this.pipes);
}
}

View File

@ -7,6 +7,7 @@ export interface Type<T> {
export interface ComponentType<T> extends Type<T> { export interface ComponentType<T> extends Type<T> {
_quarcComponent: [ComponentOptions]; _quarcComponent: [ComponentOptions];
_quarcDirectives?: DirectiveType<any>[]; _quarcDirectives?: DirectiveType<any>[];
_quarcPipes?: Type<any>[];
_scopeId: string; _scopeId: string;
} }

View File

@ -8,6 +8,7 @@ import {
DirectiveInstance, DirectiveInstance,
effect, effect,
EffectRef, EffectRef,
PipeRegistry,
} from '../index'; } from '../index';
interface QuarcScopeRegistry { interface QuarcScopeRegistry {
@ -107,10 +108,31 @@ export class WebComponent extends HTMLElement {
this.setAttribute(`_nghost-${this.runtimeScopeId}`, ''); this.setAttribute(`_nghost-${this.runtimeScopeId}`, '');
} }
this.initializePipes();
this._initialized = true; this._initialized = true;
this.renderComponent(); this.renderComponent();
} }
private initializePipes(): void {
if (!this.componentInstance || !this.componentType) return;
const pipes = this.componentType._quarcPipes || [];
const pipeRegistry = PipeRegistry.get();
const pipeInstances: Record<string, any> = {};
for (const pipeType of pipes) {
pipeRegistry.register(pipeType);
const metadata = pipeRegistry.getPipeMetadata(pipeType);
if (metadata) {
const pipeInstance = new pipeType();
pipeInstances[metadata.name] = pipeInstance;
}
}
(this.componentInstance as any)._pipes = pipeInstances;
}
renderComponent(): void { renderComponent(): void {
if (!this.componentInstance || !this.componentType) return; if (!this.componentInstance || !this.componentType) return;

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test: Devices Component</title>
<style>
body {
font-family: monospace;
padding: 20px;
background: #1e1e1e;
color: #d4d4d4;
}
.device.card {
border: 1px solid #444;
padding: 10px;
margin: 10px 0;
display: flex;
gap: 10px;
}
.footer {
margin-top: 20px;
padding: 10px;
border-top: 1px solid #444;
}
#test-output {
background: #252526;
padding: 15px;
margin-top: 20px;
border: 1px solid #444;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Test: Devices Component Rendering</h1>
<div id="app-container"></div>
<div id="test-output"></div>
<script type="module">
import { runDevicesComponentTests } from './compiled/test-devices-component.js';
const output = document.getElementById('test-output');
const originalLog = console.log;
const originalError = console.error;
console.log = (...args) => {
originalLog(...args);
output.textContent += args.join(' ') + '\n';
};
console.error = (...args) => {
originalError(...args);
output.textContent += 'ERROR: ' + args.join(' ') + '\n';
};
runDevicesComponentTests();
</script>
</body>
</html>

View File

@ -0,0 +1,161 @@
import { Component, signal, OnInit } from "../../core/index";
import { bootstrapApplication } from "../../platform-browser/browser";
// Symulacja IconComponent
@Component({
selector: 'test-icon',
template: '<span>Icon</span>',
})
class TestIconComponent {}
// Symulacja Device interface
interface Device {
address: string;
name: string;
offline: boolean;
}
// Reprodukcja komponentu DevicesComponent z /web/IoT/Ant
@Component({
selector: 'test-devices',
template: `
<div class="content">
@for (device of devices(); track device.address) {
<div class="device card" (click)="openDevice(device.address)">
<div class="icon">
<test-icon></test-icon>
</div>
<div class="content">
<div class="name">{{ device.name || 'Unnamed' }}</div>
<div class="address">{{ device.address }}</div>
</div>
</div>
}
</div>
<div class="footer">
Devices: <span>{{ deviceCount() }}</span>
</div>
`,
imports: [TestIconComponent],
})
class TestDevicesComponent implements OnInit {
public devices = signal<Device[]>([]);
public deviceCount = signal(0);
ngOnInit(): void {
this.loadDevices();
}
private loadDevices(): void {
const mockDevices: Device[] = [
{ address: '192.168.1.1', name: 'Device 1', offline: false },
{ address: '192.168.1.2', name: 'Device 2', offline: false },
{ address: '192.168.1.3', name: 'Device 3', offline: true },
];
this.devices.set(mockDevices);
this.deviceCount.set(mockDevices.length);
}
public openDevice(address: string): void {
console.log('Opening device:', address);
}
}
// Root component
@Component({
selector: 'test-app',
template: '<test-devices></test-devices>',
imports: [TestDevicesComponent],
})
class TestAppComponent {}
// Test suite
export function runDevicesComponentTests() {
console.log('\n=== Test: Devices Component Rendering ===\n');
const container = document.createElement('div');
container.id = 'test-container';
document.body.appendChild(container);
bootstrapApplication(TestAppComponent, {
providers: [],
});
setTimeout(() => {
const appElement = document.querySelector('test-app');
console.log('App element:', appElement);
console.log('App element HTML:', appElement?.innerHTML);
const devicesElement = document.querySelector('test-devices');
console.log('\nDevices element:', devicesElement);
console.log('Devices element HTML:', devicesElement?.innerHTML);
const contentDiv = document.querySelector('.content');
console.log('\nContent div:', contentDiv);
console.log('Content div HTML:', contentDiv?.innerHTML);
const deviceCards = document.querySelectorAll('.device.card');
console.log('\nDevice cards found:', deviceCards.length);
const footerDiv = document.querySelector('.footer');
console.log('Footer div:', footerDiv);
console.log('Footer text:', footerDiv?.textContent);
// Testy
const tests = {
'App element exists': !!appElement,
'Devices element exists': !!devicesElement,
'Content div exists': !!contentDiv,
'Device cards rendered': deviceCards.length === 3,
'Footer exists': !!footerDiv,
'Footer shows count': footerDiv?.textContent?.includes('3'),
};
console.log('\n=== Test Results ===');
let passed = 0;
let failed = 0;
Object.entries(tests).forEach(([name, result]) => {
const status = result ? '✓ PASS' : '✗ FAIL';
console.log(`${status}: ${name}`);
if (result) passed++;
else failed++;
});
console.log(`\nTotal: ${passed} passed, ${failed} failed`);
// Sprawdzenie czy template został przetworzony
const componentInstance = (devicesElement as any)?.componentInstance;
if (componentInstance) {
console.log('\n=== Component State ===');
console.log('devices():', componentInstance.devices());
console.log('deviceCount():', componentInstance.deviceCount());
}
// Sprawdzenie czy @for został przekształcony
const componentType = (devicesElement as any)?.componentType;
if (componentType) {
const template = componentType._quarcComponent?.[0]?.template;
console.log('\n=== Transformed Template ===');
console.log(template);
if (template) {
const hasNgFor = template.includes('*ngFor');
const hasNgContainer = template.includes('ng-container');
console.log('\nTemplate transformation check:');
console.log(' Contains *ngFor:', hasNgFor);
console.log(' Contains ng-container:', hasNgContainer);
console.log(' @for was transformed:', hasNgFor && hasNgContainer);
}
}
if (failed > 0) {
console.error('\n❌ DEVICES COMPONENT TEST FAILED - Component nie renderuje contentu');
} else {
console.log('\n✅ DEVICES COMPONENT TEST PASSED');
}
document.body.removeChild(container);
}, 500);
}

View File

@ -0,0 +1,100 @@
/**
* Test transformacji @for do *ngFor
* Reprodukuje problem z komponentu devices z /web/IoT/Ant
*/
import { ControlFlowTransformer } from '../../cli/helpers/control-flow-transformer';
console.log('\n=== Test: @for Transformation ===\n');
const transformer = new ControlFlowTransformer();
// Test 1: Prosty @for jak w devices component
const template1 = `
<div class="content">
@for (device of devices(); track device.address) {
<div class="device card">
<div class="name">{{ device.name }}</div>
</div>
}
</div>
`;
console.log('Test 1: Prosty @for z track');
console.log('Input:', template1);
const result1 = transformer.transform(template1);
console.log('Output:', result1);
console.log('Contains *ngFor:', result1.includes('*ngFor'));
console.log('Contains ng-container:', result1.includes('ng-container'));
// Test 2: @for z wywołaniem funkcji (devices())
const template2 = `@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}`;
console.log('\n\nTest 2: @for z wywołaniem funkcji');
console.log('Input:', template2);
const result2 = transformer.transform(template2);
console.log('Output:', result2);
// Test 3: Zagnieżdżony @for
const template3 = `
@for (device of devices(); track device.address) {
<div>
@for (sensor of device.sensors; track sensor.id) {
<span>{{ sensor.name }}</span>
}
</div>
}
`;
console.log('\n\nTest 3: Zagnieżdżony @for');
console.log('Input:', template3);
const result3 = transformer.transform(template3);
console.log('Output:', result3);
// Test 4: @for z interpolacją w środku
const template4 = `
@for (device of devices(); track device.address) {
<div class="device">
<div>{{ device.name || 'Unnamed' }}</div>
<div>{{ device.address }}</div>
</div>
}
`;
console.log('\n\nTest 4: @for z interpolacją');
console.log('Input:', template4);
const result4 = transformer.transform(template4);
console.log('Output:', result4);
// Sprawdzenie czy wszystkie transformacje zawierają wymagane elementy
const tests = [
{ name: 'Test 1: *ngFor exists', pass: result1.includes('*ngFor') },
{ name: 'Test 1: ng-container exists', pass: result1.includes('ng-container') },
{ name: 'Test 1: track preserved', pass: result1.includes('track') || result1.includes('trackBy') },
{ name: 'Test 2: *ngFor exists', pass: result2.includes('*ngFor') },
{ name: 'Test 3: nested *ngFor exists', pass: (result3.match(/\*ngFor/g) || []).length === 2 },
{ name: 'Test 4: interpolation preserved', pass: result4.includes('device.name') },
];
console.log('\n\n=== Test Results ===');
let passed = 0;
let failed = 0;
tests.forEach(test => {
const status = test.pass ? '✓ PASS' : '✗ FAIL';
console.log(`${status}: ${test.name}`);
if (test.pass) passed++;
else failed++;
});
console.log(`\nTotal: ${passed} passed, ${failed} failed`);
if (failed > 0) {
console.error('\n❌ @FOR TRANSFORMATION TEST FAILED');
process.exit(1);
} else {
console.log('\n✅ @FOR TRANSFORMATION TEST PASSED');
process.exit(0);
}

View File

@ -0,0 +1,133 @@
/**
* Test transformacji interpolacji {{ }}
* Sprawdza czy interpolacja działa poprawnie po dodaniu obsługi pipes
*/
import { TemplateTransformer } from '../../cli/processors/template/template-transformer';
console.log('\n=== Test: Interpolation Transformation ===\n');
const transformer = new TemplateTransformer();
// Test 1: Prosta interpolacja bez pipes
const template1 = `<div>{{ device.name }}</div>`;
console.log('Test 1: Prosta interpolacja');
console.log('Input:', template1);
const result1 = transformer.transformInterpolation(template1);
console.log('Output:', result1);
console.log('Contains [innerText]:', result1.includes('[innerText]'));
// Test 2: Interpolacja z operatorem ||
const template2 = `<div>{{ device.name || 'Unnamed' }}</div>`;
console.log('\n\nTest 2: Interpolacja z operatorem ||');
console.log('Input:', template2);
const result2 = transformer.transformInterpolation(template2);
console.log('Output:', result2);
// Test 3: Interpolacja z wywołaniem funkcji
const template3 = `<div>{{ deviceCount() }}</div>`;
console.log('\n\nTest 3: Interpolacja z wywołaniem funkcji');
console.log('Input:', template3);
const result3 = transformer.transformInterpolation(template3);
console.log('Output:', result3);
// Test 4: Wiele interpolacji w jednym elemencie
const template4 = `<div>{{ device.name }} - {{ device.address }}</div>`;
console.log('\n\nTest 4: Wiele interpolacji');
console.log('Input:', template4);
const result4 = transformer.transformInterpolation(template4);
console.log('Output:', result4);
// Test 5: Interpolacja w atrybucie
const template5 = `<div title="{{ device.name }}">Content</div>`;
console.log('\n\nTest 5: Interpolacja w atrybucie');
console.log('Input:', template5);
const result5 = transformer.transformInterpolation(template5);
console.log('Output:', result5);
// Test 6: Pełny template jak w devices component
const template6 = `
<div class="content">
@for (device of devices(); track device.address) {
<div class="device card">
<div class="name">{{ device.name || 'Unnamed' }}</div>
<div class="address">{{ device.address }}</div>
</div>
}
</div>
<div class="footer">
Devices: <span>{{ deviceCount() }}</span>
</div>
`;
console.log('\n\nTest 6: Pełny template devices component');
console.log('Input:', template6);
const result6 = transformer.transformAll(template6);
console.log('Output:', result6);
// Sprawdzenie czy wszystkie transformacje są poprawne
const tests = [
{
name: 'Test 1: Simple interpolation transformed',
pass: result1.includes('[innerText]') && result1.includes('device.name')
},
{
name: 'Test 2: OR operator preserved',
pass: result2.includes('||') && result2.includes('Unnamed')
},
{
name: 'Test 3: Function call preserved',
pass: result3.includes('deviceCount()')
},
{
name: 'Test 4: Multiple interpolations',
pass: (result4.match(/\[innerText\]/g) || []).length === 2
},
{
name: 'Test 5: Attribute interpolation',
pass: result5.includes('data-quarc-attr-bindings') || result5.includes('[attr.title]')
},
{
name: 'Test 6: Full template has *ngFor',
pass: result6.includes('*ngFor')
},
{
name: 'Test 6: Full template has interpolations',
pass: result6.includes('[inner-text]') || result6.includes('[innerText]')
},
{
name: 'Test 6: No pipe errors in simple expressions',
pass: !result6.includes('this._pipes') || result6.includes('|')
}
];
console.log('\n\n=== Test Results ===');
let passed = 0;
let failed = 0;
tests.forEach(test => {
const status = test.pass ? '✓ PASS' : '✗ FAIL';
console.log(`${status}: ${test.name}`);
if (test.pass) passed++;
else failed++;
});
console.log(`\nTotal: ${passed} passed, ${failed} failed`);
// Dodatkowa diagnostyka
console.log('\n\n=== Diagnostyka ===');
console.log('Czy result1 zawiera this._pipes?:', result1.includes('this._pipes'));
console.log('Czy result2 zawiera this._pipes?:', result2.includes('this._pipes'));
console.log('Czy result3 zawiera this._pipes?:', result3.includes('this._pipes'));
console.log('\nResult6 check:');
console.log('Zawiera [inner-text]?:', result6.includes('[inner-text]'));
console.log('Zawiera [innerText]?:', result6.includes('[innerText]'));
console.log('Liczba wystąpień [inner-text]:', (result6.match(/\[inner-text\]/g) || []).length);
console.log('Liczba wystąpień [innerText]:', (result6.match(/\[innerText\]/g) || []).length);
if (failed > 0) {
console.error('\n❌ INTERPOLATION TRANSFORMATION TEST FAILED');
process.exit(1);
} else {
console.log('\n✅ INTERPOLATION TRANSFORMATION TEST PASSED');
process.exit(0);
}

View File

@ -0,0 +1,88 @@
/**
* Test aby upewnić się, że operatory logiczne (||, &&) nie mylone z pipe separator |
*/
import { TemplateTransformer } from '../../cli/processors/template/template-transformer';
console.log('\n=== Test: Pipe vs Logical Operators ===\n');
const transformer = new TemplateTransformer();
// Test 1: Operator || nie powinien być traktowany jako pipe
const test1 = `{{ value || 'default' }}`;
console.log('Test 1: Operator ||');
console.log('Input:', test1);
const result1 = transformer.transformInterpolation(test1);
console.log('Output:', result1);
const pass1 = !result1.includes('this._pipes') && result1.includes('||');
console.log('Pass:', pass1);
// Test 2: Operator && nie powinien być traktowany jako pipe
const test2 = `{{ condition && value }}`;
console.log('\nTest 2: Operator &&');
console.log('Input:', test2);
const result2 = transformer.transformInterpolation(test2);
console.log('Output:', result2);
const pass2 = !result2.includes('this._pipes') && result2.includes('&&');
console.log('Pass:', pass2);
// Test 3: Prawdziwy pipe powinien być transformowany
const test3 = `{{ value | uppercase }}`;
console.log('\nTest 3: Prawdziwy pipe');
console.log('Input:', test3);
const result3 = transformer.transformInterpolation(test3);
console.log('Output:', result3);
const pass3 = result3.includes('this._pipes') && result3.includes('uppercase');
console.log('Pass:', pass3);
// Test 4: Pipe z argumentami
const test4 = `{{ value | slice:0:10 }}`;
console.log('\nTest 4: Pipe z argumentami');
console.log('Input:', test4);
const result4 = transformer.transformInterpolation(test4);
console.log('Output:', result4);
const pass4 = result4.includes('this._pipes') && result4.includes('slice');
console.log('Pass:', pass4);
// Test 5: Kombinacja || i pipe
const test5 = `{{ (value || 'default') | uppercase }}`;
console.log('\nTest 5: Kombinacja || i pipe');
console.log('Input:', test5);
const result5 = transformer.transformInterpolation(test5);
console.log('Output:', result5);
const pass5 = result5.includes('this._pipes') && result5.includes('||') && result5.includes('uppercase');
console.log('Pass:', pass5);
// Test 6: Wielokrotne ||
const test6 = `{{ value1 || value2 || 'default' }}`;
console.log('\nTest 6: Wielokrotne ||');
console.log('Input:', test6);
const result6 = transformer.transformInterpolation(test6);
console.log('Output:', result6);
const pass6 = !result6.includes('this._pipes') && (result6.match(/\|\|/g) || []).length === 2;
console.log('Pass:', pass6);
// Test 7: Łańcuch pipes
const test7 = `{{ value | lowercase | slice:0:5 }}`;
console.log('\nTest 7: Łańcuch pipes');
console.log('Input:', test7);
const result7 = transformer.transformInterpolation(test7);
console.log('Output:', result7);
const pass7 = result7.includes('lowercase') && result7.includes('slice');
console.log('Pass:', pass7);
const allTests = [pass1, pass2, pass3, pass4, pass5, pass6, pass7];
const passed = allTests.filter(p => p).length;
const failed = allTests.length - passed;
console.log('\n=== Summary ===');
console.log(`Passed: ${passed}/${allTests.length}`);
console.log(`Failed: ${failed}/${allTests.length}`);
if (failed > 0) {
console.error('\n❌ PIPE VS LOGICAL OPERATORS TEST FAILED');
process.exit(1);
} else {
console.log('\n✅ PIPE VS LOGICAL OPERATORS TEST PASSED');
process.exit(0);
}