Skip to content

feat(hooks): Initial useOptimizelyFeature hook #26

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
"@types/hoist-non-react-statics": "^3.3.1",
"@types/jest": "^23.3.12",
"@types/prop-types": "^15.5.6",
"@types/react": "^16.7.18",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.7.1",
"@types/react": "^16.8.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"jest": "^23.6.0",
"react": "^16.7.0",
"react": "^16.8.0",
"react-dom": "^16.7.0",
"rollup": "^1.1.0",
"rollup-plugin-commonjs": "^9.2.0",
Expand Down
312 changes: 312 additions & 0 deletions src/hooks/useOptimizelyFeature.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
/**
* Copyright 2018-2019, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from "react"
import * as Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"
Enzyme.configure({ adapter: new Adapter() })

import { mount } from "enzyme"
import { OptimizelyProvider } from "../Provider"
import { ReactSDKClient } from "../client"
import { useOptimizelyFeature } from "./useOptimizelyFeature"

describe("useOptimizelyFeature()", () => {
let resolver: any
let optimizelyMock: ReactSDKClient
const isEnabled = true
const featureVariables = {
foo: "bar"
}

jest.spyOn(React, "useEffect").mockImplementation(f => f())

beforeEach(() => {
const onReadyPromise = new Promise((resolve, reject) => {
resolver = {
reject,
resolve
}
})

optimizelyMock = ({
onReady: jest.fn().mockImplementation(config => onReadyPromise),
getFeatureVariables: jest.fn().mockImplementation(() => featureVariables),
isFeatureEnabled: jest.fn().mockImplementation(() => isEnabled),
onUserUpdate: jest.fn().mockImplementation(handler => () => { }),
notificationCenter: {
addNotificationListener: jest
.fn()
.mockImplementation((type, handler) => { }),
removeNotificationListener: jest.fn().mockImplementation(id => { })
},
user: {
id: "testuser",
attributes: {}
}
} as unknown) as ReactSDKClient
})

it("throws an error when not rendered in the context of an OptimizelyProvider", () => {
expect(() => {
// @ts-ignore
mount(function TestComponent() {
useOptimizelyFeature("feature1")
return "Test Component"
})
}).toThrow()
})

describe("when the isServerSide prop is false", () => {
it("should wait until onReady() is resolved then render result of isFeatureEnabled and getFeatureVariables", async () => {
function TestComponent() {
const { isEnabled, variables, loading } = useOptimizelyFeature(
"feature1"
)
if (loading) return null
return <>{`${isEnabled ? "true" : "false"}|${variables.foo}`}</>
}
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock}>
<TestComponent />
</OptimizelyProvider>
)

expect(optimizelyMock.onReady).toHaveBeenCalled()

// while it's waiting for onReady()
expect(component.text()).toBe("")
resolver.resolve({ success: true })

await optimizelyMock.onReady()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledWith("feature1")
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith(
"feature1"
)
expect(component.text()).toBe("true|bar")
})

it("should respect the timeout provided in <OptimizelyProvider>", async () => {
function TestComponent() {
const { isEnabled, variables, loading } = useOptimizelyFeature(
"feature1"
)
if (loading) return null
return <>{`${isEnabled ? "true" : "false"}|${variables.foo}`}</>
}
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock} timeout={200}>
<TestComponent />
</OptimizelyProvider>
)

expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 200 })

// while it's waiting for onReady()
expect(component.text()).toBe("")
resolver.resolve({ sucess: true })

await optimizelyMock.onReady()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledWith("feature1")
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith(
"feature1"
)
expect(component.text()).toBe("true|bar")
})

it("should respect a locally passed timeout prop", async () => {
function TestComponent() {
const {
isEnabled,
variables,
loading
} = useOptimizelyFeature("feature1", { timeout: 100 })
if (loading) return null
return <>{`${isEnabled ? "true" : "false"}|${variables.foo}`}</>
}
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock} timeout={200}>
<TestComponent />
</OptimizelyProvider>
)

expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 100 })

// while it's waiting for onReady()
expect(component.text()).toBe("")
resolver.resolve({ sucess: true })

await optimizelyMock.onReady()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledWith("feature1")
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith(
"feature1"
)
expect(component.text()).toBe("true|bar")
})

describe(`when the "autoUpdate" prop is true`, () => {
it("should update when the OPTIMIZELY_CONFIG_UPDATE handler is called", async () => {
function TestComponent() {
const {
isEnabled,
variables,
loading
} = useOptimizelyFeature("feature1", { autoUpdate: true })
if (loading) return null
return <>{`${isEnabled ? "true" : "false"}|${variables.foo}`}</>
}
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock} timeout={200}>
<TestComponent />
</OptimizelyProvider>
)

expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 200 })

// while it's waiting for onReady()
expect(component.text()).toBe("")
resolver.resolve({ sucess: true })

await optimizelyMock.onReady()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledWith(
"feature1"
)
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith(
"feature1"
)
expect(component.text()).toBe("true|bar")

const updateFn = (optimizelyMock.notificationCenter
.addNotificationListener as jest.Mock).mock.calls[0][1]
// change the return value of activate
const mockIFE = optimizelyMock.isFeatureEnabled as jest.Mock
mockIFE.mockImplementationOnce(() => false)
const mockGFV = optimizelyMock.getFeatureVariables as jest.Mock
mockGFV.mockImplementationOnce(() => ({
foo: "baz"
}))

updateFn()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledTimes(2)
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledTimes(2)

expect(component.text()).toBe("false|baz")
})

it("should update when the user changes", async () => {
function TestComponent() {
const {
isEnabled,
variables,
loading
} = useOptimizelyFeature("feature1", { autoUpdate: true })
if (loading) return null
return <>{`${isEnabled ? "true" : "false"}|${variables.foo}`}</>
}
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock} timeout={200}>
<TestComponent />
</OptimizelyProvider>
)

expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 200 })

// while it's waiting for onReady()
expect(component.text()).toBe("")
resolver.resolve({ sucess: true })

await optimizelyMock.onReady()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledWith(
"feature1"
)
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith(
"feature1"
)
expect(component.text()).toBe("true|bar")

const updateFn = (optimizelyMock.onUserUpdate as jest.Mock).mock
.calls[0][0]
const mockIFE = optimizelyMock.isFeatureEnabled as jest.Mock
mockIFE.mockImplementationOnce(() => false)
const mockGFV = optimizelyMock.getFeatureVariables as jest.Mock
mockGFV.mockImplementationOnce(() => ({
foo: "baz"
}))

updateFn()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledTimes(2)
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledTimes(2)

expect(component.text()).toBe("false|baz")
})
})

describe("when the onReady() promise returns { success: false }", () => {
it("should still render", async () => {
function TestComponent() {
const { isEnabled, variables, loading } = useOptimizelyFeature(
"feature1"
)
if (loading) return null
return <>{`${isEnabled ? "true" : "false"}|${variables.foo}`}</>
}
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock} timeout={200}>
<TestComponent />
</OptimizelyProvider>
)

expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 200 })

// while it's waiting for onReady()
expect(component.text()).toBe("")
resolver.resolve({ sucess: false, reason: "fail" })

await optimizelyMock.onReady()

component.update()

expect(optimizelyMock.isFeatureEnabled).toHaveBeenCalledWith(
"feature1"
)
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith(
"feature1"
)
expect(component.text()).toBe("true|bar")
})
})
})
})
Loading