מדריך בדיקות
מדריך זה מספק תיעוד מקיף לבדיקת התוסף WooAI Chatbot Pro בכל השכבות: בדיקות יחידה ואינטגרציה ב-PHP, בדיקות קומפוננטות JavaScript, וזרימות end-to-end.
1. סקירת בדיקות
פילוסופיית הבדיקות
אסטרטגיית הבדיקות של WooAI Chatbot Pro עוקבת אחר עקרונות ליבה אלה:
-
בידוד בדיקות: כל בדיקה רצה באופן עצמאי עם מצב נקי. mocks גלובליים ו-transients מאופסים בין בדיקות למניעת זיהום צולב.
-
Mock לתלויות חיצוניות: ספקי AI, WooCommerce, ו-APIs חיצוניים מקבלים mock כדי להבטיח התנהגות דטרמיניסטית והרצה מהירה ללא קריאות רשת.
-
כיסוי היררכי: בדיקות יחידה מאמתות קומפוננטות בודדות, בדיקות אינטגרציה מאמתות אינטראקציות בין קומפוננטות, ובדיקות E2E מאמתות זרימות משתמש מלאות.
-
כישלון מהיר: הבדיקות מוגדרות במצב קפדני (
failOnRisky,failOnWarning) כדי לתפוס בעיות פוטנציאליות מוקדם.
סוגי בדיקות
| סוג | Framework | ספרייה | מטרה |
|---|---|---|---|
| יחידה | PHPUnit | tests/AI, tests/Search, tests/Chat |
בדיקת מחלקה/מתודה בודדת |
| אינטגרציה | PHPUnit | tests/Integration |
בדיקת אינטראקציה בין קומפוננטות |
| E2E | PHPUnit | tests/E2E |
בדיקת זרימה מלאה |
| JavaScript | Jest | assets/src/**/__tests__ |
בדיקת קומפוננטות React והוקס |
יעדי כיסוי
הפרויקט אוכף סף כיסוי קפדני:
// jest.config.js
coverageThreshold: {
global: {
lines: 85,
branches: 85,
functions: 85,
statements: 85,
},
},
יעדי כיסוי PHP: 80%+ כיסוי שורות למודולי ליבה.
2. בדיקות PHP (PHPUnit)
הגדרה
תצורת PHPUnit
phpunit.xml מגדיר שלוש חבילות בדיקות עם הרצה קפדנית:
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="tests/bootstrap.php"
colors="true"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/AI</directory>
<directory>tests/Search</directory>
<directory>tests/Chat</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="E2E">
<directory>tests/E2E</directory>
</testsuite>
</testsuites>
</phpunit>
הגדרת בסיס נתונים לבדיקות
הבדיקות משתמשות בפונקציות WordPress מדומות המוגדרות ב-tests/bootstrap.php. לא נדרש חיבור לבסיס נתונים אמיתי לבדיקות יחידה:
// אחסון mock גלובלי
global $_wp_options, $_wp_transients;
$_wp_options = array();
$_wp_transients = array();
משתני סביבה לספקי AI מוגדרים בתצורת PHPUnit:
<php>
<env name="GEMINI_API_KEY" value="test-key"/>
<env name="OPENAI_API_KEY" value="test-key"/>
<env name="ANTHROPIC_API_KEY" value="test-key"/>
<env name="SUPABASE_URL" value="https://test.supabase.co"/>
<env name="SUPABASE_KEY" value="test-key"/>
</php>
מבנה בדיקות
ארגון קבצי בדיקות
tests/
├── bootstrap.php # Bootstrap של PHPUnit עם mocks של WordPress
├── Helpers/
│ ├── TestCase.php # מחלקת בדיקה בסיסית עם כלי עזר
│ ├── MockAIProvider.php # test double לספק AI
│ └── MockWooCommerce.php # סימולציית WooCommerce
├── AI/
│ └── ProvidersTest.php # בדיקות יחידה לספקי AI
├── Search/
│ └── SemanticSearchTest.php # בדיקות חיפוש סמנטי
├── Chat/
│ └── MessageHandlerTest.php # בדיקות טיפול בהודעות
├── Integration/
│ └── WooCommerceTest.php # בדיקות אינטגרציה עם WooCommerce
└── E2E/
└── ChatFlowTest.php # בדיקות זרימה end-to-end
מוסכמות שמות בדיקות
עקבו אחר תבנית זו לשמות מתודות בדיקה:
public function test_{method_or_feature}_{scenario}_{expected_outcome}()
דוגמאות:
– test_gemini_provider_initialization()
– test_gemini_provider_unavailable_without_key()
– test_rate_limit_simulation()
מחלקות בדיקה בסיסיות
הרחיבו את WooAIChatbotTestsHelpersTestCase לגישה לכלי עזר נפוצים:
namespace WooAIChatbotTestsAI;
use WooAIChatbotTestsHelpersTestCase;
class ProvidersTest extends TestCase {
protected function setUp(): void {
parent::setUp();
// הגדרה ספציפית לבדיקה
}
protected function tearDown(): void {
parent::tearDown();
// ניקוי
}
}
מתודות עזר זמינות ב-TestCase:
| מתודה | מטרה |
|---|---|
setEnv($name, $value) |
הגדרת משתנה סביבה לבדיקה |
createWPError($code, $message) |
יצירת mock WP_Error |
assertIsWPError($actual) |
טענה שהערך הוא WP_Error |
assertNotWPError($actual) |
טענה שהערך אינו WP_Error |
assertWPErrorCode($code, $error) |
טענה על קוד שגיאה ספציפי |
assertArrayHasKeys($keys, $array) |
טענה שלמערך יש את כל המפתחות |
getPrivateProperty($obj, $prop) |
גישה למאפיין פרטי |
callPrivateMethod($obj, $method) |
קריאה למתודה פרטית |
createTempFile($content) |
יצירת קובץ זמני |
הרצת בדיקות
פקודות בדיקה של Composer
# הרצת כל הבדיקות
composer test
# הרצת חבילת בדיקות ספציפית
composer test:unit
composer test:integration
composer test:e2e
# הרצה עם כיסוי
composer test:coverage # דוח HTML
composer test:coverage:text # פלט קונסול
הרצת בדיקה בודדת
# הרצת קובץ בדיקה ספציפי
./vendor/bin/phpunit tests/AI/ProvidersTest.php
# הרצת מתודת בדיקה ספציפית
./vendor/bin/phpunit --filter test_gemini_provider_initialization
# הרצה לפי אנוטציית קבוצה
./vendor/bin/phpunit --group ai
./vendor/bin/phpunit --group woocommerce
כיסוי קוד
דוחות כיסוי נוצרים ל-coverage/:
composer test:coverage
מיקומי פלט:
– coverage/html/index.html – דוח HTML אינטראקטיבי
– coverage/clover.xml – פורמט לאינטגרציית CI
– coverage/junit.xml – פורמט JUnit XML
כתיבת בדיקות
דוגמת בדיקת יחידה
/**
* בדיקת אתחול GeminiProvider
*
* @covers WooAIChatbotAIProvidersGeminiProvider::__construct
* @group ai
* @group providers
*/
public function test_gemini_provider_initialization() {
// Arrange
$this->setEnv( 'GEMINI_API_KEY', 'test-gemini-key' );
// Act
$provider = new GeminiProvider();
// Assert
$this->assertInstanceOf( GeminiProvider::class, $provider );
$this->assertTrue( $provider->is_available() );
$this->assertEquals( 'Google Gemini', $provider->get_provider_name() );
}
דוגמת בדיקת אינטגרציה
/**
* בדיקת חילוץ קונטקסט מוצר
*
* @group integration
* @group woocommerce
*/
public function test_product_context_extraction() {
// Arrange
$product = MockWooCommerce::create_product(
array(
'id' => 123,
'name' => 'Premium Headphones',
'price' => 199.99,
'description' => 'High-quality wireless headphones',
'in_stock' => true,
)
);
// Act
$context = array(
'product_id' => $product->get_id(),
'name' => $product->get_name(),
'price' => $product->get_price(),
'description' => $product->get_description(),
'in_stock' => $product->is_in_stock(),
);
// Assert
$this->assertArrayHasKeys(
array( 'product_id', 'name', 'price', 'description', 'in_stock' ),
$context
);
$this->assertEquals( 'Premium Headphones', $context['name'] );
$this->assertTrue( $context['in_stock'] );
}
אסטרטגיות Mocking
שימוש ב-MockAIProvider:
// תרחיש הצלחה
$mock_provider = new MockAIProvider(
array(
'response' => array(
'content' => 'Test response',
'tokens' => 5,
'model' => 'test-model',
),
)
);
$response = $mock_provider->generate_response( $messages );
$this->assertEquals( 'Test response', $response['content'] );
// תרחיש שגיאה
$mock_provider = new MockAIProvider(
array(
'response' => $this->createWPError( 'rate_limit_exceeded', 'Rate limit exceeded' ),
)
);
$response = $mock_provider->generate_response( array() );
$this->assertIsWPError( $response );
// אימות קריאה
$this->assertTrue( $mock_provider->was_called( 'generate_response' ) );
$this->assertEquals( 1, $mock_provider->get_call_count( 'generate_response' ) );
שימוש ב-MockWooCommerce:
// יצירת מוצרי בדיקה
MockWooCommerce::create_product( array(
'id' => 1,
'name' => 'Product 1',
'price' => 29.99,
));
// הוספה לעגלה
MockWooCommerce::add_to_cart( 1, 2 );
// קבלת עגלה
$cart = MockWooCommerce::get_cart();
// איפוס בין בדיקות
MockWooCommerce::reset();
3. בדיקות JavaScript (Jest)
הגדרה
תצורת Jest
jest.config.js מגדיר תמיכת TypeScript עם כינויי נתיבים:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/assets/src'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/assets/src/$1',
'^@admin/(.*)$': '<rootDir>/assets/src/admin/$1',
'^@chat/(.*)$': '<rootDir>/assets/src/chat/$1',
'^@components/(.*)$': '<rootDir>/assets/src/components/$1',
'^@hooks/(.*)$': '<rootDir>/assets/src/hooks/$1',
'^@utils/(.*)$': '<rootDir>/assets/src/utils/$1',
'.(css|less|scss|sass)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
אינטגרציית Testing Library
jest.setup.js מספק גלובליים של WordPress ותצורה:
import '@testing-library/jest-dom';
// Mock גלובלי של WordPress
global.wp = {
i18n: {
__: (text) => text,
_x: (text) => text,
_n: (single, plural, number) => (number === 1 ? single : plural),
},
hooks: {
addAction: jest.fn(),
addFilter: jest.fn(),
doAction: jest.fn(),
applyFilters: jest.fn(),
},
};
// Mock ל-window.wooAIConfig
global.window.wooAIConfig = {
apiUrl: 'http://localhost/wp-json/woo-ai/v1',
nonce: 'test-nonce',
locale: 'en_US',
isRtl: false,
};
הרצת בדיקות
# הרצת כל בדיקות JavaScript
npm run test
# מצב watch לפיתוח
npm run test:watch
# יצירת דוח כיסוי
npm run test:coverage
כתיבת בדיקות
דוגמת בדיקת קומפוננטה
import { render, screen, fireEvent } from '@testing-library/react';
import { ChatMessage } from '@components/ChatMessage';
describe('ChatMessage', () => {
it('renders user message correctly', () => {
// Arrange
const message = {
id: '1',
role: 'user' as const,
content: 'Hello, I need help with shoes',
timestamp: new Date().toISOString(),
};
// Act
render(<ChatMessage message={message} />);
// Assert
expect(screen.getByText('Hello, I need help with shoes')).toBeInTheDocument();
expect(screen.getByRole('article')).toHaveClass('user-message');
});
it('renders assistant message with products', () => {
// Arrange
const message = {
id: '2',
role: 'assistant' as const,
content: 'Here are some shoes I found',
data: {
products: [
{ id: 1, name: 'Running Shoes', price: '$99.00' },
],
},
timestamp: new Date().toISOString(),
};
// Act
render(<ChatMessage message={message} />);
// Assert
expect(screen.getByText('Running Shoes')).toBeInTheDocument();
expect(screen.getByText('$99.00')).toBeInTheDocument();
});
});
דוגמת בדיקת Hook
import { renderHook, act } from '@testing-library/react';
import { useChat } from '@hooks/useChat';
describe('useChat', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
it('initializes with empty messages', () => {
const { result } = renderHook(() => useChat());
expect(result.current.messages).toEqual([]);
expect(result.current.isLoading).toBe(false);
});
it('sends message and receives response', async () => {
// Mock תגובת API
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
content: 'Here are your results',
type: 'text',
}),
});
const { result } = renderHook(() => useChat());
await act(async () => {
await result.current.sendMessage('Show me laptops');
});
expect(result.current.messages).toHaveLength(2);
expect(result.current.messages[0].role).toBe('user');
expect(result.current.messages[1].role).toBe('assistant');
});
});
דוגמת בדיקת שירות
import { ChatService } from '@utils/ChatService';
describe('ChatService', () => {
let chatService: ChatService;
beforeEach(() => {
chatService = new ChatService({
apiUrl: 'http://localhost/wp-json/woo-ai/v1',
nonce: 'test-nonce',
});
global.fetch = jest.fn();
});
it('sends chat message with correct headers', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ content: 'Response' }),
});
await chatService.send('Hello');
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost/wp-json/woo-ai/v1/chat',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'X-WP-Nonce': 'test-nonce',
}),
})
);
});
it('handles API errors gracefully', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
await expect(chatService.send('Hello')).rejects.toThrow('Network error');
});
});
4. בדיקות E2E (Playwright)
הגדרה
Playwright כלול כתלות לבדיקות E2E מבוססות דפדפן:
# התקנת דפדפני Playwright
npx playwright install
תצורת Playwright
צרו playwright.config.ts לתרחישי E2E של WordPress:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
use: {
baseURL: 'http://localhost:8080',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } },
],
});
כתיבת בדיקות E2E
תבנית Page Object
// e2e/pages/ChatWidgetPage.ts
import { Page, Locator } from '@playwright/test';
export class ChatWidgetPage {
readonly page: Page;
readonly chatButton: Locator;
readonly chatInput: Locator;
readonly sendButton: Locator;
readonly messagesContainer: Locator;
constructor(page: Page) {
this.page = page;
this.chatButton = page.locator('[data-testid="chat-widget-button"]');
this.chatInput = page.locator('[data-testid="chat-input"]');
this.sendButton = page.locator('[data-testid="send-button"]');
this.messagesContainer = page.locator('[data-testid="messages-container"]');
}
async openChat() {
await this.chatButton.click();
await this.page.waitForSelector('[data-testid="chat-input"]');
}
async sendMessage(message: string) {
await this.chatInput.fill(message);
await this.sendButton.click();
}
async waitForResponse() {
await this.page.waitForSelector('[data-testid="assistant-message"]');
}
async getLastMessage(): Promise<string> {
const messages = this.messagesContainer.locator('[data-testid="message"]');
const lastMessage = messages.last();
return lastMessage.textContent() ?? '';
}
}
תרחישים נפוצים
// e2e/chat-flow.spec.ts
import { test, expect } from '@playwright/test';
import { ChatWidgetPage } from './pages/ChatWidgetPage';
test.describe('Chat Widget Flow', () => {
let chatWidget: ChatWidgetPage;
test.beforeEach(async ({ page }) => {
chatWidget = new ChatWidgetPage(page);
await page.goto('/shop');
});
test('opens chat widget and sends message', async ({ page }) => {
await chatWidget.openChat();
await chatWidget.sendMessage('Show me running shoes');
await chatWidget.waitForResponse();
const response = await chatWidget.getLastMessage();
expect(response).toContain('running shoes');
});
test('displays product cards in response', async ({ page }) => {
await chatWidget.openChat();
await chatWidget.sendMessage('I need a laptop under $1000');
await chatWidget.waitForResponse();
const productCards = page.locator('[data-testid="product-card"]');
await expect(productCards).toHaveCount.greaterThan(0);
});
});
5. כלי איכות קוד
ESLint
תצורה
.eslintrc.json מרחיב כללים מומלצים של TypeScript ו-React:
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
פקודות
# Lint עם תיקון אוטומטי
npm run lint
# בדיקה בלבד (ללא שינויים)
npm run lint:check
PHPCS
תקני WordPress
תצורת phpcs.xml אוכפת כללי תחביר ואבטחה של PHP:
<ruleset name="WooAI Chatbot Pro">
<file>./woo-ai-chatbot-pro.php</file>
<file>./includes</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/tests/*</exclude-pattern>
<!-- תחביר ואבטחה -->
<rule ref="Generic.PHP.Syntax"/>
<rule ref="Squiz.PHP.Eval"/>
<rule ref="Generic.PHP.NoSilencedErrors"/>
</ruleset>
פקודות
# הרצת PHPCS
npm run phpcs
# או
composer phpcs
# תיקון אוטומטי של בעיות
npm run phpcs:fix
# או
composer phpcbf
TypeScript
תצורת מצב קפדני
tsconfig.json מאפשר את כל אפשרויות הבדיקה הקפדניות:
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true
}
}
בדיקת טיפוסים
# הרצת בדיקת טיפוסים
npm run typecheck
6. הוקי Pre-commit (Husky)
תצורת Hook
התקנת הוקי Husky:
npm run prepare
זה מריץ husky install ומגדיר הוקי Git ב-.husky/.
הגדרת Lint-staged
הוסיפו ל-package.json:
{
"lint-staged": {
"assets/src/**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"includes/**/*.php": [
"composer phpcs"
]
}
}
הוק Pre-commit
צרו .husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run typecheck
npx lint-staged
npm run test -- --passWithNoTests
7. נתוני בדיקה ו-Fixtures
נתונים לדוגמה
השתמשו ב-MockWooCommerce ליצירת מוצרי בדיקה עקביים:
protected function createTestProducts(): array {
return array(
MockWooCommerce::create_product( array(
'id' => 1,
'name' => 'Running Shoes',
'price' => 129.99,
)),
MockWooCommerce::create_product( array(
'id' => 2,
'name' => 'Hiking Boots',
'price' => 189.99,
)),
);
}
תבניות Factory
class ProductFactory {
private static int $id = 0;
public static function create( array $overrides = array() ): MockProduct {
return MockWooCommerce::create_product( array_merge(
array(
'id' => ++self::$id,
'name' => 'Product ' . self::$id,
'price' => rand( 10, 500 ) + 0.99,
),
$overrides
));
}
public static function createMany( int $count ): array {
return array_map( fn() => self::create(), range( 1, $count ) );
}
}
זריעת בסיס נתונים
לבדיקות אינטגרציה הדורשות מצב בסיס נתונים:
protected function seedTestData(): void {
global $_wp_options;
$_wp_options['woo_ai_settings'] = array(
'primary_provider' => 'gemini',
'fallback_providers' => array( 'openai', 'claude' ),
'temperature' => 0.7,
);
// יצירת מוצרי בדיקה
for ( $i = 1; $i <= 10; ++$i ) {
MockWooCommerce::create_product( array(
'id' => $i,
'name' => "Test Product {$i}",
'price' => $i * 10.00,
));
}
}
8. אינטגרציית CI/CD
GitHub Actions
צרו .github/workflows/tests.yml:
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
php-tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPUnit
run: composer test:coverage:text
- name: Run PHPCS
run: composer phpcs
js-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint:check
- name: Run tests
run: npm run test:coverage
- name: Build
run: npm run build
שערי Deployment
הבדיקות חייבות לעבור לפני deployment. הגדירו כללי הגנת branch:
- בדיקות נדרשות:
php-tests,js-tests - סף כיסוי: כישלון אם הכיסוי יורד מתחת ל-80%
- ללא force pushes: הגנה על branch ראשי
דיווח כיסוי
העלאת כיסוי לשירותים חיצוניים:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/clover.xml
fail_ci_if_error: true
התייחסות מהירה
| משימה | פקודה |
|---|---|
| הרצת כל בדיקות PHP | composer test |
| הרצת בדיקות יחידה PHP | composer test:unit |
| הרצת בדיקות אינטגרציה PHP | composer test:integration |
| הרצת בדיקות E2E של PHP | composer test:e2e |
| דוח כיסוי PHP | composer test:coverage |
| הרצת כל בדיקות JS | npm run test |
| מצב watch של JS | npm run test:watch |
| כיסוי JS | npm run test:coverage |
| בדיקת TypeScript | npm run typecheck |
| ESLint | npm run lint |
| PHPCS | npm run phpcs |
| תיקון בעיות PHPCS | npm run phpcs:fix |
פתרון בעיות
בעיות נפוצות
Bootstrap של PHPUnit נכשל:
– ודאו ש-composer install הושלם בהצלחה
– ודאו שקובץ .env קיים (הועתק מ-.env.example)
שגיאות פתרון מודולים ב-Jest:
– הריצו npm ci להתקנה נקייה של תלויות
– ודאו שכינויי נתיבים ב-jest.config.js תואמים ל-tsconfig.json
כיסוי מתחת לסף:
– הוסיפו בדיקות לענפים לא מכוסים
– עיינו ב-collectCoverageFrom בתצורת Jest להחרגות
Mock לא מתאפס בין בדיקות:
– קראו ל-MockWooCommerce::reset() ב-setUp()
– ודאו שנקראים קודם ל-setUp() של ההורה

