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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
- Dedicated support guide (`SUPPORT.md`) and issue-template contact links for support/security routing

### Changed
- Public `web-next` homepage refreshed around the reviewer-safe live snapshot, anonymized receipt copy, and demo-first CTA path.
- README command examples and tooling descriptions updated to match current CLI and MCP behavior
- PR template and contributing checklist now enforce changelog and security/audit validation gates
- PR template and contributor guide now enforce small, single-purpose PR scope and Linear-linked branch/PR conventions
Expand Down
2 changes: 2 additions & 0 deletions tests/web-next-demo-fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const LAB_METRICS_PACKET_FILE = path.join(

const UI_FIXTURE_FILES = [
path.join(REPO_ROOT, 'web-next', 'app', 'demo', 'page.tsx'),
path.join(REPO_ROOT, 'web-next', 'components', 'bento-section.tsx'),
path.join(REPO_ROOT, 'web-next', 'components', 'changelog-section.tsx'),
path.join(REPO_ROOT, 'web-next', 'components', 'dashboard-preview.tsx'),
path.join(REPO_ROOT, 'web-next', 'components', 'hero-section.tsx'),
LIVE_METRICS_SNAPSHOT_FILE,
Expand Down
34 changes: 34 additions & 0 deletions tests/web-next-homepage-anchors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

function readRepoFile(relativePath: string): string {
return readFileSync(path.join(REPO_ROOT, relativePath), 'utf8');
}

describe('web-next homepage anchors', () => {
it('keeps faq-section owned by a single rendered component root', () => {
const pageContent = readRepoFile('web-next/app/page.tsx');
const faqContent = readRepoFile('web-next/components/faq-section.tsx');

expect(pageContent).not.toContain('id="faq-section"');
expect((faqContent.match(/id="faq-section"/g) ?? [])).toHaveLength(1);
});
Comment thread
AndrewNordstrom marked this conversation as resolved.

it('keeps the footer audit log link pointed at the history route', () => {
const footerContent = readRepoFile('web-next/components/footer-section.tsx');

expect(footerContent).toMatch(/<Link href="\/history"[^>]*>\s*Audit log\s*<\/Link>/);
});

it('keeps shared landing CTAs rendered through Button asChild', () => {
const ctaContent = readRepoFile('web-next/components/landing-ctas.tsx');

expect(ctaContent).not.toMatch(/<Link href="\/(?:demo|sign-in)">\s*<Button/s);
expect(ctaContent).toMatch(/<Button\s+asChild[\s\S]*?<Link href="\/demo">/);
expect(ctaContent).toMatch(/<Button\s+asChild[\s\S]*?<Link href="\/sign-in">/);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
136 changes: 136 additions & 0 deletions tests/web-next-landing-ctas-render.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, expect, it } from 'vitest';
import { DemoCTA, SignInCTA } from '../web-next/components/landing-ctas';

type ElementType = string | ((props: ElementProps) => RenderedNode);
type RenderedNode =
| RenderedElement
| readonly RenderedNode[]
| string
| number
| boolean
| null
| undefined;

interface ElementProps {
readonly [key: string]: unknown;
readonly children?: RenderedNode;
}

interface RenderedElement {
readonly type: ElementType;
readonly props: ElementProps;
}

function escapeHtml(value: string): string {
return value
.replaceAll('&', '&amp;')
.replaceAll('"', '&quot;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}

function stringifyAttributeValue(value: unknown): string {
if (typeof value === 'string') {
return value;
}

if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}

if (value !== null && value !== undefined && typeof value === 'object' && 'toString' in value) {
return value.toString();
}

return '';
}

function renderAttributes(props: ElementProps): string {
return Object.entries(props)
.filter(([key, value]) => key !== 'children' && key !== 'ref' && value !== false && value !== undefined && value !== null)
.map(([key, value]) => {
if (value === true) {
return key;
}

return `${key}="${escapeHtml(stringifyAttributeValue(value))}"`;
})
.join(' ');
}

function createElement(type: ElementType, props: ElementProps): RenderedElement {
return {
type,
props,
};
}

function renderToStaticMarkup(node: RenderedNode): string {
if (node === null || node === undefined || typeof node === 'boolean') {
return '';
}

if (typeof node === 'string' || typeof node === 'number') {
return escapeHtml(String(node));
}

if (Array.isArray(node)) {
return node.map((child) => renderToStaticMarkup(child)).join('');
}

if (typeof node.type === 'function') {
return renderToStaticMarkup(node.type(node.props));
}

const renderedChildren = renderToStaticMarkup(node.props.children);
const attributes = renderAttributes(node.props);
const attributePrefix = attributes.length > 0 ? ` ${attributes}` : '';

return `<${node.type}${attributePrefix}>${renderedChildren}</${node.type}>`;
}

function countMatches(value: string, pattern: RegExp): number {
return value.match(pattern)?.length ?? 0;
}

describe('web-next landing CTAs', () => {
it('renders DemoCTA as one demo anchor without a nested button', () => {
const markup = renderToStaticMarkup(createElement(DemoCTA, { className: 'qa-demo-class' }));

expect(countMatches(markup, /<a\b/g)).toBe(1);
expect(markup).toContain('href="/demo"');
expect(markup).not.toContain('<button');
expect(markup).toContain('qa-demo-class');
expect(markup).toContain('inline-flex');
expect(markup).toContain('rounded-full');
expect(markup).toContain('bg-primary');
});

it('renders SignInCTA as one sign-in anchor without a nested button', () => {
const markup = renderToStaticMarkup(createElement(SignInCTA, { className: 'qa-sign-in-class' }));

expect(countMatches(markup, /<a\b/g)).toBe(1);
expect(markup).toContain('href="/sign-in"');
expect(markup).not.toContain('<button');
expect(markup).toContain('qa-sign-in-class');
expect(markup).toContain('inline-flex');
expect(markup).toContain('rounded-full');
});

it('keeps required base CTA classes when optional className adds layout tokens', () => {
const demoMarkup = renderToStaticMarkup(createElement(DemoCTA, { className: 'qa-demo-wide sm:px-8' }));
const signInMarkup = renderToStaticMarkup(createElement(SignInCTA, { className: 'qa-sign-in-wide sm:px-6' }));

expect(demoMarkup).toContain('qa-demo-wide');
expect(demoMarkup).toContain('sm:px-8');
expect(demoMarkup).toContain('inline-flex');
expect(demoMarkup).toContain('rounded-full');
expect(demoMarkup).toContain('bg-primary');

expect(signInMarkup).toContain('qa-sign-in-wide');
expect(signInMarkup).toContain('sm:px-6');
expect(signInMarkup).toContain('inline-flex');
expect(signInMarkup).toContain('rounded-full');
expect(signInMarkup).toContain('hover:text-foreground');
});
});
Comment on lines +96 to +136

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing edge case: default render with no className prop.

All three tests pass a className. LandingCTAProps.className is optional (per landing-ctas.tsx), so add a case rendering DemoCTA/SignInCTA with no className at all to confirm cn(baseClasses, undefined) doesn't throw and still emits the required base classes (bg-primary, rounded-full, inline-flex) — this is the actual default homepage render path and isn't currently covered.

🧪 Suggested test case
   it('keeps required base CTA classes when optional className adds layout tokens', () => {
     ...
   });
+
+  it('renders DemoCTA and SignInCTA with base classes when no className is provided', () => {
+    const demoMarkup = renderToStaticMarkup(createElement(DemoCTA, {}));
+    const signInMarkup = renderToStaticMarkup(createElement(SignInCTA, {}));
+
+    expect(demoMarkup).toContain('inline-flex');
+    expect(demoMarkup).toContain('rounded-full');
+    expect(demoMarkup).toContain('bg-primary');
+    expect(signInMarkup).toContain('inline-flex');
+    expect(signInMarkup).toContain('rounded-full');
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('web-next landing CTAs', () => {
it('renders DemoCTA as one demo anchor without a nested button', () => {
const markup = renderToStaticMarkup(createElement(DemoCTA, { className: 'qa-demo-class' }));
expect(countMatches(markup, /<a\b/g)).toBe(1);
expect(markup).toContain('href="/demo"');
expect(markup).not.toContain('<button');
expect(markup).toContain('qa-demo-class');
expect(markup).toContain('inline-flex');
expect(markup).toContain('rounded-full');
expect(markup).toContain('bg-primary');
});
it('renders SignInCTA as one sign-in anchor without a nested button', () => {
const markup = renderToStaticMarkup(createElement(SignInCTA, { className: 'qa-sign-in-class' }));
expect(countMatches(markup, /<a\b/g)).toBe(1);
expect(markup).toContain('href="/sign-in"');
expect(markup).not.toContain('<button');
expect(markup).toContain('qa-sign-in-class');
expect(markup).toContain('inline-flex');
expect(markup).toContain('rounded-full');
});
it('keeps required base CTA classes when optional className adds layout tokens', () => {
const demoMarkup = renderToStaticMarkup(createElement(DemoCTA, { className: 'qa-demo-wide sm:px-8' }));
const signInMarkup = renderToStaticMarkup(createElement(SignInCTA, { className: 'qa-sign-in-wide sm:px-6' }));
expect(demoMarkup).toContain('qa-demo-wide');
expect(demoMarkup).toContain('sm:px-8');
expect(demoMarkup).toContain('inline-flex');
expect(demoMarkup).toContain('rounded-full');
expect(demoMarkup).toContain('bg-primary');
expect(signInMarkup).toContain('qa-sign-in-wide');
expect(signInMarkup).toContain('sm:px-6');
expect(signInMarkup).toContain('inline-flex');
expect(signInMarkup).toContain('rounded-full');
expect(signInMarkup).toContain('hover:text-foreground');
});
});
describe('web-next landing CTAs', () => {
it('renders DemoCTA as one demo anchor without a nested button', () => {
const markup = renderToStaticMarkup(createElement(DemoCTA, { className: 'qa-demo-class' }));
expect(countMatches(markup, /<a\b/g)).toBe(1);
expect(markup).toContain('href="/demo"');
expect(markup).not.toContain('<button');
expect(markup).toContain('qa-demo-class');
expect(markup).toContain('inline-flex');
expect(markup).toContain('rounded-full');
expect(markup).toContain('bg-primary');
});
it('renders SignInCTA as one sign-in anchor without a nested button', () => {
const markup = renderToStaticMarkup(createElement(SignInCTA, { className: 'qa-sign-in-class' }));
expect(countMatches(markup, /<a\b/g)).toBe(1);
expect(markup).toContain('href="/sign-in"');
expect(markup).not.toContain('<button');
expect(markup).toContain('qa-sign-in-class');
expect(markup).toContain('inline-flex');
expect(markup).toContain('rounded-full');
});
it('keeps required base CTA classes when optional className adds layout tokens', () => {
const demoMarkup = renderToStaticMarkup(createElement(DemoCTA, { className: 'qa-demo-wide sm:px-8' }));
const signInMarkup = renderToStaticMarkup(createElement(SignInCTA, { className: 'qa-sign-in-wide sm:px-6' }));
expect(demoMarkup).toContain('qa-demo-wide');
expect(demoMarkup).toContain('sm:px-8');
expect(demoMarkup).toContain('inline-flex');
expect(demoMarkup).toContain('rounded-full');
expect(demoMarkup).toContain('bg-primary');
expect(signInMarkup).toContain('qa-sign-in-wide');
expect(signInMarkup).toContain('sm:px-6');
expect(signInMarkup).toContain('inline-flex');
expect(signInMarkup).toContain('rounded-full');
expect(signInMarkup).toContain('hover:text-foreground');
});
it('renders DemoCTA and SignInCTA with base classes when no className is provided', () => {
const demoMarkup = renderToStaticMarkup(createElement(DemoCTA, {}));
const signInMarkup = renderToStaticMarkup(createElement(SignInCTA, {}));
expect(demoMarkup).toContain('inline-flex');
expect(demoMarkup).toContain('rounded-full');
expect(demoMarkup).toContain('bg-primary');
expect(signInMarkup).toContain('inline-flex');
expect(signInMarkup).toContain('rounded-full');
});
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/web-next-landing-ctas-render.test.tsx` around lines 96 - 136, Add a
test case in web-next landing CTAs coverage that renders DemoCTA and SignInCTA
without passing className, since LandingCTAProps.className is optional. Use the
existing DemoCTA and SignInCTA symbols from the test file and assert the default
markup still includes the required base classes such as inline-flex,
rounded-full, and bg-primary, confirming cn(baseClasses, undefined) works for
the default render path.

Source: Path instructions

43 changes: 43 additions & 0 deletions tests/web-next-percent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import {
absoluteUnitScoreToPercentValue,
clampPercentValue,
formatUnitIntervalPercent,
unitIntervalToPercentValue,
} from '../web-next/lib/percent';

describe('web-next percent helpers', () => {
it('clamps already-scaled percentages at the lowest level', () => {
expect(clampPercentValue(Number.NaN)).toBe(0);
expect(clampPercentValue(Number.POSITIVE_INFINITY)).toBe(0);
expect(clampPercentValue(Number.NEGATIVE_INFINITY)).toBe(0);
expect(clampPercentValue(-1)).toBe(0);
expect(clampPercentValue(0)).toBe(0);
expect(clampPercentValue(12.5)).toBe(13);
expect(clampPercentValue(100)).toBe(100);
expect(clampPercentValue(101)).toBe(100);
});

it('keeps unit interval bar widths and labels clamped consistently', () => {
expect(unitIntervalToPercentValue(0)).toBe(0);
expect(formatUnitIntervalPercent(0)).toBe('0%');
expect(unitIntervalToPercentValue(1)).toBe(100);
expect(formatUnitIntervalPercent(1)).toBe('100%');
expect(unitIntervalToPercentValue(Number.NaN)).toBe(0);
expect(formatUnitIntervalPercent(Number.NaN)).toBe('0%');
expect(unitIntervalToPercentValue(Number.POSITIVE_INFINITY)).toBe(0);
expect(formatUnitIntervalPercent(Number.POSITIVE_INFINITY)).toBe('0%');
expect(unitIntervalToPercentValue(Number.NEGATIVE_INFINITY)).toBe(0);
expect(formatUnitIntervalPercent(Number.NEGATIVE_INFINITY)).toBe('0%');
expect(unitIntervalToPercentValue(1.4)).toBe(100);
expect(formatUnitIntervalPercent(1.4)).toBe('100%');
expect(unitIntervalToPercentValue(-0.2)).toBe(0);
expect(formatUnitIntervalPercent(-0.2)).toBe('0%');
});

it('preserves absolute score behavior for score bars', () => {
expect(absoluteUnitScoreToPercentValue(-0.45)).toBe(45);
expect(absoluteUnitScoreToPercentValue(1.25)).toBe(100);
expect(absoluteUnitScoreToPercentValue(Number.NaN)).toBe(0);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
40 changes: 40 additions & 0 deletions tests/web-next-score.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { formatSignedScore, isNonNegativeScore, normalizeScoreValue } from '../web-next/lib/score';

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

function readRepoFile(relativePath: string): string {
return readFileSync(path.join(REPO_ROOT, relativePath), 'utf8');
}

describe('web-next score helpers', () => {
it('normalizes non-finite score values before formatting signed receipts', () => {
expect(normalizeScoreValue(Number.NaN)).toBe(0);
expect(normalizeScoreValue(Number.POSITIVE_INFINITY)).toBe(0);
expect(normalizeScoreValue(Number.NEGATIVE_INFINITY)).toBe(0);

expect(formatSignedScore(Number.NaN)).toBe('+0.00');
expect(formatSignedScore(Number.POSITIVE_INFINITY)).toBe('+0.00');
expect(formatSignedScore(Number.NEGATIVE_INFINITY)).toBe('+0.00');
expect(formatSignedScore(0.8486208006784361)).toBe('+0.85');
expect(formatSignedScore(-0.125)).toBe('-0.13');
expect(formatSignedScore(0)).toBe('+0.00');
expect(formatSignedScore(-0)).toBe('+0.00');

expect(isNonNegativeScore(Number.NaN)).toBe(true);
expect(isNonNegativeScore(-0.01)).toBe(false);
});
Comment thread
AndrewNordstrom marked this conversation as resolved.

it('keeps homepage receipt score formatting on the shared helper', () => {
const bentoContent = readRepoFile('web-next/components/bento-section.tsx');
const dashboardContent = readRepoFile('web-next/components/dashboard-preview.tsx');

expect(bentoContent).not.toContain('function formatScore');
expect(bentoContent).toContain('formatSignedScore(LIVE_RANK_ONE_EXPLANATION.totalScore)');
expect(dashboardContent).not.toContain('totalScorePrefix');
expect(dashboardContent).toContain('formatSignedScore(LIVE_RANK_ONE_EXPLANATION.totalScore)');
});
});
Loading
Loading