refactor: remove bundled personal portfolio
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const caseStudyPath = '/projects/qlockify';
|
||||
const caseStudyPath = '/projects/sample-projects';
|
||||
|
||||
async function expectDrawerOpen(page: import('@playwright/test').Page) {
|
||||
const dialog = page.locator('dialog[open]');
|
||||
@@ -14,7 +14,7 @@ test('a pointer click on the card CTA opens a route-backed drawer and restores t
|
||||
await page.evaluate(() => {
|
||||
window.scrollTo({ top: document.body.scrollHeight * 0.35, behavior: 'instant' });
|
||||
});
|
||||
const card = page.getByRole('article', { name: 'Qlockify' });
|
||||
const card = page.getByRole('article', { name: 'A meaningful project' });
|
||||
const cta = card.getByRole('link', { name: 'View case study' });
|
||||
// Measure the viewport from which the user actually clicks, after any
|
||||
// automatic scroll needed to bring the link into view.
|
||||
@@ -25,8 +25,8 @@ test('a pointer click on the card CTA opens a route-backed drawer and restores t
|
||||
await expect(page).toHaveURL(new RegExp(`${caseStudyPath}$`));
|
||||
const dialog = await expectDrawerOpen(page);
|
||||
await expect(dialog.getByRole('button', { name: 'Close case study' })).toBeFocused();
|
||||
await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('target', '_blank');
|
||||
await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
await expect(card.getByRole('link', { name: /Open the live A meaningful project/ })).toHaveAttribute('target', '_blank');
|
||||
await expect(card.getByRole('link', { name: /Open the live A meaningful project/ })).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
@@ -39,12 +39,12 @@ test('a pointer click on the card CTA opens a route-backed drawer and restores t
|
||||
test('a shared route opens over the portfolio and adjacent navigation replaces history', async ({ page }) => {
|
||||
await page.goto(caseStudyPath, { waitUntil: 'domcontentloaded' });
|
||||
const dialog = await expectDrawerOpen(page);
|
||||
await expect(page.getByRole('region', { name: 'Products I\'ve Brought to Life' })).toBeAttached();
|
||||
await expect(page.getByRole('region', { name: 'Things I have built' })).toBeAttached();
|
||||
|
||||
const historyLength = await page.evaluate(() => history.length);
|
||||
await dialog.getByRole('link', { name: /Next project Guilan ACE/ }).click();
|
||||
await expect(page).toHaveURL(/\/projects\/guilan-ace$/);
|
||||
await expect(dialog.getByRole('heading', { name: 'Guilan ACE', level: 1 })).toBeVisible();
|
||||
await dialog.getByRole('link', { name: /Next project Another project/ }).click();
|
||||
await expect(page).toHaveURL(/\/projects\/another-project$/);
|
||||
await expect(dialog.getByRole('heading', { name: 'Another project', level: 1 })).toBeVisible();
|
||||
expect(await page.evaluate(() => history.length)).toBe(historyLength);
|
||||
await dialog.getByRole('button', { name: 'Close case study' }).click();
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
@@ -55,7 +55,7 @@ test('share fallback, copy feedback, focus containment, and the localized RTL ro
|
||||
// the DOM and are covered separately through the supported-share branch.
|
||||
await page.addInitScript(() => Object.defineProperty(navigator, 'share', { configurable: true, value: undefined }));
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
await page.goto('/fa/projects/qlockify', { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('/fa/projects/sample-projects', { waitUntil: 'domcontentloaded' });
|
||||
const dialog = await expectDrawerOpen(page);
|
||||
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
|
||||
await expect(dialog.getByRole('button', { name: 'بستن مطالعه موردی' })).toBeFocused();
|
||||
@@ -65,7 +65,7 @@ test('share fallback, copy feedback, focus containment, and the localized RTL ro
|
||||
await expect(shareDialog).toBeVisible();
|
||||
await shareDialog.getByRole('button', { name: 'کپی پیوند' }).click();
|
||||
await expect(shareDialog.getByText('پیوند کپی شد', { exact: true }).first()).toBeVisible();
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toMatch(/\/fa\/projects\/qlockify$/);
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toMatch(/\/fa\/projects\/sample-projects$/);
|
||||
|
||||
await shareDialog.getByRole('button', { name: 'بستن گزینههای اشتراکگذاری' }).click();
|
||||
await expect(dialog.getByRole('button', { name: 'اشتراکگذاری این مطالعه موردی' })).toBeFocused();
|
||||
@@ -88,18 +88,18 @@ test('the gallery serves original media and opens an accessible full-screen view
|
||||
const drawer = await expectDrawerOpen(page);
|
||||
const stage = drawer.getByRole('button', { name: /Open full-screen image/ });
|
||||
const selectedImage = stage.getByRole('img');
|
||||
await expect(selectedImage).toHaveAttribute('src', '/images/screenshots/qlockify/qlockify-landing.png');
|
||||
await expect(selectedImage).toHaveAttribute('src', '/images/portfolio-placeholder.svg');
|
||||
|
||||
await stage.click();
|
||||
const viewer = page.getByRole('dialog', { name: 'Full-screen project gallery' });
|
||||
await expect(viewer).toBeVisible();
|
||||
await expect(viewer.getByText('1 of 8')).toBeVisible();
|
||||
await expect(viewer.getByText('1 of 2')).toBeVisible();
|
||||
if (isMobile) {
|
||||
await viewer.getByRole('button', { name: /^Show image 2:/ }).click();
|
||||
} else {
|
||||
await viewer.getByRole('button', { name: 'Next image' }).click();
|
||||
}
|
||||
await expect(viewer.getByText('2 of 8')).toBeVisible();
|
||||
await expect(viewer.getByText('2 of 2')).toBeVisible();
|
||||
await viewer.getByRole('button', { name: 'Close full-screen image' }).click();
|
||||
await expect(stage).toBeFocused();
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ test('the isometric footer uses editable contact destinations and email', async
|
||||
|
||||
assert.match(footer, /portfolio\.profile\.contacts/);
|
||||
assert.match(footer, /portfolio\.profile\.email/);
|
||||
assert.doesNotMatch(footer, /amiirkhl|09388263077/);
|
||||
});
|
||||
|
||||
test('the footer renders the IsometricFooter scene instead of preloading PNG artwork', async () => {
|
||||
|
||||
@@ -26,21 +26,6 @@ test('localized project routes use runtime content and metadata', async () => {
|
||||
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*=/);
|
||||
assert.match(source, /screenshots\/qlockify\/qlockify-landing\.png/);
|
||||
assert.match(source, /screenshots\/guilan-ace\/guilan-ace-landing-dark\.png/);
|
||||
assert.match(source, /screenshots\/subsource\/subtitle-detail-light\.png/);
|
||||
});
|
||||
|
||||
test('project cards separate case study navigation from live links', async () => {
|
||||
const source = await readFile('src/components/ProjectsSection.tsx', 'utf8');
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ async function waitForServer(process, getLogs) {
|
||||
throw new Error(`standalone server did not become ready\n${getLogs()}`);
|
||||
}
|
||||
|
||||
test('the standalone server renders both locales and serves local images', async (t) => {
|
||||
test('the standalone server renders generic samples without private content', 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'));
|
||||
@@ -35,7 +35,7 @@ test('the standalone server renders both locales and serves local images', async
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
const imported = spawnSync(process.execPath, ['maintenance.mjs', 'import-legacy'], { cwd: standaloneRoot, env, encoding: 'utf8' });
|
||||
const imported = spawnSync(process.execPath, ['maintenance.mjs', 'init'], { cwd: standaloneRoot, env, encoding: 'utf8' });
|
||||
assert.equal(imported.status, 0, imported.stderr);
|
||||
const server = spawn(process.execPath, ['server.js'], {
|
||||
cwd: standaloneRoot,
|
||||
@@ -48,77 +48,19 @@ test('the standalone server renders both locales and serves local images', async
|
||||
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`),
|
||||
]);
|
||||
const englishResponse = await fetch(`${origin}/`);
|
||||
assert.equal(englishResponse.status, 200);
|
||||
assert.equal(persianResponse.status, 200);
|
||||
|
||||
const [englishHtml, persianHtml] = await Promise.all([
|
||||
englishResponse.text(),
|
||||
persianResponse.text(),
|
||||
]);
|
||||
const englishHtml = await englishResponse.text();
|
||||
assert.match(englishHtml, /<html[^>]*lang="en"[^>]*dir="ltr"/i);
|
||||
assert.match(englishHtml, /href="\/fa"/i);
|
||||
assert.match(englishHtml, /hreflang="fa"/i);
|
||||
assert.match(englishHtml, />Software Engineer</);
|
||||
assert.doesNotMatch(englishHtml, /id="site-loader"/);
|
||||
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.match(englishHtml, /Your Name/);
|
||||
assert.equal((await fetch(`${origin}/fa`)).status, 404);
|
||||
assert.equal((await fetch(`${origin}/projects/sample-projects`)).status, 200);
|
||||
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');
|
||||
const placeholder = await fetch(`${origin}/images/portfolio-placeholder.svg`);
|
||||
assert.equal(placeholder.status, 200);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user