Date: 2026-05-01
Branch: alexlazarian/JUS-1204-followup-select
Scope: Confirm the TinyMCE → Tiptap migration introduces no user-visible regressions across the Select domain (15 editor surfaces, the shared message-form gem, the bidder text-answer flow, three Rails apps, and three email templates).
The plan is split into two phases. Before merge is everything a person on a dev laptop with the local stack running can confirm (build, click-through, dev mail-catcher). After deploy is everything that needs the staging environment, real mail clients, real production-shape data, or a longer soak window.
Tick a checkbox only when you have personally observed the expected behaviour or output. If a check is non-applicable (e.g. you have no permissions on a particular surface), strike it through with a note rather than ticking it.
The two items below are intentional outcomes of the migration and not bugs. Don't open tickets against them while running this checklist — they are tracked as a post-migration follow-up.
- Tables lose visual styling outside the React app. Tables you create in the new editor still preserve their structure and content. But on server-rendered pages (the bidder workflow Slim views) and inside emails, they may render without borders, padding, column widths, or background fills — because the styling is class-based and those rendering contexts don't load the matching CSS. As long as a table is structurally intact (rows, columns, cell text), this is acceptable for now.
- Underline is now a styled span, not a
<u>tag. Visually identical in every browser and mail client. If you View Source on saved content, expect to see<span style="text-decoration: underline">…</span>instead of<u>…</u>. This was deliberate — the old<u>tag was being stripped by backend sanitizers, so underline was silently failing on save in some surfaces before the migration.
Run on a local dev environment with the full Select stack (Bid, Launchpad, Messaging, Postgres, MySQL, Redis, the dev mail-catcher) up. CI must also be green before this phase is complete.
- Fresh checkout of the branch, run the workspace install — completes without warnings about missing peer dependencies for any of the three Select apps or the messaging gem.
- Bid dev server starts without errors and serves the home page. Open the dev URL, watch the terminal, then load any page that shows the negotiation setup tabs.
- Launchpad dev server starts without errors. Same — watch the boot log, then open the platform admin landing page.
- Messaging dev server starts without errors. Same — load the messaging inbox.
- Bid production-style asset build completes. Run the build command for the Bid app and confirm no error referencing TinyMCE, vite-plugin missing modules, or empty public asset paths.
- Launchpad production-style asset build completes.
- Messaging production-style asset build completes.
- The shared design-system package's component tests all pass. The rich-text editor test suite specifically should report 21 passing tests including the underline-as-span variants.
- CI pipeline on the PR is fully green. Check the PR's checks tab — biome, typecheck, vite builds, RSpec, Storybook build, and the Docker image jobs for all three apps must succeed.
For every surface below, open the page, focus the editor, type a few words, and confirm the browser console shows no red errors.
- FAQ topic content (Launchpad — FAQ manage page → click into a topic content card)
- AI Prompt system instructions (Launchpad — AI prompts page → open or create a prompt)
- Organization description (Launchpad — open an organization → General Info tab)
- Attorney overview (Launchpad — organization → attorneys → new or edit attorney)
- Attorney pro bono (same form, different field)
- Notable accomplishments — at least one item (same form, expandable list)
- Notable cases — at least one item (same form)
- Question title (Bid — Information Request setup → open a question)
- Question body (same surface, larger field)
- Question body in the wizard variant (Bid — setup wizard → questions step)
- Scorecard item description (Bid — Evaluation Guide setup → click into a scorecard item)
- Negotiation Summary body (Bid — General Settings → Summary tab)
- Invitation HTML (Bid — Participation tab → Invitation editor)
- Award email body (Bid — Award setup → Award email tab)
- Unsuccessful email body (Bid — Award setup → Unsuccessful email tab)
- Bidder text-answer (Bid — log in as a bidder, open a text-type question, click into the answer card)
- New-message body in the messaging app (Messaging — compose a new message)
- Reply body in the messaging app (Messaging — open an existing thread, focus the reply composer)
- New-message body via the messaging gem inside Bid (Bid — open the side messaging panel and start a new message)
For each of the three surface "weight classes" pick one representative surface and exercise every formatting control the user used to have. The expectation: type → format → save → fully reload the page → the formatting is still visually present, and clicking back into the editor shows it as still active in the toolbar.
Light surface — pick FAQ topic content:
- Bold, italic, underline, strikethrough each survive a save+reload.
- Bullet list survives.
- Numbered list survives.
- A link with custom URL survives — clicking it opens in a new tab and the URL is correct.
- Underline specifically: after save, View Source / inspect the saved value — confirm it's a styled span and not the
<u>tag.
Heavy surface — pick Question body or Negotiation Summary body:
- Bold, italic, underline, strikethrough survive.
- Bullet list and numbered list survive, including nested levels.
- Link, text alignment (left, center, right), text color, and highlight color all survive.
- Subscript / superscript survive (where the toolbar offers them).
- A 3-by-3 table inserts, accepts text in cells, and survives save+reload — content and structure intact even if borders look minimal.
- Headings (H1, H2, H3) survive on surfaces that accept them. (Surfaces with narrow sanitizers will silently downgrade headings to paragraph text on save — that's not a regression, it's the same allow-list as before.)
Email-template surface — pick Invitation HTML:
- Bold, italic, color, alignment, lists, and links survive a save+reload.
- Pasting from an external source (e.g. a Word doc or a previous email body) does not produce visible HTML escape sequences (
<,&, etc.) inside the editor or on save. - Liquid-style placeholders the user might type — e.g.
{{ company_name }}or{% if x %}— round-trip byte-identical through save+reload, with no curly-brace escaping.
Pick at least one record per surface kind that was created before the migration (i.e. saved by the old TinyMCE editor) and confirm it still loads and re-edits cleanly. The goal is to catch any silent stripping or corruption when old HTML hits the new editor's parser.
- An old FAQ topic with bold / italic / lists / links loads, displays correctly, and re-saves identically (no diff in the database except the underline tag swap if any underline existed).
- An old organization description with formatted content loads and re-saves cleanly.
- An old attorney overview with notable records loads and re-saves cleanly. Confirm any pre-existing
<u>underline now displays correctly via the new span pathway. - An old AI prompt's system instructions load and re-save without escaping any Liquid placeholders.
- An old question body that contains a table loads — table structure intact, cells editable.
- An old negotiation summary loads — including any inline color/alignment styling from the previous editor.
- An old invitation, award email, and unsuccessful email template each load with their content visible, and re-save cleanly.
- An old in-app message thread renders correctly: bold, color, and any image/file placeholders preserved on the in-thread reading view.
These surfaces show a read-only card by default and enter edit mode on click; on blur with no validation error, they fade back to the read-only card and trigger autosave. Expected: smooth transitions, no flash of unstyled content, no autosave on initial mount, autosave fires once after blur with the latest content.
- FAQ topic content: card → click → editor focused → type → click outside → card fades back showing the new content → autosave success indicator fires once.
- Question title: same flow.
- Question body: same flow.
- Scorecard item description: same flow.
- None of these surfaces fire an autosave on first page load. Open the network panel, load a page with one of these surfaces, confirm no save POST until you actually type and blur.
The email surfaces have a "Reset email" button that previously called into TinyMCE imperatively. The new behaviour drives the editor via prop sync — the click should still produce a visible content swap.
- Invitation tab: edit the body → click "Reset email" → editor visibly updates to the template's default text without requiring a manual page reload, no flash of the previous edited content lingering.
- Award email tab: same flow.
- Unsuccessful email tab: same flow.
- After a reset, clicking "Send test email" sends the reset content (not the previous edits) — verifiable in the dev mail-catcher.
For the surfaces that send mail, send a test message containing a paragraph, a list, bold + italic + colored text, a link, and an inline image (where the editor allows). Open the resulting email in the dev mail-catcher.
- Invitation send-test arrives in the mail-catcher — body is visible and styled, link clickable.
- Award email send-test arrives — same.
- Unsuccessful email send-test arrives — same.
- Messaging new-message email arrives — body visible and styled.
- Tables in the email body render with their cell content visible — borders may be missing but text and structure are preserved (acceptable per the known regression note above).
- No raw HTML strings (e.g.
<p>,<span>) are visible as literal text in the rendered email body.
This was the riskiest single migration because the host is jQuery/CoffeeScript and the editor is bridged in via a small adapter module. Expected behaviour: clicking the answer card mounts the editor, typing updates the character counter live, blur triggers a save, save success unmounts the editor and shows the formatted answer in the read-only card.
- Log in as a bidder on a negotiation that has at least one text-type question.
- Click into the answer card — editor mounts, focus lands inside it, no console errors, the read-only card hides cleanly.
- Type — the character count under the answer updates as you type.
- Blur — autosave triggers, success toast appears, editor unmounts, the formatted answer text is visible in the read-only card.
- Reload the page — the saved answer is still there.
- Click again into the same answer — editor mounts again, original content loads correctly, can be edited.
- Cancel save (if your test data triggers a validation error) — the editor unmounts and the previous saved content is restored without a partial-edit ghost.
This is the only surface that mounts many independent editor instances on one page (one per notable accomplishment / case). The pre-migration ceiling was effectively unlimited because TinyMCE used a lighter inline mode; Tiptap's per-editor cost is higher. Test at realistic scale.
- Open or create an attorney record with at least 20 notable records distributed across accomplishments and cases.
- Page first paint takes under 200ms after the form data finishes loading. Use the browser performance panel — measure from the network response of the form GET to first interactive paint.
- Scrolling the form is smooth (no visible jank).
- Typing into one editor does not cause perceptible lag in the others.
- Saving the form persists every notable record correctly with its formatting intact.
Run after the branch is merged and deployed to a staging environment that mirrors production data. Some checks here require real mail clients, longer-running soak windows, or production-shape data volumes that aren't available locally.
- Bid app loads its negotiation setup tabs in staging — no editor surface throws an uncaught error in the browser console on first load.
- Launchpad loads its admin tabs in staging — same.
- Messaging loads the inbox and a representative thread in staging — same.
- Browser console shows no warnings about missing CSS files, missing JS chunks, or 404s from removed legacy editor assets.
- No 5xx errors related to editor surfaces appear in the application logs for at least 30 minutes after deploy.
The dev mail-catcher is forgiving. Real mail clients are not — Outlook in particular is a strict renderer that drops or rewrites a lot of CSS. Send one test of each email-bound surface to a real inbox and inspect the result.
- Send a test invitation to a real Gmail inbox — body is visible, links work, formatting is recognizable. Tables, if any, have their content visible (borders may be absent — acceptable).
- Send the same to an Outlook inbox — same expectations.
- Send a test award email to Gmail — same.
- Send a test unsuccessful email to Gmail — same.
- Send a messaging-app message to a recipient who reads via email — the email arrives styled and readable.
- No raw HTML is visible in any rendering.
A few Bid pages render persisted HTML directly through Rails templates rather than through the React app. These don't load the design-system stylesheet, so any class-based styling the editor adds is lost there — but inline-style content should still display correctly.
- Bidder workflow → invitation page renders the saved invitation HTML with text formatting (color, alignment, lists) preserved. Tables, if present, may render plain — acceptable.
- Bidder workflow → summary page renders the saved summary HTML similarly.
- Bidder workflow → text-answer view (read-only) renders saved answers with formatting preserved.
- None of these pages produce visible HTML escape sequences in the body text.
Staging usually has more historical data than dev. Open a sampling of records — pick 3 per surface kind from across different organizations and date ranges — and confirm they still render and re-edit cleanly.
- FAQ topics: 3 random records, each loads and re-saves identically.
- Organization descriptions: 3 random records.
- Attorney overviews + notable records: 3 random attorneys.
- AI prompts: at least one prompt from each tenant that has them.
- Question titles + bodies: 3 random questions across at least 2 negotiations.
- Negotiation summaries: 3 random summaries.
- Invitation / Award / Unsuccessful templates: one of each from at least 2 negotiations.
- In-app message threads: read at least 5 historical messages — formatting renders, no broken layout in the thread view.
- No new Sentry issues mentioning rich-text-editor, Tiptap, ProseMirror, or any of the migrated surface paths in the 24 hours after staging deploy.
- No customer support tickets referencing missing formatting, broken editors, or save failures in 48 hours.
- One round of QA-by-feel: a non-engineer (PM or QA) clicks through the most-used surfaces (Question body, Org description, Messaging composer, Invitation editor) and reports no surprises.
- An attorney form with the largest historical Notable Records count in production data loads in under 500ms TTI on a representative laptop. If staging has a record exceeding 30+ notable items, use that one.
- Sentry / APM dashboard shows no regression in front-end interaction latency for any of the editor pages over the 48h post-deploy window.
- Confirm the rollback path is understood. Reverting any of the four migration commits restores the matching surface or the cleanup; reverting the cleanup commit alone is safe (the surfaces stay on the new editor).
- Confirm a Sentry alert exists, or is being created, for save failures on any of these surfaces, so a regression is detected within minutes rather than days.
When every box on both phases is ticked (or explicitly struck through with a justified note), the migration is verified.