Files
pbakaus_impeccable/tests/live-browser-source.test.mjs
T
5b5e487a4f Improve live mode configure bar and pick UX (#242)
* Fix: tear down annotation overlay when Escape exits live pick mode.

The configure prompt auto-focuses and bypasses the global Escape handler, so its local path must hide the annot overlay; togglePick off now does the same as a safety net.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Improve live mode steer pill typing affordance.

Show a visible caret and placeholder when focused, expand on pointerdown, and drop the muddy border so the graphite surface carries the affordance alone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Improve live mode configure bar layout and pill styling.

Align pills and input on a shared text track, refine muted pill chrome with a quiet action border, and center the row with symmetric inset so spacing reads evenly in the 36px bar.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add x1 to live mode variant count picker.

The configure bar count pill now cycles 1→2→3→4→1 so users can request a single variant.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Polish live mode configure bar, edit badge, and action picker.

Refine selection pill layout and tooltips, shrink edit copy to an icon aligned with the outline, right-align the action picker, and sync demo styles and regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix live mode element nav when configure input is focused.

Passthrough empty arrow keys from the configure and steer prompts so handleKeyDown can move between pickable elements without breaking autofocus typing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove accidental live.js inject from Base.astro.

Strip the localhost helper script tag left over from local live mode iteration so the PR ships only intentional UI changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix review findings: pick-cursor state sync, anchor recovery, e2e selectors.

Code review of this branch surfaced ten confirmed bugs plus three smaller
ones; this commit fixes all of them.

- Route every interaction-state transition through a new setLiveState()
  helper that re-syncs the pick-mode crosshair, fixing four confirmed
  cursor bugs: never appearing on pick toggle (sync ran before the state
  change), sticking through the configure phase, surviving teardown
  page-wide, and the style mounting inside the adapter's shadow root
  where it can't match the host document (now document.head).
- Anchor recovery: a matching id is decisive again (hashed class names
  and component tags broke recovery), empty-text elements can no longer
  match the fuzzy text passes (".includes('')" hole plus shortest-text
  preference), and the dead 2-class-subset fallback is removed.
- Selection pill: drop the hover-only "armed" guard so keyboard
  activation works; the pill arms on focus as well as hover.
- Configure chrome: remove the configure-bar tooltip on teardown, align
  restorePickerBarChrome padding with initBar (5px), share the
  configure-input stylesheet with the insert row, and sync the
  ui-core.mjs surface inventory with live-browser.js.
- Site demos: delete the stale duplicate .live-demo-ctx-selection rule
  that killed the teal pill on dark pages, and keep the configure-phase
  demo bar on the overlay's dark surface in light mode so the near-white
  prompt text stays readable.
- E2E/contract tests: match the icon-only submit button by aria-label
  ("Generate variants") instead of the removed "Go" text, and update
  source-contract pins for setLiveState and buildConfigureSubmitButton.

Verified: bun run test green, live-mode E2E 23/23 across all fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Wire insert voice button into syncVoiceUi listening state.

Voice on the insert configure row runs through the same 'configure' mode,
but syncVoiceUi only stamped data-listening/aria state on the replace
row's #impeccable-live-configure-voice, so the insert button never pulsed
while listening. Target whichever of the two row buttons is mounted, the
same either-row pattern syncConfigureInputChrome uses.

Addresses Bugbot review comment on PR #242.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Reinject from source when the session wrapper lands during anchor recovery.

The anchor-recovery observer stood down as soon as the session's variant
wrapper appeared in the DOM, without running injectVariantsFromSource.
A wrapper can land incomplete (wrap HMR landed, variant insert did not),
which is exactly the case injectVariantsFromSource's existing-wrapper
replace path handles - so recovery ended with the bar stuck and no
variants. Route both the anchor-found and wrapper-landed cases through
injectVariantsFromSource, which owns wrapper replacement, recovery-flag
clearing, and variant display.

Addresses Bugbot review comment on PR #242.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Restore inline edit drafts before configure chrome teardown disables editing.

teardownConfigureChrome called disableInlineEdit() ahead of hideBar(),
wiping inlineEditRows and the impeccableOriginalText metadata that
hideBar()'s EDITING-state restoreInlineEditDrafts() needs - so turning
Pick off mid "Edit copy" left edited DOM text in place, neither saved
nor canceled. Let hideBar() own the sequence: it restores drafts first,
then disables inline edit.

Addresses Bugbot review comment on PR #242.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:18:45 -07:00

397 lines
20 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const SOURCE = readFileSync(join(process.cwd(), 'skill/scripts/live-browser.js'), 'utf-8');
const PENDING_DOCK_POSITION_SOURCE = SOURCE.match(/function positionPendingDock\(\) \{[\s\S]*?\n \}/)?.[0] || '';
describe('live-browser source contracts', () => {
it('saves copy edits to the staged buffer with rich AI context', () => {
assert.doesNotMatch(
SOURCE,
/type: 'manual_edit_apply'|beginManualApplySession|createManualApplyOverlay|manualApplySession/,
'Save should not use the old direct manual_edit_apply loading path',
);
assert.match(
SOURCE,
/fetch\('http:\/\/localhost:' \+ PORT \+ '\/manual-edit-stash'[\s\S]{0,260}?pageUrl: location\.pathname[\s\S]{0,80}?element: extractContext\(contextElement\)[\s\S]{0,40}?ops,/,
'Save should stage edits through /manual-edit-stash with element context and ops',
);
assert.match(
SOURCE,
/fetch\([^)]*\/manual-edit-commit\?token=[\s\S]*?&async=1/,
'Apply copy edits should start /manual-edit-commit in async mode',
);
assert.match(
SOURCE,
/fetch\([^)]*\/manual-edit-discard\?token=/,
'Discard copy edits should call /manual-edit-discard',
);
assert.match(
SOURCE,
/const result = await res\.json\(\)\.catch\(\(\) => \(\{\}\)\);[\s\S]{0,120}?restoreDiscardedManualEdits\(result\.entries \|\| \[\]\);/,
'Discard copy edits should restore the visible staged DOM from returned buffer entries',
);
assert.match(
SOURCE,
/const restoreFailures = restoreDiscardedManualEdits\(result\.entries \|\| \[\]\);[\s\S]{0,260}?refresh to reset/,
'Discard restore should report unsafe local DOM restores to the caller',
);
assert.match(
SOURCE,
/function canRestoreManualEditElement\(el, op\)[\s\S]*?el\.children && el\.children\.length > 0[\s\S]*?return false;[\s\S]*?normalizeManualContextText\(el\.textContent\) === normalizeManualContextText\(op\.newText\);/,
'Discard restore should only write textContent into pure text leaves, never parent containers',
);
assert.match(
SOURCE,
/if \(!el \|\| typeof op\.originalText !== 'string' \|\| !canRestoreManualEditElement\(el, op\)\)[\s\S]*?failures \+= 1;[\s\S]*?continue;/,
'Unsafe discard restores should be skipped instead of wiping parent markup',
);
assert.match(
SOURCE,
/function parseManualEditRefSegment\(segment\)[\s\S]*?function elementMatchesManualRefSegment\(el, segment\)/,
'Discard restore should parse Impeccable document refs instead of treating them as raw CSS selectors',
);
const refMatchStart = SOURCE.indexOf('function elementMatchesManualRefSegment');
const refMatchEnd = SOURCE.indexOf('function cssIdent', refMatchStart);
const refMatchFn = SOURCE.slice(refMatchStart, refMatchEnd);
assert.match(
refMatchFn,
/if \(segment\.id && el\.id !== segment\.id\) return false;[\s\S]*for \(const cls of segment\.classes\)[\s\S]*if \(segment\.nth && indexAmongSameTag\(el\) !== segment\.nth\) return false;/,
'Discard restore refs should require id/classes and nth-of-type to match the same element',
);
assert.match(
SOURCE,
/const restoreHint = mixedTextWrapRestoreHint\(row\.el\);[\s\S]{0,80}if \(restoreHint\) op\.restore = restoreHint;/,
'Staged mixed-content text edits should carry a restore hint for their parent text node',
);
assert.match(
SOURCE,
/function restoreMixedTextNodeManualEdit\(op\)[\s\S]*?byIndex\.nodeValue = op\.originalText;/,
'Discard restore should restore unwrapped mixed-content text nodes by hint',
);
assert.doesNotMatch(
SOURCE,
/document\.querySelector\(ref\)/,
'Discard restore must not pass saved document refs directly to querySelector',
);
assert.match(
SOURCE,
/function pendingApplyLabel\(count\)[\s\S]{0,80}return count === 1 \? 'Apply copy edit' : 'Apply copy edits';/,
'the staged apply pill should use Apply copy edits copy',
);
assert.match(
SOURCE,
/function setPendingApplyLoading\(loading, count\)[\s\S]*?pendingPillSpinnerEl\.style\.display = pendingApplyInFlight \? 'inline-block' : 'none';[\s\S]*?pendingPillEl\.disabled = pendingApplyInFlight;[\s\S]*?pendingTrashBtn\.disabled = pendingApplyInFlight;[\s\S]*?schedulePendingDockPosition\(\);[\s\S]*?\n \}/,
'Apply copy edits should show a loading state and prevent double apply/discard while the AI batch runs',
);
assert.match(
SOURCE,
/function handleGo\(\)\s*\{\s*if \(pendingApplyInFlight\) \{ showManualApplyBusyToast\(\); return; \}[\s\S]*?captureAndEmit\(elForCapture, basePayload, snapshot, captureRect\);/,
'Go should be blocked while manual copy edits are applying',
);
assert.match(
SOURCE,
/function buildConfigureRow\(\)[\s\S]{0,80}?const controlsLocked = pendingApplyInFlight === true;[\s\S]*?const go = buildConfigureSubmitButton\(\{\s*controlsLocked,/,
'Configure controls should render disabled while manual copy edits are applying',
);
assert.match(
SOURCE,
/function buildConfigureSubmitButton\(\{ controlsLocked[\s\S]{0,700}?btn\.disabled = controlsLocked;/,
'the configure submit button must disable itself while manual copy edits are applying',
);
assert.match(
SOURCE,
/function handleMouseMove\(e\) \{[\s\S]{0,80}?if \(pendingApplyInFlight\) return;/,
'Element hover picking should pause while manual copy edits are applying',
);
assert.match(
SOURCE,
/function togglePick\(\) \{[\s\S]{0,100}?if \(pendingApplyInFlight\) \{ showManualApplyBusyToast\(\); return; \}/,
'Pick mode should not toggle while manual copy edits are applying',
);
assert.match(
SOURCE,
/function updateGlobalBarState\(\)[\s\S]*?const controlsLocked = pendingApplyInFlight === true;[\s\S]*?btn\.disabled = controlsLocked;/,
'Global live controls should be disabled while manual copy edits are applying',
);
assert.match(
SOURCE,
/function hidePendingApplyDock\(\)[\s\S]*?pendingDockEl\.style\.display = 'none';[\s\S]*?pendingPillEl\.style\.display = 'none';[\s\S]*?pendingTrashBtn\.style\.display = 'none';/,
'Zero pending copy edits should fully hide the Apply dock controls',
);
assert.match(
SOURCE,
/case 'manual_edit_commit_done':[\s\S]{0,120}?handleManualEditActivity\(msg\);/,
'Apply completion SSE should update the pending dock even if HMR interrupts the original fetch handler',
);
assert.match(
SOURCE,
/case 'manual_edit_apply_reply_received':[\s\S]{0,220}?case 'manual_edit_repair_needs_decision':[\s\S]{0,160}?handleManualEditActivity\(msg\);/,
'Apply progress and repair SSE should reach the pending dock handler',
);
assert.match(
SOURCE,
/function remainingManualEditCount\(payload\)[\s\S]*?payload\?\.perPage\?\.\[location\.pathname\][\s\S]*?payload\?\.remainingCount[\s\S]*?payload\?\.totalCount[\s\S]*?if \(totalCount === 0\) return 0;/,
'Apply completion counts should honor page count first and still hide the dock when only totalCount is zero',
);
assert.match(
SOURCE,
/if \(msg\.type === 'manual_edit_commit_done'\)[\s\S]*?const remainingCount = remainingManualEditCount\(msg\);[\s\S]*?updatePendingCounter\(remainingCount === null \? 0 : remainingCount\);/,
'Apply completion SSE should use the shared remaining-count helper',
);
assert.match(
SOURCE,
/function updateManualApplyProgressFromChunk\(chunk\)[\s\S]*?remainingCount[\s\S]*?phase: remainingCount > 0 \? 'applying' : 'verifying'[\s\S]*?setPendingApplyLoading\(true, remainingCount\);/,
'Apply progress should count completed chunk ops down and switch to verification after all chunk replies',
);
assert.match(
SOURCE,
/function manualApplyLoadingText\(fallbackCount\)[\s\S]*?Fixing apply issue, attempt[\s\S]*?Verifying copy edits[\s\S]*?Applying ' \+ remaining \+ ' copy edit/,
'Apply loading text should cover applying, verifying, and repair states',
);
assert.match(
SOURCE,
/manual_edit_repair_needs_decision[\s\S]*?showManualApplyDecision\(msg\);/,
'Repair exhaustion should show the user decision controls instead of hiding the dock',
);
assert.match(
SOURCE,
/function onPendingKeepFixingClick\(\)[\s\S]*?&repair=1/,
'Keep fixing should restart the repair loop against the current transaction',
);
assert.match(
SOURCE,
/function onPendingRollbackClick\(\)[\s\S]*?\/manual-edit-repair-decision[\s\S]*?action: 'rollback'/,
'Rollback should be an explicit user decision through the repair-decision endpoint',
);
const applyStart = SOURCE.indexOf('async function onPendingPillClick');
const applyEnd = SOURCE.indexOf('async function onPendingTrashClick', applyStart);
const applyFn = SOURCE.slice(applyStart, applyEnd);
assert.match(applyFn, /if \(count <= 0 \|\| pendingApplyInFlight\) return;/);
assert.doesNotMatch(applyFn, /page will reload/);
assert.match(applyFn, /setPendingApplyLoading\(true, count\);[\s\S]*?\/manual-edit-commit\?token=/);
assert.match(applyFn, /waitForSseCompletion = true;[\s\S]*?return;/);
assert.match(applyFn, /finally \{[\s\S]*?if \(waitForSseCompletion\) return;/);
assert.match(
SOURCE,
/String\(newText \|\| ''\)\.trim\(\) === ''[\s\S]{0,120}?Save rejected: copy edits cannot be empty\./,
'manual copy edits should reject empty text instead of staging unverifiable deletes',
);
assert.match(
SOURCE,
/pendingTrashTooltipEl\.textContent = 'Discard copy edits';/,
'the discard button should use tooltip copy',
);
assert.match(
SOURCE,
/const n = Array\.isArray\(result\.applied\) \? result\.applied\.length : \(result\.cleared \|\| 0\);/,
'Apply success toast should use verified applied/cleared counts only',
);
assert.doesNotMatch(
SOURCE,
/result\.applied\?\.length \|\| count/,
'Apply success toast must not fall back to the original staged count',
);
assert.match(
SOURCE,
/const width = globalBarEl\.offsetWidth;[\s\S]{0,80}?const height = globalBarEl\.offsetHeight;/,
'pending dock should position from stable bar dimensions',
);
assert.match(
SOURCE,
/pendingDockEl\.style\.bottom = Math\.round\(14 \+ \(height \/ 2\)\) \+ 'px';/,
'pending dock should use fixed bottom anchoring',
);
assert.doesNotMatch(
PENDING_DOCK_POSITION_SOURCE,
/rect\.top \+ rect\.height \/ 2/,
'pending dock should not use animated bar rect top for vertical positioning',
);
assert.match(
SOURCE,
/const sourceHint = sourceHintForElement\(row\.el\);[\s\S]{0,80}?op\.sourceHint = sourceHint;/,
'manual copy edits should preserve framework source hints when available',
);
assert.match(
SOURCE,
/const contextRef = documentRefForElement\(contextElement\);[\s\S]{0,80}?op\.contextRef = contextRef;/,
'manual copy edits should preserve the selected/container DOM path',
);
assert.match(
SOURCE,
/data-astro-source-file[\s\S]{0,120}?data-astro-source-loc/,
'Astro source metadata should be captured as optional source hints',
);
assert.match(
SOURCE,
/op\.leaf = copyEditLeafContext\(row\.el, row\.text, newText\);/,
'manual copy edits should capture the edited leaf details',
);
assert.match(
SOURCE,
/op\.nearbyEditableTexts = nearbyEditableTextsForManualEdit\(inlineEditRows, row\.el, row\.text, newText\);/,
'manual copy edits should capture nearby editable sibling text',
);
assert.match(
SOURCE,
/function sanitizedContextOuterHTML\(el, maxLength\)[\s\S]*?stripManualEditRuntimeState\(clone\);/,
'manual copy edit prompt context should strip browser-only edit markers before staging HTML',
);
assert.match(
SOURCE,
/outerHTML: sanitizedContextOuterHTML\(el, 10000\),/,
'staged element context should not include live edit runtime attributes',
);
assert.match(
SOURCE,
/function copyEditLeafContext\(el, originalText, newText\)[\s\S]*?outerHTML: sanitizedContextOuterHTML\(el, 3000\) \|\| null,/,
'staged leaf context should not include live edit runtime attributes',
);
assert.match(
SOURCE,
/if \(container\) for \(const op of ops\) op\.container = container;/,
'manual copy edits should attach selected/container context to each op',
);
assert.match(
SOURCE,
/const acceptPayload = \{[\s\S]{0,160}?pageUrl: location\.pathname,/,
'accept events should carry pageUrl so post-accept staged-edit cleanup is page-scoped',
);
const sourceHintStart = SOURCE.indexOf('function sourceHintForElement');
const sourceHintEnd = SOURCE.indexOf('function parseSourceLoc', sourceHintStart);
const sourceHintFn = SOURCE.slice(sourceHintStart, sourceHintEnd);
assert.doesNotMatch(
sourceHintFn,
/parentElement/,
'source hints should come from the edited leaf itself, not inherited generated-container ancestors',
);
});
it('keeps sendEvent fire-and-forget by default while accept/discard opt into rejection', () => {
assert.match(
SOURCE,
/function sendEvent\(msg, opts\)[\s\S]*if \(opts && opts\.throwOnError\) \{[\s\S]*console\.error\('\[impeccable\] Failed to send event:', err\);[\s\S]*throw err;[\s\S]*\}[\s\S]*console\.debug\('\[impeccable\] Dropped optional live event:', err\);[\s\S]*return null;/,
'event=live_browser.send_event_contract actor=browser operation=send_event_failure risk=fire_and_forget_callers_get_unhandled_rejections expected=default swallow with opt-in throw actual=missing',
);
assert.match(SOURCE, /if \(res\.ok\) return res;[\s\S]*const body = await res\.json\(\)\.catch\(\(\) => \(\{\}\)\);[\s\S]*handleFailure\(new Error\(body\.error \|\| \('HTTP ' \+ res\.status \+ ' ' \+ res\.statusText\)\)\)/);
assert.match(
SOURCE,
/\.then\(async res => \{[\s\S]*if \(res\.ok\) return res;[\s\S]*\}\)\.catch\(handleFailure\)/,
'event=live_browser.http_error_contract actor=browser operation=accept_discard_ack risk=http_500_clears_local_state_without_durable_receipt expected=non-ok response handled before then-success actual=missing',
);
assert.match(SOURCE, /sendEvent\(acceptPayload, \{ throwOnError: true \}\)/);
assert.match(SOURCE, /sendEvent\(\{ type: 'discard', id: currentSessionId \}, \{ throwOnError: true \}\)/);
});
it('waits for post-carbonize completion before final accepted DOM cleanup', () => {
assert.match(
SOURCE,
/let pendingAcceptedSession = null;/,
'accept flow should keep pending completion state after the browser sends the accept intent',
);
assert.match(
SOURCE,
/case 'complete':\s*case 'accept':\s*if \(maybeCompleteAcceptedSession\(msg\)\) break;/,
'final accepted DOM cleanup should be driven by explicit complete or harness accept replies',
);
assert.match(
SOURCE,
/case 'error':\s*if \(pendingAcceptedSession\?\.id && msg\.id === pendingAcceptedSession\.id\) \{[\s\S]{0,80}?pendingAcceptedSession = null;[\s\S]{0,80}?setLiveState\('CYCLING'\);[\s\S]{0,80}?updateBarContent\('cycling'\);[\s\S]{0,160}?break;/,
'an SSE error for a queued accept should invalidate pending accept state and keep variants retryable',
);
assert.equal(
SOURCE.match(/function cssIdent\(value\)/g)?.length || 0,
1,
'accepted DOM cleanup should reuse the existing cssIdent helper instead of shadowing it',
);
const agentDoneStart = SOURCE.indexOf("case 'agent_done':");
const errorCaseStart = SOURCE.indexOf("case 'error':", agentDoneStart);
const agentDoneSource = SOURCE.slice(agentDoneStart, errorCaseStart);
assert.match(agentDoneSource, /Carbonize accepts are not terminal/);
assert.match(agentDoneSource, /break;/);
assert.match(
SOURCE,
/function handleGo\(\)[\s\S]{0,900}?pendingAcceptedSession = null;[\s\S]{0,80}?currentSessionId = id8\(\);/,
'starting a new generation should clear any stale accepted-session sentinel first',
);
const handleAcceptStart = SOURCE.indexOf('function handleAccept()');
const maybeCompleteStart = SOURCE.indexOf('function maybeCompleteAcceptedSession', handleAcceptStart);
const handleAcceptSource = SOURCE.slice(handleAcceptStart, maybeCompleteStart);
assert.doesNotMatch(
handleAcceptSource,
/state = 'CONFIRMED'|cleanupAcceptedSession\(|hideBar\(\)/,
'accept enqueue should not clear or confirm the browser session before source cleanup completes',
);
assert.match(
SOURCE,
/function scheduleAcceptCleanup\(accepted\)[\s\S]*?acceptedDomAlreadyClean\(accepted\)[\s\S]*?setTimeout\(function\(\) \{[\s\S]*?ensureAcceptedDomClean\(accepted\);[\s\S]*?cleanupAcceptedSession\(\);[\s\S]*?\}, 1800\);/,
'post-cleanup fallback should give HMR a second chance before mutating React-owned DOM',
);
assert.match(
SOURCE,
/function ensureAcceptedDomClean\(pending\)[\s\S]*?acceptedDomAlreadyClean\(pending\)[\s\S]*?findAcceptedRuntimeWrappers\(sessionId\)[\s\S]*?for \(const wrapper of wrappers\)[\s\S]*?parent\.insertBefore\(accepted\.firstChild, wrapper\);[\s\S]*?wrapper\.remove\(\);[\s\S]*?acceptedDomAlreadyClean\(pending\)/,
'post-cleanup fallback should unwrap the accepted variant instead of preserving live runtime wrappers',
);
assert.match(
SOURCE,
/function acceptedDomAlreadyClean\(pending\)[\s\S]*?matches\.length > 0[\s\S]*?matches\.every[\s\S]*?data-impeccable-carbonize/,
'accepted DOM should not be considered clean while any matching root is still inside a carbonize wrapper',
);
assert.match(
SOURCE,
/function findAcceptedRuntimeWrappers\(sessionId\)[\s\S]*?querySelectorAll\('\[data-impeccable-variants=[\s\S]*?querySelectorAll\('\[data-impeccable-carbonize=/,
'post-cleanup fallback should remove every stale variants/carbonize wrapper left by React HMR after accept',
);
assert.match(
SOURCE,
/if \(!accepted\) \{[\s\S]{0,80}?wrapper\.remove\(\);[\s\S]{0,80}?continue;/,
'post-cleanup fallback should not leave a variants wrapper behind when the accepted variant node is missing',
);
assert.match(
SOURCE,
/function maybeCompleteAcceptedSession\(msg\)[\s\S]{0,260}?if \(currentSessionId && currentSessionId !== pending\.id\) \{[\s\S]{0,80}?pendingAcceptedSession = null;[\s\S]{0,80}?return false;/,
'stale accepted completions should not clean up a newer active browser session',
);
assert.match(
SOURCE,
/function reloadAfterMissingAcceptedDom\(pending\)[\s\S]*?location\.reload\(\);/,
'missing accepted DOM after clean source should recover by reloading the clean page',
);
});
it('normalizes generated JSX source before source-fallback DOM parsing', () => {
assert.match(
SOURCE,
/parser\.parseFromString\(normalizeSourceFallbackBlock\(block, filePath\), 'text\/html'\)/,
'source fallback should normalize JSX wrapper syntax before DOMParser sees it',
);
assert.match(
SOURCE,
/function normalizeSourceFallbackBlock\(block, filePath\)[\s\S]*?<style\\b\(\[\^>\]\*\)>\\s\*\\\{\\s\*`\(\[\\s\\S\]\*\?\)`\\s\*\\\}\\s\*<\\\/style>/,
'source fallback should unwrap JSX style template literals',
);
assert.match(
SOURCE,
/replace\(\/\\bclassName\\s\*=\/g, 'class='\)/,
'source fallback should translate className back to HTML class attributes',
);
assert.match(
SOURCE,
/value\.replace\(\/\\\$\\\{\[\^}\]\*\\\}\/g, ' '\)/,
'source fallback should reduce JSX template className values to literal class tokens',
);
assert.doesNotMatch(
SOURCE,
/querySelectorAll\(tag \+ '\\.' \+ cls\.split/,
'source fallback should not construct unsafe selectors from JSX-ish class strings',
);
assert.match(
SOURCE,
/function jsxStyleObjectToCss\(body\)/,
'source fallback should translate simple JSX style objects such as display:none',
);
});
});