feat: add route-backed project case studies
Some checks failed
Test and deploy / Verify production build (push) Has been cancelled
Test and deploy / Deploy production (push) Has been cancelled

This commit is contained in:
2026-09-09 22:00:57 +03:30
parent b3cfcfe057
commit 24cd86ede7
25 changed files with 1892 additions and 65 deletions

View File

@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { access, readFile } from 'node:fs/promises';
import test from 'node:test';
const routeFiles = [
'src/app/(en)/projects/layout.tsx',
'src/app/(en)/projects/[projectId]/page.tsx',
'src/app/(fa)/fa/projects/layout.tsx',
'src/app/(fa)/fa/projects/[projectId]/page.tsx',
];
test('localized project routes are statically generated and reject unknown slugs', async () => {
await Promise.all(routeFiles.map((file) => access(file)));
const pages = (
await Promise.all(
routeFiles
.filter((file) => file.endsWith('page.tsx'))
.map((file) => readFile(file, 'utf8')),
)
).join('\n');
assert.match(pages, /generateStaticParams/);
assert.match(pages, /dynamicParams\s*=\s*false/);
assert.match(pages, /await params/);
assert.match(pages, /generateMetadata/);
});
test('every project exposes an evidence-based localized case study', async () => {
const source = await readFile('src/data/projects.ts', 'utf8');
assert.doesNotMatch(source, /\blink\??:/);
assert.doesNotMatch(source, /\blink:/);
assert.ok((source.match(/\bcaseStudy:/g) ?? []).length >= 14);
assert.equal((source.match(/\bgallery:\s*\[/g) ?? []).length, 7);
assert.match(source, /export const getProject\s*=/);
assert.match(source, /export const getAdjacentProjects\s*=/);
assert.match(source, /export const getProjectPath\s*=/);
});
test('project cards separate case study navigation from live links', async () => {
const source = await readFile('src/components/ProjectsSection.tsx', 'utf8');
assert.match(source, /import Link from ['"]next\/link['"]/);
assert.match(source, /scroll=\{false\}/);
assert.match(source, /target="_blank"/);
assert.match(source, /rel="noopener noreferrer"/);
assert.match(source, /project\.liveUrl/);
assert.match(source, /caseStudyInput/);
});
test('the case study drawer has accessible and pointer-safe dismissal paths', async () => {
const [component, styles] = await Promise.all([
Promise.all([
'src/components/CaseStudyDrawer.tsx',
'src/components/useDrawerDrag.ts',
].map((file) => readFile(file, 'utf8'))).then((files) => files.join('\n')),
readFile('src/components/CaseStudyDrawer.module.css', 'utf8'),
]);
assert.match(component, /showModal\(\)/);
assert.match(component, /onCancel=/);
assert.match(component, /setPointerCapture/);
assert.match(component, /navigator\.clipboard/);
assert.match(component, /aria-live="polite"/);
assert.match(styles, /::backdrop/);
assert.match(styles, /env\(safe-area-inset-bottom/);
assert.match(styles, /prefers-reduced-motion:\s*reduce/);
assert.doesNotMatch(styles, /transition:\s*all/);
});

View File

@@ -61,6 +61,28 @@ test('the standalone server renders both locales and optimizes local images', as
assert.match(persianHtml, /<html[^>]*lang="fa"[^>]*dir="rtl"/i);
assert.match(persianHtml, /سلام، من امیرحسین خلیلی هستم/);
const projectIds = [
'qlockify',
'guilan-ace',
'zarinpal',
'iran-implant',
'interanet-solutions',
'postgresql-job-queue',
'subsource',
];
const projectRoutes = projectIds.flatMap((id) => [`/projects/${id}`, `/fa/projects/${id}`]);
const projectResponses = await Promise.all(projectRoutes.map((route) => fetch(`${origin}${route}`)));
for (const response of projectResponses) assert.equal(response.status, 200);
const projectHtml = await Promise.all(projectResponses.map((response) => response.text()));
for (let index = 0; index < projectHtml.length; index += 1) {
assert.match(projectHtml[index], /<dialog[^>]*aria-labelledby="case-study-title"/i);
assert.match(projectHtml[index], /id="case-study-title"/i);
assert.match(projectHtml[index], /Qlockify|Guilan|زرین|Implant|ایمپلنت|Interanet|اینترانت|PostgreSQL|Subsource|سابسورس/i);
assert.match(projectHtml[index], index % 2 === 0 ? /<html[^>]*dir="ltr"/i : /<html[^>]*dir="rtl"/i);
}
assert.equal((await fetch(`${origin}/projects/not-a-project`)).status, 404);
assert.equal((await fetch(`${origin}/fa/projects/not-a-project`)).status, 404);
const staticAsset = englishHtml.match(/(?:src|href)="(\/_next\/static\/[^"?#]+)/)?.[1];
assert.ok(staticAsset, 'expected a built static asset');
assert.equal((await fetch(`${origin}${staticAsset}`)).status, 200);