479 lines
13 KiB
JavaScript
479 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Polyfill window dla Node.js
|
|
(global as any).window = (global as any).window || { __quarc: {} };
|
|
(global as any).window.__quarc = (global as any).window.__quarc || {};
|
|
|
|
import { signal, computed, effect, WritableSignal, Signal, EffectRef } from '../../core/angular/signals';
|
|
|
|
interface TestResult {
|
|
name: string;
|
|
passed: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const results: TestResult[] = [];
|
|
|
|
function test(name: string, fn: () => void | Promise<void>): void {
|
|
try {
|
|
const result = fn();
|
|
if (result instanceof Promise) {
|
|
result
|
|
.then(() => results.push({ name, passed: true }))
|
|
.catch((e) => results.push({ name, passed: false, error: String(e) }));
|
|
} else {
|
|
results.push({ name, passed: true });
|
|
}
|
|
} catch (e) {
|
|
results.push({ name, passed: false, error: String(e) });
|
|
}
|
|
}
|
|
|
|
function assertEqual<T>(actual: T, expected: T, message?: string): void {
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`${message || 'Assertion failed'}\nExpected: ${expected}\nActual: ${actual}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertTrue(condition: boolean, message?: string): void {
|
|
if (!condition) {
|
|
throw new Error(message || 'Expected condition to be true');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Signal Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n=== TESTY SYGNAŁÓW I REAKTYWNOŚCI ===\n');
|
|
|
|
test('signal: tworzy sygnał z wartością początkową', () => {
|
|
const count = signal(0);
|
|
assertEqual(count(), 0);
|
|
});
|
|
|
|
test('signal: set zmienia wartość', () => {
|
|
const count = signal(0);
|
|
count.set(5);
|
|
assertEqual(count(), 5);
|
|
});
|
|
|
|
test('signal: update modyfikuje wartość', () => {
|
|
const count = signal(10);
|
|
count.update(v => v + 5);
|
|
assertEqual(count(), 15);
|
|
});
|
|
|
|
test('signal: asReadonly zwraca readonly signal', () => {
|
|
const count = signal(0);
|
|
const readonly = count.asReadonly();
|
|
assertEqual(readonly(), 0);
|
|
count.set(10);
|
|
assertEqual(readonly(), 10);
|
|
assertTrue(!('set' in readonly), 'readonly signal nie powinien mieć metody set');
|
|
});
|
|
|
|
test('signal: equal option zapobiega niepotrzebnym aktualizacjom', () => {
|
|
let updateCount = 0;
|
|
const obj = signal({ id: 1 }, { equal: (a, b) => a.id === b.id });
|
|
|
|
effect(() => {
|
|
obj();
|
|
updateCount++;
|
|
});
|
|
|
|
// Poczekaj na pierwszy effect
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
const initialCount = updateCount;
|
|
obj.set({ id: 1 }); // Ten sam id - nie powinno triggerować
|
|
|
|
setTimeout(() => {
|
|
assertEqual(updateCount, initialCount, 'Nie powinno być dodatkowych aktualizacji');
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Computed Tests
|
|
// ============================================================================
|
|
|
|
test('computed: oblicza wartość z sygnałów', () => {
|
|
const a = signal(2);
|
|
const b = signal(3);
|
|
const sum = computed(() => a() + b());
|
|
assertEqual(sum(), 5);
|
|
});
|
|
|
|
test('computed: aktualizuje się gdy zależności się zmieniają', () => {
|
|
const a = signal(2);
|
|
const b = signal(3);
|
|
const sum = computed(() => a() + b());
|
|
|
|
assertEqual(sum(), 5);
|
|
a.set(10);
|
|
|
|
// Computed używa microtask do ustawienia isDirty, więc musimy poczekać
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(sum(), 13);
|
|
resolve();
|
|
}, 10);
|
|
});
|
|
});
|
|
|
|
test('computed: cachuje wartość', () => {
|
|
let computeCount = 0;
|
|
const a = signal(1);
|
|
const doubled = computed(() => {
|
|
computeCount++;
|
|
return a() * 2;
|
|
});
|
|
|
|
doubled();
|
|
doubled();
|
|
doubled();
|
|
|
|
assertEqual(computeCount, 1, 'Computed powinien być wywołany tylko raz');
|
|
});
|
|
|
|
test('computed: zagnieżdżone computed', () => {
|
|
const a = signal(2);
|
|
const doubled = computed(() => a() * 2);
|
|
const quadrupled = computed(() => doubled() * 2);
|
|
|
|
assertEqual(quadrupled(), 8);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Effect Tests
|
|
// ============================================================================
|
|
|
|
test('effect: uruchamia się przy pierwszym wywołaniu', () => {
|
|
let ran = false;
|
|
effect(() => {
|
|
ran = true;
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertTrue(ran, 'Effect powinien się uruchomić');
|
|
resolve();
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('effect: reaguje na zmiany sygnału', () => {
|
|
const count = signal(0);
|
|
let effectValue = -1;
|
|
|
|
effect(() => {
|
|
effectValue = count();
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 0);
|
|
count.set(5);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 5);
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('effect: destroy zatrzymuje reakcje', () => {
|
|
const count = signal(0);
|
|
let effectValue = -1;
|
|
|
|
const ref = effect(() => {
|
|
effectValue = count();
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 0);
|
|
ref.destroy();
|
|
count.set(100);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 0, 'Effect nie powinien reagować po destroy');
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('effect: śledzi wiele sygnałów', () => {
|
|
const a = signal(1);
|
|
const b = signal(2);
|
|
let sum = 0;
|
|
|
|
effect(() => {
|
|
sum = a() + b();
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(sum, 3);
|
|
a.set(10);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(sum, 12);
|
|
b.set(20);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(sum, 30);
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('effect: reaguje na computed', () => {
|
|
const a = signal(2);
|
|
const doubled = computed(() => a() * 2);
|
|
let effectValue = 0;
|
|
|
|
effect(() => {
|
|
effectValue = doubled();
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 4);
|
|
a.set(5);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 10);
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Granular Reactivity Tests
|
|
// ============================================================================
|
|
|
|
test('granular: wiele effects na tym samym sygnale', () => {
|
|
const count = signal(0);
|
|
let effect1Value = -1;
|
|
let effect2Value = -1;
|
|
|
|
effect(() => { effect1Value = count(); });
|
|
effect(() => { effect2Value = count() * 2; });
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(effect1Value, 0);
|
|
assertEqual(effect2Value, 0);
|
|
count.set(5);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(effect1Value, 5);
|
|
assertEqual(effect2Value, 10);
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('granular: niezależne sygnały nie wpływają na siebie', () => {
|
|
const a = signal(1);
|
|
const b = signal(2);
|
|
let aEffectCount = 0;
|
|
let bEffectCount = 0;
|
|
|
|
effect(() => { a(); aEffectCount++; });
|
|
effect(() => { b(); bEffectCount++; });
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
const initialA = aEffectCount;
|
|
const initialB = bEffectCount;
|
|
|
|
a.set(10);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(aEffectCount, initialA + 1, 'Effect A powinien się uruchomić');
|
|
assertEqual(bEffectCount, initialB, 'Effect B nie powinien się uruchomić');
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Template Rendering Scenario Tests
|
|
// ============================================================================
|
|
|
|
test('template: computed aktualizuje się synchronicznie po set na signal', () => {
|
|
const containerDimensions = signal({ width: 0, height: 0 });
|
|
const sizeAttribute = computed(() => {
|
|
const size = containerDimensions();
|
|
return `${size.width} x ${size.height}`;
|
|
});
|
|
|
|
assertEqual(sizeAttribute(), '0 x 0');
|
|
|
|
containerDimensions.set({ width: 100, height: 200 });
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(sizeAttribute(), '100 x 200', 'Computed powinien mieć nową wartość');
|
|
resolve();
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('template: effect reaguje na zmianę computed który zależy od signal', () => {
|
|
const containerDimensions = signal({ width: 0, height: 0 });
|
|
const sizeAttribute = computed(() => {
|
|
const size = containerDimensions();
|
|
return `${size.width} x ${size.height}`;
|
|
});
|
|
|
|
let effectValue = '';
|
|
let effectRunCount = 0;
|
|
|
|
effect(() => {
|
|
effectValue = sizeAttribute();
|
|
effectRunCount++;
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, '0 x 0');
|
|
assertEqual(effectRunCount, 1);
|
|
|
|
containerDimensions.set({ width: 100, height: 200 });
|
|
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, '100 x 200', 'Effect powinien mieć nową wartość z computed');
|
|
assertEqual(effectRunCount, 2, 'Effect powinien się uruchomić ponownie');
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('template: łańcuch signal -> computed -> computed -> effect', () => {
|
|
const base = signal(10);
|
|
const doubled = computed(() => base() * 2);
|
|
const quadrupled = computed(() => doubled() * 2);
|
|
|
|
let effectValue = 0;
|
|
|
|
effect(() => {
|
|
effectValue = quadrupled();
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 40);
|
|
|
|
base.set(5);
|
|
|
|
setTimeout(() => {
|
|
assertEqual(effectValue, 20, 'Effect powinien reagować na zmianę w łańcuchu');
|
|
resolve();
|
|
}, 50);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('template: wielokrotne zmiany signal w krótkim czasie', () => {
|
|
const count = signal(0);
|
|
const doubled = computed(() => count() * 2);
|
|
|
|
let effectValues: number[] = [];
|
|
|
|
effect(() => {
|
|
effectValues.push(doubled());
|
|
});
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
count.set(1);
|
|
count.set(2);
|
|
count.set(3);
|
|
|
|
setTimeout(() => {
|
|
const lastValue = effectValues[effectValues.length - 1];
|
|
assertEqual(lastValue, 6, 'Ostatnia wartość powinna być 6');
|
|
resolve();
|
|
}, 100);
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
test('template: computed z obiektem - Object.is comparison', () => {
|
|
const dimensions = signal({ width: 0, height: 0 });
|
|
let computeCount = 0;
|
|
|
|
const formatted = computed(() => {
|
|
computeCount++;
|
|
const d = dimensions();
|
|
return `${d.width}x${d.height}`;
|
|
});
|
|
|
|
assertEqual(formatted(), '0x0');
|
|
assertEqual(computeCount, 1);
|
|
|
|
// Ustawienie tego samego obiektu - Object.is zwróci false bo to nowy obiekt
|
|
dimensions.set({ width: 0, height: 0 });
|
|
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
formatted(); // Wymuszamy odczyt
|
|
assertEqual(computeCount, 2, 'Computed powinien się przeliczyć dla nowego obiektu');
|
|
resolve();
|
|
}, 50);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Run all tests
|
|
// ============================================================================
|
|
|
|
async function runTests() {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
console.log('\n=== PODSUMOWANIE ===');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (const result of results) {
|
|
if (result.passed) {
|
|
console.log(`✅ ${result.name}`);
|
|
passed++;
|
|
} else {
|
|
console.log(`❌ ${result.name}`);
|
|
console.log(` Error: ${result.error}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log(`\n✅ Testy zaliczone: ${passed}`);
|
|
console.log(`❌ Testy niezaliczone: ${failed}`);
|
|
console.log(`📊 Procent sukcesu: ${((passed / results.length) * 100).toFixed(1)}%`);
|
|
|
|
if (failed === 0) {
|
|
console.log('\n🎉 Wszystkie testy przeszły pomyślnie!\n');
|
|
} else {
|
|
console.log('\n❌ Niektóre testy nie przeszły.\n');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
runTests();
|