Skip to content

Commit

Permalink
Merge pull request #579 from nfdi4plants/react_tests
Browse files Browse the repository at this point in the history
init react component tests 🎉
  • Loading branch information
Freymaurer authored Nov 25, 2024
2 parents 48aa7b3 + c3819e5 commit 2f38e39
Show file tree
Hide file tree
Showing 26 changed files with 2,997 additions and 1,116 deletions.
6 changes: 1 addition & 5 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ insert_final_newline = false
fsharp_multiline_bracket_style = stroustrup
fsharp_newline_before_multiline_computation_expression = false

[*.fsproj]
[*.{fsproj,yml,json,ts,tsx}]
indent_style = space
indent_size = 2

[*.yml]
indent_style = space
indent_size = 2
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ release.sh
/.db/neo4j
/src/Client/output
/tests/Client/output
/tests/Components/output
/.paket

1 change: 1 addition & 0 deletions build/Build.fs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ module Tests =
"server", dotnet [ "run" ] serverTestsPath
"client", dotnet [ "fable"; "-o"; "output"; "-s"; "--run"; "npx"; "mocha"; $"{clientTestsPath}/output/Client.Tests.js" ] clientTestsPath
]|> runParallel
run npm [ "run"; "test:run" ] "."

Target.create "Format" (fun _ ->
run dotnet [ "fantomas"; "."; "-r" ] "src"
Expand Down
3,772 changes: 2,687 additions & 1,085 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@
"node": "~18 || ~20",
"npm": "~9 || ~10"
},
"scripts": {},
"scripts": {
"test": "dotnet fable watch ./src/Components/Components.fsproj --lang ts -e tsx -o ./tests/Components/output --run vitest -r ./tests/Components",
"pretest:run": "dotnet fable ./src/Components/Components.fsproj --lang ts -e tsx -o ./tests/Components/output --noCache",
"test:run": "vitest run -r ./tests/Components"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/node": "^20.10.3",
"@types/react": "^18.3.12",
"@types/testing-library__react": "^10.0.1",
"@vitejs/plugin-basic-ssl": "^1.0.2",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"daisyui": "^4.12.14",
"jsdom": "^25.0.1",
"mocha": "^10.8.2",
"office-addin-mock": "^2.4.6",
"postcss": "^8.4.39",
Expand All @@ -20,7 +29,8 @@
"tailwindcss": "^3.4.4",
"typescript": "^5.6.3",
"vite": "^5.0.5",
"vite-plugin-node-polyfills": "^0.22.0"
"vite-plugin-node-polyfills": "^0.22.0",
"vitest": "^2.1.5"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
Expand Down
16 changes: 8 additions & 8 deletions src/Client/MainComponents/Navbar.fs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let private QuickAccessButtonListStart (state: LocalHistory.Model) dispatch =
Spreadsheet.UpdateHistoryPosition newPosition |> Msg.SpreadsheetMsg |> dispatch
),
isDisabled = (state.NextPositionIsValid(state.HistoryCurrentPosition + 1) |> not)
)
) |> toReact
QuickAccessButton.QuickAccessButton(
"Forward",
React.fragment [
Expand All @@ -61,7 +61,7 @@ let private QuickAccessButtonListStart (state: LocalHistory.Model) dispatch =
Spreadsheet.UpdateHistoryPosition newPosition |> Msg.SpreadsheetMsg |> dispatch
),
isDisabled = (state.NextPositionIsValid(state.HistoryCurrentPosition - 1) |> not)
)
) |> toReact
]
]

Expand All @@ -83,7 +83,7 @@ let private QuickAccessButtonListEnd (model: Model) dispatch =
Spreadsheet.ExportXlsx model.SpreadsheetModel.ArcFile.Value |> SpreadsheetMsg |> dispatch
| _ -> ()
)
)
) |> toReact
match model.PersistentStorageState.Host with
| Some Swatehost.Browser ->
QuickAccessButton.QuickAccessButton(
Expand All @@ -93,7 +93,7 @@ let private QuickAccessButtonListEnd (model: Model) dispatch =
],
(fun _ -> Modals.Controller.renderModal("ResetTableWarning", Modals.ResetTable.Main dispatch)),
classes = "hover:!text-error"
)
) |> toReact
NavbarBurger.Main(model, dispatch)
| _ ->
Html.none
Expand All @@ -111,7 +111,7 @@ let private WidgetNavbarList (model, dispatch, addWidget: Widget -> unit) =
]
],
(fun _ -> addWidget Widget._BuildingBlock)
)
) |> toReact
let addTemplate =
QuickAccessButton.QuickAccessButton(
"Add Template",
Expand All @@ -120,23 +120,23 @@ let private WidgetNavbarList (model, dispatch, addWidget: Widget -> unit) =
Html.i [prop.className "fa-solid fa-table" ]
],
(fun _ -> addWidget Widget._Template)
)
) |> toReact
let filePicker =
QuickAccessButton.QuickAccessButton(
"File Picker",
React.fragment [
Html.i [prop.className "fa-solid fa-file-signature" ]
],
(fun _ -> addWidget Widget._FilePicker)
)
) |> toReact
let dataAnnotator =
QuickAccessButton.QuickAccessButton(
"Data Annotator",
React.fragment [
Html.i [prop.className "fa-solid fa-object-group" ]
],
(fun _ -> addWidget Widget._DataAnnotator)
)
) |> toReact
Html.div [
prop.className "flex flex-row"
prop.children [
Expand Down
2 changes: 1 addition & 1 deletion src/Client/SharedComponents/NavbarBurger.fs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ type NavbarBurger =
"More",
NavbarBurger.Dropdown(isOpen, setIsOpen, model, dispatch),
(fun _ -> setIsOpen (not isOpen))
)
) |> toReact
12 changes: 6 additions & 6 deletions src/Client/SidebarComponents/Navbar.fs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ let private QuickAccessList toggleMetdadataModal model (dispatch: Messages.Msg -
Html.i [prop.className "fa-solid fa-info"]
],
toggleMetdadataModal
)
) |> toReact

QuickAccessButton.QuickAccessButton(
"Create Annotation Table",
Expand All @@ -227,7 +227,7 @@ let private QuickAccessList toggleMetdadataModal model (dispatch: Messages.Msg -
let ctrl = e.metaKey || e.ctrlKey
SpreadsheetInterface.CreateAnnotationTable ctrl |> InterfaceMsg |> dispatch
)
)
) |> toReact
match model.PersistentStorageState.Host with
| Some Swatehost.Excel ->
QuickAccessButton.QuickAccessButton(
Expand All @@ -241,7 +241,7 @@ let private QuickAccessList toggleMetdadataModal model (dispatch: Messages.Msg -
let ctrl = not (e.metaKey || e.ctrlKey)
OfficeInterop.AutoFitTable ctrl |> OfficeInteropMsg |> dispatch
)
)
) |> toReact
| _ ->
()
QuickAccessButton.QuickAccessButton(
Expand All @@ -254,15 +254,15 @@ let private QuickAccessList toggleMetdadataModal model (dispatch: Messages.Msg -
(fun _ ->
SpreadsheetInterface.RectifyTermColumns |> InterfaceMsg |> dispatch
)
)
) |> toReact
QuickAccessButton.QuickAccessButton(
"Remove Building Block",
React.fragment [
Html.i [prop.className "fa-solid fa-minus pr-1"]
Html.i [prop.className "fa-solid fa-table-columns"]
],
(fun _ -> SpreadsheetInterface.RemoveBuildingBlock |> InterfaceMsg |> dispatch)
)
) |> toReact
QuickAccessButton.QuickAccessButton(
"Get Building Block Information",
React.fragment [
Expand All @@ -271,7 +271,7 @@ let private QuickAccessList toggleMetdadataModal model (dispatch: Messages.Msg -
Html.i [prop.className "fa-solid fa-table-columns"]
],
(fun _ -> SpreadsheetInterface.GetBuildingBlockDetails |> InterfaceMsg |> dispatch)
)
) |> toReact
]
|> React.fragment

Expand Down
2 changes: 2 additions & 0 deletions src/Components/Components.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
</ItemGroup>

<ItemGroup>
<Compile Include="Util.fs" />
<Compile Include="Example.fs" />
<Compile Include="QuickAccessButton.fs" />
</ItemGroup>

Expand Down
33 changes: 33 additions & 0 deletions src/Components/Example.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Components

open Fable.Core
open Feliz


/// These 2 Attributes will remove the type from the generated JS code
/// ⚠️ Due to [<Mangle(false)>] you can never use overloads in the same file!
[<Erase; Mangle(false)>]
type TestInput =

/// [<ExportDefault>] can only be used on a single member in a module
/// [<NamedParams>] is used for correct type hinting in typescript
/// with: export function TestInput({ children, number }: {children?: ReactElement, number?: int32 }): any {
/// without: export function TestInput(children?: ReactElement, number?: int32): any {
/// ⚠️ ... fails because react requires object as input
[<ExportDefault; NamedParams>]
static member TestInput(?children: ReactElement, ?number: int) : JSX.Element =
let state, useState = React.useState (number |> Option.defaultValue 0)
Html.div [
prop.children [
if children.IsSome then children.Value
Html.div [
prop.textf "Number: %d" state
]
Html.input [
prop.type'.number
prop.onChange(fun (number:int) -> useState number)
]
]
]
// 👀 This is used to remove type hint to: ReactElement in typescript. Which would trigger warning when using in .tsx file.
|> unbox<JSX.Element>
14 changes: 8 additions & 6 deletions src/Components/QuickAccessButton.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ open Browser.Types
[<Erase; Mangle(false)>]
type QuickAccessButton =

[<NamedParams; ReactComponent(true)>]
static member QuickAccessButton(desc:string, children: ReactElement, onclick: Event -> unit, ?isDisabled, ?props, ?classes: string) =
[<ExportDefault; NamedParams>]
static member QuickAccessButton(
desc:string, children: ReactElement, onclick: Event -> unit,
?isDisabled: bool, ?props: IReactProperty seq, ?classes: string
) =
let isDisabled = defaultArg isDisabled false
Html.button [
prop.className [
Expand All @@ -20,7 +23,6 @@ type QuickAccessButton =
prop.disabled isDisabled
prop.onClick onclick
if props.IsSome then yield! props.Value
prop.children [
children
]
]
prop.children children
]
|> unbox<JSX.Element>
22 changes: 22 additions & 0 deletions src/Components/Util.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Components

open Fable.Core
open Feliz

/// https://fable.io/blog/2022/2022-10-12-react-jsx.html
[<AutoOpen>]
module Util =

let inline toJsx (el: ReactElement) : JSX.Element = unbox el
let inline toReact (el: JSX.Element) : ReactElement = unbox el

/// Enables use of Feliz styles within a JSX hole
let inline toStyle (styles: IStyleAttribute list) : obj = JsInterop.createObj (unbox styles)
let toClass (classes: (string * bool) list) : string =
classes
|> List.choose (fun (c, b) ->
match c.Trim(), b with
| "", _
| _, false -> None
| c, true -> Some c)
|> String.concat " "
15 changes: 15 additions & 0 deletions tests/Components/Base.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, test, expect } from 'vitest';

describe('Native DOM', () => {
test('creates an element in the DOM', () => {
const div = document.createElement('div');
expect(div).toBeDefined();
});


test('environment should be jsdom', () => {
expect(globalThis.navigator).toBeDefined();
expect(globalThis.navigator.userAgent).toContain('jsdom');
});

});
58 changes: 58 additions & 0 deletions tests/Components/Example.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';

// Assuming TestInput is the transpiled component from Fable
import Example from './output/Example'; // Update the import path as needed

describe('TestInput Component', () => {
it('renders with initial number and children', () => {
render(
<Example number={42}>
<span>Child Element</span>
</Example>
);

// Check for child element
expect(screen.getByText('Child Element')).toBeInTheDocument();

// Check for initial number
expect(screen.getByText('Number: 42')).toBeInTheDocument();
});

it('renders with default number (0) when no number prop is provided', () => {
render(
<Example>
<span>Default Number</span>
</Example>
);

// Check for default number
expect(screen.getByText('Number: 0')).toBeInTheDocument();
});

it('updates number when input value changes', async () => {
render(<Example number={10} />);

// Get the number input
const input = screen.getByRole('spinbutton'); // Default ARIA role for number inputs

// Simulate changing the input value
fireEvent.change(input, { target: { value: '25' } });

// Check if the number is updated
expect(screen.getByText('Number: 25')).toBeInTheDocument();
});

it('handles non-numeric input gracefully', async () => {
render(<Example />);

// Get the number input
const input = screen.getByRole('spinbutton');

// Simulate entering non-numeric value
fireEvent.change(input, { target: { value: 'not-a-number' } });

// Check if the number remains 0 (default state)
expect(screen.getByText('Number: 0')).toBeInTheDocument();
});
});
Loading

0 comments on commit 2f38e39

Please sign in to comment.