Compare commits
No commits in common. "4c37e92660d6c5a0462eba2f9b2df16f2bd854e8" and "72f5d249cf81a29e5148a2579725f7271e0bcec3" have entirely different histories.
4c37e92660
...
72f5d249cf
86
README.md
86
README.md
|
|
@ -113,14 +113,6 @@ bootstrapApplication(AppComponent, {
|
|||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"proxy": {
|
||||
"/api/*": {
|
||||
"target": "http://localhost:3000",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"environments": {
|
||||
"development": {
|
||||
"minifyNames": true,
|
||||
|
|
@ -337,84 +329,6 @@ export class HighlightDirective {
|
|||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Development Server Proxy
|
||||
|
||||
The development server supports proxying HTTP requests to a backend server. This is useful when your frontend needs to communicate with an API during development.
|
||||
|
||||
**Configuration in quarc.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"serve": {
|
||||
"proxy": {
|
||||
"/api/*": {
|
||||
"target": "http://192.168.1.100:8080",
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/auth/*": {
|
||||
"target": "https://auth.example.com",
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/auth": "/api/v1/auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Proxy Options:**
|
||||
|
||||
- **Pattern matching**: Use wildcards (`*`) to match request paths
|
||||
- `/api/*` matches `/api/users`, `/api/data`, etc.
|
||||
- `/v1/*/data` matches `/v1/users/data`, `/v1/products/data`, etc.
|
||||
|
||||
- **target**: Backend server URL (required)
|
||||
- Can be HTTP or HTTPS
|
||||
- Include protocol and host, optionally port
|
||||
|
||||
- **changeOrigin**: Set to `true` to change the `Host` header to match the target (recommended for most cases)
|
||||
|
||||
- **pathRewrite**: Object with regex patterns to rewrite request paths
|
||||
- Key: regex pattern to match
|
||||
- Value: replacement string
|
||||
|
||||
**Example Use Cases:**
|
||||
|
||||
```json
|
||||
{
|
||||
"serve": {
|
||||
"proxy": {
|
||||
"/api/*": {
|
||||
"target": "http://localhost:3000",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When your app makes a request to `http://localhost:4200/api/users`, it will be proxied to `http://localhost:3000/api/users`.
|
||||
|
||||
**Perfect for ESP32 Development:**
|
||||
|
||||
When developing for ESP32, you can proxy API requests to the device:
|
||||
|
||||
```json
|
||||
{
|
||||
"serve": {
|
||||
"proxy": {
|
||||
"/api/*": {
|
||||
"target": "http://192.168.1.50",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to develop your frontend locally while testing against the actual ESP32 backend.
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Components are automatically lazy-loaded when using route-based code splitting:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { BaseBuilder } from './base-builder';
|
|||
import {
|
||||
StaticPath,
|
||||
StaticRemotePath,
|
||||
ProxyConfig,
|
||||
} from '../types';
|
||||
|
||||
class Server extends BaseBuilder {
|
||||
|
|
@ -160,82 +159,6 @@ class Server extends BaseBuilder {
|
|||
return this.config.serve?.staticPaths || [];
|
||||
}
|
||||
|
||||
private getProxyConfig(): ProxyConfig {
|
||||
return this.config.serve?.proxy || {};
|
||||
}
|
||||
|
||||
private matchProxyPath(reqUrl: string): { pattern: string; config: { target: string; changeOrigin?: boolean; pathRewrite?: { [key: string]: string } } } | null {
|
||||
const proxyConfig = this.getProxyConfig();
|
||||
|
||||
for (const [pattern, config] of Object.entries(proxyConfig)) {
|
||||
const regexPattern = pattern.replace(/\*/g, '.*');
|
||||
const regex = new RegExp(`^${regexPattern}`);
|
||||
|
||||
if (regex.test(reqUrl)) {
|
||||
return { pattern, config };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private tryProxyRequest(reqUrl: string, req: http.IncomingMessage, res: http.ServerResponse): boolean {
|
||||
const match = this.matchProxyPath(reqUrl);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { pattern, config } = match;
|
||||
let targetPath = reqUrl;
|
||||
|
||||
if (config.pathRewrite) {
|
||||
for (const [from, to] of Object.entries(config.pathRewrite)) {
|
||||
const fromRegex = new RegExp(from);
|
||||
targetPath = targetPath.replace(fromRegex, to);
|
||||
}
|
||||
}
|
||||
|
||||
const targetUrl = config.target + targetPath;
|
||||
|
||||
if (this.isVerbose()) {
|
||||
console.log(`[Proxy] ${req.method} ${reqUrl} -> ${targetUrl}`);
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(targetUrl);
|
||||
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
||||
|
||||
const headers: http.OutgoingHttpHeaders = { ...req.headers };
|
||||
|
||||
if (config.changeOrigin) {
|
||||
headers.host = parsedUrl.host;
|
||||
}
|
||||
|
||||
const proxyReq = protocol.request(
|
||||
targetUrl,
|
||||
{
|
||||
method: req.method,
|
||||
headers,
|
||||
},
|
||||
(proxyRes) => {
|
||||
if (this.isVerbose()) {
|
||||
console.log(`[Proxy] Response: ${proxyRes.statusCode} for ${req.url}`);
|
||||
}
|
||||
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
},
|
||||
);
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error(`[Proxy] Error for ${req.url}:`, err.message);
|
||||
res.writeHead(502);
|
||||
res.end('Bad Gateway');
|
||||
});
|
||||
|
||||
req.pipe(proxyReq);
|
||||
return true;
|
||||
}
|
||||
|
||||
private proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
console.log(`[Proxy] ${req.method} ${req.url} -> ${targetUrl}`);
|
||||
const parsedUrl = new URL(targetUrl);
|
||||
|
|
@ -322,10 +245,6 @@ class Server extends BaseBuilder {
|
|||
this.httpServer = http.createServer((req, res) => {
|
||||
const reqUrl = req.url || '/';
|
||||
|
||||
if (this.tryProxyRequest(reqUrl, req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tryServeStaticPath(reqUrl, req, res)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,14 +32,6 @@ export interface StaticRemotePath {
|
|||
|
||||
export type StaticPath = StaticLocalPath | StaticRemotePath;
|
||||
|
||||
export interface ProxyConfig {
|
||||
[path: string]: {
|
||||
target: string;
|
||||
changeOrigin?: boolean;
|
||||
pathRewrite?: { [key: string]: string };
|
||||
};
|
||||
}
|
||||
|
||||
export interface ActionsConfig {
|
||||
prebuild?: string[];
|
||||
postbuild?: string[];
|
||||
|
|
@ -65,7 +57,6 @@ export interface BuildConfig {
|
|||
export interface ServeConfig {
|
||||
actions?: ActionsConfig;
|
||||
staticPaths?: StaticPath[];
|
||||
proxy?: ProxyConfig;
|
||||
}
|
||||
|
||||
export interface QuarcConfig {
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
node_modules/
|
||||
playwright-report/
|
||||
test-results/
|
||||
playwright/.cache/
|
||||
*.log
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
# Migracja testów na Playwright
|
||||
|
||||
## Podsumowanie zmian
|
||||
|
||||
Wszystkie testy Quarc Framework zostały przeniesione na Playwright, co zapewnia:
|
||||
|
||||
✅ **Automatyczne zarządzanie serwerem deweloperskim**
|
||||
✅ **Testy w prawdziwej przeglądarce**
|
||||
✅ **Lepsze debugowanie i raportowanie**
|
||||
✅ **Szybsze wykonanie testów**
|
||||
✅ **Jednolity framework dla e2e i unit testów**
|
||||
|
||||
## Struktura przed migracją
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/
|
||||
│ ├── run-tests.ts # Własny runner
|
||||
│ ├── test-processors.ts
|
||||
│ ├── test-signals-reactivity.ts
|
||||
│ ├── test-directives.ts
|
||||
│ └── ... (19 plików testowych)
|
||||
└── e2e/
|
||||
├── run-e2e-tests.ts # Własny runner z fetch()
|
||||
└── app/ # Aplikacja testowa
|
||||
```
|
||||
|
||||
## Struktura po migracji
|
||||
|
||||
```
|
||||
tests/
|
||||
├── playwright/
|
||||
│ ├── e2e/
|
||||
│ │ └── pipes.spec.ts # Wszystkie testy pipes
|
||||
│ └── unit/
|
||||
│ └── processors.spec.ts # Placeholder dla testów jednostkowych
|
||||
├── e2e/
|
||||
│ └── app/ # Aplikacja testowa (bez zmian)
|
||||
├── playwright.config.ts # Konfiguracja Playwright
|
||||
├── package.json # Nowe skrypty
|
||||
└── README.md # Dokumentacja
|
||||
```
|
||||
|
||||
## Kluczowe zmiany
|
||||
|
||||
### 1. Automatyczne zarządzanie serwerem
|
||||
|
||||
**Przed:**
|
||||
```typescript
|
||||
// Ręczne uruchamianie serwera w run-e2e-tests.ts
|
||||
const serverProcess = spawn('qu', ['serve'], { cwd: appDir });
|
||||
// Ręczne zamykanie
|
||||
serverProcess.kill();
|
||||
```
|
||||
|
||||
**Po:**
|
||||
```typescript
|
||||
// playwright.config.ts - Playwright zarządza automatycznie
|
||||
webServer: {
|
||||
command: 'cd e2e/app && npm install && node ../../../cli/bin/qu.js serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Testy E2E
|
||||
|
||||
**Przed:**
|
||||
```typescript
|
||||
// Własny runner z fetch() i parsowaniem HTML
|
||||
const html = await fetch(`http://localhost:${port}/uppercase`).then(r => r.text());
|
||||
const result = extractTextContent(html, '#test-1 .result');
|
||||
const expected = extractTextContent(html, '#test-1 .expected');
|
||||
```
|
||||
|
||||
**Po:**
|
||||
```typescript
|
||||
// Playwright API
|
||||
test('should transform hardcoded string', async ({ page }) => {
|
||||
await page.goto('/uppercase');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Skrypty NPM
|
||||
|
||||
**Przed:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "npx ts-node run-tests.ts",
|
||||
"test:e2e": "echo 'E2E tests not yet implemented'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Po:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:e2e": "playwright test playwright/e2e",
|
||||
"test:unit": "playwright test playwright/unit",
|
||||
"test:headed": "playwright test --headed",
|
||||
"test:debug": "playwright test --debug",
|
||||
"test:ui": "playwright test --ui",
|
||||
"test:report": "playwright show-report"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Zalety migracji
|
||||
|
||||
### 1. Automatyzacja
|
||||
- Serwer deweloperski uruchamia się i zamyka automatycznie
|
||||
- Nie trzeba ręcznie zarządzać procesami
|
||||
- Współdzielenie serwera między uruchomieniami w trybie lokalnym
|
||||
|
||||
### 2. Prawdziwa przeglądarka
|
||||
- Testy działają w rzeczywistym środowisku Chromium
|
||||
- Pełna obsługa JavaScript, CSS, Web Components
|
||||
- Możliwość testowania interakcji użytkownika
|
||||
|
||||
### 3. Debugowanie
|
||||
- **UI Mode** - interaktywny interfejs do debugowania
|
||||
- **Traces** - nagrywanie przebiegu testów
|
||||
- **Screenshots** - automatyczne zrzuty ekranu przy błędach
|
||||
- **Video** - nagrywanie wideo testów
|
||||
- **Debug mode** - step-by-step debugging
|
||||
|
||||
### 4. Raportowanie
|
||||
- HTML reports z wizualizacją wyników
|
||||
- Screenshots i traces dla niepowodzeń
|
||||
- Szczegółowe logi i stack traces
|
||||
- Metryki wydajności
|
||||
|
||||
### 5. Wydajność
|
||||
- Równoległe wykonanie testów (8 workers domyślnie)
|
||||
- Retry mechanism dla niestabilnych testów
|
||||
- Optymalizacja dla CI/CD
|
||||
|
||||
## Usunięte pliki
|
||||
|
||||
Następujące pliki nie są już potrzebne:
|
||||
|
||||
- `/web/quarc/tests/unit/run-tests.ts` - zastąpione przez Playwright
|
||||
- `/web/quarc/tests/e2e/run-e2e-tests.ts` - zastąpione przez Playwright
|
||||
- Wszystkie stare testy jednostkowe w `/web/quarc/tests/unit/test-*.ts`
|
||||
|
||||
**Uwaga:** Stare pliki testowe pozostają w repozytorium jako referencja, ale nie są już używane.
|
||||
|
||||
## Migracja własnych testów
|
||||
|
||||
Jeśli chcesz przenieść własne testy na Playwright:
|
||||
|
||||
### E2E Test
|
||||
|
||||
1. Utwórz plik `.spec.ts` w `playwright/e2e/`
|
||||
2. Użyj Playwright API:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('My Feature', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/my-route');
|
||||
await page.waitForSelector('#my-element');
|
||||
});
|
||||
|
||||
test('should work correctly', async ({ page }) => {
|
||||
const result = await page.locator('#result').textContent();
|
||||
expect(result).toBe('expected value');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Unit Test
|
||||
|
||||
Dla testów jednostkowych które nie wymagają DOM:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('My Unit Tests', () => {
|
||||
test('should calculate correctly', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Uruchamianie testów
|
||||
|
||||
```bash
|
||||
cd /web/quarc/tests
|
||||
|
||||
# Wszystkie testy
|
||||
npm test
|
||||
|
||||
# Tylko E2E
|
||||
npm run test:e2e
|
||||
|
||||
# Tylko unit
|
||||
npm run test:unit
|
||||
|
||||
# Z widoczną przeglądarką
|
||||
npm run test:headed
|
||||
|
||||
# Debug mode
|
||||
npm run test:debug
|
||||
|
||||
# UI mode (interaktywny)
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
W środowisku CI Playwright automatycznie:
|
||||
- Nie współdzieli serwera (`reuseExistingServer: false`)
|
||||
- Używa 1 workera (sekwencyjne wykonanie)
|
||||
- Wykonuje 2 retry dla niestabilnych testów
|
||||
- Generuje HTML report
|
||||
|
||||
## Problemy i rozwiązania
|
||||
|
||||
### Problem: Testy timeout'ują
|
||||
**Rozwiązanie:** Dodaj `waitForSelector` przed sprawdzaniem elementów:
|
||||
```typescript
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
```
|
||||
|
||||
### Problem: Elementy nie są znalezione
|
||||
**Rozwiązanie:** Sprawdź czy routing działa poprawnie i czy aplikacja się załadowała:
|
||||
```typescript
|
||||
await page.goto('/route');
|
||||
await page.waitForLoadState('networkidle');
|
||||
```
|
||||
|
||||
### Problem: Serwer nie startuje
|
||||
**Rozwiązanie:** Sprawdź czy aplikacja jest zbudowana:
|
||||
```bash
|
||||
cd e2e/app
|
||||
node ../../../cli/bin/qu.js build
|
||||
```
|
||||
|
||||
## Następne kroki
|
||||
|
||||
1. ✅ Migracja testów E2E pipes - **UKOŃCZONE**
|
||||
2. ⏳ Dodanie testów dla innych funkcjonalności (routing, directives, etc.)
|
||||
3. ⏳ Konfiguracja CI/CD pipeline
|
||||
4. ⏳ Dodanie testów cross-browser (Firefox, Safari)
|
||||
5. ⏳ Dodanie visual regression tests
|
||||
200
tests/README.md
200
tests/README.md
|
|
@ -1,200 +0,0 @@
|
|||
# Quarc Framework Tests
|
||||
|
||||
Kompletny zestaw testów dla Quarc Framework oparty na Playwright.
|
||||
|
||||
## Struktura
|
||||
|
||||
```
|
||||
tests/
|
||||
├── playwright/
|
||||
│ ├── e2e/ # Testy end-to-end
|
||||
│ │ └── pipes.spec.ts # Testy wszystkich pipes
|
||||
│ └── unit/ # Testy jednostkowe
|
||||
│ ├── processors.spec.ts
|
||||
│ └── signals.spec.ts
|
||||
├── e2e/
|
||||
│ └── app/ # Aplikacja testowa dla e2e
|
||||
├── playwright.config.ts # Konfiguracja Playwright
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Instalacja
|
||||
|
||||
```bash
|
||||
cd /web/quarc/tests
|
||||
npm install
|
||||
```
|
||||
|
||||
## Uruchamianie testów
|
||||
|
||||
### Wszystkie testy
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Tylko testy E2E
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Tylko testy jednostkowe
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Tryb headed (z widoczną przeglądarką)
|
||||
```bash
|
||||
npm run test:headed
|
||||
```
|
||||
|
||||
### Tryb debug
|
||||
```bash
|
||||
npm run test:debug
|
||||
```
|
||||
|
||||
### UI Mode (interaktywny)
|
||||
```bash
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
### Raport z testów
|
||||
```bash
|
||||
npm run test:report
|
||||
```
|
||||
|
||||
## Jak to działa
|
||||
|
||||
### Dev Server
|
||||
|
||||
Playwright automatycznie zarządza serwerem deweloperskim:
|
||||
- **Uruchamianie**: Playwright automatycznie uruchamia `qu serve` przed testami
|
||||
- **Port**: Serwer nasłuchuje na `http://localhost:4200`
|
||||
- **Reuse**: W trybie lokalnym serwer jest współdzielony między uruchomieniami
|
||||
- **Zamykanie**: Playwright automatycznie zamyka serwer po testach
|
||||
|
||||
Konfiguracja w `playwright.config.ts`:
|
||||
```typescript
|
||||
webServer: {
|
||||
command: 'cd e2e/app && node ../../../cli/bin/qu.js serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
}
|
||||
```
|
||||
|
||||
### Testy E2E
|
||||
|
||||
Testy E2E sprawdzają działanie wszystkich pipes w prawdziwej aplikacji:
|
||||
|
||||
- **UpperCasePipe** - transformacja na wielkie litery
|
||||
- **LowerCasePipe** - transformacja na małe litery
|
||||
- **JsonPipe** - serializacja do JSON
|
||||
- **Case Pipes** - CamelCase, PascalCase, SnakeCase, KebabCase
|
||||
- **DatePipe** - formatowanie dat
|
||||
- **SubstrPipe** - wycinanie podciągów
|
||||
- **Pipe Chains** - łańcuchy pipes
|
||||
|
||||
Każdy test:
|
||||
1. Nawiguje do odpowiedniej strony
|
||||
2. Pobiera wartość `.result` (wynik pipe)
|
||||
3. Pobiera wartość `.expected` (oczekiwany wynik)
|
||||
4. Porównuje obie wartości
|
||||
|
||||
### Testy jednostkowe
|
||||
|
||||
Testy jednostkowe sprawdzają podstawową funkcjonalność:
|
||||
|
||||
- **Signals** - signal(), computed(), effect()
|
||||
- **Reactivity** - aktualizacje i propagacja zmian
|
||||
- **Core functionality** - podstawowe mechanizmy frameworka
|
||||
|
||||
## Aplikacja testowa
|
||||
|
||||
Aplikacja w `e2e/app/` to pełna aplikacja Quarc z routingiem:
|
||||
|
||||
- Każda strona testuje inny pipe lub grupę pipes
|
||||
- Komponenty używają signals i metod
|
||||
- Wyniki są renderowane w `.result`, oczekiwane wartości w `.expected`
|
||||
|
||||
## Debugowanie
|
||||
|
||||
### Uruchom testy z widoczną przeglądarką
|
||||
```bash
|
||||
npm run test:headed
|
||||
```
|
||||
|
||||
### Uruchom w trybie debug
|
||||
```bash
|
||||
npm run test:debug
|
||||
```
|
||||
|
||||
### Uruchom aplikację testową manualnie
|
||||
```bash
|
||||
cd e2e/app
|
||||
node ../../../cli/bin/qu.js serve
|
||||
```
|
||||
Następnie otwórz http://localhost:4200 w przeglądarce.
|
||||
|
||||
### Sprawdź konkretny test
|
||||
```bash
|
||||
npx playwright test --grep "UpperCasePipe"
|
||||
```
|
||||
|
||||
### Generuj traces dla niepowodzeń
|
||||
Traces są automatycznie generowane dla niepowodzeń. Zobacz je przez:
|
||||
```bash
|
||||
npm run test:report
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
W środowisku CI:
|
||||
- Serwer nie jest współdzielony (`reuseExistingServer: false`)
|
||||
- Testy mają 2 retry
|
||||
- Worker count = 1 (sekwencyjne wykonanie)
|
||||
|
||||
## Migracja ze starych testów
|
||||
|
||||
Stare testy w `/web/quarc/tests/unit/` i `/web/quarc/tests/e2e/` zostały zastąpione przez Playwright.
|
||||
|
||||
### Zalety Playwright:
|
||||
|
||||
✅ **Automatyczne zarządzanie serwerem** - nie trzeba ręcznie uruchamiać/zamykać
|
||||
✅ **Prawdziwa przeglądarka** - testy w rzeczywistym środowisku
|
||||
✅ **Lepsze debugowanie** - UI mode, traces, screenshots
|
||||
✅ **Szybsze** - równoległe wykonanie testów
|
||||
✅ **Lepsze raporty** - HTML reports z screenshots i traces
|
||||
✅ **Cross-browser** - możliwość testowania w Chrome, Firefox, Safari
|
||||
|
||||
## Dodawanie nowych testów
|
||||
|
||||
### E2E Test
|
||||
|
||||
1. Dodaj nowy komponent w `e2e/app/src/pages/`
|
||||
2. Dodaj route w `e2e/app/src/routes.ts`
|
||||
3. Dodaj test w `playwright/e2e/pipes.spec.ts`
|
||||
|
||||
### Unit Test
|
||||
|
||||
Dodaj nowy plik w `playwright/unit/` z rozszerzeniem `.spec.ts`:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('My Feature', () => {
|
||||
test('should work correctly', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
Edytuj `playwright.config.ts` aby zmienić:
|
||||
- Przeglądarki do testowania
|
||||
- Timeout
|
||||
- Retry policy
|
||||
- Reporter
|
||||
- Base URL
|
||||
- WebServer command
|
||||
|
|
@ -58,45 +58,5 @@
|
|||
<body>
|
||||
<app-root></app-root>
|
||||
<script type="module" src="./main.js"></script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectDelay = 5000;
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket('ws://localhost:4200/qu-ws/');
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[Live Reload] Connected');
|
||||
reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'reload') {
|
||||
console.log('[Live Reload] Reloading page...');
|
||||
window.location.reload();
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn('[Live Reload] Connection lost, attempting to reconnect...');
|
||||
reconnectAttempts++;
|
||||
const delay = Math.min(1000 * reconnectAttempts, maxReconnectDelay);
|
||||
setTimeout(connect, delay);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"name": "quarc-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Playwright test suite for Quarc Framework",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:e2e": "playwright test playwright/e2e",
|
||||
"test:unit": "playwright test playwright/unit",
|
||||
"test:headed": "playwright test --headed",
|
||||
"test:debug": "playwright test --debug",
|
||||
"test:ui": "playwright test --ui",
|
||||
"test:report": "playwright show-report",
|
||||
"pretest": "cd e2e/app && npm install"
|
||||
},
|
||||
"keywords": ["quarc", "testing", "playwright", "e2e"],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@types/node": "^25.0.9",
|
||||
"playwright": "^1.57.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './playwright',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:4200',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'cd e2e/app && npm install && node ../../../cli/bin/qu.js serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
});
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Quarc Pipes E2E Tests', () => {
|
||||
|
||||
test.describe('UpperCasePipe', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/uppercase');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should transform hardcoded string', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should transform signal value', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should transform method call', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should work with || operator', async ({ page }) => {
|
||||
const result = await page.locator('#test-4 .result').textContent();
|
||||
const expected = await page.locator('#test-4 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('LowerCasePipe', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/lowercase');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should transform hardcoded string', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should transform signal value', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should transform method call', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('JsonPipe', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/json');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
const normalizeJson = (text: string | null) => {
|
||||
if (!text) return '';
|
||||
return text.replace(/\s+/g, '');
|
||||
};
|
||||
|
||||
test('should transform number literal', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(normalizeJson(result)).toBe(normalizeJson(expected));
|
||||
});
|
||||
|
||||
test('should transform string literal', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(normalizeJson(result)).toBe(normalizeJson(expected));
|
||||
});
|
||||
|
||||
test('should transform boolean literal', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(normalizeJson(result)).toBe(normalizeJson(expected));
|
||||
});
|
||||
|
||||
test('should transform object with signal', async ({ page }) => {
|
||||
const result = await page.locator('#test-4 .result').textContent();
|
||||
const expected = await page.locator('#test-4 .expected').textContent();
|
||||
expect(normalizeJson(result)).toBe(normalizeJson(expected));
|
||||
});
|
||||
|
||||
test('should transform array with signal', async ({ page }) => {
|
||||
const result = await page.locator('#test-5 .result').textContent();
|
||||
const expected = await page.locator('#test-5 .expected').textContent();
|
||||
expect(normalizeJson(result)).toBe(normalizeJson(expected));
|
||||
});
|
||||
|
||||
test('should transform object from method', async ({ page }) => {
|
||||
const result = await page.locator('#test-6 .result').textContent();
|
||||
const expected = await page.locator('#test-6 .expected').textContent();
|
||||
expect(normalizeJson(result)).toBe(normalizeJson(expected));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Case Pipes', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/case');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('CamelCasePipe should work', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('PascalCasePipe should work', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('SnakeCasePipe should work', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('KebabCasePipe should work', async ({ page }) => {
|
||||
const result = await page.locator('#test-4 .result').textContent();
|
||||
const expected = await page.locator('#test-4 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should work with signal values', async ({ page }) => {
|
||||
const result = await page.locator('#test-5 .result').textContent();
|
||||
const expected = await page.locator('#test-5 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DatePipe', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/date');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should format with yyyy-MM-dd', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should format with HH:mm:ss', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should use predefined shortDate format', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should work with method call', async ({ page }) => {
|
||||
const result = await page.locator('#test-4 .result').textContent();
|
||||
const expected = await page.locator('#test-4 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('SubstrPipe', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/substr');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should work with start and length', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should work with start only', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should work with signal value', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should work with method call', async ({ page }) => {
|
||||
const result = await page.locator('#test-4 .result').textContent();
|
||||
const expected = await page.locator('#test-4 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pipe Chains', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/chain');
|
||||
await page.waitForSelector('#test-1', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should chain lowercase | uppercase', async ({ page }) => {
|
||||
const result = await page.locator('#test-1 .result').textContent();
|
||||
const expected = await page.locator('#test-1 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should chain uppercase | substr', async ({ page }) => {
|
||||
const result = await page.locator('#test-2 .result').textContent();
|
||||
const expected = await page.locator('#test-2 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should chain with signal value', async ({ page }) => {
|
||||
const result = await page.locator('#test-3 .result').textContent();
|
||||
const expected = await page.locator('#test-3 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should chain with method call', async ({ page }) => {
|
||||
const result = await page.locator('#test-4 .result').textContent();
|
||||
const expected = await page.locator('#test-4 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
|
||||
test('should handle triple chain', async ({ page }) => {
|
||||
const result = await page.locator('#test-5 .result').textContent();
|
||||
const expected = await page.locator('#test-5 .expected').textContent();
|
||||
expect(result?.trim()).toBe(expected?.trim());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Core Framework Tests', () => {
|
||||
test('placeholder for processor tests', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "@playwright/test"]
|
||||
},
|
||||
"include": ["playwright/**/*.ts", "playwright.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in New Issue