Page MenuHomePhabricator

Need a soft dependency mechanism for QUnitTestModules?
Closed, DeclinedPublic

Description

We have a use case in which the Cite extension includes an optional ResourceLoader module ext.cite.referencePreviews which runs as a plugin to Popups. From a production perspective, this is perfectly safe because the module is only ever loaded by Popups. However, our QUnit tests are problematic because dependencies are "hard" and must be declared statically. If we don't list the Popups-dependent module as a dependency, then it seems to be impossible to load this module from our tests. If we do list the dependency, then the tests crash during the gated extension run where Popups is not loaded. Adding Popups to the gated extensions would be the least desirable workaround.

Potential workarounds:

  • Define a new module whose only purpose is to wrap the glue module in a soft way. The dependencies of this module would have to be dynamically modifiable from a PHP hook in the extension.
  • Introduce a mechanism for modifying QUnitTestModules.dependencies at run-time.
  • Find a way of loading RL modules not specified in dependencies.
  • Only run the glue tests manually, with a separate package.json target.

Event Timeline

awight removed awight as the assignee of this task.Mar 6 2024, 3:23 PM

This may have been simpler than I thought. The "load randomly" workaround seems to be just fine:

mw.loader.using( 'ext.cite.referencePreviews' )

I can include an error handler as the third parameter, and find some way of skipping the dependent tests.

There's no built-in mechanism in ResourceLoader, but it's fairly easy to implement it for your own module, for example:

Elegant workaround! I've used this now to implement an optional dependency for the QUnitModule and it helps with the situation, by causing the depdency to be conditionally required and made available.

However, there's still a gap caused by the inability to conditionally skip QUnit tests. In case it clarifies my issue, I'm currently forced to use code like this in each test:

( mw.loader.getModuleNames().indexOf( 'ext.popups.main' ) !== -1 ?
       QUnit.module :
       QUnit.module.skip )( 'ext.cite.referencePreviews#createReferenceGateway', {

I could use the same trick as you suggest in order to dynamically change the tests so that units dependent on Popups are absent, but I prefer to make the skipping visible. Ideally, QUnit would support something like before() { if ( COND ) this.skipModule() } but does not.

@awight

  • You can group these tests together in a nested QUnit.module scope, and skip it in its entirety using a single check by calling QUnit.module.skip. https://api.qunitjs.com/QUnit/module/
QUnit.module( 'ext.Example', function () {

  QUnit.test( 'a',  ); // ext.Example:  a
  QUnit.test( 'b',  ); // ext.Example: b

  QUnit.module.skip( 'popups', function () {

    QUnit.test( 'c',  ); // SKIP ext.Example: popups: c
    QUnit.test( 'd',  ); // SKIP ext.Example: popups: d

  } );
} );

That makes sense, but my question is how to do this dynamically? The only workaround I've come up with is quite an eyesore: T359409#9623218 . Maybe it will look nicer if I assign the module contents to a variable and write the conditional something like this:

const moduleBody = function () ...

if ( POPUPS_COND ) {
  QUnit.module( 'cite.and.popups', moduleBody );
} else {
  QUnit.module.skip( 'cite.and.popups', moduleBody );
}

I guess what I'm looking for is something more dynamic which can set the module skip property from e.g. a before hook. But looking at QUnit source code, I don't think this can be done.

You could wrap that in a function, right?

QUnit.moduleSkipIf = function ( name, cond, body ) {
  ( !cond ? QUnit.module : QUnit.module.skip )( name, body );
};
QUnit.moduleSkipIf( 'cite.and.popups', mw.loader.getState( 'ext.popups.main' )..., ... )

I'm not sure what the problem is at this point. Even without that, it seems clear enough for me. I don't feel like adding a helper in core would be helpful here, at least not until we see this pattern in several extensions.

Note that the entire file does need to be wrapped by the skip per-se. Something like following could work as well.

if ( POPUPS_COND ) {
  QUnit.test.skip( 'cite.and.popups' );
  return;
}

QUnit.module( 'cite.and.popups', function ( hooks ) {
  QUnit.test( , function ( assert ) {
    
  } );
} );

QUnit.test.skip() does not require a function body. One could also leave out the skip call. But I suppose it's useful to at least include a trace in the CLI output that a test was skipped. That's essentially all it is. A way to print a message.

I assume that there's nothing else to do here, but feel free to reopen if you disagree. It seems to me that adding a helper mechanism for soft dependencies is not needed right now, given that it's easy enough to implement already, as discussed above.