ilia 0d6c7ebb53
CI / skip-ci-check (push) Successful in 39s
CI / secret-scan (push) Successful in 38s
CI / node-ci (push) Successful in 1m40s
Merge pull request 'Add 1200×630 OG image and large Twitter card' (#13) from feat/og-image-1200x630 into master
2026-08-12 12:58:31 -05:00
2026-05-12 16:27:19 -04:00
2026-07-29 13:55:42 -04:00
2026-07-26 14:39:05 -05:00
2026-07-29 13:55:42 -04:00
2026-07-29 13:55:42 -04:00
2026-07-29 13:55:42 -04:00

portfolio.spec.ts

License: MIT Live

Career portfolio styled as a Playwright test runner — sidebar explorer, editor tabs, trace viewer, Network tab, keyboard shortcuts.

Live: sdet.levkin.ca · Public mirror: github.com/Gitilia/sdetProfile

Why this exists

Hiring managers who work with SDETs already live in test runners. This site uses that UI metaphor instead of a generic landing page: sidebar explorer, editor tabs, status pill, trace viewer, network panel, and keyboard shortcuts modeled on Playwright and VS Code.

Stack

Intentionally zero-framework. The whole point is craftsmanship: hand-rolled HTML, CSS variables, and vanilla JS. Easy to read, easy to fork, deploys anywhere static.

Layer Choice
Markup Single index.html
Styling css/base.css (tokens) + css/app.css
Behavior js/data.js (content) + js/app.js (UI)
Type Inter (sans) + JetBrains Mono (mono)
Icons / Logo Hand-written inline SVG
Build None — open the file
Tests (dev) @playwright/test against npx serve
Hosting Any static host (S3, Netlify, GitHub Pages, your homelab)

Project structure

portfolio/
├── index.html              # Single-page shell — topbar, editor bar, sidebar, tabs, statusbar
├── README.md               # You are here
├── IDEAS.md                # Future work, ranked by effort/payoff
├── package.json            # Dev-only — Playwright test runner
├── playwright.config.ts    # Playwright config — serves site locally on port 3173
├── scripts/
│   └── fetch-gitea-repos.mjs   # Optional: regenerate `giteaRepos` from git.levkin.ca API + READMEs
├── tests/
│   └── portfolio.spec.ts   # 37 Playwright specs exercising the live site
├── css/
│   ├── base.css            # Design tokens: colors, type, spacing, dark/light/hc themes
│   └── app.css             # Component styles: tree, tabs, results, trace, network, source, etc.
├── js/
│   ├── data.js             # All portfolio content — single source of truth (incl. `giteaRepos`)
│   └── app.js              # Test-runner behavior: tree, run engine, tabs, theme, drawer
└── assets/
    ├── favicon.svg
    └── DobkinResume26.pdf

Quick start

# clone / open in Cursor, then:
cd portfolio
python3 -m http.server 8765
open http://localhost:8765

That's it. No build step, no dependencies. Edit a file, refresh the page.


Running the Playwright tests

The site that looks like a Playwright report is itself tested by real Playwright. The tests/ directory ships 37 specs across 12 describe blocks covering smoke checks, the run engine, theme cycling, tab navigation, editor strip switching, grep/tag filtering, keyboard shortcuts, the network panel, accessibility basics, the overflow menu, mobile responsiveness, and a full lifecycle scenario.

npm install
npx playwright install    # downloads browser binaries
npm test                  # runs against Chromium by default

Useful variants:

npm run test:headed       # watch the browser — great for debugging
npm run test:ui           # Playwright's interactive UI mode
npm run report            # open the HTML report from the last run

By default the config spins up a local static server on port 3173 (via npx serve) and runs Chromium only. Firefox and WebKit projects are present in playwright.config.ts — uncomment them to fan out. To test against a deployed URL instead of the local server:

BASE_URL=https://sdet.levkin.ca npx playwright test

Note: package.json and node_modules/ are test-only concerns — the site itself still has zero build step.


Lint & quality checks

The site has no build step, but the dev toolchain runs a four-step quality pass on every commit-worthy change. All checks have an npm script and can be run individually.

npm run lint        # eslint + stylelint + html-validate, in parallel
npm run lint:js     # ESLint over js/, scripts/, tests/, *.ts (vanilla JS + TS)
npm run lint:css    # Stylelint over css/*.css (bug-only ruleset, no style nags)
npm run lint:html   # html-validate over index.html (a11y, roles, DOCTYPE)
npm run typecheck   # tsc --noEmit on playwright.config.ts + tests/*.ts
npm run check       # lint + typecheck + test (the full guardrail set)

Conventions worth knowing:

  • ESLint uses a flat config (eslint.config.mjs) with three scopes — browser globals for js/, Node ESM for scripts/, and typescript-eslint for tests/ + *.ts. console.warn / console.error are allowed; console.log is not.
  • Stylelint runs a minimal "bug-only" ruleset (.stylelintrc.json) — block-no-empty, no-invalid-hex, function-no-unknown, property-no-unknown, etc. Compact handwritten CSS (single-line rules, rgba(), 6-char hex) is intentional and not flagged.
  • html-validate (.htmlvalidate.json) enforces uppercase DOCTYPE, accessible button names, non-redundant ARIA roles, and valid landmark usage.
  • tsc (tsconfig.json) runs in --noEmit strict mode against the test files — fails fast if a selector signature drifts.

npm-run-all2 parallelizes the linters for speed (npm run lint finishes in ~3s); npm run check is the one to wire into a Gitea Actions runner.


Editing content

All content lives in js/data.js under window.PORTFOLIO. Change a title, swap a job, retag a skill — the UI updates automatically because every section is rendered from this single object.

window.PORTFOLIO = {
  person: { first: 'Ilia', last: 'Dobkin', /* ... */ },

  // Master tag palette — drives the filter bar (first 6 shown, rest in "+N more")
  tags: ['@playwright', '@cypress', '@api', '@ci', /* ... */],

  // Open .spec.ts files — each becomes a tab in the editor strip
  specs: [
    { id: 'portfolio',  file: 'portfolio.spec.ts',  describe: 'Ilia Dobkin · portfolio' },
    { id: 'projects',   file: 'projects.spec.ts',   describe: 'Levkin · projects' },
    { id: 'skills',     file: 'skills.spec.ts',     describe: 'Ilia Dobkin · skills' },
    { id: 'playground', file: 'playground.spec.ts', describe: 'Ilia Dobkin · playground' },
  ],

  // The test suite — each entry maps to one portfolio section
  suite: {
    name: 'Ilia Dobkin · portfolio',
    tests: [
      {
        id: 'about',
        spec: 'portfolio',                // ← which file this test lives in
        title: 'should introduce Ilia Dobkin',
        tags: ['@playwright', '@leadership'],
        duration: 142,
        steps: [
          { kind: 'info', title: 'navigate to /about',     dur: 12 },
          { kind: 'ok',   title: 'render bio',             dur: 48 },
          { kind: 'ok',   title: 'assert credentials',     dur: 82 },
        ],
        render: renderAbout, // function that returns the section HTML
      },
      {
        id: 'perf-budget',
        spec: 'portfolio',
        title: 'should meet performance budget',
        skip: true,                       // ← amber ⊘ icon, excluded from Run All
        skipReason: 'Lighthouse CI not wired — pending infra',
        // ...
      },
      // ...
    ],
  },

  experience: [ /* roles, in reverse-chronological order */ ],
  skills:     [ /* name, level (0–100), tags */ ],
  projects:   [ /* name, desc, tags */ ],
  stack:      { Editors: [...], Languages: [...], /* ... */ },
  metrics:    [ /* label / value pairs for the KPI cards */ ],
  giteaRepos: [ /* full_name, html_url, language, description — Network tab */ ],
};

Add a new test (section)

  1. Add an entry to PORTFOLIO.suite.tests — set spec to the spec it belongs in (matching one of PORTFOLIO.specs[].id).
  2. Write a render<Name>() function further down in data.js that returns HTML.
  3. Reference it as the render property. Done — it appears in the sidebar + report when its spec tab is active.

Skip a test

Set skip: true on the test entry. It will render with the amber ⊘ icon, be excluded from Run All, and display its skipReason in the body.

Add a new spec file (tab)

  1. Append an entry to PORTFOLIO.specs ({ id, file, describe }).
  2. Tag any tests into it via the spec: '<id>' field.

That's it — a new tab appears in the editor strip with the count badge.

Add a new tag

Append to PORTFOLIO.tags. To make it filter anything, also add it to the tags: [] array on the relevant tests. Only the first 6 tags are shown by default; the rest hide behind a "+N more" chip.

Add an experience entry

Push to PORTFOLIO.experience. The Trace tab parses when ("Aug 2023 – Apr 2026") automatically and lays it out on the timeline.

Refresh Gitea repo descriptions

Public repos are listed at git.levkin.ca/explore/repos; the HTTP UI may split results across pages, but the Gitea API returns all 19 public repos in one repos/search response. To refresh blurbs from live READMEs:

node scripts/fetch-gitea-repos.mjs

Copy the printed giteaRepos: [ … ] block into js/data.js (or merge rows by hand). Descriptions prefer the repo's Gitea description field, then the first paragraph of README.md.


Customizing the look

All design tokens live in css/base.css as CSS variables, scoped to :root[data-theme='dark'], :root[data-theme='light'], and :root[data-theme='hc'] (WCAG AAA high-contrast).

Want a different accent? Change --accent (default #4ec9b0, Playwright's signature teal).

Different font? Swap the <link> in index.html and --font-sans / --font-mono in base.css.

Different statusbar color? --statusbar-bg (default VS Code blue #007acc).


Deploying

Any static host works because there's no build:

  • S3 / CloudFront — upload the folder; entry point index.html.
  • GitHub Pages — push to gh-pages branch, point to root.
  • Netlify / Vercel — drag-and-drop the folder.
  • Your homelab (Caddy / nginx) — just serve the directory.
  • Custom domain (e.g. sdet.levkin.ca) — point an A/CNAME record at your host.

Note: the theme toggle cycles dark → light → high-contrast (WCAG AAA) and persists via a cookie, which works fine on any normal domain. If you embed in a sandboxed iframe that strips cookies, the theme will reset on reload but otherwise works.


Architecture cheatsheet

Render loop is simple and stateless per-test:

state[testId] = { status: 'idle' | 'running' | 'passed' | 'skipped', runtime: ms }

runTest(id)
  ├─ if test.skip → log warning, bail
  ├─ flip state to 'running'
  ├─ refreshTreeRow + refreshResultRow         (update sidebar + main pane)
  ├─ animate progress bar via requestAnimationFrame
  └─ on completion → state 'passed', renderBody() injects section HTML

Where to look for things:

You want to change… Open
What a test looks like inside data.js — the matching render* fn
The order or set of tests data.js — PORTFOLIO.suite.tests
The set of spec files (tabs) data.js — PORTFOLIO.specs (+ spec on each test)
The editor bar + overflow menu app.js — renderEditorStrip() / initOverflowMenu()
The status pill / summary stripe app.js — updateStatusbar() / updateSummaryStripe()
The tag bar + clear button app.js — renderTagBar() / syncTagBarUI() / clearTagFilter()
The trace tab parsing app.js — renderTrace() + parseMon()
The fake source code app.js — renderSource()
The Network / Gitea repo list data.js — giteaRepos + app.js — renderNetwork()
Run-animation timing app.js — runTest() / tween()
Theme colors base.css — :root[data-theme=*]
Mobile breakpoints app.css — @media (max-width: 900px)

Roadmap

See IDEAS.md for the backlog. Quick wins on top, ambitious experiments at the bottom.


License

MIT — see LICENSE. Fork and replace the content as you like.

S
Description
SDET portfolio styled as a Playwright test report
Readme MIT
776 KiB
Languages
JavaScript 52.6%
CSS 27.6%
TypeScript 10%
HTML 7.7%
Shell 1.9%
Other 0.2%