fix: close final portfolio quality gaps
This commit is contained in:
@@ -1,105 +1,168 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import ContactDestination from '@/components/ContactDestination.vue';
|
||||
import InfrastructureDiagram from '@/components/InfrastructureDiagram.vue';
|
||||
import ProjectCaseStudy from '@/components/ProjectCaseStudy.vue';
|
||||
import ToolkitGrid from '@/components/ToolkitGrid.vue';
|
||||
import type { ContactContent, InfrastructureCaseStudy, PublicProject, ToolkitGroup } from '@/content/types';
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import ContactDestination from "@/components/ContactDestination.vue";
|
||||
import InfrastructureDiagram from "@/components/InfrastructureDiagram.vue";
|
||||
import ProjectCaseStudy from "@/components/ProjectCaseStudy.vue";
|
||||
import ToolkitGrid from "@/components/ToolkitGrid.vue";
|
||||
import type {
|
||||
ContactContent,
|
||||
InfrastructureCaseStudy,
|
||||
PublicProject,
|
||||
ToolkitGroup,
|
||||
} from "@/content/types";
|
||||
|
||||
const infrastructure: InfrastructureCaseStudy = {
|
||||
id: 'safe-infrastructure',
|
||||
title: 'Secure self-hosted platform and lifecycle',
|
||||
summary: 'A compact infrastructure case study.',
|
||||
id: "safe-infrastructure",
|
||||
title: "Secure self-hosted platform and lifecycle",
|
||||
summary: "A compact infrastructure case study.",
|
||||
bands: [
|
||||
{ id: 'ingress-tls', title: 'Ingress and TLS', summary: 'Managed encrypted ingress.' },
|
||||
{ id: 'identity-access', title: 'Identity and access', summary: 'Protected service access.' },
|
||||
{ id: 'service-lifecycle', title: 'Service lifecycle', summary: 'Managed container lifecycle.' },
|
||||
{ id: 'operations', title: 'Operations', summary: 'Routine maintenance and investigation.' },
|
||||
{
|
||||
id: "ingress-tls",
|
||||
title: "Ingress and TLS",
|
||||
summary: "Managed encrypted ingress.",
|
||||
},
|
||||
{
|
||||
id: "identity-access",
|
||||
title: "Identity and access",
|
||||
summary: "Protected service access.",
|
||||
},
|
||||
{
|
||||
id: "service-lifecycle",
|
||||
title: "Service lifecycle",
|
||||
summary: "Managed container lifecycle.",
|
||||
},
|
||||
{
|
||||
id: "operations",
|
||||
title: "Operations",
|
||||
summary: "Routine maintenance and investigation.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const privateProject: PublicProject = {
|
||||
id: 'private-work',
|
||||
title: 'Private self-hosted medication tracker',
|
||||
visibility: 'private',
|
||||
maturity: 'active',
|
||||
summary: 'A private application.',
|
||||
problem: 'A recurring workflow needs reliable tracking.',
|
||||
contribution: 'Designed and maintained the application.',
|
||||
technicalDecisions: ['Used a synchronised client and server design.'],
|
||||
outcome: 'The work remains private.',
|
||||
id: "private-work",
|
||||
title: "Private self-hosted medication tracker",
|
||||
visibility: "private",
|
||||
maturity: "active",
|
||||
summary: "A private application.",
|
||||
problem: "A recurring workflow needs reliable tracking.",
|
||||
contribution: "Designed and maintained the application.",
|
||||
technicalDecisions: ["Used a synchronised client and server design."],
|
||||
outcome: "The work remains private.",
|
||||
assetIds: [],
|
||||
};
|
||||
|
||||
const publicProject: PublicProject = {
|
||||
...privateProject,
|
||||
id: 'public-work',
|
||||
title: 'Footy Tracker',
|
||||
visibility: 'public',
|
||||
url: 'https://github.com/example/footy-tracker',
|
||||
id: "public-work",
|
||||
title: "Footy Tracker",
|
||||
visibility: "public",
|
||||
url: "https://github.com/example/footy-tracker",
|
||||
};
|
||||
|
||||
describe('semantic section components', () => {
|
||||
it('gives the infrastructure graphic an accessible name, description, caption, and visible text equivalent', () => {
|
||||
describe("semantic section components", () => {
|
||||
it("gives the infrastructure graphic an accessible name, description, caption, and visible text equivalent", () => {
|
||||
const wrapper = mount(InfrastructureDiagram, { props: { infrastructure } });
|
||||
const flow = wrapper.get('[data-testid="infrastructure-flow"]');
|
||||
|
||||
expect(flow.attributes('role')).toBe('img');
|
||||
expect(flow.attributes('aria-labelledby')).toBe('safe-infrastructure-diagram-title');
|
||||
expect(flow.attributes('aria-describedby')).toBe('safe-infrastructure-diagram-description');
|
||||
expect(wrapper.get('figcaption').text()).toContain('Secure self-hosted platform and lifecycle');
|
||||
expect(wrapper.get('[data-testid="infrastructure-text-equivalent"]').text()).toContain('Ingress and TLS');
|
||||
expect(wrapper.get('[data-testid="infrastructure-text-equivalent"]').text()).toContain('Identity and access');
|
||||
expect(flow.attributes("role")).toBe("img");
|
||||
expect(flow.attributes("aria-labelledby")).toBe(
|
||||
"safe-infrastructure-diagram-title",
|
||||
);
|
||||
expect(flow.attributes("aria-describedby")).toBe(
|
||||
"safe-infrastructure-diagram-description",
|
||||
);
|
||||
expect(wrapper.get("figcaption").text()).toContain(
|
||||
"Secure self-hosted platform and lifecycle",
|
||||
);
|
||||
expect(
|
||||
wrapper.get('[data-testid="infrastructure-text-equivalent"]').text(),
|
||||
).toContain("Ingress and TLS");
|
||||
expect(
|
||||
wrapper.get('[data-testid="infrastructure-text-equivalent"]').text(),
|
||||
).toContain("Identity and access");
|
||||
});
|
||||
|
||||
it('renders the real infrastructure route with explicit trust boundaries and operational support', () => {
|
||||
it("renders the real infrastructure route with explicit trust boundaries and operational support", () => {
|
||||
const wrapper = mount(InfrastructureDiagram, { props: { infrastructure } });
|
||||
const flow = wrapper.get('[data-testid="infrastructure-flow"]');
|
||||
|
||||
expect(flow.findAll('[data-infrastructure-node]').map((node) => node.attributes('data-infrastructure-node'))).toEqual([
|
||||
'visitor', 'nginx-tls', 'authentication', 'containerized-services', 'operations',
|
||||
expect(
|
||||
flow
|
||||
.findAll("[data-infrastructure-node]")
|
||||
.map((node) => node.attributes("data-infrastructure-node")),
|
||||
).toEqual([
|
||||
"visitor",
|
||||
"nginx-tls",
|
||||
"authentication",
|
||||
"containerized-services",
|
||||
"operations",
|
||||
]);
|
||||
expect(flow.findAll('[data-trust-boundary]').map((boundary) => boundary.text())).toEqual([
|
||||
'Public edge', 'Access boundary', 'Service boundary',
|
||||
]);
|
||||
expect(flow.text()).toContain('fail2ban guard');
|
||||
expect(
|
||||
flow.findAll("[data-trust-boundary]").map((boundary) => boundary.text()),
|
||||
).toEqual(["Public edge", "Access boundary", "Service boundary"]);
|
||||
expect(
|
||||
wrapper.get('[aria-label="Infrastructure diagram legend"]').text(),
|
||||
).toContain("Trust boundary");
|
||||
expect(
|
||||
wrapper.get('[aria-label="Infrastructure diagram legend"]').text(),
|
||||
).toContain("Request flow");
|
||||
expect(
|
||||
wrapper.get('[data-testid="infrastructure-text-equivalent"]').text(),
|
||||
).toContain("Boundary labels mark changes in trust or access rules");
|
||||
expect(flow.text()).toContain("fail2ban guard");
|
||||
});
|
||||
|
||||
it('does not turn private project content into a link container', () => {
|
||||
const wrapper = mount(ProjectCaseStudy, { props: { project: privateProject, priority: 'primary' } });
|
||||
it("does not turn private project content into a link container", () => {
|
||||
const wrapper = mount(ProjectCaseStudy, {
|
||||
props: { project: privateProject, priority: "primary" },
|
||||
});
|
||||
|
||||
expect(wrapper.get('article').findAll('a')).toHaveLength(0);
|
||||
expect(wrapper.get('h3').text()).toBe('Private self-hosted medication tracker');
|
||||
expect(wrapper.get('article').attributes('data-project-priority')).toBe('primary');
|
||||
expect(wrapper.get("article").findAll("a")).toHaveLength(0);
|
||||
expect(wrapper.get("h3").text()).toBe(
|
||||
"Private self-hosted medication tracker",
|
||||
);
|
||||
expect(wrapper.get("article").attributes("data-project-priority")).toBe(
|
||||
"primary",
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a safe external link only for a public project with an approved URL', () => {
|
||||
const wrapper = mount(ProjectCaseStudy, { props: { project: publicProject, priority: 'supporting' } });
|
||||
const link = wrapper.get('article a');
|
||||
it("uses a safe external link only for a public project with an approved URL", () => {
|
||||
const wrapper = mount(ProjectCaseStudy, {
|
||||
props: { project: publicProject, priority: "supporting" },
|
||||
});
|
||||
const link = wrapper.get("article a");
|
||||
|
||||
expect(link.attributes('href')).toBe('https://github.com/example/footy-tracker');
|
||||
expect(link.attributes('target')).toBe('_blank');
|
||||
expect(link.attributes('rel')).toBe('noopener noreferrer');
|
||||
expect(link.attributes("href")).toBe(
|
||||
"https://github.com/example/footy-tracker",
|
||||
);
|
||||
expect(link.attributes("target")).toBe("_blank");
|
||||
expect(link.attributes("rel")).toBe("noopener noreferrer");
|
||||
});
|
||||
|
||||
it('announces every toolkit technology once while hiding the decorative icon placeholder', () => {
|
||||
const groups: readonly ToolkitGroup[] = [{
|
||||
title: 'Systems & delivery',
|
||||
technologies: [{ name: 'Linux' }],
|
||||
}];
|
||||
it("announces every toolkit technology once while hiding the decorative icon placeholder", () => {
|
||||
const groups: readonly ToolkitGroup[] = [
|
||||
{
|
||||
title: "Systems & delivery",
|
||||
technologies: [{ name: "Linux" }],
|
||||
},
|
||||
];
|
||||
const wrapper = mount(ToolkitGrid, { props: { groups } });
|
||||
const tile = wrapper.get('[data-testid="toolkit-technology"]');
|
||||
|
||||
expect(tile.text()).toBe('Linux');
|
||||
expect(tile.get('[aria-hidden="true"]').attributes('aria-hidden')).toBe('true');
|
||||
expect(tile.text()).toBe("Linux");
|
||||
expect(tile.get('[aria-hidden="true"]').attributes("aria-hidden")).toBe(
|
||||
"true",
|
||||
);
|
||||
expect(tile.findAll('[aria-label="Linux"]').length).toBe(0);
|
||||
});
|
||||
|
||||
it('renders a visible mailto contact and safe verified social links', () => {
|
||||
it("renders a visible mailto contact and safe verified social links", () => {
|
||||
const contact: ContactContent = {
|
||||
email: 'hello@example.test',
|
||||
cvMessage: 'For a current CV or role-specific information, contact me directly.',
|
||||
social: [{ service: 'GitHub', url: 'https://github.com/example' }],
|
||||
email: "hello@example.test",
|
||||
cvMessage:
|
||||
"For a current CV or role-specific information, contact me directly.",
|
||||
social: [{ service: "GitHub", url: "https://github.com/example" }],
|
||||
};
|
||||
const wrapper = mount(ContactDestination, {
|
||||
props: {
|
||||
@@ -107,10 +170,14 @@ describe('semantic section components', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.get('a[href="mailto:hello@example.test"]').text()).toBe('Start a conversation');
|
||||
expect(wrapper.text()).toContain("Thanks for making the trip. The road's open if you'd like to build something together.");
|
||||
expect(wrapper.get('a[href="mailto:hello@example.test"]').text()).toBe(
|
||||
"Start a conversation",
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
"Thanks for making the trip. The road's open if you'd like to build something together.",
|
||||
);
|
||||
const social = wrapper.get('a[href="https://github.com/example"]');
|
||||
expect(social.text()).toBe('GitHub');
|
||||
expect(social.attributes('rel')).toBe('noopener noreferrer');
|
||||
expect(social.text()).toBe("GitHub");
|
||||
expect(social.attributes("rel")).toBe("noopener noreferrer");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user