220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
import { request as httpRequest } from 'node:http';
|
|
import { symlink, unlink } from 'node:fs/promises';
|
|
import { resolve } from 'node:path';
|
|
import { expect, test } from '../helpers/serve-dist';
|
|
|
|
const sectionIds = [
|
|
'start',
|
|
'profile',
|
|
'experience',
|
|
'infrastructure',
|
|
'selected-builds',
|
|
'additional-work',
|
|
'toolkit',
|
|
'contact',
|
|
] as const;
|
|
|
|
const sectionHeadings = [
|
|
'Profile',
|
|
'Experience',
|
|
'Infrastructure',
|
|
'Selected builds',
|
|
'Additional work',
|
|
'Toolkit',
|
|
'Contact',
|
|
] as const;
|
|
|
|
const rawStatus = async (
|
|
origin: string,
|
|
path: string,
|
|
method = 'GET',
|
|
): Promise<number | undefined> =>
|
|
new Promise((resolve, reject) => {
|
|
const url = new URL(origin);
|
|
const request = httpRequest(
|
|
{
|
|
host: url.hostname,
|
|
method,
|
|
path,
|
|
port: url.port,
|
|
},
|
|
(response) => {
|
|
response.resume();
|
|
response.once('end', () => resolve(response.statusCode));
|
|
},
|
|
);
|
|
request.once('error', reject);
|
|
request.end();
|
|
});
|
|
|
|
test('serves only the owned generated preview surface', async ({
|
|
distUrl,
|
|
request,
|
|
}, testInfo) => {
|
|
const document = await request.get(`${distUrl}/`);
|
|
expect(document.status()).toBe(200);
|
|
expect(document.headers()['content-type']).toBe('text/html; charset=utf-8');
|
|
|
|
const unsafeMethod = await request.post(`${distUrl}/index.html`);
|
|
expect(unsafeMethod.status()).toBe(405);
|
|
expect(unsafeMethod.headers().allow).toBe('GET, HEAD');
|
|
expect(await rawStatus(distUrl, '/%2e%2e/package.json')).toBe(403);
|
|
expect(
|
|
(await request.get(`${distUrl}/not-a-preview-resource`)).status(),
|
|
).toBe(404);
|
|
|
|
const linkName = `.task-7-escape-${process.pid}-${testInfo.workerIndex}`;
|
|
const linkPath = resolve(process.cwd(), 'dist', linkName);
|
|
await symlink(resolve(process.cwd(), 'package.json'), linkPath);
|
|
try {
|
|
expect((await request.get(`${distUrl}/${linkName}`)).status()).toBe(403);
|
|
} finally {
|
|
await unlink(linkPath);
|
|
}
|
|
});
|
|
|
|
test('exposes the complete semantic portfolio from the generated document', async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
|
|
|
await expect(page.locator('h1:visible')).toHaveCount(1);
|
|
await expect(page.locator('#start h1')).toHaveText('Luca Pîslaru');
|
|
await expect(page.locator('.hero-journey__role')).toHaveText(
|
|
'System Administrator & Full-Stack Developer',
|
|
);
|
|
await expect(page.locator('main')).toHaveCount(1);
|
|
await expect(
|
|
page.locator('nav[aria-label="Portfolio sections"]'),
|
|
).toHaveCount(1);
|
|
await expect(page.locator('footer')).toHaveCount(1);
|
|
|
|
expect(
|
|
await page
|
|
.locator('main > section')
|
|
.evaluateAll((sections) => sections.map((section) => section.id)),
|
|
).toEqual(sectionIds);
|
|
expect(await page.locator('main > section > h2').allTextContents()).toEqual(
|
|
sectionHeadings,
|
|
);
|
|
await expect(page.locator('.route-label')).toHaveCount(0);
|
|
await expect(page.getByRole('link', { name: 'Explore work' })).toHaveAttribute('href', '#selected-builds');
|
|
await expect(page.getByRole('link', { name: 'Email Luca' })).toHaveCount(2);
|
|
await expect(page.locator('[data-featured-operation]')).toContainText('without data loss');
|
|
expect(await page.locator('[data-testid="toolkit-technology"]', { hasText: 'DNS' }).locator('span').evaluate(
|
|
(label) => getComputedStyle(label).gridColumn,
|
|
)).toBe('1 / -1');
|
|
expect(await page.locator('.hero-journey').evaluate((hero) => {
|
|
const content = hero.querySelector('.hero-journey__content')!;
|
|
const heroStyle = getComputedStyle(hero);
|
|
const contentStyle = getComputedStyle(content);
|
|
return {
|
|
preservesPadding: Math.abs(Number.parseFloat(heroStyle.paddingTop) - innerHeight * 0.12) < 0.01,
|
|
preservesWidth: Math.abs(Number.parseFloat(contentStyle.width) - Math.min(544, innerWidth * 0.39)) < 0.1,
|
|
position: contentStyle.position,
|
|
zIndex: contentStyle.zIndex,
|
|
};
|
|
})).toEqual({ preservesPadding: true, preservesWidth: true, position: 'static', zIndex: 'auto' });
|
|
expect(
|
|
await page.locator('h1, h2, h3').evaluateAll((headings) => {
|
|
const levels = headings.map((heading) =>
|
|
Number(heading.tagName.slice(1)),
|
|
);
|
|
return levels.every(
|
|
(level, index) => index === 0 || level <= levels[index - 1]! + 1,
|
|
);
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test('loads the self-hosted Geist families for information and route text', async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(() => document.fonts.ready);
|
|
|
|
const fonts = await page.evaluate(() => ({
|
|
sansLoaded: document.fonts.check('16px "Geist Variable"'),
|
|
monoLoaded: document.fonts.check('16px "Geist Mono Variable"'),
|
|
headingFamily: getComputedStyle(document.querySelector('h1')!).fontFamily,
|
|
routeFamily: getComputedStyle(document.querySelector('.section-eyebrow')!).fontFamily,
|
|
}));
|
|
|
|
expect(fonts).toMatchObject({ sansLoaded: true, monoLoaded: true });
|
|
expect(fonts.headingFamily).toContain('Geist Variable');
|
|
expect(fonts.routeFamily).toContain('Geist Mono Variable');
|
|
});
|
|
|
|
test('keeps canonical native anchors and safe external destinations', async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
|
|
|
const navigation = page.locator('nav[aria-label="Portfolio sections"] a');
|
|
expect(
|
|
await navigation.evaluateAll((links) =>
|
|
links.map((link) => link.getAttribute('href')),
|
|
),
|
|
).toEqual(sectionIds.map((id) => `#${id}`));
|
|
for (const id of sectionIds) {
|
|
await expect(page.locator(`#${id}`)).toHaveCount(1);
|
|
await expect(page.locator(`#${id}-heading`)).toHaveCount(1);
|
|
}
|
|
|
|
const email = page.locator('a[href^="mailto:"]');
|
|
await expect(email).toHaveCount(2);
|
|
await expect(email).toHaveText(['Email Luca', 'Email Luca']);
|
|
expect(await email.evaluateAll((links) => [...new Set(links.map((link) => link.getAttribute('href')))])).toEqual(
|
|
['mailto:luca.pislaru@gmail.com'],
|
|
);
|
|
|
|
const externalLinks = page.locator('#contact a[target="_blank"]');
|
|
await expect(externalLinks).toHaveCount(3);
|
|
expect(
|
|
await externalLinks.evaluateAll((links) =>
|
|
links.every((link) => {
|
|
const rel = new Set((link.getAttribute('rel') ?? '').split(/\s+/u));
|
|
return (
|
|
link instanceof HTMLAnchorElement &&
|
|
link.protocol === 'https:' &&
|
|
rel.has('me') &&
|
|
rel.has('noopener') &&
|
|
rel.has('noreferrer')
|
|
);
|
|
}),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test('never links private projects or packages a résumé download', async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
|
|
|
for (const title of [
|
|
'Private self-hosted medication tracker',
|
|
'Private healthcare platform',
|
|
'Private game prototype',
|
|
]) {
|
|
await expect(
|
|
page.locator('article').filter({ hasText: title }).locator('a'),
|
|
).toHaveCount(0);
|
|
}
|
|
await expect(page.locator('a[download]')).toHaveCount(0);
|
|
expect(
|
|
await page
|
|
.locator('a')
|
|
.evaluateAll((links) =>
|
|
links.some(
|
|
(link) =>
|
|
/(?:résumé|resume|\bcv\b)/iu.test(link.textContent ?? '') ||
|
|
/\.(?:pdf|docx?)(?:$|[?#])/iu.test(link.getAttribute('href') ?? ''),
|
|
),
|
|
),
|
|
).toBe(false);
|
|
});
|