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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,4 @@ tests/ # unit/ (Bun), integration/ (Bun + RTL/jsdom), e2e/

## Current Test Count

~271 unit/integration + 81 E2E (three browsers + basePath leg).
~294 unit/integration + 112 E2E (three browsers + basePath leg).
2 changes: 1 addition & 1 deletion app/about/page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function AboutPage() {
return (
<section>
<section className="content-page">
<h1>About RefactorFirst</h1>
<p>RefactorFirst is a static analysis tool that identifies which classes in your Java
codebase you should refactor first, ranked by cost-benefit. It is based on the paper
Expand Down
2 changes: 1 addition & 1 deletion app/api/page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function ApiPage() {
return (
<section>
<section className="content-page">
<h1>API for Tool Integrations</h1>

<h2>Fetching reports programmatically</h2>
Expand Down
2 changes: 1 addition & 1 deletion app/documentation/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from 'next/link';

export default function DocumentationPage() {
return (
<section>
<section className="content-page">
<h1>Documentation</h1>

<h2>Generating Reports</h2>
Expand Down
2 changes: 1 addition & 1 deletion app/examples/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from 'next/link';

export default function ExamplesPage() {
return (
<section>
<section className="content-page">
<h1>Example Reports</h1>
<p>See RefactorFirst reports for real projects. Each link opens the live report
rendered from that repository.</p>
Expand Down
2 changes: 1 addition & 1 deletion app/faq/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from 'next/link';

export default function FaqPage() {
return (
<section>
<section className="content-page">
<h1>Frequently Asked Questions</h1>

<h2>What do the different priority colors mean?</h2>
Expand Down
2 changes: 1 addition & 1 deletion app/feedback/page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function FeedbackPage() {
return (
<section>
<section className="content-page">
<h1>Feedback</h1>
<p>We welcome your feedback! The best place to share ideas, report bugs or request
features is the RefactorFirst project on GitHub.</p>
Expand Down
2 changes: 1 addition & 1 deletion app/getting-started/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import WorkflowSample from '../../components/workflow-sample';

export default function GettingStartedPage() {
return (
<section>
<section className="content-page">
<h1>Getting Started</h1>

<h2>1. Configure the RefactorFirst Maven plugin</h2>
Expand Down
49 changes: 48 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ main:focus {
outline: none;
}

/* mvp.css makes every `section` a wrapping flex row with no gap and center
justification (`section { display: flex; flex-wrap: wrap; justify-content:
var(--justify-important) }`). It targets the mvp `section > aside` card
pattern, but our pages use sections as plain prose containers — so headings,
paragraphs, lists and code blocks were laid out side by side as flex items
and smashed together. mvp.css is loaded after this bundle, so the reset must
win on specificity: `main section` (0-0-2) beats the bare `section`
selector (0-0-1) regardless of cascade order. */
main section {
display: block;
}

/* Prose pages: roughly 75ch measure, centered in the wide main column. */
.content-page {
max-width: 48rem;
margin: 0 auto;
}

/* Landing sections (CTA, featured repos) that keep centered text now that
sections no longer flex-center their children. */
.section-center {
text-align: center;
}

.hero {
text-align: center;
padding: 2.5rem 1rem;
Expand Down Expand Up @@ -225,11 +249,22 @@ pre {

.hero-search {
position: relative;
max-width: 480px;
/* Exactly half the rendered width of the top menu (#top-menu is capped by
mvp.css at --width-content = 1080px, full width below that) and centered
under it. */
width: min(540px, 50vw);
margin: 1rem auto;
text-align: left;
}

.hero-search input {
width: 100%;
box-sizing: border-box;
padding: 0.35rem 0.6rem;
border: 1px solid #c6d2dc;
border-radius: var(--radius);
}

/* ---- Repository grid ---- */
.repo-grid,
.featured-repos ul {
Expand Down Expand Up @@ -281,6 +316,17 @@ pre {
color: #fff;
}

/* ---- User listing (/user) ---- */
.repo-listing h1,
.repo-listing .listing-subtitle {
text-align: center;
}

.listing-subtitle {
margin-top: 0.25rem;
color: var(--muted-color);
}

/* ---- Forms / add-repo ---- */
.user-info {
display: flex;
Expand All @@ -297,6 +343,7 @@ pre {
display: grid;
gap: 0.5rem;
max-width: 420px;
margin: 0 auto;
}

.form-status.error {
Expand Down
2 changes: 1 addition & 1 deletion app/privacy-policy/page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function PrivacyPolicyPage() {
return (
<section>
<section className="content-page">
<h1>Privacy Policy</h1>

<h2>Data Collected</h2>
Expand Down
2 changes: 1 addition & 1 deletion app/terms-of-service/page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function TermsOfServicePage() {
return (
<section>
<section className="content-page">
<h1>Terms of Service</h1>

<h2>Acceptable use</h2>
Expand Down
4 changes: 2 additions & 2 deletions components/landing.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export default function Landing({ repositories }) {
<p>Know which parts of your codebase to refactor first. Search for a repository to see its report.</p>
</section>
<HeroSearch repositories={repositories} />
<section>
<section className="section-center">
<Link href="/add-repo" className="cta">Add My Repo</Link>
</section>
<section>
<section className="section-center">
<h2>Featured Repositories</h2>
<div className="featured-repos">
<ul>
Expand Down
2 changes: 1 addition & 1 deletion components/repo-submission-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function RepoSubmissionForm({ onExternalRedirect }) {
const label = platformLabel(environment);

return (
<section className="add-repo-page">
<section className="add-repo-page content-page">
<h1>Add Your Repository</h1>
<p className="info">
Only repositories with a <code>.refactorfirst/refactor-first.json</code> file
Expand Down
4 changes: 3 additions & 1 deletion playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@ export default defineConfig({
command: 'bun run build && python3 scripts/serve-out.py',
port: 8003,
timeout: 300 * 1000,
// Reuse a locally started server only when explicitly requested.
reuseExistingServer: process.env.PLAYWRIGHT_REUSE_SERVER === 'true',
},
});
});
90 changes: 90 additions & 0 deletions tests/e2e/layout.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Layout cleanup (plans/layout-cleanup-plan.md): mvp.css makes every
// `section` a wrapping flex row with no gap, which smashed prose children
// together (e.g. on /getting-started the <h1> and the first <h2> shared one
// line). These tests pin the corrected behavior: block flow — each child of a
// content section starts strictly below the previous child — plus a bounded,
// centered content column.
import { test, expect } from '@playwright/test';

// Returns, for each <section> matching `selector`, the client rects of its
// direct children (in DOM order).
function sectionChildRects(selector) {
return Array.from(document.querySelectorAll(selector)).map(section =>
Array.from(section.children).map(el => el.getBoundingClientRect())
);
}

// Every child must start strictly below the previous child's bottom
// (2px tolerance for sub-pixel rounding).
function expectStacked(rects, context) {
const failures = [];
for (let i = 1; i < rects.length; i++) {
if (rects[i].top < rects[i - 1].bottom - 2) {
failures.push(`child ${i - 1} (bottom ${rects[i - 1].bottom}) and child ${i} (top ${rects[i].top}) share a row`);
}
}
expect(failures, `${context}: children must stack vertically`).toEqual([]);
}

const CONTENT_PAGES = ['/about/', '/getting-started/', '/add-repo/'];

test.describe('content page layout', () => {
test.use({ viewport: { width: 1280, height: 900 } });

for (const path of CONTENT_PAGES) {
test(`${path} stacks section children vertically`, async ({ page }) => {
await page.goto(path);
const sections = await page.evaluate(sectionChildRects, 'main section');
expect(sections.length).toBeGreaterThan(0);
for (const rects of sections) expectStacked(rects, path);
});

test(`${path} keeps prose in a bounded, centered column`, async ({ page }) => {
await page.goto(path);
const box = await page.locator('main section.content-page').boundingBox();
expect(box).toBeTruthy();
expect(box.width).toBeLessThanOrEqual(800);
const left = box.x;
const right = 1280 - box.x - box.width;
expect(Math.abs(left - right)).toBeLessThanOrEqual(8);
});
}

test('getting-started regression: title and first heading do not share a row', async ({ page }) => {
await page.goto('/getting-started/');
const [h1, firstH2] = await Promise.all([
page.locator('main section h1').boundingBox(),
page.locator('main section h2').first().boundingBox(),
]);
expect(h1).toBeTruthy();
expect(firstH2).toBeTruthy();
expect(firstH2.y).toBeGreaterThanOrEqual(h1.y + h1.height - 2);
});
});

test.describe('landing page layout', () => {
test.use({ viewport: { width: 1280, height: 900 } });

test('hero, call-to-action and featured sections stack vertically', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const rects = await page.evaluate(() =>
Array.from(document.querySelectorAll('main > section, main > .hero-search')).map(el =>
el.getBoundingClientRect()
)
);
expect(rects.length).toBeGreaterThanOrEqual(3);
expectStacked(rects, 'landing');
});

test('featured heading sits above the repository grid, not beside it', async ({ page }) => {
await page.goto('/');
const [heading, grid] = await Promise.all([
page.locator('main section h2').boundingBox(),
page.locator('.featured-repos ul').boundingBox(),
]);
expect(heading).toBeTruthy();
expect(grid).toBeTruthy();
expect(grid.y).toBeGreaterThanOrEqual(heading.y + heading.height - 2);
});
});
5 changes: 5 additions & 0 deletions tests/integration/landing-page.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ describe('Landing page', () => {
const cta = screen.getByRole('link', { name: 'Add My Repo' });
expect(cta.getAttribute('href')).toBe('/add-repo');
expect(cta.className).toBe('cta');
// Layout cleanup: the CTA section is centered (block-flowed prose layout).
expect(cta.closest('section').className).toContain('section-center');
});

test('lists the first 6 repositories as featured links', () => {
render(_jsx(Landing, { repositories: REPOS }));
const featured = document.querySelector('.featured-repos');
expect(featured).toBeTruthy();
// Layout cleanup: the featured section is centered (block-flowed prose
// layout, see plans/layout-cleanup-plan.md).
expect(featured.closest('section').className).toContain('section-center');
const links = [...featured.querySelectorAll('a')];
expect(links.map(a => a.textContent)).toEqual([
'apache/tomcat',
Expand Down
9 changes: 9 additions & 0 deletions tests/integration/static-pages.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ describe('static content pages', () => {
const html = renderToStaticMarkup(_jsx(Page, {}));
expect(html).toContain(`<h1>${heading}</h1>`);
});

// Layout cleanup: prose pages carry the .content-page class so the CSS
// gives them a bounded, centered, block-flowed column (see
// plans/layout-cleanup-plan.md — mvp.css would otherwise make the
// section a wrapping flex row that smashes the children together).
test(`/${name} uses the content-page layout class`, () => {
const html = renderToStaticMarkup(_jsx(Page, {}));
expect(html).toMatch(/^<section class="content-page">/);
});
}

test('getting-started carries the workflow-sample slot', () => {
Expand Down
9 changes: 9 additions & 0 deletions tests/integration/submission-form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ describe('RepoSubmissionForm', () => {
expect(container.querySelector('.form-status')?.getAttribute('aria-live')).toBe('polite');
});

// Layout cleanup: the add-repo section uses the bounded prose column
// (plans/layout-cleanup-plan.md).
test('uses the content-page layout class on its section', () => {
const { container } = renderForm();
const section = container.querySelector('section');
expect(section.className).toContain('add-repo-page');
expect(section.className).toContain('content-page');
});

test('shows inline validation errors for empty fields', async () => {
const { container } = renderForm();
submit(container, {});
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/layout-styles.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Layout cleanup: protect the mvp.css section-flex reset and the readability
// rules (plans/layout-cleanup-plan.md). mvp.css makes every `section` a
// wrapping flex row, which smashes prose children together; globals.css must
// neutralize it and give prose pages a readable, centered column.

import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const css = readFileSync(
join(dirname(fileURLToPath(import.meta.url)), '../../app/globals.css'),
'utf-8'
);

// Naive-but-sufficient block parser: strips comments, then maps each selector
// to its declaration body (e.g. "main section" -> "display: block; ...").
function ruleMap(source) {
const noComments = source.replace(/\/\*[\s\S]*?\*\//g, '');
const rules = new Map();
const ruleRe = /([^{}]+)\{([^{}]*)\}/g;
let match;
while ((match = ruleRe.exec(noComments)) !== null) {
for (const selector of match[1].split(',')) {
rules.set(selector.trim(), match[2]);
}
}
return rules;
}

const rules = ruleMap(css);

describe('layout styles (mvp.css section-flex reset)', () => {
test('main sections render as a normal block container, not a flex row', () => {
const body = rules.get('main section');
expect(body).toBeTruthy();
expect(body).toMatch(/display:\s*block\s*;/);
});

test('prose pages get a bounded, centered content column via .content-page', () => {
const body = rules.get('.content-page');
expect(body).toBeTruthy();
expect(body).toMatch(/max-width:\s*\d+/);
expect(body).toMatch(/margin:\s*0\s+auto\s*;/);
});

test('landing CTA / featured sections can be centered via .section-center', () => {
const body = rules.get('.section-center');
expect(body).toBeTruthy();
expect(body).toMatch(/text-align:\s*center\s*;/);
});

test('the add-repo form is centered on its now block-flowed page', () => {
const body = rules.get('#repo-form');
expect(body).toBeTruthy();
expect(body).toMatch(/margin:\s*0\s+auto\s*;/);
});
});
Loading