Cannot read properties of undefined: your test asserted the host, not the tool
TLDR: If your component behaves differently depending on what the host injected, a test that runs in only one of those environments is testing half the component. Run it in both, and name which one you are in.
The error
page.evaluate: TypeError: Cannot read properties of undefined (reading 'create')
at eval (eval at evaluate, <anonymous>:59:32)
Line 59 was window.GxScene.create({ ... }) in a Playwright test I had just written against our schema editor.
The bug class
A component that adapts to its host has at least two behaviours, and a test harness silently picks one of them. The one it picks is almost always the one the author was thinking about, which is the one already working.
This shows up wherever capability is injected rather than imported. A plugin that may or may not have a host API. A library that uses IntersectionObserver when present. A tool that runs embedded in a shell and also standalone.
The concrete case
Our tools are forkable. Each one is a real repository someone can clone and run on its own, and each one is also loaded into an OS style shell that injects shared contracts as globals.
GxScene is one of those contracts, a shared spatial description used by several tools so that a figure rendered in one and re-embedded in another comes out pixel identical. The shell provides it. A fork does not have it.
The editor's import path knows this and branches:
if (window.GxScene) {
return window.GxScene.toSvg(scene || { width: dim.w, height: dim.h, nodes: [] },
{ backdrop: it.raster || null });
}
// No GxScene injected (forked standalone): a raster-only underlay is
// still honest, it just is not editable, which is what it looks like.
if (!it.raster) return null;
return <svg ...>;
My test loaded the tool's own index.html directly from a static server. No shell, so no injection, so the standalone branch. Every assertion I wrote about dimensions passed, because both branches now resolve dimensions the same way, and then the test hit line 59 where I built a scene to exercise the other branch.
The error was real and it was mine. But it was pointing at something worth keeping.
The fix
Two runs, one file, with the environment named in the output.
const WITH_SCENE = process.argv[2] !== 'standalone';
await p.goto('http://localhost:8791/tools/schema-editor/index.html', { waitUntil: 'networkidle' });
// The OS shell injects GxScene into the tool iframe; a fork has no shell.
if (WITH_SCENE) await p.addScriptTag({ url: 'http://localhost:8791/assets/os/scene.js' });
and, inside the page, a guard that reports the environment instead of throwing:
if (!window.GxScene) { res.sceneSvgHead = '(no GxScene: forked standalone)'; return res; }
Then run it twice:
node test.mjs # embedded
node test.mjs standalone # forked
Both print a banner saying which one they are, so a passing result cannot be mistaken for coverage of the other.
This caught something the single run would not have: the two branches emit different preserveAspectRatio values. The shared renderer keeps none so a raster backdrop stays co registered with the vector geometry drawn over it. The standalone path has no geometry to co register with, so it uses meet and letterboxes rather than distorting. Both are right, for different reasons, and I would not have been able to say that if I had only ever seen one of them.
Preventing the class
Make the injected dependency explicit in the test name. Not test.mjs but a run that announces "embedded" or "standalone" in its own output. A test that does not say which world it is in will be assumed to cover both.
Guard the assertion, not just the code. The production code already handled the missing global correctly. The test did not, which meant the test could only ever run in one world.
Count the branches, then count the runs. If the component has two host environments and the suite has one run, the ratio tells you the answer before you read anything.