hero-composer-dom-continuity.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. // Regression drive for the unified hero composer (0729-0357-hero-unify):
  2. // cold start with zero workspaces -> create a workspace -> type. Asserts the
  3. // composer textarea is the SAME DOM node across the disabled->live flip (a
  4. // remount drops the __heroMark marker property) — the session-maybe
  5. // composer.bar contract.
  6. //
  7. // Prereqs: `pnpm run build`, then a fresh server against empty state:
  8. // rm -rf .storages && DSH_HOME=$(mktemp -d) node --experimental-transform-types \
  9. // --import ./scripts/tspath-loader.ts apps/cli/src/bin.ts web --port 44285 \
  10. // --workspace-root $(mktemp -d)
  11. // Run: node scripts/hero-composer-dom-continuity.mjs
  12. // (BASE_URL overrides the target; screenshots land in .artifacts/.)
  13. import { createRequire } from 'node:module'
  14. // playwright is a devDependency of apps/web only — resolve through its tree.
  15. const require = createRequire(new URL('../apps/web/package.json', import.meta.url))
  16. const { chromium } = require('playwright')
  17. const BASE = process.env.BASE_URL ?? 'http://127.0.0.1:44285'
  18. const SHOTS = new URL('../.artifacts/screenshots/0729-0357-hero-unify/', import.meta.url).pathname
  19. const browser = await chromium.launch()
  20. const page = await browser.newPage({ viewport: { width: 1280, height: 800 } })
  21. page.on('console', msg => { if (msg.type() === 'error') console.log('[console.error]', msg.text()) })
  22. page.on('pageerror', err => { console.log('[pageerror]', err.message) })
  23. await page.goto(BASE)
  24. await page.waitForSelector('textarea', { timeout: 20000 })
  25. await page.screenshot({ path: SHOTS + '01-cold-start.png' })
  26. const initial = await page.evaluate(() => {
  27. const boxes = [...document.querySelectorAll('textarea')]
  28. boxes.forEach((b, i) => { b.__heroMark = 'alive-' + i })
  29. return boxes.map(b => ({ disabled: b.disabled, placeholder: b.placeholder }))
  30. })
  31. console.log('cold-start textareas:', JSON.stringify(initial))
  32. // Open the picker and create a workspace by name (typed-input flow). The name
  33. // must be unique per registry; keystrokes go through pressSequentially so the
  34. // dialog's React onChange enables the submit button.
  35. await page.getByRole('button', { name: 'Choose workspace' }).click()
  36. await page.getByText('Create a new workspace').click()
  37. await page.screenshot({ path: SHOTS + '03-create-form.png' })
  38. const nameBox = page.getByPlaceholder('Workspace name')
  39. await nameBox.click()
  40. const wsName = 'proj-' + Date.now().toString(36)
  41. await nameBox.pressSequentially(wsName, { delay: 30 })
  42. await page.locator('button:text-is("Create workspace")').click()
  43. // Wait for the composer to go live (placeholder flips, textarea enabled).
  44. await page.waitForFunction(() => {
  45. const box = document.querySelector('textarea')
  46. return box !== null && !box.disabled
  47. }, { timeout: 20000 })
  48. await page.screenshot({ path: SHOTS + '04-live.png' })
  49. const after = await page.evaluate(() => {
  50. const boxes = [...document.querySelectorAll('textarea')]
  51. return boxes.map(b => ({
  52. mark: b.__heroMark ?? 'REMOUNTED',
  53. disabled: b.disabled,
  54. placeholder: b.placeholder,
  55. }))
  56. })
  57. console.log('post-pick textareas:', JSON.stringify(after))
  58. // Type into the live composer.
  59. await page.locator('textarea').first().fill('hello from acceptance run')
  60. const typed = await page.evaluate(() => document.querySelector('textarea')?.value)
  61. console.log('typed value:', JSON.stringify(typed))
  62. await page.screenshot({ path: SHOTS + '05-typed.png' })
  63. const survived = after.length === 1 && after[0].mark === 'alive-0'
  64. console.log(survived
  65. ? 'DOM-CONTINUITY: PASS (same textarea node across cold-start -> live)'
  66. : 'DOM-CONTINUITY: FAIL ' + JSON.stringify(after))
  67. await browser.close()
  68. process.exit(survived && typed === 'hello from acceptance run' ? 0 : 1)