|
| 1 | +package org.elixir_lang.heex |
| 2 | + |
| 3 | +import com.intellij.lang.html.HTMLLanguage |
| 4 | +import com.intellij.openapi.fileTypes.FileTypeManager |
| 5 | +import com.intellij.psi.templateLanguages.ConfigurableTemplateLanguageFileViewProvider |
| 6 | +import org.elixir_lang.PlatformTestCase |
| 7 | +import org.elixir_lang.ElixirLanguage |
| 8 | +import org.elixir_lang.eex.Language as EExLanguage |
| 9 | + |
| 10 | +class HEExFileTypeTest : PlatformTestCase() { |
| 11 | + |
| 12 | + fun testFileTypeRegistered() { |
| 13 | + val fileType = FileTypeManager.getInstance().getFileTypeByExtension("heex") |
| 14 | + assertInstanceOf(fileType, org.elixir_lang.heex.file.Type::class.java) |
| 15 | + assertEquals("HEEx", fileType.name) |
| 16 | + } |
| 17 | + |
| 18 | + fun testFileTypeProperties() { |
| 19 | + val fileType = org.elixir_lang.heex.file.Type.INSTANCE |
| 20 | + assertEquals("HEEx", fileType.name) |
| 21 | + assertEquals("heex", fileType.defaultExtension) |
| 22 | + assertNotNull(fileType.icon) |
| 23 | + } |
| 24 | + |
| 25 | + fun testDefaultTemplateDataLanguageIsHtml() { |
| 26 | + val fileType = org.elixir_lang.heex.file.Type.INSTANCE as org.elixir_lang.heex.file.Type |
| 27 | + assertEquals(HTMLLanguage.INSTANCE, fileType.defaultTemplateDataLanguage()) |
| 28 | + } |
| 29 | + |
| 30 | + fun testEexDefaultTemplateDataLanguageIsNull() { |
| 31 | + val fileType = org.elixir_lang.eex.file.Type.INSTANCE as org.elixir_lang.eex.file.Type |
| 32 | + assertNull(fileType.defaultTemplateDataLanguage()) |
| 33 | + } |
| 34 | + |
| 35 | + fun testViewProviderUsesHtmlForHeexFile() { |
| 36 | + val file = myFixture.configureByText("template.heex", "<div><%= @name %></div>") |
| 37 | + val viewProvider = file.viewProvider |
| 38 | + |
| 39 | + assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java) |
| 40 | + val templateViewProvider = viewProvider as ConfigurableTemplateLanguageFileViewProvider |
| 41 | + assertEquals(HTMLLanguage.INSTANCE, templateViewProvider.templateDataLanguage) |
| 42 | + } |
| 43 | + |
| 44 | + fun testViewProviderLanguagesIncludeElixir() { |
| 45 | + val file = myFixture.configureByText("template.heex", "<div><%= @name %></div>") |
| 46 | + val languages = file.viewProvider.languages |
| 47 | + |
| 48 | + assertTrue("Should contain EEx language", languages.any { it.isKindOf(EExLanguage.INSTANCE) }) |
| 49 | + assertTrue("Should contain HTML language", languages.any { it.isKindOf(HTMLLanguage.INSTANCE) }) |
| 50 | + assertTrue("Should contain Elixir language", languages.any { it.isKindOf(ElixirLanguage.INSTANCE) }) |
| 51 | + } |
| 52 | + |
| 53 | + fun testEexFileDoesNotDefaultToHtml() { |
| 54 | + val file = myFixture.configureByText("template.eex", "<%= @name %>") |
| 55 | + val viewProvider = file.viewProvider |
| 56 | + |
| 57 | + assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java) |
| 58 | + val templateViewProvider = viewProvider as ConfigurableTemplateLanguageFileViewProvider |
| 59 | + // Plain .eex without double extension should NOT be HTML |
| 60 | + assertFalse( |
| 61 | + "Plain .eex should not default to HTML", |
| 62 | + templateViewProvider.templateDataLanguage == HTMLLanguage.INSTANCE |
| 63 | + ) |
| 64 | + } |
| 65 | + |
| 66 | + fun testHtmlEexFileUsesHtml() { |
| 67 | + val file = myFixture.configureByText("template.html.eex", "<div><%= @name %></div>") |
| 68 | + val viewProvider = file.viewProvider |
| 69 | + |
| 70 | + assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java) |
| 71 | + val templateViewProvider = viewProvider as ConfigurableTemplateLanguageFileViewProvider |
| 72 | + assertEquals(HTMLLanguage.INSTANCE, templateViewProvider.templateDataLanguage) |
| 73 | + } |
| 74 | + |
| 75 | + fun testHeexWithEexTags() { |
| 76 | + val file = myFixture.configureByText( |
| 77 | + "page.heex", |
| 78 | + """ |
| 79 | + <div class="container"> |
| 80 | + <%= if @show do %> |
| 81 | + <p><%= @message %></p> |
| 82 | + <% end %> |
| 83 | + </div> |
| 84 | + """.trimIndent() |
| 85 | + ) |
| 86 | + assertNotNull(file) |
| 87 | + assertFalse(file.text.isEmpty()) |
| 88 | + } |
| 89 | + |
| 90 | + // ------------------------------------------------------------------ |
| 91 | + // HEEx-specific syntax patterns (from Phoenix LiveView assigns-eex guide). |
| 92 | + // These use curly-brace expressions that our EEx parser does not handle |
| 93 | + // natively -- they are treated as HTML text. These tests verify the files |
| 94 | + // still parse without exceptions. |
| 95 | + // ------------------------------------------------------------------ |
| 96 | + |
| 97 | + fun testHeexCurlyBraceAssignExpression() { |
| 98 | + // {@ ...} assign access syntax |
| 99 | + assertHeexParses( |
| 100 | + """ |
| 101 | + <div id={"user_#{@user.id}"}> |
| 102 | + {@user.name} |
| 103 | + </div> |
| 104 | + """.trimIndent() |
| 105 | + ) |
| 106 | + } |
| 107 | + |
| 108 | + fun testHeexCurlyBraceFunctionCall() { |
| 109 | + // {function(...)} expression syntax |
| 110 | + assertHeexParses( |
| 111 | + """ |
| 112 | + <h1>{expand_title(@title)}</h1> |
| 113 | + """.trimIndent() |
| 114 | + ) |
| 115 | + } |
| 116 | + |
| 117 | + fun testHeexFunctionComponent() { |
| 118 | + // <.component /> syntax |
| 119 | + assertHeexParses( |
| 120 | + """ |
| 121 | + <.show_name name={@user.name} /> |
| 122 | + """.trimIndent() |
| 123 | + ) |
| 124 | + } |
| 125 | + |
| 126 | + fun testHeexFunctionComponentWithBody() { |
| 127 | + // <.component>...</.component> syntax with inner block |
| 128 | + assertHeexParses( |
| 129 | + """ |
| 130 | + <div class="card"> |
| 131 | + <.card_header title={@title} class={@title_class} /> |
| 132 | + <.card_body> |
| 133 | + {render_slot(@inner_block)} |
| 134 | + </.card_body> |
| 135 | + <.card_footer on_close={@on_close} /> |
| 136 | + </div> |
| 137 | + """.trimIndent() |
| 138 | + ) |
| 139 | + } |
| 140 | + |
| 141 | + fun testHeexAssignsSpread() { |
| 142 | + // {assigns} spread syntax |
| 143 | + assertHeexParses( |
| 144 | + """ |
| 145 | + <div class="card"> |
| 146 | + <.card_header {assigns} /> |
| 147 | + </div> |
| 148 | + """.trimIndent() |
| 149 | + ) |
| 150 | + } |
| 151 | + |
| 152 | + fun testHeexForComprehension() { |
| 153 | + // :for special attribute |
| 154 | + assertHeexParses( |
| 155 | + """ |
| 156 | + <section :for={post <- @posts}> |
| 157 | + <h1>{expand_title(post.title)}</h1> |
| 158 | + </section> |
| 159 | + """.trimIndent() |
| 160 | + ) |
| 161 | + } |
| 162 | + |
| 163 | + fun testHeexForComprehensionWithKey() { |
| 164 | + // :for + :key special attributes |
| 165 | + assertHeexParses( |
| 166 | + """ |
| 167 | + <section :for={post <- @posts} :key={post.id}> |
| 168 | + <h1>{expand_title(post.title)}</h1> |
| 169 | + </section> |
| 170 | + """.trimIndent() |
| 171 | + ) |
| 172 | + } |
| 173 | + |
| 174 | + fun testHeexEexForComprehension() { |
| 175 | + // Traditional EEx for comprehension (still supported in HEEx) |
| 176 | + assertHeexParses( |
| 177 | + """ |
| 178 | + <%= for post <- @posts do %> |
| 179 | + <section> |
| 180 | + <h1>{expand_title(post.title)}</h1> |
| 181 | + </section> |
| 182 | + <% end %> |
| 183 | + """.trimIndent() |
| 184 | + ) |
| 185 | + } |
| 186 | + |
| 187 | + fun testHeexMixedSyntax() { |
| 188 | + // Realistic template mixing EEx tags and HEEx curly-brace expressions |
| 189 | + assertHeexParses( |
| 190 | + """ |
| 191 | + <div class="container"> |
| 192 | + <%= if @show do %> |
| 193 | + <.header title={@page_title} /> |
| 194 | + <section :for={item <- @items} :key={item.id}> |
| 195 | + <p>{item.name}</p> |
| 196 | + <p><%= item.description %></p> |
| 197 | + </section> |
| 198 | + <% end %> |
| 199 | + </div> |
| 200 | + """.trimIndent() |
| 201 | + ) |
| 202 | + } |
| 203 | + |
| 204 | + private fun assertHeexParses(content: String) { |
| 205 | + val file = myFixture.configureByText("template.heex", content) |
| 206 | + assertNotNull("File should be created", file) |
| 207 | + assertFalse("File should not be empty", file.text.isEmpty()) |
| 208 | + |
| 209 | + val viewProvider = file.viewProvider |
| 210 | + assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java) |
| 211 | + assertEquals( |
| 212 | + HTMLLanguage.INSTANCE, |
| 213 | + (viewProvider as ConfigurableTemplateLanguageFileViewProvider).templateDataLanguage |
| 214 | + ) |
| 215 | + } |
| 216 | +} |
0 commit comments