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
44 changes: 44 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: E2E Tests

on:
push:
branches:
- main
- master
- claude/research-simulation-framework-YKcXK
pull_request:
workflow_dispatch:

jobs:
e2e:
name: Playwright E2E
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Build site
run: npm run build

- name: Run E2E tests
run: npx playwright test

- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 14
97 changes: 97 additions & 0 deletions e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { test, expect } from '@playwright/test';

const BASE = '/computational-relationship-dynamic';

const pages = [
{ path: `${BASE}/`, title: /Computational Relationship Dynamics/ },
{ path: `${BASE}/methodology`, title: /Methodology/ },
{ path: `${BASE}/simulation`, title: /Simulation/ },
{ path: `${BASE}/results`, title: /Results/ },
];

test.describe('All pages load without 404s', () => {
for (const { path, title } of pages) {
test(`${path} returns 200 and correct title`, async ({ page }) => {
const response = await page.goto(path);
expect(response?.status()).toBe(200);
await expect(page).toHaveTitle(title);
});
}
});

test.describe('Navigation links use correct base path', () => {
test('all nav links point inside /computational-relationship-dynamic', async ({ page }) => {
await page.goto(`${BASE}/`);

const links = page.locator('nav.site-nav a');
const count = await links.count();
expect(count).toBeGreaterThan(0);

for (let i = 0; i < count; i++) {
const href = await links.nth(i).getAttribute('href');
expect(href, `Nav link ${i} has wrong href: ${href}`).toMatch(
/^\/computational-relationship-dynamic/
);
}
});

test('CTA buttons on home page point inside /computational-relationship-dynamic', async ({ page }) => {
await page.goto(`${BASE}/`);

const ctaLinks = page.locator('a.btn');
const count = await ctaLinks.count();
expect(count).toBeGreaterThan(0);

for (let i = 0; i < count; i++) {
const href = await ctaLinks.nth(i).getAttribute('href');
expect(href, `CTA button ${i} has wrong href: ${href}`).toMatch(
/^\/computational-relationship-dynamic/
);
}
});

test('"Open Simulation" button on results page points inside /computational-relationship-dynamic', async ({ page }) => {
await page.goto(`${BASE}/results`);
const btn = page.locator('a.btn.btn-primary').first();
const href = await btn.getAttribute('href');
expect(href).toMatch(/^\/computational-relationship-dynamic/);
});
});

test.describe('Nav links actually navigate to correct pages', () => {
test('Methodology link navigates correctly', async ({ page }) => {
await page.goto(`${BASE}/`);
await page.locator('nav a', { hasText: 'Methodology' }).click();
await expect(page).toHaveURL(/\/computational-relationship-dynamic\/methodology/);
await expect(page).toHaveTitle(/Methodology/);
});

test('Simulation link navigates correctly', async ({ page }) => {
await page.goto(`${BASE}/`);
await page.locator('nav a', { hasText: 'Simulation' }).click();
await expect(page).toHaveURL(/\/computational-relationship-dynamic\/simulation/);
await expect(page).toHaveTitle(/Simulation/);
});

test('Results link navigates correctly', async ({ page }) => {
await page.goto(`${BASE}/`);
await page.locator('nav a', { hasText: 'Results' }).click();
await expect(page).toHaveURL(/\/computational-relationship-dynamic\/results/);
await expect(page).toHaveTitle(/Results/);
});
});

test.describe('Assets load without 404', () => {
test('simulation page has no failed asset requests', async ({ page }) => {
const failed: string[] = [];
page.on('response', (response) => {
if (response.status() === 404) failed.push(response.url());
});

await page.goto(`${BASE}/simulation`);
// Wait for network idle so all dynamic imports finish
await page.waitForLoadState('networkidle');

expect(failed, `404s on simulation page:\n${failed.join('\n')}`).toHaveLength(0);
});
});
69 changes: 69 additions & 0 deletions package-lock.json

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

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/svelte": "^7.2.5",
Expand All @@ -18,5 +19,8 @@
"svelte": "^5.53.0",
"three": "^0.183.1",
"typescript": "^5.9.3"
},
"devDependencies": {
"@playwright/test": "^1.58.2"
}
}
26 changes: 26 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'list',
use: {
baseURL: 'http://localhost:4321',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run preview',
port: 4321,
reuseExistingServer: !process.env.CI,
timeout: 30000,
},
});
24 changes: 18 additions & 6 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
---
import { execSync } from 'child_process';

export interface Props {
title?: string;
description?: string;
Expand All @@ -7,6 +9,10 @@ const {
title = 'Computational Relationship Dynamics',
description = 'A collision-based framework for simulating romantic pair bonding at scale.',
} = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');

let commit = 'unknown';
try { commit = execSync('git rev-parse --short HEAD').toString().trim(); } catch {}
---

<!doctype html>
Expand All @@ -16,7 +22,7 @@ const {
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<title>{title}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href={`${base}favicon.svg`} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
Expand All @@ -26,12 +32,12 @@ const {
<body>
<nav class="site-nav">
<div class="container nav-inner">
<a href="/" class="nav-logo">⚛ CRD</a>
<a href={base} class="nav-logo">⚛ CRD</a>
<ul class="nav-links">
<li><a href="/">Abstract</a></li>
<li><a href="/methodology">Methodology</a></li>
<li><a href="/simulation">Simulation</a></li>
<li><a href="/results">Results</a></li>
<li><a href={base}>Abstract</a></li>
<li><a href={`${base}methodology`}>Methodology</a></li>
<li><a href={`${base}simulation`}>Simulation</a></li>
<li><a href={`${base}results`}>Results</a></li>
</ul>
</div>
</nav>
Expand All @@ -45,6 +51,7 @@ const {
<p>
Collision-Based Modeling of Romantic Pair Bonding &mdash;
Computational Relationship Dynamics Framework &copy; 2025
&ensp;<code>{commit}</code>
</p>
</div>
</footer>
Expand All @@ -67,4 +74,9 @@ const {
text-align: center;
margin: 0;
}
footer code {
font-family: var(--font-mono);
font-size: 0.75rem;
opacity: 0.6;
}
</style>
7 changes: 4 additions & 3 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
---

<BaseLayout title="CRD — Computational Relationship Dynamics">
Expand All @@ -17,8 +18,8 @@ import BaseLayout from '../layouts/BaseLayout.astro';
to the science of human connection.
</p>
<div class="hero-actions">
<a href="/simulation" class="btn btn-primary">▶ Launch Simulation</a>
<a href="/methodology" class="btn btn-outline">Read Methodology</a>
<a href={`${base}simulation`} class="btn btn-primary">▶ Launch Simulation</a>
<a href={`${base}methodology`} class="btn btn-outline">Read Methodology</a>
</div>

<div class="hero-stats">
Expand Down Expand Up @@ -168,7 +169,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
Adjust population size, pairing strategy, stress levels, and equation parameters —
then watch the collision events unfold in real time.
</p>
<a href="/simulation" class="btn btn-primary" style="font-size:1.05rem;padding:0.75rem 2rem;">
<a href={`${base}simulation`} class="btn btn-primary" style="font-size:1.05rem;padding:0.75rem 2rem;">
▶ Open Simulation Engine
</a>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/results.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
---

<BaseLayout title="Results — CRD">
Expand Down Expand Up @@ -139,7 +140,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';

<div class="cta-box">
<p>Explore these patterns yourself in the interactive simulation engine.</p>
<a href="/simulation" class="btn btn-primary">▶ Open Simulation</a>
<a href={`${base}simulation`} class="btn btn-primary">▶ Open Simulation</a>
</div>
</div>
</BaseLayout>
Expand Down
Loading