diff --git a/composer.json b/composer.json index 9a29348..b05b3bf 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,19 @@ "only": [ "wpackagist-plugin/*" ] + }, + { + "type": "package", + "package": { + "name": "pronamic/woocommerce-subscriptions", + "version": "9.2.0", + "type": "wordpress-plugin", + "source": { + "type": "git", + "url": "https://github.com/pronamic/woocommerce-subscriptions.git", + "reference": "0a2c496cd263811707caa37c8bf01ffa450c6009" + } + } } ], "require": { @@ -18,6 +31,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8", "composer/installers": "^2.3", + "pronamic/woocommerce-subscriptions": "^9.2", "roots/wordpress": "^7.0", "wp-phpunit/wp-phpunit": "^7.0", "wpackagist-plugin/woocommerce": "^11.0" diff --git a/composer.lock b/composer.lock index 740387a..c7e816a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "100daeaa732ad02c2952242d18b7d8f1", + "content-hash": "a4c1e95fb5e6b3f2fa1b051e281e06c0", "packages": [], "packages-dev": [ { @@ -210,6 +210,16 @@ ], "time": "2024-06-24T20:46:46+00:00" }, + { + "name": "pronamic/woocommerce-subscriptions", + "version": "9.2.0", + "source": { + "type": "git", + "url": "https://github.com/pronamic/woocommerce-subscriptions.git", + "reference": "0a2c496cd263811707caa37c8bf01ffa450c6009" + }, + "type": "wordpress-plugin" + }, { "name": "roots/wordpress", "version": "7.1.2", diff --git a/functions.php b/functions.php index f593b4c..0579b2f 100644 --- a/functions.php +++ b/functions.php @@ -19,3 +19,4 @@ require_once __DIR__ . '/inc/product.php'; require_once __DIR__ . '/inc/cpf-cnpj.php'; require_once __DIR__ . '/inc/checkout.php'; +require_once __DIR__ . '/inc/onboarding.php'; diff --git a/inc/account.php b/inc/account.php index d779e97..5b54e12 100644 --- a/inc/account.php +++ b/inc/account.php @@ -148,9 +148,8 @@ function libresign_theme_render_lost_password_form() { * - Any other account page → WooCommerce my-account shortcode (which uses * the woocommerce/myaccount/form-login.php template override for guests * and the standard dashboard for logged-in users). - * - Shop / checkout → prepend the SaaS onboarding block pattern. */ -function libresign_theme_prepend_saas_onboarding_to_content( $content ) { +function libresign_theme_render_account_content( $content ) { if ( is_admin() || ! in_the_loop() || ! is_main_query() ) { return $content; } @@ -178,23 +177,9 @@ function libresign_theme_prepend_saas_onboarding_to_content( $content ) { return function_exists( 'do_shortcode' ) ? do_shortcode( '[woocommerce_my_account]' ) : $content; } - $should_prepend = ( function_exists( 'is_shop' ) && is_shop() ) - || ( function_exists( 'is_checkout' ) && is_checkout() ); - - if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) { - $should_prepend = true; - } - - if ( $should_prepend ) { - $onboarding_block = ''; - if ( false === strpos( $content, 'libresign/saas-onboarding' ) ) { - $content = do_blocks( $onboarding_block ) . $content; - } - } - return $content; } -add_filter( 'the_content', 'libresign_theme_prepend_saas_onboarding_to_content', 5 ); +add_filter( 'the_content', 'libresign_theme_render_account_content', 5 ); // --------------------------------------------------------------------------- // Direct /lost-password/ route handler diff --git a/inc/onboarding.php b/inc/onboarding.php new file mode 100644 index 0000000..664e12b --- /dev/null +++ b/inc/onboarding.php @@ -0,0 +1,27 @@ + + + +
+ +

+ + + +
+ +
+ +

+ + + +

+ +
+ + + +
+ +

+ + + +

+ +
+ + + +
+ +

+ + + +

+ +
+ +
+ +
+ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4480303..722a7a6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -15,6 +15,9 @@ parameters: bootstrapFiles: - vendor-bin/phpstan/vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php - vendor-bin/phpunit/vendor/autoload.php + scanFiles: + - vendor/test-plugins/woocommerce-subscriptions/includes/core/wcs-functions.php + - vendor/test-plugins/woocommerce-subscriptions/includes/core/abstracts/abstract-wcs-related-order-store.php scanDirectories: - vendor/wp-phpunit/wp-phpunit/includes ignoreErrors: diff --git a/readme.txt b/readme.txt index 337b45d..dee67d7 100644 --- a/readme.txt +++ b/readme.txt @@ -67,8 +67,8 @@ test under `tests/E2E` follows the same names. `tests/Unit/StructureTest.php` enforces this and also fails on a test whose file no longer exists. In CI, octocov fails the run when line coverage is below 90% or below the last report of `main` (`.octocov.yml`). -`composer install` brings in WordPress, the WordPress test suite and WooCommerce, -so the tests only need a MySQL/MariaDB database they are allowed to wipe on every +`composer install` brings in WordPress, the WordPress test suite, WooCommerce and +WooCommerce Subscriptions, so the tests only need a MySQL/MariaDB database they are allowed to wipe on every run. The connection comes from `WP_TESTS_DB_NAME` (`wordpress_test`), `WP_TESTS_DB_USER` (`root`), `WP_TESTS_DB_PASSWORD` (`root`), `WP_TESTS_DB_HOST` (`mariadb`) and `WP_TESTS_TABLE_PREFIX` (`wptests_`). @@ -82,7 +82,8 @@ On the local SaaS stack: wordpress-docker-wordpress-1 composer test The browser tests in `tests/E2E` cover creating a workspace together with a plan, -the CPF/CNPJ field and the policy consent at checkout, and the lost password page. +the CPF/CNPJ field and the policy consent at checkout, the lost password page, +and the onboarding on the shop, checkout and order received pages. They need Docker, Node.js and a `composer install`, since the stack mounts WooCommerce from `vendor/test-plugins`: diff --git a/templates/archive-product.html b/templates/archive-product.html index d546814..8b6e6b6 100644 --- a/templates/archive-product.html +++ b/templates/archive-product.html @@ -2,6 +2,8 @@
+ + diff --git a/templates/page-checkout.html b/templates/page-checkout.html index 92209f6..72566c6 100644 --- a/templates/page-checkout.html +++ b/templates/page-checkout.html @@ -2,6 +2,7 @@
+
diff --git a/tests/E2E/Patterns/SaasOnboarding.spec.ts b/tests/E2E/Patterns/SaasOnboarding.spec.ts new file mode 100644 index 0000000..8199771 --- /dev/null +++ b/tests/E2E/Patterns/SaasOnboarding.spec.ts @@ -0,0 +1,41 @@ +import { expect, Page, test } from '@playwright/test'; + +import { addThePlanToTheCart, fillTheBillingAddress, placeOrder } from '../support/store'; + +async function expectTheOnboarding( page: Page ): Promise< void > { + await expect( page.getByRole( 'heading', { name: 'How your LibreSign workspace works' } ) ).toHaveCount( 1 ); + await expect( page.getByRole( 'heading', { name: '1. Choose a plan' } ) ).toBeVisible(); + await expect( page.getByRole( 'heading', { name: '2. Create your workspace' } ) ).toBeVisible(); + await expect( page.getByRole( 'heading', { name: '3. Start signing' } ) ).toBeVisible(); +} + +test.describe( 'Showing how the workspace works while buying', () => { + test( 'on the shop', async ( { page } ) => { + await page.goto( '/shop/' ); + + await expectTheOnboarding( page ); + } ); + + test( 'on the checkout', async ( { page } ) => { + await addThePlanToTheCart( page ); + await page.goto( '/checkout/' ); + + await expectTheOnboarding( page ); + } ); + + test( 'on the order received', async ( { page } ) => { + await addThePlanToTheCart( page ); + await fillTheBillingAddress( page, 'Brazil', '529.982.247-25' ); + await page.getByLabel( 'I agree to the terms and privacy policy before placing the order.' ).check(); + await placeOrder( page ); + + await expect( page ).toHaveURL( /\/order-received\// ); + await expectTheOnboarding( page ); + } ); + + test( 'not on other pages', async ( { page } ) => { + await page.goto( '/product/basic/' ); + + await expect( page.getByRole( 'heading', { name: 'How your LibreSign workspace works' } ) ).toHaveCount( 0 ); + } ); +} ); diff --git a/tests/Integration/Inc/AccountTest.php b/tests/Integration/Inc/AccountTest.php index 2be5d3f..a43bc64 100644 --- a/tests/Integration/Inc/AccountTest.php +++ b/tests/Integration/Inc/AccountTest.php @@ -165,14 +165,14 @@ public function test_the_account_page_leaves_the_reset_confirmation_to_woocommer public function test_keeps_the_content_of_other_pages() { $this->in_the_loop_of( home_url() ); - $this->assertSame( '

Page

', libresign_theme_prepend_saas_onboarding_to_content( '

Page

' ) ); + $this->assertSame( '

Page

', libresign_theme_render_account_content( '

Page

' ) ); } public function test_keeps_the_content_outside_the_loop() { add_filter( 'woocommerce_is_account_page', '__return_true' ); $this->go_to( home_url() ); - $this->assertSame( '

Page

', libresign_theme_prepend_saas_onboarding_to_content( '

Page

' ) ); + $this->assertSame( '

Page

', libresign_theme_render_account_content( '

Page

' ) ); } public function test_keeps_the_content_in_the_admin() { @@ -180,7 +180,7 @@ public function test_keeps_the_content_in_the_admin() { $this->in_the_loop_of( home_url() ); set_current_screen( 'dashboard' ); - $this->assertSame( '

Page

', libresign_theme_prepend_saas_onboarding_to_content( '

Page

' ) ); + $this->assertSame( '

Page

', libresign_theme_render_account_content( '

Page

' ) ); } /** @@ -206,7 +206,7 @@ private function account_page_content( $query = array() ) { add_filter( 'woocommerce_is_account_page', '__return_true' ); $this->in_the_loop_of( add_query_arg( $query, home_url( '/' ) ) ); - return libresign_theme_prepend_saas_onboarding_to_content( '' ); + return libresign_theme_render_account_content( '' ); } private function in_the_loop_of( $url ) { diff --git a/tests/Integration/Inc/OnboardingTest.php b/tests/Integration/Inc/OnboardingTest.php new file mode 100644 index 0000000..9e0c337 --- /dev/null +++ b/tests/Integration/Inc/OnboardingTest.php @@ -0,0 +1,93 @@ +store = new StoreFactory(); + } + + public function tear_down() { + unset( $GLOBALS['wp']->query_vars['order-pay'], $GLOBALS['wp']->query_vars['order-received'] ); + set_query_var( 'order-received', '' ); + + parent::tear_down(); + } + + public function test_shows_the_onboarding_while_buying() { + $this->assertStringContainsString( self::HEADING, $this->onboarding() ); + } + + public function test_shows_the_onboarding_when_a_new_order_is_received() { + $this->receive( $this->order() ); + + $this->assertStringContainsString( self::HEADING, $this->onboarding() ); + } + + public function test_hides_the_onboarding_when_paying_an_existing_order() { + $GLOBALS['wp']->query_vars['order-pay'] = (string) $this->order()->get_id(); + + $this->assertSame( '', trim( $this->onboarding() ) ); + } + + /** + * @dataProvider provide_orders_of_existing_subscriptions + */ + public function test_hides_the_onboarding_when_an_order_of_an_existing_subscription_is_received( $relation ) { + $order = $this->order(); + WCS_Related_Order_Store::instance()->add_relation( $order, $this->subscription(), $relation ); + $this->receive( $order ); + + $this->assertSame( '', trim( $this->onboarding() ) ); + } + + public static function provide_orders_of_existing_subscriptions() { + yield 'a renewal' => array( 'renewal' ); + yield 'a switch' => array( 'switch' ); + yield 'a resubscribe' => array( 'resubscribe' ); + } + + public function test_keeps_other_groups_while_hiding_the_onboarding() { + $GLOBALS['wp']->query_vars['order-pay'] = (string) $this->order()->get_id(); + + $this->assertStringContainsString( 'Other group', do_blocks( '

Other group

' ) ); + } + + private function onboarding() { + return do_blocks( '' ); + } + + private function order() { + $order = wc_create_order(); + $order->add_product( $this->store->plan() ); + $order->save(); + + return $order; + } + + private function subscription() { + return wcs_create_subscription( + array( + 'customer_id' => self::factory()->user->create(), + 'billing_period' => 'month', + 'billing_interval' => 1, + ) + ); + } + + private function receive( $order ) { + $GLOBALS['wp']->query_vars['order-received'] = (string) $order->get_id(); + set_query_var( 'order-received', (string) $order->get_id() ); + } +} diff --git a/tests/Integration/Patterns/SaasOnboardingTest.php b/tests/Integration/Patterns/SaasOnboardingTest.php new file mode 100644 index 0000000..1186830 --- /dev/null +++ b/tests/Integration/Patterns/SaasOnboardingTest.php @@ -0,0 +1,30 @@ +assertTrue( \WP_Block_Patterns_Registry::get_instance()->is_registered( 'libresign/saas-onboarding' ) ); + } + + /** + * @dataProvider provide_templates_of_the_purchase + */ + public function test_the_template_shows_the_onboarding( $template ) { + $html = do_blocks( get_block_template( get_stylesheet() . '//' . $template )->content ); + + $this->assertSame( 1, substr_count( $html, 'How your LibreSign workspace works' ) ); + $this->assertStringContainsString( '1. Choose a plan', $html ); + $this->assertStringContainsString( '2. Create your workspace', $html ); + $this->assertStringContainsString( '3. Start signing', $html ); + } + + public static function provide_templates_of_the_purchase() { + yield 'the shop' => array( 'archive-product' ); + yield 'the checkout' => array( 'page-checkout' ); + yield 'the order received' => array( 'order-confirmation' ); + } +} diff --git a/tests/bootstrap-integration.php b/tests/bootstrap-integration.php index 8663c88..1e95a58 100644 --- a/tests/bootstrap-integration.php +++ b/tests/bootstrap-integration.php @@ -32,6 +32,7 @@ 'muplugins_loaded', static function () use ( $test_plugins_dir ) { require $test_plugins_dir . '/woocommerce/woocommerce.php'; + require $test_plugins_dir . '/woocommerce-subscriptions/woocommerce-subscriptions.php'; register_theme_directory( dirname( __DIR__, 2 ) ); } ); @@ -39,7 +40,10 @@ static function () use ( $test_plugins_dir ) { tests_add_filter( 'pre_option_active_plugins', static function () { - return array( 'woocommerce/woocommerce.php' ); + return array( + 'woocommerce/woocommerce.php', + 'woocommerce-subscriptions/woocommerce-subscriptions.php', + ); } );