diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e6a8c4a08..30934211c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -47,3 +47,8 @@ jobs: run: yarn playwright install --with-deps - name: Vitest run: yarn ci:test + - name: Commit and Push changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update vitest screenshots [skip ci]" + file_pattern: "**/__screenshots__/**/*.png" diff --git a/.gitignore b/.gitignore index 59dc8d7ad..bba900005 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,8 @@ __tests__/**/*.jsx _scripts # Local env files -.env.local \ No newline at end of file +.env.local + +# Vitest screenshots +!__tests__/__screenshots__/**/* +.vitest-attachments \ No newline at end of file diff --git a/__tests__/Example.test.res b/__tests__/Example.test.res deleted file mode 100644 index df140a2db..000000000 --- a/__tests__/Example.test.res +++ /dev/null @@ -1,28 +0,0 @@ -open Vitest - -module Example = { - @react.component - let make = (~handleClick) => -
- -
-} - -test("basic assertions", async () => { - expect("foo")->toBe("foo") - - expect(true)->toBe(true) -}) - -test("component rendering", async () => { - let callback = fn() - let screen = await render() - - await element(screen->getByText("testing"))->toBeVisible - - let button = await screen->getByRole(#button) - - await button->click - - expect(callback)->toHaveBeenCalled -}) diff --git a/__tests__/NavbarPrimary_.test.res b/__tests__/NavbarPrimary_.test.res new file mode 100644 index 000000000..02c7f7469 --- /dev/null +++ b/__tests__/NavbarPrimary_.test.res @@ -0,0 +1,100 @@ +open ReactRouter +open Vitest + +test("desktop has everything visible", async () => { + await viewport(1440, 500) + + let screen = await render( + + + , + ) + + let leftContent = await screen->getByTestId("navbar-primary-left-content") + + await element(leftContent->getByText("Docs"))->toBeVisible + await element(leftContent->getByText("Playground"))->toBeVisible + await element(leftContent->getByText("Blog"))->toBeVisible + await element(leftContent->getByText("Community"))->toBeVisible + + let rightContent = await screen->getByTestId("navbar-primary-right-content") + + await element(rightContent->getByLabelText("Github"))->toBeVisible + await element(rightContent->getByLabelText("X (formerly Twitter)"))->toBeVisible + await element(rightContent->getByLabelText("Bluesky"))->toBeVisible + await element(rightContent->getByLabelText("Forum"))->toBeVisible + + let navbar = await screen->getByTestId("navbar-primary") + + await element(navbar)->toMatchScreenshot("desktop-navbar-primary") +}) + +test("tablet has everything visible", async () => { + await viewport(900, 500) + + let screen = await render( + + + , + ) + + let leftContent = await screen->getByTestId("navbar-primary-left-content") + + await element(leftContent->getByText("Docs"))->toBeVisible + await element(leftContent->getByText("Playground"))->toBeVisible + await element(leftContent->getByText("Blog"))->toBeVisible + await element(leftContent->getByText("Community"))->toBeVisible + + let rightContent = await screen->getByTestId("navbar-primary-right-content") + + await element(rightContent->getByLabelText("Github"))->toBeVisible + await element(rightContent->getByLabelText("X (formerly Twitter)"))->toBeVisible + await element(rightContent->getByLabelText("Bluesky"))->toBeVisible + await element(rightContent->getByLabelText("Forum"))->toBeVisible + + let navbar = await screen->getByTestId("navbar-primary") + + await element(navbar)->toMatchScreenshot("tablet-navbar-primary") +}) + +test("phone has some things hidden and a mobile nav that can be toggled", async () => { + await viewport(600, 1200) + + let screen = await render( + + + , + ) + + let leftContent = await screen->getByTestId("navbar-primary-left-content") + + await element(leftContent->getByText("Docs"))->toBeVisible + await element(leftContent->getByText("Playground"))->notToBeVisible + await element(leftContent->getByText("Blog"))->notToBeVisible + await element(leftContent->getByText("Community"))->notToBeVisible + + let rightContent = await screen->getByTestId("navbar-primary-right-content") + + await element(rightContent->getByLabelText("Github"))->notToBeVisible + await element(rightContent->getByLabelText("X (formerly Twitter)"))->notToBeVisible + await element(rightContent->getByLabelText("Bluesky"))->notToBeVisible + await element(rightContent->getByLabelText("Forum"))->notToBeVisible + + await element(screen->getByTestId("mobile-nav"))->notToBeVisible + + let button = await screen->getByTestId("toggle-mobile-overlay") + + await element(button)->toBeVisible + + await button->click + + let mobileNav = await screen->getByTestId("mobile-nav") + + await element(mobileNav)->toBeVisible + + let navbar = await screen->getByTestId("navbar-primary") + + await element(navbar)->toMatchScreenshot("mobile-navbar-primary") + + await element(mobileNav)->toMatchScreenshot("mobile-overlay-navbar-primary") +}) diff --git a/__tests__/NavbarTertiary_.test.res b/__tests__/NavbarTertiary_.test.res new file mode 100644 index 000000000..33f319232 --- /dev/null +++ b/__tests__/NavbarTertiary_.test.res @@ -0,0 +1,93 @@ +open ReactRouter +open Vitest + +let sidebarContent = + + +let breadcrumbs = + {React.string("Docs / Language Manual / Installation")} + +let editLink = {React.string("Edit")} + +test("desktop shows breadcrumbs and edit link", async () => { + await viewport(1440, 500) + + let screen = await render( + + + breadcrumbs + editLink + + , + ) + + let navbar = await screen->getByTestId("navbar-tertiary") + + await element(navbar)->toBeVisible + + let crumbs = await screen->getByTestId("breadcrumbs") + await element(crumbs)->toBeVisible + + let edit = await screen->getByTestId("edit-link") + await element(edit)->toBeVisible + + await element(navbar)->toMatchScreenshot("desktop-navbar-tertiary") +}) + +test("mobile shows breadcrumbs and drawer button", async () => { + await viewport(600, 1200) + + let screen = await render( + + + breadcrumbs + editLink + + , + ) + + let navbar = await screen->getByTestId("navbar-tertiary") + await element(navbar)->toBeVisible + + let crumbs = await screen->getByTestId("breadcrumbs") + await element(crumbs)->toBeVisible + + let edit = await screen->getByTestId("edit-link") + await element(edit)->toBeVisible + + await element(navbar)->toMatchScreenshot("mobile-navbar-tertiary") +}) + +test("mobile drawer can be toggled open", async () => { + await viewport(600, 1200) + + let screen = await render( + + + breadcrumbs + editLink + + , + ) + + // Sidebar dialog should not be visible initially + let sidebar = await screen->getByTestId("sidebar-categories") + await element(sidebar)->notToBeVisible + + // Click the drawer toggle button + let drawerButton = await screen->getByRole(#button) + await drawerButton->click + + // Sidebar content should now be visible + let sidebarAfter = await screen->getByTestId("sidebar-categories") + await element(sidebarAfter)->toBeVisible + + let versionSelect = await screen->getByTestId("sidebar-version-select") + await element(versionSelect)->toBeVisible +}) diff --git a/__tests__/VersionSelect_.test.res b/__tests__/VersionSelect_.test.res new file mode 100644 index 000000000..54943bdae --- /dev/null +++ b/__tests__/VersionSelect_.test.res @@ -0,0 +1,80 @@ +open Vitest + +test("renders current version label", async () => { + let screen = await render() + + let el = await screen->getByTestId("version-select") + await element(el)->toBeVisible + + let label = await screen->getByText("v12 (latest)") + await element(label)->toBeVisible +}) + +test("clicking button shows older versions", async () => { + let screen = await render() + + // Menu should be hidden initially + let v11 = await screen->getByText("v11") + await element(v11)->notToBeVisible + + // Click the trigger button + let button = await screen->getByRole(#button) + await button->click + + // Older versions should now be visible + let v11After = await screen->getByText("v11") + await element(v11After)->toBeVisible + + let v9 = await screen->getByText("v9.1 - v10.1") + await element(v9)->toBeVisible + + let v8 = await screen->getByText("v8.2 - v9.0") + await element(v8)->toBeVisible + + let v6 = await screen->getByText("v6.0 - v8.1") + await element(v6)->toBeVisible +}) + +test("clicking button again closes older versions", async () => { + let screen = await render() + + let button = await screen->getByRole(#button) + + // Open + await button->click + let v11 = await screen->getByText("v11") + await element(v11)->toBeVisible + + // Close + await button->click + let v11After = await screen->getByText("v11") + await element(v11After)->notToBeVisible +}) + +test("multiple instances have unique popover IDs", async () => { + let screen = await render( +
+
+ +
+
+ +
+
, + ) + + let first = await screen->getByTestId("first") + let second = await screen->getByTestId("second") + + // Click the button in the first instance + let firstButton = await first->getByRole(#button) + await firstButton->click + + // First instance menu should be visible + let firstV11 = await first->getByText("v11") + await element(firstV11)->toBeVisible + + // Second instance menu should remain hidden + let secondV11 = await second->getByText("v11") + await element(secondV11)->notToBeVisible +}) diff --git a/__tests__/__screenshots__/NavbarPrimary_.test.jsx/desktop-navbar-primary-chromium-linux.png b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/desktop-navbar-primary-chromium-linux.png new file mode 100644 index 000000000..9f2555de1 Binary files /dev/null and b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/desktop-navbar-primary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarPrimary_.test.jsx/mobile-navbar-primary-chromium-linux.png b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/mobile-navbar-primary-chromium-linux.png new file mode 100644 index 000000000..e6b54e021 Binary files /dev/null and b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/mobile-navbar-primary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarPrimary_.test.jsx/mobile-overlay-navbar-primary-chromium-linux.png b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/mobile-overlay-navbar-primary-chromium-linux.png new file mode 100644 index 000000000..37f28fd3d Binary files /dev/null and b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/mobile-overlay-navbar-primary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarPrimary_.test.jsx/tablet-navbar-primary-chromium-linux.png b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/tablet-navbar-primary-chromium-linux.png new file mode 100644 index 000000000..16c5d079f Binary files /dev/null and b/__tests__/__screenshots__/NavbarPrimary_.test.jsx/tablet-navbar-primary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarTertiary_.test.jsx/desktop-navbar-tertiary-chromium-linux.png b/__tests__/__screenshots__/NavbarTertiary_.test.jsx/desktop-navbar-tertiary-chromium-linux.png new file mode 100644 index 000000000..a6be3b920 Binary files /dev/null and b/__tests__/__screenshots__/NavbarTertiary_.test.jsx/desktop-navbar-tertiary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarTertiary_.test.jsx/mobile-navbar-tertiary-chromium-linux.png b/__tests__/__screenshots__/NavbarTertiary_.test.jsx/mobile-navbar-tertiary-chromium-linux.png new file mode 100644 index 000000000..249d18863 Binary files /dev/null and b/__tests__/__screenshots__/NavbarTertiary_.test.jsx/mobile-navbar-tertiary-chromium-linux.png differ diff --git a/app/root.res b/app/root.res index 255797d01..cbddcf72d 100644 --- a/app/root.res +++ b/app/root.res @@ -38,17 +38,6 @@ open ReactRouter @react.component let default = () => { - let {pathname} = ReactRouter.useLocation() - let (isOverlayOpen, setOverlayOpen) = React.useState(_ => false) - let (isScrollLockEnabled, setIsScrollLockEnabled) = React.useState(_ => false) - - React.useEffect(() => { - // When the path changes close the sidebar and disable scroll lock - setOverlayOpen(_ => false) - setIsScrollLockEnabled(_ => false) - None - }, [pathname]) - @@ -65,15 +54,11 @@ let default = () => { /> - - - - - - - - - + + + + + } diff --git a/app/routes.res b/app/routes.res index fd039b386..656db708c 100644 --- a/app/routes.res +++ b/app/routes.res @@ -28,6 +28,8 @@ let stdlibRoutes = let beltRoutes = beltPaths->Array.map(path => route(path, "./routes/ApiRoute.jsx", ~options={id: path})) +let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx") + let default = [ index("./routes/LandingPageRoute.jsx"), route("packages", "./routes/PackagesRoute.jsx"), @@ -42,6 +44,6 @@ let default = [ route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}), ...stdlibRoutes, ...beltRoutes, - ...mdxRoutes("./routes/MdxRoute.jsx"), + ...mdxRoutes, route("*", "./routes/NotFoundRoute.jsx"), ] diff --git a/app/routes/ApiRoute.res b/app/routes/ApiRoute.res index 8a6dc90cf..6ee7c0345 100644 --- a/app/routes/ApiRoute.res +++ b/app/routes/ApiRoute.res @@ -160,8 +160,36 @@ let default = () => { ->Array.at(0) ->Option.flatMap(str => String.split(str, ".")[0]) + let breadcrumbs = { + let prefix = {Url.name: "API", href: "/docs/manual/api"} + let crumbs = ApiLayout.makeBreadcrumbs(~prefix, pathname) + list{{Url.name: "Docs", href: "/docs/manual/api"}, ...crumbs} + } + + let sidebarContent = switch loaderData { + | Ok({toctree, module_: {items}}) => +
+
+
+ +
+ +
+ +
+ | Error(_) => React.null + } + <> + + + + } diff --git a/app/routes/DocsOverview.res b/app/routes/DocsOverview.res index 946326571..e5df82e57 100644 --- a/app/routes/DocsOverview.res +++ b/app/routes/DocsOverview.res @@ -31,7 +31,7 @@ let default = (~showVersionSelect=true) => { ] -
+
//
{React.string("Docs")} diff --git a/app/routes/MdxRoute.res b/app/routes/MdxRoute.res index c3b495b7c..55f1bf4fb 100644 --- a/app/routes/MdxRoute.res +++ b/app/routes/MdxRoute.res @@ -291,8 +291,43 @@ let default = () => { <> {if (pathname :> string) == "/docs/manual/api" { + let breadcrumbs = list{ + {Url.name: "Docs", href: `/docs/manual/api`}, + {name: "API", href: `/docs/manual/api`}, + } + let sidebarContent = + + <> Nullable.getOr("ReScript API")} /> + + + +
{component()}
@@ -304,10 +339,9 @@ let default = () => { ) { <> Nullable.getOr("")} /> - Option.map(crumbs => + + { + let breadcrumbs = loaderData.breadcrumbs->Option.map(crumbs => List.mapWithIndex(crumbs, (item, index) => { if index === 0 { if (pathname :> string)->String.includes("docs/manual") { @@ -321,11 +355,64 @@ let default = () => { item } }) - )} - editHref={`https://github.com/rescript-lang/rescript-lang.org/blob/master${loaderData.filePath->Option.getOrThrow}`} - > -
{component()}
-
+ ) + let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master${loaderData.filePath->Option.getOrThrow}` + + let sidebarContent = + + + <> + + {breadcrumbs->Option.mapOr(React.null, crumbs => + + )} + + {React.string("Edit")} + + + +
{component()}
+
+ + } } else if (pathname :> string)->String.includes("community") { @@ -347,7 +434,7 @@ let default = () => { ->Option.getOr("Syntax Lookup | ReScript API")} description={attributes.description->Nullable.getOr("")} /> - + {component()} diff --git a/app/routes/SyntaxLookupRoute.res b/app/routes/SyntaxLookupRoute.res index ff71bc73a..b2a0e8e25 100644 --- a/app/routes/SyntaxLookupRoute.res +++ b/app/routes/SyntaxLookupRoute.res @@ -34,6 +34,7 @@ let default = () => { let {mdxSources} = useLoaderData() <> {React.string("Syntax Lookup | ReScript API")} + } diff --git a/functions/ogimage/[[path]]/index.png.res b/functions/ogimage/[[path]]/index.png.res index bdc524e7b..0b215994f 100644 --- a/functions/ogimage/[[path]]/index.png.res +++ b/functions/ogimage/[[path]]/index.png.res @@ -16,8 +16,6 @@ let loadGoogleFont = async (family: string) => { type context = {request: FetchAPI.request, params: {path: array}} let onRequest = async ({params}: context) => { - Console.log(params.path) - let title = params.path[0]->Option.getOr("ReScript")->decodeURIComponent // let url = WebAPI.URL.make(~url=request.url) // let title = url.searchParams->URLSearchParams.get("title") diff --git a/package.json b/package.json index 39407f269..34f375558 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build:vite": "react-router build", "build": "yarn build:res && yarn build:scripts && yarn build:update-index && yarn build:vite", "ci:format": "prettier . --check --experimental-cli", - "ci:test": "yarn vitest --run --browser.headless", + "ci:test": "yarn vitest --run --browser.headless --update", "clean:res": "rescript clean", "convert-images": "auto-convert-images", "dev:res": "rescript watch", diff --git a/src/ApiDocs.res b/src/ApiDocs.res index f718abca3..20817bd4f 100644 --- a/src/ApiDocs.res +++ b/src/ApiDocs.res @@ -95,7 +95,7 @@ module RightSidebar = { module SidebarTree = { @react.component - let make = (~isOpen: bool, ~toggle: unit => unit, ~node: node, ~items: array) => { + let make = (~node: node, ~items: array) => { open ReactRouter let location = useLocation() @@ -115,7 +115,7 @@ module SidebarTree = { | true =>
    - +
| false => React.null @@ -176,54 +176,26 @@ module SidebarTree = { } } - let preludeSection = -
- -
- - +
{"submodules"->React.string}
+ {node.children + ->Array.toSorted((v1, v2) => String.compare(v1.name, v2.name)) + ->Array.filter(child => child.name !== node.name) + ->Array.map(renderNode) + ->React.array} + } } @@ -285,48 +257,8 @@ module DocstringsStylize = { } } -let useIsMobile = () => { - let query = switch Type.Classify.classify(globalThis["window"]) { - | Undefined => "(max-width: 768px)" // Fallback value for SSR - | _ => { - let documentElt = WebAPI.HTMLElement.asElement(document.documentElement) - let computedStyle = window->WebAPI.Window.getComputedStyle(~elt=documentElt) - let mdBreakpoint = - computedStyle->WebAPI.CSSStyleDeclaration.getPropertyValue("--breakpoint-md")->String.trim - `(max-width: ${mdBreakpoint})` - } - } - Hooks.useMediaQuery(query) -} - @react.componentWithProps let make = (props: props) => { - let (_, setScrollLock) = ScrollLockContext.useScrollLock() - let (isSidebarOpen, setSidebarOpen) = React.useState(_ => true) - let isMobile = useIsMobile() - - // Close sidebar and remove scroll lock when not on mobile - React.useEffect(() => { - if !isMobile { - setSidebarOpen(_ => true) - setScrollLock(_ => false) - } - - None - }, [isMobile]) - - let toggleSidebar = () => - setSidebarOpen(prev => { - // Only toggle on mobile devices, - // on desktop the sidebar is always open and shouldn't affect scroll lock - if isMobile && prev { - setScrollLock(_ => !prev) - !prev - } else { - prev - } - }) - let children = { open Markdown switch props { @@ -370,7 +302,7 @@ let make = (props: props) => {