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:
Brandon Lee Kitajchuk
2022-09-26 10:14:41 -07:00
committed by GitHub
parent a6aee3802c
commit 76857c0a4b
24 changed files with 2524 additions and 48 deletions
+37
View 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");
});
});
+1 -1
View File
@@ -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
View 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", "");
});
});
+9 -11
View File
@@ -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
View 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");
});
});
+9 -10
View File
@@ -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
View 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
View 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>
);
};
+6 -18
View File
@@ -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>
);
+2 -3
View File
@@ -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) {
+5
View File
@@ -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,