mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-04 19:21:58 +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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+6
-18
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user