mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-18 21:27:43 +00:00
Implement initial Cypress testing configuration (#115)
* Implement initial Cypress testing configuration * Add docs on cypress for testing * Cypress cache for GitHub actions
This commit is contained in:
committed by
GitHub
parent
a6aee3802c
commit
76857c0a4b
11
.github/workflows/pr-checks.yml
vendored
11
.github/workflows/pr-checks.yml
vendored
@@ -21,10 +21,21 @@ jobs:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache cypress binary
|
||||
id: cypress-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /home/runner/.cache/Cypress
|
||||
key: cypress-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install node_modules
|
||||
if: steps.node-cache.outputs.cache-hit != 'true'
|
||||
run: npm install
|
||||
|
||||
- name: Install cypress
|
||||
if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: npx cypress install
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
npm run format
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -24,4 +24,7 @@ yarn.lock
|
||||
*.sw?
|
||||
|
||||
# Jambonz
|
||||
/public/fonts
|
||||
/public/fonts
|
||||
|
||||
# Cypress
|
||||
cypress/videos
|
||||
@@ -5,7 +5,7 @@
|
||||
npx lint-staged
|
||||
|
||||
# run tests
|
||||
npm run test
|
||||
# npm run test
|
||||
|
||||
# run build -- tsc
|
||||
# npm run build
|
||||
|
||||
11
cypress.config.ts
Normal file
11
cypress.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
video: false,
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
10
cypress/fixtures/accounts.json
Normal file
10
cypress/fixtures/accounts.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "default account",
|
||||
"account_sid": "9351f46a-678c-43f5-b8a6-d4eb58d131af"
|
||||
},
|
||||
{
|
||||
"name": "custom account",
|
||||
"account_sid": "04e36b64-a4e5-4221-be7e-db80da39234d"
|
||||
}
|
||||
]
|
||||
10
cypress/fixtures/applications.json
Normal file
10
cypress/fixtures/applications.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "dial time",
|
||||
"application_sid": "4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e"
|
||||
},
|
||||
{
|
||||
"name": "hello world",
|
||||
"application_sid": "7087fe50-8acb-4f3b-b820-97b573723aab"
|
||||
}
|
||||
]
|
||||
39
cypress/support/commands.ts
Normal file
39
cypress/support/commands.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export {};
|
||||
12
cypress/support/component-index.html
Normal file
12
cypress/support/component-index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
41
cypress/support/component.ts
Normal file
41
cypress/support/component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import jambonz global styles
|
||||
import "src/styles/index.scss";
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
import { mount } from "cypress/react18";
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
// with a <reference path="./component" /> at the top of your spec.
|
||||
declare global {
|
||||
// Disabling, but: https://typescript-eslint.io/rules/no-namespace/...
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("mount", mount);
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
@@ -29,6 +29,7 @@ with [typescript](https://www.typescriptlang.org/),
|
||||
[prettier](https://prettier.io/), [eslint](https://eslint.org/),
|
||||
[husky](https://typicode.github.io/husky/#/)
|
||||
and [lint-staged](https://www.npmjs.com/package/lint-staged).
|
||||
For testing we're using [cypress](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test).
|
||||
|
||||
## :lock: Auth middleware
|
||||
|
||||
@@ -177,6 +178,17 @@ the `BEM` style we use:
|
||||
}
|
||||
```
|
||||
|
||||
## :robot: Testing
|
||||
|
||||
We're using [cypress](https://docs.cypress.io/guides/component-testing/writing-your-first-component-test)
|
||||
for component testing. Cypress is already configured and we're actively working on backfilling complete
|
||||
test coverage of the application. There are some issues open for this so you may refer to those to check
|
||||
out the progress.
|
||||
|
||||
All new components should have tests written alongside them. For example, if you component is called
|
||||
`my-component.tsx` then you would also have a `my-component.cy.tsx` test file to go with it. You can
|
||||
refer to existing tests for some common patterns we use to write tests.
|
||||
|
||||
## :heart: Contributing
|
||||
|
||||
If you would like to contribute to this project please follow these simple guidelines:
|
||||
|
||||
2049
package-lock.json
generated
2049
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-webapp",
|
||||
"description": "Simple provisioning webapp for jambonz",
|
||||
"description": "A simple provisioning web app for jambonz",
|
||||
"version": "v1.0.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -34,7 +34,8 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --max-warnings=0",
|
||||
"format": "prettier --check .",
|
||||
"test": "echo 'need to setup testing'",
|
||||
"test": "cypress run --component --headless",
|
||||
"test:open": "cypress open --component",
|
||||
"serve": "serve -s dist -l ${HTTP_PORT:-3001}",
|
||||
"pm2": "pm2 start npm --name \"jambonz-webapp\" -- run serve",
|
||||
"deploy": "npm i && npm run build && npm run pm2"
|
||||
@@ -59,6 +60,7 @@
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^10.8.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
|
||||
37
src/components/access-control.cy.tsx
Normal file
37
src/components/access-control.cy.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
|
||||
import { MainApp } from "src/main-app";
|
||||
import { AccessControl } from "./access-control";
|
||||
|
||||
import type { ACLProps } from "./access-control";
|
||||
|
||||
/** Wrapper to pass different ACLs */
|
||||
const AccessControlTestWrapper = (props: Partial<ACLProps>) => {
|
||||
return (
|
||||
<MainApp>
|
||||
<AccessControl acl={props.acl!}>
|
||||
<div className="acl-div" />
|
||||
</AccessControl>
|
||||
</MainApp>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<AccessControl>", () => {
|
||||
it("mounts", () => {
|
||||
cy.mount(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
||||
});
|
||||
|
||||
it("has admin ACL", () => {
|
||||
cy.mount(<AccessControlTestWrapper acl="hasAdminAuth" />);
|
||||
|
||||
/** Default ACL applies admin auth -- the singleton admin user */
|
||||
cy.get(".acl-div").should("exist");
|
||||
});
|
||||
|
||||
it("has doesn't have teams fqdn ACL", () => {
|
||||
cy.mount(<AccessControlTestWrapper acl="hasMSTeamsFqdn" />);
|
||||
|
||||
/** Default ACL disables MS Teams FQDN */
|
||||
cy.get(".acl-div").should("not.exist");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { useSelectState } from "src/store";
|
||||
|
||||
import type { ACL } from "src/store/types";
|
||||
|
||||
type ACLProps = {
|
||||
export type ACLProps = {
|
||||
acl: keyof ACL;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
74
src/components/account-filter.cy.tsx
Normal file
74
src/components/account-filter.cy.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import { AccountFilter } from "./account-filter";
|
||||
|
||||
import type { AccountFilterProps } from "./account-filter";
|
||||
import type { Account } from "src/api/types";
|
||||
|
||||
/** Import fixture data directly so we don't use cy.fixture() ... */
|
||||
import accounts from "../../cypress/fixtures/accounts.json";
|
||||
|
||||
/** Wrapper to perform React state setup */
|
||||
const AccountFilterTestWrapper = (props: Partial<AccountFilterProps>) => {
|
||||
const [account, setAccount] = useState("");
|
||||
|
||||
return (
|
||||
<AccountFilter
|
||||
label="Test"
|
||||
accounts={accounts as Account[]}
|
||||
account={[account, setAccount]}
|
||||
defaultOption={props.defaultOption}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<AccountFilter>", () => {
|
||||
/** The AccountFilter uses sort with `localeCompare` */
|
||||
const accountsSorted = accounts.sort(sortLocaleName);
|
||||
|
||||
it("mounts", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
});
|
||||
|
||||
it("has label text", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Label text is properly set */
|
||||
cy.get("label").should("have.text", "Test:");
|
||||
});
|
||||
|
||||
it("has default value", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Default value is properly set to first option */
|
||||
cy.get("select").should("have.value", accountsSorted[0].account_sid);
|
||||
});
|
||||
|
||||
it("updates value onChange", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Assert onChange value updates */
|
||||
cy.get("select").select(accountsSorted[1].account_sid);
|
||||
cy.get("select").should("have.value", accountsSorted[1].account_sid);
|
||||
});
|
||||
|
||||
it("manages the focused state", () => {
|
||||
cy.mount(<AccountFilterTestWrapper />);
|
||||
|
||||
/** Test the `focused` state className (applied onFocus) */
|
||||
cy.get("select").select(accountsSorted[1].account_sid);
|
||||
cy.get(".account-filter").should("have.class", "focused");
|
||||
cy.get("select").blur();
|
||||
cy.get(".account-filter").should("not.have.class", "focused");
|
||||
});
|
||||
|
||||
it("renders with default option", () => {
|
||||
/** Test with the `defaultOption` prop */
|
||||
cy.mount(<AccountFilterTestWrapper defaultOption />);
|
||||
|
||||
/** No default value is set when this prop is present */
|
||||
cy.get("select").should("have.value", "");
|
||||
});
|
||||
});
|
||||
@@ -4,9 +4,9 @@ import { classNames } from "jambonz-ui";
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { Account } from "src/api/types";
|
||||
import { hasLength } from "src/utils";
|
||||
import { hasLength, sortLocaleName } from "src/utils";
|
||||
|
||||
type AccountFilterProps = {
|
||||
export type AccountFilterProps = {
|
||||
label?: string;
|
||||
account: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
accounts?: Account[];
|
||||
@@ -54,15 +54,13 @@ export const AccountFilter = ({
|
||||
!accounts.length && <option value="">No accounts</option>
|
||||
)}
|
||||
{hasLength(accounts) &&
|
||||
accounts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((acct) => {
|
||||
return (
|
||||
<option key={acct.account_sid} value={acct.account_sid}>
|
||||
{acct.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
accounts.sort(sortLocaleName).map((acct) => {
|
||||
return (
|
||||
<option key={acct.account_sid} value={acct.account_sid}>
|
||||
{acct.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
|
||||
87
src/components/application-filter.cy.tsx
Normal file
87
src/components/application-filter.cy.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import { ApplicationFilter } from "./application-filter";
|
||||
|
||||
import type { ApplicationFilterProps } from "./application-filter";
|
||||
import type { Application } from "src/api/types";
|
||||
|
||||
/** Import fixture data directly so we don't use cy.fixture() ... */
|
||||
import applications from "../../cypress/fixtures/applications.json";
|
||||
|
||||
/** Wrapper to perform React state setup */
|
||||
const ApplicationFilterTestWrapper = (
|
||||
props: Partial<ApplicationFilterProps>
|
||||
) => {
|
||||
const [application, setApplication] = useState("");
|
||||
|
||||
return (
|
||||
<ApplicationFilter
|
||||
label="Test"
|
||||
applications={applications as Application[]}
|
||||
application={[application, setApplication]}
|
||||
defaultOption={props.defaultOption}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<ApplicationFilter>", () => {
|
||||
/** The AccountFilter uses sort with `localeCompare` */
|
||||
const applicationsSorted = applications.sort(sortLocaleName);
|
||||
|
||||
it("mounts", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
});
|
||||
|
||||
it("has label text", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Label text is properly set */
|
||||
cy.get("label").should("have.text", "Test:");
|
||||
});
|
||||
|
||||
it("has default value", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Default value is properly set to first option */
|
||||
cy.get("select").should(
|
||||
"have.value",
|
||||
applicationsSorted[0].application_sid
|
||||
);
|
||||
});
|
||||
|
||||
it("updates value onChange", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Assert onChange value updates */
|
||||
cy.get("select").select(applicationsSorted[1].application_sid);
|
||||
cy.get("select").should(
|
||||
"have.value",
|
||||
applicationsSorted[1].application_sid
|
||||
);
|
||||
});
|
||||
|
||||
it("manages focused state", () => {
|
||||
cy.mount(<ApplicationFilterTestWrapper />);
|
||||
|
||||
/** Test the `focused` state className (applied onFocus) */
|
||||
cy.get("select").select(applicationsSorted[1].application_sid);
|
||||
cy.get(".application-filter").should("have.class", "focused");
|
||||
cy.get("select").blur();
|
||||
cy.get(".application-filter").should("not.have.class", "focused");
|
||||
});
|
||||
|
||||
it("renders default option", () => {
|
||||
/** Test with the `defaultOption` prop */
|
||||
cy.mount(
|
||||
<ApplicationFilterTestWrapper defaultOption="Choose Application" />
|
||||
);
|
||||
|
||||
/** No default value is set when this prop is present */
|
||||
cy.get("select").should("have.value", "");
|
||||
|
||||
/** Validate that our prop renders correct default option text */
|
||||
cy.get("option").first().should("have.text", "Choose Application");
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,11 @@ import React, { useState } from "react";
|
||||
import { classNames } from "jambonz-ui";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import type { Application } from "src/api/types";
|
||||
|
||||
type ApplicationFilterProps = JSX.IntrinsicElements["select"] & {
|
||||
export type ApplicationFilterProps = JSX.IntrinsicElements["select"] & {
|
||||
label?: string;
|
||||
application: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
applications?: Application[];
|
||||
@@ -40,15 +41,13 @@ export const ApplicationFilter = ({
|
||||
>
|
||||
{defaultOption && <option value="">{defaultOption}</option>}
|
||||
{applications &&
|
||||
applications
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((app) => {
|
||||
return (
|
||||
<option key={app.application_sid} value={app.application_sid}>
|
||||
{app.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
applications.sort(sortLocaleName).map((app) => {
|
||||
return (
|
||||
<option key={app.application_sid} value={app.application_sid}>
|
||||
{app.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>
|
||||
<Icons.ChevronUp />
|
||||
|
||||
71
src/components/select-filter.cy.tsx
Normal file
71
src/components/select-filter.cy.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { SelectFilter } from "./select-filter";
|
||||
|
||||
/** Wrapper to perform React state setup */
|
||||
const SelectFilterTestWrapper = ({ onSelectSpy = () => null }) => {
|
||||
const [selected, setSelected] = useState("");
|
||||
|
||||
return (
|
||||
<SelectFilter
|
||||
id="test"
|
||||
label="Test"
|
||||
options={[
|
||||
{ name: "Option one", value: "1" },
|
||||
{ name: "Option two", value: "2" },
|
||||
{ name: "Option three", value: "3" },
|
||||
]}
|
||||
filter={[selected, setSelected]}
|
||||
handleSelect={onSelectSpy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<SelectFilter>", () => {
|
||||
it("mounts", () => {
|
||||
cy.mount(<SelectFilterTestWrapper />);
|
||||
});
|
||||
|
||||
it("has label text", () => {
|
||||
cy.mount(<SelectFilterTestWrapper />);
|
||||
|
||||
/** Label text is properly set */
|
||||
cy.get("label").should("have.text", "Test:");
|
||||
});
|
||||
|
||||
it("has id prop", () => {
|
||||
cy.mount(<SelectFilterTestWrapper />);
|
||||
|
||||
/** Select ID attribute is properly set */
|
||||
cy.get("select").should("have.id", "test");
|
||||
});
|
||||
|
||||
it("has default value", () => {
|
||||
cy.mount(<SelectFilterTestWrapper />);
|
||||
|
||||
/** Default value is properly set to first option */
|
||||
cy.get("select").should("have.value", "1");
|
||||
});
|
||||
|
||||
it("calls onChange handler", () => {
|
||||
/** Create a spy for the `onChange` handler */
|
||||
const onSelectSpy = cy.spy().as("onSelectSpy");
|
||||
|
||||
cy.mount(<SelectFilterTestWrapper onSelectSpy={onSelectSpy} />);
|
||||
|
||||
/** Assert onChange value and custom handler is called */
|
||||
cy.get("select").select("2");
|
||||
cy.get("select").should("have.value", "2");
|
||||
cy.get("@onSelectSpy").should("have.been.calledOnce");
|
||||
});
|
||||
|
||||
it("manages focused state", () => {
|
||||
cy.mount(<SelectFilterTestWrapper />);
|
||||
|
||||
/** Test the `focused` state className (applied onFocus) */
|
||||
cy.get("select").select("3");
|
||||
cy.get(".select-filter").should("have.class", "focused");
|
||||
cy.get("select").blur();
|
||||
cy.get(".select-filter").should("not.have.class", "focused");
|
||||
});
|
||||
});
|
||||
18
src/main-app.tsx
Normal file
18
src/main-app.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { StateProvider } from "./store";
|
||||
import { AuthProvider } from "./router/auth";
|
||||
|
||||
/** Export `MainApp` so it can be used in Cypress component tests */
|
||||
export const MainApp = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<StateProvider>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</StateProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
};
|
||||
24
src/main.tsx
24
src/main.tsx
@@ -1,27 +1,15 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import { MainApp } from "./main-app";
|
||||
import { Router } from "./router";
|
||||
import { StateProvider } from "./store";
|
||||
import { AuthProvider } from "./router/auth";
|
||||
|
||||
import "./styles/index.scss";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<StateProvider>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Router />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</StateProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
};
|
||||
|
||||
const root: Element = document.getElementById("root")!;
|
||||
|
||||
createRoot(root).render(<App />);
|
||||
createRoot(root).render(
|
||||
<MainApp>
|
||||
<Router />
|
||||
</MainApp>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getUser, getServiceProviders } from "src/api";
|
||||
import { sortLocaleName } from "src/utils";
|
||||
|
||||
import type { State, Action } from "./types";
|
||||
import type { ServiceProvider, User } from "src/api/types";
|
||||
@@ -17,9 +18,7 @@ export const serviceProvidersAction = (
|
||||
action: Action<keyof State>
|
||||
) => {
|
||||
// Sorts for consistent list view
|
||||
action.payload = (<ServiceProvider[]>action.payload).sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
action.payload = (<ServiceProvider[]>action.payload).sort(sortLocaleName);
|
||||
|
||||
// Sets initial currentServiceProvider
|
||||
if (!state.currentServiceProvider) {
|
||||
|
||||
@@ -120,6 +120,11 @@ export const formatTime = (seconds: number) => {
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
};
|
||||
|
||||
export const sortLocaleName = (
|
||||
a: Required<{ name: string }>,
|
||||
b: Required<{ name: string }>
|
||||
) => a.name.localeCompare(b.name);
|
||||
|
||||
export {
|
||||
withSuspense,
|
||||
useMobileMedia,
|
||||
|
||||
@@ -22,6 +22,6 @@
|
||||
"src": ["./src"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "cypress"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user