Switch homepage focus to SDET

This commit is contained in:
codex 2026-08-01 03:24:51 -03:00
parent 824a2187ea
commit b29cdfde20
14 changed files with 278 additions and 80 deletions

View File

@ -32,7 +32,7 @@
{
"@type": "Person",
"name": "Brad Stein",
"jobTitle": "DevOps Automation Engineer / Senior SDET",
"jobTitle": "Senior SDET / DevOps Automation Engineer",
"url": "https://bstein.dev/",
"email": "mailto:brad@bstein.dev",
"sameAs": ["https://www.linkedin.com/in/steinbradley/", "https://scm.bstein.dev/bstein"]

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
<title id="title">Brad Stein, DevOps Automation Engineer and Senior SDET</title>
<title id="title">Brad Stein, Senior SDET and DevOps Automation Engineer</title>
<desc id="desc">A dark bstein.dev social card with professional engineering positioning and Titan Lab platform proof.</desc>
<defs>
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
@ -20,7 +20,7 @@
<circle cx="880" cy="174" r="6" fill="#00e5c5"/>
<text x="80" y="196" fill="#00e5c5" font-family="JetBrains Mono, monospace" font-size="26" letter-spacing="3">BSTEIN.DEV</text>
<text x="80" y="286" fill="#f7fbff" font-family="Space Grotesk, Inter, sans-serif" font-size="74" font-weight="700">Brad Stein</text>
<text x="80" y="352" fill="#d7e5ff" font-family="Space Grotesk, Inter, sans-serif" font-size="40" font-weight="600">DevOps Automation Engineer / Senior SDET</text>
<text x="80" y="352" fill="#d7e5ff" font-family="Space Grotesk, Inter, sans-serif" font-size="40" font-weight="600">Senior SDET / DevOps Automation Engineer</text>
<text x="80" y="422" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="30">CI/CD · Kubernetes · Python · Linux · Observability</text>
<text x="80" y="482" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="26">Titan Lab is the live platform proof behind the work.</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -350,7 +350,7 @@ function pickIcon(name) {
if (h.includes("translation")) return "TR";
if (h.includes("grafana")) return "GF";
if (h.includes("pegasus")) return "PG";
if (h.includes("veles")) return "VL";
if (h.includes("cassandra")) return "CA";
if (h.includes("ai chat")) return "AI";
if (h.includes("ai image") || h.includes("vision")) return "VI";
if (h.includes("ai speech")) return "SP";
@ -366,7 +366,7 @@ function serviceRank(service) {
"Flux",
"Grafana",
"OpenSearch",
"Veles",
"Cassandra",
"Nextcloud",
"Outline",
"Planka",

View File

@ -1,7 +1,7 @@
<template>
<header class="topbar">
<a class="skip-link" href="#main-content">Skip to content</a>
<RouterLink class="profile" to="/" @click="closeMenu">
<RouterLink class="profile" :to="homePath" @click="closeMenu">
<div class="avatar" aria-hidden="true">
<img src="@/assets/profile-avatar.jpg" alt="" />
</div>
@ -36,7 +36,20 @@
<a v-for="link in sectionLinks" :key="link.href" class="nav-link" :href="link.href" @click="closeMenu">
{{ link.label }}
</a>
<a class="nav-link professional-action" href="/#contact" @click="closeMenu">Contact</a>
<a class="nav-link professional-action" :href="`${homePath}#contact`" @click="closeMenu">Contact</a>
<div class="mode-switch" aria-label="Homepage mode">
<RouterLink
v-for="mode in homepageModes"
:key="mode.id"
class="nav-link mode-link"
:class="{ active: mode.id === currentMode }"
:to="mode.path"
@click="closeMenu"
>
{{ mode.label }}
</RouterLink>
</div>
<div class="utility">
<template v-if="!auth.ready">
@ -62,21 +75,29 @@
</template>
<script setup>
import { nextTick, onUnmounted, ref, watch } from "vue";
import { RouterLink } from "vue-router";
import { computed, nextTick, onUnmounted, ref, watch } from "vue";
import { RouterLink, useRoute } from "vue-router";
import { auth, login, logout } from "@/auth";
import { homepageContent } from "@/data/homepageContent";
import {
homepageContentForMode,
homepageModeFromRoute,
homepageModes,
homepagePathForMode,
} from "@/data/homepageContent";
import { useMemberAccessState } from "@/member/useMemberAccessState";
const { memberAction } = useMemberAccessState();
const content = homepageContent;
const route = useRoute();
const currentMode = computed(() => homepageModeFromRoute(route));
const content = computed(() => homepageContentForMode(currentMode.value));
const homePath = computed(() => homepagePathForMode(currentMode.value));
const sectionLinks = [
{ label: "Work", href: "/#work" },
{ label: "Capabilities", href: "/#capabilities" },
{ label: "Platform", href: "/#platform" },
{ label: "Process", href: "/#process" },
];
const sectionLinks = computed(() => [
{ label: "Work", href: `${homePath.value}#work` },
{ label: "Capabilities", href: `${homePath.value}#capabilities` },
{ label: "Platform", href: `${homePath.value}#platform` },
{ label: "Process", href: `${homePath.value}#process` },
]);
const menuOpen = ref(false);
const navEl = ref(null);
@ -239,6 +260,15 @@ onUnmounted(() => {
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.mode-switch {
display: flex;
align-items: center;
gap: 4px;
margin-left: 4px;
padding-left: 10px;
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.nav-link {
min-height: 44px;
display: inline-flex;
@ -253,6 +283,18 @@ onUnmounted(() => {
white-space: nowrap;
}
.mode-link {
min-height: 36px;
padding: 7px 9px;
color: var(--text-muted);
}
.mode-link.active {
color: var(--accent-cyan);
border-color: rgba(0, 229, 197, 0.28);
background: rgba(0, 229, 197, 0.07);
}
.button {
background: transparent;
cursor: pointer;
@ -340,6 +382,16 @@ onUnmounted(() => {
flex-direction: column;
}
.mode-switch {
margin-left: 0;
padding-left: 0;
padding-top: 10px;
border-left: 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
align-items: stretch;
flex-direction: column;
}
.nav-link {
justify-content: flex-start;
white-space: normal;

View File

@ -1,4 +1,4 @@
const focusMode = (import.meta.env?.VITE_HOMEPAGE_FOCUS || "devops").trim().toLowerCase();
const focusMode = (import.meta.env?.VITE_HOMEPAGE_FOCUS || "sdet").trim().toLowerCase();
const focusProfiles = {
devops: {
@ -6,21 +6,27 @@ const focusProfiles = {
role: "DevOps Automation Engineer / Senior SDET",
shortRole: "DevOps Automation / Platform",
summary:
"I build and repair the machinery that gets code safely into production: CI/CD, Docker, Kubernetes, Linux infrastructure, Python automation, observability, and test gates.",
"I build custom software, tooling, and infrastructure that make development and test workflows easier, safer, and more repeatable: platform tools, CI/CD, Docker, Kubernetes, Linux systems, Python automation, observability, and release gates.",
experienceLine:
"Engineering experience across Boeing, IBM, deployment automation, infrastructure tooling, system testing, and production-minded platform operations.",
techStack: [
"Jenkins",
"Docker",
"Kubernetes",
"Docker",
"Linux",
"Terraform",
"Ansible",
"Flux",
"Helm",
"Jenkins",
"Harbor",
"Keycloak",
"Vault",
"Grafana",
"Prometheus",
"Python",
"Bash",
"Pytest",
"Selenium",
"Playwright",
],
capabilityOrder: [
@ -57,7 +63,7 @@ const focusProfiles = {
description:
"Inspectable repos, live services, and professional work samples that show how I approach real infrastructure.",
},
selectedWorkOrder: ["Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing", "Veles"],
selectedWorkOrder: ["Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing", "Cassandra"],
engagementOrder: [
"Fixed-Scope Technical Rescue",
"Short-Term Remote Contract",
@ -69,7 +75,7 @@ const focusProfiles = {
description:
"Send the problem, technical stack, expected outcome, and timeline. I will respond with whether it is a fit, what information is needed, and the most practical next step.",
},
veles: {
cassandra: {
description:
"Homegrown AI playtester for Magic: The Gathering deck construction, deployed for the community on Atlas with backend orchestration and disposable simulation workers.",
points: ["community AI service", "Kubernetes deployment", "worker orchestration", "job cleanup"],
@ -123,14 +129,15 @@ const focusProfiles = {
"Pytest",
"Selenium",
"Playwright",
"API testing",
"API Testing",
"E2E Testing",
"CI/CD",
"Jenkins",
"Docker",
"Kubernetes",
"Linux",
"Grafana",
"Prometheus",
"Terraform",
],
capabilityOrder: [
"Test Automation & SDET",
@ -166,7 +173,7 @@ const focusProfiles = {
description:
"Engineering work across system testing, cloud validation, delivery gates, and live platform operations.",
},
selectedWorkOrder: ["Boeing", "IBM", "Titan Lab + titan-iac", "Atlas operator tools", "Veles"],
selectedWorkOrder: ["Boeing", "IBM", "Titan Lab + titan-iac", "Atlas operator tools", "Cassandra"],
engagementOrder: [
"Short-Term Remote Contract",
"Fixed-Scope Technical Rescue",
@ -178,7 +185,7 @@ const focusProfiles = {
description:
"Send the problem, technical stack, expected outcome, and timeline. I will respond with whether it is a fit, what information is needed, and the most practical next step.",
},
veles: {
cassandra: {
description:
"Homegrown AI playtester with simulation flows, backend service boundaries, and useful surfaces for API, worker, regression, and result-quality testing.",
points: ["simulation flows", "API coverage", "regression surfaces", "result checks"],
@ -219,7 +226,7 @@ const focusProfiles = {
],
},
},
dev: {
developer: {
eyebrow: "AVAILABLE FOR REMOTE PRODUCT, PLATFORM, AND AI SERVICE WORK",
role: "Full-Stack Platform Engineer / Senior SDET",
shortRole: "Product / Platform Engineering",
@ -230,15 +237,17 @@ const focusProfiles = {
techStack: [
"Vue",
"Python",
"AI integration",
"API design",
"Flask",
"API Design",
"PostgreSQL",
"Redis",
"Workers",
"AI Integration",
"Docker",
"Kubernetes",
"Jenkins",
"Flux",
"Grafana",
"PostgreSQL",
"CI/CD",
"Pytest",
"Selenium",
"Playwright",
],
capabilityOrder: [
@ -275,7 +284,7 @@ const focusProfiles = {
description:
"Inspectable repos, live services, and professional work samples that show product engineering backed by real operations.",
},
selectedWorkOrder: ["Veles", "Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing"],
selectedWorkOrder: ["Cassandra", "Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing"],
engagementOrder: [
"Short-Term Remote Contract",
"Fixed-Scope Technical Rescue",
@ -287,7 +296,7 @@ const focusProfiles = {
description:
"Send the idea or problem, technical stack, expected outcome, and timeline. I will respond with whether it is a fit, what information is needed, and the most practical next step.",
},
veles: {
cassandra: {
description:
"Homegrown AI service for Magic: The Gathering deck construction, with product workflow, AI integration, backend orchestration, content seeding, and disposable simulation jobs.",
points: ["AI integration", "product workflow", "service boundaries", "simulation workers"],
@ -315,7 +324,7 @@ const focusProfiles = {
},
{
label: "Testing",
items: ["Pytest", "Playwright", "Selenium", "Jest", "API", "E2E", "Regression", "Contract"],
items: ["Pytest", "Selenium", "Playwright", "Jest", "API", "E2E", "Regression", "Contract"],
},
{
label: "Observability",
@ -330,7 +339,40 @@ const focusProfiles = {
},
};
export const homepageFocus = focusProfiles[focusMode] ? focusMode : "devops";
export const homepageModes = [
{ id: "sdet", label: "SDET", path: "/" },
{ id: "devops", label: "DevOps", path: "/devops" },
{ id: "developer", label: "Developer", path: "/developer" },
];
const focusAliases = {
dev: "developer",
development: "developer",
qa: "sdet",
test: "sdet",
testing: "sdet",
};
export function normalizeHomepageFocus(mode = "") {
const normalized = String(mode || "").trim().toLowerCase();
const aliased = focusAliases[normalized] || normalized;
return focusProfiles[aliased] ? aliased : "sdet";
}
export const homepageFocus = normalizeHomepageFocus(focusMode);
export function homepagePathForMode(mode = homepageFocus) {
return homepageModes.find((item) => item.id === normalizeHomepageFocus(mode))?.path || "/";
}
export function homepageAboutPathForMode(mode = homepageFocus) {
const focus = normalizeHomepageFocus(mode);
return focus === "sdet" ? "/about" : `/${focus}/about`;
}
export function homepageModeFromRoute(route) {
return normalizeHomepageFocus(route?.params?.focus || route?.meta?.homepageFocus || route?.query?.focus || homepageFocus);
}
const capabilities = [
{
@ -421,8 +463,6 @@ function orderedBy(items, order) {
return [...items].sort((a, b) => (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99));
}
const selectedFocus = focusProfiles[homepageFocus];
const workDetailsByFocus = {
devops: {
titan: {
@ -483,7 +523,7 @@ const workDetailsByFocus = {
points: ["Python", "Selenium", "system E2E", "microservices", "web UI", "quality gates", "test architecture"],
},
},
dev: {
developer: {
titan: {
kind: "Live platform",
description:
@ -509,13 +549,16 @@ const workDetailsByFocus = {
},
};
const workDetails = workDetailsByFocus[homepageFocus] || workDetailsByFocus.devops;
export function homepageContentForMode(mode = homepageFocus) {
const focus = normalizeHomepageFocus(mode);
const selectedFocus = focusProfiles[focus];
const workDetails = workDetailsByFocus[focus] || workDetailsByFocus.devops;
export const homepageContent = {
return {
professionalName: "Brad Stein",
role: selectedFocus.role,
shortRole: selectedFocus.shortRole,
focus: homepageFocus,
focus,
eyebrow: selectedFocus.eyebrow,
title: "Brad Stein | SDET, DevOps Automation & Platform Engineering",
description:
@ -532,7 +575,7 @@ export const homepageContent = {
email: "brad@bstein.dev",
subject: "Project, contract, or engineering inquiry from bstein.dev",
},
resumeUrl: "/about",
resumeUrl: homepageAboutPathForMode(focus),
resumePdfUrl: "",
linkedInUrl: "https://www.linkedin.com/in/steinbradley/",
sourceUrl: "https://scm.bstein.dev/bstein",
@ -597,18 +640,18 @@ export const homepageContent = {
{
icon: "AI",
kind: "Community AI service",
title: "Veles",
description: selectedFocus.veles.description,
points: selectedFocus.veles.points,
title: "Cassandra",
description: selectedFocus.cassandra.description,
points: selectedFocus.cassandra.points,
weight: "minor",
links: [
{
label: "Live app",
href: "https://veles.bstein.dev/",
href: "https://cassandra.bstein.dev/",
},
{
label: "Content seed",
href: "https://scm.bstein.dev/bstein/veles-content",
href: "https://scm.bstein.dev/bstein/cassandra-content",
},
],
},
@ -639,9 +682,11 @@ export const homepageContent = {
title: "Senior Remote Engineering Role",
duration: "Full-time remote roles where the fit is strong.",
description:
homepageFocus === "devops"
focus === "devops"
? "I am open to well-aligned remote DevOps Automation, platform engineering, CI/CD, infrastructure automation, Kubernetes, Linux, or systems reliability roles. SDET and test automation depth comes with it."
: "I am open to well-aligned remote SDET, QA Automation, systems testing, release quality, CI/CD, or DevOps Automation roles. Platform engineering depth comes with it.",
: focus === "developer"
? "I am open to well-aligned remote developer, full-stack, platform product, AI service, Python, Vue, automation, or internal tooling roles. SDET and DevOps depth comes with it."
: "I am open to well-aligned remote SDET, QA Automation, systems testing, release quality, CI/CD, or DevOps Automation roles. Platform engineering depth comes with it.",
},
].sort((a, b) => {
const rank = new Map(selectedFocus.engagementOrder.map((title, index) => [title, index]));
@ -782,9 +827,9 @@ export const homepageContent = {
description: "Ask the Titan Lab AI assistant for help.",
},
{
name: "Veles",
name: "Cassandra",
icon: "🃏",
href: "https://veles.bstein.dev/",
href: "https://cassandra.bstein.dev/",
description: "Test Magic: The Gathering deck ideas with AI playtesting.",
},
{
@ -794,10 +839,13 @@ export const homepageContent = {
description: "Use the private RPC endpoint through the Monero guide.",
},
],
};
};
}
export function contactMailto() {
const email = homepageContent.primaryContact.email;
const subject = encodeURIComponent(homepageContent.primaryContact.subject);
export const homepageContent = homepageContentForMode(homepageFocus);
export function contactMailto(content = homepageContent) {
const email = content.primaryContact.email;
const subject = encodeURIComponent(content.primaryContact.subject);
return `mailto:${email}?subject=${subject}`;
}

View File

@ -234,12 +234,12 @@ export function fallbackServices() {
status: "live",
},
{
name: "Veles",
name: "Cassandra",
icon: "🃏",
category: "ai",
summary: "Magic: The Gathering AI playtester for deck construction.",
link: "https://veles.bstein.dev/",
host: "veles.bstein.dev",
link: "https://cassandra.bstein.dev/",
host: "cassandra.bstein.dev",
status: "live",
},
{

View File

@ -12,8 +12,11 @@ import OnboardingView from "./views/OnboardingView.vue";
export default createRouter({
history: createWebHistory(),
routes: [
{ path: "/", name: "home", component: HomeView },
{ path: "/about", name: "about", component: AboutView },
{ path: "/", name: "home", component: HomeView, meta: { homepageFocus: "sdet" } },
{ path: "/about", name: "about", component: AboutView, meta: { homepageFocus: "sdet" } },
{ path: "/dev", redirect: "/developer" },
{ path: "/:focus(devops|developer|sdet)", name: "home-focus", component: HomeView },
{ path: "/:focus(devops|developer|sdet)/about", name: "about-focus", component: AboutView },
{ path: "/ai", redirect: "/ai/chat" },
{ path: "/ai/chat", name: "ai-chat", component: AiView },
{ path: "/ai/roadmap", name: "ai-roadmap", component: AiPlanView },

View File

@ -89,9 +89,9 @@
</p>
<p>
<span class="mono">Oceanus</span> and <span class="mono">Tethys</span> are now part of Atlas capacity instead of separate public status surfaces.
The dedicated non-cluster hosts are the control-plane database node and the jumphost. <span class="mono">Veles</span>,
The dedicated non-cluster hosts are the control-plane database node and the jumphost. <span class="mono">Cassandra</span>,
the MTG AI playtester, is deployed at
<a href="https://veles.bstein.dev/" target="_blank" rel="noopener noreferrer">veles.bstein.dev</a>.
<a href="https://cassandra.bstein.dev/" target="_blank" rel="noopener noreferrer">cassandra.bstein.dev</a>.
</p>
</div>
</section>
@ -115,9 +115,13 @@
</template>
<script setup>
import { homepageContent } from "../data/homepageContent";
import { computed } from "vue";
import { useRoute } from "vue-router";
import { homepageContentForMode, homepageModeFromRoute } from "../data/homepageContent";
const about = homepageContent.about;
const route = useRoute();
const content = computed(() => homepageContentForMode(homepageModeFromRoute(route)));
const about = computed(() => content.value.about);
const timeline = [
{

View File

@ -39,13 +39,14 @@
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import HomeContactFooter from "../components/HomeContactFooter.vue";
import HomeHeroStatus from "../components/HomeHeroStatus.vue";
import HomeMemberAccess from "../components/HomeMemberAccess.vue";
import HomeProfessionalSections from "../components/HomeProfessionalSections.vue";
import PlatformSection from "../components/PlatformSection.vue";
import { auth } from "../auth";
import { contactMailto, homepageContent } from "../data/homepageContent";
import { contactMailto, homepageContentForMode, homepageModeFromRoute } from "../data/homepageContent";
import { useMemberAccessState } from "../member/useMemberAccessState";
const props = defineProps({
@ -58,10 +59,12 @@ const props = defineProps({
error: String,
});
const content = homepageContent;
const route = useRoute();
const homepageMode = computed(() => homepageModeFromRoute(route));
const content = computed(() => homepageContentForMode(homepageMode.value));
const { memberState, memberAction } = useMemberAccessState();
const currentYear = new Date().getFullYear();
const mailtoHref = contactMailto();
const mailtoHref = computed(() => contactMailto(content.value));
const statusItems = computed(() => [
{
@ -134,8 +137,8 @@ const lastCheckedLabel = computed(() => {
});
const memberPanelCopy = computed(() => {
if (auth.enabled && auth.authenticated) return content.memberAccessCopy.authenticated;
if (memberAction.value?.kind === "restricted") return content.memberAccessCopy.restricted;
if (auth.enabled && auth.authenticated) return content.value.memberAccessCopy.authenticated;
if (memberAction.value?.kind === "restricted") return content.value.memberAccessCopy.restricted;
return "";
});

View File

@ -77,13 +77,13 @@ test("shows Brad's professional homepage and opens the platform diagram overlay"
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Brad Stein" })).toBeVisible();
await expect(page.getByRole("heading", { name: "DevOps Automation Engineer / Senior SDET" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Senior SDET / DevOps Automation Engineer" })).toBeVisible();
await expect(page.getByRole("link", { name: "Discuss a Project" })).toBeVisible();
await expect(page.getByRole("link", { name: "View Résumé" })).toBeVisible();
await expect(page.getByRole("link", { name: "Explore the Live Platform" })).toBeVisible();
await expect(page.getByRole("button", { name: "Login" })).toBeVisible();
await expect(page.getByRole("link", { name: "Register" })).toBeVisible();
await expect(page.getByRole("heading", { name: "From 0 to 100" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Quality Systems I Can Take From Zero to Reliable" })).toBeVisible();
await expect(page.getByRole("link", { name: "Nextcloud", exact: true })).toBeVisible();
await expect(page.getByText("2 of 2 online")).toBeVisible();
await expect(page.getByText("Database machine")).toBeVisible();
@ -121,3 +121,21 @@ test("keeps the homepage usable when public status fails", async ({ page }) => {
await expect(page.getByText("Live status is temporarily unavailable").first()).toBeVisible();
await expect(page.getByText("status 503")).toHaveCount(0);
});
test("serves developer and SDET homepage modes", async ({ page }) => {
await page.goto("/developer", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Full-Stack Platform Engineer / Senior SDET" })).toBeVisible();
await expect(page.getByText("Product and Platform Showcase")).toBeVisible();
await expect(page.getByRole("link", { name: "Developer", exact: true })).toHaveClass(/active/);
await page.getByRole("link", { name: "View Résumé" }).click();
await expect(page).toHaveURL(/\/developer\/about$/);
await expect(page.getByText("AI Services · Vue · Python · Kubernetes")).toBeVisible();
await page.goto("/sdet", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Senior SDET / DevOps Automation Engineer" })).toBeVisible();
await expect(page.getByText("Testing, Automation, and Platform Work")).toBeVisible();
await expect(page.getByRole("link", { name: "SDET", exact: true })).toHaveClass(/active/);
await page.getByRole("link", { name: "View Résumé" }).click();
await expect(page).toHaveURL(/\/about$/);
await expect(page.getByText("Test Automation · Release Quality · CI/CD")).toBeVisible();
});

View File

@ -53,6 +53,9 @@ describe("frontend entrypoint and router", () => {
expect(router.routes.map((route) => route.path)).toEqual([
"/",
"/about",
"/dev",
"/:focus(devops|developer|sdet)",
"/:focus(devops|developer|sdet)/about",
"/ai",
"/ai/chat",
"/ai/roadmap",
@ -63,6 +66,9 @@ describe("frontend entrypoint and router", () => {
"/onboarding",
]);
expect(router.routes.find((route) => route.path === "/ai")).toMatchObject({ redirect: "/ai/chat" });
expect(router.routes.find((route) => route.path === "/dev")).toMatchObject({ redirect: "/developer" });
expect(router.routes.find((route) => route.name === "home-focus").component).toBeDefined();
expect(router.routes.find((route) => route.name === "about-focus").component).toBeDefined();
expect(router.routes.find((route) => route.name === "account").component).toBeDefined();
});
});

View File

@ -3,8 +3,20 @@ import { RouterLinkStub, flushPromises, mount, shallowMount } from "@vue/test-ut
import PlatformSection from "../../../frontend/src/components/PlatformSection.vue";
import { auth } from "../../../frontend/src/auth.js";
import { homepageContentForMode, normalizeHomepageFocus } from "../../../frontend/src/data/homepageContent.js";
import HomeView from "../../../frontend/src/views/HomeView.vue";
let mockRoute = { meta: { homepageFocus: "sdet" }, params: {}, query: {} };
jest.mock("vue-router", () => ({
RouterLink: {
name: "RouterLink",
props: ["to"],
template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>",
},
useRoute: () => mockRoute,
}), { virtual: true });
function resetAuth() {
auth.ready = true;
auth.enabled = true;
@ -18,6 +30,7 @@ function resetAuth() {
describe("HomeView", () => {
beforeEach(() => {
mockRoute = { meta: { homepageFocus: "sdet" }, params: {}, query: {} };
global.fetch = jest.fn(async (resource) => {
const url = typeof resource === "string" ? resource : resource?.url || "";
if (url.includes("/api/account/member-state")) {
@ -72,14 +85,14 @@ describe("HomeView", () => {
});
expect(wrapper.text()).toContain("Brad Stein");
expect(wrapper.text()).toContain("DevOps Automation Engineer / Senior SDET");
expect(wrapper.text()).toContain("Senior SDET / DevOps Automation Engineer");
expect(wrapper.text()).toContain("Discuss a Project");
expect(wrapper.text()).toContain("View Résumé");
expect(wrapper.text()).toContain("Explore the Live Platform");
expect(wrapper.text()).toContain("From 0 to 100");
expect(wrapper.text()).toContain("Platform Showcase");
expect(wrapper.text()).toContain("Quality Systems I Can Take From Zero to Reliable");
expect(wrapper.text()).toContain("Testing, Automation, and Platform Work");
expect(wrapper.text()).toContain("Atlas operator tools");
expect(wrapper.text()).toContain("Veles");
expect(wrapper.text()).toContain("Cassandra");
expect(wrapper.text()).not.toContain("TaskWatcher");
expect(wrapper.text()).toContain("family, friends, and the lucky few");
expect(wrapper.text()).toContain("Register");
@ -152,6 +165,50 @@ describe("HomeView", () => {
expect(wrapper.text()).not.toContain("app_password");
expect(wrapper.text()).not.toContain("mailu_app_password");
});
it("builds developer and SDET homepage modes", () => {
const devops = homepageContentForMode("devops");
const developer = homepageContentForMode("developer");
const sdet = homepageContentForMode("sdet");
expect(normalizeHomepageFocus("dev")).toBe("developer");
expect(devops.summary).toContain("custom software, tooling, and infrastructure");
expect(devops.techStack).toEqual([
"Kubernetes",
"Docker",
"Linux",
"Terraform",
"Ansible",
"Flux",
"Helm",
"Jenkins",
"Harbor",
"Keycloak",
"Vault",
"Grafana",
"Prometheus",
"Python",
"Bash",
"Pytest",
"Selenium",
"Playwright",
]);
expect(developer.focus).toBe("developer");
expect(developer.role).toBe("Full-Stack Platform Engineer / Senior SDET");
expect(developer.resumeUrl).toBe("/developer/about");
expect(developer.techStack).toContain("Vue");
expect(developer.techStack.indexOf("Selenium")).toBeLessThan(developer.techStack.indexOf("Playwright"));
expect(developer.workSection.heading).toBe("Product and Platform Showcase");
expect(developer.selectedWork[0].title).toBe("Cassandra");
expect(sdet.focus).toBe("sdet");
expect(sdet.role).toBe("Senior SDET / DevOps Automation Engineer");
expect(sdet.resumeUrl).toBe("/about");
expect(sdet.techStack).toContain("Selenium");
expect(sdet.techStack.indexOf("Selenium")).toBeLessThan(sdet.techStack.indexOf("Playwright"));
expect(sdet.capabilities[0].title).toBe("Test Automation & SDET");
expect(sdet.selectedWork[0].title).toBe("Boeing");
});
});
describe("PlatformSection", () => {

View File

@ -20,7 +20,7 @@ describe("sample data builders", () => {
expect(hardware.specialty.map((node) => node.alias)).toEqual(["atlas-db", "theia"]);
expect(services.services.some((service) => service.name === "Keycloak")).toBe(true);
expect(services.services.some((service) => service.name === "Oceanus")).toBe(false);
expect(services.services.some((service) => service.name === "Veles")).toBe(true);
expect(services.services.some((service) => service.name === "Cassandra")).toBe(true);
expect(services.services.some((service) => service.name === "AI Chat")).toBe(true);
expect(services.services.find((service) => service.name === "AI Chat").link).toBe("https://bstein.dev/ai/chat");
expect(services.services.find((service) => service.name === "Monero").link).toBe("https://bstein.dev/monero");

View File

@ -27,6 +27,7 @@ jest.mock("vue-router", () => ({
props: ["to"],
template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>",
},
useRoute: () => ({ meta: { homepageFocus: "sdet" }, params: {}, query: {} }),
}), { virtual: true });
describe("static shell views and components", () => {
@ -44,7 +45,7 @@ describe("static shell views and components", () => {
const about = shallowMount(AboutView);
expect(about.text()).toContain("About Me");
expect(about.text()).toContain("Titan Lab");
expect(about.text()).toContain("DevOps Automation Engineer / Senior SDET");
expect(about.text()).toContain("Senior SDET / DevOps Automation Engineer");
expect(about.text()).toContain("Kubernetes");
expect(about.text()).toContain("IBM Cloud");
expect(about.text()).toContain("TradeHat");
@ -130,10 +131,16 @@ describe("static shell views and components", () => {
try {
expect(wrapper.text()).toContain("Contact");
expect(wrapper.text()).toContain("DevOps");
expect(wrapper.text()).toContain("Developer");
expect(wrapper.text()).toContain("SDET");
expect(wrapper.text()).toContain("Login");
expect(wrapper.text()).toContain("Register");
expect(wrapper.text()).not.toContain("Reset Password");
expect(wrapper.find("a[href='/#contact']").exists()).toBe(true);
expect(wrapper.find("a[href='/devops']").exists()).toBe(true);
expect(wrapper.find("a[href='/developer']").exists()).toBe(true);
expect(wrapper.find(".mode-link.active").text()).toBe("SDET");
await wrapper.find("button.menu-toggle").trigger("click");
const nav = wrapper.find("nav");