| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // The GIF gate MessageBubble's AttachmentImage animates behind: the payload |
| 5 | // type says "GIF" (what /gif posts — see gif.js) and the decoded image really |
| 6 | // has frames to play. Single-frame GIFs and mislabelled stills stay photos. |
| 7 | final class AnimatedGIFTests: XCTestCase { |
| 8 | func testIsGIFMatchesExactType() { |
| 9 | XCTAssertTrue(Attachment(id: "a", type: "image/gif").isGIF) |
| 10 | XCTAssertFalse(Attachment(id: "a", type: "image/jpeg").isGIF) |
| 11 | XCTAssertFalse(Attachment(id: "a", type: "image/gif+xml").isGIF) |
| 12 | XCTAssertFalse(Attachment(id: "a", type: nil).isGIF) |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) |
| 17 | import AppKit |
| 18 | import ImageIO |
| 19 | import UniformTypeIdentifiers |
| 20 | |
| 21 | final class GIFFrameCountTests: XCTestCase { |
| 22 | func testAnimatedGIFReportsItsFrames() throws { |
| 23 | let img = try XCTUnwrap(NSImage(data: makeImage(frames: 3, as: .gif))) |
| 24 | XCTAssertEqual(img.gifFrameCount, 3) |
| 25 | } |
| 26 | |
| 27 | func testSingleFrameGIFReportsOne() throws { |
| 28 | let img = try XCTUnwrap(NSImage(data: makeImage(frames: 1, as: .gif))) |
| 29 | XCTAssertEqual(img.gifFrameCount, 1) |
| 30 | } |
| 31 | |
| 32 | func testStillPNGReportsOne() throws { |
| 33 | let img = try XCTUnwrap(NSImage(data: makeImage(frames: 1, as: .png))) |
| 34 | XCTAssertEqual(img.gifFrameCount, 1) |
| 35 | } |
| 36 | |
| 37 | // A tiny solid-colour image encoded with `frames` frames, via ImageIO — |
| 38 | // the same decoder family NSImage(data:) reads back through. |
| 39 | private func makeImage(frames: Int, as type: UTType) throws -> Data { |
| 40 | let data = NSMutableData() |
| 41 | let dest = try XCTUnwrap(CGImageDestinationCreateWithData( |
| 42 | data, type.identifier as CFString, frames, nil)) |
| 43 | let frameProps = [kCGImagePropertyGIFDictionary: |
| 44 | [kCGImagePropertyGIFDelayTime: 0.1]] as CFDictionary |
| 45 | for i in 0..<frames { |
| 46 | let shade = CGFloat(i) / CGFloat(max(frames - 1, 1)) |
| 47 | let ctx = try XCTUnwrap(CGContext( |
| 48 | data: nil, width: 4, height: 4, bitsPerComponent: 8, bytesPerRow: 0, |
| 49 | space: CGColorSpaceCreateDeviceRGB(), |
| 50 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)) |
| 51 | ctx.setFillColor(CGColor(red: shade, green: 0, blue: 1 - shade, alpha: 1)) |
| 52 | ctx.fill(CGRect(x: 0, y: 0, width: 4, height: 4)) |
| 53 | CGImageDestinationAddImage(dest, try XCTUnwrap(ctx.makeImage()), frameProps) |
| 54 | } |
| 55 | XCTAssertTrue(CGImageDestinationFinalize(dest)) |
| 56 | return data as Data |
| 57 | } |
| 58 | } |
| 59 | #endif |
| 60 | |