Doubles
1 week ago
Providers
1 week ago
BulkRearmOnGrowthTest.php
1 week ago
BulkReviewsUpdateStuckStateTest.php
1 week ago
ClearCacheRelayResetTest.php
1 week ago
DeleteSourceRelayFailureTest.php
1 week ago
ErrorHandlerFalsyOptionTest.php
1 week ago
FeedCacheUpdateServiceTest.php
1 week ago
FeedMalformedPayloadTest.php
1 week ago
ForceKeylessRefetchTest.php
1 week ago
LicenseDeactivateStaleStateTest.php
1 week ago
MediaFinderMemoTest.php
1 week ago
MultiSourceAggregationTest.php
1 week ago
ReconcileMigratedLicenseRoutineTest.php
1 week ago
ReconcileRemovalTest.php
1 week ago
RegisterWebsiteRoutineTest.php
1 week ago
RelaySlowEndpointsTest.php
1 week ago
RemoteRequestMemoTest.php
1 week ago
ReviewAlertHeaderTotalsTest.php
1 week ago
ReviewAlertPageTargetingTest.php
1 week ago
ReviewAlertStarFillTest.php
1 week ago
ShortcodeNeutralizationTest.php
1 week ago
SiteMigrationRecoveryTest.php
1 week ago
Smash1130UsageTrackingHardeningTest.php
1 week ago
Smash1130UsageTrackingHooksTest.php
1 week ago
Smash1583HeaderParityTest.php
1 week ago
Smash1631MultiLanguageBulkTest.php
1 week ago
Smash1631UpdateSingleLangScopeTest.php
1 week ago
Smash1706TripAdvisorPlaceIdTest.php
1 week ago
Smash1756SchemaServiceTest.php
1 week ago
Smash1785AvatarLocalUrlGuardTest.php
1 week ago
Smash1785AvatarReHealTest.php
1 week ago
Smash1795ReviewTextXssTest.php
1 week ago
Smash1835TripAdvisorKeyShapeTest.php
1 week ago
Smash1973WordpressOrgPlaceIdNullTest.php
1 week ago
Smash1987NestedSourceErrorShapeTest.php
1 week ago
Smash782BookingHeaderRatingTest.php
1 week ago
Smash782CountryFlagEmojiTest.php
1 week ago
Smash782ExternalRefreshCronTest.php
1 week ago
Smash782ExtrasTemplateTest.php
1 week ago
Smash782ReviewAlertProviderDataTest.php
1 week ago
SourceIdLookupTest.php
1 week ago
WpmlGetCurrentLanguageTest.php
1 week ago
WpmlLanguageMappingTest.php
1 week ago
Smash782ReviewAlertProviderDataTest.php
242 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SmashBalloon\Reviews\Tests\Unit; |
| 4 | |
| 5 | use PHPUnit\Framework\TestCase; |
| 6 | use SmashBalloon\Reviews\Common\ReviewAlerts\SBR_Review_Alert_Frontend; |
| 7 | |
| 8 | /** |
| 9 | * SMASH-782 — Review Alert provider-data pass-through (data layer). |
| 10 | * |
| 11 | * The alert popup renders the same provider-specific elements the feed does |
| 12 | * (Booking pros/cons + 0-10 score + helpful + photos, AliExpress variants / |
| 13 | * translated / buyer-flag / followup, Airbnb reply). Those all live in the |
| 14 | * review's `metadata` / `reply` / `title` / `reviewer_photos`, but the frontend |
| 15 | * formatter previously emitted only a whitelist (id/text/rating/time/reviewer/ |
| 16 | * provider), so the popup never received them. This guards that the formatter |
| 17 | * now forwards the provider shape (additive — the original keys are unchanged). |
| 18 | * |
| 19 | * Reflection is used because format_reviews_for_frontend() is private and reads |
| 20 | * only its argument (no WP/DB state). |
| 21 | */ |
| 22 | final class Smash782ReviewAlertProviderDataTest extends TestCase |
| 23 | { |
| 24 | /** @param array<int,array<string,mixed>> $reviews */ |
| 25 | private function format(array $reviews): array |
| 26 | { |
| 27 | $fe = (new \ReflectionClass(SBR_Review_Alert_Frontend::class))->newInstanceWithoutConstructor(); |
| 28 | $m = new \ReflectionMethod($fe, 'format_reviews_for_frontend'); |
| 29 | $m->setAccessible(true); |
| 30 | return $m->invoke($fe, $reviews); |
| 31 | } |
| 32 | |
| 33 | public function test_booking_score_and_word_are_resolved_server_side_from_the_raw_rating(): void |
| 34 | { |
| 35 | // The trap this pins: `rating` is cast to int for the star renderer, so a JS |
| 36 | // mirror that doubled it would publish 8.0 for a 4.5-star review while the |
| 37 | // feed card, the popup template and the JSON-LD all say 9.0. The formatter |
| 38 | // resolves the pair from the RAW rating before that cast happens. |
| 39 | $out = $this->format([[ |
| 40 | 'review_id' => 'b-half', |
| 41 | 'text' => 'Lovely', |
| 42 | 'rating' => 4.5, |
| 43 | 'reviewer' => ['name' => 'Eva', 'avatar' => ''], |
| 44 | 'provider' => ['name' => 'booking'], |
| 45 | ]]); |
| 46 | $r = $out[0]; |
| 47 | |
| 48 | $this->assertSame(9.0, $r['bookingScore']); |
| 49 | $this->assertSame('Superb', $r['bookingScoreWord']); |
| 50 | // The int cast on `rating` stays — the star renderer depends on it. |
| 51 | $this->assertSame(4, $r['rating']); |
| 52 | } |
| 53 | |
| 54 | public function test_booking_score_ignores_the_property_wide_metadata_score(): void |
| 55 | { |
| 56 | // metadata.review_score is the HOTEL's score, identical on every card. Reading |
| 57 | // it here is what made the popup show one score no matter who was cycled in. |
| 58 | $out = $this->format([[ |
| 59 | 'review_id' => 'b2', |
| 60 | 'text' => 'Fine', |
| 61 | 'rating' => 4, |
| 62 | 'reviewer' => ['name' => 'Dan', 'avatar' => ''], |
| 63 | 'provider' => ['name' => 'booking'], |
| 64 | 'metadata' => ['review_score' => 9.5, 'review_score_word' => 'Exceptional'], |
| 65 | ]]); |
| 66 | $r = $out[0]; |
| 67 | |
| 68 | $this->assertSame(8.0, $r['bookingScore']); |
| 69 | $this->assertSame('Very good', $r['bookingScoreWord']); |
| 70 | } |
| 71 | |
| 72 | public function test_preview_and_frontend_resolve_a_booking_score_from_the_same_basis(): void |
| 73 | { |
| 74 | // The builder preview computes the badge itself (SbUtils.bookingReviewScore → |
| 75 | // rating × 2) because there is no PHP round-trip in the editor, so it needs the |
| 76 | // same rating the frontend resolves from. get_preview_reviews() used to ship |
| 77 | // `(int) $rating`: a 4.5 review rendered "8.0 Very good" in the popup editor and |
| 78 | // "9.0 Superb" in the live popup. Ten reviewers in the local cache sit at 4.5, so |
| 79 | // this was live, not hypothetical. |
| 80 | // |
| 81 | // Reflection on the shaping expression only — get_preview_reviews() itself needs |
| 82 | // the Feed/DB stack. What this pins is that the two sides agree on the number. |
| 83 | $review = [ |
| 84 | 'review_id' => 'b-half', |
| 85 | 'text' => 'Lovely', |
| 86 | 'rating' => 4.5, |
| 87 | 'reviewer' => ['name' => 'Adrian', 'avatar' => ''], |
| 88 | 'provider' => ['name' => 'booking'], |
| 89 | ]; |
| 90 | |
| 91 | $frontend = $this->format([$review])[0]; |
| 92 | $this->assertSame(9.0, $frontend['bookingScore'], 'frontend basis'); |
| 93 | $this->assertSame('Superb', $frontend['bookingScoreWord']); |
| 94 | |
| 95 | // The preview ships the rating itself; both must double to the same score. |
| 96 | $preview_rating = $this->preview_rating_for('booking', $review); |
| 97 | $this->assertSame(4.5, $preview_rating, 'preview must not truncate a Booking rating'); |
| 98 | $this->assertSame( |
| 99 | $frontend['bookingScore'], |
| 100 | sbr_booking_review_score(['rating' => $preview_rating]), |
| 101 | 'preview and frontend must land on the same 0-10 score' |
| 102 | ); |
| 103 | |
| 104 | // Non-Booking keeps the int cast the star renderer depends on. |
| 105 | $this->assertSame(4, $this->preview_rating_for('google', ['rating' => 4.5])); |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Mirror of the rating expression in SBR_Review_Alert_Service::get_preview_reviews(). |
| 110 | * Kept in step by the guard below, which fails if that line stops sending a float. |
| 111 | * |
| 112 | * @param array<string,mixed> $review |
| 113 | * @return float|int |
| 114 | */ |
| 115 | private function preview_rating_for(string $provider, array $review) |
| 116 | { |
| 117 | return 'booking' === $provider |
| 118 | ? (float) ($review['rating'] ?? 0) |
| 119 | : (int) $review['rating']; |
| 120 | } |
| 121 | |
| 122 | public function test_the_preview_payload_sends_booking_ratings_unrounded(): void |
| 123 | { |
| 124 | // Guards the mirror above: get_preview_reviews() needs the Feed/DB stack, so |
| 125 | // there is no DB-free way to call it and the source is what gets pinned. Without |
| 126 | // this, dropping the Booking branch leaves the test above green while the preview |
| 127 | // silently drifts a whole band from the live popup. |
| 128 | // |
| 129 | // The CAST is the load-bearing part, not just the branch — `(int)` inside the |
| 130 | // same ternary would truncate exactly as before — so the pattern requires it. |
| 131 | $src = (string) file_get_contents( |
| 132 | dirname(__DIR__, 2) . '/class/Common/ReviewAlerts/SBR_Review_Alert_Service.php' |
| 133 | ); |
| 134 | |
| 135 | $this->assertMatchesRegularExpression( |
| 136 | "/'rating'\s*=>\s*'booking'\s*===\s*\\\$review_provider\s*\?\s*\(float\)/", |
| 137 | $src, |
| 138 | 'get_preview_reviews() must send Booking ratings as a float — see the test above.' |
| 139 | ); |
| 140 | // And nothing may re-truncate them on the Booking arm. |
| 141 | $this->assertDoesNotMatchRegularExpression( |
| 142 | "/'rating'\s*=>\s*'booking'\s*===\s*\\\$review_provider\s*\?\s*\(int\)/", |
| 143 | $src |
| 144 | ); |
| 145 | } |
| 146 | |
| 147 | public function test_non_booking_reviews_carry_no_booking_score_keys(): void |
| 148 | { |
| 149 | $out = $this->format([[ |
| 150 | 'review_id' => 'g1', |
| 151 | 'text' => 'Nice', |
| 152 | 'rating' => 5, |
| 153 | 'reviewer' => ['name' => 'Gina', 'avatar' => ''], |
| 154 | 'provider' => ['name' => 'google'], |
| 155 | ]]); |
| 156 | |
| 157 | $this->assertArrayNotHasKey('bookingScore', $out[0]); |
| 158 | $this->assertArrayNotHasKey('bookingScoreWord', $out[0]); |
| 159 | } |
| 160 | |
| 161 | public function test_booking_review_forwards_metadata_title_and_photos(): void |
| 162 | { |
| 163 | $out = $this->format([[ |
| 164 | 'review_id' => 'b1', |
| 165 | 'text' => 'Great stay', |
| 166 | 'title' => 'It was excellent', |
| 167 | 'rating' => 5, |
| 168 | 'reviewer' => ['name' => 'Alison', 'avatar' => ''], |
| 169 | 'provider' => ['name' => 'booking'], |
| 170 | 'metadata' => [ |
| 171 | 'pros' => 'Spacious room', |
| 172 | 'cons' => 'Pricey breakfast', |
| 173 | 'review_score' => 8.6, |
| 174 | 'review_score_word' => 'Fabulous', |
| 175 | 'helpful_vote_count' => 14, |
| 176 | ], |
| 177 | 'reviewer_photos' => [['90_90' => 'https://x/a.jpg']], |
| 178 | 'source' => ['id' => '1377073'], |
| 179 | ]]); |
| 180 | $r = $out[0]; |
| 181 | $this->assertSame('It was excellent', $r['title']); |
| 182 | $this->assertSame('Spacious room', $r['metadata']['pros']); |
| 183 | $this->assertSame('Pricey breakfast', $r['metadata']['cons']); |
| 184 | $this->assertSame(8.6, $r['metadata']['review_score']); |
| 185 | $this->assertSame(14, $r['metadata']['helpful_vote_count']); |
| 186 | $this->assertNotEmpty($r['reviewer_photos']); |
| 187 | $this->assertSame('1377073', $r['source']['id']); |
| 188 | } |
| 189 | |
| 190 | public function test_aliexpress_review_forwards_variants_translated_flag(): void |
| 191 | { |
| 192 | $out = $this->format([[ |
| 193 | 'text' => 'Nice shirt', |
| 194 | 'rating' => 4, |
| 195 | 'reviewer' => ['name' => 'Shopper'], |
| 196 | 'provider' => ['name' => 'aliexpress'], |
| 197 | 'metadata' => [ |
| 198 | 'item_spec' => 'Color:Black Size:XL', |
| 199 | 'translated' => true, |
| 200 | 'buyer_country' => 'US', |
| 201 | ], |
| 202 | ]]); |
| 203 | $md = $out[0]['metadata']; |
| 204 | $this->assertSame('Color:Black Size:XL', $md['item_spec']); |
| 205 | $this->assertTrue($md['translated']); |
| 206 | $this->assertSame('US', $md['buyer_country']); |
| 207 | } |
| 208 | |
| 209 | public function test_airbnb_review_forwards_reply(): void |
| 210 | { |
| 211 | $out = $this->format([[ |
| 212 | 'text' => 'Lovely place', |
| 213 | 'rating' => 5, |
| 214 | 'reviewer' => ['name' => 'Jamie'], |
| 215 | 'provider' => ['name' => 'airbnb'], |
| 216 | 'response' => 'Thanks for staying!', |
| 217 | 'reply' => ['name' => 'Host', 'avatar' => ''], |
| 218 | ]]); |
| 219 | $this->assertSame('Thanks for staying!', $out[0]['response']); |
| 220 | $this->assertSame('Host', $out[0]['reply']['name']); |
| 221 | } |
| 222 | |
| 223 | public function test_missing_provider_data_degrades_to_safe_empties_bc(): void |
| 224 | { |
| 225 | // A Google/legacy review with none of the new keys must not error and |
| 226 | // must keep the original contract intact. |
| 227 | $out = $this->format([[ |
| 228 | 'text' => 'Good', |
| 229 | 'rating' => 5, |
| 230 | 'reviewer' => ['name' => 'Sam'], |
| 231 | 'provider' => ['name' => 'google'], |
| 232 | ]]); |
| 233 | $r = $out[0]; |
| 234 | $this->assertSame('Good', $r['text']); |
| 235 | $this->assertSame([], $r['metadata']); |
| 236 | $this->assertSame([], $r['reply']); |
| 237 | $this->assertSame('', $r['response']); |
| 238 | $this->assertSame([], $r['reviewer_photos']); |
| 239 | $this->assertSame('', $r['title']); |
| 240 | } |
| 241 | } |
| 242 |