@khanacademy/wonder-blocks-link
Advanced tools
Comparing version 3.3.1 to 3.4.0
@@ -36,2 +36,3 @@ // @flow | ||
pressed: state === "pressed", | ||
waiting: false, | ||
}; | ||
@@ -38,0 +39,0 @@ test(`kind:${kind} href:${href} light:${String( |
@@ -8,81 +8,339 @@ // @flow | ||
const wait = (delay: number = 0) => | ||
new Promise((resolve, reject) => { | ||
// eslint-disable-next-line no-restricted-syntax | ||
return setTimeout(resolve, delay); | ||
}); | ||
describe("Link", () => { | ||
beforeEach(() => { | ||
// Note: window.location.assign needs a mock function in the testing | ||
// environment. | ||
window.location.assign = jest.fn(); | ||
unmountAll(); | ||
}); | ||
test("client-side navigation", () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link testId="link" href="/foo"> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
afterEach(() => { | ||
window.location.assign.mockClear(); | ||
}); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", {button: 0}); | ||
describe("client-side navigation", () => { | ||
test("works for known URLs", () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link testId="link" href="/foo"> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Assert | ||
expect(wrapper.find("#foo").exists()).toBe(true); | ||
}); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", {button: 0}); | ||
test("client-side navigation with unknown URL fails", () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link testId="link" href="/unknown"> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Assert | ||
expect(wrapper.find("#foo").exists()).toBe(true); | ||
}); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", {button: 0}); | ||
test("navigation to without route does not render", () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link testId="link" href="/unknown"> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Assert | ||
expect(wrapper.find("#foo").exists()).toBe(false); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", {button: 0}); | ||
// Assert | ||
expect(wrapper.find("#foo").exists()).toBe(false); | ||
}); | ||
test("waits until beforeNav resolves before navigating", async () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.resolve()} | ||
> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
await wait(0); | ||
wrapper.update(); | ||
// Assert | ||
expect(wrapper.find("#foo")).toExist(); | ||
}); | ||
test("doesn't navigate before beforeNav resolves", async () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.resolve()} | ||
> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
// Assert | ||
expect(wrapper.find("#foo")).not.toExist(); | ||
}); | ||
test("does not navigate if beforeNav rejects", async () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.reject()} | ||
> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
await wait(0); | ||
wrapper.update(); | ||
// Assert | ||
expect(wrapper.find("#foo")).not.toExist(); | ||
}); | ||
test("runs safeWithNav if set", async () => { | ||
// Arrange | ||
const safeWithNavMock = jest.fn(); | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.resolve()} | ||
safeWithNav={safeWithNavMock} | ||
> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
await wait(0); | ||
wrapper.update(); | ||
// Assert | ||
expect(safeWithNavMock).toHaveBeenCalled(); | ||
}); | ||
test("doesn't run safeWithNav until beforeNav resolves", () => { | ||
// Arrange | ||
const safeWithNavMock = jest.fn(); | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.resolve()} | ||
safeWithNav={safeWithNavMock} | ||
> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
// Assert | ||
expect(safeWithNavMock).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
test("client-side navigation with `skipClientNav` set to `true` fails", () => { | ||
// Arrange | ||
const wrapper = mount( | ||
<MemoryRouter> | ||
<div> | ||
<Link testId="link" href="/foo" skipClientNav> | ||
Click me! | ||
</Link> | ||
<Switch> | ||
<Route path="/foo"> | ||
<div id="foo">Hello, world!</div> | ||
</Route> | ||
</Switch> | ||
</div> | ||
</MemoryRouter>, | ||
); | ||
describe("full page load navigation", () => { | ||
test("doesn't redirect if safeWithNav hasn't resolved yet when skipClientNav=true", () => { | ||
// Arrange | ||
jest.spyOn(window.location, "assign").mockImplementation(() => {}); | ||
const wrapper = mount( | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
safeWithNav={() => Promise.resolve()} | ||
skipClientNav={true} | ||
> | ||
Click me! | ||
</Link>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", {button: 0}); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
// Assert | ||
expect(wrapper.find("#foo").exists()).toBe(false); | ||
// Assert | ||
expect(window.location.assign).not.toHaveBeenCalled(); | ||
}); | ||
test("redirects after safeWithNav resolves when skipClientNav=true", async () => { | ||
// Arrange | ||
jest.spyOn(window.location, "assign").mockImplementation(() => {}); | ||
const wrapper = mount( | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
safeWithNav={() => Promise.resolve()} | ||
skipClientNav={true} | ||
> | ||
Click me! | ||
</Link>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
await wait(0); | ||
wrapper.update(); | ||
// Assert | ||
expect(window.location.assign).toHaveBeenCalledWith("/foo"); | ||
}); | ||
test("redirects after beforeNav and safeWithNav resolve when skipClientNav=true", async () => { | ||
// Arrange | ||
jest.spyOn(window.location, "assign").mockImplementation(() => {}); | ||
const wrapper = mount( | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.resolve()} | ||
safeWithNav={() => Promise.resolve()} | ||
skipClientNav={true} | ||
> | ||
Click me! | ||
</Link>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
await wait(0); | ||
wrapper.update(); | ||
// Assert | ||
expect(window.location.assign).toHaveBeenCalledWith("/foo"); | ||
}); | ||
test("doesn't redirect before beforeNav resolves when skipClientNav=true", () => { | ||
// Arrange | ||
jest.spyOn(window.location, "assign").mockImplementation(() => {}); | ||
const wrapper = mount( | ||
<Link | ||
testId="link" | ||
href="/foo" | ||
beforeNav={() => Promise.resolve()} | ||
skipClientNav={true} | ||
> | ||
Click me! | ||
</Link>, | ||
); | ||
// Act | ||
const buttonWrapper = wrapper.find(`[data-test-id="link"]`).first(); | ||
buttonWrapper.simulate("click", { | ||
button: 0, | ||
}); | ||
// Assert | ||
expect(window.location.assign).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
@@ -43,2 +43,3 @@ // @flow | ||
testId, | ||
waiting: _, | ||
...handlers | ||
@@ -45,0 +46,0 @@ } = this.props; |
@@ -103,2 +103,19 @@ // @flow | ||
onClick?: (e: SyntheticEvent<>) => mixed, | ||
/** | ||
* Run async code before navigating to the URL passed to `href`. If the | ||
* promise returned rejects then navigation will not occur. | ||
* | ||
* If both safeWithNav and beforeNav are provided, beforeNav will be run | ||
* first and safeWithNav will only be run if beforeNav does not reject. | ||
*/ | ||
beforeNav?: () => Promise<mixed>, | ||
/** | ||
* Run async code in the background while client-side navigating. If the | ||
* browser does a full page load navigation, the callback promise must be | ||
* settled before the navigation will occur. Errors are ignored so that | ||
* navigation is guaranteed to succeed. | ||
*/ | ||
safeWithNav?: () => Promise<mixed>, | ||
|}; | ||
@@ -135,2 +152,4 @@ | ||
onClick, | ||
beforeNav, | ||
safeWithNav, | ||
href, | ||
@@ -151,5 +170,7 @@ skipClientNav, | ||
disabled={false} | ||
onClick={onClick} | ||
href={href} | ||
role="link" | ||
onClick={onClick} | ||
beforeNav={beforeNav} | ||
safeWithNav={safeWithNav} | ||
> | ||
@@ -156,0 +177,0 @@ {(state, handlers) => { |
@@ -242,3 +242,4 @@ import { createElement, Component } from 'react'; | ||
testId = _this$props.testId, | ||
handlers = _objectWithoutProperties(_this$props, ["caret", "children", "skipClientNav", "focused", "hovered", "href", "kind", "light", "visitable", "pressed", "style", "testId"]); | ||
_ = _this$props.waiting, | ||
handlers = _objectWithoutProperties(_this$props, ["caret", "children", "skipClientNav", "focused", "hovered", "href", "kind", "light", "visitable", "pressed", "style", "testId", "waiting"]); | ||
@@ -363,6 +364,8 @@ var router = this.context.router; | ||
onClick = _this$props.onClick, | ||
beforeNav = _this$props.beforeNav, | ||
safeWithNav = _this$props.safeWithNav, | ||
href = _this$props.href, | ||
skipClientNav = _this$props.skipClientNav, | ||
children = _this$props.children, | ||
sharedProps = _objectWithoutProperties(_this$props, ["onClick", "href", "skipClientNav", "children"]); | ||
sharedProps = _objectWithoutProperties(_this$props, ["onClick", "beforeNav", "safeWithNav", "href", "skipClientNav", "children"]); | ||
@@ -372,5 +375,7 @@ var ClickableBehavior = getClickableBehavior(href, skipClientNav, this.context.router); | ||
disabled: false, | ||
href: href, | ||
role: "link", | ||
onClick: onClick, | ||
href: href, | ||
role: "link" | ||
beforeNav: beforeNav, | ||
safeWithNav: safeWithNav | ||
}, function (state, handlers) { | ||
@@ -377,0 +382,0 @@ return /*#__PURE__*/createElement(LinkCore, _extends({}, sharedProps, state, handlers, { |
@@ -226,3 +226,4 @@ module.exports = | ||
testId = _this$props.testId, | ||
handlers = _objectWithoutProperties(_this$props, ["caret", "children", "skipClientNav", "focused", "hovered", "href", "kind", "light", "visitable", "pressed", "style", "testId"]); | ||
_ = _this$props.waiting, | ||
handlers = _objectWithoutProperties(_this$props, ["caret", "children", "skipClientNav", "focused", "hovered", "href", "kind", "light", "visitable", "pressed", "style", "testId", "waiting"]); | ||
@@ -384,6 +385,8 @@ var router = this.context.router; | ||
onClick = _this$props.onClick, | ||
beforeNav = _this$props.beforeNav, | ||
safeWithNav = _this$props.safeWithNav, | ||
href = _this$props.href, | ||
skipClientNav = _this$props.skipClientNav, | ||
children = _this$props.children, | ||
sharedProps = link_objectWithoutProperties(_this$props, ["onClick", "href", "skipClientNav", "children"]); | ||
sharedProps = link_objectWithoutProperties(_this$props, ["onClick", "beforeNav", "safeWithNav", "href", "skipClientNav", "children"]); | ||
@@ -393,5 +396,7 @@ var ClickableBehavior = Object(wonder_blocks_core_["getClickableBehavior"])(href, skipClientNav, this.context.router); | ||
disabled: false, | ||
href: href, | ||
role: "link", | ||
onClick: onClick, | ||
href: href, | ||
role: "link" | ||
beforeNav: beforeNav, | ||
safeWithNav: safeWithNav | ||
}, function (state, handlers) { | ||
@@ -398,0 +403,0 @@ return /*#__PURE__*/external_react_["createElement"](link_core_LinkCore, link_extends({}, sharedProps, state, handlers, { |
{ | ||
"name": "@khanacademy/wonder-blocks-link", | ||
"version": "3.3.1", | ||
"version": "3.4.0", | ||
"design": "v1", | ||
@@ -18,4 +18,4 @@ "publishConfig": { | ||
"dependencies": { | ||
"@khanacademy/wonder-blocks-color": "^1.1.12", | ||
"@khanacademy/wonder-blocks-core": "^2.5.2" | ||
"@khanacademy/wonder-blocks-color": "^1.1.13", | ||
"@khanacademy/wonder-blocks-core": "^2.6.0" | ||
}, | ||
@@ -31,3 +31,3 @@ "peerDependencies": { | ||
}, | ||
"gitHead": "a2b5548f5d7db108c39961071d081ec5d96fa88f" | ||
"gitHead": "65c91a0dcf5f6f6e3b235d40bab721ddfdd4f5cf" | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
84424
17
1457
299
12
12
2
174