trypost/tests/Feature/UpdatePostRequestTest.php

98 lines
3.4 KiB
PHP
Raw Normal View History

feat(tiktok): photo carousel support + UX Content Sharing API compliance ## Photo carousel support - Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect, supportsImage true, supportsVideo false) and JS mirror in content-type.ts. - Variant pill picker (Video / Photo carousel) at the top of TikTokSettings, mirroring the Instagram pattern. Wired through ScheduleTab to the parent editor's existing update:platformContentType emit. - i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es. - Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`, TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap 4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos — its only previous consumer (silent privacy_level fallback) is gone. ## UX Content Sharing API compliance Per TikTok review feedback citing https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app Point 1 — already satisfied (creator_info fetch + nickname display). Point 2/4 — Music Usage Confirmation declaration is now always visible in TikTokSettings; text changes between "Music Usage Confirmation" and "Branded Content Policy and Music Usage Confirmation" based on toggle state. Previously the entire `<p>` block was conditional on a brand toggle being selected, hiding the baseline declaration. Point 2b — privacy_level may not have a default. UI was already correct; backend hardened: UpdatePostRequest now requires meta.privacy_level for tiktok platforms when status is publishing/scheduled (via withValidator); TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException (ContentPolicy category) when missing instead of silently falling back to the creator's preferred level. Point 2c — interaction settings now condition on content type: - Photo posts hide Duet/Stitch (they don't apply per TikTok docs). - Photo posts hide AIGC (also video-only). - Video posts hide Auto Add Music (photos-only feature). - Max-duration warning hidden when not a video post. Source of truth is the user-selected contentType prop, not inferred from media — ensures the UI reacts immediately to the variant pill. Point 3a — publish button stays disabled when Disclose toggle is on without a sub-selection (already the case via tiktokComplianceValid). The disabled tooltip now uses the verbatim TikTok-required text "You need to indicate if your content promotes yourself, a third party, or both." instead of the generic "Some platform settings are incomplete..." when the only blocker is TikTok disclosure incompleteness. Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out when Branded Content is checked. It is rendered disabled with a hover tooltip "Branded content visibility cannot be set to private." plus a persistent amber warning paragraph below the dropdown. When the user toggles Branded Content while privacy is SELF_ONLY, the privacy clears and a vue-sonner warning toast surfaces the change. ## Cross-cutting - New `resources/js/enums/platform.ts` mirrors the PHP Platform enum, used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete) and ScheduleTab.vue (all selected*Platforms computeds) to replace string literal comparisons against `'tiktok'` / `'facebook'` / etc. - PostPlatformFactory tiktok() state defaults meta.privacy_level to SELF_ONLY so existing test fixtures keep passing under the new publisher/FormRequest requirements. ## Tests - New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php covering the new enum case (4 tests). - TikTokPublisherTest: added "video uses title not description" and "throws when meta.privacy_level missing" regression tests; renamed two existing tests that depended on the removed silent fallback. - New tests/Feature/UpdatePostRequestTest.php with 3 tests covering the FormRequest's privacy_level enforcement (publish-rejected, publish-passes, draft-allowed). Full Pest suite: 1490 passed, 2 skipped (pre-existing).
2026-05-09 15:47:49 +00:00
<?php
declare(strict_types=1);
use App\Enums\Post\Status;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Enums\UserWorkspace\Role;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
beforeEach(function () {
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]);
$this->user->update(['current_workspace_id' => $this->workspace->id]);
$this->post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
// Media payload used by tests that need to satisfy ContentTypeCompatibleWithMedia.
$this->mediaPayload = [
[
'id' => 'test-media-video',
'path' => 'media/2026-01/test-video.mp4',
'url' => 'https://example.com/media/2026-01/test-video.mp4',
'type' => 'video',
'mime_type' => 'video/mp4',
'original_filename' => 'test-video.mp4',
],
];
$this->socialAccount = SocialAccount::factory()->create([
'workspace_id' => $this->workspace->id,
'platform' => Platform::TikTok,
]);
$this->postPlatform = PostPlatform::factory()->tiktok()->create([
'post_id' => $this->post->id,
'social_account_id' => $this->socialAccount->id,
// Override factory default so we control privacy_level per test.
'meta' => [],
]);
});
test('publishing a tiktok post without privacy_level is rejected', function () {
$response = $this->actingAs($this->user)
->put(route('app.posts.update', $this->post), [
'status' => Status::Publishing->value,
'media' => $this->mediaPayload,
'platforms' => [
[
'id' => $this->postPlatform->id,
'content_type' => ContentType::TikTokVideo->value,
'meta' => [],
],
],
]);
$response->assertSessionHasErrors('platforms.0.meta.privacy_level');
});
test('publishing a tiktok post with privacy_level passes privacy_level validation', function () {
$response = $this->actingAs($this->user)
->put(route('app.posts.update', $this->post), [
'status' => Status::Publishing->value,
'media' => $this->mediaPayload,
'platforms' => [
[
'id' => $this->postPlatform->id,
'content_type' => ContentType::TikTokVideo->value,
'meta' => ['privacy_level' => 'SELF_ONLY'],
],
],
]);
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.privacy_level']);
});
test('saving a tiktok post as draft without privacy_level skips the privacy_level rule', function () {
$response = $this->actingAs($this->user)
->put(route('app.posts.update', $this->post), [
'status' => Status::Draft->value,
'platforms' => [
[
'id' => $this->postPlatform->id,
'content_type' => ContentType::TikTokVideo->value,
'meta' => [],
],
],
]);
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.privacy_level']);
});