cleanup(); $this->current_time = Carbon::createFromTimestamp((new WPFunctions())->currentTime('timestamp')); Carbon::setTestNow($this->current_time); $this->wp = $this->makeEmpty(WPFunctions::class, [ 'currentTime' => $this->current_time->getTimestamp(), ]); WPFunctions::set($this->wp); $this->woo_commerce_mock = $this->mockWooCommerceClass(WooCommerce::class, []); $this->woo_commerce_cart_mock = $this->mockWooCommerceClass(WC_Cart::class, ['is_empty']); $this->woo_commerce_mock->cart = $this->woo_commerce_cart_mock; $this->woo_commerce_helper_mock = $this->make(WooCommerceHelper::class, [ 'isWooCommerceActive' => true, 'WC' => $this->woo_commerce_mock, ]); $this->page_visit_tracker_mock = $this->makeEmpty(AbandonedCartPageVisitTracker::class); } function testItGetsEventDetails() { $event = new AbandonedCart(); $result = $event->getEventDetails(); expect($result)->notEmpty(); expect($result['slug'])->equals(AbandonedCart::SLUG); } function testItRegistersWooCommerceCartEvents() { $abandoned_cart_email = $this->createAbandonedCartEmail(); $registered_actions = []; $this->wp->method('addAction')->willReturnCallback(function ($name) use (&$registered_actions) { $registered_actions[] = $name; }); $abandoned_cart_email->init(); expect($registered_actions)->contains('woocommerce_add_to_cart'); expect($registered_actions)->contains('woocommerce_cart_item_removed'); expect($registered_actions)->contains('woocommerce_after_cart_item_quantity_update'); expect($registered_actions)->contains('woocommerce_before_cart_item_quantity_zero'); expect($registered_actions)->contains('woocommerce_cart_emptied'); expect($registered_actions)->contains('woocommerce_cart_item_restored'); } function testItRegistersPageVisitEvent() { $abandoned_cart_email = $this->createAbandonedCartEmail(); $registered_actions = []; $this->wp->method('addAction')->willReturnCallback(function ($name) use (&$registered_actions) { $registered_actions[] = $name; }); $abandoned_cart_email->init(); expect($registered_actions)->contains('wp'); } function testItFindsUserByWordPressSession() { $this->createNewsletter(); $this->createSubscriberAsCurrentUser(); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(false); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->handleCartChange(); expect(ScheduledTask::findMany())->count(1); } function testItFindsUserByCookie() { $this->createNewsletter(); $subscriber = $this->createSubscriber(); $this->wp->method('wpGetCurrentUser')->willReturn( $this->makeEmpty(WP_User::class, [ 'exists' => false, ]) ); $_COOKIE['mailpoet_abandoned_cart_tracking'] = json_encode([ 'subscriber_id' => $subscriber->id, ]); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(false); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->handleCartChange(); expect(ScheduledTask::findMany())->count(1); } function testItSchedulesEmailWhenItemAddedToCart() { $this->createNewsletter(); $this->createSubscriberAsCurrentUser(); // ensure tracking started $this->page_visit_tracker_mock->expects($this->once())->method('startTracking'); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(false); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->handleCartChange(); $expected_time = $this->getExpectedScheduledTime(); $scheduled_tasks = ScheduledTask::findMany(); expect($scheduled_tasks)->count(1); expect($scheduled_tasks[0]->status)->same(ScheduledTask::STATUS_SCHEDULED); expect($scheduled_tasks[0]->scheduled_at)->same($expected_time->format('Y-m-d H:i:s')); } function testItPostponesEmailWhenCartEdited() { $newsletter = $this->createNewsletter(); $subscriber = $this->createSubscriberAsCurrentUser(); $scheduled_in_near_future = clone $this->current_time; $scheduled_in_near_future->addMinutes(5); $this->createSendingTask($newsletter, $subscriber, $scheduled_in_near_future); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(false); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->handleCartChange(); $expected_time = $this->getExpectedScheduledTime(); $scheduled_tasks = ScheduledTask::findMany(); expect($scheduled_tasks)->count(1); expect($scheduled_tasks[0]->status)->same(ScheduledTask::STATUS_SCHEDULED); expect($scheduled_tasks[0]->scheduled_at)->same($expected_time->format('Y-m-d H:i:s')); } function testItCancelsEmailWhenCartEmpty() { $newsletter = $this->createNewsletter(); $subscriber = $this->createSubscriberAsCurrentUser(); $scheduled_in_future = clone $this->current_time; $scheduled_in_future->addHours(2); $this->createSendingTask($newsletter, $subscriber, $scheduled_in_future); // ensure tracking cancelled $this->page_visit_tracker_mock->expects($this->once())->method('stopTracking'); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(true); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->handleCartChange(); expect(ScheduledTask::findMany())->count(0); expect(ScheduledTaskSubscriber::findMany())->count(0); expect(SendingQueue::findMany())->count(0); } function testItSchedulesNewEmailWhenEmailAlreadySent() { $newsletter = $this->createNewsletter(); $subscriber = $this->createSubscriberAsCurrentUser(); $scheduled_in_past = clone $this->current_time; $scheduled_in_past->addHours(-10); $this->createSendingTask($newsletter, $subscriber, $scheduled_in_past); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(false); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->handleCartChange(); $expected_time = $this->getExpectedScheduledTime(); expect(ScheduledTask::findMany())->count(2); $completed = ScheduledTask::where('status', ScheduledTask::STATUS_COMPLETED)->findOne(); expect($completed->scheduled_at)->same($scheduled_in_past->format('Y-m-d H:i:s')); $scheduled = ScheduledTask::where('status', ScheduledTask::STATUS_SCHEDULED)->findOne(); expect($scheduled->scheduled_at)->same($expected_time->format('Y-m-d H:i:s')); } function testItPostponesEmailWhenPageVisited() { $newsletter = $this->createNewsletter(); $subscriber = $this->createSubscriberAsCurrentUser(); $scheduled_in_near_future = clone $this->current_time; $scheduled_in_near_future->addMinutes(5); $this->createSendingTask($newsletter, $subscriber, $scheduled_in_near_future); // ensure last visit timestamp updated & execute tracking callback $this->page_visit_tracker_mock ->expects($this->once()) ->method('trackVisit') ->willReturnCallback(function (callable $onTrackCallback) { $onTrackCallback(); }); $this->woo_commerce_cart_mock->method('is_empty')->willReturn(false); $abandoned_cart_email = $this->createAbandonedCartEmail(); $abandoned_cart_email->init(); $abandoned_cart_email->trackPageVisit(); $expected_time = $this->getExpectedScheduledTime(); $scheduled_tasks = ScheduledTask::findMany(); expect($scheduled_tasks)->count(1); expect($scheduled_tasks[0]->status)->same(ScheduledTask::STATUS_SCHEDULED); expect($scheduled_tasks[0]->scheduled_at)->same($expected_time->format('Y-m-d H:i:s')); } private function createAbandonedCartEmail() { return $this->make(AbandonedCart::class, [ 'wp' => $this->wp, 'woo_commerce_helper' => $this->woo_commerce_helper_mock, 'cookies' => new Cookies(), 'page_visit_tracker' => $this->page_visit_tracker_mock, 'scheduler' => new AutomaticEmailScheduler(), ]); } private function createNewsletter() { $newsletter = Newsletter::create(); $newsletter->type = Newsletter::TYPE_AUTOMATIC; $newsletter->status = Newsletter::STATUS_ACTIVE; $newsletter->save(); $this->createNewsletterOptions($newsletter, [ 'group' => WooCommerceEmail::SLUG, 'event' => AbandonedCart::SLUG, 'afterTimeType' => 'hours', 'afterTimeNumber' => self::SCHEDULE_EMAIL_AFTER_HOURS, 'sendTo' => 'user', ]); return $newsletter; } private function createSendingTask(Newsletter $newsletter, Subscriber $subscriber, Carbon $schedule_at) { $task = SendingTask::create(); $task->newsletter_id = $newsletter->id; $task->setSubscribers([$subscriber->id]); $task->updateProcessedSubscribers([$subscriber->id]); $task->save(); $scheduled_task = $task->task(); $scheduled_task->scheduled_at = $schedule_at; $scheduled_task->status = $this->current_time < $schedule_at ? ScheduledTask::STATUS_SCHEDULED : ScheduledTask::STATUS_COMPLETED; $scheduled_task->save(); return $task; } private function createNewsletterOptions(Newsletter $newsletter, array $options) { foreach ($options as $option => $value) { $newsletter_option_field = NewsletterOptionField::where('name', $option) ->where('newsletter_type', $newsletter->type) ->findOne(); if (!$newsletter_option_field) { $newsletter_option_field = NewsletterOptionField::create(); $newsletter_option_field->hydrate([ 'newsletter_type' => $newsletter->type, 'name' => $option, ]); $newsletter_option_field->save(); } $newsletter_option = NewsletterOption::where('newsletter_id', $newsletter->id) ->where('option_field_id', $newsletter_option_field->id) ->findOne(); if (!$newsletter_option) { $newsletter_option = NewsletterOption::create(); $newsletter_option->hydrate([ 'newsletter_id' => $newsletter->id, 'option_field_id' => $newsletter_option_field->id, 'value' => $value, ]); $newsletter_option->save(); } } } private function createSubscriber() { $subscriber = Subscriber::create(); $subscriber->status = Subscriber::STATUS_SUBSCRIBED; $subscriber->email = 'subscriber@example.com'; $subscriber->first_name = 'First'; $subscriber->last_name = 'Last'; $subscriber->wp_user_id = 123; return $subscriber->save(); } private function createSubscriberAsCurrentUser() { $subscriber = $this->createSubscriber(); $this->wp->method('wpGetCurrentUser')->willReturn( $this->makeEmpty(WP_User::class, [ 'ID' => $subscriber->wp_user_id, 'exists' => true, ]) ); return $subscriber; } private function getExpectedScheduledTime() { $expected_time = clone $this->current_time; $expected_time->addHours(self::SCHEDULE_EMAIL_AFTER_HOURS); return $expected_time; } private function mockWooCommerceClass($class_name, array $methods) { // WooCommerce class needs to be mocked without default 'disallowMockingUnknownTypes' // since WooCommerce may not be active (would result in error mocking undefined class) return $this->getMockBuilder($class_name) ->disableOriginalConstructor() ->disableOriginalClone() ->disableArgumentCloning() ->setMethods($methods) ->getMock(); } private function cleanup() { ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); ORM::raw_execute('TRUNCATE ' . Newsletter::$_table); ORM::raw_execute('TRUNCATE ' . NewsletterOption::$_table); ORM::raw_execute('TRUNCATE ' . NewsletterOptionField::$_table); ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table); ORM::raw_execute('TRUNCATE ' . ScheduledTask::$_table); ORM::raw_execute('TRUNCATE ' . ScheduledTaskSubscriber::$_table); } function _after() { WPFunctions::set(new WPFunctions()); Carbon::setTestNow(); $this->cleanup(); } }