Skip to content

Commit

Permalink
Add Basic Integration Tests (#11)
Browse files Browse the repository at this point in the history
This PR adds some initial integration tests for the helper function part of this library; by integration tests I mean that I am only tested exported functionality of the library, by creating a Vite Fragment and then checking the contents of that generated HTML fragment.

The tests are based on the instructions in the [Vite docs for backend integration](https://vitejs.dev/guide/backend-integration), so I have added the manifest they use in those docs, and then I check that each of the HTML tags the docs say should be generated are contained in the fragment we produce with this library.

After writing the tests, they failed initially, as I think we are generating the `modulepreload` links incorrectly. So I have added a change to fix how we generate the `<link>` elements for preloading.

Co-authored-by: Oliver Eilhard <[email protected]>
  • Loading branch information
danclaytondev and olivere authored Sep 10, 2024
1 parent c8f2bf8 commit a7a77c1
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 8 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 'stable'

- name: Test
run: go test -v .
15 changes: 11 additions & 4 deletions fragment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"html/template"
"net/url"
)

// Fragment holds HTML content generated for Vite integration, intended to be
Expand Down Expand Up @@ -104,8 +105,14 @@ func HTMLFragment(config Config) (*Fragment, error) {
// Create a buffer to store the executed template output
var buf bytes.Buffer

// Pass the JoinPath function to the template so we
// can use {{ urljoin .base .path }}
templateFuncs := template.FuncMap{
"urljoin": url.JoinPath,
}

// Parse the predefined headTmpl into a new template
tmpl, err := template.New("vite").Parse(htmlTmpl)
tmpl, err := template.New("vite").Funcs(templateFuncs).Parse(htmlTmpl)
if err != nil {
// Return an error if parsing fails
return nil, fmt.Errorf("vite: parse template: %w", err)
Expand All @@ -128,11 +135,11 @@ func HTMLFragment(config Config) (*Fragment, error) {
const htmlTmpl = `
{{- if .IsDev }}
{{ .PluginReactPreamble }}
<script type="module" src="{{ .ViteURL }}/@vite/client"></script>
<script type="module" src="{{ urljoin .ViteURL "/@vite/client" }}"></script>
{{- if ne .ViteEntry "" }}
<script type="module" src="{{ .ViteURL }}/{{ .ViteEntry }}"></script>
<script type="module" src="{{ urljoin .ViteURL .ViteEntry }}"></script>
{{- else }}
<script type="module" src="{{ .ViteURL }}/src/main.tsx"></script>
<script type="module" src="{{ urljoin .ViteURL "/src/main.tsx" }}"></script>
{{- end }}
{{- else }}
{{- if .StyleSheets }}
Expand Down
209 changes: 209 additions & 0 deletions helper_function_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package vite_test

import (
"fmt"
"io/fs"
"strings"
"testing"
"testing/fstest"

"github.com/olivere/vite"
)

// from https://github.com/vitejs/vite/blob/242f550eb46c93896fca6b55495578921e29a8af/docs/guide/backend-integration.md
const exampleManifest string = `
{
"_shared-CPdiUi_T.js": {
"file": "assets/shared-ChJ_j-JJ.css",
"src": "_shared-CPdiUi_T.js"
},
"_shared-B7PI925R.js": {
"file": "assets/shared-B7PI925R.js",
"name": "shared",
"css": ["assets/shared-ChJ_j-JJ.css"]
},
"baz.js": {
"file": "assets/baz-B2H3sXNv.js",
"name": "baz",
"src": "baz.js",
"isDynamicEntry": true
},
"views/bar.js": {
"file": "assets/bar-gkvgaI9m.js",
"name": "bar",
"src": "views/bar.js",
"isEntry": true,
"imports": ["_shared-B7PI925R.js"],
"dynamicImports": ["baz.js"]
},
"views/foo.js": {
"file": "assets/foo-BRBmoGS9.js",
"name": "foo",
"src": "views/foo.js",
"isEntry": true,
"imports": ["_shared-B7PI925R.js"],
"css": ["assets/foo-5UjPuW-k.css"]
}
}
`

// these are the tags we should be generating based on the manifest
const fooEntrpointTagsBlock string = `
<link rel="stylesheet" href="/assets/foo-5UjPuW-k.css">
<link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css">
<script type="module" src="/assets/foo-BRBmoGS9.js"></script>
<link rel="modulepreload" href="/assets/shared-B7PI925R.js">
`

const barEntrypointTagsBlock string = `
<link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css">
<script type="module" src="/assets/bar-gkvgaI9m.js"></script>
<link rel="modulepreload" href="/assets/shared-B7PI925R.js">
`

func getTestFS() fs.FS {
manifestFile := fstest.MapFile{
Data: []byte(exampleManifest),
}
return fstest.MapFS{
".vite/manifest.json": &manifestFile,
}
}

func TestFragmentContainsTagsForFooEntrpointFromManifest(t *testing.T) {
viteFragment, err := vite.HTMLFragment(vite.Config{
FS: getTestFS(),
IsDev: false,
ViteEntry: "views/foo.js",
})

if err != nil {
t.Fatal("Unable to produce Vite HTML Fragment", err)
}

generatedHTML := string(viteFragment.Tags)

fooEntrypointTags := strings.Split(fooEntrpointTagsBlock, "\n")

for _, tag := range fooEntrypointTags {
if tag == "" {
continue
}

HTMLContainsTag := strings.Contains(generatedHTML, strings.TrimSpace(tag))
if !HTMLContainsTag {
t.Logf(`
------------ Generated HTML: --- %s
`, generatedHTML)
t.Fatalf("Generated HTML block does not contain needed tag: %s", tag)
}
}
}

func TestFragmentContainsTagsForBarEntrpointFromManifest(t *testing.T) {
viteFragment, err := vite.HTMLFragment(vite.Config{
FS: getTestFS(),
IsDev: false,
ViteEntry: "views/bar.js",
})

if err != nil {
t.Fatal("Unable to produce Vite HTML Fragment", err)
}

generatedHTML := string(viteFragment.Tags)

barEntrypointTags := strings.Split(barEntrypointTagsBlock, "\n")

for _, tag := range barEntrypointTags {
if tag == "" {
continue
}

HTMLContainsTag := strings.Contains(generatedHTML, strings.TrimSpace(tag))
if !HTMLContainsTag {
t.Logf(`
------------ Generated HTML: --- %s
`, generatedHTML)
t.Fatalf("Generated HTML block does not contain needed tag: %s", tag)

}
}
}

func TestDevModeFragmentContainsModuleTags(t *testing.T) {
const entrypoint string = "/main.js"

viteFragment, err := vite.HTMLFragment(vite.Config{
FS: getTestFS(),
IsDev: true,
ViteURL: "http://localhost:5173",
ViteEntry: entrypoint,
})

if err != nil {
t.Fatal("Unable to produce Vite HTML Fragment", err)
}

generatedHTML := string(viteFragment.Tags)

const viteClientTag string = `<script type="module" src="http://localhost:5173/@vite/client"></script>`
var entrypointTag string = fmt.Sprintf(`<script type="module" src="http://localhost:5173%s"></script>`, entrypoint)

if !strings.Contains(generatedHTML, viteClientTag) {
t.Fatalf("Generated HTML block does not contain: %s", viteClientTag)
}

if !strings.Contains(generatedHTML, entrypointTag) {
t.Fatalf("Generated HTML block does not contain: %s", entrypointTag)
}
}

func TestDevModeFragmentContainsModuleTagsWithoutEntrypointSet(t *testing.T) {

viteFragment, err := vite.HTMLFragment(vite.Config{
FS: getTestFS(),
IsDev: true,
ViteURL: "http://localhost:5173",
})

if err != nil {
t.Fatal("Unable to produce Vite HTML Fragment", err)
}

generatedHTML := string(viteFragment.Tags)

const viteClientTag string = `<script type="module" src="http://localhost:5173/@vite/client"></script>`
const entrypointTag string = `<script type="module" src="http://localhost:5173/src/main.tsx"></script>`

if !strings.Contains(generatedHTML, viteClientTag) {
t.Fatalf("Generated HTML block does not contain: %s", viteClientTag)
}

if !strings.Contains(generatedHTML, entrypointTag) {
t.Fatalf("Generated HTML block does not contain: %s", entrypointTag)
}
}

func TestDevModeFragmentWorksWithTrailingSlash(t *testing.T) {
const entrypoint string = "main.js"

viteFragment, err := vite.HTMLFragment(vite.Config{
FS: getTestFS(),
IsDev: true,
ViteURL: "http://localhost:5173/",
ViteEntry: entrypoint,
})

if err != nil {
t.Fatal("Unable to produce Vite HTML Fragment", err)
}

generatedHTML := string(viteFragment.Tags)

const viteClientTag string = `<script type="module" src="http://localhost:5173/@vite/client"></script>`

if !strings.Contains(generatedHTML, viteClientTag) {
t.Fatalf("Generated HTML block does not contain: %s", viteClientTag)
}
}
10 changes: 6 additions & 4 deletions manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"strings"
)

Expand Down Expand Up @@ -69,13 +70,14 @@ func (m Manifest) GetChunk(name string) (*Chunk, bool) {
// PluginReactPreamble returns the script tag that should be injected into the
// HTML to enable React Fast Refresh.
func PluginReactPreamble(server string) string {
url, _ := url.JoinPath(server, "/@react-refresh")
return fmt.Sprintf(`<script type="module">
import RefreshRuntime from '%s/@react-refresh'
import RefreshRuntime from '%s'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>`, server)
</script>`, url)
}

// GenerateCSS generates the CSS links for the given chunk.
Expand Down Expand Up @@ -154,10 +156,10 @@ func (m Manifest) GeneratePreloadModules(name string) string {
}

if chunk.File != "" {
sb.WriteString(`<script type="preloadmodule" src="`)
sb.WriteString(`<link rel="modulepreload" href="`)
sb.WriteString("/")
sb.WriteString(chunk.File)
sb.WriteString(`"></script>`)
sb.WriteString(`">`)
}

for _, imp := range chunk.Imports {
Expand Down

0 comments on commit a7a77c1

Please sign in to comment.