-
Notifications
You must be signed in to change notification settings - Fork 116
Metrics demo react #4868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Metrics demo react #4868
Changes from all commits
e200331
412e951
9c27e8a
511c8ae
4391acc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,4 +13,35 @@ declare interface Window { | |
MktoForms2: any; | ||
Vimeo: any; | ||
DNS_VERIFICATION_TOKEN: string; | ||
SENTRY_DSN: string; | ||
CSRF_TOKEN: string; | ||
SNAP_PUBLICISE_DATA: { | ||
hasScreenshot: boolean; | ||
isReleased: boolean; | ||
private: boolean; | ||
trending: boolean; | ||
}; | ||
SNAP_SETTINGS_DATA: { | ||
blacklist_countries: string[]; | ||
blacklist_country_keys: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Inclusive naming check] reported by reviewdog 🐶 |
||
countries: Array<{ key: string; name: string }>; | ||
country_keys_status: string | null; | ||
private: boolean; | ||
publisher_name: string; | ||
snap_id: string; | ||
snap_name: string; | ||
snap_title: string; | ||
status: string; | ||
store: string; | ||
territory_distribution_status: string; | ||
unlisted: boolean; | ||
update_metadata_on_release: boolean; | ||
visibility: string; | ||
visibility_locked: boolean; | ||
whitelist_countries: string[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Inclusive naming check] reported by reviewdog 🐶 |
||
whitelist_country_keys: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Inclusive naming check] reported by reviewdog 🐶 |
||
}; | ||
SNAP_LISTING_DATA: { | ||
DNS_VERIFICATION_TOKEN: string; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { render, screen, waitFor } from "@testing-library/react"; | ||
import userEvent from "@testing-library/user-event"; | ||
import "@testing-library/jest-dom"; | ||
|
||
import SaveAndPreview from "./SaveAndPreview"; | ||
|
||
const reset = jest.fn(); | ||
|
||
const renderComponent = ( | ||
isDirty: boolean, | ||
isSaving: boolean, | ||
isValid: boolean | ||
) => { | ||
return render( | ||
<SaveAndPreview | ||
snapName="test-snap-name" | ||
isDirty={isDirty} | ||
reset={reset} | ||
isSaving={isSaving} | ||
isValid={isValid} | ||
/> | ||
); | ||
}; | ||
|
||
test("the 'Revert' button is disabled by default", () => { | ||
renderComponent(false, false, true); | ||
expect(screen.getByRole("button", { name: "Revert" })).toHaveAttribute("aria-disabled","true"); | ||
}); | ||
|
||
test("the 'Revert' button is enabled is data is dirty", () => { | ||
renderComponent(true, false, true); | ||
expect(screen.getByRole("button", { name: "Revert" })).not.toBeDisabled(); | ||
}); | ||
|
||
test("the 'Save' button is disabled by default", () => { | ||
renderComponent(false, false, true); | ||
expect(screen.getByRole("button", { name: "Save" })).toHaveAttribute("aria-disabled","true"); | ||
}); | ||
|
||
test("the 'Save' button is enabled is data is dirty", () => { | ||
renderComponent(true, false, true); | ||
expect(screen.getByRole("button", { name: "Save" })).not.toBeDisabled(); | ||
}); | ||
|
||
test("the 'Save' button shows loading state if saving", () => { | ||
renderComponent(true, true, true); | ||
expect(screen.getByRole("button", { name: "Saving" })).toBeInTheDocument(); | ||
}); | ||
|
||
test("the 'Save' button is disabled when saving", () => { | ||
renderComponent(true, true, true); | ||
expect(screen.getByRole("button", { name: "Saving" })).toHaveAttribute("aria-disabled","true"); | ||
}); | ||
|
||
test("the 'Save' button is disabled if the form is invalid", () => { | ||
renderComponent(false, false, false); | ||
expect(screen.getByRole("button", { name: "Save" })).toHaveAttribute("aria-disabled","true"); | ||
}); | ||
|
||
test("revert button resets the form", async () => { | ||
const user = userEvent.setup(); | ||
renderComponent(true, false, true); | ||
await user.click(screen.getByRole("button", { name: "Revert" })); | ||
await waitFor(() => { | ||
expect(reset).toHaveBeenCalled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { useRef } from "react"; | ||
import { Row, Col, Button } from "@canonical/react-components"; | ||
|
||
import debounce from "../../../libs/debounce"; | ||
|
||
type Props = { | ||
snapName: string; | ||
isDirty: boolean; | ||
reset: Function; | ||
isSaving: boolean; | ||
isValid: boolean; | ||
showPreview?: boolean; | ||
}; | ||
|
||
function SaveAndPreview({ | ||
snapName, | ||
isDirty, | ||
reset, | ||
isSaving, | ||
showPreview, | ||
}: Props) { | ||
const stickyBar = useRef<HTMLDivElement>(null); | ||
const mainPanel = document.querySelector(".l-main") as HTMLElement; | ||
|
||
const handleScroll = () => { | ||
stickyBar?.current?.classList.toggle( | ||
"sticky-shadow", | ||
stickyBar?.current?.getBoundingClientRect()?.top === 0 | ||
); | ||
}; | ||
|
||
if (mainPanel) { | ||
mainPanel.addEventListener("scroll", debounce(handleScroll, 10, false)); | ||
} | ||
|
||
return ( | ||
<> | ||
<div | ||
className="snapcraft-p-sticky js-sticky-bar" | ||
ref={stickyBar} | ||
style={{ margin: "0 -1.5rem", padding: "0 1.5rem" }} | ||
> | ||
<Row> | ||
<Col size={7}> | ||
<p className="u-no-margin--bottom"> | ||
Updates to this information will appear immediately on the{" "} | ||
<a href={`/${snapName}`}>snap listing page</a>. | ||
</p> | ||
</Col> | ||
<Col size={5}> | ||
<div className="u-align--right"> | ||
{showPreview && ( | ||
<Button | ||
type="submit" | ||
className="p-button--base p-tooltip--btm-center" | ||
aria-describedby="preview-tooltip" | ||
form="preview-form" | ||
> | ||
Preview | ||
<span | ||
className="p-tooltip__message" | ||
role="tooltip" | ||
id="preview-tooltip" | ||
> | ||
Previews will only work in the same browser, locally | ||
</span> | ||
</Button> | ||
)} | ||
<Button | ||
appearance="default" | ||
disabled={!isDirty} | ||
type="reset" | ||
onClick={() => { | ||
reset(); | ||
}} | ||
> | ||
Revert | ||
</Button> | ||
<Button | ||
appearance="positive" | ||
disabled={!isDirty || isSaving} | ||
type="submit" | ||
style={{ minWidth: "68px" }} | ||
> | ||
{isSaving ? ( | ||
<i className="p-icon--spinner is-light u-animation--spin"> | ||
Saving | ||
</i> | ||
) : ( | ||
"Save" | ||
)} | ||
</Button> | ||
</div> | ||
</Col> | ||
</Row> | ||
</div> | ||
<div className="u-fixed-width"> | ||
<hr className="u-no-margin--bottom" /> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default SaveAndPreview; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from "./SaveAndPreview"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { Notification } from "@canonical/react-components"; | ||
|
||
type Props = { | ||
hasSaved: boolean; | ||
setHasSaved: Function; | ||
savedError: boolean | Array<{ message: string }>; | ||
setSavedError: Function; | ||
}; | ||
|
||
function SaveStateNotifications({ | ||
hasSaved, | ||
setHasSaved, | ||
savedError, | ||
setSavedError, | ||
}: Props) { | ||
return ( | ||
<> | ||
{hasSaved && ( | ||
<div className="u-fixed-width"> | ||
<Notification | ||
severity="positive" | ||
title="Changes applied successfully." | ||
onDismiss={() => { | ||
setHasSaved(false); | ||
}} | ||
/> | ||
</div> | ||
)} | ||
|
||
{savedError && ( | ||
<div className="u-fixed-width"> | ||
<Notification | ||
severity="negative" | ||
title="Error" | ||
onDismiss={() => { | ||
setHasSaved(false); | ||
setSavedError(false); | ||
}} | ||
> | ||
Changes have not been saved. | ||
<br /> | ||
{savedError === true | ||
? "Something went wrong." | ||
: savedError.map((error) => `${error.message}`).join("\n")} | ||
</Notification> | ||
</div> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
export default SaveStateNotifications; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { screen, render } from "@testing-library/react"; | ||
import userEvent from "@testing-library/user-event"; | ||
import "@testing-library/jest-dom"; | ||
|
||
import SaveStateNotifications from "../SaveStateNotifications"; | ||
|
||
type Options = { | ||
hasSaved?: boolean; | ||
setHasSaved?: Function; | ||
savedError?: boolean | Array<{ message: string }>; | ||
setSavedError?: Function; | ||
}; | ||
|
||
const renderComponent = (options: Options) => { | ||
return render( | ||
<SaveStateNotifications | ||
hasSaved={options.hasSaved || false} | ||
setHasSaved={options.setHasSaved || jest.fn()} | ||
savedError={options.savedError || false} | ||
setSavedError={options.setSavedError || jest.fn()} | ||
/> | ||
); | ||
}; | ||
|
||
describe("SaveStateNotifications", () => { | ||
test("shows success notification if saved", () => { | ||
renderComponent({ hasSaved: true }); | ||
expect( | ||
screen.getByRole("heading", { name: "Changes applied successfully." }) | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
test("doesn't show success notification if not saved", () => { | ||
renderComponent({ hasSaved: false }); | ||
expect( | ||
screen.queryByRole("heading", { name: "Changes applied successfully." }) | ||
).not.toBeInTheDocument(); | ||
}); | ||
|
||
test("success notifcation can be closed", async () => { | ||
const user = userEvent.setup(); | ||
const setHasSaved = jest.fn(); | ||
renderComponent({ hasSaved: true, setHasSaved }); | ||
await user.click( | ||
screen.getByRole("button", { name: "Close notification" }) | ||
); | ||
expect(setHasSaved).toHaveBeenCalled(); | ||
}); | ||
|
||
test("shows error notification if saved", () => { | ||
renderComponent({ savedError: true }); | ||
expect( | ||
screen.getByText(/Changes have not been saved./) | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
test("doesn't show error notification if not saved", () => { | ||
renderComponent({ savedError: false }); | ||
expect( | ||
screen.queryByText(/Changes have not been saved./) | ||
).not.toBeInTheDocument(); | ||
}); | ||
|
||
test("shows generic error if message is boolean", () => { | ||
renderComponent({ savedError: true }); | ||
expect(screen.getByText(/Something went wrong./)).toBeInTheDocument(); | ||
}); | ||
|
||
test("shows custom error if message is an array", () => { | ||
renderComponent({ | ||
savedError: [ | ||
{ message: "Saving error" }, | ||
{ message: "Field is required" }, | ||
], | ||
}); | ||
expect(screen.getByText(/Saving error/)).toBeInTheDocument(); | ||
expect(screen.getByText(/Field is required/)).toBeInTheDocument(); | ||
}); | ||
|
||
test("error notifcation can be closed", async () => { | ||
const user = userEvent.setup(); | ||
const setHasSaved = jest.fn(); | ||
renderComponent({ savedError: true, setHasSaved }); | ||
await user.click( | ||
screen.getByRole("button", { name: "Close notification" }) | ||
); | ||
expect(setHasSaved).toHaveBeenCalled(); | ||
}); | ||
|
||
test("error notifcation can be cleared", async () => { | ||
const user = userEvent.setup(); | ||
const setSavedError = jest.fn(); | ||
renderComponent({ savedError: true, setSavedError }); | ||
await user.click( | ||
screen.getByRole("button", { name: "Close notification" }) | ||
); | ||
expect(setSavedError).toHaveBeenCalled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from "./SaveStateNotifications"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Inclusive naming check] reported by reviewdog 🐶
[warning]
blacklist
may be insensitive, usedenylist
,blocklist
instead