first commit
This commit is contained in:
51
services/web/test/frontend/shared/components/icon.test.jsx
Normal file
51
services/web/test/frontend/shared/components/icon.test.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
|
||||
import Icon from '../../../../frontend/js/shared/components/icon'
|
||||
|
||||
describe('<Icon />', function () {
|
||||
it('renders basic fa classes', function () {
|
||||
const { container } = render(<Icon type="angle-down" />)
|
||||
const element = container.querySelector('i.fa.fa-angle-down')
|
||||
expect(element).to.exist
|
||||
})
|
||||
|
||||
it('renders with aria-hidden', function () {
|
||||
const { container } = render(<Icon type="angle-down" />)
|
||||
const element = container.querySelector('i[aria-hidden="true"]')
|
||||
expect(element).to.exist
|
||||
})
|
||||
|
||||
it('renders accessible label', function () {
|
||||
render(<Icon type="angle-down" accessibilityLabel="Accessible Foo" />)
|
||||
screen.getByText('Accessible Foo')
|
||||
})
|
||||
|
||||
it('renders with spin', function () {
|
||||
const { container } = render(<Icon type="angle-down" spin />)
|
||||
const element = container.querySelector('i.fa.fa-angle-down.fa-spin')
|
||||
expect(element).to.exist
|
||||
})
|
||||
|
||||
it('renders with fw', function () {
|
||||
const { container } = render(<Icon type="angle-down" fw />)
|
||||
const element = container.querySelector('i.fa.fa-angle-down.fa-fw')
|
||||
expect(element).to.exist
|
||||
})
|
||||
|
||||
it('renders with modifier', function () {
|
||||
const { container } = render(<Icon type="angle-down" modifier="2x" />)
|
||||
const element = container.querySelector('i.fa.fa-angle-down.fa-2x')
|
||||
expect(element).to.exist
|
||||
})
|
||||
|
||||
it('renders with custom clases', function () {
|
||||
const { container } = render(
|
||||
<Icon type="angle-down" className="custom-icon-class" />
|
||||
)
|
||||
const element = container.querySelector(
|
||||
'i.fa.fa-angle-down.custom-icon-class'
|
||||
)
|
||||
expect(element).to.exist
|
||||
})
|
||||
})
|
@@ -0,0 +1,62 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
import Notification from '../../../../frontend/js/shared/components/notification'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('<Notification />', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
|
||||
beforeEach(function () {
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
})
|
||||
|
||||
it('renders and is not dismissible by default', function () {
|
||||
render(<Notification type="info" content={<p>A notification</p>} />)
|
||||
screen.getByText('A notification')
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).to.be.null
|
||||
})
|
||||
|
||||
it('renders with action', function () {
|
||||
render(
|
||||
<Notification
|
||||
type="info"
|
||||
content={<p>A notification</p>}
|
||||
action={<a href="/">Action</a>}
|
||||
/>
|
||||
)
|
||||
screen.getByText('A notification')
|
||||
screen.getByRole('link', { name: 'Action' })
|
||||
})
|
||||
|
||||
it('renders with close button', function () {
|
||||
render(
|
||||
<Notification type="info" content={<p>A notification</p>} isDismissible />
|
||||
)
|
||||
screen.getByText('A notification')
|
||||
screen.getByRole('button', { name: 'Close' })
|
||||
})
|
||||
|
||||
it('renders with title and content passed as HTML', function () {
|
||||
render(
|
||||
<Notification
|
||||
type="info"
|
||||
content={<p>A notification</p>}
|
||||
title="A title"
|
||||
/>
|
||||
)
|
||||
screen.getByText('A title')
|
||||
screen.getByText('A notification')
|
||||
})
|
||||
|
||||
it('renders with content when passed as a string', function () {
|
||||
render(
|
||||
<Notification type="info" content="A notification" title="A title" />
|
||||
)
|
||||
screen.getByText('A notification')
|
||||
})
|
||||
})
|
@@ -0,0 +1,67 @@
|
||||
import { expect } from 'chai'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Pagination from '../../../../frontend/js/shared/components/pagination'
|
||||
|
||||
describe('<Pagination />', function () {
|
||||
it('renders with current page handled', async function () {
|
||||
render(
|
||||
<Pagination currentPage={6} totalPages={10} handlePageClick={() => {}} />
|
||||
)
|
||||
await screen.findByLabelText('Page 6, Current Page')
|
||||
})
|
||||
it('renders with nearby page buttons and prev/next button', async function () {
|
||||
render(
|
||||
<Pagination currentPage={2} totalPages={4} handlePageClick={() => {}} />
|
||||
)
|
||||
await screen.findByLabelText('Page 2, Current Page')
|
||||
await screen.findByLabelText('Go to page 1')
|
||||
await screen.findByLabelText('Go to page 3')
|
||||
await screen.findByLabelText('Go to page 4')
|
||||
await screen.findByLabelText('Go to Previous Page')
|
||||
await screen.findByLabelText('Go to Next Page')
|
||||
})
|
||||
it('does not render the prev button when expected', async function () {
|
||||
render(
|
||||
<Pagination currentPage={1} totalPages={2} handlePageClick={() => {}} />
|
||||
)
|
||||
await screen.findByLabelText('Page 1, Current Page')
|
||||
await screen.findByLabelText('Go to Next Page')
|
||||
expect(screen.queryByLabelText('Go to Prev Page')).to.be.null
|
||||
})
|
||||
it('does not render the next button when expected', async function () {
|
||||
render(
|
||||
<Pagination currentPage={2} totalPages={2} handlePageClick={() => {}} />
|
||||
)
|
||||
await screen.findByLabelText('Page 2, Current Page')
|
||||
await screen.findByLabelText('Go to Previous Page')
|
||||
expect(screen.queryByLabelText('Go to Next Page')).to.be.null
|
||||
})
|
||||
it('renders 1 ellipses when there are more pages than buttons and on first page', async function () {
|
||||
render(
|
||||
<Pagination currentPage={1} totalPages={10} handlePageClick={() => {}} />
|
||||
)
|
||||
const ellipses = await screen.findAllByText('…')
|
||||
expect(ellipses.length).to.equal(1)
|
||||
})
|
||||
it('renders 1 ellipses when on last page and there are more previous pages than buttons', async function () {
|
||||
render(
|
||||
<Pagination currentPage={10} totalPages={10} handlePageClick={() => {}} />
|
||||
)
|
||||
const ellipses = await screen.findAllByText('…')
|
||||
expect(ellipses.length).to.equal(1)
|
||||
})
|
||||
it('renders 2 ellipses when there are more pages than buttons', async function () {
|
||||
render(
|
||||
<Pagination currentPage={5} totalPages={10} handlePageClick={() => {}} />
|
||||
)
|
||||
const ellipses = await screen.findAllByText('…')
|
||||
expect(ellipses.length).to.equal(2)
|
||||
})
|
||||
it('only renders the number of page buttons set by maxOtherPageButtons', async function () {
|
||||
render(
|
||||
<Pagination currentPage={1} totalPages={100} handlePageClick={() => {}} />
|
||||
)
|
||||
const items = document.querySelectorAll('button')
|
||||
expect(items.length).to.equal(6) // 5 page buttons + next button
|
||||
})
|
||||
})
|
@@ -0,0 +1,16 @@
|
||||
import { expect } from 'chai'
|
||||
import { render } from '@testing-library/react'
|
||||
import Processing from '../../../../frontend/js/shared/components/processing'
|
||||
|
||||
describe('<Processing />', function () {
|
||||
it('renders processing UI when isProcessing is true', function () {
|
||||
const { container } = render(<Processing isProcessing />)
|
||||
const element = container.querySelector('i.fa.fa-refresh')
|
||||
expect(element).to.exist
|
||||
})
|
||||
it('does not render processing UI when isProcessing is false', function () {
|
||||
const { container } = render(<Processing isProcessing={false} />)
|
||||
const element = container.querySelector('i.fa.fa-refresh')
|
||||
expect(element).to.not.exist
|
||||
})
|
||||
})
|
@@ -0,0 +1,57 @@
|
||||
import { expect } from 'chai'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import RadioChip from '@/shared/components/radio-chip'
|
||||
|
||||
describe('<RadioChip />', function () {
|
||||
const defaultProps = {
|
||||
name: 'test',
|
||||
label: 'Test',
|
||||
value: 'testValue',
|
||||
onChange: () => {},
|
||||
}
|
||||
|
||||
describe('component renders and with label', function () {
|
||||
it('renders and label is provided', function () {
|
||||
render(<RadioChip {...defaultProps} />)
|
||||
screen.getByText('Test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('props', function () {
|
||||
it('should be checked when the checked prop is provided', function () {
|
||||
render(<RadioChip {...defaultProps} checked />)
|
||||
const radioChip = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radioChip.checked).to.equal(true)
|
||||
})
|
||||
|
||||
it('should be disabled when the disabled prop is provided', function () {
|
||||
render(<RadioChip {...defaultProps} disabled />)
|
||||
const radioChip = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radioChip.disabled).to.equal(true)
|
||||
})
|
||||
|
||||
it('should have the required attribute when the required prop is provided', function () {
|
||||
render(<RadioChip {...defaultProps} required />)
|
||||
const radioChip = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radioChip.required).to.equal(true)
|
||||
})
|
||||
|
||||
it('should use the provided name prop', function () {
|
||||
render(<RadioChip {...defaultProps} name="testName" />)
|
||||
const radioChip = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radioChip.name).to.equal('testName')
|
||||
})
|
||||
|
||||
it('should use the provided value prop', function () {
|
||||
render(<RadioChip {...defaultProps} />)
|
||||
const radioChip = screen.getByRole('radio') as HTMLInputElement
|
||||
expect(radioChip.value).to.equal('testValue')
|
||||
})
|
||||
|
||||
it('should have the data-disabled attribute when the disabled prop is provided', function () {
|
||||
render(<RadioChip {...defaultProps} disabled />)
|
||||
const label = screen.getByText('Test')?.closest('label')
|
||||
expect(label?.getAttribute('data-disabled')).to.equal('true')
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,93 @@
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { expect } from 'chai'
|
||||
import React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import useAbortController from '../../../../frontend/js/shared/hooks/use-abort-controller'
|
||||
import { getJSON } from '../../../../frontend/js/infrastructure/fetch-json'
|
||||
|
||||
describe('useAbortController', function () {
|
||||
let status: {
|
||||
loading: boolean
|
||||
success: boolean | null
|
||||
error: any | null
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
status = {
|
||||
loading: false,
|
||||
success: null,
|
||||
error: null,
|
||||
}
|
||||
})
|
||||
|
||||
after(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
function AbortableRequest({ url }: { url: string }) {
|
||||
const { signal } = useAbortController()
|
||||
|
||||
React.useEffect(() => {
|
||||
status.loading = true
|
||||
|
||||
getJSON(url, { signal })
|
||||
.then(() => {
|
||||
status.success = true
|
||||
})
|
||||
.catch(error => {
|
||||
status.error = error
|
||||
})
|
||||
.finally(() => {
|
||||
status.loading = false
|
||||
})
|
||||
}, [signal, url])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
it('calls then when the request succeeds', async function () {
|
||||
fetchMock.get('/test', { status: 204 }, { delay: 100 })
|
||||
|
||||
render(<AbortableRequest url="/test" />)
|
||||
|
||||
expect(status.loading).to.be.true
|
||||
await waitFor(() => expect(status.loading).to.be.false)
|
||||
|
||||
expect(status.success).to.be.true
|
||||
expect(status.error).to.be.null
|
||||
})
|
||||
|
||||
it('calls catch when the request fails', async function () {
|
||||
fetchMock.get('/test', { status: 500 }, { delay: 100 })
|
||||
|
||||
render(<AbortableRequest url="/test" />)
|
||||
|
||||
expect(status.loading).to.be.true
|
||||
await waitFor(() => expect(status.loading).to.be.false)
|
||||
|
||||
expect(status.success).to.be.null
|
||||
expect(status.error).not.to.be.null
|
||||
})
|
||||
|
||||
it('cancels a request when unmounted', async function () {
|
||||
fetchMock.get('/test', { status: 204 }, { delay: 100 })
|
||||
|
||||
const { unmount } = render(<AbortableRequest url="/test" />)
|
||||
|
||||
expect(status.loading).to.be.true
|
||||
|
||||
unmount()
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
expect(fetchMock.callHistory.done()).to.be.true
|
||||
|
||||
// wait for Promises to be resolved
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
expect(status.success).to.be.null
|
||||
expect(status.error).to.be.null
|
||||
expect(status.loading).to.be.true
|
||||
})
|
||||
})
|
182
services/web/test/frontend/shared/hooks/use-async.test.ts
Normal file
182
services/web/test/frontend/shared/hooks/use-async.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks'
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import useAsync from '../../../../frontend/js/shared/hooks/use-async'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
function deferred() {
|
||||
let res!: (
|
||||
value: Record<string, unknown> | PromiseLike<Record<string, unknown>>
|
||||
) => void
|
||||
let rej!: (reason?: any) => void
|
||||
|
||||
const promise = new Promise<Record<string, unknown>>((resolve, reject) => {
|
||||
res = resolve
|
||||
rej = reject
|
||||
})
|
||||
|
||||
return { promise, resolve: res, reject: rej }
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
status: 'idle',
|
||||
data: null,
|
||||
error: null,
|
||||
|
||||
isIdle: true,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
}
|
||||
|
||||
const pendingState = {
|
||||
...defaultState,
|
||||
status: 'pending',
|
||||
isIdle: false,
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
const resolvedState = {
|
||||
...defaultState,
|
||||
status: 'resolved',
|
||||
isIdle: false,
|
||||
isSuccess: true,
|
||||
}
|
||||
|
||||
const rejectedState = {
|
||||
...defaultState,
|
||||
status: 'rejected',
|
||||
isIdle: false,
|
||||
isError: true,
|
||||
}
|
||||
|
||||
describe('useAsync', function () {
|
||||
let spyOnDebugConsoleError: sinon.SinonSpy
|
||||
beforeEach(function () {
|
||||
spyOnDebugConsoleError = sinon.spy(debugConsole, 'error')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
spyOnDebugConsoleError.restore()
|
||||
})
|
||||
|
||||
it('exposes the methods', function () {
|
||||
const { result } = renderHook(() => useAsync())
|
||||
|
||||
expect(result.current.setData).to.be.a('function')
|
||||
expect(result.current.setError).to.be.a('function')
|
||||
expect(result.current.runAsync).to.be.a('function')
|
||||
})
|
||||
|
||||
it('calling `runAsync` with a promise which resolves', async function () {
|
||||
const { promise, resolve } = deferred()
|
||||
const { result } = renderHook(() => useAsync())
|
||||
|
||||
expect(result.current).to.include(defaultState)
|
||||
|
||||
let p: Promise<unknown>
|
||||
act(() => {
|
||||
p = result.current.runAsync(promise)
|
||||
})
|
||||
|
||||
expect(result.current).to.include(pendingState)
|
||||
|
||||
const resolvedValue = {}
|
||||
await act(async () => {
|
||||
resolve(resolvedValue)
|
||||
await p
|
||||
})
|
||||
|
||||
expect(result.current).to.include({
|
||||
...resolvedState,
|
||||
data: resolvedValue,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.reset()
|
||||
})
|
||||
|
||||
expect(result.current).to.include(defaultState)
|
||||
})
|
||||
|
||||
it('calling `runAsync` with a promise which rejects', async function () {
|
||||
const { promise, reject } = deferred()
|
||||
const { result } = renderHook(() => useAsync())
|
||||
|
||||
expect(result.current).to.include(defaultState)
|
||||
|
||||
let p: Promise<unknown>
|
||||
act(() => {
|
||||
p = result.current.runAsync(promise).catch(() => {})
|
||||
})
|
||||
|
||||
expect(result.current).to.include(pendingState)
|
||||
|
||||
const rejectedValue = Symbol('rejected value')
|
||||
await act(async () => {
|
||||
reject(rejectedValue)
|
||||
await p
|
||||
})
|
||||
|
||||
expect(result.current).to.include({
|
||||
...rejectedState,
|
||||
error: rejectedValue,
|
||||
})
|
||||
})
|
||||
|
||||
it('can specify an initial state', function () {
|
||||
const mockData = Symbol('resolved value')
|
||||
const customInitialState = { status: 'resolved' as const, data: mockData }
|
||||
const { result } = renderHook(() => useAsync(customInitialState))
|
||||
|
||||
expect(result.current).to.include({
|
||||
...resolvedState,
|
||||
...customInitialState,
|
||||
})
|
||||
})
|
||||
|
||||
it('can set the data', function () {
|
||||
const mockData = Symbol('resolved value')
|
||||
const { result } = renderHook(() => useAsync())
|
||||
|
||||
act(() => {
|
||||
result.current.setData(mockData)
|
||||
})
|
||||
|
||||
expect(result.current).to.include({
|
||||
...resolvedState,
|
||||
data: mockData,
|
||||
})
|
||||
})
|
||||
|
||||
it('can set the error', function () {
|
||||
const mockError = new Error('rejected value')
|
||||
const { result } = renderHook(() => useAsync())
|
||||
|
||||
act(() => {
|
||||
result.current.setError(mockError)
|
||||
})
|
||||
|
||||
expect(result.current).to.include({
|
||||
...rejectedState,
|
||||
error: mockError,
|
||||
})
|
||||
})
|
||||
|
||||
it('no state updates happen if the component is unmounted while pending', async function () {
|
||||
const { promise, resolve } = deferred()
|
||||
const { result, unmount } = renderHook(() => useAsync())
|
||||
|
||||
let p: Promise<unknown>
|
||||
act(() => {
|
||||
p = result.current.runAsync(promise)
|
||||
})
|
||||
unmount()
|
||||
await act(async () => {
|
||||
resolve({})
|
||||
await p
|
||||
})
|
||||
|
||||
expect(debugConsole.error).not.to.have.been.called
|
||||
})
|
||||
})
|
@@ -0,0 +1,34 @@
|
||||
import sinon from 'sinon'
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import useCallbackHandlers from '../../../../frontend/js/shared/hooks/use-callback-handlers'
|
||||
|
||||
describe('useCallbackHandlers', function () {
|
||||
it('adds, removes and calls all handlers without duplicate', async function () {
|
||||
const handler1 = sinon.stub()
|
||||
const handler2 = sinon.stub()
|
||||
const handler3 = sinon.stub()
|
||||
|
||||
const { result } = renderHook(() => useCallbackHandlers())
|
||||
|
||||
result.current.addHandler(handler1)
|
||||
result.current.deleteHandler(handler1)
|
||||
result.current.addHandler(handler1)
|
||||
|
||||
result.current.addHandler(handler2)
|
||||
result.current.deleteHandler(handler2)
|
||||
|
||||
result.current.addHandler(handler3)
|
||||
result.current.addHandler(handler3)
|
||||
|
||||
result.current.callHandlers('foo')
|
||||
result.current.callHandlers(1337)
|
||||
|
||||
sinon.assert.calledTwice(handler1)
|
||||
sinon.assert.calledWith(handler1, 'foo')
|
||||
sinon.assert.calledWith(handler1, 1337)
|
||||
|
||||
sinon.assert.notCalled(handler2)
|
||||
|
||||
sinon.assert.calledTwice(handler3)
|
||||
})
|
||||
})
|
@@ -0,0 +1,113 @@
|
||||
import { FC } from 'react'
|
||||
import useDetachAction from '../../../../frontend/js/shared/hooks/use-detach-action'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
|
||||
const DetachActionTest: FC<{
|
||||
actionName: string
|
||||
actionFunction: () => void
|
||||
handleClick: (trigger: (value: any) => void) => void
|
||||
}> = ({ actionName, actionFunction, handleClick }) => {
|
||||
const trigger = useDetachAction(
|
||||
actionName,
|
||||
actionFunction,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
|
||||
return (
|
||||
<button id="trigger" onClick={() => handleClick(trigger)}>
|
||||
trigger
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useDetachAction', function () {
|
||||
it('broadcast message as sender', function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachActionTest
|
||||
actionName="some-action"
|
||||
actionFunction={cy.stub().as('actionFunction')}
|
||||
handleClick={trigger => trigger('foo')}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
cy.get('#trigger').click()
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detacher',
|
||||
event: 'action-some-action',
|
||||
data: { args: ['foo'] },
|
||||
})
|
||||
cy.get('@actionFunction').should('not.have.been.called')
|
||||
})
|
||||
|
||||
it('call function as non-sender', function () {
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachActionTest
|
||||
actionName="some-action"
|
||||
actionFunction={cy.stub().as('actionFunction')}
|
||||
handleClick={trigger => trigger('foo')}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
cy.get('#trigger').click()
|
||||
cy.get('@postDetachMessage').should('not.have.been.called')
|
||||
cy.get('@actionFunction').should('have.been.calledWith', 'foo')
|
||||
})
|
||||
|
||||
it('receive message and call function as target', function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachActionTest
|
||||
actionName="some-action"
|
||||
actionFunction={cy.stub().as('actionFunction')}
|
||||
handleClick={trigger => trigger('foo')}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'action-some-action',
|
||||
data: { args: ['foo'] },
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('@actionFunction').should('have.been.calledWith', 'foo')
|
||||
})
|
||||
|
||||
it('receive message and does not call function as non-target', function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachActionTest
|
||||
actionName="some-action"
|
||||
actionFunction={cy.stub().as('actionFunction')}
|
||||
handleClick={trigger => trigger('foo')}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'action-some-action',
|
||||
data: { args: [] },
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('@actionFunction').should('not.have.been.called')
|
||||
})
|
||||
})
|
@@ -0,0 +1,249 @@
|
||||
import useDetachLayout from '../../../../frontend/js/shared/hooks/use-detach-layout'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
const DetachLayoutTest = () => {
|
||||
const { role, reattach, detach, isLinked, isLinking, isRedundant } =
|
||||
useDetachLayout()
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>
|
||||
role: <span id="role">{role || 'none'}</span>
|
||||
</legend>
|
||||
<OLFormGroup>
|
||||
<OLFormCheckbox id="isLinked" inline checked={isLinked} readOnly />
|
||||
<OLFormLabel>linked</OLFormLabel>
|
||||
</OLFormGroup>
|
||||
<OLFormGroup>
|
||||
<OLFormCheckbox id="isLinking" inline checked={isLinking} readOnly />
|
||||
<OLFormLabel>linking</OLFormLabel>
|
||||
</OLFormGroup>
|
||||
<OLFormGroup>
|
||||
<OLFormCheckbox
|
||||
id="isRedundant"
|
||||
inline
|
||||
checked={isRedundant}
|
||||
readOnly
|
||||
/>
|
||||
<OLFormLabel>redundant</OLFormLabel>
|
||||
</OLFormGroup>
|
||||
<OLButton id="reattach" onClick={reattach}>
|
||||
reattach
|
||||
</OLButton>
|
||||
<OLButton id="detach" onClick={detach}>
|
||||
detach
|
||||
</OLButton>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useDetachLayout', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.stub(window, 'open').as('openWindow')
|
||||
cy.stub(window, 'close').as('closeWindow')
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('detaching', function () {
|
||||
// 1. create hook in normal mode
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachLayoutTest />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'none')
|
||||
|
||||
// 2. detach
|
||||
cy.get('#detach').click()
|
||||
cy.get('@openWindow').should(
|
||||
'have.been.calledOnceWith',
|
||||
Cypress.sinon.match(/\/detached/),
|
||||
'_blank'
|
||||
)
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('be.checked')
|
||||
cy.get('#role').should('have.text', 'detacher')
|
||||
})
|
||||
|
||||
it('detacher role', function () {
|
||||
// 1. create hook in detacher mode
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachLayoutTest />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detacher')
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'connected',
|
||||
})
|
||||
})
|
||||
|
||||
// 2. simulate connected detached tab
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detacher',
|
||||
event: 'up',
|
||||
})
|
||||
|
||||
cy.get('#isLinked').should('be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detacher')
|
||||
|
||||
// 3. simulate closed detached tab
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'closed',
|
||||
})
|
||||
})
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detacher')
|
||||
|
||||
// 4. simulate up detached tab
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'up',
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('#isLinked').should('be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detacher')
|
||||
|
||||
// 5. reattach
|
||||
cy.get('@postDetachMessage').invoke('resetHistory')
|
||||
cy.get('#reattach').click()
|
||||
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'none')
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detacher',
|
||||
event: 'reattach',
|
||||
})
|
||||
})
|
||||
|
||||
it('reset detacher role when other detacher tab connects', function () {
|
||||
// 1. create hook in detacher mode
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachLayoutTest />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detacher')
|
||||
|
||||
// 2. simulate other detacher tab
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'up',
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('#isRedundant').should('be.checked')
|
||||
cy.get('#role').should('have.text', 'none')
|
||||
})
|
||||
|
||||
it('detached role', function () {
|
||||
// 1. create hook in detached mode
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachLayoutTest />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detached')
|
||||
|
||||
// 2. simulate up detacher tab
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'up',
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('#isLinked').should('be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detached')
|
||||
|
||||
// 3. simulate closed detacher tab
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'closed',
|
||||
})
|
||||
})
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detached')
|
||||
|
||||
// 4. simulate up detacher tab
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'up',
|
||||
})
|
||||
})
|
||||
cy.get('#isLinked').should('be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detached')
|
||||
|
||||
// 5. simulate closed detached tab
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'closed',
|
||||
})
|
||||
})
|
||||
cy.get('#isLinked').should('be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detached')
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detached',
|
||||
event: 'up',
|
||||
})
|
||||
|
||||
// 6. simulate reattach event
|
||||
cy.get('@postDetachMessage').invoke('resetHistory')
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'reattach',
|
||||
})
|
||||
})
|
||||
cy.get('#isLinked').should('not.be.checked')
|
||||
cy.get('#isLinking').should('not.be.checked')
|
||||
cy.get('#role').should('have.text', 'detached')
|
||||
cy.get('@closeWindow').should('have.been.called')
|
||||
})
|
||||
})
|
@@ -0,0 +1,100 @@
|
||||
import { FC } from 'react'
|
||||
import useDetachState from '../../../../frontend/js/shared/hooks/use-detach-state'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
|
||||
const DetachStateTest: FC<{
|
||||
stateKey: string
|
||||
defaultValue: any
|
||||
senderRole?: string
|
||||
targetRole?: string
|
||||
handleClick: (setValue: (value: any) => void) => void
|
||||
}> = ({ stateKey, defaultValue, handleClick, senderRole, targetRole }) => {
|
||||
const [value, setValue] = useDetachState(
|
||||
stateKey,
|
||||
defaultValue,
|
||||
senderRole,
|
||||
targetRole
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="value">{value}</div>
|
||||
<button id="setValue" onClick={() => handleClick(setValue)}>
|
||||
set value
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useDetachState', function () {
|
||||
it('create and update state', function () {
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachStateTest
|
||||
stateKey="some-key"
|
||||
defaultValue="foobar"
|
||||
handleClick={setValue => {
|
||||
setValue('barbaz')
|
||||
}}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('#value').should('have.text', 'foobar')
|
||||
cy.get('#setValue').click()
|
||||
cy.get('#value').should('have.text', 'barbaz')
|
||||
})
|
||||
|
||||
it('broadcast message as sender', function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachStateTest
|
||||
stateKey="some-key"
|
||||
defaultValue={null}
|
||||
senderRole="detacher"
|
||||
targetRole="detached"
|
||||
handleClick={setValue => {
|
||||
setValue('barbaz1')
|
||||
}}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
cy.get('#setValue').click()
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detacher',
|
||||
event: 'state-some-key',
|
||||
data: { value: 'barbaz1' },
|
||||
})
|
||||
})
|
||||
|
||||
it('receive message as target', function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<DetachStateTest
|
||||
stateKey="some-key"
|
||||
defaultValue={null}
|
||||
senderRole="detacher"
|
||||
targetRole="detached"
|
||||
handleClick={() => {}}
|
||||
/>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'state-some-key',
|
||||
data: { value: 'barbaz2' },
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('#value').should('have.text', 'barbaz2')
|
||||
})
|
||||
})
|
@@ -0,0 +1,157 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { expect } from 'chai'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import useExpandCollapse from '../../../../frontend/js/shared/hooks/use-expand-collapse'
|
||||
|
||||
const sampleContent = (
|
||||
<div>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
||||
<p>Maecenas maximus ultrices sollicitudin.</p>
|
||||
<p>Praesent mollis arcu eget molestie viverra.</p>
|
||||
<p>Pellentesque eget molestie nisl, non hendrerit lectus.</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const originalScrollHeight = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetHeight'
|
||||
)
|
||||
const originalScrollWidth = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetWidth'
|
||||
)
|
||||
|
||||
function ExpandCollapseTestUI({ expandCollapseArgs }) {
|
||||
const { expandableProps } = useExpandCollapse(expandCollapseArgs)
|
||||
return (
|
||||
<>
|
||||
<div {...expandableProps}>{sampleContent}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ExpandCollapseTestUI.propTypes = {
|
||||
expandCollapseArgs: PropTypes.object,
|
||||
}
|
||||
|
||||
describe('useExpandCollapse', function () {
|
||||
// JSDom doesn't compute layout/sizing, so we need to simulate sizing for the elements
|
||||
// Here we are simulating that the content is bigger than the `collapsedSize`, so
|
||||
// the expand-collapse widget is used
|
||||
beforeEach(function () {
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
|
||||
configurable: true,
|
||||
value: 500,
|
||||
})
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
|
||||
configurable: true,
|
||||
value: 500,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'scrollHeight',
|
||||
originalScrollHeight
|
||||
)
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'scrollWidth',
|
||||
originalScrollWidth
|
||||
)
|
||||
})
|
||||
|
||||
describe('custom CSS classes', function () {
|
||||
it('supports a custom CSS class', function () {
|
||||
const testArgs = {
|
||||
classes: {
|
||||
container: 'my-custom-class',
|
||||
},
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const elWithCustomCSSClass = container.querySelector('div')
|
||||
expect(elWithCustomCSSClass).to.exist
|
||||
})
|
||||
it('supports an extra custom CSS class for the collapsed state', function () {
|
||||
const testArgs = {
|
||||
classes: {
|
||||
containerCollapsed: 'my-custom-collapsed-class',
|
||||
},
|
||||
}
|
||||
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const elWithCustomCollapsedCSSClass = container.querySelector(
|
||||
'.my-custom-collapsed-class'
|
||||
)
|
||||
expect(elWithCustomCollapsedCSSClass).to.exist
|
||||
})
|
||||
it('ignores the collapsed CSS class when expanded', function () {
|
||||
const testArgs = {
|
||||
initiallyExpanded: true,
|
||||
classes: {
|
||||
containerCollapsed: 'my-custom-collapsed-class',
|
||||
},
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const elWithCustomCollapsedCSSClass = container.querySelector(
|
||||
'.my-custom-collapsed-class'
|
||||
)
|
||||
expect(elWithCustomCollapsedCSSClass).to.not.exist
|
||||
})
|
||||
})
|
||||
describe('height and width support via dimension argument', function () {
|
||||
it('defaults to height', function () {
|
||||
const { container } = render(<ExpandCollapseTestUI />)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.not.be.empty
|
||||
expect(expandCollapseEl.style.width).to.be.empty
|
||||
})
|
||||
it('supports width', function () {
|
||||
const testArgs = {
|
||||
dimension: 'width',
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.be.empty
|
||||
expect(expandCollapseEl.style.width).to.not.be.empty
|
||||
})
|
||||
})
|
||||
describe('collapsed size support via collapsedSize argument', function () {
|
||||
it('defaults to 0px', function () {
|
||||
const { container } = render(<ExpandCollapseTestUI />)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.equal('0px')
|
||||
})
|
||||
it('supports a custom collapsed size', function () {
|
||||
const testArgs = {
|
||||
collapsedSize: 200,
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.equal('200px')
|
||||
})
|
||||
it('supports a custom collapsed size for width', function () {
|
||||
const testArgs = {
|
||||
collapsedSize: 200,
|
||||
dimension: 'width',
|
||||
}
|
||||
const { container } = render(
|
||||
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
|
||||
)
|
||||
const expandCollapseEl = container.firstChild
|
||||
expect(expandCollapseEl.style.height).to.be.empty
|
||||
expect(expandCollapseEl.style.width).to.equal('200px')
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,183 @@
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import { useEffect } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import usePersistedState from '../../../../frontend/js/shared/hooks/use-persisted-state'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
|
||||
describe('usePersistedState', function () {
|
||||
beforeEach(function () {
|
||||
sinon.spy(window.Storage.prototype, 'getItem')
|
||||
sinon.spy(window.Storage.prototype, 'removeItem')
|
||||
sinon.spy(window.Storage.prototype, 'setItem')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('reads the value from localStorage', function () {
|
||||
const key = 'test'
|
||||
localStorage.setItem(key, 'foo')
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
const Test = () => {
|
||||
const [value] = usePersistedState(key)
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
screen.getByText('foo')
|
||||
|
||||
expect(window.Storage.prototype.getItem).to.have.callCount(1)
|
||||
expect(window.Storage.prototype.removeItem).to.have.callCount(0)
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
expect(localStorage.getItem(key)).to.equal('foo')
|
||||
})
|
||||
|
||||
it('uses the default value without storing anything', function () {
|
||||
const key = 'test:default'
|
||||
|
||||
const Test = () => {
|
||||
const [value] = usePersistedState(key, 'foo')
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
screen.getByText('foo')
|
||||
|
||||
expect(window.Storage.prototype.getItem).to.have.callCount(1)
|
||||
expect(window.Storage.prototype.removeItem).to.have.callCount(0)
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(0)
|
||||
|
||||
expect(localStorage.getItem(key)).to.be.null
|
||||
})
|
||||
|
||||
it('stores the new value in localStorage', function () {
|
||||
const key = 'test:store'
|
||||
localStorage.setItem(key, 'foo')
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
const Test = () => {
|
||||
const [value, setValue] = usePersistedState(key, 'bar')
|
||||
|
||||
useEffect(() => {
|
||||
setValue('baz')
|
||||
}, [setValue])
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
|
||||
screen.getByText('baz')
|
||||
|
||||
expect(window.Storage.prototype.getItem).to.have.callCount(1)
|
||||
expect(window.Storage.prototype.removeItem).to.have.callCount(0)
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(2)
|
||||
|
||||
expect(localStorage.getItem(key)).to.equal('baz')
|
||||
})
|
||||
|
||||
it('removes the value from localStorage if it equals the default value', function () {
|
||||
const key = 'test:store-default'
|
||||
localStorage.setItem(key, 'foo')
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
const Test = () => {
|
||||
const [value, setValue] = usePersistedState(key, 'bar')
|
||||
|
||||
useEffect(() => {
|
||||
// set a different value
|
||||
setValue('baz')
|
||||
expect(localStorage.getItem(key)).to.equal('baz')
|
||||
|
||||
// set the default value again
|
||||
setValue('bar')
|
||||
}, [setValue])
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
|
||||
screen.getByText('bar')
|
||||
|
||||
expect(window.Storage.prototype.getItem).to.have.callCount(2)
|
||||
expect(window.Storage.prototype.removeItem).to.have.callCount(1)
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(2)
|
||||
|
||||
expect(localStorage.getItem(key)).to.be.null
|
||||
})
|
||||
|
||||
it('handles function values', function () {
|
||||
const key = 'test:store'
|
||||
localStorage.setItem(key, 'foo')
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
const Test = () => {
|
||||
const [value, setValue] = usePersistedState<string>(key)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(value => value + 'bar')
|
||||
}, [setValue])
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
|
||||
screen.getByText('foobar')
|
||||
|
||||
expect(window.Storage.prototype.getItem).to.have.callCount(1)
|
||||
expect(window.Storage.prototype.removeItem).to.have.callCount(0)
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(2)
|
||||
|
||||
expect(localStorage.getItem(key)).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('handles syncing values via storage event', async function () {
|
||||
const key = 'test:sync'
|
||||
localStorage.setItem(key, 'foo')
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(1)
|
||||
|
||||
// listen for storage events
|
||||
const storageEventListener = sinon.stub()
|
||||
window.addEventListener('storage', storageEventListener)
|
||||
|
||||
const Test = () => {
|
||||
const [value, setValue] = usePersistedState(key, 'bar', true)
|
||||
|
||||
useEffect(() => {
|
||||
setValue('baz')
|
||||
}, [setValue])
|
||||
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
render(<Test />)
|
||||
|
||||
screen.getByText('baz')
|
||||
|
||||
expect(window.Storage.prototype.getItem).to.have.callCount(1)
|
||||
expect(window.Storage.prototype.removeItem).to.have.callCount(0)
|
||||
expect(window.Storage.prototype.setItem).to.have.callCount(2)
|
||||
|
||||
expect(localStorage.getItem(key)).to.equal('baz')
|
||||
|
||||
expect(storageEventListener).to.have.callCount(0)
|
||||
|
||||
// set the new value in localStorage
|
||||
localStorage.setItem(key, 'cat')
|
||||
|
||||
// dispatch a "storage" event and check that it's picked up by the hook
|
||||
window.dispatchEvent(new StorageEvent('storage', { key }))
|
||||
|
||||
await screen.findByText('cat')
|
||||
|
||||
expect(storageEventListener).to.have.callCount(1)
|
||||
})
|
||||
})
|
@@ -0,0 +1,37 @@
|
||||
import { useRecaptcha } from '@/shared/hooks/use-recaptcha'
|
||||
import * as ReactGoogleRecaptcha from 'react-google-recaptcha'
|
||||
|
||||
const ReCaptcha = () => {
|
||||
const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha()
|
||||
|
||||
const handleClick = async () => {
|
||||
await getReCaptchaToken()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactGoogleRecaptcha.ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
size="invisible"
|
||||
sitekey="123456"
|
||||
badge="inline"
|
||||
/>
|
||||
<button onClick={handleClick}>Click</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useRecaptcha', function () {
|
||||
it('should reset the captcha', function () {
|
||||
cy.spy(ReactGoogleRecaptcha.ReCAPTCHA.prototype, 'reset').as('resetSpy')
|
||||
cy.spy(ReactGoogleRecaptcha.ReCAPTCHA.prototype, 'executeAsync').as(
|
||||
'executeAsyncSpy'
|
||||
)
|
||||
|
||||
cy.mount(<ReCaptcha />)
|
||||
|
||||
cy.findByRole('button', { name: /click/i }).click()
|
||||
cy.get('@resetSpy').should('have.been.calledOnce')
|
||||
cy.get('@executeAsyncSpy').should('have.been.calledOnce')
|
||||
})
|
||||
})
|
115
services/web/test/frontend/shared/hooks/use-resize.spec.tsx
Normal file
115
services/web/test/frontend/shared/hooks/use-resize.spec.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
usePersistedResize,
|
||||
useResize,
|
||||
} from '../../../../frontend/js/shared/hooks/use-resize'
|
||||
|
||||
function Template({
|
||||
mousePos,
|
||||
getTargetProps,
|
||||
getHandleProps,
|
||||
}: ReturnType<typeof useResize>) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div
|
||||
id="target"
|
||||
{...getTargetProps({
|
||||
style: {
|
||||
width: '200px',
|
||||
height: '200px',
|
||||
border: '2px solid black',
|
||||
...(mousePos?.x && { width: `${mousePos.x}px` }),
|
||||
},
|
||||
})}
|
||||
>
|
||||
Demo content demo content demo content demo content demo content demo
|
||||
content
|
||||
</div>
|
||||
<div
|
||||
id="handle"
|
||||
{...getHandleProps({
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
height: '100%',
|
||||
width: '4px',
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PersistedResizeTest() {
|
||||
const props = usePersistedResize({ name: 'test' })
|
||||
|
||||
return <Template {...props} />
|
||||
}
|
||||
|
||||
function ResizeTest() {
|
||||
const props = useResize()
|
||||
|
||||
return <Template {...props} />
|
||||
}
|
||||
|
||||
describe('useResize', function () {
|
||||
it('should apply provided styles to the target', function () {
|
||||
cy.mount(<ResizeTest />)
|
||||
|
||||
// test a css prop being applied
|
||||
cy.get('#target').should('have.css', 'width', '200px')
|
||||
})
|
||||
|
||||
it('should apply provided styles to the handle', function () {
|
||||
cy.mount(<ResizeTest />)
|
||||
|
||||
// test a css prop being applied
|
||||
cy.get('#handle').should('have.css', 'width', '4px')
|
||||
})
|
||||
|
||||
it('should apply default styles to the handle', function () {
|
||||
cy.mount(<ResizeTest />)
|
||||
|
||||
cy.get('#handle')
|
||||
.should('have.css', 'cursor', 'col-resize')
|
||||
.and('have.css', 'user-select', 'none')
|
||||
})
|
||||
|
||||
it('should resize the target horizontally on mousedown and mousemove', function () {
|
||||
const xPos = 400
|
||||
cy.mount(<ResizeTest />)
|
||||
|
||||
cy.get('#handle').trigger('mousedown', { button: 0 })
|
||||
cy.get('#handle').trigger('mousemove', { clientX: xPos })
|
||||
cy.get('#handle').trigger('mouseup')
|
||||
|
||||
cy.get('#target').should('have.css', 'width', `${xPos}px`)
|
||||
})
|
||||
|
||||
it('should persist the resize data', function () {
|
||||
const xPos = 400
|
||||
cy.mount(<PersistedResizeTest />)
|
||||
|
||||
cy.get('#handle').trigger('mousedown', { button: 0 })
|
||||
cy.get('#handle').trigger('mousemove', { clientX: xPos })
|
||||
cy.get('#handle').trigger('mouseup')
|
||||
|
||||
cy.window()
|
||||
.its('localStorage.resizeable-test')
|
||||
.should('eq', `{"x":${xPos}}`)
|
||||
|
||||
// render the component again
|
||||
cy.mount(<PersistedResizeTest />)
|
||||
|
||||
cy.get('#target').should('have.css', 'width', `${xPos}px`)
|
||||
})
|
||||
})
|
35
services/web/test/frontend/shared/utils/colors.test.js
Normal file
35
services/web/test/frontend/shared/utils/colors.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { getHueForUserId } from '@/shared/utils/colors'
|
||||
|
||||
describe('colors', function () {
|
||||
const currentUserId = '5bf7dab7a18b0b7a1cf6738c'
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-user_id', currentUserId)
|
||||
})
|
||||
|
||||
describe('getHueForUserId', function () {
|
||||
it('returns the OWN_HUE for the current user', function () {
|
||||
expect(getHueForUserId(currentUserId)).to.equal(200)
|
||||
})
|
||||
|
||||
it('returns the ANONYMOUS_HUE for an anonymous user', function () {
|
||||
expect(getHueForUserId()).to.equal(100)
|
||||
expect(getHueForUserId('anonymous-user')).to.equal(100)
|
||||
})
|
||||
|
||||
it('generates a hue based on user id', function () {
|
||||
expect(
|
||||
getHueForUserId('59ad79f46337430b3d37cb9e', currentUserId)
|
||||
).to.equal(146)
|
||||
})
|
||||
|
||||
it('shifts the hue away from the OWN_HUE if it is within a threshold', function () {
|
||||
// Ordinarily, this user id would generate a hue of 183. However, this is
|
||||
// visually "too close" to the OWN_HUE, meaning that it could be
|
||||
// misinterpreted. Therefore we shift it away
|
||||
expect(getHueForUserId('20ad79f46337430b3d37cb9f')).to.equal(323)
|
||||
})
|
||||
})
|
||||
})
|
316
services/web/test/frontend/shared/utils/currency.test.js
Normal file
316
services/web/test/frontend/shared/utils/currency.test.js
Normal file
@@ -0,0 +1,316 @@
|
||||
import { expect } from 'chai'
|
||||
import { formatCurrency } from '../../../../frontend/js/shared/utils/currency'
|
||||
|
||||
/*
|
||||
Users can select any language we support, regardless of the country where they are located.
|
||||
Which mean that any combination of "supported language"-"supported currency" can be displayed
|
||||
on the user's screen.
|
||||
|
||||
Users located in the USA visiting https://fr.overleaf.com/user/subscription/plans
|
||||
should see amounts in USD (because of their IP address),
|
||||
but with French text, number formatting and currency formats (because of language choice).
|
||||
(e.g. 1 000,00 $)
|
||||
|
||||
Users located in the France visiting https://www.overleaf.com/user/subscription/plans
|
||||
should see amounts in EUR (because of their IP address),
|
||||
but with English text, number formatting and currency formats (because of language choice).
|
||||
(e.g. €1,000.00)
|
||||
*/
|
||||
|
||||
describe('formatCurrency', function () {
|
||||
describe('en', function () {
|
||||
const format = currency => priceInCents =>
|
||||
formatCurrency(priceInCents, currency)
|
||||
|
||||
describe('USD', function () {
|
||||
const formatUSD = format('USD')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatUSD(0)).to.equal('$0.00')
|
||||
expect(formatUSD(12.34)).to.equal('$12.34')
|
||||
expect(formatUSD(123)).to.equal('$123.00')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatUSD(1_000)).to.equal('$1,000.00')
|
||||
expect(formatUSD(98_765_432.1)).to.equal('$98,765,432.10')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatUSD(-0.01)).to.equal('-$0.01')
|
||||
expect(formatUSD(-12.34)).to.equal('-$12.34')
|
||||
expect(formatUSD(-123)).to.equal('-$123.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('EUR', function () {
|
||||
const formatEUR = format('EUR')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatEUR(0)).to.equal('€0.00')
|
||||
expect(formatEUR(12.34)).to.equal('€12.34')
|
||||
expect(formatEUR(123)).to.equal('€123.00')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatEUR(1_000)).to.equal('€1,000.00')
|
||||
expect(formatEUR(98_765_432.1)).to.equal('€98,765,432.10')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatEUR(-0.01)).to.equal('-€0.01')
|
||||
expect(formatEUR(-12.34)).to.equal('-€12.34')
|
||||
expect(formatEUR(-123)).to.equal('-€123.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('HUF', function () {
|
||||
const formatHUF = format('HUF')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatHUF(0)).to.equal('Ft 0.00')
|
||||
expect(formatHUF(12.34)).to.equal('Ft 12.34')
|
||||
expect(formatHUF(123)).to.equal('Ft 123.00')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatHUF(1_000)).to.equal('Ft 1,000.00')
|
||||
expect(formatHUF(98_765_432.1)).to.equal('Ft 98,765,432.10')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatHUF(-0.01)).to.equal('-Ft 0.01')
|
||||
expect(formatHUF(-12.34)).to.equal('-Ft 12.34')
|
||||
expect(formatHUF(-123)).to.equal('-Ft 123.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('CLP', function () {
|
||||
const formatCLP = format('CLP')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatCLP(0)).to.equal('$0')
|
||||
expect(formatCLP(12.34)).to.equal('$12')
|
||||
expect(formatCLP(123)).to.equal('$123')
|
||||
expect(formatCLP(1234)).to.equal('$1,234')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatCLP(1_000)).to.equal('$1,000')
|
||||
expect(formatCLP(98_765_432.1)).to.equal('$98,765,432')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatCLP(-1)).to.equal('-$1')
|
||||
expect(formatCLP(-12.34)).to.equal('-$12')
|
||||
expect(formatCLP(-1234)).to.equal('-$1,234')
|
||||
})
|
||||
})
|
||||
|
||||
describe('all currencies', function () {
|
||||
it('should format 1 "minimal atomic units"', function () {
|
||||
const amount = 1
|
||||
|
||||
// "no cents currencies"
|
||||
expect(format('CLP')(amount)).to.equal('$1')
|
||||
expect(format('JPY')(amount)).to.equal('¥1')
|
||||
expect(format('KRW')(amount)).to.equal('₩1')
|
||||
expect(format('VND')(amount)).to.equal('₫1')
|
||||
|
||||
// other currencies
|
||||
expect(format('AUD')(amount)).to.equal('$1.00')
|
||||
expect(format('BRL')(amount)).to.equal('R$1.00')
|
||||
expect(format('CAD')(amount)).to.equal('$1.00')
|
||||
expect(format('CHF')(amount)).to.equal('CHF 1.00')
|
||||
expect(format('CNY')(amount)).to.equal('¥1.00')
|
||||
expect(format('COP')(amount)).to.equal('$1.00')
|
||||
expect(format('DKK')(amount)).to.equal('kr 1.00')
|
||||
expect(format('EUR')(amount)).to.equal('€1.00')
|
||||
expect(format('GBP')(amount)).to.equal('£1.00')
|
||||
expect(format('HUF')(amount)).to.equal('Ft 1.00')
|
||||
expect(format('IDR')(amount)).to.equal('Rp 1.00')
|
||||
expect(format('INR')(amount)).to.equal('₹1.00')
|
||||
expect(format('MXN')(amount)).to.equal('$1.00')
|
||||
expect(format('MYR')(amount)).to.equal('RM 1.00')
|
||||
expect(format('NOK')(amount)).to.equal('kr 1.00')
|
||||
expect(format('NZD')(amount)).to.equal('$1.00')
|
||||
expect(format('PEN')(amount)).to.equal('PEN 1.00')
|
||||
expect(format('PHP')(amount)).to.equal('₱1.00')
|
||||
expect(format('SEK')(amount)).to.equal('kr 1.00')
|
||||
expect(format('SGD')(amount)).to.equal('$1.00')
|
||||
expect(format('THB')(amount)).to.equal('฿1.00')
|
||||
expect(format('USD')(amount)).to.equal('$1.00')
|
||||
})
|
||||
|
||||
it('should format 1_234_567.897_654 "minimal atomic units"', function () {
|
||||
const amount = 1_234_567.897_654
|
||||
|
||||
// "no cents currencies"
|
||||
expect(format('CLP')(amount)).to.equal('$1,234,568')
|
||||
expect(format('JPY')(amount)).to.equal('¥1,234,568')
|
||||
expect(format('KRW')(amount)).to.equal('₩1,234,568')
|
||||
expect(format('VND')(amount)).to.equal('₫1,234,568')
|
||||
|
||||
// other currencies
|
||||
expect(format('AUD')(amount)).to.equal('$1,234,567.90')
|
||||
expect(format('BRL')(amount)).to.equal('R$1,234,567.90')
|
||||
expect(format('CAD')(amount)).to.equal('$1,234,567.90')
|
||||
expect(format('CHF')(amount)).to.equal('CHF 1,234,567.90')
|
||||
expect(format('CNY')(amount)).to.equal('¥1,234,567.90')
|
||||
expect(format('COP')(amount)).to.equal('$1,234,567.90')
|
||||
expect(format('DKK')(amount)).to.equal('kr 1,234,567.90')
|
||||
expect(format('EUR')(amount)).to.equal('€1,234,567.90')
|
||||
expect(format('GBP')(amount)).to.equal('£1,234,567.90')
|
||||
expect(format('HUF')(amount)).to.equal('Ft 1,234,567.90')
|
||||
expect(format('IDR')(amount)).to.equal('Rp 1,234,567.90')
|
||||
expect(format('INR')(amount)).to.equal('₹1,234,567.90')
|
||||
expect(format('MXN')(amount)).to.equal('$1,234,567.90')
|
||||
expect(format('MYR')(amount)).to.equal('RM 1,234,567.90')
|
||||
expect(format('NOK')(amount)).to.equal('kr 1,234,567.90')
|
||||
expect(format('NZD')(amount)).to.equal('$1,234,567.90')
|
||||
expect(format('PEN')(amount)).to.equal('PEN 1,234,567.90')
|
||||
expect(format('PHP')(amount)).to.equal('₱1,234,567.90')
|
||||
expect(format('SEK')(amount)).to.equal('kr 1,234,567.90')
|
||||
expect(format('SGD')(amount)).to.equal('$1,234,567.90')
|
||||
expect(format('THB')(amount)).to.equal('฿1,234,567.90')
|
||||
expect(format('USD')(amount)).to.equal('$1,234,567.90')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fr', function () {
|
||||
const format = currency => priceInCents =>
|
||||
formatCurrency(priceInCents, currency, 'fr')
|
||||
|
||||
describe('USD', function () {
|
||||
const formatUSD = format('USD')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatUSD(0)).to.equal('0,00 $')
|
||||
expect(formatUSD(12.34)).to.equal('12,34 $')
|
||||
expect(formatUSD(123)).to.equal('123,00 $')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatUSD(1_000)).to.equal('1 000,00 $')
|
||||
expect(formatUSD(98_765_432.1)).to.equal('98 765 432,10 $')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatUSD(-0.01)).to.equal('-0,01 $')
|
||||
expect(formatUSD(-12.34)).to.equal('-12,34 $')
|
||||
expect(formatUSD(-123)).to.equal('-123,00 $')
|
||||
})
|
||||
})
|
||||
|
||||
describe('EUR', function () {
|
||||
const formatEUR = format('EUR')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatEUR(0)).to.equal('0,00 €')
|
||||
expect(formatEUR(12.34)).to.equal('12,34 €')
|
||||
expect(formatEUR(123)).to.equal('123,00 €')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatEUR(1_000)).to.equal('1 000,00 €')
|
||||
expect(formatEUR(98_765_432.1)).to.equal('98 765 432,10 €')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatEUR(-0.01)).to.equal('-0,01 €')
|
||||
expect(formatEUR(-12.34)).to.equal('-12,34 €')
|
||||
expect(formatEUR(-123)).to.equal('-123,00 €')
|
||||
})
|
||||
})
|
||||
|
||||
describe('HUF', function () {
|
||||
const formatHUF = format('HUF')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatHUF(0)).to.equal('0,00 Ft')
|
||||
expect(formatHUF(12.34)).to.equal('12,34 Ft')
|
||||
expect(formatHUF(123)).to.equal('123,00 Ft')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatHUF(1_000)).to.equal('1 000,00 Ft')
|
||||
expect(formatHUF(98_765_432.1)).to.equal('98 765 432,10 Ft')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatHUF(-0.01)).to.equal('-0,01 Ft')
|
||||
expect(formatHUF(-12.34)).to.equal('-12,34 Ft')
|
||||
expect(formatHUF(-123)).to.equal('-123,00 Ft')
|
||||
})
|
||||
})
|
||||
|
||||
describe('CLP', function () {
|
||||
const formatCLP = format('CLP')
|
||||
|
||||
it('should format basic amounts', function () {
|
||||
expect(formatCLP(0)).to.equal('0 $')
|
||||
expect(formatCLP(12.34)).to.equal('12 $')
|
||||
expect(formatCLP(123)).to.equal('123 $')
|
||||
expect(formatCLP(1234)).to.equal('1 234 $')
|
||||
})
|
||||
|
||||
it('should format thousand separators', function () {
|
||||
expect(formatCLP(100_000)).to.equal('100 000 $')
|
||||
expect(formatCLP(9_876_543_210)).to.equal('9 876 543 210 $')
|
||||
})
|
||||
|
||||
it('should format negative amounts', function () {
|
||||
expect(formatCLP(-1)).to.equal('-1 $')
|
||||
expect(formatCLP(-12.34)).to.equal('-12 $')
|
||||
expect(formatCLP(-1234)).to.equal('-1 234 $')
|
||||
})
|
||||
})
|
||||
|
||||
describe('all currencies', function () {
|
||||
it('should format 1 "minimal atomic units"', function () {
|
||||
const amount = 1
|
||||
|
||||
// "no cents currencies"
|
||||
expect(format('CLP')(amount)).to.equal('1 $')
|
||||
expect(format('JPY')(amount)).to.equal('1 ¥')
|
||||
expect(format('KRW')(amount)).to.equal('1 ₩')
|
||||
expect(format('VND')(amount)).to.equal('1 ₫')
|
||||
|
||||
// other currencies
|
||||
expect(format('AUD')(amount)).to.equal('1,00 $')
|
||||
expect(format('BRL')(amount)).to.equal('1,00 R$')
|
||||
expect(format('CAD')(amount)).to.equal('1,00 $')
|
||||
expect(format('CHF')(amount)).to.equal('1,00 CHF')
|
||||
expect(format('CNY')(amount)).to.equal('1,00 ¥')
|
||||
expect(format('COP')(amount)).to.equal('1,00 $')
|
||||
|
||||
expect(format('EUR')(amount)).to.equal('1,00 €')
|
||||
expect(format('GBP')(amount)).to.equal('1,00 £')
|
||||
expect(format('USD')(amount)).to.equal('1,00 $')
|
||||
})
|
||||
|
||||
it('should format 1_234_567.897_654 "minimal atomic units"', function () {
|
||||
const amount = 1_234_567.897_654
|
||||
|
||||
// "no cents currencies"
|
||||
expect(format('CLP')(amount)).to.equal('1 234 568 $')
|
||||
expect(format('JPY')(amount)).to.equal('1 234 568 ¥')
|
||||
expect(format('KRW')(amount)).to.equal('1 234 568 ₩')
|
||||
expect(format('VND')(amount)).to.equal('1 234 568 ₫')
|
||||
|
||||
// other currencies
|
||||
expect(format('AUD')(amount)).to.equal('1 234 567,90 $')
|
||||
expect(format('BRL')(amount)).to.equal('1 234 567,90 R$')
|
||||
expect(format('CAD')(amount)).to.equal('1 234 567,90 $')
|
||||
expect(format('CHF')(amount)).to.equal('1 234 567,90 CHF')
|
||||
expect(format('CNY')(amount)).to.equal('1 234 567,90 ¥')
|
||||
expect(format('COP')(amount)).to.equal('1 234 567,90 $')
|
||||
|
||||
expect(format('EUR')(amount)).to.equal('1 234 567,90 €')
|
||||
expect(format('GBP')(amount)).to.equal('1 234 567,90 £')
|
||||
expect(format('USD')(amount)).to.equal('1 234 567,90 $')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
46
services/web/test/frontend/shared/utils/email.test.tsx
Normal file
46
services/web/test/frontend/shared/utils/email.test.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { expect } from 'chai'
|
||||
import { isValidEmail } from '../../../../frontend/js/shared/utils/email'
|
||||
|
||||
const validEmailAddresses = [
|
||||
'email@example.com',
|
||||
'firstname.lastname@example.com',
|
||||
'firstname-lastname@example.com',
|
||||
'email@subdomain.example.com',
|
||||
'firstname+lastname@example.com',
|
||||
'1234567890@example.com',
|
||||
'email@example-one.com',
|
||||
'_@example.com',
|
||||
'email@example.name',
|
||||
'email@example.co.jp',
|
||||
]
|
||||
|
||||
const invalidEmailAddresses = [
|
||||
'plaintext',
|
||||
'#@%^%#$@#$@#.com',
|
||||
'@example.com',
|
||||
'email.example.com',
|
||||
'.email@example.com',
|
||||
'email.@example.com',
|
||||
'email..email@example.com',
|
||||
'email@example.com (Joe Smith)',
|
||||
'email@example',
|
||||
'email@111.222.333.44444',
|
||||
'email@example..com',
|
||||
]
|
||||
|
||||
describe('isValidEmail', function () {
|
||||
it('should return true for valid email addresses', function () {
|
||||
validEmailAddresses.forEach(email =>
|
||||
expect(isValidEmail(email)).to.equal(true, email + ' should be valid ')
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false for invalid email addresses', function () {
|
||||
invalidEmailAddresses.forEach(email =>
|
||||
expect(isValidEmail(email)).to.equal(
|
||||
false,
|
||||
email + ' should not be valid '
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
19
services/web/test/frontend/shared/utils/md5.test.js
Normal file
19
services/web/test/frontend/shared/utils/md5.test.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { generateMD5Hash } from '@/shared/utils/md5'
|
||||
|
||||
describe('md5', function () {
|
||||
describe('generateSHA1Hash', function () {
|
||||
it('sample string 1', function () {
|
||||
expect(generateMD5Hash('sample string 1')).to.equal(
|
||||
'b7988250a49c21459260b41d2b435dae'
|
||||
)
|
||||
})
|
||||
|
||||
it('sample string 2', function () {
|
||||
expect(generateMD5Hash('sample string 2')).to.equal(
|
||||
'371b9c84c640a9e121523156aeae4958'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
79
services/web/test/frontend/shared/utils/sha1.test.js
Normal file
79
services/web/test/frontend/shared/utils/sha1.test.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { expect } from 'chai'
|
||||
import { generateSHA1Hash } from '../../../../frontend/js/shared/utils/sha1'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
function generateSHA1HashNode(content) {
|
||||
return crypto.createHash('sha1').update(content, 'utf8').digest('hex')
|
||||
}
|
||||
|
||||
function generateRandomUnicodeString(length) {
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
const range = [0x0000, 0x10ffff]
|
||||
const codePoint =
|
||||
Math.floor(Math.random() * (range[1] - range[0] + 1)) + range[0]
|
||||
result += String.fromCharCode(codePoint)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function generateMultipleRandomStrings(numStrings, maxLength) {
|
||||
const randomStrings = []
|
||||
for (let i = 0; i < numStrings; i++) {
|
||||
const length = Math.floor(Math.random() * maxLength)
|
||||
randomStrings.push(generateRandomUnicodeString(length))
|
||||
}
|
||||
return [...new Set(randomStrings)]
|
||||
}
|
||||
|
||||
describe('sha1', function () {
|
||||
describe('generateSHA1Hash', function () {
|
||||
const randomStrings = generateMultipleRandomStrings(100, 1000)
|
||||
|
||||
it('verify 100 random strings', function () {
|
||||
for (const str of randomStrings) {
|
||||
expect(generateSHA1Hash(str)).to.equal(generateSHA1HashNode(str))
|
||||
}
|
||||
})
|
||||
|
||||
it('sample string 1', function () {
|
||||
expect(generateSHA1Hash('sample string 1')).to.equal(
|
||||
'135028161629af5901ea2f15554730dc0de38a01'
|
||||
)
|
||||
})
|
||||
|
||||
it('sample string 2', function () {
|
||||
expect(generateSHA1Hash('sample string 2')).to.equal(
|
||||
'db9460374e49a7c737b609c2fb37302381f345d6'
|
||||
)
|
||||
})
|
||||
|
||||
it('abc', function () {
|
||||
expect(generateSHA1Hash('abc')).to.equal(
|
||||
'a9993e364706816aba3e25717850c26c9cd0d89d'
|
||||
)
|
||||
})
|
||||
|
||||
it('generates a sha1 for an empty string', function () {
|
||||
expect(generateSHA1Hash('')).to.equal(
|
||||
'da39a3ee5e6b4b0d3255bfef95601890afd80709'
|
||||
)
|
||||
})
|
||||
|
||||
it('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq', function () {
|
||||
expect(
|
||||
generateSHA1Hash(
|
||||
'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'
|
||||
)
|
||||
).to.equal('84983e441c3bd26ebaae4aa1f95129e5e54670f1')
|
||||
})
|
||||
|
||||
it('abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu', function () {
|
||||
expect(
|
||||
generateSHA1Hash(
|
||||
'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu'
|
||||
)
|
||||
).to.equal('a49b2446a02c645bf419f995b67091253a04a259')
|
||||
})
|
||||
})
|
||||
})
|
45
services/web/test/frontend/shared/utils/url-helper.test.js
Normal file
45
services/web/test/frontend/shared/utils/url-helper.test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { expect } from 'chai'
|
||||
import { cleanURL } from '@/shared/utils/url-helper'
|
||||
|
||||
describe('url-helper', function () {
|
||||
describe('cleanURL', function () {
|
||||
describe('without mode', function () {
|
||||
it('removes trailing slash', function () {
|
||||
const url = new URL('https://www.ovelreaf.com/project/1abc/')
|
||||
expect(cleanURL(url).href).to.equal(
|
||||
'https://www.ovelreaf.com/project/1abc'
|
||||
)
|
||||
})
|
||||
|
||||
it('clears the mode from the detached URL', function () {
|
||||
const url = new URL('https://www.ovelreaf.com/project/2abc/detached')
|
||||
expect(cleanURL(url).href).to.equal(
|
||||
'https://www.ovelreaf.com/project/2abc'
|
||||
)
|
||||
})
|
||||
|
||||
it('clears the mode from the detacher URL', function () {
|
||||
const url = new URL('https://www.ovelreaf.com/project/2abc/detacher/')
|
||||
expect(cleanURL(url).href).to.equal(
|
||||
'https://www.ovelreaf.com/project/2abc'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with mode', function () {
|
||||
it('handles with trailing slash', function () {
|
||||
const url = new URL('https://www.ovelreaf.com/project/3abc/')
|
||||
expect(cleanURL(url, 'detacher').href).to.equal(
|
||||
'https://www.ovelreaf.com/project/3abc/detacher'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles without trailing slash', function () {
|
||||
const url = new URL('https://www.ovelreaf.com/project/4abc')
|
||||
expect(cleanURL(url, 'detached').href).to.equal(
|
||||
'https://www.ovelreaf.com/project/4abc/detached'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user