first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import BetaBadge from '../../../../frontend/js/shared/components/beta-badge'
describe('beta badge', function () {
it('renders the url and tooltip text', function () {
cy.mount(
<BetaBadge
link={{ href: '/foo' }}
tooltip={{
id: 'test-tooltip',
text: 'This is a test',
}}
/>
)
cy.get('a[href="/foo"]').contains('This is a test')
})
})

View File

@@ -0,0 +1,50 @@
import React from 'react'
import LanguagePicker from '../../../../frontend/js/features/ui/components/bootstrap-5/language-picker'
import getMeta from '@/utils/meta'
import exposedSettings from '../../../../modules/admin-panel/test/frontend/js/features/user/data/exposedSettings'
describe('LanguagePicker', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-i18n', {
currentLangCode: 'en',
})
window.metaAttributesCache.set('ol-footer', {
showThinFooter: false,
translatedLanguages: {
en: 'English',
fr: 'Français',
es: 'Español',
},
subdomainLang: {
en: { lngCode: 'en', url: 'overleaf.com' },
fr: { lngCode: 'fr', url: 'fr.overleaf.com' },
es: { lngCode: 'es', url: 'es.overleaf.com' },
},
})
Object.assign(getMeta('ol-ExposedSettings'), exposedSettings)
})
it('renders the language picker with the current language', function () {
cy.mount(<LanguagePicker showHeader />)
cy.get('#language-picker-toggle').should('contain', 'English')
})
it('opens the dropdown and lists available languages', function () {
cy.mount(<LanguagePicker showHeader />)
cy.get('#language-picker-toggle').click()
cy.get('.dropdown-menu').within(() => {
cy.contains('English').should('exist')
cy.contains('Français').should('exist')
cy.contains('Español').should('exist')
})
})
it('changes the language and updates the URL when a language is selected', function () {
cy.mount(<LanguagePicker showHeader />)
cy.get('#language-picker-toggle').should('exist').click()
cy.contains('Français').click()
cy.url().should('include', 'fr.overleaf.com')
})
})

View File

@@ -0,0 +1,30 @@
import MaterialIcon from '@/shared/components/material-icon'
import unfilledIconTypes from '../../../../frontend/fonts/material-symbols/unfilled-symbols.mjs'
const FONT_SIZE = 40
describe('MaterialIcon', function () {
describe('Filled', function () {
it('contains symbols', function () {
cy.mount(<MaterialIcon type="home" style={{ fontSize: FONT_SIZE }} />)
cy.get('.material-symbols').as('icon')
cy.get('@icon')
.invoke('width')
.should('be.within', FONT_SIZE - 1, FONT_SIZE + 1)
})
})
describe('Unfilled', function () {
it('Contain all unfilled symbol', function () {
for (const type of unfilledIconTypes) {
cy.mount(
<MaterialIcon type={type} unfilled style={{ fontSize: FONT_SIZE }} />
)
cy.get('.material-symbols').as('icon')
cy.get('@icon')
.invoke('width')
.should('be.within', FONT_SIZE - 1, FONT_SIZE + 1)
}
})
})
})

View File

@@ -0,0 +1,345 @@
import { useCallback, FormEvent } from 'react'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLForm from '@/features/ui/components/ol/ol-form'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import {
Select,
SelectProps,
} from '../../../../frontend/js/shared/components/select'
const testData = [1, 2, 3].map(index => ({
key: index,
value: `Demo item ${index}`,
sub: `Subtitle ${index}`,
}))
type RenderProps = Partial<SelectProps<(typeof testData)[number]>> & {
onSubmit?: (formData: object) => void
}
function render(props: RenderProps) {
const submitHandler = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (props.onSubmit) {
const formData = new FormData(event.target as HTMLFormElement)
// a plain object is more convenient to work later with assertions
props.onSubmit(Object.fromEntries(formData.entries()))
}
}
cy.mount(
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<form onSubmit={submitHandler}>
<Select
items={testData}
itemToString={x => String(x?.value)}
label={props.label}
name="select_control"
defaultText={props.defaultText}
defaultItem={props.defaultItem}
itemToSubtitle={props.itemToSubtitle}
itemToKey={x => String(x.key)}
onSelectedItemChanged={props.onSelectedItemChanged}
selected={props.selected}
disabled={props.disabled}
itemToDisabled={props.itemToDisabled}
optionalLabel={props.optionalLabel}
loading={props.loading}
selectedIcon={props.selectedIcon}
/>
<button type="submit">submit</button>
</form>
</div>
)
}
describe('<Select />', function () {
describe('initial rendering', function () {
it('renders default text', function () {
render({ defaultText: 'Choose an item' })
cy.findByTestId('spinner').should('not.exist')
cy.findByRole('textbox', { name: 'Choose an item' })
})
it('renders default item', function () {
render({ defaultItem: testData[2] })
cy.findByRole('textbox', { name: 'Demo item 3' })
})
it('default item takes precedence over default text', function () {
render({ defaultText: 'Choose an item', defaultItem: testData[2] })
cy.findByRole('textbox', { name: 'Demo item 3' })
})
it('renders label', function () {
render({
defaultText: 'Choose an item',
label: 'test label',
optionalLabel: false,
})
cy.findByRole('textbox', { name: 'test label' })
cy.findByRole('textbox', { name: '(Optional)' }).should('not.exist')
})
it('renders optional label', function () {
render({
defaultText: 'Choose an item',
label: 'test label',
optionalLabel: true,
})
cy.findByRole('textbox', { name: 'test label (Optional)' })
})
it('renders a spinner while loading when there is a label', function () {
render({
defaultText: 'Choose an item',
label: 'test label',
loading: true,
})
cy.findByTestId('spinner')
})
it('does not render a spinner while loading if there is no label', function () {
render({
defaultText: 'Choose an item',
loading: true,
})
cy.findByTestId('spinner').should('not.exist')
})
})
describe('items rendering', function () {
it('renders all items', function () {
render({ defaultText: 'Choose an item' })
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 1' })
cy.findByRole('option', { name: 'Demo item 2' })
cy.findByRole('option', { name: 'Demo item 3' })
})
it('renders subtitles', function () {
render({
defaultText: 'Choose an item',
itemToSubtitle: x => String(x?.sub),
})
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 1 Subtitle 1' })
cy.findByRole('option', { name: 'Demo item 2 Subtitle 2' })
cy.findByRole('option', { name: 'Demo item 3 Subtitle 3' })
})
})
describe('item selection', function () {
it('cannot select an item when disabled', function () {
render({ defaultText: 'Choose an item', disabled: true })
cy.findByRole('textbox', { name: 'Choose an item' }).click({
force: true,
})
cy.findByRole('option', { name: 'Demo item 1' }).should('not.exist')
cy.findByRole('option', { name: 'Demo item 2' }).should('not.exist')
cy.findByRole('option', { name: 'Demo item 3' }).should('not.exist')
cy.findByRole('textbox', { name: 'Choose an item' })
})
it('renders only the selected item after selection', function () {
render({ defaultText: 'Choose an item' })
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 1' })
cy.findByRole('option', { name: 'Demo item 2' })
cy.findByRole('option', { name: 'Demo item 3' }).click()
cy.findByRole('textbox', { name: 'Choose an item' }).should('not.exist')
cy.findByRole('option', { name: 'Demo item 1' }).should('not.exist')
cy.findByRole('option', { name: 'Demo item 2' }).should('not.exist')
cy.findByRole('textbox', { name: 'Demo item 3' })
})
it('invokes callback after selection', function () {
const selectionHandler = cy.stub().as('selectionHandler')
render({
defaultText: 'Choose an item',
onSelectedItemChanged: selectionHandler,
})
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 2' }).click()
cy.get('@selectionHandler').should(
'have.been.calledOnceWith',
testData[1]
)
})
})
describe('when the form is submitted', function () {
it('populates FormData with the default selected item', function () {
const submitHandler = cy.stub().as('submitHandler')
render({ defaultItem: testData[1], onSubmit: submitHandler })
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {
select_control: 'Demo item 2',
})
})
it('populates FormData with the selected item', function () {
const submitHandler = cy.stub().as('submitHandler')
render({ defaultItem: testData[1], onSubmit: submitHandler })
cy.findByRole('textbox', { name: 'Demo item 2' }).click() // open dropdown
cy.findByText('Demo item 3').click() // choose a different item
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {
select_control: 'Demo item 3',
})
})
it('does not populate FormData when no item is selected', function () {
const submitHandler = cy.stub().as('submitHandler')
render({ defaultText: 'Choose an item', onSubmit: submitHandler })
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {})
})
})
describe('with react-bootstrap forms', function () {
type FormWithSelectProps = {
onSubmit: (formData: object) => void
}
const FormWithSelect = ({ onSubmit }: FormWithSelectProps) => {
const selectComponent = useCallback(
() => (
<Select
name="select_control"
items={testData}
defaultItem={testData[0]}
itemToString={x => String(x?.value)}
itemToKey={x => String(x.key)}
/>
),
[]
)
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
// a plain object is more convenient to work later with assertions
onSubmit(Object.fromEntries(formData.entries()))
}
return (
<OLForm onSubmit={handleSubmit}>
<OLFormControl as={selectComponent} />
<OLButton type="submit">submit</OLButton>
</OLForm>
)
}
it('populates FormData with the selected item when the form is submitted', function () {
const submitHandler = cy.stub().as('submitHandler')
cy.mount(
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<FormWithSelect onSubmit={submitHandler} />
</div>
)
cy.findByRole('textbox', { name: 'Demo item 1' }).click() // open dropdown
cy.findByRole('option', { name: 'Demo item 3' }).click() // choose a different item
cy.findByText('submit').click()
cy.get('@submitHandler').should('have.been.calledOnceWith', {
select_control: 'Demo item 3',
})
})
})
describe('keyboard navigation', function () {
it('can select an item using the keyboard', function () {
render({ defaultText: 'Choose an item' })
cy.findByRole('textbox', { name: 'Choose an item' }).type(
'{Enter}{downArrow}{Enter}',
{ force: true }
)
cy.findByRole('textbox', { name: 'Demo item 1' }).should('exist')
cy.findByRole('option', { name: 'Demo item 2' }).should('not.exist')
})
})
describe('selectedIcon', function () {
it('renders a selected icon if the prop is set', function () {
render({
defaultText: 'Choose an item',
selectedIcon: true,
})
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 1' }).click()
cy.findByRole('textbox', { name: 'Demo item 1' }).click()
cy.findByText('check').should('exist')
})
it('renders no selected icon if the prop is not set', function () {
render({
defaultText: 'Choose an item',
selectedIcon: false,
})
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 1' }).click()
cy.findByRole('textbox', { name: 'Demo item 1' }).click()
cy.findByText('check').should('not.exist')
})
})
describe('itemToDisabled', function () {
it('prevents selecting a disabled item', function () {
render({
defaultText: 'Choose an item',
itemToDisabled: x => x?.key === 2,
})
cy.findByRole('textbox', { name: 'Choose an item' }).click()
cy.findByRole('option', { name: 'Demo item 2' }).click({ force: true })
// still showing other list items
cy.findByRole('option', { name: 'Demo item 3' }).should('exist')
cy.findByRole('option', { name: 'Demo item 1' }).click()
// clicking an enabled item dismisses the list
cy.findByRole('option', { name: 'Demo item 3' }).should('not.exist')
})
})
describe('selected', function () {
it('shows the item provided in the selected prop', function () {
render({
defaultText: 'Choose an item',
selected: testData[1],
})
cy.findByRole('textbox', { name: 'Demo item 2' }).should('exist')
})
it('should show default text when selected is null', function () {
render({
selected: null,
defaultText: 'Choose an item',
})
cy.findByRole('textbox', { name: 'Choose an item' }).should('exist')
})
})
})

View File

@@ -0,0 +1,228 @@
import SplitTestBadge from '../../../../frontend/js/shared/components/split-test-badge'
import { EditorProviders } from '../../helpers/editor-providers'
describe('split test badge', function () {
it('renders an alpha badge with the url and tooltip text', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'active',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'alpha',
badgeInfo: {
url: '/alpha/participate',
tooltipText: 'This is an alpha feature',
},
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.findByRole('link', { name: /this is an alpha feature/i })
.should('have.attr', 'href', '/alpha/participate')
.find('.badge')
.contains('α')
})
it('does not render the alpha badge when user is not assigned to the variant', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'default',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'alpha',
badgeInfo: {
url: '/alpha/participate',
tooltipText: 'This is an alpha feature',
},
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.get('.badge').should('not.exist')
})
it('renders a beta badge with the url and tooltip text', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'active',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'beta',
badgeInfo: {
url: '/beta/participate',
tooltipText: 'This is a beta feature',
},
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.findByRole('link', { name: /this is a beta feature/i })
.should('have.attr', 'href', '/beta/participate')
.find('.badge')
.contains('β')
})
it('does not render the beta badge when user is not assigned to the variant', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'default',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'beta',
badgeInfo: {
url: '/beta/participate',
tooltipText: 'This is a beta feature',
},
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.get('.badge').should('not.exist')
})
it('renders an info badge with the url and tooltip text', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'active',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'release',
badgeInfo: {
url: '/feedback/form',
tooltipText: 'This is a new feature',
},
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.findByRole('link', { name: /this is a new feature/i })
.should('have.attr', 'href', '/feedback/form')
.find('.info-badge')
})
it('does not render the info badge when user is not assigned to the variant', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'default',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'release',
badgeInfo: {
url: '/feedback/form',
tooltipText: 'This is a new feature',
},
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.get('.badge').should('not.exist')
})
it('does not render the badge when no split test info is available', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'active',
})
win.metaAttributesCache.set('ol-splitTestInfo', {})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.get('.badge').should('not.exist')
})
it('default badge url and text are used when not provided', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'cypress-test': 'active',
})
win.metaAttributesCache.set('ol-splitTestInfo', {
'cypress-test': {
phase: 'release',
},
})
})
cy.mount(
<EditorProviders>
<SplitTestBadge
splitTestName="cypress-test"
displayOnVariants={['active']}
/>
</EditorProviders>
)
cy.findByRole('link', {
name: /we are testing this new feature.*click to give feedback/i,
})
.should('have.attr', 'href', '/beta/participate')
.find('.info-badge')
})
})

View File

@@ -0,0 +1,95 @@
import StartFreeTrialButton from '../../../../frontend/js/shared/components/start-free-trial-button'
import getMeta from '@/utils/meta'
describe('start free trial button', function () {
beforeEach(function () {
cy.intercept('POST', '/event/paywall-prompt', {
statusCode: 204,
}).as('event-paywall-prompt')
cy.intercept('POST', '/event/paywall-click', {
statusCode: 204,
}).as('event-paywall-click')
getMeta('ol-ExposedSettings').isOverleaf = true
})
it('renders the button with default text', function () {
cy.mount(<StartFreeTrialButton source="cypress-test" />)
cy.wait('@event-paywall-prompt')
.its('request.body.paywall-type')
.should('eq', 'cypress-test')
cy.get('button').contains('Start Free Trial!')
})
it('renders the button with custom text', function () {
cy.mount(
<StartFreeTrialButton source="cypress-test">
Some Custom Text
</StartFreeTrialButton>
)
cy.wait('@event-paywall-prompt')
.its('request.body.paywall-type')
.should('eq', 'cypress-test')
cy.get('button').contains('Some Custom Text')
})
it('renders the button with styled button', function () {
cy.mount(
<StartFreeTrialButton
source="cypress-test"
buttonProps={{
variant: 'danger',
size: 'lg',
}}
/>
)
cy.wait('@event-paywall-prompt')
cy.get('button.btn.btn-danger.btn-lg').contains('Start Free Trial!')
})
it('renders the button with custom class', function () {
cy.mount(
<StartFreeTrialButton
source="cypress-test"
buttonProps={{ className: 'ct-test-class' }}
/>
)
cy.wait('@event-paywall-prompt')
.its('request.body.paywall-type')
.should('eq', 'cypress-test')
cy.get('.ct-test-class').contains('Start Free Trial!')
})
it('calls onClick callback and opens a new tab to the subscription page on click', function () {
const onClickStub = cy.stub()
cy.mount(
<StartFreeTrialButton source="cypress-test" handleClick={onClickStub} />
)
cy.wait('@event-paywall-prompt')
cy.window().then(win => {
cy.stub(win, 'open').as('Open')
})
cy.get('button.btn').contains('Start Free Trial!').click()
cy.wrap(null).then(() => {
cy.wait('@event-paywall-click')
.its('request.body.paywall-type')
.should('eq', 'cypress-test')
cy.get('@Open').should(
'have.been.calledOnceWithExactly',
'/user/subscription/choose-your-plan?itm_campaign=cypress-test'
)
expect(onClickStub).to.be.called
})
})
})

View File

@@ -0,0 +1,35 @@
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
describe('<OLTooltip />', function () {
it('calls the bound handler and blur then hides text on click', function () {
const clickHandler = cy.stub().as('clickHandler')
const blurHandler = cy.stub().as('blurHandler')
const description = 'foo'
const btnText = 'Click me!'
cy.mount(
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<OLTooltip id="abc" description={description}>
<button onClick={clickHandler} onBlur={blurHandler}>
{btnText}
</button>
</OLTooltip>
</div>
)
cy.findByRole('button', { name: btnText }).as('button')
cy.get('@button').trigger('mouseover')
cy.findByText(description)
cy.get('@button').click()
cy.get('@clickHandler').should('have.been.calledOnce')
cy.get('@blurHandler').should('have.been.calledOnce')
cy.findByText(description).should('not.exist')
})
})