sdetProfile/tests/portfolio.spec.ts
Builder b596b2d608 Editor strip, Network/Console/Trace tabs, real Playwright tests, HC theme
UI:
- Editor tab strip above the report with per-spec scoping (sidebar tree,
  results, source, status counts, hero copy); cookie-persisted active spec
- Status pill + workers / headed overflow menu moved from topbar into the
  editor bar to slim the global chrome
- Summary stripe and pending-preview body so the report never lands empty
- Tag bar with first-6 + "+N more" + clear; auto-run first idle test on load
- Mobile drawer for the test explorer; keyboard shortcuts overlay (?)
- Skipped + failed sample tests with proper icons / step rendering

Tabs:
- Network tab: public git.levkin.ca repos rendered as Playwright-style
  GET ... 200 rows with expandable JSON bodies and repo links
- Trace tab: career timeline as a Gantt-style waterfall
- Console tab: live run events; Source tab regenerates per active spec

Theming:
- Wire up high-contrast (WCAG AAA) theme: cycle dark → light → hc → dark,
  widen theme cookie regex to accept "hc", add HC overrides for syntax
  tokens and a few hardcoded "text-on-accent" sites in app.css

Testing:
- Add @playwright/test dev dependency + playwright.config.ts on port 3173
- tests/portfolio.spec.ts: 37 specs across 12 describe blocks
- scripts/fetch-gitea-repos.mjs to refresh giteaRepos from the Gitea API

Docs / housekeeping:
- README rewritten to reflect editor strip, network tab, HC theme, test
  runner, and updated project structure
- IDEAS.md trimmed to remaining roadmap; shipped items removed
- .gitignore ignores stray PDFs at repo root (canonical resume in assets/)
- Add assets/ilia-dobkin-resume.pdf as the canonical resume binary

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 22:55:48 -04:00

444 lines
15 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Wait for idle UI state (no spinners, status says "idle" or "all passed"). */
async function waitForIdle(page: Page) {
await expect(page.locator("#status-text")).not.toHaveText("running", {
timeout: 15_000,
});
}
/** Click "Run all" and wait for every runnable test to finish. */
async function runAllAndWait(page: Page) {
await page.click("#run-all");
await expect(page.locator("#status-text")).toHaveText("all passed", {
timeout: 30_000,
});
}
/** Run a single test by clicking its ▶ button in the sidebar tree. */
async function runSingleTest(page: Page, testId: string) {
await page.click(`.test[data-id="${testId}"] .test__run`);
await expect(page.locator(`#result-${testId}`)).toHaveClass(/is-open/, {
timeout: 15_000,
});
}
// ---------------------------------------------------------------------------
// Smoke — page loads and renders the shell
// ---------------------------------------------------------------------------
test.describe("smoke", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("page has correct title", async ({ page }) => {
await expect(page).toHaveTitle(/Ilia Dobkin/);
});
test("hero section renders name and role", async ({ page }) => {
await expect(page.locator(".hero__title")).toHaveText("Ilia Dobkin");
await expect(page.locator(".hero__sub")).toContainText("Senior SDET");
});
test("topbar contains Run all, Stop, Reset, and Theme toggle", async ({
page,
}) => {
await expect(page.locator("#run-all")).toBeVisible();
await expect(page.locator("#stop-all")).toBeVisible();
await expect(page.locator("#reset-all")).toBeVisible();
await expect(page.locator("#theme-toggle")).toBeVisible();
});
test("sidebar test explorer lists tests", async ({ page }) => {
const tests = page.locator("#tree .test");
await expect(tests.first()).toBeVisible();
expect(await tests.count()).toBeGreaterThan(0);
});
test("status bar shows Playwright version", async ({ page }) => {
await expect(page.locator(".statusbar")).toContainText("Playwright");
});
test("five tabs exist: Report, Trace, Source, Network, Console", async ({
page,
}) => {
for (const label of ["Report", "Trace", "Source", "Network", "Console"]) {
await expect(page.locator(`.tab[data-tab]`, { hasText: label })).toBeVisible();
}
});
});
// ---------------------------------------------------------------------------
// Run engine — Run All, individual runs, reset
// ---------------------------------------------------------------------------
test.describe("run engine", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await waitForIdle(page);
});
test("Run All executes and all tests pass", async ({ page }) => {
await runAllAndWait(page);
const passCount = page.locator("#cnt-pass");
expect(Number(await passCount.textContent())).toBeGreaterThan(0);
await expect(page.locator("#cnt-fail")).toHaveText("0");
});
test("clicking ▶ on a single test runs only that test", async ({
page,
}) => {
await runSingleTest(page, "about");
await expect(
page.locator('.test[data-id="about"]')
).toHaveClass(/is-passed/);
const passCount = Number(await page.locator("#cnt-pass").textContent());
expect(passCount).toBeGreaterThanOrEqual(1);
});
test("Reset clears all results back to idle", async ({ page }) => {
await runAllAndWait(page);
await page.click("#reset-all");
await expect(page.locator("#status-text")).toHaveText("idle");
await expect(page.locator("#cnt-pass")).toHaveText("0");
});
test("skipped test shows skip icon and badge", async ({ page }) => {
const skippedRow = page.locator('.test[data-id="perf-budget"]');
await expect(skippedRow).toHaveClass(/is-skipped/);
});
});
// ---------------------------------------------------------------------------
// Theme toggle
// ---------------------------------------------------------------------------
test.describe("theme", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("starts in dark mode by default", async ({ page }) => {
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
});
test("theme toggle cycles through dark → light → hc → dark", async ({
page,
}) => {
await page.click("#theme-toggle");
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await page.click("#theme-toggle");
await expect(page.locator("html")).toHaveAttribute("data-theme", "hc");
await page.click("#theme-toggle");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
});
});
// ---------------------------------------------------------------------------
// Tab navigation
// ---------------------------------------------------------------------------
test.describe("tabs", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("clicking each tab reveals corresponding pane", async ({ page }) => {
const tabs = ["report", "trace", "source", "network", "console"];
for (const tabId of tabs) {
await page.click(`.tab[data-tab="${tabId}"]`);
await expect(page.locator(`#pane-${tabId}`)).toHaveClass(/is-active/);
}
});
test("Source tab renders spec-like code", async ({ page }) => {
await page.click('.tab[data-tab="source"]');
await expect(page.locator("#source")).toContainText("import");
await expect(page.locator("#source")).toContainText("test.describe");
});
test("Trace tab renders career timeline", async ({ page }) => {
await page.click('.tab[data-tab="trace"]');
const rows = page.locator(".trace-row");
expect(await rows.count()).toBeGreaterThan(0);
});
test("Network tab renders Gitea repo rows", async ({ page }) => {
await page.click('.tab[data-tab="network"]');
const entries = page.locator(".network-entry");
await expect(entries.first()).toBeVisible();
expect(await entries.count()).toBeGreaterThan(0);
});
test("Console tab logs events after a test run", async ({ page }) => {
await runSingleTest(page, "about");
await page.click('.tab[data-tab="console"]');
const lines = page.locator(".console__line");
expect(await lines.count()).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
// Editor strip — switching spec files
// ---------------------------------------------------------------------------
test.describe("editor strip", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("editor tabs exist for all spec files", async ({ page }) => {
for (const file of [
"portfolio.spec.ts",
"projects.spec.ts",
"skills.spec.ts",
"playground.spec.ts",
]) {
await expect(
page.locator(".espec", { hasText: file })
).toBeVisible();
}
});
test("clicking a spec tab switches the active spec", async ({ page }) => {
await page.click('.espec[data-spec="projects"]');
await expect(
page.locator('.espec[data-spec="projects"]')
).toHaveClass(/is-active/);
const crumb = page.locator(".crumb strong");
await expect(crumb).toHaveText("projects.spec.ts");
});
test("switching spec shows Report pane with updated hero", async ({
page,
}) => {
await page.click('.espec[data-spec="projects"]');
await expect(page.locator("#pane-report")).toHaveClass(/is-active/);
await expect(page.locator(".hero__title")).toHaveText("Projects");
});
test("switching spec updates the Source tab", async ({ page }) => {
await page.click('.espec[data-spec="skills"]');
await page.click('.tab[data-tab="source"]');
await expect(page.locator("#source")).toContainText("skills");
});
});
// ---------------------------------------------------------------------------
// Sidebar — grep filter and tag chips
// ---------------------------------------------------------------------------
test.describe("filtering", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("typing in grep input filters visible tests", async ({ page }) => {
const allBefore = await page.locator("#tree .test").count();
await page.fill("#grep", "resume");
await page.waitForTimeout(200);
const visible = page.locator('#tree .test:not([style*="display: none"])');
const visibleCount = await visible.count();
expect(visibleCount).toBeLessThan(allBefore);
expect(visibleCount).toBeGreaterThan(0);
});
test("clicking a tag chip activates it and filters", async ({ page }) => {
const tag = page.locator('#tag-bar .tag[data-tag="@api"]');
await tag.click();
await expect(tag).toHaveClass(/is-active/);
await expect(page.locator("#grep")).toHaveValue(/@api/);
});
test("clear button removes all tag filters", async ({ page }) => {
await page.click('#tag-bar .tag[data-tag="@api"]');
await page.click("#tag-bar [data-clear]");
await expect(page.locator("#grep")).toHaveValue("");
});
});
// ---------------------------------------------------------------------------
// Keyboard shortcuts
// ---------------------------------------------------------------------------
test.describe("keyboard shortcuts", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await waitForIdle(page);
});
test("pressing ? opens keyboard shortcuts overlay", async ({ page }) => {
await page.keyboard.press("?");
await expect(page.locator("#kshelp")).not.toHaveAttribute("hidden", "");
});
test("pressing Escape closes the shortcuts overlay", async ({ page }) => {
await page.keyboard.press("?");
await expect(page.locator("#kshelp")).not.toHaveAttribute("hidden", "");
await page.keyboard.press("Escape");
await expect(page.locator("#kshelp")).toHaveAttribute("hidden", "");
});
test("pressing T toggles theme", async ({ page }) => {
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await page.keyboard.press("t");
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
});
test("pressing / focuses the grep input", async ({ page }) => {
await page.keyboard.press("/");
await expect(page.locator("#grep")).toBeFocused();
});
});
// ---------------------------------------------------------------------------
// Overflow menu (workers / headed)
// ---------------------------------------------------------------------------
test.describe("overflow menu", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("clicking ⋯ opens the overflow menu", async ({ page }) => {
await page.click("#overflow-btn");
await expect(page.locator("#overflow-menu")).not.toHaveAttribute(
"hidden",
""
);
});
test("workers dropdown is accessible", async ({ page }) => {
await page.click("#overflow-btn");
await expect(page.locator("#workers")).toBeVisible();
await page.selectOption("#workers", "4");
await expect(page.locator("#workers")).toHaveValue("4");
});
});
// ---------------------------------------------------------------------------
// Network tab — expand/collapse repo detail
// ---------------------------------------------------------------------------
test.describe("network detail", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.click('.tab[data-tab="network"]');
});
test("clicking a network row expands its detail panel", async ({ page }) => {
const firstEntry = page.locator(".network-entry").first();
await firstEntry.locator(".network-row").click();
await expect(firstEntry).toHaveClass(/is-open/);
await expect(firstEntry.locator(".network-detail")).not.toHaveAttribute(
"hidden",
""
);
});
test("expanded detail shows JSON body and repo link", async ({ page }) => {
const firstEntry = page.locator(".network-entry").first();
await firstEntry.locator(".network-row").click();
await expect(
firstEntry.locator(".network-detail__body")
).toContainText("full_name");
await expect(
firstEntry.locator(".network-detail__link")
).toBeVisible();
});
});
// ---------------------------------------------------------------------------
// Accessibility basics
// ---------------------------------------------------------------------------
test.describe("accessibility", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("all interactive buttons have accessible names", async ({ page }) => {
const buttons = page.locator(
"button:visible:not(.tag):not(.espec):not(.tab):not(.cmdpal__item)"
);
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const btn = buttons.nth(i);
const name = await btn.getAttribute("aria-label");
const title = await btn.getAttribute("title");
const text = (await btn.textContent())?.trim();
const hasName = (name && name.length > 0) ||
(title && title.length > 0) ||
(text && text.length > 0);
expect(hasName, `Button ${i} should have an accessible name`).toBe(true);
}
});
test("landmark roles are present", async ({ page }) => {
await expect(page.locator('[role="banner"]')).toBeVisible();
await expect(page.locator('[role="contentinfo"]')).toBeVisible();
});
});
// ---------------------------------------------------------------------------
// Responsive — mobile viewport
// ---------------------------------------------------------------------------
test.describe("responsive", () => {
test.use({ viewport: { width: 375, height: 812 } });
test("sidebar is hidden by default on mobile", async ({ page }) => {
await page.goto("/");
const sidebar = page.locator("#sidebar");
await expect(sidebar).not.toHaveClass(/is-open/);
});
test("menu button opens sidebar drawer on mobile", async ({ page }) => {
await page.goto("/");
await page.click("#menu-btn");
await expect(page.locator("#sidebar")).toHaveClass(/is-open/);
});
});
// ---------------------------------------------------------------------------
// End-to-end: full run lifecycle
// ---------------------------------------------------------------------------
test.describe("full lifecycle", () => {
test("run all → verify results → reset → verify idle", async ({ page }) => {
await page.goto("/");
await waitForIdle(page);
await runAllAndWait(page);
const passed = Number(await page.locator("#cnt-pass").textContent());
expect(passed).toBeGreaterThan(0);
await expect(page.locator("#cnt-fail")).toHaveText("0");
await expect(page.locator("#summary-stripe")).toContainText("passed");
await page.click("#reset-all");
await expect(page.locator("#status-text")).toHaveText("idle");
await expect(page.locator("#cnt-pass")).toHaveText("0");
});
});