A rich text editor for the markup language - #31
Merged
Conversation
Groundwork for a WYSIWYG editor. An editor parses on open and serializes on
save, so every stored document eventually makes the round trip -- but there was
no serializer at all, and no way to write a literal asterisk.
Add one, plus a backslash escape. A backslash before anything else stays a
literal backslash, so `C:\shared` and `match \d+ digits` keep their meaning and
no migration is needed.
A generated property test over 2000 trees covers the whole grammar with
adversarial content and asserts that serializing and reparsing is lossless and
idempotent. It caught a bare email address swallowing the word after it.
Fix three ways the parser lost data, each now tested. An unclosed quote lost its
last character to slice(1, -1). A URL containing a second @ lost everything
after it to split('@'). Headings kept the space after the hashes in their text.
Add Reference as its own segment, for `<text@role>` targets that aren't URLs.
It serializes byte for byte as before, so nothing stored changes. Resolving it
against the organization moves to ReferenceView, which lets LinkView drop its
getOrg() dependency -- the reason MarkupView only worked inside an org route. An
unresolvable reference now says so instead of rendering a dead link.
Record each block's source span, so the editor can eventually rewrite only the
blocks someone edited and leave the rest of the source byte for byte.
Along the way: the kind emoji was in the accessible name of every link, so
VoiceOver read role links as "gear Registrar, link"; and the combobox pointed
aria-controls at itself rather than at its list of options.
Add `npm run audit:markup` to measure what reserializing would do to stored
markup before any of this is turned on. It is read-only and skipped unless
pointed at a database. It checks its own detector against known cases first: a
corpus of plain prose reports that nothing would change whether the comparison
works or not, so it also reports how much was even in scope.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first third of the editor, and the part that can be proven without a browser. Rendering is imperative rather than the Svelte components that render markup for reading. Svelte's reconciler cannot own DOM the browser is also mutating: the moment an each block reruns over the paragraph someone is typing in, their cursor is gone. Reading goes back through the markup serializer rather than writing source itself, so there is exactly one implementation of escaping. It copes with what browsers produce as well as what the renderer produces -- <b> for <strong>, a text node split in three by a keystroke, a stray <span style> from a paste -- and drops anything the grammar cannot express instead of smuggling it through to be lost at save time. The caret is held as a character offset into a line, not as a node and offset. A saved node is stale as soon as normalizing the block replaces it; an offset describes the text rather than the nodes the text happens to live in. A pill counts as one character, which is what makes a reference take one step of arrow travel and one press of backspace without any of that being written by hand. Rendering and reading are held to being inverses over 1000 generated trees, rather than by comparing HTML -- what has to match is the markup that comes back out, not the tags. Two bugs came out of that: adjacent bold runs merged into one, rewriting `*a**b*` as `*ab*`, and a formatted run split by typing came back as several segments instead of one. Add happy-dom, scoped to these two files. They are near pure tree walks, and they are where corruption bugs live. Selection and composition are deliberately not tested here -- happy-dom stubs enough of them to prove things that aren't true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bold, Enter, Backspace and block type changes are pure functions from a document and a caret to a document and a caret. The editor re-renders the block it changed rather than performing surgery on live nodes. Range surgery would avoid the re-render, but a command is something someone did deliberately, not something that happens per keystroke, and typing itself never comes through here. What this buys is that the fiddliest part of an editor -- where the caret ends up after Enter on an empty list item, what Backspace does at the top of a list -- can be tested without a browser, a selection, or a mock. The cases that eat 40% of an editor's bug budget are 18 tests here instead. Offsets count what a reader counts: a character is one and a whole pill is one, so a reference takes a single step of travel and a single backspace. Slicing keeps a pill only when the range covers all of it, because half a reference is not something the grammar can express. Undo is owned outright. Canceling a beforeinput takes that operation out of the browser's stack, so a document edited partly by the browser and partly by us would undo incoherently -- three characters, then a jump past a bold toggle to a state from a minute ago. Entries are whole documents: these are comments and role descriptions, so snapshotting is affordable and correct, where an inverse operation stack is where weeks go. Typing coalesces into one step per burst; commands are always their own step and close the burst before them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wires the pieces together into something someone can type in: a toolbar, the shortcuts, paste, a live region, and a toggle back to markup source. The arrangement is that the browser owns the DOM while someone is typing, the DOM is read back afterwards, and the source is derived from that. Typing is never intercepted and never causes a re-render, which is what keeps the caret, a screen reader's cursor, a braille cursor and an input method's own buffer where they were. Only deliberate commands rebuild the block they changed. Notably not done: cancelling every input and re-rendering, the way a strictly controlled editor would. Composition cannot be cancelled at all, so that arrangement is a fiction during exactly the input it claims to control, and every avoidable rebuild near the caret costs a screen reader user their place. The editable region carries no role on purpose. A textbox role is a leaf, so screen readers stop exposing the headings, lists and quotes inside it to structural navigation, which is most of what the grammar is for. Only the blocks that changed are rewritten. Someone who fixed a typo did not ask for the other forty lines to be normalized, and a diff that says they did is one nobody can review. Ctrl+Alt is AltGr on many European layouts, where it is how an @ is typed, so heading shortcuts stand aside when that modifier is set. A toolbar is one tab stop, not one per button: on a process page with a dozen editable steps that is twelve tab presses instead of a hundred and twenty. Two call sites wrapped the editor in a Labeled, which only names labelable elements -- so a rich text editor had no accessible name at all. Labeled can now name something by id instead. Driving a real browser found three things unit tests could not. Splicing dropped the blank lines between blocks, closing up deliberate spacing elsewhere in a document. Without pre-wrap the browser ate a leading space, so Enter mid-sentence lost it. And browsers substitute non-breaking spaces wherever an ordinary one would collapse, which nobody typed and the grammar cannot tell apart. Those tests mount the editor on a bare element against the dev server, so they run without a signed in account. The Playwright config now runs the application against a production build and the editor against dev, and npm test includes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The help text under the editor named Alt+F10 for the toolbar and Ctrl+Shift+M for markup source. Neither existed. Ctrl+Shift+M is worth having, so it now works. The host does not own the mode, so it calls back to the component that does. Alt+F10 is dropped. It is the ARIA practices convention, and TinyMCE and CKEditor both use it, but the reason it exists does not apply here: those editors capture Tab for indentation, which strands the toolbar outside ordinary navigation. This grammar has no nesting, so Tab keeps its meaning, and the toolbar sits ahead of the editor with a single tab stop. Shift+tab already reaches it in one keystroke, so Alt+F10 would only be a second and far less discoverable route to the same place. The help text says shift+tab instead. Escape is dropped too, before it was written. The plan had it leave editing only when nothing had changed, which is unpredictable in exactly the situation that matters -- it would look broken whenever there was unsaved work. There is no discard in MarkupView for it to mean, and Escape already closes a Dialog, so binding it here would make one key mean two things depending on state nobody can see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A selection spanning blocks is the browser's to put on the clipboard, so what comes back on paste is the editor's own HTML rather than markup. The paste walker knew about <strong> and <a> but not about the editor's own pills, so copying a paragraph turned every reference in it into plain words. Bold survived; references did not. Found by driving a real browser rather than by reasoning about it. The rest of what was assumed to be broken turns out to work: cut across paragraphs merges the remaining halves correctly, and copy and paste across paragraphs round trips through the HTML path intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copying a phrase wrote markup to the clipboard while copying across paragraphs left the browser to write rendered text, so what an external paste got depended on how much had been selected. Pasting back into Adminima was unaffected either way, since the HTML the browser writes carries formatting and references through. This markup is not meant to travel, so the rendered text is the more useful thing to hand to Slack or an email, and now both cases hand over the same. The special case is gone rather than extended: writing markup across blocks would have meant cancelling the cut and reimplementing deletion, replacing browser behavior that turns out to be correct. Removing it exposed a test that had been passing for the wrong reason. Selecting "the reference" used raw text offsets, which picked out the first letter of its label; the old code mapped that to the whole pill on its way to the clipboard, so the test passed without ever selecting what it claimed to. It now selects the reference and the words on either side, and the reference survives because the paste walker understands pills, which is where that belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Enter split at the caret and ignored what was selected, so pressing it with text highlighted left that text sitting beside the break instead of replacing it. Across blocks it was worse: selecting the end of one paragraph and the start of the next and pressing Enter kept both in full and added a third. Backspace was already right, because a selection is left to the browser and the browser does it correctly. Only Enter was intercepted unconditionally. Fixing it needed a deletion that spans blocks, which nothing had needed before. What is left of the two ends becomes one line belonging to the block the selection started in, and anything below the selection in the last block stays the kind of block it was. The rest of this is coverage. Enter and Backspace at every boundary the grammar has -- both ends of a heading, of a quoted line, of a list item, of a paragraph against each of those -- is 48 cases as a table, because the commands are transforms on a document rather than surgery on nodes. That was most of the reason for writing them that way. Two of those cases were written wrong before they were right, both because the parser trims a line before deciding what it is: "- " with nothing after it is not an empty list item, it is a paragraph containing a hyphen. Shapes the parser cannot produce from source are now built directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The toolbar only reported bold and italic for a selection, so putting the cursor inside a bold word showed bold as off. Sighted people can see the word is bold and ignore the toolbar; someone reading it through a screen reader is told the opposite of what is true. It now reads the character before the caret, or the one after it at the start of a line, which is the usual convention. Block kind and selections were already tracked correctly. These are checked by mounting the real component and reading the attributes it renders, rather than by checking the state that feeds it and trusting the rest to follow -- which is how the last few assumptions here turned out to be wrong. That confirms what had only been reasoning: aria-pressed carries the state, undo and redo carry none because they are not toggles, the ten buttons share a single tab stop, and the arrow keys move along the row and wrap. A test found a test bug first: window.status is a property Window already has, so an object stored there never survived to be read back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Command+K opens a dialog offering the organization's roles and processes, or a web address, since `<name@target>` says both the same way. Selected words become the label; choosing something with nothing selected takes its name. A reference the caret is already on opens the dialog on itself and is replaced in place, and can be removed while keeping its words -- which was otherwise a thing with no way back out of it. A dialog rather than an inline trigger on an @ key. It moves focus somewhere real, Escape closes it, and it behaves the same with a screen reader as without, where an inline combobox would mean putting a combobox role on the editable region and taking away the structural navigation that is most of what the grammar is for. Opening it takes the selection away with the focus, so where the result goes is remembered when the shortcut is pressed rather than looked up afterwards. The rule for matching a reference to a role or process was written twice, in the view that resolves one and now in the picker that offers one. They have to agree or the picker offers references that render as unknown, so it is written once and tested, including that what the picker offers is what the view resolves. Two harness faults surfaced on the way, both mine rather than the editor's. Placing a caret inside a reference does not focus the editor the way clicking does, so the keyboard went to the document instead. And the page the tests borrow has an error region above where they mount. Clicking a reference does focus the editor, which is now checked rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pressing bold with nothing selected did nothing but explain itself, while the toolbar showed bold as on because the caret was sitting in a bold word. Now it means what it means everywhere else: the next thing typed comes out bold. It works in both directions, so turning bold off at the end of a bold run carries on plainly. Only the first keystroke after choosing is intercepted. It lands inside what it makes, so the browser can have the rest of the word back and the caret, the screen reader's cursor and the input method are left alone for all of it. That keystroke joins the same undo step as the word it begins rather than being a step of its own. The choice is remembered against the position it was made at, so moving the caret abandons it rather than surprising someone later, and composition drops it outright -- composition cannot be intercepted, and plain text under a toolbar insisting it is bold would be worse than admitting it. Writing the tests turned up a worse bug than the one they were for: an empty document rendered no paragraph at all. The browser would put what was typed straight into the editable element, where reading back does not look, so a new comment would take text and save none of it. Empty documents now have a paragraph to type into, and keep one when the last of their text is deleted. The state assertions now poll. A selection change reaches the host through an event, so reading straight after moving the caret raced it -- and every state assertion written so far had that race, passing only because it usually won. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Composition is the part of an editor that cannot be reasoned about safely: the events cannot be cancelled, and anything that touches the DOM while one is running desynchronizes the keyboard's own buffer. Driving it through the devtools protocol is the only automated coverage there is -- composing mid paragraph, beside a reference, inside formatted text, into an empty document, and with formatting chosen for what comes next. It found that composed text could not be undone at all. Composition is never intercepted and the input events it raises are all skipped, so nothing ever recorded the state before it. That is most of what typing is for anyone using an input method, and everything typed on Android. Chromium only, so Safari and Android are still hand testing. Two more bugs came out of running axe and mounting the real component: aria-multiline is not a global attribute, and the editable region has no role to take it -- deliberately, since a textbox role is a leaf and would hide the headings and lists inside it from structural navigation. Switching to the source view threw every time. Svelte clears a bound element to null rather than undefined, so the check guarding it passed null through and an editor was built around nothing. The picker also read the organization straight out of context, which throws anywhere there is not one. It now offers a web address alone rather than refusing to open. The help text is checked against what the editor does, both ways: every shortcut it names is driven, and every key it mentions must be one of them. It caught its first drift immediately -- the shortcut for the picker had been described as added and never was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It reported how many stored values would change if reserialized whole, which overstates the risk twice over. Saving rewrites only the blocks somebody edited, so what matters is how likely the one block being edited is to be rewritten -- and a document whose blank line spacing would change is not a document that loses anything. So it now separates the two: meaning lost, which has to be zero, from bytes changed, which is untidy rather than dangerous. And it counts blocks as well as values, which is the number that says what one edit costs. That number was silently zero. The tree and the spans came from two separate parses, so the map was keyed by blocks that were equal to the ones being looked up and not identical, and every lookup missed. It would have reported nothing to worry about against production for the same reason a broken smoke alarm reports no fire, so the counter now proves itself against known cases first, the way the other detectors here already did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test runner swallows console output when nothing fails, which is exactly when this runs. Written straight to stdout instead, so the numbers arrive without having to go looking in the file for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A direct connection host resolves to IPv6 only unless the project has the IPv4 add-on, so on a network without IPv6 it fails at name resolution before anything is attempted. The recovery guide gave that form in its example, which is the worst place for it: it is read while data is missing, by someone who then has a resolver error to interpret before they can start. It now gives the session pooler, says why the direct connection is not offered, and mentions that the pooler's username is postgres.<project-ref> rather than postgres -- which fails as a password error and sends you looking in the wrong place entirely. The connection code says the same thing at the moment it happens, rather than passing the resolver's error along on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Supabase signs its own certificates -- the pooler presents one from Supabase Intermediate 2021 CA, which no public trust store carries -- so every connection outside the local stack needs one passed in. The connection code has said so all along, and told people to use --ca. The audit had no way to accept it, so the advice was unanswerable. AUDIT_DB_CA now takes the path, and expands a leading ~, which is the one thing the connection code does not do for itself. Verification is not made optional to get around this, and the recovery guide now says why: a connection that cannot prove what it is talking to is not one to send a production password down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The session pooler's username is postgres.<project-ref>, which sits at the front of the connection string right where a database name sits at the end. Putting it in the wrong half fails as "database postgres.<ref> does not exist", which reads as a missing database and sends the search somewhere there is nothing to find. The connection now says what it actually is, and prints the string put back together the right way round. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Production found it. Whitespace just inside a formatting run was being moved outside it, and at the end of a line that put it somewhere parsing trims: `_Overview _` became `_Overview_ ` became `_Overview_`, and a space was gone. Real documents are full of that shape -- four of the first five findings were this and nothing else. Whitespace now stays where it was written. The one exception is a bold run opening a line with a space, which reads as a bullet, since `* ` is one. That space moves out, where the line trim takes it -- but a leading space at the start of a line was never going to survive the trim either way. A stray asterisk in the middle of a sentence now comes back exactly as it went in, apart from the closing marker the parser was already supplying for itself. The audit was asking the wrong question, which is why this looked worse than it was. It called any change of tree shape "meaning lost", so the normalization it was built to permit came back as four false alarms with the real one buried among them. It now separates words going missing, which must be zero, from a document that never stops changing, which must also be zero, from a tidier tree saying the same thing, which is neither. The shapes production turned up are kept as cases, so the distinction cannot quietly rot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three thousand values and five thousand blocks, 42% of them holding something the round trip could touch: no words dropped, nothing that fails to settle, and every tree preserved exactly. What changes is 7.7% of blocks getting tidied when somebody edits them, and none of it shows up on the page. Written down so a later run has something to be compared against. Moving any of the first three off zero is a regression whatever else changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Switching to the markup view threw and took the content with it. Svelte clears a bound element to null rather than undefined, and the check on the text area only looked for undefined. That is the same fault fixed on the element beside it two weeks ago; fixing one and not its sibling is why both are now typed as possibly null, so the compiler insists. Bolding a selection collapsed the caret to the start of it, putting it back before the words just formatted. The words now stay selected and face the way they were chosen, so a second command acts on the same text. Quotes did not look like quotes while being edited. Their appearance lived in QuoteView's scoped style, which cannot reach DOM the editor builds itself. It moves to the global styles both read from, rather than being written twice. The shortcuts move out of the footer and onto the buttons, where the tooltip writes them for the keyboard in front of the reader -- ⌘⇧8 on an Apple keyboard, Ctrl+Shift+8 elsewhere. They go in aria-keyshortcuts as well, which is where a screen reader is meant to find them, rather than in the accessible name, where they would be read out every time a button was passed. There is a button for the picker now, which had a shortcut and no way to be clicked. Undo and redo are ↺ and ↻. Headings are h1 and h2 rather than shouting. The drift guard moves with the shortcuts: it reads them off the buttons, checks both forms are present and that neither leaks into the name, and holds the keys the toolbar advertises against the ones the tests drive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The link button was taller than the others because its glyph came from an emoji font, whose em box is taller than the page's own. It is an @ now, which is both the same height and the character the grammar actually uses to write a reference. The footer is gone from the rich text view. It existed to name the shortcuts, and the buttons name their own, so all it did was get read out ahead of every visit to the field. The source view keeps its syntax note, which nothing else says. Rich text no longer claims to be described by anything. Making room for the toolbar shoves everything under it down the page. It slides in now, so where the space came from is visible rather than the text appearing to jump by itself -- unless the reader has asked for less of that, in which case it still just appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A two-way transition held the editor on the page while its exit played, and saving puts the rendered version up at that same moment -- so for a sixth of a second there were two copies of the same text, one above the other. Only the way in was ever wanted: seeing where the space came from as everything below moves down. Going the other way, the result should be there at once. Eased on the way in as well, which is the rest of what made it feel rough. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Symmetry, with the overlap that broke it last time handled properly rather than by taking one half away. The two-way transition holds the editor on the page while it plays, and saving puts the rendered version up at that same moment, so the same paragraph was briefly on screen twice. The rendered version now takes no height until the editor has finished going -- not animated itself, just waiting, which is all it needed to do. Both sides read the length of the wait from one place, since the whole thing only works while they agree on it, and both stay still for anyone who has asked for less movement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two states were both children of the row that holds the content and the edit button, so while one was leaving and the other had arrived, the row shared its width between them and the editor's text rewrapped at half size as it went. Taking up no height was not enough, because height is not what a row divides. They live in one column now, which is a single child of that row however many things are inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a rich text editor for the markup language, with the raw markup still a keystroke away.
What it does
Descriptions, comments and process steps are edited as formatted text rather than as source: a toolbar, a real caret, keyboard shortcuts for every command, and Command+K to insert a reference to a role or process. The markup source view is still there behind one button, and everything stored is still markup — no migration, no new column, no change to what a description is.
How it is built
The browser owns the DOM while somebody is typing. Typing is never intercepted and never triggers a re-render; the DOM is read back into a document afterwards and the source derived from that. This is what keeps the caret, a screen reader's cursor, a braille cursor and an input method's own buffer where they were. Only deliberate commands — bold, Enter, a block type change — go the other way and rebuild the block they changed.
Deliberately not a strictly controlled editor that cancels every input and re-renders. Composition cannot be cancelled at all, so that arrangement is a fiction during exactly the input it claims to control, and every avoidable rebuild near the caret costs a screen reader user their place.
Commands are transforms on a document, not surgery on live nodes. Where the caret ends up after Enter on an empty list item, what Backspace does at the top of a quote — the matrix that eats most of an editor's bug budget is 48 table-driven cases here rather than a browser suite.
Only the blocks that changed are rewritten. Someone who fixed a typo did not ask for the other forty lines to be normalized, and a diff that says they did is one nobody can review.
No third party editor. ProseMirror is the better engine and this grammar would use perhaps a fifth of it, while fighting its schema to express "lists cannot nest" and "marks cannot combine" — and the serializer, the toolbar, the ARIA and the reference node all still have to be written either way. The DOM layer sits behind one small interface, so swapping the engine later means writing one implementation of eight methods.
Grammar changes
The editor parses on open and serializes on save, so the grammar needed a serializer, which it had never had. It also gains a
\escape, which finally makes a literal asterisk typeable. A backslash before anything else stays a literal backslash, soC:\sharedandmatch \d+ digitskeep their meaning and nothing stored needs migrating.<name@role>becomes a first class node rather than a link resolved at render time. It serializes byte for byte as before.Three ways the parser lost data are fixed, each with a test: an unclosed quote lost its last character, a URL containing a second
@lost everything after it, and headings kept the space after the hashes in their text.Measured against production
npm run audit:markupran read-only against production over 3332 values and 5183 blocks, 42% of which hold something a round trip could touch:The 7.7% is the chance that the one block somebody edits gets tidied — a closing
*added to an unterminated run,*bullets becoming-. Nothing renders differently, because every tree is preserved exactly. Blocks nobody touches keep their bytes.That run found a real bug: whitespace just inside a formatting run was being moved outside it, and at the end of a line that put it where parsing trims it away.
_Overview _became_Overview_, and a space somebody typed was gone. Real documents are full of that shape and none of the 2000 generated test trees had it, because the generator avoided exactly the shape real writing is full of.What is tested
What is not tested
No screen reader has been near this. Everything in this branch about NVDA, JAWS, VoiceOver, focus mode and braille cursors is reasoning from specification rather than observation, and this branch's own history is a decent argument for the difference: composed text that could not be undone, an empty comment box that saved nothing, references quietly degrading on paste, Enter ignoring the selection — every one of those came from running something rather than thinking about it.
Rich text is the default for everyone as this stands. Manual passes across VoiceOver/Safari, NVDA/Firefox, NVDA/Chrome, JAWS/Chrome, iOS VoiceOver and Android TalkBack should happen before this ships, or the default should start as markup source and move once they have.
The three things most likely to be wrong, in order: Android TalkBack with GBoard, where every word is composed and the devtools protocol tests only reach desktop Chromium; whether a block command's re-render costs somebody their place in a virtual buffer; and whether arrow travel across a reference reads as the single object it is meant to be.
Also here
restore/'s recovery guide told people to use a direct database connection, which resolves to IPv6 only and fails at name resolution on a network without it — read while data is missing, by someone who then has a resolver error to interpret first. It now says to use the session pooler, why, and that its username is not what it looks like. The connection code says the same at the moment it happens.