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
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ jobs:
with:
go-version-file: "go.mod"
cache: false
- run: go vet ./...
# Scoped to src: node_modules ships a stray Go package that ./... would
# otherwise build.
- run: go vet ./src/...

build:
runs-on: ubuntu-latest
Expand Down
21 changes: 20 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,26 @@ jobs:
with:
go-version-file: "go.mod"
cache: false
- run: go test ./...
# Scoped to src: node_modules ships a stray Go package that ./... would
# otherwise build.
- run: go vet ./src/...
- run: go test ./src/...
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: ".tool-versions"
- run: npm ci
# The Playwright suite is linted with type information, which needs its
# own dependencies resolvable.
- run: npm ci
working-directory: e2e
- run: npm run lint
- name: Type-check the Playwright suite
run: npx tsc --noEmit
working-directory: e2e
styles:
runs-on: ubuntu-latest
steps:
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

Guidance for agents and contributors working in this repository.

## Code layout

`src` is one `main` package split by concern, one file per subject with its
tests beside it: `application.go` (wiring and entry point), `platform.go`
(client IP and header stripping), `endpoint.go` (endpoint IDs and URLs),
`capture.go` (the capture handler), `api.go` (the JSON API), `pages.go` (page
rendering), `assets.go` (content-hashed asset URLs), `security.go` (CSP and
security headers), `sockets.go` (the live feed), `requestlog.go` (correlation
IDs and the access log).

`newApplication` builds the entire routing surface from arguments, so tests
drive real requests through it without a listening socket. Anything that pulls
configuration out of the environment belongs in `main`, not in a handler.

## Comments

Comments describe what the code does now and warn about non-obvious constraints
Expand Down
32 changes: 21 additions & 11 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,13 +456,23 @@ must never be borrowed for a disabled, errored, or drop-target surface.

The system is implemented, not only described. `src/styles/components.css`
carries the classes these entries specify (`.btn` and its variants, `.field`,
`.field-label`, `.panel`, `.kv-row`, `.icon`, `.badge`, `.code-block`,
`.empty-value`, `.btn-lg`), and templates compose them rather
`.field-label`, `.region-label`, `.panel`, `.kv-row`, `.icon`, `.badge`,
`.code-block`, `.empty-value`, `.btn-lg`), and templates compose them rather
than repeating utility strings. Every page uses them: a template that re-spells
a component as a utility string is the bug, not a shortcut. Utilities stay the default for one-off composition; anything whose
tokens must not drift between call sites belongs in that file. Before adding a
variant, check whether an existing class should absorb it: eight spellings of one
button is how two nominally identical controls ended up 4px apart.
variant, check whether an existing class should absorb it. Several spellings of
one component is how two nominally identical controls come to sit a few pixels
apart.

There is one surface class, `.panel`: a white fill, a 1px neutral-200 seam and
no shadow. A second, quieter panel would be the One Edge Rule broken by another
name, so a surface that needs to read as nested takes its distinction from
spacing or tone rather than from a class of its own.

`.field-label` and `.region-label` carry one type token between them. The field
label owns the spacing above its input; the region label takes spacing from the
call site, because a heading inside a flex row must not carry a bottom margin.

### Buttons

Expand Down Expand Up @@ -503,9 +513,9 @@ Every other route is reachable from the footer: feedback, the GitHub repository
with an inline brand glyph, and the Formspark credit: set at 0.875rem neutral-500,
centred, hovering to brand-600.

Every link carries its own padding and a 2.75rem minimum height. As bare inline
text their hit area collapses to the text box, which lands under the 24px target
minimum and makes the page's only exits hard to hit on a phone. Links also carry
Every link carries its own padding and a 2.75rem minimum height. Bare inline
text has a hit area no larger than its text box, which lands under the 24px
target minimum and makes the page's only exits hard to hit on a phone. Links also carry
the same authored `focus-visible` ring as buttons rather than falling back to the
browser's default outline, which is engine-specific and belongs to no design
system.
Expand Down Expand Up @@ -551,10 +561,10 @@ substitution, not a degradation.
### Render Window

The stream renders 25 cards and reveals another 25 per press of a Show more
control, rather than rendering everything the store holds. Each card is roughly
104 DOM nodes, so a full page of them is a five-figure node count and a visible
stall on every filter change. The store still holds every captured request; only
the DOM is bounded.
control, rather than rendering everything the store holds. Each card is around a
hundred DOM nodes, so a full endpoint's worth is a five-figure node count and a
visible stall on every filter change. The store still holds every captured
request; only the DOM is bounded.

### Waiting State

Expand Down
18 changes: 16 additions & 2 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,26 @@ Run project:
go run ./src
```

Lint the page scripts and the Playwright suite:

```bash
npm run lint
npm run lint:fix
```

The Playwright suite is linted with type information, so `e2e` needs its own
dependencies installed. The Go application is checked by `go vet` instead.

Run unit tests:

```bash
go test ./...
go vet ./src/...
go test ./src/...
```

Every Go package lives under `src`. `./...` also walks `node_modules`, which
ships a stray Go package once the npm tooling is installed.

Run E2E tests (Playwright auto-starts the binary; build it first):

```bash
Expand All @@ -48,7 +62,7 @@ cd e2e && npx playwright test
View test coverage:

```bash
go test ./... -coverprofile=coverage.out
go test ./src/... -coverprofile=coverage.out
go tool cover -html=coverage.out
```

Expand Down
45 changes: 38 additions & 7 deletions e2e/tests/contact-screen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,45 @@ test.describe("Contact screen", () => {
await page.goto("/contact");
});

test("title should be correct", async ({ page }) => {
await expect(page).toHaveTitle("Contact | httphq");
test.describe("Page", () => {
test("the title names the page", async ({ page }) => {
await expect(page).toHaveTitle("Contact | httphq");
});

test("the page ships no javascript", async ({ page }) => {
await expect(page.locator("script")).toHaveCount(0);
});
});

test("form should be functional", async ({ page }) => {
await page.locator('input[name="name"]').fill("John Doe");
await page.locator('input[name="email"]').fill("john@doe.test");
await page.locator('textarea[name="message"]').fill("Hello, World!");
await expect(page.locator('button[data-test="send-form"]')).toBeEnabled();
test.describe("Form", () => {
test("a filled form is submittable", async ({ page }) => {
await page.locator('input[name="name"]').fill("John Doe");
await page.locator('input[name="email"]').fill("john@doe.test");
await page.locator('textarea[name="message"]').fill("Hello, World!");
await expect(page.locator('button[data-test="send-form"]')).toBeEnabled();
});

test("every field is labelled and required", async ({ page }) => {
for (const label of ["Name", "Email", "Message"]) {
const field = page.getByLabel(label, { exact: true });
await expect(field).toBeVisible();
await expect(field).toHaveAttribute("required", "");
}
});

test("an empty form does not submit", async ({ page }) => {
await page.locator('button[data-test="send-form"]').click();
await expect(page).toHaveURL(/\/contact$/);
});

// The hidden checkbox is a honeypot: a real visitor never sees it, so a
// submission that ticks it came from something filling fields blindly.
test("the honeypot field stays out of the visible form", async ({
page,
}) => {
const honeypot = page.locator('input[name="accept"]');
await expect(honeypot).toBeHidden();
await expect(honeypot).toHaveAttribute("tabindex", "-1");
});
});
});
Loading
Loading