Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/pages/AboutPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<a v-lazy-link class="blog-link" title="send me an email" aria-label="send me an email" :href="`mailto:${profile.email}`"> email </a>
to discuss projects and ideas. While I’m not always available for freelance or long-term work, please don’t hesitate to reach out anytime.
</p>
<AboutConnectSkeletonPartial v-else />
</div>
</div>
</section>
Expand Down Expand Up @@ -90,6 +91,7 @@ import SideNavPartial from '@partials/SideNavPartial.vue';
import WidgetSocialPartial from '@partials/WidgetSocialPartial.vue';
import WidgetSkillsPartial from '@partials/WidgetSkillsPartial.vue';
import WidgetSkillsSkeletonPartial from '@partials/WidgetSkillsSkeletonPartial.vue';
import AboutConnectSkeletonPartial from '@partials/AboutConnectSkeletonPartial.vue';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_LD } from '@/support/seo';

import { useApiStore } from '@api/store.ts';
Expand Down
16 changes: 10 additions & 6 deletions src/pages/ResumePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@
</nav>
<!-- Page content -->
<div class="text-slate-500 dark:text-slate-400 space-y-12">
<span id="education" class="block h-0" aria-hidden="true"></span>
<EducationPartial v-if="education" :education="education" />
<span id="experience" class="block h-0" aria-hidden="true"></span>
<ExperiencePartial v-if="experience" :experience="experience" />
<span id="recommendations" class="block h-0" aria-hidden="true"></span>
<RecommendationPartial v-if="recommendations" :recommendations="recommendations" />
<ResumePageSkeletonPartial v-if="isLoadingProfile" />
<template v-else>
<span id="education" class="block h-0" aria-hidden="true"></span>
<EducationPartial v-if="education" :education="education" />
<span id="experience" class="block h-0" aria-hidden="true"></span>
<ExperiencePartial v-if="experience" :experience="experience" />
<span id="recommendations" class="block h-0" aria-hidden="true"></span>
<RecommendationPartial v-if="recommendations" :recommendations="recommendations" />
</template>
</div>
</section>
</div>
Expand Down Expand Up @@ -68,6 +71,7 @@ import WidgetLangPartial from '@partials/WidgetLangPartial.vue';
import WidgetSkillsPartial from '@partials/WidgetSkillsPartial.vue';
import WidgetSkillsSkeletonPartial from '@partials/WidgetSkillsSkeletonPartial.vue';
import RecommendationPartial from '@partials/RecommendationPartial.vue';
import ResumePageSkeletonPartial from '@partials/ResumePageSkeletonPartial.vue';

import { ref, onMounted } from 'vue';
import { useApiStore } from '@api/store.ts';
Expand Down
7 changes: 7 additions & 0 deletions src/partials/AboutConnectSkeletonPartial.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<div data-testid="about-connect-skeleton" class="space-y-3 animate-pulse" aria-hidden="true">
<div class="h-4 w-48 max-w-full bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-full max-w-[460px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-2/3 max-w-[280px] bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
</template>
79 changes: 79 additions & 0 deletions src/partials/ResumePageSkeletonPartial.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<template>
<div data-testid="resume-page-skeleton" class="space-y-12 animate-pulse" aria-hidden="true">
<section class="space-y-8">
<h2 class="h3 font-aspekta text-slate-800 dark:text-slate-100">Education</h2>
<ul class="space-y-8">
<li v-for="item in 2" :key="`resume-education-skeleton-${item}`" class="relative group">
<div
class="flex items-start before:absolute before:left-0 before:h-full before:w-px before:bg-slate-200 dark:before:bg-slate-800 before:self-start before:ml-[28px] before:-translate-x-1/2 before:translate-y-8 group-last-of-type:before:hidden"
>
<div
class="absolute left-0 h-14 w-14 flex items-center justify-center border border-slate-200 dark:border-slate-800 dark:bg-linear-to-t dark:from-slate-800 dark:to-slate-800/30 bg-white dark:bg-slate-900 rounded-full"
>
<div class="size-8 rounded-full bg-slate-200 dark:bg-slate-700"></div>
</div>
<div class="pl-20 space-y-3 w-full">
<div class="h-3 w-32 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-3/4 max-w-[320px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-1/2 max-w-[220px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="space-y-2 pt-1">
<div class="h-3 w-full max-w-[520px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-3 w-5/6 max-w-[440px] bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
</div>
</div>
</li>
</ul>
</section>
<section class="space-y-8">
<h2 class="h2 font-aspekta text-slate-700 dark:text-slate-300">Work Experience</h2>
<ul class="space-y-8">
<li v-for="item in 3" :key="`resume-experience-skeleton-${item}`" class="relative group">
<div
class="flex items-start before:absolute before:left-0 before:h-full before:w-px before:bg-slate-200 dark:before:bg-slate-800 before:self-start before:ml-[28px] before:-translate-x-1/2 before:translate-y-8 group-last-of-type:before:hidden"
>
<div
class="absolute left-0 h-14 w-14 flex items-center justify-center border border-slate-200 dark:border-slate-800 dark:bg-linear-to-t dark:from-slate-800 dark:to-slate-800/30 bg-white dark:bg-slate-900 rounded-full"
>
<div class="size-8 rounded-full bg-slate-200 dark:bg-slate-700"></div>
</div>
<div class="pl-20 space-y-3 w-full">
<div class="h-3 w-40 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-3/4 max-w-[360px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-1/2 max-w-[240px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-3 w-full max-w-[520px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-2.5 w-2/3 max-w-[320px] bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
</div>
</li>
</ul>
</section>
<section class="space-y-8">
<h2 class="h3 font-aspekta text-slate-800 dark:text-slate-100">Recommendations</h2>
<ul class="space-y-8">
<li v-for="item in 2" :key="`resume-recommendation-skeleton-${item}`" class="relative group">
<div class="flex items-start">
<div
class="absolute left-0 h-14 w-14 flex items-center justify-center border border-slate-200 dark:border-slate-800 dark:bg-linear-to-t dark:from-slate-800 dark:to-slate-800/30 bg-white dark:bg-slate-900 rounded-full"
>
<div class="h-14 w-14 rounded-full bg-slate-200 dark:bg-slate-700"></div>
</div>
<div class="pl-20 space-y-3 w-full">
<div class="h-4 w-2/3 max-w-[320px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-4 w-1/2 max-w-[220px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="flex justify-between text-xs text-slate-400 dark:text-slate-500 pb-2">
<div class="h-3 w-24 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-3 w-20 bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
<div class="space-y-2">
<div class="h-3 w-full max-w-[520px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-3 w-5/6 max-w-[440px] bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-3 w-2/3 max-w-[360px] bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
</div>
</div>
</li>
</ul>
</section>
</div>
</template>
24 changes: 23 additions & 1 deletion tests/pages/AboutPage.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mount, flushPromises } from '@vue/test-utils';
import { faker } from '@faker-js/faker';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, afterEach } from 'vitest';
import AboutPage from '@pages/AboutPage.vue';
import type { ProfileResponse, ProfileSkillResponse } from '@api/response/index.ts';

Expand Down Expand Up @@ -28,6 +28,10 @@
vi.mock('@api/http-error.ts', () => ({ debugError: vi.fn() }));

describe('AboutPage', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('shows formatted nickname', async () => {
const wrapper = mount(AboutPage, {
global: {
Expand All @@ -46,10 +50,28 @@
expect(wrapper.find('h1').text()).toContain(formatted);
});

it('renders skeleton while loading the profile', () => {
getProfile.mockReturnValueOnce(new Promise(() => {}));

const wrapper = mount(AboutPage, {
global: {
stubs: {
SideNavPartial: true,
HeaderPartial: true,
WidgetSocialPartial: true,
WidgetSkillsPartial: true,
FooterPartial: true,
},
},
});

expect(wrapper.find('[data-testid="about-connect-skeleton"]').exists()).toBe(true);
});

it('handles profile errors gracefully', async () => {
const error = new Error('fail');
getProfile.mockRejectedValueOnce(error);
const wrapper = mount(AboutPage, {

Check warning on line 74 in tests/pages/AboutPage.test.ts

View workflow job for this annotation

GitHub Actions / format

'wrapper' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 74 in tests/pages/AboutPage.test.ts

View workflow job for this annotation

GitHub Actions / format

'wrapper' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 74 in tests/pages/AboutPage.test.ts

View workflow job for this annotation

GitHub Actions / format

'wrapper' is assigned a value but never used. Allowed unused vars must match /^_/u
global: {
stubs: {
SideNavPartial: true,
Expand Down
27 changes: 26 additions & 1 deletion tests/pages/ResumePage.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mount, flushPromises } from '@vue/test-utils';
import { faker } from '@faker-js/faker';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, afterEach } from 'vitest';
import ResumePage from '@pages/ResumePage.vue';
import type { ProfileResponse, ProfileSkillResponse, EducationResponse, ExperienceResponse, RecommendationsResponse } from '@api/response/index.ts';

Expand Down Expand Up @@ -64,6 +64,10 @@
vi.mock('@api/http-error.ts', () => ({ debugError: vi.fn() }));

describe('ResumePage', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('fetches data on mount', async () => {
const wrapper = mount(ResumePage, {
global: {
Expand All @@ -87,10 +91,31 @@
expect(wrapper.find('h1').text()).toContain('My resume');
});

it('renders skeleton while the resume data is loading', () => {
getProfile.mockReturnValueOnce(new Promise(() => {}));
getExperience.mockReturnValueOnce(new Promise(() => {}));
getRecommendations.mockReturnValueOnce(new Promise(() => {}));
getEducation.mockReturnValueOnce(new Promise(() => {}));

const wrapper = mount(ResumePage, {
global: {
stubs: {
SideNavPartial: true,
HeaderPartial: true,
FooterPartial: true,
WidgetLangPartial: true,
WidgetSkillsPartial: true,
},
},
});

expect(wrapper.find('[data-testid="resume-page-skeleton"]').exists()).toBe(true);
});

it('handles fetch failures', async () => {
const error = new Error('oops');
getProfile.mockRejectedValueOnce(error);
const wrapper = mount(ResumePage, {

Check warning on line 118 in tests/pages/ResumePage.test.ts

View workflow job for this annotation

GitHub Actions / format

'wrapper' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 118 in tests/pages/ResumePage.test.ts

View workflow job for this annotation

GitHub Actions / format

'wrapper' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 118 in tests/pages/ResumePage.test.ts

View workflow job for this annotation

GitHub Actions / format

'wrapper' is assigned a value but never used. Allowed unused vars must match /^_/u
global: {
stubs: {
SideNavPartial: true,
Expand Down
Loading