RedlineKnowledge base

B3 UX

The three thesis capabilities and the network states, each proven by a named Playwright spec against the fixture site. Every spec starts by calling POST http://localhost:9999/__mock/reset and POST /__mock/up in a beforeEach.

Shared helper to create first: test/e2e/helpers.js exporting resetMock(request), setMockDown(request, down), mockState(request), openFooter(page) (clicks [data-disclosure-toggle] inside #history), selectText(page, blockId, text) (uses page.evaluate to build a Range over the first occurrence of text inside [data-redline-block="<id>"], add it to the selection, and dispatch mouseup on the element), and connectGitLab(page) (clicks the connect link and waits for navigation back to baseURL).


T3.0 Shared client store

  • Wave: 3
  • Depends on: T2.4
  • Size: M
  • Design: state-machine.md (client state machine)
  • Touches: client.js, index.d.ts, index.js, test/client-store.test.js (new)
  • Why: three components each fetch the same payload and each invent their own error handling. One store owns the machine; components render it.

Steps

  1. client.js: export createRedlineStore({ serviceUrl, siteId, documentPath, fetch }) returning an object with getters status, payload, error, updatedAt; load() (dedupes an in-flight load, applies the transitions, never starts a new load within 5 seconds of the previous start unless load({ force: true })); subscribe(listener) (called immediately with the current snapshot and on every change; returns an unsubscribe function); connectUrl(returnTo); message() returning the string for the current status via REDLINE_MESSAGES.
  2. Export getRedlineStore(options) memoised on globalThis.__redlineStores by ${serviceUrl}|${siteId}|${documentPath}.
  3. The store requests /v1/sites/<siteId>/documents/state?path=<documentPath> with credentials: 'include', Accept: application/json, no custom headers (keeps the request CORS-simple).

Acceptance

test/client-store.test.js with a fake fetch:

  • store moves static to probing to live
  • store derives connect-required from the payload
  • store classifies a network failure as offline-network and keeps the previous payload
  • store classifies gitlab-unreachable as offline-gitlab
  • store dedupes concurrent loads
  • store throttles reloads to five seconds unless forced
  • store notifies subscribers on every transition
npm test 2>&1 | grep -c "^✔ store"   # prints 7

T3.2 Remove browser dialogs

  • Wave: 3
  • Depends on: none (listed in wave 3 to batch with component work)
  • Size: S
  • Touches: GitComments.astro, test/components.test.js (new)
  • Why: window.prompt and window.confirm block the page and cannot be driven reliably by automation. Replies and deletes become inline controls.

Steps

  1. Reply: each thread gets a hidden <form data-reply-form> with a textarea and a submit button; the Reply button toggles it. Submitting posts the reply and hides the form.
  2. Delete: the Delete button becomes a two-step control. First click sets data-confirm="true" and changes its text to Confirm delete; second click deletes; any other click in the thread resets it.
  3. No window.prompt, window.confirm or alert( anywhere in the three components.

Acceptance

test/components.test.js:

  • components use no blocking browser dialogs (reads the three .astro sources; asserts none contains window.prompt, window.confirm, confirm(, alert()
  • components compile (reuse scripts/check-components.js logic)
npm test 2>&1 | grep -c "^✔ components"   # prints 2

T3.1 Reachability states in components

  • Wave: 4
  • Depends on: T3.0, T0.3, T0.5, T3.2
  • Size: L
  • Design: state-machine.md, network.md
  • Touches: GitHistoryFooter.astro, GitTrackChanges.astro, GitComments.astro, test/e2e/reachability.spec.js (new), test/e2e/static-fallback.spec.js (new), test/e2e/helpers.js (new)
  • Why: the reader must be told which edge failed and what they can do.

Steps

  1. Every component’s <script> imports getRedlineStore and REDLINE_MESSAGES from ./client.js (relative; the file sits beside the component in the published package) and subscribes to the store built from its data-service-url, data-site-id, data-document-path.
  2. Each component sets data-redline-state="<status>" on its root element on every snapshot, and renders store.message() in its status element: footer [data-history-status] > span:last-child, track changes [data-track-status], comments [data-comment-status].
  3. The footer calls store.load() when opened and on its poll interval; the other two call it on mount. The footer’s existing renderLiveState runs on live and connect-required; on any failure the build-time markup is left as it is (it already is).
  4. The comments component shows [data-gitlab-connect] only in connect-required, with href = store.connectUrl(window.location.href).
  5. Remove each component’s own fetch code.

Acceptance

test/e2e/reachability.spec.js:

  • shows the private-network message when the service is unreachable (page.route('http://localhost:8787/**', r => r.abort('connectionrefused')); open footer; expect #history to have data-redline-state="offline-network" and to contain REDLINE_MESSAGES['offline-network']; same for [data-git-comments])
  • shows the gitlab-unavailable message when gitlab is down (setMockDown(request, true); reload; expect offline-gitlab state and message on all three roots)
  • shows the connect prompt when comments need a gitlab session (default; expect [data-git-comments] state connect-required, link visible with href starting http://localhost:8787/v1/auth/gitlab/start?returnTo=)
  • recovers to live when gitlab comes back (down → reload → up → click [data-track-refresh] → expect live)

test/e2e/static-fallback.spec.js:

  • renders build-time history without the service (abort service routes; expect the footer summary [data-summary-meta] to match /\d+ revisions?/ and the history tab to list at least one commit row before and after opening)
npx playwright test test/e2e/reachability.spec.js test/e2e/static-fallback.spec.js 2>&1 | tail -3   # "5 passed"
npm run test:e2e 2>&1 | tail -3   # "6 passed" (health + 5)

T3.3 OAuth connect journey

  • Wave: 5
  • Depends on: T3.1, T0.4
  • Size: S
  • Touches: test/e2e/oauth.spec.js (new), test/e2e/helpers.js
  • Why: proves the identity edge end to end in a browser: site → service → GitLab authorize → service callback → site, with a session cookie that the next request carries.

Steps

  1. connectGitLab(page): click [data-gitlab-connect], await page.waitForURL(/localhost:4321/).
  2. Spec.

Acceptance

test/e2e/oauth.spec.js:

  • connects gitlab through oauth and shows the comment form (connect; expect [data-git-comments] state live, [data-comment-form] visible, and GET http://localhost:8787/v1/auth/gitlab/status via page.request (shares cookies) to return { connected: true, user: { username: 'test' } })
  • logout clears the session (page.request.post('http://localhost:8787/v1/auth/gitlab/logout'); reload; expect connect-required)
npx playwright test test/e2e/oauth.spec.js 2>&1 | tail -3   # "2 passed"
npm run test:e2e 2>&1 | tail -3                             # all passed; the total depends on which wave-5 tasks are done

T3.5 Inline redline

  • Wave: 5
  • Depends on: T3.0, T2.2, T2.6, T0.3
  • Size: L
  • Design: state-machine.md, fixture-data.md
  • Touches: GitRedline.astro (new), client.js (applyInlineRedline, clearInlineRedline), index.js, index.d.ts, package.json (exports, files), test/fixtures/site/src/pages/index.astro (add the component), test/e2e/redline.spec.js (new), README.md (section “Inline redline”)
  • Why: capability 1 of the thesis.

Steps

  1. client.js: export applyInlineRedline(root, layer, { diffWords }) and clearInlineRedline(root):
    • replace: find [data-redline-block="<blockId>"] inside root; store its original child nodes in a WeakMap; set data-redline-change="replace"; replace its children with the segments of diffWords(stripMarker(before.content), stripMarker(after.content)) rendered as text nodes, <del> and <ins>. stripMarker removes a leading #{1,6} for headings and a leading list marker for list items; code blocks render as one <del> and one <ins> of the whole content.
    • delete: wrap the existing children in a <del>; set data-redline-change="delete".
    • insert: create the element type matching the block (h1…h6 by heading level, li, pre, else p) with an <ins> containing the content, attributes data-redline-block="<blockId>", data-redline-change="insert", data-redline-inserted="true"; insert it after [data-redline-block="<afterBlockId>"] or as the first child of root when afterBlockId is null.
    • Each decorated element also gets data-redline-layer="<layer.iid>" and dispatches redline:change as decorateDocumentChanges does today.
    • clearInlineRedline restores originals, removes inserted elements and the three attributes.
  2. GitRedline.astro (props serviceUrl, siteId, documentPath, contentSelector default [data-redline-document], title default Proposed changes): a toolbar listing one <button type="button" data-redline-layer-toggle="<iid>" aria-pressed="false">!<iid> · <title></button> per payload.changeLayers, a status line bound to the store, and data-redline-state. Pressing a button applies that layer inline and clears any other; pressing it again clears. Only one layer at a time.
  3. Fixture page: add <GitRedline …/> directly above the article.

Acceptance

test/e2e/redline.spec.js:

  • renders the proposal as inline tracked changes (press the !7 toggle; [data-redline-block="title:paragraph:paragraph"] has data-redline-change="replace", its del has text Revised, its ins has text Proposed, and its full text is RevisedProposed paragraph.)
  • clearing the layer restores the original text (press again; attribute gone; text Revised paragraph.)
  • only one layer is inline at a time (with one layer in the fixture this asserts the toggle’s aria-pressed flips and no element has data-redline-inserted after clearing)
npx playwright test test/e2e/redline.spec.js 2>&1 | tail -3   # "3 passed"
npm run test:e2e 2>&1 | tail -3                               # all passed

T3.6 Changed since

  • Wave: 5
  • Depends on: T3.0, T2.2, T2.3, T0.3
  • Size: M
  • Design: fixture-data.md (blocks changed since)
  • Touches: GitChangedSince.astro (new), client.js (markChangedSince), index.js, index.d.ts, package.json, test/fixtures/site/src/pages/index.astro, test/e2e/changed-since.spec.js (new), test/client-state.test.js (unit for markChangedSince), README.md (section “Changed since”)
  • Why: capability 2 of the thesis.

Steps

  1. client.js: export changedBlocksSince(history, sha) returning the set of block ids with any blockHistory entry whose commit appears before sha in history.commits (newest first); sha === head gives an empty set; an unknown sha gives every block with history. Export markChangedSince(root, blockIds) that sets data-redline-changed-since="true" on matching [data-redline-block] elements and clears it elsewhere; returns the count marked.
  2. GitChangedSince.astro (same props as GitRedline plus storageKey default redline:last-seen): a <select data-since> with one option per commit (<shortSha> · <date> · <title>, value sha), an option Last visit (value last-visit, present only when localStorage[storageKey:<siteId>:<documentPath>] holds a sha that is in the list), and a <p data-since-status>. On change compute and mark, and set the status to <N> blocks changed since <shortSha> or No blocks changed since <shortSha>. On live, if a last-visit sha exists select it automatically; then store the current head sha as the last-visit value (wrap storage access in try/catch).
  3. Fixture page: add <GitChangedSince …/> beside GitRedline.

Acceptance

Unit (test/client-state.test.js):

  • changed blocks since a revision follow the fixture counts (since c1 → 3, c2 → 2, c3 → 0 using the expected blockHistory)

test/e2e/changed-since.spec.js:

  • marks blocks changed since a chosen revision (select aaaa…; expect 3 elements with data-redline-changed-since="true" and status 3 blocks changed since aaaaaaaa; select cccc…; expect 0 and No blocks changed since cccccccc)
  • remembers the last visit (first load seeds storage; set storage to c2 via page.evaluate; reload; expect the Last visit option selected and 2 marked)
npx playwright test test/e2e/changed-since.spec.js 2>&1 | tail -3   # "2 passed"
npm run test:e2e 2>&1 | tail -3                                     # all passed

T3.4 Anchored comments journey

  • Wave: 6
  • Depends on: T3.3, T3.2, T2.2, T3.1
  • Size: M
  • Touches: GitComments.astro, test/e2e/comments.spec.js (new)
  • Why: capability 3 of the thesis, end to end: select, comment, attributed, anchored, reply, resolve, delete, all through GitLab.
  • Interim store allowed: if GitLab’s discussion model makes anchored comments unreliable (for example the anchor marker in the note body is mangled, or issue search is too slow to bind a comment to its document), the executor may add a small interim store in the service (SQLite file or JSON under a mounted volume) that holds the anchor and the GitLab note id, with GitLab still holding the body and author. A task T3.7 propagate interim comment records must then be added to B3 and STATUS.md with its own acceptance test proving every interim record reaches GitLab. The Playwright assertions on the mock state stay as written.

Steps

  1. GitComments.astro: remove the fallback that invents dom:<tag>:<n> ids. Blocks without data-redline-block produce a document-level anchor ({ selectedText } only).
  2. Highlighting must set data-redline-comment="<discussionId>" on the anchored block in addition to the CSS highlight, so tests can assert it.
  3. Spec.

Acceptance

test/e2e/comments.spec.js (each test connects GitLab first):

  • creates an anchored comment attributed to the gitlab user (selectText(page, 'title:paragraph:paragraph', 'Revised'); expect [data-selection-label] to contain Commenting on: “Revised”; fill the textarea with Needs a source.; submit; expect one .agc__thread containing Test User and Needs a source.; expect [data-redline-block="title:paragraph:paragraph"] to have data-redline-comment; mockState(request) shows one issue titled [Document Review] src/content/doc.md and one discussion whose first note body contains <!-- redline-anchor: and decodes to an anchor with blockId: 'title:paragraph:paragraph', startOffset: 0, endOffset: 7)
  • replies resolves and deletes without dialogs (reply Added. → thread shows two notes; Resolve → thread has class agc__thread--resolved; Delete, Confirm delete → no threads; mock state has no discussions)
  • a document-level comment works without a selection (clear selection; submit General note.; thread appears; no element has data-redline-comment)
npx playwright test test/e2e/comments.spec.js 2>&1 | tail -3   # "3 passed"
npm run test:e2e 2>&1 | tail -3                                # "16 passed"
Git history

Loading the page's history…