Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ jobs:
run: yarn workspace @rescript-lang/docs ci:test:scripts
- name: Guide Vitest
run: yarn workspace @rescript-lang/guide ci:test
- name: Guide browser history E2E
env:
# Keep the prerender server and requests on the same localhost address in Docker.
NODE_OPTIONS: --dns-result-order=ipv4first
run: yarn workspace @rescript-lang/guide test:e2e
- name: Upload guide browser test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: guide-browser-test-artifacts
path: apps/guide/test-results
Homepage_E2E:
runs-on: ubuntu-latest
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ apps/guide/.vitest-attachments

# Browser test artifacts
apps/docs/test-results/
apps/guide/test-results/

# Lighthouse artifacts
.lighthouseci/
4 changes: 2 additions & 2 deletions apps/docs/__tests__/BlogArticleBehavior_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ test("blog article marks body content for DocSearch crawling", async () => {
await viewport(1440, 900)

let _screen = await render(
<BrowserRouter>
<MemoryRouter>
<BlogArticle frontmatter=mockFrontmatter isArchived=false path="/blog/test-article">
<p> {React.string("This is the blog post body content for testing.")} </p>
</BlogArticle>
</BrowserRouter>,
</MemoryRouter>,
)

switch document->WebAPI.Document.querySelector("article.DocSearch-content") {
Expand Down
62 changes: 62 additions & 0 deletions apps/docs/__tests__/CopyButtons_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
open Vitest

test(
"code copy waits for the write, copies exact code, and permits retry after failure",
async () => {
let copied = ref(None)
let complete = ref(_ => ())
let writeClipboard = code => {
copied := Some(code)
Promise.make((resolve, _) => complete := resolve)
}
let screen = await render(<CodeCopyButton code="let answer = 42" writeClipboard />)
let button = await screen->getByLabelText("Copy code")
await button->click
await element(button)->toBeDisabled
expect(copied.contents)->toEqual(Some("let answer = 42"))
complete.contents(Error(Clipboard.WriteFailed))
let retry = await screen->getByLabelText("Copy failed, try again")
await element(retry)->notToBeDisabled
await retry->click
complete.contents(Ok())
let feedback = await screen->getByText("Copied to clipboard")
await element(feedback)->toBeVisible
await element(button)->toBeDisabled
await screen->unmount
await element(feedback)->notToBeInTheDocument
},
)

test(
"share copy writes the full URL and shows success only after the write completes",
async () => {
let copied = ref(None)
let complete = ref(_ => ())
let writeClipboard = url => {
copied := Some(url)
Promise.make((resolve, _) => complete := resolve)
}
let screen = await render(<PlaygroundShareButton writeClipboard />)
let button = await screen->getByText("Copy Share Link")
await button->click
await element(button)->toBeDisabled
expect(copied.contents)->toEqual(Some(window.location.href))
complete.contents(Error(Clipboard.WriteFailed))
let retry = await screen->getByText("Copy failed, try again")
await element(retry)->notToBeDisabled
await retry->click
complete.contents(Ok())
await element(await screen->getByText("Copied to clipboard!"))->toBeVisible
},
)

test("a previous share write cannot change feedback for a new share link", async () => {
let complete = ref(_ => ())
let writeClipboard = _ => Promise.make((resolve, _) => complete := resolve)
let screen = await render(<PlaygroundShareButton key="old-link" writeClipboard />)
await (await screen->getByText("Copy Share Link"))->click
await screen->rerender(<PlaygroundShareButton key="new-link" writeClipboard />)
complete.contents(Ok())
await element(await screen->getByText("Copy Share Link"))->notToBeDisabled
await element(await screen->getByText("Copied to clipboard!"))->notToBeInTheDocument
})
47 changes: 13 additions & 34 deletions apps/docs/__tests__/HighlightLanguages_.test.res
Original file line number Diff line number Diff line change
@@ -1,50 +1,29 @@
open Vitest

type registeredLanguage
type highlightOptions = {language: string}
type highlightResult = {value: string}

@module("highlight.js/lib/core") @scope("default")
external createHighlighter: unit => HighlightLanguages.highlighter = "newInstance"

@module("highlight.js/lib/languages/javascript")
external javascript: HighlightLanguages.definition = "default"

@module("highlight.js/lib/languages/plaintext")
external plaintext: HighlightLanguages.definition = "default"

@send
external getLanguage: (HighlightLanguages.highlighter, string) => option<registeredLanguage> =
"getLanguage"

@send
external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult =
"highlight"

test("registering a missing language makes its grammar and aliases available", async () => {
let highlighter = createHighlighter()
let highlighter = HighlightLanguages.make(HighlightLanguages.defaultInstance)

expect(highlighter->getLanguage("javascript"))->toEqual(None)
expect(highlighter->getLanguage("js"))->toEqual(None)
highlighter->HighlightLanguages.ensureRegistered("javascript", javascript)
expect(highlighter->HighlightLanguages.getLanguage("javascript"))->toEqual(None)
expect(highlighter->HighlightLanguages.getLanguage("js"))->toEqual(None)
highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.javascript)

let result = highlighter->highlight("const answer = 42;", {language: "js"})
let result = highlighter->HighlightLanguages.highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"<span class=\"hljs-keyword\">const</span> answer = <span class=\"hljs-number\">42</span>;",
)
})

test("registering an existing name or alias does not replace its grammar", async () => {
let highlighter = createHighlighter()
highlighter->HighlightLanguages.ensureRegistered("javascript", javascript)
let original = highlighter->getLanguage("javascript")
let highlighter = HighlightLanguages.make(HighlightLanguages.defaultInstance)
highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.javascript)
let original = highlighter->HighlightLanguages.getLanguage("javascript")

highlighter->HighlightLanguages.ensureRegistered("javascript", plaintext)
highlighter->HighlightLanguages.ensureRegistered("js", plaintext)
highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.text)
highlighter->HighlightLanguages.ensureRegistered("js", HighlightJsBindings.text)

expect(highlighter->getLanguage("javascript"))->toBe(original)
expect(highlighter->getLanguage("js"))->toBe(original)
let result = highlighter->highlight("const answer = 42;", {language: "js"})
expect(highlighter->HighlightLanguages.getLanguage("javascript"))->toBe(original)
expect(highlighter->HighlightLanguages.getLanguage("js"))->toBe(original)
let result = highlighter->HighlightLanguages.highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"<span class=\"hljs-keyword\">const</span> answer = <span class=\"hljs-number\">42</span>;",
)
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/__tests__/NavbarTertiaryBehavior_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ test("mobile drawer can be toggled open", async () => {
await viewport(600, 1200)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<NavbarTertiary sidebar=sidebarContent>
breadcrumbs
editLink
</NavbarTertiary>
</BrowserRouter>,
</MemoryRouter>,
)

let sidebar = await screen->getByTestId("sidebar-categories")
Expand Down
65 changes: 21 additions & 44 deletions apps/docs/__tests__/Search_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ let makeHit = (~type_: DocSearch.contentType, ~url: string): DocSearch.docSearch
_snippetResult: {content: Nullable.null},
}

let makeCrawlerHit = (): DocSearch.docSearchHit => {
objectID: "crawler-hit",
content: Nullable.make("map(array, fn) returns a new array."),
url: "https://rescript-lang.org/docs/manual/api/stdlib/array/#value-map",
url_without_anchor: Nullable.undefined,
type_: Content,
anchor: Nullable.undefined,
deprecated: None,
hierarchy: {
lvl0: Nullable.make("Array"),
lvl1: Nullable.make("map"),
lvl2: Nullable.null,
lvl3: Nullable.null,
lvl4: Nullable.null,
lvl5: Nullable.null,
lvl6: Nullable.null,
},
}

let withHierarchy = (
hit: DocSearch.docSearchHit,
~lvl0: string,
Expand Down Expand Up @@ -268,28 +287,7 @@ test("hitComponent routes relative hit URLs through React Router", async () => {
test(
"getContentHtml falls back to plain content when crawler hit has no snippet result",
async () => {
let hit: DocSearch.docSearchHit = Obj.magic(
Dict.fromArray([
("objectID", "crawler-hit"),
("content", "map(array, fn) returns a new array."),
("url", "https://rescript-lang.org/docs/manual/api/stdlib/array/#value-map"),
("type", "content"),
(
"hierarchy",
Obj.magic(
Dict.fromArray([
("lvl0", Obj.magic("Array")),
("lvl1", Obj.magic("map")),
("lvl2", Obj.magic(Nullable.null)),
("lvl3", Obj.magic(Nullable.null)),
("lvl4", Obj.magic(Nullable.null)),
("lvl5", Obj.magic(Nullable.null)),
("lvl6", Obj.magic(Nullable.null)),
]),
),
),
]),
)
let hit: DocSearch.docSearchHit = makeCrawlerHit()

expect(SearchResults.getContentHtml(hit))->toEqual(Some("map(array, fn) returns a new array."))
},
Expand Down Expand Up @@ -480,28 +478,7 @@ test("normalizeHitUrls rewrites absolute site URLs to relative paths", async ()
})

test("normalizeHitUrls tolerates crawler hits without url_without_anchor", async () => {
let hit: DocSearch.docSearchHit = Obj.magic(
Dict.fromArray([
("objectID", "crawler-hit"),
("content", "map(array, fn) returns a new array."),
("url", "https://rescript-lang.org/docs/manual/api/stdlib/array/#value-map"),
("type", "content"),
(
"hierarchy",
Obj.magic(
Dict.fromArray([
("lvl0", Obj.magic("Array")),
("lvl1", Obj.magic("map")),
("lvl2", Obj.magic(Nullable.null)),
("lvl3", Obj.magic(Nullable.null)),
("lvl4", Obj.magic(Nullable.null)),
("lvl5", Obj.magic(Nullable.null)),
("lvl6", Obj.magic(Nullable.null)),
]),
),
),
]),
)
let hit: DocSearch.docSearchHit = makeCrawlerHit()

let result = SearchResults.normalizeHitUrls([hit], ~siteUrl="https://rescript-lang.org/")

Expand Down
4 changes: 2 additions & 2 deletions apps/docs/__tests__/SyntaxLookupBehavior_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ test("syntax lookup detail marks active content for DocSearch crawling", async (
await viewport(1440, 900)

let _screen = await render(
<BrowserRouter>
<MemoryRouter>
<SyntaxLookup mdxSources=mockItems activeItem={mockItems->Array.getUnsafe(0)}>
<p> {React.string("Detail content for @as decorator.")} </p>
</SyntaxLookup>
</BrowserRouter>,
</MemoryRouter>,
)

switch document->WebAPI.Document.querySelector(".DocSearch-content h1") {
Expand Down
24 changes: 12 additions & 12 deletions apps/docs/__tests__/visual/BlogArticle_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ test("desktop blog article renders header, author, date, and body", async () =>
await viewport(1440, 900)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<div dataTestId="blog-article-wrapper">
<BlogArticle frontmatter=mockFrontmatter isArchived=false path="/blog/test-article">
<div> {React.string("This is the blog post body content for testing.")} </div>
</BlogArticle>
</div>
</BrowserRouter>,
</MemoryRouter>,
)

let title = await screen->getByText("Test Blog Post Title")
Expand All @@ -50,13 +50,13 @@ test("desktop archived blog article shows warning banner", async () => {
await viewport(1440, 900)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<div dataTestId="blog-article-wrapper">
<BlogArticle frontmatter=mockFrontmatter isArchived=true path="/blog/test-article">
<div> {React.string("This is the blog post body content for testing.")} </div>
</BlogArticle>
</div>
</BrowserRouter>,
</MemoryRouter>,
)

let important = await screen->getByText("Important:")
Expand All @@ -70,13 +70,13 @@ test("mobile blog article", async () => {
await viewport(600, 1200)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<div dataTestId="blog-article-wrapper">
<BlogArticle frontmatter=mockFrontmatter isArchived=false path="/blog/test-article">
<div> {React.string("This is the blog post body content for testing.")} </div>
</BlogArticle>
</div>
</BrowserRouter>,
</MemoryRouter>,
)

let title = await screen->getByText("Test Blog Post Title")
Expand Down Expand Up @@ -115,15 +115,15 @@ test("desktop blog article with co-authors shows all authors", async () => {
await viewport(1440, 900)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<div dataTestId="blog-article-wrapper">
<BlogArticle
frontmatter=mockFrontmatterWithCoAuthors isArchived=false path="/blog/collab-post"
>
<div> {React.string("Collaborative content.")} </div>
</BlogArticle>
</div>
</BrowserRouter>,
</MemoryRouter>,
)

let mainAuthor = await screen->getByText("Test Author")
Expand Down Expand Up @@ -151,15 +151,15 @@ test("desktop blog article with article image shows image", async () => {
await viewport(1440, 900)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<div dataTestId="blog-article-wrapper">
<BlogArticle
frontmatter=mockFrontmatterWithArticleImg isArchived=false path="/blog/image-post"
>
<div> {React.string("Content below the article image.")} </div>
</BlogArticle>
</div>
</BrowserRouter>,
</MemoryRouter>,
)

let title = await screen->getByText("Blog Post With Article Image")
Expand All @@ -185,13 +185,13 @@ test("desktop blog article without description", async () => {
await viewport(1440, 900)

let screen = await render(
<BrowserRouter>
<MemoryRouter>
<div dataTestId="blog-article-wrapper">
<BlogArticle frontmatter=mockFrontmatterNoDescription isArchived=false path="/blog/no-desc">
<div> {React.string("Content without a description above.")} </div>
</BlogArticle>
</div>
</BrowserRouter>,
</MemoryRouter>,
)

let title = await screen->getByText("Blog Post Without Description")
Expand Down
Loading
Loading