test: lock semantic and no-script portfolio behavior
This commit is contained in:
179
tests/browser/semantic.spec.ts
Normal file
179
tests/browser/semantic.spec.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
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 = [
|
||||
'START',
|
||||
'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('h1')).toHaveText('Luca Pîslaru');
|
||||
await expect(page.locator('.site-header > p').first()).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,
|
||||
);
|
||||
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('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(1);
|
||||
await expect(email).toHaveText('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('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);
|
||||
});
|
||||
Reference in New Issue
Block a user