feat: refine portfolio content, contact, and supporting media
This commit is contained in:
@@ -79,7 +79,7 @@ async function enterState(
|
||||
for (const link of links) link.closest("li")?.remove();
|
||||
});
|
||||
await expect(page.locator('#contact a[target="_blank"]')).toHaveCount(0);
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ test("forced colors preserves visible focus, controls, content, and the Contact
|
||||
|
||||
await clickNavigationLink(page, "#contact");
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() =>
|
||||
|
||||
@@ -151,7 +151,7 @@ async function assertReflowGeometry(page: Page): Promise<void> {
|
||||
|
||||
await clickNavigationLink(page, "#contact");
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
}
|
||||
|
||||
test("reflows at a 320 CSS pixel mobile viewport without lost content or controls", async ({
|
||||
|
||||
96
tests/browser/contact-refinements.spec.ts
Normal file
96
tests/browser/contact-refinements.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
|
||||
test('start navigation, contact identity, and toolkit icons use the approved content', async ({ page, distUrl }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
const response = await page.goto(distUrl);
|
||||
const html = await response!.text();
|
||||
expect(html).not.toContain('luca.pislaru@gmail.com');
|
||||
expect(html).not.toContain('mailto:');
|
||||
await expect(page.locator('.contact-email')).toHaveAttribute('data-enhanced', 'true');
|
||||
await page.locator('.hero-journey__action--primary').click();
|
||||
await expect(page).toHaveURL(/#contact$/);
|
||||
await expect(page.locator('#contact')).toBeInViewport();
|
||||
await expect(page.locator('.contact-avatar')).toBeVisible();
|
||||
await expect(page.locator('.contact-social-link')).toHaveText(['LinkedIn', 'GitHub', 'GitLab']);
|
||||
const missing = await page.locator('[data-testid="toolkit-technology"]').evaluateAll(items => items.filter(item => !item.querySelector('svg')).map(item => item.textContent));
|
||||
expect(missing).toEqual([]);
|
||||
for (const name of ['SPSS', 'SoccerNet', 'Microsoft Excel', 'Btrfs', 'rsync']) {
|
||||
await expect(page.locator('#toolkit')).not.toContainText(name);
|
||||
}
|
||||
});
|
||||
|
||||
test('email is revealed on focus, with an explicit disclosure and copy fallback', async ({ page, distUrl }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(`${distUrl}/#contact`);
|
||||
const email = page.getByRole('link', { name: 'Email Me', exact: true });
|
||||
await expect(email).toHaveAttribute('href', '#contact-email-details');
|
||||
await expect(page.locator('.contact-email__preview')).not.toBeVisible();
|
||||
await email.focus();
|
||||
await expect(email).toHaveAttribute('href', 'mailto:luca.pislaru@gmail.com');
|
||||
await page.locator('#contact summary').click();
|
||||
await expect(page.locator('.contact-email__address')).toHaveText('luca.pislaru@gmail.com');
|
||||
await page.getByRole('button', { name: 'Copy email' }).click();
|
||||
await expect(page.locator('.contact-email__status')).toHaveText(/Copied|Select the address to copy it\./);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.locator('#contact summary')).toBeFocused();
|
||||
await expect(page.locator('#contact-email-details')).not.toHaveAttribute('open');
|
||||
});
|
||||
|
||||
test('contact fits desktop and stays aligned and usable across narrow widths', async ({ page, distUrl }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'desktop-chromium', 'Viewport matrix is run once.');
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(`${distUrl}/#contact`);
|
||||
for (const [width, height] of [[1440, 900], [1280, 720], [768, 1024], [390, 844], [320, 740], [844, 390]]) {
|
||||
await page.setViewportSize({ width: width!, height: height! });
|
||||
await page.locator('#contact').scrollIntoViewIfNeeded();
|
||||
const geometry = await page.locator('#contact').evaluate(section => {
|
||||
const box = section.getBoundingClientRect();
|
||||
const avatar = section.querySelector('.contact-avatar')!.getBoundingClientRect();
|
||||
return {
|
||||
height: box.height,
|
||||
overflow: document.documentElement.scrollWidth > innerWidth,
|
||||
socialY: [...section.querySelectorAll('.contact-social-link')].map(el => el.getBoundingClientRect().y),
|
||||
avatarInside: avatar.right <= box.right && avatar.bottom < section.querySelector('.contact-destination__copy')!.getBoundingClientRect().top,
|
||||
};
|
||||
});
|
||||
expect(geometry.overflow, `${width}px overflow`).toBe(false);
|
||||
expect(Math.max(...geometry.socialY) - Math.min(...geometry.socialY), `${width}px social alignment`).toBeLessThan(1);
|
||||
expect(geometry.avatarInside, `${width}px avatar`).toBe(true);
|
||||
if (width! >= 1280) expect(geometry.height).toBeLessThan(height! - 140);
|
||||
await page.screenshot({ path: testInfo.outputPath(`contact-${width}x${height}.png`) });
|
||||
}
|
||||
});
|
||||
|
||||
test('native email disclosure remains usable without JavaScript', async ({ browser, distUrl }) => {
|
||||
const context = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 844 } });
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${distUrl}/#contact`);
|
||||
await expect(page.locator('.contact-email__address')).not.toBeVisible();
|
||||
await page.locator('#contact summary').click();
|
||||
await expect(page.locator('.contact-email__address')).toHaveText('luca.pislaru [at] gmail [dot] com');
|
||||
await expect(page.locator('.video-fallback')).toHaveAttribute('href', '/assets/project-private-game-loop.mp4');
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('reduced motion keeps the gameplay still until Play and supports pause', async ({ page, distUrl }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(distUrl);
|
||||
const figure = page.locator('.project-media--video');
|
||||
await figure.scrollIntoViewIfNeeded();
|
||||
await expect(figure.locator('source')).toHaveCount(0);
|
||||
await figure.getByRole('button', { name: 'Play video' }).click();
|
||||
await expect(figure.getByRole('button', { name: 'Pause video' })).toBeVisible();
|
||||
expect(await figure.locator('video').evaluate(element => { const video = element as HTMLVideoElement; return { muted: video.muted, loop: video.loop, paused: video.paused }; })).toEqual({ muted: true, loop: true, paused: false });
|
||||
await figure.getByRole('button', { name: 'Pause video' }).click();
|
||||
await expect(figure.getByRole('button', { name: 'Play video' })).toBeVisible();
|
||||
await expect(figure.locator('source')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('contact and supporting media have no serious accessibility violations', async ({ page, distUrl }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(`${distUrl}/#contact`);
|
||||
await page.locator('#contact summary').click();
|
||||
const result = await new AxeBuilder({ page }).include('#contact').include('#additional-work').analyze();
|
||||
expect(result.violations.filter(violation => ['serious', 'critical'].includes(violation.impact ?? ''))).toEqual([]);
|
||||
});
|
||||
@@ -226,10 +226,10 @@ test('an aborted generated client entry leaves the complete SSR portfolio usable
|
||||
await expect(page.locator('main > section')).toHaveCount(8);
|
||||
await expect(page.locator('nav a')).toHaveCount(8);
|
||||
await expect(
|
||||
page.locator('a[href="mailto:luca.pislaru@gmail.com"]'),
|
||||
page.locator('#contact summary'),
|
||||
).toBeVisible();
|
||||
await page.locator('nav a[href="#contact"]').click();
|
||||
await expect(page).toHaveURL(`${distUrl}/#contact`);
|
||||
await expect(page.locator('#contact')).toBeInViewport();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ test("tabs from the skip link through navigation and every contact action", asyn
|
||||
const expectedTargets = [
|
||||
{ selector: ".skip-link", href: "#portfolio-content" },
|
||||
...navHrefs.map((href) => ({ selector: `nav a[href="${href}"]`, href })),
|
||||
{ selector: '.hero-journey__action--primary', href: 'mailto:luca.pislaru@gmail.com' },
|
||||
{ selector: '.hero-journey__action--primary', href: '#contact' },
|
||||
{ selector: '.hero-journey__actions a[href="#selected-builds"]', href: "#selected-builds" },
|
||||
{
|
||||
selector: 'a[href="https://github.com/LucaPisl/footy-tracker"]',
|
||||
@@ -66,6 +66,14 @@ test("tabs from the skip link through navigation and every contact action", asyn
|
||||
'a[href="https://github.com/LucaPisl/opinion-dynamics-simulation"]',
|
||||
href: "https://github.com/LucaPisl/opinion-dynamics-simulation",
|
||||
},
|
||||
{
|
||||
selector: '.video-controls button',
|
||||
href: null,
|
||||
},
|
||||
{
|
||||
selector: '.video-fallback',
|
||||
href: '/assets/project-private-game-loop.mp4',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'a[href="https://github.com/LucaPisl/CurrencyConverterAvalonia"]',
|
||||
@@ -76,9 +84,17 @@ test("tabs from the skip link through navigation and every contact action", asyn
|
||||
href: "https://github.com/LucaPisl/LinuxModdingGuide",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href^="mailto:"]',
|
||||
selector: '#contact :is(a, summary):text-is("Email Me")',
|
||||
href: "mailto:luca.pislaru@gmail.com",
|
||||
},
|
||||
{
|
||||
selector: '#contact summary',
|
||||
href: null,
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href="https://linkedin.com/in/luca-pisl"]',
|
||||
href: "https://linkedin.com/in/luca-pisl",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href="https://github.com/LucaPisl"]',
|
||||
href: "https://github.com/LucaPisl",
|
||||
@@ -87,17 +103,13 @@ test("tabs from the skip link through navigation and every contact action", asyn
|
||||
selector: '#contact a[href="https://gitlab.com/luca_pisl"]',
|
||||
href: "https://gitlab.com/luca_pisl",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href="https://linkedin.com/in/luca-pisl"]',
|
||||
href: "https://linkedin.com/in/luca-pisl",
|
||||
},
|
||||
];
|
||||
|
||||
for (const target of expectedTargets) {
|
||||
await page.keyboard.press("Tab");
|
||||
const locator = page.locator(target.selector);
|
||||
await expect(locator).toBeFocused();
|
||||
await expect(locator).toHaveAttribute("href", target.href);
|
||||
if (target.href !== null) await expect(locator).toHaveAttribute("href", target.href);
|
||||
await assertVisibleUnobscuredFocus(page);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ test('F7 replaces a failed poster with exactly one CSS fallback and keeps its de
|
||||
expect(await readUnhandledRejections(page)).toEqual([]);
|
||||
});
|
||||
|
||||
test('F8 remains authenticated and request-free while the public manifest has no approved video', async ({ page, distUrl }) => {
|
||||
test('F8 remains authenticated and request-free before the gameplay video is requested', async ({ page, distUrl }) => {
|
||||
const mediaRequests: string[] = [];
|
||||
page.on('request', (request) => {
|
||||
if (/\.(?:mp4|webm)(?:$|[?#])/iu.test(request.url())) mediaRequests.push(request.url());
|
||||
@@ -40,7 +40,8 @@ test('F8 remains authenticated and request-free while the public manifest has no
|
||||
await page.goto(authorizedFaultUrl(distUrl, 'F8-video-failure'), { waitUntil: 'networkidle' });
|
||||
|
||||
expect((await readFaultSnapshot(page)).fault).toBe('F8-video-failure');
|
||||
await expect(page.locator('video, video source')).toHaveCount(0);
|
||||
await expect(page.locator('video')).toHaveCount(1);
|
||||
await expect(page.locator('video source')).toHaveCount(0);
|
||||
expect(mediaRequests).toEqual([]);
|
||||
await assertSemanticFaultOracle(page);
|
||||
expect(errors.pageErrors).toEqual([]);
|
||||
|
||||
@@ -31,7 +31,7 @@ test('approved still evidence uses manifest assets and intrinsic lazy image sema
|
||||
await expect(page.locator('main > section')).toHaveCount(8);
|
||||
await expect(page.locator('#selected-builds article')).toHaveCount(3);
|
||||
await expect(page.locator('#additional-work article')).toHaveCount(4);
|
||||
await expect(page.locator('[data-project-media]')).toHaveCount(approvedStillCount);
|
||||
await expect(page.locator('[data-project-media]')).toHaveCount(projectPresentations.length);
|
||||
await expect(page.locator('[data-project-media] img')).toHaveCount(expectedImages.length);
|
||||
expect(await page.locator('[data-project-media] img').evaluateAll((images) => images.map((image) => ({
|
||||
src: new URL((image as HTMLImageElement).src).pathname,
|
||||
@@ -41,8 +41,9 @@ test('approved still evidence uses manifest assets and intrinsic lazy image sema
|
||||
loading: (image as HTMLImageElement).loading,
|
||||
decoding: (image as HTMLImageElement).decoding,
|
||||
})))).toEqual(expectedImages.map((image) => ({ ...image, loading: 'lazy', decoding: 'async' })));
|
||||
await expect(page.locator('video, video source')).toHaveCount(0);
|
||||
await expect(page.locator('[data-project-id="private-medication-tracker"] a')).toHaveCount(0);
|
||||
await expect(page.locator('video')).toHaveCount(1);
|
||||
await expect(page.locator('video source')).toHaveCount(0);
|
||||
await expect(page.locator('[data-project-id="private-medication-tracker"] a.project-link')).toHaveCount(0);
|
||||
await expect(page.locator('.journey-poster')).not.toHaveAttribute('src', /.+/u);
|
||||
expect(videoRequests).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -37,10 +37,10 @@ const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> =
|
||||
};
|
||||
|
||||
const visibleTextLeafSelector =
|
||||
"h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,svg text";
|
||||
"h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,summary,svg text";
|
||||
const visibleTextSelector = [
|
||||
".site-header :is(h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a)",
|
||||
".portfolio-main :is(h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a)",
|
||||
".portfolio-main :is(h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,summary)",
|
||||
".portfolio-main svg text",
|
||||
].join(",");
|
||||
|
||||
@@ -111,6 +111,7 @@ const expectedVisibleTextSegments = (
|
||||
(project.media ?? []).flatMap((presentation) => [
|
||||
presentation.caption,
|
||||
presentation.longDescription,
|
||||
...(presentation.kind === "video" ? ["Open gameplay video"] : []),
|
||||
...(presentation.kind === "chart"
|
||||
? [
|
||||
"Variables",
|
||||
@@ -136,7 +137,7 @@ const expectedVisibleTextSegments = (
|
||||
content.identity.role,
|
||||
content.identity.valueProposition,
|
||||
content.identity.availability,
|
||||
"Email Luca",
|
||||
"Contact Me",
|
||||
"Explore work",
|
||||
...section("profile", [
|
||||
content.profile.introduction,
|
||||
@@ -197,7 +198,6 @@ const expectedVisibleTextSegments = (
|
||||
project.summary,
|
||||
...projectMediaText(project),
|
||||
project.contribution,
|
||||
project.outcome,
|
||||
...projectLinkText(project),
|
||||
]),
|
||||
),
|
||||
@@ -212,8 +212,7 @@ const expectedVisibleTextSegments = (
|
||||
"contact",
|
||||
[
|
||||
content.contact.introduction,
|
||||
"Email Luca",
|
||||
content.contact.email,
|
||||
"Email Me",
|
||||
content.contact.cvMessage,
|
||||
...content.contact.social.map((link) => link.service),
|
||||
],
|
||||
@@ -230,6 +229,7 @@ const rawVisibleTextSegments = async (
|
||||
({ html, leafSelector, rootSelector }) => {
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
const isHidden = (element: Element): boolean => {
|
||||
if (element.closest('.project-media--video[data-enhanced="false"] .video-controls')) return true;
|
||||
for (
|
||||
let current: Element | null = element;
|
||||
current;
|
||||
@@ -420,12 +420,14 @@ test("keeps every raw SSR reading-path segment visible without JavaScript", asyn
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
const email = page.locator('a[href="mailto:luca.pislaru@gmail.com"]');
|
||||
await expect(email).toHaveCount(2);
|
||||
await expect(email).toHaveText(["Email Luca", "Email Luca"]);
|
||||
await expect(page.locator('a[href^="mailto:"]')).toHaveCount(0);
|
||||
const email = page.locator('#contact-email-details');
|
||||
await email.locator('summary').click();
|
||||
await expect(email.locator('.contact-email__address')).toBeVisible();
|
||||
await expect(email.locator('.contact-email__address')).toHaveText('luca.pislaru [at] gmail [dot] com');
|
||||
await expect(
|
||||
page.getByText(
|
||||
"For a current CV or role-specific information, contact me directly.",
|
||||
"Get in touch for my CV or to talk about a role.",
|
||||
{ exact: true },
|
||||
),
|
||||
).toBeVisible();
|
||||
@@ -528,5 +530,5 @@ test("retains native section navigation when scripts are disabled", async ({
|
||||
|
||||
await expect(page).toHaveURL(`${distUrl}/#contact`);
|
||||
await expect(page.locator("#contact")).toBeInViewport();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ test('boots reduced motion without requesting the live scene or automatic media'
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.locator('.journey-hud')).toBeVisible();
|
||||
await expect(page.locator('nav[aria-label="Portfolio sections"]')).toBeVisible();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
await expect(page.getByRole('status')).toContainText('reduced motion requested');
|
||||
expect(requests.some((url) => environmentChunkPattern.test(url))).toBe(false);
|
||||
await expect(page.locator('video[src], video source[src]')).toHaveCount(0);
|
||||
|
||||
@@ -151,7 +151,7 @@ test('renders the validated eight-section portfolio structure before responsive
|
||||
.toEqual(['#start', '#profile', '#experience', '#infrastructure', '#selected-builds', '#additional-work', '#toolkit', '#contact']);
|
||||
await expect(page.locator('#selected-builds article, #additional-work article')).not.toHaveCount(0);
|
||||
await expect(page.locator('#toolkit [data-testid="toolkit-technology"]')).not.toHaveCount(0);
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toHaveCount(1);
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toHaveCount(1);
|
||||
await expect(page.locator('#contact a[target="_blank"]')).not.toHaveCount(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -100,11 +100,10 @@ test('exposes the complete semantic portfolio from the generated document', asyn
|
||||
);
|
||||
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.getByRole('link', { name: 'Contact Me' })).toHaveAttribute('href', '#contact');
|
||||
await expect(page.getByRole('link', { name: 'Email Me' })).toHaveCount(1);
|
||||
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');
|
||||
await expect(page.locator('[data-testid="toolkit-technology"]', { hasText: 'DNS' }).locator('svg')).toHaveCount(1);
|
||||
expect(await page.locator('.hero-journey').evaluate((hero) => {
|
||||
const content = hero.querySelector('.hero-journey__content')!;
|
||||
const heroStyle = getComputedStyle(hero);
|
||||
@@ -172,8 +171,10 @@ test('keeps canonical native anchors and safe external destinations', async ({
|
||||
}
|
||||
|
||||
const email = page.locator('a[href^="mailto:"]');
|
||||
await expect(email).toHaveCount(2);
|
||||
await expect(email).toHaveText(['Email Luca', 'Email Luca']);
|
||||
await expect(email).toHaveCount(0);
|
||||
await page.getByRole('link', { name: 'Email Me' }).focus();
|
||||
await expect(email).toHaveCount(1);
|
||||
await expect(email).toHaveText('Email Me');
|
||||
expect(await email.evaluateAll((links) => [...new Set(links.map((link) => link.getAttribute('href')))])).toEqual(
|
||||
['mailto:luca.pislaru@gmail.com'],
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ test("falls back to one poster and no scene loop when the deferred scene chunk i
|
||||
await expect(page.locator("main > section")).toHaveCount(8);
|
||||
await revealNavigationLinks(page);
|
||||
await expect(page.locator('nav a[href="#contact"]')).toBeVisible();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
@@ -74,7 +74,7 @@ test("contains a WebGL initialization failure without hiding the portfolio", asy
|
||||
await expect(page.locator(".journey-poster")).toHaveCount(1);
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await expect(page.locator("#portfolio-content")).toBeVisible();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
|
||||
@@ -466,7 +466,7 @@ test.describe("production visual composition", () => {
|
||||
});
|
||||
await page.locator('nav a[href="#contact"]').click();
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
await expect(page.locator('#contact a[target="_blank"]')).toHaveCount(0);
|
||||
await captureViewport(page, testInfo, "visual-missing-social");
|
||||
});
|
||||
@@ -482,7 +482,7 @@ test.describe("production visual composition", () => {
|
||||
await expect(page.locator(".journey-hud__section")).toHaveText("Contact");
|
||||
await expect(page.locator(".journey-hud__arrival")).toHaveText("ARRIVED");
|
||||
await expect(page.locator("#contact")).toBeInViewport();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
|
||||
await captureViewport(page, testInfo, "visual-contact-arrival");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ const digest = 'a'.repeat(64);
|
||||
const canonical = 'https://portfolio.example/';
|
||||
const alternate = 'http://www.portfolio.example/';
|
||||
const escapedTitle = 'Luca Pîslaru — System Administrator & Full-Stack Developer';
|
||||
const description = 'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.';
|
||||
const description = '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.';
|
||||
const socialImage = `${canonical}assets/project-social-preview.webp`;
|
||||
const projectStillMaxBytes = 400 * 1024;
|
||||
const socialImageFixture = Buffer.alloc(15_068);
|
||||
@@ -27,7 +27,7 @@ const html = `<!doctype html><html><head>
|
||||
<meta name="twitter:title" content="${escapedTitle}" />
|
||||
<meta name="twitter:description" content="${description}" />
|
||||
<meta name="twitter:image" content="${socialImage}" />
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"ProfilePage","name":"Luca Pîslaru","description":"${description}","url":"${canonical}","mainEntity":{"@type":"Person","name":"Luca Pîslaru","jobTitle":"System Administrator & Full-Stack Developer","description":"${description}","url":"${canonical}","email":"mailto:luca.pislaru@gmail.com","homeLocation":{"@type":"Place","address":{"@type":"PostalAddress","addressLocality":"Bucharest","addressCountry":"RO"}},"sameAs":["https://github.com/LucaPisl","https://gitlab.com/luca_pisl","https://linkedin.com/in/luca-pisl"]}}</script>
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"ProfilePage","name":"Luca Pîslaru","description":"${description}","url":"${canonical}","mainEntity":{"@type":"Person","name":"Luca Pîslaru","jobTitle":"System Administrator & Full-Stack Developer","description":"${description}","url":"${canonical}","homeLocation":{"@type":"Place","address":{"@type":"PostalAddress","addressLocality":"Bucharest","addressCountry":"RO"}},"sameAs":["https://linkedin.com/in/luca-pisl","https://github.com/LucaPisl","https://gitlab.com/luca_pisl"]}}</script>
|
||||
<script type="module" src="/assets/app.12345678.js"></script></head>
|
||||
<body><main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><section><h2>Selected builds</h2><p>Evidence-led project work.</p></section><section><h2>Contact</h2><p>Discuss systems and software work.</p><a href="mailto:hello@example.com">Contact</a></section></main></body></html>`;
|
||||
|
||||
|
||||
2
tests/fixtures/portfolio-content.valid.json
vendored
2
tests/fixtures/portfolio-content.valid.json
vendored
@@ -111,7 +111,7 @@
|
||||
}],
|
||||
"contact": {
|
||||
"introduction": "Open to System Administrator and Full-Stack Developer opportunities.",
|
||||
"email": "hello@example.test",
|
||||
"email": { "localPart": "hello", "domain": "example.test" },
|
||||
"social": [{ "service": "GitHub", "url": "https://github.com/example" }],
|
||||
"cvMessage": "For a current CV or role-specific information, contact me directly."
|
||||
},
|
||||
|
||||
@@ -18,11 +18,13 @@ const contentTypes: Readonly<Record<string, string>> = {
|
||||
".json": "application/json; charset=utf-8",
|
||||
".map": "application/json; charset=utf-8",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".opus": "audio/ogg",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml; charset=utf-8",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".webp": "image/webp",
|
||||
".webm": "video/webm",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".xml": "application/xml; charset=utf-8",
|
||||
|
||||
@@ -133,7 +133,7 @@ export const assertSemanticFaultOracle = async (page: Page): Promise<void> => {
|
||||
await expect(page.locator('main > section')).toHaveCount(8);
|
||||
await expect(page.locator('nav[aria-label="Portfolio sections"] a')).toHaveCount(8);
|
||||
await expect(page.locator('nav a[href="#contact"]')).toBeVisible();
|
||||
const email = page.locator('#contact a[href^="mailto:"]');
|
||||
const email = page.locator('#contact :is(a, summary):text-is("Email Me")');
|
||||
await expect(email).toBeVisible();
|
||||
for (const title of [
|
||||
'Private self-hosted medication tracker',
|
||||
|
||||
@@ -67,9 +67,11 @@ describe('raw prerendered document', () => {
|
||||
expect(document.body.textContent).toContain('Secure self-hosted platform and lifecycle');
|
||||
expect(document.body.textContent).toContain('Private self-hosted medication tracker');
|
||||
expect(document.body.textContent).toContain('Linux');
|
||||
const mailLinks = document.querySelectorAll('a[href="mailto:luca.pislaru@gmail.com"]');
|
||||
expect(mailLinks).toHaveLength(2);
|
||||
expect([...mailLinks].map((link) => link.textContent)).toEqual(['Email Luca', 'Email Luca']);
|
||||
expect(document.querySelectorAll('a[href^="mailto:"]')).toHaveLength(0);
|
||||
expect(html).not.toContain('luca.pislaru@gmail.com');
|
||||
expect(document.querySelector('#start a[href="#contact"]')?.textContent).toBe('Contact Me');
|
||||
expect(document.querySelector('#contact-email-details summary')?.textContent).toBe('Email Me');
|
||||
expect(document.querySelector('.contact-email__address')?.textContent).toBe('luca.pislaru [at] gmail [dot] com');
|
||||
expect(document.querySelector('nav a[href="#infrastructure"]')).not.toBeNull();
|
||||
expect(snapshot).not.toBeNull();
|
||||
expect(moduleScript).not.toBeNull();
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('approved public portfolio projection', () => {
|
||||
]);
|
||||
expect(content.projects.selected[1]?.contribution).toMatch(/collaborator/iu);
|
||||
expect(content.projects.selected[1]?.outcome).toMatch(/0\.942 mAP@0\.5.*0\.198/iu);
|
||||
expect(content.projects.selected[0]?.outcome).toContain('not presented as production-ready.');
|
||||
expect(content.projects.selected[0]?.outcome).toContain('further work needed before a wider release.');
|
||||
expect(content.systemsOperations.title).toBe('Professional systems operations');
|
||||
expect(content.infrastructure.title).toBe('Secure self-hosted platform and lifecycle');
|
||||
expect(content.projects.selected).toHaveLength(3);
|
||||
@@ -98,13 +98,14 @@ describe('approved public portfolio projection', () => {
|
||||
.toBe('Private game prototype');
|
||||
expect(content.toolkit.flatMap((group) => group.technologies).map((technology) => technology.name))
|
||||
.toEqual(expect.arrayContaining(['Linux', 'Docker', 'Python', 'Vue.js', 'Java']));
|
||||
expect(content.contact).toEqual(expect.objectContaining({ email: 'luca.pislaru@gmail.com' }));
|
||||
expect(content.contact).toEqual(expect.objectContaining({ email: { localPart: 'luca.pislaru', domain: 'gmail.com' } }));
|
||||
expect(content.contact.social.map((link) => link.url)).toEqual([
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
'https://github.com/LucaPisl',
|
||||
'https://gitlab.com/luca_pisl',
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
]);
|
||||
const publishedReferences = [
|
||||
content.contact.avatarAssetId,
|
||||
...content.projects.selected.flatMap((project) => project.assetIds),
|
||||
...content.projects.additional.flatMap((project) => project.assetIds),
|
||||
...content.toolkit.flatMap((group) => group.technologies.flatMap((technology) =>
|
||||
|
||||
@@ -158,32 +158,30 @@ describe('artifact verification', () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'a missing hero mailto link',
|
||||
mutate: (html: string) => html.replace('href="mailto:luca.pislaru@gmail.com">Email Luca</a>', 'href="#selected-builds">Email Luca</a>'),
|
||||
name: 'a missing hero contact link',
|
||||
mutate: (html: string) => html.replace('href="#contact">Contact Me</a>', 'href="#selected-builds">Contact Me</a>'),
|
||||
},
|
||||
{
|
||||
name: 'a duplicate mailto link beyond the hero and contact actions',
|
||||
mutate: (html: string) => html.replace('</main>', '<a href="mailto:luca.pislaru@gmail.com">Email Luca</a></main>'),
|
||||
},
|
||||
{
|
||||
name: 'a mailto link with an address that differs from public content',
|
||||
mutate: (html: string) => html.replace('href="mailto:luca.pislaru@gmail.com">Email Luca</a>', 'href="mailto:wrong@example.test">Email Luca</a>'),
|
||||
name: 'a missing native email disclosure',
|
||||
mutate: (html: string) => html.replace('<summary>Email Me</summary>', '<span>Email Me</span>'),
|
||||
},
|
||||
{
|
||||
name: 'a hidden mailto link outside the app root',
|
||||
mutate: (html: string) => html.replace('</body>', '<a hidden href="mailto:luca.pislaru@gmail.com">Email Luca</a></body>'),
|
||||
},
|
||||
{
|
||||
name: 'two mailto links placed beneath contact instead of one per section',
|
||||
mutate: (html: string) => html
|
||||
.replace('href="mailto:luca.pislaru@gmail.com">Email Luca</a>', 'href="#selected-builds">Email Luca</a>')
|
||||
.replace('</footer>', '<a href="mailto:luca.pislaru@gmail.com">Email Luca</a></footer>'),
|
||||
name: 'a plain address hidden in initial page data',
|
||||
mutate: (html: string) => html.replace('</footer>', '<span hidden>luca.pislaru@gmail.com</span></footer>'),
|
||||
},
|
||||
])('rejects $name', async ({ mutate }) => {
|
||||
const { directory, html, inputs, contract } = await writeValidArtifacts();
|
||||
await writeFile(join(directory, 'index.html'), mutate(html));
|
||||
|
||||
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/mailto/i);
|
||||
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/contact|email|mailto/i);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
|
||||
|
||||
@@ -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.',
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
52
tests/unit/contact-interaction.test.ts
Normal file
52
tests/unit/contact-interaction.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user