Extract alt-text truncation onto MediaItem and close test gaps
Centralize the per-platform mb_substr truncation that every publisher was repeating into MediaItem::altTextFor(Platform), delete each publisher's private altFor() helper, and clarify the Platform::altTextMaxLength() docblock so it doesn't imply Instagram's documented 1000-char cap is a guess. Add the assertions review flagged as missing: LinkedInPage/ InstagramFacebook alt-text caps, non-string and literal-"0" alt_text normalization, altTextFor() truncation/unsupported-platform behavior, Mastodon's no-description-part case, and the public API's accept/reject path for media.*.meta.alt_text.
This commit is contained in:
parent
0e93d7e5e3
commit
f74053b118
15 changed files with 196 additions and 85 deletions
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use App\Enums\Media\Source;
|
||||
use App\Enums\Media\Type;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
|
||||
class MediaItem
|
||||
{
|
||||
|
|
@ -75,6 +76,23 @@ public function altText(): ?string
|
|||
return $alt === '' ? null : $alt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The alt text truncated to the given platform's cap, or null when no alt
|
||||
* text is set or the platform doesn't support it. Single place that applies
|
||||
* the per-platform cap so publishers don't each repeat the truncation.
|
||||
*/
|
||||
public function altTextFor(Platform $platform): ?string
|
||||
{
|
||||
$alt = $this->altText();
|
||||
$max = $platform->altTextMaxLength();
|
||||
|
||||
if ($alt === null || $max === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mb_substr($alt, 0, $max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -129,9 +129,11 @@ public function maxImages(): int
|
|||
|
||||
/**
|
||||
* Character cap the platform's API accepts for image alt text (accessibility
|
||||
* description), or null when the platform has no alt-text field. Facebook and
|
||||
* Threads document no limit, so a defensive 1000 is used. Single source of
|
||||
* truth — publishers truncate to this value, never a literal.
|
||||
* description), or null when the platform has no alt-text field. X, LinkedIn,
|
||||
* Instagram, Pinterest, and Discord use documented API maxes. Facebook,
|
||||
* Threads, Mastodon, and Bluesky document no limit, so a defensive cap is
|
||||
* used instead. Single source of truth — publishers truncate to this value,
|
||||
* never a literal.
|
||||
*/
|
||||
public function altTextMaxLength(): ?int
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\DataTransferObjects\MediaItem;
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
|
|
@ -127,7 +126,7 @@ private function publishPost(?string $content, $media): array
|
|||
if ($mediaUrn) {
|
||||
$payload['content'] = ['media' => array_filter([
|
||||
'id' => $mediaUrn,
|
||||
'altText' => $this->altFor($media->first()),
|
||||
'altText' => $media->first()->altTextFor($this->platform()),
|
||||
], fn ($v) => $v !== null)];
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +148,7 @@ private function publishCarousel(?string $content, $media): array
|
|||
if ($imageUrn) {
|
||||
$images[] = array_filter([
|
||||
'id' => $imageUrn,
|
||||
'altText' => $this->altFor($item),
|
||||
'altText' => $item->altTextFor($this->platform()),
|
||||
], fn ($v) => $v !== null);
|
||||
}
|
||||
}
|
||||
|
|
@ -247,18 +246,6 @@ private function resolveDocumentTitle(PostPlatform $postPlatform): string
|
|||
return $postPlatform->post->mediaItems->first(fn ($media) => $media->isDocument())?->original_filename ?? 'Document';
|
||||
}
|
||||
|
||||
/**
|
||||
* The accessibility description to send for an uploaded image, capped to
|
||||
* what the platform accepts. Null when the user hasn't set one, so the
|
||||
* caller can omit the `altText` key entirely rather than sending it empty.
|
||||
*/
|
||||
private function altFor(MediaItem $media): ?string
|
||||
{
|
||||
$alt = $media->altText();
|
||||
|
||||
return $alt === null ? null : mb_substr($alt, 0, $this->platform()->altTextMaxLength());
|
||||
}
|
||||
|
||||
private function uploadMedia($mediaItem): ?string
|
||||
{
|
||||
return match (true) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\DataTransferObjects\MediaItem;
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\BlueskyPublishException;
|
||||
|
|
@ -64,7 +63,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$blob = $this->uploadBlob($account, $service, $media->url, $media->mime_type);
|
||||
if ($blob) {
|
||||
$images[] = [
|
||||
'alt' => $this->altFor($media),
|
||||
'alt' => $media->altTextFor(Platform::Bluesky) ?? '',
|
||||
'image' => $blob,
|
||||
];
|
||||
}
|
||||
|
|
@ -702,21 +701,6 @@ private function buildPostUrl(string $handle, string $postId): string
|
|||
return "{$webApp}/profile/{$handle}/post/{$postId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bluesky requires `alt` on every image, so this returns '' (never null)
|
||||
* when the user set no description, and the capped description otherwise.
|
||||
*/
|
||||
private function altFor(MediaItem $media): string
|
||||
{
|
||||
$alt = $media->altText();
|
||||
|
||||
if ($alt === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_substr($alt, 0, Platform::Bluesky->altTextMaxLength());
|
||||
}
|
||||
|
||||
private function handleApiError(Response $response): never
|
||||
{
|
||||
throw BlueskyPublishException::fromApiResponse($response);
|
||||
|
|
|
|||
|
|
@ -129,10 +129,10 @@ private function sendWithMedia(string $channelId, array $payload, Collection $me
|
|||
|
||||
$attachment = ['id' => $index, 'filename' => $filename];
|
||||
|
||||
$alt = $item->altText();
|
||||
$alt = $item->altTextFor(Platform::Discord);
|
||||
|
||||
if ($alt !== null) {
|
||||
$attachment['description'] = mb_substr($alt, 0, Platform::Discord->altTextMaxLength());
|
||||
$attachment['description'] = $alt;
|
||||
}
|
||||
|
||||
$attachments[] = $attachment;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\DataTransferObjects\MediaItem;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
|
|
@ -136,7 +135,7 @@ private function publishSingleImagePost(string $pageId, string $accessToken, ?st
|
|||
$payload['message'] = $content;
|
||||
}
|
||||
|
||||
$alt = $this->altFor($media);
|
||||
$alt = $media->altTextFor(Platform::Facebook);
|
||||
|
||||
if ($alt !== null) {
|
||||
$payload['alt_text_custom'] = $alt;
|
||||
|
|
@ -177,7 +176,7 @@ private function publishMultiImagePost(string $pageId, string $accessToken, ?str
|
|||
'access_token' => $accessToken,
|
||||
];
|
||||
|
||||
$alt = $this->altFor($media);
|
||||
$alt = $media->altTextFor(Platform::Facebook);
|
||||
|
||||
if ($alt !== null) {
|
||||
$uploadPayload['alt_text_custom'] = $alt;
|
||||
|
|
@ -428,16 +427,6 @@ private function handleApiError(Response $response): never
|
|||
throw FacebookPublishException::fromApiResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* User-provided alt text for the image, capped to Facebook's accepted length.
|
||||
*/
|
||||
private function altFor(MediaItem $media): ?string
|
||||
{
|
||||
$alt = $media->altText();
|
||||
|
||||
return $alt === null ? null : mb_substr($alt, 0, Platform::Facebook->altTextMaxLength());
|
||||
}
|
||||
|
||||
protected function cropFailureException(string $message): SocialPublishException
|
||||
{
|
||||
return new FacebookPublishException(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\DataTransferObjects\MediaItem;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
|
|
@ -89,7 +88,7 @@ private function publishSingleImage(string $instagramId, string $accessToken, ?s
|
|||
'access_token' => $accessToken,
|
||||
];
|
||||
|
||||
$alt = $this->altFor($media);
|
||||
$alt = $media->altTextFor(Platform::Instagram);
|
||||
|
||||
if ($alt !== null) {
|
||||
$params['alt_text'] = $alt;
|
||||
|
|
@ -217,7 +216,7 @@ private function publishCarousel(string $instagramId, string $accessToken, ?stri
|
|||
} else {
|
||||
$params['image_url'] = $this->cropImageForAspectRatio($media->url, $aspectRatio);
|
||||
|
||||
$alt = $this->altFor($media);
|
||||
$alt = $media->altTextFor(Platform::Instagram);
|
||||
|
||||
if ($alt !== null) {
|
||||
$params['alt_text'] = $alt;
|
||||
|
|
@ -371,16 +370,4 @@ private function handleApiError(Response $response): never
|
|||
{
|
||||
throw InstagramPublishException::fromApiResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* User-provided alt text for the image, capped to Instagram's accepted
|
||||
* length. Instagram only accepts `alt_text` on image containers/children,
|
||||
* never on video/reel containers.
|
||||
*/
|
||||
private function altFor(MediaItem $media): ?string
|
||||
{
|
||||
$alt = $media->altText();
|
||||
|
||||
return $alt === null ? null : mb_substr($alt, 0, Platform::Instagram->altTextMaxLength());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
|
||||
// Upload media first (max 4)
|
||||
foreach ($medias->take(4) as $media) {
|
||||
$mediaId = $this->uploadMedia($account, $instance, $media->url, $media->original_filename, $media->altText());
|
||||
$mediaId = $this->uploadMedia($account, $instance, $media->url, $media->original_filename, $media->altTextFor(Platform::Mastodon));
|
||||
if ($mediaId) {
|
||||
$mediaIds[] = $mediaId;
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ private function uploadMedia(SocialAccount $account, string $instance, string $u
|
|||
->attach('file', $stream, $name);
|
||||
|
||||
if ($altText !== null) {
|
||||
$request = $request->attach('description', mb_substr($altText, 0, Platform::Mastodon->altTextMaxLength()));
|
||||
$request = $request->attach('description', $altText);
|
||||
}
|
||||
|
||||
$response = $request->post("{$instance}/api/v1/media");
|
||||
|
|
|
|||
|
|
@ -119,10 +119,10 @@ private function publishImagePin(PostPlatform $postPlatform, ?string $content):
|
|||
$payload['link'] = data_get($postPlatform->meta, 'link');
|
||||
}
|
||||
|
||||
$alt = $postPlatform->post->mediaItems->first(fn ($m) => $m->isImage())?->altText();
|
||||
$alt = $postPlatform->post->mediaItems->first(fn ($m) => $m->isImage())?->altTextFor(Platform::Pinterest);
|
||||
|
||||
if ($alt !== null) {
|
||||
$payload['alt_text'] = mb_substr($alt, 0, Platform::Pinterest->altTextMaxLength());
|
||||
$payload['alt_text'] = $alt;
|
||||
}
|
||||
|
||||
$response = $this->socialHttp()->withToken($account->access_token)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\DataTransferObjects\MediaItem;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\ThreadsPublishException;
|
||||
|
|
@ -109,7 +108,7 @@ private function publishImagePost(string $userId, string $accessToken, ?string $
|
|||
'access_token' => $accessToken,
|
||||
];
|
||||
|
||||
$alt = $this->altFor($media);
|
||||
$alt = $media->altTextFor(Platform::Threads);
|
||||
|
||||
if ($alt !== null) {
|
||||
$params['alt_text'] = $alt;
|
||||
|
|
@ -195,7 +194,7 @@ private function publishCarousel(string $userId, string $accessToken, ?string $c
|
|||
$params['media_type'] = 'IMAGE';
|
||||
$params['image_url'] = $media->url;
|
||||
|
||||
$alt = $this->altFor($media);
|
||||
$alt = $media->altTextFor(Platform::Threads);
|
||||
|
||||
if ($alt !== null) {
|
||||
$params['alt_text'] = $alt;
|
||||
|
|
@ -347,15 +346,4 @@ private function handleApiError(Response $response): never
|
|||
{
|
||||
throw ThreadsPublishException::fromApiResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* User-provided alt text for the image, capped to Threads' accepted
|
||||
* length. Scoped to image containers/children only.
|
||||
*/
|
||||
private function altFor(MediaItem $media): ?string
|
||||
{
|
||||
$alt = $media->altText();
|
||||
|
||||
return $alt === null ? null : mb_substr($alt, 0, Platform::Threads->altTextMaxLength());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ private function getHttpClient(): PendingRequest
|
|||
*/
|
||||
private function uploadAltText(string $mediaId, MediaItem $mediaItem): void
|
||||
{
|
||||
$alt = $mediaItem->altText();
|
||||
$alt = $mediaItem->altTextFor(Platform::X);
|
||||
|
||||
if ($alt === null) {
|
||||
return;
|
||||
|
|
@ -124,7 +124,7 @@ private function uploadAltText(string $mediaId, MediaItem $mediaItem): void
|
|||
'id' => $mediaId,
|
||||
'metadata' => [
|
||||
'alt_text' => [
|
||||
'text' => mb_substr($alt, 0, Platform::X->altTextMaxLength()),
|
||||
'text' => $alt,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -446,3 +446,69 @@
|
|||
|
||||
expect($this->post->fresh()->media)->toBe($original);
|
||||
});
|
||||
|
||||
it('accepts and persists media alt text on create', function () {
|
||||
$this->socialAccount->update(['is_active' => true]);
|
||||
Http::preventStrayRequests();
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'content' => 'Alt text post',
|
||||
'media' => [[
|
||||
'id' => 'media-1',
|
||||
'path' => 'assets/foo.jpg',
|
||||
'url' => 'https://cdn.trypost.test/assets/foo.jpg',
|
||||
'type' => 'image',
|
||||
'meta' => ['alt_text' => 'A description of the photo'],
|
||||
]],
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
|
||||
],
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
$media = Post::where('content', 'Alt text post')->firstOrFail()->media;
|
||||
|
||||
expect(data_get($media, '0.meta.alt_text'))->toBe('A description of the photo');
|
||||
});
|
||||
|
||||
it('accepts and persists media alt text on update', function () {
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->putJson(route('api.posts.update', $this->post), [
|
||||
'status' => 'draft',
|
||||
'media' => [[
|
||||
'id' => 'media-1',
|
||||
'path' => 'assets/foo.jpg',
|
||||
'url' => 'https://cdn.trypost.test/assets/foo.jpg',
|
||||
'type' => 'image',
|
||||
'meta' => ['alt_text' => 'Updated alt text'],
|
||||
]],
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
expect(data_get($this->post->fresh()->media, '0.meta.alt_text'))->toBe('Updated alt text');
|
||||
});
|
||||
|
||||
it('rejects media alt text over 2000 characters', function () {
|
||||
$this->socialAccount->update(['is_active' => true]);
|
||||
Http::preventStrayRequests();
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'content' => 'Alt text too long post',
|
||||
'media' => [[
|
||||
'id' => 'media-1',
|
||||
'path' => 'assets/foo.jpg',
|
||||
'url' => 'https://cdn.trypost.test/assets/foo.jpg',
|
||||
'type' => 'image',
|
||||
'meta' => ['alt_text' => str_repeat('a', 2001)],
|
||||
]],
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
|
||||
],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['media.0.meta.alt_text']);
|
||||
|
||||
expect(Post::where('content', 'Alt text too long post')->exists())->toBeFalse();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -196,6 +196,71 @@
|
|||
@unlink($optimizedFile);
|
||||
});
|
||||
|
||||
test('mastodon publisher sends no description part when image has no alt text', function () {
|
||||
// Minimal 1x1 JPEG bytes so mime_content_type() detects image/jpeg
|
||||
$minimalJpeg = "\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
|
||||
."\xFF\xDB\x00\x43\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\x09\x09\x08\x0A\x0C"
|
||||
."\x14\x0D\x0C\x0B\x0B\x0C\x19\x12\x13\x0F\x14\x1D\x1A\x1F\x1E\x1D\x1A\x1C\x1C\x20"
|
||||
."\xFF\xC0\x00\x0B\x08\x00\x01\x00\x01\x01\x01\x11\x00\xFF\xC4\x00\x1F\x00\x00\x01"
|
||||
."\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05"
|
||||
."\xFF\xDA\x00\x08\x01\x01\x00\x00\x3F\x00\xFB\xD3\xFF\xD9";
|
||||
|
||||
$optimizedFile = tempnam(sys_get_temp_dir(), 'masto_no_alt_opt_');
|
||||
file_put_contents($optimizedFile, str_repeat('x', 1024));
|
||||
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
[
|
||||
'id' => 'test-media-id',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/test-image.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'test.jpg',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->mock(MediaOptimizer::class)
|
||||
->shouldReceive('optimizeImage')
|
||||
->once()
|
||||
->with(Mockery::any(), Platform::Mastodon)
|
||||
->andReturn($optimizedFile);
|
||||
|
||||
Http::fake(function ($request) use ($minimalJpeg) {
|
||||
$url = $request->url();
|
||||
|
||||
if (str_contains($url, '/api/v1/media')) {
|
||||
return Http::response([
|
||||
'id' => 'media-no-alt-123',
|
||||
'type' => 'image',
|
||||
'url' => 'https://mastodon.social/media/image.jpg',
|
||||
], 200);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/api/v1/statuses')) {
|
||||
return Http::response([
|
||||
'id' => '109876543210',
|
||||
'url' => 'https://mastodon.social/@testuser/109876543210',
|
||||
], 200);
|
||||
}
|
||||
|
||||
// Media download: return valid JPEG so mime_content_type() detects image/jpeg
|
||||
return Http::response($minimalJpeg, 200, ['Content-Type' => 'image/jpeg']);
|
||||
});
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), '/api/v1/media')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return collect($request->data())->firstWhere('name', 'description') === null;
|
||||
});
|
||||
|
||||
@unlink($optimizedFile);
|
||||
});
|
||||
|
||||
test('mastodon publisher includes media ids in post', function () {
|
||||
Http::fake([
|
||||
'https://mastodon.social/api/v1/statuses' => Http::response([
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use App\DataTransferObjects\MediaItem;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
|
||||
test('altText returns the trimmed meta alt_text', function () {
|
||||
$item = MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u', 'meta' => ['alt_text' => ' a cat ']]);
|
||||
|
|
@ -14,3 +15,23 @@
|
|||
expect(MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u'])->altText())->toBeNull()
|
||||
->and(MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u', 'meta' => ['alt_text' => ' ']])->altText())->toBeNull();
|
||||
});
|
||||
|
||||
test('altText is null when meta alt_text is not a string', function () {
|
||||
expect(MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u', 'meta' => ['alt_text' => 123]])->altText())->toBeNull()
|
||||
->and(MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u', 'meta' => ['alt_text' => ['x']]])->altText())->toBeNull();
|
||||
});
|
||||
|
||||
test('altText keeps the literal string "0"', function () {
|
||||
$item = MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u', 'meta' => ['alt_text' => '0']]);
|
||||
|
||||
expect($item->altText())->toBe('0');
|
||||
});
|
||||
|
||||
test('altTextFor truncates to the platform cap and is null for an unsupported platform', function () {
|
||||
$longAlt = str_repeat('a', Platform::X->altTextMaxLength() + 50);
|
||||
$item = MediaItem::fromArray(['id' => 'a', 'path' => 'p.jpg', 'url' => 'u', 'meta' => ['alt_text' => $longAlt]]);
|
||||
|
||||
expect($item->altTextFor(Platform::X))->toBe(mb_substr($longAlt, 0, Platform::X->altTextMaxLength()))
|
||||
->and(mb_strlen($item->altTextFor(Platform::X)))->toBe(Platform::X->altTextMaxLength())
|
||||
->and($item->altTextFor(Platform::TikTok))->toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
->and(Platform::X->altTextMaxLength())->toBe(1000)
|
||||
->and(Platform::Mastodon->altTextMaxLength())->toBe(1500)
|
||||
->and(Platform::LinkedIn->altTextMaxLength())->toBe(4086)
|
||||
->and(Platform::LinkedInPage->altTextMaxLength())->toBe(4086)
|
||||
->and(Platform::Instagram->altTextMaxLength())->toBe(1000)
|
||||
->and(Platform::InstagramFacebook->altTextMaxLength())->toBe(1000)
|
||||
->and(Platform::Pinterest->altTextMaxLength())->toBe(500)
|
||||
->and(Platform::Discord->altTextMaxLength())->toBe(1024)
|
||||
->and(Platform::Facebook->altTextMaxLength())->toBe(1000)
|
||||
|
|
@ -24,5 +26,7 @@
|
|||
|
||||
test('supportsAltText mirrors altTextMaxLength', function () {
|
||||
expect(Platform::Bluesky->supportsAltText())->toBeTrue()
|
||||
->and(Platform::LinkedInPage->supportsAltText())->toBeTrue()
|
||||
->and(Platform::InstagramFacebook->supportsAltText())->toBeTrue()
|
||||
->and(Platform::TikTok->supportsAltText())->toBeFalse();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue