-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't create custom elements in main and fix various small issues on tests #747
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, very good debugging/investigation in this PR, thank you!
I am a bit confused about this though:
The PyScriptTest will wait for ===PyScript page fully initialized=== to be printed in the console, once playwright sees the log, it starts the tests and in this case, the clicking. But the script hasn't been added to the head of the page yet, so no event is registered. Here's the relevant code:
I don't understand how this is possible.
(In the following I will put links to the source code in #737 because the logging is better and easier to follow, but the core logic is unchanged).
This is the workflow:
-
PyConfig.connectedCallback()
is called, and it callsthis.loadRuntimes()
pyscript/pyscriptjs/src/components/pyconfig.ts
Lines 46 to 50 in 4694602
appConfig.set(this.values); logger.info('config set:', this.values); this.loadRuntimes(); } -
in
loadRuntimes
, we create the<script>
tag to download pyodide, add aload
event listener and append it todocument.head
:
pyscript/pyscriptjs/src/components/pyconfig.ts
Lines 62 to 73 in 4694602
loadRuntimes() { logger.info('Initializing runtimes'); for (const runtime of this.values.runtimes) { const runtimeObj: Runtime = new PyodideRuntime(runtime.src, runtime.name, runtime.lang); const script = document.createElement('script'); // create a script DOM node script.src = runtimeObj.src; // set its src to the provided URL script.addEventListener('load', () => { void runtimeObj.initialize(); }); document.head.appendChild(script); } } -
Once the script has been loaded, the
load
event is fired and we callruntimeObj.initialize()
-
runtimeObj.initialize()
is this, and at the end it printsPyScript page fully initialized
:
pyscript/pyscriptjs/src/runtime.ts
Lines 135 to 181 in 4694602
async initialize(): Promise<void> { loader?.log('Loading runtime...'); await this.loadInterpreter(); const newEnv = { id: 'default', runtime: this, state: 'loading', }; runtimeLoaded.set(this); // Inject the loader into the runtime namespace // eslint-disable-next-line this.globals.set('pyscript_loader', loader); loader?.log('Runtime created...'); loadedEnvironments.update(environments => ({ ...environments, [newEnv['id']]: newEnv, })); // now we call all initializers before we actually executed all page scripts loader?.log('Initializing components...'); for (const initializer of initializers_) { await initializer(); } loader?.log('Initializing scripts...'); for (const script of scriptsQueue_) { await script.evaluate(); } scriptsQueue.set([]); // now we call all post initializers AFTER we actually executed all page scripts loader?.log('Running post initializers...'); if (appConfig_ && appConfig_.autoclose_loader) { loader?.close(); } for (const initializer of postInitializers_) { await initializer(); } // NOTE: this message is used by integration tests to know that // pyscript initialization has complete. If you change it, you need to // change it also in tests/integration/support.py logger.info('PyScript page fully initialized'); }
So, I don't really understand how it is possible that (4) is called before the <script>
is added to the head. I also tried to put a console.log
after document.head.appendChild(script)
and I confirm that it's printed way before PyScript page fully initialized
.
So, there must be another reason for why the time.sleep
is needed, but I think this investigation should not be a blocker to merge this PR, as it contains a lot of good stuff.
Moreover, I plan to do completely do a refactoring to improve the life cycle of a page and probably the code will change a lot, so maybe it's better to continue the investigation after the refactoring.
Thank you for the review and the walkthrough! I was clearly wrong thinking that the appendChild step was the reason why we had to wait a short period of time 🤔
Although I should have added a log statement there, but I forgot 😞 So yeah, It's a mystery still why we need to wait this short time, I'll probably poke at it a bit more to see if I can figure out what's happening 😄 |
e0d4c60
to
29d0884
Compare
also, I see that currently the tests are failing because of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, looks good to me!
Another related issue is #763 which aims to refactor heavily the life cycle of a page, but I'm happy to merge this PR immediately.
Thank you!
…tests (pyscript#747) * Create custom elements when the runtime finishes loading * Remove xfails and fix repl integration test * Fix commented ignore * Address Antonio's comments * Fix bad rebase * Make ure to wait for repl to be in attached state before asserting content * Move createCustomeElement up so it runs before we close the loader, xfail flaky d3 test * Fix xfail
* Execute pys-on* events when triggered, not at load Mimicing the behavior of Javascripts 'onLoad' event, we should not be executing the use code at page-load time, only when the event is triggered. * Update examples to new syntax * Fix merge issue * Await running event handler code * Restore pys-on* events with original behavior, deprecation warning * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * xfail toga example * Add missing { (typo) * Adjust callback chandling to make linter happy * Change alpha to latest (#760) * Don't create custom elements in main and fix various small issues on tests (#747) * Create custom elements when the runtime finishes loading * Remove xfails and fix repl integration test * Fix commented ignore * Address Antonio's comments * Fix bad rebase * Make ure to wait for repl to be in attached state before asserting content * Move createCustomeElement up so it runs before we close the loader, xfail flaky d3 test * Fix xfail * [pre-commit.ci] pre-commit autoupdate (#762) updates: - [github.com/pre-commit/mirrors-eslint: v8.23.0 → v8.23.1](pre-commit/mirrors-eslint@v8.23.0...v8.23.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * change documentation to point to latest instead of frozen alpha (#764) * Toga example is xpass * Correct 'xpass' to 'xfail' mark Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Peter W <34256109+pww217@users.noreply.github.com> Co-authored-by: Fábio Rosado <fabiorosado@outlook.com>
As the title stays, the main goal of the branch is to kill the infamous runtimeLoaded global store and all the complications, problems and bugs caused by the fact that in many places we needed to ensure/wait that the global runtime was properly set before being able to execute code. The core idea is that runtime is never a global object and that it's passed around explicitly, which means that when a function receives it, it is guaranteed to be initialized&ready. This caused a bit of complications in pybutton.ts, pyinputbox.ts and pyrepl.ts, because they indirectly want to call runtime.run from connectedCallback, which is the only place where we cannot explicitly pass the runtime because it's automatically called by the browser. But also, it is also a sign of a bad design, because it were entirely possible that connectedCallback was called before the runtime was ready, which probably caused many bugs, see e.g. #673 and #747. The solution to is use dependency injection and create the class later on: so instead of having a global PyButton class which relies on a global runtime (whose state is uncertain) we have a make_PyButton function which takes a runtime and make a PyButton class which is tied to that specific runtime (whose state is certainly ready, because we call make_PyButton only when we know that the runtime is ready). Similar for PyInputBox and PyRepl. Other highlights: thanks to this, I could kill the also infamous runAfterRuntimeInitialized and a couple of smelly lines which used setTimeout to "wait" for the runtime. While I was at it, I also called a lot of other stores which were completely unused and where probably leftovers from a past universe.
As the title stays, the main goal of the branch is to kill the infamous runtimeLoaded global store and all the complications, problems and bugs caused by the fact that in many places we needed to ensure/wait that the global runtime was properly set before being able to execute code. The core idea is that runtime is never a global object and that it's passed around explicitly, which means that when a function receives it, it is guaranteed to be initialized&ready. This caused a bit of complications in pybutton.ts, pyinputbox.ts and pyrepl.ts, because they indirectly want to call runtime.run from connectedCallback, which is the only place where we cannot explicitly pass the runtime because it's automatically called by the browser. But also, it is also a sign of a bad design, because it were entirely possible that connectedCallback was called before the runtime was ready, which probably caused many bugs, see e.g. pyscript#673 and pyscript#747. The solution to is use dependency injection and create the class later on: so instead of having a global PyButton class which relies on a global runtime (whose state is uncertain) we have a make_PyButton function which takes a runtime and make a PyButton class which is tied to that specific runtime (whose state is certainly ready, because we call make_PyButton only when we know that the runtime is ready). Similar for PyInputBox and PyRepl. Other highlights: thanks to this, I could kill the also infamous runAfterRuntimeInitialized and a couple of smelly lines which used setTimeout to "wait" for the runtime. While I was at it, I also called a lot of other stores which were completely unused and where probably leftovers from a past universe.
As the title stays, the main goal of the branch is to kill the infamous runtimeLoaded global store and all the complications, problems and bugs caused by the fact that in many places we needed to ensure/wait that the global runtime was properly set before being able to execute code. The core idea is that runtime is never a global object and that it's passed around explicitly, which means that when a function receives it, it is guaranteed to be initialized&ready. This caused a bit of complications in pybutton.ts, pyinputbox.ts and pyrepl.ts, because they indirectly want to call runtime.run from connectedCallback, which is the only place where we cannot explicitly pass the runtime because it's automatically called by the browser. But also, it is also a sign of a bad design, because it were entirely possible that connectedCallback was called before the runtime was ready, which probably caused many bugs, see e.g. pyscript#673 and pyscript#747. The solution to is use dependency injection and create the class later on: so instead of having a global PyButton class which relies on a global runtime (whose state is uncertain) we have a make_PyButton function which takes a runtime and make a PyButton class which is tied to that specific runtime (whose state is certainly ready, because we call make_PyButton only when we know that the runtime is ready). Similar for PyInputBox and PyRepl. Other highlights: thanks to this, I could kill the also infamous runAfterRuntimeInitialized and a couple of smelly lines which used setTimeout to "wait" for the runtime. While I was at it, I also called a lot of other stores which were completely unused and where probably leftovers from a past universe.
As the title stays, the main goal of the branch is to kill the infamous runtimeLoaded global store and all the complications, problems and bugs caused by the fact that in many places we needed to ensure/wait that the global runtime was properly set before being able to execute code. The core idea is that runtime is never a global object and that it's passed around explicitly, which means that when a function receives it, it is guaranteed to be initialized&ready. This caused a bit of complications in pybutton.ts, pyinputbox.ts and pyrepl.ts, because they indirectly want to call runtime.run from connectedCallback, which is the only place where we cannot explicitly pass the runtime because it's automatically called by the browser. But also, it is also a sign of a bad design, because it were entirely possible that connectedCallback was called before the runtime was ready, which probably caused many bugs, see e.g. pyscript#673 and pyscript#747. The solution to is use dependency injection and create the class later on: so instead of having a global PyButton class which relies on a global runtime (whose state is uncertain) we have a make_PyButton function which takes a runtime and make a PyButton class which is tied to that specific runtime (whose state is certainly ready, because we call make_PyButton only when we know that the runtime is ready). Similar for PyInputBox and PyRepl. Other highlights: thanks to this, I could kill the also infamous runAfterRuntimeInitialized and a couple of smelly lines which used setTimeout to "wait" for the runtime. While I was at it, I also called a lot of other stores which were completely unused and where probably leftovers from a past universe.
This PR fixes a few things:
Hopefully, you don't mind this wall of text, explaining the reasoning behind the decisions in the PR 😄
runAfterRuntimeInitialized running immediately on page load
After adding a bunch of
console.traces
I've noticed thatrunAfterRuntimeInitialized
was being called frommain.js
which was a bit odd, following the code path, it seems that it was being run when we were creating the py-button custom element inconst xPyButton = customElements.define('py-button', PyButton);
See the console output for a better visual.Console output with traces
Initially I wrapped the
customElements
in asetTimeout
and waited for 2.5s. This was sufficient most of the time, but there were some odd times that it wasn't. Increasing it to 3s made the button appear after the page loaded, which looked odd.The main.js file will only create elements for
PyScript
,PyConfig
,PyLoader
andPyEnv
. and the rest of the elements will be created once the runtime loads successfully and we closed the loader. With this change, the elements were always rendered successfully on the page and the errorCannot use 'in' operator to search for 'run' in undefined
stopped.Initially, the
createCustomElements
was in theutils.ts
but that caused jest to fail with a strange behaviour, so this function was moved to its own file.Removing the time.sleep from tests
While working on the tests last week (or two?) I added a few
time.sleep
because events weren't being triggered correctly. After looking into thepy-button
integration test and noticing that the clicks weren't registered, I started looking into thePyScriptTest
and the PyScript code.The
PyScriptTest
will wait for===PyScript page fully initialized===
to be printed in the console, once playwright sees the log, it starts the tests and in this case, the clicking. But the script hasn't been added to the head of the page yet, so no event is registered. Here's the relevant code:pyscript/pyscriptjs/src/components/pyconfig.ts
Line 68 in e31e03a
Adding a small
page.wait_for_timeout
was enough for the tests to start passing 😄 I'm wondering if perhaps we can make this timeout even smaller?In a few tests, I've also added a
self.page.wait_for_selector
which seems to work nicely so playwright will wait until the element is loaded before trying to click or check the contents of the selector.Please let me know if you would like me to change anything here (or if my assumptions/observations are incorrect 😄 )
Fixes: #673, #677, #726