All checks were successful
ci / fast (linux) (push) Successful in 7m16s
Implement full form submission pipeline: textarea form data collection with CR+LF newline normalization, submit button value inclusion via submitter tracking (Option<NodeId>), multipart/form-data and text/plain encoding support, configurable content_type threading through navigate_post → NetworkStack → HttpLoader, and cancelable submit event dispatch with preventDefault support at all three trigger sites. Includes review fixes for UTF-8 safety in newline normalization, RFC 7578 field name escaping in multipart encoding, and boundary collision avoidance. 15 unit tests, 3 integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
222 lines
17 KiB
Markdown
222 lines
17 KiB
Markdown
# Story 4.4: Form Submission
|
|
|
|
Status: done
|
|
|
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
## Story
|
|
|
|
As a web user,
|
|
I want forms to submit my data to the server,
|
|
so that I can log in, search, and interact with web applications.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **GET form submission:** A `<form method="GET">` with filled controls serializes data as `application/x-www-form-urlencoded` and appends it to the action URL as a query string, then navigates to the resulting URL. Per HTML §4.10.21.3.
|
|
|
|
2. **POST form submission:** A `<form method="POST">` with filled controls serializes data as `application/x-www-form-urlencoded` in the request body, then navigates to the action URL with the POST body. Per HTML §4.10.21.3.
|
|
|
|
3. **Multipart/form-data encoding:** A `<form enctype="multipart/form-data" method="POST">` serializes data using multipart MIME format with correct boundary delimiters per RFC 2388. Each field becomes a MIME part with `Content-Disposition: form-data; name="..."` headers.
|
|
|
|
4. **Excluded form controls:** Disabled controls, controls without `name` attributes, unchecked checkboxes/radios, and buttons that are not the submitter are excluded from submitted data. Per HTML §4.10.21.3.
|
|
|
|
5. **Form action & method attributes:** The `action` attribute determines the submission URL (defaults to current page URL). The `method` attribute determines GET or POST (defaults to GET, case-insensitive). The `enctype` attribute determines encoding (defaults to `application/x-www-form-urlencoded`). Per HTML §4.10.6.
|
|
|
|
6. **Textarea form submission:** A `<textarea name="...">` element includes its runtime text content (from `input_states`) in form data. Newlines are normalized to CR+LF per HTML §4.10.11.
|
|
|
|
7. **Submit button value inclusion:** When a specific submit button triggers submission, its `name`/`value` pair is included in the form data. Other submit buttons in the same form are excluded. Per HTML §4.10.21.3.
|
|
|
|
8. **Submit event dispatch:** Before actual submission, a `submit` event (cancelable, bubbles) is dispatched on the form element. If `event.preventDefault()` is called, submission is cancelled. Per HTML §4.10.21.3.
|
|
|
|
9. **Integration tests** verify each submission method and encoding, golden tests are not affected (form submission is runtime behavior), and `just ci` passes.
|
|
|
|
## What NOT to Implement
|
|
|
|
- **No file input support** -- `<input type="file">` is deferred; multipart encoding only handles text fields in this story.
|
|
- **No `<input type="image">` coordinate submission** -- image inputs deferred.
|
|
- **No `formaction`/`formmethod`/`formenctype` overrides on buttons** -- deferred to a future story.
|
|
- **No `target` attribute support** -- all submissions navigate in the current browsing context.
|
|
- **No form validation before submission** -- that's Story 4.5. This story submits unconditionally (except submit event cancellation).
|
|
- **No `<input type="hidden">` special handling** -- hidden inputs already work as text inputs with name/value.
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Task 1: Add textarea to form data collection (AC: #6)
|
|
- [x] 1.1 In `form.rs` `collect_form_data_recursive()`, add a `"textarea"` branch: read runtime text from `input_states` (falling back to DOM `text_content()`), add `name=value` to form data
|
|
- [x] 1.2 Normalize textarea newlines to CR+LF (`\r\n`) before encoding per HTML §4.10.11
|
|
- [x] 1.3 Add unit tests in `form_tests.rs`: textarea with runtime value, textarea with DOM-only value, textarea without name (excluded), disabled textarea (excluded)
|
|
|
|
- [x] Task 2: Add submit button value to form data (AC: #7)
|
|
- [x] 2.1 Add `submitter: Option<NodeId>` parameter to `submit_form()` and `collect_form_data()` — the NodeId of the button/input that triggered submission
|
|
- [x] 2.2 In `collect_form_data_recursive()`, when encountering `type="submit"` input or `<button type="submit">`: include its `name`/`value` ONLY if it matches the submitter NodeId
|
|
- [x] 2.3 Thread `submitter` from click handlers in `event_handler.rs` through to `submit_form()`
|
|
- [x] 2.4 For Enter-key submission, submitter is `None` (no button value included) — matches spec behavior
|
|
- [x] 2.5 Add unit tests: submit button value included when it's the submitter, excluded when it's not the submitter, excluded when submitter is None
|
|
|
|
- [x] Task 3: Implement multipart/form-data encoding (AC: #3)
|
|
- [x] 3.1 Add `enctype` attribute reading in `submit_form()`: parse `enctype` from form element (default `application/x-www-form-urlencoded`, also support `multipart/form-data` and `text/plain`)
|
|
- [x] 3.2 Create `encode_multipart_form_data()` function in `form.rs`: generate a random boundary string, encode each name=value pair as a MIME part with `Content-Disposition: form-data; name="..."` header, separated by `--boundary` lines
|
|
- [x] 3.3 Update `FormSubmission::Post` to include `content_type: String` field (instead of hardcoded `application/x-www-form-urlencoded`)
|
|
- [x] 3.4 Update `navigate_post()` in `app_state.rs` and `post_form()` in `net` to accept and use the content type from FormSubmission
|
|
- [x] 3.5 In `http_loader.rs` `post_form_with_headers()`, use the provided content type instead of hardcoded `application/x-www-form-urlencoded`
|
|
- [x] 3.6 Add unit tests: multipart encoding format correctness (boundary, headers, body parts), `text/plain` encoding (simple name=value lines)
|
|
|
|
- [x] Task 4: Dispatch submit event before submission (AC: #8)
|
|
- [x] 4.1 In `event_handler.rs`, before calling `submit_form()`, dispatch a `submit` event on the form element: `{type: "submit", bubbles: true, cancelable: true}`
|
|
- [x] 4.2 Use the existing event dispatch infrastructure (`dispatch_dom_event` or similar) — check how `change` event is dispatched in Story 4.1/4.3 for the pattern
|
|
- [x] 4.3 If the submit event's `defaultPrevented` is true after dispatch, skip the form submission entirely
|
|
- [x] 4.4 Add integration tests in `js_events.rs`: submit event fires, submit event bubbles, submit event can be cancelled via preventDefault
|
|
|
|
- [x] Task 5: Wire everything together and update navigation (AC: #1, #2, #5)
|
|
- [x] 5.1 Update all three form submission trigger sites in `event_handler.rs` (input[type=submit], button[type=submit], Enter key) to: dispatch submit event first (Task 4), pass submitter NodeId (Task 2), then call `submit_form()`
|
|
- [x] 5.2 Update `navigate_post()` in `app_state.rs` to accept content_type parameter and pass it through to `NetworkStack::post_form()`
|
|
- [x] 5.3 Update `NetworkStack::post_form()` to accept and use content_type parameter
|
|
- [x] 5.4 Verify GET submission still appends query string correctly (existing behavior)
|
|
- [x] 5.5 Verify POST submission still sends body correctly (existing behavior)
|
|
|
|
- [x] Task 6: Integration tests and CI (AC: #9)
|
|
- [x] 6.1 Add integration test: GET form submission with mixed controls (text, checkbox, radio, select, textarea) produces correct query string
|
|
- [x] 6.2 Add integration test: POST form submission produces correct body
|
|
- [x] 6.3 Add integration test: disabled controls and nameless controls excluded
|
|
- [x] 6.4 Add integration test: submit button value included only for submitter
|
|
- [x] 6.5 Verify all existing form tests pass (no regressions)
|
|
- [x] 6.6 Run `just ci` — must pass
|
|
- [x] 6.7 Update `docs/HTML5_Implementation_Checklist.md` with form submission support
|
|
|
|
## Dev Notes
|
|
|
|
### Existing Infrastructure (DO NOT REBUILD)
|
|
|
|
**Form data collection already works** (`crates/app_browser/src/form.rs`):
|
|
- `submit_form()` (lines ~155-240): Reads `method`/`action` from form, resolves URLs, collects data, encodes, returns `FormSubmission::Get{url}` or `FormSubmission::Post{url, body}`
|
|
- `collect_form_data_recursive()` (lines ~53-119): Walks DOM tree, handles `<input>` (text/checkbox/radio), `<select>` (via `collect_select_data()`). **MISSING: textarea and submit button value.**
|
|
- `find_ancestor_form()` (lines ~17-28): Walks up DOM to find parent `<form>`
|
|
- URL encoding via `url::form_urlencoded::Serializer` already works
|
|
|
|
**Navigation after submission already works** (`crates/app_browser/src/app_state.rs`):
|
|
- `navigate()`: For GET — parses URL, fetches via `NetworkStack::load_sync()`, runs pipeline
|
|
- `navigate_post()`: For POST — same flow but uses `NetworkStack::post_form()`
|
|
- Both clear input/checked/select states, push history, trigger re-render
|
|
|
|
**Network POST already works** (`crates/net/src/`):
|
|
- `NetworkStack::post_form()` → `HttpLoader::post_form_with_headers()`: Sends POST with body, handles redirects (302/303→GET, 307/308→POST), reads response
|
|
- **Currently hardcodes `Content-Type: application/x-www-form-urlencoded`** — must make configurable for multipart
|
|
|
|
**Event dispatch exists** (`crates/app_browser/src/event_handler.rs`):
|
|
- `dispatch_change_event_with_swap()` in form.rs dispatches change events — follow same pattern for submit event
|
|
- Three form submission trigger sites: input[type=submit] (~line 375), button[type=submit] (~line 449), Enter key (~line 1010)
|
|
|
|
**Form submission tests exist** (`crates/app_browser/src/tests/form_tests.rs`):
|
|
- 23 existing tests covering GET/POST, URL resolution, encoding, checkbox/radio/select inclusion/exclusion
|
|
- **No textarea tests, no submit button value tests, no multipart tests, no submit event tests**
|
|
|
|
### What Needs to Be Built
|
|
|
|
1. **Textarea in form data**: Add `"textarea"` branch to `collect_form_data_recursive()`. Read from `input_states` (runtime value) with DOM `text_content()` fallback. Normalize newlines to CR+LF.
|
|
|
|
2. **Submit button value**: Add `submitter: Option<NodeId>` parameter threading from click handlers through `submit_form()` → `collect_form_data()` → `collect_form_data_recursive()`. Include submit button name/value only when it matches the submitter.
|
|
|
|
3. **Multipart encoding**: New `encode_multipart_form_data()` function. Update `FormSubmission::Post` to carry `content_type`. Thread content type through `navigate_post()` → `NetworkStack::post_form()` → `HttpLoader::post_form_with_headers()`.
|
|
|
|
4. **Submit event**: Dispatch cancelable `submit` event on form element before submission. If cancelled, skip submission. Use existing event dispatch pattern from change events.
|
|
|
|
### Architecture Compliance
|
|
|
|
| Rule | How This Story Complies |
|
|
|------|------------------------|
|
|
| Layer boundaries | All form logic in `app_browser` (Layer 3). Network in `net` (Layer 1). No upward dependencies. |
|
|
| Unsafe policy | No unsafe code needed. |
|
|
| Pipeline sequence | Form submission is runtime behavior, doesn't affect style/layout/paint pipeline. |
|
|
| Arena ID pattern | Uses existing `NodeId` for submitter tracking. No new ID types. |
|
|
| Existing patterns | Follows `checked_states`/`select_states` patterns for runtime state. Follows `dispatch_change_event` pattern for submit event. |
|
|
|
|
### File Modification Plan
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `crates/app_browser/src/form.rs` | Add textarea branch in `collect_form_data_recursive()`, add submitter param, add `encode_multipart_form_data()`, read `enctype` attribute, update `FormSubmission::Post` to include content_type |
|
|
| `crates/app_browser/src/event_handler.rs` | Dispatch submit event before submission at all 3 trigger sites, pass submitter NodeId through to `submit_form()` |
|
|
| `crates/app_browser/src/app_state.rs` | Update `navigate_post()` to accept and pass content_type |
|
|
| `crates/net/src/lib.rs` | Update `post_form()` signature to accept content_type |
|
|
| `crates/net/src/http_loader.rs` | Update `post_form_with_headers()` to use provided content_type instead of hardcoded value |
|
|
| `crates/app_browser/src/tests/form_tests.rs` | Add textarea tests, submit button value tests, multipart encoding tests |
|
|
| `tests/js_events.rs` | Add submit event integration tests (fires, bubbles, preventDefault cancels) |
|
|
| `docs/HTML5_Implementation_Checklist.md` | Update form submission status |
|
|
|
|
### Testing Strategy
|
|
|
|
- **Unit tests** (in `form_tests.rs`): Textarea collection, submit button value inclusion/exclusion, multipart encoding format, text/plain encoding, enctype parsing
|
|
- **Integration tests** (in `js_events.rs`): Submit event dispatch, cancellation, bubble. Full form submission with mixed control types.
|
|
- **Existing tests**: All 23 existing form tests must pass without modification (except adding new `submitter` param with `None` default)
|
|
- **No golden tests needed**: Form submission is runtime behavior, not visual rendering
|
|
|
|
### Previous Story Intelligence
|
|
|
|
Story 4.3 established patterns this story MUST follow:
|
|
- **Parameter threading**: When adding new params to `submit_form()`, all callers must be updated (3 trigger sites in event_handler.rs + all test calls in form_tests.rs)
|
|
- **Runtime state over DOM**: Always prefer `input_states`/`checked_states`/`select_states` over DOM attributes
|
|
- **Event dispatch pattern**: `dispatch_change_event_with_swap()` shows how to dispatch events with proper bubbling
|
|
- **`just ci` after every task**: Don't batch — verify incrementally
|
|
- **Deferred items are OK**: Mark tasks as deferred with clear rationale rather than implementing half-baked solutions
|
|
- **Review found H2/H3 bugs**: Invalidation and state threading through fast paths — when adding content_type to navigation, ensure it reaches all code paths
|
|
|
|
### Git Intelligence
|
|
|
|
Recent commits show stable codebase:
|
|
- `71f263f` Story 4.3 complete (select menus)
|
|
- `f8e0c47` Story 4.2 complete (buttons, checkboxes, radio)
|
|
- `e9d3ffd` Code review fixes for 4.1
|
|
- `64a3439` Story 4.1 complete (text inputs, textareas)
|
|
- No in-flight refactoring. Convention: descriptive commit messages without story numbers.
|
|
|
|
### References
|
|
|
|
- [Source: crates/app_browser/src/form.rs#L155-L240] — `submit_form()` main entry
|
|
- [Source: crates/app_browser/src/form.rs#L53-L119] — `collect_form_data_recursive()` (needs textarea + submitter)
|
|
- [Source: crates/app_browser/src/event_handler.rs#L375-L401] — Input submit trigger site
|
|
- [Source: crates/app_browser/src/event_handler.rs#L449-L469] — Button submit trigger site
|
|
- [Source: crates/app_browser/src/event_handler.rs#L1010-L1032] — Enter key submit trigger site
|
|
- [Source: crates/net/src/http_loader.rs#L331-L456] — `post_form_with_headers()` (hardcoded content type)
|
|
- [Source: crates/app_browser/src/app_state.rs#L114-L142] — `navigate_post()`
|
|
- [HTML §4.10.21.3] — Form submission algorithm
|
|
- [HTML §4.10.11] — Textarea element (newline normalization)
|
|
- [RFC 2388] — Multipart form-data encoding
|
|
- [RFC 7231] — HTTP POST semantics
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude Opus 4.6 (1M context)
|
|
|
|
### Debug Log References
|
|
|
|
No blocking issues encountered. All tasks completed in a single pass.
|
|
|
|
### Completion Notes List
|
|
|
|
- **Task 1**: Added `collect_textarea_data()` with runtime value (input_states) fallback to DOM text_content, CR+LF newline normalization via `normalize_newlines_crlf()`. 5 unit tests.
|
|
- **Task 2**: Added `submitter: Option<NodeId>` parameter threaded through `submit_form()` → `collect_form_data()` → `collect_form_data_recursive()`. Submit button (`<input type="submit">` and `<button type="submit">`) name/value included only when matching submitter. All 3 event_handler trigger sites updated. 4 unit tests.
|
|
- **Task 3**: Added `enctype` attribute reading, `encode_multipart_form_data()` (RFC 2388 with boundary), `encode_text_plain()`. Updated `FormSubmission::Post` to carry `content_type`. Threaded content_type through `navigate_post()` → `NetworkStack::post_form()` → `HttpLoader::post_form_with_headers()`. Replaced hardcoded `application/x-www-form-urlencoded`. 3 unit tests.
|
|
- **Task 4**: Added `dispatch_submit_event()` to `web_api` and `browser_runtime`. Added `dispatch_submit_event_with_swap()` helper in event_handler. Wired submit event dispatch before submission at all 3 trigger sites. If `defaultPrevented`, submission is skipped. 3 integration tests in js_events.rs.
|
|
- **Task 5**: Already completed during Tasks 2-4 (all wiring done incrementally).
|
|
- **Task 6**: Added 3 mixed-control integration tests. `just ci` passes. HTML5 checklist updated.
|
|
|
|
### File List
|
|
|
|
- `crates/app_browser/src/form.rs` — Added textarea collection, submitter param, multipart/text-plain encoding, enctype reading, content_type in FormSubmission::Post
|
|
- `crates/app_browser/src/event_handler.rs` — Submit event dispatch before submission at all 3 trigger sites, submitter NodeId threading, dispatch_submit_event_with_swap helper
|
|
- `crates/app_browser/src/app_state.rs` — navigate_post accepts content_type param
|
|
- `crates/app_browser/src/tests/form_tests.rs` — 15 new tests (textarea: 5, submit button: 4, multipart: 3, mixed-controls: 3)
|
|
- `crates/net/src/lib.rs` — post_form accepts content_type param
|
|
- `crates/net/src/http_loader.rs` — post_form/post_form_with_headers accept content_type, use it instead of hardcoded value
|
|
- `crates/web_api/src/lib.rs` — dispatch_submit_event (bubbles, cancelable)
|
|
- `crates/browser_runtime/src/lib.rs` — dispatch_submit_event wrapper returning default_prevented
|
|
- `tests/js_events.rs` — 3 submit event integration tests (fires, bubbles, preventDefault cancels)
|
|
- `docs/HTML5_Implementation_Checklist.md` — Updated form submission status
|
|
- `tests/manual/js_test.html` — Manual test page for form submission (debug artifact)
|
|
|
|
### Change Log
|
|
|
|
- 2026-03-29: Implemented Story 4.4 Form Submission — textarea form data, submit button value (submitter tracking), multipart/form-data and text/plain encoding, submit event dispatch with preventDefault cancellation, content_type threading through navigation stack. 15 new unit tests, 3 integration tests. All ACs satisfied, `just ci` passes.
|