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
81 changes: 81 additions & 0 deletions apps/docs/__tests__/ImageGallery_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
open Vitest

let images = ["/lp/community-3.avif", "/lp/community-2.avif", "/lp/community-1.avif"]

test(
"gallery selectors show their photo and the next control wraps after the last photo",
async () => {
let screen = await render(<ImageGallery imgSrcs=images />)
let first = await screen->getByLabelText("Show community photo 1")
let third = await screen->getByLabelText("Show community photo 3")
let next = await screen->getByLabelText("Next community photo")

await element(first)->toHaveAttribute("aria-pressed", "true")
await third->click
await element(third)->toHaveAttribute("aria-pressed", "true")
let lastImage = await screen->getByAltText("ReScript community photo 3")
await element(lastImage)->toHaveAttribute("src", "/lp/community-1.avif")
await next->click

await element(first)->toHaveAttribute("aria-pressed", "true")
await element(third)->toHaveAttribute("aria-pressed", "false")
let firstImage = await screen->getByAltText("ReScript community photo 1")
await element(firstImage)->toHaveAttribute("src", "/lp/community-3.avif")
},
)

test("gallery renders no controls for an empty image list", async () => {
let screen = await render(<ImageGallery imgSrcs=[] />)
expect(screen->container->textContent->Nullable.toOption)->toEqual(Some(""))
let next = await screen->getByLabelText("Next community photo")
await element(next)->notToBeInTheDocument
})

test("gallery returns to the first image when the selected image is removed", async () => {
let screen = await render(<ImageGallery imgSrcs=images />)
let third = await screen->getByLabelText("Show community photo 3")
await third->click

await screen->rerender(<ImageGallery imgSrcs=["/lp/community-3.avif"] />)

let firstImage = await screen->getByAltText("ReScript community photo 1")
await element(firstImage)->toHaveAttribute("src", "/lp/community-3.avif")
let next = await screen->getByLabelText("Next community photo")
await next->click
await element(firstImage)->toHaveAttribute("src", "/lp/community-3.avif")
})

test("gallery keeps the first image selected when a shortened list grows again", async () => {
let screen = await render(<ImageGallery imgSrcs=images />)
let third = await screen->getByLabelText("Show community photo 3")
await third->click

await screen->rerender(<ImageGallery imgSrcs=["/lp/community-3.avif"] />)
let first = await screen->getByLabelText("Show community photo 1")
await element(first)->toHaveAttribute("aria-pressed", "true")

await screen->rerender(<ImageGallery imgSrcs=images />)

await element(first)->toHaveAttribute("aria-pressed", "true")
await element(third)->toHaveAttribute("aria-pressed", "false")
let firstImage = await screen->getByAltText("ReScript community photo 1")
await element(firstImage)->toHaveAttribute("src", "/lp/community-3.avif")
})

test("gallery keeps the first image selected after an empty list is restored", async () => {
let screen = await render(<ImageGallery imgSrcs=images />)
let third = await screen->getByLabelText("Show community photo 3")
await third->click

await screen->rerender(<ImageGallery imgSrcs=[] />)
let next = await screen->getByLabelText("Next community photo")
await element(next)->notToBeInTheDocument

await screen->rerender(<ImageGallery imgSrcs=images />)

let first = await screen->getByLabelText("Show community photo 1")
await element(first)->toHaveAttribute("aria-pressed", "true")
await element(third)->toHaveAttribute("aria-pressed", "false")
let firstImage = await screen->getByAltText("ReScript community photo 1")
await element(firstImage)->toHaveAttribute("src", "/lp/community-3.avif")
})
64 changes: 64 additions & 0 deletions apps/docs/__tests__/LandingPageCopyButton_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
open Vitest

test(
"copy button writes its command and clears success feedback before copying again",
async () => {
let copiedCommand = ref(None)
let writeClipboard = async code => {
copiedCommand := Some(code)
Ok()
}
let screen = await render(<LandingPageCopyButton code="npm install rescript" writeClipboard />)
let button = await screen->getByLabelText("Copy npm install rescript command")

await button->click

let feedback = await screen->getByText("Copied!")
await element(feedback)->toBeVisible
expect(copiedCommand.contents)->toEqual(Some("npm install rescript"))
await element(button)->toBeDisabled
await element(button)->notToBeDisabled
await element(feedback)->notToBeInTheDocument
await button->click
await element(feedback)->toBeVisible
},
)

test("copy button exposes clipboard failure and allows retry", async () => {
let shouldFail = ref(true)
let writeClipboard = async _ => {
if shouldFail.contents {
shouldFail := false
Error(Clipboard.WriteFailed)
} else {
Ok()
}
}
let screen = await render(<LandingPageCopyButton code="npm install rescript" writeClipboard />)
let button = await screen->getByLabelText("Copy npm install rescript command")

await button->click

let feedback = await screen->getByText("Could not copy. Try again.")
await element(feedback)->toBeVisible
await element(button)->notToBeDisabled
await button->click

let copied = await screen->getByText("Copied!")
await element(copied)->toBeVisible
await element(feedback)->notToBeInTheDocument
})

test("copy button disables duplicate writes until the clipboard operation settles", async () => {
let complete = ref(_ => ())
let writeClipboard = _ => Promise.make((resolve, _) => complete := resolve)
let screen = await render(<LandingPageCopyButton code="npm install rescript" writeClipboard />)
let button = await screen->getByLabelText("Copy npm install rescript command")

await button->click
await element(button)->toBeDisabled
complete.contents(Ok())

let feedback = await screen->getByText("Copied!")
await element(feedback)->toBeVisible
})
4 changes: 0 additions & 4 deletions apps/docs/__tests__/visual/LandingPage_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ let snapshotSection = async (~width, ~height, ~sectionTestId, ~screenshotName) =
if sectionTestId == "landing-other-selling-points" {
let sourceSelector = `[data-testid="${sectionTestId}"]`
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 TestUtils.sleep(1100)
}

let sandboxTestId = `${sectionTestId}-snapshot`
Expand Down
7 changes: 4 additions & 3 deletions apps/docs/e2e/bindings/Cypress.res
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ type rec window = {document: Dom.document, console: console, navigator: {clipboa
and clipboard
type response = {status: int, body: string}
type automation = {command: string, params?: {permissions: array<string>, origin: string}}
type clickOptions = {scrollBehavior: string}
type request = {url: string, resourceType: string}
type routeMatcher = {resourceType?: string, pathname?: string}
type url
Expand Down Expand Up @@ -60,6 +59,7 @@ external shouldCss: (chain<elements>, @as("have.css") _, string, string) => chai
"should"
@send
external shouldCssProperty: (chain<elements>, @as("have.css") _, string) => chain<string> = "should"
@send external shouldInt: (chain<'a>, string, int) => chain<'a> = "should"
@send external shouldEqual: (chain<'a>, @as("equal") _, 'a) => chain<'a> = "should"
@send external shouldDeepEqual: (chain<'a>, @as("deep.equal") _, 'a) => chain<'a> = "should"
@send external shouldMatch: (chain<'a>, @as("match") _, RegExp.t) => chain<'a> = "should"
Expand All @@ -72,8 +72,9 @@ external shouldAttribute: (chain<elements>, @as("have.attr") _, string, string)
@send external propertyInt: (chain<'a>, string) => chain<int> = "its"
@send external shouldSatisfy: (chain<'a>, 'a => unit) => chain<'a> = "should"
@send external click: chain<elements> => chain<elements> = "click"
@send
external realClick: (chain<elements>, clickOptions) => chain<elements> = "realClick"
@send external focusElement: chain<elements> => chain<elements> = "focus"
@send external realClick: chain<elements> => chain<elements> = "realClick"
@send external realPress: (chain<elements>, string) => chain<elements> = "realPress"
@send external scrollIntoView: chain<elements> => chain<elements> = "scrollIntoView"
@send external each: (chain<elements>, elements => unit) => chain<elements> = "each"
@send external as_: (chain<'a>, string) => chain<'a> = "as"
Expand Down
8 changes: 1 addition & 7 deletions apps/docs/e2e/homepage/Homepage.cy.res
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
open Cypress
open HomepageHelpers

it("homepage hydrates with working links and copy feedback", () => {
grantClipboardPermissions()
it("homepage hydrates with working links and images", () => {
visit("/")
containsIn("h1", headline)->should("be.visible")->ignore
containsIn("a", "Get started")->shouldAttribute("href", "/docs/manual/installation")->ignore
containsIn("a", "Edit this example in Playground")
->attribute("href")
->shouldMatch(/\/try\?code=.+/)
->ignore
get(`button[aria-label="Copy npm install rescript command"]`)
->realClick({scrollBehavior: "center"})
->ignore
contains("Copied!")->should("be.visible")->ignore
readClipboard()->shouldEqual("npm install rescript")->ignore
get("img")
->each(image => {
wrap(image)
Expand Down
9 changes: 9 additions & 0 deletions apps/docs/e2e/homepage/HomepageHelpers.res
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ let grantClipboardPermissions = () => {
)->ignore
}

let denyClipboardPermissions = () => {
run(() =>
automate({
command: "Browser.grantPermissions",
params: {permissions: [], origin: baseUrl()},
})
)->ignore
}

let readClipboard = () => cyWindow()->thenPromise(window => window.navigator.clipboard->readText)

let homepageDocument = callback => {
Expand Down
44 changes: 44 additions & 0 deletions apps/docs/e2e/homepage/HomepageInteractions.cy.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
open Cypress
open HomepageHelpers

it("community gallery supports keyboard selection and wraps to the first photo", () => {
visit("/")
containsIn("h1", headline)->realClick->ignore
let first = `button[aria-label="Show community photo 1"]`
let third = `button[aria-label="Show community photo 3"]`
let next = `button[aria-label="Next community photo"]`

get(first)->shouldAttribute("aria-pressed", "true")->ignore
get(third)->scrollIntoView->focusElement->realPress("Enter")->ignore
get(third)->shouldAttribute("aria-pressed", "true")->should("be.focused")->ignore
get(`img[alt="ReScript community photo 3"]`)->should("be.visible")->ignore
get(next)->focusElement->realPress("Space")->ignore
get(first)->shouldAttribute("aria-pressed", "true")->ignore
get(next)->should("be.focused")->ignore
get(`img[alt="ReScript community photo 1"]`)->should("be.visible")->ignore
})

it("clipboard denial can recover and both install commands can be copied repeatedly", () => {
denyClipboardPermissions()
visit("/")
let first = `button[aria-label="Copy npm install rescript command"]`
get(`[role="status"]`)->shouldInt("have.length", 2)->ignore
get(`button [role="status"]`)->should("not.exist")->ignore
get(first)->realClick->ignore
containsIn(`[role="status"]`, "Could not copy. Try again.")->should("be.visible")->ignore
get(first)->should("be.enabled")->ignore
grantClipboardPermissions()

["npm install rescript", "npx create-rescript-app"]->Array.forEach(command => {
let button = `button[aria-label="Copy ${command} command"]`
get(button)->realClick->ignore
containsIn(`[role="status"]`, "Copied!")->should("be.visible")->ignore
get(button)->should("be.disabled")->ignore
readClipboard()->shouldEqual(command)->ignore
get(button)->should("be.enabled")->ignore
contains("Copied!")->should("not.exist")->ignore
get(button)->realClick->ignore
contains("Copied!")->should("be.visible")->ignore
get(button)->should("be.enabled")->ignore
})
})
10 changes: 10 additions & 0 deletions apps/docs/src/common/Clipboard.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type error = WriteFailed

let writeText = async text => {
try {
await navigator.clipboard->WebAPI.Clipboard.writeText(text)
Ok()
} catch {
| _ => Error(WriteFailed)
}
}
Loading
Loading