瀏覽代碼

Convert all test code to Spec

jherve 1 年之前
父節點
當前提交
83867bd6f4
共有 6 個文件被更改,包括 186 次插入268 次删除
  1. 3 2
      spago.dhall
  2. 19 26
      test/ArtDecoCard.purs
  3. 98 100
      test/JobsUnifiedTopCard.purs
  4. 12 7
      test/Main.purs
  5. 20 44
      test/PageUrl.purs
  6. 34 89
      test/UIStringParser.purs

+ 3 - 2
spago.dhall

@@ -4,11 +4,11 @@ You can edit this file as you like.
 -}
 -}
 { name = "web-extension"
 { name = "web-extension"
 , dependencies =
 , dependencies =
-  [ "argonaut-codecs"
+  [ "aff"
+  , "argonaut-codecs"
   , "argonaut-core"
   , "argonaut-core"
   , "argonaut-generic"
   , "argonaut-generic"
   , "arrays"
   , "arrays"
-  , "assert"
   , "console"
   , "console"
   , "control"
   , "control"
   , "datetime"
   , "datetime"
@@ -29,6 +29,7 @@ You can edit this file as you like.
   , "partial"
   , "partial"
   , "prelude"
   , "prelude"
   , "profunctor-lenses"
   , "profunctor-lenses"
+  , "spec"
   , "strings"
   , "strings"
   , "transformers"
   , "transformers"
   , "tuples"
   , "tuples"

File diff suppressed because it is too large
+ 19 - 26
test/ArtDecoCard.purs


+ 98 - 100
test/JobsUnifiedTopCard.purs

@@ -8,7 +8,7 @@ import Data.List.NonEmpty (NonEmptyList(..))
 import Data.Maybe (Maybe(..), fromJust)
 import Data.Maybe (Maybe(..), fromJust)
 import Data.NonEmpty (NonEmpty(..))
 import Data.NonEmpty (NonEmpty(..))
 import Data.Traversable (traverse)
 import Data.Traversable (traverse)
-import Effect (Effect)
+import Effect.Class (liftEffect)
 import LinkedIn.DetachedNode (DetachedNode(..), toDetached)
 import LinkedIn.DetachedNode (DetachedNode(..), toDetached)
 import LinkedIn.Extractible (query)
 import LinkedIn.Extractible (query)
 import LinkedIn.Jobs.JobOffer (JobOffer(..))
 import LinkedIn.Jobs.JobOffer (JobOffer(..))
@@ -19,124 +19,123 @@ import LinkedIn.UI.Basic.Types (JobFlexibility(..))
 import LinkedIn.UI.Components.JobsUnifiedTopCard (JobsUnifiedTopCardElement(..), TopCardAction(..), TopCardInsight(..), TopCardInsightContent(..), TopCardPrimaryDescription(..), TopCardSecondaryInsight(..))
 import LinkedIn.UI.Components.JobsUnifiedTopCard (JobsUnifiedTopCardElement(..), TopCardAction(..), TopCardInsight(..), TopCardInsightContent(..), TopCardPrimaryDescription(..), TopCardSecondaryInsight(..))
 import Node.JsDom (jsDomFromFile)
 import Node.JsDom (jsDomFromFile)
 import Partial.Unsafe (unsafePartial)
 import Partial.Unsafe (unsafePartial)
-import Test.Assert (assert, assertEqual)
+import Test.Spec (Spec, describe, it)
+import Test.Spec.Assertions (shouldEqual)
 import Test.Utils (fromDetachedToUI)
 import Test.Utils (fromDetachedToUI)
 
 
-main :: Effect Unit
-main = do
-  dom <- jsDomFromFile "test/examples/job_offer.html"
+jobsUnifiedTopCardSpec :: Spec Unit
+jobsUnifiedTopCardSpec = do
+  describe "Jobs top card parsing" do
+    it "works" do
+      dom <- liftEffect $ jsDomFromFile "test/examples/job_offer.html"
+      wep <- liftEffect $ runQuery $ query @JobOfferPage dom
 
 
-  wep <- runQuery $ query dom
+      isRight wep `shouldEqual` true
 
 
-  assert $ isRight wep
+      let
+        JobOfferPage jobCard = unsafePartial $ fromJust $ hush wep
 
 
-  let
-    JobOfferPage jobCard = unsafePartial $ fromJust $ hush wep
+      topCard <- liftEffect $ traverse toDetached jobCard
 
 
-  topCard <- traverse toDetached jobCard
-
-  assertEqual {
-    actual: topCard,
-    expected:  JobsUnifiedTopCardElement {
-      actions: (Just (NonEmptyList
-        (NonEmpty (TopCardActionButton (DetachedButton {
-          classes: ("jobs-apply-button" : "artdeco-button" : "artdeco-button--3" : "artdeco-button--primary" : "ember-view" : Nil),
-          content: "Candidature simplifiée",
-          role: Nothing
-        })) ((TopCardActionButton (DetachedButton {
-          classes: ("jobs-save-button" : "artdeco-button" : "artdeco-button--3" : "artdeco-button--secondary" : Nil),
-          content: "Enregistrer Enregistrer Data Engineer H/F - Secteur Energie chez LINCOLN",
-          role: Nothing
-        })) : Nil)))),
-      header: (DetachedElement {
-        classes: ("t-24" : "t-bold" : "job-details-jobs-unified-top-card__job-title" : Nil),
-        content: "Data Engineer H/F - Secteur Energie",
-        id: Nothing,
-        tag: "H1"
-      }),
-      insights: (Just (NonEmptyList
-        (NonEmpty (TopCardInsight {
-          content: (TopCardInsightContentSecondary {
-            primary: (DetachedElement {
-              classes: Nil,
-              content: "Sur site",
-              id: Nothing,
-              tag: "SPAN"
-            }),
-            secondary: (NonEmptyList (NonEmpty (TopCardSecondaryInsightNested
-              (DetachedElement {
+      topCard `shouldEqual` JobsUnifiedTopCardElement {
+        actions: (Just (NonEmptyList
+          (NonEmpty (TopCardActionButton (DetachedButton {
+            classes: ("jobs-apply-button" : "artdeco-button" : "artdeco-button--3" : "artdeco-button--primary" : "ember-view" : Nil),
+            content: "Candidature simplifiée",
+            role: Nothing
+          })) ((TopCardActionButton (DetachedButton {
+            classes: ("jobs-save-button" : "artdeco-button" : "artdeco-button--3" : "artdeco-button--secondary" : Nil),
+            content: "Enregistrer Enregistrer Data Engineer H/F - Secteur Energie chez LINCOLN",
+            role: Nothing
+          })) : Nil)))),
+        header: (DetachedElement {
+          classes: ("t-24" : "t-bold" : "job-details-jobs-unified-top-card__job-title" : Nil),
+          content: "Data Engineer H/F - Secteur Energie",
+          id: Nothing,
+          tag: "H1"
+        }),
+        insights: (Just (NonEmptyList
+          (NonEmpty (TopCardInsight {
+            content: (TopCardInsightContentSecondary {
+              primary: (DetachedElement {
                 classes: Nil,
                 classes: Nil,
-                content: "Temps plein",
+                content: "Sur site",
                 id: Nothing,
                 id: Nothing,
                 tag: "SPAN"
                 tag: "SPAN"
-              })) ((TopCardSecondaryInsightPlain
-              (DetachedElement {
-                classes: ("job-details-jobs-unified-top-card__job-insight-view-model-secondary" : Nil),
-                content: "Confirmé",
-                id: Nothing,
-                tag: "SPAN"
-              })) : Nil))) }),
-            icon: DetachedLiIcon "job"
-        }) ((TopCardInsight {
-          content: (TopCardInsightContentSingle (DetachedElement {
-            classes: Nil,
-            content: "201-500 employés · Technologies et services de l’information",
-            id: Nothing,
-            tag: "SPAN" })),
-          icon: DetachedLiIcon "company"
-        }) : (TopCardInsight {
+              }),
+              secondary: (NonEmptyList (NonEmpty (TopCardSecondaryInsightNested
+                (DetachedElement {
+                  classes: Nil,
+                  content: "Temps plein",
+                  id: Nothing,
+                  tag: "SPAN"
+                })) ((TopCardSecondaryInsightPlain
+                (DetachedElement {
+                  classes: ("job-details-jobs-unified-top-card__job-insight-view-model-secondary" : Nil),
+                  content: "Confirmé",
+                  id: Nothing,
+                  tag: "SPAN"
+                })) : Nil))) }),
+              icon: DetachedLiIcon "job"
+          }) ((TopCardInsight {
             content: (TopCardInsightContentSingle (DetachedElement {
             content: (TopCardInsightContentSingle (DetachedElement {
               classes: Nil,
               classes: Nil,
-              content: "2 anciens élèves travaillent ici",
+              content: "201-500 employés · Technologies et services de l’information",
               id: Nothing,
               id: Nothing,
               tag: "SPAN" })),
               tag: "SPAN" })),
-            icon: DetachedLiIcon "people"
-            }) : (TopCardInsight {
+            icon: DetachedLiIcon "company"
+          }) : (TopCardInsight {
               content: (TopCardInsightContentSingle (DetachedElement {
               content: (TopCardInsightContentSingle (DetachedElement {
                 classes: Nil,
                 classes: Nil,
-                content: "Découvrez comment vous vous positionnez par rapport à 87 candidats. Essai Premium pour 0 EUR",
+                content: "2 anciens élèves travaillent ici",
                 id: Nothing,
                 id: Nothing,
                 tag: "SPAN" })),
                 tag: "SPAN" })),
-              icon: (DetachedSvgElement { dataTestIcon: (Just "lightbulb-medium"), id: Nothing, tag: "svg" })
-            }) : (TopCardInsight {
-              content: (TopCardInsightContentButton (DetachedButton {
-                classes: ("job-details-jobs-unified-top-card__job-insight-text-button" : Nil),
-                content: "9 compétences sur 11 correspondent à votre profil, vous pourriez bien convenir pour ce poste",
-                role: Nothing
-              })),
-              icon: (DetachedSvgElement { dataTestIcon: (Just "checklist-medium"), id: Nothing, tag: "svg" })
-            }) : Nil)))),
-      primaryDescription: (TopCardPrimaryDescription {
-        link: (DetachedA { content: "LINCOLN", href: "https://www.linkedin.com/company/lincoln-/life" }),
-        text: (DetachedText "· Boulogne-Billancourt, Île-de-France, France"),
-        tvmText: (Just (NonEmptyList
-          (NonEmpty (DetachedElement {
-            classes: ("tvm__text" : "tvm__text--neutral" : Nil),
-            content: "il y a 2 semaines",
-            id: Nothing,
-            tag: "SPAN"
-          }) ((DetachedElement {
-            classes: ("tvm__text" : "tvm__text--neutral" : Nil),
-            content: "·",
-            id: Nothing,
-            tag: "SPAN"
-          }) : (DetachedElement {
-            classes: ("tvm__text" : "tvm__text--neutral" : Nil),
-            content: "87 candidats",
-            id: Nothing,
-            tag: "SPAN"
-          }) : Nil
+              icon: DetachedLiIcon "people"
+              }) : (TopCardInsight {
+                content: (TopCardInsightContentSingle (DetachedElement {
+                  classes: Nil,
+                  content: "Découvrez comment vous vous positionnez par rapport à 87 candidats. Essai Premium pour 0 EUR",
+                  id: Nothing,
+                  tag: "SPAN" })),
+                icon: (DetachedSvgElement { dataTestIcon: (Just "lightbulb-medium"), id: Nothing, tag: "svg" })
+              }) : (TopCardInsight {
+                content: (TopCardInsightContentButton (DetachedButton {
+                  classes: ("job-details-jobs-unified-top-card__job-insight-text-button" : Nil),
+                  content: "9 compétences sur 11 correspondent à votre profil, vous pourriez bien convenir pour ce poste",
+                  role: Nothing
+                })),
+                icon: (DetachedSvgElement { dataTestIcon: (Just "checklist-medium"), id: Nothing, tag: "svg" })
+              }) : Nil)))),
+        primaryDescription: (TopCardPrimaryDescription {
+          link: (DetachedA { content: "LINCOLN", href: "https://www.linkedin.com/company/lincoln-/life" }),
+          text: (DetachedText "· Boulogne-Billancourt, Île-de-France, France"),
+          tvmText: (Just (NonEmptyList
+            (NonEmpty (DetachedElement {
+              classes: ("tvm__text" : "tvm__text--neutral" : Nil),
+              content: "il y a 2 semaines",
+              id: Nothing,
+              tag: "SPAN"
+            }) ((DetachedElement {
+              classes: ("tvm__text" : "tvm__text--neutral" : Nil),
+              content: "·",
+              id: Nothing,
+              tag: "SPAN"
+            }) : (DetachedElement {
+              classes: ("tvm__text" : "tvm__text--neutral" : Nil),
+              content: "87 candidats",
+              id: Nothing,
+              tag: "SPAN"
+            }) : Nil
+            ))
           ))
           ))
-        ))
-      })
-    }
-  }
+        })
+      }
+
 
 
+      let
+        jobOffer = (JJO.fromUI <=< fromDetachedToUI) topCard
 
 
-  assertEqual {
-    actual: (JJO.fromUI <=< fromDetachedToUI) topCard,
-    expected:
-      Right (JobOffer {
+      jobOffer `shouldEqual` Right (JobOffer {
         companyDomain: (Just "Technologies et services de l’information"),
         companyDomain: (Just "Technologies et services de l’information"),
         companyLink: "https://www.linkedin.com/company/lincoln-/life",
         companyLink: "https://www.linkedin.com/company/lincoln-/life",
         companyName: "LINCOLN",
         companyName: "LINCOLN",
@@ -146,4 +145,3 @@ main = do
         flexibility: (Just JobFlexOnSite),
         flexibility: (Just JobFlexOnSite),
         title: "Data Engineer H/F - Secteur Energie"
         title: "Data Engineer H/F - Secteur Energie"
       })
       })
-  }

+ 12 - 7
test/Main.purs

@@ -3,12 +3,17 @@ module Test.Main where
 import Prelude
 import Prelude
 
 
 import Effect (Effect)
 import Effect (Effect)
-import Test.ArtDecoCard as ArtDecoCard
-import Test.JobsUnifiedTopCard as JobsUnifiedTopCard
-import Test.UIStringParser as UIStringParser
+import Effect.Aff (launchAff_)
+import Test.ArtDecoCard (artDecoCardsSpec)
+import Test.JobsUnifiedTopCard (jobsUnifiedTopCardSpec)
+import Test.PageUrl (pageUrlSpec)
+import Test.Spec.Reporter.Console (consoleReporter)
+import Test.Spec.Runner (runSpec)
+import Test.UIStringParser (uiStringParserSpec)
 
 
 main :: Effect Unit
 main :: Effect Unit
-main = do
-  ArtDecoCard.main
-  JobsUnifiedTopCard.main
-  UIStringParser.main
+main = launchAff_ $ runSpec [consoleReporter] do
+  pageUrlSpec
+  uiStringParserSpec
+  artDecoCardsSpec
+  jobsUnifiedTopCardSpec

+ 20 - 44
test/PageUrl.purs

@@ -1,7 +1,4 @@
-module Test.PageUrl
-  ( main
-  )
-  where
+module Test.PageUrl where
 
 
 import Prelude
 import Prelude
 
 
@@ -9,51 +6,30 @@ import Data.Either (Either(..), isLeft)
 import Data.Int64 (Int64)
 import Data.Int64 (Int64)
 import Data.Int64 as I64
 import Data.Int64 as I64
 import Data.Maybe (fromJust)
 import Data.Maybe (fromJust)
-import Effect (Effect)
 import LinkedIn.PageUrl (PageUrl(..), pageUrlP)
 import LinkedIn.PageUrl (PageUrl(..), pageUrlP)
 import LinkedIn.UI.Basic.Types (JobOfferId(..))
 import LinkedIn.UI.Basic.Types (JobOfferId(..))
 import Parsing (runParser)
 import Parsing (runParser)
 import Partial.Unsafe (unsafePartial)
 import Partial.Unsafe (unsafePartial)
-import Test.Assert (assert, assertEqual)
+import Test.Spec (Spec, describe, it)
+import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
 
 
 toI64 ∷ String → Int64
 toI64 ∷ String → Int64
 toI64 s = unsafePartial $ fromJust $ I64.fromString s
 toI64 s = unsafePartial $ fromJust $ I64.fromString s
 
 
-main :: Effect Unit
-main = do
-  assertEqual {
-    actual: runParser "/in/username/details/projects/" pageUrlP,
-    expected: Right(UrlProjects "username")
-  }
-
-  assertEqual {
-    actual: runParser "/in/username/details/skills/" pageUrlP,
-    expected: Right(UrlSkills "username")
-  }
-
-  assertEqual {
-    actual: runParser "/in/username/details/experience/" pageUrlP,
-    expected: Right(UrlWorkExperience "username")
-  }
-
-  assertEqual {
-    actual: runParser "/in/username/details/languages/" pageUrlP,
-    expected: Right(UrlLanguage "username")
-  }
-
-  assertEqual {
-    actual: runParser "/in/username/details/education/" pageUrlP,
-    expected: Right(UrlEducation "username")
-  }
-
-  assertEqual {
-    actual: runParser "/jobs/view/3764313323/" pageUrlP,
-    expected: Right(UrlJobOffer (JobOfferId (toI64 "3764313323")))
-  }
-
-  assertEqual {
-    actual: runParser "/in/username/" pageUrlP,
-    expected: Right(UrlProfileMain "username")
-  }
-
-  assert $ isLeft $ runParser "/not/an/url/" pageUrlP
+pageUrlSpec :: Spec Unit
+pageUrlSpec = do
+  describe "Page URL parsers" do
+    it "projects page" do
+      runParser "/in/username/details/projects/" pageUrlP `shouldEqual` Right(UrlProjects "username")
+    it "skills page" do
+      runParser "/in/username/details/skills/" pageUrlP `shouldEqual` Right(UrlSkills "username")
+    it "experience page" do
+      runParser "/in/username/details/experience/" pageUrlP `shouldEqual` Right(UrlWorkExperience "username")
+    it "languages page" do
+      runParser "/in/username/details/languages/" pageUrlP `shouldEqual` Right(UrlLanguage "username")
+    it "education page" do
+      runParser "/in/username/details/education/" pageUrlP `shouldEqual` Right(UrlEducation "username")
+    it "jobs page" do
+      runParser "/jobs/view/3764313323/" pageUrlP `shouldEqual` Right(UrlJobOffer (JobOfferId (toI64 "3764313323")))
+    it "not an url" do
+      runParser "/not/a/supported/url/" pageUrlP `shouldSatisfy` isLeft

+ 34 - 89
test/UIStringParser.purs

@@ -1,110 +1,55 @@
-module Test.UIStringParser
-  ( main
-  )
-  where
+module Test.UIStringParser where
 
 
 import Prelude
 import Prelude
 
 
 import Data.Date (Month(..))
 import Data.Date (Month(..))
 import Data.Either (Either(..))
 import Data.Either (Either(..))
-import Effect (Effect)
 import LinkedIn.UI.Basic.Parser (durationP, monthYearP, timeSpanP)
 import LinkedIn.UI.Basic.Parser (durationP, monthYearP, timeSpanP)
 import LinkedIn.UI.Basic.Types (Duration(..), TimeSpan(..))
 import LinkedIn.UI.Basic.Types (Duration(..), TimeSpan(..))
 import LinkedIn.UI.Strings.Parser (uiStringDurationP, uiStringdotSeparatedP)
 import LinkedIn.UI.Strings.Parser (uiStringDurationP, uiStringdotSeparatedP)
 import LinkedIn.UI.Strings.Types (UIString(..))
 import LinkedIn.UI.Strings.Types (UIString(..))
 import Parsing (ParseError(..), Position(..), runParser)
 import Parsing (ParseError(..), Position(..), runParser)
-import Test.Assert (assertEqual)
+import Test.Spec (Spec, describe, it)
+import Test.Spec.Assertions (shouldEqual)
 import Test.Utils (toMonthYear')
 import Test.Utils (toMonthYear')
 
 
-testMonthYearParser ∷ Effect Unit
-testMonthYearParser = do 
-  assertEqual {
-    actual:  run "fév. 2004",
-    expected: Right(toMonthYear' February 2004)
-  }
-  assertEqual {
-    actual:  run "juin 2012",
-    expected: Right(toMonthYear' June 2012)
-  }
+uiStringParserSpec :: Spec Unit
+uiStringParserSpec = do
+  describe "month year parser" do
+    let run s = runParser s monthYearP
 
 
-  where run s = runParser s monthYearP
+    it "works" do
+      run "fév. 2004" `shouldEqual` Right(toMonthYear' February 2004)
+      run "juin 2012" `shouldEqual` Right(toMonthYear' June 2012)
 
 
-testTimeSpanParser ∷ Effect Unit
-testTimeSpanParser = do 
-  assertEqual {
-    actual:  run "juin 2012 - aujourd’hui",
-    expected: Right(TimeSpanToToday (toMonthYear' June 2012))
-  }
-  assertEqual {
-    actual:  run "juin 2012 - mai 2021",
-    expected: Right(TimeSpanBounded (toMonthYear' June 2012) (toMonthYear' May 2021))
-  }
+  describe "timespan parser" do
+    let run s = runParser s timeSpanP
 
 
-  where run s = runParser s timeSpanP
+    it "works" do
+      run "juin 2012 - aujourd’hui" `shouldEqual` Right(TimeSpanToToday (toMonthYear' June 2012))
+      run "juin 2012 - mai 2021" `shouldEqual` Right(TimeSpanBounded (toMonthYear' June 2012) (toMonthYear' May 2021))
 
 
-testDurationParser ∷ Effect Unit
-testDurationParser = do 
-  assertEqual {
-    actual: run "2 ans 3 mois",
-    expected: Right(YearsMonth 2 3)
-  }
-  assertEqual {
-    actual: run "1 an 3 mois",
-    expected: Right(YearsMonth 1 3)
-  }
-  assertEqual {
-    actual: run "3 mois",
-    expected: Right(Months 3)
-  }
-  assertEqual {
-    actual: run "3 ans",
-    expected: Right(Years 3)
-  }
-  assertEqual {
-    actual: run "1 an",
-    expected: Right(Years 1)
-  }
+  describe "duration parser" do
+    let run s = runParser s durationP
 
 
-  where run s = runParser s durationP
+    it "works" do
+      run "2 ans 3 mois" `shouldEqual` Right(YearsMonth 2 3)
+      run "1 an 3 mois" `shouldEqual` Right(YearsMonth 1 3)
+      run "3 mois" `shouldEqual` Right(Months 3)
+      run "3 ans" `shouldEqual` Right(Years 3)
+      run "1 an" `shouldEqual` Right(Years 1)
 
 
-testUIParserDuration ∷ Effect Unit
-testUIParserDuration = do
-  assertEqual {
-    actual: run "2 ans 3 mois",
-    expected: Right(UIStringDuration (YearsMonth 2 3))
-  }
+  describe "UI duration parser" do
+    let run s = runParser s uiStringDurationP
 
 
-  where run s = runParser s uiStringDurationP
+    it "works" do
+      run "2 ans 3 mois" `shouldEqual` Right(UIStringDuration (YearsMonth 2 3))
 
 
-testUIParserDotSeparated ∷ Effect Unit
-testUIParserDotSeparated = do
-  assertEqual {
-    actual: run "some text 1 · some text 2",
-    expected: Right(UIStringDotSeparated (UIStringPlain "some text 1") (UIStringPlain "some text 2"))
-  }
+  describe "UI dot separated string parser" do
+    let run s = runParser s uiStringdotSeparatedP
 
 
-  assertEqual {
-    actual: run "· some text after a dot",
-    expected: Right(UIStringDotSeparated (UIStringPlain "") (UIStringPlain "some text after a dot"))
-  }
-
-  assertEqual {
-    actual: run "some text before a dot ·",
-    expected: Right(UIStringDotSeparated (UIStringPlain "some text before a dot") (UIStringPlain ""))
-  }
-
-  assertEqual {
-    actual: run "string with no dot",
-    expected: (Left (ParseError "Expected '•'" (Position { column: 19, index: 18, line: 1 })))
-  }
-
-  where run s = runParser s uiStringdotSeparatedP
-
-main :: Effect Unit
-main = do
-  testMonthYearParser
-  testTimeSpanParser
-  testDurationParser
-
-  testUIParserDuration
-  testUIParserDotSeparated
+    it "works" do
+      run "some text 1 · some text 2" `shouldEqual` Right(UIStringDotSeparated (UIStringPlain "some text 1") (UIStringPlain "some text 2"))
+      run "· some text after a dot" `shouldEqual` Right(UIStringDotSeparated (UIStringPlain "") (UIStringPlain "some text after a dot"))
+      run "some text before a dot ·" `shouldEqual` Right(UIStringDotSeparated (UIStringPlain "some text before a dot") (UIStringPlain ""))
+      run "string with no dot" `shouldEqual` (Left (ParseError "Expected '•'" (Position { column: 19, index: 18, line: 1 })))