summaryrefslogtreecommitdiff
path: root/source/Test/Unit/Html.hs
blob: 14ecdc3ddbbe19db0e6b36ea61bc74e804eae183 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
{-# LANGUAGE OverloadedStrings #-}

module Test.Unit.Html (unitTests) where

import Base
import Api qualified
import Felix.Parse qualified as Parse
import Felix.Source
import Felix.Source.Graph
import Render.Html qualified as Html
import Render.Html.Context
import Render.Html.Layout
import Report.Location (Location, pattern Nowhere)
import Syntax.Abstract

import Data.Text qualified as Text
import Data.Text.IO qualified as TextIO
import Data.List.NonEmpty qualified as NonEmpty
import System.Directory qualified as Directory
import Test.Tasty
import Test.Tasty.HUnit

unitTests :: TestTree
unitTests = testGroup "HTML renderer"
    [ testCase "one shared index resolves local and cross-page previews" referencePreviews
    , testCase "missing reference preview data falls back to readable text" missingReferenceFallback
    , testCase "datatype rendering omits unchecked derived facts" datatypeDerivedFactsAreOmitted
    ]

referencePreviews :: Assertion
referencePreviews = do
    graph <-
        expectRight =<<
            Api.prepareDefaultSourceGraph
                "test/html-fixtures/root-preview.tex"
    workspace <-
        expectRight =<< Parse.parseResolvedSourceGraph graph
    hints <- TextIO.readFile "library/lexicon.tsv"
    layout <-
        expectRight
            (layoutHtmlSourceGraph
                Api.defaultHtmlMountPrefixes
                graph)
    let nodes = Parse.parsedWorkspaceImportedBeforeImporter workspace
        sourceBlocks =
            (\node ->
                ( Parse.parsedModuleResolved node
                , Parse.parsedModuleBlocks node
                ))
                <$> nodes
        (renderIndex, pages) =
            Html.buildRenderIndex sourceBlocks
        rootPage = NonEmpty.last pages
    context <-
        expectRight
            (htmlRenderContext
                layout
                (Html.htmlPagePresentationSource rootPage))
    html <-
        expectRight
            (Html.renderDocument
                context
                hints
                renderIndex
                rootPage)
    let supportScript = Html.supportScriptAssetContents
    assertContains "local references keep page anchors" "href=\"#local_prop\"" html
    assertContains "local references point previews at the visible target block" "data-reference-label=\"local_prop\" data-preview-target-id=\"local_prop\"" html
    assertNotContains "local references do not use hidden preview ids" "data-reference-label=\"local_prop\" data-preview-id=\"" html
    assertContains "visible target blocks expose preview metadata" "id=\"local_prop\" data-preview-kind=\"Proposition\" data-preview-label=\"local_prop\"" html
    assertContains "imported references get preview metadata" "data-reference-label=\"imported_prop\"" html
    assertContains "imported references link to their relative encoded theory route" "href=\"imported-preview#imported_prop\"" html
    assertContains "imported references still use hidden preview templates" "data-reference-label=\"imported_prop\" data-preview-id=\"reference-preview-" html
    assertNotContains "imported references do not get broken page anchors" "href=\"#imported_prop\"" html
    assertNotContains "imported references are not non-clickable spans" "<span class=\"ref-badge has-preview\" data-reference-label=\"imported_prop\"" html
    assertCount "hidden preview store only contains the imported fixture preview" 1 "class=\"reference-preview-template\"" html
    assertContains "page grid is scoped to an explicit shell" "class=\"page-layout\"" html
    assertNotContains "page grid does not apply to every body div" "body > div" html
    assertContains "page heading uses the selected mounted source" "<h1>project:test/html-fixtures/root-preview.tex</h1>" html
    assertContains "imported preview records its mounted source" "project:test/html-fixtures/imported-preview.tex" html
    assertContains "imported preview source links to the same route" "href=\"imported-preview\"><code>project:test/html-fixtures/imported-preview.tex</code>" html
    assertContains "imported source renders on its own line" "class=\"reference-preview-source\"" html
    assertContains "multi-reference rendering preserves the comma separator" ", <a href=\"imported-preview#imported_prop\" class=\"ref-badge has-preview\" data-reference-label=\"imported_prop\"" html
    assertContains "calculation justifications also use target previews" "Step 2: by <a href=\"#local_prop\" class=\"ref-badge has-preview\" data-reference-label=\"local_prop\" data-preview-target-id=\"local_prop\"" html
    assertContains "large reference lists collapse to an ellipsis trigger" "Follows by <span class=\"ref-badge has-preview ref-badge-group\" data-preview-group=\"true\" data-reference-label=\"5 references\"" html
    assertContains "collapsed references keep current-page item metadata" "data-reference-label=\"group_source\" data-preview-link=\"#group_source\" data-preview-target-id=\"group_source\"" html
    assertContains "collapsed references keep imported item metadata" "data-reference-label=\"imported_prop\" data-preview-link=\"imported-preview#imported_prop\" data-preview-id=\"reference-preview-" html
    assertNotContains "collapsed references do not inline the long list" "Follows by <a href=\"#local_prop\" class=\"ref-badge has-preview\" data-reference-label=\"local_prop\" data-preview-target-id=\"local_prop\" aria-describedby=\"reference-preview-popup\">local_prop</a>, <a href=\"#uses_refs\"" html
    assertContains "collapsed tooltips compose full preview templates" "const preview = cloneHiddenPreview(item) || buildCurrentPreview(item) || buildMissingPreview(item);" supportScript
    assertContains "collapsed tooltip labels become links" "template.append(linkGroupHeading(item, preview));" supportScript
    assertContains "collapsed tooltip labels use generated reference links" "link.href = href;" supportScript
    assertContains "collapsed tooltip heading links have hover affordance" ".reference-preview-heading a:hover," html
    assertContains "collapsed tooltips use stacked preview sections" "className = 'reference-preview-group-template'" supportScript
    assertContains "visible preview popup accepts pointer interaction" "pointer-events: auto;" html
    assertContains "preview popup uses a wider bounded layout" "width: 44rem;" html
    assertContains "preview popup uses a taller bounded layout" "max-height: min(34rem, calc(100vh - 2rem));" html
    assertContains "behavior loads from the shared external script asset" "src=\"../../_static/naproche-html.js\"" html
    assertNotContains "inline script bundles are not emitted" "<script type=\"text/javascript\">" html
    assertContains "preview popup cancels delayed hide on pointer entry" "popup.addEventListener('pointerenter', clearHideTimer);" supportScript
    assertContains "preview popup schedules delayed hide on pointer exit" "popup.addEventListener('pointerleave', scheduleHide);" supportScript
    assertContains "group click pins the preview popup" "showPreview(trigger, event, true);" supportScript
    assertContains "group keyboard activation pins the preview popup" "showPreview(trigger, null, true);" supportScript
    assertContains "preview statements use a full-width paragraph" "class=\"reference-preview-statement\"" html
    assertContains "preview popup is emitted once" "id=\"reference-preview-popup\"" html

missingReferenceFallback :: Assertion
missingReferenceFallback = do
    let proof = Qed (Just Nowhere) (JustificationRef ("missing_ref" :| []))
        blocks = [BlockProof Nowhere proof Nowhere]
    html <- renderSynthetic blocks
    assertContains "missing references remain visible" "missing_ref" html
    assertNotContains "missing references do not claim preview content" "data-preview-id=" html

datatypeDerivedFactsAreOmitted :: Assertion
datatypeDerivedFactsAreOmitted = do
    let blocks =
            [ propformDatatypeBlock Nowhere
            , referenceClaimBlock "uses_datatype_fact"
            , referenceProofBlock "propform_induct"
            ]
    html <- renderSynthetic blocks
    assertContains "datatype declarations remain visible" "Datatype of " html
    assertContains "derived fact references remain readable" "propform_induct" html
    assertNotContains "unchecked datatype facts are not rendered" "<summary>Derived facts</summary>" html
    assertNotContains "unchecked datatype facts do not become preview targets" "data-preview-label=\"propform_induct\"" html

renderSynthetic :: [Block] -> IO Text
renderSynthetic blocks = do
    currentDirectory <- Directory.getCurrentDirectory
    mounts <-
        expectRight =<<
            prepareSourceMounts
                [(sourceMountId "project", currentDirectory)]
    request <-
        expectRight
            (searchedRoot "test/html-fixtures/root-preview.tex")
    graph <-
        expectRight =<<
            buildResolvedSourceGraph mounts request
    layout <-
        expectRight
            (layoutHtmlSourceGraph
                [(sourceMountId "project", [])]
                graph)
    context <-
        expectRight
            (htmlRenderContext
                layout
                (sourceGraphRootSource graph))
    let (renderIndex, page :| _remainingPages) =
            Html.buildRenderIndex
                ((sourceGraphRootSource graph, blocks) :| [])
    expectRight
        (Html.renderDocument
            context
            ""
            renderIndex
            page)

expectRight :: (Show e, HasCallStack) => Either e a -> IO a
expectRight = \case
    Left err ->
        assertFailure ("expected Right, got Left " <> show err)
    Right value ->
        pure value

propformDatatypeBlock :: Location -> Block
propformDatatypeBlock blockLoc =
    BlockData blockLoc Nothing "propform" propformDatatype

propformDatatype :: Datatype
propformDatatype =
    Datatype
        { datatypeHeadExpr = ExprOp Nowhere (constSymbol "propform") []
        , datatypeClauses =
            DatatypeClause (ExprOp Nowhere (constSymbol "propbot") []) (ExprOp Nowhere (constSymbol "propform") []) [] :|
                [ DatatypeClause (ExprOp Nowhere (unarySymbol "propvar") [ExprVar "n"]) (ExprOp Nowhere (constSymbol "propform") []) [("n", ExprOp Nowhere (constSymbol "naturals") [])]
                , DatatypeClause
                    (ExprOp Nowhere (infixSymbol "propto") [ExprVar "p", ExprVar "q"])
                    (ExprOp Nowhere (constSymbol "propform") [])
                    [ ("p", ExprOp Nowhere (constSymbol "propform") [])
                    , ("q", ExprOp Nowhere (constSymbol "propform") [])
                    ]
                ]
        }

referenceClaimBlock :: Marker -> Block
referenceClaimBlock marker =
    BlockClaim Proposition Nowhere Nothing marker (Claim [] (StmtFormula (PropositionalConstant Nowhere IsTop)))

referenceProofBlock :: Marker -> Block
referenceProofBlock marker =
    BlockProof Nowhere (Qed (Just Nowhere) (JustificationRef (marker :| []))) Nowhere

constSymbol :: Text -> FunctionSymbol
constSymbol name =
    mkMixfixItem [Just (Command name)] (Marker name) NonAssoc

unarySymbol :: Text -> FunctionSymbol
unarySymbol name =
    mkMixfixItem [Just (Command name), Just InvisibleBraceL, Nothing, Just InvisibleBraceR] (Marker name) NonAssoc

infixSymbol :: Text -> FunctionSymbol
infixSymbol name =
    mkMixfixItem [Nothing, Just (Command name), Nothing] (Marker name) NonAssoc

assertContains :: HasCallStack => String -> Text -> Text -> Assertion
assertContains label needle haystack =
    assertBool
        (label <> "\nExpected to find: " <> Text.unpack needle)
        (needle `Text.isInfixOf` haystack)

assertNotContains :: HasCallStack => String -> Text -> Text -> Assertion
assertNotContains label needle haystack =
    assertBool
        (label <> "\nDid not expect to find: " <> Text.unpack needle)
        (not (needle `Text.isInfixOf` haystack))

assertCount :: HasCallStack => String -> Int -> Text -> Text -> Assertion
assertCount label expected needle haystack =
    assertEqual
        (label <> "\nExpected count for: " <> Text.unpack needle)
        expected
        (Text.count needle haystack)