Skip to content
Open
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
37 changes: 16 additions & 21 deletions apps/docs/__tests__/LandingPageBehavior_.test.res
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
open ReactRouter
open Vitest

let expectedExample = `module Button = {
@react.component
let make = (~count) => {
let times = switch count {
| 1 => "once"
| 2 => "twice"
| n => n->Int.toString ++ " times"
}
let text = \`Click me $\{times\}\`

<button> {text->React.string} </button>
}
}`

let getOpenGraphImageUrl = () => {
switch document->WebAPI.Document.querySelector("meta[property='og:image']") {
| Value(meta) =>
Expand All @@ -29,7 +15,7 @@ let getOpenGraphImageUrl = () => {
test("landing page Open Graph image targets its absolute page URL", async () => {
let _screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)
let pageUrl = WebAPI.URL.make(~url=Env.root_url)
Expand All @@ -52,7 +38,7 @@ test(
async () => {
let screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)

Expand Down Expand Up @@ -82,14 +68,14 @@ test(
->Nullable.toOption

expect(decodedCode->Option.isSome)->toBe(true)
expect(decodedCode->Option.getOrThrow)->toBe(expectedExample)
expect(decodedCode->Option.getOrThrow)->toBe(LandingPageFixture.expectedExample)
},
)

test("landing page playground hero renders highlighted code tokens", async () => {
let screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)

Expand All @@ -111,12 +97,21 @@ test("landing page playground hero renders highlighted code tokens", async () =>

expect(rescriptCodeBlock.innerHTML->String.includes("<span"))->toBe(true)
expect(javascriptCodeBlock.innerHTML->String.includes("<span"))->toBe(true)
expect(rescriptCodeBlock.textContent->Null.toOption)->toEqual(
Some(LandingPageFixture.expectedExample),
)
expect(javascriptCodeBlock.textContent->Null.toOption)->toEqual(
Some(LandingPageFixture.expectedJavascript),
)
expect(rescriptCodeBlock.className)->toBe("hljs lang-res dark")
expect(javascriptCodeBlock.className)->toBe("hljs lang-js dark")
expect(rescriptCodeBlock->WebAPI.Element.querySelector("button")->Null.toOption)->toEqual(None)
})

test("landing page renders its critical sections", async () => {
let screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)

Expand All @@ -142,7 +137,7 @@ test("landing page renders its critical sections", async () => {
test("landing page copy button shows success feedback", async () => {
let screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)

Expand All @@ -160,7 +155,7 @@ test(

let screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)

Expand Down
31 changes: 31 additions & 0 deletions apps/docs/__tests__/LandingPagePlaygroundLoader_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
open Vitest

test(
"homepage example loader produces only serializable highlighted code and its URL",
async () => {
let data = LandingPagePlaygroundLoader.build()
let serialized = data->JSON.stringifyAny->Option.getOrThrow
let decoded = JSON.parseOrThrow(serialized)
let expected = JSON.Object(
Dict.fromArray([
("rescriptHtml", JSON.String(data.rescriptHtml)),
("javascriptHtml", JSON.String(data.javascriptHtml)),
("playgroundHref", JSON.String(data.playgroundHref)),
]),
)

expect(decoded)->toEqual(expected)
expect(data.rescriptHtml->String.includes("<span"))->toBe(true)
expect(data.javascriptHtml->String.includes("<span"))->toBe(true)
let {pathname, searchParams} = WebAPI.URL.make(
~url=data.playgroundHref,
~base="https://rescript-lang.org",
)
let code = searchParams->WebAPI.URLSearchParams.get("code")

expect(pathname)->toBe("/try")
expect(LzString.lzString.decompressFromEncodedURIComponent(code))->toBe(
LandingPageFixture.expectedExample,
)
},
)
2 changes: 1 addition & 1 deletion apps/docs/__tests__/visual/LandingPage_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) =

let screen = await render(
<MemoryRouter initialEntries=["/"]>
<LandingPage />
<LandingPage playgroundData=LandingPageFixture.playgroundData />
</MemoryRouter>,
)

Expand Down
4 changes: 2 additions & 2 deletions apps/docs/app/routes/LandingPage.res
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@react.component
let make = () => {
let make = (~playgroundData) => {
<>
<Meta
title="The ReScript Programming Language"
Expand All @@ -16,7 +16,7 @@ let make = () => {
<div className="mt-16 md:mt-32 lg:mt-40 mb-12">
<LandingPageIntro />
</div>
<LandingPagePlayground />
<LandingPagePlayground playgroundData />
<LandingPageQuickInstall />
<LandingPageMainSellingPoints />
<LandingPageOtherSellingPoints />
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/routes/LandingPage.resi
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/** Use hosted Inter faces within the homepage without changing other routes. */
@react.component
let make: unit => React.element
let make: (~playgroundData: LandingPagePlayground.playgroundData) => React.element
12 changes: 9 additions & 3 deletions apps/docs/app/routes/LandingPageRoute.res
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ let links = () => [
fontPreload("/fonts/red-hat-mono-700.woff2"),
]

type loaderData = {playgroundData: LandingPagePlayground.playgroundData}

let loader: ReactRouter.Loader.t<loaderData> = async _ => {
Comment thread
jderochervlk marked this conversation as resolved.
playgroundData: LandingPagePlaygroundLoader.build(),
}

@react.component
let default = () => {
<>
<LandingPage />
</>
let {playgroundData}: loaderData = ReactRouter.useLoaderData()
<LandingPage playgroundData />
}
6 changes: 6 additions & 0 deletions apps/docs/app/routes/LandingPageRoute.resi
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ type fontPreload = {
/** Preload above-the-fold faces, including Inter 700 only at the desktop breakpoint. */
let links: unit => array<fontPreload>

type loaderData = {playgroundData: LandingPagePlayground.playgroundData}

/** Prerender the fixed example so highlighting and URL compression stay off the client. */
let loader: ReactRouter.Loader.t<loaderData>

@react.component
let default: unit => React.element
4 changes: 4 additions & 0 deletions apps/docs/e2e/bindings/Cypress.res
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ external shouldValue: (chain<elements>, @as("have.value") _, string) => chain<el
@send
external shouldAttribute: (chain<elements>, @as("have.attr") _, string, string) => chain<elements> =
"should"
@send
external shouldProperty: (chain<elements>, @as("have.prop") _, string, string) => chain<elements> =
"should"
@send external attribute: (chain<elements>, @as("attr") _, string) => chain<string> = "invoke"
@send external propertyInt: (chain<'a>, string) => chain<int> = "its"
@send external shouldSatisfy: (chain<'a>, 'a => unit) => chain<'a> = "should"
Expand Down Expand Up @@ -125,6 +128,7 @@ type parser
@get external documentElement: Dom.document => Dom.element = "documentElement"
@get external currentScript: Dom.document => Null.t<Dom.element> = "currentScript"
@get external textContent: Dom.element => Null.t<string> = "textContent"
@get external innerHTML: Dom.element => string = "innerHTML"
@get external outerHTML: Dom.element => string = "outerHTML"
@send external getAttribute: (Dom.element, string) => Null.t<string> = "getAttribute"

Expand Down
61 changes: 61 additions & 0 deletions apps/docs/e2e/homepage/HomepageHighlighting.cy.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
open Cypress
open HomepageHelpers

it("initial homepage scripts do not include example preparation or compression", () => {
homepageDocument(document => {
let scripts = document->initialScriptUrls
expect(scripts->Array.length)->greaterThan(0)
scripts->Array.forEach(
asset =>
request(asset)
->then(
response => {
expect(response.status, ~message=asset)->equal(200)
expect(response.body, ~message=asset)->notInclude("compressToEncodedURIComponent")
expect(response.body, ~message=asset)->notInclude("function Playground$Button(props)")
},
)
->ignore,
)
})
})

it("prepared examples survive hydration and navigation back from documentation", () => {
homepageDocument(document => {
let examples = ["res", "js"]->Array.map(
language => {
let selector = `code.lang-${language}`
let html =
document
->querySelector(selector)
->Null.toOption
->Option.map(element => element->innerHTML)
->Option.getOrThrow
(selector, html)
},
)
let playgroundHref =
document
->querySelector(`a[href^="/try?code="]`)
->Null.toOption
->Option.flatMap(element => element->getAttribute("href")->Null.toOption)
->Option.getOrThrow
expect(playgroundHref)->match_(/^\/try\?code=.+/)
examples->Array.forEach(((_, html)) => expect(html)->include_(`<span class="hljs-`))
visit("/")
examples->Array.forEach(
((selector, html)) => get(selector)->shouldProperty("innerHTML", html)->ignore,
)
containsInRegex("a", /^Docs$/)->click->ignore
cyLocation("pathname")->shouldEqual("/docs/manual/introduction")->ignore
get(`a[aria-label="homepage"]`)->click->ignore
cyLocation("pathname")->shouldEqual("/")->ignore
examples->Array.forEach(
((selector, html)) =>
get(selector)->should("be.visible")->shouldProperty("innerHTML", html)->ignore,
)
containsIn("a", "Edit this example in Playground")
->shouldAttribute("href", playgroundHref)
->ignore
})
})
19 changes: 19 additions & 0 deletions apps/docs/src/bindings/HighlightJsBindings.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type t
type language
type highlightOptions = {language: string}
type highlightResult = {value: string}

@module("highlight.js/lib/core") @scope("default")
external make: unit => t = "newInstance"

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

@module("highlightjs-rescript")
external rescript: language = "default"

@send
external registerLanguage: (t, string, language) => unit = "registerLanguage"

@send
external highlight: (t, string, highlightOptions) => highlightResult = "highlight"
57 changes: 14 additions & 43 deletions apps/docs/src/components/LandingPagePlayground.res
Original file line number Diff line number Diff line change
@@ -1,46 +1,11 @@
type example = {
res: string,
js: string,
}

let example = {
res: `module Button = {
@react.component
let make = (~count) => {
let times = switch count {
| 1 => "once"
| 2 => "twice"
| n => n->Int.toString ++ " times"
}
let text = \`Click me $\{times\}\`

<button> {text->React.string} </button>
}
}`,
js: `import * as JsxRuntime from "react/jsx-runtime";

function Playground$Button(props) {
let count = props.count;
let times = count !== 1 ? (
count !== 2 ? count.toString() + " times" : "twice"
) : "once";
let text = "Click me " + times;
return JsxRuntime.jsx("button", {
children: text
});
}

let Button = {
make: Playground$Button
};

export {
Button,
}`,
type playgroundData = {
rescriptHtml: string,
javascriptHtml: string,
playgroundHref: string,
}

@react.component
let make = () => {
let make = (~playgroundData: playgroundData) => {
<section dataTestId="landing-playground-hero" className="relative mt-20 bg-gray-10">
<div className="relative mx-auto w-full pt-6 pb-8 sm:px-8 md:px-16 max-w-[1400px]">
<div
Expand All @@ -53,7 +18,10 @@ let make = () => {
{React.string("Write in ReScript")}
</div>
<pre className="text-14 px-8 pt-6 pb-12 whitespace-pre-wrap">
{HighlightJs.renderHLJS(~darkmode=true, ~code=example.res, ~lang="res", ())}
<code
className="hljs lang-res dark"
dangerouslySetInnerHTML={"__html": playgroundData.rescriptHtml}
/>
</pre>
</div>
<div className="md:w-1/2 ">
Expand All @@ -63,12 +31,15 @@ let make = () => {
{React.string("Compile to JavaScript")}
</div>
<pre className="text-14 px-8 pt-6 pb-14 md:border-l border-gray-80 whitespace-pre-wrap">
{HighlightJs.renderHLJS(~darkmode=true, ~code=example.js, ~lang="js", ())}
<code
className="hljs lang-js dark"
dangerouslySetInnerHTML={"__html": playgroundData.javascriptHtml}
/>
</pre>
</div>
</div>
<ReactRouter.Link.String
to={`/try?code=${LzString.lzString.compressToEncodedURIComponent(example.res)}`}
to=playgroundData.playgroundHref
className="captions md:px-0 border-b border-gray-40 hover:border-gray-60 text-gray-60"
>
{React.string("Edit this example in Playground")}
Expand Down
9 changes: 8 additions & 1 deletion apps/docs/src/components/LandingPagePlayground.resi
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
type playgroundData = {
rescriptHtml: string,
javascriptHtml: string,
playgroundHref: string,
}

/** Render only trusted highlighted markup produced by the homepage loader. */
@react.component
let make: unit => React.element
let make: (~playgroundData: playgroundData) => React.element
Loading
Loading