├── codeception.dist.yml ├── composer.json ├── inc ├── class-clone-blocks.php ├── class-helpers.php ├── class-options.php ├── class-rest-api.php └── class-srb-rest-block-controller.php ├── readme.md ├── readme.txt ├── shared-reusable-blocks.php └── tests ├── _data ├── .gitkeep └── dump.sql ├── _output ├── .gitkeep ├── TestCept.fail.html └── debug │ └── login_page.png ├── _support ├── AcceptanceTester.php ├── FunctionalTester.php ├── Helper │ ├── Acceptance.php │ ├── Functional.php │ ├── Unit.php │ └── Wpunit.php ├── UnitTester.php ├── WpunitTester.php └── _generated │ ├── AcceptanceTesterActions.php │ ├── FunctionalTesterActions.php │ ├── UnitTesterActions.php │ └── WpunitTesterActions.php ├── acceptance.suite.yml ├── acceptance └── Test.feature ├── functional.suite.yml ├── functional ├── AnotherCept.php ├── TestCept.php └── _bootstrap.php ├── readme.txt ├── unit.suite.yml └── wpunit.suite.yml /codeception.dist.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | output: tests/_output 4 | data: tests/_data 5 | support: tests/_support 6 | envs: tests/_envs 7 | actor_suffix: Tester 8 | extensions: 9 | enabled: 10 | - Codeception\Extension\RunFailed 11 | commands: 12 | - Codeception\Command\GenerateWPUnit 13 | - Codeception\Command\GenerateWPRestApi 14 | - Codeception\Command\GenerateWPRestController 15 | - Codeception\Command\GenerateWPRestPostTypeController 16 | - Codeception\Command\GenerateWPAjax 17 | - Codeception\Command\GenerateWPCanonical 18 | - Codeception\Command\GenerateWPXMLRPC 19 | params: 20 | - .env 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "richardtape/shared-reusable-blocks", 3 | "description": "Share reusable blocks across a multisite network", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Richard Tape", 9 | "email": "richard.tape@ubc.ca" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "require": {}, 14 | "require-dev": { 15 | "lucatume/wp-browser": "^2.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /inc/class-clone-blocks.php: -------------------------------------------------------------------------------- 1 | register_hooks(); 25 | 26 | }//end init() 27 | 28 | /** 29 | * Calls our methods to register actions and filters. 30 | * 31 | * @return void 32 | */ 33 | public function register_hooks() { 34 | 35 | $this->register_actions(); 36 | 37 | }//end register_hooks() 38 | 39 | /** 40 | * Register the actions we need to hook into the WP ecosystem. 41 | * 42 | * @return void 43 | */ 44 | public function register_actions() { 45 | 46 | // When a spoke site adds a hub, grab the reusable blocks from that hub. 47 | add_action( 'updated_option', array( $this, 'updated_option__clone_hub_blocks_to_spoke' ), 30, 3 ); 48 | 49 | // When a new block is added on a hub, push it to the spokes. 50 | add_action( 'rest_after_insert_wp_block', array( $this, 'rest_after_insert_wp_block__push_block_to_spoke' ), 20, 3 ); 51 | 52 | // When a block is edited on the hub, push changes to the spoke. 53 | add_action( 'rest_after_insert_wp_block', array( $this, 'rest_after_insert_wp_block__edit_block_on_spoke' ), 30, 3 ); 54 | 55 | }//end register_actions() 56 | 57 | 58 | /** 59 | * When a spoke site adds a hub, we clone the reusable blocks from the hub into 60 | * the spoke. Each reusable block is a post of post type wp_block. We copy them over 61 | * and then register an association as post meta as the post IDs would highly likely 62 | * be different. This allows us to update this post in the future when a reusable block 63 | * is updated/deleted on the hub. 64 | * 65 | * @param string $option The option being changed. 66 | * @param string $old_value The previous value of the option. 67 | * @param string $value The new value of the option. 68 | * @return void 69 | */ 70 | public function updated_option__clone_hub_blocks_to_spoke( $option, $old_value, $value ) { 71 | 72 | // Ensure we only do this if it's our option that is being saved. 73 | if ( 'srb_use_as_hubs' !== $option ) { 74 | return; 75 | } 76 | 77 | if ( ! is_admin() ) { 78 | return; 79 | } 80 | 81 | // If no hub is selected then there'll be 1 item in the array that is empty. 82 | if ( ! is_array( $value ) || ! isset( $value[0] ) || ( 1 === count( $value ) && empty( $value[0] ) ) ) { 83 | 84 | // @TODO Ask what to do here; remove all imported reusable blocks? 85 | file_put_contents( WP_CONTENT_DIR . '/debug.log', print_r( array( 'all hubs removed. currently leaving any imported reusable blocks' ), true ), FILE_APPEND ); 86 | return; 87 | } 88 | 89 | // We have at least one hub. We'll now loop over the hubs to get a list of the reusable 90 | // blocks available on each hub. 91 | // Each block post is created on the spoke and post meta is attached to indicate the 92 | // site ID of the hub this came from and the post ID on the hub. 93 | $data_to_import = array(); 94 | 95 | foreach ( $value as $id => $hub_site_id ) { 96 | 97 | // Get the base URL so we can make a REST request. 98 | $rest_url = get_rest_url( absint( $hub_site_id ), 'wp/v2/blocks/' ); 99 | $args = array(); 100 | 101 | // Allow us to work locally on self-signed certs. 102 | if ( defined( 'SRB_DO_NOT_VERIFY_SSL' ) && true === constant( 'SRB_DO_NOT_VERIFY_SSL' ) ) { 103 | $args['sslverify'] = false; 104 | } 105 | 106 | $response = wp_remote_get( esc_url_raw( $rest_url ), $args ); 107 | $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); 108 | 109 | // Nothing to get? Move on. 110 | if ( ! $api_response || ! is_array( $api_response ) ) { 111 | continue; 112 | } 113 | 114 | $data_to_import[ $hub_site_id ] = $api_response; 115 | 116 | } 117 | 118 | $helpers = new Helpers(); 119 | 120 | // Now loop over $data_to_import which contains all the data to import keyed by the hub site ID. 121 | foreach ( $data_to_import as $hub_id => $reusable_blocks_json_array ) { 122 | 123 | foreach ( $reusable_blocks_json_array as $uid => $reusable_block ) { 124 | 125 | $new_post_id = $helpers->create_reusable_block_on_spoke( $reusable_block, $hub_id ); 126 | 127 | // Now detect if this is a block with media, so we can load that. 128 | $parsed_blocks = parse_blocks( $reusable_block['content']['raw'] ); 129 | 130 | if ( ! is_array( $parsed_blocks ) ) { 131 | continue; 132 | } 133 | 134 | foreach ( $parsed_blocks as $pid => $block ) { 135 | $block_name = $block['blockName']; 136 | } 137 | } 138 | } 139 | 140 | }//end updated_option__clone_hub_blocks_to_spoke() 141 | 142 | 143 | /** 144 | * When a reusable block is created on a hub, push this to the attached spokes. 145 | * 146 | * @param WP_Post $post which post is being edited. 147 | * @param WP_REST_Request $request - the full REST Request. 148 | * @param bool $creating True when creating a post, false when updating. 149 | * @return void 150 | */ 151 | public function rest_after_insert_wp_block__push_block_to_spoke( $post, $request, $creating ) { 152 | 153 | // Only do this operation when creating. We have a separate method for editing. 154 | if ( 1 !== absint( $creating ) ) { 155 | return; 156 | } 157 | 158 | // Which site was the block just created on? 159 | $hub_id = get_current_blog_id(); 160 | 161 | $helpers = new Helpers(); 162 | 163 | // Is this site a hub? 164 | if ( ! $helpers->site_is_a_hub( $hub_id ) ) { 165 | return; 166 | } 167 | 168 | // Which spokes are attached to this hub? 169 | $spoke_sites = $helpers->get_spoke_sites_for_hub( $hub_id ); 170 | 171 | if ( ! is_array( $spoke_sites ) || empty( $spoke_sites ) ) { 172 | return; 173 | } 174 | 175 | // Create the data we need to create the reusable block on the spoke. 176 | $reusable_block = $helpers->create_reusable_block_data_from_post_object( $post ); 177 | 178 | // This hub has spoke sites. Let's loop over each spoke site and push 179 | // this new block to each spoke. 180 | foreach ( $spoke_sites as $id => $spoke_site_id ) { 181 | switch_to_blog( $spoke_site_id ); 182 | $helpers->create_reusable_block_on_spoke( $reusable_block, $hub_id ); 183 | restore_current_blog(); 184 | } 185 | 186 | }//end rest_after_insert_wp_block__push_block_to_spoke() 187 | 188 | 189 | /** 190 | * When a block is edited on a hub, we have to push those changes to the spoke sites. The first edit 191 | * happens just after a block is created - when the user adds a title. Each block created as a 192 | * clone on a spoke has post meta which relates it to the hub which created it (Saves the hub ID and 193 | * the post ID of the block on the hub). We then work out which post on the spoke relates to the 194 | * block just updated on the hub and then update that post on the spoke with the new edits made on 195 | * the hub. 196 | * 197 | * @param WP_Post $post which post is being edited. 198 | * @param WP_REST_Request $request - the full REST Request. 199 | * @param bool $creating True when creating a post, false when updating. 200 | * @return void 201 | */ 202 | public function rest_after_insert_wp_block__edit_block_on_spoke( $post, $request, $creating ) { 203 | 204 | if ( 1 === $creating ) { 205 | return; 206 | } 207 | 208 | // We're editing an already-created block, so we need to ensure to update the 209 | // spoke and not create a new one. 210 | 211 | // Which site was the block just edited on? 212 | $hub_id = get_current_blog_id(); 213 | 214 | $helpers = new Helpers(); 215 | 216 | // Is this site a hub? 217 | if ( ! $helpers->site_is_a_hub( $hub_id ) ) { 218 | return; 219 | } 220 | 221 | // Which spokes are attached to this hub? 222 | $spoke_sites = $helpers->get_spoke_sites_for_hub( $hub_id ); 223 | 224 | if ( ! is_array( $spoke_sites ) || empty( $spoke_sites ) ) { 225 | return; 226 | } 227 | 228 | $edited_block_id = $post->ID; 229 | 230 | // On the spoke, for each block sent from the hub we have 2 pieces of meta. 231 | // One tells us which hub site ID the block originated, and the second 232 | // is what post ID on the hub that the block originated. 233 | foreach ( $spoke_sites as $id => $spoke_site_id ) { 234 | 235 | switch_to_blog( $spoke_site_id ); 236 | 237 | global $wpdb; 238 | 239 | $postids_from_hub = $wpdb->get_results( 240 | $wpdb->prepare( 241 | " 242 | SELECT post_id 243 | FROM $wpdb->postmeta 244 | WHERE meta_key = %s 245 | AND meta_value = %d 246 | ", 247 | sanitize_key( 'srb_from_post' ), 248 | absint( $edited_block_id ) 249 | ) 250 | ); 251 | 252 | if ( ! is_array( $postids_from_hub ) || empty( $postids_from_hub ) ) { 253 | restore_current_blog(); 254 | return; 255 | } 256 | 257 | $usable_ids = array(); 258 | foreach ( $postids_from_hub as $oid => $object_with_pid ) { 259 | $usable_ids[] = $object_with_pid->post_id; 260 | } 261 | 262 | $csv_of_postids = implode( ',', $usable_ids ); 263 | 264 | $post_id_on_spoke = $wpdb->get_var( 265 | $wpdb->prepare( 266 | " 267 | SELECT post_id 268 | FROM $wpdb->postmeta 269 | WHERE meta_key = %s 270 | AND meta_value = %d 271 | AND post_id IN (%s) 272 | ", 273 | sanitize_key( 'srb_from_site' ), 274 | absint( $hub_id ), 275 | trim( $csv_of_postids ) 276 | ) 277 | ); 278 | 279 | if ( ! $post_id_on_spoke ) { 280 | restore_current_blog(); 281 | return; 282 | } 283 | 284 | // Now we have the post ID on the spoke for the block which has been edited on the hub. 285 | // Update that post. 286 | wp_update_post( 287 | array( 288 | 'ID' => $post_id_on_spoke, 289 | 'post_title' => $post->post_title, 290 | 'post_content' => $post->post_content, 291 | 'post_name' => $post->post_name, 292 | ) 293 | ); 294 | 295 | restore_current_blog(); 296 | } 297 | 298 | }//end rest_after_insert_wp_block__edit_block_on_spoke() 299 | 300 | }//end class 301 | -------------------------------------------------------------------------------- /inc/class-helpers.php: -------------------------------------------------------------------------------- 1 | get_results( 28 | $wpdb->prepare( 29 | " 30 | SELECT blog_id 31 | FROM $wpdb->blogmeta 32 | WHERE meta_key = %s 33 | AND meta_value = %d 34 | ", 35 | $meta_key, 36 | $meta_value 37 | ) 38 | ); 39 | 40 | if ( ! $hubs || ! is_array( $hubs ) ) { 41 | return array(); 42 | } 43 | 44 | $hub_sites = array(); 45 | 46 | foreach ( $hubs as $id => $hub ) { 47 | $hub_sites[] = $hub->blog_id; 48 | } 49 | 50 | return $hub_sites; 51 | 52 | }//end fetch_hub_sites() 53 | 54 | 55 | /** 56 | * Determine if a site is a hub or not. 57 | * 58 | * @param int $site_id Which site to test for b eing a hub. 59 | * @return bool True if site is set as a hub, false otherwise. 60 | */ 61 | public function site_is_a_hub( $site_id ) { 62 | 63 | $site_id = absint( $site_id ); 64 | 65 | $hubs = $this->fetch_hub_sites(); 66 | 67 | if ( ! in_array( $site_id, array_values( $hubs ), false ) ) { 68 | return false; 69 | } 70 | 71 | return true; 72 | 73 | }//end site_is_a_hub() 74 | 75 | 76 | /** 77 | * For the passed $hub_id return an array of site IDs for all the spokes 78 | * attached to this hub. 79 | * 80 | * @param int $hub_id The site ID of the site we are retrieving the spokes for. 81 | * @return array The site IDs for the spokes attached to the passed $hub_id 82 | */ 83 | public function get_spoke_sites_for_hub( $hub_id ) { 84 | 85 | $hub_id = absint( $hub_id ); 86 | 87 | global $wpdb; 88 | 89 | $meta_key = 'is_a_hub_for_site'; 90 | 91 | $spokes = $wpdb->get_results( 92 | $wpdb->prepare( 93 | " 94 | SELECT meta_value 95 | FROM $wpdb->blogmeta 96 | WHERE meta_key = %s 97 | AND blog_id = %d 98 | ", 99 | $meta_key, 100 | $hub_id 101 | ) 102 | ); 103 | 104 | if ( ! $spokes || ! is_array( $spokes ) ) { 105 | return array(); 106 | } 107 | 108 | $spoke_sites = array(); 109 | 110 | foreach ( $spokes as $id => $spoke ) { 111 | $spoke_sites[] = $spoke->meta_value; 112 | } 113 | 114 | return $spoke_sites; 115 | 116 | }//end get_spoke_sites_for_hub() 117 | 118 | /** 119 | * Create a reusable block post on the spoke and relate it to the post on the hub. 120 | * 121 | * @param array $block_json A JSON array containing the details of the block. 122 | * @param int $hub_id The site ID on which the block is to be created. 123 | * @return int New Post ID 124 | */ 125 | public function create_reusable_block_on_spoke( $block_json = array(), $hub_id ) { 126 | 127 | // Insert post. 128 | $post_args = array( 129 | 'post_content' => $block_json['content']['raw'], 130 | 'post_title' => $block_json['title']['raw'], 131 | 'post_type' => 'wp_block', 132 | 'post_status' => $block_json['status'], 133 | ); 134 | 135 | // @TODO: Test if we already have this post on the spoke. Look for a post with post meta 136 | // key "srb_from_post" equal to $block_json['id'] 137 | global $wpdb; 138 | 139 | // First test if we have ANY posts from this hub, if we don't, then we're safe to add. 140 | $hub_query = $wpdb->prepare( "SELECT COUNT(meta_value) FROM $wpdb->postmeta WHERE meta_key ='srb_from_site' and meta_value = %d", absint( $hub_id ) ); 141 | $num_of_posts_from_this_hub = $wpdb->get_col( $hub_query ); 142 | 143 | if ( $num_of_posts_from_this_hub && $num_of_posts_from_this_hub > 0 ) { 144 | 145 | // We have posts already from this hub. Test if we have this specific post. 146 | $posts_with_this_post_id_from_a_hub = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key ='srb_from_post' and meta_value = %d", absint( $block_json['id'] ) ); 147 | $post_id_on_spoke_where_post_from_spoke_matches_current_post_id = $wpdb->get_results( $posts_with_this_post_id_from_a_hub ); 148 | 149 | if ( $post_id_on_spoke_where_post_from_spoke_matches_current_post_id && ! empty( $post_id_on_spoke_where_post_from_spoke_matches_current_post_id ) ) { 150 | // We have this block on the spoke, so bail. 151 | return $post_id_on_spoke_where_post_from_spoke_matches_current_post_id; 152 | } 153 | 154 | } 155 | 156 | $new_post_id = wp_insert_post( $post_args ); 157 | 158 | // Now add meta to show which post ID and Blog ID this came from. 159 | add_post_meta( $new_post_id, 'srb_from_site', absint( $hub_id ) ); 160 | add_post_meta( $new_post_id, 'srb_from_post', absint( $block_json['id'] ) ); 161 | 162 | return absint( $new_post_id ); 163 | 164 | }//end create_reusable_block_on_spoke() 165 | 166 | 167 | /** 168 | * Create the reusable block data we need to create a reusable block 169 | * from a WP_Post object. 170 | * 171 | * @param WP_Post $post The post object from which we create the json object. 172 | * @return array 173 | */ 174 | public function create_reusable_block_data_from_post_object( $post ) { 175 | 176 | $reusable_block = array( 177 | 'status' => $post->post_status, 178 | 'id' => $post->ID, 179 | 'content' => array( 180 | 'raw' => $post->post_content, 181 | ), 182 | 'title' => array( 183 | 'raw' => $post->post_title, 184 | ), 185 | ); 186 | 187 | return $reusable_block; 188 | 189 | }//end create_reusable_block_data_from_post_object() 190 | 191 | }//end class 192 | -------------------------------------------------------------------------------- /inc/class-options.php: -------------------------------------------------------------------------------- 1 | register_hooks(); 26 | 27 | }// end init() 28 | 29 | /** 30 | * Calls our methods to register actions and filters. 31 | * 32 | * @return void 33 | */ 34 | public function register_hooks() { 35 | 36 | $this->register_actions(); 37 | $this->register_filters(); 38 | 39 | }// end register_hooks() 40 | 41 | /** 42 | * Register the actions we need to hook into the WP ecosystem. 43 | * 44 | * @return void 45 | */ 46 | public function register_actions() { 47 | 48 | /* Hub Options */ 49 | /* *********** */ 50 | // Register the option to say if a site should act as a hub or not 51 | add_action( 'admin_init', array( $this, 'admin_init__register_hub_option' ) ); 52 | 53 | // When the srb_site_is_hub option is saved, hook in and add/remove blogmeta 54 | add_action( 'updated_option', array( $this, 'updated_option__srb_site_is_hub' ), 20, 3 ); 55 | 56 | /* Spoke Options */ 57 | /* ************* */ 58 | // Register the option to allow a spoke site to add one or more hubs. 59 | add_action( 'admin_init', array( $this, 'admin_init__register_spoke_hub_choice' ) ); 60 | 61 | // When a spoke site chooses (a) hub(s), ensure the hub site knows about it 62 | add_action( 'updated_option', array( $this, 'updated_option__srb_use_as_hubs' ), 20, 3 ); 63 | 64 | }// end register_actions() 65 | 66 | 67 | /** 68 | * Register our filters. 69 | * 70 | * @return void 71 | */ 72 | public function register_filters() { 73 | 74 | }// end register_filters() 75 | 76 | 77 | /** 78 | * Register the option to allow admins of the current site to stipulate that this 79 | * site will be used as a 'hub' for shared reusable blocks. Option appears in the 80 | * settings > writing panel as a checkbox. 81 | * 82 | * @return void 83 | */ 84 | public function admin_init__register_hub_option() { 85 | 86 | // Admins of this site only 87 | if ( ! current_user_can( 'manage_options' ) ) { 88 | return; 89 | } 90 | 91 | // What is the key used to store our option? 92 | $option_key = 'srb_site_is_hub'; 93 | 94 | // Which setting screen is it shown on? 95 | $screen = 'writing'; 96 | 97 | // Register the field... 98 | register_setting( 99 | $screen, 100 | $option_key, 101 | array( 102 | 'type' => 'integer', 103 | 'sanitize_callback' => 'absint', 104 | 'default' => 0, 105 | ) 106 | ); 107 | 108 | // ... and now add it. 109 | add_settings_field( 110 | $option_key, 111 | __( 'Shared Reusable Blocks Hub?', 'srb' ), 112 | array( $this, 'hub_option_markup' ), 113 | $screen, 114 | 'default', 115 | array( 116 | 'label_for' => $option_key, 117 | ) 118 | ); 119 | 120 | }// end admin_init__register_hub_option() 121 | 122 | /** 123 | * Outputs the checkbox field for saying a site is a hub 124 | * 125 | * @return void 126 | */ 127 | public function hub_option_markup() { 128 | 129 | $is_hub = absint( get_option( 'srb_site_is_hub' ) ); 130 | $help_text = __( 'Should the reusable blocks on this site be available on other sites which specifically choose to use this site as a hub? i.e. should this site act as a shared reusable blocks hub?', 'srb' ); 131 | ?> 132 | 133 | /> 134 | 135 | writing screen to allow an admin to 180 | * select which sites they wish to use as a hub for shared reusable blocks. 181 | * 182 | * @return void 183 | */ 184 | public function admin_init__register_spoke_hub_choice() { 185 | 186 | // Admins of this site only 187 | if ( ! current_user_can( 'manage_options' ) ) { 188 | return; 189 | } 190 | 191 | // Check that we have at least one hub registered. 192 | if ( ! $this->at_least_one_hub_registered() ) { 193 | return; 194 | } 195 | 196 | // What is the key used to store our option? 197 | $option_key = 'srb_use_as_hubs'; 198 | 199 | // Which setting screen is it shown on? 200 | $screen = 'writing'; 201 | 202 | // Register the field... 203 | register_setting( 204 | $screen, 205 | $option_key, 206 | array( 207 | 'type' => 'string', 208 | 'default' => '', 209 | ) 210 | ); 211 | 212 | // ... and now add it. 213 | add_settings_field( 214 | $option_key, 215 | __( 'Use other site as hub?', 'srb' ), 216 | array( $this, 'spoke_option_markup' ), 217 | $screen, 218 | 'default', 219 | array( 220 | 'label_for' => $option_key, 221 | ) 222 | ); 223 | 224 | }// end admin_init__register_spoke_hub_choice() 225 | 226 | 227 | /** 228 | * Markup for the option which allows an admin to select other sites to use as 229 | * a hub for shared reusable blocks. 230 | * 231 | * @return void 232 | */ 233 | public function spoke_option_markup() { 234 | 235 | $use_hubs = get_option( 'srb_use_as_hubs' ); 236 | $help_text = __( 'From which hub site(s) would you like to fetch reusable blocks?', 'srb' ); 237 | 238 | if ( ! is_array( $use_hubs ) ) { 239 | $use_hubs = array(); 240 | } 241 | 242 | $helpers = new Helpers(); 243 | $hub_sites = $helpers->fetch_hub_sites(); 244 | 245 | $options = ''; 246 | 247 | foreach ( $hub_sites as $id => $blog_id ) { 248 | 249 | $blog_id = absint( $blog_id ); 250 | 251 | // Don't do anything if the current blog has set itself up as a hub, too 252 | if ( absint( get_current_blog_id() ) === $blog_id ) { 253 | continue; 254 | } 255 | 256 | $blog_name = get_blog_option( $blog_id, 'blogname', $blog_id ); 257 | $selected = ( in_array( $blog_id, array_values( $use_hubs ), false ) ) ? 'selected' : ''; 258 | $options .= ""; 259 | } 260 | 261 | ?> 262 | 263 | 267 | 268 | remove_spoke_site_from_all_hubs( get_current_blog_id() ); 303 | return; 304 | } 305 | 306 | // There are hubs selected 307 | // Remove all existing hubs for this spoke 308 | $this->remove_spoke_site_from_all_hubs( get_current_blog_id() ); 309 | 310 | // And now add back all the ones that have just been asked for. 311 | foreach ( $value as $id => $hub_site_id ) { 312 | add_site_meta( $hub_site_id, $option_key_on_hub, get_current_blog_id() ); 313 | } 314 | 315 | }// end updated_option__srb_use_as_hubs() 316 | 317 | 318 | 319 | 320 | /** 321 | * A spoke site has updated and chosen to have no hubs. We look in the 322 | * blogmeta table and remove all references 323 | * 324 | * @param int $spoke_site_id 325 | * @return void 326 | */ 327 | public function remove_spoke_site_from_all_hubs( $spoke_site_id = null ) { 328 | 329 | global $wpdb; 330 | 331 | $meta_key = 'is_a_hub_for_site'; 332 | $meta_value = absint( $spoke_site_id ); 333 | 334 | $wpdb->delete( 335 | $wpdb->blogmeta, 336 | array( 337 | 'meta_key' => $meta_key, 338 | 'meta_value' => $meta_value, 339 | ), 340 | array( 341 | '%s', 342 | '%d', 343 | ) 344 | ); 345 | 346 | }// end remove_spoke_site_from_all_hubs() 347 | 348 | 349 | /** 350 | * Helper method to determine if there is at least one hub registered. 351 | * 352 | * @return bool true if one or more hubs registered, false otherwise. 353 | */ 354 | public function at_least_one_hub_registered() { 355 | 356 | $helpers = new Helpers(); 357 | $hubs = $helpers->fetch_hub_sites(); 358 | 359 | if ( $hubs && count( $hubs ) >= 1 ) { 360 | return true; 361 | } 362 | 363 | return false; 364 | 365 | }// end at_least_one_hub_registered() 366 | 367 | }// end class Options 368 | -------------------------------------------------------------------------------- /inc/class-rest-api.php: -------------------------------------------------------------------------------- 1 | register_hooks(); 25 | 26 | }//end init() 27 | 28 | /** 29 | * Calls our methods to register actions and filters. 30 | * 31 | * @return void 32 | */ 33 | public function register_hooks() { 34 | 35 | $this->register_actions(); 36 | $this->register_filters(); 37 | 38 | }//end register_hooks() 39 | 40 | /** 41 | * Register the actions we need to hook into the WP ecosystem. 42 | * 43 | * @return void 44 | */ 45 | public function register_actions() { 46 | 47 | }//end register_actions() 48 | 49 | /** 50 | * Register our filters. 51 | * 52 | * @return void 53 | */ 54 | public function register_filters() { 55 | 56 | add_filter( 'register_post_type_args', array( $this, 'register_post_type_args__add_reusable_blocks_to_rest_api' ), 99, 2 ); 57 | 58 | }//end register_filters() 59 | 60 | 61 | /** 62 | * Add reusable blocks to the rest API. We do this by registering a new class 63 | * to handle the REST API responses and ensuring we give non-logged-in users the 64 | * ability to read them. 65 | * 66 | * @param array $args Array of arguments for registering a post type. 67 | * @param string $post_type Post type key. 68 | * 69 | * @return array The (potentially) modified post type arguments. 70 | */ 71 | public function register_post_type_args__add_reusable_blocks_to_rest_api( $args, $post_type ) { 72 | 73 | if ( 'wp_block' !== $post_type ) { 74 | return $args; 75 | } 76 | 77 | $helpers = new Helpers(); 78 | 79 | // Check that this site is a hub and only add reusable blocks if it is. 80 | $hubs = $helpers->fetch_hub_sites(); 81 | 82 | if ( ! in_array( get_current_blog_id(), $hubs, false ) ) { 83 | return $args; 84 | } 85 | 86 | $args['rest_controller_class'] = '\SharedReusableBlocks\SRB_REST_Block_Controller'; 87 | 88 | return $args; 89 | 90 | }//end register_post_type_args__add_reusable_blocks_to_rest_api() 91 | 92 | }//end class 93 | -------------------------------------------------------------------------------- /inc/class-srb-rest-block-controller.php: -------------------------------------------------------------------------------- 1 | init(); 41 | 42 | require_once 'inc/class-clone-blocks.php'; 43 | $clone_blocks = new \SharedReusableBlocks\Clone_Blocks(); 44 | $clone_blocks->init(); 45 | 46 | if ( ! srb_is_rest() ) { 47 | return; 48 | } 49 | 50 | require_once 'inc/class-rest-api.php'; 51 | require_once 'inc/class-srb-rest-block-controller.php'; 52 | $rest_api = new \SharedReusableBlocks\Rest_API(); 53 | $rest_api->init(); 54 | 55 | }//end load_srb_files() 56 | 57 | /** 58 | * Checks if the current request is a WP REST API request. 59 | * 60 | * Case #1: After WP_REST_Request initialisation 61 | * Case #2: Support "plain" permalink settings 62 | * Case #3: URL Path begins with wp-json/ (your REST prefix) 63 | * Also supports WP installations in subfolders 64 | * 65 | * @return boolean 66 | * @author matzeeable ref https://wordpress.stackexchange.com/a/317041 67 | */ 68 | function srb_is_rest() { 69 | 70 | $prefix = rest_get_url_prefix(); 71 | 72 | if ( defined( 'REST_REQUEST' ) && REST_REQUEST || isset( $_GET['rest_route'] ) && strpos( trim( $_GET['rest_route'], '\\/' ), $prefix, 0 ) === 0 ) { 73 | return true; 74 | } 75 | 76 | // (#3) 77 | $rest_url = wp_parse_url( site_url( $prefix ) ); 78 | $current_url = wp_parse_url( add_query_arg( array() ) ); 79 | 80 | return strpos( $current_url['path'], $rest_url['path'], 0 ) === 0; 81 | 82 | }//end srb_is_rest() 83 | 84 | -------------------------------------------------------------------------------- /tests/_data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardtape/shared-reusable-blocks/9bd803b9e0e150e6267efed3baf7efd15e28215c/tests/_data/.gitkeep -------------------------------------------------------------------------------- /tests/_output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardtape/shared-reusable-blocks/9bd803b9e0e150e6267efed3baf7efd15e28215c/tests/_output/.gitkeep -------------------------------------------------------------------------------- /tests/_output/TestCept.fail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello world! – SharedReusableBlocks 8 | 9 | 10 | 11 | 12 | 16 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 62 | 63 |
64 | 65 |
66 |
67 | 68 | 69 | 89 | 90 |
91 |
92 |

93 | Join the Conversation

94 | 95 |
96 |
    97 |
  1. 98 |
99 |

100 | 1 Comment 101 |

102 |
103 |
104 |
    105 |
  1. 106 | 127 | 128 |
  2. 129 |
130 |
131 | Leave a comment 132 |
133 |

134 |

Your email address will not be published. Required fields are marked *

135 | 136 |

137 | 138 |

139 | 140 |

141 |
142 | 143 |
144 |
145 | 146 |
147 |
148 | 149 | 150 |
151 | 152 | 188 | 189 |
190 | 191 | 192 | 193 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /tests/_output/debug/login_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardtape/shared-reusable-blocks/9bd803b9e0e150e6267efed3baf7efd15e28215c/tests/_output/debug/login_page.png -------------------------------------------------------------------------------- /tests/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | amOnPage( '/' ); 29 | // throw new \Codeception\Exception\Incomplete( "Step `I am on the home page` is not defined" ); 30 | 31 | } 32 | 33 | /** 34 | * @When I click the log in button 35 | */ 36 | public function iClickTheLogInButton() { 37 | $this->click( 'Log in' ); 38 | } 39 | 40 | /** 41 | * @Then I should be on the log in page 42 | */ 43 | public function iShouldBeOnTheLogInPage() { 44 | $this->seeInCurrentUrl( 'wp-login.php' ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/_support/FunctionalTester.php: -------------------------------------------------------------------------------- 1 | assertEquals(5, $element->getChildrenCount()); 27 | * ``` 28 | * 29 | * Floating-point example: 30 | * ```php 31 | * assertEquals(0.3, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01); 33 | * ``` 34 | * 35 | * @param $expected 36 | * @param $actual 37 | * @param string $message 38 | * @param float $delta 39 | * @see \Codeception\Module\Asserts::assertEquals() 40 | */ 41 | public function assertEquals($expected, $actual, $message = null, $delta = null) { 42 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEquals', func_get_args())); 43 | } 44 | 45 | 46 | /** 47 | * [!] Method is generated. Documentation taken from corresponding module. 48 | * 49 | * Checks that two variables are not equal. If you're comparing floating-point values, 50 | * you can specify the optional "delta" parameter which dictates how great of a precision 51 | * error are you willing to tolerate in order to consider the two values not equal. 52 | * 53 | * Regular example: 54 | * ```php 55 | * assertNotEquals(0, $element->getChildrenCount()); 57 | * ``` 58 | * 59 | * Floating-point example: 60 | * ```php 61 | * assertNotEquals(0.4, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01); 63 | * ``` 64 | * 65 | * @param $expected 66 | * @param $actual 67 | * @param string $message 68 | * @param float $delta 69 | * @see \Codeception\Module\Asserts::assertNotEquals() 70 | */ 71 | public function assertNotEquals($expected, $actual, $message = null, $delta = null) { 72 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEquals', func_get_args())); 73 | } 74 | 75 | 76 | /** 77 | * [!] Method is generated. Documentation taken from corresponding module. 78 | * 79 | * Checks that two variables are same 80 | * 81 | * @param $expected 82 | * @param $actual 83 | * @param string $message 84 | * @see \Codeception\Module\Asserts::assertSame() 85 | */ 86 | public function assertSame($expected, $actual, $message = null) { 87 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertSame', func_get_args())); 88 | } 89 | 90 | 91 | /** 92 | * [!] Method is generated. Documentation taken from corresponding module. 93 | * 94 | * Checks that two variables are not same 95 | * 96 | * @param $expected 97 | * @param $actual 98 | * @param string $message 99 | * @see \Codeception\Module\Asserts::assertNotSame() 100 | */ 101 | public function assertNotSame($expected, $actual, $message = null) { 102 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotSame', func_get_args())); 103 | } 104 | 105 | 106 | /** 107 | * [!] Method is generated. Documentation taken from corresponding module. 108 | * 109 | * Checks that actual is greater than expected 110 | * 111 | * @param $expected 112 | * @param $actual 113 | * @param string $message 114 | * @see \Codeception\Module\Asserts::assertGreaterThan() 115 | */ 116 | public function assertGreaterThan($expected, $actual, $message = null) { 117 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterThan', func_get_args())); 118 | } 119 | 120 | 121 | /** 122 | * [!] Method is generated. Documentation taken from corresponding module. 123 | * 124 | * Checks that actual is greater or equal than expected 125 | * 126 | * @param $expected 127 | * @param $actual 128 | * @param string $message 129 | * @see \Codeception\Module\Asserts::assertGreaterThanOrEqual() 130 | */ 131 | public function assertGreaterThanOrEqual($expected, $actual, $message = null) { 132 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterThanOrEqual', func_get_args())); 133 | } 134 | 135 | 136 | /** 137 | * [!] Method is generated. Documentation taken from corresponding module. 138 | * 139 | * Checks that actual is less than expected 140 | * 141 | * @param $expected 142 | * @param $actual 143 | * @param string $message 144 | * @see \Codeception\Module\Asserts::assertLessThan() 145 | */ 146 | public function assertLessThan($expected, $actual, $message = null) { 147 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessThan', func_get_args())); 148 | } 149 | 150 | 151 | /** 152 | * [!] Method is generated. Documentation taken from corresponding module. 153 | * 154 | * Checks that actual is less or equal than expected 155 | * 156 | * @param $expected 157 | * @param $actual 158 | * @param string $message 159 | * @see \Codeception\Module\Asserts::assertLessThanOrEqual() 160 | */ 161 | public function assertLessThanOrEqual($expected, $actual, $message = null) { 162 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessThanOrEqual', func_get_args())); 163 | } 164 | 165 | 166 | /** 167 | * [!] Method is generated. Documentation taken from corresponding module. 168 | * 169 | * Checks that haystack contains needle 170 | * 171 | * @param $needle 172 | * @param $haystack 173 | * @param string $message 174 | * @see \Codeception\Module\Asserts::assertContains() 175 | */ 176 | public function assertContains($needle, $haystack, $message = null) { 177 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertContains', func_get_args())); 178 | } 179 | 180 | 181 | /** 182 | * [!] Method is generated. Documentation taken from corresponding module. 183 | * 184 | * Checks that haystack doesn't contain needle. 185 | * 186 | * @param $needle 187 | * @param $haystack 188 | * @param string $message 189 | * @see \Codeception\Module\Asserts::assertNotContains() 190 | */ 191 | public function assertNotContains($needle, $haystack, $message = null) { 192 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotContains', func_get_args())); 193 | } 194 | 195 | 196 | /** 197 | * [!] Method is generated. Documentation taken from corresponding module. 198 | * 199 | * Checks that string match with pattern 200 | * 201 | * @param string $pattern 202 | * @param string $string 203 | * @param string $message 204 | * @see \Codeception\Module\Asserts::assertRegExp() 205 | */ 206 | public function assertRegExp($pattern, $string, $message = null) { 207 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertRegExp', func_get_args())); 208 | } 209 | 210 | 211 | /** 212 | * [!] Method is generated. Documentation taken from corresponding module. 213 | * 214 | * Checks that string not match with pattern 215 | * 216 | * @param string $pattern 217 | * @param string $string 218 | * @param string $message 219 | * @see \Codeception\Module\Asserts::assertNotRegExp() 220 | */ 221 | public function assertNotRegExp($pattern, $string, $message = null) { 222 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotRegExp', func_get_args())); 223 | } 224 | 225 | 226 | /** 227 | * [!] Method is generated. Documentation taken from corresponding module. 228 | * 229 | * Checks that a string starts with the given prefix. 230 | * 231 | * @param string $prefix 232 | * @param string $string 233 | * @param string $message 234 | * @see \Codeception\Module\Asserts::assertStringStartsWith() 235 | */ 236 | public function assertStringStartsWith($prefix, $string, $message = null) { 237 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringStartsWith', func_get_args())); 238 | } 239 | 240 | 241 | /** 242 | * [!] Method is generated. Documentation taken from corresponding module. 243 | * 244 | * Checks that a string doesn't start with the given prefix. 245 | * 246 | * @param string $prefix 247 | * @param string $string 248 | * @param string $message 249 | * @see \Codeception\Module\Asserts::assertStringStartsNotWith() 250 | */ 251 | public function assertStringStartsNotWith($prefix, $string, $message = null) { 252 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringStartsNotWith', func_get_args())); 253 | } 254 | 255 | 256 | /** 257 | * [!] Method is generated. Documentation taken from corresponding module. 258 | * 259 | * Checks that variable is empty. 260 | * 261 | * @param $actual 262 | * @param string $message 263 | * @see \Codeception\Module\Asserts::assertEmpty() 264 | */ 265 | public function assertEmpty($actual, $message = null) { 266 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEmpty', func_get_args())); 267 | } 268 | 269 | 270 | /** 271 | * [!] Method is generated. Documentation taken from corresponding module. 272 | * 273 | * Checks that variable is not empty. 274 | * 275 | * @param $actual 276 | * @param string $message 277 | * @see \Codeception\Module\Asserts::assertNotEmpty() 278 | */ 279 | public function assertNotEmpty($actual, $message = null) { 280 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEmpty', func_get_args())); 281 | } 282 | 283 | 284 | /** 285 | * [!] Method is generated. Documentation taken from corresponding module. 286 | * 287 | * Checks that variable is NULL 288 | * 289 | * @param $actual 290 | * @param string $message 291 | * @see \Codeception\Module\Asserts::assertNull() 292 | */ 293 | public function assertNull($actual, $message = null) { 294 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNull', func_get_args())); 295 | } 296 | 297 | 298 | /** 299 | * [!] Method is generated. Documentation taken from corresponding module. 300 | * 301 | * Checks that variable is not NULL 302 | * 303 | * @param $actual 304 | * @param string $message 305 | * @see \Codeception\Module\Asserts::assertNotNull() 306 | */ 307 | public function assertNotNull($actual, $message = null) { 308 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotNull', func_get_args())); 309 | } 310 | 311 | 312 | /** 313 | * [!] Method is generated. Documentation taken from corresponding module. 314 | * 315 | * Checks that condition is positive. 316 | * 317 | * @param $condition 318 | * @param string $message 319 | * @see \Codeception\Module\Asserts::assertTrue() 320 | */ 321 | public function assertTrue($condition, $message = null) { 322 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertTrue', func_get_args())); 323 | } 324 | 325 | 326 | /** 327 | * [!] Method is generated. Documentation taken from corresponding module. 328 | * 329 | * Checks that the condition is NOT true (everything but true) 330 | * 331 | * @param $condition 332 | * @param string $message 333 | * @see \Codeception\Module\Asserts::assertNotTrue() 334 | */ 335 | public function assertNotTrue($condition, $message = null) { 336 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotTrue', func_get_args())); 337 | } 338 | 339 | 340 | /** 341 | * [!] Method is generated. Documentation taken from corresponding module. 342 | * 343 | * Checks that condition is negative. 344 | * 345 | * @param $condition 346 | * @param string $message 347 | * @see \Codeception\Module\Asserts::assertFalse() 348 | */ 349 | public function assertFalse($condition, $message = null) { 350 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFalse', func_get_args())); 351 | } 352 | 353 | 354 | /** 355 | * [!] Method is generated. Documentation taken from corresponding module. 356 | * 357 | * Checks that the condition is NOT false (everything but false) 358 | * 359 | * @param $condition 360 | * @param string $message 361 | * @see \Codeception\Module\Asserts::assertNotFalse() 362 | */ 363 | public function assertNotFalse($condition, $message = null) { 364 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotFalse', func_get_args())); 365 | } 366 | 367 | 368 | /** 369 | * [!] Method is generated. Documentation taken from corresponding module. 370 | * 371 | * Checks if file exists 372 | * 373 | * @param string $filename 374 | * @param string $message 375 | * @see \Codeception\Module\Asserts::assertFileExists() 376 | */ 377 | public function assertFileExists($filename, $message = null) { 378 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFileExists', func_get_args())); 379 | } 380 | 381 | 382 | /** 383 | * [!] Method is generated. Documentation taken from corresponding module. 384 | * 385 | * Checks if file doesn't exist 386 | * 387 | * @param string $filename 388 | * @param string $message 389 | * @see \Codeception\Module\Asserts::assertFileNotExists() 390 | */ 391 | public function assertFileNotExists($filename, $message = null) { 392 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFileNotExists', func_get_args())); 393 | } 394 | 395 | 396 | /** 397 | * [!] Method is generated. Documentation taken from corresponding module. 398 | * 399 | * @param $expected 400 | * @param $actual 401 | * @param $description 402 | * @see \Codeception\Module\Asserts::assertGreaterOrEquals() 403 | */ 404 | public function assertGreaterOrEquals($expected, $actual, $description = null) { 405 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterOrEquals', func_get_args())); 406 | } 407 | 408 | 409 | /** 410 | * [!] Method is generated. Documentation taken from corresponding module. 411 | * 412 | * @param $expected 413 | * @param $actual 414 | * @param $description 415 | * @see \Codeception\Module\Asserts::assertLessOrEquals() 416 | */ 417 | public function assertLessOrEquals($expected, $actual, $description = null) { 418 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessOrEquals', func_get_args())); 419 | } 420 | 421 | 422 | /** 423 | * [!] Method is generated. Documentation taken from corresponding module. 424 | * 425 | * @param $actual 426 | * @param $description 427 | * @see \Codeception\Module\Asserts::assertIsEmpty() 428 | */ 429 | public function assertIsEmpty($actual, $description = null) { 430 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsEmpty', func_get_args())); 431 | } 432 | 433 | 434 | /** 435 | * [!] Method is generated. Documentation taken from corresponding module. 436 | * 437 | * @param $key 438 | * @param $actual 439 | * @param $description 440 | * @see \Codeception\Module\Asserts::assertArrayHasKey() 441 | */ 442 | public function assertArrayHasKey($key, $actual, $description = null) { 443 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArrayHasKey', func_get_args())); 444 | } 445 | 446 | 447 | /** 448 | * [!] Method is generated. Documentation taken from corresponding module. 449 | * 450 | * @param $key 451 | * @param $actual 452 | * @param $description 453 | * @see \Codeception\Module\Asserts::assertArrayNotHasKey() 454 | */ 455 | public function assertArrayNotHasKey($key, $actual, $description = null) { 456 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArrayNotHasKey', func_get_args())); 457 | } 458 | 459 | 460 | /** 461 | * [!] Method is generated. Documentation taken from corresponding module. 462 | * 463 | * Checks that array contains subset. 464 | * 465 | * @param array $subset 466 | * @param array $array 467 | * @param bool $strict 468 | * @param string $message 469 | * @see \Codeception\Module\Asserts::assertArraySubset() 470 | */ 471 | public function assertArraySubset($subset, $array, $strict = null, $message = null) { 472 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArraySubset', func_get_args())); 473 | } 474 | 475 | 476 | /** 477 | * [!] Method is generated. Documentation taken from corresponding module. 478 | * 479 | * @param $expectedCount 480 | * @param $actual 481 | * @param $description 482 | * @see \Codeception\Module\Asserts::assertCount() 483 | */ 484 | public function assertCount($expectedCount, $actual, $description = null) { 485 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertCount', func_get_args())); 486 | } 487 | 488 | 489 | /** 490 | * [!] Method is generated. Documentation taken from corresponding module. 491 | * 492 | * @param $class 493 | * @param $actual 494 | * @param $description 495 | * @see \Codeception\Module\Asserts::assertInstanceOf() 496 | */ 497 | public function assertInstanceOf($class, $actual, $description = null) { 498 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertInstanceOf', func_get_args())); 499 | } 500 | 501 | 502 | /** 503 | * [!] Method is generated. Documentation taken from corresponding module. 504 | * 505 | * @param $class 506 | * @param $actual 507 | * @param $description 508 | * @see \Codeception\Module\Asserts::assertNotInstanceOf() 509 | */ 510 | public function assertNotInstanceOf($class, $actual, $description = null) { 511 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotInstanceOf', func_get_args())); 512 | } 513 | 514 | 515 | /** 516 | * [!] Method is generated. Documentation taken from corresponding module. 517 | * 518 | * @param $type 519 | * @param $actual 520 | * @param $description 521 | * @see \Codeception\Module\Asserts::assertInternalType() 522 | */ 523 | public function assertInternalType($type, $actual, $description = null) { 524 | return $this->getScenario()->runStep(new \Codeception\Step\Action('assertInternalType', func_get_args())); 525 | } 526 | 527 | 528 | /** 529 | * [!] Method is generated. Documentation taken from corresponding module. 530 | * 531 | * Fails the test with message. 532 | * 533 | * @param $message 534 | * @see \Codeception\Module\Asserts::fail() 535 | */ 536 | public function fail($message) { 537 | return $this->getScenario()->runStep(new \Codeception\Step\Action('fail', func_get_args())); 538 | } 539 | 540 | 541 | /** 542 | * [!] Method is generated. Documentation taken from corresponding module. 543 | * 544 | * Handles and checks exception called inside callback function. 545 | * Either exception class name or exception instance should be provided. 546 | * 547 | * ```php 548 | * expectException(MyException::class, function() { 550 | * $this->doSomethingBad(); 551 | * }); 552 | * 553 | * $I->expectException(new MyException(), function() { 554 | * $this->doSomethingBad(); 555 | * }); 556 | * ``` 557 | * If you want to check message or exception code, you can pass them with exception instance: 558 | * ```php 559 | * expectException(new MyException("Don't do bad things"), function() { 562 | * $this->doSomethingBad(); 563 | * }); 564 | * ``` 565 | * 566 | * @param $exception string or \Exception 567 | * @param $callback 568 | * 569 | * @deprecated Use expectThrowable instead 570 | * @see \Codeception\Module\Asserts::expectException() 571 | */ 572 | public function expectException($exception, $callback) { 573 | return $this->getScenario()->runStep(new \Codeception\Step\Action('expectException', func_get_args())); 574 | } 575 | 576 | 577 | /** 578 | * [!] Method is generated. Documentation taken from corresponding module. 579 | * 580 | * Handles and checks throwables (Exceptions/Errors) called inside the callback function. 581 | * Either throwable class name or throwable instance should be provided. 582 | * 583 | * ```php 584 | * expectThrowable(MyThrowable::class, function() { 586 | * $this->doSomethingBad(); 587 | * }); 588 | * 589 | * $I->expectThrowable(new MyException(), function() { 590 | * $this->doSomethingBad(); 591 | * }); 592 | * ``` 593 | * If you want to check message or throwable code, you can pass them with throwable instance: 594 | * ```php 595 | * expectThrowable(new MyError("Don't do bad things"), function() { 598 | * $this->doSomethingBad(); 599 | * }); 600 | * ``` 601 | * 602 | * @param $throwable string or \Throwable 603 | * @param $callback 604 | * @see \Codeception\Module\Asserts::expectThrowable() 605 | */ 606 | public function expectThrowable($throwable, $callback) { 607 | return $this->getScenario()->runStep(new \Codeception\Step\Action('expectThrowable', func_get_args())); 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /tests/_support/_generated/WpunitTesterActions.php: -------------------------------------------------------------------------------- 1 | getScenario()->runStep(new \Codeception\Step\Action('bootstrapActions', func_get_args())); 24 | } 25 | 26 | 27 | /** 28 | * [!] Method is generated. Documentation taken from corresponding module. 29 | * 30 | * 31 | * @see \Codeception\Module\WPLoader::switchTheme() 32 | */ 33 | public function switchTheme() { 34 | return $this->getScenario()->runStep(new \Codeception\Step\Action('switchTheme', func_get_args())); 35 | } 36 | 37 | 38 | /** 39 | * [!] Method is generated. Documentation taken from corresponding module. 40 | * 41 | * 42 | * @see \Codeception\Module\WPLoader::activatePlugins() 43 | */ 44 | public function activatePlugins() { 45 | return $this->getScenario()->runStep(new \Codeception\Step\Action('activatePlugins', func_get_args())); 46 | } 47 | 48 | 49 | /** 50 | * [!] Method is generated. Documentation taken from corresponding module. 51 | * 52 | * Loads the plugins required by the test. 53 | * @see \Codeception\Module\WPLoader::loadPlugins() 54 | */ 55 | public function loadPlugins() { 56 | return $this->getScenario()->runStep(new \Codeception\Step\Action('loadPlugins', func_get_args())); 57 | } 58 | 59 | 60 | /** 61 | * [!] Method is generated. Documentation taken from corresponding module. 62 | * 63 | * Accessor method to get the object storing the factories for things. 64 | * 65 | * Example usage: 66 | * 67 | * $postId = $I->factory()->post->create(); 68 | * 69 | * @return \tad\WPBrowser\Module\WPLoader\FactoryStore 70 | * @see \Codeception\Module\WPLoader::factory() 71 | */ 72 | public function factory() { 73 | return $this->getScenario()->runStep(new \Codeception\Step\Action('factory', func_get_args())); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for acceptance tests. 4 | # Perform tests in browser using the WPWebDriver or WPBrowser. 5 | # Use WPDb to set up your initial database fixture. 6 | # If you need both WPWebDriver and WPBrowser tests - create a separate suite. 7 | 8 | actor: AcceptanceTester 9 | modules: 10 | enabled: 11 | - WPDb 12 | - WPWebDriver 13 | - \Helper\Acceptance 14 | config: 15 | WPDb: 16 | dsn: 'mysql:host=%TEST_SITE_DB_HOST%:%TEST_SITE_DB_PORT%;dbname=%TEST_SITE_DB_NAME%' 17 | user: '%TEST_SITE_DB_USER%' 18 | password: '%TEST_SITE_DB_PASSWORD%' 19 | dump: 'tests/_data/dump.sql' 20 | #import the dump before the tests; this means the test site database will be repopulated before the tests. 21 | populate: true 22 | # re-import the dump between tests; this means the test site database will be repopulated between the tests. 23 | cleanup: true 24 | waitlock: 0 25 | url: '%TEST_SITE_WP_URL%' 26 | urlReplacement: true #replace the hardcoded dump URL with the one above 27 | tablePrefix: '%TEST_SITE_TABLE_PREFIX%' 28 | # WPBrowser: 29 | # url: '%TEST_SITE_WP_URL%' 30 | # adminUsername: '%TEST_SITE_ADMIN_USERNAME%' 31 | # adminPassword: '%TEST_SITE_ADMIN_PASSWORD%' 32 | # adminPath: '%TEST_SITE_WP_ADMIN_PATH%' 33 | WPWebDriver: 34 | url: '%TEST_SITE_WP_URL%' 35 | adminUsername: '%TEST_SITE_ADMIN_USERNAME%' 36 | adminPassword: '%TEST_SITE_ADMIN_PASSWORD%' 37 | adminPath: '%TEST_SITE_WP_ADMIN_PATH%' 38 | browser: chrome 39 | port: 9515 40 | window_size: false 41 | capabilities: 42 | chromeOptions: 43 | args: ["--headless", "--disable-gpu", "--proxy-server='direct://'", "--proxy-bypass-list=*"] -------------------------------------------------------------------------------- /tests/acceptance/Test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | In order to log in 3 | As a not signed in user 4 | I need to click the Log in button 5 | 6 | Scenario: When a not signed in user clicks the log in button they are sent to the log in page 7 | Given I am on the home page 8 | When I click the log in button 9 | Then I should be on the log in page 10 | -------------------------------------------------------------------------------- /tests/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for functional tests 4 | # Emulate web requests and make WordPress process them 5 | 6 | actor: FunctionalTester 7 | modules: 8 | enabled: 9 | - WPDb 10 | - WPBrowser 11 | - WPFilesystem 12 | - Asserts 13 | - \Helper\Functional 14 | config: 15 | WPDb: 16 | dsn: 'mysql:host=%TEST_SITE_DB_HOST%:%TEST_SITE_DB_PORT%;dbname=%TEST_SITE_DB_NAME%' 17 | user: '%TEST_SITE_DB_USER%' 18 | password: '%TEST_SITE_DB_PASSWORD%' 19 | dump: 'tests/_data/dump.sql' 20 | populate: true 21 | cleanup: true 22 | waitlock: 0 23 | url: '%TEST_SITE_WP_URL%' 24 | urlReplacement: true 25 | tablePrefix: '%TEST_SITE_TABLE_PREFIX%' 26 | WPBrowser: 27 | url: '%TEST_SITE_WP_URL%' 28 | adminUsername: '%TEST_SITE_ADMIN_USERNAME%' 29 | adminPassword: '%TEST_SITE_ADMIN_PASSWORD%' 30 | adminPath: '%TEST_SITE_WP_ADMIN_PATH%' 31 | WPFilesystem: 32 | wpRootFolder: '%WP_ROOT_FOLDER%' 33 | plugins: '/wp-content/plugins' 34 | mu-plugins: '/wp-content/mu-plugins' 35 | themes: '/wp-content/themes' 36 | uploads: '/wp-content/uploads' -------------------------------------------------------------------------------- /tests/functional/AnotherCept.php: -------------------------------------------------------------------------------- 1 | wantTo( 'Testing clicking about' ); 5 | 6 | $i->amOnPage( '/' ); 7 | 8 | $i->click( 'Log in' ); 9 | 10 | $i->amOnPage( '/wp-login.php' ); 11 | -------------------------------------------------------------------------------- /tests/functional/TestCept.php: -------------------------------------------------------------------------------- 1 | wantTo( 'use Chrome for acceptance tests' ); 5 | 6 | $i->havePostInDatabase( [ 7 | 'post_title' => 'Hello World!', 8 | 'post_status' => 'publish', 9 | ] ); 10 | 11 | $i->amOnPage( '/' ); 12 | 13 | $i->see( 'Hello World!' ); 14 | 15 | $i->amOnPage( '/hello-world/' ); 16 | 17 | $i->see( 'then start writing' ); 18 | -------------------------------------------------------------------------------- /tests/functional/_bootstrap.php: -------------------------------------------------------------------------------- 1 |