From f90811f6bddca4b42313dd8b66221af2fce5eca7 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 9 May 2026 13:06:10 -0400 Subject: [PATCH 1/6] feat: add docs guides section Adds a new /docs/guides/overview section, wires it into the docs route map and secondary navigation, and covers the new nav item in Vitest and Cypress navigation tests. --- .../visual/NavbarSecondary_.test.res | 3 + apps/docs/app/DocsRoutes.res | 6 ++ apps/docs/app/DocsRoutes.resi | 1 + apps/docs/app/routes/DocsGuidesRoute.res | 84 +++++++++++++++++++ apps/docs/app/routes/DocsGuidesRoute.resi | 12 +++ apps/docs/e2e/Navigation.cy.res | 11 +++ .../markdown-pages/docs/guides/overview.mdx | 24 ++++++ apps/docs/src/components/NavbarSecondary.res | 7 ++ packages/shared/src/Path.res | 1 + 9 files changed, 149 insertions(+) create mode 100644 apps/docs/app/routes/DocsGuidesRoute.res create mode 100644 apps/docs/app/routes/DocsGuidesRoute.resi create mode 100644 apps/docs/markdown-pages/docs/guides/overview.mdx diff --git a/apps/docs/__tests__/visual/NavbarSecondary_.test.res b/apps/docs/__tests__/visual/NavbarSecondary_.test.res index 796b30a99..c33eea709 100644 --- a/apps/docs/__tests__/visual/NavbarSecondary_.test.res +++ b/apps/docs/__tests__/visual/NavbarSecondary_.test.res @@ -14,6 +14,7 @@ test("desktop secondary navbar shows all doc section links", async () => { await element(await navbar->getByText("Language Manual"))->toBeVisible await element(await navbar->getByText("API"))->toBeVisible + await element(await navbar->getByText("Guides"))->toBeVisible await element(await navbar->getByText("Syntax Lookup"))->toBeVisible await element(await navbar->getByText("React"))->toBeVisible @@ -33,6 +34,7 @@ test("mobile secondary navbar shows all links", async () => { await element(await navbar->getByText("Language Manual"))->toBeVisible await element(await navbar->getByText("API"))->toBeVisible + await element(await navbar->getByText("Guides"))->toBeVisible await element(await navbar->getByText("Syntax Lookup"))->toBeVisible await element(await navbar->getByText("React"))->toBeVisible @@ -52,6 +54,7 @@ test("secondary navbar highlights active section", async () => { await element(await navbar->getByText("React"))->toBeVisible await element(await navbar->getByText("Language Manual"))->toBeVisible + await element(await navbar->getByText("Guides"))->toBeVisible await element(navbar)->toMatchScreenshot("desktop-navbar-secondary-react-active") }) diff --git a/apps/docs/app/DocsRoutes.res b/apps/docs/app/DocsRoutes.res index 4f4183d7e..7605eb070 100644 --- a/apps/docs/app/DocsRoutes.res +++ b/apps/docs/app/DocsRoutes.res @@ -57,6 +57,11 @@ let docsReactRoutes = route(path, "./routes/DocsReactRoute.jsx", ~options={id: path}) ) +let docsGuidesRoutes = + MdxFile.scanPaths(~dir="markdown-pages/docs/guides", ~alias="docs/guides")->Array.map(path => + route(path, "./routes/DocsGuidesRoute.jsx", ~options={id: path}) + ) + let docsGuidelinesRoutes = MdxFile.scanPaths( ~dir="markdown-pages/docs/guidelines", @@ -95,6 +100,7 @@ let default = [ ...beltRoutes, ...domRoutes, ...docsManualRoutes, + ...docsGuidesRoutes, ...docsReactRoutes, ...docsGuidelinesRoutes, route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}), diff --git a/apps/docs/app/DocsRoutes.resi b/apps/docs/app/DocsRoutes.resi index ebb3514d4..ee0b0c808 100644 --- a/apps/docs/app/DocsRoutes.resi +++ b/apps/docs/app/DocsRoutes.resi @@ -7,6 +7,7 @@ let beltRoutes: array let blogArticleRoutes: array let docsManualRoutes: array let docsReactRoutes: array +let docsGuidesRoutes: array let docsGuidelinesRoutes: array let communityRoutes: array let syntaxLookupDetailRoutes: array diff --git a/apps/docs/app/routes/DocsGuidesRoute.res b/apps/docs/app/routes/DocsGuidesRoute.res new file mode 100644 index 000000000..6b66d7022 --- /dev/null +++ b/apps/docs/app/routes/DocsGuidesRoute.res @@ -0,0 +1,84 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + categories: array, + entries: array, + title: string, + description: string, + filePath: string, +} + +let guidesTableOfContents = async () => { + let groups = + (await MdxFile.loadAllAttributes(~dir="markdown-pages/docs")) + ->Mdx.filterMdxPages("docs/guides") + ->Mdx.groupBySection + ->Dict.mapValues(values => + values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/docs/guides") + ) + + SidebarHelpers.getAllGroups(groups, ["Overview"]) +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/docs/guides", + ~alias="docs/guides", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let description = FrontmatterUtils.getField(frontmatter, "description") + let title = FrontmatterUtils.getField(frontmatter, "title") + + let categories = await guidesTableOfContents() + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + let entries = TocUtils.buildEntries(raw) + + { + compiledMdx, + categories, + entries, + title: `${title} | ReScript Guides`, + description, + filePath, + } +} + +let default = () => { + let {compiledMdx, categories, entries, title, description, filePath} = ReactRouter.useLoaderData() + + let breadcrumbs = list{ + {Url.name: "Docs", href: "/docs/guides/overview"}, + { + Url.name: "Guides", + href: "/docs/guides/overview", + }, + } + + let docsAppRoot = "apps/docs" + let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${docsAppRoot}/${filePath}` + + let activeToc = {TableOfContents.title, entries} + + <> + + }> + + + {React.string("Edit")} + + + +
+ +
+
+ +} diff --git a/apps/docs/app/routes/DocsGuidesRoute.resi b/apps/docs/app/routes/DocsGuidesRoute.resi new file mode 100644 index 000000000..6eea0299c --- /dev/null +++ b/apps/docs/app/routes/DocsGuidesRoute.resi @@ -0,0 +1,12 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + categories: array, + entries: array, + title: string, + description: string, + filePath: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/apps/docs/e2e/Navigation.cy.res b/apps/docs/e2e/Navigation.cy.res index 5bb185a91..0b04f759a 100644 --- a/apps/docs/e2e/Navigation.cy.res +++ b/apps/docs/e2e/Navigation.cy.res @@ -105,6 +105,11 @@ describe("Desktop Navigation", () => { clickNavLink(~testId="navbar-secondary", ~text="API") url()->shouldInclude("/docs/manual/api")->ignore + // Guides + clickNavLink(~testId="navbar-secondary", ~text="Guides") + url()->shouldInclude("/docs/guides/overview")->ignore + get("h1")->shouldContainText("Guides")->ignore + // Syntax Lookup clickNavLink(~testId="navbar-secondary", ~text="Syntax Lookup") url()->shouldInclude("/syntax-lookup")->ignore @@ -198,6 +203,12 @@ describe("Mobile Navigation", () => { clickNavLink(~testId="navbar-secondary", ~text="API") url()->shouldInclude("/docs/manual/api")->ignore + // Guides + cyScrollTo("top") + clickNavLink(~testId="navbar-secondary", ~text="Guides") + url()->shouldInclude("/docs/guides/overview")->ignore + get("h1")->shouldContainText("Guides")->ignore + // Syntax Lookup cyScrollTo("top") clickNavLink(~testId="navbar-secondary", ~text="Syntax Lookup") diff --git a/apps/docs/markdown-pages/docs/guides/overview.mdx b/apps/docs/markdown-pages/docs/guides/overview.mdx new file mode 100644 index 000000000..3a8138f04 --- /dev/null +++ b/apps/docs/markdown-pages/docs/guides/overview.mdx @@ -0,0 +1,24 @@ +--- +title: "Guides" +description: "Task-oriented ReScript guides" +canonical: "/docs/guides/overview" +section: "Overview" +order: 1 +--- + +# Guides + +Task-oriented guides collect workflows that cut across the language manual, API reference, and framework-specific docs. + +## Start Here + +- [ReScript for JavaScript Developers](../manual/rescript-for-javascript-developers.mdx) +- [Converting from JS](../manual/converting-from-js.mdx) +- [Project Structure](../manual/project-structure.mdx) +- [Dead Code Analysis in ReScript](../manual/editor-code-analysis.mdx) + +## React + +- [Beyond JSX](../react/beyond-jsx.mdx) +- [Extensions of Props](../react/extensions-of-props.mdx) +- [Forwarding Refs](../react/forwarding-refs.mdx) diff --git a/apps/docs/src/components/NavbarSecondary.res b/apps/docs/src/components/NavbarSecondary.res index 8e25b3434..a17d05b56 100644 --- a/apps/docs/src/components/NavbarSecondary.res +++ b/apps/docs/src/components/NavbarSecondary.res @@ -31,6 +31,13 @@ let make = () => { > {React.string("API")} + + {React.string("Guides")} + Date: Sat, 9 May 2026 15:18:27 -0400 Subject: [PATCH 2/6] fix: reset docs navbar on route changes --- apps/docs/app/layouts/DocsLayoutRoute.res | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/docs/app/layouts/DocsLayoutRoute.res b/apps/docs/app/layouts/DocsLayoutRoute.res index 3cc313263..d60a507ca 100644 --- a/apps/docs/app/layouts/DocsLayoutRoute.res +++ b/apps/docs/app/layouts/DocsLayoutRoute.res @@ -1,7 +1,9 @@ @react.component let default = () => { + let location = ReactRouter.useLocation() + <> - + string)} /> } From 7fc705758ea17223ea292fd5a3f7d10a1ffe2986 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 9 May 2026 15:27:52 -0400 Subject: [PATCH 3/6] docs: explain docs navbar remount --- apps/docs/app/layouts/DocsLayoutRoute.res | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/docs/app/layouts/DocsLayoutRoute.res b/apps/docs/app/layouts/DocsLayoutRoute.res index d60a507ca..fb19c2034 100644 --- a/apps/docs/app/layouts/DocsLayoutRoute.res +++ b/apps/docs/app/layouts/DocsLayoutRoute.res @@ -3,6 +3,9 @@ let default = () => { let location = ReactRouter.useLocation() <> + // This layout persists across docs route changes. Key the secondary nav by + // pathname so its scroll-direction state cannot keep the mobile nav hidden + // after client-side navigation. string)} /> From 1eb119d2527f5c01ca5a8da508b404e2f73968d4 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 9 May 2026 16:20:09 -0400 Subject: [PATCH 4/6] docs: move package publishing guide Move the existing docs/guidelines package publishing page into the guides section, wire it into the guides sidebar, and add a permanent redirect from the old URL. --- apps/docs/app/DocsRoutes.res | 7 --- apps/docs/app/DocsRoutes.resi | 1 - apps/docs/app/routes/DocsGuidelinesRoute.res | 59 ------------------- apps/docs/app/routes/DocsGuidelinesRoute.resi | 11 ---- apps/docs/app/routes/DocsGuidesRoute.res | 2 +- apps/docs/app/routes/Packages.res | 4 +- .../markdown-pages/docs/guides/overview.mdx | 4 ++ .../publishing-packages.mdx | 4 +- apps/docs/public/_redirects | 1 + apps/docs/scripts/test-redirects.mjs | 5 ++ packages/shared/src/Path.res | 2 +- 11 files changed, 17 insertions(+), 83 deletions(-) delete mode 100644 apps/docs/app/routes/DocsGuidelinesRoute.res delete mode 100644 apps/docs/app/routes/DocsGuidelinesRoute.resi rename apps/docs/markdown-pages/docs/{guidelines => guides}/publishing-packages.mdx (91%) diff --git a/apps/docs/app/DocsRoutes.res b/apps/docs/app/DocsRoutes.res index 7605eb070..2992c859f 100644 --- a/apps/docs/app/DocsRoutes.res +++ b/apps/docs/app/DocsRoutes.res @@ -62,12 +62,6 @@ let docsGuidesRoutes = route(path, "./routes/DocsGuidesRoute.jsx", ~options={id: path}) ) -let docsGuidelinesRoutes = - MdxFile.scanPaths( - ~dir="markdown-pages/docs/guidelines", - ~alias="docs/guidelines", - )->Array.map(path => route(path, "./routes/DocsGuidelinesRoute.jsx", ~options={id: path})) - let communityRoutes = MdxFile.scanPaths(~dir="markdown-pages/community", ~alias="community")->Array.map(path => route(path, "./routes/CommunityRoute.jsx", ~options={id: path}) @@ -102,7 +96,6 @@ let default = [ ...docsManualRoutes, ...docsGuidesRoutes, ...docsReactRoutes, - ...docsGuidelinesRoutes, route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}), ...syntaxLookupDetailRoutes, ], diff --git a/apps/docs/app/DocsRoutes.resi b/apps/docs/app/DocsRoutes.resi index ee0b0c808..51f6d36ee 100644 --- a/apps/docs/app/DocsRoutes.resi +++ b/apps/docs/app/DocsRoutes.resi @@ -8,7 +8,6 @@ let blogArticleRoutes: array let docsManualRoutes: array let docsReactRoutes: array let docsGuidesRoutes: array -let docsGuidelinesRoutes: array let communityRoutes: array let syntaxLookupDetailRoutes: array let default: array diff --git a/apps/docs/app/routes/DocsGuidelinesRoute.res b/apps/docs/app/routes/DocsGuidelinesRoute.res deleted file mode 100644 index d73721675..000000000 --- a/apps/docs/app/routes/DocsGuidelinesRoute.res +++ /dev/null @@ -1,59 +0,0 @@ -type loaderData = { - compiledMdx: CompiledMdx.t, - entries: array, - title: string, - description: string, - filePath: string, -} - -let loader: ReactRouter.Loader.t = async ({request}) => { - let {pathname} = WebAPI.URL.make(~url=request.url) - let filePath = MdxFile.resolveFilePath( - (pathname :> string), - ~dir="markdown-pages/docs/guidelines", - ~alias="docs/guidelines", - ) - - let raw = await Node.Fs.readFile(filePath, "utf-8") - let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) - - let description = FrontmatterUtils.getField(frontmatter, "description") - let title = FrontmatterUtils.getField(frontmatter, "title") - - let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) - - let entries = TocUtils.buildEntries(raw) - - { - compiledMdx, - entries, - title: `${title} | ReScript Guidelines`, - description, - filePath, - } -} - -let default = () => { - let {compiledMdx, entries, title, description, filePath} = ReactRouter.useLoaderData() - - let docsAppRoot = "apps/docs" - let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${docsAppRoot}/${filePath}` - - let categories: array = [] - - <> - - - - {React.string("Edit")} - - - -
- -
-
- -} diff --git a/apps/docs/app/routes/DocsGuidelinesRoute.resi b/apps/docs/app/routes/DocsGuidelinesRoute.resi deleted file mode 100644 index 307767dcb..000000000 --- a/apps/docs/app/routes/DocsGuidelinesRoute.resi +++ /dev/null @@ -1,11 +0,0 @@ -type loaderData = { - compiledMdx: CompiledMdx.t, - entries: array, - title: string, - description: string, - filePath: string, -} - -let loader: ReactRouter.Loader.t - -let default: unit => React.element diff --git a/apps/docs/app/routes/DocsGuidesRoute.res b/apps/docs/app/routes/DocsGuidesRoute.res index 6b66d7022..820e81d92 100644 --- a/apps/docs/app/routes/DocsGuidesRoute.res +++ b/apps/docs/app/routes/DocsGuidesRoute.res @@ -16,7 +16,7 @@ let guidesTableOfContents = async () => { values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/docs/guides") ) - SidebarHelpers.getAllGroups(groups, ["Overview"]) + SidebarHelpers.getAllGroups(groups, ["Overview", "Packages"]) } let loader: ReactRouter.Loader.t = async ({request}) => { diff --git a/apps/docs/app/routes/Packages.res b/apps/docs/app/routes/Packages.res index 3397540de..c8d2e416c 100644 --- a/apps/docs/app/routes/Packages.res +++ b/apps/docs/app/routes/Packages.res @@ -287,11 +287,11 @@ module InfoSidebar = {

{React.string("Guidelines")}

    - + {React.string("Publishing ReScript npm packages")} /*
  • */ - /* */ + /* */ /* {React.string("Writing Bindings & Libraries")} */ /* */ /*
  • */ diff --git a/apps/docs/markdown-pages/docs/guides/overview.mdx b/apps/docs/markdown-pages/docs/guides/overview.mdx index 3a8138f04..977e7e2bf 100644 --- a/apps/docs/markdown-pages/docs/guides/overview.mdx +++ b/apps/docs/markdown-pages/docs/guides/overview.mdx @@ -22,3 +22,7 @@ Task-oriented guides collect workflows that cut across the language manual, API - [Beyond JSX](../react/beyond-jsx.mdx) - [Extensions of Props](../react/extensions-of-props.mdx) - [Forwarding Refs](../react/forwarding-refs.mdx) + +## Packages + +- [Publishing ReScript packages](./publishing-packages.mdx) diff --git a/apps/docs/markdown-pages/docs/guidelines/publishing-packages.mdx b/apps/docs/markdown-pages/docs/guides/publishing-packages.mdx similarity index 91% rename from apps/docs/markdown-pages/docs/guidelines/publishing-packages.mdx rename to apps/docs/markdown-pages/docs/guides/publishing-packages.mdx index 7c0f9d6aa..b57e25fc8 100644 --- a/apps/docs/markdown-pages/docs/guidelines/publishing-packages.mdx +++ b/apps/docs/markdown-pages/docs/guides/publishing-packages.mdx @@ -1,7 +1,9 @@ --- title: "Publishing ReScript packages" description: "Guidelines on publishing ReScript bindings and libraries to our Package Index" -canonical: "/docs/guidelines/publishing-packages" +canonical: "/docs/guides/publishing-packages" +section: "Packages" +order: 1 --- [Back to packages](/packages) diff --git a/apps/docs/public/_redirects b/apps/docs/public/_redirects index e7ca207cc..c82a19b89 100644 --- a/apps/docs/public/_redirects +++ b/apps/docs/public/_redirects @@ -1,5 +1,6 @@ /community /community/overview 308 /bucklescript-rebranding /blog/bucklescript-is-rebranding 308 +/docs/guidelines/publishing-packages /docs/guides/publishing-packages 308 /docs/manual/v12.0.0/* /docs/manual/:splat 308 /docs/manual/next/* /docs/manual/:splat 308 diff --git a/apps/docs/scripts/test-redirects.mjs b/apps/docs/scripts/test-redirects.mjs index f1d907a43..6999dafed 100644 --- a/apps/docs/scripts/test-redirects.mjs +++ b/apps/docs/scripts/test-redirects.mjs @@ -69,6 +69,11 @@ let assertManualLlmsVersionRedirects = (sourceVersion, destinationVersion) => { }; assertRedirect("/llms/manual/llms.txt", "/llms.txt", "307"); +assertRedirect( + "/docs/guidelines/publishing-packages", + "/docs/guides/publishing-packages", + "308", +); const latestAlias = assertRedirect( "/llms/manual/latest/llms.txt", "/llms.txt", diff --git a/packages/shared/src/Path.res b/packages/shared/src/Path.res index 2899299cd..cd26075e6 100644 --- a/packages/shared/src/Path.res +++ b/packages/shared/src/Path.res @@ -311,7 +311,7 @@ type t = [ | #"/docs/manual/array-and-list" | #"/docs/manual/api" | #"/docs/guides/overview" - | #"/docs/guidelines/publishing-packages" + | #"/docs/guides/publishing-packages" | #"/community/translations" | #"/community/roadmap" | #"/community/overview" From 7897cafb598b9b025e87373b46a906db95436a2a Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 9 May 2026 16:29:35 -0400 Subject: [PATCH 5/6] test: stabilize landing visual snapshots Wait for the live Headless UI gallery transition before cloning the landing page section for visual snapshots, avoiding a transient hidden image state in CI. --- apps/docs/__tests__/visual/LandingPage_.test.res | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/docs/__tests__/visual/LandingPage_.test.res b/apps/docs/__tests__/visual/LandingPage_.test.res index 96db60725..8ee26c0bf 100644 --- a/apps/docs/__tests__/visual/LandingPage_.test.res +++ b/apps/docs/__tests__/visual/LandingPage_.test.res @@ -4,6 +4,13 @@ open Vitest @module("vitest") external testWithTimeout: (string, unit => promise, int) => unit = "test" +let sleep = ms => + Promise.make((resolve, _) => { + let _timeoutId = setTimeout(~handler=() => { + resolve() + }, ~timeout=ms) + }) + let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) => { await viewport(width, height) @@ -20,6 +27,15 @@ let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) = | Null => failwith(`expected to find section ${sectionTestId}`) } + if sectionTestId == "landing-other-selling-points" { + let sourceSelector = `[data-testid="${sectionTestId}"]` + await waitForImages(sourceSelector) + // Headless UI's appear transition mutates classes after first render. Since + // these tests snapshot a cloned outerHTML string, wait for the live section + // to settle so the clone does not preserve a transient opacity-0 state. + await sleep(1100) + } + let sandboxTestId = `${sectionTestId}-snapshot` let snapshotHtml = sourceSection.outerHTML await screen->unmount From 0fc04400942ba9f5fe85008a89453063efc5739b Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sun, 10 May 2026 12:27:44 -0400 Subject: [PATCH 6/6] test: move test helpers out of vitest bindings --- .gitignore | 2 ++ .../__tests__/visual/BlogArticle_.test.res | 2 +- .../__tests__/visual/LandingPage_.test.res | 16 +++--------- .../visual/MarkdownComponents_.test.res | 4 +-- apps/docs/rescript.json | 5 ++++ apps/docs/test-utils/TestUtils.res | 26 +++++++++++++++++++ packages/shared/src/Vitest.res | 23 +++------------- 7 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 apps/docs/test-utils/TestUtils.res diff --git a/.gitignore b/.gitignore index 512668a86..0fce7f12c 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,8 @@ apps/docs/functions/**/*.mjs apps/docs/functions/**/*.jsx apps/docs/__tests__/**/*.mjs apps/docs/__tests__/**/*.jsx +apps/docs/test-utils/**/*.mjs +apps/docs/test-utils/**/*.jsx apps/guide/__tests__/**/*.mjs apps/guide/__tests__/**/*.jsx apps/docs/e2e/**/*.mjs diff --git a/apps/docs/__tests__/visual/BlogArticle_.test.res b/apps/docs/__tests__/visual/BlogArticle_.test.res index 59b3cd464..c3f6083a4 100644 --- a/apps/docs/__tests__/visual/BlogArticle_.test.res +++ b/apps/docs/__tests__/visual/BlogArticle_.test.res @@ -166,7 +166,7 @@ test("desktop blog article with article image shows image", async () => { await element(title)->toBeVisible let wrapper = await screen->getByTestId("blog-article-wrapper") - await waitForImages("[data-testid='blog-article-wrapper']") + await TestUtils.waitForImages("[data-testid='blog-article-wrapper']") await element(wrapper)->toMatchScreenshot("desktop-blog-article-with-image") }) diff --git a/apps/docs/__tests__/visual/LandingPage_.test.res b/apps/docs/__tests__/visual/LandingPage_.test.res index 8ee26c0bf..fd8d8aabc 100644 --- a/apps/docs/__tests__/visual/LandingPage_.test.res +++ b/apps/docs/__tests__/visual/LandingPage_.test.res @@ -1,16 +1,6 @@ open ReactRouter open Vitest -@module("vitest") -external testWithTimeout: (string, unit => promise, int) => unit = "test" - -let sleep = ms => - Promise.make((resolve, _) => { - let _timeoutId = setTimeout(~handler=() => { - resolve() - }, ~timeout=ms) - }) - let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) => { await viewport(width, height) @@ -29,11 +19,11 @@ let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) = if sectionTestId == "landing-other-selling-points" { let sourceSelector = `[data-testid="${sectionTestId}"]` - await waitForImages(sourceSelector) + await TestUtils.waitForImages(sourceSelector) // Headless UI's appear transition mutates classes after first render. Since // these tests snapshot a cloned outerHTML string, wait for the live section // to settle so the clone does not preserve a transient opacity-0 state. - await sleep(1100) + await TestUtils.sleep(1100) } let sandboxTestId = `${sectionTestId}-snapshot` @@ -50,7 +40,7 @@ let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) = let snapshotTarget = await snapshotScreen->getByTestId(sandboxTestId) await element(snapshotTarget)->toBeVisible - await waitForImages(`[data-testid="${sandboxTestId}"]`) + await TestUtils.waitForImages(`[data-testid="${sandboxTestId}"]`) await element(snapshotTarget)->toMatchScreenshot(screenshotName) await snapshotScreen->unmount } diff --git a/apps/docs/__tests__/visual/MarkdownComponents_.test.res b/apps/docs/__tests__/visual/MarkdownComponents_.test.res index 9433e8a07..7976d557f 100644 --- a/apps/docs/__tests__/visual/MarkdownComponents_.test.res +++ b/apps/docs/__tests__/visual/MarkdownComponents_.test.res @@ -214,7 +214,7 @@ test("renders Image with caption", async () => { await element(caption)->toBeVisible let wrapper = await screen->getByTestId("image-wrapper") - await waitForImages("[data-testid='image-wrapper']") + await TestUtils.waitForImages("[data-testid='image-wrapper']") await element(wrapper)->toMatchScreenshot("markdown-image") }) @@ -292,7 +292,7 @@ test("renders Image with small size", async () => { await element(caption)->toBeVisible let wrapper = await screen->getByTestId("image-small-wrapper") - await waitForImages("[data-testid='image-small-wrapper']") + await TestUtils.waitForImages("[data-testid='image-small-wrapper']") await element(wrapper)->toMatchScreenshot("markdown-image-small") }) diff --git a/apps/docs/rescript.json b/apps/docs/rescript.json index 927e59552..d77357b84 100644 --- a/apps/docs/rescript.json +++ b/apps/docs/rescript.json @@ -19,6 +19,11 @@ "subdirs": true, "type": "dev" }, + { + "dir": "test-utils", + "subdirs": true, + "type": "dev" + }, { "dir": "app", "subdirs": true diff --git a/apps/docs/test-utils/TestUtils.res b/apps/docs/test-utils/TestUtils.res new file mode 100644 index 000000000..6a5e7780d --- /dev/null +++ b/apps/docs/test-utils/TestUtils.res @@ -0,0 +1,26 @@ +let getByTextExact = (element, text) => Vitest.getByTextWithOptions(element, text, {"exact": true}) + +let sleep = ms => + Promise.make((resolve, _) => { + let _timeoutId = setTimeout(~handler=() => { + resolve() + }, ~timeout=ms) + }) + +external imageFromNode: WebAPI.DOMAPI.node => WebAPI.DOMAPI.htmlImageElement = "%identity" + +let waitForImages = async (selector: string) => { + let root = switch document->WebAPI.Document.querySelector(selector) { + | Value(root) => root + | Null => failwith(`expected to find screenshot target ${selector}`) + } + + let images = root->WebAPI.Element.querySelectorAll("img") + + if images.length > 0 { + for i in 0 to images.length - 1 { + let image = images->WebAPI.NodeList.item(i)->imageFromNode + await image->WebAPI.HTMLImageElement.decode + } + } +} diff --git a/packages/shared/src/Vitest.res b/packages/shared/src/Vitest.res index 18fb832e4..2555d707b 100644 --- a/packages/shared/src/Vitest.res +++ b/packages/shared/src/Vitest.res @@ -9,6 +9,9 @@ type mock @module("vitest") external test: (string, unit => promise) => unit = "test" +@module("vitest") +external testWithTimeout: (string, unit => promise, int) => unit = "test" + @module("vitest") @scope("vi") external fn: unit => 'a => 'b = "fn" @@ -45,8 +48,6 @@ external getByText: (element, string) => promise = "getByText" @send external getByTextWithOptions: (element, string, {"exact": bool}) => promise = "getByText" -let getByTextExact = (element, text) => getByTextWithOptions(element, text, {"exact": true}) - @send external getByLabelText: (element, string) => promise = "getByLabelText" @@ -56,24 +57,6 @@ external getAllByLabelText: (element, string) => promise> = "getA @send external getByRole: (element, [#button]) => promise = "getByRole" -external imageFromNode: WebAPI.DOMAPI.node => WebAPI.DOMAPI.htmlImageElement = "%identity" - -let waitForImages = async (selector: string) => { - let root = switch document->WebAPI.Document.querySelector(selector) { - | Value(root) => root - | Null => failwith(`expected to find screenshot target ${selector}`) - } - - let images = root->WebAPI.Element.querySelectorAll("img") - - if images.length > 0 { - for i in 0 to images.length - 1 { - let image = images->WebAPI.NodeList.item(i)->imageFromNode - await image->WebAPI.HTMLImageElement.decode - } - } -} - /** * Actions */