import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { access, cp, readFile, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { once } from 'node:events'; import path from 'node:path'; import test from 'node:test'; const projectRoot = path.resolve('.'); const standaloneRoot = path.join(projectRoot, '.next', 'standalone'); const port = 43217; const origin = `http://127.0.0.1:${port}`; async function waitForServer(process, getLogs) { const deadline = Date.now() + 60_000; while (Date.now() < deadline) { if (process.exitCode !== null) throw new Error('standalone server exited early'); try { const response = await fetch(origin, { signal: AbortSignal.timeout(10_000) }); if (response.ok) return; } catch {} await new Promise((resolve) => setTimeout(resolve, 150)); } throw new Error(`standalone server did not become ready\n${getLogs()}`); } test('the standalone server renders both locales and serves local images', async (t) => { const dataDir = await mkdtemp(path.join(tmpdir(), 'portfolio-runtime-')); const env = { ...process.env, DATA_DIR: dataDir, SITE_URL: origin, HOSTNAME: '127.0.0.1', PORT: String(port) }; await access(path.join(standaloneRoot, 'server.js')); await cp(path.join(projectRoot, 'public'), path.join(standaloneRoot, 'public'), { recursive: true }); await cp( path.join(projectRoot, '.next', 'static'), path.join(standaloneRoot, '.next', 'static'), { recursive: true }, ); const imported = spawnSync(process.execPath, ['maintenance.mjs', 'import-legacy'], { cwd: standaloneRoot, env, encoding: 'utf8' }); assert.equal(imported.status, 0, imported.stderr); const server = spawn(process.execPath, ['server.js'], { cwd: standaloneRoot, env, stdio: ['ignore', 'pipe', 'pipe'], }); let serverLogs = ''; server.stdout.on('data', (chunk) => { serverLogs += chunk; }); server.stderr.on('data', (chunk) => { serverLogs += chunk; }); t.after(async () => { if (server.exitCode === null) { const exited = once(server, 'exit'); server.kill(); await exited; } await rm(dataDir, { recursive: true, force: true }); }); await waitForServer(server, () => serverLogs); const [englishResponse, persianResponse] = await Promise.all([ fetch(`${origin}/`), fetch(`${origin}/fa`), ]); assert.equal(englishResponse.status, 200); assert.equal(persianResponse.status, 200); const [englishHtml, persianHtml] = await Promise.all([ englishResponse.text(), persianResponse.text(), ]); assert.match(englishHtml, /]*lang="en"[^>]*dir="ltr"/i); assert.match(englishHtml, /href="\/fa"/i); assert.match(englishHtml, /hreflang="fa"/i); assert.match(englishHtml, />Software Engineer]*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], /]*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 ? /]*dir="ltr"/i : /]*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); const optimized = await fetch( `${origin}/_next/image?url=%2Fimages%2Favatar.png&w=256&q=75`, { headers: { Accept: 'image/webp' } }, ); assert.equal(optimized.status, 200); assert.match( optimized.headers.get('content-type') ?? '', /^image\/(?:webp|png)$/, 'expected WebP output or Next.js PNG fallback on unsupported CPUs', ); assert.match(optimized.headers.get('cache-control') ?? '', /max-age=/); assert.ok((await optimized.arrayBuffer()).byteLength > 0); }); test('Persian source copy uses standard characters and punctuation', async () => { const localizedFiles = [ 'src/i18n/content.ts', 'src/data/projects.ts', 'src/data/experience.ts', 'src/data/education.ts', 'src/data/milestones.ts', ]; const source = ( await Promise.all(localizedFiles.map((file) => readFile(file, 'utf8'))) ).join('\n'); assert.doesNotMatch(source, /[يك]/, 'use Persian ی and ک instead of Arabic forms'); assert.doesNotMatch(source, /[—–]/, 'use Persian punctuation instead of em/en dashes'); });