JSX testing explained
Architecture approach¶
The plugin-based approach for sourcing JSX components that Whitebox uses makes it a powerful system, but with it comes some complexity. If you're looking to get JSX tests up and running quickly, check the Testing JSX code section of the plugin guide.
The static build of the frontend React app is what constitutes the Whitebox's kernel. All the features that it provides when launched (e.g. map), are a part of some plugin's code that augments the application through its provided JSX.
JSX code is provided outside the static build, using vite-plugin-federation
,
which are built and server by the backend server. This adds a layer of
complexity to the testing process, as neither the frontend code, nor its tests
are directly available to the test runner.
To ensure that the tests are run in the same environment and conditions as the application, federated tests are prepared and served by the backend server, to the frontend test runner. This approach does make unit testing a bit unconventional, but it ensures that the environment the tests run in is the same as the one the application runs in.
vite-plugin-federation
creates a static build of federated components, which
are then exposed over the network, and then the federation's internal mechanism
uses import()
to import those components. As Node.js does not support
import
ing files over http
, testing federation properly means we have to use
a browser-like environment to run the tests.
vitest
, the test runner that we're using for unit
tests, supports browser mode, which
allows us to run tests inside an actual browser, through Playwright
.
This abstraction, while flagged as experimental at the time of writing, works
with minimal changes to the test configuration, and with no changes to the
actual tests themselves.
Testing¶
The plugins' JSX lifecycle looks like this:
- The plugins are loaded by the backend server
- The plugins' JSX is collected by the backend server for federation build
- Optionally, test JSX files are collected if the testing flag is enabled
- The plugins' JSX is built by the backend server
- The plugins' JSX is exposed by the backend server
- Frontend loads the federation configuration from backend
- Optionally, test configuration is loaded from backend if the testing flag is enabled
- Frontend loads the federated components from the backend
- Test files can also be loaded. These do not contain any components, but rather contain test blocks that will be executed by the test runner
In Python code's case, the tests have been isolated into a separate package, so that the final deployment does not include the tests. While we could technically do the same for JSX code, we chose not to, as it would provide for a subpar development experience for a few reasons:
- As the JSX files are collected and built in an environment that is created specifically for the build, if tests were located outside the location where other JSX lives, import statements would not reflect the actual location of the files, looking like "invalid" imports
- With the "invalid" import statements, IDE support would be limited to nonexistent, as the IDE would not be able to resolve the imports
- The JSX testing framework would become much more complex - with Python code, we simply install a package in a native way, while with JSX, we would need to do some guess work to figure out where the tests are located, and where the files they test are located, which could question the reliability of the test suite in a more complex plugins & scenarios
Vitest discovery explained¶
Vitest performs test discovery during its initial module graph scan, so tests must be statically defined at the top level of the module, so they’re picked up as the file is parsed.
Let's say we're importing some module containing tests by using import('testfile')
(federation works slightly different from this, this is just for illustration
purposes). In the following example:
// When you import a file, tests "defined" merely by importing their file will
// be collected and included in the test suite
const mod = await import('testfile');
// However, any test produced "dynamically", e.g. by using a function, will not
// be collected:
mod.default(); // too late, already missed the collection phase
In the federation case, the test files are not imported directly, but rather
through the federation's mechanism, defined in federationUnitTestRunner.js
, which,
with the top-level awaits, imports all the test files so that they can be
collected in time. Any federated module containing .test.
in its name will be
imported at that time, and any top-level describe
/it
calls will be
collected and included in the test suite.
Running the test suite¶
By default, the test files (files ending with .test.jsx
) are not included in
the build, as they are not needed for the application to run. However, when
running the test suite, we need to include them in the build, so that they can
be loaded by the frontend test runner. This is done by setting the
JSX_INCLUDE_TESTS
environment variable when running the backend, which will
ensure that test files are collected as well.
To run the test suite, you first need to run the backend server with the testing flag enabled (make target sets the environment variable):
Then, in a separate shell, run:
This will run the test suite in the headless browser, using Playwright. In case
of any failures, you might notice that the test runner does not provide the
standard stacktrace you would otherwise expect. This is because of how
vite-plugin-federation
handles the module loading. Additionally, for the time
being, debugging is not possible, as the federation-provided are dynamically
built, and the IDE cannot resolve the paths to the files.
Migration from vite-plugin-federation
to module-federation
is planned, which might remedy some of these problems. For more info, take a
look at GitLab issue #249.