{"record":{"id":"5f0022b896fd6766","repo":"heygen-com/hyperframes","slug":"drawelement-composition-root-has-no-parent-node","errorCode":null,"errorMessage":"drawElement: composition root has no parent node","messagePattern":"drawElement: composition root has no parent node","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/engine/src/services/drawElementService.ts","lineNumber":223,"sourceCode":"  height: number,\n): Promise<void> {\n  await page.evaluate(\n    ({ w, h }: { w: number; h: number }) => {\n      const root = document.querySelector(\"[data-composition-id]\") as HTMLElement | null;\n      if (!root || document.getElementById(\"__hf_de_canvas\")) return;\n      // Record the root's base opacity (timeline at 0, before any entrance/\n      // outro tween) for the LEGACY root-opacity ratio correction. The\n      // correction only runs on paths whose paint does not bake the root's\n      // current opacity into the snapshot — BeginFrame (sync=false) captures\n      // and builds without canvas.requestPaint(); see __hfDeInvalidate below.\n      try {\n        (window as unknown as { __HF_ROOT_BASE_OPACITY__?: number }).__HF_ROOT_BASE_OPACITY__ =\n          parseFloat(getComputedStyle(root).opacity) || 1;\n      } catch {\n        /* leave undefined → ratio defaults to 1 */\n      }\n      const parent = root.parentNode;\n      if (!parent) throw new Error(\"drawElement: composition root has no parent node\");\n      const canvas = document.createElement(\"canvas\") as HTMLCanvasElement & {\n        layoutsubtree: boolean;\n      };\n      canvas.id = \"__hf_de_canvas\";\n      canvas.setAttribute(\"layoutsubtree\", \"\");\n      canvas.width = w;\n      canvas.height = h;\n      canvas.style.cssText = \"display:block;position:absolute;top:0;left:0;z-index:0\";\n      parent.insertBefore(canvas, root);\n      canvas.appendChild(root);\n      // Invalidation sentinel: a canvas child OUTSIDE the captured root.\n      // Toggling its background each capture is a PAINT-level dirty\n      // (layout/transform toggles do NOT fire the canvas `paint` event), so a\n      // paint — and a fresh snapshot — is guaranteed even for static frames,\n      // without the sentinel ever appearing in drawElementImage(root) output.\n      const tick = document.createElement(\"div\");\n      tick.id = \"__hf_de_tick\";\n      tick.style.cssText =","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/engine/src/services/drawElementService.ts#L205-L241","documentation":"Thrown during injectDrawElementCanvas() when the composition root element (matched by [data-composition-id]) has a null parentNode. The canvas injection flow needs a parent to insertBefore(canvas, root) and then reparent the root into the canvas. A detached root (not attached to document.body) has no parent and cannot be wrapped.","triggerScenarios":"injectDrawElementCanvas(page, width, height) calls page.evaluate() which queries [data-composition-id], finds it, checks document.getElementById('__hf_de_canvas') is absent, then reads root.parentNode. If the root was created but never appended to the document (or was removed), parentNode is null.","commonSituations":"The composition HTML loaded but the root element was removed by a script before injection ran. injectDrawElementCanvas was called before the page finished loading the composition DOM (race condition with page.goto). A custom composition template doesn't attach the root to document.body. The root was moved to a shadow DOM.","solutions":["Ensure injectDrawElementCanvas is called after the composition root is in the live DOM: wait for document.querySelector('[data-composition-id]') to return a node whose parentNode is non-null.","Use page.waitForSelector('[data-composition-id]') before calling injectDrawElementCanvas.","Verify the composition HTML template includes the root as a child of body (not a detached fragment).","If using shadow DOM, adjust the querySelector to pierce it or attach the canvas inside the shadow root."],"exampleFix":"// before\nawait page.goto(url, { waitUntil: 'domcontentloaded' });\nawait injectDrawElementCanvas(page, w, h);\n\n// after\nawait page.goto(url, { waitUntil: 'domcontentloaded' });\nawait page.waitForSelector('[data-composition-id]');\nawait injectDrawElementCanvas(page, w, h);","handlingStrategy":"validation","validationCode":"// before calling injectDrawElementCanvas:\nconst hasRoot = await page.evaluate(() => {\n  const root = document.querySelector('[data-composition-id]');\n  return root !== null && root.parentNode !== null;\n});\nif (!hasRoot) {\n  throw new Error('Composition root not attached to DOM');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await injectDrawElementCanvas(page, w, h);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('no parent node')) {\n    // wait for DOM and retry\n    await page.waitForSelector('[data-composition-id]');\n    await injectDrawElementCanvas(page, w, h);\n  }\n  throw err;\n}","preventionTips":["Call page.waitForSelector('[data-composition-id]') before injectDrawElementCanvas.","Ensure composition templates attach the root to document.body.","Avoid scripts that remove or replace the root element after load."],"tags":["drawelement","dom","canvas","race-condition","browser"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}