mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 13:55:00 +00:00
Add component tree renderer, hooks, and tests
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
169
tests/e2e/playbooks.spec.ts
Normal file
169
tests/e2e/playbooks.spec.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Playwright E2E tests using playbooks from features.json
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { getAllPlaywrightPlaybooks, type PlaywrightPlaybook, type PlaywrightStep } from '@/utils/featureConfig';
|
||||
|
||||
// Execute a single playbook step
|
||||
async function executeStep(page: any, step: PlaywrightStep, variables: Record<string, string> = {}) {
|
||||
// Replace variables in step values
|
||||
const replaceVars = (value?: string) => {
|
||||
if (!value) return value;
|
||||
return Object.entries(variables).reduce((acc, [key, val]) => {
|
||||
return acc.replace(new RegExp(`{{${key}}}`, 'g'), val);
|
||||
}, value);
|
||||
};
|
||||
|
||||
switch (step.action) {
|
||||
case 'goto':
|
||||
await page.goto(replaceVars(step.url));
|
||||
break;
|
||||
|
||||
case 'click':
|
||||
await page.click(replaceVars(step.selector));
|
||||
break;
|
||||
|
||||
case 'fill':
|
||||
await page.fill(replaceVars(step.selector), replaceVars(step.value) || '');
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
await page.selectOption(replaceVars(step.selector), replaceVars(step.value) || '');
|
||||
break;
|
||||
|
||||
case 'wait':
|
||||
await page.waitForTimeout(step.timeout || 1000);
|
||||
break;
|
||||
|
||||
case 'expect':
|
||||
if (step.text === 'visible' && step.selector) {
|
||||
await expect(page.locator(replaceVars(step.selector))).toBeVisible();
|
||||
} else if (step.text === 'redirected' && step.url) {
|
||||
await expect(page).toHaveURL(replaceVars(step.url));
|
||||
} else if (step.text && step.selector) {
|
||||
await expect(page.locator(replaceVars(step.selector))).toHaveText(replaceVars(step.text) || '');
|
||||
} else if (step.text === '401') {
|
||||
// Check for 401 status
|
||||
const response = await page.waitForResponse((resp: any) => resp.status() === 401);
|
||||
expect(response.status()).toBe(401);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'screenshot':
|
||||
if (step.selector) {
|
||||
await page.locator(replaceVars(step.selector)).screenshot();
|
||||
} else {
|
||||
await page.screenshot();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown action: ${step.action}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute a full playbook
|
||||
async function executePlaybook(page: any, playbook: PlaywrightPlaybook, variables: Record<string, string> = {}) {
|
||||
for (const step of playbook.steps) {
|
||||
await executeStep(page, step, variables);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute playbook cleanup steps
|
||||
async function cleanupPlaybook(page: any, playbook: PlaywrightPlaybook, variables: Record<string, string> = {}) {
|
||||
if (playbook.cleanup) {
|
||||
for (const step of playbook.cleanup) {
|
||||
try {
|
||||
await executeStep(page, step, variables);
|
||||
} catch (err) {
|
||||
console.warn('Cleanup step failed:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all playbooks
|
||||
const playbooks = getAllPlaywrightPlaybooks();
|
||||
|
||||
// Test: API Security Check
|
||||
test.describe('API Security', () => {
|
||||
const playbook = playbooks.securityCheck;
|
||||
|
||||
if (playbook) {
|
||||
test(playbook.name, async ({ page }) => {
|
||||
await executePlaybook(page, playbook);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test: Query Builder
|
||||
test.describe('Query Builder', () => {
|
||||
const playbook = playbooks.queryBuilder;
|
||||
|
||||
if (playbook) {
|
||||
test.skip(playbook.name, async ({ page }) => {
|
||||
// This test requires authentication, skipping for now
|
||||
const variables = {
|
||||
tableName: 'users',
|
||||
columnName: 'id',
|
||||
};
|
||||
|
||||
await executePlaybook(page, playbook, variables);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test: Create Table
|
||||
test.describe('Table Management', () => {
|
||||
const playbook = playbooks.createTable;
|
||||
|
||||
if (playbook) {
|
||||
test.skip(playbook.name, async ({ page }) => {
|
||||
// This test requires authentication, skipping for now
|
||||
const variables = {
|
||||
tableName: 'test_table_' + Date.now(),
|
||||
};
|
||||
|
||||
await executePlaybook(page, playbook, variables);
|
||||
|
||||
// Cleanup
|
||||
await cleanupPlaybook(page, playbook, variables);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test: Add Column
|
||||
test.describe('Column Management', () => {
|
||||
const playbook = playbooks.addColumn;
|
||||
|
||||
if (playbook) {
|
||||
test.skip(playbook.name, async ({ page }) => {
|
||||
// This test requires authentication and an existing table, skipping for now
|
||||
const variables = {
|
||||
tableName: 'users',
|
||||
columnName: 'test_column_' + Date.now(),
|
||||
dataType: 'VARCHAR',
|
||||
};
|
||||
|
||||
await executePlaybook(page, playbook, variables);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test: Create Index
|
||||
test.describe('Index Management', () => {
|
||||
const playbook = playbooks.createIndex;
|
||||
|
||||
if (playbook) {
|
||||
test.skip(playbook.name, async ({ page }) => {
|
||||
// This test requires authentication and an existing table, skipping for now
|
||||
const variables = {
|
||||
tableName: 'users',
|
||||
columnName: 'id',
|
||||
indexName: 'idx_test_' + Date.now(),
|
||||
};
|
||||
|
||||
await executePlaybook(page, playbook, variables);
|
||||
});
|
||||
}
|
||||
});
|
||||
195
tests/unit/componentTreeRenderer.test.tsx
Normal file
195
tests/unit/componentTreeRenderer.test.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Unit tests for component tree renderer
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { renderComponentNode } from '@/utils/componentTreeRenderer';
|
||||
import type { ComponentNode } from '@/utils/featureConfig';
|
||||
|
||||
describe('componentTreeRenderer', () => {
|
||||
it('should render a simple Box component', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Box',
|
||||
props: {
|
||||
sx: { p: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
const context = { data: {}, actions: {}, state: {} };
|
||||
const result = renderComponentNode(node, context);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type.toString()).toContain('Box');
|
||||
});
|
||||
|
||||
it('should render Typography with text prop', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Typography',
|
||||
props: {
|
||||
variant: 'h5',
|
||||
text: 'Hello World',
|
||||
},
|
||||
};
|
||||
|
||||
const context = { data: {}, actions: {}, state: {} };
|
||||
const result = renderComponentNode(node, context);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.props.variant).toBe('h5');
|
||||
});
|
||||
|
||||
it('should interpolate template variables', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Typography',
|
||||
props: {
|
||||
text: '{{data.message}}',
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
data: { message: 'Test Message' },
|
||||
actions: {},
|
||||
state: {},
|
||||
};
|
||||
|
||||
const result = renderComponentNode(node, context);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Box',
|
||||
children: [
|
||||
{
|
||||
component: 'Typography',
|
||||
props: {
|
||||
text: 'Child 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Typography',
|
||||
props: {
|
||||
text: 'Child 2',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const context = { data: {}, actions: {}, state: {} };
|
||||
const result = renderComponentNode(node, context);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(Array.isArray(result?.props.children)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle condition and not render when false', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Box',
|
||||
condition: 'data.show',
|
||||
props: {
|
||||
sx: { p: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
data: { show: false },
|
||||
actions: {},
|
||||
state: {},
|
||||
};
|
||||
|
||||
const result = renderComponentNode(node, context);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle condition and render when true', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Box',
|
||||
condition: 'data.show',
|
||||
props: {
|
||||
sx: { p: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
data: { show: true },
|
||||
actions: {},
|
||||
state: {},
|
||||
};
|
||||
|
||||
const result = renderComponentNode(node, context);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle forEach loops', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Box',
|
||||
forEach: 'data.items',
|
||||
children: [
|
||||
{
|
||||
component: 'Typography',
|
||||
props: {
|
||||
text: '{{item.name}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const context = {
|
||||
data: {
|
||||
items: [
|
||||
{ name: 'Item 1' },
|
||||
{ name: 'Item 2' },
|
||||
{ name: 'Item 3' },
|
||||
],
|
||||
},
|
||||
actions: {},
|
||||
state: {},
|
||||
};
|
||||
|
||||
const result = renderComponentNode(node, context);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map onClick to action function', () => {
|
||||
const mockAction = vi.fn();
|
||||
const node: ComponentNode = {
|
||||
component: 'Button',
|
||||
props: {
|
||||
text: 'Click Me',
|
||||
onClick: 'handleClick',
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
data: {},
|
||||
actions: { handleClick: mockAction },
|
||||
state: {},
|
||||
};
|
||||
|
||||
const result = renderComponentNode(node, context);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.props.onClick).toBe(mockAction);
|
||||
});
|
||||
|
||||
it('should handle nested template interpolation', () => {
|
||||
const node: ComponentNode = {
|
||||
component: 'Typography',
|
||||
props: {
|
||||
text: 'User: {{data.user.name}}, Age: {{data.user.age}}',
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
data: {
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
state: {},
|
||||
};
|
||||
|
||||
const result = renderComponentNode(node, context);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
141
tests/unit/useApiCall.test.ts
Normal file
141
tests/unit/useApiCall.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Unit tests for useApiCall hook
|
||||
*/
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { useApiCall } from '@/hooks/useApiCall';
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('useApiCall', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize with correct default state', () => {
|
||||
const { result } = renderHook(() => useApiCall());
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle successful API call', async () => {
|
||||
const mockData = { message: 'Success', result: 42 };
|
||||
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApiCall());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('/api/test');
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle API error', async () => {
|
||||
const errorMessage = 'Request failed';
|
||||
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: async () => ({ error: errorMessage }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApiCall());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.execute('/api/test');
|
||||
} catch (err) {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('should set loading state during API call', async () => {
|
||||
(global.fetch as any).mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(() => resolve({
|
||||
ok: true,
|
||||
json: async () => ({ data: 'test' }),
|
||||
}), 100))
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useApiCall());
|
||||
|
||||
act(() => {
|
||||
result.current.execute('/api/test');
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle POST request with body', async () => {
|
||||
const requestBody = { name: 'test', value: 123 };
|
||||
const mockData = { success: true };
|
||||
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApiCall());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('/api/test', {
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
});
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
});
|
||||
|
||||
it('should reset state', async () => {
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: 'test' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApiCall());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('/api/test');
|
||||
});
|
||||
|
||||
expect(result.current.data).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user