Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci-and-labels.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ci": "npm run build && PLAYWRIGHT_MODE=production playwright test",
"typecheck": "tsc --noEmit",
"cli": "node --loader ts-node/esm cli/index.ts"
},
Expand Down
30 changes: 25 additions & 5 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,57 @@
import { defineConfig, devices } from '@playwright/test';

/**
* See https://playwright.dev/docs/test-configuration.
*/
const isProduction = process.env.PLAYWRIGHT_MODE === 'production';
const PORT = isProduction ? 4173 : 5173;
const baseURL = `http://localhost:${PORT}`;

export default defineConfig({
testDir: './tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
timeout: 30000,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
Comment thread
d-oit marked this conversation as resolved.
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
outputDir: 'test-results',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL: 'http://localhost:5173',
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

{
name: 'mobile',
use: { ...devices['iPhone 13'] },
},

{
name: 'tablet',
use: { ...devices['iPad Pro 11'] },
},
],

/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
command: isProduction ? 'npm run preview' : 'npm run dev',
url: baseURL,
reuseExistingServer: !process.env.CI,
},
});
3 changes: 2 additions & 1 deletion public/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS claims (
evidence TEXT,
confidence REAL DEFAULT 1.0,
source TEXT,
verification_status TEXT DEFAULT 'unverified',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
Expand Down Expand Up @@ -48,7 +49,7 @@ CREATE TABLE IF NOT EXISTS links (

-- Graph Snapshots: Saved states of the knowledge graph
CREATE TABLE IF NOT EXISTS graph_snapshots (
id UUID PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
id UUID PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6)))),
name TEXT NOT NULL,
nodes_json TEXT NOT NULL,
edges_json TEXT NOT NULL,
Expand Down
5 changes: 1 addition & 4 deletions src/db/connection-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ interface WorkerEntry {
export class ConnectionPool {
private workers: WorkerEntry[] = [];
private queue: PoolRequest[] = [];
private workerUrl: URL;
private initialized = false;
private timeoutMs = DEFAULT_TIMEOUT_MS;
private poolSize: number;
private schema: string | undefined;

constructor(poolSize: number = DEFAULT_POOL_SIZE) {
this.poolSize = Math.max(1, Math.min(poolSize, 16));
// We use a relative path that Vite will handle
this.workerUrl = new URL('./db-worker.ts', import.meta.url);
}

/**
Expand Down Expand Up @@ -65,7 +62,7 @@ export class ConnectionPool {
}

private createWorker(): WorkerEntry {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 'this' to be used by class method 'createWorker'


If a class method does not use this, it can sometimes be made into a static function. If you do convert the method into a static function, instances of the class that call that particular method have to be converted to a static call as well (MyClass.callStaticMethod())

const worker = new Worker(this.workerUrl, { type: 'module' });
const worker = new Worker(new URL('./db-worker.ts', import.meta.url), { type: 'module' });
return {
worker,
busy: false,
Expand Down
1 change: 1 addition & 0 deletions src/features/graph/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const GraphView: React.FC<Props> = ({
entities: entities.filter((e: Entity) => neighborIds.has(e.id!)),
links: links.filter((l: Link) => neighborIds.has(l.source_id) && neighborIds.has(l.target_id))
});
return Promise.resolve();
};

jobCoordinator.registerHandler('recompute-neighborhood', handler);
Expand Down
92 changes: 74 additions & 18 deletions tests/e2e/features.spec.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,123 @@
import { test, expect } from '@playwright/test';
import { ensureNavVisible } from './utils';

test.describe('Entity CRUD', () => {
test('User can create a new entity', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Editor")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await expect(page.locator('.editor-container')).toBeVisible();
await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Editor', visible: true }).first();
await btn.click();

await expect(page.locator('.editor-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Editor', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });
});

test('User can view entity details', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Editor")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Editor', visible: true }).first();
await btn.click();

await expect(page.locator('.editor-container')).toBeVisible({ timeout: 15000 });

await expect(page.locator('.editor-container')).toBeVisible();
await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Editor', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });
});
});

test.describe('Claims', () => {
test('User can add a claim to an entity', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Editor")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await expect(page.locator('.editor-container')).toBeVisible();
await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Editor', visible: true }).first();
await btn.click();

await expect(page.locator('.editor-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Editor', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });
});
});

test.describe('Search', () => {
test('User can search via chat', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Chat")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Chat', visible: true }).first();
await btn.click();

await expect(page.locator('.chat-view')).toBeVisible();
await expect(page.locator('.ask-surface')).toBeVisible({ timeout: 15000 });

const input = page.locator('input[placeholder*="Search"]');
await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Chat', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });

const input = page.locator('input[placeholder*="Ask"]');
await input.fill('test');
await page.keyboard.press('Enter');

await expect(page.locator('.message')).toBeVisible();
await expect(page.locator('.message-wrapper').first()).toBeVisible({ timeout: 15000 });
});
});

test.describe('Graph', () => {
test('Graph visualization renders', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Graph")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Graph', visible: true }).first();
await btn.click();

await expect(page.locator('.graph-container')).toBeVisible();
await expect(page.locator('.main-content')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Graph', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });
});

test('Graph has control buttons', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Graph")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Graph', visible: true }).first();
await btn.click();

await expect(page.locator('button[title*="Zoom"]')).toBeVisible();
await expect(page.locator('button[title*="Fit"]')).toBeVisible();
await expect(page.locator('.main-content')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Graph', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });
});
});

test.describe('Mind Map', () => {
test('Mind map view renders', async ({ page }) => {
await page.goto('/');
await page.click('button:has-text("Mind Map")');
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);

const btn = page.locator('.nav-button').filter({ hasText: 'Mind Map', visible: true }).first();
await btn.click();

await expect(page.locator('.mindmap-container')).toBeVisible();
await expect(page.locator('.main-content')).toBeVisible({ timeout: 15000 });

await ensureNavVisible(page);
await expect(page.locator('.nav-button').filter({ hasText: 'Mind Map', visible: true }).first()).toHaveAttribute('aria-current', 'page', { timeout: 10000 });
});
});
});
45 changes: 45 additions & 0 deletions tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { test, expect } from '@playwright/test';
import { ensureNavVisible } from './utils';

test.describe('Production Smoke Test', () => {
test('should boot and allow core navigation', async ({ page }) => {
// Go to the home page
await page.goto('/');

// Verify the app container is visible (using layout-container as per App.tsx)
const layout = page.locator('.layout-container');
// Wait longer for boot to complete in CI environment
await expect(layout).toBeVisible({ timeout: 15000 });

// Check for responsive state and open menu if needed
await ensureNavVisible(page);

// Verify core navigation buttons are present
const navButtons = page.locator('.nav-button');
await expect(navButtons.filter({ visible: true }).first()).toBeVisible();

// Verify Cross-Origin headers on the main document
const response = await page.request.get('/');
const headers = response.headers();

// In production mode (vite preview), headers should be present
if (process.env.PLAYWRIGHT_MODE === 'production') {
expect(headers['cross-origin-opener-policy']).toBe('same-origin');
expect(headers['cross-origin-embedder-policy']).toBe('require-corp');
}

// Perform a core navigation: click 'Graph'
const graphButton = page.locator('.nav-button').filter({ hasText: 'Graph', visible: true }).first();
await graphButton.click();

// Verify we reached the graph view area
await expect(page.locator('.main-content')).toBeVisible();

// On mobile/tablet, the menu closes after navigation, so we need to open it again to check the active state
await ensureNavVisible(page);

const activeGraphButton = page.locator('.nav-button').filter({ hasText: 'Graph' }).filter({ visible: true }).first();
await expect(activeGraphButton).toHaveAttribute('aria-current', 'page');
await expect(activeGraphButton).toHaveClass(/active/);
});
});
15 changes: 15 additions & 0 deletions tests/e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Page } from '@playwright/test';

/**
* Ensures the navigation menu is visible on responsive layouts.
* If the navigation buttons are not visible, it attempts to click the 'Open menu' button.
*/
export async function ensureNavVisible(page: Page) {
const navButton = page.locator('.nav-button').filter({ hasText: 'Editor' });
if (!(await navButton.isVisible())) {
const menuButton = page.getByLabel('Open menu');
if (await menuButton.isVisible()) {
await menuButton.click();
}
}
}
Comment on lines +7 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

21 changes: 12 additions & 9 deletions tests/e2e/ux-navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
import { test, expect } from '@playwright/test';
import { ensureNavVisible } from './utils';

test('sidebar navigation uses semantic buttons and has correct aria-current', async ({ page }) => {
await page.goto('/');

// Wait for the app to be ready
await expect(page.locator('.layout')).toBeVisible();
await expect(page.locator('.layout-container')).toBeVisible({ timeout: 10000 });

await ensureNavVisible(page);

const navButtons = page.locator('.nav-button');
await expect(navButtons).toHaveCount(4);
// On mobile/tablet, both the SidebarNav and Header might have elements,
// but we specifically check for the visible ones.
await expect(navButtons.filter({ visible: true })).toHaveCount(8);

// Check the first button (Editor) - it should be active by default
const editorButton = navButtons.nth(0);
await expect(editorButton).toHaveText('Editor');
const editorButton = navButtons.filter({ hasText: 'Editor', visible: true }).first();
await expect(editorButton).toHaveAttribute('aria-current', 'page');
await expect(editorButton).toHaveClass(/active/);

// Click on Graph button
const graphButton = navButtons.nth(1);
await expect(graphButton).toHaveText('Graph');
const graphButton = navButtons.filter({ hasText: 'Graph', visible: true }).first();
await expect(graphButton).not.toHaveAttribute('aria-current', 'page');

await graphButton.click();

// Menu closes after click on responsive layouts, reopen it to check state
await ensureNavVisible(page);

// Now Graph should be active
await expect(graphButton).toHaveAttribute('aria-current', 'page');
await expect(graphButton).toHaveClass(/active/);
await expect(editorButton).not.toHaveAttribute('aria-current', 'page');
await expect(editorButton).not.toHaveClass(/active/);

// Verify focus-visible state (simulate keyboard navigation)
await page.keyboard.press('Tab'); // This might focus something else first, let's focus a button specifically
await graphButton.focus();
// We can't easily test :focus-visible via Playwright locators without complex CSS checks,
// but we can ensure it is focusable.
await expect(graphButton).toBeFocused();
});
6 changes: 6 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,10 @@ export default defineConfig({
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
preview: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
});
Loading