import time import py import pytest ROOT = py.path.local(__file__).dirpath("..", "..") BUILD = ROOT.join("pyscriptjs", "build") @pytest.mark.usefixtures("init") class PyScriptTest: """ Base class to write PyScript integration tests, based on playwright. It provides a simple API to generate HTML files and load them in playwright. It also provides a Pythonic API on top of playwright for the most common tasks; in particular: - self.console collects all the JS console.* messages. Look at the doc of ConsoleMessageCollection for more details. - self.check_errors() checks that no JS errors have been thrown - after each test, self.check_errors() is automatically run to ensure that no JS error passes uncaught. - self.wait_for_console waits until the specified message appears in the console - self.wait_for_pyscript waits until all the PyScript tags have been evaluated - self.pyscript_run is the main entry point for pyscript tests: it creates an HTML page to run the specified snippet. """ @pytest.fixture() def init(self, request, tmpdir, http_server, logger, page): """ Fixture to automatically initialize all the tests in this class and its subclasses. The magic is done by the decorator @pyest.mark.usefixtures("init"), which tells pytest to automatically use this fixture for all the test method of this class. Using the standard pytest behavior, we can request more fixtures: tmpdir, http_server and page; 'page' is a fixture provided by pytest-playwright. Then, we save these fixtures on the self and proceed with more initialization. The end result is that the requested fixtures are automatically made available as self.xxx in all methods. """ self.testname = request.function.__name__.replace("test_", "") self.tmpdir = tmpdir # create a symlink to BUILD inside tmpdir tmpdir.join("build").mksymlinkto(BUILD) self.tmpdir.chdir() self.http_server = http_server self.logger = logger self.init_page(page) # # this extra print is useful when using pytest -s, else we start printing # in the middle of the line print() def init_page(self, page): self.page = page self.console = ConsoleMessageCollection(self.logger) self._page_errors = [] page.on("console", self.console.add_message) page.on("pageerror", self._on_pageerror) def teardown_method(self): # we call check_errors on teardown: this means that if there are still # non-cleared errors, the test will fail. If you expect errors in your # page and they should not cause the test to fail, you should call # self.check_errors() in the test itself. self.check_errors() def _on_pageerror(self, error): self.logger.log("JS exception", error.stack, color="red") self._page_errors.append(error) def check_errors(self): """ Check whether JS errors were reported. If it finds a single JS error, raise JsError. If it finds multiple JS errors, raise JsMultipleErrors. Upon return, all the errors are cleared, so a subsequent call to check_errors will not raise, unless NEW JS errors have been reported in the meantime. """ exc = None if len(self._page_errors) == 1: # if there is a single error, wrap it exc = JsError(self._page_errors[0]) elif len(self._page_errors) >= 2: exc = JsMultipleErrors(self._page_errors) self._page_errors = [] if exc: raise exc def clear_errors(self): """ Clear all JS errors. """ self._page_errors = [] def writefile(self, filename, content): """ Very thin helper to write a file in the tmpdir """ f = self.tmpdir.join(filename) f.write(content) def goto(self, path): self.logger.reset() self.logger.log("page.goto", path, color="yellow") url = f"{self.http_server}/{path}" self.page.goto(url) def wait_for_console(self, text, *, timeout=None, check_errors=True): """ Wait until the given message appear in the console. Note: it must be the *exact* string as printed by e.g. console.log. If you need more control on the predicate (e.g. if you want to match a substring), use self.page.expect_console_message directly. timeout is expressed in milliseconds. If it's None, it will use playwright's own default value, which is 30 seconds). If check_errors is True (the default), it also checks that no JS errors were raised during the waiting. """ pred = lambda msg: msg.text == text try: with self.page.expect_console_message(pred, timeout=timeout): pass finally: # raise JsError if there were any javascript exception. Note that # this might happen also in case of a TimeoutError. In that case, # the JsError will shadow the TimeoutError but this is correct, # because it's very likely that the console message never appeared # precisely because of the exception in JS. if check_errors: self.check_errors() def wait_for_pyscript(self, *, timeout=None, check_errors=True): """ Wait until pyscript has been fully loaded. Timeout is expressed in milliseconds. If it's None, it will use playwright's own default value, which is 30 seconds). If check_errors is True (the default), it also checks that no JS errors were raised during the waiting. """ # this is printed by pyconfig.ts:PyodideRuntime.initialize self.wait_for_console( "===PyScript page fully initialized===", timeout=timeout, check_errors=check_errors, ) def pyscript_run(self, snippet): """ Main entry point for pyscript tests. snippet contains a fragment of HTML which will be put inside a full HTML document. In particular, the
automatically contains the correct {snippet}