packages feed

hstratus-notes-0.1.0.0: test/HStratus/Notes/ProtoSpec.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : HStratus.Notes.ProtoSpec
Copyright   : (c) 2026 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause

Tests for the proto3-wire decoders in
'Network.HStratus.Internal.Notes.Proto'.
-}
module HStratus.Notes.ProtoSpec (spec) where

import Data.ByteString (ByteString)
import HStratus.Notes.Arbitraries ()
import HStratus.Notes.TestHelper
import Network.HStratus.Internal.Notes.Proto
import Test.Hspec
import Test.Hspec.Benri (endsLeft_, endsRight)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck (counterexample, (===))


spec :: Spec
spec = describe "decodeNoteStoreProto" $ do
  it "decodes a minimal note with text only" $
    pure (decodeNoteStoreProto minimalNoteBytes)
      `endsRight` ProtoNote{pnNoteText = "hello", pnAttributeRuns = []}

  it "returns an error for empty input" $
    endsLeft_ $
      pure (decodeNoteStoreProto "")

  it "decodes attribute run length and paragraph style" $
    case decodeNoteStoreProto noteWithRunBytes of
      Left err -> expectationFailure err
      Right note -> do
        pnNoteText note `shouldBe` "hi"
        case pnAttributeRuns note of
          [run] -> do
            parLength run `shouldBe` 2
            parParagraphStyle run
              `shouldBe` Just
                ProtoParagraphStyle
                  { ppsStyleType = 1
                  , ppsIndent = 0
                  , ppsChecked = Nothing
                  , ppsListStart = Nothing
                  , ppsBlockQuote = False
                  }
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes strikethrough field 7" $
    case decodeNoteStoreProto noteWithStrikethroughBytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] -> do
            parLength run `shouldBe` 2
            parStrikethrough run `shouldBe` 1
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes indent_amount field 4 into ppsIndent" $
    case decodeNoteStoreProto bulletIndent1Bytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] ->
            fmap ppsIndent (parParagraphStyle run) `shouldBe` Just 1
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes checklist.done = 1 into ppsChecked = Just True" $
    case decodeNoteStoreProto checklistDoneBytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] ->
            fmap ppsChecked (parParagraphStyle run) `shouldBe` Just (Just True)
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes checklist.done = 0 into ppsChecked = Just False" $
    case decodeNoteStoreProto checklistUndoneBytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] ->
            fmap ppsChecked (parParagraphStyle run) `shouldBe` Just (Just False)
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes absent checklist into ppsChecked = Nothing" $
    case decodeNoteStoreProto checklistAbsentBytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] ->
            fmap ppsChecked (parParagraphStyle run) `shouldBe` Just Nothing
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes starting_list_item_number into ppsListStart = Just 3" $
    case decodeNoteStoreProto numberedListStart3Bytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] ->
            fmap ppsListStart (parParagraphStyle run) `shouldBe` Just (Just 3)
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  it "decodes block_quote into ppsBlockQuote = True" $
    case decodeNoteStoreProto blockQuoteBytes of
      Left err -> expectationFailure err
      Right note ->
        case pnAttributeRuns note of
          [run] ->
            fmap ppsBlockQuote (parParagraphStyle run) `shouldBe` Just True
          runs -> expectationFailure $ "expected 1 run, got " <> show (length runs)

  prop "encode/decode roundtrip preserves ProtoParagraphStyle" $ \ps ->
    case decodeNoteStoreProto (mkNote "x" [runWith 1 (encodeParagraphStyle ps)]) of
      Left err -> counterexample err False
      Right note -> case pnAttributeRuns note of
        [run] -> parParagraphStyle run === Just ps
        runs -> counterexample ("expected 1 run, got " <> show (length runs)) False


minimalNoteBytes :: ByteString
minimalNoteBytes = mkNote "hello" []


noteWithRunBytes :: ByteString
noteWithRunBytes = mkNote "hi" [runWith 2 (psStyleType 1)]


noteWithStrikethroughBytes :: ByteString
noteWithStrikethroughBytes = mkNote "hi" [runFields 2 [(7, 1)]]


bulletIndent1Bytes :: ByteString
bulletIndent1Bytes = mkNote "hi" [runWith 1 (psStyleType 100 <> psIndentAmount 1)]


checklistDoneBytes :: ByteString
checklistDoneBytes = mkNote "hi" [runWith 1 (psStyleType 103 <> psChecklist True)]


checklistUndoneBytes :: ByteString
checklistUndoneBytes = mkNote "hi" [runWith 1 (psStyleType 103 <> psChecklist False)]


checklistAbsentBytes :: ByteString
checklistAbsentBytes = mkNote "hi" [runWith 1 (psStyleType 103)]


numberedListStart3Bytes :: ByteString
numberedListStart3Bytes = mkNote "hi" [runWith 1 (psStyleType 102 <> psListStart 3)]


blockQuoteBytes :: ByteString
blockQuoteBytes = mkNote "hi" [runWith 1 psBlockQuote]