pipes
This commit is contained in:
parent
c3dda73d3a
commit
ba47312f55
|
|
@ -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.
|
||||||
|
|
@ -40,19 +40,25 @@ 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(', ')}];`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insert) {
|
||||||
replacements.push({
|
replacements.push({
|
||||||
position: scopeIdEnd,
|
position: scopeIdEnd,
|
||||||
insert: directivesProperty,
|
insert,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||||
const r = replacements[i];
|
const r = replacements[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(',')
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* Test aby upewnić się, że operatory logiczne (||, &&) nie są 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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue