@@ -1060,10 +1060,82 @@ function scaffoldInto(answers, targetDir) {
10601060 ] . join ( '\n' ) ) ;
10611061 }
10621062
1063+ // One data-provider case per module class that implements init_hooks(),
1064+ // for the generated Module_Hooks_Test.php ({{MODULE_HOOK_CASES}}) --
1065+ // verifies each module actually registers the hooks it claims to,
1066+ // instead of only ever being constructed inside a single-assertion test
1067+ // for one of its other methods. Collected as structured data rather than
1068+ // pre-rendered PHP text, because WordPress-Extra requires every `=>` in
1069+ // this array to align to its widest key -- unknowable until every module
1070+ // block below has run, so rendering is deferred to renderModuleHookCases().
1071+ const moduleHookCases = [ ] ;
1072+
1073+ /**
1074+ * Register one module's init_hooks() contract.
1075+ *
1076+ * @param {string } short PHP class path relative to {{NS}} (e.g.
1077+ * 'Admin\\Assets') -- also the data-provider key, unique since each
1078+ * module contributes at most one entry.
1079+ * @param {string } ctorExpr A `new \{{NS}}\...( ... )` PHP expression,
1080+ * wrapped in a factory closure so a constructor-injected service can be
1081+ * a fresh Mockery double built when the test runs, not when this
1082+ * provider is collected.
1083+ * @param {{type: 'action'|'filter'|'shortcode'|'cli_command', hook: string, raw?: boolean}[] } hooks
1084+ * The hooks init_hooks() is expected to register. `hook` is emitted as
1085+ * a single-quoted PHP string unless `raw` is set, for the two hooks
1086+ * built from a class constant at runtime rather than a literal.
1087+ */
1088+ function addModuleHookCase ( short , ctorExpr , hooks ) {
1089+ moduleHookCases . push ( { short, ctorExpr, hooks } ) ;
1090+ }
1091+
1092+ /**
1093+ * Render the collected module-hook cases as one `module_hook_provider()`
1094+ * array body ({{MODULE_HOOK_CASES}}), WPCS-formatted: every top-level
1095+ * `=>` aligned to the widest data-provider key (WordPress.Arrays.
1096+ * MultipleStatementAlignment), and every multi-item associative array --
1097+ * the provider entries themselves, and each hook's { type, hook } pair --
1098+ * one key per line (WordPress.Arrays.ArrayDeclarationSpacing).
1099+ *
1100+ * @param {{short: string, ctorExpr: string, hooks: {type: string, hook: string, raw?: boolean}[]}[] } cases
1101+ * @return {string }
1102+ */
1103+ function renderModuleHookCases ( cases ) {
1104+ const maxKeyLen = Math . max ( ...cases . map ( ( { short } ) => `'${ short } '` . length ) ) ;
1105+ return cases
1106+ . map ( ( { short, ctorExpr, hooks } ) => {
1107+ const key = `'${ short } '` . padEnd ( maxKeyLen , ' ' ) ;
1108+ const phpHooks = hooks
1109+ . map ( ( { type, hook, raw } ) => {
1110+ const hookExpr = raw ? hook : `'${ hook . replace ( / \\ / g, '\\\\' ) . replace ( / ' / g, "\\'" ) } '` ;
1111+ return [
1112+ '\t\t\t\t\tarray(' ,
1113+ `\t\t\t\t\t\t'type' => '${ type } ',` ,
1114+ `\t\t\t\t\t\t'hook' => ${ hookExpr } ,` ,
1115+ '\t\t\t\t\t),' ,
1116+ ] . join ( '\n' ) ;
1117+ } )
1118+ . join ( '\n' ) ;
1119+ return [
1120+ `\t\t\t${ key } => array(` ,
1121+ `\t\t\t\tstatic fn () => ${ ctorExpr } ,` ,
1122+ '\t\t\t\tarray(' ,
1123+ phpHooks ,
1124+ '\t\t\t\t),' ,
1125+ '\t\t\t),' ,
1126+ ] . join ( '\n' ) ;
1127+ } )
1128+ . join ( '\n' ) ;
1129+ }
1130+
10631131 if ( selectedModules . includes ( 'cli' ) ) {
10641132 writeTemplateFile ( path . join ( templatesDir , 'src/CLI/Commands.php' ) , 'src/CLI/Commands.php' ) ;
10651133 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Commands_Test.php' ) , 'tests/Unit/Commands_Test.php' ) ;
10661134 bootLines . push ( "\t\tif ( defined( 'WP_CLI' ) && WP_CLI ) {\n\t\t\t( new CLI\\Commands() )->init_hooks();\n\t\t}" ) ;
1135+ addModuleHookCase ( 'CLI\\Commands' , 'new \\{{NS}}\\CLI\\Commands()' , [
1136+ { type : 'cli_command' , hook : '{{PREFIX}} status' } ,
1137+ { type : 'cli_command' , hook : '{{PREFIX}} cache clear' } ,
1138+ ] ) ;
10671139 }
10681140 if ( selectedModules . includes ( 'admin_settings' ) ) {
10691141 writeTemplateFile ( path . join ( templatesDir , 'src/Admin/Settings_Repository.php' ) , 'src/Admin/Settings_Repository.php' ) ;
@@ -1075,16 +1147,30 @@ function scaffoldInto(answers, targetDir) {
10751147
10761148 bootLines . push ( '\t\t( new Admin\\Settings_Registrar( Services::settings_repository() ) )->init_hooks();' ) ;
10771149 addService ( 'settings_repository' , 'Admin\\Settings_Repository' ) ;
1150+ addModuleHookCase (
1151+ 'Admin\\Settings_Registrar' ,
1152+ 'new \\{{NS}}\\Admin\\Settings_Registrar( \\Mockery::mock( \\{{NS}}\\Admin\\Settings_Repository::class ) )' ,
1153+ [
1154+ { type : 'action' , hook : 'admin_menu' } ,
1155+ { type : 'action' , hook : 'admin_init' } ,
1156+ ]
1157+ ) ;
10781158 }
10791159 if ( selectedModules . includes ( 'shortcode' ) ) {
10801160 writeTemplateFile ( path . join ( templatesDir , 'src/Frontend/Shortcode.php' ) , 'src/Frontend/Shortcode.php' ) ;
10811161 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Shortcode_Test.php' ) , 'tests/Unit/Shortcode_Test.php' ) ;
10821162 bootLines . push ( '\t\t( new Frontend\\Shortcode() )->init_hooks();' ) ;
1163+ addModuleHookCase ( 'Frontend\\Shortcode' , 'new \\{{NS}}\\Frontend\\Shortcode()' , [
1164+ { type : 'shortcode' , hook : '{{PREFIX}}_display' } ,
1165+ ] ) ;
10831166 }
10841167 if ( selectedModules . includes ( 'rest_api' ) ) {
10851168 writeTemplateFile ( path . join ( templatesDir , 'src/Rest/Rest_Controller.php' ) , 'src/Rest/Rest_Controller.php' ) ;
10861169 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Rest_Controller_Test.php' ) , 'tests/Unit/Rest_Controller_Test.php' ) ;
10871170 bootLines . push ( '\t\t( new Rest\\Rest_Controller() )->init_hooks();' ) ;
1171+ addModuleHookCase ( 'Rest\\Rest_Controller' , 'new \\{{NS}}\\Rest\\Rest_Controller()' , [
1172+ { type : 'action' , hook : 'rest_api_init' } ,
1173+ ] ) ;
10881174 }
10891175 if ( selectedModules . includes ( 'ajax_handler' ) ) {
10901176 writeTemplateFile ( path . join ( templatesDir , 'src/Ajax/Ajax_Handler.php' ) , 'src/Ajax/Ajax_Handler.php' ) ;
@@ -1094,16 +1180,28 @@ function scaffoldInto(answers, targetDir) {
10941180 // file rides along with the module instead of the baseline.
10951181 writeTemplateFile ( path . join ( templatesDir , 'assets/js/main.js' ) , 'assets/js/main.js' ) ;
10961182 bootLines . push ( '\t\t( new Ajax\\Ajax_Handler() )->init_hooks();' ) ;
1183+ // allow_nopriv defaults to false, so the wp_ajax_nopriv_ variant isn't
1184+ // registered on a bare `new Ajax_Handler()` -- only the two below are.
1185+ addModuleHookCase ( 'Ajax\\Ajax_Handler' , 'new \\{{NS}}\\Ajax\\Ajax_Handler()' , [
1186+ { type : 'action' , hook : 'wp_ajax_{{PREFIX}}_action' } ,
1187+ { type : 'action' , hook : 'wp_enqueue_scripts' } ,
1188+ ] ) ;
10971189 }
10981190 if ( selectedModules . includes ( 'cpt_taxonomy' ) ) {
10991191 writeTemplateFile ( path . join ( templatesDir , 'src/PostTypes/Post_Types.php' ) , 'src/PostTypes/Post_Types.php' ) ;
11001192 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Post_Types_Test.php' ) , 'tests/Unit/Post_Types_Test.php' ) ;
11011193 bootLines . push ( '\t\t( new PostTypes\\Post_Types() )->init_hooks();' ) ;
1194+ addModuleHookCase ( 'PostTypes\\Post_Types' , 'new \\{{NS}}\\PostTypes\\Post_Types()' , [
1195+ { type : 'action' , hook : 'init' } ,
1196+ ] ) ;
11021197 }
11031198 if ( selectedModules . includes ( 'cron' ) ) {
11041199 writeTemplateFile ( path . join ( templatesDir , 'src/Cron/Scheduler.php' ) , 'src/Cron/Scheduler.php' ) ;
11051200 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Scheduler_Test.php' ) , 'tests/Unit/Scheduler_Test.php' ) ;
11061201 bootLines . push ( '\t\t( new Cron\\Scheduler() )->init_hooks();' ) ;
1202+ addModuleHookCase ( 'Cron\\Scheduler' , 'new \\{{NS}}\\Cron\\Scheduler()' , [
1203+ { type : 'action' , hook : '{{PREFIX}}_cron_event' } ,
1204+ ] ) ;
11071205 }
11081206 if ( selectedModules . includes ( 'caching' ) ) {
11091207 writeTemplateFile ( path . join ( templatesDir , 'src/Cache/Cache_Service.php' ) , 'src/Cache/Cache_Service.php' ) ;
@@ -1117,6 +1215,9 @@ function scaffoldInto(answers, targetDir) {
11171215 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Item_Repository_Test.php' ) , 'tests/Unit/Item_Repository_Test.php' ) ;
11181216 bootLines . push ( '\t\t( new Database\\Schema() )->init_hooks();' ) ;
11191217 addService ( 'item_repository' , 'Database\\Item_Repository' ) ;
1218+ addModuleHookCase ( 'Database\\Schema' , 'new \\{{NS}}\\Database\\Schema()' , [
1219+ { type : 'action' , hook : 'plugins_loaded' } ,
1220+ ] ) ;
11201221 }
11211222 if ( selectedModules . includes ( 'elementor_widget' ) ) {
11221223 if ( selectedModules . includes ( 'editor_config' ) ) {
@@ -1130,6 +1231,15 @@ function scaffoldInto(answers, targetDir) {
11301231 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Widget_Registrar_Test.php' ) , 'tests/Unit/Widget_Registrar_Test.php' ) ;
11311232 bootLines . push ( '\t\t( new Elementor\\Dependency_Notice() )->init_hooks();' ) ;
11321233 bootLines . push ( '\t\t( new Elementor\\Widget_Registrar() )->init_hooks();' ) ;
1234+ addModuleHookCase ( 'Elementor\\Dependency_Notice' , 'new \\{{NS}}\\Elementor\\Dependency_Notice()' , [
1235+ { type : 'action' , hook : 'admin_notices' } ,
1236+ ] ) ;
1237+ addModuleHookCase ( 'Elementor\\Widget_Registrar' , 'new \\{{NS}}\\Elementor\\Widget_Registrar()' , [
1238+ { type : 'filter' , hook : '{{PREFIX}}_cache_keys' } ,
1239+ { type : 'action' , hook : 'wp_enqueue_scripts' } ,
1240+ { type : 'action' , hook : 'elementor/editor/after_enqueue_styles' } ,
1241+ { type : 'action' , hook : 'elementor/widgets/register' } ,
1242+ ] ) ;
11331243 }
11341244 if ( hasWooGateway ) {
11351245 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Gateway_Provider.php' ) , 'src/Woo/Providers/Gateway_Provider.php' ) ;
@@ -1138,12 +1248,19 @@ function scaffoldInto(answers, targetDir) {
11381248 writeTemplateFile ( path . join ( templatesDir , 'react/assets/src/wc-gateway-block.js' ) , 'assets/src/wc-gateway-block.js' ) ;
11391249 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Gateway_Test.php' ) , 'tests/Unit/Gateway_Test.php' ) ;
11401250 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Gateway_Provider() )->init_hooks();' ) ;
1251+ addModuleHookCase ( 'Woo\\Providers\\Gateway_Provider' , 'new \\{{NS}}\\Woo\\Providers\\Gateway_Provider()' , [
1252+ { type : 'filter' , hook : 'woocommerce_payment_gateways' } ,
1253+ { type : 'action' , hook : 'woocommerce_blocks_loaded' } ,
1254+ ] ) ;
11411255 }
11421256 if ( hasWooShipping ) {
11431257 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Shipping_Provider.php' ) , 'src/Woo/Providers/Shipping_Provider.php' ) ;
11441258 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Shipping/Shipping_Method.php' ) , 'src/Woo/Shipping/Shipping_Method.php' ) ;
11451259 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Shipping_Method_Test.php' ) , 'tests/Unit/Shipping_Method_Test.php' ) ;
11461260 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Shipping_Provider() )->init_hooks();' ) ;
1261+ addModuleHookCase ( 'Woo\\Providers\\Shipping_Provider' , 'new \\{{NS}}\\Woo\\Providers\\Shipping_Provider()' , [
1262+ { type : 'filter' , hook : 'woocommerce_shipping_methods' } ,
1263+ ] ) ;
11471264 }
11481265 if ( hasWooEmail ) {
11491266 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Email_Provider.php' ) , 'src/Woo/Providers/Email_Provider.php' ) ;
@@ -1152,12 +1269,20 @@ function scaffoldInto(answers, targetDir) {
11521269 writeTemplateFile ( path . join ( templatesDir , 'woo-email-templates/emails/plain/custom-email.php' ) , `templates/emails/plain/${ answers . prefix . toLowerCase ( ) } -custom-email.php` ) ;
11531270 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Custom_Email_Test.php' ) , 'tests/Unit/Custom_Email_Test.php' ) ;
11541271 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Email_Provider() )->init_hooks();' ) ;
1272+ addModuleHookCase ( 'Woo\\Providers\\Email_Provider' , 'new \\{{NS}}\\Woo\\Providers\\Email_Provider()' , [
1273+ { type : 'filter' , hook : 'woocommerce_email_classes' } ,
1274+ ] ) ;
11551275 }
11561276 if ( hasWooProductType ) {
11571277 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Product_Type_Provider.php' ) , 'src/Woo/Providers/Product_Type_Provider.php' ) ;
11581278 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Products/Custom_Product.php' ) , 'src/Woo/Products/Custom_Product.php' ) ;
11591279 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Custom_Product_Test.php' ) , 'tests/Unit/Custom_Product_Test.php' ) ;
11601280 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Product_Type_Provider() )->init_hooks();' ) ;
1281+ addModuleHookCase ( 'Woo\\Providers\\Product_Type_Provider' , 'new \\{{NS}}\\Woo\\Providers\\Product_Type_Provider()' , [
1282+ { type : 'filter' , hook : 'woocommerce_product_class' } ,
1283+ { type : 'filter' , hook : 'product_type_selector' } ,
1284+ { type : 'action' , hook : 'woocommerce_single_product_summary' } ,
1285+ ] ) ;
11611286 }
11621287 if ( hasWooBlocks ) {
11631288 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Blocks_Provider.php' ) , 'src/Woo/Providers/Blocks_Provider.php' ) ;
@@ -1169,27 +1294,54 @@ function scaffoldInto(answers, targetDir) {
11691294 writeTemplateFile ( path . join ( templatesDir , 'react/assets/src/blocks/cart-summary/render.php' ) , 'assets/src/blocks/cart-summary/render.php' ) ;
11701295 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Cart_Summary_Block_Test.php' ) , 'tests/Unit/Cart_Summary_Block_Test.php' ) ;
11711296 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Blocks_Provider() )->init_hooks();' ) ;
1297+ addModuleHookCase ( 'Woo\\Providers\\Blocks_Provider' , 'new \\{{NS}}\\Woo\\Providers\\Blocks_Provider()' , [
1298+ { type : 'action' , hook : 'init' } ,
1299+ { type : 'action' , hook : 'woocommerce_blocks_loaded' } ,
1300+ ] ) ;
11721301 }
11731302 if ( hasWooOrderStatus ) {
11741303 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Order_Status_Provider.php' ) , 'src/Woo/Providers/Order_Status_Provider.php' ) ;
11751304 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Orders/Order_Status_Service.php' ) , 'src/Woo/Orders/Order_Status_Service.php' ) ;
11761305 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Order_Status_Service_Test.php' ) , 'tests/Unit/Order_Status_Service_Test.php' ) ;
11771306 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Order_Status_Provider( Services::order_status_service() ) )->init_hooks();' ) ;
11781307 addService ( 'order_status_service' , 'Woo\\Orders\\Order_Status_Service' ) ;
1308+ addModuleHookCase (
1309+ 'Woo\\Providers\\Order_Status_Provider' ,
1310+ 'new \\{{NS}}\\Woo\\Providers\\Order_Status_Provider( \\Mockery::mock( \\{{NS}}\\Woo\\Orders\\Order_Status_Service::class ) )' ,
1311+ [
1312+ { type : 'action' , hook : 'init' } ,
1313+ { type : 'filter' , hook : 'wc_order_statuses' } ,
1314+ ]
1315+ ) ;
11791316 }
11801317 if ( hasWooActionScheduler ) {
11811318 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Action_Scheduler_Provider.php' ) , 'src/Woo/Providers/Action_Scheduler_Provider.php' ) ;
11821319 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Tasks/Action_Scheduler_Service.php' ) , 'src/Woo/Tasks/Action_Scheduler_Service.php' ) ;
11831320 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Action_Scheduler_Service_Test.php' ) , 'tests/Unit/Action_Scheduler_Service_Test.php' ) ;
11841321 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Action_Scheduler_Provider( Services::action_scheduler_service() ) )->init_hooks();' ) ;
11851322 addService ( 'action_scheduler_service' , 'Woo\\Tasks\\Action_Scheduler_Service' ) ;
1323+ addModuleHookCase (
1324+ 'Woo\\Providers\\Action_Scheduler_Provider' ,
1325+ 'new \\{{NS}}\\Woo\\Providers\\Action_Scheduler_Provider( \\Mockery::mock( \\{{NS}}\\Woo\\Tasks\\Action_Scheduler_Service::class ) )' ,
1326+ [
1327+ { type : 'action' , hook : 'init' } ,
1328+ // A class constant, not a literal -- built the same way at
1329+ // test-run time instead of duplicating its value here.
1330+ { type : 'action' , hook : '\\{{NS}}\\Woo\\Tasks\\Action_Scheduler_Service::HOOK' , raw : true } ,
1331+ ]
1332+ ) ;
11861333 }
11871334 if ( hasWooStoreApi ) {
11881335 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Store_Api_Provider.php' ) , 'src/Woo/Providers/Store_Api_Provider.php' ) ;
11891336 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Api/Store_Api_Extension.php' ) , 'src/Woo/Api/Store_Api_Extension.php' ) ;
11901337 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Store_Api_Extension_Test.php' ) , 'tests/Unit/Store_Api_Extension_Test.php' ) ;
11911338 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Store_Api_Provider( Services::store_api_extension() ) )->init_hooks();' ) ;
11921339 addService ( 'store_api_extension' , 'Woo\\Api\\Store_Api_Extension' ) ;
1340+ addModuleHookCase (
1341+ 'Woo\\Providers\\Store_Api_Provider' ,
1342+ 'new \\{{NS}}\\Woo\\Providers\\Store_Api_Provider( \\Mockery::mock( \\{{NS}}\\Woo\\Api\\Store_Api_Extension::class ) )' ,
1343+ [ { type : 'action' , hook : 'woocommerce_blocks_loaded' } ]
1344+ ) ;
11931345 }
11941346 if ( hasWooMyAccount ) {
11951347 writeTemplateFile ( path . join ( templatesDir , 'src/Woo/Providers/Account_Endpoint_Provider.php' ) , 'src/Woo/Providers/Account_Endpoint_Provider.php' ) ;
@@ -1198,12 +1350,30 @@ function scaffoldInto(answers, targetDir) {
11981350 writeTemplateFile ( path . join ( templatesDir , 'tests/Unit/Account_Endpoint_Service_Test.php' ) , 'tests/Unit/Account_Endpoint_Service_Test.php' ) ;
11991351 wooBootLines . push ( '\t\t\t( new Woo\\Providers\\Account_Endpoint_Provider( Services::account_endpoint_service() ) )->init_hooks();' ) ;
12001352 addService ( 'account_endpoint_service' , 'Woo\\Account\\Account_Endpoint_Service' ) ;
1353+ addModuleHookCase (
1354+ 'Woo\\Providers\\Account_Endpoint_Provider' ,
1355+ 'new \\{{NS}}\\Woo\\Providers\\Account_Endpoint_Provider( \\Mockery::mock( \\{{NS}}\\Woo\\Account\\Account_Endpoint_Service::class ) )' ,
1356+ [
1357+ { type : 'action' , hook : 'init' } ,
1358+ { type : 'filter' , hook : 'woocommerce_account_menu_items' } ,
1359+ // Built from a class constant at runtime, same as above.
1360+ {
1361+ type : 'action' ,
1362+ hook : "'woocommerce_account_' . \\{{NS}}\\Woo\\Account\\Account_Endpoint_Service::ENDPOINT . '_endpoint'" ,
1363+ raw : true ,
1364+ } ,
1365+ ]
1366+ ) ;
12011367 }
12021368 if ( selectedModules . includes ( 'interactivity' ) ) {
12031369 writeTemplateFile ( path . join ( templatesDir , 'src/Frontend/Interactivity.php' ) , 'src/Frontend/Interactivity.php' ) ;
12041370 // Hand-written ESM served directly as a script module — no build step.
12051371 writeTemplateFile ( path . join ( templatesDir , 'interactivity/view.js' ) , 'assets/js/view.js' ) ;
12061372 bootLines . push ( '\t\t( new Frontend\\Interactivity() )->init_hooks();' ) ;
1373+ addModuleHookCase ( 'Frontend\\Interactivity' , 'new \\{{NS}}\\Frontend\\Interactivity()' , [
1374+ { type : 'action' , hook : 'init' } ,
1375+ { type : 'shortcode' , hook : '{{PREFIX}}_interactivity_demo' } ,
1376+ ] ) ;
12071377 }
12081378 if ( hasBlock ) {
12091379 // Block_Registrar globs assets/build/blocks/*, so it's variant-agnostic;
@@ -1224,6 +1394,9 @@ function scaffoldInto(answers, targetDir) {
12241394 writeTemplateFile ( path . join ( templatesDir , 'blocks/example-static/save.js' ) , 'assets/src/blocks/example-static/save.js' ) ;
12251395 }
12261396 bootLines . push ( '\t\t( new Blocks\\Block_Registrar() )->init_hooks();' ) ;
1397+ addModuleHookCase ( 'Blocks\\Block_Registrar' , 'new \\{{NS}}\\Blocks\\Block_Registrar()' , [
1398+ { type : 'action' , hook : 'init' } ,
1399+ ] ) ;
12271400 }
12281401
12291402 // React admin app (wp-admin only) + WooCommerce Blocks/Gateway + native
@@ -1249,6 +1422,9 @@ function scaffoldInto(answers, targetDir) {
12491422 // Assets.php scopes its enqueue via a {{#if admin_settings}}/{{else}} block.
12501423 writeTemplateFile ( path . join ( templatesDir , 'src/Admin/Assets.php' ) , 'src/Admin/Assets.php' ) ;
12511424 bootLines . push ( '\t\t( new Admin\\Assets() )->init_hooks();' ) ;
1425+ addModuleHookCase ( 'Admin\\Assets' , 'new \\{{NS}}\\Admin\\Assets()' , [
1426+ { type : 'action' , hook : 'admin_enqueue_scripts' } ,
1427+ ] ) ;
12521428 }
12531429
12541430 if ( needsBuildPipeline ) {
@@ -1447,6 +1623,19 @@ ${entries.join('\n')}
14471623 fs . writeFileSync ( servicesTestDest , servicesTest , 'utf8' ) ;
14481624 }
14491625
1626+ // Module_Hooks_Test.php: one data-provider test asserting every selected
1627+ // module's init_hooks() registers exactly the hooks it claims to. Only
1628+ // ships when at least one module contributed a case (a zero-module,
1629+ // non-React build has none).
1630+ if ( moduleHookCases . length > 0 ) {
1631+ let moduleHooksTest = fs . readFileSync ( path . join ( templatesDir , 'tests/Unit/Module_Hooks_Test.php' ) , 'utf8' ) ;
1632+ moduleHooksTest = moduleHooksTest . replace ( '{{MODULE_HOOK_CASES}}' , ( ) => renderModuleHookCases ( moduleHookCases ) ) ;
1633+ moduleHooksTest = processTemplateContent ( moduleHooksTest , 'tests/Unit/Module_Hooks_Test.php' ) ;
1634+ const moduleHooksTestDest = path . join ( targetDir , 'tests/Unit/Module_Hooks_Test.php' ) ;
1635+ fs . mkdirSync ( path . dirname ( moduleHooksTestDest ) , { recursive : true } ) ;
1636+ fs . writeFileSync ( moduleHooksTestDest , moduleHooksTest , 'utf8' ) ;
1637+ }
1638+
14501639 // Single supported PHP line — see MIN_PHP. The matrix also runs the next
14511640 // minor so a scaffold surfaces forward-compat breakage early.
14521641 const ciPhpMatrix = "['8.2', '8.3', '8.4']" ;
0 commit comments