feat: refine portfolio content, contact, and supporting media

This commit is contained in:
2026-09-05 20:08:57 +03:00
parent 15bcdf7cec
commit 103427f95b
53 changed files with 824 additions and 204 deletions

View File

@@ -62,10 +62,10 @@ describe('semantic portfolio document', () => {
'System Administrator & Full-Stack Developer',
validInputs.content.identity.valueProposition,
validInputs.content.identity.availability,
'Email Luca',
'Contact Me',
'Explore work',
]);
expect(hero.querySelector('a[href="mailto:hello@example.test"]')?.textContent).toBe('Email Luca');
expect(hero.querySelector('a[href="#contact"]')?.textContent).toBe('Contact Me');
expect(document.querySelector('[data-featured-operation]')?.textContent).toContain(
validInputs.content.infrastructure.featuredOperation.title,
);
@@ -94,8 +94,10 @@ describe('semantic portfolio document', () => {
expect([...navigation.querySelectorAll('a')].map((link) => link.getAttribute('href'))).toEqual([
'#start', '#profile', '#experience', '#infrastructure', '#selected-builds', '#additional-work', '#toolkit', '#contact',
]);
const email = document.querySelector('a[href="mailto:hello@example.test"]');
expect(email?.textContent).toBe('Email Luca');
const email = document.querySelector('#contact-email-details');
expect(email?.querySelector('summary')?.textContent).toBe('Email Me');
expect(email?.textContent).toContain('hello [at] example [dot] test');
expect(document.querySelector('a[href^="mailto:"]')).toBeNull();
expect(document.body.textContent).toContain('For a current CV or role-specific information, contact me directly.');
});

View File

@@ -160,6 +160,10 @@ describe('ProjectMedia', () => {
const video = wrapper.get('video');
expect(video.attributes('preload')).toBe('none');
expect(video.attributes()).toHaveProperty('loop');
expect(video.attributes()).toHaveProperty('muted');
expect(video.attributes()).not.toHaveProperty('autoplay');
expect(wrapper.get('.video-fallback').attributes('href')).toBe('/assets/project-loop.mp4');
expect(video.attributes()).not.toHaveProperty('controls');
expect(video.findAll('source')).toHaveLength(0);
expect(wrapper.get('button').text()).toBe('Play video');
@@ -175,15 +179,16 @@ describe('ProjectMedia', () => {
'/assets/project-loop.mp4',
]);
expect(HTMLMediaElement.prototype.play).toHaveBeenCalledOnce();
expect(HTMLMediaElement.prototype.load).toHaveBeenCalledOnce();
});
it('attaches inside 300 CSS pixels when automatic media is allowed and detaches offscreen', async () => {
it('attaches in the viewport when automatic media is allowed and detaches offscreen', async () => {
const wrapper = mountMedia({
kind: 'video', assetId: 'project-loop', posterAssetId: 'project-loop-poster',
alt: 'Gameplay excerpt.', caption: 'Gameplay.', longDescription: 'A gameplay loop.',
}, true);
const observer = IntersectionObserverFake.instances[0]!;
expect(observer.options?.rootMargin).toBe('300px 0px 300px 0px');
expect(observer.options?.rootMargin).toBe('0px');
observer.emit(true);
await nextTick();
@@ -238,6 +243,8 @@ describe('ProjectMediaGallery', () => {
expect(mediaProjects.map(({ id }) => id)).toEqual([
'private-medication-tracker',
'opinion-dynamics-simulation',
'private-game-prototype',
'private-healthcare-platform',
'currency-converter',
]);
@@ -247,7 +254,7 @@ describe('ProjectMediaGallery', () => {
}));
const images = wrappers.flatMap((wrapper) => wrapper.findAll('img'));
expect(images).toHaveLength(6);
expect(images).toHaveLength(7);
expect(images.every((image) => image.attributes('loading') === 'lazy')).toBe(true);
expect(images.every((image) => image.attributes('decoding') === 'async')).toBe(true);
expect(images.every((image) => Number(image.attributes('width')) > 0 && Number(image.attributes('height')) > 0)).toBe(true);
@@ -345,7 +352,7 @@ describe('media content schema', () => {
additional: [{ id: 'extra', title: 'Extra', visibility: 'public', maturity: 'learning', summary: 'S.', problem: 'P.', contribution: 'C.', technicalDecisions: ['D.'], outcome: 'O.', assetIds: [], media: [] }],
},
toolkit: [{ title: 'Tools', technologies: [{ name: 'Linux' }] }],
contact: { introduction: 'Open to roles.', email: 'hello@example.test', social: [], cvMessage: 'Contact directly.' },
contact: { introduction: 'Open to roles.', email: { localPart: 'hello', domain: 'example.test' }, social: [], cvMessage: 'Contact directly.' },
metadata: { title: 'Title.', description: 'Description.' },
sceneDescription: 'Scene.',
}));

View File

@@ -9,7 +9,7 @@ const socialLabels = content.contact.social.map((link) => link.service);
describe('official brand icon registry', () => {
it('resolves every toolkit technology with an honest official mark', () => {
for (const label of toolkitLabels.filter(brandIconAvailable)) {
for (const label of toolkitLabels) {
const icon = brandIconFor(label);
expect(icon.title).toBeTruthy();
expect(icon.hex).toMatch(/^[0-9A-F]{6}$/u);
@@ -31,8 +31,9 @@ describe('official brand icon registry', () => {
'Git', 'Python', 'TypeScript/JavaScript', 'Vue.js', 'Flutter/Dart', 'Fastify',
'C#/.NET', 'Headless WordPress', 'Java', 'PostgreSQL', 'Drift/SQLite',
'Drizzle ORM', 'PyTorch', 'Ultralytics/YOLO', 'OpenCV', 'GitHub', 'GitLab', 'LinkedIn',
'DNS', 'TLS', 'Fail2Ban', 'SQL',
]);
expect(['DNS', 'TLS', 'Fail2Ban', 'Btrfs', 'rsync', 'SQL', 'SPSS', 'Microsoft Excel', 'SoccerNet']
expect(['Btrfs', 'rsync', 'SPSS', 'Microsoft Excel', 'SoccerNet']
.every((label) => !brandIconAvailable(label))).toBe(true);
expect(() => brandIconFor('Unlisted technology')).toThrow(/No official brand icon/u);
});

View File

@@ -163,21 +163,21 @@ describe("semantic section components", () => {
expect(tile.findAll('[aria-label="Linux"]').length).toBe(0);
});
it("renders a generic toolkit concept as visible text without inventing an icon", () => {
it("renders a toolkit concept with a decorative vector symbol", () => {
const wrapper = mount(ToolkitGrid, {
props: { groups: [{ title: "Systems", technologies: [{ name: "DNS" }] }] },
});
const tile = wrapper.get('[data-testid="toolkit-technology"]');
expect(tile.text()).toBe("DNS");
expect(tile.find("svg").exists()).toBe(false);
expect(tile.get("span").classes()).toContain("toolkit-item__label--text-only");
expect(tile.find("svg").exists()).toBe(true);
expect(tile.get("svg").attributes('aria-hidden')).toBe('true');
});
it("renders a visible mailto contact and safe verified social links", () => {
it("keeps email behind an interaction and renders safe social links", () => {
const contact: ContactContent = {
introduction: "Open to System Administrator and Full-Stack Developer opportunities.",
email: "hello@example.test",
email: { localPart: "hello", domain: "example.test" },
cvMessage:
"For a current CV or role-specific information, contact me directly.",
social: [{ service: "GitHub", url: "https://github.com/example" }],
@@ -188,9 +188,8 @@ describe("semantic section components", () => {
},
});
expect(wrapper.get('a[href="mailto:hello@example.test"]').text()).toBe(
"Email Luca",
);
expect(wrapper.find('a[href^="mailto:"]').exists()).toBe(false);
expect(wrapper.get('summary').text()).toBe('Email Me');
expect(wrapper.text()).toContain(contact.introduction);
const social = wrapper.get('a[href="https://github.com/example"]');
expect(social.text()).toBe("GitHub");

View File

@@ -0,0 +1,52 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { afterEach, describe, expect, it, vi } from 'vitest';
import ContactDestination from '@/components/ContactDestination.vue';
import { parsePortfolioContent } from '@/content/schemas';
import content from '@/../content/public/portfolio-content.json';
const contact = parsePortfolioContent(content).contact;
afterEach(() => vi.unstubAllGlobals());
describe('contact disclosure', () => {
it('assembles a mail link only after pointer or keyboard interaction', async () => {
const wrapper = mount(ContactDestination, { props: { contact } });
await nextTick();
expect(wrapper.find('a[href^="mailto:"]').exists()).toBe(false);
expect(wrapper.get('.contact-email__preview').text()).toBe('');
await wrapper.get('.contact-email__action').trigger('focusin');
expect(wrapper.get('a.contact-link').attributes('href')).toBe('mailto:luca.pislaru@gmail.com');
expect(wrapper.get('a.contact-link').text()).toBe('Email Me');
wrapper.unmount();
});
it('reveals a copyable address through the native disclosure', async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
vi.stubGlobal('navigator', { clipboard: { writeText } });
const wrapper = mount(ContactDestination, { props: { contact } });
await nextTick();
const details = wrapper.get('details');
(details.element as HTMLDetailsElement).open = true;
await details.trigger('toggle');
expect(wrapper.get('summary').text()).toBe('Hide email');
expect(wrapper.get('.contact-email__address').text()).toBe('luca.pislaru@gmail.com');
await wrapper.get('.contact-email__copy').trigger('click');
expect(writeText).toHaveBeenCalledWith('luca.pislaru@gmail.com');
expect(wrapper.get('[role="status"]').text()).toBe('Copied');
await wrapper.get('.contact-email').trigger('keydown', { key: 'Escape' });
expect((details.element as HTMLDetailsElement).open).toBe(false);
wrapper.unmount();
});
it('keeps the address selectable when clipboard permission is unavailable', async () => {
vi.stubGlobal('navigator', { clipboard: { writeText: vi.fn().mockRejectedValue(new Error('Unavailable')) } });
const wrapper = mount(ContactDestination, { props: { contact } });
await nextTick();
(wrapper.get('details').element as HTMLDetailsElement).open = true;
await wrapper.get('details').trigger('toggle');
await wrapper.get('.contact-email__copy').trigger('click');
expect(wrapper.get('[role="status"]').text()).toBe('Select the address to copy it.');
expect(wrapper.get('.contact-email__address').text()).toContain('@');
wrapper.unmount();
});
});

View File

@@ -36,7 +36,7 @@ describe('safe metadata', () => {
expect(metadata.title).toBe('Luca Pîslaru — System Administrator & Full-Stack Developer');
expect(metadata.description).toBe(
'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.',
'I’m Luca Pîslaru, a System Administrator & Full-Stack Developer based in Bucharest. Explore my work in Linux infrastructure and end-to-end application development.',
);
expect(metadata.canonicalUrl).toBe('https://portfolio.example.test/');
const previewAsset = manifest.assets.find(({ id }) => id === content.metadata.socialPreviewAssetId)!;
@@ -59,7 +59,6 @@ describe('safe metadata', () => {
jobTitle: 'System Administrator & Full-Stack Developer',
description: metadata.description,
url: 'https://portfolio.example.test/',
email: 'mailto:luca.pislaru@gmail.com',
homeLocation: {
'@type': 'Place',
address: {
@@ -69,14 +68,14 @@ describe('safe metadata', () => {
},
},
sameAs: [
'https://linkedin.com/in/luca-pisl',
'https://github.com/LucaPisl',
'https://gitlab.com/luca_pisl',
'https://linkedin.com/in/luca-pisl',
],
},
});
expect(JSON.stringify(metadata.jsonLd)).not.toMatch(
/streetAddress|postalCode|telephone|interactionStatistic|"image"/iu,
/streetAddress|postalCode|telephone|interactionStatistic|"image"|"email"/iu,
);
const expectedHash = createHash('sha256').update(metadata.serializedJsonLd).digest('base64');
expect(metadata.jsonLdCspHash).toBe(`sha256-${expectedHash}`);